if !is_valid_email(&form.email) || !is_valid_password(&form.password) {
	return HttpResponse::BadRequest().finish();
}

Этот код — кусок дерьма; кошмар, который вот-вот случится. Чтобы понять, почему и как это исправить, сначала нужно понять главный урок, который мне преподал Rust: силу использования системы типов для обеспечения инвариантов.

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

struct BankAccount {
    // in cents
    balance: i32,
}

Но как мы можем соблюсти этот инвариант?

Есть несколько подходов, которые можно разделить на две категории: ручное соблюдение инварианта и его автоматическое соблюдение.

Ручное соблюдение инварианта

Ручное соблюдение инварианта включает в себя:

  • Код-ревью

  • Комментарии в коде

  • Документацию

  • Проектные документы

  • Даже устные договоренности, разделяемые между членами команды

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

Однако, если мы будем использовать ручной подход в нашем примере с банковским балансом, это быстро приведет нас к краху.

struct BankAccount {
    // in cents
    // should never be less than 0!
    balance: i32,
}

Мы не можем здесь позволить себе нарушение инварианта; нам нужен более надежный метод его обеспечения.

Автоматическое соблюдение инварианта

Включает в себя:

  • Ассерты в рантайме

  • Проверки в рантайме

  • Тестирование

  • Валидацию ввода

  • Использование системы типов

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

Ассерты

Начнем с ассертов как метода автоматического соблюдения инварианта.

struct BankAccount {
    balance: i32,
}

impl BankAccount {
    fn new(initial_balance: i32) -> Self {
        assert!(initial_balance >= 0, "Initial balance cannot be negative");
        Self {
            balance: initial_balance,
        }
    }

    fn deposit(&mut self, amount: i32) {
        assert!(amount >= 0, "Deposit amount cannot be negative");
        self.balance += amount;
    }

    fn withdraw(&mut self, amount: i32) {
        assert!(amount >= 0, "Withdrawal amount cannot be negative");
        assert!(self.balance >= amount, "Insufficient funds");
        self.balance -= amount;
    }
}

Мы утверждаем, что начальный баланс должен быть больше или равен нулю. И такие же ассерты ставим в методах deposit и withdraw.

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

Использование системы типов Rust

Давайте улучшим этот код, используя систему типов Rust. Мы изменим тип баланса с 32-битного знакового целого числа на 32-битное беззнаковое целое число, и баланс теперь в принципе не может быть отрицательным числом. Теперь можно удалить ассерты в функциях new и deposit, а также первый ассерт в функции withdraw.

impl BankAccount {
    fn new(initial_balance: u32) -> Self {
        Self {
            balance: initial_balance,
        }
    }

    fn deposit(&mut self, amount: u32) {
        self.balance += amount;
    }

    fn withdraw(&mut self, amount: u32) {
        assert!(self.balance >= amount, "Insufficient funds");
        self.balance -= amount;
    }
}

Однако нам все еще нужно убедиться, что на счету достаточно средств.

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

fn withdraw(&mut self, amount: u32) -> Result<u32, String> {
	if self.balance >= amount {
		self.balance -= amount;
		Ok(self.balance)
	} else {
		Err("Insufficient funds".to_string())
	}
}

Затем внутри функции мы выполним простую рантайм-проверку. Поскольку функция withdraw возвращает тип Result, она заставит вызывающий код обработать потенциальную ошибку. Мы также можем добавить тесты, чтобы убедиться, что withdraw работает правильно.

С этим подходом наш код не скомпилируется, или наши тесты завершатся с ошибкой, если инвариант будет нарушен. Это делает наш код гораздо более надежным.

Этот мощный метод проектирования программного обеспечения, использующий систему типов для обеспечения инвариантов, называется type-driven design. Хотя наш предыдущий пример был простым, система типов может обеспечивать соблюдение очень сложных инвариантов, особенно если язык статически типизирован и имеет выразительную систему типов, как Rust.

Почему этот код — кошмар?

В начале видео я сказал, что этот код — кошмар, который вот-вот произойдет:

if !is_valid_email(&form.email) || !is_valid_password(&form.password) {
	return HttpResponse::BadRequest().finish();
}

Почему?

