Почему List.Contains не работает, как я ожидаю?

Почему эта программа печатает "не добавлено", в то время как я думаю, что она должна печатать "добавлена"?

using System;
using System.Collections.Generic;

class Element
{
    public int id;

    public Element(int id)
    {
        this.id = id;
    }

    public static implicit operator Element(int d)  
    {
        Element ret = new Element(d);
        return ret;
    }

    public static bool operator ==(Element e1, Element e2)
    {
        return (e1.id == e2.id);
    }

    public static bool operator !=(Element e1, Element e2)
    {
        return !(e1.id == e2.id);
    }
}

class MainClass
{
    public static void Main(string[] args)
    {
        List<Element> element = new List<Element>();
        element.Add(2);
        if(element.Contains(2))
            Console.WriteLine("added");
        else
            Console.WriteLine("not added");
    }
}

Метод Contains не использует оператор ==. В чем проблема?

Ответ 1

Метод Contains не использует оператор ==

Нет - он использует Equals, который вы не переопределили... поэтому вы получаете поведение по умолчанию Equals, которое вместо этого должно проверять ссылочный идентификатор. Вы должны переопределить Equals(object) и GetHashCode чтобы они были согласованы друг с другом - и для здравого смысла, в соответствии с вашей перегрузкой ==.

Я также рекомендую использовать IEquatable<Element>, который List<Element> будет использовать в предпочтении Equals(object), в качестве EqualityComparer<T>.Default подбирает его соответствующим образом.

О, и ваши перегрузки оператора должны обрабатывать и нулевые ссылки.

Я также настоятельно рекомендую использовать частные поля вместо публичных и сделать ваш тип неизменным - запечатать его и сделать id чтения. Реализация равенства для изменяемых типов может привести к возникновению нечетных ситуаций. Например:

Dictionary<Element, string> dictionary = new Dictionary<Element, string>();
Element x = new Element(10);
dictionary[x] = "foo";
x.id = 100;
Console.WriteLine(dictionary[x]); // No such element!

Это произошло бы потому, что хэш-код будет меняться (по крайней мере, в большинстве реализаций), поэтому хеш-таблица, лежащая в основе словаря, не сможет найти даже ссылку на тот же объект, что уже там.

Таким образом, ваш класс будет выглядеть примерно так:

internal sealed class Element : IEquatable<Element>
{
    private readonly int id;

    public int Id { get { return id; } }

    public Element(int id)
    {
        this.id = id;
    }

    public static implicit operator Element(int d)  
    {
        return new Element(d);
    }

    public static bool operator ==(Element e1, Element e2)
    {
        if (object.ReferenceEquals(e1, e2))
        {
            return true; 
        }
        if (object.ReferenceEquals(e1, null) ||
            object.ReferenceEquals(e2, null))
        {
            return false; 
        }
        return e1.id == e2.id;
    }

    public static bool operator !=(Element e1, Element e2)
    {
        // Delegate...
        return !(e1 == e2);
    }

    public bool Equals(Element other)
    {
        return this == other;
    }

    public override int GetHashCode()
    {
        return id;
    }

    public override bool Equals(object obj)
    {
        // Delegate...
        return Equals(obj as Element);
    }
}

(Кстати, я не уверен в достоинстве неявного преобразования, я, как правило, держался подальше от них.)

Ответ 2

Метод Contains не использует оператор ==. В чем проблема?

Это верно.

Этот метод [Содержит] определяет равенство с помощью сопоставления равенства по умолчанию, как определено реализацией объекта метода IEquatable.Equals для T (тип значений в списке).

http://msdn.microsoft.com/en-us/library/bhkz42b3(v=vs.110).aspx

Вы также должны переопределить Equals(). Обратите внимание, когда вы перегружаете Equals(), почти всегда правильно также переопределять GetHashCode().

Ответ 3

Переопределить Equals и GetHashCode как:

class Element
{
    public int id;

    protected bool Equals(Element other)
    {
        return id == other.id;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Element) obj);
    }

    public override int GetHashCode()
    {
        return id; //or id.GetHashCode();
    }
 //..... rest of the class

См.: List<T>.Contains Method

Этот метод определяет равенство с помощью IEquatable<T>.Equals равенства по умолчанию, как определено реализацией объекта метода IEquatable<T>.Equals для T (тип значений в списке).