Tại sao C # thực hiện các phương thức không phải là ảo theo mặc định?


105

Không giống như Java, tại sao C # lại coi các phương thức là hàm không ảo theo mặc định? Nó có nhiều khả năng là một vấn đề về hiệu suất hơn là các kết quả có thể xảy ra khác không?

Tôi nhớ lại khi đọc một đoạn văn của Anders Hejlsberg về một số lợi thế mà kiến ​​trúc hiện tại đang mang lại. Nhưng, những gì về tác dụng phụ? Có thực sự là một sự đánh đổi tốt khi có các phương pháp không ảo theo mặc định?


1
Các câu trả lời đề cập đến lý do hiệu suất bỏ qua thực tế là trình biên dịch C # chủ yếu biên dịch các cuộc gọi phương thức thành callvirt chứ không phải cuộc gọi . Đó là lý do tại sao trong C #, không thể có một phương thức hoạt động khác nếu thistham chiếu là null. Xem ở đây để biết thêm thông tin.
Andy

Thật! lệnh gọi IL chủ yếu dành cho các lệnh gọi đến các phương thức tĩnh.
RBT

1
Suy nghĩ của kiến ​​trúc sư Anders Hejlsberg của C # ở đâyở đây .
RBT

Câu trả lời:


98

Các lớp nên được thiết kế để kế thừa để có thể tận dụng lợi thế của nó. Có các phương thức virtualtheo mặc định có nghĩa là mọi chức năng trong lớp có thể được cắm ra và thay thế bằng một chức năng khác, điều này không thực sự là một điều tốt. Nhiều người thậm chí còn tin rằng các lớp lẽ ra phải được sealedmặc định.

virtualcác phương pháp cũng có thể có một hàm ý hiệu suất nhỏ. Tuy nhiên, đây có thể không phải là lý do chính.


7
Cá nhân, tôi nghi ngờ phần đó về hiệu suất. Ngay cả đối với các hàm ảo, trình biên dịch rất có thể tìm ra nơi để thay thế callvirtbằng một mã đơn giản calltrong mã IL (hoặc thậm chí xuống dòng xa hơn khi JITting). Java HotSpot cũng làm như vậy. Phần còn lại của câu trả lời là tại chỗ.
Konrad Rudolph

5
Tôi đồng ý rằng hiệu suất không phải là yếu tố hàng đầu, nhưng nó cũng có thể có những tác động về hiệu suất. Nó có lẽ rất nhỏ. Tuy nhiên, chắc chắn đó không phải là lý do cho sự lựa chọn này. Chỉ muốn đề cập đến điều đó.
Mehrdad Afshari

71
Lớp học kín mít khiến tôi muốn đấm trẻ sơ sinh.
mxmissile

6
@mxmissile: tôi cũng vậy, nhưng dù sao thiết kế API tốt cũng khó. Tôi nghĩ rằng mã được niêm phong theo mặc định có ý nghĩa hoàn hảo cho mã bạn sở hữu. Thông thường, các lập trình viên khác trong nhóm của bạn đã không xem xét tính kế thừa khi họ triển khai lớp đó mà bạn cần và được niêm phong theo mặc định sẽ truyền tải thông điệp đó khá tốt.
Roman Starkov

49
Nhiều người cũng là kẻ thái nhân cách, không có nghĩa là chúng ta nên lắng nghe họ. Ảo phải là mặc định trong C #, mã phải có thể mở rộng mà không cần phải thay đổi triển khai hoặc viết lại hoàn toàn. Những người bảo vệ quá mức API của họ thường kết thúc với các API chết hoặc sử dụng một nửa, điều này còn tệ hơn nhiều so với trường hợp ai đó lạm dụng nó.
Chris Nicola

89

Tôi ngạc nhiên rằng dường như có sự đồng thuận ở đây rằng không ảo theo mặc định là cách đúng đắn để thực hiện mọi việc. Tôi sẽ đi xuống phía bên kia - tôi nghĩ thực dụng - bên hàng rào.

