ファイル ベース実行
概要
.NET 10 (C# 14 と同世代)で単独の .cs ファイルだけで C# プログラムを実行できるようになりました。
例えば、app1.cs という名前で保存した C# ファイルを dotnet app1.cs という1コマンドだけで実行できます。
それに伴って、C# 14 で #! と #: (無視ディレクティブ)という機能が追加されています。
(C# 言語の新文法というよりは、C# コンパイラーの1機能という感じのものです。
バージョン的にも C# 14 である必要はなくて、.NET 10 以降付属の C# コンパイラーであれば言語バージョン問わず #! と #: を認識します。)
本項ではこの「単独のファイルでの実行」(ファイル ベース実行)の話と、
C# 14 の #! と #: (無視ディレクティブ)について説明します。
サンプル コード: FileBaseApp
ファイル ベース実行
改めて、 .NET 10 で C# ファイルを直接1コマンドで実行できるようになりました。
例えば、以下の1行だけ書いたファイル app1.cs を用意して、
Console.WriteLine("🐈");
以下のようなコマンドを打つと、この C# ファイルを単独で実行できます。
> dotnet app1.cs
🐈
これはスクリプト実行ではなく、通常の※脚注 C# 実行になります。 この仕組みをファイル ベース実行(file-based execution) と言い、これを使って書かれた C# プログラムをファイル ベース アプリ(file-based app)と言ったりします。
この機能の追加に伴い、これまでであればプロジェクト (実体は拡張子 .csproj の XML ファイル)に書いていた設定の類を C# 中に直接書けるようになりました。
以下の2つが追加されています。
ちなみに、普通に .csproj ファイルを使って C# プロジェクトをコンパイルする際には、#! や #: があるとコンパイル エラーになります。
ただし、.csproj ファイル中に <Features>FileBasedProgram</Features> オプションを書いておくとコンパイルでき、この場合、#! や #: から始まる行は単に無視されます。
C# コンパイラーからすると「単に無視するもの」なので、無視ディレクティブ(ignored directive)と呼ばれます。
※ スクリプト実行が「それ専用の構文がいくつかある」状態なのに対して、
ファイル ベース実行は本当に普通の C# です。
スクリプト実行みたいに「1行1行追加で実行」みたいなことができない一方で、
「コードが多くなってきたから .csproj 形式の通常の C# プロジェクトに切り替えたい」というときにスムーズに移行できます。
移行を自動化するための dotnet project convert というコマンドも用意されています。
shebang
#! (通称 shebang。 sharp + bang が由来)は主に Unix のスクリプト言語で使われるもので、
ソースコードの先頭にこの記号から始まる行を入れると「何を使ってこのスクリプトを実行するか」を指定できます。
C# 14 で、C# にもこの1行を入れることができるようになりました。
例えば前節の app1.cs ファイルにちょっと手を加えて以下のような内容にします。
#!/usr/bin/env dotnet Console.WriteLine("🐈");
このファイルは bash などの Unix 系シェルで ./app1.cs みたいに直接実行できるようになります。
(実行権限が必要なので、最初に1回 chmod +x などの操作が必要。)
$ ls app1.cs $ chmod +x app1.cs $ ./app1.cs 🐈
用途的に、#! はファイルの先頭にのみ書けます。
#! の前には改行はもちろんのこと、空白文字や BOM を入れることもできません。
: 無視ディレクティブ
#: から始まる行は dotnet コマンドがプロジェクト設定として解釈するために使い、
C# 上は単に無視されます。
例えば、以下のような .cs ファイルをファイル ベース実行するのは、
#:property InvariantGlobalization=true Console.WriteLine(new DateTime(2000, 1, 2, 3, 4, 5));
以下のような2ファイルを使って既存の .csproj ベースの dotnet run をするのとほぼ同じ意味になります。
app1.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</Project>
app1.cs:
Console.WriteLine(new DateTime(2000, 1, 2, 3, 4, 5));
(ちなみに InvariantGlobalization を指定すると書式が北米フォーマットになるので、出力される結果は 01/02/2000 03:04:05 (MM/dd/yyyy)になります。)
#: で始まる無視ディレクティブは shebang とコメントを除いて、ファイルの先頭に置く必要があります。
例えば以下のコードでは、5行目(LangVersion の行)は問題なく、
9行目(ImplicitUsings の行)でだけコンパイル エラーを起こします。
#!/usr/bin/env dotnet // コメントはあってもいい。 #:property LangVersion=13 Console.WriteLine("🐈"); #:property ImplicitUsings=disable
.NET 10 時点で、dotnet コマンドは以下のディレクティブを解釈できます。
| ディレクティブ | 意味 | .csproj での書き方 |
|---|---|---|
#:sdk |
プロジェクト SDK を指定 | <Project Sdk="これ"> |
#:property |
プロパティ要素 | <PropertyGroup> の子要素 |
#:package |
パッケージ参照 | <PackageReference> 要素 |
#:project |
プロジェクト参照 | <ProjectReference> 要素 |
sdk ディレクティブ
#:sdk は、 .csproj では <Project Sdk="Identifier"> と書いていたものです。
省略した場合は Microsoft.NET.Sdk (ライブラリやコンソール プログラムで使う一番シンブルな SDK)になります。
実質的には「ASP.NET プログラムを書きたいときに Microsoft.NET.Sdk.Web にするもの」です。
例えば、以下のようなコードで ASP.NET なコードをファイル ベース実行できます。
#:sdk Microsoft.NET.Sdk.Web var app = WebApplication.CreateBuilder(args).Build(); app.MapGet("/", () => "Hello World!"); app.Run();
property ディレクティブ
#:property は、 .csproj では <PropertyGroup> の子要素として書いていたものです。
.csproj の <Tag>Value</Tag> 要素が #:property Tag=Value という書き方になります。
無視ディレクティブの節の冒頭の InvariantGlobalization の例もこれになります。
その他、例えば unsafe ブロックはオプションを指定しないと使えない構文なわけですが、以下のように書くことでそのオプションを指定できます。
#:property AllowUnsafeBlocks=true // unsafe ブロックはオプションをつけないと使えない構文。 unsafe { int n = 1; int* pn = &n; Console.WriteLine($"{(nint)pn:x}"); }
package ディレクティブ
#:package は、 .csproj では <PackageReference> 要素で書いていたものです。
.csproj の <PackageReference Include="PackageName" Version="x.y.z" /> 要素が #:package PackageName@x.y.z という書き方になります。
例として Microsoft.CodeAnalysis.CSharp パッケージ(C# 中から C# コンパイラー自身を呼ぶためのライブラリ)を参照したコードを書くと以下のようになります。
(ちなみに、4.14.0 は C# 13 当時のバージョンです。)
#:package Microsoft.CodeAnalysis.CSharp@4.14.0 using Microsoft.CodeAnalysis.CSharp; var tree = CSharpSyntaxTree.ParseText("class Class1;"); var root = await tree.GetRootAsync(); Console.WriteLine(root.GetFirstToken().Text);
project ディレクティブ
#:project は、 .csproj では <ProjectReference> 要素で書いていたものです。
.csproj の <ProjectReference Include="path" /> 要素が #:project path という書き方になります。
例えば以下のような書き方で、.cs のある場所からの相対パスで Lib/Lib.csproj プロジェクトを参照できます。
#:project Lib/Lib.csproj Console.WriteLine(Lib.Class1.Name);
未対応のディレクティブ
未対応の #: ディレクティブは、ファイル ベース実行するとエラーを起こします。
例えば以下のようなコードを書いて dotnet app1.cs コマンド実行すると、
「認識されないディレクティブ ' aaa' です。」というエラーが出ます。
#:aaa Console.WriteLine("🐈");
ちなみにこのエラーを出すのはあくまで dotnet コマンドであって、
C# コンパイラー的には「#: で始まるディレクティブはすべて無視」という挙動になっています。
<Features>FileBasedProgram</Features> オプションを書いた .csproj ファイルを用意して、
旧来方式でコンパイルすると #:aaa の行のエラーは出ません。
