Функции и асинхронность
О чём эта статья
Функции первого класса, async/await, Future, Stream, Isolate.run для тяжёлой работы.
Дальше: консоль и HTTP, Flutter, галерея виджетов (Lab) — FutureBuilder и загрузка списка с API.
Функции и асинхронность
Что такое функция
Функция — это именованный фрагмент кода, который принимает входные данные, выполняет определённую последовательность действий и может возвращать результат. Каждая функция имеет имя, список параметров (возможно, пустой), тело и тип возвращаемого значения. Даже если функция ничего не возвращает явно, она всё равно имеет тип void или, в некоторых случаях, dynamic.
В Dart функции являются полноценными объектами первого класса. Это означает, что функцию можно присвоить переменной, передать как аргумент другой функции, вернуть из функции или сохранить в структуре данных. Такой подход открывает широкие возможности для функционального программирования, создания замыканий и реализации гибких архитектурных решений.
Интерактивное демо — вызов функции и стек на примере JavaScript. В Dart объявление другое, но вызов, локальные переменные и возврат устроены так же. Обобщённо: функции в коде.
Play ITЗагрузка интерактивного демо…
Объявление функции
Самый распространённый способ объявления функции в Dart выглядит следующим образом:
возвращаемый_тип имя_функции(список_параметров) {
// тело функции
}
Разбор:
- Общий шаблон объявления функции в Dart.
возвращаемый_тип— тип результата (int,String,void,Future<T>).список_параметров— входные аргументы; может быть пустым.- Тело в
{ ... }содержит исполняемые инструкции.
Например:
int add(int a, int b) {
return a + b;
}
Разбор:
intв начале — тип возвращаемого значения.aиb— обязательные позиционные параметры.return a + bзавершает функцию и отдаёт сумму вызывающему коду.
Эта функция называется add, принимает два целочисленных параметра a и b, выполняет их сложение и возвращает результат типа int. Все элементы объявления здесь обязательны для понимания контракта функции — кто её вызывает, знает, какие аргументы передавать и какой тип результата ожидать.
Dart поддерживает вывод типов, поэтому в некоторых случаях можно опустить явное указание типа возвращаемого значения или типов параметров. Однако в продуманном коде рекомендуется всегда указывать типы явно. Это повышает читаемость, помогает компилятору находить ошибки на раннем этапе и делает интерфейс функции более предсказуемым.
Тип возвращаемого значения
Каждая функция в Dart имеет определённый тип возвращаемого значения. Если функция должна вернуть результат, этот тип указывается перед её именем. Если функция предназначена только для выполнения побочных эффектов — например, вывода в консоль, изменения состояния объекта или записи в файл — её тип возвращаемого значения указывается как void.
Пример функции с типом void:
void logMessage(String message) {
print('Лог: $message');
}
Разбор:
void— функция выполняет действие, но не возвращает полезное значение.messageпередаёт текст для вывода.print— побочный эффект (запись в консоль).- Результат вызова нельзя использовать в выражении.
Такая функция ничего не возвращает. Попытка использовать её результат в выражении приведёт к ошибке времени компиляции.
Важно отметить, что в Dart даже функции с типом void технически могут содержать оператор return без значения. Это допустимо и используется для досрочного завершения выполнения функции.
Параметры функции
Параметры функции определяют данные, которые функция ожидает получить при вызове. В Dart существует несколько видов параметров: обязательные, именованные и параметры со значениями по умолчанию.
Обязательные позиционные параметры
Это наиболее простой и часто используемый тип параметров. Они перечисляются в скобках после имени функции и должны быть переданы в том же порядке, в котором объявлены.
String greet(String name, int age) {
return 'Привет, $name! Тебе $age лет.';
}
Разбор:
- Два позиционных параметра разных типов.
$nameи$age— интерполяция в строке.- Порядок аргументов при вызове должен совпадать с объявлением.
При вызове такой функции необходимо передать ровно два аргумента: сначала строку, затем число. Нарушение порядка или количества приведёт к ошибке.
Именованные параметры
Именованные параметры заключаются в фигурные скобки {} и позволяют вызывать функцию, явно указывая имя каждого аргумента. Это делает вызов более читаемым, особенно когда параметров много или некоторые из них необязательны.
void configure({String host = 'localhost', int port = 8080, bool ssl = false}) {
print('Подключение к $host:$port, SSL: $ssl');
}
Разбор:
- Параметры в
{...}— именованные. - Значения по умолчанию подставляются, если аргумент не передан.
- Вызов читается как
configure(host: ..., ssl: ...).
Вызов такой функции может выглядеть так:
configure(host: 'api.example.com', ssl: true);
Разбор:
- Передаются только нужные именованные аргументы.
portне указан — используется8080из объявления.- Порядок именованных аргументов не важен.
Здесь параметр port не указан — подставится 8080. Именованный параметр необязателен, если есть значение по умолчанию или тип допускает null (int? port). Чтобы сделать именованный параметр обязательным, используют required:
void connect({required String host, int port = 8080}) { /* ... */ }
Разбор:
requiredделаетhostобязательным именованным параметром.portостаётся необязательным с дефолтом8080.- Компилятор не даст вызвать функцию без
host:.
Параметры со значениями по умолчанию
Как показано выше, именованным параметрам можно присваивать значения по умолчанию с помощью оператора =. То же самое применимо и к позиционным параметрам, если они заключены в квадратные скобки [].
String introduce(String name, [String title = 'Гость']) {
return '$title $name';
}
Разбор:
[String title = 'Гость']— необязательный позиционный параметр.- Можно вызвать с одним или двумя аргументами.
- Дефолт
'Гость'применяется, если второй аргумент не передан.
Теперь функцию можно вызывать с одним или двумя аргументами:
print(introduce('Анна')); // Гость Анна
print(introduce('Борис', 'Инженер')); // Инженер Борис
Разбор:
- Первый вызов использует title по умолчанию.
- Второй явно передаёт
'Инженер'. - Комментарии фиксируют ожидаемый результат.
Квадратные скобки обозначают, что параметры внутри них необязательны. Они должны располагаться после всех обязательных позиционных параметров.
Тело функции и оператор return
Тело функции — это блок кода, заключённый в фигурные скобки. В нём описывается вся логика, которую функция должна выполнить. Внутри тела могут находиться переменные, циклы, условные конструкции, вызовы других функций и любые другие допустимые в Dart выражения.
Оператор return завершает выполнение функции и передаёт управление обратно вызывающему коду. Если функция имеет непустой тип возвращаемого значения, каждый путь выполнения в теле функции должен содержать оператор return с выражением соответствующего типа.
Dart также поддерживает сокращённую запись функции, если её тело состоит из одного выражения. В этом случае используется "стрелочная" нотация (=>):
int square(int x) => x * x;
Разбор:
- Стрелочная форма
=>для тела из одного выражения. - Результат выражения автоматически возвращается.
- Эквивалент
{ return x * x; }.
Это эквивалентно полной форме:
int square(int x) {
return x * x;
}
Разбор:
- Полная форма с явным
return. - Предпочтительна, если в теле больше одной операции.
- Логически идентична стрелочной записи выше.
Стрелочная нотация особенно удобна для коротких вспомогательных функций, коллбэков и лямбда-выражений.
Функции как объекты
В Dart функции — это объекты типа Function. Это позволяет обращаться с ними так же, как с любыми другими значениями. Например, можно создать переменную, которая ссылается на функцию:
int Function(int, int) mathOperation = add;
Разбор:
- Тип переменной описывает сигнатуру функции (два
int→int). - В переменную можно положить ссылку на
addили другую совместимую функцию. - Функции в Dart — значения первого класса.
Здесь mathOperation — переменная, тип которой описан как функция, принимающая два int и возвращающая int. Эту переменную можно вызывать как обычную функцию:
int result = mathOperation(3, 4); // 7
Разбор:
- Переменная-функция вызывается как обычная функция.
mathOperation(3, 4)делегирует вызов функцииadd.- Результат
7сохраняется вresult.
Такой подход широко используется при передаче функций в качестве аргументов, например, в методы коллекций:
List<int> numbers = [1, 2, 3, 4];
List<int> doubled = numbers.map((x) => x * 2).toList();
Разбор:
mapприменяет функцию к каждому элементу списка.(x) => x * 2— анонимная функция-удвоитель.toList()материализует результат в новыйList<int>.
Здесь (x) => x * 2 — это анонимная функция, переданная методу map. Она применяется к каждому элементу списка.
Анонимные функции
Анонимные функции — это функции без имени. Они часто создаются "на лету" и используются там, где требуется кратковременный коллбэк или обработчик. Синтаксис анонимной функции аналогичен обычной, но без имени:
(numbers).forEach((number) {
print(number);
});
Разбор:
forEachвызывает переданную функцию для каждого элемента.(number) { ... }— многострочная анонимная функция.- Порядок вывода совпадает с порядком элементов в списке.
Анонимные функции могут быть как многострочными (в фигурных скобках), так и однострочными (со стрелкой). Они захватывают переменные из окружающей области видимости, образуя замыкания.
Замыкания
Замыкание — это функция, которая имеет доступ к переменным из внешней области видимости, даже после того, как эта область уже завершила своё выполнение. В Dart замыкания создаются автоматически, когда анонимная или вложенная функция ссылается на переменные, объявленные вне её тела.
Пример:
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
Разбор:
makeCounterсоздаёт локальную переменнуюcount.- Возвращаемая функция замыкает
countи увеличивает её при каждом вызове. - Состояние сохраняется между вызовами
counter(), хотяmakeCounterуже завершился.
Здесь возвращаемая функция "запоминает" переменную 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.- Цикл вызывает
action()заданное количество раз. sayHelloпередаётся как конкретное действие без дублирования цикла.
Здесь repeatAction — функция высшего порядка. Она получает функцию action, не имеющую параметров и не возвращающую значение, и вызывает её указанное количество раз. Такой подход позволяет отделить логику повторения от конкретного действия, делая код более универсальным.
Аналогично, функция может возвращать другую функцию:
Function makeGreeter(String greeting) {
return (String name) => '$greeting, $name!';
}
var greetHello = makeGreeter('Здравствуй');
print(greetHello('Мария')); // Здравствуй, Мария!
Разбор:
makeGreeterвозвращает новую функцию, зависящую отgreeting.greetHello('Мария')подставляет имя в шаблон приветствия.greetingживёт в замыкании после выхода изmakeGreeter.
В этом примере makeGreeter создаёт и возвращает новую функцию, зависящую от значения greeting. Полученная функция сохраняет доступ к переменной greeting даже после завершения работы makeGreeter — это замыкание, упомянутое ранее.
Рекурсия
Рекурсия — это техника, при которой функция вызывает саму себя для решения подзадачи. В Dart рекурсия применяется так же, как и в других языках программирования, но требует осторожности: каждый рекурсивный вызов добавляет новый фрейм в стек вызовов, и чрезмерная глубина может привести к переполнению стека.
Классический пример — вычисление факториала:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Разбор:
- Базовый случай
n <= 1останавливает рекурсию. - Рекурсивный шаг умножает
nнаfactorial(n - 1). - Глубокая рекурсия увеличивает стек вызовов — для больших
nлучше цикл.
Эта функция корректно работает для небольших значений 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('Конец');
}
Разбор:
async/awaitделают асинхронный код похожим на синхронный.Future.delayedимитирует долгую операцию без блокировки изолята.main() asyncпозволяет использоватьawaitв точке входа.- Порядок вывода: "Начало" → пауза → "Данные получены" → "Конец".
Тип возвращаемого значения такой функции — Future<T>, где T — тип результата. Если функция ничего не возвращает, её тип — Future<void>.
Асинхронные функции интегрированы в систему типов Dart и могут использоваться в выражениях, передаваться как аргументы и возвращаться из других функций. Они также совместимы с обработкой ошибок через try-catch, что делает управление исключениями в асинхронном коде интуитивным.
Тяжёлую работу (обработка изображения, парсинг большого файла) выносят в отдельный изолят, чтобы не блокировать UI и event loop:
import 'dart:isolate';
Future<int> sumLargeList(List<int> data) {
return Isolate.run(() => data.reduce((a, b) => a + b));
}
Разбор:
Isolate.runвыполняет функцию в отдельном изоляте.reduceсуммирует элементы списка.- Возвращается
Future<int>— результат приходит асинхронно. - Подходит для тяжёлых вычислений без подвисания UI.
Во Flutter для тех же задач часто используют обёртку compute из package:flutter/foundation.dart — она запускает функцию в фоновом изолете и возвращает Future.
Функции в контексте классов
В Dart функции, объявленные внутри класса, называются методами. Они имеют доступ к полям и другим методам того же объекта через ключевое слово this. Методы могут быть экземплярными, статическими или абстрактными.
Экземплярный метод:
class Calculator {
int add(int a, int b) {
return a + b;
}
}
Разбор:
- Метод
addвызывается на экземпляре:Calculator().add(1, 2). - Внутри метода доступны поля объекта через
this(если они объявлены).
Статический метод принадлежит не экземпляру, а самому классу и вызывается без создания объекта:
class MathUtils {
static int square(int x) => x * x;
}
print(MathUtils.square(5)); // 25
Разбор:
staticметод принадлежит классу, а не экземпляру.- Вызов без создания объекта:
MathUtils.square(5). - Удобно для утилит без состояния.
Абстрактные методы объявляются в абстрактных классах или интерфейсах и не содержат реализации. Их обязаны переопределить подклассы.
Кроме того, Dart поддерживает геттеры и сеттеры — специальные методы, которые выглядят как свойства, но позволяют выполнять произвольную логику при чтении или записи:
Код ITЗагрузка примера кода…
Разбор:
get areaвычисляет площадь по текущемуradius.set areaпересчитываетradiusиз желаемой площади.- Снаружи
circle.areaвыглядит как свойство, но это методы. piиsqrtимпортируются изdart:math.
Здесь area ведёт себя как поле, но на самом деле вызывает соответствующий метод.
Сигнатуры функций и typedef
Иногда бывает полезно дать имя сложному типу функции. Для этого в Dart используется typedef:
typedef Operation = int Function(int, int);
Operation add = (a, b) => a + b;
Operation multiply = (a, b) => a * b;
Разбор:
typedef Operationзадаёт короткое имя для типа функции.addиmultiply— переменные, хранящие совместимые функции.- Упрощает сигнатуры коллбэков и полей в API.
Теперь Operation — это псевдоним для типа функции, принимающей два int и возвращающей int. Это упрощает объявление переменных, параметров и возвращаемых типов, делая код чище и понятнее.
typedef также поддерживает именованные параметры:
typedef ConfigCallback = void Function({String host, int port});
Разбор:
- Псевдоним для функции с именованными параметрами.
- Документирует ожидаемую форму обработчика конфигурации.
- Часто используется в API фреймворков и плагинов.
Такие сигнатуры часто встречаются в API фреймворков, особенно при конфигурации компонентов или регистрации обработчиков.
Особенности в Flutter и экосистеме Dart
В контексте разработки под Flutter функции играют центральную роль в построении пользовательского интерфейса. Виджеты часто принимают функции в качестве коллбэков — например, обработчики нажатий кнопок:
ElevatedButton(
onPressed: () {
print('Кнопка нажата');
},
child: Text('Нажми меня'),
)
Разбор:
onPressedпринимает коллбэк нажатия кнопки.- Анонимная
() { print(...); }выполняется при tap пользователя. child: Text(...)задаёт подпись на кнопке.- Типичный паттерн обработки событий во Flutter.
Здесь анонимная функция передаётся в параметр onPressed. Такой подход позволяет легко связывать логику с действиями пользователя, сохраняя декларативный стиль описания интерфейса.
Кроме того, функции используются для построения маршрутов, анимаций, потоков данных (Stream) и реактивных состояний (StatefulWidget, Provider, Riverpod). Понимание их природы как объектов первого класса критически важно для эффективной работы с современными Dart-приложениями.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.