Методы-расширения (далее расширения) позволяют добавлять функции к API существующих библиотек. Возможно, вы даже использовали их, не подозревая об этом. Например, когда автодополнение кода в IDE предлагает вам наряду с обычными методами еще и "расширяющие", именно так они и работают.

Обзор

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

Рассмотрим пример кода, который разбирает строку, пытаясь преобразовать ее в целое число:

int.parse('42')

Было бы удобнее и компактнее записать это напрямую с типом String, а не int:

'42'.parseInt()

Чтобы сделать такое преобразование доступным, вы можете импортировать библиотеку, содержащую расширение класса String:

import 'string_apis.dart';
// ···
print('42'.parseInt()); // Используем расширяющий метод.

Расширения могут определять не только методы, но и другие члены класса, такие как геттеры, сеттеры и операторы. Кроме того, можно давать расширениям собственные имена, что помогает разрешать конфликты имен API. Вот как можно реализовать метод parseInt(), используя расширение (под названием NumberParsing), работающее со строками:

// lib/string_apis.dart
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

Далее описано, как использовать расширения, а затем – как их реализовывать.

Использование расширений

Как и любой код Dart, расширения содержатся в библиотеках. Вы уже видели, как их использовать: просто импортируете библиотеку, которая содержит расширение, и пользуетесь им как обычным методом:

// Импорт библиотеки с расширением для типа String.
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // Обычный метод String.
print('42'.parseInt()); // Расширяющий метод.

Для простого использования расширений большего обычно и не требуется. Однако, по мере написания кода вам может понадобиться узнать, как расширяющие методы взаимодействуют со статическими типами (в отличие от dynamic) и как решать конфликты API.

Статические типы и dynamic

Нельзя вызвать метод-расширение для переменной типа dynamic. Например, этот код приведет к ошибке во время выполнения:

dynamic d = '2';
print(d.parseInt()); // Ошибка выполнения: NoSuchMethodError

А вот с выводом типов Dart (type inference) расширения работают. Следующий код не выдаст ошибок, так как в случае с переменной v вывод типов сработает как ожидается и определит ее тип как String:

var v = '2';
print(v.parseInt()); // Вывод: 2

Ключевое отличие от работы с типом dynamic состоит в том, что разрешение расширения производится относительно статического типа получателя (receiver). Именно благодаря тому, что разрешение методов происходит статически, вызов ра��ширяющих методов выполняется так же быстро, как вызов обычных статических функций.

Узнать больше о статических типах и dynamic можно в разделе системы типов Dart в документации.

Разрешение конфликтов API

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

Один из них – указать при импорте расширения, какие члены API вы хотите сделать видимыми, а какие – скрыть, используя сочетание show и hide:

// Определяет расширяющий метод String.parseInt().
import 'string_apis.dart';
// Также определяет parseInt(),  но скрывая NumberParsing2
// и, соответственно, его метод.
import 'string_apis_2.dart' hide NumberParsing2;
// ···
// Используется parseInt(), определенный в 'string_apis.dart'.
print('42'.parseInt());

Другой вариант – применить расширение явно. В этом случае код будет выглядеть так, словно расширение – это класс-оболочка (wrapper class):

// Обе библиотеки определяют расширения для String, содержащие parseInt(),
// но имеют разные имена расширений.
import 'string_apis.dart'; // Содержит расширение NumberParsing.
import 'string_apis_2.dart'; // Содержит расширение NumberParsing2.
// ···
// print('42'.parseInt()); // Не сработает.
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());

Следующий способ применим, когда имя расширения и/или метода совпадает: импорт расширения с префиксом

// Обе библиотеки содержат расширения, называющиеся NumberParsing,
//внутри которых есть метод parseInt(). Однако, только одно из них
//(в 'string_apis_3.dart') содержит еще и метод parseNum().
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;
// ···
// print('42'.parseInt()); // Не сработает.
// Использование расширения ParseNumbers из string_apis.dart.
print(NumberParsing('42').parseInt());
// Использование ParseNumbers из string_apis_3.dart.
print(rad.NumberParsing('42').parseInt());
// Метод parseNum() определен только в string_apis_3.dart
print('42'.parseNum());

Обратите внимание, вызывать расширяющие методы можно неявно, даже если импорт осуществлен с префиксом. Вам понадобится указать префикс только если необходимо явно вызвать расширение, избегая тем самым конфликта имен.

Реализация расширений

Расширяющие методы создаются с помощью следующего синтаксиса:

extension <имя_расширения>? on <тип> {
  (<определения_членов>)*
}

Например, вот так можно реализовать расширения для класса String:

// lib/string_apis.dart
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
double parseDouble() {
return double.parse(this);
}
}

Членами расширения могут быть методы, геттеры, сеттеры или операторы. Расширения также могут содержать статические поля и статические вспомогательные методы. Обращение к статическим членам за пределами объявления самого расширения осуществляется через имя расширения, аналогично переменным и методам классов.

Безымянные расширения

При объявлении расширения вы можете не указывать его имя. Такие "безымянные" расширения будут видны только в библиотеке, где они объявлены. Поскольку у них нет имени, они не могут использоваться для явного разрешения конфликтов API.

extension on String {
bool get isBlank => trim().isEmpty;
}

Важное примечание: к статическим членам безымянного расширения можно обратиться только изнутри самого расширения.

Реализация универсальных (generic) расширений

Расширения могут содержать универсальные (generic) параметры типов. К примеру, вот код, расширяющий встроенный тип List<T> геттером, оператором и методом:

extension MyFancyList<T> on List<T> {
int get doubleLength => length * 2;
List<T> operator -() => reversed.toList();
List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

Тип T "подставляется" на основе статического типа списка, для которого вызван метод из расширения.