Aloha.zone.io

Aloha's technical notes.

View on GitHub

Rust notes

Some interesting facts and knowledge points in learning Rust.

Struct and Trait

Think different from class concept in C++ or Java, data and method are separated in Rust.

1. No inheritance, but composition for struct.

For data in struct, Rust do not have inheritance, but recommend composition.

Given a struct B is composed by another struct A:

   struct StructA {
     x: i32;
   };
   struct StructB {
     a: StructA, 
     // other fields...
   };

We have a instance of B let b: B;, then wecan access A’s member via: b.a.x;

Or, if we do not like the indirect way, but want a inherited style like a direct b.x, using the Deref and DerefMut traits will make it possible.

   impl std::ops::Deref for StructB {
     type Target = StructA;
     fn deref(&self) -> &Self::Target {
       &self.a
     }
   }

Then, we can make it real.

   let b = StructB { a: StructA };
   println!("{}", b.x);

2. Trait and dyn

Trait is a group of shared behavior, which looks like the concept interface.

Typically we use implement Trait for Struct to attach the methods to data. But since 1.0, Trait is used in double contexts.

3. Trait Bounds and Supertrait

4. Polymorphism

Polymorphism is real an important mechansim to make abstraction and reduce redundant code, no matter in OOP or FP. Though we have no inheritance for struct, but we can do polymorphism.

The above two sections mentioned:

 a. `dyn`, which runs dynamic dispatch mechanism. 
 
 b. Geneirc bounds, which use trait to stipulate what functionality a type implements.

That’s how polymorphism works in Rust. Use t: dyn Trait as the type of signature, the t.someMethod() will be dynamically decided using which implemention of Trait.

Example:

      struct Circle {
        radius: f64
      }
      struct Rectangle {
        height: f64,
        width: f64
      }

      trait Shape {
        fn area(&self) -> f64;
      }

      impl Shape for Circle {
        fn area(&self) -> f64 {
          PI * self.radius * self.radius
        }
      }
      impl Shape for Rectangle {
        fn area(&self) -> f64 {
          self.height * self.width
        }
      }
      
      // Polymorphism with "dyn" trait type
      fn print_area(shape: &dyn Shape) {
        println!("{}", shape.area());
      }
      
      // Polymorphism by bounds on generic type 
      fn print_area_generic<T: Shape> (shape: &T) {
        println!("{}", shape.area());
      }
      
      fn main() {
        let circle = Circle{radius: 2.0};
        let rectangle = Rectangle{height: 3.0, width: 5.0};
        print_area(&circle); // 12.5664
        print_area(&rectangle); // 15
        print_area_generic(&circle); // 12.5664
        print_area_generic(&rectangle); // 15
      }

5. Delegate boxed/wrapped struct to trait object

It is widely used for convenience, we may have some structs encapulate the other types with trait implemented. And we want the implemented trait could be used on the wrapper struct.

The most common case is like Box<T>, Rc<T>, Arc<T>.

A widely used practise is implement trait for wrapper structs to delegate the implementation, like this:

   impl<S: Solid + ?Sized> Solid for Box<S> {
       fn intersect(&self, ray: f32) -> f32 {
           (**self).intersect(ray)
           // Some people prefer this less-ambiguous form
           // S::intersect(self, ray)
       }
   }

Tips: bound ?Sized is necessary to make it could be optionally sized to support “S” as could be a trait type.

Formatter

Formatter trait makes it really simply to construct specific strings.

It is really useful for building strings or in a CLI tools.

Fill / Alignment

We need to align (to left or right) the strings with indents or some specific characters to an assigned length, or fill some characters to strings.

Using formatter with fill/alignment syntax is really helpful. It typically looks like {:0>8}

The align bit supports the align mode to left, center and right.

   < - the argument is left-aligned in width columns
   ^ - the argument is center-aligned in width columns
   > - the argument is right-aligned in width columns

Example scenario:

We are implementing Display trait for a date time struct, like:

     struct MyDateTime {
       year: i32,
       month: i32,
       day: i32,
       hour: i32,
       minute: i32,
       seconds: i32,
     }

The data is in i32 structure. To display them in a standard format like “dd/MM/yyyy - hh:mm:ss”, we need to fill some “0”s for the single-bit data. e.g. “01/02/2000 - 03:04:05”.

So we can format the data: 1. align to right; 2. make length to be 2 for day/month/hour/minute/second, and 4 for year; 3. fill the bits with “0”s to satify the length;

Now we have such a formatter for display:

      impl fmt::Display for MyDateTime {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          write!(f, "{:0>2}/{:0>2}/{:0>4} - {:0>2}:{:0>2}:{:0>2}", 
            self.day, 
            self.month, 
            self.year, 
            self.hour, 
            self.minute, 
            self.second,
          )
        }
      }

Reference