Come utilizzare LINQ per selezionare un oggetto con valore di proprietà minimo o massimo


466

Ho un oggetto Person con una proprietà DateOfBirth Nullable. Esiste un modo per utilizzare LINQ per interrogare un elenco di oggetti Person per quello con il valore DateOfBirth più vecchio / più piccolo.

Ecco cosa ho iniziato con:

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

I valori Null DateOfBirth sono impostati su DateTime.MaxValue per escluderli dal corrispettivo Min (presupponendo che almeno uno abbia un DOB specificato).

Ma tutto ciò che fa per me è impostare firstBornDate su un valore DateTime. Quello che mi piacerebbe ottenere è l'oggetto Person che corrisponde a quello. Devo scrivere una seconda query in questo modo:

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

O c'è un modo più snello di farlo?


24
Solo un commento sul tuo esempio: probabilmente non dovresti usare Single qui. Sarebbe un'eccezione se due persone avessero lo stesso DateOfBirth
Niki

1
Vedi anche lo stackoverflow.com/questions/2736236/… quasi duplicato , che contiene alcuni esempi concisi.
arrivederci

4
Che caratteristica semplice e utile. MinBy dovrebbe essere nella libreria standard. Dovremmo inviare una richiesta pull a Microsoft github.com/dotnet/corefx
Colonnello Panic

2
Questo sembra esistere oggi, basta fornire una funzione per scegliere la proprietà:a.Min(x => x.foo);
jackmott

4
Per dimostrare il problema: in Python, max("find a word of maximal length in this sentence".split(), key=len)restituisce la stringa "frase". In C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)calcola che 8 è la lunghezza più lunga di qualsiasi parola, ma non vi dico ciò che la parola più lunga è .
Colonnello Panic,

Risposte:


299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

16
Probabilmente un po 'più lento della semplice implementazione di IComparable e usando Min (o un ciclo for). Ma +1 per una soluzione O (n) linqy.
Matthew Flaschen,

3
Inoltre, deve essere <curmin.DateOfBirth. Altrimenti, stai confrontando un DateTime con una persona.
Matthew Flaschen,

2
Fai anche attenzione quando lo usi per confrontare due orari di data. Lo stavo usando per trovare l'ultimo record di modifica in una raccolta non ordinata. Non è riuscito perché il disco che volevo è finito con la stessa data e ora.
Simon Gill,

8
Perché fai il controllo superfluo curMin == null? curMinpotrebbe essere solo nullse stavi usando Aggregate()un seme null.
Good Night Nerd Pride,


226

Sfortunatamente non esiste un metodo integrato per farlo, ma è abbastanza facile da implementare per te stesso. Eccone il coraggio:

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

Esempio di utilizzo:

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

Nota che ciò genererà un'eccezione se la sequenza è vuota e restituirà il primo elemento con il valore minimo se ce n'è più di uno.

In alternativa, puoi utilizzare l'implementazione che abbiamo in MoreLINQ , in MinBy.cs . (C'è un corrispondente MaxBy, ovviamente.)

Installa tramite la console di gestione dei pacchetti:

PM> Installa-pacchetto morelinq


1
Sostituirei Ienumerator + mentre con un foreach
ggf31416

5
Non è possibile farlo facilmente a causa della prima chiamata a MoveNext () prima del ciclo. Ci sono alternative, ma sono IMO più disordinate.
Jon Skeet,

2
Mentre potrei restituire il valore predefinito (T) che mi sembra inappropriato. Ciò è più coerente con metodi come First () e l'approccio dell'indicizzatore del dizionario. Potresti adattarlo facilmente se lo desideri.
Jon Skeet,

8
Ho assegnato la risposta a Paul a causa della soluzione non bibliotecaria, ma grazie per questo codice e il collegamento alla biblioteca MoreLINQ, che penso inizierò a usare!
slolife,


135

NOTA: includo questa risposta per completezza poiché l'OP non ha menzionato la fonte dei dati e non dovremmo fare ipotesi.

