前回は、名前空間とアセンブリの基本を見ました。
今回は、.NET アセンブリの中に何が入っているのか、そしてクラスライブラリとコンソールアプリがどう違うのかを整理します。
.NET アセンブリの中身
.NET アセンブリは、単なるバイナリの塊ではありません。
実行コードだけでなく、型情報、参照情報、バージョン情報、リソースなどを含んだ自己記述的なファイルです。
代表的な構成要素は次の通りです。
- Windows がファイルを認識するためのヘッダー
- CLR が .NET アセンブリとして扱うためのヘッダー
- C# から変換された中間言語
- 型、メソッド、プロパティなどのメタデータ
- アセンブリ名、バージョン、参照情報などのマニフェスト
- 埋め込みリソース
この構造があるため、.NET ランタイムはアセンブリを読み込み、型を解決し、実行時にメタデータを調べることができます。
OS が認識するファイル情報
.NET の .dll や .exe は、Windows 上では PE 形式のファイルです。
そのため、Windows が実行可能ファイルやライブラリとして扱うための情報を持っています。
通常の C# 開発では、この低レベルなファイル構造を直接意識することは多くありません。
ただし、「.NET の DLL も OS から見れば特定形式のバイナリファイルである」という点は押さえておくとよいです。
CLR が使う情報
.NET アセンブリには、CLR が読み込むための情報も含まれています。
CLR は、アセンブリ内の中間言語やメタデータを読み、必要に応じて JIT コンパイルしてネイティブコードとして実行します。
C# ソースコード
↓ ビルド
中間言語とメタデータを含むアセンブリ
↓ 実行時
JIT コンパイル
↓
ネイティブコードとして実行
この流れにより、C# はソースコードから直接 OS ネイティブコードになるのではなく、いったん共通の中間表現を経由します。
中間言語、型メタデータ、マニフェスト
アセンブリの中心になるのは、次の 3 つです。
中間言語
C# コンパイラーが生成する、CLR 用の命令です。
実行時に JIT コンパイラーによって CPU が実行できるネイティブコードへ変換されます。
型メタデータ
クラス、構造体、インターフェイス、メソッド、プロパティ、属性などの情報です。
リフレクションで型情報を調べられるのは、このメタデータがあるためです。
マニフェスト
アセンブリ自身の名前、バージョン、カルチャ、参照している他のアセンブリなどの情報です。
ランタイムが依存関係を解決するときにも使われます。
次のコードは、実行中アセンブリの基本情報を表示します。
using System.Reflection;
Assembly assembly = Assembly.GetExecutingAssembly();
AssemblyName name = assembly.GetName();
Console.WriteLine($"名前: {name.Name}");
Console.WriteLine($"バージョン: {name.Version}");
Console.WriteLine($"完全名: {assembly.FullName}");
Console.WriteLine("参照しているアセンブリ:");
foreach (AssemblyName referenced in assembly.GetReferencedAssemblies())
{
Console.WriteLine($"- {referenced.FullName}");
}
埋め込みリソース
アセンブリには、画像、テキスト、設定用ファイルなどをリソースとして埋め込むことができます。
プロジェクトファイルで次のように指定します。
<ItemGroup>
<EmbeddedResource Include="Resources\Message.txt" />
</ItemGroup>
読み込むコードは次のようになります。
using System.Reflection;
Assembly assembly = Assembly.GetExecutingAssembly();
string? resourceName = assembly
.GetManifestResourceNames()
.FirstOrDefault(name => name.EndsWith("Message.txt"));
if (resourceName == null)
{
Console.WriteLine("リソースが見つかりません。");
return;
}
using Stream? stream = assembly.GetManifestResourceStream(resourceName);
using StreamReader reader = new StreamReader(stream!);
Console.WriteLine(reader.ReadToEnd());
埋め込みリソースは、配布ファイルを増やしたくない場合や、ライブラリ内部で固定データを持ちたい場合に使えます。
クラスライブラリとコンソールアプリの違い
クラスライブラリは、他のアプリケーションから参照されることを目的としたプロジェクトです。
ビルドすると通常 .dll が生成されます。
dotnet new classlib -n MyMath
コンソールアプリは、実行可能な入口を持つプロジェクトです。
ビルドすると実行可能なアプリとして動かせます。
dotnet new console -n MyTool
違いを簡単にまとめると、次のようになります。
- クラスライブラリ: 再利用されるコードをまとめる
- コンソールアプリ:
Mainから起動される実行プログラムを作る
クラスライブラリには、通常アプリの開始点はありません。
コンソールアプリや Web アプリなどが参照して使います。
対象フレームワークの考え方
.NET プロジェクトは、どのランタイムや API セットを対象にするかを指定します。
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
複数の対象を指定することもできます。
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
</PropertyGroup>
net8.0 は .NET 8 向けのアプリやライブラリを作るときに使います。
netstandard2.0 は、古い .NET 実装も含めて広く互換性を持たせたいライブラリで使われることがあります。
新しいアプリケーション開発では、基本的に現在利用する .NET バージョンを対象にします。
広い互換性が必要なライブラリを作るときだけ、netstandard を検討するとよいです。
まとめ
.NET アセンブリには、中間言語、型メタデータ、マニフェスト、リソースなどが含まれています。
この自己記述的な構造があるため、ランタイムはアセンブリを読み込み、依存関係を解決し、リフレクションで型情報を調べられます。
また、クラスライブラリは再利用されるコードの単位、コンソールアプリは実行されるプログラムの単位です。
次回は、アプリケーション設定を扱う構成ファイルについて見ていきます。