Sự khác biệt giữa một lớp con và một kiểu con là gì?


44

Câu trả lời được đánh giá cao nhất cho câu hỏi này về Nguyên tắc thay thế Liskov phải đau đớn để phân biệt giữa các thuật ngữ phân nhómphân lớp . Nó cũng đưa ra quan điểm rằng một số ngôn ngữ kết hợp cả hai, trong khi những ngôn ngữ khác thì không.

Đối với các ngôn ngữ hướng đối tượng mà tôi quen thuộc nhất (Python, C ++), "type" và "class" là các khái niệm đồng nghĩa. Về mặt C ++, việc phân biệt giữa phân nhóm và phân lớp có nghĩa là gì? Nói, ví dụ, đó Foolà một lớp con, nhưng không phải là một kiểu con , FooBase. Nếu foolà một ví dụ của Foo, dòng này sẽ:

FooBase* fbPoint = &foo;

không còn giá trị?


6
Trên thực tế, trong Python "type" và "class" là các khái niệm riêng biệt . Trong thực tế, Python được tạo kiểu động, "loại" không phải là một khái niệm nào cả bằng Python. Thật không may, các nhà phát triển Python không hiểu điều đó và vẫn kết luận cả hai.
Jörg W Mittag

11
"Loại" và "lớp" là khác biệt trong C ++. "Mảng ints" là một loại; nó là lớp nào "Con trỏ tới một biến kiểu int" là một kiểu; nó là lớp nào Những thứ này không phải là bất kỳ lớp nào, nhưng chúng chắc chắn là các loại.
Eric Lippert

2
Tôi đã tự hỏi điều này rất nhiều sau khi đọc câu hỏi đó và câu trả lời đó.
dùng369450

4
@JorgWMittag Nếu không có khái niệm về một "loại" trong python sau đó một người nào đó nên nói với bất cứ ai viết tài liệu: docs.python.org/3/library/stdtypes.html
Matt

@ Công bằng để công bằng, các loại đã xuất hiện trong 3.5, khá gần đây, đặc biệt là theo tiêu chuẩn sản xuất tôi được phép sử dụng trong sản xuất.
Jared Smith

Câu trả lời:


53

Subtyping là một dạng đa hình kiểu trong đó một kiểu con là một kiểu dữ liệu có liên quan đến một kiểu dữ liệu khác (siêu kiểu) bởi một số khái niệm về khả năng thay thế, có nghĩa là các phần tử chương trình, điển hình là chương trình con hoặc hàm, được viết để hoạt động trên các phần tử của siêu kiểu cũng có thể hoạt động trên các yếu tố của tiểu loại.

Nếu Slà một kiểu con của T, mối quan hệ của kiểu con thường được viết S <: T, có nghĩa là bất kỳ thuật ngữ loại nào Scũng có thể được sử dụng một cách an toàn trong ngữ cảnh mà một thuật ngữ loại Tđược mong đợi. Các ngữ nghĩa chính xác của việc phân nhóm chủ yếu phụ thuộc vào các chi tiết của "được sử dụng an toàn trong ngữ cảnh" nghĩa là gì trong một ngôn ngữ lập trình nhất định.

Phân lớp không nên nhầm lẫn với phân nhóm. Nói chung, phân nhóm thiết lập mối quan hệ is-a, trong khi phân lớp chỉ sử dụng lại việc thực hiện và thiết lập mối quan hệ cú pháp, không nhất thiết phải là mối quan hệ ngữ nghĩa (kế thừa không đảm bảo phân nhóm hành vi).

Để phân biệt các khái niệm này, phân nhóm còn được gọi là kế thừa giao diện , trong khi phân lớp được gọi là kế thừa thực hiện hoặc kế thừa mã.

Tài liệu tham khảo
Subtyping
thừa kế


1
Nói rất hay. Có thể đáng nói trong bối cảnh câu hỏi rằng các lập trình viên C ++ thường sử dụng các lớp cơ sở ảo thuần túy để truyền đạt các mối quan hệ phụ cho hệ thống loại. Tất nhiên phương pháp lập trình chung thường được ưa thích.
Aluan Haddad

6
"Các ngữ nghĩa chính xác của việc phân nhóm chủ yếu phụ thuộc vào các chi tiết của" những gì được sử dụng một cách an toàn trong bối cảnh "có nghĩa là trong một ngôn ngữ lập trình nhất định." Lọ và LSP định nghĩa một ý tưởng hơi hợp lý về ý nghĩa của "một cách an toàn" và cho chúng tôi biết những hạn chế mà các chi tiết đó phải đáp ứng để cho phép hình thức "an toàn" đặc biệt này.
Jörg W Mittag

Thêm một mẫu trên đống: nếu tôi hiểu chính xác, trong C ++, publicthừa kế giới thiệu một kiểu con trong khi privatethừa kế giới thiệu một lớp con.
Quentin

Kế thừa công khai @Quentin vừa là một kiểu con và một lớp con, nhưng riêng tư chỉ là một lớp con, nhưng không phải là một kiểu con. Bạn có thể có subtyping mà không subclassing với các cấu trúc giống như giao diện Java
eques

27

Một loại , trong bối cảnh mà chúng ta đang nói đến ở đây, về cơ bản là một tập hợp các đảm bảo hành vi. Một hợp đồng , nếu bạn muốn. Hoặc, thuật ngữ mượn từ Smalltalk, một giao thức .

Một lớp là một bó các phương thức. Nó là một tập hợp các hành vi thực hiện .

Subtyping là một phương tiện để tinh chỉnh giao thức. Phân lớp là một phương tiện sử dụng lại mã vi sai, tức là sử dụng lại mã bằng cách chỉ mô tả sự khác biệt trong hành vi.

Nếu bạn đã sử dụng Java hoặc C♯, thì bạn có thể gặp phải lời khuyên rằng tất cả các loại nên là interfaceloại. Trên thực tế, nếu bạn đọc Hiểu về trừu tượng dữ liệu của William Cook , được xem lại , thì bạn có thể biết rằng để thực hiện OO trong các ngôn ngữ đó, bạn chỉ phải sử dụng interfaces làm loại. (Ngoài ra, thực tế thú vị: Java interfaceđược đặt trực tiếp từ các giao thức của Objective-C, lần lượt được lấy trực tiếp từ Smalltalk.)

Bây giờ, nếu chúng ta làm theo lời khuyên mã hóa đó đến kết luận logic của nó và tưởng tượng một phiên bản Java, trong đó chỉ có interface các loại và các lớp và nguyên thủy không có, thì một interfacekế thừa từ một thứ khác sẽ tạo ra một mối quan hệ phụ, trong khi một classkế thừa từ một ý chí khác chỉ đơn thuần để tái sử dụng mã vi sai thông qua super.

Theo như tôi biết, không có ngôn ngữ chính được gõ tĩnh nào phân biệt nghiêm ngặt giữa việc thừa kế (kế thừa thực hiện / phân lớp) và kế thừa hợp đồng (phân nhóm). Trong Java và C♯, kế thừa giao diện là kiểu con thuần túy (hoặc ít nhất là cho đến khi giới thiệu các phương thức mặc định trong Java 8 và có khả năng là C♯ 8), nhưng kế thừa lớp cũng là kiểu con cũng như kế thừa thực hiện. Tôi nhớ đã đọc về một phương ngữ LISP hướng đối tượng được gõ tĩnh, được phân biệt nghiêm ngặt giữa các mixin (có chứa hành vi), cấu trúc (chứa trạng thái), giao diện ( mô tảhành vi) và các lớp (bao gồm 0 hoặc nhiều cấu trúc với một hoặc nhiều mixin và phù hợp với một hoặc nhiều giao diện). Chỉ các lớp có thể được khởi tạo và chỉ các giao diện có thể được sử dụng làm kiểu.

Trong một ngôn ngữ OO được gõ động như Python, Ruby, ECMAScript hoặc Smalltalk, chúng ta thường nghĩ về loại (các) đối tượng là tập hợp các giao thức mà nó tuân thủ. Lưu ý số nhiều: một đối tượng có thể có nhiều loại và tôi không chỉ nói về thực tế rằng mỗi đối tượng của loại Stringcũng là một đối tượng của loại Object. (BTW: lưu ý cách tôi sử dụng tên lớp để nói về các loại? Thật ngu ngốc đối với tôi!) Một đối tượng có thể thực hiện nhiều giao thức. Ví dụ, trong Ruby, Arrayscó thể được thêm vào, chúng có thể được lập chỉ mục, chúng có thể được lặp lại và chúng có thể được so sánh. Đó là bốn giao thức khác nhau mà họ thực hiện!

