6

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>.)

Community
  • 1
  • 1
sapi
  • 9,944
  • 8
  • 41
  • 71
  • Rather than casting it to a protocol, you should change the class, itself, to just conform to that protocol (e.g. `@interface MyClass ` in the .h, or `@interface MyClass () ` in the .m. Then the compiler will allow autocomplete with that protocol's methods and no casting is needed. It's generally better to fix the object or class definition than it is to cast it. Also, by defining the class to conform to a particular protocol, the compiler will also warn you if you failed to implement some required methods of the protocol. – Rob Dec 29 '12 at 03:01
  • Thanks, but maybe I wasn't clear enough. cls is a class object (an object of type Class), not a class I've defined (in other words, it's passed in as [SomeClass class]). In this simple example, if arg != nil, then I know that cls is a class which implements Protocol. I just want Xcode to realise that. – sapi Dec 29 '12 at 03:33
  • I'm very surprised you don't get compiler warnings here because, first that you're comparing a BOOL to a pointer (passing an `arg` of `nil` is the same as passing `FALSE`), and second that you're not returning anything from this method. Perhaps you're using an older version of the compiler which doesn't generate the warning, or perhaps you've turned off some warnings. Your construct makes sense if `arg` was `id` or a pointer, but not if `BOOL`. Also, you're not returning `instance`. When I compile your code, I get warnings for both of those. – Rob Dec 29 '12 at 07:11
  • That code obviously isn't the whole function, it's just the first few lines. arg was meant to be id, but I copied it over incorrectly. The only bit I'm concerned about is the class cast. – sapi Dec 29 '12 at 08:57

2 Answers2

3

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;
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Hi Rob, I know that cls conforms to the given protocol already (it's a precondition of the function that if arg is not nil then cls will conform). What I'm after is a way to make that clear to someone who reads my code *without* testing for conformance explicitly (as in the question I linked to above). I'm really just curious whether Class< Protocol> is a valid cast, and if not, what the valid class would be – sapi Dec 29 '12 at 08:59
  • @sapi `Class` is valid, though unnecessary and atypical. But if you want to perform this cast, this syntax is appropriate. – Rob Dec 29 '12 at 15:13
  • As an aside, the question to which you refer _does_ explicitly test for conformance (which is good, defensive programming), and only casts the pointer to a protocol only to prevent the compiler warning. With all respect, neither of those two conditions hold here. You're requiring a future developer to look at the details of an implementation to infer what class should be passed, but you don't gracefully handle the scenario in which they accidentally pass the wrong class. I would argue for either `conformsToProtocol` or `respondsToSelector` (whether in conjunction with your cast or in lieu of). – Rob Dec 29 '12 at 15:14
  • With all respect, I'm going to have to disagree with that. If I'm specifying a precondition for a function, then I'm not going to test it at runtime (except with assertions). There is nothing sensible this function, or the app, can do if the precondition is not met, so the overhead of a protocol check is not useful. – sapi Jan 03 '13 at 02:15
  • @sapi Fair enough. We'll agree to disagree on that one. :) – Rob Jan 03 '13 at 03:04
0

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.

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49