В предыдущем параграфе мы начали наш короткий обзор 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; - Нельзя переопределить
index,hashCodeили оператор равенства==; - Нельзя использовать имя
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 нет модификаторов доступа: public, private и 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
Сам компилятор постоянно следит за разработчиком и подсвечивает потенциально опасные места:
Код просто не скомпилируется, если компилятор увидит потенциально опасный вызов.
Выходов из ситуации несколько:
- Оператор
?.; - Оператор
!.; - Вычисление типа компилятором.
Рассмотрим каждый их них подробнее.
Оператор ? или 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, следуя за тенденциями, предоставляет большой инструментарий по работе с функциями. Рассмотрим его поближе.
Параметры функции
Все виды параметров условно можно распределить на следующей диаграмме.
А теперь по порядку.
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 реализовано ООП — Объектно ориентированное программирование.
