This topic started from question on StackOverflow: different collections (Array and List) have different implementation of IList.IndexOf method.
Example
using System.Collections;
using System.Collections.Generic;
namespace IndexOf
{
class Program
{
public static void Main()
{
var item = new Item { Id = 1 };
IList list = new List<Item> { item };
IList array = new[] { item };
var newItem = new Item { Id = 1 };
var lIndex = list.IndexOf(newItem);
var aIndex = array.IndexOf(newItem);
Console.WriteLine(lIndex); //0
Console.WriteLine(aIndex); //-1
}
public class Item : IEquatable<Item>
{
public int Id { get; set; }
public bool Equals(Item other) => other != null && other.Id == Id;
}
}
}
As we see Item impelments generic interface IEquatable<T> with generic method Equals(T other). Both collections are declared as IList that has non-generic method IndexOf(object value). But, why results are different? We can find item in list, but not in array.
Let’s look at List<T> implementation of the method:
int IList.IndexOf(object item)
{
if (List<T>.IsCompatibleObject(item))
return this.IndexOf((T) item);
return -1;
}
It calls generic implementation IndexOf(T item):
public int IndexOf(T item)
{
return Array.IndexOf<T>(this._items, item, 0, this._size);
}
And static generic method IndexOf of Array class:
public static int IndexOf<T>(T[] array, T value, int startIndex, int count)
{
if (array == null)
throw new ArgumentNullException("array");
if (startIndex < 0 || startIndex > array.Length)
throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_Index"));
if (count < 0 || count > array.Length - startIndex)
throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_Count"));
return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count);
}
The main part is EqualityComparer<T>.Default. Here .net creates comparer for out class Item. As our class impelments IEquatable<> interface, GenericEqualityComparer will be created. Check with:
Console.WriteLine(EqualityComparer<Item>.Default.GetType());
GenericEqualityComparer uses IEquatable<T>.Equals(T other) method to compare objects:
internal class GenericEqualityComparer<T> : EqualityComparer<T> where T : IEquatable<T>
{
public override bool Equals(T x, T y)
{
if ((object) x != null)
{
if ((object) y != null)
return x.Equals(y);
return false;
}
return (object) y == null;
}
//...
}
So, method IndexOf implemented in List always uses IEquatable<T>.Equals(T other). What about arrays?
Array implementation:
int IList.IndexOf(object value)
{
return Array.IndexOf(this, value);
}
There is not calls to generic implementation, it operates with object:
public static int IndexOf(Array array, object value)
{
if (array == null)
throw new ArgumentNullException("array");
int lowerBound = array.GetLowerBound(0);
return Array.IndexOf(array, value, lowerBound, array.Length);
}
And again calls non-generic methods. Final method has this code:
for (int index = startIndex; index < num; ++index)
{
object obj = objArray[index];
if (obj != null && obj.Equals(value))
return index;
}
Where obj.Equals(value) is Equals(object obj) method of Object class.
So, Array and List has different behavior, when we search index of item. It also the same for Contains method:
Console.WriteLine(list.Contains(newItem)); //true
Console.WriteLine(array.Contains(newItem)); //false
Ok, but we worked with non-generic interface IList and it looks not so strange. Let’s change example to use generic IList<T>:
using System;
using System.Collections;
using System.Collections.Generic;
namespace IndexOf
{
class Program
{
public static void Main()
{
var item = new Item { Id = 1 };
IList<Item> list = new List<Item> { item };
IList<Item> array = new[] { item };
var newItem = new Item { Id = 1 };
var lIndex = list.IndexOf(newItem);
var aIndex = array.IndexOf(newItem);
Console.WriteLine(lIndex); //0
Console.WriteLine(aIndex); //-1
Console.WriteLine(list.Contains(newItem)); //true
Console.WriteLine(array.Contains(newItem)); //false
}
public class Item : IEquatable<Item>
{
public int Id { get; set; }
public bool Equals(Item other) => other != null && other.Id == Id;
}
}
}
But result is the same! We can find item in list, but not in array.
List uses generic method that we saw before:
public int IndexOf(T item)
{
return Array.IndexOf<T>(this._items, item, 0, this._size);
}
But array… has no generic implementation of IndexOf<T> method! Here magic begins. CLR uses special wrapper for array — SZArrayHelper and it’s method IndexOf<T>:
int IndexOf<T>(T value) {
//! 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 Array.IndexOf(_this, value);
}
It look like we work with generic method and objects and should lead to GenericEqualityComparer<T>:
public static int IndexOf<T>(T[] array, T value) {
if (array==null) {
throw new ArgumentNullException("array");
}
Contract.Ensures((Contract.Result<int>() < 0) ||
(Contract.Result<int>() >= 0 && Contract.Result<int>() < array.Length && EqualityComparer<T>.Default.Equals(value, array[Contract.Result<int>()])));
Contract.EndContractBlock();
return IndexOf(array, value, 0, array.Length);
}
But the problem is, how CLR calls this method:

Despite the fact that we use generic IList<Item> and pass Item, CLR cast it to object type and use ObjectEqualityComparer<T> instead of GenericEqualityComparer<T>. Funny, that ObjectEqualityComparer<T> is generic too but compares values as objects:
internal class ObjectEqualityComparer<T> : EqualityComparer<T>
{
public override bool Equals(T x, T y)
{
if ((object) x != null)
{
if ((object) y != null)
return x.Equals((object) y);
return false;
}
return (object) y == null;
}
//...
}
And we returned to Item that has only generic Equals, not base one.
So, don’t forget to override Equals(object obj):
public class Item : IEquatable<Item>
{
public int Id { get; set; }
public bool Equals(Item other) => other != null && other.Id == Id;
public override bool Equals(object obj) => Equals(obj as Item);
}