20

このクラスがあるとします:

public class Animal : IEquatable<Animal>
{
    public string Name { get; set; }

    public bool Equals(Animal other)
    {
        return Name.Equals(other.Name);
    }
    public override bool Equals(object obj)
    {
        return Equals((Animal)obj);
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

これはテストです:

var animals = new[] { new Animal { Name = "Fred" } };

今、私がするとき:

animals.ToList().Contains(new Animal { Name = "Fred" }); 

正しいジェネリックEqualsオーバーロードを呼び出します。問題は配列型にあります。私がそうするとします:

animals.Contains(new Animal { Name = "Fred" });

非ジェネリックEqualsメソッドを呼び出します。実際にはメソッドT[]を公開していません。ICollection<T>.Contains上記の場合、IEnumerable<Animal>.Contains拡張機能のオーバーロードが呼び出され、次にICollection<T>.Contains. IEnumerable<T>.Contains実装方法は次のとおりです。

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> collection = source as ICollection<TSource>;
    if (collection != null)
    {
        return collection.Contains(value); //this is where it gets done for arrays
    }
    return source.Contains(value, null);
}

だから私の質問は:

  1. なぜと の振る舞いが異なるのでしょうか? 言い換えれば、両方のコレクションがジェネリックであるにもかかわらず、前者がジェネリックを呼び出し、後者が非ジェネリックを呼び出すのはなぜですか?List<T>.ContainsT[].ContainsEqualsEquals
  2. T[].Contains実装を確認する方法はありますか?

編集:なぜそれが重要なのか、なぜ私はこれを尋ねているのですか:

  1. 彼女が実装時に非ジェネリックEqualsをオーバーライドするのを忘れた場合、それは 1 つトリップしIEquatable<T>ますT[].Contains。特に、すべてのジェネリック コレクションがジェネリックEquals上で動作することを期待している場合。

  2. 実装のすべての利点が失われますIEquatable<T>(参照型の惨事ではありませんが)。

  3. コメントで述べたように、内部の詳細と設計の選択を知りたいだけです。非ジェネリックEqualsが優先される場所であると考えることができる他のジェネリックな状況はありません。それは、任意List<T>またはセットベース(Dictionary<K,V>など)の操作です。さらに悪いことに、Animal が構造体だった場合、Animal[].Contains はgenericEqualsを呼び出します。これらすべてが T[] の実装を奇妙にしています。これは開発者が知っておくべきことです。

注:のジェネリック バージョンは、クラスが を実装Equalsする場合にのみ呼び出されます。クラスが を実装していない場合、またはによって呼び出されたかどうかに関係なく、 の非ジェネリック オーバーロードが呼び出されます。IEquatable<T>IEquatable<T>EqualsList<T>.ContainsT[].Contains

4

3 に答える 3

11

配列はIList<T>、多次元でゼロベースでない可能性があるため、実装されません。

ただし、実行時には、下限がゼロの 1 次元配列が自動的に実装されIList<T>、その他の汎用インターフェイスがいくつか実装されます。このランタイム ハックの目的は、以下の 2 つの引用で詳しく説明されています。

ここでhttp://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspxと書かれています:

C# 2.0 以降では、下限が 0 の 1 次元配列は自動的に を実装しIList<T>ます。これにより、同じコードを使用して配列やその他のコレクション型を反復処理できるジェネリック メソッドを作成できます。この手法は、主にコレクション内のデータを読み取る場合に役立ちます。IList<T>インターフェイスを使用して、配列の要素を追加または削除することはできません。このコンテキストで配列IList<T>などのメソッドを呼び出そうとすると、例外がスローされます。RemoveAt

ジェフリー・リヒターは著書の中で次のように述べています。

ただし、CLR チームは、 、およびSystem.Arrayを実装したくありませんでした。これは、多次元配列と非ゼロベースの配列に関連する問題のためです。System.Array でこれらのインターフェイスを定義すると、すべての配列型に対してこれらのインターフェイスが有効になります。代わりに、CLR はちょっとしたトリックを実行します。1 次元のゼロ下限の配列型が作成されると、CLR は自動的に配列型に 、 、および(は配列の要素型) を実装し、さらに 3 つのインターフェイスを実装します。参照型である限り、配列型のすべての基本型。IEnumerable<T>ICollection<T>IList<T>IEnumerable<T>ICollection<T>IList<T>T

さらに掘り下げると、SZArrayHelperは、単一次元のゼロベースの配列に対してこの「ハッキーな」IList 実装を提供するクラスです。

クラスの説明は次のとおりです。

//----------------------------------------------------------------------------------------
// ! 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.)
//----------------------------------------------------------------------------------------

実装が含まれています:

    bool Contains<T>(T value) {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = this as T[];
        BCLDebug.Assert(_this!= null, "this should be a T[]");
        return Array.IndexOf(_this, value) != -1;
    }

したがって、次のメソッドを呼び出します

public static int IndexOf<T>(T[] array, T value, int startIndex, int count) {
    ...
    return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count);
}

ここまでは順調ですね。しかし、ここで最も興味深い/バグのある部分に到達します。

次の例を検討してください(フォローアップの質問に基づいて)

public struct DummyStruct : IEquatable<DummyStruct>
{
    public string Name { get; set; }

    public bool Equals(DummyStruct other) //<- he is the man
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj)
    {
        throw new InvalidOperationException("Shouldn't be called, since we use Generic Equality Comparer");
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

public class DummyClass : IEquatable<DummyClass>
{
    public string Name { get; set; }

    public bool Equals(DummyClass other)
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj) 
    {
        throw new InvalidOperationException("Shouldn't be called, since we use Generic Equality Comparer");
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

IEquatable<T>.Equals()非実装の両方に例外スローを植えました。

驚きは次のとおりです。

    DummyStruct[] structs = new[] { new DummyStruct { Name = "Fred" } };
    DummyClass[] classes = new[] { new DummyClass { Name = "Fred" } };

    Array.IndexOf(structs, new DummyStruct { Name = "Fred" });
    Array.IndexOf(classes, new DummyClass { Name = "Fred" });

このコードは例外をスローしません。IEquatable Equals の実装に直接到達します。

しかし、次のコードを試すと:

    structs.Contains(new DummyStruct {Name = "Fred"});
    classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method

2 行目は、次のスタック トレースで例外をスローします。

DummyClass.Equals(Object obj) で System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] 配列、T 値、Int32 startIndex、Int32 カウント) で System.Array.IndexOf(T[] 配列、T 値) でSystem.SZArrayHelper.Contains(T 値)

今バグ?またはここでの大きな質問は、実装する DummyClass から ObjectEqualityComparer にどのように到達したIEquatable<T>かです。

次のコードのため:

var t = EqualityComparer<DummyStruct>.Default;
            Console.WriteLine(t.GetType());
            var t2 = EqualityComparer<DummyClass>.Default;
            Console.WriteLine(t2.GetType());

プロデュース

System.Collections.Generic.GenericEqualityComparer 1[DummyStruct] System.Collections.Generic.GenericEqualityComparer1[DummyClass]

どちらも、IEquatable メソッドを呼び出す GenericEqualityComparer を使用します。実際、CreateComparer メソッドに続いてデフォルトの比較関数が呼び出されます。

private static EqualityComparer<T> CreateComparer()
{
    RuntimeType c = (RuntimeType) typeof(T);
    if (c == typeof(byte))
    {
        return (EqualityComparer<T>) new ByteEqualityComparer();
    }
    if (typeof(IEquatable<T>).IsAssignableFrom(c))
    {
        return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(GenericEqualityComparer<int>), c);
    } // RELEVANT PART
    if (c.IsGenericType && (c.GetGenericTypeDefinition() == typeof(Nullable<>)))
    {
        RuntimeType type2 = (RuntimeType) c.GetGenericArguments()[0];
        if (typeof(IEquatable<>).MakeGenericType(new Type[] { type2 }).IsAssignableFrom(type2))
        {
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(NullableEqualityComparer<int>), type2);
        }
    }
    if (c.IsEnum && (Enum.GetUnderlyingType(c) == typeof(int)))
    {
        return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(EnumEqualityComparer<int>), c);
    }
    return new ObjectEqualityComparer<T>(); // CURIOUS PART
}

気になる部分は太字で。明らかに、Contains を使用した DummyClass の場合、最後の行に到達し、パスしませんでした

typeof(IEquatable).IsAssignableFrom(c)

小切手!

なぜだめですか?SZArrayHelper 記述クラスの次の行が原因で、構造体とは異なるバグまたは実装の詳細のいずれかだと思います。

「T」は、メソッドの呼び出しに使用されるインターフェースを反映します。実際のランタイム「this」は、「T[]」にキャスト可能な配列になります (つまり、プリミティブと値型の場合、>>正確に「T[]」になります- oref の場合、「U[]」の場合があります)。 U は T から派生します。)

だから私たちは今、ほとんどすべてを知っています。typeof(IEquatable<T>).IsAssignableFrom(c)残された唯一の疑問は、なぜ U がチェックに合格しないのかということです。

PS: より正確に言うと、SZArrayHelper には SSCLI20 の実装コードが含まれています。現在、実装が変更されているようです。原因は、リフレクターがこのメソッドに対して次のように表示することです。

private bool Contains<T>(T value)
{
    return (Array.IndexOf<T>(JitHelpers.UnsafeCast<T[]>(this), value) != -1);
}

JitHelpers.UnsafeCast は、dotnetframework.org の次のコードを示しています

   static internal T UnsafeCast<t>(Object o) where T : class
    {
        // The body of this function will be replaced by the EE with unsafe code that just returns o!!!
        // See getILIntrinsicImplementation for how this happens.
        return o as T;
    }

今、私は 3 つの感嘆符について疑問に思っていgetILIntrinsicImplementationます。

于 2013-11-10T08:33:42.457 に答える
1

Arrays はジェネリック InterfacesIList<T>を実装ICollection<T>IEnumerable<T>ますが、実装は実行時に提供されるため、ドキュメント ビルド ツールには表示されません (そのためICollection<T>.Contains、msdn ドキュメントに が表示されませんArray)。

ランタイムの実装はIList.Contains(object)、配列が既に持っている非ジェネリックを呼び出すだけだと思います。
そのため、クラスの非ジェネリックEqualsメソッドが呼び出されます。

于 2013-11-10T08:45:53.577 に答える
0

配列には、contains という名前のメソッドがありません。これは、Enumerable クラスの拡張メソッドです。

配列で使用している Enumerable.Contains メソッド、

デフォルトの等値比較子を使用しています。

デフォルトの等値比較子は、Object.Equality メソッドのオーバーライドが必要です。

これは下位互換性のためです。

リストには独自の固有の実装がありますが、Enumerable は、.NET 1 から .NET 4.5 までの任意の Enumerable と互換性がある必要があります。

幸運を

于 2013-11-10T08:37:59.423 に答える