使用 Pairenumerator 一次迭代两个容器






3.40/5 (3投票s)
一个可枚举类,能够一次迭代两个可枚举集合。直到两者都完成,或者其中一个完成。
引言
本文介绍了一个可枚举类,该类能够一次迭代两个可枚举集合,直到两者都完成,或者其中一个完成。
背景
最近,我不得不将两个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
以及 IEnumerable
的 GetEnumerator()
方法的返回值是必需的。在迭代两个不同的容器时,它们的尺寸通常不同。那么,如果一个容器到达了它的末端,你会期望什么样的行为?有两种可能性:直接退出,或者一直进行直到第二个也到达它的末端。在后一种情况下,必须采取一些预防措施来决定返回什么。它返回的是一个 Pair
,但 Pair
永远是一个 Pair
。所以必须有一个 null 或默认值给元素。Pair
可以用任何类型实例化。无论是值类型、引用类型还是基本类型。根据类型,默认值将是:一个所有成员都设置为 0 的 struct
(例如,new DateTime()
),引用 null
(例如,string
)或者 0 本身(例如,对于 int
)。出于这个原因,C# 语言预见了 default
关键字。应用于像方法调用这样的泛型变量时,它会分配其类型特定的默认值。这在 Pairenumerator
的 Current
属性中得到了体现。另一个方面是 MoveNext()
方法。它会推进它包含的所有枚举器。如果一个枚举器到达了它的末端,它必须决定是继续还是退出。这由一个成员变量 disjunctive 决定,该变量源于 Pairenumerable
的构造函数。如果设置为 true
,它在某种意义上具有 OR 语义,即只要其中一个仍然可以移动,它就会继续。在这种情况下必须小心,因为一旦枚举器到达末端,就不能再次调用它的 MoveNext()
方法。否则,它会抛出异常。所以 bool
标志在 Pairenumerator
的 MoveNext()
中阻止了这种情况。
代码
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 变量。
要点
这是一个很好的例子,说明如何通过利用泛型来减少编程错误。虽然为了构建这些辅助类需要大量的文字,但反复编写相同内容(外加出错)与包含该文件并使用辅助类之间的权衡,后者更有利。
最后的寄语
继续努力,或者如果还没有开始,就开始写文章吧。即使你只有一个小主题,也要围绕它写一篇文章。这会让您和潜在读者都变得更聪明。可以说是双赢的局面。