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.