Preferencje stylu LINQ [zamknięte]


21

Używam LINQ w moim codziennym programowaniu. W rzeczywistości rzadko, jeśli w ogóle, używam wyraźnej pętli. Odkryłem jednak, że nie używam już składni podobnej do SQL. Po prostu używam funkcji rozszerzenia. Zamiast mówić:

from x in y select datatransform where filter 

Używam:

x.Where(c => filter).Select(c => datatransform)

Który styl LINQ preferujesz i z kim inni w twoim zespole czują się komfortowo?


5
Warto zauważyć, że oficjalne stanowisko MS jest takie, że preferowana jest składnia zapytania.
— R0MANARMY

1
Ostatecznie to nie ma znaczenia. Liczy się to, że kod jest zrozumiały. Jedna forma może być lepsza w jednym przypadku, a druga w innym przypadku. Więc używaj tego, co w danym momencie jest odpowiednie.
— ChrisF

Uważam, że twój drugi przykład nazywa się składnią lambda, z której korzystam w 95% przypadków. Z pozostałych 5% korzystam ze składni zapytania, czyli kiedy wykonuję sprzężenia, próbuję przejść do sprzężeń składni lambda, ale jak inni zauważyli, robi się bałagan.
— The Muffin Man,

Odpowiedzi:


26

Uważam za niefortunne, że stanowisko Microsoftu według dokumentacji MSDN jest takie, że preferowana jest składnia zapytania, ponieważ nigdy jej nie używam, ale cały czas używam składni metody LINQ. Uwielbiam być w stanie odpalać zapytania jednowierszowe do treści mojego serca. Porównać:

var products = from p in Products
               where p.StockOnHand == 0
               select p;

Do:

var products = Products.Where(p => p.StockOnHand == 0);

Szybciej, mniej linii, a moim oczom wygląda na czystsze. Składnia zapytania nie obsługuje również wszystkich standardowych operatorów LINQ. Przykładowe zapytanie, które ostatnio zrobiłem, wyglądało mniej więcej tak:

var itemInfo = InventoryItems
    .Where(r => r.ItemInfo is GeneralMerchInfo)
    .Select(r => r.ItemInfo)
    .Cast<GeneralMerchInfo>()
    .FirstOrDefault(r => r.Xref == xref);

Według mojej wiedzy, aby zreplikować to zapytanie przy użyciu składni zapytania (w możliwym zakresie), wyglądałoby to tak:

var itemInfo = (from r in InventoryItems
                where r.ItemInfo is GeneralMerchInfo
                select r.ItemInfo)
                .Cast<GeneralMerchInfo>()
                .FirstOrDefault(r => r.Xref == xref);

Nie wydaje mi się bardziej czytelny i i tak musisz wiedzieć, jak korzystać ze składni metod. Osobiście jestem naprawdę zachwycony deklaratywnym stylem, który umożliwia LINQ i używam go w każdej sytuacji, w której jest to w ogóle możliwe - być może czasami na moją szkodę. W tym przypadku ze składnią metody mogę zrobić coś takiego:

// projects an InventoryItem collection with total stock on hand for each GSItem
inventoryItems = repository.GSItems
    .Select(gsItem => new InventoryItem() {
        GSItem = gsItem,
        StockOnHand = repository.InventoryItems
            .Where(inventoryItem => inventoryItem.GSItem.GSNumber == gsItem.GSNumber)
            .Sum(r => r.StockOnHand)
     });

Wyobrażam sobie, że powyższy kod byłby trudny do zrozumienia dla kogoś wchodzącego do projektu bez dobrej dokumentacji, a jeśli nie mają solidnego doświadczenia w LINQ, mogą go nie zrozumieć. Mimo to składnia metody ujawnia pewne dość potężne możliwości szybkiego wyświetlania (w kategoriach wierszy kodu) zapytania w celu uzyskania zbiorczych informacji o wielu kolekcjach, które w innym przypadku wymagałyby wielu żmudnych pętli foreach. W takim przypadku składnia metody jest ultra-kompaktowa dla tego, co z niej wyciągniesz. Próba zrobienia tego za pomocą składni zapytania może stać się niewygodna dość szybko.


Rzutowanie, które możesz wykonać wewnątrz zaznaczenia, ale niestety nie możesz określić, aby brać najlepsze X rekordy bez uciekania się do korzystania z metod LINQ. Jest to szczególnie denerwujące w miejscach, w których wiesz, że potrzebujesz tylko jednego rekordu i musisz umieścić wszystkie zapytania w nawiasach.
— Ziv

