次の方法で共有


23 安全でないコード

23.1 全般

安全でないコードをサポートしない実装は、この句で定義されている構文規則の使用法を診断するために必要です。

この句の残りの部分 (すべてのサブクラウスを含む) は、条件付きで規範的です。

: 前の句で定義されているコア C# 言語は、ポインターをデータ型として省略する点で C および C++ とは特に異なります。 代わりに、C# は参照と、ガベージ コレクターによって管理されるオブジェクトを作成する機能を提供します。 この設計は、他の機能と組み合わせて、C# を C や C++ よりもはるかに安全な言語にします。 C# のコア言語では、初期化されていない変数、"未解決" ポインター、またはその境界を超えて配列のインデックスを作成する式を持つことはできません。 したがって、C および C++ プログラムを日常的に悩ましているバグのカテゴリ全体が排除されます。

C または C++ のすべてのポインター型コンストラクトには C# で対応する参照型がありますが、それでもポインター型へのアクセスが必要になる状況があります。 たとえば、基になるオペレーティング システムとやり取りしたり、メモリ マップト デバイスにアクセスしたり、タイム クリティカルなアルゴリズムを実装したりすることは、ポインターにアクセスしないと不可能または実用的でない場合があります。 このニーズに対処するために、C# は安全なコード 記述する機能を提供

安全でないコードでは、ポインターを宣言して操作したり、ポインターと整数型の間の変換を実行したり、変数のアドレスを受け取ったりすることができます。 ある意味では、安全でないコードを記述することは、C# プログラム内で C コードを記述するのとよく似ています。

安全でないコードは、実際には開発者とユーザーの両方の観点から見た "安全な" 機能です。 安全でないコードは修飾子 unsafeで明確にマークされるため、開発者が誤って安全でない機能を使用することはできず、実行エンジンは信頼されていない環境で安全でないコードを実行できないように動作します。

end note

23.2 安全でないコンテキスト

C# の安全でない機能は、安全でないコンテキストでのみ使用できます。 安全でないコンテキストは、型、メンバー、またはローカル関数の宣言に unsafe 修飾子を含めるか、 unsafe_statementを使用して導入されます。

  • クラス、構造体、インターフェイス、またはデリゲートの宣言には、 unsafe 修飾子を含めることができます。その場合、その型宣言のテキストエクステント全体 (クラス、構造体、またはインターフェイスの本体を含む) は安全でないコンテキストと見なされます。

    : type_declaration が部分的な場合、その部分のみが安全でないコンテキストです。 end note

  • フィールド、メソッド、プロパティ、イベント、インデクサー、演算子、インスタンス コンストラクター、ファイナライザー、静的コンストラクター、またはローカル関数の宣言には、 unsafe 修飾子を含めることができます。その場合、そのメンバー宣言のテキストエクステント全体が安全でないコンテキストと見なされます。
  • unsafe_statementを使用すると、block内で安全でないコンテキストを使用できます。 関連付けられた block のテキストエクステント全体は、安全でないコンテキストと見なされます。 安全でないコンテキスト内で宣言されたローカル関数自体は安全ではありません。

関連する文法拡張を以下に示し、その後のサブクラウスで示します。

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

: 次のコード内

public unsafe struct Node
{
    public int Value;
    public Node* Left;
    public Node* Right;
}

構造体宣言で指定された unsafe 修飾子により、構造体宣言のテキストエクステント全体が安全でないコンテキストになります。 したがって、 Left および Right フィールドをポインター型として宣言できます。 上記の例も記述できます

public struct Node
{
    public int Value;
    public unsafe Node* Left;
    public unsafe Node* Right;
}

ここでは、フィールド宣言の unsafe 修飾子によって、これらの宣言が安全でないコンテキストと見なされます。

end の例

安全でないコンテキストを確立してポインター型の使用を許可する以外に、 unsafe 修飾子は型またはメンバーには影響しません。

: 次のコード内

public class A
{
    public unsafe virtual void F() 
    {
        char* p;
        ...
    }
}

public class B : A
{
    public override void F() 
    {
        base.F();
        ...
    }
}

AFメソッドの安全でない修飾子は、Fのテキスト範囲を、言語の安全でない機能を使用できる安全でないコンテキストになります。 BFのオーバーライドでは、unsafe修飾子を再指定する必要はありません。もちろん、BFメソッド自体が安全でない機能にアクセスする必要がある場合を除きます。

ポインター型がメソッドのシグネチャの一部である場合、状況は若干異なります

public unsafe class A
{
    public virtual void F(char* p) {...}
}

public class B: A
{
    public unsafe override void F(char* p) {...}
}

ここでは、 Fのシグネチャにはポインター型が含まれているため、安全でないコンテキストでのみ記述できます。 ただし、安全でないコンテキストは、Aの場合と同様にクラス全体を安全にするか、Bの場合と同様に、メソッド宣言にunsafe修飾子を含めることで導入できます。

end の例

部分型宣言 (§15.2.7) でunsafe修飾子が使用されている場合、その特定の部分のみが安全でないコンテキストと見なされます。

23.3 ポインター型

安全でないコンテキストでは、 (§8.1) は、 pointer_type だけでなく、 value_typereference_type、または type_parameterにすることができます。 安全でないコンテキストでは、 pointer_type は配列の要素型である場合もあります (§17)。 pointer_typeは、安全でないコンテキストの外部 (§12.8.18) の型式でも使用できます (このような使用は安全ではありません)。

pointer_typeは、unmanaged_type (§8.8) またはキーワード voidとして書き込まれ、その後に* トークンが続きます。

pointer_type
    : value_type ('*')+
    | 'void' ('*')+
    ;

ポインター型の * の前に指定された型は、ポインター型の 参照先の型 と呼ばれます。 これは、ポインター型の値が指す変数の型を表します。

pointer_typeは、安全でないコンテキスト (§23.2) のarray_typeでのみ使用できます。 non_array_typeは、それ自体がarray_typeではない任意の型です。

