сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Трейты и дженерики

Orphan rule и newtype в Rust

Orphan rule и newtype

Практический блок: почему нельзя реализовать Display для Vec<u8>, как это обходят и во что обходится обёртка. Заодно проверяют понимание, зачем языку вообще нужна согласованность реализаций.

Стержень: для пары трейт-тип должна существовать не более одной реализации во всей программе, поэтому чужой трейт для чужого типа реализовать нельзя.

// Формулировки: «почему impl Display for Vec<u8> запрещён?», «что такое newtype и сколько он стоит?», «почему в Rust нет специализации?»

Почему нельзя чужое для чужого

Представь, что два независимых крейта реализовали Display для Vec<u8> по-своему. Пока они живут порознь - всё в порядке, но как только оба попали в одну программу, выбор становится неразрешимым, и семантика кода начинает зависеть от списка зависимостей.

Orphan rule закрывает этот сценарий: хотя бы одна сторона - трейт или тип - должна принадлежать вашему крейту. Иначе E0117. Это цена за свойство, которое называют согласованностью: для пары трейт-тип реализация во всей программе одна.

// Правило иногда мешает, но альтернатива хуже: конфликты, проявляющиеся только при определённом наборе зависимостей.

orphan rule
запрет реализовывать чужой трейт для чужого типа
coherence
не более одной реализации на пару трейт-тип

Newtype: обход и типобезопасность

Стандартный обход - обернуть чужой тип в свой: struct Hex(Vec<u8>), и Display пишется уже для Hex. Кортежная структура с одним полем имеет ровно ту же раскладку, что и поле, поэтому в рантайме обёртки нет - только на уровне типов.

Тот же приём используют и без всякого orphan rule, ради безопасности: Meters(f64) и Seconds(f64) перестают складываться друг с другом, а функция, ждущая UserId, не примет случайный i64. Ошибки такого рода ловятся компилятором и стоят ноль.

// Минус один: обёртка не наследует методы внутреннего типа. Нужные пробрасывают вручную или реализуют Deref, но с Deref стоит быть осторожным, он размывает границу абстракции.

struct Hex(Vec<u8>);
impl std::fmt::Display for Hex { /* .. */ }
newtype
обёртка из одного поля с собственным типом
нулевая стоимость
раскладка обёртки совпадает с раскладкой поля

Blanket impl и отсутствие специализации

Сплошная реализация раздаёт поведение целому семейству: impl<T: Display> ToString for T в стандартной библиотеке - именно она. Побочный эффект в том, что после такого impl добавить частный случай для конкретного типа уже нельзя: компилятор увидит перекрытие.

Отсюда частый вопрос: почему в стабильном Rust нет специализации, когда более конкретная реализация побеждает общую. Проблема не техническая, а в согласованности: наивная специализация позволяет поведению зависеть от лайфтаймов, которые стираются, то есть от того, чего в рантайме уже нет. Фича живёт под флагом min_specialization и используется внутри std, где ограничения контролируются вручную.

// Ещё одна полезная сплошная реализация: из From<A> for B автоматически следует Into<B> for A. Поэтому в сигнатурах просят Into, а реализуют всегда From.

blanket impl
реализация трейта сразу для всех подходящих типов
специализация
приоритет более конкретной реализации; в стабильном Rust отсутствует

Как отвечать: «Почему нельзя реализовать Display для Vec<u8> и что делать?»

Мешает orphan rule: и трейт, и тип чужие, а язык гарантирует, что на пару трейт-тип во всей программе есть не больше одной реализации. Если бы два крейта дали Vec<u8> разные Display, при сборке вместе выбор стал бы неразрешимым. Обхожу это через newtype: заворачиваю в свою структуру Hex(Vec<u8>) и пишу Display уже для неё. В рантайме такая обёртка бесплатна - у кортежной структуры с одним полем та же раскладка, что у поля. Заодно она часто полезна сама по себе: типобезопасность вроде Meters и Seconds, которые не должны складываться.

Ты объясняешь причину запрета через конфликт реализаций, даёшь стандартный обход и добавляешь, что он ничего не стоит. Три вещи, которые интервьюер как раз и хочет проверить.

