Compartir a través de


23 Código no seguro

23.1 General

Se requiere una implementación que no admita código no seguro para diagnosticar cualquier uso de las reglas sintácticas definidas en esta cláusula.

El resto de esta cláusula, incluidas todas sus subclases, es condicionalmente normativa.

Nota: El lenguaje principal de C#, tal como se define en las cláusulas anteriores, difiere en particular de C y C++ en su omisión de punteros como un tipo de datos. En su lugar, C# proporciona referencias y la capacidad de crear objetos administrados por un recolector de elementos no utilizados. Este diseño, junto con otras características, hace de C# un lenguaje mucho más seguro que C o C++. En el lenguaje C# principal, simplemente no es posible tener una variable sin inicializar, un puntero "pendiente" o una expresión que indexa una matriz más allá de sus límites. Por lo tanto, se eliminan todas las categorías de errores que plagan de forma rutinaria los programas de C y C++.

Aunque prácticamente todas las construcciones de tipo de puntero en C o C++ tienen un homólogo de tipo de referencia en C#, sin embargo, hay situaciones en las que el acceso a los tipos de puntero se convierte en una necesidad. Por ejemplo, la interacción con el sistema operativo subyacente, el acceso a un dispositivo asignado a memoria o la implementación de un algoritmo crítico para el tiempo podrían no ser posibles o prácticos sin acceso a punteros. Para solucionar esta necesidad, C# proporciona la capacidad de escribir código no seguro.

En el código no seguro, es posible declarar y operar en punteros, realizar conversiones entre punteros y tipos enteros, tomar la dirección de las variables, etc. En cierto sentido, escribir código no seguro es muy parecido a escribir código C en un programa de C#.

De hecho, el código no seguro es una característica "segura" desde la perspectiva de los desarrolladores y los usuarios. El código no seguro se marcará claramente con el modificador unsafe, por lo que los desarrolladores no pueden usar accidentalmente características no seguras y el motor de ejecución funciona para asegurarse de que el código no seguro no se puede ejecutar en un entorno que no es de confianza.

nota final

23.2 Contextos no seguros

Las características no seguras de C# solo están disponibles en contextos no seguros. Un contexto no seguro se introduce mediante la inclusión de un unsafe modificador en la declaración de un tipo, miembro o función local, o mediante el empleo de un unsafe_statement:

  • Una declaración de una clase, estructura, interfaz o delegado puede incluir un unsafe modificador, en cuyo caso, toda la extensión textual de esa declaración de tipo (incluido el cuerpo de la clase, la estructura o la interfaz) se considera un contexto no seguro.

    Nota: Si el type_declaration es parcial, solo esa parte es un contexto no seguro. nota final

  • Una declaración de un campo, método, propiedad, evento, indexador, operador, constructor de instancia, finalizador, constructor estático o función local puede incluir un unsafe modificador, en cuyo caso, toda la extensión textual de esa declaración de miembro se considera un contexto no seguro.
  • Un unsafe_statement permite el uso de un contexto no seguro dentro de un bloque. La extensión textual completa del bloque asociado se considera un contexto no seguro. Una función local declarada dentro de un contexto no seguro es no segura.

Las extensiones de gramática asociadas se muestran a continuación y en subclases posteriores.

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

Ejemplo: en el código siguiente

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

el unsafe modificador especificado en la declaración de estructura hace que toda la extensión textual de la declaración de estructura se convierta en un contexto no seguro. Por lo tanto, es posible declarar los Left campos y Right de un tipo de puntero. El ejemplo anterior también se podría escribir

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

Aquí, los unsafe modificadores de las declaraciones de campo hacen que esas declaraciones se consideren contextos no seguros.

ejemplo final

Aparte de establecer un contexto no seguro, lo que permite el uso de tipos de puntero, el unsafe modificador no tiene ningún efecto en un tipo o un miembro.

Ejemplo: en el código siguiente

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

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

El modificador no seguro en el F método simplemente A hace que la extensión textual de F se convierta en un contexto no seguro en el que se pueden usar las características no seguras del lenguaje. En la invalidación de F en B, no es necesario volver a especificar el unsafe modificador, a menos que, por supuesto, el F método en B sí mismo necesite acceso a características no seguras.

La situación es ligeramente diferente cuando un tipo de puntero forma parte de la firma del método

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

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

Aquí, dado que Fla firma incluye un tipo de puntero, solo se puede escribir en un contexto no seguro. Sin embargo, el contexto no seguro se puede introducir haciendo que toda la clase no sea segura, como es el caso en A, o mediante la inclusión de un unsafe modificador en la declaración de método, como es el caso en B.

ejemplo final

Cuando el unsafe modificador se usa en una declaración de tipo parcial (§15.2.7), solo esa parte concreta se considera un contexto no seguro.

23.3 Tipos de puntero

En un contexto no seguro, un tipo (§8.1) puede ser un pointer_type, así como un value_type, un reference_type o un type_parameter. En un contexto no seguro, un pointer_type también puede ser el tipo de elemento de una matriz (§17). También se puede usar un pointer_type en una expresión typeof (§12.8.18) fuera de un contexto no seguro (por ejemplo, el uso no es seguro).