Hầu hết những lời biện minh đọc cho tôi nghe đều giống như lập luận cũ "Nếu chúng tôi cho bạn sức mạnh, bạn có thể tự làm hại chính mình". Từ các lập trình viên ?!

Đối với tôi, có vẻ như người lập trình viên không biết đủ (hoặc có đủ thời gian) để thiết kế thư viện của họ để kế thừa và / hoặc khả năng mở rộng là người lập trình đã tạo ra chính xác thư viện mà tôi có thể phải sửa hoặc chỉnh sửa - chính xác là thư viện nơi khả năng ghi đè sẽ hữu ích nhất.

Số lần tôi phải viết mã công việc tồi tệ, tuyệt vọng (hoặc từ bỏ việc sử dụng và đưa ra giải pháp thay thế của riêng mình) vì tôi không thể ghi đè lên, nhiều hơn nhiều lần tôi từng bị cắn ( ví dụ như trong Java) bằng cách ghi đè lên nơi mà nhà thiết kế có thể không xem xét tôi có thể.

Không-ảo-theo-mặc-định khiến cuộc sống của tôi vất vả hơn.

CẬP NHẬT: Đã chỉ ra [khá chính xác] rằng tôi đã không thực sự trả lời câu hỏi. Vì vậy - và với lời xin lỗi vì đã khá muộn ....

Tôi muốn có thể viết một cái gì đó thú vị như "C # thực hiện các phương thức không phải là ảo theo mặc định vì một quyết định tồi đã được đưa ra khiến chương trình đánh giá cao hơn lập trình viên". (Tôi nghĩ rằng điều đó có thể được chứng minh phần nào dựa trên một số câu trả lời khác cho câu hỏi này - như hiệu suất (tối ưu hóa sớm, có ai không?) Hoặc đảm bảo hành vi của các lớp.)

Tuy nhiên, tôi nhận ra rằng tôi chỉ đang nêu ý kiến ​​của mình chứ không phải câu trả lời dứt khoát mà Stack Overflow mong muốn. Chắc chắn, tôi nghĩ, ở cấp độ cao nhất, câu trả lời dứt khoát (nhưng vô ích) là:

Theo mặc định, chúng không phải là ảo bởi vì các nhà thiết kế ngôn ngữ đã đưa ra quyết định và đó là những gì họ đã chọn.

Bây giờ tôi đoán lý do chính xác mà họ đưa ra quyết định đó, chúng tôi sẽ không bao giờ .... ồ, chờ đã! Bản ghi của một cuộc trò chuyện!

Vì vậy, có vẻ như các câu trả lời và nhận xét ở đây về sự nguy hiểm của việc ghi đè API và nhu cầu thiết kế rõ ràng để kế thừa đang đi đúng hướng nhưng tất cả đều thiếu một khía cạnh quan trọng về thời gian: Mối quan tâm chính của Anders là về việc duy trì ngầm định của một lớp hoặc API hợp đồng giữa các phiên bản . Và tôi nghĩ rằng anh ấy thực sự quan tâm hơn đến việc cho phép nền tảng .Net / C # thay đổi theo mã hơn là quan tâm đến việc thay đổi mã người dùng trên nền tảng. (Và quan điểm "thực dụng" của anh ấy hoàn toàn trái ngược với tôi vì anh ấy đang nhìn từ phía khác.)

(Nhưng không thể họ chỉ chọn ảo theo mặc định và sau đó xếp "cuối cùng" thông qua cơ sở mã? Có lẽ điều đó không hoàn toàn giống nhau .. và Anders rõ ràng là thông minh hơn tôi nên tôi sẽ để nó nói dối.)


2
Không thể đồng ý hơn. Rất khó chịu khi sử dụng api của Bên thứ ba và muốn ghi đè một số hành vi nhưng không thể.
Andy

