次のシナリオがあります。
この動作の標準的な設計/概念はありますか?
私はそれを「無限」 Semaphore であると考えましたが、それはその逆バージョンのように機能します。 C#に CountdownEvent Class が含まれていることを確認しましたが、必要以上のようです。
公開インターフェース:
public class ComponentA
{
public ComponentA();
public bool IsBlocked();
public Block RequestBlock();
}
public class Block()
{
public void Release();
}
使用法:
ComponentA myComponent = new ComponentA();
myComponent.IsBlocked(); // false
Block firstBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true
Block secondBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true
firstBlock.Release();
myComponent.IsBlocked(); // true
secondBlock.Release();
myComponent.IsBlocked(); // false
実装の詳細:
public class ComponentA
{
private int _numBlocks;
public ComponentA() {
this._numBlocks = 0;
}
public bool IsBlocked() {
return _numBlocks > 0;
}
public Block RequestBlock() {
this._numBlocks++;
return new Block(() => { this._numBlocks--; });
}
}
public class Block
{
private Action _unBlockAction;
internal Block(Action unblockAction) {
this._unBlockAction = unblockAction;
}
public void Release() {
this._unBlockAction();
}
}
Readers-Writer Lock と記述しているが、書き込みが開始されてスレッドセーフがなくなった後でも、リーダーは読み取ることができます。
プレイヤーがダイアログでインベントリを開くことを許可しないが、インベントリが開いているときにプレイヤーがダイアログに入ることができるようにすることは一貫性がないと思います。ただし、要件を対称にすると、Reader-Writerロックの要件と完全に一致します。
それ自体デザインパターンはないと思います。適切な同期プリミティブを選択するだけです。
呼び出し元をドレインする必要がない場合、新しい呼び出し元が先に進まないようにするだけの場合は、 手動リセットイベント を使用できます。これは空港でのタキシングのようなもので、パイロットはタワーからの許可が必要ですが、地上管制は許可の発行を停止できますが、ゲートに到着するのを待たずに済みます。ブロックを発行するまで待つ必要はありません。
排他的なセマンティクスが必要な場合は、 読み取り/書き込みロック のような共有/排他ロックの取得を使用します。タワーが滑走路の除氷を希望する場合、着陸の許可を与えられたすべての飛行機が実際に着陸するまで待たなければなりません。ブロックを発行するまで待機しています。 (技術的には、読者が流出するのを待つことができませんでしたが、誰がブロック解除を発行しますか?)
つまり、これは State Management であり、これに対処するにはいくつかの方法があります。ユーザーインターフェイスがある世界では、 [〜#〜] mvvm [〜#〜] アーキテクチャが一般的です。
MVVM、C#では、ICommand
インターフェイスを実装して、さまざまなゲームシステムを表す他のすべてのモデルの状態をチェックするために実装されたインベントリウィンドウを開きます。例えば:
_public class InventoryCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object context)
{
var world = context as World;
bool canExecute = world != null;
// do all your checks here on your game state
canExecute = canExecute && ....;
return canExecute;
}
public void Execute(object context)
{
// actually do the thing here
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(EventArgs.Empty);
}
}
_
この段階で残っているのは、世界の状態が気になるように変化したときにRaiseCanExecuteChanged()
メソッドを呼び出すことだけです。 InventoryCommand
は、インベントリを開くコントロール(ボタン、キーの組み合わせなど)にバインドされます。少なくとも、これはMVVMアプローチが機能する方法です。
問題の解決策は、本質的に状態管理を処理する別の方法ですが、 参照カウント と呼ばれる単純なガベージコレクションメカニズムを使用します。
参照をクリーンアップするように強制する方法がないため、実装は何かを望まないままにします。あなたはブロックを適切に解放することを覚えておくためにユーザーの良い恵みに頼っています。また、ユーザーがループで誤ってRelease
メソッドを呼び出さないようにする必要もあります。これらは、参照カウントが最近ガベージコレクションされた言語で一般的に使用されているものではない2つの理由です。
Block
実装を変更してIDisposable
パターンを使用する場合、C#のガベージコレクションとusing
構成を利用してブロックのリリースを自動化できます。
_internal class Block : IDisposable
{
private Action onComplete;
public Block(Action completeAction)
{
onComplete = completeAction;
}
~Block()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // prevent second call in finalizer
}
private void Dispose(bool calledExplicitly)
{
onComplete?.Invoke();
onComplete = null; // clear it to prevent double calls
}
}
_
_ComponentA.RequestBlock
_クラスは次のように変更する必要があります。
_public IDisposable RequestBlock() // or AcquireBlock()
{
this._numBlocks++;
return new Block(() => { this._numBlocks--; });
}
_
それはこのようにそれを使用する正しい方法を残します:
_using(inventory.RequestBlock())
{
// do things, the block is automatically released
// when the using block ends.
}
_
また、プログラマがusing
ステートメントを忘れた場合、Block
は最終的にガベージコレクタによってキャプチャされ、カウンタが減少します。ここでの実装により、アクションが一度だけ呼び出されることが保証されます。