参照 (参照型の値) とは異なり、ポインターはガベージ コレクターによって追跡されません。ガベージ コレクターはポインターと、ポインターが指すデータを認識しません。 このため、ポインターは参照または参照を含む構造体を指すのを許可されず、ポインターの参照先の型は unmanaged_typeである必要があります。 ポインター型自体はアンマネージ型であるため、ポインター型は別のポインター型の参照先型として使用できます。

ポインターと参照を混在させる直感的なルールは、参照 (オブジェクト) の参照先にはポインターを含めるが、ポインターの参照先には参照を含めないことを意味します。

: ポインター型の例をいくつか次の表に示します。

説明
byte* へのポインター byte
char* へのポインター char
int** ポインターへのポインター int
int*[] へのポインターの 1 次元配列 int
void* 不明な型へのポインター

end の例

特定の実装では、すべてのポインター型のサイズと表現が同じである必要があります。

: C および C++ とは異なり、複数のポインターが同じ宣言で宣言されている場合、C# では、 * は基になる型と共にのみ書き込まれ、各ポインター名のプレフィックス区切り記号として書き込まれません。 次に例を示します。

int* pi, pj; // NOT as int *pi, *pj;  

end note

T* を持つポインターの値は、 T型の変数のアドレスを表します。 ポインター間接演算子 * (§23.6.2) を使用して、この変数にアクセスできます。

: int*型の変数Pを指定すると、式*Pは、Pに含まれるアドレスにあるint変数を表します。 end の例

オブジェクト参照と同様に、ポインターを nullできます。 間接演算子を null値ポインターに適用すると、実装定義の動作になります (§23.6.2)。 値 null を持つポインターは、すべてビット 0 で表されます。

void*型は、不明な型へのポインターを表します。 参照先の型は不明であるため、間接演算子を void*型のポインターに適用することも、このようなポインターに対して算術演算を実行することもできません。 ただし、 void* 型のポインターは、他のポインター型 (およびその逆) にキャストでき、他のポインター型 (§23.6.8) の値と比較できます。

ポインター型は、型の別のカテゴリです。 参照型と値型とは異なり、ポインター型は object から継承されず、ポインター型と objectの間に変換は存在しません。 特に、ポインターのボックス化とボックス化解除 (§8.3.13) はサポートされていません。 ただし、異なるポインター型間、およびポインター型と整数型の間で変換が許可されます。 これについては、 §23.5 で説明されています。

型引数 (§8.4) としてpointer_typeを使用することはできません。型引数をポインター型として推論したジェネリック メソッド呼び出しでは、型推論 (§12.6.3) は失敗します。

動的にバインドされた操作の部分式の型として pointer_type を使用することはできません (§12.3.3)。

拡張メソッド (§15.6.10) では、pointer_typeを最初のパラメーターの型として使用することはできません。

pointer_typeは、揮発性フィールド (§15.5.4) の型として使用できます。

E*型の動的消去は、Eの動的消去の参照先型を持つポインター型です。

ポインター型を持つ式を使用して、anonymous_object_creation_expression内のmember_declaratorの値を指定することはできません (§12.8.17.7)。

ポインター型の既定値 (§9.3) が null

: ポインターは参照渡しパラメーターとして渡すことができますが、呼び出されたメソッドが戻ったときに存在しなくなったローカル変数を指すようにポインターが設定される場合や、ポインターが指していた固定オブジェクトが固定されなくなったため、未定義の動作が発生する可能性があります。 次に例を示します。

class Test
{
    static int value = 20;

    unsafe static void F(out int* pi1, ref int* pi2) 
    {
        int i = 10;
        pi1 = &i;       // return address of local variable
        fixed (int* pj = &value)
        {
            // ...
            pi2 = pj;   // return address that will soon not be fixed
        }
    }

    static void Main()
    {
        int i = 15;
        unsafe 
        {
            int* px1;
            int* px2 = &i;
            F(out px1, ref px2);
            int v1 = *px1; // undefined
            int v2 = *px2; // undefined
        }
    }
}

end note

メソッドは、何らかの型の値を返すことができます。また、その型はポインターにすることができます。

: intの連続したシーケンス、そのシーケンスの要素数、およびその他の int 値へのポインターを指定すると、次のメソッドは、一致する場合、そのシーケンス内のその値のアドレスを返します。それ以外の場合は、 nullを返します。

unsafe static int* Find(int* pi, int size, int value)
{
    for (int i = 0; i < size; ++i)
    {
        if (*pi == value)
        {
            return pi;
        }
        ++pi;
    }
    return null;
}

end の例

安全でないコンテキストでは、ポインターで操作するためにいくつかのコンストラクトを使用できます。

  • 単項 * 演算子は、ポインター間接参照 (§23.6.2) を実行するために使用できます。
  • ->演算子は、ポインター (§23.6.3) を介して構造体のメンバーにアクセスするために使用できます。
  • []演算子を使用してポインターのインデックスを作成できます (§23.6.4)。
  • 単項 & 演算子を使用して、変数のアドレスを取得できます (§23.6.5)。
  • ++演算子と--演算子は、ポインターのインクリメントとデクリメントに使用できます (§23.6.6)。
  • 二項 + 演算子と - 演算子を使用してポインター演算を実行できます (§23.6.7)。
  • ポインターの比較には、 ==!=<><=、および >= 演算子を使用できます (§23.6.8)。
  • stackalloc演算子を使用して、呼び出し履歴からメモリを割り当てることができます (§23.9)。
  • fixedステートメントを使用して、変数のアドレスを取得できるように変数を一時的に修正できます (§23.7)。

23.4 固定変数と移動可能変数

address-of 演算子 (§23.6.5) と fixed ステートメント (§23.7) は、変数を Fixed 変数 および moveable 変数の 2 つのカテゴリに分割

固定変数は、ガベージ コレクターの操作の影響を受けないストレージの場所に存在します。 (固定変数の例としては、ローカル変数、値パラメーター、逆参照ポインターによって作成された変数などがあります)。一方、移動可能変数は、ガベージ コレクターによる再配置または破棄の対象となるストレージの場所に存在します。 (移動可能な変数の例には、オブジェクト内のフィールドと配列の要素が含まれます)。

