Herman J. Radtke III

Read more of my blog or subscribe to my feed.


Getter Functions In Rust

Written by Herman J. Radtke III on 14 Jan 2015

As soon as I started writing implementations for structs in Rust I started fighting with the compiler. Writing what seemed like a simple getter function caused me a lot of frustration. The self parameter can really throw me off in Rust. I reflexively treat it like this in C++, which has no concept of & or &mut. I do this because I think of impl Person as defining methods on a class as I would do in C++. This can be really misleading.

Consider this Ruby code that we want to port to Rust:

class Person
   attr_reader :name

   def initialize(name)
      @name = name
   end
end

In Rust this would look something like:

struct Person {
   name: String
}

impl Person {
   fn new(name: String) -> Person {
      Person { name: name }
   }

   fn get_name(self) -> String {
      self.name
   }
}

#[test]
fn test_get_person() {
    let p = Person::new("Herman".to_string());
    assert!(p.get_name().as_slice() == "Herman");
}

I run rustc --test person.rs, everything compiles and things are looking good. Even the test passes. What happens if I want to use p again though? If I modify my test to call .get_name() again I receive a cryptic error:

#[test]
fn test_get_person() {
    let p = Person::new("Herman".to_string());
    assert!(p.get_name().as_slice() == "Herman");
    assert!(p.get_name().as_slice() == "Herman");
}
$ rustc person.rs

person.rs:21:13: 21:14 error: use of moved value: `p`
person.rs:21     assert!(p.get_name().as_slice() == "Herman");
                         ^
<std macros>:1:1: 5:46 note: in expansion of assert!
person.rs:21:5: 21:50 note: expansion site
person.rs:20:13: 20:14 note: `p` moved here because it has type `Person`, which is non-copyable
person.rs:20     assert!(p.get_name().as_slice() == "Herman");
                         ^
<std macros>:1:1: 5:46 note: in expansion of assert!
person.rs:20:5: 20:50 note: expansion site
error: aborting due to previous error

I read about ownership in the Rust Book and recall some of what moved means, but it is not clear where to go from here. What many people new to Rust do is resort to using .clone(), but even that will not satisfy the compiler. Thinking back to C++, using a reference makes sense! Let’s changing the first parameter to .get_name() from self to &self:

$ rustc person.rs

person.rs:13:7: 13:11 error: cannot move out of borrowed content
person.rs:13       self.name

Whenever I see the word borrowed I know the compiler is referring to someething that is being passed by reference. In this case &self is being passed by reference. The compiler is trying to tell me that it cannot move ownership of name from my borrowed &self. I do not want to give up ownership of name though. I simply want my test to have access to the value for a little while. So, the next step is to return a reference to a String, via &String, so ownership doesn’t change. Compiling that shows me:

$ rustc person.rs

person.rs:13:7: 13:16 error: mismatched types: expected `&collections::string::String`, found `collections::string::String` (expected &-ptr, found struct collections::string::String)
person.rs:13       self.name
                   ^~~~~~~~~
error: aborting due to previous error

This sort of error is very familar in Rust. Turning self.name into a reference via &self.name makes everything compile and leaves us with:

struct Person {
   name: String
}

impl Person {
   fn new(name: String) -> Person {
      Person { name: name }
   }

   fn get_name(&self) -> &String {
      &self.name
   }
}

#[test]
fn test_get_person() {
    let p = Person::new("Herman".to_string());
    assert!(p.get_name().as_slice() == "Herman");
    assert!(p.get_name().as_slice() == "Herman");
}

Comparison to C++

What made things really click for me is to think about how references work in C++. I also see the Rust Book now includes the language We should default to using &self, as it’s the most common. within the context of Rust’s methods.