การสร้างคลาสนามธรรมใน Objective-C


507

เดิมทีฉันเป็นโปรแกรมเมอร์ Java ที่ตอนนี้ทำงานร่วมกับ Objective-C ฉันต้องการสร้างคลาสนามธรรม แต่ก็ไม่เป็นไปได้ใน Objective-C เป็นไปได้ไหม

ถ้าไม่ฉันจะเข้าใกล้คลาสนามธรรมได้อย่างไรใน Objective-C


18
คำตอบด้านล่างนั้นยอดเยี่ยม ฉันพบสิ่งนี้ปัญหาของคลาส abstract นั้นมีความสัมพันธ์กับวิธีไพรเวต - ทั้งคู่เป็นวิธีสำหรับการ จำกัด โค้ดไคลเอ็นต์ที่สามารถทำได้และไม่มีอยู่ใน Objective-C ฉันคิดว่ามันช่วยให้เข้าใจว่าความคิดของภาษานั้นแตกต่างจากภาษาจาวาอย่างแท้จริง ดูคำตอบของฉันไปที่: stackoverflow.com/questions/1020070/#1020330
Quinn Taylor

ขอบคุณสำหรับข้อมูลเกี่ยวกับความคิดของชุมชน Objective-C เมื่อเทียบกับภาษาอื่น ๆ นั่นแก้ปัญหาที่เกี่ยวข้องกับคำถามของฉันได้ (เช่นทำไมไม่มีกลไกที่ตรงไปตรงมาสำหรับวิธีส่วนตัว ฯลฯ )
Jonathan Arbogast

1
ลองดูที่เว็บไซต์ CocoaDev ซึ่งให้เปรียบเทียบ java java cocoadev.com/index.pl?AbstractSuperClass

2
แม้ว่า Barry พูดถึงมันเป็นความคิดที่หลัง (ยกโทษให้ฉันถ้าฉันอ่านมันผิด) ฉันคิดว่าคุณกำลังมองหาพิธีสารในวัตถุประสงค์ C. ดูตัวอย่างพิธีสารคืออะไร? .
jww

คำตอบ:


633

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

[NSException raise:NSInternalInconsistencyException 
            format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)];

หากวิธีการของคุณคืนค่ามันเป็นเรื่องง่ายกว่าที่จะใช้

@throw [NSException exceptionWithName:NSInternalInconsistencyException
                               reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
                             userInfo:nil];

จากนั้นคุณไม่จำเป็นต้องเพิ่มคำสั่ง return จากเมธอด

หากคลาสนามธรรมเป็นอินเทอร์เฟซ (เช่นไม่มีวิธีการใช้งานที่เป็นรูปธรรม) การใช้โปรโตคอล Objective-C เป็นตัวเลือกที่เหมาะสมกว่า


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

13
ในการชี้แจง: คุณสามารถประกาศวิธีการในการ@protocolกำหนด แต่คุณไม่สามารถกำหนดวิธีการที่มี
ริชาร์ด

ด้วยวิธีการหมวดหมู่ใน NSException วิธี+ (instancetype)exceptionForCallingAbstractMethod:(SEL)selectorนี้ใช้ได้ดีมาก
Patrick Pijnappel

นี้ทำงานออกสำหรับฉันตั้งแต่ IMHO, การขว้างปายกเว้นเป็นที่ชัดเจนมากขึ้นในการพัฒนาอื่น ๆ doesNotRecognizeSelectorที่ว่านี้เป็นพฤติกรรมที่ต้องการมากไปกว่า
Chris

ใช้โปรโตคอล (สำหรับระดับนามธรรมสมบูรณ์) หรือรูปแบบวิธีการที่แม่แบบระดับนามธรรมมีตรรกะการดำเนินงาน / ไหลบางส่วนตามที่กำหนดที่นี่stackoverflow.com/questions/8146439/... ดูคำตอบของฉันด้านล่าง
hadaytullah

267

ไม่ไม่มีวิธีในการสร้างคลาสนามธรรมใน Objective-C

คุณสามารถเยาะเย้ยคลาสนามธรรม - โดยการเรียกเมธอด / selectors doesNotRecognizeSelector: และยกระดับข้อยกเว้นที่ทำให้คลาสใช้ไม่ได้

ตัวอย่างเช่น:

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

คุณสามารถทำสิ่งนี้เพื่อ init


5
@ ชัคฉันไม่ได้ลงคะแนน แต่การNSObjectอ้างอิงแนะนำให้ใช้สิ่งนี้ซึ่งคุณไม่ต้องการที่จะสืบทอดวิธีการที่จะไม่บังคับใช้วิธีการเอาชนะ แม้ว่าสิ่งเหล่านี้อาจจะเหมือนกันบางที :)
Dan Rosenstark

