Có thể đặt phương thức -init thành riêng tư trong Objective-C không?


147

Tôi cần ẩn (đặt riêng tư) -initphương thức của lớp trong Objective-C.

Làm thế nào tôi có thể làm điều đó?


3
Bây giờ có một cơ sở cụ thể, sạch sẽ và mô tả để đạt được điều này, như thể hiện trong câu trả lời dưới đây . Cụ thể : NS_UNAVAILABLE. Tôi thường khuyến khích bạn sử dụng phương pháp này. OP sẽ xem xét sửa đổi câu trả lời được chấp nhận của họ? Các câu trả lời khác ở đây cung cấp rất nhiều chi tiết hữu ích, nhưng không phải là phương pháp ưa thích để đạt được điều này.
Stewohn

Như những người khác đã lưu ý dưới đây, NS_UNAVAILABLEvẫn cho phép một người gọi gọi initgián tiếp thông qua new. Đơn giản chỉ cần ghi đè initđể trả lại nilsẽ xử lý cả hai trường hợp.
Greg Brown

Câu trả lời:


88

Mục tiêu-C, giống như Smalltalk, không có khái niệm về phương pháp "riêng tư" so với "công khai". Bất kỳ tin nhắn có thể được gửi đến bất kỳ đối tượng bất cứ lúc nào.

Những gì bạn có thể làm là ném một NSInternalInconsistencyExceptionnếu -initphương thức của bạn được gọi:

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

Một lựa chọn khác - có lẽ tốt hơn nhiều trong thực tế - là làm -initđiều gì đó hợp lý cho lớp học của bạn nếu có thể.

Nếu bạn đang cố gắng làm điều này bởi vì bạn đang cố gắng "đảm bảo" một đối tượng đơn lẻ được sử dụng, đừng bận tâm. Cụ thể, đừng bận tâm với "ghi đè +allocWithZone:, -init, -retain, -release" phương pháp của việc tạo ra độc thân. Nó hầu như luôn luôn không cần thiết và chỉ là thêm sự phức tạp cho không có lợi thế đáng kể thực sự.

Thay vào đó, chỉ cần viết mã của bạn sao cho +sharedWhateverphương thức của bạn là cách bạn truy cập vào một singleton và ghi lại như là cách để lấy cá thể singleton trong tiêu đề của bạn. Đó nên là tất cả những gì bạn cần trong phần lớn các trường hợp.


2
Là sự trở lại thực sự cần thiết ở đây?
philsquared

5
Vâng, để giữ cho trình biên dịch hạnh phúc. Mặt khác, trình biên dịch có thể phàn nàn rằng không có trả về từ một phương thức có trả về không trống.
Chris Hanson

Thật buồn cười, nó không dành cho tôi. Có lẽ một phiên bản trình biên dịch hoặc chuyển đổi khác nhau? (Tôi chỉ đang sử dụng các công tắc gcc mặc định với XCode 3.1)
philsquared

3
Dựa vào nhà phát triển để theo một mô hình không phải là một ý tưởng tốt. Tốt hơn hết là ném một ngoại lệ, vì vậy các nhà phát triển trong một nhóm khác không biết. Tôi quan niệm riêng tư sẽ tốt hơn.
Nick Turner

4
"Không có lợi thế đáng kể thực sự" . Hoàn toàn sai sự thật. Ưu điểm đáng kể là bạn muốn thực thi mẫu singleton. Nếu bạn cho phép các phiên bản mới được tạo, thì nhà phát triển không quen với API có thể sử dụng allocinit và có chức năng mã của họ không đúng bởi vì họ có lớp đúng, nhưng trường hợp sai. Đây là bản chất của nguyên tắc đóng gói trong OO. Bạn ẩn những thứ trong API của mình mà các lớp khác không cần, hoặc có, truy cập. Bạn không chỉ giữ mọi thứ công khai và mong đợi con người theo dõi tất cả.
Nate

345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Đây là phiên bản ngắn của thuộc tính không có sẵn. Nó lần đầu tiên xuất hiện trong macOS 10.7iOS 5 . Nó được định nghĩa trong NSObjCR nb.h là #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Có một phiên bản chỉ tắt phương thức này cho các máy khách Swift , không phải cho mã ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Thêm unavailablethuộc tính vào tiêu đề để tạo ra lỗi biên dịch trên bất kỳ lệnh gọi nào đến init.

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

biên dịch lỗi thời gian

Nếu bạn không có lý do, chỉ cần gõ __attribute__((unavailable))hoặc thậm chí __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Sử dụng doesNotRecognizeSelector:để nâng cao NSInvalidArgumentException. Hệ thống thời gian chạy gọi phương thức này bất cứ khi nào một đối tượng nhận được thông báo aSelector mà nó không thể phản hồi hoặc chuyển tiếp.

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

NSAssert

Sử dụng NSAssertđể ném NSIternalInconsistencyException và hiển thị một thông báo:

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

raise:format:

