Tại sao các ngôn ngữ OOP tĩnh chính mạnh mẽ ngăn chặn kế thừa nguyên thủy?


53

Tại sao điều này là OK và chủ yếu là mong đợi:

abstract type Shape
{
   abstract number Area();
}

concrete type Triangle : Shape
{
   concrete number Area()
   {
      //...
   }
}

... trong khi điều này không ổn và không ai phàn nàn:

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

Động lực của tôi là tối đa hóa việc sử dụng hệ thống loại để xác minh tính chính xác của thời gian biên dịch.

PS: vâng, tôi đã đọc cái này và gói là một công việc khó khăn.


1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
maple_shaft

Tôi đã có một động lực tương tự trong câu hỏi này , bạn có thể thấy nó thú vị.
mặc định.kramer

Tôi sẽ thêm một câu trả lời xác nhận "bạn không muốn thừa kế" ý tưởng, và gói mà rất mạnh mẽ, bao gồm cung cấp nào của ngầm hay rõ ràng đúc (hoặc thất bại) mà bạn muốn, đặc biệt là với optimisations JIT gợi ý bạn sẽ Dù sao cũng đạt được hiệu suất gần như nhau, nhưng bạn đã liên kết với câu trả lời đó :-) Tôi sẽ chỉ thêm, thật tuyệt nếu ngôn ngữ thêm các tính năng để giảm mã soạn sẵn cần thiết cho các phương thức / phương thức chuyển tiếp, đặc biệt là nếu chỉ có một giá trị duy nhất.
Đánh dấu Hurd

Câu trả lời:


82

Tôi giả sử bạn đang nghĩ về các ngôn ngữ như Java và C #?

Trong các ngôn ngữ đó, nguyên thủy (như int) về cơ bản là một sự thỏa hiệp cho hiệu suất. Chúng không hỗ trợ tất cả các tính năng của các đối tượng, nhưng chúng nhanh hơn và ít chi phí hơn.

Để các đối tượng hỗ trợ kế thừa, mỗi phiên bản cần "biết" trong thời gian chạy mà nó là một thể hiện của lớp. Mặt khác, các phương thức bị ghi đè không thể được giải quyết trong thời gian chạy. Đối với các đối tượng, điều này có nghĩa là dữ liệu cá thể được lưu trữ trong bộ nhớ cùng với một con trỏ tới đối tượng lớp. Nếu thông tin đó cũng nên được lưu trữ cùng với các giá trị nguyên thủy, các yêu cầu về bộ nhớ sẽ xuất hiện. Giá trị nguyên 16 bit sẽ yêu cầu 16 bit của nó cho giá trị và thêm vào đó là bộ nhớ 32 hoặc 64 bit cho một con trỏ tới lớp của nó.