ทำไมคุณไม่ต้องการใช้โปรโตคอลในอินสแตนซ์นี้ สำหรับฉันนี่เป็นเรื่องดีที่รู้ว่าสำหรับวิธีการที่สมบูรณ์เท่านั้น แต่ไม่ใช่สำหรับคลาสนามธรรมทั้งหมด กรณีการใช้งานสำหรับสิ่งนี้ดูเหมือน จำกัด
lewiguez

ฉันไม่ได้ลงคะแนน แต่ข้อเสนอแนะที่คุณควรทำให้เกิดข้อยกเว้นในวิธีการเริ่มต้นนั้นเป็นสาเหตุที่เป็นไปได้มากที่สุด รูปแบบที่พบมากที่สุดสำหรับคลาสย่อยจะเริ่มเป็นวิธีการเริ่มต้นของตัวเองโดยการเรียกตนเอง = [super init] - ซึ่งจะเชื่อฟังข้อยกเว้น รูปแบบนี้ทำงานได้ดีสำหรับวิธีการส่วนใหญ่ แต่ฉันจะไม่ทำในสิ่งใดก็ตามที่คลาสย่อยอาจเรียกว่าเป็นการติดตั้งขั้นสูง
Xono

5
คุณสามารถสร้างคลาสนามธรรมใน Objective-C และเป็นเรื่องปกติ มีคลาสเฟรมเวิร์กจำนวนมากของ Apple ที่ทำสิ่งนี้ คลาสนามธรรมของ Apple ส่งข้อยกเว้นเฉพาะ (NSInvalidArgumentException) บ่อยครั้งโดยเรียก NSInvalidAbstractInvocation () การเรียกใช้เมธอด abstract เป็นข้อผิดพลาดในการเขียนโปรแกรมซึ่งเป็นสาเหตุที่ทำให้เกิดข้อยกเว้น โรงงานที่เป็นนามธรรมมักถูกนำไปใช้เป็นกลุ่มของคลาส
ระงับ

2
@quellish: ที่คุณพูดว่า: การเรียกวิธีนามธรรมเป็นข้อผิดพลาดการเขียนโปรแกรม ควรได้รับการปฏิบัติเช่นนี้แทนที่จะอาศัยการรายงานข้อผิดพลาดรันไทม์ (NSException) ฉันคิดว่านี่เป็นปัญหาที่ใหญ่ที่สุดสำหรับนักพัฒนาซอฟต์แวร์ที่มาจากภาษาอื่นซึ่งนามธรรมหมายถึง "ไม่สามารถยกตัวอย่างวัตถุประเภทนี้" ใน Obj-C มันหมายถึง "สิ่งต่าง ๆ จะผิดปกติที่รันไทม์เมื่อคุณสร้างคลาสนี้
Cross_

60

เพียงแค่ riffing ตามคำตอบของ @Barry Wark ด้านบน (และอัปเดตสำหรับ iOS 4.3) และปล่อยให้สิ่งนี้สำหรับการอ้างอิงของฉันเอง:

#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define methodNotImplemented() mustOverride()

จากนั้นในวิธีการของคุณคุณสามารถใช้สิ่งนี้

- (void) someMethod {
     mustOverride(); // or methodNotImplemented(), same thing
}



หมายเหตุ:ไม่แน่ใจว่าการทำให้แมโครดูเหมือนฟังก์ชั่น C เป็นความคิดที่ดีหรือไม่ แต่ฉันจะเก็บไว้จนกว่าจะเรียนจบ ฉันคิดว่ามันถูกต้องมากกว่าที่จะใช้NSInvalidArgumentException(แทนที่จะNSInternalInconsistencyException) เพราะนั่นคือสิ่งที่ระบบรันไทม์ส่งต่อเพื่อตอบสนองต่อการdoesNotRecognizeSelectorถูกเรียก (ดูNSObjectเอกสาร)


1
แน่นอน @TomA ฉันหวังว่ามันจะช่วยให้คุณมีความคิดเกี่ยวกับโค้ดอื่น ๆ ที่คุณสามารถใช้มาโครได้ มาโครที่ใช้มากที่สุดของฉันคือการอ้างอิงที่ง่ายในการเดี่ยว: รหัสกล่าวว่าแต่มันจะขยายuniverse.thing [Universe universe].thingสนุกใหญ่ประหยัดหลายพันตัวอักษรของรหัส ...
แดน Rosenstark

ยิ่งใหญ่ เปลี่ยนไปเล็กน้อย: #define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil].
noamtm

1
@Yar: ฉันไม่คิดอย่างนั้น เราใช้__PRETTY_FUNCTION__ทั่วทุกสถานที่ผ่านทาง DLog ( ... ) แมโครเป็นข้อเสนอแนะที่นี่: stackoverflow.com/a/969291/38557
noamtm

1
สำหรับรายละเอียดเพิ่มเติม -#define setMustOverride() NSLog(@"%@ - method not implemented", NSStringFromClass([self class])); mustOverride()
gbk