Un pointer_type se escribe como un unmanaged_type (§8.8) o la palabra clave void, seguido de un * token:

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

El tipo especificado antes de en * un tipo de puntero se denomina tipo de referencia del tipo de puntero. Representa el tipo de la variable a la que apunta un valor del tipo de puntero.

Un pointer_type solo se puede usar en un array_type en un contexto no seguro (§23.2). Un non_array_type es cualquier tipo que no sea un array_type.

A diferencia de las referencias (valores de tipos de referencia), el recolector de elementos no utilizados no realiza un seguimiento de los punteros y los datos a los que apuntan. Por este motivo, no se permite que un puntero apunte a una referencia o a una estructura que contenga referencias, y el tipo de referencia de un puntero será un unmanaged_type. Los propios tipos de puntero son tipos no administrados, por lo que se puede usar un tipo de puntero como tipo de referencia para otro tipo de puntero.

La regla intuitiva para mezclar punteros y referencias es que los referentes de referencias (objetos) pueden contener punteros, pero no se permite que los punteros de punteros contengan referencias.

Ejemplo: algunos ejemplos de tipos de puntero se proporcionan en la tabla siguiente:

Ejemplo Descripción
byte* Puntero a byte
char* Puntero a char
int** Puntero al puntero a int
int*[] Matriz unidimensional de punteros a int
void* Puntero al tipo desconocido

ejemplo final

Para una implementación determinada, todos los tipos de puntero tendrán el mismo tamaño y representación.

Nota: A diferencia de C y C++, cuando se declaran varios punteros en la misma declaración, en C# solo * se escribe junto con el tipo subyacente, no como un signo de puntuación de prefijo en cada nombre de puntero. Por ejemplo:

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

nota final

El valor de un puntero que tiene tipo T* representa la dirección de una variable de tipo T. El operador * de direccionamiento indirecto del puntero (§23.6.2) se puede usar para acceder a esta variable.

Ejemplo: dada una variable P de tipo int*, la expresión *P denota la int variable que se encuentra en la dirección contenida en P. ejemplo final

Al igual que una referencia de objeto, un puntero puede ser null. La aplicación del operador de direccionamiento indirecto a un nullpuntero con valores da como resultado un comportamiento definido por la implementación (§23.6.2). Un puntero con valor null se representa mediante todos los bits-cero.

El void* tipo representa un puntero a un tipo desconocido. Dado que se desconoce el tipo de referencia, el operador de direccionamiento indirecto no se puede aplicar a un puntero de tipo void*, ni se puede realizar ninguna aritmética en dicho puntero. Sin embargo, un puntero de tipo void* se puede convertir a cualquier otro tipo de puntero (y viceversa) y en comparación con los valores de otros tipos de puntero (§23.6.8).

Los tipos de puntero son una categoría independiente de tipos. A diferencia de los tipos de referencia y los tipos de valor, los tipos de puntero no heredan de object y no existen conversiones entre tipos de puntero y object. En concreto, no se admiten conversión boxing y unboxing (§8.3.13) para punteros. Sin embargo, las conversiones se permiten entre diferentes tipos de puntero y entre tipos de puntero y los tipos enteros. Esto se describe en §23.5.

No se puede usar un pointer_type como argumento de tipo (§8.4) y la inferencia de tipos (§12.6.3) produce un error en las llamadas de método genérico que habrían inferido un argumento de tipo para que sea un tipo de puntero.

No se puede usar un pointer_type como un tipo de subexpresión de una operación enlazada dinámicamente (§12.3.3).

No se puede usar un pointer_type como el tipo del primer parámetro en un método de extensión (§15.6.10).

Un pointer_type se puede usar como el tipo de un campo volátil (§15.5.4).

La eliminación dinámica de un tipo E* es el tipo de puntero con el tipo de referencia de la eliminación dinámica de E.

No se puede usar una expresión con un tipo de puntero para proporcionar el valor en un member_declarator dentro de un anonymous_object_creation_expression (§12.8.17.7).

El valor predeterminado (§9.3) para cualquier tipo de puntero es null.

Nota: Aunque los punteros se pueden pasar como parámetros por referencia, si lo hace, puede provocar un comportamiento indefinido, ya que el puntero podría estar bien establecido para apuntar a una variable local que ya no existe cuando el método llamado devuelve o el objeto fijo al que se usó para apuntar ya no se ha corregido. Por ejemplo:

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

nota final

Un método puede devolver un valor de algún tipo y ese tipo puede ser un puntero.

Ejemplo: Cuando se proporciona un puntero a una secuencia contigua de ints, el recuento de elementos de esa secuencia y otro int valor, el método siguiente devuelve la dirección de ese valor en esa secuencia, si se produce una coincidencia; de lo contrario, devuelve 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;
}

ejemplo final

En un contexto no seguro, hay varias construcciones disponibles para funcionar en punteros:

  • El operador unario * se puede usar para realizar la direccionamiento indirecto del puntero (§23.6.2).
  • El -> operador se puede usar para acceder a un miembro de una estructura a través de un puntero (§23.6.3).
  • El [] operador se puede usar para indizar un puntero (§23.6.4).
  • El operador unario & se puede usar para obtener la dirección de una variable (§23.6.5).
  • Los ++ operadores y -- se pueden usar para incrementar y disminuir punteros (§23.6.6).
  • Los operadores binarios + y - se pueden usar para realizar la aritmética de puntero (§23.6.7).
  • Los ==operadores , !=, <, >, <=y >= se pueden usar para comparar punteros (§23.6.8).
  • El stackalloc operador se puede usar para asignar memoria desde la pila de llamadas (§23.9).
  • La fixed instrucción se puede usar para corregir temporalmente una variable para que se pueda obtener su dirección (§23.7).

