Одно из самых захватывающих изменений в Dart в Dart 2 - это священная комбинация удаления необязательных типов и добавления мощного вывода типов.

Первое означает, что больше нет различий между режимами «проверено» (тип аннотаций относится к типу) и «unchecked» (аннотации типов вообще не имеют значения). Последнее означает, что для уже хорошо типизированных приложений вам не придется много делать!

Дротик 1

Давайте сначала рассмотрим, как аннотации типов работали в Dart 1.0:

void main() {
  String name = 5;
  print(name + 2); // 7
}

Это допустимо как синтаксически, так и семантически, в Dart 1. Dart 1 также поставляется с режимом разработки, обычно используемым в Dartium или в команде -line VM (через --checked), где это будет помечено (во время выполнения) как ошибка утверждения - но в производственных приложениях (без проверено) это приложение будет работать полностью нормально .

Конечно, вполне вероятно, что это не будет работать нормально. Давайте посмотрим на этот пример:

void main() 
  String name = 5;
  print(name.substring(1)); // ERROR: int does not have substring.
}

Опять же, действительный Dart - но, как и другие динамические языки (JavaScript, Python, Ruby), нам пришлось бы подождать, пока среда выполнения не обнаружит, что name было введено с ошибкой, или run в режиме проверено.

Еще не запутались? Ну, есть / был также статический код анализатор Dart, отдельный от языка или виртуальной машины, который мог бы попытаться предупредить вас о потенциально недопустимом коде:

Бывают случаи, когда даже режим отмечен, иначе анализатор не может помочь, особенно с универсальными типами. Dart 1 действительно поддерживал универсальные типы - и даже поддерживал овеществленные универсальные типы, но:

  • Только по class определениям.
  • Параметры типа нужно было передавать явно.

Давайте рассмотрим. Вот пример использования универсальных типов в Dart 1:

Как и для неуниверсальных типов, анализатор и режим checked могут обнаруживать некоторые ошибки (сообщаемые как предупреждения или только как информация - опять же, все это действительный Dart 1). В сочетании с практичными краткими опциями для переменных и полей, таких как var, это означало, что легко попасть в (действительное) плохое состояние.

void main() {
  var x = ['a'];
  print(x.runtimeType); // List, *not* List<String>
  List y = x;
  y.add(5);
  print(x); // ['a', 5];
}

Да, мы успешно добавили int в предполагаемый List<String>.

Дротик 2

Я не собираюсь вдаваться в дискуссию о сравнении динамических и статических языков и систем типов, в Интернете уже много всего. Вместо этого я хочу показать вам, как работает Dart 2, чтобы изменить язык с динамического и необязательно типизированного на статический с сильной / системой звуковых типов.

Вы можете часто слышать слово звук или надежность - это означает, попросту говоря, ваша программа не может перейти в недопустимое состояние - например , где вы думали, что написали что-то как List<String>, но на самом деле там было int.

Dart 2 по-прежнему довольно лаконичен, лаконичен и выразителен - поэтому он выполняет этот подвиг так же, как и большинство языков программирования высокого уровня - с комбинацией статических (во время компиляции) и динамических (во время выполнения) проверок.

В отличие от Dart 1, ошибки, создаваемые анализатором, практически идентичны ошибкам, которые вы обнаружите при запуске кода, скомпилированного в JavaScript, с Flutter или в автономной виртуальной машине. Давайте вернемся к тем же образцам, что и выше, с Dart 2 (и строгим режимом типа system / runtime).

Когда вы переводите свои проекты с Dart 1 на Dart 2, вам нужно будет искать распространенные подводные камни - ошибки времени выполнения, когда код «просто работал» раньше, и новые статические ошибки, которых вы никогда раньше не видели. Итак - вот самая распространенная ошибка времени выполнения, которую я обнаруживаю при переходе на Dart 2 - и как ее исправить!

Тип… не является подтипом типа…

Как правило, это означает, что вы притворялись, что на самом деле чего-то не было, но в Dart 1 ваш код «просто работал». Хотя можно было перейти в допустимое состояние - например, попытаться использовать методы, которые не существовали бы во время выполнения, - вы этого не сделали.

Exception: type 'Iterable' is not a subtype of type 'List<String>'

Обычно это происходило из-за того, что я написал такой код:

List<String> tastiestFlavors(List<String> iceCreamFlavors) =>
  iceCreamFlavors.where((f) => f.contains('Chocolate'));

В Dart 1, если вы никогда не вызывали метод, который существовал только на List, а не Iterable, это сработает! В Dart 2 у вас есть два варианта:

  1. Исправьте ваши типы (в этом примере вместо этого верните Iterable<String>).
  2. Создайте новый универсальный объект правильного типа (используйте .toList() здесь).
// 1.
Iterable<String> tastiestFlavors(List<String> iceCreamFlavors) =>
// 2.
iceCreamFlavors.where((f) => f.contains('Chocolate')).toList();

Большой! А что насчет этих ошибок?

// In Flutter or the command-line VM:
Exception: type 'List' is not a subtype of type 'List<String>'
// Or on the web, for example, in DDC:
Exception: type 'JSArray' is not a subtype of type 'List<String>'

Этот немного сложнее; вы можете подумать про себя, подождите, это это List, верно?

