Penggunaan 'imbal hasil' yang benar

903

Kata kunci hasil adalah salah satu kata kunci dalam C # yang terus membingungkan saya, dan saya tidak pernah yakin bahwa saya menggunakannya dengan benar.

Dari dua potongan kode berikut, mana yang lebih disukai dan mengapa?

Versi 1: Menggunakan pengembalian hasil

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

Versi 2: Kembalikan daftar

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

        return products.ToList<Product>();
    }
}
senfo
sumber
38
yieldterikat IEnumerable<T>dan jenisnya. Ini dalam evaluasi malas entah bagaimana
Jaider
di sini ada jawaban greate untuk pertanyaan serupa. stackoverflow.com/questions/15381708/…
Sanjeev Rai
1
Berikut ini contoh penggunaan yang baik: stackoverflow.com/questions/3392612/…
ValGe
6
Saya melihat kasus yang baik untuk digunakan yield returnjika kode yang berulang melalui hasil GetAllProducts()memungkinkan pengguna kesempatan untuk membatalkan pemrosesan lebih awal.
JMD
2
Saya menemukan utas ini sangat membantu: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Jawaban:

806

Saya cenderung menggunakan imbal hasil ketika saya menghitung item berikutnya dalam daftar (atau bahkan grup item berikutnya).

Menggunakan Versi 2 Anda, Anda harus memiliki daftar lengkap sebelum kembali. Dengan menggunakan imbal hasil, Anda benar-benar hanya perlu memiliki item berikutnya sebelum kembali.

Di antara hal-hal lain, ini membantu menyebarkan biaya perhitungan perhitungan yang kompleks dalam kerangka waktu yang lebih besar. Misalnya, jika daftar terhubung ke GUI dan pengguna tidak pernah pergi ke halaman terakhir, Anda tidak pernah menghitung item akhir dalam daftar.

Kasus lain di mana imbal hasil lebih disukai adalah jika IEnumerable mewakili set yang tak terbatas. Pertimbangkan daftar Nomor Utama, atau daftar nomor acak tanpa batas. Anda tidak pernah dapat mengembalikan IEnumerable penuh sekaligus, jadi Anda menggunakan imbal hasil untuk mengembalikan daftar secara bertahap.

Dalam contoh khusus Anda, Anda memiliki daftar lengkap produk, jadi saya akan menggunakan Versi 2.

kasar
sumber
31
Saya akan mengatakan bahwa dalam contoh Anda dalam pertanyaan 3 mengonfigurasi dua manfaat. 1) Ini menyebar biaya komputasi (kadang-kadang manfaat, kadang tidak) 2) Mungkin malas menghindari perhitungan tanpa batas dalam banyak kasus penggunaan. Anda gagal menyebutkan kelemahan potensial yang disimpan di sekitar negara perantara. Jika Anda memiliki sejumlah besar kondisi antara (katakanlah HashSet untuk penghapusan duplikat) maka penggunaan hasil dapat meningkatkan jejak memori Anda.
Kennet Belenky
8
Juga, jika setiap elemen individu sangat besar, tetapi mereka hanya perlu diakses secara berurutan, hasilnya lebih baik.
Kennet Belenky
2
Dan akhirnya ... ada teknik yang sedikit miring tetapi kadang-kadang efektif untuk menggunakan hasil untuk menulis kode asinkron dalam bentuk serial.
Kennet Belenky
12
Contoh lain yang mungkin menarik adalah ketika membaca file CSV yang agak besar. Anda ingin membaca setiap elemen tetapi Anda juga ingin mengekstrak ketergantungan Anda. Hasil pengembalian IEnumerable <> akan memungkinkan Anda untuk mengembalikan setiap baris dan memproses setiap baris secara terpisah. Tidak perlu membaca file 10 Mb ke memori. Hanya satu baris dalam satu waktu.
Maxime Rouiller
1
Yield returntampaknya menjadi singkatan untuk menulis kelas iterator kustom Anda sendiri (mengimplementasikan IEnumerator). Karenanya, manfaat yang disebutkan juga berlaku untuk kelas iterator khusus. Pokoknya, kedua konstruksi menjaga keadaan menengah. Dalam bentuk yang paling sederhana, ini tentang memegang referensi ke objek saat ini.
J. Ouwehand
641

