Skip to content

Instantly share code, notes, and snippets.

@aglee
Created September 30, 2017 17:07
Show Gist options
  • Save aglee/478ff54fc6c45fb852d7d22de237307b to your computer and use it in GitHub Desktop.
Save aglee/478ff54fc6c45fb852d7d22de237307b to your computer and use it in GitHub Desktop.
#import <Foundation/Foundation.h>
// Two things to know about NSSet:
//
// - NSSet assumes its elements will never mutate. It breaks if they do.
// - Unlike NSDictionary, NSSet does not copy its unique objects. It stores
// the original pointers.
//
// Bottom line: DO NOT MUTATE OBJECTS THAT ARE ELEMENTS OF AN NSSET. The
// code below demonstrates the kind of bugs you could run into if you
// disregard this guideline. The output I got is at the bottom of the file.
//
// Ways to avoid these problems:
//
// - Keep set elements simple. For example, use a set of database row ids
// instead of entire database row objects.
// - Copy the values you add, e.g. `[string1 copy]` instead of `string1`.
// This example has the side benefit that `copy` returns an immutable
// string, so you can't accidentally mutate the set element.
// One way to break an NSSet, simply by mutating one element.
void demo1() {
NSMutableString *string1 = [NSMutableString stringWithString:@"abc"];
NSMutableString *string2 = [NSMutableString stringWithString:@"abcdef"];
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
NSLog(@"%x %@", string2, string2);
// Unlike NSDictionary, NSSet does not copy. The pointers listed below
// are the same as above.
NSSet *set = [NSSet setWithObjects:string1, string2, nil];
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
NSLog(@"contains %@? %d", string1, [set containsObject:string1]);
// Simply mutate one element to be "abcxxx". According to the output, the
// set does not contain "abcxxx", even though that value is listed when you
// enumerate the set.
[string1 appendString:@"xxx"];
NSLog(@"MUTATED string1");
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
NSLog(@"%x %@", string2, string2);
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
for (NSString *str in @[@"abc", @"abcxxx"]) {
NSLog(@"contains %@? %d", str, [set containsObject:str]);
}
}
// Variation of demo1() that only has one element in the set. In this case
// the output *does* correctly report after mutation that the set contains
// "abcxxx". This could burn you if you never tested your code with sets
// containing more than one element.
void demo2() {
NSMutableString *string1 = [NSMutableString stringWithString:@"abc"];
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
// Unlike NSDictionary, NSSet does not copy. The pointers listed below
// are the same as above.
NSSet *set = [NSSet setWithObjects:string1, nil];
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
NSLog(@"contains %@? %d", string1, [set containsObject:string1]);
// Mutate one element to be "abcxxx".
[string1 appendString:@"xxx"];
NSLog(@"MUTATED string1");
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
for (NSString *str in @[@"abc", @"abcxxx"]) {
NSLog(@"contains %@? %d", str, [set containsObject:str]);
}
}
// Another way to break an NSSet, by mutating one element to equal another.
void demo3() {
NSMutableString *string1 = [NSMutableString stringWithString:@"abc"];
NSMutableString *string2 = [NSMutableString stringWithString:@"abcdef"];
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
NSLog(@"%x %@", string2, string2);
// Unlike NSDictionary, NSSet does not copy. The pointers listed below
// are the same as above.
NSSet *set = [NSSet setWithObjects:string1, string2, nil];
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
NSLog(@"contains %@? %d", string1, [set containsObject:string1]);
// Mutate string1 to match string2. According to the output, the set's
// count is still 2, and enumerating it lists 2 elements, even though it
// contains only one unique value.
[string1 appendString:@"def"];
NSLog(@"MUTATED string1 to match string2");
NSLog(@"Strings:");
NSLog(@"%x %@", string1, string1);
NSLog(@"%x %@", string2, string2);
NSLog(@"Set:");
for (NSString *str in set) {
NSLog(@"%x %@", str, str);
}
NSLog(@"set count = %d", [set count]);
NSLog(@"contains abc? %d", [set containsObject:@"abc"]);
NSLog(@"contains abcdef? %d", [set containsObject:@"abcdef"]);
}
int main(int argc, char *argv[]) {
@autoreleasepool {
NSLog(@"---- demo1 ----");
demo1();
NSLog(@"\n\n\n");
NSLog(@"---- demo2 ----");
demo2();
NSLog(@"\n\n\n");
NSLog(@"---- demo3 ----");
demo3();
}
}
// The output I got:
//
// ---- demo1 ----
// Strings:
// 67400520 abc
// 674007c0 abcdef
// Set:
// 67400520 abc
// 674007c0 abcdef
// set count = 2
// contains abc? 1
// MUTATED string1
// Strings:
// 67400520 abcxxx
// 674007c0 abcdef
// Set:
// 67400520 abcxxx
// 674007c0 abcdef
// set count = 2
// contains abc? 0
// contains abcxxx? 0
//
//
//
// ---- demo2 ----
// Strings:
// 677042a0 abc
// Set:
// 677042a0 abc
// set count = 1
// contains abc? 1
// MUTATED string1
// Strings:
// 677042a0 abcxxx
// Set:
// 677042a0 abcxxx
// set count = 1
// contains abc? 0
// contains abcxxx? 1
//
//
//
// ---- demo3 ----
// Strings:
// 67706370 abc
// 677063b0 abcdef
// Set:
// 67706370 abc
// 677063b0 abcdef
// set count = 2
// contains abc? 1
// MUTATED string1 to match string2
// Strings:
// 67706370 abcdef
// 677063b0 abcdef
// Set:
// 67706370 abcdef
// 677063b0 abcdef
// set count = 2
// contains abc? 0
// contains abcdef? 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment