2014-10-02 14 views
7

ten reprodukuje problem:TDictionary Hashing jest podzielony na zapisy ciągów

program Project1; 

{$APPTYPE CONSOLE} 

uses 
    Generics.Collections; 
type 
    TStringRec = record 
    s1 : string; 
    s2 : string; 
    end; 
    TGetHash<TKey,TValue> = class(TEnumerable<TPair<TKey,TValue>>) 
    public 
    type 
     TItem = record 
     HashCode: Integer; 
     Key: TKey; 
     Value: TValue; 
     end; 
     TItemArray = array of TItem; 
    public 
    FItems: TItemArray; 
    end; 
var 
    LCrossRef : TDictionary<TStringRec, integer>; 
    LRec : TStringRec; 
    i : integer; 
begin 
    LCrossRef := TDictionary<TStringRec, integer>.Create(); 
    LRec.s1 := 'test1'; 
    LRec.s2 := 'test2'; 
    LCrossRef.Add(LRec, 1); 
    LRec.s1 := 'test1'; 
    LRec.s2 := 'test2'; 
    if LCrossRef.TryGetValue(LRec, i) then begin 
    writeln('ok'); 
    end else begin 
    LCrossRef.Add(LRec, 1); 
    for i := Low(TGetHash<TStringRec, integer> 
       (LCrossRef).FItems) 
      to High(TGetHash<TStringRec, integer> 
       (LCrossRef).FItems) do 
     WriteLn(TGetHash<TStringRec, integer>(LCrossRef).FItems[i].HashCode); 
    WriteLn('not ok'); 
    end; 
    ReadLn; 
end. 

Słownik nie odzyskać pozycję i generuje inny HashCode dla rekordów zawierających identyczne ciągi.

ta jest częściowo zauważyć w QC-#122791 ale obejście używać suchy rekordy nie działa dla zapisów ciągów (przynajmniej powyższy przykład nie również kiedy TStringRec jest zadeklarowana jako packed record).

Czy istnieje rozsądne obejście tego problemu?

Moja obecna strategia polega na łączeniu łańcuchów, które w innym przypadku byłyby zapisane w nagraniu i użycia zamiast tego wartości TDictionary<string, TValue>, ale jest to naturalnie niesatysfakcjonujące.

+0

Co powiesz na używanie TObjectDictionary z niestandardowym narzędziem IEqualityComparer, które implementuje GetHashCode dla określonego typu? –

+0

@VilleKrumlinde Nie masz na myśli 'TObjectDictionary'. Nie ma tu własności obiektu. –

+0

Wdrażanie 'IEqualityComparer' jest zdecydowanie drogą do zrobienia. –

Odpowiedz

6

Jest to znane ograniczenie, to jest zgodnie z projektem. Domyślne porównywarki i nagłówki dla rekordów są przeznaczone wyłącznie dla rekordów typu czystej wartości oraz dla rekordów bez dopełnienia.

Projektanci mogli wybrać RTTI do porównywania/hash rekordów. Jednak zdecydowali się tego nie robić. Niektóre oczywiste powody tego wyboru są następujące:

  1. Nie chciały wymusić użycia RTTI na odreagowaniu.
  2. Występuje znaczny hit związany z wydajnością spowodowany używaniem RTTI.

Sposobem na poradzenie sobie z tym problemem jest dostarczenie własnych komparatorów i mieszaczy podczas korzystania z ogólnych kolekcji.

Twoja obecna strategia łączenia łańcuchów nie będzie działać. Rozważmy 'a' i 'aa', a następnie 'aa' i 'a'. Aby użyć podejścia opartego na tekście, należy przekształcić rekord na, na przykład, JSON.

+0

Zgadzam się, że ma to sens w odniesieniu do typów referencji w ogóle.Będąc specjalnym i zarządzanym przez kompilator, jak napisy byłyby jednak, spodziewałbym się, że przynajmniej zostaną domyślnie zakodowane w sposób rozsądny (nie wydaje się to nadzwyczajne lub niemożliwe). Niestandardowe porównywarki i przetaktowania. Zgadzam się, że konkatenacja jest strategią śmieci - w moim przypadku był to hak, który z natury specyficznego problemu praktycznie nie ma konfliktu w sposób, który zauważyłeś. To było tymczasowe obejście, dopóki nie zdecydowałem się na lepszą strategię. –

+0

W jaki sposób zostanie to wdrożone? System musiałby być w stanie generować porównywarki oparte na RTTI. Nie trywialne, sądzę. –

+0

Być może. Przyznaję, że nie przemyślałam tego z żadnym rygorem. W każdym razie kwestia jest dyskusyjna. Musi to być inne rozwiązanie. –

2

Aby rozwinąć odpowiedź Dawida na przykładzie z mojej bazy kodów. Mam słownika

Records: TDictionary<TGazetteerRecord,TGazetteerRecord> 

który jest tworzony

Records := TDictionary<TGazetteerRecord,TGazetteerRecord>.Create(InitCapacity, TGazRecordComparer.Create); 

Co sprawia, że ​​ta praca jest posiadanie własnego porównywarka w budowie słownika.

TGazRecordComparer = class(TEqualityComparer<TGazetteerRecord>) 
private 
public 
    function Equals(const Left, Right: TGazetteerRecord): Boolean; override; 
    function GetHashCode(const Value: TGazetteerRecord): Integer; override; 
end; 

Implementacja Th zastępuje domyślny kod dla typu rekordu. Mój przykład faktycznie używa klasy, a nie rekordu, ale nie rozumiem, dlaczego nie powinno to działać idealnie dobrze z typem rekordu. Zwróć uwagę, że klasa porównująca jest liczona odwołaniem i dlatego zostanie automatycznie usunięta po zniszczeniu słownika.

+1

Nie widzę sensu tego. Gdzie jest implementacja. Wydaje się, że mówisz nam, że zaimplementowałeś porównywarkę.FWIW normalnym sposobem jest wywołanie metody klasy Construct. –