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.

Você vai aprender
  • 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 .cs com um pedaço da classe, marcado com partial;
  • 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(). O partial avisa: "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 o set do Preco).
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 Produto como uma classe qualquer: chama o Mostrar() (parte 1) e o AplicarDesconto() (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)
No editor do guia

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.

RegraSe quebrar
Todas as partes com partialCS0260 (faltou em uma) ou CS0101 (faltou em todas)
Mesmo nome, mesmo namespace, mesmo projetoSenão, são classes diferentes
Modificadores de acesso iguais (ou só em uma parte)CS0262
Cada membro aparece numa parte sóCS0111
Também funciona num arquivo só

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 MudarPreco chama 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

CS0260: "Modificador parcial ausente na declaração do tipo …"
$ 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.

CS0101: "… já contém uma definição para …"
$ 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.

CS0246 com a classe em outro arquivo

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.

CS8795: "O método parcial … precisa ter uma parte de implementação …"
$ 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

EscritaSignificado
partial class Produto { … }Uma parte da classe; as partes do mesmo projeto viram uma classe só
Produto.cs, Produto.Precos.csConvençã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 recordTambé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:

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 class divide 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 Loja e dotnet run na 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.