Orientação a objetos: Sobrescrita e Sobrecarga de métodos
Sobrescrita e sobrecarga são dois dos recursos mais importantes da orientação a objetos, porque permitem dar novas formas a métodos e propriedades sem perder a organização do código. Neste artigo, vamos ver na prática como cada um funciona no C#, com exemplos simples e os erros que o compilador aponta quando algo sai do lugar.
O que são sobrescrita e sobrecarga
Apesar dos nomes parecidos, sobrescrita e sobrecarga resolvem problemas diferentes. Por isso, vale separar bem os dois conceitos antes de partir para o código.
A sobrescrita (em inglês, override) acontece entre uma classe pai e uma classe filha. Em outras palavras, a classe derivada fornece uma nova versão de um método que já existe na classe base.
A sobrecarga (em inglês, overload) acontece dentro do mesmo tipo. Nesse caso, você declara vários métodos com o mesmo nome, mas com listas de parâmetros diferentes.
Uma forma simples de lembrar a diferença entre sobrescrita e sobrecarga é pensar no momento da decisão. Na sobrescrita, o runtime escolhe a versão do método conforme o tipo real do objeto, enquanto na sobrecarga o compilador escolhe a versão conforme os argumentos da chamada.
Se você ainda está começando com o paradigma, vale revisar antes o artigo sobre os pilares da orientação a objetos. Afinal, herança e polimorfismo são a base de tudo o que veremos aqui.
Sobrescrita de métodos no C#
Sobrescrever um método ou uma propriedade significa dar uma nova forma a esse membro, ou seja, criar uma nova versão dele em uma classe derivada. Vimos essa ideia no artigo anterior, quando falamos sobre os pilares da orientação a objetos.
No C#, a sobrescrita depende do uso conjunto dos modificadores virtual e override. Sempre que marcamos uma propriedade ou um método como virtual, indicamos que as classes filhas podem sobrescrever aquele membro.
Em seguida, a classe filha executa a sobrescrita com a palavra reservada override, escrita antes do tipo de retorno da propriedade ou do método. De acordo com a documentação oficial do modificador override, o método sobrescrito precisa ter a mesma assinatura do método base.
Partindo de uma classe base
Para contextualizar melhor, vamos tomar como base um objeto que possui uma finalidade. Neste exemplo, a classe Bottle representa uma garrafa genérica.
public class Bottle
{
public void Purpose()
{
Console.WriteLine("Generic bottle");
}
}
Embora esse objeto esteja bem materializado, um dos pontos fortes da orientação a objetos é criar novos objetos a partir de um objeto base. Como vimos no artigo anterior, esse conceito se chama herança.
Queremos, então, criar novos modelos de garrafa com características similares, mas com finalidades diferentes. Por exemplo, uma garrafa térmica continua sendo uma garrafa, porém o seu objetivo principal é manter a temperatura.
Marcando o método como virtual
Os métodos do C# não são virtuais por padrão. Por isso, precisamos definir o método Purpose como virtual para que as classes filhas possam sobrescrevê-lo depois.
public class Bottle
{
public virtual void Purpose()
{
Console.WriteLine("Generic bottle");
}
}
Dessa forma, uma nova garrafa com uma finalidade diferente pode ter um método com o mesmo nome, porém executando uma ação completamente diferente. Além disso, nada muda para quem já usa a classe Bottle diretamente.
Aplicando o override na classe filha
Para sobrescrever o método Purpose que está no objeto pai, utilizamos o modificador override na classe ThermalBottle.
public class ThermalBottle : Bottle
{
public override void Purpose()
{
Console.WriteLine("Keep the temperature");
}
}
Agora, toda instância de ThermalBottle executa a nova versão do método. Além disso, a assinatura continua idêntica, já que o nome e os parâmetros não mudaram.
Chamando a versão do pai com base
Ainda podemos, caso necessário, chamar o método Purpose do objeto pai com a palavra reservada base. Assim, a classe filha estende o comportamento original em vez de substituí-lo por completo.
public class ThermalBottle : Bottle
{
public override void Purpose()
{
// executa primeiro a versão da classe base
base.Purpose();
Console.WriteLine("Keep the temperature");
}
}
Nesse caso, a saída do programa seria a mostrada abaixo. Afinal, chamamos tanto o método Purpose do pai quanto o do filho.
Generic bottle
Keep the temperature
Sobrescrita, sobrecarga e polimorfismo
O grande valor da sobrescrita aparece quando usamos uma variável do tipo base para guardar um objeto do tipo derivado. Nesse cenário, o runtime verifica o tipo real do objeto e chama a versão mais derivada do método.
Bottle bottle = new ThermalBottle();
// imprime a versão sobrescrita, porque o objeto é uma ThermalBottle
bottle.Purpose();
Mesmo com a variável declarada como Bottle, o código executa a versão de ThermalBottle. De fato, esse é o comportamento que chamamos de polimorfismo em tempo de execução.
Já a sobrecarga representa o polimorfismo em tempo de compilação. Ou seja, sobrescrita e sobrecarga são duas faces do mesmo princípio, decididas em momentos diferentes.
Por outro lado, se nenhuma classe derivada sobrescrever o membro, o runtime chama a implementação original. Portanto, o virtual funciona como um convite, e não como uma obrigação.
Propriedades também podem ser sobrescritas
Os modificadores virtual e override também valem para propriedades, indexadores e eventos. Por exemplo, a garrafa base pode expor uma capacidade padrão que a garrafa térmica redefine.
public class Bottle
{
public virtual int CapacityInMilliliters => 500;
}
public class ThermalBottle : Bottle
{
// a garrafa térmica tem outra capacidade padrão
public override int CapacityInMilliliters => 1000;
}
Nesse caso, a propriedade sobrescrita precisa manter o mesmo nome, o mesmo tipo e o mesmo modificador de acesso. Assim como nos métodos, o override não pode alterar a acessibilidade do membro virtual.
Vale notar que propriedades aceitam sobrescrita, mas não sobrecarga, já que elas não recebem parâmetros. Somente os indexadores combinam sobrescrita e sobrecarga, porque possuem lista de parâmetros.
Regras da sobrescrita e sobrecarga que o compilador verifica
O compilador do C# protege bastante o desenvolvedor, tanto na sobrescrita quanto na sobrecarga. Por isso, entender as regras evita boa parte dos erros de compilação mais comuns.
No caso da sobrescrita, a documentação do C# define algumas restrições importantes:
- Não é possível sobrescrever um método que não seja
virtual,abstractouoverride. - Métodos
staticnunca participam da sobrescrita. - O método com
overrideprecisa manter o mesmo nível de acesso do método virtual. - Não podemos combinar
overridecomnew,staticouvirtualno mesmo método.
Além disso, desde o C# 9 os métodos sobrescritos aceitam retornos covariantes. Em outras palavras, o retorno do override pode ser um tipo derivado do retorno do método base.
Outra possibilidade é usar sealed override em uma classe intermediária. Com isso, você impede que as classes seguintes da hierarquia sobrescrevam aquele método mais uma vez, tema que detalhamos no artigo sobre classes seladas.
Sobrescrita e sobrecarga versus esconder métodos com new
Se a classe filha declarar um método com a mesma assinatura do pai sem usar override, o compilador emite um aviso. Nesse caso, o método da filha apenas esconde o método do pai, como se tivesse o modificador new.
public class GlassBottle : Bottle
{
// esconde o método da classe base em vez de sobrescrevê-lo
public new void Purpose()
{
Console.WriteLine("Store drinks");
}
}
A diferença aparece quando usamos uma variável do tipo base. Com new, a chamada bottle.Purpose() executa a versão de Bottle, enquanto com override ela executa a versão da classe filha.
Portanto, use override quando quiser polimorfismo de verdade. Por outro lado, reserve o new para situações específicas de versionamento, nas quais a classe base ganhou um membro com o mesmo nome de um membro que já existia na derivada.
Sobrecarga de métodos: o outro lado da sobrescrita
Sobrecarregar métodos significa ter vários métodos com nomes iguais, mas com assinaturas diferentes. Diferente da sobrescrita, a sobrecarga não depende de herança e acontece dentro da própria classe.
A assinatura de um método no C# corresponde à junção do seu nome com os seus parâmetros de entrada. Segundo a especificação da linguagem, ela considera o nome, o número de parâmetros genéricos, o tipo e o modo de passagem de cada parâmetro.
Tomando como base o método abaixo, sua assinatura seria Fill(int). Isso nos permite ter outros métodos chamados Fill, desde que eles tenham parâmetros diferentes.
Aqui fica clara mais uma diferença entre sobrescrita e sobrecarga: a primeira exige a mesma assinatura, enquanto a segunda exige assinaturas distintas.
public void Fill(int milliliters)
{
// ...
}
Sobrecargas que não compilam
No caso abaixo, os dois últimos métodos não seriam permitidos e resultariam em um erro de compilação. Afinal, o compilador enxerga a mesma assinatura nas três declarações.
public void Fill(int milliliters)
{
// ...
}
// Não permitido: só o nome do parâmetro mudou
public void Fill(int liters)
{
// ...
}
// Não permitido: só o tipo de retorno mudou
public double Fill(int ml)
{
// ...
}
Mesmo mudando o retorno do método, ainda teríamos um erro de compilação, porque o retorno não faz parte da assinatura. Da mesma forma, os nomes dos parâmetros também ficam fora dela.
Na prática, o compilador acusa o erro CS0111, informando que o tipo já define um membro chamado Fill com os mesmos tipos de parâmetros. Portanto, a solução é sempre variar o tipo, a quantidade ou o modo de passagem dos parâmetros.
Sobrecargas válidas
Agora temos alguns exemplos que funcionam, pois trocamos o tipo de entrada do método. Mesmo com parâmetros de nomes iguais, os tipos são diferentes, o que permite a sobrecarga.
public void Fill(int milliliters)
{
// ...
}
public void Fill(double liters)
{
// ...
}
public double Fill(float milliliters)
{
// ...
}
Repare que o último método também muda o tipo de retorno. Isso é permitido, porque o que diferencia a assinatura nesse caso é o parâmetro float, e não o double do retorno.
Outro exemplo comum é ter diferentes métodos com o mesmo nome acumulando parâmetros. Vemos esse padrão em várias classes do próprio .NET, como nos construtores de DateTime.
public void SetDate(int day)
{
// ...
}
public void SetDate(int day, int month)
{
// ...
}
public void SetDate(int day, int month, int year)
{
// ...
}
Modificadores de parâmetro e a assinatura
Os modificadores ref, out e in também fazem parte da assinatura. Por exemplo, Fill(int) e Fill(ref int) podem existir juntos na mesma classe.
No entanto, você não pode criar duas sobrecargas que diferem apenas entre ref, out e in. Do mesmo modo, o modificador params não diferencia uma sobrecarga de outra.
Sobrescrita e sobrecarga com parâmetros opcionais
Por fim, não se esqueça de que podemos reduzir bastante código e a quantidade de métodos com os parâmetros opcionais do C#. Assim, um único método substitui as três sobrecargas de SetDate que vimos acima.
public void SetDate(int day, int month = 1, int year = 1990)
{
// ...
}
Com essa versão, as chamadas SetDate(10), SetDate(10, 5) e SetDate(10, 5, 2024) continuam válidas. Além disso, os argumentos nomeados permitem informar apenas o ano, como em SetDate(10, year: 2024).
Porém, misturar parâmetros opcionais com sobrecargas exige atenção. Quando dois candidatos são igualmente bons, o compilador prefere o método que não precisou preencher parâmetros opcionais omitidos.
Por isso, evite criar sobrecargas que competem com versões opcionais do mesmo método. Caso contrário, a leitura do código fica confusa e a escolha do compilador pode surpreender quem mantém o projeto.
Essa regra vale também para métodos virtuais, onde sobrescrita e sobrecarga se encontram com frequência.
Quando usar sobrescrita e sobrecarga no dia a dia
A escolha entre sobrescrita e sobrecarga depende do problema que você quer resolver. Se o comportamento muda conforme o tipo do objeto, a resposta costuma ser a sobrescrita.
Por exemplo, diferentes formas de pagamento podem sobrescrever um método Process herdado de uma classe base. Assim, o restante do sistema trabalha apenas com a abstração, sem conhecer cada implementação.
Por outro lado, se a mesma operação aceita entradas diferentes, a sobrecarga deixa a API mais amigável. Nesse caso, quem consome a classe chama sempre o mesmo nome e passa os dados que tem em mãos.
Também é comum combinar sobrescrita e sobrecarga na mesma hierarquia. Uma classe base pode oferecer várias sobrecargas públicas que delegam para um único método protected virtual, que as classes filhas sobrescrevem.
Para aprofundar esse tipo de modelagem, vale ler também sobre abstração na orientação a objetos. Afinal, métodos abstratos são outra porta de entrada para a sobrescrita.
Cursos sobre orientação a objetos
Não deixe de conhecer nosso material sobre C#, SOLID, Clean Code e muita orientação a objetos. Nossos cursos reúnem teoria e prática de forma direta e objetiva, e a lista abaixo já segue a ordem de estudo.
Fundamentos do C#: este curso reúne toda a base necessária para evoluir na orientação a objetos com a linguagem C#. São mais de 140 aulas em 14 módulos e mais de 12 horas de conteúdo, disponíveis em https://balta.io/cursos/fundamentos-csharp.
Modelando domínios ricos: neste curso, colocamos a mão na massa do começo ao fim, resolvendo um problema completo somente com orientação a objetos e SOLID. É um divisor de águas na carreira de muitos desenvolvedores, disponível em https://balta.io/cursos/modelando-dominios-ricos.
Refatorando para testes de unidade: esta continuação do curso anterior mostra na prática como manter a qualidade e a manutenibilidade do software com testes de unidade. Acesse em https://balta.io/cursos/refatorando-para-testes-de-unidade.
Carreira Backend Developer .NET: caso queira ver todos os cursos anteriores e ainda aprender sobre APIs, padronização, documentação, performance e Cloud, temos uma carreira completa. Ela é extensa, mas oferece toda a base para APIs robustas e de alta performance, em https://balta.io/carreiras/backend-developer-dotnet.
Vídeo sobre sobrescrita e sobrecarga em outras linguagens
Também fizemos uma versão em vídeo com exemplos destes códigos em TypeScript, JavaScript e Dart. Vale muito a pena conferir o vídeo A base do programador no YouTube.
Conclusão
Sobrescrita e sobrecarga compartilham a ideia de reaproveitar nomes de métodos, mas atuam em momentos diferentes. A sobrescrita usa virtual, override e base para que a classe filha redefina um comportamento herdado.
Já a sobrecarga cria variações de um método dentro do mesmo tipo, sempre com assinaturas distintas. Lembre-se de que o tipo de retorno e os nomes dos parâmetros não diferenciam uma sobrecarga.
Por fim, os parâmetros opcionais reduzem a quantidade de sobrecargas, desde que você evite ambiguidades. Com esses cuidados, sobrescrita e sobrecarga deixam seu código mais expressivo e fácil de manter.
FAQ
Qual a diferença entre sobrescrita e sobrecarga?
A sobrescrita redefine, na classe filha, um método herdado com a mesma assinatura. Já a sobrecarga cria vários métodos com o mesmo nome e parâmetros diferentes dentro do mesmo tipo.
Posso sobrescrever um método que não é virtual?
Não. O método base precisa ser virtual, abstract ou override, caso contrário o compilador gera um erro.
O tipo de retorno diferencia uma sobrecarga?
Não. O retorno não faz parte da assinatura, então dois métodos que mudam apenas o retorno não compilam.
Para que serve a palavra base?
Ela permite chamar a implementação da classe pai dentro do método sobrescrito. Assim, a classe filha estende o comportamento em vez de substituí-lo.
Parâmetros opcionais substituem a sobrecarga?
Em muitos casos, sim, porque um único método com valores padrão cobre várias combinações de chamada. Ainda assim, evite misturar as duas abordagens para não gerar ambiguidade.