Cảnh báo loại bỏ "Danh mục đang triển khai một phương thức cũng sẽ được thực hiện bởi lớp chính của nó"


98

Tôi đã tự hỏi làm thế nào để ngăn chặn cảnh báo:

Category đang triển khai một phương thức cũng sẽ được thực hiện bởi lớp chính của nó.

Tôi có cái này cho một danh mục mã cụ thể:

+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}

Bằng phương pháp swizzling. Mặc dù tôi sẽ không làm điều đó - có lẽ bạn có thể tạo một lớp con UIFont thay vào đó ghi đè cùng một phương thức và gọi supercách khác.
Alan Zeino

4
Vấn đề của bạn không phải là cảnh báo. Vấn đề của bạn là bạn có tên phương thức giống nhau, điều này sẽ dẫn đến sự cố.
gnasher729

Xem Ghi đè các phương pháp sử dụng danh mục trong Objective-C để biết lý do tại sao bạn không nên ghi đè các phương pháp sử dụng danh mục và để biết các giải pháp thay thế.
Senseful

Nếu mọi người biết một giải pháp thanh lịch hơn để đặt phông chữ cho toàn ứng dụng, tôi thực sự muốn nghe nó!
To1ne

Câu trả lời:


64

Một danh mục cho phép bạn thêm các phương thức mới vào một lớp hiện có. Nếu bạn muốn thực hiện lại một phương thức đã tồn tại trong lớp, bạn thường tạo một lớp con thay vì một thể loại.

Tài liệu của Apple: Tùy chỉnh các lớp hiện có

Nếu tên của một phương thức được khai báo trong một danh mục giống với một phương thức trong lớp ban đầu hoặc một phương thức trong một danh mục khác trên cùng một lớp (hoặc thậm chí là một lớp cha), thì hành vi không được xác định về việc triển khai phương thức nào được sử dụng tại thời gian chạy.

Hai phương thức có cùng một chữ ký trong cùng một lớp sẽ dẫn đến hành vi không thể đoán trước, bởi vì mỗi người gọi không thể chỉ định việc triển khai nào họ muốn.

Vì vậy, bạn nên sử dụng một danh mục và cung cấp các tên phương thức mới và duy nhất cho lớp hoặc lớp con nếu bạn muốn thay đổi hành vi của một phương thức hiện có trong một lớp.


1
tôi hoàn toàn đồng ý với những ý tưởng được giải thích ở trên và cố gắng làm theo chúng trong quá trình phát triển. nhưng vẫn có thể có những trường hợp, trong đó các phương thức ghi đè trong danh mục có thể phù hợp. ví dụ: các trường hợp có thể sử dụng đa kế thừa (như trong c ++) hoặc giao diện (như trong c #). vừa phải đối mặt với điều đó trong dự án của tôi và nhận ra rằng các phương pháp ghi đè trong danh mục là lựa chọn tốt nhất.
peetonn

4
Nó có thể hữu ích khi đơn vị kiểm tra một số mã có một singleton trong đó. Tốt nhất, các singleton nên được đưa vào mã như một giao thức, cho phép bạn chuyển đổi việc triển khai. Nhưng nếu bạn đã có một mã được nhúng bên trong mã của mình, bạn có thể thêm một danh mục của singleton trong bài kiểm tra đơn vị của mình và ghi đè sharedInstance và các phương pháp bạn cần kiểm soát để biến chúng thành đối tượng giả.
bandejapaisa

Cảm ơn @PsychoDad. Tôi đã cập nhật liên kết và thêm trích dẫn từ tài liệu có liên quan đến bài đăng này.
bneely

Có vẻ tốt. Apple có cung cấp tài liệu về hành vi sử dụng danh mục có tên phương thức hiện có không?
jjxtra 22/09/13

1
tuyệt vời, không chắc liệu tôi có nên đi với danh mục hay phân lớp :-)
kernix

343

Mặc dù mọi thứ được đưa ra đều đúng, nhưng nó không thực sự trả lời câu hỏi của bạn về cách ngăn chặn cảnh báo.

Nếu bạn phải có mã này vì lý do nào đó (trong trường hợp của tôi, tôi có HockeyKit trong dự án của mình và chúng ghi đè một phương thức trong danh mục UIImage [sửa: đây không còn là trường hợp nữa]) và bạn cần phải biên dịch dự án của mình , bạn có thể sử dụng các #pragmacâu lệnh để chặn cảnh báo như sau:

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

// do your override

#pragma clang diagnostic pop

Tôi tìm thấy thông tin ở đây: http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html


Cảm ơn rất nhiều! Vì vậy, tôi thấy pragmas cũng có thể ngăn chặn các cảnh báo. :-p
Constantino Tsarouhas

Đúng, và mặc dù đây là những tuyên bố cụ thể của LLVM, nhưng cũng có những tuyên bố tương tự đối với GCC.
Ben Baron

1
Cảnh báo trong dự án thử nghiệm của bạn là cảnh báo trình liên kết, không phải cảnh báo trình biên dịch llvm, do đó pragma llvm không làm gì cả. Tuy nhiên, bạn sẽ nhận thấy rằng dự án thử nghiệm của bạn vẫn được xây dựng với chế độ "coi cảnh báo là lỗi" được bật vì đó là cảnh báo của trình liên kết.
Ben Baron

12
Đây thực sự phải là câu trả lời được chấp nhận, vì nó thực sự trả lời câu hỏi.
Rob Jones

