ปัญหาที่แน่นอนเกี่ยวกับการสืบทอดหลายรายการคืออะไร?


121

ฉันเห็นคนถามตลอดเวลาว่าควรรวมการสืบทอดหลายรายการไว้ใน C # หรือ Java เวอร์ชันถัดไปหรือไม่ ชาว C ++ ที่โชคดีพอที่จะมีความสามารถนี้บอกว่านี่เหมือนกับการให้เชือกใครสักคนเพื่อแขวนคอตัวเองในที่สุด

มรดกหลายรายการมีอะไรบ้าง? มีตัวอย่างคอนกรีตหรือไม่?


54
ฉันจะพูดถึงว่า C ++ นั้นยอดเยี่ยมสำหรับการให้เชือกแขวนคอตัวเอง
tloach

1
สำหรับทางเลือกในการสืบทอดหลายรายการที่อยู่ (และ IMHO แก้ไข) ปัญหาเดียวกันหลาย ๆ อย่างให้ดูที่ลักษณะ ( iam.unibe.ch/~scg/Research/Traits )
Bevan

52
ฉันคิดว่า C ++ ให้เชือกเพียงพอที่จะยิงตัวเองที่เท้า
KeithB

6
คำถามนี้ดูเหมือนจะสมมติว่ามีปัญหากับ MI โดยทั่วไปในขณะที่ฉันพบหลายภาษาที่ MI ใช้งานทั่วไป มีปัญหาอย่างแน่นอนกับการจัดการ MI บางภาษา แต่ฉันไม่ทราบว่า MI โดยทั่วไปมีปัญหาสำคัญ
David Thornley

คำตอบ:


86

ปัญหาที่ชัดเจนที่สุดคือการลบล้างฟังก์ชัน

สมมติว่ามีสองชั้นAและซึ่งทั้งสองวิธีการที่กำหนดB doSomethingตอนนี้คุณกำหนดคลาสที่สามCซึ่งสืบทอดมาจากทั้งสองAและBแต่คุณไม่ได้แทนที่doSomethingเมธอด

เมื่อคอมไพเลอร์เมล็ดรหัสนี้ ...

C c = new C();
c.doSomething();

... ควรใช้วิธีไหน? หากไม่มีการชี้แจงเพิ่มเติมใด ๆ ก็เป็นไปไม่ได้ที่คอมไพเลอร์จะแก้ไขความคลุมเครือ

นอกจากการลบล้างแล้วปัญหาใหญ่อื่น ๆ ที่มีการสืบทอดหลายรายการคือโครงร่างของวัตถุทางกายภาพในหน่วยความจำ

ภาษาเช่น C ++ และ Java และ C # สร้างโครงร่างตามที่อยู่คงที่สำหรับวัตถุแต่ละประเภท สิ่งนี้:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

เมื่อคอมไพลเลอร์สร้างรหัสเครื่อง (หรือ bytecode) จะใช้การชดเชยตัวเลขเหล่านั้นเพื่อเข้าถึงแต่ละเมธอดหรือฟิลด์

การสืบทอดหลายรายการทำให้ยุ่งยากมาก

ถ้าคลาสCสืบทอดจากทั้งสองAและBคอมไพลเลอร์จะต้องตัดสินใจว่าจะจัดวางข้อมูลABตามลำดับหรือBAตามลำดับ

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

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

ดังนั้น ... เรื่องสั้นสั้น ๆ ... มันปวดคอสำหรับผู้เขียนคอมไพเลอร์เพื่อรองรับการสืบทอดหลาย ๆ ดังนั้นเมื่อมีคนอย่าง Guido van Rossum ออกแบบ python หรือเมื่อ Anders Hejlsberg ออกแบบ c # พวกเขารู้ว่าการสนับสนุนการสืบทอดหลาย ๆ อย่างจะทำให้การใช้งานคอมไพเลอร์ซับซ้อนขึ้นอย่างมากและพวกเขาไม่คิดว่าผลประโยชน์จะคุ้มค่ากับต้นทุน


62
เอ่อ Python รองรับ MI
Nemanja Trifunovic

