为什么 C# 编译器可以“看到"?DLL 中未引用的类的静态属性,而不是实例方法? [英] Why can the C# compiler "see" static properties, but not instance methods, of a class in a DLL that is not referenced?

查看:34
本文介绍了为什么 C# 编译器可以“看到"?DLL 中未引用的类的静态属性,而不是实例方法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  • 名为Foo 的库依赖于名为Bar
  • 的库
  • Foo 中的类扩展了 Bar 中的类
  • Foo 定义了简单地传递给 Bar 的属性/方法
  • FooBar 应用程序仅依赖于 Foo

考虑以下示例:

class 程序{静态无效主(字符串 [] args){Foo foo = Foo.Instance;int id = foo.Id;//编译器很高兴foo.DoWorkOnBar();//编译器不高兴}}

Foo 定义如下

公共类 Foo : Bar{公共新静态 Foo 实例 { get =>(Foo)Bar.Instance;}public new int Id { get =>Bar.Id;}公共无效 DoWorkOnBar(){Instance.DoWork();}}

Bar定义如下

公共类栏{公共静态酒吧实例 { get =>新酒吧();}public static int Id { get =>5;}公共无效 DoWork() { }}

完全让我难受的部分:

不引用Bar

  • FooBar 可以检索由 Bar 提供的 ID(或者至少可以编译)
  • FooBar 不能要求Foo做最终由Bar
  • 完成的工作

foo.DoWorkOnBar();相关的编译器错误是

<块引用>

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

为什么编译器中似乎存在差异?

我认为如果没有 FooBar 添加对 Bar 的引用,这些操作都不会编译.

解决方案

首先要注意Foo.IdFoo.DoWorkOnBar的实现代码>无关紧要;即使实现不访问 Bar,编译器也会以不同的方式对待 foo.Idfoo.DoWorkOnBar():

//在 Foo 类中:公共新 int Id =>0;公共无效 DoWorkOnBar() { }

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

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

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

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

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

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

<块引用>

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

所以在我看来答案是是":C# 编译器理论上可以看到 Foo 声明了一个适用的 DoWorkOnBar 方法,而不必费心查看 条形码.然而,对于 Roslyn 编译器,这将涉及对编译器的成员查找和重载解析代码进行重大重写——考虑到开发人员可以轻松地自行解决此错误,这可能不值得付出努力.

<小时>

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

<小时>

¹ 参见 Microsoft.CodeAnalysis.CSharp.Binder 类.

² 参见Microsoft.CodeAnalysis.CSharp.OverloadResolution 类.

The premise of my question, in plain english:

  • A library named Foo depends on a library named Bar
  • A class within Foo extends a class within Bar
  • Foo defines properties/methods that simply pass-through to Bar
  • An application, 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.

解决方案

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:

  1. Start with the "method group" consisting of the set of all overloads of DoWorkOnBar declared in Foo and its base classes.
  2. Narrow the set down to "candidate" methods (basically, the methods whose parameters are compatible with the supplied arguments).
  3. Remove any candidate method that is shadowed by a candidate method in a more derived class.
  4. Choose the "best" of the remaining candidate methods.

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.

这篇关于为什么 C# 编译器可以“看到"?DLL 中未引用的类的静态属性,而不是实例方法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