0

I am working with a NSMutableSet that contains objects that mutable objects. I am finding inconsistencies on NSMutableSet when trying to remove an object that has been mutated.

In order to isolate the problem I have coded a quick test to illustrate the issue (if any):

Example 1: Working as I expected

NSMutableSet *colors = [[NSMutableSet alloc] init];
NSMutableString *color1 = [[NSMutableString alloc] initWithString:@"Blue"];
NSMutableString *color2 = [[NSMutableString alloc] initWithString:@"Green"];
[colors addObject:color1];
[colors addObject:color2];
NSLog(@"%@",[colors description]); // {( Green, Blue )}
[color1 insertString:@"Cobalt " atIndex:0]; 
NSLog(@"%@",[colors description]); // {( Green, "Cobalt Blue" )}
[colors removeObject:color1];
NSLog(@"%@",[colors description]); {( Green )}

Example 2: NOT working as I expected

NSMutableSet *surnames = [[NSMutableSet alloc] init];
NSMutableString *surname1 = [[NSMutableString alloc] initWithString:@"Brown"];
NSMutableString *surname2 = [[NSMutableString alloc] initWithString:@"Homes"];
[surnames addObject:surname1];
[surnames addObject:surname2];
NSLog(@"%@",[surnames description]); // {( Homes, Brown )}

[surname1 appendString:@"ie"];
NSLog(@"%@",[surnames description]); // {( Homes, Brownie )}
[surnames removeObject:surname1];
NSLog(@"%@",[surnames description]); // {( Homes, Brownie )}

NSString *surnameToRemove = nil;
for (NSString *surname in surnames) {
    if ([surname isEqualToString:@"Brownie"]) {
        surnameToRemove = surname;
        break;
    }
}
[surnames removeObject:surnameToRemove];
NSLog(@"%@",[surnames description]); // {( Homes, Brownie )}

As shown in Example 2, after mutating surname1, removeObject is not removing it, even after taking the reference by searching it. Why this is happening? Can't the mutable containers contain mutable objects?

I have read in this post that NSSet caches the hashes of the contained objects and that might be the issue. If so, is there a way to clean it up? Any alternative solution?

Just for the sake of curiosity, why Example 1 is working?

UPDATE:

From Apple's Collection Programming Topics:

If mutable objects are stored in a set, either the hash method of the objects shouldn’t depend on the internal state of the mutable objects or the mutable objects shouldn’t be modified while they’re in the set. For example, a mutable dictionary can be put in a set, but you must not change it while it is in there. (Note that it can be difficult to know whether or not a given object is in a collection).

UPDATE 2:

Important, Example 2 is returning different log outputs if you running on a Mac or iOS app:

Log in a Mac application (working as I expected):

{( Green, Blue )}
{( Green, "Cobalt Blue" )}
{( Green )}
{( Brown, Homes )}
{( Brownie, Homes )}
{( Homes )}
-[__NSSetM removeObject:]: object cannot be nil

Log in a iOS application (not working as I expected):

{( Green, Blue )}
{( Green, "Cobalt Blue" )}
{( Green )}
{( Homes, Brown )}
{( Homes, Brownie )}
{( Homes, Brownie )}
{( Homes, Brownie )}

UPDATE 3:

Same code than in Example 2 but with NSMutableArray seems to work... so guessing how NSMutableSet is working with hashes. I believe as commented in the linked thread above it's caching them:

NSMutableArray *surnames = [[NSMutableArray alloc] init];
NSMutableString *surname1 = [[NSMutableString alloc] initWithString:@"Brown"];
NSMutableString *surname2 = [[NSMutableString alloc] initWithString:@"Homes"];
[surnames addObject:surname1];
[surnames addObject:surname2];
NSLog(@"%@",[surnames description]);  // {( Homes, Brown )}
[surname1 appendString:@"ie"];
NSLog(@"%@",[surnames description]);  // {( Homes, Brownie )}
[surnames removeObject:surname1];
NSLog(@"%@",[surnames description]);  // {( Homes )}
atxe
  • 5,029
  • 2
  • 36
  • 50
  • Does removeAllObjects() work in Example 2? – Philip Massey Jun 30 '14 at 19:58
  • Yes, it works. There's the workaround of building the set again without `surnameToRemove`. But I do find it an overkill. – atxe Jun 30 '14 at 19:59
  • You can do the same with NSMutableArray, and then, make NSMutableSet with array... – arturdev Jun 30 '14 at 20:01
  • Does NSMutableSet copy the object when adding it? Because Example 2 seems to hint that the object gets unalised. This could explain why you need to find the object with a loop instead of using the previous instance. Can you print out both objects and see if they are actually the same object? – Philip Massey Jun 30 '14 at 20:03
  • @arturdev Indeed, but I need to work with a `NSMutableSet`. This example is used to illustrate the problem i'm facing. Doing that all the time will be too much overhead. – atxe Jun 30 '14 at 20:03
  • @PhilipMassey No, as you can see, when appending a string, the contained string in the set is updated. In the loop to search for the string, if I replace `isEqualToString` by `isEqual`, it actually finds it. – atxe Jun 30 '14 at 20:06
  • Interesting. So I suppose that because the hashing of a string depends explicitly on the text, it breaks? – Philip Massey Jun 30 '14 at 20:08
  • 1
    I can't reproduce your behavior. When I run *Example 2*, the third `NSLog()` shows that `Brownie` was removed and `Homes` is the only item in the set. – Craig Otis Jun 30 '14 at 20:09
  • @PhilipMassey I guess so. – atxe Jun 30 '14 at 20:11
  • @CraigOtis Uhm! Thanks for pointing it out. I realize that this code is logging different outputs in a Mac app and a iOS app... In a Mac app in working properly... – atxe Jun 30 '14 at 20:12
  • Question has been updated. – atxe Jun 30 '14 at 20:18
  • 3
    Understand that a Set is going to follow the same basic scheme as the key value of a Dictionary. The hash and compare functions are used to place the object in a hashtable. If, after it's inserted into the table, you mutate the object in such a way as to alter how it hashes or compares, the object will not be findable. – Hot Licks Jun 30 '14 at 20:50
  • Thank you @HotLicks for explaining *why* objects in (hash-based) sets should not be altered. – Craig Otis Jun 30 '14 at 21:15
  • If you're reasonably careful you can define the hash and compare functions of your object to be independent of "permitted" modifications. – Hot Licks Jun 30 '14 at 21:18
  • @HotLicks Absolutely helpful remarks, thanks. How that would be done? In the code i'm working on I defined `hash` and `isEqual` and the returned values for the hash are exactly the same, but it keeps not removing them. Could it be the case that the hashes are cached in `NSSet` for performance purposes? This would explain the "disclaimer" in Apple's Collection Programming Topics, wouldn't? – atxe Jun 30 '14 at 21:27
  • If the hashes are cached it shouldn't make any difference, since the hash shouldn't change. – Hot Licks Jun 30 '14 at 21:51
  • @HotLicks In the example hashes are changing when mutating the string. Actually if you print them when iterating the set, they are correct. I then do believe that the existence of a hash cache could be the case, that is not refreshed when mutating the contained objects. – atxe Jun 30 '14 at 21:59
  • The guys from objc.io are claiming that there are bugs in NSSet: http://www.objc.io/issue-7/collections.html "Adding objects to a set and then later changing that object will result in weird bugs and will corrupt the state of the set." – atxe Jun 30 '14 at 22:05
  • Understand that when an object is inserted into a hash table the (duh!) value of the object's hash is used to select which table slot it goes into. Sometimes the hashtable will store the hash of the object alongside it's pointer, to make subsequent searches more efficient. This should have no functional effect since **the object's hash value should never change** while it's in the table. If you do change the hash, different algorithms will behave differently, but that's irrelevant since they're operating outside of the specified parameters of the function. – Hot Licks Jul 01 '14 at 17:13
  • Thanks for the explanation. I understand that, in *Example 2*, the hash value is being changed and then the search algorithm for the removal is being affected. But how would you explain then that *Example 1*'s removal is working? When mutating the string with `insertString:` its hash should change as well, shouldn't? Actually, the fact that *Example 2* is working different on iOS and MacOS is also weird, isn't? – atxe Jul 02 '14 at 10:42

0 Answers0