Search code examples
objective-cobjective-c++objective-c-category

How to compactly initialise a class readonly property of a category in Objective-C?


I have the following header and implementation files where NSURLMyObject is a category of NSURL, and contains a readonly class property myString:

NSURL+MyClass.h
@interface NSURL (NSURLMyClass)

@property(class, readonly) NSString* myString;

@end
NSURL+MyClass.mm
@implementation MyObject

static NSString* _myString;

+ (NSString*) myString 
{
    if (_myString == nil) {
        _myString = @"Hello";
    }
    return _myString;
}

@end

I have seen other threads where either:

  • a property is re-declared readwrite in the implementation and then synthesised
  • or the underscored member variable is accessed directly, but there is no discussion of class properties, nor of class properties of categories.

I believe such class readonly properties are roughly equivalent to static constexpr member variables in modern C++, where initialising them is a one-liner in the header:

MyClass.hxx
#include <string>

using std::literals;

struct MyClass 
{
    static constexpr auto myString = "Hello"s;
};

// access with, for example, `std::println("{}", MyClass::myString);`

Now on to my questions:

  1. Can MyClass.myString be initialised more compactly, similar to C++ without having to declare the static underscore-prefixed variable, the getter, and the test for nullity?

  2. Should I declare an + (instancetype) init method and then initialise myString inside it? Would this not override the provided +init method that NSURL already has?

  3. Is there some other way altogether, i.e. subclass rather than category? I'm not intimately familiar with Objective-C semantics so I'm not sure if an NSURL* returned by some Foundation class can be straightforwardly cast to MyClass* where @interface MyClass : NSURL.


Solution

  • tl;dr:

    1. If you don't need myString to be associated with the NSURL class specifically, and myString is a string literal, the most succinct way to express this would be

      // MyString.h
      extern NSString * const myString;
      
      // MyString.mm
      NSString * const myString = @"Hello";
      
      // OtherFile.mm
      NSString *someThing = myString; // accessed like a global var
      
    2. If you do want or need this to be associated with NSURL and myString is a string literal, you can avoid the intermediate storage:

      + (NSString *)myString {
          return @"Hello";
      }
      
    3. If myString is not a string literal, or would otherwise need to be computed, then the existing pattern you've seen with _myString storage and initialization is the most common and succinct way to do it


    I believe such class readonly properties are roughly equivalent to static constexpr member variables in modern C++, where initialising them is a one-liner in the header:

    This isn't quite right. In Objective-C, properties are a way to succinctly synthesize methods, and offer an alternative syntax for calling those methods. In general, a property called foo:

    1. Creates a foo getter method and a setFoo: setter method, which
    2. Then allows for calling those methods as x.foo[x foo] and x.foo = f[x setFoo:f]

    This is the same for both instance properties and class properties — though the storage semantics are a little different for class properties by default.

    The approximate C++ equivalent to your example would be generating something like

    #include <string>
    
    using std::literals;
    
    struct MyClass 
    {
        static constexpr auto myString() {
            return "Hello"s;
        }
    };
    

    and the very rough equivalent to your Objective-C code would be something like

    struct MyClass
    {
        static auto myString() {
            if (!myString_) {
                myString_ = "Hello";
            }
            
            return *myString_;
        }
        
    private:
        static const char *myString_;
    };
    

    Depending on what you're actually trying to do here, this can both be wasteful, and unnecessary.

    Can MyClass.myString be initialised more compactly, similar to C++ without having to declare the static underscore-prefixed variable, the getter, and the test for nullity?

    Depending on what you're trying to do, you may be able to avoid a property altogether. For example, does myString necessarily need to be associated with NSURL? If not, this is perfectly valid in Objective-C (and the common way to declare string constants):

    // MyString.h
    extern NSString * const myString;
    
    // MyString.mm
    NSString * const myString = @"Hello";
    

    If you do want to reference this data via NSURL.myString, and if the string is a constant, then you can also avoid having to store it altogether:

    + (NSString *)myString 
    {
        return @"Hello";
    }
    

    Literal NSStrings are already stored in your binary in a constant location; if myString never changes, there's no need to add static storage and store a pointer to it there. If the value of myString were computed then it would make sense to store it to avoid recreating the value on every call, but that doesn't sound like what you're looking for.

    With more details, it'll be possible to give a more specific recommendation here.

    Should I declare an + (instancetype) init method and then initialise myString inside it? Would this not override the provided +init method that NSURL already has?

    This would be atypical:

    1. +init would declare a method named init on your class, and init is typically only called on instances of a type to initialize them; calling [NSURL init] would be very out of place
    2. This isn't a common pattern in Objective-C at all: like you've seen, values like this are typically computed as needed and stored on the first call; it's rare to need to explicitly initialize class values like this — what if you forget to call +init and try to access the value?

    Is there some other way altogether, i.e. subclass rather than category? I'm not intimately familiar with Objective-C semantics so I'm not sure if an NSURL* returned by some Foundation class can be straightforwardly cast to MyClass* where @interface MyClass : NSURL.

    This is likely straying even further from what you're looking for — you could subclass NSURL, but subclassing is a mechanism for affecting existing behavior on subtypes, but this doesn't win you anything here. You're not looking to customize anything about NSURL or its instances, just add an additional bit of data to the type.

    Since you're looking to add a class method, instances wouldn't come into play here: if a method returns an NSURL object to you, you won't be able to call myString on it anyway, because myString belongs to the class, not the instance.