web-dev-qa-db-fra.com

Les constructeurs peuvent-ils être asynchrones?

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.

240
Marty Neal

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.

185
svick

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;   
    }
   }  
202
Pierre Poliakoff

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);
}
41
Harald Coppoolse

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é. :)

4
Dmitry Shechtman

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.

2
tsemer

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.

2
Emir Akaydın
1
Tealc Wu

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.

0
johnsmith