.NETの「Improving C# Memory Safety」で最も大きく変わるのは、unsafe が「ポインターを使うための構文」から「呼び出し側が守るべき安全契約を示す印」へ変わる点です。通常のC#アプリを安全な範囲だけで書いている開発者への影響は限定的ですが、Unsafe、Marshal、NativeMemory、P/Invoke、IntPtr、ポインター、低レイヤーなライブラリを扱うチームは、コードレビュー・CI・パッケージ管理の見直しが必要になります。
Microsoftの.NET Blogで公開された公式記事「Improving C# Memory Safety」では、この新しいモデルを.NET 11でプレビュー、.NET 12で正式提供する計画が示されています。現時点では初期段階のオプトイン機能であり、最終的なプロジェクト設定名などは.NET 11プレビューで固まる見込みです。(Microsoft for Developers)
Improving C# Memory Safetyの要点
今回の変更は、C#をRustのような言語に変えるものではありません。C#の通常の安全なコードはそのまま維持しつつ、どうしても必要な低レイヤー処理だけを明確に囲い、責任の所在を見えるようにする取り組みです。
| 観点 | これまで | 新しいモデルでの考え方 |
|---|---|---|
unsafe の意味 | 主にポインター構文を許可するための印 | 呼び出し側に安全上の義務が残ることを示す契約 |
| 危険箇所の見え方 | 実装内部や慣習に依存しやすい | unsafe ブロック、unsafe シグネチャ、<safety> コメントで追いやすい |
| 影響する主なコード | ポインター、Unsafe、Marshal、P/Invoke、NativeMemory など | 従来より広い範囲のメモリアクセス関連API |
| 適用方法 | C# 1.0以来の従来ルール | .NET 11でプレビュー予定のプロジェクト単位オプトイン |
| 実行時への影響 | なし | 新しい実行時チェックは追加されず、性能影響も想定されていない |
新しいモデルの目的は「unsafeコードを簡単に書けるようにすること」ではなく、「unsafeコードをレビューしやすくすること」です。公式記事でも、コンパイラが未検証のメモリ操作を構造化し、開発者が安全性の根拠を明示できるようにする点が強調されています。(Microsoft for Developers)
何が変わるのか:unsafe は構文ではなく契約になる
従来のC#では、unsafe は主に「この範囲ではポインター関連の構文を使える」という意味で使われてきました。たとえば、メソッドに unsafe を付けると、そのメソッド内でポインターを扱えるようになります。
新しいモデルでは、unsafe の役割が分かれます。
| 種類 | 役割 |
|---|---|
メンバーの unsafe | 呼び出し側に安全上の義務が残ることを示す |
メソッド内部の unsafe { } | 実際に未検証の操作をしている箇所を局所化する |
/// <safety> | 呼び出し側が満たすべき条件を文書化する |
// SAFETY: | 実装内部で、その unsafe 操作が安全だと判断した根拠を書く |
重要なのは、メソッドに unsafe を付けても、そのメソッド全体が自動的に unsafe コンテキストになるわけではない点です。新しいモデルでは、unsafe メソッドの中でも、実際の危険操作は明示的な unsafe { } ブロックに入れる必要があります。これにより、「どの行が本当に危険なのか」をレビューしやすくなります。(Microsoft for Developers)
たとえば、次のような考え方です。
/// <safety>
/// buffer は length バイト以上の読み取り可能なメモリを指している必要がある。
/// </safety>
public static unsafe byte ReadFirstByte(byte* buffer, int length)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
unsafe
{
// SAFETY: length が 1 以上であることは確認した。
// ただし buffer が有効なメモリを指す責任は呼び出し側に残る。
return buffer[0];
}
}
この例では、length の検証はメソッド内でできます。しかし、buffer が本当に読み取り可能なメモリを指しているかどうかは、呼び出し側の文脈を知らなければ判断できません。そのため、メソッド自体を unsafe にして、<safety> に呼び出し側の義務を書きます。
既存コードへの主な影響
.NETの通常のWebアプリ、業務アプリ、APIサーバーなどで、低レイヤーなメモリ操作を直接使っていない場合、大きな修正は発生しにくいです。C#ではもともと AllowUnsafeBlocks が既定で false であり、unsafeコードはデフォルトで許可されていません。新しいモデルを有効にした場合、この既定値の意味がさらに強くなります。(Microsoft for Developers)
一方で、次のようなコードを持つプロジェクトは確認が必要です。
| 確認対象 | 見るべきポイント |
|---|---|
System.Runtime.CompilerServices.Unsafe | 呼び出し箇所が安全境界として成立しているか |
System.Runtime.InteropServices.Marshal | IntPtr 経由で実質的にポインターを扱っていないか |
P/Invoke / LibraryImport | safe または unsafe の明示が必要になる箇所がないか |
NativeMemory.Alloc / Free | 所有権、解放済みポインター、別名参照の有無を説明できるか |
IntPtr / nint | 数値なのかポインターなのか、API設計として曖昧でないか |
byte* / void* | ポインターを渡すだけでなく、どこで逆参照しているか |
stackalloc | Span<T> 化や SkipLocalsInit と組み合わさる箇所がないか |
| ソースジェネレーター | 生成コードが unsafe や設定変更を含まないか |
特に注意したいのは IntPtr です。従来は、ポインター型をシグネチャに出すと unsafe として見えやすい一方、IntPtr は「ただの整数のように見えるポインター」として扱われることがありました。新しいモデルでは、このような慣習に頼った危険なAPIを、より明示的に扱う方向へ寄せています。(Microsoft for Developers)
呼び出し側に伝播する unsafe と、安全境界で止める unsafe
新しいモデルで開発者が判断すべき中心は、「unsafeを呼び出し側に伝播させるか、現在のメソッド内で安全に閉じ込めるか」です。
呼び出し側に伝播させるべきケース
次の条件をメソッド内で検証できない場合は、unsafe をシグネチャに付けて呼び出し側に責任を伝えるべきです。
| 呼び出し側に残りやすい義務 | 具体例 |
|---|---|
| メモリが生存していること | 解放済みポインターではない |
| 読み書き可能な範囲が十分であること | length 分のバッファが本当に存在する |
| 所有権が正しいこと | 別のアロケーターで確保した領域を解放していない |
| 別名参照がないこと | 解放時にまだ Span やポインターが生きていない |
| ネイティブAPIの前提を満たすこと | P/Invoke先が期待する文字列終端、構造体レイアウト、呼び出し規約など |
この場合、APIは次のように「呼び出し側が何を守るべきか」を明文化します。
/// <safety>
/// ptr は、互換性のあるアロケーターで確保され、
/// まだ解放されていないメモリを指している必要がある。
/// この呼び出し中、その領域を参照する生存中の Span やポインターがあってはならない。
/// </safety>
public static unsafe void ReleaseBuffer(void* ptr)
{
unsafe
{
// SAFETY: 解放対象の妥当性は呼び出し側の契約に依存する。
NativeMemory.Free(ptr);
}
}
メソッド内で安全境界にできるケース
一方で、メソッド内で入力検証、範囲チェック、寿命管理、try/finally による解放を完結できるなら、外部には安全なAPIとして見せる選択肢があります。
public static byte[] CopyFromNative(byte* source, int length)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentOutOfRangeException.ThrowIfNegative(length);
var result = new byte[length];
unsafe
{
// SAFETY: length は検証済み。
// result の書き込み範囲は配列長で確保済み。
// source の有効性だけは本来呼び出し元の前提になり得るため、
// 公開APIにする場合は設計を再検討する。
fixed (byte* destination = result)
{
Buffer.MemoryCopy(source, destination, length, length);
}
}
return result;
}
この例は、まだ完全な安全境界とは言い切れません。source が有効なメモリを指すかどうかをメソッド内で確認できないためです。公開APIとして安全にしたいなら、ReadOnlySpan<byte> を受け取る設計に変える、ネイティブバッファの所有権を SafeHandle で管理する、といった設計変更を検討すべきです。
「コンパイルを通すために unsafe を付ける」のではなく、「呼び出し側に残る義務を本当に伝えるべきか」を判断することが重要です。
プロジェクト設定で確認すべきこと
C# 16相当の新しい安全モデルには、プロジェクト単位の新しいオプトイン設定が用意される予定です。ただし、公式記事では最終的なプロパティ名は.NET 11プレビューで確定すると説明されています。そのため、現時点で独自にそれらしいプロパティ名を決め打ちして設定ファイルへ書くのは避けるべきです。(Microsoft for Developers)
既存の <AllowUnsafeBlocks> は引き続き重要です。これは unsafe キーワードの使用自体を許可する設定で、既定値は false です。
| 新しい安全モデル | AllowUnsafeBlocks | 状態 | 実務上の意味 |
|---|---|---|---|
| 有効 | false | 最も安全寄り | 新モデルに参加しつつ、unsafeコードは許可しない |
| 有効 | true | 新モデルでunsafeを許可 | unsafe APIを使う箇所に明示的なブロックと契約が必要 |
| 無効 | false | 従来モデルでunsafe不可 | 現行の安全寄り構成 |
| 無効 | true | 従来モデルでunsafe可 | 既存の低レイヤーコードが残る構成 |
管理者やリード開発者は、まず Directory.Build.props、各 .csproj、CIテンプレートを確認し、AllowUnsafeBlocks が不用意に true になっていないかを洗い出してください。AI支援ツールや自動修正ツールが、ビルドエラーを避けるために設定を緩める可能性もあるため、設定ファイルの変更はコードレビューで必ず確認する運用にした方が安全です。
P/InvokeとLibraryImportでは safe / unsafe の判断が必要になる
新しいモデルでは、extern 宣言に対して safe または unsafe を明示する考え方が導入されます。LibraryImport の partial method も対象です。公式記事では、引数も戻り値も単純な getpid のような例は safe、ネイティブ側がポインターを逆参照する strlen のような例は unsafe として示されています。(Microsoft for Developers)
判断の目安は次の通りです。
| ネイティブ呼び出しの内容 | 推奨される考え方 |
|---|---|
| プリミティブ値だけを返し、呼び出し側のメモリを読まない | safe にできる可能性がある |
| ポインター、バッファ、文字列ポインターを渡す | unsafe として契約を書く可能性が高い |
| 構造体レイアウトや呼び出し規約に依存する | 安全性の根拠をレビューで確認する |
| ハンドルを扱う | 可能なら SafeHandle を使う |
IntPtr でポインターを隠している | 型付きポインターや安全なラッパーへの移行を検討する |
P/Invoke周りでは、「動いているから安全」と判断しないことが重要です。文字列終端、バッファ長、構造体のパッキング、所有権、解放タイミングのどれかが曖昧なAPIは、後からレビューできるように契約を書いておく必要があります。
ライブラリ開発者がやるべき移行作業
ライブラリ開発者は、アプリ開発者よりも影響を受けやすい立場です。理由は、公開APIの unsafe は利用者のコードに伝播し、パッケージ利用時の体験を左右するためです。
移行時は、次の順序で確認すると効率的です。
| 手順 | 作業内容 | 判断基準 |
|---|---|---|
| 1 | unsafe、IntPtr、Marshal、Unsafe、NativeMemory を検索する | 危険箇所の入口を把握する |
| 2 | 各メソッドの入力検証を確認する | null、範囲、長さ、所有権、寿命を説明できるか |
| 3 | 安全境界にできるAPIを選ぶ | 外部に義務を残さず内部で検証できるか |
| 4 | 残る義務があるAPIに unsafe を付ける | 呼び出し側が読める <safety> を書けるか |
| 5 | IntPtr APIを見直す | ポインターなら型付きポインター、ハンドルなら SafeHandle を検討する |
| 6 | CIで設定変更を監視する | AllowUnsafeBlocks や新オプトイン設定の変更をレビュー対象にする |
| 7 | リリースノートに影響を書く | 利用者が呼び出し側の修正を予測できるようにする |
公式記事では、移行支援として dotnet format によるベストエフォートの修正機能を提供する計画も示されています。ただし、この修正は unsafe 呼び出しを unsafe { } で囲むなどの機械的な支援にとどまり、安全契約や <safety> コメントを自動で正しく書いてくれるものではありません。(Microsoft for Developers)
つまり、自動修正は「コンパイルに近づける入口」であり、「安全な移行の完了」ではありません。
<safety> コメントに何を書くべきか
<safety> コメントは、通常のXMLドキュメントコメントとは役割が違います。メソッドの使い方を親切に説明するのではなく、呼び出し側が満たさなければ未定義動作やメモリ破壊につながり得る条件を書く場所です。
良い <safety> コメントには、次の要素が入ります。
| 書くべきこと | 例 |
|---|---|
| 有効なメモリ範囲 | ptr は length バイト以上読み取り可能である |
| 寿命 | 返された Span は所有元が破棄された後に使ってはならない |
| 所有権 | ptr はこのAPIと互換性のあるアロケーターで確保されている |
| 別名参照 | 解放時に同じ領域を参照する生存中の参照がない |
| スレッド安全性 | 別スレッドの解放と競合しないよう参照カウントを保持する |
| 文字列・構造体の前提 | UTF-8終端、固定長、パッキング、アラインメントなど |
避けたいのは、次のような曖昧なコメントです。
/// <safety>
/// 正しく使うこと。
/// </safety>
これではレビューにも利用者にも役立ちません。最低限、何を満たせば安全なのかを具体的に書く必要があります。
/// <safety>
/// ptr は null ではなく、少なくとも byteCount バイトの読み取り可能なメモリを指している必要がある。
/// byteCount は実際のバッファ長を超えてはならない。
/// 呼び出し中に、そのメモリが別スレッドで解放または書き換えられてはならない。
/// </safety>
実装内部の // SAFETY: には、直前の検証や不変条件によって、その unsafe 操作がなぜ成立すると判断したのかを書きます。公式記事でも、/// <safety> は呼び出し側向けの正式な契約、// SAFETY: は実装内部の根拠として区別されています。(Microsoft for Developers)
管理者・開発リーダーが展開時に注意すべきポイント
企業やチームで.NETプロジェクトを管理している場合、単に新しい言語機能として扱うのではなく、サプライチェーンと開発プロセスの問題として見るべきです。
unsafeコードの利用方針を明文化する
まず、プロジェクトで unsafeコードを許可する条件を決めます。
| 方針 | 向いているプロジェクト |
|---|---|
| unsafe禁止 | 一般的なWebアプリ、業務システム、APIサーバー |
| 一部プロジェクトのみ許可 | 画像処理、暗号処理、ネイティブ連携、パフォーマンス重視の共通基盤 |
| ライブラリ内部のみ許可 | 公開APIは安全にし、内部でだけ最適化する |
| 公開unsafe APIを許可 | 低レイヤーライブラリ、ランタイム拡張、ネイティブ連携ライブラリ |
全プロジェクトで漫然と AllowUnsafeBlocks=true にするのは避けるべきです。必要なプロジェクトだけに限定し、理由をレビュー可能にしておく方が管理しやすくなります。
CIで設定変更を検出する
AI支援ツールが普及すると、ビルドエラーを解消するために設定を緩める変更が混入する可能性があります。公式記事でも、AI支援によるコード生成の増加が今回のモデルの動機の一つとして触れられています。(Microsoft for Developers)
CIでは、次のような変更をレビュー必須にするとよいでしょう。
| 監視対象 | 理由 |
|---|---|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | unsafeコードの許可範囲が広がる |
| 新しい安全モデルのオプトイン設定 | コンパイルルールが変わる |
TreatWarningsAsErrors の無効化 | <safety> 不足などの検出力が下がる |
Directory.Build.props の変更 | 複数プロジェクトへ一括影響する |
| ソースジェネレーター更新 | 生成コードに unsafe が入る可能性がある |
| 低レイヤー系NuGet更新 | unsafe 契約が変わる可能性がある |
NuGet依存関係の確認も必要
.NETライブラリはバイナリ配布されることが多いため、依存パッケージが新しい安全モデルに対応しているかどうかは、今後の確認ポイントになります。公式記事では、nuget.org上で新モデルへの対応状況を示すバッジのような仕組みも検討されていると説明されています。ただし、これは検討段階であり、現時点で運用前提にするべきではありません。(Microsoft for Developers)
当面は、利用している低レイヤーライブラリのリリースノート、GitHub Issue、公開APIの変更履歴を確認する運用が現実的です。
失敗しやすいポイント
新しい unsafe モデルでは、表面的にコードを直すだけでは安全になりません。特に次の失敗に注意してください。
| 失敗例 | なぜ危険か | 対策 |
|---|---|---|
とりあえずメソッドに unsafe を付ける | 呼び出し側へ責任を投げるだけになる | 残る義務を <safety> に書けるか確認する |
メソッド全体を大きな unsafe { } で囲む | 危険箇所がぼやける | 逆参照や unsafe API呼び出しの周辺だけに絞る |
IntPtr を安全な整数として扱う | 実質的なポインター操作が隠れる | 型付きポインター、SafeHandle、安全なラッパーを検討する |
| try/catchで安全になったと考える | 不正メモリアクセスは例外で回復できるとは限らない | 入力検証、所有権、寿命を事前に保証する |
| 自動修正後にレビューしない | 機械的に囲っただけでは安全契約が不足する | unsafeブロックごとに根拠を書く |
| AI生成コードをそのまま採用する | 設定変更や検証漏れが混入しやすい | CIとレビューで設定・契約・境界を確認する |
特に「例外が出るから安全」という考え方は危険です。不正なポインターが未マップメモリを指す場合、プロセスが終了する可能性があります。逆に、たまたま読めるメモリを指してしまうと、例外も警告もなく不正な値を処理してしまうことがあります。公式記事でも、IntPtr を使った読み取り例を通じて、try/catchが安全性の検証にはならない点が説明されています。(Microsoft for Developers)
いま取るべき行動
.NET 11のプレビュー段階で慌てて全プロジェクトを移行する必要はありません。ただし、unsafeコードを含むプロジェクトでは、今から棚卸しを始める価値があります。
まずは、リポジトリ全体で次のキーワードを検索してください。
unsafe
IntPtr
nint
Marshal.
Unsafe.
MemoryMarshal.
NativeMemory.
LibraryImport
DllImport
stackalloc
SkipLocalsInit
fixed
void*
byte*
検索結果を、次の3つに分類します。
| 分類 | 対応方針 |
|---|---|
| 削除できる unsafe | 安全な標準API、Span<T>、BitConverter などへ置き換える |
| 内部に閉じ込められる unsafe | 入力検証と // SAFETY: を追加し、安全な公開APIにする |
| 呼び出し側に義務が残る unsafe | unsafe シグネチャと <safety> コメントで契約を明示する |
次に、AllowUnsafeBlocks が有効なプロジェクトを一覧化し、「なぜ必要なのか」をプロジェクトごとに説明できる状態にします。説明できないものは、無効化できる可能性があります。
最後に、コードレビューの観点を更新します。unsafeコードを含むPull Requestでは、次の質問に答えられる状態を必須にするとよいでしょう。
| レビュー質問 | 確認したいこと |
|---|---|
| この unsafe は削除できないか | 安全な代替APIがないか |
| どの操作が本当に危険か | unsafeブロックが最小化されているか |
| 呼び出し側に何の義務が残るか | <safety> が具体的か |
| 境界で何を検証しているか | null、長さ、範囲、寿命、所有権が確認されているか |
| 設定を緩めていないか | AllowUnsafeBlocks や警告設定が不用意に変わっていないか |
Improving C# Memory Safetyは、すべての.NET開発者がすぐに書き方を変える機能ではありません。しかし、低レイヤーAPI、ネイティブ連携、パフォーマンス最適化、共通ライブラリを扱うチームにとっては、今後のC#コードレビューの基準を変える重要な変更です。
まずは「unsafeを使っている場所」を探し、「消せるもの」「閉じ込められるもの」「呼び出し側へ契約として出すもの」に分けることから始めてください。それだけでも、.NET 11プレビュー以降の移行作業はかなり見通しやすくなります。

コメント