VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > temp > C#教程 >
  • c#派生类方法里的惹名delegate调用基类里的方法会产生无法验证的代码

制作者:剑锋冷月 单位:无忧统计网,www.51stat.net
 

 

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

 

  开篇花絮:

  假如我们现在有一个枚举类型E,其中有一个枚举值的名字是x。

  你或许知道这个表达式是对的:

  C#代码  

 

0 | E.x

 

 

 

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

  C#代码   

 

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中编译测试的话,会看到编译器完全没对上面提及的第二种情况作出警告:

  C#代码  

 

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?

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

  考虑这段代码片段:

  引用

  C#代码   

 

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提升为一个成员方法——不过并不是提升到原本的类中,而是一个由编译器构造的私有内部类中。上面的代码,会被编译器变成类似以下的形式:

  引用

  C#代码   

 

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:FXsharetestClosure.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代码为:

  Java代码  

 

IL_0011:  call       instance void Alpha::Blah()

 

 

 

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

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

  前面的例子是用匿名delegate,在C# 3.0中换成Lambda Expression也一样:

  C#代码   

 

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

 

 

 

  上面提到了编译器会生成不可验证代码的状况。不过要是把前面例子中Charlie()里的x给去掉,变成这样的话:

  C#代码  

 

    public void Charlie( ) {
        D d = delegate {
            this.Blah( );
            base.Blah( );
        };
        d( );
    }

 

 

 

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

  C#代码

 

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方面则是没有做这样的优化,仍然会与前面所说的状况一样,生成不可验证的代码。

 

 

 

  在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?

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

  考虑这段代码片段:

  引用

  C#代码   

 

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) { }
}

 



相关教程