TEqualityComparer 可能会失败的记录,由于对准
我们最近有一个问题与无法查找正确的项目,都已经包含在字典中的TDictionary<T>
实例。这个问题只发生在64Bit的版本中。我能够将问题分解为以下代码:TEqualityComparer <T>可能会失败的记录,由于对准
var
i1, i2: TPair<Int64,Integer>;
begin
FillMemory(@i1, sizeof(i1), $00);
FillMemory(@i2, sizeof(i1), $01);
i1.Key := 2;
i1.Value := -1;
i2.Key := i1.Key;
i2.Value := i1.Value;
Assert(TEqualityComparer<TPair<Int64,Integer>>.Default.Equals(i1, i2));
Assert(TEqualityComparer<TPair<Int64,Integer>>.Default.GetHashCode(i1) = TEqualityComparer<TPair<Int64,Integer>>.Default.GetHashCode(i2));
end;
Win64构建中的断言失败。这个问题似乎是由于记录对齐而发生的:这个TPair的大小是16个字节,但只有12个字节被数据填充。但TEqualityComparer
需要考虑全部16个字节。所以2记录值可以被视为不相等,尽管所有成员都是平等的,内存
的不同以往的内容是否可以将其看作是设计中的错误或行为只是因为?无论如何这都是一个陷阱。什么是这种情况的最佳解决方案?
作为解决方法,您可以使用NativeInt
而不是Integer
,但此Integer
类型不在我们的控制范围之内。
我不认为这是一个错误。行为是设计的。如果没有检查或者编译时间的支持来理解这些类型,就很难为任意结构类型编写通用比较器。
默认记录比较器只能安全地对类型使用没有填充和包含可使用幼稚二进制比较进行比较只有某些简单值类型。例如,浮点类型因为它们的比较运算符更复杂而不存在。想想NaNs,负零等。
我认为处理这个问题的唯一可靠方法是编写自己的相等比较器。其他人建议默认初始化所有记录实例,但这会给这些类型的消费者带来很大的负担,并且在某些代码忘记默认初始化的情况下存在模糊和难以追踪缺陷的风险。
我会用TEqualityComparer<T>.Construct
来创建合适的相等比较器。这需要最少量的样板。你提供了两个匿名方法:一个equals函数和一个散列函数,并且Construct
返回一个新铸造的比较器。
您可以像这样在一个通用类包装这件事:
uses
System.Generics.Defaults,
System.Generics.Collections;
{$IFOPT Q+}
{$DEFINE OverflowChecksEnabled}
{$Q-}
{$ENDIF}
function CombinedHash(const Values: array of Integer): Integer;
var
Value: Integer;
begin
Result := 17;
for Value in Values do begin
Result := Result*37 + Value;
end;
end;
{$IFDEF OverflowChecksEnabled}
{$Q+}
{$ENDIF}
type
TPairComparer = class abstract
public
class function Construct<TKey, TValue>(
const EqualityComparison: TEqualityComparison<TPair<TKey, TValue>>;
const Hasher: THasher<TPair<TKey, TValue>>
): IEqualityComparer<TPair<TKey, TValue>>; overload; static;
class function Construct<TKey, TValue>: IEqualityComparer<TPair<TKey, TValue>>; overload; static;
end;
class function TPairComparer.Construct<TKey, TValue>(
const EqualityComparison: TEqualityComparison<TPair<TKey, TValue>>;
const Hasher: THasher<TPair<TKey, TValue>>
): IEqualityComparer<TPair<TKey, TValue>>;
begin
Result := TEqualityComparer<TPair<TKey, TValue>>.Construct(
EqualityComparison,
Hasher
);
end;
class function TPairComparer.Construct<TKey, TValue>: IEqualityComparer<TPair<TKey, TValue>>;
begin
Result := Construct<TKey, TValue>(
function(const Left, Right: TPair<TKey, TValue>): Boolean
begin
Result :=
TEqualityComparer<TKey>.Default.Equals(Left.Key, Right.Key) and
TEqualityComparer<TValue>.Default.Equals(Left.Value, Right.Value);
end,
function(const Value: TPair<TKey, TValue>): Integer
begin
Result := CombinedHash([
TEqualityComparer<TKey>.Default.GetHashCode(Value.Key),
TEqualityComparer<TValue>.Default.GetHashCode(Value.Value)
]);
end
)
end;
我提供了两个重载。如果您的两种类型的默认比较器足够,那么您可以使用无参数过载。否则,您可以提供两种匿名方法来定制类型。
你的类型,你会得到这样的比较器:
TPairComparer.Construct<Int64, Integer>
这两种简单的类型都有,你可以使用默认的平等comparers。因此可以使用无参数Construct
过载。
我同意复杂的记录需要一个自定义的编译器。对于两种基本值类型的TPair,我期望得到编译器或RTL的更多支持。我喜欢你的解决方案,因为它非常通用。 –
记录的默认比较器仅适用于没有填充的纯值类型的记录。通常,依靠它不是一个好主意。对于任何需要精确散列和平等比较的记录,你都需要编写自己的比较器。
如前所述,使用Default()
初始化所有记录也是一种选择,但这种方法既乏味又容易出错 - 很容易忘记初始化记录,并且很难在记录时忽略这种遗漏它发生。该方法也仅在与填充消除错误有效而自定义比较还可以处理参考类型等
此,例如,演示了一个工作解决问题的方法:
program Project1;
uses
SysUtils, Windows, StrUtils, Generics.Collections, Generics.Defaults,
System.Hash;
type
TPairComparer<TKey, TValue> = class(TEqualityComparer<TPair<TKey, TValue>>)
public
function Equals(const Left, Right: TPair<TKey, TValue>): Boolean; override;
function GetHashCode(const Value: TPair<TKey, TValue>): Integer; override;
end;
TInt64IntDict<TValue> = class(TDictionary<TPair<Int64, Integer>, TValue>)
public constructor Create;
end;
function TPairComparer<TKey, TValue>.Equals(const Left: TPair<TKey, TValue>;
const Right: TPair<TKey, TValue>) : boolean;
begin
result := TEqualityComparer<TKey>.Default.Equals(Left.Key, Right.Key) and
TEqualityComparer<TValue>.Default.Equals(Left.Value, Right.Value);
end;
{$IFOPT Q+}
{$DEFINE OVERFLOW_ON}
{$Q-}
{$ELSE}
{$UNDEF OVERFLOW_ON}
{$ENDIF}
function TPairComparer<TKey, TValue>.GetHashCode(const Value: TPair<TKey, TValue>) : integer;
begin
result := THashBobJenkins.GetHashValue(Value.Key, SizeOf(Value.Key), 23 * 31);
result := THashBobJenkins.GetHashValue(Value.Value, SizeOf(Value.Value), result * 31);
end;
{$IFDEF OVERFLOW_ON}
{$Q+}
{$UNDEF OVERFLOW_ON}
{$ENDIF}
constructor TInt64IntDict<TValue>.Create;
begin
inherited Create(0, TPairComparer<Int64, Integer>.Create);
end;
var
i1, i2: TPair<Int64, Integer>;
LI64c : TPairComparer<Int64, Integer>;
LDict : TInt64IntDict<double>;
begin
FillMemory(@i1, SizeOf(i1), $00);
FillMemory(@i2, SizeOf(i1), $01);
i1.Key := 2;
i1.Value := -1;
i2.Key := i1.Key;
i2.Value := i1.Value;
WriteLn(Format('i1 key = %d, i1 value = %d', [i1.Key, i1.Value]));
WriteLn(Format('i2 key = %d, i2 value = %d', [i2.Key, i2.Value]));
WriteLn; WriteLn('Using Default comparer');
if TEqualityComparer<TPair<Int64, Integer>>.Default.Equals(i1, i2) then
WriteLn('i1 equals i2') else WriteLn('i1 not equals i2');
if TEqualityComparer<TPair<Int64, Integer>>.Default.GetHashCode(i1) =
TEqualityComparer<TPair<Int64, Integer>>.Default.GetHashCode(i2) then
WriteLn('i1, i2 - hashes match') else WriteLn('i1, i2 - hashes do not match');
WriteLn; WriteLn('Using custom comparer');
LI64c := TPairComparer<Int64, Integer>.Create;
if LI64c.Equals(i1, i2) then
WriteLn('i1 equals i2') else WriteLn('i1 not equals i2');
if LI64c.GetHashCode(i1) = LI64c.GetHashCode(i2) then
WriteLn('i1, i2 - hashes match') else WriteLn('i1, i2 - hashes do not match');
WriteLn;
LDict := TInt64IntDict<double>.Create;
LDict.Add(i1, 1.23);
if LDict.ContainsKey(i2) then
WriteLn('Dictionary already contains key') else
WriteLn('Dictionary does not contain key');
ReadLn;
end.
这产生输出
I1键= 2,I 1值= -1
I2键= 2,I 2值= -1使用默认的比较器
I1不等于I2
I1,I2 - 哈希值不匹配使用自定义比较
I1等于I2
I1,I2 - 哈希匹配字典已经包含了关键
也就是说,正如戴维的答案所表明的那样,使用委托比较器会产生更少的开销,应该在实践中受到青睐。
@DavidHeffernan缺点是显而易见的,我应该这样想。就像我说的,关键不是要回答'我怎样才能为通用对象编写组合散列'这个问题,而是回答如何为自定义比较器打开样板文件的问题。无论如何,无论如何,现在它都有了。 ;) –
是的。我认为其中一些细节很重要。我想展示如何用Construct来做到这一点,我觉得它更简洁。并不是说这些都非常简洁。会很高兴能够以标准方式将这些东西附加到该类型,以便整个包裹可以交付.... –
@DavidHeffernan同意。我从这里开始的目的是将其缩短为一些简短的内容,但实际上这是不可能的。我自己总是使用委托比较器,但答案已经很久了。 –
你必须写一个定制的比较器。我的观点是默认比较存在记录的缺陷ID。 –
这就是为什么你应该使用默认()来初始化记录。 AFAIK它生成的代码也会将填充内存清零。 –
@Stefan:也许你是对的,但这会使记录使用起来很不方便,TDictionary如何处理这些超出了我的控制范围。 –