Vấn đề chính xác với đa kế thừa là gì?


121

Tôi có thể thấy mọi người luôn hỏi liệu đa kế thừa có nên được đưa vào phiên bản tiếp theo của C # hay Java hay không. Những người trong C ++, những người đủ may mắn có được khả năng này, nói rằng điều này giống như việc cho ai đó một sợi dây để cuối cùng tự treo cổ mình.

Có vấn đề gì với đa kế thừa? Có mẫu bê tông nào không?


54
Tôi chỉ muốn đề cập rằng C ++ là tuyệt vời để cung cấp cho bạn đủ dây để treo cổ bản thân.
tloach 22/10/08

1
Đối với một thay thế cho đa kế thừa rằng địa chỉ (và, phá được IMHO) nhiều vấn đề tương tự, nhìn vào đặc điểm ( iam.unibe.ch/~scg/Research/Traits )
Bevan

52
Tôi nghĩ C ++ cho bạn đủ sợi dây để tự bắn vào chân mình.
KeithB

6
Câu hỏi này dường như giả định rằng có vấn đề với MI nói chung, trong khi tôi đã tìm thấy rất nhiều ngôn ngữ mà MI được sử dụng bình thường. Chắc chắn có vấn đề với việc xử lý MI của một số ngôn ngữ nhất định, nhưng tôi không biết rằng MI nói chung có những vấn đề đáng kể.
David Thornley,

Câu trả lời:


86

Vấn đề rõ ràng nhất là với chức năng ghi đè.

Giả sử có hai lớp ABcả hai đều xác định một phương thức doSomething. Bây giờ bạn xác định một lớp thứ ba C, lớp này kế thừa từ cả AB, nhưng bạn không ghi đè doSomethingphương thức.

Khi trình biên dịch bắt nguồn mã này ...

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

... nó nên sử dụng phương pháp thực hiện nào? Nếu không có bất kỳ giải thích rõ ràng nào, trình biên dịch không thể giải quyết sự mơ hồ.

Bên cạnh việc ghi đè, một vấn đề lớn khác với đa kế thừa là cách bố trí các đối tượng vật lý trong bộ nhớ.

Các ngôn ngữ như C ++, Java và C # tạo ra một bố cục dựa trên địa chỉ cố định cho từng loại đối tượng. Một cái gì đó như thế này:

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

Khi trình biên dịch tạo mã máy (hoặc mã bytecode), nó sử dụng các hiệu số đó để truy cập từng phương thức hoặc trường.

Việc thừa kế nhiều lần làm cho nó rất phức tạp.

Nếu lớp Ckế thừa từ cả hai AB, trình biên dịch phải quyết định xem có nên bố trí dữ liệu trongAB thứ tự hay theo BAthứ tự.

Nhưng bây giờ hãy tưởng tượng rằng bạn đang gọi các phương thức trên một Bđối tượng. Nó thực sự chỉ là một B? Hay nó thực sự là mộtC đối tượng được gọi là đa hình, thông qua Bgiao diện của nó ? Tùy thuộc vào nhận dạng thực tế của đối tượng, bố cục vật lý sẽ khác nhau và không thể biết được phần bù của hàm để gọi tại trang web gọi.

Cách để xử lý loại hệ thống này là loại bỏ cách tiếp cận bố cục cố định, cho phép từng đối tượng được truy vấn về bố cục của nó trước khi cố gắng gọi các hàm hoặc truy cập các trường của nó.

Vì vậy, ... truyện dài ngắn ... thật là đau đầu cho các tác giả biên dịch để hỗ trợ đa kế thừa. Vì vậy, khi ai đó như Guido van Rossum thiết kế python hoặc khi Anders Hejlsberg thiết kế c #, họ biết rằng việc hỗ trợ đa kế thừa sẽ làm cho việc triển khai trình biên dịch phức tạp hơn đáng kể và có lẽ họ không nghĩ rằng lợi ích đó xứng đáng với chi phí.


