2013-04-09 39 views
11

Próbuję wykonać wzór RAII w moich klasach usług, co oznacza, że ​​po skonstruowaniu obiektu, jest on w pełni inicjalizowany. Jednak mam problemy z asynchronicznymi interfejsami API. Struktura klasy w pytaniu wygląda poJak zainicjować obiekt przy użyciu wzoru async-await

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 
    } 
} 

Jestem również kierowanie pozbyć się skutków ubocznych w ImportantValue getter, aby thread-safe.

Teraz użytkownicy ServiceProvider utworzą jego instancję, zasubskrybują wydarzenie o ImportantValue zmianie i otrzymają wstępną ImportantValue. I tu pojawia się problem z początkową wartością. Ponieważ ImportantValue jest obliczany asynchronicznie, nie można w pełni inicjalizować klasy w konstruktorze. Może być w porządku, aby początkowo mieć tę wartość jako zerową, ale muszę mieć pewne miejsce, w którym zostanie ona obliczona po raz pierwszy. Naturalnym miejscem na to może być getter z ImportantValue, ale zamierzam uczynić go bezpiecznym dla wątków i bez żadnych skutków ubocznych.

Więc zasadniczo utknąłem z tymi sprzecznościami. Czy możesz mi pomóc i zaoferować alternatywę? Posiadanie wartości zainicjowanej w konstruktorze, gdy jest ładne, nie jest tak naprawdę konieczne, ale żadne skutki uboczne i bezpieczeństwo nici własności nie są obowiązkowe.

Z góry dziękuję.

EDYCJA: Jeszcze jedna rzecz do dodania. Używam programu Ninject do tworzenia wystąpień i, o ile rozumiem, nie obsługuje on metod asynchronicznych w celu utworzenia powiązania. Chociaż podejście z zainicjowaniem niektórych operacji opartych na zadaniach w konstruktorze będzie działać, nie mogę czekać na jego wynik.

tj. Kolejne dwa podejścia (oferowane jako odpowiedzi tak daleko) nie będzie kompilować, ponieważ zadanie jest zwracany, a nie mojego obiektu:

Kernel.Bind<IServiceProvider>().ToMethod(async ctx => await ServiceProvider.CreateAsync()) 

lub

Kernel.Bind<IServiceProvider>().ToMethod(async ctx => 
{ 
    var sp = new ServiceProvider(); 
    await sp.InitializeAsync(); 
}) 

Proste wiązanie będzie działać, ale nie jestem w oczekiwaniu na rezultatem asynchronicznego inicjalizacji rozpoczęła się w konstruktora, zaproponowane przez Stephen Cleary:

Kernel.Bind<IServiceProvider>().To<ServiceProvider>(); 

... i to nie wygląda dobrze dla mnie.

+0

Nie sądzę, że to RAII. Prawdopodobnie najważniejszą częścią jest dealokacja zasobów (choć nazwa tego nie sugeruje). I tak naprawdę nie dotyczy to C#, ponieważ GC powoduje niedeterministyczną dealokację. – svick

+0

Sprawdź moją bibliotekę [AsyncContainer] (https: // github.com/RafaelSalguero/AsyncContainer) – rafael

Odpowiedz

25

Mam blogu, który opisuje several approaches to async construction.

Polecam asynchroniczną metodę fabryczną opisaną przez Reeda, ale czasami nie jest to możliwe (np. Wstrzyknięcie zależności).W takich przypadkach można użyć asynchronicznego inicjalizacji wzór takiego:

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); 
    } 
} 

Następnie można skonstruować typu normalnie, ale należy pamiętać, że budowa rozpocznie się jedynie asynchroniczny inicjowania. Kiedy trzeba typ zostać zainicjowany, Twój kod może zrobić:

await myTypeInstance.Initialization; 

pamiętać, że jeśli Initialization jest już kompletny, wykonanie (synchronicznie) nadal obok await.


Jeżeli chcesz rzeczywiste asynchronous property, I have a blog post for that, too. Twoja sytuacja brzmi jak może korzystać z 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; } 
} 
4

Jedną z potencjalnych opcji byłoby przeniesienie jej do metody fabryki zamiast użycia konstruktora.

