Lately I've been encountering some terrible Objective-C code, and I have a few words to say about it. One of the first problems I've found is the overuse of #define. Don't get me wrong, #define has its place, but when it comes to constants #define should not be the first choice.
Anti-PatternsOne of the problems with #define is that it's simply a macro substitution. There is generally no type associated with the value. This makes is harder for the compiler to warn you of potential errors. Macros can also have side effects you may forget about. For example:
#define MY_STRING_CONSTANT [[NSString alloc] initWithFormat:@"Hello World!"] // Very bad.
The above example will cause a memory leak unless you remember to release the value each time. Just a side note, Apple's static analyzer will pick up this error if you turn it on. I should also mention that the following, related example is not preferable. I have actually seen this used in production code before.
#define MY_STRING_CONSTANT [NSString stringWithFormat:@"Hello World!"] // Still not great.
NumbersWith a couple anti-patterns out of the way, let's go over the proper way to define constants. You will find that it doesn't take much more effort to properly define constants. Numbers are the simplest and one of the most common, so we'll start with them. In your .m file (applies to .c or .cpp too) you will define your constant like the following:
const NSInteger kMyInt = -100;
const NSUInteger kMyUnsignedInt = 42;
const CGFloat kMyFloat = 3.14;
You could instead use 'int', 'unsigned int', and 'float' if you prefer, but the above types are generally the preferred data types. If you want to keep your constant visible to just your implementation file, then you are done. If not, you need to do one last thing. In your header file, you will need to use the following:
extern const NSInteger kMyInt;
extern const NSUInteger kMyUnsignedInt;
extern const CGFloat kMyFloat;
The reserved word 'extern' makes a promise to the compiler. You are letting the compiler know that you are not defining the value right away, but at link time, that value will exist. In this case, it's defined in your implementation file. With that in place, you are now done; your constant is defined and other classes can #import (#include for C and C++) your header file and use your number constants.
You may wonder why you can't define the constant right in your header file. If you do, you will get an error message that looks like this: "
ld: duplicate symbol <Constant Name> in <File 1> and <File 2>." Generally #import handles duplicate imports properly; however, it doesn't work for constants. I figure this error is why developers unaware of 'extern' default to #define.
EnumerationsClosely related to numbers are enumerations. If you ever have a series of related number constants, you probably want an enum. For example:
// A contrived example but should get the idea across.
enum {
kJanuary = 1,
kFebruary = 2,
kMarch = 3,
kApril = 4,
kMay = 5,
kJune = 6,
kJuly = 7,
kAugust = 8,
kSeptember = 9,
kOctober = 10,
kNovember = 11,
kDecember = 12,
};
Enumerations are great for bit masks too.
// Another contrived example.
enum {
kBit1 = 1 << 0,
kBit2 = 1 << 1,
kBit3 = 1 << 2,
kBit4 = 1 << 3,
};
Enumerations are where type checking really helps. Say you are using a switch statement, the compiler can warn you if you didn't include one of your enum values as a switch case.
StringsStrings are also very common in Cocoa programming. There is one extra detail that makes them different from numbers. At first you may want to make a constant like this:
const NSString * kMyString = @"Hello World!"; // Wrong - String Anti-Pattern 1
This is wrong for a couple different reasons. First, NSString is immutable, meaning that it can't be changed. This essentially makes it a constant by default. However, there is a big flaw with the above anti-pattern. I will first show the proper way:
NSString * const kMyString = @"Hello World!"; // Right
At first glance, you are probably wondering why 'const' is next to the constant name. The difference between the number and string examples is that the NSString is a pointer. The first string example tells the system that the value @"Hello World!" can't change, but the pointer value can change. You probably wouldn't, but you could do the following:
kMyString = @"Goodbye World!"; // Very bad, but allowed in String Anti-Pattern 1.
You didn't change the value of @"Hello World!", but you did change what kMyString pointed to. That doesn't sound like a constant at all.
By the way, the following is also permissible, but wrong.
const NSString * const kMyString = @"Hello World!"; // Wrong - String Anti-Pattern 2
This makes both the value and the pointer constant. However, the first 'const' changes the data type of the constants. None of the Cocoa frameworks accept a const NSString * (since it's redundant). This means that you will have a compiler warning anytime you try to pass your constant to a Cocoa framework method. You might do something similar with other objects, but in Objective-C, such cases should be very rare. This is something more common in C or C++.
ObjectsObjects other than strings can be a little more tricky. You can't do the following:
MyObject * const kObject = [[MyObject alloc] init]; // Wrong - Object Anti-Pattern 1
The problems is the right-hand side does not evaluate to a compile time constant. Furthermore, if it was possible, it would be difficult to configure the object properly. This requires a different technique.
// Right
+ (MyObject *)MyConstObject {
static MyObject * obj = nil;
if (!obj) {
obj = [[MyObject alloc] init];
// Any other initial setup.
}
return obj;
}
Looking at the return type of MyObject *, you may think this has the same problem that String Anti-Pattern 1 had. This is not the case here. The original constant pointer is protected within the method; it can't be changed externally. When you call the method, you receive a copy of the object's address. You can change it if you want, but it won't affect anyone else. Naively you may want to change the return type to MyObject * const, but a const pointer doesn't affect the data type. You could do the following regardless:
MyObject * obj = [[self class] MyConstObject]; // Type MyObject * is the same as MyObject * const
One last note about object constants is that they are technically memory leaks. However, they are permissible for the same reason that
singletons are permissible. What's more interesting is that Apple's latest static analyzer now flags singletons as memory leaks but still doesn't catch the above constant.
When to use #defineI should probably include a kind word about #define. If you want to use any pre-processor macros such as #if or #ifdef, then #define is your only choice. #define is also good for simple, inline functions or when you want to write some code that will do automatic renaming (see Cocoa With Love's
synthesized singleton). #define has countless valuable uses, but when it comes to constants, try something else first.