Generics.Collections.TDictionary の ContainsValue


ウォレス  2011-02-10 21:04:51  No: 40013

標記の件、エンバカデロのサンプルを触ってみているんですが、ContainsValueの動作がよくわかりません。
教えていただけないでしょうか?

D2010,WinXpSp3です。

uses ・・・Generics.Collections

type
  TCity = record
    Country   : string;
    Latitude  : Integer;
    Longitude : Integer;
  end;

procedure TForm1.Button1Click(Sender: TObject);

var
  Dictionary : TDictionary<String, TCity>;
  City,City2 : TCity;
  exist      : boolean;
begin

  Dictionary := TDictionary<String, TCity>.Create;
  Dictionary.Clear;

  Button1.Caption := Dictionary.ToString;

  City.Country   := 'イギリス';
  City.Latitude  := 515;
  City.Longitude := -17;

  Dictionary.Add('ロンドン', City);
  exist := Dictionary.ContainsValue(City);
  ShowMessage(BoolToStr(exist, true));
  // False・・・なぜ??

  City2 :=Dictionary.Items['ロンドン'];
  exist := Dictionary.ContainsValue(City2);
  ShowMessage(BoolToStr(exist, true));
  // True

  if ( City.Country   = City2.Country   ) and
     ( City.Latitude  = City2.Latitude  ) and
     ( City.Longitude = City2.Longitude )
  then
    ShowMessage('同じ')
  else
    ShowMessage('違う');
  //同じ

  Dictionary.Destroy;

end;


にしの  2011-02-10 21:41:16  No: 40014

手元にDelphiがないので予想ですが。
中身が同じでも、インスタンスが違うためにFalseなのでは?
TCityに、Equal演算子(NotEqual演算子も?)をオーバーライドしてやると幸せになるかもです。


ウェンドレン  2011-02-11 02:20:35  No: 40015

AddでTDictionary内に格納される際、Cityそのものではなくそのコピーが格納されます。
この時点でCityとDictionary['ロンドン']は別物になります。
一方で、City2はDictionary['ロンドン']そのものが返されるため同一になります。

また、TDictionaryのTValueの比較は外部から比較方法を用意する手段がなく、
自動的にTEqualityComparer<TValue>.Defaultが比較関数として使用されます。
今回のrecordの場合は内容をバイナリとしてみた単純比較になりますので、
内部的には単なるポインタであるstringを比較した場合、
中身の文字列が同じであろうとポインタの指す場所が違えばFalseが返ります。
この処理内ではオーバーライドされた演算子を使用しませんので、
オーバーライドしても結果は変わりません。

これを解決する方法はいくつかあります。
stringの部分を静的なChar配列に変更することでも期待した値が返りますし、
TCityをキーとしたTDictionaryをもう一つ作り、
Create時の引数でComparerを渡してやることでも解決可能です。
ただし、ハッシュ値で高速に検索可能なContainsKeyに対し、
ContainsValueはforで順番に調べているだけなので低速です。
頻繁にContainsValueを呼ぶ可能性がある場合は後者をお勧めします。


ウォレス  2011-02-15 21:38:49  No: 40016

にしのさん、回答ありがとうございます。

type
  TCity = record
    public
      Country   : string;
      Latitude  : Integer;
      Longitude : Integer;

      class operator Equal ( A, B : TCity ) : Boolean;
      class operator NotEqual( A, B : TCity ) : Boolean;

  end;

class operator TCity.Equal(A, B: TCity): Boolean;
begin
   Result := false;
   if (A.Country  = B.Country  ) and
      (A.Latitude = B.Latitude ) and
      (A.Longitude= B.Longitude) then
      Result := True;

end;

class operator TCity.NotEqual(A, B: TCity): Boolean;
begin
   Result := True;
   if (A.Country  = B.Country  ) and
      (A.Latitude = B.Latitude ) and
      (A.Longitude= B.Longitude) then
      Result := false;

end;

とすると、

if City=City2  then  ・・・

のようにできるんですね。(Delphiで演算子のオーバーロードは初めて使いました)
しかしながら、ウェンドレン さんの仰る様に動作は変わりませんでした。

ウェンドレン さん、非常に丁寧な解説ありがとうございます。

>stringの部分を静的なChar配列に変更すること
でできることはわかりました。内部実装はバイナリ比較なんですね。

>Create時の引数でComparerを渡してやること
は、高度すぎて私には無理でした・・ググってもサンプルが見つからなかったので・・


にしの  2011-02-15 21:49:12  No: 40017

演算子のオーバーロードでは無理でしたか。
お役に立てず済みません。

英語ですが、サンプルがありました。
http://docwiki.embarcadero.com/CodeExamples/en/Generics_Defaults_TEqualityComparer_(Delphi)

ここでやっているのはString型で、これをTCityとして作ってやればよいかと思います。


au  2011-02-15 23:01:49  No: 40018

TCityをレコードじゃなくてクラスで宣言して Equalsメソッドをオーバーライドするって方法でもいけます。
TCityがレコード型でないと駄目な場合は使えませんが


ウォレス  2011-02-16 18:33:33  No: 40019

