Dart — а что ты такое?
В 2011 году Dart появился как замена JavaScript. Это значит, что знакомый с web-программированием, найдёт схожие с JavaScript возможности в Dart. На сегодняшний день язык прошёл достаточно долгий путь, получил поддержку null-safety, статическую типизацию и Dart-VM.
Чаще всего его можно увидеть в паре с фреймворком Flutter в сфере разработки кроссплатформенных приложений, также сейчас есть перспективные попытки использовать Dart для написания серверов.
В этом параграфе поговорим про интересные возможности, отличающие Dart от других языков.
Переменные
В Dart есть много встроенных типов. На перечислении всех мы останавливаться не будем, подробнее о них можно прочитать в документации языка.
Создание переменной мало чем отличается от других языков. Пример:
Код выше создаёт переменную типа String, присваивает ей значение Hello, world!
и выводит результат в консоль.
var
Dart поддерживает вывод типов (type-inference), так что указывать тип переменной сразу необязательно. На помощь приходит ключевое слово var
:
dynamic
В Dart есть ключевое слово dynamic
, отключающее проверки типов для переменной. Им стоит пользоваться, если тип переменной не известен до запуска программы и его нельзя вывести. Давайте заменим var
из предыдущего примера на dynamic
и слегка модифицируем код:
Пользоваться стоит с осторожностью, потому что любые ошибки, когда используете данную переменную, можно заметить только во время работы программы.
Заметим, что в случае с var
вычисление типа переменной всегда происходит на этапе компиляции. Следовательно, если переменная, помеченная var
, не будет инициализирована при объявлении, то такая переменная будет иметь «неизвестный тип» (null
), что позволит присвоить ей значение любого типа.
Рассмотрим описанное поведение на примере ниже:
final and const
Во всех примерах выше переменные можно модифицировать и переопределять после объявления. Но чаще всего при разработке появляется потребность запретить изменение переменной после определения. Например, хорошей практикой считается делать свойства модели, и следовательно, саму модель неизменяемыми. А константные объекты позволяют выиграть в производительности.
final
Ключевое слово final
запрещает переопределение переменной после инициализации.
Важно. Переменная, помеченная final
, — не константа, просто её значение нельзя переопределить после инициализации.
Давайте создадим переменную final
и попытаемся изменить её значение.
Действительно, переменная a
получит значение 5
, а при дальнейших попытках переприсвоить ей значение мы увидим исключение на этапе компиляции.
При этом можно модифицировать внутреннее состояние значения, присвоенного переменной final
.
late
Ключевое слово late
позволяет объявить переменную и не инициализировать её значение сразу.
Это полезно, когда хотим объявить переменную non-nullable
(о null-safety
поговорим ниже), а значение ей задать позже.
//Без late будет исключение compile-time
late String name;
void main() {
name = 'Dart';
print('Hello, $name');
}
const
Ключевое слово const
помечает константы. Их значения известны ещё на этапе компиляции, и их нельзя модифицировать или переопределять во время исполнения. Про запрет на модификацию поговорим ниже вместе с константными конструкторами классов.
А пока давайте заменим final
на const
в примере выше.
В итоге получаем сразу несколько ошибок до компиляции:
- Переменную
const
нельзя оставить неинициализированной; - Переменную
const
нельзя переопределить; - Переменной
const
нельзя изменить тип, что довольно очевидно и вытекает из предыдущего пункта и системы типов языка.
Теперь давайте решим задачу.
Задача 1
Создадим два списка const
и final
и попробуем добавить в них новые элементы. Что выведет программа?
void main() {
final finalList = <int>[];
finalList.add(3);
print(finalList);
const constList = <int>[];
constList.add(2);
print(constList);
}
{% cut "Ответ" %}
[3]
Ошибка — Unsupported operation
Коллекции
В этом блоке рассмотрим работу с коллекциями.
Объявление коллекций
При необходимости Dart позволяет указывать тип элементов в коллекциях.
final list = [1, "string", 2.4]; // любой тип
final strongTypedList = <int>[1, 2, "string"]; // только целочисленные значения, из-за "string" получаем ошибку на этапе компиляции
Обратите внимание: хотя указание типа не обязательно, — компилятор вычислит его самостоятельно на основе элементов коллекции.
Он будет считать, что в внутри списка list
лежат переменные dynamic
. В итоге код не гарантирует Type-Safety.
Распаковка коллекций
Мы можем распаковать несколько коллекций в одну с помощью оператора ...
.
final collection1 = [1,2,3];
final collection2 = [4,5,6];
final compositeCollection = [...collection1, ...collection2];
If-else внутри коллекций
При объявлении коллекций мы можем внести в неё условия на добавление элементов.
Попробуйте переключить condition
на false
и посмотреть на результат.
Enhanced enums
В Dart есть знакомые многим enum-типы, объявить простейший enum можно так:
enum Color {red, green, blue}
В версии языка 2.17 разработчики добавили Enhanced enums.
Enhanced enum — класс со своими полями, методами и конструкторами, но с определёнными ограничениями:
- Поля класса должны быть
final
, включая те, что подмешаны черезwith
; - Все конструкторы должны быть константными;
- Все
factory
-конструкторы могут вернуть только один из объявленных экземпляровEnum
; - Нельзя переопределить
index
,hashCode
или оператор равенства==
; - Нельзя использовать имя
values
для членов класса, потому что это конфликтует с геттеромvalues
из API стандартныхenum
; - В начале
enum
нужно объявить все возможные значения. При этом в целом значений должно быть не менее одного.
Enhanced enums могут пригодиться во многих случаях. К примеру, при работе с иконками:
enum MyIcon {
close(path: 'assets/close', width: 200),
car(path: 'assets/car', width: 300),
driver(path: 'assets/driver', width: 300);
const MyIcon({
required this.path,
required this.width,
});
factory MyIcon.withName(String name) =>
values.firstWhere((v) => v.name.contains(name), orElse: () => close);
final String path;
final double width;
String getThemedPath(bool isDarkTheme) =>
isDarkTheme ? '${path}_night.webp' : '$path.webp';
}
Тернарный оператор
В определённых ситуациях if-else
избыточен, тогда на помощь приходит тернарный оператор:
bool isEvenFull(int value) {
bool result;
if (value % 2 == 0) {
result = true;
} else {
result = false;
}
return result;
}
//Может быть чуть короче
bool isEvenShort(int value) {
return value % 2 == 0 ? true : false
}
typedef
Это ключевое слово позволяет переименовать какой-то тип.
typedef JSON = Map<String, Object?>;
typedef Converter = String Function(int value); // Что это такое? см. Функции
String convertToString(int value, Converter converter) => converter?.call(value);
class MyModel {
final int value;
MyModel(this.value);
factory MyModel.fromJson(JSON json) => MyModel(json['value'] ?? 0);
}
Строки
Dart предоставляет обширный инструментарий для удобной работы со строками.
Как создать строку
В Dart есть несколько способов, чтобы создать строку:
- Через одинарные кавычки
'string'
; - Через двойные кавычки
"string"
; - Через тройные кавычки — одинарные
'''string'''
или двойные"""string"""
; - Как «сырую» строку
r'string'
; - Через
StringBuffer
. В основном он нужен для манипуляций со строками.
Общепринятым является первый способ. Однако, это не значит, что другие избыточны. Давайте рассмотрим следующий пример:
На первый взгляд, последний пример с «сырой» строкой (r'Hello, world! I like dollar$$$$’
) может показаться непонятным. Давайте разберёмся.
Например, $
— зарезервированный спецсимвол, и в строках, созданных с помощью кавычек, компилятор будет воспринимать его как символ интерполяции, и в финальную строку не добавит. Чтобы явно сказать компилятору, что в строке нет спецсимволов, нужно использовать сырые строки.
Интерполяция строк
Выше вы могли заметить использование символа $
при выводе значений. Это интерполяция строк — одна из возможностей языка, позволяющих манипулировать со строками.
В Dart есть три способа, чтобы объединить их в одну:
- Конкатенация — склеивание двух строк в одну;
- Интерполяция — встраивание в строку вычисленных значений;
StringBuffer
.
Давайте рассмотрим пример, показывающий, как получить одну и ту же строку всеми тремя способами:
Сильной разницы в производительности между ними нет, так что вы вольны выбирать именно тот, который удобнее в вашей ситуации.
Экранирование спецсимволов
Когда мы рассматривали способы, которыми создаются строки, то сказали, что можно «отключить» все спецсимволы в сырой строке. Но такой способ далеко не всегда нам нужен, зачастую мы хотим экранировать только пару символов из всей строки.
Достичь этого можно с помощью ещё одного спецсимвола — \
.
final productCost = 400;
// Итог: Hi i'm Michael. This thing costs only $400
final constString = 'Hi i\'m Michael. This thing costs only \$$productCost';
Регулярные выражения
В Dart есть удобный механизм по работе с регулярными выражениями — RegExp
.
Приведём пример, который проверит, есть ли в строке слово world
:
На этом вся мощь регулярных выражений не заканчивается. Подробнее можно прочитать в документации.
Модификаторы доступа
В отличие от других языков, в Dart нет модификаторов доступа: public
, private
и protected
.
Есть только возможность ограничить видимость переменной, функции или класса с помощью нижнего подчеркивания _
.
Оно буквально ограничивает видимость за пределами файла, в котором была объявлена переменная или функция. Но есть исключение — нотации part of/part. На них не будем долго задерживаться, но скажем, что они позволяют разбить один файл «на части» с общей видимостью, как будто это один файл.
Например, мы можем ограничить видимость полей и методов класса. Так никто не сможет получить доступ к ним за пределами файла-объявления:
class MyClass{
final _privateField = "Hi, im private";
void _privateFunction(){
print(_privateField);
}
}
Чтобы лучше понять фразу «видимость за пределами контекста», давайте рассмотрим пример:
MyClass
вместе с main()
и SecondClass
объявлены внутри одного файла, поэтому приватные части MyClass
видны другим членам файла или контекста.
Если же кто-то попытается из другого файла вызвать _privateFunction()
, то у него ничего не выйдет.
null-safety
Dart поддерживает sound null-safety
как необязательную функцию на версиях с 2.12 по 2.19 и как обязательную часть с версии 3. Это значит, что разработчик защищен от NullReferenceException
, так называемой «ошибки на миллиард долларов».
Подробнее о реализации null-safety
в Dart можно прочитать в документации.
Мы же остановимся на основах.
С включенным null-safety
в Dart все переменные не могут иметь значение null
, если об этом не сказано явно с помощью ?
:
String nonNullVariable = "Hello, world!";
// При попытке присвоить null будет ошибка
nonNullVariable = null;
String? nullableVariable = "Hello, world!";
// Теперь ничего не мешает присвоить null
nullableVariable = null;
Сам компилятор постоянно следит за разработчиком и подсвечивает потенциально опасные места:
Код просто не скомпилируется, если компилятор увидит потенциально опасный вызов.
Выходов из ситуации несколько:
- Оператор
?.
; - Оператор
!.
; - Вычисление типа компилятором.
Оператор ?
или null-aware
В опасных местах можно пользоваться null-aware. Он буквально говорит компилятору: «Если переменная не null
, то вызывай код, иначе — верни null
».
final value = myClass?.foo();
Компилятор ругаться перестанет, и мы получим следующее:
Тип value
— int?
.
myClass
не null — value == 5
.
myClass
не присвоено или null — value == null
Оператор !
или force unwrap
Данный оператор говорит компилятору: «Я уверен, что данное значение не может быть null
».
final value = myClass!.foo();
// Чуть длиннее. Осторожно, force unwrap и приведение это не одно и то же!
final value = (myClass as MyClass).foo();
Компилятор ругаться перестанет, и мы получим следующее:
- Тип
value
—int
. myClass
присвоено не null —value == 5
.myClass
не присвоено или null — код в рантайме выброситNullReferenceException
.
Стоит пользоваться с осторожностью. Force unwrap
лишает нас кода null-safe
, так что в итоге можно не заметить потенциальную NRE
. Предпочтительнее использовать ?.
и ??
.
Вычисление типа компилятором
Мы можем положить nullable-значение в локальную переменную и специально проверить эту локальную переменную на null
.
final myList = <MyClass?>[MyClass(), null];
final firstElement = myList.first; // first == myList[0]
int? value;
if(firstElement != null){
value = firstElement.foo();
}
В итоге получим следующее:
Тип value
— int?
или тот, что вы указали при объявлении (ex. dynamic
);
Первый элемент не null — value == 5
;
Первый элемент null — value == null
.
Dart не видит, что первый элемент коллекции существует. Так происходит из-за full sound safety, компилятор может гарантировать вычисление только для локальных переменных.
За подробностями можно обратиться к документации или видео от разработчиков.
Бонус
В качестве бонуса в языке Dart есть ещё несколько операторов:
??
— оператор null
. Выполнит код справа-налево, только если значение левого операнда null
.
getIntegerOrZero(null); // 0
getIntegerOrZero(5); // 5
int getIntegerOrZero(int? integer) {
return interger ?? 0;
}
?...
— оператор null-spread. Распакует коллекцию, только если она не null
.
final list = [1, 2, 3];
final List<String>? nullableList;
final myGreatList = [...list, ?...nullableList];
??=
— оператор null-aware assignment
. Присвоит значение переменной слева, только если она сейчас null
.
int value; // null
value ??= 5; // 5
value ??= 6; // value != null -> value все еще 5
Функции
Dart, следуя за тенденциями, предоставляет большой инструментарий по работе с функциями. Рассмотрим его в данном параграфе.
Параметры функции
Все виды параметров условно можно распределить на следующей диаграмме.
А теперь по порядку.
positional
— параметры функции.
Их всегда нужно передать, даже если один из них nullable (то есть необязательный):
void foo(int value, String? value1) {}
void main() {
foo(5, null);
}
optional positional
— параметры, которые необязательно передавать при вызове. Но нельзя, чтобы у таких параметров non-nullable не было значения.
void foo(int value, [String value1]) {} // Это ошибка, у value1 гарантированно должно быть значение
// У optional параметров могут быть значения по-умолчанию. Важно: значение default — всегда константа
void foo(int value, [String value1 = '']) {}
// Либо можно сделать их nullable, тогда их значение по умолчанию — null
void foo(int value, [String? value1]) {}
void main() {
foo(5);
}
named
— параметры, которые позволяют создавать функции с огромным количеством параметров и не ошибиться при вызове.
void foo1(int value1, bool flag, int value2, int value3) {} // неудобно и сложно
void foo2({int value1, bool flag, int value2, int value3}) {} // удобно и читаемо
void main() {
foo1(5, true, 2, 1);
foo2(value1: 5, flag: true, value2: 2, value3: 3);
}
В коде выше foo2
объявлена с ошибкой, поскольку мы можем не передать один из параметров при вызове. При этом значение у параметра не может отсутствовать. На помощь приходит ключевое слово required
— оно обязывает при вызове передать именованный параметр:
void foo2({required int value1, required bool flag, required int value2, required int value3}) {} // удобно и читаемо
void main() {
foo2(value1: 5, flag: true, value2: 2); // ошибка, value3 не передали
}
Так же, как и с positional
-параметрами, мы можем задать значения по умолчанию для именованного параметра, тогда можно отказаться от required
и не передавать его при вызове:
void foo2({required int value1, required bool flag, required int value2, int value3 = 0}) {} // удобно и читаемо
void main() {
foo2(value1: 5, flag: true, value2: 2); // ошибки нет, value3 == 0
}
Функции верхнего уровня и переменные
Это понятие носит много имён: глобальные функции, функции высшего порядка, «просто» функции. Для удобства мы ниже будем называть их функциями верхнего уровня. Но суть у них одна — они и переменные могут существовать вне контекста какого-то класса. Простейший пример такой функции — main()
. Это не только точка входа в программу, но ещё и функция верхнего уровня.
// Функция верхнего уровня
void topLevelfunc() {}
// Переменная верхнего уровня
int topLevelVariable = 5;
Независимо от модификатора доступа и места объявления в файле такие функции видны всем членам «библиотеки», а сами они видят такие же функции верхнего порядка и переменные.
Стрелочные функции
Стрелочные функции позволяют описывать функции в одну строку:
int getInteger() => 5; // Код вернет 5
// 1. Стрелочная функция может и не возвращать какого-то конкретного значения
// 2. А void может быть опущен
// И то, и другое - плохой тон, хоть и так можно.
printInteger() => print(getInteger());
// Ниже эквивалентный код без стрелочных функций
int getInteger() {
return 5;
}
printInteger() {
print(getInteger());
}
Анонимные функции
Из функционального подхода в Dart перекочевали анонимные функции — их можно воспринимать как объект типа Function
.
У типа Function
есть метод call
, он и есть вызов самой функции. Его полезно использовать, если переменная-функция может быть null
.
typedef PrinterFunc = String Function(String name, int years);
void main() {
PrinterFunc? expression = (String name, int years) => "Hello, my name is $name, i'm $years";
print(expression?.call('Daniil', 20));
}
Каскадный вызов методов
Функция, позволяет вызывать последовательно методы одной переменной:
final list = [1, 2, 3];
print(list..removeLast()..remove(0)); // 2
Результат каскадного вызова — исходное значение. Чтобы это лучше понять, рассмотрим следующий пример: toLowerCase()
и toUpperCase()
возвращают изменённое значение value
, но при каскадном вызове программа не выведет результат этих функций.
final String value = 'Hello, world!';
print(value..toUpperCase()..toLowerCase()); // 'Hello, world!' - исходное значение value
print(value.toUpperCase().toLowerCase()); // 'hello, world!' - результат toLowerCase()
Замыкания
Этот механизм позволяет анонимной функции, объявленной внутри другой, получать доступ к вышестоящему «контексту».
Звучит страшно, так что рассмотрим пример замыкания:
Функция верхнего уровня robotFactory
возвращает анонимную функцию типа RobotBuild
. Сама анонимная функция формирует строку из переданного name
и доступных из контекста robotFactory
переменных model
и specifications
.
Генераторы
В Dart есть синхронные и асинхронные генераторы. Здесь мы расскажем про синхронные, а асинхронные рассмотрим чуть позже, в параграфе «Dart Concurrency, изоляты».
Синхронные генераторы, например, пригодятся, когда вам нужно сгенерировать много тестовых данных: