Vincent Gable’s Blog

May 15, 2009

Concise NSDictionary and NSArray Lookup

I started writing a list of ways I thought Objective-C could be improved, and I realized that many of my wishes involved more compact syntax. For example [array objectAtIndex:1] is so verbose I think it diminishes readability, compared to array[1].

I can’t quite match that brevity (can you, by using Objective-C++?), but with a one-line category, you can say, x = [array:1];.

@interface NSArray (ConciseLookup)
- (id):(NSUInteger)index;
@end
@implementation NSArray (ConciseLookup)
- (id):(NSUInteger)index;
{
	return [self objectAtIndex:index];
}
@end

My question is: do you find this compact “syntax” useful at all, or is it added complexity with no substantial code compression? Personally I think the latter, but the number of wishes I had involving more concise Objective-C syntax makes me wonder…

April 22, 2009

-[NSURL isEqual:] Gotcha

Filed under: Bug Bite,Cocoa,iPhone,MacOSX,Programming,Sample Code | , , , , , ,
― Vincent Gable on April 22, 2009

BREAKING UPDATE: Actually comparing the -absoluteURL or -absoluteString of two NSURLs that represent a file is not good enough. One may start file:///, and the other file://localhost/, and they will not be isEqual:! A work around is to compare the path of each NSURL. I’m still looking into the issue, but for now I am using the following method to compare NSURLs.

@implementation NSURL (IsEqualTesting)
- (BOOL) isEqualToURL:(NSURL*)otherURL;
{
	return [[self absoluteURL] isEqual:[otherURL absoluteURL]] || 
	[self isFileURL] && [otherURL isFileURL] &&
	([[self path] isEqual:[otherURL path]]);
}
@end

[a isEqual:b] may report NO for two NSURLs that both resolve to the same resource (website, file, whatever). So compare NSURLs like [[a absoluteString] isEqual:[b absoluteString]]. It’s important to be aware of this gotcha, because URLs are Apple’s preferred way to represent file paths, and APIs are starting to require them. Equality tests that worked for NSString file-paths may fail with NSURL file-paths.

The official documentation says

two NSURLs are considered equal if they both have the same base baseURL and relativeString.

Furthermore,

An NSURL object is composed of two parts—a potentially nil base URL and a string that is resolved relative to the base URL. An NSURL object whose string is fully resolved without a base is considered absolute; all others are considered relative.

In other words, two NSURL objects can resolve to the same absolute URL, but have a different base URL, and be considered !isEqual:.

An example should make this all clear,

NSURL *VGableDotCom = [NSURL URLWithString:@"http://vgable.com"];
NSURL *a = [[NSURL alloc] initWithString:@"blog" relativeToURL:VGableDotCom];
NSURL *b = [[NSURL alloc] initWithString:@"http://vgable.com/blog" relativeToURL:nil];
LOG_INT([a isEqual:b]);
LOG_INT([[a absoluteURL] isEqual:[b absoluteURL]]);
LOG_ID([a absoluteURL]);
LOG_ID([b absoluteURL]);

[a isEqual:b] = 0
[[a absoluteURL] isEqual:[b absoluteURL]] = 1
[a absoluteURL] = http://vgable.com/blog
[b absoluteURL] = http://vgable.com/blog

Remember that collections use isEqual: to determine equality, so you may have to convert an NSURL to an absoluteURL to get the behavior you expect, especially with NSSet and NSDictionary.

December 16, 2008

isEmpty?

Checking if a Cocoa object is empty is a little harder then in other languages, say C++, (but easier in some ways). Because every object in Objective-C is actually a pointer to an object, there are two ways, obj, can be empty.

obj = {}

obj points to an object that is empty. Say an array with 0 items, or the string "", etc..

obj = nil

obj, the pointer obj, is NULL, nil, 0, or whatever you want to call it. You might argue that obj isn’t really an object, but it is empty, because there’s nothing in it.

Bug:

When I first started writing Objective-C, I made the mistake of writing code like: if([name isEqualToString:@""]){ ... }, to test for empty strings. And this code would work for a while until I used it in a situation where name was nil, and then, because sending any method called on nil “returns” NO, I would have mysterious errors. (Worse then a crash, because it’s harder to track down.)

Bug:

It’s tempting to avoid the previous bug, by explicitly testing for nil and {}. Say with code like:

if (email == nil || ![email isEqualTo:@""] )
   email = @"An email address is required";

But generally this is a bad idea. It means more code, which means more places for a bug. I know it’s only one trivial test, but I’m serious, when I say it’s asking for a bug — like the bug in the example above, which sets email to @"An email address is required", whenever it is not the empty string, rather then when it is empty. (Values have been changed tho protect the innocent but it’s a bug I’ve seen.)

Solutions:

Wil Shipley suggests using the global function:

static inline BOOL IsEmpty(id thing) {
    return thing == nil
        || ([thing respondsToSelector:@selector(length)]
        && [(NSData *)thing length] == 0)
        || ([thing respondsToSelector:@selector(count)]
        && [(NSArray *)thing count] == 0);
}

