C# Classes Parciais
Até aqui, cada classe ficou inteira num lugar só, entre um { e um }. Com a palavra partial ("parcial"), uma classe pode ser escrita em pedaços, em arquivos diferentes, e o compilador junta tudo numa classe só. Parece uma curiosidade, mas é assim que funcionam as ferramentas que escrevem código para você: elas ficam com um arquivo, você fica com outro, e ninguém apaga o trabalho de ninguém.
- O que é uma classe parcial (
partial class) e como dividi-la em vários arquivos de um projeto. - Como o compilador junta as partes, e as regras que ele cobra (
CS0260,CS0101,CS0262,CS0111). - Para que servem as classes parciais: código gerado por ferramentas e classes grandes.
- Métodos parciais: declarar num arquivo e, se quiser, escrever em outro.
- Propriedades parciais C# 13 e construtores parciais C# 14.
A ideia: um livro escrito a quatro mãos
Dois autores escrevem um livro juntos. Um fica com os capítulos 1 a 5; o outro, com os capítulos 6 a 10. Cada um escreve no seu próprio caderno, sem mexer no do outro. No fim, a editora junta os cadernos e imprime um livro só. Quem compra o livro nem sabe que ele foi escrito em dois cadernos.
Uma classe parcial é esse livro:
- cada caderno é um arquivo
.cscom um pedaço da classe, marcado compartial; - a editora é o compilador, que junta os pedaços;
- o livro é a classe final: para quem usa, ela é uma classe comum.
Um projeto com vários arquivos
Até agora, a Ana rodava arquivos soltos com dotnet run arquivo.cs. Uma classe espalhada em vários arquivos pede um projeto (veja Projetos): uma pasta em que todos os .cs são compilados juntos. Ela criou o projeto da lojinha na pasta ~/projetos:
$ cd ~/projetos $ dotnet new console -o Loja O modelo "Aplicativo do Console" foi criado com êxito. Processando ações pós-criação... Restaurando /home/ana/projetos/Loja/Loja.csproj: Determinando os projetos a serem restaurados... /home/ana/projetos/Loja/Loja.csproj restaurado (em 108 ms). A restauração foi bem-sucedida. $ cd Loja
Depois, ela trocou o conteúdo do Program.cs e criou mais dois arquivos na mesma pasta, com o editor de texto:
$ ls Loja.csproj obj Produto.cs Produto.Precos.cs Program.cs
Os três arquivos .cs:
Produto cafe = new Produto("Café", 18.90m);
cafe.Mostrar();
cafe.AplicarDesconto(10);
cafe.Mostrar();
partial class Produto
{
public string Nome { get; }
public decimal Preco { get; private set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public void Mostrar()
{
Console.WriteLine($"{Nome}: {Preco:C}");
}
}
partial class Produto
{
public void AplicarDesconto(int porcento)
{
Preco -= Preco * porcento / 100;
}
}
- partial class Produto (em Produto.cs)
- A primeira parte da classe: as propriedades, o construtor e o
Mostrar(). Opartialavisa: "esta não é a classe inteira; procure as outras partes". - partial class Produto (em Produto.Precos.cs)
- A segunda parte, com o mesmo nome. Aqui fica o que mexe em preços.
- Preco -= Preco * porcento / 100;
- O método da segunda parte usa a propriedade
Preco, que foi declarada na primeira. Para o compilador, é tudo uma classe só: cada parte enxerga tudo das outras, inclusive o que éprivate(como osetdoPreco). - Produto.Precos.cs
- O nome do arquivo não importa para o compilador, mas a convenção é
Classe.Assunto.cs: quem abre a pasta vê na hora que os dois arquivos são da mesma classe. - Program.cs
- Usa o
Produtocomo uma classe qualquer: chama oMostrar()(parte 1) e oAplicarDesconto()(parte 2) sem saber de onde cada um veio.
Num projeto, o comando é só dotnet run, sem o nome do arquivo: ele compila todos os .cs da pasta.
$ dotnet run Café: R$ 18,90 Café: R$ 17,01 « 18,90 menos 10% (1,89)
Nas caixas "Experimente" com vários arquivos, cada arquivo vira uma aba. Todas as abas são compiladas juntas, como num projeto. Clique no nome do arquivo para trocar de aba.
Missão: a Ana já escreveu, no Program.cs, a chamada cafe.Aumentar(10), mas o método não existe (rode e veja o erro). Na aba Produto.Precos.cs, crie o método public void Aumentar(int porcento), que soma ao preço a porcentagem pedida (Preco += Preco * porcento / 100;). A saída deve terminar com Café: R$ 20,79.
Produto cafe = new Produto("Café", 18.90m);
cafe.Mostrar();
cafe.Aumentar(10);
cafe.Mostrar();
partial class Produto
{
public string Nome { get; }
public decimal Preco { get; private set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public void Mostrar()
{
Console.WriteLine($"{Nome}: {Preco:C}");
}
}
partial class Produto
{
public void AplicarDesconto(int porcento)
{
Preco -= Preco * porcento / 100;
}
// crie aqui o método Aumentar
}
partial class Produto
{
public void AplicarDesconto(int porcento)
{
Preco -= Preco * porcento / 100;
}
public void Aumentar(int porcento)
{
Preco += Preco * porcento / 100;
}
}
As regras que o compilador cobra
1. Todas as partes levam partial
A Ana esqueceu o partial no Produto.Precos.cs (ficou só class Produto):
$ dotnet run /home/ana/projetos/Loja/Produto.Precos.cs(1,7): error CS0260: Modificador parcial ausente na declaração do tipo "Produto"; existe outra declaração parcial deste tipo [/home/ana/projetos/Loja/Loja.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Falta o modificador partial na declaração de Produto; existe outra declaração parcial." O compilador achou uma parte com partial (a de Produto.cs) e outra sem. Num projeto, a mensagem termina com o nome do .csproj entre colchetes: é o projeto que estava sendo compilado.
2. Sem partial, são duas classes com o mesmo nome
E se nenhuma parte tiver partial? Aí o compilador vê duas classes diferentes com o mesmo nome:
$ dotnet run /home/ana/projetos/Loja/Produto.Precos.cs(1,7): error CS0101: O namespace "<global namespace>" já contém uma definição para "Produto" [/home/ana/projetos/Loja/Loja.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O "namespace global" é o lugar das classes que não estão em nenhum namespace. Ali já existe um Produto, e não cabe outro.
3. As partes têm de concordar
Os modificadores da classe têm de combinar. Com internal partial class Produto num arquivo e public partial class Produto no outro:
$ dotnet run /home/ana/projetos/Loja/Produto.cs(1,24): error CS0262: Declarações parciais de "Produto" têm modificadores de acessibilidade conflitantes [/home/ana/projetos/Loja/Loja.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Escreva o mesmo modificador nas duas partes, ou escreva só numa (as outras herdam). O mesmo vale para a classe mãe: se uma parte diz partial class Produto : Item, as outras podem repetir : Item ou não dizer nada, mas não podem citar outra mãe. Já as interfaces se somam: cada parte pode acrescentar as suas.
4. Um membro só existe uma vez
As partes são uma classe só, então não podem repetir um membro. A Ana criou um terceiro arquivo, Produto.Extra.cs, com outro Mostrar():
partial class Produto
{
public void Mostrar() => Console.WriteLine(Nome);
}
$ dotnet run /home/ana/projetos/Loja/Produto.Extra.cs(3,17): error CS0111: Tipo "Produto" já define um membro chamado "Mostrar" com os mesmos tipos de parâmetro [/home/ana/projetos/Loja/Loja.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
É o mesmo erro de escrever dois Mostrar() iguais numa classe comum (veja Sobrecarga): o partial não cria cópias, só divide.
| Regra | Se quebrar |
|---|---|
Todas as partes com partial | CS0260 (faltou em uma) ou CS0101 (faltou em todas) |
| Mesmo nome, mesmo namespace, mesmo projeto | Senão, são classes diferentes |
| Modificadores de acesso iguais (ou só em uma parte) | CS0262 |
| Cada membro aparece numa parte só | CS0111 |
As partes podem estar até no mesmo arquivo, e então dotnet run arquivo.cs serve. Não é útil no dia a dia, mas mostra que partial não tem nada a ver com arquivos: é só "esta classe continua em outro lugar". O que não funciona é espalhar as partes em projetos diferentes: o compilador só junta as partes de um mesmo projeto.
Para que serve?
1. Código gerado por ferramentas
É o uso principal. Muitas ferramentas do .NET escrevem código C# automaticamente:
- o desenhista de telas do Windows Forms e do WPF grava os botões e as caixas de texto que você arrastou num arquivo como
Form1.Designer.cs; - os geradores de código (source generators) do compilador criam código na hora de compilar. O
[GeneratedRegex]da aula de Regex e o gerador do JSON trabalham assim.
A ferramenta fica com a parte dela e pode reescrevê-la a qualquer momento. Você escreve a sua parte em outro arquivo, que a ferramenta nunca toca. Sem partial, cada vez que a ferramenta regravasse o arquivo, o seu código sumiria.
2. Classes grandes
Uma classe muito grande pode ser dividida por assunto: Pedido.cs, Pedido.Validacao.cs, Pedido.Impressao.cs. Use com cuidado: uma classe tão grande que precisa de vários arquivos muitas vezes está fazendo coisas demais e seria melhor dividida em classes menores.
Métodos parciais
Uma parte da classe pode declarar um método sem corpo e chamá-lo, e outra parte pode escrever esse corpo. É o jeito de uma ferramenta deixar um "gancho": "se você quiser fazer algo quando o preço mudar, escreva este método".
A Ana montou outro projeto, ~/projetos/Precos, com três arquivos:
Produto cafe = new Produto("Café", 18.90m);
cafe.MudarPreco(21.50m);
cafe.MudarPreco(19.90m);
partial class Produto
{
public string Nome { get; }
public decimal Preco { get; private set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public void MudarPreco(decimal novo)
{
decimal antigo = Preco;
Preco = novo;
AoMudarPreco(antigo, novo);
}
partial void AoMudarPreco(decimal antigo, decimal novo);
}
partial class Produto
{
partial void AoMudarPreco(decimal antigo, decimal novo)
{
Console.WriteLine($"{Nome}: {antigo:C} → {novo:C}");
}
}
- partial void AoMudarPreco(decimal antigo, decimal novo);
- A declaração do método parcial, sem corpo, terminada em
;. É o gancho. - AoMudarPreco(antigo, novo);
- O
MudarPrecochama o gancho depois de trocar o preço. - partial void AoMudarPreco(…) { … }
- A implementação, em outro arquivo, com a mesma assinatura e o corpo.
$ dotnet run Café: R$ 18,90 → R$ 21,50 Café: R$ 21,50 → R$ 19,90
A surpresa: um método parcial void, sem modificador de acesso, não precisa ser implementado. A Ana tirou o Produto.Avisos.cs da pasta e rodou de novo:
$ mv Produto.Avisos.cs ~/ $ dotnet run $ « compilou, rodou e não mostrou nada
Sem implementação, o compilador apaga o gancho e todas as chamadas a ele, como se nunca tivessem existido. Por isso esse tipo de método parcial tem regras: tem de ser void, não pode ter modificador de acesso (é sempre private) nem parâmetros out.
Com modificador de acesso, a implementação é obrigatória C# 9
Desde o C# 9, um método parcial pode ser public, devolver valores e ter out. Em troca, a implementação vira obrigatória (o compilador não tem como "apagar" uma chamada que devolve um valor):
Console.WriteLine(new Cliente().Desconto());
partial class Cliente
{
public partial decimal Desconto();
}
$ dotnet run SemImpl.cs /home/ana/cs/SemImpl.cs(5,28): error CS8795: O método parcial 'Cliente.Desconto()' precisa ter uma parte de implementação porque ele tem modificadores de acessibilidade. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
É assim que os geradores de código modernos trabalham: você declara public partial … (o que quer) e o gerador escreve a implementação (o como).
Propriedades e construtores parciais C# 13 C# 14
O C# 13 (.NET 9) estendeu a ideia às propriedades, e o C# 14 (.NET 10), aos construtores e eventos. A regra é a mesma do método com acesso: uma parte declara, outra implementa, e as duas são obrigatórias.
Console.WriteLine(new Cliente("Ana").Saudacao);
partial class Cliente
{
public partial Cliente(string nome);
public partial string Saudacao { get; }
}
partial class Cliente
{
private readonly string nome;
public partial Cliente(string nome)
{
this.nome = nome;
}
public partial string Saudacao => $"Olá, {nome}!";
}
$ dotnet run Novo.cs Olá, Ana!
Aqui as duas partes estão no mesmo arquivo só para caber num exemplo. Na vida real, a primeira parte seria sua e a segunda viria de um gerador. Você vai usar isso pouco no começo, mas vai encontrar em código moderno.
Quando dá errado
$ dotnet run /home/ana/projetos/Loja/Produto.Precos.cs(1,7): error CS0260: Modificador parcial ausente na declaração do tipo "Produto"; existe outra declaração parcial deste tipo [/home/ana/projetos/Loja/Loja.csproj]
O que aconteceu: uma das partes está sem partial.
Como resolver: escreva partial class no arquivo apontado.
$ dotnet run /home/ana/projetos/Loja/Produto.Precos.cs(1,7): error CS0101: O namespace "<global namespace>" já contém uma definição para "Produto" [/home/ana/projetos/Loja/Loja.csproj]
O que aconteceu: duas classes com o mesmo nome, nenhuma com partial. Também acontece sem querer, quando você copia um arquivo para testar e esquece o original na pasta do projeto.
Como resolver: se a ideia era dividir a classe, use partial nas duas partes. Se não, renomeie uma delas ou apague a cópia.
O que aconteceu: o Program.cs diz que Produto "não pode ser encontrado". Quase sempre, o arquivo da classe está fora da pasta do projeto (por exemplo, na pasta de cima), ou o nome da classe está escrito diferente nos dois lugares (produto × Produto).
Como resolver: coloque todos os .cs na pasta do .csproj (ou em subpastas dela) e rode dotnet run nessa pasta.
$ dotnet run SemImpl.cs /home/ana/cs/SemImpl.cs(5,28): error CS8795: O método parcial 'Cliente.Desconto()' precisa ter uma parte de implementação porque ele tem modificadores de acessibilidade.
O que aconteceu: um método parcial com public (ou que devolve valor) foi só declarado.
Como resolver: escreva a implementação, com public partial e o corpo, em uma das partes. Se o método vem de um gerador de código, confira se o pacote do gerador está instalado no projeto.
Referência
| Escrita | Significado |
|---|---|
partial class Produto { … } | Uma parte da classe; as partes do mesmo projeto viram uma classe só |
Produto.cs, Produto.Precos.cs | Convenção de nomes dos arquivos de uma classe parcial |
partial void Gancho(); | Método parcial opcional: sem implementação, ele e as chamadas somem |
public partial decimal Desconto(); | Método parcial com acesso C# 9: implementação obrigatória |
public partial string Saudacao { get; } | Propriedade parcial C# 13 |
public partial Cliente(string nome); | Construtor parcial C# 14 |
partial struct, partial interface, partial record | Também existem |
dotnet run (num projeto) | Compila todos os .cs da pasta |
Exercícios
1. A palavra-chave
Complete para esta ser uma das partes da classe Pedido:
2. O nome do arquivo
Seguindo a convenção, complete o nome do arquivo da parte de Pedido que cuida da impressão:
3. O gancho
Complete a declaração do método parcial opcional AoSalvar, sem parâmetros:
4. Produto.cs tem partial class Produto e Produto.Precos.cs tem class Produto, sem partial. O que acontece?
5. Um método da parte 2 pode usar um campo private declarado na parte 1?
6. partial void AoMudarPreco(); foi declarado e chamado, mas ninguém escreveu a implementação. O que acontece?
Missão final: no projeto dos preços, o Produto.cs já declara e chama o gancho partial void AoMudarPreco(decimal antigo, decimal novo);. Rode: nada aparece, porque ninguém implementou o gancho. Na aba Produto.Avisos.cs, escreva a implementação, que mostra {Nome}: {antigo:C} → {novo:C} com Console.WriteLine. A primeira linha da saída deve ser Café: R$ 18,90 → R$ 21,50. (Você pode copiar o → deste texto.)
Produto cafe = new Produto("Café", 18.90m);
cafe.MudarPreco(21.50m);
cafe.MudarPreco(19.90m);
partial class Produto
{
public string Nome { get; }
public decimal Preco { get; private set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public void MudarPreco(decimal novo)
{
decimal antigo = Preco;
Preco = novo;
AoMudarPreco(antigo, novo);
}
partial void AoMudarPreco(decimal antigo, decimal novo);
}
partial class Produto
{
// implemente aqui o AoMudarPreco
}
partial class Produto
{
partial void AoMudarPreco(decimal antigo, decimal novo)
{
Console.WriteLine($"{Nome}: {antigo:C} → {novo:C}");
}
}
Resumo
partial classdivide uma classe em partes, em arquivos diferentes do mesmo projeto. O compilador junta tudo numa classe só, e cada parte enxerga as outras.- Todas as partes levam
partial(CS0260; sem nenhum,CS0101), concordam nos modificadores (CS0262) e não repetem membros (CS0111). - Vários arquivos pedem um projeto:
dotnet new console -o Lojaedotnet runna pasta. - O uso principal é o código gerado por ferramentas: a ferramenta fica com uma parte, você com outra.
- Método parcial
partial void X();é um gancho opcional, apagado se não for implementado. Com modificador de acesso C# 9, propriedades C# 13 e construtores C# 14, a implementação é obrigatória (CS8795). Próxima aula: A Classe object.