about
04/28/2022
RustBites - Structs
Code Repository Code Examples Rust Bites Repo

Rust Bite - Structs

methods, traits, dispatching, graph, examples

Rust uses structs to represent objects, e.g., an aggregate managing data with a set of methods. This is very similar to the way C++ uses classes to define objects. In the first section we will implement methods for structs, turning them into user-defined types. This is a big step forward. It allows us to structure our programs around data that the program manages, rather than simply using a lot of operation fragments to manage public data. Methods allow us to build abstractions that are consistent with our problem domain or lower-level abstractions that help us organize the implementation.
  1. Methods:

    First, let's start with a simple demo that illustrates how to endow a struct with methods.
    methods::main.rs fn demo_basic() { print!("\n -- demo_basic --"); #[derive(Debug)] pub struct Demo { value: i32 } impl Demo { pub fn new() -> Demo { Demo { value:0 } } pub fn set_value(&mut self, v: i32) { self.value = v; } pub fn get_value(&self) -> i32 { self.value } } let mut d = Demo::new(); d.set_value(42); print!("\n d = {:?}", d); print!("\n d.value = {:?}", d.get_value()); } Output: -- demo_basic -- d = Demo { value: 42 } d.value = 42 Comments: For the struct Demo we are defining an associated function, new(), that is invoked as Demo::new(). We also define two methods, set_value(&mut self, v: i32), and get_value(&self) -> i32. Methods always pass &self or &mut self as the first argument in the definition. That is not used for invocations, as you see at the bottom of the left panel.
    The example below provides a user-defined type, Point<T>, that is closer to what you will see in production code than the previous example, but uses the same techniques.
    Method Example - Point<T>: methods.main.rs fn demo_point() { print!("\n -- demo_point --"); use std::fmt::*; #[derive(Debug, Default)] pub struct Point<T> where T:Default + Debug { x:T, y:T, z:T } impl Point<T> where T:Default + Debug + Clone { pub fn new() -> Point<T> { Point { x:std::default::Default::default(), y:std::default::Default::default(), z:std::default::Default::default() } } pub fn default() -> Point<T> { Point::new() } pub fn get_coordinates(&self) -> [T; 3] { [self.x.clone(), self.y.clone(), self.z.clone()] } pub fn set_coordinates(&mut self, coor: &[T; 3]) { self.x = coor[0].clone(); self.y = coor[1].clone(); self.z = coor[2].clone(); } } let mut pt = Point::::new(); pt.set_coordinates(&[1, 2, 3]); print!("\n pt = {:?}", &pt); print!("\n coordinates are: {:?}", &pt.get_coordinates()); pt.set_coordinates(&[3, 2, 1]); print!("\n pt = {:?}", pt); print!("\n coordinates are: {:?}", pt.get_coordinates()); } Outputs: -- demo_point -- pt = Point { x: 1, y: 2, z: 3 } coordinates are: [1, 2, 3] pt = Point { x: 3, y: 2, z: 1 } coordinates are: [3, 2, 1] That's all Folks! Comments:
    1. Note that Debug and Default are derived traits for Point<T>.
    2. impl<T> has a generic parameter to match Point<T>.
    3. new() and default() are associated functions.
    4. get_coordinates(&self) -> [T; 3] and set_coordinates(&mut self, coor: &[T; 3]) are methods.
    5. In set_coordinates we used clones of the array elements to set the coordinate values because Rust does not allow moving elements out of an array.
    6. Using arrays is not the only interface that could work, but it's efficient, and provides a nice example of when and how to pass arrays.
    User-defined types become really useful when we couple them with traits. The next section extends the Point<T> type with SpaceTime<T> and Display traits and provides motivation for the use of traits.
  2. Traits:

    We presented a lot of introductory material for Traits in the Generics and Traits Bite. If you haven't looked at that yet, now is a good time to do so. In the example, below, we extend the Point<T> type with traits SpaceTime<T> and Display. SpaceTime<T> declares an interface for accessing 3-dimensional space coordinates and date time information from a Point<T> or any other type that implements the trait. Here's the SpaceTime<T> definition: SpaceTime<T> pub trait SpaceTime<T> { fn get_coordinates(&self) -> [T; 3]; fn set_coordinates(&mut self, coor: &[T; 3]); fn set_time(&mut self, st: DateTime<Local>); fn get_time(&self) -> DateTime<Local>; fn get_time_string(&self) -> String; } A Point<T> that implements this trait could be used to annotate data from an astronomical observatory. A system for such annotation would probably use several different types of points to mark different types of observables. The trait allows any of those point types to make their space and time information available to mapping and display functions. That works because the functions accept a SpaceTime<T> trait object rather than a specific point type. So traits allow us to build very flexible software based on use rather than implementation details.
    Example: Point<T> with SpaceTime & Display Traits point::lib.rs ///////////////////////////////////////////////////////////// // point::lib.rs - Demo Traits // // // // Jim Fawcett, https://JimFawcett.github.io, 24 Jun 2020 // ///////////////////////////////////////////////////////////// /* Point represents points in 3-dimensional space and time. Example hypothetical applications: - Aircraft flight navigation systems - Airport and Marine port area management systems - Autonomous vehicle control - Drawing applications Point implements a SpaceTime trait. That allows a function to use space and time information from any object that implements the SpaceTime<T>trait. For example, in an Aircraft, points might be used as fundamental data structure for capturing instrument information, navigation computations, and in flight recording. Each of these applications might use different internal data, e.g., ids, other information, ... but a function that accepts a SpaceTime trait object can extract that information from any of them. */ use chrono::offset::Local; use chrono::DateTime; use chrono::{Datelike, Timelike}; use std::fmt::*; /*----------------------------------------------------------- declare SpaceTime<T> trait - note that this trait is generic */ pub trait SpaceTime<T> { fn get_coordinates(&self) -> [T; 3]; fn set_coordinates(&mut self, coor: &[T; 3]); fn set_time(&mut self, st: DateTime<Local>); fn get_time(&self) -> DateTime<Local>; fn get_time_string(&self) -> String; } /*-- define Point<T> type --*/ #[derive(Debug)] pub struct Point<T> where T:Default + Debug { x:T, y:T, z:T, t:DateTime<Local>, n:String, } /*-- implement Time trait --*/ impl<T> SpaceTime<T> for Point<T> where T:Default + Debug + Clone { fn set_time(&mut self, st: DateTime<Local>) { self.t = st; } fn get_time(&self) -> DateTime<Local> { self.t } fn get_time_string(&self) -> String { let year = self.t.year().to_string(); let mon = self.t.month().to_string(); let day = self.t.day().to_string(); let hour = self.t.hour().to_string(); let min = self.t.minute().to_string(); let sec = self.t.second().to_string(); let dt = format!( "{}::{:0>2}::{:0>2} {:0>2}::{:0>2}::{:0>2}", year, mon, day, hour, min, sec ); dt } /*-- set coordinates from array slice --*/ fn set_coordinates(&mut self, coor: &[T; 3]) { self.x = coor[0].clone(); self.y = coor[1].clone(); self.z = coor[2].clone(); } /*-- return array of three spatial coordinates --*/ fn get_coordinates(&self) -> [T; 3] { [self.x.clone(), self.y.clone(), self.z.clone()] } /*-- time is returned with Time::get_time() --*/ } /*-- implement Display trait --*/ impl<T> Display for Point<T> where T:Default + Debug + Clone { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{{ {:?}, {:?}, {:?}, {}, {:?} }}", &self.x, &self.y, &self.z, &self.get_time_string(), &self.n ) } } /*-- implement Point methods --*/ impl<T> Point<T> where T:Default + Debug + Clone { pub fn new() -> Point<T> { Point { x:std::default::Default::default(), y:std::default::Default::default(), z:std::default::Default::default(), t:Local::now(), n:String::default() } } pub fn default() -> Point<T> { Point::new() } pub fn set_name(&mut self, name: &str) { self.n = name.to_string(); } pub fn get_name(&self) -> &str { &self.n } } #[cfg(test)] mod tests { use super::*; #[test] fn construct () { let pt = Point::<i32>::new(); assert_eq!(pt.get_coordinates(), [0, 0, 0]); } #[test] fn set_coor() { let mut pt = Point::<i32>::new(); pt.set_coordinates(&[1, 2, 3]); assert_eq!(pt.get_coordinates(), [1, 2, 3]); } #[test] fn get_time() { use chrono::{Duration}; let two_sec = Duration::seconds(2); let ts_start = Local::now() - two_sec; let pt = Point::<i32>::new(); let ts_now = pt.get_time(); let ts_stop = Local::now() + two_sec; assert!(ts_start < ts_now); assert!(ts_now < ts_stop); } } Using Code - point::test1.rs ///////////////////////////////////////////////////////////// // point::test1.rs - Demo Point<T> with SpaceTime and // // Display Traits // // // // Jim Fawcett, https://JimFawcett.github.io, 24 Jun 2020 // ///////////////////////////////////////////////////////////// #![allow(dead_code)] use point::{*}; use point::SpaceTime; use chrono::{Local}; use std::fmt::*; /*----------------------------------------------------------- function accepting SpaceTime<T> trait object - Does not depend on a specific type for pt, just the SpaceTime<T> trait - like an interface. - Can't use name. That's not part of trait. */ fn use_space_time_info<T:Debug>(pt : &dyn SpaceTime<T>) { print!("\n -- using SpaceTime trait object --"); print!( "\n -- display coords & time w/ Display format --" ); print!( "\n coordinates are: {:?}", &pt.get_coordinates() ); print!("\n point time is: {}", &pt.get_time_string()); } /*----------------------------------------------------------- Exercise Point<i32> and Point<f64> funtionality */ fn main() { print!("\n Demonstrating Point Type"); print!("\n =========================="); print!( "\n -- create Point<i32> " "& display w/ Display format --" ); let mut pt = Point::<i32>::new(); pt.set_coordinates(&[1, 2, 3]); pt.set_time(Local::now()); pt.set_name("pt"); print!("\n pt = {}", &pt); println!(); use_space_time_info(&pt); println!(); print!( "\n -- mutate coordinates & display again --" ); pt.set_coordinates(&[3, 2, 1]); pt.set_name("mutated pt"); print!("\n pt = {}", pt); print!( "\n coordinates are: {:?}", pt.get_coordinates() ); println!(); print!("\n -- display point with Debug format --"); print!("\n pt in Debug format: {:?}", pt); println!(); print!("\n -- creating Point<f64> --"); let mut ptd = Point::<f64>::new(); ptd.set_coordinates(&[0.5, -0.5, 1.75]); ptd.set_name("ptd"); print!("\n ptd = {}", ptd); use_space_time_info(&ptd); print!("\n\n That's all Folks!\n\n"); } Output: Demonstrating Point Type ========================== -- create Point and display with Display format -- pt = { 1, 2, 3, 2020::06::25 16::55::36, "pt" } -- using SpaceTime trait object -- -- display coordinates and time with Display format -- coordinates are: [1, 2, 3] point time is: 2020::06::25 16::55::36 -- mutate coordinates and display again -- pt = { 3, 2, 1, 2020::06::25 16::55::36, "mutated pt" } coordinates are: [3, 2, 1] -- display point with Debug format -- pt in Debug format: Point { x: 3, y: 2, z: 1, t: 2020-06-25T16:55:36.838484600-04:00, n: "mutated pt" } -- creating Point -- ptd = { 0.5, -0.5, 1.75, 2020::06::25 16::55::36, "ptd" } -- using SpaceTime trait object -- -- display coordinates and time with Display format -- coordinates are: [0.5, -0.5, 1.75] point time is: 2020::06::25 16::55::36 That's all Folks!
  3. Static vs. Dynamic Binding

    Dispatching method arguments statically is just what we have been doing in all of the earlier Bites. Binding of call to compiled method code happens at compile time. Dynamic dispatching binds method calls to compiled code at run time. Which object is passed to the function isn't known until run time, so the binding has to happen then. This binding is effected via a dyn object that contains two pointers, one to the object being passed, and one to a virtual pointer table (vtable). Each type that implements a trait constructs a vtable during the compilation of the impl trait for type part of its definition. That compilation phase knows where the code for each of the type's Methods is stored and constructs a table with pointers to each of those code blocks, e.g. the vtable. When a function is specified to accept a trait object - do_draw's item in the right panel below - it is passed a reference to a dyn with pointer to the item and another pointer to the item's type's vtable. Here's what the code looks like:
    Static Dispatch Function struct SomeVisualItem { /* contents elided */ } impl SomeVisualItem { fn draw(&self) { /* code elided */ } } Invocation let svi = SomeVisualItem { /* content elided */ }; svi.draw(); Dynamic Dispatch Function trait Draw { fn draw(&self); } struct SomeVisualItem { /* contents elided */ } impl Draw for SomeVisualItem { fn draw(&self) { /* code elided */ } } fn do_draw(item: &dyn Draw) { item.draw(); } Invocation let svi = SomeVisualItem { /* content elided */ }; do_draw(&svi); // works for any type implementing Draw trait
    The Example code below represents a graphics editor that is drawing on some form of canvas. It defines three graphics shapes that each implement a Draw trait - rectangles, circles, and textboxes. An application would define many additional types, but only three are used here to keep things as simple as practical. An editor will construct a draw_list of all the defined graphics objects and, when instructed, iterate through its list calling a do_draw function using dynamic dispatching. In this way, the editor does not need to know anything about the graphics objects other than they implement the Draw trait.
    Examples: static and dynamic dispatch dispatch::main.rs /////////////////////////////////////////////////////////////// // dispatch - demo static and dynamic fn arg dispatching // // // // Jim Fawcett, https://JimFawcett.github.com, 27 Jun 2020 // /////////////////////////////////////////////////////////////// #![allow(dead_code)] trait Draw { fn draw(&self); } /*-- define Rectangle --*/ #[derive(Debug)] struct Point { x:i32, y:i32 } type Corner = Point; #[derive(Debug)] struct Rect { ll:Corner, ur:Corner } impl Draw for Rect { fn draw(&self) { print!("\n drawing Rect:\n {:?}", &self); } } /*-- define Circle --*/ #[derive(Debug)] struct Circle { orig:Point, rad:i32 } impl Draw for Circle { fn draw(&self) { print!("\n drawing Circle:\n {:?}", &self); } } /*-- define TextBox --*/ #[derive(Debug)] struct TextBox { region:Rect, text:String } impl Draw for TextBox { fn draw(&self) { print!("\n drawing TextBox:\n {:?}", &self); } } /*-- dynamic dispatch of trait objects --*/ fn do_draw(item:&dyn Draw) { item.draw(); } Using Code fn main() { print!("\n -- static dispatch --"); let r = Rect { ll:Corner {x:1,y:0}, ur:Corner {x:2,y:5} }; r.draw(); print!("\n -- dynamic dispatch --"); do_draw(&r); print!("\n -- dynamic dispatch of draw_list --"); let mut draw_list = Vec::<&dyn Draw>::new(); let r1 = Rect{ ll:Corner {x:42, y:-42}, ur:Corner {x:-13, y:14} }; let r2 = Rect{ ll:Corner {x:81, y:53}, ur:Corner {x:35, y:36} }; let c1 = Circle{ orig:Point{x:5,y:19}, rad:3 }; let t1 = TextBox{ region:Rect{ ll:Corner{x:20,y:30}, ur:Corner{x:40, y:50} }, text:"some text".to_string()}; draw_list.push(&r1); draw_list.push(&r2); draw_list.push(&c1); draw_list.push(&t1); for item in draw_list { do_draw(item); } print!("\n\n That's all Folks!\n\n"); } Output: -- static dispatch -- drawing Rect: Rect { ll: Point { x: 1, y: 0 }, ur: Point { x: 2, y: 5 } } -- dynamic dispatch -- drawing Rect: Rect { ll: Point { x: 1, y: 0 }, ur: Point { x: 2, y: 5 } } -- dynamic dispatch of draw_list -- drawing Rect: Rect { ll: Point { x: 42, y: -42 }, ur: Point { x: -13, y: 14 } } drawing Rect: Rect { ll: Point { x: 81, y: 53 }, ur: Point { x: 35, y: 36 } } drawing Circle: Circle { orig: Point { x: 5, y: 19 }, rad: 3 } drawing TextBox: TextBox { region: Rect { ll: Point { x: 20, y: 30 }, ur: Point { x: 40, y: 50 } }, text: "some text" } That's all Folks!
    You have to use a bit of imagination as you look at the dispatching example. We aren't drawing on a canvas, of course. Just writing out information to the console. But you see that all of the information necessary to draw on a canvas is handed to the draw method, via its instance's state. So, in a galaxy not to far away, Rust generates webassembly code to use this information to draw on a web browser canvas.
  4. Graph Example

    A directed graph, consisting of vertices connected with directed edges, is a useful example to illustrate how the Rust memory model affects our design strategies. If we try to implement the graph with references - not hard to do in C# for example - we will almost immediately violate the Rust ownership rules. Rust demands that a mutable pointer to any data item has exclusive access. But a directed graph may have several edge references to a vertex and we may wish to mutate the vertice's state. It turns out that we can put the vertices in a Vec<Vertex> and access them through Vec indices. That doesn't violate the ownership rules. Each vertex is owned by the Vec<Vertex> - let's call it an adjacency list. Access to a vertex through another vertex's edge is just an index operation with no additional reference needed. There are design issues to be worked out, e.g., how to efficiently delete vertices, ... But those are all "business as usual" problems. This is a placeholder for example code. I will be turning to other things for awhile, so it will take some time before Rust code appears here. In the mean time, you can look at a Directed Graph class I built in C++ that uses this strategy. It has an adjacency list of vertices with each vertex holding an edge that is a pair of an edge type E instance and an index for the vertex it "points" to. The code is in the CppGraph C++ Repository.
  5. Exercises:

    1. Change the storage type, in the Point<T> example above, to a Vec. Run the same demonstrations as the example.
    2. Develop a user-defined type, Person, with fields for name, age, and name of occupation. Implement useful functions and methods for the Person type. Demonstrate it in operation.
    3. Create a user-defined type representing Teams, using the Person class you developed in the first exercise.
  Next Prev Pages Sections About Keys