เป็นไปได้ไหมที่จะทำให้ -init method เป็นส่วนตัวใน Objective-C


147

ฉันต้องการซ่อน (ทำให้เป็นส่วนตัว) -initวิธีการเรียนของฉันใน Objective-C

ฉันจะทำสิ่งนั้นได้อย่างไร


3
ขณะนี้มีเฉพาะความสะอาดและสิ่งอำนวยความสะดวกที่สื่อความหมายเพื่อให้บรรลุนี้ดังแสดงในคำตอบด้านล่างนี้ โดยเฉพาะ: NS_UNAVAILABLE. โดยทั่วไปฉันอยากให้คุณใช้วิธีนี้ OP จะพิจารณาทบทวนคำตอบที่ยอมรับหรือไม่ คำตอบอื่น ๆ ที่นี่ให้รายละเอียดที่เป็นประโยชน์มากมาย แต่ไม่ใช่วิธีที่ต้องการในการบรรลุเป้าหมายนี้
Benjohn

ดังที่คนอื่น ๆ ได้ระบุไว้ด้านล่างNS_UNAVAILABLEยังอนุญาตให้ผู้โทรสามารถเรียกinitผ่านทางอ้อมnewได้ เพียงเอาชนะที่initจะกลับมาnilจะจัดการกับทั้งสองกรณี
เกร็กบราวน์

คำตอบ:


88

Objective-C เช่น Smalltalk ไม่มีแนวคิดของวิธีการ "ส่วนตัว" กับ "สาธารณะ" ข้อความใด ๆ ที่สามารถส่งไปยังวัตถุใด ๆ ได้ตลอดเวลา

คุณสามารถทำอะไรได้NSInternalInconsistencyExceptionถ้า-initวิธีการของคุณถูกเรียกใช้:

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

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

หากคุณกำลังพยายามทำเช่นนี้เพราะคุณกำลังพยายาม "ให้ความมั่นใจ" ว่ามีการใช้วัตถุแบบซิงเกิลไม่ต้องกังวล โดยเฉพาะไม่รำคาญกับ "แทนที่+allocWithZone:, -init, -retain, -release" วิธีการสร้าง singletons มันไม่จำเป็นเสมอไปและกำลังเพิ่มความซับซ้อนโดยไม่มีข้อได้เปรียบที่สำคัญอย่างแท้จริง

เพียงแค่เขียนรหัสของคุณเพื่อให้+sharedWhateverวิธีการของคุณเป็นวิธีการเข้าถึงซิงเกิลตันและเอกสารที่เป็นวิธีการรับอินสแตนซ์ซิงเกิลในส่วนหัวของคุณ นั่นควรจะเป็นสิ่งที่คุณต้องการในกรณีส่วนใหญ่


2
จำเป็นต้องส่งคืนจริงหรือไม่
philsquared

5
ใช่เพื่อให้คอมไพเลอร์มีความสุข มิฉะนั้นคอมไพเลอร์อาจบ่นว่าไม่มีการส่งคืนจากวิธีที่ไม่มีผลตอบแทนเป็นโมฆะ
344 Chris

ตลกมันไม่ได้สำหรับฉัน อาจเป็นเวอร์ชั่นคอมไพเลอร์หรือสวิตช์ที่ต่างออกไป? (ฉันเพียงแค่ใช้ GCC เริ่มต้นสลับกับ XCode 3.1)
philsquared

3
การนับนักพัฒนาเพื่อติดตามรูปแบบไม่ใช่ความคิดที่ดี จะเป็นการดีกว่าถ้ามีข้อยกเว้นดังนั้นนักพัฒนาซอฟต์แวร์ในทีมอื่นก็ไม่ควรทำ ฉันแนวคิดส่วนตัวจะดีกว่า
Nick Turner