Bây giờ, Ruby không có loại. Nhưng cộng đồng Ruby có các loại! Chúng chỉ tồn tại trong đầu của các lập trình viên, mặc dù. Và trong tài liệu. Ví dụ, bất kỳ đối tượng đáp ứng với một phương pháp gọi là eachbởi năng suất các yếu tố của nó từng người một được coi là một đếm được đối tượng. Và có một mixin gọi Enumerablephụ thuộc vào giao thức này. Vì vậy, nếu đối tượng của bạn có đúng loại (trong đó chỉ tồn tại trong đầu của lập trình viên), sau đó nó được phép trộn trong (kế thừa từ) các Enumerablemixin, và nó cũng có được tất cả các loại phương pháp mát miễn phí, như map, reduce, filtervà do đó trên, bật.

Tương tự, nếu một đối tượng đáp ứng <=>, sau đó nó được coi là thực hiện so sánh giao thức, và nó có thể kết hợp trong Comparablemixin và nhận được những thứ như thế <, <=, >, <=, ==, between?, và clampmiễn phí. Tuy nhiên, nó cũng có thể tự thực hiện tất cả các phương thức đó, và không kế thừa từ Comparabletất cả, và nó vẫn sẽ được coi là tương đương .

Một ví dụ điển hình là StringIOthư viện, về cơ bản làm giả các luồng I / O bằng các chuỗi. Nó thực hiện tất cả các phương thức giống như IOlớp, nhưng không có mối quan hệ thừa kế giữa hai. Tuy nhiên, một StringIOcó thể được sử dụng ở mọi nơi một IOcó thể được sử dụng. Điều này rất hữu ích trong các bài kiểm tra đơn vị, trong đó bạn có thể thay thế một tệp hoặc stdinbằng một StringIOmà không phải thực hiện bất kỳ thay đổi nào nữa cho chương trình của bạn. Vì StringIOtuân thủ cùng một giao thức IO, cả hai đều cùng loại, mặc dù chúng là các lớp khác nhau và không chia sẻ mối quan hệ nào (ngoại trừ tầm thường mà cả hai đều mở rộng Objecttại một số điểm).


Có thể hữu ích nếu các ngôn ngữ cho phép các chương trình khai báo đồng thời một loại lớp và giao diện mà lớp đó là một triển khai và cũng cho phép các triển khai chỉ định "hàm tạo" (sẽ tạo chuỗi cho các hàm tạo của các lớp được chỉ định bởi giao diện). Đối với các loại đối tượng mà các tham chiếu sẽ được chia sẻ công khai, mẫu ưa thích sẽ là loại lớp chỉ được sử dụng khi tạo các lớp dẫn xuất; hầu hết các tài liệu tham khảo nên thuộc loại giao diện. Có thể chỉ định các nhà xây dựng giao diện sẽ hữu ích trong các tình huống trong đó ...
supercat

... Ví dụ: mã cần một bộ sưu tập sẽ cho phép một bộ giá trị nhất định được đọc theo chỉ mục, nhưng không thực sự quan tâm nó thuộc loại nào. Mặc dù có những lý do hợp lý để nhận ra các lớp và giao diện là các loại loại riêng biệt, có nhiều tình huống chúng có thể phối hợp chặt chẽ hơn với nhau so với các ngôn ngữ hiện nay cho phép.
supercat

Bạn có tham khảo hoặc một số từ khóa tôi có thể tìm kiếm thêm một số thông tin về phương ngữ LISP thử nghiệm mà bạn đã đề cập để phân biệt chính thức các mixin, cấu trúc, giao diện và lớp không?
điện thoại

@tel: Không, xin lỗi. Có lẽ là khoảng 15-20 năm trước, và tại thời điểm đó, sở thích của tôi ở khắp mọi nơi. Tôi không thể bắt đầu nói với bạn những gì tôi đang tìm kiếm khi tôi tình cờ thấy điều này.
Jörg W Mittag

À. Đó là chi tiết thú vị nhất trong tất cả các câu trả lời này. Việc một sự phân tách chính thức của các khái niệm đó thực sự có thể xảy ra trong quá trình thực hiện một ngôn ngữ thực sự đã giúp kết tinh sự phân biệt lớp / loại đối với tôi. Tôi đoán tôi sẽ tự mình đi tìm LISP đó, trong mọi trường hợp. Bạn có tình cờ nhớ nếu bạn đọc về nó trong một bài báo / cuốn sách tạp chí, hoặc nếu bạn chỉ nghe về nó trong cuộc trò chuyện?
điện thoại

2

Có lẽ đầu tiên là hữu ích để phân biệt giữa một loại và một lớp và sau đó đi sâu vào sự khác biệt giữa phân nhóm và phân lớp.

