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

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


当前回答

你可以像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();

其他回答

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

var min = People.Min(p => p.DateOfBirth);
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min);
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

你可以像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();

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

所以你要求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;