web-dev-qa-db-ja.com

配列がジェネリック型ではないのはなぜですか?

Arrayが宣言されています:

public abstract class Array
    : ICloneable, IList, ICollection, IEnumerable {

私はなぜそうではないのだろうと思っています:

public partial class Array<T>
    : ICloneable, IList<T>, ICollection<T>, IEnumerable<T> {
  1. ジェネリック型として宣言された場合、問題は何になりますか?

  2. ジェネリック型の場合、非ジェネリック型がまだ必要ですか、それともArray<T>から派生できますか?といった

    public partial class Array: Array<object> { 
    
53
Ken Kin

歴史

配列がジェネリック型になった場合、どのような問題が発生しますか?

C#1.0に戻って、彼らは主にJavaから配列の概念をコピーしました。当時ジェネリックは存在しませんでしたが、作成者はスマートだと思い、Java配列が持つ壊れた共変配列のセマンティクスをコピーしました。エラー(ただし、代わりに実行時エラー):

Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths;            // Covariant conversion
animals[1] = new Giraffe();             // Run-time exception

C#2.0ではジェネリックが導入されましたが、共変/反変のジェネリック型はありません。配列がジェネリックにされた場合、(壊れていたとしても)以前にできることであるMammoth[]Animal[]にキャストできませんでした。したがって、配列を汎用化すると、コードが壊れてしまいますa lot

C#4.0でのみ、インターフェイスの共変/反変のジェネリック型が導入されました。これにより、壊れた配列共分散を完全に修正することが可能になりました。しかし、繰り返しますが、これは既存のコードの多くを壊してしまいます。

Array<Mammoth> mammoths = new Array<Mammoth>(10);
Array<Animal> animals = mammoths;           // Not allowed.
IEnumerable<Animals> animals = mammoths;    // Covariant conversion

配列は汎用インターフェースを実装します

なぜ配列は一般的なIList<T>ICollection<T>およびIEnumerable<T>インターフェイスを実装しないのですか?

すべての配列T[]doesランタイムトリックのおかげで、IEnumerable<T>ICollection<T>、およびIList<T>が自動的に実装されます。1 Arrayクラスのドキュメント から:

1次元配列は、IList<T>ICollection<T>IEnumerable<T>IReadOnlyList<T>、およびIReadOnlyCollection<T>ジェネリックインターフェイスを実装します。実装は実行時に配列に提供されるため、汎用インターフェイスはArrayクラスの宣言構文に表示されません。


配列で実装されたインターフェイスのすべてのメンバーを使用できますか?

いいえ。ドキュメントは次のコメントに続きます。

これらのインターフェイスの1つに配列をキャストするときに注意すべき重要なことは、要素を追加、挿入、または削除するメンバーがNotSupportedExceptionをスローすることです。

(たとえば)ICollection<T>にはAddメソッドがありますが、配列には何も追加できないためです。例外をスローします。これは、.NET Frameworkの初期設計エラーの別の例であり、実行時に例外がスローされます。

ICollection<Mammoth> collection = new Mammoth[10];  // Cast to interface type
collection.Add(new Mammoth());                      // Run-time exception

また、ICollection<T>は共変ではないため(明らかな理由により)、これを行うことはできません。

ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths;     // Not allowed

もちろん、現在、共変 IReadOnlyCollection<T> interface があります。これは、内部の配列によっても実装されます。1、ただしCountのみが含まれているため、使用が制限されています。


基本クラスArray

配列がジェネリックである場合、非ジェネリックArrayクラスが必要ですか?

初期の頃はそうでした。すべての配列は、非汎用 IListICollection および IEnumerable インターフェイスを実装します基本クラスArray。これは、すべての配列に特定のメソッドとインターフェイスを提供する唯一の合理的な方法であり、Array基本クラスの主な用途です。列挙型にも同じ選択肢があります。それらは値型ですが、Enumからメンバーを継承します。 MulticastDelegateを継承するデリゲート。

ジェネリックがサポートされているので、非ジェネリックベースクラスArrayを削除できますか?

はい、すべての配列で共有されているメソッドとインターフェイスは、汎用Array<T>クラスが存在する場合に定義できます。そして、例えば、Copy<T>(T[] source, T[] destination)の代わりにCopy(Array source, Array destination)を書くことで、何らかのタイプセーフティの利点を追加できます。

ただし、オブジェクト指向プログラミングの観点からは、タイプに関係なくany配列を参照するために使用できる一般的な非ジェネリックベースクラスArrayがあると便利です。その要素。 IEnumerable<T>IEnumerable(一部のLINQメソッドでまだ使用されている)から継承する方法と同じです。

Array基本クラスはArray<object>から派生できますか?

いいえ、それは循環依存関係を作成します:Array<T> : Array : Array<object> : Array : ...。また、これはanyオブジェクトを配列に格納できることを意味します(結局、すべての配列は最終的に_Array<object>型から継承します)。


未来

既存のコードにあまり影響を与えずに、新しい汎用配列型Array<T>を追加できますか?

いいえ。構文を適合させることはできましたが、既存の配列共分散は使用できませんでした。

配列は、.NETの特別な型です。 Common Intermediate Languageでの独自の指示もあります。 .NETとC#のデザイナーがこの道を進むことに決めた場合、T[]Array<T>構文構文シュガーを(T?Nullable<T>の構文シュガーと同じように)作成し、特別な命令と配列を連続して割り当てるサポートを使用できますメモリ。

ただし、Mammoth[]の配列をその基本型Animal[]のいずれかにキャストする機能は失われます。これは、List<Mammoth>List<Animal>にキャストできない方法と同様です。しかし、配列の共分散はとにかく壊れており、より良い選択肢があります。

配列の共分散の代替案

すべての配列はIList<T>を実装します。 IList<T>インターフェイスが適切な共変インターフェイスになっている場合、任意の配列Array<Mammoth>(またはそれに関するリスト)をIList<Animal>にキャストできます。ただし、これにはIList<T>インターフェイスを書き換えて、基になる配列を変更する可能性のあるすべてのメソッドを削除する必要があります。

interface IList<out T> : ICollection<T>
{
    T this[int index] { get; }
    int IndexOf(object value);
}

interface ICollection<out T> : IEnumerable<T>
{
    int Count { get; }
    bool Contains(object value);
}

(入力位置のパラメーターのタイプは、共分散を破壊するため、Tにはできません。ただし、objectContainsには、IndexOfで十分です。不正な型のオブジェクトが渡されたときにfalseを返すだけです。これらのインターフェイスを実装するコレクションは、独自の汎用IndexOf(T value)およびContains(T value)を提供できます。)

次に、これを行うことができます:

Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths;    // Covariant conversion

ランタイムは、配列の要素の値を設定するときに、割り当てられた値が配列要素の実際の型と型互換性があるかどうかをチェックする必要がないため、わずかなパフォーマンスの改善さえあります。


それで私の刺し

上記の実際の共変のArray<T>およびIList<T>インターフェイスと組み合わせて、C#および.NETで実装された場合に、そのようなICollection<T>型がどのように機能するかを確認しました。また、不変のIMutableList<T>およびIMutableCollection<T>インターフェイスを追加して、新しいIList<T>およびICollection<T>インターフェイスにない突然変異メソッドを提供しました。

その周りにシンプルなコレクションライブラリを構築しました。 BitBucketからソースコードとコンパイル済みバイナリをダウンロード にするか、NuGetパッケージをインストールします。

M42.Collections –組み込みの.NETコレクションよりも多くの機能、機能、使いやすさを備えた専用コレクションクラス。


1).Net 4.5のT[]配列は、その基本クラスArrayを介して実装されます: ICloneableIList 、- ICollectionIEnumerableIStructuralComparableIStructuralEquatable ;そして、ランタイムを通して静かに: IList<T>ICollection<T>IEnumerable<T>IReadOnlyList<T> 、および IReadOnlyCollection<T> =。

121

[更新、新しい洞察、今まで何かが足りないと感じていた]

以前の回答について:

  • 配列は他の型と同様に共変です。 'object [] foo = new string [5];'のようなものを実装できます。共分散があるので、それは理由ではありません。
  • 互換性はおそらくデザインを再検討しない理由ですが、これも正しい答えではないと思います。

しかし、私が考えることができるもう1つの理由は、配列がメモリ内の要素の線形セットの「基本型」であるためです。私はArray <T>の使用について考えてきましたが、Tがオブジェクトである理由と、この「オブジェクト」が存在する理由を疑問に思うかもしれません。このシナリオでは、T []はArrayと共変であるArray <T>の別の構文と考えるものです。実際にはタイプが異なるため、2つのケースは似ていると考えます。

基本オブジェクトと基本配列の両方がOO言語の要件ではないことに注意してください。C++はこのための完璧な例です。これらの基本構成に対して基本型がないという警告はありませんリフレクションを使用して配列またはオブジェクトを操作できます。オブジェクトの場合、「オブジェクト」を自然に感じるFooのオブジェクトを作成することに慣れています。実際には、配列の基本クラスがないと、Foo頻繁に使用されますが、パラダイムにとっても同様に重要です。

したがって、Arrayベース型なしでC#を使用し、豊富なランタイム型(特にリフレクション)を使用することはできません。

詳細については...

使用される配列はどこで、なぜ配列なのか

配列と同じくらい基本的なものの基本型を持つことは、多くのことと正当な理由で使用されます:

  • 単純な配列

まあ、人々はT[]を使用するのと同じように、List<T>を使用することを既に知っていました。正確には、両方のインターフェイスの共通セットを実装します:IList<T>ICollection<T>IEnumerable<T>IListICollectionおよび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());
}

強い型はウィンドウの外に出たばかりで、コレクション型IListICollection、および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」を実装して、all直接割り当てるもの。

見てみると、「固定」サイズで割り当てられるものは、配列、すべての基本的な値の型、ポインター、クラスの2つだけです。どうやら、基本型を異なる方法で処理するように、配列を異なる方法で処理しているようです。

型安全性に関する補足

そもそもなぜこれらすべての「アクセスポイント」インターフェースが必要なのでしょうか?

すべての場合のベストプラクティスは、ユーザーにタイプセーフなアクセスポイントを提供することです。これは、次のようなコードを比較することで説明できます。

array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...

このようなコードに:

((Array)someArray).GetLength(0); // do!

タイプセーフティを使用すると、プログラミング時にずさんなことができます。正しく使用すると、コンパイラは実行時に検出するのではなく、作成したエラーを検出します。これがどれほど重要であるかを十分に強調することはできません。結局のところ、コードはテストケースでまったく呼び出されないかもしれませんが、コンパイラは常にそれを評価します。

すべて一緒に置く

それで...すべてをまとめましょう。私たちが欲しい:

  • 強く型付けされたデータブロック
  • そのデータは継続的に保存されます
  • ILサポートにより、CPUのクールな命令を使用して確実に出血させることができます。
  • すべての機能を公開する共通インターフェース
  • 型の安全性
  • 多次元性
  • 値型を値型として保存したい
  • そして、他の言語と同じマーシャリング構造
  • また、メモリ割り当てが容易になるため、固定サイズ

これは、どのコレクションでもかなり低レベルの要件です... IL/CPUへの変換だけでなく、特定の方法でメモリを構成する必要があります...それが基本型と見なされる正当な理由があると思います。

12
atlaste

互換性。配列は、ジェネリックがなかった時代に遡る歴史的なタイプです。

今日では、Array、次にArray<T>、次に特定のクラス;)

11
TomTom

したがって、なぜそうではないのか知りたい:

その理由は、C#の最初のバージョンにはジェネリックが存在しなかったためです。

しかし、私は自分で何が問題になるのか理解できません。

問題は、Arrayクラスを使用する大量のコードが破損することです。 C#は多重継承をサポートしないため、次のような行

Array ary = Array.Copy(.....);
int[] values = (int[])ary;

壊れるでしょう。

MSが最初からC#と.NETを作り直した場合、おそらくArrayをジェネリッククラスにするのに問題はないでしょうが、それは現実ではありません。

5
JLRishe

人々が言及した他の問題に加えて、一般的な_Array<T>_を追加しようとすると、他のいくつかの困難が生じます。

  • ジェネリックが導入された瞬間から今日の共分散機能が存在していたとしても、配列には十分ではありませんでした。 _Car[]_を並べ替えるように設計されたルーチンは、配列の要素をCar型の要素にコピーしてからコピーして戻す必要がある場合でも、_Buick[]_を並べ替えることができます。 。タイプCarから_Buick[]_への要素のコピーは、実際にはタイプセーフではありませんが、便利です。ソートを可能にするような方法で、共変配列の1次元配列インターフェースを定義することができます[e.g. `Swap(int firstIndex、int secondIndex)method]を含めることにより、しかし、配列と同じくらい柔軟な何かを作ることは難しいでしょう。

  • _Array<T>_型は_T[]_に対してうまく機能するかもしれませんが、汎用型システム内では_T[]_、_T[,]_、_T[,,]_、_T[,,,]_など、任意の数の添え字用。

  • .netには、2つの型を同一と見なす必要があるという概念を表現する手段がありません。そのため、型_T1_の変数は、型_T2_のいずれかにコピーできます。同じオブジェクトへの参照を保持します。 _Array<T>_型を使用する人は、おそらく_T[]_を期待するコードにインスタンスを渡し、_T[]_を使用するコードからインスタンスを受け入れたいと思うでしょう。古いスタイルの配列を新しいスタイルを使用するコードとの間でやり取りできない場合、新しいスタイルの配列は機能というよりも障害になります。

型システムをジンクスして、本来の動作をする_Array<T>_型を許可する方法があるかもしれませんが、そのような型は、他の一般的な型とはまったく異なる多くの方法で動作し、既に型があるため目的の動作(つまり_T[]_)を実装するため、別の定義によってどのような利点が得られるかは明確ではありません。

3
supercat

誰もが言っているように、元のArrayはジェネリックではありません。なぜなら、v1でジェネリックが存在していなかったからです。以下の推測...

「配列」をジェネリックにする(今では意味があります)には、

  1. 既存のArrayを保持し、汎用バージョンを追加します。これは素晴らしいことですが、「配列」のほとんどの使用法は、時間の経過とともに成長することを伴い、同じ概念List<T>のより良い実装が代わりに実装されたと考えられます。この時点で、「成長しない要素の連続リスト」の汎用バージョンを追加しても、あまり魅力的ではありません。

  2. 非ジェネリックArrayを削除し、同じインターフェースを持つジェネリックArray<T>実装に置き換えます。次に、既存のArray型ではなく新しい型で動作するように、古いバージョン用にコンパイルされたコードを作成する必要があります。フレームワークのコードがこのような移行をサポートすることは可能ですが(おそらく難しい)、他の人が書いたコードは常にたくさんあります。

    Arrayは非常に基本的なタイプなので、ほとんどすべての既存のコード(リフレクションを備えたカスタムコードとネイティブコードとCOMによるマーシャリングを含む)がそれを使用します。その結果、バージョン間(1.x-> 2.xの.Net Framework)のわずかな非互換性の価格も非常に高くなります。

そのため、結果としてArray型は永久に存在し続けます。現在、使用する汎用的な同等物としてList<T>があります。

2
Alexei Levenkov

何かが足りないかもしれませんが、配列インスタンスがICollection、IEnumerableなどにキャストまたは使用されない限り、Tの配列では何も得られません。

配列は高速であり、すでにタイプセーフであり、ボクシング/アンボクシングのオーバーヘッドは発生しません。

0
user1758003