ระงับคำเตือน "หมวดหมู่กำลังใช้วิธีการที่คลาสหลักจะนำไปใช้ด้วย"


98

ฉันสงสัยว่าจะระงับคำเตือนได้อย่างไร:

หมวดหมู่กำลังใช้วิธีการซึ่งจะถูกนำไปใช้โดยคลาสหลักด้วย

ฉันมีสิ่งนี้สำหรับหมวดหมู่รหัสเฉพาะ:

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

โดยวิธีการ swizzling แม้ว่าฉันจะไม่ทำอย่างนั้น - บางทีคุณอาจสร้างคลาสย่อย UIFont ที่แทนที่วิธีการเดียวกันแทนและเรียกsuperอย่างอื่น
Alan Zeino

4
ปัญหาของคุณไม่ใช่คำเตือน ปัญหาของคุณคือคุณมีชื่อวิธีการเดียวกันซึ่งจะนำไปสู่ปัญหา
gnasher729

ดูวิธีการลบล้างโดยใช้หมวดหมู่ใน Objective-Cสำหรับเหตุผลที่คุณไม่ควรลบล้างวิธีการโดยใช้หมวดหมู่และสำหรับวิธีแก้ปัญหาอื่น ๆ
Senseful

หากคุณรู้จักวิธีการแก้ปัญหาที่หรูหรากว่าในการตั้งค่าแบบอักษรทั่วทั้งแอปพลิเคชันฉันอยากจะได้ยินมันจริงๆ!
To1ne

คำตอบ:


64

หมวดหมู่ช่วยให้คุณสามารถเพิ่มวิธีการใหม่ให้กับคลาสที่มีอยู่ หากคุณต้องการนำเมธอดที่มีอยู่แล้วไปใช้อีกครั้งโดยทั่วไปคุณจะสร้างคลาสย่อยแทนหมวดหมู่

เอกสารของ Apple: การปรับแต่งคลาสที่มีอยู่

หากชื่อของเมธอดที่ประกาศในหมวดหมู่นั้นเหมือนกับเมธอดในคลาสดั้งเดิมหรือเมธอดในหมวดหมู่อื่นในคลาสเดียวกัน (หรือแม้แต่ซูเปอร์คลาส) พฤติกรรมจะไม่ได้กำหนดว่าจะใช้เมธอดใดที่ รันไทม์

สองวิธีที่มีลายเซ็นเดียวกันแน่นอนในคลาสเดียวกันจะนำไปสู่พฤติกรรมที่คาดเดาไม่ได้เนื่องจากผู้เรียกแต่ละคนไม่สามารถระบุได้ว่าต้องการใช้งานแบบใด

ดังนั้นคุณควรใช้ประเภทและระบุชื่อเมธอดที่ใหม่และไม่ซ้ำกันสำหรับคลาสหรือคลาสย่อยถ้าคุณต้องการเปลี่ยนพฤติกรรมของเมธอดที่มีอยู่ในคลาส


1
ฉันเห็นด้วยอย่างยิ่งกับแนวคิดเหล่านั้นที่อธิบายไว้ข้างต้นและพยายามติดตามพวกเขาในระหว่างการพัฒนา แต่ก็ยังมีบางกรณีที่วิธีการแทนที่ในหมวดหมู่อาจเหมาะสม ตัวอย่างเช่นกรณีที่สามารถใช้การสืบทอดหลายรายการ (เช่นใน c ++) หรืออินเตอร์เฟส (เช่นใน c #) เพิ่งเผชิญกับสิ่งนั้นในโครงการของฉันและตระหนักว่าวิธีการลบล้างในหมวดหมู่เป็นทางเลือกที่ดีที่สุด
peetonn

4
จะมีประโยชน์เมื่อหน่วยทดสอบโค้ดบางโค้ดที่มีซิงเกิลตันอยู่ ตามหลักการแล้ว Singletons ควรถูกแทรกลงในโค้ดเป็นโปรโตคอลเพื่อให้คุณสามารถเปลี่ยนการใช้งาน แต่ถ้าคุณมีโค้ดที่ฝังอยู่แล้วคุณสามารถเพิ่มหมวดหมู่ของซิงเกิลตันในการทดสอบหน่วยของคุณและแทนที่ sharedInstance และวิธีการที่คุณจะควบคุมเพื่อเปลี่ยนให้เป็นวัตถุจำลอง
bandejapaisa

ขอบคุณ @PsychoDad ฉันอัปเดตลิงค์และเพิ่มคำพูดจากเอกสารที่เกี่ยวข้องกับโพสต์นี้
bneely

ดูดี. Apple ให้เอกสารเกี่ยวกับพฤติกรรมการใช้หมวดหมู่ที่มีชื่อวิธีการที่มีอยู่หรือไม่
jjxtra

1
ยอดเยี่ยมไม่แน่ใจว่าฉันควรใช้หมวดหมู่หรือคลาสย่อย :-)
kernix

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


ขอบคุณมาก! ดังนั้นฉันเห็นว่า pragmas สามารถระงับคำเตือนได้เช่นกัน :-p
Constantino Tsarouhas

ใช่และแม้ว่านี่จะเป็นข้อความเฉพาะของ LLVM แต่ก็มีข้อความที่คล้ายกันสำหรับ GCC เช่นกัน
Ben Baron

1
คำเตือนในโครงการทดสอบของคุณคือคำเตือนตัวเชื่อมโยงไม่ใช่คำเตือนคอมไพเลอร์ llvm ดังนั้น llvm pragma จึงไม่ทำอะไรเลย อย่างไรก็ตามคุณจะสังเกตเห็นว่าโครงการทดสอบของคุณยังคงสร้างโดยเปิด "ถือว่าคำเตือนเป็นข้อผิดพลาด" เนื่องจากเป็นคำเตือนตัวเชื่อมโยง
Ben Baron

