Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar

Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar

Instalar o .NET 10 e entender o caminho do código até o processador: SDK e runtime, o primeiro projeto e o .csproj, o Main escondido nas instruções de nível superior, IL, CLR e JIT, argumentos e código de saída, o programa de arquivo único e os comandos build, run e publish.
Linguagem C#

• • 12 min de leitura

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:

  • OutputType com valor Exe diz que o projeto gera um programa executável, e não uma biblioteca.
  • TargetFramework diz para qual versão do .NET compilar.
  • ImplicitUsings importa automaticamente os namespaces mais comuns. É por isso que você escreve Console sem precisar declarar de onde ele vem.
  • Nullable liga 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 .dll em bin/Debug, sem executar.
  • dotnet run: compila, se algo mudou, e executa.
  • dotnet publish -c Release: gera uma versão otimizada, pronta para distribuir, em bin/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

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.

Comentários

Mais em Linguagem C#

Validação, ProblemDetails e OpenAPI: uma API que explica a si mesma
Validação, ProblemDetails e OpenAPI: uma API que explica a si mesma

Uma API que diz ao cliente o que ele errou: validação nativa do .NET 10 com…

Tempo real com SignalR: quando o servidor toma a iniciativa
Tempo real com SignalR: quando o servidor toma a iniciativa

A Livraria passa a avisar quem está conectado: polling, SSE e WebSockets, hubs…

Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos
Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos

A Livraria olhada como um todo: a regra de dependência de Clean Architecture e…