23 不安全代码

23.1 一般

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

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

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

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

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

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

尾注

23.2 不安全上下文

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

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

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

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

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

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

示例:在以下代码中

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

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

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

在这里,字段声明中的 unsafe 修饰符会导致这些声明被视为不安全上下文。

示例结束

除了建立不安全的上下文(因此允许使用指针类型)外, unsafe 修饰符对类型或成员没有影响。

示例:在以下代码中

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

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

F 中的 A 方法上的不安全修饰符只是使 F 的文本范围成为一个不安全上下文,在其中可以使用语言的不安全功能。 在 FB 的替代中,无需重新指定 unsafe 修饰符 — 当然,除非 F 中的 B 方法本身需要访问不安全的功能。

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

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声明中那样。

示例结束

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

23.3 指针类型

在不安全的上下文中,类型§8.1)可以是指针类型值类型引用类型类型参数。 在不安全的上下文中, pointer_type 也可能是数组的元素类型(§17)。 在非不安全上下文中,pointer_type 也可以在类型表达式(§12.8.18)中使用,因为这样的用法是安全的

pointer_typeunmanaged_type§8.8)或关键字void编写,后跟一个*标记:

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

指针类型中 * 前面指定的类型称为指针类型的参考类型。 它表示指针类型指向的值所指向的变量的类型。

pointer_type 只能在不安全的上下文中的 array_type 中使用 (§23.2)。 non_array_type 是任何不属于 array_type 的类型。

与引用(引用类型的值)不同,垃圾回收器不会跟踪指针-垃圾回收器不知道指针及其指向的数据。 因此,不允许指针指向引用或包含引用的结构,指针的引用类型应为 unmanaged_type。 指针类型本身是非托管类型,因此一个指针类型可以用作另一个指针类型的引用类型。

指针和引用混合的直观规则是,引用的引用(对象)允许包含指针,但指针的引用不允许包含引用。

示例:下表提供了指针类型的一些示例:

示例 描述
byte* 指向 byte 的指针
char* 指向 char 的指针
int** 指向 int 的指针的指针
int*[] 指向 int 的指针的一维数组
void* 指向未知类型的指针

示例结束

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

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

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

尾注

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

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

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

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

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

pointer_type不能用作类型参数(§8.4),而在泛型方法调用中,当类型推理(§12.6.3)需要推断类型参数为指针类型时,它将失败。

指针类型不能用作动态绑定操作(§12.3.3)的子表达式类型。

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

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

类型 E*是指针类型,其引用类型是 E 的动态擦除。

指针类型的表达式不能用于在匿名对象创建表达式§12.8.17.3)中提供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
        }
    }
}

尾注

方法可以返回某种类型的值,并且该类型可以是指针。

示例:如果给定指向连续序列 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;
}

示例结束

在不安全的上下文中,多个构造可用于对指针进行操作:

  • 一元 * 运算符可用于执行指针间接(§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.6.5)允许在不限制的情况下获取固定变量的地址。 但是,由于可移动变量受垃圾回收器重定位或处置的约束,因此只能使用 fixed statement§23.7) 获取可移动变量的地址,并且该地址仅在该 fixed 语句的持续时间内有效。

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

所有其他变量都归类为可移动变量。

静态字段被归类为可移动变量。 此外,即使为参数提供的参数是固定变量,按引用传递的参数也被分类为可移动变量。 最后,通过取消引用指针生成的变量始终归类为固定变量。

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_typesbytebyteshortushortintuintlongulong

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

  • 从任意 pointer_type 到类型 void*
  • null 字面到任意 pointer_type

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

在将一种指针类型转换为另一种指针类型时,如果产生的指针没有正确对齐所指向的类型,那么在对结果进行取消引用时,其行为将是未定义的。 一般而言,“正确对齐”的概念是可传递的:如果指向类型 A 的指针与指向类型 B 的指针正确对齐,而指向后者的指针又与指向类型 C 的指针正确对齐,那么指向类型 A 的指针与指向类型 C 的指针也会正确对齐。

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

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
}

