Delphiを使用していると、エラーにつながる興味深い落とし穴が見つかります。 それらのいくつかをタスクの形であなたと共有したいと思います。 数日中に回答を公開しますが、今のところは、自明でない動作を自分で理解してみてください。 興味があれば、猫へようこそ。
タスク1
type IData = interface function Ptr: Pointer; end; procedure AddData(Data1, Data2: IData; out OutData: IData); begin OutData := CreateMerged_IData(Data1.Ptr, Data2.Ptr); // Create_IData IData end; var DataArr: array of IData; procedure AddDataToAll(const AExtraData: IData); var i: Integer; begin if not Assigned(AExtraData) then Exit; for i := 0 to Length(DataArr) - 1 do AddData(DataArr[i], AExtraData, DataArr[i]); end;
そのため、何らかのデータを保存するIDataインターフェイスがあります。 AddData関数は、他の2つに基づいて新しいIDataインスタンスを作成する必要があります。
また、null要素がないDataArr配列があり、ある時点でAddDataToAllを呼び出します。 しかし、この手順は期待どおりに機能しません。 なんで?
タスク2
var FCollection: TDictionary<TObject, Integer>; procedure KillObject(var Obj: TObject); begin if FCollection.ContainsKey(Obj) then begin //DoSomething obj.Free; FCollection.Remove(obj); obj := nil; end; end;
ここで、FCollectionは次のように作成されました。FCollection:= TDictionary <TObject、Integer> .Create; 誰もOnKeyNotifyおよびOnValueNotify通知を設定しません。
Objは常に有効な既存のオブジェクトです。 しかし一方で、このコードは時々クラッシュします。 なんで?
パズルの答え1
ご存知のように、Delphiは自動的にリンクカウンターと連動します。
これは、インターフェイス変数に内部で新しい値を割り当てると、デルファイは次のようなことを行うことを意味します。
// Value := NewValue; // : if Assigned(Pointer(Value)) then Value._Release; if Assigned(Pointer(NewValue)) then NewValue._AddRef; Pointer(Value) := Pointer(NewValue);
次に、関数にパラメーターを渡す方法を見てみましょう。
procedure DoWork(AValue: IUnknown);
AValueは関数内で異なる場合があります。 同時に、値は外部で変更されるべきではありません。 これは、変数を明示的にスタックにコピーして、参照カウントを増やす必要があることを意味します。 実際、関数内でこの変数に新しい値が割り当てられると、._ Releaseが呼び出されます。 また、DoWork内では、終了時にコンパイラがAValue._Releaseを慎重に挿入します。
カウンターを使用した操作の擬似コード:
procedure DoWork(AValue: Pointer); begin try //DoWork implementation finally IUnknown(AValue)._Release; end; end; MyValue._AddRef; DoWork(Pointer(MyValue));
次の場合:
procedure DoWork(const AValue: IUnknown);
コンパイラーは、関数内のAValueの値が変化しないという事実に依存して、参照カウンターで何もしません。 インターフェイスへのポインタは、単にスタックにコピーされます。
擬似コード:
procedure DoWork(const AValue: Pointer); begin //DoWork implementation end; DoWork(Pointer(MyValue));
参照による送信の場合:
procedure DoWork(var AValue: IUnknown);
読み取り可能なデータは関数に渡されますが、外部ではデータを変更できることが期待されます。 ここで、コンパイラは、constの場合のように、参照カウンターを使用してジェスチャーを行いません。
擬似コード:
procedure DoWork(var AValue: Pointer); begin //DoWork implementation end; DoWork(Pointer(MyValue));
ただし、この場合:
procedure DoWork(out AValue: IUnknown);
すべてがvarのように機能しません。 出力パラメータは、初期化されていないデータを関数に転送できることを意味します。 初期化されていないデータの場合、._ Releaseを呼び出すことはできません。 したがって、Delphiを呼び出す前に、必ず._Releaseを挿入し、関数内でこのoutパラメーターを削除します。
カウンターを使用した操作の擬似コード:
procedure DoWork(out AValue: Pointer); begin Pointer(AValue) := nil; // , ._Release //DoWork implementation end; MyValue._Release; DoWork(Pointer(MyValue));
関数の場合:
function DoWork: IUnknown;
コンパイラーは、スタックに一時変数を割り当て、varパラメーターと同様に動作します。
擬似コードは次のとおりです。
procedure DoWork(var Result: Pointer); begin //DoWork implementation end; var FuncResult: IUnknown; Pointer(FuncResult) := nil; DoWork(Pointer(FuncResult)); MyValue := FuncResult;
これで、問題の答えに直接進むことができます。
広告を見てみましょう
procedure AddData(Data1, Data2: IData; out OutData: IData);
ここで、呼び出しの前に、参照カウンターはData1、Data2では増加し、OutDataでは減少します。
私たちの挑戦の場合
DataArr [i]で、カウンターは増減します。 カウンターが最初に減らされてゼロになった場合、オブジェクトは破棄されます。AddData(DataArr[i], AExtraData, DataArr[i]);
ただし、参照カウンターを使用した操作の順序は定義されていません。 したがって、DataArr [i] ._ Releaseが最初に呼び出され、AddData内でData1にアクセスするときに、アドレス00000000でアクセス違反が発生するリスクがあります。
正しい解決策は、AddDataを
function AddData(Data1, Data2: IData): IData;
または、呼び出す前に一時変数を設定します。
問題1の問題の正しい説明についてKemetにおめでとう。AddData(DataArr[i], AExtraData, TmpData); DataArr[i] := TmpData;
パズル2への答え
オブジェクトが最初に破壊され、その後コレクションから削除されるという事実-多くの人が自然に気づきました。 例では実際にこれだけが示されているため、理解できます。 しかし、それは何が問題なのでしょうか? 結局のところ、コードは: var obj: TObject; obj := Nil; FCollection.Add(obj, 13); WriteLn(FCollection.Items[obj]); // 13, .. FCollection.Remove(obj)
非常に機能し、問題はありません。 ハッシュはポインタから取得されているようですが...そうです。
実際、TDictionaryを作成するとき、IEqualityComparerコンパレーターをTDictionaryに渡すか、TDictionaryが既定のコンパレーターを使用します。 タスクは、TDictionaryがパラメーターなしで作成されることを具体的に示しています。つまり、デフォルトのコンパレーターを使用します。 彼を見てみましょう。
Generics.Defaultsで説明されています。 作成時に呼び出されます
function _LookupVtableInfo(intf: TDefaultGenericInterface; info: PTypeInfo; size: Integer): Pointer;
コンパレーターのテーブルからVtableInfoは、info:PTypeInfoに渡される型に応じて、目的の関数ポインターを受け取ります。 オブジェクト(tkClass)の場合、EqualityComparer_Instance_Classへのポインターがテーブルに保存されます。Pointer = @EqualityComparer_Vtable_Class;
EqualityComparer_Vtable_Classは、次のような関数のテーブルです。
EqualityComparer_Vtable_Class: array[0..4] of Pointer = ( @NopQueryInterface, @NopAddref, @NopRelease, @Equals_Class, @GetHashCode_Class );
ここに興味があります:
function Equals_Class(Inst: PSimpleInstance; Left, Right: TObject): Boolean; begin if Left = nil then Result := Right = nil else Result := Left.Equals(Right); end; function GetHashCode_Class(Inst: PSimpleInstance; Value: TObject): Integer; begin if Value = nil then Result := 42 else Result := Value.GetHashCode; end;
ご覧のとおり、nilが関数に渡されると42になります。それ以外の場合は、TObjectのいくつかのメソッドが呼び出されます。 ここにあります:
function Equals(Obj: TObject): Boolean; virtual; function GetHashCode: Integer; virtual;
これらは2つの仮想メソッドです。 これは、nilではないオブジェクトを転送する場合、ポインターの名前が変更されることが保証されることを意味します。
これらの2つの方法は、C#での動作に非常に似ています。 ただし、C#はマネージ言語であり、メモリ内のガベージへの参照を持つことはできません。 したがって、ここでの金種は常に許容されます。 個人的には、この動作をコピーするのは論理的ではないと思います。 これらの2つのメソッドを次のように宣言する方がはるかに正確です。
class function Equals(Instance: TObject; Other: TObject): Boolean; virtual; class function GetHashCode(Instance: TObject): Integer; virtual;
オーバーラップ機能は保持されますが、デフォルトの動作では、ポインターによるハッシュを実装できます。 まあ。
最初にコレクションから削除し、その後でのみデータを破棄する必要があることに同意します。 しかし、実際には重大なエラーが含まれている場合でも、上記のコードは多くの人にとって単純に疑わしいと確信しています。
さらに、TObjectのポインターに従ってハッシュがどのように実装されるかに注目したい(少なくともDelphi 2010では):
function TObject.GetHashCode: Integer; begin Result := Integer(Self); end;
見ての通り、非常に悪いです。 割り当ての場合、整列されたデータ+オブジェクトのサイズに合わせた粒度のデータを受け取る可能性が高く、これにより多くの衝突が発生する可能性があります。