'Verim getirisinin' doğru kullanımı


903

Verim anahtar kelime onlardan biri anahtar kelimeler beni şaşırtmak devam ediyor C # ve eğer bunu doğru kullanıyorum ben emin olmamıştım.

Aşağıdaki iki kod parçasından hangisi tercih edilir ve neden?

Sürüm 1: Verim getirisini kullanma

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

Sürüm 2: Listeyi iade et

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

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

38
yieldbağlıdır IEnumerable<T>ve onun tür. Bir şekilde tembel değerlendirmede
Jaider

İşte benzer soruya büyük bir cevap. stackoverflow.com/questions/15381708/…
Sanjeev Rai

1
İşte iyi bir kullanım örneği: stackoverflow.com/questions/3392612/…
ValGe

6
yield returnEğer sonuçları üzerinden yinelenen kod GetAllProducts()kullanıcı erken işleme iptal etmek için bir şans sağlarsa kullanmak için iyi bir durum görüyorum .
JMD

2
Bu konuyu gerçekten yararlı buldum: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Yanıtlar:


806

Listedeki bir sonraki öğeyi (hatta bir sonraki öğe grubunu) hesaplarken getiri-getiri kullanma eğilimindeyim.

Sürüm 2'nizi kullanarak, geri dönmeden önce tam listeye sahip olmalısınız. Getiri-dönüş kullanarak, gerçekten geri dönmeden önce bir sonraki öğeye sahip olmanız gerekir.

Diğer şeylerin yanı sıra, bu, karmaşık hesaplamaların hesaplama maliyetinin daha büyük bir zaman dilimine yayılmasına yardımcı olur. Örneğin, liste bir GUI'ye bağlanırsa ve kullanıcı asla son sayfaya gitmezse, listedeki son öğeleri asla hesaplamazsınız.

Verim-getirinin tercih edilebilir olduğu başka bir durum, IEnumerable'ın sonsuz bir seti temsil edip etmediğidir. Asal Sayılar listesini veya sonsuz rasgele sayı listesini düşünün. Asla IEnumerable'ın tamamını bir kerede geri veremezsiniz, bu nedenle listeyi aşamalı olarak döndürmek için verim-dönüşü kullanın.

Özel örneğinizde, ürünlerin tam listesine sahipsiniz, bu yüzden Sürüm 2'yi kullanırım.


31
Soru 3'teki örneğinizde iki fayda sağladığını söyleyebilirim. 1) Hesaplama maliyetini yayar (bazen bir fayda, bazen değil) 2) Birçok kullanım durumunda süresiz olarak hesaplamayı geçici olarak önleyebilir. Orta düzeyde tutmasının potansiyel dezavantajından bahsetmiyorsunuz. Önemli miktarda ara durumunuz varsa (yinelenen eliminasyon için bir HashSet deyin), o zaman verim kullanımı bellek ayak izinizi şişirebilir.
Kennet Belenky

8
Ayrıca, her bir eleman çok büyükse, ancak sadece sıralı olarak erişilmesi gerekiyorsa, bir verim daha iyidir.
Kennet Belenky

2
Ve son olarak ... asenkron kodu çok serileştirilmiş biçimde yazmak için biraz sakat ama bazen etkili bir teknik var.
Kennet Belenky

12
İlginç olabilecek başka bir örnek, oldukça büyük CSV dosyalarını okurken. Her bir öğeyi okumak istiyorsunuz ama bağımlılığınızı da çıkarmak istiyorsunuz. IEnumerable <> döndüren verim, her satırı döndürmenize ve her satırı ayrı ayrı işlemenize olanak tanır. 10 Mb'lık bir dosyayı belleğe okumaya gerek yok. Her seferinde sadece bir satır.
Maxime Rouiller

