web-dev-qa-db-ja.com

async-awaitパターンを使用してオブジェクトを初期化する方法

サービスクラスで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>();

...そしてそれは私にはよく見えません。

17
Haspemulator

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; }
}
42
Stephen Cleary

考えられるオプションの1つは、コンストラクターを使用する代わりに、これをファクトリメソッドに移動することです。

ファクトリメソッドはTask<ServiceProvider>を返すことができます。これにより、初期化を非同期で実行できますが、ServiceProviderが(非同期に)計算されるまで、構築されたImportantValueは返されません。

これにより、ユーザーは次のようなコードを記述できます。

var sp = await ServiceProvider.CreateAsync();
int iv = sp.ImportantValue; // Will be initialized at this point
4
Reed Copsey

私の 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>();
2
Rafael

これは、非同期初期化の@StephenClearyパターンに対するわずかな変更です。

呼び出し元であるという違いは、awaitInitializationTaskを「覚えている」必要はなく、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();
    }
 }
2
Simon_Weaver

私はこれが古い質問であることを知っています、しかしそれはグーグルに現れる最初のものであり、そして率直に言って、受け入れられた答えは悪い答えです。 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を返すことと同じです。

0
John Doeses