如何使用LINQ选择具有最小或最大属性值的对象

我有一个具有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);

或者有没有更精简的方法?

325022 次浏览

编辑:

对不起。除了错过了可空值之外,我看错了函数,

Min< (& lt; (TSource TResult >) >) (IEnumerable< (& lt; (TSource >) >), Func< (& lt; (TSource, TResult >) >))确实返回您所说的结果类型。

我认为一个可能的解决方案是实现IComparable并使用Min< (& lt; (TSource >) >) (IEnumerable< (& lt; (TSource >) >)),它确实从IEnumerable中返回一个元素。当然,如果不能修改元素,这也没有帮助。我觉得微软的设计有点奇怪。

当然,如果你需要的话,你总是可以做一个for循环,或者使用Jon Skeet给出的MoreLINQ实现。

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

注意,如果序列为空,将抛出异常;如果序列多于一个,将返回值最小的第一个元素。

或者,你也可以使用我们在MoreLINQMinBy.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:索引LINQ,或对象数据库db4o),使这个解决方案比Aggregate()MaxBy()/MinBy()更快,因为它们需要迭代整个集合一次。事实上,LINQ to Objects可以(理论上)在OrderBy()中为像SortedList<T>这样的排序集合创建特殊情况,但据我所知,它没有。

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

会成功的

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

无需额外包装的解决方案:

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²)不是最佳解。保罗•贝茨给出的溶液比我的更肥。但我仍然是LINQ解决方案,它比这里的其他解决方案更简单,更简短。

我自己也在寻找类似的东西,最好不使用库或对整个列表进行排序。我的解决方案与问题本身相似,只是简化了一点。

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

下面是更通用的解决方案。它本质上做相同的事情(以O(N)顺序),但对任何IEnumerable类型,并且可以与属性选择器可以返回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

所以你要求的是ArgMinArgMax。c#没有针对这些的内置API。

我一直在寻找一种干净高效(O(n) in time)的方法来做到这一点。我想我找到了一个:

这种模式的一般形式是:

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类型。原来的问题要求排除那些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 firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

唯一的缺点是每个sequence元素访问属性两次,这可能会很昂贵。这很难解决。

另一种实现,它可以使用可空的选择器键,对于引用类型的集合,如果没有找到合适的元素,则返回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);

试试下面的方法:

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

如果要选择具有最小或最大属性值的对象。另一种方法是使用实现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);
}

例子,对象是一个机场,我们想找到最近的机场到给定的(纬度,经度)。机场有纬度(纬度、纬度)功能。

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

net 6支持MaxBy/MinBy。你可以用简单的

# EYZ0

你可以像SQL中的order by和limit/fetch一样。按出生日期递增排序,然后取第一行。

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

你可以使用现有的linq扩展,比如MoreLinq。但如果你只需要这些方法,那么你可以使用这里的简单代码:

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

从。net 6 (Preview 7)或更高版本开始,有新的内置方法可列举的。MaxBy可列举的。MinBy来实现这一点。

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


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

这是一个获取最小值和最大值的简单方法:

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