Tại sao C # được tạo ra với các từ khóa mới và các trò chơi ảo + ghi đè của ảo mới không giống như Java?


61

Trong Java không có virtual, new, overridetừ khóa cho định nghĩa phương pháp. Vì vậy, việc làm của một phương pháp là dễ hiểu. Nguyên nhân là nếu DeruredClass mở rộng BaseClass và có một phương thức có cùng tên và cùng chữ ký của BaseClass thì việc ghi đè sẽ diễn ra ở đa hình thời gian chạy (với điều kiện là phương thức này không static).

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

Bây giờ đến với C # có thể có rất nhiều nhầm lẫn và khó hiểu làm thế nào newhoặc virtual+derivehoặc ghi đè ảo mới hoặc hoạt động.

Tôi không thể hiểu tại sao trên thế giới tôi sẽ thêm một phương thức trong DerivedClasscùng tên và cùng chữ ký BaseClassvà xác định một hành vi mới nhưng ở đa hình thời gian chạy, BaseClassphương thức sẽ được gọi! (đó không phải là ghi đè nhưng phải hợp lý).

Trong trường hợp virtual + overridemặc dù triển khai logic là chính xác, nhưng lập trình viên phải suy nghĩ phương pháp nào anh ta nên cấp phép cho người dùng ghi đè tại thời điểm mã hóa. Trong đó có một số pro-con (chúng ta đừng đến đó ngay bây giờ).

Vậy tại sao trong C # có quá nhiều không gian cho lý luận và sự nhầm lẫn phi lý. Vì vậy, tôi có thể điều chỉnh lại câu hỏi của mình như trong bối cảnh thế giới thực nào tôi nên nghĩ đến việc sử dụng virtual + overridethay vì newvà cũng sử dụng newthay vì virtual + override?


Sau một số câu trả lời rất hay đặc biệt là từ Omar , tôi nhận thấy rằng các nhà thiết kế C # đã nhấn mạnh hơn về các lập trình viên nên suy nghĩ trước khi họ thực hiện một phương pháp, điều này tốt và xử lý một số lỗi tân binh từ Java.

Bây giờ tôi có một câu hỏi trong đầu. Như trong Java nếu tôi có một mã như

Vehicle vehicle = new Car();
vehicle.accelerate();

và sau này tôi tạo lớp mới SpaceShipbắt nguồn từ Vehicle. Sau đó, tôi muốn thay đổi tất cả carthành một SpaceShipđối tượng tôi chỉ phải thay đổi một dòng mã

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

Điều này sẽ không phá vỡ bất kỳ logic nào của tôi tại bất kỳ điểm mã nào.

Nhưng trong trường hợp C # nếu SpaceShipkhông ghi đè Vehiclelớp ' acceleratevà sử dụng newthì logic mã của tôi sẽ bị hỏng. Đó không phải là một bất lợi sao?


77
Bạn chỉ quen với cách Java thực hiện và đơn giản là bạn đã không dành thời gian để hiểu các từ khóa C # theo cách riêng của chúng. Tôi làm việc với C #, hiểu các thuật ngữ ngay lập tức và tìm ra cách mà Java làm điều đó kỳ quặc.
Robert Harvey

12
IIRC, C # đã làm điều này để "ủng hộ sự rõ ràng." Nếu bạn phải nói rõ ràng "mới" hoặc "ghi đè", điều đó làm cho nó rõ ràng và ngay lập tức rõ ràng những gì đang xảy ra, thay vì bạn phải cố gắng tìm hiểu xem phương thức có ghi đè một số hành vi trong lớp cơ sở hay không. Tôi cũng thấy rất hữu ích khi có thể chỉ định phương thức nào tôi muốn chỉ định là ảo và phương pháp nào tôi không. (Java thực hiện điều này với final; nó chỉ là cách ngược lại).
Robert Harvey

15
Trong Java không có từ khóa ảo, mới, ghi đè cho định nghĩa phương thức @Override.
Svick


