Why is AnyObject's bridging so weird in Swift 3?


(Charles Srstka) #1

I’ve been trying to figure out the rationale for why the code below behaves the way it does for some time:

import Foundation

class C: NSObject {
  dynamic var foo: Int { return 5 }
  dynamic func bar() -> Int { return 6 }
}

struct S {}

let c = C()
let s = S()

print(c is AnyObject) // warning: 'is' test is always true
print(s is AnyObject) // warning: 'is' test is always true (?!)

print(c as AnyObject) // <Project.C: 0x0123456789012345>
print(s as AnyObject) // Project.S()

print(c as? AnyObject) // warning: conditional cast from 'C' to 'AnyObject' always succeeds
print(s as? AnyObject) // warning: conditional cast from 'S' to 'AnyObject' always succeeds

print(c as AnyObject? as Any) // Optional(<Project.C: 0x0123456789012345>)
print(s as AnyObject? as Any) // Optional(Project.S())

print((c as AnyObject?)?.foo as Any) // Optional(5)
print((s as AnyObject?)?.foo as Any) // nil
print((c as AnyObject?)?.bar() as Any) // Optional(6)
print((s as AnyObject?)?.bar() as Any) // crash! -[_SwiftValue bar]: unrecognized selector sent to instance 0x5432109876543210

So what we have is:

1. Any type you have will always claim it’s a class type, every time, even if it’s actually a non-bridgeable value type.

2. Conditional casting will also succeed, even if you use it on a non-bridgeable value type.

3. Non-conditional casting works too, despite that the underlying type might be a non-bridgeable value type.

4. Bridging to an optional will *also* always give you a value, even if what’s underneath is a non-bridgeable value type.

5. Trying to call a property on an optional from #4 will, surprisingly, work as you’d expect. The class type that implements the property returns the property, the value type returns nil.

6. Trying to call a method on an optional from #4 will crash.

This raises a few questions:

1. Why in the blazes is it implemented like this? Why not only allow conditional casting to AnyObject, which would only succeed if the type either actually was an object or could actually be bridged to an object? Why make the cast guaranteed, even when in actuality it’s not?

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

3. Is the behavior in #5 guaranteed? i.e., is this safe:

@IBAction func foo(_ sender: Any) {
    let tag = (sender as AnyObject?)?.tag ?? 0 // kinda ugly

    ...
}

or should we all be doing something like this:

@IBAction func foo(_ sender: Any) {
    let tag = type(of: sender) is AnyObject.Type ? (sender as AnyObject?)?.tag ?? 0 : 0 // holy cow, ugly

    …
}

