Protocol extension with type restriction (AnyObject / class)

Hi everyone,

Recently I came across a restriction I did not think I would encounter while working on protocol extensions and composition.

I have two protocols, one that can be applied to any type and the other one that can only be applied to class:

protocol ProtocolA {
    var myVarA: Int { get set }
}

protocol ProtocolB: class {
    var myVarB: Int { get set }
}

When I try to extend ProtocolB when self is also ProtocolA I cannot mutate myVarA property

extension ProtocolB where Self: ProtocolA {
    var myVar: Int {
        get { fatalError() }
        set { self.myVarA = self.myVarB } // 🛑 Cannot assign to property: 'self' is immutable
    }
}

I understand that I get this error because ProtocolA can be applied to structs, which are immutable.

However, considering that ProtocolB can only be applied to class types, why doesn't it force Self to be mutable as well, even if ProtocolA can be applied to value type? Shouldnt the error be shown only when both protocols are applicable to value type?

Plus, thanks to this post I found out this was possible to get around this error very easily by doing so:

extension ProtocolB where Self: ProtocolA {
    var myVar: Int {
        get { fatalError() }
        set {
            var s = self
            s.myVarA = self.myVarB
        }
    }
}

Which behaves exactly as intended: It modifies myVarA only if Self is reference type, which is forced by ProtocolB conformance.

Is it an intended restriction? If not why Self does not assume both ProtocolA and ProtocolB type restrictions in extension?

Consider this extension of ProtocolA that replaces self with another value:

extension ProtocolA {
    var myVarA: Int { 
        get { 1 } 
        set { self = other }
    }
}

What happen if you conform a class to this protocol? The setter will replace the reference with a reference to another object. Thus it is mutating.


You can also make the setter nonmutating in ProtocolA, but this will prevent properties of conforming structs from being mutated:

protocol ProtocolA {
    var myVarA: Int { get nonmutating set }
}

I am not really sure I understand, I do want varA to be modified when varB is set.

What I dont understand is why in my extension, the conformance to ProtocolA does not automatically assume Self is a class / AnyObject due to ProtocolB ? Isn't it more logical to present this error only when both protocols can be applied to value type? like:

protocol ProtocolA {}
protocol ProtocolB {}
// Does show the error 🛑 Cannot assign to property: 'self' is immutable

protocol ProtocolA {}
protocol ProtocolB: class {}
// Should not show the error... 

protocol ProtocolA: class {}
protocol ProtocolB: class {}
// ... just like this doesnt shows the error

My reasoning is that in ProtocolB extension we know for sure that Self is class type, even if it is conform to ProtocolA.


Here is my full playground, when playing with :class extensions

import UIKit
import Foundation

protocol ProtocolA: class {
    var myVarA: Int { get set }
}

protocol ProtocolB: class {
    var myVarB: Int { get set }
}

extension ProtocolB where Self: ProtocolA {
    var myVarB: Int {
        get { return myVarA }
        set {
            myVarA = newValue
        }
    }
}

class Foo: ProtocolB, ProtocolA {
    var myVarA: Int = 1
}

var f = Foo()

print(f.myVarA) // 1
print(f.myVarB) // 1

f.myVarB = 3

print(f.myVarA) // 3
print(f.myVarB) // 3

@michelf is talking about scenario like this:

protocol ProtocolA {
    var myVarA: Int { get set }
    init() // Added to demonstrate the self-replacement behaviour.
}

extension ProtocolA {
    var myVarA: Int { 
        get { 1 } 
        set { self = .init() }
    }
}

final class Foo: ProtocolA {
    init() { }
}

In this case mutating myVarA will replace Foo with a new instance. There's nothing wrong with it. ProtocolA simply allows (self-replacing) mutation. If you now add:

extension ProtocolB where Self: ProtocolA {
    var myVarB: Int {
        get { return myVarA }
        set { myVarA = newValue }
    }
}

Then mutating myVarB will also mutate and replace self, which isn't allow since myVarB setter is immutable as ProtocolB is a class-based protocol.

Note that in class-based protocol, setter is immutable. In fact, everything must be immutable in class-based protocol. So you're simply calling a mutable method (myVarA setter) in another immutable method (myVarB setter), which is simply disallowed.

1 Like