3
Chú thích @svick và từ khóa không giống nhau :)
Anirban Nag 'tintinmj'

Câu trả lời:


86

Vì bạn đã hỏi tại sao C # lại làm theo cách này, tốt nhất nên hỏi những người tạo C #. Anders Hejlsberg, kiến ​​trúc sư trưởng của C #, đã trả lời lý do tại sao họ chọn không đi với ảo theo mặc định (như trong Java) trong một cuộc phỏng vấn , đoạn trích thích hợp ở bên dưới.

Hãy nhớ rằng Java có ảo theo mặc định với từ khóa cuối cùng để đánh dấu một phương thức là không ảo. Vẫn còn hai khái niệm để tìm hiểu, nhưng nhiều người không biết về từ khóa cuối cùng hoặc không sử dụng một cách chủ động. C # buộc người ta phải sử dụng ảo và mới / ghi đè để có ý thức đưa ra các quyết định đó.

Có một số lý do. Một là hiệu suất . Chúng ta có thể quan sát rằng khi mọi người viết mã bằng Java, họ quên đánh dấu các phương thức của họ cuối cùng. Do đó, những phương pháp đó là ảo. Bởi vì chúng là ảo, chúng cũng không hoạt động. Chỉ có chi phí hiệu năng liên quan đến việc là một phương thức ảo. Đó là một vấn đề.

Một vấn đề quan trọng hơn là phiên bản . Có hai trường phái suy nghĩ về phương pháp ảo. Trường phái tư tưởng nói: "Mọi thứ nên là ảo, bởi vì tôi có thể muốn ghi đè lên một ngày nào đó." Trường phái tư tưởng thực dụng, xuất phát từ việc xây dựng các ứng dụng thực chạy trong thế giới thực, nói: "Chúng ta phải thực sự cẩn thận về những gì chúng ta tạo ra ảo".

Khi chúng tôi tạo ra thứ gì đó ảo trong một nền tảng, chúng tôi sẽ đưa ra rất nhiều lời hứa về cách nó phát triển trong tương lai. Đối với một phương thức không ảo, chúng tôi hứa rằng khi bạn gọi phương thức này, x và y sẽ xảy ra. Khi chúng tôi xuất bản một phương thức ảo trong API, chúng tôi không chỉ hứa rằng khi bạn gọi phương thức này, x và y sẽ xảy ra. Chúng tôi cũng hứa rằng khi bạn ghi đè phương thức này, chúng tôi sẽ gọi nó theo trình tự cụ thể này liên quan đến các phương thức khác và trạng thái sẽ nằm trong điều này và bất biến.

Mỗi khi bạn nói ảo trong một API, bạn đang tạo ra một cuộc gọi lại. Là một nhà thiết kế khung hệ điều hành hoặc API, bạn phải thực sự cẩn thận về điều đó. Bạn không muốn người dùng ghi đè và móc nối tại bất kỳ điểm tùy ý nào trong API, bởi vì bạn không nhất thiết phải thực hiện những lời hứa đó. Và mọi người có thể không hoàn toàn hiểu những lời hứa mà họ đang thực hiện khi họ thực hiện một cái gì đó ảo.

Cuộc phỏng vấn có nhiều thảo luận về cách các nhà phát triển nghĩ về thiết kế kế thừa lớp và cách điều đó dẫn đến quyết định của họ.

Bây giờ đến câu hỏi sau:

Tôi không thể hiểu tại sao trên thế giới tôi sẽ thêm một phương thức trong DeruredClass của mình có cùng tên và cùng chữ ký với BaseClass và xác định một hành vi mới nhưng ở dạng đa hình thời gian chạy, phương thức BaseClass sẽ được gọi! (đó không phải là ghi đè nhưng phải hợp lý).

