Sử dụng đúng 'lợi nhuận'


903

Các năng suất từ khóa là một trong những từ khóa trong C # mà tiếp tục mystify tôi, và tôi chưa bao giờ tự tin rằng mình đang sử dụng nó một cách chính xác.

Trong hai đoạn mã sau đây, mã nào được ưu tiên và tại sao?

Phiên bản 1: Sử dụng lợi nhuận

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Phiên bản 2: Trả về danh sách

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldđược gắn với IEnumerable<T>và loại của nó. Đó là trong một đánh giá lười biếng nào đó
Jaider

đây là một câu trả lời tuyệt vời cho câu hỏi tương tự stackoverflow.com/questions/15381708/ từ
Sanjeev Rai

1
Đây là một ví dụ sử dụng tốt: stackoverflow.com/questions/3392612/ từ
ValGe

6
Tôi thấy một trường hợp tốt để sử dụng yield returnnếu mã lặp qua kết quả GetAllProducts()cho phép người dùng có cơ hội hủy sớm quá trình xử lý.
JMD

2
Tôi tìm thấy chủ đề này thực sự hữu ích: lập trình
viên.stackexchange.com/a/97350/136144

Câu trả lời:


806

Tôi có xu hướng sử dụng lợi nhuận khi tôi tính toán mục tiếp theo trong danh sách (hoặc thậm chí nhóm mục tiếp theo).

Sử dụng Phiên bản 2 của bạn, bạn phải có danh sách đầy đủ trước khi quay lại. Bằng cách sử dụng lợi nhuận, bạn thực sự chỉ cần có mục tiếp theo trước khi quay trở lại.

Trong số những thứ khác, điều này giúp phân bổ chi phí tính toán của các phép tính phức tạp trong khung thời gian lớn hơn. Ví dụ: nếu danh sách được nối với GUI và người dùng không bao giờ đi đến trang cuối cùng, bạn không bao giờ tính toán các mục cuối cùng trong danh sách.

Một trường hợp khác mà lợi nhuận mang lại là tốt hơn là nếu IEnumerable đại diện cho một tập hợp vô hạn. Xem xét danh sách các số nguyên tố hoặc danh sách vô số các số ngẫu nhiên. Bạn không bao giờ có thể trả lại toàn bộ IEn Countable cùng một lúc, vì vậy bạn sử dụng lợi nhuận để trả về danh sách tăng dần.

Trong ví dụ cụ thể của bạn, bạn có danh sách đầy đủ các sản phẩm, vì vậy tôi sẽ sử dụng Phiên bản 2.


31
Tôi muốn biết rằng trong ví dụ của bạn trong câu hỏi 3 có hai lợi ích. 1) Nó phân bổ chi phí tính toán (đôi khi là lợi ích, đôi khi không) 2) Nó có thể lười biếng tránh tính toán vô thời hạn trong nhiều trường hợp sử dụng. Bạn không đề cập đến nhược điểm tiềm năng mà nó giữ xung quanh trạng thái trung gian. Nếu bạn có số lượng đáng kể trạng thái trung gian (giả sử Hashset để loại bỏ trùng lặp) thì việc sử dụng năng suất có thể làm tăng dấu chân bộ nhớ của bạn.
Kennet Belenky

8
Ngoài ra, nếu mỗi phần tử riêng lẻ rất lớn, nhưng chúng chỉ cần được truy cập tuần tự, một sản lượng sẽ tốt hơn.
Kennet Belenky

2
Và cuối cùng ... có một kỹ thuật hơi mạnh mẽ nhưng đôi khi hiệu quả để sử dụng năng suất để viết mã không đồng bộ ở dạng rất tuần tự.
Kennet Belenky

12
Một ví dụ khác có thể thú vị là khi đọc các tệp CSV khá lớn. Bạn muốn đọc từng yếu tố nhưng bạn cũng muốn rút ra sự phụ thuộc của mình. Hiệu suất trả về một IEnumerable <> sẽ cho phép bạn trả lại từng hàng và xử lý từng hàng riêng lẻ. Không cần đọc tệp 10 Mb vào bộ nhớ. Chỉ một dòng tại một thời điểm.
Maxime Rouiller

