หลังจากประสบการณ์เกือบ 4 ปีฉันไม่ได้เห็นรหัสที่ใช้คำหลักผลตอบแทน ใครสามารถแสดงให้ฉันเห็นการใช้งานจริง (ตามคำอธิบาย) ของคำหลักนี้และถ้าเป็นเช่นนั้นจะไม่มีวิธีอื่น ๆ ที่จะเติมเต็มสิ่งที่มันสามารถทำได้หรือไม่?
yield
สามารถนำมาใช้โดยทั่วไป
หลังจากประสบการณ์เกือบ 4 ปีฉันไม่ได้เห็นรหัสที่ใช้คำหลักผลตอบแทน ใครสามารถแสดงให้ฉันเห็นการใช้งานจริง (ตามคำอธิบาย) ของคำหลักนี้และถ้าเป็นเช่นนั้นจะไม่มีวิธีอื่น ๆ ที่จะเติมเต็มสิ่งที่มันสามารถทำได้หรือไม่?
yield
สามารถนำมาใช้โดยทั่วไป
คำตอบ:
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 ที่สร้างขึ้นสำหรับวิธีการวนซ้ำแตกต่างอย่างมากจากที่สร้างขึ้นสำหรับวิธีการปกติ (ไม่ใช่ตัววนซ้ำ) บทความนี้ (คำหลักที่ให้ผลตอบแทนจริงๆสร้างขึ้นจริงๆคืออะไร)ให้ข้อมูลเชิงลึกประเภทนั้น
database.Customers.Count()
แจกแจงจำนวนลูกค้าทั้งหมดไม่ได้ระบุหรือไม่ดังนั้นจึงต้องใช้รหัสที่มีประสิทธิภาพมากขึ้นในการผ่านทุกรายการ
บางครั้งที่ผ่านมาฉันมีตัวอย่างการปฏิบัติสมมติว่าคุณมีสถานการณ์เช่นนี้:
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 หรือดัชนีซ้ำซ้อนใด ๆ - นั่นคือสิ่งที่ฉันชอบ! ดัชนี / รหัสสามารถเป็นตัวเลขใดก็ได้
ตัวอย่างการปฏิบัติอาจพบได้ที่นี่:
http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html
มีข้อดีหลายข้อในการใช้ผลตอบแทนมากกว่ารหัสมาตรฐาน:
อย่างไรก็ตามตามที่ Jan_V พูด (เพียงแค่เอาชนะฉันในไม่กี่วินาที :-) คุณสามารถอยู่ได้โดยปราศจากมันเพราะภายในคอมไพเลอร์จะสร้างโค้ดเกือบเหมือนกันทั้งสองกรณี
นี่คือตัวอย่าง:
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
ประกอบการทำให้มันเป็นไปได้ที่จะย้ำกว่าคอลเลกชันนี้คิดโดยไม่ต้องสร้างคอลเลกชันของตัวเอง
ฉันมีเลเยอร์ข้อมูล 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
ชั้นจะมีคุณสมบัติและAge
Name
จากนั้นคุณสามารถสร้างcommand
วัตถุที่เติมคุณสมบัติของมันและส่งต่อไปยังdb
อินเทอร์เฟซที่จริงแล้วการเรียกคำสั่ง
สรุปแล้วทำให้ง่ายต่อการทำงานกับคำสั่ง SQL และพิมพ์คำสั่งเหล่านั้น
แม้ว่ากรณีการรวมกันได้รับการครอบคลุมในคำตอบที่ยอมรับแล้วให้ฉันแสดงวิธีการขยาย 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 () ในวิธีการแจงนับเนื่องจากจะทำให้กระบวนการแจงนับแยกต่างหาก
ตัวอย่าง Elastic Search .NET repo มีตัวอย่างที่ดีของการใช้yield return
เพื่อแบ่งพาร์ติชันคอลเลกชันออกเป็นหลายคอลเลกชันที่มีขนาดที่ระบุ:
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);
}
}
เมื่อเพิ่มคำตอบของ Jan_V ฉันเพิ่งจะได้รับกรณีเกี่ยวกับโลกแห่งความจริงที่เกี่ยวข้อง:
ฉันต้องการใช้ FindFirstFile / FindNextFile เวอร์ชัน Kernel32 คุณจะได้รับการจัดการตั้งแต่สายแรกและป้อนไปยังสายที่ตามมาทั้งหมด ล้อมสิ่งนี้ไว้ในตัวแจงนับและคุณจะได้สิ่งที่คุณสามารถใช้กับ foreach ได้โดยตรง