Structs and Enums
Structs and enums are the fundamental building blocks of data modeling in both Swift and Rust. The two languages share the same core ideas – named fields, methods, associated functions, and enums with associated data – but organize them differently. The biggest structural difference is that Rust separates data definition from behavior: you define a struct’s fields in one place and its methods in a separate impl block.
Defining structs
Section titled “Defining structs”Both languages define structs with named fields, but the syntax for instantiation differs:
// Swiftstruct Point { var x: Double var y: Double}
let origin = Point(x: 0.0, y: 0.0)// Ruststruct Point { x: f64, y: f64,}
fn main() { let origin = Point { x: 0.0, y: 0.0 }; println!("({}, {})", origin.x, origin.y);}Swift uses a function-call-style initializer with parentheses, while Rust uses curly braces. Rust generates no automatic initializer – you always construct structs with the literal StructName { field: value } syntax.
Field init shorthand
Section titled “Field init shorthand”When a variable has the same name as a struct field, Rust lets you omit the field: value repetition:
// Ruststruct Point { x: f64, y: f64,}
fn make_point(x: f64, y: f64) -> Point { Point { x, y } // shorthand for Point { x: x, y: y }}
fn main() { let p = make_point(3.0, 4.0); println!("({}, {})", p.x, p.y);}Swift does not have this shorthand – you always write the labels explicitly.
Struct update syntax
Section titled “Struct update syntax”Rust has a spread-like syntax for creating a new struct from an existing one, replacing only the fields you specify:
// Ruststruct Config { width: u32, height: u32, fullscreen: bool,}
fn main() { let default_config = Config { width: 1920, height: 1080, fullscreen: false, };
let custom = Config { fullscreen: true, ..default_config };
println!("{}x{}, fullscreen: {}", custom.width, custom.height, custom.fullscreen);}Swift does not have a built-in equivalent, though you can achieve a similar effect by copying a struct and modifying properties (since Swift structs are value types with var properties).
Tuple structs
Section titled “Tuple structs”Rust also supports tuple structs – structs with unnamed fields accessed by index. These are useful for creating distinct types around a single value (the newtype pattern):
// Ruststruct Meters(f64);struct Seconds(f64);
fn main() { let distance = Meters(100.0); let duration = Seconds(9.58); println!("{} meters in {} seconds", distance.0, duration.0);}Swift does not have tuple structs, but you can achieve the same effect with a regular struct that has a single property.
Unit structs
Section titled “Unit structs”Rust supports unit structs – structs with no fields at all. They occupy zero bytes and are useful as marker types or when implementing traits:
// Ruststruct Marker;
fn main() { let _m = Marker;}Methods and impl blocks
Section titled “Methods and impl blocks”In Swift, methods are defined inside the type’s body. In Rust, methods are defined in a separate impl (implementation) block:
// Swiftstruct Circle { var radius: Double
func area() -> Double { return .pi * radius * radius }
mutating func scale(by factor: Double) { radius *= factor }}// Ruststruct Circle { radius: f64,}
impl Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn scale(&mut self, factor: f64) { self.radius *= factor; }}
fn main() { let mut c = Circle { radius: 5.0 }; println!("Area: {}", c.area()); c.scale(2.0); println!("Scaled area: {}", c.area());}The self parameter
Section titled “The self parameter”In Swift, self is implicitly available in all methods. In Rust, the first parameter of a method must be self in one of these forms:
&self: borrows the value immutably (like a non-mutating method in Swift)&mut self: borrows the value mutably (like amutatingmethod in Swift)self: takes ownership of the value (consumes it – the caller can no longer use it)
// Ruststruct Ticket { id: u32, description: String,}
impl Ticket { fn summary(&self) -> String { format!("#{}: {}", self.id, self.description) }
fn update_description(&mut self, new_desc: String) { self.description = new_desc; }
fn into_description(self) -> String { self.description // takes ownership, Ticket is consumed }}
fn main() { let mut ticket = Ticket { id: 1, description: String::from("Fix login bug"), };
println!("{}", ticket.summary()); ticket.update_description(String::from("Fix auth bug")); println!("{}", ticket.summary());
let desc = ticket.into_description(); // ticket is no longer usable here – it was moved println!("Description: {desc}");}The self (by value) form has no direct Swift equivalent. In Swift, structs are value types and do not have the concept of ownership transfer. Methods cannot consume the instance. In Rust, self by value means the method takes ownership and the original is moved. This is part of the ownership system covered in Part III.
Multiple impl blocks
Section titled “Multiple impl blocks”Rust allows multiple impl blocks for the same type. This is sometimes used to organize related methods or to separate trait implementations from inherent methods:
// Ruststruct Rectangle { width: f64, height: f64,}
impl Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Rectangle { fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }}
fn main() { let r = Rectangle { width: 10.0, height: 5.0 }; println!("Area: {}, Perimeter: {}", r.area(), r.perimeter());}Swift uses extensions for a similar purpose – splitting method definitions across multiple blocks. The difference is that in Rust, both inherent impl blocks and separate impl blocks are the same construct.
Associated functions
Section titled “Associated functions”Functions in an impl block that do not take self as their first parameter are called associated functions. They are called on the type itself, not on an instance – similar to static methods in Swift:
// Swiftstruct Color { var r: UInt8 var g: UInt8 var b: UInt8
static func red() -> Color { Color(r: 255, g: 0, b: 0) }}
let red = Color.red()// Ruststruct Color { r: u8, g: u8, b: u8,}
impl Color { fn red() -> Color { Color { r: 255, g: 0, b: 0 } }}
fn main() { let red = Color::red(); println!("({}, {}, {})", red.r, red.g, red.b);}Note the syntax: Rust calls associated functions with :: (double colon), while Swift uses . (dot). In Rust, . is reserved for method calls on instances; :: is used for associated functions, module paths, and enum variants.
The most common associated function is new, which serves as a conventional constructor. Rust does not have a language-level init like Swift – new is just a naming convention:
// Ruststruct Player { name: String, score: u32,}
impl Player { fn new(name: String) -> Player { Player { name, score: 0 } }}
fn main() { let p = Player::new(String::from("Alice")); println!("{}: {}", p.name, p.score);}Both languages support enums, and both go far beyond C-style enumerations. Swift and Rust enums can carry associated data, have methods, and be used in pattern matching. The terminology and syntax differ slightly.
Simple enums
Section titled “Simple enums”// Swiftenum Direction { case north case south case east case west}// Rustenum Direction { North, South, East, West,}
fn main() { let heading = Direction::North; match heading { Direction::North => println!("Going north"), Direction::South => println!("Going south"), Direction::East => println!("Going east"), Direction::West => println!("Going west"), }}Swift refers to values with Direction.north (or just .north when the type is known). Rust uses Direction::North (double colon, and variants are PascalCase by convention).
Enum variants with data
Section titled “Enum variants with data”Both languages support enums where each variant can carry different data. Swift calls this “associated values”; Rust calls them “tuple variants” and “struct variants.”
Tuple variants (unnamed fields):
// Swiftenum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double)}// Rustenum Shape { Circle(f64), Rectangle(f64, f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => std::f64::consts::PI * radius * radius, Shape::Rectangle(width, height) => width * height, }}
fn main() { let c = Shape::Circle(5.0); let r = Shape::Rectangle(10.0, 3.0); println!("Circle area: {}", area(&c)); println!("Rectangle area: {}", area(&r));}Struct variants (named fields):
// Rustenum Event { Click { x: i32, y: i32 }, KeyPress { code: u32, shift: bool }, Quit,}
fn describe(event: &Event) -> String { match event { Event::Click { x, y } => format!("Click at ({x}, {y})"), Event::KeyPress { code, shift } => { format!("Key {code}, shift: {shift}") } Event::Quit => String::from("Quit"), }}
fn main() { let e = Event::Click { x: 100, y: 200 }; println!("{}", describe(&e));}Rust’s struct variants are similar to Swift’s associated values with labels, but the syntax mirrors struct definitions. A Rust enum can mix unit variants (no data), tuple variants (positional data), and struct variants (named data) in the same enum.
Methods on enums
Section titled “Methods on enums”Both languages let you add methods to enums. In Rust, you use an impl block, just as with structs:
// Swiftenum Coin { case penny, nickel, dime, quarter
func value() -> Int { switch self { case .penny: 1 case .nickel: 5 case .dime: 10 case .quarter: 25 } }}// Rustenum Coin { Penny, Nickel, Dime, Quarter,}
impl Coin { fn value(&self) -> u32 { match self { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }}
fn main() { let coin = Coin::Quarter; println!("Value: {} cents", coin.value());}Option<T> and Result<T, E>
Section titled “Option<T> and Result<T, E>”Two of the most important types in Rust’s standard library are enums: Option<T> and Result<T, E>. Option<T> closely corresponds to Swift optionals. Result<T, E> plays a similar role to Swift’s fallible functions, but Rust represents success or failure as an explicit enum rather than with throws syntax.
Option<T>
Section titled “Option<T>”Swift represents the absence of a value with optionals (T?), which is syntactic sugar for Optional<T>. Rust uses Option<T>, which is defined as:
// Rust (standard library definition)enum Option<T> { Some(T), None,}Usage comparison:
// Swiftfunc find(name: String, in list: [String]) -> Int? { list.firstIndex(of: name)}
let index = find(name: "Alice", in: ["Bob", "Alice", "Charlie"])if let index { print("Found at \(index)")}// Rustfn find(name: &str, list: &[&str]) -> Option<usize> { list.iter().position(|&item| item == name)}
fn main() { let names = ["Bob", "Alice", "Charlie"]; let index = find("Alice", &names); if let Some(i) = index { println!("Found at {i}"); }}Some and None are so common in Rust that they are included in the prelude – you can use them without the Option:: prefix.
Result<T, E>
Section titled “Result<T, E>”Swift usually marks a function that can fail with throws, while Rust usually returns Result<T, E> directly. Swift also has a standard library Result<Success, Failure> type, but idiomatic Swift APIs more often use throws for fallible functions. Result<T, E> is an enum:
// Rust (standard library definition)enum Result<T, E> { Ok(T), Err(E),}Usage comparison:
// Swiftenum ParseError: Error { case invalidInput(String)}
func parseAge(_ input: String) throws -> Int { guard let age = Int(input) else { throw ParseError.invalidInput(input) } return age}// Rustuse std::num::ParseIntError;
fn parse_age(input: &str) -> Result<i32, ParseIntError> { input.parse::<i32>()}
fn main() { match parse_age("25") { Ok(age) => println!("Age: {age}"), Err(e) => println!("Error: {e}"), }}The key difference in philosophy: Swift hides the error handling mechanism behind try/catch syntax, making it look similar to exceptions (though it is not exception-based). Rust makes the Result type explicit in the function signature and requires the caller to handle it. Error handling is covered in depth in Part V.
Visibility
Section titled “Visibility”By default, struct fields and enum variants have different visibility rules:
- Struct fields are private by default. Even if the struct is public, its fields are private unless explicitly marked
pub. - Enum variants inherit the visibility of the enum. If the enum is
pub, all its variants are automatically public.
// Rustpub struct User { pub name: String, // public email: String, // private – not accessible outside this module}
pub enum Status { Active, // public because Status is public Inactive, // also public}In Swift, struct properties and enum cases follow the type’s access level by default, and you can override individual members with access modifiers like private or internal.
Key differences and gotchas
Section titled “Key differences and gotchas”- Separate
implblocks: Rust separates data (struct/enum definition) from behavior (implblock). Swift defines methods inside the type body. - No automatic initializers: Rust does not generate memberwise initializers. You either use struct literal syntax or write an associated function like
new. selfmust be explicit: Rust methods must declareselfas a parameter (&self,&mut self, orself). The choice between these forms affects ownership and borrowing.::vs.: Associated functions use::(Color::red()); methods use.(circle.area()). In Swift, both use..- Enum variant casing: Rust enum variants are
PascalCase(Direction::North). Swift enum cases arecamelCase(Direction.north). - Enum variant access: Rust uses
EnumName::Variant; Swift usesEnumName.caseor shorthand.case. - No
init: Rust has no initializer syntax. Thenewconvention is just an associated function. - Struct fields are private by default: In Rust, marking a struct as
pubdoes not make its fields public. Each field must be individually markedpub. OptionandResultare regular enums: They are not language-level syntax. You interact with them through pattern matching, method calls, and combinators.
Further reading
Section titled “Further reading”- Structs: The Rust Programming Language
- Enums and Pattern Matching: The Rust Programming Language
Optiondocumentation: Rust standard libraryResultdocumentation: Rust standard library