我有以下来自元组的自定义类:
public class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
}
和HashSet<CustomTuple>
。问题是,当我向集合中添加项目时,它们不会被识别为重复项目。即输出2,但输出1:
void Main()
{
HashSet<CustomTuple> set = new HashSet<CustomTuple>();
var a = new CustomTuple(new List<string>(), new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
Console.Write(set.Count); // Outputs 2
}
如何覆盖Equals和GetHashCode方法以使此代码输出设置数为1?
答案 0 :(得分:1)
您应该覆盖在System.Object类中定义的GetHashCode和Equals虚拟方法。
请记住:
如果两个对象在逻辑上相等&#34;然后他们必须有相同的哈希码!
如果两个对象具有相同的哈希码,则不一定要使对象相等。
另外,我发现代码中存在架构问题: List是一个可变类型,但重写Equals和GetHashCode通常会使您的类在逻辑上表现得像值类型。所以&#34; Item1&#34;一个可变类型,表现得像一个值类型是非常危险的。我建议用ReadOnlyCollection替换你的List。然后你必须创建一个方法来检查两个ReadOnlyCollections是否为Equal。
对于GetHashCode()方法,只需从Item1中找到的所有字符串项组成一个字符串,然后附加一个表示日期时间的哈希代码的字符串,然后最终调用连接结果&#34; GetHashCode()&#34 ;覆盖了字符串方法。通常你会有:
override int GetHashCode () {
return (GetHashCodeForList (Item1) + (Item2 ?? DateTime.MinValue).GetHashCode ()).GetHashCode ();
}
GetHashCodeForList方法将是这样的:
private string GetHashCodeForList (IEnumerable <string> lst) {
if (lst == null) return string.Empty;
StringBuilder sb = new StringBuilder ();
foreach (var item in lst) {
sb.Append (item);
}
return sb.ToString ();
}
最后注意事项:您可以缓存GetHashCode结果,因为它相对昂贵,并且您的整个类将变为不可变(如果您使用只读集合替换List)。
答案 1 :(得分:0)
HashSet<T>
首先会调用GetHashCode
,因此您需要首先处理该问题。有关实施,请参阅以下答案:https://stackoverflow.com/a/263416/1250301
所以一个简单,天真的实现可能看起来像这样:
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + this.Item2.GetHashCode();
foreach (var s in this.Item1)
{
hash = hash * 23 + s.GetHashCode();
}
return hash;
}
}
但是,如果您的列表很长,那么这可能效率不高。所以你必须根据你对碰撞的容忍程度来决定妥协的地方。
如果两个项目的GetHashCode
结果相同,那么,只有这样,它才会调用Equals
。 Equals
的实现需要比较列表中的项目。像这样:
public override bool Equals(object o1)
{
var o = o1 as CustomTuple;
if (o == null)
{
return false;
}
if (Item2 != o.Item2)
{
return false;
}
if (Item1.Count() != o.Item1.Count())
{
return false;
}
for (int i=0; i < Item1.Count(); i++)
{
if (Item1[i] != o.Item1[i])
{
return false;
}
}
return true;
}
请注意,我们先检查日期(Item2
),因为这很便宜。如果日期不一样,我们不打扰其他任何事情。接下来,我们检查两个集合Count
上的Item1
。如果它们不匹配,那么迭代集合是没有意义的。然后我们遍历两个集合并比较每个项目。一旦我们找到一个不匹配的,我们就会返回false
,因为没有必要继续查看。
正如George的回答所指出的那样,你也遇到了一个问题,即你的列表是可变的,这会导致HashSet
出现问题,例如:
var a = new CustomTuple(new List<string>() {"foo"} , new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
// Hashset now has two entries
((List<string>)a.Item1).Add("foo");
// Hashset still has two entries, but they are now identical.
要解决此问题,您需要强制IEnumerable<string>
只读。你可以这样做:
public class CustomTuple : Tuple<IReadOnlyList<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList().AsReadOnly(), time)
{
}
public override bool Equals(object o1)
{
// as above
}
public override int GetHashCode()
{
// as above
}
}
答案 2 :(得分:0)
这就是我的目标,根据需要输出1:
private class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var that = (CustomTuple) obj;
if (Item1 == null && that.Item1 != null || Item1 != null && that.Item1 == null) return false;
if (Item2 == null && that.Item2 != null || Item2 != null && that.Item2 == null) return false;
if (!Item2.Equals(that.Item2)) return false;
if (that.Item1.Count != Item1.Count) return false;
for (int i = 0; i < Item1.Count; i++)
{
if (!Item1[i].Equals(that.Item1[i])) return false;
}
return true;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash*23 + Item2.GetHashCode();
return Item1.Aggregate(hash, (current, s) => current*23 + s.GetHashCode());
}
}