ファイル ベース実行
概要
.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
の行のエラーは出ません。