自家製拡張メソッド制作のすすめ だいx回 BufferWithPadding
- 2011-12-09
Ix(Interactive Extensions)は使っていますか?Rxから逆移植されてきている(IxのNuGet上のアイコンはRxのアイコンの逆向きなのですね)、LINQ to Objectsを更に拡張するメソッド群です。みんな大好きForEachなど、色々入っています。その中でも、私はBufferというものをよく使っています。Ixが参照できない場合は何度も何度も自作するぐらいに使いどころいっぱいあって、便利です。こんなの。
// 指定個数分をまとめたIList<T>を返します
// 第二引数を使うとずらす個数を指定することもできます
// これの結果は
// 0123
// 4567
// 89
foreach (var xs in Enumerable.Range(0, 10).Buffer(4))
{
xs.ForEach(Console.Write);
Console.WriteLine();
}
標準でこういうのできないのー?というと、できないんですよねえ、残念なことに。
さて、ところで、この場合、指定個数に足りなかった場合はその分縮められたものが帰ってきます。上の例だと返ってくるListの長さは4, 4, 2でした。でも、埋めて欲しい場合ってあります。足りない分は0で埋めて長さは4, 4, 4であって欲しい、と。そこはLINQなので、創意工夫で頑張りましょう。例えば
// EnumerableEx.Repeatは指定の値の無限リピート
// それと結合して、Takeで詰めることで足りない場合だけ右を埋めることが出来る
// 0123
// 4567
// 8900
foreach (var xs in Enumerable.Range(0, 10).Buffer(4))
{
xs.Concat(EnumerableEx.Repeat(0)).Take(4).ForEach(Console.Write);
Console.WriteLine();
}
EnumerableEx.RepeatはIxにある無限リピート。Ixを参照しない場合は Enumerable.Repeat(value, int.MaxValue) で代用することも一応可能です。
さて、しかしこれも面倒なので、自家製拡張メソッドを作りましょう。拡張メソッドはばんばん作るべきなのです。
// 指定した値で埋めるように。これの結果は
// 0123
// 4567
// 89-1-1
foreach (var xs in Enumerable.Range(0, 10).BufferWithPadding(4, -1))
{
xs.ForEach(Console.Write);
Console.WriteLine();
}
public static class EnumerableExtensions
{
public static IEnumerable<T[]> BufferWithPadding<T>(this IEnumerable<T> source, int count, T paddingValue = default(T))
{
if (source == null) throw new ArgumentNullException("source");
if (count <= 0) throw new ArgumentOutOfRangeException("count");
return BufferWithPaddingCore(source, count, paddingValue);
}
static IEnumerable<T[]> BufferWithPaddingCore<T>(this IEnumerable<T> source, int count, T paddingValue)
{
var buffer = new T[count];
var index = 0;
foreach (var item in source)
{
buffer[index++] = item;
if (index == count)
{
yield return buffer;
index = 0;
buffer = new T[count];
}
}
if (index != 0)
{
for (; index < count; index++)
{
buffer[index] = paddingValue;
}
yield return buffer;
}
}
}
すっきりしますね!Emptyの時は何も列挙しないようにしていますが、Emptyの時は埋めたのを一つ欲しい、と思う場合は最後のifの囲みを外せばOK。あと、最後のif...for...yieldの部分を var dest = new T[index]; Array.Copy(buffer, dest, index); yield return dest; に変えればパディングしないBufferになります。Ix参照したくないけどBuffer欲しいなあ、と思ったときにコピペってどうぞ。
本体のコードと引数チェックを分けているのは、yield returnは本体が丸ごと遅延評価されるため、引数チェックのタイミング的によろしくないからです。少し面倒ですが、分割するのが良い書き方。詳しくはneue cc - 詳説Ix Share/Memoize/Publish編(もしくはyield returnの注意点)で書いていますので見てください。