24 不安全代码

24.1 常规

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

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

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

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

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

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

尾注

24.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)时,只有该特定部分被视为不安全的上下文。

24.3 指针类型

24.3.1 常规

指针是一个能够包含变量或静态方法的地址的变量,称为该指针的目标。 具有值的null指针是空指针,当前不指向变量或静态方法。 尝试访问指针目标的行为称为 取消引用§24.6.2§24.6.4)。

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

pointer_type
    : dataptr_type
    | funcptr_type
    | voidptr_type
    ;

指针类型的目标类型称为指针 类型的引用类型 。 它表示指针类型指向的值所指向的变量的类型。

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

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

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

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

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

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

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

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

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

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

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

任何指针类型的默认值 (§9.3) 为 null

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

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

示例结束

24.3.2 数据指针

数据指针是一个指针,它包含具有value_type§8.3.1)、funcptr_type§24.3.3)或voidptr_type(§24.3.4)的变量的地址。

dataptr_type
    : value_type ('*')+
    | funcptr_type ('*')+
    | voidptr_type ('*')+
    ;

dataptr_type作为unmanaged_type(§8.8)、funcptr_typevoidptr_type的value_type写入,后跟一个或多个*令牌。

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

示例 描述
byte* 指向 byte 的指针
int*[] 指向 int 的指针的一维数组
char** 指向 char 的指针的指针
delegate*<void>* 指向没有参数和返回类型的静态方法的 void 指针的指针
void** 指向指向未知类型的指针的指针

示例结束

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

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

尾注

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

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

注意:虽然指针可以作为按引用参数传递,但使用数据指针这样做可能会导致未定义的行为,因为指针可能已设置为指向调用方法返回时不再存在的局部变量,或者它用来指向的固定对象不再固定。 例如:

using System;

class Test
{
    static int value = 20;

    unsafe static void F(out int* pi1, ref int* pi2) 
    {
        int i = 10;
        pi1 = &i;
        fixed (int* pj = &value)
        {
            // ...
            pi2 = pj;
        }
    }

    static void Main()
    {
        int i = 10;
        unsafe 
        {
            int* px1;
            int* px2 = &i;
            F(out px1, ref px2);
            // Undefined behavior
            // Console.WriteLine($"*px1 = {*px1}, *px2 = {*px2}");
        }
    }
}

尾注

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

  • 一元 * 运算符可用于执行指针间接(§24.6.2)。
  • ->运算符可用于通过指针(§24.6.3)访问结构的成员。
  • 运算符 [] 可用于为指针编制索引(§24.6.4)。
  • 一元 & 运算符可用于获取变量的地址(§24.6.5)。
  • ++--运算符可用于递增和递减指针(§24.6.6)。
  • 二进制 +- 运算符可用于执行指针算术(§24.6.7)。
  • ==!=<><=>=运算符可用于比较指针(§24.6.8)。
  • stackalloc运算符可用于从调用堆栈(§24.9)分配内存。
  • fixed 语句可用于暂时修复变量,以便获取其地址(§24.7)。

24.3.3 函数指针

函数指针是一个能够包含静态方法地址的指针。

funcptr_type
    : 'delegate' '*' calling_convention_specifier? 
      '<' funcptr_parameter_list funcptr_return_type '>'
    ;

calling_convention_specifier
    : 'managed'
    | 'unmanaged' ('[' unmanaged_calling_convention ']')?
    ;

unmanaged_calling_convention
    : 'Cdecl'
    | 'Stdcall'
    | 'Thiscall'
    | 'Fastcall'
    | identifier (',' identifier)*
    ;

funcptr_parameter_list
    : (funcptr_parameter ',')*
    ;

funcptr_parameter
    : parameter_mode_modifier? type
    ;

funcptr_return_type
    : ref_kind? return_type
    ;

与方法具有签名(§7.6)一样,函数指针类型具有可能指向的方法类型的签名。 该签名包括调用约定。

具有类型的 T 函数指针的非 null 值表示具有与类型 T兼容的签名的方法的地址。

如果未提供 calling_convention_specifier ,则默认值为 managed,这会导致使用执行环境的默认机制。 可以使用 unmanaged_calling_convention 指定特定的非托管约定,其令牌映射到具有实现定义语义的实现定义名称。 这些令牌的有效组合集是实现定义的。

注意calling_convention_specifier 允许选择一种可能更高效的调用机制,或调用用 C# 以外的语言编写的方法。 尾注。

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

