2

I want to find out if an exact instance of an object is in an Array. This seemed like a pretty useful function to have, so I tried to make an extension of Array:

extension Array {
  func containsIdenticalObject(object: AnyObject)->Bool {
    if self.count > 0 {
      for (_, objectToCompare) in self.enumerate() {
        if object === objectToCompare {
          return true
        }
      }
    }
    return false
  }
}

I get the message:

error: binary operator '===' cannot be applied to operands of type 'AnyObject' and 'Element'. 

I have tried jiggering it around with various generic modifications, such as <T: This> and where Self: That, but I always get the same message.

This seems like it should definitely be possible. How do I need to modify this function to make it work?

Edit

I have been able to make this work as a stand-alone function:

func arrayContainsExactInstance<T>(array:[T], _ object:T)->Bool {
  if array.count > 0 {
    for (_, givenObject) in array.enumerate() {
        let givenObjectAsAnyObject = givenObject as! AnyObject
        let targetObjectAsAnyObject = object as! AnyObject
        if ObjectIdentifier(givenObjectAsAnyObject) == ObjectIdentifier(targetObjectAsAnyObject) {
          return true
      }
    }
  }
  return false
}

...which is great, except a) it seems over-complicated, and b) there must be some way to add it to an actual extension...

Le Mot Juiced
  • 3,761
  • 1
  • 26
  • 46

1 Answers1

3

The "identical operator" === is defined for instances of classes only, therefore you have to restrict the extension to arrays where the elements are instances of a class as well.

AnyObject is the protocol to which all classes implicitly conform, so you can add a where Element : AnyObject restriction to the extension declaration.

Note that the method itself can be simplified.

extension Array where Element : AnyObject {
    func containsIdenticalObject(object: AnyObject) -> Bool {
        return contains { $0 === object }
    }
}

Example:

class MyClass { }

let a = MyClass()
let b = MyClass()
let c = MyClass()
let array = [a, b]

array.containsIdenticalObject(c) // false
array.containsIdenticalObject(b) // true

Update: Unfortunately – as you observed – this does not work if the array element type is a protocol:

protocol MyProtocol: AnyObject {} 
class MyClass: MyProtocol {} 
let classInstance = MyClass() 
let classArray: [MyProtocol] = [classInstance] 
classArray.containsIdenticalObject(classInstance) 
// using 'MyProtocol' as a concrete type conforming to protocol 'AnyObject' is not supported

The reason is that a protocol does not conform to itself or to a protocol that it inherits from (compare Protocol doesn't conform to itself?). The same problem would occur with your free function if you restrict the type T to class types instead of forcefully casting the elements to AnyObject:

func arrayContainsExactInstance<T : AnyObject>(array:[T], _ object:T) -> Bool

I don't know how to solve that with the current Swift 2.1, but this problem is unrelated to whether you use an extension method or a free function.

If you are willing to assume that the elements are instances of a class then you can write the extension method as

extension Array {
    func containsIdenticalObject(object: Element) -> Bool {
        guard let obj = object as? AnyObject else { return false }
        return contains { ($0 as? AnyObject) === obj }
    }
}

which is roughly what your function does (only with a conditional cast instead of a forced cast). But note that this might give unexpected results for non-class elements because some types (e.g. Int are silently converted to objects (e.g. NSNumber) where necessary.

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • have you tested this in a Playground? It doesn't throw an error itself, but when I try to use it with an array of custom class `MyClass`, for example, i get the message: `error: using 'MyClass' as a concrete type conforming to protocol 'AnyObject' is not supported` – Le Mot Juiced Oct 28 '15 at 20:31
  • @LeMotJuiced: Cannot reproduce, it works both in a compiled project and in a Playground for me. I have added my test code to the answer for your convenience. – Which Xcode version are you using? – Martin R Oct 28 '15 at 20:36
  • Try this, in a Playground with your extension as written: `protocol MyProtocol: class, AnyObject {} //newline class MyClass: MyProtocol {} //newline let classInstance = MyClass() //newline let classArray: [MyProtocol] = [] //newline classArray.containsIdenticalObject(classInstance) //newline ` (sorry about the spacing, I don't know how to do that properly in Markdown in the comments). – Le Mot Juiced Oct 28 '15 at 20:39
  • @LeMotJuiced: There are some problems/restrictions if the element type of an array is a protocol and not a concrete type, compare e.g. http://stackoverflow.com/questions/33112559/protocol-doesnt-conform-to-itself. – Martin R Oct 28 '15 at 20:48
  • so my function in the edit is the only way to do it? – Le Mot Juiced Oct 29 '15 at 16:58
  • @LeMotJuiced: Your function *assumes* that all elements are objects (because of the forced cast to `AnyObject`). With `func arrayContainsExactInstance(...)` you would have the same problem. You could to the same in an array extension. The problems (in both cases!) are: 1) It can crash at runtime, 2) Some types (like Int) are silently converted to AnyObject. – Martin R Oct 29 '15 at 18:02
  • FWIW: ( `//` indicating newlines): `extension Array { // func containsInstance(object: T)->Bool { // return contains { // let elementAsObj = $0 as! AnyObject // let objectAsObj = object as! AnyObject // return ObjectIdentifier(elementAsObj) == ObjectIdentifier(objectAsObj) // } // } // } //` ... and you're right, this doesn't protect itself from being improperly used, leading to crashes. The funny thing is that crashes could happen because it can't guarantee being used on an object, but if you make it guarantee it's used on an object, it won't work. Shrug. – Le Mot Juiced Oct 29 '15 at 18:18
  • @LeMotJuiced: This seems to be a restriction of current Swift, report a bug at Apple! – I have updated the answer and tried to summarize the possible options. – Martin R Oct 29 '15 at 18:26
  • This works and is safe, I think: `extension Array { // func containsInstance(object: T)->Bool { return contains { // let elementAsObj = $0 as? AnyObject // let objectAsObj = object as? AnyObject // if elementAsObj == nil || objectAsObj == nil { // return false // } else { // return ObjectIdentifier(elementAsObj!) == ObjectIdentifier(objectAsObj!) // } // } // } // } //` – Le Mot Juiced Oct 29 '15 at 18:27
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/93718/discussion-between-martin-r-and-le-mot-juiced). – Martin R Oct 29 '15 at 18:29