示例结束

当一个指针类型转换为指向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"

示例结束

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

注意:但是,在具有线性地址空间的 32 位和 64 位 CPU 架构上,指针与整数类型之间的转换通常与uint值或ulong值与这些整数类型之间的转换完全相同。 尾注

23.5.2 指针数组

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

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

这些限制意味着在 foreach 中描述的对数组的语句扩展不能应用于指针数组。 相反,foreach 语句(形式为

foreach (V v in x) embedded_statement

其中,x 的类型是形如 T[,,...,] 的数组类型,n 是维度数减1,而 TV 是指针类型,通过如下嵌套的 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),则会生成错误,并且不会采取进一步的步骤。 如果 x 具有该值 null,则会在运行时引发 a System.NullReferenceException

注意:虽然不允许指针类型作为类型参数,但指针数组可以用作类型参数。 尾注

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_expression§12.8)和unary_expression§12.9)生成规则允许其他构造,如以下分项所述。

注意:不安全运算符的优先级和关联性由语法隐含。 尾注

23.6.2 指针间接

pointer_indirection_expression 由星号 (*) 后跟 unary_expression 组成。

pointer_indirection_expression
    : '*' unary_expression
    ;

一元 * 运算符表示指针间接,用于获取指针指向的变量。 计算 *P的结果,其中 P 是指针类型的 T*表达式,是类型的 T变量。 将一元 * 运算符应用于类型 void* 或不是指针类型的表达式是编译时错误。

将一元 * 运算符应用于 null 值指针的效果由实现定义。 具体而言,无法保证该操作会引发 System.NullReferenceException

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

注意:在使用一元*运算符取消引用指针时,无效值包括:用于指向的类型不恰当对齐的地址(请参阅 §23.5 中的示例),以及变量生命周期结束后的地址。

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

23.6.3 指针成员访问

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

示例结束

23.6.4 指针元素访问

pointer_element_access一个primary_expression组成,后面跟着一个表达式,该表达式被“[”和“]”括起来。

pointer_element_access
    : primary_expression '[' expression ']'
    ;

当识别primary_expression时,如果element_accesspointer_element_access§23.6.4)两种可能性都适用,并且嵌入的primary_expression是指针类型(§23.3),则应选择后者。

在指针元素访问表单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;
            }
        }
    }
}

示例结束

指针元素访问运算符不会检查越界错误,访问越界元素时的行为是未定义的。

注意:这与 C 和 C++ 相同。 尾注

23.6.5 address-of 运算符

addressof_expression 由一个省略号 (&) 和一个 unary_expression 组成。

addressof_expression
    : '&' unary_expression
    ;

给定一个E类型且分类为固定变量(T)的表达式,构造&E将计算由该E变量给出的地址。 结果的类型是T*,并被归类为一个值。 如果 E 未被分类为变量,或者 E 被分类为只读局部变量,或者 E 表示一个可移动变量,则会发生编译时错误。 在最后一种情况下,固定语句(§23.7)可用于在获取变量地址之前暂时“修复”变量。

注意:如 §12.8.7 中所述,在定义readonly字段的结构或类的实例构造函数或静态构造函数外部,该字段被视为值,而不是变量。 因此,无法获取其地址。 同样,无法获取常量地址。 尾注

&运算符不要求对其参数进行明确分配,但在&操作之后,在发生该操作的执行路径中,应用此运算符的变量被视为已明确分配。 程序员有责任确保在这种情况下正确进行变量的初始化。

示例:在以下代码中

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

在用于初始化 i&i 操作之后,p 被视为已确定分配。 对于 *p 的赋值有效地初始化了 i,但包括这一初始化是程序员的责任,如果删除该赋值,则不会出现编译时错误。

示例结束

注意:运算符的明确赋值 & 规则存在,因此可以避免局部变量的冗余初始化。 例如,许多外部 API 都需要一个指向结构的指针,该结构由 API 填充。 对此类 API 的调用通常传递本地结构变量的地址,如果没有规则,则需要对结构变量进行冗余初始化。 尾注