62
Ehm, Python hỗ trợ MI
Nemanja Trifunović

26
Đây không phải là những lập luận thuyết phục lắm - bố cục cố định không khó chút nào trong hầu hết các ngôn ngữ; trong C ++, nó phức tạp vì bộ nhớ không mờ và do đó bạn có thể gặp một số khó khăn với các giả định số học con trỏ. Trong các ngôn ngữ mà định nghĩa lớp là tĩnh (như trong java, C # và C ++), xung đột tên nhiều kế thừa có thể bị cấm thời gian biên dịch (và C # thực hiện điều này với các giao diện!).
Eamon Nerbonne 24/09/09

10
OP chỉ muốn hiểu các vấn đề và tôi đã giải thích chúng mà không biên tập riêng về vấn đề này. Tôi chỉ nói rằng các nhà thiết kế ngôn ngữ và trình biên dịch "có lẽ không nghĩ rằng lợi ích là xứng đáng với chi phí".
benjismith, 24/09/09

12
" Vấn đề rõ ràng nhất là ghi đè hàm. " Điều này không liên quan gì đến ghi đè hàm. Đó là một vấn đề mơ hồ đơn giản.
curiousguy

10
Câu trả lời này có một số thông tin sai về Guido và Python, vì Python hỗ trợ MI. "Tôi đã quyết định rằng miễn là tôi sẽ ủng hộ việc kế thừa, tôi cũng có thể ủng hộ một phiên bản đa kế thừa có tư duy đơn giản." - Guido van Rossum python-history.blogspot.com/2009/02/… - Ngoài ra, việc giải quyết sự không rõ ràng là khá phổ biến trong các trình biên dịch (các biến có thể là cục bộ để khối, cục bộ cho hàm, cục bộ để bao hàm, thành viên đối tượng, thành viên lớp, hình cầu, v.v.), tôi không thấy phạm vi bổ sung sẽ tạo ra sự khác biệt như thế nào.
marcus

46

Những vấn đề mà các bạn đề cập thực ra không quá khó để giải quyết. Trong thực tế, ví dụ như Eiffel làm điều đó hoàn toàn tốt! (và không giới thiệu các lựa chọn tùy ý hoặc bất cứ điều gì)

Ví dụ: nếu bạn kế thừa từ A và B, cả hai đều có phương thức foo (), thì tất nhiên bạn không muốn có một lựa chọn tùy ý trong lớp C của mình kế thừa từ cả A và B. Bạn phải xác định lại foo để rõ ràng sẽ là gì được sử dụng nếu c.foo () được gọi hoặc nếu không bạn phải đổi tên một trong các phương thức trong C. (nó có thể trở thành bar ())

Ngoài ra tôi nghĩ rằng đa kế thừa thường khá hữu ích. Nếu bạn nhìn vào các thư viện của Eiffel, bạn sẽ thấy rằng nó được sử dụng khắp nơi và cá nhân tôi đã bỏ lỡ tính năng này khi tôi phải quay lại lập trình bằng Java.


26
Tôi đồng ý. Lý do chính khiến mọi người ghét MI cũng giống như với JavaScript hoặc với tính năng nhập tĩnh: hầu hết mọi người đã từng sử dụng các triển khai rất tệ của nó - hoặc đã sử dụng nó rất tệ. Đánh giá MI bằng C ++ cũng giống như đánh giá OOP bằng PHP hoặc đánh giá xe ô tô bằng Pintos.
Jörg W Mittag 13/12/08

2
@curiousguy: MI giới thiệu thêm một bộ phức tạp khác cần lo lắng, giống như nhiều "tính năng" của C ++. Chỉ vì nó rõ ràng không làm cho nó dễ dàng làm việc với hoặc gỡ lỗi. Xóa chuỗi này vì nó lạc đề và bạn vẫn làm hỏng nó.
Guvante

4
@Guvante vấn đề duy nhất với MI trong bất kỳ ngôn ngữ nào là các lập trình viên tồi nghĩ rằng họ có thể đọc một hướng dẫn và đột nhiên biết một ngôn ngữ.
Miles rout

2
Tôi sẽ tranh luận rằng các tính năng ngôn ngữ không chỉ giúp giảm thời gian viết mã. Chúng cũng nhằm tăng tính biểu cảm của một ngôn ngữ và tăng hiệu suất.
Miles Rout

4
Ngoài ra, lỗi chỉ xảy ra từ MI khi những kẻ ngốc sử dụng nó không đúng cách.
Miles Rout,

27

Vấn đề kim cương :

một sự mơ hồ nảy sinh khi hai lớp B và C kế thừa từ A và lớp D kế thừa từ cả B và C. Nếu có một phương thức trong A mà B và C đã ghi đè và D không ghi đè nó, thì phiên bản nào của phương thức nào D kế thừa: của B, hay của C?

... Nó được gọi là "vấn đề kim cương" vì hình dạng của sơ đồ kế thừa lớp trong tình huống này. Trong trường hợp này, lớp A ở trên cùng, cả B và C riêng biệt bên dưới nó, và D kết hợp cả hai với nhau ở phía dưới để tạo thành hình thoi ...


4
có một giải pháp được gọi là kế thừa ảo. Nó chỉ là một vấn đề nếu bạn làm điều đó sai.
Ian Goldby

1
@IanGoldby: Kế thừa ảo là một cơ chế để giải quyết một phần của vấn đề, nếu người ta không cần cho phép các luồng lên và xuống bảo toàn danh tính giữa tất cả các loại mà từ đó một thể hiện được tạo ra hoặc nó có thể thay thế được . Cho X: B; Y: B; và Z: X, Y; giả sử someZ là một thể hiện của Z. Với thừa kế ảo, (B) (X) someZ và (B) (Y) someZ là các đối tượng riêng biệt; cho một trong hai, người ta có thể lấy cái kia thông qua một downcast và upcast, nhưng điều gì sẽ xảy ra nếu một người có một someZvà muốn truyền nó tới Objectrồi đến B? Mà Bnó sẽ nhận được?
supercat

2
@supercat Có lẽ, nhưng các vấn đề như vậy chủ yếu là lý thuyết và trong mọi trường hợp đều có thể được trình biên dịch báo hiệu. Điều quan trọng là nhận thức được vấn đề bạn đang cố gắng giải quyết và sau đó sử dụng công cụ tốt nhất, bỏ qua giáo điều từ những người không muốn bản thân hiểu 'tại sao?'
Ian Goldby

@IanGoldby: Các vấn đề như vậy chỉ có thể được trình biên dịch báo hiệu nếu nó có quyền truy cập đồng thời vào tất cả các lớp được đề cập. Trong một số khung công tác, bất kỳ thay đổi nào đối với lớp cơ sở sẽ luôn yêu cầu biên dịch lại tất cả các lớp dẫn xuất, nhưng khả năng sử dụng các phiên bản mới hơn của các lớp cơ sở mà không cần phải biên dịch lại các lớp dẫn xuất (mà một lớp có thể không có mã nguồn) là một tính năng hữu ích cho các khuôn khổ có thể cung cấp nó. Hơn nữa, các vấn đề không chỉ là lý thuyết. Nhiều lớp trong .NET dựa vào thực tế là một dàn diễn viên từ bất kỳ loại tài liệu tham khảo để Objectvà trở lại kiểu đó ...
supercat

3
@IanGoldby: Đủ công bằng. Quan điểm của tôi là những người triển khai Java và .NET không chỉ "lười biếng" trong việc quyết định không hỗ trợ MI tổng quát; hỗ trợ MI tổng quát sẽ ngăn cản khuôn khổ của họ đề cao các tiên đề khác nhau mà giá trị của nó hữu ích hơn cho nhiều người dùng so với MI.
supercat

21

Đa kế thừa là một trong những thứ không được sử dụng thường xuyên, và có thể bị lạm dụng, nhưng đôi khi vẫn cần thiết.

Tôi chưa bao giờ hiểu rằng không thêm một tính năng, chỉ vì nó có thể bị sử dụng sai, khi không có lựa chọn thay thế tốt. Giao diện không phải là một sự thay thế cho đa kế thừa. Đối với một, họ không cho phép bạn thực thi các điều kiện trước hoặc điều kiện sau. Cũng giống như bất kỳ công cụ nào khác, bạn cần biết khi nào thích hợp để sử dụng và cách sử dụng nó.


Bạn có thể giải thích tại sao họ không để bạn thực thi các điều kiện trước và sau không?
Yttrill

2
@Yttrill vì giao diện không thể có triển khai phương thức. Bạn đặt ở assertđâu?
tò mò

1
@curiousguy: bạn sử dụng ngôn ngữ có cú pháp phù hợp cho phép bạn đưa các điều kiện trước và sau trực tiếp vào giao diện: không cần "khẳng định". Ví dụ từ Felix: fun div (num: int, den: int khi den! = 0): int mong đợi kết quả == 0 ngụ ý num == 0;
Yttrill

@Yttrill OK, nhưng một số ngôn ngữ, như Java, không hỗ trợ MI hoặc "điều kiện trước và sau trực tiếp vào giao diện".
tò mò

Nó không được sử dụng thường xuyên vì nó không có sẵn và chúng ta không biết cách sử dụng nó tốt. Nếu bạn xem qua một số mã Scala, bạn sẽ thấy mọi thứ bắt đầu phổ biến như thế nào và có thể được cấu trúc lại thành các đặc điểm (Ok, đó không phải là MI, nhưng chứng minh quan điểm của tôi).
santiagobasulto

16

giả sử bạn có các đối tượng A và B đều được kế thừa bởi C. A và B đều triển khai foo () và C thì không. Tôi gọi C.foo (). Việc triển khai nào được chọn? Có những vấn đề khác, nhưng loại vấn đề này là một vấn đề lớn.


1
Nhưng đó không hẳn là một ví dụ cụ thể. Nếu cả A và B đều có một hàm, rất có thể C cũng sẽ cần triển khai riêng của nó. Nếu không, nó vẫn có thể gọi A :: foo () trong hàm foo () của chính nó.
Peter Kühne 22/10/08

@Quantum: Nếu không thì sao? Dễ dàng nhận thấy vấn đề với một cấp độ kế thừa, nhưng nếu bạn có nhiều cấp độ và bạn có một số chức năng ngẫu nhiên ở đâu đó hai lần thì điều này sẽ trở thành một vấn đề rất khó khăn.
tloach 22/10/08

Ngoài ra, vấn đề không phải là bạn không thể gọi phương thức A hoặc B bằng cách chỉ định phương thức nào bạn muốn, vấn đề là nếu bạn không chỉ định thì không có cách nào tốt để chọn một phương thức. Tôi không chắc về cách C ++ xử lý điều này, nhưng nếu ai đó biết có thể đề cập đến nó không?
tloach 22/10/08

2
@tloach - nếu C không giải quyết được sự mơ hồ, trình biên dịch có thể phát hiện lỗi này và trả về lỗi thời gian biên dịch.
Eamon Nerbonne 24/09/09

@Earmon - Do tính đa hình, nếu foo () là ảo, trình biên dịch thậm chí có thể không biết tại thời điểm biên dịch rằng đây sẽ là một vấn đề.
tloach

5

Vấn đề chính với đa kế thừa được tóm tắt độc đáo với ví dụ của tloach. Khi kế thừa từ nhiều lớp cơ sở triển khai cùng một chức năng hoặc trường, trình biên dịch phải đưa ra quyết định về việc triển khai nào sẽ kế thừa.

Điều này trở nên tồi tệ hơn khi bạn kế thừa từ nhiều lớp kế thừa từ cùng một lớp cơ sở. (kim cương thừa kế, nếu bạn vẽ cây thừa kế, bạn sẽ có một hình kim cương)

Những vấn đề này không thực sự là vấn đề đối với một trình biên dịch để khắc phục. Nhưng sự lựa chọn mà trình biên dịch phải thực hiện ở đây là khá tùy ý, điều này làm cho mã kém trực quan hơn nhiều.

Tôi thấy rằng khi thiết kế OO tốt, tôi không bao giờ cần đa kế thừa. Trong những trường hợp cần thiết, tôi thường thấy rằng mình đang sử dụng tính năng thừa kế để sử dụng lại chức năng trong khi tính năng thừa kế chỉ thích hợp cho quan hệ "is-a".

Có những kỹ thuật khác như mixin giải quyết các vấn đề tương tự và không có các vấn đề như đa kế thừa.


4
Trình biên dịch không cần phải thực hiện một lựa chọn tùy ý - nó chỉ có thể xảy ra lỗi. Trong C #, loại của là ([..bool..]? "test": 1)gì?
Eamon Nerbonne 24/09/09

4
Trong C ++, trình biên dịch không bao giờ đưa ra các lựa chọn tùy ý như vậy: đó là một lỗi khi xác định một lớp mà trình biên dịch sẽ cần đưa ra lựa chọn tùy ý.
tò mò

5

Tôi không nghĩ vấn đề kim cương là một vấn đề, tôi sẽ coi đó là sự ngụy biện, không có gì khác.

Vấn đề tồi tệ nhất, theo quan điểm của tôi, với đa kế thừa là RAD - nạn nhân và những người tự nhận là nhà phát triển nhưng thực tế lại bị mắc kẹt với một nửa kiến ​​thức (tốt nhất là).

Cá nhân tôi sẽ rất vui nếu cuối cùng tôi có thể làm điều gì đó trong Windows Forms như thế này (nó không phải là mã chính xác, nhưng nó sẽ cung cấp cho bạn ý tưởng):

public sealed class CustomerEditView : Form, MVCView<Customer>

Đây là vấn đề chính mà tôi gặp phải khi không có đa thừa kế. Bạn CÓ THỂ làm điều gì đó tương tự với các giao diện, nhưng có cái mà tôi gọi là "mã s ***", đó là c *** lặp đi lặp lại gây đau đớn này mà bạn phải viết trong mỗi lớp của mình để lấy ngữ cảnh dữ liệu chẳng hạn.

Theo tôi, hoàn toàn không cần, không phải nhỏ nhất, đối với BẤT KỲ sự lặp lại mã nào trong ngôn ngữ hiện đại.


Tôi có xu hướng đồng ý, nhưng chỉ có xu hướng: cần có một số dư thừa trong bất kỳ ngôn ngữ nào để phát hiện lỗi sai. Nhưng dù sao thì bạn cũng nên tham gia nhóm nhà phát triển Felix vì đó là mục tiêu cốt lõi. Ví dụ: tất cả các khai báo là đệ quy lẫn nhau và bạn có thể nhìn về phía trước cũng như ngược lại, do đó bạn không cần khai báo chuyển tiếp (phạm vi được thiết lập khôn ngoan, như nhãn C goto).
Yttrill

Tôi hoàn toàn đồng ý với điều này - Tôi vừa gặp phải một vấn đề tương tự ở đây . Mọi người nói về vấn đề kim cương, họ trích dẫn nó một cách tôn giáo, nhưng theo tôi thì nó rất dễ bị tránh. (Tất cả chúng ta không cần phải viết chương trình của mình giống như họ đã viết thư viện iostream.) Tính kế thừa nên được sử dụng một cách hợp lý khi bạn có một đối tượng cần chức năng của hai lớp cơ sở khác nhau không có chức năng hoặc tên chức năng trùng lặp. Trong tay phải, nó là một công cụ.
jedd.ahyoung

3
@Turing Complete: wrt không có bất kỳ đoạn mã lặp lại nào: đây là một ý tưởng hay nhưng nó không chính xác và không thể thực hiện được. Có một số lượng lớn các mẫu sử dụng và chúng tôi muốn trừu tượng hóa những mẫu phổ biến vào thư viện, nhưng việc trừu tượng hóa tất cả chúng là điều vô ích vì ngay cả khi chúng ta có thể tải ngữ nghĩa nhớ tất cả các tên là quá cao. Những gì bạn muốn là một sự cân bằng tốt đẹp. Đừng quên sự lặp lại là thứ mang lại cấu trúc cho mọi thứ (khuôn mẫu ngụ ý sự dư thừa).
Yttrill

@ trưameat317: Thực tế là mã thường không nên được viết theo cách mà 'viên kim cương' sẽ gây ra vấn đề, không có nghĩa là một nhà thiết kế ngôn ngữ / khuôn khổ có thể bỏ qua vấn đề này. Nếu một khuôn khổ cung cấp việc dự báo lên và dự báo xuống bảo toàn danh tính đối tượng, muốn cho phép các phiên bản sau của một lớp để tăng số loại mà nó có thể được thay thế mà không phải thay đổi đột ngột và muốn cho phép tạo kiểu thời gian chạy, Tôi không nghĩ rằng nó có thể cho phép kế thừa nhiều lớp (trái ngược với kế thừa giao diện) trong khi đáp ứng các mục tiêu trên.
supercat

3

Hệ thống đối tượng Lisp chung (CLOS) là một ví dụ khác về cái gì đó hỗ trợ MI trong khi tránh các vấn đề về kiểu C ++: kế thừa được cung cấp một mặc định hợp lý , trong khi vẫn cho phép bạn tự do quyết định rõ ràng cách chính xác, ví dụ, gọi hành vi của siêu .


Vâng, Clos là một trong hầu hết các hệ thống đối tượng vượt trội kể từ khi khởi đầu của máy tính hiện đại trong thậm chí có từ lâu trong quá khứ :)
rostamn739

