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