このコードの何が問題になっていますか?

画像 みなさんこんにちは。

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では減少します。

私たちの挑戦の場合
 AddData(DataArr[i], AExtraData, DataArr[i]);
      
      



DataArr [i]で、カウンターは増減します。 カウンターが最初に減らされてゼロになった場合、オブジェクトは破棄されます。

ただし、参照カウンターを使用した操作の順序は定義されていません。 したがって、DataArr [i] ._ Releaseが最初に呼び出され、AddData内でData1にアクセスするときに、アドレス00000000でアクセス違反が発生するリスクがあります。

正しい解決策は、AddDataを

 function AddData(Data1, Data2: IData): IData;
      
      



または、呼び出す前に一時変数を設定します。

 AddData(DataArr[i], AExtraData, TmpData); DataArr[i] := TmpData;
      
      



問題1の問題の正しい説明についてKemetにおめでとう。



パズル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;
      
      



見ての通り、非常に悪いです。 割り当ての場合、整列されたデータ+オブジェクトのサイズに合わせた粒度のデータを受け取る可能性が高く、これにより多くの衝突が発生する可能性があります。



All Articles