Pomiń ostrzeżenie „Kategoria implementuje metodę, która będzie również implementowana przez jej klasę podstawową”

98

Zastanawiałem się, jak zdusić ostrzeżenie:

Kategoria implementuje metodę, która będzie również implementowana przez klasę podstawową.

Mam to dla określonej kategorii kodu:

+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}
Doz
źródło
Metoda swizzling. Chociaż nie zrobiłbym tego - być może mógłbyś stworzyć podklasę UIFont, która zamiast tego zastępuje tę samą metodę, i wywołać superinaczej.
Alan Zeino,
4
Twój problem nie jest ostrzeżeniem. Twój problem polega na tym, że masz tę samą nazwę metody, co prowadzi do problemów.
gnasher729
Zobacz Przesłanianie metod przy użyciu kategorii w Objective-C , aby zapoznać się z powodami, dla których nie należy zastępować metod przy użyciu kategorii, oraz alternatywnymi rozwiązaniami.
Senseful
Jeśli znacie bardziej eleganckie rozwiązanie do ustawiania czcionki dla całej aplikacji, naprawdę chciałbym to usłyszeć!
To1ne

Odpowiedzi:

64

Kategoria umożliwia dodawanie nowych metod do istniejącej klasy. Jeśli chcesz ponownie zaimplementować metodę, która już istnieje w klasie, zazwyczaj tworzysz podklasę zamiast kategorii.

Dokumentacja Apple: dostosowywanie istniejących klas

Jeśli nazwa metody zadeklarowanej w kategorii jest taka sama, jak metoda w oryginalnej klasie lub metoda w innej kategorii tej samej klasy (lub nawet nadklasy), zachowanie jest niezdefiniowane co do tego, która implementacja metody jest używana w runtime.

Dwie metody z dokładnie taką samą sygnaturą w tej samej klasie doprowadziłyby do nieprzewidywalnego zachowania, ponieważ każdy obiekt wywołujący nie może określić, której implementacji chce.

Dlatego należy albo użyć kategorii i podać nazwy metod, które są nowe i unikalne dla klasy, albo podklasę, jeśli chcesz zmienić zachowanie istniejącej metody w klasie.

bneely
źródło
1
Całkowicie zgadzam się z tymi pomysłami wyjaśnionymi powyżej i staram się ich przestrzegać podczas tworzenia. ale nadal mogą zdarzyć się przypadki, w których zastąpienie metod z kategorii może być odpowiednie. na przykład przypadki, w których można użyć wielokrotnego dziedziczenia (jak w C ++) lub interfejsów (jak w C #). Właśnie z tym spotkałem się w moim projekcie i zdałem sobie sprawę, że nadrzędne metody w kategoriach są najlepszym wyborem.
peetonn
4
Może to być przydatne podczas testowania jednostkowego kodu, który zawiera singleton. Najlepiej byłoby, gdyby singletony były wstrzykiwane do kodu jako protokół, umożliwiając przełączanie implementacji. Ale jeśli masz już jeden osadzony w swoim kodzie, możesz dodać kategorię singletona w swoim teście jednostkowym i zastąpić sharedInstance i metody, które chcesz kontrolować, aby przekształcić je w obiekty fikcyjne.
bandejapaisa
Dzięki @PsychoDad. Zaktualizowałem link i dodałem cytat z dokumentacji, który dotyczy tego postu.
bneely
Wygląda dobrze. Czy firma Apple udostępnia dokumentację dotyczącą zachowania przy użyciu kategorii z istniejącą nazwą metody?
jjxtra,
1
super, nie byłem pewien, czy powinienem wybrać kategorię lub podklasę :-)
kernix
343

Chociaż wszystko, co zostało powiedziane, jest poprawne, w rzeczywistości nie odpowiada to na pytanie, jak usunąć ostrzeżenie.

Jeśli z jakiegoś powodu musisz mieć ten kod (w moim przypadku mam HockeyKit w swoim projekcie i zastępują one metodę w kategorii UIImage [edytuj: to już nie jest przypadek]) i musisz skompilować swój projekt , możesz użyć #pragmainstrukcji, aby zablokować ostrzeżenie w następujący sposób:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

// do your override

#pragma clang diagnostic pop

Znalazłem informacje tutaj: http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html

Ben Baron
źródło
Wielkie dzięki! Widzę więc, że pragmy mogą również tłumić ostrzeżenia. :-p
Constantino Tsarouhas
Tak, i chociaż są to instrukcje specyficzne dla LLVM, istnieją również podobne instrukcje dla GCC.
Ben Baron,
1
Ostrzeżenie w projekcie testowym jest ostrzeżeniem konsolidatora, a nie ostrzeżeniem kompilatora llvm, dlatego pragma llvm nic nie robi. Jednak zauważysz, że projekt testowy nadal kompiluje się z włączoną opcją „Traktuj ostrzeżenia jako błędy”, ponieważ jest to ostrzeżenie konsolidatora.
Ben Baron,
12
To naprawdę powinna być akceptowana odpowiedź, biorąc pod uwagę, że faktycznie odpowiada na pytanie.
Rob Jones
1
Ta odpowiedź powinna być poprawna. W każdym razie ma więcej głosów niż wybrana jako odpowiedź.
Juan Catalan
20

