Top-Level Programs, Implicit Usings e Namespaces
Objective
Um projeto de console novo tem uma linha de código, e Console resolve sem
nenhum using. Não há mágica: o compilador envolve seus comandos numa classe e
num método gerados, e o SDK escreve um arquivo oculto cheio de diretivas
global using. Este conceito mostra o que é gerado, as regras de um
top-level program, como ImplicitUsings, global using, using static e
aliases de using se combinam, como funcionam namespaces com escopo de arquivo
e como resolver um conflito de nomes com alias ou global::.
Use Cases
- Ler um stack trace que menciona
Program.<Main>$e saber de onde veio. - Tornar o
Programgerado visível para um projeto de testes (testes de integração que precisam do tipo do ponto de entrada). - Corrigir um erro de "referência ambígua" causado por um using implícito.
- Manter imports compartilhados num lugar só com
global usingou itens<Using>. - Dar um nome legível a uma tupla, um array ou um tipo genérico comprido.
Deep Dive
Em que um top-level program se transforma
Um arquivo com top-level statements é compilado para um método de ponto de
entrada dentro de uma classe chamada Program, no namespace global. Segundo a
documentação, a forma do método gerado (mostrado como Main por conveniência)
depende do que o seu código contém:
| O código contém | Ponto de entrada |
|---|---|
await e return |
static async Task<int> Main(string[] args) |
await |
static async Task Main(string[] args) |
return |
static int Main(string[] args) |
| nenhum dos dois | static void Main(string[] args) |
Por isso args existe sem ser declarado, return 3; define o código de saída
e await funciona no topo. Os nomes reais não são Main. Um programa que lança
exceção numa função local imprime este stack trace no .NET 10:
csharpvoid Fail() => throw new InvalidOperationException("boom");
try { Fail(); } catch (Exception e) { Console.WriteLine(e.StackTrace); }
// at Program.<<Main>$>g__Fail|0_0()
// at Program.<Main>$(String[] args)O método é <Main>$ (um nome que nenhum código C# consegue escrever) e a sua
função local virou um método de Program com nome gerado pelo compilador. Num
projeto com await, reflexão sobre Program listou <Main>$, mais um
wrapper <Main> sintetizado e as funções locais com nomes do compilador. A
classe Program é internal por padrão: typeof(Program).IsPublic imprimiu
False e Namespace imprimiu string vazia.
As regras
- Só um arquivo do projeto pode ter top-level statements. Um segundo arquivo
falha com
CS8802: Only one compilation unit can have top-level statements. - Você pode escrever um método chamado
Main, mas ele não será o ponto de entrada, e a opção de compilador-mainnão consegue escolher um. - Os
usingvêm primeiro. Namespaces e declarações de tipo precisam vir depois dos comandos, e os top-level statements estão implicitamente no namespace global, então um namespace declarado no mesmo arquivo não os envolve.
Para tornar o Program acessível a outro assembly, declare uma classe parcial
em qualquer arquivo:
csharppublic partial class Program { }Compilou e typeof(Program).IsPublic imprimiu True. É a forma padrão de
deixar um projeto de testes referenciar o tipo do ponto de entrada.
De onde vem o Console: ImplicitUsings
Desde o .NET 6, projetos C# no estilo SDK podem habilitar
<ImplicitUsings>enable</ImplicitUsings>. O SDK então gera um arquivo em
obj/ com um global using para cada namespace de um conjunto padrão que
depende do SDK:
Microsoft.NET.Sdk:System,System.Collections.Generic,System.IO,System.Linq,System.Net.Http,System.Threading,System.Threading.Tasks.Microsoft.NET.Sdk.Web: esse conjunto maisSystem.Net.Http.Jsone os namespaces do ASP.NET Core e deMicrosoft.Extensionspara hosting, configuração, injeção de dependência, roteamento e logging.Microsoft.NET.Sdk.Worker: o conjunto base mais os namespaces de configuração, injeção de dependência, hosting e logging deMicrosoft.Extensions.
O arquivo gerado na minha execução (obj/Debug/net10.0/<nome>.GlobalUsings.g.cs)
tinha exatamente os sete namespaces base como global using System; e assim
por diante. Você personaliza o conjunto com itens MSBuild:
plaintext<ItemGroup> <Using Include="System.Globalization" /> <Using Include="System.Text.Json" Alias="Json" /> <Using Include="System.Math" Static="true" /> <Using Remove="System.Net.Http" /> </ItemGroup>
As quatro formas compilaram e funcionaram num projeto de console .NET 10: um namespace, um alias, um import estático e uma remoção.
global using, using static e aliases
global using tem o efeito de adicionar a mesma diretiva a todos os arquivos da
compilação. Ele precisa vir antes de todos os usings não globais e antes de
qualquer declaração de namespace ou tipo, e a ordem entre os usings globais não
importa. using static importa os membros estáticos e os tipos aninhados de um
tipo, então using static System.Console; permite escrever WriteLine. Dá para
combinar os dois (global using static System.Math;). Extension members
declarados num tipo de using static ficam disponíveis para a busca de
extensões, mas os nomes deles não são importados para uso sem qualificação.
Um alias dá nome a um namespace ou a um tipo. Desde o C# 12 o alvo pode ser qualquer tipo, incluindo tuplas e arrays (a documentação cita também ponteiros e outros tipos unsafe):
csharpusing Point = (int X, int Y);
using Matrix = int[,];
using Cache = System.Collections.Generic.Dictionary<string, int>;
Point p = (1, 2);
Matrix m = new int[2, 2];
Cache c = new() { ["a"] = 1 };
Console.WriteLine($"{p} {m.Length} {c.Count}"); // (1, 2) 4 1Um alias não pode ser um genérico aberto (List<T> não vale, List<int> vale),
e o lado direito precisa ser totalmente qualificado: não pode usar outro alias.
Namespaces com escopo de arquivo e conflitos
namespace Acme.Billing; (C# 10) coloca o arquivo inteiro no namespace sem um
nível de aninhamento. Um arquivo só pode ter um, e misturar com um namespace de
bloco falha com CS8955. Namespaces de bloco continuam necessários quando um
arquivo declara vários namespaces.
Um using implícito pode criar ambiguidade. Com using System.Timers; num
projeto com implicit usings, isto falha:
csharpusing System.Timers;
Timer t = new(100);
// error CS0104: 'Timer' is an ambiguous reference between
// 'System.Timers.Timer' and 'System.Threading.Timer'Resolva com um alias (using Timer = System.Timers.Timer;) ou o nome completo.
Quando um tipo seu esconde um do framework, global:: começa a busca na raiz:
com Acme.Data.File declarado, typeof(File) continuou significando
System.IO.File no arquivo top-level, e typeof(global::Acme.Data.File)
nomeou o seu. A forma alias::Nome (por exemplo S::Socket) só funciona
quando o lado esquerdo é um alias de namespace.
Trade-offs
- Top-level statements tiram cerimônia e escondem o ponto de entrada. Bom
para ferramentas e scripts pequenos; o
Programgerado surpreende quem procurastatic void Main. Mitigação: conheça o nome<Main>$nos stack traces e use umMainexplícito ou uma classe quando o time preferir. - Implicit usings reduzem ruído e tornam invisíveis as dependências de um
arquivo. O leitor não vê de onde vem
Task, e um namespace implícito novo pode criar ambiguidade (o exemplo doTimer). Mitigação: mantenha curta a lista de<Using>e desabiliteImplicitUsingsem bibliotecas que querem imports explícitos. global usingeusing staticencurtam o código e o enfraquecem. Um import estático deConsoleé aceitável num script, mas umusing staticde um tipo com nomes comuns (Math,File) deixa as chamadas ambíguas para quem lê. Mantenha os usings globais num arquivo só.- Aliases nomeiam formas, não tipos.
using Point = (int X, int Y);é uma ferramenta de legibilidade: não cria um tipo novo, então umPointe qualquer outro(int, int)são intercambiáveis e não carregam comportamento. Use umrecord structquando o nome precisa significar algo para o compilador.
Documentation Links
- Top-level statements, C# fundamentals, Microsoft Learn
- The using directive, C# reference, Microsoft Learn
- .NET project SDK overview, implicit using directives, Microsoft Learn
- MSBuild properties for .NET SDK projects, Microsoft Learn
- Namespaces, C# fundamentals, Microsoft Learn
- What's new in C# 12, Microsoft Learn