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

使用 Pairenumerator 一次迭代两个容器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.40/5 (3投票s)

2008年2月24日

CPOL

6分钟阅读

viewsIcon

26853

downloadIcon

156

一个可枚举类,能够一次迭代两个可枚举集合。直到两者都完成,或者其中一个完成。

引言

本文介绍了一个可枚举类,该类能够一次迭代两个可枚举集合,直到两者都完成,或者其中一个完成。

背景

Pairenumerable.PNG

最近,我不得不将两个List(容器)填充到一个ListView(控件)中,每个列表填充一列。你可能会想,这没什么问题。列表是可枚举的,并且一个像样的 foreach 就能搞定。确实如此。

private void btTry1_Click(object sender, EventArgs e)
{
    listView.Items.Clear();
    foreach (var item in listA)
    {
        listView.Items.Add(item);
    }
    int counter = 0;
    foreach (var iitem in listB)
    {
        listView.Items[counter].SubItems.Add( iitem.ToString());
        counter+=1;
    }
}

处理第一个列表的 foreach 工作正常;然而,处理第二个可枚举集合时就显得很麻烦。你可以稍后修改 ListView 的数据。你需要索引 ListView 的集合,同时迭代 List 容器。这很糟糕,而且你如何保证 List A 的第 n 个元素与 List B 的第 n 个元素在同一行?好吧,只要没有不同的线程,就不会有威胁(GUI 的正常情况)。但是,将这个问题应用到多线程应用程序中并发使用的数据结构上。它将不可靠,或者需要长时间的锁。你可以通过一次枚举两个容器(List)来克服这个问题。也就是说,调用 listA 的枚举器的 MoveNext()listB 的枚举器的 MoveNext()。这可以解决问题,并将可能的锁限制在内层循环。这稍微好一些,也更安全……然而,它不够简洁和优雅;有很多东西需要编写和注意,这很容易出错——而且也无法利用令人愉悦的 foreach 循环。

private void btTry2_Click(object sender, EventArgs e)
{
    listView.Items.Clear();
    IEnumerator<string> enumA = listA.GetEnumerator();
    IEnumerator<int> enumB = listB.GetEnumerator();
    while( true )
    {
        // anvance both but quit if anyone reached the end
        bool hasmoreA = enumA.MoveNext();
        bool hasmoreB = enumB.MoveNext();
        if (!( hasmoreA || hasmoreB))
        {
            break;
        }
        else
        {
            listView.Items.Add(new ListViewItem(
                new string[] { enumA.Current, enumB.Current.ToString() })
                );
        }
    }
}

值得一提的是,在结束条件的情况下,必须将 MoveNext() 调用移出 if 语句。否则,逻辑 OR 的惰性求值会反噬,只会调用第一个变量的方法调用,然后一旦超出,就会调用第二个。然而,在“最长”(析取)和“最短”(合取)枚举之间切换非常容易:通过改变 AND 和 OR 之间的条件。

所以,这也不是我想要的。事实一定介于两者之间。

解决方案

这让我想到了 Pair 和 Pairenumerators。如果枚举器接口适用于一个变量,为什么不将两个变量包装在一个 Pair 中,让它以同样的方式工作呢?然后,特殊逻辑将包装在一些类中。当我搜索这些名字时,我找不到任何东西。所以我在想:是否可以创建一个封装了两个现有可枚举对象的对象,并使其也像一个可枚举对象一样行为,以便在 foreach 循环中使用它?我决定将此视为一个很好的练习,以更多地探索泛型。我的前提是有一个 Pair<A,B> 类,或者更好的是一个 struct,作为期望的返回值。由于我一开始找不到“Pair”,我很快就草拟了一个(是的,这比搜索+集成要快,而且是另一个练习)。KeyValuePair<>(来自 Dictionary),我后来才找到,本来也可以工作,但名字不对。

public struct Pair<T1,T2>
{
    public Pair(T1 frst, T2 sec)
    {
        first= frst;
        second = sec;
    }
    public T1 first;
    public T2 second;
}

就这样。泛型类型没有 where 子句。你可以几乎用任何东西来使用它。它变成了一个 struct(值类型),因为它很小,所以在栈上使用更好。所以,给定 Pair 和一个待开发的 Pairenumerable,让我们看看人们期望它如何被使用。

private void btTry3_Click(object sender, EventArgs e)
{
    listView.Items.Clear();
    foreach (var el in new Pairenumerable<string, int>(listA, listB,cbOr.Checked))
    {
        listView.Items.Add(new ListViewItem(
                // careful: int-var.ToString() works bacause its value type
                // but string-var.ToString() fails if its null (ref type)
                new string[] { el.first, el.second.ToString() })  );
    }
}

这看起来很整洁。foreach 表达式稍微长一些,但好处是自动化了两个可枚举对象的迭代。你甚至可以选择是提前退出(如果一个集合已完成)还是稍后退出(如果两个集合都已完成)。在第二种情况下,根据泛型参数是引用类型还是值类型,可能会存在危险。如果一个集合用完了,引用类型的默认值是 null。因此,任何方法调用都会导致异常。相反,值类型的默认值是实际的默认值。因此,即使结果有意义,对它们进行方法调用仍然会成功。因此,在实际应用程序中,你可能想在循环中写如下内容:

string kto1 = ele.first!=null ? ele.first.ToString() : string.Empty;
string kto2 = ele.second!=null ? ele.second.ToString() : string.Empty;
listView.Items.Add(new ListViewItem( new string[] { kto1, kto2 }));

