Коротко о Dart: часть вторая

В предыдущем параграфе мы начали наш короткий обзор Dart — поговорили о переменных, коллекциях и строках.

Мы сфокусируемся на функциях и null-safety — системой, которая предотвращает ошибки доступа к null на этапе компиляции.

Но в первую очередь поговорим про:

  • тернарный оператор;
  • класс enhanced enum;
  • typedef;
  • модификаторы доступа.

Давайте приступим!

Тернарный оператор

В определённых ситуациях if-else избыточен, тогда на помощь приходит тернарный оператор:

1bool isEvenFull(int value) {
2 bool result;
3
4 if (value % 2 == 0) {
5  result = true;
6 } else {
7  result = false;
8 }
9
10 return result;
11}
12
13//Может быть чуть короче
14bool isEvenShort(int value) {
15 return value % 2 == 0 ? true : false
16}

Класс еnhanced enum

В Dart есть знакомые многим enum-типы, объявить простейший enum можно так:

1enum Color {red, green, blue}

В версии языка 2.17 разработчики добавили enhanced enum.

Enhanced enum — класс со своими полями, методами и конструкторами, но с определёнными ограничениями:

  • Поля класса должны быть final, включая те, что подмешаны через with;
  • Все конструкторы должны быть константными;
  • Все factory-конструкторы могут вернуть только один из объявленных экземпляров Enum;
  • Нельзя переопределить indexhashCode или оператор равенства ==;
  • Нельзя использовать имя values для членов класса, потому что это конфликтует с геттером values из API стандартных enum;
  • В начале enum нужно объявить все возможные значения. При этом в целом значений должно быть не менее одного.

Enhanced enum может пригодиться во многих случаях.

К примеру, при работе с иконками:

1enum MyIcon {
2  close(path: 'assets/close', width: 200),
3  car(path: 'assets/car', width: 300),
4  driver(path: 'assets/driver', width: 300);
5
6  const MyIcon({
7    required this.path,
8    required this.width,
9  });
10
11  factory MyIcon.withName(String name) =>
12      values.firstWhere((v) => v.name.contains(name), orElse: () => close);
13
14  final String path;
15  final double width;
16
17  String getThemedPath(bool isDarkTheme) =>
18      isDarkTheme ? '${path}_night.webp' : '$path.webp';
19}
20

typedef

Это ключевое слово позволяет переименовать какой-то тип.

1typedef JSON = Map<String, Object?>;
2typedef Converter = String Function(int value); // Что это такое? см. Функции
3
4String convertToString(int value, Converter converter) => converter?.call(value);
5
6class MyModel {
7 final int value;
8
9 MyModel(this.value);
10
11  factory MyModel.fromJson(JSON json) => MyModel(json['value'] ?? 0);
12}
13

Модификаторы доступа

В отличие от других языков, в Dart нет модификаторов доступа: publicprivate и protected.

Есть только возможность ограничить видимость переменной, функции или класса с помощью нижнего подчеркивания _.

Оно буквально ограничивает видимость за пределами файла, в котором была объявлена переменная или функция. Но есть исключение — нотации part of/part. На них не будем долго задерживаться, но скажем, что они позволяют разбить один файл «на части» с общей видимостью, как будто это один файл.

Например, мы можем ограничить видимость полей и методов класса. Так никто не сможет получить доступ к ним за пределами файла-объявления:

1class MyClass{
2 final _privateField = "Hi, im private";
3
4 void _privateFunction(){
5  print(_privateField);
6 }
7}
8

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

MyClassвместе с main() и SecondClass объявлены внутри одного файла, поэтому приватные части MyClass видны другим членам файла или контекста.

Если же кто-то попытается из другого файла вызвать _privateFunction(), то у него ничего не выйдет.

null-safety

Dart поддерживает sound null-safety как необязательную функцию на версиях с 2.12 по 2.19 и как обязательную часть с версии 3. Это значит, что разработчик защищен от NullReferenceException, так называемой «ошибки на миллиард долларов».

Подробнее о реализации null-safety в Dart можно прочитать в документации. Мы же остановимся на основах.

С включенным null-safety в Dart все переменные не могут иметь значение null, если об этом не сказано явно с помощью оператора ?:

