2.4. Коротко о Dart: часть первая
Автор: Даниил Артамонов
В этой (и следующей) статье мы будем говорить о Dart. Наша цель — в первую очередь освежить знания. В этой статье поговорим о:
- переменных;
- коллекциях;
- строках;
Но сперва — давайте вспомним, что нам известно о самом Dart.
Этот язык программирования появился в 2011 году как замена JavaScript. То есть — если вы знакомы с web-программированием, то найдёте в Dart схожие с JavaScript возможности. На сегодняшний день язык прошёл достаточно долгий путь, получил поддержку null-safety (мы вскользь упоминали её в предыдущих статьях), статическую типизацию и Dart-VM.
Чаще всего Dart можно увидеть в паре с фреймворком Flutter в сфере разработки кроссплатформенных приложений, также сейчас есть перспективные попытки использовать Dart для написания серверов.
Переменные
В Dart есть много встроенных типов. На перечислении всех мы останавливаться не будем, подробнее о них можно прочитать в документации языка.
Создание переменной мало чем отличается от других языков. Пример:
void main() {
String helloString = "Hello, world!";
print(helloString);
}
Результат выполнения
Hello, world!
Код выше создаёт переменную типа String, присваивает ей значение Hello, world! и выводит результат в консоль.
var
Dart поддерживает вывод типов (type-inference), так что указывать тип переменной сразу необязательно. На помощь приходит ключевое слово var:
void main() {
var helloString = 'Hello, world!';
print(helloString);
//Код ниже выбросит compile time исключение, потому что на строке 2 для переменной был вычислен тип String
helloString = 4;
print(helloString);
}
Результат выполнения
main.dart:7:17: Error: A value of type 'int' can't be assigned to a variable of type 'String'.
helloString = 4;
^
dynamic
В Dart есть ключевое слово dynamic, отключающее проверки типов для переменной. Им стоит пользоваться, если тип переменной не известен до запуска программы, и его нельзя вывести. Давайте заменим var из предыдущего примера на dynamic и слегка модифицируем код:
void main() {
dynamic helloString = 'Hello,world!';
print(helloString);
// Ошибки теперь не будет
helloString = 4;
print(helloString);
//А теперь запишем в переменную строчку и попробуем использовать с ней математическую операцию
helloString = 'I am string';
print(helloString / 4);
}
Результат выполнения
Hello,world!
4
Unhandled exception:
NoSuchMethodError: Class 'String' has no instance method '/'.
Receiver: "I am string"
Tried calling: /(4)
Примечание
Важно: пользоваться стоит с осторожностью, потому что любые ошибки, когда используете данную переменную, можно заметить только во время работы программы.
Заметим, что в случае с var вычисление типа переменной всегда происходит на этапе компиляции. Следовательно, если переменная, помеченная var, не будет инициализирована при объявлении, то такая переменная будет иметь «неизвестный тип» (null), что позволит присвоить ей значение любого типа.
Рассмотрим описанное поведение на примере ниже:
void main() {
// Чтобы избежать такого неоднозначного поведения, достаточно явно указать тип переменной
// String? variable;
var variable;
print('Not initialized variable:');
print(variable.runtimeType);
print(variable);
//Теперь присвоим целочисленное значение variable
variable = 1;
print('Integer variable:');
print(variable.runtimeType);
print(variable);
//И нам так же не мешает этой же перменной присвоить Строку
variable = '2';
print('String variable:');
print(variable.runtimeType);
print(variable);
}
Результат выполнения
Not initialized variable:
Null
null
Integer variable:
int
1
String variable:
String
2
final and const
Во всех примерах выше переменные можно модифицировать и переопределять после объявления. Но чаще всего при разработке появляется потребность запретить изменение переменной после определения. Например, хорошей практикой считается делать свойства модели, и следовательно, саму модель неизменяемыми. А константные объекты позволяют выиграть в производительности.
final
Ключевое слово final запрещает переопределение переменной после инициализации.
Примечание
Важно. Переменная, помеченная final, — не константа, просто её значение нельзя переопределить после инициализации.
Давайте создадим переменную final и попытаемся изменить её значение.
void main() {
final a;
a = 5;
print('Integer variable: $a');
a = 6;
print('Integer variable 2: $a');
a = 'Some string';
print('String variable: $a');
}
Результат выполнения
main.dart:8:3: Error: Final variable 'a' might already be assigned at this point.
a = 6;
^
main.dart:11:3: Error: Final variable 'a' might already be assigned at this point.
a = 'Some string';
^
Действительно, переменная a получит значение 5, а при дальнейших попытках переприсвоить ей значение мы увидим исключение на этапе компиляции.
При этом можно модифицировать внутреннее состояние значения, присвоенного переменной final.
void main() async {
final list = [1, 2, 3];
list.add(5);
print(list);
}
Результат выполнения
[1, 2, 3, 5]
late
Ключевое слово late позволяет объявить переменную и не инициализировать её значение сразу.
Это полезно, когда хотим объявить переменную non-nullable (о null-safety поговорим в следующей статье), а значение ей задать позже.
//Без late будет исключение compile-time
late String name;
void main() {
name = 'Dart';
print('Hello, $name');
}
const
Ключевое слово const помечает константы. Их значения известны ещё на этапе компиляции, и их нельзя модифицировать или переопределять во время исполнения. Про запрет на модификацию поговорим ниже вместе с константными конструкторами классов.
А пока давайте заменим final на const в примере выше.
void main() {
const a = 5;
a = 5;
print('Integer variable: $a');
a = 6;
print('Integer variable 2: $a');
a = 'Some string';
print('String variable: $a');
}
Результат выполнения
main.dart:3:3: Error: Can't assign to the const variable 'a'.
a = 5;
^
main.dart:7:3: Error: Can't assign to the const variable 'a'.
a = 6;
^
main.dart:10:3: Error: Can't assign to the const variable 'a'.
a = 'Some string';
^
В итоге получаем сразу несколько ошибок до компиляции:
- Переменную
constнельзя оставить неинициализированной; - Переменную
constнельзя переопределить; - Переменной
constнельзя изменить тип, что довольно очевидно и вытекает из предыдущего пункта и системы типов языка.
Теперь давайте решим задачу.
Задача
Создадим два списка const и final и попробуем добавить в них новые элементы. Что выведет программа?
void main() {
final finalList = <int>[];
finalList.add(3);
print(finalList);
const constList = <int>[];
constList.add(2);
print(constList);
}
Ответ
[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 внутри коллекций
При объявлении коллекций мы можем внести в неё условия на добавление элементов.
void main() {
final condition = true;
final map = {
if (condition) 'foo': 2 else 'bar': 3,
'baz': 4,
};
final Set<int> mySet = {1, if (condition) 3 else 1, 2};
final list = [1, if (condition) 2 else 3, 4];
print(list);
print(mySet);
print(map);
}
Результат выполнения
[1, 2, 4]
{1, 3, 2}
{foo: 2, baz: 4}
Попробуйте переключить condition на false и посмотреть на результат.
Строки
Dart предоставляет обширный инструментарий для удобной работы со строками.
Как создать строку
В Dart есть несколько способов, чтобы создать строку:
- Через одинарные кавычки
'string'; - Через двойные кавычки
"string"; - Через тройные кавычки — одинарные
'''string'''или двойные"""string"""; - Как «сырую» строку
r'string'; - Через
StringBuffer. В основном он нужен для манипуляций со строками.
Общепринятым является первый способ. Однако, это не значит, что другие избыточны. Давайте рассмотрим следующий пример:
void main() {
// Предпочтительно
final singleQuotedString = 'Hello, world!';
// В основном нужен, если в строке могут быть одинарные кавычки.
// В остальном, полная копия предыдущего
final doubleQuotedString = "Hello, world! I'm 30!";
// Сохраняет форматирование со всеми терминаторами
final trippleQuotedString = '''
Не кури, не влюбляйся, не пей вина
И на лбу себе высеки:
Жизнь тебе для того дана,
Что бы ты её отдал физике!
''';
// Игнорирует любые спецсимволы
final rawString = r'Hello, world! I like dollar$$$$';
print(singleQuotedString);
print(doubleQuotedString);
print(trippleQuotedString);
print(rawString);
}
Результат выполнения
Hello, world!
Hello, world! I'm 30!
Не кури, не влюбляйся, не пей вина
И на лбу себе высеки:
Жизнь тебе для того дана,
Что бы ты её отдал физике!
Hello, world! I like dollar$$$$
На первый взгляд, последний пример с «сырой» строкой (r'Hello, world! I like dollar$$$$’) может показаться непонятным. Давайте разберёмся.
Например, $ — зарезервированный спецсимвол, и в строках, созданных с помощью кавычек, компилятор будет воспринимать его как символ интерполяции, и в финальную строку не добавит. Чтобы явно сказать компилятору, что в строке нет спецсимволов, нужно использовать сырые строки. В обычные строки зарезервированные спецсимволы можно добавлять, используя бэк-слеш перед каждым таким символом. Например: Hello, world! I like dollar\$\$\$\$.
Интерполяция строк
Выше вы могли заметить использование символа $ при выводе значений. Это интерполяция строк — одна из возможностей языка, позволяющих манипулировать со строками.
В Dart есть три способа, чтобы объединить их в одну:
- Конкатенация — склеивание двух строк в одну;
- Интерполяция — встраивание в строку вычисленных значений;
StringBuffer.
Давайте рассмотрим пример, показывающий, как получить одну и ту же строку всеми тремя способами:
void main() {
final value = 20;
final concatenatedString = getConcatenatedString(value);
final interpolatedString = getInterpolatedString(value);
final stringBufferString = getStringBufferString(value);
print(concatenatedString);
print(interpolatedString);
print(stringBufferString);
}
String getConcatenatedString(int value) {
//Конкатенировать можно только строки
return 'Hello! I am ' + value.toString() + ' years old';
}
String getInterpolatedString(int value) {
return 'Hello! I am $value years old';
}
String getStringBufferString(int value) {
final stringBuffer = StringBuffer();
stringBuffer.write('Hello! I am ');
// С StringBuffer toString() уже вызывать не нужно
// write принимает значения типа Object?
stringBuffer.write(value);
stringBuffer.write(' years old');
return stringBuffer.toString();
}
Результат выполнения
Hello! I am 20 years old
Hello! I am 20 years old
Hello! I am 20 years old
Сильной разницы в производительности между ними нет, так что вы вольны выбирать именно тот, который удобнее в вашей ситуации.
Экранирование спецсимволов
Когда мы рассматривали способы, которыми создаются строки, то сказали, что можно «отключить» все спецсимволы в сырой строке. Но такой способ далеко не всегда нам нужен, зачастую мы хотим экранировать только пару символов из всей строки.
Достичь этого можно с помощью ещё одного спецсимвола — \.
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:
void main() {
final regExp = RegExp('world');
final myString = 'Hello, world!';
final hasWorld = regExp.hasMatch(myString);
print(hasWorld);
}
Результат выполнения
true
На этом вся мощь регулярных выражений не заканчивается. Подробнее можно прочитать в документации.
На этом всё. В следующей статье мы продолжим вспоминать основы Dart — сфокусируемся на функциях и null-safety, но не забудем и про другие важные концепции — модификаторы доступа, typedef и другие.