Ngoài chi phí bộ nhớ, bạn cũng có thể mong đợi có thể ghi đè các hoạt động phổ biến trên các nguyên thủy như toán tử số học. Không có phân nhóm, các toán tử như +có thể được biên dịch thành một lệnh mã máy đơn giản. Nếu nó có thể được ghi đè, bạn sẽ cần phải giải quyết các phương pháp trong thời gian chạy, một nhiều hoạt động tốn kém hơn. (Bạn có thể biết rằng C # hỗ trợ nạp chồng toán tử - nhưng điều này không giống nhau. Quá tải toán tử được giải quyết tại thời điểm biên dịch, do đó không có hình phạt thời gian chạy mặc định.)

Các chuỗi không phải là nguyên thủy nhưng chúng vẫn "đặc biệt" trong cách chúng được thể hiện trong bộ nhớ. Ví dụ, chúng được "thực tập", có nghĩa là hai chuỗi ký tự bằng nhau có thể được tối ưu hóa cho cùng một tham chiếu. Điều này sẽ không thể thực hiện được (hoặc ít hiệu quả hơn rất nhiều) nếu các trường hợp chuỗi cũng nên theo dõi lớp.

Những gì bạn mô tả chắc chắn sẽ hữu ích, nhưng hỗ trợ nó sẽ đòi hỏi chi phí hiệu năng cho mỗi lần sử dụng nguyên thủy và chuỗi, ngay cả khi chúng không tận dụng lợi thế của thừa kế.

Ngôn ngữ Smalltalk không (tôi tin) cho phép phân lớp các số nguyên. Nhưng khi Java được thiết kế, Smalltalk được coi là quá chậm và chi phí cho mọi thứ là một đối tượng được coi là một trong những lý do chính. Java đã hy sinh một số sự thanh lịch và tinh khiết về khái niệm để có được hiệu năng tốt hơn.


12
@Den: stringđược niêm phong vì nó được thiết kế để hành xử bất biến. Nếu một người có thể kế thừa từ chuỗi, có thể tạo ra các chuỗi có thể thay đổi, điều này sẽ khiến nó thực sự dễ bị lỗi. Hàng tấn mã, bao gồm chính .NET framework, dựa vào các chuỗi không có tác dụng phụ. Xem thêm ở đây, nói với bạn như vậy: quora.com/Why-String- class
Doc Brown

5
@DocBrown Đây cũng là lý do Stringđược đánh dấu finaltrong Java.
Dev

47
"khi Java được thiết kế, Smalltalk được coi là quá chậm [Mạnh]. Java đã hy sinh một số sự thanh lịch và tinh khiết về khái niệm để có được hiệu suất tốt hơn." - Trớ trêu thay, tất nhiên, Java đã không thực sự đạt được hiệu suất đó cho đến khi Sun mua một công ty Smalltalk để có quyền truy cập vào công nghệ Smalltalk VM vì JVM của Sun bị chậm và phát hành HotSpot JVM, một VM Smalltalk được sửa đổi một chút.
Jörg W Mittag

3
@underscore_d: Câu trả lời bạn liên kết với rất rõ ràng rằng C♯ không có kiểu nguyên thủy. Chắc chắn, một số nền tảng tồn tại việc triển khai C♯ có thể có hoặc không có các kiểu nguyên thủy, nhưng điều đó không có nghĩa là C♯ có các kiểu nguyên thủy. Ví dụ, có một triển khai Ruby cho CLI và CLI có các kiểu nguyên thủy, nhưng điều đó không có nghĩa là Ruby có các kiểu nguyên thủy. Việc triển khai có thể hoặc không chọn thực hiện các loại giá trị bằng cách ánh xạ chúng thành các kiểu nguyên thủy của nền tảng nhưng đó là một chi tiết triển khai nội bộ riêng tư và không phải là một phần của thông số kỹ thuật.
Jörg W Mittag

10
Đó là tất cả về trừu tượng. Chúng ta phải giữ cho đầu của chúng ta rõ ràng, nếu không chúng ta kết thúc với vô nghĩa. Ví dụ: C♯ được triển khai trên .NET. .NET được triển khai trên Windows NT. Windows NT được triển khai trên x86. x86 được thực hiện trên silicon dioxide. SiO₂ chỉ là cát. Vì vậy, a stringtrong C♯ chỉ là cát? Không, tất nhiên là không, a stringtrong C♯ là những gì mà thông số C♯ nói. Làm thế nào nó được thực hiện là không liên quan. Việc triển khai C riêng sẽ triển khai các chuỗi dưới dạng các mảng byte, việc triển khai ECMAScript sẽ ánh xạ chúng tới các ECMAScript String, v.v.
Jörg W Mittag

20

Điều mà một số ngôn ngữ đề xuất không phải là phân lớp, mà là phân nhóm . Ví dụ, Ada cho phép bạn tạo có nguồn gốc loại hoặc phân nhóm . Phần Lập trình / Loại hệ thống Ada đáng để đọc để hiểu tất cả các chi tiết. Bạn có thể giới hạn phạm vi của các giá trị, đó là điều bạn muốn hầu hết thời gian:

 type Angle is range -10 .. 10;
 type Hours is range 0 .. 23; 

Bạn có thể sử dụng cả hai loại dưới dạng Số nguyên nếu bạn chuyển đổi chúng rõ ràng. Cũng lưu ý rằng bạn không thể sử dụng cái này thay cho cái khác, ngay cả khi phạm vi tương đương về mặt cấu trúc (các loại được kiểm tra theo tên).

 type Reference is Integer;
 type Count is Integer;

Các loại trên không tương thích, mặc dù chúng đại diện cho cùng một phạm vi giá trị.

(Nhưng bạn có thể sử dụng Unchecked_Conversion; đừng nói với những người tôi đã nói với bạn điều đó)


2
Trên thực tế, tôi nghĩ rằng nó là nhiều hơn về ngữ nghĩa. Sử dụng một số lượng mà một chỉ mục được mong đợi sau đó hy vọng sẽ gây ra lỗi thời gian biên dịch
Marjan Venema

@MarjanVenema Nó có, và điều này được thực hiện trên mục đích để bắt lỗi logic.
coredump

Quan điểm của tôi là không phải tất cả các trường hợp bạn muốn có ngữ nghĩa, bạn sẽ cần các phạm vi. Sau đó, bạn sẽ có type Index is -MAXINT..MAXINT;cái gì đó không làm gì cho tôi vì tất cả các số nguyên sẽ hợp lệ? Vì vậy, loại lỗi nào tôi sẽ chuyển qua Góc tới Chỉ mục nếu tất cả những gì được kiểm tra là phạm vi?
Marjan Venema

1
@MarjanVenema Trong ví dụ thứ hai, cả hai loại đều là kiểu con của Integer. Tuy nhiên, nếu bạn khai báo một hàm chấp nhận Đếm, bạn không thể vượt qua Tham chiếu vì việc kiểm tra loại dựa trên sự tương đương tên , trái với "tất cả những gì được kiểm tra là phạm vi". Điều này không giới hạn ở số nguyên, bạn có thể sử dụng các loại hoặc bản ghi liệt kê. ( archive.adaic.com/stiterias/83rat/html/ratl-04-03.html )
coredump

1
@Marjan Một ví dụ hay về lý do tại sao các loại gắn thẻ có thể khá mạnh mẽ có thể được tìm thấy trong loạt bài của Eric Lippert về việc triển khai Zorg ở OCaml . Làm điều này cho phép trình biên dịch bắt được rất nhiều lỗi - mặt khác, nếu bạn cho phép chuyển đổi hoàn toàn các kiểu này thì điều này dường như làm cho tính năng này trở nên vô dụng .. điều đó không có nghĩa là có thể gán loại PersonAge cho loại PersonId bởi vì cả hai đều có cùng một loại cơ bản.
Voo

16

Tôi nghĩ rằng đây rất có thể là một câu hỏi X / Y. Điểm nổi bật, từ câu hỏi ...

Động lực của tôi là tối đa hóa việc sử dụng hệ thống loại để xác minh tính chính xác của thời gian biên dịch.

... và từ bình luận của bạn xây dựng:

Tôi không muốn có thể thay thế cái này cho cái khác một cách ngầm định.

Xin lỗi nếu tôi thiếu thứ gì đó, nhưng ... Nếu đây là mục đích của bạn, thì tại sao bạn lại nói về sự kế thừa? Sự thay thế tiềm ẩn là ... giống như ... toàn bộ điều của nó. Bạn biết, Nguyên tắc thay thế Liskov?

Trên thực tế, những gì bạn dường như muốn là khái niệm 'typedef mạnh' - theo đó một cái gì đó 'là' ví dụ intvề phạm vi và đại diện nhưng không thể được thay thế thành các bối cảnh mong đợi intvà ngược lại. Tôi khuyên bạn nên tìm kiếm thông tin về thuật ngữ này và bất kỳ ngôn ngữ nào bạn chọn có thể gọi nó. Một lần nữa, nó hoàn toàn trái ngược với sự kế thừa.

Và đối với những người có thể không thích câu trả lời X / Y, tôi nghĩ rằng tiêu đề vẫn có thể trả lời được khi tham khảo LSP. Các kiểu nguyên thủy là nguyên thủy bởi vì chúng làm một cái gì đó rất đơn giản, và đó là tất cả những gì chúng làm . Cho phép chúng được thừa kế và do đó tạo ra vô số hiệu ứng có thể của chúng sẽ dẫn đến bất ngờ lớn khi vi phạm LSP tốt nhất và gây tử vong ở mức tồi tệ nhất. Nếu tôi có thể lạc quan cho rằng Thales Pereira sẽ không phiền tôi khi trích dẫn nhận xét phi thường này :

Có một vấn đề được thêm vào là nếu ai đó có thể thừa kế từ Int, bạn sẽ có mã vô tội như "int x = y + 2" (trong đó Y là lớp dẫn xuất) hiện ghi nhật ký vào Cơ sở dữ liệu, mở URL và bằng cách nào đó phục sinh Elvis. Các loại nguyên thủy được cho là an toàn và với ít nhiều được đảm bảo, hành vi được xác định rõ.

Nếu ai đó nhìn thấy một kiểu nguyên thủy, trong một ngôn ngữ lành mạnh, họ cho rằng nó sẽ luôn luôn làm một việc nhỏ của nó, rất tốt, không có gì bất ngờ. Các kiểu nguyên thủy không có khai báo lớp có sẵn báo hiệu cho dù chúng có thể hoặc không được kế thừa và có các phương thức của chúng bị ghi đè. Nếu đúng như vậy, điều đó thực sự rất đáng ngạc nhiên (và hoàn toàn phá vỡ khả năng tương thích ngược, nhưng tôi biết đó là câu trả lời ngược cho 'tại sao X không được thiết kế với Y').

... mặc dù, như Mooing Duck đã chỉ ra trong phản ứng, các ngôn ngữ cho phép quá tải toán tử cho phép người dùng tự nhầm lẫn ở mức độ tương tự hoặc bằng nhau nếu họ thực sự muốn, vì vậy không rõ liệu cuộc tranh luận cuối cùng này có tồn tại hay không. Và tôi sẽ ngừng tóm tắt ý kiến ​​của người khác bây giờ, heh.


4

Để cho phép kế thừa với công văn ảo 8which thường được coi là khá mong muốn trong thiết kế ứng dụng), người ta cần thông tin loại thời gian chạy. Đối với mọi đối tượng, một số dữ liệu liên quan đến loại đối tượng phải được lưu trữ. Một nguyên thủy, theo định nghĩa, thiếu thông tin này.

Có hai ngôn ngữ OOP chính (được quản lý, chạy trên VM) có tính năng nguyên thủy: C # và Java. Nhiều ngôn ngữ khác không có nguyên thủy ngay từ đầu hoặc sử dụng lý do tương tự để cho phép chúng / sử dụng chúng.

Nguyên thủy là một sự thỏa hiệp cho hiệu suất. Đối với mỗi đối tượng, bạn cần không gian cho tiêu đề đối tượng của nó (Trong Java, thường là 2 * 8 byte trên máy ảo 64 bit), cộng với các trường của nó, cộng với phần đệm cuối cùng (Trong Hotspot, mọi đối tượng chiếm một số byte là bội số của số 8). Vì vậy, một intđối tượng dưới dạng sẽ cần ít nhất 24 byte bộ nhớ được giữ xung quanh, thay vì chỉ có 4 byte (trong Java).

Do đó, các loại nguyên thủy đã được thêm vào để cải thiện hiệu suất. Họ làm cho rất nhiều thứ dễ dàng hơn. Điều đó a + bcó nghĩa gì nếu cả hai đều là kiểu con của int? Một số loại phân tán phải được thêm vào để chọn bổ sung chính xác. Điều này có nghĩa là công văn ảo. Có khả năng sử dụng một opcode rất đơn giản để bổ sung là rất nhiều, nhanh hơn nhiều và cho phép tối ưu hóa thời gian biên dịch.

