Apakah mungkin membuat metode -it pribadi di Objective-C?


147

Saya perlu menyembunyikan (merahasiakan) -initmetode kelas saya di Objective-C.

Bagaimana saya bisa melakukan itu?


3
Sekarang ada fasilitas spesifik, bersih, dan deskriptif untuk mencapai ini, seperti yang ditunjukkan dalam jawaban di bawah ini . Secara khusus: NS_UNAVAILABLE. Saya biasanya mendesak Anda untuk menggunakan pendekatan ini. Apakah OP akan mempertimbangkan untuk merevisi jawaban yang mereka terima? Jawaban lain di sini memberikan banyak detail berguna, tetapi bukan metode yang disukai untuk mencapai ini.
Benjohn

Seperti yang telah dicatat orang lain di bawah ini, NS_UNAVAILABLEmasih memungkinkan penelepon untuk memanggil initsecara tidak langsung melalui new. Cukup mengganti inituntuk kembali nilakan menangani kedua kasus.
Greg Brown

Anda tentu saja dapat membuat NS_UNAVAILABLE 'baru' dan 'init' - yang saat ini menjadi praktik umum.
Motti Shneor

Jawaban:


88

Objective-C, seperti Smalltalk, tidak memiliki konsep metode "pribadi" versus "publik". Pesan apa pun dapat dikirim ke objek apa saja kapan saja.

Apa yang dapat Anda lakukan adalah melempar NSInternalInconsistencyExceptionjika -initmetode Anda dipanggil:

- (id)init {
    [self release];
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"-init is not a valid initializer for the class Foo"
                                 userInfo:nil];
    return nil;
}

Alternatif lain - yang mungkin jauh lebih baik dalam praktiknya - adalah membuat -initmelakukan sesuatu yang masuk akal untuk kelas Anda jika memungkinkan.

Jika Anda mencoba melakukan ini karena Anda mencoba "memastikan" objek tunggal digunakan, jangan repot-repot. Secara khusus, tidak repot-repot dengan "override +allocWithZone:, -init, -retain, -release" metode menciptakan lajang. Ini sebenarnya selalu tidak perlu dan hanya menambah kerumitan tanpa keuntungan nyata yang nyata.

Alih-alih, tulis saja kode Anda sedemikian rupa sehingga +sharedWhatevermetode Anda adalah bagaimana Anda mengakses singleton, dan mendokumentasikannya sebagai cara untuk mendapatkan instance singleton di header Anda. Hanya itulah yang Anda butuhkan di sebagian besar kasus.


2
Apakah pengembalian sebenarnya diperlukan di sini?
philsquared

5
Ya, untuk membuat kompiler senang. Kalau tidak, kompiler dapat mengeluh bahwa tidak ada pengembalian dari metode dengan pengembalian tidak batal.
Chris Hanson

