tinker
tinker

Reputation: 3424

References in Rust

I try to understand the borrow mechanism and references in Rust, and for this reason I created the following small example:

extern crate core;

use core::fmt::Debug;

#[derive(Copy, Clone)]
pub struct Element(pub (crate) [u8; 5]);

impl Debug for Element {
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        write!(f, "Element({:?})", &self.0[..])
    }
}

impl Element {
    fn one() -> Element {
        Element([1, 1, 1, 1, 1])
    }
    fn double(&self) -> Element {
        let mut result = *self;
        for i in 0..5 { result.0[i] = 2*result.0[i]; }
        result
    }
    fn mut_double(&mut self) -> Element {
      for i in 0..5 { self.0[i] = 2*self.0[i]; }
      *self
    }
}

fn main() {
  let mut a = Element::one();
  println!("a = {:?}", a); // a = Element([1, 1, 1, 1, 1])
  a = a.double();
  println!("a = {:?}", a); // a = Element([2, 2, 2, 2, 2])
  a = (&a).double();
  println!("a = {:?}", a); // a = Element([4, 4, 4, 4, 4])
  a = a.mut_double();
  println!("a = {:?}", a); // a = Element([8, 8, 8, 8, 8])
  a = (&mut a).mut_double();
  println!("a = {:?}", a); // a = Element([16, 16, 16, 16, 16])
}

So, the above code works, but my confusion comes when calling the double method. As you can see it is defined as fn double(&self) -> Element, so it basically takes an immutable reference. Now in the main, I create a new Element variable called a, and then call double method on it twice. First time I just do a.double(), second time (&a).double(). Both of them seem to work correctly, but I do not understand why the first call a.double() is a valid and compiler doesn't complain about it. Since a is of type Element, not of type &Element, and clearly the double method asks for &Element, so about a reference. Same thing also happens with the mut_double method. Why do I not have to specify (&a) or (&mut a) when calling the double and mut_double methods, respectively? What is happening under the hood with Rust?

Upvotes: 2

Views: 217

Answers (1)

mcarton
mcarton

Reputation: 30001

Short: the language works like that because it's a lot more convenient.

Long (extract from the book, emphasis is mine):

Where’s the -> Operator?

In languages like C++, two different operators are used for calling methods: you use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

Rust doesn’t have an equivalent to the -> operator; instead, Rust has a feature called automatic referencing and dereferencing. Calling methods is one of the few places in Rust that has this behavior.

Here’s how it works: when you call a method with object.something(), Rust automatically adds in &, &mut, or * so object matches the signature of the method. In other words, the following are the same:

p1.distance(&p2);
(&p1).distance(&p2);

The first one looks much cleaner. This automatic referencing behavior works because methods have a clear receiver—the type of self. Given the receiver and name of a method, Rust can figure out definitively whether the method is reading (&self), mutating (&mut self), or consuming (self). The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

Upvotes: 4

Related Questions