Compartilhar via


23 Código inseguro

23.1 Geral

Uma implementação que não dá suporte a código não seguro é necessária para diagnosticar qualquer uso das regras sintáticas definidas nesta cláusula.

O restante desta cláusula, incluindo todas as suas subcláusulas, é condicionalmente normativo.

Observação: a linguagem C# principal, conforme definido nas cláusulas anteriores, difere notavelmente de C e C++ em sua omissão de ponteiros como um tipo de dados. Em vez disso, o C# fornece referências e a capacidade de criar objetos gerenciados por um coletor de lixo. Esse design, juntamente com outros recursos, torna o C# uma linguagem muito mais segura do que C ou C++. Na linguagem C# principal, simplesmente não é possível ter uma variável não inicializada, um ponteiro "pendente" ou uma expressão que indexe uma matriz além de seus limites. Categorias inteiras de bugs que rotineiramente atormentam os programas C e C++ são assim eliminadas.

Embora praticamente todos os constructos de tipo de ponteiro em C ou C++ tenham uma contraparte de tipo de referência em C#, há situações em que o acesso a tipos de ponteiro se torna uma necessidade. Por exemplo, a interface com o sistema operacional subjacente, o acesso a um dispositivo mapeado na memória ou a implementação de um algoritmo de tempo crítico podem não ser possíveis ou práticos sem acesso a ponteiros. Para atender a essa necessidade, o C# fornece a capacidade de escrever código não seguro.

Em código inseguro, é possível declarar e operar em ponteiros, realizar conversões entre ponteiros e tipos integrais, obter o endereço de variáveis e assim por diante. De certa forma, escrever código inseguro é muito parecido com escrever código C em um programa C#.

O código inseguro é, na verdade, um recurso "seguro" da perspectiva de desenvolvedores e usuários. O código não seguro deve ser claramente marcado com o modificador unsafe, para que os desenvolvedores não possam usar recursos inseguros acidentalmente, e o mecanismo de execução funciona para garantir que o código não seguro não possa ser executado em um ambiente não confiável.

nota final

23.2 Contextos inseguros

Os recursos não seguros do C# estão disponíveis apenas em contextos não seguros. Um contexto não seguro é introduzido incluindo um unsafe modificador na declaração de um tipo, membro ou função local, ou empregando um unsafe_statement:

  • Uma declaração de uma classe, struct, interface ou delegado pode incluir um unsafe modificador, nesse caso, toda a extensão textual dessa declaração de tipo (incluindo o corpo da classe, struct ou interface) é considerada um contexto não seguro.

    Observação: se o type_declaration for parcial, somente essa parte será um contexto inseguro. nota final

  • Uma declaração de um campo, método, propriedade, evento, indexador, operador, construtor de instância, finalizador, construtor estático ou função local pode incluir um unsafe modificador, nesse caso, toda a extensão textual dessa declaração de membro é considerada um contexto não seguro.
  • Um unsafe_statement permite o uso de um contexto não seguro dentro de um bloco. Toda a extensão textual do bloco associado é considerada um contexto inseguro. Uma função local declarada em um contexto inseguro é insegura.

As extensões gramaticais associadas são mostradas abaixo e nas subcláusulas subsequentes.

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

Exemplo: no código a seguir

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

O unsafe modificador especificado na declaração struct faz com que toda a extensão textual da declaração struct se torne um contexto inseguro. Assim, é possível declarar os Left campos e Right como sendo de um tipo de ponteiro. O exemplo acima também pode ser escrito

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

Aqui, os unsafe modificadores nas declarações de campo fazem com que essas declarações sejam consideradas contextos inseguros.

exemplo de fim

Além de estabelecer um contexto inseguro, permitindo assim o uso de tipos de ponteiro, o unsafe modificador não tem efeito sobre um tipo ou membro.

Exemplo: no código a seguir

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

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

O modificador inseguro no F método em A simplesmente faz com que a extensão textual de F se torne um contexto inseguro no qual os recursos inseguros da linguagem podem ser usados. Na substituição de F in B, não há necessidade de especificar novamente o unsafe modificador, a menos, é claro, que o F método em B si precise de acesso a recursos não seguros.

A situação é um pouco diferente quando um tipo de ponteiro faz parte da assinatura do método

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

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

Aqui, como Fa assinatura de inclui um tipo de ponteiro, ela só pode ser gravada em um contexto inseguro. No entanto, o contexto inseguro pode ser introduzido tornando toda a classe insegura, como é o caso em A, ou incluindo um unsafe modificador na declaração do método, como é o caso em B.

exemplo de fim

Quando o unsafe modificador é usado em uma declaração de tipo parcial (§15.2.7), somente essa parte específica é considerada um contexto não seguro.

23.3 Tipos de ponteiro

Em um contexto não seguro, um tipo (§8.1) pode ser um pointer_type bem como um value_type, um reference_type ou um type_parameter. Em um contexto inseguro, um pointer_type também pode ser o tipo de elemento de uma matriz (§17). Um pointer_type também pode ser usado em uma expressão typeof (§12.8.18) fora de um contexto inseguro (já que esse uso não é inseguro).

Um pointer_type é escrito como um unmanaged_type (§8.8) ou a palavra-chave void, seguida por um * token:

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

