$ sudo teach IT
Модуль 2 · Логика программы · Урок 2.2

Условия

Вы научите программу решать, какой путь выбрать: if, else if, else, и узнаете, что if в Rust умеет возвращать значение.

Теория + практика~25 минутНовичок

Функции мы уже пишем: total считает сумму трат, percent — долю лимита, left — остаток. Но все они выполняются «по прямой»: строка за строкой, всегда одинаково. Настоящая программа должна выбирать. Потратили 1 649 рублей из 30 000 — спокойно, потратили 33 000 — надо предупредить.

if и else: развилка на дороге

Представьте развилку с табличкой: «если мост закрыт — в объезд, иначе — прямо». Программа делает то же самое: проверяет условие, и от ответа «да» или «нет» зависит, какой кусок кода выполнится.

В Rust это конструкция **if (по-русски «если») с необязательной веткой else** («иначе»). Условие — это выражение, которое даёт значение типа bool, то есть true или false (с этим типом вы познакомились в уроке про типы данных). При true выполняется первый блок в фигурных скобках, при false — блок после else.

Rust · main.rs
fn main() {
    let spent: u32 = 31_000;
    let limit: u32 = 30_000;

    if spent > limit {
        println!("Лимит превышен");
    } else {
        println!("Лимит не превышен");
    }
}