Stringlà một trường hợp khác. Cả trong Java và C #, Stringlà một đối tượng. Nhưng trong C #, nó được niêm phong và trong Java là bản cuối cùng. Điều đó bởi vì cả các thư viện chuẩn Java và C # đều yêu cầu Strings là bất biến và việc phân lớp chúng sẽ phá vỡ tính bất biến này.

Trong trường hợp của Java, VM có thể (và không) thực hiện Chuỗi và "gộp" chúng, cho phép thực hiện tốt hơn. Điều này chỉ hoạt động khi String thực sự bất biến.

Thêm vào đó, người ta hiếm khi cần phân lớp các kiểu nguyên thủy. Miễn là người nguyên thủy không thể được phân lớp, có rất nhiều điều gọn gàng mà toán học cho chúng ta biết về chúng. Ví dụ, chúng ta có thể chắc chắn rằng bổ sung là giao hoán và kết hợp. Đó là một cái gì đó định nghĩa toán học của số nguyên cho chúng ta biết. Hơn nữa, chúng ta có thể dễ dàng prrofnts bất biến trên các vòng qua cảm ứng trong nhiều trường hợp. Nếu chúng tôi cho phép phân lớp int, chúng tôi sẽ mất các công cụ mà toán học cung cấp cho chúng tôi, bởi vì chúng tôi không còn có thể được đảm bảo rằng các thuộc tính nhất định giữ. Vì vậy, tôi muốn nói rằng khả năng không thể phân lớp các kiểu nguyên thủy thực sự là một điều tốt. Ít thứ hơn ai đó có thể phá vỡ, cộng với một trình biên dịch thường có thể chứng minh rằng anh ta được phép thực hiện một số tối ưu hóa nhất định.


1
Câu trả lời này là vực thẳm ... hẹp. to allow inheritance, one needs runtime type information.Sai trái. For every object, some data regarding the type of the object has to be stored.Sai trái. There are two mainstream OOP languages that feature primitives: C# and Java.Cái gì, là C ++ không phải là chủ đạo bây giờ? Tôi sẽ sử dụng nó làm phản bác vì thông tin loại thời gian chạy thuật ngữ C ++. Nó hoàn toàn không cần thiết trừ khi sử dụng dynamic_casthoặc typeid. Và ngay cả khi bật RTTI, việc thừa kế chỉ tiêu tốn dung lượng nếu một lớp có virtualcác phương thức mà một bảng phương thức cho mỗi lớp phải được trỏ tới mỗi phiên bản
underscore_d