O tipo especificado antes de * um tipo de ponteiro é chamado de tipo de referência do tipo de ponteiro. Ele representa o tipo da variável para a qual um valor do tipo de ponteiro aponta.

Um pointer_type só pode ser usado em um array_type em um contexto inseguro (§23.2). Um non_array_type é qualquer tipo que não seja em si um array_type.

Ao contrário das referências (valores de tipos de referência), os ponteiros não são rastreados pelo coletor de lixo — o coletor de lixo não tem conhecimento dos ponteiros e dos dados para os quais eles apontam. Por esse motivo, um ponteiro não tem permissão para apontar para uma referência ou para um struct que contém referências, e o tipo de referência de um ponteiro deve ser um unmanaged_type. Os próprios tipos de ponteiro são tipos não gerenciados, portanto, um tipo de ponteiro pode ser usado como o tipo de referência para outro tipo de ponteiro.

A regra intuitiva para a mistura de ponteiros e referências é que referentes de referências (objetos) podem conter ponteiros, mas referentes de ponteiros não podem conter referências.

Exemplo: Alguns exemplos de tipos de ponteiro são fornecidos na tabela abaixo:

Exemplo Descrição
byte* Ponteiro para byte
char* Ponteiro para char
int** Ponteiro para ponteiro int
int*[] Matriz unidimensional de ponteiros para int
void* Ponteiro para tipo desconhecido

exemplo de fim

Para uma determinada implementação, todos os tipos de ponteiro devem ter o mesmo tamanho e representação.

Observação: ao contrário de C e C++, quando vários ponteiros são declarados na mesma declaração, em C# o * é gravado apenas junto com o tipo subjacente, não como um marcador de prefixo em cada nome de ponteiro. Por exemplo:

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

nota final

O valor de um ponteiro com tipo T* representa o endereço de uma variável do tipo T. O operador * de indireção do ponteiro (§23.6.2) pode ser usado para acessar essa variável.

Exemplo: Dada uma variável P do tipo int*, a expressão *P denota a int variável encontrada no endereço contido em P. exemplo de fim

Como uma referência de objeto, um ponteiro pode ser null. A aplicação do operador indireto a um nullponteiro com valor de -resulta em um comportamento definido pela implementação (§23.6.2). Um ponteiro com valor null é representado por todos os bits zero.

O void* tipo representa um ponteiro para um tipo desconhecido. Como o tipo de referência é desconhecido, o operador de indireção não pode ser aplicado a um ponteiro do tipo void*, nem qualquer aritmética pode ser executada em tal ponteiro. No entanto, um ponteiro do tipo void* pode ser convertido em qualquer outro tipo de ponteiro (e vice-versa) e comparado a valores de outros tipos de ponteiro (§23.6.8).

Os tipos de ponteiro são uma categoria separada de tipos. Ao contrário dos tipos de referência e dos tipos de valor, os tipos de ponteiro não herdam e não existem conversões entre os tipos de object ponteiro e object. Em particular, não há suporte para boxing e unboxing (§8.3.13) para ponteiros. No entanto, as conversões são permitidas entre diferentes tipos de ponteiro e entre tipos de ponteiro e os tipos integrais. Isso é descrito no §23.5.

Um pointer_type não pode ser usado como um argumento de tipo (§8.4) e a inferência de tipo (§12.6.3) falha em chamadas de método genéricas que teriam inferido um argumento de tipo como um tipo de ponteiro.

Um pointer_type não pode ser usado como um tipo de subexpressão de uma operação ligada dinamicamente (§12.3.3).

Um pointer_type não pode ser usado como o tipo do primeiro parâmetro em um método de extensão (§15.6.10).

Um pointer_type pode ser usado como o tipo de um campo volátil (§15.5.4).

A eliminação dinâmica de um tipo E* é o tipo de ponteiro com o tipo de referência da eliminação dinâmica de E.

Uma expressão com um tipo de ponteiro não pode ser usada para fornecer o valor em um member_declarator dentro de um anonymous_object_creation_expression (§12.8.17.7).

O valor padrão (§9.3) para qualquer tipo de ponteiro é null.

Nota: Embora os ponteiros possam ser passados como parâmetros por referência, isso pode causar um comportamento indefinido, pois o ponteiro pode muito bem ser configurado para apontar para uma variável local que não existe mais quando o método chamado retorna, ou o objeto fixo para o qual ele costumava apontar, não é mais fixo. Por exemplo:

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

Um método pode retornar um valor de algum tipo, e esse tipo pode ser um ponteiro.

Exemplo: Quando dado um ponteiro para uma sequência contígua de s, a contagem de intelementos dessa sequência e algum outro int valor, o método a seguir retorna o endereço desse valor nessa sequência, se ocorrer uma correspondência; caso contrário, ele retorna: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;
}

exemplo de fim

Em um contexto inseguro, várias construções estão disponíveis para operar em ponteiros:

  • O operador unário * pode ser usado para executar a indireção do ponteiro (§23.6.2).
  • O -> operador pode ser usado para acessar um membro de um struct por meio de um ponteiro (§23.6.3).
  • O [] operador pode ser usado para indexar um ponteiro (§23.6.4).
  • O operador unário & pode ser usado para obter o endereço de uma variável (§23.6.5).
  • Os ++ operadores and -- podem ser usados para incrementar e diminuir ponteiros (§23.6.6).
  • Os operadores binário + e podem ser usados para executar a aritmética do ponteiro (§23.6.7-).
  • Os ==operadores , !=, <, >, <=, e >= podem ser usados para comparar ponteiros (§23.6.8).
  • O stackalloc operador pode ser usado para alocar memória da pilha de chamadas (§23.9).
  • A fixed instrução pode ser usada para corrigir temporariamente uma variável para que seu endereço possa ser obtido (§23.7).

