Вызов переопределенного метода из родительского класса ctor
Я попытался вызвать переопределенный метод из конструктора родительского класса и заметил различное поведение в разных языках.
C++
- отголоскиA.foo()
class A{
public:
A(){foo();}
virtual void foo(){cout<<"A.foo()";}
};
class B : public A{
public:
B(){}
void foo(){cout<<"B.foo()";}
};
int main(){
B *b = new B();
}
Java
- отголоскиB.foo()
class A{
public A(){foo();}
public void foo(){System.out.println("A.foo()");}
}
class B extends A{
public void foo(){System.out.println("B.foo()");}
}
class Demo{
public static void main(String args[]){
B b = new B();
}
}
C#
- отголоскиB.foo()
class A{
public A(){foo();}
public virtual void foo(){Console.WriteLine("A.foo()");}
}
class B : A{
public override void foo(){Console.WriteLine("B.foo()");}
}
class MainClass
{
public static void Main (string[] args)
{
B b = new B();
}
}
Я понимаю, что в C++ объекты создаются из самых верхних родительских элементов, идущих вниз по иерархии, поэтому, когда конструктор вызывает переопределенный метод, B даже не существует, поэтому он вызывает версию метода A'. Тем не менее, я не уверен, почему я получаю различное поведение в Java и C# (из C++)
2 ответа
В C++, как вы правильно заметили, объект имеет тип A
до A
Конструктор закончен. Объект фактически меняет тип во время своего строительства. Вот почему vtable из A
класс используется, так A::foo()
вызывается вместо B::foo()
,
В Java и C# vtable (или эквивалентный механизм) самого производного типа используется повсеместно, даже во время создания базовых классов. Так что на этих языках B.foo()
вызывается.
Обратите внимание, что обычно не рекомендуется вызывать виртуальный метод из конструктора. Если вы не очень осторожны, виртуальный метод может предположить, что объект полностью построен, даже если это не так. В Java, где каждый метод неявно виртуален, у вас нет выбора.
Хотя я понимаю, что вы делаете это для экспериментов, важно отметить следующую цитату из Effective Java 2nd Edition, Item 17: Разработка и документация для наследования, или же запретить это:
Есть еще несколько ограничений, которым должен следовать класс, чтобы разрешить наследование. Конструкторы не должны вызывать переопределяемые методы, прямо или косвенно. Если вы нарушите это правило, произойдет сбой программы. Конструктор суперкласса выполняется перед конструктором подкласса, поэтому метод переопределения в подклассе будет вызван до запуска конструктора подкласса. Если переопределяющий метод зависит от какой-либо инициализации, выполняемой конструктором подкласса, метод не будет работать должным образом.
Вот пример для иллюстрации:
public class ConstructorCallsOverride {
public static void main(String[] args) {
abstract class Base {
Base() { overrideMe(); }
abstract void overrideMe();
}
class Child extends Base {
final int x;
Child(int x) { this.x = x; }
@Override void overrideMe() {
System.out.println(x);
}
}
new Child(42); // prints "0"
}
}
Здесь, когда Base
вызов конструктора overrideMe
, Child
не завершил инициализацию final int x
и метод получает неправильное значение. Это почти наверняка приведет к ошибкам и ошибкам.