---
title: "Cheatsheet: Flyweight em Zig"
url: "https://ziglang.com.br/padroes/cheatsheet-flyweight-em-zig/"
markdown_url: "https://ziglang.com.br/padroes/cheatsheet-flyweight-em-zig.MD"
description: "Design pattern Flyweight implementado em Zig: compartilhar estado para suportar muitos objetos eficientemente, interning de strings e cache de dados imutáveis. Guia completo em português."
date: "2026-02-21"
author: "Zig Brasil"
---

# Cheatsheet: Flyweight em Zig

Design pattern Flyweight implementado em Zig: compartilhar estado para suportar muitos objetos eficientemente, interning de strings e cache de dados imutáveis. Guia completo em português.


# Flyweight em Zig

O padrão Flyweight reduz o uso de memória compartilhando o máximo de dados possível entre objetos similares. Em vez de cada objeto armazenar seus próprios dados, dados comuns (estado intrínseco) são compartilhados, enquanto dados únicos (estado extrínseco) são passados externamente. Em Zig, com controle manual de memória, este padrão é especialmente poderoso.

## Quando Usar

- Renderização de texto (milhares de caracteres, poucos glifos)
- Terrenos de jogos (milhões de tiles, poucos tipos)
- String interning — deduplicação de strings repetidas
- Partículas em jogos e simulações
- Cache de objetos imutáveis

## Implementação: Tilemap com Flyweight

```zig
const std = @import("std");

const TipoTile = struct {
    nome: []const u8,
    cor: u32,
    transitavel: bool,
    velocidade: f32,
};

// Dados compartilhados (flyweight) — poucos tipos
const TIPOS_TILE = [_]TipoTile{
    .{ .nome = "grama", .cor = 0x00FF00, .transitavel = true, .velocidade = 1.0 },
    .{ .nome = "agua", .cor = 0x0000FF, .transitavel = false, .velocidade = 0.0 },
    .{ .nome = "areia", .cor = 0xFFFF00, .transitavel = true, .velocidade = 0.7 },
    .{ .nome = "pedra", .cor = 0x888888, .transitavel = true, .velocidade = 0.9 },
    .{ .nome = "lava", .cor = 0xFF0000, .transitavel = false, .velocidade = 0.0 },
};

// Dados por instância (extrínseco) — apenas o índice do tipo
const Tile = struct {
    tipo_idx: u8, // 1 byte em vez de toda a struct TipoTile
    elevacao: i8,  // dado único por tile

    pub fn tipo(self: Tile) TipoTile {
        return TIPOS_TILE[self.tipo_idx];
    }
};

// Mapa 1000x1000 = 1M tiles, usando apenas 2 bytes cada
const LARGURA = 1000;
const ALTURA = 1000;

const Mapa = struct {
    tiles: [LARGURA * ALTURA]Tile,

    pub fn obterTile(self: *const Mapa, x: usize, y: usize) Tile {
        return self.tiles[y * LARGURA + x];
    }

    pub fn tipoDoTile(self: *const Mapa, x: usize, y: usize) TipoTile {
        return self.obterTile(x, y).tipo();
    }
};
// Sem flyweight: 1M * ~50 bytes = ~50MB
// Com flyweight: 1M * 2 bytes + 5 * ~50 bytes = ~2MB
```

## String Interning

```zig
const std = @import("std");

const StringInterner = struct {
    strings: std.StringHashMap(void),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) StringInterner {
        return .{
            .strings = std.StringHashMap(void).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *StringInterner) void {
        var iter = self.strings.keyIterator();
        while (iter.next()) |key| {
            self.allocator.free(key.*);
        }
        self.strings.deinit();
    }

    pub fn intern(self: *StringInterner, texto: []const u8) ![]const u8 {
        const resultado = try self.strings.getOrPut(texto);
        if (!resultado.found_existing) {
            // Primeira vez — alocar cópia
            resultado.key_ptr.* = try self.allocator.dupe(u8, texto);
        }
        return resultado.key_ptr.*;
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var interner = StringInterner.init(gpa.allocator());
    defer interner.deinit();

    // Mesma string na memória — só uma alocação
    const a = try interner.intern("hello");
    const b = try interner.intern("hello");
    std.debug.print("Mesmo ponteiro: {}\n", .{a.ptr == b.ptr}); // true
}
```

## Flyweight com Packed Structs para Máxima Economia

Em Zig, `packed struct` permite controle bit-a-bit do layout de memória, maximizando a densidade de dados extrínsecos:

```zig
// Tile com dados extrínsecos ultra-compactos: apenas 8 bits por tile
const TileCompacto = packed struct(u8) {
    tipo_idx: u3,      // 8 tipos possíveis (0-7)
    elevacao: i3,      // elevação de -4 a +3
    visivel: bool,     // 1 bit
    explorado: bool,   // 1 bit
};

// Mapa 1000x1000 = 1M tiles * 1 byte = apenas 1MB
const MapaCompacto = struct {
    tiles: [1000 * 1000]TileCompacto,
};

// Sem flyweight + sem packed: 1M * ~50 bytes ≈ 50MB
// Com flyweight + packed:    1M *  1 byte  ≈  1MB
```

A combinação de Flyweight (estado intrínseco compartilhado) com `packed struct` (estado extrínseco denso) é extremamente poderosa para jogos e simulações.

## Considerações de Performance

- **Cache locality é o maior benefício**: ao reduzir o tamanho por objeto (de 50 bytes para 1-2 bytes), muito mais tiles cabem em uma linha de cache. Iterações sobre o mapa ficam entre 10x e 50x mais rápidas em benchmarks reais.
- **String interning reduz comparações de igualdade**: após o interning, comparar se duas strings são iguais é apenas comparar ponteiros (`a.ptr == b.ptr`) — O(1) em vez de O(n). Útil para parsers, compiladores e sistemas com muitos identificadores repetidos.
- **HashMap de interning tem custo de lookup**: cada chamada a `intern` faz um lookup no HashMap. Se você está internando a mesma string em um loop, salve o resultado e reutilize.
- **Estado intrínseco deve ser imutável**: se você precisar mutá-lo, crie um novo flyweight. Estado mutável compartilhado requer sincronização e elimina a maioria dos benefícios do padrão.

## Erros Comuns

**Compartilhar flyweights entre threads sem sincronização**: se múltiplas threads acessam o `StringInterner.intern` simultaneamente, o `StringHashMap` interno pode ser corrompido. Adicione um `Mutex` ou use um interner por thread com merge posterior.

**Liberar o flyweight enquanto clientes ainda o referenciam**: no exemplo de `StringInterner`, liberar a string internada (`allocator.free(key.*)`) enquanto alguém ainda usa o ponteiro retornado por `intern` causa undefined behavior. O flyweight deve viver pelo menos tanto quanto todos os seus consumidores.

**Usar flyweight para objetos genuinamente únicos**: se cada objeto tem dados diferentes (IDs únicos, timestamps, posições distintas), o flyweight não economiza nada — você ainda precisa de um objeto por instância.

## Perguntas Frequentes

**Qual é a diferença entre Flyweight e Pool de Objetos?**
O Pool de Objetos reutiliza objetos completos para evitar alocação frequente — cada cliente tem acesso exclusivo ao objeto enquanto o usa. O Flyweight compartilha o estado intrínseco *simultaneamente* entre múltiplos clientes — todos os tiles do tipo "grama" apontam para o mesmo `TipoTile` ao mesmo tempo.

**Como saber se vale implementar Flyweight?**
Pergunte: quantos objetos únicos existem vs quantas instâncias? Se você tem 5 tipos de tile mas 1 milhão de instâncias, o Flyweight economiza memória proporcionalmente. Se a relação é próxima de 1:1, o padrão não ajuda.

## Quando Evitar

- Poucos objetos (overhead do flyweight supera o benefício)
- Quando cada objeto é genuinamente único
- Dados mutáveis que não podem ser compartilhados
- Quando a complexidade extra não se justifica

## Veja Também

- [Pool de Objetos](/padroes/pool-objetos/) — Reutilizar objetos pré-alocados
- [Arena Pattern](/padroes/arena-pattern/) — Alocação eficiente em bloco
- [Allocators](/cheatsheets/allocators/) — Controle de memória
- [Structs](/cheatsheets/structs/) — Packed structs para economia de memória
- [FAQ Performance](/faq/faq-performance/) — Otimização de uso de memória