23.4 Variáveis fixas e móveis

O operador address-of (§23.6.5) e a instrução (§23.7) dividem as fixed variáveis em duas categorias: variáveis fixas e variáveis móveis.

As variáveis fixas residem em locais de armazenamento que não são afetados pela operação do coletor de lixo. (Exemplos de variáveis fixas incluem variáveis locais, parâmetros de valor e variáveis criadas por ponteiros de desreferenciamento.) Por outro lado, as variáveis móveis residem em locais de armazenamento que estão sujeitos a realocação ou descarte pelo coletor de lixo. (Exemplos de variáveis móveis incluem campos em objetos e elementos de matrizes.)

O & operador (§23.6.5) permite que o endereço de uma variável fixa seja obtido sem restrições. No entanto, como uma variável móvel está sujeita a realocação ou descarte pelo coletor de lixo, o endereço de uma variável móvel só pode ser obtido usando um fixed statement (§23.7), e esse endereço permanece válido apenas durante a duração dessa fixed instrução.

Em termos precisos, uma variável fixa é uma das seguintes:

Todas as outras variáveis são classificadas como variáveis móveis.

Um campo estático é classificado como uma variável móvel. Além disso, um parâmetro por referência é classificado como uma variável móvel, mesmo que o argumento dado para o parâmetro seja uma variável fixa. Finalmente, uma variável produzida pela desreferenciação de um ponteiro é sempre classificada como uma variável fixa.

23.5 Conversões de ponteiro

23.5.1 Geral

Em um contexto não seguro, o conjunto de conversões implícitas disponíveis (§10.2) é estendido para incluir as seguintes conversões de ponteiro implícitas:

  • De qualquer pointer_type para o tipo void*.
  • Do null literal (§6.4.5.7) para qualquer pointer_type.

Além disso, em um contexto não seguro, o conjunto de conversões explícitas disponíveis (§10.3) é estendido para incluir as seguintes conversões explícitas de ponteiro:

  • De qualquer pointer_type para qualquer outro pointer_type.
  • De sbyte, byte, short, ushort, int, uint, longou ulong para qualquer pointer_type.
  • De qualquer pointer_type para sbyte, byte, short, ushortint, , uint, longou ulong.

Por fim, em um contexto não seguro, o conjunto de conversões implícitas padrão (§10.4.2) inclui as seguintes conversões de ponteiro:

  • De qualquer pointer_type para o tipo void*.
  • Do null literal a qualquer pointer_type.

As conversões entre dois tipos de ponteiro nunca alteram o valor real do ponteiro. Em outras palavras, uma conversão de um tipo de ponteiro para outro não tem efeito sobre o endereço subjacente fornecido pelo ponteiro.

Quando um tipo de ponteiro é convertido em outro, se o ponteiro resultante não estiver alinhado corretamente para o tipo apontado, o comportamento será indefinido se o resultado for desreferenciado. Em geral, o conceito "alinhado corretamente" é transitivo: se um ponteiro para o tipo A estiver alinhado corretamente para um ponteiro para o tipo B, que, por sua vez, estiver alinhado corretamente para um ponteiro para o tipo C, então um ponteiro para o tipo A será alinhado corretamente para um ponteiro para o tipo C.

Exemplo: considere o seguinte caso em que uma variável com um tipo é acessada por meio de um ponteiro para um tipo diferente:

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
}

exemplo de fim

Quando um tipo de ponteiro é convertido em um ponteiro para byte, o resultado aponta para o endereço byte mais baixo da variável. Incrementos sucessivos do resultado, até o tamanho da variável, geram ponteiros para os bytes restantes dessa variável.

Exemplo: o método a seguir exibe cada um dos oito bytes em a double como um 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();
        }
    }
}

Claro, a saída produzida depende da endianidade. Uma possibilidade é " BA FF 51 A2 90 6C 24 45".

exemplo de fim

Os mapeamentos entre ponteiros e inteiros são definidos pela implementação.

Observação: no entanto, em arquiteturas de CPU de 32 e 64 bits com um espaço de endereço linear, as conversões de ponteiros de ou para tipos integrais normalmente se comportam exatamente como conversões de uint ou ulong valores, respectivamente, para ou desses tipos integrais. nota final

23.5.2 Matrizes de ponteiros