2
Tylko dla rekordu możesz zrobić Wybierz (x => x.ItemInfo) .OfType <GeneralMerchInfo> () zamiast Where (). Select (). Cast <> (), który moim zdaniem jest szybszy (duży O 2n zamiast n * 2m myślę). Ale masz całkowitą rację, składnia lambda jest znacznie lepsza z punktu widzenia czytelności.
— Ed James

16

Uważam, że funkcjonalna składnia jest przyjemniejsza dla oka. Jedynym wyjątkiem jest to, że muszę dołączyć więcej niż dwa zestawy. Join () bardzo szybko wariuje.


Zgadzam się ... O wiele bardziej wolę wygląd i czytelność od metod rozszerzenia oprócz (jak wskazano) podczas dołączania. Dostawcy komponentów (np. Telerik) bardzo często korzystają z metod rozszerzeń. Przykład, o którym myślę, to ich formanty Rad w ASP.NET MVC. Musisz być bardzo biegły w używaniu metod rozszerzeń, aby z nich korzystać / czytać.
— Catchops,

Przyszło to powiedzieć. Zwykle używam lambda, chyba że w grę wchodzi połączenie. Gdy nastąpi sprzężenie, składnia LINQ staje się bardziej czytelna.
— Sean

10

Czy jest już za późno, aby dodać kolejną odpowiedź?

Napisałem mnóstwo kodu LINQ-to-objects i twierdzę, że przynajmniej w tej domenie dobrze jest zrozumieć obie składnie, aby zastosować którykolwiek z nich, który upraszcza kod - który nie zawsze jest składnią kropkową.

Oczywiście zdarzają się sytuacje, w których składnia kropkowa JEST właściwą drogą - inni podali kilka takich przypadków; myślę jednak, że zmieniono rozumienie - jeśli źle zrobisz, otrzymałeś zły rap. Podam więc próbkę, w której, moim zdaniem, zrozumienie jest przydatne.

Oto rozwiązanie zagadki polegającej na zastępowaniu cyfr: (rozwiązanie napisane przy użyciu LINQPad, ale może być samodzielne w aplikacji na konsolę)

// NO
// NO
// NO
//+NO
//===
// OK

var solutions =
    from O in Enumerable.Range(1, 8) // 1-9
                    //.AsQueryable()
    from N in Enumerable.Range(1, 8) // 1-9
    where O != N
    let NO = 10 * N + O
    let product = 4 * NO
    where product < 100
    let K = product % 10
    where K != O && K != N && product / 10 == O
    select new { N, O, K };

foreach(var i in solutions)
{
    Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}

//Console.WriteLine("\nsolution expression tree\n" + solutions.Expression);

... które wyjścia:

N = 1, O = 6, K = 4

Nieźle, logika płynie liniowo i widzimy, że powstaje jedno poprawne rozwiązanie. Ta łamigłówka jest łatwa do rozwiązania ręcznie: rozumowanie, że 3>> N0 i O> 4 * N implikuje 8> = O> = 4. Oznacza to, że istnieje maksymalnie 10 przypadków do przetestowania ręcznie (2 dla N- by -5 dla O). Wystarczająco zbłądziłem - ta łamigłówka jest oferowana w celach ilustracyjnych LINQ.

Transformacje kompilatora

Kompilator robi wiele, aby przetłumaczyć to na równoważną składnię kropkową. Poza zwykłymi drugimi i kolejnymi fromklauzulami zamienianymi w SelectManywywołania , mamy letklauzule, które stają się Selectwywołaniami z rzutami, które wykorzystują przezroczyste identyfikatory . Jak zamierzam pokazać, konieczność nazwania tych identyfikatorów w składni kropek odbiera czytelność tego podejścia.

Mam sposób na ujawnienie, co robi kompilator, tłumacząc ten kod na składnię kropkową. Jeśli usuniesz komentarz z dwóch wyżej wymienionych wierszy i uruchom go ponownie, otrzymasz następujące dane wyjściowe:

N = 1, O = 6, K = 4