示例 描述
delegate*<void> 指向没有参数和返回类型的托管方法的 void 指针
delegate*<void>[] 指向托管方法的指针数组,没有参数和 void 返回类型
delegate*<string, string, bool> 指向具有两 string 个参数和返回类型的托管方法的 bool 指针
delegate*<ref readonly int> 指向没有参数并返回 的托管方法的指针 ref readonly int
delegate*<delegate*<int>, void> 指向托管方法的指针,该参数是指向没有参数和 int 返回类型的方法的指针,以及 void 返回类型
delegate* unmanaged[Stdcall]<void> 使用调用约定指向没有参数和 void 返回类型的非托管方法的 Stdcall 指针

考虑以下情况:

unsafe class Util
{
    static void Log() { ... }
    static void Log(string p1) { ... }

    static void User()
    {
        delegate*<void>[] ary1 = new delegate*<void>[] { &Log, null };
        foreach (var element in ary1)
        {
            if (element != null)
            {
                element();     // call the method being pointed to
            }
        }
    }
}

鉴于数组中的函数指针指向没有参数的方法, &Log 则采用没有参数的方法的 Log 地址。 示例结束

unmanaged_calling_convention支持少量预定义约定(CdeclStdcallThiscallFastcall上下文关键字),这些约定可以独立使用,也可以用作unmanaged_calling_convention标识符列表中的标识符。 允许其他实现定义的约定,并且可以使用 标识符 列表(可能包含其中一个或多个预定义约定)组合多个约定。 此列表中的标识符的查找和处理以实现定义的方式完成。

示例:给定实现定义的调用约定 SuppressGCTransition

unsafe class C
{
    delegate* unmanaged[SuppressGCTransition]<int, int> fpx;
    delegate* unmanaged[Stdcall, SuppressGCTransition]<int, int> fpy;
}

这两种情况都使用标识符列表语法规则。 示例结束

自定义属性不能应用于 funcptr_type 或其任何元素。

funcptr_type 类型的参数 不应标记为 params§15.6.2.1)。

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

  • &运算符可用于获取静态方法的地址(§24.6.5
  • ==!=<><==>运算符可用于比较指针(§24.6.8)。
  • invocation_expression运算符 ()可用于调用指向的方法(§12.8.9.1)。

24.3.4 Void 指针

void 指针是一个能够包含数据指针或函数指针值的指针。

voidptr_type
    : 'void' '*'
    ;

voidptr_type将作为关键字void写入后跟 thye * 标记。

示例:下表提供了 void-pointer 类型的一些示例:

示例 描述
void* 指向未知类型的指针
void*[,,] 指向未知类型的指针的三维数组

示例结束

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

24.4 固定和可移动变量

address-of 运算符(§24.6.5)和 fixed 语句(§24.7)将变量分为两类: 固定变量可移动变量

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

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

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

  • 由引用局部变量、值参数或参数数组 的simple_name§12.8.4)生成的变量,除非该变量由非static 匿名函数(§12.22.6.2)捕获。
  • 由形式为 member_access (V.I) 产生的变量,其中 Vstruct_type 的固定变量。
  • 由窗体的pointer_indirection_expression§24.6.2)、窗体*Ppointer_member_access§24.6.3)或窗体P->Ipointer_element_accessP[E])生成的变量。

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

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

24.5 指针转换

24.5.1 常规

在不安全的上下文中,扩展了一组可用的隐式转换(§10.2),以包括以下隐式指针转换:

  • 从任意 pointer_type 到类型 void*
  • null_literal§6.4.5.7)到任何 pointer_type
  • funcptr_typeF0funcptr_typeF1,前提是以下所有内容均属实:
    • F0并且F1具有相同数量的参数,并且每个D0n参数F0的按引用参数修饰符与相应的D1n参数F1相同。
    • 对于每个值参数,存在从参数类型到相应参数类型的F0F1标识转换、隐式引用转换或隐式指针转换。
    • 对于每个按引用参数,参数 F0 类型与相应的 F1参数类型相同。
    • 如果返回类型按值,则存在从返回类型到返回类型的F1F0标识、隐式引用或隐式指针转换。
    • 如果返回类型是按引用的,则返回类型和 ref 修饰符 F1 与返回类型和修饰符的返回类型和 ref 修饰符 F0相同。
    • F0 的调用约定与 F1的调用约定相同。

此外,在不安全的上下文中,扩展了一组可用的显式转换(§10.3),以包括以下显式指针转换:

  • 从任何 pointer_type 到任何其他 pointer_type
  • sbytebyteshortushortintuintnintnuint、或longulong任意pointer_type
  • 从任何pointer_typesbyte、、byteshortushortintuintnintnuint或。 longulong

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

  • 从任意 pointer_type 到类型 void*
  • null_literal 到任何 pointer_type

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

当一个指针类型转换为 dataptr_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值与这些整数类型之间的转换完全相同。 尾注