Mengisi daftar sementara seperti mengunduh seluruh video, sedangkan menggunakan yieldseperti streaming video itu.

anar khalilov
sumber
180
Saya sangat menyadari bahwa jawaban ini bukan jawaban teknis, tetapi saya percaya bahwa kemiripan antara hasil dan streaming video berfungsi sebagai contoh yang baik ketika memahami kata kunci hasil. Semuanya teknis telah dikatakan tentang hal ini, jadi saya mencoba menjelaskan "dengan kata lain". Apakah ada aturan komunitas yang mengatakan Anda tidak dapat menjelaskan ide-ide Anda secara non-teknis?
anar khalilov
13
Saya tidak yakin siapa yang memilih Anda atau mengapa (saya berharap mereka akan berkomentar), tetapi saya pikir itu agak menggambarkannya dari calon non-teknis.
senfo
22
Masih memahami konsep ini dan ini membantu membawanya lebih jauh ke dalam fokus, analogi yang bagus.
Tony
11
Saya suka jawaban ini, tetapi tidak menjawab pertanyaan.
ANeves
73

Sebagai contoh konseptual untuk memahami kapan Anda harus menggunakan yield, katakanlah metode ConsumeLoop()memproses item yang dikembalikan / dihasilkan oleh ProduceList():

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

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

Tanpa yield, panggilan ke ProduceList()mungkin memakan waktu lama karena Anda harus melengkapi daftar sebelum kembali:

//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]

Menggunakan yield, itu diatur ulang, semacam bekerja "secara paralel":

//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

Dan terakhir, seperti banyak yang sudah disarankan sebelumnya, Anda harus menggunakan Versi 2 karena Anda sudah memiliki daftar yang lengkap.

Kache
sumber
30

Saya tahu ini adalah pertanyaan lama, tetapi saya ingin menawarkan satu contoh bagaimana kata kunci hasil dapat digunakan secara kreatif. Saya benar - benar mendapat manfaat dari teknik ini. Semoga ini bisa menjadi bantuan bagi siapa pun yang menemukan pertanyaan ini.

Catatan: Jangan menganggap kata kunci hasil sebagai hanya cara lain untuk membangun koleksi. Sebagian besar dari kekuatan hasil datang dalam kenyataan bahwa eksekusi dijeda dalam metode atau properti Anda hingga kode panggilan beralih ke nilai berikutnya. Inilah contoh saya:

Dengan menggunakan kata kunci hasil (di samping implementasi coribine Coribine Caliburn.Micro karya Rob Eisenburg ) memungkinkan saya untuk mengekspresikan panggilan asinkron ke layanan web seperti ini:

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

Apa yang akan dilakukan adalah mengaktifkan BusyIndicator saya, memanggil metode Login di layanan web saya, mengatur bendera IsLoggedIn saya ke nilai kembali, dan kemudian mematikan BusyIndicator kembali.

Begini cara kerjanya: IResult memiliki metode Execute dan acara Selesai. Caliburn.Micro mengambil IEnumerator dari panggilan ke HandleButtonClick () dan meneruskannya ke metode Coroutine.BeginExecute. Metode BeginExecute mulai iterasi melalui IResult. Ketika IResult pertama dikembalikan, eksekusi dijeda di dalam HandleButtonClick (), dan BeginExecute () melampirkan pengendali acara ke acara Selesai dan panggilan Execute (). IResult.Execute () dapat melakukan tugas sinkron atau asinkron dan menjalankan acara Selesai saat selesai.

LoginResult terlihat seperti ini:

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

Mungkin membantu untuk mengatur sesuatu seperti ini dan melangkah melalui eksekusi untuk menonton apa yang terjadi.

