C#の配列はどのようにIList <T>を部分的に実装しますか?


99

ご存じかもしれませんが、C#の配列はIList<T>、他のインターフェイスの中でも特に実装されています。どういうわけか、彼らはIList<T>!のCountプロパティを公に実装せずにこれを行います。配列にはLengthプロパティのみがあります。

これは、インターフェイスの実装に関する独自のルールを破るC#/。NETの露骨な例ですか、それとも何か不足していますか?


2
ArrayクラスをC#で記述する必要があるとは誰も言っていません。
user541686

Arrayは「マジック」クラスであり、C#または.netを対象とするその他の言語では実装できませんでした。ただし、この特定の機能はC#で使用できます。
CodesInChaos

回答:


81

ハンスの答えに照らした新しい答え

ハンスの答えのおかげで、実装が思ったよりもやや複雑であることがわかります。コンパイラーとCLRの両方が、配列型が実装しているような印象を与えるために非常に一生懸命に努力しますIList<T>が、配列の違いにより、これがさらに難しくなります。Hansの回答とは逆に、特定の配列の型はそうではない ため、配列型(とにかく1次元、ゼロベース)は、ジェネリックコレクションを直接実装します。これは配列の基本型にSystem.Arrayすぎません。配列型にサポートするインターフェイスを尋ねると、一般的な型が含まれます。

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

出力:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

1次元のゼロベースの配列の場合、言語に関する限り、配列も実際に実装されIList<T>ます。C#仕様のセクション12.1.2はそう述べています。したがって、基礎となる実装が行うことは何でも、言語は、他のインターフェイスと同様に、実装のタイプのように動作する必要があります。この観点から、インターフェース、明示的に実装されているメンバーの一部(など)を使用して実装されます。これが、言語レベルで何が起こっているかを説明する最良の説明です。T[]IList<T>Count

これは、1次元配列(およびゼロベースの配列)にのみ当てはまることに注意してください。言語としてのC#は、非ゼロベースの配列については何も述べていません。実装T[,] していませんIList<T>

CLRの観点から見ると、もっとおかしなことが起こっています。ジェネリックインターフェイスタイプのインターフェイスマッピングは取得できません。例えば:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

例外を与えます:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

ではなぜ奇妙なのでしょうか?まあ、それは本当に配列の共分散によるものだと思います。これは型システムIMOのいぼです。にもかかわらずIList<T>はない(かつ安全にすることはできません)共変、配列の共分散は、仕事にこれを許可します。

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... 実際にはそうではないときに、それを実装のように見せますtypeof(string[])IList<object>

CLI仕様(ECMA-335)パーティション1、セクション8.7.1には次の内容があります。

署名タイプTは、以下の少なくとも1つが成り立つ場合に限り、署名タイプUと互換性があります。

...

Tは、ゼロベースのランク1配列でV[]あり、UでありIList<W>、そしてVは、アレイ素子互換-とWです。

(これは、実際には言及していないICollection<W>か、IEnumerable<W>私は仕様のバグであると考えています。)

非差異の場合、CLI仕様は言語仕様と直接連動します。パーティション1のセクション8.9.1から:

さらに、エレメントタイプTで作成されたベクターは、インターフェイスを実装しますSystem.Collections.Generic.IList<U>。ここで、U:= Tです(§8.7)。

ベクトルは、底がゼロの1次元配列です。)

今の観点から実装の詳細、明確にCLRは、ここに代入互換性を維持するために、いくつかのファンキーなマッピングを行っている次の場合string[]の実施を求めているICollection<object>.Count、それがでていることを扱うことができない非常に通常の方法。これは明示的なインターフェース実装としてカウントされますか?インターフェイスマッピングを直接要求しない限り、言語の観点からは常にそのように動作するので、そのように扱うことは合理的だと思います。

どうICollection.Countですか?

ここまでは、ジェネリックインターフェイスについて説明してきましたが、ICollectionそのCountプロパティを持つ非ジェネリックもあります。今回はインターフェースのマッピング取得できますSystem.Array。実際、インターフェースはによって直接実装されています。ICollection.Countプロパティ実装のドキュメントには、Array明示的なインターフェース実装で実装されていると記載されています。

この種の明示的なインターフェースの実装が「通常の」明示的なインターフェースの実装とは異なる方法を誰かが考えることができるなら、私はそれをさらに調査したいと思います。

明示的なインターフェース実装に関する古い答え

上記にもかかわらず、配列の知識のためにさらに複雑ですが、明示的なインターフェイスの実装を通じて、同じ目に見える効果を使って何かを行うことができます

