Pitch: Deprecate/remove AnyObject method dispatch

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava

6 Likes

The main thing I can think of off the top of my head is getting the tag from the sender in an IBAction:

@IBAction private func someAction(_ sender: Any?) {
    guard let tag = (sender as AnyObject?)?.tag as Int? else { return }

    ...
}

Unfortunately given how many unrelated Cocoa objects there are that implement -tag, it’s not really practical to implement this without the AnyObject dispatch. If a TagContaining protocol could be introduced and all the objects that implement -tag could be made to conform to it, then that would work around the problem (I believe I pitched this at some point long ago, but without catching any interest).

Charles

···

On Oct 24, 2017, at 5:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava:
  How would this change affect this code from Alamofire, used to compare two SecKey instances for public key pinning:

outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
    for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
        if serverPublicKey.isEqual(pinnedPublicKey) {
            serverTrustIsValid = true
            break outerLoop
        }
    }
}

I’m not sure why, but the AnyObject casting makes this work correctly, even in Swift 4. I’ve tried to “modernize” it by using Sets (compare the pinned keys with the available keys Sets) but it doesn’t work (hash implementation for SecKey is weird?). I’m guessing that with the AnyObject case and isEqual, some underlying implementation is changed and equality works properly (or well enough for this use). I’m happy to get rid of this hack, but I’m not sure of any other method to accomplish our goals here.

Jon Shier

···

On Oct 24, 2017, at 6:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

yes, please. Especially having quite a long time (4.1 until 5.0) where this works but issues a warning sounds like a great transition plan.

···

On 24 Oct 2017, at 11:02 pm, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

isEqual is defined on NSObject. You could also try using an array of AnyHashable and calling == instead of isEqual.

Slava

···

On Oct 24, 2017, at 3:09 PM, Jon Shier <jon@jonshier.com> wrote:

Slava:
  How would this change affect this code from Alamofire, used to compare two SecKey instances for public key pinning:

outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
   for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
       if serverPublicKey.isEqual(pinnedPublicKey) {
           serverTrustIsValid = true
           break outerLoop
       }
   }
}

I’m not sure why, but the AnyObject casting makes this work correctly, even in Swift 4. I’ve tried to “modernize” it by using Sets (compare the pinned keys with the available keys Sets) but it doesn’t work (hash implementation for SecKey is weird?). I’m guessing that with the AnyObject case and isEqual, some underlying implementation is changed and equality works properly (or well enough for this use). I’m happy to get rid of this hack, but I’m not sure of any other method to accomplish our goals here.

Jon Shier

On Oct 24, 2017, at 6:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

You can implement an @objc protocol with an optional requirement, and make NSObject conform to it.

Slava

···

On Oct 24, 2017, at 4:05 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Oct 24, 2017, at 5:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

The main thing I can think of off the top of my head is getting the tag from the sender in an IBAction:

@IBAction private func someAction(_ sender: Any?) {
    guard let tag = (sender as AnyObject?)?.tag as Int? else { return }

    ...
}

Unfortunately given how many unrelated Cocoa objects there are that implement -tag, it’s not really practical to implement this without the AnyObject dispatch. If a TagContaining protocol could be introduced and all the objects that implement -tag could be made to conform to it, then that would work around the problem (I believe I pitched this at some point long ago, but without catching any interest).

Charles

Thanks, Slava, that works. Do you have any idea why that works, but sets of the two arrays are always disjoint? The Set method works for SecCertificate but not SecKey.

Jon

···

On Oct 24, 2017, at 6:10 PM, Slava Pestov <spestov@apple.com> wrote:

isEqual is defined on NSObject. You could also try using an array of AnyHashable and calling == instead of isEqual.

Slava

On Oct 24, 2017, at 3:09 PM, Jon Shier <jon@jonshier.com> wrote:

Slava:
  How would this change affect this code from Alamofire, used to compare two SecKey instances for public key pinning:

outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
  for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
      if serverPublicKey.isEqual(pinnedPublicKey) {
          serverTrustIsValid = true
          break outerLoop
      }
  }
}

I’m not sure why, but the AnyObject casting makes this work correctly, even in Swift 4. I’ve tried to “modernize” it by using Sets (compare the pinned keys with the available keys Sets) but it doesn’t work (hash implementation for SecKey is weird?). I’m guessing that with the AnyObject case and isEqual, some underlying implementation is changed and equality works properly (or well enough for this use). I’m happy to get rid of this hack, but I’m not sure of any other method to accomplish our goals here.

Jon Shier

On Oct 24, 2017, at 6:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I guess, but doesn’t it seem far more elegant to have a protocol for tag-containing objects? This is a feature that’s pretty heavily used…

Charles

···

On Oct 24, 2017, at 6:06 PM, Slava Pestov <spestov@apple.com> wrote:

You can implement an @objc protocol with an optional requirement, and make NSObject conform to it.

Slava

On Oct 24, 2017, at 4:05 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Oct 24, 2017, at 5:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

The main thing I can think of off the top of my head is getting the tag from the sender in an IBAction:

@IBAction private func someAction(_ sender: Any?) {
    guard let tag = (sender as AnyObject?)?.tag as Int? else { return }

    ...
}

Unfortunately given how many unrelated Cocoa objects there are that implement -tag, it’s not really practical to implement this without the AnyObject dispatch. If a TagContaining protocol could be introduced and all the objects that implement -tag could be made to conform to it, then that would work around the problem (I believe I pitched this at some point long ago, but without catching any interest).

Charles

It is possible that they implement equality, but not hashing. So two equal values could hash differently. I don’t really know how NSObject implements but stuff, sorry.