Semoga ini bisa membantu seseorang! Saya benar-benar menikmati menjelajahi berbagai cara menghasilkan yang dapat digunakan.

Adam W. McKinley
sumber
1
contoh kode Anda adalah contoh yang sangat baik tentang cara menggunakan hasil DI LUAR blok for foreach. Sebagian besar contoh menunjukkan pengembalian hasil dalam iterator. Sangat membantu karena saya baru saja akan mengajukan pertanyaan pada SO Bagaimana cara menggunakan hasil di luar iterator!
shelbypereira
Tidak pernah terpikir oleh saya untuk menggunakan yieldcara ini. Sepertinya cara yang elegan untuk meniru pola async / menunggu (yang saya asumsikan akan digunakan alih-alih yieldjika ini ditulis ulang hari ini). Pernahkah Anda menemukan bahwa penggunaan materi iklan yang kreatif yieldini menghasilkan (tanpa bermaksud kata-kata) yang semakin berkurang selama bertahun-tahun karena C # telah berkembang sejak Anda menjawab pertanyaan ini? Atau apakah Anda masih datang dengan kasus penggunaan cerdas modern seperti ini? Dan jika demikian, maukah Anda berbagi skenario menarik lainnya untuk kami?
Lopsided
27

Ini akan tampak seperti saran yang aneh, tetapi saya belajar bagaimana menggunakan yieldkata kunci dalam C # dengan membaca presentasi tentang generator dengan Python: http://www.dabeaz.com/generators/Generators.pdf David M. Beazley . Anda tidak perlu tahu banyak tentang Python untuk memahami presentasi - saya tidak. Saya merasa sangat membantu dalam menjelaskan tidak hanya bagaimana generator bekerja tetapi mengapa Anda harus peduli.

Robert Rossney
sumber
1
Presentasi memberikan gambaran sederhana. Rincian cara kerjanya dalam C # dibahas oleh Ray Chen di tautan di stackoverflow.com/a/39507/939250 Tautan pertama menjelaskan secara terperinci bahwa ada pengembalian kedua, tersirat di akhir metode pengembalian hasil.
Donal Lafferty
18

Pengembalian hasil bisa sangat kuat untuk algoritma di mana Anda harus beralih melalui jutaan objek. Pertimbangkan contoh berikut di mana Anda perlu menghitung kemungkinan perjalanan untuk berbagi perjalanan. Pertama, kami menghasilkan perjalanan yang mungkin:

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

Kemudian ulangi setiap perjalanan:

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

Jika Anda menggunakan Daftar alih-alih hasil, Anda perlu mengalokasikan 1 juta objek ke memori (~ 190mb) dan contoh sederhana ini akan membutuhkan ~ 1400ms untuk dijalankan. Namun, jika Anda menggunakan hasil, Anda tidak perlu meletakkan semua objek temp ini ke memori dan Anda akan mendapatkan kecepatan algoritma yang jauh lebih cepat: contoh ini hanya akan memakan waktu ~ 400 ms untuk berjalan tanpa konsumsi memori sama sekali.

Andzej Maciusovic
sumber
2
di bawah selimut apa hasil? Saya akan berpikir itu adalah daftar, maka bagaimana cara meningkatkan penggunaan memori?
bergulung
1
@rolls yieldbekerja di bawah penutup dengan menerapkan mesin negara secara internal. Inilah jawaban SO dengan 3 posting blog MSDN terperinci yang menjelaskan implementasinya dengan sangat rinci. Ditulis oleh Raymond Chen @ MSFT
Shiva
13

Dua potong kode ini benar-benar melakukan dua hal yang berbeda. Versi pertama akan menarik anggota saat Anda membutuhkannya. Versi kedua akan memuat semua hasil ke dalam memori sebelum Anda mulai melakukan apa pun dengannya.

