Rand Random
Rand Random

Reputation: 7440

Calling a base method throws stackoverflow if derived class inherits from same interface

I would like to understand why this code runs into a Stack Overflow

using System;
                    
public class Program
{
    public static void Main()
    {
        var docNode = new DocumentNode();
        docNode.Transactions.ExecuteInvisibleTransaction();
        
        var docNodeExt = new DocumentNodeExt();
        docNodeExt.Transactions.ExecuteInvisibleTransaction();
    }
}

public class DocumentNode : ITransactions
{
    void ITransactions.ExecuteInvisibleTransaction() => Console.WriteLine("ExecuteInvisibleTransaction in DocumentNode");
    public ITransactions Transactions => this;
}

public class DocumentNodeExt : DocumentNode, ITransactions
{       
    public void ExecuteInvisibleTransaction() => base.Transactions.ExecuteInvisibleTransaction();
}

public interface ITransactions
{
    void ExecuteInvisibleTransaction();
}

Demo: https://dotnetfiddle.net/k4mJ5Y

But when I drop the interface ITransactions from the class DocumentNodeExt the Stack Overflow doesn't occure?

using System;
                    
public class Program
{
    public static void Main()
    {
        var docNode = new DocumentNode();
        docNode.Transactions.ExecuteInvisibleTransaction();
        
        var docNodeExt = new DocumentNodeExt();
        docNodeExt.Transactions.ExecuteInvisibleTransaction();
    }
}

public class DocumentNode : ITransactions
{
    void ITransactions.ExecuteInvisibleTransaction() => Console.WriteLine("ExecuteInvisibleTransaction in DocumentNode");
    public ITransactions Transactions => this;
}

public class DocumentNodeExt : DocumentNode
{       
    public void ExecuteInvisibleTransaction() => base.Transactions.ExecuteInvisibleTransaction();
}

public interface ITransactions
{
    void ExecuteInvisibleTransaction();
}

Demo: https://dotnetfiddle.net/g0JX58

Upvotes: 1

Views: 104

Answers (2)

quetzalcoatl
quetzalcoatl

Reputation: 33536

That is because the base keyword is special. The base keyword changed the lookup rules on the immediately following term and only on that.

Look:

base.Transactions.ExecuteInvisibleTransaction()
     ^^^^^^^^^^^^    ^^^^
     only here       nope

Since the derived class doesn't provide its own Transactions property, the base.Transactions is exactly the same as this.Transactions. And here we have it. The ExecuteInvisibleTransaction is then called on whatever object Transactions returned, and, well, it's the same this (as implemented in the base).

From how the code looks like, I see that what you intended, was to call the base ExecuteInvisibleTransaction, not the base Transactions. But base doesn't work like that. It only works on the immediate term, only at the first . dot after base.

Write:

base.Transactions.ExecuteInvisibleTransaction()

think:

(base.Transactions).ExecuteInvisibleTransaction()

EDIT:

I just noticed that my explanation doesn't cover the second part, and yeah, it's about that dual-implementation of ITransaction, like JoeSewell explained. In a bit simpler wording:

  • normal implementation of an interface, implicitly makes that method virtual, so derived classes re-implementing that interface will "just bind" to the base, and calling that method via interface will pick the "latest" implementation (from the "furthest" child class that provided its own version)

  • buuut.. explicit implementation is special. It's private-ish, so it can't be virtual

Therefore, what Joe said. In first case, base.Transactions.ExecuteInvisibleTransaction called (virtual ITransactions.ExecuteInvisibleTransaction, hence the one from child class..

Thanks Joe for very detailed answer!

Upvotes: 2

Joe Sewell
Joe Sewell

Reputation: 6620

I don't know the exact line in the C# spec that addresses this, but as someone who's spent a lot of time looking at interface implementation rules in C# and IL, I conceptually have the rule that a class method C.M may only implicitly implement an interface method I.M if C directly implements I or a descendant interface of I.

In your first example, DocumentNodeExt directly implements ITransactions, so DocumentNodeExt.ExecuteInvisibleTransaction implicitly implements ITransactions.ExecuteInvisibleTransaction. To prove this, if you check sharplab you'll see that the class method is marked virtual on the IL side.

Thus, here's what happens to cause the stack overflow:

  1. Calling docNodeExt.Transactions results in an implicit conversion of docNodeExt to DocumentNode (as DocumentNodeExt doesn't define that property), so the getter called is DocumentNode.Transactions, which produces the same object as docNodeExt, implicitly converted to ITransactions.
  2. Calling ExecuteInvisibleTransaction on that instance of ITransactions leads to DocumentNodeExt.ExecuteInvisibleTransaction being called, as that is the most specific implementation of that interface method for the underlying class type.
  3. Which calls base.Transactions - base meaning the type DocumentNode - resulting in the same object as determined in step 1.
  4. Calling ExecuteInvisibleTransaction on that instance of ITransactions does the same thing as step 2. Repeat steps 2, 3, and 4 until you run out of space on the stack. (Unbounded recursion.)

In your second example, DocumentNodeExt does not directly implement ITransactions, so DocumentNodeExt.ExecuteInvisibleTransaction does not implicitly implement ITransactions.ExecuteInvisibleTransaction. It's just a non-virtual class method that happens to have the same name and signature. Comment out the , ITransactions part in the sharplab link above and now the IL will no longer say the class method is virtual.

Thus, in step 2 above, instead of the most specific implementation being DocumentNodeExt.ExecuteInvisibleTransaction, it is the explicit implementation in DocumentNode. This terminates with calling Console.WriteLine, so there's no unbounded recursion.

Upvotes: 2

Related Questions