次に、簡単なスタンドアロンの例を示します。

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}

5
foo.M1()でコンパイル時エラーが発生すると思います。foo.M2();ではありません
Kevin Aenmey

ここでの課題は、配列などの非ジェネリッククラスに、IList <>などのジェネリックインターフェイスタイプを実装させることです。あなたのスニペットはそれをしません。
ハンスパッサント2012年

@HansPassant:非ジェネリッククラスにジェネリックインターフェイスタイプを実装させるのは非常に簡単です。ささいなこと。それがOPが要求していたことを示す兆候は見られません。
Jon Skeet

4
@JohnSaunders:実際には、以前は不正確だったとは思いません。私はそれを大幅に拡張し、CLRが配列を奇妙に扱う理由を説明しましたが、明示的なインターフェイス実装の私の答えは以前はかなり正しかったと思います。どのようにあなたは反対しますか?繰り返しになりますが、詳細は役に立ちます(適切な場合は、おそらくあなた自身の答えで)。
Jon Skeet

1
@RBT:はい、使用にCountは違いがありAddますが、配列は固定サイズであるため、常にスローされます。
Jon Skeet 2017

86

ご存じかもしれませんが、C#の配列はIList<T>、他のインターフェイスの中でも特に

ええ、ええと、そうではありません。これは、.NET 4フレームワークのArrayクラスの宣言です。

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

System.Collections.Generic.IList <> ではなく、 System.Collections.IListを実装します。できません。Arrayはジェネリックではありません。ジェネリックIEnumerable <>およびICollection <>インターフェースについても同様です。

ただし、CLRはその場で具象配列型を作成するため、技術的にはこれらのインターフェイスを実装する配列型を作成できます。ただし、これは当てはまりません。たとえば、次のコードを試してください。

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

GetInterfaceMap()呼び出しは、「インターフェースが見つかりません」という具象配列型に対して失敗します。しかし、IEnumerable <>へのキャストは問題なく機能します。

これはカモのようなカモのタイピングです。Objectから派生するValueTypeからすべての値型が派生するという錯覚を生み出すのは、同じ種類のタイピングです。コンパイラーとCLRはどちらも、値型の場合と同様に、配列型について特別な知識を持っています。コンパイラーは、IList <>へのキャストの試みを確認し、「わかりました。それを行う方法を知っています!」と言います。そして、キャストクラスIL命令を発行します。CLRは問題なく、基になる配列オブジェクトで機能するIList <>の実装を提供する方法を知っています。それは、そうでなければ隠されたSystem.SZArrayHelperクラス、これらのインターフェースを実際に実装するラッパーの組み込みの知識を持っています。

誰もが主張するように明示的には行いませんが、あなたが尋ねたCountプロパティは次のようになります:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

はい、確かにそのコメントを「ルールを破る」と呼ぶことができます:)それ以外の点では便利です。非常によく隠されているので、CLRの共有ソースディストリビューションであるSSCLI20でこれを確認できます。「IList」を検索して、型の置換が行われる場所を確認します。実際の動作を確認するには、clr / src / vm / array.cpp、GetActualImplementationForArrayGenericIListMethod()メソッドが最適です。

CLRでのこの種の置換は、WinRT(別名Metro)のマネージコードの記述を可能にするCLRの言語プロジェクションで発生することと比較すると、かなり穏やかです。ほぼすべてのコア.NETタイプがそこで置き換えられます。IList <>は、たとえば完全に管理されていない型であるIVector <>にマップされます。COMはジェネリック型をサポートしていません。

まあ、それはカーテンの後ろで何が起こっているのかを見ていました。マップの最後にドラゴンが住んでいるため、非常に不快で奇妙で見慣れない海になる可能性があります。地球を平らにして、マネージコードで実際に起こっていることの異なるイメージをモデル化することは非常に便利です。それをみんなのお気に入りの答えにマッピングすることは、そのように快適です。これは値型ではうまく機能しませんが(構造体を変更しないでください!)、これは非常によく隠されています。GetInterfaceMap()メソッドの失敗は、私が考えることができる抽象化の唯一のリークです。


1
これはArrayクラスの宣言であり、配列の型ではありません。これは配列の基本型です。C#の1次元配列はを実装していますIList<T>。そして、非ジェネリック型は確かにとにかくジェネリックインターフェイスを実装できます... さまざまな型がたくさんあるため機能します- typeof(int[])!= typeof(string []), so typeof(int []) `implements IList<int>typeof(string[])implements IList<string>
Jon Skeet

2
@HansPassant:不安を感じたからといって私が反対票を投じるとは思わないでください。あなたの推論経由Array(あなたが示すように、抽象クラスなので、配列オブジェクトの実際の型にはなり得ない)と結論(それが実装されていないIList<T>)の両方が正しくないIMOであるという事実は変わりません。それが実装されているがIList<T>、珍しいと面白いです、私は同意するだろう-それは純粋だ実装の詳細。T[]実装していないと主張することは、IList<T>IMOを誤解させることになります。これは仕様と観察されたすべての動作に反します。
Jon Skeet

6
まあ、あなたはそれが間違っていると思うことを確認してください。仕様書で読んだことでうまくいかなくてはなりません。お気軽にご確認ください。ただし、GetInterfaceMap()が失敗する理由を説明することはできません。「ファンキーな何か」は洞察の多くではありません。私は実装メガネをかけています。もちろん失敗します。それはカモのような型打ちです。具体的な配列型は実際にはICollection <>を実装していません。それについてファンキーなことは何もありません。ここに置いておきましょう、私たちは決して同意しません。
ハンスパッサント2013年

4
配列が実装できないIList<T> ために 実装できないと主張する偽のロジックを少なくとも取り除くのArrayはどうですか?その論理は私が反対していることの大部分です。それを超えて、型がインターフェイスを実装することの意味の定義に同意する必要があると思います。私の考えでは、配列型は以外のを実装する型の観察可能な機能すべて表示します。繰り返しますが、実装の詳細は異なりますが、それが不変であると言っても問題ないように、それがどのように達成されるかは私にとってそれほど重要ではありません。IList<T>GetInterfaceMappingSystem.String
Jon Skeet

1
C ++ CLIコンパイラについてはどうですか?それは明らかに「私はそれを行う方法がわかりません!」エラーを発行します。機能するには、明示的なキャストが必要IList<T>です。
Tobias Knauss

21

IList<T>.Count明示的に実装されています:

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

これは、単純な配列変数がある場合に、両方がなくCountLength直接使用できないようにするために行われます。

一般に、明示的なインターフェイス実装は、型のすべてのコンシューマーにそのように考えることを強いることなく、型を特定の方法で使用できるようにする場合に使用されます。

編集:おっと、悪いリコールがあります。ICollection.Count明示的に実装されています。ジェネリックIList<T>は、以下のハンス記述として処理されます


4
しかし、なぜそれらが長さの代わりにプロパティカウントを呼び出さなかったのか不思議に思いますか?配列は、そのようなプロパティを持つ唯一の一般的なコレクションです(を数えない限りstring)。
Tim S.

5
@TimS良い質問(そして答えがわからない質問です。)その理由は、「count」はいくつかの項目を意味するのに対して、配列は割り当てられるとすぐに変更できない「長さ」を持つためだと思います(値を持つ要素に関係なく。)
dlev '22

1
@TimSは、私はので、それが終わっていると思いICollection宣言しCount、そしてそれもされるだろうよりそれで単語「コレクション」とタイプは使用しなかった場合は混乱Count:)。これらの決定を行う際には、常にトレードオフがあります。
dlev、

4
@JohnSaunders:繰り返しになりますが... 有益な情報がない投票だけです。
Jon Skeet

5
@JohnSaunders:私はまだ確信が持てません。HansはSSCLIの実装について言及しましたがIList<T>、言語とCLIの仕様の両方が反対になっているにもかかわらず、配列型はを実装しないとも主張しました。インターフェースの実装が内部で機能する方法は複雑かもしれませんが、それは多くの状況に当てはまります。内部の仕組みが変更可能であるSystem.Stringという理由だけで、それが不変であると言って誰かに反対票を投じますか?以下のために、すべての実用的な目的-と確かに限りC#言語を懸念している-それはある明確な独自の実装。
Jon Skeet


2

IListの明示的なインターフェイス実装と同じです。インターフェイスを実装したからといって、そのメンバーがクラスメンバーとして表示される必要があるわけではありません。Countプロパティを実装してますが、X []で公開していません。


1

参照ソースを利用できる場合:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

具体的には、この部分:

インターフェーススタブディスパッチャーは、これを特殊なケースとして扱い、SZArrayHelperをロードし、対応するジェネリックメソッド( メソッド名で単純に一致)を見つけ、型に対してインスタンス化して実行します。

(エンファシス鉱山)

ソース(上にスクロール)。

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