1

Here's my code:

NSArray *allIds = [self.purchasableObjects valueForKey:@"productIdentifier"];
NSInteger index = [allIds indexOfObject:productId];

if (index == NSNotFound)
    return;

Which one to compare to NSNotFound... NSInteger or NSUInteger and why?

Ethan Allen
  • 14,425
  • 24
  • 101
  • 194

2 Answers2

2

The reason why he's asked this question is because the AppKit is inconsistent with Foundation indexes, especially for NSMenu and NSTableView:

id obj = ...;
NSMenu * menu = ...;

/* This returns an NSInteger, returning -1 (which when casted, is NSNotFound) */
NSInteger index = [ menu indexOfItemWithRepresentedObject:obj ];

/* This returns an NSUInteger, since arrays cannot have negative indices. */
NSUInteger idx = [ [ menu itemArray ] indexOfObjectIdenticalTo:obj ];

Because of this, one must cast back and forth, but this creates new problems:

Because indexes cannot be negative, the number of rows (or submenus, etc) is lower BY HALF of what normal arrays can be.

If you attempt to read or insert an object to a negative index (-2) in a UI element, you do not get range exceptions. If you cast to unsigned, the index is undefined. So this code, which is wrong, compiles without error:

id obj = ...;
NSMenu * menu = ...;
/* Or the result of some integer arithmetic. */
NSInteger idx = -2; 

/* Compiles, doesn't throw exception. Index COULD be NSIntegerMax - 1, but the ending index is undefined. */
[ menu insertItem:obj atIndex:idx ];

/* Compiles, but throws NSRangeException. */
[ [ menu mutableArrayValueForKey:@"items" ] insertObject:obj atIndex:( NSUInteger )idx ];

In the first case, when inserting an item beyond menu.numberOfItems (which is an NSInteger), in general (but not guaranteed), the method is equivalent to [ menu addItem ]. So, index is transformed thus:

[ menu insertItem:obj atIndex:MIN( idx, menu.numberOfItems ) ];

But in older versions of AppKit, index is rounded up!:

[ menu insertItem:obj atIndex:MAX( MIN( idx, menu.numberOfItems ), 0 ) ];

So a programmer must take great care when inserting and/or retrieving indexes from UI elements, or use an NSArrayController instead.

For all cases, though it is safe to check both [ menu indexOfItem:obj ] and [ array indexOfObjectIdenticalTo:obj ] are equal NSNotFound, so long as you use an explicit cast. While NSNotFound is has a declared type of NSUInteger, it is defined as NSUIntegerMax, which when cast equals -1. NEVER check if a value is less than NSNotFound as you will create yet another infinite loop. But feel free to:

NSInteger index = [ menu indexOfItem:nonexistantObject ];
if( ( NSUInteger )index == NSNotFound ) {
...blah...
}

or

NSUInteger index = [ array indexOfItem:nonexistantObject ];
if( index == NSNotFound ) {
...blah...
}

However, if the return type is NSInteger, you should not assume that if the returned index isn't NSNotFound that it is a valid index (especially if you're using a 3rd-party framework or library). Instead, you should check to see if the returned index is in a valid range:

NSInteger index = [ menu indexOfItem:nonexistantObject ];
if( ( NSUInteger )index == NSNotFound ) {
 /* Can't find it, so insert or do something else. */
} else if( !( index >= 0 && index <= menu.numberOfItems ) ) {
/* Throw an invalid range exception. There's a major bug! */
} else {
/* woo hoo! we found it! */
}

Of course that check is computationally expensive, so it should only be used in code that runs infrequently, is not in a loop, or in debug code. If you want to ensure that you always get a valid index as quickly as possible, you should use block enumeration, which is faster than just about any other method:

id obj = ..;
/* Get an array from the UI, or just use a normal array. */
NSArray * array = [ menu itemArray ];

/* Find an index quickly */
NSUInteger index = [ array indexOfObjectWithOptions:NSEnumerationConcurrent passingTest:^BOOL( NSUInteger idx, id testObj, BOOL * stop ) {
if( obj == testObj ) {
*stop = TRUE;
return TRUE;
}
} ];

The returned index is guaranteed to be valid, either NSNotFound or in NSMakeRange( 0, array.count - 1 ). Notice I did not use -isEqual: as I was checking for a specific instance not an object that could be interpreted as the same as another.

If you need to do something with the indices in the loop, you can use the same technique (notice the indices are passed to the block, and are always valid):

[ array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^( NSUInteger idx, id obj, BOOL * stop ) {
/* do something to this object, like adding it to another array, etc */
} ];

Using the block methods on arrays always get you valid indices, that can be cast to NSIntegers for use in UI elements. There are also range-specific block methods that can restrict the values even further. I'd recommend reading through the NSArray and NSMutableArray documentation for pointers. It's also quite easy to misuse blocks, by adding code to the [enumerate...withBlock:][3] methods, especially if you are adding the objects to another collection class:

NSArray * source = ...;
NSMutableArray * dest = ...;

[ source enumerateObjectsUsingBlock:^( NSUInteger idx, id obj, BOOL * stop ) {
if( /* some test */ ) {
/* This is wasteful, and possibly incorrect if you're using NSEnumerationConcurrent */
[ dest addObject:obj ];
}
} ];

It's better to do this instead:

/* block removed for clarity */
NSArray * source = ...;
NSMutableArray * dest = ...;
NSIndexSet * indices = [ source indexesOfObjectsPassingTest:testBlock ];

[ dest addObjects:[ source objectsAtIndexes:indices ]  ];

Sorry for the long-winded response, but this is a problem with Cocoa indexing (and especially NSNotFound) that my developers have to solve and re-solve, so I thought it would be helpful to share.

Rob Dotson
  • 46
  • 4
  • This is wrong: "While NSNotFound is has a declared type of NSUInteger, it is defined as NSUIntegerMax". In NSObjCRuntime.h, this is the declaration: static const NSInteger NSNotFound = NSIntegerMax; – Michael Domino Jul 27 '18 at 20:13
  • Actually, it’s not, though the declaration has changed several times over the years. The value NSIntegerMax has always been a static constant with the equivalent of: (int_t)((uint_t)(-1)) which is 0b111111...1 or all ones. The legacy documentation made this clear, though the current documentation is less so. – Rob Dotson Jul 29 '18 at 23:51
1

You should use NSUInteger

The reason behind this is "The array index is not going to minus (i.e object at index -5)"

typedef int NSInteger;
typedef unsigned int NSUInteger;

NSInteger must be used when there are probability to get values in plus or minus.

Hope it helps. Thanks

NSSwift
  • 215
  • 2
  • 8
  • I really hate this when I try to stick with this unsigned array index concept but I cannot write `for(NSUInteger index i = [allIds count] - 1; i >= 0; i--)` – tia Jul 10 '15 at 02:57
  • @tia you can not use NSUInteger in decrement operator. Because when you reach at index 0, and i-- will try to make it -1 (but it's NSUInteger which is unsigned so it will fail that operation and this iteration will infinite.That is the reason not to use NSUInteger in loop) – NSSwift Jul 10 '15 at 03:06
  • Actually, the reason to use `NSUInteger` is because that is the return type of `NSArray indexOfObject:`. – rmaddy Jul 10 '15 at 03:49
  • 1
    @rmaddy yes.. right. and why the return type of indexofobject is NSUInteger ;) That is the above reason – NSSwift Jul 10 '15 at 05:12
  • Angel's answer is what I was looking for. The explanation. – Ethan Allen Jul 10 '15 at 22:11
  • @EthanAllen happy to hear. Thanks – NSSwift Jul 11 '15 at 02:18