(and yes, I did file a radar asking for a “contains tag” protocol so we wouldn’t have to mess with this stuff here. rdar://29623107 <rdar://29623107>)

Is there a reason it works this way? I’m tempted to write up a proposal to get rid of “as AnyObject” and just have people do an “as? AnyObject” which will work if it can be done and return nil otherwise, but I’m curious as to what the reasoning behind the current behavior is.

Charles


(Joe Groff) #2

I’ve been trying to figure out the rationale for why the code below behaves the way it does for some time:

import Foundation

class C: NSObject {
  dynamic var foo: Int { return 5 }
  dynamic func bar() -> Int { return 6 }
}

struct S {}

let c = C()
let s = S()

print(c is AnyObject) // warning: 'is' test is always true
print(s is AnyObject) // warning: 'is' test is always true (?!)

print(c as AnyObject) // <Project.C: 0x0123456789012345>
print(s as AnyObject) // Project.S()

print(c as? AnyObject) // warning: conditional cast from 'C' to 'AnyObject' always succeeds
print(s as? AnyObject) // warning: conditional cast from 'S' to 'AnyObject' always succeeds

print(c as AnyObject? as Any) // Optional(<Project.C: 0x0123456789012345>)
print(s as AnyObject? as Any) // Optional(Project.S())

print((c as AnyObject?)?.foo as Any) // Optional(5)
print((s as AnyObject?)?.foo as Any) // nil
print((c as AnyObject?)?.bar() as Any) // Optional(6)
print((s as AnyObject?)?.bar() as Any) // crash! -[_SwiftValue bar]: unrecognized selector sent to instance 0x5432109876543210

So what we have is:

1. Any type you have will always claim it’s a class type, every time, even if it’s actually a non-bridgeable value type.

2. Conditional casting will also succeed, even if you use it on a non-bridgeable value type.

3. Non-conditional casting works too, despite that the underlying type might be a non-bridgeable value type.

4. Bridging to an optional will *also* always give you a value, even if what’s underneath is a non-bridgeable value type.

5. Trying to call a property on an optional from #4 will, surprisingly, work as you’d expect. The class type that implements the property returns the property, the value type returns nil.

6. Trying to call a method on an optional from #4 will crash.

This raises a few questions:

1. Why in the blazes is it implemented like this? Why not only allow conditional casting to AnyObject, which would only succeed if the type either actually was an object or could actually be bridged to an object? Why make the cast guaranteed, even when in actuality it’s not?

Everything *can* be bridged to an object as a result of SE-0116 ("id-as-Any"), so there's no longer such a thing as a "non-bridgable value type". #6 is a bug, since the AnyObject method lookup ought to produce `nil` if the ObjC method isn't implemented on the object.

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

What are you trying to do that requires you to know whether something is a class or bridgable to a class (which, as mentioned above, includes everything)?

-Joe

···

On Dec 12, 2016, at 12:12 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:


(Charles Srstka) #3

All right, immediately after sending this, I realized what was wrong with this; bar() should have been bar?(). If the question mark is added, the last line returns nil instead of crashing. The rest of my questions still stand, and we can add to the list why the above doesn’t raise a compiler warning.

Charles

···

On Dec 12, 2016, at 2:12 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

print((c as AnyObject?)?.bar() as Any) // Optional(6)
print((s as AnyObject?)?.bar() as Any) // crash! -[_SwiftValue bar]: unrecognized selector sent to instance 0x5432109876543210


(Charles Srstka) #4

Everything *can* be bridged to an object as a result of SE-0116 ("id-as-Any"), so there's no longer such a thing as a "non-bridgable value type”.

Ah, so that arbitrary Swift types can be stashed in Objective-C userInfo dictionaries and the like. Of course. Still wish this could have just happened at the bridge, as part of whatever magic converts Arrays to NSArrays, Dictionaries to NSDictionaries, etc. Oh well.

#6 is a bug, since the AnyObject method lookup ought to produce `nil` if the ObjC method isn't implemented on the object.

Good to know that it’s a bug. Hopefully it’ll get fixed in a future release (or at least warn if the ? isn’t before the parens).

By the way, while we’re at it, I managed to find a way to get the property version to crash by leaving out some question marks, as well:

print((s as AnyObject).foo as Any) // fatal error: unexpectedly found nil while unwrapping an Optional value

Interestingly, it works in the context of string interpolation. 'print("\((s as AnyObject).foo)”)’ prints nil, just like you’d expect. It’s just when you pass it straight to print that the crash happens. Once again, the compiler doesn’t give us any kind of warning about this.

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

What are you trying to do that requires you to know whether something is a class or bridgable to a class (which, as mentioned above, includes everything)?

The immediate use case? Avoiding the crash on #6. :wink: At least there turned out to be a workaround on that.

Beyond that? Making the whole thing less head-hurtingly complicated, and avoiding random crashing bugs popping up due to the difficulty of completely reasoning about how the whole thing works. But I was a fan of the lamentably departed SE-0083, so grumble, grumble.

Charles

···

On Dec 12, 2016, at 2:46 PM, Joe Groff <jgroff@apple.com> wrote:


(Joe Groff) #5

Yeah, it would be nice if we could revisit SE-0083. I think it's still doable in a way that could localize the dynamic bridging complexity to Anys that were sourced from Objective-C.

-Joe

···

On Dec 12, 2016, at 12:59 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Dec 12, 2016, at 2:46 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

Everything *can* be bridged to an object as a result of SE-0116 ("id-as-Any"), so there's no longer such a thing as a "non-bridgable value type”.

Ah, so that arbitrary Swift types can be stashed in Objective-C userInfo dictionaries and the like. Of course. Still wish this could have just happened at the bridge, as part of whatever magic converts Arrays to NSArrays, Dictionaries to NSDictionaries, etc. Oh well.

#6 is a bug, since the AnyObject method lookup ought to produce `nil` if the ObjC method isn't implemented on the object.

Good to know that it’s a bug. Hopefully it’ll get fixed in a future release (or at least warn if the ? isn’t before the parens).

By the way, while we’re at it, I managed to find a way to get the property version to crash by leaving out some question marks, as well:

print((s as AnyObject).foo as Any) // fatal error: unexpectedly found nil while unwrapping an Optional value

Interestingly, it works in the context of string interpolation. 'print("\((s as AnyObject).foo)”)’ prints nil, just like you’d expect. It’s just when you pass it straight to print that the crash happens. Once again, the compiler doesn’t give us any kind of warning about this.

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

What are you trying to do that requires you to know whether something is a class or bridgable to a class (which, as mentioned above, includes everything)?

The immediate use case? Avoiding the crash on #6. :wink: At least there turned out to be a workaround on that.

Beyond that? Making the whole thing less head-hurtingly complicated, and avoiding random crashing bugs popping up due to the difficulty of completely reasoning about how the whole thing works. But I was a fan of the lamentably departed SE-0083, so grumble, grumble.


(Christopher Kornher) #6

+1 Perhaps adding a way to specify “bridgeability” e.g.

  struct S : AutoBridged {}

···

On Dec 12, 2016, at 2:09 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 12, 2016, at 12:59 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Dec 12, 2016, at 2:46 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

Everything *can* be bridged to an object as a result of SE-0116 ("id-as-Any"), so there's no longer such a thing as a "non-bridgable value type”.

Ah, so that arbitrary Swift types can be stashed in Objective-C userInfo dictionaries and the like. Of course. Still wish this could have just happened at the bridge, as part of whatever magic converts Arrays to NSArrays, Dictionaries to NSDictionaries, etc. Oh well.

#6 is a bug, since the AnyObject method lookup ought to produce `nil` if the ObjC method isn't implemented on the object.

Good to know that it’s a bug. Hopefully it’ll get fixed in a future release (or at least warn if the ? isn’t before the parens).

By the way, while we’re at it, I managed to find a way to get the property version to crash by leaving out some question marks, as well:

print((s as AnyObject).foo as Any) // fatal error: unexpectedly found nil while unwrapping an Optional value

Interestingly, it works in the context of string interpolation. 'print("\((s as AnyObject).foo)”)’ prints nil, just like you’d expect. It’s just when you pass it straight to print that the crash happens. Once again, the compiler doesn’t give us any kind of warning about this.

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

What are you trying to do that requires you to know whether something is a class or bridgable to a class (which, as mentioned above, includes everything)?

The immediate use case? Avoiding the crash on #6. :wink: At least there turned out to be a workaround on that.

Beyond that? Making the whole thing less head-hurtingly complicated, and avoiding random crashing bugs popping up due to the difficulty of completely reasoning about how the whole thing works. But I was a fan of the lamentably departed SE-0083, so grumble, grumble.

Yeah, it would be nice if we could revisit SE-0083. I think it's still doable in a way that could localize the dynamic bridging complexity to Anys that were sourced from Objective-C.

-Joe


(Charles Srstka) #7

You’d get my +1.

Charles

···

On Dec 12, 2016, at 3:09 PM, Joe Groff <jgroff@apple.com> wrote:

On Dec 12, 2016, at 12:59 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Dec 12, 2016, at 2:46 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

Everything *can* be bridged to an object as a result of SE-0116 ("id-as-Any"), so there's no longer such a thing as a "non-bridgable value type”.

Ah, so that arbitrary Swift types can be stashed in Objective-C userInfo dictionaries and the like. Of course. Still wish this could have just happened at the bridge, as part of whatever magic converts Arrays to NSArrays, Dictionaries to NSDictionaries, etc. Oh well.

#6 is a bug, since the AnyObject method lookup ought to produce `nil` if the ObjC method isn't implemented on the object.

Good to know that it’s a bug. Hopefully it’ll get fixed in a future release (or at least warn if the ? isn’t before the parens).

By the way, while we’re at it, I managed to find a way to get the property version to crash by leaving out some question marks, as well:

print((s as AnyObject).foo as Any) // fatal error: unexpectedly found nil while unwrapping an Optional value

Interestingly, it works in the context of string interpolation. 'print("\((s as AnyObject).foo)”)’ prints nil, just like you’d expect. It’s just when you pass it straight to print that the crash happens. Once again, the compiler doesn’t give us any kind of warning about this.

2. Why is there no obvious way to figure out whether something can actually be an object? The already kind of non-obvious “type(of: s) is AnyObject.Type” trick works to tell you if the thing is already a class, but not if something is bridgeable to a class; using it on a string, for example, returns false. And trying something like “type(of: s as AnyObject) is AnyObject.Type” returns true (?!), so that doesn’t work to detect bridgeable things.

What are you trying to do that requires you to know whether something is a class or bridgable to a class (which, as mentioned above, includes everything)?

The immediate use case? Avoiding the crash on #6. :wink: At least there turned out to be a workaround on that.

Beyond that? Making the whole thing less head-hurtingly complicated, and avoiding random crashing bugs popping up due to the difficulty of completely reasoning about how the whole thing works. But I was a fan of the lamentably departed SE-0083, so grumble, grumble.

Yeah, it would be nice if we could revisit SE-0083. I think it's still doable in a way that could localize the dynamic bridging complexity to Anys that were sourced from Objective-C.