Что ж, у вас может не быть List<String>. Давайте посмотрим на этот пример:

void main() {
  var names = [];
  names.add('Matan');
  printNames(names);
}
void printNames(List<String> names) { ... }

Выше names выводится как List<dynamic>, а не как List<String>, потому что, когда он был создан (с использованием буквальной нотации списка []), не было элементов для вывода типа, и не было предоставлено никакого явного типа. Вы можете исправить это двумя способами:

  1. Используйте аргумент явного типа (var names = <String>[])
  2. Передайте элемент того типа, который вам нужен (var names = [‘Matan’])

Иногда вы не можете управлять списком (например, читать из JSON), но не волнуйтесь, у вас все еще есть 3 дополнительных параметра в Dart 2, если вы не контролируете источник коллекции:

  1. Аргументы типа удаления (например, List f = json['f'] вместо List<String>)
  2. Используйте List.from (или аналогичный для других типов), чтобы создать типобезопасную копию.
  3. Используйте <List>.cast (или аналогичный для других типов), чтобы обернуть нужным типом

Давайте посмотрим на примеры!

void decodeJson1(Map json) {
  var f = List<String>.from(json['f']);
}
void decodeJson2(Map json) {
  var f = (json['f] as List).cast<String>();
}

Выше decodeJson1 будет создан новый List<String>, содержащий элементы json['f']. Если какой-либо из элементов не String, произойдет ошибка времени выполнения.

Если вы не можете создать копию (т. Е. json - это динамический набор данных, который будет изменен), вы можете использовать .castметод, как показано в decodeJson2. Вам следует стараться избегать использования этих специальных cast методов, если только это не является абсолютно необходимым.

Как «звучат» ошибки времени выполнения?

Я часто вижу, как другие спрашивают, в том числе даже сотрудники Google, но Dart выдает ошибки времени выполнения - вместо того, чтобы иметь статические ошибки - как это звучит?. Я думаю, что существует распространенное заблуждение относительно статических типов и звуковых типов.

Статические типы на самом деле становятся все более распространенными. Например, TypeScript добавляет отличную систему статических типов поверх другого нетипизированного JavaScript. Но это не звук (источник):

Система типов TypeScript позволяет выполнять определенные операции, которые не могут быть известны во время компиляции, чтобы быть безопасными. Когда система типов имеет это свойство, она считается ненадежной. Места, где TypeScript допускает ненадлежащее поведение, были тщательно рассмотрены, и в этом документе мы объясним, где это происходит, и мотивирующие сценарии, стоящие за ними.

Простой пример - использование типа any чтобы« обмануть систему типов»:

function outputFirstCharacter(name: string) {
    console.log(name.substring(0, 1));
    //          ^^^^^^^^^^^^^^ Where error occurs in TypeScript
}
let iAmANumber = 5;
let iAmAString = (iAmANumber as any);
outputFirstCharacter(iAmAString);

Uncaught TypeError: name.substring не является функцией

Вы не заметите никаких статических ошибок. Вы рано или поздно получите (загадочную) ошибку времени выполнения, если попытаетесь использовать iAmAString в качестве String. Сравните это с Dart 2:

void outputFirstCharacter(String name) {
    print(name.substring(0, 1));
}
void main() {
  var iAmANumber = 5;
  String iAmAString = (iAmANumber as dynamic);
  //     ^^^^^^^^^^^^ Where error occurs in Dart 2.
  outputFirstCharacter(iAmAString);
}

TypeError: 5: тип JSInt не является подтипом типа 'String'.

Конечно, есть и другие, более сложные примеры. Главный вывод здесь заключается в том, что в Dart (2) все, что имеет аннотацию типа, является окончательно этим типом и не может маскироваться под несовместимый тип. Это правда, что не обязательно помогает программисту, но помогает компиляторам.

Возможна оптимизация компилятора для Dart 2

Примечание. Я не разработчик компиляторов и приношу свои извинения за любые неточности.

Использование гарантии надежности системы типов означает, что можно доверять правильности данного типа и избегать проверок во время выполнения. Например, современные виртуальные машины JavaScript будут компилироваться в что-то подобное для указанной выше программы (аннотации типов TypeScript не используются виртуальной машиной JavaScript):

// This is not real TypeScript or JavaScript, but is simply an
// example of the type of code the JavaScript VM will have to
// generate and run in order to execute the above function.
function outputFirstCharacter(name) {
  if (!('substring' in name) || !$isFunction(name['substring'])) {
    throw 'Uncaught TypeError: name.substring is not a function';
  }
  console.log(name.substring(0, 1));
}

… Тогда как среда выполнения Dart может избежать этой проверки и всегда вызывать substring.

Конечно, современные виртуальные машины JavaScript используют JIT-компиляцию для в конечном итоге оптимизации этой проверки, но это может занять время, чтобы программа достигла максимальной производительности. Это также проблема для таких областей, как развертывание iOS, где выпуск Dart ограничен в режиме с опережением времени (AOT).

В любом случае, это огромное упрощение того, как все на самом деле работает, в основном, чтобы быть понятным для людей (таких как я!), У которых нет опыта работы с компилятором.

Я надеюсь, что это небольшое введение в систему типов Dart 2 поможет вам на пути к переходу с Dart 1 на Dart 2 или к запуску Dart в первый раз!