1
Kế thừa trong C ++ hoạt động hoàn toàn khác nhau sau đó trong các ngôn ngữ chạy trên VM. công văn ảo yêu cầu RTTI, một cái gì đó không phải là một phần của C ++. Kế thừa mà không có công văn ảo là rất hạn chế và tôi thậm chí không chắc bạn có nên so sánh nó với kế thừa với công văn ảo hay không. Hơn nữa, khái niệm về một "đối tượng" rất khác nhau trong C ++, sau đó là trong C # hoặc Java. Bạn nói đúng, có một số điều tôi có thể nói tốt hơn, nhưng việc đi sâu vào tất cả các điểm khá liên quan nhanh chóng dẫn đến việc phải viết một cuốn sách về thiết kế ngôn ngữ.
Polygnome

3
Ngoài ra, đây không phải là trường hợp "công văn ảo yêu cầu RTTI" trong C ++. Một lần nữa, chỉ dynamic_casttypeinfoyêu cầu điều đó. Công văn ảo được thực hiện thực tế bằng cách sử dụng một con trỏ tới vtable cho lớp cụ thể của đối tượng, do đó cho phép các hàm đúng được gọi, nhưng nó không yêu cầu chi tiết về kiểu và quan hệ vốn có trong RTTI. Tất cả các trình biên dịch cần biết là liệu lớp của một đối tượng có đa hình hay không, và nếu vậy, vptr của thể hiện là gì. Người ta có thể biên dịch tầm thường hầu như các lớp được gửi với -fno-rtti.
gạch dưới

2
Trên thực tế, theo cách khác, RTTI yêu cầu gửi ảo. Nghĩa đen -C ++ không cho phép dynamic_casttrên các lớp mà không có công văn ảo. Lý do thực hiện là RTTI thường được triển khai như một thành viên ẩn của vtable.
MSalters

1
@MilesRout C ++ có mọi thứ mà ngôn ngữ cần cho OOP, ít nhất là các tiêu chuẩn có phần mới hơn. Người ta có thể lập luận rằng các tiêu chuẩn C ++ cũ hơn thiếu một số thứ cần thiết cho ngôn ngữ OOP, nhưng thậm chí đó là một sự kéo dài. C ++ không phải là ngôn ngữ OOP cấp cao , vì nó cho phép kiểm soát mức độ trực tiếp, thấp hơn đối với một số thứ, nhưng dù sao nó cũng cho phép OOP. (Cấp độ cao / Cấp độ thấp ở đây về tính trừu tượng , ngôn ngữ khác như ngôn ngữ được quản lý trừu tượng hơn hệ thống sau đó là C ++, do đó độ trừu tượng của chúng cao hơn).
Polygnome

4

Trong các ngôn ngữ OOP tĩnh mạnh chính, gõ phụ được xem chủ yếu như một cách để mở rộng một loại và ghi đè các phương thức hiện tại của loại.

Để làm như vậy, 'đối tượng' chứa một con trỏ tới kiểu của chúng. Đây là một chi phí chung: mã trong một phương thức sử dụng một Shapethể hiện trước tiên phải truy cập thông tin loại của thể hiện đó, trước khi nó biết Area()phương thức chính xác để gọi.

Một người nguyên thủy có xu hướng chỉ cho phép các hoạt động trên nó có thể dịch thành các hướng dẫn ngôn ngữ máy đơn lẻ và không mang theo bất kỳ thông tin loại nào với chúng. Làm cho một số nguyên chậm hơn để ai đó có thể phân lớp nó không đủ hấp dẫn để ngăn chặn bất kỳ ngôn ngữ nào đã trở thành chính thống.

Vì vậy, câu trả lời cho:

Tại sao các ngôn ngữ OOP tĩnh chính mạnh mẽ ngăn chặn kế thừa nguyên thủy?

Là:

  • Có ít nhu cầu
  • Và nó sẽ làm cho ngôn ngữ quá chậm
  • Subtyping chủ yếu được xem là một cách để mở rộng một loại, chứ không phải là một cách để kiểm tra loại tĩnh (do người dùng định nghĩa) tốt hơn.

Tuy nhiên, chúng tôi đang bắt đầu nhận các ngôn ngữ cho phép kiểm tra tĩnh dựa trên các thuộc tính của các biến khác sau đó là 'loại', ví dụ F # ​​có "thứ nguyên" và "đơn vị" để bạn không thể thêm chiều dài vào một khu vực .

Ngoài ra còn có các ngôn ngữ cho phép 'loại do người dùng xác định' không thay đổi (hoặc trao đổi) những gì một loại làm, nhưng chỉ giúp kiểm tra loại tĩnh; xem câu trả lời của coredump.


Đơn vị đo F # là một tính năng hay, mặc dù không may bị đặt tên sai. Ngoài ra, nó chỉ có thời gian biên dịch, vì vậy không siêu hữu ích, ví dụ như khi sử dụng gói NuGet đã biên dịch. Đúng hướng, mặc dù.
chối

