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.
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(_:)
.
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)
}
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
}
}
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.
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.