Prioridade de Resolução de Sobrecarga
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 saber mais sobre o processo de adoção das especificações de recursos no padrão da linguagem C# no artigo sobre as especificações .
Problema do especialista: https://github.com/dotnet/csharplang/issues/7706
Resumo
Apresentamos um novo atributo, System.Runtime.CompilerServices.OverloadResolutionPriority
, que pode ser usado por autores de API para ajustar a prioridade relativa de sobrecargas em um único tipo como um meio de orientar os consumidores de API a usar APIs específicas, mesmo que essas APIs normalmente sejam consideradas ambíguas ou não sejam escolhidas pelas regras de resolução de sobrecarga de C#.
Motivação
Os autores de API geralmente têm um problema sobre o que fazer com um membro depois que ele se torna obsoleto. Para fins de compatibilidade com versões anteriores, muitos manterão o membro existente com ObsoleteAttribute
definido como erro em perpetuidade, a fim de evitar a interrupção de consumidores que atualizam binários no runtime. Isso afeta particularmente os sistemas de plug-in, em que o autor de um plug-in não controla o ambiente no qual o plug-in é executado. O criador do ambiente pode manter um método mais antigo presente, mas bloquear o acesso a ele para qualquer código recém-desenvolvido. No entanto, ObsoleteAttribute
por si só não é suficiente. O tipo ou membro ainda está visível na resolução de sobrecarga e pode causar falhas indesejadas nesse processo quando existe uma alternativa totalmente válida, mas essa alternativa pode ser ambígua com o membro obsoleto, ou a presença do membro obsoleto faz com que a resolução de sobrecarga termine prematuramente, sem considerar o membro adequado. Para essa finalidade, queremos ter uma maneira de os autores de API orientarem a resolução de sobrecarga na solução da ambiguidade, para que possam evoluir suas áreas de superfície de API e direcionar os usuários para APIs com desempenho sem precisar comprometer a experiência do usuário.
A equipe de BCL (Bibliotecas de Classes Base) tem vários exemplos de onde isso pode ser útil. Alguns exemplos (hipotéticos) são:
- Criar uma sobrecarga de
Debug.Assert
que utilizaCallerArgumentExpression
para obter a expressão declarada, permitindo sua inclusão na mensagem e fazendo com que seja preferida em relação à sobrecarga existente. - Tornando
string.IndexOf(string, StringComparison = Ordinal)
preferencial ao invés destring.IndexOf(string)
. Isso teria que ser discutido como uma possível alteração significativa, mas alguns consideram que é a melhor opção padrão, e é mais provável que seja o que o usuário tinha em mente. - Uma combinação dessa proposta e
CallerAssemblyAttribute
permitiria que métodos com uma identidade de chamador implícita evitassem percorrer a pilha de chamadas de forma dispendiosa.Assembly.Load(AssemblyName)
faz isso hoje, e pode ser muito mais eficiente. -
Microsoft.Extensions.Primitives.StringValues
expõe uma conversão implícita astring
estring[]
. Isso significa que é ambígua quando passada para um método com sobrecargas deparams string[]
eparams ReadOnlySpan<string>
. Esse atributo pode ser usado para priorizar uma das sobrecargas para evitar a ambiguidade.
Projeto detalhado
Prioridade de resolução de sobrecarga
Definimos um novo conceito, overload_resolution_priority, que é usado durante o processo de resolução de um grupo de métodos. overload_resolution_priority é um valor inteiro de 32 bits. Todos os métodos têm um overload_resolution_priority com padrão 0, que pode ser alterado aplicando OverloadResolutionPriorityAttribute
a um método. Atualizamos a seção 12.6.4.1 da especificação C# da seguinte maneira (alteração em negrito):
Depois que os membros da função candidata e a lista de argumentos tiverem sido identificados, a seleção do membro da melhor função será a mesma em todos os casos:
- Primeiro, o conjunto de membros da função candidata é reduzido aos membros da função aplicáveis em relação à lista de argumentos determinada (§12.6.4.2). Se esse conjunto reduzido estiver vazio, ocorrerá um erro de tempo de compilação.
- Em seguida, o conjunto reduzido de membros candidatos é agrupado de acordo com o tipo declarador. Em cada grupo:
- Os membros da função candidata são ordenados por overload_resolution_priority. Se o membro for uma substituição, a overload_resolution_priority vem da declaração menos derivada desse membro.
- Todos os membros que têm uma overload_resolution_priority inferior à mais alta encontrada em seu grupo de tipos declarantes são removidos.
- Os grupos reduzidos são recombinados no conjunto final de membros da função candidata aplicável.
- Em seguida, o melhor membro da função do conjunto de membros da função candidata aplicável é posicionado. Se o conjunto contiver apenas um membro de função, esse membro da função será o melhor membro da função. Caso contrário, o melhor membro da função é o membro de uma função que é melhor do que todos os outros membros da função em relação à lista de argumentos fornecida, desde que cada membro da função seja comparado a todos os outros membros da função usando-se as regras em §12.6.4.3. Se não existir exatamente um membro da função que seja melhor do que todos os outros, a invocação do membro da função será ambígua e ocorrerá um erro de tempo de vinculação.
Por exemplo, esse recurso faria com que o seguinte trecho de código exibisse "Span" em vez de "Array":
using System.Runtime.CompilerServices;
var d = new C1();
int[] arr = [1, 2, 3];
d.M(arr); // Prints "Span"
class C1
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority
public void M(int[] a) => Console.WriteLine("Array");
}
O efeito dessa alteração é que, como a partição para tipos mais derivados, adicionamos uma partição final para prioridade de resolução de sobrecarga. Como essa partição ocorre no final do processo de resolução de sobrecarga, isso significa que um tipo base não pode tornar seus membros mais prioritários do que qualquer tipo derivado. Isso é intencional e impede que uma corrida competitiva ocorra em que um tipo base possa tentar ser sempre melhor do que um tipo derivado. Por exemplo:
using System.Runtime.CompilerServices;
var d = new Derived();
d.M([1, 2, 3]); // Prints "Derived", because members from Base are not considered due to finding an applicable member in Derived
class Base
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Base");
}
class Derived : Base
{
public void M(int[] a) => Console.WriteLine("Derived");
}
Números negativos têm permissão para serem usados e podem ser usados para marcar uma sobrecarga específica como pior do que todas as outras sobrecargas padrão.
A overload_resolution_priority de um membro vem da declaração menos derivada desse membro. A overload_resolution_priority não é herdada ou inferida de membros de interface que um membro do tipo pode implementar e, dependendo do membro Mx
que implementa um membro da interface, Mi
, nenhum aviso é emitido se Mx
e Mi
tiverem overload_resolution_priorities diferentes.
NB: A intenção dessa regra é replicar o comportamento do modificador
params
.
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
Apresentamos o seguinte atributo ao BCL:
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
public int Priority => priority;
}
Todos os métodos em C# têm um overload_resolution_priority padrão 0, salvo quando são atribuídos com OverloadResolutionPriorityAttribute
. Se eles forem atribuídos com esse atributo, sua overload_resolution_priority será o valor inteiro fornecido ao primeiro argumento do atributo.
É um erro aplicar OverloadResolutionPriorityAttribute
aos seguintes locais:
- Propriedades não indexadoras
- Acessadores de propriedade, indexador ou evento
- Operadores de conversão
- Lambdas
- Funções locais
- Finalizadores
- Construtores estáticos
Os atributos encontrados nesses locais em metadados são ignorados por C#.
É um erro aplicar OverloadResolutionPriorityAttribute
em um local que seria ignorado, como em uma substituição de um método base, pois a prioridade é lida da declaração menos derivada de um membro.
Observação: isso difere intencionalmente do comportamento do modificador do
params
, que permite a respecificação ou adição quando ignorado.
Capacidade de chamada de membros
Uma ressalva importante para OverloadResolutionPriorityAttribute
é que ele pode tornar determinados membros efetivamente inacessíveis a partir do código-fonte. Por exemplo:
using System.Runtime.CompilerServices;
int i = 1;
var c = new C3();
c.M1(i); // Will call C3.M1(long), even though there's an identity conversion for M1(int)
c.M2(i); // Will call C3.M2(int, string), even though C3.M1(int) has less default parameters
class C3
{
public void M1(int i) {}
[OverloadResolutionPriority(1)]
public void M1(long l) {}
[Conditional("DEBUG")]
public void M2(int i) {}
[OverloadResolutionPriority(1), Conditional("DEBUG")]
public void M2(int i, [CallerArgumentExpression(nameof(i))] string s = "") {}
public void M3(string s) {}
[OverloadResolutionPriority(1)]
public void M3(object o) {}
}
Para estes exemplos, as sobrecargas de prioridade padrão efetivamente se tornam vestigiais e só podem ser chamadas por meio de algumas etapas que demandam um esforço adicional.
- Converta o método em um delegado e, em seguida, use esse delegado.
- Para alguns cenários de variação de tipo de referência, como
M3(object)
priorizado sobreM3(string)
, essa estratégia falhará. - Métodos condicionais, como
M2
, também não podem ser chamados com essa estratégia, pois não podem ser convertidos em delegados.
- Para alguns cenários de variação de tipo de referência, como
- Use o recurso de runtime
UnsafeAccessor
para chamá-lo por meio da assinatura correspondente. - Usar manualmente a reflexão para obter uma referência ao método e, em seguida, invocá-lo.
- O código que não for recompilado continuará chamando métodos antigos.
- O IL manuscrito pode especificar o que quiser.
Perguntas em aberto
Agrupamento de métodos de extensão (respondido)
Da forma em que escritos atualmente, os métodos de extensão são ordenados por prioridade somente dentro de seu próprio tipo. Por exemplo:
new C2().M([1, 2, 3]); // Will print Ext2 ReadOnlySpan
static class Ext1
{
[OverloadResolutionPriority(1)]
public static void M(this C2 c, Span<int> s) => Console.WriteLine("Ext1 Span");
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext1 ReadOnlySpan");
}
static class Ext2
{
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext2 ReadOnlySpan");
}
class C2 {}
Ao fazer a resolução de sobrecarga para membros de extensão, não devemos ordenar pelo tipo declarado e, em vez disso, considerar todas as extensões no mesmo escopo?
Resposta
Sempre faremos agrupamentos. O exemplo acima imprimirá Ext2 ReadOnlySpan
Herança de atributo em substituições (respondida)
O atributo deve ser herdado? Se não, qual é a prioridade do membro substituto?
Se o atributo for especificado em um membro virtual, uma substituição desse membro deverá ser necessária para repetir o atributo?
Resposta
O atributo não será marcado como herdado. Examinaremos a declaração de menor derivação de um membro para determinar sua prioridade de resolução de sobrecarga.
Erro ou aviso de aplicação na substituição (respondido)
class Base
{
[OverloadResolutionPriority(1)] public virtual void M() {}
}
class Derived
{
[OverloadResolutionPriority(2)] public override void M() {} // Warn or error for the useless and ignored attribute?
}
O que devemos fazer na aplicação de um OverloadResolutionPriorityAttribute
em um contexto em que ele é ignorado, como uma substituição:
- Não faça nada, deixe-o ser ignorado silenciosamente.
- Emita um aviso de que o atributo será ignorado.
- Emita um erro informando que o atributo não é permitido.
3 é a abordagem mais cautelosa, se considerarmos que pode haver um espaço no futuro em que talvez queiramos permitir uma substituição para especificar esse atributo.
Resposta
Vamos utilizar 3 e bloquear o uso em locais onde seria ignorado.
Implementação de interface implícita (respondida)
Qual deve ser o comportamento de uma implementação de interface implícita? Deve ser necessário especificar OverloadResolutionPriority
? Qual deve ser o comportamento do compilador quando ele encontra uma implementação implícita sem prioridade? Isso quase certamente acontecerá, pois uma biblioteca de interfaces pode ser atualizada, mas não uma implementação. O segredo com params
é não especificar e não carregar o valor:
using System;
var c = new C();
c.M(1, 2, 3); // error CS1501: No overload for method 'M' takes 3 arguments
((I)c).M(1, 2, 3);
interface I
{
void M(params int[] ints);
}
class C : I
{
public void M(int[] ints) { Console.WriteLine("params"); }
}
Nossas opções são:
- Siga
params
.OverloadResolutionPriorityAttribute
não será carregado implicitamente nem será necessário especificá-lo. - Carregue o atributo implicitamente.
- Não carregar o atributo implicitamente, exigir que ele seja especificado no site de chamada.
- Isso levanta uma pergunta extra: qual deverá ser o comportamento quando o compilador encontrar esse cenário com referências compiladas?
Resposta
Vamos com 1.
Outros erros de aplicativo (respondidos)
Há mais alguns locais como este que precisam ser confirmados. Elas incluem:
- Operadores de conversão – A especificação nunca diz que os operadores de conversão passam pela resolução de sobrecarga, portanto, a implementação impede o uso desses membros. Isso deve ser confirmado?
- Lambdas – Da mesma forma, lambdas nunca estão sujeitas à resolução de sobrecarga, portanto, são bloqueadas pela implementação. Isso deve ser confirmado?
- Destruidores – novamente, bloqueados no momento.
- Construtores estáticos – novamente, bloqueados no momento.
- Funções locais - Elas não estão bloqueadas no momento, pois passam por resolução de sobrecarga, e você simplesmente não pode sobrecarregá-las. Isso é semelhante à forma como não ocorre erro quando o atributo é aplicado a um membro de um tipo que não possui sobrecarga. Esse comportamento deve ser confirmado?
Resposta
Todos os locais listados acima são bloqueados.
Comportamento de Langversion (respondido)
No momento, a implementação só emite erros de langversion quando OverloadResolutionPriorityAttribute
é aplicado, mas não com ou, a menos que realmente influencie algo. Essa decisão foi tomada porque há APIs que o BCL adicionará (agora e ao longo do tempo) que começarão a usar esse atributo. Se o usuário definir manualmente a versão do idioma como C# 12 ou versões anteriores, ele poderá ver esses membros e, dependendo do nosso comportamento de langversion, pode ser:
- Se ignorarmos o atributo em C# <13, encontraremos um erro de ambiguidade, porque a API é verdadeiramente ambígua sem o atributo.
- Se ocorrer um erro quando o atributo tiver afetado o resultado, encontraremos um erro que torna a API inutilizável. Isso será especialmente ruim porque
Debug.Assert(bool)
está sendo despriorizado em .NET 9 ou; - Se alterarmos a resolução de maneira silenciosa, podemos enfrentar comportamentos potencialmente diferentes entre versões distintas do compilador, caso uma compreenda o atributo e a outra não.
O último comportamento foi escolhido porque proporciona maior compatibilidade futura, mas o resultado alterado pode ser surpreendente para alguns usuários. Devemos confirmar isso ou escolher uma das outras opções?
Resposta
Vamos escolher a opção 1, ignorando silenciosamente o atributo em versões anteriores da linguagem.
Alternativas
Uma proposta anterior tentou especificar uma abordagem BinaryCompatOnlyAttribute
, que foi muito pesada na exclusão de itens da visibilidade. No entanto, isso apresenta muitos problemas de implementação difíceis, que podem tornar a proposta muito forte para ser útil (impedindo, por exemplo, o teste de APIs antigas) ou tão fraca que perdem alguns dos objetivos originais (como a capacidade de ter uma API que, de outra forma, seria considerada ambígua, chamando uma nova API). Essa versão está replicada abaixo.
Proposta de BinaryCompatOnlyAttribute (obsoleto)
BinaryCompatOnlyAttribute
Projeto detalhado
System.BinaryCompatOnlyAttribute
Introduzimos um novo atributo reservado:
namespace System;
// Excludes Assembly, GenericParameter, Module, Parameter, ReturnValue
[AttributeUsage(AttributeTargets.Class
| AttributeTargets.Constructor
| AttributeTargets.Delegate
| AttributeTargets.Enum
| AttributeTargets.Event
| AttributeTargets.Field
| AttributeTargets.Interface
| AttributeTargets.Method
| AttributeTargets.Property
| AttributeTargets.Struct,
AllowMultiple = false,
Inherited = false)]
public class BinaryCompatOnlyAttribute : Attribute {}
Quando aplicado a um membro de tipo, esse membro é tratado como inacessível em qualquer local pelo compilador, o que significa que ele não contribui para a pesquisa de membros, resolução de sobrecargas ou qualquer outro processo similar.
Domínios de acessibilidade
Atualizamos domínios de acessibilidade do §7.5.3 conforme segue:
O domínio de acessibilidade de um membro consiste nas seções (possivelmente disjuntas) do texto do programa nas quais o acesso ao membro é permitido. Para determinar o domínio de acessibilidade de um membro, considera-se que um membro é de nível superior se não for declarado dentro de um tipo, enquanto um membro é considerado aninhado se for declarado dentro de outro tipo. Além disso, o texto do programa de um programa é definido como todo o texto contido em todas as unidades de compilação do programa, e o texto do programa de um tipo é definido como todo o texto contido nas type_declaration desse tipo (incluindo, possivelmente, tipos que estão aninhados dentro dele).
O domínio de acessibilidade de um tipo predefinido (como
object
,int
oudouble
) é ilimitado.O domínio de acessibilidade de um tipo não vinculado de nível superior
T
(§8.4.4) que é declarado em um programaP
é definido da seguinte forma:
- Se
T
estiver marcado comBinaryCompatOnlyAttribute
, o domínio de acessibilidade deT
será totalmente inacessível ao texto do programa deP
e qualquer programa que faz referência aP
.- Se a acessibilidade declarada de
T
for pública, o domínio de acessibilidade deT
será o texto do programa deP
e qualquer programa que faz referência aP
.- Se a acessibilidade declarada de
T
for interna, o domínio de acessibilidade deT
será o texto do programa deP
.Observação: a partir dessas definições, conclui-se que o domínio de acessibilidade de um tipo não vinculado de nível superior é sempre, no mínimo, o texto do programa no qual esse tipo é declarado. fim da observação
O domínio de acessibilidade para um tipo construído
T<A₁, ..., Aₑ>
é a interseção do domínio de acessibilidade do tipo genérico não vinculadoT
e os domínios de acessibilidade dos argumentos de tipoA₁, ..., Aₑ
.O domínio de acessibilidade de um membro aninhado
M
declarado em um tipoT
dentro de um programaP
é definido da seguinte maneira (observando queM
pode ser um tipo em si):
- Se
M
estiver marcado comBinaryCompatOnlyAttribute
, o domínio de acessibilidade deM
será totalmente inacessível ao texto do programa deP
e qualquer programa que faz referência aP
.- Se a acessibilidade declarada de
M
forpublic
, então o domínio de acessibilidade deM
será igual ao deT
.- Se a acessibilidade declarada de
M
forprotected internal
, aD
será a união do texto do programa deP
com o texto do programa de qualquer tipo derivado deT
, declarado fora deP
. O domínio de acessibilidade deM
é a interseção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
forprivate protected
, aD
será a interseção do texto do programa deP
, do texto do programa deT
e de qualquer tipo derivado deT
. O domínio de acessibilidade deM
é a interseção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
forprotected
, aD
será a união do texto do programa deT
e o texto do programa de qualquer tipo derivado deT
. O domínio de acessibilidade deM
é a interseção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
forinternal
, o domínio de acessibilidade deM
será a interseção do domínio de acessibilidade deT
com o texto de programa deP
.- Se a acessibilidade declarada de
M
forprivate
, o domínio de acessibilidade deM
será o texto de programa deT
.
O objetivo dessas adições é possibilitar que os membros marcados com BinaryCompatOnlyAttribute
estejam completamente inacessíveis a qualquer local, não participem da pesquisa de membros e não possam afetar o restante do programa. Isso significa que eles não podem implementar membros da interface, não podem chamar uns aos outros e não podem ser substituídos (métodos virtuais), ocultados ou implementados (membros da interface). Se isso é excessivamente restritivo é o tema de várias perguntas em aberto abaixo.
Perguntas não resolvidas
Métodos virtuais e substituição
O que fazemos quando um método virtual é marcado como BinaryCompatOnly
? As substituições em uma classe derivada podem nem estar no assembly atual, e pode ser que o usuário esteja procurando introduzir uma nova versão de um método que, por exemplo, só difere pelo tipo de retorno, algo para o qual o C# normalmente não permite sobrecarga de métodos. O que acontece com quaisquer substituições desse método anterior na recompilação? Eles têm permissão para substituir o membro BinaryCompatOnly
se também estiverem marcados como BinaryCompatOnly
?
Uso dentro da mesma DLL
Esta proposta afirma que os membros de BinaryCompatOnly
não estão visíveis em nenhum lugar, nem mesmo no assembly que está sendo compilado no momento. Isso é muito rigoroso, ou os membros de BinaryCompatAttribute
precisam se conectar uns aos outros?
Implementar membros de interface de forma implícita
Os membros de BinaryCompatOnly
deveriam poder implementar membros da interface? Ou isso deveria ser impedido? Isso exigiria que, quando um usuário quisesse transformar uma implementação de interface implícita em BinaryCompatOnly
, ele também teria que fornecer uma implementação de interface explícita, provavelmente clonando o corpo do membro BinaryCompatOnly
, já que a implementação da interface explícita não seria mais capaz de ver o membro original.
Implementar membros da interface marcados com BinaryCompatOnly
O que fazemos quando um membro de interface é marcado como BinaryCompatOnly
? O tipo ainda precisa fornecer uma implementação para esse membro; pode ser que devêssemos simplesmente afirmar que membros de interface não podem ser marcados como BinaryCompatOnly
.
C# feature specifications