给定存储 List<T> 的自定义泛型 class,如何防止将类型 T 的对象多次添加到 List<T>?

Given a custom generic class that stores a List<T> how do I prevent adding an object of type T more than once to the List<T>?

这是我试过的代码,但它不止一次添加了同一个对象:

namespace TestComparison
{
    public interface IAddable
    {
        int RandomIntValue { get; set; } // often Times this value will repeat.
    }

    public class AdditionManager<T> where T : IAddable
    {
        private List<T> addables;

        public AdditionManager()
        {
            addables = new List<T>();
        }

        public void Add(T _addable)
        {
            if (!addables.Contains(_addable))
            {
                addables.Add(_addable);
            }
        }

    }

    public class TestAddable : IAddable
    {
        public int RandomIntValue { get; set; }
        public Data UniqueData = new Data() { UniqueId = 10023 }; // This is what really make each item unique
    }

    public class Data
    {
        public int UniqueId { get; set; }
    }
}

我听说过 IEqualityComparer,我已经在非泛型中实现了它 类,但我不太确定如何在这里实现它。

您可以使用依赖注入为您提供通用实现。为此,您需要在构造通用对象时提供所需的自定义 IEqualityComparer<T> 实现。

public class AdditionManager<T> where T : IAddable
{
    private List<T> addables;
    private IEqualityComparer<T> comparer;

    public AdditionManager()
        : this (EqualityComparer<T>.Default)
    { }

    public AdditionManager(IEqualityComparer<T> _comparer)
    {
        addables = new List<T>();
        comparer = _comparer;
    }

    public void Add(T _addable)
    {
         if (!addables.Contains(_addable, comparer))
         {
             addables.Add(_addable);
         }
    }
}

但是,如果您希望 addables 的列表基于某些约束是唯一的,出于性能原因,我不会使用上述实现。随着列表变大,List<T>.Contains 检查会变慢。

如果列表的顺序无关紧要,请将您的 List<T> 更改为 HashSet<T>HashSet<T>.Contains 将与 Dictionary<TKey, TValue> 查找一样快。但是使用 HashSet<T> 可以完全避免此调用,因为 Add 调用将首先检查该项目是否在集合中,然后再添加它,并且 return truefalse 表示是否添加`

因此,如果 addables 的顺序不重要,那么我将使用以下实现。

public class AdditionManager<T> where T : IAddable
{
    private HashSet<T> addables;

    public AdditionManager()
        : this(EqualityComparer<T>.Default)
    { }

    public AdditionManager(IEqualityComparer<T> _comparer)
    {
        addables = new HashSet<T>(_comparer);
    }

    public void Add(T _addable)
    {
        // will not add the item to the HashSet if it is already present
        addables.Add(_addable);
    }
}

如果您需要维护 addables 的顺序,那么我建议维护 HashSet<T>List<T> 中的对象列表。这将为您提供上述实施的性能,但会保持项目的添加顺序。在此实现中,您需要执行的任何操作都针对 List<T> 进行,并且仅使用 HashSet<T> 来确保添加到 List<T> 时该项目不存在 如果您将进行某种类型的 Remove 操作,请确保从 HashSet<T>List<T>

中删除该项目
public class AdditionManager<T> where T : IAddable
{
    private HashSet<T> set;
    private List<T> list;

    public AdditionManager()
        : this(EqualityComparer<T>.Default)
    { }

    public AdditionManager(IEqualityComparer<T> _comparer)
    {
        set = new HashSet<T>(_comparer);
        list = new List<T>();
    }

    public void Add(T _addable)
    {
        if (set.Add(_addable))
            list.Add(_addable);
    }
}

要使用 TestAddable 创建此对象,您需要如下所示的 IEqualityComparer<TestAddable>。正如其他人所建议的那样,您正在比较的字段应该是不可变的,因为可变键会导致错误。

public class TestAddableComparer : IEqualityComparer<TestAddable>
{
    public bool Equals(TestAddable x, TestAddable y)
    {
        return x.UniqueData.Equals(y.UniqueData);
    }

    public int GetHashCode(TestAddable obj)
    {
        // since you are only comparing use `UniqueData` use that here for the hash code
        return obj.UniqueData.GetHashCode();
    }
}

然后创建管理器对象:

var additionManager = new AdditionManager<TestAddable>(new TestAddableComparer());

您可以使用字典代替列表。如果您的代码的其他部分需要一个列表,很容易添加 属性 仅公开 Values

public class AdditionManager<T> where T : IAddable
{
    private Dictionary<int,T> addables;

    public AdditionManager()
    {
        addables = new Dictionary<int,T>();
    }

    public void Add(T _addable)
    {
        if (!addables.ContainsKey(_addable.Data.RandomIntValue))
        {
            addables.Add(_addable.Data.RandomIntValue, _addable);
        }
    }

    public Dictionary<int,T>.ValueCollection Values => _addables.Values;
}

您的问题似乎确实与缺少 IEqualityComparer 有关。

想象一下:

class TestClass
{
    public int x;
}

class Program
{
    static void Main(string[] args)
    {
        TestClass nine = new TestClass() { x = 9 };
        TestClass twelve = new TestClass() { x = 12 };
        TestClass anotherNine = new TestClass() { x = 9 };

        Console.WriteLine(nine == twelve);
        Console.WriteLine(nine == anotherNine);
    }
}

这个程序会输出什么? “令人惊讶”的答案是它输出 False 两次。这是因为对象是相互比较的,而不是对象的成员。要实现实际值比较,通过对象的内容而不是引用来比较对象,您需要考虑很多事情。如果你想真正完整,你需要IComparable,IEquality,GetHashcode等。那里很容易出错。

但是从 C# 9.0 开始,可以使用一种新类型来代替 class。类型是 record。这个新的记录类型默认实现了我提到的所有内容。如果您想走更远的路,我建议您查看新的记录类型及其实际情况。

这意味着您需要做的就是将 TestAddableData 的类型从 class 更改为记录,您应该没问题。