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

Классы и ООП в Dart

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

:::tip Сначала — общие понятия (раздел 4 «Код») Если ООП для вас новое или вы учите Dart с нуля, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм. Ниже — классы, миксины и интерфейсы в Dart. :::

Классы и ООП в Dart

Dart — объектно-ориентированный язык: каждое значение — объект, корень иерархии — Object. Базовый синтаксис классов уже разобран в основах; здесь — механизмы, которые отличают Dart от «классического» ООП и важны в реальных проектах и во Flutter.

Связанные темы: типы и record, паттерны и sealed, функции и методы.


Поля, конструкторы и this

Класс объединяет состояние (поля) и поведение (методы):

class User {
final String id;
String displayName;

User(this.id, this.displayName);

void rename(String name) {
displayName = name;
}
}

Синтаксис User(this.id, this.displayName) — сокращение: параметры конструктора сразу инициализируют одноимённые поля. Именованные параметры в конструкторе повышают читаемость:

class Config {
final String host;
final int port;

Config({required this.host, this.port = 8080});
}

Инкапсуляция — библиотека и префикс _

В Dart нет ключевых слов public / private на уровне члена. Приватность — по области видимости библиотеки (файла): имя, начинающееся с _, видно только внутри того же файла (или части library, если несколько файлов объединены директивой part).

class Account {
final String _id;
double _balance = 0;

Account(this._id);

void deposit(double amount) {
if (amount <= 0) throw ArgumentError.value(amount);
_balance += amount;
}

double get balance => _balance;
}

Снаружи файла к _balance не обратиться — только через публичный API (deposit, balance). Это защищает инварианты объекта без boilerplate геттеров на каждое поле.


late — отложенная инициализация

Ключевое слово late сообщает компилятору: поле будет присвоено до первого чтения, но не обязательно в конструкторе.

class Report {
late final String title;

void load(Map<String, dynamic> json) {
title = json['title'] as String;
}
}

Варианты:

  • late final — присвоить один раз, затем только читать.
  • late без final — изменяемое поле с отложенным стартом.

Ошибка LateInitializationError возникает, если прочитать late-поле до инициализации. В Flutter late часто применяют для контроллеров (TextEditingController), которые нельзя создать в конструкторе виджета до initState.

Не путать с nullable (String?): late обещает «значение появится позже, но не null», ? — «может быть null».


final, const и неизменяемые объекты

  • final на поле — ссылку переназначить нельзя; для изменяемых коллекций содержимое всё ещё можно менять, если тип коллекции изменяемый.
  • const конструктор — объект создаётся на этапе компиляции и может быть каноническим (один экземпляр на одинаковые аргументы).
class Point {
final double x;
final double y;

const Point(this.x, this.y);
}

const origin = Point(0, 0);

Все поля класса с const-конструктором должны быть final и тоже допускать const-инициализацию. Во Flutter const виджеты уменьшают пересборки дерева — см. Flutter.


Factory-конструкторы

Обычный конструктор всегда создаёт новый объект. factory может вернуть существующий экземпляр или подтип:

class Logger {
static final Map<String, Logger> _cache = {};

final String name;

factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}

Logger._internal(this.name);
}

Типичные случаи factory:

  • кэш и singleton;
  • выбор реализации по аргументу (factory Auth.fromConfig(...)OAuth или Basic);
  • парсинг: factory User.fromJson(Map<String, dynamic> json).

Именованный redirecting factory перенаправляет на другой конструктор:

factory Vector.zero() = Vector.fromValues;

Наследование и абстрактные классы

Один класс наследует один суперкласс (extends). Переопределение методов помечают @override:

abstract class Shape {
double area();

void describe() => print('фигура, площадь $area');
}

class Circle extends Shape {
final double radius;
Circle(this.radius);

@override
double area() => 3.14159 * radius * radius;
}

abstract class нельзя инстанцировать; методы без тела задают контракт. abstract interface class (Dart 3) — вариант для чистого контракта без реализации по умолчанию.


Интерфейсы без ключевого слова interface

Любой класс задаёт неявный интерфейс — набор публичных методов. Реализация через implements (нужно описать все методы заново) или extends (наследование реализации):

abstract class Serializable {
Map<String, dynamic> toJson();
}

class Product implements Serializable {
final String sku;
Product(this.sku);

@override
Map<String, dynamic> toJson() => {'sku': sku};
}

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


Миксины (mixins)

mixin добавляет поведение без цепочки наследования «от животного к млекопитающему». Подключение — with:

mixin Timestamped {
DateTime? updatedAt;
void touch() => updatedAt = DateTime.now();
}

class Document with Timestamped {
final String title;
Document(this.title);
}

Ограничение on задаёт, к каким классам можно примешивать mixin:

mixin Pilot on Vehicle {
void fly() => print('взлёт');
}

Миксины — основа композиции в Dart; не злоупотребляйте глубокими иерархиями extends.


Геттеры, сеттеры и операторы

Свойства могут быть вычисляемыми:

class Rectangle {
final double width, height;
Rectangle(this.width, this.height);

double get area => width * height;
set scale(double factor) {
// в реальном классе поля должны быть изменяемыми или через внутреннее состояние
}
}

Операторы переопределяют через operator:

class Vector {
final int x, y;
const Vector(this.x, this.y);

Vector operator +(Vector other) => Vector(x + other.x, y + other.y);
}

Не забывайте согласованно переопределять == и hashCode, если объекты сравнивают по значению (как для коллекций в Map/Set).


Sealed и final class (Dart 3)

sealed class — закрытая иерархия для исчерпывающего switch. final class запрещает наследование снаружи файла. base / interface — тонкая настройка, кто может расширять или реализовывать тип. В прикладном коде чаще всего встречаются sealed для моделей состояния и final для «листьев» иерархии.


Область видимости

Помимо _ на уровне библиотеки:

  • топ-уровневые функции и переменные в файле — видны всему файлу;
  • локальные — внутри блока if, цикла, метода;
  • статические члены класса — static, принадлежат типу, не экземпляру.

Вложенные функции (как в статье про функции) видят переменные внешней функции — основа замыканий.


ООП и исключения

Методы бросают исключения через throw; перехват — try/catch. Собственный тип:

class ValidationException implements Exception {
final String field;
ValidationException(this.field);

@override
String toString() => 'ошибка поля $field';
}

Dart не объявляет checked exceptions в сигнатуре — контракт ошибок документируют в комментариях и тестах.


Сводка — что запомнить

МеханизмЗачем
_Скрыть детали реализации в файле
lateИнициализация после конструктора
const / finalПредсказуемость и производительность
factoryКэш, подтипы, fromJson
abstractКонтракт без экземпляра
implements / withКонтракт и миксины без лишнего наследования
sealedЗакрытый набор вариантов для switch

Дальше: паттерны для разбора вариантов данных, консоль и HTTP для сервисного слоя, чек-лист для самопроверки.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).