Swift & Objective-C

メソッドシグネチャに戻り値の型は含まれる?

「メソッドシグネチャとは?」と聞かれると、多くの人は「メソッドの名前とパラメータ…みたいなもの」と曖昧に答えます。

読了 8 分
メソッドシグネチャに戻り値の型は含まれる?のカバー画像

「メソッドシグネチャとは?」と聞かれると、多くの人は「メソッドの名前とパラメータ…みたいなもの」と曖昧に答えます。

間違いではありませんが、もう一歩踏み込むと明確に分かれます。「では、戻り値の型はシグネチャに含まれますか?」ここから答えが揺らぎ始めます。技術面接でも定番の質問であり、オーバーロードの可否を分ける基準でもあります。

さらに興味深いのは、この問いの答えが言語によって異なる点です。Javaでは戻り値の型はシグネチャに含まれませんが、Swiftでは戻り値の型だけが異なってもオーバーロードできます。

この記事では、シグネチャの正確な定義から言語ごとの違い、実務で重要な理由まで順に整理します。


シグネチャはメソッドの身分証

メソッドシグネチャとは、ひと言で言えばコンパイラがこのメソッドを他のメソッドと区別するために使う識別情報です。

人でいえば身分証のようなものです。名前だけでは同姓同名を区別できないため、身分証には名前以外に生年月日などの情報も含まれます。メソッドも同じです。同じ名前のメソッドが複数存在できるため(オーバーロード)、名前以外の情報で一つを正確に特定する必要があります。

一般的に、シグネチャを構成する要素は次のとおりです。

  • メソッド名
  • パラメータの型
  • パラメータの数
  • パラメータの順序
// この4つのメソッドはすべてシグネチャが異なります
void print(int value)              // print(int)
void print(String value)           // print(String) — 型が異なる
void print(int a, int b)           // print(int, int) — 数が異なる
void print(String s, int n)        // print(String, int)
void print(int n, String s)        // print(int, String) — 順序が異なる

注意点が一つあります。パラメータ名はシグネチャではありませんprint(int value)print(int number)は同じシグネチャです。コンパイラから見ると、呼び出し側のprint(3)だけを見て区別する方法がないためです。

アクセス修飾子(publicprivate)、staticfinal、例外宣言(throws)もシグネチャには含まれません。シグネチャに含まれるのは、「どのメソッドを呼ぶか決めるために必要な情報」だけです。


では、戻り値の型は含まれる?

Javaの場合、答えは含まれません。Java言語仕様(JLS 8.4.2)は、シグネチャを「メソッド名と引数の型」と定義しています。

そのため、戻り値の型だけが異なるオーバーロードはコンパイルエラーになります。

int parse(String input) { ... }

// コンパイルエラー: 'parse(String)' is already defined
String parse(String input) { ... }

なぜこの設計なのかは、呼び出し側を見ると分かります。

parse("42");  // 戻り値を受け取らない場合、どの parseを呼ぶか決められない

Javaでは戻り値を無視してメソッドを呼び出すことが常に許可されています。この場合、コンパイラには2つのparseのどちらを呼ぶか判断する根拠がありません。だから戻り値の型をシグネチャから完全に外したのです。C++も同じ理由で、戻り値の型だけが異なるオーバーロードを禁止しています。

知っておくとよい細部があります。JVMバイトコードレベルでは、戻り値の型もメソッドを区別する情報に含まれます。クラスファイル内では、メソッドはparse(Ljava/lang/String;)Iのようなディスクリプタで識別され、最後のIが戻り値の型(int)です。つまり「戻り値の型はシグネチャではない」のはJava言語の規則であり、JVMという実行環境の規則ではありません。この隙間があるから、ジェネリックのブリッジメソッドのような仕組みが可能になります。

メソッドシグネチャの構成要素図 — 名前・パラメータ型・数・順序はJavaとSwiftに共通で含まれ、引数ラベルと戻り値の型はSwiftだけに含まれる
同じ質問でもJavaとSwiftでは答えが分かれる — オレンジ色の2つのセルが言語ごとの差です

Swiftでは答えが異なる

同じ質問をSwiftにすると答えが逆になります。Swiftでは戻り値の型だけが異なってもオーバーロードできます

func random() -> Int { Int.random(in: 0...100) }
func random() -> Double { Double.random(in: 0...1) }

let n: Int = random()     // Int バージョンを呼び出す
let x: Double = random()  // Double バージョンを呼び出す

Swiftコンパイラは、呼び出し結果がどの型として使われるか(型コンテキスト)まで見てオーバーロードを解決するためです。ただしコンテキストがないとエラーになります。

let value = random()  // コンパイルエラー: ambiguous use of 'random()'