Điều này sẽ là khi một lớp dẫn xuất muốn tuyên bố rằng nó không tuân theo hợp đồng của lớp cơ sở, nhưng có một phương thức có cùng tên. (Đối với bất kỳ ai không biết sự khác biệt giữa newoverridetrong C #, hãy xem trang MSDN này ).

Một kịch bản rất thực tế là đây:

  • Bạn đã tạo một API, trong đó có một lớp được gọi Vehicle.
  • Tôi bắt đầu sử dụng API của bạn và bắt nguồn Vehicle.
  • VehicleLớp học của bạn không có bất kỳ phương pháp nào PerformEngineCheck().
  • Trong Carlớp học của tôi , tôi thêm một phương thức PerformEngineCheck().
  • Bạn đã phát hành phiên bản API mới và thêm a PerformEngineCheck().
  • Tôi không thể đổi tên phương thức của mình vì các khách hàng của tôi phụ thuộc vào API của tôi và nó sẽ phá vỡ chúng.
  • Vì vậy, khi tôi biên dịch lại API mới của bạn, C # cảnh báo tôi về vấn đề này, vd

    Nếu cơ sở PerformEngineCheck()không virtual:

    app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    Use the new keyword if hiding was intended.
    

    Và nếu cơ sở PerformEngineCheck()virtual:

    app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
    
  • Bây giờ, tôi phải đưa ra quyết định rõ ràng liệu lớp của tôi có thực sự gia hạn hợp đồng của lớp cơ sở hay không, nếu đó là một hợp đồng khác nhưng lại có cùng tên.

  • Bằng cách tạo ra nó new, tôi không phá vỡ các máy khách của mình nếu chức năng của phương thức cơ sở khác với phương thức dẫn xuất. Bất kỳ mã nào được tham chiếu Vehiclesẽ không thấy Car.PerformEngineCheck()được gọi, nhưng mã có tham chiếu Carsẽ tiếp tục thấy chức năng tương tự mà tôi đã cung cấp PerformEngineCheck().

Một ví dụ tương tự là khi một phương thức khác trong lớp cơ sở có thể đang gọi PerformEngineCheck()(đặc biệt là trong phiên bản mới hơn), làm thế nào để ngăn không cho nó gọi PerformEngineCheck()lớp dẫn xuất? Trong Java, quyết định đó sẽ thuộc về lớp cơ sở, nhưng nó không biết gì về lớp dẫn xuất. Trong C #, quyết định đó thuộc về cả lớp cơ sở (thông qua virtualtừ khóa) trên lớp dẫn xuất (thông qua newoverridetừ khóa).

Tất nhiên, các lỗi mà trình biên dịch ném cũng cung cấp một công cụ hữu ích để các lập trình viên không mắc lỗi bất ngờ (nghĩa là ghi đè hoặc cung cấp chức năng mới mà không nhận ra như vậy.)

Giống như Anders đã nói, thế giới thực buộc chúng ta vào những vấn đề như vậy, nếu chúng ta bắt đầu lại từ đầu, chúng ta sẽ không bao giờ muốn vào.

EDIT: Đã thêm một ví dụ về nơi newsẽ phải được sử dụng để đảm bảo khả năng tương thích giao diện.

EDIT: Trong khi xem qua các bình luận, tôi cũng bắt gặp một bài viết của Eric Lippert (khi đó là một trong những thành viên của ủy ban thiết kế C #) về các tình huống ví dụ khác (được đề cập bởi Brian).


PHẦN 2: Dựa trên câu hỏi cập nhật

Nhưng trong trường hợp C # nếu SpaceShip không ghi đè tăng tốc của lớp Xe và sử dụng mới thì logic mã của tôi sẽ bị phá vỡ. Đó không phải là một bất lợi sao?

Ai quyết định liệu SpaceShipcó thực sự ghi đè Vehicle.accelerate()hoặc nếu nó khác nhau? Nó phải là SpaceShipnhà phát triển. Vì vậy, nếu SpaceShipnhà phát triển quyết định rằng họ không giữ hợp đồng của lớp cơ sở, thì cuộc gọi của bạn Vehicle.accelerate()không nên đến SpaceShip.accelerate(), hay nên? Đó là khi họ sẽ đánh dấu nó là new. Tuy nhiên, nếu họ quyết định rằng nó thực sự giữ hợp đồng, thì thực tế họ sẽ đánh dấu nó override. Trong cả hai trường hợp, mã của bạn sẽ hoạt động chính xác bằng cách gọi phương thức chính xác dựa trên hợp đồng . Làm thế nào mã của bạn có thể quyết định liệu SpaceShip.accelerate()thực sự ghi đè Vehicle.accelerate()hoặc nếu đó là một xung đột tên? (Xem ví dụ của tôi ở trên).

Tuy nhiên, trong trường hợp thừa kế tiềm ẩn, ngay cả khi SpaceShip.accelerate()đã không giữ hợp đồng của Vehicle.accelerate(), cuộc gọi phương pháp sẽ vẫn đi đến SpaceShip.accelerate().


12
Điểm hiệu suất là hoàn toàn lỗi thời bây giờ. Để chứng minh, hãy xem điểm chuẩn của tôi cho thấy rằng việc truy cập vào một trường thông qua phương pháp không phải là cuối cùng nhưng không bao giờ bị quá tải cần một chu kỳ duy nhất.
maaartinus

7
Chắc chắn, đó có thể là trường hợp. Câu hỏi là khi C # quyết định làm như vậy, tại sao lại làm như vậy vào thời điểm đó, và do đó câu trả lời này là hợp lệ. Nếu câu hỏi là liệu nó có còn ý nghĩa hay không, thì đó là một cuộc thảo luận khác, IMHO.
Omer Iqbal

1
Tôi hoàn toàn đồng ý với bạn.
maaartinus

2
IMHO, trong khi chắc chắn có những cách sử dụng để có các chức năng không ảo, nguy cơ những cạm bẫy bất ngờ xảy ra khi một thứ không được mong đợi sẽ ghi đè lên một phương thức lớp cơ sở hoặc thực hiện một giao diện, thực hiện như vậy.
supercat

3
@Nilzor: Do đó tranh luận liên quan đến học thuật và thực dụng. Thực tế, trách nhiệm phá vỡ một cái gì đó nằm ở người thay đổi cuối cùng. Nếu bạn thay đổi lớp cơ sở của mình và một lớp dẫn xuất hiện có phụ thuộc vào nó không thay đổi, đó không phải là vấn đề của họ ("họ" có thể không còn tồn tại). Vì vậy, các lớp cơ sở bị khóa trong hành vi của các lớp dẫn xuất của chúng, ngay cả khi lớp đó không bao giờ là ảo . Và như bạn nói, RhinoMock tồn tại. Có, nếu bạn hoàn thành chính xác tất cả các phương thức, mọi thứ đều tương đương. Ở đây chúng tôi chỉ vào mọi hệ thống Java từng được xây dựng.
deworde

94

Nó đã được thực hiện bởi vì đó là điều chính xác để làm. Thực tế là cho phép tất cả các phương thức bị ghi đè là sai; nó dẫn đến vấn đề lớp cơ sở mong manh, trong đó bạn không có cách nào để biết nếu một thay đổi đối với lớp cơ sở sẽ phá vỡ các lớp con. Do đó, bạn phải liệt kê các phương thức không được ghi đè hoặc đưa vào danh sách trắng các phương thức được phép ghi đè. Trong cả hai, danh sách trắng không chỉ an toàn hơn (vì bạn không thể vô tình tạo ra một lớp cơ sở mỏng manh ), nó cũng đòi hỏi ít công việc hơn vì bạn nên tránh kế thừa theo hướng có lợi cho sáng tác.


7
Cho phép bất kỳ phương pháp tùy ý nào bị ghi đè là sai. Bạn chỉ có thể ghi đè các phương thức mà nhà thiết kế siêu lớp đã chỉ định là an toàn để ghi đè.
Doval

21
Eric Lippert thảo luận chi tiết về vấn đề này trong bài đăng của mình, Phương thức ảo và Lớp cơ sở dễ vỡ .
Brian

12
Java có finalsửa đổi, vậy vấn đề là gì?
Sange Borsch

13
@corsiKa "các đối tượng nên được mở rộng" - tại sao? Nếu nhà phát triển của lớp cơ sở không bao giờ nghĩ về thừa kế thì gần như đảm bảo rằng có các phụ thuộc và giả định tinh tế trong mã sẽ dẫn đến lỗi (nhớ HashMapkhông?). Hoặc thiết kế để thừa kế và làm cho hợp đồng rõ ràng hoặc không và đảm bảo không ai có thể kế thừa lớp. @Overrideđã được thêm vào dưới dạng băng bó vì đã quá muộn để thay đổi hành vi, nhưng ngay cả các nhà phát triển Java ban đầu (đặc biệt là Josh Bloch) cũng đồng ý rằng đây là một quyết định tồi.
Voo

9
@jwenting "Ý kiến" là một từ hài hước; mọi người thích gắn nó với sự thật để họ có thể coi thường chúng. Nhưng đối với bất cứ điều gì cũng đáng, đó cũng là "ý kiến" của Joshua Bloch (xem: Java hiệu quả , Mục 17 ), "ý kiến" của James Gosling (xem cuộc phỏng vấn này ), và như đã nêu trong câu trả lời của Omer Iqbal , đó cũng là "ý kiến" của Anders Hejlsberg. (Và mặc dù anh ấy không bao giờ đề cập đến việc chọn tham gia so với từ chối, Eric Lippert rõ ràng đồng ý thừa kế cũng nguy hiểm.) Vậy ai đang đề cập đến?
Doval

33

Như Robert Harvey đã nói, đó là tất cả những gì bạn đã từng làm. Tôi thấy Java thiếu tính linh hoạt này.

Điều đó nói rằng, tại sao có điều này ở nơi đầu tiên? Cũng vì lý do tương tự mà C # có public, internal(cũng "không có gì"), protected, protected internal, và private, nhưng Java chỉ có public, protectedkhông có gì, và private. Nó cung cấp kiểm soát hạt tốt hơn đối với hành vi của những gì bạn đang mã hóa, với chi phí có nhiều thuật ngữ và từ khóa để theo dõi.

Trong trường hợp newso với virtual+override, nó sẽ giống như thế này:

  • Nếu bạn muốn buộc các lớp con thực hiện phương thức, hãy sử dụng abstractoverridetrong lớp con.
  • Nếu bạn muốn cung cấp chức năng nhưng cho phép lớp con thay thế nó, hãy sử dụng virtualoverridetrong lớp con.
  • Nếu bạn muốn cung cấp chức năng mà các lớp con không bao giờ cần ghi đè, đừng sử dụng bất cứ thứ gì.
    • Nếu sau đó bạn có một lớp con trường hợp đặc biệt không cần phải cư xử khác, hãy sử dụng newtrong lớp con.
    • Nếu bạn muốn đảm bảo rằng không có lớp con có thể bao giờ thay đổi hành vi, sử dụng sealedtrong các lớp cơ sở.

Đối với một ví dụ thực tế: Một dự án tôi đã làm việc trên các đơn đặt hàng thương mại điện tử được xử lý từ nhiều nguồn khác nhau. Có một cơ sở OrderProcessorcó hầu hết logic, với một số phương thức abstract/ nhất định virtualcho mỗi lớp con của nguồn để ghi đè. Điều này hoạt động tốt, cho đến khi chúng tôi có được một nguồn mới có cách xử lý đơn hàng hoàn toàn khác, do đó chúng tôi phải thay thế một chức năng cốt lõi. Chúng tôi đã có hai lựa chọn tại thời điểm này: 1) Thêm vào virtualphương thức cơ bản và overrideở trẻ em; hoặc 2) Thêm newvào đứa trẻ.

