Функции
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Функции
Как вызов функции работает на уровне стека, фреймов и
return— Внутреннее устройство функций.
Что такое функция?
На ранних этапах освоения любого языка программирования внимание сосредоточено на базовых операциях — присваивании, сравнении, условных переходах, циклах. Однако по мере роста сложности задачи становится очевидным, что линейное выполнение команд быстро приводит к неуправляемому разрастанию кода, дублированию логики и снижению читаемости. Именно в этот момент вводится одно из самых важных понятий в программировании — функция.
★ Функция – блок кода, который можно вызывать по имени.
Это фундаментальный элемент программирования, который представляет собой выполнение определённой задачи.
Поначалу код был алгоритмическим и линейным:
действие1;
действие2;
действие3;
действие4;
действие5
Но позже, код разрастался:
действие1;
действие2;
действие3;
действие4;
действие5;
действие1;
действие2;
действие3;
действие4;
действие5;
действие1;
действие2;
действие3;
действие4;
действие5
И тогда, в целях избежания однотипных действий, решили упаковать действия в функцию и дать ей имя, чтобы просто её вызывать:
функция Делать() {
действие1;
действие2;
действие3;
действие4;
действие5
}
Делать();
Делать();
Главное преимущество функций заключается в том, что они позволяют избежать повторений в коде. Вместо того, чтобы писать один и тот же код несколько раз, можно вынести его в функцию и вызывать по имени. Это помогает организовать программу, делая её более читаемой и управляемой.
Play ITЗагрузка интерактивного демо…
Например, не обязательно каждый раз писать код для суммирования двух чисел (a+b), можно создать функцию, назвать её sum. Такая функция должна будет принимать два аргумента (числа), и будет возвращать их сумму.
И вместо того, чтобы каждый раз приравнивать a=10, a=11, a=15, и повторять одно и то же сложение, можно вызывать функцию sum(10, 20). Поскольку функция представляет собой шаблон sum(a, b), в самом блоке мы просто пишем a+b, и просто вызываем с передачей аргументов:
sum(a, b)
{
return a+b
}
sum(10, 20)
Здесь a и b - параметры, а sum - функция. Функция может не иметь аргументов, и именно поэтому все функции пишутся как имяФункции() - скобки являются характерной чертой функций.
Параметры и аргументы
Ключевой признак функции в любом языке программирования — наличие скобок () после её имени. Именно скобки сигнализируют системе о том, что требуется не просто обратиться к значению, а выполнить некоторую последовательность действий.
Независимо от языка программирования, функция всегда отличается от других сущностей по следующему признаку:
Если после имени следует пара круглых скобок — это вызов функции.
Если скобок нет — это обращение к значению (переменной, константе, параметру и т. д.).
Это правило работает даже в тех случаях, когда имя функции совпадает с именем переменной, параметра или другого объекта в той же области видимости.
Имя функции может совпадать с именами переменных или параметров. Это допустимо, потому что контекст использования (наличие или отсутствие скобок) однозначно определяет, о чём идёт речь:
x— обращение к значению переменной или параметра с именемx;x()— вызов функции с именемx.
Такой подход позволяет гибко организовывать пространства имён и не накладывает искусственных ограничений на именование.
Рассмотрим следующую конструкцию:
x = 1
def x(x):
x = x
x(x)
Получился каламбур, не так ли? Давайте попробуем разобраться, как же будет действовать интерпретатор языка программирования Python в такой ситуации.
Как вы думаете, в конструкции x = x:
- Что произойдет:
- а) изменится глобальная переменная
x; - б) объявится локальная переменная
x; - в) изменится значение параметра
x;
- Какое значение присвоится
x:
- а) параметр функции
x(x); - б) глобальная
x; - в) переменная будет равна сама себе?
Разберём каждую строку:
x = 1— создаётся глобальная переменнаяx, которой присваивается значение1.def x(x):— объявляется функция с именемx. Внутри этой функции вводится параметр, также названныйx. На момент входа в тело функции этот параметр скрывает глобальную переменнуюx.x = x— внутри тела функции выполняется присваивание: локальной переменнойx(которая в данном случае совпадает с параметром) присваивается значение этого же параметра. Эта операция не изменяет ни глобальную переменную, ни сам параметр как таковой — она лишь подтверждает текущее значение локальной области видимости.x(x)— вызывается функцияx, в которую передаётся в качестве аргумента значение глобальной переменнойx(то есть1).
Итого, ответы будут такими:
1. Что произойдёт в строке x = x внутри функции?
В теле функции def x(x): параметр x становится локальной переменной, скрывающей любую внешнюю (в том числе глобальную) переменную с тем же именем. Это происходит сразу при входе в функцию, ещё до выполнения первой строки тела.
Следовательно, в выражении x = x:
- Левая часть (
x =) — это объявление или присваивание локальной переменной. - Правая часть (
= x) — это чтение значения локальной переменной, то есть параметра функции.
Таким образом, ничто не изменяется глобально, новая переменная не создаётся (параметр уже существует как локальная переменная), и значение параметра просто копируется в самого себя.
Ответ на вопрос 1:
в) изменится значение параметра x — технически это присваивание параметру его же собственного значения. Глобальная переменная не затрагивается, новая локальная переменная не создаётся.
2. Какое значение присвоится x в строке x = x?
Поскольку в момент вызова функции x(x) аргументом передаётся глобальная переменная x, равная 1, то:
- Параметр функции
xинициализируется значением1. - Внутри функции имя
xссылается только на этот параметр. - Выражение
x = xприсваивает переменнойxеё текущее значение — то есть1.
Ответ на вопрос 2:
а) параметр функции x(x) — именно он используется в правой части выражения.
Значение, которое присваивается, — это значение, полученное из глобальной переменной при вызове, но внутри функции оно уже хранится в параметре и не связано напрямую с глобальной областью.
То есть, глобальная переменная x останется неизменной, внутри функции параметр x получит значение 1 и присвоится самому себе.
# Объявляем глобальную переменную x и присваиваем ей значение 1
x = 1
# Определяем функцию с именем x, принимающую один параметр (также названный x)
def x(x):
# Внутри функции имя x ссылается на параметр (локальную переменную)
# Присваиваем локальной переменной x её же текущее значение — операция без эффекта
x = x
# Вызываем функцию x, передавая в неё значение глобальной переменной x (то есть 1)
x(x)
В этом примере система различает три сущности с одинаковым именем x, но разным смыслом:
- Глобальная переменная — существует вне функции.
- Параметр функции — существует только внутри функции и инициализируется значением аргумента при вызове.
- Имя функции — ссылка на исполняемый блок кода.
Разрешение имён происходит в соответствии с правилами области видимости конкретного языка. В большинстве современных языков действует правило: локальные имена имеют приоритет над глобальными.
Важный момент - порой люди называют аргументы параметрами, а параметры - аргументами. Это не то же самое.
Параметр - это переменная, которую мы передаём в функцию.
Аргумент - это значение переменной которое передаём.
То есть, в функции sum(a, b), a и b это параметры, а 10 и 20 (значения a и b соответственно) являются аргументами.
ФУНКЦИЯ ИМЯ(ПАРАМЕТР) {
ТЕЛО ФУНКЦИИ
}
ИМЯ(АРГУМЕНТ)
То есть, пусть вас не смущает ИМЯ(ПАРАМЕТР) и ИМЯ(АРГУМЕНТ) - одинаковые конструкции, но они служат простому результату:
- при определении функции в скобках пишется параметр, как имя переменной;
- при вызове в скобках пишется аргумент, как значение ранее объявленной переменной.
Параметры создаются для того, чтобы использовать их в теле функции. Например, вы можете сделать универсальную функцию для приветствия:
def greet(name):
print(f"Привет, {name}!")
Это позволит многократно вызывать функцию, и отправлять разные значения при каждом вызове, что позволит подставлять значение параметра name в блок кода внутри функции:
greet("Тимур") # Результат - Привет, Тимур!
greet("Андрей") # Результат - Привет, Андрей!
greet("Василий") # Результат - Привет, Василий!
Именно это и есть упрощение как результат функции, использование разных аргументов для однотипных действий.
Переменное число аргументов
Иногда заранее неизвестно, сколько аргументов передадут в функцию. Многие языки поддерживают "сбор" лишних значений в одну структуру:
| Язык | Позиционные | Именованные |
|---|---|---|
| Python | *args → кортеж | **kwargs → словарь |
| JavaScript | rest-параметр ...args → массив | объект { key: value } |
| Ruby | *args → массив | **kwargs → хеш |
В Python это выглядит так:
def greet(*args, **kwargs):
for name in args:
print(f"Hi {name}")
for key, value in kwargs.items():
print(f"{key} = {value}")
greet("Ana", "Bob", age=30, city="Lisbon")
Такой приём делает функции гибкими и переиспользуемыми — особенно в декораторах и обёртках, которые должны принять любые аргументы и передать их дальше. Подробный разбор — Функции в Python.
Стадии работы функции
Стадии работы функции:
- определение (функция создаётся с помощью уникального имени, определяются входные данные (аргументы));
- вызов - указание имени функции и передача аргументов (при необходимости);
- результат - функция выполняет свою задачу и возвращает результат, который можно использовать дальше в программе.
Переменные и функции можно комбинировать, к примеру, сделав так, что c = sum(a, b), обозначив, что результат выполнения функции будет являться значением переменной.
Посмотрим пример на Python.
То есть, если у функции есть return, то можно записать её в переменную. Например, в Python это выглядит так:
def double(x):
result=x*2
return result
num=double(5)
print(num)
# Результат - 10
Поясняю - здесь:
def- ключевое слово для объявления функции,double- имя функции,(x)- параметр,return- ключевое слово для возвращения значения,result- переменная, выступающая в качестве возвращаемого значения,double(5)- вызов функции с аргументом5,num- переменная, равная значению, возвращаемого функциейdouble(5),print(num)- вызов функцииprintс передачей аргументаnum.
Функция double(x) выполняет умножение параметра x на 2, и когда мы вызвали через double(5), мы передали аргумент 5, который умножился на 2 и записался в переменную result. И именно значение этой переменной мы записали в num и вывели через встроенную функцию языка Python - print(), которая выводит указанное в аргументе значение в консоль.
А теперь посмотрим пример на JavaScript.
function double(x) {
let result = x * 2;
return result;
}
const num = double(5);
console.log(num);
Здесь:
function— ключевое слово для определения функции;double— имя функции;(x)— параметр;let result— объявление локальной переменной;return— возврат значения из функции;double(5)— вызов функции с аргументом5;const num— переменная, хранящая результат вызова;console.log(num)— вывод значения в консоль (аналогprintв Python).
Поведение идентично: функция принимает число, удваивает его и возвращает результат.
Пример на Java:
public class Main {
public static int doubleValue(int x) {
int result = x * 2;
return result;
}
public static void main(String[] args) {
int num = doubleValue(5);
System.out.println(num);
}
}
Здесь:
public static int doubleValue(int x)— сигнатура метода (в Java функции внутри классов называются методами);intперед именем — тип возвращаемого значения;(int x)— параметр с явным указанием типа;int result— локальная переменная целочисленного типа;return result— возврат значения;doubleValue(5)— вызов метода с аргументом5;int num— переменная для хранения результата;System.out.println(num)— вывод значения в консоль.
Java требует явного указания типов, и ведение работы именно в классах, но логика работы функции остаётся той же:
- получение входных данных;
- обработка;
- возврат результата.
Во всех трёх языках функция выполняет одну и ту же роль — инкапсулирует логику, принимает данные, возвращает результат. Различия касаются только синтаксиса и требований к типизации.
Получается, что функции могут отличаться некоторыми ключевыми словами, правилами оформления, знаками препинания, но везде есть единая основа:
- необходимость определения функции;
- указание имени функции;
- добавление скобок для указания параметров;
- параметры и аргументы;
- видимость и вложенность;
- вызов функции;
- возврат значения.
Определение обычно использует указание сигнатуры - набор ключевых слов (видимость, тип возвращаемого значения, слово def или function).
<набор выражений> <имя функции>(<параметры>)
Вызов всегда одинаковый - <имя функции>(<аргументы>).
Возврат значения обычно используется через ключевое слово return.
return <значение>
После возврата значений, функция уже не выполняется.
То есть, если будет такая конструкция:
def test(x):
result = x + 2
return result
# дальнейший код до конца функции уже не выполнится:
x = 3
print(x)
...то в таком случае x = 3 и print(x) не исполнятся, потому что возврат значения подразумевает завершение работы функции и выход из неё во внешний блок кода.
Виды функций
Встроенные и пользовательские
Функции могут быть встроенными (частью языка программирования) или пользовательскими (созданными программистом). Встроенные функции предоставляют готовые решения для распространённых задач, к примеру, вывод текста на экран или работа с файлами.
Как раз функция print() - встроенная функция языка Python, а double(x) - пользовательская.
Они могут принадлежать более крупным элементам - встроенным объектам. Допустим, в Javascript есть объект console, и у него есть функция log():
console.log("Привет, мир!")
Очень много встроенных функций имеется в PHP. Они бывают разные, бывают даже без скобок, например - echo для вывода информации:
echo "Привет, мир";
sleep(5);
exit("Ошибка");
Встроенные функции каждого языка определяются задачами, под которые заточен язык. В основном, это:
- вывод информации;
- проверка значений;
- преобразование типов;
- работа с типами;
- работа с датой и временем;
- работа с файлами;
- работа с системой;
- работа с сетью;
- форматирование;
- работа с коллекциями.
Поэтому, прежде чем составлять свою функцию, убедитесь, нет ли в языке уже готовых реализаций.
Функции могут быть реализованы по-разному в зависимости от их назначения, способа определения и использования.
Анонимные функции
Анонимные функции — это функции без имени. Они часто используются для выполнения небольших операций, которые не требуют отдельного определения.
функция(x, y) -> x * y
Как видим, у такой функции имени нет - и в разных языках используется разный подход.
Эта анонимная функция принимает два аргумента x и y и возвращает их произведение. Её можно использовать сразу, например, передав в качестве аргумента в другую функцию. Анонимные функции полезны, когда нужно быстро выполнить операцию без создания полноценной функции.
Анонимные функции нужны для того, чтобы не плодить сущности. Если функция нужна здесь и сейчас, то нет смысла давать ей имя и отдельно определять.
К примеру, можно встретить в сортировке и фильтрации, коллбэках, событиях, функциях высшего порядка (для передачи функции как аргумента):
Код ITЗагрузка примера кода…
Здесь функция без имени передаётся как аргумент в параметр predicate, и это самый распространённый способ использования анонимных функций. Суть в том, что функция может быть значением, которое можно присвоить переменной, параметру, передать в другую функцию, вернуть из функции. Анонимки делают это удобнее.
Как понять, стоит ли определить анонимную функцию, или обычную? Как правило, наличием необходимости обращаться по имени. Если вам нужно выполнить череду действий лишь сейчас, то можно как раз обойтись анонимной функцией.
Лямбда-функции
Пусть вас не смущает слово лямбда - это вид анонимной функции. Это концепция из функционального программирования, подразумевающая функцию как значение, когда функцию можно передать, вернуть, присвоить переменной. В JavaScript, например, к лямбдам относят и анонимные, и стрелочные функции.
Лямбда-функции (анонимные функции) — это короткие, одноразовые функции, которые не имеют имени. Они используются для выполнения простых операций, которые не требуют создания полноценной функции. Это частный случай анонимной функции.
лямбда(x, y) -> x + y
Здесь мы создали анонимную функцию, которая принимает два аргумента x и y и возвращает их сумму. Такую функцию можно использовать сразу, например, передав её в качестве аргумента в другую функцию. А слово лямбда указывает, что эта функция именно такого типа. Лямбда-функции удобны, когда нужно выполнить небольшую операцию, которую нет смысла выносить в отдельную функцию.
Все лямбда-функции являются анонимными функциями, но не все анонимные функции обязательно являются лямбда-функциями.
Лямбда — это математическая концепция, которая описывает анонимную функцию. Она была впервые введена в лямбда-исчислении (λ-calculus), которое является основой функционального программирования.
Лямбда-выражение — это реализация лямбда-функции в программировании. Оно представляет собой компактный способ записи анонимной функции.
Вот пример обычной анонимной функции на JavaScript:
const multiply = function(x, y) {
return x * y;
};
Это обычная функция без имени, её можно присвоить переменной или передать как аргумент. Но в JavaScript "лямбдой" можно назвать любую функцию, которую можно передать как значение. В повседневном JS "лямбда" и "стрелочная функция" часто используются как синонимы.
Стрелочная функция:
const multiply = (x, y) => x * y;
Если вы присмотритесь, то это просто немного другой синтаксис для создания анонимных функций. Концептуально, лямбда представляет собой термин из λ-исчисления (Алонзо Чёрч, 1936), означающий, что функция - это значение, которое можно передавать, возвращать и присваивать.
В Python реализация только лямбды и ограничена одним выражением:
# Лямбда (и она же анонимная)
square = lambda x: x ** 2
# Обычная функция — не лямбда
def square(x):
return x ** 2
В Java (до Java 8) используются анонимные классы, что более громоздко:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked!");
}
});
Java 8 представили уже как раз лямбды без стрелочного синтаксиса:
button.addActionListener(e -> System.out.println("Clicked!"));
В C# есть и анонимные методы, и лямбды:
// Лямбда-выражение
Func<int, int> square = x => x * x;
// Анонимный метод
Func<int, int> square = delegate(int x) { return x * x; };
В Ruby - необычный синтаксис, там блоки и лямбды:
# Блок (часто используется)
[1,2,3].map { |x| x * 2 }
# Лямбда — более строгий вариант
square = ->(x) { x * 2 }
square.call(5) # 10
В Go только анонимные функции, а понятия "стрелочная" нет:
square := func(x int) int {
return x * x
}
Swift и Kotlin используют термин "замыкания" (closure), но по сути это лямбда:
let square = { (x: Int) in x * x }
Получается, что не все языки поддерживают три варианта, где-то лямбда это просто слово lambda, где-то стрелочный синтаксис, а где-то есть только анонимные функции.
Замыкания и реализация под капотом
Одно и то же слово "лямбда" в разных языках может означать чуть разный синтаксис, но замыкание везде одно по смыслу: функция тащит за собой окружение (переменные из внешних областей), к которому обращается при вызове. Отличается то, как рантайм или компилятор это окружение хранит.
| Язык / платформа | Типичная модель | Коротко о механизме |
|---|---|---|
| C# | Делегат + сгенерированный метод или класс-носитель | Без захвата — отдельный метод; с захватом — вложенный класс с полями, делегат держит метод экземпляра (лямбды и отложенная инициализация в C#). |
| Python | Объект функции с ячейками __closure__ | Вложенная def и lambda создают функциональный объект; свободные переменные хранятся в ячейках (cell), общих для всех функций, созданных в одном вызове внешней функции. |
| JavaScript / TypeScript | Лексическое окружение в спецификации ECMAScript | Движок связывает функцию с записью окружения; для стрелочных функций нет собственного this и arguments — они берутся из вне (это отдельный контракт от замыканий). |
| Java | invokedynamic + сгенерированный класс или разделение на захваты/аргументы | Лямбда компилируется в вызов bootstrap-метода; на практике часто появляется синтетический класс с полями под захваченные значения. |
| Kotlin / Scala (JVM) | То же JVM плюс оптимизации компилятора | Смысл как у Java: функциональный интерфейс или SAM, под капотом объект с полями или без них для "пустых" лямбд. |
| C++ | Уникальный тип замыкания (функтор) | У каждой лямбды свой класс с operator(); список захвата [=] / [&] задаёт копии или ссылки на внешние объекты как члены. |
| Rust | Анонимный тип, трейты Fn / FnMut / FnOnce | Замыкание — структура с полями под захваченные переменные; компилятор проверяет, можно ли вызывать её по shared, mutable или один раз. |
| Go | Литерал функции с захватом | Компилятор может размещать захваченные переменные на куче, если замыкание "убегает" из области; иначе остаётся на стеке. |
| Swift | Замыкание с списком захвата [weak self] и т.д. | Ссылочный тип; явный capture list управляет циклами ссылок и семантикой self. |
| Ruby | Proc / lambda / блоки | Блок — не объект, пока не обернуть в Proc; lambda строже к return и числу аргументов, чем "обычный" Proc. |
| Haskell | Функции как значения, частичное применение | "Лямбда" — часть синтаксиса; окружение обычно упаковано в thunk или структуру данных GHC, без ОО-классов в стиле C#. |
Ниже — несколько типичных отличий, с которыми сталкиваются при переносе привычек между языками.
Python — lambda и вложенные def
Выражение lambda x: x + n и вложенная функция def inner(x): return x + n при одинаковом n используют одну и ту же модель замыкания: свободная переменная n живёт в ячейке, на которую ссылается атрибут __closure__ функции. Ограничение у lambda одно — тело только из одного выражения; на замыкание это не влияет.
Типичная ловушка — отложенное связывание в цикле — если в списке создать несколько лямбд, читающих одну переменную цикла, все они увидят её значение на момент вызова, а не на момент создания. Исправление — явная привязка через параметр по умолчанию:
funcs = [(lambda x, i=i: i) for i in range(3)] # i=i фиксирует текущее значение
В C# похожий эффект даёт одна переменная цикла for; в Python чаще спотыкаются именно о comprehension и колбэки.
JavaScript — стрелки и обычные функции
Стрелочная функция создаёт замыкание на лексическое окружение так же, как function, но не получает собственные this, super, arguments и не может быть конструктором. Для колбеков в классах и DOM это часто решающий фактор.
Java на JVM
Синтаксис x -> x + k компилируется в байткод через механизм лямбд Java 8+. Снаружи это "маленькая функция", внутри — связка invokedynamic, сгенерированных классов и, при необходимости, полей под захваты. Писать об этом нужно как о детали реализации: конкретная стратегия может меняться между версиями VM, контракт для программиста остаётся в терминах функциональных интерфейсов.
C++ — явный список захвата
Замыкание в C++ — объект с известным на этапе компиляции типом (часто только через auto). Захват по значению копирует объект на момент создания лямбды; по ссылке — нужно следить, чтобы захваченные объекты жили дольше лямбды. Это ближе к Rust по духу контроля времени жизни, чем к C#.
Go — замыкание и побег на кучу
Литерал func() { ... }, который сохраняют в переменную, возвращают из функции или передают дальше по горутине, может заставить компилятор разместить захваченные переменные в куче. Если замыкание не "убегает", накладные расходы меньше. Профилировщик и -gcflags=-m показывают, где происходит escape.
Когда думать о "подкапотной" модели
- долгоживущие подписки (таймеры, события, очереди сообщений);
- лямбды в горячих циклах и аллокации;
- циклы ссылок в GUI и мобильных фреймворках (Swift, C++ с
shared_ptr).
В учебных примерах достаточно помнить — замыкание связывает функцию с данными, а конкретный механизм (класс, ячейка, функтор, invokedynamic) — это уже уровень языка и компилятора.
Стрелочные функции
Стрелочные функции — это упрощённый синтаксис для записи функций.
sum(a, b) => a + b
Здесь мы определили функцию sum, которая принимает два аргумента a и b и возвращает их сумму. Символ => указывает, что это стрелочная функция. Стрелочные функции делают код короче и читаемее, особенно если функция выполняет простую операцию — это позволяет не создавать блок кода, а писать "тело" функции сразу после =>, в одну строку.
Можно сказать, что это просто удобная запись для лямбд, которая появилась в языках чуть позже.
Ключевой элемент - оператор => (стрелка).
| Язык | Синтаксис | Пример |
|---|---|---|
| JavaScript | => | (x) => x * 2 |
| TypeScript | => | (x: number) => x * 2 |
| C# | => | x => x * 2 |
| Java | -> (с Java 8) | x -> x * 2 |
| Kotlin | -> | { x -> x * 2 } |
| Scala | => | (x: Int) => x * 2 |
| Python | Нет | — |
| Ruby | Нет | — |
| Go | Нет | — |
Почему именно стрелка?
Исторически:
- Lisp (1958) использовал lambda;
- ML (1973) использовал fun;
- C++ (2011) лямбды без стрелки:
[] (int x) { return x*x; }.
И благодаря C# и его технологии LINQ в 2007 году, стрелка => стала популярной, и эту идею подхватили уже в JavaScript в 2015 году, также Java и Kotlin.
Стрелку выбрали, потому что это:
- короче, чем
function; - визуально похоже на математическую стрелку
f: A → B - ассоциируется с указанием на результат.
К примеру, в JavaScript есть варианты синтаксиса:
- Только выражение (без return):
const square = x => x * x;
- Блок (с return):
const square = x => {
return x * x;
};
- Несколько аргументов:
const add = (a, b) => a + b;
- Без аргументов:
const getRandom = () => Math.random();
Причина существования та же:
- короче;
- удобнее для колбеков и даёт возможность передать функцию на лету;
- читаемо, сразу видно, что это трансформация.
Пример:
// Было
numbers.map(function(x) {
return x * 2;
});
// Стало
numbers.map(x => x * 2);
В результате, мы формируем всё в одну строку.
Функции высшего порядка
Функции высшего порядка — это функции, которые могут принимать другие функции в качестве аргументов или возвращать функции как результат.
applyOperation(operation, x, y) -> operation(x, y)
Здесь функция applyOperation принимает три аргумента: функцию operation и два числа x и y. Она вызывает переданную функцию operation с аргументами x и y.
Теперь мы можем использовать эту функцию с разными операциями:
sum(a, b) -> a + b
multiply(a, b) -> a * b
result1 = applyOperation(sum, 5, 3) // Результат: 8
result2 = applyOperation(multiply, 5, 3) // Результат: 15
Здесь мы создали ещё две функции - sum, multiply и обе являются аргументами в applyOperation.
Функции высшего порядка могут сочетать оба свойства - и принимать функции как аргументы, и возвращать функцию как результат.
Рекурсивные функции
Рекурсивные функции — это функции, которые вызывают сами себя.
ОБЪЯВИТЬ ФУНКЦИЯ(X):
ЕСЛИ X БОЛЬШЕ НУЛЯ:
ФУНКЦИЯ(X - 1)
ФУНКЦИЯ(10)
В такой ситуации, мы получаем рекурсивное выполнение:
- при первом вызове функции, параметр имеет значение 10;
- проверка даёт истину, ведь 10 больше нуля;
- функция вызывает саму себя, и отправляет аргумент
10-1, то есть9; - функция снова и снова будет повторять вызов, пока не дойдёт до нуля;
- проверка даст ложь, ведь X теперь имеет значение
0; - только тогда функция перестанет себя вызывать.
Они полезны для решения задач, которые можно разбить на подзадачи того же типа.
factorial(n) ->
если n == 0
вернуть 1
иначе
вернуть n * factorial(n - 1)
Здесь функция factorial вычисляет факториал числа n. Она вызывает саму себя с уменьшенным значением n, пока не достигнет базового случая (n == 0).
Рекурсивные функции позволяют решать сложные задачи, такие как обход деревьев или вычисление факториала, в компактной форме. Если вы не знаете, что такое факториал, можете не углубляться — это математическая операция.
Теоретически, любая функция может быть рекурсивной, если она вызывает сама себя. Но функция должна иметь условие выхода из рекурсии, чтобы избежать бесконечного цикла. Для, собственно, бесконечной обработки есть специальные средства - циклы. Каждый рекурсивный вызов должен приближать задачу к базовому случаю, иначе стек будет переполнен.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(5); // 120
Стек растёт вглубь, потом сворачивается обратно:
factorial(5)
→ factorial(4)
→ factorial(3)
→ factorial(2)
→ factorial(1) → возвращает 1
→ 2 * 1 = 2
→ 3 * 2 = 6
→ 4 * 6 = 24
→ 5 * 24 = 120
Поэтому, чтобы не поломать логику, нужно использовать либо условие выхода, либо изменённый аргумент.
Пример базового случая с условием выхода:
if (n <= 1) return 1;
Рекурсивный случай же подразумевает как раз изменение аргумента:
return n * factorial(n - 1);
Хвостовая рекурсия подразумевает рекурсивный вызов как последнюю операцию:
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc); // хвост
}
Взаимная рекурсия более забавная. Это случай, когда две функции вызывают друг друга:
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
if (n === 0) return false;
return isEven(n - 1);
}
Рекурсию используют для задач, когда требуется обход дерева:
function traverse(node) {
if (!node) return;
console.log(node.value);
traverse(node.left);
traverse(node.right);
}
Также для глубокого копирования:
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const copy = Array.isArray(obj) ? [] : {};
for (const key in obj) {
copy[key] = deepClone(obj[key]);
}
return copy;
}
Но! Всё, что можно сделать рекурсией, можно сделать циклом:
Код ITЗагрузка примера кода…
Цикл эффективнее по памяти. Но о них мы поговорим отдельно.
Встроенные функции
Встроенные функции — это функции, которые уже реализованы в языке программирования. Они предоставляют готовые решения для распространённых задач.
Чистые функции
Чистые функции — это функции, которые всегда возвращают один и тот же результат при одинаковых входных данных и не имеют побочных эффектов (например, изменения глобальных переменных).
add(a, b) -> a + b
Функция add является чистой, так как она всегда возвращает сумму a и b и не изменяет ничего вне своей области видимости.
Функции, которые ничего не возвращают, не могут быть чистыми (к примеру, функции типа void).
Практика
Напишите любую функцию. Это будет функция 1.
Придумайте имя для функции.
Напишите другую функцию с другим именем.
Это будет функция 2.
Вызовите функцию 1 из функции 2.
Подпрограммы — функция, процедура и метод
Функция — подпрограмма для вычисления и возврата результата. Она принимает входные данные, обрабатывает их по заданному алгоритму и выдаёт значение, которое можно использовать в других частях программы.
Процедура — подпрограмма для последовательности действий — изменение состояния, вывод данных, запись в файл. Процедура может принимать аргументы и отдавать данные через выходные параметры, но не возвращает значение через основной оператор возврата (в языках, где такой термин выделен явно).
Метод — функция или процедура, связанная с объектом или классом. Метод задаёт поведение экземпляра и имеет доступ к его полям и свойствам. Он может возвращать результат или выполнять побочные эффекты.
Эти три категории различаются по назначению, контексту вызова и связи с данными. Ниже — сравнение на уровне идей; термины в конкретных языках и синтаксис — в разделе 5. Языки (у каждого языка свой маршрут).
| Критерий | Функция | Процедура | Метод |
|---|---|---|---|
| Назначение | Вычисление и возврат результата | Последовательность действий | Поведение объекта или класса |
| Возврат значения | Обычно возвращает значение | Без возврата значения (или void) | С результатом или без |
| Контекст вызова | имя(аргументы) | имя(аргументы) | объект.имя(аргументы) или Тип.имя(...) |
| Принадлежность | Независимая подпрограмма | Независимая подпрограмма | Класс, объект, тип (Go, Rust) |
| Доступ к данным | Только аргументы и замыкание | Только аргументы | Поля и состояние "своего" объекта |
| Пример | sum(a, b), hash(x) | вывод в лог, init() | user.save(), list.append(x) |
Методы и привязка к объекту
Вызов по-прежнему выглядит как имя(аргументы), но перед именем появляется объект или тип: текст.upper(), Math.max(a, b).
Подробнее о классах и объектах — в разделе ООП; ниже — как методы соотносятся с обычными функциями.
★ Метод — функция (или процедура), привязанная к объекту, классу или типу.
Объект хранит данные и через методы выполняет действия над ними. У строки в Python метод upper() преобразует символы; в JavaScript у литерала "Hello" те же задачи решают toUpperCase(), includes(), slice() — см. методы строк в JS. У списка append(x) добавляет элемент. Метод "знает" экземпляр: внутри доступны его поля без передачи их каждым аргументом.
// Свободная функция
функция Делать() {
действие1;
}
// Метод у объекта
некийОбъект {
метод Делать() {
действие1; // может читать поля объекта
}
}
В Java, C#, Kotlin и Swift подпрограммы внутри класса в спецификации называются методами; отдельных "функций" вне класса в Java нет (есть static-методы). В Python и JavaScript есть и свободные функции (def, function), и методы у объектов — в таблице ниже.
Типичная путаница
| Ситуация | Как запомнить |
|---|---|
| Java / C# | Всё исполняемое в классе — метод; public static void main — тоже метод |
| JavaScript | function f() — функция; obj.m() — метод (вызов через точку) |
| Go | Функции пакета + методы с приёмником: func (p *Person) Greet() |
| Python | def f() — функция; "abc".upper() — метод встроенного типа |
| SQL | Функция возвращает значение в выражении; хранимая процедура — отдельный объект каталога |
Термины по языкам
В разных языках одни и те же идеи называются по-разному. В таблице — что есть в спецификации языка, что встречается по смыслу, и куда перейти в энциклопедии.
Условные обозначения
| Символ | Значение |
|---|---|
| в языке | Есть ключевое слово или устойчивый термин |
| по смыслу | То же поведение, другие слова (void, def без return) |
| — | Отдельного понятия в языке нет |
Общее правило из начала статьи сохраняется: скобки () означают вызов подпрограммы — функции, процедуры или метода.
Скриптовые и универсальные
| Язык | Функции | Методы | Процедуры | Ещё | В энциклопедии |
|---|---|---|---|---|---|
| Python | def, модуль | в языке — у объектов (str.upper()) | по смыслу — def без return | lambda, @staticmethod | Функции · ООП · дандер-методы |
| JavaScript | function, стрелочные | по смыслу — obj.method() | по смыслу — без return | колбэк, замыкание | Функции · объекты |
| PHP | function | в языке — в классе | по смыслу — без возврата | замыкание | Функции · классы |
| Ruby | def, блоки | в языке — у объекта | по смыслу — без полезного return | блок, -> лямбда | встроенные · классы |
| Bash | function / func | — | по смыслу — команда без результата | встроенная команда | Функции · команды |
| PowerShell | function | по смыслу — у объекта | — | командлет Verb-Noun | Функции · командлеты |
Компилируемые и ООП
| Язык | Функции | Методы | Процедуры | Ещё | В энциклопедии |
|---|---|---|---|---|---|
| Java | — (вне класса) | в языке — в классе | по смыслу — void | static-метод | ООП · встроенные |
| C# | локальные функции | в языке — в типе | по смыслу — void | делегат, лямбда | ООП · встроенные · делегаты |
| Kotlin | fun верхнего уровня | в языке — в классе | по смыслу — Unit | extension, функция с получателем | ООП · расширения · получатель |
| Swift | func | в языке — у типа | по смыслу — без возврата | замыкание | параметры API · встроенные |
| C++ | свободные функции | в языке — член класса | по смыслу — void | лямбда, перегрузка | Функции · ООП |
| Dart | function | в языке — в классе | по смыслу — void | async/await | Функции · ООП |
Системные и без "классов Java"
| Язык | Функции | Методы | Процедуры | Ещё | В энциклопедии |
|---|---|---|---|---|---|
| C | в языке — function | — | по смыслу — void | указатель на функцию | Функции |
| Go | в языке — func | в языке — с приёмником | по смыслу — без возврата | анонимная func | Функции и методы |
| Rust | в языке — fn | в языке — в impl | по смыслу — -> () | трейт, замыкание | стандартная библиотека · ООП-идеи |
Классика, данные и функциональные
| Язык | Функции | Методы | Процедуры | Ещё | В энциклопедии |
|---|---|---|---|---|---|
| Pascal | в языке — function | — | в языке — procedure | раздел implementation | Процедуры и функции |
| SQL | в языке — функция СУБД | — | в языке — хранимая процедура | триггер, UDF | Процедуры · PL/pgSQL, T-SQL |
| 1С | в языке | по смыслу — метод объекта | в языке — процедура | экспортная процедура | Функции и процедуры |
| Haskell | в языке — всё fun | — | — | каррирование, композиция | Функции · основы ФП |
Прямая передача результатов функций
Порой можно получить значения, и записать их в переменные, путём как раз-таки использования функций.
К примеру, если определить функцию, вычисляющую сумму чисел a и b, и возвращающую результат, то можно такой результат записывать в переменные:
def sum(a,b):
return a+b
x = sum(1,2)
Здесь есть два подхода:
# Первый
print(x)
# Второй
print(sum(1,2))
Второй - это прямая передача результатов функции.
Прямая передача результата функции в качестве аргумента другой функции представляет собой идиоматичный подход к построению цепочек вызовов. Такой стиль кода выражает намерение разработчика лаконично и без промежуточных звеньев.
Семантическая целостность выражения
Когда результат функции немедленно используется в другом вызове, прямая передача сохраняет логическую связность операции. Выражение воспринимается как единое действие, а не как последовательность разрозненных шагов. Чтение кода происходит за один проход без необходимости отслеживать жизненный цикл временной переменной.
Пример на Python демонстрирует эту связность:
print(greet())
Вызов функции greet и вывод результата образуют неразрывную операцию. Контекст остаётся локальным, мысленная нагрузка на разработчика снижается.
Аналогичный пример на JavaScript:
console.log(sum(1, 2));
Результат сложения передаётся напрямую в вывод. Цепочка преобразований завершается в одной точке.
Пример на C#:
Console.WriteLine(FormatMessage("Приветствие"));
Форматирование сообщения и его вывод объединены в атомарное действие.
Пример на Java:
System.out.println(calculateTotal(items));
Подсчёт итоговой суммы и её отображение происходят в рамках единого выражения.
Технические аспекты выполнения
Современные среды выполнения обрабатывают оба варианта записи с одинаковой эффективностью. Компиляторы и интерпретаторы применяют оптимизации, устраняющие избыточные операции с временными значениями. Прямая передача результата соответствует естественному порядку вычислений в стековой машине: значение возвращается из функции и немедленно используется в следующей инструкции.
Промежуточное присваивание добавляет символическую операцию записи в память и последующего чтения. На уровне байт-кода или машинных инструкций это может проявляться как дополнительные шаги, хотя в большинстве случаев они оптимизируются на этапе компиляции или выполнения.
Читаемость и контекст использования
Прямая передача результатов уместна в следующих ситуациях:
- Результат функции используется однократно
- Функция имеет понятное название, отражающее её назначение
- Выражение остаётся компактным и легко воспринимается визуально
- Отладка такого участка кода не требует пошагового анализа промежуточных значений
Компактная запись снижает вероятность случайного использования временной переменной в другом контексте. Область видимости значения ограничивается точкой его применения.
Когда оправдано промежуточное присваивание
Промежуточное сохранение результата в переменную становится предпочтительным в определённых сценариях:
- Результат требуется использовать многократно в последующих операциях
- Переменная получает осмысленное имя, поясняющее семантику значения
- Код проходит этап активной отладки, требующей инспекции промежуточного состояния
- Выражение становится излишне сложным при прямой передаче
Пример оправданного присваивания на Python:
user_profile = fetch_profile(user_id)
print(user_profile)
save_to_cache(user_profile)
Здесь переменная user_profile используется дважды, её имя поясняет содержание, а присваивание устраняет повторный вызов функции.