Scale Asynchronous Client-Server Links with Reactive
On 25 stycznia, 2022 by admin- 01/31/2019
- 17 minutes to read
June 2016
Volume 31 Number 6
By Peter Vogel | June 2016
As asynchronous processing has become more common in application development, Microsoft .NET Framework zyskał szeroką gamę narzędzi, które wspierają określone asynchroniczne wzorce projektowe. Często tworzenie dobrze zaprojektowanej aplikacji asynchronicznej sprowadza się do rozpoznania wzorca projektowego, który implementuje Twoja aplikacja, a następnie wybrania odpowiedniego zestawu komponentów .NET.
W niektórych przypadkach dopasowanie wymaga zintegrowania kilku komponentów .NET. Artykuł Stephena Cleary’ego „Patterns for Asynchronous MVVM Applications: Commands” (bit.ly/233Kocr), pokazuje, jak w pełni wspierać wzorzec Model-View-ViewModel (MVVM) w sposób asynchroniczny. W innych przypadkach wsparcie wymaga tylko jednego komponentu z .NET Framework. Omówiłem implementację wzorca provider/konsument przy użyciu BlockingCollection w moich kolumnach VisualStudioMagazine.com Practical .NET, „Create Simple, Reliable Asynchronous Apps with BlockingCollection” (bit.ly/1TuOpE6), oraz „Create Sophisticated Asynchronous Applications with BlockingCollection” (bit.ly/1SpYyD4).
Innym przykładem jest implementacja wzorca projektowego observer do asynchronicznego monitorowania długo trwających operacji. W tym scenariuszu metoda asynchroniczna, która zwraca pojedynczy obiekt Task, nie działa, ponieważ klient często zwraca strumień wyników. Dla takich scenariuszy można wykorzystać co najmniej dwa narzędzia z .NET Framework: ObservableCollection oraz Reactive Extensions (Rx). Dla prostych rozwiązań, ObservableCollection (wraz ze słowami kluczowymi async i await) jest wszystkim czego potrzebujesz. Jednak w przypadku bardziej „interesujących”, a zwłaszcza napędzanych zdarzeniami problemów, Rx zapewnia lepszą kontrolę nad procesem.
Definiowanie wzorca
Pomimo że wzorzec obserwatora jest często używany we wzorcach projektowych UI – w tym Model-View-Controller (MVC), Model-View-Presenter (MVP) i MVVM-UI – powinien być traktowany jako tylko jeden scenariusz z większego zbioru scenariuszy, w których wzorzec obserwatora ma zastosowanie. Definicja wzorca obserwatora (cytat z Wikipedii) to: „Obiekt, zwany podmiotem, utrzymuje listę swoich zależnych obiektów, zwanych obserwatorami, i powiadamia je automatycznie o wszelkich zmianach stanu, zwykle przez wywołanie jednej z ich metod.”
Prawdę mówiąc, wzorzec obserwatora dotyczy uzyskiwania wyników z długo trwających procesów do klienta, gdy tylko te wyniki są dostępne. Bez jakiejś wersji wzorca obserwatora, klienci muszą czekać, aż ostatni wynik będzie dostępny, a następnie mieć wszystkie wyniki wysłane do nich w jednej bryle. W coraz bardziej asynchronicznym świecie, chcesz, aby obserwatorzy przetwarzali wyniki równolegle z klientem, gdy tylko wyniki staną się dostępne. Aby podkreślić, że mówisz o czymś więcej niż o interfejsach użytkownika podczas korzystania z wzorca obserwatora, będę używał słów „klient” i „serwer” zamiast „obserwator” i „podmiot” w pozostałej części tego artykułu.
Problemy i możliwości
W przypadku wzorca obserwatora istnieją co najmniej trzy problemy i dwie możliwości. Pierwszym problemem jest problem lapsed-listener: Wiele implementacji wzorca obserwatora wymaga, aby serwer posiadał referencję do wszystkich swoich klientów. W rezultacie, klienci mogą być przetrzymywani w pamięci przez serwer aż do jego wyjścia. To oczywiście nie jest optymalne rozwiązanie dla długo trwającego procesu w dynamicznym systemie, w którym klienci często się łączą i rozłączają.
Problem lapsed-listener, jednakże, jest tylko symptomem drugiego, większego problemu: wiele implementacji wzorca obserwatora wymaga, aby serwer i klient były ściśle sprzężone, wymagając zarówno serwera, jak i klienta, aby były obecne przez cały czas. Przynajmniej klient powinien być w stanie określić, czy serwer jest obecny i wybrać, aby nie dołączać; ponadto serwer powinien być w stanie funkcjonować nawet wtedy, gdy nie ma klientów akceptujących wyniki.
Trzecia kwestia jest związana z wydajnością: Jak długo potrwa, zanim serwer powiadomi wszystkich klientów? Na wydajność we wzorcu obserwatora bezpośredni wpływ ma liczba klientów, którzy mają być powiadomieni. Dlatego istnieje możliwość poprawy wydajności we wzorcu obserwatora poprzez umożliwienie klientowi wstępnego filtrowania wyników, które wracają z serwera. Dotyczy to również scenariuszy, w których serwer produkuje więcej wyników (lub szerszą gamę wyników) niż klient jest zainteresowany: Klient może wskazać, że ma być powiadamiany tylko w określonych przypadkach. Druga możliwość wydajności istnieje wokół rozpoznawania, kiedy serwer nie ma wyników lub skończył je produkować. Klienci mogą pominąć nabywanie zasobów wymaganych do przetwarzania zdarzeń serwera, dopóki klient nie będzie miał gwarancji, że jest coś do przetworzenia, a klienci mogą zwolnić te zasoby, gdy tylko dowiedzą się, że przetworzyli ostatni wynik.
Od obserwatora do publikowania/subskrybowania
Uwzględnienie tych rozważań prowadzi od prostych implementacji wzorca obserwatora do powiązanego modelu publikowania/subskrybowania. Publish/subscribe implementuje wzorzec obserwatora w luźno sprzężony sposób, który pozwala serwerom i klientom wykonywać swoje zadania nawet wtedy, gdy drugi z nich jest aktualnie niedostępny. Publish/subscribe zazwyczaj implementuje również filtrowanie po stronie klienta, pozwalając klientowi na subskrybowanie określonych tematów/kanałów („Powiadom mnie o zamówieniach zakupu”) lub atrybutów związanych z różnymi rodzajami treści („Powiadom mnie o wszelkich pilnych prośbach”).
Jedna kwestia pozostaje jednak nierozwiązana. Wszystkie implementacje wzorca obserwatora mają tendencję do ścisłego łączenia klientów i serwerów z określonym formatem wiadomości. Zmiana formatu wiadomości w większości implementacji publish/subscribe może być trudna, ponieważ wszyscy klienci muszą zostać zaktualizowani, aby użyć nowego formatu.
Na wiele sposobów jest to podobne do opisu kursora po stronie serwera w bazie danych. Aby zminimalizować koszty transmisji, serwer bazy danych nie zwraca wyników, gdy każdy wiersz jest pobierany. Jednakże, dla dużych zestawów wierszy, baza danych również nie zwraca wszystkich wierszy w pojedynczej partii na końcu. Zamiast tego, serwer bazy danych zazwyczaj zwraca podzbiory z kursora utrzymywanego na serwerze często, gdy te podzbiory stają się dostępne. W przypadku bazy danych, klient i serwer nie muszą być jednocześnie obecne: Serwer bazy danych może działać, gdy nie ma na nim klientów; klient może sprawdzić, czy serwer jest dostępny, a jeśli nie, zdecydować, co (jeśli w ogóle) może jeszcze zrobić. Proces filtrowania (SQL) jest również bardzo elastyczny. Jednakże, jeśli silnik bazy danych zmieni format, którego używa do zwracania wierszy, wtedy wszyscy klienci muszą, co najmniej, zostać przekompilowani.
Przetwarzanie pamięci podręcznej obiektów
Jako moje studium przypadku dla spojrzenia na prostą implementację wzorca obserwatora, używam jako mojego serwera klasy, która przeszukuje pamięć podręczną faktur. Serwer ten mógłby, na końcu swojego przetwarzania, zwrócić kolekcję wszystkich faktur. Wolałbym jednak, aby klient przetwarzał faktury indywidualnie i równolegle do procesu wyszukiwania na serwerze. Oznacza to, że preferuję wersję procesu, która zwraca każdą fakturę w momencie jej znalezienia i pozwala klientowi przetwarzać każdą fakturę równolegle z procesem wyszukiwania następnej faktury.
Prosta implementacja serwera może wyglądać następująco:
private List<Invoice> foundInvoices = new List<Invoice>();public List<Invoice> FindInvoices(decimal Amount){ foundInvoices.Clear(); Invoice inv; // ...search logic to add invoices to the collection foundInvoices.Add(inv); // ...repeat until all invoices found return foundInvoices;}
Rozwiązania bardziej wyrafinowane mogą wykorzystywać yield return do zwracania każdej faktury w momencie jej znalezienia, a nie do tworzenia listy. Niezależnie od tego, klient, który wywołuje metodę FindInvoices będzie chciał wykonać kilka krytycznych czynności przed i po przetwarzaniu. Na przykład, gdy pierwszy element zostanie znaleziony, klient może chcieć włączyć listę MatchingInvoices, aby przechowywać faktury u klienta lub pozyskać/zainicjować wszelkie zasoby wymagane do przetworzenia faktury. W miarę dodawania kolejnych faktur klient musiałby przetwarzać każdą fakturę, a gdy serwer zasygnalizuje, że ostatnia faktura została pobrana, zwolnić wszystkie zasoby, które nie są już wymagane, ponieważ nie ma już więcej faktur do przetworzenia.
Podczas pobierania bazy danych, na przykład, odczyt będzie blokowany do momentu zwrócenia pierwszego wiersza. Gdy pierwszy wiersz zostanie zwrócony, klient inicjalizuje wszystkie zasoby potrzebne do przetworzenia wiersza. Odczyt zwraca również false, gdy ostatni wiersz zostanie zwrócony, pozwalając klientowi zwolnić te zasoby, ponieważ nie ma więcej wierszy do przetworzenia.
Tworzenie prostych rozwiązań z ObservableCollection
Najbardziej oczywistym wyborem dla implementacji wzorca obserwatora w .NET Framework jest ObservableCollection. ObservableCollection będzie powiadamiać klienta (poprzez zdarzenie) za każdym razem, gdy zostanie zmieniona.
Przepisanie mojego przykładowego serwera w celu użycia klasy ObservableCollection wymaga tylko dwóch zmian. Po pierwsze, kolekcja przechowująca wyniki musi być zdefiniowana jako ObservableCollection i upubliczniona. Po drugie, nie jest już konieczne, aby metoda zwracała wynik: Serwer musi jedynie dodawać faktury do kolekcji.
Nowa implementacja serwera może wyglądać tak:
public List<Invoice> FindInvoices(decimal Amount){ public ObservableCollection<Invoice> foundInvoices = new ObservableCollection<Invoice>(); public void FindInvoices(decimal Amount) { foundInvoices.Clear(); Invoice inv; // ...search logic to set inv foundInvoices.Add(inv); // ...repeat until all invoices are added to the collection }
Klient, który korzysta z tej wersji serwera, musi jedynie podłączyć obsługę zdarzenia do zdarzenia CollectionChanged kolekcji InvoiceManagement’s foundInvoices. W poniższym kodzie kazałem klasie zaimplementować interfejs IDisposable, aby obsłużyć odłączenie od zdarzenia:
public class SearchInvoices: IDisposable{ InvoiceManagement invMgmt = new InvoiceManagement(); public void SearchInvoices() { invMgmt.foundInvoices.CollectionChanged += InvoicesFound; } public void Dispose() { invMgmt.foundInvoices.CollectionChanged -= InvoicesChanged; }
W kliencie, zdarzenie CollectionChanged przekazuje obiekt NotifyCollectionChangedEventArgs jako swój drugi parametr. Właściwość Action tego obiektu określa zarówno, jaka zmiana została wykonana w kolekcji (akcje są następujące: kolekcja została wyczyszczona, nowe elementy zostały dodane do kolekcji, istniejące elementy zostały przeniesione/zastąpione/usunięte) oraz informacje o zmienionych elementach (kolekcja wszystkich dodanych elementów, kolekcja elementów obecnych w kolekcji przed dodaniem nowych elementów, pozycja elementu, który został przeniesiony/usunięty/zastąpiony).
Prostszy kod w kliencie, który asynchronicznie przetwarzałby każdą fakturę, gdy jest ona dodawana do kolekcji na serwerze, wyglądałby jak kod na rysunku 1.
Figura 1 Asynchroniczne przetwarzanie faktur przy użyciu ObservableCollection
private async void InvoicesFound(object sender, NotifyCollectionChangedEventArgs e){ switch (e.Action) { case NotifyCollectionChangedAction.Reset: { // ...initial item processing return; } case NotifyCollectionChangedAction.Add: { foreach (Invoice inv in e.NewItems) { await HandleInvoiceAsync(inv); } return; } }}
Choć prosty, ten kod może być nieodpowiedni dla twoich potrzeb, zwłaszcza jeśli obsługujesz długo trwające procesy lub pracujesz w dynamicznym środowisku. Z punktu widzenia projektowania asynchronicznego, na przykład, kod ten mógłby przechwycić obiekt Task zwrócony przez HandleInvoiceAsync, aby klient mógł zarządzać zadaniami asynchronicznymi. Będziesz również chciał się upewnić, że zdarzenie CollectionChanged jest podnoszone w wątku UI, nawet jeśli FindInvoices działa w wątku tła.
Ze względu na to, gdzie metoda Clear jest wywoływana w klasie serwera (tuż przed wyszukaniem pierwszej Faktury), wartość Reset właściwości Action może być używana jako sygnał, że pierwszy element zostanie wkrótce pobrany. Jednakże, oczywiście, żadna faktura może nie zostać znaleziona podczas wyszukiwania, więc użycie akcji Reset może spowodować, że klient będzie alokował zasoby, które nigdy nie zostaną faktycznie wykorzystane. Aby faktycznie obsługiwać przetwarzanie „pierwszej pozycji”, należałoby dodać flagę do przetwarzania Dodaj akcję, aby była wykonywana tylko wtedy, gdy znaleziono pierwszą pozycję.
Dodatkowo, serwer ma ograniczoną liczbę opcji wskazujących, że ostatnia faktura została znaleziona, aby klient mógł przestać czekać na „następną”. Serwer mógłby, przypuszczalnie, wyczyścić kolekcję po znalezieniu ostatniego elementu, ale to po prostu wymusiłoby bardziej złożone przetwarzanie do przetwarzania Reset Action (czy przetwarzałem Faktury? Jeśli tak, to przetworzyłem ostatnią Fakturę; jeśli nie, to zamierzam przetworzyć pierwszą Fakturę).
Choć dla prostych problemów ObservableCollection będzie w porządku, każda rozsądnie wyrafinowana implementacja oparta na ObservableCollection (i każda aplikacja, która ceni wydajność) będzie wymagać trochę skomplikowanego kodu, szczególnie w kliencie.
Rx Solutions
Jeśli chcesz asynchronicznego przetwarzania, to Rx (dostępny przez NuGet) może zapewnić lepsze rozwiązanie do implementacji wzorca obserwatora poprzez zapożyczenie z modelu publish/subscribe. To rozwiązanie zapewnia również model filtrowania oparty na LINQ, lepszą sygnalizację dla warunków pierwszego/ostatniego elementu i lepszą obsługę błędów.
Rx może również obsługiwać bardziej interesujące implementacje obserwatora niż są możliwe z ObservableCollection. W moim studium przypadku, po zwróceniu początkowej listy faktur, mój serwer może kontynuować sprawdzanie nowych faktur, które są dodawane do pamięci podręcznej po zakończeniu pierwotnego wyszukiwania (i które oczywiście spełniają kryteria wyszukiwania). Kiedy pojawi się faktura spełniająca kryteria, klient będzie chciał zostać powiadomiony o tym zdarzeniu, aby nowa faktura mogła zostać dodana do listy. Rx obsługuje tego rodzaju oparte na zdarzeniach rozszerzenia wzorca obserwatora lepiej niż ObservableCollection.
W Rx istnieją dwa kluczowe interfejsy do obsługi wzorca obserwatora. Pierwszy z nich to IObservable<T>, implementowany przez serwer i określający pojedynczą metodę: Subscribe. Serwer implementujący metodę Subscribe otrzyma od klienta referencję do obiektu. Aby poradzić sobie z problemem lapsed listener, metoda Subscribe zwraca klientowi referencję do obiektu, który implementuje interfejs IDisposable. Klient może użyć tego obiektu do rozłączenia się z serwerem. Gdy klient się rozłączy, oczekuje się, że serwer usunie klienta z każdej ze swoich wewnętrznych list.
Drugim jest interfejs IObserver<T>, który musi być zaimplementowany przez klienta. Ten interfejs wymaga, aby klient zaimplementował i wyeksponował trzy metody dla serwera: OnNext, OnCompleted oraz OnError. Krytyczną metodą jest tutaj OnNext, która jest używana przez serwer do przekazywania wiadomości do klienta (w moim przypadku tą wiadomością będą nowe obiekty faktur, które będą zwracane w miarę pojawiania się każdego z nich). Serwer może wykorzystać metodę OnCompleted klienta, aby zasygnalizować, że nie ma więcej danych. Trzecia metoda, OnError, zapewnia sposób, w jaki serwer sygnalizuje klientowi, że wystąpił wyjątek.
Możesz oczywiście zaimplementować interfejs IObserver samodzielnie (jest on częścią .NET Framework). Wraz z ObservableCollection, może to być wszystko, czego potrzebujesz, jeśli tworzysz rozwiązanie synchroniczne (napisałem o tym również kolumnę „Writing Cleaner Code with Reactive Extensions”).
Jednakże Rx zawiera kilka pakietów, które zapewniają asynchroniczne implementacje tych interfejsów, w tym implementacje dla JavaScript i usług RESTful. Klasa Rx Subject dostarcza implementację IObservable, która upraszcza implementację asynchronicznej wersji publish/subscribe wzorca obserwatora.
Tworzenie asynchronicznego rozwiązania
Tworzenie serwera do pracy z obiektem Subject wymaga bardzo niewielu zmian w oryginalnym synchronicznym kodzie po stronie serwera. Zastępuję starą kolekcję ObservableCollection obiektem Subject, który będzie przekazywał każdą fakturę w momencie jej pojawienia się do wszystkich klientów nasłuchujących. Deklaruję obiekt Subject jako publiczny, aby klienci mieli do niego dostęp:
public class InvoiceManagement{ public IObservable<Invoice> foundInvoice = new Subject<Invoice>();
W ciele metody, zamiast dodawać fakturę do kolekcji, używam metody OnNext obiektu Subject, aby przekazać każdą fakturę do klienta, gdy zostanie znaleziona:
public void FindInvoices(decimal Amount){ inv = GetInvoicesForAmount(Amount) // Poll for invoices foundInvoice.OnNext(inv); // ...repeat...}
W moim kliencie, najpierw deklaruję instancję klasy serwera. Następnie, w metodzie oznaczonej jako async, wywołuję metodę Subscribe obiektu Subject, aby wskazać, że chcę rozpocząć pobieranie wiadomości:
public class InvoiceManagementTests{ InvoiceManagement invMgmt = new InvoiceManagement(); public async void ProcessInvoices() { invMgmt.foundInvoice.Subscribe<Invoice>();
Aby przefiltrować wyniki tylko do tych faktur, które chcę, mogę zastosować instrukcję LINQ do obiektu Subject. Ten przykład filtruje faktury do tych, które są zamówione wstecz (aby użyć rozszerzeń Rx LINQ, musisz dodać instrukcję using dla przestrzeni nazw System.Reactive.Linq):
invMgmt.foundInvoice.Where(i => i.BackOrder == "BackOrder").Subscribe();
Gdy już zacznę słuchać obiektu Subject, mogę określić, jakie przetwarzanie chcę wykonać, gdy otrzymam fakturę. Mogę, na przykład, użyć FirstAsync do przetworzenia tylko pierwszej faktury zwróconej przez usługę. W tym przykładzie, używam instrukcji await z wywołaniem FirstAsync, dzięki czemu mogę zwrócić kontrolę do głównego ciała mojej aplikacji podczas przetwarzania faktury. Ten kod czeka na pobranie tej pierwszej faktury, następnie przechodzi do dowolnego kodu, którego używam do zainicjowania procesu przetwarzania faktury i w końcu przetwarza fakturę:
Invoice inv;inv = await invMgmt.foundInvoice.FirstAsync();// ...setup code invoices...HandleInvoiceAsync(inv);
Jedno zastrzeżenie: FirstAsync zablokuje się, jeśli serwer nie przyniósł jeszcze żadnych wyników. Jeśli chcesz uniknąć blokowania, możesz użyć FirstOrDefaultAsync, który zwróci null, jeśli serwer nie przyniósł żadnych wyników. Jeśli nie ma żadnych wyników, klient może zdecydować, co, jeśli w ogóle, zrobić.
Najbardziej typowym przypadkiem jest to, że klient chce przetworzyć wszystkie zwrócone faktury (po filtrowaniu) i zrobić to asynchronicznie. W takim przypadku, zamiast używać kombinacji Subscribe i OnNext, możesz po prostu użyć metody ForEachAsync. Możesz przekazać metodę lub wyrażenie lambda, które przetwarza przychodzące wyniki. Jeśli przekażesz metodę (która nie może być asynchroniczna), tak jak ja to robię tutaj, ta metoda zostanie przekazana do faktury, która wywołała ForEachAsync:
invMgmt.foundInvoice.ForEachAsync(HandleInvoice);
Metodzie ForEachAsync można również przekazać token anulowania, aby pozwolić klientowi zasygnalizować, że się rozłącza. Dobrą praktyką byłoby przekazanie tokena podczas wywoływania dowolnej z metod Rx *Async, aby wesprzeć umożliwienie klientowi zakończenia przetwarzania bez konieczności oczekiwania na przetworzenie wszystkich obiektów.
ForEachAsync nie przetworzy żadnego wyniku już przetworzonego przez metodę First (lub FirstOrDefaultAsync), więc możesz użyć FirstOrDefaultAsync z ForEachAsync, aby sprawdzić, czy serwer ma coś do przetworzenia przed przetworzeniem kolejnych obiektów. Jednakże, metoda IsEmpty obiektu wykona to samo sprawdzenie w prostszy sposób. Jeśli klient musi przydzielić jakiekolwiek zasoby wymagane do przetwarzania wyników, IsEmpty pozwala klientowi sprawdzić, czy jest coś do zrobienia przed przydzieleniem tych zasobów (alternatywą byłoby przydzielenie tych zasobów na pierwszym elemencie przetwarzanym w pętli). Użycie IsEmpty z klientem, który sprawdza, czy są jakieś wyniki przed przydzieleniem zasobów (i rozpoczęciem przetwarzania), jednocześnie wspierając anulowanie, dałoby kod, który wygląda jak Rysunek 2.
Figure 2 Code to Support Cancellation and Defer Processing Until Results are Ready
CancellationTokenSource cancelSource = new CancellationTokenSource();CancellationToken cancel;cancel = cancelSource.Token;if (!await invMgmt.foundInvoice.IsEmpty()){ // ...setup code for processing invoices... try { invMgmt.foundInvoice.ForEachAsync(HandleInvoice, cancel); } catch (Exception ex) { if (ex.GetType() != typeof(CancellationToken)) { // ...report message } } // ...clean up code when all invoices are processed or client disconnects}
Wrapping Up
Jeśli wszystko, czego potrzebujesz, to prosta implementacja wzorca obserwatora, to ObservableCollection może zrobić wszystko, czego potrzebujesz do przetwarzania strumienia wyników. Dla lepszej kontroli i dla aplikacji opartej na zdarzeniach, klasa Subject i rozszerzenia dostarczane z Rx pozwolą twojej aplikacji pracować w trybie asynchronicznym poprzez wspieranie potężnej implementacji modelu publish/subscribe (i nie spojrzałem na bogatą bibliotekę operatorów dostarczanych z Rx). Jeśli pracujesz z Rx, warto pobrać Rx Design Guide (bit.ly/1VOPxGS), w którym omówiono najlepsze praktyki konsumowania i produkowania obserwowalnych strumieni.
Rx zapewnia również pewne wsparcie dla konwersji typu wiadomości przekazywanych między klientem a serwerem za pomocą interfejsu ISubject<TSource, TResult>. Interfejs ISubject<TSource, TResult> określa dwa datatypy: datatype „in” i datatype „out”. Wewnątrz klasy Subject, która implementuje ten interfejs, możesz wykonać dowolne operacje niezbędne do konwersji wyniku zwróconego z serwera (typ danych „in”) na wynik wymagany przez klienta (typ danych „out”). Co więcej, parametr in jest kowariantny (przyjmie określony typ danych lub cokolwiek, po czym ten typ danych dziedziczy), a parametr out jest kontrawariantny (przyjmie określony typ danych lub cokolwiek, co się z niego wywodzi), co daje dodatkową elastyczność.
Żyjemy w coraz bardziej asynchronicznym świecie i w tym świecie wzorzec obserwatora będzie stawał się coraz ważniejszy – jest to użyteczne narzędzie dla każdego interfejsu między procesami, gdzie proces serwera zwraca więcej niż jeden wynik. Na szczęście masz kilka opcji implementacji wzorca obserwatora w .NET Framework, w tym ObservableCollection i Rx.
Peter Vogel jest architektem systemów i dyrektorem w PH&V Information Services. PH&V zapewnia konsulting full-stack od projektowania UX poprzez modelowanie obiektowe i projektowanie baz danych.
Podziękowania dla następujących ekspertów technicznych firmy Microsoft za recenzję tego artykułu: Stephen Cleary, James McCaffrey i Dave Sexton
Stephen Cleary pracuje z wielowątkowością i programowaniem asynchronicznym od 16 lat i korzysta z obsługi async w Microsoft .NET Framework od czasu pierwszego podglądu technologii społeczności. Jest autorem książki „Concurrency in C# Cookbook” (O’Reilly Media, 2014). Jego strona domowa, w tym blog, znajduje się pod adresem stephencleary.com.
Dyskutuj o tym artykule na forum MSDN Magazine
.
Dodaj komentarz