Matrizes de ponteiros podem ser construídas usando array_creation_expression (§12.8.17.5) em um contexto não seguro. Somente algumas das conversões que se aplicam a outros tipos de matriz são permitidas em matrizes de ponteiro:

  • A conversão de referência implícita (§10.2.8) de qualquer array_type para System.Array e as interfaces que ele implementa também se aplica a matrizes de ponteiro. No entanto, qualquer tentativa de acessar os elementos da matriz ou System.Array as interfaces que ela implementa pode resultar em uma exceção em tempo de execução, pois os tipos de ponteiro não podem ser convertidos em object.
  • As conversões de referência implícitas e explícitas (§10.2.8, §10.3.5) de um tipo S[] de matriz unidimensional para System.Collections.Generic.IList<T> e suas interfaces base genéricas nunca se aplicam a matrizes de ponteiro.
  • A conversão de referência explícita (§10.3.5) e System.Array as interfaces que ela implementa para qualquer array_type se aplicam a matrizes de ponteiro.
  • As conversões de referência explícitas (§10.3.5) de System.Collections.Generic.IList<S> e suas interfaces base para um tipo T[] de matriz unidimensional nunca se aplicam a matrizes de ponteiro, pois os tipos de ponteiro não podem ser usados como argumentos de tipo e não há conversões de tipos de ponteiro para tipos que não são de ponteiro.

Essas restrições significam que a expansão da foreach instrução sobre matrizes descritas em §9.4.4.17 não pode ser aplicada a matrizes de ponteiro. Em vez disso, uma foreach declaração do formulário

foreach (V v in x)embedded_statement

onde o tipo de x é um tipo de matriz do formulário T[,,...,], n é o número de dimensões menos 1 e T ou V é um tipo de ponteiro, é expandido usando loops for aninhados da seguinte maneira:

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

As variáveis a, i0, i1, ... in não são visíveis ou acessíveis ao x embedded_statement ou a qualquer outro código-fonte do programa. A variável v é somente leitura na instrução incorporada. Se não houver uma conversão explícita (§23.5) de (o tipo de T elemento) para V, um erro será produzido e nenhuma outra etapa será executada. Se x tiver o valor null, a System.NullReferenceException é lançado em tempo de execução.

Observação: embora os tipos de ponteiro não sejam permitidos como argumentos de tipo, as matrizes de ponteiro podem ser usadas como argumentos de tipo. nota final

23.6 Ponteiros em expressões

23.6.1 Geral

Em um contexto não seguro, uma expressão pode produzir um resultado de um tipo de ponteiro, mas fora de um contexto não seguro, é um erro de tempo de compilação que uma expressão seja de um tipo de ponteiro. Em termos precisos, fora de um contexto inseguro, ocorrerá um erro em tempo de compilação se qualquer simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10) ou element_access (§12.8.12) for de um tipo de ponteiro.

Em um contexto inseguro, as produções primary_no_array_creation_expression (§12.8) e unary_expression (§12.9) permitem construções adicionais, que são descritas nas subcláusulas a seguir.

Observação: a precedência e a associatividade dos operadores não seguros estão implícitas na gramática. nota final

23.6.2 Indireção do ponteiro

Um pointer_indirection_expression consiste em um asterisco (*) seguido por um unary_expression.

pointer_indirection_expression
    : '*' unary_expression
    ;

O operador unário * denota a indireção do ponteiro e é usado para obter a variável para a qual um ponteiro aponta. O resultado da avaliação *P, onde P é uma expressão de um tipo T*de ponteiro , é uma variável do tipo T. É um erro de tempo de compilação aplicar o operador unário * a uma expressão do tipo void* ou a uma expressão que não é de um tipo de ponteiro.

O efeito da aplicação do operador unário * a um nullponteiro com valor de -é definido pela implementação. Em particular, não há garantia de que essa operação gere um System.NullReferenceException.

Se um valor inválido tiver sido atribuído ao ponteiro, o comportamento do operador unário * será indefinido.

Observação: entre os valores inválidos para desreferenciar um ponteiro pelo operador unário * estão um endereço alinhado inadequadamente para o tipo apontado (consulte o exemplo em §23.5) e o endereço de uma variável após o final de seu tempo de vida.

Para fins de análise de atribuição definida, uma variável produzida pela avaliação de uma expressão da forma *P é considerada inicialmente atribuída (§9.4.2).

23.6.3 Acesso ao membro do ponteiro

Um pointer_member_access consiste em um primary_expression, seguido por um token "->", seguido por um identificador e um type_argument_list opcional.

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

Em um membro de ponteiro, o acesso do formulário P->I, P deve ser uma expressão de um tipo de ponteiro e I deve denotar um membro acessível do tipo para o qual P aponta.

Um acesso de membro de ponteiro do formulário P->I é avaliado exatamente como (*P).I. Para obter uma descrição do operador de indireção do ponteiro (*), consulte §23.6.2. Para obter uma descrição do operador de acesso de membro (.), consulte §12.8.7.

Exemplo: no código a seguir

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

O -> operador é usado para acessar campos e invocar um método de um struct por meio de um ponteiro. Como a operação P->I é precisamente equivalente a (*P).I, o Main método poderia igualmente ter sido escrito:

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

exemplo de fim

23.6.4 Acesso ao elemento de ponteiro

Um pointer_element_access consiste em um primary_no_array_creation_expression seguido por uma expressão entre "[" e "]".

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

Em um elemento de ponteiro, o acesso do formulário P[E], P deve ser uma expressão de um tipo de ponteiro diferente de void*, e E deve ser uma expressão que pode ser implicitamente convertida em int, uint, long, ou ulong.

Um acesso ao elemento de ponteiro do formulário P[E] é avaliado exatamente como *(P + E). Para obter uma descrição do operador de indireção do ponteiro (*), consulte §23.6.2. Para obter uma descrição do operador de adição de ponteiro (+), consulte §23.6.7.

