Недавно у нас возникла проблема с экземпляром 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;

Утверждения терпят неудачу в сборках Win64. Кажется, проблема возникает из-за выравнивания записей: размер этого TPair составляет 16 байтов, но только 12 байтов заполнены данными. TEqualityComparer однако учитывает все 16 байтов. Таким образом, 2 значения записи могут рассматриваться как не равные, хотя все члены равны, просто из-за разного предыдущего содержимого памяти

Можно ли это считать ошибкой или поведением по замыслу? Это ловушка в любом случае. Каково лучшее решение для таких ситуаций?

В качестве обходного пути можно использовать NativeInt вместо Integer, однако этот тип Integer не был под нашим контролем.

7
Joachim Marder 5 Апр 2017 в 14:28

2 ответа

Лучший ответ

Я не думаю, что это ошибка. Поведение - это дизайн. Без проверки или, возможно, некоторой поддержки времени компиляции для понимания этих типов трудно написать универсальный компаратор для произвольных структурированных типов.

Сравнитель записей по умолчанию можно безопасно использовать только для типов без заполнения и, содержащих только определенные типы простых значений, которые можно сравнивать с помощью простого двоичного сравнения. Например, типы с плавающей точкой отсутствуют, потому что их операторы сравнения являются более сложными. Думайте о 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;

Я предоставил две перегрузки. Если для сравнения двух типов по умолчанию достаточно сравнения, можно использовать перегрузку без параметров. В противном случае вы можете предоставить два анонимных метода для каждого типа.

Для вашего типа вы бы получили такой компаратор:

TPairComparer.Construct<Int64, Integer>

Оба этих простых типа имеют по умолчанию средства сравнения на равенство, которые вы можете использовать. Следовательно, можно использовать перегрузку Construct без параметров.

5
David Heffernan 5 Апр 2017 в 19:35

Компаратор по умолчанию для записей работает только для записей с чистыми типами значений без заполнения. Полагаться на это, как правило, не очень хорошая идея. Для любых записей, которые требуют точного хеширования и сравнения на равенство, вам действительно нужно написать свои собственные компараторы.

Как уже было отмечено, инициализация всех ваших записей с помощью 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, значение i1 = -1
ключ i2 = 2, значение i2 = -1

Использование сравнения по умолчанию
i1 не равен i2
i1, i2 - хэши не совпадают

Использование пользовательского компаратора
i1 равен i2
i1, i2 - совпадение хэшей

Словарь уже содержит ключ

Тем не менее, как показывает ответ Дэвида, использование делегированного компаратора приведет к меньшим издержкам и должно быть одобрено на практике.

4
J... 5 Апр 2017 в 19:02