我试图从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
时,x
和y
才会被认为是重复的)。
例如,你的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()
起作用的键的属性是不可变的,这一点非常重要。如果你把一个元素放到哈希容器中,然后改变其中任何一个属性,就会发生糟糕的事情。
根据定义,哈希包含的信息比原始哈希少,并且会导致冲突。使用它作为字典键会导致错误。
从注释来看,似乎真正的问题是使用组合键。您可以使用任何使用值相等的类型。两个选项是ValueTuple
s和record
,例如:
.ToDictionary(d=>(d.Id,d.Author,d.Activity,d.DateTime));
一个可能的问题是ValueTuple
是可变的。
可以使用record
或record 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(...)
更改为其他内容,这取决于您如何在数据库中存储这些值。
散列时会丢失信息。
你从拥有不同类型的多个属性(字符串,日期时间,数字等),并将其减少到一个整数。对于两组不同的属性值,哈希函数很可能返回相同的结果。
GetHashCode
不是唯一的键。
相反,为每行生成一个唯一的键可能是一个更好的主意(使用Guid
之类的东西)。或者,也许,使用您似乎已经拥有的Id属性?