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