【问题标题】:Why can the C# compiler "see" static properties, but not instance methods, of a class in a DLL that is not referenced?为什么 C# 编译器可以“看到” DLL 中未引用的类的静态属性,但看不到实例方法?
【发布时间】:2019-01-03 22:26:18
【问题描述】:

我的问题的前提,用简单的英语:

  • 名为Foo 的库依赖于名为Bar 的库
  • Foo 中的类扩展 Bar 中的类
  • Foo 定义了简单地传递给 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();
    }
}

条形定义如下

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();相关的编译器错误是

“Bar”类型是在未引用的程序集中定义的。您必须添加对程序集 'Bar, Version 1.0.0.0, Culture=Neutral, PublicKeyToken=null' 的引用。

为什么编译器会出现差异?

如果没有 FooBar 添加对 Bar 的引用,我会假设这些操作都不会编译。

【问题讨论】:

  • 因为属性是直接在 foo 上定义的,所以编译器很高兴。但是当你想使用 Instance.DoWork() 并且 DoWork 定义在 bar 上时,编译器需要知道它在哪里可以找到那个 DoWork 方法。因此它需要对 bar 的引用
  • 如果您将实现减少到public class Bar { }public class Foo : Bar { public static Foo Instance => null; public int Id => 42; public DoWorkOnBar() { } },问题是否仍然存在?疯狂猜测:方法可以重载而属性不能重载是有原因的。
  • 可用于重现问题的最小Foopublic class Foo : Bar { public static int P => 0; public static int M() => 0; }。调用Foo.P 没问题;调用Foo.M() 会使编译器对Bar 产生强烈要求。为什么?现在这可能需要编译器作者进一步解释。 (但@PetSerAl 对“责备重载解决方案”的猜测是安全的,因为这是迄今为止语言中最复杂的部分,并且有很多有趣的阴暗角落。)
  • @Matt 我建议你 edit 你的问题并将 FooBar 实现减少到最小的必要版本来重现错误,所以其他人不会因为额外的问题而陷入错误的轨道分心。
  • 感谢这个绝妙的问题。很难找到这个,因为甚至很难找到问题:D

标签: c# compiler-errors dependencies dependency-management


【解决方案1】:

首先,请注意Foo.IdFoo.DoWorkOnBar实现 是不相关的;即使实现不访问Bar,编译器也会以不同的方式处理foo.Idfoo.DoWorkOnBar()

// In class Foo:
public new int Id => 0;
public void DoWorkOnBar() { }

foo.Id 编译成功而foo.DoWorkOnBar() 编译失败的原因是编译器使用不同的逻辑¹来查找属性和方法。

对于foo.Id,编译器首先在Foo 中查找名为Id 的成员。当编译器看到Foo 有一个名为Id 的属性时,编译器会停止搜索并且不会费心查看Bar。编译器可以执行此优化,因为派生类中的属性会隐藏基类中的所有同名成员,因此foo.Id 将始终引用Foo.Id,无论@987654342 中的哪些成员可能被命名为Id @。

对于foo.DoWorkOnBar(),编译器首先在Foo 中查找名为DoWorkOnBar 的成员。当编译器发现Foo 有一个名为DoWorkOnBar 的方法时,编译器会继续在所有基类中搜索名为DoWorkOnBar 的方法。编译器这样做是因为(与属性不同)方法可以重载,并且编译器以与 C# 规范中描述的方式基本相同的方式实现²重载解析算法:

  1. 从“方法组”开始,该“方法组”由DoWorkOnBarFoo 中声明的所有重载集合组成及其基类
  2. 将集合缩小到“候选”方法(基本上,其参数与提供的参数兼容的方法)。
  3. 删除任何被派生类中的候选方法遮蔽的候选方法。
  4. 选择剩余候选方法中的“最佳”。

第 1 步触发要求您添加对程序集 Bar 的引用。

C# 编译器能否以不同的方式实现该算法?根据 C# 规范:

上述解析规则的直观效果如下:要定位方法调用所调用的特定方法,从方法调用所指示的类型开始,沿着继承链往上走,直到至少有一个适用、可访问、找到非覆盖方法声明。然后对在该类型中声明的一组适用、可访问、不可覆盖的方法执行类型推断和重载决策,并调用由此选择的方法。

所以在我看来答案是“是”:理论上,C# 编译器可以看到Foo 声明了一个适用的DoWorkOnBar 方法,而不必费心查看Bar。然而,对于 Roslyn 编译器,这将涉及对编译器的成员查找和重载解析代码进行重大重写——考虑到开发人员自己解决这个错误是多么容易,这可能不值得。


TL;DR — 当您调用方法时,编译器需要您引用基类程序集,因为这是编译器的实现方式。


¹ 请参阅 Microsoft.CodeAnalysis.CSharp.Binder class 的 LookupMembersInClass 方法。

² 请参阅Microsoft.CodeAnalysis.CSharp.OverloadResolution class 的 PerformMemberOverloadResolution 方法。

【讨论】:

  • 我同意它是由需要重载决议引起的断言,但是如果任何派生类重载适用,是否仍然有必要查看基类定义?即使在基类中定义的重载通过签名更好地匹配,派生类中定义的 AFAIK 重载也是严格首选的。例如:ideone.com/SBpJuD 这里它在派生类中调用long 重载,即使基类中的int 重载匹配得更好。
  • @PetSerAl:你可能是对的。查看source code,编译器似乎根据规范中规定的算法实现了重载解析:首先构造候选方法集(这需要查看基类),然后丢弃那些在较少派生中声明的方法类。
  • 哇!我把头撞在墙上,因为这对我来说没有意义。现在我明白了,但很生气,那是行不通的:D
猜你喜欢
  • 1970-01-01
  • 2015-08-01
  • 2020-07-14
  • 2011-02-09
  • 2021-10-20
  • 1970-01-01
  • 2012-10-02
  • 2022-10-20
  • 2014-10-18
相关资源
最近更新 更多