GetHashCode对不同的对象返回相同的int值



我试图从CSV文件导入数据,不幸的是,没有主键可以让我唯一地标识给定的行。所以我创建了一个字典,其中的键是GetHashCode返回给我的值。我之所以使用字典,是因为它的搜索速度比使用linq和where搜索要快得多,并且带有几个属性的条件。

我的GetHashCode重写看起来像这样:

public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + this.Id.GetHashCode();
hash = hash * 23 + this.Author?.GetHashCode() ?? 0.GetHashCode();
hash = hash * 23 + this.Activity?.GetHashCode() ?? 0.GetHashCode();
hash = hash * 23 + this.DateTime?.GetHashCode() ?? 0.GetHashCode();
return hash;
}
}

从DB获取数据后,我做:

.ToDictionary(d => d.GetHashCode());

问题来了,我检查了数据库,当涉及这四个参数时,我没有任何重复。但是当运行导入时,我经常得到一个错误,即给定的键已经存在于字典中,但是如果我下次对相同的数据再次运行导入,一切都会正常运行。

如何修复这个错误?导入应用程序是用。net 5编写的

Id - long

作者,活动-字符串

DateTime - DateTime?

不幸的是,这个ID更像是FK不是唯一的,可能有许多行具有相同的ID,作者,活动,但例如不同的日期时间

GetHashCode()不产生唯一值,因此使用它作为字典中的键可以给您观察到的错误。

您应该为您的键类型实现GetHashCode()IEquatable<T>。然后,只要没有重复条目,就可以安全地将它的实例放入散列容器中。(只有当GetHashCode()的值相同且x.Equals(y)返回true时,xy才会被认为是重复的)。

例如,你的data key类可以是这样的:

public sealed class DataKey : IEquatable<DataKey>
{
public long      Id       { get; }
public string?   Author   { get; }
public string?   Activity { get; }
public DateTime? DateTime { get; }
public DataKey(long id, string? author, string? activity, DateTime? dateTime)
{
Id       = id;
Author   = author;
Activity = activity;
DateTime = dateTime;
}
public bool Equals(DataKey? other)
{
if (other is null)
return false;
if (ReferenceEquals(this, other))
return true;
return Id == other.Id && Author == other.Author && Activity == other.Activity && Nullable.Equals(DateTime, other.DateTime);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is DataKey other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Id.GetHashCode();
hashCode = (hashCode * 397) ^ (Author?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (Activity?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (DateTime?.GetHashCode() ?? 0);
return hashCode;
}
}
}

这是一大堆样板代码。幸运的是,如果您使用的是最新版本的c#/。您可以使用record类型将其简化为:

public sealed record DataKey(
long      Id,
string?   Author,
string?   Activity,
DateTime? DateTime);

record类型为您正确实现IEquatable<T>GetHashCode()(对于特定类型long,string?DateTime?)。

注意上面的两个示例类型都是不可变的。在使用哈希容器时,对GetHashCode()Equals()起作用的键的属性是不可变的,这一点非常重要。如果你把一个元素放到哈希容器中,然后改变其中任何一个属性,就会发生糟糕的事情。

根据定义,哈希包含的信息比原始哈希少,并且会导致冲突。使用它作为字典键会导致错误。

从注释来看,似乎真正的问题是使用组合键。您可以使用任何使用值相等的类型。两个选项是ValueTuples和record,例如:

.ToDictionary(d=>(d.Id,d.Author,d.Activity,d.DateTime));

一个可能的问题是ValueTuple是可变的。

可以使用recordrecord struct创建使用值相等的预定义键类型。

public record ActivityKey( int Id, 
string Author, 
string Activity, 
DateTime DateTime);
...
.ToDictionary(d=>new ActivityKey(d.Id,d.Author,d.Activity,d.DateTime));

看来你可能使用了一种与你需要的不同的哈希方案。

如果您要对进行散列以将行数据表示为唯一值,您可能需要比int更长的内容。

您的GetHashCode()实现看起来不错。然而,它是用于哈希表,而不是代表性的ID哈希,这可能是您想要的。

试试这样写:

public class Record {
public int ID;
public string Author;
public string Activity;
public DateTime? DateTime;

public string GetRowHash() {
var builder = new System.Text.StringBuilder();
builder.Append(this.ID.ToString());
builder.Append(this.Author ?? "");
builder.Append(this.Activity ?? "");
builder.Append(this.DateTime?.ToString() ?? "");

using (var md5 = System.Security.Cryptography.MD5.Create()) {
byte[] buffer = System.Text.Encoding.ASCII.GetBytes(builder.ToString());
byte[] hash = md5.ComputeHash(buffer);
return Convert.ToBase64String(hash);
}
}
}

然后使用GetRowHash()作为您的ID。如果有重复,那是因为行信息是重复的,而不是因为超出了int哈希值。

您可能需要将Convert.ToBase64String(...)更改为其他内容,这取决于您如何在数据库中存储这些值。

顺便说一下,如果你只有4个数据字段,在数据库中比较值(使用SQL)比在代码中更容易(也更快)。一个查找重复项的好查询将更有效地工作。如果您的数据足够干净,您甚至可能会发现直接将CSV加载到表中是一个不错的选择。

散列时会丢失信息。

你从拥有不同类型的多个属性(字符串,日期时间,数字等),并将其减少到一个整数。对于两组不同的属性值,哈希函数很可能返回相同的结果。

GetHashCode不是唯一的键。

相反,为每行生成一个唯一的键可能是一个更好的主意(使用Guid之类的东西)。或者,也许,使用您似乎已经拥有的Id属性?

相关内容

  • 没有找到相关文章

最新更新