私はコンストラクタでいくつかのデータを投入しようとしているプロジェクトがあります:
public class ViewModel
{
public ObservableCollection<TData> Data { get; set; }
async public ViewModel()
{
Data = await GetDataTask();
}
public Task<ObservableCollection<TData>> GetDataTask()
{
Task<ObservableCollection<TData>> task;
//Create a task which represents getting the data
return task;
}
}
残念ながら、エラーが発生しています。
修飾子
async
はこのアイテムでは無効です
もちろん、標準メソッドでラップし、コンストラクターから呼び出す場合:
public async void Foo()
{
Data = await GetDataTask();
}
正常に動作します。同様に、古いインサイドアウトの方法を使用する場合
GetData().ContinueWith(t => Data = t.Result);
それも機能します。なぜコンストラクタ内から直接await
を呼び出せないのか疑問に思っていました。たぶん、多くの(明白な)エッジのケースとそれに対する理由があります、私はただ考えられません。私も説明を探しましたが、見つけられないようです。
コンストラクターは、構築された型を返すメソッドと非常によく似ています。また、async
メソッドはどの型も返せず、「fire and forget」void
またはTask
でなければなりません。
タイプT
のコンストラクターが実際にTask<T>
を返した場合、それは非常に紛らわしいでしょう。
非同期コンストラクターがasync void
メソッドと同じように動作する場合、その種のコンストラクターの意図が破られます。コンストラクターが戻った後、完全に初期化されたオブジェクトを取得する必要があります。将来の未定義のポイントで実際に適切に初期化されるオブジェクトではありません。つまり、幸運で非同期初期化が失敗しない場合です。
これはすべて推測に過ぎません。しかし、非同期コンストラクタの可能性があることは、それが価値があるよりも多くのトラブルをもたらすように思えます。
実際にasync void
メソッドの「ファイアアンドフォーゲット」セマンティクス(必要に応じて回避する必要があります)が必要な場合は、質問で述べたように、すべてのコードをasync void
メソッドに簡単にカプセル化し、コンストラクターから呼び出すことができます。
非同期コンストラクターを作成することはできないため、プライベートコンストラクターによって作成されたクラスインスタンスを返す静的非同期メソッドを使用します。これはエレガントではありませんが、大丈夫です。
public class ViewModel
{
public ObservableCollection<TData> Data { get; set; }
//static async method that behave like a constructor
async public static Task<ViewModel> BuildViewModelAsync()
{
ObservableCollection<TData> tmpData = await GetDataTask();
return new ViewModel(tmpData);
}
// private constructor called by the async method
private ViewModel(ObservableCollection<TData> Data)
{
this.Data=Data;
}
}
あなたの問題は、ファイルオブジェクトの作成とファイルを開くことに匹敵します。実際、オブジェクトを実際に使用する前に2つの手順を実行する必要があるクラスが多数あります。作成+初期化(多くの場合、Openに似たものと呼ばれます)。
これの利点は、コンストラクターを軽量化できることです。必要に応じて、オブジェクトを実際に初期化する前にいくつかのプロパティを変更できます。すべてのプロパティが設定されると、Initialize
/Open
関数が呼び出され、使用するオブジェクトを準備します。このInitialize
関数は非同期にすることができます。
欠点は、クラスの他の関数を使用する前にInitialize()
を呼び出すクラスのユーザーを信頼する必要があることです。実際、クラスを完全なプルーフ(フールプルーフ?)にしたい場合は、Initialize()
が呼び出されたすべての関数をチェックインする必要があります。
これを簡単にするパターンは、コンストラクターをプライベートに宣言し、オブジェクトを構築し、構築されたオブジェクトを返す前にInitialize()
を呼び出すパブリックな静的関数を作成することです。これにより、オブジェクトにアクセスできるすべての人がInitialize
関数を使用したことがわかります。
この例は、目的の非同期コンストラクターを模倣するクラスを示しています
public MyClass
{
public static async Task<MyClass> CreateAsync(...)
{
MyClass x = new MyClass();
await x.InitializeAsync(...)
return x;
}
// make sure no one but the Create function can call the constructor:
private MyClass(){}
private async Task InitializeAsync(...)
{
// do the async things you wanted to do in your async constructor
}
public async Task<int> OtherFunctionAsync(int a, int b)
{
return await OtherFunctionAsync(a, b);
}
使用方法は次のとおりです。
public async Task<int> SomethingAsync()
{
// Create and initialize a MyClass object
MyClass myObject = await MyClass.CreateAsync(...);
// use the created object:
return await myObject.OtherFunctionAsync(4, 7);
}
この特定のケースでは、タスクを起動し、完了時にビューに通知するためにviewModelが必要です。 「非同期コンストラクター」ではなく「非同期プロパティー」が適切です。
AsyncMVVM をリリースしました。これは(とりわけ)この問題を正確に解決します。使用すると、ViewModelは次のようになります。
public class ViewModel : AsyncBindableBase
{
public ObservableCollection<TData> Data
{
get { return Property.Get(GetDataAsync); }
}
private Task<ObservableCollection<TData>> GetDataAsync()
{
//Get the data asynchronously
}
}
奇妙なことに、Silverlightがサポートされています。 :)
なぜコンストラクタ内から直接
await
を呼び出せないのか疑問に思っていました。
私は簡単な答えは単純だと思います:.Netチームはこの機能をプログラムしていないからです。
私は正しい構文でこれを実装することができ、混乱しすぎたり、エラーが発生したりするべきではないと考えています。 Stephen Clearyの ブログ投稿 と他のいくつかの回答は、それに対する根本的な理由はないことを暗黙のうちに指摘しており、それ以上は回避策でその不足を解決したと思います。これらの比較的単純な回避策の存在は、おそらくこの機能が(まだ)実装されていない理由の1つです。
コンストラクタを非同期にすると、オブジェクトを作成した後、インスタンスオブジェクトではなくnull値などの問題が発生する可能性があります。例えば;
MyClass instance = new MyClass();
instance.Foo(); // null exception here
それが彼らがこれを許可しない理由だと思います。
いくつかの答えには、新しいpublic
メソッドの作成が含まれます。これを行わずに、Lazy<T>
クラスを使用します。
public class ViewModel
{
private Lazy<ObservableCollection<TData>> Data;
async public ViewModel()
{
Data = new Lazy<ObservableCollection<TData>>(GetDataTask);
}
public ObservableCollection<TData> GetDataTask()
{
Task<ObservableCollection<TData>> task;
//Create a task which represents getting the data
return task.GetAwaiter().GetResult();
}
}
Data
を使用するには、Data.Value
を使用します。