Published on

Rust tricks

Authors

@Author: Garfield Zhu

Rust Tricks

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.

  • As a trait: what is original designed, like:

    impl MyTrait for StructA {}
    
  • As a type: trait itself should also be allowed to be implemented, or implement other traits for it:

    impl MyTrait {}
    impl AnotherTrait for MyTrait {}
    

    Implement a trait, is just like making a "interface default method" in Java. And implement trait for trait is used for using the methods in trait to implement another trait.

    However, traits are actually unsized / dynamically-sized (or say "abstract"), using it as type leads to complex runtime errors.

    To resolve this, dyn is introduced since 1.27. Read RFC 2113 for more details.


    It makes the real type of the trait object (used as type) to be decided at runtime via dynamic dispatch

     trait Printable {
         fn stringify(&self) -> String;
     }
    
     impl Printable for i32 {
         fn stringify(&self) -> String { self.to_string() }
     }
    
     fn print(a: Box<dyn Printable>) {
         println!("{}", a.stringify());
     }
    
     fn main() {
         print(Box::new(10) as Box<dyn Printable>);
     }     
    
    

3. Trait Bounds and Supertrait

  • Bounds

    The above section mentioned dyn which decorates a trait to be a type. It's useful, but not always enough.

    Generics are much more expressive in type definition, and we can also need stipulate what functionality a generic type implements.

    Bounds is what we can use trait to restrict what the generic needs to implement.

    // Bounds Sample:
    fn printer<T: Display>(t: T) {
     println!("{}", t);
    }
    

    The bounds are common in other languages (typically uses keyword extends), like the Bounded Type in Java, Generic Constraints, etc.

    Bounds allows muliple traits (with + operator), and where clause to be more expressive.

  • Supertraits

    Rust doesn't have "inheritance", but you can define a trait as being a superset of another trait.

    It's easy to understand since the supertraits just like the syntax of "extends" of "interface" (not class) in Java. It means: 1) implement the subtrait (child interface) must implement all methods in supertrait (parent interface); 2) one trait can have multiple supertrait.

     trait Person {
         fn name(&self) -> String;
     }
    
     // Person is a supertrait of Student.
     // Implementing Student requires you to also impl Person.
     trait Student: Person {
         fn university(&self) -> String;
     }
    
     trait Programmer {
         fn fav_language(&self) -> String;
     }
    
     // CompSciStudent (computer science student) is a subtrait of both Programmer 
     // and Student. Implementing CompSciStudent requires you to impl both supertraits.
     trait CompSciStudent: Programmer + Student {
         fn git_username(&self) -> String;
     }
     
     /** Showcase of using an object with multiple supertraits */
     fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
      format!(
          "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
          student.name(),
          student.university(),
          student.fav_language(),
          student.git_username()
      )
    }
    

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}

  • Format syntax for fill/alignment:
  assert_eq!("00000110", format!("{:0>8}", "110"));
  //                                |||
  //                                ||+-- width
  //                                |+--- align
  //                                +---- fill

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

Rust Toolchain Cheat Sheet

If you already know Java, TypeScript/JavaScript, or Python, this table maps Rust concepts and tools to their nearest familiar counterparts. These are practical analogies rather than exact equivalents—Cargo, in particular, combines work that is often split across several tools in other ecosystems.

ConceptRustJavaTypeScript / JavaScriptPython
Compiler / runtimerustc (usually invoked through Cargo)javac + JVMTypeScript: tsc or a bundler; JavaScript: Node.js runtimepython interpreter (typically CPython)
Toolchain / version managerrustupSDKMAN! / jenvnvm / fnm / Voltauv python / pyenv
Package / project toolCargoMaven / Gradlenpm / pnpm / Yarnuv / pip / Poetry
Project manifestCargo.tomlpom.xml / build.gradle(.kts)package.jsonpyproject.toml
LockfileCargo.lockMaven: versions in the POM or a BOM; Gradle: dependency lockfilespackage-lock.json / pnpm-lock.yaml / yarn.lockuv.lock / poetry.lock / Pipfile.lock; newer standard: pylock.toml
Package unitCrate (Cargo package)JAR / Maven artifactnpm packagePython distribution (.whl / sdist)
Internal code unitModule (mod)Java packageES modulePython module / package
Multi-module projectCargo workspaceMaven multi-module / Gradle multi-projectnpm / pnpm / Yarn workspaceuv workspace
Public registrycrates.ioMaven Centralnpm registryPyPI
Add a dependencycargo add clapAdd it to pom.xml / build.gradle(.kts)pnpm add commanderuv add typer
Compile / type-checkcargo checkmvn compiletsc --noEmit (TypeScript)No required ahead-of-time compile step; python -m compileall checks syntax, while mypy / pyright check types
Buildcargo buildmvn packagetsc, vite build, tsup, etc.uv build / python -m build (wheel + source distribution)
Run an appcargo runmvn spring-boot:run / java -jarnode, tsx, npm run ...python app.py / python -m package / uv run ...
Unit testscargo testJUnit + Maven / GradleVitest / Jestpytest / unittest
Integration teststests/ + cargo testJUnit / TestcontainersVitest / Jest / Playwrightpytest (often with Testcontainers or other plugins)
Formattingcargo fmtSpotless / formatter pluginsPrettierruff format / Black
Lintingcargo clippyCheckstyle / SpotBugs / Error ProneESLintRuff (ruff check) / mypy
Documentationcargo docJavadocTypeDocpydoc / Sphinx / MkDocs
CLI argument parsingclappicocliCommander / yargsargparse / Click / Typer
Error handlingResult<T, E>, anyhow, thiserrorExceptionsExceptions / Result-style librariesExceptions
DistributionNative executableJAR + JVM / native imageJS bundle + Node.js, or a packaged executableWheel / source distribution; normally needs Python
Cross-platform buildUsually compile per OS / architectureUsually one JAR, given a compatible JVMUsually one JS bundle, given compatible runtime APIsPure-Python packages usually travel well; native extensions need platform-specific wheels
Binary size / runtime dependencyNative binary; no Rust runtime requiredJVM normally required (unless using a native image)Node.js normally required (unless packaged with the app)Python interpreter normally required