การใช้ประโยชน์จากคำหลัก "ผลตอบแทน" ใน C # [ปิด]


76

หลังจากประสบการณ์เกือบ 4 ปีฉันไม่ได้เห็นรหัสที่ใช้คำหลักผลตอบแทน ใครสามารถแสดงให้ฉันเห็นการใช้งานจริง (ตามคำอธิบาย) ของคำหลักนี้และถ้าเป็นเช่นนั้นจะไม่มีวิธีอื่น ๆ ที่จะเติมเต็มสิ่งที่มันสามารถทำได้หรือไม่?


9
LINQ ทั้งหมด (หรืออย่างน้อยที่สุด) ทั้งหมดจะถูกใช้งานโดยใช้อัตราผลตอบแทน นอกจากนี้กรอบ Unity3D ยังพบการใช้งานที่ดีสำหรับมัน - มันถูกใช้เพื่อหยุดการทำงานของฟังก์ชั่น (ในคำสั่งให้ผลตอบแทน) และกลับมาทำงานในภายหลังโดยใช้สถานะใน IEnumerable
Dani

2
ไม่ควรย้ายสิ่งนี้ไปที่ StackOverflow หรือ
Danny Varod

4
@Danny - ไม่เหมาะสำหรับ Stack Overflow เนื่องจากคำถามไม่ได้ขอให้แก้ปัญหาเฉพาะ แต่ถามเกี่ยวกับสิ่งที่yieldสามารถนำมาใช้โดยทั่วไป
ChrisF

9
จริงเหรอ ฉันไม่สามารถนึกถึงแอปเดียวที่ฉันไม่ได้ใช้
Aaronaught

คำตอบ:


107

อย่างมีประสิทธิภาพ

yieldคำหลักได้อย่างมีประสิทธิภาพสร้างการแจงนับขี้เกียจกว่ารายการคอลเลกชันที่สามารถมีประสิทธิภาพมากขึ้น ตัวอย่างเช่นหากforeachลูปของคุณวนซ้ำเพียง 5 รายการแรกจาก 1 ล้านรายการนั่นคือการyieldส่งคืนทั้งหมดและคุณไม่ได้สร้างคอลเลกชัน 1 ล้านรายการภายในครั้งแรก ในทำนองเดียวกันคุณจะต้องการใช้yieldกับIEnumerable<T>ค่าส่งคืนในสถานการณ์การเขียนโปรแกรมของคุณเองเพื่อให้ได้ประสิทธิภาพเดียวกัน

ตัวอย่างของประสิทธิภาพที่ได้รับในบางสถานการณ์

ไม่ใช่วิธีการวนซ้ำการใช้คอลเลกชันขนาดใหญ่ที่ไม่มีประสิทธิภาพอาจเกิดขึ้น
(คอลเลกชันระดับกลางถูกสร้างขึ้นโดยมีรายการจำนวนมาก)

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

รุ่น Iterator ที่มีประสิทธิภาพ
(ไม่มีการรวบรวมกลาง)

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

ทำให้สถานการณ์การเขียนโปรแกรมง่ายขึ้น

ในอีกกรณีหนึ่งมันทำให้การเรียงลำดับและการรวมรายการบางรายการง่ายขึ้นเนื่องจากคุณเพิ่งyieldกลับรายการในลำดับที่ต้องการแทนที่จะเรียงลำดับลงในคอลเล็กชันกลางและสลับไปที่นั่น มีสถานการณ์ดังกล่าวมากมาย

ตัวอย่างเดียวคือการรวมสองรายการ:

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

วิธีนี้ให้ผลตอบแทนรายการที่อยู่ติดกันหนึ่งรายการผสานอย่างมีประสิทธิภาพโดยไม่ต้องมีการรวบรวมกลาง

ข้อมูลเพิ่มเติม

yieldคำหลักที่สามารถนำมาใช้เฉพาะในบริบทของวิธี iterator (ให้มีประเภทการกลับมาของIEnumerable, IEnumerator, IEnumerable<T>หรือIEnumerator<T>.) foreachและมีความสัมพันธ์พิเศษกับ การวนซ้ำเป็นวิธีพิเศษ เอกสารผลผลิต MSDNและiterator เอกสารมีจำนวนมากของข้อมูลที่น่าสนใจและคำอธิบายแนวคิด ให้แน่ใจว่าจะมีความสัมพันธ์กับคำหลักโดยการอ่านเกี่ยวกับเรื่องนี้มากเกินไปที่จะเสริมความเข้าใจของ iteratorsforeach

เพื่อเรียนรู้ว่าตัววนซ้ำบรรลุประสิทธิภาพได้อย่างไรความลับอยู่ในโค้ด IL ที่สร้างโดยคอมไพเลอร์ C # IL ที่สร้างขึ้นสำหรับวิธีการวนซ้ำแตกต่างอย่างมากจากที่สร้างขึ้นสำหรับวิธีการปกติ (ไม่ใช่ตัววนซ้ำ) บทความนี้ (คำหลักที่ให้ผลตอบแทนจริงๆสร้างขึ้นจริงๆคืออะไร)ให้ข้อมูลเชิงลึกประเภทนั้น


2
มีประโยชน์อย่างยิ่งสำหรับอัลกอริทึมที่ใช้ลำดับ (อาจเป็นไปได้นาน) และสร้างอีกอันที่การทำแผนที่ไม่ใช่แบบตัวต่อตัว ตัวอย่างนี้คือการตัดรูปหลายเหลี่ยม; ขอบใด ๆ ที่เฉพาะเจาะจงอาจสร้างขอบจำนวนมากหรือแม้กระทั่งไม่มีขอบเมื่อตัด ผู้ทำซ้ำทำให้เรื่องนี้ง่ายขึ้นอย่างมากในการแสดงออกและการยอมจำนนเป็นหนึ่งในวิธีที่ดีที่สุดในการเขียน
Donal Fellows

+1 คำตอบที่ดีกว่านี้มากเมื่อฉันจดไว้ ตอนนี้ฉันยังได้เรียนรู้ว่าผลตอบแทนที่ดีสำหรับประสิทธิภาพที่ดีขึ้น
Jan_V

3
กาลครั้งหนึ่งฉันใช้ผลผลิตเพื่อสร้างแพ็กเก็ตสำหรับโปรโตคอลเครือข่ายไบนารี ดูเหมือนเป็นทางเลือกที่เป็นธรรมชาติที่สุดใน C #
György Andrasek

4
การdatabase.Customers.Count()แจกแจงจำนวนลูกค้าทั้งหมดไม่ได้ระบุหรือไม่ดังนั้นจึงต้องใช้รหัสที่มีประสิทธิภาพมากขึ้นในการผ่านทุกรายการ
Stephen

5
โทรหาฉันทางทวารหนัก แต่นั่นคือการต่อข้อมูลไม่รวมกัน (และ linq มีวิธี Concat แล้ว)
OldFart

4

บางครั้งที่ผ่านมาฉันมีตัวอย่างการปฏิบัติสมมติว่าคุณมีสถานการณ์เช่นนี้:

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}", ???));
   }
}

วัตถุปุ่มไม่ทราบตำแหน่งของตัวเองในคอลเลกชัน ข้อ จำกัด เดียวกันนี้ใช้กับDictionary<T>หรือประเภทคอลเลกชันอื่น ๆ

นี่คือทางออกของฉันโดยใช้yieldคำหลัก:

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

และนั่นคือวิธีที่ฉันใช้:

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

ฉันไม่จำเป็นต้องforวนซ้ำคอลเล็กชันของฉันเพื่อค้นหาดัชนี ปุ่มของฉันมี ID และยังใช้เป็นดัชนีในIndexerList<T>ดังนั้นคุณจึงหลีกเลี่ยง ID หรือดัชนีซ้ำซ้อนใด ๆ - นั่นคือสิ่งที่ฉันชอบ! ดัชนี / รหัสสามารถเป็นตัวเลขใดก็ได้


2

ตัวอย่างการปฏิบัติอาจพบได้ที่นี่:

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

มีข้อดีหลายข้อในการใช้ผลตอบแทนมากกว่ารหัสมาตรฐาน:

  • หากใช้ตัววนซ้ำเพื่อสร้างรายการคุณสามารถให้ผลตอบแทนและผู้เรียกสามารถตัดสินใจได้ว่าเขาต้องการผลลัพธ์นั้นในรายการหรือไม่
  • ผู้โทรอาจตัดสินใจยกเลิกการวนซ้ำด้วยเหตุผลที่อยู่นอกขอบเขตของสิ่งที่คุณกำลังทำอยู่ในการทำซ้ำ
  • รหัสสั้นกว่าเล็กน้อย

