about
04/28/2022
RustBites - Generics and Traits
Code Repository Code Examples

Rust Bite - Generics and Traits

generic types, traits, exercises

1. Generics:

Generics are abstract types used in functions and structs as placeholders for concrete types that will be supplied by an application. This allows the functions and structs to be used with many different concrete types, without writing a definition for each type. Essentially, a generic function is a code generator for concrete functions and a generic struct is a code generator too, for a constrained set of concrete types. Constraints are supplied with traits. In the illustration below, the Debug trait is used to limit concrete tyypes to those that can be formatted with the display specifier, "{:?}". Code in Rust Playground Generic Function and Struct ///////////////////////////////////////////////////// // generics_and_traits::main.rs - funct & struct // ///////////////////////////////////////////////////// use std::any::type_name; use std::fmt::*; /*----------------------------------------- Generic function gt - underscore in name, _t, indicates it will not be used in the function - the :Debug, below, is a trait constraint required by gf - if not satisfied, compilation fails */ fn gf<T: Debug>(_t:T) { let tn = type_name::<T>(); print!("\n t is type {:?}", tn); } /*----------------------------------------- Generic struct - #[derive(Debug)] requests compiler to implement the Debug trait for Point */ #[derive(Debug)] struct Point<T> { x:T, y:T, z:T, } Using Code: /*----------------------------------------- First demnstration of generics and traits */ fn main() { gf(3.14159); let pt = Point { x:0, y:1, z:2 }; gf(pt); println!("\n\n That's all Folks!\n\n"); } Output: cargo run -q t is type "f64" t is type "generics_and_traits::Point<i32>" That's all Folks!

2. Traits:

A trait specifies one or more method or associated function signatures that a type with that trait is obligated to implement. Marker traits, like copy, are an exception, declaring no signature. But they affect code generated by the compiler.
Trait Expression pub trait Clone { fn clone(&self) -> Self; fn clone_from(&mut self, source: &Self) { ... } } std::clone::Clone from Rust crone crate documentation. Comments
  1. Traits may define methods they declare, but usually don't.
  2. The &self and &mut self arguments are used for any methods bound to a struct.
  3. The return type Self requires the clone method to return an instance of its own type.
Traits are used to constrain generic parameter types and to support dynamic dispatching in polymorphic designs. We will defer discussions of dynamic dispatch to the Abstraction Bite. The table below lists the most commonly used traits defined in the Rust libraries. Custom traits are less frequently used, except for polymorphic designs.
 

Frequently Used Standard Traits:

Trait Description
Copy Copy is a marker trait, so it has no methods for application code to call. It's used by compiler to decide how to handle bindings and assignments. If the data is Copy its value is copied from source to destination.
Clone Clone creates a new instance of the cloner type and copies into it the cloner's resources. This is an expensive operation so Rust makes that explicit with the fn clone(&self) -> Self method.
Debug Debug enables functions and structs to use the Debug format specifier "{:?}". That formats output in a relatively simple fashion, intended for debugging code, but useful elsewhere as well. The Rust primitives and most of the Rust library types implement this Trait.
Display Display provides custom formatting for user-defined functions and structs with the "{}" format placeholder. Some of the Rust types, like Strings, implement Display.
Default Default requires implementors to supply the associated function fn default() -> Self. This is intended to allow users of the implementing type to set a default value for an instance of the type at construction.
ToString ToString requires the method: fn to_string(&self) -> String. This trait is automatically implemented for types that implement Display trait. The Rust docs say "ToString shouldn't be implemented directly: Display should be implemented instead, ..."
From and Into From requires the method: fn from(T) -> Self. That produces a value conversion that consumes the original value. Into requires: fn into(self) -> T with the same result. Implementing From automatically implements Into, but the reverse is not true, so you should favor implementing From.
FromStr FromStr requires the function fn from_str(s: &str) -> Result<Self, Self::Err> This function is usually used implicitly through str's parse method.
AsRef AsRef requires the function: fn as_ref(&self) -> &T and the Trait ?Sized. The type of String.as_ref() is &String. AsRef allows a function accepting an &str to accept any type that implements AsRef<String>.
Deref Deref specifies the function: fn deref(&self) -> &Serlf::Target and requires the associated type: type Target: ?Sized;. It is used to silently convert a reference, r, into its referend, as if you invoked *r.
Sized and ?Sized Sized is a marker trait for types with constant size known at compile time. The ?Sized trait means that the type size is not known at compile time, e.g., a heap-based array.
Read Read specifies function: fn read(&mut self, buf: &mut [u8]) -> Result<usize>. The std::io::Read provides many additional useful functions.
Write Write specifies the function: fn write(&mut self, buf: &[u8]) -> Result<usize>. std::io::Write also specifies many additional useful functions.
Iterator Iterator specifies the function: fn next(&mut self) -> Option<Self::Item> where item is an associated type: type Item;. Iterator has many additional methods that make it useful for operating on collections.
IntoIterator IntoIterator specifies the function: fn into_iter(self) -> Self::IntoIter where Item is an associated type: type Item; and IntoIter is also an associated type: type IntoIter: Iterator;. IntoIterator defines how a collection type converts to an Iterator.
Many of the these traits are derivable for user-defined types, e.g., structs, as shown in the opening example. The Rust compiler generates a default implementation if it can. You will see traits defined explicitly in the Structs Bite. Steve Donovan has provided a nicely crafted description of these traits, on which much of This discussion was based: The Common Rust Traits . There is one important aspect of traits that is not discussed here: use for dynamic dispatch and polymorphic operations. That is deferred to the Structs Bite. If you are curious, Steve Donovan briefly discusses dynamic dispatch in the link above. Also, Josh Leeb has provided a related description of Trait Objects That addresses some of the ideas we will present in Abstraction.

3. Exercises:

  1. Write a generic function that accepts an array, [N; T] and converts that to a Vec<T>. Do that directly, without using any additional help from the Rust libraries.
  2. Repeat exercise #1 using an iterator to do all the work. Write the shortest line of code you can to accomplish this.
  3. Create a struct that holds the fields: name, occupation, and age (you pick the types). Now, can you endow that with the Display trait?
  4. Write code to demonstrate all the ways you can think of to convert a literal string: "a string" into a String instance.
  Next Prev Pages Sections About Keys