Lucu, itu bukan untuk saya. Mungkin versi kompiler atau switch yang berbeda? (Saya hanya menggunakan switch gcc default dengan
XCode

3
Mengandalkan pengembang untuk mengikuti pola bukanlah ide yang baik. Lebih baik untuk melemparkan pengecualian, sehingga pengembang di tim yang berbeda tahu untuk tidak melakukannya. Konsep pribadi saya akan lebih baik.
Nick Turner

4
"Tanpa keuntungan nyata nyata" . Sama sekali tidak benar. Keuntungan penting adalah bahwa Anda ingin menegakkan pola tunggal. Jika Anda membiarkan kasus baru yang akan dibuat, maka pengembang yang tidak akrab dengan API dapat menggunakan allocdan initdan memiliki fungsi kode mereka salah karena mereka memiliki kelas yang tepat, tetapi contoh yang salah. Ini adalah esensi dari prinsip enkapsulasi dalam OO. Anda menyembunyikan hal-hal di API Anda yang tidak perlu, atau dapatkan, diakses oleh kelas lain. Anda tidak hanya membiarkan semuanya publik dan mengharapkan manusia untuk melacak semuanya.
Nate

346

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Ini adalah versi singkat dari atribut yang tidak tersedia. Ini pertama kali muncul di macOS 10.7 dan iOS 5 . Ini didefinisikan dalam NSObjCRuntime.h sebagai #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Ada versi yang menonaktifkan metode ini hanya untuk klien Swift , bukan untuk kode ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Tambahkan unavailableatribut ke header untuk menghasilkan kesalahan kompiler pada panggilan apa pun untuk init.

-(instancetype) init __attribute__((unavailable("init not available")));  

kompilasi kesalahan waktu

Jika Anda tidak memiliki alasan, ketikkan saja __attribute__((unavailable)), atau bahkan __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Gunakan doesNotRecognizeSelector:untuk meningkatkan NSInvalidArgumentException. "Sistem runtime memanggil metode ini setiap kali suatu objek menerima pesan aSelector yang tidak bisa ditanggapi atau diteruskan."

- (instancetype) init {
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

NSAssert

Gunakan NSAssertuntuk melempar NSInternalInconsistencyException dan menampilkan pesan:

- (instancetype) init {
    [self release];
    NSAssert(false,@"unavailable, use initWithBlah: instead");
    return nil;
}

raise:format:

Gunakan raise:format:untuk melempar pengecualian Anda sendiri:

- (instancetype) init {
    [self release];
    [NSException raise:NSGenericException 
                format:@"Disabled. Use +[[%@ alloc] %@] instead",
                       NSStringFromClass([self class]),
                       NSStringFromSelector(@selector(initWithStateDictionary:))];
    return nil;
}

[self release]diperlukan karena objek sudah allocterpasang. Saat menggunakan ARC, kompiler akan memanggilnya untuk Anda. Bagaimanapun, bukan sesuatu yang perlu dikhawatirkan ketika Anda akan menghentikan eksekusi dengan sengaja.

objc_designated_initializer

Jika Anda bermaksud menonaktifkan inituntuk memaksa penggunaan penginisialisasi yang ditunjuk, ada atribut untuk itu:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Ini menghasilkan peringatan kecuali metode inisialisasi lainnya memanggil secara myOwnInitinternal. Detail akan dipublikasikan di Adopting Modern Objective-C setelah rilis Xcode berikutnya (saya kira).


Ini bisa bagus untuk metode selain init. Karena, jika metode ini tidak valid, mengapa Anda menginisialisasi objek? Plus, saat melempar pengecualian, Anda akan dapat menentukan beberapa pesan khusus yang mengomunikasikan init*metode yang benar kepada pengembang, sementara Anda tidak memiliki opsi seperti itu jika terjadi doesNotRecognizeSelector.
Aleks N.

Tidak tahu Aleks, itu seharusnya tidak ada di sana :) Saya mengedit jawabannya.
Jano

Ini aneh karena akhirnya menabrak sistem Anda. Saya kira lebih baik tidak membiarkan sesuatu terjadi tetapi saya bertanya-tanya apakah ada cara yang lebih baik. Yang saya inginkan adalah agar pengembang lain tidak dapat memanggilnya dan membiarkannya ditangkap oleh kompiler ketika 'berjalan' atau 'membangun'
okysabeni

1
Saya mencoba ini dan tidak berhasil: - (id) init __attribute __ ((tidak tersedia ("init tidak tersedia")))) {NSAssert (false, @ "Use initWithType"); kembali nihil; }
okysabeni

1
@Miraaj Kedengarannya tidak didukung di kompiler Anda. Ini didukung dalam Xcode 6. Anda harus mendapatkan "Penginisialisasi kenyamanan melewatkan panggilan 'mandiri' ke penginisialisasi lain" jika penginisialisasi tidak memanggil yang ditunjuk.
Jano

101

Apple telah mulai menggunakan yang berikut ini dalam file header mereka untuk menonaktifkan konstruktor init:

- (instancetype)init NS_UNAVAILABLE;

Ini ditampilkan dengan benar sebagai kesalahan kompiler di Xcode. Secara khusus, ini diatur dalam beberapa file header HealthKit mereka (HKUnit adalah salah satunya).


3
Perhatikan bahwa Anda masih dapat membuat objek dengan [MyObject new];
José

11
Anda juga dapat melakukan + (instancetype) NS_UNAVAILABLE baru;
sonicfly

@sonicfly Sudah mencoba melakukan itu tetapi proyek ini masih dapat dikompilasi
CyberMew

3

Jika Anda berbicara tentang metode default -it maka Anda tidak bisa. Ini diwarisi dari NSObject dan setiap kelas akan menanggapinya tanpa peringatan.

Anda bisa membuat metode baru, katakan -initMyClass, dan letakkan dalam kategori pribadi seperti yang disarankan Matt. Kemudian tentukan metode default -init untuk menaikkan pengecualian jika dipanggil atau (lebih baik) memanggil private -initMyClass Anda dengan beberapa nilai default.

Salah satu alasan utama orang tampaknya ingin menyembunyikan init adalah untuk objek tunggal . Jika itu masalahnya maka Anda tidak perlu menyembunyikan -init, cukup kembalikan objek singleton saja (atau buat jika belum ada).


Ini sepertinya pendekatan yang lebih baik daripada hanya meninggalkan 'init' sendirian di singleton Anda, dan mengandalkan dokumentasi untuk berkomunikasi dengan pengguna yang seharusnya mereka akses melalui 'sharedWh Apapun'. Orang-orang biasanya tidak membaca dokumen sampai mereka sudah menghabiskan banyak waktu untuk mencari tahu masalah.
Greg Maletic


3

Anda dapat mendeklarasikan metode apa pun yang tidak tersedia menggunakan NS_UNAVAILABLE.

Jadi Anda bisa meletakkan baris-baris ini di bawah @interface Anda

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

Bahkan lebih baik mendefinisikan makro di header awalan Anda

#define NO_INIT \
- (instancetype)init NS_UNAVAILABLE; \
+ (instancetype)new NS_UNAVAILABLE;

dan

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

2

Itu tergantung pada apa yang Anda maksud dengan "jadikan pribadi". Dalam Objective-C, memanggil metode pada objek mungkin lebih baik digambarkan sebagai mengirim pesan ke objek itu. Tidak ada dalam bahasa yang melarang klien memanggil metode apa pun pada objek; yang terbaik yang dapat Anda lakukan adalah tidak mendeklarasikan metode dalam file header. Jika klien tetap memanggil metode "pribadi" dengan tanda tangan yang tepat, itu masih akan dijalankan pada saat runtime.

Yang mengatakan, cara paling umum untuk membuat metode pribadi di Objective-C adalah membuat Kategori dalam file implementasi, dan mendeklarasikan semua metode "tersembunyi" di sana. Ingat bahwa ini tidak akan benar-benar mencegah panggilan untuk tidak initberjalan, tetapi kompiler akan memuntahkan peringatan jika ada yang mencoba melakukan ini.

MyClass.m

@interface MyClass (PrivateMethods)
- (NSString*) init;
@end

@implementation MyClass

- (NSString*) init
{
    // code...
}

@end

Ada utas yang layak di MacRumors.com tentang topik ini.


3
Sayangnya, dalam hal ini, pendekatan kategori tidak akan membantu. Biasanya itu memberi Anda waktu kompilasi peringatan bahwa metode ini mungkin tidak didefinisikan di kelas. Namun, karena MyClass harus mewarisi dari salah satu dari root clases dan mereka mendefinisikan init, tidak akan ada peringatan.
Barry Wark

2

baik masalah mengapa Anda tidak dapat membuatnya "pribadi / tidak terlihat" adalah karena metode init akan dikirim ke id (karena alokasi mengembalikan id) tidak ke YourClass

