Всем привет!

TL;DR: Как отличить строку «имя» от строки «e-mail»? К примеру, чтобы не ошибиться в передаче в функцию. На помощь приходит паттерн Newtype, но его не очень удобно использовать. В статье разберем как добавить удобства.

Итак. В Rust создание обертки над типом (struct Username(String)) полезно для строгой типизации, но неудобно в использовании — приходится постоянно обращаться к полю .0. Использование трейта Deref позволяет обертке «притворяться» внутренним типом.

Зачем это нужно

  1. Семантическая строгость: Вы не перепутаете Username и Email, хотя оба — строки. Это дает не сам Deref, а само создание обертки.

  2. Прозрачный API: Вы получаете доступ ко всем методам внутреннего типа (например, .len(), .is_empty(), .contains()) без дублирования кода.

Реализация

use std::ops::Deref;

struct Username(String);// Newtype

impl Deref for Username {
    type Target = str; // Указываем целевой тип для разыменования

    fn deref(&self) -> &Self::Target {
        &self.0 // Возвращаем ссылку на внутренние данные
    }
}

Как это работает на практике

Благодаря Deref-coercion, компилятор автоматически вызывает .deref(), когда видит, что тип не совпадает с ожидаемым, но может быть к нему приведен.

let name = Username("Anton".to_string());

// 1. Прямой вызов методов внутреннего типа &str
println!("{}", name.len());        
println!("{}", name.contains("A")); 

// 2. Использование в функциях, принимающих &str
fn greet(s: &str) {
    println!("Hello, {s}");
}

greet(&name); // Работает автоматически

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

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


  1. Dhwtj
    10.01.2026 07:51

    Просто напишу идиоматичный вариант

    use std::fmt;
    use std::str::FromStr;
    use serde::{Deserialize, Serialize};
    use thiserror::Error;
    
    #[derive(Error, Debug)]
    pub enum EmailError {
        #[error("invalid email format")]
        InvalidFormat,
    }
    
    #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
    #[serde(transparent)]
    pub struct Email(String);
    
    impl Email {
        pub fn new(value: impl Into<String>) -> Result<Self, EmailError> {
            let s = value.into();
            if s.contains('@') && s.contains('.') {
                Ok(Self(s))
            } else {
                Err(EmailError::InvalidFormat)
            }
        }
    
        pub fn as_str(&self) -> &str {
            &self.0
        }
    }
    
    impl fmt::Display for Email {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            self.0.fmt(f)
        }
    }
    
    impl FromStr for Email {
        type Err = EmailError;
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            Self::new(s)
        }
    }
    
    impl AsRef<str> for Email {
        fn as_ref(&self) -> &str {
            &self.0
        }
    }
    
    impl<'de> Deserialize<'de> for Email {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::Deserializer<'de>,
        {
            let s = String::deserialize(deserializer)?;
            Self::new(s).map_err(serde::de::Error::custom)
        }
    }

    Большая часть не нужна, правда.

    А вот с ускорялками

    use derive_more::{AsRef, Display, From};
    use nutype::nutype;
    
    #[nutype(
        validate(predicate = |s| s.contains('@') && s.contains('.')),
        derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Display, Serialize, Deserialize)
    )]
    pub struct Email(String);
    
    // Использование
    fn send(to: Email, subject: &str) {
        println!("Sending '{}' to {}", subject, to);
    }
    
    fn main() -> Result<(), EmailParseError> {
        let email = Email::new("user@example.com")?;
    
        // или через parse
        let email2: Email = "other@test.org".parse()?;
    
        send(email, "Hello");
    
        // доступ к значению
        println!("{}", email2.as_ref());
    
        Ok(())
    }

    Или не писать derive AsRef , чтобы совсем инкапсуляция была


    1. Dhwtj
      10.01.2026 07:51

      Пардон, EmailError

      nutype автоматически генерирует тип ошибки по имени структуры: Email => EmailError


    1. anton_dolganin Автор
      10.01.2026 07:51

      Интересное дополнение, спасибо. Но меня больше удивил подход с десериализацией e-mail. Это вы просто кусок кода взяли свой или считаете, что сразу лучше забустить тип e-mail "на будущее"?