Twoja metoda fabryka mogłaby następnie zwróci Task<ServiceProvider>, które pozwalają na wykonywanie inicjalizacji asynchronicznie, ale nie zwracają skonstruowane ServiceProviderImportantValue została (asynchronicznie) obliczane.

Pozwoliłoby to użytkownikom na pisanie kodu, takich jak:

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

Przykro mi, zapomniałem wspomnieć o pytaniu, że moja klasa musi zaimplementować interfejs i jest przeznaczone do użycia tylko przez interfejs. To wyklucza statyczną metodę fabryczną. – Haspemulator

+0

@Haspemulator W takim przypadku zawsze będziesz miał metodę fabryczną (nie możesz zbudować interfejsu) - to ułatwia ... –

+0

Czy masz przykład, jak wyglądałaby CreateAsync – Demodave

0

wiem, jest to stara sprawa, ale to jest pierwszy, który pojawia się w Google, a całkiem szczerze mówiąc, przyjęta odpowiedź jest kiepską odpowiedzią. Nigdy nie należy wymuszać opóźnienia, aby móc korzystać z oczekującego operatora.

Lepszym podejściem do sposobu inicjalizacji:

private async Task<bool> InitializeAsync() 
{ 
    try{ 
     // Initialize this instance. 
    } 

    catch{ 
     // Handle issues 
     return await Task.FromResult(false); 
    } 

    return await Task.FromResult(true); 
} 

ten użyje ramy asynchronicznej zainicjować swój cel, ale potem zwróci wartość logiczną.

Dlaczego to lepsze podejście? Po pierwsze, nie zmuszasz do opóźnienia w kodzie, który IMHO całkowicie pokonuje cel wykorzystania asynchronicznego środowiska. Po drugie, jest to dobra zasada, aby zwrócić coś z metody asynchronicznej. W ten sposób wiesz, czy twoja metoda asynchroniczna rzeczywiście działała/wykonywała to, co powinna. Zwrócenie właśnie zadania jest odpowiednikiem zwracania pustki w metodzie nie asynchronicznej.

+3

"Czekaj na zadanie. Opóźnienie w mojej odpowiedzi jest symbolem zastępczym dla "wykonaj asynchroniczną pracę tutaj, aby zainicjować instancję". W rzeczywistym kodzie nie byłoby "Task.Delay". –

+2

Uważam, że to sprawia, że ​​twoja odpowiedź jest gorsza, ponieważ nie wyjaśniłeś, co robisz i co pokazujesz, że powinieneś wymuszać opóźnienia, aby uzyskać asynchroniczne zachowanie podczas inicjalizacji/konstrukcji. Powinieneś pamiętać, że każdy z różnych poziomów doświadczenia może zobaczyć Twój post, a nie podałeś żadnych użytecznych informacji w swojej odpowiedzi i pokazał słabe kodowanie bez jasności. Innymi słowy, ktoś nowy w strukturze asynchronicznej zobaczy Twoją odpowiedź i zaimplementuje opóźnienia w swoim kodzie, aby uzyskać asynchroniczną inicjalizację/konstrukcję. –

+0

@StephenCleary oboje macie rację ;-) –

1

Możesz użyć mojego kontenera IoC AsyncContainer, który obsługuje dokładnie ten sam scenariusz, co Ty.

Obsługuje on również inne przydatne scenariusze takie jak asynchronicznych inicjalizatorów uruchom czasie fabryk warunkowe, uzależnione od async i synchronizacji funkcji fabrycznych

//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>(); 
0

To niewielkie modyfikacje @StephenCleary wzór inicjalizacji asynchronicznym.

Różnica polegająca na tym, że dzwoniący nie musi "pamiętać" o await z InitializationTask, ani nawet nic nie wiedzieć o initializationTask (w rzeczywistości jest teraz zmieniony na prywatny).

Sposób działania polega na tym, że w każdej metodzie, która używa zainicjowanych danych, znajduje się pierwsze połączenie z numerem await _initializationTask. Powraca natychmiast za drugim razem - ponieważ sam obiekt _initializationTask będzie miał zestaw boolowski (IsCompleted, który sprawdza mechanizm "oczekiwać") - więc nie martw się, że zostanie zainicjowany wiele razy.

Jedynym haczykiem, o którym wiem, jest to, że nie można zapomnieć o wywołaniu go w każdej metodzie wykorzystującej dane.

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(); 
    } 
}