記述する C# コードのほとんどは、検証可能に安全なコードです。 安全なコードは.NETツールでコードが安全であることを確認できることを意味します。 一般に、安全なコードはポインターを使用してメモリに直接アクセスしません。 また、生メモリは割り当てられません。 代わりにマネージド オブジェクトが作成されます。
C# 言語リファレンスには、C# 言語の最新リリース バージョンが記載されています。 また、今後の言語リリースのパブリック プレビューの機能に関する初期ドキュメントも含まれています。
このドキュメントでは、言語の最後の 3 つのバージョンまたは現在のパブリック プレビューで最初に導入された機能を特定します。
ヒント
C# で機能が初めて導入された時期を確認するには、 C# 言語バージョン履歴に関する記事を参照してください。
C# では、検証できないコードを記述できるunsafe コンテキストもサポートされています。 安全でないコードは必ずしも危険ではありません。.NETツールでは安全性を検証できないコードにすぎません。 安全でないコードを使用して、ポインターを必要とするネイティブ関数を呼び出し、場合によっては配列境界チェックを回避する直接メモリ アクセスによってパフォーマンスを向上させます。 安全でないコードでは、セキュリティと安定性のリスクも発生します。
unsafe コンテキストを含むコードをコンパイルするには、AllowUnsafeBlocks コンパイラ オプションを追加します。
C# では、安全でないコードとしてカウントされる 2 つのモデルが定義されています。元のモデルと、C# 15 と .NET 11 でプレビュー段階にある更新されたメモリ安全モデルです。 2 つのモデルの違いについては、 安全でないコードの 2 つのモデルを参照してください。
C# での安全でないコードのベスト プラクティスについては、「 安全でないコードのベスト プラクティス」を参照してください。
安全でないコードの 2 つのモデル
C# では、安全でないコード用の 2 つのモデルが定義されています。 実際のモデルは、 unsafe コンテキストを必要とする操作と、メンバーの unsafe 修飾子が呼び出し元にどのように影響するかを決定します。
-
元の安全でないモデル:
unsafeコンテキストは 、ポインター特徴の存在をカバーします。 ポインター型の宣言、変数のアドレスの取得、ポインターの逆参照、stackalloc式のポインターへの変換、またはunsafeコンテキスト内でのみ任意の型へのsizeofの適用を行います。 (安全なコードでは、Span<T>またはReadOnlySpan<T>に割り当てられたstackalloc式を使用できます)。型、メンバー、またはブロックのunsafe修飾子は、そのコンテキストを確立しますが、呼び出し元には義務を負いません。 C# 1.0 ではこのモデルが導入され、既定値のままです。 -
メモリ安全性モデルの更新:
unsafeコンテキストは、 ランタイムが管理していないメモリにアクセスする操作を対象としています。 ポインターの存在は安全ではありません。ポインターの逆参照は次の値です。 メンバーのunsafe修飾子は、呼び出し元に安全性を監査する義務を伝達するコントラクトになります。 このモデルは、C# 15 および .NET 11 でプレビュー段階にあります。
次の表では、各モデルで unsafe コンテキストを必要とする操作を比較します。
| Operation | 元のモデル | モデルの更新 |
|---|---|---|
ポインター型を宣言するか、 & |
要求 unsafe |
安全なコードで許可 |
fixed ステートメント |
要求 unsafe |
安全なコードで許可 |
stackalloc式をポインターに変換する |
要求 unsafe |
安全なコードで許可 |
アンマネージ型の sizeof 演算子 |
要求 unsafe |
安全なコードで許可 |
ポインター間接参照 (*p)、メンバー アクセス (p->m)、または要素アクセス (p[i]) |
要求 unsafe |
要求 unsafe |
| 関数ポインターの呼び出し | 要求 unsafe |
要求 unsafe |
| 固定サイズ バッファーでの要素アクセス | 要求 unsafe |
要求 unsafe |
マークされたメンバーを呼び出す unsafe |
呼び出し元の要件なし | 要求 unsafe |
更新されたモデルを試すには、.NET 11 SDK (プレビュー段階) を使用し、LangVersion コンパイラ オプションを preview に設定します。 ポインターの緩和は、C# 15 コンパイラと preview 言語バージョンでコンパイルするたびに適用されます。 呼び出し元の義務やアセンブリのオプトインを含む完全な適用はまだ開発中です。 詳細については、「 更新されたメモリ安全モデル (プレビュー)」を参照してください。
元の安全でないモデル
元のモデルでは、 unsafe キーワードは、型、メンバー、またはブロックに安全でないコンテキストを確立し、そのコンテキストによって、次のセクションで説明するポインター機能のロックが解除されます。
unsafe修飾子は、マークされたコードが実行できる操作のみを変更します。呼び出し元には必要ありません。 これらの例のいずれかをコンパイルするには、 AllowUnsafeBlocks コンパイラ オプションを設定します。
ポインター型
安全でないコンテキストでは、型には、値型または参照型に加えて、ポインター型を指定できます。 ポインター型宣言は、次のいずれかの形式になります。
type* identifier;
void* identifier; //allowed but not recommended
ポインター型の * の前に指定する型は 、参照先の型です。
ポインター型は オブジェクトから継承せず、ポインター型と objectの間に変換は存在しません。 また、ボックス化とボックス化解除もポインターをサポートしません。 ただし、異なるポインター型間、およびポインター型と整数型の間で変換できます。
同じ宣言で複数のポインターを宣言する場合は、基になる型と共にアスタリスク (*) のみを記述します。 各ポインター名のプレフィックスとしては使用されません。 例えば次が挙げられます。
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
ガベージ コレクターは、オブジェクトを指すポインター型があるかどうかを追跡しません。 参照先がマネージド ヒープ内のオブジェクト (ラムダ式または匿名デリゲートによってキャプチャされたローカル変数を含む) である場合は、ポインターが使用されている限り、オブジェクトを ピン留め する必要があります。
MyType*型のポインター変数の値は、MyType型の変数のアドレスです。 ポインター型宣言の例を次に示します。
-
int* p:pは整数へのポインターです。 -
int** p:pは、整数へのポインターのポインターです。 -
int*[] p:pは、整数へのポインターの 1 次元配列です。 -
char* p:pは char へのポインターです。 -
void* p:pは不明な型へのポインターです。
ポインター間接演算子 * を使用して、ポインター変数が指す場所にあるコンテンツにアクセスできます。 たとえば、次の宣言を考えてみましょう。
int* myVariable;
式*myVariableは、intに含まれるアドレスにあるmyVariable変数を表します。
fixed ステートメントの記事には、いくつかのポインターの例があります。 次の例では、 unsafe キーワードと fixed ステートメントを使用し、内部ポインターをインクリメントする方法を示します。 このコードをコンソール アプリケーションの Main 関数に貼り付けて実行できます。 これらの例は、 AllowUnsafeBlocks コンパイラ オプション セットを使用してコンパイルする必要があります。
// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}
Console.WriteLine("--------");
Console.WriteLine(a[0]);
/*
Output:
10
20
30
--------
10
11
12
--------
12
*/
間接演算子を void*型のポインターに適用することはできません。 ただし、キャストを使用して void ポインターを他のポインター型に変換し、その逆を行うことができます。
ポインターは nullできます。 間接演算子を null ポインターに適用すると、実装で定義された動作が発生します。
メソッド間でポインターを渡すと、未定義の動作が発生する可能性があります。
in、out、またはrefパラメーターを介して、または関数の結果としてローカル変数へのポインターを返すメソッドについて考えてみましょう。 ポインターが固定ブロックに設定されている場合、ポインターが指す変数が固定されなくなる可能性があります。
次の表に、安全でないコンテキストでポインターを操作できる演算子とステートメントを示します。
| オペレーター/ステートメント | 用途 |
|---|---|
* |
ポインターの間接参照を実行します。 |
-> |
ポインターを介して構造体のメンバーにアクセスします。 |
[] |
ポインターのインデックスを作成します。 |
& |
変数のアドレスを取得します。 |
++ と -- |
ポインターをインクリメントおよびデクリメントします。 |
+ と - |
ポインターの算術演算を実行します。 |
==、!=、<、>、<=、>= |
ポインターを比較します。 |
stackalloc |
スタックにメモリを割り当てます。 |
fixed ステートメント |
アドレスを見つけることができるように、変数を一時的に修正します。 |
ポインター関連演算子の詳細については、「 ポインター関連の演算子」を参照してください。
ポインター型は、暗黙的に void* 型に変換できます。 ポインター型には、 null値を割り当てることができます。 キャスト式を使用して、任意のポインター型を他のポインター型に明示的に変換できます。 任意の整数型をポインター型に変換することも、任意のポインター型を整数型に変換することもできます。 これらの変換には、明示的なキャストが必要です。
次の例では、 int* を byte*に変換します。 ポインターが変数の最下位のアドレス指定バイトを指していることに注意してください。 結果を連続してインクリメントすると、 int のサイズ (4 バイト) まで、変数の残りのバイトを表示できます。
int number = 1024;
unsafe
{
// Convert to byte:
byte* p = (byte*)&number;
System.Console.Write("The 4 bytes of the integer:");
// Display the 4 bytes of the int variable:
for (int i = 0 ; i < sizeof(int) ; ++i)
{
System.Console.Write(" {0:X2}", *p);
// Increment the pointer:
p++;
}
System.Console.WriteLine();
System.Console.WriteLine($"The value of the integer: {number}");
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
固定サイズ バッファー
配列は参照型であるため、セーフ コードでは、配列である構造体フィールドには、要素自体ではなく、配列の要素への参照のみが格納されます。
pathNameは参照であるため、次のstructのサイズは配列内の要素の数に依存しません。
public struct PathArray
{
public char[] pathName;
private int reserved;
}
構造体自体の内部に配列の内容を格納するには、 fixed キーワードを使用して 固定サイズのバッファーを宣言します。
fixed キーワードには、unsafe コンテキストが必要です。 固定サイズバッファーは、他の言語またはプラットフォームのデータ ソースと相互運用するメソッドを記述する場合に便利です。 固定サイズのバッファーは、通常の構造体メンバーに許可されている任意の属性または修飾子を受け取ることができます。 唯一の制限は、配列の型が bool、 byte、 char、 short、 int、 long、 sbyte、 ushort、 uint、 ulong、 float、または doubleである必要があるということです。
private fixed char name[30];
次の例では、 fixedBuffer 配列のサイズは固定されています。
fixed ステートメントを使用して最初の要素へのポインターを取得し、そのポインターを介して配列の要素にアクセスします。
fixed ステートメントは、fixedBuffer インスタンス フィールドをメモリ内の特定の場所にピン留めします。
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
internal unsafe class Example
{
public Buffer buffer = default;
}
private static void AccessEmbeddedArray()
{
var example = new Example();
unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);
// Modify through the index:
example.buffer.fixedBuffer[0] = 'B';
Console.WriteLine(example.buffer.fixedBuffer[0]);
}
}
配列 char 128 要素のサイズは 256 バイトです。 固定サイズの char バッファーは、エンコードに関係なく、常に 1 文字あたり 2 バイトを受け取ります。 この配列サイズは、 CharSet = CharSet.Auto または CharSet = CharSet.Ansiを持つ API メソッドまたは構造体に char バッファーがマーシャリングされている場合でも同じです。 詳細については、CharSetを参照してください。
前の例では、ピン留めせずに fixed フィールドにアクセスする方法を示します。 もう 1 つの一般的な固定サイズ配列は 、ブール 配列です。
bool配列内の要素のサイズは常に 1 バイトです。
bool 配列は、ビット配列またはバッファーの作成には適していません。
固定サイズのバッファーは、 System.Runtime.CompilerServices.UnsafeValueTypeAttributeでコンパイルされます。これにより、型にオーバーフローする可能性があるアンマネージ配列が含まれていることを共通言語ランタイム (CLR) に指示します。
stackalloc を使用して割り当てられたメモリでは、CLR のバッファー オーバーラン検出機能も自動的に有効になります。 前の例は、固定サイズのバッファーが unsafe structにどのように存在するかを示しています。
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Buffer用にコンパイラによって生成された C# の属性は次のとおりです。
internal struct Buffer
{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
固定サイズバッファーは、次の点で通常の配列とは異なります。
-
unsafeコンテキストでのみ使用できます。 - 構造体のインスタンス フィールドのみを指定できます。
- これらは常にベクトルまたは 1 次元配列です。
- 宣言には、
fixed char id[8]などの長さを含める必要があります。fixed char id[]を使用することはできません。
関数ポインター
C# には、安全な関数ポインター オブジェクトを定義するための delegate 型が用意されています。 デリゲートを呼び出すには、 System.Delegate から派生した型をインスタンス化し、その Invoke メソッドを仮想メソッド呼び出しする必要があります。 この仮想呼び出しでは、 callvirt IL 命令が使用されます。 パフォーマンス クリティカルなコード パスでは、 calli IL 命令を使用する方が効率的です。
delegate*構文を使用して、関数ポインターを定義できます。 コンパイラは、calli オブジェクトをインスタンス化してdelegateを呼び出すのではなく、Invoke命令を使用して関数を呼び出します。 次のコードでは、同じ型の 2 つのオブジェクトを結合するために delegate または delegate* を使用する 2 つのメソッドを宣言します。 最初のメソッドは、 System.Func<T1,T2,TResult> デリゲート型を使用します。 2 番目のメソッドは、同じパラメーターと戻り値の型を持つ delegate* 宣言を使用します。
public static T Combine<T>(Func<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>
combinator(left, right);
次のコードは、静的ローカル関数を宣言し、そのローカル関数へのポインターを使用して UnsafeCombine メソッドを呼び出す方法を示しています。
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
上記のコードは、関数ポインターとしてアクセスされる関数に関するいくつかの規則を示しています。
- 関数ポインターは、
unsafeコンテキストでのみ宣言できます。 -
delegate*コンテキストでdelegate*を受け取る (またはunsafeを返す) メソッドのみを呼び出すことができます。 - 関数のアドレスを取得する
&演算子は、static関数でのみ許可されます。 この規則は、メンバー関数とローカル関数の両方に適用されます。
構文には、 delegate 型の宣言とポインターの使用との並列があります。
*のdelegateサフィックスは、宣言が関数ポインターであることを示します。 メソッド グループを関数ポインターに割り当てる際の & は、操作がメソッドのアドレスを受け取ります。
キーワードのdelegate*とmanagedを使用して、unmanagedの呼び出し規則を指定できます。 さらに、 unmanaged 関数ポインターの場合は、呼び出し規則を指定できます。 次の宣言は、それぞれの例を示しています。 最初の宣言では、既定の managed 呼び出し規則が使用されます。 次の 4 つでは、 unmanaged 呼び出し規則が使用されます。 それぞれ、ECMA 335 呼び出し規則 ( Cdecl、 Stdcall、 Fastcall、または Thiscall) のいずれかを指定します。 最後の宣言では、 unmanaged 呼び出し規則を使用し、プラットフォームの既定の呼び出し規則を選択するように CLR に指示します。 CLR は、実行時に呼び出し規則を選択します。
public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);
関数ポインターの詳細については、C# 言語仕様の 関数ポインター セクションを参照してください。
例: ポインターを使用してバイトの配列をコピーする
次の例では、ポインターを使用して、ある配列から別の配列にバイトをコピーします。
この例では、 unsafe キーワードを使用します。これにより、 Copy メソッドでポインターを使用できます。
fixed ステートメントは、ソース配列とコピー先配列へのポインターを宣言します。
fixed ステートメントは、ガベージ コレクションが配列を移動しないように、メモリ内のソース配列とコピー先配列の位置を固定します。
fixed ブロックは、ブロックのスコープ内の配列のメモリ ブロックをピン留めします。 この例の Copy メソッドは unsafe キーワードを使用するため、 AllowUnsafeBlocks コンパイラ オプションを使用してコンパイルする必要があります。
この例では、2 番目のアンマネージ ポインターではなくインデックスを使用して、両方の配列の要素にアクセスします。
pSourceポインターとpTarget ポインターの宣言により、配列がピン留めされます。
static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
int targetOffset, int count)
{
// If either array is not instantiated, you cannot complete the copy.
if ((source == null) || (target == null))
{
throw new System.ArgumentException("source or target is null");
}
// If either offset, or the number of bytes to copy, is negative, you
// cannot complete the copy.
if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
{
throw new System.ArgumentException("offset or bytes to copy is negative");
}
// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
}
// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}
static void UnsafeCopyArrays()
{
// Create two arrays of the same length.
int length = 100;
byte[] byteArray1 = new byte[length];
byte[] byteArray2 = new byte[length];
// Fill byteArray1 with 0 - 99.
for (int i = 0; i < length; ++i)
{
byteArray1[i] = (byte)i;
}
// Display the first 10 elements in byteArray1.
System.Console.WriteLine("The first 10 elements of the original are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray1[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of byteArray1 to byteArray2.
Copy(byteArray1, 0, byteArray2, 0, length);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of the last 10 elements of byteArray1 to the
// beginning of byteArray2.
// The offset specifies where the copying begins in the source array.
int offset = length - 10;
Copy(byteArray1, offset, byteArray2, 0, length - offset);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
/* Output:
The first 10 elements of the original are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
90 91 92 93 94 95 96 97 98 99
*/
}
更新されたメモリ安全モデル (プレビュー)
Important
更新されたメモリ安全モデルは、C# 15 および .NET 11 のプレビュー機能です。 これは、プレビュー リリース中のフィードバックに基づいて進化し続けます。 モデルを試すには、.NET 11 (プレビュー) SDK を使用し、LangVersion コンパイラ オプションを preview に設定します。 .NET 11 Preview 5 のコンパイラはポインターの緩和を実装しますが、呼び出し元の義務、アセンブリオプトイン、または safe キーワードはまだ適用されていません。 完全な設計については、 メモリ安全機能の仕様を参照してください。
更新されたモデルは、元のモデルが 1 つとして扱う 2 つの事柄 (ポインター コードの 存在 と呼び出し元への安全義務の 伝達 ) を分離します。 メンバー unsafe マークすると、本体内のポインターは許可されなくなります。メンバーの 呼び出し元は安全でないため、すべての呼び出し元がその義務を伝達するか、検証済みの安全な呼び出し可能な境界の背後に排出する必要があります。 この分離をサポートするために、モデルでは安全でないコンテキストも絞り込まれます。ポインターの存在は安全ではありません。ランタイムが管理していないメモリにアクセスする操作だけです。 縮小を使用すると、安全なコードでポインターを保持、渡し、返すことができますが、 unsafe は、メモリの安全性に実際に違反する可能性がある操作とメンバーをマークします。
呼び出し元の安全でないメンバー
元のモデルでは、メンバーの unsafe 修飾子は、メンバーのシグネチャと本文内のポインターのみを許可します。 呼び出し元に安全性については通知しません。 更新されたモデルは、呼び出し元に修飾子の意味を与えます。 メンバー unsafeをマークすると、コンパイラはそれを呼び出し元セーフ (requires-unsafe とも呼ばれます) として扱います。すべての呼び出し元は、unsafe コンテキストから呼び出す必要があり、その呼び出し元に安全を監査する義務が移ります。
メンバーシグネチャの unsafe 修飾子は、本文の安全でないコンテキストを確立しなくなりました。 次の 2 つのロールが分割されます。
- 署名の
unsafe修飾子は、呼び出し元に義務を伝達します。 - 内部
unsafeブロックは、アンマネージ メモリにアクセスする操作のスコープを設定します。
次のプレビュー モックアップでは、 ReadInt32 は呼び出し元が安全ではありません。 シグネチャは unsafe 修飾子を持ち、内部 unsafe ブロックは逆参照をラップします。
// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
unsafe
{
return *(int*)source;
}
}
呼び出し元は、独自の unsafe ブロックで呼び出しをラップします。
// Preview
unsafe
{
int value = ReadInt32(buffer);
}
更新されたモデルでは、いくつかの関連ルールも強化されます。
-
unsafe修飾子は、型宣言、静的コンストラクター、およびファイナライザーでエラーを生成します。これは、修飾子に通知する呼び出し元がないためです。 - デリゲートは型型であるため、デリゲートを
unsafeできません。 - パラメーターなしのコンストラクターが
unsafeされている型は、new()制約を満たしていません。
安全でないコンテキストを必要とする操作
ポイント先のメモリにアクセスする操作には、 unsafe コンテキストが必要です。
- ポインター間接参照 (
*p)、ポインター メンバー アクセス (p->member)、ポインター要素アクセス (p[i])。 - 関数ポインターの呼び出し。
- 固定サイズ バッファーでの要素アクセス。
次の例では、 unsafe コンテキストを使用せずに配列をピン留めしますが、ポインターを 1 つ内で逆参照します。
public static int ReadValue(int[] numbers)
{
fixed (int* first = numbers)
{
// Dereferencing a pointer accesses unmanaged memory, so it still
// requires an unsafe context.
unsafe
{
return *first;
}
}
}
緩やかな操作
指し示されたメモリにアクセスしない操作では、 unsafe コンテキストが不要になります。
- ポインター型を宣言し、
&演算子を使用して変数のアドレスを取得します。 - 変数をピン留めする
fixedステートメント。 -
stackalloc式をポインターに変換する。 - アンマネージ型に適用される
sizeof演算子。
次の例では、 unsafe コンテキストなしでポインターを作成してピン留めします。
public static void CreatePointer()
{
int value = 42;
// Creating a pointer doesn't require an unsafe context.
int* pointer = &value;
int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
// The fixed statement no longer requires an unsafe context.
fixed (int* first = numbers)
{
int* current = first;
}
}
これらの緩和は、アセンブリが更新されたメモリ安全規則にオプトインするかどうかに関係なく、 preview 言語バージョンでコンパイルするたびに適用されます。
呼び出し元の安全でない義務を放棄する
呼び出し元の安全でない操作を呼び出すメンバーには、義務を伝達するか、それを排出するかの 2 つの選択肢があります。
-
伝達: 独自のメンバー
unsafeをマークします。 義務は呼び出し元に渡されます。 自分で義務を完全に検証できない場合は、伝達を使用します。 -
退院: メンバーの署名は安全なままにしておきます。 メンバー内の義務 (通常はランタイム ガードを使用) を検証し、内部
unsafeブロックで安全でない操作を実行します。 内部unsafeブロックを含むが、独自の署名をマークしないメンバーunsafeは 安全でない境界です。安全でないコードを安全に呼び出し可能なサーフェスに変換します。
次のプレビュー モックアップでは、ガードを使用してその入力を検証し、マネージド配列をピン留めして、ポインターを読み取ります。 呼び出し元は unsafe コンテキストを必要としません。これは、メソッドが義務を負うためです。
// Preview
public static int SumBytes(byte[] source)
{
ArgumentNullException.ThrowIfNull(source);
fixed (byte* first = source)
{
unsafe
{
// SAFETY: the null check and source.Length bound every read to the pinned array.
int total = 0;
for (int i = 0; i < source.Length; i++)
{
total += first[i];
}
return total;
}
}
}
null チェックと配列の長さは、読み取りがバッファーを越えて実行される入力を除外するため、 unsafe ブロック内の逆参照はサウンドになります。 このメソッドは残余義務を負いません。そのため、安全な呼び出し可能な署名が公開されます。
安全性に関するドキュメント
呼び出し元の安全でないメンバーは、呼び出し元が保証する必要がある内容を文書化する必要があります。 更新されたモデルでは、次の 2 つの補完的なコメント スタイルが推奨されます。
- 署名の上にある
/// <safety>ドキュメント ブロックには、正式なコントラクト (呼び出し元が満たす必要がある条件) が記載されています。 アナライザーは、存在しない呼び出し元の安全でないメンバーにフラグを設定できます。 -
unsafeブロック内の// SAFETY:コメントは、本文を読む開発者と監査者のために、その場所で操作が鳴る理由を記録します。
次のプレビュー モックアップは、呼び出し元が安全でない ReadByte メソッドの両方のスタイルを示しています。
// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
byte* address = (byte*)ptr;
unsafe
{
// SAFETY: relies on the caller obligation stated in the <safety> block.
return address[offset];
}
}
/// <safety> ブロックによってコントラクトが通知されます。コントラクトは、すべての呼び出し元とレビュー担当者が参照するドキュメントに属します。
安全でないフィールド
フィールドの unsafe 修飾子は、宣言された型が、外側の型が保持し、他のコードが依存するコントラクトを表していない場合に使用します。 安全でないのは、型システムが見るものと型が約束するものの間のギャップに存在します。 この修飾子は、フィールドへのすべての書き込みを unsafe ブロックに強制し、書き込みを 1 か所で確認できるようにします。
最も明確なケースは、ネイティブ ポインターを保持するフィールドです。 ポインターは、 System.Span<T> と同じようにアドレス指定するバイト数を宣言しないため、格納型はその情報自体を保持します。
// Preview
public class NativeBuffer
{
/// <safety>
/// Null, or points to a buffer of Length bytes.
/// </safety>
private unsafe byte* _pointer;
public int Length { get; }
public byte ReadAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
unsafe
{
// SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
return _pointer[index];
}
}
}
readonly unsafe フィールドは、コントラクトと組み込みのガードを組み合わせています。unsafeインバリアントに名前を付けます。readonly、構築後に書き込みが中断されるのを防ぎます。 プロパティまたはイベント unsafe をマークしても、バッキング フィールドの呼び出し元は安全ではありません。
[StructLayout(LayoutKind.Explicit)]を持つ構造体では、すべてのフィールドをsafeまたはunsafeマークします。
safe キーワード
更新されたモデルでは、宣言を証明する safe コンテキスト キーワードが追加されます。これは、コンパイラが選択を明示的に行う必要がある場合に正しいかどうかを証明します。
extern メンバーはネイティブ コードを呼び出すので、コンパイラはその安全性を分類できません。 更新されたモデルでは、safeまたはunsafeのいずれかの部分メソッドLibraryImport含め、すべてのextern宣言をマークします。
// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();
[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);
getpid はパラメーターを受け取らず、プリミティブを返すので、作成者は呼び出しが安全であることを証明し、呼び出し元は式なしでそれを使用します。
strlen はネイティブ コードが逆参照する生ポインターを受け取るので、宣言は unsafe され、呼び出し元に義務が伝達されます。 両方の修飾子を省略するとエラーが発生するため、安全上の判断が強制されます。 明示的なレイアウトを持つ構造体内のフィールドは、同じ規則を使用します。
オプトインとクロスアセンブリの動作
更新されたモデルには、2 つの独立したプロジェクト レベル スイッチがあります。
- 新しいオプトイン プロパティは、更新されたルールをオンにします。 プロパティがオフの場合は、元のルールが適用されます。 オンの場合、メンバーの
unsafeが呼び出し元に伝達され、コンパイラはアセンブリ内の選択肢を MemorySafetyRulesAttribute 属性で記録します。 - 既存の AllowUnsafeBlocks プロパティは、呼び出しサイトの内部ブロックを含め、
unsafeキーワードのすべての外観をゲートします。 既定値はfalseであるため、既定のプロジェクトでは安全でない API を呼び出すことはできません。
2 つのプロパティは次のように結合されます。
| オプトイン プロパティ | AllowUnsafeBlocks |
Result |
|---|---|---|
| オン | オフ (既定値) | 最も安全な構成。 プロジェクトは更新されたモデルを使用し、安全でないコードを許可しません。 |
| オン | オン | プロジェクトは更新されたモデルを使用し、安全でないコードを許可します。 |
| オフ | オフ | 元のモデルが適用され、プロジェクトではポインター型を使用できません。 |
| オフ | オン | 元のモデルが適用され、プロジェクトではポインター型を使用できます。 |
あるアセンブリが別のアセンブリに対して更新された規則を適用するかどうかは、どの側がオプトインするかによって異なります。
-
更新されたモデル呼び出し元、更新されたモデルの呼び出し先: 呼び出し先の
unsafeマーカーはメタデータを通過します。 呼び出し元は、呼び出し元の安全でないメンバーへの各呼び出しをunsafeブロックでラップします。 -
更新されたモデルの呼び出し元、元のモデルの呼び出し先: 互換性モードでは、シグネチャにポインター型を持つ呼び出し先メンバーが呼び出し元セーフとして扱われます。そのため、呼び出しサイトには外側の
unsafeブロックが必要です。 このモードでは、ポインターベースの API がunsafeの要件を警告なく失うのを防ぐ。 - 元のモデル呼び出し元、更新されたモデル呼び出し先: 元のポインター規則が引き続き適用されます。 元のモデルの呼び出し元は新しいマーカーを読み取ることができないため、シグネチャにポインター型がない呼び出し元の安全でないメンバーは、安全なコードから呼び出し可能になります。
C# 言語仕様
詳細については、C# 言語仕様の安全でないコードの章を参照してください。
更新されたメモリ安全モデルの設計については、 メモリ安全機能の仕様を参照してください。
こちらも参照ください
.NET