Прежде чем перейти к следующей главе, я хотел бы еще раз напомнить черты и дженерики. Большие идеи требуют времени, чтобы утвердиться, и у меня было несколько. Более того, видео (я расскажу о нем в следующей главе) помогло мне понять сверхвысокоуровневое чувство дженериков и трейтов.

Вот моя интерпретация того, что такое черты и дженерики:

Когда у нас есть язык со статической типизацией, мы не можем использовать одну и ту же функцию для объектов с аналогичным поведением, сохраняя при этом строгую систему типов. Мы можем ослабить строгость системы типов, но это снизит ее полезность. Или, лучше, мы можем создать механизм, чтобы обогатить систему типов способностью разрешать «межтиповое взаимодействие» под некоторым контролем некоторой выразительной семантики, которая позволит делать хорошие (преднамеренные) вещи и запрещать плохие (непреднамеренные). В Rust это делается с помощью дженериков и трейтов. Обобщения позволяют нам сказать, что функция может работать с более чем одним типом, а система свойств помогает нам описать, какими свойствами должны обладать эти типы. Или, другими словами, трейты позволяют нам сузить чрезмерно широкий «любой тип» до менее разрешительного «любого типа, который способен к…» (способен быть добавленным, сравненным, преобразованным в строку и т. д.). Система трейтов позволяет указать аспекты типов, которые мы собираемся использовать в нашей функции, игнорируя остальные (например, если нам нужен трейт для сравнения, нам все равно, итерируемый он или нет).

Кроме того, система свойств позволяет нам реализовать эти свойства для наших собственных типов данных, чтобы они соответствовали потребностям других функций (даже если эти функции были написаны не нами), поэтому наш новый блестящий тип по-прежнемубудет полезен. так же, как и более старые ранее существовавшие типы.

Черты еще лучше. Они не определяют одну операцию, которую нам нужно (предоставить), они определяют целый список операций, семантически сгруппированных вместе.

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

Другой источник информации

Даже если я решил следовать книге Rust, я обнаруживаю, что все больше и больше отклоняюсь от разных источников. Одним из них являются видео на YouTube. Вчера было опубликовано видео с отличным введением в Rust (к сожалению, аудио было не самым лучшим). В этом видео была ссылка на репозиторий гитхаба с перекомментированной игрой в крестики-нолики, которую я изучу чуть позже.

Тесты, тесты, тесты

Тем временем я возвращаюсь обратно к книге Rust. Глава, посвященная тестированию, прямо в середине книги для начинающих — это действительно хорошо, но…

… Но я еле скользил по нему. Я был рад узнать, что есть глава о тестировании, и я вернусь к ней, когда буду писать свой первый тест на ржавчину, но большую часть времени все вуду тестирования — это просто список условностей и удобств, и они имеют небольшое влияние за пределами поле испытаний. Есть несколько исключений, т.е. как организовать код, чтобы помочь с тестированием, но опять же, это в основном сложные темы, и я не хочу сейчас на них сосредотачиваться.

Более того, даже в режиме проблеска я почувствовал две маленькие царапины недовольства после знакомства с тестами на ржавчину.

Во-первых, это использование assert_eq. Это 2018 год. У нас есть pytest. Почему мы не можем иметь такую ​​же красоту на других языках?

Сравните эти два:

Ржавчина:

#[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }

Питест:

def test_it_adds_two():
    assert add_two(2) == 4

Pytest поддерживает правильный assert, но распечатает все детали «левой» и «правой» частей в случае сбоя теста. Он правильно понимает все операции Python: список словарей и сравнение наборов, все логические операции (‹, ›, !=, «is», «in») и т. д.

Ржавчина здесь немного ржавая.

Вторая ошибка связана с этим: мы не можем создавать интеграционные тесты в каталоге тестов и использовать extern crate для импорта функций, определенных в файле src/main.rs.

Мах… Почему? Я понимаю, что это языковой дизайн, но для меня это облом. Сделал исключение, взломал компилятор, сделал для этого директиву компилятора-отладчика. Но без возможности сделать 100% покрытие кода любая тестовая система разочаровывает.

