Please consider this Swift code. I have a class which wraps an instance of another class. When I set a property on the held value, the wrapper class's property observer is run.
protocol MyProtocol {
var msgStr: String? { get set }
}
class MyClass: MyProtocol {
var msgStr: String? {
didSet {
print("In MyClass didSet")
}
}
}
class MyWrapperClass {
var myValue: MyProtocol! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
let wrapperObj = MyWrapperClass()
wrapperObj.myValue = MyClass() // Line1
wrapperObj.myValue.msgStr = "Some other string" // Line2
The output of above code is:
In MyWrapperClass didSet
In MyClass didSet
In MyWrapperClass didSet
I know that didSet
is called when the value of the variable changes.
So when above code at "Line1" executes I understand that "In MyWrapperClass didSet" is printed, and that is fine.
Next when Line2 executes, I expect "In MyClass didSet" to be printed which correctly happens, but I am not sure why "In MyWrapperClass didSet" is printed, as the property myValue
is not changed. Can someone explain why?
Swift needs to treat the mutation of myValue.msgStr
as having value semantics; meaning that a property observer on myValue
needs to be triggered. This is because:
myValue
is a protocol-typed property (which also just happens to be optional). This protocol isn't class-bound, so conforming types could be both value and reference types.
The myStr
property requirement has an implicitly mutating
setter because of both (1) and the fact that it hasn't been marked nonmutating
. Therefore the protocol-typed value may well be mutated on mutating though its myStr
requirement.
Consider that the protocol could have been adopted by a value type:
struct S : MyProtocol {
var msgStr: String?
}
In which case a mutation of msgStr
is semantically equivalent to re-assigning an S
value with the mutated value of msgStr
back to myValue
(see this Q&A for more info).
Or a default implementation could have re-assigned to self
:
protocol MyProtocol {
init()
var msgStr: String? { get set }
}
extension MyProtocol {
var msgStr: String? {
get { return nil }
set { self = type(of: self).init() }
}
}
class MyClass : MyProtocol {
required init() {}
}
class MyWrapperClass {
// consider writing an initialiser rather than using an IUO as a workaround.
var myValue: MyProtocol! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
In which case the mutation of myValue.myStr
re-assigns a completely new instance to myValue
.
If MyProtocol
had been class-bound:
protocol MyProtocol : class {
var msgStr: String? { get set }
}
or if the msgStr
requirement had specified that the setter must be non-mutating:
protocol MyProtocol {
var msgStr: String? { get nonmutating set }
}
then Swift would treat the mutation of myValue.msgStr
as having reference semantics; that is, a property observer on myValue
won't get triggered.
This is because Swift knows that the property value cannot change:
In the first case, only classes can conform, and property setters on classes cannot mutate self
(as this is an immutable reference to the instance).
In the second case, the msgStr
requirement can only either be satisfied by a property in a class (and such properties don't mutate the reference) or by a computed property in a value type where the setter is non-mutating (and must therefore have reference semantics).
Alternatively, if myValue
had just been typed as MyClass!
, you would also get reference semantics because Swift knows you're dealing with a class:
class MyClass {
var msgStr: String? {
didSet {
print("In MyClass didSet")
}
}
}
class MyWrapperClass {
var myValue: MyClass! {
didSet {
print("In MyWrapperClass didSet")
}
}
}
let wrapperObj = MyWrapperClass()
wrapperObj.myValue = MyClass() // Line1
wrapperObj.myValue.msgStr = "Some other string" // Line2
// In MyWrapperClass didSet
// In MyClass didSet