23.4 Variables fijas y movibles

El operador address-of (§23.6.5) y la fixed instrucción (§23.7) dividen las variables en dos categorías: Variables fijas y variables desplazables.

Las variables fijas residen en ubicaciones de almacenamiento que no se ven afectadas por el funcionamiento del recolector de elementos no utilizados. (Algunos ejemplos de variables fijas incluyen variables locales, parámetros de valor y variables creadas por punteros de desreferencia). Por otro lado, las variables movibles residen en ubicaciones de almacenamiento que están sujetas a reubicación o eliminación por parte del recolector de elementos no utilizados. (Algunos ejemplos de variables desplazables incluyen campos en objetos y elementos de matrices).

El & operador (§23.6.5) permite obtener la dirección de una variable fija sin restricciones. Sin embargo, dado que una variable desplazable está sujeta a reubicación o eliminación por parte del recolector de elementos no utilizados, la dirección de una variable desplazable solo se puede obtener mediante un fixed statement (§23.7) y esa dirección permanece válida solo durante la duración de esa fixed instrucción.

En términos precisos, una variable fija es una de las siguientes:

Todas las demás variables se clasifican como variables desplazables.

Un campo estático se clasifica como una variable desplazable. Además, un parámetro por referencia se clasifica como una variable desplazable, incluso si el argumento proporcionado para el parámetro es una variable fija. Por último, una variable generada por la desreferenciación de un puntero siempre se clasifica como una variable fija.

23.5 Conversiones de puntero

23.5.1 General

En un contexto no seguro, el conjunto de conversiones implícitas disponibles (§10.2) se extiende para incluir las siguientes conversiones implícitas de puntero:

  • De cualquier pointer_type al tipo void*.
  • Desde el null literal (§6.4.5.7) hasta cualquier pointer_type.

Además, en un contexto no seguro, el conjunto de conversiones explícitas disponibles (§10.3) se extiende para incluir las siguientes conversiones de puntero explícitas:

  • Desde cualquier pointer_type a cualquier otro pointer_type.
  • De sbyte, byte, short, ushort, int, uint, longo ulong a cualquier pointer_type.
  • De cualquier pointer_type a , , ushortintbyteuintshort, , longo .ulongsbyte

Por último, en un contexto no seguro, el conjunto de conversiones implícitas estándar (§10.4.2) incluye las siguientes conversiones de puntero:

  • De cualquier pointer_type al tipo void*.
  • Desde el null literal hasta cualquier pointer_type.

Las conversiones entre dos tipos de puntero nunca cambian el valor real del puntero. En otras palabras, una conversión de un tipo de puntero a otro no tiene ningún efecto en la dirección subyacente dada por el puntero.

Cuando un tipo de puntero se convierte en otro, si el puntero resultante no está alineado correctamente para el tipo apuntado, el comportamiento no se define si el resultado se desreferencia. En general, el concepto "alineado correctamente" es transitivo: si un puntero al tipo A está alineado correctamente para un puntero al tipo B, que, a su vez, se alinea correctamente para un puntero al tipo C, un puntero al tipo se alinea correctamente para un puntero Cal tipo A .

Ejemplo: considere el siguiente caso en el que se tiene acceso a una variable que tiene un tipo a través de un puntero a otro tipo:

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
}

ejemplo final

Cuando un tipo de puntero se convierte en un puntero a byte, el resultado apunta a la dirección byte más baja de la variable. Los incrementos sucesivos del resultado, hasta el tamaño de la variable, producen punteros a los bytes restantes de esa variable.

Ejemplo: el método siguiente muestra cada uno de los ocho bytes de un double como un valor hexadecimal:

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

Por supuesto, la salida producida depende de la endianidad. Una posibilidad es " BA FF 51 A2 90 6C 24 45".

ejemplo final

Las asignaciones entre punteros y enteros están definidas por la implementación.

Nota: Sin embargo, en arquitecturas de CPU de 32 y 64 bits con un espacio de direcciones lineal, las conversiones de punteros a tipos enteros o desde ellos normalmente se comportan exactamente como conversiones de uint valores o ulong , respectivamente, hacia o desde esos tipos enteros. nota final

23.5.2 Matrices de puntero

