Understanding the 'self' parameter in the context of trait implementations
Asked Answered
C

2

9

When implementing a trait, we often use the keyword self, a sample is as follows. I want to understand the representation of the many uses of self in this code sample.

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

trait HasArea {
    fn area(&self) -> f64;          // first self: &self is equivalent to &HasArea
}

impl HasArea for Circle {
    fn area(&self) -> f64 {         //second self: &self is equivalent to &Circle
        std::f64::consts::PI * (self.radius * self.radius) // third:self
    }
}

My understanding is:

  1. The first self: &self is equivalent to &HasArea.
  2. The second self: &self is equivalent to &Circle.
  3. Is the third self representing Circle? If so, if self.radius was used twice, will that cause a move problem?

Additionally, more examples to show the different usage of the self keyword in varying context would be greatly appreciated.

Crowboot answered 1/11, 2016 at 6:56 Comment(2)
You may be interested in What is the difference between self and Self.Dahomey
The question about self.radius is slightly different than the question about itself (it's about ownership and Copy). I suggest you edit it out to focus on what self is about here since that's the most difficult (and novel) part, whereas we already have plenty of questions about ownership.Dahomey
M
10

You're mostly right.

The way I think of it is that in a method signature, self is a shorthand:

impl S {
    fn foo(self) {}      // equivalent to fn foo(self: S)
    fn foo(&self) {}     // equivalent to fn foo(self: &S)
    fn foo(&mut self) {} // equivalent to fn foo(self: &mut S)
}

It's not actually equivalent since self is a keyword and there are some special rules (for example for lifetime elision), but it's pretty close.

Back to your example:

impl HasArea for Circle {
    fn area(&self) -> f64 {   // like fn area(self: &Circle) -> ... 
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

The self in the body is of type &Circle. You can't move out of a reference, so self.radius can't be a move even once. In this case radius implements Copy, so it's just copied out instead of moved. If it were a more complex type which didn't implement Copy then this would be an error.

Metamorphosis answered 1/11, 2016 at 9:22 Comment(2)
So self.radius is like Circle.radius, since radius is f64 so it implement Copy trait. Correct?Crowboot
Yes, self is a reference to a Circle, since it's a method on Circle, so self.radius is an f64.Metamorphosis
D
7

You are mostly correct.


There is a neat trick to let the compiler tell you the type of variables rather than trying to infer them: let () = ...;.

Using the Playground I get for the 1st case:

9 |         let () = self;
  |             ^^ expected &Self, found ()

and for the 2nd case:

16 |         let () = self;
   |             ^^ expected &Circle, found ()

The first case is actually special, because HasArea is not a type, it's a trait.

So what is self? It's nothing yet.

Said another way, it poses for any possible concrete type that may implement HasArea. And thus the only guarantee we have about this trait is that it provides at least the interface of HasArea.

The key point is that you can place additional bounds. For example you could say:

trait HasArea: Debug {
    fn area(&self) -> f64;
}

And in this case, Self: HasArea + Debug, meaning that self provides both the interfaces of HasArea and Debug.


The second and third cases are much easier: we know the exact concrete type for which the HasArea trait is implemented. It's Circle.

Therefore, the type of self in the fn area(&self) method is &Circle.

Note that if the type of the parameter is &Circle then it follows that in all its uses in the method it is &Circle. Rust has static typing (and no flow-dependent typing) so the type of a given binding does not change during its lifetime.


Things can get more complicated, however.

Imagine that you have two traits:

struct Segment(Point, Point);

impl Segment {
    fn length(&self) -> f64;
}

trait Segmentify {
    fn segmentify(&self) -> Vec<Segment>;
}

trait HasPerimeter {
    fn has_perimeter(&self) -> f64;
}

Then, you can implement HasPerimeter automatically for all shapes that can be broken down in a sequence of segments.

impl<T> HasPerimeter for T
    where T: Segmentify
{
    // Note: there is a "functional" implementation if you prefer
    fn has_perimeter(&self) -> f64 {
        let mut total = 0.0;
        for s in self.segmentify() { total += s.length(); }
        total
    }
}

What is the type of self here? It's &T.

What's T? Any type that implements Segmentify.

And therefore, all we know about T is that it implements Segmentify and HasPerimeter, and nothing else (we could not use println("{:?}", self); because T is not guaranteed to implement Debug).

Dahomey answered 1/11, 2016 at 9:28 Comment(2)
"Note that if the type of the parameter is &Circle then it follows that in all its uses in the method it is &Circle. Rust has static typing (and no flow-dependent typing) so the type of a given binding does not change during its lifetime." So, self.radius is like &Circle.radius??Crowboot
@enaJ: self.radius is f64 yes, which is a Copy type.Dahomey

© 2022 - 2024 — McMap. All rights reserved.