26
ข้อโต้แย้งเหล่านี้ไม่น่าเชื่อมากนัก - การจัดวางแบบตายตัวนั้นไม่ยุ่งยากเลยในภาษาส่วนใหญ่ ใน C ++ เป็นเรื่องยุ่งยากเนื่องจากหน่วยความจำไม่ทึบแสงดังนั้นคุณอาจประสบปัญหากับสมมติฐานเลขคณิตของตัวชี้ ในภาษาที่คำจำกัดความของคลาสเป็นแบบคงที่ (เช่นเดียวกับใน java, C # และ C ++) การปะทะกันของชื่อที่สืบทอดหลายชื่ออาจเป็นสิ่งต้องห้ามในการคอมไพล์เวลา (และ C # ทำสิ่งนี้กับอินเทอร์เฟซ!)
Eamon Nerbonne

10
OP เพียงต้องการที่จะเข้าใจปัญหาและฉันอธิบายพวกเขาโดยไม่ได้แก้ไขเรื่องนี้เป็นการส่วนตัว ฉันแค่บอกว่านักออกแบบภาษาและผู้ใช้งานคอมไพเลอร์ "คงไม่คิดว่าผลประโยชน์คุ้มค่ากับค่าใช้จ่าย"
benjismith

12
" ปัญหาที่ชัดเจนที่สุดคือการลบล้างฟังก์ชัน " ซึ่งไม่เกี่ยวข้องกับการลบล้างฟังก์ชัน มันเป็นปัญหาความไม่ชัดเจนง่ายๆ
ซอกแซก

10
คำตอบนี้มีข้อมูลที่ผิดเกี่ยวกับ Guido และ Python เนื่องจาก Python รองรับ MI "ฉันตัดสินใจว่าตราบใดที่ฉันจะสนับสนุนการสืบทอดฉันก็อาจสนับสนุนการสืบทอดหลายแบบในรูปแบบที่เรียบง่ายด้วย" - Guido van Rossum python-history.blogspot.com/2009/02/… - นอกจากนี้การแก้ปัญหาความคลุมเครือเป็นเรื่องธรรมดาในคอมไพเลอร์ (ตัวแปรสามารถเป็น local เพื่อบล็อก, local to function, local เพื่อปิดฟังก์ชั่น, สมาชิกอ็อบเจ็กต์, สมาชิกคลาส, ลูกโลก ฯลฯ ) ฉันไม่เห็นว่าขอบเขตพิเศษจะสร้างความแตกต่างได้อย่างไร
marcus

46

ปัญหาที่พวกคุณพูดถึงไม่ใช่เรื่องยากที่จะแก้ไข ในความเป็นจริงเช่น Eiffel ทำได้ดีมาก! (และไม่แนะนำตัวเลือกตามอำเภอใจหรืออะไรก็ตาม)

เช่นถ้าคุณสืบทอดจาก A และ B ทั้งที่มี method foo () แน่นอนว่าคุณไม่ต้องการตัวเลือกโดยพลการในคลาส C ของคุณที่สืบทอดจากทั้ง A และ B คุณต้องกำหนด foo ใหม่ดังนั้นจึงชัดเจนว่าจะเป็นอย่างไร ใช้ถ้าเรียก c.foo () หรือมิฉะนั้นคุณต้องเปลี่ยนชื่อหนึ่งในวิธีการใน C. (อาจกลายเป็น bar ())

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


26
ฉันเห็นด้วย. สาเหตุหลักที่ผู้คนเกลียด MI นั้นเหมือนกับ JavaScript หรือการพิมพ์แบบคงที่: คนส่วนใหญ่เคยใช้การใช้งานที่ไม่ดีมากเท่านั้นหรือเคยใช้มันไม่ดี การตัดสิน MI โดย C ++ ก็เหมือนกับการตัดสิน OOP ด้วย PHP หรือการตัดสินรถยนต์โดย Pintos
Jörg W Mittag

2
@curiousguy: MI ขอแนะนำอีกชุดของภาวะแทรกซ้อนที่ต้องกังวลเช่นเดียวกับ "คุณสมบัติ" ของ C ++ เพียงเพราะมันไม่คลุมเครือไม่ได้ทำให้ง่ายต่อการทำงานหรือแก้ไขข้อบกพร่อง การถอดโซ่นี้ออกไปเนื่องจากมันหลุดหัวข้อและคุณก็ทำมันออกไป
Guvante

4
@ Guvante ปัญหาเดียวกับ MI ในทุกภาษาคือโปรแกรมเมอร์ที่ห่วยคิดว่าพวกเขาสามารถอ่านบทช่วยสอนและรู้ภาษาได้ทันที
เส้นทางไมล์

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

4
นอกจากนี้ข้อบกพร่องจะเกิดขึ้นจาก MI เมื่อคนโง่ใช้มันอย่างไม่ถูกต้อง
เส้นทางไมล์

27

ปัญหาเพชร :

ความคลุมเครือที่เกิดขึ้นเมื่อสองคลาส B และ C สืบทอดมาจาก A และคลาส D สืบทอดจากทั้ง B และ C หากมีวิธีการใน A ที่ B และ C ได้แทนที่และ D ไม่ได้แทนที่มันแล้วเวอร์ชันของ วิธี D สืบทอด: ของ B หรือของ C?

... เรียกว่า "ปัญหาเพชร" เพราะรูปร่างของแผนภาพการสืบทอดคลาสในสถานการณ์นี้ ในกรณีนี้คลาส A จะอยู่ด้านบนทั้ง B และ C แยกกันอยู่ข้างใต้และ D รวมทั้งสองเข้าด้วยกันที่ด้านล่างเพื่อสร้างรูปทรงเพชร ...


4
ซึ่งมีโซลูชันที่เรียกว่าการสืบทอดเสมือน เป็นเพียงปัญหาถ้าคุณทำผิด
Ian Goldby

1
@IanGoldby: การสืบทอดเสมือนเป็นกลไกในการแก้ปัญหาส่วนหนึ่งหากไม่จำเป็นต้องอนุญาตให้อัปเดตและดาวน์แคสต์แบบรักษาเอกลักษณ์ในทุกประเภทที่อินสแตนซ์ได้รับมาหรือซึ่งสามารถทดแทนได้ ให้ X: B; Y: B; และ Z: X, Y; สมมติว่า someZ เป็นตัวอย่างของ Z ด้วยการสืบทอดเสมือน (B) (X) someZ และ (B) (Y) someZ เป็นวัตถุที่แตกต่างกัน ให้ทั้งสองอย่างหนึ่งสามารถรับอีกคนหนึ่งผ่านทาง downcast และ upcast แต่ถ้ามีsomeZและต้องการที่จะส่งไปObjectยังB? ซึ่งBจะได้รับ?
supercat

2
@supercat บางที แต่ปัญหาเช่นนี้ส่วนใหญ่เป็นทฤษฎีและไม่ว่าในกรณีใด ๆ ก็สามารถส่งสัญญาณโดยคอมไพเลอร์ได้ สิ่งสำคัญคือต้องตระหนักถึงปัญหาที่คุณกำลังพยายามแก้ไขจากนั้นใช้เครื่องมือที่ดีที่สุดโดยไม่สนใจความเชื่อจากคนที่ไม่อยากกังวลกับความเข้าใจว่า 'ทำไม?'
Ian Gold โดย

@IanGoldby: ปัญหาเช่นนี้สามารถส่งสัญญาณโดยคอมไพเลอร์ได้ก็ต่อเมื่อมีการเข้าถึงคลาสทั้งหมดที่เป็นปัญหาพร้อมกัน ในบางเฟรมเวิร์กการเปลี่ยนแปลงใด ๆ กับคลาสฐานจะต้องมีการคอมไพล์ใหม่ของคลาสที่ได้รับทั้งหมดเสมอ แต่ความสามารถในการใช้คลาสพื้นฐานเวอร์ชันใหม่กว่าโดยไม่ต้องคอมไพล์คลาสที่ได้รับซ้ำ (ซึ่งอาจไม่มีซอร์สโค้ด) เป็นคุณสมบัติที่มีประโยชน์ สำหรับกรอบที่สามารถให้ได้ นอกจากนี้ปัญหาไม่ได้เป็นเพียงทางทฤษฎี หลายคลาสใน. NET ขึ้นอยู่กับความจริงที่ว่านักแสดงจากประเภทอ้างอิงใด ๆ ไปObjectยังประเภทนั้น ...
supercat

3
@IanGoldby: ยุติธรรมพอแล้ว ประเด็นของฉันคือผู้ใช้ Java และ. NET ไม่ใช่แค่ "ขี้เกียจ" ในการตัดสินใจไม่สนับสนุน MI ทั่วไป การสนับสนุน MI โดยทั่วไปจะป้องกันไม่ให้กรอบการทำงานของพวกเขาสนับสนุนสัจพจน์ต่างๆซึ่งความถูกต้องมีประโยชน์ต่อผู้ใช้จำนวนมากมากกว่าที่ MI จะเป็น
supercat

21

การสืบทอดหลายรายการเป็นหนึ่งในสิ่งเหล่านั้นที่ไม่ได้ใช้บ่อยและสามารถนำไปใช้ในทางที่ผิดได้ แต่บางครั้งก็จำเป็น

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


คุณอธิบายได้ไหมว่าทำไมพวกเขาไม่ยอมให้คุณบังคับใช้เงื่อนไขก่อนและหลัง
Yttrill

2
@Yttrill เนื่องจากอินเทอร์เฟซไม่สามารถใช้งานเมธอดได้ คุณวางไว้assertที่ไหน?
ซอกแซก

1
@curiousguy: คุณใช้ภาษาที่มีไวยากรณ์ที่เหมาะสมซึ่งช่วยให้คุณใส่เงื่อนไขก่อนและหลังลงในอินเทอร์เฟซได้โดยตรง: ไม่จำเป็นต้อง "ยืนยัน" ตัวอย่างจาก Felix: fun div (num: int, den: int เมื่อ den! = 0): int expect result == 0 หมายถึง num == 0;
Yttrill

@Yttrill OK แต่บางภาษาเช่น Java ไม่รองรับ MI หรือ "เงื่อนไขก่อนและหลังลงในอินเทอร์เฟซโดยตรง"
ซอกแซก

มันไม่ได้ใช้บ่อยเพราะมันไม่สามารถใช้ได้และเราไม่รู้ว่าจะใช้มันอย่างไรดี หากคุณดูรหัส Scala คุณจะเห็นว่าสิ่งต่าง ๆ เริ่มเป็นเรื่องธรรมดาและสามารถปรับโครงสร้างใหม่ให้เป็นลักษณะได้อย่างไร (ตกลงไม่ใช่ MI แต่เป็นข้อพิสูจน์ของฉัน)
santiagobasulto

16

สมมติว่าคุณมีอ็อบเจกต์ A และ B ซึ่งทั้งคู่สืบทอดมาโดย C A และ B ทั้งคู่ใช้ foo () และ C ไม่ได้ ฉันเรียก C.foo () เลือกใช้งานใด มีปัญหาอื่น ๆ แต่ประเภทนี้เป็นเรื่องใหญ่


1
แต่นั่นไม่ใช่ตัวอย่างที่เป็นรูปธรรมจริงๆ หากทั้ง A และ B มีฟังก์ชันเป็นไปได้มากที่ C จะต้องใช้งานเป็นของตัวเองเช่นกัน มิฉะนั้นก็ยังสามารถเรียก A :: foo () ในฟังก์ชัน foo () ของตัวเองได้
Peter Kühne

@Quantum: ถ้าไม่เป็นเช่นนั้นล่ะ? มันง่ายที่จะเห็นปัญหาเกี่ยวกับการสืบทอดระดับหนึ่ง แต่ถ้าคุณมีเลเวลมากมายและคุณมีฟังก์ชันสุ่มที่อยู่ที่ไหนสักแห่งสองครั้งสิ่งนี้จะกลายเป็นปัญหาที่ยากมาก
tloach

นอกจากนี้ประเด็นไม่ได้อยู่ที่คุณไม่สามารถเรียกเมธอด A หรือ B โดยระบุวิธีที่คุณต้องการได้ประเด็นก็คือถ้าคุณไม่ระบุก็ไม่มีวิธีที่ดีในการเลือกวิธีใดวิธีหนึ่ง ฉันไม่แน่ใจว่า C ++ จัดการกับสิ่งนี้อย่างไร แต่ถ้ามีใครรู้ว่าจะพูดถึงมันได้หรือไม่?
tloach

2
@tloach - ถ้า C ไม่แก้ไขความกำกวมคอมไพเลอร์จะตรวจพบข้อผิดพลาดนี้และส่งคืนข้อผิดพลาดเวลาคอมไพล์
Eamon Nerbonne

@Earmon - เนื่องจากความหลากหลายถ้า foo () เป็นเสมือนคอมไพเลอร์อาจไม่รู้ด้วยซ้ำในเวลาคอมไพล์ว่านี่จะเป็นปัญหา
tloach

5

ปัญหาหลักของการสืบทอดหลายรายการสรุปได้อย่างดีกับตัวอย่างของ tloach เมื่อรับช่วงจากคลาสพื้นฐานหลายคลาสที่ใช้ฟังก์ชันหรือฟิลด์เดียวกันคอมไพเลอร์จะต้องตัดสินใจว่าจะนำไปใช้งานใด

สิ่งนี้จะแย่ลงเมื่อคุณสืบทอดจากหลายคลาสที่สืบทอดมาจากคลาสพื้นฐานเดียวกัน (มรดกเพชรถ้าคุณวาดต้นไม้มรดกคุณจะได้รูปเพชร)

ปัญหาเหล่านี้ไม่ใช่ปัญหาสำหรับคอมไพเลอร์ที่จะเอาชนะ แต่ทางเลือกที่คอมไพเลอร์ต้องทำในที่นี้ค่อนข้างจะเป็นไปตามอำเภอใจซึ่งทำให้โค้ดใช้งานง่ายน้อยลง

ฉันพบว่าเมื่อทำการออกแบบ OO ที่ดีฉันไม่ต้องการการสืบทอดหลาย ๆ ในกรณีที่ฉันต้องการฉันมักจะพบว่าฉันใช้การสืบทอดเพื่อใช้ฟังก์ชันการทำงานซ้ำในขณะที่การสืบทอดนั้นเหมาะสมสำหรับความสัมพันธ์แบบ "is-a" เท่านั้น

มีเทคนิคอื่น ๆ เช่น mixins ที่แก้ปัญหาเดียวกันและไม่มีปัญหาในการสืบทอดหลาย ๆ


4
การคอมไพล์ไม่จำเป็นต้องทำการเลือกโดยพลการ - มันอาจผิดพลาดได้ ใน C # ประเภทของ([..bool..]? "test": 1)อะไร?
Eamon Nerbonne

4
ใน C ++ คอมไพลเลอร์ไม่เคยทำการเลือกโดยพลการดังกล่าวเป็นข้อผิดพลาดในการกำหนดคลาสที่คอมไพลเลอร์จะต้องทำการเลือกโดยพลการ
ซอกแซก

5

ฉันไม่คิดว่าปัญหาเพชรเป็นปัญหาฉันคิดว่าซับซ้อนไม่มีอะไรอื่น

ปัญหาที่เลวร้ายที่สุดจากมุมมองของฉันที่มีการสืบทอดหลายอย่างคือ RAD - เหยื่อและคนที่อ้างตัวว่าเป็นนักพัฒนา แต่ในความเป็นจริงนั้นติดอยู่กับความรู้เพียงครึ่งเดียว (อย่างดีที่สุด)

โดยส่วนตัวแล้วฉันจะมีความสุขมากถ้าในที่สุดฉันสามารถทำบางอย่างใน Windows Forms เช่นนี้ได้ (ไม่ใช่รหัสที่ถูกต้อง แต่ควรให้แนวคิดแก่คุณ):

public sealed class CustomerEditView : Form, MVCView<Customer>

นี่เป็นปัญหาหลักที่ฉันมีกับการไม่มีมรดกหลายรายการ คุณสามารถทำสิ่งที่คล้ายกันกับอินเทอร์เฟซ แต่มีสิ่งที่ฉันเรียกว่า "s *** code" นี่คือ c *** ซ้ำซากที่เจ็บปวดที่คุณต้องเขียนในแต่ละชั้นเรียนของคุณเพื่อให้ได้บริบทข้อมูลเช่น

ในความคิดของฉันไม่ควรมีความจำเป็นอย่างยิ่งสำหรับการทำโค้ดซ้ำ ๆ ในภาษาสมัยใหม่


ฉันมักจะเห็นด้วย แต่มีแนวโน้มเท่านั้น: มีความจำเป็นจากความซ้ำซ้อนในภาษาใด ๆ เพื่อตรวจจับความผิดพลาด อย่างไรก็ตามคุณควรเข้าร่วมทีมนักพัฒนา Felix เพราะนั่นคือเป้าหมายหลัก ตัวอย่างเช่นการประกาศทั้งหมดจะเกิดซ้ำพร้อมกันและคุณสามารถดูไปข้างหน้าและข้างหลังได้ดังนั้นคุณจึงไม่จำเป็นต้องมีการประกาศไปข้างหน้า (ขอบเขตคือการตั้งค่าอย่างชาญฉลาดเช่นป้ายกำกับ C goto)
Yttrill

ผมเห็นด้วยกับเรื่องนี้ - ฉันเพียงแค่วิ่งเข้าไปในปัญหาที่คล้ายกันที่นี่ ผู้คนพูดถึงปัญหาเพชรพวกเขาอ้างถึงเรื่องศาสนา แต่ในความคิดของฉันมันหลีกเลี่ยงได้ง่ายมาก (เราทุกคนไม่จำเป็นต้องเขียนโปรแกรมของเราเหมือนที่เขียนไลบรารี iostream) การสืบทอดหลายรายการควรใช้อย่างมีเหตุผลเมื่อคุณมีอ็อบเจ็กต์ที่ต้องการฟังก์ชันการทำงานของคลาสฐานที่แตกต่างกันสองคลาสที่ไม่มีฟังก์ชันหรือชื่อฟังก์ชันทับซ้อนกัน ในมือขวามันเป็นเครื่องมือ
jedd.ahyoung

3
@Turing Complete: wrt ไม่มีการซ้ำรหัสใด ๆ : นี่เป็นความคิดที่ดี แต่ไม่ถูกต้องและเป็นไปไม่ได้ มีรูปแบบการใช้งานจำนวนมากและเราต้องการที่จะนำรูปแบบทั่วไปที่เป็นนามธรรมมาไว้ในไลบรารี แต่มันเป็นเรื่องเหลวไหลที่จะนามธรรมทั้งหมดเพราะแม้ว่าเราจะสามารถโหลดความหมายที่จำชื่อทั้งหมดได้สูงเกินไป สิ่งที่คุณต้องการคือความสมดุลที่ดี อย่าลืมว่าการทำซ้ำเป็นสิ่งที่ทำให้เกิดโครงสร้างของสิ่งต่างๆ (รูปแบบหมายถึงความซ้ำซ้อน)
Yttrill

@ lunchmeat317: ความจริงที่ว่ารหัสโดยทั่วไปไม่ควรเขียนในลักษณะที่ 'เพชร' จะก่อให้เกิดปัญหาไม่ได้หมายความว่าผู้ออกแบบภาษา / กรอบงานสามารถเพิกเฉยต่อปัญหาได้ หากเฟรมเวิร์กระบุว่าการอัปคาสต์และดาวน์คาสติ้งจะเก็บรักษาเอกลักษณ์ของอ็อบเจ็กต์ไว้ต้องการอนุญาตให้คลาสเวอร์ชันที่ใหม่กว่าเพิ่มจำนวนประเภทที่สามารถใช้แทนได้โดยไม่เป็นการเปลี่ยนแปลงที่หมดสภาพและต้องการอนุญาตให้สร้างชนิดรันไทม์ ฉันไม่คิดว่ามันจะอนุญาตให้มีการสืบทอดคลาสหลาย ๆ คลาส (เมื่อเทียบกับการสืบทอดอินเทอร์เฟซ) ในขณะที่บรรลุเป้าหมายข้างต้น
supercat

3

Common Lisp Object System (CLOS) เป็นอีกตัวอย่างหนึ่งของสิ่งที่รองรับ MI ในขณะที่หลีกเลี่ยงปัญหาสไตล์ C ++: การสืบทอดจะได้รับค่าเริ่มต้นที่สมเหตุสมผลในขณะที่ยังให้คุณมีอิสระในการตัดสินใจอย่างชัดเจนว่าจะพูดอย่างไรเรียกพฤติกรรมของ super .


ใช่ CLOS เป็นหนึ่งในระบบออบเจ็กต์ที่เหนือชั้นที่สุดนับตั้งแต่มีการเริ่มต้นใช้งานคอมพิวเตอร์สมัยใหม่ในช่วงเวลาที่ผ่านมา :)
rostamn739

