如何为Objective-C协议提供默认实现?

25

我想指定一个带有可选方法的Objective-C协议。当遵循该协议的类没有实现该方法时,我希望使用一个默认实现来代替它。在协议本身中是否有定义这个默认实现的位置?如果没有,最佳实践是什么,以减少在各处复制粘贴此默认实现?


2
这篇文章提供了一种优雅而新颖的解决方案:使用NSObject上的类别。https://dev59.com/YmIk5IYBdhLWcg3wYtP7?rq=1 - John Henckel
6个回答

19

Objective-C协议没有默认实现功能。它们纯粹是一组方法声明,可以由其他类来实现。在Objective-C中的标准做法是,在调用对象的方法之前,在运行时测试该对象是否响应给定的选择器,使用-[NSObject respondsToSelector:]。如果对象不响应给定的选择器,则不会调用该方法。

您可以通过在调用类中定义一个方法来封装您所需的默认行为,并在对象未通过测试时调用该方法,以实现您想要的结果。

另一种方法是将该方法定义为协议中必须包含的方法,并在可能不想提供特定实现的任何类的超类中提供默认实现。

也许还有其他选项,但总的来说,在Objective-C中并没有特定的标准实践,除了根据我上面第一段所述的方法,如果对象没有实现给定的方法,则不会调用该方法。


17

由于协议不应定义任何实现,因此没有标准方法可用。

由于Objective-C配备了一个整洁的运行时,如果您确实认为需要以这种方式添加行为(并且无法通过继承来实现相同的效果),那么当然可以添加此类行为。

假设您声明了MyProtocol,则只需在协议声明下的.h文件中添加一个同名接口:

@interface MyProtocol : NSObject <MyProtocol>

+ (void)addDefaultImplementationForClass:(Class)conformingClass;

@end

创建相应的实现文件(这里使用MAObjCRuntime使内容易于阅读,但标准运行时函数不需要更多代码):

@implementation MyProtocol

+ (void)addDefaultImplementationForClass:(Class)conformingClass {
  RTProtocol *protocol = [RTProtocol protocolWithName:@"MyProtocol"];
  // get all optional instance methods
  NSArray *optionalMethods = [protocol methodsRequired:NO instance:YES];
  for (RTMethod *method in optionalMethods) {
    if (![conformingClass rt_methodForSelector:[method selector]]) {
      RTMethod *myMethod = [self rt_methodForSelector:[method selector]];
      // add the default implementation from this class
      [conformingClass rt_addMethod:myMethod];
    }
  }
}

- (void)someOptionalProtocolMethod {
  // default implementation
  // will be added to any class that calls addDefault...: on itself
}

然后你只需要调用

[MyProtocol addDefaultImplementationForClass:[self class]];

在符合该协议的类的初始化程序中,所有默认方法将被添加。


曾经我尝试使用https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/EXTConcreteProtocol.h但是未能让它正常工作,而且具体协议部分似乎已经不再维护。所以这让我不确定自己是否在正确的方向上。但我会研究MAObjCRuntime或标准运行时函数。感谢您的指引! - Grav
我提供了一个以下实现,不使用第三方库。请参见https://dev59.com/uG855IYBdhLWcg3wc0Dy#23066691 - John Henckel

4

一种非常有趣的方法是使用运行时。在程序执行的早期阶段,可以执行以下操作:

  1. 枚举所有类,找到实现该协议的类
  2. 检查该类是否实现了一个方法
  3. 如果没有,为该类添加默认实现

这个过程可以很容易地完成。


1
你怎么敢说这是hackish的 ;) - w-m
5
我实际上已经编写了一个完整的、公共领域的模块,来做这件事情。它并不像听起来那么粗糙,因为它使用完全支持和文档化的功能,并且在生产代码中经过了充分的测试。http://code.google.com/p/libextobjc/source/browse/extobjc/Modules/EXTConcreteProtocol.h - Justin Spahr-Summers
好的,我将“hackish”替换为“迷人的”。这样是否能满足你们? - Yuji
也许这不算是黑客行为,但它并不是标准做法,而且在调试时肯定会令人困惑。 - Ryan

2
我同意"w.m."的观点。一个非常好的解决方案是将所有默认实现放入一个接口中(与协议名称相同)。在任何子类的"+initialize"方法中,它可以简单地从默认接口中复制任何未实现的方法到自身中。
以下辅助函数适用于我:
#import <objc/runtime.h>

// Get the type string of a method, such as "v@:".
// Caller must allocate sufficent space. Result is null terminated.
void getMethodTypes(Method method, char*result, int maxResultLen)
{
    method_getReturnType(method, result, maxResultLen - 1);
    int na = method_getNumberOfArguments(method);
    for (int i = 0; i < na; ++i)
    {
        unsigned long x = strlen(result);
        method_getArgumentType(method, i, result + x, maxResultLen - 1 - x);
    }
}

// This copies all the instance methods from one class to another
// that are not already defined in the destination class.
void copyMissingMethods(Class fromClass, Class toClass)
{
    // This gets the INSTANCE methods only
    unsigned int numMethods;
    Method* methodList = class_copyMethodList(fromClass, &numMethods);
    for (int i = 0; i < numMethods; ++i)
    {
        Method method = methodList[i];
        SEL selector = method_getName(method);
        char methodTypes[50];
        getMethodTypes(method, methodTypes, sizeof methodTypes);

        if (![toClass respondsToSelector:selector])
        {
            IMP methodImplementation = class_getMethodImplementation(fromClass, selector);
            class_addMethod(toClass, selector, methodImplementation, methodTypes);
        }
    }
    free(methodList);
}

然后你可以在类初始化器中调用它,例如...
@interface Foobar : NSObject<MyProtocol>  
@end

@implementation Foobar
+(void)initialize
{
    // Copy methods from the default
    copyMissingMethods([MyProtocol class], self);
}
@end

Xcode会提示Foobar缺少方法,但可以忽略这些警告。

此技术仅复制方法,而不是实例变量。如果这些方法访问不存在的数据成员,则可能导致奇怪的错误。您必须确保数据与代码兼容。就像从Foobar重新解释为MyProtocol一样。


1
正如Ryan所提到的,协议没有默认实现。除了在超类中实现之外,另一种选择是实现一个“处理程序”类,该类可以包含在任何想要提供默认实现的类中,然后适当的方法调用默认处理程序的实现。

1
我最终创建了一个宏,其中包含方法的默认实现。
我在协议的头文件中定义了它,然后在每个实现中只需要一行代码即可。
这样,我就不必在多个地方更改实现,而且它是在编译时完成的,因此不需要运行时魔法。

1
宏?噓,嘘。最佳答案來自"w.m."。你應該創建一個包含所有默認實現的類。同時提供一個工具(例如addDefaultImplementationForClass),用於複製默認方法(使用class_addMethod)。在每個類的初始化中調用這個工具。考慮到Objective-C是LISP的後代,這種做法並不像黑客那樣。 - John Henckel
我同意宏不是一个好的解决方案,并且利用运行时是Objective-C的惯用法。实际上,我已经放弃了宏,目前我正在使用我的协议规定的帮助对象。但这违反了最小知识原则,所以我认为我会考虑w.m.的解决方案。 - Grav

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接