"Verim" anahtar sözcüğünün C # [kapalı] olarak pratik kullanımı


76

Neredeyse 4 yıllık deneyimden sonra, verim anahtar sözcüğünün kullanıldığı bir kod görmedim . Biri bana bu anahtar kelimenin pratik bir kullanımını (açıklama boyunca) gösterebilir mi ve eğer öyleyse, yapabileceklerini doldurmanın başka yolları da yok mu?


9
LINQ'un tümü (veya en azından çoğu) verim kullanılarak uygulanır. Ayrıca Unity3D çerçevesi bunun için iyi bir kullanım alanı buldu - işlevleri (verim ifadelerinde) duraklatmak ve daha sonra IEnumerable'daki durumu kullanarak sürdürmek için kullanılır.
Dani,

2
Bu StackOverflow'a taşınmamalı mı?
Danny Varod

4
@Danny - Yığın Taşması için uygun değil, çünkü soru belirli bir sorunu çözmek istemiyor, ancak yieldgenel olarak neyin kullanılabileceğini soruyor .
ChrisF

9
Gerçek mi? Ben tek bir uygulama düşünemiyorum değil kullandı.
Aarona

Yanıtlar:


107

verim

yieldAnahtar kelime etkin bir çok daha verimli olabilir toplama öğeleri üzerinde tembel numaralandırma oluşturur. Örneğin, foreachdöngünüz 1 milyon öğeden sadece ilk 5 öğesinde yineleniyorsa yield, hepsi geri döner ve ilk önce 1 milyon ürün koleksiyonu oluşturmadınız. Aynı şekilde , aynı verimliliği elde etmek için kendi programlama senaryolarınızda dönüş değerleri yieldile kullanmak isteyeceksiniz IEnumerable<T>.

Belirli bir senaryoda kazanılmış verimlilik örneği

Bir yineleyici yöntem değil, büyük bir koleksiyonun potansiyel verimsiz kullanımı,
(Orta düzey toplama çok sayıda öğeye sahip olarak oluşturulmuştur)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

Yineleyici versiyonu, verimli
(Hiçbir ara toplama yapılmamıştır)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

Bazı programlama senaryolarını basitleştirin

Başka bir durumda, bazı sıralama türlerini ve listelerin birleştirilmesini programlamayı kolaylaştırır, çünkü yieldonları bir ara koleksiyona ayırıp yerine takas etmek yerine sadece istediğiniz sıraya geri yerleştirirsiniz. Böyle birçok senaryo var.

Sadece bir örnek iki listenin birleştirilmesidir:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

Bu yöntem, bir bitişik öğe listesini geri verir, etkili bir şekilde ara toplama gerektirmeden birleştirme sağlar.

Daha fazla bilgi

yieldAnahtar, sadece (bir dönüş türüne sahip bir yineleyici yöntem bağlamında kullanılabilir IEnumerable, IEnumerator, IEnumerable<T>ya da IEnumerator<T>.) İle özel bir ilişki vardır foreach. Yineleyiciler özel yöntemlerdir. MSDN verim dokümantasyon ve yineleyici dokümantasyon ilginç bilgiler ve kavramların açıklanması bolca içerir. İle ilişkilendirmek için emin olun anahtar kelimenin Yineleyicilerin anlayışınızı tamamlamak için de bu konuda okuyarak.foreach

Yineleyicilerin verimliliklerini nasıl elde ettiklerini öğrenmek için sır, C # derleyicisi tarafından oluşturulan IL kodundadır. Bir yineleyici yöntem için oluşturulan IL, normal (yineleyici olmayan) bir yöntem için oluşturulandan büyük ölçüde farklıdır. Bu makale (Verim Gerçekten Anahtar Kelime Üretir mi?) Bu türden bir içgörü sağlar.


2
Bunlar, (muhtemelen uzun) bir sekans alan ve haritalamanın bire bir olmadığı başka bir tane üreten algoritmalar için kullanışlıdır. Bunun bir örneği çokgen kırpma; herhangi bir belirli kenar, kırpıldıktan sonra birçok kenar hatta hiç kenar oluşturamaz. Yineleyiciler bunu ifade etmeyi çok daha kolay hale getirir ve elde etmek onları yazmanın en iyi yollarından biridir.
Donal Fellows,

+1 Yazdığım kadarıyla çok daha iyi cevap. Şimdi daha iyi performans için verimin de iyi olduğunu öğrendim.
Jan_V

3
Bir zamanlar, ikili ağ protokolü için paketler üretmek için verimi kullandım. C # en doğal seçim gibi görünüyordu.
György Andrasek

