1. はじめに
リアルな生活でも、いろんな動作ってまるで多機能なスイスアーミーナイフみたいなもんだよね:同じコマンドでも使うツールが違うだけ。例えばATMを想像してみて:カードを入れたらPINコードを聞かれるし、電話番号を入力したらSMSの認証コードを待つ。やることは「ユーザーを確認する」だけど、やり方が違うんだ。
プログラミングでも似たような状況がよくあるよ:一つの操作をしたいけど、データの型やパラメータの数が違う場合がある。例えば、メソッドで人にも動物にも挨拶したいとか、2つ・3つ・10個の整数を合計したいとかね。
もちろん、メソッド名を全部変えてもいいけど:SumTwo、SumThree、SumArrayみたいに。でもプログラマーは基本めんどくさがり(怠け者は進歩の原動力って言うしね)。しかも、そうするとコードが読みにくくなる。
メソッドのオーバーロード
メソッドのオーバーロードは、同じメソッド名でパラメータの組み合わせが違うメソッドを「使い分ける」やり方だよ。これはポリモーフィズムの一種だけど、継承とは関係ないんだ。
オーバーロードは、1つのクラス(またはstruct)で同じ名前のメソッドを複数作れるってこと。ただし、パラメータの型・数・順番が違う必要があるよ。
メソッドのシグネチャ
C#でのシグネチャは、メソッド名+パラメータの型と順番のこと。戻り値の型はシグネチャに含まれない! これが原因で意外なエラーが出ることもある(後で説明するね)。
2. オーバーロードの実例:シンプルなサンプル
じゃあ、Greeterクラスを作ってみよう。これはユーザーにいろんな挨拶をする:名前だけ、名前と年齢、またはパラメータなし。
public class Greeter
{
// パラメータなしの挨拶
public void Greet()
{
Console.WriteLine("こんにちは、世界!");
}
// 名前付きの挨拶
public void Greet(string name)
{
Console.WriteLine($"こんにちは、{name}!");
}
// 名前と年齢付きの挨拶
public void Greet(string name, int age)
{
Console.WriteLine($"こんにちは、{name}!もう{age}歳?すごいね!");
}
}
これでどのメソッドも呼び出せるし、C#のコンパイラがパラメータの数や型を見て自動で正しいバージョンを選んでくれるよ。
var greeter = new Greeter();
greeter.Greet(); // こんにちは、世界!
greeter.Greet("アーニャ"); // こんにちは、アーニャ!
greeter.Greet("ピョートル", 23); // こんにちは、ピョートル!もう23歳?すごいね!
3. パラメータの型と数の違い
オーバーロードは、メソッドが次の点で違えばOK:
- パラメータの数
- 少なくとも1つのパラメータの型
- パラメータ型の順番(ただし注意が必要)
年齢だけを受け取るオーバーロードも追加してみよう:
public void Greet(int age)
{
Console.WriteLine($"その年齢、カッコいいね!({age}歳)");
}
呼び出し例:
greeter.Greet(10); // その年齢、カッコいいね!(10歳)
重要:戻り値の型だけが違う場合はオーバーロードできない。例えば、こんなコードはエラーになる:
// コンパイルエラー!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
コンパイラは「もうFoo(string)ってメソッドあるよ、もっと工夫して!」って怒るよ。
4. C#標準ライブラリとオーバーロード
オーバーロードは自作のGreetだけじゃない。.NETのConsole.WriteLineのドキュメントを見てみよう:
| シグネチャ | 用途 |
|---|---|
|
空行を出力 |
|
文字列を出力 |
|
整数を出力 |
|
小数を出力 |
|
1つの引数で文字列をフォーマット |
|
複数引数でフォーマット |
これ全部同じWriteLineメソッドのオーバーロードだよ。だからいつでもこう書けるんだ:
Console.WriteLine("ただの文字列");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("合計: {0}", 42);
そしてコンパイラはいつも正しく解釈してくれる!
5. コンパイラはどのオーバーロードを選ぶの?
ここは厳密だよ:実際に渡された引数の型と数を見て決める。分かりやすく表にしてみた:
| 呼び出し | どのバージョン? |
|---|---|
|
|
|
|
曖昧な場合はどうなる?
たまに状況がカオスになる。曖昧なオーバーロードの例:コンパイラがどれを選ぶか決められない
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// エラー:曖昧すぎてどのPrintを呼ぶか分からない(どっちも合いそう)
コンパイラは曖昧エラーを出す。こういう場合は、パラメータ数が同じで型が近いオーバーロードは避けた方がいいよ。
6. params — 可変長パラメータ
例えば、いくつでも数値を受け取るメソッドを作りたいとき。そんな時はparamsキーワードが便利。
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"合計: {sum}");
}
これでこう呼べる:
SumAll(1, 2, 3); // 合計: 6
SumAll(10, 20); // 合計: 30
SumAll(); // 合計: 0
params付きメソッドはオーバーロードと組み合わせてもOKだけど、どのバージョンを呼ぶかコンパイラが迷わないように注意してね。
7. オーバーロードとパラメータ修飾子 (ref, out, in)
C#ではパラメータ修飾子もシグネチャに含まれる(つまり、void Foo(int a)とvoid Foo(ref int a)は別物として共存できる):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
refなしは最初のバージョン、refありは2つ目に行くよ:
int n = 5;
SetValue(n); // nは5のまま(値がコピーされる)
SetValue(ref n); // nは100になる
8. 図解:オーバーロードって何?
+----------+
| MyClass |
+----------+
|
| (メソッドの一部)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
コードで表すと:
// オーバーロードされたFoo()を呼ぶ:
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. サンプル:自作アプリでメソッドをオーバーロードしよう
じゃあ、学習用アプリを進化させて、動物クラスの階層にメソッドのオーバーロードを追加しよう。
public class Animal
{
public string Name { get; set; }
// サウンドを出すメソッド
public virtual void MakeSound()
{
Console.WriteLine("なんかよく分からない音…");
}
// オーバーロード:音量指定
public void MakeSound(int volume)
{
Console.WriteLine($"動物が{volume}dBの音を出す。");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("ワン!");
}
// オーバーロード:音量付きのワン!
public void MakeSound(int volume)
{
Console.WriteLine($"ワン!(音量: {volume}dB)");
}
}
こんな呼び方を試してみて:
Dog rex = new Dog();
rex.MakeSound(); // ワン!
rex.MakeSound(75); // ワン!(音量: 75dB)
注目ポイント:サブクラス(Dog)でMakeSound(int volume)をオーバーロードしたから、パラメータあり・なし両方のバージョンが使えるよ。
10. メソッドオーバーロードでよくあるミス
ミス1:戻り値の型だけでオーバーロードしようとする。
これは無理!戻り値の型はシグネチャに含まれない。オーバーロードは入力パラメータの数や型で区別しよう。voidやintだけじゃダメ。
ミス2:曖昧なオーバーロードでコンパイラを混乱させる。
パラメータ数が同じで型が近い(例:intとdouble)と、コンパイラが混乱する。例:Print(int a, double b)とPrint(double a, int b)、Print(1, 1)を呼ぶと曖昧エラー。
ミス3:paramsと他のオーバーロードがバッティング。
params付きメソッドが他のオーバーロードを「横取り」することがある。型が同じだと、コンパイラが意図しない方を選ぶことも。
ミス4:refやoutもシグネチャに含まれるのを忘れる。
Do(ref int x)とDo(out int x)は別のオーバーロード。これを忘れると、間違ったバージョンを呼びやすいよ。
GO TO FULL VERSION