Slava

···

On Oct 24, 2017, at 3:26 PM, Jon Shier <jon@jonshier.com> wrote:

  Thanks, Slava, that works. Do you have any idea why that works, but sets of the two arrays are always disjoint? The Set method works for SecCertificate but not SecKey.

Jon

On Oct 24, 2017, at 6:10 PM, Slava Pestov <spestov@apple.com> wrote:

isEqual is defined on NSObject. You could also try using an array of AnyHashable and calling == instead of isEqual.

Slava

On Oct 24, 2017, at 3:09 PM, Jon Shier <jon@jonshier.com> wrote:

Slava:
  How would this change affect this code from Alamofire, used to compare two SecKey instances for public key pinning:

outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
     if serverPublicKey.isEqual(pinnedPublicKey) {
         serverTrustIsValid = true
         break outerLoop
     }
}
}

I’m not sure why, but the AnyObject casting makes this work correctly, even in Swift 4. I’ve tried to “modernize” it by using Sets (compare the pinned keys with the available keys Sets) but it doesn’t work (hash implementation for SecKey is weird?). I’m guessing that with the AnyObject case and isEqual, some underlying implementation is changed and equality works properly (or well enough for this use). I’m happy to get rid of this hack, but I’m not sure of any other method to accomplish our goals here.

Jon Shier

On Oct 24, 2017, at 6:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

Dynamic dispatch of methods through AnyObject is a source of implementation complexity, has many known bugs which we are not going to fix, and no longer makes sense now in an id-as-Any world; AnyObject is not the ‘common currency’ type for arbitrary Objective-C objects anymore.

I would like to propose we deprecate it as follows:

- Unconditional warning in Swift 4.1, with a fixit to add an ‘as’ cast to cast the base value to the right type
- Error in Swift 5 in -swift-version 5 mode

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

Slava
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

That would be a Cocoa-side change, since UIView, NSView, NSMenuItem, NSCell are all Cocoa-level objects. (I’m not aware of any other type that would be commonly passed as a ‘sender’ parameter and would have a tag.) That change wouldn’t go through Swift-Evolution.

So in theory, if Swift 5 decides to get rid of the AnyObject dispatch, the Cocoa teams at Apple could work around that by introducing “@objc protocol IntegerTagged”, and introducing a migrator pass to look for '(sender as AnyObject).tag’ and rewrite it as `(sender as! IntegerTagged)?.tag.

If the ‘tag’ property is the only reason to keep AnyObject dispatch, I don’t think it’s worth its weight in the language.

-BJ

···

On Oct 25, 2017, at 8:28 AM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

I guess, but doesn’t it seem far more elegant to have a protocol for tag-containing objects? This is a feature that’s pretty heavily used…

Charles

On Oct 24, 2017, at 6:06 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

You can implement an @objc protocol with an optional requirement, and make NSObject conform to it.

Slava

On Oct 24, 2017, at 4:05 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Oct 24, 2017, at 5:02 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Thoughts? Does anyone actually rely on this feature, instead of just stumbling on it by accident once in a while?

The main thing I can think of off the top of my head is getting the tag from the sender in an IBAction:

@IBAction private func someAction(_ sender: Any?) {
    guard let tag = (sender as AnyObject?)?.tag as Int? else { return }

    ...
}

Unfortunately given how many unrelated Cocoa objects there are that implement -tag, it’s not really practical to implement this without the AnyObject dispatch. If a TagContaining protocol could be introduced and all the objects that implement -tag could be made to conform to it, then that would work around the problem (I believe I pitched this at some point long ago, but without catching any interest).

Charles

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

That would be a Cocoa-side change, since UIView, NSView, NSMenuItem, NSCell are all Cocoa-level objects. (I’m not aware of any other type that would be commonly passed as a ‘sender’ parameter and would have a tag.) That change wouldn’t go through Swift-Evolution.

So in theory, if Swift 5 decides to get rid of the AnyObject dispatch, the Cocoa teams at Apple could work around that by introducing “@objc protocol IntegerTagged”, and introducing a migrator pass to look for '(sender as AnyObject).tag’ and rewrite it as `(sender as! IntegerTagged)?.tag.

The Swift team and the Cocoa team communicate with each other, though? I ask because there have been some changes to the Cocoa frameworks in the past (see below) that were clearly made in the interest of improving Swift interop, and also because the Swift bug tracker has a field for Radar report IDs.

FWIW, I filed a Radar last year on this (rdar://29623107 <rdar://29623107>)

If the ‘tag’ property is the only reason to keep AnyObject dispatch, I don’t think it’s worth its weight in the language.

Potentially there could be more; what would need to be done would be to dig through the documentation and find all the methods that returned untyped proxy objects. NSTreeNode’s arrangedObjects *was* one of these until very recently; prior to 10.13, it returned an ‘id’ which responded to a couple of methods described in the documentation. They finally changed it in High Sierra to return an NSTreeNode, because using it had been very obnoxious in Swift. My intuition is that there may be more APIs like this in the frameworks that return proxy objects like this, although I can’t remember any specifically off the top of my head right now. Any such methods would need to be fixed, though, because without the AnyObject behavior, these objects would become completely unusable—even the “extension on NSObject” trick isn’t guaranteed to work, since objects typed ‘id’ are not guaranteed to be derived from NSObject (they could, for instance, be NSProxy).

Charles

···

On Oct 25, 2017, at 10:27 AM, BJ Homer <bjhomer@gmail.com> wrote:

It doesn't sound like anyone is heavily relying on AnyObject dispatch. Is there anything else blocking this? Are we just waiting for someone to write up a proposal?

5 Likes