ChatGPT解决这个技术问题 Extra ChatGPT

如何使用 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);

还是有更精简的方法?

只是对您的示例的评论:您可能不应该在这里使用 Single 。如果两个人有相同的出生日期,它会抛出一个异常
另请参阅几乎重复的 stackoverflow.com/questions/2736236/…,其中包含一些简洁的示例。
多么简单而有用的功能。 MinBy 应该在标准库中。我们应该向 Microsoft github.com/dotnet/corefx 提交拉取请求
这在今天似乎确实存在,只需提供一个函数来选择属性:a.Min(x => x.foo);
为了演示这个问题:在 Python 中,max("find a word of maximal length in this sentence".split(), key=len) 返回字符串 'sentence'。在 C# 中,"find a word of maximal length in this sentence".Split().Max(word => word.Length) 计算出 8 是任何单词的最长长度,但不会告诉您最长的单词 是什么

S
Sheridan
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

可能比仅实现 IComparable 和使用 Min (或 for 循环)慢一点。但是对于 O(n) linqy 解决方案 +1。
此外,它必须是 < curmin.DateOfBirth 。否则,您将 DateTime 与 Person 进行比较。
使用它来比较两个日期时间时也要小心。我正在使用它来查找无序集合中的最后一个更改记录。它失败了,因为我想要的记录以相同的日期和时间结束。
为什么要做多余的检查curMin == null?如果您将 Aggregate()null 的种子一起使用,则 curMin 只能是 null
A
AustinWBryan

不幸的是,没有内置的方法可以做到这一点,但它很容易为自己实现。这是它的胆量:

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

请注意,如果序列为空,这将引发异常,如果有多个,则返回具有最小值的第一个元素。

或者,您可以在 MinBy.cs 中使用我们在 MoreLINQ 中获得的实现。 (当然,有一个对应的 MaxBy。)

通过包管理器控制台安装:

PM> Install-Package morelinq


我会用 foreach 替换 Ienumerator + while
由于在循环之前第一次调用 MoveNext() ,因此无法轻松做到这一点。还有其他选择,但它们更混乱 IMO。
虽然我可以返回我觉得不合适的 default(T)。这与 First() 等方法和 Dictionary 索引器的方法更加一致。如果你愿意,你可以很容易地适应它。
由于非库解决方案,我将答案授予 Paul,但感谢此代码和 MoreLINQ 库的链接,我想我将开始使用它!
L
Lucas

注意:为了完整起见,我包含了这个答案,因为 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: Indexed LINQ 或对象数据库 db4o)并使此解决方案比需要迭代整个集合的 Aggregate()MaxBy()/MinBy() 更快一次。事实上,LINQ to Objects 可以(理论上)在 OrderBy() 中为 SortedList<T> 之类的排序集合创建特殊情况,但据我所知,它没有。


有人已经发布了,但在我评论它有多慢(和占用空间)之后显然删除了它(与 min 的 O(n) 相比,速度最多为 O(n log n) )。 :)
是的,因此我警告说这是一个幼稚的解决方案:) 但是它非常简单,并且在某些情况下可能可用(小集合或 DateOfBirth 是索引数据库列)
另一个特殊情况(也不存在)是可以使用 orderby 的知识并首先搜索最小值而不进行排序。
对集合进行排序是 Nlog(N) 操作,它并不优于线性或 O(n) 时间复杂度。如果我们只需要一个最小或最大序列中的 1 个元素/对象,我认为我们应该坚持线性时间复杂性。
@yawar 集合可能已经被排序(更有可能被索引),在这种情况下你可以有 O(log n)
R
Rune FS
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

会做的伎俩


这个太棒了!在 linq 投影的情况下,我使用了 OrderByDesending(...).Take(1) 。
这个使用排序,超过 O(N) 时间,也使用 O(N) 内存。
@GeorgePolevoy 假设我们对数据源了解很多。如果数据源在给定字段上已经有一个排序索引,那么这将是一个(低)常量,并且它比需要遍历整个列表的公认答案快得多。另一方面,如果数据源是例如数组,那么您当然是对的
@RuneFS - 您仍然应该在答案中提及这一点,因为它很重要。
表演会拖累你。我很难学会。如果您想要具有 Min 或 Max 值的对象,那么您不需要对整个数组进行排序。只需 1 次扫描就足够了。查看接受的答案或查看 MoreLinq 包。
n
nt86

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

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