2

Không có gì sai trong bản thân đa kế thừa. Vấn đề là thêm đa kế thừa vào một ngôn ngữ không được thiết kế với đa kế thừa ngay từ đầu.

Ngôn ngữ Eiffel đang hỗ trợ đa kế thừa mà không có hạn chế theo cách rất hiệu quả và hiệu quả nhưng ngôn ngữ này được thiết kế ngay từ đầu để hỗ trợ nó.

Tính năng này rất phức tạp để triển khai đối với các nhà phát triển trình biên dịch, nhưng có vẻ như nhược điểm đó có thể được bù đắp bằng thực tế là hỗ trợ đa kế thừa tốt có thể tránh được sự hỗ trợ của các tính năng khác (tức là không cần Giao diện hoặc Phương thức mở rộng).

Tôi nghĩ rằng hỗ trợ đa kế thừa hay không là vấn đề của sự lựa chọn, vấn đề ưu tiên. Một tính năng phức tạp hơn cần nhiều thời gian hơn để được triển khai và vận hành chính xác và có thể gây tranh cãi nhiều hơn. Việc triển khai C ++ có thể là lý do tại sao đa kế thừa không được triển khai trong C # và Java ...


1
Hỗ trợ C ++ cho MI không " rất hiệu quả và năng suất "?
tò mò

