Monday, 6 August 2012

Virtual Method Call In contructor

I recently discovered the following warning from ReSharper:

So what exactly does Virtual member call in constructor actually mean, and why am I being warned about this?

Virtual members


In C# a virtual member is a method or property which is prepended with the modifier virtual, meaning that it may be overridden in a child class but unlike the abstract modifier it doesn't have to be.  Since a virtual member may be overridden many times, only the most derived type will be executed.

Constructors


Since only the most derived virtual member is executed this can lead to a unique problem in the scenario where the virtual member is contained within a constructor. To understand this consider the following two classes:

ClassA
class ClassA
    {
        public ClassA()
        {
            SplitTheWords();
        }

        public virtual void SplitTheWords()
        {
            //I've been overridden
        }
    }

ClassB
class ClassB : ClassA
    {
        private readonly String _output;

        public ClassB()
        {
            _output = "Constructor has occured";
        }

        public override void SplitTheWords()
        {
            String[] something = _output.Split(new[]{' '}); //Bang!
        }
    }

If I create an instance of ClassB, then the constructor of ClassA will be called and SplitTheWords() will be executed.  Now because SplitTheWords() is virtual, then only SplitTheWords() in ClassB will actually be executed (the most derived class) before the constructor of ClassB has been.  Which means we could be using an object in an unsuitable state.

Resharper recommends to make the virtual member in ClassA sealed, which means that it cannot be overridden and must be the most derived type, which entirely solves this problem.

I believe this is exactly the kind of thing which would get overlooked in your codebase, and just goes to show what a useful tool ReSharper actually is.  A more concise explanation of this situation is available in the ReSharper docs.

Note about comments below:

Due to a typo I made, an earlier version of the post didn't exhibit the behaviour described, and I very much thank those who took the time to point this out to me.  This have now been fixed, although I believe a deeper explanation as to the reasons behind this article is warranted:

[10.11] of the C# Specification tells us that object constructors run in order from the base class first, to the most inherited class last.  Whereas [10.6.3] of the specification tells us that it is the most derived implementation of a virtual member (in this case ClassB) which is executed at run-time.

Therefore when you call a virtual member in a constructor the following can happen because the most derived implementation of a virtual member was contained within an object whose constructor hadn't been executed yet:


9 comments:

  1. Glad you found it helpful. I would appreciate it if you could recommend or share this article.

    ReplyDelete
  2. "If I create an instance of ClassA, then the constructor will be called and SplitTheWords() will be executed. Now because SplitTheWords() is virtual, then only SplitTheWords() in ClassB will actually be executed (the most derived class) before the constructor of ClassB has been."

    There is some error in above sentence as when instance of class A will be created, then SplitTheWords() method of class A only will be executed. SplitTheWords() method of class B will be executed only while creating the instance of class B.

    ReplyDelete
    Replies
    1. Thanks for your comment Anonymous. What you've said would seem to make sense, but the point of this article is to show this isn't the way it works.

      We know that a constructor will always run when you create a new object, and you'll see that I'm instantiating _output in the constructor of ClassB.

      However if you make a virtual call in a constructor (ClassA), and the type you're on is not the most derived type, you'll end up calling it on a class whose constructor has not been run.

      Therefore all isn't as it seems, and hence the exception occurred (because classB was never instantiated therefore the constructor never ran).

      Hope this makes sense.

      Delete
  3. First, thank you. But when you say : "If you create these classes yourself, and create an instance of ClassA you will see an exception is thrown in ClassB even though you never created an instance of ClassB.", this is actually not true, you can instance an object of ClassA without having any exception of ClassB.

    ReplyDelete
    Replies
    1. I understand what you're saying, but will have to disagree. Do you have any source code, links or anything else to back up your view point?

      Delete
  4. Hello Matthew,

    all the people above are trying to say is that the program you provides just works. Yes, exactly, no exception happens. It stops working if you execute something like this:

    ClassA a = new ClassB();

    The thing is that when initializing ClassA a = ClassA(), you have no chance to call any of ClassB methods, even if they override parent methods.

    Kind regards,
    Alexey Khoroshikh

    ps: have you tried to compile your program? I just have. ;-)

    ReplyDelete
  5. As an update - the program I ran:

    void Main()
    {
    ClassA a = new ClassB();
    }

    class ClassA
    {
    public ClassA()
    {
    SplitTheWords();
    Console.WriteLine("That's all, folks!");
    }

    public virtual void SplitTheWords()
    {
    //I've been overridden
    }
    }

    class ClassB : ClassA
    {
    private readonly String _output;

    public ClassB()
    {
    _output = "Constructor has occured";
    }

    public override void SplitTheWords()
    {
    String[] something = _output.Split(new[]{' '}); //Bang!
    }
    }


    If you're curious, the following article regarding the case (and the attached example of code) might seem interesting for you, as well: http://msdn.microsoft.com/en-us/library/ms182331(v=vs.80).aspx

    ReplyDelete
    Replies
    1. Many thanks for taking the time to point out the problem with my article. I've now made the necessary revisions.

      Delete