1
ถ้าคุณเพิ่มคลาสฐานแล้วสืบทอดคลาสนี้ตัวอย่าง 10 ครั้งและลืมนำไปใช้ในคลาสใดคลาสหนึ่งคุณจะได้รับข้อความที่มีชื่อคลาสคลาสที่ไม่สืบทอดมาเช่นTerminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BaseDynamicUIViewController localizeUI] must be overridden in a subclass/category'ในกรณีที่ฉันเสนอให้คุณรับHomeViewController - method not implementedHomeViewController สืบทอดมาจาก Base - จะให้ข้อมูลเพิ่มเติม
gbk

42

วิธีแก้ปัญหาที่ฉันพบคือ:

  1. สร้างโปรโตคอลสำหรับทุกสิ่งที่คุณต้องการในชั้น "นามธรรม" ของคุณ
  2. สร้างคลาสพื้นฐาน (หรืออาจเรียกว่าเป็นนามธรรม) ซึ่งใช้โปรโตคอล สำหรับวิธีการทั้งหมดที่คุณต้องการ "นามธรรม" นำไปใช้ในไฟล์. m แต่ไม่ใช่ไฟล์. h
  3. ให้คลาสลูกของคุณสืบทอดจากคลาสฐานและใช้โปรโตคอล

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

มันไม่รวบรัดเหมือนกับใน Java แต่คุณจะได้รับคำเตือนคอมไพเลอร์ที่ต้องการ


2
+1 นี่เป็นวิธีแก้ปัญหาที่ใกล้เคียงกับคลาสนามธรรมใน Java ฉันใช้วิธีนี้ด้วยตัวเองและใช้งานได้ดี มันยังได้รับอนุญาตให้ตั้งชื่อโปรโตคอลเหมือนกับคลาสพื้นฐาน (เช่นเดียวกับที่ Apple ทำกับNSObject) ถ้าคุณใส่โพรโทคอลและการประกาศคลาสพื้นฐานในไฟล์ส่วนหัวเดียวกันมันแทบจะแยกไม่ออกจากคลาสนามธรรม
codingFriend1

อา แต่คลาสนามธรรมของฉันใช้ส่วนหนึ่งของโปรโตคอลส่วนที่เหลือจะดำเนินการโดยคลาสย่อย
Roger CS Wernersson

1
คุณสามารถมีคลาสย่อยสำหรับเด็กเท่านั้นที่ใช้โพรโทคอลและปล่อยให้ซูเปอร์คลาสเมธอดทั้งหมดเข้าด้วยกันแทนที่จะว่างเปล่า จากนั้นมีคุณสมบัติประเภท Superclass <MyProtocol> นอกจากนี้เพื่อความยืดหยุ่นที่เพิ่มขึ้นคุณสามารถนำหน้าวิธีการของคุณในโปรโตคอลของคุณด้วย @optional
dotToString

สิ่งนี้ดูเหมือนจะไม่ทำงานใน Xcode 5.0.2; มันสร้างคำเตือนสำหรับคลาส "นามธรรม" เท่านั้น การไม่ขยายคลาส "abstract" จะสร้างคำเตือนคอมไพเลอร์ที่เหมาะสม แต่เห็นได้ชัดว่าไม่อนุญาตให้คุณสืบทอดเมธอด
Topher Fangio

ฉันชอบโซลูชันนี้ แต่ไม่ชอบจริงๆมันไม่ได้เป็นโครงสร้างโค้ดที่ดีในโครงการ // ควรใช้สิ่งนี้เป็นประเภทฐานสำหรับคลาสย่อย typedef BaseClass <BaseClassProtocol> BASECLASS; มันแค่กฎสัปดาห์ฉันไม่ชอบ
Itachi

35

จากรายชื่อผู้รับจดหมายกลุ่ม Omni :

Objective-C ไม่มีคอมไพเลอร์นามธรรมที่สร้างอย่าง Java ในขณะนี้

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

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

ฉันยังทำต่อไปนี้เพื่อป้องกันการเริ่มต้นของระดับนามธรรมผ่านเริ่มต้นเริ่มต้น

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

1
ฉันไม่ได้คิดถึงการใช้ -doesNotRecognizeSelector: และฉันชอบวิธีนี้ในบางวิธี ไม่มีใครรู้วิธีทำให้คอมไพเลอร์ออกคำเตือนสำหรับวิธี "นามธรรม" ที่สร้างขึ้นด้วยวิธีนี้หรือโดยการเพิ่มข้อยกเว้น? ว่าจะน่ากลัว ...
ควินน์เทย์เลอร์

26
DoNotRecognizeSelector วิธีการป้องกันรูปแบบการแนะนำตนเองของ Apple = [super init]
dlinsin

