Skip to content

Variables, Constants, and Types

Swift and Rust both use let to declare values – but the keyword means something different in each language. In Swift, let creates an immutable binding and var creates a mutable one. In Rust, let creates an immutable binding too, but you opt into mutability with let mut rather than a separate keyword.

In Swift, you choose between let and var:

// Swift
let name = "Alice" // immutable
var age = 30 // mutable
age = 31

In Rust, all bindings are immutable by default. You add mut when you need to reassign:

// Rust
fn main() {
let name = "Alice"; // immutable
let mut age = 30; // mutable
age = 31;
println!("{name} is {age}");
}

Attempting to reassign an immutable binding is a compile-time error in both languages. The difference is purely syntactic: Swift uses two keywords (let/var), while Rust uses one keyword with an optional modifier (let/let mut).

Rust’s design makes immutability the path of least resistance. You have to consciously decide to make a binding mutable, which encourages a style where most values never change.

Swift has a default integer type, Int, which is platform-sized (64 bits on modern Apple hardware). You can also use explicit sizes like Int8, Int16, Int32, Int64, and their unsigned counterparts UInt8 through UInt64.

Rust has no default integer type in the same sense. Instead, you choose an explicit size whenever you annotate a type. The naming convention is shorter: i8, i16, i32, i64, i128 for signed integers, and u8, u16, u32, u64, u128 for unsigned. Rust also provides isize and usize, which are pointer-sized – equivalent to Swift’s Int and UInt.

// Swift
let count: Int = 42
let byte: UInt8 = 255
let big: Int64 = 1_000_000
// Rust
fn main() {
let count: i32 = 42;
let byte: u8 = 255;
let big: i64 = 1_000_000;
println!("{count}, {byte}, {big}");
}

When you write an integer literal without a type annotation, Rust infers i32 by default – not a pointer-sized type. This is a common surprise for Swift developers who expect the default to be platform-sized.

SwiftRustSize
Int8i88-bit signed
Int16i1616-bit signed
Int32i3232-bit signed
Int64i6464-bit signed
IntisizePointer-sized
UInt8u88-bit unsigned
UInt16u1616-bit unsigned
UInt32u3232-bit unsigned
UInt64u6464-bit unsigned
UIntusizePointer-sized

Rust also has 128-bit integers (i128/u128), which Swift does not offer natively.

Swift traps on integer overflow by default in both debug and optimized builds unless you explicitly opt into wrapping with operators like &+, &-, and &*. Rust makes a different tradeoff: integer overflow checks panic in debug builds and wrap in release builds. For explicit wrapping, Rust provides wrapping_add, wrapping_sub, and related methods, as well as the Wrapping<T> type:

// Rust
fn main() {
let x: u8 = 255;
let y = x.wrapping_add(1); // 0, no panic
println!("{y}");
}

Both languages have 32-bit and 64-bit floating-point types. Swift uses Float (32-bit) and Double (64-bit), with Double being the default for float literals. Rust uses f32 and f64, with f64 as the default.

// Swift
let pi: Double = 3.14159
let approx: Float = 3.14
// Rust
fn main() {
let pi: f64 = 3.14159;
let approx: f32 = 3.14;
println!("{pi}, {approx}");
}

Both languages have a boolean type. Swift calls it Bool; Rust calls it bool (lowercase, following Rust’s convention for primitive types).

// Swift
let isReady: Bool = true
// Rust
fn main() {
let is_ready: bool = true;
println!("{is_ready}");
}

Rust has a char type that represents a single Unicode scalar value. Swift’s nearest counterpart is Character, which represents an extended grapheme cluster. In Swift, character literals use double quotes. In Rust, character literals use single quotes – double quotes are for string slices.

// Swift
let letter: Character = "A"
let emoji: Character = "🦀"
// Rust
fn main() {
let letter: char = 'A';
let emoji: char = '🦀';
println!("{letter}, {emoji}");
}

Rust’s char is always 4 bytes and represents a Unicode scalar value (U+0000 to U+D7FF and U+E000 to U+10FFFF). Swift’s Character represents an extended grapheme cluster, which can contain multiple Unicode scalars. This means a Swift Character like ”👨‍👩‍👧” (a family emoji composed of multiple scalars joined by zero-width joiners) is a single character, while Rust would need a &str or String to represent it.

Both languages have strong type inference. In most cases, you can omit the type annotation and the compiler will figure it out:

// Swift
let name = "Alice" // String
let count = 42 // Int
let ratio = 3.14 // Double
let flag = true // Bool
// Rust
fn main() {
let name = "Alice"; // &str (string slice, not String)
let count = 42; // i32 (not isize)
let ratio = 3.14; // f64
let flag = true; // bool
println!("{name}, {count}, {ratio}, {flag}");
}

Two differences to note. First, Rust infers string literals as &str (a borrowed string slice), not String. This distinction matters and is covered in the Strings chapter. Second, as mentioned earlier, integer literals default to i32, not a pointer-sized integer.

Rust’s type inference is also context-sensitive. It can infer types based on how a value is used later in the function:

// Rust
fn main() {
let mut numbers = Vec::new(); // type not yet known
numbers.push(5_u64); // now inferred as Vec<u64>
println!("{numbers:?}");
}