2

ไม่มีอะไรผิดในการสืบทอดหลาย ๆ อย่างเอง ปัญหาคือการเพิ่มการสืบทอดหลายรายการให้กับภาษาที่ไม่ได้ออกแบบโดยคำนึงถึงการสืบทอดหลายรายการตั้งแต่เริ่มต้น

ภาษา Eiffel รองรับการสืบทอดหลายรายการโดยไม่มีข้อ จำกัด ด้วยวิธีที่มีประสิทธิภาพและประสิทธิผล แต่ภาษาได้รับการออกแบบตั้งแต่เริ่มต้นเพื่อรองรับ

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

ฉันคิดว่าการสนับสนุนหลายมรดกหรือไม่เป็นเรื่องของการเลือกมากกว่าเป็นเรื่องของลำดับความสำคัญ คุณลักษณะที่ซับซ้อนกว่านั้นต้องใช้เวลามากกว่าในการนำไปใช้และปฏิบัติอย่างถูกต้องและอาจมีการโต้เถียงกันมากขึ้น การใช้งาน C ++ อาจเป็นสาเหตุที่ไม่ได้ใช้การสืบทอดหลายรายการใน C # และ Java ...


1
การสนับสนุน C ++ สำหรับ MI นั้น " มีประสิทธิภาพและประสิทธิผลมาก "?
ซอกแซก