4
"สำหรับประโยชน์ที่สำคัญไม่มีจริง" ไม่จริงโดยสิ้นเชิง ข้อได้เปรียบที่สำคัญคือคุณต้องการบังคับใช้รูปแบบซิงเกิล ถ้าคุณอนุญาตให้อินสแตนซ์ใหม่ที่จะถูกสร้างขึ้นแล้วนักพัฒนาผู้ที่ไม่คุ้นเคยกับ API ที่อาจจะใช้allocและinitมีฟังก์ชั่นรหัสของพวกเขาอย่างไม่ถูกต้องเพราะพวกเขามีระดับที่ถูกต้อง แต่กรณีที่ไม่ถูกต้อง นี่คือสาระสำคัญของหลักการห่อหุ้มใน OO คุณซ่อนสิ่งต่าง ๆ ใน API ของคุณที่คลาสอื่นไม่ต้องการหรือรับเข้าถึง คุณไม่เพียง แต่ทำให้ทุกอย่างเป็นสาธารณะและคาดหวังให้มนุษย์ติดตามทุกสิ่ง
Nate

345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

นี่เป็นแอททริบิวต์ที่ไม่มีในเวอร์ชันย่อ มันปรากฏตัวครั้งแรกใน macOS 10.7และ iOS 5 มันถูกกำหนดไว้ใน NSObjCRuntime.h #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTEเป็น

มีรุ่นที่เป็น ปิดใช้งานเมธอดสำหรับไคลเอ็นต์ Swiftเท่านั้นไม่ใช่สำหรับรหัส ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

เพิ่มunavailableคุณสมบัติให้กับส่วนหัวเพื่อสร้างข้อผิดพลาดคอมไพเลอร์ในการเรียกเพื่อเริ่ม

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

ข้อผิดพลาดในการรวบรวมเวลา

หากคุณไม่มีเหตุผลเพียงพิมพ์__attribute__((unavailable))หรือแม้กระทั่ง__unavailable :

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

ใช้doesNotRecognizeSelector:เพื่อเพิ่ม NSInvalidArgumentException “ ระบบรันไทม์จะเรียกใช้เมธอดนี้ทุกครั้งที่วัตถุได้รับข้อความ aSelector ซึ่งไม่สามารถตอบสนองหรือส่งต่อได้

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

NSAssert

ใช้ NSAssertเพื่อโยน NSInternalInconsistencyException และแสดงข้อความ:

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

raise:format:

ใช้ raise:format:เพื่อโยนข้อยกเว้นของคุณเอง:

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

[self release]เป็นสิ่งจำเป็นเพราะวัตถุที่ถูกแล้วallocated เมื่อใช้ ARC คอมไพเลอร์จะโทรหาคุณ ไม่ว่าในกรณีใดก็ตามอย่ากังวลเมื่อคุณกำลังจะหยุดทำการโดยเจตนา

objc_designated_initializer

ในกรณีที่คุณตั้งใจจะปิดการใช้งาน initเพื่อบังคับใช้ initializer ที่กำหนดมีคุณสมบัติสำหรับที่:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

สิ่งนี้จะสร้างคำเตือนเว้นแต่ว่าวิธีการเริ่มต้นอื่นจะเรียกmyOwnInitภายใน รายละเอียดจะถูกเผยแพร่ในAdopting Modern Objective-Cหลังจาก Xcode รุ่นถัดไป (ฉันเดา)


นี้สามารถที่ดีสำหรับวิธีการอื่น ๆ initกว่า เนื่องจากถ้าวิธีนี้ไม่ถูกต้องคุณจะเริ่มต้นวัตถุทำไม นอกจากนั้นเมื่อขว้างปายกเว้นคุณจะสามารถที่จะระบุข้อความที่กำหนดเองบางสื่อสารที่ถูกต้องวิธีการที่นักพัฒนาในขณะที่คุณไม่ได้มีตัวเลือกดังกล่าวในกรณีของinit* doesNotRecognizeSelector
Aleks N.

ไม่มีความคิด Aleks ที่ไม่ควรมี :) ฉันแก้ไขคำตอบ
Jano

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

1
ฉันลองและไม่ได้ผล: - (id) init __attribute __ ((ไม่พร้อมใช้งาน ("ไม่พร้อมใช้งาน init"))) {NSAssert (false, @ "Use initWithType"); กลับศูนย์ }
okysabeni

