Partilhar via


ref readonly parâmetros

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 reunião de design de linguagem (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 .

Questão campeã: https://github.com/dotnet/csharplang/issues/6010

Resumo

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

Anotação de ponto de chamada Parâmetro ref parámetro ref readonly parâmetro in out parâmetro
ref Permitido Permitido Aviso Erro
in Erro Permitido Permitido Erro
out Erro Erro Erro Permitido
Sem anotação Erro Aviso Permitido Erro

Observe que há uma alteração nas regras existentes: o parâmetro in com a anotação ref gera um aviso ao invés de um erro.

Altere as regras de valor do argumento da seguinte maneira:

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

Onde lvalue significa uma variável (ou seja, um valor com uma localização; 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. in Parâmetros permitem ambos os lvalues e rvalues e podem ser usados sem qualquer anotação no local da chamada. No entanto, as APIs que capturam ou retornam referências de seus parâmetros gostariam de não permitir rvalues e também impor alguma indicação no site de chamada de que uma referência está sendo capturada. Os parâmetros ref readonly são ideais nesses casos, pois avisam se usados com *rvalues* ou sem qualquer anotação no ponto de chamada.

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

  • ref parâmetros desde que foram introduzidos antes de in se tornar disponível e mudar para in seria uma alteração que quebra a compatibilidade a nível de código-fonte e binário, por exemplo, QueryInterface, ou
  • in parâmetros para aceitar referências somente leitura, mesmo que passar rvalues para eles não faça muito sentido, por exemplo, ReadOnlySpan<T>..ctor(in T value)ou
  • ref parâmetros para impedir rvalues mesmo que não alterem a referência passada, por exemplo, Unsafe.IsNullRef.

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

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

Na direção oposta, mudando

  • ref readonlyref seria potencialmente uma alteração que quebra a compatibilidade com o código-fonte (a menos que apenas a anotação de local de chamada ref fosse usada e apenas referências de leitura apenas fossem usadas como argumentos) e uma alteração de quebra binária para métodos virtuais,
  • ref readonlyin não seria uma alteração disruptiva (mas a anotação de ponto de chamada 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 delegada pode ser uma alteração de quebra de origem (se um usuário estiver atribuindo um método com ref parâmetro a esse tipo de delegado, isso se tornará um erro após a alteração da API).

Desenho detalhado

Em geral, as regras para os parâmetros ref readonly são as mesmas que as especificadas para os parâmetros in na sua proposta , exceto quando explicitamente alteradas nesta proposta.

Declarações de parâmetros

Não são necessárias alterações gramaticais. O modificador ref readonly será permitido para parâmetros. Além dos métodos normais, ref readonly serão permitidos para parâmetros indexadores (como in mas ao contrário ref), mas não permitidos para parâmetros do operador (como ref mas ao contrário in).

Os valores padrão dos parâmetros serão permitidos para os parâmetros ref readonly com um aviso, pois são equivalentes a passar rvalues. Isso permite que os autores da API alterem os parâmetros in com valores padrão para os parâmetros ref readonly sem introduzir uma alteração que quebre a origem.

Verificações de tipo de valor

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

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

Resolução de sobrecarga

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

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

  • o recetor numa invocação do método de extensão,
  • usado implicitamente como parte do inicializador de coleção personalizado ou manipulador de cadeia de caracteres interpolada.

As sobrecargas por valor serão preferidas às sobrecargas ref readonly no caso de não haver um modificador de argumento (os parâmetrosin têm o mesmo comportamento).

Conversões de método

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

  • ref readonly parâmetro do método de destino pode corresponder a in ou ref parâmetro do delegado,
  • O parâmetro in do método de destino pode corresponder ao parâmetro ref readonly ou, dependendo da LangVersion, ao parâmetro ref do delegado.
  • Nota: O parâmetro ref do método de destino não é permitido para corresponder ao parâmetro in nem ao parâmetro 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, conversões implícitas de ponteiros de funções não são permitidas se houver uma incompatibilidade entre modificadores do tipo de referência, e conversões explícitas são sempre permitidas sem avisos.

Correspondência de assinaturas

Os membros declarados num único tipo não podem diferir na assinatura apenas por 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 num aviso no local da declaração [§7.6]. Isso não se aplica ao alinhar a declaração partial com a sua implementação e ao alinhar a assinatura do interceptor com a assinatura intercetada. Observe que não há nenhuma alteração na sobreposição para os pares modificadores ref/in e ref readonly/ref, eles não podem ser trocados, porque as assinaturas não são compatíveis binariamente. Por uma questão de coerência, o mesmo se aplica a outros fins de correspondência de assinaturas (por exemplo, ocultação).

Codificação de metadados

Como lembrete,

  • Os parâmetros ref são emitidos como tipos por referência "byref" simples (T& em IL).
  • in parâmetros são como ref mais eles são anotados com System.Runtime.CompilerServices.IsReadOnlyAttribute. Em C# 7.3 e posteriores, eles também são emitidos com [in] e, se forem virtuais, com modreq(System.Runtime.InteropServices.InAttribute).

ref readonly parâmetros serão emitidos como [in] T&, além de 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 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 in parâmetros, nenhum [IsReadOnly] será emitido para ref readonly parâmetros para evitar o aumento do tamanho dos metadados e também para fazer com que versões mais antigas do compilador interpretem ref readonly parâmetros como parâmetros ref (e, portanto, não será uma mudança de quebra de fonte, mesmo entre diferentes versões refref readonly do compilador).

O RequiresLocationAttribute será identificado pelo nome qualificado do namespace e gerado pelo compilador, caso ainda não esteja incluído na compilação.

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

Em ponteiros de função, os parâmetros in são associados a modreq(System.Runtime.InteropServices.InAttribute) (veja a proposta de ponteiros de função ). Os ref readonly parâmetros serão emitidos sem aquele modreq, mas com o modopt(System.Runtime.CompilerServices.RequiresLocationAttribute). Versões mais antigas do compilador ignorarão o modopt e, portanto, interpretarão ref readonly parâmetros como parâmetros ref (consistente com o comportamento mais antigo do compilador para métodos normais com parâmetros ref readonly conforme descrito acima) e novas versões do compilador cientes do modopt o usarão para reconhecer parâmetros ref readonly para emitir avisos durante conversões e invocações. Para manter a consistência com versões mais antigas do compilador, as novas versões do compilador com LangVersion <= 11 reportarão erros indicando 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 só ocorrerá para chamadores com LangVersion <= 11 ao alterar inref readonly (se invocar o ponteiro com o modificador do local de chamada in), de forma consistente com os métodos normais.

Mudanças significativas

O relaxamento de incompatibilidade de ref/in na resolução de sobrecarga introduz uma alteração que causa ruptura no comportamento, como demonstrado 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";
}

Em C# 11, a chamada liga-se a E.Me, portanto, "E" é impressa. No C# 12, C.M é permitido vincular (com um aviso) e nenhum escopo de extensão é pesquisado, pois temos um candidato aplicável, portanto, "C" é impresso.

Há também uma alteração que quebra a origem devido ao mesmo motivo. O exemplo abaixo imprime "1" em C# 11, mas falha ao compilar com um erro de ambiguidade em C# 12:

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 para invocações de método, mas como estas são causadas por alterações na resolução de sobrecarga, elas podem ser acionadas de forma semelhante para conversões de métodos.

Alternativas

Declarações de parâmetros

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

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

Verificações de tipo de valor

Passar lvalues sem modificadores para parâmetros ref readonly pode ser permitido sem quaisquer avisos, semelhante aos parâmetros byref implícitos do C++. Isso foi discutido em LDM 2022-05-11, observando que a principal motivação para os parâmetros ref readonly é que as APIs capturam ou retornam referências destes parâmetros, por isso é bom ter um marcador de algum tipo.

Passar rvalue para um ref readonly pode ser um erro, não um aviso. Isso foi inicialmente aceite em LDM 2022-04-25, mas discussões posteriores por e-mail relaxaram isso porque perderíamos a capacidade de alterar APIs existentes sem causar problemas aos utilizadores.

in pode ser o modificador de callsite "natural" para parâmetros ref readonly e usar ref pode resultar em avisos de alerta. 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 inicialmente aceite em LDM 2022-04-25. No entanto, os avisos podem ser um ponto de atrito para os autores da 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 pendente do LDM

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

Declarações de parâmetros

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

Os valores padrão dos parâmetros podem resultar em erro para os parâmetros ref readonly.

Verificações de tipo de valor

Erros podem ser gerados em vez de avisos ao passar rvalues para parâmetros ref readonly ou quando há incompatibilidade entre anotações de callsite e modificadores de parâmetros. Da mesma forma, um modreq especial poderia ser usado em vez de um atributo para garantir que os parâmetros ref readonly sejam distintos dos parâmetros in no nível binário. Isso forneceria garantias mais fortes, então seria bom para novas APIs, mas impediria a adoção em APIs de tempo de execução existentes que não podem introduzir alterações de quebra.

As verificações do tipo de valor podem ser flexibilizadas para permitir a passagem de referências de leitura apenas via ref em parâmetros in/ref readonly. Isso seria semelhante a como as atribuições ref e os retornos ref funcionam hoje — eles também permitem passar referências como somente leitura por meio do modificador ref na expressão de origem. No entanto, o ref geralmente está perto do local onde o alvo é declarado como ref readonly, então é claro que estamos passando uma referência como somente leitura, ao contrário de invocações cujos modificadores de argumento e parâmetro geralmente estão distantes. Além disso, eles permitem que apenas 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 tornariam praticamente obsoletos 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 ref e retornos ref).

Resolução de sobrecarga

Resolução de sobrecarga, substituição e conversão podem impedir a intercambiabilidade de modificadores de ref readonly e in.

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

Invocar um método de extensão com ref readonly recetor poderia resultar em aviso "O argumento 1 deve ser passado com ref ou in palavra-chave", como aconteceria para invocações sem extensão sem modificadores de callsite (o usuário poderia corrigir esse aviso transformando a invocação do método de extensão em invocação de método estático). A mesma advertência pode ser emitida ao usar um inicializador de coleção personalizado ou um manipulador de cadeia de caracteres interpolada com o parâmetro ref readonly, mesmo que o utilizador não o possa contornar.

ref readonly sobrecargas podem ser preferíveis em vez de sobrecargas por valor quando não há um modificador no local de chamada ou pode ocorrer um erro de ambiguidade.

Conversões de Método

Poderíamos permitir que ref parâmetro do método de destino correspondesse a in e ref readonly parâmetro do delegado. Isso permitiria que os autores da API alterassem, por exemplo, ref para in em assinaturas delegadas sem quebrar seus usuários (de forma consistente com o que é permitido para assinaturas de método normais). No entanto, também resultaria numa violação das garantias 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
    }
}