1String nonNullVariable = "Hello, world!";
2// При попытке присвоить null будет ошибка
3nonNullVariable = null;
4
5String? nullableVariable = "Hello, world!";
6// Теперь ничего не мешает присвоить null
7nullableVariable = null;
8

Сам компилятор постоянно следит за разработчиком и подсвечивает потенциально опасные места:

Код просто не скомпилируется, если компилятор увидит потенциально опасный вызов.

Выходов из ситуации несколько:

  1. Оператор ?.;
  2. Оператор !.;
  3. Вычисление типа компилятором.

Рассмотрим каждый их них подробнее.

Оператор ? или null-aware

В опасных местах можно пользоваться null-aware. Он буквально говорит компилятору: «Если переменная не null, то вызывай код, иначе — верни null».

1final value = myClass?.foo();
2

Компилятор ругаться перестанет, и мы получим следующее:

Тип value — int?.

myClass не null — value == 5.

myClass не присвоено или null — value == null

Оператор ! или force unwrap

Этот оператор говорит компилятору: «Я уверен, что данное значение не может быть null».

1final value = myClass!.foo();
2// Чуть длиннее. Осторожно, force unwrap и приведение это не одно и то же!
3final value = (myClass as MyClass).foo();
4

Компилятор ругаться перестанет, и мы получим следующее:

  • Тип value — int.
  • myClass присвоено не null — value == 5.
  • myClass не присвоено или null — код в рантайме выбросит NullReferenceException.

Важно: используйте force unwrap с осторожностью. Это оператор лишает нас кода null-safe, так что в итоге можно не заметить потенциальную NullReferenceException. Предпочтительнее использовать ?. и ??.

Вычисление типа компилятором

Мы можем положить nullable-значение в локальную переменную и специально проверить эту локальную переменную на null.

1final myList = <MyClass?>[MyClass(), null];
2final firstElement = myList.first; // first == myList[0]
3
4int? value;
5if(firstElement != null){
6 value = firstElement.foo();
7}
8

В итоге получим следующее:

Тип value — int? или тот, что вы указали при объявлении (например, dynamic);

Первый элемент не null — value == 5;

Первый элемент null — value == null.

Dart не видит, что первый элемент коллекции существует. Так происходит из-за full sound safety, компилятор может гарантировать вычисление только для локальных переменных.

За подробностями можно обратиться к документации или видео от разработчиков.

Бонуcные операторы

В качестве бонуса в языке Dart есть ещё несколько операторов:

?? — оператор null. Выполнит код справа-налево, только если значение левого операнда null.

1getIntegerOrZero(null); // 0
2getIntegerOrZero(5); // 5
3
4int getIntegerOrZero(int? integer) {
5 return integer?? 0;
6}

?... — оператор null-spread. Распакует коллекцию, только если она не null.

1final list = [1, 2, 3];
2final List<String>? nullableList;
3final myGreatList = [...list, ?...nullableList];
4

??= — оператор null-aware assignment. Присвоит значение переменной слева, только если она сейчас null.

1  int value; // null
2  value ??= 5; // 5
3  value ??= 6; // value != null -> value все еще 5
4

С null-safety разобрались. Теперь поговорим о функциях.

Функции

Dart, следуя за тенденциями, предоставляет большой инструментарий по работе с функциями. Рассмотрим его поближе.

Параметры функции

Все виды параметров условно можно распределить на следующей диаграмме.

fluttern

А теперь по порядку.

positional-параметры

Их всегда нужно передать, даже если один из них nullable (то есть необязательный):

1void foo(int value, String? value1) {}
2
3void main() {
4 foo(5, null);
5}
6

optional positional — параметры, которые необязательно передавать при вызове. Но нельзя, чтобы у таких параметров non-nullable не было значения.

1void foo(int value, [String value1]) {} // Это ошибка, у value1 гарантированно должно быть значение
2
3// У optional параметров могут быть значения по-умолчанию. Важно: значение default — всегда константа
4void foo(int value, [String value1 = '']) {}
5
6// Либо можно сделать их nullable, тогда их значение по умолчанию — null
7void foo(int value, [String? value1]) {}
8
9void main() {
10 foo(5);
11}
12

named-параметры

Они которые позволяют создавать функции с огромным количеством параметров и не ошибиться при вызове.

