抑制警告“类别正在实现其主类也将实现的方法”

100
我想知道如何抑制警告:

类别正在实现一个方法,该方法也将被其主要类实现。

对于特定代码类别,我有以下代码:
+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}

通过方法交换。虽然我不会这样做——也许你可以创建一个UIFont子类,覆盖相同的方法,否则调用super - Alan Zeino
4
你的问题不在于警告,而是你有相同的方法名称,这会导致问题。 - gnasher729
请参考在Objective-C中使用分类重写方法的原因,以及替代方案,了解为什么不应该使用分类来重写方法。 - Senseful
如果你们知道一种更优雅的解决方案来设置应用程序的全局字体,我真的很想听听! - To1ne
8个回答

347
尽管bneely说的一切都是正确的,但它实际上并没有回答你如何抑制警告的问题。
如果由于某种原因你必须使用这段代码(在我的情况下,我在项目中有HockeyKit,并且它们覆盖了UIImage类别中的一个方法[编辑:这不再是这种情况]),并且你需要使项目编译通过,那么您可以使用#pragma语句来阻止警告,如下所示:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

// do your override

#pragma clang diagnostic pop
我在这里找到了相关信息:http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html

1
你测试项目中的警告是链接器警告,而不是 LLVM 编译器警告,因此 LLVM pragma 没有起到任何作用。但是,你会注意到即使打开了“将警告视为错误”,你的测试项目仍然可以构建,因为它是一个链接器警告。 - Ben Baron
12
鉴于它实际上回答了问题,这确实应该是被接受的答案。 - Rob Jones
那么使用类别来更改方法会有定义行为吗? - jjxtra
我认为从技术上讲,这种行为是未定义的。然而,在实践中,如果您在类别中覆盖了一个方法,它将使用类别实现,但是如果它在不同的类别中被多次覆盖,它可能会使用其中任何一个实现(或者只是最后加载的实现)。因此,通常由于其行为未定义,使用它是不好的实践。 - Ben Baron
1
这个答案应该是正确的。无论如何,它比被选为答案的那个答案获得了更多的投票。 - Juan Catalan
显示剩余11条评论

65

分类(Category)允许你为现有的类添加新的方法。如果你想要重新实现一个已经存在于类中的方法,通常情况下你会创建一个子类而不是一个分类。

苹果公司文档:自定义现有类

如果一个分类中声明的方法和原始类中已经存在的方法名称相同,或者和同一类(甚至是超类)上另一个分类中的方法名称相同,那么在运行时使用哪个方法实现是没有定义的。

在同一个类中拥有两个具有完全相同签名的方法会导致不可预测的行为,因为每个调用者不能指定他们所需要的实现。

因此,如果你想要为类提供新的唯一方法名,应该使用一个分类;如果你想要改变一个类中一个已存在的方法的行为,则应该使用一个子类。


1
我完全同意上述解释的这些想法,并在开发过程中尝试遵循它们。但仍然可能存在一些情况,其中在类别中覆盖方法可能是合适的。例如,在多重继承(如C++)或接口(如C#)可以使用的情况下。我在我的项目中遇到了这种情况,并意识到在类别中覆盖方法是最好的选择。 - peetonn
4
当进行单元测试某些含有单例的代码时,这种方法会很有用。理想情况下,应该将单例作为协议注入到代码中,以便您可以切换实现方式。但是,如果您已经将其嵌入到代码中,则可以在单元测试中添加一个单例的分类,并覆盖 sharedInstance 和您想要控制的方法,将它们变成虚拟对象。 - bandejapaisa
感谢 @PsychoDad。我更新了链接,并添加了一段与此帖子相关的文档引用。 - bneely
看起来不错。苹果公司是否提供有关使用现有方法名称的类别行为的文档? - jjxtra
1
太棒了,我不确定是选择类别还是子类 :-) - kernix
显示剩余2条评论

20

更好的替代方案(查看bneely的答案以了解为什么这个警告会拯救你于水深火热之中)是使用方法交换。通过使用方法交换,您可以替换类别中的现有方法,而无需关注“谁赢”,同时保留调用旧方法的能力。秘密是给覆盖一个不同的方法名称,然后使用运行时函数交换它们。

#import <objc/runtime.h> 
#import <objc/message.h>

void MethodSwizzle(Class c, SEL orig, SEL new) {
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
    method_exchangeImplementations(origMethod, newMethod);
}

然后定义你的自定义实现:

+ (UIFont *)mySystemFontOfSize:(CGFloat)fontSize {
...
}

用你自己的实现覆盖默认实现:

MethodSwizzle([UIFont class], @selector(systemFontOfSize:), @selector(mySystemFontOfSize:));

10

在你的代码中尝试这个:

+(void)load{
    EXCHANGE_METHOD(Method1, Method1Impl);
}

更新2:添加此宏

#import <Foundation/Foundation.h>
#define EXCHANGE_METHOD(a,b) [[self class]exchangeMethod:@selector(a) withNewMethod:@selector(b)]

@interface NSObject (MethodExchange)
+(void)exchangeMethod:(SEL)origSel withNewMethod:(SEL)newSel;
@end

