Cách sử dụng LINQ để chọn đối tượng có giá trị thuộc tính tối thiểu hoặc tối đa


466

Tôi có một đối tượng Person với thuộc tính DateOfBirth không có giá trị. Có cách nào để sử dụng LINQ để truy vấn danh sách các đối tượng Person cho đối tượng có giá trị DateOfBirth sớm nhất / nhỏ nhất.

Đây là những gì tôi bắt đầu với:

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

Các giá trị Null DateOfBirth được đặt thành DateTime.MaxValue để loại trừ chúng khỏi sự cân nhắc tối thiểu (giả sử ít nhất một cái có DOB được chỉ định).

Nhưng tất cả những gì làm cho tôi là đặt FirstBornDate thành giá trị DateTime. Những gì tôi muốn nhận là đối tượng Person phù hợp với điều đó. Tôi có cần phải viết một truy vấn thứ hai như vậy không:

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

Hoặc có một cách gọn gàng hơn để làm điều đó?


24
Chỉ cần một nhận xét về ví dụ của bạn: Có lẽ bạn không nên sử dụng Độc thân ở đây. Sẽ là một ngoại lệ nếu hai Người có cùng DateOfBirth
Niki

1
Xem thêm stackoverflow.com/questions/2736236/ , gần như trùng lặp , có một số ví dụ ngắn gọn.
tạm biệt

4
Thật là một tính năng đơn giản và hữu ích. MinBy nên có trong thư viện tiêu chuẩn. Chúng tôi nên gửi yêu cầu kéo tới Microsoft github.com/dotnet/corefx
Đại tá Panic

2
Điều này dường như tồn tại ngày nay, chỉ cần cung cấp một chức năng để chọn tài sản:a.Min(x => x.foo);
jackmott

4
Để giải thích vấn đề: trong Python, max("find a word of maximal length in this sentence".split(), key=len) trả về chuỗi 'câu'. Trong C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)tính toán rằng 8 là độ dài dài nhất của bất kỳ từ nào, nhưng không cho bạn biết từ dài nhất là gì .
Đại tá Panic

Câu trả lời:


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

16
Có lẽ chậm hơn một chút so với việc chỉ thực hiện IComparable và sử dụng Min (hoặc vòng lặp for). Nhưng +1 cho giải pháp linqy O (n).
Matthew Flaschen

3
Ngoài ra, nó cần phải là <curmin.DateOfBirth. Mặt khác, bạn đang so sánh DateTime với một người.
Matthew Flaschen

2
Cũng cẩn thận khi sử dụng này để so sánh hai lần ngày. Tôi đã sử dụng điều này để tìm bản ghi thay đổi cuối cùng trong một bộ sưu tập không có thứ tự. Nó thất bại vì hồ sơ tôi muốn kết thúc cùng ngày và giờ.
Simon Gill

8
Tại sao bạn làm kiểm tra thừa curMin == null? curMinchỉ có thể là nullnếu bạn đang sử dụng Aggregate()với một hạt giống null.
Niềm tự hào Nerd chúc ngủ ngon


226

Thật không may, không có một phương pháp tích hợp sẵn để làm điều này, nhưng nó đủ dễ để tự thực hiện. Đây là can đảm của nó:

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

Ví dụ sử dụng:

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

Lưu ý rằng điều này sẽ đưa ra một ngoại lệ nếu chuỗi trống và sẽ trả về phần tử đầu tiên có giá trị tối thiểu nếu có nhiều hơn một.

Ngoài ra, bạn có thể sử dụng triển khai mà chúng tôi đã có trong MoreLINEQ , trong MinBy.cs . (Tất nhiên là có tương ứng MaxBy.)

Cài đặt qua bảng điều khiển quản lý gói:

PM> Cài đặt-Gói morelinq


1
Tôi sẽ thay thế Ienumerator + trong khi bằng một foreach
ggf31416

5
Không thể thực hiện điều đó một cách dễ dàng do cuộc gọi đầu tiên tới MoveNext () trước vòng lặp. Có những lựa chọn thay thế, nhưng IMO lộn xộn hơn.
Jon Skeet

2
Trong khi tôi có thể trả về mặc định (T) cảm thấy không phù hợp với tôi. Điều này phù hợp hơn với các phương thức như First () và cách tiếp cận của bộ chỉ mục Từ điển. Bạn có thể dễ dàng thích ứng nó nếu bạn muốn mặc dù.
Jon Skeet

8
Tôi đã trao câu trả lời cho Paul vì giải pháp không phải thư viện, nhưng cảm ơn về mã này và liên kết đến thư viện MoreLINEQ, mà tôi nghĩ rằng tôi sẽ bắt đầu sử dụng!
slolife


135

LƯU Ý: Tôi bao gồm câu trả lời này cho đầy đủ vì OP không đề cập đến nguồn dữ liệu là gì và chúng tôi không nên đưa ra bất kỳ giả định nào.

Truy vấn này đưa ra câu trả lời đúng, nhưng có thể chậm hơn vì có thể phải sắp xếp tất cả các mục vào People, tùy thuộc vào cấu trúc dữ liệu Peoplelà gì:

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

