Mục đích / lợi thế của việc sử dụng trình vòng lặp trả về lợi nhuận trong C # là gì?


80

Tất cả các ví dụ tôi đã thấy về việc sử dụng yield return x;bên trong một phương thức C # có thể được thực hiện theo cùng một cách bằng cách chỉ trả lại toàn bộ danh sách. Trong những trường hợp đó, có lợi ích hoặc lợi thế nào trong việc sử dụng yield returncú pháp so với việc trả về danh sách không?

Ngoài ra, trong những loại kịch bản nào sẽ yield returnđược sử dụng mà bạn không thể chỉ trả lại danh sách đầy đủ?


15
Tại sao bạn lại giả định rằng có "một danh sách" ở vị trí đầu tiên? Nếu không có thì sao?
Eric Lippert

2
@Eric, tôi đoán đó là những gì tôi đã hỏi. Khi nào bạn không có danh sách ở vị trí đầu tiên. Các luồng tệp và chuỗi vô hạn là 2 ví dụ tuyệt vời trong các câu trả lời cho đến nay.
CoderDennis

1
Nếu bạn có danh sách sau đó, chắc chắn, chỉ cần trả lại nó; nhưng nếu bạn đang tạo một danh sách bên trong phương thức và trả về thì bạn có thể / nên sử dụng các trình vòng lặp để thay thế. Nhường lần lượt các mục. Có rất nhiều lợi ích.
justin.m.chase

2
Tôi chắc chắn đã học được rất nhiều điều kể từ khi đặt câu hỏi này cách đây 5 năm!
CoderDennis

1
Ưu điểm lớn nhất của việc có yields là bạn không phải đặt tên cho một biến trung gian nào khác.
nawfal

Câu trả lời:


120

Nhưng nếu bạn đang tự mình xây dựng một bộ sưu tập thì sao?

Nói chung, các trình vòng lặp có thể được sử dụng để tạo một chuỗi các đối tượng một cách lười biếng . Ví dụ Enumerable.Rangephương pháp không có bất kỳ loại tập hợp nội bộ. Nó chỉ tạo ra số tiếp theo theo yêu cầu . Có nhiều cách sử dụng để tạo chuỗi lười này bằng cách sử dụng máy trạng thái. Hầu hết chúng được đề cập trong các khái niệm lập trình chức năng .

Theo ý kiến ​​của tôi, nếu bạn đang xem các trình lặp chỉ như một cách để liệt kê thông qua một tập hợp (nó chỉ là một trong những trường hợp sử dụng đơn giản nhất), bạn đang đi sai đường. Như tôi đã nói, trình vòng lặp là phương tiện để trả về trình tự. Chuỗi thậm chí có thể là vô hạn . Sẽ không có cách nào để trả về một danh sách có độ dài vô hạn và sử dụng 100 mục đầu tiên. Nó để được lười biếng đôi khi. Trả về một bộ sưu tập khác đáng kể so với việc trả lại một bộ tạo bộ sưu tập (chính là một trình lặp). Đó là so sánh táo với cam.

Ví dụ giả thuyết:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

Ví dụ này in các số nguyên tố nhỏ hơn 10000. Bạn có thể dễ dàng thay đổi nó để in các số nhỏ hơn một triệu mà không cần chạm vào thuật toán tạo số nguyên tố. Trong ví dụ này, bạn không thể trả về danh sách tất cả các số nguyên tố vì dãy số là vô hạn và người tiêu dùng thậm chí không biết họ muốn có bao nhiêu mặt hàng ngay từ đầu.


Đúng. Tôi đã xây dựng danh sách, nhưng việc trả lại từng mục một so với trả về toàn bộ danh sách thì có tác dụng gì?
CoderDennis

4
Trong số các lý do khác, nó làm cho mã của bạn mô-đun hơn để bạn có thể tải một mục, xử lý, sau đó lặp lại. Ngoài ra, hãy xem xét trường hợp tải một vật phẩm rất đắt hoặc có rất nhiều vật phẩm (hàng triệu người nói). Trong những trường hợp đó, việc tải toàn bộ danh sách là không mong muốn.
Dana the Sane