Mặc dù một trong hai có thể hoạt động, đầu tiên sẽ làm cho nó rất dễ dàng ghi đè lại phương thức cụ thể đó trong tương lai. Nó sẽ hiển thị trong tự động hoàn thành, ví dụ. Đây là một trường hợp đặc biệt, tuy nhiên, vì vậy chúng tôi đã chọn sử dụng newthay thế. Điều đó bảo tồn tiêu chuẩn của "phương pháp này không cần phải ghi đè", trong khi cho phép trong trường hợp đặc biệt mà nó đã làm. Đó là một sự khác biệt về ngữ nghĩa làm cho cuộc sống dễ dàng hơn.

Do lưu ý, tuy nhiên, có một sự khác biệt hành vi liên quan với điều này, không chỉ là một sự khác biệt ngữ nghĩa. Xem bài viết này để biết chi tiết. Tuy nhiên, tôi chưa bao giờ gặp phải tình huống cần tận dụng hành vi này.


7
Tôi nghĩ rằng cố ý sử dụng cách newnày là một lỗi đang chờ xảy ra. Nếu tôi gọi một phương thức trên một số trường hợp, tôi hy vọng nó sẽ luôn làm điều tương tự, ngay cả khi tôi đưa cá thể đó vào lớp cơ sở của nó. Nhưng đó không phải là cách newcác phương thức ed hành xử.
Svick

