Tại sao nên sử dụng từ khóa suất, khi tôi chỉ có thể sử dụng một IEnumerable thông thường?


171

Cho mã này:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            yield return item;
    }
}

Tại sao tôi không chỉ viết mã theo cách này?:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

Tôi sắp xếp để hiểu những gì các yieldtừ khóa làm. Nó báo cho trình biên dịch xây dựng một loại điều nhất định (một trình vòng lặp). Nhưng tại sao lại sử dụng nó? Ngoài việc nó ít mã hơn một chút, nó làm gì cho tôi?


28
Tôi nhận ra đây chỉ là một ví dụ, nhưng thực sự, mã nên được viết như thế này: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft

Câu trả lời:


241

Sử dụng yieldlàm cho bộ sưu tập lười biếng.

Giả sử bạn chỉ cần năm mục đầu tiên. Theo cách của bạn, tôi phải lặp qua toàn bộ danh sách để có được năm mục đầu tiên. Với yield, tôi chỉ lặp qua năm mục đầu tiên.


14
Lưu ý rằng việc sử dụng FullList.Where(IsItemInPartialList)sẽ chỉ là lười biếng. Chỉ, nó yêu cầu trình biên dịch tùy chỉnh tạo mã --- gunk --- ít hơn nhiều. Và ít nhà phát triển thời gian viết và duy trì. (Tất nhiên, đó chỉ là ví dụ này)
sehe

4
Đó là Linq, phải không? Tôi tưởng tượng Linq làm một cái gì đó rất giống nhau dưới vỏ bọc.
Robert Harvey

1
Có, Linq đã sử dụng thực thi bị trì hoãn ( yield return) khi có thể.
Chad Schouggins

11
Đừng quên rằng nếu tuyên bố lợi nhuận không bao giờ thực thi, bạn vẫn nhận được kết quả bộ sưu tập trống nên không cần phải lo lắng về ngoại lệ tham chiếu null. năng suất trở lại là tuyệt vời với rắc sô cô la.
Dao cạo

127

Lợi ích của các khối lặp là chúng hoạt động lười biếng. Vì vậy, bạn có thể viết một phương thức lọc như thế này:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Điều đó sẽ cho phép bạn lọc một luồng bao lâu tùy thích, không bao giờ đệm nhiều hơn một mục mỗi lần. Ví dụ, nếu bạn chỉ cần giá trị đầu tiên từ chuỗi được trả về, tại sao bạn muốn sao chép mọi thứ vào một danh sách mới?

Một ví dụ khác, bạn có thể dễ dàng tạo một luồng vô hạn bằng cách sử dụng các khối lặp. Ví dụ: đây là một chuỗi các số ngẫu nhiên:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Làm thế nào bạn sẽ lưu trữ một chuỗi vô hạn trong một danh sách?

My blog của loạt Edulinq đưa ra một thực hiện mẫu LINQ to Objects mà làm nặng sử dụng các khối iterator. LINQ về cơ bản là lười biếng ở nơi có thể - và đưa mọi thứ vào danh sách đơn giản là không hoạt động theo cách đó.


1
Tôi không chắc có thích bạn RandomSequencehay không. Đối với tôi, IEnumerable có nghĩa là - đầu tiên và quan trọng nhất - rằng tôi có thể lặp lại với foreach, nhưng điều này rõ ràng sẽ dẫn đến một vòng lặp không xác định ở đây. Tôi coi đây là một sự lạm dụng khá nguy hiểm của khái niệm IEnumerable, nhưng YMMV.
Sebastian Negraszus

5
@SebastianNegraszus: Một chuỗi các số ngẫu nhiên là vô hạn về mặt logic. IEnumerable<BigInteger>Ví dụ, bạn có thể dễ dàng tạo một chuỗi đại diện cho chuỗi Fibonacci. Bạn có thể sử dụng foreachvới nó, nhưng không có gì IEnumerable<T>đảm bảo rằng nó sẽ là hữu hạn.
Jon Skeet