Las matrices de punteros se pueden construir mediante array_creation_expression (§12.8.17.5) en un contexto no seguro. Solo se permiten algunas de las conversiones que se aplican a otros tipos de matriz en matrices de punteros:

  • La conversión de referencia implícita (§10.2.8) de cualquier array_type a System.Array y las interfaces que implementa también se aplica a las matrices de punteros. Sin embargo, cualquier intento de acceder a los elementos de matriz a través System.Array de o las interfaces que implementa puede dar lugar a una excepción en tiempo de ejecución, ya que los tipos de puntero no se pueden convertir en object.
  • Las conversiones de referencia implícitas y explícitas (§10.2.8, §10.3.5) de un tipo S[] de matriz unidimensional a System.Collections.Generic.IList<T> y sus interfaces base genéricas nunca se aplican a las matrices de punteros.
  • La conversión de referencia explícita (§10.3.5) de System.Array y las interfaces que implementa en cualquier array_type se aplica a las matrices de punteros.
  • Las conversiones de referencia explícitas (§10.3.5) de System.Collections.Generic.IList<S> y sus interfaces base a un tipo T[] de matriz unidimensional nunca se aplican a matrices de punteros, ya que los tipos de puntero no se pueden usar como argumentos de tipo y no hay conversiones de tipos de puntero a tipos que no son de puntero.

Estas restricciones significan que la expansión de la foreach instrucción sobre matrices descritas en §9.4.4.17 no se puede aplicar a matrices de punteros. En su lugar, una foreach instrucción del formulario

foreach (V v in x)embedded_statement

donde el tipo de es un tipo de x matriz de la forma T[,,...,], n es el número de dimensiones menos 1 y T o V es un tipo de puntero, se expande mediante bucles for anidados como se indica a continuación:

{
    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*
            }
        }
    }
}

Las variables a, i0, i1, ... in no son visibles ni accesibles para x ni para el embedded_statement ni para ningún otro código fuente del programa. La variable v es de solo lectura en la instrucción insertada. Si no hay una conversión explícita (§23.5) de T (el tipo de elemento) a V, se produce un error y no se realizan pasos adicionales. Si x tiene el valor null, se produce una System.NullReferenceException excepción en tiempo de ejecución.

Nota: Aunque los tipos de puntero no se permiten como argumentos de tipo, las matrices de punteros se pueden usar como argumentos de tipo. nota final

23.6 Punteros en expresiones

23.6.1 General

En un contexto no seguro, una expresión puede producir un resultado de un tipo de puntero, pero fuera de un contexto no seguro, es un error en tiempo de compilación para que una expresión sea de un tipo de puntero. En términos precisos, fuera de un contexto no seguro, se produce un error en tiempo de compilación si hay algún simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10) o element_access (§12.8.12) es de un tipo de puntero.

En un contexto no seguro, las primary_no_array_creation_expression (§12.8) y unary_expression (§12.9) permiten construcciones adicionales, que se describen en las subclases siguientes.

Nota: La gramática implica la precedencia y la asociatividad de los operadores no seguros. nota final

23.6.2 Direccionamiento indirecto de puntero

Un pointer_indirection_expression consta de un asterisco (*) seguido de un unary_expression.

pointer_indirection_expression
    : '*' unary_expression
    ;

El operador unario * denota la direccionamiento indirecto del puntero y se usa para obtener la variable a la que apunta un puntero. El resultado de evaluar *P, donde P es una expresión de un tipo T*de puntero , es una variable de tipo T. Se trata de un error en tiempo de compilación para aplicar el operador unario * a una expresión de tipo void* o a una expresión que no es de un tipo de puntero.

El efecto de aplicar el operador unario * a un nullpuntero con valores es definido por la implementación. En concreto, no hay ninguna garantía de que esta operación produzca una System.NullReferenceExceptionexcepción .

Si se ha asignado un valor no válido al puntero, el comportamiento del operador unario * no está definido.

Nota: Entre los valores no válidos para desreferenciar un puntero por el operador unario * se alinean de forma inapropiada para el tipo al que apunta (vea el ejemplo de §23.5) y la dirección de una variable después del final de su vigencia.

Para fines del análisis de asignación definitiva, una variable generada mediante la evaluación de una expresión del formulario *P se considera asignada inicialmente (§9.4.2).

Acceso a miembros del puntero 23.6.3

Un pointer_member_access consta de un primary_expression, seguido de un token "->", seguido de un identificador y un type_argument_list opcional.

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

En un acceso de miembro de puntero del formulario P->I, P debe ser una expresión de un tipo de puntero y I denotará un miembro accesible del tipo al que P apunta.

Un acceso de miembro de puntero del formulario P->I se evalúa exactamente como (*P).I. Para obtener una descripción del operador de direccionamiento indirecto de puntero (*), consulte §23.6.2. Para obtener una descripción del operador de acceso a miembros (.), consulte §12.8.7.

Ejemplo: en el código siguiente

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

El -> operador se usa para acceder a los campos e invocar un método de una estructura a través de un puntero. Dado que la operación P->I es exactamente equivalente a (*P).I, el Main método podría haber sido igualmente escrito:

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

ejemplo final

Acceso al elemento puntero 23.6.4

Un pointer_element_access consta de un primary_no_array_creation_expression seguido de una expresión entre "[" y "]".

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

En un acceso de elemento de puntero del formato P[E], P debe ser una expresión de un tipo de puntero distinto void*de , y E será una expresión que se puede convertir implícitamente en int, uint, longo ulong.

El acceso a un elemento de puntero del formulario P[E] se evalúa exactamente como *(P + E). Para obtener una descripción del operador de direccionamiento indirecto de puntero (*), consulte §23.6.2. Para obtener una descripción del operador de suma de puntero (+), consulte §23.6.7.