@svick - Có khả năng, vâng. Trong kịch bản cụ thể của chúng tôi, điều đó sẽ không bao giờ xảy ra, nhưng đó là lý do tại sao tôi thêm cảnh báo.
Bobson

Một ứng dụng tuyệt vời newlà trong WebViewPage <TModel> trong khung MVC. Nhưng tôi cũng đã bị ném bởi một lỗi liên quan đến newnhiều giờ, vì vậy tôi không nghĩ đó là một câu hỏi không hợp lý.
pdr

1
@ C.Champagne: Bất kỳ công cụ nào cũng có thể được sử dụng tồi và công cụ này là một công cụ đặc biệt sắc nét - bạn có thể tự cắt dễ dàng. Nhưng đó không phải là lý do để xóa công cụ khỏi hộp công cụ và xóa tùy chọn khỏi nhà thiết kế API tài năng hơn.
pdr

1
@svick Đúng, đó là lý do tại sao nó thường là một cảnh báo trình biên dịch. Nhưng có khả năng "mới" bao gồm các điều kiện cạnh (chẳng hạn như điều kiện đã cho), và thậm chí tốt hơn, làm cho thực sự rõ ràng rằng bạn đang làm điều gì đó kỳ lạ khi bạn đến để chẩn đoán lỗi không thể tránh khỏi. "Tại sao lớp này và chỉ có lớp này lỗi ... ah-hah," mới ", hãy đi kiểm tra xem SuperClass được sử dụng ở đâu".
deworde