1
@ David: ฉันค่อนข้างมั่นใจว่าประเด็นทั้งหมดคือการยกข้อยกเว้นโดยเร็วที่สุด เป็นการดีที่ควรจะเป็นเวลารวบรวม แต่เนื่องจากไม่มีวิธีการทำพวกเขาตัดสินสำหรับข้อยกเว้นเวลาทำงาน สิ่งนี้คล้ายกับความล้มเหลวในการยืนยันซึ่งไม่ควรยกมาในรหัสการผลิตในตอนแรก ในความเป็นจริงการยืนยัน (false) อาจดีกว่าเนื่องจากชัดเจนว่ารหัสนี้ไม่ควรรัน ผู้ใช้ไม่สามารถแก้ไขสิ่งใดได้นักพัฒนาจะต้องแก้ไข ดังนั้นการเพิ่มข้อยกเว้นหรือความล้มเหลวในการยืนยันที่นี่ดูเหมือนเป็นความคิดที่ดี
เหตุผล

2
ฉันไม่คิดว่านี่เป็นวิธีแก้ปัญหาที่ทำงานได้เนื่องจากคลาสย่อยอาจมีการเรียก [super init] อย่างถูกต้องตามกฎหมาย
Raffi Khatchadourian

@dlinsin: นั่นคือสิ่งที่คุณต้องการเมื่อระดับนามธรรมไม่ควรเริ่มต้นผ่าน init คลาสย่อยของมันน่าจะรู้ว่าซุปเปอร์เมธอดใดที่จะเรียกใช้ แต่สิ่งนี้จะหยุดการเรียกใช้โดยประมาทผ่าน "new" หรือ "alloc / init"
gnasher729

21

แทนที่จะพยายามสร้างคลาสพื้นฐานที่เป็นนามธรรมให้พิจารณาใช้โปรโตคอล (คล้ายกับอินเตอร์เฟส Java) สิ่งนี้ช่วยให้คุณสามารถกำหนดชุดของวิธีการจากนั้นยอมรับวัตถุทั้งหมดที่สอดคล้องกับโปรโตคอลและใช้วิธีการ ตัวอย่างเช่นฉันสามารถกำหนดโปรโตคอลการดำเนินงานแล้วมีฟังก์ชั่นเช่นนี้:

- (void)performOperation:(id<Operation>)op
{
   // do something with operation
}

โดยที่ op สามารถเป็นวัตถุใด ๆ ที่ใช้โปรโตคอลการดำเนินการ

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


สำหรับฉันดูเหมือนว่าจะเป็นวิธีที่เหมาะสมในการเลือกกรณีที่คุณจะใช้คลาสนามธรรมอย่างน้อยก็เมื่อมันหมายถึง "อินเตอร์เฟส" จริงๆ (เหมือนใน C ++) มีข้อเสียที่ซ่อนอยู่สำหรับวิธีการนี้หรือไม่?
febeling

1
@febeling คลาสนามธรรม - อย่างน้อยใน Java - ไม่ได้เป็นเพียงส่วนต่อประสาน พวกเขายังกำหนดพฤติกรรม (หรือส่วนใหญ่) บางอย่าง แม้ว่าวิธีการนี้อาจดีในบางกรณี
Dan Rosenstark

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

19

หัวข้อนี้เก่าและส่วนใหญ่ที่ฉันต้องการแบ่งปันอยู่ที่นี่แล้ว

อย่างไรก็ตามวิธีที่ฉันชอบไม่ได้กล่าวถึงและ AFAIK ไม่มีการสนับสนุนดั้งเดิมใน Clang ปัจจุบันดังนั้นที่นี่ฉันไป ...

อันดับแรกและสำคัญที่สุด (อย่างที่คนอื่น ๆ ได้ชี้ให้เห็นแล้ว) คลาสนามธรรมเป็นสิ่งที่แปลกมากใน Objective-C - เรามักจะใช้องค์ประกอบ (บางครั้งผ่านการมอบหมาย) แทน นี่อาจเป็นเหตุผลว่าทำไมคุณสมบัติดังกล่าวยังไม่มีอยู่ในภาษา / คอมไพเลอร์นอกเหนือจาก@dynamicคุณสมบัติซึ่ง IIRC ถูกเพิ่มใน ObjC 2.0 พร้อมกับการแนะนำ CoreData แล้ว

แต่เนื่องจาก (หลังจากการประเมินสถานการณ์ของคุณอย่างระมัดระวัง!) คุณได้ข้อสรุปว่าการมอบหมาย (หรือองค์ประกอบโดยทั่วไป) ไม่เหมาะสมในการแก้ไขปัญหาของคุณนี่คือวิธีที่ฉันทำ:

  1. ใช้ทุกวิธีนามธรรมในคลาสฐาน
  2. ทำให้การใช้งานที่[self doesNotRecognizeSelector:_cmd];...
  3. …ตามด้วย__builtin_unreachable();การปิดเสียงเตือนคุณจะได้รับวิธีการที่ไม่เป็นโมฆะโดยบอกให้คุณ“ ควบคุมการสิ้นสุดฟังก์ชั่นที่ไม่เป็นโมฆะโดยไม่มีการคืนค่า "
  4. รวมขั้นตอนที่ 2 และ 3 เข้าด้วยกันในแมโครหรือใส่คำอธิบายประกอบ-[NSObject doesNotRecognizeSelector:]โดยใช้__attribute__((__noreturn__))ในหมวดหมู่โดยไม่มีการใช้งานเพื่อที่จะไม่แทนที่การใช้งานดั้งเดิมของวิธีการนั้นและรวมส่วนหัวสำหรับหมวดหมู่นั้นใน PCH ของโครงการของคุณ