#import <objc/runtime.h>

@implementation NSObject (MethodExchange)

+(void)exchangeMethod:(SEL)origSel withNewMethod:(SEL)newSel{
    Class class = [self class];

    Method origMethod = class_getInstanceMethod(class, origSel);
    if (!origMethod){
        origMethod = class_getClassMethod(class, origSel);
    }
    if (!origMethod)
        @throw [NSException exceptionWithName:@"Original method not found" reason:nil userInfo:nil];
    Method newMethod = class_getInstanceMethod(class, newSel);
    if (!newMethod){
        newMethod = class_getClassMethod(class, newSel);
    }
    if (!newMethod)
        @throw [NSException exceptionWithName:@"New method not found" reason:nil userInfo:nil];
    if (origMethod==newMethod)
        @throw [NSException exceptionWithName:@"Methods are the same" reason:nil userInfo:nil];
    method_exchangeImplementations(origMethod, newMethod);
}

@end

1
这不是一个完整的示例。实际上,Objective-C运行时并没有定义名为EXCHANGE_METHOD的宏。 - Richard J. Ross III
@Vitaly,-1。该类类型尚未实现该方法。您使用的是哪个框架? - Richard J. Ross III
抱歉,再试一下,我创建了文件NSObject+MethodExchange。 - Vitaliy Gervazuk
有了NSObject类别,为什么还要使用宏呢?为什么不直接使用“exchangeMethod”进行方法交换呢? - hvanbrug

5

您可以使用方法交换来抑制此编译器警告。以下是我为UITextField中的自定义背景与UITextBorderStyleNone一起绘制边距实现方法交换的方式:

#import <UIKit/UIKit.h>

@interface UITextField (UITextFieldCatagory)

+(void)load;
- (CGRect)textRectForBoundsCustom:(CGRect)bounds;
- (CGRect)editingRectForBoundsCustom:(CGRect)bounds;
@end

#import "UITextField+UITextFieldCatagory.h"
#import <objc/objc-runtime.h>

@implementation UITextField (UITextFieldCatagory)

+(void)load
{
    Method textRectForBounds = class_getInstanceMethod(self, @selector(textRectForBounds:));
    Method textRectForBoundsCustom = class_getInstanceMethod(self, @selector(textRectForBoundsCustom:));

    Method editingRectForBounds = class_getInstanceMethod(self, @selector(editingRectForBounds:));
    Method editingRectForBoundsCustom = class_getInstanceMethod(self, @selector(editingRectForBoundsCustom:));


    method_exchangeImplementations(textRectForBounds, textRectForBoundsCustom);
    method_exchangeImplementations(editingRectForBounds, editingRectForBoundsCustom);

}


- (CGRect)textRectForBoundsCustom:(CGRect)bounds
{
    CGRect inset = CGRectMake(bounds.origin.x + 10, bounds.origin.y, bounds.size.width - 10, bounds.size.height);
    return inset;
}

- (CGRect)editingRectForBoundsCustom:(CGRect)bounds
{
    CGRect inset = CGRectMake(bounds.origin.x + 10, bounds.origin.y, bounds.size.width - 10, bounds.size.height);
    return inset;
}

@end

2
使用类扩展(匿名类别)可以覆盖属性,但是对于常规类别则无效。
根据Apple文档,使用类扩展(匿名类别)可以创建一个私有接口到公共类,使得私有接口可以覆盖公开的属性。也就是说,可以将只读属性更改为可读写属性。
这种技术的一个用例是编写限制对公共属性访问的库,而同一属性在库内需要完全读写访问权限。
参考Apple文档链接:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html,搜索"Use Class Extensions to Hide Private Information"。
因此,这种技术对于类扩展有效,但对于类别则无效。

1
我在将委托方法实现在类别中而不是主类中时遇到了这个问题(即使没有主类实现)。对我来说,解决方案是将 从主类头文件移动到类别头文件中。这样就可以正常工作了。

0

分类是一件好事,但它们可能会被滥用。 在编写分类时,原则上不应重新实现现有的方法。 这样做可能会导致奇怪的副作用,因为您现在正在重写另一个类依赖的代码。您可能会破坏已知的类,并最终使调试器失效。 这只是糟糕的编程。

如果您需要这样做,确实应该对其进行子类化。

然后是交换建议,对我来说完全不行。

在运行时进行交换是完全不可取的。

你想让香蕉看起来像橙子,但只在运行时? 如果你想要一个橙子,那就写一个橙子。

不要让香蕉看起来和行动像橙子。 更糟糕的是:不要把你的香蕉变成一个秘密特工,默默地支持橙子破坏全球的香蕉。

天啊!


5
在测试环境中,运行时交换可能对模拟行为很有用。 - Ben G
3
尽管幽默,但你的回答实际上只是说所有可能的方法都不好。问题的本质是有时候你真的无法创建子类,因此你只能使用分类,特别是如果你没有拥有你要分类的类的源代码,有时你需要进行方法混合,它是否是一种不受欢迎的方法并不重要。 - hvanbrug

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