← Оглавление

3.2 Что можно требовать

Хороший рецепт не пишет “мука торговой марки X в упаковке по 500 граммов”. Он пишет “мука”. Плохой инструмент разницы не делает: требует ровно то, что требовалось в прошлый раз, и отказывает всему остальному без объяснений. Это принято называть строгостью. Правильное слово - случайная специфика.

Попытка

Перенесём создание задачи внутрь add: пусть принимает id и title напрямую, без Task::new у вызывающего. Заголовок - строка, очевидный выбор типа:

fn add(tasks: &mut Vec<Task>, id: u64, title: String) -> Result<(), TqError> {
    tasks.push(Task::new(id, &title)?);
    Ok(())
}

Вызов выглядит естественно:

add(&mut tasks, 1, "Buy coffee")?;

Компилятор замечает несоответствие:

error[E0308]: mismatched types
  --> src/main.rs:XX:XX
   |
XX |     add(&mut tasks, 1, "Buy coffee")?;
   |                        ^^^^^^^^^^^^ expected `String`, found `&str`
   |
   = note: expected struct `String`
             found reference `&str`
help: try using a conversion method
   |
XX |     add(&mut tasks, 1, "Buy coffee".to_string())?;
   |                                   +++++++++++++

Компилятор даже предлагает выход: .to_string(). Он не ошибается - это сработает. Но перекладывать эту работу на каждого вызывающего - именно этого и хотелось избежать.

Что замечает компилятор

String и &str - разные типы (M1.2 объясняет оба). add объявил, что принимает String. Литерал "Buy coffee" - &str. Компилятор держит их отдельно и не конвертирует неявно.

Решение - не требовать конкретный тип, а требовать способность: “дай мне что-нибудь, что умеет становиться String”. В Rust это записывается как граница на тип:

title: impl Into<String>

impl Into<String> означает: любой тип, реализующий трейт Into<String>. И &str, и String его реализуют - для обоих существует From, а From даёт Into автоматически (M3.1). Внутри функции title.into() превращает переданное значение в String - один раз, в одном месте.

Заклинание

fn add(tasks: &mut Vec<Task>, id: u64, title: impl Into<String>) -> Result<(), TqError> {
    let title = title.into();
    tasks.push(Task::new(id, &title)?);
    Ok(())
}

Теперь оба варианта работают без изменений у вызывающего:

add(&mut tasks, 1, "Buy coffee")?;               // &str - работает
add(&mut tasks, 2, String::from("Buy milk"))?;   // String - тоже работает

main становится чище: вместо add(&mut tasks, Task::new(1, "Buy coffee")?) - просто add(&mut tasks, 1, "Buy coffee")?.

Два теста закрывают новое поведение:

#[test]
fn add_accepts_str_literal() {
    let mut tasks = Vec::new();
    assert!(add(&mut tasks, 1, "Buy coffee").is_ok());
    assert_eq!(tasks.len(), 1);
}

#[test]
fn add_rejects_empty_title() {
    let mut tasks = Vec::new();
    assert!(add(&mut tasks, 1, "").is_err());
}

Первый - что &str проходит как есть. Второй - что валидация из Task::new никуда не делась: пустой заголовок по-прежнему отклоняется. Полный набор тестов - в tq/.

make ci проходит. add принимает любую строку - и не перекладывает лишнюю работу на вызывающего.

Полный код tq для этой главы - 3-a-voice/02-what-you-can-demand/.