LINQ Style Einstellung [geschlossen]

21

Ich bin dazu gekommen, LINQ in meiner täglichen Programmierung häufig zu verwenden. Tatsächlich verwende ich selten, wenn überhaupt, eine explizite Schleife. Ich habe jedoch festgestellt, dass ich die SQL-ähnliche Syntax nicht mehr verwende. Ich benutze nur die Erweiterungsfunktionen. Also lieber dann sagen:

from x in y select datatransform where filter 

Ich benutze:

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

Welchen LINQ-Stil bevorzugen Sie und womit fühlen sich andere in Ihrem Team wohl?

Erin
quelle
5
Es kann erwähnenswert sein, dass die offizielle Haltung der MS darin besteht, dass die Abfragesyntax vorzuziehen ist.
R0MANARMY
1
Letztendlich spielt es keine Rolle. Wichtig ist, dass der Code verständlich ist. Eine Form kann in einem Fall besser sein, die andere in einem anderen Fall. So verwenden Sie, was immer zu der Zeit angemessen ist.
ChrisF
Ich glaube, Ihr zweites Beispiel heißt Lambda-Syntax, die ich in 95% der Fälle verwende. Die anderen 5% verwende ich die Abfragesyntax, die ist, wenn ich Verknüpfungen mache, ich versuche, zu Lambda-Syntax-Verknüpfungen überzugehen, aber wie andere darauf hingewiesen haben, wird es chaotisch.
The Muffin Man

Antworten:

26

Ich finde es bedauerlich, dass Microsoft laut MSDN-Dokumentation die Abfragesyntax bevorzugt, da ich sie nie verwende, aber immer die LINQ-Methodensyntax verwende. Ich liebe es, One-Liner-Anfragen nach Herzenslust abzufeuern. Vergleichen Sie:

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

Zu:

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

Schneller, weniger Linien, und für meine Augen sieht es sauberer aus. Die Abfragesyntax unterstützt auch nicht alle Standard-LINQ-Operatoren. Eine Beispielabfrage, die ich kürzlich durchgeführt habe, sah ungefähr so ​​aus:

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

Meines Wissens würde es so aussehen, wenn diese Abfrage mithilfe der Abfragesyntax (soweit möglich) repliziert wird:

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

Sieht für mich nicht lesbarer aus, und Sie müssen sowieso wissen, wie man Methodensyntax verwendet. Persönlich bin ich sehr verliebt in den deklarativen Stil, den LINQ ermöglicht, und nutze ihn in jeder Situation, in der es überhaupt möglich ist - vielleicht manchmal zu meinem Nachteil. Beispiel: Mit der Methodensyntax kann ich Folgendes tun:

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

Ich würde mir vorstellen, dass der obige Code für jemanden, der ohne gute Dokumentation in das Projekt einsteigt, schwer zu verstehen ist, und wenn er keinen soliden Hintergrund in LINQ hat, versteht er ihn möglicherweise sowieso nicht. Dennoch bietet die Methodensyntax eine ziemlich leistungsstarke Funktion, um schnell (in Bezug auf Codezeilen) eine Abfrage zu projizieren, um aggregierte Informationen zu mehreren Auflistungen abzurufen, die andernfalls eine Menge langwieriger foreach-Schleifen erfordern würden. In einem solchen Fall ist die Methodensyntax äußerst kompakt für das, was Sie daraus machen. Der Versuch, dies mit der Abfragesyntax zu tun, kann ziemlich schnell unhandlich werden.

klir2m
quelle
Die Umwandlung, die Sie in der Auswahl vornehmen können, aber leider können Sie nicht angeben, ob Sie die Top-X-Datensätze übernehmen möchten, ohne die LINQ-Methoden zu verwenden. Dies ist besonders ärgerlich, wenn Sie wissen, dass Sie nur einen einzigen Datensatz benötigen und alle Abfragen in Klammern setzen müssen.
Ziv
2
Nur für den Datensatz können Sie Select (x => x.ItemInfo) .OfType <GeneralMerchInfo> () anstelle von Where (). Select (). Cast <> () ausführen, was meiner Meinung nach schneller ist (großes O von 2n) statt n * 2m denke ich). Aber Sie haben vollkommen Recht, die Lambda-Syntax ist vom Standpunkt der Lesbarkeit viel besser.
Ed James
16

Ich finde die funktionale Syntax für das Auge angenehmer. Die einzige Ausnahme ist, wenn ich mehr als zwei Sätze verbinden muss. Das Join () wird sehr schnell verrückt.

