Skip to content

Instantly share code, notes, and snippets.

@niw
Last active December 23, 2024 08:46
Show Gist options
  • Save niw/f41b83aa431d991b171b1ccfbce8fc10 to your computer and use it in GitHub Desktop.
Save niw/f41b83aa431d991b171b1ccfbce8fc10 to your computer and use it in GitHub Desktop.
Use `var` in `struct` in Swift

Use var in struct in Swift

TL;DR: Use var for properties in struct as long as it serves as a nominal tuple. In most cases, there is no obvious benefit to using let for struct properties.

Simple Example

Let's start with a simple example:

struct MyStruct {
    let name: String
}

When using a struct as a nominal tuple, a term from type theory meaning a tuple where each field is identified by its name, there are no strong reasons to prefer let over var.

The intention behind using let might be to prevent accidental changes, making the property immutable. However, this immutability is not absolute due to Swift's mutating behavior. Here's an example:

extension MyStruct {
    mutating func setName(_ newName: String) {
        self = .init(name: newName)
    }
}

Someone can write this code without your knowledge somewhere in the code base. Even though name is declared with let, you can still mutate it if MyStruct itself is declared as var:

var test = MyStruct(name: "test")
// test.name = "modified" // This is not possible.
test.setName("modified") // But this is possible.
print(test.name) // Output: "modified"

Thus, if a struct value is declared as var, it can be mutated regardless of whether the properties are declared with let or var. Conversely, if the value is declared as let, no mutation is possible, and the compiler will prevent the use of setName(_:).

Example of API Response Struct

Consider an API response struct where we use let to "feel safe":

struct User: Codable {
    let name: String
    let nicknames: [String]
}

However, this perceived safety can be circumvented:

extension User {
    mutating func addNickname(_ newNickname: String) {
        var newNicknames = nicknames
        newNicknames.append(newNickname)
        self = .init(name: name, nicknames: newNicknames)
    }
}

The following code demonstrates this:

var user = User(name: "test", nicknames: ["alpha", "beta"])
user.addNickname("charlie")
print(user.nicknames) // Output: "alpha", "beta", "charlie"

In this example, you might realize that addNickname(_:) is actually useful for implementing an application feature. Using let does not prevent mutability in practice, and it can add unnecessary complexity.

If the stored properties were var, this code could be much simpler, and in some cases, an explicit addNickname(_:) might not even be needed:

struct User: Codable {
    var name: String
    var nicknames: [String]
}

var user = User(name: "test", nicknames: ["alpha", "beta"])
user.nicknames.append("charlie")
print(user.nicknames) // Output: "alpha", "beta", "charlie"

Using let here adds boilerplate code, like reinitializing the User value, which is unnecessary and cumbersome, especially if the struct has more properties:

func addItem(_ item: Item) {
    var newItems = items
    newItems.append(item)
    self = .init(a: a, b: b, c: c, ..., items: newItems)
}

Strong Consistency Between Properties

There are rare cases where using let might make sense, such as ensuring strong consistency between multiple stored properties. Consider the following:

protocol Rectangle {
    var width: Double { get }
    var height: Double { get }
}

struct Square: Rectangle {
    let width: Double
    let height: Double

    init(size: Double) {
        width = size
        height = size
    }
}

In this case, width and height need to remain consistent to ensure the integrity of the Square. However, even in this case, you could implement the consistency logic directly:

struct Square: Rectangle {
    private var size: Double

    var width: Double { size }
    var height: Double { size }

    init(size: Double) {
        self.size = size
    }
}

Rare Use Cases for let

A reasonable use case for let is, for example, when caching a value that is computationally expensive to calculate, and you want to compute it only once:

struct DataWithExpensiveHash {
    let data: Data
    let expensiveHash: String
    
    init(_ data: Data) async {
        self.data = data
        self.expensiveHash = await /* slow calculation of hash */
    }
}

However, these situations are rare compared to the typical use cases like the User struct.

Conclusion

In conclusion, using let for stored properties in struct provides no obvious benefits in most cases and often introduces unnecessary complexity. Therefore, I recommend using var for struct properties, especially when the struct functions as a nominal tuple.

Note: This discussion only applies to value types (struct). For reference types (class), the considerations are different, and using let by default is generally advisable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment