关于c#:如何使用LINQ选择具有最小或最大属性值的对象

How to use LINQ to select object with minimum or maximum property value

我有一个具有可以为空的出生日期属性的Person对象。有没有一种方法可以使用LINQ查询具有最早/最小出生日期值的人对象列表?

以下是我的出发点:

1
var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

空的dateofbirth值设置为datetime.maxvalue,以便将其排除在最小考虑范围之外(假设至少有一个具有指定的dob)。

但对我来说,所做的就是将firstborndate设置为datetime值。我想得到的是与之匹配的Person对象。是否需要编写第二个这样的查询:

1
var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

还是有更精简的方法?


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


不幸的是,没有内置的方法可以做到这一点。

PM> Install-Package morelinq

1
var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

或者,您可以使用我们在morelinq和minby.cs中得到的实现。(当然有一个对应的MaxBy)这是它的本质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 ?? 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;
    }
}

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


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

这个查询给出了正确的答案,但可能会变慢,因为它可能需要根据People中的数据结构对所有项目进行排序,这取决于People的数据结构:

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

更新:实际上我不应该称这个解决方案为"幼稚的",但是用户需要知道他在查询什么。此解决方案的"慢度"取决于基础数据。如果这是一个数组或List,那么linq-to对象除了在选择第一个项之前对整个集合进行排序之外别无选择。在这种情况下,它将比建议的其他解决方案慢。但是,如果这是一个linq to sql表,并且DateOfBirth是一个索引列,那么SQL Server将使用索引而不是对所有行进行排序。其他自定义IEnumerable实现也可以使用索引(参见i4o:indexed linq或对象数据库db4o),使此解决方案比Aggregate()MaxBy()MinBy()更快,后者需要对整个集合进行一次迭代。事实上,linq-to-objects(理论上)可以在OrderBy()中为分类收集(如SortedList)制作特殊的案例,但据我所知,它没有。


1
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

会成功的


所以你要的是ArgMinArgMax。C没有内置的API。

我一直在寻找一种干净有效的方法来做到这一点。我想我找到了一个:

这种模式的一般形式是:

1
2
3
4
var min = data.Select(x => (key(x), x)).Min().Item2;
                            ^           ^       ^
              the sorting key           |       take the associated original item
                                Min by key(.)

特别是,使用原始问题中的示例:

对于支持值元组的C 7.0及以上版本:

1
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;

对于7.0之前的C版本,可以使用匿名类型:

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

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

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

注释

在我上面的ArgMin实现中,为了简单和清晰,我假设DateOfBirth采用DateTime类型。原始问题要求排除那些字段为空DateOfBirth的条目:

Null DateOfBirth values are set to DateTime.MaxValue in order to rule them out of the Min consideration (assuming at least one has a specified DOB).

它可以通过预过滤来实现。

1
people.Where(p => p.DateOfBirth.HasValue)

因此,实施ArgMinArgMax的问题无关紧要。

注释2

上述方法有一个警告,即当有两个实例具有相同的最小值时,Min()实现将尝试将这些实例作为一个连接中断器进行比较。但是,如果实例的类不实现IComparable,则会引发运行时错误:

At least one object must implement IComparable

幸运的是,这仍然可以相当干净地修复。其思想是将一个distanct"id"与每个条目关联起来,这些条目充当明确的连接断路器。我们可以为每个条目使用一个增量ID。仍以年龄为例:

1
2
var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;


不带额外软件包的解决方案:

1
2
var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

也可以将其包装成扩展名:

1
2
3
4
5
6
7
8
9
10
11
12
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();
    }
}

在这种情况下:

1
2
var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

顺便说一句。。。O(n^2)不是最佳解决方案。保罗贝茨给出了比我更大的解决方案。但我的仍然是LINQ解决方案,它比这里的其他解决方案更简单和简短。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}

非常简单地使用聚合(相当于其他语言的折叠):

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

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


下面是更通用的解决方案。它基本上执行相同的操作(按O(N)顺序),但可以在任何IEnumerable类型上执行,并且可以与属性选择器可以返回空值的类型混合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
        });
    }
}

测验:

1
2
var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass

再次编辑:

对不起的。除了缺少nullable之外,我在看错误的函数,

Min<(of<(tsource,tresult>)>)(ienumerable<(of<(tsource>)>),func<(of<(tsource,tresult>)>)按您所说的返回结果类型。

我想说一个可能的解决方案是实现IComparable并使用Min<(of<(tsource>)>)(IEnumerable<(of<(tsource>)>),这确实会从IEnumerable返回一个元素。当然,如果您不能修改元素,这对您没有帮助。我觉得微软的设计有点奇怪。

当然,如果需要的话,您可以始终执行for循环,或者使用jon skeet提供的morelinq实现。


我在找类似的东西,最好不用图书馆或排序整个列表。我的解决方案最终类似于问题本身,只是稍微简化了一点。

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


要从对象数组中获取属性的最大值或最小值,请执行以下操作:

列出存储每个属性值的列表:

1
list<int> values = new list<int>;

将所有属性值添加到列表:

1
2
foreach (int i in obj.desiredProperty)
{    values.add(i);  }

从列表中获取最大值或最小值:

1
2
int Max = values.Max;
int Min = values.Min;

现在,您可以循环遍历对象数组,并将要检查的属性值与max或min int进行比较:

1
2
3
4
5
6
7
8
foreach (obj o in yourArray)
{
    if (o.desiredProperty == Max)
       {return o}

    else if (o.desiredProperty == Min)
        {return o}
}