Когда и зачем использовать AsRef‹T› вместо &T

AsRef документация пишет

Используется для дешевого преобразования ссылки на ссылку.

Я понимаю часть reference-to-reference что означает cheap? Надеюсь, это никак не связано со сложностью теоретической (большой о. и т.п.) дешевизны.

Пример:

User {
  email: String, 
  age: u8,
}

impl AsRef<User> for User {
  fn as_ref(&self) -> &User {
     &self
  }
}

fn main() {
  let user = User { email: String::from("[email protected]"), age: 25 };
  let user_ref = &user;
  //...
}

В чем причина реализации AsRef для User, если я могу взять ссылку просто &user? Каково правило реализации AsRef?

PS: я не смог найти ничего, отвечающего на эти вопросы, на других форумах и в других документах.


person HardFork    schedule 03.02.2021    source источник


Ответы (2)


Как вы заметили, impl AsRef<User> for User кажется немного бессмысленным, поскольку вы можете просто сделать &user. Вы можете использовать impl AsRef<String> for User или impl AsRef<u8> for User в качестве альтернативы &user.email и &user.age, но эти примеры, вероятно, являются неправильным использованием черты. Что означает значит возможность преобразования User в &String? Является ли &String их электронной почтой, их именем, их фамилией, их паролем? Это не имеет большого смысла и разваливается в тот момент, когда User имеет более одного поля String.

Допустим, мы начинаем писать приложение, и у нас есть только User с электронной почтой и возрастом. Мы бы смоделировали это в Rust следующим образом:

struct User {
    email: String,
    age: u8,
}

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

struct User {
    email: String,
    age: u8,
}

enum Privilege {
    // imagine different moderator privileges here
}

struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

Теперь мы могли просто добавить вектор privileges непосредственно в структуру User, но поскольку менее 1% User будут Moderator, добавление вектора к каждому отдельному User кажется пустой тратой памяти. Добавление типа Moderator заставляет нас писать немного неуклюжий код, потому что все наши функции по-прежнему принимают Users, поэтому мы должны передать им &moderator.user:

#[derive(Default)]
struct User {
    email: String,
    age: u8,
}

enum Privilege {
    // imagine different moderator privileges here
}

#[derive(Default)]
struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

fn takes_user(user: &User) {}

fn main() {
    let user = User::default();
    let moderator = Moderator::default();
    
    takes_user(&user);
    takes_user(&moderator.user); // awkward
}

Было бы очень хорошо, если бы мы могли просто передать &moderator любой функции, ожидающей &User, потому что модераторы на самом деле просто пользователи с несколькими дополнительными привилегиями. С AsRef мы можем! Вот как мы это реализуем:

#[derive(Default)]
struct User {
    email: String,
    age: u8,
}

// obviously
impl AsRef<User> for User {
    fn as_ref(&self) -> &User {
        self
    }
}

enum Privilege {
    // imagine different moderator privileges here
}

#[derive(Default)]
struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

// since moderators are just regular users
impl AsRef<User> for Moderator {
    fn as_ref(&self) -> &User {
        &self.user
    }
}

fn takes_user<U: AsRef<User>>(user: U) {}

fn main() {
    let user = User::default();
    let moderator = Moderator::default();
    
    takes_user(&user);
    takes_user(&moderator); // yay
}

Теперь мы можем передать &Moderator любой функции, ожидающей &User, и для этого потребовался лишь небольшой рефакторинг кода. Кроме того, этот шаблон теперь масштабируется для любого количества типов пользователей, мы можем добавить Admins, PowerUsers и SubscribedUsers, и пока мы реализуем для них AsRef<User>, они будут работать со всеми нашими функциями.

Причина, по которой &Moderator в &User работает из коробки без необходимости писать явный impl AsRef<User> for &Moderator, заключается в общая реализация в стандартной библиотеке:

impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
    T: AsRef<U>,
{
    fn as_ref(&self) -> &U {
        <T as AsRef<U>>::as_ref(*self)
    }
}

