self Examination
July 04, 2021
Chris Eidhof posted a really interesting quiz question on the objc.io blog that sparked this post.
I'm about to spoil the question - Chris provides an answer on the same page where he asks the question - but I want to give you an opportunity to take a moment and answer the question before you continue here.
I'll explain the issue more clearly in a moment, but the key is that you can modify a let property in a struct in a mutating method.
Not only does this feel just plain wrong to me, I didn't know you could do this.
That's not unusual - Chris often explains something to me that I didn't know or understand before.
In this case it really bothered me that this was possible.
That said, everyone I discussed it with said, "that's weird", then they thought a moment and said, "oh that makes sense, self is mutable."
So it's just me here - but let me illustrate the issue and why it's got my brain bent a bit.
Here's a slight variation on Chris' example so that I can contrast his case with two others.
struct Person {
let name: String
private(set) var age: Int
var nickName: String
}
From the outside I look at Person and I see it as a struct that contains three properties. I'm free to change nickName because it's a var. age can change because it's a var but private(set) means that I can't change it at all from the outside. name is a let. The contract says that once my instance is created, name can't be changed.
As Chris shows, this last guarantee is not worth the bits it's printed on.
We can (and soon will) add a method to Person that allows the caller of the method to modify a let constant.
Let's create an instance of Person. We'll make it a var so that we can change various aspects of it.
var daniel = Person(name: "Daniel",
age: 103,
nickName: "Swifty McSwiftFace")
Since daniel is a var and nickname is a var we can change daniel's nickname to something else using simple assignment.
daniel.nickName = "Something Else"
No surprises there.
Now, suppose we want to change daniel's age.
This time it's not so easy. daniel is var and age is a var so it's possible but we can't do it from outside of Person because age is marked as private(set).
We can add a method to Person that sets Person's age.
But - and really this is part of the point I'm trying to make - because Person is a struct and age is an Int which is also a struct - this changes the contents of daniel so the method must be marked as mutating.
Add the method changeAge() in an extension and use it like this.
extension Person {
mutating func changeAge(to newAge: Int) {
age = newAge
}
}
daniel.changeAge(to: 25)
So when you change age you are changing daniel and we are working with value types so what I've always imagined is happening behind the scenes is that a new instance of Person is created with the same name and nickname and this new age. Then this new instance replaces the memory that daniel occupies.
From the outside it feels as if it's the same instance as before with the age changed.
What I didn't know until I read Chris' post, is that we can make this new instance and swap explicit like this.
extension Person {
mutating func changeAge(to newAge: Int) {
self = Person(name: name,
age: newAge,
nickName: nickName)
}
}
Yuck.
"Why would you ever do that?" you ask.
Hey, back off. I wouldn't.
But note that this same technique allows us to modify name - a let constant - in the same way.
extension Person {
mutating func changeName(to newName: String) {
self = Person(name: newName,
age: age,
nickName: nickName)
}
}
daniel.changeName(to: "Disappointed")
I hate that this works.
"But," you say, "in a mutating method self is mutable. We can assign to it.
I want to take a moment to note that Chris is not saying you should write code like this. He's just pointing out that it is valid Swift.
I don't want you to ever write code like this.
I am not talking about enums here - I'm talking about properties in structs. I'll add an example below of why this can be dangerous.
But first, here's what I'd rather see.
I want it to be obvious to the caller that they are changing a let constant.
If the caller wants to change age then they should have to reassign daniel explicitly. changeName() should no longer be mutating - it should return the changed instance of Person.
extension Person {
mutating func changeName(to newName: String) -> Person {
Person(name: newName,
age: age,
nickName: nickName)
}
}
daniel = daniel.changeName(to: "Disappointed")
So what's so dangerous about assigning self?
Let's modify the changeAge() method like this.
extension Person {
mutating func changeAge(to newAge: Int) {
self = Person(name: "Gotcha",
age: newAge,
nickName: nickName)
}
}
The caller is calling a method named changeAge() and passing in a new age. They have every expectation that age will change and because age is a var this feels right.
But, because self is mutable, the method can also change name which is a let.
This is completely unexpected.
If instead we couldn't assign self inside of a mutating function we would have had to write this like this instead.
extension Person {
mutating func changeAge(to newAge: Int) {
name = "Gotcha" // this doesn't compile
age = newAge
}
}
This doesn't compile because name is a let.
I see from the responses I'm getting that some people disagree, but I think it's important that code express our intent. Consumers of our code may not be able to see method implementations but they can see signatures and access levels for methods and properties.
While we're at it, change changeAge() back.
extension Person {
mutating func changeAge(to newAge: Int) {
age = newAge
}
}
Perhaps I'm making a big deal out of nothing, but Swift generally makes code easy to reason about and that feels to me like a big hole. A let should not behave like a private(set) var.