Đối với phần còn lại của câu trả lời này, tôi sẽ giả định rằng các loại trong cuộc thảo luận là các loại tĩnh (vì việc phân nhóm thường xuất hiện trong một bối cảnh tĩnh).

Tôi sẽ phát triển một mã giả đồ chơi để giúp minh họa sự khác biệt giữa một loại và một lớp vì hầu hết các ngôn ngữ đều giới thiệu chúng ít nhất một phần (vì lý do chính đáng mà tôi sẽ chạm vào một chút).

Hãy bắt đầu với một loại. Một loại là một nhãn cho một biểu thức trong mã của bạn. Giá trị của nhãn này và liệu nó có nhất quán (đối với một số định nghĩa nhất quán theo hệ thống cụ thể) với tất cả giá trị của các nhãn khác có thể được xác định bởi một chương trình bên ngoài (một máy đánh chữ) mà không cần chạy chương trình của bạn. Đó là những gì làm cho các nhãn này đặc biệt và xứng đáng với tên riêng của họ.

Trong ngôn ngữ đồ chơi của chúng tôi, chúng tôi có thể cho phép tạo ra các nhãn như vậy.

declare type Int
declare type String

Sau đó, chúng tôi có thể gắn nhãn các giá trị khác nhau thuộc loại này.

0 is of type Int
1 is of type Int
-1 is of type Int
...

"" is of type String
"a" is of type String
"b" is of type String
...

Với các câu lệnh này, máy đánh chữ của chúng tôi giờ đây có thể từ chối các câu lệnh như

0 is of type String

nếu một trong những yêu cầu của hệ thống loại của chúng tôi là mọi biểu thức có một loại duy nhất.

Bây giờ chúng ta hãy bỏ qua việc này thật rắc rối và bạn sẽ gặp vấn đề như thế nào khi gán vô số loại biểu thức. Chúng ta có thể quay lại nó sau.

Mặt khác, một lớp là một tập hợp các phương thức và các trường được nhóm lại với nhau (có khả năng với các sửa đổi truy cập như là riêng tư hoặc công khai).

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

Một thể hiện của lớp này có khả năng tạo hoặc sử dụng các định nghĩa có sẵn của các phương thức và trường này.

Chúng ta có thể chọn liên kết một lớp với một loại sao cho mọi phiên bản của một lớp được tự động gắn nhãn với loại đó.

associate StringClass with String

Nhưng không phải loại nào cũng cần có một lớp liên kết.

# Hmm... Doesn't look like there's a class for Int

Cũng có thể hình dung rằng trong ngôn ngữ đồ chơi của chúng ta, không phải lớp nào cũng có một loại, đặc biệt là nếu không phải tất cả các biểu thức của chúng ta đều có loại. Sẽ khó hơn một chút (nhưng không phải là không thể) để tưởng tượng các quy tắc nhất quán của hệ thống sẽ trông như thế nào nếu một số biểu thức có kiểu và một số thì không.

Hơn nữa, trong ngôn ngữ đồ chơi của chúng tôi, các hiệp hội này không phải là duy nhất. Chúng ta có thể liên kết hai lớp với cùng một loại.

associate MyCustomStringClass with String

Bây giờ hãy nhớ rằng không có yêu cầu nào cho trình đánh máy của chúng tôi để theo dõi giá trị của một biểu thức (và trong hầu hết các trường hợp, nó sẽ không hoặc không thể làm như vậy). Tất cả những gì nó biết là nhãn bạn đã nói với nó. Như một lời nhắc nhở trước đây, người đánh máy chỉ có thể từ chối câu lệnh 0 is of type Stringvì quy tắc loại được tạo giả tạo của chúng tôi rằng các biểu thức phải có các kiểu duy nhất và chúng tôi đã gắn nhãn cho biểu thức 0khác. Nó không có bất kỳ kiến ​​thức đặc biệt nào về giá trị của 0.

Vậy còn phân nhóm thì sao? Subtyping cũng là một tên cho một quy tắc phổ biến trong việc đánh máy giúp thư giãn các quy tắc khác mà bạn có thể có. Cụ thể là nếu A is subtype of Bsau đó ở khắp mọi nơi máy đánh chữ của bạn yêu cầu một nhãn B, nó cũng sẽ chấp nhận A.

Ví dụ, chúng tôi có thể làm như sau cho các số của chúng tôi thay vì những gì chúng tôi đã có trước đây.

