Introdução
Se você é desenvolvedor C#, Zig vai representar uma mudança significativa de paradigma. C# é uma linguagem de alto nível com garbage collector, classes, herança, LINQ e um ecossistema rico (.NET). Zig é uma linguagem de sistemas sem GC, sem classes, sem exceções e com controle manual de memória.
Isso não significa que Zig é “pior” — apenas que opera em outro nível. Este guia ajuda você a traduzir conceitos familiares de C# para Zig. Para outras perspectivas, veja Zig para Programadores Kotlin e Zig para Programadores Java.
Mapeamento de Conceitos
| C# | Zig | Notas |
|---|---|---|
class | struct | Zig não tem classes nem herança |
interface | Comptime duck typing | Sem vtable |
var x = 42 | const x = 42 | Inferência de tipo |
List<T> | std.ArrayList(T) | Requer allocator |
Dictionary<K,V> | std.HashMap(K,V,...) | Requer allocator |
try/catch | try/catch (error unions) | Sem exceções |
using | defer | Limpeza de recursos |
async/await | Sem equivalente direto | Usar threads |
null | null (optional) | Tipo ?T |
Nullable<T> | ?T | Optionals nativos |
| Generics | comptime | Resolvido em compilação |
| GC | Allocators explícitos | Diferença fundamental |
string | []const u8 | Bytes, não chars |
| LINQ | Loops explícitos | Sem query syntax |
Classes vs Structs
C# usa classes com herança. Zig usa structs sem herança:
C#
public class Animal {
public string Nome { get; set; }
public virtual void Falar() => Console.WriteLine("...");
}
public class Cachorro : Animal {
public override void Falar() => Console.WriteLine("Au au!");
}
Zig
const Animal = struct {
nome: []const u8,
falarFn: *const fn (*const Animal) void,
pub fn falar(self: *const Animal) void {
self.falarFn(self);
}
};
fn falarCachorro(_: *const Animal) void {
std.debug.print("Au au!\n", .{});
}
const cachorro = Animal{
.nome = "Rex",
.falarFn = falarCachorro,
};
Em vez de herança, Zig usa composição e ponteiros de função quando polimorfismo de runtime é necessário. Para polimorfismo de compilação, use comptime:
fn alimentar(animal: anytype) void {
animal.comer();
}
Generics vs Comptime
C#
public class Pilha<T> {
private List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop() => _items[^1]; // simplificado
}
var pilha = new Pilha<int>();
pilha.Push(42);
Zig
fn Pilha(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(allocator) };
}
pub fn deinit(self: *Self) void {
self.items.deinit();
}
pub fn push(self: *Self, item: T) !void {
try self.items.append(item);
}
pub fn pop(self: *Self) ?T {
return self.items.popOrNull();
}
};
}
var pilha = Pilha(u32).init(allocator);
defer pilha.deinit();
try pilha.push(42);
A diferença fundamental: generics em C# existem em runtime (com type erasure parcial para value types); em Zig, comptime gera código especializado para cada tipo em tempo de compilação.
Error Handling
C#
try {
var conteudo = File.ReadAllText("config.json");
var config = JsonSerializer.Deserialize<Config>(conteudo);
}
catch (FileNotFoundException ex) {
Console.WriteLine($"Arquivo não encontrado: {ex.FileName}");
}
catch (JsonException ex) {
Console.WriteLine($"JSON inválido: {ex.Message}");
}
Zig
const config = lerConfig("config.json") catch |err| switch (err) {
error.FileNotFound => {
std.debug.print("Arquivo não encontrado\n", .{});
return;
},
error.InvalidJson => {
std.debug.print("JSON inválido\n", .{});
return;
},
else => return err,
};
Zig não tem exceções. Erros são valores de retorno tipados. O compilador garante que todo erro seja tratado ou propagado com try. Veja Error Sets Customizados e Error Logging.
Nullable e Optional
C#
int? valor = null;
if (valor.HasValue) {
Console.WriteLine(valor.Value);
}
// Ou com pattern matching
if (valor is int v) {
Console.WriteLine(v);
}
Zig
const valor: ?i32 = null;
if (valor) |v| {
std.debug.print("{}\n", .{v});
}
// Ou com orelse
const seguro = valor orelse 0;
Gerenciamento de Memória
Esta é a maior mudança para programadores C#. Em C#, o GC gerencia tudo. Em Zig, você é responsável:
C#
var lista = new List<string>();
lista.Add("Olá");
lista.Add("Mundo");
// GC libera quando não há mais referências
Zig
var lista = std.ArrayList([]const u8).init(allocator);
defer lista.deinit(); // VOCÊ libera
try lista.append("Olá");
try lista.append("Mundo");
O padrão em Zig é init + defer deinit — similar ao using em C# mas mais explícito. Veja ArenaAllocator e GeneralPurposeAllocator.
LINQ vs Loops Explícitos
C# tem LINQ para consultas declarativas. Zig usa loops explícitos:
C#
var resultado = numeros
.Where(n => n > 10)
.Select(n => n * 2)
.OrderBy(n => n)
.ToList();
Zig
var resultado = std.ArrayList(i32).init(allocator);
defer resultado.deinit();
for (numeros) |n| {
if (n > 10) {
try resultado.append(n * 2);
}
}
std.mem.sort(i32, resultado.items, {}, std.sort.asc(i32));
Zig é mais verboso aqui, mas o código é mais previsível em termos de alocações e performance.
using vs defer
C#
using var stream = File.OpenRead("dados.bin");
using var reader = new BinaryReader(stream);
var dados = reader.ReadBytes(1024);
Zig
const arquivo = try std.fs.cwd().openFile("dados.bin", .{});
defer arquivo.close();
var buffer: [1024]u8 = undefined;
const lidos = try arquivo.readAll(&buffer);
defer em Zig funciona no escopo do bloco e executa quando o bloco termina, independentemente de erro ou retorno normal. errdefer executa apenas em caso de erro — sem equivalente em C#. Veja Padrões Errdefer.
Testes
C#
[Test]
public void TestSoma() {
Assert.AreEqual(5, Calculadora.Soma(2, 3));
}
Zig
test "soma" {
try std.testing.expectEqual(@as(i32, 5), soma(2, 3));
}
Testes em Zig são inline no código fonte e executados com zig test. Veja Testes Unitários Básicos e Testes com Allocator.
Conclusão
A transição de C# para Zig é significativa — você troca a conveniência de uma linguagem gerenciada pelo controle total de uma linguagem de sistemas. As maiores adaptações serão o gerenciamento manual de memória, a ausência de classes/herança, e a troca de exceções por error unions.
O benefício é código sem overhead de runtime, binários pequenos, performance previsível, e a capacidade de trabalhar em qualquer nível — de drivers de hardware a aplicações de servidor.
Se você está considerando a transição de uma linguagem gerenciada para uma linguagem de sistemas, veja também nosso portal sobre Kotlin, que compartilha desafios semelhantes na migração para programação de mais baixo nível.
Para começar, visite Introdução ao Zig e Como Instalar Zig. Se quiser entender quando Zig é a ferramenta certa, leia Quando Usar Zig.