I’ve been using his IsEmpty() for about a year. I’ve had zero problems with it, while it’s made my code more readable and concise.

Another solution is to take advantage of what happens when you send a message to nil. (To over-simplify, you get back 0 or NO.) So you can just say “if ([obj count] == 0) then obj is empty.” This often means reversing your thinking, and testing “IsNotEmpty()” instead of “IsEmpty()”. I don’t think it’s as clear is IsEmpty() in general, but in cases where it is, there you have it.

August 31, 2008

Better List Termination

Filed under: MacOSX,Objective-C,Programming,Usability | , ,
― Vincent Gable on August 31, 2008

As I’ve written before, using nil to terminate a variable-length list (eg [NSArray arrayWithObject:foo,bar,nil]) is a bad choice because of how easy it is to get a nil value in Objective-C without knowing it. (Unlike, say C++, doing stuff with nil won’t cause an exception, leading to some subtle bugs). Well, here is a solution.

A Better Sentinel

#define END_OF_LIST ((id) -1)
is my current choice of a good sentinel-value. It is an invalid pointer, so unlike nil, you should never get it back from some library. Sending it any message will fail fast, and recognizably (you will see 0xFF...FF as an illegal address somewhere in the back-trace).

Example:


id aNilObject = nil;
NSArray *standard = [NSArray arrayWithObjects:@"1", @"2", aNilObject, @"4", nil];
LOG_ID(standard);
NSArray *better = [NSArray arrayWithSentinelTerminatedObjects:@"1", @"2", aNilObject, @"4", END_OF_LIST];
LOG_ID(better);


standard = (
   1,
   2
)
better = (
   1,
   2,
   <null>,
   4
)

The Code:


#define END_OF_LIST ((id) -1)

@implementation NSArray (SentinelTerminatedConstructors)

+ (NSArray*) arrayWithSentinelTerminatedObjects:(id) firstObject, ...
{
   NSMutableArray *array = [NSMutableArray array];
   
   if (firstObject != END_OF_LIST)
   {
      if(!firstObject)
         firstObject = [NSNull null];

      [array addObject:firstObject];
      
      id eachObject;
      va_list argumentList;
      va_start(argumentList, firstObject);
      while (END_OF_LIST != (eachObject = va_arg(argumentList, id)))
      {
         if(!eachObject)
            eachObject = [NSNull null];

         [array addObject:eachObject];
      }
      va_end(argumentList);
   }
   
   return array;
}

@end

Some Downsides

arrayWithSentinelTerminatedObjects” is too long of a name. If you’ve got a better one, please share!

You won’t get a compiler-warning if you leave out END_OF_LIST, but if you compile with -Wformat, you will get a warning if you leave out nil. The full story

There are six collection creation methods in Foundation which require nil termination to function properly. They are:

+[NSArray arrayWithObjects:]
-[NSArray initWithObject:]
+[NSDictionary dictionaryWithObjectsAndKeys:]
-[NSDictionary initWithObjectsAndKeys:]
+[NSSet setWithObjects:]
-[NSSet initWithObjects:]

These methods have been decorated with the NS_REQUIRES_NIL_TERMINATION macro, which adds an additional check to invocations of those methods to make sure the nil has been included at the end of the argument list. This warning is only available when compiling with -Wformat.

What do you think?

Frankly, I haven’t used END_OF_LIST-terminated collection creation enough yet to have a final verdict. So please let me know what you think! Was this code useful? Did it cause problems, or preempt them? As always, if you have a better solution, please share!

July 17, 2008

Null-Terminated Argument Lists

Filed under: Bug Bite,C++,Cocoa,Design,Objective-C,Programming,Usability | , , , , ,
― Vincent Gable on July 17, 2008

I was using +[NSDictionary dictionaryWithObjectsAndKeys:] to make a new dictionary, but one of the objects in the dictionary was the result of a call to a method that was returning nil, so the dictionary was incomplete.

This got me thinking about NULL/nil terminated argument lists. I don’t think they are a great idea (the compiler should be able to handle the list-termination for you!), but I think they are an especially bad idea in Objective-C.

The problem that it’s very common to have a nil object in Objective-C, relative to, say C++. Many Cocoa methods return nil on error. Since doing stuff with nil (generally) won’t cause an exception, these nils stick around much longer then in other languages. As you can see, nil is a pretty poor choice of a sentinel value.

It’s the 21st century! The compiler could tell an Obj-C method using a variable-argument-list how many arguments are in the list. This is trivial when all arguments are of type id. Since Obj-C methods use a radically different syntax from C functions, it shouldn’t effect existing C-code. Unfortunately, I don’t see this being added, because Objective-C is already so mature.

In the meantime. Be a little more suspicious of any objective-C methods taking a NULL-terminated list. I wish I had a perfect solution to avoid them, but I don’t! Sometimes they are the best way to do something. If you have a great work-around for constructing, say an NSDictionary with a variable number of key/values please let me know!

Powered by WordPress