Consider the following:
interface ISomething
{
void Call(string arg);
}
sealed class A : ISomething
{
public void Call(string arg) => Console.WriteLine($"A, {arg}");
}
sealed class Caller<T> where T : ISomething
{
private readonly T _something;
public Caller(T something) => _something = something;
public void Call() => _something.Call("test");
}
new Caller<A>(new A()).Call();
Both the call to Caller<A>.Call, as well as its nested tcall to A.Call are lodged through the callvirt instruction.
But why? Both types are exactly known. Unless I'm misunderstanding something, shouldn't it be possible do use call rather than callvirt here?
If so - why is this not done? Is that merely an optimisation not done by the compiler, or is there any specific reason behind this?
You're missing two things.
The first is that
callvirtdoes a null-check on the receiver, whereascalldoes not. This means that usingcallvirton anullreceiver will raise aNullReferenceException, whereascallwill happily call the method and passnullas the first parameter, meaning that the method will get athisparameter which isnull.Sound surprising? It is. IIRC in very early .NET versions
callwas used in the way you suggest, and people got very confused about howthiscould benullinside a method. The compiler switched tocallvirtto force the runtime to do a null-check upfront.There are only a handful of places where the compiler will emit a
call:null, and we also explicitly do not want to make a virtual call).foo?.Method()whereMethodis non-virtual.That last point in particular means that making a method
virtualis a binary-breaking change.Just for fun, see this check for
this == nullinString.Equals.The second thing is that
_something.Call("test");is not a virtual call, it's a constrained virtual call. There's aconstrainedopcode which appears before it.Constrained virtual calls were introduced with generics. The problem is that method calls on classes and on structs are a bit different:
ldloc), then usecall/callvirt.ldloc.a), then usecall.object, you need to load the struct value (e.g. withldloc), box it, then usecall/callvirt.If a generic type is unconstrained (i.e. it could be a class or a struct), the compiler doesn't know what to do: should it use
ldlocorldloc.a? Should it box or not?callorcallvirt?Constrained virtual calls move this responsibility to the runtime. To quote the doc above: