it-swarm.com.de

Verwendung von LINQ zum Auswählen von Objekten mit minimalem oder maximalem Eigenschaftswert

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

Hier ist, womit ich angefangen habe:

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

Null-DateOfBirth-Werte werden auf DateTime.MaxValue festgelegt, um sie von der Min-Berücksichtigung auszuschließen (vorausgesetzt, mindestens einer hat ein angegebenes DOB).

Aber alles, was ich tun kann, ist firstBornDate auf einen DateTime-Wert zu setzen. Was ich bekommen möchte, ist das Objekt Person, das dazu passt. Muss ich eine zweite Abfrage wie folgt schreiben:

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

Oder gibt es eine schlankere Art, das zu tun?

421
slolife
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
278
Paul Betts

Leider gibt es keine eingebaute Methode, um dies zu tun.

PM> Installationspaket morelinq

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

Alternativ können Sie die Implementierung verwenden, die wir in MoreLINQ haben, in MinBy.cs . (Es gibt natürlich ein entsprechendes MaxBy.) Hier ist der Mut dazu:

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

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

209
Jon Skeet

HINWEIS: Ich füge diese Antwort der Vollständigkeit halber bei, da im OP die Datenquelle nicht genannt wurde und wir keine Annahmen treffen sollten.

Diese Abfrage gibt die richtige Antwort, aber könnte langsamer sein da es möglicherweise erforderlich ist alle die Elemente in People zu sortieren, abhängig von der 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, wovon er abfragt. Die "Langsamkeit" dieser Lösung hängt von den zugrunde liegenden Daten ab. Wenn dies ein Array oder List<T> Ist, hat LINQ to Objects keine andere Wahl, als zuerst die gesamte Sammlung zu sortieren, bevor Sie das erste Element auswählen. In diesem Fall ist es langsamer als die andere vorgeschlagene Lösung. Wenn dies jedoch eine LINQ to SQL-Tabelle ist und DateOfBirth eine indizierte Spalte ist, verwendet SQL Server den Index, anstatt alle Zeilen zu sortieren. Andere benutzerdefinierte IEnumerable<T> - Implementierungen könnten ebenfalls Indizes verwenden (siehe i4o: Indexed LINQ oder die Objektdatenbank db4o ) und diese Lösung schneller machen als Aggregate() oder MaxBy()/MinBy(), die die gesamte Sammlung einmal durchlaufen müssen. Tatsächlich hätte LINQ to Objects (theoretisch) Sonderfälle in OrderBy() für sortierte Sammlungen wie SortedList<T> Erstellen können, aber meines Wissens nicht.

118
Lucas
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Würde den Trick machen

60
Rune FS

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

Ich habe nach einem sauberen und effizienten Weg gesucht, dies zu tun. Und ich glaube ich habe eines 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(.)
_

Verwenden Sie speziell das Beispiel in der ursprünglichen Frage:

Unterstützt ab C # 7.0 Wert Tuple :

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

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

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

Sie funktionieren, weil sowohl der Wert Tuple als auch der anonyme Typ sinnvolle Standardvergleicher haben: Für (x1, y1) und (x2, y2) werden zuerst _x1_ vs _x2_ und dann _y1_ vs _y2_. Aus diesem Grund kann der integrierte _.Min_ für diese Typen verwendet werden.

Und da sowohl anonymer Typ als auch Wert-Tupel Werttypen sind, sollten beide sehr effizient sein.

[~ # ~] note [~ # ~]

In meinen obigen ArgMin Implementierungen habe ich angenommen, dass DateOfBirth der Einfachheit und Klarheit halber den Typ DateTime verwendet. In der ursprünglichen Frage wird gefragt, ob diese Einträge mit dem Feld null DateOfBirth ausgeschlossen werden sollen:

Null-DateOfBirth-Werte werden auf DateTime.MaxValue festgelegt, um sie von der Min-Berücksichtigung auszuschließen (vorausgesetzt, mindestens einer hat ein angegebenes DOB).

Dies kann mit einer Vorfilterung erreicht werden

_people.Where(p => p.DateOfBirth.HasValue)
_

Daher ist es für die Frage der Implementierung von ArgMin oder ArgMax unerheblich.

ANMERKUNG 2

Der obige Ansatz hat den Vorbehalt, dass die Implementierung von Min() versucht, die Instanzen als Tiebreaker zu vergleichen, wenn zwei Instanzen denselben min-Wert haben. Wenn die Klasse der Instanzen jedoch IComparable nicht implementiert, wird ein Laufzeitfehler ausgegeben:

Mindestens ein Objekt muss IComparable implementieren

Zum Glück lässt sich das noch ziemlich sauber beheben. Die Idee ist, jedem Eintrag eine eindeutige "ID" zuzuordnen, die als eindeutiger Trenner dient. Wir können eine inkrementelle ID für jeden Eintrag verwenden. Verwenden Sie 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;
_
21
KFL

Lösung ohne zusätzliche Pakete:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

sie können es auch in eine Erweiterung packen:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

und in diesem Fall:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

Übrigens ... O (n ^ 2) ist nicht die beste Lösung. Paul Betts gab fatster Lösung als meine. Aber meine ist immer noch eine LINQ-Lösung und sie ist einfacher und kürzer als andere Lösungen hier.

11
Andrew
public class Foo {
    public int bar;
    public int stuff;
};

void Main()
{
    List<Foo> fooList = new List<Foo>(){
    new Foo(){bar=1,stuff=2},
    new Foo(){bar=3,stuff=4},
    new Foo(){bar=2,stuff=3}};

    Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v);
    result.Dump();
}
3
JustDave

Das Folgende ist die allgemeinere Lösung. Es macht im Wesentlichen dasselbe (in der Reihenfolge O(N)), jedoch für alle IEnumberable-Typen und kann mit Typen gemischt werden, deren Eigenschaftsselektoren null zurückgeben könnten.

public static class LinqExtensions
{
    public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> selector)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        if (selector == null)
        {
            throw new ArgumentNullException(nameof(selector));
        }
        return source.Aggregate((min, cur) =>
        {
            if (min == null)
            {
                return cur;
            }
            var minComparer = selector(min);
            if (minComparer == null)
            {
                return cur;
            }
            var curComparer = selector(cur);
            if (curComparer == null)
            {
                return min;
            }
            return minComparer.CompareTo(curComparer) > 0 ? cur : min;
        });
    }
}

Tests:

var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
0
Anonymous

Perfekt einfache Verwendung von Aggregat (entspricht in anderen Sprachen falten):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

Der einzige Nachteil ist, dass auf die Eigenschaft zweimal pro Sequenzelement zugegriffen wird, was teuer sein kann. Das ist schwer zu beheben.

0
david.pfx

Erneut BEARBEITEN:

Es tut uns leid. Abgesehen davon, dass ich die Nullable vermisst habe, habe ich mir die falsche Funktion angesehen,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>) gibt das zurück Ergebnistyp wie Sie sagten.

Ich würde sagen, dass eine mögliche Lösung darin besteht, IComparable zu implementieren und Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>) zu verwenden, was wirklich ein Element zurückgibt aus dem IEnumerable. Das hilft Ihnen natürlich nicht, wenn Sie das Element nicht ändern können. Ich finde das Design von MS hier etwas seltsam.

Natürlich können Sie jederzeit eine for-Schleife erstellen oder die von Jon Skeet bereitgestellte MoreLINQ-Implementierung verwenden.

0