Что в основном просто говорит, что если у нас есть impl AsRef<U> for T, мы также автоматически получаем impl AsRef<U> for &T для всех T бесплатно.

person pretzelhammer    schedule 03.02.2021
comment
Возможно, стоит отметить, что AsRef<U> является общим impl для любого &T, где T: AsRef<U>, поэтому вы можете вызывать takes_user(&user) и не нужно передавать user по значению. Это большая часть того, что делает его полезным в качестве конверсионного признака общего назначения (эталона). - person trentcl; 03.02.2021
comment
Но я до сих пор не понимаю, что это мне дает. Я вижу, что &moderator лучше, чем &moderator.user, но я также могу отправить &moderator и взять пользователя как user = moderator.user внутри take_user вместо того, чтобы писать уродливое <U: AsRef<User>>. Ясно и прямолинейно. - person HardFork; 03.02.2021
comment
@HardFork Как бы вы передали &Moderator в takes_user(user: &User)? Вы не можете, это ошибка компиляции. Вы должны сделать takes_user<U: AsRef<User>>(user: U). Кроме того, можно не любить черту и не использовать ее в своих проектах API. Если в вашем приложении нет варианта использования AsRef, то никто не заставляет вас его использовать. Стандартная библиотека Rust получает достойный пробег от трейта предоставляя импликации для AsRef<Path> и AsRef<OsStr> для множества различных типов строк. - person pretzelhammer; 03.02.2021
comment
@крендельхаммер take_user(&Moderator m) {...} - person HardFork; 03.02.2021
comment
@HardFork вы изменили сигнатуру функции, чтобы она больше не работала для обычных пользователей, теперь передача &User является ошибкой времени компиляции. - person pretzelhammer; 03.02.2021
comment
@pretzelhammer Зачем мне проходить User? Просто пройди модератор. Вы можете получить доступ к пользователю, если у вас есть модератор. - person HardFork; 03.02.2021
comment
@HardFork все модераторы являются пользователями, но не все пользователи являются модераторами. В своем ответе я привел пример, что менее 1% пользователей будут модераторами. Если вы измените сигнатуру функции, чтобы принимать только модераторов, вы исключите 99% ваших пользователей. - person pretzelhammer; 03.02.2021
comment
@pretzelhammer Хорошо, вы имеете в виду, что хотите сделать функцию универсальной, например, take_user должен иметь возможность принимать все, что реализует AsRef‹User›. - person HardFork; 03.02.2021
comment
@HardFork Да, это основной вариант использования черты для написания универсальных функций. Если вы пишете любые программы на Rust, работающие с файловой системой, вы заметите, что многие стандартные библиотечные функции и методы являются универсальными и принимают <P: AsRef<Path>> в качестве аргумента. - person pretzelhammer; 03.02.2021

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

Это абсолютно так. AsRef предназначен практически ничего не стоит.

В чем причина реализации AsRef для пользователя, если я могу получить ссылку просто с помощью &user?

Там, вероятно, не один. AsRef полезен для универсального кода, особенно (хотя и не исключительно) для эргономики.

Например, если std::fs::rename взял &Path, вам нужно было бы написать:

fs::rename(Path::new("a.txt"), Path::new("b.txt"))?;

что многословно и раздражает.

Однако, поскольку он действительно принимает AsRef<Path>, он работает со строками из коробки, то есть вы можете просто вызвать:

fs::rename("a.txt", "b.txt")?;

который совершенно прямолинеен и гораздо более читабелен.

person Masklinn    schedule 03.02.2021
comment
rename будет выглядеть даже (немного) хуже, так как вам придется ссылаться на пути: fs::rename(&Path::new("a.txt"), &Path::new("b.txt"))? - person user4815162342; 04.02.2021
comment
@user4815162352 нет, это немного необычно, но Path::new на самом деле возвращает &Path. Иначе и быть не могло, так как Path не имеет размеров, голый Path по сути то же самое, что и голый str. - person Masklinn; 04.02.2021