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

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


当前回答

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

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

其他回答

注意:我之所以包含这个答案是为了完整性,因为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.

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

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

人。MinBy(p => p.出生日期)

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

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