c# - Lista<T>.Contains y T[]. Contiene comportarse de manera diferente
arrays equals (3)
La matriz no tiene un método con el nombre de contiene, este es un método de extensión de la clase Enumerable.
Enumerable.Contains método, que está utilizando en su matriz,
está utilizando el comparador de igualdad por defecto .
El comparador de igualdad predeterminado, necesita anular el método Object.Equality.
Esto es debido a la compatibilidad con versiones anteriores.
Las listas tienen sus propias implementaciones específicas, pero Enumerable debe ser compatible con cualquier Enumerable, desde .NET 1 hasta .NET 4.5
Buena suerte
Digamos que tengo esta clase:
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();
}
}
Esta es la prueba:
var animals = new[] { new Animal { Name = "Fred" } };
Ahora, cuando lo haga:
animals.ToList().Contains(new Animal { Name = "Fred" });
llama a la derecha sobrecarga de genéricos Equals
. El problema es con los tipos de matriz. Supongamos que yo hago:
animals.Contains(new Animal { Name = "Fred" });
Se llama método Equals
no genérico . En realidad, T[]
no expone ICollection<T>.Contains
método. En el caso anterior, IEnumerable<Animal>.Contains
se llama sobrecarga de extensión que a su vez llama a ICollection<T>.Contains
. Aquí es cómo se 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);
}
Así que mis preguntas son:
- ¿Por qué debería
List<T>.Contains
yT[].Contains
comportarse de manera diferente? En otras palabras, ¿por qué el primero llama a losEquals
genéricos y los últimos a los no genéricos aunque ambas colecciones sean genéricas ? - ¿Hay alguna forma de ver
T[].Contains
implementación?
Edit: ¿Por qué importa o por qué estoy preguntando esto:
Se dispara uno en caso de que se olvide de anular los
Equals
no genéricos al implementarIEquatable<T>
en cuyo caso las llamadas comoT[].Contains
una verificación de igualdad referencial. Especialmente cuando espera que todas las colecciones genéricas operen enEquals
genéricos .Pierde todos los beneficios de implementar
IEquatable<T>
(aunque no es un desastre para los tipos de referencia).Como se señaló en los comentarios, solo me interesa conocer los detalles internos y las opciones de diseño. No hay otra situación genérica en la que pueda pensar dónde se preferirán los
Equals
no genéricos , ya sea en operaciones deList<T>
o basadas en conjuntos (Dictionary<K,V>
etc.). Peor aún, si Animal hubiera sido una estructura, Animal []. Contiene las llamadas GenéricoEquals
, todo lo que hace que la implementación de T [] sea un poco extraña, algo que los desarrolladores deberían saber.
Nota: la versión genérica de Equals
solo se llama cuando la clase implementa IEquatable<T>
. Si la clase no implementa IEquatable<T>
, la sobrecarga no genérica de Equals
se llama independientemente de si se llama mediante la List<T>.Contains
o T[].Contains
.
Las matrices implementan las interfaces genéricas IList<T>
, ICollection<T>
y IEnumerable<T>
pero la implementación se proporciona en tiempo de ejecución y, por lo tanto, no están visibles para las herramientas de compilación de documentación (Es por eso que no ve ICollection<T>.Contains
en la documentación msdn de Array
).
Sospecho que la implementación en tiempo de ejecución simplemente llama al IList.Contains(object)
no genérico. IList.Contains(object)
que la matriz ya tiene.
Y para Equals
se llama el método Equals
no genérico en su clase.
Las matrices no implementan IList<T>
porque pueden ser multidimensionales y no basadas en cero.
Sin embargo, en el tiempo de ejecución las matrices unidimensionales que tienen un límite inferior de cero implementan automáticamente IList<T>
y algunas otras interfaces genéricas. El propósito de este hack de tiempo de ejecución se detalla a continuación en 2 citas.
Aquí http://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx dice:
En C # 2.0 y versiones posteriores, las matrices unidimensionales que tienen un límite inferior de cero implementan automáticamente
IList<T>
. Esto le permite crear métodos genéricos que pueden usar el mismo código para iterar a través de matrices y otros tipos de colección. Esta técnica es principalmente útil para leer datos en colecciones. La interfazIList<T>
no se puede usar para agregar o eliminar elementos de una matriz. Se lanzará una excepción si intenta llamar a un métodoIList<T>
comoRemoveAt
en una matriz en este contexto.
Jeffrey Richter en su libro dice:
Sin embargo, el equipo de CLR no quería que
System.Array
implementaraIEnumerable<T>
,ICollection<T>
eIList<T>
, debido a problemas relacionados con matrices multidimensionales y matrices no basadas en cero. La definición de estas interfaces en System.Array habría habilitado estas interfaces para todos los tipos de matriz. En cambio, el CLR realiza un pequeño truco: cuando se crea un tipo de matriz unidimensional, cero-límite inferior, el CLR automáticamente hace que el tipo de matriz se implementeIEnumerable<T>
,ICollection<T>
eIList<T>
(dondeT
es el tipo de elemento de la matriz) y también implementa las tres interfaces para todos los tipos base del tipo de matriz siempre que sean tipos de referencia.
Al profundizar, SZArrayHelper es la clase que proporciona estas implementaciones de IList "piratas" para matrices basadas en Cero de dimensión única.
Aquí está la descripción de la clase:
//---------------------------------------------------------------------------------------- // ! 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.) //----------------------------------------------------------------------------------------
Y contiene la implementación:
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; }
Así que llamamos al siguiente método.
public static int IndexOf<T>(T[] array, T value, int startIndex, int count) {
...
return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count);
}
Hasta ahora tan bueno. Pero ahora llegamos a la parte más curiosa / buggy.
Considere el siguiente ejemplo (basado en su pregunta de seguimiento)
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();
}
}
He plantado lanzamientos de excepción en IEquatable<T>.Equals()
no IEquatable<T>.Equals()
.
La sorpresa es:
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" });
Este código no arroja ninguna excepción. ¡Llegamos directamente a la implementación de IEquatable Equals!
Pero cuando probamos el siguiente código:
structs.Contains(new DummyStruct {Name = "Fred"});
classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method
La segunda línea lanza la excepción, con el siguiente seguimiento de pila:
DummyClass.Equals (objeto obj) en System.Collections.Generic.ObjectEqualityComparer`1.IndexOf (T [] array, valor T, Int32 startIndex, cuenta Int32) en System.Array.IndexOf (T [] array, valor T) en System.SZArrayHelper.Contains (valor de T)
Ahora el error? o la Gran pregunta aquí es cómo llegamos a ObjectEqualityComparer desde nuestro DummyClass que implementa IEquatable<T>
.
Porque el siguiente código:
var t = EqualityComparer<DummyStruct>.Default;
Console.WriteLine(t.GetType());
var t2 = EqualityComparer<DummyClass>.Default;
Console.WriteLine(t2.GetType());
Produce
System.Collections.Generic.GenericEqualityComparer
1[DummyStruct] System.Collections.Generic.GenericEqualityComparer
1 [DummyClass]
Ambos usan GenericEqualityComparer, que llama al método IEquatable. De hecho, el comparador predeterminado llama al método 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
}
Las partes curiosas están en negrita. Evidentemente, para DummyClass with Contains llegamos a la última línea y no pasamos
typeof (IEquatable) .IsAssignableFrom (c)
¡comprobar!
Por qué no? Bueno, supongo que es un error o un detalle de implementación, que difiere para las estructuras debido a la siguiente línea en la clase de descripción SZArrayHelper:
La "T" reflejará la interfaz utilizada para invocar el método. El tiempo de ejecución real "esto" será un conjunto que se puede convertir en "T []" (es decir, para primitivs y valuetypes, será >> exactamente "T []" - para orefs, puede ser una "U []" donde U deriva de T )
Así que ya sabemos casi todo. La única pregunta que queda, es cómo U no pasa typeof(IEquatable<T>).IsAssignableFrom(c)
check?
PD: para ser más precisos, el código de implementación de SZArrayHelper Contains es de SSCLI20. Parece que la implementación actual ha cambiado, porque el reflector muestra lo siguiente para este método:
private bool Contains<T>(T value)
{
return (Array.IndexOf<T>(JitHelpers.UnsafeCast<T[]>(this), value) != -1);
}
JitHelpers.UnsafeCast muestra el siguiente código de 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;
}
Ahora me pregunto acerca de los tres signos de exclamación y cómo sucede exactamente en esa misteriosa aplicación getILIntrinsicImplementation
.