C# 9.0のTop-level statementsとその動作

あけましておめでとうございます。2021年の書き始めはC#9.0です。

Top-level statementsとは

C# 9.0 に入った Top-level statements は多くのアプリケーションの不要な式を取り除くための導入された機能です。具体的にはコンソールアプリケーションのエントリーポイントの記述を簡素に行うことができます。下記のようなコードについて考えます。

using System;
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Top-level statementsを用いると上記のコードは下記で記述することができます。

using System;
Console.WriteLine("Hello World!");

エントリーポイントのためのクラスやメソッドの定義が省略でき、簡素に実装できることが確認できます。

ちなみに動作環境は、コマンドラインの実行はdotnet 5.0.100で、C#コードのデコンパイルはSharpLabでC#のバージョンはdotnet/roslyn 6c5cef5で確認しました。また、コマンドライン実行のためのプロジェクトのcsprojは下記のとおりです。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>

コマンドライン引数のとり方

args という変数が定義されています。

using System;
for (var i = 0; i < args.Length; ++i)
{
    Console.WriteLine($"{i} - {args[i]}");
}

下記のようにプログラムを実行すると、コマンドライン引数を処理できていることが確認できます。

$ dotnet run arg1 arg2 arg3                                                                                                              
0 - arg1                                                                                                                                                                                 
1 - arg2                                                                                                                                                                                 
2 - arg3

メソッド定義

(一応)メソッドも定義できます。

using System;
void Echo()
{
    Console.WriteLine("Hello world");
}

Echo(); // "Hello world"

クラスや構造体の定義

クラスや構造体の定義も可能ですが、Top-level statementsの後に記述する必要があります。

using System;

// struct Test
// {
//     public int IntVal;
// }
// ここに定義すると下記のエラーがでる。
// error CS8803: Top-level statements must precede namespace and type declarations.

var test = new Test();

Console.WriteLine($"{test.IntVal}");

struct Test
{
    public int IntVal;
}

async / await

通常のエントリーポイント同様、async/await を用いる事もできます。

using System;
using System.Threading.Tasks;
await Test();

static async Task Test()
{
    Console.WriteLine("First line.");
    await Task.Delay(1000);
    Console.WriteLine("Second line.");
    await Task.Delay(1000);
    Console.WriteLine("Third line.");
    // First line.
    // Second line. (1秒後に表示)
    // Third line. (さらに1秒後に表示)
}

内部動作

このTop-level statementsがどのように実装されているのか見てみます。

using System;
Console.WriteLine("Hello World!");

このコードをコンパイル後、デコンパイルしたコードは下記のようになります。

[CompilerGenerated]
internal static class <Program>$
{
    private static void <Main>$(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

省略したエントリーポイントのためのクラス及びメソッド(上記だと クラスが <Program> で メソッドが <Main> )コンパイラが生成し、Top-level statementsとして定義した文はその中に定義しているようです。そのメソッドが args 変数を定義しているため、その変数でコマンドライン引数がとれます。

ちなみに、メソッドを定義した場合(上記の例だと Echo)は、下記のように展開されます。

[CompilerGenerated]
internal static class <Program>$
{
    private static void <Main>$(string[] args)
    {
        <<Main>$>g__Echo|0_0();
    }

    internal static void <<Main>$>g__Echo|0_0()
    {
        Console.WriteLine("Hello world");
    }
}

生成したクラスの internal な静的メソッドとして定義し( <<Main>$>g__Echo|0_0 というメソッド名で定義 )、それを呼び出しています。

ちなみにクラスや構造体の定義について触れましたが、こちらは Top-level statements とは関係なく、コンパイラが自動生成するエントリーポイントクラスの外に(普通に)クラスが配置されます。

[CompilerGenerated]
internal static class <Program>$
{
    private static void <Main>$(string[] args)
    {
        Test test = default(Test);
        Console.WriteLine(string.Format("{0}", test.IntVal));
    }
}
internal struct Test
{
    public int IntVal;
}

字句解析のルール上、名前空間および型の宣言までを Top-level statements として、そのステートメントの情報を元にエントリーポイントのクラスを生成します。

ちなみに async/awaitでは、IAsyncStateMachineを実装したクラスを自動生成している。が、このへんがちゃんとわからずそのうち調べる...。 (https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/)

制約

1プロジェクトにつき Top-level statements は1つしか定義できません。定義した場合は CS8802 エラーを吐きます。

まとめ

C# 9.0のTop-level statementsについて、その使い方と、内部的にどのように動作するのか紹介しました。

参考