1
Trên thực tế, nó hơi bị hỏng theo nghĩa là nó không phù hợp với các tính năng khác của C ++. Phép gán không hoạt động đúng với kế thừa, chưa nói đến đa kế thừa (hãy kiểm tra các quy tắc thực sự không tốt). Việc tạo ra những viên kim cương một cách chính xác là rất khó, ủy ban Tiêu chuẩn đã vặn chặt hệ thống phân cấp ngoại lệ để giữ cho nó đơn giản và hiệu quả hơn là làm đúng. Trên một trình biên dịch cũ hơn mà tôi đang sử dụng vào thời điểm đó, tôi đã thử nghiệm điều này và một vài MI mixin và triển khai các ngoại lệ cơ bản có giá trên một Megabyte mã và mất 10 phút để biên dịch .. chỉ là các định nghĩa.
Yttrill

1
Kim cương là một ví dụ điển hình. Trong Eiffel, viên kim cương được giải quyết một cách rõ ràng. Ví dụ, hãy tưởng tượng Học sinh và Giáo viên đều kế thừa từ Người. Người có lịch, vì vậy cả Học sinh và Giáo viên sẽ kế thừa lịch này. Nếu bạn tạo một hình kim cương bằng cách tạo một TeachingStudent kế thừa từ cả Giáo viên và Học sinh, bạn có thể quyết định đổi tên một trong các lịch kế thừa để giữ cho cả hai lịch có sẵn riêng biệt hoặc quyết định hợp nhất chúng để nó hoạt động giống Người hơn. Nhiều thừa kế có thể được thực hiện độc đáo, nhưng nó đòi hỏi một thiết kế cẩn thận một tốt ngay từ đầu ...
Christian Lemer

