Skip to content

Latest commit

 

History

History
277 lines (227 loc) · 7.3 KB

method-syntax.md

File metadata and controls

277 lines (227 loc) · 7.3 KB

Method Syntax

In the last section on ownership, we made several references to ‘methods’. Methods look like this:

let s1 = String::from("hello");

// call a method on our String
let s2 = s1.clone();

println!("{}", s1);

The call to clone() is attatched to s1 with a dot. This is called ‘method syntax’, and it’s a way to call certain functions with a different style.

Why have two ways to call functions? We’ll talk about some deeper reasons related to ownership in a moment, but one big reason is that methods look nicer when chained together:

// with functions
h(g(f(x)));

// with methods
x.f().g().h();

The nested-functions version reads in reverse: we call f(), then g(), then h(), but it reads as h(), then g(), then f().

Before we get into the details, let’s talk about how to define your own methods.

Defining methods

We can define methods with the impl keyword. impl is short for ‘implementation’. Doing so looks like this:

#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}

let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };

assert_eq!(8.200609733428363, p1.distance(&p2));

Let’s break this down. First, we have our Point struct from earlier in the chapter. Next comes our first use of the impl keyword:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
# 
impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
# 
#        f64::sqrt(x_squared + y_squared)
#    }
}
# 
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
# 
# assert_eq!(8.200609733428363, p1.distance(&p2));

Everything we put inside of the curly braces will be methods implemented on Point.

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
# 
# impl Point {
    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
# 
#        f64::sqrt(x_squared + y_squared)
    }
# }
# 
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
# 
# assert_eq!(8.200609733428363, p1.distance(&p2));

Next is our definition. This looks very similar to our previous definition of distance() as a function:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
fn distance(p1: Point, p2: Point) -> f64 {
#     let x_squared = f64::powi(p2.x - p1.x, 2);
#     let y_squared = f64::powi(p2.y - p1.y, 2);
# 
#     f64::sqrt(x_squared + y_squared)
# }

Other than this, the rest of the example is familliar: an implementation of distance(), and using the method to find an answer.

There are two differences. The first is in the first argument. Instead of a name and a type, we have written &self. This is what distinguishes a method from a function: using self inside of an impl block. Because we already know that we are implementing this method on Point, we don’t need to write the type of self out. However, we have written &self, not only self. This is because we want to take our argument by reference rather than by ownership. In other words, these two forms are the same:

fn foo(self: &Point)
fn foo(&self)

Just like any other parameter, you can take self in three forms. Here’s the list, with the most common form first:

fn foo(&self) // take self by reference
fn foo(&mut self) // take self by mutable reference
fn foo(self) // take self by ownership

In this case, we only need a reference. We don’t plan on taking ownership, and we don’t need to mutate either point. Taking by reference is by far the most common form of method, followed by a mutable reference, and then occasionally by ownership.

Methods and automatic referencing

We’ve left out an important detail. It’s in this line of the example:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
# 
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
# 
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# 
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
# 
assert_eq!(8.200609733428363, p1.distance(&p2));

When we defined distance(), we took both self and the other argument by reference. Yet, we needed a & for p2 but not p1. What gives?

This feature is called ‘automatic referencing’, and calling methods is one of the few places in Rust that has behavior like this. Here’s how it works: when you call a method with self.(, Rust will automatically add in &s or &muts to match the signature. In other words, these three are the same:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
# 
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
# 
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
Point::distance(&p1, &p2);

The first one looks much, much cleaner. Here’s another example:

let mut s = String::from("Hello,");

s.push_str(" world!");

// The above is the same as:
// (&mut s).push_str(" world!");

assert_eq!("Hello, world!", s);

Because push_str() has the following signature:

fn push_str(&mut self, string: &str) {

This automatic referencing behavior works because methods have a clear receiver — the type of self — and in most cases it’s clear given the receiver and name of a method whether the method is just reading (so needs &self), mutating (so &mut self), or consuming (so self). The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

Methods can be called like functions

Furthermore, if we have a method, we can also call it like a function:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
# 
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
# 
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
let d1 = p1.distance(&p2);
let d2 = Point::distance(&p1, &p2);

assert_eq!(d1, d2);

Instead of using self.(, we use Point and the namespace operator to call it like a function instead. Because functions do not do the automatic referencing, we must pass in &p1 explicitly.

While methods can be called like functions, functions cannot be called like methods. If the first argument isn’t named self, it cannot be called like a method.