Compilation error in assigning to Obj-C `optional` protocol property

Relaying a question from StackOverflow here, on behalf of a user who doesn't appear to be able to access the forums directly: why does assigning to a mutable @optional Objective-C protocol property result in a Swift compilation error? (Reading from it does not.)

The original SO question has the full example, but it can be simplified down to the following "pure" Swift on Darwin platforms:

import Foundation

@objc protocol Foo {
    @objc /* optional */ var bar: Any { get set }
}

func foo(_ f: Foo) {
    print(f.bar)
    f.bar = 42
}

When Foo.bar is not marked as optional, the above compiles as expected. When Foo.bar is marked as optional, the code no longer compiles:

print(f.bar) // warning: Expression implicitly coerced from 'Any?' to 'Any'
f.bar = 42 // error: Cannot assign to property: 'f' is a 'let' constant

The warning is expected: since the property is optional, the getter may not be present, so f.bar is wrapped in a layer of optionality which returns nil when the property is not implemented. The error, however, is a bit surprising, since f is necessarily an object.

Some iteration:

  • More explicitly constraining Foo to AnyObject (in case this somehow hits another code path) does not change the diagnostic, nor does deriving from NSObjectProtocol

  • Introducing an explicitly-mutable intermediate variable does change the diagnostic, but not the result:

    var g = f
    g.bar = 42 // error: Cannot assign to property: 'g' is immutable
    

Is there a reason for this being the case? My gut feeling is that this is likely a very niche edge case that isn't quite handled, but poking through SE-0070 and associated proposals + threads didn't bring up anything seemingly definitive. (Theoretically, it does seem possible to me to model writing to an optional var the same as calling an optional method, and simply no-op'ing if the setter isn't implemented, but again, this does appear to be pretty niche, and the proposal does pretty clearly call out optional requirements as explicitly for compatibility only.)

If there's anything more explicitly documented somewhere that I've missed, I'd be very curious to know!

(/cc @Douglas_Gregor as the author of SE-0070, in case you remember)


(As an aside, this does compile in pure Objective-C, of course with the caveat that if you don't implement Foo.bar, assignment will trigger an unrecognized selector exception on -setBar:.)

Normally optional requirements add an extra level of optionality:

  • Methods become optional themselves (f.bar?())
  • Property getters wrap the value in an extra level of Optional (if let bar = f.bar)

But there's nowhere to put that extra level of Optional for a property setter. That's really the entire story: we never figured out how to expose optional property setters in a safe way, and didn't want to pick any particular unsafe solution. If someone can think of something that'd be great!

2 Likes

Thanks, Jordan! I figured it could very well be as simple as that; appreciate the history.

(Theoretically, we could either mimic the method spelling with f.bar? = 42, or require explicitly calling a setter method like f.setBar?(42), but that's neither here nor there.)

If you find yourself needing this, it appears that at the moment, the workaround would be to either:

  1. Explicitly add an optional method to the protocol which performs the assignment so you can call it directly
    • Sadly, there's no straightforward way to give that setter a default implementation, as Objective-C doesn't support default implementations, and the property cannot be set directly from Swift
    • The method also cannot be named the same as the property (e.g. defining both @property (...) id bar and -setBar:) since the method is imported as part of the property into Swift (even if the property is explicitly readonly)
  2. Introduce a protocol hierarchy like you would in Swift, where Foo inherits from FooBase and offers bar as a non-optional property

I think the original recommended workaround was "write a static inline function in Objective-C to do it for you", but that's not wonderful either. setValue(_:forKey:) can also be good enough in practice if it's not in a hot path.

1 Like

Ah yeah! Those would also definitely work. Thanks, Jordan.