Ejemplo: en el código siguiente

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

Se usa un acceso de elemento de puntero para inicializar el búfer de caracteres en un for bucle. Dado que la operación P[E] es exactamente equivalente a *(P + E), el ejemplo podría haber sido igualmente escrito:

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

ejemplo final

El operador de acceso al elemento de puntero no comprueba si hay errores no enlazados y el comportamiento al acceder a un elemento fuera de límite no está definido.

Nota: Esto es lo mismo que C y C++. nota final

23.6.5 El operador address-of

Un addressof_expression consta de una y comercial (&) seguida de un unary_expression.

addressof_expression
    : '&' unary_expression
    ;

Dada una expresión E que es de un tipo T y se clasifica como una variable fija (§23.4), la construcción &E calcula la dirección de la variable dada por E. El tipo del resultado es T* y se clasifica como un valor. Se produce un error en tiempo de compilación si E no se clasifica como una variable, si E se clasifica como una variable local de solo lectura o si E denota una variable desplazable. En el último caso, se puede usar una instrucción fija (§23.7) para "corregir" temporalmente la variable antes de obtener su dirección.

Nota: Como se indica en §12.8.7, fuera de un constructor de instancia o constructor estático para una estructura o clase que define un readonly campo, ese campo se considera un valor, no una variable. Por lo tanto, no se puede tomar su dirección. Del mismo modo, no se puede tomar la dirección de una constante. nota final

El & operador no requiere que su argumento se asigne definitivamente, pero después de una & operación, la variable a la que se aplica el operador se considera definitivamente asignada en la ruta de acceso de ejecución en la que se produce la operación. Es responsabilidad del programador asegurarse de que la inicialización correcta de la variable realmente tiene lugar en esta situación.

Ejemplo: en el código siguiente

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

i se considera definitivamente asignado después de la &i operación utilizada para inicializar p. La asignación a *p en vigor inicializa i, pero la inclusión de esta inicialización es responsabilidad del programador y no se produciría ningún error en tiempo de compilación si se quitase la asignación.

ejemplo final

Nota: Las reglas de asignación definitiva para el & operador existen de modo que se pueda evitar la inicialización redundante de variables locales. Por ejemplo, muchas API externas toman un puntero a una estructura rellenada por la API. Las llamadas a estas API normalmente pasan la dirección de una variable de estructura local y, sin la regla, se requeriría la inicialización redundante de la variable de estructura. nota final

Nota: Cuando una función anónima captura una variable local, un parámetro de valor o una matriz de parámetros (§12.8.24), esa variable local, parámetro o matriz de parámetros ya no se considera una variable fija (§23.7), pero en su lugar se considera una variable desplazable. Por lo tanto, es un error para que cualquier código no seguro tome la dirección de una variable local, un parámetro de valor o una matriz de parámetros capturada por una función anónima. nota final

23.6.6 Incremento y disminución del puntero

En un contexto no seguro, los ++ operadores y -- (§12.8.16 y §12.9.6) se pueden aplicar a variables de puntero de todos los tipos excepto void*. Por lo tanto, para cada tipo T*de puntero , se definen implícitamente los operadores siguientes:

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

Los operadores producen los mismos resultados que x+1 y x-1, respectivamente (§23.6.7). Es decir, para una variable de puntero de tipo T*, el ++ operador agrega sizeof(T) a la dirección contenida en la variable y el -- operador resta sizeof(T) de la dirección contenida en la variable.

Si una operación de incremento o disminución de puntero desborda el dominio del tipo de puntero, el resultado está definido por la implementación, pero no se generan excepciones.

23.6.7 Aritmética de puntero

En un contexto no seguro, el operador (§12.10.5) y - el + operador (§12.10.6) se pueden aplicar a los valores de todos los tipos de puntero excepto void*. Por lo tanto, para cada tipo T*de puntero , se definen implícitamente los operadores siguientes:

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

Dada una expresión P de un tipo T* de puntero y una expresión N de tipo int, , longuinto ulong, las expresiones P + N y N + P calculan el valor de puntero de tipo T* que resulta de agregar N * sizeof(T) a la dirección dada por P. Del mismo modo, la expresión P – N calcula el valor de puntero de tipo T* que resulta de restar N * sizeof(T) de la dirección dada por P.

Dadas dos expresiones, y Q, de un tipo T*de puntero , la expresión P – Q calcula la diferencia entre las direcciones dadas por P yQ, a continuación, P divide esa diferencia por sizeof(T). El tipo del resultado siempre longes . En efecto, P - Q se calcula como ((long)(P) - (long)(Q)) / sizeof(T).

Ejemplo:

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

que genera la salida:

p - q = -14
q - p = 14

ejemplo final

Si una operación aritmética de puntero desborda el dominio del tipo de puntero, el resultado se trunca de forma definida por la implementación, pero no se generan excepciones.

Comparación de punteros 23.6.8

En un contexto no seguro, los ==operadores , !=, <>, , <=y >= (§12.12) se pueden aplicar a los valores de todos los tipos de puntero. Los operadores de comparación de punteros son:

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

Dado que existe una conversión implícita desde cualquier tipo de puntero al void* tipo, los operandos de cualquier tipo de puntero se pueden comparar mediante estos operadores. Los operadores de comparación comparan las direcciones dadas por los dos operandos como si fueran enteros sin signo.

