C#では、派生クラスのメソッドの匿名delegateがベースクラスを呼び出すメソッドは検証できないコードを生成します


開発者のblogを読むのは知識を得る近道のようです.特に、その開発者が担当している製品があなたが毎日使っているインフラストラクチャの一つである場合、例えば......コンパイラです.読んでいる
Eric Lippertのblogの時、私は何気なく多くの私が以前よく知らなかった知識を知っていました.例えば、いくつかの言語の特性、いくつかのプログラミング思想などを話しました.しかし、もっと面白いのは、彼が担当している製品の奇妙なところをたくさん知っています.
===================================================
冒頭のエピソード:
もし私たちが今列挙タイプEを持っているならば、その中の1つの列挙値の名前は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! (エリックのニュアンスを真似て
次のコードを置くとNET Framework 3.5 Beta 2でのコンパイルテストでは、コンパイラが上記の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 CS 1911:匿名メソッド、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」の役割範囲を漏らすようなものである.そう、コンパイルされたコードは確かに実行できるが、検証不可能になる.
しかし、これは使用ではありません.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::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側ではこのような最適化は行われておらず,前述の状況と同様に検証不可能なコードが生成される.