Partilhar via


23 Código inseguro

23.1 Generalidades

Uma implementação que não suporta 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.

Nota: A linguagem C# principal, conforme definida 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 que são 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 "pendurado" ou uma expressão que indexa uma matriz além de seus limites. Categorias inteiras de bugs que rotineiramente atormentam programas C e C++ são assim eliminadas.

Embora praticamente todas as construções de tipo de ponteiro em C ou C++ tenham uma contrapartida de tipo de referência em C#, no entanto, 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 crítico em termos de tempo 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, tomar o endereço de variáveis, e assim por diante. Em certo sentido, escrever código inseguro é muito parecido com escrever código C dentro de um programa C#.

Código inseguro é, na verdade, um recurso "seguro" da perspetiva 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 trabalha 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 pela inclusão de 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, caso em que toda a extensão textual dessa declaração de tipo (incluindo o corpo da classe, struct ou interface) é considerada um contexto inseguro.

    Nota: Se o type_declaration for parcial, apenas essa parte é 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, caso em que toda a extensão textual dessa declaração de membro é considerada um contexto inseguro.
  • Um unsafe_statement permite o uso de um contexto inseguro dentro de um bloco. Toda a extensão textual do bloco associado é considerada um contexto inseguro. Uma função local declarada dentro de um contexto inseguro é, em si mesma, insegura.

As extensões gramaticais associadas são mostradas abaixo e em 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 poderia 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 final

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 um 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 se F torne um contexto inseguro no qual os recursos inseguros da linguagem podem ser usados. Na substituição de in F, não há necessidade de B reespecificar o unsafe modificador — a menos, é claro, que o F método em B si precise de acesso a recursos inseguros.

A situação é ligeiramente 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 do inclui um tipo de ponteiro, ela só pode ser escrita 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 de método, como é o caso em B.

Exemplo final

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

23.3 Tipos de ponteiro

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

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

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

O tipo especificado antes do * em um tipo de ponteiro é chamado de tipo referente 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 utilizado num array_type num contexto não seguro (§23.2). Um non_array_type é qualquer tipo que não seja em si um array_type.