Exemplo: no código a seguir

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

Um acesso ao elemento de ponteiro é usado para inicializar o buffer de caracteres em um for loop. Como a operação P[E] é precisamente equivalente a *(P + E), o exemplo poderia igualmente ter sido escrito:

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

exemplo de fim

O operador de acesso ao elemento de ponteiro não verifica se há erros fora dos limites e o comportamento ao acessar um elemento fora dos limites é indefinido.

Nota: Isso é o mesmo que C e C++. nota final

23.6.5 O operador de endereço

Um addressof_expression consiste em um E comercial (&) seguido por um unary_expression.

addressof_expression
    : '&' unary_expression
    ;

Dada uma expressão E que é de um tipo T e é classificada como uma variável fixa (§23.4), a construção &E calcula o endereço da variável dada por E. O tipo do resultado é T* e é classificado como um valor. Um erro de tempo de compilação ocorre se E não for classificado como uma variável, se E for classificado como uma variável local somente leitura ou se E denotar uma variável móvel. No último caso, uma instrução fixa (§23.7) pode ser usada para "consertar" temporariamente a variável antes de obter seu endereço.

Observação: conforme declarado em §12.8.7, fora de um construtor de instância ou construtor estático para um struct ou classe que define um readonly campo, esse campo é considerado um valor, não uma variável. Como tal, seu endereço não pode ser tomado. Da mesma forma, o endereço de uma constante não pode ser obtido. nota final

O & operador não exige que seu argumento seja definitivamente atribuído, mas após uma & operação, a variável à qual o operador é aplicado é considerada definitivamente atribuída no caminho de execução no qual a operação ocorre. É responsabilidade do programador garantir que a inicialização correta da variável realmente ocorra nessa situação.

Exemplo: no código a seguir

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

i é considerado definitivamente atribuído após a &i operação usada para inicializar p. A atribuição de *p inicializa i, mas a inclusão dessa inicialização é de responsabilidade do programador, e nenhum erro de tempo de compilação ocorreria se a atribuição fosse removida.

exemplo de fim

Nota: As regras de atribuição definida para o operador existem de modo que a & inicialização redundante de variáveis locais pode ser evitada. Por exemplo, muitas APIs externas usam um ponteiro para uma estrutura que é preenchida pela API. As chamadas para essas APIs normalmente passam o endereço de uma variável de struct local e, sem a regra, seria necessária a inicialização redundante da variável de struct. nota final

Observação: quando uma variável local, parâmetro de valor ou matriz de parâmetros é capturada por uma função anônima (§12.8.24), essa variável local, parâmetro ou matriz de parâmetros não é mais considerada uma variável fixa (§23.7), mas é considerada uma variável móvel. Portanto, é um erro para qualquer código não seguro obter o endereço de uma variável local, parâmetro de valor ou matriz de parâmetros que foi capturada por uma função anônima. nota final

23.6.6 Incremento e diminuição do ponteiro

Em um contexto inseguro, os ++ operadores and -- (§12.8.16 e §12.9.6) podem ser aplicados a variáveis de ponteiro de todos os tipos, exceto void*. Assim, para cada tipo T*de ponteiro , os seguintes operadores são definidos implicitamente:

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

Os operadores produzem os mesmos resultados que x+1 e x-1, respectivamente (§23.6.7). Em outras palavras, para uma variável de ponteiro do tipo T*, o ++ operador adiciona sizeof(T) ao endereço contido na variável e o -- operador subtrai sizeof(T) do endereço contido na variável.

Se uma operação de incremento ou decremento de ponteiro estourar o domínio do tipo de ponteiro, o resultado será definido pela implementação, mas nenhuma exceção será produzida.

23.6.7 Aritmética de ponteiro

Em um contexto não seguro, o operador (§12.10.5) e - o + operador (§12.10.6) podem ser aplicados a valores de todos os tipos de ponteiro, exceto void*. Assim, para cada tipo T*de ponteiro , os seguintes operadores são definidos implicitamente:

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 uma expressão P de um tipo T* de ponteiro e uma expressão N do tipo int, uint, long, ou ulong, as expressões P + N e N + P calculam o valor do ponteiro do tipo T* que resulta da adição N * sizeof(T) ao endereço fornecido por P. Da mesma forma, a expressão P – N calcula o valor do ponteiro do tipo T* que resulta da subtração N * sizeof(T) do endereço fornecido por P.

Dadas duas expressões, P e Q, de um tipo T*de ponteiro , a expressão P – Q calcula a diferença entre os endereços dados por P e Q e então divide essa diferença por sizeof(T). O tipo do resultado é sempre long. Com efeito, P - Q é calculado como ((long)(P) - (long)(Q)) / sizeof(T).

Exemplo:

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 produz a saída:

p - q = -14
q - p = 14

exemplo de fim

Se uma operação aritmética de ponteiro estourar o domínio do tipo de ponteiro, o resultado será truncado de maneira definida pela implementação, mas nenhuma exceção será produzida.

23.6.8 Comparação de ponteiros

Em um contexto não seguro, os ==operadores , !=, <, >, <=, e >= (§12.12) podem ser aplicados a valores de todos os tipos de ponteiro. Os operadores de comparação de ponteiro são:

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

