23 不安全代码

23.1 常规

需要不支持不安全代码的实现来诊断此子句中定义的语法规则的任何用法。

这一条款的其余部分,包括其所有子项,是有条件的规范性的。

注意:如前面的子句中定义的核心 C# 语言与 C 和C++在将指针作为数据类型的遗漏时不同。 相反,C# 提供了引用和创建由垃圾回收器管理的对象的功能。 这种设计与其他功能结合使用,使 C# 比 C 或C++更安全。 在核心 C# 语言中,不可能具有未初始化的变量、“悬空”指针或索引超出其边界的数组的表达式。 因此,消除了经常困扰 C 和 C++ 程序的整个类别 bug。

尽管 C 或 C++ 中的每个指针类型构造在 C# 中都有一个引用类型对应项,但在某些情况下,访问指针类型就变得有必要了。 例如,与基础操作系统交互、访问内存映射设备或实现时间关键型算法可能无法或实际访问指针。 为了满足此需求,C# 提供编写 不安全代码的功能。

在不安全的代码中,可以声明和操作指针、在指针和整型类型之间执行转换、获取变量的地址等。 从某种意义上说,编写不安全的代码非常类似于在 C# 程序中编写 C 代码。

从开发人员和用户的角度来看,不安全代码实际上是一项“安全”功能。 不安全代码应使用修饰符 unsafe明确标记,因此开发人员不能意外使用不安全的功能,并且执行引擎可以确保不安全的代码不能在不受信任的环境中执行。

end note

23.2 不安全上下文

C# 的不安全功能仅在不安全的上下文中可用。 通过在类型、成员或本地函数的声明中包括 unsafe 修饰符,或者使用 unsafe_statement来引入不安全上下文:

  • 类、结构、接口或委托的声明可以包括 unsafe 修饰符,在这种情况下,该类型声明(包括类的正文、结构或接口)的整个文本范围被视为不安全的上下文。

    注意:如果 type_declaration 部分部分,则只有该部分是不安全的上下文。 end note

  • 字段、方法、属性、事件、索引器、运算符、实例构造函数、终结器、静态构造函数或本地函数的声明可能包括 unsafe 修饰符,在这种情况下,该成员声明的整个文本范围被视为不安全上下文。
  • unsafe_statement允许在块中使用不安全的上下文。 关联的 的整个文本范围被视为不安全的上下文。 在不安全上下文中声明的本地函数本身不安全。

关联的语法扩展如下所示,并在后续子项中显示。

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

示例:在以下代码中

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

结构 unsafe 声明中指定的修饰符会导致结构声明的整个文本范围成为不安全的上下文。 因此,可以将字段Right声明Left为指针类型。 还可以编写上面的示例

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();
        ...
    }
}

方法FA上的不安全修饰符只会使文本范围F变为不安全的上下文,在该上下文中可以使用语言的不安全功能。 在重写FB,无需重新指定unsafe修饰符,除非FB该方法本身需要访问不安全的功能。

当指针类型是方法签名的一部分时,情况略有不同

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

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

此处,由于 F签名包含指针类型,因此只能在不安全的上下文中写入它。 但是,可以通过使整个类变得不安全(如以下 A情况)或方法声明中包括 unsafe 修饰符来引入不安全上下文,就像在方法 B声明中那样。

end 示例

unsafe当修饰符用于分部类型声明(§15.2.7)时,只有该特定部分被视为不安全的上下文。

23.3 指针类型

在不安全的上下文中,类型§8.1)可以是pointer_type也可以是value_typereference_typetype_parameter。 在不安全的上下文中, pointer_type 也可能是数组的元素类型(§17)。 在不安全的上下文之外,还可以在类型表达式(§12.8.18)中使用pointer_type(例如,使用不不安全)。

pointer_typeunmanaged_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*[] 指向的指针的单维数组 int
void* 指向未知类型的指针

end 示例

对于给定的实现,所有指针类型的大小和表示形式应相同。

注意:与 C 和C++不同,当多个指针在同一声明中声明时,在 C# * 中只写入基础类型,而不是作为每个指针名称上的前缀标点符。 例如:

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

end note

具有类型的 T* 指针的值表示类型的 T变量的地址。 指针间接运算符 *§23.6.2)可用于访问此变量。

示例:给定类型的int*变量P时,表达式*P表示int在包含的P地址中找到的变量。 end 示例

与对象引用一样,指针可以是 null。 将间接运算符应用于 null-valued 指针会导致实现定义的行为(§23.6.2)。 具有值的 null 指针由全位零表示。

void* 类型表示指向未知类型的指针。 由于引用类型未知,因此间接运算符不能应用于类型的 void*指针,也不能对此类指针执行任何算术。 但是,可以将类型的 void* 指针转换为任何其他指针类型(反之亦然),与其他指针类型的值(§23.6.8)进行比较。