1
จริงๆแล้วมันค่อนข้างเสียในแง่ที่มันไม่เข้ากับคุณสมบัติอื่น ๆ ของ C ++ การมอบหมายงานไม่ทำงานอย่างถูกต้องกับการรับมรดกนับประสาการสืบทอดหลายรายการ (ดูกฎที่ไม่ดีจริงๆ) การสร้างเพชรอย่างถูกต้องเป็นเรื่องยากคณะกรรมการมาตรฐานได้กำหนดลำดับชั้นของข้อยกเว้นเพื่อให้ง่ายและมีประสิทธิภาพแทนที่จะทำอย่างถูกต้อง ในคอมไพเลอร์รุ่นเก่าที่ฉันใช้ในขณะที่ฉันทดสอบสิ่งนี้และการผสม MI สองสามรายการและการใช้งานข้อยกเว้นพื้นฐานมีค่าใช้จ่ายมากกว่ารหัสเมกะไบต์และใช้เวลา 10 นาทีในการรวบรวม.. เป็นเพียงคำจำกัดความ
Yttrill

1
เพชรเป็นตัวอย่างที่ดี ในไอเฟลเพชรได้รับการแก้ไขอย่างชัดเจน ตัวอย่างเช่นสมมติว่านักเรียนและครูทั้งคู่สืบทอดมาจากบุคคล บุคคลนั้นมีปฏิทินดังนั้นทั้งนักเรียนและครูจะสืบทอดปฏิทินนี้ หากคุณสร้างเพชรด้วยการสร้าง TeachingStudent ที่สืบทอดมาจากทั้งครูและนักเรียนคุณอาจตัดสินใจเปลี่ยนชื่อปฏิทินที่สืบทอดมาเพื่อเก็บปฏิทินทั้งสองไว้แยกกันหรือตัดสินใจรวมเข้าด้วยกันเพื่อให้ทำงานเหมือนบุคคลมากขึ้น การสืบทอดหลายอย่างสามารถนำไปใช้ได้อย่างสวยงาม แต่ต้องมีการออกแบบอย่างรอบคอบโดยเฉพาะอย่างยิ่งตั้งแต่เริ่มต้น ...
Christian Lemer