にしのさん、サンプルありがとうございます。
このように記述するんですね。無知です。
動作は確認できました。
GetHashCodeもかならずオーバーライドしないとだめなんですね。(削除するとエラーになる)
GetHashCodeはかなりいい加減に作っても期待通り動作するのがなんとも・・。
ハッシュ値が全部同じになってもなんだか動くんですね・・・?このあたりサッパリわかっておりません。

au さん、回答ありがとうございます。
恥ずかしながらクラスでの演算子オーバーロード(オーバーライド?)の方法がわかりません。Helpだと出来る様に書いてあるんですが。

type
  Myclass = record // class にするとコンパイル不能。recordならOK
  private
    F: Integer;
  public
    class operator Add(a, b: Myclass): Myclass;
    constructor create(I: Integer);
  end;

constructor Myclass.create(I: Integer);
begin

  F := I;
end;

class operator Myclass.Add(a, b: Myclass): Myclass;
begin

  Result.F := a.F + b.F;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Ctest1, Ctest2, Ctest3: Myclass;
begin

  Ctest1 := Myclass.create(1);
  Ctest2 := Myclass.create(2);
  Ctest3 := Myclass.create(0);

  Ctest3 := Ctest1 + Ctest2;

  ShowMessage(IntToStr(Ctest3.F));

end;


au  2011-02-16 19:29:51  No: 40020

演算子のオーバーロードはrecord型でしか出来ないぽいですね。
Dictionaryに入れて検索する目的だとEqualsメソッドてのがTObjectで宣言されてるのでそいつをオーバーライドしてやるです。

+とかの演算子をオーバーロードするならこっちの方法は駄目ですね。

  TCity = class(TObject)
    Country   : string;
    Latitude  : Integer;
    Longitude : Integer;

    function Equals(V: TObject): boolean; override;
  end;

function TCity.Equals(V: TObject): boolean;
var
  V1: TCity;
begin
  if V is TCity then
  begin
    V1 := V as TCity;
    if AnsiSameText(V1.Country, Country)
      And (V1.Latitude = Latitude) And (V1.Longitude = Longitude) then
      Result := True
    else
      Result := False;
  end else
    Result := False;
end;


ウェンドレン  2011-02-17 00:37:51  No: 40021

>GetHashCodeはかなりいい加減に作っても期待通り動作するのがなんとも・・。
>ハッシュ値が全部同じになってもなんだか動くんですね・・・?このあたりサッパリわかっておりません。

TDictionaryの場合、使用するハッシュ値はたかだか32bitですから、
どれだけ高性能なハッシュ関数を使ったところで必ず衝突します。
ですので、衝突した場合は内部的にずらして格納するようにできています。
しかしハッシュ値が衝突すると格納時、読み出し時に多くの無駄な処理が発生し、
適当に書いても動くからと言って疎かにすると、速度が非常に遅くなります。
最悪の場合配列をforで回して探すより遅いですから、TDictionaryを使う意味がなくなりますね。

幸いハッシュ関数は高性能なものがGenerics.Defaultsに実装されていますので、
(function BobJenkinsHash(const Data; Len, InitData: Integer): Integer)
これを使えば高頻度の衝突は避けられると思います。
使い方は、例えば文字列変数 S のハッシュを取る場合、
Result := BobJenkinsHash(S[1], Length(S) * SizeOf(S[1]), 0);
というようにします。
最後の引数は何を指定しても良いのですが、通常は0を指定すれば問題ありません。


ウォレス  2011-02-17 21:20:59  No: 40022

auさん、ウェンドレンさん、 何度もありがとうございます。

recordをclassにするだけでContainsValueは思っている動作になりました。
(Equalsメソッドをオーバーライドしてもしなくても)
Equalsメソッドはやはり=演算子とは関係ないんですね。

BobJenkinsHash  に関しましては、
http://stackoverflow.com/questions/1611636/how-to-create-a-case-in-sensitive-tequalitycomparer-for-tdictionary
で見つけました。丁寧な解説ありがとうございます。

Delphiの文法については理解が浅くて苦労します。
(classの演算子オーバーロードはやはりできないのかな?)


au  2011-02-18 01:22:49  No: 40023

Equalsをオーバーライドしないと、メモリアドレスの比較になってしまってると思いますよ。

例えば、TCityを二つCreateして同じ値をセットして片方をDictionaryに登録した後、もう一方を使って検索した場合に見つからないって事になるかと。

procedure TForm1.Button2Click(Sender: TObject);
var
  Dictionary : TDictionary<String, TCity>;
  City     : TCity;
  City2     : TCity;
  exist      : boolean;
begin
  Dictionary := TDictionary<String, TCity>.Create;
  Dictionary.Clear;

  Button1.Caption := Dictionary.ToString;

  City := TCity.Create;
  City.Country   := 'イギリス';
  City.Latitude  := 515;
  City.Longitude := -17;

  Dictionary.Add('ロンドン', City);

  City2 := TCity.Create;
  City2.Country   := 'イギリス';
  City2.Latitude  := 515;
  City2.Longitude := -17;

  exist := Dictionary.ContainsValue(City2);
  ShowMessage(BoolToStr(exist, true));

  FreeAndNil(City2);
  FreeAndNil(City);

  FreeAndNil(Dictionary);
end;


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加