Сокращённая запись через точку (.foo) в Dart
Обзор
Синтаксис сокращённой записи через точку — .foo — позволяет писать более лаконичный Dart-код, опуская имя типа там, где компилятор может вывести его из контекста. Это аккуратная альтернатива полному варианту вида ContextType.foo при обращении к значениям перечислений (enum), статическим членам и конструкторам.
По сути, «точечные» сокращения позволяют начинать выражение с одного из следующих вариантов и при необходимости навешивать на него дальнейшие операции:
Идентификатор:
.myValueВызов конструктора:
.new()Создание константы:
const .myValue()
Краткий пример, как это упрощает присваивание значения enum:
// Используем сокращённую запись через точку для enum:
enum Status { none, running, stopped, paused }
Status currentStatus = .running; // Вместо Status.running
// Используем сокращённую запись через точку для статического метода:
int port = .parse('8080'); // Вместо int.parse('8080')
// Используем сокращённую запись через точку для конструктора:
class Point {
final int x, y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0;
}
Point origin = .origin(); // Вместо Point.origin()
Роль контекстного типа
Сокращённая запись через точку опирается на контекстный тип (context type), чтобы понять, к какому члену нужно обратиться. Контекстный тип — это тип, который Dart ожидает увидеть в данном месте программы, исходя из его окружения.
Например, в выражении:
Status currentStatus = .running;
компилятор знает, что ожидается значение типа Status, и, исходя из этого, интерпретирует .running как Status.running.
Лексическая структура и синтаксис
Сокращение для статического члена — это выражение, которое начинается с ведущей точки (.). Когда тип известен из окружающего контекста, такой синтаксис даёт компактный способ обращаться к статическим членам, конструкторам и значениям enum.
Перечисления (enum)
Основной и настоятельно рекомендуемый сценарий применения сокращённой записи через точку — это работа с enum, особенно в присваиваниях и switch-выражениях, где тип перечисления очевиден.
enum LogLevel { debug, info, warning, error }
/// Возвращает цветовой код для заданного уровня логирования [level].
String colorCode(LogLevel level) {
// Используем сокращённую запись через точку для значений enum в switch:
return switch (level) {
.debug => 'gray', // Вместо LogLevel.debug
.info => 'blue', // Вместо LogLevel.info
.warning => 'orange', // Вместо LogLevel.warning
.error => 'red', // Вместо LogLevel.error
};
}
// Пример использования:
String warnColor = colorCode(.warning); // Вернёт 'orange'
Именованные конструкторы
Сокращённая запись через точку удобна для вызова именованных конструкторов и factory-конструкторов. Этот синтаксис также работает при передаче аргументов типа (generic-параметров) в конструктор обобщённого класса.
class Point {
final double x, y;
const Point(this.x, this.y);
const Point.origin() : x = 0, y = 0; // Именованный конструктор
// Factory-конструктор
factory Point.fromList(List<double> list) {
return Point(list[0], list[1]);
}
}
// Сокращённая запись для именованного конструктора:
Point origin = .origin(); // Вместо Point.origin()
// Сокращённая запись для factory-конструктора:
Point p1 = .fromList([1.0, 2.0]); // Вместо Point.fromList([1.0, 2.0])
// Сокращённая запись для конструктора обобщённого класса:
List<int> intList = .filled(5, 0); // Вместо List.filled(5, 0)
Неименованные конструкторы
Сокращение .new предоставляет компактный способ вызова неименованного конструктора класса. Это удобно при присваивании полям или переменным, тип которых уже явно указан.
Такой синтаксис особенно полезен для «очистки» повторяющихся инициализаций полей класса. Как видно в примере ниже (вариант «после» с сокращённой записью), его можно применять к конструкторам как с аргументами, так и без них. Параметры типа (generic-аргументы) также выводятся из контекста.
Без сокращённой записи:
class _PageState extends State<Page> {
late final AnimationController _animationController = AnimationController(
vsync: this,
);
final ScrollController _scrollController = ScrollController();
final GlobalKey<ScaffoldMessengerState> scaffoldKey =
GlobalKey<ScaffoldMessengerState>();
Map<String, Map<String, bool>> properties = <String, Map<String, bool>>{};
// ...
}
С использованием сокращённой записи:
// Используем сокращённую запись через точку для вызова неименованных конструкторов:
class _PageState extends State<Page> {
late final AnimationController _animationController = .new(vsync: this);
final ScrollController _scrollController = .new();
final GlobalKey<ScaffoldMessengerState> scaffoldKey = .new();
Map<String, Map<String, bool>> properties = .new();
// ...
}
Статические члены
Сокращённую запись через точку можно использовать для вызова статических методов и доступа к статическим полям/геттерам. Компилятор выводит целевой класс из контекстного типа выражения.
// Сокращённая запись для вызова статического метода:
int httpPort = .parse('80'); // Вместо int.parse('80')
// Сокращённая запись для доступа к статическому полю или геттеру:
BigInt bigIntZero = .zero; // Вместо BigInt.zero
Константные выражения
Сокращённую запись через точку можно использовать в константном контексте, если член, к которому идёт обращение, является константой времени компиляции. Это типичный случай для значений enum и вызова const-конструкторов.
enum Status { none, running, stopped, paused }
class Point {
final double x, y;
const Point(this.x, this.y);
const Point.origin() : x = 0.0, y = 0.0;
}
// Сокращённая запись для значения enum:
const Status defaultStatus = .running; // Вместо Status.running
// Сокращённая запись для вызова константного именованного конструктора:
const Point myOrigin = .origin(); // Вместо Point.origin()
// Сокращённая запись в константном литерале коллекции:
const List<Point> keyPoints = [.origin(), .new(1.0, 1.0)];
// Вместо [Point.origin(), Point(1.0, 1.0)]
Правила и ограничения
Сокращённая запись через точку опирается на однозначный контекстный тип, что приводит к ряду конкретных правил и ограничений, о которых важно знать.
Требование однозначного контекстного типа в цепочках
Хотя к выражению с сокращённой записью можно «добавлять» дальнейшие операции (вызовы методов, доступ к полям и т. п.), всё выражение целиком проверяется на соответствие контекстному типу.
Сначала компилятор использует контекст, чтобы определить, к чему именно относится сокращённая запись. Любые последующие операции в цепочке должны возвращать значение, совместимое с этим исходным контекстным типом.
// .fromCharCode(72) даёт строку "H",
// затем на этой строке вызывается метод экземпляра .toLowerCase().
String lowerH = .fromCharCode(72).toLowerCase();
// Вместо String.fromCharCode(72).toLowerCase()
print(lowerH); // Вывод: h
Асимметричные проверки на равенство
Операторы == и != имеют специальное правило для сокращённой записи через точку. Когда сокращение используется непосредственно в правой части проверки на равенство, Dart использует статический тип левой части для определения класса или enum, к которому относится сокращение.
Например, в выражении myColor == .green контекст задаётся типом переменной myColor. Это означает, что компилятор интерпретирует .green как Color.green.
enum Color { red, green, blue }
// Сокращённая запись через точку в выражениях с равенством:
void allowedExamples() {
Color myColor = Color.red;
bool condition = true;
// OK: `myColor` имеет тип `Color`, значит `.green` выводится как `Color.green`.
if (myColor == .green) {
print('The color is green.');
}
// OK: То же работает и с `!=`.
if (myColor != .blue) {
print('The color is not blue.');
}
// OK: Контекст для тернарного оператора задаётся переменной `inferredColor`,
// имеющей тип `Color`.
Color inferredColor = condition ? .green : .blue;
print('Inferred color is $inferredColor');
}
Сокращённая запись через точку должна находиться в правой части операторов == или !=. Сравнение с более сложным выражением, например с тернарным оператором, также не допускается.
// static analysis: failure
enum Color { red, green, blue }
void notAllowedExamples() {
Color myColor = Color.red;
bool condition = true;
// ОШИБКА: сокращение должно быть справа от `==`.
// Для этой возможности оператор `==` в Dart работает несимметрично.
if (.red == myColor) {
print('This will not compile.');
}
// ОШИБКА: справа используется сложное выражение (тернарный оператор),
// которое не может быть корректной целью сокращения в сравнении.
if (myColor == (condition ? .green : .blue)) {
print('This will not compile.');
}
// ОШИБКА: контекстный тип теряется при приведении `myColor` к `Object`.
// Компилятор больше не знает, что `.green` должно означать `Color.green`.
if ((myColor as Object) == .green) {
print('This will not compile.');
}
}
Операторные выражения не могут начинаться с точки
Чтобы избежать потенциальных неоднозначностей разбора синтаксиса в будущем, операторное выражение (expression statement) не может начинаться с токена ..
// static analysis: failure
class Logger {
static void log(String message) {
print(message);
}
}
void main() {
// ОШИБКА: операторное выражение не может начинаться с `.`.
// У компилятора нет контекстного типа (например, присваивания переменной),
// который позволил бы вывести, что `.log` — это Logger.log.
.log('Hello');
}
Ограниченная поддержка объединённых типов (union types)
Поддержка сокращённой записи для некоторых «объединённых» типовых форм ограничена. Для T? и FutureOr<T> есть специальная логика, но не для всех случаев.
Для nullable-типа
T?можно обращаться к статическим членам типаT, но не к членамNull.Для
FutureOr<T>можно обращаться к статическим членамT(в первую очередь для поддержки возвращаемых значенийasync-функций), но нельзя обращаться к статическим членам классаFuture.