23.6.9 El operador sizeof

Para determinados tipos predefinidos (§12.8.19), el sizeof operador produce un valor constante int . Para todos los demás tipos, el resultado del sizeof operador está definido por la implementación y se clasifica como un valor, no como una constante.

El orden en el que los miembros se empaquetan en un struct no se especifican.

Con fines de alineación, puede haber relleno sin nombre al principio de una estructura, dentro de una estructura y al final de la estructura. El contenido de los bits usados como relleno es indeterminado.

Cuando se aplica a un operando que tiene un tipo de estructura, el resultado es el número total de bytes de una variable de ese tipo, incluido cualquier relleno.

23.7 Instrucción fija

En un contexto no seguro, la producción de embedded_statement (§13.1) permite una construcción adicional, la instrucción fija, que se usa para "corregir" una variable desplazable de modo que su dirección permanezca constante durante la duración de la instrucción.

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
    ;

Cada fixed_pointer_declarator declara una variable local del pointer_type especificado e inicializa esa variable local con la dirección calculada por el fixed_pointer_initializer correspondiente. Se puede acceder a una variable local declarada en una instrucción fija en cualquier fixed_pointer_initializerque se produzca a la derecha de la declaración de esa variable y en la embedded_statement de la instrucción fija. Una variable local declarada por una instrucción fija se considera de solo lectura. Se produce un error en tiempo de compilación si la instrucción insertada intenta modificar esta variable local (a través de la asignación o los ++ operadores y -- ) o pasarla como parámetro de referencia o salida.

Es un error usar una variable local capturada (§12.19.6.2), un parámetro de valor o una matriz de parámetros en un fixed_pointer_initializer. Un fixed_pointer_initializer puede ser uno de los siguientes:

  • El token "&" seguido de un variable_reference (§9.5) a una variable movible (§23.4) de un tipo Tno administrado, siempre que el tipo T* se pueda convertir implícitamente al tipo de puntero proporcionado en la fixed instrucción . En este caso, el inicializador calcula la dirección de la variable especificada y se garantiza que la variable permanezca en una dirección fija mientras dure la instrucción fija.
  • Expresión de un array_type con elementos de un tipo Tno administrado , siempre que el tipo T* se puede convertir implícitamente al tipo de puntero especificado en la instrucción fija. En este caso, el inicializador calcula la dirección del primer elemento de la matriz y se garantiza que toda la matriz permanezca en una dirección fija durante la duración de la fixed instrucción. Si la expresión de matriz es null o si la matriz tiene cero elementos, el inicializador calcula una dirección igual a cero.
  • Expresión de tipo string, siempre que el tipo char* se puede convertir implícitamente al tipo de puntero especificado en la fixed instrucción . En este caso, el inicializador calcula la dirección del primer carácter de la cadena y se garantiza que toda la cadena permanezca en una dirección fija mientras dure la fixed instrucción. El comportamiento de la instrucción está definido por la fixed implementación si la expresión de cadena es null.
  • Una expresión de tipo que no sea array_type o string, siempre que exista un método accesible o un método de extensión accesible que coincida con la firma ref [readonly] T GetPinnableReference(), donde T es un unmanaged_type y T* se puede convertir implícitamente al tipo de puntero especificado en la fixed instrucción . En este caso, el inicializador calcula la dirección de la variable devuelta y esa variable se garantiza que permanezca en una dirección fija mientras dure la fixed instrucción. La instrucción puede usar un GetPinnableReference() método cuando la fixed resolución de sobrecarga (§12.6.4) genera exactamente un miembro de función y ese miembro de función satisface las condiciones anteriores. El GetPinnableReference método debe devolver una referencia a una dirección igual a cero, como la que se devuelve cuando System.Runtime.CompilerServices.Unsafe.NullRef<T>() no hay datos que anclar.
  • Un simple_name o member_access que hace referencia a un miembro de búfer de tamaño fijo de una variable desplazable, siempre que el tipo del miembro de búfer de tamaño fijo se pueda convertir implícitamente al tipo de puntero especificado en la fixed instrucción . En este caso, el inicializador calcula un puntero al primer elemento del búfer de tamaño fijo (§23.8.3) y se garantiza que el búfer de tamaño fijo permanezca en una dirección fija durante la instrucción fixed .

Para cada dirección calculada por un fixed_pointer_initializer la fixed instrucción garantiza que la variable a la que hace referencia la dirección no esté sujeta a reubicación o eliminación por parte del recolector de elementos no utilizados durante la duración de la fixed instrucción.

Ejemplo: si la dirección calculada por un fixed_pointer_initializer hace referencia a un campo de un objeto o a un elemento de una instancia de matriz, la instrucción fija garantiza que la instancia de objeto contenedor no se reubica ni elimina durante la vigencia de la instrucción. ejemplo final

Es responsabilidad del programador asegurarse de que los punteros creados por instrucciones fijas no sobreviven más allá de la ejecución de esas instrucciones.

Ejemplo: cuando los punteros creados por fixed instrucciones se pasan a api externas, es responsabilidad del programador asegurarse de que las API no conservan memoria de estos punteros. ejemplo final