CẬP NHẬT: Thật ra tôi không nên gọi giải pháp này là "ngây thơ", nhưng người dùng không cần biết anh ta đang truy vấn điều gì. "Sự chậm chạp" của giải pháp này phụ thuộc vào dữ liệu cơ bản. Nếu đây là một mảng hoặc List<T>, thì LINQ to Object không có lựa chọn nào khác ngoài việc sắp xếp toàn bộ bộ sưu tập trước khi chọn mục đầu tiên. Trong trường hợp này, nó sẽ chậm hơn các giải pháp khác được đề xuất. Tuy nhiên, nếu đây là bảng LINQ to SQL và DateOfBirthlà một cột được lập chỉ mục, thì SQL Server sẽ sử dụng chỉ mục thay vì sắp xếp tất cả các hàng. Các IEnumerable<T>triển khai tùy chỉnh khác cũng có thể sử dụng các chỉ mục (xem i4o: LINQ được lập chỉ mục hoặc cơ sở dữ liệu đối tượng db4o ) và làm cho giải pháp này nhanh hơnAggregate() hoặc MaxBy()/MinBy()mà cần phải lặp lại toàn bộ bộ sưu tập một lần. Trên thực tế, LINQ to Object có thể (về lý thuyết) đã tạo ra các trường hợp đặc biệt OrderBy()cho các bộ sưu tập được sắp xếp như thế SortedList<T>, nhưng theo như tôi biết thì không.


1
Ai đó đã đăng nó, nhưng dường như đã xóa nó sau khi tôi nhận xét tốc độ của nó (và tiêu tốn dung lượng) chậm như thế nào (O (n log n) ở tốc độ tốt nhất so với O (n) trong tối thiểu). :)
Matthew Flaschen

vâng, do đó, cảnh báo của tôi về việc là giải pháp ngây thơ :) tuy nhiên nó rất đơn giản và có thể sử dụng được trong một số trường hợp (bộ sưu tập nhỏ hoặc nếu DateOfBirth là một cột DB được lập chỉ mục)
Lucas

một trường hợp đặc biệt khác (cũng không có) là có thể sử dụng kiến ​​thức về trật tự và trước tiên để tìm kiếm giá trị thấp nhất mà không cần sắp xếp.
Rune FS

Sắp xếp một bộ sưu tập là hoạt động Nlog (N) không tốt hơn độ phức tạp thời gian tuyến tính hoặc O (n). Nếu chúng ta chỉ cần 1 phần tử / đối tượng từ một chuỗi là tối thiểu hoặc tối đa, tôi nghĩ rằng chúng ta nên gắn bó với tính linh hoạt thời gian tuyến tính.
Yawar Murtaza

@yawar bộ sưu tập có thể đã được sắp xếp (có khả năng được lập chỉ mục nhiều hơn) trong trường hợp đó bạn có thể có O (log n)
Rune FS

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

Sẽ làm điều đó


1
Điều này là tuyệt vời! Tôi đã sử dụng với OrderByDesending (...). Lấy (1) trong trường hợp linq projetion của tôi.
Vedran Mandić 6/2/2015

1
Cái này sử dụng sắp xếp, vượt quá thời gian O (N) và cũng sử dụng bộ nhớ O (N).
George Polevoy

@GeorgePolevoy giả định rằng chúng tôi biết khá nhiều về nguồn dữ liệu. Nếu nguồn dữ liệu đã có một chỉ mục được sắp xếp trên trường đã cho, thì đây sẽ là hằng số (thấp) và nó sẽ nhanh hơn rất nhiều so với câu trả lời được chấp nhận sẽ yêu cầu duyệt qua toàn bộ danh sách. Mặt khác, nếu nguồn dữ liệu là một mảng thì bạn hoàn toàn đúng
Rune FS

@RuneFS - bạn vẫn nên đề cập đến điều đó trong câu trả lời của mình vì nó quan trọng.
rory.ap

Hiệu suất sẽ kéo bạn xuống. Tôi đã học nó một cách khó khăn. Nếu bạn muốn đối tượng có giá trị Min hoặc Max, thì bạn không cần phải sắp xếp toàn bộ mảng. Chỉ cần 1 lần quét là đủ. Nhìn vào câu trả lời được chấp nhận hoặc nhìn vào gói MoreLinq.
Sau001

35

Vì vậy, bạn đang yêu cầu ArgMinhoặc ArgMax. C # không có API tích hợp cho những thứ đó.

