Sembunyikan peringatan "Kategori sedang menerapkan metode yang juga akan diterapkan oleh kelas utamanya"


98

Saya bertanya-tanya bagaimana cara menekan peringatan:

Kategori menerapkan metode yang juga akan diterapkan oleh kelas utamanya.

Saya memiliki ini untuk kategori kode tertentu:

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

Dengan metode swizzling. Meskipun saya tidak akan melakukannya - mungkin Anda bisa membuat subkelas UIFont yang menggantikan metode yang sama, dan memanggil supersebaliknya.
Alan Zeino

4
Masalah Anda bukanlah peringatannya. Masalah Anda adalah Anda memiliki nama metode yang sama, yang akan menyebabkan masalah.
gnasher729

Lihat Mengganti metode menggunakan kategori di Objective-C untuk alasan mengapa Anda tidak boleh mengganti metode menggunakan kategori, dan untuk solusi alternatif.
Senseful

Jika Anda tahu solusi yang lebih elegan untuk mengatur font seluruh aplikasi, saya sangat ingin mendengarnya!
To1ne

Jawaban:


64

Kategori memungkinkan Anda menambahkan metode baru ke kelas yang ada. Jika Anda ingin menerapkan kembali metode yang sudah ada di kelas, Anda biasanya membuat subkelas, bukan kategori.

Dokumentasi Apple: Menyesuaikan kelas yang ada

Jika nama metode yang dideklarasikan dalam kategori sama dengan metode di kelas asli, atau metode di kategori lain di kelas yang sama (atau bahkan superclass), perilaku tidak ditentukan untuk implementasi metode mana yang digunakan di runtime.

Dua metode dengan tanda tangan yang sama persis di kelas yang sama akan menyebabkan perilaku yang tidak dapat diprediksi, karena setiap pemanggil tidak dapat menentukan implementasi yang diinginkan.

Jadi, Anda harus menggunakan kategori dan memberikan nama metode yang baru dan unik untuk kelas tersebut, atau subkelas jika Anda ingin mengubah perilaku metode yang ada di kelas.


1
Saya sangat setuju dengan ide-ide yang dijelaskan di atas dan mencoba mengikutinya selama pengembangan. tetapi masih ada kasus, di mana metode penggantian dalam kategori mungkin sesuai. misalnya, kasus di mana beberapa warisan (seperti di c ++) atau antarmuka (seperti di c #) dapat digunakan. baru saja dihadapkan pada hal itu dalam proyek saya dan menyadari bahwa metode utama dalam kategori adalah pilihan terbaik.
peetonn

4
Ini bisa berguna saat unit menguji beberapa kode yang memiliki singleton di dalamnya. Idealnya, lajang harus dimasukkan ke dalam kode sebagai protokol, memungkinkan Anda untuk mengganti implementasinya. Tetapi jika Anda sudah memiliki satu yang disematkan di dalam kode Anda, Anda dapat menambahkan kategori singleton dalam pengujian unit Anda dan menimpa sharedInstance dan metode yang Anda kontrol untuk mengubahnya menjadi objek dummy.
bandejapaisa

Terima kasih @PsychoDad. Saya memperbarui tautan dan menambahkan kutipan dari dokumentasi yang relevan dengan posting ini.
bneely

Kelihatan bagus. Apakah Apple menyediakan dokumentasi tentang perilaku penggunaan kategori dengan nama metode yang ada?
jjxtra

1
luar biasa, tidak yakin apakah saya harus menggunakan kategori atau subkelas :-)
kernix

343

Meskipun semua yang dikatakan dengan indah benar, itu tidak benar-benar menjawab pertanyaan Anda tentang bagaimana cara menekan peringatan itu.

Jika Anda harus memiliki kode ini karena suatu alasan (dalam kasus saya, saya memiliki HockeyKit di proyek saya dan mereka mengganti metode dalam kategori UIImage [edit: ini bukan lagi kasusnya]) dan Anda perlu membuat proyek Anda untuk dikompilasi , Anda dapat menggunakan #pragmapernyataan untuk memblokir peringatan seperti ini:

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

// do your override

#pragma clang diagnostic pop

Saya menemukan informasinya di sini: http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html


Terima kasih banyak! Jadi, saya lihat pragma juga bisa menekan peringatan. :-p
Constantino Tsarouhas

Ya, dan meskipun ini adalah pernyataan khusus LLVM, ada juga pernyataan serupa untuk GCC.
Ben Baron

1
Peringatan dalam proyek pengujian Anda adalah peringatan linker, bukan peringatan compiler llvm, oleh karena itu pragma llvm tidak melakukan apa-apa. Namun, Anda akan melihat bahwa project pengujian Anda masih dibuat dengan mengaktifkan "perlakukan peringatan sebagai error" karena ini adalah peringatan linker.
Ben Baron

