なぜこれは問題なく、ほとんどが期待されているのですか?
abstract type Shape
{
abstract number Area();
}
concrete type Triangle : Shape
{
concrete number Area()
{
//...
}
}
...これは大丈夫ではなく、誰も文句を言わない:
concrete type Name : string
{
}
concrete type Index : int
{
}
concrete type Quantity : int
{
}
私の動機は、コンパイル時の正当性検証のための型システムの使用を最大化することです。
PS:はい、読みました this で、ラッピングはハックな回避策です。
Java and C#?
これらの言語では、プリミティブ(int
など)は基本的にパフォーマンスの妥協点です。オブジェクトのすべての機能をサポートしているわけではありませんが、高速でオーバーヘッドが少なくなっています。
オブジェクトが継承をサポートするには、各インスタンスが実行時に、それがどのクラスのインスタンスであるかを「知る」必要があります。そうでない場合、オーバーライドされたメソッドは実行時に解決できません。オブジェクトの場合、これはインスタンスデータがクラスオブジェクトへのポインタと共にメモリに格納されることを意味します。そのような情報もプリミティブ値とともに保存する必要がある場合、メモリ要件は膨らみます。 16ビット整数値は、その値に16ビットが必要であり、さらにそのクラスへのポインタに32または64ビットのメモリが必要です。
メモリのオーバーヘッドは別として、算術演算子などのプリミティブでの一般的な操作をオーバーライドできることも期待できます。サブタイプなしで、+
のような演算子は、単純なマシンコード命令にコンパイルできます。それをオーバーライドできる場合は、実行時にメソッドを解決する必要がありますmuchよりコストのかかる操作。 (C#が演算子のオーバーロードをサポートしていることをご存じかもしれませんが、これは同じではありません。演算子のオーバーロードはコンパイル時に解決されるため、デフォルトの実行時のペナルティはありません。)
文字列はプリミティブではありませんが、メモリ内での表現方法は「特別」です。たとえば、それらは「インターン」されます。つまり、等しい2つの文字列リテラルを同じ参照に最適化できるということです。文字列インスタンスもクラスを追跡する必要がある場合、これは不可能です(または少なくともかなり効果的ではありません)。
あなたが説明することは確かに役に立ちますが、それをサポートするには、プリミティブと文字列を継承を利用しない場合でも、プリミティブと文字列を使用するたびにパフォーマンスのオーバーヘッドが必要になります。
Smalltalk does(多分)言語は整数のサブクラス化を許可します。しかし、Javaが設計されたとき、Smalltalkは遅すぎると見なされ、すべてをオブジェクトにするオーバーヘッドが主な理由の1つと見なされました。Javaはいくつかを犠牲にしましたより良いパフォーマンスを得るための優雅さと概念的な純粋さ。
一部の言語が提案するのはサブクラス化ではなく、サブタイピングです。たとえば、Adaではderived型またはsubtypesを作成できます。 Adaプログラミング/型システム セクションは、すべての詳細を理解するために読む価値があります。ほとんどの場合に必要な値の範囲を制限できます。
type Angle is range -10 .. 10;
type Hours is range 0 .. 23;
明示的に変換すれば、両方の型を整数として使用できます。また、範囲が構造的に同等である場合でも(タイプは名前でチェックされます)、使用できないことはできません。
type Reference is Integer;
type Count is Integer;
上記の型は、値の範囲が同じであっても互換性がありません。
(しかし、Unchecked_Conversionを使用できます。私が言ったことを他人に教えないでください)
これはX/Yの質問になると思います。質問からの特徴...
私の動機は、コンパイル時の正当性検証のための型システムの使用を最大化することです。
...そして あなたのコメント から:
暗黙的に1つを別のものに置き換えることができるようにしたくありません。
何かが足りない場合はすみませんが...これらがあなたの目的である場合、なぜ地球上で継承について話しているのですか?暗黙的な代用可能性は...まるで...全部です。リスコフの代替原則を知っていますか?
あなたが望んでいるように見えるのは、実際には「強いtypedef」の概念です。範囲と表現の観点からint
butは、int
を期待するコンテキストに置き換えることはできません。逆も同様です。この用語に関する情報を検索することをお勧めします。選択した言語でそれを呼び出すものは何でもかまいません。繰り返しますが、これは文字通り継承とは正反対です。
そして、X/Yの答えが気に入らない人のために、私はタイトルがLSPに関してまだ答えられるかもしれないと思います。プリミティブ型はプリミティブです。これは、非常に単純なことを行うためですそして、それだけです。それらが継承されることを許可し、したがってそれらの可能な影響を無限にすることは、最悪の場合でも最高で致命的なLSP違反で大きな驚きにつながります。 Thales Pereiraが楽観的に仮定する場合、 これ 驚異的なコメントを引用してもかまいません。
誰かがIntから継承できた場合、「int x = y + 2」(Yは派生クラス)のような無害なコードがデータベースにログを書き込み、URLを開き、どういうわけかエルビスを復活させます。プリミティブ型は安全で、多かれ少なかれ保証された、明確に定義された動作を備えているはずです。
誰かが原始的なタイプを正気な言語で見た場合、彼らは当然のことながら、それが常に1つの小さなことを、驚くことなく、非常にうまくいくと思います。プリミティブ型には、継承されるかどうかを通知するクラス宣言がなく、メソッドがオーバーライドされます。もしそうなら、それは本当に驚くべきことです(そして完全に後方互換性を壊しますが、私は 'XがYで設計されなかった理由'に対する後方回答であることを知っています)。
...ただし、 Mooing Duckが指摘したように の応答として、演算子のオーバーロードを許可する言語では、ユーザーが本当に望んでいる場合、ユーザーが同程度または同等の範囲で混乱する可能性があるため、この最後の引数が成立するかどうかは疑問です。そして、私は今、他の人々のコメントを要約するのをやめるつもりです。
主流の強い静的OOP言語では、サブタイピングは主に型を拡張し、型の現在のメソッドをオーバーライドする方法と見なされます。
そのために、「オブジェクト」にはそのタイプへのポインタが含まれています。これはオーバーヘッドです。Shape
インスタンスを使用するメソッドのコードは、正しいArea()
メソッドを呼び出す前に、そのインスタンスの型情報にアクセスする必要があります。
プリミティブは、単一の機械語命令に変換でき、タイプ情報をそれらと一緒に運ばない操作のみを許可する傾向があります。 誰かがサブクラス化できるように整数を遅くすると、それが主流になった言語を停止するのに十分な魅力がなくなりました。
だから答え:
主流の強い静的OOP言語がプリミティブの継承を妨げるのはなぜですか?
です:
ただし、 'type'以外の変数のプロパティに基づいて静的チェックを許可する言語を取得し始めています。たとえば、F#には「dimension」と「unit」があるため、たとえば領域に長さを追加することはできません。 。
型の動作を変更(または交換)せずに静的型チェックを支援する「ユーザー定義型」を許可する言語もあります。コアダンプの答えを見てください。
8アプリケーションの設計で非常に望ましいと見なされることが多い仮想ディスパッチでの継承を可能にするために、ランタイムタイプ情報が必要です。すべてのオブジェクトについて、オブジェクトのタイプに関するいくつかのデータを保存する必要があります。プリミティブには、定義ごとに、この情報が不足しています。
2つの(管理、VMで実行)メインストリームOOPプリミティブを備えた言語:C#とJavaがあります。他の多くの言語しないそもそもプリミティブがあります。または、それらを許可/使用するための同様の推論を使用します。
プリミティブはパフォーマンスの妥協点です。オブジェクトごとに、オブジェクトヘッダー(Javaでは64ビットVMで通常2 * 8バイト)、フィールド、および最終的なパディング(ホットスポットでは、すべてのオブジェクトが倍数のバイト数を占める)のためのスペースが必要です。 8)。したがって、int
asオブジェクトは、(Javaで)4バイトだけではなく、少なくとも24バイトのメモリを保持する必要があります。
したがって、パフォーマンスを向上させるためにプリミティブ型が追加されました。それらは多くのことをより簡単にします。両方がint
のサブタイプである場合、a + b
はどういう意味ですか?正しい追加を選択するために、ある種の非同調を追加する必要があります。これは仮想ディスパッチを意味します。加算に非常に単純なオペコードを使用する機能があると、はるかに高速になり、コンパイル時の最適化が可能になります。
String
は別のケースです。 JavaとC#では、String
はオブジェクトです。ただし、C#ではその封印されており、Javaその最後です。 JavaおよびC#標準ライブラリではString
sが不変である必要があり、それらをサブクラス化するとこの不変性が壊れます。
Javaの場合、VMは、インターン文字列をインターンして「プール」できるため、それらを「プール」できるため、パフォーマンスが向上します。これは、ストリングが真に不変である場合にのみ機能します。
さらに、1つまれにはプリミティブ型をサブクラス化する必要があります。プリミティブをサブクラス化できない限り、数学がそれらについて教えてくれるたくさんのきちんとしたことがたくさんあります。たとえば、加算は可換的かつ連想的であることを確認できます。これは、整数の数学的定義からわかることです。さらに、多くの場合、誘導を介してループ上で不変条件を簡単に作成できます。 int
のサブクラス化を許可すると、特定のプロパティが保持されることが保証されなくなるため、数学で得られるツールが失われます。したがって、プリミティブ型をサブクラス化できる能力notは実際には良いことだと思います。誰かが破ることができるより少ないものに加えて、コンパイラーはしばしば、彼が特定の最適化を行うことが許可されていることを証明することができます。
ここで何かを見落としているかどうかはわかりませんが、答えはかなり簡単です。
2つの強力な静的OOP言語がさえ持っているプリミティブ、AFAIK:JavaとC++(実際、後者についてはよくわかりません。C++についてはあまり知りません。検索時に見つけたものは混乱を招きました。)
C++では、プリミティブは基本的にCから継承された(しゃれた意図のある)レガシーです。したがって、Cにはオブジェクトシステムも継承もないため、プリミティブはオブジェクトシステム(および継承)に参加しません。
Javaでは、プリミティブはパフォーマンスを改善するための誤った試みの結果です。プリミティブもシステム内の唯一の値型です。実際、Javaで値型を記述することは不可能であり、オブジェクトを値型にすることは不可能です。したがって、プリミティブがオブジェクトシステムに参加しないという事実とは別に、「継承」の概念は、ifあなたはそれらから継承することができ、あなたは「価値観」を維持することができません。これは、例えばとは異なります。 C♯whichdoesには値タイプ(struct
s)がありますが、それでもオブジェクトです。
もう1つは、継承できないことも、実際にはプリミティブに固有ではないということです。 C♯では、struct
sはSystem.Object
から暗黙的に継承し、interface
sを実装できますが、class
esまたはstruct
sから継承も継承もできません。 。また、sealed
class
esは継承できません。 Javaでは、final
class
esは継承できません。
tl; dr:
主流の強い静的OOP言語がプリミティブの継承を妨げるのはなぜですか?
final
またはsealed
in Java orC♯、struct
s in C♯、case class
es in Scala)「Effective Java」のJoshua Blochは、継承を明示的に設計するか、継承を禁止することを推奨しています。プリミティブクラスは、不変であるように設計されており、継承を許可するとサブクラスでそれを変更する可能性があるため、継承用に設計されていません Liskov原理 を破壊し、多くのバグの原因となります。
とにかく、なぜ this はハックな回避策ですか?継承よりも構成を優先する必要があります。理由があなたの要点よりもパフォーマンスであり、あなたの質問に対する答えがすべての機能をJavaに配置することは不可能であることです。機能の追加のすべての異なる側面を分析するには時間がかかるためです。 。たとえば、Javaには1.5より前のジェネリックはありませんでした。
忍耐力がたくさんある場合は、幸運です。値クラスをJavaに追加する プラン があり、これにより、値クラスを作成して、パフォーマンスが向上すると同時に、柔軟性が向上します。
抽象レベルでは、設計している言語で必要なものをすべて含めることができます。
実装レベルでは、それらのいくつかは実装が簡単で、いくつかは複雑になり、いくつかは高速にでき、いくつかは低速にバインドされるなど、不可避です。これを説明するために、設計者はしばしば難しい決定と妥協をしなければなりません。
実装レベルで、変数にアクセスするために考え出された最も速い方法の1つは、そのアドレスを見つけてそのアドレスの内容をロードすることです。ほとんどのCPUには、アドレスからデータをロードするための特定の命令があり、それらの命令は通常、ロードする必要があるバイト数(1、2、4、8など)とロードするデータを配置する場所(単一レジスタ、レジスタ)を知る必要がありますペア、拡張レジスタ、その他のメモリなど)。変数のサイズを知ることにより、コンパイラーはその変数の使用のためにどの命令を発行するかを正確に知ることができます。変数のサイズがわからない場合、コンパイラーはより複雑でおそらくより遅いものに頼る必要があります。
抽象レベルでは、サブタイピングのポイントは、同等またはより一般的なタイプが期待される1つのタイプのインスタンスを使用できるようにすることです。言い換えると、これが何であるかを事前に知らなくても、特定のタイプのオブジェクトまたはより派生したものを期待するコードを書くことができます。明らかに、派生型が増えるとデータメンバーも追加されるため、派生型は必ずしも基本型と同じメモリ要件を持つ必要はありません。
実装レベルでは、事前に決定されたサイズの変数が未知のサイズのインスタンスを保持し、通常は効率的に呼び出す方法でアクセスされる簡単な方法はありません。ただし、少し移動して変数を使用してオブジェクトを格納するのではなく、オブジェクトを識別してそのオブジェクトを別の場所に格納する方法があります。その方法は参照(メモリアドレスなど)です-情報を介してオブジェクトを見つけることができる限り、変数はある種の固定サイズの情報のみを保持する必要があることを保証する追加の間接レベルです。これを実現するには、アドレス(固定サイズ)をロードするだけでよく、その後、オブジェクトのオフセットにデータが不明であっても、有効であることがわかっているオブジェクトのオフセットを使用して通常どおり作業できます。これにアクセスできるのは、ストレージへのアクセス時にストレージ要件を気にする必要がないためです。
抽象レベルでは、このメソッドを使用すると、([aへの参照])string
をobject
にする情報を失うことなく、string
変数に格納できます。すべてのタイプがこのように機能することは問題ありませんが、多くの点でエレガントであると言うこともできます。
それでも、実装レベルでは、追加の間接レベルにはより多くの命令が含まれ、ほとんどのアーキテクチャではオブジェクトへの各アクセスが多少遅くなります。余分なレベルの間接参照(参照)を持たない一般的に使用される型を言語に含めると、コンパイラーがプログラムからより多くのパフォーマンスを引き出せるようになります。しかし、そのレベルの間接参照を削除することにより、コンパイラーはメモリー安全な方法でサブタイプすることを許可できなくなります。これは、型にさらにデータメンバーを追加し、より一般的な型に割り当てると、ターゲット変数に割り当てられたスペースに収まらない余分なデータメンバーが切り取られるためです。
プリミティブが期待される場所では特別なタイプを置き換えることができないため、通常、継承は必要なセマンティクスではありません。あなたの例から借りるために、Quantity + Index
は意味的に意味をなさないため、継承関係は間違った関係です。
ただし、いくつかの言語には、value typeの概念があり、これは、説明している種類の関係を表します。 Scala はその一例です。値タイプは、基本表現としてプリミティブを使用しますが、外部ではクラスIDと操作が異なります。これはプリミティブ型を拡張する効果がありますが、それは継承関係ではなく構成のほうが多くなります。
クラスが抽象(メタファー:穴のあるボックス)の場合、「穴を埋める」ことは可能です(使用可能な何かが必要です!)。これが、抽象クラスをサブクラス化する理由です。
クラスが具象(メタファー:ボックスがいっぱい)の場合、既存のものを変更しても問題ありません。ボックスの中に何かを追加する余地はないので、具象クラスをサブクラス化すべきではありません。
プリミティブは、設計による具体的なクラスです。それらは、よく知られていて完全に明確なもの(私は何か抽象的なもののプリミティブ型を見たことがない、そうでなければそれはもうプリミティブではない)を表し、システム全体で広く使用されています。プリミティブ型をサブクラス化し、プリミティブの設計された動作に依存する他の人に独自の実装を提供できるようにすると、多くの副作用と大きな損害を引き起こす可能性があります。