Como existe uma conversão implícita de qualquer tipo de ponteiro para o void* tipo, os operandos de qualquer tipo de ponteiro podem ser comparados usando esses operadores. Os operadores de comparação comparam os endereços fornecidos pelos dois operandos como se fossem inteiros sem sinal.

23.6.9 O tamanho do operador

Para determinados tipos predefinidos (§12.8.19), o sizeof operador produz um valor constante int . Para todos os outros tipos, o sizeof resultado do operador é definido pela implementação e é classificado como um valor, não uma constante.

A ordem na qual os membros são empacotados em um struct não é especificada.

Para fins de alinhamento, pode haver preenchimento sem nome no início de um struct, dentro de um struct e no final do struct. O conteúdo dos bits usados como preenchimento é indeterminado.

Quando aplicado a um operando que tem o tipo struct, o resultado é o número total de bytes em uma variável desse tipo, incluindo qualquer preenchimento.

23.7 A declaração fixa

Em um contexto inseguro, a produção de embedded_statement (§13.1) permite uma construção adicional, a instrução fixed, que é usada para "consertar" uma variável móvel de modo que seu endereço permaneça constante durante a instrução.

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 uma variável local do pointer_type fornecido e inicializa essa variável local com o endereço calculado pelo fixed_pointer_initializer correspondente. Uma variável local declarada em uma instrução fixa pode ser acessada em qualquer fixed_pointer_initializerque ocorra à direita da declaração dessa variável e no embedded_statement da instrução fixa. Uma variável local declarada por uma instrução fixed é considerada somente leitura. Um erro de tempo de compilação ocorrerá se a instrução incorporada tentar modificar essa variável local (por meio de atribuição ou dos ++ operadores and -- ) ou passá-la como um parâmetro de referência ou saída.

É um erro usar uma variável local capturada (§12.19.6.2), parâmetro de valor ou matriz de parâmetros em um fixed_pointer_initializer. Um fixed_pointer_initializer pode ser um dos seguintes:

  • O token "&" seguido por um variable_reference (§9.5) para uma variável móvel (§23.4) de um tipo Tnão gerenciado, desde que o tipo T* seja implicitamente conversível para o tipo de ponteiro fornecido na fixed instrução. Nesse caso, o inicializador calcula o endereço da variável especificada e a variável tem a garantia de permanecer em um endereço fixo durante a instrução fixa.
  • Uma expressão de um array_type com elementos de um tipo Tnão gerenciado , desde que o tipo T* seja implicitamente conversível para o tipo de ponteiro fornecido na instrução fixed. Nesse caso, o inicializador calcula o endereço do primeiro elemento na matriz e toda a matriz tem a garantia de permanecer em um endereço fixo durante a fixed instrução. Se a expressão de matriz for null ou se a matriz tiver zero elementos, o inicializador calculará um endereço igual a zero.
  • Uma expressão do tipo string, desde que o tipo char* seja implicitamente conversível para o tipo de ponteiro fornecido na fixed instrução. Nesse caso, o inicializador calcula o endereço do primeiro caractere na cadeia de caracteres e toda a cadeia de caracteres tem a garantia de permanecer em um endereço fixo durante a fixed instrução. O comportamento da instrução será definido pela implementação se a expressão de cadeia de fixed caracteres for null.
  • Uma expressão do tipo diferente de array_type ou string, desde que exista um método acessível ou um método de extensão acessível que corresponda à assinatura ref [readonly] T GetPinnableReference(), em que T é um unmanaged_type e T* é implicitamente conversível para o tipo de ponteiro fornecido na fixed instrução. Nesse caso, o inicializador calcula o endereço da variável retornada e essa variável tem a garantia de permanecer em um endereço fixo durante a fixed instrução. Um GetPinnableReference() método pode ser usado pela instrução quando a fixed resolução de sobrecarga (§12.6.4) produz exatamente um membro da função e esse membro da função atende às condições anteriores. O GetPinnableReference método deve retornar uma referência a um endereço igual a zero, como o retornado de System.Runtime.CompilerServices.Unsafe.NullRef<T>() quando não há dados para fixar.
  • Um simple_name ou member_access que faz referência a um membro de buffer de tamanho fixo de uma variável móvel, desde que o tipo do membro de buffer de tamanho fixo seja implicitamente conversível para o tipo de ponteiro fornecido na fixed instrução. Nesse caso, o inicializador calcula um ponteiro para o primeiro elemento do buffer de tamanho fixo (§23.8.3) e o buffer de tamanho fixo tem a garantia de permanecer em um endereço fixo durante a fixed instrução.

Para cada endereço calculado por um fixed_pointer_initializer a fixed instrução garante que a variável referenciada pelo endereço não esteja sujeita a realocação ou descarte pelo coletor de lixo durante a fixed instrução.

Exemplo: se o endereço calculado por um fixed_pointer_initializer fizer referência a um campo de um objeto ou a um elemento de uma instância de matriz, a instrução fixed garantirá que a instância do objeto que a contém não seja realocada ou descartada durante o tempo de vida da instrução. exemplo de fim

É responsabilidade do programador garantir que os ponteiros criados por instruções fixas não sobrevivam além da execução dessas instruções.

Exemplo: quando ponteiros criados por fixed instruções são passados para APIs externas, é responsabilidade do programador garantir que as APIs não retenham memória desses ponteiros. exemplo de fim

Objetos fixos podem causar fragmentação do heap (porque eles não podem ser movidos). Por esse motivo, os objetos devem ser fixados apenas quando absolutamente necessário e apenas pelo menor tempo possível.

Exemplo: O exemplo

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

demonstra vários usos da fixed declaração. A primeira instrução corrige e obtém o endereço de um campo estático, a segunda instrução corrige e obtém o endereço de um campo de instância e a terceira instrução corrige e obtém o endereço de um elemento de matriz. Em cada caso, teria sido um erro usar o operador regular & , uma vez que as variáveis são todas classificadas como variáveis móveis.

A terceira e a quarta fixed afirmações no exemplo acima produzem resultados idênticos. Em geral, para uma instância ade array , especificar a[0] em uma fixed instrução é o mesmo que simplesmente especificar a.

exemplo de fim

Em um contexto inseguro, os elementos de matriz de matrizes unidimensionais são armazenados em ordem crescente de índice, começando com índice 0 e terminando com índice Length – 1. Para matrizes multidimensionais, os elementos da matriz são armazenados de forma que os índices da dimensão mais à direita sejam aumentados primeiro, depois a próxima dimensão à esquerda e assim por diante à esquerda.

Dentro de uma fixed instrução que obtém um ponteiro p para uma instância ade matriz, os valores do ponteiro variam de p para p + a.Length - 1 representar endereços dos elementos na matriz. Da mesma forma, as variáveis que variam de para p[a.Length - 1] representar os elementos reais da p[0] matriz. Dada a maneira como as matrizes são armazenadas, uma matriz de qualquer dimensão pode ser tratada como se fosse linear.

Exemplo:

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 produz a saída:

[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

exemplo de fim

Exemplo: no código a seguir

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

Uma fixed instrução é usada para corrigir uma matriz para que seu endereço possa ser passado para um método que usa um ponteiro.

exemplo de fim

Um char* valor produzido pela correção de uma instância de cadeia de caracteres sempre aponta para uma cadeia de caracteres terminada em nulo. Dentro de uma instrução fixa que obtém um ponteiro p para uma instância sde cadeia de caracteres, os valores de ponteiro que variam de para p + s.Length ‑ 1 representar endereços dos caracteres na cadeia de caracteres e o valor p + s.Length do p ponteiro sempre aponta para um caractere nulo (o caractere com valor '\0').

Exemplo:

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

exemplo de fim

Exemplo: o código a seguir mostra um fixed_pointer_initializer com uma expressão do tipo diferente de array_type ou 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)
        {
            // ...
        }
    }
}

Type C tem um método acessível GetPinnableReference com a assinatura correta. fixed Na instrução, o ref int retorno desse método quando ele é chamado é c usado para inicializar o int* ponteiro p. exemplo de fim

A modificação de objetos de tipo gerenciado por meio de ponteiros fixos pode resultar em um comportamento indefinido.

Observação: por exemplo, como as cadeias de caracteres são imutáveis, é responsabilidade do programador garantir que os caracteres referenciados por um ponteiro para uma cadeia de caracteres fixa não sejam modificados. nota final

Observação: a terminação nula automática de strings é particularmente conveniente ao chamar APIs externas que esperam strings "estilo C". Observe, no entanto, que uma instância de cadeia de caracteres tem permissão para conter caracteres nulos. Se esses caracteres nulos estiverem presentes, a cadeia de char*caracteres aparecerá truncada quando tratada como um . nota final

23.8 Buffers de tamanho fixo

23.8.1 Geral

Os buffers de tamanho fixo são usados para declarar matrizes em linha "estilo C" como membros de structs e são úteis principalmente para fazer interface com APIs não gerenciadas.

23.8.2 Declarações de buffer de tamanho fixo

Um buffer de tamanho fixo é um membro que representa o armazenamento de um buffer de comprimento fixo de variáveis de um determinado tipo. Uma declaração de buffer de tamanho fixo introduz um ou mais buffers de tamanho fixo de um determinado tipo de elemento.

Observação: como uma matriz, um buffer de tamanho fixo pode ser considerado como contendo elementos. Dessa forma, o termo tipo de elemento, conforme definido para uma matriz, também é usado com um buffer de tamanho fixo. nota final

Buffers de tamanho fixo só são permitidos em declarações struct e só podem ocorrer em contextos não 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 ']'
    ;

Uma declaração de buffer de tamanho fixo pode incluir um conjunto de atributos (§22), um new modificador (§15.3.5), modificadores de acessibilidade correspondentes a qualquer uma das acessibilidades declaradas permitidas para membros struct (§16.4.3) e um unsafe modificador (§23.2). Os atributos e modificadores se aplicam a todos os membros declarados pela declaração de buffer de tamanho fixo. É um erro para o mesmo modificador aparecer várias vezes em uma declaração de buffer de tamanho fixo.

Uma declaração de buffer de tamanho fixo não tem permissão para incluir o static modificador.

O tipo de elemento de buffer de uma declaração de buffer de tamanho fixo especifica o tipo de elemento do(s) buffer(s) introduzido(s) pela declaração. O tipo de elemento buffer deve ser um dos tipos sbytepredefinidos , byte, short, ushortint, ulongfloatlongcharuint, ou . doublebool

