Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does "conformsToProtocol" not check for "required" method implementations?

I am trying to enforce a "formal" @protocol, but cannot reliably test my classes/instances as to whether they ACTUALLY implement the protocol's "required" methods, vs. simply "declaring" that they conform to the protocol.

A complete example of my quandary…

#import <Foundation/Foundation.h>

@protocol       RequiredProtocol 
@required                   
- (NSString*) mustImplement;                              @end 
@interface      Cog         : NSObject <RequiredProtocol> @end
@implementation Cog                                       @end
@interface      Sprocket    : NSObject                    @end 
@implementation Sprocket 
- (NSString*) mustImplement
  { return @"I conform, but ObjC doesn't care!"; }        @end

int main(int argc, char *argv[]) {

    Protocol *required = @protocol(RequiredProtocol);
    SEL    requiredSEL = @selector(mustImplement);
    void (^testProtocolConformance)(NSObject*) = ^(NSObject *x){
        NSLog(@"Protocol:%@\n"
               "Does %@ class conform:%@     \n"
               "Do  instances conform:%@     \n"
               "Required method's result:\"%@\"", 
        NSStringFromProtocol ( required ),
        NSStringFromClass    ( x.class  ), 
        [x.class conformsToProtocol:required] ? @"YES" : @"NO", 
        [x       conformsToProtocol:required] ? @"YES" : @"NO",
        [x    respondsToSelector:requiredSEL] ? [x mustImplement]
                                              : nil );
    };
    testProtocolConformance ( Cog.new      );
    testProtocolConformance ( Sprocket.new );
}

Result:

Protocol:RequiredProtocol
Does Cog class conform:YES
Do instances conform:YES
Required method's result:"(null)"

Protocol:RequiredProtocol
Does Sprocket class conform:NO
Do instances conform:NO
Required method's result:"I conform, but ObjC doesn't care!"

Why is it that a class and it's instances that DO implement the @protocol's methods (Sprocket) return NO to conformsToProtocol?

And why does one that DOESN'T ACTUALLY conform, but SAYS that it DOES (Cog) return YES?

What is the point of a formal protocol if the declaration is all that's needed to feign conformance?

How can you ACTUALLY check for complete implementation of multiple @selectors without MULTIPLE calls to respondsToSelector?

@Josh Caswell.. Without diffing the two.. I'd guess that your response achieves similar effect to the category on NSObject I've been using in the meantime…

@implementation NSObject (ProtocolConformance)
- (BOOL) implementsProtocol:(id)nameOrProtocol {
   Protocol *p = [nameOrProtocol isKindOfClass:NSString.class] 
               ? NSProtocolFromString(nameOrProtocol) 
               : nameOrProtocol;  // Arg is string OR protocol
   Class klass = self.class;
   unsigned int outCount = 0;
   struct objc_method_description *methods = NULL;
   methods = protocol_copyMethodDescriptionList( p, YES, YES, &outCount);
   for (unsigned int i = 0; i < outCount; ++i) {
       SEL selector = methods[i].name;
       if (![klass instancesRespondToSelector: selector]) {
           if (methods) free(methods); methods = NULL; return NO;
       }
    }
    if (methods) free(methods); methods = NULL; return YES;
}
@end
like image 599
Alex Gray Avatar asked May 29 '13 17:05

Alex Gray


2 Answers

Conforming to a protocol is just a "promise", you can't know if the receiver of conformsToProtocol: actually implements all the required methods. Is enough that you declare that the class conforms to the protocol using the angle brackets syntax, and conformsToProtocol: will return yes:

Discussion
A class is said to “conform to” a protocol if it adopts the protocol or inherits from another class that adopts it. Protocols are adopted by listing them within angle brackets after the interface declaration.

Full source: NSObject's conformsToProtocol: .

Protocols declarations have just the advantage that you can know at compile time if a class really adopts that required methods. If not, a warning will be given. I suggest to don't rely on conformsToProtocol:, but to use introspection instead. That is, verify if a class/object implements a method by calling instancesRespondToSelector: / respondsToSelector: :

+ (BOOL)instancesRespondToSelector:(SEL)aSelector;
- (BOOL)respondsToSelector:(SEL)aSelector;
like image 196
Ramy Al Zuhouri Avatar answered Nov 05 '22 20:11

Ramy Al Zuhouri


What compiler are you using? Xcode/Clang issues 2 warnings and 1 error...

Think of a protocol as a club with membership requirements. Asking whether someone is a member of the club, provable by them having a membership card (NSObject<ReqiredProtocol>), should tell you that a person meets those requirements. However the lack of a membership doesn't mean they don't meet the requirements.

E.g. someone (Sprocket) might meet all the requirements to join but choose not to. Someone else (Cog) may failed to meet the requirements but a sloppy administrator might let them in.

The latter is why I asked about the compiler (the sloppy administrator ;-)). Try your code as entered on Xcode 4.6.3/Clang 4.2 produces warnings and errors (as does using GCC 4.2):

  1. The warnings state that Cog fails to implement the required methods;
  2. The error complains about [x mustImplement] as x is not known to have the required method as it is of type NSObject - you need to cast to remove that, just [(id)x mustImplement] will do as you've already tested the method exists.

In summary, you can only rely on conformsToProtocol if you know the originator of the code didn't ignore compiler warnings - the checking is done at compile time.

Addendum

I missed the last sentence of your question. If you wish to discover whether a class meets the requirements of a protocol, even if it doesn't declare that it does, e.g. Sprocket above (or if you are obtaining code from folk who ignore compiler warnings - the Cog author above), then you can do so using the facilities of the Obj-C runtime. And you'll only have to write one call to repsondsToSelector...

I just typed in the following and quickly tested it on your sample. It is not throughly tested by any means, caveat emptor etc. Code assumes ARC.

#import <objc/runtime.h>

@interface ProtocolChecker : NSObject

+ (BOOL) doesClass:(Class)aClass meetTheRequirementsOf:(Protocol *)aProtocol;

@end

@implementation ProtocolChecker

+ (BOOL) doesClass:(Class)aClass meetTheRequirementsOf:(Protocol *)aProtocol
{

   struct objc_method_description *methods;
   unsigned int count;

   // required instance methods
   methods = protocol_copyMethodDescriptionList(aProtocol, YES, YES, &count);
   for (unsigned int ix = 0; ix < count; ix++)
   {
      if (![aClass instancesRespondToSelector:methods[ix].name])
      {
         free(methods);
         return NO;
      }
   }
   free(methods);

   // required class methods
   methods = protocol_copyMethodDescriptionList(aProtocol, YES, NO, &count);
   for (unsigned int ix = 0; ix < count; ix++)
   {
      if (![aClass respondsToSelector:methods[ix].name])
      {
         free(methods);
         return NO;
      }
   }
   free(methods);

   // other protocols
   Protocol * __unsafe_unretained *protocols = protocol_copyProtocolList(aProtocol, &count);
   for (unsigned int ix = 0; ix < count; ix++)
   {
      if (![self doesClass:aClass meetTheRequirementsOf:protocols[ix]])
      {
         free(protocols);
         return NO;
      }
   }
   free(protocols);

   return YES;
}

@end

You should of course want to know exactly how this works, especially the * __unsafe_unretained * bit. That is left as an exercise :-)

like image 43
CRD Avatar answered Nov 05 '22 22:11

CRD