簡體   English   中英

如何使用 LINQ 選擇具有最小或最大屬性值的對象

[英]How to use LINQ to select object with minimum or maximum property value

我有一個帶有 Nullable DateOfBirth 屬性的 Person 對象。 有沒有一種方法可以使用 LINQ 來查詢 Person 對象列表中具有最早/最小 DateOfBirth 值的對象?

這是我開始的:

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

Null DateOfBirth 值設置為 DateTime.MaxValue 以便將它們排除在 Min 考慮之外(假設至少有一個具有指定的 DOB)。

但對我來說所做的只是將 firstBornDate 設置為 DateTime 值。 我想得到的是與之匹配的 Person 對象。 我是否需要像這樣編寫第二個查詢:

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

還是有更精簡的方法?

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

不幸的是,沒有一個內置的方法可以做到這一點,但它很容易為你自己實現。 這是它的膽量:

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

用法示例:

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

請注意,如果序列為空,這將引發異常,如果有多個,將返回具有最小值的第一個元素。

或者,您可以使用我們在MoreLINQ中的MinBy.cs 中的實現 (當然,有一個相應的MaxBy 。)

通過包管理器控制台安裝:

PM> 安裝包 morelinq

注意:我包含這個答案是為了完整性,因為 OP 沒有提到數據源是什么,我們不應該做出任何假設。

這個查詢給出了正確的答案,但可能會更慢,因為它可能必須對People所有項目進行排序,具體取決於People是什么數據結構:

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

更新:實際上我不應該稱這個解決方案“幼稚”,但用戶確實需要知道他在查詢什么。 此解決方案的“緩慢”取決於基礎數據。 如果這是一個數組或List<T> ,則 LINQ to Objects 只能在選擇第一項之前先對整個集合進行排序。 在這種情況下,它會比建議的其他解決方案慢。 但是,如果這是一個 LINQ to SQL 表並且DateOfBirth是一個索引列,則 SQL Server 將使用該索引而不是對所有行進行排序。 其他自定義IEnumerable<T>實現也可以使用索引(請參閱i4o: Indexed LINQ或對象數據庫db4o )並使此解決方案比需要迭代整個集合的Aggregate()MaxBy() / MinBy()更快一次。 事實上,LINQ to Objects 可以(理論上)在OrderBy()為排序集合(如SortedList<T> )創建特殊情況,但據我所知,事實並非如此。

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

會做的伎倆

所以你要求ArgMinArgMax C# 沒有這些的內置 API。

我一直在尋找一種干凈有效(時間為 O(n))的方法來做到這一點。 我想我找到了一個:

這種模式的一般形式是:

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

特別是,使用原始問題中的示例:

對於支持值元組的C# 7.0 及更高版本:

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

對於 7.0 之前的 C# 版本,可以使用匿名類型

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

它們有效是因為值元組和匿名類型都有合理的默認比較器:對於 (x1, y1) 和 (x2, y2),它首先比較x1x2 ,然后是y1y2 這就是內置.Min可以用於這些類型的原因。

而且由於匿名類型和值元組都是值類型,所以它們都應該非常有效。

筆記

在我上面的ArgMin實現中,為了簡單和清晰,我假設DateOfBirth采用DateTime類型。 原始問題要求排除具有 null DateOfBirth字段的條目:

Null DateOfBirth 值設置為 DateTime.MaxValue 以將它們排除在 Min 考慮之外(假設至少有一個具有指定的 DOB)。

它可以通過預過濾來實現

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

因此,實施ArgMinArgMax的問題無關緊要。

筆記2

上述方法有一個警告,當有兩個實例具有相同的最小值時, Min()實現將嘗試比較這些實例作為決勝局。 但是,如果實例的類沒有實現IComparable ,則會拋出運行時錯誤:

至少一個對象必須實現 IComparable

幸運的是,這仍然可以相當干凈地修復。 這個想法是將一個遙遠的“ID”與作為明確決勝局的每個條目相關聯。 我們可以為每個條目使用一個增量 ID。 仍然以人們的年齡為例:

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

沒有額外包的解決方案:

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

你也可以把它包裝成擴展:

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

在這種情況下:

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

順便說一句... O(n^2) 不是最好的解決方案。 Paul Betts給出了比我更胖的解決方案。 但我的仍然是 LINQ 解決方案,它比這里的其他解決方案更簡單、更短。

