代码之家  ›  专栏  ›  技术社区  ›  G-Wiz RameshVel

C#编译如何绕过需要的头文件?

  •  29
  • G-Wiz RameshVel  · 技术社区  · 15 年前

    根据我的研究,C/C++/ObjC编译需要预先声明所有遇到的符号。我也明白建造是一个分两步的过程。首先,将每个源文件编译成各个对象文件。这些对象文件可能有未定义的“符号”(通常对应于头文件中声明的标识符)。其次,将对象文件链接在一起以形成最终输出。这是一个相当高层次的解释,但足以满足我的好奇心。但我也希望对C#build过程有类似的高层理解。

    问: C#构建过程如何绕过头文件的需要?我想,也许编译步骤需要两次?

    (编辑:此处为后续问题。) How do C/C++/Objective-C compare with C# when it comes to using libraries? )

    5 回复  |  直到 7 年前
        1
  •  39
  •   Lasse V. Karlsen    15 年前

    “头文件元数据”存在于已编译的程序集中,因此任何添加引用的程序集都将允许编译器从这些程序集中提取元数据。

    对于当前解决方案中尚未编译的内容,它将进行两次编译,首先读取名称空间、类型名称、成员名称,即除了代码之外的所有内容。然后,当这个检查出来时,它将读取代码并编译它。

    这允许编译器知道(在它的宇宙中)存在什么和不存在什么。

    要查看双过程编译器是否有效,请测试以下代码,其中有3个问题、两个与声明相关的问题和一个代码问题:

    using System;
    
    namespace ConsoleApplication11
    {
        class Program
        {
            public static Stringg ReturnsTheWrongType()
            {
                return null;
            }
    
            static void Main(string[] args)
            {
                CallSomeMethodThatDoesntExist();
            }
    
            public static Stringg AlsoReturnsTheWrongType()
            {
                return null;
            }
        }
    }
    

    Stringg 无法找到的类型。如果您修复了这些问题,那么它会抱怨在主方法中调用的方法名无法找到。

        2
  •  98
  •   Glorfindel RameshVel    4 年前

    the subject of my blog for February 4th 2010 . 谢谢你的提问!

    让我为你安排一下。从最基本的意义上讲,编译器是一个“双过程编译器”,因为编译器所经历的阶段是:

    1. 一代 .
    2. 一代 .

    除了

    IL是方法体中的所有内容——实际的命令式代码,而不是关于代码结构的元数据。

    第一阶段实际上是通过对源代码进行多次传递来实现的。不止两个。

    我们要做的第一件事是获取源代码的文本并将其分解为一个令牌流。也就是说,我们通过词汇分析来确定

    class c : b { }
    

    然后我们进行“顶级解析”,验证令牌流是否定义了语法正确的C#程序。但是,我们跳过解析方法体。当我们击中一个方法主体时,我们只是在标记中燃烧,直到找到匹配的close curly。我们稍后会再讨论这个问题;我们现在只关心获取足够的信息来生成元数据。

    然后我们做一个“声明”过程,在这里我们对程序中每个名称空间和类型声明的位置进行注释。

    然后执行一个过程,验证所有声明的类型在其基类型中没有循环。我们需要首先这样做,因为在随后的每个过程中,我们都需要能够遍历类型层次结构,而不必处理循环。

    然后执行一个过程,验证泛型类型上的所有泛型参数约束也是非循环的。

    然后执行一个过程,检查每种类型的每个成员(类的方法、结构的字段、枚举值等等)是否一致。枚举中没有循环,每个重写方法都重写实际上是虚拟的东西,等等。此时,我们可以计算所有接口、具有虚拟方法的类等的“vtable”布局。

    然后我们做一个过程,计算出所有“const”字段的值。

    此时,我们有足够的信息来发出此程序集的几乎所有元数据。我们仍然没有关于迭代器/匿名函数闭包或匿名类型的元数据的信息;我们做得很晚。

    我们现在可以开始生成IL。对于每个方法体(以及属性、索引器、构造函数等),我们将lexer倒带到方法体开始的位置并解析方法体。

    一旦解析了方法体,我们将执行一个初始的“绑定”过程,尝试确定每个语句中每个表达式的类型。然后我们在每个方法体上进行一整堆的传递。

    (接下来的几次传球会寻找坏东西。)

    然后,我们运行一个pass,搜索尚未发出元数据的匿名类型的使用,并发出这些类型。

    然后我们运行一个pass,搜索表达式树的错误用法。例如,在表达式树中使用++运算符。

    然后我们运行一个过程,在迭代器块中查找非法模式。

    然后我们运行可达性检查器,对无法访问的代码发出警告,并告诉您何时执行了类似于在非void方法末尾忘记返回的操作。

    然后我们运行一个pass,验证每个goto都指向一个合理的标签,并且每个标签都指向一个可到达的goto。

    然后,我们运行一个pass,检查所有局部变量在使用前是否都被明确分配,注意哪些局部变量在匿名函数或迭代器的外部变量上是闭合的,哪些匿名函数在可访问代码中(这个通行证太多了。一段时间以来,我一直想重构它。)

    接下来,我们运行一个pass,检测COM对象调用缺少的ref参数并修复它们(这是C#4中的一个新特性。)

    然后,我们运行一个pass,查找形式为“newmydelegate(Foo)”的内容,并将其重写为对CreateDelegate的调用。

    然后我们运行一个pass,将所有可为空的算术重写为测试HasValue的代码,以此类推。

    然后我们运行一个pass,查找表单base.Blah()的所有引用,并将它们重写为代码,该代码对基类方法进行非虚拟调用。

    然后,我们运行一个过程,查找对象和集合初始值设定项,并将它们转换为适当的属性集,依此类推。

    然后,我们运行一个pass,查找对已删除方法的调用(也就是说,没有实际实现的分部方法,或者没有定义条件编译符号的条件方法)都会变成无操作。

    然后,我们运行一个优化过程,重写平凡的“is”和“as”操作符。

    然后,我们运行一个优化过程,查找开关(常量),并将其作为一个分支直接重写为正确的大小写。

    然后我们运行一个pass,它将字符串连接转换为对string.Concat的正确重载的调用。

    然后,我们运行一个pass,它将命名参数和可选参数的使用重写为调用,在这些调用中,所有副作用都以正确的顺序发生。

    然后我们运行一个优化算法的过程;例如,如果我们知道M()返回一个int,我们有1*M(),那么我们就把它转换成M()。

    然后,我们将这个主体中的匿名函数转换为闭包类的方法。

    最后,我们将迭代器块转换为基于开关的状态机。

    然后我们为刚刚计算的转换树发出IL。

    容易极了!

        3
  •  5
  •   Hans Passant    15 年前

    它使用引用程序集中的元数据。它包含完整的类型声明,与头文件中的内容相同。

    作为一个双过程编译器,它完成了其他一些事情:您可以在一个源文件中使用一个类型,然后再在另一个源代码文件中声明它。

        4
  •  1
  •   codekaizen    15 年前
        5
  •  1
  •   Henk Holterman    15 年前

    可以从引用的部件中获得所有必要的信息。

    需要访问正在使用的DLL。