24.5.2 指针数组

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

  • 从任何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

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

24.6 表达式中的指针

24.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)生成规则允许其他构造,如以下分项所述。

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

有关函数指针的类型推理的所有方面,均在 §12.6§12.8 的相应子项中介绍。

24.6.2 指针间接

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

pointer_indirection_expression
    : '*' unary_expression
    ;

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

注意:在 C/C++ 中,可以取消引用函数指针以在基础函数上调用它,如中所示 (*fp)()。 C# 中不允许此类显式取消引用。 尾注

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

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

注意:一元 * 运算符取消引用数据指针的无效值中,是指向的类型(见 §24.5 中的示例)的地址与变量在生存期结束时的地址不适当的对齐。

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

24.6.3 指针成员访问

pointer_member_accessprimary_expression 和一个“->”令牌组成,之后是 identifier 和一个可选的 type_argument_list

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

在表单 P->I的指针成员访问中, P 应是数据指针类型的表达式,并 I 表示指向的 P 类型的可访问成员。 对于类型Pvoidptr_type,这是一个编译时错误

形式为 P->I 的指针式成员访问完全按照 (*P).I 的方式进行求值。 有关指针间接运算符的说明(*),请参阅 §24.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());
        }
    }
}

示例结束

24.6.4 指针元素访问

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

pointer_element_access
    : primary_expression '[' expression ']'
    ;

primary_expression 如果element_access和pointer_element_access§24.6.4)替代项都适用,则如果嵌入primary_expression是指针类型(§24.3),则选择后者。

在表单 P[E]的指针元素访问中, P 应是指针类型的 void*表达式,并且 E 应是可以隐式转换为 intuint、、 nintnuintlongulong表达式。

形式为 P[E] 的指针式元素访问完全按照 *(P + E) 的方式进行求值。 有关指针间接运算符的说明(*),请参阅 §24.6.2。 有关指针加法运算符的说明(+),请参阅 §24.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++ 相同。 尾注

24.6.5 地址运算符

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

addressof_expression
    : '&' unary_expression
    ;

unary_expression 应指定变量或方法组。 变量大小写如下。

给定一个E类型且分类为固定变量(T)的表达式,构造&E将计算该变量的E地址。 结果的类型是T*,并被归类为一个值。 如果 E 未被分类为变量,或者 E 被分类为只读局部变量,或者 E 表示一个可移动变量,则会发生编译时错误。 在最后一种情况下,固定语句(§24.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)捕获局部变量、值参数或参数数组时,该局部变量、参数或参数数组不再被视为固定变量(§24.7),而是被视为可移动变量。 因此,对于任何不安全的代码来说,获取匿名函数捕获的局部变量、值参数或参数数组的地址都是错误的。 尾注

下面将立即介绍指定方法组 unary_expression 的情况。

在不安全的上下文中,如果以下所有内容均为 true,则方法 Mfuncptr_typeF 兼容:

  • MF 的参数数相同,M 中的每个参数 refoutin 修饰符与 F中的相应参数相同。
  • 对于每个值参数,存在从参数类型到相应参数类型的MF标识转换、隐式引用转换或隐式指针转换。
  • 对于每个按引用参数,参数 M 类型与相应的 F参数类型相同。
  • 如果返回类型按值,则存在从返回类型到返回类型的FM标识、隐式引用或隐式指针转换。
  • 如果返回类型是按引用的,则返回类型和 ref 修饰符 F 与返回类型和修饰符的返回类型和 ref 修饰符 M相同。
  • M 的调用约定与 F的调用约定相同。
  • M 是静态方法。

隐式转换存在于目标为方法组的unary_expressionE如果包含至少一个方法,则其目标为方法组F,如果E包含至少一个方法,该方法的正常形式适用于通过使用参数类型和修饰符F构造的参数列表,如下所述:

  • 选择与表单 M 的方法调用相对应的单个方法 E(A),并进行以下修改:
    • 参数列表 A 是表达式列表,每个表达式都分类为变量,以及相应 funcptr_parameter_list 的类型 F和修饰符。
    • 候选方法只包括那些在正常形式中适用的方法,而不是适用于扩展形式的方法。
    • 候选方法只是静态方法。
  • 如果重载解析算法生成错误,则会发生编译时错误。 否则,该算法将生成一个最佳方法,M 具有与 F 相同的参数数,并且转换被视为存在。
  • 所选方法 M 应与函数指针类型 F兼容(如上所述)。 否则,将发生编译时错误。
  • 转换的结果是 F类型的函数指针。

24.6.6 指针递增和递减

