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

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


当前回答

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

其他回答

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

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

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

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解决方案,它比这里的其他解决方案更简单,更简短。

你可以像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();
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

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