4
Does not database.Customers.Count()böylece her öğeye geçmek daha verimli kod gerektiren tüm müşteriler numaralandırma numaralandırmak?
Stephen

5
Bana anal de, ama bu birleştirme değil, birleştirme. (Ve linq zaten bir Concat yöntemine sahiptir.)
OldFart

4

Bir süre önce pratik bir örneğim vardı, şöyle bir durum olduğunu varsayalım:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

Button nesnesi koleksiyondaki kendi konumunu bilmiyor. Aynı sınırlama, Dictionary<T>diğer koleksiyon türleri için de geçerlidir .

yieldAnahtar kelimeyi kullanarak çözümüm :

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

Ve bunu böyle kullanıyorum:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

forDizini bulmak için koleksiyonum üzerinde bir döngü yapmak zorunda değilim . Butonumun bir kimliği var ve bu da indeks girişi olarak kullanılıyor IndexerList<T>, böylece herhangi bir gereksiz ID'den veya indekslerden kaçınmıyorsunuz - işte hoşuma gidiyor! Dizin / Id isteğe bağlı bir sayı olabilir.


2

Pratik bir örnek burada bulunabilir:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

Standart kod üzerinden verim kullanmanın bir çok avantajı vardır:

  • Eğer yineleyici bir liste oluşturmak için kullanılıyorsa, geri dönüşü verebilirsiniz ve arayan kişi bunun bir listeyle sonuçlanıp sonuçlanmayacağına karar verebilir .
  • Arayan kişi, yinelemede yaptıklarınızın kapsamı dışında olan bir nedenle yinelemeyi iptal etmeye karar verebilir.
  • Kod biraz daha kısa.

Ancak, Jan_V'nin dediği gibi (sadece birkaç saniye beni yendi :-) o olmadan yaşayabilirsin çünkü dahili olarak derleyici her iki durumda da hemen hemen aynı kod üretecek.


1

İşte bir örnek:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

Sınıf, çalışma haftasına göre tarih hesaplamaları yapar. Sınıfın bir örneğini Bob'un hafta içi her gün saat 9: 30-17: 30 arasında çalıştığını söyleyebilirim. Bu bilgiyle, AscendingShifts () işlevi verilen tarihler arasında çalışma vardiyası nesnelerini oluşturur. Bob'un 1 Ocak ile 1 Şubat arasındaki tüm çalışma vardiyalarını listelemek için aşağıdaki gibi kullanabilirsiniz:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

Sınıf gerçekten bir koleksiyon üzerinde yinelenmez. Bununla birlikte, iki tarih arasındaki kaymalar bir koleksiyon olarak düşünülebilir. yieldOperatör mümkün koleksiyon kendisi yaratmadan bu hayal topluluğu üzerinde yineleme kolaylaştırır.


1

commandSQL komut metnini, komut türünü ayarladığınız ve 'komut parametreleri' şeklinde bir IEnumerable döndüren bir sınıfa sahip küçük bir db veri katmanına sahibim .

Temel olarak fikir, SqlCommandözellikleri ve parametreleri her zaman elle doldurmak yerine CLR komutlarını yazmaktır .

Yani şuna benzeyen bir işlev var:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

Bu sınıfı miras alan sınıfın commandözellikleri Ageve Name.

Sonra commandözelliklerini doldurmuş bir nesneyi yenileyebilir ve onu dbkomut çağrısı yapan bir arabirime geçirebilirsiniz .

Sonuç olarak, SQL komutlarıyla çalışmayı ve yazılmaya devam etmesini gerçekten kolaylaştırır.


1

Birleşme davası kabul edilmiş cevapta kapsanmış olsa da, size birleştirme params uzatma yöntemi ™ göstereyim:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

Bunu bir ağ protokolünün paketlerini oluşturmak için kullanıyorum:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

MarkChecksumYöntem, örneğin, bu gibi görünüyor. Ve bir yieldde var:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

Ancak, bir numaralandırma yönteminde ayrı bir numaralandırma işlemini tetiklerken Sum () gibi toplu yöntemler kullanırken dikkatli olun.


1

Elastik Arama .NET örnek repo, bir yield returnkoleksiyonu belirtilen boyutta birden fazla koleksiyona bölmek için kullanmanın harika bir örneğini sunar :

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Jan_V'nin cevabını genişleterek, bununla ilgili gerçek dünyadan bir olaya çarptım:

FindFirstFile / FindNextFile sürümünün Kernel32 sürümlerini kullanmam gerekiyordu. İlk aramadan bir tanıtıcı alıp sonraki tüm aramalara beslersiniz. Bunu bir numaralandırıcıya sarın ve doğrudan foreach ile kullanabileceğiniz bir şey elde edin.

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.