Тем не менее, я могу привести один аргумент в пользу Rust. В Python мы отчаянно стремимся к 100% покрытию кода, потому что нет другого способа найти простейшие опечатки. Даже самая безобидная строка может вызвать ошибку времени выполнения. В Rust компилятор делает тяжелую работу и просто отказывается компилировать большинство тривиальных ошибок. Так что несколько тривиальных строк в main.rs не так опасны, как опечатка в Python. Я писал об этой драме недавно.

IO

Как и было обещано, я углублюсь в реальное программирование на Rust сразу после главы «IO». Теперь эта глава, наконец, наступила.

Проблема номер один: аргументы командной строки. Их разбор всегда тонны bolierplate. Я думал о переходе с argparse на click для Python, но у меня не было шанса. Теперь очередь Rust.

Аргументы командной строки в Rust представлены как Vec из String. Книга дает мне пример:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

Это интересный выбор, потому что они могли использовать Vec из &str. В Linux аргументы командной строки не размещаются в куче, они помещаются в фиксированную (на время выполнения программы) область памяти над стеком. Фиксированное место в памяти, которое не требует освобождения, определенно является хорошим кандидатом на роль str.

… Перед этим я могу построить Vec с &str? Да, я могу:

fn main() {
    let mut foo: Vec<&str> = [].to_vec();
    foo.push("Hello, world!");
    println!("{}", foo.pop().unwrap());
}

Так почему же env:args().collect() возвращает Vect из String?

Я играл с кодом. Упрощенная версия:

use std::env;
fn main() {
    let args = env::args();
    println!("{:?}", args);
}

Он работает и возвращает Args { inner: [“target/debug/minigrep”, “foo”] }. Args — это структура итератора (в книге по Rust о них еще не говорилось, но после Python я более чем знаком с этой идеей). Все функции, определенные в Args, работают с String.

У меня есть догадка, почему.

Rust использует unicode и ожидает, что Strings будет списком символов unicode (utf-8). Но что, если я передам недопустимый юникод в командной строке? Ни в коем случае это уже не str. Итак, нам нужно скопировать строки, чтобы преобразовать их из командной строки в utf-8. Более того, кто сказал, что командная строка в UTF-8? Как насчет KOI8-R или Shift-JS? Оба нуждаются в преобразовании в utf-8, чтобы продолжить. Это объясняет, почему все операции с обычными аргументами команды возвращают память в куче, а не указатели на предварительно выделенную ОС память. Существует структура ArgsOS, которая по-прежнему возвращает выделенную память. Мне нужно посмотреть, как Rust обращается к исходным значениям. Исходный код для std::env::args?

fn _var_os(key: &OsStr) -> Option<OsString> {
    os_imp::getenv(key).unwrap_or_else(|e| {
        panic!("failed to get environment variable `{:?}`: {}", key, e)
    })
}

Прежде чем применить свой рабочий навык чтения случайного исходного кода, чтобы добраться до сути, я хотел бы понять, что это такое.

Объявляем функцию _var_os. Я предполагаю, что использование _ перед именем функции является показателем «конфиденциальности». Функция принимает ключ (имя?) в качестве аргумента. Тип «ключа» — это ссылка на OsStr (я думаю, это нативная «str», о которой я говорил ранее).

Вызываем os_imp::getenv(key), где os_imp — имя модуля sys::os (в начале файла стоит use sys::os as os_imp;). Getenv возвращает нам что-то, что имеет метод unwrap_or_else. И тогда все становится странно. Я вижу panic! макросов, которые явно не всегда вызываются. Похоже на лямбда или блок Руби… Надо проверить. Немного позже. Насколько я могу сказать, этот код пытается «развернуть» и вернуть значение (с типом Option<OsString>) и паникует, если развернуть не удалось.

Позвольте мне следить за стеком вызовов. sys::os::getnev (ссылка). Это, очевидно, зависит от ОС, и я ищу способ Unix (каламбур).

pub fn getenv(k: &OsStr) -> io::Result<Option<OsString>> {
    // environment variables with a nul byte can't be set, so their value is
    // always None as well
    let k = CString::new(k.as_bytes())?;
    unsafe {
        ENV_LOCK.lock();
        let s = libc::getenv(k.as_ptr()) as *const libc::c_char;
        let ret = if s.is_null() {
            None
        } else {
            Some(OsStringExt::from_vec(CStr::from_ptr(s).to_bytes().to_vec()))
        };
        ENV_LOCK.unlock();
        return Ok(ret)
    }
}

