参照カウントを使用する場合、循環参照を処理するための可能な解決策/手法は何ですか?
最もよく知られている解決策は弱参照を使用することですが、この主題に関する多くの記事は、他の方法もあることを示唆していますが、弱参照の例を繰り返し続けます。これらの他の方法は何ですか?
私は、参照カウントの代替手段は何かを尋ねているのではなく、参照カウントを使用する場合の循環参照の解決策は何かを尋ねています。
この質問は、特定の問題/実装/言語に関するものではなく、一般的な質問です。
参照カウントを使用する場合、循環参照を処理するための可能な解決策/手法は何ですか?
最もよく知られている解決策は弱参照を使用することですが、この主題に関する多くの記事は、他の方法もあることを示唆していますが、弱参照の例を繰り返し続けます。これらの他の方法は何ですか?
私は、参照カウントの代替手段は何かを尋ねているのではなく、参照カウントを使用する場合の循環参照の解決策は何かを尋ねています。
この質問は、特定の問題/実装/言語に関するものではなく、一般的な質問です。
私は何年にもわたって問題をさまざまな方法で調べてきましたが、毎回機能することがわかった唯一の解決策は、循環参照を使用しないように解決策を再構築することです。
編集:
拡大できますか?たとえば、子供が親について知っている/親にアクセスする必要がある場合、親子関係にどのように対処しますか?– OB OB
私が言ったように、あなたがそれらを安全に扱うことができるランタイムを使用していない限り、唯一の良い解決策はそのような構造を避けることです。
とはいえ、子が親について知っているツリー/親子データ構造が必要な場合は、手動で呼び出される独自のティアダウンシーケンス(つまり、実装する可能性のあるデストラクタの外部)を実装する必要があります。ルート(またはプルーニングするブランチ)で、ツリーの深さ優先探索を実行して、葉から参照を削除します。
それは複雑で面倒になるので、IMOの唯一の解決策はそれを完全に回避することです。
これが私が見た解決策です:
各オブジェクトにメソッドを追加して、他のオブジェクトへの参照を解放するように指示します。たとえば、Teardown() と呼びます。
次に、誰が各オブジェクトを「所有」しているかを知る必要があり、オブジェクトの所有者は、処理が完了したときにそのオブジェクトに対して Teardown() を呼び出す必要があります。
A <-> B などの循環参照があり、C が A を所有している場合、C の Teardown() が呼び出されると、A の Teardown が呼び出され、B で Teardown が呼び出され、B が A への参照を解放し、A が解放されます。 B への参照 (B を破棄)、C は A への参照を解放 (A を破棄) します。
弱参照を持つことは 1 つの解決策です。私が知っている唯一の他の解決策は、循環所有参照をまとめて回避することです。オブジェクトへのポインタを共有している場合、これは意味的に、そのオブジェクトを共有方法で所有していることを意味します。この方法だけで共有ポインタを使用すると、循環参照はほとんど得られません。オブジェクトが循環的にお互いを所有することはめったにありませんが、オブジェクトは通常、階層的なツリーのような構造を介して接続されています。これは、次に説明するケースです。
親子関係を持つオブジェクトを持つツリーがある場合、親は子よりも長生きするため、子はその親への所有参照を必要としません。したがって、所有していない未加工のバック ポインターで十分です。これは、要素が配置されているコンテナを指す要素にも適用されます。可能であれば、コンテナーは共有ポインターの代わりに一意のポインターまたは値を使用する必要があります。
相互に乱暴に指す可能性のあるオブジェクトがたくさんあり、一部のオブジェクトに到達できなくなったらすぐにクリーンアップしたい場合は、それらのコンテナーとルート参照の配列を構築して、ガベージを実行することをお勧めします。手動で収集します。
現実の世界では、共有ポインターの実際の使用例は非常に限られていることがわかりました。一意のポインター、生のポインター、または値型だけを使用することをお勧めします。共有ポインタは通常、共有変数を指す参照が複数ある場合に使用されます。共有は摩擦や競合の原因となるため、可能であれば最初から避ける必要があります。一意のポインターと、所有していない生のポインターおよび/または値は、はるかに簡単に推論できます。ただし、共有ポインタが必要になる場合もあります。共有ポインタは、オブジェクトの有効期間を延長するためにも使用されます。通常、これによって循環参照が発生することはありません。
共有ポインタは慎重に使用してください。一意のポインターと、所有していない生のポインターまたはプレーンな値を優先します。共有ポインタは共有所有権を示します。このように使用してください。オブジェクトを階層的に並べます。階層内の同じレベルにある子オブジェクトまたはオブジェクトは、相互またはその親への所有共有参照を使用するべきではありませんが、代わりに非所有生ポインターを使用する必要があります。
ガベージコレクターが使用する別の方法は、「マークアンドスイープ」だと思います。
私が思いついた少し異なる方法を提案したいと思います。正式な名前があるかどうかはわかりません。
テーマ自体のオブジェクトには参照カウンターがありません。代わりに、1 つ以上のオブジェクトのグループには、グループ全体の単一の参照カウンターがあり、グループ内のすべてのオブジェクトの有効期間が定義されます。
同様に、参照はオブジェクトとグループを共有するか、null グループに属します。
オブジェクトへの参照は、(参照が) グループの外部にある場合にのみ、(オブジェクトの) グループの参照カウントに影響します。
2 つのオブジェクトが循環参照を形成する場合、それらを同じグループの一部にする必要があります。2 つのグループが循環参照を作成する場合は、それらを 1 つのグループにまとめる必要があります。
グループが大きいほど、参照の自由度が高くなりますが、グループのオブジェクトは、必要がない間も存続する可能性が高くなります。
周期的なデータを格納する必要がある場合は、スナップショットを文字列に格納します。
循環する可能性のあるオブジェクトに、循環ブール値をアタッチします。
ステップ 1: データを JSON 文字列に解析するときに、使用されていない object.is_cyclic を配列にプッシュし、インデックスを文字列に保存します。(使用済みのオブジェクトは既存のインデックスに置き換えられます)。
ステップ 2: オブジェクトの配列をトラバースし、children.is_cyclic を指定されたインデックスに設定するか、新しいオブジェクトを配列にプッシュします。次に、配列を JSON 文字列に解析します。
注: 新しい循環オブジェクトを配列の最後にプッシュすることにより、すべての循環参照が削除されるまで再帰が強制されます。
ステップ 3: 最後に、両方の JSON 文字列を 1 つの文字列に解析します。
ここにJavaScriptフィドルがあります... https://jsfiddle.net/7uondjhe/5/
function my_json(item) {
var parse_key = 'restore_reference',
stringify_key = 'is_cyclic';
var referenced_array = [];
var json_replacer = function(key,value) {
if(typeof value == 'object' && value[stringify_key]) {
var index = referenced_array.indexOf(value);
if(index == -1) {
index = referenced_array.length;
referenced_array.push(value);
};
return {
[parse_key]: index
}
}
return value;
}
var json_reviver = function(key, value) {
if(typeof value == 'object' && value[parse_key] >= 0) {
return referenced_array[value[parse_key]];
}
return value;
}
var unflatten_recursive = function(item, level) {
if(!level) level = 1;
for(var key in item) {
if(!item.hasOwnProperty(key)) continue;
var value = item[key];
if(typeof value !== 'object') continue;
if(level < 2 || !value.hasOwnProperty(parse_key)) {
unflatten_recursive(value, level+1);
continue;
}
var index = value[parse_key];
item[key] = referenced_array[index];
}
};
var flatten_recursive = function(item, level) {
if(!level) level = 1;
for(var key in item) {
if(!item.hasOwnProperty(key)) continue;
var value = item[key];
if(typeof value !== 'object') continue;
if(level < 2 || !value[stringify_key]) {
flatten_recursive(value, level+1);
continue;
}
var index = referenced_array.indexOf(value);
if(index == -1) (item[key] = {})[parse_key] = referenced_array.push(value)-1;
else (item[key] = {})[parse_key] = index;
}
};
return {
clone: function(){
return JSON.parse(JSON.stringify(item,json_replacer),json_reviver)
},
parse: function() {
var object_of_json_strings = JSON.parse(item);
referenced_array = JSON.parse(object_of_json_strings.references);
unflatten_recursive(referenced_array);
return JSON.parse(object_of_json_strings.data,json_reviver);
},
stringify: function() {
var data = JSON.stringify(item,json_replacer);
flatten_recursive(referenced_array);
return JSON.stringify({
data: data,
references: JSON.stringify(referenced_array)
});
}
}
}
問題を回避するために、私は常に再設計してきました。これが発生する一般的なケースの 1 つは、子が親について知る必要がある親子関係です。これには2つの解決策があります
親をサービスに変換すると、親は子を認識せず、子がなくなるか、メイン プログラムが親の参照を削除すると、親は終了します。
親が子にアクセスする必要がある場合は、オブジェクト ポインターなどの参照カウントされないポインターを受け入れる登録メソッドと、対応する登録解除メソッドを親に用意します。子は register メソッドと unregister メソッドを呼び出す必要があります。親が子にアクセスする必要がある場合、オブジェクト ポインターを参照カウント インターフェイスに型キャストします。
参照カウントを使用する場合、循環参照を処理するために可能な解決策/手法は何ですか?
3 つのソリューション:
サイクル検出器を使用して単純な参照カウントを強化します。ゼロ以外の値にデクリメントされたカウントは、サイクルの潜在的なソースと見なされ、それらの周りのヒープ トポロジでサイクルが検索されます。
mark-sweep のような従来のガベージ コレクタを使用して、素朴な参照カウントを強化します。
プログラムが非循環 (別名、単方向) ヒープのみを生成できるように、言語を制限します。Erlang と Mathematica はこれを行います。
参照をディクショナリ ルックアップに置き換えてから、サイクルを収集できる独自のガベージ コレクタを実装します。
私も、循環参照カウントの問題に対する適切な解決策を探しています。
実績を扱う World of Warcraft から API を借りて盗んでいました。循環参照があることに気付いたとき、暗黙のうちにそれをインターフェイスに変換していました。
注:実績が気に入らない場合は、実績という単語を注文に置き換えることができます。しかし、成果が嫌いな人はいますか?
成果自体があります:
IAchievement = interface(IUnknown)
function GetName: string;
function GetDescription: string;
function GetPoints: Integer;
function GetCompleted: Boolean;
function GetCriteriaCount: Integer;
function GetCriteria(Index: Integer): IAchievementCriteria;
end;
そして、アチーブメントの基準のリストがあります:
IAchievementCriteria = interface(IUnknown)
function GetDescription: string;
function GetCompleted: Boolean;
function GetQuantity: Integer;
function GetRequiredQuantity: Integer;
end;
すべての実績は、次のように中央に登録されますIAchievementController
。
IAchievementController = interface
{
procedure RegisterAchievement(Achievement: IAchievement);
procedure UnregisterAchievement(Achievement: IAchievement);
}
そして、コントローラーを使用して、すべての実績のリストを取得できます。
IAchievementController = interface
{
procedure RegisterAchievement(Achievement: IAchievement);
procedure UnregisterAchievement(Achievement: IAchievement);
function GetAchievementCount(): Integer;
function GetAchievement(Index: Integer): IAchievement;
}
何か興味深いことが起こったときに、システムが を呼び出して、何か興味深いことが起こったことIAchievementController
を通知するという考えがありました。
IAchievementController = interface
{
...
procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
}
イベントが発生すると、コントローラーは各子を繰り返し処理し、独自のNotify
メソッドを介してイベントを通知します。
IAchievement = interface(IUnknown)
function GetName: string;
...
function GetCriteriaCount: Integer;
function GetCriteria(Index: Integer): IAchievementCriteria;
procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
end;
Achievement
オブジェクトがイベントに関心があると判断した場合、オブジェクトはその子基準を通知します。
IAchievementCriteria = interface(IUnknown)
function GetDescription: string;
...
procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
end;
これまで、依存関係グラフは常にトップダウンでした:
IAchievementController --> IAchievement --> IAchievementCriteria
しかし、アチーブメントの基準が満たされるとどうなるでしょうか? Criteria
オブジェクトは親に「達成:
IAchievementController --> IAchievement --> IAchievementCriteria
^ |
| |
+----------------------+
Criteria
親への参照が必要であることを意味します。現在お互いを参照しているユーザー -メモリ リーク。
アチーブメントが最終的に完了したら、ビューを更新できるように親コントローラーに通知する必要があります。
IAchievementController --> IAchievement --> IAchievementCriteria
^ | ^ |
| | | |
+----------------------+ +----------------------+
Controller
とその子がAchievements
相互に循環参照するようになりました -より多くのメモリ リークが発生します。
おそらく、オブジェクトは代わりに に通知し、その親への参照を削除できると思いました。しかし、まだ循環参照があり、時間がかかります: Criteria
Controller
IAchievementController --> IAchievement --> IAchievementCriteria
^ | |
| | |
+<---------------------+ |
| |
+-------------------------------------------------+
現在、World of Warcraft API はオブジェクト指向に適していません。ただし、循環参照は解決します。
への参照を渡さないでくださいController
。単一のグローバルなシングルトンController
クラスを持ちます。そうすれば、アチーブメントはコントローラーを参照する必要はなく、使用するだけです。
短所:既知のグローバル変数が必要なため、テストとモックが不可能になります。
実績は基準のリストを認識していません。が必要な場合はCriteria
、次のAchievement
ように尋ねController
ます。
IAchievementController = interface(IUnknown)
function GetAchievementCriteriaCount(AchievementGUID: TGUID): Integer;
function GetAchievementCriteria(Index: Integer): IAchievementCriteria;
end;
短所:は基準を持たないため、 にAchievement
通知を渡すことを決定できなくなります。に登録する必要があります。Criteria
Criteria
Controller
Criteria
が完了すると、 に通知され、 はに次のようController
に通知しAchievement
ます。
IAchievementController-->IAchievement IAchievementCriteria
^ |
| |
+----------------------------------------------+
短所:頭が痛くなる。
Teardown
システム全体を恐ろしく乱雑な API に再構築する方法がはるかに望ましいと確信しています。
しかし、あなたが思うように、もっと良い方法があるかもしれません。
これを歩き回るために私が知っているいくつかの方法があります:
最初の(そして好ましいもの)は、単に共通のコードを3番目のアセンブリに抽出し、両方の参照にその1つを使用させることです。
2つ目は、「プロジェクト参照」ではなく「ファイル参照」(dll)として参照を追加することです。
お役に立てれば