На чём валятся

  • Считают newtype дорогой обёрткой - в рантайме её нет.
  • Не могут объяснить, зачем нужен orphan rule, и говорят «просто такое правило».
  • Пишут blanket impl, а потом не могут добавить частный случай для одного типа.
  • Ждут, что специализация работает, как перегрузка в C++.
  • Реализуют Into вместо From и лишаются автоматического второго направления.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_coherence1 / 5
    Сколько стоит обёртка-newtype struct Meters(f64) в рантайме?
    A)Восемь байт сверху: обёртка хранит тег типа
    B)Ничего: раскладка совпадает с f64, обёртка исчезает при компиляции
    C)Зависит от оптимизаций: в debug обёртка остаётся, в release пропадает
    D)Один указатель: значение переезжает в кучу под обёртку
    показать ответ и разбор
    +B)Ничего: раскладка совпадает с f64, обёртка исчезает при компиляции

    // разбор: Кортежная структура с одним полем имеет ту же раскладку, что и поле: никаких тегов и косвенности. Поэтому newtype — стандартный приём и для обхода orphan rule, и для типобезопасности: Meters и Seconds перестают складываться друг с другом, а машинный код остаётся прежним.

  2. #rs_coherence2 / 5
    Что делает impl<T: Display> MyTrait for T { ... }?
    A)Реализует MyTrait разом для всех типов с Display — blanket impl
    B)Создаёт заготовку, которую конкретные типы должны переопределить
    C)Объявляет, что MyTrait требует Display как супертрейт
    D)Разрешает приводить Display-типы к MyTrait через as
    показать ответ и разбор
    +A)Реализует MyTrait разом для всех типов с Display — blanket impl

    // разбор: Сплошная реализация раздаёт поведение всему семейству типов сразу — так работает ToString: он реализован для каждого T: Display. Побочный эффект — конфликты: после такого impl отдельную реализацию MyTrait для конкретного типа с Display написать уже не выйдет, компилятор увидит перекрытие.

  3. #rs_coherence3 / 5
    Почему в стабильном Rust нет специализации — более конкретная реализация не перекрывает общую?
    A)Специализация требует рантайм-информации о типе, которой нет
    B)Компилятор не умеет сравнивать реализации по конкретности
    C)Она несовместима с мономорфизацией дженериков
    D)Она ломает согласованность: выбор зависит от лайфтаймов
    показать ответ и разбор
    +D)Она ломает согласованность: выбор зависит от лайфтаймов

    // разбор: Наивная специализация позволяет заметить разницу там, где её быть не должно: лайфтаймы стираются, а поведение при этом менялось бы в зависимости от того, что компилятор знает в точке вызова — это дырка в надёжности. Фича живёт под флагом min_specialization и используется внутри стандартной библиотеки, где ограничения контролируются вручную.

  4. #rs_coherence4 / 5
    Реализовали From<u8> для своего типа C. Что появляется автоматически?
    A)TryFrom<u8> с проверкой диапазона
    B)From<C> для u8 — обратное преобразование
    C)Into<C> для u8: 5u8.into() вернёт C
    D)Display для C через форматирование исходного значения
    показать ответ и разбор
    +C)Into<C> для u8: 5u8.into() вернёт C

    // разбор: В стандартной библиотеке есть сплошная реализация: если есть From<A> for B, то появляется Into<B> for A. Поэтому в API пишут ограничение impl Into<C>, а реализуют всегда From — так вызывающий получает и явный C::from(x), и короткий x.into(). Обратное преобразование и Display надо писать отдельно.

  5. #rs_coherence5 / 5
    Можно ли реализовать свой трейт для чужого типа, например для i32?
    A)Да, но только через newtype-обёртку вокруг исходного типа
    B)Нет: реализации для типов стандартной библиотеки запрещены во внешних крейтах
    C)Да: трейт ваш, и этого достаточно для соблюдения правила сиротства
    D)Да, если пометить реализацию атрибутом, разрешающим внешние impl-блоки
    показать ответ и разбор
    +C)Да: трейт ваш, и этого достаточно для соблюдения правила сиротства

    // разбор: Правило требует, чтобы вашим был либо трейт, либо тип. Свой трейт для i32, для Vec<T>, для String — законная и очень частая практика: так работают крейты-расширения вроде itertools, добавляющие методы к чужим итераторам. Запрещена только комбинация «чужой трейт для чужого типа» — там и появляется newtype.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.