Perhatikan bahwa dari titik kompiler (pemeriksa) id dapat berpotensi merespons apa pun yang pernah diketik (ia tidak dapat memeriksa apa yang benar-benar masuk ke id saat runtime), sehingga Anda bisa menyembunyikannya hanya ketika tidak ada tempat (publik = dalam publik header) gunakan metode init, daripada kompilasi akan tahu, bahwa tidak ada cara bagi id untuk menanggapi init, karena tidak ada init di mana saja (di sumber Anda, semua libs dll ...)

jadi Anda tidak dapat melarang pengguna untuk melewatkan init dan dihancurkan oleh kompiler ... tetapi yang dapat Anda lakukan adalah mencegah pengguna mendapatkan contoh nyata dengan memanggil init

cukup dengan mengimplementasikan init, yang mengembalikan nihil dan memiliki inisialisasi (pribadi / tidak terlihat) yang nama orang lain tidak akan dapatkan (seperti initOnce, initWithSpecial ...)

static SomeClass * SInstance = nil;

- (id)init
{
    // possibly throw smth. here
    return nil;
}

- (id)initOnce
{
    self = [super init];
    if (self) {
        return self;
    }
    return nil;
}

+ (SomeClass *) shared 
{
    if (nil == SInstance) {
        SInstance = [[SomeClass alloc] initOnce];
    }
    return SInstance;
}

Catatan: seseorang dapat melakukan ini

SomeClass * c = [[SomeClass alloc] initOnce];

dan itu sebenarnya akan mengembalikan contoh baru, tetapi jika initOnce tidak akan diumumkan dalam proyek kami (di header), itu akan menghasilkan peringatan (id mungkin tidak merespons ...) dan lagi pula orang yang menggunakan ini, perlu untuk mengetahui dengan tepat bahwa inisialisasi sebenarnya adalah initOnce

kita bisa mencegah ini lebih jauh, tetapi tidak perlu


0

Saya harus menyebutkan bahwa menempatkan pernyataan dan meningkatkan pengecualian untuk menyembunyikan metode dalam subkelas memiliki jebakan buruk untuk yang dimaksudkan dengan baik.

Saya akan merekomendasikan penggunaan __unavailableseperti yang dijelaskan Jano untuk contoh pertamanya .

Metode dapat diganti dalam subkelas. Ini berarti bahwa jika suatu metode dalam superclass menggunakan metode yang hanya menimbulkan pengecualian dalam subkelas, itu mungkin tidak akan berfungsi sebagaimana dimaksud. Dengan kata lain, Anda baru saja melanggar apa yang dulu bekerja. Ini berlaku dengan metode inisialisasi juga. Berikut adalah contoh implementasi yang agak umum:

- (SuperClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    ...bla bla...
    return self;
}

- (SuperClass *)initWithLessParameters:(Type1 *)arg1
{
    self = [self initWithParameters:arg1 optional:DEFAULT_ARG2];
    return self;
}

Bayangkan apa yang terjadi pada -initWithLessParameters, jika saya melakukan ini di subkelas:

- (SubClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

Ini menyiratkan bahwa Anda harus cenderung menggunakan metode pribadi (tersembunyi), terutama dalam metode inisialisasi, kecuali jika Anda berencana untuk mengganti metode tersebut. Tapi, ini adalah topik lain, karena Anda tidak selalu memiliki kendali penuh dalam implementasi superclass. (Ini membuat saya mempertanyakan penggunaan __attribute ((objc_designated_initializer)) sebagai praktik yang buruk, meskipun saya belum menggunakannya secara mendalam.)

Ini juga menyiratkan bahwa Anda dapat menggunakan pernyataan dan pengecualian dalam metode yang harus ditimpa dalam subkelas. (Metode "abstrak" seperti dalam Membuat kelas abstrak di Objective-C )

Dan, jangan lupakan + metode kelas baru.

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.