Tidak ada jawaban benar atau salah untuk yang satu ini. Mana yang lebih disukai hanya tergantung pada situasinya. Misalnya, jika ada batas waktu untuk menyelesaikan kueri dan Anda perlu melakukan sesuatu yang semi-rumit dengan hasilnya, versi kedua bisa lebih disukai. Namun waspadalah terhadap hasil yang besar, terutama jika Anda menjalankan kode ini dalam mode 32-bit. Saya telah digigit oleh pengecualian OutOfMemory beberapa kali ketika melakukan metode ini.

Namun, hal utama yang perlu diingat adalah: perbedaannya dalam efisiensi. Dengan demikian, Anda mungkin harus memilih yang mana yang membuat kode Anda lebih mudah dan mengubahnya hanya setelah membuat profil.

Jason Baker
sumber
11

Yield memiliki dua kegunaan besar

Ini membantu untuk menyediakan iterasi khusus tanpa membuat koleksi temp. (memuat semua data dan perulangan)

Ini membantu untuk melakukan iterasi stateful. ( Streaming)

Di bawah ini adalah video sederhana yang saya buat dengan demonstrasi penuh untuk mendukung dua poin di atas

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

Shivprasad Koirala
sumber
10

Inilah yang dikatakan Chris Sells tentang pernyataan itu dalam Bahasa Pemrograman C # ;

Saya terkadang lupa bahwa imbal hasil tidak sama dengan pengembalian, karena kode setelah imbal hasil dapat dieksekusi. Misalnya, kode setelah pengembalian pertama di sini tidak pernah dapat dijalankan:

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

Sebaliknya, kode setelah pengembalian hasil pertama di sini dapat dieksekusi:

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

Ini sering menggigit saya dalam pernyataan if:

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

Dalam kasus ini, mengingat bahwa imbal hasil bukanlah "final" seperti pengembalian sangat membantu.

Teoman shipahi
sumber
untuk mengurangi ambiguitas, harap klarifikasi ketika Anda mengatakan bisa, apakah itu, akan atau mungkin? mungkinkah yang pertama kembali dan tidak mengeksekusi hasil kedua?
Johno Crawford
@JohnoCrawford pernyataan hasil kedua hanya akan dieksekusi jika nilai kedua / berikutnya dari IEnumerable disebutkan. Sangat mungkin itu tidak akan terjadi, misalnya F().Any()- ini akan kembali setelah mencoba untuk menghitung hasil pertama saja. Secara umum, Anda tidak boleh mengandalkan pada IEnumerable yielduntuk mengubah status program, karena itu mungkin tidak benar-benar dipicu
Zac Faragher
8

Dengan asumsi produk Anda kelas LINQ menggunakan hasil yang sama untuk penghitungan / iterasi, versi pertama lebih efisien karena hanya menghasilkan satu nilai setiap kali iterasi berakhir.

Contoh kedua adalah mengubah enumerator / iterator ke daftar dengan metode ToList (). Ini berarti secara manual beralih ke semua item dalam enumerator dan kemudian mengembalikan daftar datar.

Soviut
sumber
8

Ini agak tidak penting, tetapi karena pertanyaannya ditandai praktik terbaik, saya akan melanjutkan dan memasukkan dua sen saya. Untuk hal semacam ini saya lebih suka membuatnya menjadi properti:

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

            return products;
        }
    }
}

Tentu, ini sedikit lebih boiler-plate, tetapi kode yang menggunakan ini akan terlihat jauh lebih bersih:

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

vs.

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

Catatan: Saya tidak akan melakukan ini untuk metode apa pun yang mungkin memerlukan waktu untuk melakukan pekerjaan mereka.

Mark A. Nicolosi
sumber
7

Dan bagaimana dengan ini?

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

        return products.ToList();
    }
}

Saya kira ini jauh lebih bersih. Saya tidak punya VS2008 di tangan untuk memeriksa, meskipun. Dalam kasus apa pun, jika Produk mengimplementasikan IEnumerable (seperti yang terlihat - digunakan dalam pernyataan foreach), saya akan mengembalikannya secara langsung.

