[Из песочницы] Null safety в Dart

Привет, Хабр! Представляю вашему вниманию перевод статьи «Announcing sound null safety» автора Filip Hracek с моими комментариями:

Null safety — безопасная работа с пустыми ссылками. Далее по тексту для краткости и по причине устойчивости термина будет использоваться английское наименование null, null safety. Да и перевод «нулевая безопасность» наводит совсем на противоположные мысли.
sound — в данном контексте (sound null safety) можно перевести как «надежный».
Если есть предложения по улучшению перевода или нашли ошибки — пишите в личку, постараемся исправиться.

Наступил важный этап для команды Dart с их представлением технического превью наработок по null safety. Null safety позволяет избежать целого класса ошибок, которые часто трудно обнаружить, а в качестве бонуса обеспечивает ряд улучшений производительности. На данный момент мы выпустили предварительное техническое превью и ждем от вас обратной связи.

В этой статье мы раскроем планы команды Dart по развертыванию null safety, а также объясним, что скрывается за термином Sound null safety, и чем этот подход отличается от других языков программирования.

Описываемая версия была представлена 10-го июня 2020 года.

Зачем нужна null safety?


Dart — типобезопасный (type-safe) язык. Это означает, что когда вы получаете переменную некоторого типа, компилятор может гарантировать, что она принадлежит ему. Но безопасность типов сама по себе не гарантирует, что переменная не равна null.

Null-ошибки встречаются часто. Поиск на GitHub находит тысячи репортов (issue), вызванных нулевыми значениями в Dart-коде, и еще больше коммитов, пытающихся решить эти проблемы.

Попробуйте обнаружить проблему обнуления ссылки в следующем примере:

void printLengths(List files) {
  for (var file in files) {
    print(file.lengthSync());
  }
}


Эта функция, безусловно, завершится ошибкой, если вызывается с обнуленным параметром, но есть и второй случай, который следует рассмотреть:

void main() {
  // Error case 1: passing a null to files.
  printLengths(null);
 
  // Error case 2: passing list of files, containing a null item.
  printLengths([File('filename1'), File('filename2'), null]);
}


Null safety устраняет эту проблему:

image

С null safety вы можете с большей уверенностью опираться на свой код. Не будет больше надоедливых ошибок обращения к обнуленной переменной во время выполнения. Только статические ошибки в момент компиляции кода.

Если быть совсем честными, то текущая реализация все же оставляет несколько возможностей словить null-ошибки в момент выполнения, о них чуть позже.

Sound (надежная) null safety


Реализация null safety в языке Dart надежна (sound). Если разбирать на приведенном выше примере, то это означает, что компилятор Dart на 100% уверен, что массив файлов и элементы в нем не могут быть нулевыми. Когда компилятор Dart проанализирует ваш код и определит, что переменная не имеет значения null, то эта переменная всегда будет иметь значение: если вы проверите свой исполняемый код в отладчике, вы увидите, что возможность обнуления попросту отсутствует во время выполнения. Существуют реализации, не являющиеся «надежными», в которых все еще необходимо выполнять проверки наличия значения в момент выполнения. В отличие от прочих языков, Dart разделяет надежность реализации с языком Swift.

Немного спорное заявление, которое заставило меня копнуть в эту тему немного глубже, чтобы понять, как реализуется null safety в разных языках. В итоге я сравнил эти реализации в Swift, Kotlin и Dart. Результат этих изысканий можно посмотреть на записи нашего внутрикомандного доклада.

Подобная надежная реализация null safety в Dart имеет еще одно приятное следствие: это означает, что ваши программы могут быть меньше и быстрее. Поскольку Dart действительно уверен, что переменные никогда не могут быть обнулены, Dart может оптимизировать результат компиляции. Например, AOT-компилятор может создавать меньший и более быстрый нативный код, поскольку ему не нужно добавлять проверки на пустые ссылки.

Мы видели некоторые очень многообещающие предварительные результаты. Например, мы увидели улучшение производительности на 19% в микробенчмарке, который эмулирует типичные шаблоны рендеринга в инфраструктуре Flutter.

Основные принципы


Прежде чем приступить к детальному проектированию null safety, команда Dart определила три основных принципа:

Необнуляемость по-умолчанию. /** Часто можно увидеть в виде абревиатуры NNBD в документации **/ Если вы явно не скажете Dart«у, что переменная может быть обнулена, он сочтет ее необнуляемой. Мы выбрали это как значение по умолчанию, потому что обнаружили, что в API ненулевое значение на текущий момент является наиболее распространенным. /** Вероятно, речь идет о переработке текущего API Flutter **/.

Поэтапная применимость. Мы понимаем, что должна существовать возможность постепенного перехода к null safety шаг за шагом. На самом деле, нужно иметь обнуляемый и null safety код в одном проекте. Для этого мы планируем предоставить инструменты, помогающие с миграцией кода.

