В предыдущем параграфе мы начали наш короткий обзор 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 реализовано ООП — Объектно ориентированное программирование.