John Kraft
quelle
Einverstanden ... Ich bevorzuge das Aussehen und die Lesbarkeit der Erweiterungsmethoden, außer (wie bereits erwähnt) beim Beitritt. Komponentenanbieter (z. B. Telerik) verwenden die Erweiterungsmethoden häufig. Das Beispiel, über das ich nachdenke, sind die Rad Controls in ASP.NET MVC. Sie müssen sehr gut mit Erweiterungsmethoden umgehen können, um diese zu verwenden / zu lesen.
Catchops
Kam das zu sagen. Normalerweise benutze ich Lambdas, es sei denn, ein Join ist beteiligt. Sobald es einen Join gibt, ist die LINQ-Syntax in der Regel besser lesbar.
Sean
10

Ist es jemals zu spät, eine weitere Antwort hinzuzufügen?

Ich habe eine Menge LINQ-to-Objects-Code geschrieben und behaupte, dass es zumindest in diesem Bereich gut ist, beide Syntaxen zu verstehen, um den einfacheren Code zu verwenden - was nicht immer Punktsyntax ist.

Natürlich gibt es Zeiten , in denen Punktsyntax IS der Weg zu gehen - andere haben mehrere dieser Fälle vorgesehen ist ; Ich denke jedoch, dass das Verständnis zu kurz gekommen ist - wenn man so will, mit einem schlechten Ruf. Ich werde daher ein Beispiel bereitstellen, in dem ich Verständnis für nützlich halte.

Hier ist eine Lösung für ein Ziffernersetzungspuzzle: (Lösung, die mit LINQPad geschrieben wurde, aber in einer Konsolen-App eigenständig sein kann)

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

... welche Ausgänge:

N = 1, O = 6, K = 4

Nicht schlecht, die Logik fließt linear und wir können sehen, dass es eine einzige richtige Lösung gibt. Dieses Rätsel lässt sich leicht von Hand lösen: Die Schlussfolgerung 3>> N0 und O> 4 * N impliziert 8> = O> = 4. Das bedeutet, dass maximal 10 Fälle von Hand getestet werden können (2 für N-by- 5 für O). Ich habe mich genug verirrt - dieses Rätsel wird zur Veranschaulichung von LINQ angeboten.

Compiler-Transformationen

Der Compiler unternimmt viel, um dies in äquivalente Punktsyntax umzusetzen. Abgesehen davon, dass die üblichen zweiten und nachfolgenden fromKlauseln in SelectManyAufrufe umgewandelt werden , gibt es letKlauseln, die zu SelectAufrufen mit Projektionen werden und beide transparente Bezeichner verwenden . Wie ich gleich zeigen werde, beeinträchtigt das Benennen dieser Bezeichner in der Punktsyntax die Lesbarkeit dieses Ansatzes.

Ich habe einen Trick, um herauszufinden, was der Compiler bei der Übersetzung dieses Codes in Punktsyntax tut. Wenn Sie die beiden obigen Kommentarzeilen auskommentieren und erneut ausführen, erhalten Sie die folgende Ausgabe:

N = 1, O = 6, K = 4

Lösungsausdrucksbaum 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)). Wählen Sie (<> h_ TransparentIdentifier1 => new <> 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))). Where (<> 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 _TransparentIdentifier2. product / 10) == <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.O))). Select (<> h_ TransparentIdentifier3 => new <> 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))

Setzen Sie jeden LINQ-Operator in eine neue Zeile, übersetzen Sie die "unaussprechlichen" Bezeichner in diejenigen, die wir "sprechen" können, ändern Sie die anonymen Typen in ihre vertraute Form und ändern Sie den AndAlsoAusdrucksbaumjargon, um &&die Transformationen anzuzeigen, die der Compiler ausführt, um zu einem Äquivalent zu gelangen in Punktsyntax:

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

Was, wenn Sie ausführen, können Sie überprüfen, dass es wieder ausgibt:

N = 1, O = 6, K = 4

... aber würdest du jemals so einen Code schreiben?

Ich würde wetten, dass die Antwort NONBHN ist (nicht nur Nein, sondern verdammt Nein!) - weil es einfach zu komplex ist. Sicher können Sie sich aussagekräftigere Bezeichnernamen als "temp0" .. "temp3" einfallen lassen, aber der Punkt ist, dass sie dem Code nichts hinzufügen - sie sorgen nicht dafür, dass der Code besser funktioniert, sie tun es nicht Verbessern Sie die Lesbarkeit des Codes, da der Code nur hässlich ist. Wenn Sie dies von Hand tun würden, würden Sie ihn ohne Zweifel ein oder drei Mal durcheinander bringen, bevor Sie es richtig machen. Außerdem ist das Spielen des "Namensspiels" für aussagekräftige Bezeichner schwierig genug, sodass ich die Unterbrechung des Namensspiels begrüße, die mir der Compiler beim Verständnis von Abfragen bereitstellt.

