2017-04-05 14 views
5

最近、辞書に既に含まれている項目を正しく検索できなかった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つのレコードの値が等しくないとみなされる可能性があります。

これは設計上のバグまたは動作と考えることができますか?いずれにしても落とし穴です。そのような状況に最適な解決策は何ですか?

回避方法として、Integerの代わりにNativeIntを使用することがありますが、このIntegerタイプは私たちの管理下にありませんでした。

+1

個別の比較対象を作成する必要があります。私の見解は、レコードのデフォルト比較が存在する欠陥IDです。 –

+3

そのため、レコードを初期化するには、デフォルト()を使用する必要があります。 AFAIKは、パディングメモリをゼロにする処理も行うコードを生成します。 –

+0

@Stefan:あなたが正しいかもしれませんが、これはレコードを使用するのが本当に不便ですし、TDictionary がどう扱うのかは私のコントロールの範囲を超えています。 –

答えて

4

これはバグだとは限りません。動作は仕様です。これらの型を理解するための検査やコンパイル時のサポートがなければ、任意の構造化型に対して汎用の比較関数を記述することは困難です。

デフォルトのレコードコンペアラーは、パディングなしで、単純なバイナリ比較を使用して比較できる単純な値型のみを含む型でのみ安全に使用できます。たとえば、浮動小数点型は比較演算子がより複雑なために出力されます。NaNs、負のゼロなどを考えてください。

これを処理する唯一の堅牢な方法は、あなた自身の等価比較子を書くことです。他の人々は、デフォルトではすべてのレコードインスタンスを初期化することを提案していますが、これはそのようなタイプの消費者に重大な負担をかけ、デフォルトの初期化を忘れた場合には不明確で、

TEqualityComparer<T>.Constructを使用して、適切な等価比較者を作成します。これには最低限の定型文が必要です。 2つの匿名メソッド(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; 

私は2つのオーバーロードを提供してきました。あなたの2つのタイプのデフォルト比較者が十分であれば、パラメータなしのオーバーロードを使用できます。さもなければ、タイプに2つの匿名メソッドを付けることができます。

TPairComparer.Construct<Int64, Integer> 

これらの単純なタイプの両方が使用できるデフォルト等号する比較器を持っている:

あなたのタイプのために

、あなたはこのような比較子を取得します。したがって、パラメータなしConstructの過負荷を使用できます。

+0

私は複雑なレコードにはカスタムcomprarerが必要であることに同意します。 2つの基本的な値型のTPairの場合、私はコンパイラまたはRTLからのサポートをもう少し期待していました。私は非常に一般的なので、あなたのソリューションが好きです。 –

3

レコードのデフォルトの比較子は、パディングなしの純粋な値型のレコードに対してのみ機能します。それに頼ることは、一般的には良い考えではありません。正確なハッシングと等価性の比較を必要とするレコードについては、あなた自身の比較者を書く必要があります。

すべてのレコードを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に等しいと一致していませんがつまり、デービッドの答えが示すように、委任された比較演算子を使用することでオーバーヘッドが少なくなり、実際に優先されるはずです。

+0

@DavidHeffernan欠点は明らかでした。私は考えるべきです。私が言ったように、「一般的なオブジェクトにハッシュを組み合わせて書くにはどうすればいいですか」という質問に答えるのではなく、カスタム比較者の定型文をノックする方法の問題に答えることでした。どのような場合でも、それは両方のことを持っています。 ;) –

+1

うん。私はこれらの細部のいくつかが重要だと思います。そして私は、私が感じるコンストラクトをもっと簡潔にする方法を示したかったのです。これのどれも非常に簡潔ではありません。これらのものを型に標準的な方法で取り付けることができれば、全体のパッケージが配送できるようになります。 –

+0

@DavidHeffernan合意。私はこれを短いものに凝縮する目的でここから始めましたが、実際には不可能です。私はいつも委任された比較主体を自分で使いますが、答えはすでに長くなっていました。 –

関連する問題