首页 > 技术文章 > C#函数式程序设计之惰性列表工具——迭代器

Ribbon 2014-03-17 15:11 原文

有效地处理数据时当今程序设计语言和框架的一个任务。.NET拥有一个精心构建的集合类系统,它利用迭代器的功能实现对数据的顺序访问。

惰性枚举是一个迭代方法,其核心思想是只在需要的时候才去读取数据。这个思想保证了任何迭代算法都十分有效,同时又可以灵活地根据需要读取任意多的数据,而且不会造成过多的开销。

C#函数式程序设计之枚举元素

.NET集合类型的基础是一个名为IEnumberable的接口,以下就是这个接口的声明:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

实际上IEnumberable接口只允许程序员做一件事:查询类的另一个接口IEnumerator:

public interface IEnumerator
{
     object Current { get; }
     bool MoveNext();
     void Reset();
}

下面这个类事实上是根据IEnumberable和IEnumerator接口声明的迭代模式的最简实现形式:

public class EndlessListWithoutInterfaces
{
    public EndlessListWithoutInterfaces GetEnumerator()
    {
         return this;
    }

    public bool NoveNext()
    {
         return true;
     }

    public object Current
    {
         get
         { return "Something"; }
     }
}

在EndlessListWithoutInterfaces类中也可以使用C#的foreach结构:

var list = new EndlessListWithoutInterfaces();
foreach (var item in list)
{
     Console.WriteLine(item);
}

此实现极其基本,因此这将导致一个无限循环。

下面是EndlessList类的实现,这个类使用IEnumerator和IEnumberable两个类,这两个类相互嵌套,尽管它们不需要里外嵌套。但实际上,在IEnumberable类中嵌套IEnumerator类是一个常见模式:

    public class EndlessList:IEnumerable
    {
        public class Enumerator:IEnumerator
        {
            int val = -1;

            public object Current
            {
                get
                { return val; }
            }

            public bool NoveNext()
            {
                val++;
                return true;
            }

            public void Reset()
            {
                val = -1;
            }
        }

        public IEnumerator GetEnumerator()
        {
            return new Enumerator();
        }
    }

值序列的这种实现模式是非常灵活的,而且功能也强大。

C#函数式程序设计之迭代器函数的实现

C#2.0版本中引入了迭代器。它允许程序员创建IEnumerator/IEnumberable组合的实现,而无须手动实现其中的任意一个接口。除了支持非泛型接口外,它还支持泛型接口,因此可以只实现IEnumerator。

通常为了应用这个功能,只需要定义一个返回某个类型值的函数。编译器为了使用它的转换操作而采用的第二个准则是在函数中至少使用几个特殊关键字中的一个。最常用的是yield return语句。例如,前面的EndlessList例子可以由如下的一个C#迭代器实现:

        public static IEnumerable<int> EndlessListFunc()
        {
            int val = 0;
            while (true)
                yield return val++;
        }

此函数的返回类型是一个IEnumberable<int>,因此在原本使用一个能实现此接口的类实例的地方现在可以使用这个函数。下面几个语句可以迭代访问EndlessListFunc序列:

var list = EndlessListFunc();
foreach (var item in list)
{
     Console.WriteLine(item);
}

IEnumerator/IEnumberable两个接口使用的整个声明模式与惰性有关,即只是在需要时才读取数据。这种模式的优点是:一个序列只是整个算法的一个很少但非常灵活的部分,序列也无需对它的将来使用进行任何假设。

下面这段代码是一个迭代器的实际例子,利用Twitter流的一个搜索,从Web服务读取数据。从Web服务读取数据也采用惰性方法,因为它使用了自动分页技术。迭代器根据需要读取新页,把长度未知的字符串序列返回给调用者。调用者按照自己喜欢的方式处理这个序列:

        private static void TwitterSearchIterator()
        {
            foreach (var tweet in GetTweets("#msdn").Take(10))
                Console.WriteLine(tweet);
        }

        private static IEnumerable<string> GetTweets(string searchString)
        {
            var url = "http://search.twitter.com/search.atom?q=";
            int page = 1;
            var escapedSearchString = searchString.Replace("@", "%40").Replace("#", "%23");
            XNamespace ns = "http://www.w3.org/2005/Atom";

            while (true)
            {
                var doc = XDocument.Load(String.Format("{0}{1} & page={2}", url, escapedSearchString, page));
                var entries = doc.Root.Elements(ns + "entry");
                if (entries.Count() == 0)
                    yield break;
                foreach (var entry in entries)
                    yield return
                        entry.Element(ns+"author").Element(ns+"name").Value+": "+WebUtility.HtmlDecode(entry.Element(ns+"title").Value;
                page++;
            }
        }

迭代器不仅可以返回前面例子使用的IEnumberable和IEnumberable<T>接口,还可以返回IEnumerator和IEnumerator<T>接口。

C#函数式程序设计之链式迭代器

很容易把函数形式的迭代器链接在一起,并通过它们创建复杂的处理流程。这个思想广泛用于LINQ,这是函数式程序设计一个最重要的概念。

前面介绍了EndlessListFunc迭代器,它可以返回一个无穷的整数值序列,处理序列的迭代器函数可以与这个迭代器一起使用。例如下例是Take函数的实现,它通过参数接受一个证书序列和一个计数值,且只返回源序列中的前几个元素:

public static IEnumerable<int> Take(int count, IEnumerable<int> source)
{
     int used = 0;
     foreach (var item in source)
         if (count > used++)
               yield return item;
         else
               yield break;
}

可以把Take与EndlessListFunc一起使用:

var FiveElements = Take(5, EndlessListFunc());
foreach (var item in FiveElements)
    Console.WriteLine(item);

这个例子很好地说明了如何把迭代器当成模块使用。

用在链式迭代器中的第二种类型函数是使用实际内容的函数,比如执行数值计算。

推荐阅读