Minimum veya maksimum özellik değeri olan nesneyi seçmek için LINQ nasıl kullanılır


466

Nullable DateOfBirth özelliği olan bir Person nesnesi var. En erken / en küçük DateOfBirth değerine sahip bir Kişi nesneleri listesini sorgulamak için LINQ kullanmanın bir yolu var mı.

İşte bunlarla başladım:

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

Boş DateOfBirth değerleri, bunları Min değerlendirmesinin dışında bırakmak için DateTime.MaxValue olarak ayarlanır (en az bir tanesinin belirli bir DOB'u olduğu varsayılarak).

Ama benim için tek yapmanız gereken firstBornDate öğesini bir DateTime değerine ayarlamak. Almak istediğim, buna uyan Kişi nesnesidir. Bunun gibi ikinci bir sorgu yazmak gerekir mi:

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

Yoksa bunu yapmak için daha yalın bir yol var mı?


24
Sadece örneğinize bir yorum: Muhtemelen burada Single kullanmamalısınız. İki kişi aynı DateOfBirth'e sahip olsaydı bir istisna atar
Niki

1
Ayrıca bazı özlü örnekleri olan neredeyse yinelenen stackoverflow.com/questions/2736236/… adresine bakın.
goodeye

4
Ne kadar basit ve kullanışlı bir özellik. MinBy'nin standart kütüphanede olması gerekir. Microsoft github.com/dotnet/corefx
Albay

2
Bu bugün var gibi görünüyor, sadece özelliği seçmek için bir işlev sağlamak:a.Min(x => x.foo);
jackmott

4
Sorunu göstermek için: Python'da max("find a word of maximal length in this sentence".split(), key=len)'cümle' dizesini döndürür. C # olarak "find a word of maximal length in this sentence".Split().Max(word => word.Length)hesaplar ve 8 herhangi bir kelimenin en uzun boy, ancak en uzun kelime ne diyeceğim yok olduğunu .
Albay Panik

Yanıtlar:


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

16
Muhtemelen sadece IComparable uygulamak ve Min (veya bir for döngüsü) kullanmaktan biraz daha yavaştır. Ancak O (n) linqy çözeltisi için +1.
Matthew Flaschen

3
Ayrıca, <curmin.DateOfBirth olması gerekir. Aksi takdirde, DateTime'ı bir Kişi ile karşılaştırırsınız.
Matthew Flaschen

2
Ayrıca bunu iki tarih saatini karşılaştırmak için kullanırken dikkatli olun. Sırasız bir koleksiyondaki son değişiklik kaydını bulmak için bunu kullanıyordum. İstediğim kayıt aynı tarih ve saatle sonuçlandığı için başarısız oldu.
Simon Gill

8
Neden gereksiz kontrol yapıyorsunuz curMin == null? curMinsadece bir tohumla nullkullanıyorsan olabilirdi . Aggregate()null
İyi Geceler Nerd Pride


226

Ne yazık ki, bunu yapmak için yerleşik bir yöntem yoktur, ancak kendiniz için uygulamak yeterince kolaydır. İşte bunun cesaretleri:

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

Örnek kullanım:

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

Sekans boşsa bunun bir istisna atacağını ve birden fazla varsa ilk öğeyi minimum değerle döndüreceğini unutmayın .

Alternatif olarak, biz var uygulanmasını kullanabilirsiniz MoreLINQ içinde, MinBy.cs . ( MaxByElbette buna karşılık gelen bir şey var .)

Paket yöneticisi konsolu üzerinden yükleme:

PM> Kurulum Paketi morelinq


1
Ienumerator + 'ı bir foreach ile değiştiririm
ggf31416

5
Döngüden önceki ilk MoveNext () çağrısı nedeniyle bunu kolayca yapamazsınız. Alternatifler var, ama daha da IMO.
Jon Skeet

2
Ben iken olabilir bana uygunsuz hissediyor varsayılan (T) döndürür. Bu, First () ve Dictionary dizinleyicisinin yaklaşımı gibi yöntemlerle daha tutarlıdır. İsterseniz kolayca adapte olabilirsiniz.
Jon Skeet

8
Kütüphane dışı çözüm nedeniyle Paul'e cevap verdim, ancak bu kod için teşekkürler ve kullanmaya başlayacağımı düşündüğüm MoreLINQ kütüphanesine bağlantı!
slolife


135

NOT: OP'nin veri kaynağının ne olduğundan bahsetmediği ve herhangi bir varsayımda bulunmamamız gerektiği için bu cevabı tamlık için ekliyorum.

Bu sorgu doğru yanıtı verir, ancak veri yapısının ne olduğuna bağlı olarak tüm öğeleri sıralamak zorunda kalabileceğinden daha yavaş olabilir :PeoplePeople

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

GÜNCELLEME: Aslında bu çözümü "saf" dememeliyim, ancak kullanıcının neye karşı sorguladığını bilmesine gerek yok. Bu çözümün "yavaşlığı" temel verilere bağlıdır. Bu bir dizi veya List<T>daha sonra, LINQ to Objects seçeneği, ilk öğeyi seçmeden önce tüm koleksiyonu sıralamaktan başka bir seçeneğe sahip değildir. Bu durumda önerilen diğer çözümden daha yavaş olacaktır. Ancak, bu bir LINQ to SQL tablosu ve DateOfBirthdizinli bir sütun ise, SQL Server tüm satırları sıralamak yerine dizini kullanır. Diğer özel IEnumerable<T>uygulamalar da dizinleri kullanabilir (bkz. İ4o: Dizinlenmiş LINQ veya nesne veritabanı db4o ) ve bu çözümü Aggregate()veya MaxBy()/MinBy()tüm koleksiyonu bir kez yinelemek gerekiyor. Aslında, Nesneler için LINQ (teorik olarak) OrderBy()gibi sıralanmış koleksiyonlar için özel durumlar yapabilirdi SortedList<T>, ama bildiğim kadarıyla değil.


1
Birisi bunu zaten yayınladı, ancak görünüşe göre ne kadar yavaş (ve alan tüketen) olduğunu söyledikten sonra sildi (min için O (n) ile karşılaştırıldığında en iyi O (n log n) hızı). :)
Matthew Flaschen