3
Tôi nhiệt tình đồng ý. Nếu bạn định xuất bản một API (cho dù trong nội bộ công ty hay ra thế giới bên ngoài), bạn thực sự có thể và nên đảm bảo rằng mã của bạn được thiết kế để kế thừa. Bạn nên làm cho API tốt và bóng bẩy nếu bạn đang xuất bản nó để được nhiều người sử dụng. So với nỗ lực cần thiết để thiết kế tổng thể tốt (nội dung tốt, trường hợp sử dụng rõ ràng, kiểm tra, tài liệu), thiết kế để kế thừa thực sự không quá tệ. Và nếu bạn không xuất bản, ảo theo mặc định ít tốn thời gian hơn và bạn luôn có thể khắc phục một phần nhỏ các trường hợp có vấn đề.
Gravity

2
Bây giờ nếu chỉ có một tính năng biên tập trong Visual Studio để tự động gắn thẻ tất cả các phương thức / thuộc tính là virtual... Visual Studio add-in có ai không?
kevinarpe

1
Mặc dù tôi hết lòng đồng ý với bạn, nhưng đây không phải là câu trả lời chính xác.
Chris Morgan

2
Xem xét các thực tiễn phát triển hiện đại như chèn phụ thuộc, khuôn khổ chế tạo và ORM, có vẻ như các nhà thiết kế C # của chúng tôi đã bỏ lỡ một chút dấu ấn. Thật khó chịu khi phải kiểm tra phụ thuộc khi bạn không thể ghi đè một thuộc tính theo mặc định.
Jeremy Holovacs

17

Vì quá dễ dàng để quên rằng một phương thức có thể bị ghi đè và không được thiết kế cho điều đó. C # khiến bạn phải suy nghĩ trước khi biến nó thành ảo. Tôi nghĩ đây là một quyết định thiết kế tuyệt vời. Một số người (chẳng hạn như Jon Skeet) thậm chí đã nói rằng các lớp nên được niêm phong theo mặc định.


12

Để tóm tắt những gì những người khác đã nói, có một số lý do:

1- Trong C #, có rất nhiều thứ trong cú pháp và ngữ nghĩa đến từ C ++. Thực tế là các phương thức không ảo theo mặc định trong C ++ đã ảnh hưởng đến C #.

2- Có mọi phương thức ảo theo mặc định là một mối quan tâm về hiệu suất vì mọi cuộc gọi phương thức phải sử dụng Bảng ảo của đối tượng. Hơn nữa, điều này hạn chế mạnh mẽ khả năng của trình biên dịch Just-In-Time trong nội tuyến các phương thức và thực hiện các loại tối ưu hóa khác.

3- Quan trọng nhất, nếu các phương thức không phải là ảo theo mặc định, bạn có thể đảm bảo hành vi của các lớp của mình. Khi chúng ảo theo mặc định, chẳng hạn như trong Java, bạn thậm chí không thể đảm bảo rằng một phương thức getter đơn giản sẽ hoạt động như dự định vì nó có thể bị ghi đè để thực hiện bất kỳ điều gì trong một lớp dẫn xuất (tất nhiên bạn có thể và nên làm phương pháp và / hoặc cuối cùng của lớp).

Người ta có thể thắc mắc, như Zifre đã đề cập, tại sao ngôn ngữ C # không tiến thêm một bước nữa và làm cho các lớp được niêm phong theo mặc định. Đó là một phần của toàn bộ cuộc tranh luận về các vấn đề kế thừa thực thi, một chủ đề rất thú vị.


1
Tôi cũng sẽ có các lớp ưu tiên được niêm phong theo mặc định. Sau đó, nó sẽ nhất quán.
Andy

9

C # chịu ảnh hưởng của C ++ (và hơn thế nữa). C ++ không cho phép điều phối động (chức năng ảo) theo mặc định. Một lập luận (tốt?) Cho điều này là câu hỏi: "Bạn có thường triển khai các lớp là thành viên của một nghiên cứu lớp không?". Một lý do khác để tránh bật điều phối động theo mặc định là dấu chân bộ nhớ. Một lớp không có con trỏ ảo (vpointer) trỏ đến một bảng ảo , dĩ nhiên là nhỏ hơn lớp tương ứng có kích hoạt ràng buộc trễ.

Vấn đề hiệu suất không phải là quá dễ dàng để nói "có" hoặc "không". Lý do cho điều này là biên dịch Just In Time (JIT) là một tối ưu hóa thời gian chạy trong C #.

