Reputation: 7440
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
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
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:
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
.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.base.Transactions
- base
meaning the type DocumentNode
- resulting in the same object as determined in step 1.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