2

I am looking at a code base which is full of

NSString *const kTabChart = @"Charts";
NSString *const kTabNews = @"News";

and then

setSelectedTab:(NSString *)title;
...
someThingElse:(NSString *)title;

So these weak-typed NSString go far and all the way around the code and this just irritates my eyes. Enums would be better to some extent, but enums would not have the names available programmatically and I do not want to define all unrelated tab names from different views within the same enum {}

I wonder if there is a better way? I am dreaming of a way to make it something like

@interface PageTitle:NSSting;
PageTitle kTabChart = /some kind of initializer with @"Chart"/;
PageTitle kTabNews = /some kind of initializer with @"News"/;

I suspect that this would not play well with the whole "not a compile-time constant" constraint, but I wonder if there are tricks/patters/hacks to define constants of my own class type.

AndreyB
  • 139
  • 1
  • 1
  • 6
  • Add class methods in yours PageTitle, that returns yours constants. And btw. you can't subclass NSString. – Cy-4AH Jun 15 '15 at 19:48
  • It is possible to subclass `NSString` but is generally a bad choice, rarley done and not easy. – zaph Jun 15 '15 at 20:01
  • 1
    That's really what enums are for. And those strings for the UI don't belong there anyway. – Eiko Jun 15 '15 at 20:47

3 Answers3

0

You should use NS_TYPED_ENUM as this makes it into a rawVlaue. More info https://developer.apple.com/documentation/swift/objective-c_and_c_code_customization/grouping_related_objective-c_constants

doozMen
  • 690
  • 1
  • 7
  • 14
-1

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

  1. Is it safe at all - yes by design, trust me (for now); and
  2. 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:

  1. It does not matter; and
  2. 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 NSStrings should then they are NSStrings - 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:

  1. S is a subclass of T - so it quacks like a T; and
  2. S adds no instance state (variables) to T; and
  3. S adds no instance behaviour (new methods, overrides etc.) to T; and
  4. 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?

  1. 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.
  2. It is safe, indeed its not even scary :-)
  3. 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.
  4. 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;
CRD
  • 52,522
  • 5
  • 70
  • 86
  • 2
    Hmmm... subclassing NSString is a difficult matter, even for this "simple" task. Casting NSString to your subclass doesn't make it one, so it's pointer is now spreading a lie about its content, which opens a good can of worms for the future. Also, NSString is a class cluster, making it even harder to subclass. I think this approach, although a "quick fix", makes things much worse in the long run, is a heavy abuse of the language and should be considered a big NO-NO with a big fat red flag. – Eiko Jun 15 '15 at 20:41
  • @Eiko - In this case you needn't worry. There never is any instance of `StringConstant`, they are all just `NSString` (or whatever actual class from the class cluster is used). Ask one it's type an it won't say `StringConstant` - its pointer tells no more of a "lie" than an `NSString *` does. The whole of Objective-C works like this (in particular think `id` and class clusters). What this gives you is compile-time warnings without changing the run-time dynamic typing at all. – CRD Jun 15 '15 at 21:29
  • @Eiko - BTW If you *really* want to lock it down further add `-init`, `+new`, `+alloc` `-copy` and all the other {init,copy,alloc,string}* methods tagged `NS_UNAVAILABLE` so instances cannot be created. (Just copy them out of `NSString`, add the `NS_UNAVAILABLE`, and place in the header - no implementations are required. – CRD Jun 15 '15 at 21:30
  • There's actually a huge difference. Every 'NSString' is actually an instance or a subclass of it, but the StringConstant isn't. It's down casting to the wrong class. This is absolutely *not* how NSString, id, or anything other in Obj-C works. Look at string constants, they are typically of type __NSCFConstantString->__NSCFString->NSMutableString->NSString. Every dog is an animal, but here you're telling that some animal is a cat although it isn't. – Eiko Jun 16 '15 at 06:37
  • Why you covered constants with `@interface` and `@implementation`? It's useless. – Cy-4AH Jun 16 '15 at 12:14
  • @Cy-4AH - You are correct, it has no physical effect, its just a personal preference - it suggests "belonging" in lieu of real class constants. – CRD Jun 16 '15 at 18:01
  • @Eiko - Added an addendum, hopefully this puts your concerns to rest. This code is *safe*. – CRD Jun 16 '15 at 21:09
  • It is an ugly hack. It's as easy as '[yourStringConstant isKindOfClass:[StringConstant class] == NO'. Put an assert around it, and it breaks. It may break as soon as some other "clever" boy does some fooling around with that class. Adding a category on your new class? Sorry, won't work. It breaks the only promise it makes - it's class. It's probably working ok for now if nothing fancy is going on. It may break silently in the future when the compiler makes more speculative assumptions about the object because of the wrong promises. I'm sure you wanted to do good things, but it's just so wrong. – Eiko Jun 16 '15 at 21:19
  • @Eiko - Its an *enumeration* - can you add a category to an enumeration? Are there other classes you cannot easily extend? It adds a useful compile time check to a set of constants when used as designed. It is safe. The compiler is not going to break it (without breaking Obj-C). It is not the epitome of programming art, its hardly "clever". If you want to make a true `NSString` subclass you can - it's actually not hard - and add categories to your heart's content to that. You are of course entitled not to like it. Beauty (and hence ugliness) is in the eye of the beholder. Have a nice day :-) – CRD Jun 16 '15 at 21:39
-3

Have you thought about the #define macro?

#define kTabChart @"Charts"

During the pre-processing step of compilation, the compiler will swap out all kTabChart w/ your desired constant.

If you want constants of your own custom class, then you will have to use const as user @JeremyP says in the linked answer

Community
  • 1
  • 1
A O
  • 5,516
  • 3
  • 33
  • 68
  • 2
    Macros are not type-safe, should rarley be used and in general a very poor choice. – zaph Jun 15 '15 at 20:03
  • Hm I didn't know that-- I just figured it was type safe because the compiler won't let you assign to the macro. Could you give me an example of where you could modify the underlying type of the macro? I'm having a hard time wrapping my head around it – A O Jun 15 '15 at 20:52
  • #define does not have a type, it is essentially just a text substitution. In this case the compiler can make a type determinatioin but in general no. In the example `#define x 3` x and 3 could be used as a char, short, long, float, etc. With `NSString *const kTabChart = @"Charts";` kTabChart directly has a type. – zaph Jun 15 '15 at 21:06
  • so kTabChart is type-safe, but in general #define is not if you don't specify an underlying type? – A O Jun 15 '15 at 21:11
  • Type-safe means that the compiler can verify that types match. In general the use of #define is discouraged and many languages do not support it sich as Swift. – zaph Jun 15 '15 at 21:14
  • ahhh okay I get it now, I think I was taking the definition too literally. thanks! – A O Jun 15 '15 at 21:15