1
Yield returndường như là viết tắt cho việc viết lớp trình vòng lặp tùy chỉnh của riêng bạn (triển khai IEnumerator). Do đó, các lợi ích được đề cập cũng áp dụng cho các lớp lặp tùy chỉnh. Dù sao, cả hai cấu trúc giữ trạng thái trung gian. Ở dạng đơn giản nhất, đó là về việc giữ một tham chiếu đến đối tượng hiện tại.
J. Ouwehand

641

Việc điền một danh sách tạm thời giống như tải xuống toàn bộ video, trong khi sử dụng yieldcũng giống như truyền phát video đó.


180
Tôi hoàn toàn biết rằng câu trả lời này không phải là câu trả lời kỹ thuật nhưng tôi tin rằng sự giống nhau giữa năng suất và phát trực tuyến video là một ví dụ tốt khi hiểu từ khóa năng suất. Tất cả mọi thứ kỹ thuật đã được nói về chủ đề này, vì vậy tôi đã cố gắng giải thích "nói cách khác". Có một quy tắc cộng đồng nào nói rằng bạn không thể giải thích ý tưởng của mình bằng các thuật ngữ phi kỹ thuật không?
anar khalilov

13
Tôi không chắc ai đã bình chọn cho bạn hoặc tại sao (tôi ước họ sẽ bình luận), nhưng tôi nghĩ nó phần nào mô tả nó từ một triển vọng phi kỹ thuật.
senfo

22
Vẫn nắm bắt được khái niệm và điều này đã giúp đưa nó vào trọng tâm hơn, tương tự tốt đẹp.
Tony

11
Tôi thích câu trả lời này, nhưng nó không trả lời câu hỏi.
ANeves

73

Như một ví dụ khái niệm để hiểu khi nào bạn nên sử dụng yield, giả sử phương thức ConsumeLoop()xử lý các mục được trả về / mang lại bởi ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Nếu không yield, cuộc gọi đến ProduceList()có thể mất nhiều thời gian vì bạn phải hoàn thành danh sách trước khi quay lại:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Sử dụng yield, nó trở thành sắp xếp lại, loại làm việc "song song":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

Và cuối cùng, như nhiều người trước đây đã đề xuất, bạn nên sử dụng Phiên bản 2 vì dù sao bạn cũng đã có danh sách hoàn thành.


30

Tôi biết đây là một câu hỏi cũ, nhưng tôi muốn đưa ra một ví dụ về cách từ khóa năng suất có thể được sử dụng một cách sáng tạo. Tôi thực sự hưởng lợi từ kỹ thuật này. Hy vọng rằng điều này sẽ giúp ích cho bất cứ ai khác vấp phải câu hỏi này.

Lưu ý: Đừng nghĩ về từ khóa năng suất chỉ là một cách khác để xây dựng bộ sưu tập. Một phần lớn sức mạnh của năng suất đến trong thực tế là việc thực thi bị tạm dừng trong phương thức hoặc thuộc tính của bạn cho đến khi mã gọi lặp lại giá trị tiếp theo. Đây là ví dụ của tôi:

Sử dụng từ khóa suất (cùng với triển khai Caliburn.Micro coroutines của Rob Eisenburg ) cho phép tôi thể hiện một cuộc gọi không đồng bộ đến một dịch vụ web như thế này:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Điều này sẽ làm là bật BusyIndicator của tôi, gọi phương thức Đăng nhập trên dịch vụ web của tôi, đặt cờ IsLoggedIn của tôi thành giá trị trả về, sau đó tắt BusyIndicator.

Đây là cách thức hoạt động: IResult có phương thức Thực thi và sự kiện Đã hoàn thành. Caliburn.Micro lấy IEnumerator từ lệnh gọi đến HandButtonClick () và chuyển nó vào một phương thức Coroutine.BeginExecute. Phương thức BeginExecute bắt đầu lặp qua IResults. Khi IResult đầu tiên được trả về, việc thực thi bị tạm dừng bên trong HandButtonClick () và BeginExecute () đính kèm một trình xử lý sự kiện vào sự kiện đã hoàn thành và gọi Execute (). IResult.Execute () có thể thực hiện tác vụ đồng bộ hoặc không đồng bộ và kích hoạt sự kiện Đã hoàn thành khi hoàn thành.

LoginResult trông giống như thế này:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Nó có thể giúp thiết lập một cái gì đó như thế này và từng bước thực hiện để xem những gì đang diễn ra.