โดยส่วนตัวฉันชอบรุ่นมาโครที่ช่วยให้ฉันลดความร้อนได้มากที่สุด

นี่มันคือ:

// Definition:
#define D12_ABSTRACT_METHOD {\
 [self doesNotRecognizeSelector:_cmd]; \
 __builtin_unreachable(); \
}

// Usage (assuming we were Apple, implementing the abstract base class NSString):
@implementation NSString

#pragma mark - Abstract Primitives
- (unichar)characterAtIndex:(NSUInteger)index D12_ABSTRACT_METHOD
- (NSUInteger)length D12_ABSTRACT_METHOD
- (void)getCharacters:(unichar *)buffer range:(NSRange)aRange D12_ABSTRACT_METHOD

#pragma mark - Concrete Methods
- (NSString *)substringWithRange:(NSRange)aRange
{
    if (aRange.location + aRange.length >= [self length])
        [NSException raise:NSInvalidArgumentException format:@"Range %@ exceeds the length of %@ (%lu)", NSStringFromRange(aRange), [super description], (unsigned long)[self length]];

    unichar *buffer = (unichar *)malloc(aRange.length * sizeof(unichar));
    [self getCharacters:buffer range:aRange];

    return [[[NSString alloc] initWithCharactersNoCopy:buffer length:aRange.length freeWhenDone:YES] autorelease];
}
// and so forth…

@end

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

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

ทำไมฉันถึงเลือกวิธีนี้

  1. มันเป็นงานที่ทำได้อย่างมีประสิทธิภาพและค่อนข้างสะดวก
  2. มันค่อนข้างง่ายที่จะเข้าใจ (โอเคนั่น__builtin_unreachable()อาจทำให้คนแปลกใจ แต่ก็ง่ายที่จะเข้าใจเช่นกัน)
  3. ไม่สามารถถอดออกได้ในบิลด์รีลีสโดยไม่สร้างคำเตือนคอมไพเลอร์หรือข้อผิดพลาดอื่น ๆ ซึ่งแตกต่างจากวิธีการที่อิงกับมาโครการยืนยันตัวใดตัวหนึ่ง

จุดสุดท้ายนั้นต้องการคำอธิบายบางอย่างฉันเดาว่า:

บางคน (ส่วนใหญ่?) ถอนคำยืนยันในการสร้างงาน (ผมไม่เห็นด้วยกับนิสัยนั้น แต่ที่เรื่องอื่น ... ) ล้มเหลวในการใช้วิธีการที่จำเป็น - แต่ - เป็นที่ไม่ดี , น่ากลัว , ผิดและพื้นสิ้นสุดของจักรวาลสำหรับโปรแกรมของคุณ โปรแกรมของคุณไม่สามารถทำงานได้อย่างถูกต้องในเรื่องนี้เพราะมันไม่ได้กำหนดและพฤติกรรมที่ไม่ได้กำหนดเป็นสิ่งที่เลวร้ายที่สุดที่เคย ดังนั้นความสามารถในการตัดการวินิจฉัยเหล่านั้นโดยไม่สร้างการวินิจฉัยใหม่จะไม่สามารถยอมรับได้อย่างสมบูรณ์

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


นี่คือทางออกใหม่ที่ฉันโปรดปราน - __builtin_unreachable();อัญมณีทำให้การทำงานนี้สมบูรณ์แบบ แมโครทำให้การจัดทำเอกสารด้วยตนเองและพฤติกรรมตรงกับสิ่งที่เกิดขึ้นหากคุณเรียกวิธีการที่ขาดหายไปบนวัตถุ
อเล็กซ์ MDC

12

การใช้@propertyและ@dynamicสามารถใช้งานได้ หากคุณประกาศคุณสมบัติแบบไดนามิกและไม่ให้การใช้งานวิธีการจับคู่ทุกอย่างจะยังคงรวบรวมโดยไม่มีคำเตือนและคุณจะได้รับunrecognized selectorข้อผิดพลาดในขณะทำงานหากคุณพยายามเข้าถึง นี่เป็นสิ่งเดียวกับการโทร[self doesNotRecognizeSelector:_cmd]แต่พิมพ์น้อยกว่ามาก


7

ใน Xcode (โดยใช้เสียงดังกราว ฯลฯ ) ฉันชอบที่จะใช้__attribute__((unavailable(...)))แท็กคลาสนามธรรมเพื่อให้คุณได้รับข้อผิดพลาด / คำเตือนถ้าคุณลองใช้