Swift does the same when it can, but Rust’s inference is particularly effective with generic collections and iterators.

When inference is not sufficient or when you want to be explicit, both languages let you annotate types. The syntax differs: Swift puts the type after a colon with no space between the name and the colon, while Rust does the same.

// Swift
let count: Int = 42
let name: String = "Alice"
// Rust
fn main() {
let count: i32 = 42;
let name: String = String::from("Alice");
println!("{count}, {name}");
}

For numeric literals in Rust, you can also use a type suffix instead of an annotation:

// Rust
fn main() {
let count = 42_i64;
let size = 1024_usize;
println!("{count}, {size}");
}

Swift does not have type suffixes for literals.

This is one of the larger behavioral differences between the two languages. Rust allows you to redeclare a variable with the same name in the same scope – this is called shadowing. The new binding replaces the old one:

// Rust
fn main() {
let x = 5;
let x = x + 1; // shadows the first x
let x = x * 2; // shadows the second x
println!("{x}"); // prints 12
}

Swift does not allow shadowing in the same scope. This code would be a compiler error:

// Swift
let x = 5
let x = x + 1 // error: invalid redeclaration of 'x'

Swift does allow shadowing across scopes (e.g., a local variable can shadow a parameter, and an inner scope can shadow an outer one), but Rust allows it within the same scope too.

Shadowing in Rust is useful for transforming a value while keeping the same name, and it also lets you change the type of a binding:

// Rust
fn main() {
let input = "42";
let input: i32 = input.parse().expect("not a number");
println!("{input}");
}

Here, input starts as a &str and is shadowed by a new binding of type i32. This is a common Rust pattern. In Swift, you would need to use a different variable name since you cannot redeclare the same name and also cannot change its type.

Note that shadowing creates a new binding – it does not mutate the original. The old value is simply no longer accessible by that name (and will be dropped if nothing else references it).

Both languages support tuples – anonymous groupings of values. The syntax is nearly identical:

// Swift
let point: (Int, Int) = (10, 20)
let x = point.0
let y = point.1
// Rust
fn main() {
let point: (i32, i32) = (10, 20);
let x = point.0;
let y = point.1;
println!("({x}, {y})");
}

Both languages support destructuring tuples:

// Swift
let (x, y) = (10, 20)
// Rust
fn main() {
let (x, y) = (10, 20);
println!("({x}, {y})");
}

Tuples can contain mixed types in both languages:

// Rust
fn main() {
let record: (i32, f64, bool) = (42, 3.14, true);
let (id, score, active) = record;
println!("{id}, {score}, {active}");
}

One difference: Swift supports named tuple elements (let point: (x: Int, y: Int) = (x: 10, y: 20)), while Rust does not. If you need named fields in Rust, use a struct.

Rust has a type called the unit type, written (). It is a tuple with zero elements, and it represents the absence of a meaningful value. Functions that do not return anything implicitly return ().

// Rust
fn greet(name: &str) {
println!("Hello, {name}!");
// implicitly returns ()
}
fn greet_explicit(name: &str) -> () {
println!("Hello, {name}!");
}
fn main() {
greet("Alice");
greet_explicit("Bob");
}

In Swift, the equivalent is Void, which is actually a type alias for the empty tuple ():

// Swift
func greet(name: String) {
print("Hello, \(name)!")
// implicitly returns Void
}
func greetExplicit(name: String) -> Void {
print("Hello, \(name)!")
}

The parallel is exact: both languages use the empty tuple as their “nothing” return type, and both let you omit it from function signatures.

Both languages let you create new names for existing types:

// Swift
typealias UserID = Int
typealias Coordinate = (Double, Double)
let id: UserID = 42
let location: Coordinate = (37.7749, -122.4194)
// Rust
type UserId = i32;
type Coordinate = (f64, f64);
fn main() {
let id: UserId = 42;
let location: Coordinate = (37.7749, -122.4194);
println!("User {id} at ({}, {})", location.0, location.1);
}

Swift uses typealias; Rust uses type. In both languages, a type alias does not create a new distinct type – it is just an alternative name for the same type. Values of the alias type and the original type are interchangeable.

  • Mutability keyword: Swift uses var for mutable bindings; Rust uses let mut. Rust’s let alone is immutable.
  • Default integer type: Swift defaults to Int (pointer-sized); Rust defaults to i32 (32-bit).
  • Integer sizes: Rust names are shorter (i32 vs Int32) and include 128-bit types.
  • Shadowing: Rust allows redeclaring a variable in the same scope with let, even changing its type. Swift only allows shadowing across different scopes.
  • String literals: In Swift, a string literal produces a String. In Rust, a string literal produces a &str (a borrowed reference). This is covered in detail in the Strings chapter.
  • Character literals: Rust uses single quotes for char and double quotes for strings. Swift uses double quotes for both.
  • Named tuple fields: Swift supports them; Rust does not. Use a struct in Rust when you need named fields.
  • No implicit conversions: Neither language performs implicit numeric conversions. You must explicitly cast with as in Rust or initializers in Swift. Rust uses value as f64; Swift uses Double(value).