Mong rằng nó giúp ai đó thoát! Tôi thực sự rất thích khám phá những cách khác nhau có thể sử dụng năng suất.


1
mẫu mã của bạn là một ví dụ tuyệt vời về cách sử dụng năng suất NGOÀI TRỜI của một khối for hoặc foreach. Hầu hết các ví dụ cho thấy lợi nhuận mang lại trong một iterator. Rất hữu ích vì tôi vừa mới đặt câu hỏi về SO Làm thế nào để sử dụng năng suất bên ngoài một trình vòng lặp!
Shelbypereira

Nó chưa bao giờ xảy ra với tôi để sử dụng yieldtheo cách này. Nó có vẻ như là một cách thanh lịch để mô phỏng mô hình async / await (mà tôi giả sử sẽ được sử dụng thay vì yieldnếu điều này được viết lại ngày hôm nay). Bạn có thấy rằng những cách sử dụng sáng tạo này yieldđã mang lại lợi nhuận giảm dần (không có ý định chơi chữ) trong những năm qua vì C # đã phát triển kể từ khi bạn trả lời câu hỏi này? Hay bạn vẫn đang đến với các trường hợp sử dụng thông minh được hiện đại hóa như thế này? Và nếu vậy, bạn có phiền khi chia sẻ một kịch bản thú vị khác cho chúng tôi không?
Bỏ qua

27

Đây có vẻ là một gợi ý kỳ quái, nhưng tôi đã học cách sử dụng yieldtừ khóa trong C # bằng cách đọc một bài thuyết trình về trình tạo trong Python: http://www.dabeaz.com/generators/Generators.pdf của David M. Beazley . Bạn không cần biết nhiều Python để hiểu bài thuyết trình - Tôi thì không. Tôi thấy nó rất hữu ích trong việc giải thích không chỉ cách thức máy phát điện hoạt động mà tại sao bạn nên quan tâm.


1
Bài thuyết trình cung cấp một cái nhìn tổng quan đơn giản. Các chi tiết về cách thức hoạt động của nó trong C # được Ray Chen thảo luận trong các liên kết tại stackoverflow.com/a/39507/939250 Liên kết đầu tiên giải thích chi tiết rằng có một lợi nhuận thứ hai, ẩn ở cuối các phương thức hoàn trả lợi tức.
Donal Lafferty

18

Lợi nhuận mang lại có thể rất mạnh đối với các thuật toán mà bạn cần lặp qua hàng triệu đối tượng. Xem xét ví dụ sau đây nơi bạn cần tính toán các chuyến đi có thể để chia sẻ đi xe. Đầu tiên chúng tôi tạo ra các chuyến đi có thể:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Sau đó lặp đi lặp lại qua mỗi chuyến đi:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Nếu bạn sử dụng Danh sách thay vì sản lượng, bạn sẽ cần phân bổ 1 triệu đối tượng vào bộ nhớ (~ 190mb) và ví dụ đơn giản này sẽ mất ~ 1400ms để chạy. Tuy nhiên, nếu bạn sử dụng năng suất, bạn không cần đặt tất cả các đối tượng tạm thời này vào bộ nhớ và bạn sẽ nhận được tốc độ thuật toán nhanh hơn đáng kể: ví dụ này sẽ chỉ mất ~ 400ms để chạy mà không tiêu thụ bộ nhớ.


2
dưới vỏ bọc năng suất là gì? Tôi đã nghĩ rằng đó là một danh sách, do đó nó sẽ cải thiện việc sử dụng bộ nhớ như thế nào?
cuộn

1
@rolls yieldhoạt động dưới vỏ bọc bằng cách thực hiện một bộ máy nhà nước trong nội bộ. Đây là một câu trả lời SO với 3 bài viết trên blog MSDN chi tiết giải thích việc thực hiện rất chi tiết. Viết bởi Raymond Chen @ MSFT
Shiva

13

Hai đoạn mã thực sự đang làm hai việc khác nhau. Phiên bản đầu tiên sẽ kéo các thành viên khi bạn cần họ. Phiên bản thứ hai sẽ tải tất cả các kết quả vào bộ nhớ trước khi bạn bắt đầu làm bất cứ điều gì với nó.