petr k.
sumber
2
Harap edit OP untuk memasukkan lebih banyak info daripada mengirim jawaban.
Brian Rasmussen
Nah, Anda harus memberi tahu saya apa yang OP sebenarnya perjuangkan :-) Terima kasih
petr k.
Posting Asli, saya kira. Saya tidak dapat mengedit posting, jadi ini sepertinya jalan yang harus saya tempuh.
petr k.
5

Saya akan menggunakan versi 2 kode dalam kasus ini. Karena Anda memiliki daftar lengkap produk yang tersedia dan itulah yang diharapkan oleh "konsumen" dari pemanggilan metode ini, Anda harus mengirim informasi lengkap kembali ke pemanggil.

Jika penelepon dari metode ini memerlukan "satu" informasi pada satu waktu dan konsumsi informasi berikutnya adalah berdasarkan permintaan, maka akan bermanfaat untuk menggunakan pengembalian hasil yang akan memastikan perintah eksekusi akan dikembalikan ke penelepon ketika satu unit informasi tersedia.

Beberapa contoh di mana seseorang dapat menggunakan pengembalian hasil adalah:

  1. Perhitungan kompleks, langkah demi langkah di mana penelepon menunggu data langkah demi langkah
  2. Paging di GUI - di mana pengguna mungkin tidak pernah mencapai halaman terakhir dan hanya sub-set informasi yang diperlukan untuk diungkapkan pada halaman saat ini

Untuk menjawab pertanyaan Anda, saya akan menggunakan versi 2.

IntelligentBinary
sumber
3

Kembalikan daftar secara langsung. Manfaat:

  • Lebih jelas
  • Daftar ini dapat digunakan kembali. (iterator tidak) sebenarnya tidak benar, Terima kasih Jon

Anda harus menggunakan iterator (hasil) dari saat Anda berpikir Anda mungkin tidak perlu mengulangi sampai ke akhir daftar, atau ketika itu tidak memiliki akhir. Misalnya, panggilan klien akan mencari produk pertama yang memenuhi beberapa predikat, Anda dapat mempertimbangkan menggunakan iterator, meskipun itu adalah contoh yang dibuat-buat, dan mungkin ada cara yang lebih baik untuk mencapainya. Pada dasarnya, jika Anda tahu sebelumnya bahwa seluruh daftar perlu dihitung, lakukan saja di depan. Jika Anda berpikir itu tidak akan terjadi, maka pertimbangkan untuk menggunakan versi iterator.

rekursif
sumber
Jangan lupa bahwa ia kembali di IEnumerable <T>, bukan IEnumerator <T> - Anda dapat memanggil GetEnumerator lagi.
Jon Skeet
Bahkan jika Anda tahu sebelumnya, seluruh daftar perlu dihitung, mungkin masih bermanfaat untuk menggunakan pengembalian hasil. Salah satu contohnya adalah ketika koleksi berisi ratusan ribu item.
Val
1

Frasa kunci hasil pengembalian digunakan untuk memelihara mesin keadaan untuk koleksi tertentu. Di mana pun CLR melihat frasa kunci hasil pengembalian yang digunakan, CLR mengimplementasikan pola Enumerator ke potongan kode itu. Jenis implementasi ini membantu pengembang dari semua jenis pipa leding yang harus kami lakukan jika tidak ada kata kunci.

Misalkan jika pengembang memfilter beberapa koleksi, iterasi koleksi tersebut dan kemudian ekstrak objek-objek itu dalam beberapa koleksi baru. Pipa jenis ini cukup monoton.

Lebih lanjut tentang kata kunci di sini di artikel ini .

Vikram
sumber
-4

Penggunaan hasil mirip dengan kata kunci kembali , kecuali bahwa itu akan mengembalikan pembangkit . Dan objek generator hanya akan melintas satu kali .

hasil memiliki dua manfaat:

  1. Anda tidak perlu membaca nilai-nilai ini dua kali;
  2. Anda bisa mendapatkan banyak simpul anak tetapi tidak harus memasukkan semuanya ke dalam memori.

Ada penjelasan lain yang jelas mungkin bisa membantu Anda.

123
sumber