Bug? Known? Protocol extension with settable computed property


(Garth Snyder) #1

Swift refuses to call a setter defined in a protocol extension. This is in Xcode 10.2 beta 4.

// Freestanding test case

protocol HasTally {}
class ClassWithTally: HasTally {}

extension HasTally {
    var tally: Int {
        get { return 42 }
        set { print("Setting tally") }
    }
}

ClassWithTally().tally = 42  // Error: Cannot assign to property: function call returns immutable value

This is a simplified test case. I first encountered the problem when trying to use Kingfisher 5 with Swift 5 in Xcode 10.2 beta 4. In that context, the failing code was (essentially):

// As seen in the wild

import Kingfisher

extension UIImageView {
    func shouldWork() {
        kf.indicatorType = .activity
    }
}

However, the type and protocol hierarchy behind the live problem is a bit more complex.


(Suyash Srijan) #2

This seems to be an RValueAsLValue error. I don't think this is a bug. You need to separate out the calls:

var instance = ClassWithTally()
instance.tally = 42 // Ok

(Garth Snyder) #4

The separated version you posted works for me. However, this version does not:

protocol HasTally {}
class ClassWithTally: HasTally {}

extension HasTally {
    var tally: Int {
        get { return 42 }
        set { print("Setting tally") }
    }
}

extension ClassWithTally {
    func shouldWork() {
        tally = 42 // Error: cannot assign to property: 'self' is immutable
    }
}

Is this a different error, or is it the same one in a different form?


(Xiaodi Wu) #5

Because set is implicitly mutating in a protocol extension (when not constrained to AnyObject) or value type, this means that you could reassign self in the setter. Therefore, the compiler cannot allow you to call a mutating setter when self is immutable. You can solve this problem by writing nonmutating set.

The reason that you don't need to do this if you define the setter in the class (or a class-constrained protocol extension) is that, inside a class (or a class-constrained protocol extension), the compiler stops you from reassigning self in the setter.

Check out the difference:

protocol HasTally {
    var tally: Int { get set }
    init()
}

final class ClassWithTally: HasTally {
    required init() {}
}

extension HasTally {
    var tally: Int {
        get { return 42 }
        set { self = Self(); print("Setting tally") }
    }
}

extension ClassWithTally {
    func shouldWork() {
        tally = 42 // compiler error is here, not in the setter, because reassigning self is allowed in a protocol extension setter
    }
}
protocol HasTally {
    var tally: Int { get set }
    init()
}

final class ClassWithTally: HasTally {
    required init() {}
}

extension ClassWithTally {
    var tally: Int {
        get { return 42 }
        set { self = ClassWithTally(); print("Setting tally") } // error is here, because reassigning 'self' isn't allowed
    }
}

extension ClassWithTally {
    func shouldWork() {
        tally = 42
    }
}
protocol HasTally: AnyObject {
    var tally: Int { get set }
    init()
}

final class ClassWithTally: HasTally {
    required init() {}
}

extension HasTally {
    var tally: Int {
        get { return 42 }
        set { self = Self(); print("Setting tally") } // error is also here
    }
}

extension ClassWithTally {
    func shouldWork() {
        tally = 42
    }
}

(Suyash Srijan) #6

You need to constrain HasTally to AnyObject or mark the setter as nonmutating.


(Garth Snyder) #7

Ah, I gotcha. Yes, that makes sense. Thanks.


(Adrian Zubarev) #8

I just asked the same question a day ago, it‘s a little mind bending but it makes sense.