Entre o arquivo .cs que você escreve e o programa rodando existem quatro etapas. Conhecê-las agora poupa horas de confusão depois.
Na aula anterior, "Da Rivalidade com o Java ao Código Aberto", vimos que o C# é uma linguagem e o .NET é a plataforma que a executa. Hoje essa distinção sai do papel. Vamos instalar o .NET, criar o primeiro programa e segui-lo passo a passo até ele virar instruções que o processador executa. Se você vem do C, prepare-se para uma diferença importante: aqui o compilador não gera código de máquina diretamente. Existe uma etapa intermediária, e ela explica muito do que o C# consegue fazer.
SDK, runtime e as peças do .NET
Quando você vai baixar o .NET, encontra dois pacotes, e é bom saber a diferença desde já:
- Runtime: o necessário para executar programas .NET. É o que um usuário final precisa ter instalado.
- SDK (Software Development Kit): o runtime mais o compilador, as ferramentas de linha de comando e os modelos de projeto. É o que você, programador, precisa.
Instale sempre o SDK. Ele já inclui o runtime.
Instalando o .NET 10 SDK
Windows. A forma mais simples é pelo gerenciador de pacotes que já vem no sistema. Abra o terminal e rode:
winget install Microsoft.DotNet.SDK.10
Se preferir, baixe o instalador em https://dotnet.microsoft.com/download.
macOS. Baixe o instalador .pkg no mesmo endereço ou, se usar Homebrew:
brew install --cask dotnet-sdk
Linux. Em distribuições recentes do Ubuntu, o SDK está nos repositórios oficiais:
sudo apt update
sudo apt install dotnet-sdk-10.0
Para outras distribuições, siga a página de instalação da Microsoft (link nas referências), que tem instruções para Fedora, Debian, Arch e outras.
Depois de instalar, feche e reabra o terminal e confira:
dotnet --version
A saída deve começar com 10.. Para ver tudo o que está instalado, inclusive versões antigas que podem conviver na mesma máquina, use dotnet --info.
Escolhendo um editor
Qualquer editor de texto serve para escrever C#, mas um bom editor muda a experiência. As opções mais usadas são:
- Visual Studio Code com a extensão C# Dev Kit: gratuito, leve e roda nos três sistemas. É a escolha que vou assumir nos exemplos.
- JetBrains Rider: completo e multiplataforma, gratuito para uso não comercial.
- Visual Studio (a versão "grande", não o Code): o ambiente mais tradicional, só para Windows, com uma edição Community gratuita.
A escolha não afeta nada no curso. Todo exemplo roda pela linha de comando com dotnet, independente do editor.
O primeiro projeto
Entre na pasta desta aula e crie um projeto de console:
dotnet new console -n Ola
cd Ola
dotnet run
A saída é:
Hello, World!
Três comandos, e você tem um programa funcionando. Vamos olhar o que o dotnet new criou:
Ola/
├── Ola.csproj
├── Program.cs
└── obj/
O Program.cs tem uma única linha:
Console.WriteLine("Hello, World!");
Troque o texto e rode de novo:
Console.WriteLine("Olá, C#!");
Console.WriteLine("Este é o meu primeiro programa.");
Console.WriteLine escreve o texto e pula para a linha seguinte. Existe também o Console.Write, que escreve sem pular linha. Repare no ponto e vírgula no fim de cada instrução: em C#, assim como em C, ele é obrigatório.
O que é o .csproj
O arquivo Ola.csproj descreve o projeto para as ferramentas de build:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Linha a linha:
OutputTypecom valorExediz que o projeto gera um programa executável, e não uma biblioteca.TargetFrameworkdiz para qual versão do .NET compilar.ImplicitUsingsimporta automaticamente os namespaces mais comuns. É por isso que você escreveConsolesem precisar declarar de onde ele vem.Nullableliga as verificações de nulidade, que ganham uma aula inteira na Fase 3.
Em C, você chamaria o compilador com uma lista de parâmetros na linha de comando. Em .NET, esses parâmetros ficam registrados nesse arquivo, e qualquer pessoa que clonar seu projeto compila exatamente do mesmo jeito.
O Main que ficou escondido
Se você já viu código C# mais antigo, deve estar estranhando: cadê a classe, cadê o Main? Desde o C# 9, um arquivo pode ter instruções de nível superior (top-level statements): você escreve o código solto e o compilador gera a estrutura por você. O programa acima é equivalente a isto:
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Olá, C#!");
Console.WriteLine("Este é o meu primeiro programa.");
}
}
As duas formas produzem o mesmo resultado. A forma curta é a padrão para programas novos, e vamos usá-la na maior parte do curso. Mas você vai encontrar a forma longa em muito código existente, e ela volta a ser importante quando o programa crescer e tivermos várias classes. A regra é: só um arquivo do projeto pode ter instruções de nível superior, porque só pode existir um ponto de entrada.
Da fonte ao processo: as quatro etapas
Quando você roda dotnet run, acontece o seguinte:
1. Compilação para IL. O compilador de C#, o Roslyn, lê o Program.cs e gera um arquivo Ola.dll dentro de bin/Debug/net10.0/. Esse arquivo não contém código de máquina. Contém IL (Intermediate Language), um conjunto de instruções para uma máquina virtual, que não depende de processador nem de sistema operacional. Junto com o .dll, o build gera um pequeno executável (Ola.exe no Windows, Ola no Linux e no macOS) cuja única função é iniciar o .NET e carregar o .dll.
2. Carregamento pelo CLR. O runtime do .NET, chamado CLR (Common Language Runtime), carrega o .dll e as bibliotecas de que ele depende e prepara a memória do programa. A segurança de tipos vem do compilador e das checagens que o runtime faz durante a execução, como a de limites de array e a de conversões.
3. Compilação JIT. Na primeira vez que um método vai ser executado, o JIT (Just-In-Time compiler) traduz o IL daquele método para código de máquina do seu processador específico: x64, ARM64 ou outro. É uma compilação feita no momento da execução, só para os métodos que realmente são chamados.
4. Execução. O processador executa o código de máquina gerado. Nas chamadas seguintes, o JIT não trabalha de novo: o código já traduzido é reaproveitado.
Dá para ver o IL que o compilador gerou. Cole o seu programa no site https://sharplab.io e escolha "IL" no menu de resultado. Para Console.WriteLine("Olá, C#!");, o trecho principal é:
IL_0000: ldstr "Olá, C#!"
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: ret
Traduzindo: "carregue esta string", "chame o método WriteLine passando-a" e "retorne". É uma linguagem de montagem para uma máquina imaginária.
Por que essa etapa extra? Porque ela traz vantagens concretas. O mesmo .dll roda em qualquer processador e sistema onde exista um runtime .NET. O JIT conhece o processador exato da máquina e pode usar instruções específicas dele. E, como o runtime vê o programa rodando, ele consegue recompilar métodos muito usados com otimizações mais agressivas, a chamada compilação em camadas.
E o custo? A primeira execução de cada método paga o tempo de compilação. Em programas que rodam por muito tempo, como um servidor, isso é irrelevante. Em ferramentas de linha de comando que precisam abrir instantaneamente, pode incomodar. Para esses casos existe a compilação Native AOT, que gera código de máquina antes da execução, como o C faz. Guarde este nome: ela ganha espaço próprio na Fase 10.
Se você vem do C, a comparação direta é esta: o gcc faz as etapas de compilar e ligar e entrega um executável nativo. O C# divide o trabalho: o compilador entrega IL, e a tradução final acontece na máquina de quem roda o programa.
Argumentos e código de saída
Programas de linha de comando recebem argumentos e devolvem um código de saída ao sistema operacional. Nas instruções de nível superior, os argumentos estão disponíveis numa variável chamada args, e você pode encerrar com return:
if (args.Length == 0)
{
Console.Error.WriteLine("Uso: dotnet run -- <seu nome>");
return 1; // código diferente de zero: algo deu errado
}
Console.WriteLine($"Olá, {args[0]}!");
return 0; // zero: sucesso
Para passar argumentos via dotnet run, coloque-os depois de --. Assim o dotnet sabe que eles são do seu programa, e não dele:
dotnet run -- Maria
Olá, Maria!
Duas novidades apareceram aqui. Console.Error.WriteLine escreve na saída de erro, um canal separado da saída normal. Isso permite a quem roda seu programa separar mensagens de erro dos resultados. E $"Olá, {args[0]}!" é uma string interpolada: o que está entre chaves é avaliado e inserido no texto. Ela ganha tratamento completo daqui a duas aulas.
Para ver o código de saída depois de rodar, use echo $? no Linux e no macOS ou echo $LASTEXITCODE no PowerShell.
Um programa de um arquivo só
A partir do .NET 10, dá para rodar um arquivo .cs isolado, sem criar projeto:
dotnet run ola.cs
O SDK cria o projeto por baixo dos panos. É ótimo para experimentar uma ideia rápida ou escrever pequenos scripts. Para o curso, vamos continuar criando projetos com dotnet new, porque eles crescem melhor. Mas use à vontade o formato de arquivo único para testar trechos das aulas.
build, run e publish
Três comandos que você vai usar o tempo todo:
dotnet build: compila e gera o.dllembin/Debug, sem executar.dotnet run: compila, se algo mudou, e executa.dotnet publish -c Release: gera uma versão otimizada, pronta para distribuir, embin/Release/net10.0/publish.
A diferença entre Debug e Release é a mesma do mundo C: em Debug, o código favorece a depuração; em Release, o compilador otimiza. Nunca meça desempenho em Debug.
Fontes e leituras recomendadas
- Instalar o .NET (Microsoft Learn): https://learn.microsoft.com/dotnet/core/install/. Traz instruções atualizadas para cada sistema operacional e distribuição Linux.
- Visão geral da CLI do .NET: https://learn.microsoft.com/dotnet/core/tools/. É a referência completa dos comandos
dotnet new,build,run,publishe outros. - Processo de execução gerenciada: https://learn.microsoft.com/dotnet/standard/managed-execution-process. É a explicação oficial do caminho entre fonte, IL, JIT e execução.
- Instruções de nível superior: https://learn.microsoft.com/dotnet/csharp/fundamentals/program-structure/top-level-statements. Mostra as regras e as limitações da forma curta de programa.
- SharpLab: https://sharplab.io. Mostra o IL e o código de máquina gerados para qualquer trecho de C#, e vamos voltar a ele várias vezes.
- ILSpy: https://github.com/icsharpcode/ILSpy. É um descompilador de código aberto que abre qualquer
.dlle mostra o IL e o C# equivalente.
Exercícios
Exercício 1
Modifique o Program.cs para imprimir seu nome em uma linha e sua cidade na linha seguinte usando uma única chamada de método.
Ver resposta
✓ Resposta: Use a sequência \n, que representa uma quebra de linha dentro da string:
Console.WriteLine("Maria Souza\nCuritiba");
Uma alternativa é Console.Write("Maria Souza\nCuritiba\n");. O WriteLine só acrescenta a quebra de linha final, enquanto o \n do meio precisa estar lá de qualquer jeito.
Exercício 2
Apague o ponto e vírgula do fim de uma linha e rode dotnet build. Qual é o código do erro e o que a mensagem diz?
Ver resposta
✓ Resposta: O compilador responde com o erro CS1002, cuja mensagem diz que era esperado um ;, e indica o arquivo, a linha e a coluna do problema. Todo erro do compilador de C# tem um código CS seguido de números. Pesquisar esse código leva direto à página da documentação que explica a causa e a solução. É um hábito que vale criar desde já.
Exercício 3
Reescreva o programa de argumentos e código de saída na forma clássica, com classe e Main explícitos.
Ver resposta
✓ Resposta: O Main precisa receber string[] args e, como devolve um código, ter tipo de retorno int em vez de void:
using System;
class Program
{
static int Main(string[] args)
{
if (args.Length == 0)
{
Console.Error.WriteLine("Uso: dotnet run -- <seu nome>");
return 1;
}
Console.WriteLine($"Olá, {args[0]}!");
return 0;
}
}
Nas instruções de nível superior, o compilador infere sozinho que o Main gerado deve retornar int, porque encontra return com valor. Na forma clássica, você declara isso explicitamente.
Exercício 4
Rode o programa de argumentos sem passar nenhum nome e depois com um nome, verificando o código de saída a cada vez. Em seguida, no Linux ou no macOS, rode dotnet run -- Ana && echo "deu certo" e depois dotnet run && echo "deu certo". Explique a diferença.
Ver resposta
✓ Resposta: Sem argumentos, o programa escreve a mensagem de uso na saída de erro e termina com código 1. Com um nome, cumprimenta e termina com 0. O operador && do shell só executa o segundo comando se o primeiro terminar com código zero. No primeiro caso aparece "deu certo". No segundo, não aparece, porque o programa devolveu 1. É por esse mecanismo que scripts e ferramentas de automação sabem se um programa funcionou. Devolver o código de saída correto é o que faz seu programa se comportar bem nessas cadeias de comandos.
Exercício 5
No SharpLab, compile int x = 2 + 3; Console.WriteLine(x); e observe o IL. Onde está a soma?
Ver resposta
✓ Resposta: A soma não está lá. O IL mostra algo como ldc.i4.5, que significa "carregue a constante inteira 5", seguido da chamada a WriteLine. Como 2 + 3 é uma expressão constante, o próprio compilador de C# calcula o resultado durante a compilação e grava apenas o 5. Isso se chama constant folding. A lição vai além do exemplo: nem tudo que você escreve vira instrução em tempo de execução, e ferramentas como o SharpLab mostram o que realmente chega à máquina.