Методы-расширения (далее расширения) позволяют добавлять функции к 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 "подставляется" на основе статического типа списка, для которого вызван метод из расширения.