#[post("/user/register")]
pub async fn register_user(
    form: web::Form<FormData>,
    pool: web::Data<PgPool>
) -> HttpResponse {
    if !is_valid_email(&form.email) || !is_valid_password(&form.password) {
        return HttpResponse::BadRequest().finish();
    }

    let user = User {
        email: form.email.clone(),
        password: form.password.clone(),
    };

    match insert_user(&pool, &user).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

У нас есть API-эндпоинт для создания новых пользователей. Инвариант заключается в том, что электронная почта и пароль должны быть всегда действительными. Мы обеспечиваем это через валидацию ввода: пользователи предоставляют непроверенные данные, и мы вызываем несколько функций валидации, чтобы убедиться, что введенные данные соответствуют нашим требованиям. Только после этого мы сохраняем данные в БД.

Проблема в том, что эти проверки выполняются только один раз — в начале обработчика запросов. Так может ли функция insert_user безопасно полагать, что электронная почта и пароль действительны?

#[derive(Debug)]
struct User {
    pub email: String,
    pub password: String,
}

async fn insert_user(pool: &PgPool, user: &User) -> Result<Uuid, sqlx::Error> {
    let user_id = Uuid::new_v4();
    let password = hash_password(user.password.as_str());

    // insert user into database ...

    Ok(user_id)
}

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

По мере роста и изменения кода вы можете представить, как проверка валидации случайно удаляется или данные каким-то образом изменяются, и вот мы уже приплыли:

#[post("/user/register")]
pub async fn register_user(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
    if !is_valid_email(&form.email) || !is_valid_password(&form.password) {
        return HttpResponse::BadRequest().finish();
    }

    let user = User {
        email: form.email.clone(),
        password: form.password.clone(),
    };

+   // remove sensitive data before logging
+   user.password.clear();
+   dbg!("Registering user: {:?}", &user);

    match insert_user(&pool, &user).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

Один из способов предотвратить это — снова выполнить валидацию внутри функции insert_user:

async fn insert_user(pool: &PgPool, user: &User) -> Result<Uuid, sqlx::Error> {
+   if !is_valid_email(&user.email) || !is_valid_password(&user.password) {
+       // return error...
+   }

    let user_id = Uuid::new_v4();
    let password = hash_password(user.password.as_str());

    // insert user into database ...

    Ok(user_id)
}

Однако это вводит ненужную избыточность и чревато ошибками.

Принцип «Не валидировать но парсить»

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

Сначала мы создадим два новых типа: Email и Password.

pub struct Email(String);
pub struct Password(String);

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

Чтобы обеспечить это, мы добавим функцию parse, которая принимает непроверенную строку в качестве ввода и парсит ее в тип Email или Password. Операция парсинга может завершиться неудачей, поэтому мы будем возвращать тип Result.

impl Email {
    pub fn parse(email: String) -> Result<Email, AuthError> {
        if !is_valid_email(&email) {
            Err(AuthError::ValidationError("Email must be valid".to_string()))
        } else {
            Ok(Email(email))
        }
    }
}

impl Password {
    pub fn parse(password: String) -> Result<Password, AuthError> {
        if !is_valid_password(&password) {
            Err(AuthError::ValidationError("Password must be valid".to_string()))
        } else {
            Ok(Password(password))
        }
    }
}

Здесь мы используем несколько уникальных особенностей системы типов Rust. Из-за правил видимости Rust внутренняя строка является приватной и недоступной за пределами структуры. И поскольку в Rust нет встроенных или стандартных конструкторов, единственный способ создать экземпляр Email или Password — через функцию parse.

Мы по-прежнему используем те же функции валидации, что и раньше, но теперь логика валидации содержится внутри типа, а состояние валидации сохраняется внутри типа. Мы также можем добавить метод as_str, чтобы предоставить доступ только для чтения к внутренним строковым данным.

impl Email {
    pub fn parse(email: String) -> Result<Email, AuthError> {
        // ...
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Password {
    pub fn parse(password: String) -> Result<Password, AuthError> {
        // ...
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

Теперь мы можем обновить структуру User, чтобы использовать наши новые типы:

#[derive(Debug)]
struct User {
    pub email: Email,
    pub password: Password,
}

и обновить функцию-регистратор:

#[post("/user/register")]
pub async fn register_user(
    form: web::Form<FormData>,
    pool: web::Data<PgPool>
-) -> HttpResponse {
-   if !is_valid_email(&form.email) || !is_valid_password(&form.password) {
-       return HttpResponse::BadRequest().finish();
-   }

-   let user = User {
-       email: form.email.clone(),
-       password: form.password.clone(),
-   };
+) -> Result<HttpResponse, AuthError> {
+   let email = Email::parse(form.email.clone())?;
+   let password = Password::parse(form.password.clone())?;

+   let user = User::new(email, password);

    match insert_user(&pool, &user).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

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

Продвинутые подходы в проектировании, основанном на типах

Проектирование на основе типов — большая тема, и это только верхушка айсберга. Мы только что говорили о принципе не валидировать но парсить и о том, как реализовать его, используя подход нового типа в Rust. Мы также можем использовать более сложные подходы, такие как type state pattern, который позволяет определить различные состояния, в которых может находиться объект, определить конкретные действия для каждого состояния и обеспечить допустимые переходы между состояниями.

Например, пользователь в нашем API может находиться в одном из трех состояний: зритель, редактор или администратор. Сначала мы создадим структуру, представляющую каждое состояние, а затем определим структуру User, которая является обобщенной по UserRole, по умолчанию являющейся зрителем.

pub struct Viewer;
pub struct Editor;
pub struct Admin;

pub struct User<UserRole = Viewer> {
    pub email: Email,
    pub password: Password,
    state: PhantomData<UserRole>,
}

impl User {
    pub fn new(email: Email, password: Password) -> Self {
        Self {
            email,
            password,
            state: PhantomData,
        }
    }
}

Мы будем хранить обобщение в поле state, которое использует PhantomData, чтобы избежать ненужного выделения памяти.

Затем мы можем определить методы, доступные для всех состояний, и методы, специфичные для состояний, такие как метод edit для редакторов. Мы также можем обеспечить правильные переходы между состояниями: зрители могут быть повышены до редакторов, редакторы до администраторов, а администраторы могут быть понижены до редакторов.

impl User<Viewer> {
    pub fn promote(self) -> User<Editor> { /*...*/ }
}

impl User<Editor> {
    pub fn edit(&self) { /*...*/ }
    pub fn promote(self) -> User<Admin> { /*...*/ }
}

impl User<Admin> {
    pub fn demote(self) -> User<Editor> { /*...*/ }
}

Обратите внимание, что мы используем модель владения Rust: эти функции перехода состояния принимают self в качестве ввода, что перемещает экземпляр в функцию и делает его недоступным в дальнейшем. Это означает, что если экземпляр зрителя будет повышен до редактора, старый экземпляр пользователя более не может быть использован.

fn main() {
    let viewer = User::new(
        Email::parse("bogdan@email.com".to_string()).unwrap(),
        Password::prase("password".to_string()).unwrap(),
    );

    let editor = viewer.promote();

    viewer.get_email(); // error: borrow of moved value 'viewer'
}

Итак, как начать использовать проектирование на основе типов в ваших собственных проектах на Rust? Есть много способов реализовать этот мощный метод проектирования программного обеспечения в Rust. Теоретически вы можете применить некоторые из этих паттернов и в других языках, но Rust делает это особенно практичным благодаря своему устройству системы типов. В других языках эти подходы не всегда практичны, если вообще возможны.

Комментарии (26)


  1. ionicman
    04.09.2024 08:49

    Я не силен в Rust, подскажите насчет баланса - вот если оно беззнаковое uin32 и = 0, что будет, если его уменьшить на, например, 5? Ибо в C это приведет просто к переполнению, что будет еще хуже, чем отрицательный баланс (там хоть понятно будет, на сколько ушло).

    Ну и проверка типов тоже сомнительна - вы переложили валидацию в тип - окей, можно было, как было сказано выше, поставить валидацию в саму функцию - и это тоже не проблема (не надо вот только про лишние проверки, можно подумать что с типом их не будет на этапе парсинга). Но суть проблемы здесь - отсуствие класса user, где все это должно быть инкапсулировано, те это архитектурная, а не синтаксическая проблема, ИМХО.


    1. PrinceKorwin
      04.09.2024 08:49

      вот если оно беззнаковое uin32 и = 0, что будет, если его уменьшить на, например, 5?

      Паника в текущем треде исполнения.

      вы переложили валидацию в тип - окей

      Немного не так. Не валидацию, а контроль. Это немного рвзные вещи. К сожалению статья мало этот аспект раскрывает.

      И типы это не классы с инкапсулированной валидацией.


      1. ionicman
        04.09.2024 08:49

        И типы это не классы с инкапсулированной валидацией.

        Естественно, но сути архитектурной проблемы с моей точки зрения это не меняет.


        1. PrinceKorwin
          04.09.2024 08:49

          Просто пример с email/password не очень показательный.

          Можно привести другой пример. Пусть, для упрощения, мы имеем корзину с товарами. Каждый товар может быть в разном количестве в корзинке. У товаров есть цена. И есть общая цена корзинки.

          Здесь мы можем выделить следующие типы:

          1. Корзина

          2. Товар

          3. Кол-во товаров одного типа

          4. Цена за 1 штуку товара

          Теперь можно определить возможные отношения и операции между этими типами.

          Например:

          1. тип Корзина может содержать в себе N тип Товары

          2. тип Кол-во товаров одного типа можно складывать только с таким-же типом (вычитать и другие мат. операции нельзя, например)

          3. тип Цена может быть суммирован только с таким же типом и может быть умножен на тип Кол-во товаров одного типа

          Благодаря этому компилятор не позволит скомпилировать код который случайно попытается умножить цену товара на просто число где бы вы не попытались это сделать. И для этого не нужно будет ни энкапсулировать логику в отдельном классе и следить чтобы эта логика не "утекла" из него. Ни надо расставлять нигде валидации. Компилятор всё проверит за вас.

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


          1. ionicman
            04.09.2024 08:49

            Можно так. Но опять-же это лишь перекладывание сложности.

            В приведенном вами случае, вы переложили ее в типы, если сделать класс "Корзина" - то сложность уйдет в нее.

            Что до меня - я выберу последнее, так как в жизни корзина куда сложнее устроена (скидки, акции, товары бандлом, огрничение на товарные позиции не больше/не меньше, товарная позиция, которая идет только бандлом с дургим товаром и тд). ИМХО класс в этом случае будет куда более удобным.

            Типы, естественно, никто не отменяет, но их я бы сделал просто наиболее универсальными и с ограничением только по edge-случаям.

            Попытаюсь переформулировать все мной сказанное - засовывать сильные ограничения в типы и потом с ними танцевать куда сложнее, чем делать проверку при нормальном подходе к ООП.

            Но да, в случае с типами будет доп. проверка на уровне компилятора - это плюс. Однако тесты все равно писать)

            Сейчас крайности в томже TSe - народ пытается все в типы запихать, везде юзать дженерики - это приводит к такой жести при поддержке потом, что это целая тема для отдельной статьи - не надо так.


            1. PrinceKorwin
              04.09.2024 08:49
              +1

              Можно так. Но опять-же это лишь перекладывание сложности.

              А сложность никогда никуда не девается. Просто типы предлагают переложить сложность на:

              • первичную проработку архитектуры типов

              • на момент компиляции

              Давая при этом гораздо больше гарантий правильности исполнения.

              В приведенном вами случае, вы переложили ее в типы, если сделать класс "Корзина" - то сложность уйдет в нее.

              Не совсем. Вам будет не достаточно класса "Корзина". Типы и их гарантии они сквозные - от обработки HTTP запроса, по бизнес-логике и до persist в БД. Вам нужно будет инкапсилуровать логику валидации в класс "Корзина" и в другие классы через которые потенциально эта Корзина ходит.

              Что до меня - я выберу последнее, так как в жизни корзина куда сложнее устроена

              Каждому своё. Типы и их возможности тоже куда сложнее и многограннее чем моя жалкая попытка их описать.

              Попытаюсь переформулировать все мной сказанное - засовывать сильные ограничения в типы и потом с ними танцевать куда сложнее, чем делать проверку при нормальном подходе к ООП.

              Полностью согласен. Сделать согласованное непротиворечивое описание типов в большом проекте - это тот еще челендж. Но и бенефита вы получаете гораздо больше чем "делать проверку при нормальном подходе к ООП".

              Я много пишу на Java и на Rust. И система типов в Rust меня не один раз спасала от ошибок которые бы Java пропустила. И я больше рад иметь возможность описывать типы тогда когда это имеет смысл, чем не иметь такой возможности.

              Но да, в случае с типами будет доп. проверка на уровне компилятора - это плюс. Однако тесты все равно писать)

              Ну нет. Вы же объявляя аргумент функции как int не будете делать тест проверяющий, что туда на вход пытаются передать String, это не имеет смысла - компилятор проверит. Вот и с типами так же.

              С другой стороны. Имея persistence для корзинки. Как вы напишете тест, что поле total amount корректно во всех сценариях было посчитано? Надо обкладывать тестами и следить чтобы ничего не пропустили в будущем.

              В случае с типами вам для этого вообще не нужно будет писать тестов. Гарантии даст компилятор. И эти гарантии будут распространяться также на будущий код.


    1. domix32
      04.09.2024 08:49
      +1

      Собственно, к языку тут отношения на самом деле примерно никакого. С тем же успехом можно было б взять условный C# и также с голыми интами и строками напилить if ради кривых примеров - суть не поменяется.

      вот если оно беззнаковое uin32 и = 0, что будет, если его уменьшить на, например, 5

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

      fn main() {
          let mut x = 0usize;
          x -= 1;
      }
      error: this arithmetic operation will overflow
       --> src/main.rs:3:5
        |
      6 |     x -= 1;
        |     ^^^^^^ attempt to compute `0_usize - 1_usize`, which would overflow
        |
        = note: `#[deny(arithmetic_overflow)]` on by default

      Для динамических данных в дебаге оно будет паниковать, в релизе - переполняться. Для знающих есть wrapping/saturating операции.


      1. ionicman
        04.09.2024 08:49

        У меня как раз и был вопрос - в рантайме в раст будет ли паника при уменьшении uint32 ниже нуля, как я понял из ответа - паника будет. Если не будет - тогда зачем все это делать ).

        В Си ничего не будет - просто будет число = макисмальное число от типа переменной - ( то что вычли - 1 ).


        1. PrinceKorwin
          04.09.2024 08:49
          +1

          Код на Rust:

          fn main() {
              let a: usize = 5;
              let b: usize = a - five();
              let c: usize = b - five();
              println!("Finished with value: {}", c)
          }
          
          fn five() -> usize {
              5
          }
          

          Результат компиляции и исполнения - DEBUG:

             Compiling playground v0.0.1 (/playground)
              Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
               Running `target/debug/playground`
          thread 'main' panicked at src/main.rs:4:20:
          attempt to subtract with overflow
          

          Результат компиляции и исполнения - RELEASE:

             Compiling playground v0.0.1 (/playground)
              Finished `release` profile [optimized] target(s) in 0.45s
               Running `target/release/playground`
          
          Finished with value: 18446744073709551611
          


        1. domix32
          04.09.2024 08:49

          В релизе будет переполнение, в дебаге - паника.


    1. AskePit Автор
      04.09.2024 08:49
      +1

      Про вопрос о вычитании: обычное вычитание через - запаникует. При желании в Rust можно воспользоваться встроенными в u32 (и в другие целочисленные типы) методами:

      // Вернет `None` при переполнении
      pub const fn checked_sub(self, rhs: u32) -> Option<u32>
      
      // Вычитание с враппингом по модулю
      pub const fn wrapping_sub(self, rhs: u32) -> u32
      
      // Вернет wrapped-результат и флаг переполнения
      pub const fn overflowing_sub(self, rhs: u32) -> (u32, bool)
      
      // Насыщающее вычитание
      pub const fn saturating_sub(self, rhs: u32) -> u32
      
      // Вычитание с паникой при переполнении, даже если отключена проверка на переполнение для обычного вычитания
      pub const fn strict_sub(self, rhs: u32) -> u32
      

      Вот как эти виды вычитаний будут себя вести для случаев переполнения:

      assert_eq!( 0u32.checked_sub(1), None );
      assert_eq!( 0u32.wrapping_sub(1), u32::MAX );
      assert_eq!( 0u32.overflowing_sub(1), (u32::MAX, true) );
      assert_eq!( 0u32.saturating_sub(1), 0 );
      0u32.strict_sub(1); // will panic
      


      1. PrinceKorwin
        04.09.2024 08:49
        +1

        Про вопрос о вычитании: обычное вычитание через - запаникует

        Только в debug сборке. В release отработает без паники. Пример кода и результата выше по треду.


        1. aiscy
          04.09.2024 08:49

          Может быть не совсем верно привязываться к профилю. Все же никто не помешает нам сделать

          [profile.release]
          overflow-checks = true


          1. PrinceKorwin
            04.09.2024 08:49

            Да, конечно! Но про такую особенность нужно, конечно знать заранее.


  1. Vladime
    04.09.2024 08:49

    Пример из "Parse don't validate" подан неверно уже в источнике, не сказано, например, что password должен быть приватным. Видимо, первоисточник: Parse, Don't validate: An Effective Error Handling Strategy, там изложено правильно.


    1. AskePit Автор
      04.09.2024 08:49

      Спасибо за приведенную вами статью, я искренне удивился, что перевожу даже не первичный контент, а вторичный :)

      По поводу приватного password: я прочитал первоисточник, но не смог заметить принципиально новой информации о приватности password. В данной статье и оригинальном видео сказано:

      Из-за правил видимости Rust внутренняя строка является приватной и недоступной за пределами структуры.

      Наверное, вы что-то иное имели в виду? Можете показать конкретный тест в первоисточнике, которого идеологически нехватает в видео Богдана?


      1. Vladime
        04.09.2024 08:49

        Я не знаю, какой из текстов первичен. Но у вас (а также на видео), есть код:

        #[derive(Debug)]
        struct User {
            pub email: Email,
            pub password: Password,
        }

        А дальше идет user.password.clear(); , что также может быть опасно (если реализовать метод clear() и случайно сделать mut user). Лучше, в любом случае, поля инкапсулировать. Хотя да, согласен, и по моей ссылке этого нет, приходится додумывать.


  1. Revertis
    04.09.2024 08:49

    Но ведь здесь проблема не решена от слова совсем:


    1. AskePit Автор
      04.09.2024 08:49

      Дико прощу прощения — это кривые руки автора-переводчика. Поправил это, спасибо за указание


      1. Revertis
        04.09.2024 08:49

        То есть убрали код, который мог привести к ошибкам, и решили, что проблема решена???


        1. AskePit Автор
          04.09.2024 08:49

          Этот код не скомпилировался бы, т.к. к password теперь нельзя достучаться напрямую — он теперь исключительно readonly через as_str(&self) -> &str.

          Поэтому формально да — все как вы сказали, мы убираем код, который теперь не компилируется, и проблема действительно решена. И даже недосмотр автора перевода не может сломать программу


          1. Revertis
            04.09.2024 08:49

            Так ведь далее возникнет так же задача вывести в лог данные юзера с санитацией. Как будет решаться ЭТА проблема?


            1. AskePit Автор
              04.09.2024 08:49

              Я не вижу принципиальной проблемы в ЭТОЙ проблеме. Если вы грамотно простроите интерфейс класса юзера, вы прекрасно сможете справиться с поставленной задачей.

              Вы, конечно можете спросить меня: "А как тогда будет решаться проблема X?", "А проблема Y???", "А проблема Z???!!!1!!1!", и мы с вами сможем в прямом эфире написать прямо здесь, в хабра-комментариях полноценное приложение — благо проблем можно придумать на ходу сколько пожелается. Но неизменным будет, что не всегда решение одной проблемы будет универсально покрывать и решать все остальные проблемы разрабатываемого приложения.


              1. Revertis
                04.09.2024 08:49

                Так изначальный посыл автора был в том, что если кто-то захочет вывести в лог данные юзера, то их надо будет санитайзить, и тут-то возникнет проблема.

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


                1. 0x1b6e6
                  04.09.2024 08:49
                  +2

                  Но в итоге никто не показал как решить именно эту проблему.

                  Тогда я напишу. Это можно решить переопределением трейта Debug для типа Password:

                  #[derive(Debug)]
                  struct User {
                      email: Email,
                      password: Password,
                  }
                  
                  #[derive(Debug)]
                  struct Email(String);
                  
                  struct Password(String);
                  
                  impl std::fmt::Debug for Password {
                      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                          f.write_str("[hidden]")
                      }
                  }
                  
                  fn main() {
                      let user = User {
                          email: Email("mail@example.com".to_string()),
                          password: Password("my p@$$w0rD".to_string()),
                      };
                  
                      println!("user = {user:?}");  // user = User { email: Email("mail@example.com"), password: [hidden] }
                  }
                  


                  1. Revertis
                    04.09.2024 08:49

                    Вооот! То есть можно убрать parse() и вообще добавить реализацию Debug для User, а не только для Password.

                    Хотя да, для валидации данных parse() лучше оставить.