568 Stimmen

Verwendung von LINQ zur Auswahl von Objekten mit minimalem oder maximalem Eigenschaftswert

Ich habe ein Person-Objekt mit einer nullbaren DateOfBirth-Eigenschaft. Gibt es eine Möglichkeit, LINQ zu verwenden, um eine Liste von Person-Objekten für die eine mit dem frühesten/kleinsten DateOfBirth-Wert abzufragen?

Damit habe ich angefangen:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

NulldateOfBirth-Werte werden auf DateTime.MaxValue gesetzt, um sie von der Min-Betrachtung auszuschließen (vorausgesetzt, mindestens einer hat ein bestimmtes Geburtsdatum).

Aber alles, was das für mich tut, ist, firstBornDate auf einen DateTime-Wert zu setzen. Was ich gerne hätte, ist das Person-Objekt, das dem entspricht. Muss ich eine zweite Abfrage wie folgt schreiben:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

Oder gibt es einen schlankeren Weg, dies zu tun?

347voto

Ana Betts Punkte 72423
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

262voto

Jon Skeet Punkte 1325502

Leider gibt es dafür keine eingebaute Methode, aber es ist einfach genug, sie selbst zu implementieren. Hier sind die Grundzüge der Methode:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer ??= Comparer<TKey>.Default;

    using (var sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var min = sourceIterator.Current;
        var minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            var candidate = sourceIterator.Current;
            var candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

Beispiel für die Verwendung:

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

Beachten Sie, dass dies eine Ausnahme auslöst, wenn die Sequenz leer ist, und die erste Element mit dem minimalen Wert, wenn es mehr als eines gibt.

Alternativ können Sie auch die Implementierung verwenden, die wir in MehrLINQ , in MinBy.cs . (Es gibt eine entsprechende MaxBy natürlich).

Installation über die Konsole des Paketmanagers:

PM> Install-Package morelinq

176voto

Lucas Punkte 16934

HINWEIS: Ich füge diese Antwort der Vollständigkeit halber ein, da der Auftraggeber die Datenquelle nicht erwähnt hat und wir keine Vermutungen anstellen sollten.

Diese Abfrage gibt die richtige Antwort, aber könnte langsamer sein da sie möglicherweise sortieren muss todo die Artikel in People je nachdem, welche Datenstruktur People ist:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();

UPDATE: Eigentlich sollte ich diese Lösung nicht als "naiv" bezeichnen, aber der Benutzer muss wissen, was er abfragt. Die "Langsamkeit" dieser Lösung hängt von den zugrunde liegenden Daten ab. Handelt es sich um ein Array oder List<T> dann hat LINQ to Objects keine andere Wahl, als zuerst die gesamte Sammlung zu sortieren, bevor das erste Element ausgewählt wird. In diesem Fall wird es langsamer sein als die andere vorgeschlagene Lösung. Wenn es sich jedoch um eine LINQ to SQL-Tabelle handelt und DateOfBirth eine indizierte Spalte ist, dann verwendet SQL Server den Index, anstatt alle Zeilen zu sortieren. Andere benutzerdefinierte IEnumerable<T> Implementierungen könnten auch Indizes verwenden (siehe i4o: Indizierter LINQ oder die Objektdatenbank db4o ) und machen diese Lösung schneller als Aggregate() o MaxBy() / MinBy() die die gesamte Sammlung einmal durchlaufen müssen. In der Tat hätte LINQ to Objects (theoretisch) Sonderfälle in OrderBy() für sortierte Sammlungen wie SortedList<T> aber das ist nicht der Fall, soweit ich weiß.

72voto

Rune FS Punkte 20934
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Das würde den Zweck erfüllen

60voto

KFL Punkte 15635

Sie fragen also nach ArgMin o ArgMax . C# hat keine eingebaute API für diese.

Ich habe nach einem sauberen und effizienten (O(n) in time) Weg gesucht, dies zu tun. Und ich glaube, ich habe einen gefunden:

Die allgemeine Form dieses Musters ist:

var min = data.Select(x => (key(x), x)).Min().Item2;
                            ^           ^       ^
              the sorting key           |       take the associated original item
                                Min by key(.)

Vor allem, wenn man das Beispiel aus der ursprünglichen Frage verwendet:

Für C# 7.0 und höher, das Folgendes unterstützt Wertetupel :

var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;

Für C# Versionen vor 7.0, anonymer Typ kann stattdessen verwendet werden:

var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;

Sie funktionieren, weil sowohl Wertetupel als auch anonymer Typ sinnvolle Standardkomparatoren haben: Für (x1, y1) und (x2, y2) wird zuerst verglichen x1 gegen x2 entonces y1 gegen y2 . Deshalb ist die eingebaute .Min kann für diese Typen verwendet werden.

Und da sowohl anonymous type als auch value tuple Werttypen sind, sollten sie beide sehr effizient sein.

ANMERKUNG

In meinem obigen ArgMin Implementierungen habe ich angenommen DateOfBirth zum Mitschreiben DateTime der Einfachheit und Klarheit halber. In der ursprünglichen Frage wird darum gebeten, die Einträge mit Null auszuschließen DateOfBirth Feld:

NulldateOfBirth-Werte werden auf DateTime.MaxValue gesetzt, um sie von der Min-Betrachtung auszuschließen (vorausgesetzt, mindestens einer hat ein bestimmtes Geburtsdatum).

Dies kann mit einer Vorfilterung erreicht werden

people.Where(p => p.DateOfBirth.HasValue)

Es ist also unerheblich für die Frage der Umsetzung ArgMin o ArgMax .

ANMERKUNG 2

Der obige Ansatz hat den Nachteil, dass, wenn es zwei Instanzen gibt, die denselben Min-Wert haben, die Min() Implementierung wird versuchen, die Instanzen zu vergleichen, um ein Unentschieden zu erreichen. Wenn die Klasse der Instanzen jedoch nicht IComparable dann wird ein Laufzeitfehler ausgelöst:

Mindestens ein Objekt muss IComparable implementieren

Zum Glück lässt sich das noch recht sauber beheben. Die Idee besteht darin, jedem Eintrag eine entfernte "ID" zuzuordnen, die als eindeutiger Tie-Breaker dient. Wir können eine inkrementelle ID für jeden Eintrag verwenden. Wir verwenden immer noch das Alter der Menschen als Beispiel:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X