Một câu hỏi khác, tương tự về " tốc độ của cuộc gọi ảo .. "


Tôi hơi nghi ngờ rằng các phương thức ảo có tác động đến hiệu suất đối với C #, vì trình biên dịch JIT. Đó là một trong những lĩnh vực mà JIT có thể tốt hơn so với biên dịch ngoại tuyến, vì chúng có thể nội tuyến các lệnh gọi hàm "không xác định" trước thời gian chạy
David Cournapeau

1
Trên thực tế, tôi nghĩ nó bị ảnh hưởng bởi Java nhiều hơn là C ++ theo mặc định.
Mehrdad Afshari

5

Lý do đơn giản là chi phí thiết kế và bảo trì bên cạnh chi phí hiệu suất. Phương thức ảo có chi phí bổ sung so với phương thức không phải ảo bởi vì người thiết kế lớp phải lập kế hoạch cho những gì sẽ xảy ra khi phương thức bị ghi đè bởi lớp khác. Điều này có tác động lớn nếu bạn mong đợi một phương pháp cụ thể cập nhật trạng thái nội bộ hoặc có một hành vi cụ thể. Bây giờ bạn phải lập kế hoạch cho những gì sẽ xảy ra khi một lớp dẫn xuất thay đổi hành vi đó. Khó hơn nhiều để viết mã đáng tin cậy trong tình huống đó.

Với một phương pháp không ảo, bạn có toàn quyền kiểm soát. Bất cứ điều gì sai sót là lỗi của tác giả gốc. Mã dễ dàng hơn để giải thích.


Đây là một bài viết thực sự cũ nhưng đối với một người mới đang viết dự án đầu tiên của mình, tôi liên tục băn khoăn về những hậu quả không mong muốn / chưa biết của đoạn mã tôi viết. Thật an ủi khi biết rằng các phương pháp không ảo hoàn toàn là lỗi của TÔI.
trevorc

2

Nếu tất cả các phương thức C # đều là ảo thì vtbl sẽ lớn hơn nhiều.

Các đối tượng C # chỉ có các phương thức ảo nếu lớp có các phương thức ảo được định nghĩa. Đúng là tất cả các đối tượng đều có thông tin kiểu bao gồm một vtbl tương đương, nhưng nếu không có phương thức ảo nào được xác định thì sẽ chỉ có các phương thức Đối tượng cơ sở.

@Tom Hawtin: Có lẽ chính xác hơn khi nói rằng C ++, C # và Java đều thuộc họ ngôn ngữ C :)


1
Tại sao nó sẽ là một vấn đề cho vtable lớn hơn nhiều? Chỉ có 1 vtable cho mỗi lớp (và không phải cho mỗi trường hợp), vì vậy kích thước của nó không tạo ra nhiều khác biệt.
Gravity

1

Xuất thân từ nền tảng perl, tôi nghĩ C # đã niêm phong sự diệt vong của mọi nhà phát triển, những người có thể muốn mở rộng và sửa đổi hành vi của một lớp cơ sở 'thông qua một phương thức không ảo mà không buộc tất cả người dùng của lớp mới phải nhận thức được tiềm năng đằng sau hậu trường chi tiết.

Hãy xem xét phương thức Add của lớp List. Điều gì sẽ xảy ra nếu một nhà phát triển muốn cập nhật một trong một số cơ sở dữ liệu tiềm năng bất cứ khi nào một Danh sách cụ thể được 'Thêm' vào? Nếu 'Add' là ảo theo mặc định, nhà phát triển có thể phát triển một lớp 'BackedList' ghi đè phương thức 'Add' mà không buộc tất cả mã máy khách biết đó là 'BackedList' thay vì 'List' thông thường. Đối với tất cả các mục đích thực tế, 'BackedList' có thể được xem như một 'Danh sách' khác từ mã khách hàng.

