Перейти к основному содержимому

Основы языка Dart

Разработчику Архитектору

Основы языка Dart

Что такое Dart?

Dart — это язык программирования со следующими особенностями:

  • Типизация — статическая, сильная, звуковая (sound null safety с Dart 2.12); автоматический вывод типов (var, inferred types).
  • Парадигма — объектно-ориентированный (всё — объект), функциональный (функции первого класса, методы высшего порядка для коллекций), императивный.
  • Уровень — высокоуровневый.
  • Выполнение — компилируемый — JIT (Dart VM, разработка), AOT (нативный машинный код), транспиляция в JavaScript (dart compile js).
  • Память — автоматическая (генерационный GC).
  • Платформа — кроссплатформенный (мобильные, десктоп, веб, сервер); управляемый runtime (Dart VM); AOT в нативный код; транспиляция в JavaScript для браузера.
  • Формат разработки — один файл можно запустить (dart run file.dart), но идиоматично — структура проекта с pubspec.yaml и pub.
  • Направление — кроссплатформенный UI (Flutter), мобильная и десктопная разработка, веб, бэкенд (Shelf, Dart Frog), CLI-утилиты.
  • REPL — нет классического интерактивного REPL в CLI; интерактивность через DartPad (веб), hot reload в Flutter/Dart VM, отладчик в IDE.
  • Поколение — современный (с 2011), активно развивающийся (Dart 3).
  • Параллелизм и асинхронность — однопоточный event loop в изоляте; нативная асинхронность (async/await, Future, Stream); параллелизм через изоляты (обмен сообщениями, без shared memory).
  • Безопасность — относительно безопасный — sound null safety, статическая типизация, нет арифметики указателей; зона "опасности" — dart:ffi и небезопасный код через interop.

Если какой-то пункт из списка непонятен — подробные определения и примеры в Язык программирования.

Скелет программы — main, переменные, встроенные типы, функции. Flutter пока в стороне — тот же void main() будет и во Flutter.

Дальше: синтаксистипыasyncпервая программа.

Dart — это современный язык программирования, созданный компанией Google и предназначенный для разработки высокопроизводительных, масштабируемых и кроссплатформенных приложений. Язык сочетает в себе простоту синтаксиса, строгую типизацию, объектно-ориентированную архитектуру и мощные инструменты для асинхронного программирования. Dart получил широкое распространение благодаря фреймворку Flutter, который позволяет создавать нативные мобильные, десктопные и веб-приложения из единой кодовой базы. Однако сам язык не ограничивается экосистемой Flutter и может использоваться самостоятельно для серверной разработки, скриптов, утилит командной строки и других задач.


Кратко о языке

Dart — open-source язык (лицензия BSD), развиваемый Google и сообществом. Сегодня он чаще всего используется вместе с Flutter, но подходит и для консольных утилит, серверов и скриптов. Хронологию версий, null safety и роль Flutter см. в статье История языка Dart.


Синтаксис и структура программы

Программа на Dart начинается с точки входа — функции main. Эта функция вызывается автоматически при запуске приложения и служит отправной точкой для выполнения кода. Простейшая программа выглядит так:

void main() {
print('Привет, Вселенная IT!');
}

Разбор:

  • void main() — стандартная точка входа Dart-программы.
  • print(...) выводит строку в консоль.
  • Кавычки '...' задают литерал типа String.
  • Это минимальный скелет приложения без классов и зависимостей.

Ключевое слово void указывает, что функция ничего не возвращает. Функция print выводит текст в консоль. Такой минималистичный пример демонстрирует явное объявление функций, фигурные скобки для тела блока и опциональные точки с запятой в конце строк (в Dart 3 их обычно опускают).

Dart следует принципам C-подобного синтаксиса — операторы, условия, циклы и объявления функций используют привычные конструкции, такие как if, for, while, return. Это делает язык доступным для разработчиков, имеющих опыт в Java, C#, JavaScript или TypeScript.


Типы данных и переменные

Dart — язык со статической типизацией, но он также поддерживает вывод типов. Это означает, что разработчик может явно указывать тип переменной, а может позволить компилятору определить его автоматически на основе значения.