Los objetos fijos pueden provocar la fragmentación del montón (porque no se pueden mover). Por ese motivo, los objetos solo deben corregirse cuando sea absolutamente necesario y, a continuación, solo durante la menor cantidad de tiempo posible.

Ejemplo: El ejemplo

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

muestra varios usos de la fixed instrucción . La primera instrucción corrige y obtiene la dirección de un campo estático, la segunda instrucción corrige y obtiene la dirección de un campo de instancia, y la tercera instrucción corrige y obtiene la dirección de un elemento de matriz. En cada caso, habría sido un error usar el operador normal & , ya que todas las variables se clasifican como variables movibles.

Las instrucciones tercera y cuarta fixed del ejemplo anterior generan resultados idénticos. En general, para una instancia ade matriz , especificar a[0] en una fixed instrucción es igual que simplemente especificar a.

ejemplo final

En un contexto no seguro, los elementos de matriz de matrices unidimensionales se almacenan en orden de índice creciente, empezando por el índice 0 y terminando con el índice Length – 1. En el caso de las matrices multidimensionales, los elementos de matriz se almacenan de forma que los índices de la dimensión situada más a la derecha se incrementan primero, luego la siguiente dimensión izquierda, etc. a la izquierda.

Dentro de una fixed instrucción que obtiene un puntero p a una instancia ade matriz , los valores de puntero que van desde p para p + a.Length - 1 representar direcciones de los elementos de la matriz. Del mismo modo, las variables que van desde p[0] para p[a.Length - 1] representar los elementos de matriz reales. Dada la forma en que se almacenan las matrices, se puede tratar una matriz de cualquier dimensión como si fuera lineal.

Ejemplo:

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

que genera la salida:

[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

ejemplo final

Ejemplo: en el código siguiente

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

Se usa una fixed instrucción para corregir una matriz para que su dirección se pueda pasar a un método que toma un puntero.

ejemplo final

Un char* valor generado mediante la corrección de una instancia de cadena siempre apunta a una cadena terminada en null. Dentro de una instrucción fija que obtiene un puntero p a una instancia sde cadena , los valores de puntero que van desde p para p + s.Length ‑ 1 representar direcciones de los caracteres de la cadena y el valor p + s.Length de puntero siempre apunta a un carácter NULL (el carácter con el valor '\0').

Ejemplo:

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

ejemplo final

Ejemplo: el código siguiente muestra un fixed_pointer_initializer con una expresión de tipo distinto de array_type o 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)
        {
            // ...
        }
    }
}

El tipo C tiene un método accesible GetPinnableReference con la firma correcta. En la fixed instrucción , el ref int devuelto desde ese método cuando se llama a en c se usa para inicializar el int* puntero p. ejemplo final

La modificación de objetos de tipo administrado a través de punteros fijos puede dar lugar a un comportamiento indefinido.

Nota: Por ejemplo, dado que las cadenas son inmutables, es responsabilidad del programador asegurarse de que los caracteres a los que hace referencia un puntero a una cadena fija no se modifican. nota final

Nota: La terminación automática de valores NULL de las cadenas es especialmente conveniente al llamar a las API externas que esperan cadenas de "estilo C". Tenga en cuenta, sin embargo, que se permite que una instancia de cadena contenga caracteres NULL. Si estos caracteres NULL están presentes, la cadena aparecerá truncada cuando se trate como un objeto terminado char*en null. nota final

23.8 Búferes de tamaño fijo

23.8.1 General

Los búferes de tamaño fijo se usan para declarar matrices en línea de "estilo C" como miembros de estructuras y son principalmente útiles para interactuar con LAS API no administradas.

23.8.2 Declaraciones de búfer de tamaño fijo

Un búfer de tamaño fijo es un miembro que representa el almacenamiento de un búfer de longitud fija de variables de un tipo determinado. Una declaración de búfer de tamaño fijo introduce uno o varios búferes de tamaño fijo de un tipo de elemento determinado.

Nota: Al igual que una matriz, se puede considerar un búfer de tamaño fijo como elementos contenedor. Por lo tanto, el término tipo de elemento tal como se define para una matriz también se usa con un búfer de tamaño fijo. nota final