注意:当匿名函数(§12.8.24)捕获局部变量、值参数或参数数组时,该局部变量、参数或参数数组不再被视为固定变量(§23.7),而是被视为可移动变量。 因此,对于任何不安全的代码来说,获取匿名函数捕获的局部变量、值参数或参数数组的地址都是错误的。 尾注

23.6.6 指针递增和递减

在不安全的上下文中 ++ ,和 -- 运算符(§12.8.16§12.9.6)可应用于除所有类型的指针变量之外 void*。 因此,对于每个指针类型 T*,将隐式定义以下运算符:

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

运算符分别生成与x+1x-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);

给定一个指针类型的表达式P和一个类型是T*Nintuint的表达式,longulong这两个表达式计算出通过将P + N添加到N + P给定地址后得到的T*类型的指针值。 同样,表达式P – N计算出通过从T*给定地址中减去N * sizeof(T)得到的P类型的指针值。

给定两个指针类型P的表达式QT*,表达式P – Q计算PQ给出的地址之间的差异,然后将该差异除以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

示例结束

如果指针算术运算溢出指针类型的域,则结果以实现定义的方式截断,但不会生成异常。

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),前提是该T*类型可以隐式转换为语句中给定的fixed指针类型。 在这种情况下,初始值设定项计算给定变量的地址,并保证该变量在固定语句的持续时间内保持固定地址。
  • array_type 的表达式,其元素为非托管类型 T,前提是类型 T* 可隐式转换为固定语句中给出的指针类型。 在这种情况下,初始化器计算数组中第一个元素的地址,并且保证整个数组在fixed语句的执行期间保持固定地址。 如果数组表达式为 null 或数组具有零个元素,初始值设定项将计算一个等于零的地址。
  • 类型表达式 string,前提是类型 char* 可隐式转换为语句中给定的 fixed 指针类型。 在这种情况下,初始化程序计算出字符串中第一个字符的地址,并保证在 fixed 语句的执行期间,整个字符串保持在一个固定的地址。 如果字符串表达式为 fixed,则 null 语句的行为由实现定义。
  • array_typestring 以外类型的表达式,条件是存在与签名 ref [readonly] T GetPinnableReference() 匹配的可访问方法或可访问扩展方法,其中 Tunmanaged_type,并且 T* 可以隐式转换为 fixed 语句中给出的指针类型。 在这种情况下,初始值设定项会计算返回变量的地址,并且该变量在fixed语句执行期间被保证保持在一个固定的地址。 当重载解析 (GetPinnableReference()) 只产生一个函数成员,且该函数成员满足上述条件时,fixed 语句可以使用 方法。 GetPinnableReference 方法应返回一个等于零的地址引用,例如,当没有数据要引脚时,从 System.Runtime.CompilerServices.Unsafe.NullRef<T>() 返回的地址引用。
  • 可以引用可移动变量中固定大小缓冲区成员的 simple_namemember_access,条件是该固定大小缓冲区成员的类型可以隐式转换为 fixed 语句中给定的指针类型。 在这种情况下,初始值设定项计算指向固定大小缓冲区(§23.8.3)的第一个元素的指针,并保证在 fixed 语句的整个持续期间,固定大小缓冲区的地址保持不变。

对于 fixed_pointer_initializer 计算出的每个地址,fixed 语句确保该地址引用的变量在 fixed 语句期间不会被垃圾回收器重新定位或处置。

示例:如果由fixed_pointer_initializer计算的地址引用某个对象的字段或数组实例的元素,固定语句保证在语句的生存期内,该对象实例不会被重新定位或释放。 示例结束

程序员有责任确保由固定语句创建的指针不会在执行这些语句之后幸存下来。

示例:将 fixed 语句创建的指针传递给外部 API 时,程序员有责任确保 API 不保留这些指针的内存。 示例结束

固定对象可能会导致堆碎片(因为它们无法移动)。 因此,仅当绝对必要时,才应修复对象,并且修复时间应尽可能短。

