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类型不在我们的控制范围之内。

+1

你必须写一个定制的比较器。我的观点是默认比较存在记录的缺陷ID。 –

+3

这就是为什么你应该使用默认()来初始化记录。 AFAIK它生成的代码也会将填充内存清零。 –

+0

@Stefan:也许你是对的,但这会使记录使用起来很不方便,TDictionary 如何处理这些超出了我的控制范围。 –

我不认为这是一个错误。行为是设计的。如果没有检查或者编译时间的支持来理解这些类型,就很难为任意结构类型编写通用比较器。

默认记录比较器只能安全地对类型使用没有填充和包含可使用幼稚二进制比较进行比较只有某些简单值类型。例如,浮点类型因为它们的比较运算符更复杂而不存在。想想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过载。

+0

我同意复杂的记录需要一个自定义的编译器。对于两种基本值类型的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 - 哈希匹配

字典已经包含了关键

也就是说,正如戴维的答案所表明的那样,使用委托比较器会产生更少的开销,应该在实践中受到青睐。

+0

@DavidHeffernan缺点是显而易见的,我应该这样想。就像我说的,关键不是要回答'我怎样才能为通用对象编写组合散列'这个问题,而是回答如何为自定义比较器打开样板文件的问题。无论如何,无论如何,现在它都有了。 ;) –

+1

是的。我认为其中一些细节很重要。我想展示如何用Construct来做到这一点,我觉得它更简洁。并不是说这些都非常简洁。会很高兴能够以标准方式将这些东西附加到该类型,以便整个包裹可以交付.... –

+0

@DavidHeffernan同意。我从这里开始的目的是将其缩短为一些简短的内容,但实际上这是不可能的。我自己总是使用委托比较器,但答案已经很久了。 –