Không có câu trả lời đúng hay sai cho câu hỏi này. Cái nào thích hợp hơn chỉ phụ thuộc vào tình huống. Ví dụ: nếu có giới hạn thời gian bạn phải hoàn thành truy vấn của mình và bạn cần thực hiện một điều gì đó phức tạp với kết quả, thì phiên bản thứ hai có thể thích hợp hơn. Nhưng hãy cẩn thận với các kết quả lớn, đặc biệt nếu bạn đang chạy mã này ở chế độ 32 bit. Tôi đã bị cắn bởi ngoại lệ OutOfMemory nhiều lần khi thực hiện phương pháp này.

Điều quan trọng cần ghi nhớ là điều này mặc dù: sự khác biệt là về hiệu quả. Vì vậy, có lẽ bạn nên đi với bất kỳ ai làm cho mã của bạn đơn giản hơn và chỉ thay đổi nó sau khi định hình.


11

Năng suất có hai công dụng tuyệt vời

Nó giúp cung cấp phép lặp tùy chỉnh với việc tạo ra các bộ sưu tập tạm thời. (tải tất cả dữ liệu và lặp)

Nó giúp làm lặp đi lặp lại trạng thái. (phát trực tuyến)

Dưới đây là một video đơn giản mà tôi đã tạo với trình diễn đầy đủ để hỗ trợ hai điểm trên

http://www.youtube.com/watch?v=4fju3xcm21M


10

Đây là những gì Chris Sells kể về những tuyên bố đó trong Ngôn ngữ lập trình C # ;

Đôi khi tôi quên rằng lợi nhuận mang lại không giống như lợi nhuận, trong đó mã sau khi hoàn vốn có thể được thực thi. Ví dụ, mã sau lần trả về đầu tiên ở đây không bao giờ được thực thi:

    int F() {
return 1;
return 2; // Can never be executed
}

Ngược lại, mã sau lợi nhuận đầu tiên ở đây có thể được thực thi:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Điều này thường cắn tôi trong một câu lệnh if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

Trong những trường hợp này, hãy nhớ rằng lợi nhuận mang lại không phải là quyết định cuối cùng giống như lợi nhuận.


để giảm bớt sự mơ hồ, xin vui lòng làm rõ khi bạn nói có thể, đó là, sẽ hay có thể? có thể cho người đầu tiên quay trở lại và không thực hiện sản lượng thứ hai?
Johno Crawford

@JohnoCrawford, báo cáo lợi tức thứ hai sẽ chỉ thực hiện nếu giá trị thứ hai / tiếp theo của IEnumerable được liệt kê. Hoàn toàn có thể là nó sẽ không, ví dụ F().Any()- điều này sẽ trở lại sau khi cố gắng liệt kê kết quả đầu tiên mà thôi. Nói chung, bạn không nên dựa vào IEnumerable yieldtrạng thái thay đổi chương trình, bởi vì nó có thể không thực sự được kích hoạt
Zac Faragher

8

Giả sử các sản phẩm của bạn Lớp LINQ sử dụng một năng suất tương tự để liệt kê / lặp lại, phiên bản đầu tiên hiệu quả hơn vì nó chỉ mang lại một giá trị mỗi lần lặp lại.

Ví dụ thứ hai là chuyển đổi trình liệt kê / iterator thành một danh sách với phương thức ToList (). Điều này có nghĩa là nó lặp lại thủ công trên tất cả các mục trong bảng liệt kê và sau đó trả về một danh sách phẳng.


8

Đây là một vấn đề quan trọng, nhưng vì câu hỏi được gắn thẻ tốt nhất nên tôi sẽ tiếp tục và ném hai xu của mình. Đối với loại điều này, tôi rất thích biến nó thành một tài sản:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Chắc chắn, đó là một tấm nồi hơi nhiều hơn, nhưng mã sử dụng này sẽ trông gọn gàng hơn nhiều:

prices = Whatever.AllProducts.Select (product => product.price);

đấu với

prices = Whatever.GetAllProducts().Select (product => product.price);

Lưu ý: Tôi sẽ không làm điều này cho bất kỳ phương pháp nào có thể mất một thời gian để thực hiện công việc của họ.


7

Còn cái này thì sao?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Tôi đoán điều này là sạch sẽ hơn nhiều. Mặc dù vậy, tôi không có VS2008 để kiểm tra. Trong mọi trường hợp, nếu Sản phẩm thực hiện IEnumerable (dường như - nó được sử dụng trong một tuyên bố foreach), tôi sẽ trả lại trực tiếp.


