views:

5064

answers:

2

Hi,

I want to define a constant in objective-c.

Previously I had the following function:

+(NSString *) getDocumentsDir {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES);
    NSString *documentsDir = [paths objectAtIndex: 0];
    paths = nil;
    return documentsDir;
}

I'd like to define a constant "Documents_Dir" only once - when the function gets called and after that to access a previously created value.

I've tried the following code, which didn't work:

#define getDocumentsDir \
{   \
#ifdef Documents_Dir    \
return Documents_Dir;   \
#else   \
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES);  \
NSString *documentsDir = [paths objectAtIndex: 0];  \
#define Documents_Dir [paths objectAtIndex: 0]; \
paths = nil;    \
return Documents_Dir;   \
#endif  \
}   \

I am not strong with precompiler directives, so any help will be appreciated.

+15  A: 

Prelude: It pays to understand the difference between precompiler directives and true constants. A #define just does a text replacement before the compiler builds the code. This works great for numerical constants and typedefs, but is not always the best idea for function or method calls. I'm operating under the assumption that you really want a true constant, meaning that the code to create the search path should only be executed once.


In your MyClass.m file, define the variable and populate it in an +initialize method like so:

static NSArray *searchPath;

@implementation MyClass

+ (void) initialize {
    searchPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES) objectAtIndex: 0] retain];
}

...

@end

The static modifier makes it visible only within the compilation unit where it is declared. For a simple constant, this is all you need.

If the class has subclasses, +initialize will be called once for each subclass (by default), so you'll want to check whether searchPath is nil before assigning to it, so you don't leak memory. (Or, as Peter Lewis points out, you can check if the class currently being initialized is the MyClass, using either == or the -isMemberOfClass: method.) If the subclasses also need to access the constant directly, you'd need to pre-declare the variable as extern in MyClass.h file (which the child classes include):

extern NSArray *searchPath;

@interface MyClass : NSObject
...
@end

If you pre-declare the variable as extern, you must remove the static keyword from the definition to avoid compile errors. This is necessary so the variable can span multiple compilation units. (Ah, the joys of C...)

Note: In Objective-C code, the better way to declare something as extern is to use OBJC_EXPORT (a #define declared in <objc/objc-api.h>) which is set based on whether or not you're using C++. Just replace "extern" with "OBJC_EXPORT" and you're done.


Edit: I just happened upon a related SO question.

Quinn Taylor
Thanks, but when I build my project, I get the following warning: 'searchPath' defined but not used.The warning appears in all files except in those where it is straightforwardly used.The file with a constant defined is included into the precolmpiled headers.Is there a way to get rid of this warning?Thanks.
Ilya
That's because they symbol is being imported in multiple files. Since the file in question is being included in a lot of other files, use my guidance about subclasses — declare the variable as extern in the header, then choose only one place (in a .m file) to declare it as static, and one (possibly different) place to initialize it. If you're already doing that and the error persists, prefix the variable declaration with __attribute__((unused)) to tell the compiler to suppress warnings about unused symbols. (I personally use #define UNUSED __attribute__((unused)) as shorthand for this.)
Quinn Taylor
Note that if MyClass is subclassed, initialize will be called multiple times, so if you are going to use initialize, you will need to use: if ( self == [MyClass class] ) { searchPath == ...; }
Peter N Lewis
Unfortunately this didn't work for me. I added "OBJC_EXPORT NSString *Documents_Dir;" in the header file. And into the implementation file I added "static NSString *Documents_Dir;". Despite that I get a error - "static declaration of 'Documents_Dir' follows non-static declaration". What am I doing wrong?
Ilya
I should have been clearer. If you declare the variable as extern, remove the static from the definition, since you no longer want it to belong to only a single compilation unit. Modifying my answer to make that more clear.
Quinn Taylor
+6  A: 

The easiest solution is to just change paths to be a static variable and evalutate it only once, like this:

+(NSString *) getDocumentsDir {
    static NSString *documentsDir = nil;
    if ( !documentsDir ) {
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES);
        documentsDir = [paths objectAtIndex: 0];
    }
    return documentsDir;
}

The "static" tells the compiler that documentsDir is effectively a global variable, although only accessible within the function. So it is initialized to nil, and the first call to getDocumentsDir will evalutate it and then further calls will return the pre-evalutated value.

Peter N Lewis
+1 Good call, I'd forgotten you can declare a static variable inside a function or method! This is indeed the easiest way, but bear in mind that no other compilation unit (subclass or otherwise) will be able to reference the symbol. If you only need the constant in one file, try this approach.
Quinn Taylor
Just what I needed to make available a never-changing array of numbers without instantiating it over and over. If you want to use documentsDir in your class methods, place "static NSString *documentsDir = nil;" outside the class method though.
Elise van Looij