Có lẽ thật thú vị khi lưu ý rằng "thứ nguyên" không phải là "một tài sản khác ngoài" loại ", nó chỉ là một loại loại phong phú hơn bạn có thể sử dụng.
porglezomp

3

Tôi không chắc chắn nếu tôi đang xem một cái gì đó ở đây, nhưng câu trả lời khá đơn giản:

  1. Định nghĩa của nguyên thủy là: giá trị nguyên thủy không phải là đối tượng, kiểu nguyên thủy không phải là kiểu đối tượng, nguyên thủy không phải là một phần của hệ thống đối tượng.
  2. Kế thừa là một tính năng của hệ thống đối tượng.
  3. Ergo, người nguyên thủy không thể tham gia thừa kế.

Lưu ý rằng thực sự chỉ có hai ngôn ngữ OOP tĩnh mạnh thậm chí còn các ngôn ngữ nguyên thủy là AFAIK: Java và C ++. (Trên thực tế, tôi thậm chí không chắc chắn về phần sau, tôi không biết nhiều về C ++ và những gì tôi tìm thấy khi tìm kiếm rất khó hiểu.)

Trong C ++, các nguyên thủy về cơ bản là một di sản được kế thừa (ý định chơi chữ) từ C. Vì vậy, chúng không tham gia vào hệ thống đối tượng (và do đó là thừa kế) vì C không có hệ thống đối tượng cũng không phải thừa kế.

Trong Java, nguyên thủy là kết quả của một nỗ lực sai lầm trong việc cải thiện hiệu suất. Nguyên thủy cũng là các loại giá trị duy nhất trong hệ thống, trên thực tế, không thể viết các loại giá trị trong Java và các đối tượng không thể là các loại giá trị. Vì vậy, ngoài thực tế là người nguyên thủy không tham gia vào hệ thống đối tượng và do đó, ý tưởng về "sự kế thừa" thậm chí không có ý nghĩa gì, ngay cả khi bạn có thể thừa kế từ họ, bạn sẽ không thể duy trì " giá trị ". Điều này khác với ví dụ C♯ mà không có các loại giá trị ( structs), mà dù sao cũng là những đối tượng.

Một điều nữa là việc không thể kế thừa thực ra cũng không phải là duy nhất đối với người nguyên thủy. Trong C♯, structs hoàn toàn kế thừa từ System.Objectvà có thể thực hiện interfaces, nhưng chúng không thể thừa kế từ hoặc không được thừa kế bởi classes hoặc structs. Ngoài ra, sealed classes không thể được thừa kế từ. Trong Java, final classes không thể được kế thừa từ.

tl; dr :

Tại sao các ngôn ngữ OOP tĩnh chính mạnh mẽ ngăn chặn kế thừa nguyên thủy?

  1. nguyên thủy không phải là một phần của hệ thống đối tượng (theo định nghĩa, nếu có, chúng sẽ không nguyên thủy), ý tưởng về sự kế thừa được gắn với hệ thống đối tượng, kế thừa nguyên thủy của ergo là một mâu thuẫn trong các điều khoản
  2. nguyên thủy không phải là duy nhất, rất nhiều loại khác cũng không thể được kế thừa ( finalhoặc sealedtrong Java hoặc C♯, structs trong C♯, case classes trong Scala)

3
Ơ ... tôi biết nó phát âm là "C Sharp", nhưng, ehm
Mr Lister

Tôi nghĩ rằng bạn đang nhầm lẫn về phía C ++. Nó hoàn toàn không phải là ngôn ngữ OO. Các phương thức lớp theo mặc định là không virtual, có nghĩa là chúng không tuân theo LSP. Ví dụ, std::stringkhông phải là nguyên thủy, nhưng nó rất giống như một giá trị khác. Ngữ nghĩa giá trị như vậy là khá phổ biến, toàn bộ phần STL của C ++ giả định nó.
MSalters

2
'Trong Java, nguyên thủy là kết quả của một nỗ lực sai lầm trong việc cải thiện hiệu suất.' Tôi nghĩ rằng bạn không có ý tưởng nào về mức độ hiệu quả của việc thực hiện nguyên thủy khi các loại đối tượng có thể mở rộng của người dùng. Quyết định đó trong java là cả có chủ ý và có cơ sở. Chỉ cần tưởng tượng phải phân bổ bộ nhớ cho mỗi intbạn sử dụng. Mỗi phân bổ theo thứ tự 100ns cộng với chi phí chung của việc thu gom rác. So sánh điều đó với chu kỳ CPU đơn được tiêu thụ bằng cách thêm hai ints nguyên thủy . Mã java của bạn sẽ bò theo nếu các nhà thiết kế ngôn ngữ đã quyết định khác.
cmaster