Во-первых, я вижу здесь какой-то грязный код. k = function(k). Мех. Не делайте этого, это усложняет чтение. Далее k.as_bytes() — берет &OsStr, возвращает байты, легко. CString::new, 99% какой-то специальный тип данных для C-подобной строки. Да, это". Обновление: он возвращает принадлежащий объект. Его братCStr. В описании типа есть кое-что интересное (гарантия отсутствия нулей в середине, гарантия наличия \x0 в конце). Вопросительный знак раскручивает совпадение для результата и возвращает Err или дает значение внутри возврата (для этого мне нужно было освежить память). Теперь о части unsafe.

ENV_LOCK — static ENV_LOCK: Mutex = Mutex::new();

Не буду вдаваться в мьютексную часть, но более-менее понимаю, что это: защита от условий гонки для многопоточных приложений. Обе операции .lock() и .unlock() разумны.

k.as_ptr() возвращает необработанный указатель только для чтения на наш ‘k’ (то есть CString из OsStr).

Затем мы вызываем libc::getenv(). Я более-менее догадываюсь, что это. В этой строке:

let s = libc::getenv(k.as_ptr()) as *const libc::c_char;

У меня вопрос: Почему as *const libc::c_char; в конце, а не:

let s: *const libc::c_char = libc::getenv(k.as_ptr());

Это вопрос предпочтений или что-то более важное? Следующая часть становится более интересной:

let ret = if s.is_null() {
            None
        } else {
            Some(OsStringExt::from_vec(CStr::from_ptr(s).to_bytes().to_vec()))
};

ret становится None, если возвращаемое значение было Null (.is_null очевидно), или становится Some (что, как я понимаю, делает "кусок" для Option). None в Rust не имеет ничего общего с None в Python, это просто аналог Some.

pub enum Option<T> {
    None,
    Some(T),
}

Затем мы (после разблокировки) возвращаем Ok этого значения).

Внутри Some есть окончательный ответ для моего исследования: у нас есть CStr с C-подобной строкой, или, в терминах Rust, это срез. Затем мы вызываем .to_bytes(), которая возвращает фрагмент без завершающего нуля (эти возвраты работают со слайсами, которые представляют собой просто причудливые указатели без фактически перемещенных байтов). to_bytes возвращает срез (срез только для чтения) в массив u8. И мы вызываем .to_vec, чтобы преобразовать его в векторы. Этот вектор передается в OsStringExt::from_vec(), и у нас есть наша OSString, которая быстро становится строкой.

Я хотел бы посмотреть, что делает libc::getenv.

Первый: что такое use libc::{self, c_int, c_char, c_void};? Мне непонятно, я вернусь к этому вопросу позже.

Я нашел libc в виде отдельного ящика.

pub fn getenv(s: *const c_char) -> *mut c_char;

Что говорит мне, что это похоже на C extern. Он указывает мне на http://man7.org/linux/man-pages/man3/getenv.3.html.

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

Это было долгое исследование, и мое выделенное время истекает. Я продолжу читать главу IO в следующий раз.

Вывод

Теперь я могу объяснить, почему Rust не возвращает &str для переменных окружения. Эти переменные хранятся в строках стиля C, которые могут быть непредставимы в Rust str (для которого требуется действительный Unicode). Более того, Rust str несовместим с CStr, потому что использует другую структуру. Почему Rust возвращает строку, а не &str? Причина проста: чтобы преобразовать CStr (или OSStr) в нативную форму Rust, нужно выделить память из кучи. Поскольку в определенный момент от этого выделения нужно отказаться, мы возвращаем приложению принадлежащую строку. Как только переменная с этим значением выходит за пределы области видимости, память освобождается.

Другой (странный) подход состоял бы в том, чтобы преобразовать все переменные среды в звездное время и вернуть им &str. Но это вызовет дополнительные накладные расходы для программ, не использующих переменные среды, и создаст проблемы для приложений, которые хотят их изменить. Таким образом, возвращаемое значение Stringas здесь является единственным жизнеспособным вариантом.