给定存储 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 true
或 false
表示是否添加`
因此,如果 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
。这个新的记录类型默认实现了我提到的所有内容。如果您想走更远的路,我建议您查看新的记录类型及其实际情况。
这意味着您需要做的就是将 TestAddable
和 Data
的类型从 class 更改为记录,您应该没问题。
这是我试过的代码,但它不止一次添加了同一个对象:
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 true
或 false
表示是否添加`
因此,如果 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
。这个新的记录类型默认实现了我提到的所有内容。如果您想走更远的路,我建议您查看新的记录类型及其实际情况。
这意味着您需要做的就是将 TestAddable
和 Data
的类型从 class 更改为记录,您应该没问题。