サブクラスとサブタイプの違いは何ですか?


44

Liskov Substitution Principleについてのこの質問に対する最も高い評価の答えは、サブタイプサブクラスという用語を区別するのに苦労します。また、一部の言語はこの2つを統合しますが、他の言語は統合しないという点も重要です。

私が最もよく知っているオブジェクト指向言語(Python、C ++)の場合、「タイプ」と「クラス」は同義の概念です。C ++に関して、サブタイプとサブクラスを区別することはどういう意味ですか?たとえば、Fooサブクラスではなく、サブタイプであるとしFooBaseます。のfooインスタンスがの場合Foo、次の行になります:

FooBase* fbPoint = &foo;

無効になりましたか?


6
実際、Pythonでは「タイプ」と「クラス」は別個の概念です。実際、Pythonは動的に型指定されているため、「型」はPythonの概念はありません。残念ながら、Pythonの開発者がそれを理解していない、とまだ 2をconflate。
ヨルグWミットタグ

11
「タイプ」と「クラス」は、C ++でも区別されます。「intの配列」はタイプです。それはどのクラスですか?「int型の変数へのポインター」は型です。それはどのクラスですか?これらはクラスではありませんが、確かに型です。
エリックリッパー

2
その質問とその答えを読んだ後、私はこのこと自体を疑問に思っていました。
user369450

4
@JorgWMittagパイソンの「タイプ」の概念がない場合は、誰かが文書に書き込む誰でも教えてください:docs.python.org/3/library/stdtypes.htmlを
マット

@Mattは公平に言えば、タイプは3.5になりました。これは、ごく最近のことです。特に、本番環境での使用が許可されています。
ジャレッド・スミス

回答:


53

サブタイプは、サブタイプはまた、典型的にはプログラム要素、サブルーチンまたは関数は、スーパータイプの要素上で動作するように書かれたことができることを意味し、代替のいくつかの概念により他のデータ型(スーパータイプ)に関連するデータ型であるタイプの多型の形態でありますサブタイプの要素を操作します。

場合SのサブタイプであるT、サブタイプ関係が頻繁に書き込まれS <: T、タイプのいずれかの用語があることを意味するためS、安全タイプの用語は文脈で使用することができるTと予想されます。サブタイピングの正確なセマンティクスは、特定のプログラミング言語で「どこで安全に使用されるか」の意味に大きく依存します。

サブクラス化とサブタイプ化を混同しないでください。一般に、サブタイピングはis-a関係を確立しますが、サブクラス化は実装を再利用し、必ずしも意味関係ではなく構文関係を確立します(継承は動作サブタイプを保証しません)。

これらの概念を区別するために、サブタイプ化はインターフェース継承とも呼ばれ、サブクラス化は実装継承またはコード継承と呼ばれます。

参照
サブタイプの
継承


1
非常によく言いました。C ++プログラマーはサブタイプの関係を型システムと通信するために純粋な仮想ベースクラスをしばしば使用するという質問の文脈で言及する価値があるかもしれません。もちろん、一般的なプログラミング手法が好まれます。
アルアンハダッド

6
「サブタイピングの正確なセマンティクスは、特定のプログラミング言語で「どこでコンテキストで安全に使用されるか」の意味に大きく依存します。」…そして、LSPは、「安全に」が意味するもののやや合理的なアイデアを定義し、この特定の形式の「安全」を可能にするためにそれらの詳細が満たさなければならない制約を教えます。
ヨルグWミットタグ

パイルに関するもう1つのサンプル:C ++で正しく理解できれば、public継承はサブタイプをprivate導入し、継承はサブクラスを導入します。
クエンティン

@Quentinパブリック継承はサブタイプとサブクラスの両方ですが、プライベートはまだサブクラスであり、サブタイプではありません。あなたはJavaインターフェースのような構造をサブクラス化せずにサブタイピング持つことができます
eques

27

ここで説明しているコンテキストでは、typeは基本的に一連の動作保証です。契約、もし必要になります。または、プロトコルである Smalltalkから用語を借用します

クラスはメソッドの束です。これは、一連の動作実装です。

サブタイピングは、プロトコルを改良する手段です。サブクラス化は、差分コードを再利用する手段です。つまり、動作の違いを記述するだけでコードを再利用します。

JavaまたはC♯を使用している場合は、すべての型を型にする必要があるというアドバイスに出会った可能性がありinterfaceます。実際、William Cookの「データ抽象化の理解、再訪」を読むと、これらの言語でオブジェクト指向を行うには、sを型としてのみ使用する必要があることがわかりinterfaceます。(また、面白い事実:Java interfaceは、Smalltalkから直接取得されるObjective-Cのプロトコルから直接scribbedしました。)

さて、そのコーディングのアドバイスに従って論理的な結論をたどって、s のみ interfaceが型であり、クラスとプリミティブがそうでないJavaのバージョンを想像するとinterface、別のものから継承するものはサブタイプ関係を作成しますが、class別のものから継承するものは単に差分コードを再利用するためのものですsuper

私の知る限り、コードの継承(実装の継承/サブクラス化)とコントラクトの継承(サブタイピング)を厳密に区別する主流の静的型付け言語はありません。JavaおよびC♯では、インターフェースの継承は純粋なサブタイプです(または、少なくともJava 8でデフォルトメソッドが導入されるまではC♯8でもありました)が、クラス継承実装の継承と同様にサブタイプです。私は厳密に区別実験静的に型付けされたオブジェクト指向のLISP方言、読ん覚えミックスイン含む動作)、構造体(状態を含む)、インタフェースれる(説明します動作)、およびクラス(1つ以上のmixinで0個以上の構造体を構成し、1つ以上のインターフェイスに準拠します)。インスタンス化できるのはクラスのみで、タイプとして使用できるのはインターフェースのみです。

Python、Ruby、ECMAScript、Smalltalkなどの動的に型指定されたOO言語では、通常、オブジェクトの型は、準拠するプロトコルのセットと考えられます。複数形に注意してください。オブジェクトは複数のタイプを持つことができ、タイプの各オブジェクトがタイプStringのオブジェクトでもあるという事実だけではありませんObject。(ところで:型について話すためにクラス名をどのように使用したかに注意してください?私はなんて愚かです!)オブジェクトは複数のプロトコルを実装できます。たとえば、Rubyでは、Arrays追加したり、インデックスを付けたり、繰り返し処理したり、比較したりできます。それは彼らが実装する4つの異なるプロトコルです!

現在、Rubyには型がありません。しかし、Ruby コミュニティには型があります!ただし、それらはプログラマの頭の中にしか存在しません。そしてドキュメント。たとえばeach、要素を1つずつ生成することによって呼び出されるメソッドに応答するオブジェクトは、列挙可能なオブジェクトと見なされます。そして、呼ばれるミックスインがある依存し、このプロトコルに。あなたのオブジェクトが正しく持っているのであれば、タイプ(唯一のプログラマの頭の中に存在している)、(継承)で混合させるミックスイン、そしてそれがうまく自由のためのクールな方法のすべての種類を取得、のような、、などオン。EnumerableEnumerablemapreducefilter

同様に、オブジェクトへの応答があった場合は<=>、それを実現するために考えられている同等のプロトコルを、それが中に混在させることができますComparableミックスインと同じようなものを取得し<<=><===between?、そしてclamp自由のために。ただし、これらすべてのメソッド自体を実装することもできますが、まったく継承することはできずComparable、依然として同等と見なされます。

良い例は、StringIO基本的にI / Oストリームを文字列で偽造するライブラリです。IOクラスと同じメソッドをすべて実装しますが、2つの間に継承関係はありません。それでも、a StringIOは使用できるすべての場所IOで使用できます。これは、ファイルまたは交換することができますユニットテスト、に非常に有用であるstdinとのStringIOあなたのプログラムにさらに変更を加えることなく。以来、StringIO同じプロトコルに準拠しIO、それらは異なるクラスであっても、同じタイプの両方であり、(どちらも延びることが自明以外の関係共有しないObjectいくつかの点での)。


言語が、プログラムがクラス型とそのクラスが実装であるインターフェースを同時に宣言できるようにし、実装が「コンストラクター」(インターフェースで指定されたクラスのコンストラクターにチェーンする)を指定できるようにすると便利です。参照がパブリックに共有されるオブジェクトのタイプの場合、好ましいパターンは、派生クラスの作成時にのみクラスタイプが使用されることです。ほとんどの参照は、インターフェイスタイプである必要があります。インターフェイスコンストラクターを指定できると、次のような状況で役立ちます。
supercat

...たとえば、コードは、インデックスによって特定の値のセットを読み取ることができるコレクションを必要としますが、実際にはそのタイプを気にしません。クラスとインターフェースを異なる種類のタイプとして認識する正当な理由はありますが、現在許可されている言語よりも密接に連携できるようにすべき多くの状況があります。
supercat

あなたが言及した実験的なLISP方言について、mixin、structs、interfaces、classsを正式に区別する参照情報やキーワードがありますか?
tel

@tel:いいえ、ごめんなさい。たぶん15〜20年前だったと思いますが、当時は私の興味がいたるところにありました。私はこれに出くわしたときに私が探していたものをあなたに伝えることはおそらくできませんでした。
ヨルグWミットタグ

Awww。それが、これらすべての答えの中で最も興味深い詳細でした。言語の実装内でこれらの概念の正式な分離が実際に可能であるという事実は、クラス/タイプの区別を明確にするのに本当に役立ちました。いずれにしても、自分でそのLISPを探しに行くと思います。雑誌の記事や本でそれについて読んだり、会話で聞いたことがあるのを覚えていますか?
tel

2

最初に型とクラスを区別してから、サブタイプとサブクラスの違いを調べるのがおそらく便利です。

この回答の残りの部分では、議論中の型は静的な型であると仮定します(通常、サブタイプは静的なコンテキストで発生するため)。

ほとんどの言語は少なくとも部分的にそれらを融合するので、タイプとクラスの違いを説明するのに役立つおもちゃの擬似コードを開発します(簡単に触れておく正当な理由のため)。

タイプから始めましょう。タイプは、コード内の式のラベルです。このラベルの値と、それが他のすべてのラベルの値と一致するかどうか(一部の型システム固有の定義の場合)は、プログラムを実行せずに外部プログラム(型チェッカー)によって決定できます。それがこれらのラベルを特別なものにし、独自の名前にふさわしいものにしている。

おもちゃの言語では、そのようなラベルの作成を許可する場合があります。

declare type Int
declare type String

その後、さまざまな値にこのタイプのものとしてラベルを付けることができます。

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
...

これらのステートメントにより、タイプチェッカーは次のようなステートメントを拒否できるようになりました。

0 is of type String

型システムの要件の1つが、すべての式が一意の型を持つことである場合。

これがいかに不格好で、無限の数の式タイプを割り当てる際に問題が発生する可能性があるのか​​はさておきましょう。後で戻ることができます。

一方、クラスは、グループ化されたメソッドとフィールドのコレクションです(プライベートまたはパブリックなどのアクセス修飾子が含まれる可能性があります)。

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

このクラスのインスタンスは、これらのメソッドとフィールドの既存の定義を作成または使用する機能を取得します。

クラスのすべてのインスタンスがそのタイプで自動的にラベル付けされるように、クラスをタイプに関連付けることを選択できます。

associate StringClass with String

ただし、すべてのタイプにクラスを関連付ける必要があるわけではありません。

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

おもちゃの言語では、すべてのクラスに型があるわけではなく、特にすべての式に型があるわけではない場合も考えられます。一部の式に型があり、一部の型にはない場合、型システムの一貫性ルールがどのようになるかを想像するのは少し難しいです(不可能ではありません)。

さらに、おもちゃの言語では、これらの関連付けは一意である必要はありません。2つのクラスを同じタイプに関連付けることができます。

