32

An attempt to compare two objects of AnyObject type using '==' operator defined in Equatable protocol result in a compile error in Swift. Did anyone find a way to compare such objects, without knowing the real type of objects that can be used for downcasting?

The background for this question is that I have a dictionary Dictionary<String, AnyObject> where values are supposed to be provided though a subscript, then at some point I need to compare the values in the dictionary to make sure that they are unique.

EDIT Here is a snippet demonstrating the issue.

@objc(FooObject)
public class FooObject: NSManagedObject {

    @NSManaged public var properties: Dictionary<String, AnyObject>

    public subscript(property: String) -> AnyObject? {
        get {
            return properties[property]
        }
        set(newValue) {

            for propertyValue in properties.values {
                if propertyValue == newValue { // This line is not compiling: Cannot invoke '==' with AnyObject
                    println("Values in are expected to be unique!")
                    // Throw an exception here ...
                }
            }

            properties[property] = newValue
        }
    }
}

Note that generic like <T:Equatable> declared in the class definition and used as a value type of the dictionary won't solve the issue as it cannot be used in conjunction with NSManagedObject subclass.

Grimxn
  • 22,115
  • 10
  • 72
  • 85
DevGansta
  • 5,564
  • 2
  • 16
  • 16
  • What are the `AnyObject`'s before they're added to the `Dictionary`? The ideal way to solve this is to not use `AnyObject` in the first place, probably by creating a custom `protocol` or maybe an `enum` and using that instead. – Mike S Sep 18 '14 at 04:25
  • The values are of `AnyObject` type since they are provided from outside and are expected to be of any type, just like in Objective-C, where you can put anything as a value, f.e. string, number, data, etc. I was thinking about a protocol but it doesn't solve the issue. What about enum? – DevGansta Sep 18 '14 at 20:49
  • `enum`s with [Associated Values](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Enumerations.html#//apple_ref/doc/uid/TP40014097-CH12-XID_227) can be a good solution. Semi-related: http://stackoverflow.com/a/25863986/3300036 – Mike S Sep 18 '14 at 21:02
  • Mike, I have updated the question with some details. As you can see enum is not exactly what I need, since the variety of types that potentially can be stored as value in the dictionary is really big. Another issue is that the dictionary is used as NSManagedObject attribute. – DevGansta Sep 18 '14 at 21:49

9 Answers9

26

Use the === operator, from the Swift documentation:

Swift also provides two identity operators (=== and !==), which you use to test whether two object references both refer to the same object instance.

SpareTime
  • 331
  • 3
  • 6
10

I don't think this is possible. The problem is that Equatable is defined with a method that takes

func ==(a: T, b: T)

and there's no way, at compile-time, the compiler can find the correct function (as it doesn't know the types ahead of time).

The only way you could do this is if you had some idea of the types you were comparing. Then you could concretely call the appropriate equality function for each type:

func compare(a: AnyObject, b: AnyObject) {    
    if let va = a as? Int, vb = b as? Int            {if va != vb {return false}}
    else if let va = a as? String, vb = b as? String {if va != vb {return false}}
    else if let va = a as? Bool, vb = b as? Bool     {if va != vb {return false}}
            ...
    else {
        // not a type we expected
        return false;
    }
}

It's interesting that C#, for instance, gets around this by defining the IComparable interface as having a method:

int CompareTo(Object obj)

This allows every IComparable object to compare itself with any other object (but the function has to always do its own type-checking here, to make sure obj is of the correct type).

Community
  • 1
  • 1
MattM
  • 919
  • 8
  • 9
3

This should work quite well for most use cases:

func equalAny<BaseType: Equatable>(lhs: Any, rhs: Any, baseType: BaseType.Type) -> Bool {
    guard
        let lhsEquatable = lhs as? BaseType,
        let rhsEquatable = rhs as? BaseType
        else { return false }
    return lhsEquatable == rhsEquatable
}

var a: Any = "a" as Any
var b: Any = "b" as Any

equalAny(lhs: a, rhs: b, baseType: NSObject.self) // false
b = "a"
equalAny(lhs: a, rhs: b, baseType: NSObject.self) // true

a = CGFloat(5)
b = 5

equalAny(lhs: a, rhs: b, baseType: NSObject.self) // true

a = ["hello": "world"]
b = ["hello": "world"]

