В первом модуле вы собрали первую копилку: лимит, массив трат, сумма и процент, и всё это внутри единственной функции main. Пока трат три, это терпимо, но стоит захотеть посчитать сумму для второго месяца, и придётся копировать строки. В этом уроке научимся давать куску кода имя и вызывать его, когда нужно.
Зачем нужны функции
Представьте кофемашину. Вы не объясняете ей каждый раз, как молоть зёрна и нагревать воду: нажимаете кнопку «эспрессо», и машина сама выполняет весь рецепт. Функция в программировании — такая же кнопка: вы один раз записываете рецепт, даёте ему имя, а потом нажимаете «кнопку» в любом месте программы.
Функция (function) — именованный блок кода, который можно вызвать по имени. Вы уже пользуетесь одной: main тоже функция, просто её вызывает сама программа при запуске. Посмотрите, что происходит, когда трат за два месяца и считать приходится дважды:
fn main() {
let may = [350, 1200, 99];
let june = [800, 40, 2500];
let may_total = may[0] + may[1] + may[2];
let june_total = june[0] + june[1] + june[2];
println!("{} {}", may_total, june_total);
}
Формула суммы записана дважды. Если завтра трат станет четыре, править придётся в двух местах и легко забыть одно. Вынесем формулу в функцию:
fn total(expenses: [u32; 3]) -> u32 {
expenses[0] + expenses[1] + expenses[2]
}
fn main() {
let may = [350, 1200, 99];
let june = [800, 40, 2500];
let may_total = total(may);
let june_total = total(june);
println!("{} {}", may_total, june_total);
}
Разбор по строкам
fn total(expenses: [u32; 3]) -> u32 {— объявление функции:fn(от function) — ключевое слово,total— имя, в скобках — что функция получает, после стрелки->— что она возвращаетexpenses[0] + expenses[1] + expenses[2]— тело функции; последняя строка без точки с запятой и есть результат (разберём ниже)let may_total = total(may);— вызов функции: пишем имя и в скобках то, что отдаём ей на вход; результат сохраняется в переменнуюlet june_total = total(june);— тот же рецепт, другие данные: формула написана один раз, а работает для любого массива из трёх трат
Выведет программа 1649 3340. Заметьте, что функция total записана выше main, но это дело вкуса: Rust находит функции независимо от порядка, так что main можно писать и первой.
Параметры: что функция получает
Ящик, который вы кладёте в кофемашину, — капсула. Без неё машина не знает, какой кофе готовить. То, что функция получает на входе, называется параметрами (parameters). В объявлении у каждого параметра есть имя и тип, разделённые двоеточием: expenses: [u32; 3] читается как «expenses — массив из трёх чисел u32».
Тип параметра обязателен всегда. Внутри тела Rust мог бы догадаться о типе сам, но на границе функции требует явности: так по одной строке объявления понятно, как функцией пользоваться. Имена принято писать в snake_case — строчными буквами, слова через подчёркивание: total, print_header, spent_so_far.
Параметров может быть несколько, они перечисляются через запятую. Когда вы вызываете функцию, значения, которые передаёте, называются аргументами (arguments): параметр — это «пустая ячейка» в объявлении, аргумент — то, что в неё кладут при вызове.
fn percent(spent: u32, limit: u32) -> u32 {
spent * 100 / limit
}
fn main() {
println!("{}", percent(1649, 30000));
}
Разбор по строкам
fn percent(spent: u32, limit: u32) -> u32— два параметра: сколько потрачено и каков лимит, оба типаu32; результат тожеu32spent * 100 / limit— сначала умножаем, потом делим, как в практикуме первого модуля: иначе целочисленное деление округлит до нуляpercent(1649, 30000)— аргументы подставляются в параметры по порядку:1649попадает вspent,30000— вlimit
Программа напечатает 5: 1649 рублей — это около пяти процентов от 30 000.
Порядок аргументов важен
Компилятор сверяет аргументы с параметрами только по порядку и типу. Вызов percent(30000, 1649) тоже скомпилируется (оба числа — u32) и напечатает 1819 вместо 5. Ошибки, которых компилятор не видит, ловите сами: перечитывайте объявление перед вызовом.
Возврат значения
Кофемашина не просто работает — она отдаёт вам чашку. Так и функция чаще всего что-то отдаёт обратно: число, текст, ответ «да или нет». Это возвращаемое значение (return value). Его тип пишется после стрелки ->.
В Rust есть приятное правило: результатом функции становится последнее выражение в теле, если после него нет точки с запятой. Ключевое слово return для этого не нужно. Сравните две записи, они делают одно и то же:
fn total(expenses: [u32; 3]) -> u32 {
expenses[0] + expenses[1] + expenses[2]
}
fn total_with_return(expenses: [u32; 3]) -> u32 {
return expenses[0] + expenses[1] + expenses[2];
}
Слово return завершает функцию немедленно и отдаёт указанное значение. Оно пригодится, когда понадобится выйти из функции пораньше, посреди тела, — например, после проверки условия (условия мы разберём в следующем уроке). А в конце функции принято писать короче: без return и без точки с запятой.
Одна лишняя точка с запятой ломает функцию
Допишем ; после последней строки total. Программа не соберётся: функция обещала вернуть u32, а точка с запятой превратила результат в «ничего». Компилятор сам подскажет, что сделать:
error[E0308]: mismatched types
--> src/main.rs:1:33
|
1 | fn total(expenses: [u32; 3]) -> u32 {
| ----- ^^^ expected `u32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | expenses[0] + expenses[1] + expenses[2];
| - help: remove this semicolon to return this value
Прочитаем по-человечески. mismatched types — «типы не сошлись». expected u32, found () — «ждали число, а получили ()». Что за (), расскажем чуть позже. Главное — последняя строка подсказки: remove this semicolon, то есть «уберите эту точку с запятой». У Rust очень понятные сообщения; привыкайте читать их целиком.
Выражения и инструкции
Откуда берётся странное правило про точку с запятой? Из различия, которое в Rust важнее, чем в других языках. Разберём его на бытовом примере: в рецепте есть шаги-действия («нарежьте лук», «поставьте сковороду на огонь») и есть шаги, которые дают готовую вещь («получится золотистая зажарка»). Действие что-то делает, но вещи не отдаёт; вещь можно положить в тарелку.
Официально: инструкция (statement) — действие, которое ничего не возвращает; выражение (expression) — фрагмент кода, который вычисляется в значение. 5 + 1 — выражение (значение 6), total(may) — выражение (число), let a = 5; — инструкция (объявляет переменную, но значения не даёт).
Теперь важное: точка с запятой превращает выражение в инструкцию: вычисленное значение выбрасывается. Поэтому a * 3 (без точки с запятой) отдаёт шесть, а a * 3; эту шестёрку отбрасывает.
И ещё одно неожиданное для новичка: блок кода в фигурных скобках { ... } тоже выражение. Его значение — последнее выражение внутри, снова без точки с запятой.
fn main() {
let x = {
let a = 2;
a * 3
};
let y = {
let b = 2;
let c = b * 3;
};
println!("{}", x);
println!("{:?}", y);
}
Разбор по строкам
let x = {— справа от равно стоит блок — а блок можно использовать как значениеlet a = 2;— внутри блока своя переменная; это инструкция, значения она не даётa * 3— последнее выражение без точки с запятой: оно и становится значением блока, поэтомуxравен 6let c = b * 3;— последняя строка второго блока — инструкция, значения у блока нет, иyполучает «ничего»println!("{:?}", y);— формат{:?}знаком вам по первому модулю: он напечатает это «ничего» как()
Выведет программа 6 и (). Тело функции — такой же блок, поэтому его последнее выражение и становится результатом функции. Теперь правило из прошлого раздела выглядит логично: функция отдаёт значение своего блока, а точка с запятой это значение убивает.
Функция без результата
Не каждая функция обязана что-то отдавать. Кнопка «промыть кофемашину» ничего не выдаёт: она просто выполняет работу. В Rust это функция, у которой нет стрелки -> и типа после неё. Так мы вынесем в функцию печать шапки программы:
fn print_header() {
println!("Копилка 0.1");
println!("Учёт расходов за месяц");
println!();
}
fn main() {
print_header();
let nothing = print_header();
println!("{:?}", nothing);
}
Разбор по строкам
fn print_header() {— скобки пустые: параметров нет; стрелки нет: результата нетprintln!(...);— обе строки — инструкции с точкой с запятой, последнего выражения в теле нет, значит, ничего не возвращаетсяprint_header();— вызов тоже оканчивается точкой с запятой: результат (его нет) выбрасывать нечего, зато вызов становится самостоятельной строкой программыlet nothing = print_header();— можно даже сохранить «результат» в переменную, хотя пользы в этом нет — нужно лишь, чтобы показать, что там лежитprintln!("{:?}", nothing);— напечатает()
Это и есть (), который мы видели в ошибке: единичный тип (unit type), пустой кортеж, то есть «значение, в котором нет ничего». Функция без стрелки на самом деле возвращает (), просто писать это не нужно. Отсюда и ошибка с лишней точкой с запятой: функция total обещала u32, а вышел ().
Что запомнить про ()
Если компилятор говорит found (), значит, где-то вы получили «ничего» там, где ждали значение, и чаще всего виновата точка с запятой в конце строки, которая должна была отдавать результат.
Практика: функции в Копилке
Наведём порядок в проекте из первого модуля: заберём из main печать шапки, сумму, остаток и процент и сделаем из них функции. Остальные строки main остаются такими же, как в конце первого модуля.
- Откройте папку проекта
kopilkaв редакторе и файлsrc/main.rs. - Замените его содержимое кодом ниже. Константа
MONTH_LIMITпо-прежнему стоит над всеми функциями, на верхнем уровне файла. - В терминале, находясь в папке
kopilka, выполнитеcargo run.
const MONTH_LIMIT: u32 = 30_000;
fn print_header() {
println!("Копилка 0.1");
println!("Учёт расходов за месяц");
println!();
}
fn total(expenses: [u32; 3]) -> u32 {
expenses[0] + expenses[1] + expenses[2]
}
fn percent(spent: u32, limit: u32) -> u32 {
spent * 100 / limit
}
fn left(limit: u32, spent: u32) -> u32 {
limit - spent
}
fn main() {
print_header();
let expenses = [350, 1200, 99];
let spent = total(expenses);
let percent_exact = spent as f64 * 100.0 / MONTH_LIMIT as f64;
println!("Лимит месяца: {:>6} руб.", MONTH_LIMIT);
println!("Потрачено: {:>6} руб.", spent);
println!("Осталось: {:>6} руб.", left(MONTH_LIMIT, spent));
println!("Использовано лимита: {}%", percent(spent, MONTH_LIMIT));
println!("Точнее: {:.1}%", percent_exact);
}
Разбор по строкам
fn print_header()— печатает название, подзаголовок и пустую строку; ничего не принимает и не возвращаетfn total(expenses: [u32; 3]) -> u32— складывает три траты через индексы; цикл, который заменит ручное сложение, будет в одном из следующих уроковfn percent(spent: u32, limit: u32) -> u32— целая доля потраченного от лимитаfn left(limit: u32, spent: u32) -> u32— остаток лимита; сначала лимит, потом потрачено, и в вызовеleft(MONTH_LIMIT, spent)порядок тот жеlet expenses = [350, 1200, 99];— массив трат; посколькуtotalждёт[u32; 3], числа автоматически становятсяu32let percent_exact = ...— строка из практикума первого модуля, её не трогаем: точный процент с дробной частьюlet spent = total(expenses);— вызов нашей функции вместо ручного сложения трёх индексовpercent(spent, MONTH_LIMIT)— вызов прямо внутриprintln!: выражение вычисляется, и число подставляется в{}
Копилка 0.1
Учёт расходов за месяц
Лимит месяца: 30000 руб.
Потрачено: 1649 руб.
Осталось: 28351 руб.
Использовано лимита: 5%
Точнее: 5.5%
У left пока самое простое тело, обычное вычитание. Если потрачено больше лимита, получится отрицательное число, а в u32 оно не помещается: программа упадёт с паникой attempt to subtract with overflow. Компилятор такую беду заранее не заметит, потому что значения параметров функции становятся известны только во время работы. Мы починим left в уроке про условия.
Дальше немного поэкспериментируйте: так функции запоминаются лучше всего. Добавьте в массив другое число и посмотрите, как изменятся строки. Затем намеренно поставьте точку с запятой после expenses[0] + expenses[1] + expenses[2] и прочитайте ошибку, а потом уберите её. Ещё попробуйте вызвать total с массивом из двух чисел и посмотрите, чем ответит компилятор.
Проверьте себя
- Чем параметр отличается от аргумента? Назовите оба в строке
percent(spent, MONTH_LIMIT). - Почему в конце тела
totalнет точки с запятой и нет словаreturn? - Что получит
yвlet y = { let b = 1; b + 1; };и почему? - Что изменится, если поменять местами аргументы в вызове
percent, и заметит ли это компилятор? - Где в программе объявлены функции и почему
mainможет стоять выше или ниже них?
Частые ошибки
1. Точка с запятой после результата
Что видно. Сообщение error[E0308]: mismatched types ... expected u32, found () и подсказка remove this semicolon.
Почему. Точка с запятой превращает последнее выражение в инструкцию, значение пропадает, и функция возвращает () вместо обещанного типа.
Как исправить. Уберите ; в конце последней строки, которая должна быть результатом.
2. Не указан тип параметра
Что видно. Для заголовка fn percent(spent, limit: u32) компилятор пишет:
error: expected one of `:`, `@`, or `|`, found `,`
--> src/main.rs:1:17
|
1 | fn percent(spent, limit: u32) -> u32 {
| ^ expected one of `:`, `@`, or `|`
Почему. Rust требует тип у каждого параметра, догадываться он не будет. Как исправить. Допишите двоеточие и тип: spent: u32.
3. Не то число аргументов или не тот порядок
Что видно. Если аргументов не хватает, получите error[E0061]: this function takes 2 arguments but 1 argument was supplied, а если вместо целого числа подставить дробное, то error[E0308]: mismatched types. А вот перепутанные местами аргументы одного типа ошибки не дадут вовсе.
Почему. Компилятор сверяет количество и типы, но не смысл. Как исправить. Сверяйтесь с объявлением функции: порядок аргументов совпадает с порядком параметров.
4. Имя функции не в snake_case
Что видно. Функция с именем Total_Sum работает, но компилятор предупреждает:
warning: function `Total_Sum` should have a snake case name
--> src/main.rs:1:4
|
1 | fn Total_Sum(a: u32) -> u32 { a }
| ^^^^^^^^^ help: convert the identifier to snake case: `total_sum`
Почему. Это warning (предупреждение), а не error: программа соберётся, но стиль Rust велит писать функции строчными буквами с подчёркиваниями. Как исправить. Переименуйте функцию, как подсказывает компилятор: total_sum.
Функция внутри main
Если написать fn внутри main, программа соберётся, но такую функцию видно только внутри этой main. Объявляйте функции на верхнем уровне файла, рядом с main: тогда их можно вызывать откуда угодно.
Что важно запомнить
- Функция объявляется так:
fn имя(параметр: тип, ...) -> тип_результата { тело }. Имя пишут вsnake_case. - Типы параметров обязательны. Параметры стоят в объявлении, аргументы — в вызове, порядок и количество должны совпадать.
- Результат функции — последнее выражение тела без точки с запятой;
returnнужен для раннего выхода. - Выражение вычисляется в значение, инструкция выполняет действие. Точка с запятой превращает выражение в инструкцию.
- Блок
{ ... }тоже выражение. Функция без стрелки->возвращает()— «ничего». - Ошибка
expected u32, found ()почти всегда означает лишнюю точку с запятой.
Следующий шаг — научить программу принимать решения: в норме мы или уже превысили лимит. Для этого нам понадобятся условия if и else.