1
@cmaster: Scala không có nguyên thủy và hiệu suất số của nó hoàn toàn giống với Java. Bởi vì, tốt, nó biên dịch các số nguyên thành các nguyên hàm JVM int, vì vậy chúng thực hiện chính xác như nhau. (Scala bản địa biên dịch chúng thành thanh ghi máy nguyên thủy, Scala.js biên dịch chúng thành nguyên thủy ECMAScript Numbers.) Ruby không có nguyên thủy, nhưng YARV và Rubinius biên dịch nguyên thành số nguyên máy nguyên thủy, JRuby biên dịch chúng thành JVM nguyên thủy longs. Khá nhiều mỗi triển khai Lisp, Smalltalk hoặc Ruby sử dụng các nguyên hàm trong VM . Đó là nơi tối ưu hóa hiệu suất đào tạo
Jörg W Mittag

1
Sàng thuộc về: trong trình biên dịch, không phải ngôn ngữ.
Jörg W Mittag

2

Joshua Bloch trong Java hiệu quả Java Lời khuyên nên thiết kế rõ ràng để kế thừa hoặc cấm nó. Các lớp nguyên thủy không được thiết kế để kế thừa vì chúng được thiết kế để không thay đổi và cho phép thừa kế có thể thay đổi điều đó trong các lớp con, do đó phá vỡ nguyên tắc Liskov và nó sẽ là nguồn gốc của nhiều lỗi.

Dù sao, tại sao đây là một cách giải quyết khó khăn? Bạn nên thực sự thích thành phần hơn thừa kế. Nếu lý do là hiệu suất hơn bạn có một điểm và câu trả lời cho câu hỏi của bạn là không thể đưa tất cả các tính năng vào Java vì phải mất thời gian để phân tích tất cả các khía cạnh khác nhau của việc thêm tính năng. Ví dụ, Java không có Generics trước 1.5.

Nếu bạn có nhiều kiên nhẫn thì bạn thật may mắn vì có kế hoạch thêm các lớp giá trị vào Java, điều này sẽ cho phép bạn tạo các lớp giá trị giúp bạn tăng hiệu suất và đồng thời nó sẽ giúp bạn linh hoạt hơn.


2

Ở cấp độ trừu tượng, bạn có thể bao gồm mọi thứ bạn muốn bằng ngôn ngữ bạn đang thiết kế.

Ở cấp độ thực hiện, không thể tránh khỏi việc một số trong những điều đó sẽ đơn giản hơn để thực hiện, một số sẽ phức tạp, một số có thể được thực hiện nhanh chóng, một số bị ràng buộc là chậm hơn, v.v. Để giải thích cho điều này, các nhà thiết kế thường phải đưa ra quyết định khó khăn và thỏa hiệp.

Ở cấp độ triển khai, một trong những cách nhanh nhất chúng tôi đã đưa ra để truy cập vào một biến là tìm ra địa chỉ của nó và tải nội dung của địa chỉ đó. Có các hướng dẫn cụ thể trong hầu hết các CPU để tải dữ liệu từ các địa chỉ và các hướng dẫn đó thường cần biết chúng cần tải bao nhiêu byte (một, hai, bốn, tám, v.v.) và nơi để đặt dữ liệu chúng tải (đăng ký đơn, đăng ký cặp, thanh ghi mở rộng, bộ nhớ khác, vv). Bằng cách biết kích thước của một biến, trình biên dịch có thể biết chính xác hướng dẫn nào sẽ phát ra để sử dụng biến đó. Khi không biết kích thước của một biến, trình biên dịch sẽ cần phải sử dụng đến một thứ phức tạp hơn và có thể chậm hơn.

Ở cấp độ trừu tượng, điểm của phân nhóm là có thể sử dụng các thể hiện của một loại trong đó loại tương đương hoặc chung hơn được mong đợi. Nói cách khác, mã có thể được viết để mong đợi một đối tượng thuộc một loại cụ thể hoặc bất kỳ thứ gì có nguồn gốc hơn, mà không biết trước chính xác điều này sẽ là gì. Và rõ ràng, khi nhiều loại dẫn xuất có thể thêm nhiều thành viên dữ liệu, một loại dẫn xuất không nhất thiết phải có cùng yêu cầu bộ nhớ với các loại cơ sở của nó.