Questa query fornisce la risposta corretta, ma potrebbe essere più lenta poiché potrebbe essere necessario ordinare tutti gli elementi People, a seconda della struttura dei dati People:

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

AGGIORNAMENTO: In realtà non dovrei chiamare questa soluzione "ingenua", ma l'utente ha bisogno di sapere contro cosa sta interrogando. La "lentezza" di questa soluzione dipende dai dati sottostanti. Se si tratta di un array o List<T>, LINQ to Objects non ha altra scelta che ordinare l'intera raccolta prima di selezionare il primo elemento. In questo caso sarà più lento dell'altra soluzione suggerita. Tuttavia, se si tratta di una tabella LINQ to SQL ed DateOfBirthè una colonna indicizzata, SQL Server utilizzerà l'indice invece di ordinare tutte le righe. Altre IEnumerable<T>implementazioni personalizzate potrebbero anche utilizzare gli indici (vedere i4o: LINQ indicizzato o il database degli oggetti db4o ) e rendere questa soluzione più veloce diAggregate() o MaxBy()/MinBy()che devono ripetere l'intera collezione una volta. In effetti, LINQ to Objects avrebbe potuto (in teoria) creare casi speciali OrderBy()per raccolte ordinate come SortedList<T>, ma non lo so, per quanto ne so.


1
Qualcuno lo ha già pubblicato, ma apparentemente lo ha eliminato dopo che ho commentato quanto fosse lenta (e occupante spazio) (O (n log n) velocità al massimo rispetto a O (n) per min). :)
Matthew Flaschen,

sì, quindi il mio avvertimento sull'essere la soluzione ingenua :) tuttavia è molto semplice e potrebbe essere utilizzabile in alcuni casi (piccole raccolte o se DateOfBirth è una colonna di DB indicizzata)
Lucas,

un altro caso speciale (che non è nemmeno lì) è che sarebbe possibile usare la conoscenza di orderby e prima di fare una ricerca per il valore più basso senza ordinamento.
Rune FS

L'ordinamento di una raccolta è un'operazione Nlog (N) che non è migliore della complessità temporale o O (n). Se abbiamo solo bisogno di 1 elemento / oggetto da una sequenza che è min o max, penso che dovremmo attenerci alla complessità del tempo lineare.
Yawar Murtaza,

@yawar la raccolta potrebbe già essere ordinata (indicizzata più probabilmente) nel qual caso puoi avere O (log n)
Rune FS

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

Farebbe il trucco


1
Questo è fantastico! Ho usato con OrderByDesending (...). Prendi (1) nel mio caso di proiezione linq.
Vedran Mandić,

1
Questo utilizza l'ordinamento, che supera il tempo O (N) e utilizza anche la memoria O (N).
George Polevoy,

@GeorgePolevoy che presume che sappiamo molto sull'origine dei dati. Se l'origine dati ha già un indice ordinato su un determinato campo, questa sarebbe una costante (bassa) e sarebbe molto più veloce della risposta accettata che richiederebbe di attraversare l'intero elenco. Se invece l'origine dati è ad esempio un array, hai ovviamente ragione
Rune FS,

@RuneFS - dovresti comunque menzionarlo nella tua risposta perché è importante.
rory.ap,

La performance ti trascinerà verso il basso. L'ho imparato a mie spese. Se si desidera l'oggetto con il valore Min o Max, non è necessario ordinare l'intero array. Solo 1 scansione dovrebbe essere sufficiente. Guarda la risposta accettata o guarda il pacchetto MoreLinq.
Sau001,

35

Quindi stai chiedendo ArgMinoArgMax . C # non ha un'API integrata per quelli.

Ho cercato un modo pulito ed efficiente (O (n) in tempo) per farlo. E penso di averne trovato uno:

La forma generale di questo modello è:

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

In particolare, usando l'esempio nella domanda originale:

Per C # 7.0 e versioni successive che supportano la tupla valore :

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

Per la versione C # precedente alla 7.0, è possibile utilizzare invece il tipo anonimo :

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