现在,来看实际的 Pairenumerable。先想几点:我们期望什么?我们期望它是可枚举的。因此,它实现了 IEnumerable<>。我们有两种元素类型可以迭代。我们将把它们合并成一种,用作 IEnumerable<> 的类型参数。这时 Pair<> 就派上用场了。这个结构用于在所有通常只返回一个变量的地方创建一个“二合一”的对象。另一方面,我们为可枚举(IEnumerable<>)容器提供了两个成员。但是我们希望将元素类型 T 作为类型参数,因为我们需要同时构建 IEnumerable<T>IEnumerator<T>。后者对于内部类 Pairenumerator 以及 IEnumerableGetEnumerator() 方法的返回值是必需的。在迭代两个不同的容器时,它们的尺寸通常不同。那么,如果一个容器到达了它的末端,你会期望什么样的行为?有两种可能性:直接退出,或者一直进行直到第二个也到达它的末端。在后一种情况下,必须采取一些预防措施来决定返回什么。它返回的是一个 Pair,但 Pair 永远是一个 Pair。所以必须有一个 null 或默认值给元素。Pair 可以用任何类型实例化。无论是值类型、引用类型还是基本类型。根据类型,默认值将是:一个所有成员都设置为 0 的 struct(例如,new DateTime()),引用 null(例如,string)或者 0 本身(例如,对于 int)。出于这个原因,C# 语言预见了 default 关键字。应用于像方法调用这样的泛型变量时,它会分配其类型特定的默认值。这在 PairenumeratorCurrent 属性中得到了体现。另一个方面是 MoveNext() 方法。它会推进它包含的所有枚举器。如果一个枚举器到达了它的末端,它必须决定是继续还是退出。这由一个成员变量 disjunctive 决定,该变量源于 Pairenumerable 的构造函数。如果设置为 true,它在某种意义上具有 OR 语义,即只要其中一个仍然可以移动,它就会继续。在这种情况下必须小心,因为一旦枚举器到达末端,就不能再次调用它的 MoveNext() 方法。否则,它会抛出异常。所以 bool 标志在 PairenumeratorMoveNext() 中阻止了这种情况。

代码

public class Pairenumerable<T1, T2>
    : IEnumerable<Pair<T1, T2>>
{
    protected IEnumerable<T1> firstlist;
    protected IEnumerable<T2> secondlist;
    protected bool disjunktiv = true;
    public Pairenumerable(IEnumerable<T1> list1, IEnumerable<T2> list2, bool 

disjunctive_mode)
    {
        firstlist = list1;
        secondlist = list2;
        disjunktiv = disjunctive_mode;
    }

    public class Pairenumerator : IEnumerator<Pair<T1, T2>>
    {
        protected IEnumerator<T1> firstenum;
        protected IEnumerator<T2> secondenum;
        protected bool firstenumhasnext = true;
        protected bool secondenumhasnext = true;
        protected bool disjunktiv;
        public Pairenumerator(IEnumerator<T1> enum1, IEnumerator<T2> enum2, bool mode)
        {
            firstenum = enum1;
            secondenum = enum2;
            disjunktiv = mode;
        }

        public Pair<T1, T2> Current
        {
            get
            {
                return new Pair<T1, T2>(
                    (firstenumhasnext ? firstenum.Current : default(T1)),
                    (secondenumhasnext ? secondenum.Current : default(T2))
                    );
            }
        }


        public void Dispose()
        {
            firstenum.Dispose();
            secondenum.Dispose();
        }


        object System.Collections.IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            if (firstenumhasnext)
                firstenumhasnext = firstenum.MoveNext();
            if (secondenumhasnext)
                secondenumhasnext = secondenum.MoveNext();
            // hört auf (ret false), wenn einer nicht mehr kann (disj) oder
            // beide nicht mehr können (konjunktiv/!disj)
            return (disjunktiv && (secondenumhasnext || firstenumhasnext))
                     || (!disjunktiv && (secondenumhasnext && firstenumhasnext));
        }

        public void Reset()
        {
            firstenum.Reset();
            firstenumhasnext = true;
            secondenum.Reset();
            secondenumhasnext = true;
        }

    };

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return new Pairenumerator(firstlist.GetEnumerator(), 
                   secondlist.GetEnumerator(), disjunktiv);
    }

    IEnumerator<Pair<T1, T2>> IEnumerable<Pair<T1, T2>>.GetEnumerator()
    {
        return new Pairenumerator(firstlist.GetEnumerator(), 
              secondlist.GetEnumerator(), disjunktiv);
    }

}

备注 / 问题

从语言语法上看,内部类可能再次具有类型参数。但在这里没有帮助。将 Pairenumerator 设为内部类可以访问相同的类型参数,这是推荐的。这些 IEnumerable/IEnumerator 接口要求你实现一堆泛型方法和旧式非泛型方法。只需将非泛型方法委托给泛型方法即可。但无论如何都要这样做,因为否则旧代码或不支持泛型的语言就无法使用它。应该有一种方法可以从内部类访问外部类的 disjunctive 变量。

要点

这是一个很好的例子,说明如何通过利用泛型来减少编程错误。虽然为了构建这些辅助类需要大量的文字,但反复编写相同内容(外加出错)与包含该文件并使用辅助类之间的权衡,后者更有利。

最后的寄语

继续努力,或者如果还没有开始,就开始写文章吧。即使你只有一个小主题,也要围绕它写一篇文章。这会让您和潜在读者都变得更聪明。可以说是双赢的局面。

© . All rights reserved.