読者です 読者をやめる 読者になる 読者になる

C#7のローカル関数(Local Function)とは何か

C# 7には「ローカル関数(Local Function)」という機能が追加されました。

関数(メソッド)の中に関数を定義できるというものです。

以下は(処理的には何の意味も持ちませんが)ローカル関数を使った例です(リスト1)。
TestClass1クラスのTestMethod1()メソッド内に定義した「innerFunc(int n)」がローカル関数です。

// リスト1
// ローカル関数を利用
namespace TestNs
{
  public class TestClass1
  {
    public int TestMethod1(int num)
    {
      // ローカル関数
      int innerFunc(int n) {
        int i = n * 10;
        return i;
      };
      
      var result = innerFunc(num);
      return result;
    }
  }
}

ローカル関数を利用する1つのケースは、「特定のメソッド内からしか呼び出されない処理をローカル関数として定義する」ことです。
privateメソッドとして切り出しても良いのですが、同クラス内の別メソッドから予期せず呼び出されるリスクを持ちます。
ローカル関数を使用しない(privateメソッドに切り出す)実装は以下の通りです(リスト2)。

// リスト2
// 従来の実装
namespace TestNs
{
  public class TestClass2
  {
    public int TestMethod2(int num)
    {
      var result = this.InnerFunc(num);
      return result;
    }

    // TestMethod2からしか呼び出されないメソッド
    private int InnerFunc(int n)
    {
      int i = n * 10;
      return i;
    }
  }
}

MSILはどうなっているのか?

では、視点を変えまして・・・
リスト1をコンパイルした「MSIL(Microsoft Intermediate Language)」は一体どういったものでしょうか?
ildasm.exeを使って、リスト1をコンパイルしたアセンブリ(DLL)を見てみましょう。
TestMethod1()のMSILは以下の通りです(リスト3)。

// リスト3
.method public hidebysig instance int32  TestMethod1(int32 num) cil managed
{
  // Code size       16 (0x10)
  .maxstack  1
  .locals init ([0] int32 result,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  nop
  IL_0003:  ldarg.1
  IL_0004:  call       int32 TestNs.TestClass1::'<TestMethod1>g__innerFunc1_0'(int32)
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  stloc.1
  IL_000c:  br.s       IL_000e
  IL_000e:  ldloc.1
  IL_000f:  ret
} // end of method TestClass1::TestMethod1

ローカル関数を「TestNs.TestClass1::‘ginnerFunc1_0'」として呼び出しています。
「TestNs.TestClass1::’g
innerFunc1_0'」実装のILは、以下の通りです(リスト4)。

// リスト4
.method assembly hidebysig static int32  '<TestMethod1>g__innerFunc1_0'(int32 n) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       12 (0xc)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  mul
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  stloc.1
  IL_0008:  br.s       IL_000a
  IL_000a:  ldloc.1
  IL_000b:  ret
} // end of method TestClass1::'<TestMethod1>g__innerFunc1_0'

staticメソッドとして定義されています。またpublic / private等のアクセス修飾子がついていません。つまり internal なメソッドということです。
上記より、リスト4は 以下のC#実装と同意となります(リスト5)。

// リスト5
namespace TestNs
{
  public class TestClass1
  {
    public int TestMethod1(int num)
    { 
      var result = TestClass1.innerFunc(num);
      return result;
    }

    internal static int innerFunc(int n)
    {
      int i = n * 10;
      return i;
    }
  }
}

ローカル関数は「internal staticメソッド」と解釈された

つまりC#コード上での ローカル関数 は、C#コンパイラを通してMSILとなる段階で「internal staticメソッド」として解釈されていました。

が!!!!!

必ずしも「internal staticメソッド」となるわけではありません。

ローカル関数がインスタンスプロパティを参照している場合

はい、ということで以下のようなケースではどうでしょうか?(リスト6)

// リスト6
namespace TestNs
{
  public class TestClass1
  {
    private int propValue = 111;

    public int TestMethod1(int num)
    {
      // ローカル関数
      int innerFunc(int n) {
        // インスタンスプロパティを参照している
        int i = n * this.propValue;
        return i;
      };
      
      var result = innerFunc(num);
      return result;
    }
  }
}

先程と同様にリスト6をコンパイルしたアセンブリ(DLL)のinnerFunc()部のMSILを確認してみます(リスト7)。

// リスト7
.method private hidebysig instance int32 
        '<TestMethod1>g__innerFunc1_0'(int32 n) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       16 (0x10)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldarg.0
  IL_0003:  ldfld      int32 TestNs.TestClass1::propValue
  IL_0008:  mul
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  stloc.1
  IL_000c:  br.s       IL_000e
  IL_000e:  ldloc.1
  IL_000f:  ret
} // end of method TestClass1::'<TestMethod1>g__innerFunc1_0'

メソッドの実装が「internal static」から「private」に変わりました。
そう、ローカル関数内の実装が「クラスのインスタンスプロパティを参照する実装」に変わった為、ローカル関数はインスタンスメソッドとして解釈されるようになりました。

まとめ

C# 7のローカル関数とは「C#言語の仕様 」あり「MSILレベルの仕様ではない」ということです。
C#コンパイラによって解釈され、MSILでは従来通りの.NETの実装となる。
なんか、ネガティブっぽい言い方になっていますが、最近のC#言語仕様の多くは(ほとんどは?)、C#コンパイラが解釈する仕様となっています。dynamicなんかはすごいMSILに解釈されますしね。

ということで、「知っても知らなくてもアプリは作れるよ!」な内容の投稿でした^^;

※本投稿では ildasm.exe を使って MSIL を直接確認しましたが、Reflectorなんかを使って C#コードに逆コンパイルしたコードを確認するとより読みやすいと思います。