7

Thiết kế của Java sao cho đưa ra bất kỳ tham chiếu nào đến một đối tượng, một cuộc gọi đến một tên phương thức cụ thể với các kiểu tham số cụ thể, nếu nó được cho phép, sẽ luôn gọi cùng một phương thức. Có thể các chuyển đổi loại tham số ngầm có thể bị ảnh hưởng bởi loại tham chiếu, nhưng một khi tất cả các chuyển đổi đó đã được giải quyết, loại tham chiếu đó không liên quan.

Điều này đơn giản hóa thời gian chạy, nhưng có thể gây ra một số vấn đề đáng tiếc. Giả sử GrafBasekhông thực hiện void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), nhưng GrafDerivedthực hiện nó như một phương thức công khai vẽ hình bình hành có điểm thứ tư được tính ngược lại với điểm thứ nhất. Giả sử thêm rằng một phiên bản sau của GrafBasethực hiện một phương thức công khai có cùng chữ ký, nhưng điểm thứ tư được tính toán của nó ngược lại với điểm thứ hai. Các khách hàng nhận được kỳ vọng GrafBasenhưng nhận được tham chiếu đến GrafDerivedsẽ mong đợi DrawParallelogramtính toán điểm tiếp theo theo GrafBasephương thức mới , nhưng các khách hàng đã sử dụng GrafDerived.DrawParallelogramtrước khi phương thức cơ sở được thay đổi sẽ mong đợi hành vi GrafDerivedđược triển khai ban đầu.