equalAny(lhs: a, rhs: b, baseType: NSObject.self) // true
Ky -
  • 30,724
  • 51
  • 192
  • 308
  • This works for me in Swift 3, even with `Int`, `String`, etc. It looks like all of these swift types get converted somehow when they are `Any` to be some sort of Foundation equivalent, any clue how this works under the hood? – Alex Jul 18 '17 at 19:17
  • @Alex AFAIK, `Any` is Swift's equivalent of ObjC's `id`, in that it lets the _compiler_ know "Hey this could be anything", but at _runtime_, it still maintains its real type. So the `Int`s are still `Int`s and `String`s are still `String`s, but you don't know that at compile time. – Ky - Jul 19 '17 at 21:02
  • I'm just surprised `Int`, `String` and 'Bool' all cast to `NSObject` - I didn't think this would be the case – Alex Jul 20 '17 at 01:33
  • @Alex `Int`s and `Bool`s are toll-free bridged to `NSNumber`, which is an `NSObject`. Likewise with `Strings`s and `NSString`s. Further reading: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6-ID212 – Ky - Jul 20 '17 at 15:05
3

Swift 3 doesn't work with === for reference compare if the objects are of type Any. The reason is Any may contain integer and float or any other thing that is not in fact an object. So you will have to cast it to NSObject to compare 2 items. Like

if (obj1 as! NSObject) === (obj2 as! NSObject)
{
//logic goes here
}
Saad
  • 8,857
  • 2
  • 41
  • 51
1

Look at the implementation:

/// Returns true if these arrays contain the same elements.
func ==<T : Equatable>(lhs: [T], rhs: [T]) -> Bool

this means that the compiler need to know that the left hand side and the right hand side parameters must be of the same type. So the function you are implementing should look like this:

func compareFunc <T: Equatable>(par1: T, par2: T) {
    ...
    if par1 == par2 {
        ...
    }
    ...
}

Edit:

With your dictionaries should be something like this:

func compareFunc <T: Equatable>(dic1: [String : T], dic2: [String : T]) {
    ...
    if dic1[yourKey] == dic2[yourKey] {
        ...
    }
    ...
}

Edit 2:

An example:

func compareFunc <T: Equatable>(dic1: [String : T], dic2 : [String : T]) -> Bool {

    return dic1["key"] == dic2["key"]
}

compareFunc(["key": "value"], ["key": "value"])
Community
  • 1
  • 1
Alberto Barrera
  • 329
  • 1
  • 4
  • 3
    It doesn't answer the question since the type of the dictionary I'm working with is Dictionary and you cannot pass it to your compareFunc function, it won't compile. – DevGansta Sep 18 '14 at 10:06
1

I'm kind of embarrassed to even suggest this, but I just ran into this too. My scenario was that I was comparing JSON sourced Dictionaries and they in fact lived in 2 different NSManagedObjectContexts so the objects would not be the same, though the values might be.

Anyway what I did was simply to create Strings from whatever the values were and compare those. It's a hack, but in my case was for testing purposes and it works.