มันให้การป้องกันบางอย่างโดยไม่ตั้งใจโดยใช้วิธีการ

ตัวอย่าง

ใน@interfaceแท็กชั้นฐานวิธีการ "นามธรรม":

- (void)myAbstractMethod:(id)param1 __attribute__((unavailable("You should always override this")));

ด้วยการเพิ่มหนึ่งขั้นตอนต่อไปนี้ฉันสร้างแมโคร:

#define UnavailableMacro(msg) __attribute__((unavailable(msg)))

นี่ช่วยให้คุณทำสิ่งนี้:

- (void)myAbstractMethod:(id)param1 UnavailableMacro(@"You should always override this");

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


1
ฉันไม่สามารถรับมันได้ ฉันใช้ข้อเสนอแนะของคุณบนชั้นฐานของฉัน-initวิธีการและตอนนี้ Xcode ไม่อนุญาตให้ฉันสร้างตัวอย่างของการเรียนสืบทอดและเป็นเวลารวบรวมข้อผิดพลาดเกิดขึ้นไม่พร้อมใช้งาน ... คุณช่วยอธิบายเพิ่มเติมได้ไหม
anonim

ตรวจสอบให้แน่ใจว่าคุณมี-initวิธีการในคลาสย่อยของคุณ
Richard Stelling

2
เหมือนกับ NS_UNAVAILABLE ซึ่งจะทำให้เกิดข้อผิดพลาดทุกครั้งที่คุณจะพยายามเรียกวิธีการที่ทำเครื่องหมายด้วยคุณลักษณะดังกล่าว ฉันไม่เห็นว่ามันสามารถใช้กับคลาสนามธรรมได้อย่างไร
Ben Sinclair

7

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

ตัวเลือก 1: โปรโตคอล

หากคุณต้องการสร้างคลาสนามธรรมโดยไม่มีการใช้งานให้ใช้ 'โปรโตคอล' คลาสที่สืบทอดโปรโตคอลจำเป็นต้องใช้เมธอดในโปรโตคอล

@protocol ProtocolName
// list of methods and properties
@end

ตัวเลือก 2: รูปแบบวิธีการเทมเพลต

หากคุณต้องการสร้างคลาสนามธรรมที่มีการนำไปใช้บางส่วนเช่น "รูปแบบวิธีการของแม่แบบ" นี่เป็นวิธีแก้ปัญหา Objective-C - รูปแบบวิธีการของเทมเพลต?


6

ทางเลือกอื่น

เพียงตรวจสอบชั้นเรียนในชั้นนามธรรมและยืนยันหรือยกเว้นสิ่งที่คุณจินตนาการ

@implementation Orange
- (instancetype)init
{
    self = [super init];
    NSAssert([self class] != [Orange class], @"This is an abstract class");
    if (self) {
    }
    return self;
}
@end

สิ่งนี้จะลบความจำเป็นในการแทนที่ init


มันจะมีประสิทธิภาพเพียงแค่คืนค่าศูนย์หรือไม่ หรือว่าจะทำให้เกิดข้อยกเว้น / ข้อผิดพลาดอื่น ๆ ที่จะโยน?
2277872

นี่เป็นเพียงการจับโปรแกรมเมอร์เพื่อใช้งานคลาสโดยตรงซึ่งฉันคิดว่าใช้ประโยชน์ได้ดี
bigkm

5

(ข้อเสนอแนะที่เกี่ยวข้องเพิ่มเติม)

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

typedef void override_void;
typedef id override_id;

@implementation myBaseClass

// some limited default behavior (undesired by subclasses)
- (override_void) doSomething;
- (override_id) makeSomeObject;

// some internally required default behavior
- (void) doesSomethingImportant;

@end

ข้อดีคือโปรแกรมเมอร์ที่จะเห็น "แทนที่" [super ..]ในการประกาศและจะรู้ว่าพวกเขาไม่ควรจะเรียก

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

แน่นอนว่าคลาสนั้นยังสามารถใช้งานเริ่มต้นได้เมื่อส่วนขยายนั้นเป็นตัวเลือก แต่เช่นเดียวกับคำตอบอื่น ๆ ให้ใช้ข้อยกเว้นรันไทม์เมื่อเหมาะสมเช่นคลาส abstract (เสมือน)

มันจะดีถ้าได้สร้างคำแนะนำในคอมไพเลอร์เช่นนี้แม้แต่คำแนะนำที่ดีที่สุดในการ pre / post call การใช้งานของ super แทนที่จะต้องขุดผ่านข้อคิดเห็น / เอกสารประกอบหรือ ... สมมติว่า

ตัวอย่างของคำใบ้


4

หากคุณคุ้นเคยกับการรวบรวมการละเมิด instantiation นามธรรมในภาษาอื่น ๆ แล้วพฤติกรรม Objective-C น่าผิดหวัง