อย่างไรก็ตามตามที่ Jan_V พูด (เพียงแค่เอาชนะฉันในไม่กี่วินาที :-) คุณสามารถอยู่ได้โดยปราศจากมันเพราะภายในคอมไพเลอร์จะสร้างโค้ดเกือบเหมือนกันทั้งสองกรณี


1

นี่คือตัวอย่าง:

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

ชั้นเรียนทำการคำนวณวันที่ตามสัปดาห์การทำงาน ฉันสามารถบอกตัวอย่างของชั้นเรียนได้ว่าบ็อบทำงาน 9:30 - 17:30 ทุกวันหยุดพักเที่ยงหนึ่งชั่วโมงเวลา 12:30 น. ด้วยความรู้นี้ฟังก์ชั่น AscendingShifts () จะทำให้วัตถุกะทำงานระหว่างวันที่ให้ หากต้องการแสดงรายการการทำงานของ Bob ทั้งหมดระหว่างวันที่ 1 มกราคมถึง 1 กุมภาพันธ์ปีนี้คุณจะใช้สิ่งนี้:

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

ชั้นไม่ได้ย้ำกับคอลเลกชัน อย่างไรก็ตามการเลื่อนระหว่างวันที่สองวันอาจถือว่าเป็นการรวบรวม yieldประกอบการทำให้มันเป็นไปได้ที่จะย้ำกว่าคอลเลกชันนี้คิดโดยไม่ต้องสร้างคอลเลกชันของตัวเอง


1

ฉันมีเลเยอร์ข้อมูล db ขนาดเล็กที่มีcommandคลาสที่คุณตั้งค่าข้อความคำสั่ง SQL ชนิดคำสั่งและส่งคืนค่า IEnumerable ของ 'พารามิเตอร์คำสั่ง'

โดยทั่วไปแนวคิดก็คือให้พิมพ์คำสั่ง CLR แทนการเติมSqlCommandคุณสมบัติและพารามิเตอร์ด้วยตนเองตลอดเวลา

ดังนั้นจึงมีฟังก์ชั่นที่มีลักษณะดังนี้:

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

คลาสที่สืบทอดนี้commandชั้นจะมีคุณสมบัติและAgeName

จากนั้นคุณสามารถสร้างcommandวัตถุที่เติมคุณสมบัติของมันและส่งต่อไปยังdbอินเทอร์เฟซที่จริงแล้วการเรียกคำสั่ง

สรุปแล้วทำให้ง่ายต่อการทำงานกับคำสั่ง SQL และพิมพ์คำสั่งเหล่านั้น


1

แม้ว่ากรณีการรวมกันได้รับการครอบคลุมในคำตอบที่ยอมรับแล้วให้ฉันแสดงวิธีการขยาย params - ผสานผสานผลผลิต™:

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

ฉันใช้สิ่งนี้เพื่อสร้างแพ็กเก็ตของโปรโตคอลเครือข่าย:

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

MarkChecksumวิธีตัวอย่างเช่นมีลักษณะเช่นนี้ และมันก็มีyield:

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

แต่ต้องระวังเมื่อใช้วิธีการรวมเช่น Sum () ในวิธีการแจงนับเนื่องจากจะทำให้กระบวนการแจงนับแยกต่างหาก


1

ตัวอย่าง Elastic Search .NET repo มีตัวอย่างที่ดีของการใช้yield returnเพื่อแบ่งพาร์ติชันคอลเลกชันออกเป็นหลายคอลเลกชันที่มีขนาดที่ระบุ:

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 ฉันเพิ่งจะได้รับกรณีเกี่ยวกับโลกแห่งความจริงที่เกี่ยวข้อง:

ฉันต้องการใช้ FindFirstFile / FindNextFile เวอร์ชัน Kernel32 คุณจะได้รับการจัดการตั้งแต่สายแรกและป้อนไปยังสายที่ตามมาทั้งหมด ล้อมสิ่งนี้ไว้ในตัวแจงนับและคุณจะได้สิ่งที่คุณสามารถใช้กับ foreach ได้โดยตรง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.