J'ai un projet dans lequel j'essaye de renseigner des données dans un constructeur:
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;
}
}
Malheureusement, je reçois une erreur:
Le modificateur
async
n'est pas valide pour cet élément
Bien sûr, si j’enveloppe une méthode standard et l’appelle du constructeur:
public async void Foo()
{
Data = await GetDataTask();
}
ça fonctionne bien. De même, si j'utilise l'ancienne méthode à l'envers
GetData().ContinueWith(t => Data = t.Result);
Cela fonctionne aussi. Je me demandais simplement pourquoi nous ne pouvons pas appeler directement await
depuis un constructeur. Il y a probablement beaucoup de cas Edge (même évidents) et de raisons qui le contredisent. J'ai aussi cherché une explication, mais je n'arrive pas à en trouver.
Le constructeur agit de manière très similaire à une méthode renvoyant le type construit. Et la méthode async
ne peut pas renvoyer n'importe quel type, elle doit être soit “feu et oublier” void
, soit Task
.
Si le constructeur de type T
renvoie effectivement Task<T>
, ce serait très déroutant, à mon avis.
Si le constructeur asynchrone se comporte de la même manière qu'une méthode async void
, ce type de rupture rompt ce que le constructeur est censé être. Après le retour du constructeur, vous devriez obtenir un objet entièrement initialisé. Ce n'est pas un objet qui sera réellement correctement initialisé à un moment indéfini dans le futur. Autrement dit, si vous avez de la chance et que l'initialisation asynchrone n'échoue pas.
Tout cela n'est qu'une supposition. Mais il me semble qu'avoir la possibilité d'un constructeur asynchrone crée plus de problèmes que cela n'en vaut la peine.
Si vous voulez réellement utiliser la sémantique "feu et oublier" des méthodes async void
(à éviter si possible), vous pouvez facilement encapsuler tout le code dans une méthode async void
et l'appeler depuis votre constructeur, comme vous l'avez mentionné dans la question.
Puisqu'il n'est pas possible de créer un constructeur asynchrone, j'utilise une méthode asynchrone statique qui renvoie une instance de classe créée par un constructeur privé. Ce n'est pas élégant mais ça fonctionne bien.
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;
}
}
Votre problème est comparable à la création d’un objet fichier et à l’ouverture du fichier. En fait, il existe de nombreuses classes dans lesquelles vous devez effectuer deux étapes avant de pouvoir utiliser l'objet: create + Initialize (souvent appelé quelque chose de similaire à Open).
L'avantage de ceci est que le constructeur peut être léger. Si vous le souhaitez, vous pouvez modifier certaines propriétés avant d’initialiser réellement l’objet. Lorsque toutes les propriétés sont définies, la fonction Initialize
/Open
est appelée pour préparer l'objet à utiliser. Cette fonction Initialize
peut être asynchrone.
L'inconvénient est que vous devez faire confiance à l'utilisateur de votre classe qu'il appellera Initialize()
avant d'utiliser toute autre fonction de votre classe. En fait, si vous voulez que votre classe soit à l’épreuve de la preuve totale, vous devez vérifier dans chaque fonction que la Initialize()
a été appelée.
Pour rendre cela plus facile, le modèle consiste à déclarer le constructeur privé et à créer une fonction statique publique qui va construire l'objet et appeler Initialize()
avant de renvoyer l'objet construit. De cette façon, vous saurez que tous ceux qui ont accès à l'objet ont utilisé la fonction Initialize
.
L'exemple montre une classe qui imite le constructeur async souhaité
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);
}
L'utilisation sera comme suit:
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);
}
Dans ce cas particulier, un viewModel est requis pour lancer la tâche et notifier la vue lorsqu'elle est terminée. Une "propriété async", pas un "constructeur async", est en ordre.
Je viens de publier AsyncMVVM , ce qui résout exactement ce problème (entre autres). Si vous l'utilisez, votre ViewModel deviendra:
public class ViewModel : AsyncBindableBase
{
public ObservableCollection<TData> Data
{
get { return Property.Get(GetDataAsync); }
}
private Task<ObservableCollection<TData>> GetDataAsync()
{
//Get the data asynchronously
}
}
Etrangement, Silverlight est supporté. :)
Je me demandais simplement pourquoi nous ne pouvons pas appeler directement
await
depuis un constructeur.
Je crois que la réponse courte est simplement: Parce que l’équipe .Net n’a pas programmé cette fonctionnalité.
Je crois qu'avec la bonne syntaxe, cela pourrait être mis en œuvre et ne devrait pas être trop déroutant ou sujet aux erreurs. Je pense que article de blog de Stephen Cleary et plusieurs autres réponses ici ont implicitement souligné qu'il n'y avait aucune raison fondamentale de s'y opposer, et plus que cela - résolu ce manque avec des solutions de contournement. L'existence de ces solutions de contournement relativement simples est probablement l'une des raisons pour lesquelles cette fonctionnalité n'a pas (encore) été implémentée.
si vous rendez le constructeur asynchrone, après la création d'un objet, vous risquez de rencontrer des problèmes tels que des valeurs null au lieu d'objets d'instance. Par exemple;
MyClass instance = new MyClass();
instance.Foo(); // null exception here
C'est pourquoi ils ne permettent pas cela, je suppose.
appeler async dans le constructeur peut provoquer un blocage, veuillez vous référer à http://social.msdn.Microsoft.com/Forums/en/winappswithcsharp/thread/0d24419e-36ad-4157-abb5-3d9e6c5dacf1
http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115163.aspx
Certaines des réponses impliquent la création d'une nouvelle méthode public
. Sans cela, utilisez la classe 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();
}
}
Pour utiliser Data
, utilisez Data.Value
.