C#のオーバーロードとは?初心者向けに使い方・メリット・注意点をわかりやすく解説
1. C#のオーバーロードとは
1-1. 同じ名前のメソッドを複数定義できる仕組み
C#のオーバーロードとは、同じクラスの中に、名前が同じメソッドを複数定義する仕組みです。
次のコードでは、どちらのメソッドにもDisplayという名前を付けています。
class Sample{public void Display(string message){Console.WriteLine(message);}public void Display(int number){Console.WriteLine(number);}
}
メソッド名は同じですが、1つ目はstring型、2つ目はint型の引数を受け取ります。そのため、C#コンパイラは呼び出し時に渡された値を確認し、適切なメソッドを選択できます。
var sample = new Sample();sample.Display("こんにちは");sample.Display(100);
実行結果は次のとおりです。
こんにちは100オーバーロードを使わない場合、次のように型ごとに異なる名前を付ける必要があります。
DisplayString("こんにちは");DisplayInt(100);オーバーロードを使えば、処理の目的を表すDisplayという名前に統一できるため、メソッドを利用する側にとって分かりやすいAPIを設計できます。
1-2. 引数の型・個数・順序によって処理を区別する
C#では、主に次の違いによってオーバーロードされたメソッドを区別します。
引数の型
引数の個数
引数の型の並び順
たとえば、次のメソッドはそれぞれ異なるシグネチャを持つため、同じ名前で定義できます。
public void Execute(int value){}public void Execute(string value){}
public void Execute(int value1, int value2){}
public void Execute(string name, int age){}
public void Execute(int age, string name){}
一方、引数名はメソッドを区別するための条件にはなりません。
public void Execute(int value){}// 引数名だけが異なるため定義できないpublic void Execute(int number){}
valueとnumberという引数名は異なりますが、どちらも「int型の引数を1つ受け取るメソッド」です。そのため、同じシグネチャとして扱われ、コンパイルエラーになります。
1-3. オーバーロードが利用される代表的な場面
C#のオーバーロードは、次のような場面でよく利用されます。
1つ目は、受け取るデータ型に応じて処理を変えたい場合です。
Print(string value);Print(int value);Print(double value);2つ目は、利用者が必要に応じて追加情報を渡せるようにする場合です。
Save(string fileName);Save(string fileName, bool overwrite);3つ目は、オブジェクトを複数の方法で初期化できるようにする場合です。
new User();new User("田中");new User("田中", 25);.NETの標準クラスでも、オーバーロードは広く利用されています。たとえば、Console.WriteLineには文字列、整数、小数、真偽値など、さまざまな値を受け取るオーバーロードが用意されています。
Console.WriteLine("Hello");Console.WriteLine(100);Console.WriteLine(3.14);Console.WriteLine(true);利用者は型ごとに異なるメソッド名を覚える必要がなく、どの値でもWriteLineを使って出力できます。
1-4. 初心者がオーバーロードを学ぶメリット
オーバーロードを理解すると、C#の標準ライブラリや外部ライブラリを使いやすくなります。
Visual Studioなどの開発環境でメソッドを入力すると、同じメソッド名に対して複数の引数パターンが表示されることがあります。これは、そのメソッドに複数のオーバーロードが定義されているためです。
また、自分でクラスを設計するときにも、次のような判断ができるようになります。
同じ目的の処理を同じメソッド名にまとめる
型や入力パターンの違いを引数で表現する
呼び出し側が理解しやすいメソッドを作る
不必要にメソッド名を増やさない
オーバーロードは、単に同じ名前を使うための機能ではありません。クラスやライブラリを使いやすく設計するための重要な仕組みです。
2. C#でメソッドをオーバーロードする基本的な書き方
2-1. 引数の型を変えてオーバーロードする方法
最も基本的な方法は、引数の型を変えることです。
class Printer{public void Print(string value){Console.WriteLine($"文字列: {value}");}public void Print(int value){Console.WriteLine($"整数: {value}");}public void Print(double value){Console.WriteLine($"小数: {value}");}
}
呼び出し側では、渡した値の型に対応するメソッドが選択されます。
var printer = new Printer();printer.Print("C#");printer.Print(10);printer.Print(3.14);
実行結果は次のとおりです。
文字列: C#整数: 10小数: 3.14文字列にはPrint(string value)、整数にはPrint(int value)、小数にはPrint(double value)が呼び出されます。
2-2. 引数の個数を変えてオーバーロードする方法
引数の個数が異なるメソッドもオーバーロードできます。
class Calculator{public int Add(int a, int b){return a + b;}public int Add(int a, int b, int c){return a + b + c;}
}
呼び出し例は次のとおりです。
var calculator = new Calculator();Console.WriteLine(calculator.Add(10, 20));Console.WriteLine(calculator.Add(10, 20, 30));
実行結果は次のようになります。
30602つの値を渡した場合は1つ目、3つの値を渡した場合は2つ目のAddメソッドが選択されます。
2-3. 引数の順序を変えてオーバーロードする方法
引数の型の並び順が異なれば、同じ名前のメソッドを定義できます。
class UserService{public void Register(string name, int age){Console.WriteLine($"名前: {name}, 年齢: {age}");}public void Register(int age, string name){Console.WriteLine($"年齢: {age}, 名前: {name}");}
}
次の呼び出しは、それぞれ異なるメソッドを選択します。
var service = new UserService();service.Register("佐藤", 30);service.Register(30, "佐藤");
ただし、引数の順序だけが異なるオーバーロードは、利用者が混乱しやすい設計でもあります。
Register("佐藤", 30);Register(30, "佐藤");どちらも同じ情報を渡しているため、順序を間違える可能性があります。技術的には定義できますが、本当に必要かを検討してから使用しましょう。
2-4. 戻り値の型だけではオーバーロードできない
C#では、戻り値の型だけを変えてもオーバーロードにはなりません。
次のコードはコンパイルエラーになります。
class Converter{public int Convert(string value){return int.Parse(value);}public double Convert(string value){return double.Parse(value);}
}
どちらも引数がstring型の1つだけであり、メソッド名と引数リストが同じだからです。
呼び出し側で次のように記述しても、戻り値を受け取る変数だけを見て呼び出し先が決まるわけではありません。
int intValue = converter.Convert("10");double doubleValue = converter.Convert("10");このような処理を作りたい場合は、メソッド名を分けます。
public int ConvertToInt(string value){return int.Parse(value);}public double ConvertToDouble(string value){return double.Parse(value);}
または、変換先の型をジェネリック型引数などで表現する設計を検討します。
2-5. staticメソッドでもオーバーロードできる
オーバーロードは、インスタンスメソッドだけでなくstaticメソッドでも利用できます。
class MathHelper{public static int Multiply(int a, int b){return a * b;}public static double Multiply(double a, double b){return a * b;}
}
呼び出し例は次のとおりです。
int intResult = MathHelper.Multiply(3, 4);double doubleResult = MathHelper.Multiply(1.5, 2.0);Console.WriteLine(intResult);Console.WriteLine(doubleResult);
実行結果は次のようになります。
123staticであるかどうかにかかわらず、引数の型や個数が異なればオーバーロードできます。
3. C#のオーバーロードを使った実践例
3-1. 数値の型に応じて計算処理を切り替える例
整数と小数で異なる計算を行う例を見てみましょう。
class PriceCalculator{public int CalculateTax(int price){Console.WriteLine("int型のメソッドを実行");return price * 10 / 100;}public double CalculateTax(double price){Console.WriteLine("double型のメソッドを実行");return price * 0.1;}
}
呼び出し側のコードは次のとおりです。
var calculator = new PriceCalculator();int intTax = calculator.CalculateTax(1000);double doubleTax = calculator.CalculateTax(1250.5);
Console.WriteLine(intTax);Console.WriteLine(doubleTax);
1000はint型なのでCalculateTax(int price)が選ばれます。1250.5はdouble型なのでCalculateTax(double price)が選ばれます。
ただし、金額計算では丸め誤差を避けるため、実際にはdecimal型が適しているケースもあります。オーバーロードを設計するときは、対象となるデータに合った型を選ぶことが重要です。
3-2. 引数の個数に応じて処理を切り替える例
ログを出力するメソッドを、引数の個数に応じて切り替える例です。
class Logger{public void Log(string message){Console.WriteLine(message);}public void Log(string message, string category){Console.WriteLine($"<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[{category}]</span> {message}");}public void Log(string message, string category, DateTime time){Console.WriteLine($"{time:yyyy-MM-dd HH:mm:ss} <span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[{category}]</span> {message}");}
}
呼び出し側は、必要な情報だけを渡せます。
var logger = new Logger();logger.Log("処理を開始しました");logger.Log("ログインに失敗しました", "ERROR");logger.Log("データを保存しました", "INFO", DateTime.Now);
このように、基本的な呼び出し方と詳細な呼び出し方を同じメソッド名で提供できます。
3-3. 文字列と数値を受け取るメソッドの例
文字列と数値を組み合わせたオーバーロードも定義できます。
class MessageBuilder{public string Create(string message){return message;}public string Create(int code){return $"コード: {code}";}public string Create(string message, int code){return $"{message}(コード: {code})";}
}
呼び出し例は次のとおりです。
var builder = new MessageBuilder();Console.WriteLine(builder.Create("正常に完了しました"));Console.WriteLine(builder.Create(200));Console.WriteLine(builder.Create("正常に完了しました", 200));
実行結果は次のようになります。
正常に完了しましたコード: 200正常に完了しました(コード: 200)処理の目的が「メッセージを作る」という点で共通しているため、Createという名前に統一しても自然です。
3-4. コンストラクターをオーバーロードする例
C#では、通常のメソッドだけでなくコンストラクターもオーバーロードできます。
class User{public string Name { get; }public int Age { get; }public User(){Name = "名前未設定";Age = 0;}public User(string name){Name = name;Age = 0;}public User(string name, int age){Name = name;Age = age;}
}
これにより、利用者は必要な情報に応じてオブジェクトの作り方を選べます。
var user1 = new User();var user2 = new User("鈴木");var user3 = new User("鈴木", 28);重複する初期化処理がある場合は、thisを使って別のコンストラクターを呼び出すと効率的です。
class User{public string Name { get; }public int Age { get; }public User() : this("名前未設定", 0){}public User(string name) : this(name, 0){}public User(string name, int age){Name = name;Age = age;}
}
この例では、実際の初期化処理をUser(string name, int age)に集約しています。名前や年齢の設定方法を変更するときも、1か所を修正すれば済みます。
3-5. 実際の呼び出し時に選択されるメソッドを確認する
どのオーバーロードが選択されたかを確認したい場合は、それぞれのメソッド内で異なるメッセージを出力すると分かりやすくなります。
class OverloadSample{public void Show(int value){Console.WriteLine("int型のShowが呼ばれました");}public void Show(long value){Console.WriteLine("long型のShowが呼ばれました");}public void Show(double value){Console.WriteLine("double型のShowが呼ばれました");}
}
呼び出し側で型を明示します。
var sample = new OverloadSample();sample.Show(10);sample.Show(10L);sample.Show(10.0);
実行結果は次のとおりです。
int型のShowが呼ばれましたlong型のShowが呼ばれましたdouble型のShowが呼ばれました数値リテラルでは、末尾の記号によって型が変わります。
10は通常int10Lはlong10.0はdouble10.0fはfloat10.0mはdecimal
オーバーロードを利用するときは、呼び出し側の値がどの型として扱われるかも意識しましょう。
4. C#でオーバーロードを使うメリット
4-1. 関連する処理を同じメソッド名に統一できる
オーバーロードの大きなメリットは、目的が同じ処理を同じメソッド名にまとめられることです。
たとえば、さまざまな型の値を画面に表示する処理に、次のような名前を付けることもできます。
PrintString(string value);PrintInt(int value);PrintDouble(double value);しかし、利用者から見ると、すべて「値を表示する処理」です。オーバーロードを使えば、次のように統一できます。
Print(string value);Print(int value);Print(double value);処理の目的が名前に表れるため、クラス全体の設計も整理しやすくなります。
4-2. メソッド名を覚えやすくできる
型や引数の個数ごとに別の名前を付けると、利用者が覚えなければならないメソッド名が増えます。
オーバーロードを使えば、基本となるメソッド名だけを覚えれば、さまざまな入力形式に対応できます。
たとえば、データを保存する方法が複数あっても、次のようにSaveへ統一できます。
Save(string fileName);Save(string fileName, bool overwrite);Save(string fileName, Encoding encoding);利用者は、まずSaveを探し、その後で必要な引数パターンを選べます。
4-3. 呼び出し側のコードを読みやすくできる
適切に設計されたオーバーロードは、呼び出し側の意図を読み取りやすくします。
notification.Send("処理が完了しました");notification.Send("処理が失敗しました", "admin@example.com");どちらも通知を送る処理であることが、Sendというメソッド名から分かります。
一方、次のように名前が細かく分かれていると、処理の関係が見えにくくなることがあります。
notification.SendMessage("処理が完了しました");notification.SendMessageToEmail("処理が失敗しました","admin@example.com");処理の意味が共通している場合は、オーバーロードで統一したほうが自然です。
4-4. 異なる型や入力パターンに柔軟に対応できる
オーバーロードを使うと、既存のメソッド名を維持したまま、新しい入力パターンを追加できます。
Parse(string value);Parse(ReadOnlySpan<char> value);利用者は、自分が持っているデータ形式に適したメソッドを選択できます。
ただし、新しいオーバーロードを追加すると、既存コードの呼び出し先に影響する可能性があります。特に暗黙的な型変換が関係する場合は、追加後も既存の呼び出しが意図したメソッドを選ぶか確認が必要です。
4-5. クラスやライブラリの使いやすさを高められる
ライブラリを利用するとき、同じ機能に対して複数の入力方法が用意されていると便利です。
たとえば、ファイルを開く処理で次のような選択肢を提供できます。
Open(string path);Open(string path, FileMode mode);Open(string path, FileMode mode, FileAccess access);簡単に使いたい人はファイルパスだけを渡し、細かな設定が必要な人は追加の引数を指定できます。
このように、初心者向けの簡単な呼び出し方と、上級者向けの詳細な呼び出し方を同じメソッド名で提供できる点も、オーバーロードのメリットです。
5. C#でオーバーロードを使う際の注意点
5-1. 戻り値だけが異なるメソッドは定義できない
戻り値の型は、オーバーロードを区別する条件になりません。
次のコードは定義できません。
public int GetValue(){return 10;}public string GetValue(){return "10";}
どちらも引数を持たないGetValueメソッドであり、呼び出し時にどちらを選択すべきか判断できないためです。
戻り値の意味が異なる場合は、次のように名前を分ける方法が分かりやすいでしょう。
public int GetIntValue(){return 10;}public string GetStringValue(){return "10";}
5-2. 引数名だけを変えてもオーバーロードにならない
次の2つのメソッドは、引数名だけが異なります。
public void Update(int userId){}public void Update(int productId){}
どちらも「int型の引数を1つ受け取るUpdateメソッド」であるため、同時には定義できません。
引数が表す意味は違っていても、型が同じであればコンパイラには区別できません。この場合は、メソッド名を分けます。
public void UpdateUser(int userId){}public void UpdateProduct(int productId){}
独自の型を作り、型の違いで意味を明確にする方法もあります。
5-3. 暗黙的な型変換によって呼び出しが曖昧になることがある
C#では、数値型や参照型などに暗黙的な型変換があります。そのため、渡した値と完全に同じ型のオーバーロードがなくても、変換可能なメソッドが候補になります。
次の例を見てみましょう。
class Sample{public void Process(long value){Console.WriteLine("long");}public void Process(double value){Console.WriteLine("double");}
}
int型の値はlong型にもdouble型にも暗黙的に変換できます。
var sample = new Sample();sample.Process(10);この例では、C#のオーバーロード解決規則に従って、より適切と判断されたメソッドが選択されます。しかし、型の組み合わせによっては、どちらが適切か決定できず、コンパイルエラーになることがあります。
特に、複数の引数がそれぞれ別の候補に適している場合は注意が必要です。
class Sample{public void Process(long first, float second){}public void Process(float first, long second){}
}
次の呼び出しでは、どちらのオーバーロードが適切か一意に決められません。
sample.Process(10, 20);このような組み合わせは、可能な限り設計段階で避けましょう。
5-4. optional引数との組み合わせで競合することがある
optional引数とは、既定値が設定された省略可能な引数です。
public void Send(string message, int retryCount = 3){}呼び出し側では、retryCountを省略できます。
Send("こんにちは");オーバーロードとoptional引数を同時に増やすと、複数のメソッドが呼び出し候補になることがあります。
public void Execute(int count = 1, double rate = 1.0){}public void Execute(double rate = 1.0, int count = 1){}
引数なしで呼び出すと、どちらのメソッドも候補になります。
Execute();このような定義では、コンパイラが呼び出し先を一意に特定できません。
optional引数を使う場合は、似たオーバーロードを作りすぎず、引数を省略したときに候補が重ならないように設計しましょう。
5-5. オーバーロードを増やしすぎると可読性が下がる
オーバーロードは便利ですが、数が多すぎると、どれを使えばよいのか分かりにくくなります。
Save(string path);Save(string path, bool overwrite);Save(string path, int bufferSize);Save(string path, Encoding encoding);Save(string path, bool overwrite, int bufferSize);Save(string path, bool overwrite, Encoding encoding);Save(string path, int bufferSize, Encoding encoding);このようなメソッドは、入力パターンが多く、利用者が違いを理解するのに時間がかかります。
設定項目が増える場合は、設定用クラスを渡す方法も検討できます。
class SaveOptions{public bool Overwrite { get; set; }public int BufferSize { get; set; } = 4096;public Encoding Encoding { get; set; } = Encoding.UTF8;}public void Save(string path, SaveOptions options){}呼び出し側では、必要な設定だけを指定できます。
Save("data.txt", new SaveOptions{Overwrite = true,Encoding = Encoding.UTF8});5-6. nullを渡すと呼び出し先を判定できない場合がある
参照型を受け取るオーバーロードでは、nullを渡したときに曖昧になる場合があります。
using System.Text;class Sample{public void Print(string value){}
public void Print(StringBuilder value){}
}
次の呼び出しでは、nullがstringとStringBuilderのどちらにも変換できるため、呼び出し先を決められません。
var sample = new Sample();sample.Print(null);呼び出し先を明示したい場合は、キャストします。
sample.Print((string?)null);または、変数の型を明示します。
string? message = null;sample.Print(message);nullable参照型を有効にしている場合は、nullを受け入れる設計かどうかも明確にしましょう。
6. オーバーロードと似た機能の違い
6-1. オーバーロードとオーバーライドの違い
オーバーロードとオーバーライドは名前が似ていますが、目的が異なります。
オーバーロードは、同じ名前で引数が異なるメソッドを複数定義する仕組みです。
public void Show(string value){}public void Show(int value){}
オーバーライドは、基底クラスで定義されたvirtualまたはabstractメソッドの処理を、派生クラスで上書きする仕組みです。
class Animal{public virtual void Speak(){Console.WriteLine("動物の鳴き声");}}class Dog : Animal{public override void Speak(){Console.WriteLine("ワン");}}
主な違いは次のとおりです。
| 項目 | オーバーロード | オーバーライド |
|---|---|---|
| 主な目的 | 入力パターンを増やす | 継承先で処理を変更する |
| メソッド名 | 同じ | 同じ |
| 引数 | 型や個数などが異なる | 基本的に同じ |
| 継承 | 必須ではない | 必要 |
| 主なキーワード | 特別なキーワードなし | virtual、abstract、override |
「入力方法を増やしたい」ときはオーバーロード、「派生クラスごとに動作を変えたい」ときはオーバーライドを使います。
6-2. オーバーロードとoptional引数の違い
optional引数を使うと、1つのメソッドで引数の省略を許可できます。
public void Connect(string host, int port = 80){}次の2通りで呼び出せます。
Connect("example.com");Connect("example.com", 8080);同じ処理をオーバーロードで表すと、次のようになります。
public void Connect(string host){Connect(host, 80);}public void Connect(string host, int port){// 接続処理}
optional引数は、既定値を使うだけの単純な違いに向いています。オーバーロードは、引数の型や個数に応じて処理そのものを変えたい場合に向いています。
また、引数を省略した呼び出し方法を将来変更する可能性や、呼び出し元とのバージョン互換性も考慮して選択する必要があります。
6-3. オーバーロードとジェネリックメソッドの違い
ジェネリックメソッドは、型をパラメーターとして受け取ることで、さまざまな型に共通する処理を記述する仕組みです。
public void Print<T>(T value){Console.WriteLine(value);}このメソッドは、複数の型に対して呼び出せます。
Print(100);Print("こんにちは");Print(3.14);型ごとに処理内容が異なる場合は、オーバーロードが適しています。
public void Print(int value){Console.WriteLine($"整数: {value}");}public void Print(string value){Console.WriteLine($"文字列: {value}");}
判断の目安は次のとおりです。
型が違っても同じ処理をする場合はジェネリック
型ごとに異なる処理をする場合はオーバーロード
一部の型だけ特別扱いする場合は設計全体を再検討する
ジェネリックとオーバーロードを組み合わせることもできますが、どのメソッドが選ばれるか分かりにくくならないよう注意が必要です。
6-4. オーバーロードとメソッドの隠蔽の違い
派生クラスで基底クラスと同じ名前のメソッドを定義すると、基底クラスのメンバーを隠す場合があります。
class BaseClass{public void Show(int value){Console.WriteLine("BaseClass");}}class DerivedClass : BaseClass{public void Show(string value){Console.WriteLine("DerivedClass");}}
このようなコードでは、継承関係にまたがったメソッド検索や暗黙的な型変換によって、呼び出し結果が直感と異なる可能性があります。
意図して基底クラスのメンバーを隠す場合には、newキーワードを使用できます。
class DerivedClass : BaseClass{public new void Show(int value){Console.WriteLine("DerivedClass");}}ただし、これはオーバーライドではありません。変数の宣言型によって呼び出されるメソッドが変わります。
BaseClass baseValue = new DerivedClass();DerivedClass derivedValue = new DerivedClass();baseValue.Show(10);derivedValue.Show(10);
継承先で処理を置き換えたい場合は、通常はvirtualとoverrideを検討します。
6-5. どの機能を使うべきか判断する基準
機能を選ぶときは、実現したい目的を整理しましょう。
| 実現したいこと | 適した機能 |
| 引数の型や個数を変えたい | オーバーロード |
| 引数の一部を省略できるようにしたい | optional引数 |
| 複数の型に同じ処理を適用したい | ジェネリックメソッド |
| 派生クラスごとに処理を変えたい | オーバーライド |
| 基底クラスのメンバーを意図的に隠したい | newによる隠蔽 |
どの機能も組み合わせられますが、複雑に組み合わせるほど利用者が理解しにくくなります。最も単純に目的を表現できる方法を選ぶことが大切です。
7. C#のオーバーロードを適切に設計するコツ
7-1. 各メソッドの役割を統一する
同じ名前のメソッドには、共通する役割を持たせましょう。
次の例では、どちらのFormatも値を文字列として整形する役割を持っています。
public string Format(int value){return value.ToString("N0");}public string Format(DateTime value){return value.ToString("yyyy-MM-dd");}
一方、同じ名前なのに役割が大きく異なる設計は避けます。
public void Process(string filePath){// ファイルを読み込む}public int Process(int productId){// 商品を削除するreturn productId;}
どちらもProcessですが、処理の意味に共通性がありません。次のように、役割を表す名前へ分けたほうが分かりやすくなります。
public void LoadFile(string filePath){}public void DeleteProduct(int productId){}
7-2. 共通処理を1つのメソッドに集約する
複数のオーバーロードに同じ処理を書き込むと、修正漏れが起きやすくなります。
public void Save(string path){// 入力確認// ファイル保存}public void Save(string path, bool overwrite){// 入力確認// ファイル保存}
共通処理は、最も詳細な引数を受け取るメソッドやprivateメソッドに集約しましょう。
public void Save(string path){Save(path, false);}public void Save(string path, bool overwrite){// 入力確認とファイル保存をここに集約}
コンストラクターでは、thisを使って別のコンストラクターへ処理を集約できます。
public User() : this("名前未設定", 0){}public User(string name) : this(name, 0){}
public User(string name, int age){Name = name;Age = age;}
7-3. 引数の意味が分かる名前と型を選ぶ
引数名はオーバーロードを区別する条件にはなりませんが、人がコードを理解するためには重要です。
public void Schedule(DateTime startTime, TimeSpan duration){}value1やvalue2のような名前より、何を渡すのかが明確です。
public void Schedule(DateTime value1, TimeSpan value2){}同じ基本型でも意味が異なる値を頻繁に扱う場合は、専用の型を作る方法もあります。
public readonly record struct UserId(int Value);public readonly record struct ProductId(int Value);public void Find(UserId userId){}public void Find(ProductId productId){}
これにより、利用者IDと商品IDを誤って渡す問題を防ぎやすくなります。
7-4. 呼び出しが曖昧になる組み合わせを避ける
暗黙的な型変換によって候補が重なる型は、できるだけ同じオーバーロード群に含めないようにします。
たとえば、次のように似た数値型を多数並べると、呼び出し側が型を意識しなければならなくなります。
Calculate(short value);Calculate(int value);Calculate(long value);Calculate(float value);Calculate(double value);Calculate(decimal value);本当に型ごとに異なる処理が必要かを確認しましょう。処理が共通している場合は、型を1つに統一したり、ジェネリックメソッドを検討したりできます。
また、参照型のオーバーロードでは、nullを渡したときに曖昧にならないかも確認が必要です。
7-5. 利用頻度の低いパターンは別メソッド名も検討する
すべての入力パターンをオーバーロードにする必要はありません。
たとえば、通常の保存と圧縮保存では、処理の意味が十分に異なることがあります。
Save(string path);Save(string path, CompressionLevel level);圧縮処理が特殊な操作であれば、次のように別の名前にしたほうが意図を伝えやすい場合があります。
Save(string path);SaveCompressed(string path, CompressionLevel level);メソッド名を統一することよりも、呼び出し側のコードを読んだときに処理内容が分かることを優先しましょう。
7-6. XMLドキュメントコメントで違いを明確にする
公開メソッドに複数のオーバーロードがある場合は、XMLドキュメントコメントで用途の違いを説明しましょう。
/// <summary>/// 指定されたメッセージを標準出力へ表示します。/// </summary>/// <param name="message">表示するメッセージ。</param>public void Print(string message){Console.WriteLine(message);}/// <summary>/// 指定された数値を桁区切り形式で表示します。/// </summary>/// <param name="number">表示する整数。</param>public void Print(int number){Console.WriteLine(number.ToString("N0"));}
開発環境の入力補完で説明が表示されるため、利用者が適切なオーバーロードを選びやすくなります。
次の情報を記載すると効果的です。
そのオーバーロードの用途
各引数の意味
既定の動作
戻り値の内容
発生する可能性がある例外
ほかのオーバーロードとの違い
代表的な使用例
8. C#のオーバーロードで発生しやすいエラーと対処法
8-1. 同じシグネチャが重複しているエラー
同じ型の引数を同じ順番で受け取るメソッドを複数定義すると、重複エラーが発生します。
public void Display(int value){}public void Display(int number){}
引数名は違いますが、シグネチャは同じです。このような場合、一般にCS0111のコンパイルエラーが発生します。
対処法は次のいずれかです。
重複しているメソッドを削除する
メソッド名を変更する
引数の型や個数を変更する
役割が異なるなら別のクラスへ分ける
単にコンパイルを通すために型を変更するのではなく、メソッドの役割に合った設計になっているか確認しましょう。
8-2. 呼び出すメソッドを特定できないエラー
複数のオーバーロードが同程度に適切な候補になると、コンパイラが呼び出し先を決められません。
public void Output(string? value){}public void Output(StringBuilder? value){}
次の呼び出しは曖昧です。
Output(null);このような場合、一般にCS0121のコンパイルエラーが発生します。
対処するには、引数の型を明示します。
Output((string?)null);設計を変更できる場合は、曖昧になるオーバーロードそのものを避ける方法も検討しましょう。
8-3. 意図しない型変換で別のメソッドが呼ばれる問題
コンパイルエラーが発生しなくても、想定と異なるオーバーロードが選ばれることがあります。
class Sample{public void Show(int value){Console.WriteLine("int");}public void Show(double value){Console.WriteLine("double");}
}
次の呼び出しではdouble型のメソッドが選択されます。
float value = 1.5f;new Sample().Show(value);float型を受け取るオーバーロードがなく、floatからdoubleへの暗黙的な変換が利用されるためです。
どのメソッドが選ばれるか不安な場合は、次の点を確認します。
変数の宣言型
数値リテラルの型
暗黙的な数値変換
継承関係による参照型変換
ユーザー定義の暗黙的な型変換
IDEでメソッドにカーソルを合わせたり、定義へ移動したりすると、選択されるオーバーロードを確認できます。
8-4. キャストして呼び出し先を明示する方法
呼び出したいメソッドが決まっている場合は、引数をキャストして型を明示できます。
public void Print(long value){Console.WriteLine("long");}public void Print(double value){Console.WriteLine("double");}
int value = 10;Print((long)value);Print((double)value);
実行結果は次のとおりです。
longdoublenullの場合も同様です。
Print((string?)null);ただし、呼び出すたびにキャストが必要になる設計は、利用者に負担をかけます。頻繁にキャストが必要であれば、オーバーロードの構成やメソッド名を見直しましょう。
8-5. 引数やメソッド名を見直して曖昧さを解消する方法
曖昧なオーバーロードを解消する最も確実な方法は、設計を単純にすることです。
たとえば、次のメソッドはnullを渡すと曖昧になります。
Load(string? path);Load(Stream? stream);用途が明確に異なるのであれば、名前を分ける方法があります。
LoadFromFile(string path);LoadFromStream(Stream stream);呼び出し側のコードだけを見ても、どこからデータを読み込むのか分かります。
ほかにも、次のような改善方法があります。
共通の基底型を受け取る
設定用オブジェクトにまとめる
ジェネリックメソッドを使用する
専用の型を作る
利用頻度の低いオーバーロードを削除する
引数の順番だけが違う定義を避ける
オーバーロードの数を増やす前に、利用者が迷わず呼び出せるかを確認しましょう。
9. C#のオーバーロードに関するよくある質問
9-1. Mainメソッドはオーバーロードできる?
Mainも通常のメソッドと同じように、異なる引数を持つメソッドとして複数定義すること自体は可能です。
class Program{static void Main(){Main("テスト");}static void Main(string message){Console.WriteLine(message);}
}
ただし、アプリケーションのエントリーポイントとして認識されるMainには、有効なシグネチャの条件があります。
複数の有効なエントリーポイント候補が存在すると、コンパイラが開始位置を特定できず、エラーになる可能性があります。単に補助メソッドとしてオーバーロードするより、役割に合った別名を付けるほうが分かりやすい場合もあります。
また、トップレベルステートメントを使用するプロジェクトでは、エントリーポイントがコンパイラによって生成されるため、従来のMainメソッドを記述しない構成も一般的です。
9-2. コンストラクターは何個までオーバーロードできる?
C#の言語機能として、コンストラクターのオーバーロード数に「最大5個」のような小さな固定上限はありません。シグネチャが異なれば、複数のコンストラクターを定義できます。
public User(){}public User(string name){}
public User(string name, int age){}
public User(string name, int age, string email){}
ただし、数が多すぎると利用者が選びにくくなります。
初期化項目が多い場合は、次の方法も検討しましょう。
プロパティ初期化子
オブジェクト初期化子
staticファクトリーメソッド
Builderパターン
設定用オブジェクト
必須項目と任意項目の整理
技術的に定義できる数ではなく、利用者が理解できる数に抑えることが重要です。
9-3. privateメソッドもオーバーロードできる?
privateメソッドもオーバーロードできます。
class Sample{private void Validate(string value){if (string.IsNullOrWhiteSpace(value)){throw new ArgumentException("値が空です。");}}private void Validate(int value){if (value < 0){throw new ArgumentOutOfRangeException(nameof(value));}}
}
アクセス修飾子にかかわらず、シグネチャが異なればオーバーロードできます。
public、private、protected、internalのどのメソッドでも、基本的な考え方は同じです。
9-4. 継承したメソッドをオーバーロードできる?
派生クラスで、基底クラスのメソッドと同じ名前を持ち、異なる引数のメソッドを定義することは可能です。
class BasePrinter{public void Print(string value){Console.WriteLine(value);}}class NumberPrinter : BasePrinter{public void Print(int value){Console.WriteLine(value);}}
ただし、継承関係にまたがって同じ名前のメソッドを追加すると、名前の隠蔽やオーバーロード解決の影響で、期待した候補が選択されないことがあります。
利用者に基底クラスのメソッドと派生クラスのメソッドを一組の機能として見せたい場合は、実際の呼び出しをテストし、必要に応じて別名やオーバーライドを検討しましょう。
9-5. 演算子もオーバーロードできる?
C#では、特定の演算子をオーバーロードできます。たとえば、独自クラスに対して+演算子の動作を定義できます。
class Point{public int X { get; }public int Y { get; }public Point(int x, int y){X = x;Y = y;}public static Point operator +(Point left, Point right){return new Point(left.X + right.X,left.Y + right.Y);}
}
呼び出し側では、通常の数値と同じように+を使用できます。
var point1 = new Point(10, 20);var point2 = new Point(30, 40);Point result = point1 + point2;
Console.WriteLine($"{result.X}, {result.Y}");
実行結果は次のとおりです。
40, 60ただし、すべての演算子を自由にオーバーロードできるわけではありません。また、演算子から直感的に想像できない処理を実装すると、コードが理解しにくくなります。
+なら加算や結合、==なら等価性の判定というように、一般的な意味に沿って設計しましょう。
9-6. オーバーロードとオーバーライドは同時に使える?
オーバーロードとオーバーライドは、同じクラス階層で併用できます。
class MessageWriter{public virtual void Write(string message){Console.WriteLine(message);}public void Write(int number){Console.WriteLine(number);}
}
class CustomMessageWriter : MessageWriter{public override void Write(string message){Console.WriteLine($"メッセージ: {message}");}}
この例では、Write(string)をオーバーライドし、Write(int)は基底クラスに定義された別のオーバーロードとして利用しています。
var writer = new CustomMessageWriter();writer.Write("こんにちは");writer.Write(100);
オーバーライドは継承先で処理を変更し、オーバーロードは異なる入力パターンを提供します。役割が異なるため、必要に応じて組み合わせられます。
ただし、継承階層が深く、各クラスに多数のオーバーロードがあると、どのメソッドが呼ばれるか理解しにくくなります。公開APIでは、シンプルな構成を保つことが大切です。
まとめ
C#のオーバーロードは、同じ名前で引数の型、個数、型の並び順が異なるメソッドを複数定義する仕組みです。
オーバーロードを使うと、関連する処理を同じメソッド名に統一でき、呼び出し側のコードを読みやすくできます。通常のメソッドだけでなく、staticメソッドやコンストラクター、特定の演算子にも利用できます。
一方で、次の点には注意が必要です。
戻り値の型だけではオーバーロードできない
引数名だけを変えても区別できない
暗黙的な型変換で呼び出しが曖昧になることがある
nullを渡すと候補を特定できない場合があるoptional引数と組み合わせると候補が競合することがある
オーバーロードを増やしすぎると使いにくくなる
オーバーロードを設計するときは、同じ名前のメソッドが共通の目的を持っているかを確認しましょう。共通処理は1か所へ集約し、利用者が迷う組み合わせを避けることも重要です。
オーバーロード、オーバーライド、optional引数、ジェネリックメソッドの違いを理解し、目的に合った機能を選ぶことで、読みやすく使いやすいC#コードを作成できます。