Lepszą alternatywą (zobacz odpowiedź Bneely na temat tego, dlaczego to ostrzeżenie chroni Cię przed katastrofą) jest użycie zawijania metod. Używając swizzlingu metod, możesz zastąpić istniejącą metodę z kategorii bez niepewności, kto „wygrywa”, zachowując jednocześnie możliwość wywołania starej metody. Sekret polega na nadaniu przesłonięciu innej nazwy metody, a następnie zamianie ich za pomocą funkcji wykonawczych.

#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);
}

Następnie zdefiniuj własną implementację:

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

Zastąp domyślną implementację swoją:

MethodSwizzle([UIFont class], @selector(systemFontOfSize:), @selector(mySystemFontOfSize:));
Sanjit Saluja
źródło
10

Spróbuj tego w swoim kodzie:

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

UPDATE2: Dodaj to makro

#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
Vitaliy Gervazuk
źródło
1
To nie jest pełny przykład. Żadne makro o nazwie EXCHANGE_METHOD nie jest w rzeczywistości zdefiniowane przez środowisko wykonawcze Objective-c.
Richard J. Ross III
@Vitaly stil -1. ta metoda nie jest zaimplementowana dla typu klasy. Jakiego frameworka używasz?
Richard J. Ross III
Przepraszam jeszcze raz, wypróbuj to utworzyłem plik NSObject + MethodExchange
Vitaliy Gervazuk
Z kategorią na NSObject po co zawracać sobie głowę makrem? Dlaczego po prostu nie zaznajomić się z „ExchangeMethod”?
hvanbrug
5

Możesz użyć swizzling metody, aby pominąć to ostrzeżenie kompilatora. Oto jak zaimplementowałem swizzling metod do rysowania marginesów w UITextField, gdy używamy niestandardowego tła z 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
źródło
2

Właściwości zastępujące są ważne dla rozszerzenia klasy (kategoria anonimowa), ale nie dla zwykłej kategorii.

Zgodnie z dokumentami Apple Docs przy użyciu rozszerzenia klasy (kategorii anonimowej) można utworzyć interfejs prywatny do klasy publicznej, tak aby interfejs prywatny mógł przesłonić właściwości udostępnione publicznie. tzn. możesz zmienić właściwość z tylko do odczytu na tylko do odczytu.

Przykładem użycia jest sytuacja, gdy piszesz biblioteki, które ograniczają dostęp do właściwości publicznych, podczas gdy ta sama właściwość wymaga pełnego dostępu do odczytu i zapisu w bibliotece.

Łącze do dokumentów Apple: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

Wyszukaj „ Użyj rozszerzeń klas, aby ukryć informacje prywatne ”.

Więc ta technika jest ważna dla rozszerzenia klasy, ale nie dla kategorii.

Kris Subramanian
źródło
1

Kategorie są dobre, ale można je nadużywać. Pisząc kategorie, powinieneś z zasady NIE wdrażać ponownie metod wyjścia. Może to spowodować dziwny efekt uboczny, ponieważ teraz ponownie piszesz kod, od którego zależy inna klasa. możesz złamać znaną klasę i skończyć wywracając debugger na lewą stronę. To po prostu złe programowanie.

Jeśli chcesz to zrobić, naprawdę powinieneś utworzyć podklasę.

Potem sugestia swizzlingu, to dla mnie duże NIE-NIE-NIE.

Przesuwanie go w czasie wykonywania jest kompletne NIE-NIE-NIE.

Chcesz, aby banan wyglądał jak pomarańcza, ale tylko w czasie wykonywania? Jeśli chcesz pomarańczę, napisz pomarańczę.

Nie nadawaj bananowi wyglądu i zachowuj się jak pomarańcza. A co gorsza: nie zamieniaj swojego banana w tajnego agenta, który po cichu sabotuje banany na całym świecie, wspierając pomarańcze.

Yikes!

Leander
źródło
3
Swizzing w czasie wykonywania może być jednak przydatny do naśladowania zachowań w środowisku testowym.
Ben G
2
Chociaż twoja odpowiedź jest zabawna, tak naprawdę nie robi nic poza stwierdzeniem, że wszystkie możliwe metody są złe. Natura bestii polega na tym, że czasami naprawdę nie możesz podklasy, więc pozostaje Ci kategoria, a zwłaszcza jeśli nie jesteś właścicielem kodu klasy, którą kategoryzujesz, czasami musisz wykonać zawijanie, a to bycie metodą niepożądaną nie ma znaczenia.
hvanbrug
1

Miałem ten problem, gdy zaimplementowałem metodę delegata w kategorii, a nie w klasie głównej (mimo że nie było implementacji klasy głównej). Rozwiązaniem dla mnie było przeniesienie z głównego pliku nagłówkowego klasy do pliku nagłówkowego kategorii. Działa to dobrze

gheese
źródło