Why does “conformsToProtocol” not check for “requi

2019-06-22 09:22发布

问题:

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

回答1:

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;


回答2:

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



回答3:

CRD is right; the compiler tells you about actual conformance, and it should be listened to. If that's being ignored, the runtime doesn't have any built-in method to double-check. Classes maintain internal lists of protocol objects internally; conformsToProtocol: just looks at that.

At the risk that someone is going to come along and tell me to stop fiddling with the #@(%!^& runtime again, if you really truly need to check actual implementation, this is one way you can do so:

#import <objc/runtime.h>

BOOL classReallyTrulyDoesImplementAllTheRequiredMethodsOfThisProtocol(Class cls, Protocol * prtcl)
{
    unsigned int meth_count;
    struct objc_method_description * meth_list;
    meth_list = protocol_copyMethodDescriptionList(p, 
                                                   YES /*isRequired*/,
                                                   YES /*isInstanceMethod*/,
                                                   &meth_count);
    /* Check instance methods */ 
    for(int i = 0; i < meth_count; i++ ){
        SEL methName = meth_list[i].name;
        if( ![class instancesRespondToSelector:methName] ){
            /* Missing _any_ required methods means failure */ 
            free(meth_list);
            return NO;
        }
    }
    free(meth_list);

    meth_list = protocol_copyMethodDescriptionList(p, 
                                                   YES /*isRequired*/,
                                                   NO /*isInstanceMethod*/,
                                                   &meth_count);
    /* Check class methods, if any */
    for(int i = 0; i < meth_count; i++ ){
        SEL methName = meth_list[i].name;
        if( ![class respondsToSelector:methName] ){
            free(meth_list);
            return NO;
        }
    }
    free(meth_list);                          

    return YES;
}

If I had a hammer...



回答4:

All of these answers are good. To them, I would add one more point: calling conformsToProtocol: is almost always a mistake. Because it tells whether the class says that it conforms to the protocol, rather than whether it actually provides specific methods:

  • It is possible to create a class that claims to conform, but does not, by silencing various warnings, resulting in crashes if you assume that a required method exists.
  • It is possible to create a class that conforms to the protocol but does not claim to do so, resulting in methods not getting called on a delegate even though they exist.
  • It can lead to programming errors creeping in when the protocol changes, because your code checks for conformance to a protocol before calling a method that used to be required, but no longer is.

All of these issues can cause unexpected behavior.

IMO, if you want to know if a class handles a method, the safest approach is to explicitly ask it if it handles that method (respondsToSelector:), rather than asking it if it conforms to a protocol that just happens to contain that method.

IMO, conformsToProtocol: should really have been a function in the Objective-C runtime instead of being exposed on NSObject, because it generally causes more problems than it solves.