В чем разница между синтаксисом вызова метода `foo.method ()` и UFCS `Foo :: method (& foo)`? - PullRequest
3 голосов
/ 08 ноября 2019

Есть ли разница в Rust между вызовом метода для значения, например:

struct A { e: u32 }

impl A {
    fn show(&self) {
        println!("{}", self.e)
    }
}

fn main() {
    A { e: 0 }.show();
}

... и вызовом его для типа, например:

fn main() {
    A::show(&A { e: 0 })
}

1 Ответ

3 голосов
/ 08 ноября 2019

Резюме : Наиболее важным отличием является то, что u универсальный f unction c all s yntax (UFCS) является более явным , чем синтаксис вызова метода .

В UFCS практически отсутствует двусмысленность в отношении того, какую функцию вы хотите вызвать (есть еще более длинная форма UFCS для методов черты, но давайте пока проигнорируем это). Синтаксис вызова метода, с другой стороны, требует больше работы в компиляторе, чтобы выяснить, какой метод вызывать и как его вызывать. Это проявляется в основном в двух вещах:

  • Разрешение метода : выяснить, является ли метод внутренним (связанным с типом, а не признаком) или методом признака. И в последнем случае также выясните, к какому признаку он принадлежит.
  • Определите правильный тип получателя (self) и, возможно, используйте приведения типа , чтобы заставить вызов работать.



Принуждения типа получателя

Давайте рассмотрим этот пример, чтобы понять приведения типа к типу получателя:

struct Foo;

impl Foo {
    fn on_ref(&self) {}
    fn on_mut_ref(&mut self) {}
    fn on_value(self) {}
}

fn main() {
    let reference = &Foo;    // type `&Foo`
    let mut_ref = &mut Foo;  // type `&mut Foo`
    let mut value = Foo;     // type `Foo`

    // ... 
}

Итак, у нас есть три метода, которые принимают приемник Foo, &Foo и &mut Foo, и у нас есть три переменные с этими типами. Давайте попробуем все 9 комбинаций, синтаксис вызова метода и UFCS.

UFCS

Foo::on_ref(reference);    
//Foo::on_mut_ref(reference);  error: mismatched types
//Foo::on_value(reference);    error: mismatched types

//Foo::on_ref(mut_ref);      error: mismatched types
Foo::on_mut_ref(mut_ref);  
//Foo::on_value(mut_ref);    error: mismatched types

//Foo::on_ref(value);      error: mismatched types
//Foo::on_mut_ref(value);  error: mismatched types
Foo::on_value(value);

Как мы видим, только вызовы завершаются успешно, если типы верны. Чтобы другие вызовы работали, мы должны вручную добавить & или &mut или * перед аргументом. Это стандартное поведение для всех аргументов функции.

Синтаксис вызова метода

reference.on_ref();
//reference.on_mut_ref();  error: cannot borrow `*reference` as mutable
//reference.on_value();    error: cannot move out of `*reference`

mut_ref.on_ref();
mut_ref.on_mut_ref();
//mut_ref.on_value();      error: cannot move out of `*mut_ref`

value.on_ref();
value.on_mut_ref();
value.on_value();

Только три вызова метода приводят к ошибке, в то время как другие завершаются успешно. Здесь компилятор автоматически вставляет приведения типа deref (разыменование) или autoref (добавление ссылки), чтобы вызов работал. Также обратите внимание, что эти три ошибки не являются ошибками «несоответствия типов»: компилятор уже пытался корректно корректировать тип, но это приводит к другим ошибкам.


Существуют некоторые дополнительные приведения:

  • Принуждения без размера , описываемые чертой Unsize . Позволяет вызывать методы срезов для массивов и приводить типы к объектам признаков реализуемых ими признаков.
  • Расширенные приведения с разыменованием через признак Deref . Это позволяет вам вызывать методы слайса, например, Vec.



Разрешение метода: выяснение, какой метод вызывать

При написании lhs.method_name() метод method_name может быть встроенным методом типа lhs или он может принадлежать признаку, который находится в области видимости (импортирован). Компилятор должен выяснить, какой из них вызывать, и для этого есть ряд правил. Если вдаваться в детали, эти правила на самом деле очень сложны и могут привести к неожиданному поведению. К счастью, большинству программистов никогда не придется иметь дело с этим, и это «просто работает» большую часть времени.

Чтобы получить общее представление о том, как это работает, компилятор пробует следующие вещи по порядку, используя первый найденный метод.

  • Существует ли присущий метод с именем method_name, где тип получателя точно соответствует (не требует принуждения)?
  • Существует ли метод черты с именем method_nameгде тип получателя подходит точно (не требует приведения)?
  • Существует ли присущий метод с именем method_name? (приведение типов будет выполнено)
  • Существует ли метод черты с именем method_name? (Типовые принуждения будут выполняться)

(Опять же, обратите внимание, что это по-прежнему упрощение. Различные типы принуждений предпочтительнее других, например.)

Здесь показано одно правило, известное большинству программистов: собственные методы имеют более высокий приоритет, чем методы черт. Но немного неизвестным является тот факт, что то, подходит ли тип приемника идеально, является более важным фактором. Есть тест, который наглядно демонстрирует это: Rust Quiz # 23 . Более подробную информацию о точном алгоритме разрешения метода можно найти в этом ответе StackOverflow .

Этот набор правил может на самом деле вносить кучу изменений в API для прерывания изменений. В настоящее время мы имеем дело с этим в попытке добавить IntoIterator impl для массивов .




Другое - незначительное и, вероятно, очень очевидное - отличие состоит в том, что для синтаксиса вызова метода имя типа не нужно импортировать.

Кроме того, стоит указать что не отличается в двух синтаксисах :

  • Поведение во время выполнения : никакой разницы нет.
  • Производительность : синтаксис вызова метода "конвертируется" (десугарирован) в базовый UFCS довольно рано внутри компилятора, что означает отсутствие каких-либо различий в производительности.
...