15
@Dennis: Đối với một danh sách được lưu trữ tuyến tính trong bộ nhớ, nó có thể không có sự khác biệt nhưng nếu bạn đang liệt kê một tệp 10GB và xử lý từng dòng một, thì sẽ có sự khác biệt.
Mehrdad Afshari

1
+1 cho một câu trả lời xuất sắc - Tôi cũng sẽ nói thêm rằng từ khóa lợi nhuận cho phép áp dụng ngữ nghĩa của trình lặp cho các nguồn không được coi là bộ sưu tập theo truyền thống - chẳng hạn như ổ cắm mạng, dịch vụ web hoặc thậm chí các vấn đề đồng thời (xem stackoverflow.com/questions/ 481.714 / CCR-suất-and-vb-net )
LBushkin

Ví dụ hay, vì vậy về cơ bản nó là một trình tạo bộ sưu tập dựa trên ngữ cảnh (ví dụ: gọi phương thức) và không bắt đầu hoạt động cho đến khi có thứ gì đó cố gắng truy cập nó, trong khi một phương thức thu thập truyền thống không có lợi nhuận sẽ cần biết kích thước của nó để xây dựng và trả về một bộ sưu tập đầy đủ - sau đó lặp lại phần bắt buộc của bộ sưu tập đó?
Michael Harper

24

Các câu trả lời tốt ở đây cho thấy rằng một lợi ích của yield returnnó là bạn không cần phải tạo một danh sách ; Danh sách có thể tốn kém. (Ngoài ra, sau một thời gian, bạn sẽ thấy chúng cồng kềnh và thiếu lịch sự.)

Nhưng nếu bạn không có Danh sách thì sao?

yield returncho phép bạn duyệt qua cấu trúc dữ liệu (không nhất thiết là Danh sách) theo một số cách. Ví dụ: nếu đối tượng của bạn là Cây, bạn có thể duyệt các nút theo thứ tự trước hoặc sau mà không cần tạo danh sách khác hoặc thay đổi cấu trúc dữ liệu cơ bản.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

1
Ví dụ này cũng nêu bật trường hợp ủy quyền. Nếu bạn có một bộ sưu tập trong một số trường hợp nhất định có thể chứa các mục của các bộ sưu tập khác, rất đơn giản để lặp lại và sử dụng trả về lợi nhuận thay vì tạo danh sách đầy đủ tất cả các kết quả và trả về.
Tom Mayfield

1
Bây giờ C # chỉ cần triển khai yield!theo cách F # thực hiện để bạn không cần tất cả các foreachcâu lệnh.
CoderDennis

Ngẫu nhiên, ví dụ của bạn cho thấy một trong những "mối nguy" yield return: thường không rõ ràng khi nào nó sẽ mang lại mã hiệu quả hoặc không hiệu quả. Mặc dù yield returncó thể được sử dụng đệ quy, cách sử dụng như vậy sẽ áp đặt chi phí đáng kể cho việc xử lý các điều tra viên lồng nhau sâu. Quản lý trạng thái thủ công có thể phức tạp hơn để viết mã, nhưng chạy hiệu quả hơn nhiều.
supercat

17

Đánh giá lười biếng / Thực hiện trì hoãn

Các khối trình lặp "trả về lợi nhuận" sẽ không thực thi bất kỳ mã nào cho đến khi bạn thực sự gọi cho kết quả cụ thể đó. Điều này có nghĩa là chúng cũng có thể được liên kết với nhau một cách hiệu quả. Câu đố vui: đoạn mã sau sẽ lặp lại bao nhiêu lần trên tệp?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

Câu trả lời chính xác là một, và điều đó chưa xảy ra foreach. Mặc dù tôi có ba hàm toán tử linq riêng biệt, chúng tôi vẫn chỉ lặp lại nội dung của tệp một lần.

Điều này có những lợi ích khác ngoài hiệu suất. Ví dụ: tôi có thể viết một phương pháp đơn giản và chung chung để đọc và lọc trước một tệp nhật ký một lần và sử dụng cùng một phương pháp đó ở một số nơi khác nhau, nơi mỗi lần sử dụng sẽ thêm vào các bộ lọc khác nhau. Do đó, tôi duy trì hiệu suất tốt trong khi sử dụng lại mã một cách hiệu quả.