1
คอมไพเลอร์ของ Eiffel ต้องทำการวิเคราะห์โปรแกรมทั่วโลกเพื่อใช้ MI รุ่นนี้อย่างมีประสิทธิภาพ สำหรับวิธีการโทร polymorphic พวกเขาใช้ thunks มอบหมายงานหรือฝึกอบรมเบาบางตามที่อธิบายไว้ที่นี่ สิ่งนี้ไม่เข้ากันได้ดีกับการคอมไพล์แยกของ C ++ และคุณสมบัติการโหลดคลาสของ C # และ Java
cyco130

2

เป้าหมายการออกแบบอย่างหนึ่งของเฟรมเวิร์กเช่น Java และ. NET คือทำให้โค้ดที่คอมไพล์ทำงานกับไลบรารีที่คอมไพล์ไว้ล่วงหน้าเวอร์ชันหนึ่งทำงานได้ดีเท่าเทียมกันกับไลบรารีเวอร์ชันที่ตามมาแม้ว่าเวอร์ชันที่ตามมานั้น เพิ่มคุณสมบัติใหม่ ในขณะที่กระบวนทัศน์ปกติในภาษาเช่น C หรือ C ++ คือการแจกจ่ายไฟล์ปฏิบัติการที่เชื่อมโยงแบบสแตติกซึ่งมีไลบรารีทั้งหมดที่ต้องการ แต่กระบวนทัศน์ใน. NET และ Java คือการแจกจ่ายแอปพลิเคชันเป็นคอลเลกชันของส่วนประกอบที่ "เชื่อมโยง" ในขณะทำงาน .