在不安全的上下文中 ++ ,可以将和 -- 运算符(§12.8.16§12.9.7)应用于所有类型的指针变量。对于这些运算符,这些运算符应用于类型 funcptr_typevoidptr_type的变量是编译时错误。 因此,对于每个数据指针类型 T*,将隐式定义以下运算符:

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

运算符生成的结果x+1与 (x-1§24.6.7) 相同。 换句话说,对于类型的 T*数据指针变量, ++ 运算符将添加到 sizeof(T) 变量中包含的地址,运算符 -- 从变量中包含的地址中减去 sizeof(T)

如果指针递增或递减操作溢出指针类型的域,则结果是实现定义的,并且不需要异常。

24.6.7 指针算术

在不安全的上下文中 + ,运算符(§12.13.5)和 - 运算符(§12.13.6)可应用于所有数据指针类型的值。 这些运算符应用于funcptr_type或voidptr_type类型的值是编译时错误。 因此,对于每个指针类型 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);

没有使用本机整数(§8.3.6)偏移量的指针加法或减法的预定义运算符。 相反, nintnuint 应将值分别提升到 long 这些类型的预定义运算符以及 ulong分别使用指针算术。

给定数据指针类型的表达式P和类型T*Nintuint类型的表达式long,以及表达式ulong,并P + N计算从添加到N + P给定T*的地址所得到的类型N * sizeof(T)指针值的指针P值。 同样,表达式P – N计算出通过从T*给定地址中减去N * sizeof(T)得到的P类型的指针值。

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

示例结束

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

24.6.8 指针比较

在不安全的上下文中==,可以!=<><=安全地应用于所有>=s 的值以及作为dataptr_type值副本的所有voidptr_types的值(§12.15)。 指针比较运算符为:

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* 该类型的隐式转换,因此可以使用这些运算符比较任何指针类型的操作数。 比较运算符比较两个操作数给出的地址,就像它们是无符号整数一样。 但是,在比较 funcptr_types 的值或 void* 复制值时的行为是未定义的。

注意:在某些平台上,当给定方法的地址被多次采用时,结果可能会有所不同,因此与它们进行比较不可靠。 尾注

24.6.9 sizeof 运算符

对于某些预定义类型(§12.8.19), sizeof 运算符将生成常量 int 值。 对于所有其他类型,运算符的结果 sizeof 是实现定义的,并归类为值,而不是常量。

成员打包到结构中的顺序未指定。

出于对齐目的,结构开头、结构内和结构末尾可能存在未命名的填充。 用作填充的位的内容是不确定的。

应用于具有结构类型的操作数时,结果是该类型的变量(包括任何填充)中的字节总数。

24.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计算的地址初始化该局部变量。 pointer_type 不得 funcptr_type。 在固定语句中声明的局部变量,可以在该变量声明右侧出现的任何 fixed_pointer_initializer 和固定语句的 embedded_statement 中访问。 固定语句声明的局部变量被视为只读。 如果嵌入语句尝试修改此局部变量(通过赋值或 ++ 运算符 -- ),或者将其作为引用或输出参数传递,则会发生编译时错误。

fixed_pointer_initializer中使用捕获的局部变量(§12.22.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 语句中给定的指针类型。 在这种情况下,初始值设定项计算指向固定大小缓冲区(§24.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* 时,就会显示为被截断。 尾注

24.8 固定大小的缓冲区

24.8.1 常规

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

24.8.2 固定大小的缓冲区声明

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

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

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

固定大小的缓冲区声明可能包括一组属性(§23)、 new 修饰符(§15.3.5)、对应于结构成员允许的任何声明辅助功能(§16.5.3)和 unsafe 修饰符(§24.2)。 属性和修饰符适用于由固定大小缓冲区声明的所有成员。 同一修饰符在固定大小的缓冲区声明中出现多次是错误的。

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

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

示例结束

24.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 为可移动变量(§24.4),则:
    • 如果表达式 E.Ifixed_pointer_initializer§24.7),则表达式的结果是指向固定大小缓冲区成员 I 的第一个元素的 E指针。
    • 否则,如果表达式E.I是窗体element_access§12.8.12)内的primary_expression(§12.8.12.12),则结果E.I[J]为指针E.I,指向固定大小缓冲区成员P的第I一个元素,然后将封闭E计算为pointer_element_access§24.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);
    }
}

示例结束

24.8.4 明确分配检查

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

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

24.9 堆栈分配

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

stackalloc_expression作为local_variable_declaration§13.6.2)的初始化表达式发生时,其中local_variable_type是指针类型(§24.3)或推断(var),则stackalloc_expression的结果是类型的T*指针,其中Tstackalloc_expressionunmanaged_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 };
    // Cannot 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();
    }
}

示例结束

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