Ao contrário das referências (valores dos 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 esta razão, um ponteiro não tem permissão para apontar para uma referência ou para uma struct que contenha referências, e o tipo referente 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 referente para outro tipo de ponteiro.

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

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

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

Exemplo final

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

Nota: Ao contrário de C e C++, quando vários ponteiros são declarados na mesma declaração, em C# o * é escrito apenas com o tipo subjacente, não como um pontuador 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 final

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

O void* tipo representa um ponteiro para um tipo desconhecido. Como o tipo referente é desconhecido, o operador indirection 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 para qualquer outro tipo de ponteiro (e vice-versa) e comparado com 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 objecto . Em particular, boxe e unboxing (§8.3.13) não são suportados para ponteiros. No entanto, são permitidas conversões entre diferentes tipos de ponteiro e entre tipos de ponteiro e os tipos integrais. Isto é 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érico que teriam inferido um argumento de tipo como sendo um tipo de ponteiro.

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

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

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

A eliminação dinâmica de um tipo E* é o tipo de ponteiro com o tipo referente 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 de referência, isso pode causar um comportamento indefinido, uma vez que o ponteiro pode muito bem ser definido 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 retornará 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 final

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 utilizado para aceder a um membro de uma estrutura através de um ponteiro (§23.6.3).
  • O [] operador pode ser utilizado para indexar um ponteiro (§23.6.4).
  • O operador unário & pode ser utilizado para obter o endereço de uma variável (§23.6.5).
  • Os ++ operadores e -- podem ser utilizados para aumentar e diminuir os ponteiros (§23.6.6).
  • O binário e + os operadores podem ser usados para executar a aritmética de - ponteiros (§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 declaração pode ser usada para fixar temporariamente uma variável para que seu endereço possa ser obtido (§23.7).

23.4 Variáveis fixas e móveis

O endereço do operador (§23.6.5) e a declaração (fixed) dividem as 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 desreferenciação.) Por outro lado, as variáveis móveis residem em locais de armazenamento 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 obter o endereço de uma variável fixa sem restrições. No entanto, uma vez que uma variável móvel está sujeita a deslocalização ou eliminação pelo coletor de lixo, o endereço de uma variável móvel só pode ser obtido utilizando um fixed statement (§23.7), e esse endereço permanece válido apenas durante a vigência dessa fixed declaraçã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 Generalidades

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

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

Além disso, em um contexto inseguro, 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 outra pointer_type.
  • De sbyte, byte, short, , ushortint, uint, long, ou ulong para qualquer pointer_type.
  • De qualquer pointer_type a sbyte, byte, short, ushort, int, uint, long, ou ulong.

Finalmente, em um contexto inseguro, 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 "corretamente alinhado" é transitivo: se um ponteiro para digitar A está alinhado corretamente para um ponteiro digitar B, que, por sua vez, está alinhado corretamente para um ponteiro digitar C, então um ponteiro para digitar A é alinhado corretamente para um ponteiro digitar C.

Exemplo: Considere o seguinte caso em que uma variável com um tipo é acessada através 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 final

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, produzem ponteiros para os bytes restantes dessa variável.

Exemplo: O método a seguir exibe cada um dos oito bytes em um 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 que a produção depende da endianidade. Uma possibilidade é " BA FF 51 A2 90 6C 24 45".

Exemplo final

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

Nota: No entanto, em arquiteturas de CPU de 32 e 64 bits com um espaço de endereçamento 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 ponteiro

Matrizes de ponteiros podem ser construídas usando array_creation_expression (§12.8.17.5) em um contexto inseguro. Apenas 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 implementa também se aplica a matrizes de ponteiro. No entanto, qualquer tentativa de acessar os elementos da matriz através System.Array ou as interfaces que implementa pode resultar em uma exceção em tempo de execução, já que os tipos de ponteiro não são conversíveis 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 de base genéricas nunca se aplicam a matrizes de ponteiro.
  • A conversão de referência explícita (§10.3.5) de e as interfaces que implementa para qualquer System.Array aplica-se a matrizes de ponteiro.
  • As conversões de referência explícitas (§10.3.5) de e suas interfaces base para um tipo System.Collections.Generic.IList<S> de matriz unidimensional nunca se aplicam a matrizes de ponteiro, uma vez que os tipos de T[] ponteiro não podem ser usados como argumentos de tipo e não há conversões de tipos de ponteiro para tipos sem ponteiro.

Essas restrições significam que a expansão da foreach instrução sobre matrizes descrita no §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 é um tipo de matriz do formulário x, T[,,...,] é o número de dimensões menos 1 e ou T é um tipo de V ponteiro, é expandido usando loops for-loops 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 xembedded_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 é produzido e nenhuma outra etapa é tomada. Se x tiver o valor null, a System.NullReferenceException é lançado em tempo de execução.

Nota: Embora os tipos de ponteiro não sejam permitidos como argumentos de tipo, matrizes de ponteiro podem ser usadas como argumentos de tipo. Nota final

23.6 Ponteiros em expressões

23.6.1 Generalidades

Em um contexto inseguro, uma expressão pode produzir um resultado de um tipo de ponteiro, mas fora de um contexto inseguro, é um erro em tempo de compilação para uma expressão ser de um tipo de ponteiro. Em termos precisos, fora de um contexto inseguro, ocorre 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 do tipo ponteiro.

Num 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 seguintes.

Nota: A precedência e associatividade dos operadores inseguros está implícita na gramática. Nota final

23.6.2 Indirection do ponteiro

Um pointer_indirection_expression consiste num asterisco (*) seguido de um unary_expression.

pointer_indirection_expression
    : '*' unary_expression
    ;

O operador unário * denota indirection 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 em 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 valorizado é definido pela implementação. Em particular, não há garantia de que esta operação lance um System.NullReferenceException.

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

Nota: Entre os valores inválidos para desreferenciar um ponteiro pelo operador unário * estão um endereço alinhado inadequadamente para o tipo apontado (ver exemplo no §23.5) e o endereço de uma variável após o fim de sua vida útil.

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 de 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?
    ;

Num membro do ponteiro, o acesso ao formulário P->Ideve P ser uma expressão de um tipo de ponteiro e I deve indicar um membro acessível do tipo ao qual P os pontos.

Um acesso de membro de ponteiro do formulário P->I é avaliado exatamente como (*P).I. Para uma descrição do operador de indirection do ponteiro (*), ver §23.6.2. Para uma descrição do operador de acesso de membro (.), ver §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 uma struct através 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 final

23.6.4 Acesso ao elemento de ponteiro

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

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

Num elemento de ponteiro, o acesso ao 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.

O acesso de um elemento de ponteiro do formulário P[E] é avaliado exatamente como *(P + E). Para uma descrição do operador de indirection do ponteiro (*), ver §23.6.2. Para uma descrição do operador de adição de ponteiro (+), ver §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 final

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: Isto é o mesmo que C e C++. Nota final

23.6.5 Endereço do operador

Um addressof_expression consiste em um E comercial (&) seguido de 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), o constructo &E calcula o endereço da variável dado por E. O tipo do resultado é T* e é classificado como um valor. Um erro em 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 declaração fixa (§23.7) pode ser usada para "corrigir" temporariamente a variável antes de obter seu endereço.

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

