標記の件、エンバカデロのサンプルを触ってみているんですが、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;
手元にDelphiがないので予想ですが。
中身が同じでも、インスタンスが違うためにFalseなのでは?
TCityに、Equal演算子(NotEqual演算子も?)をオーバーライドしてやると幸せになるかもです。
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を呼ぶ可能性がある場合は後者をお勧めします。
にしのさん、回答ありがとうございます。
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を渡してやること
は、高度すぎて私には無理でした・・ググってもサンプルが見つからなかったので・・
演算子のオーバーロードでは無理でしたか。
お役に立てず済みません。
英語ですが、サンプルがありました。
http://docwiki.embarcadero.com/CodeExamples/en/Generics_Defaults_TEqualityComparer_(Delphi)
ここでやっているのはString型で、これをTCityとして作ってやればよいかと思います。
TCityをレコードじゃなくてクラスで宣言して Equalsメソッドをオーバーライドするって方法でもいけます。
TCityがレコード型でないと駄目な場合は使えませんが
にしのさん、サンプルありがとうございます。
このように記述するんですね。無知です。
動作は確認できました。
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;
演算子のオーバーロードは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;
>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を指定すれば問題ありません。
auさん、ウェンドレンさん、 何度もありがとうございます。
recordをclassにするだけでContainsValueは思っている動作になりました。
(Equalsメソッドをオーバーライドしてもしなくても)
Equalsメソッドはやはり=演算子とは関係ないんですね。
BobJenkinsHash に関しましては、
http://stackoverflow.com/questions/1611636/how-to-create-a-case-in-sensitive-tequalitycomparer-for-tdictionary
で見つけました。丁寧な解説ありがとうございます。
Delphiの文法については理解が浅くて苦労します。
(classの演算子オーバーロードはやはりできないのかな?)
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;
ツイート | ![]() |