Base virtual member invocation rules #409
Replies: 24 comments
-
Here is a test for this: https://github.com/paulomorgado/CSharpVirtualCallGotchas |
Beta Was this translation helpful? Give feedback.
-
It's mentioned in 7.6.8 of the C# spec:
My understanding is that this might be born out of IL limitations. The C# compiler can't use |
Beta Was this translation helpful? Give feedback.
-
Eric Lippert blogged about it in 2010 and argued this is not a bug per se. |
Beta Was this translation helpful? Give feedback.
-
I was under the impression that Visual Basic did it different but I did the same test for Visual Basic and had the same results. |
Beta Was this translation helpful? Give feedback.
-
Most static analysis tools (like SonarQube) recommend that overrides that just call the base should not exist. Because of this, I think that all non sealed classes should override all virtual members, even if just to call the base. |
Beta Was this translation helpful? Give feedback.
-
@paulomorgado that's just a hassle. It's what abstract is for, not virtual. I would dislike to do this every time. Or should the Compiler do this? Anyway, once this would be in effect, older code couldn't be used with the new compiler. It would either behave differently OR not even compile anymore. I see where you're coming from, but I disagree with what this would imply/change. |
Beta Was this translation helpful? Give feedback.
-
@Mafii, I'm not discussing or proposing any change to the language and/or compiler. I'm trying to assert what the exact behavior is and what should be the best practice. |
Beta Was this translation helpful? Give feedback.
-
You can narrow this down substantially, since the situation can only occur if it's possible to override the method from a different assembly:
💭 I lean in favor of the rule that the empty overrides should be avoided. Doing so provides a performance advantage, and it's easy enough to use tools like the Public API analyzer to avoid making changes that could potentially break external code in this manner. |
Beta Was this translation helpful? Give feedback.
-
Won't do you much good if the class you're deriving from exists in someone else's assembly, particularly one not written using this newer version of C#. Also, the JIT doesn't inline virtual methods so such would certainly cause an impact on performance. This issue has existed since C# 1.0 and it's quite rare that it actually causes problems. I don't really think that it's worth addressing. |
Beta Was this translation helpful? Give feedback.
-
If you make it a practice to always override everything, there won't be any deriving class on another assembly.
I'm aware of this, but once you go there, the decision to have an inheritance hierarchy and which members are virtual has already made. I just made a quick search on Json.NET and found this on the public override void Close()
{
base.Close();
} I don't expect that many code out there to be extending <dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly> removing that would be nothing short of a catastrophe of global proportions. |
Beta Was this translation helpful? Give feedback.
-
I'm talking about deriving from a class hierarchy that already partially exists in another assembly. For example,
Yes, but the cost of invoking those members now increases for every level of hierarchy regardless of whether or not those members have been overridden.
As would removing any accessible member. That constitutes a breaking change. This is probably why that empty overrides exists; it didn't used to be empty, but they no longer need that logic there and removing the member would break compatibility. |
Beta Was this translation helpful? Give feedback.
-
Wow, I would have expected removing an override to be in the disallowed list, not in the allowed list: https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#members |
Beta Was this translation helpful? Give feedback.
-
@jnm2, and they forgot the problem of removing an override. |
Beta Was this translation helpful? Give feedback.
-
My point is that if you're designing your class hierarchy having in mind that it will be derived by others you have to take that into account and pay the price upfront. I'm talking here about correctness and resilience over raw performance. |
Beta Was this translation helpful? Give feedback.
-
Why would removing an override be a breaking change? call instruction would fallback to grandparent in that case. If you bothered to read article I linked before, great part of this discussion wouldn't need to happen. :-) |
Beta Was this translation helpful? Give feedback.
-
It's really too bad there isn't a first-class opcode meaning "constrain this call to the nearest available superclass implementation," since that's every developer's mental model and the behavior at compile time. |
Beta Was this translation helpful? Give feedback.
-
@zippec, have you tried my demo? |
Beta Was this translation helpful? Give feedback.
-
IIRC there really is no mechanism to do that. The instance has a single vtable slot for that virtual method and it points directly at the most overridden version. Short of reflection (or some variant thereof by the JIT compiler) there really isn't any easy way to determine that there are any other overrides of that method in the inheritance chain. @zippec That does appear to be the case. If you derived from Where it could still cause an issue at runtime is if the grandfather method is |
Beta Was this translation helpful? Give feedback.
-
Wouldn't that throw a |
Beta Was this translation helpful? Give feedback.
-
Not when I tried it locally. |
Beta Was this translation helpful? Give feedback.
-
With my repo? |
Beta Was this translation helpful? Give feedback.
-
Removing an implementation for abstract method forcing all childs to implement something they didn't need to implement before is no doubt a breaking change anyway. But there is a problematic case when parent removes overriding method and introduces private or internal method with the same name. Then child is basically compiled with call to inaccessible member of another class which fails in runtime. |
Beta Was this translation helpful? Give feedback.
-
Did anyone ran the tests in my repo? |
Beta Was this translation helpful? Give feedback.
-
My Although
the runtime is cleaver enough to call Tools like .NET Reflector still complain that It's still bad to remove an overload because, if you later need to reintroduce it, you'll get into to the case of adding an overload. But my premisse was all wrong. |
Beta Was this translation helpful? Give feedback.
-
As far as I understand, the invocation of a
base
member is made by a call to the closest known implementation at compile time.Given:
Running the program will produce the following output:
IF the following change is made to the C assembly without recompiling any of the other assemblies:
Running the program will produce the following output:
IF the following change is made to the B assembly without recompiling any of the other assemblies:
Running the program will produce the following output:
That's because the
base.GetInfo()
call inC.GetInfo
was compiled to be a call toA.GetInfo()
.Is this right? Is this documented anywhere?
Beta Was this translation helpful? Give feedback.
All reactions