示例:示例

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,在语句中a[0]指定fixed与简单地指定a相同。

示例结束

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

在获取指向数组实例 fixed 的指针 pa 语句中,从 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

示例结束

示例:在以下代码中

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 用于修复数组,以便其地址可以传递给采用指针的方法。

示例结束

char*通过修复字符串实例生成的值始终指向以 null 结尾的字符串。 在获取指向字符串实例的指针p的固定语句中,指针值从sp表示字符串中字符的地址,而指针值p + s.Length ‑ 1始终指向值为“\0”的空字符(p + s.Length ‑ 1)。

示例:

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

示例结束

示例:以下代码展示了一个 fixed_pointer_initializer,其表达式类型不是 array_type 或者 string

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 语句中,在 ref int 上调用该方法时返回的 c 用于初始化 int* 指针 p示例结束

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

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

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

23.8 固定大小的缓冲区

23.8.1 常规

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

23.8.2 固定大小的缓冲区声明

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

注意:与数组一样,固定大小的缓冲区可以视为包含元素。 因此,为数组定义的术语 元素类型 也与固定大小的缓冲区一起使用。 尾注

固定大小的缓冲区只能在结构声明中得到允许,并且只能在不安全的上下文(§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 修饰符。

固定大小的缓冲区声明的缓冲区元素类型指定声明引入的缓冲区的元素类型。 缓冲区元素类型应为预定义类型sbyte之一、byteshortushortintuintlongulongcharfloat或。 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];
}

示例结束

23.8.3 表达式中的固定大小缓冲区

固定大小的缓冲区成员的成员查找(§12.5)与字段的成员查找完全相同。

可以在表达式中使用simple_name(§12.8.4)、member_access§12.8.7)或element_access§12.8.12)引用固定大小的缓冲区。

将固定大小的缓冲区成员引用为简单名称时,效果与表单 this.I的成员访问权限相同,其中 I 是固定大小的缓冲区成员。

在形式为 E.I 的成员访问(其中 E. 可以是隐式 this.)中,如果 E 属于结构类型,且该结构类型中 I 的成员查找标识了一个固定大小的成员,那么 E.I 将按以下方式进行求值和分类:

  • 如果表达式 E.I 未在不安全的上下文中发生,则会发生编译时错误。
  • 如果 E 分类为值,则会发生编译时错误。
  • 否则,如果 E 为可移动变量(§23.4),则:
    • 如果表达式E.Ifixed_pointer_initializer§23.7),那么该表达式的结果是指向I中固定大小缓冲区成员的第一个元素的指针E
    • 否则,如果表达式E.I是形式E.I[J]primary_expression§12.8.12.1)在element_access§12.8.12)内,则结果E.I是指向P的指针,指向固定大小缓冲区成员I中的第一个元素E,然后将封闭的element_access计算为pointer_element_access§23.6.4)。P[J]
    • 否则会发生编译时错误。
  • 否则,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);
    }
}

示例结束

23.8.4 明确分配检查

固定大小缓冲区不受明确赋值检查(§9.4)的约束,对于结构类型变量的明确赋值检查,将忽略固定大小的缓冲区成员。

当包含固定大小缓冲区成员的结构变量的外部是静态变量、类实例的实例变量或数组元素时,固定大小的缓冲区的元素会自动初始化为其默认值(§9.3)。 在所有其他情况下,未定义固定大小的缓冲区的初始内容。

23.9 堆栈分配

有关运算符的一般信息,请参阅 stackalloc。 在这里,将讨论该运算符导致指针的能力。

stackalloc_expression 作为 local_variable_declaration (§13.6.2) 的初始化表达式出现时,其中 local_variable_type 是指针类型 (§23.3) 或推断类型 (var),stackalloc_expression 的结果为类型 T* 的指针,其中 Tstackalloc_expressionunmanaged_type。 在这种情况下,结果是一个指向已分配块开头的指针。

示例:

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

示例结束

与访问数组或stackalloc类型的Span<T>块不同,访问指针类型的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();
    }
}

示例结束

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