The premise of my question, in plain english:
Foo
depends on a library named Bar
FooBar
, depends only on Foo
Consider the following sample:
class Program
{
static void Main(string[] args)
{
Foo foo = Foo.Instance;
int id = foo.Id; // Compiler is happy
foo.DoWorkOnBar(); // Compiler is not happy
}
}
Foo is defined as follows
public class Foo : Bar
{
public new static Foo Instance { get => (Foo)Bar.Instance; }
public new int Id { get => Bar.Id; }
public void DoWorkOnBar()
{
Instance.DoWork();
}
}
Bar is defined as follows
public class Bar
{
public static Bar Instance { get => new Bar(); }
public static int Id { get => 5; }
public void DoWork() { }
}
The part that is completely stumping me:
Without a reference to the Bar
library
FooBar
can retrieve the ID that is provided by Bar
(or at least it compiles)FooBar
cannot request Foo to do work that is ultimately accomplished by Bar
The compiler error associated with foo.DoWorkOnBar();
is
The type 'Bar' is defined in an assembly that is not referenced. You must add a reference to assembly 'Bar, Version 1.0.0.0, Culture=Neutral, PublicKeyToken=null' .
Why does there appear to be a disparity in the compiler?
I would have assumed that neither of these operations would compile without FooBar
adding a reference to Bar
.
With a price tag of about $340 million, the C-17 Globemaster III is one of the most in-demand cargo planes in the world. It can transport troops and cargo as well as aid in the evacuation of people in weather-ravaged or war-torn areas.
C exists everywhere in the modern world. A lot of applications, including Microsoft Windows, run on C. Even Python, one of the most popular languages, was built on C. Modern applications add new features implemented using high-level languages, but a lot of their existing functionalities use C.
The C-17 is capable of rapid strategic delivery of troops and all types of cargo to main operating bases or directly to forward bases in the deployment area. The aircraft can perform tactical airlift and airdrop missions and can transport litters and ambulatory patients during aeromedical evacuations.
Boeing C-17A Globemaster III (Globemaster C-17): Length overall: 174ft (53.04m) Height overall: 55ft 1in (16.79m) Wingspan: 169ft 9in (51.74m)
First, note that the implementations of Foo.Id
and Foo.DoWorkOnBar
are irrelevant; the compiler treats foo.Id
and foo.DoWorkOnBar()
differently even if the implementations don’t access Bar
:
// In class Foo:
public new int Id => 0;
public void DoWorkOnBar() { }
The reason that foo.Id
compiles successfully but foo.DoWorkOnBar()
doesn’t is that the compiler uses different logic¹ to look up properties versus methods.
For foo.Id
, the compiler first looks for a member named Id
in Foo
. When the compiler sees that Foo
has a property named Id
, the compiler stops the search and doesn’t bother looking at Bar
. The compiler can perform this optimization because a property in a derived class shadows all members with the same name in a base class, so foo.Id
will always refer to Foo.Id
, no matter what members might be named Id
in Bar
.
For foo.DoWorkOnBar()
, the compiler first looks for a member named DoWorkOnBar
in Foo
. When the compiler sees that Foo
has a method named DoWorkOnBar
, the compiler continues searching all base classes for methods named DoWorkOnBar
. The compiler does this because (unlike properties) methods can be overloaded, and the compiler implements² the overload resolution algorithm in essentially the same way it’s described in the C# specification:
DoWorkOnBar
declared in Foo
and its base classes.Step 1 triggers the requirement for you to add a reference to assembly Bar
.
Could a C# compiler implement the algorithm differently? According to the C# specification:
The intuitive effect of the resolution rules described above is as follows: To locate the particular method invoked by a method invocation, start with the type indicated by the method invocation and proceed up the inheritance chain until at least one applicable, accessible, non-override method declaration is found. Then perform type inference and overload resolution on the set of applicable, accessible, non-override methods declared in that type and invoke the method thus selected.
So it seems to me that the answer is “Yes”: a C# compiler could theoretically see that Foo
declares an applicable DoWorkOnBar
method and not bother looking at Bar
. For the Roslyn compiler, however, this would involve a major rewrite of the compiler’s member lookup and overload resolution code—probably not worth the effort given how easily developers can resolve this error themselves.
TL;DR — When you invoke a method, the compiler needs you to reference the base class assembly because that’s the way the compiler was implemented.
¹ See the LookupMembersInClass method of the Microsoft.CodeAnalysis.CSharp.Binder class.
² See the PerformMemberOverloadResolution method of the Microsoft.CodeAnalysis.CSharp.OverloadResolution class.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With