Переменные объявляются с помощью ключевых слов var, final или const, а также с указанием конкретного типа:

  • var name = 'Dart'; — компилятор выводит, что name имеет тип String.
  • String language = 'Dart'; — тип указан явно.
  • final version = '3.0'; — переменная инициализируется один раз и не может быть изменена позже.
  • const pi = 3.14159; — константа, значение которой известно на этапе компиляции и неизменно в течение всего времени выполнения.

Основные встроенные типы в Dart включают:

  • int — целые числа, например, 42.
  • double — числа с плавающей точкой, например, 3.14.
  • bool — логический тип со значениями true и false.
  • String — последовательности символов в одинарных или двойных кавычках, поддерживающие интерполяцию: 'Значение: $pi'.
  • List — упорядоченная коллекция элементов, например, [1, 2, 3].
  • Set — неупорядоченная коллекция уникальных элементов.
  • Map — ассоциативный массив, хранящий пары "ключ — значение".

Все эти типы являются объектами, даже числа и логические значения. Это означает, что у любого значения можно вызывать методы и обращаться к свойствам. Например, строка 'привет'.toUpperCase() возвращает 'ПРИВЕТ'.


Функции

Функции в Dart — это полноправные объекты первого класса. Их можно присваивать переменным, передавать как аргументы другим функциям и возвращать из функций. Объявление функции включает тип возвращаемого значения, имя, список параметров и тело.

Пример функции с явным возвратом:

int add(int a, int b) {
return a + b;
}

Разбор:

  • int в сигнатуре задаёт тип возвращаемого значения и типы параметров.
  • return a + b завершает функцию и отдаёт сумму вызывающему коду.
  • Позиционные параметры передаются строго в объявленном порядке.

Dart поддерживает сокращённый синтаксис для функций, состоящих из одного выражения:

int multiply(int a, int b) => a * b;

Разбор:

  • Стрелочная форма => эквивалентна { return ...; } для одного выражения.
  • Результат выражения a * b автоматически становится возвращаемым значением.
  • Такой синтаксис удобен для коротких математических и преобразующих функций.

Стрелочная нотация (=>) эквивалентна записи с return и фигурными скобками, но короче и читабельнее для простых операций.

Функции могут иметь именованные параметры, которые передаются по ключу, а не по позиции. Это повышает читаемость вызовов:

void greet({required String name, String? greeting}) {
print('${greeting ?? 'Привет'}, $name!');
}

Разбор:

  • Параметры в {...} — именованные: при вызове указывают name: и greeting:.
  • required делает name обязательным именованным аргументом.
  • String? greeting допускает null, если приветствие не передали.
  • greeting ?? 'Привет' подставляет значение по умолчанию.
  • ${...} в строке — интерполяция выражения.

Здесь name — обязательный именованный параметр, а greeting — необязательный, который может быть null. Оператор ?? предоставляет значение по умолчанию, если переменная равна null.


Классы и объектно-ориентированное программирование

Dart — полностью объектно-ориентированный язык. Все значения являются экземплярами классов, и каждый класс наследуется от корневого класса Object.

Класс объявляется с помощью ключевого слова class. Он может содержать поля, методы, конструкторы и геттеры/сеттеры:


import 'dart:math';

class Point {
final double x;
final double y;

Point(this.x, this.y);

double distanceToOrigin() => sqrt(x * x + y * y);
}

Разбор:

  • import 'dart:math' подключает функцию sqrt.
  • final double x, y фиксирует координаты точки после создания.
  • Point(this.x, this.y) инициализирует поля через параметры конструктора.
  • distanceToOrigin() вычисляет расстояние до (0, 0) по теореме Пифагора.

Конструктор Point(this.x, this.y) — синтаксический сахар: параметры сразу присваиваются полям. Функция sqrt из библиотеки dart:math вычисляет расстояние до начала координат (у типа double нет метода .sqrt()).

Dart поддерживает наследование, абстрактные классы, интерфейсы и миксины. Интерфейсы реализуются неявно: любой класс автоматически определяет интерфейс, содержащий все его публичные методы и свойства. Это позволяет легко применять полиморфизм без необходимости явного объявления interface. Подробнее о _, late, factory и sealed: классы и ООП.

Миксины — это способ повторного использования кода в нескольких иерархиях наследования. Они позволяют добавлять функциональность классу без создания жёсткой связи через наследование:

mixin Flyable {
void fly() => print('Лечу!');
}

class Bird with Flyable {}

Разбор:

  • mixin Flyable описывает переиспользуемое поведение без наследования класса.
  • class Bird with Flyable подмешивает метод fly() в класс Bird.
  • Экземпляр Bird() получает метод fly, хотя Flyable не является родителем.

Экземпляр Bird теперь обладает методом fly, несмотря на то, что Flyable не является классом-родителем.


Асинхронное программирование

Одной из ключевых особенностей Dart является встроенная поддержка асинхронного выполнения кода. Язык предоставляет элегантные и читаемые механизмы для работы с операциями, которые не завершаются мгновенно — сетевыми запросами, чтением файлов, взаимодействием с базами данных или ожиданием пользовательского ввода. Вместо блокировки основного потока выполнения, Dart использует модель событий и управление через объекты Future и ключевые слова async / await.

Объект Future представляет собой обещание получить значение в будущем. Он может находиться в одном из трёх состояний: незавершённом, успешно завершённом или завершённом с ошибкой. Функция, возвращающая Future, помечается как async. Внутри такой функции можно использовать await, чтобы дождаться результата асинхронной операции, не прерывая читаемость кода:


import 'package:http/http.dart' as http;

Future<String> fetchUserData() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}
return response.body;
}

Разбор:

  • Future<String> означает асинхронный результат типа String.
  • async позволяет использовать await внутри функции.
  • http.get(...) выполняет HTTP GET и возвращает Future<Response>.
  • Проверка statusCode != 200 отсекает неуспешные ответы через throw.
  • response.body возвращает текст ответа как String.

В pubspec.yaml нужна зависимость http. Такой код читается почти как синхронный: await приостанавливает только текущую async-функцию, не блокируя event loop изолята.

Stream — последовательность событий во времени (клики, WebSocket, датчики). Генераторы потоков пишут через async* и yield:

Stream<int> counter(int max) async* {
for (var i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}

Разбор:

  • Stream<int> описывает поток целых значений во времени.
  • async* делает функцию генератором потока.
  • await Future.delayed(...) имитирует паузу между событиями.
  • yield i отправляет очередное значение подписчикам stream.

Обработка ошибок

Интерактивное демо — часть сценариев на Python (try / except); в Dart — try / catch / finally, но стек вызовов и раскрутка те же. Подробнее: ошибки и исключения.

Play ITЗагрузка интерактивного демо…

Dart использует механизм исключений, аналогичный другим современным языкам. Ошибки возникают с помощью ключевого слова throw, перехватываются конструкцией try-catch, а необязательный блок finally выполняется в любом случае — успешно завершился код или произошла ошибка. Исключения в Dart не требуют объявления в сигнатуре функции, что упрощает проектирование API.

try {
var result = await riskyOperation();
print('Успех: $result');
} on Exception catch (e) {
print('Ошибка запроса: $e');
} catch (e) {
print('Неизвестная ошибка: $e');
} finally {
cleanupResources();
}

Разбор:

  • try содержит основную логику, где может возникнуть ошибка.
  • on Exception catch (e) перехватывает только исключения типа Exception.
  • Общий catch (e) ловит остальные ошибки.
  • finally выполняется всегда — удобно для освобождения ресурсов (cleanupResources).

Такой подход обеспечивает гибкость и предсказуемость при работе с ненадёжными операциями.


Коллекции и работа с данными

Dart предоставляет богатый набор встроенных коллекций: списки (List), множества (Set) и словари (Map). Все они поддерживают литералы, методы высшего порядка и функциональные преобразования. Например, список чисел можно фильтровать, преобразовывать и сворачивать одной цепочкой вызовов:

var numbers = [1, 2, 3, 4, 5];
var doubledEvens = numbers
.where((n) => n.isEven)
.map((n) => n * 2)
.toList();

Разбор:

  • where((n) => n.isEven) оставляет только чётные числа.
  • map((n) => n * 2) удваивает каждый оставшийся элемент.
  • toList() материализует ленивую цепочку в новый List.
  • Для [1,2,3,4,5] результат — [4, 8] (чётные 2 и 4, удвоенные).

Результатом будет [4, 8]. Такие операции делают обработку данных выразительной и лаконичной. Dart также поддерживает расширения коллекций через spread-оператор (...) и условные элементы (if внутри литерала), что особенно полезно при построении UI в Flutter.


Null safety

Начиная с версии 2.12, Dart включает систему звуковой null safety — механизм, который гарантирует отсутствие ошибок, связанных с обращением к null. Каждый тип в Dart теперь либо допускает значение null, либо нет. Тип String не может быть null, а String? — может. Компилятор проверяет все возможные пути выполнения и требует явной обработки случаев, когда значение может отсутствовать.

Это достигается за счёт трёх основных принципов:

  • Non-nullable by default: переменные без знака вопроса не могут содержать null.
  • Flow analysis — компилятор отслеживает, где значение уже проверено на null, и разрешает его использование.
  • Required initialisation: все поля класса должны быть инициализированы до того, как объект станет доступен.

Null safety резко снижает количество runtime-ошибок и делает код более надёжным без необходимости вручную проверять каждую переменную.


Изоляты и конкурентность

Dart однопоточный по своей природе, но поддерживает конкурентность через изоляты — независимые рабочие единицы, которые не разделяют память. Каждый изолят имеет собственную память и очередь событий, а обмен данными между ними происходит только через передачу сообщений. Это исключает гонки данных и упрощает написание параллельного кода.

Изоляты особенно полезны для выполнения тяжёлых вычислений, которые могут заблокировать основной поток UI. Например, в мобильном приложении на Flutter фоновый изолят может обрабатывать изображение, не замедляя интерфейс.


Инструментарий и экосистема

Dart поставляется с мощным набором инструментов: компилятором, виртуальной машиной (Dart VM), пакетным менеджером pub и интеграцией с популярными IDE. Пакеты Dart публикуются в реестре pub.dev, где доступны тысячи библиотек — от утилит для работы с датами до полноценных фреймворков.

Компиляция Dart возможна в несколько целей:

  • JIT (Just-In-Time) — для быстрой разработки с горячей перезагрузкой.
  • AOT (Ahead-Of-Time) — для выпуска нативных приложений с высокой производительностью.
  • JavaScript — для запуска в браузере.

Эта гибкость делает Dart универсальным языком, подходящим как для клиентской, так и для серверной разработки.


Модульность и организация кода

Dart поощряет чёткое разделение кода на логические единицы — библиотеки. Каждый файл Dart автоматически является библиотекой, даже если в нём не указано явное объявление library. Это позволяет легко импортировать функциональность из других файлов с помощью директивы import. Например, чтобы использовать математические функции или коллекции, достаточно написать:


import 'dart:math';
import 'dart:collection';

Разбор:

  • dart:math даёт математические функции (sqrt, pow и др.).
  • dart:collection содержит расширенные структуры коллекций.
  • Импорты SDK подключаются без pubspec.yaml.

Стандартная библиотека Dart разделена на тематические модули — dart:core содержит базовые типы и функции, dart:async — инструменты для асинхронности, dart:io — операции ввода-вывода, dart:convert — кодирование и декодирование данных. Все эти модули доступны без установки дополнительных пакетов.

Помимо стандартных библиотек, Dart поддерживает импорт сторонних пакетов через pubspec.yaml — файл конфигурации проекта. После добавления зависимости и выполнения команды dart pub get, любой компонент пакета становится доступен для импорта:


import 'package:http/http.dart' as http;

Разбор:

  • package:http/... — импорт внешней зависимости из pub.dev.
  • as http задаёт префикс для вызовов (http.get, http.post).
  • Так снижается риск конфликта имён между библиотеками.

Ключевое слово as создаёт псевдоним, что помогает избежать конфликтов имён. Также можно использовать show и hide, чтобы импортировать только нужные части библиотеки или скрыть нежелательные:


import 'utils.dart' show formatDate, validateEmail;

Разбор:

  • show formatDate, validateEmail импортирует только перечисленные символы.
  • Это уменьшает "засорение" пространства имён в файле.
  • Альтернатива — hide, если нужно импортировать всё, кроме части API.

Такой подход повышает читаемость и уменьшает объём загружаемого кода.


Генерики и параметризованные типы

Dart поддерживает обобщённое программирование через генерики. Это позволяет создавать классы, интерфейсы и функции, которые работают с любыми типами, сохраняя при этом безопасность типов. Например, список целых чисел объявляется как List<int>, а словарь строковых ключей и значений — как Map<String, String>.

Генерики особенно полезны при создании переиспользуемых структур данных. Рассмотрим простой контейнер:

class Box<T> {
final T value;
const Box(this.value);

T getValue() => value;
}

Разбор:

  • Box<T> — обобщённый класс с параметром типа T.
  • Поле value хранит данные конкретного типа, заданного при создании Box.
  • getValue() возвращает значение без приведения типов.

Здесь T — параметр типа. При создании экземпляра указывается конкретный тип:

var stringBox = Box<String>('Привет');
var numberBox = Box<int>(42);

Разбор:

  • Box<String>('Привет') создаёт контейнер только для строк.
  • Box<int>(42) — отдельный контейнер только для int.
  • Компилятор не позволит положить число в stringBox без ошибки типизации.

Компилятор гарантирует, что в stringBox нельзя случайно поместить число, а метод getValue() всегда вернёт значение ожидаемого типа. Это устраняет необходимость в приведении типов и предотвращает ошибки на этапе выполнения.


Метапрограммирование и аннотации

Dart предоставляет ограниченные, но эффективные средства метапрограммирования через аннотации и рефлексию. Аннотации — это метаданные, которые можно применять к классам, методам, переменным и другим элементам кода. Они не влияют на выполнение программы напрямую, но могут использоваться инструментами во время сборки или анализа.

Например, аннотация @deprecated помечает устаревший API:

@deprecated
void oldMethod() {
// ...
}

Разбор:

  • @deprecated помечает API как устаревший для анализатора и IDE.
  • Сам по себе код выполняется, но инструменты предупреждают разработчика.
  • Аннотации часто используют генераторы кода и линтеры на этапе сборки.

Современный подход к метапрограммированию в Dart — это code generation. Специальные генераторы, запускаемые через build_runner, анализируют исходный код и создают дополнительные файлы с шаблонным кодом — сериализаторы, мапперы, адаптеры. Это позволяет избежать ручного написания повторяющейся логики и сохранить производительность, так как всё генерируется на этапе компиляции, а не во время выполнения.


Стандартная библиотека и её возможности

Стандартная библиотека Dart — это фундамент, на котором строятся все приложения. Она включает в себя не только базовые типы, но и мощные утилиты для работы с датами, регулярными выражениями, URI, JSON, файловой системой и сетевыми соединениями. Все эти компоненты тщательно оптимизированы и документированы.

Работа с JSON, например, осуществляется через функции jsonEncode и jsonDecode из dart:convert. Хотя Dart не имеет встроенной поддержки автоматической сериализации объектов, паттерны преобразования легко реализуются вручную или с помощью генераторов кода.

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


Особенности Dart в контексте Flutter

Хотя Dart — самостоятельный язык, его наибольшее распространение получило в связке с Flutter. В этом контексте многие особенности языка раскрываются особенно ярко. Декларативный стиль UI, реактивность, горячая перезагрузка, эффективное управление состоянием — всё это опирается на возможности Dart — быструю компиляцию, null safety, асинхронность, генерики и работу с коллекциями.

Например, виджеты в Flutter — это обычные классы Dart. Их дерево строится с помощью конструкторов и литералов, а изменения состояния вызывают пересоздание частей дерева. Благодаря эффективному diff-алгоритму и компиляции в нативный код, такие операции выполняются мгновенно.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.