associate MyCustomStringClass with String

ここで、式チェッカーが式の値を追跡する必要がないことを念頭に置いてください(ほとんどの場合、追跡することはできないか、不可能です)。知っているのは、あなたが言ったラベルだけです。以前の注意点として、0 is of type Stringタイプチェッカーはステートメントを拒否することができました。これは、式に一意のタイプが必要であり、既に0他の式にラベルを付けているという人為的に作成されたタイプルールのためです の値に関する特別な知識はありませんでした0

では、サブタイピングについてはどうでしょうか?よくサブタイプとは、タイプチェックの一般的なルールの名前で、他のルールを緩和します。つまりA is subtype of B、タイプチェッカーがのラベルを要求するすべての場所でBA

たとえば、以前の番号ではなく、番号に対して次の操作を実行できます。

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
...

サブクラス化は、以前に宣言されたメソッドとフィールドを再利用できるようにする新しいクラスを宣言するための略記法です。

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

私たちは、仲間のインスタンスに持っていないExtendedStringClassString我々が行ったようにStringClass、それは全く新しいクラスのすべての後に、以来、私たちはずっととして記述する必要はありませんでした。これによりExtendedStringClass、タイプStringチェッカーの観点からは互換性のないタイプを指定できます。

同様に、まったく新しいクラスを作成することもできましNewClassた。

associate NewClass with String

のすべてのインスタンスは、タイプチェッカーの観点からStringClass置き換えることができNewClassます。

そのため、理論上、サブタイピングとサブクラス化はまったく異なります。しかし、私が知っている言語には、実際にこのように型とクラスを持っているものはありません。私たちの言語を減らし始め、いくつかの決定の背後にある理論的根拠を説明しましょう。

まず、理論的にはまったく異なるクラスに同じ型を与えたり、クラスにクラスのインスタンスではない値と同じ型を与えたりすることもできますが、これはタイプチェッカーの有用性を著しく阻害します。タイプチェッカーは事実上、式内で呼び出しているメソッドまたはフィールドがその値に実際に存在するかどうかをチェックする機能を奪われます。これはおそらく、タイプチェッカー。結局のところ、そのStringラベルの下に実際にある値が何であるかを誰が知っていますか。たとえば、concatenateメソッドがまったくないものかもしれません。

それでは、すべてのクラスがそのクラスと同じ名前の新しい型を自動的に生成し、associateその型を持つインスタンスをsに規定することにしましょう。私たちは取り除くことができますそれassociateだけでなく、間に別の名前StringClassString

同じ理由で、おそらく一方が他方のサブクラスである2つのクラスのタイプ間のサブタイプ関係を自動的に確立する必要があります。すべてのサブクラスには、親クラスが持つすべてのメソッドとフィールドがあることが保証されていますが、その逆は当てはまりません。したがって、サブクラスは親クラスの型が必要なときはいつでも渡すことができますが、サブクラスの型が必要な場合は親クラスの型を拒否する必要があります。

これを、すべてのユーザー定義値がクラスのインスタンスでなければならないという規定と組み合わせると、is subclass of二重の義務を引き、を取り除くことができますis subtype of

そしてこれにより、静的に型付けされた一般的なOO言語のほとんどが共有する特性に到達できます。「プリミティブ」型(例えばセットがありintfloatどのクラスに関連付けられていないと、ユーザー定義されていないされているなど)。次に、自動的に同じ名前の型を持ち、サブクラス化でサブクラス化を識別するすべてのユーザー定義クラスがあります。

最後に、値とは別に型を宣言することの不格好さについて説明します。ほとんどの言語は2つの作成を統合するため、型宣言は、その型で自動的にラベル付けされるまったく新しい値を生成するための宣言でもあります。たとえば、クラス宣言は通常、型とその型の値をインスタンス化する方法の両方を作成します。これにより、いくつかの不格好さが解消されます。また、コンストラクターが存在する場合、1回のストロークで型を持つラベルを無限に作成できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.