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

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


当前回答

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

希望这能帮助到一些人。

其他回答

所以你要求ArgMin或ArgMax。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(.)

特别地,用原题中的例子:

对于支持value tuple的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;

它们之所以有效,是因为value tuple和匿名类型都有合理的默认比较器:对于(x1, y1)和(x2, y2),它首先比较x1 vs x2,然后比较y1 vs y2。这就是内置的. min可以用于这些类型的原因。

由于匿名类型和值元组都是值类型,它们应该都非常有效。

NOTE

在我上面的ArgMin实现中,为了简单明了,我假设DateOfBirth采用DateTime类型。原来的问题要求排除那些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 firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

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

下面是更通用的解决方案。它本质上做相同的事情(以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

注意:我之所以包含这个答案是为了完整性,因为OP没有提到数据源是什么,我们不应该做任何假设。

这个查询给出了正确的答案,但可能会慢一些,因为它可能必须对People中的所有项进行排序,这取决于People是什么数据结构:

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

UPDATE: Actually I shouldn't call this solution "naive", but the user does need to know what he is querying against. This solution's "slowness" depends on the underlying data. If this is a array or List<T>, then LINQ to Objects has no choice but to sort the entire collection first before selecting the first item. In this case it will be slower than the other solution suggested. However, if this is a LINQ to SQL table and DateOfBirth is an indexed column, then SQL Server will use the index instead of sorting all the rows. Other custom IEnumerable<T> implementations could also make use of indexes (see i4o: Indexed LINQ, or the object database db4o) and make this solution faster than Aggregate() or MaxBy()/MinBy() which need to iterate the whole collection once. In fact, LINQ to Objects could have (in theory) made special cases in OrderBy() for sorted collections like SortedList<T>, but it doesn't, as far as I know.

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

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