Melhorias na estrutura de baixo nível
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Resumo
Esta proposta é um conjunto de várias propostas diferentes para struct
melhorias de desempenho: campos ref
e a capacidade de substituir as predefinições de ciclo de vida. O objetivo é um projeto que considera as várias propostas para criar um único conjunto de recursos integrados, visando melhorias em struct
de baixa complexidade.
Nota: As versões anteriores desta especificação usavam os termos "ref-safe-to-escape" e "safe-to-escape", que foram introduzidos na especificação de recursos de segurança
Span. O comité padrão ECMA alterou os nomes para "ref-safe-context" e "safe-context", respectivamente. Os valores do contexto seguro foram refinados para usar o "declaration-block", o "function-member" e o "caller-context" de forma consistente. Os speclets usaram formulações diferentes para estas expressões, e também empregaram "safe-to-return" como sinônimo de "caller-context". Este speclet foi atualizado para usar os termos no padrão C# 7.3.
Nem todos os recursos descritos neste documento foram implementados no C# 11. O C# 11 inclui:
-
ref
campos escoped
[UnscopedRef]
Esses recursos são propostas abertas para uma versão futura do C#.
-
ref
campos pararef struct
- Tipos restritos do pôr-do-sol
Motivação
Versões anteriores do C# adicionaram uma série de recursos de desempenho de baixo nível para a linguagem: retornos de ref
, ref struct
, ponteiros de função, etc. ... Isso permitiu que os desenvolvedores do .NET escrevessem código de alto desempenho enquanto continuavam a aproveitar as regras da linguagem C# para segurança de tipo e memória. Ele também permitiu a criação de tipos de desempenho fundamentais nas bibliotecas .NET, como Span<T>
.
À medida que esses recursos ganharam força no ecossistema .NET, os desenvolvedores internos e externos, têm nos fornecido informações sobre os pontos de atrito restantes no ecossistema. Lugares onde ainda precisam reverter para o código unsafe
para fazer o seu trabalho, ou exigem que o tempo de execução trate tipos de casos especiais como Span<T>
.
Hoje, Span<T>
é realizado usando internal
do tipo ByReference<T>
, que o tempo de execução efetivamente trata como um campo ref
. Isso oferece o benefício dos campos de ref
, mas com a desvantagem de que a linguagem não faculta verificação de segurança para esses, como faz para outros usos de ref
. Além disso, apenas dotnet/runtime pode usar este tipo, pois é internal
, então os terceiros não podem projetar os seus próprios primitivos com base em campos ref
. Parte da motivação para este trabalho é remover ByReference<T>
e usar campos de ref
adequados em todas as bases de código.
A presente proposta pretende abordar estas questões com base nas nossas características de baixo nível existentes. Especificamente visa:
- Permitir que tipos
ref struct
declarem camposref
. - Permita que o tempo de execução defina totalmente
Span<T>
usando o sistema de tipos do C# e remova o tipo de caso especial, tal comoByReference<T>
- Permitir que
struct
tipos retornemref
para seus campos. - Permitir que o tempo de execução remova usos
unsafe
causados por limitações de padrões vitalícios - Permitir a declaração de buffers de
fixed
seguros para tipos gerenciados e não gerenciados nostruct
Projeto de Execução
As regras de segurança ref struct
estão definidas no documento de segurança span utilizando os termos anteriores. Essas regras foram incorporadas ao padrão C# 7 no §9.7.2 e §16.4.12. O presente documento descreverá as alterações necessárias à presente proposta. Uma vez aceites como funcionalidade aprovada, estas alterações serão incorporadas nesse documento.
Uma vez concluído este projeto, a nossa definição de Span<T>
será a seguinte:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
// This constructor does not exist today but will be added as a part
// of changing Span<T> to have ref fields. It is a convenient, and
// safe, way to create a length one span over a stack value that today
// requires unsafe code.
public Span(ref T value)
{
_field = ref value;
_length = 1;
}
}
Fornecer campos de referência e de âmbito
A linguagem permitirá que os desenvolvedores declarem campos ref
dentro de um ref struct
. Isso pode ser útil, por exemplo, ao encapsular grandes instâncias de struct
mutáveis ou ao definir tipos de alto desempenho, como Span<T>
em bibliotecas além do tempo de execução.
ref struct S
{
public ref int Value;
}
Um campo ref
será emitido em metadados usando a assinatura ELEMENT_TYPE_BYREF
. Isso não é diferente de como emitimos variáveis locais ref
ou argumentos ref
. Por exemplo, ref int _field
será emitido como ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. Isso exigirá que atualizemos ECMA335 para permitir essa entrada, mas isso deve ser bastante simples.
Os desenvolvedores podem continuar a inicializar um ref struct
com um campo ref
usando a expressão default
, caso em que todos os campos ref
declarados terão o valor null
. Qualquer tentativa de usar esses campos resultará em um NullReferenceException
sendo lançado.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Enquanto a linguagem C# finge que um ref
não pode ser null
isso é legal no nível de tempo de execução e tem semântica bem definida. Os desenvolvedores que introduzem campos ref
em seus tipos precisam estar cientes dessa possibilidade e devem ser fortemente desencorajados de vazar esse detalhe para consumir código. Em vez disso, os campos
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Um campo ref
pode ser combinado com os modificadores readonly
das seguintes maneiras:
-
readonly ref
: Este é um campo que não pode ser ref reatribuído fora de um construtor ouinit
métodos. Pode ser atribuído valor mesmo fora desses contextos. -
ref readonly
: este é um campo que pode ser reatribuído como referência, mas nunca pode ter um valor atribuído. Desta forma, um parâmetroin
pode ser reatribuído ref a um camporef
. -
readonly ref readonly
: uma combinação deref readonly
ereadonly ref
.
ref struct ReadOnlyExample
{
ref readonly int Field1;
readonly ref int Field2;
readonly ref readonly int Field3;
void Uses(int[] array)
{
Field1 = ref array[0]; // Okay
Field1 = array[0]; // Error: can't assign ref readonly value (value is readonly)
Field2 = ref array[0]; // Error: can't repoint readonly ref
Field2 = array[0]; // Okay
Field3 = ref array[0]; // Error: can't repoint readonly ref
Field3 = array[0]; // Error: can't assign ref readonly value (value is readonly)
}
}
Será necessário que um readonly ref struct
exija que os campos ref
sejam declarados como readonly ref
. Não é necessário que sejam declarados readonly ref readonly
. Isso permite que um readonly struct
sofra mutações indiretas através de tal campo, mas isso não se difere de um campo readonly
que atualmente aponta para um tipo de referência (mais detalhes)
Um readonly ref
será emitido para metadados usando o sinalizador initonly
, igual a qualquer outro campo. Um campo ref readonly
será atribuído a System.Runtime.CompilerServices.IsReadOnlyAttribute
. Um readonly ref readonly
será emitido com ambos os itens.
Esse recurso requer suporte de tempo de execução e alterações na especificação ECMA. Como tal, estes só serão ativados quando o sinalizador de recurso correspondente estiver definido em corelib. O problema em rastrear a API exata é monitorado aqui https://github.com/dotnet/runtime/issues/64165
O conjunto de alterações às nossas regras de contexto seguro necessárias para permitir campos ref
é pequeno e específico. As regras já contabilizam ref
campos existentes e que estão sendo consumidos a partir de APIs. As alterações devem centrar-se apenas em dois aspetos: a forma como são criadas e como são reatribuídas.
Primeiro, as regras que estabelecem valores de ref-safe-context para campos precisam ser atualizadas para ref
campos da seguinte maneira:
Uma expressão na forma
ref e.F
ref-safe-context da seguinte forma:
- Se
F
é um camporef
, o seu ref-safe-context é o de contexto seguro dee
.- Caso contrário, se
e
for de um tipo de referência, ele terá ref-safe-context de de contexto do chamador- Caso contrário, o seu ref-safe-context é retirado do ref-safe-context de
e
.
Isso não representa uma mudança de regra, pois as regras sempre levaram em conta a existência do estado ref
dentro de um ref struct
. De facto, é assim que o estado ref
em Span<T>
sempre funcionou e as regras de consumo explicam corretamente isso. A alteração visa permitir que os desenvolvedores acedam diretamente aos campos ref
e garantir que o façam seguindo as mesmas regras existentes implicitamente aplicadas a Span<T>
.
Isso significa, no entanto, que ref
campos podem ser retornados como ref
de um ref struct
mas os campos normais não podem.
ref struct RS
{
ref int _refField;
int _field;
// Okay: this falls into bullet one above.
public ref int Prop1 => ref _refField;
// Error: This is bullet four above and the ref-safe-context of `this`
// in a `struct` is function-member.
public ref int Prop2 => ref _field;
}
Isso pode parecer um erro à primeira vista, mas este é um ponto de design deliberado. Mais uma vez, porém, esta não é uma nova regra que está a ser criada por esta proposta, mas sim reconhecer as regras existentes Span<T>
comportadas até agora que os desenvolvedores podem declarar seu próprio estado ref
.
Em seguida, as regras para reatribuição de ref precisam ser ajustadas para a presença de ref
campos. O principal cenário para a reatribuição de ref ocorre quando os construtores ref struct
armazenam os parâmetros ref
nos campos ref
. O apoio será mais geral, mas este é o cenário central. Para dar suporte a isso, as regras para reatribuição de ref serão ajustadas para considerar os campos ref
da seguinte forma:
Regras de reatribuição de referências
O operando esquerdo do operador = ref
deve ser uma expressão que se liga a uma variável local ref, um parâmetro ref (diferente de this
), um parâmetro out, ou um campo ref.
Para uma reatribuição de ref na forma
e1 = ref e2
ambas as seguintes condições devem ser verdadeiras:
e2
deve ter o ref-safe-context pelo menos tão grande quanto o ref-safe-context dee1
e1
deve ter o mesmo contexto seguro quee2
Nota
Isso significa que o construtor de Span<T>
desejado funciona sem qualquer anotação extra:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
public Span(ref T value)
{
// Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The
// safe-context of `this` is *return-only* and ref-safe-context of `value` is
// *caller-context* hence this is legal.
_field = ref value;
_length = 1;
}
}
A alteração das regras de reatribuição de ref significa que os parâmetros ref
agora podem escapar de um método como um campo ref
em um valor ref struct
. Conforme discutido na seção sobre as considerações de compatibilidade, isso pode alterar as regras para APIs existentes que nunca tiveram a intenção de que os parâmetros ref
fossem expostos como um campo ref
. As regras de tempo de vida para os parâmetros baseiam-se apenas na sua declaração e não na sua utilização. Todos os parâmetros ref
e in
têm ref-safe-context de de contexto do chamador e, portanto, agora podem ser retornados por ref
ou um campo ref
. Para dar suporte a APIs com parâmetros ref
que podem escapar ou não escapar e, assim, restaurar a semântica de chamada do C# 10, a linguagem introduzirá anotações de tempo de vida limitado.
scoped
modificador
A palavra-chave scoped
será usada para restringir o tempo de vida de um valor. Ele pode ser aplicado a um
Parâmetro ou Local | ref-safe-context | contexto seguro |
---|---|---|
Span<int> s |
membro de função | contexto do chamador |
scoped Span<int> s |
membro de função | membro de função |
ref Span<int> s |
contexto do chamador | contexto do chamador |
scoped ref Span<int> s |
membro de função | contexto do chamador |
Nesta relação, o contexto de referência de segurança de um valor nunca pode ser mais amplo que o contexto seguro .
Isso permite que as APIs em C# 11 sejam anotadas de modo que tenham as mesmas regras que o C# 10:
Span<int> CreateSpan(scoped ref int parameter)
{
// Just as with C# 10, the implementation of this method isn't relevant to callers.
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 and legal in C# 11 due to scoped ref
return CreateSpan(ref parameter);
// Legal in C# 10 and legal in C# 11 due to scoped ref
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 and legal in C# 11 due to scoped ref
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
A anotação scoped
também significa que o parâmetro this
de um struct
agora pode ser definido como scoped ref T
. Anteriormente, tinha que ser tratado como caso especial nas regras como parâmetro ref
, que tinha diferentes regras de ref-safe-context em comparação com outros parâmetros ref
(veja todas as referências para incluir ou excluir o destinatário nas regras de contexto seguro). Agora, pode ser expresso como um conceito geral em todas as regras, o que as simplifica ainda mais.
A anotação scoped
também pode ser aplicada aos seguintes locais:
- locais: Esta anotação define o tempo de vida como de contexto seguro, ou de contexto ref-safe, no caso de um
ref
local, para um de membro da função, independentemente do tempo de vida do inicializador.
Span<int> ScopedLocalExamples()
{
// Error: `span` has a safe-context of *function-member*. That is true even though the
// initializer has a safe-context of *caller-context*. The annotation overrides the
// initializer
scoped Span<int> span = default;
return span;
// Okay: the initializer has safe-context of *caller-context* hence so does `span2`
// and the return is legal.
Span<int> span2 = default;
return span2;
// The declarations of `span3` and `span4` are functionally identical because the
// initializer has a safe-context of *function-member* meaning the `scoped` annotation
// is effectively implied on `span3`
Span<int> span3 = stackalloc int[42];
scoped Span<int> span4 = stackalloc int[42];
}
Outros usos para scoped
em locais são discutidos abaixo.
A anotação scoped
não pode ser aplicada a qualquer outro local, incluindo retornos, campos, elementos de matriz, etc ... Além disso, embora scoped
tenha impacto quando aplicado a qualquer ref
, in
ou out
só tem impacto quando aplicado a valores que são ref struct
. Declarações como scoped int
não têm impacto, porque algo que não seja ref struct
é sempre seguro para devolver. O compilador criará um diagnóstico para esses casos para evitar confusão do desenvolvedor.
Alterar o comportamento dos parâmetros out
Para limitar ainda mais o impacto da alteração de compat de tornar os parâmetros out
parâmetros serão implicitamente scoped out
a partir de agora. Do ponto de vista da compatibilidade, isto significa que não podem ser processados por ref
:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Isso aumentará a flexibilidade das APIs que retornam valores de ref struct
e têm parâmetros out
porque não precisa mais considerar o parâmetro que está sendo capturado por referência. Isso é importante porque é um padrão comum em APIs de estilo leitor:
Span<byte> Read(Span<byte> buffer, out int read)
{
// ..
}
Span<byte> Use()
{
var buffer = new byte[256];
// If we keep current `out` ref-safe-context this is an error. The language must consider
// the `read` parameter as returnable as a `ref` field
//
// If we change `out` ref-safe-context this is legal. The language does not consider the
// `read` parameter to be returnable hence this is safe
int read;
return Read(buffer, out read);
}
A linguagem também não considerará mais que os argumentos passados para um parâmetro out
sejam retornáveis. Tratar a entrada de um parâmetro out
como retornável foi extremamente confuso para os desenvolvedores. Isso essencialmente subverte a intenção de out
, forçando os desenvolvedores a considerar o valor passado pelo chamador, que nunca é usado, exceto em linguagens que não respeitam out
. No futuro, os idiomas que suportam ref struct
devem garantir que o valor original passado para um parâmetro out
nunca seja lido.
O C# consegue isso por meio de suas regras de atribuição definidas. Isso tanto alcança as nossas regras de contexto seguro de referência, quanto permite que o código existente atribua e depois retorne valores de parâmetros out
.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Juntas, essas alterações significam que o argumento para um parâmetro out
não contribui com valores de contexto-seguro ou de contexto-de-referência-seguro para invocações de método. Isso reduz significativamente o impacto geral na compatibilidade dos campos de ref
, bem como simplifica a forma como os programadores pensam sobre out
. Um argumento para um parâmetro out
não contribui para o retorno, é simplesmente uma saída.
Inferir de contexto seguro de expressões de declaração
- contexto do chamador
- Se a variável out estiver marcada
scoped
, então bloco de declaração (ou seja, membro da função ou mais estreito). - Se o tipo da variável for
ref struct
, considere todos os argumentos para a invocação que a contém, incluindo o receptor:de qualquer argumento com contexto seguro , onde o seu parâmetro correspondente não sejae que tenha de contexto seguro de retorno apenas , , ou mais amplo- ref-safe-context de qualquer argumento em que seu parâmetro correspondente tenha ref-safe-context de somente de retorno ou mais ampla
Veja também Exemplos de seguro de contexto inferido de expressões de declaração.
Parâmetros scoped
implicitamente
Em geral, existem dois locais ref
que são implicitamente declarados como scoped
:
-
this
num método de instânciastruct
-
out
parâmetros
As regras de contexto seguro para ref serão escritas em termos de scoped ref
e ref
. Para fins de contexto seguro ref, um parâmetro in
é equivalente a ref
e out
é equivalente a scoped ref
. Tanto in
como out
só serão mencionados especificamente quando for importante para a semântica da regra. Caso contrário, são apenas considerados ref
e scoped ref
, respectivamente.
Ao discutir o ref-safe-context dos argumentos que correspondem aos parâmetros in
, eles serão generalizados como argumentos ref
na especificação. No caso de o argumento ser um lvalue, então o ref-safe-context é o do lvalue, caso contrário, é membro de função . Mais uma vezin
só será chamado aqui quando for importante para a semântica da regra atual.
Contexto seguro somente de retorno
O design também exige a introdução de um novo contexto seguro: apenas de retorno. Isso é semelhante a de contexto do chamador, na medida em que pode ser retornado, mas só pode ser retornado por meio de uma instrução return
.
Os detalhes de apenas retorno são que é um contexto que é maior do que o membro da função , mas menor do que o contexto do chamador . Uma expressão fornecida a uma declaração return
deve ser, pelo menos, apenas de retorno. Como tal, a maioria das regras existentes caduca. Por exemplo, a atribuição a um parâmetro
Há três locais que usam como padrão somente retorno:
- Um parâmetro
ref
ouin
terá um ref-safe-context de retorno somente . Isso é feito em parte pararef struct
evitar problemas de atribuição cíclica bobos . É feito uniformemente, no entanto, para simplificar o modelo e minimizar as alterações de compatibilidade. - Um parâmetro
out
para umref struct
terá de contexto seguro de somente de retorno. Isso permite que o retorno e aout
sejam igualmente expressivos. Isso não tem o problema bobo de atribuição cíclica porqueout
é, implicitamente,scoped
, então o contexto seguro de referência ainda é menor do que o contexto seguro . - Um parâmetro
para um construtor terá um de contexto seguro de somente de retorno. Devido ao facto de ser modelado como parâmetros out
, isso cai.
Qualquer expressão ou instrução que retorne explicitamente um valor de um método ou lambda deve ter um safe-contexte, se aplicável, um ref-safe-context, de pelo menos return-only. Isso inclui declarações return
, membros com corpo de expressão e expressões lambda.
Da mesma forma, qualquer atribuição a um out
deve ter uma de contexto seguro de pelo menos somente de retorno. No entanto, este não é um caso especial, apenas decorre das regras de atribuição existentes.
Nota: Uma expressão cujo tipo não é um tipo ref struct
tem sempre um contexto seguro de contexto do chamador.
Regras para invocação de método
As regras de contexto seguro ref para invocação de método serão atualizadas de várias maneiras. A primeira é reconhecendo o impacto que scoped
tem nos argumentos. Para um determinado argumento expr
que é passado para o parâmetro p
:
- Se
p
éscoped ref
, entãoexpr
não contribui para ref-safe-context ao considerar argumentos.- Se
p
éscoped
entãoexpr
não contribui de contexto seguro ao considerar argumentos.- Se
p
forout
expr
não contribuirá ref-safe-context ou safe-contextmais detalhes
A expressão "não contribui" significa que os argumentos simplesmente não são considerados ao calcular o valor de ref-safe-context ou de safe-context do retorno do método, respectivamente. Isso deve-se ao facto de os valores não poderem contribuir para essa vida útil, uma vez que a anotação scoped
o impede.
As regras de invocação do método agora podem ser simplificadas. O recetor já não precisa de tratamento especial, no caso de struct
é agora apenas um scoped ref T
. As regras de valor precisam ser mudadas para ter em consideração os retornos do campo ref
.
Um valor resultante de uma invocação de método
e1.M(e2, ...)
, em queM()
não retorna ref-to-ref-struct, tem um contexto seguro extraído do mais estreito dos seguintes:
- O contexto do chamador
- Quando o valor de retorno é
ref struct
, o contexto seguro e é contribuído por todas as expressões de argumento.- Quando o retorno é um
ref struct
o ref-safe-context contribuído por todos os argumentosref
Se
retornar ref-to-ref-struct, o de contexto seguro é o mesmo que o de contexto seguro de todos os argumentos que são ref-to-ref-struct. É um erro se houver vários argumentos com diferentes de contexto seguro devido a argumentos de método devem corresponder.
As regras de chamada ref
podem ser simplificadas para:
Um valor resultante de uma invocação de método
ref e1.M(e2, ...)
, ondeM()
não retorna ref-to-ref-struct, é ref-safe-context o mais estreito dos seguintes contextos:
- O contexto do chamador
- O contexto seguro contribuído por todas as expressões de argumento
- O ref-safe-context contribuído por todos os argumentos
ref
Se
retornar ref-to-ref-struct, o ref-safe-context é o mais estreitoref-safe-context contribuído por todos os argumentos que são ref-to-ref-struct.
Esta regra agora nos permite definir as duas variantes dos métodos desejados:
Span<int> CreateWithoutCapture(scoped ref int value)
{
// Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *function-member* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> CreateAndCapture(ref int value)
{
// Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *caller-context* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
// Okay: the safe-context of `span` is *caller-context* hence this is legal.
return span;
// Okay: the local `refLocal` has a ref-safe-context of *function-member* and a
// safe-context of *caller-context*. In the call below it is passed to a
// parameter that is `scoped ref` which means it does not contribute
// ref-safe-context. It only contributes its safe-context hence the returned
// rvalue ends up as safe-context of *caller-context*
Span<int> local = default;
ref Span<int> refLocal = ref local;
return ComplexScopedRefExample(ref refLocal);
// Error: similar analysis as above but the safe-context of `stackLocal` is
// *function-member* hence this is illegal
Span<int> stackLocal = stackalloc int[42];
return ComplexScopedRefExample(ref stackLocal);
}
Regras para inicializadores de objeto
O contexto seguro da expressão inicializadora de um objeto é o mais restrito de:
- O contexto seguro da chamada do construtor.
- O contexto seguro e o contexto seguro de referência de argumentos para inicializadores de membros em indexadores que possam escapar para o recetor.
- O safe-context do RHS de atribuições em inicializadores de membros para setters não readonly ou contexto ref-safe no caso de atribuição de ref.
Outra maneira de modelar isso é pensar em qualquer argumento para um inicializador de membro que possa ser atribuído ao recetor como sendo um argumento para o construtor. Isso ocorre porque o inicializador de membro é efetivamente uma chamada de construtor.
Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
Field = stackSpan;
}
// Can be modeled as
var x = new S(ref heapSpan, stackSpan);
Essa modelagem é importante porque demonstra que nossos MAMM precisam especialmente levar em conta os inicializadores de membros. Considere que este caso em particular deve ser ilegal, pois permite que um valor com um contexto seguro mais restrito seja atribuído a um contexto mais amplo.
Os argumentos do método devem corresponder
A presença de campos ref
significa que as regras em torno dos argumentos do método devem corresponder à necessidade de ser atualizada, pois um parâmetro ref
agora pode ser armazenado como um campo em um argumento ref struct
para o método. Anteriormente, a regra só tinha que considerar outro ref struct
sendo armazenado como um campo. O impacto disso é discutido em nas considerações de compatibilidade. A nova regra é...
Para qualquer invocação de método
e.M(a1, a2, ... aN)
- Calcule o contexto seguro mais estreito a partir de:
- contexto do chamador
- O contexto seguro de todos os argumentos
- O ref-safe-context de todos os argumentos ref cujos parâmetros correspondentes têm um ref-safe-context de caller-context
- Todos os argumentos
ref
de tiposref struct
devem poder ser atribuídos a um valor dentro desse contexto seguro . Este é um caso em queref
não generaliza para incluirin
eout
Para qualquer invocação de método
e.M(a1, a2, ... aN)
- Calcule o contexto seguro mais estreito a partir de:
- contexto do chamador
- O contexto seguro de todos os argumentos
- O ref-safe-context de todos os argumentos "ref" cujos parâmetros correspondentes não são
scoped
- Todos os argumentos
out
de tiposref struct
devem poder ser atribuídos a um valor dentro desse contexto seguro .
A presença de scoped
permite que os desenvolvedores reduzam o atrito que essa regra cria marcando parâmetros que não são retornados como scoped
. Isto remove os argumentos deles de (1) em ambos os casos acima e proporciona maior flexibilidade aos chamadores.
O impacto desta mudança é discutido mais profundamente abaixo. No geral, isso permitirá que os desenvolvedores tornem os sites de chamadas mais flexíveis, anotando valores ref-like que não escapem com scoped
.
Variação do escopo do parâmetro
O modificador scoped
e o atributo [UnscopedRef]
(veja abaixo) nos parâmetros também afetam a nossa sobrescrição de objetos, implementação de interface e as regras de conversão delegate
. A assinatura para uma substituição, implementação de interface ou uma conversão delegate
pode:
- Adicionar
scoped
a um parâmetroref
ouin
- Adicionar
scoped
a um parâmetroref struct
- Remover
[UnscopedRef]
de um parâmetroout
- Remover
[UnscopedRef]
de um parâmetro deref
de um tiporef struct
Qualquer outra diferença em relação a scoped
ou [UnscopedRef]
é considerada uma incompatibilidade.
O compilador relatará um diagnóstico para incompatibilidades de escopo inseguras em substituições, implementações de interface e conversões delegadas quando:
- O método retorna um
ref struct
ou retorna umref
ouref readonly
, ou o método tem um parâmetroref
ouout
de tiporef struct
, e - O método tem pelo menos um parâmetro adicional
ref
,in
ouout
, ou um parâmetro de tiporef struct
.
Estas regras ignoram os parâmetros this
porque os métodos de instância ref struct
não podem ser usados para sobreposições, implementações de interface ou conversões de delegados.
O diagnóstico é relatado como um erro se as assinaturas incompatíveis estiverem usando regras de contexto seguro ref C#11; caso contrário, o diagnóstico é um aviso .
O aviso de incompatibilidade de escopo pode ser reportado num módulo compilado com as regras de contexto seguro da ref C#7.2, onde scoped
não está disponível. Em alguns desses casos, pode ser necessário suprimir o aviso se a outra assinatura incompatível não puder ser modificada.
O modificador scoped
e o atributo [UnscopedRef]
também têm os seguintes efeitos nas assinaturas do método:
- O modificador
scoped
e o atributo[UnscopedRef]
não afetam a ocultação - As sobrecargas não podem diferir apenas em
scoped
ou[UnscopedRef]
A seção sobre ref
campo e scoped
é longa, por isso queria fechar com um breve resumo das mudanças de quebra propostas:
- Um valor que tenha
ref-safe-context para o de contexto do chamador é retornável por ou campo. - Um parâmetro
teria uma de contexto seguro de de membro da função.
Notas detalhadas:
- Um campo
ref
só pode ser declarado dentro de umref struct
- Um campo
ref
não pode ser declaradostatic
,volatile
ouconst
- Um campo
ref
não pode ter um tiporef struct
- O processo de geração do conjunto de referência deve preservar a presença de um campo
ref
dentro de umref struct
- Um
readonly ref struct
deve declarar os seus camposref
comoreadonly ref
- Para valores by-ref, o modificador
scoped
deve aparecer antes dein
,out
ouref
- O documento de regras de segurança de span será atualizado conforme descrito neste documento
- As novas regras de contexto seguro de referência entrarão em vigor quando
- A biblioteca principal contém o sinalizador de recurso que indica suporte para campos
ref
- O valor
langversion
é 11 ou superior
- A biblioteca principal contém o sinalizador de recurso que indica suporte para campos
Sintaxe
13.6.2 Declarações de variáveis locais: adicionado 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 A declaração for
: adicionado 'scoped'?
indiretamente de local_variable_declaration
.
13.9.5 A declaração foreach
: adicionado 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Listas de argumentos: adicionado 'scoped'?
à variável de declaração out
.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
12.7 Expressões de desconstrução:
[TBD]
15.6.2 Parâmetros do método: adicionado 'scoped'?
ao parameter_modifier
.
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
parameter_modifier
| 'this' 'scoped'? parameter_mode_modifier?
| 'scoped' parameter_mode_modifier?
| parameter_mode_modifier
;
parameter_mode_modifier
: 'in'
| 'ref'
| 'out'
;
20.2 Declarações de delegado: adicionado 'scoped'?
indiretamente de fixed_parameter
.
12.19 Expressões anónimas de função: foram adicionadas 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Tipos restritos do pôr-do-sol
O compilador tem um conceito de um conjunto de "tipos restritos" que é em grande parte não documentado. Esses tipos receberam um status especial porque no C# 1.0 não havia uma maneira de propósito geral para expressar seu comportamento. Mais notavelmente o fato de que os tipos podem conter referências à pilha de execução. Em vez disso, o compilador tinha um conhecimento especial deles e restringiu seu uso a maneiras que sempre seriam seguras: retornos não permitidos, não podem usar como elementos de matriz, não podem usar em genéricos, etc ...
Quando ref
campos estiverem disponíveis e estendidos para suportar ref struct
esses tipos poderão ser definidos corretamente em C# usando uma combinação de campos ref struct
e ref
. Portanto, quando o compilador deteta que um tempo de execução suporta campos ref
, ele não terá mais uma noção de tipos restritos. Em vez disso, ele usará os tipos conforme eles são definidos no código.
Para apoiar isso, nossas regras de contexto seguro de referência serão atualizadas da seguinte forma:
-
__makeref
será tratada como um método com a assinaturastatic TypedReference __makeref<T>(ref T value)
-
__refvalue
será tratada como um método com a assinaturastatic ref T __refvalue<T>(TypedReference tr)
. A expressão__refvalue(tr, int)
usará efetivamente o segundo argumento como o parâmetro type. -
__arglist
enquanto parâmetro terá um ref-safe-context e um safe-context do membro da função . -
__arglist(...)
como expressão terá um ref-safe-context e safe-context de membro da função.
Os tempos de execução conformes garantirão que TypedReference
, RuntimeArgumentHandle
e ArgIterator
sejam definidos como ref struct
. Além disso, TypedReference
deve ser visto como tendo um campo ref
para um ref struct
para qualquer tipo possível (pode armazenar qualquer valor). Isso combinado com as regras acima garantirá que as referências à pilha não escapem além de sua vida útil.
Nota: estritamente falando, este é um detalhe de implementação do compilador vs. parte da linguagem. Mas, dada a relação com os campos ref
, está a ser incluído na proposta de linguagem por uma questão de simplicidade.
Fornecer sem restrições de escopo
Um dos pontos de atrito mais notáveis é a incapacidade de retornar campos por ref
em membros de instância de um struct
. Isso significa que os desenvolvedores não podem criar métodos/propriedades que retornam ref
, tendo que expor diretamente os campos. Isso reduz a utilidade dos retornos de ref
em struct
, em que é muitas vezes o mais desejado.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
A lógica para este padrão é razoável, mas não há nada inerentemente errado com um struct
escapando this
por referência, é simplesmente o padrão escolhido pelas regras de contexto seguro ref.
Para corrigir isto, a linguagem fornecerá o oposto da anotação de duração scoped
, ao suportar um UnscopedRefAttribute
. Isso pode ser aplicado a qualquer ref
e mudará o ref-safe-context para ser um nível mais amplo do que seu padrão. Por exemplo:
UnscopedRef aplicado a | Original ref-safe-context | Novo contexto seguro de referência |
---|---|---|
Membro da instância | membro de função | somente retorno |
in
/
ref parâmetro |
somente retorno | contexto do chamador |
Parâmetro out |
membro de função | somente retorno |
Ao aplicar [UnscopedRef]
a um método de instância de um struct
ele tem o impacto de modificar o parâmetro this
implícito. Isso significa que this
atua como um ref
não anotado do mesmo tipo.
struct S
{
int field;
// Error: `field` has the ref-safe-context of `this` which is *function-member* because
// it is a `scoped ref`
ref int Prop1 => ref field;
// Okay: `field` has the ref-safe-context of `this` which is *caller-context* because
// it is a `ref`
[UnscopedRef] ref int Prop1 => ref field;
}
A anotação também pode ser colocada em parâmetros out
para restaurá-los ao comportamento C# 10.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Para efeitos de regras de contexto seguro de referência, tal [UnscopedRef] out
é considerado simplesmente um ref
. Semelhante a como in
é considerado ref
para fins de vida.
A anotação [UnscopedRef]
não será permitida em membros init
e construtores dentro de struct
. Esses membros já são especiais em relação à semântica ref
, pois veem os membros readonly
como mutáveis. Isso significa que levar ref
a esses membros se apresenta como uma simples ref
, não ref readonly
. Isso é permitido dentro do limite de construtores e init
. Permitir [UnscopedRef]
possibilitaria que tal ref
escapasse incorretamente para fora do construtor e permitisse a mutação após a semântica de readonly
ter ocorrido.
O tipo de atributo terá a seguinte definição:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Notas detalhadas:
- Um método ou propriedade de instância anotado com
[UnscopedRef]
tem ref-safe-context dethis
configurado para o contexto do chamador . - Um membro anotado com
[UnscopedRef]
não pode implementar uma interface. - É um erro usar
[UnscopedRef]
em- Um membro que não é declarado num
struct
- Um membro
static
, membroinit
ou construtor numstruct
- Um parâmetro marcado
scoped
- Um parâmetro passado por valor
- Um parâmetro passado por referência que não tem escopo implícito
- Um membro que não é declarado num
ScopedRefAttribute
As anotações scoped
serão emitidas em metadados pelo atributo de tipo System.Runtime.CompilerServices.ScopedRefAttribute
. O atributo será correspondido pelo nome qualificado pelo namespace, de modo que a definição não precise aparecer em nenhum assembly específico.
O tipo ScopedRefAttribute
é apenas para uso do compilador - não é permitido no código-fonte. A declaração de tipo é sintetizada pelo compilador se ainda não estiver incluída na compilação.
O tipo terá a seguinte definição:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
O compilador emitirá esse atributo no parâmetro com sintaxe scoped
. Isso só será emitido quando a sintaxe fizer com que o valor seja diferente de seu estado padrão. Por exemplo, scoped out
fará com que nenhum atributo seja emitido.
RefSafetyRulesAttribute
Existem várias diferenças nas regras de contexto ref safe entre C#7.2 e C#11. Qualquer uma dessas diferenças pode resultar em alterações de quebra ao recompilar com C#11 em relação a referências compiladas com C#10 ou anterior.
- parâmetros sem escopo de
ref
/in
/out
podem escapar de uma invocação de método como um camporef
em umref struct
no C#11, mas não no C#7.2 -
out
parâmetros têm escopo implícito em C#11 e não têm escopo em C#7.2 -
ref
/in
parâmetros pararef struct
tipos têm escopo implícito em C#11 e não têm um escopo definido em C#7.2
Para reduzir a chance de alterações disruptivas ao recompilar com C#11, atualizaremos o compilador C#11 para usar as regras de contexto seguro ref para a invocação de métodos que correspondam às regras que foram usadas para analisar a declaração de método. Essencialmente, ao analisar uma chamada para um método compilado com um compilador mais antigo, o compilador C#11 usará regras de contexto seguro ref C#7.2.
Para habilitar isso, o compilador emitirá um novo atributo [module: RefSafetyRules(11)]
quando o módulo for compilado com -langversion:11
ou superior ou compilado com um corlib contendo o sinalizador de recurso para campos ref
.
O argumento para o atributo indica a versão de idioma das regras de contexto seguro ref usadas quando o módulo foi compilado.
A versão está atualmente corrigida em 11
independentemente da versão de idioma real passada para o compilador.
A expectativa é que as futuras versões do compilador atualizem as regras de contexto ref safe e emitam atributos com versões distintas.
Se o compilador carregar um módulo que inclui um [module: RefSafetyRules(version)]
com um version
diferente de 11
, ele emitirá um aviso para a versão não reconhecida caso haja chamadas para métodos declarados nesse módulo.
Quando o compilador C#11 analisa uma chamada de método:
- Se o módulo que contém a declaração de método incluir
[module: RefSafetyRules(version)]
, independentemente deversion
, a chamada de método será analisada com regras C#11. - Se o módulo que contém a declaração de método for da fonte e compilado com
-langversion:11
ou com um corlib que contém o sinalizador de funcionalidade para os campos deref
, a chamada do método é analisada de acordo com as regras do C#11. - Se o módulo que contém a declaração de método referencia
System.Runtime { ver: 7.0 }
, a chamada de método é analisada com regras C#11. Esta regra é uma atenuação temporária para módulos compilados com visualizações anteriores do C#11 / .NET 7 e será removida posteriormente. - Caso contrário, a chamada de método é analisada com regras C#7.2.
Um compilador pré-C#11 ignorará qualquer RefSafetyRulesAttribute
e analisará chamadas de método apenas com regras C#7.2.
O RefSafetyRulesAttribute
será correspondido pelo nome qualificado do namespace, para que a definição não precise aparecer em nenhum assembly específico.
O tipo RefSafetyRulesAttribute
é apenas para uso do compilador - não é permitido no código-fonte. A declaração de tipo é sintetizada pelo compilador se ainda não estiver incluída na compilação.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public RefSafetyRulesAttribute(int version) { Version = version; }
public readonly int Version;
}
}
Buffers de tamanho fixo seguro
Buffers seguros de tamanho fixo não foram entregues em C# 11. Esse recurso pode ser implementado em uma versão futura do C#.
A linguagem relaxará as restrições em matrizes de tamanho fixo, de modo que elas possam ser declaradas em código seguro e o tipo de elemento possa ser gerenciado ou não gerenciado. Isso tornará legais tipos como os seguintes:
internal struct CharBuffer
{
internal char Data[128];
}
Essas declarações, assim como as suas contrapartes unsafe
, definirão uma sequência de elementos N
no tipo que contém. Esses membros podem ser acessados com um indexador e também podem ser convertidos em instâncias Span<T>
e ReadOnlySpan<T>
.
Ao indexar em um buffer de fixed
do tipo T
o estado readonly
do contêiner deve ser levado em consideração. Se o contêiner estiver readonly
, o indexador retornará ref readonly T
senão ele retornará ref T
.
Aceder a um buffer de fixed
sem um indexador não tem tipo natural; no entanto, é conversível para tipos Span<T>
. No caso de o recipiente ser readonly
o buffer é implicitamente conversível em ReadOnlySpan<T>
, caso contrário, ele pode implicitamente converter em Span<T>
ou ReadOnlySpan<T>
(a conversão Span<T>
é considerada melhor).
A instância Span<T>
resultante terá um comprimento igual ao tamanho declarado no buffer de fixed
. O de contexto seguro
Para cada declaração fixed
em um tipo em que o tipo de elemento é T
a linguagem gerará um método indexador correspondente get
somente cujo tipo de retorno é ref T
. O indexador será anotado com o atributo [UnscopedRef]
, uma vez que a implementação irá retornar campos do tipo que declara. A acessibilidade do membro corresponderá à acessibilidade no campo fixed
.
Por exemplo, a assinatura do indexador para CharBuffer.Data
será a seguinte:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Se o índice fornecido estiver fora dos limites declarados da matriz fixed
, uma IndexOutOfRangeException
será lançada. No caso de ser fornecido um valor constante, este será substituído por uma referência direta ao elemento adequado. A menos que a constante esteja fora dos limites declarados, caso em que ocorreria um erro de tempo de compilação.
Também será gerado um acessor nomeado para cada buffer de fixed
que oferece operações de valor get
e set
. Isso significa que os buffers de fixed
se assemelharão mais à semântica de matriz existente por terem um acessador de ref
, bem como as operações byval de get
e set
. Isso significa que os compiladores terão a mesma flexibilidade ao emitir código consumindo buffers de fixed
como ao consumir matrizes. Isso deve tornar operações como await
sobre fixed
buffers mais fáceis de emitir.
Isso tem também a vantagem de tornar os buffers fixed
mais fáceis de serem utilizados por outras linguagens. Indexadores nomeados é um recurso que existe desde a versão 1.0 do .NET. Mesmo linguagens que não podem emitir diretamente um indexador nomeado geralmente podem consumi-los (C# é realmente um bom exemplo disso).
O armazenamento de backup para o buffer será gerado usando o atributo [InlineArray]
. Trata-se de um mecanismo discutido na edição questão 12320, que permite especificamente declarar eficientemente a sequência de campos do mesmo tipo. Esta questão em particular ainda está em discussão ativa, e a expectativa é que a implementação desta funcionalidade ocorrerá conforme o desenrolar dessa discussão.
Inicializadores com valores ref
em expressões new
e with
Na seção 12.8.17.3 Inicializadores de objeto, atualizamos a gramática para:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
Na seção da expressão with
, a gramática foi atualizada para:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
O operando esquerdo da atribuição deve ser uma expressão que se liga a um campo ref.
O operando direito deve ser uma expressão que resulte em um valor lvalue designando um valor do mesmo tipo que o operando esquerdo.
Adicionamos uma regra semelhante a reatribuição local ref:
Se o operando esquerdo é uma ref gravável (ou seja, designa qualquer coisa diferente de um campo ref readonly
), então o operando direito deve ser um lvalue gravável.
As regras de escape para as invocações do construtor permanecem:
Uma expressão
new
que invoca um construtor obedece às mesmas regras que uma invocação de método que é considerada para retornar o tipo que está sendo construído.
Ou seja, as regras de invocação do método atualizadas acima:
Um rvalue resultante de uma invocação de método
e1.M(e2, ...)
tem de contexto seguro a partir do menor dos seguintes contextos:
- O contexto do chamador
- O contexto seguro contribuído por todas as expressões de argumento
- Quando o retorno é um
ref struct
então contexto ref-safe- contribuído por todos os argumentosref
Para uma expressão new
com inicializadores, as expressões inicializadoras contam como argumentos (elas contribuem com o seu contexto seguro) e as expressões inicializadoras ref
contam como argumentos de ref
(elas contribuem com o seu contexto seguro de referência), recursivamente.
Alterações em contexto inseguro
Os tipos de ponteiro (seção 23.3) são estendidos para permitir tipos gerenciados como tipo referente.
Esses tipos de ponteiro são escritos como um tipo gerenciado seguido por um token *
. Eles produzem um aviso.
O endereço do operador (seção 23.6.5) é relaxado para aceitar uma variável com um tipo gerenciado como seu operando.
A instrução fixed
(secção23.7) foi flexibilizada para aceitar um fixed_pointer_initializer que seja o endereço de uma variável de tipo gerido T
ou que seja uma expressão de um tipo_array com elementos de um tipo gerido T
.
O inicializador de alocação de pilha (seção 12.8.22) é igualmente relaxado.
Considerações
Há considerações que outras partes da pilha de desenvolvimento devem considerar ao avaliar esse recurso.
Considerações sobre Compatibilidade
O desafio da presente proposta prende-se com as implicações de compatibilidade que esta conceção tem para as nossas atuais regras de segurança , ou §9.7.2. Embora essas regras suportem totalmente o conceito de um ref struct
ter ref
campos, elas não permitem que APIs além de stackalloc
capturem ref
estado que faz referência à pilha. As regras de contexto seguro ref têm uma suposição , ou §16.4.12.8 de que não existe um construtor da forma Span(ref T value)
. Isso significa que as regras de segurança não levam em conta que um parâmetro ref
pode escapar como um campo ref
, permitindo assim código como o seguinte.
Span<int> CreateSpanOfInt()
{
// This is legal according to the 7.2 span rules because they do not account
// for a constructor in the form Span(ref T value) existing.
int local = 42;
return new Span<int>(ref local);
}
Efetivamente, há três maneiras de um parâmetro ref
escapar de uma invocação de método:
- Por retorno de valor
- Por
ref
regresso - Por
ref
campo noref struct
que é retornado ou passado como parâmetroref
/out
As regras existentes apenas têm em conta os pontos 1 e 2. Eles não contabilizam (3), e, portanto, lacunas como o retorno dos locais como campos ref
não são contabilizadas. Este desenho ou modelo deve alterar as regras para ter em conta (3). Isso terá um pequeno impacto na compatibilidade das APIs existentes. Especificamente, isso afetará as APIs que têm as seguintes propriedades.
- Tenha um
ref struct
na assinatura- Onde o
ref struct
é um tipo de retorno,ref
ou parâmetroout
- Tem um parâmetro adicional
in
ouref
excluindo o recetor
- Onde o
Em C# 10, os chamadores dessas APIs nunca tiveram que considerar que a entrada de estado ref
para a API podia ser capturada como um campo ref
. Isso permitiu para que vários padrões pudessem existir, com segurança em C# 10, que não seriam seguros em C# 11 devido à capacidade do estado ref
escapar como um campo ref
. Por exemplo:
Span<int> CreateSpan(ref int parameter)
{
// The implementation of this method is irrelevant when considering the lifetime of the
// returned Span<T>. The ref safe context rules only look at the method signature, not the
// implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
// to escape by ref in this method
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 but would be illegal with ref fields
return CreateSpan(ref parameter);
// Legal in C# 10 but would be illegal with ref fields
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 but would be illegal with ref fields
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Prevê-se que o impacto desta quebra de compatibilidade seja muito reduzido. A forma da API impactada fez pouco sentido na ausência de campos ref
, portanto, é improvável que os clientes tenham criado muitos deles. Experimentos executando ferramentas de deteção dessa forma de API em repositórios existentes corroboram essa afirmação. O único repositório com contagens significativas dessa forma é dotnet/runtime e isso ocorre porque esse repositório pode criar campos de ref
através do tipo intrínseco ByReference<T>
.
Mesmo assim, o design deve levar em conta tais APIs existentes porque expressa um padrão válido, mas não comum. Portanto, o design deve dar aos desenvolvedores as ferramentas para restaurar as regras de vida existentes ao atualizar para C# 10. Especificamente, ele deve fornecer mecanismos que permitam aos desenvolvedores anotar ref
parâmetros como incapazes de escapar por ref
ou ref
campo. Isso permite que os clientes definam APIs em C# 11 que tenham as mesmas regras de site de chamada do C# 10.
Assembléias de referência
Um conjunto de referência para uma compilação utilizando as características descritas na presente proposta deve manter os elementos que transmitem informações de contexto seguras de referência. Isso significa que todos os atributos de anotação vitalícia devem ser preservados em sua posição original. Qualquer tentativa de substituí-los ou omiti-los pode resultar em conjuntos de referência inválidos.
Representar campos ref
é mais nuanceado. Idealmente, um campo ref
apareceria em um conjunto de referência, assim como qualquer outro campo. No entanto, um campo ref
representa uma alteração no formato de metadados e isso pode causar problemas com cadeias de ferramentas que não são atualizadas para entender essa alteração de metadados. Um exemplo concreto é C++/CLI, que provavelmente apresentará um erro se consumir um campo ref
. Portanto, é vantajoso se os campos ref
puderem ser omitidos de assemblies de referência nas nossas bibliotecas principais.
Um campo ref
por si só não tem impacto nas regras de contexto seguro de referência. Como exemplo concreto, considere que inverter a definição de Span<T>
existente para usar um campo ref
não tem impacto no consumo. Por isso, a própria ref
pode ser omitida com segurança. No entanto, um campo ref
tem outros impactos no consumo que devem ser preservados:
- Uma
ref struct
que tenha um camporef
nunca é consideradaunmanaged
- O tipo de campo
ref
afeta infinitas regras de expansão genéricas. Portanto, se o tipo de um camporef
contém um parâmetro de tipo que deve ser preservado
Dadas essas regras, aqui está uma transformação de montagem de referência válida para um ref struct
:
// Impl assembly
ref struct S<T>
{
ref T _field;
}
// Ref assembly
ref struct S<T>
{
object _o; // force managed
T _f; // maintain generic expansion protections
}
Anotações
O tempo de vida é expresso mais naturalmente usando tipos. A vida útil de um determinado programa é segura quando os tipos de vida útil são verificados. Embora a sintaxe do C# implicitamente adicione tempos de vida aos valores, há um sistema de tipos subjacente que descreve as regras fundamentais aqui. Muitas vezes, é mais fácil discutir a implicação de mudanças no design em termos dessas regras, então elas são incluídas aqui para fins de discussão.
Note que esta não se destina a ser uma documentação 100% completa. Documentar cada comportamento não é um objetivo aqui. Em vez disso, destina-se a estabelecer um entendimento geral e uma linguagem comum pela qual o modelo, e as possíveis mudanças nele, possam ser discutidos.
Normalmente, não é necessário falar diretamente sobre tipos de duração. As exceções são locais onde o tempo de vida pode variar com base em sites de "instanciação" específicos. Este é um tipo de polimorfismo e chamamos essas vidas variáveis de "vidas genéricas", representadas como parâmetros genéricos. C# não fornece sintaxe para expressar genéricos vitalícios, então definimos uma "tradução" implícita de C# para uma linguagem reduzida expandida que contém parâmetros genéricos explícitos.
Os exemplos abaixo fazem uso de tempos de vida nomeados. A sintaxe $a
refere-se a uma vida útil chamada a
. É uma vida que não tem significado por si só, mas pode ser dada uma relação com outras vidas através da sintaxe where $a : $b
. Isto estabelece que $a
é convertível em $b
. Pode ser útil pensar nisso como estabelecer que $a
é uma duração pelo menos tão longa quanto $b
.
Existem algumas durações predefinidas para conveniência e brevidade abaixo:
-
$heap
: este é o tempo de vida de qualquer valor que exista no montículo. Está disponível em todos os contextos e assinaturas de método. -
$local
: Este é o tempo de vida de qualquer valor que existe na pilha de métodos. É efetivamente um espaço reservado de nome paramembro da função. Ele é implicitamente definido em métodos e pode aparecer em assinaturas de método, exceto para qualquer posição de saída. -
$ro
: nome placeholder para apenas retornar -
$cm
: espaço reservado para nome no contexto do chamador
Existem algumas relações predefinidas entre vidas:
-
where $heap : $a
para todas as durações de vida$a
where $cm : $ro
-
where $x : $local
para todos os tempos de vida predefinidos. Os tempos de vida definidos pelo usuário não têm relação com o local, a menos que explicitamente definidos.
As variáveis do tempo de vida, quando definidas em tipos, podem ser invariantes ou covariantes. Estes são expressos usando a mesma sintaxe que os parâmetros genéricos:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
O parâmetro de tempo de vida $this
nas definições de tipo não é predefinido, mas tem algumas regras associadas a ele quando é definido:
- Deve ser o primeiro parâmetro de tempo de vida.
- Deve ser covariante:
out $this
. - O tempo de vida dos campos de
ref
deve ser convertível em$this
- O tempo de vida
$this
de todos os campos não de referência deve ser$heap
ou$this
.
O tempo de vida de uma referência é expresso fornecendo um argumento de tempo de vida à referência. Por exemplo, um ref
que se refere à pilha é expresso como ref<$heap>
.
Ao definir um construtor no modelo, o nome new
será usado para o método. É necessário ter uma lista de parâmetros para o valor retornado, bem como os argumentos do construtor. Isso é necessário para expressar a relação entre as entradas do construtor e o valor construído. Em vez de ter Span<$a><$ro>
o modelo usará Span<$a> new<$ro>
em vez disso. O tipo de this
no construtor, incluindo tempo de vida, será definido como o valor de retorno.
As regras básicas para a vida útil são definidas como:
- Todos os tempos de vida são expressos sintaticamente como argumentos genéricos, vindo antes dos argumentos de tipo. Isso é verdade para vidas predefinidas, exceto
$heap
e$local
. - Todos os tipos
T
que não são umref struct
implicitamente têm vida útil deT<$heap>
. Isso está implícito, não há necessidade de escreverint<$heap>
em cada amostra. - Para um campo
ref
definido comoref<$l0> T<$l1, $l2, ... $ln>
:- Todas as durações de vida
$l1
até$ln
devem ser invariáveis. - O tempo de vida do
$l0
deve ser convertível em$this
- Todas as durações de vida
- Para um
ref
definido comoref<$a> T<$b, ...>
,$b
deve ser conversível em$a
- O
ref
de uma variável tem um tempo de vida definido por:- Para um
ref
local, parâmetro, campo ou retorno do tiporef<$a> T
o tempo de vida é$a
-
$heap
para todos os tipos de referência e campos de tipos de referência -
$local
para todo o resto
- Para um
- Uma cessão ou devolução é legal quando a conversão de tipo subjacente é legal
- O tempo de vida das expressões pode ser explicitado usando anotações de elenco:
-
(T<$a> expr)
o tempo de vida do valor é explicitamente$a
paraT<...>
-
ref<$a> (T<$b>)expr
o tempo de vida do valor é$b
no caso deT<...>
e o tempo de vida de referência é$a
.
-
Para fins de regras de tempo de vida, um ref
é considerado parte do tipo da expressão para efeitos de conversões. É logicamente representado pela conversão de ref<$a> T<...>
em ref<$a, T<...>>
onde $a
é covariante e T
é invariante.
Em seguida, vamos definir as regras que nos permitem mapear a sintaxe C# para o modelo subjacente.
Por uma questão de brevidade, um tipo que não tem parâmetros explícitos de tempo de vida é tratado como se houvesse out $this
definido e aplicado a todos os campos do tipo. Um tipo com um campo ref
deve definir parâmetros explícitos de tempo de vida.
Estas regras existem para suportar a nossa invariante existente que T
pode ser atribuída a scoped T
para todos os tipos. Isso significa que T<$a, ...>
pode ser atribuído a T<$local, ...>
para todos os períodos de vida conhecidos por serem conversíveis para $local
. Além disso, isso suporta outros itens, como poder atribuir Span<T>
do heap para aqueles na pilha. Isso exclui tipos em que os campos têm tempos de vida diferentes para valores não ref, mas essa é a realidade do C# hoje. Alterar isso exigiria uma mudança significativa nas regras do C# que precisariam ser mapeadas.
O tipo de this
para um tipo S<out $this, ...>
dentro de um método de instância é implicitamente definido da seguinte forma:
- Para o método de instância normal:
ref<$local> S<$cm, ...>
- Por exemplo, método anotado com
[UnscopedRef]
:ref<$ro> S<$cm, ...>
A falta de um parâmetro this
explícito força as regras implícitas aqui. Para exemplos e discussões complexas, considere a escrita como um método static
e torne this
um parâmetro explícito.
ref struct S<out $this>
{
// Implicit this can make discussion confusing
void M<$ro, $cm>(ref<$ro> S<$cm> s) { }
// Rewrite as explicit this to simplify discussion
static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}
A sintaxe do método C# corresponde ao modelo das seguintes maneiras:
- Os parâmetros de
ref
têm um tempo de vida de referência de$ro
- parâmetros do tipo
ref struct
têm esta vida útil de$cm
- As devoluções ref têm uma duração ref de
$ro
- Os retornos do tipo
ref struct
têm um tempo de vida de$ro
como valor -
scoped
em um parâmetro ouref
altera o tempo de vida da ref para ser$local
Dado isso, vamos explorar um exemplo simples que demonstra o modelo aqui:
ref int M1(ref int i) => ...
// Maps to the following.
ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
// okay: has ref lifetime $ro which is equal to $ro
return ref i;
// okay: has ref lifetime $heap which convertible $ro
int[] array = new int[42];
return ref array[0];
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Agora vamos explorar o mesmo exemplo usando um ref struct
:
ref struct S
{
ref int Field;
S(ref int f)
{
Field = ref f;
}
}
S M2(ref int i, S span1, scoped S span2) => ...
// Maps to
ref struct S<out $this>
{
// Implicitly
ref<$this> int Field;
S<$ro> new<$ro>(ref<$ro> int f)
{
Field = ref f;
}
}
S<$ro> M2<$ro>(
ref<$ro> int i,
S<$ro> span1)
S<$local> span2)
{
// okay: types match exactly
return span1;
// error: has lifetime $local which has no conversion to $ro
return span2;
// okay: type S<$heap> has a conversion to S<$ro> because $heap has a
// conversion to $ro and the first lifetime parameter of S<> is covariant
return default(S<$heap>)
// okay: the ref lifetime of ref $i is $ro so this is just an
// identity conversion
S<$ro> local = new S<$ro>(ref $i);
return local;
int[] array = new int[42];
// okay: S<$heap> is convertible to S<$ro>
return new S<$heap>(ref<$heap> array[0]);
// okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These
// are convertible.
return new S<$ro>(ref<$heap> array[0]);
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Em seguida, vamos ver como isso ajuda com o problema de autoatribuição cíclica:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
s.refField = ref s.field;
}
}
// Maps to
ref struct S<out $this>
{
int field;
ref<$this> int refField;
static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
{
// error: the types work out here to ref<$cm> int = ref<$ro> int and that is
// illegal as $ro has no conversion to $cm (the relationship is the other direction)
s.refField = ref<$ro> s.field;
}
}
Em seguida, vamos ver como isso ajuda com o problema do parâmetro de captura bobo:
ref struct S
{
ref int refField;
void Use(ref int parameter)
{
// error: this needs to be an error else every call to this.Use(ref local) would fail
// because compiler would assume the `ref` was captured by ref.
this.refField = ref parameter;
}
}
// Maps to
ref struct S<out $this>
{
ref<$this> int refField;
// Using static form of this method signature so the type of this is explicit.
static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
{
// error: the types here are:
// - refField is ref<$cm> int
// - ref parameter is ref<$ro> int
// That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and
// hence this reassignment is illegal
@this.refField = ref<$ro> parameter;
}
}
Questões em aberto
Altere o design para evitar quebras de compatibilidade
Este design propõe várias quebras de compatibilidade com as nossas regras de ref-safe-context existentes. Apesar de se acreditar que as alterações sejam minimamente impactantes, foi dada uma consideração significativa a um design sem mudanças que causassem descontinuidades.
O design de preservação da compatibilidade, no entanto, era significativamente mais complexo do que este aqui. A fim de preservar os campos ref
de compat, é necessário que tenham tempos de vida distintos para a capacidade de retornar por campo ref
e por campo ref
. Essencialmente, requer que forneçamos rastreamento de de contexto ref-field-safe-para todos os parâmetros para um método. Isso precisa ser calculado para todas as expressões e rastreado em todos os valores, praticamente em todos os lugares onde o contexto ref-safe de é rastreado hoje.
Além disso, este valor tem relações a ref-safe-context. Por exemplo, não faz sentido ter um valor que pode ser retornado como um campo ref
, mas não diretamente como ref
. Isso ocorre porque ref
campos podem ser retornados trivialmente por ref
já (ref
estado em um ref struct
pode ser retornado por ref
mesmo quando o valor que contém não pode). Por conseguinte, as regras necessitam ainda de um ajustamento constante para garantir que estes valores são sensatos uns em relação aos outros.
Também significa que a linguagem precisa de sintaxe para representar ref
parâmetros que podem ser retornados de três maneiras diferentes: por ref
campo, por ref
e por valor. O padrão é retornável por ref
. No futuro, porém, espera-se que o retorno mais natural, especialmente quando ref struct
estão envolvidos, seja pelo campo ref
ou ref
. Isso significa que as novas APIs exigem uma anotação de sintaxe extra para estarem corretas por padrão. Isto é indesejável.
Essas alterações de compatibilidade, no entanto, afetarão os métodos que têm as seguintes propriedades:
- Tenha um
Span<T>
ouref struct
- Onde o
ref struct
é um tipo de retorno,ref
ou parâmetroout
- Tem um parâmetro adicional
in
ouref
(excluindo o recetor)
- Onde o
Para entender o impacto, é útil dividir as APIs em categorias:
- Queremos que os consumidores considerem
ref
capturado como um camporef
. O exemplo principal são os construtoresSpan(ref T value)
- Não se quer que os consumidores considerem
ref
sendo capturado como um camporef
. Estes, porém, dividem-se em duas categorias:- APIs inseguras. Estes são APIs dentro dos tipos
Unsafe
eMemoryMarshal
, dos quaisMemoryMarshal.CreateSpan
é o mais proeminente. Essas APIs capturam oref
de forma insegura, mas também são conhecidas por serem APIs inseguras. - APIs seguras. Estas são APIs que aceitam
ref
parâmetros para eficiência, mas não é realmente registado em lugar nenhum. Os exemplos são pequenos, mas um éAsnDecoder.ReadEnumeratedBytes
- APIs inseguras. Estes são APIs dentro dos tipos
Esta alteração beneficia principalmente (1) acima. Espera-se que eles constituam a maioria das APIs que tomam um ref
e retornam um ref struct
no futuro. As alterações impactam negativamente (2.1) e (2.2), pois quebram a semântica da chamada existente devido à mudança das regras de ciclo de vida.
As APIs na categoria (2.1) são, no entanto, em grande parte criadas pela Microsoft ou por desenvolvedores que mais se beneficiam de campos ref
(os 'Tanners' do mundo). É razoável supor que essa classe de desenvolvedores estaria disposta a aceitar um imposto de compatibilidade na atualização para C# 11 para manter a semântica existente na forma de algumas anotações, se lhes fossem fornecidos campos ref
em retorno.
As APIs na categoria (2.2) são o maior problema. Não se sabe quantas APIs existem e não está claro se elas seriam mais ou menos frequentes no código de terceiros. A expectativa é que haja um número muito pequeno deles, especialmente se considerarmos a quebra de compatibilidade em out
. As pesquisas efetuadas até à data revelaram um número muito reduzido destes existentes na superfície public
. Este é um padrão difícil de pesquisar, pois requer análise semântica. Antes de proceder a esta alteração, seria necessária uma abordagem baseada em ferramentas para verificar os pressupostos em torno desta situação, com impacto num pequeno número de casos conhecidos.
Para ambos os casos na categoria (2), embora a correção seja simples. Os parâmetros ref
que não querem ser considerados capturáveis devem adicionar scoped
ao ref
. Em (2.1), isso provavelmente também forçará o desenvolvedor a usar Unsafe
ou MemoryMarshal
mas isso é esperado para APIs de estilo inseguro.
Idealmente, a linguagem poderia reduzir o impacto de alterações de quebra silenciosas emitindo um aviso quando uma API cai silenciosamente no comportamento problemático. Esse seria um método que leva um ref
, retorna ref struct
, mas não efetivamente captura o ref
no ref struct
. O compilador pode emitir um diagnóstico nesse caso, informando aos desenvolvedores que tais ref
devem ser anotados como scoped ref
em vez disso.
Decisão Este design pode ser alcançado, mas o recurso resultante é mais difícil de usar a tal ponto que foi tomada a decisão de fazer a quebra de compatibilidade.
Decisão O compilador fornecerá um aviso quando um método atender aos critérios, mas não capturar o parâmetro ref
como um campo ref
. Isso deve avisar adequadamente os clientes durante a atualização sobre os possíveis problemas que estão a criar.
Palavras-chave vs. atributos
Este design requer o uso de atributos para anotar as novas regras de ciclo de vida. Isso também poderia ter sido feito com a mesma facilidade com palavras-chave contextuais. Por exemplo, [DoesNotEscape]
poderia corresponder a scoped
. No entanto, as palavras-chave, mesmo as contextuais, geralmente devem atender a uma barra muito alta para a inclusão. Eles ocupam um espaço linguístico valioso e são elementos mais destacados na língua. Esse recurso, embora valioso, servirá a uma minoria de desenvolvedores de C#.
Superficialmente, isso parece favorecer o não uso de palavras-chave, mas há dois pontos importantes a considerar:
- As anotações afetarão a semântica do programa. Ter atributos que impactam a semântica do programa é uma linha que C# reluta em cruzar e não é claro se este é o recurso que deve justificar a linguagem a dar esse passo.
- Os programadores mais propensos a usar este recurso têm uma forte interseção com o grupo de programadores que usam apontadores de função. Esse recurso, embora também usado por uma minoria de desenvolvedores, garantiu uma nova sintaxe e essa decisão ainda é vista como sólida.
Em conjunto, isto significa que a sintaxe deve ser considerada.
Um esboço aproximado da sintaxe seria:
-
[RefDoesNotEscape]
mapeia parascoped ref
-
[DoesNotEscape]
mapeia parascoped
-
[RefDoesEscape]
mapeia paraunscoped
Decisão Utilizar sintaxe para scoped
e scoped ref
; use atributo para unscoped
.
Permitir locais de buffer fixo
Este design disponibiliza buffers de fixed
seguros que podem suportar qualquer tipo. Uma extensão possível aqui é permitir que esses buffers de fixed
sejam declarados como variáveis locais. Isto permitiria substituir uma série de operações de stackalloc
existentes por um buffer de fixed
. Também expandiria o conjunto de cenários em que poderíamos ter alocações tipo stack, uma vez que stackalloc
é limitado a tipos de elementos não geridos, enquanto os buffers fixed
não são.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Isso se mantém, mas exige que estendamos um pouco a sintaxe para as variáveis locais. Não está claro se isso vale ou não a pena a complexidade extra. É possível que possamos decidir não por agora e trazer de volta mais tarde, se for demonstrada a necessidade suficiente.
Exemplo de onde isso seria benéfico: https://github.com/dotnet/runtime/pull/34149
Decisão adiar isto por enquanto
Para usar modreqs ou não
Uma decisão precisa ser tomada se os métodos marcados com novos atributos de vida útil devem ou não se traduzir em modreq
na emissão. Haveria efetivamente um mapeamento de 1:1 entre anotações e modreq
se essa abordagem fosse adotada.
A lógica para adicionar um modreq
é que os atributos alteram a semântica das regras de contexto seguro ref. Apenas as línguas que compreendem estas semânticas devem pôr em causa os métodos em questão. Além disso, quando aplicados a cenários OHI, os ciclos de vida tornam-se um contrato que todos os métodos derivados devem implementar. A existência de anotações sem modreq
pode levar a situações em que cadeias de método virtual
com anotações de tempo de vida conflitantes são carregadas (pode ocorrer se apenas uma parte da cadeia virtual
for compilada e a outra não).
O trabalho inicial de contexto seguro não utilizou o modreq
, mas baseou-se nas linguagens e no framework para compreender. Ao mesmo tempo, embora todos os elementos que contribuem para as regras seguras de contexto 'ref' sejam uma parte forte da assinatura do método: ref
, in
, ref struct
, etc... Portanto, qualquer alteração nas regras existentes de um método já resulta numa alteração binária na assinatura. Para dar às novas anotações permanentes o mesmo impacto, elas precisarão de aplicação modreq
.
A preocupação é se isso é ou não exagero. Tem o efeito negativo de que tornar as assinaturas mais flexíveis, por exemplo, adicionando [DoesNotEscape]
a um parâmetro, resultará em uma alteração de compatibilidade binária. Essa troca significa que, com o tempo, estruturas como a BCL provavelmente não serão capazes de relaxar essas assinaturas. Poderia ser mitigado até certo ponto adotando uma abordagem semelhante à que a linguagem utiliza com os parâmetros in
e aplicando modreq
apenas em posições virtuais.
Decisão Não utilizar modreq
em metadados. A diferença entre out
e ref
não é modreq
mas eles agora têm diferentes valores de contexto ref safe. Não há nenhum benefício real em cumprir parcialmente as regras com modreq
aqui.
Permitir buffers fixos multidimensionais
O projeto dos buffers de fixed
deve ser estendido para incluir matrizes de estilo multidimensionais? Essencialmente permitindo declarações como as seguintes:
struct Dimensions
{
int array[42, 13];
}
Decisão Não permitir por enquanto
Violando o escopo
O repositório runtime tem várias APIs não públicas que capturam parâmetros ref
como campos ref
. Estes não são seguros porque o tempo de vida do valor resultante não é rastreado. Por exemplo, o construtor Span<T>(ref T value, int length)
.
A maioria dessas APIs provavelmente escolherá implementar um monitoramento adequado do tempo de vida no valor retornado, algo que poderá ser alcançado simplesmente ao atualizar para o C# 11. Alguns, no entanto, vão querer manter sua semântica atual de não rastrear o valor de retorno porque toda a sua intenção é ser inseguro. Os exemplos mais notáveis são MemoryMarshal.CreateSpan
e MemoryMarshal.CreateReadOnlySpan
. Isto será conseguido marcando os parâmetros como scoped
.
Isso significa que o tempo de execução precisa de um padrão estabelecido para remover inseguramente scoped
de um parâmetro:
-
Unsafe.AsRef<T>(in T value)
poderia expandir seu propósito atual mudando parascoped in T value
. Isso permitiria removerin
escoped
dos parâmetros. Em seguida, torna-se o método universal de "remover referência de segurança" - Introduza um novo método cuja finalidade é remover
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Isso também eliminain
porque, caso contrário, os utilizadores ainda precisariam de uma combinação de chamadas de método para "remover a segurança de referência", altura em que a solução existente provavelmente será suficiente.
Sem escopo por padrão?
O design tem apenas dois locais que são scoped
por padrão:
-
this
éscoped ref
-
out
éscoped ref
A decisão sobre out
é reduzir significativamente a carga de compatibilidade dos campos de ref
e, ao mesmo tempo, é um padrão mais natural. Ele permite que os desenvolvedores pensem em out
como dados fluindo apenas para fora, enquanto, se for ref
, então as regras devem considerar os dados fluindo em ambas as direções. Isso leva a uma confusão significativa do desenvolvedor.
A decisão sobre this
é indesejável porque significa que um struct
não pode retornar um campo por ref
. Este é um cenário importante para desenvolvedores de alto desempenho e o atributo [UnscopedRef]
foi adicionado essencialmente para este cenário.
Palavras-chave têm critérios elevados, e adicioná-las para um único cenário levanta suspeitas. Como tal, pensou-se se poderíamos evitar esta palavra-chave fazendo com que this
simplesmente ref
por defeito e não scoped ref
. Todos os membros que precisam this
para serem scoped ref
podem fazê-lo assim, marcando o método scoped
(da mesma forma que um método pode ser marcado readonly
para criar um readonly ref
atualmente).
Em um struct
normal, esta é principalmente uma mudança positiva, pois só introduz problemas de compatibilidade quando um membro tem um retorno ref
. Existem muito poucos desses métodos e uma ferramenta poderia detetá-los e convertê-los em scoped
membros rapidamente.
Num ref struct
, esta alteração introduz problemas significativamente maiores de compatibilidade. Considere o seguinte:
ref struct Sneaky
{
int Field;
ref int RefField;
public void SelfAssign()
{
// This pattern of ref reassign to fields on this inside instance methods would now
// completely legal.
RefField = ref Field;
}
static Sneaky UseExample()
{
Sneaky local = default;
// Error: this is illegal, and must be illegal, by our existing rules as the
// ref-safe-context of local is now an input into method arguments must match.
local.SelfAssign();
// This would be dangerous as local now has a dangerous `ref` but the above
// prevents us from getting here.
return local;
}
}
Essencialmente, isso significaria que todas as invocações de método de instância em locaisthis
. Um readonly ref struct
não tem esse problema porque a natureza readonly
impede a reatribuição de ref. Ainda assim, isto seria uma mudança significativa que rompe a compatibilidade com versões anteriores, pois impactaria praticamente todos os ref struct
mutáveis existentes.
Um readonly ref struct
, no entanto, ainda é problemático quando expandimos para incluir campos do ref
até ao ref struct
. Ele permite o mesmo problema básico, apenas movendo a captura para o valor do campo ref
:
readonly ref struct ReadOnlySneaky
{
readonly int Field;
readonly ref ReadOnlySpan<int> Span;
public void SelfAssign()
{
// Instance method captures a ref to itself
Span = new ReadOnlySpan<int>(ref Field, 1);
}
}
Pensou-se na ideia de fazer com que this
tivesse diferentes padrões com base no tipo de struct
ou membro. Por exemplo:
-
this
comoref
:struct
,readonly ref struct
oureadonly member
-
this
comoscoped ref
:ref struct
oureadonly ref struct
com camporef
pararef struct
Isso minimiza as quebras de compatibilidade e maximiza a flexibilidade, mas complica a situação para os clientes. Também não resolve totalmente o problema porque recursos futuros, como buffers de fixed
seguros, exigem que um ref struct
mutável tenha retornos ref
para campos que não funcionam apenas com esse design, pois se enquadrariam na categoria scoped ref
.
Decisão manter this
como scoped ref
. Isso significa que os exemplos sorrateiros anteriores produzem erros do compilador.
campos ref para ref struct
Esse recurso abre um novo conjunto de regras de contexto de referência segura porque permite que um campo ref
se refira a um ref struct
. Essa natureza genérica de ByReference<T>
significava que até agora o ambiente de execução não poderia ter tal estrutura. Como resultado, todas as nossas regras são escritas sob o pressuposto de que isso não é possível. A característica do campo ref
não se trata, em grande parte, de fazer novas regras, mas de codificar as regras existentes no nosso sistema. Permitir que campos de ref
a ref struct
exige que codifiquemos novas regras, porque há vários cenários novos a serem considerados.
A primeira é que um readonly ref
agora é capaz de armazenar um estado ref
. Por exemplo:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Isso significa que, ao considerar os argumentos de método que devem corresponder às regras, devemos perceber que readonly ref T
é a saída potencial do método quando T
potencialmente possui um campo ref
para um ref struct
.
A segunda questão é que a linguagem deve considerar um novo tipo de contexto seguro: ref-field-safe-context. Todos os ref struct
que contêm transitivamente um campo ref
têm outro escopo de escape que representa o(s) valor(es) no(s) campo(s) ref
. No caso de vários campos ref
, eles podem ser rastreados coletivamente como um único valor. O valor padrão para isto para parâmetros é do contexto do chamador.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Esse valor não está relacionado ao contexto seguro do contêiner; isto é, à medida que o contexto do contêiner fica menor, isso não afeta o ref-field-safe-context dos valores de camporef
. Além disso, o ref-field-safe-context nunca pode ser menor do que o safe-context do container.
ref struct Nested
{
ref Span<int> Span;
}
void M(ref Nested nested)
{
scoped ref Nested refLocal = ref nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is illegal
refLocal.Span = stackalloc int[42];
scoped Nested valLocal = nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is still illegal
valLocal.Span = stackalloc int[42];
}
Este relacionado com o contexto sempre existiu, essencialmente. Até agora, campos ref
só podiam apontar para normais struct
; portanto, era trivial colapsar para contexto de chamada. Para apoiar ref
campos a ref struct
, as nossas regras existentes precisam de ser atualizadas para ter em conta este novo ref-safe-contexto .
Em terceiro lugar, as regras de reatribuição de ref precisam ser atualizadas para garantir que não violemos o contexto do campo de referência e para os valores. Essencialmente, para x.e1 = ref e2
, em que o tipo de e1
é um ref struct
, o ref-field-safe-context deve ser igual.
Estes problemas são muito solúveis. A equipa do compilador esboçou algumas versões dessas regras e elas resultam em grande parte da nossa análise existente. O problema é que não há código de consumo para tais regras que ajude a provar a correção e usabilidade. Isso nos torna muito hesitantes em adicionar suporte por causa do medo de escolher padrões errados e voltar o tempo de execução para o canto de usabilidade quando ele tirar proveito disso. Essa preocupação é particularmente forte porque o .NET 8 provavelmente nos empurra nessa direção com allow T: ref struct
e Span<Span<T>>
. As regras seriam melhor escritas se fosse elaboradas juntamente com o código de consumo.
Decisão Atrasar a permissão para que ref
campo ref struct
até ao .NET 8, onde existam cenários que ajudarão a orientar as regras em torno desses cenários. Isso não foi implementado a partir do .NET 9
O que fará o C# 11.0?
As funcionalidades descritas neste documento não precisam ser implementadas de uma só vez. Em vez disso, eles podem ser implementados em fases em várias versões de idioma nos seguintes buckets:
-
ref
campos escoped
[UnscopedRef]
-
ref
campos pararef struct
- Tipos restritos do pôr-do-sol
- buffers de tamanho fixo
O que é implementado em que versão é apenas um exercício de delimitação.
Decisão Apenas (1) e (2) fizeram C# 11.0. O restante será considerado em versões futuras do C#.
Considerações futuras
Anotações avançadas de ciclo de vida
As anotações de ciclo de vida nesta proposta são limitadas, pois permitem que os desenvolvedores alterem o comportamento padrão de escaparem ou não escaparem de valores. Isso adiciona uma flexibilidade poderosa ao nosso modelo, mas não muda radicalmente o conjunto de relações que podem ser expressas. No núcleo, o modelo C# ainda é efetivamente binário: um valor pode ser retornado ou não?
Isso permite que relações limitadas ao longo da vida sejam compreendidas. Por exemplo, um valor que não pode ser retornado de um método tem um tempo de vida menor do que um que pode ser retornado de um método. No entanto, não há como descrever a relação de tempo de vida entre os valores que podem ser retornados de um método. Especificamente, não há como dizer que um valor tem uma vida útil maior do que o outro, uma vez estabelecido, ambos podem ser retornados de um método. O próximo passo na nossa evolução ao longo da vida seria permitir que tais relações fossem descritas.
Outros métodos, como Rust, permitem que esse tipo de relação seja expressa e, portanto, podem implementar operações de estilo scoped
mais complexas. A nossa língua poderia igualmente beneficiar se tal característica fosse incluída. De momento, não existe qualquer pressão motivadora para o fazer, mas, se houver no futuro, o nosso modelo de scoped
poderá ser alargado de modo a incluí-lo de uma forma bastante simples.
A cada scoped
pode ser atribuído um tempo de vida nomeado adicionando um argumento de estilo genérico à sintaxe. Por exemplo, scoped<'a>
é um valor que tem tempo de vida 'a
. Restrições como where
poderiam então ser usadas para descrever as relações entre essas vidas.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Este método define dois tempos de vida 'a
e 'b
e sua relação, especificamente que 'b
é maior que 'a
. Isso permite que o callsite tenha regras mais granulares sobre como os valores podem ser passados com segurança para métodos, em contraste com as regras mais gerais atualmente usadas.
Informações relacionadas
Questões
Todas as seguintes questões estão relacionadas com a presente proposta:
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Propostas
As seguintes propostas estão relacionadas com a presente proposta:
Amostras existentes
Esse trecho em particular requer insegurança porque tem problemas com a passagem de um Span<T>
que pode ser alocado para um método de instância em um ref struct
. Mesmo que este parâmetro não seja capturado, a linguagem deve assumir que é e, portanto, desnecessariamente causa atrito aqui.
Esse trecho deseja mutar um parâmetro escapando de elementos dos dados. Os dados escapados podem ser alocados em pilha para eficiência. Mesmo que o parâmetro não seja escapado, o compilador atribui-lhe um contexto seguro externo ao método envolvente, porque é um parâmetro. Isso significa que, para usar a alocação de pilha, a implementação deve usar unsafe
para atribuir de volta ao parâmetro depois de escapar dos dados.
Amostras divertidas
ReadOnlySpan<T>
public readonly ref struct ReadOnlySpan<T>
{
readonly ref readonly T _value;
readonly int _length;
public ReadOnlySpan(in T value)
{
_value = ref value;
_length = 1;
}
}
Lista de frugalidade
struct FrugalList<T>
{
private T _item0;
private T _item1;
private T _item2;
public int Count = 3;
public FrugalList(){}
public ref T this[int index]
{
[UnscopedRef] get
{
switch (index)
{
case 0: return ref _item0;
case 1: return ref _item1;
case 2: return ref _item2;
default: throw null;
}
}
}
}
Exemplos e notas
Abaixo está um conjunto de exemplos que demonstram como e por que as regras funcionam da maneira que funcionam. Estão incluídos vários exemplos que mostram comportamentos perigosos e como as regras os impedem de acontecer. É importante ter isso em mente ao fazer ajustes na proposta.
Reatribuição de referência e pontos de chamada
Demonstrar como a reatribuição ref e a invocação de método funcionam em conjunto.
ref struct RS
{
ref int _refField;
public ref int Prop => ref _refField;
public RS(int[] array)
{
_refField = ref array[0];
}
public RS(ref int i)
{
_refField = ref i;
}
public RS CreateRS() => ...;
public ref int M1(RS rs)
{
// The call site arguments for Prop contribute here:
// - `rs` contributes no ref-safe-context as the corresponding parameter,
// which is `this`, is `scoped ref`
// - `rs` contribute safe-context of *caller-context*
//
// This is an lvalue invocation and the arguments contribute only safe-context
// values of *caller-context*. That means `local1` has ref-safe-context of
// *caller-context*
ref int local1 = ref rs.Prop;
// Okay: this is legal because `local` has ref-safe-context of *caller-context*
return ref local1;
// The arguments contribute here:
// - `this` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `this` contributes safe-context of *caller-context*
//
// This is an rvalue invocation and following those rules the safe-context of
// `local2` will be *caller-context*
RS local2 = CreateRS();
// Okay: this follows the same analysis as `ref rs.Prop` above
return ref local2.Prop;
// The arguments contribute here:
// - `local3` contributes ref-safe-context of *function-member*
// - `local3` contributes safe-context of *caller-context*
//
// This is an rvalue invocation which returns a `ref struct` and following those
// rules the safe-context of `local4` will be *function-member*
int local3 = 42;
var local4 = new RS(ref local3);
// Error:
// The arguments contribute here:
// - `local4` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `local4` contributes safe-context of *function-member*
//
// This is an lvalue invocation and following those rules the ref-safe-context
// of the return is *function-member*
return ref local4.Prop;
}
}
Reatribuição de ref e fugas inseguras
A razão para a linha seguinte nas regras de reatribuição de referência pode não ser óbvia à primeira vista:
deve ter o mesmo contexto seguro de que
Isso ocorre porque o tempo de vida útil dos valores apontados pelas localizações ref
é invariante. A indireção impede-nos de permitir qualquer tipo de variância aqui, mesmo para tempos de vida mais curtos. Se o estreitamento for permitido, ele abrirá o seguinte código inseguro:
void Example(ref Span<int> p)
{
Span<int> local = stackalloc int[42];
ref Span<int> refLocal = ref local;
// Error:
// The safe-context of refLocal is narrower than p. For a non-ref reassignment
// this would be allowed as its safe to assign wider lifetimes to narrower ones.
// In the case of ref reassignment though this rule prevents it as the
// safe-context values are different.
refLocal = ref p;
// If it were allowed this would be legal as the safe-context of refLocal
// is *caller-context* and that is satisfied by stackalloc. At the same time
// it would be assigning through p and escaping the stackalloc to the calling
// method
//
// This is equivalent of saying p = stackalloc int[13]!!!
refLocal = stackalloc int[13];
}
Para uma transição de ref
para não ref struct
, esta regra é facilmente satisfeita, pois todos os valores têm o mesmo contexto seguro . Esta regra só realmente se aplica quando o valor é um ref struct
.
Este comportamento de ref
também será importante num futuro em que permitimos que ref
campos ref struct
.
Locais com escopo
O uso de scoped
em variáveis locais será particularmente útil para padrões de código que atribuem condicionalmente valores com diferentes contextos seguros de às variáveis locais. Isso significa que o código não precisa mais depender de truques de inicialização como = stackalloc byte[0]
para definir um contexto local seguro , mas agora pode simplesmente usar scoped
.
// Old way
// Span<byte> span = stackalloc byte[0];
// New way
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
span = stackalloc byte[len];
}
else
{
span = new byte[len];
}
Este padrão surge frequentemente em código de baixo nível. Quando o ref struct
envolvido é Span<T>
o truque acima pode ser usado. No entanto, não é aplicável a outros tipos de ref struct
e pode resultar na necessidade de código de baixo nível recorrer a unsafe
para contornar a incapacidade de especificar corretamente o tempo de vida.
Valores de parâmetros com escopo
A permissividade do escape padrão para parâmetros é uma fonte de atrito frequente em código de baixo nível. Eles são de contexto seguro para o contexto do chamador . Este é um padrão sensato porque se alinha com os padrões de codificação do .NET como um todo. No código de baixo nível, porém, há um uso maior de ref struct
e esse padrão pode causar atrito com outras partes das regras de contexto seguro "ref".
O principal ponto de atrito ocorre porque, no método , os argumentos devem corresponder à regra. Esta regra geralmente entra em jogo com métodos de instância em ref struct
onde pelo menos um parâmetro também é um ref struct
. Este é um padrão comum em código de baixo nível, em que os tipos de ref struct
geralmente aproveitam os parâmetros de Span<T>
em seus métodos. Por exemplo, ocorrerá em qualquer estilo de escrita ref struct
que utilize Span<T>
para transmitir buffers.
Esta regra existe para evitar cenários como os seguintes:
ref struct RS
{
Span<int> _field;
void Set(Span<int> p)
{
_field = p;
}
static void DangerousCode(ref RS p)
{
Span<int> span = stackalloc int[] { 42 };
// Error: if allowed this would let the method return a reference to
// the stack
p.Set(span);
}
}
Essencialmente, essa regra existe porque a linguagem deve assumir que todas as entradas para um método escapam ao máximo permitido contexto seguro. Quando há ref
ou out
parâmetros, incluindo os recetores, é possível que as entradas escapem como campos desses valores ref
(como acontece em RS.Set
acima).
Na prática, porém, existem muitos desses métodos que passam ref struct
como parâmetros que nunca pretendem capturá-los na saída. É apenas um valor que é usado dentro do método atual. Por exemplo:
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Error: The safe-context of `span` is function-member
// while `reader` is outside function-member hence this fails
// by the above rule.
if (reader.TextEquals(span))
{
...
}
}
}
A fim de contornar este código de baixo nível irá recorrer a unsafe
truques para mentir para o compilador sobre o tempo de vida de sua ref struct
. Isso reduz significativamente a proposta de valor dos ref struct
pois eles devem ser um meio de evitar unsafe
enquanto continuam a escrever código de alto desempenho.
É aqui que scoped
é uma ferramenta eficaz em parâmetros ref struct
porque os remove da consideração como sendo retornados do método de acordo com o atualizado os argumentos do método devem corresponder à regra. Um parâmetro ref struct
que é consumido, mas nunca retornado, pode ser rotulado como scoped
para tornar os sites de chamadas mais flexíveis.
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(scoped ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Okay: the compiler never considers `span` as capturable here hence it doesn't
// contribute to the method arguments must match rule
if (reader.TextEquals(span))
{
...
}
}
}
Impedindo atribuições complexas de referências devido a mutações de leitura única
Quando um ref
é levado para um campo readonly
em um construtor ou membro init
, o tipo ref
não é ref readonly
. Este é um comportamento de longa data que permite códigos como os seguintes:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Isso, no entanto, representa um problema potencial se tal ref
pudesse ser armazenado em um campo ref
do mesmo tipo. Isso permitiria a mutação direta de um readonly struct
de um membro da instância:
readonly ref struct S
{
readonly int i;
readonly ref int r;
public S()
{
i = 0;
// Error: `i` has a narrower scope than `r`
r = ref i;
}
public void Oops()
{
r++;
}
}
A proposta impede que isso aconteça porque viola as regras de contexto seguras de referência. Considere o seguinte:
- O contexto ref-safe-de do
this
é de membro da função e o contexto seguro de é de contexto do chamador. Ambos são padrão parathis
em um membrostruct
. - O ref-safe-context do
i
é membro da função. Isto decorre das regras de duração do campo . Especificamente a regra 4.
Nesse ponto, a linha r = ref i
é ilegal devido às regras de reatribuição de referência .
Estas regras não se destinavam a evitar este comportamento, mas fazem-no como um efeito secundário. É importante ter isso em mente para qualquer atualização futura de regras para avaliar o impacto em cenários como este.
Atribuição cíclica tola
Um aspeto com que este design se debateu é a liberdade com que um ref
pode ser devolvido a partir de um método. Permitir que todos os ref
sejam retornados tão livremente quanto os valores normais é provavelmente o que a maioria dos desenvolvedores espera intuitivamente. No entanto, permite cenários patológicos que o compilador deve considerar ao calcular a segurança de referência. Considere o seguinte:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
// Error: s.field can only escape the current method through a return statement
s.refField = ref s.field;
}
}
Este não é um padrão de código que esperamos que qualquer desenvolvedor use. No entanto, quando um ref
pode ser devolvido com a mesma duração de vida que um valor, é legal de acordo com as regras. O compilador deve considerar todos os casos legais ao avaliar uma chamada de método e isso leva a que tais APIs sejam efetivamente inutilizáveis.
void M(ref S s)
{
...
}
void Usage()
{
// safe-context to caller-context
S local = default;
// Error: compiler is forced to assume the worst and concludes a self assignment
// is possible here and must issue an error.
M(ref local);
}
Para tornar essas APIs utilizáveis, o compilador garante que o tempo de vida da ref
para um parâmetro ref
seja menor do que o tempo de vida de quaisquer referências no valor do parâmetro associado. Esta é a lógica para ter de contexto ref-safe para ref
ref struct
ser somente de retorno e out
ser de contexto do chamador. Isso evita a atribuição cíclica por causa da diferença nos tempos de vida.
Observe que [UnscopedRef]
promove o ref-safe-context de quaisquer ref
para ref struct
valores no contexto do chamador e assim permite a atribuição cíclica e força um uso viral de [UnscopedRef]
na cadeia de chamadas:
S F()
{
S local = new();
// Error: self assignment possible inside `S.M`.
S.M(ref local);
return local;
}
ref struct S
{
int field;
ref int refField;
public static void M([UnscopedRef] ref S s)
{
// Allowed: s has both safe-context and ref-safe-context of caller-context
s.refField = ref s.field;
}
}
Da mesma forma[UnscopedRef] out
permite uma atribuição cíclica porque o parâmetro tem de contexto seguro e de contexto ref-safe de somente de retorno.
Promover [UnscopedRef] ref
para de contexto do chamador é útil quando o tipo não é um ref struct
(observe que queremos manter as regras simples para que elas não distingam entre refs to ref vs non-ref structs):
int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2
static S F([UnscopedRef] ref int x)
{
S local = new();
local.M(ref x);
return local;
}
ref struct S
{
public ref int RefField;
public void M([UnscopedRef] ref int data)
{
RefField = ref data;
}
}
Em termos de anotações avançadas, o design [UnscopedRef]
cria o seguinte:
ref struct S { }
// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)
// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
where 'b >= 'a
Readonly não pode ser profundo em campos de referência
Considere o exemplo de código abaixo:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Ao conceber as regras para campos ref
em instâncias readonly
em teoria, as regras podem ser concebidas de forma válida de modo que isso seja legal ou ilegal. Essencialmente, readonly
pode validamente ser profundo através de um campo ref
ou pode aplicar-se apenas ao ref
. Aplicar apenas ao ref
impede a reatribuição ref, mas permite a atribuição normal que altera o valor referido.
Este design não existe no vácuo, porém, ele está projetando regras para tipos que já efetivamente têm ref
campos. O mais destacado dos quais, Span<T>
, já tem uma forte dependência de readonly
não ser tão profundo aqui. Seu cenário principal é a capacidade de atribuir ao campo ref
por meio de uma instância readonly
.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Isto significa que temos de escolher a interpretação superficial de readonly
.
Construtores de modelagem
Uma questão sutil de design é: Como as carrocerias dos construtores são modeladas para segurança de referência? Como é que o seguinte construtor é analisado, essencialmente?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Existem aproximadamente duas abordagens:
- Modelo como um método
static
em quethis
é um local onde o seu contexto seguro é o contexto do chamador - Modelo como um método
static
ondethis
é um parâmetroout
.
Além disso, um construtor deve atender aos seguintes invariantes:
- Assegure-se de que os parâmetros
ref
possam ser capturados como camposref
. - Certifique-se de que os campos de
ref
emthis
não possam ser escapados através dos parâmetrosref
. Isso violaria a atribuição de referência complicada .
A intenção é escolher a forma que satisfaça os nossos invariantes sem introduzir quaisquer regras especiais para construtores. Dado que o melhor modelo para construtores é ver this
como um parâmetro out
. O retorno apenas natureza do out
nos permite satisfazer todos os invariantes acima sem qualquer invólucro especial:
public static void ctor(out S @this, ref int f)
{
// The ref-safe-context of `ref f` is *return-only* which is also the
// safe-context of `this.field` hence this assignment is allowed
@this.field = ref f;
}
Os argumentos do método devem corresponder
A regra de correspondência dos argumentos do método é uma fonte comum de confusão para os desenvolvedores. É uma regra que tem uma série de casos especiais que são difíceis de entender, a menos que você esteja familiarizado com o raciocínio por trás da regra. Para melhor compreender as razões da regra, simplificaremos ref-safe-context e safe-context para simplesmente contexto.
Os métodos podem muito liberalmente retornar o estado passado a eles como parâmetros. Essencialmente, qualquer estado alcançável que não tenha escopo pode ser retornado (incluindo a possibilidade de retorno por ref
). Isso pode ser retornado diretamente por meio de uma instrução return
ou indiretamente, atribuindo um valor ref
.
As devoluções diretas não representam muitos problemas para a segurança da ref. O compilador simplesmente precisa olhar para todas as entradas retornáveis para um método e, em seguida, ele efetivamente restringe o valor de retorno para ser o mínimo contexto da entrada. Esse valor de retorno passa então pelo processamento normal.
Os retornos indiretos representam um problema significativo porque todas as ref
são entradas e saídas para o método. Essas saídas já têm um contexto conhecido . O compilador não pode inferir novos, ele tem que considerá-los em seu nível atual. Isso significa que o compilador tem que examinar cada ref
que é atribuível no método chamado, avaliar o seu contexto , e, em seguida, verificar se nenhuma entrada que possa ser retornada para o método tem um contexto menor do que esse ref
. Se houver algum desses casos, a chamada de método deve ser inválida porque pode violar a segurança ref
.
Os argumentos do método devem corresponder é o processo pelo qual o compilador afirma essa verificação de segurança.
Uma maneira diferente de avaliar isso, que muitas vezes é mais fácil para os desenvolvedores considerarem, é fazer o seguinte exercício:
- Observe a definição do método para identificar todos os lugares onde o estado pode ser retornado indiretamente: a. Parâmetros
ref
mutáveis que apontam pararef struct
b. Parâmetrosref
mutáveis com campos deref
atribuíveis por referência c. Parâmetrosref
atribuíveis ou camposref
que apontam pararef struct
(considerar recursivamente) - Consulte o ponto de chamada a. Identifique os contextos que se alinham com os locais identificados acima b. Identificar os contextos de todas as entradas do método que são retornáveis (não alinham com parâmetros
scoped
)
Se qualquer valor em 2.b for menor que 2.a, a chamada de método deve ser ilegal. Vejamos alguns exemplos para ilustrar as regras:
ref struct R { }
class Program
{
static void F0(ref R a, scoped ref R b) => throw null;
static void F1(ref R x, scoped R y)
{
F0(ref x, ref y);
}
}
Olhando para a chamada de F0
, vamos analisar (1) e (2). Os parâmetros com potencial de retorno indireto são a
e b
pois ambos podem ser atribuídos diretamente. Os argumentos que se alinham com esses parâmetros são:
-
a
que mapeia parax
que tem contexto de contexto do chamador -
b
que mapeia paray
com o contexto de função-membro
O conjunto de entradas retornáveis para o método são
-
x
com escopo de escape de do contexto do chamador -
ref x
com escopo de escape de do contexto do chamador -
y
com de escopo de escape de membro da função
O valor ref y
não é retornável, uma vez que mapeia para um scoped ref
portanto, não é considerado uma entrada. Mas dado que há pelo menos uma entrada com um escopo de escape de menor (argumentoy
) do que uma das saídas (argumentox
), a chamada de método é ilegal.
Uma variação diferente é a seguinte:
ref struct R { }
class Program
{
static void F0(ref R a, ref int b) => throw null;
static void F1(ref R x)
{
int y = 42;
F0(ref x, ref y);
}
}
Mais uma vez, os parâmetros com potencial de retorno indireto são a
e b
pois ambos podem ser atribuídos diretamente. Mas b
pode ser excluído porque não aponta para um ref struct
, portanto, não pode ser usado para armazenar ref
estado. Assim, temos:
-
a
que mapeia parax
que tem contexto de contexto do chamador
O conjunto de entradas retornáveis para o método são:
-
x
com o contexto do contexto do chamador -
ref x
com o contexto do contexto do chamador -
ref y
com contexto da função-membro
Dado que há pelo menos uma entrada com um escopo de escape de menor (argumentoref y
) do que uma das saídas (argumentox
), a chamada de método é ilegal.
Esta é a lógica que a regra de correspondência dos argumentos do método está a tentar englobar. Ele vai além, pois considera tanto scoped
como uma maneira de não considerar entradas e readonly
como uma maneira de remover ref
como uma saída (não se pode atribuir a um readonly ref
, então não pode ser uma fonte de saída). Esses casos especiais adicionam complexidade às regras, mas isso é feito para o benefício do desenvolvedor. O compilador procura remover todas as entradas e saídas que sabe que não podem contribuir para o resultado para dar aos desenvolvedores a máxima flexibilidade ao chamar um membro. Tal como a resolução de sobrecargas, vale a pena o esforço para tornar as nossas regras mais complexas quando cria mais flexibilidade para os consumidores.
Exemplos de contexto seguro inferido de expressões de declaração
Relacionado com Infer o contexto seguro das expressões de declaração.
ref struct RS
{
public RS(ref int x) { } // assumed to be able to capture 'x'
static void M0(RS input, out RS output) => output = input;
static void M1()
{
var i = 0;
var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M2(RS rs1)
{
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M3(RS rs1)
{
M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
}
}
Observe que o contexto local que resulta do modificador scoped
é o mais estreito que poderia ser usado para a variável - ser mais estreito significaria que a expressão se refere a variáveis que são declaradas apenas em um contexto mais restrito do que a expressão.
C# feature specifications