set(newValue) {
    for propertyValue in properties.values {
        if String(describing:propertyValue) == String(describing:newValue){ 
            println("Values in are expected to be unique!")
            // Throw an exception here ...
        }
    }
    properties[property] = newValue
}
markand
  • 2,829
  • 1
  • 16
  • 16
  • This is unconventional, but for a lot of uses would work well enough. Obviously how well this works depends on how well your type implements `description`, including type information (if that's important to you), but still, clever hack. – KellyHuberty Nov 11 '19 at 22:48
1

If you want to use this in Objective-C, why not make properties a Dictionary<String, NSObject>? After all, NSObject has isEqual and you can't put primitive types into a NSDictionary anyway.

So it would look like this:

@objc(FooObject)
public class FooObject: NSManagedObject {

    @NSManaged public var properties: Dictionary<String, NSObject>

    public subscript(property: String) -> NSObject? {
        get {
            return properties[property]
        }
        set(newValue) {

            for propertyValue in properties.values {
                if propertyValue == newValue { // This line is not compiling: Cannot invoke '==' with AnyObject
                    print("Values in are expected to be unique!")
                    // Throw an exception here ...
                }
            }

            properties[property] = newValue
        }
    }
}
Ky -
  • 30,724
  • 51
  • 192
  • 308
0

An attempt to compare two objects of AnyObject type using '==' operator defined in Equatable protocol result in a compile error in Swift. Did anyone found a way to compere such objects, without knowing the real type of objects that can be used for down casting?

The problem is, how do you compare two arbitrary objects for content equality? There is no general definition of it. If the objects were instances of NSObject, or at least NSObjectProtocol, then there is isEqual:, but for objects that are not, how can you do it?

newacct
  • 119,665
  • 29
  • 163
  • 224
  • 2
    Exactly, you cannot compare to AnyObjects and Swift book explains why `“If you create your own class or structure to represent a complex data model, for example, then the meaning of “equal to” for that class or structure is not something that Swift can guess for you.”` So I'm looking for ways to accomplish it. One ways is to use generics, but unfortunately they cannot be used along with NSManagedObjects subclasses. Any other thoughts? – DevGansta Sep 19 '14 at 21:29
  • @DevGansta: I still don't get what you are saying. You have not defined what you mean by "it". You keep saying you want to compare but we don't know what you mean by "compare". You cannot implement something unless you define what it is. That is the problem. You have not provided a general definition of equality. – newacct Sep 19 '14 at 21:43
  • 2
    I believe that the question clearly defines the problem. Compere objects means find out it they represent the same data, "key" == "key", 1 == 1, etc.. It is absolutely clear that in swift you can compare only those objects that conform to `Equatable` protocol, but I keep AnyObjects in the dictionary and you cannot do a cast like this: `dictionary["key"] as Equatable`. So I'm trying to find out how to compare objects without knowing their actual type. Within my task I assume that objects that are put to the dictionary conform to `Equatable` protocol. – DevGansta Sep 22 '14 at 21:45
  • 2
    @DevGansta: You cannot have a variable of type `Equatable`, because it contains an operator with `Self` in the type; but this is because even if you could, it would be useless, because you cannot compare two variables just because they are both `Equatable` -- they must be both of the same `Equatable` type. The only way to do this is to introduce a generic type parameter. – newacct Sep 23 '14 at 17:33
  • 2
    @DevGansta: Unfortunately, I don't know how you can do it in this case because you can't make the class generic because then its variables can't be accessed from Objective-C; but you also can't make the subscript generic either. – newacct Sep 23 '14 at 17:45
  • @newacct in 20+ years of programming, Swift is the only language I have encountered where it's not possible to compare two arbitrary variables. It's not an unreasonable task to try and achieve. Obviously if comparing two completely different types then they are not equal, but if they're the same type (or even similar types) you should be able to compare them. And can in almost every language. – Abhi Beckert Jun 14 '18 at 06:57
0

I use these methods when I need to do comparing of values that are not equatable, but their content is. The magic is in the compare closure. Really simple to write when you want to focus on other things than making every class equatable.

/*Pre requsite: Temp and AnotherTemp must be string convertible*/
let a:AnyObject = "1"
let b:AnyObject = Temp("2")
let b:AnyObject = AnotherTemp(3)
let arr:[AnyObject] = [a,b,c]

Utils.has(arr,2,{"\($0)") == $1})//true or false

class Utils{    
    /**
     * EXAMPLE: ["a","b","c"].has("b",{$0 == $1})//true
     */
    static func has<T,V>(_ variables:[T],_ match:V,_ method:(T,V)->Bool) -> Bool  where V:Equatable{
        return first(variables, match, method) != nil
    }
    /**
     * Think of this method as: firstOccurence of something
     * Returns the first item that matches PARAM: match according to the constraints in PARAM: method
     * EXAMPLE: ["a","b","c"].first("b",{$0 == $1})//b
     * EXAMPLE: [("a",0),("b",1)].first("b",{$0.0 == $1}).1//b
     * EXAMPLE: [(id:"a",val:0),(id:"b",val:1)].first("b",{$0.id == $1}).val//b
     * NOTE: This method should have an extension, but making an extension for two generic variables proved difficult, more research needed, for now use the ArrayParser.first method call
     * NOTE: you could do: arr.forEach{/*assert logic goes here*/} but forEach can't return early so you are forced to iterate the entire list
     */
    static func first<T,V>(_ variables:[T],_ match:V,_ method:(T,V)->Bool) -> T?  where V:Equatable{
        for item in variables{
            if(method(item,match)){
                return item
            }
        }
        return nil
    }
}
Sentry.co
  • 5,355
  • 43
  • 38