Определите результат вызовов: какой метод выбирается через точечный синтаксис, если у типа есть одноимённый inherent-метод и реализация трейта?
trait Render {
fn draw(&self) -> &'static str;
}
struct Button;
impl Button {
fn draw(&self) -> &'static str { "inherent" }
}
impl Render for Button {
fn draw(&self) -> &'static str { "trait" }
}
fn main() {
let button = Button;
println!("{}", button.draw());
println!("{}", Render::draw(&button));
}
Программа напечатает inherent, затем trait. При вызове button.draw() Rust сначала рассматривает метод, определённый непосредственно для типа Button, поэтому выбирает inherent-метод. Запись Render::draw(&button) явно задаёт реализацию трейта и вызывает его метод.
В Rust методы, объявленные в impl Button, и методы, полученные через impl Render for Button, решают разные задачи. Inherent-методы описывают поведение самого типа, а трейты позволяют добавлять общий контракт и использовать его в обобщённом коде.
Такое разделение нужно, чтобы тип мог иметь собственные операции независимо от трейтов, но при этом участвовать в полиморфном API. Поэтому язык определяет отдельные правила поиска методов и способ явного выбора реализации.
Одинаковое имя метода может появиться одновременно в inherent-реализации и в реализации трейта. Если полагаться только на название метода, можно ошибочно предположить, что вызов всегда обращается к трейту.
Неверный выбор особенно опасен при рефакторинге: добавление inherent-метода может изменить поведение вызовов с точечным синтаксисом. При этом вызов через обобщённый параметр или trait object может разрешаться уже по другим правилам.
При выражении button.draw() компилятор ищет подходящий метод для известного конкретного типа. Если найден применимый inherent-метод, он имеет приоритет над методами, доступными через трейты.
Вызов Render::draw(&button) использует UFCS-подобную квалифицированную запись: имя трейта явно указывает, из какого трейта брать метод. Эквивалентная форма с явным типом реализации выглядит так:
Квалифицированный вызов полезен не только при конфликте имён. Он позволяет явно выбрать конкретную реализацию трейта и устраняет неоднозначность, если несколько трейтов предоставляют методы с одинаковым именем.
Важное последствие проявляется в generic-коде. Если функция принимает T: Render, компилятор при проверке тела функции знает только bound Render, а не все inherent-методы каждого возможного конкретного типа. Поэтому value.draw() внутри такой функции означает вызов метода трейта Render.
Для trait object &dyn Render вызов метода трейта выполняется через таблицу виртуальных методов, если метод совместим с использованием trait object. Inherent-метод конкретного типа не становится частью интерфейса dyn Render, поэтому через такой объект он недоступен.
В библиотеке Button сначала имел trait-метод draw, а затем в него добавили inherent-метод с тем же именем для специальной отрисовки в одном из внутренних сценариев. После этого прямые вызовы button.draw() начали использовать новую inherent-реализацию, тогда как generic-функции с bound T: Render продолжили вызывать trait-метод.
Рассматривались варианты:
Выбрали сохранение API и явные вызовы <Button as Render>::draw(&button) там, где требовался контракт трейта. В пользовательском коде оставили button.draw() для основной операции типа, а тестами зафиксировали различие поведения.
Изменится ли выбор метода в generic-функции?
Да. В функции fn paint<T: Render>(value: T), вызов value.draw() разрешается по доступным ограничениям типа T, то есть через Render. Inherent-метод конкретного типа не учитывается, потому что T может быть любым типом, реализующим этот трейт.
Как явно вызвать trait-метод при наличии нескольких реализаций с одинаковым именем?
Нужно квалифицировать вызов именем трейта: Render::draw(&button) или <Button as Render>::draw(&button). Если тип реализует два трейта с методом draw, обычный вызов button.draw() через трейты может стать неоднозначным, а квалифицированная запись однозначно задаёт требуемый контракт.
Почему наличие inherent-метода не делает его доступным через &dyn Render?
Значение &dyn Render содержит только данные и указатель на таблицу методов, описанных интерфейсом Render. Inherent-методы Button не входят в этот интерфейс и не записываются в таблицу trait object. Чтобы вызвать такой метод, нужно сначала работать с конкретным типом, например после безопасного восстановления конкретного типа через подходящий API, а не напрямую через dyn Render.