12
Ini benar-benar harus menjadi jawaban yang diterima, mengingat itu benar-benar menjawab pertanyaan itu.
Rob Jones

1
Jawaban ini seharusnya benar. Pokoknya itu memiliki lebih banyak suara daripada yang dipilih sebagai jawaban.
Juan Catalan

20

Alternatif yang lebih baik (lihat jawaban Bneely mengapa peringatan ini menyelamatkan Anda dari bencana) adalah dengan menggunakan metode swizzling. Dengan menggunakan metode swizzling, Anda dapat mengganti metode yang ada dari kategori tanpa ketidakpastian siapa yang "menang", dan sambil mempertahankan kemampuan untuk memanggil metode lama. Rahasianya adalah dengan memberi penggantian nama metode yang berbeda, lalu menukarnya menggunakan fungsi runtime.

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

Kemudian tentukan implementasi kustom Anda:

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

Timpa penerapan default dengan Anda:

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

10

Coba ini di kode Anda:

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

UPDATE2: Tambahkan makro ini

#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
Ini bukan contoh lengkap. Tidak ada makro bernama EXCHANGE_METHOD yang sebenarnya ditentukan oleh runtime tujuan-c.
Richard J. Ross III

@VersiMuslimah -1. metode itu tidak diimplementasikan untuk tipe kelas. Kerangka apa yang Anda gunakan?
Richard J. Ross III

Maaf lagi, coba ini saya membuat file NSObject + MethodExchange
Vitaliy Gervazuk

Dengan kategori di NSObject mengapa repot-repot dengan makro? Mengapa tidak tertipu dengan 'exchangeMethod'?
hvanbrug

5

Anda dapat menggunakan metode swizzling untuk menyembunyikan peringatan compiler ini. Berikut adalah bagaimana saya mengimplementasikan metode swizzling untuk menggambar margin di UITextField ketika kita menggunakan latar belakang khusus dengan 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

Properti over-riding berlaku untuk Perluasan Kelas (Kategori Anonim), tetapi tidak untuk Kategori biasa.

Menurut Apple Docs dengan menggunakan Ekstensi Kelas (Kategori Anonim) Anda dapat membuat antarmuka Pribadi ke kelas publik, sehingga antarmuka pribadi dapat menimpa properti yang terbuka secara publik. yaitu Anda dapat mengubah properti dari hanya-baca menjadi tulis-baca.

Kasus penggunaan untuk ini adalah saat Anda menulis pustaka yang membatasi akses ke properti publik, sementara properti yang sama memerlukan akses baca tulis penuh di dalam pustaka.

Tautan Apple Docs: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

Cari " Gunakan Ekstensi Kelas untuk Menyembunyikan Informasi Pribadi ".

Jadi teknik ini berlaku untuk Ekstensi Kelas, tetapi tidak untuk Kategori.


1

Kategori adalah hal yang baik, tetapi dapat disalahgunakan. Saat menulis kategori, seharusnya Anda sebagai prinsip TIDAK menerapkan ulang metode yang ada. Melakukannya dapat menyebabkan efek samping yang aneh karena Anda sekarang menulis ulang kode yang bergantung pada kelas lain. Anda bisa merusak kelas yang diketahui, dan akhirnya membalik debugger Anda. Ini hanyalah pemrograman yang buruk.

Jika Anda perlu melakukannya, Anda benar-benar harus membuat subkelasnya.

Kemudian saran dari swizzling, itu adalah NO-NO-NO yang besar bagi saya.

Menggesernya saat runtime adalah NO-NO-NO yang lengkap.

Anda ingin pisang terlihat seperti jeruk, tetapi hanya saat runtime? Jika Anda menginginkan jeruk, tulislah jeruk.

Jangan membuat tampilan pisang dan bertindak seperti jeruk. Dan lebih buruk lagi: jangan ubah pisang Anda menjadi agen rahasia yang diam-diam akan menyabotase pisang di seluruh dunia untuk mendukung jeruk.

Astaga!


3
Swizzing pada waktu proses mungkin berguna untuk mengejek perilaku di lingkungan pengujian.
Ben G

2
Meskipun lucu, jawaban Anda sebenarnya tidak lebih dari mengatakan bahwa semua metode yang memungkinkan itu buruk. Sifat binatang itu adalah kadang-kadang Anda benar-benar tidak bisa membuat subkelas, jadi Anda ditinggalkan dengan kategori dan terutama jika Anda tidak memiliki kode untuk kelas yang Anda kategorikan, terkadang Anda perlu melakukan swizzle, dan itu menjadi metode yang tidak diinginkan tidak relevan.
hvanbrug

1

Saya mengalami masalah ini ketika saya menerapkan metode delegasi dalam kategori daripada kelas utama (meskipun tidak ada implementasi kelas utama). Solusi bagi saya adalah memindahkan dari file header kelas utama ke file header kategori. Ini berfungsi dengan baik

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.