В библиотечном API нужно реализовать преобразование между двумя типами ошибок из внешних crate. Как обойти правило согласованности трейтов Rust?
Нельзя напрямую реализовать From<ЧужаяОшибкаA> for ЧужаяОшибкаB, если и трейт From, и целевой тип определены вне вашего crate. Для адаптации используют локальный тип: обычно собственное перечисление ошибок или newtype-обёртку. После этого реализация From становится допустимой, потому что участвует тип, принадлежащий вашему crate.
Правило согласованности трейтов, также называемое orphan rule, является частью модели трейтов Rust. Оно предотвращает ситуацию, когда разные crate независимо объявляют несовместимые реализации одного и того же трейта для одной пары типов.
Без этого правила итог разрешения трейтов мог бы зависеть от набора подключённых зависимостей. Rust ограничивает такие реализации, чтобы поведение программы оставалось однозначным и проверяемым компилятором.
Предположим, библиотека использует внешнюю ошибку ввода-вывода и хочет представить её как ошибку другого внешнего crate. Реализация From между этими двумя типами запрещена: ни трейт From, ни тип назначения не являются локальными.
Типичный обход через псевдоним не работает. Псевдоним типа только даёт другое имя существующему типу и не делает его локальным, поэтому ограничение согласованности сохраняется.
Создайте собственный тип ошибки. Для публичного API чаще подходит перечисление, объединяющее смысловые причины сбоя. В него можно добавить варианты для внешних ошибок и реализовать локальные преобразования From:
ConfigError локален, поэтому обе реализации разрешены. Это позволяет использовать ? в функциях, возвращающих Result<_, ConfigError>: оператор применит соответствующее преобразование ошибки через From.
Newtype подходит, когда нужно обернуть один внешний тип и, возможно, реализовать для обёртки дополнительные локальные трейты. Он сохраняет типовую границу, но добавляет структуру и необходимость явно извлекать внутреннее значение.
Собственное перечисление лучше выражает доменную модель и не связывает публичный API с деталями конкретной зависимости. Однако оно требует поддержки вариантов, отображения ошибок и решения вопроса о совместимости при добавлении новых вариантов.
Не следует автоматически превращать все ошибки в Box<dyn std::error::Error>, если вызывающему коду нужно программно различать причины. Такая форма удобна для границы приложения, но обычно хуже подходит для типизированного публичного API.
Библиотека загружает конфигурацию: одна операция чтения выдаёт ошибку внешнего клиента, другая разбирает содержимое и выдаёт ошибку парсера. Рассматривались два варианта.
Возвращать конкретную внешнюю ошибку проще, но это раскрывает детали реализации и затрудняет смену зависимости. Возвращать Box<dyn std::error::Error> гибче по составу ошибок, но клиент теряет удобное сопоставление с конкретными причинами.
Выбран локальный ConfigError с отдельными вариантами и реализациями From для используемых внешних ошибок. В результате библиотека сохраняет типизированный контракт, оператор ? работает без ручного преобразования, а смена внутренней зависимости ограничивается кодом адаптера.
Нет. Псевдоним не создаёт новый тип и не меняет владельца исходного типа. Например, псевдоним внешнего типа всё равно считается тем же внешним типом, поэтому реализация трейта между двумя внешними типами останется запрещённой. Для изменения границы нужен действительно новый тип — структура, перечисление или newtype.
From для собственного типа?Локальность целевого типа сама по себе недостаточна для устранения пересечений. Обобщённая реализация вроде From<T> for MyError может пересекаться с другими реализациями, включая реализации, которые появятся в зависимостях или будущих версиях кода. Компилятор запрещает потенциально конфликтующие реализации, поэтому преобразования обычно перечисляют явно для конкретных внешних типов.
Newtype уместен, если нужно сохранить один внешний тип, скрыть его от публичного API или реализовать для него локальное поведение. Перечисление предпочтительно, когда библиотека поддерживает несколько смысловых причин сбоя и хочет дать клиенту стабильную доменную модель. Компромисс заключается в том, что перечисление требует явно управлять совместимостью вариантов, а newtype хуже выражает множество разных причин.