&演算子 (§23.6.5) を使用すると、固定変数のアドレスを制限なしで取得できます。 ただし、移動可能変数はガベージ コレクターによる再配置または破棄の対象となるため、移動可能変数のアドレスは fixed statement (§23.7) を使用してのみ取得でき、そのアドレスは fixed ステートメントの期間中のみ有効です。

正確に言うと、固定変数は次のいずれかです。

  • 変数が匿名関数 (§12.19.6.2) によってキャプチャされない限り、ローカル変数、値パラメーター、またはパラメーター配列を参照するsimple_name (§12.8.4) から生成される変数。
  • フォーム V.Imember_access (§12.8.7) に起因する変数。ここで、Vstruct_typeの固定変数です。
  • フォーム *Ppointer_indirection_expression (§23.6.2)、フォーム P->Ipointer_member_access (§23.6.3)、またはフォーム P[E]pointer_element_access (§23.6.4) から得られる変数。

他のすべての変数は、移動可能変数として分類されます。

静的フィールドは、移動可能な変数として分類されます。 また、パラメーターに指定された引数が固定変数の場合でも、参照渡しパラメーターは移動可能な変数として分類されます。 最後に、ポインターを逆参照することによって生成される変数は、常に固定変数として分類されます。

23.5 ポインター変換

23.5.1 全般

安全でないコンテキストでは、使用可能な暗黙的な変換のセット (§10.2) が拡張され、次の暗黙的なポインター変換が含まれます。

  • 任意の pointer_type から型 void*
  • nullリテラル (§6.4.5.7) から任意のpointer_typeへ。

さらに、安全でないコンテキストでは、使用可能な明示的な変換のセット (§10.3) が拡張され、次の明示的なポインター変換が含まれます。

  • 任意の pointer_type から他の pointer_typeまで。
  • sbytebyteshortushortintuintlong、またはulongから任意のpointer_type
  • 任意の pointer_type から、 sbytebyteshortushortintuintlong、または ulongまで。

最後に、安全でないコンテキストでは、標準の暗黙的な変換のセット (§10.4.2) には、次のポインター変換が含まれます。

  • 任意の pointer_type から型 void*
  • nullリテラルから任意のpointer_typeへ。

2 つのポインター型間の変換では、実際のポインター値は変更されません。 つまり、あるポインター型から別のポインター型への変換は、ポインターによって指定された基になるアドレスには影響しません。

あるポインター型が別のポインター型に変換されるときに、結果のポインターがポイント先の型に対して正しく配置されていない場合、結果が逆参照された場合、動作は未定義になります。 一般に、"正しく配置" という概念は推移的です。型 A へのポインターが型 Bへのポインターに対して正しく配置され、そのポインターが型 Cへのポインターに対して正しく配置されている場合、 A 型へのポインターは型 Cへのポインターに対して正しく配置されます。

: 1 つの型を持つ変数に別の型へのポインターを介してアクセスする場合を考えてみます。

unsafe static void M()
{
    char c = 'A';
    char* pc = &c;
    void* pv = pc;
    int* pi = (int*)pv; // pretend a 16-bit char is a 32-bit int
    int i = *pi;        // read 32-bit int; undefined
    *pi = 123456;       // write 32-bit int; undefined
}

end の例

ポインター型が byteへのポインターに変換されると、結果は変数の最下位のアドレス指定された byte を指します。 変数のサイズまで、結果の連続したインクリメントにより、その変数の残りのバイトへのポインターが生成されます。

: 次のメソッドは、 double 内の 8 バイトをそれぞれ 16 進数の値として表示します。

class Test
{
    static void Main()
    {
        double d = 123.456e23;
        unsafe
        {
            byte* pb = (byte*)&d;
            for (int i = 0; i < sizeof(double); ++i)
            {
                Console.Write($" {*pb++:X2}");
            }
            Console.WriteLine();
        }
    }
}

もちろん、生成される出力はエンディアンによって異なります。 1 つの可能性は " BA FF 51 A2 90 6C 24 45"

end の例

ポインターと整数の間のマッピングは、実装で定義されます。

: ただし、線形アドレス空間を持つ 32 ビットおよび 64 ビットの CPU アーキテクチャでは、整数型との間でのポインターの変換は、通常、 uint 値または ulong 値の変換とまったく同じように動作します。 end note

23.5.2 ポインター配列

ポインターの配列は、安全でないコンテキストで array_creation_expression (§12.8.17.5) を使用して構築できます。 ポインター配列では、他の配列型に適用される一部の変換のみが許可されます。

  • 任意のarray_typeからSystem.Arrayへの暗黙的な参照変換 (§10.2.8) と、それが実装するインターフェイスもポインター配列に適用されます。 ただし、ポインター型はobjectに変換されないため、System.Arrayまたは実装するインターフェイスを介して配列要素にアクセスしようとすると、実行時に例外が発生する可能性があります。
  • 1 次元配列 S[]型から System.Collections.Generic.IList<T> への暗黙的および明示的な参照変換 (§10.2.8§10.3.5) はポインター配列には適用されません。
  • System.Arrayからの明示的な参照変換 (§10.3.5) と、array_typeに実装されるインターフェイスはポインター配列に適用されます。
  • ポインター型は型引数として使用できず、ポインター型から非ポインター型への変換がないため、System.Collections.Generic.IList<S>とその基本インターフェイスから単一次元配列型への明示的な参照変換 (T[] §10.3.5) はポインター配列には適用されません。

これらの制限は、§9.4.4.17 で説明されている配列に対するforeach ステートメントの拡張ポインター配列に適用できないことを意味します。 代わりに、フォームの foreach ステートメント

foreach (V v in x)embedded_statement

ここで、 x の型はフォーム T[,,...,]の配列型です。 n は次元の数から 1 を引いた値で、 T または V はポインター型であり、次のように入れ子になった for ループを使用して展開されます。

{
    T[,,...,] a = x;
    for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
    {
        for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
        {
            ...
            for (int in = a.GetLowerBound(n); in <= a.GetUpperBound(n); in++) 
            {
                V v = (V)a[i0,i1,...,in];
                *embedded_statement*
            }
        }
    }
}