1
Các trình biên dịch của Eiffel phải phân tích chương trình toàn cầu để triển khai mô hình MI này một cách hiệu quả. Đối với các cuộc gọi phương thức đa hình, chúng sử dụng thu thập bộ điều phối hoặc ma trận thưa thớt như được giải thích ở đây . Điều này không kết hợp tốt với biên dịch riêng biệt của C ++ và tính năng tải lớp của C # và Java.
cyco130

2

Một trong những mục tiêu thiết kế của các khuôn khổ như Java và .NET là làm cho mã được biên dịch có thể hoạt động với một phiên bản của thư viện được biên dịch trước, hoạt động tốt như nhau với các phiên bản tiếp theo của thư viện đó, ngay cả khi các phiên bản tiếp theo đó thêm các tính năng mới. Trong khi mô hình thông thường trong các ngôn ngữ như C hoặc C ++ là phân phối các tệp thực thi được liên kết tĩnh có chứa tất cả các thư viện mà chúng cần, mô hình trong .NET và Java là phân phối các ứng dụng dưới dạng tập hợp các thành phần được "liên kết" tại thời điểm chạy .

Mô hình COM trước .NET đã cố gắng sử dụng cách tiếp cận chung này, nhưng nó không thực sự có tính kế thừa - thay vào đó, mỗi định nghĩa lớp xác định hiệu quả cả một lớp và một giao diện cùng tên chứa tất cả các thành viên công khai của nó. Các phiên bản thuộc loại lớp, trong khi các tham chiếu thuộc loại giao diện. Việc khai báo một lớp là dẫn xuất từ ​​lớp khác tương đương với việc khai báo một lớp là triển khai giao diện của lớp kia và yêu cầu lớp mới triển khai lại tất cả các thành viên công khai của các lớp mà lớp đó dẫn xuất từ ​​đó. Nếu Y và Z bắt nguồn từ X và sau đó W bắt nguồn từ Y và Z, sẽ không thành vấn đề nếu Y và Z triển khai các thành viên của X khác nhau, bởi vì Z sẽ không thể sử dụng các triển khai của chúng - nó sẽ phải xác định sở hữu. W có thể bao gồm các trường hợp của Y và / hoặc Z,