Dieses Rätselbeispiel ist möglicherweise nicht real genug, um von Ihnen ernst genommen zu werden. Es gibt jedoch auch andere Szenarien, in denen das Abfrageverständnis glänzt:

  • Die Komplexität von Joinund GroupJoin: das Scoping von Bereichsvariablen in Abfrageverständnisklauseln joinwandelt Fehler, die ansonsten in Punktsyntax kompiliert würden, in Fehler zur Kompilierungszeit in der Verständnissyntax um.
  • Jedes Mal, wenn der Compiler einen transparenten Bezeichner in die Verstehenstransformation einführt, werden Verstehen lohnenswert. Dies schließt die Verwendung einer der folgenden fromKlauseln ein : multiple Klauseln, join& join..intoKlauseln und letKlauseln.

Ich kenne mehr als eine Werkstatt in meiner Heimatstadt, in der die Syntax des Verstehens verboten ist . Ich denke, das ist schade, da die Syntax des Verständnisses nur ein Werkzeug und noch dazu ein nützliches ist. Ich denke, es ist sehr ähnlich zu sagen: "Es gibt Dinge, die Sie mit einem Schraubenzieher tun können, die Sie nicht mit einem Meißel tun können. Da Sie einen Schraubenzieher als Meißel verwenden können, werden Meißel von nun an auf Anordnung des Königs verboten."

Devgeezer
quelle
-1: Wow. Das OP suchte einen kleinen Rat. Du hast einen Roman geschrieben! Würde es Ihnen etwas ausmachen, dies etwas zu verschärfen?
Jim G.
8

Mein Rat ist, die Abfrage-Verständnis-Syntax zu verwenden, wenn der gesamte Ausdruck in der Verständnis-Syntax ausgeführt werden kann. Das heißt, ich würde bevorzugen:

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

zu

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

Aber ich würde es vorziehen

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

zu

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

Ich wünschte, wir hätten uns eine Syntax ausgedacht, die es angenehmer macht, beides zu mischen. So etwas wie:

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

Aber leider haben wir nicht.

Aber im Grunde ist es eine Frage der Präferenz. Tun Sie das, was für Sie und Ihre Mitarbeiter besser aussieht.

Eric Lippert
quelle
3
Alternativ können Sie überlegen, ein Verständnis von anderen LINQ-Operator-Aufrufen durch ein Refactoring mit "Erklärungsvariablen einführen" zu trennen. Zum Beispielvar londonCustomers = from c in ...; int count = londonCustomers.Count();
devgeezer
3

SQL-like ist ein guter Anfang. Da es jedoch begrenzt ist (es unterstützt nur die Konstruktionen, die Ihre aktuelle Sprache unterstützt), entscheiden sich Entwickler letztendlich für den Stil von Erweiterungsmethoden.

Ich möchte darauf hinweisen, dass es einige Fälle gibt, die leicht durch SQL-ähnlichen Stil implementiert werden können.

Sie können auch beide Möglichkeiten in einer Abfrage kombinieren.

SiberianGuy
quelle
2

Ich neige dazu, die Nicht-Abfragesyntax zu verwenden, es sei denn, ich muss eine Variable auf halbem Weg durch die Abfrage definieren

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

aber ich schreibe die nicht abfragesyntax gerne

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

quelle
2

Ich benutze wegen der Bestellung immer die Erweiterungsfunktionen. Nehmen Sie Ihr einfaches Beispiel - in der SQL haben Sie zuerst select geschrieben - obwohl eigentlich das, was zuerst ausgeführt wurde. Wenn Sie mit den Erweiterungsmethoden schreiben, habe ich viel mehr Kontrolle. Ich bekomme ein Gespür dafür, was angeboten wird, ich schreibe Dinge in der Reihenfolge, in der sie geschehen.

DeadMG
quelle
Ich denke, Sie werden feststellen, dass in der Syntax "Abfrageverständnis" die Reihenfolge auf der Seite mit der Reihenfolge übereinstimmt, in der die Vorgänge ausgeführt werden. Im Gegensatz zu SQL wird bei LINQ das "Auswählen" nicht an erster Stelle gesetzt.
Eric Lippert
1

Mir gefällt auch die Extension-Funktion.

Vielleicht, weil es für mich weniger ein Syntaxsprung ist.

Es fühlt sich auch für das Auge besser lesbar an, insbesondere wenn Sie Frameworks von Drittanbietern verwenden, die über linq api verfügen.

Erion
quelle
0

Hier ist die Heuristik, der ich folge:

Bevorzugen Sie LINQ-Ausdrücke gegenüber Lambdas, wenn Sie Joins haben.

Ich denke, dass Lambdas mit Joins unordentlich aussehen und schwer zu lesen sind.

Jim G.
quelle