変数 ai0i1、... in は、 xembedded_statement 、またはプログラムのその他のソース コードに対して表示またはアクセスできません。 v変数は、埋め込みステートメントでは読み取り専用です。 T (要素型) からVへの明示的な変換 (§23.5) がない場合は、エラーが生成され、それ以上の手順は実行されません。 x値がnull場合、実行時にSystem.NullReferenceExceptionがスローされます。

: ポインター型は型引数として使用できませんが、ポインター配列は型引数として使用できます。 end note

23.6 式内のポインター

23.6.1 全般

アンセーフ コンテキストでは、式はポインター型の結果を生成する可能性がありますが、安全でないコンテキストの外部では、式がポインター型のコンパイル時エラーになります。 正確には、安全でないコンテキストの外部では、 simple_name (§12.8.4) member_access (§12.8.7)、 invocation_expression (§12.8.10)、または element_access (§12.8.12) がポインター型です。

安全でないコンテキストでは、 primary_no_array_creation_expression (§12.8) と unary_expression (§12.9) の運用環境では追加のコンストラクトが許可されます。これは、次のサブクラウスで説明します。

: 安全でない演算子の優先順位と結合規則は、文法によって暗黙的に示されます。 end note

23.6.2 ポインター間接参照

pointer_indirection_expressionは、アスタリスク (*) とそれに続くunary_expressionで構成されます。

pointer_indirection_expression
    : '*' unary_expression
    ;

単項 * 演算子はポインター間接参照を示し、ポインターが指す変数を取得するために使用されます。 Pがポインター型T*の式である*Pを評価した結果は、T型の変数です。 単項 * 演算子を void* 型の式またはポインター型ではない式に適用するのはコンパイル時エラーです。

単項 * 演算子を null値ポインターに適用した場合の効果は、実装で定義されます。 特に、この操作で System.NullReferenceExceptionがスローされる保証はありません。

ポインターに無効な値が割り当てられている場合、単項 * 演算子の動作は未定義です。

: 単項 * 演算子によってポインターを逆参照するための無効な値の中には、指す型 ( §23.5 の例を参照)、および有効期間終了後の変数のアドレスに対して不適切に配置されたアドレスがあります。

明確な代入分析のために、フォーム *P の式を評価することによって生成された変数は、最初に代入されたものと見なされます (§9.4.2)。

23.6.3 ポインター メンバー アクセス

pointer_member_accessは、primary_expression、その後に "->" トークン、その後に identifier および省略可能なtype_argument_listで構成されます。

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

フォーム P->Iのポインター メンバー アクセスでは、 P ポインター型の式を指定し、 I は、 P がポイントする型のアクセス可能なメンバーを示す必要があります。

フォーム P->I のポインター メンバー アクセスは、 (*P).Iとまったく同じように評価されます。 ポインター間接演算子 (*) の説明については、 §23.6.2 を参照してください。 メンバー アクセス演算子 (.) の説明については、 §12.8.7 を参照してください。

: 次のコード内

struct Point
{
    public int x;
    public int y;
    public override string ToString() => $"({x},{y})";
}

class Test
{
    static void Main()
    {
        Point point;
        unsafe
        {
            Point* p = &point;
            p->x = 10;
            p->y = 20;
            Console.WriteLine(p->ToString());
        }
    }
}

->演算子は、フィールドにアクセスし、ポインターを介して構造体のメソッドを呼び出すために使用されます。 操作 P->I(*P).Iと正確に等しいので、 Main メソッドも同様に記述されている可能性があります。

class Test
{
    static void Main()
    {
        Point point;
        unsafe
        {
            Point* p = &point;
            (*p).x = 10;
            (*p).y = 20;
            Console.WriteLine((*p).ToString());
        }
    }
}

end の例

23.6.4 ポインター要素のアクセス

pointer_element_accessは、primary_no_array_creation_expressionの後に、"[" と "]" で囲まれた式で構成されます。

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

フォーム P[E]のポインター要素アクセスでは、 Pvoid*以外のポインター型の式で、 Eintuintlong、または ulongに暗黙的に変換できる式でなければなりません。

フォーム P[E] のポインター要素のアクセスは、 *(P + E)とまったく同じように評価されます。 ポインター間接演算子 (*) の説明については、 §23.6.2 を参照してください。 ポインター加算演算子 (+) の説明については、 §23.6.7 を参照してください。

: 次のコード内

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                p[i] = (char)i;
            }
        }
    }
}

ポインター要素のアクセスは、 for ループ内の文字バッファーを初期化するために使用されます。 操作 P[E]*(P + E)と正確に等しいので、例も同様に記述されている可能性があります。

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                *(p + i) = (char)i;
            }
        }
    }
}

end の例

ポインター要素のアクセス演算子は、範囲外のエラーをチェックしません。また、境界外の要素にアクセスするときの動作は未定義です。

: これは C および C++ と同じです。 end note

23.6.5 address-of 演算子

addressof_expressionはアンパサンド (&) とそれに続くunary_expressionで構成されます。

addressof_expression
    : '&' unary_expression
    ;

ET型であり、固定変数 (§23.4) として分類されると、コンストラクト&EEによって指定された変数のアドレスが計算されます。 結果の型は T* され、値として分類されます。 コンパイル時エラーは、 E が変数として分類されていない場合、 E が読み取り専用のローカル変数として分類されている場合、または移動可能な変数 E 示している場合に発生します。 最後のケースでは、固定ステートメント (§23.7) を使用して、アドレスを取得する前に変数を一時的に "修正" できます。

: §12.8.7 に記載されているようにreadonly フィールドを定義する構造体またはクラスのインスタンス コンストラクターまたは静的コンストラクターの外側では、そのフィールドは変数ではなく値と見なされます。 そのため、そのアドレスは取得できません。 同様に、定数のアドレスは取得できません。 end note

&演算子では、引数を確実に割り当てる必要はありませんが、&操作の後、操作が適用される変数は、操作が実行される実行パスで確実に割り当てられていると見なされます。 この状況で変数の正しい初期化が実際に行われることを保証するのはプログラマの責任です。

