由于对齐,TEqualityComparer <T>可能因记录而失败

Joa*_*der 6 delphi hash

我们最近遇到了一个问题,TDictionary<T>即无法正确查找已包含在字典中的项目.该问题仅发生在64位版本中.我能够将问题分解为这段代码:

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;
Run Code Online (Sandbox Code Playgroud)

断言在Win64版本中失败.由于记录对齐,似乎会出现问题:此TPair的大小为16个字节,但只有12个字节用数据填充.该TEqualityComparer然而通吃16个字节考虑.所以2个记录值可能被视为不相等,尽管所有成员都相等,只是因为内存的先前内容不同

这可以被视为设计中的错误或行为吗?无论如何,这是一个陷阱.这种情况的最佳解决方案是什么?

作为解决方法,可以使用NativeInt而不是Integer,但是这种Integer类型不在我们的控制之下.

Dav*_*nan 5

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

默认记录比较器只能安全地用于没有填充的类型,并且只包含可以使用朴素二进制比较进行比较的某些简单值类型.例如,浮点类型已经输出,因为它们的比较运算符更复杂.想想NaNs,负零等等.

我认为解决这个问题的唯一有效方法是编写自己的相等比较器.其他人建议默认初始化所有记录实例,但这给这类类型的消费者带来了沉重的负担,并且在某些代码忘记默认初始化的情况下,存在模糊和难以追踪缺陷的风险.

我会TEqualityComparer<T>.Construct用来创建合适的相等比较器.这需要最少量的样板.您提供了两个匿名方法:一个等于函数和一个哈希函数,并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;
Run Code Online (Sandbox Code Playgroud)

我提供了两个重载.如果两种类型的默认比较器足够,则可以使用无参数过载.否则,您可以提供两种匿名方法来定制类型.

对于您的类型,您将获得这样的比较器:

TPairComparer.Construct<Int64, Integer>
Run Code Online (Sandbox Code Playgroud)

这两种简单类型都有可以使用的默认相等比较器.因此Construct可以使用无参数过载.