Structures (struct)

Structures in Rust and C# share a few similarities:

  • They are defined with the struct keyword, but in Rust, struct simply defines the data/fields. The behavioural aspects in terms of functions and methods, are defined separately in an implementation block (impl).

  • They can implement multiple traits in Rust just as they can implement multiple interfaces in C#.

  • They cannot be sub-classed.

  • They are allocated on stack by default, unless:

    • In .NET, boxed or cast to an interface.
    • In Rust, wrapped in a smart pointer like Box, Rc/Arc.

In C#, a struct is a way to model a value type in .NET, which is typically some domain-specific primitive or compound with value equality semantics. In Rust, a struct is the primary construct for modeling any data structure (the other being an enum).

A struct (or record struct) in C# has copy-by-value and value equality semantics by default, but in Rust, this requires just one more step using the #derive attribute and listing the traits to be implemented:

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

Value types in C#/.NET are usually designed by a developer to be immutable. It's considered best practice speaking semantically, but the language does not prevent designing a struct that makes destructive or in-place modifications. In Rust, it's the same. A type has to be consciously developed to be immutable.

Since Rust doesn't have classes and consequently type hierarchies based on sub-classing, shared behaviour is achieved via traits and generics and polymorphism via virtual dispatch using trait objects.

Consider following struct representing a rectangle in C#:

struct Rectangle
{
    public Rectangle(int x1, int y1, int x2, int y2) =>
        (X1, Y1, X2, Y2) = (x1, y1, x2, y2);

    public int X1 { get; }
    public int Y1 { get; }
    public int X2 { get; }
    public int Y2 { get; }

    public int Length => Y2 - Y1;
    public int Width => X2 - X1;

    public (int, int) TopLeft => (X1, Y1);
    public (int, int) BottomRight => (X2, Y2);

    public int Area => Length * Width;
    public bool IsSquare => Width == Length;

    public override string ToString() => $"({X1}, {Y1}), ({X2}, {Y2})";
}

The equivalent in Rust would be:

#![allow(dead_code)]

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn top_left(&self) -> (i32, i32) {
        (self.x1, self.y1)
    }

    pub fn bottom_right(&self) -> (i32, i32) {
        (self.x2, self.y2)
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }

    pub fn is_square(&self)  -> bool {
        self.width() == self.length()
    }
}

use std::fmt::*;

impl Display for Rectangle {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "({}, {}), ({}, {})", self.x1, self.y2, self.x2, self.y2)
    }
}

Note that a struct in C# inherits the ToString method from object and therefore it overrides the base implementation to provide a custom string representation. Since there is no inheritance in Rust, the way a type advertises support for some formatted representation is by implementing the Display trait. This then enables for an instance of the structure to participate in formatting, such as shown in the call to println! below:

fn main() {
    let rect = Rectangle::new(12, 34, 56, 78);
    println!("Rectangle = {rect}");
}