Trong Java, sẽ không có cách nào để tác giả GrafDerivedtạo lớp đó cùng tồn tại với các máy khách sử dụng GrafBase.DrawParallelogramphương thức mới (và có thể không biết rằng GrafDerivedthậm chí còn tồn tại) mà không phá vỡ tính tương thích với mã máy khách hiện có được sử dụng GrafDerived.DrawParallelogramtrước khi GrafBaseđịnh nghĩa nó. Vì DrawParallelogramkhông thể biết loại khách hàng nào đang gọi nó, nên nó phải hành xử giống hệt nhau khi được gọi bởi các loại mã khách hàng. Vì hai loại mã khách hàng có những kỳ vọng khác nhau về cách hoạt động của nó, nên không có cách nào GrafDerivedcó thể tránh vi phạm các kỳ vọng hợp pháp của một trong số chúng (tức là phá vỡ mã khách hàng hợp pháp).

Trong C #, nếu GrafDerivedkhông được biên dịch lại, bộ thực thi sẽ cho rằng mã gọi DrawParallelogramphương thức theo tham chiếu kiểu GrafDerivedsẽ mong đợi hành vi GrafDerived.DrawParallelogram()có khi biên dịch lần cuối, nhưng mã gọi phương thức theo tham chiếu kiểu GrafBasesẽ được mong đợi GrafBase.DrawParallelogram(hành vi mà đã được thêm vào). Nếu GrafDerivedsau đó được biên dịch lại với sự có mặt của phần tăng cường GrafBase, trình biên dịch sẽ squawk cho đến khi lập trình viên chỉ định liệu phương thức của anh ta có phải là sự thay thế hợp lệ cho thành viên được kế thừa GrafBasehay không, hoặc liệu hành vi của nó có cần được gắn với các tham chiếu loại không GrafDerived, nhưng không nên thay thế hành vi của các tài liệu tham khảo của loại GrafBase.

Người ta có thể lập luận một cách hợp lý rằng có một phương pháp GrafDerivedlàm một cái gì đó khác với một thành viên GrafBasecó cùng chữ ký sẽ chỉ ra thiết kế xấu, và vì thế không nên được hỗ trợ. Thật không may, vì tác giả của loại cơ sở không có cách nào để biết phương thức nào có thể được thêm vào loại dẫn xuất, cũng như ngược lại, tình huống mà các máy khách lớp cơ sở và lớp dẫn xuất có những kỳ vọng khác nhau đối với các phương thức giống như về cơ bản là không thể tránh khỏi trừ khi không ai được phép thêm bất kỳ tên nào mà người khác cũng có thể thêm. Câu hỏi không phải là liệu sự trùng lặp tên như vậy có xảy ra hay không, mà là làm thế nào để giảm thiểu tác hại khi nó xảy ra.


2
Tôi nghĩ rằng có một câu trả lời tốt ở đây, nhưng nó bị lạc trong bức tường văn bản. Xin hãy chia nó thành một vài đoạn nữa.
Bobson

