Функции в Dart

Зачем нужны функции

Зачем нужны функции Проблема повторения кода 1 Проблема повторения кода 2 Проблема повторения кода 3 Проблема повторения кода 4 Решение с функциями Преимущества функций 1 Преимущества функций 2 Преимущества функций 3 Преимущества функций 4

Создание функции

Функция

Функция — это блок кода, который выполняет определенную задачу и может быть вызван по имени. Функции помогают организовывать код и избегать его повторения.

Преимущества функций:

В Dart функции объявляются с использованием ключевого слова void (если функция ничего не возвращает) или указанием типа возвращаемого значения. Сначала пишется тип, затем имя функции и список параметров в круглых скобках. После этого идет тело функции в фигурных скобках {}.

Dart - Простая функция

Светлая тема Темная тема
void greet() { 
  print('Привет!'); 
}

Здесь greet это имя функции, которая выводит на экран слово «Привет!». Она не принимает параметров и не возвращает значения.

Чтобы выполнить код внутри функции, нужно вызвать функцию по её имени.

Dart - Вызов функции

Светлая тема Темная тема
greet(); // Привет!

Функции могут принимать параметры, которые передают данные в функцию.

Функции могут возвращать значение с использованием ключевого слова return.

В этом случае вместо void указывается тип возвращаемого значения.

Функции с параметрами и возвращаемым значением Пример функции с return

Функции с несколькими параметрами

Функции с несколькими параметрами 1 Функции с несколькими параметрами 2

Dart - Функция с несколькими параметрами

Светлая тема Темная тема
int add(int a, int b) {
  return a + b; 
}  

int result = add(3, 5); // Выполнит код 3 + 5 и вернёт значение в переменную
print(result); // 8
Схема работы функции

Стрелочная функция

Если тело функции содержит только одну инструкцию/возвращаемое значение.

Тогда можно убрать ключевое слово return, фигурные скобки и использовать стрелочную функцию.

Стрелочная функция

Dart - Стрелочная функция

Светлая тема Темная тема
int sum(int a, int b) {
  return a + b;
};

int sum(int a, int b) => a + b;

Параметры по умолчанию

Можно назначить значения по умолчанию для параметров функции.

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

Значения по умолчанию

Значения по умолчанию указываются с помощью оператора =

Аргументы нужно обернуть в { } или [ ] скобки и они обязательно должны быть в самом конце списка аргументов

Параметры по умолчанию

Dart - Параметры по умолчанию

Светлая тема Темная тема
String hello(String name, { String message = 'Привет' }) {
  return '$message, $name!';
}

void main() {
  print(hello('Андрей'));
  print(hello( message: 'Здравствуйте', name: 'Наташа'));
}

Результат:

Привет, Андрей!
Здравствуйте, Наташа!

Именованные аргументы

Именованные аргументы

Позволяют указывать имена параметров при вызове функции.

По умолчанию значения передаются параметрам по позиции: первое значение - первому параметру, второе значение - второму параметру и так далее.

Однако, используя именованные аргументы, можно переопределить порядок их передачи параметрам.

Именованные аргументы

Dart - Именованные аргументы

Светлая тема Темная тема
String hello(String name, int age, {String message = "Привет"}) {
  return "$message, меня зовут $name! Мне $age лет";
}

main() {
  print(hello("Маша",18));
  // Привет, меня зовут Маша! Мне 18 лет
  print(hello("Маша", 18, message: "Здравствуйте"));
  // Здравствуйте, меня зовут Маша! Мне 18 лет

  // Именованный параметр не зависит от порядка
  print(hello(message: "Здравствуйте", "Маша", 18));

  // Ошибка! Первый параметр должен быть строкой
  print(hello(18, "Маша"));
}
Именованные аргументы пример 1 Именованные аргументы пример 2 Именованные аргументы пример 3

Создание своей функции

Функция — это блок кода, который выполняет определенную задачу и может быть вызван по имени. Функции помогают организовывать код и избегать его повторения.

Преимущества функций:

В Dart функции объявляются с использованием ключевого слова void (если функция ничего не возвращает) или указанием типа возвращаемого значения.

Сначала пишется тип данных, затем имя функции и список параметров в круглых скобках. После этого идет тело функции в фигурных скобках { }.

Dart - Простая функция

Светлая тема Темная тема
void greet() { 
  print('Привет!'); 
}

Здесь greet это имя функции, которая выводит на экран слово «Привет!». Она не принимает параметров и не возвращает значения.

Чтобы выполнить код внутри функции, нужно вызвать функцию по её имени.

Dart - Вызов функции

Светлая тема Темная тема
greet(); // Привет!

Функции могут принимать параметры, которые передают данные в функцию.

Функции могут возвращать значение с использованием ключевого слова return.

В этом случае вместо void указывается тип возвращаемого значения.

Необязательные параметры

Чтобы параметры по умолчанию передавать как позиционные нужно обернуть их в квадратные скобки

Dart - Необязательные параметры

Светлая тема Темная тема
String hello(String name, int age, [String message = "Привет"]) {
  return "$message я $name! Мне $age лет!";
}

main() {
  hello("Андрей", 20); // Привет я Андрей! Мне 20 лет!
  hello("Маша", 18, "Здравствуйте"); // Здравствуйте я Маша! Мне 18 лет!
}

Возвращение пустого return

Полезное применение — возвращать пустой return

Это означает, что выполнение функции закончилось и мы выходим из нее в этот момент

Dart - Пустой return

Светлая тема Темная тема
void checkAge(int age) {
  if (age < 16) {
    print("Сорян! Но ты не пройдёшь!");
    return;
  }
  print("Всё норм! Проходи...");
}

main() {
  checkAge(10); // Сорян! Но ты не пройдёшь!
  checkAge(17); // Всё норм! Проходи...
}

Функция main

Каждое приложение должно иметь top-level функцию main(), которая есть входная точка для любого приложения на языке Dart.

Из этого следует, что любой файл, в котором содержится функция main, может выступать в роли того, с которого начинается запуск приложения.

Функция это тоже объект, она имеет специальный тип Function.

Функцию можно присвоить переменной или передать как параметр в другую функцию

Dart - Функция как объект

Светлая тема Темная тема
main() {
  void printValue(int value) {
    print(value);
  }

  printValue(42);
}

Когда при выполнении приложения в коде встречается вызов функции, то поток управления переходит в неё и выполняет её код, после управление возвращается в точку вызова функции и продолжается последовательная отработка кода основной программы.

Примитивные типы данных передаются в функцию по значению, а все другие – по ссылке

Возвращаемые значения

Dart - Возвращаемые значения

Светлая тема Темная тема
void main() {

emptyFunction() {}
print(emptyFunction()); // Если не указать return то функция вернёт null

// Используем тип данных record
// Чтобы функция могла вернуть два значения
(int, int) getPoint() {
  return (12, 2);
}

var point = getPoint();
print("Координата х = ${point.$1}");
print("Координата y = ${point.$2}");
}

Анонимные функции

Дополнительно можно создавать анонимные функции Лямбда-функции, то есть функции без имени.

Dart - Анонимная функция

Светлая тема Темная тема
() { return "Я анонимная функция"; }

Чаще всего они используются для передачи колбэка в функцию

Например:

Dart - Использование анонимной функции

Светлая тема Темная тема
var list = [1, 2, 3, 4, 5];
list.forEach((item) => print('Элемент списка: $item'));

Здесь в метод forEach передаётся анонимная функция (item) => print('Элемент списка: $item'), которая вызывается для каждого элемента списка и выводит его в консоль

Стрелочная функция (подробнее)

Стрелочная функция предоставляет форму для объявления однострочных именованных или анонимных функций. Их поведение аналогично обычным функциям за тем исключением, что они по умолчанию всегда возвращают значение, то есть оператор return в этих функциях не используется, но его наличие подразумевается.