指针类型是一个单独的类型类别。 与引用类型和值类型不同,指针类型不继承, object 指针类型 object之间不存在转换。 具体而言,指针不支持装箱和取消装箱(§8.3.13)。 但是,允许在不同指针类型之间以及指针类型和整型类型之间进行转换。 这在 §23.5介绍。

不能将pointer_type用作类型参数(§8.4),并且类型推理(§12.6.3)在将类型参数推断为指针类型的泛型方法调用时失败。

不能将pointer_type用作动态绑定操作(§12.3.3.3)的子表达式类型。

不能将pointer_type用作扩展方法(§15.6.10)中的第一个参数的类型。

pointer_type可用作可变字段的类型(§15.5.4)。

类型的动态擦除E*是具有动态擦除E的引用类型的指针类型。

指针类型的表达式不能用于在anonymous_object_creation_expression(§12.8.17.7)内提供member_declarator中的值。

任何指针类型的默认值 (§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)将变量分为两个类别:固定变量可移动变量

固定变量驻留在不受垃圾回收器操作影响的情况下的存储位置。 (固定变量的示例包括通过取消引用指针创建的局部变量、值参数和变量。另一方面,可移动变量驻留在受到垃圾回收器重定位或处置的存储位置。 (可移动变量的示例包括数组的对象和元素中的字段。

运算符 &§23.6.5)允许在不限制的情况下获取固定变量的地址。 但是,由于可移动变量受垃圾回收器重定位或处置的约束,因此只能使用 fixed statement§23.7) 获取可移动变量的地址,并且该地址仅在该 fixed 语句的持续时间内有效。

确切地说,固定变量是下列变量之一:

  • 由引用局部变量、值参数或参数数组的simple_name§12.8.4)生成的变量,除非变量由匿名函数(§12.19.6.2 捕获)。
  • 由窗体V.I的member_access§12.8.7)生成的变量,其中V是struct_type固定变量。
  • 由窗体的pointer_indirection_expression(§23.6.2)、窗体*P的pointer_member_access§23.6.3)或窗体P[E]P->I的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
  • sbytebyteshortushortintuint、或longulong到任何pointer_type
  • 从任何pointer_typesbyte、、byteshortushortint、、或longulonguint

最后,在不安全的上下文中,标准隐式转换(§10.4.2)集包含以下指针转换:

  • 从任何 pointer_type 到类型 void*
  • null从文本到任何pointer_type

两种指针类型的转换永远不会更改实际指针值。 换句话说,从一个指针类型转换为另一个指针类型对指针提供的基础地址没有影响。

当一个指针类型转换为另一个指针时,如果结果指针未正确对齐指向类型,则取消引用结果时,行为是未定义的。 一般情况下,概念“正确对齐”是可传递的:如果指向类型的指针正确对齐,则指向类型的A指针正确对齐,而指针指向类型的CA指针则正确对齐类型CB

示例:请考虑以下情况:通过指向不同类型的指针访问具有一种类型的变量:

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 显示八个字节中的每一个:

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();
        }
    }
}

当然,生成的输出取决于尾数。 一种可能性是 " BA FF 51 A2 90 6C 24 45"

end 示例

指针和整数之间的映射是实现定义的。

注意:但是,在具有线性地址空间的 32 位和 64 位 CPU 体系结构上,指向整型类型的指针或从整型类型的转换通常与转换或ulong值分别与这些整型类型的转换uint完全相同。 end note

23.5.2 指针数组