1void foo1(int value1, bool flag, int value2, int value3) {} // неудобно и сложно
2
3void foo2({int value1, bool flag, int value2, int value3}) {} // удобно и читаемо
4
5void main() {
6 foo1(5, true, 2, 1);
7
8 foo2(value1: 5, flag: true, value2: 2, value3: 3);
9}
10

В коде выше foo2 объявлена с ошибкой, поскольку мы можем не передать один из параметров при вызове. При этом значение у параметра не может отсутствовать. На помощь приходит ключевое слово required — оно обязывает при вызове передать именованный параметр:

1void foo2({required int value1, required bool flag, required int value2, required int value3}) {} // удобно и читаемо
2
3void main() {
4 foo2(value1: 5, flag: true, value2: 2); // ошибка, value3 не передали
5}
6

Так же, как и с positional-параметрами, мы можем задать значения по умолчанию для именованного параметра, тогда можно отказаться от required и не передавать его при вызове:

1void foo2({required int value1, required bool flag, required int value2, int value3 = 0}) {} // удобно и читаемо
2
3void main() {
4 foo2(value1: 5, flag: true, value2: 2); // ошибки нет, value3 == 0
5}
6

Функции верхнего уровня и переменные

Это понятие носит много имён: глобальные функции, функции высшего порядка, «просто» функции. Для удобства мы будем называть их функциями верхнего уровня. Но суть у них одна — они и переменные могут существовать вне контекста какого-то класса.

Простейший пример такой функции — main(). Это не только точка входа в программу, но ещё и функция верхнего уровня.

1// Функция верхнего уровня
2void topLevelfunc() {}
3
4// Переменная верхнего уровня
5int topLevelVariable = 5;
6

Независимо от модификатора доступа и места объявления в файле такие функции видны всем членам «библиотеки», а сами они видят такие же функции верхнего порядка и переменные.

Стрелочные функции

Стрелочные функции позволяют описывать функции в одну строку:

1int getInteger() => 5; // Код вернет 5
2
3// 1. Стрелочная функция может и не возвращать какого-то конкретного значения
4// 2. А void может быть опущен
5// И то, и другое - плохой тон, хоть и так можно.
6printInteger() => print(getInteger());
7
8// Ниже эквивалентный код без стрелочных функций
9int getInteger() {
10 return 5;
11}
12
13printInteger() {
14 print(getInteger());
15}
16

Анонимные функции

Из функционального подхода в Dart перекочевали анонимные функции — их можно воспринимать как объект типа Function.

У типа Function есть метод call, он и есть вызов самой функции. Его полезно использовать, если переменная-функция может быть null.

1typedef PrinterFunc = String Function(String name, int years);
2
3void main() {
4 PrinterFunc? expression = (String name, int years) => "Hello, my name is $name, i'm $years";
5
6  print(expression?.call('Daniil', 20));
7}
8

Каскадный вызов методов

Функция, позволяет вызывать последовательно методы одной переменной:

1final list = [1, 2, 3];
2print(list..removeLast()..remove(0)); // 2
3

Результат каскадного вызова — исходное значение. Чтобы это лучше понять, рассмотрим следующий пример: toLowerCase() и toUpperCase() возвращают изменённое значение value, но при каскадном вызове программа не выведет результат этих функций.

1final String value = 'Hello, world!';
2print(value..toUpperCase()..toLowerCase()); // 'Hello, world!' - исходное значение value
3print(value.toUpperCase().toLowerCase()); // 'hello, world!' - результат toLowerCase()
4

Замыкания

Этот механизм позволяет анонимной функции, объявленной внутри другой, получать доступ к вышестоящему «контексту».

Звучит страшно, так что рассмотрим пример замыкания:

Функция верхнего уровня robotFactory возвращает анонимную функцию типа RobotBuild. Сама анонимная функция формирует строку из переданного name и доступных из контекста robotFactory переменных model и specifications.

Генераторы

В Dart есть синхронные и асинхронные генераторы. Здесь мы расскажем про синхронные, а асинхронные рассмотрим чуть позже, в одном из следующих параграфов.

Синхронные генераторы, например, пригодятся, когда вам нужно сгенерировать много тестовых данных:

Отлично, основы Dart вспомнили. Предлагаем переходить к следующему параграфу — в нём мы поговорим о том, как в Dart реализовано ООП — Объектно ориентированное программирование.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.4. Коротко о Dart: часть первая
Следующий параграф2.6. Dart: ООП