65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (111投票s)

2012 年 10 月 11 日

CPOL

4分钟阅读

viewsIcon

520208

downloadIcon

1865

本文讨论如何实现 IEnumerable 接口并使用 yield 关键字。

引言

在本文中,我们将讨论 IEnumerable 接口。我们将讨论 IEnumerable 接口如何方便地使用 foreach 语句来迭代一组数据。然后,我们将看看如何实现我们自己的集合,这些集合实现了 IEnumerable 接口。本文还将讨论 yield 关键字和枚举泛型集合的使用。

背景

每当我们处理对象集合时,我们可能会发现自己需要迭代该集合。迭代集合的最佳方法是实现迭代器模式。(参考:理解和实现 C# 和 C++ 中的迭代器模式[^])。C# 提供了非常简洁的 foreach 语句结构,用于以只读方式迭代集合。

C# 还为我们提供了使用相同的 foreach 结构和所有枚举技术在我们自定义的集合对象上实现 IEnumerable 接口的可能性。因此,让我们看看如何使用我们的自定义集合类实现 IEnumerable 接口。

Using the Code

枚举集合类

在开始讨论之前,让我们看看如何使用内置类并对其进行迭代。让我们从研究实现 IEnumerableArrayList 类开始,看看如何使用 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);
}

现在我们的目标是拥有我们自己的自定义集合类和一个泛型集合类,它们应该分别实现 IEnumerableIEnumerable<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);
    }
}

因此,现在我们有了一个集合类和一个泛型集合类,它们分别实现了 IEnumerableIEnumerable<T>。虽然这些类既不完整,也不是一个很好的实现,但它们确实服务于本文的目的,即演示 IEnumerable 接口的实现。

看点

我们在本文中试图做的是看看我们如何实现 IEnumerableIEnumberable<T> 接口。我们已经研究了 yield 关键字的意义。这些内容对大多数经验丰富的 C# 程序员来说是已知的,但初学者可能会发现它很有用。我希望这具有信息量。

历史

  • 2012年10月11日:第一个版本
© . All rights reserved.