Чаще всего используются как анонимные функции (value) => value.

Если тело функции содержит один оператор, то можно return можно не указывать.

Dart - Стрелочная функция

Светлая тема Темная тема
  // Обычная функция
  String getMessage(int count) { 
    return "У вас $count сообщений";
  }

  // Стрелочная функция
  String getMessage(int count) => "У вас $count сообщений";


  // ! Функцию можно сохранить в переменную
  //   В таком виде функция называется анонимной (без имени)
  var getMsg = (int count) => "У вас $count сообщений";
  print(getMsg(42));

Типы параметров функций

Функция может иметь любое количество обязательных required позиционных параметров. За ними могут следовать именованные параметры или необязательные позиционные параметры (НО НЕ ОБА СРАЗУ).

Именованные параметры

Для определения именованных параметров нужно их обернуть в

{ параметр1, параметр2, параметр3, ... }

Нужно у параметров:

  1. Задавать значения по умолчанию или
  2. Указывать ключ. слово required, для обязательного параметра или
  3. В теле функции, обрабатывать поведение с проверками на null

Dart - Именованные параметры

Светлая тема Темная тема
// Значения параметров по умолчанию параметр = значение
int getSuperValue1({int value1=0, int value2=0}) {
  return value1 * value2 - 42;
}

// Обязательные параметры, которые нужно указать при вызове функции
int getSuperValue2({required int value1, required int value2}) {
  return value1 * value2 - 42;
}

// Nullable параметры
int getSuperValue3({int? value1, int? value2}) {
  return (value1 ?? 0) * (value2 ?? 0) - 42;
}

Dart - Вызов с именованными параметрами

Светлая тема Темная тема
getSuperValue1(value2: 10); // У параметров есть значения по умолчанию
getSuperValue2(value1: 10, value2: 10); // Параметры указывать обязательно
getSuperValue3(value1: 10); // Здесь внутренняя проверка на null

Позиционные параметры

Dart - Позиционные параметры

Светлая тема Темная тема
String hello(String name, String msg, [int age = 18]) {
  var result = '$msg! Меня зовут $name! и мне $age';
  return result;
}

print(hello('Маша', 'Привет', 20));
print(hello('Роман', 'Привет'));

Функция НЕ МОЖЕТ иметь ОДНОВРЕМЕННО как необязательные позиционные, так и необязательные именованные параметры.

Область видимости функций

Область видимости функций в Dart можно понять как границы, в которых переменные доступны для использования. В Dart используется лексическая область видимости. Это значит, что доступ к переменным определяется тем, где они находятся в коде.

Dart - Область видимости

Светлая тема Темная тема
void main() {
  var insideMain = true;  // Переменная объявлена внутри main
  
  void myFunction() {  // Функция вложена в main
    var insideMyFunction = true; // Переменная объявлена внутри myFunction
    print(insideMain); // Можно использовать, она объявлена уровнем выше
    print(insideMyFunction); // Можно использовать, она объявлена здесь
  }
  
  print(insideMyFunction); // Ошибка! Переменная недоступна здесь
  myFunction(); // Вызываем myFunction, что напечатает значения переменных
}

Вложенные функции имеют доступ ко всем переменным уровнем выше.

Внутри функции myFunction можно обращаться к переменной insideMain, так как myFunction находится "внутри" функции main.

Но переменные, объявленные во вложенных функциях, недоступны уровнем выше.

Переменная insideMyFunction объявлена в myFunction и существует только внутри этой функции.

При попытке напечатать insideMyFunction в функции main произойдет ошибка, потому что main не знает о переменной insideMyFunction.

Это и есть принцип лексической области видимости: функции видят переменные, определенные "выше" по коду, но не "ниже".

Замыкания

Замыкания представляют собой довольно мощный инструмент, в основе которого лежит возможность функций запоминать значения переменных из внешних областей видимости. То есть из тех областей видимости, где данная функция была объявлена.

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

