c# LINQと拡張メソッド追加に関して
c#はLINQが最高です。タイトルのようなLINQの使い方に関する情報はいっぱいあるので今回は自分でLINQ風のメソッドを追加していく車輪の再発明について簡単に書いておきます。
拡張メソッド
staticクラスでthisキーワードを加えた引数のメソッドを定義すると
static class Extension { public static void WriteLine(this object obj, string text = "{0}") { Console.WriteLine(text, obj); } }
"hoge".WriteLine(); // Extension.WriteLine("hoge");
ということが出来ます。もちろんジェネリクスも使えますし戻り値も返せます。これを使って拡張メソッドを増やしていきます。
最初から使えるもの
System.Linq.Enumerableに拡張メソッドとして定義されています。
- Aggregate
- All
- Any
- Concat
- Count
- Distinct
- Expect
- First
- ElementAt
- GroupBy
- Last
- OrderBy
- Reverse
- Select
- SelectMany
- SequenceEqual
- Skip
- SkipWhile
- Take
- TakeWhile
- Union
- Where
- Zip
...
といろいろあります。今回はいろいろないものを作ります。
ForEach
var src = new[] { 10, 20, 30, 40, 50 }; src.ForEach(x => Console.WriteLine(x)); src.ForEach((x, i) => Console.WriteLine("src[{0}] : {1}", i, x)); 10 20 30 40 50 src[0] : 10 src[1] : 20 src[2] : 30 src[3] : 40 src[4] : 50
まずはforeach。Linqを使った結果をいちいちforeachかけるのはだるい。拡張メソッドでやりたいと思うなら*1関数の中で処理してやればいいので
public static void ForEach<T>(this IEnumerable<T> src, Action<T, int> func) { foreach (var item in src.Select((x, i) => new { Value = x, Index = i })) func(item.Value, item.Index); } public static void ForEach<T>(this IEnumerable<T> src, Action<T> func) { foreach (var item in src) func(item); }
SelectのオーバーロードにあるFunc<TSource,int,Result>
で実はインデックスが取れるよって話。
DebugPrint
src.DebugPrint("src").Select(x => x / 2).DebugPrint("after (x => x / 2)"); src : [10,20,30,40,50,] after (x => x / 2) : [5,10,15,20,25,]
ToStringしたいけど形名が出る。ただ単に中身がデバッグしたいという目的で制作。
public static IEnumerable<T> DebugPrint<T>(this IEnumerable<T> src, string header = "") { Console.Write("{0}[", header.Length == 0 ? "" : header + " : "); foreach (var item in src) { Console.Write("{0},", item); } Console.WriteLine("]"); return src; }
foreachを使って列挙してかつWriteで評価されるので呼ばれるタイミングでそれより手前のIEnumerableを全て引っ張ってきてしまうので注意が必要。*2
Append(Cons)
src.Append(60).DebugPrint("Append 60"); 0.Append(src).DebugPrint("Append src"); Append 60 : [10,20,30,40,50,60,] Append src : [0,10,20,30,40,50,]
前後連結はconcatがあるが、単一要素と連結したいときに
src.Concat(new []{ 60});
と書くのはあまりにもダサい。中の実装はnewしたくないばかりに少し違うものになっている。
public static IEnumerable<T> Append<T>(this IEnumerable<T> head, T tail) { foreach (var item in head) yield return item; yield return tail; } public static IEnumerable<T> Append<T>(this T head, IEnumerable<T> tail) { yield return head; foreach (var item in tail) yield return item; }
Insert
src.Insert(2, 25).DebugPrint("Insert (2,25)"); src.Insert(3, new[] { 32, 34, 36, 38 }).DebugPrint("Insert (3,[32, 34, 36, 38])"); Insert (2,25) : [10,20,25,30,40,50,] Insert (3,[32, 34, 36, 38]) : [10,20,30,32,34,36,38,40,50,]
「途中に要素を入れたい!でも
var list = src.ToList() list.Add(25); return list;
したくない!」という人向け。意外とList生成する手間が省けるからパフォーマンスにやさしいかも
public static IEnumerable<T> Insert<T>(this IEnumerable<T> src, int index, T insertItem) { foreach (var item in src.Select((x, i) => new { X = x, I = i })) { if (item.I == index) yield return insertItem; yield return item.X; } } public static IEnumerable<T> Insert<T>(this IEnumerable<T> src, int index, IEnumerable<T> insertSrc) { foreach (var item in src.Select((x, i) => new { X = x, I = i })) { if (item.I == index) foreach (var insertItem in insertSrc) yield return insertItem; yield return item.X; } }
番号を控えておいていい感じのタイミングで返す。
Pipe
src.Pipe(x => Console.WriteLine("src.Debug({0})", x)) .Select(x => x * 2) .Pipe(x => Console.WriteLine("src.Select(x => x * 2).Debug({0})", x)) .Where(x => x > 50) .DebugPrint("src |> select x * 2 |> where x > 50"); src |> select x * 2 |> where x > 50 : [src.Debug(10) src.Select(x => x * 2).Debug(20) src.Debug(20) src.Select(x => x * 2).Debug(40) src.Debug(30) src.Select(x => x * 2).Debug(60) 60,src.Debug(40) src.Select(x => x * 2).Debug(80) 80,src.Debug(50) src.Select(x => x * 2).Debug(100) 100,]
ネーミングに正直迷った。任意の処理とかを突っ込みたいけど流れを止めたくない時に。DebugPrintとは異なり評価されるタイミングで呼ばれるのでこっちのほうが自然。
public static IEnumerable<T> Pipe<T>(this IEnumerable<T> src, Action<T> func) { foreach (var item in src) { func(item); yield return item; } }
実装は至ってシンプル。
ToText(ToString)
Enumerable.Range('a', 26).Select(x => (char)x).ToText().WriteLine("ToText() : {0}"); ToText() : abcdefghijklmnopqrstuvwxyz
ありそうでなかったやつ。stringのコンストラクタでchar[]
受けてあげればいいので。
public static string ToText(this IEnumerable<char> src) { return new string(src.ToArray()); }
RandomSource
LinqExtension.RandomSource().Take(10).DebugPrint("RandomSource"); var monteMax = 1000; var randomSource = LinqExtension.RandomSourceDouble(); var monteCount = randomSource.Zip(randomSource, (x, y) => x * x + y * y < 1.0).Take(monteMax).Count(x => x); (monteCount / (double)monteMax * 4.0).WriteLine("PI = {0}"); RandomSource : [1275737024,1965004771,1565074498,1932025343,634065449,671543418,484296667,2118464586,1924109135,1969744308,] PI = 3.216
適当な数列を使いたい時だってある。例えばモンテカルロ法を書きたくなったりとか。
private static Random random = new Random(); public static IEnumerable<double> RandomSourceDouble(double min = 0, double max = 1.0) { var d = max - min; while (true) { yield return random.NextDouble() * d - min; } } public static IEnumerable<int> RandomSource(int min = 0, int max = int.MaxValue) { var d = max - min; while (true) { yield return random.Next(d) - min; } }
注意すべき点があって、Randomのインスタンスを内部で生成してると、呼ばれたタイミング同時の時にシードがおなじになってしまうという事故が発生する。ので予め初期化して回避した。
Shuffle
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> src) { return src.Zip(RandomSource(), (x, i) => new { Index = i, Value = x }).OrderBy(x => x.Index).Select(x => x.Value); } src Shuffle : [30,20,10,40,50,]
Reverse、OrderBy(ソート)があるのにシャッフルがない!
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> src) { return src.Zip(RandomSource(), (x, i) => new { Index = i, Value = x }).OrderBy(x => x.Index).Select(x => x.Value); }
さっきのRandomSourceと組み合わせた後、乱数の値を使って並び替えれば良い。
DiffSelect
var src6 = new[] { 1, 3, 4, 5, 8, 9, 10, 11, 13, 3, 0, -5, -7, -20 }; src6.DebugPrint("src6"); src6.DiffSelect((x, y) => y - x).DebugPrint("CompareSelect (x,y) => y - x"); src6 : [1,3,4,5,8,9,10,11,13,3,0,-5,-7,-20,] DiffSelect (x,y) => y - x : [2,1,1,3,1,1,1,2,-10,-3,-5,-2,-13,]
一個後の値と比較した結果がほしい時もある。微分だって出来るかもしれない。
public static IEnumerable<U> DiffSelect<T, U>(this IEnumerable<T> src, Func<T, T, U> selector) { return src.Zip(src.Skip(1), selector); }
一個ずれたシーケンスをSkipで作ってZipすればいいだけ。
Merge
var src7 = new[] { 1, 9, 10, 12, 15, 18 }; var src8 = new[] { 2, 5, 6, 11, 13, 14, 15, 20, 30 }; src7.DebugPrint("src7"); src8.DebugPrint("src8"); src7.Merge(src8).DebugPrint("src7 merge src8"); src7 : [1,9,10,12,15,18,] src8 : [2,5,6,11,13,14,15,20,30,] src7 merge src8 : [1,2,5,6,9,10,11,12,13,14,15,15,18,20,30,]
内部実装どうでもいいからくっつけて並び替えておいてくれ。
public static IEnumerable<T> Merge<T>(this IEnumerable<T> src1, IEnumerable<T> src2) { return src1.Concat(src2).OrderBy(x => x); }
ボツになったものとか標準で便利なもの
Selectでインデックスが取れる
Select((x,i) => ...)
要素の一致
src1.SequenceEqual(src2);
但し、順序まで一致している必要あり、文句があるならソートしてから使おう。
Zip
var src4 = new[] { 1, 2, 3, 4, 5 }; var src5 = new[] { "A", "B", "C", "D", "E" }; src4.DebugPrint("src4"); src5.DebugPrint("src5"); src4.Zip(src5, (i, str) => str + i.ToString()).DebugPrint("ZipSelect"); src4 : [1,2,3,4,5,] src5 : [A,B,C,D,E,] ZipSelect : [A1,B2,C3,D4,E5,]
いつも匿名クラス作ってたから完全に忘れてたけどresultSelectorは普通にSelectになる。
過去の話
string str1 = "hello"; string str2 = null; str1.WriteLine("str1 = {0}"); str2.WriteLine("str2 = {0}"); str1.NullPropagation(str => str.ToUpper()).WriteLine("str1 IsNull(ToUpper) => {0}"); str2.NullPropagation(str => str.ToUpper()).WriteLine("str2 IsNull(ToUpper) => {0}"); //C# 6.0のNull条件演算子で代用可能 //str1?.ToUpper().WriteLine(); str1 = hello str2 = str1 IsNull(ToUpper) => HELLO str2 IsNull(ToUpper) =>
public static U NullPropagation<T, U>(this T src, Func<T, U> f, U initial = default(U)) { return (src != null) ? f(src) : initial; }
これは新しいc#で実装されるみたいなのでもう不要となりそう。
OrderByとSequenceEqualを使ってください。
https://gist.github.com/kamiyaowl/ff9a05537f29dca09417
もはやタイトルから半ば意味不明になってしまっているが、普通に最初に実装したForEachが使える。