ในฐานะที่เป็นภาษาผูกพันปลายเป็นที่ชัดเจนว่า Objective-C ไม่สามารถตัดสินใจแบบคงที่ว่าคลาสนั้นเป็นนามธรรมหรือไม่ (คุณอาจจะเพิ่มฟังก์ชั่นที่รันไทม์ ... ) แต่สำหรับกรณีการใช้งานทั่วไปสิ่งนี้ดูเหมือนจะเป็นข้อบกพร่อง ฉันต้องการคอมไพเลอร์การแบนออกป้องกัน instantiations คลาสนามธรรมแทนการโยนข้อผิดพลาดที่รันไทม์

นี่คือรูปแบบที่เราใช้เพื่อรับการตรวจสอบแบบคงที่นี้โดยใช้เทคนิคสองสามอย่างเพื่อซ่อน initializers:

//
//  Base.h
#define UNAVAILABLE __attribute__((unavailable("Default initializer not available.")));

@protocol MyProtocol <NSObject>
-(void) dependentFunction;
@end

@interface Base : NSObject {
    @protected
    __weak id<MyProtocol> _protocolHelper; // Weak to prevent retain cycles!
}

- (instancetype) init UNAVAILABLE; // Prevent the user from calling this
- (void) doStuffUsingDependentFunction;
@end

//
//  Base.m
#import "Base.h"

// We know that Base has a hidden initializer method.
// Declare it here for readability.
@interface Base (Private)
- (instancetype)initFromDerived;
@end

@implementation Base
- (instancetype)initFromDerived {
    // It is unlikely that this becomes incorrect, but assert
    // just in case.
    NSAssert(![self isMemberOfClass:[Base class]],
             @"To be called only from derived classes!");
    self = [super init];
    return self;
}

- (void) doStuffUsingDependentFunction {
    [_protocolHelper dependentFunction]; // Use it
}
@end

//
//  Derived.h
#import "Base.h"

@interface Derived : Base
-(instancetype) initDerived; // We cannot use init here :(
@end

//
//  Derived.m
#import "Derived.h"

// We know that Base has a hidden initializer method.
// Declare it here.
@interface Base (Private)
- (instancetype) initFromDerived;
@end

// Privately inherit protocol
@interface Derived () <MyProtocol>
@end

@implementation Derived
-(instancetype) initDerived {
    self= [super initFromDerived];
    if (self) {
        self->_protocolHelper= self;
    }
    return self;
}

// Implement the missing function
-(void)dependentFunction {
}
@end

3

อาจเป็นสถานการณ์ประเภทนี้ควรเกิดขึ้นในเวลาการพัฒนาเท่านั้นดังนั้นสิ่งนี้อาจใช้ได้:

- (id)myMethodWithVar:(id)var {
   NSAssert(NO, @"You most override myMethodWithVar:");
   return nil;
}

3

คุณสามารถใช้วิธีการที่เสนอโดย@Yar (ด้วยการดัดแปลงบางอย่าง):

#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define setMustOverride() NSLog(@"%@ - method not implemented", NSStringFromClass([self class])); mustOverride()

ที่นี่คุณจะได้รับข้อความเช่น:

<Date> ProjectName[7921:1967092] <Class where method not implemented> - method not implemented
<Date> ProjectName[7921:1967092] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[<Base class (if inherited or same if not> <Method name>] must be overridden in a subclass/category'

หรือยืนยัน:

NSAssert(![self respondsToSelector:@selector(<MethodName>)], @"Not implemented");

ในกรณีนี้คุณจะได้รับ:

<Date> ProjectName[7926:1967491] *** Assertion failure in -[<Class Name> <Method name>], /Users/kirill/Documents/Projects/root/<ProjectName> Services/Classes/ViewControllers/YourClass:53

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


2

โกโก้ไม่ได้ให้อะไรที่เป็นนามธรรม เราสามารถสร้างบทคัดย่อระดับที่ได้รับการตรวจสอบเฉพาะที่รันไทม์และในเวลารวบรวมนี้ไม่ได้ตรวจสอบ


1

ฉันมักจะปิดการใช้งานวิธีการเริ่มต้นในชั้นเรียนที่ฉันต้องการนามธรรม:

- (instancetype)__unavailable init; // This is an abstract class.

สิ่งนี้จะสร้างข้อผิดพลาดในเวลารวบรวมเมื่อใดก็ตามที่คุณเรียกใช้ init บนคลาสนั้น ฉันใช้วิธีการเรียนเพื่อทุกอย่างอื่น

Objective-C ไม่มีวิธีการประกาศคลาสนามธรรมในตัว


แต่ฉันได้รับข้อผิดพลาดเมื่อฉันเรียก [super init] จากคลาสที่ได้รับของคลาสนามธรรมนั้น วิธีแก้นั้น
Sahil Doshi

@SahilDoshi ฉันคิดว่านั่นเป็นข้อเสียของการใช้วิธีนี้ ฉันใช้มันเมื่อฉันไม่ต้องการให้ชั้นเรียนเป็นอินสแตนซ์และไม่มีอะไรสืบทอดมาจากคลาสนั้น
mahoukov

ใช่ฉันเข้าใจแล้ว แต่โซลูชันของคุณจะช่วยในการสร้างบางอย่างเช่นคลาสซิงเกิลตันที่เราไม่ต้องการให้ใครเรียก init ตามความรู้ของฉันในกรณีของคลาสนามธรรมบางคลาสจะได้รับมัน มิฉะนั้นการใช้คลาสนามธรรมคืออะไร
Sahil Doshi

0

การเปลี่ยนแปลงเล็ก ๆ น้อย ๆ สิ่งที่ @redfood ปัญหาโดยใช้ความคิดเห็น @ dotToString ของคุณจริงมีทางออกลูกบุญธรรมของ Instagram IGListKit

  1. สร้างโพรโทคอลสำหรับวิธีการทั้งหมดที่ไม่สมเหตุสมผลที่จะกำหนดไว้ในคลาสพื้นฐาน (นามธรรม) เช่นพวกเขาต้องการการใช้งานเฉพาะในเด็ก
  2. สร้างคลาสพื้นฐาน (นามธรรม) ที่ไม่ได้ใช้โปรโตคอลนี้ คุณสามารถเพิ่มวิธีการอื่นที่เหมาะสมกับการใช้งานทั่วไป
  3. ทุกที่ในโครงการของคุณหากเด็กจากAbstractClassต้องเป็นอินพุตหรือเอาต์พุตด้วยวิธีการบางอย่างให้พิมพ์มันAbstractClass<Protocol>แทน

เนื่องจากAbstractClassไม่ได้ใช้Protocolวิธีเดียวที่จะมีAbstractClass<Protocol>อินสแตนซ์คือโดย subclassing เนื่องจากAbstractClassไม่สามารถใช้งานได้ทุกที่ในโครงการจึงกลายเป็นนามธรรม

แน่นอนว่าสิ่งนี้ไม่ได้ป้องกันนักพัฒนาที่ไม่ด้อยโอกาสจากการเพิ่มวิธีการใหม่ ๆ ที่อ้างถึงAbstractClassซึ่งจะทำให้การใช้อินสแตนซ์ของคลาสนามธรรม (ไม่ใช่อีกต่อไป)

ตัวอย่างโลกจริง: IGListKitมีชั้นฐานIGListSectionControllerที่ไม่ใช้โปรโตคอลแต่วิธีการที่ต้องใช้เช่นการเรียนว่าทุกจริงขอประเภทIGListSectionType IGListSectionController<IGListSectionType>ดังนั้นจึงไม่มีวิธีที่จะใช้ประเภทวัตถุIGListSectionControllerสำหรับสิ่งที่มีประโยชน์ในกรอบของพวกเขา


0

อันที่จริง Objective-C ไม่มีคลาสนามธรรม แต่คุณสามารถใช้โปรโตคอลเพื่อให้ได้ผลเช่นเดียวกัน นี่คือตัวอย่าง:

CustomProtocol.h

#import <Foundation/Foundation.h>

@protocol CustomProtocol <NSObject>
@required
- (void)methodA;
@optional
- (void)methodB;
@end

TestProtocol.h

#import <Foundation/Foundation.h>
#import "CustomProtocol.h"

@interface TestProtocol : NSObject <CustomProtocol>

@end

TestProtocol.m

#import "TestProtocol.h"

@implementation TestProtocol

- (void)methodA
{
  NSLog(@"methodA...");
}

- (void)methodB
{
  NSLog(@"methodB...");
}
@end

0

ตัวอย่างง่ายๆของการสร้างคลาสนามธรรม

// Declare a protocol
@protocol AbcProtocol <NSObject>

-(void)fnOne;
-(void)fnTwo;

@optional

-(void)fnThree;

@end

// Abstract class
@interface AbstractAbc : NSObject<AbcProtocol>

@end

@implementation AbstractAbc

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

-(void)fnOne{
// Code
}

-(void)fnTwo{
// Code
}

@end

// Implementation class
@interface ImpAbc : AbstractAbc

@end

@implementation ImpAbc

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

// You may override it    
-(void)fnOne{
// Code
}
// You may override it
-(void)fnTwo{
// Code
}

-(void)fnThree{
// Code
}

@end

-3

คุณสร้างผู้รับมอบสิทธิ์ไม่ได้ใช่ไหม

ผู้รับมอบสิทธิ์เป็นเหมือนคลาสพื้นฐานที่เป็นนามธรรมในแง่ที่คุณพูดว่าฟังก์ชันใดที่จำเป็นต้องกำหนด แต่คุณไม่ได้นิยามฟังก์ชันเหล่านั้นจริงๆ

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

ฟังดูเหมือนชั้นฐานที่เป็นนามธรรมสำหรับฉัน

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