Sure, just think subclassing. First our class, which is a subclass of NSString
:
@interface StringConstants : NSString
extern StringConstants * const kOptionApple;
extern StringConstants * const kOptionBlackberry;
@end
So we've defined StringConstants
and a couple of global constants for it. To implement the class without any warnings just requires some casting:
@implementation StringConstants
StringConstants * const kOptionApple = (StringConstants *)@"Apple";
StringConstants * const kOptionBlackberry = (StringConstants *)@"Blackberry";
@end
And there is our set of constants. Let's test it:
- (void) printMe:(StringConstants *)string
{
NSLog(@"string: %@", string);
}
- (void) test
{
[self printMe:kOptionApple]; // Code completion offers the constants
[self printMe:@"Rhubarb"]; // Warning: Incompatible pointer types
[self printMe:(StringConstants *)@"Custard"]; // OK
}
You only get a warning, the code will run, as with other similar type errors.
You can of course repeat the pattern and produce one "class" per set of strings.
HTH
Addendum: It Is Safe (Trust Me For Now) and the Weak Enum
Concern has been raised in the comments that the above code is essentially dangerous, it is not. However in the general case the concerns raised are valid, it is safe here by design.
Note: This is being typed directly into SO. Please forgive the inevitable spelling & grammar errors, and the probable lack of good presentation, well defined explanatory arc, missing bits, redundant bits etc.
First let's add the missing comments to the above code, start with the implementation:
// The following *downcasts* strings to be StringConstants, code that
// does this should only appear in this implementation file. Use in
// other circumstances would effectively increase the number of "enum"
// values in the set, which rather defeats the purpose of this class!
//
// In general downcasting should only be performed after type checks to
// make sure it is safe. In this particular case *by design* it is safe.
StringConstants * const kOptionApple = (StringConstants *)@"Apple";
There are two different issues here
- Is it safe at all - yes by design, trust me (for now); and
- The addition of additional "enum" values
The second is covered by the second missing comment in the test code:
[self printMe:(StringConstants *)@"Custard"]; // OK :-( - go ahead, graft
// in a new value and shoot
// yourself in the foot if
// you must ;-)
The weak enum
Dealing with the second issue first, unsurprisingly this "enum" isn't bulletproof - you can add additional values on the fly. Why unsurprising? Because you can do it in (Objective-)C as well, not only is the language not strongly typed the enum
types are the weakest of the lot. Consider:
typedef enum { kApple, kBlackberry } PieOptions;
How many valid values of PieOptions
are there? Using Xcode/Clang: 2^32, not 2. The following is perfectly valid:
PieOptions po = (PieOptions)42;
Now while you should not write such obviously wrong code the need to convert between integers and enum
values is common - e.g. when storing "enum" values in the tag field of UI controls - and thus the room for errors. C-style enumerations have to be used with discipline, and used that way are a good aid to program correctness and readability.
In the same way StringConstants
must be used with discipline, no casting arbitrary strings - the equivalent of the 42 example above - and with discipline they have similar advantages and disadvantages to standard enumerations.
With the simple discipline of not casting arbitrary strings to StringConstants
; something which is allowed only in the StringConstants
implementation itself; this type gives you a completely safe "enumeration of string values" with compile-time warnings if used incorrectly.
And if you trust me you can stop reading now...
Addendum: Digging Deeper (Just Curious or We Don't Trust You)
To understand why StringConstants
is completely safe (even adding additional values isn't really unsafe, though it may of course cause a program logic failure) we'll go through a number of issues about the nature of object-oriented programming, dynamic typing and Objective-C. SOme of what follows isn't strictly necessary to understand why StringConstants
is safe, but you're someone with an inquiring mind aren't you?
Object reference casts don't do anything at runtime
A cast from one object reference type to another is a compile-time statement that the reference should be treated as being to an object of the destination type. It has no effect on the actual referenced object at runtime - that object has an actual type and it doesn't change. In the object-oriented model upcasting, going from a class to one of its superclasses, is always safe, downcasting, going in the reverse direction, may not (not is not) be safe. For this reason downcasting should be protected by tests in the cases it may be unsafe. For example given:
NSArray *one = @[ @{ @"this": @"is", @"a" : @"dictionary" } ];
The code:
NSUInteger len = [one.firstObject length]; // error, meant count, but NO compiler help at all -> runtime error
will fail at runtime. The result type of firstObject
is id
, which means any object type, and the compiler will let you call any method on references typed as id
. The mistakes here are to not check the array bounds and that the retrieved reference is in fact a dictionary. A more bulletproof approach is:
if (one.count > 0)
{
id first = one.firstObject;
if ([first isKindOfClass:[NSDictionary class]])
{
NSDictionary *firstDict = first; // *downcast* to improve compile time checking
NSLog(@"The count of the first item is %lu", firstDict.count);
}
else
NSLog(@"The first item is not a dictionary");
}
else
NSLog(@"The array his empty");
The (invisible) cast is perfectly safe, protected as it is by the isKindOf:
test. Accidentally type firstDict.length
in the above code fragment and you will get a compile-time error.
However you only need to do this if the downcast might be invalid, if it cannot be invalid no tests are required.
Why can you call any method on references types as id
?
This is where Objective-C's dynamic runtime message lookup comes into play. The compiler checks, to the best of its ability, for type errors at compile-time. Then at runtime another check is made - does the referenced object support the called method? If it does not a runtime error is generated - as occurs with the length
example above. When an object reference is typed as id
this is an instruction to the compiler to perform no compile-time check at all and leave it all to the runtime checks.
The runtime check is not checking the type of the referenced object, but instead whether it supports the requested method, which leads us to...
Ducks, NSProxy
, Inheritance, et. al.
Ducks?!
In dynamic typing there is a saying:
If it looks like a duck, swims like a duck, and quacks like a duck, then it is a duck.
In Objective-C terms this means that at runtime if a referenced object supports the set of methods of type A then it is effectively an object of type A regardless of what it real type is.
This feature is used in many places in Objective-C, a notable example is NSProxy
:
NSProxy
is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy
can be used to implement transparent distributed messaging (for example, NSDistantObject
) or for lazy instantiation of objects that are expensive to create.
With NSProxy
you may think you have, say, an NSDictionary
- something which "looks, swims and quacks" like an dictionary - but in fact you've not got one at all. The important points are:
- It does not matter; and
- It is perfectly safe (modulo coding errors if course)
You can view this ability to substitute one object for another as a generalisation of inheritance - with the later you can always use a subclass instance in lieu of a superclass one, with the former you can use any object in place of another as long as it "looks, swims and quacks" like the object it is standing in for.
We've actually gone further than we need, ducks are not really required to understand StringConstants
, so let's get on:
When is a string an instance of NSString
?
Probably never...
NSString
is implemented by a class cluster - a collection of classes which all respond to the same set of methods that NSString
does, i.e. they all quack like NSString
. Now these classes may be subclasses of NSString
, but in Objective-C there is no actual need for them to be.
Furthermore if you think you have an instance of NSString
you may actually have an instance of NSProxy
... But it does not matter. (Well it might impact performance, but it doesn't impact safety or correctness.)
StringConstants
is a subclass of NSString
, so it certainly is an NSString
, except that NSString
instances probably don't exist - every string is actually an instance of some other class from the cluster, which may or may not itself be a subclass of NSString
. But it does not matter!
As long as instances of StringConstants
quack like NSString
s should then they are NSString
s - and all the instances we have defined in the implementation do that as they are strings (of some type, probably __NSCFConstantString
).
Which leaves us with the question is the defining of the StringConstants
constants sound? Which is the same question as:
When is Downcasting Known To Be Always Safe?
First an example of when it is not:
If you have a referenced typed as NSDictionary *
then it is not known to be safe to cast it to NSMutableDictionary *
without first testing whether the reference is to an mutable dictionary.
The compiler will always let you do the cast, you can then at compile-time make calls to mutating methods without error, but at run-time there will be errors. In this case you must test before casting.
Note that the standard test, isKindOf:
, is effectively conservative due to all those ducks. You may actually have a reference to an object which quacks like an NSMutableDictionary
but is not an instance of it - so the cast would be safe but the test will fail.
What makes this cast unsafe in general?
Simple, it is not known whether the reference object responds to the methods that an NSMutableDictionary
does...
Ergo, if you did know that the reference must respond to all the methods of the type you are casting to then the cast is always safe and no test is required.
So how do you know the reference must respond to all the methods of the target type?
Well one case is straightforward: If you have a reference typed as T
you can it to a reference of type S
safely without any checks whatsever if:
S
is a subclass of T
- so it quacks like a T
; and
S
adds no instance state (variables) to T
; and
S
adds no instance behaviour (new methods, overrides etc.) to T
; and
S
overrides no class behaviour
S
may add class new class methods (not overrides) and global variables/constants without violating these requirements.
In other words S
is defined as:
@interface S : T
// zero or more new class methods
// zero or more global variables or constants
@end
@implementation S
// implementation of any added class methods, etc.
@end
And We Made It...
Or did we, anybody still reading?
StringConstants
is by design constructed so that string instances can be cast to it. This should only be done in the implementation, sneaking in additional "enum" constants elsewhere goes against the purpose of this class.
- It is safe, indeed its not even scary :-)
- No actual instances of
StringConstants
are ever created, each constant is an instance of some string class safely at compile-time masquerading as a StringConstants
instance.
- It provides compile-time checking that a string constant is from a pre-determined set of values, it is effectively a "string valued enumeration".
Yet Another Addendum: Enforcing Discipline
You cannot completely automatically enforce the discipline required to code safely in Objective-C.
In particular you cannot have the compiler prevent programmers from casting arbitrary integer values into enum
types. Indeed, due to uses such as the tag fields of UI controls, such casts need to be allowed in certain circumstances - they cannot be totally outlawed.
In the case of StringConstants
we cannot have the compiler prevent the cast from a string everywhere except in the implementation of the class itself, just as with enum
extra "enum" literals can be grafted in. This rule requires discipline.
However if discipline is lacking the compiler can assist with preventing all the ways, other than casting, that can be used to create NSString
values and hence StringConstant
values as it is a subclass. In other words all the initX
, stringX
etc. variations can be marked as unusable on StringConstant
. This is done by simply listing them in the @interface
and adding NS_UNAVAILABLE
You don't need to do this, and the answer above does not, but if you need this assistance to your discipline you can add the declarations below - this list was produced by simply copying from NSString.h
and a quick search & replace.
+ (instancetype) new NS_UNAVAILABLE;
+ (instancetype) alloc NS_UNAVAILABLE;
+ (instancetype) allocWithZone:(NSZone *)zone NS_UNAVAILABLE;
- (instancetype) init NS_UNAVAILABLE;
- (instancetype) copy NS_UNAVAILABLE;
- (instancetype) copyWithZone:(NSZone *)zone NS_UNAVAILABLE;
- (instancetype)initWithCharactersNoCopy:(unichar *)characters length:(NSUInteger)length freeWhenDone:(BOOL)freeBuffer NS_UNAVAILABLE;
- (instancetype)initWithCharacters:(const unichar *)characters length:(NSUInteger)length NS_UNAVAILABLE;
- (instancetype)initWithUTF8String:(const char *)nullTerminatedCString NS_UNAVAILABLE;
- (instancetype)initWithString:(NSString *)aString NS_UNAVAILABLE;
- (instancetype)initWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
- (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0) NS_UNAVAILABLE;
- (instancetype)initWithFormat:(NSString *)format locale:(id)locale, ... NS_FORMAT_FUNCTION(1,3) NS_UNAVAILABLE;
- (instancetype)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0) NS_UNAVAILABLE;
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
- (instancetype)initWithBytes:(const void *)bytes length:(NSUInteger)len encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
- (instancetype)initWithBytesNoCopy:(void *)bytes length:(NSUInteger)len encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)freeBuffer NS_UNAVAILABLE;
+ (instancetype)string NS_UNAVAILABLE;
+ (instancetype)stringWithString:(NSString *)string NS_UNAVAILABLE;
+ (instancetype)stringWithCharacters:(const unichar *)characters length:(NSUInteger)length NS_UNAVAILABLE;
+ (instancetype)stringWithUTF8String:(const char *)nullTerminatedCString NS_UNAVAILABLE;
+ (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
+ (instancetype)localizedStringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
- (instancetype)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
+ (instancetype)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc NS_UNAVAILABLE;
- (instancetype)initWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
- (instancetype)initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
+ (instancetype)stringWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
+ (instancetype)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
- (instancetype)initWithContentsOfURL:(NSURL *)url usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
- (instancetype)initWithContentsOfFile:(NSString *)path usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
+ (instancetype)stringWithContentsOfURL:(NSURL *)url usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
+ (instancetype)stringWithContentsOfFile:(NSString *)path usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;