You’re reading Ry’s Objective-C Tutorial → Data Types |
NSSet
NSSet
, NSArray
, and NSDictionary
are
the three core collection classes defined by the Foundation Framework. An
NSSet
object represents a static, unordered collection of distinct
objects. Sets are optimized for membership checking, so if you’re asking
a lot of “is this object part of this group?” kind of questions,
you should be using a set—not an array.
Collections can only interact with Objective-C objects. As a result,
primitive C types like int
need to be wrapped in an NSNumber
before you can store them in a
set, array, or dictionary.
NSSet
is immutable, so you cannot add or remove elements from a
set after it’s been created. You can, however, alter mutable objects that
are contained in the set. For example, if you stored an
NSMutableString
, you’re free to call
setString:
, appendFormat:
, and the other manipulation
methods on that object. This module also covers NSMutableSet
and NSCountedSet
.
Creating Sets
An NSSet
can be created through the
setWithObjects:
class method, which accepts a
nil
-terminated list of objects. Most of the examples in this
module utilize strings, but an NSSet
instance can record
any kind of Objective-C object, and it does not have to be
homogeneous.
NSSet
*
americanMakes
=
[
NSSet
setWithObjects:
@"Chrysler"
,
@"Ford"
,
@"General Motors"
,
nil
];
NSLog
(
@"%@"
,
americanMakes
)
;
NSSet
also includes a setWithArray:
method, which
turns an NSArray
into an NSSet
. Remember that sets
are composed of unique elements, so this serves as a convenient way to
remove all duplicates in an array. For example:
NSArray
*
japaneseMakes
=
@[
@"Honda"
,
@"Mazda"
,
@"Mitsubishi"
,
@"Honda"
];
NSSet
*
uniqueMakes
=
[
NSSet
setWithArray:
japaneseMakes
];
NSLog
(
@"%@"
,
uniqueMakes
)
;
// Honda, Mazda, Mitsubishi
Sets maintain a strong relationship with their elements. That is to say, a set owns each item that it contains. You should be careful to avoid retain cycles when creating sets of custom objects by ensuring that an element in the set never has a strong reference to the set itself.
Enumerating Sets
Fast-enumeration is the preferred method of iterating through the contents
of a set, and the count
method can be used to calculate the total
number of items. For example:
NSSet
*
models
=
[
NSSet
setWithObjects:
@"Civic"
,
@"Accord"
,
@"Odyssey"
,
@"Pilot"
,
@"Fit"
,
nil
];
NSLog
(
@"The set has %li elements"
,
[
models
count
])
;
for
(
id
item
in
models
)
{
NSLog
(
@"%@"
,
item
);
}
If you’re interested in a block-based
solution, you can also use the enumerateObjectsUsingBlock:
method
to process the contents of a set. The method’s only parameter is a
^(id obj, BOOL *stop)
block. obj
is the current
object, and the *stop
pointer lets you prematurely exit the
iteration by setting its value to YES
, as demonstrated below.
[
models
enumerateObjectsUsingBlock:
^
(
id
obj
,
BOOL
*
stop
)
{
NSLog
(
@"Current item: %@"
,
obj
);
if
([
obj
isEqualToString:
@"Fit"
])
{
NSLog
(
@"I was looking for a Honda Fit, and I found it!"
);
*
stop
=
YES
;
// Stop enumerating items
}
}];
The *stop = YES
line tells the set to stop enumerating once it
reaches the @"Fit"
element. This the block equivalent of the
break
statement.
Note that since sets are unordered, it usually doesn’t make sense to
access an element outside of an enumeration. Accordingly, NSSet
does not support subscripting syntax for accessing individual elements
(e.g., models[i]
). This is one of the primary differences between
sets and arrays/dictionaries.
Comparing Sets
In addition to equality, two NSSet
objects can be checked for
subset and intersection status. All three of these comparisons are demonstrated
in the following example.
NSSet
*
japaneseMakes
=
[
NSSet
setWithObjects:
@"Honda"
,
@"Nissan"
,
@"Mitsubishi"
,
@"Toyota"
,
nil
];
NSSet
*
johnsFavoriteMakes
=
[
NSSet
setWithObjects:
@"Honda"
,
nil
];
NSSet
*
marysFavoriteMakes
=
[
NSSet
setWithObjects:
@"Toyota"
,
@"Alfa Romeo"
,
nil
];
if
([
johnsFavoriteMakes
isEqualToSet:
japaneseMakes
])
{
NSLog
(
@"John likes all the Japanese auto makers and no others"
);
}
if
([
johnsFavoriteMakes
intersectsSet:
japaneseMakes
])
{
// You'll see this message
NSLog
(
@"John likes at least one Japanese auto maker"
);
}
if
([
johnsFavoriteMakes
isSubsetOfSet:
japaneseMakes
])
{
// And this one, too
NSLog
(
@"All of the auto makers that John likes are Japanese"
);
}
if
([
marysFavoriteMakes
isSubsetOfSet:
japaneseMakes
])
{
NSLog
(
@"All of the auto makers that Mary likes are Japanese"
);
}
Membership Checking
Like all Foundation Framework collections, it’s possible to check if
an object is in a particular NSSet
. The
containsObject:
method returns a BOOL
indicating the
membership status of the argument. As an alternative, the member:
returns a reference to the object if it’s in the set, otherwise
nil
. This can be convenient depending on how you’re using
the set.
NSSet
*
selectedMakes
=
[
NSSet
setWithObjects:
@"Maserati"
,
@"Porsche"
,
nil
];
// BOOL checking
if
([
selectedMakes
containsObject:
@"Maserati"
])
{
NSLog
(
@"The user seems to like expensive cars"
);
}
// nil checking
NSString
*
result
=
[
selectedMakes
member:
@"Maserati"
];
if
(
result
!=
nil
)
{
NSLog
(
@"%@ is one of the selected makes"
,
result
);
}
Again, this is one of the strong suits of sets, so if you’re doing a
lot of membership checking, you should be using NSSet
instead of
NSArray
(unless you have a compelling reason not to).
Filtering Sets
You can filter the contents of a set using the
objectsPassingTest:
method, which accepts a block that is called
using each item in the set. The block should return YES
if the
current object should be included in the new set, and NO
if it
shouldn’t. The following example finds all items that begin with an
uppercase letter C
.
NSSet
*
toyotaModels
=
[
NSSet
setWithObjects:
@"Corolla"
,
@"Sienna"
,
@"Camry"
,
@"Prius"
,
@"Highlander"
,
@"Sequoia"
,
nil
];
NSSet
*
cModels
=
[
toyotaModels
objectsPassingTest:
^
BOOL
(
id
obj
,
BOOL
*
stop
)
{
if
([
obj
hasPrefix:
@"C"
])
{
return
YES
;
}
else
{
return
NO
;
}
}];
NSLog
(
@"%@"
,
cModels
)
;
// Corolla, Camry
Because NSSet
is immutable, the
objectsPassingTest:
method returns a new set instead of
altering the existing one. This is the same behavior as many of the
NSString
manipulation operations. But, while the set is a
new instance, it still refers to the same elements as the original
set. That is to say, filtered elements are not copied—they are
referenced.
Combining Sets
Sets can be combined using the setByAddingObjectsFromSet:
method. Since sets are unique, duplicates will be ignored if both sets contain
the same object.
NSSet
*
affordableMakes
=
[
NSSet
setWithObjects:
@"Ford"
,
@"Honda"
,
@"Nissan"
,
@"Toyota"
,
nil
];
NSSet
*
fancyMakes
=
[
NSSet
setWithObjects:
@"Ferrari"
,
@"Maserati"
,
@"Porsche"
,
nil
];
NSSet
*
allMakes
=
[
affordableMakes
setByAddingObjectsFromSet:
fancyMakes
];
NSLog
(
@"%@"
,
allMakes
)
;
NSMutableSet
Mutable sets allow you to add or delete objects dynamically, which affords a
whole lot more flexibility than the static NSSet
. In addition to
membership checking, mutable sets are also more efficient at inserting and
removing elements than NSMutableArray
.
NSMutableSet
can be very useful for recording the state of a
system. For example, if you were writing an application to manage an auto
repair shop, you might maintain a mutable set called repairedCars
and add/remove cars to reflect whether or not they have been fixed yet.
Creating Mutable Sets
Mutable sets can be created with the exact same methods as
NSSet
. Or, you can create an empty set with the
setWithCapacity:
class method. The argument defines the initial
amount of space allocated for the set, but it in no way limits the number of
items it can hold.
NSMutableSet
*
brokenCars
=
[
NSMutableSet
setWithObjects:
@"Honda Civic"
,
@"Nissan Versa"
,
nil
];
NSMutableSet
*
repairedCars
=
[
NSMutableSet
setWithCapacity:
5
];
Adding and Removing Objects
The big additions provided by NSMutableSet
are the
addObject:
and removeObject:
methods. Note that
addObject:
won’t actually do anything if the object is
already a member of the collection because sets are composed of unique
items.
NSMutableSet
*
brokenCars
=
[
NSMutableSet
setWithObjects:
@"Honda Civic"
,
@"Nissan Versa"
,
nil
];
NSMutableSet
*
repairedCars
=
[
NSMutableSet
setWithCapacity:
5
];
// "Fix" the Honda Civic
[
brokenCars
removeObject:
@"Honda Civic"
];
[
repairedCars
addObject:
@"Honda Civic"
];
NSLog
(
@"Broken cars: %@"
,
brokenCars
)
;
// Nissan Versa
NSLog
(
@"Repaired cars: %@"
,
repairedCars
)
;
// Honda Civic
Just like mutable strings, NSMutableSet
has a different
workflow than the static NSSet
. Instead of generating a new set
and re-assigning it to the variable, you can operate directly on the existing
set.
You may also find the removeAllObjects
method useful for
completely clearing a set.
Filtering With Predicates
There is no mutable version of the objectsPassingTest:
method,
but you can still filter items with filterUsingPredicate:
.
Predicates are somewhat outside the scope of this tutorial, but suffice it to
say that they are designed to make it easier to define search/filter rules.
Fortunately, the NSPredicate
class can be initialized with a block, so we don’t need to learn an
entirely new format syntax.
The following code snippet is the mutable, predicate-based version of the example from the Filtering Sets section above. Again, this operates directly on the existing set.
NSMutableSet
*
toyotaModels
=
[
NSMutableSet
setWithObjects:
@"Corolla"
,
@"Sienna"
,
@"Camry"
,
@"Prius"
,
@"Highlander"
,
@"Sequoia"
,
nil
];
NSPredicate
*
startsWithC
=
[
NSPredicate
predicateWithBlock:
^
BOOL
(
id
evaluatedObject
,
NSDictionary
*
bindings
)
{
if
([
evaluatedObject
hasPrefix:
@"C"
])
{
return
YES
;
}
else
{
return
NO
;
}
}];
[
toyotaModels
filterUsingPredicate:
startsWithC
];
NSLog
(
@"%@"
,
toyotaModels
)
;
// Corolla, Camry
For more information about predicates, please visit the official Predicate Programming Guide.
Set Theory Operations
NSMutableSet
also provides an API for the basic operations in
set theory. These methods let you take the union, intersection, and relative
complement of two sets. In addition, the setSet:
method is also
useful for creating a shallow copy of a different set. All of these are
included in the following example.
NSSet
*
japaneseMakes
=
[
NSSet
setWithObjects:
@"Honda"
,
@"Nissan"
,
@"Mitsubishi"
,
@"Toyota"
,
nil
];
NSSet
*
johnsFavoriteMakes
=
[
NSSet
setWithObjects:
@"Honda"
,
nil
];
NSSet
*
marysFavoriteMakes
=
[
NSSet
setWithObjects:
@"Toyota"
,
@"Alfa Romeo"
,
nil
];
NSMutableSet
*
result
=
[
NSMutableSet
setWithCapacity:
5
];
// Union
[
result
setSet:
johnsFavoriteMakes
];
[
result
unionSet:
marysFavoriteMakes
];
NSLog
(
@"Either John's or Mary's favorites: %@"
,
result
)
;
// Intersection
[
result
setSet:
johnsFavoriteMakes
];
[
result
intersectSet:
japaneseMakes
];
NSLog
(
@"John's favorite Japanese makes: %@"
,
result
)
;
// Relative Complement
[
result
setSet:
japaneseMakes
];
[
result
minusSet:
johnsFavoriteMakes
];
NSLog
(
@"Japanese makes that are not John's favorites: %@"
,
result
);
Enumeration Considerations
Iterating over a mutable set works the same as a static set, with one very important caveat: you aren’t allowed to change the set while you’re enumerating it. This is a general rule for any collection class.
The following example demonstrates the wrong way to mutate a set in
the middle of a for-in loop. We’ll be using the rather contrived scenario
of removing @"Toyota"
if any element in the set begins with the
letter T
.
// DO NOT DO THIS. EVER.
NSMutableSet
*
makes
=
[
NSMutableSet
setWithObjects:
@"Ford"
,
@"Honda"
,
@"Nissan"
,
@"Toyota"
,
nil
];
for
(
NSString
*
make
in
makes
)
{
NSLog
(
@"%@"
,
make
);
if
([
make
hasPrefix:
@"T"
])
{
// Throws an NSGenericException:
// "Collection was mutated while being enumerated"
[
makes
removeObject:
@"Toyota"
];
}
}
The proper way to do this is shown below. Instead of iterating over the set
directly, you should create a temporary copy of it with the
allObjects
method and iterate over that. This frees you to alter
the original set without any unintended consequences:
NSMutableSet
*
makes
=
[
NSMutableSet
setWithObjects:
@"Ford"
,
@"Honda"
,
@"Nissan"
,
@"Toyota"
,
nil
];
NSArray
*
snapshot
=
[
makes
allObjects
];
for
(
NSString
*
make
in
snapshot
)
{
NSLog
(
@"%@"
,
make
);
if
([
make
hasPrefix:
@"T"
])
{
[
makes
removeObject:
@"Toyota"
];
}
}
NSLog
(
@"%@"
,
makes
);
NSCountedSet
The NSCountedSet
class (also called a “bag”) is
worth a brief mention. It’s a subclass of NSMutableSet
, but
instead of being limited to unique values, it counts the number of
times an object has been added to the collection. This is a very efficient way
to keep object tallies, as it requires only one instance of an object
regardless of how many times it’s been added to the bag.
The main difference between a mutable set and NSCountedSet
is
the countForObject:
method. This will often be used in place of
containsObject:
(which still works as expected). For example:
NSCountedSet
*
inventory
=
[
NSCountedSet
setWithCapacity:
5
];
[
inventory
addObject:
@"Honda Accord"
];
[
inventory
addObject:
@"Honda Accord"
];
[
inventory
addObject:
@"Nissan Altima"
];
NSLog
(
@"There are %li Accords in stock and %li Altima"
,
[
inventory
countForObject:
@"Honda Accord"
],
// 2
[
inventory
countForObject:
@"Nissan Altima"
]);
// 1
Please see the official documentation for more details.
Be sure to check out Ry’s Cocoa Tutorial. This brand new guide is a complete walkthrough of Mac App development, and it leverages all of the Objective-C skills that we just discussed. Learn more › |
Mailing List
Sign up for my low-volume mailing list to find out when new content is released. Next up is a comprehensive Swift tutorial planned for late January.
You’ll only receive emails when new tutorials are released, and your contact information will never be shared with third parties. Click here to unsubscribe.