Polyglot
Languages Swift data structures
Swift § data-structures

Data structures

Swift’s principal data structures are value-typed and generic: Array<T> (ordered, indexed), Dictionary<K, V> (hash map), Set<T> (unordered unique), Range<T> (numeric or character spans), tuples (anonymous heterogeneous), and the Optional<T> enum. Structs and enums admit user-defined types; enums admit associated values (sum types — substantial discrimination capability) and raw values (mapping to underlying primitives). The standard library provides substantial protocol-oriented surfaces — Sequence, Collection, Hashable, Codable, Comparable — admitting substantial functionality through conformance. The combination — value-typed collections with COW, generic types, enums with associated values, the Codable protocol for serialisation — is the substance of Swift’s data-structure surface.

Arrays

Ordered, indexed, dynamically-sized:

var arr: [Int] = [1, 2, 3]
var arr = [Int]()                                  // empty
var arr = Array<Int>()                             // verbose form
var arr = Array(repeating: 0, count: 5)            // [0, 0, 0, 0, 0]
var arr = Array(0..<10)                            // [0, 1, ..., 9]

Access

arr.count                                          // length
arr.isEmpty                                        // bool
arr[0]                                             // 1 (crashes on out-of-bounds)
arr.first                                          // Optional(1)
arr.last                                           // Optional(3)
arr.first(where: { $0 > 1 })                       // first matching

// Slicing:
arr[0..<2]                                         // ArraySlice
arr[1...]
arr[..<2]
let copy = Array(arr[1...])                       // independent Array

// Reverse:
arr.reversed()

Mutation

arr.append(4)                                      // adds at end
arr.append(contentsOf: [5, 6, 7])
arr += [8, 9]                                      // operator form

arr.insert(0, at: 0)                               // insert at index
arr.remove(at: 0)                                  // remove at index
arr.removeFirst()
arr.removeLast()
arr.removeAll(where: { $0 < 0 })

arr[2] = 99                                        // update index

arr.sort()                                          // mutating sort
arr.reverse()
arr.shuffle()

Iteration

for x in arr {
    print(x)
}

for (i, x) in arr.enumerated() {
    print("[\(i)] = \(x)")
}

// Functional:
let doubled = arr.map { $0 * 2 }
let evens = arr.filter { $0.isMultiple(of: 2) }
let sum = arr.reduce(0, +)

Treated in Loops.

Array properties

Arrays are value types — assignment copies (with COW):

var a = [1, 2, 3]
var b = a                                          // copy
b.append(4)
print(a)                                           // [1, 2, 3] (unchanged)
print(b)                                           // [1, 2, 3, 4]

For contiguous storage (no Objective-C bridging), ContiguousArray<T>:

var arr: ContiguousArray<Int> = [1, 2, 3]

Conventional only when bridging to Objective-C is undesirable.

Dictionaries

Hash maps:

var scores: [String: Int] = ["Alice": 95, "Bob": 87]
var scores = ["Alice": 95, "Bob": 87]              // type inferred
var scores = [String: Int]()                       // empty

Access

scores["Alice"]                                    // Optional(95)
scores["Charlie"]                                  // nil

// Default:
scores["Alice"] ?? 0
scores["Charlie", default: 0]                     // 0 (no nil)

scores.count
scores.isEmpty
scores.keys                                        // Dictionary<K, V>.Keys
scores.values

Mutation

scores["Charlie"] = 78                             // add or update
scores.updateValue(100, forKey: "Alice")           // returns old value
scores["Alice"] = nil                              // remove
scores.removeValue(forKey: "Alice")                // returns the removed value

scores.merge(["Dave": 88]) { current, _ in current }  // merge with conflict resolution

Iteration

for (key, value) in scores {                       // unordered
    print("\(key): \(value)")
}

for key in scores.keys {
    print(key)
}

// Sorted:
for (key, value) in scores.sorted(by: { $0.key < $1.key }) {
    print("\(key): \(value)")
}

Dictionaries are value types; iteration order is unspecified (consistent within a run; differs across runs).

Default values

var counts = [String: Int]()
for word in words {
    counts[word, default: 0] += 1
}

The [key, default: x] admits substantial conciseness for “increment-or-initialise”.

Sets

Unordered unique collections:

var fruits: Set<String> = ["apple", "banana", "apple"]
print(fruits)                                      // ["banana", "apple"] (unique)

fruits.insert("cherry")                            // returns (inserted: Bool, memberAfterInsert: T)
fruits.contains("apple")
fruits.remove("banana")

let a: Set = [1, 2, 3]
let b: Set = [2, 3, 4]
a.union(b)                                         // {1, 2, 3, 4}
a.intersection(b)                                  // {2, 3}
a.subtracting(b)                                   // {1}
a.symmetricDifference(b)                           // {1, 4}

a.isSubset(of: [1, 2, 3, 4, 5])                    // true
a.isSuperset(of: [1, 2])
a.isStrictSubset(of: [1, 2, 3, 4])                 // true (subset and not equal)
a.isDisjoint(with: [10, 20])                       // true

The element type must conform to Hashable.

Tuples

Anonymous fixed-size aggregates:

let pair: (String, Int) = ("Alice", 30)
let triple: (Int, Int, String) = (1, 2, "three")

// Destructuring:
let (name, age) = pair
print(name, age)                                   // "Alice 30"

// Named elements:
let person: (name: String, age: Int) = (name: "Alice", age: 30)
print(person.name, person.age)

// By index:
print(pair.0, pair.1)

Tuples admit substantial conciseness for multi-return values:

func divmod(_ a: Int, _ b: Int) -> (quotient: Int, remainder: Int) {
    (a / b, a % b)
}

let result = divmod(17, 5)
print(result.quotient, result.remainder)           // 3 2

let (q, r) = divmod(17, 5)

Tuples conform to Equatable if their components do.

Structs

User-defined value types:

struct Point {
    var x: Double
    var y: Double

    func distanceTo(_ other: Point) -> Double {
        sqrt((x - other.x).squared + (y - other.y).squared)
    }

    mutating func translate(by delta: Point) {     // mutating required
        x += delta.x
        y += delta.y
    }
}

var p = Point(x: 1, y: 2)
p.translate(by: Point(x: 10, y: 20))

Structs admit synthesised initialisers:

struct Person {
    let name: String
    let age: Int
}

// Compiler synthesises:
// init(name: String, age: Int)

let p = Person(name: "Alice", age: 30)

Treated in Value and reference types.

Enums

User-defined sum types:

enum Direction {
    case up
    case down
    case left
    case right
}

let d: Direction = .up

Raw values

Enums admit raw values:

enum Status: String {
    case active = "active"
    case inactive = "inactive"
    case banned = "banned"
}

let s: Status = .active
print(s.rawValue)                                  // "active"
let parsed = Status(rawValue: "active")            // Optional<.active>

enum Priority: Int {
    case low = 1
    case medium = 2
    case high = 3
}

print(Priority.high.rawValue)                      // 3
let p = Priority(rawValue: 2)                      // Optional<.medium>

Common raw types: String, Int, Double. The compiler synthesises init(rawValue:).

Associated values

Enums admit associated values — distinguishing them from C-style enums:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(base: Double, height: Double)
}

let s: Shape = .circle(radius: 5)
let r: Result<Int, MyError> = .success(42)

The associated values admit substantial discrimination via pattern matching:

switch s {
case .circle(let r):
    print("circle radius \(r)")
case .rectangle(let w, let h):
    print("rectangle \(w) x \(h)")
case .triangle(let b, let h):
    print("triangle base \(b), height \(h)")
}

Treated in Pattern matching.

Methods on enums

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)

    func area() -> Double {
        switch self {
        case .circle(let r): return Double.pi * r * r
        case .rectangle(let w, let h): return w * h
        }
    }
}

Recursive enums

The indirect admits recursion:

indirect enum Expr {
    case literal(Int)
    case add(Expr, Expr)
    case mul(Expr, Expr)
}

let e = Expr.add(.literal(2), .mul(.literal(3), .literal(4)))

The mechanism admits substantial AST-style data structures.

CaseIterable

enum Direction: CaseIterable {
    case up, down, left, right
}

for d in Direction.allCases {
    print(d)
}

Range

Spans of values:

let r1 = 0..<10                                    // half-open
let r2 = 0...10                                    // closed
let r3 = "a"..."z"                                 // character range

let oneSided1 = 5...                               // [5, 6, ..., infinity)
let oneSided2 = ..<10                              // (-infinity, 10)
let oneSided3 = 0...                               // [0, infinity)

// Ranges as sequences:
for n in 0..<5 {
    print(n)                                       // 0, 1, 2, 3, 4
}

// Range methods:
r1.contains(5)                                     // true
r1.count                                           // 10
r1.lowerBound                                      // 0
r1.upperBound                                      // 10

Ranges are conformant Sequence; iteration admits substantial efficiency.

Codable

The Codable protocol admits automatic serialisation/deserialisation:

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// Encode:
let user = User(id: 1, name: "Alice", email: "a@b.c")
let json = try JSONEncoder().encode(user)
print(String(data: json, encoding: .utf8)!)
// {"id":1,"name":"Alice","email":"a@b.c"}

// Decode:
let decoded = try JSONDecoder().decode(User.self, from: json)

The conformance is synthesised automatically for structs/classes/enums whose properties are all Codable. The standard types (String, Int, Double, Date, URL, Data, Array, Dictionary, Set, Optional) conform.

Custom encoding

struct User: Codable {
    let id: Int
    let name: String
    let email: String

    enum CodingKeys: String, CodingKey {
        case id
        case name = "user_name"                    // map JSON "user_name" to name
        case email
    }
}

The CodingKeys admits renaming and excluding fields.

For substantial customisation:

struct User: Codable {
    let id: Int
    let createdAt: Date

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)

        let timestamp = try container.decode(Double.self, forKey: .createdAt)
        createdAt = Date(timeIntervalSince1970: timestamp)
    }

    enum CodingKeys: String, CodingKey {
        case id, createdAt
    }
}

Common patterns

Array operations

let arr = [1, 2, 3, 4, 5]

let doubled = arr.map { $0 * 2 }
let evens = arr.filter { $0.isMultiple(of: 2) }
let sum = arr.reduce(0, +)
let max = arr.max() ?? 0
let sorted = arr.sorted(by: >)
let unique = Array(Set(arr))                       // dedup (loses order)

Dictionary operations

let scores = ["Alice": 95, "Bob": 87]

scores.mapValues { $0 + 5 }                        // [String: Int]
scores.filter { $0.value > 90 }
scores.compactMapValues { $0 > 0 ? $0 : nil }      // remove non-positive

let sortedByValue = scores.sorted { $0.value > $1.value }

Group-by

let grouped = Dictionary(grouping: people) { $0.age / 10 * 10 }
// [decade: [people]]

Tally

var counts: [String: Int] = [:]
for word in words {
    counts[word, default: 0] += 1
}

// Or with reduce:
let counts2 = words.reduce(into: [String: Int]()) { acc, word in
    acc[word, default: 0] += 1
}

Set operations

let required: Set = ["name", "email", "password"]
let provided: Set = ["name", "email"]

let missing = required.subtracting(provided)       // {"password"}
let extra = provided.subtracting(required)

Type-parameterised stack

struct Stack<Element> {
    private var items: [Element] = []

    var isEmpty: Bool { items.isEmpty }
    var top: Element? { items.last }

    mutating func push(_ x: Element) {
        items.append(x)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }
}

var s = Stack<Int>()
s.push(1)
s.push(2)
s.pop()                                            // Optional(2)

Codable with nested types

struct Order: Codable {
    let id: Int
    let items: [Item]
    let customer: Customer

    struct Item: Codable {
        let name: String
        let quantity: Int
        let price: Double
    }

    struct Customer: Codable {
        let id: Int
        let name: String
    }
}

let json = try JSONEncoder().encode(order)
let decoded = try JSONDecoder().decode(Order.self, from: json)

Pretty-printed JSON

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let json = try encoder.encode(user)

Date encoding

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

Snake-case conversion

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

The conventions admit substantial handling of API conventions.

Enum with raw values for serialisation

enum Status: String, Codable {
    case active
    case inactive
    case banned
}

struct User: Codable {
    let name: String
    let status: Status                              // Codable through raw value
}

Discriminated union via enum

enum LoadState<T: Codable>: Codable {
    case idle
    case loading
    case loaded(T)
    case failed(message: String)

    enum CodingKeys: String, CodingKey {
        case state
        case data
        case message
    }

    enum State: String, Codable {
        case idle, loading, loaded, failed
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .idle:
            try container.encode(State.idle, forKey: .state)
        case .loading:
            try container.encode(State.loading, forKey: .state)
        case .loaded(let value):
            try container.encode(State.loaded, forKey: .state)
            try container.encode(value, forKey: .data)
        case .failed(let message):
            try container.encode(State.failed, forKey: .state)
            try container.encode(message, forKey: .message)
        }
    }
}

Result for async outcomes

let result: Result<Data, Error>

switch result {
case .success(let data):
    process(data)
case .failure(let error):
    log(error)
}

Range for indexing

let arr = [10, 20, 30, 40, 50]
let first3 = arr[0..<3]                            // [10, 20, 30]
let last2 = arr[3...]                              // [40, 50]
let middle = arr[1..<4]                            // [20, 30, 40]

Pre-allocated array

var arr = [Int]()
arr.reserveCapacity(1000)                          // avoid reallocs

for i in 0..<1000 {
    arr.append(i)
}

Counter pattern

let counts: [String: Int] = words.reduce(into: [:]) { acc, word in
    acc[word, default: 0] += 1
}

A note on the conventional discipline

The contemporary Swift data-structures advice:

  • Use arrays ([T]) for ordered sequences.
  • Use dictionaries ([K: V]) for key-value lookups.
  • Use sets (Set<T>) for unique-value collections.
  • Use tuples for small unnamed aggregates.
  • Use structs for named aggregates (the conventional default).
  • Use enums with associated values for sum types and discriminated unions.
  • Use let over var for properties.
  • Use Codable for serialisation; rely on synthesised conformance.
  • Use [default:] for “increment-or-initialise” patterns.
  • Use Set or sorted Array for membership tests.
  • Use mutating methods on structs that modify properties.
  • Use protocol conformances (Hashable, Equatable) for substantial flexibility.

The combination — value-typed collections with COW, generic types with substantial protocol conformances, enums with associated values for sum types, the Codable automatic serialisation, the substantial Sequence/Collection method surface — is the substance of Swift’s data-structure surface. The discipline produces concise, type-safe, expressive code with substantial built-in functionality.