2
Vui lòng chỉnh sửa OP để bao gồm thêm thông tin thay vì đăng câu trả lời.
Brian Rasmussen

Chà, bạn phải cho tôi biết OP thực sự đại diện cho điều gì :-) Cảm ơn
petr k.

Bài gốc, tôi giả sử. Tôi không thể chỉnh sửa bài viết, vì vậy đây dường như là cách để đi.
petr k.

5

Tôi đã sử dụng phiên bản 2 của mã trong trường hợp này. Vì bạn có sẵn danh sách đầy đủ các sản phẩm và đó là những gì mà "người tiêu dùng" mong đợi của cuộc gọi phương thức này, nên sẽ phải gửi lại thông tin đầy đủ cho người gọi.

Nếu người gọi phương thức này yêu cầu thông tin "một" tại một thời điểm và việc tiêu thụ thông tin tiếp theo là theo yêu cầu, thì sẽ có ích khi sử dụng lợi nhuận mang lại, đảm bảo lệnh thực thi sẽ được trả lại cho người gọi khi một đơn vị thông tin có sẵn.

Một số ví dụ mà người ta có thể sử dụng lợi nhuận là:

  1. Tính toán phức tạp, từng bước trong đó người gọi đang chờ dữ liệu của một bước tại một thời điểm
  2. Phân trang trong GUI - nơi người dùng có thể không bao giờ truy cập đến trang cuối cùng và chỉ có bộ thông tin phụ được yêu cầu tiết lộ trên trang hiện tại

Để trả lời câu hỏi của bạn, tôi đã sử dụng phiên bản 2.


3

Trả lại danh sách trực tiếp. Những lợi ích:

  • Rõ ràng hơn
  • Danh sách này có thể tái sử dụng. (iterator không) không thực sự đúng, Cảm ơn Jon

Bạn nên sử dụng iterator (suất) từ khi bạn nghĩ rằng có lẽ bạn sẽ không phải lặp đi lặp lại cho đến cuối danh sách hoặc khi nó không có kết thúc. Ví dụ, ứng dụng khách gọi sẽ tìm kiếm sản phẩm đầu tiên thỏa mãn một số vị từ, bạn có thể cân nhắc sử dụng trình lặp, mặc dù đó là một ví dụ giả định và có lẽ có nhiều cách tốt hơn để thực hiện nó. Về cơ bản, nếu bạn biết trước rằng toàn bộ danh sách sẽ cần được tính toán, chỉ cần thực hiện trước. Nếu bạn nghĩ rằng nó sẽ không, thì hãy xem xét sử dụng phiên bản iterator.


Đừng quên rằng nó sẽ trở lại trong IEnumerable <T>, không phải là IEnumerator <T> - bạn có thể gọi lại GetEnumerator.
Jon Skeet

Ngay cả khi bạn biết trước toàn bộ danh sách sẽ cần phải được tính toán, nó vẫn có thể có lợi để sử dụng lợi nhuận. Một ví dụ là khi bộ sưu tập chứa hàng trăm ngàn mặt hàng.
Val

1

Cụm từ khóa trả về năng suất được sử dụng để duy trì máy trạng thái cho một bộ sưu tập cụ thể. Bất cứ nơi nào CLR thấy cụm từ trả về lợi tức đang được sử dụng, CLR sẽ thực hiện một mẫu Số đếm cho đoạn mã đó. Kiểu triển khai này giúp nhà phát triển từ tất cả các loại hệ thống ống nước mà chúng ta sẽ phải làm nếu không có từ khóa.

Giả sử nếu nhà phát triển đang lọc một số bộ sưu tập, lặp qua bộ sưu tập và sau đó trích xuất các đối tượng đó trong một số bộ sưu tập mới. Loại ống nước này khá đơn điệu.

Thông tin thêm về từ khóa ở đây tại bài viết này .


-4

Việc sử dụng năng suất tương tự như trả về từ khóa , ngoại trừ việc nó sẽ trả về một trình tạo . Và đối tượng máy phát sẽ chỉ đi qua một lần .

năng suất có hai lợi ích:

  1. Bạn không cần phải đọc các giá trị này hai lần;
  2. Bạn có thể nhận được nhiều nút con nhưng không phải đặt tất cả chúng vào bộ nhớ.

Có một lời giải thích rõ ràng khác có thể giúp bạn.

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.