Funzionano perché sia ​​la tupla di valore che il tipo anonimo hanno comparatori predefiniti ragionevoli: per (x1, y1) e (x2, y2), confronta prima x1vsx2 , quindi y1vs y2. Ecco perché il built-in .Minpuò essere utilizzato su questi tipi.

E poiché sia ​​il tipo anonimo che la tupla di valore sono tipi di valore, dovrebbero essere entrambi molto efficienti.

NOTA

Nelle mie ArgMinimplementazioni di cui sopra ho assunto la DateOfBirthpremessa DateTimeper semplicità e chiarezza. La domanda originale chiede di escludere quelle voci con nullDateOfBirth campo :

I valori Null DateOfBirth sono impostati su DateTime.MaxValue per escluderli dal corrispettivo Min (presupponendo che almeno uno abbia un DOB specificato).

Può essere ottenuto con un pre-filtro

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

Quindi è irrilevante per la questione dell'implementazione ArgMin o ArgMax.

NOTA 2

L'approccio sopra ha un avvertimento che quando ci sono due istanze con lo stesso valore minimo, l' Min()implementazione proverà a confrontare le istanze come un tie-breaker. Tuttavia, se la classe delle istanze non viene implementataIComparable , verrà generato un errore di runtime:

Almeno un oggetto deve implementare IComparable

Fortunatamente, questo può ancora essere risolto piuttosto chiaramente. L'idea è di associare un "ID" distanziato a ciascuna voce che funge da inequivocabile tie-breaker. Possiamo usare un ID incrementale per ogni voce. Sempre usando l'età della gente come esempio:

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

1
Questo non sembra funzionare quando il tipo di valore è la chiave di ordinamento. "Almeno un oggetto deve implementare IComparable"
liang

1
troppo grande! questa dovrebbe essere la risposta migliore.
Guido Mocha,

@liang sì buona cattura. Fortunatamente c'è ancora una soluzione pulita a questo. Vedere la soluzione aggiornata nella sezione "Nota 2".
KFL

Seleziona può darti l'ID! var youngest = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy,

19

Soluzione senza pacchetti extra:

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

inoltre puoi avvolgerlo nell'estensione:

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

e in questo caso:

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

A proposito ... O (n ^ 2) non è la soluzione migliore. Paul Betts ha dato una soluzione più grassa della mia. Ma la mia è ancora la soluzione LINQ ed è più semplice e più breve di altre soluzioni qui.


3
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

Uso perfettamente aggregato dell'aggregato (equivalente a piegare in altre lingue):

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

L'unico aspetto negativo è che si accede alla proprietà due volte per elemento sequenza, il che potrebbe essere costoso. È difficile da risolvere.


1

Di seguito è la soluzione più generica. Fa essenzialmente la stessa cosa (nell'ordine O (N)) ma su qualsiasi tipo IEnumberable e può mescolarsi con tipi i cui selettori di proprietà potrebbero restituire null.

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

test:

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

0

MODIFICA di nuovo:

Scusate. Oltre a perdere il nullable stavo guardando la funzione sbagliata,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) restituisce il tipo di risultato come detto.

Direi che una possibile soluzione è implementare IComparable e usare Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , che in realtà restituisce un elemento da IEnumerable. Naturalmente, ciò non ti aiuta se non puoi modificare l'elemento. Trovo che il design di MS sia un po 'strano qui.

Ovviamente, puoi sempre fare un ciclo for se necessario o usare l'implementazione MoreLINQ di Jon Skeet.


0

Un'altra implementazione, che potrebbe funzionare con chiavi di selezione nullable, e per la raccolta del tipo di riferimento restituisce null se non viene trovato alcun elemento adatto. Ciò potrebbe essere utile, ad esempio, per l'elaborazione dei risultati del database.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

Esempio:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);

-2

Stavo cercando qualcosa di simile da solo, preferibilmente senza usare una libreria o ordinare l'intero elenco. La mia soluzione è risultata simile alla domanda stessa, semplificata un po '.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));

Non sarebbe molto più efficiente ottenere il minimo prima dell'istruzione linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Altrimenti riceve ripetutamente il minuto finché non trova quello che stai cercando.
Nieminen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.