Tôi đã tìm kiếm một cách sạch sẽ và hiệu quả (O (n) kịp thời để làm điều này. Và tôi nghĩ rằng tôi đã tìm thấy một:

Hình thức chung của mẫu này là:

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

Đặc biệt, sử dụng ví dụ trong câu hỏi ban đầu:

Đối với C # 7.0 trở lên hỗ trợ bộ giá trị :

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

Đối với phiên bản C # trước 7.0, loại ẩn danh có thể được sử dụng thay thế:

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

Chúng hoạt động vì cả hai giá trị tuple và loại ẩn danh đều có các so sánh mặc định hợp lý: for (x1, y1) và (x2, y2), trước tiên nó so sánh x1với x2, sau đó y1so với y2. Đó là lý do tại sao tích hợp .Mincó thể được sử dụng trên các loại đó.

Và vì cả loại ẩn danh và bộ giá trị là loại giá trị, nên chúng đều rất hiệu quả.

GHI CHÚ

Trong các ArgMintriển khai trên của tôi, tôi giả sử DateOfBirthlấy loại DateTimecho đơn giản và rõ ràng. Câu hỏi ban đầu yêu cầu loại trừ các mục đó với DateOfBirthtrường null :

Các giá trị Null DateOfBirth được đặt thành DateTime.MaxValue để loại trừ chúng khỏi sự cân nhắc tối thiểu (giả sử ít nhất một cái có DOB được chỉ định).

Nó có thể đạt được với một bộ lọc trước

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

Vì vậy, nó không quan trọng đối với câu hỏi thực hiện ArgMinhay ArgMax.

LƯU Ý 2

Cách tiếp cận trên có một cảnh báo rằng khi có hai trường hợp có cùng giá trị tối thiểu, thì việc Min()triển khai sẽ cố gắng so sánh các thể hiện như một bộ ngắt kết nối. Tuy nhiên, nếu lớp của các thể hiện không thực hiện IComparable, thì một lỗi thời gian chạy sẽ được đưa ra:

Ít nhất một đối tượng phải thực hiện IComparable

May mắn thay, điều này vẫn có thể được sửa chữa khá sạch sẽ. Ý tưởng là liên kết một "ID" phân tán với mỗi mục đóng vai trò là bộ ngắt kết nối rõ ràng. Chúng tôi có thể sử dụng ID gia tăng cho mỗi mục nhập. Vẫn sử dụng tuổi người như ví dụ:

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

1
Điều này dường như không hoạt động khi loại giá trị là khóa sắp xếp. "Ít nhất một đối tượng phải triển khai IComparable"
liang

1
quá tuyệt đây sẽ là câu trả lời tốt nhất
Guido Mocha

@liang vâng bắt tốt. May mắn thay, vẫn còn một giải pháp sạch cho điều đó. Xem giải pháp cập nhật trong phần "Lưu ý 2".
KFL

Chọn có thể cung cấp cho bạn ID! var youngest = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy

19

Giải pháp không có gói bổ sung:

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

bạn cũng có thể gói nó thành phần mở rộng:

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

và trong trường hợp này:

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

Nhân tiện ... O (n ^ 2) không phải là giải pháp tốt nhất. Paul Betts đã cho giải pháp fatster hơn của tôi. Nhưng giải pháp của tôi vẫn là LINQ và nó đơn giản và ngắn gọn hơn các giải pháp khác ở đây.


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

3

Sử dụng tổng hợp hoàn toàn đơn giản (tương đương với gấp trong các ngôn ngữ khác):

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

Nhược điểm duy nhất là tài sản được truy cập hai lần cho mỗi phần tử chuỗi, có thể tốn kém. Thật khó để sửa chữa.


1

Sau đây là giải pháp chung chung hơn. Về cơ bản, nó thực hiện cùng một thứ (theo thứ tự O (N)) nhưng trên bất kỳ loại IEnumberable nào và có thể trộn lẫn với các loại có bộ chọn thuộc tính có thể trả về 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;
        });
    }
}

Các xét nghiệm:

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

0

EDIT một lần nữa:

Lấy làm tiếc. Bên cạnh việc thiếu null, tôi đã nhìn vào hàm sai,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) sẽ trả về loại kết quả như bạn đã nói.

Tôi muốn nói một giải pháp khả thi là triển khai IComparable và sử dụng Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , thực sự trả về một phần tử từ IEnumerable. Tất nhiên, điều đó không giúp ích gì cho bạn nếu bạn không thể sửa đổi thành phần. Tôi thấy thiết kế của MS hơi kỳ lạ ở đây.

Tất nhiên, bạn luôn có thể thực hiện một vòng lặp for nếu bạn cần hoặc sử dụng triển khai MoreLINEQ mà Jon Skeet đã đưa ra.


0

Một triển khai khác, có thể hoạt động với các khóa chọn nullable và cho bộ sưu tập kiểu tham chiếu trả về null nếu không tìm thấy phần tử phù hợp. Điều này có thể hữu ích sau đó xử lý kết quả cơ sở dữ liệu chẳng hạn.

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

Thí dụ:

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

-2

Tôi đang tìm kiếm một cái gì đó tương tự bản thân mình, tốt nhất là không sử dụng thư viện hoặc sắp xếp toàn bộ danh sách. Giải pháp của tôi đã kết thúc tương tự như câu hỏi, chỉ đơn giản hóa một chút.

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

Nó sẽ không hiệu quả hơn nhiều để có được min trước câu lệnh linq của bạn? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Nếu không, nó sẽ nhận được min liên tục cho đến khi nó tìm thấy cái bạn đang tìm kiếm.
Nieminen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.