3

Is there any difference in Rust between calling a method on a value, like this:

struct A { e: u32 }

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

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

...and calling it on the type, like this:

fn main() {
    A::show(&A { e: 0 })
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
joel
  • 6,359
  • 2
  • 30
  • 55

1 Answers1

3

Summary: The most important difference is that the universal function call syntax (UFCS) is more explicit than the method call syntax.

With UFCS there is basically no ambiguity what function you want to call (there is still a longer form of the UFCS for trait methods, but let's ignore that for now). The method call syntax, on the other hand, requires more work in the compiler to figure out which method to call and how to call it. This manifests in mostly two things:

  • Method resolution: figure out if the method is inherent (bound to the type, not a trait) or a trait method. And in the latter case, also figure out which trait it belongs to.
  • Figure out the correct receiver type (self) and potentially use type coercions to make the call work.



Receiver type coercions

Let's take a look at this example to understand the type coercions to the receiver type:

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`

    // ... 
}

So we have three methods that take Foo, &Foo and &mut Foo receiver and we have three variables with those types. Let's try out all 9 combinations with each, method call syntax and 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);

As we can see, only the calls succeed where the types are correct. To make the other calls work we would have to manually add & or &mut or * in front of the argument. That's the standard behavior for all function arguments.

Method call syntax

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();

Only three of the method calls lead to an error while the others succeed. Here, the compiler automatically inserts deref (dereferencing) or autoref (adding a reference) coercions to make the call work. Also note that the three errors are not "type mismatch" errors: the compiler already tried to adjust the type correctly, but this lead to other errors.


There are some additional coercions:

  • Unsize coercions, described by the Unsize trait. Allows you to call slice methods on arrays and to coerce types into trait objects of traits they implement.
  • Advanced deref coercions via the Deref trait. This allows you to call slice methods on Vec, for example.



Method resolution: figuring out what method to call

When writing lhs.method_name(), then the method method_name could be an inherent method of the type of lhs or it could belong to a trait that's in scope (imported). The compiler has to figure out which one to call and has a number of rules for this. When getting into the details, these rules are actually really complex and can lead to some surprising behavior. Luckily, most programmers will never have to deal with that and it "just works" most of the time.

To give a coarse overview how it works, the compiler tries the following things in order, using the first method that is found.

  • Is there an inherent method with the name method_name where the receiver type fits exactly (does not need coercions)?
  • Is there a trait method with the name method_name where the receiver type fits exactly (does not need coercions)?
  • Is there an inherent method with the name method_name? (type coercions will be performed)
  • Is there a trait method with the name method_name? (type coercions will be performed)

(Again, note that this is still a simplification. Different type of coercions are preferred over others, for example.)

This shows one rule that most programmers know: inherent methods have a higher priority than trait methods. But a bit unknown is the fact that whether or not the receiver type fits perfectly is a more important factor. There is a quiz that nicely demonstrates this: Rust Quiz #23. More details on the exact method resolution algorithm can be found in this StackOverflow answer.

This set of rules can actually make a bunch of changes to an API to be breaking changes. We currently have to deal with that in the attempt to add an IntoIterator impl for arrays.




Another – minor and probably very obvious – difference is that for the method call syntax, the type name does not have to be imported.

Apart from that it's worth pointing out what is not different about the two syntaxes:

  • Runtime behavior: no difference whatsoever.
  • Performance: the method call syntax is "converted" (desugared) into basically the UFCS pretty early inside the compiler, meaning that there aren't any performance differences either.
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305