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. 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
Introduzimos um novo atributo, System.Runtime.CompilerServices.OverloadResolutionPriority
, que pode ser usado pelos autores de API para ajustar a prioridade relativa de sobrecargas dentro de 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 do C#.
Motivação
Os autores da API muitas vezes se deparam com um problema sobre o que fazer com um membro depois que ele foi obsoleto. Para fins de compatibilidade com versões anteriores, muitos manterão o membro existente com ObsoleteAttribute
configurado para erro em perpetuidade, de modo a evitar problemas para os utilizadores que executam binários atualizados em tempo de execução. Isso atinge particularmente os sistemas de plugins, onde o autor de um plugin não controla o ambiente em que o plugin é executado. O criador do ambiente pode querer 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 é visível na resolução de sobrecarga, e pode causar falhas indesejadas de resolução de sobrecarga quando há uma alternativa perfeitamente boa, mas essa alternativa é ambígua com o membro obsoleto, ou a presença do membro obsoleto faz com que a resolução de sobrecarga termine cedo sem nunca considerar o bom membro. Para isso, queremos ter uma maneira de os autores de API orientarem a resolução de sobrecarga na resolução da ambiguidade, para que possam evoluir suas áreas de superfície de API e orientar os usuários para APIs de alto desempenho sem ter que comprometer a experiência do usuário.
A equipe de BCL (Base Class Libraries) tem vários exemplos de onde isso pode ser útil. Alguns exemplos (hipotéticos) são:
- Criar uma sobrecarga de
Debug.Assert
que usaCallerArgumentExpression
para obter a expressão que está a ser afirmada, para que ela possa ser incluída na mensagem, e fazer com que seja preferida em relação à sobrecarga existente. - Fazendo com que
string.IndexOf(string, StringComparison = Ordinal)
seja preferido em vez destring.IndexOf(string)
. Isto teria que ser discutido como uma possível mudança de rutura, mas há algumas opiniões de que é o melhor padrão e mais provável de ser o que o utilizador pretendia. - Uma combinação desta proposta e
CallerAssemblyAttribute
permitiria que métodos com uma identidade de chamador implícita evitassem percursos de pilha dispendiosos.Assembly.Load(AssemblyName)
faz isso hoje, e poderia ser muito mais eficiente. -
Microsoft.Extensions.Primitives.StringValues
expõe uma conversão implícita parastring
estring[]
. Isto significa que é ambíguo quando passado a um método com as sobrecargasparams string[]
eparams ReadOnlySpan<string>
. Este atributo poderia ser usado para priorizar uma das sobrecargas para evitar a ambiguidade.
Design Detalhado
Prioridade de resolução de sobrecarga
Definimos um novo conceito, prioridade_de_resolução_de_sobrecarga, que é utilizado no 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 uma overload_resolution_priority de 0 por padrão, e isso pode ser alterado aplicando OverloadResolutionPriorityAttribute
a um método. Atualizamos a seção §12.6.4.1 da especificação C# da seguinte forma (alteração em negrito ):
Uma vez identificados os membros da função candidata e a lista de argumentos, a seleção do melhor membro da função é a mesma em todos os casos:
- Em primeiro lugar, o conjunto de membros da função candidata é reduzido aos membros da função que são aplicáveis em relação à lista de argumentos dada (§12.6.4.2). Se esse conjunto reduzido estiver vazio, ocorrerá um erro em tempo de compilação.
- Em seguida, o conjunto reduzido de membros candidatos é agrupado por tipo de declaração. Dentro de cada grupo:
- Os membros da função candidata são ordenados por prioridade_de_resolução_de_sobrecarga. Se o membro for uma substituição, o overload_resolution_priority vem da declaração menos derivada desse membro.
- Todos os membros que têm um overload_resolution_priority menor do que o mais alto encontrado dentro do respetivo grupo de tipos declarantes devem ser removidos.
- Os grupos reduzidos são então recombinados no conjunto final de membros das funções candidatas aplicáveis.
- Em seguida, a melhor função do conjunto de funções candidatas aplicáveis é localizada. Se o conjunto contiver apenas um membro da 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 da função que é melhor do que todos os outros membros da função em relação à lista de argumentos dada, desde que cada membro da função seja comparado com todos os outros membros da função usando as regras §12.6.4.3. Se não houver exatamente um membro da função que se destaque entre os outros, então a invocação do membro da função é ambígua e ocorre um erro em tempo de vinculação.
Como exemplo, esse recurso faria com que o seguinte trecho de código imprimisse "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 poda para a maioria dos tipos derivados, adicionamos uma poda final para prioridade de resolução de sobrecarga. Como essa poda ocorre no final do processo de resolução de sobrecarga, isso significa que um tipo base não pode dar aos seus membros uma prioridade mais alta do que qualquer tipo derivado. Isso é intencional e evita que ocorra uma corrida armamentista em que um tipo de base pode 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 podem ser 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 provém da declaração menos derivada desse membro.
overload_resolution_priority não é herdada ou inferida de nenhum membro da interface que um membro de tipo possa implementar, e dado um membro Mx
que implementa um membro da interface Mi
, nenhum aviso será emitido se Mx
e Mi
tiverem diferentes overload_resolution_priority.
NB: A intenção desta regra é replicar o comportamento do modificador de
params
.
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
Introduzimos o seguinte atributo à 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 uma overload_resolution_priority padrão de 0, a menos que sejam marcados com OverloadResolutionPriorityAttribute
. Se eles são atribuídos com esse atributo, então seu overload_resolution_priority é o valor inteiro fornecido para o primeiro argumento do atributo.
É um erro aplicar OverloadResolutionPriorityAttribute
aos seguintes locais:
- Propriedades não indexadoras
- Propriedade, indexador ou acessadores de eventos
- Operadores de conversão
- Lambdas
- Funções locais
- Finalizadores
- Construtores estáticos
Os atributos encontrados nesses locais nos metadados são ignorados pelo 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 a partir da declaração menos derivada de um membro.
NB: Isso difere intencionalmente do comportamento do modificador de
params
, que permite reespecificar e/ou adicionar quando ignorado.
Chamabilidade dos membros
Uma ressalva importante para OverloadResolutionPriorityAttribute
é que pode tornar certos membros efetivamente inacessíveis de determinada origem. 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ó são acessíveis através de etapas adicionais que exigem algum esforço extra.
- Converter o método em um delegado e, em seguida, usar esse delegado.
- Para alguns cenários de variância de tipo de referência, como
M3(object)
priorizado sobreM3(string)
, essa estratégia falhará. - Métodos condicionais, como
M2
, também não seriam utilizáveis com esta estratégia, pois os métodos condicionais não podem ser convertidos em delegates.
- Para alguns cenários de variância de tipo de referência, como
- Usando o recurso de tempo de execução
UnsafeAccessor
para chamá-lo por meio de assinatura correspondente. - Usando manualmente a reflexão para obter uma referência ao método e, em seguida, invocando-o.
- O código que não for recompilado continuará a chamar métodos antigos.
- A IL manuscrita pode especificar o que quiser.
Perguntas abertas
Agrupamento de métodos de extensão (respondido)
Tal como está redigido atualmente, os métodos de extensão são ordenados por prioridade apenas no âmbito do 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 resolver sobrecarga para membros de extensão, não deveríamos classificar pelo tipo declarado e, em vez disso, considerar todas as extensões dentro do mesmo escopo?
Resposta
Vamos sempre agrupar. O exemplo acima imprimirá Ext2 ReadOnlySpan
Herança de atributos nas substituições (respondido)
O atributo deve ser herdado? Em caso negativo, qual é a prioridade do membro superior?
Se o atributo for especificado em um membro virtual, uma substituição desse membro deve ser necessária para repetir o atributo?
Resposta
O atributo não será marcado como herdado. Analisaremos a declaração menos derivada de um membro para determinar sua prioridade de resolução de sobrecarga.
Erro de aplicação ou aviso 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 pensarmos que pode haver um espaço no futuro onde podemos querer permitir uma substituição para especificar esse atributo.
Resposta
Vamos escolher o 3, e vamos bloquear a aplicação nos locais que seriam ignorados.
Implementação de interface implícita (respondida)
Qual deve ser o comportamento de uma implementação de interface implícita? Deve ser exigido que especifique OverloadResolutionPriority
? Qual deve ser o comportamento do compilador quando encontra uma implementação implícita sem prioridade? Isso quase certamente acontecerá, pois uma biblioteca de interface pode ser atualizada, mas não uma implementação. A arte prévia aqui com params
é não especificar, e não transferir 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"); }
}
As nossas opções são:
- Siga
params
.OverloadResolutionPriorityAttribute
não será implicitamente transitado nem terá de ser especificado. - Transfira o atributo implicitamente.
- Não transfira o atributo implicitamente, exija que ele seja especificado no site de chamada.
- Isso traz uma pergunta extra: qual deve ser o comportamento quando o compilador encontra esse cenário com referências compiladas?
Resposta
Vamos com 1.
Outros erros de aplicação (Respondido)
Há mais alguns locais como este que precisam ser confirmados. Entre eles contam-se:
- Operadores de conversão - A especificação nunca diz que os operadores de conversão passam por resolução de sobrecarga, então a implementação bloqueia a aplicação nesses membros. Isso deve ser confirmado?
- Lambdas - Da mesma forma, as lambdas nunca estão sujeitas à resolução de sobrecarga, então a implementação as bloqueia. Isso deve ser confirmado?
- Destruidores - novamente, atualmente bloqueados.
- Construtores estáticos - novamente, atualmente bloqueados.
- Funções locais - Estas não estão atualmente bloqueadas, porque sofrem resolução de sobrecarga, você simplesmente não pode sobrecarregá-las. Isso é semelhante a como não erramos quando o atributo é aplicado a um membro de um tipo que não está sobrecarregado. Esse comportamento deve ser confirmado?
Resposta
Todos os locais listados acima estão bloqueados.
Comportamento de Langversion (Respondido)
A implementação atualmente só emite erros de langversion quando
- Se ignorarmos o atributo em C# <13, depararemos com um erro de ambiguidade porque a API é verdadeiramente ambígua sem o atributo, ou;
- Se cometemos um erro quando o atributo afeta o resultado, enfrentamos um erro que torna a API inutilizável. Isso será especialmente ruim porque
Debug.Assert(bool)
está sendo despriorizado no .NET 9 ou; - Se mudarmos silenciosamente a resolução, encontraremos um comportamento potencialmente diferente entre diferentes versões do compilador se um entender o atributo e outro não.
O último comportamento foi escolhido, uma vez que resulta na maior compatibilidade futura, mas o resultado alterado pode ser surpreendente para alguns utilizadores. Devemos confirmá-lo ou devemos escolher uma das outras opções?
Resposta
Optaremos pela opção 1, ignorando silenciosamente o atributo nas versões linguísticas anteriores.
Alternativas
Uma proposta anterior tentou especificar uma abordagem BinaryCompatOnlyAttribute
, que foi muito restritiva na remoção de elementos da visibilidade. No entanto, isso tem muitos problemas de implementação difíceis que significam que a proposta é muito forte para ser útil (impedindo o teste de APIs antigas, por exemplo) ou tão fraca que perdeu alguns dos objetivos originais (como ser capaz de ter uma API que de outra forma seria considerada ambígua chamar uma nova API). Essa versão é replicada abaixo.
Proposta de BinaryCompatOnlyAttribute (obsoleta)
BinaryCompatOnlyAttribute
Design 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 tipo, esse membro é tratado como inacessível em todos os locais pelo compilador, o que significa que ele não contribui para a pesquisa de membros, resolução de sobrecarga ou qualquer outro processo semelhante.
Domínios de Acessibilidade
Atualizamos §7.5.3 Domínios de acessibilidade como segue:
O domínio de acessibilidade de um membro consiste nas seções (possivelmente disjuntas) do texto do programa em que o acesso ao membro é permitido. Para fins de definição do domínio de acessibilidade de um membro, diz-se que um membro é de nível superior se não for declarado dentro de um tipo, e um membro é dito ser aninhado se for declarado dentro de outro tipo. Além disso, o texto do 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 declarações_de_tipo desse tipo (incluindo, possivelmente, tipos que estão aninhados dentro do tipo). O domínio de acessibilidade de um tipo predefinido (como
object
,int
oudouble
) é ilimitado.O domínio de acessibilidade de um tipo não acoplado 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
é completamente inacessível para o texto do programa deP
e qualquer programa que faça referênciaP
.- Se a acessibilidade declarada de
T
é pública, o domínio de acessibilidade deT
é o texto do programa deP
e qualquer programa que faça referênciaP
.- Se a acessibilidade declarada de
T
é interna, o domínio de acessibilidade deT
é o texto do programa deP
.Nota: A partir dessas definições, segue-se que o domínio de acessibilidade de um tipo não vinculado de nível superior é sempre pelo menos o texto do programa no qual esse tipo é declarado. nota final
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 acopladoT
e os domínios de acessibilidade dos argumentos de tipoA₁, ..., Aₑ
.O domínio de acessibilidade de um membro aninhado
M
declarado num tipoT
dentro de um programaP
é definido da seguinte forma (observando queM
ele próprio pode ser um tipo):
- Se
M
estiver marcado comBinaryCompatOnlyAttribute
, o domínio de acessibilidade deM
é completamente inacessível para o texto do programa deP
e qualquer programa que faça referênciaP
.- Se a acessibilidade declarada de
M
épublic
, o domínio de acessibilidade deM
é o domínio de acessibilidade deT
.- Se a acessibilidade declarada de
M
forprotected internal
, deixeD
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 intersecção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
éprivate protected
, entãoD
será a interseção entre o texto do programa deP
, o texto do programa deT
, e qualquer tipo derivado deT
. O domínio de acessibilidade deM
é a intersecção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
éprotected
, deixeD
ser a união do texto do programa deT
com o texto do programa de qualquer tipo derivado deT
. O domínio de acessibilidade deM
é a intersecção do domínio de acessibilidade deT
comD
.- Se a acessibilidade declarada de
M
éinternal
, o domínio de acessibilidade deM
é a interseção do domínio de acessibilidade deT
com o texto do programa deP
.- Se a acessibilidade declarada de
M
éprivate
, o domínio de acessibilidade deM
é o texto do programa deT
.
O objetivo dessas adições é fazer com que os membros marcados com BinaryCompatOnlyAttribute
sejam completamente inacessíveis a qualquer local, não participem da pesquisa de membros e não possam afetar o resto do programa. Consequentemente, isso significa que eles não podem implementar membros da interface, eles não podem chamar uns aos outros, e eles não podem ser substituídos (métodos virtuais), ocultos ou implementados (membros da interface). Se isto é demasiado rigoroso é o tema de várias questões em aberto abaixo.
Questões por resolver
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 por tipo de retorno, algo que o C# normalmente não permite sobrecarga. O que acontece com quaisquer substituições desse método anterior na recompilação? Podem substituir o membro BinaryCompatOnly
, caso também estejam marcados como BinaryCompatOnly
?
Use dentro da mesma DLL
Esta proposta estabelece que os membros de BinaryCompatOnly
não são visíveis em parte alguma, nem mesmo na assembleia que está a ser elaborada. Membros do BinaryCompatAttribute
precisam possivelmente de se ligar uns aos outros, ou isso é demasiado rigoroso?
Implementando implicitamente membros da interface
Devem os membros BinaryCompatOnly
ser capazes de implementar os membros da interface? Ou deveriam ser impedidos de o fazer. 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 mesmo corpo que o membro BinaryCompatOnly
, pois a implementação de interface explícita não seria mais capaz de ver o membro original.
Implementando membros da interface marcados BinaryCompatOnly
O que fazemos quando um membro da interface é marcado como BinaryCompatOnly
? O tipo ainda precisa fornecer uma implementação para esse membro; pode ser que tenhamos simplesmente de dizer que os membros da interface não podem ser marcados como BinaryCompatOnly
.
C# feature specifications