drzewo wyrażeń rozwiązania System.Linq.Enumerable + d_ b8.SelectMany (O => Range (1, 8), (O, N) => new <> f _AnonymousType0 2(O = O, N = N)).Where(<>h__TransparentIdentifier0 => (<>h__TransparentIdentifier0.O != <>h__TransparentIdentifier0.N)).Select(<>h__TransparentIdentifier0 => new <>f__AnonymousType12 (<> h_ TransparentIdentifier0 = <> h _TransparentIdentifier0, NO = ((10 * <> h_ TransparentIdentifier0.N) + <> h _TransparentIdentifier0.O))). Wybierz (<> h_ TransparentIdentifier1 => nowy <> f _AnonymousType2 2(<>h__TransparentIdentifier1 = <>h__TransparentIdentifier1, product = (4 * <>h__TransparentIdentifier1.NO))).Where(<>h__TransparentIdentifier2 => (<>h__TransparentIdentifier2.product < 100)).Select(<>h__TransparentIdentifier2 => new <>f__AnonymousType32 (<> h_ TransparentIdentifier2 = <> h _TransparentIdentifier2, K = ( <> h_ TransparentIdentifier2.product% 10))). Gdzie (<> h _TransparentIdentifier3 => (((((<> h_ TransparentIdentifier3.K! = <> h _TransparentIdentifier3. <> h_ TransparentIdentifier2. <>h _TransparentIdentifier1. <> h_TransparentIdentifier0.O) AndAlso (<> h _TransparentIdentifier3.K! = <> H_ TransparentIdentifier3. <> H _TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.N)) AndAlso ((<> h_ TransparentIdentifier3. <> H product / 10) == <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.O))). Wybierz (<> h_ TransparentIdentifier3 => nowy <> f _AnonymousType4`3 (N = < > h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.N,O = <> h_ TransparentIdentifier3. <> H_TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.O, K = <> h__TransparentIdentifier3.K))

Umieszczenie każdego operatora LINQ w nowym wierszu, tłumaczenie „niewymownych” identyfikatorów na te, które możemy „wypowiedzieć”, zmiana typów anonimowych na ich znaną formę i zmiana języka AndAlsolingwistycznego drzewa wyrażeń w celu &&ujawnienia transformacji, które kompilator robi, aby osiągnąć równoważny w składni kropkowej:

var solutions = 
    Enumerable.Range(1,8) // from O in Enumerable.Range(1,8)
        .SelectMany(O => Enumerable.Range(1, 8), (O, N) => new { O = O, N = N }) // from N in Enumerable.Range(1,8)
        .Where(temp0 => temp0.O != temp0.N) // where O != N
        .Select(temp0 => new { temp0 = temp0, NO = 10 * temp0.N + temp0.O }) // let NO = 10 * N + O
        .Select(temp1 => new { temp1 = temp1, product = 4 * temp1.NO }) // let product = 4 * NO
        .Where(temp2 => temp2.product < 100) // where product < 100
        .Select(temp2 => new { temp2 = temp2, K = temp2.product % 10 }) // let K = product % 10
        .Where(temp3 => temp3.K != temp3.temp2.temp1.temp0.O && temp3.K != temp3.temp2.temp1.temp0.N && temp3.temp2.product / 10 == temp3.temp2.temp1.temp0.O)
        // where K != O && K != N && product / 10 == O
        .Select(temp3 => new { N = temp3.temp2.temp1.temp0.N, O = temp3.temp2.temp1.temp0.O, K = temp3.K });
        // select new { N, O, K };

foreach(var i in solutions)
{
    Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}

Co, jeśli uruchomisz, możesz sprawdzić, czy to ponownie generuje:

N = 1, O = 6, K = 4

... ale czy kiedykolwiek napisałbyś taki kod?

Założę się, że odpowiedź brzmi NONBHN (nie tylko nie, ale piekło nie!) - ponieważ jest to po prostu zbyt skomplikowane. Jasne, że możesz wymyślić bardziej znaczące nazwy identyfikatorów niż „temp0” .. „temp3”, ale chodzi o to, że nie dodają niczego do kodu - nie poprawiają kodu, nie poprawiają sprawi, że kod będzie lepiej czytany, tylko brzydko go poprawiają, a jeśli robisz to ręcznie, bez wątpienia zepsułbyś go raz lub trzy, zanim zrobisz to poprawnie. Ponadto granie w „grę z imionami” jest wystarczająco trudne do uzyskania znaczących identyfikatorów, dlatego z zadowoleniem przyjmuję odejście od gry z imionami, które kompilator zapewnia mi w zrozumieniu zapytań.

Próbka ta zagadka może nie być w świecie rzeczywistym na tyle, aby podjąć poważnie; istnieją jednak inne scenariusze, w których świecą pojęcia zapytań:

  • Złożoność Joini GroupJoin: zakres zmiennych zasięgu w joinklauzulach rozumienia zapytań zamienia błędy, które w przeciwnym razie mogłyby się kompilować w składni kropkowej, w błędy czasu kompilacji w składni rozumienia.
  • Za każdym razem, gdy kompilator wprowadza przejrzysty identyfikator w transformacji rozumienia, zrozumienie staje się opłacalne. Obejmuje to użycie któregokolwiek z poniższych: wielu fromklauzul oraz join& join..intoi letklauzul.

Znam więcej niż jeden warsztat inżynieryjny w moim rodzinnym mieście, który zabronił składni rozumienia. Myślę, że szkoda, ponieważ składnia rozumienia jest tylko narzędziem i jest w tym przydatna. Myślę, że to tak, jakby powiedzieć: „Są rzeczy, które można zrobić za pomocą śrubokręta, których nie można zrobić za pomocą dłuta. Ponieważ można użyć śrubokręta jako dłuta, dłuta są odtąd zakazane na mocy dekretu króla”.


-1: Wow. OP szukał małej porady. Wydałeś powieść! Czy mógłbyś to nieco zaostrzyć?
— Jim G.

8

Radzę używać składni rozumienia zapytania, gdy całe wyrażenie można wykonać w składni rozumienia. To znaczy wolałbym:

var query = from c in customers orderby c.Name select c.Address;

do

var query = customers.OrderBy(c=>c.Name).Select(c=>c.Address);

Ale wolałbym

int count = customers.Where(c=>c.City == "London").Count();

do

int count = (from c in customers where c.City == "London" select c).Count();

Chciałbym wymyślić jakąś składnię, która sprawiła, że ​​fajniej było mieszać te dwa. Coś jak:

int count = from c in customers 
            where c.City == "London" 
            select c 
            continue with Count();

Ale niestety nie zrobiliśmy tego.

Ale w zasadzie jest to kwestia preferencji. Zrób ten, który wygląda lepiej dla ciebie i twoich współpracowników.


3
Alternatywnie można rozważyć oddzielenie rozumienia od innych wywołań operatora LINQ poprzez refaktoryzację „wprowadzaj zmienną wyjaśniającą”. na przykładvar londonCustomers = from c in ...; int count = londonCustomers.Count();
— devgeezer

3

SQL-like to dobry sposób na rozpoczęcie. Ale ponieważ jest ograniczony (obsługuje tylko te konstrukcje, które obsługuje Twój obecny język), w końcu programiści wybierają styl rozszerzeń.

Chciałbym zauważyć, że istnieją pewne przypadki, które mogą być łatwo zaimplementowane w stylu SQL.

Możesz także połączyć oba sposoby w jednym zapytaniu.


2

Zwykle używam składni bez zapytania, chyba że muszę zdefiniować zmienną w połowie drogi, chociaż zapytanie jest podobne

from x in list
let y = x.DoExpensiveCalulation()
where y > 42
select y

ale piszę składnię bez zapytań jak

x.Where(c => filter)
 .Select(c => datatransform)

2

Zawsze korzystam z funkcji rozszerzenia z powodu zamawiania. Weźmy prosty przykład - w SQL, najpierw napisałeś select - nawet jeśli faktycznie wykonano go jako pierwszy. Kiedy piszesz przy użyciu metod rozszerzenia, mam większą kontrolę. Dostaję Intellisense o tym, co jest w ofercie, piszę rzeczy w kolejności, w jakiej się pojawiają.


Myślę, że przekonasz się, że w składni „rozumienia zapytań” kolejność na stronie jest taka sama, jak kolejność operacji. LINQ nie umieszcza „wybierz” na pierwszym miejscu, w przeciwieństwie do SQL.
— Eric Lippert

1

Lubię też funkcję rozszerzenia.

Może dlatego, że w moim umyśle jest to mniejszy skok składni.

Jest również bardziej czytelny dla oka, szczególnie jeśli używasz frameworków innych firm, które mają interfejs linq api.


0

Oto heurystyka, którą śledzę:

Preferuj wyrażenia LINQ zamiast lambdas, gdy masz połączenia.

Myślę, że lambdas ze złączami wyglądają na bałagan i są trudne do odczytania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.