12
นี่ควรเป็นคำตอบที่ยอมรับได้จริง ๆ เนื่องจากตอบคำถามได้จริง
Rob Jones

1
คำตอบนี้ควรเป็นคำตอบที่ถูกต้อง อย่างไรก็ตามมีคะแนนเสียงมากกว่าคะแนนที่เลือกเป็นคำตอบ
Juan Catalan

20

ทางเลือกอื่นที่ดีกว่า (ดูคำตอบของ bneely ว่าเหตุใดคำเตือนนี้จึงช่วยคุณให้รอดพ้นจากภัยพิบัติ) คือการใช้วิธีการ swizzling ด้วยการใช้วิธีการ 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);
}

UPDATE2: เพิ่มมาโครนี้

#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
Richard J.Ross III

@Vitaly stil -1. วิธีการนั้นไม่ได้ใช้กับประเภทคลาส คุณใช้กรอบอะไร
Richard J.Ross III

ขออภัยอีกครั้งลองใช้ฉันสร้างไฟล์ NSObject + MethodExchange
Vitaliy Gervazuk

ด้วยหมวดหมู่ใน 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

2

คุณสมบัติ Over-riding ใช้ได้กับ Class Extension (Anonymous Category) แต่ไม่ใช่สำหรับ Category ทั่วไป

ตามเอกสารของ Apple โดยใช้ Class Extension (Anonymous Category) คุณสามารถสร้างอินเทอร์เฟซส่วนตัวให้กับคลาสสาธารณะเพื่อให้อินเทอร์เฟซส่วนตัวสามารถแทนที่คุณสมบัติที่เปิดเผยต่อสาธารณะ กล่าวคือคุณสามารถเปลี่ยนคุณสมบัติจากอ่านอย่างเดียวเป็นอ่านเขียน

กรณีการใช้งานสำหรับกรณีนี้คือเมื่อคุณเขียนไลบรารีที่ จำกัด การเข้าถึงคุณสมบัติสาธารณะในขณะที่คุณสมบัติเดียวกันต้องการการเข้าถึงการเขียนแบบอ่านทั้งหมดภายในไลบรารี

ลิงก์ Apple Docs: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

ค้นหา " ใช้ส่วนขยายของชั้นเรียนเพื่อซ่อนข้อมูลส่วนตัว "

ดังนั้นเทคนิคนี้ใช้ได้กับ Class Extension แต่ใช้ไม่ได้กับ Category


1

หมวดหมู่เป็นสิ่งที่ดี แต่สามารถใช้ในทางที่ผิดได้ เมื่อเขียนหมวดหมู่คุณควรเป็นหลักการอย่าใช้วิธีการออกจากระบบซ้ำ การทำเช่นนั้นอาจทำให้เกิดผลข้างเคียงที่แปลกประหลาดเนื่องจากตอนนี้คุณกำลังเขียนโค้ดใหม่ที่คลาสอื่นขึ้นอยู่ คุณสามารถทำลายคลาสที่รู้จักและจบลงด้วยการเปลี่ยนดีบักเกอร์ของคุณภายใน - ภายนอก เป็นการเขียนโปรแกรมที่ไม่ดี

หากคุณจำเป็นต้องทำคุณควรย่อยคลาสนั้น ๆ

จากนั้นข้อเสนอแนะของการหมุนวนนั่นคือ NO-NO-NO ที่ยิ่งใหญ่สำหรับฉัน

Swizzing ในขณะรันไทม์เป็น NO-NO-NO ที่สมบูรณ์

คุณต้องการให้กล้วยมีลักษณะเป็นสีส้ม แต่เฉพาะที่รันไทม์? ถ้าอยากได้ส้มก็เขียนส้ม

อย่าทำให้กล้วยมีลักษณะและทำตัวเหมือนส้ม และที่แย่กว่านั้นคืออย่าเปลี่ยนกล้วยของคุณให้เป็นสายลับที่จะทำลายกล้วยทั่วโลกอย่างเงียบ ๆ เพื่อสนับสนุนส้ม

อ๊ะ!


3
การ Swizzing ที่รันไทม์อาจเป็นประโยชน์สำหรับการเยาะเย้ยพฤติกรรมในสภาพแวดล้อมการทดสอบ
Ben G

2
แม้ว่าจะเป็นเรื่องตลก แต่คำตอบของคุณไม่ได้ทำอะไรได้มากกว่าการบอกว่าวิธีการทั้งหมดที่เป็นไปได้นั้นไม่ดี ธรรมชาติของสัตว์ร้ายคือบางครั้งคุณไม่สามารถคลาสย่อยได้จริงๆดังนั้นคุณจึงเหลือหมวดหมู่และโดยเฉพาะอย่างยิ่งถ้าคุณไม่ได้เป็นเจ้าของรหัสสำหรับคลาสที่คุณกำลังจัดหมวดหมู่บางครั้งคุณต้องทำ swizzle และมัน เป็นวิธีการที่ไม่พึงปรารถนาไม่เกี่ยวข้อง
hvanbrug

1

ฉันมีปัญหานี้เมื่อฉันใช้วิธีการมอบหมายในหมวดหมู่แทนที่จะเป็นคลาสหลัก (แม้ว่าจะไม่มีการใช้งานคลาสหลักก็ตาม) วิธีแก้ปัญหาสำหรับฉันคือการย้ายจากไฟล์ส่วนหัวของคลาสหลักไปยังไฟล์ส่วนหัวประเภทซึ่งใช้งานได้ดี

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.