1
Yield returnkendi özel yineleyici sınıfınızı yazmak için kısa bir yol gibi görünüyor (IEnumerator uygulayın). Dolayısıyla, belirtilen faydalar özel yineleyici sınıfları için de geçerlidir. Her neyse, her iki yapı da ara durumu korur. En basit haliyle, mevcut nesneye bir referans tutmakla ilgilidir.
J. Ouwehand

641

Geçici bir listeyi doldurmak, tüm videoyu indirmeye benzerken, kullanmak yieldvideonun akışına benzer.


180
Bu cevabın teknik bir cevap olmadığını çok iyi biliyorum ama verim ve video akışı arasındaki benzerliğin verim anahtar kelimesini anlarken iyi bir örnek olduğuna inanıyorum. Bu konuda teknik olan her şey zaten söylendi, bu yüzden "başka bir deyişle" açıklamaya çalıştım. Fikirlerinizi teknik olmayan terimlerle açıklayamayacağınızı söyleyen bir topluluk kuralı var mı?
anar khalilov

13
Sana kimin oy vermediğinden ya da neden oy verdiğinden emin değilim (yorum yapmasını isterdim), ancak teknik olmayan bir adaydan bir şekilde tarif ettiğini düşünüyorum.
senfo

22
Hala kavramı kavramak ve bu onu daha fazla odaklanmaya, güzel benzetmeye yardımcı oldu.
Tony

11
Bu cevabı beğendim, ama soruya cevap vermiyor.
ANeves

73

Ne zaman kullanmanız gerektiğini anlamak için kavramsal bir örnek olarak yield, yöntemin ConsumeLoop(), iade edilen / verilen ürünleri işlediğini varsayalım ProduceList():

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

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

Olmadan yield, ProduceList()geri dönmeden önce listeyi tamamlamanız gerektiğinden , çağrı uzun sürebilir:

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

Kullanarak yield, yeniden düzenlenir, bir tür "paralel" çalışma:

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

Ve son olarak, daha önce önerdiği gibi, zaten zaten tamamlanmış listeye sahip olduğunuz için Sürüm 2'yi kullanmalısınız.


30

Bunun eski bir soru olduğunu biliyorum, ancak verim anahtar kelimesinin yaratıcı bir şekilde nasıl kullanılabileceğine dair bir örnek sunmak istiyorum. Ben var gerçekten bu tekniğin yararlanmıştır. Umarım bu, bu soruyu tökezleyen herkese yardımcı olacaktır.

Not: Ürün anahtar kelimesini yalnızca koleksiyon oluşturmanın başka bir yolu olarak düşünmeyin. Verim gücünün büyük bir kısmı , çağıran kod bir sonraki değer üzerinde yinelenene kadar, yönteminizde veya mülkünüzde yürütmenin duraklatılmış olmasından kaynaklanır . İşte benim örnek:

Verim anahtar sözcüğünü (Rob Eisenburg'un Caliburn.Micro coroutines uygulamasının yanında) kullanmak, şöyle bir web hizmetine eşzamansız bir çağrı ifade etmeme izin verir:

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

Bunun yapacağı şey BusyIndicator'ımı açmak, web hizmetimdeki Login yöntemini çağırmak, IsLoggedIn bayrağımı dönüş değerine ayarlamak ve sonra BusyIndicator'ı tekrar kapatmak.

İşte böyle çalışır: IResult bir Execute yöntemi ve bir Completed olayı vardır. Caliburn.Micro, IEnumerator öğesini HandleButtonClick () çağrısından alır ve bir Coroutine.BeginExecute yöntemine iletir. BeginExecute yöntemi, IResults aracılığıyla yinelemeyi başlatır. İlk IResult döndürüldüğünde, işlem HandleButtonClick () içinde duraklatılır ve BeginExecute (), Completed olayına bir olay işleyicisi ekler ve Execute () öğesini çağırır. IResult.Execute (), eşzamanlı veya eşzamansız bir görevi gerçekleştirebilir ve tamamlandığında Tamamlandı olayını tetikler.

LoginResult şöyle görünür:

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

Böyle bir şey ayarlamak ve neler olup bittiğini izlemek için yürütme adımlarını atmak yardımcı olabilir.

Umarım bu birisine yardım eder! Verimin kullanılabileceği farklı yolları keşfetmekten gerçekten keyif aldım.


1
kod örneğiniz, for for veya foreach bloğunun OUTSIDE veriminin nasıl kullanılacağına dair mükemmel bir örnektir. Çoğu örnek bir yineleyici içinde verim geri dönüşünü gösterir. Çok yararlı ben SO hakkında bir soru sormak üzereydi gibi Bir yineleyici dışında verim nasıl kullanılır!
shelbypereira

yieldBu şekilde kullanmak hiç aklıma gelmedi . Async / await desenini taklit etmek için zarif bir yol gibi görünüyor (ki yieldbu bugün yeniden yazılmışsa yerine kullanılacak ). Bu yaratıcı kullanımın, yieldbu soruyu cevapladığınızdan bu yana C # geliştikçe yıllar içinde azalan getiri sağladığını buldunuz mu? Yoksa hâlâ bunun gibi modern ve akıllı kullanım örnekleri mi arıyorsunuz? Ve eğer öyleyse, bizim için ilginç bir senaryo paylaşmak ister misiniz?
Lopsided

27

Bu tuhaf bir öneri gibi görünecek, ancak yieldPython: David M. Beazley'in http://www.dabeaz.com/generators/Generators.pdf adresindeki jeneratörler üzerine bir sunum okuyarak anahtar kelimeyi C # 'da kullanmayı öğrendim . Sunumu anlamak için fazla Python bilmenize gerek yok - ben bilmiyordum. Sadece jeneratörlerin nasıl çalıştığını değil, neden önemsemeniz gerektiğini açıklamakta çok yararlı buldum.


1
Sunum basit bir genel bakış sunar. C # 'da nasıl çalıştığına dair ayrıntılar Ray Chen tarafından stackoverflow.com/a/39507/939250 adresindeki bağlantılarda tartışılmıştır . İlk bağlantı, getiri yöntemlerinin sonunda ikinci, örtük bir geri dönüş olduğunu ayrıntılı olarak açıklamaktadır.
Donal Lafferty

18

Milyonlarca nesneyi tekrarlamanız gereken algoritmalar için verim geri dönüşü çok güçlü olabilir. Sürüş paylaşımı için olası yolculukları hesaplamanız gereken aşağıdaki örneği düşünün. İlk olarak olası yolculukları üretiyoruz:

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

Sonra her yolculukta tekrarlayın:

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

Verim yerine List'i kullanırsanız, belleğe 1 milyon nesne ayırmanız gerekir (~ 190mb) ve bu basit örneğin çalışması ~ 1400ms sürer. Ancak, verimi kullanırsanız, tüm bu geçici nesneleri belleğe koymanız gerekmez ve önemli ölçüde daha hızlı algoritma hızı elde edersiniz: bu örnek, bellek tüketimi olmadan çalıştırmak için sadece ~ 400 ms sürecektir.


2
kapakların altında verim nedir? Bir liste olduğunu düşünürdüm, dolayısıyla bellek kullanımını nasıl geliştirirdi?
rulolar

1
@rolls yield, bir devlet makinesini dahili olarak uygulayarak kapakların altında çalışır. İşte uygulamayı ayrıntılı olarak açıklayan 3 ayrıntılı MSDN blog yazısı içeren bir SO yanıtı . Yazan: Raymond Chen @ MSFT
Shiva

13

İki kod parçası gerçekten iki farklı şey yapıyor. İlk sürüm, üyeleri ihtiyaç duyduğunuz şekilde çeker. İkinci sürüm, herhangi bir şey yapmaya başlamadan önce tüm sonuçları belleğe yükleyecektir .

Bunun doğru ya da yanlış cevabı yok. Hangisinin tercih edileceği sadece duruma bağlıdır. Örneğin, sorgunuzu tamamlamak zorunda olduğunuz zaman sınırı varsa ve sonuçlarla yarı karmaşık bir şey yapmanız gerekiyorsa, ikinci sürüm tercih edilebilir. Ancak, özellikle bu kodu 32 bit modunda çalıştırıyorsanız, büyük sonuç kümelerine dikkat edin. Bu yöntemi yaparken birkaç kez OutOfMemory istisnaları tarafından ısırıldım.

Akılda tutulması gereken anahtar şey şudur: farklılıklar verimliliktedir. Bu nedenle, muhtemelen kodunuzu basitleştirenle gitmeli ve yalnızca profil oluşturduktan sonra değiştirmelisiniz.


11

Getiri iki harika kullanıma sahiptir

Geçici koleksiyonlar oluşturmadan özel yineleme sağlamaya yardımcı olur. (tüm verileri yükleme ve döngü oluşturma)

Durumsal yineleme yapmaya yardımcı olur. ( yayın Akışı)

Aşağıda, yukarıdaki iki noktayı desteklemek için tam gösterim ile oluşturduğum basit bir video var

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


10

Bu nedir Chris Satıyor bu ifadeleri anlatır Dili Programlama C # ;

Bazen getiri getirisinin getiri ile aynı olmadığını, bir getiri iadesinden sonraki kodun çalıştırılabileceğini unutuyorum. Örneğin, buraya ilk dönüşten sonraki kod hiçbir zaman yürütülemez:

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

Buna karşılık, burada ilk verim iadesinden sonraki kod yürütülebilir:

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

Bu genellikle bir if ifadesinde ısırır:

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

Bu durumlarda, getiri getirisinin getiri gibi “nihai” olmadığını hatırlamak yardımcı olur.


belirsizliği azaltmak için lütfen can, is, will veya might dediğinizde açıklayınız. birincisinin geri dönmesi ve ikinci verimi uygulamaması mümkün olabilir mi?
Johno Crawford

@JohnoCrawford ikinci verim ifadesi yalnızca IEnumerable'ın ikinci / sonraki değeri numaralandırıldığında çalıştırılır. Alışılmaması tamamen mümkündür, örneğin F().Any()- bu sadece ilk sonucu numaralandırmaya çalıştıktan sonra geri dönecektir. Genel olarak, IEnumerable yieldprogram durumunu değiştirmek için bir güvenmemelisiniz , çünkü aslında tetiklenmeyebilir
Zac Faragher

8

Ürünlerinizin LINQ sınıfı, numaralandırma / yineleme için benzer bir verim kullandığını varsayarsak, ilk sürüm daha verimlidir çünkü her yinelemede bir değer verir.

İkinci örnek, numaralandırıcı / yineleyiciyi ToList () yöntemiyle listeye dönüştürmektir. Bu, numaralandırıcıdaki tüm öğeleri manuel olarak yinelediği ve ardından düz bir liste döndürdüğü anlamına gelir.


8

Bu, konunun yanı sıra, ama soru en iyi uygulamalar olarak etiketlendiğinden, iki sentimi atarım. Bu tür bir şey için büyük ölçüde bir mülk haline getirmeyi tercih ederim:

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

            return products;
        }
    }
}