: 次のコード内

class Test
{
    static void Main()
    {
        int i;
        unsafe
        {
            int* p = &i;
            *p = 123;
        }
        Console.WriteLine(i);
    }
}

iは、pの初期化に使用される&i操作に従って確実に割り当てられていると見なされます。 *pへの割り当ては実際にはi初期化されますが、この初期化を含めることはプログラマの責任であり、割り当てが削除された場合、コンパイル時エラーは発生しません。

end の例

: ローカル変数の冗長な初期化を回避できるように、 & 演算子の明確な割り当ての規則が存在します。 たとえば、多くの外部 API は、API によって入力される構造体へのポインターを受け取ります。 このような API の呼び出しは、通常、ローカル構造体変数のアドレスを渡します。規則がない場合は、構造体変数の冗長な初期化が必要になります。 end note

: ローカル変数、値パラメーター、またはパラメーター配列が匿名関数 (§12.8.24) によってキャプチャされた場合、そのローカル変数、パラメーター、またはパラメーター配列は固定変数 (§23.7) と見なされなくなりましたが、代わりに移動可能変数と見なされます。 したがって、匿名関数によってキャプチャされたローカル変数、値パラメーター、またはパラメーター配列のアドレスを取得するのは、安全でないコードのエラーです。 end note

23.6.6 ポインターのインクリメントとデクリメント

安全でないコンテキストでは、 ++ 演算子と -- 演算子 (§12.8.16 および §12.9.6) を、 void*を除くすべての型のポインター変数に適用できます。 したがって、ポインター型 T*ごとに、次の演算子が暗黙的に定義されます。

T* operator ++(T* x);
T* operator --(T* x);

演算子は、それぞれ x+1 および x-1 と同じ結果を生成します (§23.6.7)。 つまり、 T*型のポインター変数の場合、 ++ 演算子は変数に含まれるアドレスに sizeof(T) を追加し、 -- 演算子は変数に含まれるアドレスから sizeof(T) を減算します。

ポインターのインクリメントまたはデクリメント操作によってポインター型のドメインがオーバーフローした場合、結果は実装定義になりますが、例外は生成されません。

23.6.7 ポインターの算術演算

安全でないコンテキストでは、 + 演算子 (§12.10.5) と - 演算子 (§12.10.6) を、 void*を除くすべてのポインター型の値に適用できます。 したがって、ポインター型 T*ごとに、次の演算子が暗黙的に定義されます。

T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);

ポインター型T*の式Pと、型intuintlong、またはulongの式Nを指定すると、式はP + Nし、Pによって指定されたアドレスにN * sizeof(T)を追加した結果T*型のポインター値を計算N + P。 同様に、式P – Nは、Pによって指定されたアドレスからN * sizeof(T)を減算した結果の型T*のポインター値を計算します。

ポインター型T*PQの 2 つの式を指定すると、式P – QPQによって指定されたアドレスの差を計算し、その差をsizeof(T)で除算します。 結果の型は常に long。 実際には、 P - Q((long)(P) - (long)(Q)) / sizeof(T)として計算されます。

例:

class Test
{
    static void Main()
    {
        unsafe
        {
            int* values = stackalloc int[20];
            int* p = &values[1];
            int* q = &values[15];
            Console.WriteLine($"p - q = {p - q}");
            Console.WriteLine($"q - p = {q - p}");
        }
    }
}

出力を生成します。

p - q = -14
q - p = 14

end の例

ポインターの算術演算でポインター型のドメインがオーバーフローした場合、結果は実装定義の方法で切り捨てられますが、例外は生成されません。

23.6.8 ポインターの比較

安全でないコンテキストでは、 ==!=<><=、および >= 演算子 (§12.12) をすべてのポインター型の値に適用できます。 ポインター比較演算子は次のとおりです。

bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);

ポインター型から void* 型への暗黙的な変換が存在するため、ポインター型のオペランドは、これらの演算子を使用して比較できます。 比較演算子は、2 つのオペランドによって指定されたアドレスを符号なし整数であるかのように比較します。

23.6.9 sizeof 演算子

特定の定義済み型 (§12.8.19) の場合、 sizeof 演算子は定数 int 値を生成します。 その他のすべての型では、 sizeof 演算子の結果は実装定義であり、定数ではなく値として分類されます。

メンバーが構造体にパックされる順序は指定されていません。

配置の目的で、構造体の先頭、構造体内、および構造体の末尾に、名前のないパディングが存在する可能性があります。 パディングとして使用されるビットの内容は不確定です。

構造体型を持つオペランドに適用すると、結果は、埋め込みを含む、その型の変数内の合計バイト数になります。

23.7 固定ステートメント

安全でないコンテキストでは、 embedded_statement (§13.1) の運用環境では、追加のコンストラクト (固定ステートメント) が許可されます。固定ステートメントは、移動可能な変数を "修正" するために使用され、そのアドレスはステートメントの期間中一定のままです。

fixed_statement
    : 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
    ;

fixed_pointer_declarators
    : fixed_pointer_declarator (','  fixed_pointer_declarator)*
    ;

fixed_pointer_declarator
    : identifier '=' fixed_pointer_initializer
    ;

fixed_pointer_initializer
    : '&' variable_reference
    | expression
    ;

fixed_pointer_declarator は、指定された pointer_type のローカル変数を宣言し、対応する fixed_pointer_initializerによって計算されたアドレスを使用してそのローカル変数を初期化します。 固定ステートメントで宣言されたローカル変数には、その変数の宣言の右側で発生する fixed_pointer_initializerと、固定ステートメントの embedded_statement でアクセスできます。 固定ステートメントによって宣言されたローカル変数は、読み取り専用と見なされます。 コンパイル時エラーは、埋め込みステートメントが (代入演算子または ++ 演算子と -- 演算子を使用して) このローカル変数を変更しようとした場合、または参照パラメーターまたは出力パラメーターとして渡そうとした場合に発生します。

