[更新、新しい洞察、今まで何かが欠けていると感じた]
以前の回答について:
- 配列は、他の型と同様に共変です。「object[] foo = new string[5];」のようなものを実装できます。共分散があるので、それは理由ではありません。
- 設計を再考しない理由はおそらく互換性ですが、これも正解ではないと主張します。
ただし、私が考えることができる他の理由は、配列がメモリ内の要素の線形セットの「基本型」であるためです。私は Array<T> の使用を考えていましたが、なぜ T がオブジェクトなのか、なぜこの「オブジェクト」が存在するのか疑問に思うかもしれません。このシナリオでは、T[] は、Array と共変である Array<T> の別の構文と考えられるものです。実際にはタイプが異なるため、2 つのケースは類似していると考えます。
基本的なオブジェクトと基本的な配列の両方が OO 言語の要件ではないことに注意してください。C++ はこの完璧な例です。これらの基本構造の基本型がないことの警告は、リフレクションを使用して配列またはオブジェクトを操作できないことです。オブジェクトの場合、「オブジェクト」を自然に感じさせる Foo のものを作成することに慣れています。実際には、配列基底クラスを持たないことで、Foo を実行することも同様に不可能になります。これはそれほど頻繁に使用されるわけではありませんが、パラダイムにとって同様に重要です。
したがって、C# に Array の基本型がなくても、豊富なランタイム型 (特にリフレクション) を使用することは、IMO では不可能です。
それでは、詳細を...
配列はどこで使用され、なぜ配列なのか
配列のように基本的なものの基本型を持つことは、多くのことに使用され、正当な理由があります。
T[]
そうですね、私たちは人々が を使用するのと同じように を使用することをすでに知っていましたList<T>
。どちらも共通のインターフェイス セットを実装しています。正確には、 IList<T>
、ICollection<T>
、IEnumerable<T>
、IList
、ICollection
およびIEnumerable
です。
これを知っていれば、簡単に配列を作成できます。私たちは皆、これが真実であることを知っており、それはエキサイティングではないので、先に進みます...
リストを掘り下げると、最終的には配列になります-正確には、T []配列です。
では、なぜですか?ポインター構造 (LinkedList) を使用することもできましたが、同じではありません。リストはメモリの連続ブロックであり、メモリの連続ブロックであることで速度が向上します。これには多くの理由がありますが、簡単に言えば、連続メモリを処理することは、メモリを処理する最速の方法です。CPU には、それを高速化するための命令さえあります。
注意深い読者は、これには配列が必要ではなく、IL が理解して処理できる型 'T' の要素の連続ブロックが必要であるという事実を指摘するかもしれません。つまり、IL で同じことを行うために使用できる別の型があることを確認する限り、ここで Array 型を取り除くことができます。
値型とクラス型があることに注意してください。可能な限り最高のパフォーマンスを維持するには、それらをそのままブロックに格納する必要があります...しかし、マーシャリングの場合、それは単に要件です。
マーシャリングでは、すべての言語が通信に同意する基本型を使用します。これらの基本型は、byte、int、float、pointer...、array などです。最も顕著なのは、次のような C/C++ での配列の使用方法です。
for (Foo *foo = beginArray; foo != endArray; ++foo)
{
// use *foo -> which is the element in the array of Foo
}
基本的に、これは配列の先頭にポインターを設定し、配列の末尾に到達するまで (sizeof(Foo) バイトで) ポインターをインクリメントします。要素は *foo で取得されます。これは、ポインタ 'foo' が指している要素を取得します。
値型と参照型があることにもう一度注意してください。オブジェクトとしてボックス化されたすべてを単純に格納する MyArray は本当に必要ありません。MyArray の実装は、非常にトリッキーになりました。
注意深い読者の中には、実際にはここで配列が必要ないという事実を指摘する人もいますが、これは事実です。Foo 型の要素の連続ブロックが必要です。値型の場合は、値型 (のバイト表現) としてブロックに格納する必要があります。
もっと... 多次元性はどうですか?どうやら、ルールはそれほど白黒ではありません。なぜなら、突然すべての基本クラスがなくなったからです。
int[,] foo2 = new int[2, 3];
foreach (var type in foo2.GetType().GetInterfaces())
{
Console.WriteLine("{0}", type.ToString());
}
強い型は窓の外に出たばかりで、コレクション型IList
、ICollection
およびになりIEnumerable
ます。ねえ、どうやってサイズを取得するのですか?Array 基本クラスを使用する場合は、次のように使用できます。
Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));
... しかし、 のような代替案を見ると、IList
同等のものはありません。これをどのように解決しますか?ここに導入する必要がありIList<int, int>
ますか?基本型はただのint
. どうIMultiDimentionalList<int>
ですか?これを実行して、現在 Array にあるメソッドで埋めることができます。
配列を再割り当てするための特別な呼び出しがあることに気付きましたか? これはすべてメモリ管理に関係しています。配列は非常に低レベルであるため、成長や縮小が何であるかを理解していません。C では、これに 'malloc' と 'realloc' を使用します。また、直接割り当てるすべてのものに対して正確に固定サイズを使用することが重要である理由を理解するには、独自の 'malloc' と 'realloc' を実装する必要があります。
それを見ると、「固定」サイズで割り当てられるのは、配列、すべての基本的な値の型、ポインター、およびクラスの 2 つだけです。基本的な型の扱いが異なるように、明らかに配列の扱いも異なります。
型安全性に関する補足事項
そもそも、なぜこれらすべての「アクセス ポイント」インターフェイスが必要なのでしょうか。
どのような場合でも、ベスト プラクティスは、タイプ セーフなアクセス ポイントをユーザーに提供することです。これは、次のようなコードを比較することで説明できます。
array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...
次のようにコーディングします。
((Array)someArray).GetLength(0); // do!
型安全性により、プログラミング時にずさんになることができます。正しく使用すると、実行時にエラーを検出するのではなく、作成したエラーをコンパイラが検出します。これがどれほど重要かはいくら強調してもしすぎることはありません。結局のところ、コードはテスト ケースでまったく呼び出されない可能性がありますが、コンパイラは常にコードを評価します。
すべてを一緒に入れて
では、まとめてみましょう。欲しいもの:
- 厳密に型指定されたデータ ブロック
- データが継続的に保存されている
- IL のサポートにより、高速な処理を可能にするクールな CPU 命令を確実に使用できるようになります
- すべての機能を公開する共通インターフェース
- 型安全性
- 多次元性
- 値型を値型として保存したい
- そして、そこにある他の言語と同じマーシャリング構造
- メモリ割り当てが容易になる固定サイズ
これは、コレクションにとってかなりの低レベルの要件です...特定の方法でメモリを編成する必要があり、IL / CPUへの変換も必要です...基本的なタイプと見なされるのには十分な理由があると思います.