Swiftのシグネチャには、もう一つ特徴的な要素があります。引数ラベル(argument label)が関数名の一部だという点です。

func move(from start: Point, to end: Point) { ... }
func move(in direction: Direction) { ... }

この2つの関数の正式名はmoveではなく、それぞれmove(from:to:)move(in:)です。パラメータ型が同じでもラベルが異なれば別の関数です。Javaでパラメータ名がシグネチャから完全に除外されるのとは正反対の設計です。

まとめると次のとおりです。

構成要素 Java Swift
メソッド名 含む 含む
パラメータの型・数・順序 含む 含む
パラメータ名(ラベル) 含まない 含む(引数ラベル)
戻り値の型 含まない 含む(コンテキストで解決)

「シグネチャに戻り値の型は含まれますか?」と聞かれて、「言語によります。Javaは含まれず、Swiftは含まれます」と答えられれば、この概念を正しく理解できています。


実務でシグネチャが重要になる3つの場面

定義だけ知って終われば試験の知識です。シグネチャが実際に働く場面は別にあります。

場面1 — オーバーロードとオーバーライドの判別基準

オーバーロードは「同じ名前、異なるシグネチャ」、オーバーライドは「親と同じシグネチャを再実装すること」です。どちらもシグネチャなしには定義できません。

オーバーライドでシグネチャを少しでも間違えると、コンパイラはオーバーライドではなく新しいオーバーロードとして解釈します。親メソッドは正常に残り、自分のメソッドは呼ばれないという静かなバグになります。Javaの@Override、Swiftのoverrideキーワードは、まさにこの事故をコンパイル時に検出するためにあります。

場面2 — インターフェース・プロトコルの採用

インターフェース(プロトコル)を実装するとは、結局要求されたシグネチャと完全に一致するメソッドを提供することです。

Swiftでデリゲートメソッドを実装したのに呼ばれない場合、多くはシグネチャの不一致です。パラメータがオプショナルかどうか、ラベルがforatかというわずかな違いで、プロトコル要件とは別のメソッドになってしまいます。コンパイラが検出する必須要件と違い、オプショナル要件は静かに無視されるため、さらに見つけにくくなります。

場面3 — シグネチャ変更は互換性を壊す

ライブラリやチーム共有モジュールを作るとき、公開メソッドのシグネチャは外部との契約です。

パラメータを一つ追加したり、型をIntからInt64に変えたり、Swiftで引数ラベルを一つ直したりするだけで、そのメソッドを呼んでいたすべてのコードが壊れます。これがbreaking change(互換性を壊す変更)であり、セマンティックバージョニングでメジャーバージョンを上げる代表的な理由です。

そのため成熟したライブラリは、シグネチャを変更する代わりに新しいシグネチャのメソッドを追加し、既存のものはdeprecatedのまま維持します。既存のシグネチャという契約を一方的に破棄しないのです。

公開APIの契約書を書き換えるとコードの橋が壊れるbreaking changeのイラスト — メソッドシグネチャ変更による互換性破壊
公開シグネチャにペンを入れた瞬間、そのメソッドを呼んでいた橋が崩れます

シグネチャ設計で覚えておくこと

シグネチャを契約と捉えると、設計基準も明確になります。

**一度公開したシグネチャは変更しにくいという前提で設計してください。**内部メソッドは自由に直せますが、public APIのシグネチャは初公開前に最も慎重に検討すべきです。後から直すコストは何十倍にもなります。

**パラメータが増えそうなら型にまとめましょう。**パラメータが4〜5個あるシグネチャは順序ミスを招き、一つ追加するたびにbreaking changeになります。設定オブジェクトや構造体にまとめれば、シグネチャを維持したまま拡張できます。

同じ型のパラメータが続くなら危険信号です。transfer(account1, account2)のどちらが出金口座かをシグネチャだけで判断できないなら、Swiftではtransfer(from:to:)のようにラベルを使い、Javaでは意味のある型でパラメータを包み、シグネチャ自体に使い方を説明させるのがよいでしょう。


まとめ

  • メソッドシグネチャはコンパイラがメソッドを区別するための識別情報であり、共通する核心は名前とパラメータの型・数・順序です。
  • 戻り値の型を含むかどうかは言語によって異なります。Java・C++は含まない(戻り値の型だけが異なるオーバーロードは不可)、Swiftは含む(型コンテキストで解決)で、Swiftでは引数ラベルも関数名の一部です。
  • シグネチャはオーバーロードとオーバーライドの判別基準であり、インターフェース実装の一致条件であり、公開APIでは外部との契約です。
  • 公開シグネチャの変更はbreaking changeです。変更するのではなく追加し、最初の設計に最も力を注いでください。

あわせて読みたい