キャプチャされたローカル変数 (§12.19.6.2)、値パラメーター、またはパラメーター配列を fixed_pointer_initializerで使用するとエラーになります。 fixed_pointer_initializerには、次のいずれかを指定できます。

  • トークン "&" の後に、アンマネージ型Tの移動可能変数 (§23.4) へのvariable_reference (§9.5) が続きます。ただし、T*型は、fixed ステートメントで指定されたポインター型に暗黙的に変換できます。 この場合、初期化子は指定された変数のアドレスを計算します。変数は、固定ステートメントの期間中、固定アドレスのままであることが保証されます。
  • アンマネージ型Tの要素を持つarray_typeの式。型T*が、固定ステートメントで指定されたポインター型に暗黙的に変換可能である場合。 この場合、初期化子は配列内の最初の要素のアドレスを計算します。配列全体は、 fixed ステートメントの間、固定アドレスのままであることが保証されます。 配列式が null の場合、または配列に要素が 0 の場合、初期化子は 0 に等しいアドレスを計算します。
  • char*型が、fixed ステートメントで指定されたポインター型に暗黙的に変換できる場合、string型の式。 この場合、初期化子は文字列内の最初の文字のアドレスを計算し、文字列全体が fixed ステートメントの間、固定アドレスのままであることが保証されます。 文字列式がnullされている場合、fixed ステートメントの動作は実装で定義されます。
  • シグネチャ ref [readonly] T GetPinnableReference()に一致するアクセス可能なメソッドまたはアクセス可能な拡張メソッドが存在する場合、array_typeまたはstring以外の型の式。Tunmanaged_typeであり、T*fixed ステートメントで指定されたポインター型に暗黙的に変換できます。 この場合、初期化子は返された変数のアドレスを計算し、その変数は、 fixed ステートメントの期間中、固定アドレスのままであることが保証されます。 オーバーロード解決 (§12.6.4) によって関数メンバーが 1 つだけ生成され、その関数メンバーが上記の条件を満たす場合は、GetPinnableReference() メソッドを fixed ステートメントで使用できます。 GetPinnableReference メソッドは、ピン留めするデータがない場合にSystem.Runtime.CompilerServices.Unsafe.NullRef<T>()から返されるなど、0 に等しいアドレスへの参照を返す必要があります。
  • 固定サイズのバッファー メンバーの型が、fixed ステートメントで指定されたポインター型に暗黙的に変換できる場合に、移動可能な変数の固定サイズバッファー メンバーを参照するsimple_nameまたはmember_access。 この場合、初期化子は固定サイズ バッファー (§23.8.3) の最初の要素へのポインターを計算し、固定サイズ バッファーは、 fixed ステートメントの間、固定アドレスに残ります。

fixed_pointer_initializerによって計算された各アドレスについて、fixed ステートメントは、fixed ステートメントの期間中、アドレスによって参照される変数がガベージ コレクターによる再配置または破棄の対象にならないことを保証します。

: fixed_pointer_initializer によって計算されたアドレスがオブジェクトのフィールドまたは配列インスタンスの要素を参照する場合、固定ステートメントは、ステートメントの有効期間中に、含まれるオブジェクト インスタンスが再配置または破棄されないことを保証します。 end の例

固定ステートメントによって作成されたポインターが、これらのステートメントの実行を超えて存続しないようにするのはプログラマの責任です。

: fixed ステートメントによって作成されたポインターが外部 API に渡される場合、API がこれらのポインターのメモリを保持しないようにするのはプログラマの責任です。 end の例

固定オブジェクトは、ヒープの断片化を引き起こす可能性があります (移動できないため)。 そのため、オブジェクトは、絶対に必要な場合にのみ固定し、可能な限り最短の時間だけ固定する必要があります。

: 例

class Test
{
    static int x;
    int y;

    unsafe static void F(int* p)
    {
        *p = 1;
    }

    static void Main()
    {
        Test t = new Test();
        int[] a = new int[10];
        unsafe
        {
            fixed (int* p = &x) F(p);
            fixed (int* p = &t.y) F(p);
            fixed (int* p = &a[0]) F(p);
            fixed (int* p = a) F(p);
        }
    }
}

は、 fixed ステートメントのいくつかの使用方法を示しています。 最初のステートメントは静的フィールドのアドレスを修正して取得し、2 番目のステートメントはインスタンス フィールドのアドレスを修正して取得し、3 番目のステートメントは配列要素のアドレスを修正して取得します。 いずれの場合も、変数はすべて移動可能な変数として分類されるため、通常の & 演算子を使用するのはエラーでした。

上記の例の 3 番目と 4 番目の fixed ステートメントでは、同じ結果が生成されます。 一般に、配列インスタンスaの場合、fixed ステートメントでa[0]を指定することは、単にaを指定するのと同じです。

end の例

安全でないコンテキストでは、1 次元配列の配列要素は、インデックスの 0 から始まり、インデックス Length – 1で終わるインデックスの順序で格納されます。 多次元配列の場合、配列要素は、右端の次元のインデックスが最初に増加し、次に次の左の次元が左に増加するように格納されます。

配列インスタンス aへのポインターpを取得するfixed ステートメント内で、pからp + a.Length - 1までのポインター値は、配列内の要素のアドレスを表します。 同様に、 p[0] から p[a.Length - 1] までの変数は、実際の配列要素を表します。 配列を格納する方法を考えると、次元の配列は線形であるかのように扱うことができます。

例:

class Test
{
    static void Main()
    {
        int[,,] a = new int[2,3,4];
        unsafe
        {
            fixed (int* p = a)
            {
                for (int i = 0; i < a.Length; ++i) // treat as linear
                {
                    p[i] = i;
                }
            }
        }
        for (int i = 0; i < 2; ++i)
        {
            for (int j = 0; j < 3; ++j)
            {
                for (int k = 0; k < 4; ++k)
                {
                    Console.Write($"[{i},{j},{k}] = {a[i,j,k],2} ");
                }
                Console.WriteLine();
            }
        }
    }
}

出力を生成します。