Sử dụng raise:format:để ném ngoại lệ của riêng bạn:

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

[self release]là cần thiết bởi vì các đối tượng đã được allocated. Khi sử dụng ARC, trình biên dịch sẽ gọi nó cho bạn. Trong mọi trường hợp, không có gì phải lo lắng khi bạn chuẩn bị dừng thực thi.

objc_designated_initializer

Trong trường hợp bạn có ý định vô hiệu hóa initđể buộc sử dụng trình khởi tạo được chỉ định, có một thuộc tính cho điều đó:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Điều này tạo ra một cảnh báo trừ khi bất kỳ phương thức khởi tạo nào khác gọi myOwnInitbên trong. Chi tiết sẽ được công bố trong Thông qua Modern Objective-C sau khi phát hành Xcode tiếp theo (tôi đoán vậy).


Điều này có thể tốt cho các phương pháp khác init. Vì, nếu phương thức này không hợp lệ, tại sao bạn khởi tạo một đối tượng? Ngoài ra, khi đưa ra một ngoại lệ, bạn sẽ có thể chỉ định một số thông báo tùy chỉnh truyền đạt init*phương thức chính xác cho nhà phát triển, trong khi bạn không có tùy chọn như vậy trong trường hợp doesNotRecognizeSelector.
Aleks N.

Không biết Aleks, điều đó không nên ở đó :) Tôi đã chỉnh sửa câu trả lời.
Jano

Điều này là kỳ lạ bởi vì nó đã kết thúc hệ thống của bạn. Tôi đoán tốt hơn là không để điều gì xảy ra nhưng tôi tự hỏi liệu có cách nào tốt hơn không. Điều tôi muốn là các nhà phát triển khác không thể gọi nó và để trình biên dịch bắt kịp khi 'chạy' hoặc 'xây dựng'
okysabeni

1
Tôi đã thử cách này và nó không hoạt động: - (id) init __attribution __ ((không khả dụng ("init không khả dụng"))) {NSAssert (false, @ "Sử dụng initWithType"); trở về con số không; }
okysabeni

1
@Miraaj Nghe có vẻ như nó không được hỗ trợ trên trình biên dịch của bạn. Nó được hỗ trợ trong Xcode 6. Bạn sẽ nhận được trình khởi tạo Tiện ích của Cốt cuộc thiếu cuộc gọi 'tự' đến một trình khởi tạo khác nếu một trình khởi tạo không gọi cuộc gọi được chỉ định.
Jano

101

Apple đã bắt đầu sử dụng các mục sau trong các tệp tiêu đề của họ để vô hiệu hóa hàm tạo init:

- (instancetype)init NS_UNAVAILABLE;

Điều này hiển thị chính xác như là một lỗi biên dịch trong Xcode. Cụ thể, điều này được đặt trong một số tệp tiêu đề HealthKit của họ (HKUnit là một trong số đó).


3
Lưu ý rằng bạn vẫn có thể khởi tạo đối tượng bằng [MyObject new];
José

11
Bạn cũng có thể làm + (instancetype) NS_UNAVAILABLE mới;
sonicfly

@sonicfly Đã thử làm điều đó nhưng dự án vẫn biên dịch
CyberMew

3

Nếu bạn đang nói về phương thức -init mặc định thì bạn không thể. Nó được kế thừa từ NSObject và mọi lớp sẽ trả lời nó mà không có cảnh báo.

Bạn có thể tạo một phương thức mới, giả sử -initMyClass và đặt nó vào một danh mục riêng như Matt gợi ý. Sau đó, xác định phương thức -init mặc định để đưa ra một ngoại lệ nếu nó được gọi hoặc (tốt hơn) gọi private -initMyClass của bạn với một số giá trị mặc định.

Một trong những lý do chính khiến mọi người dường như muốn ẩn init là vì các đối tượng đơn lẻ . Nếu đó là trường hợp thì bạn không cần phải ẩn -init, chỉ cần trả lại đối tượng singleton (hoặc tạo nó nếu nó chưa tồn tại).


Đây có vẻ là một cách tiếp cận tốt hơn là chỉ để lại 'init' trong đĩa đơn của bạn và dựa vào tài liệu để liên lạc với người dùng mà họ phải truy cập thông qua 'sharedWhthing'. Mọi người thường không đọc tài liệu cho đến khi họ đã lãng phí nhiều phút để cố gắng tìm ra một vấn đề.
Greg Maletic


3

Bạn có thể khai báo bất kỳ phương pháp nào không khả dụng bằng cách sử dụng NS_UNAVAILABLE.

Vì vậy, bạn có thể đặt những dòng này bên dưới @interface của bạn

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

Thậm chí tốt hơn xác định một macro trong tiêu đề tiền tố của bạn

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

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

2