รูปแบบ COM ที่นำหน้า. NET พยายามใช้วิธีการทั่วไปนี้ แต่ไม่มีการสืบทอดจริง ๆ แต่คำจำกัดความของแต่ละคลาสจะกำหนดทั้งคลาสและอินเทอร์เฟซที่มีชื่อเดียวกันอย่างมีประสิทธิภาพซึ่งมีสมาชิกสาธารณะทั้งหมด อินสแตนซ์เป็นประเภทคลาสในขณะที่การอ้างอิงเป็นประเภทอินเทอร์เฟซ การประกาศคลาสเป็นที่มาจากคลาสอื่นเทียบเท่ากับการประกาศคลาสเป็นการใช้อินเทอร์เฟซของอีกฝ่ายและต้องการให้คลาสใหม่นำสมาชิกสาธารณะทั้งหมดของคลาสที่คลาสใหม่มาใช้ ถ้า Y และ Z มาจาก X แล้ว W มาจาก Y และ Z ก็จะไม่สำคัญว่า Y และ Z จะใช้สมาชิกของ X แตกต่างกันหรือไม่เพราะ Z จะไม่สามารถใช้การนำไปใช้งานได้ - มันจะต้องกำหนด ด้วยตัวเอง W อาจห่อหุ้มอินสแตนซ์ของ Y และ / หรือ Z