[0,0,0] =  0 [0,0,1] =  1 [0,0,2] =  2 [0,0,3] =  3
[0,1,0] =  4 [0,1,1] =  5 [0,1,2] =  6 [0,1,3] =  7
[0,2,0] =  8 [0,2,1] =  9 [0,2,2] = 10 [0,2,3] = 11
[1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15
[1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19
[1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23

end の例

: 次のコード内

class Test
{
    unsafe static void Fill(int* p, int count, int value)
    {
        for (; count != 0; count--)
        {
            *p++ = value;
        }
    }

    static void Main()
    {
        int[] a = new int[100];
        unsafe
        {
            fixed (int* p = a) Fill(p, 100, -1);
        }
    }
}

fixed ステートメントは、ポインターを受け取るメソッドにアドレスを渡すことができるように、配列を修正するために使用されます。

end の例

文字列インスタンスを修正することによって生成される char* 値は、常に null で終わる文字列を指します。 文字列インスタンスsへのポインターpを取得する固定ステートメント内では、pからp + s.Length ‑ 1までのポインター値は文字列内の文字のアドレスを表し、ポインター値p + s.Lengthは常に null 文字 (値 '\0' の文字) を指します。

例:

class Test
{
    static string name = "xx";

    unsafe static void F(char* p)
    {
        for (int i = 0; p[i] != '\0'; ++i)
        {
            System.Console.WriteLine(p[i]);
        }
    }

    static void Main()
    {
        unsafe
        {
            fixed (char* p = name) F(p);
            fixed (char* p = "xx") F(p);
        }
    }
}

end の例

: 次のコードは、array_typeまたはstring以外の型の式を持つfixed_pointer_initializerを示しています。

public class C
{
    private int _value;
    public C(int value) => _value = value;
    public ref int GetPinnableReference() => ref _value;
}

public class Test
{
    unsafe private static void Main()
    {
        C c = new C(10);
        fixed (int* p = c)
        {
            // ...
        }
    }
}

C には、正しいシグネチャを持つアクセス可能な GetPinnableReference メソッドがあります。 fixed ステートメントでは、cで呼び出されたときにそのメソッドから返されるref intを使用して、int* ポインターのpを初期化します。 end の例

固定ポインターを使用してマネージド型のオブジェクトを変更すると、未定義の動作が発生する可能性があります。

: たとえば、文字列は不変であるため、固定文字列へのポインターによって参照される文字が変更されないようにするのはプログラマの責任です。 end note

: 文字列の自動 null 終端は、"C スタイル" 文字列を必要とする外部 API を呼び出すときに特に便利です。 ただし、文字列インスタンスには null 文字を含めるのが許可されていることに注意してください。 このような null 文字が存在する場合、null で終わる char*として扱われると、文字列は切り捨てられた状態で表示されます。 end note

23.8 固定サイズバッファー

23.8.1 全般

固定サイズ バッファーは、"C スタイル" インライン配列を構造体のメンバーとして宣言するために使用され、主にアンマネージ API とのインターフェイスに役立ちます。

23.8.2 固定サイズのバッファー宣言

固定サイズバッファーは、特定の型の変数の固定長バッファーのストレージを表すメンバーです。 固定サイズ バッファー宣言では、特定の要素型の 1 つ以上の固定サイズ バッファーが導入されます。

: 配列と同様に、固定サイズのバッファーは要素を含むものとして考えることができます。 そのため、配列に対して定義されている 要素型 という用語は、固定サイズのバッファーでも使用されます。 end note

固定サイズ バッファーは構造体宣言でのみ許可され、安全でないコンテキストでのみ発生する可能性があります (§23.2)。

fixed_size_buffer_declaration
    : attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type
      fixed_size_buffer_declarators ';'
    ;

fixed_size_buffer_modifier
    : 'new'
    | 'public'
    | 'internal'
    | 'private'
    | 'unsafe'
    ;

buffer_element_type
    : type
    ;

fixed_size_buffer_declarators
    : fixed_size_buffer_declarator (',' fixed_size_buffer_declarator)*
    ;

fixed_size_buffer_declarator
    : identifier '[' constant_expression ']'
    ;

固定サイズのバッファー宣言には、属性のセット (§22)、 new 修飾子 (§15.3.5)、アクセシビリティ修飾子を含めることができます。 構造体メンバー (§16.4.3) および unsafe 修飾子 (§23.2) に対して許可されている任意の宣言されたアクセシビリティ。 属性と修飾子は、固定サイズバッファー宣言によって宣言されたすべてのメンバーに適用されます。 固定サイズのバッファー宣言で同じ修飾子が複数回出現するのはエラーです。

固定サイズのバッファー宣言には、 static 修飾子を含められません。

固定サイズのバッファー宣言のバッファー要素型は、宣言によって導入されたバッファーの要素型を指定します。 バッファー要素の型は、定義済みの型 sbytebyteshortushortintuintlongulongcharfloatdouble、または boolのいずれかになります。

バッファー要素の型の後に、固定サイズのバッファー宣言子の一覧が続き、それぞれが新しいメンバーを導入します。 固定サイズのバッファー宣言子は、メンバーに名前を付ける識別子の後に、 [ および ] トークンで囲まれた定数式で構成されます。 定数式は、その固定サイズのバッファー宣言子によって導入されたメンバー内の要素の数を表します。 定数式の型は、 int型に暗黙的に変換でき、値は 0 以外の正の整数である必要があります。

固定サイズのバッファーの要素は、メモリ内に順番に配置する必要があります。

複数の固定サイズ バッファーを宣言する固定サイズ バッファー宣言は、同じ属性と要素型を持つ 1 つの固定サイズ バッファー宣言の複数の宣言と同じです。

例:

unsafe struct A
{
    public fixed int x[5], y[10], z[100];
}

上記の式は、次の式と同じです。

unsafe struct A
{
    public fixed int x[5];
    public fixed int y[10];
    public fixed int z[100];
}

end の例

23.8.3 式の固定サイズ バッファー

固定サイズのバッファー メンバーのメンバー参照 (§12.5) は、フィールドのメンバー参照とまったく同じように処理されます。

固定サイズのバッファーは、 simple_name (§12.8.4)、 member_access (§12.8.7)、または element_access (§12.8.12) を使用して式で参照できます。

固定サイズのバッファー メンバーが単純な名前として参照されている場合、効果はフォーム this.Iのメンバー アクセスと同じです。ここで、 I は固定サイズのバッファー メンバーです。

E.が暗黙的なthis.である可能性があるフォーム E.Iのメンバー アクセスでは、Eが構造体型であり、その構造体型のIのメンバー参照によって固定サイズのメンバーが識別された場合、E.Iは評価され、次のように分類されます。

  • E.I が安全でないコンテキストで発生しない場合は、コンパイル時エラーが発生します。
  • Eが値として分類されると、コンパイル時エラーが発生します。
  • それ以外の場合、 E が移動可能な変数 (§23.4) の場合は、次のようになります。
    • E.Ifixed_pointer_initializer (§23.7) の場合、式の結果は、EI固定サイズ バッファー メンバーの最初の要素へのポインターになります。
    • それ以外の場合、式E.Iがフォーム E.I[J]element_access (§12.8.12) 内のprimary_no_array_creation_expression (§12.8.12.1) である場合、 E.Iの結果は、EI固定サイズ バッファー メンバーの最初の要素へのポインター (P) になり、外側のelement_accesspointer_element_access (§23.6.4) P[J]として評価されます。
    • それ以外の場合は、コンパイル時エラーが発生します。
  • それ以外の場合、Eは固定変数を参照し、式の結果は、EI固定サイズバッファー メンバーの最初の要素へのポインターになります。 結果は S* 型で、S は Iの要素型であり、値として分類されます。

固定サイズ バッファーの後続の要素には、最初の要素からのポインター操作を使用してアクセスできます。 配列へのアクセスとは異なり、固定サイズのバッファーの要素へのアクセスは安全でない操作であり、範囲のチェックは行われません。

: 次の例では、固定サイズのバッファー メンバーを持つ構造体を宣言して使用します。

unsafe struct Font
{
    public int size;
    public fixed char name[32];
}

class Test
{
    unsafe static void PutString(string s, char* buffer, int bufSize)
    {
        int len = s.Length;
        if (len > bufSize)
        {
            len = bufSize;
        }
        for (int i = 0; i < len; i++)
        {
            buffer[i] = s[i];
        }
        for (int i = len; i < bufSize; i++)
        {
            buffer[i] = (char)0;
        }
    }

    unsafe static void Main()
    {
        Font f;
        f.size = 10;
        PutString("Times New Roman", f.name, 32);
    }
}

end の例

23.8.4 確定代入チェック

固定サイズ バッファーは、明確な代入チェック (§9.4) の対象ではなく、固定サイズのバッファー メンバーは構造体型変数の明確な代入チェックのために無視されます。

固定サイズ バッファー メンバーの最も外側の格納構造体変数が静的変数、クラス インスタンスのインスタンス変数、または配列要素である場合、固定サイズ バッファーの要素は自動的に既定値 (§9.3 に初期化されます。 それ以外の場合は、固定サイズバッファーの初期コンテンツは未定義です。

23.9 スタックの割り当て

演算子stackallocに関する一般的な情報については、§12.8.22 を参照してください。 ここでは、ポインターを生成するその演算子の機能について説明します。

stackalloc_expression (§12.8.22) がlocal_variable_declarationの初期化式 (§13.6.2) として発生した場合、安全でないコンテキストでは、local_variable_typeはどちらかである ポインター型 (§23.3) または推論 (var) の場合、stackalloc_expressionの結果は、割り当てられたブロックの先頭T *型のポインターであり、Tstackalloc_expressionunmanaged_typeです。

それ以外の場合、安全でないコンテキストの local_variable_declaration (§13.6.2) と stackalloc_expression (§12.8.22) のセマンティクスは、安全なコンテキストに対して定義されているものに従います。

例:

unsafe 
{
    // Memory uninitialized
    int* p1 = stackalloc int[3];
    // Memory initialized
    int* p2 = stackalloc int[3] { -10, -15, -30 };
    // Type int is inferred
    int* p3 = stackalloc[] { 11, 12, 13 };
    // Can't infer context, so pointer result assumed
    var p4 = stackalloc[] { 11, 12, 13 };
    // Error; no conversion exists
    long* p5 = stackalloc[] { 11, 12, 13 };
    // Converts 11 and 13, and returns long*
    long* p6 = stackalloc[] { 11, 12L, 13 };
    // Converts all and returns long*
    long* p7 = stackalloc long[] { 11, 12, 13 };
}

end の例

Span<T>型の配列または stackalloc'ed ブロックへのアクセスとは異なり、ポインター型の stackalloc'ed ブロックの要素へのアクセスは安全でない操作であり、範囲のチェックは行われません。

: 次のコード内

class Test
{
    static string IntToString(int value)
    {
        if (value == int.MinValue)
        {
            return "-2147483648";
        }
        int n = value >= 0 ? value : -value;
        unsafe
        {
            char* buffer = stackalloc char[16];
            char* p = buffer + 16;
            do
            {
                *--p = (char)(n % 10 + '0');
                n /= 10;
            } while (n != 0);
            if (value < 0)
            {
                *--p = '-';
            }
            return new string(p, 0, (int)(buffer + 16 - p));
        }
    }

    static void Main()
    {
        Console.WriteLine(IntToString(12345));
        Console.WriteLine(IntToString(-999));
    }
}

IntToString メソッドでは、stackalloc式を使用して、スタックに 16 文字のバッファーを割り当てます。 メソッドから制御が戻ると、バッファーは自動的に破棄されます。

ただし、 IntToString はセーフ モードで書き換えることができます。つまり、ポインターを使用せずに、次のように書き換えることができます。

class Test
{
    static string IntToString(int value)
    {
        if (value == int.MinValue)
        {
            return "-2147483648";
        }
        int n = value >= 0 ? value : -value;
        Span<char> buffer = stackalloc char[16];
        int idx = 16;
        do
        {
            buffer[--idx] = (char)(n % 10 + '0');
            n /= 10;
        } while (n != 0);
        if (value < 0)
        {
            buffer[--idx] = '-';
        }
        return buffer.Slice(idx).ToString();
    }
}

end の例

条件付きで規範的なテキストの末尾。