这种模式的一般形式是:

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# 版本,可以使用 anonymous type 代替:

var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;

它们之所以起作用,是因为值元组和匿名类型都有合理的默认比较器:对于 (x1, y1) 和 (x2, y2),它首先比较 x1x2,然后是 y1y2。这就是为什么可以在这些类型上使用内置 .Min 的原因。

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

笔记

在我上面的 ArgMin 实现中,为了简单明了,我假设 DateOfBirth 采用类型 DateTime。原始问题要求排除具有 null 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;

当值类型是排序键时,这似乎不起作用。 “至少一个对象必须实现 IComparable”
太棒了!这应该是最好的答案。
@liang 是的,很好。幸运的是,仍然有一个干净的解决方案。请参阅“注 2”部分中的更新解决方案。
选择可以给你ID! var youngest = people.Select((p, i) => (p.DateOfBirth, i, p)).Min().Item2;
这是一个更易读的解决方案:var (minDateOfBirth, idx, youngestPerson) = people.Select((p, idx) => (p.DateOfBirth, idx, p)).Min()
A
Andrew____Pls_Support_Ukraine

没有额外包的解决方案:

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^2) 不是最好的解决方案。 Paul Betts 给出了比我更胖的解决方案。但我的仍然是 LINQ 解决方案,它比这里的其他解决方案更简单、更短。


h
hongxu

.NET 6 本机支持 MaxBy/MinBy。所以你可以用一个简单的方法来做到这一点

People.MinBy(p => p.DateOfBirth)


V
Vijay Nirmal

从 .Net 6(Preview 7)或更高版本开始,有新的内置方法 Enumerable.MaxByEnumerable.MinBy 来实现这一点。

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

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

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

d
david.pfx

完全简单的聚合使用(相当于其他语言的折叠):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

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


F
Fran Turkovic

你可以像 SQL 中的 order by 和 limit/fetch 一样来做。因此,您按 DateOfBirth 升序排序,然后仅获取第一行。

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

与多个答案中提出的 OrderBy + FirstOrDefault 相同,因此该答案并没有真正添加任何新内容。此外,只有 'Skip` + Take 翻译为限制/获取。 Take(1) 翻译为 TOP(1)。这是关于 LINQ 到对象,而不是 LINQ 到 SQL 后端。
m
marc_s

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

n
ncnylon

尝试以下想法:

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

i
idbrii

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

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

在您的 linq 语句之前获取最小值不是更有效率吗? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min... 否则,它会反复获取最小值,直到找到您要查找的那个。
这个解决方案分配的可能少于大多数解决方案(没有 GroupBy,但确实创建了 lambdas)并且是 O(n)。而且它比投票最多的聚合解决方案更容易理解。应该投票更高!
M
Matthew Flaschen

再次编辑:

对不起。除了缺少可空值之外,我还查看了错误的函数,

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

我想说一种可能的解决方案是实现 IComparable 并使用 Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)),它确实会从 IEnumerable 返回一个元素。当然,如果您无法修改元素,那将无济于事。我觉得 MS 的设计在这里有点奇怪。

当然,如果需要,您可以随时执行 for 循环,或者使用 Jon Skeet 提供的 MoreLINQ 实现。


Е
Евгений Орлов

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

t
tech-gayan

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

希望这会对某人有所帮助。


f
fredm73

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

对象是机场的示例,我们希望找到离给定(纬度、经度)最近的机场。机场具有 dist(lat, lon) 功能。

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

S
Shirzadeh

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

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

M
Moch Yusup

您可以使用现有的 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()];
}

没用。仅当 selector 产生可比较的类型时,才能使用 Min 和 Max。
您能否提供一些没有用的代码示例?
只需从 selector 返回一个匿名类型。
谢谢。那么如果我使用 where TVal: IComparable,它会有用吗?
谢谢。您应该在第一时间指出这一点,而不是给人留下错误的印象。我们是人,所以我们会犯错。最好指出错误并尝试提出解决方案。那会让人们过日子。 :)