42

Với mã "danh sách", bạn phải xử lý danh sách đầy đủ trước khi bạn có thể chuyển nó sang bước tiếp theo. Phiên bản "suất" chuyển mục được xử lý ngay lập tức sang bước tiếp theo. Nếu "bước tiếp theo" đó chứa ".Take (10)" thì phiên bản "suất" sẽ chỉ xử lý 10 mục đầu tiên và quên phần còn lại. Mã "danh sách" sẽ xử lý mọi thứ.

Điều này có nghĩa là bạn thấy sự khác biệt nhất khi bạn cần xử lý nhiều và / hoặc có danh sách dài các mục cần xử lý.


23

Bạn có thể sử dụng yieldđể trả về các mục không có trong danh sách. Đây là một mẫu nhỏ có thể lặp lại vô hạn thông qua một danh sách cho đến khi bị hủy.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

Điều này viết

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...Vân vân. đến bàn điều khiển cho đến khi hủy bỏ.


10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Khi đoạn mã trên được sử dụng để lặp qua FilteredList () và giả sử item.Name == "James" sẽ được thỏa mãn ở mục thứ 2 trong danh sách, phương thức sử dụng yieldsẽ mang lại hai lần. Đây là một hành vi lười biếng.

Trong khi phương thức sử dụng danh sách sẽ thêm tất cả n đối tượng vào danh sách và chuyển danh sách đầy đủ vào phương thức gọi.

Đây chính xác là trường hợp sử dụng trong đó có thể làm nổi bật sự khác biệt giữa IEnumerable và IList.


7

Ví dụ về thế giới thực tốt nhất mà tôi đã thấy cho việc sử dụng yieldsẽ là để tính toán một chuỗi Fibonacci.

Hãy xem xét các mã sau đây:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

Điều này sẽ trở lại:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

Điều này thật tuyệt vì nó cho phép bạn tính toán một chuỗi vô hạn một cách nhanh chóng và dễ dàng, cho bạn khả năng sử dụng các phần mở rộng Linq và chỉ truy vấn những gì bạn cần.


5
Tôi không thể thấy bất cứ điều gì "thế giới thực" trong phép tính trình tự Fibonacci.
Nek

Tôi đồng ý rằng đây không thực sự là "thế giới thực", nhưng thật là một ý tưởng hay.
Casey

1

Tại sao sử dụng [năng suất]? Ngoài việc nó ít mã hơn một chút, nó làm gì cho tôi?

Đôi khi nó hữu ích, đôi khi không. Nếu toàn bộ tập hợp dữ liệu phải được kiểm tra và trả lại thì sẽ không có bất kỳ lợi ích nào trong việc sử dụng năng suất vì tất cả những gì nó đã được giới thiệu trên đầu.

Khi năng suất thực sự tỏa sáng là khi chỉ một bộ được trả về. Tôi nghĩ rằng ví dụ tốt nhất là sắp xếp. Giả sử bạn có một danh sách các đối tượng có chứa một ngày và một số tiền từ năm nay và bạn muốn xem các bản ghi (5) đầu tiên của năm.

Để thực hiện điều này, danh sách phải được sắp xếp tăng dần theo ngày, và sau đó thực hiện 5 lần đầu tiên. Nếu điều này được thực hiện mà không có năng suất, toàn bộ danh sách sẽ phải được sắp xếp, ngay để đảm bảo hai ngày cuối cùng theo thứ tự.

Tuy nhiên, với năng suất, một khi 5 mục đầu tiên đã được thiết lập, các điểm dừng phân loại và kết quả có sẵn. Điều này có thể tiết kiệm một lượng lớn thời gian.


0

Báo cáo lợi nhuận cho phép bạn chỉ trả lại một mục mỗi lần. Bạn đang thu thập tất cả các mục trong một danh sách và một lần nữa trả lại danh sách đó, đó là chi phí bộ nhớ.

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.