O & operador não requer 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 po . A atribuição para *p em vigor inicializa i, mas a inclusão dessa inicialização é de responsabilidade do programador, e nenhum erro em tempo de compilação ocorreria se a atribuição fosse removida.

Exemplo final

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

Nota: Quando uma variável local, um parâmetro de valor ou uma matriz de parâmetros é capturada por uma função anónima (§12.8.24), essa variável, parâmetro ou matriz de parâmetros local deixa de ser considerada uma variável fixa (§23.7), passando a ser considerada uma variável móvel. Assim, é um erro para qualquer código não seguro tomar o endereço de uma variável local, parâmetro de valor ou matriz de parâmetros que foi capturado por uma função anônima. Nota final

23.6.6 Incremento e decréscimo do ponteiro

Em um contexto inseguro, os ++ operadores e -- (§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 implicitamente definidos:

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

Os operadores produzem os mesmos resultados que x+1 e x-1, respetivamente (§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 decréscimo 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 dos ponteiros

Em um contexto inseguro, o operador (+) 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 implicitamente definidos:

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 calcular o valor de 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 de ponteiro do tipo T* que resulta da subtração N * sizeof(T) do endereço fornecido por P.

Dadas duas expressões, e P, de um tipo Qde ponteiro , a expressão T* calcula a diferença entre os endereços dados por P – Q e P e, em seguida, Q divide essa diferença por sizeof(T). O tipo de 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 produção:

p - q = -14
q - p = 14

Exemplo final

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

23.6.8 Comparação de ponteiros

Em um contexto inseguro, 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, 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 não assinados.

23.6.9 A dimensão do operador

Para certos 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 uma estrutura não é especificada.

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

Quando aplicado a um operando que tenha 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 fixa, que é usada para "fixar" uma variável móvel de tal forma que seu endereço permaneça constante durante a duração da declaraçã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 dado 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 é acessível em qualquer fixed_pointer_initializerque ocorra à direita da declaração dessa variável e na embedded_statement da instrução fixa. Uma variável local declarada por uma instrução fixa é considerada somente leitura. Um erro em tempo de compilação ocorre se a instrução incorporada tentar modificar essa variável local (via atribuição ou os ++ 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 de 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 dado na fixed instrução. Nesse caso, o inicializador calcula o endereço da variável dada, e a variável tem a garantia de permanecer em um endereço fixo durante a duração da 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 fixa. 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 duração da fixed instrução. Se a expressão da matriz for null ou se a matriz tiver elementos zero, 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 duração da fixed instrução. O comportamento da instrução é definido pela implementação se a expressão de cadeia de fixed caracteres for null.
  • Uma expressão de tipo diferente de array_type ou , desde que exista um método acessível ou um método de extensão acessível que corresponda à assinatura string, onde ref [readonly] T GetPinnableReference() é um T e é implicitamente convertível para o tipo de ponteiro fornecido na T* instruçãofixed. 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 duração da 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 satisfaz as condições anteriores. O GetPinnableReference método deve retornar uma referência a um endereço igual a zero, como aquele 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 duração da fixed instrução.

Para cada endereço calculado por um fixed_pointer_initializer a fixed declaração garante que a variável referenciada pelo endereço não está sujeita a realocação ou descarte pelo coletor de lixo durante a vigência da fixed declaraçã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 fixa garante que a instância de objeto que contém não seja realocada ou descartada durante o tempo de vida da instrução. Exemplo final

É 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 final

Objetos fixos podem causar fragmentação da pilha (porque eles não podem ser movidos). Por essa razão, os objetos devem ser fixados apenas quando absolutamente necessário e, em seguida, apenas pelo menor período de 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 quarta fixed afirmações do exemplo acima produzem resultados idênticos. Em geral, para uma instância ade matriz, especificar a[0] em uma fixed instrução é o mesmo que simplesmente especificar a.

Exemplo final

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

Dentro de uma fixed instrução que obtém um ponteiro p para uma instância ade matriz, os valores de ponteiro variam de p para p + a.Length - 1 representar endereços dos elementos na matriz. Da mesma forma, as variáveis variam de para p[0] representar os elementos reais da p[a.Length - 1] matriz. Dada a forma 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 produção:

[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 final

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 final

Um char* valor produzido pela fixação de uma ocorrê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 ocorrência sde cadeia de caracteres , os valores de ponteiro variam de para p representar endereços dos caracteres na cadeia de p + s.Length ‑ 1 caracteres e o valor p + s.Length do 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 final

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 retornado desse método quando ele é chamado c é usado para inicializar o int* ponteiro p. Exemplo final

Modificar objetos de tipo gerenciado por meio de ponteiros fixos pode resultar em comportamento indefinido.

Nota: 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

Nota: A terminação nula automática de cadeias de caracteres é particularmente conveniente ao chamar APIs externas que esperam cadeias de caracteres "estilo C". Observe, no entanto, que uma ocorrência de cadeia de caracteres tem permissão para conter caracteres nulos. Se esses caracteres nulos estiverem presentes, a cadeia de caracteres aparecerá truncada quando tratada como terminada char*em nulo. Nota final

23.8 Buffers de tamanho fixo

23.8.1 Generalidades

Os buffers de tamanho fixo são usados para declarar matrizes em linha "estilo C" como membros de structs e são principalmente úteis para 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 para 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.

Nota: Como uma matriz, um buffer de tamanho fixo pode ser considerado como contendo elementos. Como tal, o termo tipo de elemento, conforme definido para uma matriz, também é usado com um buffer de tamanho fixo. Nota final

Os 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 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 tampão deve ser um dos tipos sbytepredefinidos , byte, short, ushort, int, uint, long, ulong, charfloatdoubleou bool.

O tipo de elemento 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 incluída e [] tokens. A expressão constante denota o número de elementos no membro introduzidos por esse declarador de buffer de tamanho fixo. O tipo de expressão constante deve ser implicitamente convertível em tipo int, e o valor deve ser um número inteiro positivo diferente de zero.

Os elementos de uma memória intermédia 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 elementos.

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 final

23.8.3 Buffers de tamanho fixo em expressões

A pesquisa de membros (§12.5) de um membro de buffer de tamanho fixo prossegue exatamente como a pesquisa de membros 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 de 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 de buffer de tamanho fixo.

Em um acesso de membro do formulário E.I onde E. pode ser o implícito this., se E é de um tipo struct e uma pesquisa de membro desse tipo struct identifica um membro de I tamanho fixo, então E.I é avaliado e classificado da seguinte forma:

  • Se a expressão E.I não ocorrer em um contexto inseguro, 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 for 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], então 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 anexador é então 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 é verificado 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 final

23.8.4 Verificação de atribuição definitiva

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 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 automaticamente inicializados 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

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

Quando um stackalloc_expression ocorre como a expressão de inicialização de um local_variable_declaration (§13.6.2), onde o local_variable_type é um tipo de ponteiro (§23.3) ou é inferido (var), o resultado do stackalloc_expression é um ponteiro de tipo T*, onde T é o unmanaged_type do stackalloc_expression. Neste caso, o resultado é um ponteiro para o início do bloco alocado.

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 final

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

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 é automaticamente descartado quando o método retorna.

Note, no entanto, que IntToString pode ser reescrito no modo de segurança, ou seja, sem usar ponteiros, da seguinte forma:

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 final

Fim do texto condicionalmente normativo.