ふとしたきっかけで、C#とJavaのオーバーライドの仕様、またC#(とC++)特有のnew修飾子の挙動について調べ直したので投稿します。
なお筆者は、C#からJava(Android)に移行し、Java側から見て執筆していることをご了承ください。
先に結論
- C#
- 原則すべてのメソッドはオーバーライド不可能
- オーバーライドする場合、virtual修飾子とoverride修飾子とをペアで使う
- オーバーライドはクラスの「拡張」、new修飾子はクラスメンバの「隠ぺい」であり、対になる概念
- Java
- 原則すべてのメソッドがオーバーライド可能(final宣言した場合を除く)
- オーバーライドする場合はOverrideアノテーション付与するだけ
- new修飾子は無い(new演算子のみ)
ひとつずつ見ていきます。
1. 原則オーバーライド可能?不可能?
このC#とJavaの違いは設計思想から来ているそうです。次の引用はC++についてですが、C#にも引き継がれている模様。
C++では、一般に仮想関数はコンパイル時にどのメンバー関数を呼び出すかを確定できないため、通常のメンバー関数呼び出しよりもパフォーマンスが悪いというデメリットがある。そのため、パフォーマンスを気にするC++プログラマには、継承する必要がないクラスのメンバー関数にvirtual修飾子をつけることを非常に嫌がる向きがある。
wiki:メソッド (計算機科学)
対して、
Javaはデフォルトで仮想的
wiki:メソッド (計算機科学)
なので、すべてのメソッドがデフォルトでオーバーライド可能になっています。
なお、「仮想的」とは、C#においてvirtual修飾子を付与したメンバを指し、オーバーライド可能であることを示します(詳細は次項)。
2. オーバーライドの書き方比較
C#の場合
C#では原則オーバーライドが不可能なため、オーバーライドする場合には次のような制約があります。
- オーバーライドされるメソッドはvirtual修飾子が必要
- 派生クラスでオーバーライドする場合、override修飾子が必要
記法は次のようになります。
class Parent {
//被オーバーライドメソッド
public virtual void Hoge() {...}
}
class Child : Parent { //Parentを継承
public override void Hoge() {...} //オーバーライド
}
Javaの場合
一方、Javaは原則オーバーライド可能であるため、被オーバーライドメソッドには特に何もせず、派生クラス側でOverrideアノテーションを付与しオーバーライドします。
class Parent {
//被オーバーライドメソッド
public void hoge() {...}
}
class Child extends Parent { //Parentを継承
//Overrideアノテーションを付けてオーバーライド
@Override
public void hoge() {...}
}
3. new修飾子って何
さて上記を踏まえた上で、C#のリファレンスを見てみます。
new と override には相反する意味があるため、同じメンバーにこの 2 つの修飾子を使用するとエラーになります。 new 修飾子は、同じ名前で新しいメンバーを作成し、元のメンバーを隠ぺいします。 override 修飾子は、継承されたメンバーの実装を拡張します。
new修飾子(C#リファレンス)
new修飾子はオーバーライドとは違い、基のメンバ1を隠蔽するとなっていますが、実際にコンソールプログラムを実行して確かめてみます。
class Program {
static void Main(string[] args) {
Parent parent = new Parent();
Child child = new Child();
Parent childInParent = new Child(); //コイツの動作が変わる
//出力
parent.Hoge(); //Parent#Hoge()
parent.VirtualMethod(); //Parent#VirtualMethod()
child.Hoge(); //Child#Hoge()
child.VirtualMethod(); //Child#VirtualMethod()
childInParent.Hoge(); //Parent#Hoge() ← Parentが出力(※1)
childInParent.VirtualMethod(); //Child#VirtualMethod()
Console.ReadLine();
}
}
public class Parent {
public void Hoge() {
Console.WriteLine("Parent#Hoge()");
}
//virtualとoverride修飾子はペア(※3)
public virtual void VirtualMethod() {
Console.WriteLine("Parent#VirtualMethod()");
}
}
public class Child : Parent {
//new修飾子で隠ぺい(※2)
public new void Hoge() {
Console.WriteLine("Child#Hoge()");
}
//オーバーライド(※3)
public override void VirtualMethod() {
Console.WriteLine("Child#VirtualMethod()");
}
}
C#のメソッドはすべて非仮想メソッドであるため、変数の型に応じてどのクラスのメソッドを呼ぶかが決まります。
上記のコードでは、childInParent変数はChild型のオブジェクトを代入しているのだからChild#Hoge()
をコールしたいところ、変数がParent型であるため、Parent#Hoge()
がコールされてしまいました(※1)。
一方で、Child型の変数child
は、Hoge()をコールするとChild#Hoge()
を出力します。なんか当たり前な感じがしますが、これはParent#Hoge()が同名のメソッドChild#Hoge()で隠ぺいされたことを示します(※2)。
また、オーバーライドする場合はvirtual修飾子を付与して仮想メソッド化し、それを派生クラスでoverride修飾子を付けて実装する形になります(※3)。
まとめ
結論を先に書いてしまったのでまとめも何もありませんが、Java沼からするとなんでこんなオーバーライドによく似た機能があるのか疑問に思うかもしれません。
Javaから見たC#の利点としては次のような感じだと思います。
- 仮想化によるパフォーマンス低下を防ぐ
- オーバーライド可能なメソッドを明示的に示せる
間違い等ありましたら修正いたします。また利点を教えていただけたらありがたいです。
読んでいただきありがとうございました。
-
プロパティ、フィールドメンバ等も仮想化できますが、メソッドが最も使いそうなので省略 ↩