declare type NaturalNum
declare type Int
NaturalNum is subtype of Int

0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...

Phân lớp là một cách viết tắt để khai báo một lớp mới cho phép bạn sử dụng lại các phương thức và trường đã khai báo trước đó.

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

Chúng tôi không phải trường hợp sư ExtendedStringClassvới Stringgiống như chúng ta đã làm với StringClasstừ đó, sau khi tất cả đó là một lớp hoàn toàn mới, chúng tôi chỉ không phải ghi càng nhiều. Điều này sẽ cho phép chúng tôi đưa ra ExtendedStringClassmột loại không tương thích với Stringquan điểm của người đánh máy.

Tương tự như vậy, chúng tôi có thể đã quyết định tạo ra một lớp hoàn toàn mới NewClassvà thực hiện

associate NewClass with String

Bây giờ mọi trường hợp StringClasscó thể được thay thế bằng NewClassquan điểm của người đánh máy.

Vì vậy, trong lý thuyết phân nhóm và phân lớp là những thứ hoàn toàn khác nhau. Nhưng không có ngôn ngữ nào tôi biết có loại và lớp thực sự làm mọi thứ theo cách này. Hãy bắt đầu giảm ngôn ngữ của chúng tôi và giải thích lý do đằng sau một số quyết định của chúng tôi.

Trước hết, mặc dù về mặt lý thuyết, các lớp hoàn toàn khác nhau có thể được cung cấp cùng loại hoặc một lớp có thể được cung cấp cùng loại với các giá trị không phải là phiên bản của bất kỳ lớp nào, điều này cản trở nghiêm trọng tính hữu ích của trình đánh máy. Trình đánh máy được đánh cắp một cách hiệu quả khả năng kiểm tra xem phương thức hoặc trường bạn đang gọi trong một biểu thức có thực sự tồn tại trên giá trị đó hay không, đây có thể là một kiểm tra mà bạn thích nếu bạn gặp rắc rối khi chơi cùng với máy đánh chữ. Rốt cuộc, ai biết giá trị thực sự bên dưới Stringnhãn đó là gì; nó có thể là một cái gì đó không có, ví dụ, một concatenatephương pháp nào cả!

Được rồi, vì vậy hãy quy định rằng mỗi lớp sẽ tự động tạo ra một loại mới cùng tên với các thể hiện của lớp đó và associates với loại đó. Điều đó cho phép chúng ta thoát khỏi associatecũng như các tên khác nhau giữa StringClassString.

Vì lý do tương tự, có lẽ chúng ta muốn tự động thiết lập mối quan hệ phụ giữa các loại của hai lớp trong đó một lớp là lớp con của lớp khác. Sau khi tất cả các lớp con được đảm bảo có tất cả các phương thức và các trường mà lớp cha thực hiện, nhưng điều ngược lại là không đúng. Do đó, trong khi lớp con có thể vượt qua bất cứ lúc nào bạn cần một loại lớp cha, loại của lớp cha nên bị loại bỏ nếu bạn cần loại của lớp con.

Nếu bạn kết hợp điều này với quy định rằng tất cả các giá trị do người dùng xác định phải là các thể hiện của một lớp, thì bạn có thể phải is subclass ofthực hiện nhiệm vụ kép và loại bỏ is subtype of.

Và điều này đưa chúng ta đến những đặc điểm mà hầu hết các ngôn ngữ OO được gõ tĩnh phổ biến chia sẻ. Có một tập các kiểu "nguyên thủy" (ví dụ int, floatvv) mà không liên quan đến bất kỳ lớp và không phải là người dùng định nghĩa. Sau đó, bạn có tất cả các lớp do người dùng định nghĩa sẽ tự động có các loại cùng tên và xác định phân lớp với phân nhóm.

Lưu ý cuối cùng tôi sẽ đưa ra là về sự vụng về của việc khai báo các loại tách biệt với các giá trị. Hầu hết các ngôn ngữ liên quan đến việc tạo ra hai, do đó, một khai báo kiểu cũng là một khai báo để tạo các giá trị hoàn toàn mới được tự động gắn nhãn với loại đó. Ví dụ, một khai báo lớp thường tạo cả kiểu cũng như cách khởi tạo các giá trị của kiểu đó. Điều này được loại bỏ một số sự vụng về và, với sự có mặt của các nhà xây dựng, cũng cho phép bạn tạo nhãn vô hạn nhiều giá trị với một kiểu trong một nét.

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.