Полная надежность (sound). Как упоминалось выше, null safety в Dart надежна. Как только вы преобразуете весь свой проект и ваши зависимости на использование null safety, вы получите все преимущества надежности.

Объявление переменных с null safety


Основной синтаксис достаточно прост. Ниже приведен пример объявления различных переменных. Обратите внимание, что по умолчанию используются необнуляемые переменные, поэтому они выглядят как прежде, но их значение не может быть обнулено.

// In null-safe Dart, none of these can ever be null.
var i = 42;
final b = Foo();
String m = '';


Dart позаботится о том, чтобы вы никогда не присваивали значение null ни одной из перечисленных выше переменных. Если вы попытаетесь выполнить i = null даже тысячу строк спустя, вы получите ошибку статического анализа и красные волнистые линии — ваша программа откажется компилироваться.

Если вы хотите, чтобы ваша переменная могла обнуляться, вы можете использовать '?' вот так:

// These are all nullable variables.
int? j = 1;  // Can be null later.
final Foo? c = getFoo();  // Maybe the function returns null.
String? n;  // Is null at first. Can be null at any later time, too


Перечисленные выше переменные ведут себя точно так же, как и все переменные в актуальной версии Dart.

'?'' можно также использовать в прочих местах:

// In function parameters.
void boogie(int? count) {
  // It's possible that count is null.
}
// In function return values.
Foo? getFoo() {
  // Can return null instead of Foo.
}
// Also: generics, typedefs, type checks, etc.
// And any combination of the above.


Но, опять же, хотелось бы, чтобы вам почти никогда не придется использовать '?'. Подавляющее большинство ваших переменных будут необнуляемыми.

Упрощение использования null safety


Команда Dart изо всех сил старается сделать null safety максимально простой в использовании. Например, посмотрите на этот код, который использует if, чтобы проверить нулевое значение:

void honk(int? loudness) {
  if (loudness == null) {
    // No loudness specified, notify the developer
    // with maximum loudness.
    _playSound('error.wav', volume: 11);
    return;
  }
  // Loudness is non-null, let's just clamp it to acceptable levels.
  _playSound('honk.wav', volume: loudness.clamp(0, 11));
}


Обратите внимание, что Dart достаточно умен, чтобы понять, что к тому времени, когда мы пройдем оператор if, переменная loudness не может иметь значение null. И поэтому Dart позволяет нам вызывать метод clamp () без лишних танцев с бубном. Это удобство обеспечивается так называемым анализом потока выполнения (flow analysis): анализатор Dart просматривает ваш код, как если бы он выполнял его, автоматически выясняя дополнительную информацию о вашем коде.

Flow analysis, уже существующая фича языка Dart, используется, например, при проверке соответствия типа. В данном случае они переиспользовали ее для null safety, что позволяет смотреть на отношения типа и опционального типа как на наследование:
foo(dynamic str) {
  if (str is String) {
    //У dynamic нет метода length, но компилятор 
    //понимает из контекста что это тип String
    print(str.length);  
  }
}


Вот еще один пример, когда Dart может быть уверен, что переменная не равна null, так как мы всегда присваиваем ей значение:
int sign(int x) {
  // The result is non-nullable.
  int result;
  if (x >= 0) {
    result = 1;
  } else {
    result = -1;
  }
  // By this point, Dart knows the result cannot be null.
  return result;
}


Если вы удалите какое-либо из перечисленных выше присвоений (например, удалив строку result = -1;), Dart не сможет гарантировать, что result будет иметь значение — вы получите статическую ошибку, и ваш код не скомпилируется.

Анализ потока выполнения работает только внутри функций. Если у вас есть глобальная переменная или поле класса, то Dart не может гарантировать, что ему будет присвоено значение. Dart не может моделировать поток выполнения всего вашего приложения. По этой причине вы можете использовать новое ключевое слово late, если знаете, что переменная будет инициализирована на момент первого к ней обращения, но не можете инициализировать ее в момент объявления.

class Goo {
  late Viscosity v;
 
  Goo(Material m) {
    v = m.computeViscosity();
  }
}


Обратите внимание, что v не может быть обнулена, хотя изначально не имеет значения. Dart считает, что вы не будете пытаться прочитать v, пока ему не будет присвоено ненулевое значение, и ваш код компилируется без ошибок.

А вот и он — момент для выстрела себе в ногу.

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

Второе спорное решение — это интеграция знакомого всем Swift-программистам восклицательного знака, то есть, теперь можно будет делать force unwrap в Dart.

void main() {
  String? t;
  print(t!.length);
}


В обоих случаях (с late и »!») мы сможем при неправильном использовании получить ошибку в момент выполнения программы.

