sapi
sapi

Reputation: 10224

How to cast Class object to conformance witih protocol

I'm trying to cast a Class object to a certain protocol, which defines class methods (+) that that class implements.

I know how to do this with (id< protocol>), as outlined in this question, but I can't seem to figure out the right way for Class objects.

The basic scenario is as follows.

I have a protocol:

@protocol Protocol <NSObject>
+ (id)classMethod:(id)arg;
@end

I then have a function which accepts a Class object, which it knows sometimes conforms to the protocol based on another argument (this is obviously very simplified):

- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if (arg != nil) {
        instance = [(Class<Protocol>)cls classMethod:arg];
    }
}

Now I don't get any warnings on this, and it looks right to me. (I'm not going to see any errors in any case, because I can guarantee that if arg != nil then the class conforms.)

However, I'm not getting autocompletion in Xcode, which makes me wonder if this is the right way to do it. Any thoughts? (Note that I am not interested in instance being id< Protocol>.)

Upvotes: 7

Views: 2500

Answers (2)

Aleksandr Medvedev
Aleksandr Medvedev

Reputation: 8978

The question is 11 years old and there is nothing wrong with the Rob's answer, but I find it unfortunate that the centrepiece part of it (whether type-casting a Class object with a protocol is a correct syntax) never got proper attention.

First of all static typing in Objective-C is very artificial thing, and it exists solely for the compiler to emit a warning (not even an error). Let's start with what Class objects really is - if you take a look at the documentation, you will find that the Class type is actually an alias for objc_class * type:

typedef struct objc_class *Class;

You can find definition of objc_class type in the source codes of Apple's objc runtime library:

// inherits objc_object with some adjustments
struct objc_class : objc_object { ... }

As you can see, objc_class is just an extension to a objc_object. Any Objective-C class is in fact instance of this objc_object. E.g. here is how NSObject or id aliases look like:


// "translation" of an Objective-C class declaration
typedef struct objc_object NSObject;

// the same for `id` type but with the pointer type included
typedef struct objc_object *id;

It means that "static typing" doesn't exist in Objective-C, the "typing" of an instance happens via introspection of a given instance (different kind of meta-information objc_object stores). It makes all Objective-C classes compatible with each other (first - because it's a pointer, second - because it's a pointer to the same structure). E.g. you can write code like this:

Class obj = [NSObject new];

..and it will happily compile.

However this purely dynamic nature of the language makes it very error-prone, exposing all kinds of mistakes a programmer can make. In order to avoid that clang in fact does compile time checking of the specified types, but it purely relies on the programmer to provide correct data for a type of an instance, and if the types are incompatible from Objective-C perspective, the compiler can emit a warning for you. This works for instance objects, but unfortunately there is no syntax in Objective-C to type a class object other than with the Class alias. It means that for the compiler all such objects are indistinguishable during compile time.

And all of this is true for protocols typing. Here I mean that when you add a protocol conformance token to a variable type (id<TDWLoadable> var) you merely ask the compiler to check whether the assigned to the variable object conforms to the given protocol:

@protocol TDWLoadable

+ (void)classMethod;
- (void)instanceMethod;

@end

@interface TDWObject : NSObject
@end

// Initializing '__strong id<TDWLoadable>' with an expression of incompatible type 'TDWObject *'
id<TDWLoadable> loadable = [TDWObject new];

For a class object, however, the same check is just ignored, because Class objects cannot be typed:

Class<TDWLoadable> loadable = [[TDWObject new] class];

This behavior is described in the Type Checking section of Protocols part in The Objective-C Programming Language (emphasis mine):

...the declaration

id <Formatting> anObject;

groups all objects that conform to the Formatting protocol into a type, regardless of their positions in the class hierarchy. The compiler can make sure only objects that conform to the protocol are assigned to the type.

In each case, the type groups similar objects—either because they share a common inheritance, or because they converge on a common set of methods.

The two types can be combined in a single declaration:

Formatter <Formatting> *anObject;

Protocols can’t be used to type class objects. Only instances can be statically typed to a protocol, just as only instances can be statically typed to a class. (However, at runtime, both classes and instances respond to a conformsToProtocol: message.)

Also, if we take into account that objc_class is in fact just an extension to objc_object then two expressions of kind:

  • Class<TDWLoadable> classObj;
  • id<TDWLoadable> obj;

Should follow the same contract (i.e. + (void)classMethod has to refer to metaclass of classObj and - (void)instanceMethod to the class object itself).


Having that said, since the syntax essentially has no effect and just ignored by the compiler, you are free to come up with your own convention to the Class<Protocol> typing.

Upvotes: 0

Rob
Rob

Reputation: 437592

If you want to determine whether cls conforms to a particular protocol (and assuming that classMethod: is a required class method of that protocol), you can simply:

- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if ([cls conformsToProtocol:@protocol(Protocol)]) {
        instance = [cls classMethod:arg];
    }

    return instance;
}

Alternatively, just see if it responds to a particular class method selector:

- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if ([cls respondsToSelector:@selector(classMethod:)]) {
        instance = [cls classMethod:arg];
    }

    return instance;
}

Upvotes: 3

Related Questions