サービスクラスでRAIIパターンに従おうとしています。つまり、オブジェクトが構築されると、完全に初期化されます。ただし、非同期APIで問題が発生しています。問題のクラスの構造は次のようになります
class ServiceProvider : IServiceProvider // Is only used through this interface
{
public int ImportantValue { get; set; }
public event EventHandler ImportantValueUpdated;
public ServiceProvider(IDependency1 dep1, IDependency2 dep2)
{
// IDependency1 provide an input value to calculate ImportantValue
// IDependency2 provide an async algorithm to calculate ImportantValue
}
}
また、ImportantValue
getterの副作用を取り除き、スレッドセーフにすることも目標としています。
これで、ServiceProvider
のユーザーはそのインスタンスを作成し、ImportantValue
変更のイベントをサブスクライブし、最初のImportantValue
を取得します。そして、ここに初期値の問題があります。 ImportantValue
は非同期で計算されるため、クラスをコンストラクターで完全に初期化することはできません。この値を最初はnullとして使用しても問題ないかもしれませんが、最初に計算される場所が必要です。そのための自然な場所はImportantValue
のゲッターかもしれませんが、私はそれをスレッドセーフで副作用のないものにすることを目標としています。
だから私は基本的にこれらの矛盾に固執しています。私を助けて、いくつかの代替案を提供していただけませんか? Niceの間にコンストラクターで値を初期化することは実際には必要ありませんが、プロパティの副作用やスレッドセーフは必須ではありません。
前もって感謝します。
編集:もう1つ追加します。私はインスタンス化にNinjectを使用していますが、私が理解している限り、バインディングを作成するための非同期メソッドをサポートしていません。コンストラクターでタスクベースの操作を開始するアプローチは機能しますが、その結果を待つことはできません。
つまり私のオブジェクトではなくタスクが返されるため、次の2つのアプローチ(これまでのところ回答として提供されています)はコンパイルされません。
Kernel.Bind<IServiceProvider>().ToMethod(async ctx => await ServiceProvider.CreateAsync())
または
Kernel.Bind<IServiceProvider>().ToMethod(async ctx =>
{
var sp = new ServiceProvider();
await sp.InitializeAsync();
})
単純なバインディングは機能しますが、Stephen Clearyによって提案されたように、コンストラクターで開始された非同期初期化の結果を待っていません。
Kernel.Bind<IServiceProvider>().To<ServiceProvider>();
...そしてそれは私にはよく見えません。
async
構築へのいくつかのアプローチ について説明しているブログ投稿があります。
Reedが説明している非同期ファクトリメソッドをお勧めしますが、それが不可能な場合もあります(依存性注入など)。このような場合、次のような非同期初期化パターンを使用できます。
public sealed class MyType
{
public MyType()
{
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// Asynchronously initialize this instance.
await Task.Delay(100);
}
}
その後、通常どおり型を作成できますが、作成は非同期初期化のみ開始であることに注意してください。タイプを初期化する必要がある場合、コードは次のことを実行できます。
await myTypeInstance.Initialization;
Initialization
がすでに完了している場合、実行は(同期的に)await
を超えて続行されることに注意してください。
実際の 非同期プロパティ、そのためのブログ投稿もあります。 あなたの状況はAsyncLazy<T>
の恩恵を受ける可能性があるようです:
public sealed class MyClass
{
public MyClass()
{
MyProperty = new AsyncLazy<int>(async () =>
{
await Task.Delay(100);
return 13;
});
}
public AsyncLazy<int> MyProperty { get; private set; }
}
考えられるオプションの1つは、コンストラクターを使用する代わりに、これをファクトリメソッドに移動することです。
ファクトリメソッドはTask<ServiceProvider>
を返すことができます。これにより、初期化を非同期で実行できますが、ServiceProvider
が(非同期に)計算されるまで、構築されたImportantValue
は返されません。
これにより、ユーザーは次のようなコードを記述できます。
var sp = await ServiceProvider.CreateAsync();
int iv = sp.ImportantValue; // Will be initialized at this point
私の AsyncContainer IoCコンテナを使用できます。これはあなたとまったく同じシナリオをサポートします。
また、非同期初期化子、実行時条件付きファクトリ、非同期および同期ファクトリ関数に依存するなど、他の便利なシナリオもサポートします。
//The email service factory is an async method
public static async Task<EmailService> EmailServiceFactory()
{
await Task.Delay(1000);
return new EmailService();
}
class Service
{
//Constructor dependencies will be solved asynchronously:
public Service(IEmailService email)
{
}
}
var container = new Container();
//Register an async factory:
container.Register<IEmailService>(EmailServiceFactory);
//Asynchronous GetInstance:
var service = await container.GetInstanceAsync<Service>();
//Safe synchronous, will fail if the solving path is not fully synchronous:
var service = container.GetInstance<Service>();
これは、非同期初期化の@StephenClearyパターンに対するわずかな変更です。
呼び出し元であるという違いは、await
InitializationTask
を「覚えている」必要はなく、initializationTask
について何も知る必要はありません(実際、現在はプライベートに変更されています) 。
それが機能する方法は、初期化されたデータを使用するすべてのメソッドで、await _initializationTask
への最初の呼び出しがあるということです。 _initializationTask
オブジェクト自体にブールセット(「await」メカニズムがチェックするIsCompleted
)があるため、これは2回目に即座に戻ります。したがって、複数回初期化されることを心配する必要はありません。
私が知っている唯一の落とし穴は、データを使用するすべてのメソッドでそれを呼び出すことを忘れてはならないということです。
public sealed class MyType
{
public MyType()
{
_initializationTask = InitializeAsync();
}
private Task _initializationTask;
private async Task InitializeAsync()
{
// Asynchronously initialize this instance.
_customers = await LoadCustomersAsync();
}
public async Task<Customer> LookupCustomer(string name)
{
// Waits to ensure the class has been initialized properly
// The task will only ever run once, triggered initially by the constructor
// If the task failed this will raise an exception
// Note: there are no () since this is not a method call
await _initializationTask;
return _customers[name];
}
// one way of clearing the cache
public void ClearCache()
{
InitializeAsync();
}
// another approach to clearing the cache, will wait until complete
// I don't really see a benefit to this method since any call using the
// data (like LookupCustomer) will await the initialization anyway
public async Task ClearCache2()
{
await InitializeAsync();
}
}
私はこれが古い質問であることを知っています、しかしそれはグーグルに現れる最初のものであり、そして率直に言って、受け入れられた答えは悪い答えです。 await演算子を使用できるようにするためだけに、遅延を強制しないでください。
初期化メソッドへのより良いアプローチ:
private async Task<bool> InitializeAsync()
{
try{
// Initialize this instance.
}
catch{
// Handle issues
return await Task.FromResult(false);
}
return await Task.FromResult(true);
}
これは非同期フレームワークを使用してオブジェクトを初期化しますが、ブール値を返します。
なぜこれがより良いアプローチなのですか?まず、IMHOが非同期フレームワークを使用する目的を完全に無効にするようなコードの遅延を強制していません。第二に、非同期メソッドから何かを返すことは経験則です。このようにして、非同期メソッドが実際に機能したかどうか、または想定どおりに機能したかどうかがわかります。 Taskだけを返すことは、非同期メソッドでvoidを返すことと同じです。