Tabii, biraz daha kazan plakası, ancak bunu kullanan kod daha temiz görünecek:

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

vs

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

Not: İşlerini yapmak biraz zaman alabilecek herhangi bir yöntem için bunu yapmam.


7

Peki ya bu?

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

        return products.ToList();
    }
}

Sanırım bu çok daha temiz. Yine de kontrol etmek için elimde VS2008 yok. Her durumda, Ürünler IEnumerable uygularsa (göründüğü gibi - bir foreach ifadesinde kullanılır), doğrudan iade ederim.


2
Lütfen yanıt göndermek yerine daha fazla bilgi eklemek için OP'yi düzenleyin.
Brian Rasmussen

Bana OP'nin tam olarak neyi temsil ettiğini söylemek zorundasın :-) Teşekkürler
petr k.

Orijinal Gönderi, sanırım. Yayınları düzenleyemiyorum, bu yüzden bu yol gibi görünüyordu.
petr k.

5

Bu durumda kodun 2. sürümünü kullanmış olurdum. Mevcut ürünlerin tam listesine sahip olduğunuzdan ve bu yöntem çağrısının "tüketici" tarafından beklendiği gibi olduğundan, tüm bilgileri arayan kişiye geri göndermek gerekir.

Bu yöntemin arayan bir kerede "bir" bilgi gerektiriyorsa ve bir sonraki bilgilerin tüketimi talep üzerine ise, yürütme komutunun arayan kişiye geri gönderilmesini sağlayacak verim iadesi kullanmak faydalı olacaktır. bir bilgi birimi mevcuttur.

