为什么 C# 编译器可以 "see" 未引用 DLL 中 class 的静态属性,而不是实例方法?
Why can the C# compiler "see" static properties, but not instance methods, of a class in a DLL that is not referenced?
我的问题的前提,用简单的英语:
- 名为
Foo
的库依赖于名为 Bar
的库
- Foo 中的 class 扩展了 Bar
中的 class
- Foo 定义 properties/methods 简单地传递给 Bar
- 一个应用程序,
FooBar
,只依赖于 Foo
考虑以下示例:
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定义如下
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定义如下
public class Bar
{
public static Bar Instance { get => new Bar(); }
public static int Id { get => 5; }
public void DoWork() { }
}
完全难倒我的部分:
没有引用 Bar
库
FooBar
可以 检索由 Bar
提供的 ID(或至少它编译)
FooBar
不能 请求 Foo 完成最终由 Bar
完成的工作
与 foo.DoWorkOnBar();
相关的编译器错误是
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' .
为什么编译器中似乎存在差异?
我会假设如果没有 FooBar
添加对 Bar
的引用,这些操作都不会编译。
首先,请注意 Foo.Id
和 Foo.DoWorkOnBar
的 实现 是无关紧要的;即使实现不访问 Bar
:
,编译器也会以不同的方式对待 foo.Id
和 foo.DoWorkOnBar()
// In class Foo:
public new int Id => 0;
public void DoWorkOnBar() { }
foo.Id
编译成功但 foo.DoWorkOnBar()
编译失败的原因是编译器使用不同的逻辑¹ 来查找属性和方法。
对于foo.Id
,编译器首先在Foo
中查找名为Id
的成员。当编译器发现 Foo
有一个名为 Id
的 属性 时,编译器停止搜索并且不会费心查看 Bar
。编译器可以执行此优化,因为派生 class 中的 属性 隐藏了基 class 中具有相同名称的所有成员,因此 foo.Id
将始终引用 Foo.Id
,无论什么成员可能在 Bar
.
中被命名为 Id
对于foo.DoWorkOnBar()
,编译器首先在Foo
中查找名为DoWorkOnBar
的成员。当编译器发现 Foo
有一个名为 DoWorkOnBar
的方法时,编译器继续在所有基 class 中搜索名为 DoWorkOnBar
的方法。编译器这样做是因为(与属性不同)方法可以重载,并且编译器实现重载决议算法的方式与 C# 规范中描述的方式基本相同:
- 从“方法组”开始,该方法组由
Foo
中声明的 DoWorkOnBar
及其基础 classes[=73= 中声明的所有重载集组成].
- 将集合缩小到“候选”方法(基本上,其参数与提供的参数兼容的方法)。
- 删除任何被更派生的候选方法隐藏的候选方法 class。
- 从剩余的候选方法中选择“最佳”。
第 1 步触发要求您添加对程序集的引用 Bar
。
C# 编译器能否以不同方式实现算法?根据 C# 规范:
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.
所以在我看来答案是“是”:C# 编译器理论上可以看到 Foo
声明了一个适用的 DoWorkOnBar
方法,而不必费心去查看 Bar
。然而,对于 Roslyn 编译器,这将涉及对编译器的成员查找和重载解析代码的重大重写——考虑到开发人员可以轻松地自行解决此错误,这样的努力可能不值得。
TL;DR — 当您调用一个方法时,编译器需要您引用基础 class 程序集,因为编译器就是这样实现的。
¹ 请参阅 Microsoft.CodeAnalysis.CSharp.Binder class 的 LookupMembersInClass 方法。
² 参见 Microsoft.CodeAnalysis.CSharp.OverloadResolution class.
的 PerformMemberOverloadResolution 方法
我的问题的前提,用简单的英语:
- 名为
Foo
的库依赖于名为Bar
的库
- Foo 中的 class 扩展了 Bar 中的 class
- Foo 定义 properties/methods 简单地传递给 Bar
- 一个应用程序,
FooBar
,只依赖于 Foo
考虑以下示例:
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定义如下
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定义如下
public class Bar
{
public static Bar Instance { get => new Bar(); }
public static int Id { get => 5; }
public void DoWork() { }
}
完全难倒我的部分:
没有引用 Bar
库
FooBar
可以 检索由Bar
提供的 ID(或至少它编译)FooBar
不能 请求 Foo 完成最终由Bar
完成的工作
与 foo.DoWorkOnBar();
相关的编译器错误是
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' .
为什么编译器中似乎存在差异?
我会假设如果没有 FooBar
添加对 Bar
的引用,这些操作都不会编译。
首先,请注意 Foo.Id
和 Foo.DoWorkOnBar
的 实现 是无关紧要的;即使实现不访问 Bar
:
foo.Id
和 foo.DoWorkOnBar()
// In class Foo:
public new int Id => 0;
public void DoWorkOnBar() { }
foo.Id
编译成功但 foo.DoWorkOnBar()
编译失败的原因是编译器使用不同的逻辑¹ 来查找属性和方法。
对于foo.Id
,编译器首先在Foo
中查找名为Id
的成员。当编译器发现 Foo
有一个名为 Id
的 属性 时,编译器停止搜索并且不会费心查看 Bar
。编译器可以执行此优化,因为派生 class 中的 属性 隐藏了基 class 中具有相同名称的所有成员,因此 foo.Id
将始终引用 Foo.Id
,无论什么成员可能在 Bar
.
Id
对于foo.DoWorkOnBar()
,编译器首先在Foo
中查找名为DoWorkOnBar
的成员。当编译器发现 Foo
有一个名为 DoWorkOnBar
的方法时,编译器继续在所有基 class 中搜索名为 DoWorkOnBar
的方法。编译器这样做是因为(与属性不同)方法可以重载,并且编译器实现重载决议算法的方式与 C# 规范中描述的方式基本相同:
- 从“方法组”开始,该方法组由
Foo
中声明的DoWorkOnBar
及其基础 classes[=73= 中声明的所有重载集组成]. - 将集合缩小到“候选”方法(基本上,其参数与提供的参数兼容的方法)。
- 删除任何被更派生的候选方法隐藏的候选方法 class。
- 从剩余的候选方法中选择“最佳”。
第 1 步触发要求您添加对程序集的引用 Bar
。
C# 编译器能否以不同方式实现算法?根据 C# 规范:
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.
所以在我看来答案是“是”:C# 编译器理论上可以看到 Foo
声明了一个适用的 DoWorkOnBar
方法,而不必费心去查看 Bar
。然而,对于 Roslyn 编译器,这将涉及对编译器的成员查找和重载解析代码的重大重写——考虑到开发人员可以轻松地自行解决此错误,这样的努力可能不值得。
TL;DR — 当您调用一个方法时,编译器需要您引用基础 class 程序集,因为编译器就是这样实现的。
¹ 请参阅 Microsoft.CodeAnalysis.CSharp.Binder class 的 LookupMembersInClass 方法。
² 参见 Microsoft.CodeAnalysis.CSharp.OverloadResolution class.
的 PerformMemberOverloadResolution 方法