Также стоит упомянуть в контексте late еще одно нововведение, которое почему-то скрывается в статьях и видео от команды Dart. Добавлено новое ключевое слово «required» для именованных параметров конструктора. Это уже знакомый »@required», только не из отдельного пакета и без собачки.

class Temp {
  String str;
  Temp({required this.str});
  
  //ну или как альтернатива
  Temp.alt({strAtr}) : this.str = strAtr;
}


Обратная совместимость


Команда Dart работала больше года, чтобы довести null safety до уровня технического превью. Это самое большое изменение языка со времен релиза второй версии. Тем не менее, это изменение, не ломающее обратную совместимость. Существующий код может вызывать код с null safety и наоборот. Даже после полноценного релиза null safety будет дополнительной опцией, которую вы сможете использовать, когда будете готовы. Ваш существующий код продолжит работать без изменений.

На днях базовые библиотеки Dart были обновлены с использованием null safety. В качестве показательного примера обратной совместимости — замена существующих базовых библиотек прошла без единого проваленного теста и без ошибок в тестовых приложениях, работающих на тестовых средах Dart и Flutter. Даже обновление базовых библиотек для множества внутренних клиентов Google прошло без сучка и задоринки. Мы планируем переделать все наши пакеты (packages) и приложения на использование null safety после релиза, надеемся, вы поступите также. Но вы можете делать это в своем темпе, пакет за пакетом, приложение за приложением.

Эти бы слова, да разработчикам Swift в уши, особенно 3-й версии…
Но даже тут не так все радужно, разработчики сами говорят, что при совмещении null safety кода и «старого» кода в одном проекте они не могут гарантировать надежность (soundness) системы типов.

Дальнейший план действий


Мы планируем развернуть null safety постепенно в три этапа:

  1. Техническое превью. Запустилось в момент выхода оригинала статьи (10.06.2020) и доступно на dev-ветке. Стоит обратить внимание на раздел «Начни уже сейчас». Все еще очень нестабильно и может поменяться, так что пока не рекомендуем использовать ее в продакшн коде. Но мы будем рады, если вы попробуете и дадите нам обратную связь!
  2. Beta-версия. Null safety станет доступна на beta-ветке Dart и больше не будет скрываться за экспериментальным флагом. Реализация будет близка к ожидаемой финальной версии. Можно будет начать миграцию ваших пакетов и плагинов на pub.dev, но пока не рекомендуется публиковать данное изменение как стабильную версию.
  3. Стабильная версия. Null safety будет доступна для всех. Вам будет предложено опубликовать ваши обновленные пакеты и плагины как стабильные версии. На этом этапе также стоит мигрировать ваши приложения.


Если все пойдет по плану, мы выпустим стабильную версию Dart с null safety до конца года. Время от времени мы будем добавлять инструменты, которые помогут перейти на null safety. Среди них:

  • Инструмент миграции, помогающий автоматизировать многие этапы обновления существующих пакетов и приложений;
  • Теги на pub.dev, помечающие пакеты с поддержкой null safety;
  • Расширение команды 'pub outdated' с поддержкой поиска последних версий ваших зависимостей, поддерживающих null safety.


Начни уже сейчас


Самый быстрый способ попробовать null safety уже сегодня — это nullsafety.dartpad.dev — версия DartPad с включенной функцией null safety. Откройте раскрывающийся список «Learn with Snippets», чтобы найти серию обучающих упражнений, описывающих новый синтаксис и основы null safety.

image

Также можно поэкспериментировать с null safety в небольших консольных приложениях (мы еще не обновили более крупные фреймворки, такие как Flutter). Для начала нужно загрузить Dart SDK с dev-ветки, затем можно скачать этот пример консольного приложения. В README файле есть инструкции по запуску приложения с включением экспериментальной функции null safety. Другие файлы в примере предоставляют конфигурации запуска, которые позволят выполнять отладку в VS Code и Android Studio.

Также можно ознакомиться с документацией (в дальнейшем появится еще):


Мы очень рады возможности реализовать null safety в Dart. Надежная безопасная работа с пустыми ссылками станет отличительной чертой Dart, которая поможет писать наиболее надежный и производительный код. Мы надеемся, что вы найдете время поэкспериментировать с текущей версией null safety и оставите свой отзыв в нашем баг-трекере. Хорошего кода!

Спасибо за то, что дочитали до конца. Перевод немного запоздал, но, надеюсь, комментарии привнесли пользы материалу и это не стало просто переводом один в один. От себя хочется добавить, что эта фича — действительно полезный шаг вперед для всей экосистемы Flutter. Жду с нетерпением, чтобы начать использовать уже на живых приложениях. А пока, как говорится на забугорском, stay tuned!

© Habrahabr.ru