1
Câu trả lời này phải là một trong những chính xác. Dù sao nó cũng có nhiều phiếu bầu hơn cái được chọn làm câu trả lời.
Juan Catalan

20

Một giải pháp thay thế tốt hơn (xem câu trả lời của bneely về lý do tại sao cảnh báo này cứu bạn khỏi thảm họa) là sử dụng phương pháp swizzling. Bằng cách sử dụng phương thức swizzling, bạn có thể thay thế một phương thức hiện có từ một danh mục mà không cần chắc chắn ai "thắng" và trong khi vẫn bảo toàn khả năng gọi đến phương thức cũ. Bí quyết là đặt cho phương thức ghi đè một tên khác, sau đó hoán đổi chúng bằng các hàm thời gian chạy.

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

Sau đó, xác định triển khai tùy chỉnh của bạn:

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

Ghi đè triển khai mặc định bằng của bạn:

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

10

Hãy thử điều này trong mã của bạn:

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

UPDATE2: Thêm macro này

#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
Đây không phải là một ví dụ hoàn chỉnh. Không có macro nào có tên EXCHANGE_METHOD thực sự được xác định bởi thời gian chạy target-c.
Richard J. Ross III

@Vitaly stil -1. phương thức đó không được triển khai cho kiểu lớp. Bạn đang sử dụng khuôn khổ nào?
Richard J. Ross III

Xin lỗi một lần nữa, hãy thử này ra tôi tạo ra tập tin NSObject + MethodExchange
Vitaliy Gervazuk

Với danh mục trên NSObject tại sao lại phải bận tâm đến macro? Tại sao không chỉ xoay xở với 'ExchangeMethod'?
hvanbrug

5

Bạn có thể sử dụng phương pháp swizzling để ngăn cảnh báo trình biên dịch này. Đây là cách tôi triển khai phương pháp swizzling để vẽ lề trong UITextField khi chúng tôi sử dụng nền tùy chỉnh với 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

Thuộc tính vượt trội có giá trị cho một Mở rộng lớp (Danh mục ẩn danh), nhưng không hợp lệ cho một Danh mục thông thường.

Theo Apple Docs sử dụng Tiện ích mở rộng lớp (Danh mục ẩn danh), bạn có thể tạo giao diện Riêng tư cho một lớp công khai, sao cho giao diện riêng tư có thể ghi đè các thuộc tính được hiển thị công khai. tức là bạn có thể thay đổi một thuộc tính từ readonly thành readwrite.

Một trường hợp sử dụng cho trường hợp này là khi bạn viết các thư viện hạn chế quyền truy cập vào các thuộc tính công cộng, trong khi cùng một thuộc tính cần toàn quyền truy cập ghi đọc trong thư viện.

Liên kết Apple Docs: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

Tìm kiếm " Sử dụng Tiện ích mở rộng Lớp để Ẩn Thông tin Riêng tư ".

Vì vậy, kỹ thuật này hợp lệ cho một Mở rộng Lớp, nhưng không hợp lệ cho một Danh mục.


1

Danh mục là một điều tốt, nhưng chúng có thể bị lạm dụng. Khi viết danh mục, bạn nên theo nguyên tắc KHÔNG thực hiện lại các phương pháp thoát. Làm như vậy có thể gây ra tác dụng phụ lạ vì bây giờ bạn đang viết lại mã mà một lớp khác phụ thuộc vào một lớp. bạn có thể phá vỡ một lớp đã biết và cuối cùng chuyển trình gỡ lỗi của bạn từ trong ra ngoài. Nó chỉ đơn giản là lập trình tồi.

Nếu bạn cần phải làm điều đó, bạn thực sự nên phân lớp nó.

Sau đó, đề nghị của swizzling, đó là một KHÔNG-KHÔNG-KHÔNG lớn đối với tôi.

Swizing nó trong thời gian chạy là một KHÔNG-KHÔNG-KHÔNG hoàn toàn.

Bạn muốn một quả chuối trông giống như một quả cam, nhưng chỉ trong thời gian chạy? Nếu bạn muốn một quả cam, thì hãy viết một quả cam.

Đừng làm cho một quả chuối trông giống như một quả cam. Và tệ hơn: đừng biến chuối của bạn thành một mật vụ sẽ âm thầm phá hoại chuối trên toàn thế giới để ủng hộ cam.

Rất tiếc!


3
Tuy nhiên, Swizzing trong thời gian chạy có thể hữu ích cho các hành vi chế nhạo trong môi trường thử nghiệm.
Ben G

2
Mặc dù hài hước nhưng câu trả lời của bạn không thực sự làm được nhiều hơn là nói rằng tất cả các phương pháp có thể là xấu. Bản chất của con quái vật là đôi khi bạn thực sự không thể phân loại, vì vậy bạn bị bỏ lại với một danh mục và đặc biệt là nếu bạn không sở hữu mã cho lớp mà bạn đang phân loại, đôi khi bạn cần phải làm điều đó, và nó là một phương pháp không mong muốn là không phù hợp.
hvanbrug

1

Tôi gặp sự cố này khi triển khai phương thức ủy nhiệm trong một danh mục chứ không phải là lớp chính (mặc dù không có triển khai lớp chính). Giải pháp cho tôi là chuyển từ tệp tiêu đề lớp chính sang tệp tiêu đề danh mục Điều này hoạt động tốt

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.