In my class i overwrite the isEqual
#interface MyClass : NSObject
#property (nonatomic, strong) NSString * customID;
#end
I overwrite the isEqual so it checks only the equality of customID
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[MyClass class]]) {
if (self.customID == nil) {
return NO;
}
return [self.customID isEqual:[object customID]];
}
return [super isEqual:object];
}
Now the NSSet is practically a hash table, making it fast to check, if it contains a hash value... thats something we know
but, let imagine this code
NSArray * instancesToCheck = ...;
NSArray * allInstances = ...;
for (MyClass * instance in allInstances) {
if ([instancesToCheck containsObject:instance]) {
// do smth
}
}
i would like to "optimize" with this one (use a NSSet for membership testing)
NSArray * instancesToCheck = ...;
NSArray * allInstances = ...;
NSSet * instancesToCheckAsSet = [NSSet setWithArray:instancesToCheck];
for (MyClass * instance in allInstances) {
if ([instancesToCheckAsSet containsObject:instance]) {
// do smth
}
}
Does the second code provide any performance benefit at all (under the assumption, that there were no duplicates in the array from which it was created, and the instancesToCheck contains different pointers, but some of the objects have the same customID, making isEqual==YES but pointer comparison==NO)?
When i looked up the docs, i found out, that the containsObject calls the isEqual, so it has to iterate over all objects anyway
What are the performance implications when using NSSet with objects, that overwrite isEqual? Becomes the NSSet less effective then?
Does the second code provide any performance benefit at all
Absolutely. An array must cycle through the array examining every object. A set knows more or less instantly whether an object is contained, because it is a hash table. Indeed, this sort of thing is exactly what a set is for.
You MUST overwrite hash, if you overwrite isEqual: doing otherwise might break the functionality and things might not behave as expected
Two objects that are considered "equal" must return the same hash value.
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[MyClass class]]) {
if (self.customID == nil) {
return NO;
}
return [self.customID isEqual:[object customID]];
}
return [super isEqual:object];
}
// MUST overwrite hash
- (NSUInteger)hash {
return [self.customID hash];
}
Related
It is clear from this question that there are many ways to remove duplicates from an NSArray when the array's elements are primitive types, or when the elements are perfect duplicates. But, is there a way to remove duplicates based on a transformation applied to each element, as is permitted in Underscore.js's uniq function, rather than by simply comparing the whole elements? And if a manual implementation would be difficult to optimize, is there an efficient system-provided method (or 3rd party library algorithm) for accomplishing this that I am missing?
A simple approach:
NSMutableArray* someArray = something;
for (int i = someArray.count - 1; i > 0; i--) {
MyObject* myObject = someArray[i];
for (int j = 0; j < i; j++) {
MyObject* myOtherObject = someArray[j];
if ([myObject isSortaEqual:myOtherObject]) {
[someArray removeObjectAtIndex:i];
break;
}
}
}
Yes, it's N-squared, but that's not a biggie unless the array is fairly large.
If you want to redefine what equality means for your objects, then consider overriding -hash and -isEqual:. Then you can create an NSSet from your array if order is irrelevant, or an NSOrderedSet if it is relevant. Here's an example of a Person class where I want the name of the person to determine object equality.
#interface Person
#property (nonatomic, copy) NSString *name;
#end
#implementation Person
- (BOOL)isEqual:(id)object
{
Person *otherPerson = (Person *)object;
return [self.name isEqualToString:otherPerson.name];
}
- (NSUInteger)hash
{
return [self.name hash];
}
#end
Uniquing them now is rather easy:
NSArray *people = ...;
// If ordered is irrelevant, use an NSSet
NSSet *uniquePeople = [NSSet setWithArray:people];
// Otherwise use an NSOrderedSet
NSOrderedSet *uniquePeople = [NSOrderedSet orderedSetWithArray:people];
Absolutely. You are looking for a way to pass your own method for testing for uniqueness (at least, that's what the uniq function you refer to does).
indexesOfObjectsPassingTest: will allow you to pass your own block to determine uniqueness. The result will be an NSIndexSet of all the objects in the array that matched your test. With that you can derive a new array. The block you are passing is roughly equivalent to the Underscore iterator passed to uniq.
The sister method, indexesOfObjectsWithOptions:passingTest: also allows you to specify enumeration options (i.e. concurrent, reverse order, etc.).
As you mention in your question, there are lots of ways to accomplish this. NSExpressions with blocks, Key-value coding collections operators, etc. could be used for this as well. indexesOfObjectsPassingTest: is probably the closest to what you seem to be looking for, though you can do much the same thing (with a lot more typing) using expressions.
I just came up against this problem, so I wrote a category on NSArray:
#interface NSArray (RemovingDuplicates)
- (NSArray *)arrayByRemovingDuplicatesAccordingToKey:(id (^)(id obj))keyBlock;
#end
#implementation NSArray (RemovingDuplicates)
- (NSArray *)arrayByRemovingDuplicatesAccordingToKey:(id (^)(id obj))keyBlock
{
NSMutableDictionary *temp = [NSMutableDictionary dictionaryWithCapacity:[self count]];
for (NSString *item in self) {
temp[keyBlock(item)] = item;
}
return [temp allValues];
}
#end
You can use it like this (this example removes duplicate words, ignoring case):
NSArray *someArray = #[ #"dave", #"Dave", #"Bob", #"shona", #"bob", #"dave", #"jim" ];
NSLog(#"result: %#", [someArray arrayByRemovingDuplicatesAccordingToKey:^(id obj){
return [obj lowercaseString];
}]);
Output:
2015-02-17 17:44:10.268 Untitled[4043:7711273] result: (
dave,
shona,
jim,
bob
)
The 'key' is a block that returns an identifier used to compare the objects. So if you wanted to remove Person objects according to their name, you'd pass ^(id obj){ return [obj name]; }.
This solution is O(n), so is suitable to large arrays, but doesn't preserve order.
I'm working on a Form which consists of dynamically created Fields, and I'd like each Field to have a method that returns a RACSignal which sends a nextEvent each time the Field's value changes.
The payload of the nextEvent should include BOOL that indicates weather the Field is complete (which wraps up some validation logic along the way).
The best I can illustrate my intent so far is with the pseudo-code below:
// Inside Form.m
// Create an array of signals from all the fields in the form
#property(nonatomic) BOOL *formComplete;
-(void)setUpSignals {
NSMutableArray *fieldCompleteSignals = [NSMutableArray arrayWithCapacity:0];
for (Field *field in self.fields)
{
RACSignal *completeSignal = [field complete];
[fieldCompleteSignals addObject:completeSignal];
}
// ? S.O. Help here
[RACSignal combineLatest:self.fieldCompleteSignals reduce:^id {
self.formComplete = ?? // combined BOOL values, if any are NO, then returns NO- if all are YES, returns YES
return ??; // when all the BOOL values returned from the fields are YES, then the form is complete
}];
}
// Inside Field.m
#property(nonatomic, copy) NSString *value;
-(RACSignal *)fieldComplete
{
return [RACObserve(self, value) map:^id(id value) {
return #([self validateCurrentValue]);
}];
}
-(BOOL)validateCurrentValue
{
// snazzy validation logic here
return YES | NO;
}
This Form is dynamically created from an XML document, and I'd like to set up a pipeline that handles dynamically created RACSignals.
What I'd like to end up with is a stream of BOOL values, collected from each Field, and reduced to a single formIsComplete BOOL- I'm wondering if I'm on the right track to setting up this pipeline if there's a better way (or other examples) of handling this?
Fo your first question, you can use map along with RACTuple:
#property (assign, nonatomic) BOOL formComplete;
- (void)setUpSignals {
NSMutableArray *fieldCompleteSignals =
[NSMutableArray arrayWithCapacity:self.fields.count];
for (Field *field in self.fields) {
RACSignal *completeSignal = [field complete];
[fieldCompleteSignals addObject:completeSignal];
}
RAC(self, formComplete) =
[[RACSignal
combineLatest:self.fieldCompleteSignals]
map:^id (RACTuple *allValues) {
return #([allValues.rac_sequence all:^BOOL(id value) {
return [value boolValue];
}]);
}];
}
RACTuple is an ordered collection and conforms to NSFastEnumeration protocol (To learn more about RACTuple see RACTuple reference).
For your second question, yes boxing primitive types is appropriate. Say that you would like to enable some button only when some other BOOL is YES, you can do it like this:
RAC(self.button, enabled) = RACObserve(self, formComplete);
How does one create a NSSet of objects from an array based on a property.
e.g. Array of objects, each with a strong reference to a type property, and multiple occurrences of each type exist in the array. How can this be turned into an NSSet holding a single object of each type.
NSSet *distinctSet = [NSSet setWithArray:[array valueForKeyPath:#"#distinctUnionOfObjects.property"]];
A dictionary essentially has this functionality already. Its keys are a set, so you can create the dictionary to hold the objects, keyed by whatever attribute you're interested in:
[NSDictionary dictionaryWithObjects:arrayOfObjects
forKeys:[arrayOfObjects valueForKey:theAttribute]];
If you ask the dictionary for allValues now, you have only one object for each attribute. I should mention that with this procedure, the later objects will be kept in favor of earlier. If the order of your original array is significant, reverse it before creating the dictionary.
You can't actually put those objects into an NSSet, because the NSSet will use the objects' isEqual: and hash methods to determine whether they should be members, rather than the key attribute (of course, you can override these methods if this is your own class, but that would likely interfere with their behavior in other collections).
If you really really feel that you need a set, you will have to write your own class. You can subclass NSSet, but conventional wisdom is that composition of Cocoa collections is far easier than subclassing. Essentially, you write a class which covers any set methods you're interested in. Here's a (quite incomplete and totally untested) sketch:
#interface KeyedMutableSet : NSObject
/* This selector is performed on any object which is added to the set.
* If the result already exists, then the object is not added.
*/
#property (assign, nonatomic) SEL keySEL;
- (id)initWithKeySEL:(SEL)keySEL;
- (id)initWithArray:(NSArray *)initArray usingKeySEL:(SEL)keySEL;
- (void)addObject:(id)obj;
- (NSArray *)allObjects;
- (NSArray *)allKeys;
- (BOOL)containsObject:(id)obj;
- (NSUInteger)count;
-(void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block;
// And so on...
#end
#import "KeyedMutableSet.h"
#implementation KeyedMutableSet
{
NSMutableArray * _objects;
NSMutableSet * _keys;
}
- (id)initWithKeySEL:(SEL)keySEL
{
return [self initWithArray:nil usingKeySEL:keySEL];
}
- (id)initWithArray:(NSArray *)initArray usingKeySEL:(SEL)keySEL
{
self = [super init];
if( !self ) return nil;
_keySEL = keySEL;
_objects = [NSMutableArray array];
_keys = [NSMutableSet set];
for( id obj in initArray ){
[self addObject:obj];
}
return self;
}
- (void)addObject:(id)obj
{
id objKey = [obj performSelector:[self keySEL]];
if( ![keys containsObject:objKey] ){
[_keys addObject:objKey];
[_objects addObject:obj];
}
}
- (NSArray *)allObjects
{
return _objects;
}
- (NSArray *)allKeys
{
return [_keys allObjects];
}
- (BOOL)containsObject:(id)obj
{
return [_keys containsObject:[obj performSelector:[self keySEL]]];
}
- (NSUInteger)count
{
return [_objects count];
}
- (NSString *)description
{
return [_objects description];
}
-(void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block
{
for( id obj in _objects ){
BOOL stop = NO;
block(obj, &stop);
if( stop ) break;
}
}
#end
NSMutableSet* classes = [[NSMutableSet alloc] init];
NSMutableSet* actualSet = [[NSMutableSet alloc] init];
for(id object in array) {
if([classes containsObject:[object class]] == NO) {
[classes addObject:[object class]];
[actualSet addObject:object];
}
}
You would use:
NSSet* mySetWithUniqueItems= [NSSet setWithArray: yourArray];
This should work regardless of the type of objects in your array and would populate the NSSet with only one occurence of any duplicate objects in your array.
I hope this helps.
Update:
Next best thing: is use concatenation of class name and object property first then use the above method.
self.concatenatedArray=[NSMutableArray arrayWithCapacity:4];
for (TheClass* object in self.myArray)
[self.concatenatedArray addObject:[NSString stringWithFormat:#"%#-%#",[object class], object.theProperty]];
self.mySet=[NSSet setWithArray:self.concatenatedArray];
I am not sure what you will use the NSSet output for but you can probably modify the concatenation elements to have the information you need in the NSSet output.
I have created a simple library, called Linq to ObjectiveC, which is a collection of methods that makes this kind of problem much easier to solve. In your case you need the Linq-to-ObjectiveC distinct method:
NSSet* dictionary = [NSSet setWithArray:[sourceArray distinct:^id(id item) {
return [item type] ;
}]];
This returns a set where each item has a distinct type property.
For example the Object is something like this:
MyUser: NSObject{
NSString *firstName;
NSString *lastName;
NSString *gender;
int age;
}
and I would like to compare to user, if their attributes are the same, I will treat it as equal... instead of write a static method to compare enough attribute one by one, can I have a lazy way to get all the attribute to compare themselves, Thanks.?
For comparison, this is what you're trying to avoid writing.
-(NSUInteger)hash {
return [firstName hash] ^ [lastName hash] ^ [gender hash] ^ age;
}
-(BOOL)isEqual:(id)other {
return [other isKindOfClass:[self class]]
&& age == other.age
&& [gender isEqualToString:other.gender]
&& [firstName isEqualToString:other.firstName]
&& [lastName isEqualToString:other.lastName];
}
Using XOR is an extremely simple way of combining hashes, and I mostly include it as a stand-in. It may hurt the quality of the hash value, depending on distribution of the underlying hash functions. If the hashes have a uniform distribution, it should be all right. Note also that combining hashes only works because NSStrings that are equal in content have the same hashes. This approach won't work with all types; in particular, it won't work with types that use the default implementation of hash.
To get around writing the above, first change the type of the age property to NSNumber, so it doesn't have to be handled as a special case. You don't have to change the ivar, though you can if you want.
#interface MyUser : NSObject {
...
unsigned int age; // Or just make this an NSNumber*
}
...
#property (assign,nonatomic) NSNumber *age;
#implementation MyUser
#synthesize firstName, lastName, gender;
/* if the age ivar is an NSNumber*, the age property can be synthesized
instead of explicitly defining accessors.
*/
#dynamic age;
-(NSNumber*)age {
return [NSNumber numberWithUnsignedInt:age];
}
-(void)setAge:(NSNumber*)newAge {
age = [newAge unsignedIntValue];
}
Second, make sure your class supports the fast enumeration protocol. If it doesn't, you can implement -countByEnumeratingWithState:objects:count: by making use of reflection (with the Objective-C runtime functions) to get the list of properties for instances of your class. For example (taken in part from "Implementing countByEnumeratingWithState:objects:count:" on Cocoa With Love):
#import <objc/runtime.h>
...
#interface MyUser (NSFastEnumeration) <NSFastEnumeration>
-(NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len;
#end
#implementation MyUser
#synthesize firstName, lastName, gender;
/* defined in the main implementation rather than a category, since there
can be only one +[MyUser initialize].
*/
static NSString **propertyNames=0;
static unsigned int cProperties=0;
+(void)initialize {
unsigned int i;
const char *propertyName;
objc_property_t *properties = class_copyPropertyList([self class], &cProperties);
if ((propertyNames = malloc(cProperties * sizeof(*propertyNames)))) {
for (i=0; i < cProperties; ++i) {
propertyName = property_getName(properties[i]);
propertyNames[i] = [[NSString alloc]
initWithCString:propertyName
encoding:NSASCIIStringEncoding];
}
} else {
cProperties = 0;
// Can't initialize property names. Fast enumeration won't work. What do?
}
}
...
#end
#implementation MyUser (NSFastEnumeration)
-(NSUInteger)
countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id *)stackbuf
count:(NSUInteger)len
{
if (state->state >= cProperties) {
return 0;
}
state->itemsPtr = propertyNames;
state->state = cProperties;
state->mutationsPtr = (unsigned long *)self;
return cProperties;
}
#end
Last, implement hash (using fast enumeration) and isEqual:. Hash should calculate the hashes of all properties, then combine them to create the hash for the MyUser instance. isEqual: can simply check the other object is an instance of MyUser (or a subclass thereof) and compare hashes. For example:
-(NSUInteger)hash {
NSUInteger myHash=0;
for (NSString *property in self) {
// Note: extremely simple way of combining hashes. Will likely lead
// to bugs
myHash ^= [[self valueForKey:property] hash];
}
return myHash;
}
-(BOOL)isEqual:(id)other {
return [other isKindOfClass:[self class]]
&& [self hash] == [other hash];
}
Now, ask yourself which is less work overall. If you want a single approach what will work for all your classes, it might be the second (with some changes, such as turning +initialize into a class method on NSObject that returns the property name array and length), but in all likelihood the former is the winner.
There's a danger in both of the above hash implementations with calculating the hash based on property values. From Apple's documentation on hash:
If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash method of the object must not change while the object is in the collection. Therefore, either the hash method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection.
Since you want isEqual: to be true whenever two objects have the same property values, the hashing scheme must depend directly or indirectly on the object's state, so there's no getting around this danger.
I have an array with custom objects. Each array item has a field named "name". Now I want to remove duplicate entries based on this name value.
How should I go about achieving this?
I do not know of any standard way to to do this provided by the frameworks. So you will have to do it in code. Something like this should be doable:
NSArray* originalArray = ... // However you fetch it
NSMutableSet* existingNames = [NSMutableSet set];
NSMutableArray* filteredArray = [NSMutableArray array];
for (id object in originalArray) {
if (![existingNames containsObject:[object name]]) {
[existingNames addObject:[object name]];
[filteredArray addObject:object];
}
}
You might have to actually write this filtering method yourself:
#interface NSArray (CustomFiltering)
#end
#implementation NSArray (CustomFiltering)
- (NSArray *) filterObjectsByKey:(NSString *) key {
NSMutableSet *tempValues = [[NSMutableSet alloc] init];
NSMutableArray *ret = [NSMutableArray array];
for(id obj in self) {
if(! [tempValues containsObject:[obj valueForKey:key]]) {
[tempValues addObject:[obj valueForKey:key]];
[ret addObject:obj];
}
}
[tempValues release];
return ret;
}
#end
I know this is an old question but here is another possibility, depending on what you need.
Apple does provide a way to do this -- Key-Value Coding Collection Operators.
Object operators let you act on a collection. In this case, you want:
#distinctUnionOfObjects
The #distinctUnionOfObjects operator returns an array containing the distinct objects in the property specified by the key path to the right of the operator.
NSArray *distinctArray = [arrayWithDuplicates valueForKeyPath:#"#distinctUnionOfObjects.name"];
In your case, though, you want the whole object. So what you'd have to do is two-fold:
1) Use #distinctUnionOfArrays instead. E.g. If you had these custom objects coming from other collections, use #distinctUnionOfArray.myCollectionOfObjects
2) Implement isEqual: on those objects to return if their .name's are equal
I'm going to get flak for this...
You can convert your array into a dictionary. Not sure how efficient this is, depends on the implementation and comparison call, but it does use a hash map.
//Get unique entries
NSArray *myArray = #[#"Hello", #"World", #"Hello"];
NSDictionary *uniq = [NSDictionary dictionaryWithObjects:myArray forKeys:myArray];
NSLog(#"%#", uniq.allKeys);
*Note, this may change the order of your array.
If you'd like your custom NSObject subclasses to be considered equal when their names are equal you may implement isEqual: and hash. This will allow you to add of the objects to an NSSet/NSMutableSet (a set of distinct objects).
You may then easily create a sorted NSArray by using NSSet's sortedArrayUsingDescriptors:method.
MikeAsh wrote a pretty solid piece about implementing custom equality: Friday Q&A 2010-06-18: Implementing Equality and Hashing
If you are worried about the order
NSArray * newArray =
[[NSOrderedSet orderedSetWithArray:oldArray] array]; **// iOS 5.0 and later**
It is quite simple in one line
NSArray *duplicateList = ...
If you don't care about elements order then (unordered)
NSArray *withoutDUP1 = [[NSSet setWithArray:duplicateList] allObjects];
Keep the elements in order then (ordered)
NSArray *withoutDUP2 = [[NSOrderedSet orderedSetWithArray:duplicateList] array];
Implement isEqual to make your objects comparable:
#interface SomeObject (Equality)
#end
#implementation SomeObject (Equality)
- (BOOL)isEqual:(SomeObject*)other
{
return self.hash == other.hash;
}
- (NSUInteger)hash
{
return self.name;///your case
}
#end
How to use:
- (NSArray*)distinctObjectsFromArray:(NSArray*)array
{
return [array valueForKeyPath:#"#distinctUnionOfObjects.self"];
}