初学者教程:实现 IEnumerable 接口和理解 yield 关键字






4.82/5 (111投票s)
本文讨论如何实现 IEnumerable 接口并使用 yield 关键字。
引言
在本文中,我们将讨论 IEnumerable
接口。我们将讨论 IEnumerable
接口如何方便地使用 foreach
语句来迭代一组数据。然后,我们将看看如何实现我们自己的集合,这些集合实现了 IEnumerable
接口。本文还将讨论 yield
关键字和枚举泛型集合的使用。
背景
每当我们处理对象集合时,我们可能会发现自己需要迭代该集合。迭代集合的最佳方法是实现迭代器模式。(参考:理解和实现 C# 和 C++ 中的迭代器模式[^])。C# 提供了非常简洁的 foreach
语句结构,用于以只读方式迭代集合。
C# 还为我们提供了使用相同的 foreach
结构和所有枚举技术在我们自定义的集合对象上实现 IEnumerable
接口的可能性。因此,让我们看看如何使用我们的自定义集合类实现 IEnumerable
接口。
Using the Code
枚举集合类
在开始讨论之前,让我们看看如何使用内置类并对其进行迭代。让我们从研究实现 IEnumerable
的 ArrayList
类开始,看看如何使用 foreach
语句对其进行只读迭代。
// Let us first see how we can enumerate an object implementing IEnumerable
ArrayList list = new ArrayList();
list.Add("1");
list.Add(2);
list.Add("3");
list.Add('4');
foreach (object s in list)
{
Console.WriteLine(s);
}
枚举泛型集合类
Arraylist
类是一个通用类,它允许我们保存一个集合。我们也可以拥有一个泛型类,我们可以在其中提供类型以及数据。迭代泛型集合类也是可能的,因为它们实现了 IEnumerable<T>
接口。让我们看看如何枚举一个泛型集合。
// Let us first see how we can enumerate an object implementing IEnumerable<T>
List<string> listOfStrings = new List<string>();
listOfStrings.Add("one");
listOfStrings.Add("two");
listOfStrings.Add("three");
listOfStrings.Add("four");
foreach (string s in listOfStrings)
{
Console.WriteLine(s);
}
现在我们的目标是拥有我们自己的自定义集合类和一个泛型集合类,它们应该分别实现 IEnumerable
和 IEnumerable<T>
接口,以提供枚举它们的可能性。
理解 Yield 关键字
在开始实现这些类之前,我们需要理解一个非常重要的关键字 yield
,它实际上方便了集合的枚举。yield
语句用于从函数返回值。
一个普通的函数调用,如下所示,将只返回第一个值,无论它被调用多少次。
static int SimpleReturn()
{
return 1;
return 2;
return 3;
}
static void Main(string[] args)
{
// Let's see how simple return works
Console.WriteLine(SimpleReturn());
Console.WriteLine(SimpleReturn());
Console.WriteLine(SimpleReturn());
Console.WriteLine(SimpleReturn());
}
原因是普通的 return
语句在返回时不会保留函数的状态,也就是说,每次调用这个函数都是一个新调用,它将只返回第一个值。
而如果我用 yield
return
替换 return 关键字,那么函数将能够保存其状态,同时返回该值,也就是说,当函数被第二次调用时,它将从上一次调用返回的位置继续处理。
static IEnumerable<int> YieldReturn()
{
yield return 1;
yield return 2;
yield return 3;
}
static void Main(string[] args)
{
// Let's see how yield return works
foreach (int i in YieldReturn())
{
Console.WriteLine(i);
}
}
当我们运行上面的代码时,它将返回 1、2 和 3。在使用 yield
return 语句时唯一的陷阱是该函数应该返回一个 IEnumerable
,并且应该从一个迭代块调用,即 foreach
语句。
在我们的自定义集合类中实现 IEnumerable
现在在我们的自定义集合类中,如果我们定义一个将迭代集合中所有元素并使用 yield
关键字返回它们的函数,我们将能够获取集合中的所有元素。
因此,让我们定义我们自己的 MyArrayList
类并实现 IEnumerable
接口,这将迫使我们实现 GetEnumerator
函数。此函数将迭代集合并对所有元素进行 yield
return。
class MyArrayList : IEnumerable
{
object[] m_Items = null;
int freeIndex = 0;
public MyArrayList()
{
// For the sake of simplicity, let's keep them as arrays
// ideally, it should be link list
m_Items = new object[100];
}
public void Add(object item)
{
// Let us only worry about adding the item
m_Items[freeIndex] = item;
freeIndex++;
}
// IEnumerable Member
public IEnumerator GetEnumerator()
{
foreach (object o in m_Items)
{
// Let's check for end of list (it's bad code since we used arrays)
if(o == null)
{
break;
}
// Return the current element and then on next function call
// resume from next element rather than starting all over again;
yield return o;
}
}
}
这个类现在允许我们使用 foreach
语句枚举所有元素。
static void Main(string[] args)
{
//Let us now go ahead and use our custom MyArrayList with IEnumerable implemented
MyArrayList myList = new MyArrayList();
myList.Add("1");
myList.Add(2);
myList.Add("3");
myList.Add('4');
foreach (object s in myList)
{
Console.WriteLine(s);
}
}
注意:此类既不完整,也不是一个很好的实现。此示例实现的唯一目的是演示 IEnumerable
接口的实现。
在我们的自定义泛型集合类中实现 IEnumerable<T>
现在让我们将这种方法更进一步,定义一个可以被枚举的泛型集合类。为此,我们需要实现 IEnumerable<T>
接口。
class MyList<T> : IEnumerable<T>
{
T[] m_Items = null;
int freeIndex = 0;
public MyList()
{
// For the sake of simplicity, let's keep them as arrays
// ideally, it should be link list
m_Items = new T[100];
}
public void Add(T item)
{
// Let us only worry about adding the item
m_Items[freeIndex] = item;
freeIndex++;
}
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
foreach (T t in m_Items)
{
// Let's check for end of list (it's bad code since we used arrays)
if (t == null) // this won't work if T is not a nullable type
{
break;
}
// Return the current element and then on next function call
// resume from next element rather than starting all over again;
yield return t;
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
// Let's call the generic version here
return this.GetEnumerator();
}
#endregion
}
这个类现在允许我们使用 foreach
语句枚举所有元素。
static void Main(string[] args)
{
// Let us first see how we can enumerate an custom MyList<t> class
// implementing IEnumerable<T>
MyList<string> myListOfStrings = new MyList<string>();
myListOfStrings.Add("one");
myListOfStrings.Add("two");
myListOfStrings.Add("three");
myListOfStrings.Add("four");
foreach (string s in myListOfStrings)
{
Console.WriteLine(s);
}
}
因此,现在我们有了一个集合类和一个泛型集合类,它们分别实现了 IEnumerable
和 IEnumerable<T>
。虽然这些类既不完整,也不是一个很好的实现,但它们确实服务于本文的目的,即演示 IEnumerable
接口的实现。
看点
我们在本文中试图做的是看看我们如何实现 IEnumerable
和 IEnumberable<T>
接口。我们已经研究了 yield
关键字的意义。这些内容对大多数经验丰富的 C# 程序员来说是已知的,但初学者可能会发现它很有用。我希望这具有信息量。
历史
- 2012年10月11日:第一个版本