Defining and Instantiating Structs

Structs are one of the fundamental ways to create custom types in Oxide. A struct, short for "structure," lets you package together related values under a single name. If you're coming from object-oriented languages, a struct is similar to a class's data attributes.

Defining Structs

To define a struct, use the struct keyword followed by the struct name and curly braces containing the field definitions. Each field has a name and a type, using Oxide's camelCase naming convention for field names.

struct User {
    active: Bool,
    username: String,
    email: String,
    signInCount: UInt64,
}

Notice that:

  • Field names use camelCase (e.g., signInCount, not sign_in_count)
  • Types use Oxide's type aliases (Bool, UInt64) or standard types (String)
  • The struct body uses curly braces, just like Rust

Public Structs and Fields

To make a struct accessible from other modules, use the public keyword:

public struct User {
    public active: Bool,
    public username: String,
    email: String,           // Private by default
    signInCount: UInt64,     // Private by default
}

The public keyword replaces Rust's pub for visibility. You can apply it to both the struct itself and individual fields.

Creating Instances

To create an instance of a struct, specify the struct name followed by curly braces containing the field values:

fn main() {
    let user = User {
        active: true,
        username: "alice123".toString(),
        email: "alice@example.com".toString(),
        signInCount: 1,
    }
}

The order of fields doesn't matter when creating an instance, but all fields must be provided (unless they have default values through other mechanisms).

Mutable Instances

If you need to modify a struct instance after creation, use var instead of let:

fn main() {
    var user = User {
        active: true,
        username: "alice123".toString(),
        email: "alice@example.com".toString(),
        signInCount: 1,
    }

    // Now we can modify fields
    user.email = "newemail@example.com".toString()
    user.signInCount += 1
}

Note that in Oxide, the entire instance must be mutable to change any field. You cannot mark only certain fields as mutable.

Accessing Field Values

Use dot notation to access struct fields:

fn main() {
    let user = User {
        active: true,
        username: "alice123".toString(),
        email: "alice@example.com".toString(),
        signInCount: 1,
    }

    println!("Username: \(user.username)")
    println!("Email: \(user.email)")
    println!("Sign-in count: \(user.signInCount)")
}

Oxide's string interpolation with \(expression) makes it easy to embed field values directly in output strings.

Field Init Shorthand

When variable names match field names, you can use the shorthand syntax:

fn createUser(username: String, email: String): User {
    User {
        active: true,
        username,   // Same as username: username
        email,      // Same as email: email
        signInCount: 1,
    }
}

This shorthand reduces repetition when the parameter names match the field names.

Struct Update Syntax

When creating a new instance that reuses most values from an existing instance, use the struct update syntax with ..:

fn main() {
    let user1 = User {
        active: true,
        username: "alice123".toString(),
        email: "alice@example.com".toString(),
        signInCount: 1,
    }

    let user2 = User {
        email: "bob@example.com".toString(),
        ..user1  // Use remaining fields from user1
    }

    // user2 has bob's email but alice's username, active status, and signInCount
}

The ..user1 must come last in the struct literal. It copies all remaining fields from the source instance.

Important ownership note: The struct update syntax moves data from the source. After the update, user1 cannot be used if any of its fields were moved (like String fields). However, if only copyable types (like Bool or UInt64) are transferred, the source remains valid.

Tuple Structs

Oxide supports tuple structs, which are structs without named fields. These are useful when you want to give a tuple a distinct type name:

struct Color(Int, Int, Int)
struct Point(Int, Int, Int)

fn main() {
    let black = Color(0, 0, 0)
    let origin = Point(0, 0, 0)

    // Access fields by index
    let red = black.0
    let green = black.1
    let blue = black.2
}

Even though Color and Point have the same field types, they are different types. A function expecting a Color won't accept a Point.

Unit-Like Structs

You can also define structs with no fields, called unit-like structs:

struct AlwaysEqual

fn main() {
    let subject = AlwaysEqual
}

Unit-like structs are useful when you need to implement a trait on a type but don't need to store any data.

Adding Attributes

Structs commonly use derive attributes to automatically implement traits:

#[derive(Debug, Clone, PartialEq)]
public struct User {
    active: Bool,
    username: String,
    email: String,
    signInCount: UInt64,
}

The #[derive(...)] attribute automatically generates implementations for common traits:

  • Debug: Enables printing with {:?} format specifier
  • Clone: Enables creating deep copies with .clone()
  • PartialEq: Enables comparison with == and !=

Complete Example

Here's a complete example showing struct definition and usage:

#[derive(Debug, Clone)]
public struct Rectangle {
    width: Int,
    height: Int,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    }

    println!("Rectangle: {:?}", rect)
    println!("Width: \(rect.width)")
    println!("Height: \(rect.height)")

    // Create a modified copy
    let wider = Rectangle {
        width: 60,
        ..rect
    }

    println!("Wider rectangle: {:?}", wider)
}

Rust Comparison

AspectRustOxide
Visibilitypubpublic
Field namingsnake_casecamelCase
Struct syntaxstruct { }struct { } (same)
Tuple structstruct Point(i32, i32)struct Point(Int, Int)
Typesi32, bool, u64Int, Bool, UInt64

The underlying semantics are identical. Oxide structs are Rust structs with different naming conventions and type aliases. The compiled binary is exactly the same as equivalent Rust code.

Summary

Structs let you create custom types that package related data together:

  • Use struct with curly braces for named fields
  • Use camelCase for field names
  • Use public for visibility
  • Create instances with struct literals
  • Use var for mutable instances
  • Derive common traits with #[derive(...)]

In the next section, we'll build a complete program using structs to see how they work in practice.