Category Archives: Objective-C

Swift Set vs NSMutableSet

As of Swift 1.2 there is a native Set<> type in the standard library, see Swift 1.2 and Xcode 6.3 beta.

New native Set data structure — An unordered collection of unique elements that bridges with NSSet and provides value semantics like Array and Dictionary.

It sounded great. Another thing added to the done list.

Beware though, Swift’s Set and Objective-C’s NSSet and NSMutableSet have fundamental differences.

Just to not keep you waiting for the punchline. Set’s insert is actually an upsert (insert or update) operation. Other methods, like union and unionInPlace are also updating the existing elements if they were already in the set.

Different function names are not an issue, e.g. NSMutableSet has -addObject:, Set has insert(_:). So far so good.

Let’s check out the documentation.

NSMutableSet
- addObject:
Adds a given object to the set, if it is not already a member.

Set
insert(_:)
Insert a member into the set.

Is there even a difference? It’s not apparent at first, but there is a huge discrepancy. Let’s check it out with a small code.


class Foo: Hashable, CustomStringConvertible {
  let value: Int
  //Additional Information
  let addInf: Int
  init(value: Int, addInf: Int) {
    self.value = value
    self.addInf = addInf
  }
  var hashValue: Int {
    return value.hashValue
  }
  var description: String {
    return "{Foo, v:\(value), a:\(addInf)}"
  }
}

func ==(lhs: Foo, rhs: Foo) -> Bool {
  return lhs.value == rhs.value
}

I use Swift 2 here. You can copy the above code into a Playground.

To finish the experiment run the remaining code.


var s = Set()
// []
s.insert(Foo(value: 1, addInf: 100))
// {{value 1, addInf 100}} 
print(s)
// "[{Foo, v:1, a:100}]\n"
s.contains(Foo(value: 1, addInf: 100))
// true
s.contains(Foo(value: 1, addInf: 200))
// true
// Everything is okay so far.
s.insert(Foo(value: 1, addInf: 200))
// {{value 1, addInf 200}}
print(s)
// "[{Foo, v:1, a:200}]\n"
// Should have printed "[{Foo, v:1, a:100}]\n"!

These results shed light on the meaning of the short documentation.

Insert a member into the set.

It always inserts the element, even if it means it overwrites the existing element in the Set. It's not an insert it's an upsert (insert or update).

It's hard to pinpoint why this is a problem. Maybe it's not, I just got used to other kinds of APIs, like the C++ STL.
One fact is that it ruins a type of algorithm which I like to use. I now have to check for containment before every insert, which ruins the readability of the code. So I definitely got burned by this. (A simple solution to the latter is adding a checking insert function to the Set with an extension.)

I still got the feeling that this new API is wrong. I opened a bug report 24101520 about it. We will see whether this is a bug or a feature.

Just for the sake of completeness I included the NSMutableSet version too below.


@objc class Foo2: NSObject {
  let value: Int
  let addInf: Int
  init(value: Int, addInf: Int) {
    self.value = value
    self.addInf = addInf
  }
  override var hashValue: Int {
    return value.hashValue
  }
  override var hash: Int {
    return value.hashValue
  }
  override func isEqual(object: AnyObject?) -> Bool {
    if let object = object as? Foo2 {
      return value == object.value
    } else {
      return false
    }
  }
  override var description: String {
    return "{Foo, v:\(value), a:\(addInf)}"
  }
}

func ==(lhs: Foo2, rhs: Foo2) -> Bool {
    return lhs.value == rhs.value
}

And the evaluation.


var r = NSMutableSet()
// {()}
r.addObject(Foo2(value: 1, addInf: 100))
// {{NSObject, value 1, addInf 100}}
print(r)
// "{(\n    {Foo, v:1, a:100}\n)}\n"
r.containsObject(Foo2(value: 1, addInf: 100))
// true
r.containsObject(Foo2(value: 1, addInf: 200))
// true
r.addObject(Foo2(value: 1, addInf: 200))
// {{NSObject, value 1, addInf 100}}
print(r)
// "{(\n    {Foo, v:1, a:100}\n)}\n"
// Prints what one expects. No update here.