As conversões de ponteiro de função poderiam alertar sobre ref readonly/ref/in incompatibilidade, mas se quiséssemos excluir isso no LangVersion, seria necessário um investimento significativo na implementação, pois hoje as conversões de tipo não precisam de acesso à compilação. Além disso, mesmo que a incompatibilidade seja atualmente um erro, é fácil para os usuários adicionar um elenco para permitir a incompatibilidade, se quiserem.

Codificação de metadados

Especificar o RequiresLocationAttribute na origem pode ser permitido, da mesma forma que os atributos In e Out. Pode, alternativamente, tratar-se de um erro quando aplicado em outros contextos além de apenas parâmetros, de forma semelhante ao atributo IsReadOnly; para preservar mais espaço de design.

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

Modificadores Pode ser reconhecido em compilações Os compiladores antigos vêem-nos 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, interrupção na origem Ok
modreq(RequiresLocation) Sim sem suporte binário, ruptura no código fonte binário, quebra de origem
modopt(RequiresLocation) Sim ref quebra binária binário, quebra de código

Poderíamos emitir atributos [RequiresLocation] e [IsReadOnly] para ref readonly parâmetros. Então inref readonly não seria uma mudança de quebra mesmo para versões mais antigas do compilador, mas refref readonly se tornaria uma mudança de quebra de fonte para versões mais antigas do compilador (como eles interpretariam ref readonly como in, não permitindo modificadores ref) e novas versões do compilador com LangVersion <= 11 (para consistência).

Poderíamos tornar o comportamento para LangVersion <= 11 diferente do comportamento para 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 ser sempre permitido sem erros.

Quebrando mudanças

Esta proposta sugere aceitar uma alteração disruptiva de comportamento porque é pouco provável de ocorrer, é controlada pelo LangVersion, e os utilizadores podem contornar isso chamando o método de extensão explicitamente. Em vez disso, poderíamos mitigá-lo

  • desautorizar 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),
  • modificar as regras de resolução de sobrecarga para continuar a procurar uma melhor correspondência (determinada pelas regras de melhoria especificadas abaixo) quando houver uma incompatibilidade do tipo ref introduzida nesta proposta,
    • ou alternativamente, continuar apenas com o desajuste de ref vs. in, e não com os outros (por valorref readonly vs. ref/in).
Regras de melhoria

O exemplo a seguir atualmente resulta em três erros de ambiguidade para as três invocações de M. Poderíamos acrescentar novas regras de melhoria para resolver as ambiguidades. Isso também resolveria a alteração de quebra de fonte descrita anteriormente. Uma maneira seria fazer com que o exemplo imprimisse 221 (onde ref readonly parâmetro é correspondido com in argumento, já que seria um aviso chamá-lo sem modificador, enquanto para in parâmetro isso é 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 melhoria poderiam marcar como pior o parâmetro cujo argumento poderia ter sido passado com um modificador de argumento diferente para torná-lo melhor. Em outras palavras, 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 sobre um parâmetro in porque o usuário pode passar o argumento por valor para escolher o parâmetro in. Esta 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 dos seus parâmetros for melhor e nenhum for pior do que o parâmetro correspondente de outra sobrecarga).

argumento melhor parâmetro pior parâmetro
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 de forma semelhante. O exemplo a seguir atualmente resulta em dois erros de ambiguidade para as duas atribuições de delegado. Novas regras de otimização podem preferir um parâmetro de método cujo modificador de refness corresponda ao do parâmetro de delegado de destino, em vez de um que tenha uma incompatibilidade. Assim, 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