1
@Miraaj ฟังดูเหมือนว่าคอมไพเลอร์ของคุณไม่รองรับ ได้รับการสนับสนุนใน Xcode 6 คุณควรได้รับ“ การเริ่มต้นความสะดวกไม่มีสายเรียกเข้า "ตัวเอง" ไปยังตัวเริ่มต้นอื่น "หากผู้เริ่มต้นไม่เรียกผู้ที่ได้รับมอบหมาย
Jano

101

Apple เริ่มใช้สิ่งต่อไปนี้ในไฟล์ส่วนหัวเพื่อปิดใช้งานตัวสร้าง init:

- (instancetype)init NS_UNAVAILABLE;

สิ่งนี้แสดงอย่างถูกต้องว่าเป็นข้อผิดพลาดของคอมไพเลอร์ใน Xcode โดยเฉพาะจะถูกตั้งค่าในไฟล์ส่วนหัว HealthKit หลายไฟล์ (HKUnit เป็นหนึ่งในนั้น)


3
โปรดทราบว่าคุณยังสามารถสร้างอินสแตนซ์ของวัตถุด้วย [MyObject ใหม่];
โฮเซ

11
คุณยังสามารถทำ + (อินสแตนซ์ประเภท) ใหม่ NS_UNAVAILABLE;
sonicfly

@sonicfly พยายามทำเช่นนั้น แต่โครงการยังคงคอมไพล์
CyberMew

3

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

คุณสามารถสร้างวิธีการใหม่พูด -initMyClass และวางไว้ในหมวดหมู่ส่วนตัวอย่างที่ Matt แนะนำ จากนั้นกำหนดเมธอด -init ดีฟอลต์เพื่อเพิ่มข้อยกเว้นถ้าถูกเรียกหรือ (ดีกว่า) เรียกไพรเวต -initMyClass ของคุณด้วยค่าเริ่มต้นบางอย่าง

หนึ่งในเหตุผลหลักที่คนดูเหมือนจะต้องการที่จะซ่อน init สำหรับวัตถุเดี่ยว หากเป็นกรณีนี้คุณไม่จำเป็นต้องซ่อน -init เพียงส่งคืนวัตถุ singleton แทน (หรือสร้างหากยังไม่มีอยู่)


ดูเหมือนว่าวิธีการที่ดีกว่าเพียงแค่ปล่อยให้ 'init' อยู่คนเดียวในซิงเกิลตันของคุณและพึ่งพาเอกสารเพื่อสื่อสารกับผู้ใช้ว่าพวกเขาควรจะเข้าถึงผ่าน 'sharedWhething' ผู้คนมักจะไม่อ่านเอกสารจนกว่าพวกเขาจะเสียเวลาไปหลายนาทีในการพยายามหาปัญหา
Greg Maletic

3

ใส่ไว้ในไฟล์ส่วนหัว

- (id)init UNAVAILABLE_ATTRIBUTE;

ไม่แนะนำ สถานะเอกสาร c วัตถุประสงค์ที่ทันสมัยของ Apple ที่ init ควรส่งคืนอินสแตนซ์ประเภทไม่ใช่ id developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058

5
และเป็น: - (อินสแตนซ์ประเภท) init NS_UNAVAILABLE;
bandejapaisa

3

คุณสามารถประกาศวิธีการใด ๆ ที่จะไม่สามารถใช้ได้โดยใช้ NS_UNAVAILABLEที่จะไม่ได้มีการใช้

ดังนั้นคุณสามารถใส่บรรทัดเหล่านี้ใต้ @interface ของคุณ

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

ยิ่งกว่านั้นกำหนดแมโครในส่วนหัวของคำนำหน้า

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

และ

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

2

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

วิธีที่พบมากที่สุดในการสร้างวิธีการส่วนตัวใน Objective-C คือการสร้างหมวดหมู่ในไฟล์การนำไปใช้และประกาศวิธีการ "ซ่อน" ทั้งหมดในนั้น โปรดจำไว้ว่านี่จะไม่เป็นการป้องกันการโทรinitแต่คอมไพเลอร์จะคายคำเตือนหากมีคนพยายามทำเช่นนี้

MyClass.m

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

@implementation MyClass

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

@end

มีหัวข้อที่เหมาะสมใน MacRumors.com เกี่ยวกับหัวข้อนี้


