![](/img/trans.png)
[英]Get minimum and maximum time value from list of object property using 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()
會做的伎倆
所以你要求ArgMin
或ArgMax
。 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),它首先比較x1
與x2
,然后是y1
與y2
。 這就是內置.Min
可以用於這些類型的原因。
而且由於匿名類型和值元組都是值類型,所以它們都應該非常有效。
筆記
在我上面的ArgMin
實現中,為了簡單和清晰,我假設DateOfBirth
采用DateTime
類型。 原始問題要求排除具有 null DateOfBirth
字段的條目:
Null DateOfBirth 值設置為 DateTime.MaxValue 以將它們排除在 Min 考慮之外(假設至少有一個具有指定的 DOB)。
它可以通過預過濾來實現
people.Where(p => p.DateOfBirth.HasValue)
因此,實施ArgMin
或ArgMax
的問題無關緊要。
筆記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.MaxBy和Enumerable.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.