← Оглавление

2.1 Отсутствующая задача

Как известно отрицательный результат тоже результат. Об этом легко забыть, особенно если ты инструмент. Инструмент, который не умеет его дать, опрокидывает верстак. Снаружи разница между “не нашёл” и “сломался” очевидна. Изнутри - совершенно нет.

Что нужно

tq умеет добавлять задачи, перебирать их, отмечать выполненными. Навыка найти одну конкретную по идентификатору пока нет. Можно обратиться к списку напрямую: tasks[0], tasks[1]. Но это индекс, а не идентификатор (id) - и если запрошенного нет, список опрокидывает верстак.

Создадим функцию get: принимает список и идентификатор, возвращает задачу - или честно сообщает, что её нет.

Сборка

В Rust для этого есть тип Option<T> - два варианта:

Option<&Task> и &Task - разные типы. Чтобы добраться до задачи внутри, нужно явно разобрать оба случая. После такого определения результата случайно проигнорировать отсутствие не получится.

fn get(tasks: &[Task], id: u64) -> Option<&Task> {
    for task in tasks {
        if task.id == id {
            return Some(task);
        }
    }
    None
}

Вызов - через match: каждый вариант получает свой путь:

match get(&tasks, 1) {
    Some(task) => println!("#{}: {}", task.id, task.title),
    None => println!("not found"),
}

Два теста - два пути:

#[test]
fn get_finds_existing_task() {
    let mut tasks = Vec::new();
    add(&mut tasks, Task::new(1, "Buy coffee"));
    assert!(get(&tasks, 1).is_some());
}

#[test]
fn get_returns_none_for_absent_task() {
    let tasks: Vec<Task> = Vec::new();
    assert!(get(&tasks, 99).is_none());
}

Полный набор тестов - в tq/.

Итог

main ниже показывает только новое - два вызова get. Полный код с list и остальным накопленным кодом - в tq/.

fn main() {
    let mut tasks: Vec<Task> = Vec::new();

    add(&mut tasks, Task::new(1, "Buy coffee"));
    add(&mut tasks, Task::new(2, "Buy milk"));
    add(&mut tasks, Task::new(3, "Buy eggs"));

    match get(&tasks, 1) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    match get(&tasks, 99) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }
}
#1: Buy coffee
not found

make ci проходит. get возвращает либо задачу, либо её отсутствие - вызывающий код решает, что с этим делать.

Полный код tq для этой главы - 2-honest/01-the-absent-task/.


Lore: iter().find()

Функция get реализует стандартный поиск: перебрать, проверить условие, вернуть первый подходящий. Это же можно реализовать с помощью iter().find():

fn get(tasks: &[Task], id: u64) -> Option<&Task> {
    tasks.iter().find(|task| task.id == id)
}

Option здесь тот же - find возвращает Some(&Task) если нашёл, None если нет. В тексте - развёрнутая, чтобы Some и None были видны явно. Однако в коде в tq/ использует эту форму не только из эстетических соображений: clippy знает линт manual_find и отклоняет развёрнутый цикл. make ci не пропустит вариант из текста без изменений - это намеренное расхождение между иллюстрацией и рабочим кодом.


Lore: if let

match требует обработки обоих вариантов. Когда нужен только один - if let короче:

if let Some(task) = get(&tasks, 1) {
    println!("#{}: {}", task.id, task.title);
}

Если задача не нашлась - ничего не происходит. Для случаев, когда отсутствие не требует отдельного действия, это точнее, чем match с пустым None => {}.