Verim getirisini kullanabileceğiniz bazı örnekler:

  1. Arayanın aynı anda bir adımın verilerini beklediği karmaşık, adım adım hesaplama
  2. GUI'de sayfalama - kullanıcının hiçbir zaman son sayfaya ulaşamayacağı ve geçerli sayfada yalnızca bilgi alt kümesinin açıklanması gerekir

Sorularınızı cevaplamak için sürüm 2'yi kullanırdım.


3

Listeyi doğrudan döndür. Yararları:

  • Daha açık
  • Liste yeniden kullanılabilir. (yineleyici değil) aslında doğru değil, teşekkürler Jon

Yineleyiciyi (verimi), listenin sonuna kadar yinelemeniz gerekmeyeceğini düşündüğünüz zamandan veya sonu olmadığında kullanmalısınız. Örneğin, müşteri çağrısı, bazı yüklemleri tatmin eden ilk ürünü arayacak, yineleyici bir örnek olmasına rağmen yineleyiciyi kullanmayı düşünebilirsiniz ve muhtemelen bunu başarmanın daha iyi yolları vardır. Temel olarak, tüm listenin hesaplanması gerektiğini önceden biliyorsanız, bunu önceden yapın. Bunu yapmayacağını düşünüyorsanız, yineleyici sürümünü kullanmayı düşünün.


IEnumerator <T> yerine IEnumerable <T> 'de geri döndüğünü unutmayın - GetEnumerator'ü tekrar arayabilirsiniz.
Jon Skeet

Önceden tüm listenin hesaplanması gerektiğini bilseniz bile, getiri getirisini kullanmak yine de yararlı olabilir. Bir örnek, koleksiyonun yüz binlerce öğe içermesidir.
Val

1

Verim dönüşü anahtar kelimesi, belirli bir toplama için durum makinesini korumak için kullanılır. CLR'nin kullanılan geri dönüş anahtar kelimesini gördüğü her yerde, CLR bu kod parçasına bir Enumerator modeli uygular. Bu tür bir uygulama, geliştiriciye, aksi takdirde anahtar kelime olmadan yapmamız gereken her türlü sıhhi tesisattan yardımcı olur.

Geliştiricinin bir koleksiyonu filtrelediğini, koleksiyondan yinelediğini ve ardından bu nesneleri yeni bir koleksiyonda ayıkladığını varsayalım. Bu tür bir tesisat oldukça monotondur.

Bu makaledeki anahtar kelime hakkında daha fazla bilgi .


-4

Getiri kullanımı, bir jeneratör döndürmesi dışında return anahtar sözcüğüne benzer . Ve jeneratör nesnesi sadece bir kez hareket edecektir .

verimin iki faydası vardır:

  1. Bu değerleri iki kez okumanıza gerek yoktur;
  2. Çok sayıda alt düğüm alabilirsiniz, ancak hepsini hafızaya almak zorunda değilsiniz.

Size yardımcı olabilecek başka bir açık açıklama daha var.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.