Compartilhar via


Parâmetros ref readonly

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode aprender mais sobre o processo de adoção das especificações de recursos para o padrão de linguagem C# no artigo sobre as especificações .

Problema do especialista: https://github.com/dotnet/csharplang/issues/6010

Resumo

Permite o modificador de declaração no local do parâmetro ref readonly e altera as regras do ponto de chamada da seguinte maneira:

Anotação do local de chamada Parâmetro ref Parâmetro ref readonly Parâmetro in Parâmetro out
ref Permitido Permitido Aviso Erro
in Erro Permitido Permitido Erro
out Erro Erro Erro Permitido
Nenhuma anotação Erro Aviso Permitido Erro

(Observe que houve uma alteração nas regras existentes: o parâmetro in com a anotação ref no local de chamada produz um aviso em vez de um erro.)

Altere as regras de valor do argumento da seguinte maneira:

Tipo de valor Parâmetro ref Parâmetro ref readonly Parâmetro in Parâmetro out
rvalue Erro Aviso Permitido Erro
lvalue Permitido Permitido Permitido Permitido

Quando lvalue significa uma variável (ou seja, um valor com um local; não precisa ser gravável/atribuível) e rvalue significa qualquer tipo de valor.

Motivação

O C# 7.2 introduziu parâmetros in como uma maneira de passar referências somente leitura. Os parâmetros in permitem tanto lvalues quanto rvalues e podem ser usados sem nenhuma anotação no local de chamada. No entanto, as APIs que capturam ou retornam referências de seus parâmetros gostariam de não permitir rvalues e impor alguma indicação no local de chamada de que uma referência está sendo capturada. Os parâmetros ref readonly são ideais nesses casos, pois sinalizam quando são usados com rvalues ou sem nenhuma anotação no local de chamada.

Além disso, há APIs que precisam apenas de referências de leitura apenas, mas usam

  • parâmetros ref desde pois foram introduzidos antes de in ficar disponível e mudar para in seria uma mudança que quebra a compatibilidade de origem e binária, por exemplo, QueryInterface ou
  • Parâmetros in para aceitar referências de somente leitura, mesmo que passar rvalues para elas não faça muito sentido, como, por exemplo, ReadOnlySpan<T>..ctor(in T value) ou
  • Parâmetros ref para não permitir rvalues mesmo quando não modificam a referência passada, por exemplo, Unsafe.IsNullRef.

Essas APIs podem migrar para parâmetros ref readonly sem interromper os usuários. Para obter detalhes sobre compatibilidade binária, consulte a codificação de metadados proposta . Especificamente, alterando

  • refref readonly seria uma mudança que quebra a compatibilidade binária apenas para métodos virtuais,
  • refin também seria uma alteração que quebra a compatibilidade binária para métodos virtuais, mas não uma alteração que quebra a compatibilidade de origem (uma vez que as regras mudam apenas para avisar sobre argumentos ref passados para parâmetros in),
  • inref readonly não seria uma alteração significativa (mas nenhuma anotação de chamada ou rvalue resultaria em um aviso),
    • observe que essa seria uma alteração que quebra a compatibilidade para usuários que usam versões mais antigas do compilador (pois elas interpretam parâmetros ref readonly como parâmetros ref, não permitindo in ou a ausência de anotação no ponto de chamada) e novas versões do compilador com LangVersion <= 11 (para consistência com versões mais antigas do compilador, um erro será emitido indicando que os parâmetros ref readonly não são suportados, a menos que os argumentos correspondentes sejam passados com o modificador ref).

No sentido oposto, alterando

  • ref readonlyref seria potencialmente uma mudança de quebra de fonte (a menos que apenas a anotação do site de chamada ref tenha sido usada e apenas referências somente leitura sejam usadas como argumentos), e uma mudança de quebra binária para métodos virtuais.
  • ref readonlyin não seria uma alteração significativa (mas a anotação no local de chamada de ref resultaria em um aviso).

Observe que as regras descritas acima se aplicam a assinaturas de método, mas não a assinaturas delegadas. Por exemplo, alterar ref para in em uma assinatura de delegado pode ser uma alteração que causa quebra de compatibilidade (se um usuário estiver atribuindo um método com parâmetro ref a esse tipo de delegado, isso se tornaria um erro após a alteração da API).

Projeto detalhado

Em geral, as regras para os parâmetros de são as mesmas especificadas para os parâmetros na propostade , exceto quando explicitamente alteradas nesta proposta.

Declarações de parâmetro

Nenhuma alteração na gramática é necessária. O modificador ref readonly será permitido para parâmetros. Além dos métodos normais, ref readonly será permitido para parâmetros do indexador (como in, mas ao contrário de ref), mas não será permitido para parâmetros de operador (como ref, mas ao contrário de in).

Os valores de parâmetro padrão serão permitidos para parâmetros ref readonly, com um aviso, pois são equivalentes a passar rvalues. Isso permite que os autores de API alterem parâmetros in com valores padrão para parâmetros ref readonly sem causar uma quebra de compatibilidade de código-fonte.

Verificações de tipos de valores

Observe que, embora o modificador de argumento ref seja permitido para os parâmetros ref readonly, nada muda com respeito às verificações de tipo de valor, ou seja,

  • ref só pode ser usado com valores atribuíveis;
  • para passar referências de apenas leitura, é preciso usar o modificador de argumento in;
  • para passar rvalues, não é preciso usar nenhum modificador (o que resulta em um aviso para parâmetros de ref readonly, conforme descrito no resumo desta proposta).

Resolução de sobrecarga

A resolução de sobrecarga permitirá misturar ref/ref readonly/in/sem anotações de chamada e modificadores de parâmetros, conforme indicado pela tabela no resumo desta proposta, ou seja, todos os casos permitidos e de aviso serão considerados como possíveis candidatos durante a resolução de sobrecarga. Especificamente, há uma alteração no comportamento existente em que os métodos com o parâmetro in corresponderão a chamadas com o argumento correspondente marcado como ref— essa alteração será controlada pelo LangVersion.

No entanto, o aviso para passar um argumento sem modificador de local de chamada para um parâmetro ref readonly será suprimido se o parâmetro for

  • o receptor em uma invocação de método de extensão,
  • usado implicitamente como parte do inicializador de coleção personalizada ou manipulador de cadeia de caracteres interpolada.

As sobrecargas por valor serão preferenciais em vez de sobrecargas ref readonly caso não haja nenhum modificador de argumento (parâmetros in têm o mesmo comportamento).

Conversões de método

Da mesma forma, para fins de conversões de função anônima [§10.7] e grupo de métodos [§10.8], esses modificadores são considerados compatíveis (mas qualquer conversão permitida entre modificadores diferentes resulta em um aviso):

  • O parâmetro ref readonly do método de destino pode corresponder ao parâmetro in ou ao ref do delegado.
  • O parâmetro in do método de destino tem permissão para corresponder ao parâmetro ref readonly ou, condicionado à LangVersion, ao parâmetro ref do delegado.
  • Observação: o parâmetro ref do método de destino não pode corresponder ao parâmetro in nem ao ref readonly do delegado.

Por exemplo:

DIn dIn = (ref int p) => { }; // error: cannot match `ref` to `in`
DRef dRef = (in int p) => { }; // warning: mismatch between `in` and `ref`
DRR dRR = (ref int p) => { }; // error: cannot match `ref` to `ref readonly`
dRR = (in int p) => { }; // warning: mismatch between `in` and `ref readonly`
dIn = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `in`
dRef = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `ref`
delegate void DIn(in int p);
delegate void DRef(ref int p);
delegate void DRR(ref readonly int p);

Observe que não há nenhuma alteração no comportamento das conversões de ponteiro da função . Como lembrete, as conversões de ponteiro de função implícita não serão permitidas se houver uma incompatibilidade entre modificadores de tipo de referência e conversões explícitas sempre serão permitidas sem avisos.

Correspondência de assinatura

Os membros declarados em um único tipo não podem diferir na assinatura apenas devido a ref/out/in/ref readonly. Para outros fins de correspondência de assinatura (por exemplo, ocultar ou substituir), ref readonly pode ser trocado pelo modificador in, mas isso resulta em um aviso no local da declaração [§7.6]. Isso não se aplica ao comparar a declaração partial com sua implementação e ao comparar a assinatura do interceptor com a assinatura interceptada. Observe que não há nenhuma alteração na substituição dos pares de modificadores ref/in e ref readonly/ref, eles não podem ser trocados, pois as assinaturas não são compatíveis em termos binários. Para manter a consistência, o mesmo se aplica a outras finalidades de correspondência de assinatura (por exemplo, ocultação).

Codificação de metadados

Como lembrete,

  • Os parâmetros ref são emitidos como tipos de byref simples (T& em IL),
  • Os parâmetros in são como ref e eles são anotados com System.Runtime.CompilerServices.IsReadOnlyAttribute. No C# 7.3 e versões posteriores, eles também são emitidos com [in] e, se forem virtuais, com modreq(System.Runtime.InteropServices.InAttribute).

Os parâmetros ref readonly serão emitidos como [in] T&, e anotados com o seguinte atributo:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class RequiresLocationAttribute : Attribute
    {
    }
}

Além disso, se forem virtuais, eles serão emitidos com modreq(System.Runtime.InteropServices.InAttribute) para garantir a compatibilidade binária com os parâmetros in. Observe que, ao contrário dos parâmetros in, nenhum [IsReadOnly] será emitido para parâmetros ref readonly para evitar o aumento do tamanho dos metadados e também para fazer com que versões mais antigas do compilador interpretem parâmetros ref readonly como parâmetros ref (e, portanto, refref readonly não será uma alteração interruptiva de origem mesmo entre diferentes versões do compilador).

Se não estiver incluído na compilação, o RequiresLocationAttribute será correspondido pelo nome qualificado do namespace e sintetizado pelo compilador.

Especificar o atributo na origem será um erro se ele for aplicado a um parâmetro, da mesma forma que ParamArrayAttribute.

Ponteiros de função

Nos ponteiros de função, os parâmetros in são emitidos junto a modreq(System.Runtime.InteropServices.InAttribute) (consulte a proposta de ponteiros de função ). Os parâmetros ref readonly serão emitidos sem esse modreq, mas sim com modopt(System.Runtime.CompilerServices.RequiresLocationAttribute). As versões mais antigas do compilador ignorarão modopt e, portanto, interpretarão os parâmetros ref readonly como parâmetros ref (consistentes com o comportamento do compilador mais antigo para métodos normais com parâmetros ref readonly conforme descrito acima) e novas versões do compilador com reconhecimento de modopt o usarão para reconhecer parâmetros ref readonly para emitir avisos durante conversões e invocações . Para consistência com versões mais antigas do compilador, novas versões do compilador que possuem LangVersion <= 11 indicarão erros de que os parâmetros ref readonly não são suportados, a menos que os argumentos correspondentes sejam passados com o modificador ref.

Observe que é uma quebra binária para alterar modificadores em assinaturas de ponteiro de função se eles fizerem parte de APIs públicas, portanto, será uma quebra binária ao alterar ref ou in para ref readonly. No entanto, uma quebra de origem ocorrerá apenas para chamadores com LangVersion <= 11 ao alterar inref readonly (se o ponteiro for invocado com o modificador de local de chamada in), consistente com métodos normais.

Alterações da falha

O relaxamento de incompatibilidades ref/in na resolução de sobrecarga introduz uma mudança de quebra de comportamento, demonstrada no exemplo a seguir:

class C
{
    string M(in int i) => "C";
    static void Main()
    {
        int i = 5;
        System.Console.Write(new C().M(ref i));
    }
}
static class E
{
    public static string M(this C c, ref int i) => "E";
}

No C# 11, a chamada se associa a E.M; portanto, "E" é impresso. No C# 12, C.M pode se vincular (com um aviso) e não são buscados escopos de extensão já que temos um candidato aplicável; portanto, "C" é impresso.

Há também uma alteração radical na origem devido ao mesmo motivo. O exemplo a seguir imprime "1" no C# 11, mas, no C# 12, falha na compilação devido a um erro de ambiguidade.

var i = 5;
System.Console.Write(C.M(null, ref i));

interface I1 { }
interface I2 { }
static class C
{
    public static string M(I1 o, ref int x) => "1";
    public static string M(I2 o, in int x) => "2";
}

Os exemplos acima demonstram as interrupções nas invocações de método, mas como são causadas por alterações na resolução de sobrecarga, elas também podem ser acionadas para conversões de método.

Alternativas

Declarações de parâmetro

Os autores de API poderiam anotar parâmetros in projetados para aceitar apenas lvalues com um atributo personalizado e fornecer um analisador para sinalizar usos incorretos. Isso não permitiria que os autores de API alterassem assinaturas de APIs existentes que optassem por usar parâmetros ref para não permitir rvalues. Os chamadores dessas APIs ainda precisariam executar um trabalho extra para obter ref se tiverem acesso apenas a uma variável ref readonly. Alterar essas APIs de ref para [RequiresLocation] in seria uma mudança que quebra a compatibilidade com o código-fonte (e, no caso de métodos virtuais, também uma mudança que quebra a compatibilidade binária).

Em vez de permitir o modificador ref readonly, o compilador pode reconhecer quando um atributo especial (como [RequiresLocation]) é aplicado a um parâmetro. Isso foi discutido em LDM 2022-04-25, onde decidiu-se que isso é um recurso de linguagem, e não um analisador, por isso deve, portanto, parecer com um.

Verificações de tipos de valores

A passagem de lvalues sem modificadores para parâmetros ref readonly poderia ser permitida sem avisos, similar aos parâmetros de byref implícitos do C++. Isso foi discutido em LDM 2022-05-11, observando que a principal motivação para parâmetros ref readonly são APIs que capturam ou retornam referências desses parâmetros, portanto, marcador de algum tipo é uma vantagem.

Passar rvalue para ref readonly pode ser um erro, não um aviso. Isso foi inicialmente aceito em LDM 2022-04-25, mas discussões posteriores por e-mail viabilizaram uma flexibilização disso, pois perderíamos a capacidade de alterar as APIs existentes sem interromper o uso pelos usuários.

in pode ser o modificador de local de chamada "natural" para parâmetros ref readonly e usar ref pode resultar em avisos. Isso garantiria um estilo de código consistente e tornaria óbvio no local de chamada que a referência é somente leitura (ao contrário de ref). Foi aceito inicialmente no LDM 2022-04-25. No entanto, os avisos podem ser um ponto de atrito para os autores de API passarem de ref para ref readonly. Além disso, in foi redefinido como ref readonly + recursos de conveniência, portanto, isso foi rejeitado em LDM 2022-05-11.

Revisão LDM pendente

Nenhuma das opções a seguir foi implementada no C# 12. Continuam sendo propostas potenciais.

Declarações de parâmetro

A ordenação inversa de modificadores (readonly ref em vez de ref readonly) poderia ser permitida. Isso seria inconsistente com a maneira como readonly ref é retornado e os campos se comportam (a ordenação inversa não é permitida ou significa algo diferente, respectivamente) e poderia entrar em conflito com parâmetros somente leitura se implementados no futuro.

Os valores padrão dos parâmetros podem representar um erro para os parâmetros do tipo ref readonly.

Verificações de tipos de valores

Podem ser gerados erros em vez de avisos ao passar rvalues para parâmetros ref readonly ou em caso de incompatibilidades entre anotações de chamada e modificadores de parâmetros. Da mesma forma, valores modreq especiais podem ser usados em vez de usar um atributo para garantir que os parâmetros ref readonly sejam distintos dos parâmetros in no nível binário. Isso proporcionaria garantias mais fortes, portanto, seria bom para novas APIs, mas evitaria a adoção em APIs de runtime existentes que não podem introduzir alterações significativas.

As verificações de tipo de valor podem ser relaxadas para permitir que referências somente leitura sejam passadas através de ref para os parâmetros de in/ref readonly. Seria semelhante à forma como as atribuições ref e os retornos ref funcionam hoje—eles também permitem a passagem de referências como leitura única por meio do modificador ref na expressão de origem. No entanto, o ref normalmente está próximo do local onde o alvo é declarado como ref readonly, portanto, é claro que estamos passando uma referência como somente leitura (readonly), ao contrário das invocações cujos modificadores de argumento e parâmetro geralmente estão distantes. Além disso, eles permitem que somente o modificador ref ao contrário dos argumentos que permitem também in, portanto, in e ref se tornariam intercambiáveis para argumentos, ou in se tornaria praticamente obsoleto se os usuários quisessem tornar seu código consistente (eles provavelmente usariam ref em todos os lugares, já que é o único modificador permitido para atribuições de ref e retornos de ref).

Resolução de sobrecarga

A resolução de sobrecarga, substituição e conversão poderiam impedir a intercambiabilidade dos modificadores ref readonly e in.

A alteração da resolução de sobrecarga para parâmetros de in existentes poderia ser tomada incondicionalmente (não considerando LangVersion), mas isso seria uma alteração significativa.

Invocar um método de extensão com o receptor ref readonly pode resultar no aviso "O argumento 1 deve ser passado com a palavra-chave ref ou in", como ocorreria para invocações não-extensivas sem modificadores de local de chamada (o usuário pode corrigir esse aviso transformando a chamada do método de extensão em uma chamada de método estático). O mesmo aviso pode ser relatado ao usar um inicializador de coleção personalizado ou um manipulador de cadeia de caracteres interpolada com o parâmetro ref readonly, embora o usuário não possa contornar o problema.

As sobrecargas ref readonly podem ser preferidas a sobrecargas por valor quando não há modificador no local de chamada ou pode haver um erro de ambiguidade.

Conversões de método

Poderíamos permitir que o parâmetro ref do método de destino corresponda aos parâmetros in e ref readonly do delegado. Isso permitiria que os autores de API alterassem, por exemplo, ref para in em assinaturas delegadas sem interromper seus usuários (consistentemente com o que é permitido para assinaturas de método normais). No entanto, isso também resultaria na seguinte violação das garantias de readonly com apenas um aviso:

class Program
{
    static readonly int f = 123;
    static void Main()
    {
        var d = (in int x) => { };
        d = (ref int x) => { x = 42; }; // warning: mismatch between `ref` and `in`
        d(f); // changes value of `f` even though it is `readonly`!
        System.Console.WriteLine(f); // prints 42
    }
}

Conversões de ponteiro de função poderiam emitir um aviso sobre uma incompatibilidade de ref readonly/ref/in, mas se quiséssemos condicionar isso à LangVersion, seria necessário um investimento significativo em implementação, pois as conversões de tipo atuais não precisam de acesso à compilação. Além disso, embora a incompatibilidade seja atualmente um erro, é fácil para os usuários adicionar uma conversão para aceitar a incompatibilidade, se desejarem.

Codificação de metadados

Especificar RequiresLocationAttribute na origem pode ser permitido, da mesma forma que atributos In e Out. Como alternativa, pode ser um erro quando aplicado em outros contextos que não apenas parâmetros, da mesma forma que o atributo IsReadOnly; para preservar o espaço de design adicional.

Parâmetros do ponteiro de função ref readonly podem ser emitidos com diferentes combinações de modopt/modreq (observe que "quebra de origem" nesta tabela significa para chamadores com LangVersion <= 11):

Modificadores Pode ser reconhecido entre compilações Compiladores antigos os veem como refref readonly inref readonly
modreq(In) modopt(RequiresLocation) sim in binário, quebra de origem quebra binária
modreq(In) não in binário, quebra de origem Ok
modreq(RequiresLocation) sim não compatível binário, quebra de origem binário, quebra de origem
modopt(RequiresLocation) sim ref quebra binária binário, quebra de origem

Poderíamos emitir atributos [RequiresLocation] e [IsReadOnly] para parâmetros ref readonly. Em seguida, inref readonly não seria uma alteração de quebra mesmo para versões mais antigas do compilador, mas refref readonly se tornaria uma alteração de quebra de código-fonte para versões mais antigas do compilador (pois interpretariam ref readonly como in, o que não permitiria os modificadores ref) e para novas versões do compilador com LangVersion <= 11 (por questões de consistência).

Poderíamos tornar o comportamento de LangVersion <= 11 diferente do comportamento de versões mais antigas do compilador. Por exemplo, pode ser um erro sempre que um parâmetro ref readonly é chamado (mesmo ao usar o modificador ref no local de chamada) ou pode sempre ser permitido sem erros.

Alterações da falha

Esta proposta sugere aceitar uma alteração de comportamento disruptivo porque deve ser rara de ocorrer, é controlada por LangVersion, e os usuários podem contornar isso chamando explicitamente o método de extensão. Em vez disso, poderíamos atenuá-lo por

  • não permitir a incompatibilidade de ref/in (que só impediria a migração para in para APIs antigas que usavam ref porque in ainda não estava disponível),
  • modificando as regras de resolução de sobrecarga para continuar procurando uma correspondência melhor (determinada pelas regras de aperfeiçoamento especificadas abaixo) quando há uma incompatibilidade de tipo ref introduzida nesta proposta,
    • ou, como alternativa, continuar apenas para ref versus in incompatibilidade, não para os outros (ref readonly vs. ref/in/por valor).
Regras de aperfeiçoamento

O exemplo a seguir resulta em três erros de ambiguidade para as três invocações de M. Poderíamos adicionar novas regras de aperfeiçoamento para resolver as ambiguidades. Isso também resolveria a mudança de ruptura no código-fonte descrita anteriormente. Uma maneira seria fazer o exemplo imprimir 221 (onde o parâmetro ref readonly é correspondido ao argumento in, pois seria um aviso chamá-lo sem um modificador, enquanto que para o parâmetro in é permitido).

interface I1 { }
interface I2 { }
class C
{
    static string M(I1 o, in int i) => "1";
    static string M(I2 o, ref readonly int i) => "2";
    static void Main()
    {
        int i = 5;
        System.Console.Write(M(null, ref i));
        System.Console.Write(M(null, in i));
        System.Console.Write(M(null, i));
    }
}

Novas regras de aperfeiçoamento podem marcar como pior o parâmetro cujo argumento possa ter sido passado com um modificador de argumento diferente para torná-lo melhor. Ou seja, o usuário deve ser sempre capaz de transformar um parâmetro pior em um parâmetro melhor alterando seu modificador de argumento correspondente. Por exemplo, quando um argumento é passado por in, um parâmetro ref readonly é preferido em vez de um parâmetro in, para que o usuário possa passar o argumento por valor e escolher o parâmetro in. Essa regra é apenas uma extensão da regra de preferência por valor/in que está em vigor hoje (é a última regra de resolução de sobrecarga e toda a sobrecarga é melhor se qualquer um de seus parâmetros for melhor e nenhum for pior do que o parâmetro correspondente de outra sobrecarga).

argumento parâmetro melhor parâmetro pior
ref/in ref readonly in
ref ref ref readonly/in
por valor por valor/in ref readonly
in in ref

Devemos lidar com conversões de método da mesma forma. O exemplo a seguir resulta em dois erros de ambiguidade para as duas atribuições delegadas. Novas regras de aperfeiçoamento podem preferir um parâmetro de método cujo modificador de referência corresponde ao modificador de referência do parâmetro de delegado de destino correspondente, em vez de um que tenha uma incompatibilidade. Portanto, o exemplo a seguir imprimiria 12.

class C
{
    void M(I1 o, ref readonly int x) => System.Console.Write("1");
    void M(I2 o, ref int x) => System.Console.Write("2");
    void Run()
    {
        D1 m1 = this.M;
        D2 m2 = this.M; // currently ambiguous

        var i = 5;
        m1(null, in i);
        m2(null, ref i);
    }
    static void Main() => new C().Run();
}
interface I1 { }
interface I2 { }
class X : I1, I2 { }
delegate void D1(X s, ref readonly int x);
delegate void D2(X s, ref int x);

Reuniões de design