5.22. Функции
Функции
Что такое функция
Функция — это именованный фрагмент кода, который принимает входные данные, выполняет определённую последовательность действий и может возвращать результат. Каждая функция имеет имя, список параметров (возможно, пустой), тело и тип возвращаемого значения. Даже если функция ничего не возвращает явно, она всё равно имеет тип void или, в некоторых случаях, dynamic.
В Dart функции являются полноценными объектами первого класса. Это означает, что функцию можно присвоить переменной, передать как аргумент другой функции, вернуть из функции или сохранить в структуре данных. Такой подход открывает широкие возможности для функционального программирования, создания замыканий и реализации гибких архитектурных решений.
Объявление функции
Самый распространённый способ объявления функции в Dart выглядит следующим образом:
возвращаемый_тип имя_функции(список_параметров) {
// тело функции
}
Например:
int add(int a, int b) {
return a + b;
}
Эта функция называется add, принимает два целочисленных параметра a и b, выполняет их сложение и возвращает результат типа int. Все элементы объявления здесь обязательны для понимания контракта функции: кто её вызывает, знает, какие аргументы передавать и какой тип результата ожидать.
Dart поддерживает вывод типов, поэтому в некоторых случаях можно опустить явное указание типа возвращаемого значения или типов параметров. Однако в продуманном коде рекомендуется всегда указывать типы явно. Это повышает читаемость, помогает компилятору находить ошибки на раннем этапе и делает интерфейс функции более предсказуемым.
Тип возвращаемого значения
Каждая функция в Dart имеет определённый тип возвращаемого значения. Если функция должна вернуть результат, этот тип указывается перед её именем. Если функция предназначена только для выполнения побочных эффектов — например, вывода в консоль, изменения состояния объекта или записи в файл — её тип возвращаемого значения указывается как void.
Пример функции с типом void:
void logMessage(String message) {
print('Лог: $message');
}
Такая функция ничего не возвращает. Попытка использовать её результат в выражении приведёт к ошибке времени компиляции.
Важно отметить, что в Dart даже функции с типом void технически могут содержать оператор return без значения. Это допустимо и используется для досрочного завершения выполнения функции.
Параметры функции
Параметры функции определяют данные, которые функция ожидает получить при вызове. В Dart существует несколько видов параметров: обязательные, именованные и параметры со значениями по умолчанию.
Обязательные позиционные параметры
Это наиболее простой и часто используемый тип параметров. Они перечисляются в скобках после имени функции и должны быть переданы в том же порядке, в котором объявлены.
String greet(String name, int age) {
return 'Привет, $name! Тебе $age лет.';
}
При вызове такой функции необходимо передать ровно два аргумента: сначала строку, затем число. Нарушение порядка или количества приведёт к ошибке.
Именованные параметры
Именованные параметры заключаются в фигурные скобки {} и позволяют вызывать функцию, явно указывая имя каждого аргумента. Это делает вызов более читаемым, особенно когда параметров много или некоторые из них необязательны.
void configure({String host = 'localhost', int port = 8080, bool ssl = false}) {
print('Подключение к $host:$port, SSL: $ssl');
}
Вызов такой функции может выглядеть так:
configure(host: 'api.example.com', ssl: true);
Здесь параметр port не указан, поэтому будет использовано значение по умолчанию — 8080. Именованные параметры всегда необязательны, если для них задано значение по умолчанию. Если значение по у defaults не задано, параметр всё равно остаётся необязательным, но его тип должен допускать значение null, либо вызов без него вызовет ошибку.
Параметры со значениями по умолчанию
Как показано выше, именованным параметрам можно присваивать значения по умолчанию с помощью оператора =. То же самое применимо и к позиционным параметрам, если они заключены в квадратные скобки [].
String introduce(String name, [String title = 'Гость']) {
return '$title $name';
}
Теперь функцию можно вызывать с одним или двумя аргументами:
print(introduce('Анна')); // Гость Анна
print(introduce('Борис', 'Инженер')); // Инженер Борис
Квадратные скобки обозначают, что параметры внутри них необязательны. Они должны располагаться после всех обязательных позиционных параметров.
Тело функции и оператор return
Тело функции — это блок кода, заключённый в фигурные скобки. В нём описывается вся логика, которую функция должна выполнить. Внутри тела могут находиться переменные, циклы, условные конструкции, вызовы других функций и любые другие допустимые в Dart выражения.
Оператор return завершает выполнение функции и передаёт управление обратно вызывающему коду. Если функция имеет непустой тип возвращаемого значения, каждый путь выполнения в теле функции должен содержать оператор return с выражением соответствующего типа.
Dart также поддерживает сокращённую запись функции, если её тело состоит из одного выражения. В этом случае используется «стрелочная» нотация (=>):
int square(int x) => x * x;
Это эквивалентно полной форме:
int square(int x) {
return x * x;
}
Стрелочная нотация особенно удобна для коротких вспомогательных функций, коллбэков и лямбда-выражений.
Функции как объекты
В Dart функции — это объекты типа Function. Это позволяет обращаться с ними так же, как с любыми другими значениями. Например, можно создать переменную, которая ссылается на функцию:
int Function(int, int) mathOperation = add;
Здесь mathOperation — переменная, тип которой описан как функция, принимающая два int и возвращающая int. Эту переменную можно вызывать как обычную функцию:
int result = mathOperation(3, 4); // 7
Такой подход широко используется при передаче функций в качестве аргументов, например, в методы коллекций:
List<int> numbers = [1, 2, 3, 4];
List<int> doubled = numbers.map((x) => x * 2).toList();
Здесь (x) => x * 2 — это анонимная функция, переданная методу map. Она применяется к каждому элементу списка.
Анонимные функции
Анонимные функции — это функции без имени. Они часто создаются «на лету» и используются там, где требуется кратковременный коллбэк или обработчик. Синтаксис анонимной функции аналогичен обычной, но без имени:
(numbers).forEach((number) {
print(number);
});
Анонимные функции могут быть как многострочными (в фигурных скобках), так и однострочными (со стрелкой). Они захватывают переменные из окружающей области видимости, образуя замыкания.
Замыкания
Замыкание — это функция, которая имеет доступ к переменным из внешней области видимости, даже после того, как эта область уже завершила своё выполнение. В Dart замыкания создаются автоматически, когда анонимная или вложенная функция ссылается на переменные, объявленные вне её тела.
Пример:
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
Здесь возвращаемая функция «запоминает» переменную count, объявленную в makeCounter. Каждый вызов counter() увеличивает одно и то же значение, несмотря на то, что makeCounter уже завершил работу.
Замыкания — мощный инструмент для инкапсуляции состояния, создания фабрик функций и реализации функциональных паттернов.
Функции высшего порядка
Функция высшего порядка — это функция, которая принимает другую функцию в качестве аргумента или возвращает функцию в качестве результата. В Dart такие функции встречаются повсеместно, особенно при работе с коллекциями, обработке событий и реализации асинхронной логики.
Пример передачи функции как аргумента:
void repeatAction(int times, void action()) {
for (int i = 0; i < times; i++) {
action();
}
}
void sayHello() {
print('Привет!');
}
repeatAction(3, sayHello);
Здесь repeatAction — функция высшего порядка. Она получает функцию action, не имеющую параметров и не возвращающую значение, и вызывает её указанное количество раз. Такой подход позволяет отделить логику повторения от конкретного действия, делая код более универсальным.
Аналогично, функция может возвращать другую функцию:
Function makeGreeter(String greeting) {
return (String name) => '$greeting, $name!';
}
var greetHello = makeGreeter('Здравствуй');
print(greetHello('Мария')); // Здравствуй, Мария!
В этом примере makeGreeter создаёт и возвращает новую функцию, зависящую от значения greeting. Полученная функция сохраняет доступ к переменной greeting даже после завершения работы makeGreeter — это замыкание, упомянутое ранее.
Рекурсия
Рекурсия — это техника, при которой функция вызывает саму себя для решения подзадачи. В Dart рекурсия применяется так же, как и в других языках программирования, но требует осторожности: каждый рекурсивный вызов добавляет новый фрейм в стек вызовов, и чрезмерная глубина может привести к переполнению стека.
Классический пример — вычисление факториала:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Эта функция корректно работает для небольших значений n. Однако для больших чисел предпочтительнее использовать итеративный подход или хвостовую рекурсию, если язык её оптимизирует (Dart не гарантирует оптимизацию хвостовой рекурсии).
Рекурсия особенно полезна при работе с древовидными структурами данных, например, при обходе файловой системы, парсинге вложенных JSON-объектов или реализации алгоритмов на графах.
Асинхронные функции
Dart активно использует асинхронную модель выполнения, основанную на событиях и futures. Асинхронные функции позволяют выполнять длительные операции — такие как сетевые запросы, чтение файлов или задержки — без блокировки основного потока выполнения.
Объявление асинхронной функции начинается с ключевого слова async, а внутри тела используется await для ожидания завершения асинхронной операции:
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Данные получены';
}
void main() async {
print('Начало');
String result = await fetchData();
print(result);
print('Конец');
}
Тип возвращаемого значения такой функции — Future<T>, где T — тип результата. Если функция ничего не возвращает, её тип — Future<void>.
Асинхронные функции интегрированы в систему типов Dart и могут использоваться в выражениях, передаваться как аргументы и возвращаться из других функций. Они также совместимы с обработкой ошибок через try-catch, что делает управление исключениями в асинхронном коде интуитивным.
Функции в контексте классов
В Dart функции, объявленные внутри класса, называются методами. Они имеют доступ к полям и другим методам того же объекта через ключевое слово this. Методы могут быть экземплярными, статическими или абстрактными.
Экземплярный метод:
class Calculator {
int add(int a, int b) {
return a + b;
}
}
Статический метод принадлежит не экземпляру, а самому классу и вызывается без создания объекта:
class MathUtils {
static int square(int x) => x * x;
}
print(MathUtils.square(5)); // 25
Абстрактные методы объявляются в абстрактных классах или интерфейсах и не содержат реализации. Их обязаны переопределить подклассы.
Кроме того, Dart поддерживает геттеры и сеттеры — специальные методы, которые выглядят как свойства, но позволяют выполнять произвольную логику при чтении или записи:
class Circle {
double radius;
Circle(this.radius);
double get area => 3.14159 * radius * radius;
set area(double value) {
radius = (value / 3.14159).sqrt();
}
}
Здесь area ведёт себя как поле, но на самом деле вызывает соответствующий метод.
Сигнатуры функций и typedef
Иногда бывает полезно дать имя сложному типу функции. Для этого в Dart используется typedef:
typedef Operation = int Function(int, int);
Operation add = (a, b) => a + b;
Operation multiply = (a, b) => a * b;
Теперь Operation — это псевдоним для типа функции, принимающей два int и возвращающей int. Это упрощает объявление переменных, параметров и возвращаемых типов, делая код чище и понятнее.
typedef также поддерживает именованные параметры:
typedef ConfigCallback = void Function({String host, int port});
Такие сигнатуры часто встречаются в API фреймворков, особенно при конфигурации компонентов или регистрации обработчиков.
Особенности в Flutter и экосистеме Dart
В контексте разработки под Flutter функции играют центральную роль в построении пользовательского интерфейса. Виджеты часто принимают функции в качестве коллбэков — например, обработчики нажатий кнопок:
ElevatedButton(
onPressed: () {
print('Кнопка нажата');
},
child: Text('Нажми меня'),
)
Здесь анонимная функция передаётся в параметр onPressed. Такой подход позволяет легко связывать логику с действиями пользователя, сохраняя декларативный стиль описания интерфейса.
Кроме того, функции используются для построения маршрутов, анимаций, потоков данных (Stream) и реактивных состояний (StatefulWidget, Provider, Riverpod). Понимание их природы как объектов первого класса критически важно для эффективной работы с современными Dart-приложениями.