Подавить предупреждение «Категория реализует метод, который также будет реализован ее основным классом»

98

Мне было интересно, как подавить предупреждение:

Категория реализует метод, который также будет реализован его основным классом.

У меня есть это для конкретной категории кода:

+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}
Doz
источник
Методом обжаривания. Хотя я бы этого не делал - возможно, вы могли бы создать подкласс UIFont, который вместо этого переопределяет тот же метод и вызывает в superпротивном случае.
Алан Зейно,
4
Ваша проблема не в предупреждении. Ваша проблема в том, что у вас одно и то же имя метода, что может привести к проблемам.
gnasher729
См. Переопределение методов с использованием категорий в Objective-C, чтобы узнать о причинах, по которым вы не должны переопределять методы с использованием категорий, и об альтернативных решениях.
Senseful
Если вы знаете более элегантное решение для установки шрифта для всего приложения, я бы очень хотел его услышать!
To1ne 02

Ответы:

64

Категория позволяет добавлять новые методы в существующий класс. Если вы хотите повторно реализовать метод, который уже существует в классе, вы обычно создаете подкласс вместо категории.

Документация Apple: настройка существующих классов

Если имя метода, объявленного в категории, совпадает с именем метода в исходном классе или метода в другой категории того же класса (или даже суперкласса), поведение не определено относительно того, какая реализация метода используется в время выполнения.

Два метода с одной и той же сигнатурой в одном классе могут привести к непредсказуемому поведению, поскольку каждый вызывающий объект не может указать, какую реализацию они хотят.

Итак, вы должны либо использовать категорию и предоставить имена методов, которые являются новыми и уникальными для класса, либо подкласс, если вы хотите изменить поведение существующего метода в классе.

неприятно
источник
1
Я полностью согласен с изложенными выше идеями и стараюсь следовать им в процессе разработки. но все же могут быть случаи, когда методы переопределения в категории могут быть уместными. например, случаи, когда можно использовать множественное наследование (как в C ++) или интерфейсы (как в C #). только что столкнулся с этим в своем проекте и понял, что методы переопределения в категориях - лучший выбор.
peetonn
4
Это может быть полезно при модульном тестировании кода, в котором есть синглтон. В идеале синглтоны следует внедрять в код как протокол, позволяющий отказаться от реализации. Но если у вас уже есть один встроенный в ваш код, вы можете добавить категорию синглтона в свой модульный тест и переопределить sharedInstance и методы, которыми вы должны управлять, чтобы превратить их в фиктивные объекты.
bandejapaisa
Спасибо @PsychoDad. Я обновил ссылку и добавил цитату из документации, относящейся к этому сообщению.
21
Выглядит хорошо. Предоставляет ли Apple документацию о поведении при использовании категории с существующим именем метода?
jjxtra
1
потрясающе, не был уверен, что лучше
выбрать
343

Хотя все сказанное правильно, на самом деле это не отвечает на ваш вопрос о том, как подавить предупреждение.

Если вам по какой-то причине нужен этот код (в моем случае у меня есть 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

Бен Барон
источник
Большое спасибо! Итак, я вижу, что прагмы тоже могут подавлять предупреждения. :-p
Константино Царухас
Да, и хотя это специфичные для LLVM операторы, есть аналогичные для GCC.
Бен Барон
1
Предупреждение в вашем тестовом проекте - это предупреждение компоновщика, а не предупреждение компилятора llvm, поэтому прагма llvm ничего не делает. Однако вы заметите, что ваш тестовый проект по-прежнему строится с включенной функцией «обрабатывать предупреждения как ошибки», потому что это предупреждение компоновщика.
Бен Барон
12
Это действительно должен быть принятый ответ, учитывая, что он действительно отвечает на вопрос.
Роб Джонс
1
Этот ответ должен быть правильным. В любом случае у него больше голосов, чем у того, кто был выбран в качестве ответа.
Хуан Каталан
20

Лучшая альтернатива (см. Ответ bneely о том, почему это предупреждение спасает вас от катастрофы) - использовать метод swizzling. Используя смену метода, вы можете заменить существующий метод из категории без неопределенности в том, кто «выиграет», и при этом сохраняя возможность вызова старого метода. Секрет в том, чтобы дать переопределению другое имя метода, а затем поменять их местами с помощью функций времени выполнения.

#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
Это не полный пример. Макрос с именем EXCHANGE_METHOD на самом деле не определяется средой выполнения objective-c.
Ричард Дж. Росс III,
@ Виталий стиль -1. этот метод не реализован для данного типа класса. Какой фреймворк вы используете?
Ричард Дж. Росс III,
Извините еще раз, попробуйте это я создал файл NSObject + MethodExchange
Виталий Гервазук
С категорией на NSObject зачем вообще заморачиваться с макросом? Почему бы просто не использовать «exchangeMethod»?
hvanbrug
5

Вы можете использовать swizzling для подавления этого предупреждения компилятора. Вот как я реализовал swizzling метода для рисования полей в 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
Say2Manuj
источник
2

Переопределяющие свойства действительны для расширения класса (анонимная категория), но не для обычной категории.

Согласно Apple Docs, используя расширение класса (анонимная категория), вы можете создать частный интерфейс для общедоступного класса, так что частный интерфейс может переопределить публично открытые свойства. т.е. вы можете изменить свойство с "только чтение" на "чтение-запись".

Пример использования для этого - когда вы пишете библиотеки, которые ограничивают доступ к общедоступным свойствам, в то время как для того же свойства требуется полный доступ для чтения и записи в библиотеке.

Ссылка на Apple Docs: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

Найдите « Используйте расширения класса, чтобы скрыть личную информацию ».

Таким образом, этот метод действителен для расширения класса, но не для категории.

Крис Субраманиан
источник
1

Категории - это хорошо, но ими можно злоупотреблять. При написании категорий вы, как правило, НЕ должны повторно реализовывать методы выхода. Это может вызвать странный побочный эффект, поскольку вы сейчас переписываете код, от которого зависит другой класс. вы можете сломать известный класс и в конечном итоге вывернуть отладчик наизнанку. Это просто плохое программирование.

Если вам нужно это сделать, вам действительно следует создать подкласс.

Тогда предложение выпить, это большой НЕТ-НЕТ-НЕТ для меня.

Свизинг во время выполнения - это полное НЕТ-НЕТ-НЕТ.

Вы хотите, чтобы банан выглядел как апельсин, но только во время выполнения? Если хотите апельсин, напишите апельсин.

Не делайте банан похожим на апельсин. И что еще хуже: не превращайте свой банан в секретного агента, который будет незаметно саботировать бананы по всему миру в поддержку апельсинов.

Ой!

Leander
источник
3
Однако свизинг во время выполнения может быть полезен для имитации поведения в среде тестирования.
Ben G
2
Несмотря на юмористический характер, ваш ответ на самом деле не более чем говорит о том, что все возможные методы плохие. Природа зверя такова, что иногда вы действительно не можете создать подкласс, поэтому у вас остается категория, и особенно если у вас нет кода для класса, который вы классифицируете, иногда вам нужно сделать swizzle, и это быть нежелательным методом не имеет значения.
hvanbrug
1

У меня возникла эта проблема, когда я реализовал метод делегата в категории, а не в основном классе (хотя реализации основного класса не было). Решение для меня заключалось в том, чтобы переместить из главного файла заголовка класса в файл заголовка категории. Это отлично работает

топленое масло
источник