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 new
và override
trong 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
.
Vehicle
Lớp học của bạn không có bất kỳ phương pháp nào PerformEngineCheck()
.
- Trong
Car
lớ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()
là 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 Vehicle
sẽ không thấy Car.PerformEngineCheck()
được gọi, nhưng mã có tham chiếu Car
sẽ 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 virtual
từ khóa) và trên lớp dẫn xuất (thông qua new
và override
từ 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 new
sẽ 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 SpaceShip
có thực sự ghi đè Vehicle.accelerate()
hoặc nếu nó khác nhau? Nó phải là SpaceShip
nhà phát triển. Vì vậy, nếu SpaceShip
nhà 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()
.