Danh sách vô hạn

Xem câu trả lời của tôi cho câu hỏi này để biết ví dụ điển hình:
Hàm C # fibonacci trả về lỗi

Về cơ bản, tôi triển khai trình tự fibonacci bằng cách sử dụng một khối trình lặp sẽ không bao giờ dừng (ít nhất, không phải trước khi đạt đến MaxInt), và sau đó sử dụng việc triển khai đó một cách an toàn.

Ngữ nghĩa được cải thiện và phân tách các mối quan tâm

Một lần nữa bằng cách sử dụng ví dụ tệp ở trên, giờ đây chúng ta có thể dễ dàng tách mã đọc tệp khỏi mã lọc ra các dòng không cần thiết từ mã thực sự phân tích kết quả. Đặc biệt, cái đầu tiên đó rất có thể tái sử dụng.

Đây là một trong những điều khó giải thích bằng văn xuôi hơn nhiều so với những người có hình ảnh đơn giản 1 :

Mối quan tâm tách biệt giữa mệnh lệnh và chức năng

Nếu bạn không thể nhìn thấy hình ảnh, nó hiển thị hai phiên bản của cùng một mã, với các điểm nổi bật nền cho các mối quan tâm khác nhau. Mã linq có tất cả các màu được nhóm lại độc đáo, trong khi mã mệnh lệnh truyền thống có các màu xen kẽ. Tác giả lập luận (và tôi đồng tình) rằng kết quả này là điển hình của việc sử dụng linq so với sử dụng mã mệnh lệnh ... rằng linq thực hiện tốt hơn công việc tổ chức mã của bạn để có luồng tốt hơn giữa các phần.


1 Tôi tin rằng đây là nguồn gốc: https://twitter.com/mariofusco/status/571999216039542784 . Cũng lưu ý rằng mã này là Java, nhưng C # sẽ tương tự.


1
Thực thi hoãn lại có lẽ là lợi ích lớn nhất của trình vòng lặp.
justin.m.chase

12

Đôi khi các chuỗi bạn cần trả lại quá lớn để vừa với bộ nhớ. Ví dụ: khoảng 3 tháng trước, tôi đã tham gia một dự án di chuyển dữ liệu giữa các cơ sở dữ liệu MS SLQ. Dữ liệu được xuất ở định dạng XML. Lợi nhuận thu về hóa ra lại khá hữu ích với XmlReader . Nó làm cho việc lập trình trở nên dễ dàng hơn. Ví dụ: giả sử một tệp có 1000 phần tử Khách hàng - nếu bạn chỉ đọc tệp này vào bộ nhớ, điều này sẽ yêu cầu lưu trữ tất cả chúng trong bộ nhớ cùng một lúc, ngay cả khi chúng được xử lý tuần tự. Vì vậy, bạn có thể sử dụng trình vòng lặp để duyệt qua từng bộ sưu tập. Trong trường hợp đó, bạn chỉ phải dành bộ nhớ cho một phần tử.

Hóa ra, sử dụng XmlReader cho dự án của chúng tôi là cách duy nhất để làm cho ứng dụng hoạt động - nó hoạt động trong một thời gian dài, nhưng ít nhất nó không làm treo toàn bộ hệ thống và không phát sinh OutOfMemoryException . Tất nhiên, bạn có thể làm việc với XmlReader mà không cần trình vòng lặp năng suất. Nhưng các trình vòng lặp đã làm cho cuộc sống của tôi dễ dàng hơn nhiều (tôi sẽ không viết mã để nhập quá nhanh và không gặp rắc rối). Hãy xem trang này để biết cách sử dụng trình lặp năng suất để giải quyết các vấn đề thực tế (không chỉ là khoa học với chuỗi vô hạn).


9