O tipo de elemento de buffer é seguido por uma lista de declaradores de buffer de tamanho fixo, cada um dos quais introduz um novo membro. Um declarador de buffer de tamanho fixo consiste em um identificador que nomeia o membro, seguido por uma expressão constante entre [ tokens e ] . A expressão constante denota o número de elementos no membro introduzido por esse declarador de buffer de tamanho fixo. O tipo da expressão constante deve ser implicitamente conversível em tipo int, e o valor deve ser um inteiro positivo diferente de zero.

Os elementos de um buffer de tamanho fixo devem ser dispostos sequencialmente na memória.

Uma declaração de buffer de tamanho fixo que declara vários buffers de tamanho fixo é equivalente a várias declarações de uma única declaração de buffer de tamanho fixo com os mesmos atributos e tipos de elemento.

Exemplo:

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

é equivalente a

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

exemplo de fim

23.8.3 Buffers de tamanho fixo em expressões

A pesquisa de membro (§12.5) de um membro do buffer de tamanho fixo prossegue exatamente como a pesquisa de membro de um campo.

Um buffer de tamanho fixo pode ser referenciado em uma expressão usando um simple_name (§12.8.4), um member_access (§12.8.7) ou um element_access (§12.8.12).

Quando um membro do buffer de tamanho fixo é referenciado como um nome simples, o efeito é o mesmo que um acesso de membro do formulário this.I, onde I é o membro do buffer de tamanho fixo.

Em um acesso de membro do formulário E.I onde E. pode ser o implícito this., se E for de um tipo de struct e uma pesquisa de membro nesse tipo de I struct identificar um membro de tamanho fixo, será E.I avaliado e classificado da seguinte maneira:

  • Se a expressão E.I não ocorrer em um contexto não seguro, ocorrerá um erro em tempo de compilação.
  • Se E for classificado como um valor, ocorrerá um erro em tempo de compilação.
  • Caso contrário, se E é uma variável móvel (§23.4) então:
    • Se a expressão E.I for um fixed_pointer_initializer (§23.7), o resultado da expressão será um ponteiro para o primeiro elemento do membro I do buffer de tamanho fixo em E.
    • Caso contrário, se a expressão E.I for um primary_no_array_creation_expression (§12.8.12.1) dentro de um element_access (§12.8.12) do formulário E.I[J], o resultado de E.I é um ponteiro, P, para o primeiro elemento do membro I do buffer de tamanho fixo em E, e o element_access delimitador é avaliado como o pointer_element_access (§23.6.4) P[J].
    • Caso contrário, ocorrerá um erro em tempo de compilação.
  • Caso contrário, E faz referência a uma variável fixa e o resultado da expressão é um ponteiro para o primeiro elemento do membro I do buffer de tamanho fixo em E. O resultado é do tipo S*, onde S é o tipo de elemento de I, e é classificado como um valor.

Os elementos subsequentes do buffer de tamanho fixo podem ser acessados usando operações de ponteiro do primeiro elemento. Ao contrário do acesso a matrizes, o acesso aos elementos de um buffer de tamanho fixo é uma operação insegura e não é verificada por intervalo.

Exemplo: o seguinte declara e usa um struct com um membro de buffer de tamanho fixo.

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

exemplo de fim

23.8.4 Verificação de atribuição definida

Os buffers de tamanho fixo não estão sujeitos à verificação de atribuição definida (§9.4) e os membros do buffer de tamanho fixo são ignorados para fins de verificação de atribuição definida de variáveis de tipo struct.

Quando a variável struct que contém mais externa de um membro de buffer de tamanho fixo é uma variável estática, uma variável de instância de uma instância de classe ou um elemento de matriz, os elementos do buffer de tamanho fixo são inicializados automaticamente para seus valores padrão (§9.3). Em todos os outros casos, o conteúdo inicial de um buffer de tamanho fixo é indefinido.

23.9 Alocação de pilha

Consulte §12.8.22 para obter informações gerais sobre o operador stackalloc. Aqui, a capacidade desse operador de resultar em um ponteiro é discutida.

Em um contexto não seguro, se um stackalloc_expression (§12.8.22) ocorrer como a expressão de inicialização de um local_variable_declaration (§13.6.2), em que o local_variable_type é um tipo de ponteiro (§23.3) ou inferido (var), o resultado do stackalloc_expression é um ponteiro do tipo T * para ser o início do bloco alocado, onde T é o unmanaged_type do stackalloc_expression.

Em todos os outros aspectos, a semântica de local_variable_declaration s (§13.6.2) e stackalloc_expressions (§12.8.22) em contextos inseguros segue aquelas definidas paracontextos seguros.

Exemplo:

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

exemplo de fim

Ao contrário do Span<T> acesso a arrays ou stackallocblocos do tipo 'ed, o acesso aos elementos de um stackallocbloco 'ed do tipo ponteiro é uma operação insegura e não é verificado por intervalo.

Exemplo: no código a seguir

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

Uma stackalloc expressão é usada no IntToString método para alocar um buffer de 16 caracteres na pilha. O buffer é descartado automaticamente quando o método retorna.

Observe, no entanto, que IntToString isso pode ser reescrito no modo de segurança; ou seja, sem usar ponteiros, da seguinte maneira:

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

exemplo de fim

Fim do texto condicionalmente normativo.