Điều này có ý nghĩa từ quan điểm của một lớp chính lớn có thể cung cấp quyền truy cập vào một hoặc nhiều thành phần danh sách mà bản thân chúng được hỗ trợ bởi một hoặc nhiều lược đồ trong cơ sở dữ liệu. Cho rằng các phương thức C # không phải là ảo theo mặc định, danh sách được cung cấp bởi lớp chính không thể là một IEnumerable hoặc ICollection đơn giản hoặc thậm chí là một cá thể Danh sách mà thay vào đó, phải được quảng cáo cho khách hàng dưới dạng 'Danh sách ngược' để đảm bảo rằng phiên bản mới của hoạt động 'Thêm' được gọi để cập nhật lược đồ chính xác.


Theo mặc định, các phương thức ảo thực sự làm cho công việc dễ dàng hơn, nhưng liệu nó có hợp lý không? Có rất nhiều thứ bạn có thể sử dụng để làm cho cuộc sống dễ dàng hơn. Tại sao không chỉ các trường công khai trong các lớp? Thay đổi hành vi của nó là quá dễ dàng. Theo tôi mọi thứ bằng ngôn ngữ nên được gói gọn một cách chặt chẽ, cứng nhắc và có thể thay đổi theo mặc định. Chỉ thay đổi nó nếu được yêu cầu. Chỉ là quyết định thiết kế abt triết học. Tiếp tục ..
nawfal

... Để nói về chủ đề hiện tại, mô hình Thừa kế chỉ nên được sử dụng nếu BA. Nếu Byêu cầu một cái gì đó khác với Athì nó không A. Tôi tin rằng bản thân việc ghi đè lên như một khả năng trong ngôn ngữ là một lỗ hổng thiết kế. Nếu bạn cần một Addphương thức khác , thì lớp thu thập của bạn không phải là a List. Cố gắng nói nó là giả mạo. Cách tiếp cận đúng ở đây là bố cục (và không phải giả mạo). Đúng là toàn bộ khung công tác được xây dựng dựa trên khả năng ghi đè, nhưng tôi không thích nó.
nawfal

Tôi nghĩ rằng tôi hiểu quan điểm của bạn, nhưng trên ví dụ cụ thể được cung cấp: 'BackedList' chỉ có thể triển khai giao diện 'IList' và khách hàng chỉ biết về giao diện. đúng? tui bỏ lỡ điều gì vậy? tuy nhiên, tôi hiểu điểm rộng hơn mà bạn đang cố gắng thực hiện.
Vetras

0

Nó chắc chắn không phải là một vấn đề hiệu suất. Trình thông dịch Java của Sun sử dụng cùng một mã để gửi đi ( invokevirtualbytecode) và HotSpot tạo ra chính xác cùng một mã cho dù có finalhay không. Tôi tin rằng tất cả các đối tượng C # (nhưng không phải cấu trúc) đều có các phương thức ảo, vì vậy bạn sẽ luôn cần vtblnhận dạng lớp / runtime. C # là một phương ngữ của "các ngôn ngữ giống Java". Đề xuất nó đến từ C ++ là không hoàn toàn trung thực.

Có ý kiến ​​cho rằng bạn nên "thiết kế để thừa kế nếu không thì cấm". Điều này nghe có vẻ là một ý tưởng tuyệt vời cho đến thời điểm bạn gặp phải tình huống kinh doanh nghiêm trọng cần giải quyết nhanh chóng. Có lẽ kế thừa từ mã mà bạn không kiểm soát.


Hotspot buộc phải trải qua một khoảng thời gian dài để thực hiện việc tối ưu hóa đó, chính xác bởi vì tất cả các phương pháp đều là ảo theo mặc định, điều này có tác động rất lớn đến hiệu suất. CoreCLR có thể đạt được hiệu suất tương tự trong khi đơn giản hơn nhiều
Yair Halberstadt

@YairHalberstadt Hotspot cần có khả năng sao lưu mã đã biên dịch vì nhiều lý do khác. Đã nhiều năm kể từ khi tôi xem xét nguồn, nhưng sự khác biệt giữa các phương pháp finalhiệu quả và hiệu quả finallà không đáng kể. Cũng cần lưu ý rằng nó có thể thực hiện nội tuyến hai hình, đó là các phương thức nội tuyến với hai cách triển khai khác nhau.
Tom Hawtin - tackline
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.