DerivedGraphics? A class using GrafDeriving` là gì?
C.Champagne

@ C.Champagne: Ý tôi là GrafDerived. Tôi bắt đầu sử dụng DerivedGraphics, nhưng cảm thấy nó hơi dài. Thậm chí GrafDerivedvẫn còn hơi dài, nhưng không biết cách tốt nhất để đặt tên cho các loại trình kết xuất đồ họa cần có mối quan hệ cơ bản / xuất phát rõ ràng.
supercat

1
@Bobson: Tốt hơn?
supercat

6

Tình huống chuẩn:

Bạn là chủ sở hữu của một lớp cơ sở được sử dụng bởi nhiều dự án. Bạn muốn thay đổi lớp cơ sở đã nói sẽ phá vỡ giữa 1 và vô số lớp dẫn xuất, nằm trong các dự án cung cấp giá trị trong thế giới thực (Một khung cung cấp giá trị tốt nhất trong một lần xóa, không Real Human nào muốn một Framework, họ muốn điều chạy trên khung). Chúc may mắn cho các chủ sở hữu bận rộn của các lớp dẫn xuất; "Tốt, bạn phải thay đổi, bạn không nên ghi đè phương thức đó" mà không nhận được đại diện là "Khung: Xóa dự án và nguyên nhân gây ra lỗi" cho những người phải phê duyệt quyết định.

Đặc biệt là, bằng cách không tuyên bố nó không thể ghi đè, bạn đã hoàn toàn tuyên bố rằng họ ổn để làm điều mà bây giờ ngăn chặn sự thay đổi của bạn.

Và nếu bạn không có một số lượng đáng kể các lớp dẫn xuất cung cấp giá trị thế giới thực bằng cách ghi đè lớp cơ sở của bạn, tại sao nó lại là lớp cơ sở ở vị trí đầu tiên? Hy vọng là một động lực mạnh mẽ, nhưng cũng là một cách rất tốt để kết thúc với mã không được kiểm chứng.

Kết quả cuối cùng: Mã lớp cơ sở khung của bạn trở nên cực kỳ mong manh và tĩnh và bạn thực sự không thể thực hiện các thay đổi cần thiết để duy trì hiện tại / hiệu quả. Ngoài ra, khung công tác của bạn có một đại diện cho sự không ổn định (các lớp dẫn xuất tiếp tục phá vỡ) và mọi người sẽ không sử dụng nó, vì lý do chính để sử dụng một khung là để làm cho mã hóa nhanh hơn và đáng tin cậy hơn.

Nói một cách đơn giản, bạn không thể yêu cầu chủ dự án bận rộn trì hoãn dự án của họ để sửa các lỗi mà bạn đang giới thiệu và mong đợi mọi thứ tốt hơn là "biến mất" trừ khi bạn cung cấp lợi ích đáng kể cho họ, ngay cả khi "lỗi" ban đầu là của họ, đó là tốt nhất có thể tranh cãi.

Tốt hơn hết là đừng để họ làm điều sai trái ngay từ đầu, đó là nơi "không ảo theo mặc định" xuất hiện. Và khi ai đó đến với bạn với một lý do rất rõ ràng tại sao họ cần phương pháp đặc biệt này để được ghi đè, và tại sao nó sẽ an toàn, bạn có thể "mở khóa" nó mà không có nguy cơ phá vỡ mã của người khác.


0

Mặc định cho các giả định không ảo rằng nhà phát triển lớp cơ sở là hoàn hảo. Theo kinh nghiệm của tôi, các nhà phát triển không hoàn hảo. Nếu nhà phát triển của lớp cơ sở không thể tưởng tượng được trường hợp sử dụng trong đó phương thức có thể bị ghi đè hoặc quên thêm ảo thì tôi không thể tận dụng tính đa hình khi mở rộng lớp cơ sở mà không sửa đổi lớp cơ sở. Trong thế giới thực, việc sửa đổi lớp cơ sở thường không phải là một lựa chọn.

Trong C #, nhà phát triển lớp cơ sở không tin tưởng nhà phát triển lớp con. Trong Java, nhà phát triển lớp con không tin tưởng nhà phát triển lớp cơ sở. Nhà phát triển lớp con chịu trách nhiệm cho lớp con và nên (imho) được trao quyền mở rộng lớp cơ sở khi họ thấy phù hợp (loại bỏ từ chối rõ ràng và trong java họ thậm chí có thể hiểu sai điều này).

Đó là một tài sản cơ bản của định nghĩa ngôn ngữ. Nó không đúng hay sai, đó là những gì nó có và không thể thay đổi.


2
điều này dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong 5 câu trả lời trước
gnat
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.