Khó khăn trong Java và .NET là mã được phép kế thừa các thành viên và có quyền truy cập vào chúng ngầm tham chiếu đến các thành viên mẹ. Giả sử một có các lớp WZ liên quan như trên:

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

Có vẻ như W.Test()nên tạo một phiên bản của cuộc gọi W để thực hiện phương thức ảo Foođược định nghĩa trong X. Tuy nhiên, giả sử rằng Y và Z thực sự nằm trong một mô-đun được biên dịch riêng và mặc dù chúng được định nghĩa như trên khi X và W được biên dịch, nhưng sau đó chúng đã được thay đổi và biên dịch lại:

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

Bây giờ tác dụng của việc gọi là W.Test()gì? Nếu chương trình phải được liên kết tĩnh trước khi phân phối, giai đoạn liên kết tĩnh có thể nhận ra rằng mặc dù chương trình không có sự mơ hồ trước khi thay đổi Y và Z, nhưng những thay đổi đối với Y và Z đã khiến mọi thứ trở nên mơ hồ và trình liên kết có thể từ chối xây dựng chương trình trừ khi hoặc cho đến khi sự mơ hồ đó được giải quyết. Mặt khác, có thể người có cả W và các phiên bản mới của Y và Z là người chỉ muốn chạy chương trình và không có mã nguồn nào cho nó. Khi W.Test()chạy, nó sẽ không còn rõ ràngW.Test() thì nên làm, nhưng cho đến khi người dùng cố gắng chạy W với phiên bản mới của Y và Z, sẽ không có cách nào bất kỳ bộ phận nào của hệ thống có thể nhận ra có sự cố (trừ khi W được coi là không hợp lệ ngay cả trước khi thay đổi thành Y và Z ).