Trong các tình huống đồ chơi / trình diễn, không có nhiều sự khác biệt. Nhưng có những tình huống mà các trình vòng lặp mang lại rất hữu ích - đôi khi, toàn bộ danh sách không có sẵn (ví dụ: các luồng) hoặc danh sách tốn kém về mặt tính toán và không cần thiết phải sử dụng toàn bộ.


2

Nếu toàn bộ danh sách khổng lồ, nó có thể ngốn rất nhiều bộ nhớ chỉ để ngồi xung quanh, trong khi với lợi nhuận, bạn chỉ chơi với những gì bạn cần, khi bạn cần, bất kể có bao nhiêu mục.



2

Bằng cách sử dụng, yield returnbạn có thể lặp lại các mục mà không cần phải tạo danh sách. Nếu bạn không cần danh sách, nhưng muốn lặp lại một số tập hợp các mục, có thể dễ dàng hơn để viết

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Hơn

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Bạn có thể đặt tất cả logic để xác định xem bạn có muốn hoạt động trên foo bên trong phương thức của mình hay không bằng cách sử dụng trả về lợi nhuận và bạn thấy trước vòng lặp có thể ngắn gọn hơn nhiều.


2

Đây là câu trả lời được chấp nhận trước đây của tôi cho chính xác câu hỏi tương tự:

Lợi nhuận từ khóa giá trị gia tăng?

Một cách khác để xem xét các phương thức trình lặp là chúng thực hiện công việc khó khăn để chuyển một thuật toán "từ trong ra ngoài". Hãy xem xét một trình phân tích cú pháp. Nó kéo văn bản từ một luồng, tìm kiếm các mẫu trong đó và tạo ra mô tả logic cấp cao về nội dung.

Bây giờ, tôi có thể thực hiện điều này dễ dàng cho chính mình với tư cách là tác giả phân tích cú pháp bằng cách sử dụng phương pháp SAX, trong đó tôi có giao diện gọi lại mà tôi sẽ thông báo bất cứ khi nào tôi tìm thấy phần tiếp theo của mẫu. Vì vậy, trong trường hợp SAX, mỗi lần tôi tìm thấy phần bắt đầu của một phần tử, tôi gọi beginElementphương thức, v.v.

Nhưng điều này tạo ra rắc rối cho người dùng của tôi. Họ phải triển khai giao diện trình xử lý và do đó họ phải viết một lớp máy trạng thái đáp ứng các phương thức gọi lại. Điều này thật khó để làm đúng, vì vậy điều dễ dàng nhất để làm là sử dụng một triển khai cổ phiếu xây dựng cây DOM, và sau đó họ sẽ có được sự thuận tiện khi có thể đi bộ trên cây. Nhưng sau đó toàn bộ cấu trúc được lưu vào bộ nhớ - không tốt.

Nhưng thay vào đó, tôi viết trình phân tích cú pháp của mình như một phương thức trình lặp thì sao?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Điều đó sẽ không khó viết hơn cách tiếp cận giao diện gọi lại - chỉ cần trả về một đối tượng bắt nguồn từ LanguageElementlớp cơ sở của tôi thay vì gọi một phương thức gọi lại.

Người dùng hiện có thể sử dụng foreach để lặp qua đầu ra của trình phân tích cú pháp của tôi, vì vậy họ sẽ có được một giao diện lập trình mệnh lệnh rất thuận tiện.

Kết quả là cả hai mặt của một API tùy chỉnh trông giống như chúng đang được kiểm soát và do đó dễ viết và dễ hiểu hơn.


2

Lý do cơ bản để sử dụng lợi nhuận là nó tự tạo / trả về một danh sách. Chúng ta có thể sử dụng danh sách trả về để lặp lại thêm.


Đúng về mặt khái niệm nhưng không đúng về mặt kỹ thuật. Nó trả về một thể hiện của IEnumerable mà chỉ đơn giản là trừu tượng hóa một trình lặp. Trình lặp đó thực sự là logic để lấy mục tiếp theo chứ không phải danh sách cụ thể hóa. Việc sử dụng return yieldkhông tạo ra danh sách, nó chỉ tạo mục tiếp theo trong danh sách và chỉ khi được yêu cầu (lặp lại).
Sinaesthetic 8/1017
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.