Ostatnio patrzyłem na język F # i chociaż nie zamierzam w najbliższym czasie przeskoczyć przez barierę, zdecydowanie podkreśla niektóre obszary, w których C # (lub obsługa bibliotek) może ułatwić życie.
W szczególności myślę o możliwości dopasowania wzorców w F #, która pozwala na bardzo bogatą składnię - znacznie bardziej wyrazistą niż bieżące odpowiedniki przełącznika / warunkowe C #. Nie będę próbował podawać bezpośredniego przykładu (mój F # nie jest do tego przystosowany), ale w skrócie pozwala:
- dopasuj według typu (ze sprawdzaniem pełnego pokrycia dla związków rozłącznych) [uwaga: to również określa typ zmiennej powiązanej, dając dostęp do elementu członkowskiego itp.]
- dopasować według predykatu
- kombinacje powyższych (i być może kilka innych scenariuszy, których nie znam)
Chociaż byłoby cudownie, gdyby C # ostatecznie pożyczył [ahem] część tego bogactwa, w międzyczasie przyglądałem się temu, co można zrobić w czasie wykonywania - na przykład dość łatwo jest połączyć niektóre obiekty, aby umożliwić:
var getRentPrice = new Switch<Vehicle, int>()
.Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
.Case<Bicycle>(30) // returns a constant
.Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
.Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
.ElseThrow(); // or could use a Default(...) terminator
gdzie getRentPrice to Func <Vehicle, int>.
[uwaga - być może Switch / Case to niewłaściwe terminy ... ale pokazuje ideę]
Dla mnie jest to o wiele jaśniejsze niż ekwiwalent z użyciem powtórzonego if / else lub złożonego trójskładnikowego warunku (co staje się bardzo nieuporządkowane w przypadku nietrywialnych wyrażeń - mnóstwo nawiasów). Pozwala również uniknąć wielu rzutów i umożliwia proste rozszerzenie (bezpośrednio lub metodami rozszerzania) na bardziej szczegółowe dopasowania, na przykład dopasowanie InRange (...) porównywalne z VB Select ... Przypadek "x To y " stosowanie.
Po prostu próbuję ocenić, czy ludzie uważają, że konstrukcje takie jak powyżej dają wiele korzyści (przy braku obsługi języka)?
Uwaga dodatkowo, że grałem z 3 wariantami powyższego:
- wersja Func <TSource, TValue> do oceny - porównywalna do złożonych trójskładnikowych instrukcji warunkowych
- wersja Action <TSource> - porównywalna do if / else if / else if / else if / else
- wersja Expression <Func <TSource, TValue >> - jako pierwsza, ale do wykorzystania przez dowolnych dostawców LINQ
Ponadto użycie wersji opartej na wyrażeniach umożliwia ponowne zapisywanie drzewa wyrażeń, zasadniczo umieszczając wszystkie gałęzie w jednym złożonym wyrażeniu warunkowym, zamiast używać wielokrotnego wywołania. Nie sprawdzałem ostatnio, ale w niektórych wczesnych kompilacjach Entity Framework wydaje mi się, że jest to konieczne, ponieważ nie bardzo lubił InvocationExpression. Pozwala również na bardziej wydajne użycie z LINQ-to-Objects, ponieważ pozwala uniknąć powtarzających się wywołań delegatów - testy pokazują dopasowanie podobne do powyższego (przy użyciu formularza Expression) działające z tą samą prędkością [w rzeczywistości nieznacznie szybsze] w porównaniu z równoważnym C # złożona instrukcja warunkowa. Aby zapewnić kompletność, wersja oparta na Func <...> zajęła 4 razy więcej czasu niż instrukcja warunkowa C #, ale nadal jest bardzo szybka i jest mało prawdopodobne, aby była głównym wąskim gardłem w większości przypadków użycia.
Z zadowoleniem przyjmuję wszelkie przemyślenia / wkład / krytykę / itp. Dotyczące powyższego (lub możliwości bogatszej obsługi języka C # ... mam nadzieję ;-p).
źródło
switch-case
oświadczenia. Nie zrozum mnie źle, myślę, że ma swoje miejsce i prawdopodobnie będę szukał sposobu na wdrożenie.Odpowiedzi:
Wiem, że to stary temat, ale w c # 7 możesz:
źródło
Po zrobieniu takich „funkcjonalnych” rzeczy w C # (a nawet przeczytaniu książki o tym), doszedłem do wniosku, że nie, z kilkoma wyjątkami, takie rzeczy nie pomagają zbytnio.
Głównym powodem jest to, że języki takie jak F # czerpią dużą moc z prawdziwego wspierania tych funkcji. Nie „dasz radę”, ale „to proste, jasne, oczekuje się”.
Na przykład podczas dopasowywania wzorców kompilator informuje Cię, czy jest niekompletne dopasowanie lub kiedy inne dopasowanie nigdy nie zostanie trafione. Jest to mniej przydatne w przypadku typów otwartych, ale przy dopasowywaniu unii dyskryminowanej lub krotek jest bardzo sprytne. W języku F # oczekujesz, że ludzie będą dopasowywać wzorce i natychmiast ma to sens.
„Problem” polega na tym, że kiedy zaczniesz używać pewnych koncepcji funkcjonalnych, naturalna jest chęć kontynuowania. Jednak wykorzystanie krotek, funkcji, częściowego stosowania metod i curry, dopasowywania wzorców, funkcji zagnieżdżonych, typów ogólnych, obsługi monad itp. W C # staje się bardzo brzydkie, bardzo szybko. To zabawne, a niektórzy bardzo mądrzy ludzie zrobili kilka bardzo fajnych rzeczy w C #, ale w rzeczywistości używanie go wydaje się ciężkie.
Czego często używałem (między projektami) w C #:
** Należy jednak pamiętać: brak automatycznego uogólniania i wnioskowania o typie naprawdę utrudnia korzystanie z tych funkcji. **
Wszystko to powiedziane, jak ktoś wspomniał, w małym zespole, w określonym celu, tak, być może mogą pomóc, jeśli utkniesz z C #. Ale z mojego doświadczenia wynika, że zwykle wydawały się bardziej kłopotliwe, niż były warte - YMMV.
Inne linki:
źródło
Prawdopodobnie powodem, dla którego C # nie ułatwia włączania typu, jest to, że jest to przede wszystkim język zorientowany obiektowo, a `` poprawnym '' sposobem zrobienia tego w kategoriach obiektowych byłoby zdefiniowanie metody GetRentPrice w Vehicle i zastąpić go w klasach pochodnych.
To powiedziawszy, spędziłem trochę czasu bawiąc się wieloparadygmatami i językami funkcjonalnymi, takimi jak F # i Haskell, które mają tego typu możliwości i natknąłem się na wiele miejsc, w których byłoby to przydatne wcześniej (np. nie piszesz typów, które musisz włączyć, więc nie możesz zaimplementować na nich metody wirtualnej) i jest to coś, co chciałbym w tym języku wraz z dyskryminowanymi związkami.
[Edycja: Usunięto część dotyczącą wydajności, ponieważ Marc wskazał, że może to być zwarcie]
Innym potencjalnym problemem jest użyteczność - z końcowego połączenia jasno wynika, co się stanie, jeśli mecz nie spełni żadnych warunków, ale jakie jest zachowanie, jeśli spełnia dwa lub więcej warunków? Czy powinien zgłosić wyjątek? Czy powinien zwrócić pierwszy czy ostatni mecz?
Sposobem, którego używam do rozwiązywania tego rodzaju problemów, jest użycie pola słownika z typem jako kluczem i lambdą jako wartością, co jest dość zwięzłe do skonstruowania przy użyciu składni inicjatora obiektu; jednak uwzględnia to tylko konkretny typ i nie zezwala na dodatkowe predykaty, więc może nie być odpowiednie dla bardziej złożonych przypadków. [Uwaga dodatkowa - jeśli spojrzysz na dane wyjściowe kompilatora C #, często konwertuje on instrukcje przełączania na tabele skoku oparte na słownikach, więc nie wydaje się, aby istniał dobry powód, dla którego nie mógł obsługiwać przełączania typów]
źródło
Nie sądzę, aby tego rodzaju biblioteki (działające jak rozszerzenia językowe) zyskały powszechną akceptację, ale są zabawne i mogą być naprawdę przydatne dla małych zespołów pracujących w określonych domenach, w których jest to przydatne. Na przykład, jeśli piszesz mnóstwo `` reguł biznesowych / logiki '', które wykonują dowolne testy typu, takie jak ten i inne, widzę, jak byłoby to przydatne.
Nie mam pojęcia, czy kiedykolwiek będzie to funkcja języka C # (wydaje się wątpliwa, ale kto może przewidzieć przyszłość?).
Dla porównania odpowiedni F # to w przybliżeniu:
zakładając, że zdefiniowałbyś hierarchię klas zgodnie z liniami
źródło
Aby odpowiedzieć na twoje pytanie, tak, myślę, że konstrukcje składniowe dopasowywania wzorców są przydatne. Na przykład chciałbym zobaczyć obsługę składniową w C # dla tego.
Oto moja implementacja klasy, która zapewnia (prawie) taką samą składnię, jak opisujesz
Oto kod testowy:
źródło
Dopasowanie wzorców (zgodnie z opisem tutaj ), jego celem jest dekonstrukcja wartości zgodnie z ich specyfikacją typu. Jednak koncepcja klasy (lub typu) w C # nie zgadza się z tobą.
Nie ma nic złego w projektowaniu języka z wieloma paradygmatami, wręcz przeciwnie, bardzo fajnie jest mieć lambdy w C #, a Haskell może robić niezbędne rzeczy, np. IO. Ale to niezbyt eleganckie rozwiązanie, nie w stylu Haskella.
Ale ponieważ sekwencyjne języki programowania proceduralnego można rozumieć w kategoriach rachunku lambda, a C # tak się składa, że dobrze pasuje do parametrów sekwencyjnego języka proceduralnego, jest to dobre dopasowanie. Ale wzięcie czegoś z czystego kontekstu funkcjonalnego, powiedzmy Haskell, a następnie umieszczenie tej cechy w języku, który nie jest czysty, cóż, zrobienie tego, nie gwarantuje lepszego wyniku.
Chodzi mi o to, że to, co sprawia, że dopasowywanie wzorców działa tak, jest związane z projektem języka i modelem danych. Powiedziawszy to, nie uważam, aby dopasowywanie wzorców było użyteczną funkcją języka C #, ponieważ nie rozwiązuje ono typowych problemów języka C # ani nie pasuje do imperatywnego paradygmatu programowania.
źródło
IMHO, sposobem OO robienia takich rzeczy jest wzorzec gościa. Twoje metody składowe gościa działają po prostu jako konstrukcje wielkości liter i pozwalasz samemu językowi obsługiwać odpowiednie wywołanie bez konieczności „podglądania” typów.
źródło
Chociaż nie jest to bardzo „C-sharpey”, aby włączyć typ, wiem, że konstrukt byłby całkiem pomocny w ogólnym użyciu - mam co najmniej jeden osobisty projekt, który mógłby go używać (chociaż jest to zarządzalny bankomat). Czy jest dużo problemów z wydajnością kompilacji, z ponownym zapisywaniem drzewa wyrażeń?
źródło
Myślę, że wygląda to naprawdę interesująco (+1), ale jedna rzecz, na którą należy uważać: kompilator C # całkiem dobrze optymalizuje instrukcje przełącznika. Nie tylko do zwarcia - otrzymujesz zupełnie inną IL w zależności od liczby przypadków i tak dalej.
Twój konkretny przykład robi coś, co uważam za bardzo przydatne - nie ma odpowiednika składni dla wielkości liter według typu, ponieważ (na przykład)
typeof(Motorcycle)
nie jest stałą.To staje się bardziej interesujące w dynamicznych aplikacjach - twoja logika może być łatwo oparta na danych, dając wykonanie w stylu "silnika reguł".
źródło
Możesz osiągnąć to, czego szukasz, korzystając z biblioteki, którą napisałem o nazwie OneOf
Główną zaletą w stosunku do
switch
(iif
iexceptions as control flow
) jest to, że jest bezpieczny w czasie kompilacji - nie ma domyślnej obsługi ani nie działaJest na Nuget i celuje w net451 i netstandard1.6
źródło