3
น่าเสียดายที่ในกรณีนี้วิธีการหมวดหมู่จะไม่ช่วยได้จริงๆ โดยปกติแล้วคุณจะได้รับคำเตือนเวลารวบรวมว่าวิธีการอาจไม่ได้กำหนดไว้ในชั้นเรียน อย่างไรก็ตามเนื่องจาก MyClass ต้องสืบทอดจากหนึ่งในรูท clases และพวกเขากำหนด init จึงไม่มีคำเตือน
Barry Wark

2

ดีปัญหาที่คุณไม่สามารถทำให้ "ส่วนตัว / มองไม่เห็น" เป็นสาเหตุให้วิธีการ init ได้รับการส่งไปยัง id (เป็น alloc คืนรหัส) ไม่ให้ YourClass

โปรดทราบว่าจากจุดของคอมไพเลอร์ (ตัวตรวจสอบ) id สามารถ potencialy ตอบสนองต่อสิ่งที่เคยพิมพ์ (มันไม่สามารถตรวจสอบสิ่งที่จะเข้าไปใน id ที่ runtime) ดังนั้นคุณสามารถซ่อน init เมื่อไม่มีอะไรจะ (สาธารณะ = ใน ส่วนหัว) ใช้วิธีการเริ่มต้นกว่าการรวบรวมจะรู้ว่าไม่มีวิธีสำหรับ ID ที่จะตอบสนองต่อการเริ่มต้นเนื่องจากไม่มีที่ใดก็ได้ (ในแหล่งที่มาของคุณ libs ทั้งหมด ฯลฯ ... )

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

ง่ายๆโดยการใช้ init ซึ่งส่งกลับศูนย์และมี initializer (ส่วนตัว / ที่มองไม่เห็น) ที่ชื่อคนอื่นจะไม่ได้รับ (เช่น 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;
}

หมายเหตุ: ใครบางคนสามารถทำได้

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

และในความเป็นจริงจะส่งคืนอินสแตนซ์ใหม่ แต่ถ้า initOnce ไม่ปรากฏในโครงการของเราต่อสาธารณะ (ในส่วนหัว) ประกาศจะสร้างคำเตือน (id อาจไม่ตอบสนอง ... ) และบุคคลที่ใช้สิ่งนี้จะต้อง เพื่อทราบอย่างชัดเจนว่า initializer ที่แท้จริงคือ initOnce

เราสามารถป้องกันปัญหานี้ได้อีก แต่ไม่จำเป็น


0

ฉันต้องพูดถึงว่าการวางการยืนยันและการยกระดับข้อยกเว้นเพื่อซ่อนวิธีการในคลาสย่อยนั้นมีกับดักที่น่ารังเกียจสำหรับเจตนาดี

ฉันอยากจะแนะนำให้ใช้__unavailableเป็นJano อธิบายสำหรับตัวอย่างแรกของเขาอธิบายตัวอย่างแรกของเขา

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

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

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

ลองนึกภาพสิ่งที่เกิดขึ้นกับ -initWithLessParameters ถ้าฉันทำสิ่งนี้ในคลาสย่อย:

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

นี่ก็หมายความว่าคุณควรจะใช้วิธีการส่วนตัว (ซ่อน) โดยเฉพาะอย่างยิ่งในวิธีการเริ่มต้นเว้นแต่ว่าคุณวางแผนที่จะมีวิธีการแทนที่ แต่นี่เป็นหัวข้ออื่นเนื่องจากคุณไม่สามารถควบคุมการใช้ superclass ได้อย่างเต็มที่ (นี่ทำให้ฉันตั้งคำถามกับการใช้ __attribute ((objc_designated_initializer)) ว่าเป็นแนวปฏิบัติที่ไม่ดีแม้ว่าฉันจะไม่ได้ใช้ในเชิงลึกก็ตาม)

นอกจากนี้ยังหมายความว่าคุณสามารถใช้การยืนยันและข้อยกเว้นในวิธีการที่จะต้องแทนที่ในคลาสย่อย (วิธีการ "นามธรรม" เช่นเดียวกับในการสร้างระดับนามธรรมใน Objective-C )

และอย่าลืมเกี่ยวกับวิธีการ + new class

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