经过将近4年的经验,我还没有看到使用yield关键字的代码。有人可以告诉我该关键字的实际用法(以及解释)吗?如果可以,是否还有其他方法可以更轻松地实现它的功能?
yield
总体上可以使用什么。
经过将近4年的经验,我还没有看到使用yield关键字的代码。有人可以告诉我该关键字的实际用法(以及解释)吗?如果可以,是否还有其他方法可以更轻松地实现它的功能?
yield
总体上可以使用什么。
Answers:
该yield
关键字有效地创建了藏品一个懒惰的枚举,可以更高效。例如,如果您的foreach
循环仅对100万个项目的前5个项目进行迭代,那么这都是yield
返回值,而您并没有首先在内部建立100万个项目的集合。同样,你将要使用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.
迭代器版本,高效
(不构建中间集合)
// 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
关键字只能在一个迭代方法的上下文中使用(具有一个返回类型IEnumerable
,IEnumerator
,IEnumerable<T>
,或IEnumerator<T>
。),并有一个与一特殊的关系foreach
。迭代器是特殊的方法。在MSDN文档产量和迭代器文档包含很多有趣的信息和概念的解释。请务必将其与相关的foreach
关键字念叨太,来补充你的迭代器的理解。
要了解迭代器如何实现其效率,秘密在于C#编译器生成的IL代码。为迭代器方法生成的IL与为常规(非迭代器)方法生成的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,它也用作in的索引IndexerList<T>
,因此您可以避免任何多余的ID或索引 -这就是我喜欢的!索引/ Id可以是任意数字。
一个实际的例子可以在这里找到:
http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html
与标准代码相比,使用yield有许多优点:
但是,正如Jan_V所说的(将我击败它几秒钟:-),您可以没有它,因为在内部编译器将在两种情况下产生几乎相同的代码。
这是一个例子:
https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158
该班级基于一个工作周执行日期计算。我可以告诉班上一个实例,鲍勃每周工作9:30到17:30,在12:30休息一小时。有了这些知识,AscendingShifts()函数将在提供的日期之间产生工作班次对象。要列出鲍勃今年1月1日至2月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命令并保持其键入非常简单。
尽管合并的情况已经包含在可接受的答案中,但是让我向您展示yield-merge 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示例存储库提供了一个很好的示例,该示例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一起使用的功能。