`
RednaxelaFX
  • 浏览: 3048809 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

C#里,派生类的方法里的匿名delegate调用基类的方法会产生无法验证的代码

    博客分类:
  • C#
阅读更多
看来阅读一个开发人员的blog是获取知识的一个捷径,特别是当那位开发人员负责的产品是你天天都用的基础设施之一,例如说……编译器。在阅读Eric Lippert的blog时,我无意中了解到了很多我以前所不熟悉的知识,例如说一些语言特性,一些编程思想之类;但更有趣的,我了解到了很多他所负责的产品中的诡异地方。

===================================================

开篇花絮

假如我们现在有一个枚举类型E,其中有一个枚举值的名字是x。
你或许知道这个表达式是对的:
0 | E.x

但是你或许不知道这个表达式(根据语言规范应该)是错的:
0 | 0 | E.x

对此感到好奇的请到原文查看详情:The Root Of All Evil, Part One
错误在于,C# 2.0的规范中说明“字面量0”可以被转化为任意枚举类型。是“字面量0”,而不是“编译时常量0”。
这这这...Aargh, it's driving me nuts! (模仿Eric的语气
如果你把下面的代码放到.NET Framework 3.5 Beta 2中编译测试的话,会看到编译器完全没对上面提及的第二种情况作出警告:
enum E {
    x = 1
}

class Program {
    public static void Main(string[] args) {
        E e = 0 | 0 | E.x;
    }
}

编译器会抱怨局部变量e没有被使用过(也就潜在意味着这个变量没有作用,是多余的),但并没对这里我们关心的问题给出警告。正好刚装上了.NET Framework 3.5的RTM,测试结果仍然一样。Mono 1.2.5.1的在这点上的行为与前述一致。

Unified C# 3.0 Specification的1.10 Enum中,规定了
引用
In order for the default value of an enum type to be easily available, the literal 0 implicitly converts to any enum type.

与前几个版本的规定没怎么改变,仍然是说“字面量0”而不是“编译时常量0”可以被转换为任意枚举类型。
于是.NET Framework与Mono都“很无奈”的在这点上无法与规范保持一致了。=_=||

===================================================

C#里派生类的方法里的匿名delegate调用基类的方法会产生无法验证的代码

原文:Why are base class calls from anonymous delegates nonverifiable?

前面开篇花絮里提到的是没有熟思而做的优化带来的后果,而下面要关注的问题就稍微复杂一些了。

考虑这段代码片段:
引用
using System;

public delegate void D( );

public class Alpha {

    public virtual void Blah( ) {
        Console.WriteLine( "Alpha.Blah" );
    }
}

public class Bravo : Alpha {

    public override void Blah( ) {
        Console.WriteLine( "Bravo.Blah" );
        base.Blah( );
    }

    public void Charlie( ) {
        int x = 123;
        D d = delegate {
            this.Blah( );
            base.Blah( );
            Console.WriteLine( x );
        };
        d( );
    }
}

class Program {
    // do nothing, just to make the compiler happy
    // else we'd compiler with /target:library
    public static void Main(string[] args) { }
}

用.NET Framework 3.5 Beta 2附带的C#编译器(csc.exe)编译上面的代码,会得到以下警告:
引用
Microsoft (R) Visual C# 2008 Compiler Beta 2 version 3.05.20706.1 for Microsoft (R) .NET Framework version 3.5
版权所有 (C) Microsoft Corporation。保留所有权利。

test1.cs(23,13): warning CS1911: 从匿名方法、lambda表达式、查询表达式或迭代器通过“base”关键字访问成员“Alpha.Blah()”会导致代码无法验证。请考虑将这种访问移入针对包含类型的辅助方法中。

刚装了.NET Framework 3.5的RTM,测试结果一样。至于Mono 1.2.5.1更有趣,完全没有报错。

这里有什么问题呢?Charlie()方法里用this/base去访问自身/基类的成员,不是很正常的么。问题出在C#中应对闭包生成的代码。
在C# 2.0中,引入了匿名delegate的概念,因而可以定义嵌套方法;在C# 3.0中,更进一步引入了Lambda Expression,同样可以用于定义嵌套方法。这里,嵌套的方法的作用域遵守词法作用域,也就是说内部方法可以访问外部包围作用域的变量,包括外部的“this”。外部包围作用域就对嵌套内部方法形成了“闭包”。
由于当一个嵌套方法生成(实例化)后,它的生命周期与它的外部方法不一定相同。它从外部环境中“捕获”到的变量,就像是从外部“逃逸”出来了一样。上面的例子中,Charlie()方法里x和this都成为了逃逸变量。
这些逃逸变量必须与嵌套方法的生命周期相同,即使外部方法已经返回也不能被立即销毁;因此这些逃逸变量也不能在栈上分配。这样,就需要为逃逸变量另外分配空间,常见的做法是在堆上分配。

Unified C# 3.0 Specification中,
引用
7.14.4 Outer variables
Any local variable, value parameter, or parameter array whose scope includes the lambda-expression or anonymous-method-expression is called an outer variable of the anonymous function. In an instance function member of a class, the this value is considered a value parameter and is an outer variable of any anonymous function contained within the function member.

7.14.4.1 Captured outer variables
When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated (§5.1.7). However, the lifetime of a captured outer variable is extended at least until the delegate or expression tree created from the anonymous function becomes eligible for garbage collection.


不同语言为闭包分配空间的具体方式不同。C#中,编译器会将逃逸变量提升为成员变量,并将匿名delegate提升为一个成员方法——不过并不是提升到原本的类中,而是一个由编译器构造的私有内部类中。上面的代码,会被编译器变成类似以下的形式:
引用
public class Bravo : Alpha {

    public override void Blah() {
        Console.WriteLine("Bravo.Blah");
        base.Blah();
    }
    
    // compiler generated inner class
    private class __locals {
        public int __x;
        public Bravo __this;
        public void __method() {
            this.__this.blah();
            // on the next line, no such "__nonvirtual__" in C#
            __nonvirtual__ ((Alpha)this.__this).Blah());
            Console.WriteLine(this.__x);
        }
    }
    
    public void Charlie() {
        __locals locals = new __locals();
        locals.__x = 123;
        locals.__this = this;
        D d = new D(locals.__method);
        d();
    }
}

当然这只是伪代码。C#中并没有"__nonvirtual__"关键字。一般来说,C#中的方法调用都是通过callvirt的IL指令完成的;而通过base关键字所做的方法调用则不遵循虚函数要使用最具体版本的规则,因而使用的是call的IL指令来完成。这里所谓"__nonvirtual__"就是要表现这个意思。

可以看到,原本代码中匿名delegate里对base的访问,实际上被生成到了另外一个类(私有内部类)的方法中,而那个类的"base"其实应该是System.Object……于是就有问题了。关键字“base”本来应该只能在同一个继承系的派生类中使用,这样生成的代码就像是让“base”的作用范围泄露了一般。没错,编译出来的代码确实是能运行,却变得不可验证(unverifiable)。
但这并不是使用.NET Framework的程序员的错;他们只是想在正确的地方正确的使用base而已。所以.NET Framework的应对方法是给出一个警告信息,提醒程序员修改代码来避开这个问题。不幸的是,Mono并没有提供任何警告提示。用Mono 1.2.5.1编译上面的代码,并用.NET Framework的PEVerify来验证,会看到下面的错误信息:
引用
Microsoft (R) .NET Framework PE Verifier.  Version  3.5.20706.1
Copyright (c) Microsoft Corporation.  All rights reserved.

[IL]: Error: [F:\FX\share\testClosure.exe : Bravo+<>c__CompilerGenerated0::<Charlie>c__1][offset 0x00000011] The 'this' parameter to the call must be the calling method's 'this' parameter.
1 Error Verifying testClosure.exe

错误所对应的IL代码为:
IL_0011:  call       instance void Alpha::Blah()

也就是原本的base.Blah()。

一个有趣的观察:虽然C#的语言规范中没有说明具体该如何为闭包生成代码,但.NET Framework与Mono所做的几乎是一样的。这大概是因为Mono要尽量保持与.NET Framework的兼容吧。

前面的例子是用匿名delegate,在C# 3.0中换成Lambda Expression也一样:
    public void Charlie( ) { // int x = 123;
        int x = 123;
        D d = ( ) => {
            this.Blah( );
            base.Blah( );
            Console.WriteLine( x );
        };
        d( );
    }


---------------------------------------

上面提到了编译器会生成不可验证代码的状况。不过要是把前面例子中Charlie()里的x给去掉,变成这样的话:
    public void Charlie( ) {
        D d = delegate {
            this.Blah( );
            base.Blah( );
        };
        d( );
    }

那么.NET Framework的C#编译器能发现唯一的逃逸变量是this,于是不会生成一个私有的内部类,而是直接将那个匿名delegate生成为Bravo的一个私有成员方法。也就是生成类似这样的代码:
public class Bravo : Alpha {

    public override void Blah() {
        Console.WriteLine("Bravo.Blah");
        base.Blah();
    }
    
    // compiler generated method
    public void __method() {
        this.blah();
        // on the next line, no such "__nonvirtual__" in C#
        __nonvirtual__ ((Alpha)this).Blah());
    }
    
    public void Charlie() {
        D d = new D(this.__method);
        d();
    }
}


换句话说,当逃逸变量只有this时,编译器并不会生成不可验证的变量。不过为了外表上行为的一致性,.NET Framework的C#编译器仍然会给出跟上面一样的警告。

Mono方面则是没有做这样的优化,仍然会与前面所说的状况一样,生成不可验证的代码。
分享到:
评论

相关推荐

    C# Delegate讲解

    在C#编译器处理委托时,会自动产生一个派生自System.MulticastDelegate的密封类,这个类和它的基类System.Delegate一起为委托提供必要的基础设施。我们可以使用ildasm.exe查看生成的委托类,发现它定义了三个公共的...

    01.C# 知识回顾 - 委托 delegate.pdf

    匿名方法也可以被封装在委托中,尽管这种方式在C#新版本中已不常用。 委托可以链接在一起,即可以将多个委托实例连接起来,形成一个委托链。当调用一个委托链时,链中的每一个委托都将依次被调用。委托链调用时,...

    C#高级编程(PPT)

    `展示了如何在派生类中直接调用基类的成员函数,而无需重新编写代码。 接口在C#中扮演着契约的角色,它定义了一组方法签名,但不提供实现。类可以实现多个接口,这体现了多态性。例如,`public class Graduate: ...

    C#程序设计复习题.doc

    8. 在派生类中重新定义基类方法时,应保持与基类完全相同的方法名、参数列表和返回类型,这是方法的重写(Override),否则就是定义新的方法。 9. C#中所有异常类的基类是`Exception`,主动引发异常的语句关键字是`...

    浅析C# 中object sender与EventArgs e

    当用户点击按钮时,`button1_Click`方法会被调用,并且`sender`参数将会是`button1`对象本身,而`e`参数则是一个`EventArgs`对象(默认情况下,由于没有附加数据,所以使用基本的`EventArgs`类)。 #### 四、自定义...

    chap8.pptchap8.ppt

    - 派生类实例化:在`main()`方法中,`Derived dr_obj = new Derived()`创建了一个派生类的对象,然后调用`dr_obj.Base_fun1()`执行基类的方法,显示了继承的特性,即无需在派生类中重新编写基类的功能。 - **多态...

    C#事件及响应方法详解

    事件是类的一种特殊成员,用于声明特定情况的发生,而响应方法则是处理这些事件的代码。下面我们将深入探讨C#中事件和响应方法的工作原理。 首先,事件在C#中通过`event`关键字声明。例如,`public event ...

    c#学习笔记.txt

    virtual在派生类中声明其实现可由重写成员更改的方法或访问器。 volatile指示字段可由操作系统、硬件或并发执行的线程等在程序中进行修改。 9,语句 语句是程序指令。除非特别说明,语句都按顺序执行。C# 具有下列...

    C# 事件的继承.txt

    当一个派生类想要使用基类的事件时,它通常会通过覆盖与事件关联的方法来实现这一目标。在我们的例子中,`MyBusiness`类通过重写`OnProgress`方法来实现对`ProgressEvent`事件的自定义处理。 ### 总结 C#中的事件...

    Object Oriented Programming Using C#(c#面向对象编程基础)

    抽象方法使得基类可以定义方法的签名,而具体的实现留给派生类完成。例如: ```csharp public abstract class Plant { public abstract void Grow(); } public class Flower : Plant { public override void ...

    C#_重载重写_隐藏_数组_集合_委托

    调用`DeriveClass`的`F`方法时,会先调用基类的`F`方法,然后执行派生类中的额外逻辑。 ### 隐藏(Hiding) 在C#中,如果派生类中有一个与基类同名的方法或字段,即使签名相同,也不一定会发生重写。这种情况下,...

    武汉大学计算机学院C#考试卷子.pdf

    由于代码未给出完整部分,这里假设`B`类的构造函数会调用基类的构造函数,因此输出结果将是"A",表示`A`类的构造函数被调用。 以上是对《C#程序设计》试卷中部分内容的详细解析,涵盖了字符串、引用类型、索引器、...

    C# 经典教程(C#语言标准 3.0版)

    - **通过继承隐藏:** 派生类中的成员可以隐藏基类中的同名成员。 **3.8 命名空间和类型名称** - **命名空间:** 用于组织相关类型。 - **类型名称:** 类、接口、枚举等的名称。 - **完全限定名:** 包含命名空间...

    C#练习题word

    如果基类没有默认构造函数,则派生类必须显式地调用基类的构造函数。 **示例代码:** ```csharp public class BaseClass { public BaseClass() { Console.WriteLine("BaseClass constructor called."); } } ...

    C#面试题(带答案)

    如果一个方法被声明为`virtual`,那么在派生类中可以通过`override`关键字来提供不同的实现。 3. .NET与Java事件模型的区别: - .NET框架使用委托(Delegate)来实现事件模型。委托是一种类型安全的函数指针,可以...

    自己做的C#课上用的关于事件继承的讲演PPT

    3. **提供受保护的虚拟方法**:为了使派生类能够触发事件,基类通常会提供一个受保护的虚拟方法,如`OnProgress`,该方法实际上执行事件的调用。 4. **派生类重写**:在派生类中,可以通过重写基类的`OnProgress`...

Global site tag (gtag.js) - Google Analytics