Zig para Programadores C#

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#ZigNotas
classstructZig não tem classes nem herança
interfaceComptime duck typingSem vtable
var x = 42const x = 42Inferência de tipo
List<T>std.ArrayList(T)Requer allocator
Dictionary<K,V>std.HashMap(K,V,...)Requer allocator
try/catchtry/catch (error unions)Sem exceções
usingdeferLimpeza de recursos
async/awaitSem equivalente diretoUsar threads
nullnull (optional)Tipo ?T
Nullable<T>?TOptionals nativos
GenericscomptimeResolvido em compilação
GCAllocators explícitosDiferença fundamental
string[]const u8Bytes, não chars
LINQLoops explícitosSem 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.

Para começar, visite Introdução ao Zig e Como Instalar Zig. Se quiser entender quando Zig é a ferramenta certa, leia Quando Usar Zig.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.