2

Kim cương không phải là vấn đề, miễn là bạn không sử dụng bất kỳ thứ gì như thừa kế ảo C ++: trong kế thừa thông thường, mỗi lớp cơ sở giống với một trường thành viên (thực sự chúng được bố trí trong RAM theo cách này), cung cấp cho bạn một số cú pháp và một thêm khả năng ghi đè nhiều phương thức ảo hơn. Điều đó có thể gây ra một số mơ hồ tại thời điểm biên dịch nhưng điều đó thường dễ giải quyết.

Mặt khác, với thừa kế ảo, nó quá dễ dàng vượt quá tầm kiểm soát (và sau đó trở thành một mớ hỗn độn). Hãy coi như một ví dụ về sơ đồ "trái tim":

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

Trong C ++, điều đó là hoàn toàn không thể: ngay sau khi FGđược hợp nhất thành một lớp, các ký tự của chúng Acũng được hợp nhất, dấu chấm. Điều đó có nghĩa là bạn có thể không bao giờ coi các lớp cơ sở là không rõ ràng trong C ++ (trong ví dụ này, bạn phải xây dựng Atrong Hđó để bạn phải biết rằng nó hiện diện ở đâu đó trong hệ thống phân cấp). Tuy nhiên, trong các ngôn ngữ khác, nó có thể hoạt động; ví dụ, FGcó thể tuyên bố rõ ràng A là "nội bộ", do đó ngăn cấm việc hợp nhất do hậu quả và làm cho chúng trở nên vững chắc một cách hiệu quả.

Một ví dụ thú vị khác ( không phải C ++ - cụ thể):

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

Ở đây, chỉ Bsử dụng kế thừa ảo. Vì vậy, Echứa hai Bs chia sẻ cùng một A. Bằng cách này, bạn có thể nhận được một A*con trỏ trỏ đến E, nhưng bạn không thể bỏ nó vào một B*con trỏ mặc dù đối tượng thực sự B như dàn diễn viên như vậy là mơ hồ, và sự mơ hồ này không thể được phát hiện tại thời gian biên dịch (trừ khi trình biên dịch thấy toàn bộ chương trình). Đây là mã kiểm tra:

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

Hơn nữa, việc triển khai có thể rất phức tạp (phụ thuộc vào ngôn ngữ; xem câu trả lời của benjismith).


Đó là vấn đề thực sự với MI. Các lập trình viên có thể cần các độ phân giải khác nhau trong một lớp. Một giải pháp toàn ngôn ngữ sẽ hạn chế những gì có thể xảy ra và buộc các lập trình viên phải tạo kludges để chương trình hoạt động chính xác.
shawnhcorey
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.