Разбор по строкам

  • let spent: u32 = 31_000; и let limit: u32 = 30_000; — две переменные: сколько потратили и какой лимит. Подчёркивания в числе — просто разделители, как пробел в «31 000».
  • if spent > limit { — вопрос «потрачено больше лимита?». Знак > сравнивает числа и выдаёт true или false. Скобки вокруг условия не нужны.
  • println!("Лимит превышен"); — выполняется, только если ответ «да». Здесь 31 000 больше 30 000, поэтому печатается именно эта строка.
  • } else { — всё, что после else, выполняется, когда ответ «нет».
  • Фигурные скобки { } обязательны даже для одной строки — в Rust нельзя написать if без блока.

Условие обязано быть bool

Здесь Rust строже Python и JavaScript, где в if можно положить число или строку, и язык сам решит, «правда» это или «ложь». В Rust в условии должен стоять именно bool, иначе программа не соберётся.

Ошибка компилятора
error[E0308]: mismatched types
 --> src/main.rs:3:8
  |
3 |     if spent {
  |        ^^^^^ expected `bool`, found `u32`

Компилятор говорит: «ждал bool, а нашёл u32». Лечится явным вопросом: не if spent, а if spent > 0 или if spent > limit.

Какие вопросы можно задавать

Сравнения дают bool, а несколько вопросов можно связать между собой. Всё это — операторы, то есть значки, которые что-то вычисляют.

ОператорЧто спрашиваетПример (spent = 500)
==равно лиspent == 500 даёт true
!=не равно лиspent != 500 даёт false
< >меньше / большеspent < 1000 даёт true
<= >=меньше или равно / больше или равноspent >= 500 даёт true
&&«и»: верны оба условияspent > 0 && spent < 1000 даёт true
||«или»: верно хотя бы одноspent == 0 || spent == 500 даёт true
!«не»: переворачивает ответ!(spent > 1000) даёт true

Обратите внимание на двойной знак равенства. Одинарный = — это присваивание («положи в переменную»), а == — вопрос «равны ли?». Условие if spent = 500 компилятор не пропустит.

Rust · main.rs
fn main() {
    let spent: u32 = 500;

    if spent > 0 && spent < 1000 {
        println!("Скромная трата");
    }
}

Разбор по строкам

  • spent > 0 && spent < 1000 — два вопроса, связанных словом «и»: больше нуля И меньше тысячи. Оба верны, значит, всё условие true.
  • Ветки else нет — так тоже можно. Если условие ложно, блок просто пропускается и программа идёт дальше.

else if: цепочка вопросов

Два пути — это мало. Копилке нужны три состояния: «в норме», «почти лимит» (потрачено 80 % и больше) и «лимит превышен». Представьте кассира: «Билет на сегодня? Нет? На завтра? Нет? Тогда на послезавтра». Как только он слышит «да», остальные вопросы не задаёт.

В Rust такая цепочка строится из **else if («иначе если»). Проверка идёт сверху вниз, срабатывает первая** подошедшая ветка, остальные пропускаются. Завершает цепочку обычный else — «во всех остальных случаях».

Rust · main.rs
fn print_status(spent: u32, limit: u32) {
    if spent > limit {
        println!("Статус: лимит превышен");
    } else if spent * 100 >= limit * 80 {
        println!("Статус: почти лимит");
    } else {
        println!("Статус: в норме");
    }
}

Разбор по строкам

  • fn print_status(spent: u32, limit: u32) { — функция принимает потраченное и лимит. Результата у неё нет (нет ->): она только печатает.
  • if spent > limit { — самый тяжёлый случай проверяем первым: потратили больше лимита.
  • } else if spent * 100 >= limit * 80 { — если не превысили, спрашиваем: потрачено хотя бы 80 % лимита? Вместо деления умножаем обе стороны: так целочисленное деление не отбросит дробь, и ровно 80 % сработают честно.
  • } else { — все остальные случаи: потрачено меньше 80 %, значит, в норме.

Порядок веток важен

Rust выбирает первую подходящую ветку и дальше не смотрит. Поэтому порядок — это часть логики. Поменяйте первые две ветки местами:

Rust · main.rs
if spent * 100 >= limit * 80 {
    println!("Статус: почти лимит");
} else if spent > limit {
    println!("Статус: лимит превышен");
}

При spent = 31_000 и limit = 30_000 первое условие верно (31 000 — это больше 80 %), напечатается «почти лимит», а до проверки «превышен» дело просто не дойдёт. Компилятор промолчит: код корректный, но отвечает неправильно. Правило такое: сначала самые узкие и строгие условия, потом более широкие.

Ловушка: широкое условие впереди узкого

Если широкое условие стоит выше узкого, узкое никогда не сработает. Проверяйте границы: ровно 80 %, ровно лимит, лимит плюс рубль.

if как выражение: ветка, которая возвращает значение

Это самая необычная часть. Вы уже знаете, что блок { } тоже может быть выражением — то есть давать значение. С if то же самое: он выбирает одну из веток, и значение этой ветки становится значением всего if. Аналогия: продавец смотрит на количество и выбирает ярлык — «1 трата» или «3 траты».

Rust · main.rs
fn main() {
    let count = 1;
    let word = if count == 1 { "трата" } else { "траты" };
    println!("{} {}", count, word);
}

Разбор по строкам

  • let count = 1; — число трат; Rust сам понимает, что это целое число.
  • let word = if count == 1 { "трата" } else { "траты" }; — переменной word достаётся результат if. Если count равно 1, берётся «трата», иначе — «траты».
  • println!("{} {}", count, word); — два места {} заполняются по порядку: сначала count, потом word. Получится 1 трата.

Две важные детали. Первая: внутри веток нет точки с запятой после значения — "трата", а не "трата";. Точка с запятой превращает выражение в инструкцию и значение пропадает. Вторая: у всех веток должен быть один и тот же тип. Rust обязан заранее знать тип переменной word, и он не может зависеть от того, какая ветка выберется.

Ошибка компилятора
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:3:45
  |
3 |     let word = if n == 1 { "трата" } else { 5 };
  |                            -------          ^ expected `&str`, found integer
  |                            |
  |                            expected because of this

В одной ветке текст, в другой число — Rust отказывается собирать. Исправление: сделать обе ветки одного типа. И обратите внимание: у if, который используется как значение, ветка else обязательна — иначе непонятно, что делать при «нет».

Так мы исправим остаток

Вспомните, чем кончалось вычитание в практикуме первого модуля и в функции left из прошлого урока: если потрачено больше лимита, limit - spent у типа u32 не получится (беззнаковое число не бывает меньше нуля). Когда оба числа известны заранее, компилятор остановит сборку ошибкой this arithmetic operation will overflow. Когда они приходят во время работы, как параметры функции, программа упадёт с паникой attempt to subtract with overflow. Теперь у нас есть чем это закрыть. Функция может целиком состоять из одного if-выражения:

Rust · main.rs
fn left(limit: u32, spent: u32) -> u32 {
    if spent > limit {
        0
    } else {
        limit - spent
    }
}

Разбор по строкам

  • -> u32 — функция возвращает число.
  • if spent > limit { 0 } — если потрачено больше лимита, остаток считаем нулём. Вычитание в этой ветке вообще не выполняется, поэтому паники нет.
  • else { limit - spent } — иначе обычная разность; она безопасна, потому что spent <= limit.
  • Ни у 0, ни у limit - spent нет ; — if является последним выражением функции, и его значение становится результатом left. Слова return не нужно.

Где почитать подробнее

Условия описаны в главе «Control Flow» официальной книги: https://doc.rust-lang.org/book/ch03-05-control-flow.html .

Практика: статус копилки

Подключаем условия к проекту. Откройте kopilka/src/main.rs — файл, который остался после прошлого урока (в нём уже есть MONTH_LIMIT, print_header, total, percent и left). Мы сделаем три вещи: исправим left, добавим print_status и вызовем её из main.

  1. Замените функцию left на безопасную версию из раздела выше.
  2. Добавьте функцию print_status (она тоже уже разобрана выше) после left.
  3. В main посчитайте остаток через left, выберите пометку через if-выражение и вызовите print_status.
  4. Запустите проект: в терминале, в папке kopilka, выполните cargo run.

Полный код файла:

Rust · main.rs
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 {
    if spent > limit {
        0
    } else {
        limit - spent
    }
}

fn print_status(spent: u32, limit: u32) {
    if spent > limit {
        println!("Статус: лимит превышен");
    } else if spent * 100 >= limit * 80 {
        println!("Статус: почти лимит");
    } else {
        println!("Статус: в норме");
    }
}

fn main() {
    print_header();

    let expenses = [350, 1200, 99];
    let spent = total(expenses);
    let reserve = left(MONTH_LIMIT, spent);
    let note = if reserve == 0 { "запаса нет" } else { "запас есть" };
    let percent_exact = spent as f64 * 100.0 / MONTH_LIMIT as f64;

    println!("Лимит месяца:  {:>6} руб.", MONTH_LIMIT);
    println!("Потрачено:     {:>6} руб.", spent);
    println!("Осталось:      {:>6} руб. ({})", reserve, note);
    println!("Использовано лимита: {}%", percent(spent, MONTH_LIMIT));
    println!("Точнее: {:.1}%", percent_exact);
    print_status(spent, MONTH_LIMIT);
}

Запустите cargo run. Должно получиться так:

Вывод
Копилка 0.1
Учёт расходов за месяц

Лимит месяца:   30000 руб.
Потрачено:       1649 руб.
Осталось:       28351 руб. (запас есть)
Использовано лимита: 5%
Точнее: 5.5%
Статус: в норме

Теперь поэкспериментируйте: поменяйте в main массив трат и запустите снова. Для [20000, 5000, 1000] получится 86 % и «Статус: почти лимит». Для [20000, 9000, 4000] потрачено 33 000, остаток 0 («запаса нет») и «Статус: лимит превышен».

Разбор по строкам

  • print_header(); и строка percent_exact — те же, что в прошлых уроках; новые здесь только reserve, note и print_status.
  • let reserve = left(MONTH_LIMIT, spent); — остаток через безопасную функцию; имя reserve (а не left), чтобы не путать переменную с функцией.
  • let note = if reserve == 0 { ... } else { ... }; — if-выражение выбирает пометку; обе ветки — текст, точки с запятой внутри нет, а после закрывающей } она стоит, потому что это конец let.
  • {:>6} руб. ({}) в строке «Осталось» — выравнивание из первого модуля, а в скобках печатается пометка note.
  • print_status(spent, MONTH_LIMIT); — вызываем функцию, которая сама напечатает статус. Возвращать ей нечего.

Проверьте себя

  • Запускается ли программа с массивом [20000, 9000, 4000] без паники, и что она печатает в строке «Осталось»?
  • При каком значении spent (лимит 30 000) статус меняется с «в норме» на «почти лимит»? Проверьте 23999 и 24000.
  • Уберите ветку else у let note = ... — какую ошибку выдаёт компилятор?

Частые ошибки

Условие не bool

Что видно: error[E0308]: mismatched types ... expected bool, found u32 на строке с if. Как исправить: напишите явное сравнение, например if spent > 0.

Ветки if и else разных типов

Что видно: error[E0308]: if and else have incompatible types. Почему: переменная получает значение из if, а значит, тип веток должен совпадать. Как исправить: приведите обе ветки к одному типу и уберите лишнюю ;.

Перепутан порядок else if

Что видно: программа собирается, но для 31 000 печатает «почти лимит». Почему: первой сработала широкая ветка. Как исправить: ставьте узкие условия выше широких и проверяйте границы.

Что важно запомнить

  • if работает только с bool; «правдивых» чисел и строк в Rust нет.
  • Сравнения: == != < > <= >=; связки: && («и»), || («или»), ! («не»).
  • Ветки проверяются сверху вниз, срабатывает первая подошедшая: порядок else if — часть логики.
  • if — выражение: let word = if ... { ... } else { ... };. Ветки одного типа, внутри без ;, else обязателен.
  • Безопасное вычитание для u32: if spent > limit { 0 } else { limit - spent }.

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

На следующий урок