Los búferes de tamaño fijo solo se permiten en declaraciones de estructura y solo pueden producirse en contextos no seguros (§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 ']'
    ;

Una declaración de búfer de tamaño fijo puede incluir un conjunto de atributos (§22), un new modificador (§15.3.5), modificadores de accesibilidad correspondientes a cualquiera de las accesibilidades declaradas permitidas para los miembros de estructura (§16.4.3) y un unsafe modificador (§23.2). Los atributos y modificadores se aplican a todos los miembros declarados por la declaración de búfer de tamaño fijo. Es un error para que el mismo modificador aparezca varias veces en una declaración de búfer de tamaño fijo.

No se permite incluir el static modificador una declaración de búfer de tamaño fijo.

El tipo de elemento de búfer de una declaración de búfer de tamaño fijo especifica el tipo de elemento de los búferes introducidos por la declaración. El tipo de elemento de búfer debe ser uno de los tipos predefinidos sbyte, , longushortintuintshortbytecharfloatulongdoubleo .bool

El tipo de elemento de búfer va seguido de una lista de declaradores de búfer de tamaño fijo, cada uno de los cuales presenta un nuevo miembro. Un declarador de búfer de tamaño fijo consta de un identificador que asigna un nombre al miembro, seguido de una expresión constante incluida en [ los tokens y ] . La expresión constante denota el número de elementos del miembro introducido por ese declarador de búfer de tamaño fijo. El tipo de la expresión constante se podrá convertir implícitamente en el tipo inty el valor será un entero positivo distinto de cero.

Los elementos de un búfer de tamaño fijo se colocarán secuencialmente en memoria.

Una declaración de búfer de tamaño fijo que declara varios búferes de tamaño fijo equivale a varias declaraciones de una sola declaración de búfer de tamaño fijo con los mismos atributos y tipos de elementos.

Ejemplo:

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

es equivalente a

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

ejemplo final

23.8.3 Búferes de tamaño fijo en expresiones

La búsqueda de miembros (§12.5) de un miembro de búfer de tamaño fijo continúa exactamente igual que la búsqueda de miembros de un campo.

Se puede hacer referencia a un búfer de tamaño fijo en una expresión mediante un simple_name (§12.8.4), un member_access (§12.8.7) o un element_access (§12.8.12).

Cuando se hace referencia a un miembro de búfer de tamaño fijo como un nombre simple, el efecto es el mismo que un acceso de miembro del formulario this.I, donde I es el miembro de búfer de tamaño fijo.

En un acceso de miembro del formulario E.I donde E. puede ser el implícito this., si E es de un tipo de estructura y una búsqueda de miembro de I en ese tipo de estructura identifica un miembro de tamaño fijo, se E.I evalúa y clasifica de la siguiente manera:

  • Si la expresión E.I no se produce en un contexto no seguro, se produce un error en tiempo de compilación.
  • Si E se clasifica como un valor, se produce un error en tiempo de compilación.
  • De lo contrario, si E es una variable que se puede mover (§23.4), a continuación:
    • Si la expresión E.I es un fixed_pointer_initializer (§23.7), el resultado de la expresión es un puntero al primer elemento del miembro I de búfer de tamaño fijo en E.
    • De lo contrario, si la expresión E.I es un primary_no_array_creation_expression (§12.8.12.1) dentro de una element_access (§12.8.12) del formulario E.I[J], el resultado de E.I es un puntero, P, al primer elemento del miembro I de búfer de tamaño fijo en Ey, a continuación, el element_access envolvente se evalúa como el pointer_element_access (§23.6.4). P[J]
    • De lo contrario, se produce un error en tiempo de compilación.
  • De lo contrario, E hace referencia a una variable fija y el resultado de la expresión es un puntero al primer elemento del miembro I de búfer de tamaño fijo en E. El resultado es de tipo S*, donde S es el tipo de elemento de Iy se clasifica como un valor.

Se puede acceder a los elementos posteriores del búfer de tamaño fijo mediante operaciones de puntero desde el primer elemento. A diferencia del acceso a las matrices, el acceso a los elementos de un búfer de tamaño fijo es una operación no segura y no se comprueba el intervalo.

Ejemplo: lo siguiente declara y usa una estructura con un miembro de búfer de tamaño fijo.

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

ejemplo final

23.8.4 Comprobación de la asignación definitiva

Los búferes de tamaño fijo no están sujetos a la comprobación de asignaciones definitiva (§9.4) y los miembros del búfer de tamaño fijo se omiten con fines de comprobación de asignación definitiva de variables de tipo struct.

Cuando la variable de estructura más externa de un miembro de búfer de tamaño fijo es una variable estática, una variable de instancia de una instancia de clase o un elemento de matriz, los elementos del búfer de tamaño fijo se inicializan automáticamente en sus valores predeterminados (§9.3). En todos los demás casos, el contenido inicial de un búfer de tamaño fijo no está definido.

23.9 Asignación de pila

Consulte §12.8.22 para obtener información general sobre el operador stackalloc. Aquí se describe la capacidad de ese operador para dar lugar a un puntero.

En un contexto no seguro si se produce un stackalloc_expression (§12.8.22) como expresión inicializadora de un local_variable_declaration (§13.6.2), donde el local_variable_type es un tipo de puntero (§23.3) o inferido (var), el resultado del stackalloc_expression es un puntero de tipo T * que se va a iniciar del bloque asignado, donde T es el unmanaged_type del stackalloc_expression.

En el resto de aspectos, la semántica de local_variable_declarations (§13.6.2) y stackalloc_expressions (§12.8.22) en contextos no seguros siguen las definidas para contextos seguros.

Ejemplo:

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

ejemplo final

A diferencia del acceso a matrices o stackallocbloques de tipo "ed Span<T> ", el acceso a los elementos de un stackalloc"bloque ed de tipo de puntero es una operación no segura y no está activada el intervalo.

Ejemplo: en el código siguiente

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

Se usa una stackalloc expresión en el IntToString método para asignar un búfer de 16 caracteres en la pila. El búfer se descarta automáticamente cuando el método devuelve.

Tenga en cuenta, sin embargo, que IntToString se puede reescribir en modo seguro; es decir, sin usar punteros, como se indica a continuación:

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

ejemplo final

Fin del texto normativo condicionalmente.