私は業務では繰り返し回数を指定することはあんまりないのですが、たまに競技プログラミングの問題を解くことがあります。
競技プログラミングの場合、標準入力からの受取でfor(var i=0; i<n; ++i){
をよく書くことがありました。
RubyだとTimesというメドッドがあって、繰り返しをこんな風に書けます。
3.times{|i|
p i
}
# out =>
# 0
# 1
# 2
C#でもEnumerable.Rangeというメソッドがあって同じように書くとこうなります。
using System;
using System.Linq;
public class Program{
public static void Main(){
Enumerable.Range(0,3).ToList().ForEach(i =>
Console.WriteLine(i)
);
}
}
正直なところforの方がましです。
using System;
public class Program{
public static void Main(){
// Enumerable.Range(0,3).ToList().ForEach(i =>
for(var i = 0; i < 3; ++i){
Console.WriteLine(i);
};
}
}
コメントで並べてみましたがRangeは使う気にならないです。
ただし、そこそこ固い言語なのに拡張性が豊かなのがC#の良いところ、下記のコードが動作するようにできました。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program{
public static void Main(){
3.Times(i =>
Console.Write(i)
);
}
}
usingは増えていますが、RubyのTimesと見劣りがないレベルになりました。
どうやって実現しているかというと拡張メソッド(詳細リンク)を使っています。
3行くらいで説明すると、
- 拡張メソッドは既存のクラスに外部からメソッドを追加する機能です。
- 元のクラスを直接拡張したかのようにメソッドを追加できますが、実際に動作するのは、拡張元のクラスとは別に定義された静的メソッドです。
- intのような型であっても拡張可能です。
まずは、Rubyのuptoと同等のメソッドを実装します。
uptoのサンプルを書きます。
1.upto(3).each{|i|
p i
}
# out =>
# 1
# 2
# 3
で、C#でこう書けるようにしたい。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program{
public static void Main(){
1.To(3).ToList().ForEach(i =>
Console.Write(i)
);
}
}
// out =>
// 1
// 2
// 3
.ToList().ForEach(...
ってなっている部分、長くて嫌ですね。実は普通のforeachの方が素直になります。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program{
public static void Main(){
foreach(var i in 1.To(3)) {
Console.Write(i);
};
}
}
ただ、メソッドチェーンで書くのは素早く書ける(=気持ちがイイ)んです。読みやすさを犠牲にしないような設計が求められます。
それで、Toメソッドの戻りをListにすれば済むんですけど、そこらへんの話を詳しく調べたり書いたりするのは長くなりそうなのでやめときます。とにかくTimesを実装することに集中します。
で話をもどして、Toの実装です。拡張メソッドは、制的クラスに制的メソッドを定義して、第一引数にthisを付けると、thisを付けたクラスに拡張メソッドが追加されます。
using System;
using System.Collections.Generic;
using System.Linq;
public static class Extension{
public static IEnumerable<int> To(this int from, int to){
return Enumerable.Range(from, to - from + 1);
}
}
Enumerable.Rangeの置き換えという目的なのでEnumerable.Rangeを使っています。Rubyにはuptoのほかにdowntoもあるようですが、まとめてToとしてます。にも拘わらずuptoの機能しかない上に入力チェックもしていません。。。
まとめるとこうなります。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program{
public static void Main(){
1.To(3).ToList().ForEach(i =>
Console.Write(i)
);
}
}
public static class Extension{
public static IEnumerable<int> To(this int from, int to){
return Enumerable.Range(from, to - from + 1);
}
}
これをもとに、Timesを追加します。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program{
public static void Main(){
3.Times(i =>
Console.Write(i)
);
}
}
public static class Extension{
public static IEnumerable<int> To(this int from, int to){
return Enumerable.Range(from, to - from + 1);
}
// albireoさんの提案のコード追記
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action){
source.All(x => {action(x); return true;});
}
public static void Times(this int times, Action<int> action){
// 0.To(times - 1).ToList().ForEach(x => action(x));
0.To(times - 1).ForEach(x => action(x));
}
}
あまり説明する部分はないのですが、forの置き換えなので、Toの引数は1減らしてあります。
競技プログラミング用なので、Mainの後ろにコピペする形式なのですが、プロジェクトで使う場合は、usingで読み込む方法を使ってください。
いかがでしょうか、実のところ拡張メソッドであることがわかりずらいので、デバッグの際に面倒になる心配もあります。自分のものでないframeworkを好みにカスタマイズできる醍醐味は捨てがたいです。
実用には実行速度や細かい仕様の詰めなども必要だとは思いますが、ざっくりと拡張メソッドの面白さが伝わればなと思います。
追記について-
albireoさんのコメントを適用しました。
ToListメソッドを実行するとListにデータを格納するために、全件の評価を行う必要があります。LINQのIEnumerableでの拡張ははyeildによって値を1件づつ呼び出すこと(遅延評価)で負荷を分散する仕組みになっています。つまり途中に一回でもToListを行うとその手前では負荷分散ができないことになります。(詳しくは”LINQ と遅延評価”)
Allは他のLINQでのIEnumerableの拡張と同様に引数として与えるActionを遅延評価で呼び出してくれることが期待されます。ただし、メソッドの役割も名前も今回のような用途のためではないため、コードの意図が判りずらくなります。とはいえ一般常識になることもあり得ると考えています。