Например, есть функция add, которая создаёт и возвращает новую функцию. Эта новая функция использует переменную из функции add. Переменная, к которой обращается функция, замыкается внутри неё, и её значение сохраняется, даже когда функция используется позже.

Dart - Замыкания

Светлая тема Темная тема
// Тип данных Function означает, что функция возвращает другую функцию
Function add(num add) {
  return (num i) => add + i;
}

var add2 = add(2); // Создаем функцию add2, которая всегда добавляет 2

print(add2(3)); // 2 + 3 = 5
print(add2(4)); // 2 + 4 = 6

Ещё пример

Dart - Замыкание со счетчиком

Светлая тема Темная тема
Function makeCounter(int increment) {
  int count = 0; // Инициализируем счётчик, который будет "запомнен"

  return () {
    count += increment; // Прибавляем increment к count при каждом вызове
    return count; // Возвращаем текущее значение count
  };
}

void main() {
  var counter1 = makeCounter(1); // счётчик, который увеличивается на 1
  var counter2 = makeCounter(2); // счётчик, который увеличивается на 2

  print(counter1()); // 1
  print(counter1()); // 2
  print(counter1()); // 3

  print(counter2()); // 2
  print(counter2()); // 4
  print(counter2()); // 6
}

Рекурсия

Рекурсия - это когда функция вызывает саму себя для выполнения какой-то задачи.

"Чтобы понять рекурсию, нужно сначала понять рекурсию" (с)

Этот механизм часто используется вместо циклов, когда нужно работать с данными, которые могут иметь сложную и неизвестную заранее структуру или глубину, например, при работе с деревьями или списками.

Рекурсивная функция всегда должна:

  1. Иметь базовый случай, который останавливает рекурсию. Если его нет, функция будет вызывать себя бесконечно, что приведет к ошибке.
  2. Выполнять шаг, приближающий её к базовому случаю. То есть она должна как-то изменять свои данные, чтобы рано или поздно дойти до остановки.

Пример рекурсии: нахождение суммы элементов списка

Функция sum, которая считает сумму чисел в списке. Она выполняет это рекурсивно, разбивая задачу на подзадачи, пока не останется один элемент.

Dart - Рекурсивная функция

Светлая тема Темная тема
int sum(List numbers) {
  print(numbers); // Печатаем текущий список для наглядности

  // Базовый случай: если в списке 1 элемент, возвращаем его
  if (numbers.length == 1) return numbers[0];

  // Рекурсивный случай: возвращаем первый элемент + сумма оставшихся
  return numbers[0] + sum(numbers.sublist(1));
}

print(sum([1, 2, 3, 4, 5, 6, 7, 8, 9])); // Выводит 45

Пошаговое объяснение работы:

  1. Первый вызов: sum([1, 2, 3, 4, 5, 6, 7, 8, 9])
    • Функция получает список [1, 2, 3, 4, 5, 6, 7, 8, 9]
    • Проверка: длина списка больше 1, так что базовый случай ещё не достигнут
    • Возвращаем 1 + sum([2, 3, 4, 5, 6, 7, 8, 9])
  2. Второй вызов: sum([2, 3, 4, 5, 6, 7, 8, 9])
    • Теперь список: [2, 3, 4, 5, 6, 7, 8, 9]
    • Возвращаем 2 + sum([3, 4, 5, 6, 7, 8, 9])
  3. Третий вызов: sum([3, 4, 5, 6, 7, 8, 9])
    • Список: [3, 4, 5, 6, 7, 8, 9]
    • Возвращаем 3 + sum([4, 5, 6, 7, 8, 9])
  4. Так продолжается, пока список не сократится до одного элемента.

...

Последний вызов: sum([9])

Подсчёт суммы

После того как функция достигла базового случая и вернула 9, каждый вызов начинает возвращать результат обратно по цепочке:

Функция sum последовательно складывает элементы, возвращая конечный результат — 45

Преимущества и Важные моменты