C#::二つのコレクションを同時にforeach

二つのコレクションに値が入っていて、それの同一インデックスに対する処理を行うときにひとつのforeachでやってしまおうという魔術。単純には、forを二重にして回してインデクサでコレクションから値をとってくればいいんだけど、以下の場合にとても役立ちます。

  • インデクサがクソ重い(O(1)でないどこかO(n^2)とかある)が、最初から順番に取得していくのは早い
  • そもそもインデクサが定義されていない(LinkedListとか)
  • インデクサが嫌い(最近この病気を発症しつつある私)

forechはIEnumerableに対する糖衣構文のようなものであるので、それをバラせばforeachを使わずにforeachの動きを再現することはできる。その考えに基づいて拡張メソッドを書いてみると、以下のようになるようだ。ちなみにid:zecl:20090209:p1のnyaruru氏のコメントを参考にしました。

//.NET 3.5(C#3.0): VS2008
public static IEnumerable<Tr> Zip<T1, T2, Tr>(this IEnumerable<T1> l1, IEnumerable<T2> l2, Func<T1, T2, Tr> selector)
{
  using (var i1 = l1.GetEnumerator())
  using (var i2 = l2.GetEnumerator())
  {
    while (i1.MoveNext() && i2.MoveNext())
    {
      yield return selector(i1.Current, i2.Current);
    }
  }
}
//Usage:
//foreach (var data in l1.Zip(l2, (x, y) => new { v1 = x, v2 = y })){ ... }

でもって、このZip()は.NETFramework4.0に含まれているのです!・・・・なんだと!?がんばって損した!でも普段使ってるのがVS2008だからOKOK!
http://msdn.microsoft.com/ja-jp/library/dd267698.aspx

でもさぁ、.NET4.0って「タプル」ってのがあるらしいので、それを返してくれるものもあったらさらに便利だと思うんだよね?
http://msdn.microsoft.com/ja-jp/library/system.tuple.aspx

//.NET 4.0: VS2010
public static IEnumerable<Tuple<T1,T2>> Zip<T1, T2>(this IEnumerable<T1> l1, IEnumerable<T2> l2)
{
  using (var i1 = l1.GetEnumerator())
  using (var i2 = l2.GetEnumerator())
  {
    while (i1.MoveNext() && i2.MoveNext())
    {
      yield return new Tuple<T1, T2>(i1.Current, i2.Current);
    }
  }
}
//Usage:
//foreach (var data in l1.Zip(l2)){ ... }

そしてこれは最初に提示したNyaruru氏のコードと100%同じと言う罠(笑)。

リストの長さが違うのに無理やり合わせこみたい場合はたぶんdefault(T1)とか使えばいいんだろう。

public static IEnumerable<Tr> ZipDefault<T1, T2, Tr>(this IEnumerable<T1> l1, IEnumerable<T2> l2, Func<T1, T2, Tr> selector)
{
  using (var i1 = l1.GetEnumerator())
  using (var i2 = l2.GetEnumerator())
  {
    var b1 = i1.MoveNext();
    var b2 = i2.MoveNext();
    while (b1 || b2)
    {
      if (b1 && b2)
      {
        yield return selector(i1.Current, i2.Current);
      }
      else if (b1)
      {
        yield return selector(i1.Current, default(T2));
      }
      else if (b2)
      {
        yield return selector(default(T1), i2.Current);
      }
      if (b1) b1 = i1.MoveNext();
      if (b2) b2 = i2.MoveNext();
    }
  }
}