ความยากลำบากใน Java และ. NET คือรหัสได้รับอนุญาตให้สืบทอดสมาชิกและเข้าถึงได้โดยปริยายหมายถึงสมาชิกหลัก สมมติว่ามีคลาส WZ ที่เกี่ยวข้องดังนี้:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

ดูเหมือนว่าW.Test()ควรสร้างอินสแตนซ์ของ W เรียกใช้วิธีการเสมือนที่Fooกำหนดไว้ในX. อย่างไรก็ตามสมมติว่า Y และ Z อยู่ในโมดูลที่คอมไพล์แยกกันและแม้ว่าจะถูกกำหนดไว้ข้างต้นเมื่อรวบรวม X และ W แต่ก็มีการเปลี่ยนแปลงและคอมไพล์ใหม่ในภายหลัง:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

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


2

เพชรไม่ใช่ปัญหาตราบใดที่คุณไม่ได้ใช้อะไรเลยเช่นการสืบทอดเสมือนของ C ++: ในการสืบทอดปกติแต่ละคลาสพื้นฐานจะมีลักษณะคล้ายกับฟิลด์สมาชิก (จริงๆแล้วพวกมันถูกจัดวางใน RAM ด้วยวิธีนี้) ทำให้คุณมีน้ำตาลวากยสัมพันธ์และ ความสามารถพิเศษในการแทนที่วิธีการเสมือนจริงเพิ่มเติม นั่นอาจทำให้เกิดความคลุมเครือบางอย่างในเวลาคอมไพล์ แต่มักจะแก้ได้ง่าย

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

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