可以在不安全的上下文中使用 array_creation_expression§12.8.17.5)构造指针数组。 指针数组只允许应用于其他数组类型的某些转换:

  • 从任何array_type到的隐式引用转换(§10.2.8)以及它实现的接口也适用于指针数组。System.Array 但是,任何通过 System.Array 它实现的数组元素或接口访问数组元素的尝试都可能导致运行时异常,因为指针类型不可转换为 object
  • 从单维数组类型到其泛型基接口的隐式和显式引用转换(§10.2.8§10.3.5)永远不会应用于指针数组。System.Collections.Generic.IList<T> S[]
  • 显式引用转换(§10.3.5System.Array 及其实现到任何 array_type 的接口适用于指针数组。
  • 显式引用转换(§10.3.5)从 System.Collections.Generic.IList<S> 其基接口到单维数组类型 T[] 永远不会应用于指针数组,因为指针类型不能用作类型参数,并且没有从指针类型到非指针类型的转换。

这些限制意味着对 §9.4.4.17 中所述的数组的语句扩展foreach不能应用于指针数组。 相反, foreach 窗体的语句

foreach (V v in x)embedded_statement

其中,类型 x 是窗体 T[,,...,]的数组类型, n 是维度数减 1, T 或者 V 是指针类型,使用嵌套 for-loop 进行扩展,如下所示:

{
    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对程序embedded_statement或任何其他源代码不可见或可访问xv变量在嵌入式语句中是只读的。 如果没有从T(元素类型)到V显式转换(§23.5),则会生成错误,并且不会采取进一步的步骤。 如果 x 具有该值 null,则会在运行时引发 a 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)或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的结果,其中 P 是指针类型的 T*表达式,是类型的 T变量。 将一元 * 运算符应用于类型 void* 或不是指针类型的表达式是编译时错误。

将一元 * 运算符应用于 null-valued 指针的效果是实现定义的。 具体而言,不能保证此操作会引发 。System.NullReferenceException

如果向指针分配了无效值,则未定义一元 * 运算符的行为。

注意:一元*运算符取消引用指针的无效值中,是指向的类型(请参阅 §23.5 中的示例)的地址与变量生存期结束后的地址不恰当对齐。

为了进行明确的赋值分析,通过计算表单 *P 表达式生成的变量最初被视为赋值 (§9.4.2)。

23.6.3 指针成员访问

pointer_member_access由primary_expression、后跟“->”令牌、标识符和可选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]的指针元素访问中, P 应是指针 void*类型的表达式,并且 E 应是可隐式转换为 intuintlongulong表达式。

窗体 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 地址运算符

addressof_expression由与()和(&)组成,后跟unary_expression

addressof_expression
    : '&' unary_expression
    ;

给定一个T类型且分类为固定变量(§23.4)的表达式E,构造&E将计算由该E变量给出的地址。 结果的类型被 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);

给定指针类型的表达式和类型为 、uintlongulongP + N表达式,并N + P计算从添加到N * sizeof(T)给定P地址而得出的类型T*指针值的指针值。intN P T* 同样,表达式P – N计算从给定的地址中减去N * sizeof(T)P类型的T*指针值。

给定两个表达式,P以及指针类型的T*表达式,该表达式P – Q计算给定P地址之间的差异,Q然后除以sizeof(T)Q该差异。 结果的类型始终 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* 该类型的隐式转换,因此可以使用这些运算符比较任何指针类型的操作数。 比较运算符比较两个操作数给出的地址,就像它们是无符号整数一样。

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访问。 固定语句声明的局部变量被视为只读。 如果嵌入语句尝试修改此局部变量(通过赋值或 ++ 运算符 -- ),或者将其作为引用或输出参数传递,则会发生编译时错误。

