この記事は「C#アドベントカレンダー 22日目」です。
はじめに
皆様、タプってますか?(^^)
とっても便利ですよねタプル(Tuple)!
※"タピる"はともかく"タプる"って聞いた事がない...?
大丈夫、私も聞いたことないです!
今日は、弊社での拡張メソッドの利用法やタプルを使った例をお伝えします!
弊社での拡張メソッド利用例
※拡張メソッドの書き方は後述します。
- 文字列を数値化する
→ToInt()、ToDecimal() ... - 文字列をDateTimeに変換する
→ToDateTime() - DateTimeを書式文字列化する ※N部分は表の数値
→FormatDateN()
| N | 書式文字列 |
|---|---|
| 6 | yyyyMM |
| 7 | yyyy/MM |
| 8 | yyyyMMdd |
| 10 | yyyy/MM/dd |
→FormatTimeN()
| N | 書式文字列 |
|---|---|
| 4 | HHmm |
| 5 | HH:mm |
| 6 | HHmmss |
| 8 | HH$:$mm$:$ss |
→FormatDateTimeN()
| N | 書式文字列 |
|---|---|
| 12 | yyyyMMddHHmm |
| 14 | yyyyMMddHHmmss |
| 16 | yyyy/MM/dd HH:mm |
| 19 | yyyy/MM/dd HH$:$mm$:$ss |
メソッド末尾の数字(N)で結果の幅(バイト数)を分けています。
※こういう悲劇を防ぐ趣旨です...
いずれも中身は至って簡単なものですが、
対象の変数の後にピリオド打つだけで候補に出てきてくれて簡単に変換できるのは実に快適です!
拡張メソッドの簡単なおさらい
※わかってるよって方は、スキップで...
拡張メソッドとは、Google検索のAIさん説明では
既存のクラス(stringやListなど)のソースコードを直接変更せずに、あたかもそのクラスに新しいメソッドを追加したかのように振る舞わせる機能です。
...でした。説明通りですね!
新しいメソッド(拡張メソッド)の生やし方は下記の通りです。
//適切なネームスペースで
static public class HeishaCommon
{
static public String FormatDate8(this DateTime dt)
{
return dt.ToString("yyyyMMdd");
}
}
ポイントは、
-
トップレベルのstatic(静的)なクラスのメソッドである事
→クラスの入れ子の中には作れない -
メソッドの第1引数が拡張する対象のクラス/インターフェースになる
→上記例では"FormatDate8"がメソッド、
"DateTime dt"が第1引数なのでDateTimeが拡張対象になる - 第1引数の前に "this" を付ける
...特にthisのお陰で利用する側クラス/インターフェースの変数に
string s = DateTime変数.FormatDate8();
...とFormatDate8()を生やす事ができます!
this = DateTime変数 ...と解釈される訳です。
this を付け忘れると、
var r = HeishaCommon.FormatDate8(DateTime変数);
...としか書けません。
※thisがあっても、こう書くことは可能...拡張メソッドのメリットないですが(^^;
※よく見かける例では"DateTimeExtensions.cs"作って
"DateTimeExtensions静的クラス"作って...のように書かれていますが、
それ自体は拡張メソッドの動作に必須な要件ではないです。
タプルの簡単なおさらい
話を進める前に、簡単なおさらいを。
※わかってるよって方は、スキップで...
タプルとは、Google検索のAIさん説明では
複数の異なる型のデータを1つのまとまり(軽量データ構造)として扱える機能です。C# 7.0以降で導入され、メソッドが複数の値を返す場合や、名前付きで一時的なデータをグループ化する際に便利で、()(括弧)を使って簡潔に定義・利用できます。
...でした。説明通りですね!
使い方は下記の通りです。
var tp1 = ("テスト", 123, DateTime.Now);
// (要素1, 要素2, ...) と2以上の要素を括弧()で括る
// tp1の型: (string,int,DateTime Now)
Console.WriteLine(tp1);
// (テスト, 123, 実行日時)
// ※toString()で型名ではなく中身を表示してくれる
Console.WriteLine(tp1.Item3.FormatDate8());
// Item1..3で各要素にアクセス
// (Item3はDateTimeなので弊社環境の拡張メソッドが使える)
Console.WriteLine(tp1.Now.FormatDate8());
// Item3は元のDateTime.Now から"Now"と名付けてくれているので
// Nowも使える
var tp2 = (Name: "TEST", Number: 456, Genzai: DateTime.Now);
// 要素名: 要素 で各要素に要素名を付けられる(付けなくても良い)
// tp2の型: (string Name, int Number, DateTime Genzai)
Console.WriteLine(tp2.Genzai.FormatDate8());
// 名付けた要素名でアクセスできる
// (引き続きItem3も使える)
//Console.WriteLine(tp2.Now.FormatDate8());
// これはコンパイルできない。
// ※"Genzai"と名付けたので"Now"は使えなくなった
//引数:tpl(string Namae, int Suuji, DateTime Imai) のタプル
//戻値:(string NameVal, int NumberVal, DateTime GenzaiVal) のタプル
(string NameVal, int NumberVal, DateTime GenzaiVal)
TupleMethod((string Namae, int Suuji, DateTime Ima) tpl)
{
Console.WriteLine(tpl.Ima.FormatDate8());
//メソッド内は引数のタプルとしてアクセスできる
return tpl;
//結果が(string,int,DateTime)であれば良い
//(戻値の要素名と一致していなくてもOK)
//もちろん
// return ("文字列",123,DateTime.Now);
//とタプルに組み立てても良い
}
var tp3 = TupleMethod(tp2);
//tp2が(string,int,DateTime)であれば良い
//(メソッドの要素名と一致していなくてもOK)
var tp4 = TupleMethod(("namae", 999, DateTime.Today));
//その場でタプルを組み立てても良い
//tp3,tp4はメソッド戻値のタプル
//(string NameVal, int NumberVal, DateTime GenzaiVal)
var (s, i, dt) = TupleMethod(tp3);
//varによる変数宣言&展開も有難い!
//そして不要な要素(例えばi)は'_'にすると捨てられます。
//var (s, _, dt) = TupleMethod(tp3);
...このように、カッコ()で括れば皆タプル!
※説明のためメソッド引数にもタプルを使いましたが、
実際の使いどころは戻値かと思います。
タプルのお陰でメソッドへの参照渡しを書くことがなくなりました!
稀によくある話
いよいよ本題です。
取り込みたいデータのファイル等の中身をforeachでループしていて、
foreach (var line in fileLines)
{
////lineを使った処理
}
エラーメッセージとかで行番号が必要になり、
int count = 0;
foreach (var line in fileLines)
{
count++;
////lineを使った処理
}
...こんな感じでcountを追加することって稀によくありますよね...?
※LINQ慣れしている人ならSelectのデリゲートの第2引数でタプるのでしょうけど...
そこで拡張メソッドの追加
上記をタプルでタプって解決しよう!
...という事で下記の拡張メソッドを追加しました。
static public IEnumerable<(T Item, int Count)> ToCounterTuple<T>(
this IEnumerable<T> enumerable, int startCount = 1)
{
return enumerable.Select((item, index) => ((item, index + startCount)));
}
ポイントは、
- 戻値をIEnumerableのタプルにする(IEnumerable<(T Item, int Count)>)
- Selectメソッドの結果をメソッドの戻り値にする
- Selectメソッドのデリゲートの結果を(Item, Count)のタプルにする
→Item=デリゲート第1引数(item)、
Count=デリゲート第2引数(index)+開始値(startCount: 省略時1)
...事で、カウント作業をアウトソーシング&タプル化しちゃいます。
使い方(とっても簡単!)
最初の"ループしていて"を下記のように修正します。
foreach (var (line, count) in fileLines.ToCounterTuple())
{
////lineを使った処理、もうcountも利用可能!
}
- 大元のリストや配列に .ToCounterTuple() を追加
- var item を var (item, count) に修正
このようにタプルの利用で、ループの中身の修正は不要で手軽にcountを追加できます!
var tupでタプル状態でも受け取れます(tup.Item,tup.Count のように使う)が、
例のようにvar (item, count) とすると各変数に展開してくれるので、
修正前の変数を活かした形にできるのが嬉しいですね!
オプション引数
拡張メソッド ToCounterTuple のオプション引数を
int startCount = 1
...としているので、
foreach (var (line, index) in fileLines.ToCounterTuple(0))
{
////lineを使った処理、オプション引数0とすればindexな感じにもできる!
}
...上記のように引数0を指定すると、0オリジンのindex扱いにもできます。
拡張メソッドのデメリット
便利な拡張メソッドですが、
- 会社で追加して、自宅にない事を忘れて使おうとしてしまう
→そして自宅にも足しますね... - 慣れすぎると標準機能なのか自作の拡張メソッドなのかを忘れる
→いつかQiitaに上げるコード例に、そのまま使ってしまいそう(笑) - 拡張メソッドを足せない環境の場合、イライラする
→「あぁ使えないんだった...」と都度引っかかり、地味にストレス(^^;
...と、このような副作用にご注意下さい。
これも便利さの裏返しと言えましょう。
まとめ
- タプル万歳!拡張メソッド万歳!ジェネリクス万歳!
- みんな生まれてきてくれてありがとう!
- やっぱりC#楽しい!
つまり"皆でタプろう!拡張メソッド活用しよう!"というお話でした。
この記事が少しでも、どなたかの参考になれば幸いです(^^)
余談
- FormatXX数字 ってネーミングはどうなの...?
- ToCounterTuple ってネーミングはどうなの...?
- (Type)Extensions.csで(Type)Extensions静的クラスが良い!
...と思う方は、お好みで修正して下さい!
明日は @dexcter さんの「【WinForms】Formが重くなってきたら始めるリファクタ2選」です(^^)/