Điều đó phụ thuộc vào những gì bạn có nghĩa là "làm cho riêng tư". Trong Objective-C, việc gọi một phương thức trên một đối tượng có thể được mô tả tốt hơn là gửi một thông điệp tới đối tượng đó. Không có gì trong ngôn ngữ cấm khách hàng gọi bất kỳ phương thức đã cho nào trên một đối tượng; cách tốt nhất bạn có thể làm là không khai báo phương thức trong tệp tiêu đề. Tuy nhiên, nếu khách hàng gọi phương thức "riêng tư" bằng chữ ký đúng, nó vẫn sẽ thực thi khi chạy.

Điều đó nói rằng, cách phổ biến nhất để tạo một phương thức riêng tư trong Objective-C là tạo một Thể loại trong tệp thực hiện và khai báo tất cả các phương thức "ẩn" trong đó. Hãy nhớ rằng điều này sẽ không thực sự ngăn chặn các cuộc gọi đếninit chạy, nhưng trình biên dịch sẽ đưa ra cảnh báo nếu có ai cố gắng thực hiện việc này.

MyClass.m

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

@implementation MyClass

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

@end

Có một chủ đề hay trên MacRumors.com về chủ đề này.


3
Thật không may, trong trường hợp này, cách tiếp cận danh mục sẽ không thực sự hữu ích. Thông thường nó mua cho bạn các cảnh báo thời gian biên dịch rằng phương thức có thể không được xác định trên lớp. Tuy nhiên, vì MyClass phải kế thừa từ một trong các nhóm gốc và họ xác định init, nên sẽ không có cảnh báo nào.
Barry Wark

2

vấn đề tại sao bạn không thể làm cho nó "riêng tư / vô hình" là do phương thức init được gửi đến id (vì cấp phát trả về một id) không cho YourClass

Lưu ý rằng từ điểm của trình biên dịch (trình kiểm tra), id có thể phản hồi potencialy với bất kỳ thứ gì đã gõ (nó không thể kiểm tra những gì thực sự đi vào id khi chạy), vì vậy bạn chỉ có thể ẩn init khi không có gì cả (công khai = in tiêu đề) sử dụng một phương thức init, hơn trình biên dịch sẽ biết, không có cách nào để id trả lời init, vì không có init ở bất cứ đâu (trong nguồn của bạn, tất cả các lib, v.v.)

vì vậy bạn không thể cấm người dùng vượt qua init và bị trình biên dịch đập vỡ ... nhưng điều bạn có thể làm là ngăn người dùng lấy một thể hiện thực bằng cách gọi init

chỉ đơn giản bằng cách triển khai init, trả về nil và có trình khởi tạo (riêng tư / vô hình) mà tên người khác sẽ không nhận được (như initOnce, initWith Special ...)

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

Lưu ý: ai đó có thể làm điều này

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

và trên thực tế nó sẽ trả về một thể hiện mới, nhưng nếu initOnce không được khai báo công khai (trong tiêu đề), nó sẽ tạo ra một cảnh báo (id có thể không phản hồi ...) và dù sao thì người sử dụng điều này, sẽ cần để biết chính xác rằng trình khởi tạo thực sự là initOnce

chúng ta có thể ngăn chặn điều này hơn nữa, nhưng không cần


0

Tôi phải đề cập đến việc đặt các xác nhận và đưa ra các ngoại lệ để ẩn các phương thức trong lớp con có một cái bẫy khó chịu cho mục đích tốt.

Tôi sẽ khuyên bạn nên sử dụng __unavailablenhư Jano giải thích cho ví dụ đầu tiên của mình .

Các phương thức có thể được ghi đè trong các lớp con. Điều này có nghĩa là nếu một phương thức trong lớp cha sử dụng một phương thức chỉ tạo ra một ngoại lệ trong lớp con, thì nó có thể sẽ không hoạt động như dự định. Nói cách khác, bạn vừa phá vỡ những gì đã từng làm việc. Điều này đúng với các phương thức khởi tạo là tốt. Dưới đây là một ví dụ về việc thực hiện khá phổ biến như vậy:

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

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

Hãy tưởng tượng điều gì xảy ra với -initWithLessParameter, nếu tôi làm điều này trong lớp con:

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

Điều này ngụ ý rằng bạn nên có xu hướng sử dụng các phương thức riêng tư (ẩn), đặc biệt là trong các phương thức khởi tạo, trừ khi bạn có kế hoạch ghi đè các phương thức. Nhưng, đây là một chủ đề khác, vì bạn không phải lúc nào cũng có toàn quyền kiểm soát việc triển khai siêu lớp. (Điều này khiến tôi nghi ngờ việc sử dụng __attribution ((objc_designated_initializer)) là cách thực hành tồi, mặc dù tôi chưa sử dụng nó một cách sâu sắc.)

Nó cũng ngụ ý rằng bạn có thể sử dụng các xác nhận và ngoại lệ trong các phương thức phải được ghi đè trong các lớp con. (Các phương thức "trừu tượng" như trong Tạo một lớp trừu tượng trong Objective-C )

Và đừng quên phương thức lớp mới.

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.