ใน C ++ เป็นไปไม่ได้เลย: ทันทีที่FและGรวมเข้าเป็นคลาสเดียวAs ของพวกเขาจะถูกรวมเข้าด้วยกันเช่นกัน ซึ่งหมายความว่าคุณอาจไม่เคยพิจารณาฐานเรียนทึบแสงใน C ++ (ในตัวอย่างนี้คุณจะต้องสร้างAในHเพื่อให้คุณได้รู้ว่ามันอยู่ที่ไหนสักแห่งในปัจจุบันในลำดับชั้น) อย่างไรก็ตามในภาษาอื่นอาจใช้งานได้ ตัวอย่างเช่นFและGสามารถประกาศ A อย่างชัดเจนว่าเป็น "ภายใน" ดังนั้นจึงห้ามไม่ให้มีการรวมผลที่ตามมาและทำให้ตัวเองเป็นของแข็งอย่างมีประสิทธิภาพ

อีกตัวอย่างที่น่าสนใจ ( ไม่ใช่เฉพาะ C ++):

  A
 / \
B   B
|   |
C   D
 \ /
  E

ที่นี่Bใช้การสืบทอดเสมือนเท่านั้น ดังนั้นEมีสองBs Aที่ใช้ร่วมกันเดียวกัน ด้วยวิธีนี้คุณจะได้A*ตัวชี้ที่ชี้ไปEแต่คุณไม่สามารถส่งไปยังB*ตัวชี้แม้ว่าวัตถุนั้น ๆเป็นจริงB เป็นนักแสดงดังกล่าวเป็นที่คลุมเครือและความคลุมเครือนี้ไม่สามารถตรวจพบที่รวบรวมเวลา (ยกเว้นกรณีที่คอมไพเลอร์เห็น โปรแกรมทั้งหมด) นี่คือรหัสทดสอบ:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

นอกจากนี้การนำไปใช้งานอาจมีความซับซ้อนมาก (ขึ้นอยู่กับภาษาดูคำตอบของ benjismith)


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