在fixed_pointer_initializer中使用捕获的局部变量(§12.19.6.2)、值参数或参数数组是错误的。 fixed_pointer_initializer可以是下列项之一:

  • 标记“&”后跟variable_reference§9.5)到非托管类型的T可移动变量(§23.4),前提是该类型T*可隐式转换为语句中给定的fixed指针类型。 在这种情况下,初始值设定项计算给定变量的地址,并保证该变量在固定语句的持续时间内保持固定地址。
  • 具有非托管类型的T元素的array_type表达式,前提是该类型T*可隐式转换为固定语句中给定的指针类型。 在这种情况下,初始值设定项计算数组中第一个元素的地址,并且保证整个数组在语句的持续时间 fixed 内保持固定地址。 如果数组表达式为 null 或数组具有零个元素,初始值设定项将计算一个等于零的地址。
  • 类型表达式 string,前提是类型 char* 可隐式转换为语句中给定的 fixed 指针类型。 在这种情况下,初始值设定项计算字符串中第一个字符的地址,并且保证整个字符串在语句的持续时间 fixed 内保持固定地址。 如果字符串表达式为 null,则语句的行为fixed是实现定义的。
  • 除了array_typestring,或者,如果存在与签名ref [readonly] T GetPinnableReference()匹配的可访问方法或可访问扩展方法(其中Tunmanaged_type),并且T*可隐式转换为语句中给定的fixed指针类型。 在这种情况下,初始值设定项计算返回的变量的地址,并且该变量保证在语句的持续时间 fixed 内保持固定地址。 当重载解析(§12.6.4)生成一个函数成员且该函数成员满足上述条件时,语句可以使用fixed方法GetPinnableReference()。 该方法 GetPinnableReference 应返回对地址等于零的引用,例如从 System.Runtime.CompilerServices.Unsafe.NullRef<T>() 没有要固定的数据时返回的引用。
  • 引用 可移动变量的固定大小的缓冲区成员的simple_namemember_access ,前提是固定大小的缓冲区成员的类型可隐式转换为语句中给定的 fixed 指针类型。 在这种情况下,初始值设定项计算指向固定大小缓冲区(§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 多个用法。 第一个语句修复并获取静态字段的地址,第二个语句修复并获取实例字段的地址,第三个语句修复并获取数组元素的地址。 在每个情况下,使用常规 & 运算符是错误的,因为变量都归类为可移动变量。

上面的示例中的第三个和第四 fixed 个语句生成相同的结果。 通常,对于数组实例a,在语句中fixed指定a[0]与简单地指定a相同。

end 示例

在不安全的上下文中,单维数组的数组元素以递增索引顺序存储,从索引 0 开始,以索引 Length – 1结尾。 对于多维数组,数组元素存储,以便首先增加最右侧维度的索引,然后是下一个左维度,依此类图。

fixed在获取指向数组实例a的指针p的语句中,从范围pp + 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 结尾的字符串。 在获取指向字符串实例的指针p的固定语句中,从范围pp + s.Length ‑ 1表示字符串中字符的地址的指针值,指针值p + s.Length始终指向 null 字符(值为“\0”的s字符)。

示例:

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 示例

示例:以下代码显示了一个 fixed_pointer_initializer,该表达式 的类型非 array_typestring

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 方法。 在语句中 fixedref int 调用 c 该方法时从该方法返回的函数用于初始化 int* 指针 pend 示例

通过固定指针修改托管类型的对象可能会导致未定义的行为。

注意:例如,由于字符串是不可变的,因此程序员有责任确保不会修改指向固定字符串的指针引用的字符。 end note

注意:调用需要“C 样式”字符串的外部 API 时,字符串的自动 null 终止尤其方便。 但是,请注意,允许字符串实例包含 null 字符。 如果存在此类 null 字符,当被视为以 null 结尾 char*时,字符串将显示为截断。 end note

23.8 固定大小的缓冲区

23.8.1 常规

固定大小的缓冲区用于将“C 样式”内联数组声明为结构的成员,并且主要用于与非托管 API 进行交互。

23.8.2 固定大小的缓冲区声明

固定大小的缓冲区是一个成员,表示给定类型的变量固定长度缓冲区的存储。 固定大小的缓冲区声明引入了给定元素类型的一个或多个固定大小的缓冲区。

注意:与数组一样,固定大小的缓冲区可以视为包含元素。 因此,为数组定义的术语 元素类型 也与固定大小的缓冲区一起使用。 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)、修饰符(§15.3.5)、与结构成员允许的任何声明辅助功能(§16.4.3)和unsafe修饰符(§23.2)对应的辅助功能修饰符。new 属性和修饰符适用于由固定大小缓冲区声明声明的所有成员。 同一修饰符在固定大小的缓冲区声明中出现多次是错误的。

不允许固定大小的缓冲区声明包括 static 修饰符。

固定大小的缓冲区声明的缓冲区元素类型指定声明引入的缓冲区的元素类型。 缓冲区元素类型应为预定义类型sbyte之一、byteintshortcharulongfloatuintlongushort或。 doublebool

缓冲区元素类型后跟固定大小的缓冲区声明符列表,每个声明符都会引入一个新成员。 固定大小的缓冲区声明符由一个标识符组成,该标识符命名成员,后跟封闭的 [ 常量表达式和 ] 标记。 常量表达式表示该固定大小的缓冲区声明符引入的成员中的元素数。 常量表达式的类型应隐式转换为类型 int,该值应为非零正整数。

固定大小的缓冲区的元素应按顺序在内存中布局。

声明多个固定大小的缓冲区的固定大小缓冲区声明等效于具有相同属性和元素类型的单个固定大小的缓冲区声明的多个声明。

示例:

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 是固定大小的缓冲区成员。

在可能为隐式this.窗体E.IE.的成员访问中,如果是E结构类型,并且该结构类型中的成员查找I标识固定大小的成员,则E.I按如下所示进行评估和分类:

  • 如果表达式 E.I 未在不安全的上下文中发生,则会发生编译时错误。
  • 如果 E 分类为值,则会发生编译时错误。
  • 否则,如果 E 为可移动变量(§23.4),则:
  • 否则, E 引用固定变量和表达式的结果是指向固定大小的缓冲区成员 I 的第一个元素的 E指针。 结果为类型 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_expression的unmanaged_type

在所有其他方面,local_variable_declaration s (§13.6.2) 和不安全上下文中的 stackalloc_expressions (§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 示例

与访问数组或 stackalloc“类型 ed 块 Span<T> ”不同,对“指针类型的 ed 块”元素 stackalloc的访问是不安全的操作,并且未检查范围。

示例:在以下代码中

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));
    }
}

stackalloc方法中使用IntToString表达式在堆栈上分配 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 示例

有条件的规范文本的结尾。