Sobrecarga é uma funcionalidade presente em muitas linguagens de programação, permitindo que múltiplos métodos (ou funções) compartilhem o mesmo nome, desde que suas assinaturas difiram. A assinatura de um método é definida pelo número, tipo e ordem de seus parâmetros. Mas como o compilador C# decide qual método chamar quando há várias sobrecargas disponíveis?
Considere o seguinte exemplo para ilustrar o mecanismo de decisão:
void ExemploPrincipal()
{
char caractereInicial = 'L';
Cachorro meuCachorro = new Cachorro();
meuCachorro.EmitirSom(caractereInicial); // Chama EmitirSom(int) após conversão implícita
}
class Animal
{
public void EmitirSom(char somInicial)
{
Console.WriteLine($"Som do Animal (char): {somInicial}");
}
}
class Cachorro : Animal
{
public void EmitirSom(int vezesLatido)
{
Console.WriteLine($"Latido do Cachorro (int): {vezesLatido}");
}
}
Neste cenário, a classe base Animal possui um método EmitirSom que aceita um parâmetro char. A classe derivada Cachorro introduz uma sobrecarga para EmitirSom que recebe um int. Ao instanciar Cachorro e invocar EmitirSom com um valor char ('L'), a saída pode ser Latido do Cachorro (int): 76 (76 é o valor ASCII para 'L'), em vez de Som do Animal (char): L. Por que o método da classe base, que possui uma correspondência de tipo exata, não foi escolhido, levando a uma conversão implícita do argumento?
A respsota reside no mecanismo de resolução de sobrecarga do C#. Este não é um caso de inferência de tipo (que é geralmente aplicada a tipos implícitos e genéricos), mas sim um processo de tempo de compilação que seleciona o "melhor" membro de função para ser invocado, com base na lista de argumentos fornecidos e um conjunto de métodos candidatos.
O C# suporta sobrecarga para os seguintes tipos de membros:
- Métodos (funções)
- Construtores
- Indexadores
- Operadores
Embora a sintaxe para definir sobrecargas possa variar entre esses membros, o processo subjacente de resolução para selecionar o membro mais adequado é consistente.
Processo de Resolução de Sobrecarga
A decisão de sobrecarga segue duas etapas principais:
- Identificação de Membros Aplicáveis: Primeiramente, o compilador examina o conjunto de membros candidatos e identifica aqueles que são "aplicáveis" à lista de argumentos fornecida. Se nenhum membro aplicável for encontrado, um erro de compilação é gerado.
- Seleção do Melhor Membro: Dentre os membros aplicáveis, o compilador tenta determinar o "melhor" membro. Se apenas um membro for aplicável, ele é obviamente o melhor. Caso contrário, o compilador aplica um conjunto de regras para encontrar o membro que é superior a todos os outros. Se não houver um único membro que seja estritamente melhor do que todos os outros, a chamada é considerada ambígua, resultando em um erro de compilação.
É importante notar, conforme a Especificação da Linguagem C#, que métodos marcados com
overridenão são considerados no conjunto de candidatos para uma chamada de método. Além disso, se um método aplicável for encontrado em uma classe derivada, os métodos da classe base com o mesmo nome não são considerados candidatos adicionais para aquela sobrecarga específica. Isso explica por que, no exemplo doCachorro/Animal, o métodoEmitirSom(char)da classeAnimalnão foi escolhido uma vez queEmitirSom(int)da classeCachorroera aplicável (via conversão implícita decharparaint).
Membros de Função Aplicáveis
Um membro de função é considerado "aplicável" a uma lista de argumentos A se todas as condições a seguir forem satisfeitas:
- Cada argumento em
Acorresponde a um parâmetro na declaração do membro da função. Cada parâmetro é correspondido por no máximo um argumento. Quaisquer parâmetros sem um argumento correspondente devem ser parâmetros opcionais. - Para cada argumento em
A, o modo de passagem do argumento deve ser compatível com o modo de passagem do parâmetro correspondente:- Para parâmetros de valor ou arrays de parâmetros (
params), uma conversão implícita deve existir do tipo do argumento para o tipo do parâmetro. - Para parâmetros modificados por
refouout, deve existir uma conversão de identidade do tipo do argumento para o tipo do parâmetro (ou seja, os tipos devem ser exatamente os mesmos e o argumento deve ser uma variável). - Para parâmetros modificados por
in, deve existir uma conversão de identidade do tipo do argumento para o tipo do parâmetro (ou seja, os tipos devem ser exatamente os mesmos e o argumento deve ser uma variável). - Se um parâmetro
infor chamado sem o modificadorin(passado por valor), uma conversão implícita deve existir do tipo do argumento para o tipo do parâmetro.
- Para parâmetros de valor ou arrays de parâmetros (
Vamos exemplificar a aplicabilidade com o seguinte código:
void ExemploDeAplicabilidade()
{
int valInt = 10;
uint valUint = 34U;
var valVar = 5; // Tipo inferido como int
dynamic valDynamic = 5;
// Cenário 1: Processar(in int) e Processar(int)
Processar(in valInt); // Chamada para Processar(in int p1) é aplicável (correspondência exata 'in int')
// Processar(in valUint); // Erro de compilação: 'uint' não pode ser convertido para 'in int' (requer conversão de identidade, 'uint' != 'int')
Processar(valInt); // Ambas Processar(int) e Processar(in int) são aplicáveis (implicitamente 'in int' pode receber um 'int')
// Processar(valUint); // Erro de compilação: 'uint' não tem conversão implícita para 'int', então Processar(int) não é aplicável.
// Nem Processar(in int) é aplicável sem 'in'.
Processar(in valVar); // Chamada para Processar(in int p1) é aplicável (valVar é 'int', conversão de identidade)
Console.WriteLine("---");
// Cenário 2: Manipular(long) e Manipular(in object)
Manipular(valUint); // Manipular(long) é aplicável (uint para long tem conversão implícita);
// Manipular(in object) também é aplicável (uint para object tem boxing implícito, sem 'in' permite)
// Manipular(valDynamic); // Não é aplicável em tempo de compilação sem 'in'
Manipular(in valDynamic); // Chamada para Manipular(in object p1) é aplicável (conversão de identidade para 'in object')
Manipular(in valVar); // Chamada para Manipular(in object p1) é aplicável (valVar é 'int', conversão de identidade para 'in object')
Console.WriteLine("---");
// Cenário 3: Atualizar(ref object)
object obj = "Teste";
// Atualizar(valUint); // Erro de compilação: 'ref' e 'out' exigem correspondência exata do modificador e tipo.
// 'uint' não pode ser passado como 'ref object'.
Atualizar(ref obj); // Chamada para Atualizar(ref object p1) é aplicável (correspondência exata)
}
public static void Processar(int p1) { Console.WriteLine("Processar (int): " + p1); }
public static void Processar(in int p1) { Console.WriteLine("Processar (in int): " + p1); }
public static void Manipular(long p1) { Console.WriteLine("Manipular (long): " + p1); }
public static void Manipular(in object p1) { Console.WriteLine("Manipular (in object): " + p1); }
public static void Atualizar(ref object p1) { Console.WriteLine("Atualizar (ref object): " + p1); }
O Melhor Membro de Função
Dadas duas funções aplicáveis, digamos M_v(P₁, P₂, ..., P_n) e M_x(Q₁, Q₂, ..., Q_n), e uma lista de argumentos {E₁, E₂, ..., E_n}, M_v é considerada "melhor" do que M_x se:
- Para cada argumento, a conversão implícita de
E_iparaQ_inão é melhor do que a conversão implícita deE_iparaP_i. - E, para pelo menos um argumento, a conversão implícita de
E_iparaP_ié estritamente melhor do que a conversão implícita deE_iparaQ_i.
Se, após esta comparação, M_v e M_x forem consideradas equivalentes (por exemplo, todas as conversões de argumento são de identidade para ambos os métodos), critérios adicionais são aplicados:
- Um método não genérico é preferido a um método genérico.
- Um método regular (definido diretamente) é preferido a um método de extensão.
- Se ambos forem métodos de extensão, o método com menos parâmetros implícitos (e.g., ignorando o
thispara o primiero parâmetro de um método de extensão de instância) é preferido. - Se um método tiver parâmetros com tipos mais específicos do que os parâmetros correspondentes do outro método (por exemplo,
stringé mais específico queobject), ele é preferido.
Modos de Passagem de Parâmetros
Quando dois métodos sobrecarregados são idênticos em seus tipos de parâmetros, mas diferem apenas em seus modos de passagem (como um por valor e outro com in), por exemplo:
public static void ExemploPassagem(int param) { /* ... */ }
public static void ExemploPassagem(in int param) { /* ... */ }
Ao chamar ExemploPassagem(10), ambos os métodos são considerados aplicáveis. Neste caso específico, o modo de passagem de valor é considerado o melhor.
É crucial lembrar que os modos de passagem ref e out não permitem flexibilidade; eles exigem uma correspondência exata do modificador na chamada.