.NET 6 Preview 4本身支持 MaxBy/MinBy。 所以你可以用一個簡單的方法來做到這一點

People.MinBy(p => p.DateOfBirth)

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

完全簡單的聚合使用(相當於其他語言中的 fold ):

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

唯一的缺點是每個序列元素訪問該屬性兩次,這可能很昂貴。 這很難修復。

從 .Net 6 (Preview 7) 或更高版本開始,有新的內置方法Enumerable.MaxByEnumerable.MinBy來實現這一點。

var lastBorn = people.MaxBy(p => p.DateOfBirth);

var firstBorn = people.MinBy(p => p.DateOfBirth);

以下是更通用的解決方案。 它本質上做同樣的事情(以 O(N) 順序),但在任何 IEnumberable 類型上,並且可以與屬性選擇器可以返回 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;
        });
    }
}

測試:

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

嘗試以下想法:

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

我自己也在尋找類似的東西,最好不使用庫或對整個列表進行排序。 我的解決方案最終類似於問題本身,只是簡化了一點。

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

另一種實現可以與可為空的選擇器鍵一起使用,並且如果沒有找到合適的元素,則對於引用類型的集合返回 null。 例如,這可能有助於處理數據庫結果。

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

例子:

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

如果要選擇具有最小或最大屬性值的對象。 另一種方法是使用實​​現 IComparable。

public struct Money : IComparable<Money>
{
   public Money(decimal value) : this() { Value = value; }
   public decimal Value { get; private set; }
   public int CompareTo(Money other) { return Value.CompareTo(other.Value); }
}

最大實施將是。

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Max();

最小執行將。

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Min();

這樣就可以比較任意對象,在返回對象類型的同時得到Max和Min。

希望這會幫助某人。

通過 IEnumerable 上的擴展函數返回對象和找到的最小值的方法。 它需要一個可以對集合中的對象進行任何操作的 Func:

public static (double min, T obj) tMin<T>(this IEnumerable<T> ienum, 
            Func<T, double> aFunc)
        {
            var okNull = default(T);
            if (okNull != null)
                throw new ApplicationException("object passed to Min not nullable");

            (double aMin, T okObj) best = (double.MaxValue, okNull);
            foreach (T obj in ienum)
            {
                double q = aFunc(obj);
                if (q < best.aMin)
                    best = (q, obj);
            }
            return (best);
        }

對象是機場的示例,我們希望找到距離給定(緯度、經度)最近的機場。 機場有一個 dist(lat, lon) 函數。

(double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));

您可以像 SQL 中的 order by 和 limit/fetch only 技巧一樣做到這一點。 所以你按 DateOfBirth 升序排序,然后只獲取第一行。

var query = from person in People
            where person.DateOfBirth!=null
            orderby person.DateOfBirth
            select person;
var firstBorn = query.Take(1).toList();

這是獲取最小值和最大值的簡單方法:

    `dbcontext.tableName.Select(x=>x.Feild1).Min()`
    

再次編輯:

對不起。 除了缺少可空值之外,我還看錯了函數,

Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>))確實返回了您所說的結果類型。

我想說一種可能的解決方案是實現 IComparable 並使用Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)) ,它確實從 IEnumerable 返回一個元素。 當然,如果您不能修改元素,那對您沒有幫助。 我覺得 MS 的設計在這里有點奇怪。

當然,如果需要,您始終可以執行 for 循環,或者使用 Jon Skeet 提供的 MoreLINQ 實現。

您可以像 MoreLinq 一樣使用現有的 linq 擴展。 但是如果你只需要這些方法,那么你可以使用這里的簡單代碼:

public static IEnumerable<T> MinBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
    var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
    return dict[dict.Keys.Min()];
}
public static IEnumerable<T> MaxBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
    var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
    return dict[dict.Keys.Max()];
}

要從對象數組獲取屬性的最大值或最小值:

列出存儲每個屬性值的列表:

list<int> values = new list<int>;

將所有屬性值添加到列表中:

foreach (int i in obj.desiredProperty)
{    values.add(i);  }

從列表中獲取最大值或最小值:

int Max = values.Max;
int Min = values.Min;

現在,您可以遍歷對象數組,並將要檢查的屬性值與max或min int進行比較:

foreach (obj o in yourArray)
{
    if (o.desiredProperty == Max)
       {return o}

    else if (o.desiredProperty == Min)
        {return o}
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM