Introdução a Zig - Parte 2

Tipos de dados e structs

Enzo Soares

Publicado em 20 de Março de 2025 às 10:44

Ahoy, programador!

No blog de hoje, vou tratar de uma das bases de toda linguagem de programação: tipos de dados. No nível mais básico, todo valor armazenado por um computador é uma sequência de uns e zeros, o que é completamente inutilizável por nós humanos. Nós damos tipos a sequências específicas de binário como uma abstração que nos permitem definir que tipos de operações podemos fazer de forma segura com aquele dados e o que elas significam. Por exemplo, definir a sequência binária 01000001001000000000000000000000 como um número inteiro sem sinal nos permite convertê-la ao número 1092616192. No entanto, a mesma sequência, se definida como uma representação de ponto flutuante, é apenas o número 10.

Tipos primitivos

Os tipos primitivos usados em Zig podem ser encontrados aqui, mas os mais relevantes são:

  1. ux
  2. ix
  3. fx
  4. bool
  5. type
  6. void

O x no final de ux, ix e fx indica que o tipo pode ter a precisão de x bits, sendo x uma potência de dois. Então ux na verdade pode ser u16, u32, etc. O u indica que é um inteiro sem sinal, i que é um inteiro com sinal e f que é uma representação de ponto flutuante, que nada mais é do que uma notação científica em binário. Zig também suporta precisões arbitrárias, o que quer dizer que u7777 é um tipo válido. bool representa verdadeiro ou falso e é armazenado em apenas um bit. type é o tipo dos tipos e void representa a falta de um tipo.

Por causa dessas diferenças de significado, valores que têm tipos diferentes com frequência não podem interagir entre si. Por exemplo, não faz sentido adicionar uma variável do tipo u8 a uma do tipo f16 sem algum tipo de conversão entre elas. No Zig, essa conversão específica é realizada implicitamente, mas isso só pode acontecer se não houver perda de dados NENHUMA no operação.

Para o caso de conversões explícitas, temos algumas ferramentas:

@as()

A função @as nos permite converter valores caso a operação seja segura, ou seja, não há perda de dados e o tipo de destino comporta inteiramente o tipo original.

var x : u8 = 5;
var y = @as(u32, x);
var z = @as(i32, x);
var error = @as(bool, x);

As duas primeiras conversões são seguras e podem ser realizadas sem problemas. Já a última não é segura, já que 1 byte é maior do que 1 bit.

@truncate()

@truncate nos permite converter tipos maiores em tipos menores, como u16 para u8, ao remover todos os bits mais significativos que não couberem no destino

var x: u16 = 257; //0000000100000001
var z: u8 = @truncate(u8, x) //00000001

Pode haver perda de dados ao usar essa função :(.

@bitCast()

Essa função apenas funciona se os tipos destino e origem tiverem o mesmo tamanho, como i64 e u64. Nenhum dado é alterado, apenas a interpretação (tipo) é.

var x: u32 = 1092616192;
var z: i32 = @bitCast(i32, x)

No exemplo acima, z tem o valor de 10.

Structs

Structs são tipos compostos de dados, o que quer dizer são junções de um ou mais tipos primitivos e são utilizados para agrupar valores.

const Point = struct {
    x: f32,
    y: f32,
    z: f32,
};

var point = Point{
   .x = 1,
   .y = 2,
   .z = 3,
}

std.debug.print("{d}", .{point.x}); //1
std.debug.print("{d}", .{point.y}); //2
std.debug.print("{d}", .{point.z}); //3

Nesse exemplo, foi definido uma nova struct Point com três pontos: x, y e z. Logo em seguida, criamos uma instância chamada point, definimos os valores dos atributos e acessamos através da notação ..

Structs podem ter atributos que são outras structs ou até mesmo funções:

const Chair = struct {
    color: []u8,
};

const Table = struct {
    length: f32,
    width: f32,
    first_chair: Chair,
    second_chair: Chair,

    pub fn init(length: f32, width: f32, first_chair: Chair, second_chair: Chair) Table {
        return Table{
            .length = length,
            .width = width,
            .first_chair = first_chair,
            .second_chair = second_chair,
        };
    }
};

Esses métodos das Structs podem até modificar ela própria, da seguinte forma:

const Table = struct {
    length: f32,
    width: f32,
    first_chair: Chair,
    second_chair: Chair,

    const Self = @This();

    pub fn init(length: f32, width: f32, first_chair: Chair, second_chair: Chair) Table {
        return Table{
            .length = length,
            .width = width,
            .first_chair = first_chair,
            .second_chair = second_chair,
        };
    }

    pub fn break_in_half(self: *Self) void {
        self.length /= 2;
        self.width /= 2;
    }
};

const Self = @This() é um utilitário que pega uma referência ao tipo da struct atual e coloca na constante Self. É um bom padrão para se seguir: ele torna mais fácil alterar o nome da struct no futuro, mas é opcional.

Isso é tudo por hoje!