开发者

Only perform selector if target supports it?

How do you call optional protocol methods?

@protocol Foo
@optional
- (void) doA;
- (void) doB;
@end

Now we have to check 开发者_运维百科each time we want to call doA or doB:

if ([delegate respondsToSelector:@selector(doA)])
    [delegate performSelector:@selector(doA)];

That’s just silly. I’ve come up with a category on NSObject that adds:

- (void) performSelectorIfSupported: (SEL) selector
{
    if ([self respondsToSelector:selector])
        [self performSelector:selector];
}

…which is not that much better. Do you have a smarter solution, or do you just put up with the conditionals before each call?


I'm not completely sure that I understand your objection to be honest. As far as I can see the code does exactly what you'd expect for an optional method and does it with very little extra verbiage. I don't think your category makes your intent any clearer.

The only change to your first option would be to do it like this:

if ([delegate respondsToSelector:@selector(doA)])
    [delegate doA];


Your second option is equivalent to making the optional method required, and then writing a null implementation of it.

The first method is the correct one. Optional methods are optional for a reason, and your calling code might want to do something different if they are not available.


How about an NSObject category that intercepts the invocation? Using MAObjCRuntime, it would look something like this:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
  id target = [anInvocation target];
  SEL selector = [anInvocation selector];

  for(RTProtocol *protocol in [[target class] rt_protocols])
  {
    // check optional instance methods
    NSArray *methods = [protocol methodsRequired:NO instance:YES];
    for (RTMethod *method in methods)
    {
      if ([method selector] == selector)
      {
        // NSLog(@"target %@'s protocol %@ contains selector %@", target, protocol, NSStringFromSelector(selector));
        // just drop the invocation
        return;
      }
    }
  }

  // selector does not seem to be part of any optional protocol
  // use default NSObject implementation:
  [self doesNotRecognizeSelector:selector];
}

You could easily add checking for incorporated protocols and whatnot, but for the stated case, this should already work.


Your category is ok, but extremely inflexible. Delegate callbacks should almost always include at least one parameter (the calling object), and your approach doesn't allow for parameters. Delegate methods also often return values, and this approach doesn't allow for that either.

As Stephen noted, the correct code should not use performSelector:, but rather just call the method directly. This has the advantage of compile-time checking for typos, particularly if coupled with the "Undeclared Selector" warning option (GCC_WARN_UNDECLARED_SELECTOR), which I highly recommend.

If the typing is a problem, then a solution is a trampoline. The problem is that trampolines are dramatically slower than just making the method call, but they are convenient. For instance, here's an example of what you're talking about. (I haven't tested this; it's stripped down from a more complicated one I use for sending messages to multiple delegates, where this is more worth it).

#import <objc/runtime.h>

@interface RNDelegateTrampoline : NSObject {
@private
    id delegate_;
    Protocol *protocol_;
}
@property (nonatomic, readwrite, assign) id delegate;
@property (nonatomic, readwrite, retain) Protocol *protocol;
- (id)initWithProtocol:(Protocol *)aProtocol delegate:(id)aDelegate;
@end

@implementation RNDelegateTrampoline

- (id)initWithProtocol:(Protocol *)aProtocol delegate:(id)aDelegate {
    if ((self = [super init])) {
        protocol_ = [aProtocol retain];
        delegate_ = aDelegate;
    }
    return self;
}

- (void)dealloc {
    [protocol_ release], protocol_ = nil;
    delegate_ = nil;
    [super dealloc];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    // Look for a required method
    struct objc_method_description desc = protocol_getMethodDescription(self.protocol, selector, YES, YES);
    if (desc.name == NULL) {
        // Maybe it's optional
        desc = protocol_getMethodDescription(self.protocol, selector, NO, YES);
    }
    if (desc.name == NULL) {
        [self doesNotRecognizeSelector:selector];   // Raises NSInvalidArgumentException
        return nil;
    }
    else {
        return [NSMethodSignature signatureWithObjCTypes:desc.types];
    }   
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([[self delegate] respondsToSelector:[invocation selector]]) {
        [invocation invokeWithTarget:[self delegate]];
    }
}
@synthesize delegate = delegate_;
@synthesize protocol = protocol_;
@end

You'd then use it like this:

@property (nonatomic, readwrite, retain) id delegateTramp;

self.delegateTramp = [[[RNDelegateTrampoline alloc] initWithProtocol:@protocol(ThisObjectDelegate) delegate:aDelegate] autorelease];

...

[self.delegateTramp thisObject:self didSomethingWith:x];

Note that we've used id rather than RNDelegateTrampoline as the type of our delegate trampoline. This is important or else you will get compiler warnings for everything you try to send to it. Declaring it as an id overcomes that. Of course it also throws away compile-time warnings if you pass unknown methods to your delegate. You'll still get a run-time exception, though.


The problem with a category for this is that it automatically holds for all calls on NSObject. I would solve it with a macro like the following:

#define BM_PERFORM_IF_RESPONDS(x) { @try { (x); } @catch (NSException *e) { if (![e.name isEqual:NSInvalidArgumentException]) @throw e; }}

To be used as follows:

id <SomeProtocol> delegate = ...;

//Call the optional protocol method
BM_PERFORM_IF_RESPONDS( [delegate doOptionalProtocolMethod:arg] );
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