evet, bu yüzden saf çözüm olma konusunda uyarım :) ancak ölü basit ve bazı durumlarda kullanılabilir olabilir (küçük koleksiyonlar veya DateOfBirth dizinlenmiş bir DB sütunu ise)
Lucas

başka bir özel durum (ya da orada değildir), sıralama bilgisini kullanmanın ve önce sıralama yapmadan en düşük değeri aramak için mümkün olabileceğidir.
Rune FS

Bir koleksiyonun sıralanması doğrusal veya O (n) zaman karmaşıklığından daha iyi olmayan Nlog (N) işlemidir. Min veya maks olan bir sekanstan sadece 1 elemente / nesneye ihtiyacımız varsa, bence Doğrusal zaman kompleksliğine bağlı kalmalıyız.
Yawar Murtaza

@yawar koleksiyonu zaten sıralanabilir (daha büyük olasılıkla dizine eklenmiş olabilir), bu durumda O (giriş n) olabilir
Rune FS

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

Hile yapmak


1
Bu harika! Ben OrderByDesending (...) ile kullandım. Linq projeksiyon benim durumumda (1) alın.
Vedran Mandić

1
Bu, O (N) süresini aşan ve aynı zamanda O (N) belleği kullanan sıralama kullanır.
George Polevoy

@GeorgePolevoy, veri kaynağı hakkında çok şey bildiğimizi varsayar. Veri kaynağı zaten verilen alanda sıralanmış bir dizine sahipse, bu (düşük) bir sabit olur ve tüm listeyi taramak için gereken kabul edilen cevaptan çok daha hızlı olur. Öte yandan veri kaynağı örneğin bir dizi ise, elbette haklısınız
Rune FS

@RuneFS - Cevabınızda yine de belirtmelisiniz çünkü önemli.
rory.ap

Performans sizi aşağı çekecek. Zor yoldan öğrendim. Nesnenin Min veya Maks değerine sahip olmasını istiyorsanız, dizinin tamamını sıralamanız gerekmez. Sadece 1 tarama yeterlidir. Kabul edilen cevaba veya MoreLinq paketine bakın.
Sau001

35

Demek istiyorsun ArgMinya da ArgMax. C # için bunlar için yerleşik bir API yoktur.

Bunu yapmak için temiz ve verimli (O (n) zamanında) bir yol arıyordum. Ve sanırım birini buldum:

Bu desenin genel formu:

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

Özellikle, orijinal sorudaki örneği kullanarak:

Değer demetini destekleyen C # 7.0 ve üstü için :

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

7.0'dan önceki C # sürümü için bunun yerine anonim tür kullanılabilir:

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

Değer tanımlama grubu ve anonim tip hem mantıklı varsayılan comparers var çünkü çalışmak: (x1, y1) ve (x2, y2) için, ilk karşılaştırır x1vs x2sonra, y1vs y2. Bu yüzden yerleşik .Minbu türlerde kullanılabilir.

Hem anonim tür hem de değer grubu değer türleri olduğu için ikisi de çok verimli olmalıdır.

NOT

Benim yukarıda yılında ArgMinuygulamalarda ben kabul DateOfBirthtipini almaya DateTimesadelik ve netlik için. Orijinal soru, boş DateOfBirthalana sahip girişleri hariç tutmanızı ister :

Boş DateOfBirth değerleri, bunları Min değerlendirmesinin dışında bırakmak için DateTime.MaxValue olarak ayarlanır (en az bir tanesinin belirli bir DOB'u olduğu varsayılarak).

Ön filtreleme ile elde edilebilir

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

Yani, uygulama ArgMinya da ArgMax.

NOT 2

Yukarıdaki yaklaşım, aynı min değerine sahip iki örnek olduğunda, Min()uygulamanın örnekleri bir bağlantı kesici olarak karşılaştırmaya çalışacağına dair bir uyarı verir . Ancak, örneklerin sınıfı uygulanmazsa IComparable, bir çalışma zamanı hatası atılır:

En az bir nesne IComparable uygulamalıdır

Neyse ki, bu hala temiz bir şekilde düzeltilebilir. Fikir uzak bir "ID" yi, açık bir bağlayıcı-kırıcı görevi gören her bir girişle ilişkilendirmektir. Her giriş için artımlı bir kimlik kullanabiliriz. Hala insanların yaşını örnek olarak kullanıyorum:

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

1
Değer türü sıralama anahtarı olduğunda bu çalışmaz. "En az bir nesne IComparable uygulamalıdır"
liang

1
çok harika! bu en iyi cevap olmalı.
Guido Mocha

@ liang evet iyi yakala. Neyse ki buna hala temiz bir çözüm var. "Not 2" bölümündeki güncellenmiş çözüme bakın.
KFL

Seçin size kimlik verebilir! en genç var = insanlar. ((p, i) => (p.DateOfBirth, i, p)) öğesini seçin. Min ().
Jeremy

19

Ek paket içermeyen çözüm:

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

ayrıca uzatma içine sarabilirsiniz:

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

ve bu durumda:

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

Bu arada ... O (n ^ 2) en iyi çözüm değil. Paul Betts benden daha yağlı bir çözüm verdi. Ama benim hala LINQ çözümü ve buradaki diğer çözümlerden daha basit ve daha kısa.


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

Agregatın mükemmel derecede basit kullanımı (diğer dillerde katlanmaya eşdeğer):

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

Tek dezavantajı, özelliğe dizi öğesi başına iki kez erişilmesidir, bu da pahalı olabilir. Bunu düzeltmek zor.


1

Aşağıdakiler daha genel bir çözümdür. Aslında aynı şeyi (O (N) sırayla) yapar, ancak herhangi bir IEnumberable türünde ve özellik seçicileri null dönebilen türlerle karıştırılabilir.

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

Testler:

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

0

Tekrar DÜZENLE:

Afedersiniz. Yanlış fonksiyona baktığım nullable'ı kaçırmanın yanı sıra,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) sonuç türünü söylediğiniz gibi döndürür.

Ben olası bir çözüm IComparable uygulamak ve gerçekten IEnumerable bir öğe döndürür Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) kullanmak olduğunu söyleyebilirim . Elbette, öğeyi değiştiremezseniz size yardımcı olmaz. MS'in tasarımını burada biraz garip buluyorum.

Tabii ki, ihtiyacınız varsa her zaman bir for döngüsü yapabilirsiniz veya Jon Skeet'in verdiği MoreLINQ uygulamasını kullanabilirsiniz.


0

Null edilebilir seçici anahtarlarla çalışabilen ve başvuru türü koleksiyonu için uygun bir öğe bulunamazsa başka bir uygulama null değerini döndürür. Bu, örneğin veritabanı sonuçlarının işlenmesinde yardımcı olabilir.

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

Misal:

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

0

Aşağıdaki fikri deneyin:

var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();

-2

Tercihen bir kütüphane kullanmadan veya tüm listeyi sıralamaksızın kendime benzer bir şey arıyordum. Benim çözümüm sorunun kendisine benzedi, sadece basitleştirildi.

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

Linq ifadenizden önce min'i almak çok daha verimli olmaz mıydı? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Aksi takdirde, aradığınız kişiyi bulana kadar dakikayı tekrar tekrar alır.
Nieminen
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.