期せずして知識の詰まったメソッドを生んだ
C#を好んで使っている自分が、嫌々ながらJavaScriptを触り始めた折、2次元配列の作り方をChatGPTに尋ねてmap
メソッドを知った。 このmap
に興味を持ち、更にC#でその実装を書かせたところ、C#の基礎的なシンタックスを駆使した良い回答を得た。(参考まで『C#で、JavaScriptのmap関数っぽく2次元配列を作る』)
public static class ArrayExtensions
{
public static T2[] Map<T1, T2>(this T1[] source, Func<T1, T2> func)
{
T2[] result = new T2[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = func(source[i]);
}
return result;
}
}
これに含まれるC#の基礎的なシンタックスとして、
- 拡張メソッド
- ジェネリックメソッド
-
Func
デリゲート
が上げられる。これらの知識がないと、このメソッドを理解することはできない。
復習の意味で、以下にこれらについての説明をまとめてみた。
拡張メソッド
既存のクラスにメソッドを追加する場合、自作のクラスであれば直接クラス自体にメソッドを追加するだろう。
他人が作ったクラスにメソッドを追加する場合はどうか?クラスが非公開だったら?
派生クラスを作ってそこに追加メソッドを書くかもしれない。しかし、継承できない様にsealed
されていたら、それもできない。
そんな状況でも独自のメソッドを追加したいときに用いるのが、拡張メソッドだ。
以下は日付データを日本語文字列にフォーマットするDateTime
の拡張メソッドの例である。
public static class DateTimeExtensions
{
public static string ToStringJp(this DateTime date)
{
return date.ToString("yyyy年MM月dd日");
}
}
拡張メソッドの書き方
- 静的クラスの静的メソッドとする
- 静的メソッドの第一引数がポイント
-
this
キーワードを書く -
this
の後に拡張する型の引数を書く - 型は構造体でもよい
-
- 静的クラス内で、拡張する型はメソッドごとに変えてもよい
これを使ってみると、
DateTime dt = DateTime.Now;
Console.WriteLine(dt.ToStringJp());
今日の日付が2023/6/5なら、、、
>>> 2023年06月05日
dt.ToStringJp()
のようにdt
変数にドットで続けて、あたかもクラスのメンバーのように使えるのだ。
IntelliSenseにも現れるようになる。
自作の静的クラス
ここで疑問に思うのが、静的メソッドなのになぜクラスのインスタンスから呼べるのかということだ。
静的メソッドなら、クラス名で呼び出し、インスタンスからは呼び出せない筈だ。
これは、実際には以下のようにコンパイラによって解釈されているからだ。
// string hoge = dt.ToStringJp();
// は、以下と等価
string hoge = DateTimeExtensions.ToStringJp(dt);
拡張メソッドは、単に自作の静的クラスのメソッドを呼び出しているに過ぎないのだ。
ジェネリックメソッド
メソッドを書いていて、処理は同じパターンだが、データ型だけ変えたいなんてことがある。必要なデータ型の数だけ同じパターンのロジックを持つメソッドを書かなければならない。そんな時、以下の様に何でも受け入れられるobject
型を使ってメソッドを一つにまとめる方法が考えられる。
public void DoSomething(object arg){
// 処理...
}
しかし、これでは引数に値型を与えた時にボックス化(値型から参照型への変換)が起きてしまう。
そこで、ジェネリックメソッドの登場だ。
public void DoSomething<T>(T arg){
// 処理...
}
DoSomething<T>
のT
は型パラメータと言い、このメソッドのスコープ内で型を特定する。このメソッドを使用する時にT
の型を決めるのだ。
例えば、こんな風に、
DoSomething<int>(25);
DoSomething<string>("ネコ");
Class1 c1 = new();
DoSomething<Class1>(c1);
また、推論できる場合<T>
は省略できる。
DoSomething(25);
DoSomething("ネコ");
Class1 c1 = new();
DoSomething(c1);
メソッド定義において、引数が複数必要な時は、以下の様に増やせる。
public void DoSomething<T>(T arg1, T arg2, T arg3){
// 処理...
}
引数の型が複数必要な時は、以下の様に型パラメータを増やす。
public void DoSomething<T1, T2>(T1 arg1, T2 arg2){
// 処理...
}
型パラメータのT
の文字列は実装者の好みで自由に変えられる。
Func
デリゲート (ついでにAction
デリゲート)
デリゲートは、振る舞いなどのロジック自体を変数に格納し、再利用可能にするものだ。
Func
デリゲートは、C#の言語機能の一部として用意されたデリゲート変数を定義する際の型となるものだ。
例えば、メソッドの引数としてこのデリゲート型を設定できる。その場合にどの様なシグネチャにするかをFunc
で表現する。
Func<TResult> // 引数なし、戻り値TResult
Func<T, TResult> // 1つの引数、戻り値TResult
Func<T1, T2, TResult> // 2つの引数、戻り値TResult
// ... 以下16個の引数を持つものまで定義されている
戻り値がない場合はAction
デリゲートを使う。 ( なぜか、VBの ) 型パラメータに戻り値がないことを判別できる様にするためだろう。Function
とSub
の如く別れている
紛らわしいのが、見た目がジェネリックメソッドに似ていることだ。Func
の型パラメータはジェネリックメソッドのそれとは若干意味が異なる。
Func
デリゲートの型パラメータの説明
- 1番後ろは戻り値となる
- 型パラメータが1つの場合、引数なしの関数定義となる
- 型パラメータの数は、1番後ろのものを除き、そのまま引数の数である
- 例えば、
T1
とT2
が同じ型になり得る
以下はFunc
デリゲートを説明する英語のサイトだ。日本語サイトもあるが機械翻訳が酷くて読めたものではない。(マイクロソフト様、改善をお願いします)
最後に
以上を踏まえて、もう一度Map
メソッドを見てみよう。
public static class ArrayExtensions
{
public static T2[] Map<T1, T2>(this T1[] source, Func<T1, T2> func)
{
T2[] result = new T2[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = func(source[i]);
}
return result;
}
}
どうでしょうか。頭が整理できてから見返すと、見え方が変わると思いませんか
この記事を書くことで、自分でも曖昧だった部分がクリアになったり、新たな知識を得たりもできた。
AI全盛の昨今でも、テキスト文書の力は健在だ。AIから得た知恵をどれだけ生かせるか、AIから先が重要であることを書いていて実感できた