Objective-C
Consider this code:
SKNode *n1 = [SKNode new];
SKNode *n2 = [SKNode new];
NSMutableDictionary *d = [NSMutableDictionary new];
[d setObject:#"A" forKey:n1];
[d setObject:#"B" forKey:n2];
if ([d objectForKey:n1]) {
NSLog(#"true");
}
I'm using SKNode as keys in NSDictionary. I need to test if a given SKNode exists as a Key. The common approaches for testing don't seem to work, as above the result will not return (or print) "true". Neither will the following code:
if ([d allKeys] containsObject:n1]) {
NSLog(#"true");
}
If I make the keys strings; i.e #"key1", #"key2", "true" is printed.
Is it possible to test in this manner?
(It occurred to me that what I wrote as a comment was actually the answer to the question.)
NSDictionary copies its keys (using -copy from the NSCopying protocol). SKNode conforms to NSCopying, but copying a node returns a new node that isn't equal to the old one:
SKNode *a = [SKNode new];
SKNode *b = [a copy];
[a isEqual:b]; // NO
let a = SKNode()
let b = a.copy() as! SKNode
a == b // false
So it's not safe to use SKNodes as keys in a dictionary. As for workarounds: you could perhaps use the node's name as a key (if your nodes' names are unique). You could also use NSMapTable, which doesn't copy its keys.
Your code should work. So something is wrong here, possibly with the implementation of the hashcode and isEqual: method of SKNode.
Log the hash code of your two keys, check what isEqual: returns, log the dictionary and all the keys, and check for anything unexpected.
PS. It turns jtbandes answer is exactly the right one. Of course following my advice and logging everything you would have found that the keys in allKeys are not what's expected, with a fighting chance of figuring out the answer.
So if you have a class with trivial hashcode and isEqual, just taking the pointer as hashcode and comparing pointers for isEqual, then either you have a class where copy creates a new object and a dictionary won't work, or you have a class where copy just returns the original object and it works fine.
Related
I need a Cocoa data structure to keep something like a list of locations of a set of insects in a field over time. Clearly an NSDictionary has to have unique keys, so I couldn't use the insect as a key and MKUserlocation as the object.
Is there any other class besides NSArray where one does not have to step through, if/else-ing at each index?
There seems to be such a thing as NSList discussed in various places but nothing in the Apple docs.
I also saw a reference to NSIndexSet one uses in conjunction to an NSArray, but again reading the Apple docs suggests it's not useful to me.
Since Cocoa framework does not offer a multimap container, you could implement your own by creating an NSMutableDictionary, and populating it with NSMutableArray objects.
NSMutableDictionary locByInsect = [NSMutableDictionary dictionary];
// Adding a location for an insect:
MyInsect *insect = ... // Don't forget to provide hash code and equality checks
MyLocation *location = ...
NSMutableDictionary *insectLocations = locByInsect[insect];
if (insectLocations == nil) {
insectLocations = [NSMutableArray array];
locByInsect[insect] = insectLocations;
}
[insectLocations addObject:location];
// Retrieving all locations for an insect
for (MyLocation *loc in locByInsect[insect]) {
NSLog(#"Saw %# at %#", insect, loc);
}
For each insect have a key in an NSMutableDictionary. Make each key's object an NSMutableArray of locations.
I just noticed that calling addObject: on an NSMutableArray doesn't access that array's setter.
E.g., for NSMutableArray self.myArray, [self.myArray addObject:object] does not use [self setMyArray:array] to add the object.
Previously I have been using custom setters and getter to check assignment before assigning; e.g., if I wanted an array that only accepted objects of class MyClass, I would do the following:
- (void)setMyArray:(NSMutableArray *)myArray
{
for (id object in myArray)
{
if (![object isKindOfClass:[MyClass class]]) return;
}
_myArray = myArray;
}
- (NSMutableArray *)myArray
{
if (!_myArray) _myArray = [[NSMutableArray alloc] init];
_myArray = myArray;
}
How do I go about achieving this same functionality when changing the array via addObject:, removeObject:, and other similar functions that may circumvent the setter?
Generally this kind of problem is the reason why NSMutableArray is usually avoided in preference of NSArray.
This is the simple solution, use NSArray instead of NSMutableArray:
self.myArray = [self.myArray arrayByAddingObject:foo];
However, if the array is really big that will cause performance issues. Then you've got two options:
you can have your own addObjectToMyArray: method in your class and always use that
you can create an NSArrayController and use that to access your array. It will implement key value observing and bindings and all of that stuff.
NSMutableArray is designed to perform addObject: with as few CPU instructions as possible and therefore does not proved any way for external code to be notified that the object was added. You have to have some other class wrapped around it.
Do not try to subclass NSMutableArray, because it is a "class cluster" making subclasses extremely complicated.
If what you wish to do is ensure objects in the array are of a particular class then this answer to the question "NSMutableArray - force the array to hold specific object type only" provides code to do exactly that.
If you wish to do other checks on assignment then you can use the code in that answer as a starting point.
Let's say I've got an array with strings.
NSArray *names = [NSArray arrayWithObjects: #"One", #"Two", #"Three", nil];
What I want is to initiate objects of some custom class and them add them to a mutable array. I'm using a custom init method that takes a string argument.
To be more specific, I want to [SomeClass alloc] initWithName: aName] and add the resulting object to a NSMutableArray.
I'm thinking of using Objective-C fast enumeration. So what I get is:
NSMutableArray *objects = [NSMutableArray arrayWithCapacity: [names count];
for (NSString *name in names) {
[objects addObject: [[[SomeClass alloc] initWithName: name] autorelease]];
}
The problem is that I can't add nil to the array and I don't like exception handling. However, my initiation method may return nil. So I decide to check first before adding (prevention). My new for-in-loop is:
SomeClass *someObject;
for (NSString *name in names) {
someObject = [[[SomeClass alloc] initWithName: name] autorelease];
if (someObject) {
[objects addObject: someObject];
}
}
Now, instead of immediately passing the new object to the array, I'm setting up a pointer someObject first and then passing the pointer to the array instead.
This example raises a question to me. When I someObject = [[[SomeClass alloc] initWithName: name] autorelease] in the loop, do the existing objects (which are added using the same pointer) in the array change too?
To put it in other words: does the addObject: (id)someObject method make a new internal copy of the pointer I pass or do I have to create a copy of the pointer — I don't know how — and pass the copy myself?
Thanks a lot! :-)
It's fine to reuse someObject; if you think about it, you're already reusing name each time you go through the loop.
-addObject: may or may not copy the object that you pass in. (It doesn't -- it retains the object rather than copying it, but it's conceivable that some NSMutableArray subclass could copy instead.) The important thing is that this code really shouldn't care about what -addObject: does.
Also, don't lose sight of the distinction between a pointer and the object that it points to. Pointers are just references, and a pointer is copied each time you pass it into a method or function. (Like C, Objective-C passes parameters by value, so passing a pointer into a method results in putting the value of the pointer on the stack.) The object itself isn't copied, however.
Short answer: no, you don't have to worry about reusing someObject.
Slightly longer answer: the assignment—someObject = ... assigns a new pointer value to the someObject variable; addObject: is then getting that value, not the address of someObject itself.
I think you're getting confused in the concept of pointer here. When you say someObject = [[[SomeClass alloc] init... you are basically pointing the someObject pointer to a new object. So to answer your question- your current code is fine.
As for whether arrays maintain copies of the objects added to them - NO, the array retains the object you add to it. However, that doesn't matter to your code above.
Three20 provides the answer!
If you can't get an object with objectAtIndex: from an NSSet then how do you retrieve objects?
There are several use cases for a set. You could enumerate through (e.g. with enumerateObjectsUsingBlock or NSFastEnumeration), call containsObject to test for membership, use anyObject to get a member (not random), or convert it to an array (in no particular order) with allObjects.
A set is appropriate when you don't want duplicates, don't care about order, and want fast membership testing.
NSSet doesn't have a method objectAtIndex:
Try calling allObjects which returns an NSArray of all the objects.
it is possible to use filteredSetUsingPredicate if you have some kind of unique identifier to select the object you need.
First create the predicate (assuming your unique id in the object is called "identifier" and it is an NSString):
NSPredicate *myPredicate = [NSPredicate predicateWithFormat:#"identifier == %#", identifier];
And then choose the object using the predicate:
NSObject *myChosenObject = [mySet filteredSetUsingPredicate:myPredicate].anyObject;
NSArray *myArray = [myNSSet allObjects];
MyObject *object = [myArray objectAtIndex:(NSUInteger *)]
replace NSUInteger with the index of your desired object.
For Swift3 & iOS10 :
//your current set
let mySet : NSSet
//targetted index
let index : Int
//get object in set at index
let object = mySet.allObjects[index]
NSSet uses the method isEqual: (which the objects you put into that set must override, in addition, the hash method) to determine if an object is inside of it.
So, for example if you have a data model that defines its uniqueness by an id value (say the property is:
#property NSUInteger objectID;
then you'd implement isEqual: as
- (BOOL)isEqual:(id)object
{
return (self.objectID == [object objectID]);
}
and you could implement hash:
- (NSUInteger)hash
{
return self.objectID; // to be honest, I just do what Apple tells me to here
// because I've forgotten how Sets are implemented under the hood
}
Then, you can get an object with that ID (as well as check for whether it's in the NSSet) with:
MyObject *testObject = [[MyObject alloc] init];
testObject.objectID = 5; // for example.
// I presume your object has more properties which you don't need to set here
// because it's objectID that defines uniqueness (see isEqual: above)
MyObject *existingObject = [mySet member: testObject];
// now you've either got it or existingObject is nil
But yeah, the only way to get something out of a NSSet is by considering that which defines its uniqueness in the first place.
I haven't tested what's faster, but I avoid using enumeration because that might be linear whereas using the member: method would be much faster. That's one of the reasons to prefer the use of NSSet instead of NSArray.
for (id currentElement in mySet)
{
// ** some actions with currentElement
}
Most of the time you don't care about getting one particular object from a set. You care about testing to see if a set contains an object. That's what sets are good for. When you want to see if an object is in a collection sets are much faster than arrays.
If you don't care about which object you get, use -anyObject which just gives you one object from the set, like putting your hand in a bag and grabbing something.
Dog *aDog = [dogs anyObject]; // dogs is an NSSet of Dog objects
If you care about what object you get, use -member which gives you back the object, or nil if it's not in the set. You need to already have the object before you call it.
Dog *spot = [Dog dogWithName:#"Spot"];
// ...
Dog *aDog = [dogs member:spot]; // Returns the same object as above
Here's some code you can run in Xcode to understand more
NSString *one = #"One";
NSString *two = #"Two";
NSString *three = #"Three";
NSSet *set = [NSSet setWithObjects:one, two, three, nil];
// Can't use Objective-C literals to create a set.
// Incompatible pointer types initializing 'NSSet *' with an expression of type 'NSArray *'
// NSSet *set = #[one, two, three];
NSLog(#"Set: %#", set);
// Prints looking just like an array but is actually not in any order
//Set: {(
// One,
// Two,
// Three
// )}
// Get a random object
NSString *random = [set anyObject];
NSLog(#"Random: %#", random); // Random: One
// Iterate through objects. Again, although it prints in order, the order is a lie
for (NSString *aString in set) {
NSLog(#"A String: %#", aString);
}
// Get an array from the set
NSArray *array = [set allObjects];
NSLog(#"Array: %#", array);
// Check for an object
if ([set containsObject:two]) {
NSLog(#"Set contains two");
}
// Check whether a set contains an object and return that object if it does (nil if not)
NSString *aTwo = [set member:two];
if (aTwo) {
NSLog(#"Set contains: %#", aTwo);
}
I have an NSDictionary in which I use my own classes (NSObject subclasses) as keys and would like to make sure that I do not include the same key twice. However, because NSDictionary copies its keys, if I try to check whether an object is in the dictionary, it never thinks it is.
For example,
MyClass* obj = [[MyClass alloc] init];
NSMutableDictionary* dict = [NSMutableDictionary dictionary];
[dict setObject:someObj forKey:obj];
if ([[dict allKeys] contains:obj]) // always returns false
// obj is already in dict
else
// add obj to dict etc.
Similarly, if I want to change the object associated with this key, it seems to create a new entry.
// dict is empty
// say [obj description] gives 'MyClass : 0x1' - (impossible address?)
[dict setObject:someObj forKey:obj];
// dict: { 'MyClass : 0x2' = someObjDesc }
[dict setObject:someOtherObj forKey:obj];
// dict: { 'MyClass : 0x2' = someObjDesc , 'MyClass : 0x3' = someOtherObjDesc }
Also, this same thing leads to not being able to access the items in the dictionary from the original object
[dict setObject:someObj forKey:obj];
[dict objectForKey:obj]; // returns null
So, as far as the uniqueness is concerned, would I be best off keeping track of the keys in a separate array or is there a better way of doing this.
I considered implementing an isEqual method based on a unique variable (such as a name) but didn't think that was the Right Thing to do.
Background (in case it turns out that maybe I'm just using the wrong thing entirely):
I want to keep track of information about a group of people going to different places. So each person at each place has some info. What I've done is used nested dictionaries so the key to the main dictionary is a Person object and the object another dictionary. This latter dictionary has key Place and info as the object. I think this is Java syntax but something like > (the array holds the info). I want to be able to add a Person only if the don't already exist, add a Place (for each person), change the array.
Any help on any of this would be greatly appreciated!
You should always use NSStrings as keys for dictionaries, especially if you are new at objective-C. There are a few things that I can see you are doing wrong with your current implementation - you would need to read up on key requirements for NSDictionaries.
You can do what you want with strings as keys - person's name, etc.
The objects in a dictionary have all the info about a certain person:
NSDictionary* personsInfo = [mainDict objectForKey:#"Jane Smith"];
NSString* addressLine1 = [personsInfo objectForKey#"addressLine1"];
--Tom
The simple answer would be to make it so that the MyClass doesn't actually copy anything.
That would be something like changing:
- (id) copyWithZone:(NSZone *)zone {
MyClass * foo = (MyClass *)[super copyWithZone:zone];
[foo configureCopy];
return foo;
}
To:
- (id) copyWithZone:(NSZone *)zone {
return [self retain];
}