C# Bibliotecas de Classes
A Ana escreveu as regras do boletim da escola: lançar notas, calcular a média, dizer se o aluno passou. Agora ela quer usar essas mesmas regras em dois programas: o que a professora usa para lançar as notas e o que imprime o relatório da turma. Copiar o código para os dois seria pedir problema: uma correção num, esquecida no outro. A saída é pôr as regras numa biblioteca de classes: um projeto que não roda sozinho, só serve para outros projetos usarem.
- A criar uma biblioteca com
dotnet new classlibe o que muda no.csproj. - O que outro projeto enxerga:
public×internale o erroCS0122. - A ligar a biblioteca a um programa (
dotnet reference add) e onde o.dlldela vai parar. - A usar a mesma biblioteca em dois programas.
- Como uma exceção da biblioteca aparece no programa.
- A documentar a biblioteca com
///(e o avisoCS1591) e a empacotá-la comdotnet pack.
Todas as saídas desta aula são reais, feitas com o .NET 10 SDK (10.0.112) na máquina fictícia da Ana, na solução ~/projetos/Escola. Antes, leia Soluções e Vários Projetos, que mostra a solução e o dotnet reference add.
A ideia: a caixa de ferramentas
Um eletricista não fabrica uma chave de fenda para cada serviço. Ele tem uma caixa de ferramentas e a leva para todas as casas. A caixa não faz nada sozinha: quem trabalha é o eletricista, usando as ferramentas dela.
| Na oficina | No .NET |
|---|---|
| A caixa de ferramentas | A biblioteca de classes (Escola.Notas): classes prontas, sem Main |
| Cada serviço numa casa | Cada programa (Escola.App, Escola.Relatorio): tem por onde começar a rodar |
| As ferramentas que ficam à mostra | As classes public |
| As peças de dentro da caixa (que o cliente não mexe) | As classes internal |
| Uma caixa vendida na loja | Um pacote NuGet (dotnet pack) |
Você já usa bibliotecas o tempo todo: Console, List<T> e File moram em bibliotecas do .NET, e o Humanizer da aula Pacotes NuGet é uma biblioteca de outra pessoa. Agora você vai escrever a sua.
1. Criar a biblioteca: dotnet new classlib
$ mkdir Escola && cd Escola
$ dotnet new sln > /dev/null
$ dotnet new classlib -o src/Escola.Notas > /dev/null
$ ls src/Escola.Notas
Class1.cs
Escola.Notas.csproj
obj
$ cat src/Escola.Notas/Escola.Notas.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
$ cat src/Escola.Notas/Class1.cs
namespace Escola.Notas;
public class Class1
{
}
- dotnet new classlib
- O modelo "Biblioteca de Classes" (class library). O
> /dev/nullsó esconde as mensagens de sempre dodotnet new. - sem <OutputType>
- Compare com o
.csprojde um console (veja O Arquivo .csproj): falta a linha<OutputType>Exe</OutputType>. Sem ela, o padrão éLibrary: o resultado é só um.dll, sem executável. - Class1.cs
- Uma classe vazia de exemplo, no lugar do
Program.cs. Não há instruções de nível superior: uma biblioteca só tem classes (e outros tipos). - namespace Escola.Notas;
- O namespace vem do nome do projeto. É por ele que os outros projetos vão chamar as classes (
using Escola.Notas;, veja Namespaces e using).
Uma biblioteca não roda:
$ dotnet run Não é possível continuar com o projeto "/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj". Verifique se você tem um tipo de projeto executável. Um projeto executável deve ter como destino um TFM executável (por exemplo, net10.0) e ter OutputType "Exe". O OutputType atual é "Library".
A mensagem é clara: para rodar, o projeto precisaria de OutputType Exe (o "TFM" é o Target Framework Moniker, o net10.0 do TargetFramework). Mas a biblioteca não é para rodar: é para ser usada.
2. As classes da biblioteca: public e internal
A Ana apagou o Class1.cs e escreveu dois arquivos. O primeiro é a classe que os programas vão usar:
namespace Escola.Notas;
public class Boletim(string aluno)
{
private readonly List<decimal> _notas = [];
public string Aluno { get; } = aluno;
public void Lancar(decimal nota)
{
if (nota < 0 || nota > 10)
{
throw new ArgumentOutOfRangeException(nameof(nota), "A nota vai de 0 a 10.");
}
_notas.Add(nota);
}
public decimal Media => _notas.Count == 0 ? 0 : Regras.Arredondar(_notas.Average());
public string Situacao => Media >= Regras.MediaMinima ? "Aprovado" : "Recuperação";
}
O segundo guarda as regras da escola, que só o Boletim usa:
namespace Escola.Notas;
internal static class Regras
{
public const decimal MediaMinima = 7.0m;
public static decimal Arredondar(decimal valor) => Math.Round(valor, 1);
}
- public class Boletim
public("público"): a classe fica visível para os outros projetos que usarem a biblioteca. É a "ferramenta à mostra".- internal static class Regras
internal("interno"): visível só dentro deste projeto. OBoletimusa asRegras, mas os programas não enxergam essa classe. Assim, a Ana pode mudar as regras (o arredondamento, a média mínima) sem quebrar nenhum programa.- sem modificador
- Uma classe sem
publicneminternaléinternal. Por isso, numa biblioteca, esquecer opublicdeixa a classe invisível para os programas (veja Modificadores de Acesso). - throw new ArgumentOutOfRangeException(…)
- A biblioteca não sabe se está num console, num site ou num celular, então ela não escreve mensagens na tela: ela lança uma exceção e deixa o programa decidir o que mostrar (veja throw).
| Modificador | Quem enxerga | Use para |
|---|---|---|
public | Todo mundo: este projeto e quem o referenciar | O que os programas devem usar (o Boletim) |
internal (o padrão das classes) | Só o próprio projeto | Os detalhes de dentro (as Regras) |
private (o padrão dos membros) | Só a própria classe | Os campos (_notas) |
3. Um programa que usa a biblioteca
$ dotnet new console -o src/Escola.App > /dev/null $ dotnet sln add src/Escola.Notas src/Escola.App O projeto ‘src/Escola.Notas/Escola.Notas.csproj’ foi adicionado à solução. O projeto ‘src/Escola.App/Escola.App.csproj’ foi adicionado à solução. $ dotnet reference add src/Escola.Notas --project src/Escola.App A referência ‘..\Escola.Notas\Escola.Notas.csproj’ foi adicionada ao projeto.
using Escola.Notas;
var ana = new Boletim("Ana");
ana.Lancar(8.5m);
ana.Lancar(6.0m);
ana.Lancar(9.25m);
Console.WriteLine($"{ana.Aluno}: média {ana.Media} ({ana.Situacao})");
$ dotnet build
Determinando os projetos a serem restaurados...
/home/ana/projetos/Escola/src/Escola.App/Escola.App.csproj restaurado (em 229 ms).
1 de 2 projetos estão atualizados para restauração.
Escola.Notas -> /home/ana/projetos/Escola/src/Escola.Notas/bin/Debug/net10.0/Escola.Notas.dll
Escola.App -> /home/ana/projetos/Escola/src/Escola.App/bin/Debug/net10.0/Escola.App.dll
Compilação com êxito.
0 Aviso(s)
0 Erro(s)
Tempo Decorrido 00:00:10.19
$ dotnet run --project src/Escola.App
Ana: média 7,9 (Aprovado)
(8,5 + 6,0 + 9,25) ÷ 3 = 7,9166…, arredondado para uma casa: 7,9. O programa só conhece o Boletim; a conta e o arredondamento ficaram dentro da biblioteca.
Para onde vai o .dll da biblioteca
$ ls src/Escola.Notas/bin/Debug/net10.0/ Escola.Notas.deps.json Escola.Notas.dll Escola.Notas.pdb $ ls src/Escola.App/bin/Debug/net10.0/ Escola.App Escola.App.deps.json Escola.App.dll Escola.App.pdb Escola.App.runtimeconfig.json Escola.Notas.dll Escola.Notas.pdb
- Escola.Notas.dll (na pasta da biblioteca)
- A biblioteca compilada. Não há executável nem
runtimeconfig.json: ela não roda sozinha. - Escola.Notas.dll (na pasta do programa)
- O
dotnet buildcopiou a biblioteca para junto do programa. Quando oEscola.Approda, ele procura oEscola.Notas.dllna própria pasta. Para levar o programa para outro computador, as duas.dlltêm de ir juntas (a aula Publicar o Programa faz isso por você). - .pdb
- As informações de depuração: os nomes dos arquivos e os números das linhas (seção 5 e Depuração).
No editor do guia não há projetos separados: os arquivos das abas são compilados juntos, como um projeto só. Mesmo assim dá para treinar a divisão em arquivos:
Missão: o Program.cs quer mostrar a maior nota da Ana, mas a classe Boletim ainda não tem essa propriedade: rode e veja o erro CS1061. Na aba Boletim.cs, acrescente public decimal Maior => _notas.Max(); e rode de novo. (No editor, a aba Regras.cs também seria visível para o Program.cs, porque tudo é um projeto só; num projeto separado, o internal a esconderia.)
using Escola.Notas;
var ana = new Boletim("Ana");
ana.Lancar(8.5m);
ana.Lancar(6.0m);
ana.Lancar(9.25m);
Console.WriteLine($"{ana.Aluno}: média {ana.Media} ({ana.Situacao})");
Console.WriteLine($"Maior nota: {ana.Maior}");
namespace Escola.Notas;
public class Boletim(string aluno)
{
private readonly List<decimal> _notas = [];
public string Aluno { get; } = aluno;
public void Lancar(decimal nota)
{
if (nota < 0 || nota > 10)
{
throw new ArgumentOutOfRangeException(nameof(nota), "A nota vai de 0 a 10.");
}
_notas.Add(nota);
}
public decimal Media => _notas.Count == 0 ? 0 : Regras.Arredondar(_notas.Average());
public string Situacao => Media >= Regras.MediaMinima ? "Aprovado" : "Recuperação";
}
namespace Escola.Notas;
internal static class Regras
{
public const decimal MediaMinima = 7.0m;
public static decimal Arredondar(decimal valor) => Math.Round(valor, 1);
}
namespace Escola.Notas;
public class Boletim(string aluno)
{
private readonly List<decimal> _notas = [];
public string Aluno { get; } = aluno;
public void Lancar(decimal nota)
{
if (nota < 0 || nota > 10)
{
throw new ArgumentOutOfRangeException(nameof(nota), "A nota vai de 0 a 10.");
}
_notas.Add(nota);
}
public decimal Media => _notas.Count == 0 ? 0 : Regras.Arredondar(_notas.Average());
public string Situacao => Media >= Regras.MediaMinima ? "Aprovado" : "Recuperação";
public decimal Maior => _notas.Max();
}
4. A mesma biblioteca em dois programas
Agora o motivo de tudo isso: um segundo programa, o relatório da turma, usando as mesmas regras.
$ dotnet new console -o src/Escola.Relatorio > /dev/null $ dotnet sln add src/Escola.Relatorio O projeto ‘src/Escola.Relatorio/Escola.Relatorio.csproj’ foi adicionado à solução. $ dotnet reference add src/Escola.Notas --project src/Escola.Relatorio A referência ‘..\Escola.Notas\Escola.Notas.csproj’ foi adicionada ao projeto.
using Escola.Notas;
var turma = new List<Boletim>();
var ana = new Boletim("Ana");
ana.Lancar(8.5m); ana.Lancar(6.0m); ana.Lancar(9.25m);
turma.Add(ana);
var beto = new Boletim("Beto");
beto.Lancar(5.0m); beto.Lancar(7.5m);
turma.Add(beto);
foreach (Boletim b in turma.OrderByDescending(b => b.Media))
{
Console.WriteLine($"{b.Aluno,-6} {b.Media,4} {b.Situacao}");
}
$ dotnet build -v q 2>&1 | tail -4
0 Aviso(s)
0 Erro(s)
Tempo Decorrido 00:00:02.87
$ dotnet run --project src/Escola.Relatorio
Ana 7,9 Aprovado
Beto 6,2 Recuperação
A solução agora tem três projetos: uma biblioteca e dois programas que a usam. Se a escola mudar a média mínima para 6,0, a Ana muda uma linha em Regras.cs, e os dois programas passam a seguir a regra nova no próximo dotnet build.
Math.Round(6.25m, 1) deu 6,2, não 6,3. Quando o número está exatamente no meio, o Math.Round arredonda para o vizinho par (o "arredondamento bancário"). Se a escola quiser o arredondamento da escola (6,25 → 6,3), a regra vira Math.Round(valor, 1, MidpointRounding.AwayFromZero), e a mudança é numa linha só da biblioteca (veja Math).
5. Uma exceção que atravessa projetos
A Ana lançou uma nota 11, por engano, no Escola.App:
$ echo 'ana.Lancar(11m);' >> src/Escola.App/Program.cs $ dotnet run --project src/Escola.App; echo "código de saída: $?" Ana: média 7,9 (Aprovado) Unhandled exception. System.ArgumentOutOfRangeException: A nota vai de 0 a 10. (Parameter 'nota') at Escola.Notas.Boletim.Lancar(Decimal nota) in /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs:line 13 at Program.<Main>$(String[] args) in /home/ana/projetos/Escola/src/Escola.App/Program.cs:line 9 código de saída: 134
- A nota vai de 0 a 10. (Parameter 'nota')
- A mensagem que a biblioteca escreveu, mais o nome do parâmetro, vindo do
nameof(nota). - at Escola.Notas.Boletim.Lancar … Boletim.cs:line 13
- Onde a exceção nasceu: dentro da biblioteca, na linha 13 do
Boletim.cs(othrow). O nome completo inclui o namespace:Escola.Notas.Boletim. - at Program.<Main>$ … Program.cs:line 9
- Quem chamou: a linha 9 do programa, o
ana.Lancar(11m). A pilha atravessa os dois projetos (veja Exceções). Os números de linha vêm dos arquivos.pdb.
O erro é do programa (passou uma nota inválida), e quem tem de tratar é ele, com try/catch (veja try e catch). A biblioteca fez a parte dela: recusou a nota e explicou por quê.
6. Documentar a biblioteca: ///
Quem usa uma biblioteca não lê o código dela: lê a documentação. No C#, ela é escrita em comentários com três barras, ///, logo acima de cada coisa public. Para o compilador cobrar esses comentários, a Ana ligou no Escola.Notas.csproj:
<GenerateDocumentationFile>true</GenerateDocumentationFile>
$ dotnet build src/Escola.Notas 2>&1 | grep warning | sort -u /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(18,20): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim.Media" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj] /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(20,19): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim.Situacao" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj] /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(3,14): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim.Boletim(string)" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj] /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(3,14): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj] /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(7,19): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim.Aluno" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj] /home/ana/projetos/Escola/src/Escola.Notas/Boletim.cs(9,17): warning CS1591: Comentário XML ausente para tipo publicamente visível ou membro "Boletim.Lancar(decimal)" [/home/ana/projetos/Escola/src/Escola.Notas/Escola.Notas.csproj]
Seis avisos CS1591 ("comentário XML ausente para tipo ou membro visível publicamente"): um para cada coisa public. As Regras, que são internal, não precisam. A Ana documentou o Boletim:
namespace Escola.Notas;
/// <summary>As notas de um aluno e a situação dele.</summary>
/// <param name="aluno">O nome do aluno.</param>
public class Boletim(string aluno)
{
private readonly List<decimal> _notas = [];
/// <summary>O nome do aluno.</summary>
public string Aluno { get; } = aluno;
/// <summary>Lança uma nota de 0 a 10.</summary>
/// <exception cref="ArgumentOutOfRangeException">A nota é menor que 0 ou maior que 10.</exception>
public void Lancar(decimal nota)
{
«… igual …»
}
/// <summary>A média das notas, com uma casa decimal (0 se não houver notas).</summary>
public decimal Media => _notas.Count == 0 ? 0 : Regras.Arredondar(_notas.Average());
/// <summary>"Aprovado" com média 7,0 ou mais; senão, "Recuperação".</summary>
public string Situacao => Media >= Regras.MediaMinima ? "Aprovado" : "Recuperação";
}
$ dotnet build src/Escola.Notas 2>&1 | grep -E "warning|Aviso"
0 Aviso(s)
$ ls src/Escola.Notas/bin/Debug/net10.0/
Escola.Notas.deps.json
Escola.Notas.dll
Escola.Notas.pdb
Escola.Notas.xml
$ head -12 src/Escola.Notas/bin/Debug/net10.0/Escola.Notas.xml
<?xml version="1.0"?>
<doc>
<assembly>
<name>Escola.Notas</name>
</assembly>
<members>
<member name="T:Escola.Notas.Boletim">
<summary>As notas de um aluno e a situação dele.</summary>
<param name="aluno">O nome do aluno.</param>
</member>
<member name="M:Escola.Notas.Boletim.#ctor(System.String)">
<summary>As notas de um aluno e a situação dele.</summary>
- <summary>
- "Resumo": uma frase dizendo o que a coisa é ou faz.
- <param name="aluno">
- O que é cada parâmetro.
- <exception cref="…">
- Que exceção pode sair dali, e quando.
- Escola.Notas.xml
- O compilador juntou os comentários num arquivo ao lado do
.dll. Os editores (VS Code, Visual Studio, Rider) leem esse arquivo e mostram o resumo quando você passa o mouse sobreLancarno programa. É assim que aparece a explicação deConsole.WriteLineno seu editor.
Os comentários de documentação têm aula própria: veja Comentários.
7. Empacotar: dotnet pack
Para outras pessoas (ou outras soluções) usarem a biblioteca como um pacote NuGet:
$ dotnet pack src/Escola.Notas -c Release Determinando os projetos a serem restaurados... Todos os projetos estão atualizados para restauração. Escola.Notas -> /home/ana/projetos/Escola/src/Escola.Notas/bin/Release/net10.0/Escola.Notas.dll Falta um pacote Escola.Notas.1.0.0 Acesse. Acesse https://aka.ms/nuget/authoring-best-practices/readme para saber porque readmes de pacote são importantes. Pacote criado com êxito '/home/ana/projetos/Escola/src/Escola.Notas/bin/Release/Escola.Notas.1.0.0.nupkg'. $ ls src/Escola.Notas/bin/Release/ Escola.Notas.1.0.0.nupkg net10.0
- dotnet pack … -c Release
- "Empacote": compila em Release e cria um
.nupkg, o mesmo tipo de arquivo que vem donuget.org. - Escola.Notas.1.0.0.nupkg
- Nome e versão. Sem
<Version>no.csproj, a versão é1.0.0. - Falta um pacote … Acesse.
- A tradução saiu truncada, mas o original diz: "o pacote
Escola.Notas.1.0.0não tem um README". É uma recomendação, não um erro: onuget.orgmostra o README na página do pacote.
Esse .nupkg pode ser publicado no nuget.org (com uma conta e uma chave) ou num servidor da empresa. Para uma solução só, como a da Ana, a referência de projeto é mais simples: não precisa empacotar nada.
Biblioteca ou só uma pasta no projeto?
| Situação | Escolha |
|---|---|
| Um programa só, código organizado em vários arquivos | Pastas e namespaces dentro do mesmo projeto (veja Namespaces). |
| As mesmas regras em dois ou mais programas (console, site, testes) | Biblioteca de classes + dotnet reference add. |
| Separar as regras da tela (o boletim não sabe que existe um console) | Biblioteca: fica claro o que é regra e o que é tela. |
| Testar as regras (veja Testes) | Biblioteca: o projeto de testes a referencia como qualquer programa. |
| Outras pessoas, em outras soluções, vão usar | Biblioteca + dotnet pack (pacote NuGet). |
Quando dá errado
error CS0122: "Regras" é inacessível devido ao seu nível de proteção
$ echo 'Console.WriteLine(Regras.MediaMinima);' >> src/Escola.App/Program.cs $ dotnet build 2>&1 | grep error | sort -u /home/ana/projetos/Escola/src/Escola.App/Program.cs(9,19): error CS0122: "Regras" é inacessível devido ao seu nível de proteção [/home/ana/projetos/Escola/src/Escola.App/Escola.App.csproj]
O que aconteceu: o programa tentou usar a classe Regras, que é internal na biblioteca. O compilador sabe que ela existe, mas não deixa usar (em inglês, 'Regras' is inaccessible due to its protection level). Se a classe tivesse ficado sem modificador, o erro seria o mesmo, porque o padrão é internal.
Como resolver: se o programa precisa mesmo daquilo, torne public. Se não (como aqui), deixe internal e use o que a biblioteca oferece em público (Boletim.Situacao). Para um projeto de testes enxergar o internal, existe o atributo [assembly: InternalsVisibleTo("Escola.Notas.Tests")].
Não é possível continuar com o projeto … O OutputType atual é "Library".
O que aconteceu: o dotnet run foi chamado na pasta da biblioteca (seção 1).
Como resolver: rode o programa que usa a biblioteca: dotnet run --project src/Escola.App.
error CS0246: O nome do tipo ou do namespace "Escola" não pode ser encontrado
O que aconteceu: o programa não tem a referência para a biblioteca, ou falta o using Escola.Notas; (veja o mesmo erro em Soluções).
Como resolver: dotnet reference list --project src/Escola.App mostra as referências. Se a biblioteca não estiver lá: dotnet reference add src/Escola.Notas --project src/Escola.App.
Referência rápida
| Comando ou código | O que faz |
|---|---|
dotnet new classlib -o src/Nome | Cria uma biblioteca de classes. |
dotnet reference add src/Biblioteca --project src/Programa | O programa passa a usar a biblioteca. |
public class … | Visível para quem usa a biblioteca. |
internal class … (ou sem modificador) | Só dentro da biblioteca. |
<GenerateDocumentationFile>true</GenerateDocumentationFile> | Gera o .xml da documentação e avisa (CS1591) o que falta documentar. |
/// <summary>…</summary> | Comentário de documentação. |
dotnet pack -c Release | Cria o pacote .nupkg. |
[assembly: InternalsVisibleTo("Outro")] | Deixa o projeto Outro (os testes) ver o internal. |
Exercícios
1. Criar a biblioteca
Complete o comando que cria uma biblioteca de classes em src/Loja.Precos:
2. Visível para os programas
Complete para que a classe possa ser usada por outros projetos:
3. Ligar o programa à biblioteca
Complete para o src/Loja.App usar a src/Loja.Precos:
4. A Ana esqueceu o modificador: escreveu só class Boletim na biblioteca. O que acontece no programa que a usa?
5. Por que a biblioteca lança uma exceção em vez de escrever "nota inválida" com Console.WriteLine?
6. Depois do dotnet build, onde está o Escola.Notas.dll que o Escola.App usa ao rodar?
Resumo
- Uma biblioteca de classes (
dotnet new classlib) é um projeto semOutputTypeExe: vira um.dllque não roda sozinho, só é usado. publicé o que os outros projetos enxergam;internal(o padrão das classes) fica dentro da biblioteca.- O programa usa a biblioteca com
dotnet reference addeusing. Obuildcopia o.dllda biblioteca para a pasta do programa. - Uma biblioteca serve a vários programas: uma correção nela vale para todos.
- A biblioteca lança exceções; o programa decide o que mostrar.
///documenta;GenerateDocumentationFilegera o.xmle cobra o que falta (CS1591).dotnet packcria um pacote NuGet.