- Item 1: Familiarize Yourself with Objective-C's Roots
- Item 2: Minimize Importing Headers in Headers
- Item 3: Prefer Literal Syntax over the Equivalent Methods
- Item 4: Prefer Typed Constants to Preprocessor #define
- Item 5: Use Enumerations for States, Options, and Status Codes
Item 4: Prefer Typed Constants to Preprocessor #define
When writing code, you will often want to define a constant. For example, consider a UI view class that presents and dismisses itself using animations. A typical constant that you’d likely want to factor out is the animation duration. You’ve learned all about Objective-C and its C foundations, and so you take the approach of defining the constant like this:
#define ANIMATION_DURATION
0.3
This is a preprocessor directive; whenever the string ANIMATION_DURATION is found in your source code, it is replaced with 0.3. This might seem exactly what you want, but this definition has no type information. It is likely that something declared as a “duration” means that the value is related to time, but it’s not made explicit. Also, the preprocessor will blindly replace all occurrences of ANIMATION_DURATION, so if that were declared in a header file, anything else that imported that header would see the replacement done.
To solve these problems, you should make use of the compiler. There is always a better way to define a constant than using a preprocessor define. For example, the following defines a constant of type NSTimeInterval:
static constNSTimeInterval
kAnimationDuration =0.3
;
Note that with this style, there is type information, which is beneficial because it clearly defines what the constant is. The type is NSTimeInterval, and so it helps to document the use of that variable. If you have a lot of constants to define, this will certainly help you and other people who read the code later.
Also note how the constant is named. The usual convention for constants is to prefix with the letter k for constants that are local to a translation unit (implementation file). For constants that are exposed outside of a class, it is usual to prefix with the class name. Item 19 explains more about naming conventions.
It is important where you define your constants. Sometimes, it is tempting to declare preprocessor defines in header files, but that is extremely bad practice, especially if the defines are not named in such a way that they won’t clash. For example, the ANIMATION_DURATION constant would be a bad name to appear in a header file. It would be present in all other files that imported the header. Even the static const as it stands should not appear in a header file. Since Objective-C has no namespaces, it would declare a global variable called kAnimationDuration. Its name should be prefixed with something that scopes it to the class it is to be used with, such as EOCViewClassAnimationDuration. Item 19 explains more about using a clear naming scheme.
A constant that does not need to be exposed to the outside world should be defined in the implementation file where it is used. For example, if the animation duration constant were used in a UIView subclass, for use in an iOS application that uses UIKit, it would look like this:
// EOCAnimatedView.h
#import <UIKit/UIKit.h>
@interface EOCAnimatedView :UIView
- (void
)animate; @end// EOCAnimatedView.m
#import "EOCAnimatedView.h"
static constNSTimeInterval
kAnimationDuration =0.3
; @implementation EOCAnimatedView - (void
)animate { [UIView
animateWithDuration
:kAnimationDurationanimations
:^(){// Perform animations
}]; } @end
It is important that the variable is declared as both static and const. The const qualifier means that the compiler will throw an error if you try to alter the value. In this scenario, that’s exactly what is required. The value shouldn’t be allowed to change. The static qualifier means that the variable is local to the translation unit in which it is defined. A translation unit is the input the compiler receives to generate one object file. In the case of Objective-C, this usually means that there is one translation unit per class: every implementation (.m) file. So in the preceding example, kAnimationDuration will be declared locally to the object file generated from EOCAnimatedView.m. If the variable were not declared static, the compiler would create an external symbol for it. If another translation unit also declared a variable with the same name, the linker would throw an error with a message similar to this:
duplicate symbol _kAnimationDuration in: EOCAnimatedView.o EOCOtherView.o
In fact, when declaring the variable as both static and const, the compiler doesn’t end up creating a symbol at all but instead replaces occurrences just like a preprocessor define does. Remember, however, the benefit is that the type information is present.
Sometimes, you will want to expose a constant externally. For example, you might want to do this if your class will notify others using NSNotificationCenter. This works by one object posting notifications and others registering to receive them. Notifications have a string name, and this is what you might want to declare as an externally visible constant variable. Doing so means that anyone wanting to register to receive such notifications does not need to know the actual string name but can simply use the constant variable.
Such constants need to appear in the global symbol table to be used from outside the translation unit in which they are defined. Therefore, these constants need to be declared in a different way from the static const example. These constants should be defined like so:
// In the header file
externNSString
*const EOCStringConstant;// In the implementation file
NSString
*const EOCStringConstant =@"VALUE"
;
The constant is “declared” in the header file and “defined” in the implementation file. In the constant’s type, the placement of the const qualifier is important. These definitions are read backward, meaning that in this case, EOCStringConstant is a “constant pointer to an NSString.” This is what we want; the constant should not be allowed to change to point to a different NSString object.
The extern keyword in the header tells the compiler what to do when it encounters the constant being used in a file that imports it. The keyword tells the compiler that there will be a symbol for EOCStringConstant in the global symbol table. This means that the constant can be used without the compiler’s being able to see the definition for it. The compiler simply knows that the constant will exist when the binary is linked.
The constant has to be defined once and only once. It is usually defined in the implementation file that relates to the header file in which it is declared. The compiler will allocate storage for the string in the data section of the object file that is generated from this implementation file. When this object file is linked with other object files to produce the final binary, the linker will be able to resolve the global symbol for EOCStringConstant wherever else it has been used.
The fact that the symbol appears in the global symbol table means that you should name such constants carefully. For example, a class that handles login for an application may have a notification that is fired after login has finished. The notification may look like this:
// EOCLoginManager.h
#import <Foundation/Foundation.h>
externNSString
*const EOCLoginManagerDidLoginNotification; @interface EOCLoginManager :NSObject
- (void
)login; @end// EOCLoginManager.m
#import "EOCLoginManager.h"
NSString
*const EOCLoginManagerDidLoginNotification =@"EOCLoginManagerDidLoginNotification";
@implementation EOCLoginManager - (void
)login {// Perform login asynchronously, then call 'p_didLogin'.
} - (void
)p_didLogin { [[NSNotificationCenter
defaultCenter]postNotificationName
:EOCLoginManagerDidLoginNotificationobject
:nil]; } @end
Note the name given to the constant. Prefixing with the class name that the constant relates to is prudent and will help you avoid potential clashes. This is common throughout the system frameworks as well. UIKit, for example, declares notification names as global constants in the same way. The names include UIApplicationDidEnterBackgroundNotification and UIApplicationWillEnterForegroundNotification.
The same can be done with constants of other types. If the animation duration needed to be exposed outside of the EOCAnimatedView class in the preceding examples, you could declare it like so:
// EOCAnimatedView.h
extern constNSTimeInterval
EOCAnimatedViewAnimationDuration;// EOCAnimatedView.m
constNSTimeInterval
EOCAnimatedViewAnimationDuration =0.3
;
Defining a constant in this way is much better than a preprocessor define because the compiler is used to ensure that the value cannot change. Once defined in EOCAnimatedView.m, that value is used everywhere. A preprocessor define could be redefined by mistake, meaning that different parts of an application end up using different values.
In conclusion, avoid using preprocessor defines for constants. Instead, use constants that are seen by the compiler, such as static const globals declared in implementation files.
Things to Remember
- Avoid preprocessor defines. They don’t contain any type information and are simply a find and replace executed before compilation. They could be redefined without warning, yielding inconsistent values throughout an application.
- Define translation-unit-specific constants within an implementation file as static const. These constants will not be exposed in the global symbol table, so their names do not need to be namespaced.
- Define global constants as external in a header file, and define them in the associated implementation file. These constants will appear in the global symbol table, so their names should be namespaced, usually by prefixing them with the class name to which they correspond.