Ở cấp độ triển khai, không có cách đơn giản nào để một biến có kích thước được xác định trước giữ một thể hiện có kích thước không xác định và được truy cập theo cách bạn thường gọi hiệu quả. Nhưng có một cách để di chuyển mọi thứ xung quanh một chút và sử dụng một biến không phải để lưu trữ đối tượng, mà để xác định đối tượng và để đối tượng đó được lưu trữ ở một nơi khác. Cách đó là một tham chiếu (ví dụ: địa chỉ bộ nhớ) - một mức độ bổ sung bổ sung để đảm bảo rằng một biến chỉ cần giữ một số loại thông tin có kích thước cố định, miễn là chúng ta có thể tìm thấy đối tượng thông qua thông tin đó. Để đạt được điều đó, chúng ta chỉ cần tải địa chỉ (kích thước cố định) và sau đó chúng ta có thể làm việc như bình thường bằng cách sử dụng các giá trị bù trừ của đối tượng mà chúng ta biết là hợp lệ, ngay cả khi đối tượng đó có nhiều dữ liệu hơn mà chúng ta không biết. Chúng tôi có thể làm điều đó bởi vì chúng tôi không '

Ở cấp độ trừu tượng, phương pháp này cho phép bạn lưu trữ một (tham chiếu đến a) stringvào một objectbiến mà không làm mất thông tin làm cho nó a string. Tất cả các loại đều hoạt động tốt như thế này và bạn cũng có thể nói nó thanh lịch ở nhiều khía cạnh.

Tuy nhiên, ở cấp độ triển khai, mức độ bổ sung thêm bao gồm nhiều hướng dẫn hơn và trên hầu hết các kiến ​​trúc, nó làm cho mỗi lần truy cập vào đối tượng có phần chậm hơn. Bạn có thể cho phép trình biên dịch giảm hiệu năng ra khỏi chương trình nếu bạn đưa vào ngôn ngữ của mình một số loại thường được sử dụng mà không có mức độ bổ sung (tham chiếu) đó. Nhưng bằng cách loại bỏ mức độ gián tiếp đó, trình biên dịch không thể cho phép bạn phân nhóm theo cách an toàn cho bộ nhớ nữa. Đó là bởi vì nếu bạn thêm nhiều thành viên dữ liệu vào loại của mình và bạn gán cho loại chung hơn, mọi thành viên dữ liệu bổ sung không phù hợp với không gian được phân bổ cho biến mục tiêu sẽ bị cắt đi.


1

Nói chung

Nếu một lớp là trừu tượng (ẩn dụ: một hộp có lỗ (s)), thì nó vẫn ổn (thậm chí bắt buộc phải có thứ gì đó có thể sử dụng được!) Để "lấp đầy lỗ hổng", đó là lý do tại sao chúng ta phân lớp các lớp trừu tượng.

Nếu một lớp là cụ thể (ẩn dụ: một hộp đầy), sẽ không ổn khi thay đổi cái hiện có bởi vì nếu nó đầy, nó đầy. Chúng tôi không có chỗ để thêm một cái gì đó vào bên trong hộp, đó là lý do tại sao chúng tôi không nên phân lớp các lớp cụ thể.

Với người nguyên thủy

Nguyên thủy là các lớp bê tông theo thiết kế. Chúng đại diện cho một cái gì đó nổi tiếng, hoàn toàn xác định (Tôi chưa bao giờ thấy một loại nguyên thủy với một cái gì đó trừu tượng, nếu không nó không còn là nguyên thủy nữa) và được sử dụng rộng rãi trong hệ thống. Cho phép phân lớp một kiểu nguyên thủy và cung cấp triển khai của riêng bạn cho những người khác dựa vào hành vi được thiết kế của người nguyên thủy có thể gây ra rất nhiều tác dụng phụ và thiệt hại lớn!



Liên kết là một ý kiến ​​thiết kế thú vị. Cần suy nghĩ nhiều hơn cho tôi.
Den

1

Thông thường thừa kế không phải là ngữ nghĩa mà bạn muốn, bởi vì bạn không thể thay thế loại đặc biệt của mình ở bất cứ nơi nào mà người nguyên thủy mong đợi. Để mượn từ ví dụ của bạn, a Quantity + Indexkhông có ý nghĩa về mặt ngữ nghĩa, vì vậy một mối quan hệ thừa kế là mối quan hệ sai.

Tuy nhiên, một số ngôn ngữ có khái niệm về một loại giá trị thể hiện loại mối quan hệ bạn đang mô tả. Scala là một ví dụ. Một loại giá trị sử dụng một nguyên thủy làm biểu diễn cơ bản, nhưng có một nhận dạng lớp và hoạt động khác ở bên ngoài. Điều đó có tác dụng mở rộng một kiểu nguyên thủy, nhưng đó là một tác phẩm thay vì mối quan hệ thừa kế.

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.