Struct methods

It's often the case that you wish to write functions that operate on a specific struct or return the values of a specific struct. That's when you write implementation blocks with the impl keyword.

For instance, we could extend the previously defined character struct with two methods: a constructor that takes a name and sets default values for all the character attributes and a getter method for character strength:

// structmethods.rs struct Character { strength: u8, dexterity: u8, constitution: u8, wisdom: u8, intelligence: u8, charisma: u8, name: String, } impl Character { fn new_named(name: String) -> Character { Character { strength: 9, constitution: 9, dexterity: 9, wisdom: 9, intelligence: 9, charisma: 9, name: name, } } fn get_strength(&self) -> u8 { self.strength } }

TheÂ new_named method is called an associated function because it does not take self as the first parameter. It is not far from what many other languages would call a static method. It is also a constructor method since it follows the convention of starting with the word,Â new , and because it returns a struct of the same type ( Character ) for which we're defining an implementation. Since new_named is an associated function, it can be called by prefixing the struct name and double colon:

Character::new_named("Dave")

The self parameter in get_strength is special in that its type is inferred to be the same as the impl block's type, and because it is the thing that makes get_strength a callable method on the struct. In other words, get_strength can be called on an already created instance of the struct:

let character = Character::new_named("Dave"); character.get_strength();

The ampersand before self means that self is borrowed for the duration of the method, which is exactly what we want here. Without the ampersand, the ownership would be moved to the method, which means that the value would be deallocated after leaving get_strength . Ownerships are a distinguishing feature of Rust, and will be dealt in depth in Chapter 6, Memory, Lifetimes, and Borrowing.

Using other pieces of code in your module A quick word about how to include code from other places into the module you are writing. Rust's module system has its own pecularities, but it's enough to note now that the use statement brings code from another module into the current namespace. It does not load external pieces of code, it merely changes the visibility of things: // use.rs use std::ascii::AsciiExt; fn main() { let lower_case_a = 'a'; let upper_case_a = lower_case_a.to_ascii_uppercase(); println!("{} upper cased is {}", lower_case_a, upper_case_a); } In this example, the AsciiExt module contains an implementation of the to_ascii_uppercase for the char type, so including that in this module makes it possible to use the method here. The compiler manages again to be quite helpful if you miss a particular use statement, like what happens here if we remove the first line and try to compile:

Sequences One more thing to cover and then we can wrap up the basics. Rust has a few built-in ways to construct sequences of data: arrays and tuples . Then, it has a way to take a view to a piece of that data: slices . Thirdly, it has several data structures as libraries, of which we will cover Vectors (for dynamically growable sequences) and HashMaps (for key/value data). Arrays are C-like: they have a fixed length that you need to specify along with the type of the elements of the array when declaring it. The notation for array types is [<type>, <size>] : // arrays.rs fn main() { let numbers: [u8; 10] = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11]; let floats = [0.1, 0.2, 0.3]; println!("The first number is {}", numbers[0]); for number in &numbers { println!("Number is {}", number); } for float_number in &floats { println!("Float is {}", float_number); } } As said before, Rust is able to infer the types of local variables, so writing them out is optional. Slices offer a way to safely point to a continuous range in an existing data structure. The type of slices is &[T] . Its syntax looks similar to arrays: // slices.rs fn main() { let numbers: [u8; 4] = [1, 2, 4, 5]; let all_numbers_slice: &[u8] = &numbers[..]; let first_two_numbers: &[u8] = &numbers[0..2]; println!("All numbers: {:?}", all_numbers_slice); println!("The second of the first two numbers: {}", first_two_numbers[1]); } Tuples differ from arrays in the way that arrays are sequences of the same type, while tuple elements have varying types: // tuples.rs fn main() { let number_and_string: (u8, &str) = (40, "a static string"); println!("Number and string in a tuple: {:?}", number_and_string); } They are useful for simple, type-safe compounding of data, generallyÂ used when returning multiple values from a function. Vectors are like arrays except that their contents or length don't have to be known in advance. They are created with either calling the constructor Vec::new or by using the vec! macro: // vec.rs fn main() { let mut numbers_vec: Vec<u8> = Vec::new(); numbers_vec.push(1); numbers_vec.push(2); let mut numbers_vec_with_macro = vec![1]; numbers_vec_with_macro.push(2); println!("Both vectors have equal contents: {}", numbers_vec == numbers_vec_with_macro); } These are not the only ways to create vectors, and one typical way needs to be covered here. Rust defines iterators, things that can be iterated one by one, in a generic way. For instance, a program's runtime arguments areÂ iterators, which would be a problem if you wanted to get the nth argument. However, every iterator has a collect method, which gathers all the items in the iterator into a single collection, such as a vector, which can be indexed. There's an example of this usage in the chapter's exercise. Finally, HashMaps can be used for key/value data. They are created with the HashMap::new constructor: // hashmap.rs use std::collections::HashMap; fn main() { let mut configuration = HashMap::new(); configuration.insert("path", "/home/user/".to_string()); println!("Configured path is {:?}", configuration.get("path")); }