Вызов переопределенного метода из 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();              
    }
}

Я понимаю, что в С++ объекты создаются из самого верхнего родителя, идущего вниз по иерархии, поэтому, когда конструктор вызывает переопределенный метод, B даже не существует, поэтому он вызывает версию метода A. Однако я не уверен, почему я получаю различное поведение в Java и С# (от С++)


person Vaibhav Bajpai    schedule 24.05.2010    source источник
comment
Не вызывайте виртуальную функцию в конструкторе C++... parashift.com/c++-faq-lite/ctors.html#faq-10.7   -  person Harald Scheirich    schedule 24.05.2010
comment
Вопрос странный вопрос. C# и C++ — разные языки, поэтому, конечно, у них разные правила. Если бы у них были одинаковые правила, то они были бы одним и тем же языком. Почему должен C# следовать правилам C++?   -  person Eric Lippert    schedule 25.05.2010
comment
Мой связанный пост о конструкторах Java и шаблоне метода шаблона: novyden.blogspot.com/2011/08/   -  person topchef    schedule 08.08.2011


Ответы (2)


В С++, как вы правильно заметили, объект имеет тип A, пока конструктор A не будет завершен. Объект фактически меняет тип во время своего построения. Вот почему используется vtable класса A, поэтому вместо B::foo() вызывается A::foo().

В Java и C# виртуальная таблица (или эквивалентный механизм) наиболее производного типа используется повсюду, даже при создании базовых классов. Итак, на этих языках B.foo() вызывается.

Обратите внимание, что обычно не рекомендуется вызывать виртуальный метод из конструктора. Если вы не будете очень осторожны, виртуальный метод может предположить, что объект полностью сконструирован, даже если это не так. В Java, где каждый метод неявно виртуален, у вас нет выбора.

person Thomas    schedule 24.05.2010
comment
Вы можете пометить методы как final в Java, что предотвратит их переопределение подклассами. Я считаю, что в C# есть аналогичный механизм (возможно, sealed?). - person Michael Myers; 24.05.2010
comment
Да, было бы разумно пометить методы, которые (прямо или косвенно) вызываются из конструктора, как final. В C# нет необходимости в подобном механизме, потому что в C# методы являются virtual только в том случае, если вы явно объявляете их таковыми. - person Thomas; 24.05.2010
comment
В Java и C# везде используется vtable (или эквивалентный механизм) наиболее производного типа - не могли бы вы пояснить, хотя я знаю, что такое vtable. - person Vaibhav Bajpai; 25.05.2010

Хотя я понимаю, что вы делаете это для экспериментов, важно отметить следующую цитату из Effective Java 2nd Edition, Item 17: Design and Document для наследования или запретить его:

Есть еще несколько ограничений, которым должен подчиняться класс, чтобы разрешить наследование. Конструкторы не должны вызывать переопределяемые методы прямо или косвенно. Если вы нарушите это правило, произойдет сбой программы. Конструктор суперкласса запускается перед конструктором подкласса, поэтому переопределяющий метод в подклассе будет вызван до запуска конструктора подкласса. Если метод переопределения зависит от какой-либо инициализации, выполняемой конструктором подкласса, метод не будет вести себя должным образом.

Вот пример для иллюстрации:

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, и метод получает неправильное значение. Это почти наверняка приведет к багам и ошибкам.

person polygenelubricants    schedule 24.05.2010
comment
поэтому будет вызван переопределяющий метод в подклассе - как суперкласс во время своего построения может даже вызывать метод подкласса (когда подкласс еще не создан)? (Я понял из приведенного выше примера, что инициализация подкласса не была выполнена, но я надеялся, что метод даже не должен был вызываться, потому что подкласс еще не построен) - person Vaibhav Bajpai; 25.05.2010
comment
Ваш вопрос основан на ошибочном предположении, что метод не может быть вызван без создания объекта производного класса. В Яве можно. - person klaar; 06.08.2015