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?

Erin
źródło
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.

klir2m
źródło
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.

John Kraft
źródło
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”.

devgeezer
źródło
-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.

Eric Lippert
źródło
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.

SiberianGuy
źródło
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)

źródło
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ą.

DeadMG
źródło
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.

Erion
źródło
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.

Jim G.
źródło