Este texto é inspirado em A Half Hour to Learn Rust.
O comando zig run my_code.zig
compila e executa imediatamente seu programa
Zig. Cada um destes trechos de código contém um programa Zig que você pode
testar. Alguns deles contêm erros de compilação que você pode comentar.
Você precisará declarar uma função main()
para começar a executar código.
Este é um programa que não faz nada:
// comments look like this and go to the end of the line
pub fn main() void {}
Você pode importar a biblioteca padrão usando o @import
da linguagem e
atribuindo o namespace a um identificador. Quase tudo em Zig precisa ser
explicitamente atribuído a um identificador. Você também pode importar outros
arquivos Zig desta forma, e arquivos C de maneira semelhante mediante
@cImport
.
const std = @import("std");
pub fn main() void {
std.debug.print("hello world!\n", .{});
}
Nota: eu explicarei o segundo parâmetro engraçado no comando print
mais
tarde na seção sobre structs
.
var
declara uma variável. Na maioria dos casos você deve declarar o seu
tipo.
const std = @import("std");
pub fn main() void {
var x: i32 = 47; // declares "x" of type i32 to be 47.
std.debug.print("x: {}\n", .{x});
}
const
declara que o valor de uma variável é imutável.
pub fn main() void {
const x: i32 = 47;
x = 42; // error: cannot assign to constant
}
Zig é bastante rigoroso, e não permitirá que você sombreie identificadores de um escopo mais externo, a fim de evitar que você se confunda:
const x: i32 = 47;
pub fn main() void {
var x: i32 = 42; // error: redefinition of 'x'
}
Constantes no escopo global são valores de tempo de compilação (comptime) por padrão, e se você omitir o tipo elas são tipadas em comptime e podem se tornar em tipos de tempo de execução para seus valores de tempo de execução.
const x: i32 = 47;
const y = -47; // comptime integer.
pub fn main() void {
var a: i32 = y; // comptime constant coerced into correct type
var b: i64 = y; // comptime constant coerced into correct type
var c: u32 = y; // error: cannot cast negative value -47 to unsigned integer
}
Você pode explicitamente escolher deixar a variável indefinida se ela for
receber um valor no futuro. Zig irá definir um valor fictício com os bytes
0XAA
, a fim de ajudar na detecção de erros, no caso de você provocar um
erro ao usar acidentalmente estes valores no momento da depuração.
const std = @import("std");
pub fn main() void {
var x: i32 = undefined;
std.debug.print("undefined: {}\n", .{x});
}
Em alguns casos, Zig te permitirá omitir a informação de tipo se ele puder descobrir.
const std = @import("std");
pub fn main() void {
var x: i32 = 47;
var y: i32 = 47;
var z = x + y; // declares z and sets it to 94.
std.debug.print("z: {}\n", .{z});
}
Mas seja cuidadoso, inteiros literais são tipados em comptime, logo isto aqui não vai funcionar:
pub fn main() void {
var x = 47; // error: variable of type 'comptime_int' must be const or comptime
}
Eis uma função (foo
) que não retorna nada. A palavra-chave pub
significa
que a função é exportável a partir do escopo corrente; é por isso que
main()
precisa ser pub
. Você chama funções da mesma forma que faria na
maioria das linguagens de programação:
const std = @import("std");
fn foo() void {
std.debug.print("foo!\n", .{});
//optional:
return;
}
pub fn main() void {
foo();
}
Eis uma função que retorna um valor inteiro:
const std = @import("std");
fn foo() i32 {
return 47;
}
pub fn main() void {
var result = foo();
std.debug.print("foo: {}\n", .{result});
}
Zig não te permite ignorar os valores de retorno das funções:
fn foo() i32 {
return 47;
}
pub fn main() void {
foo(); // error: expression value is ignored
}
Mas você pode fazê-lo “explicitamente” se atribuir este retorno à variável de
descarte _
.
fn foo() i32 {
return 47;
}
pub fn main() void {
_ = foo();
}
Você pode fazer uma função que pode receber um parâmetro, declarando seu tipo:
const std = @import("std");
fn foo(x: i32) void {
std.debug.print("foo param: {}\n", .{x});
}
pub fn main() void {
foo(47);
}
Estruturas (structs) são declaradas atribuindo a elas um nome, usando a
palavra-chave const
. Elas podem receber valores fora de ordem, e podem ser
utilizadas mediante de-referência, com a sintaxe usual de ponto .
.
const std = @import("std");
const Vec2 = struct{
x: f64,
y: f64
};
pub fn main() void {
var v = Vec2{.y = 1.0, .x = 2.0};
std.debug.print("v: {}\n", .{v});
}
Structs podem ter valores default. Structs também podem ser anônimas, e podem ser convertidos (coerced) para outra struct desde que todos os valores possam ser inferidos:
const std = @import("std");
const Vec3 = struct{
x: f64 = 0.0,
y: f64,
z: f64
};
pub fn main() void {
var v: Vec3 = .{.y = 0.1, .z = 0.2}; // ok
var w: Vec3 = .{.y = 0.1}; // error: missing field: 'z'
std.debug.print("v: {}\n", .{v});
}
Você pode inserir funções dentro de uma struct a fim de fazer com que ela
funcione como um objeto similar à programação orientada a objetos. Tem-se o
açúcar sintático onde se você fizer o primeiro parâmetro da função ser um
ponteiro para o objeto, ele pode ser chamado à moda da orientação a objetos,
semelhante a como Python usa as funções com parâmetro self. A convenção
típica é tornar isto óbvio dando à variável o nome self
.
const std = @import("std");
const LikeAnObject = struct{
value: i32,
fn print(self: *LikeAnObject) void {
std.debug.print("value: {}\n", .{self.value});
}
};
pub fn main() void {
var obj = LikeAnObject{.value = 47};
obj.print();
}
A propósito, esta coisa que passamos no segundo parâmetro de
std.debug.print
é uma tupla. Sem entrar em detalhes, é uma struct anônima
com campos numerados. Em tempo de compilação, std.debug.print
infere os
tipos dos parâmetros desta tupla e gera uma versão de si mesma sintonizada
para a string de parâmetros que você forneceu - e é assim que Zig sabe como
fazer o conteúdo desta impressão ser bem formatado!
const std = @import("std");
pub fn main() void {
std.debug.print("{}\n", .{1, 2}); # error: Unused arguments
}
Enumerações (enums) são declaradas atribuindo o grupo de enums como um tipo
usando a palavra-chave const
.
Note:
- Em alguns casos você pode encurtar o nome da enum.
- Você pode estabelecer um valor de uma Enum para um inteiro, mas ele não é
automaticamente convertido, você precisar usar
@enumToInt
ou@intToEnum
para realizar as conversões.
const std = @import("std");
const EnumType = enum{
EnumOne,
EnumTwo,
EnumThree = 3
};
pub fn main() void {
std.debug.print("One: {}\n", .{EnumType.EnumOne});
std.debug.print("Two?: {}\n", .{EnumType.EnumTwo == .EnumTwo});
std.debug.print("Three?: {}\n", .{@enumToInt(EnumType.EnumThree) == 3});
}
Zig tem vetores (arrays), que são memórias contíguas com comprimento
conhecido em tempo de compilação. Você pode inicializá-los declarando seus
tipos de antemão e fornecendo uma lista de valores. Você pode acessar o
comprimento mediante o campo len
do array.
Nota:
- Arrays em Zig são indexados a partir do zero.
const std = @import("std");
pub fn main() void {
var array: [3]u32 = [3]u32{47, 47, 47};
// also valid:
// var array = [_]u32{47, 47, 47};
var invalid = array[4]; // error: index 4 outside array of size 3.
std.debug.print("array[0]: {}\n", .{array[0]});
std.debug.print("length: {}\n", .{array.len});
}
Zig também tem fatias (slices), que têm comprimento conhecido em tempo de
execução. Você pode construir slices a partir de arrays ou de outros slices
usando a operação de recorte (slicing). Semelhante a arrays, slices têm um
campo len
que informa o comprimento.
Nota:
- O parâmetro de intervalo é aberto (não inclusivo) no extremo superior.
A tentativa de acessar um slice além do limite gera um pânico em tempo de execução (isto significa que teu programa vai quebrar).
const std = @import("std");
pub fn main() void {
var array: [3]u32 = [_]u32{47, 47, 47};
var slice: []u32 = array[0..2];
// also valid:
// var slice = array[0..2];
var invalid = slice[3]; // panic: index out of bounds
std.debug.print("slice[0]: {}\n", .{slice[0]});
std.debug.print("length: {}\n", .{slice.len});
}
Literais de string são arrays de bytes const u8
terminados em null
encodados em UTF-8. Caracteres unicode só são permitidos em strings e
comentários.
Nota:
- O comprimento não inclui o null terminal (oficialmente chamado “sentinela de terminação”).
- É seguro acessar o terminador null.
- Índices são por byte, não por glifo Unicode.
const std = @import("std");
const string = "hello 世界";
const world = "world";
pub fn main() void {
var slice: []const u8 = string[0..5];
std.debug.print("string {}\n", .{string});
std.debug.print("length {}\n", .{world.len});
std.debug.print("null {}\n", .{world[5]});
std.debug.print("slice {}\n", .{slice});
std.debug.print("huh? {}\n", .{string[0..7]});
}
Arrays const podem ser convertidos em slices const.
const std = @import("std");
fn foo() []const u8 { // note function returns a slice
return "foo"; // but this is a const array.
}
pub fn main() void {
std.debug.print("foo: {}\n", .{foo()});
}
Zig te fornece uma estrutura if
, que funciona como o esperado.
const std = @import("std");
fn foo(v: i32) []const u8 {
if (v < 0) {
return "negative";
}
else {
return "non-negative";
}
}
pub fn main() void {
std.debug.print("positive {}\n", .{foo(47)});
std.debug.print("negative {}\n", .{foo(-47)});
}
Bem como uma estrutura switch
:
const std = @import("std");
fn foo(v: i32) []const u8 {
switch (v) {
0 => return "zero",
else => return "nonzero"
}
}
pub fn main() void {
std.debug.print("47 {}\n", .{foo(47)});
std.debug.print("0 {}\n", .{foo(0)});
}
Zig fornece um laço (loop) for
, que funciona somente em arrays e slices.
const std = @import("std");
pub fn main() void {
var array = [_]i32{47, 48, 49};
for (array) | value | {
std.debug.print("array {}\n", .{value});
}
for (array) | value, index | {
std.debug.print("array {}:{}\n", .{index, value});
}
var slice = array[0..2];
for (slice) | value | {
std.debug.print("slice {}\n", .{value});
}
for (slice) | value, index | {
std.debug.print("slice {}:{}\n", .{index, value});
}
}
Zig fornece um laço while
que também funciona como esperado:
const std = @import("std");
pub fn main() void {
var array = [_]i32{47, 48, 49};
var index: u32 = 0;
while (index < 2) {
std.debug.print("value: {}\n", .{array[index]});
index += 1;
}
}
Erros são tipos especiais de uniões. Você denota que uma função pode errar
inserindo !
em frente a ela. Você atira um erro simplesmente retornando-o
como um return
normal.
const MyError = error{
GenericError, // just a list of identifiers, like an enum.
OtherError
};
pub fn main() !void {
return MyError.GenericError;
}
Se você escreve uma função que pode errar, você deve decidir o que fazer com
este erro quando ele retornar. Duas opções comuns são try
, que é bem
preguiçosa e simplesmente redireciona o erro para ser o erro para a
função. catch
explicitamente lida com o erro.
Nota:
try
é mero açúcar sintático paracatch | err | {return err}
.
const std = @import("std");
const MyError = error{
GenericError
};
fn foo(v: i32) !i32 {
if (v == 42) return MyError.GenericError;
return v;
}
pub fn main() !void {
// catch traps and handles errors bubbling up
_ = foo(42) catch |err| {
std.debug.print("error: {}\n", .{err});
};
// try won't get activated here.
std.debug.print("foo: {}\n", .{try foo(47)});
// this will ultimately cause main to print an error trace and return nonzero
_ = try foo(42);
}
Você também pode usá-lo para conferir erros.
const std = @import("std");
const MyError = error{
GenericError
};
fn foo(v: i32) !i32 {
if (v == 42) return MyError.GenericError;
return v;
}
// note that it is safe for wrap_foo to not have an error ! because
// we handle ALL cases and don't return errors.
fn wrap_foo(v: i32) void {
if (foo(v)) | value | {
std.debug.print("value: {}\n", .{value});
} else | err | {
std.debug.print("error: {}\n", .{err});
}
}
pub fn main() void {
wrap_foo(42);
wrap_foo(47);
}
Tipos ponteiro são declarados colocando *
em frente do tipo. Nada de
declarações espiraladas como em C! Eles são de-referenciados mediante o campo
.*
:
const std = @import("std");
pub fn printer(value: *i32) void {
std.debug.print("pointer: {}\n", .{value});
std.debug.print("value: {}\n", .{value.*});
}
pub fn main() void {
var value: i32 = 47;
printer(&value);
}
Note:
- Em Zig, ponteiros devem ser corretamente alinhados com o alinhamento do valor que ele está apontando.
Para structs, semelhante a Java, você pode de-referenciar o ponteiro e obter
o campo em um único passo com o operador .
. Note que isso só funciona com
um nível de indireção, de forma que se voc6e tiver um ponteiro para ponteiro,
você deve de-referenciar o ponteiro mais exterior primeiro.
const std = @import("std");
const MyStruct = struct {
value: i32
};
pub fn printer(s: *MyStruct) void {
std.debug.print("value: {}\n", .{s.value});
}
pub fn main() void {
var value = MyStruct{.value = 47};
printer(&value);
}
Zig permite que qualquer tipo (não apenas ponteiros) seja anulável
(nullable), mas note que eles são uniões do tipo base com o valor especial
null
. Para acessar o tipo opcional sem o envelopamento (wrap), use o campo
.?
:
const std = @import("std");
pub fn main() void {
var value: i32 = 47;
var vptr: ?*i32 = &value;
var throwaway1: ?*i32 = null;
var throwaway2: *i32 = null; // error: expected type '*i32', found '(null)'
std.debug.print("value: {}\n", .{vptr.*}); // error: attempt to dereference non-pointer type
std.debug.print("value: {}\n", .{vptr.?.*});
}
Nota:
- Quando você usa ponteiros da interface binária (ABI) de C, eles são automaticamente convertidos para ponteiros anuláveis.
Outra forma de obter o valor opcional do ponteiro sem o envelopamento é com a
estrutura if
:
const std = @import("std");
fn nullChoice(value: ?*i32) void {
if (value) | v | {
std.debug.print("value: {}\n", .{v.*});
} else {
std.debug.print("null!\n", .{});
}
}
pub fn main() void {
var value: i32 = 47;
var vptr1: ?*i32 = &value;
var vptr2: ?*i32 = null;
nullChoice(vptr1);
nullChoice(vptr2);
}
A meta-programação em Zig é dirigida por alguns conceitos básicos:
- Tipos são valores válidos em tempo de compilação
- Maior parte do código de tempo de execução também funcionará em tempo de compilação
- Evaluação de campos de struct é duck-typed em tempo de compilação
- A biblioteca padrão do Zig te dá ferramentas para realizar reflexão em tempo de compilação
Eis um exemplo de despacho múltiplo (você já deve ter visto isso em ação com
std.debug.print
, agora possivelmente você pode imaginar como isto é
implementado):
const std = @import("std");
fn foo(x : anytype) @TypeOf(x) {
// note that this if statement happens at compile-time, not runtime.
if (@TypeOf(x) == i64) {
return x + 2;
} else {
return 2 * x;
}
}
pub fn main() void {
var x: i64 = 47;
var y: i32 = 47;
std.debug.print("i64-foo: {}\n", .{foo(x)});
std.debug.print("i32-foo: {}\n", .{foo(y)});
}
Eis um exemplo de tipos genéricos:
const std = @import("std");
fn Vec2Of(comptime T: type) type {
return struct{
x: T,
y: T
};
}
const V2i64 = Vec2Of(i64);
const V2f64 = Vec2Of(f64);
pub fn main() void {
var vi = V2i64{.x = 47, .y = 47};
var vf = V2f64{.x = 47.0, .y = 47.0};
std.debug.print("i64 vector: {}\n", .{vi});
std.debug.print("f64 vector: {}\n", .{vf});
}
A partir destes conceitos, você pode construir genéricos bastante poderosos!
Zig te fornece muitas formas de interagir com o heap, e usualmente requer que você seja explícito acerca de suas escolhas. Todas elas seguem o mesmo padrão:
- Crie uma struct fábrica Allocator.
- Recupere a struct
std.mem.Allocator
struct criada pela fábrica Allocator. - Use as funções alloc/free e create/destroy para manipular o heap.
- (opcional) De-inicialize a fábrica Allocator.
Nossa! Isso parece ser coisa para caramba! Mas
- Isto serve para desencorajar o uso do heap.
- Isto torna qualquer coisa que chama o heap (o qual é fundamentalmente falível) óbvia.
- Ao ser desprovido de dogmatismo, você pode cuidadosamente sintonizar seus contrapesos e usar as estruturas de dados padrão sem ter que reescrever a biblioteca padrão.
- Você pode utilizar um alocador extremamente seguro em seus testes e trocá-lo por um alocador diferente na distribuição/produção.
Ok. Mas você ainda pode ser preguiçoso. Você sente falta de usar jemalloc em todo lugar? Apenas use um alocador global e use-o em todo canto (tomando cuidado que alguns alocadores são seguros-para-threads enquanto outros não)! Por favor não faça isso se estiver escrevendo alguma biblioteca de propósito geral.
Neste exemplo utilizaremos a fábrica std.heap.GeneralPurposeAllocator
a fim
de criar um alocador com um monte de frufrus (incluindo detecção de
vazamentos) e ver como isso tudo surge.
Uma última coisa. Este trecho usa a palavra-chave defer
, que é bastante
semelhante à mesma palavra em Go! Existe também uma errdefer
, mas para
aprender sobre ela confira a documentação de Zig (link ao final).
const std = @import("std");
// factory type
const Gpa = std.heap.GeneralPurposeAllocator(.{});
pub fn main() !void {
// instantiates the factory
var gpa = Gpa{};
// retrieves the created allocator.
var galloc = &gpa.allocator;
// scopes the lifetime of the allocator to this function and
// performs cleanup;
defer _ = gpa.deinit();
var slice = try galloc.alloc(i32, 2);
// uncomment to remove memory leak warning
// defer galloc.free(slice);
var single = try galloc.create(i32);
// defer gallo.destroy(single);
slice[0] = 47;
slice[1] = 48;
single.* = 49;
std.debug.print("slice: [{}, {}]\n", .{slice[0], slice[1]});
std.debug.print("single: {}\n", .{single.*});
}
E é isto! Agora você sabe um montante decente de Zig! Algumas coisas bastante importantes que eu não cobri aqui incluem:
- Testes! Ah, rapaz, por favor escreva testes! Zig facilita demais escrever testes.
- A biblioteca padrão.
- O modelo de memória (de maneira um tanto única, Zig é agressivamente adogmático sobre alocadores).
- Async
- Compilação cruzada
build.zig
Para maiores detalhes, confira a documentação mais recente: https://ziglang.org/documentation/master/
Ou para um tutorial mais aprofundado, leia: https://ziglearn.org/
- Original: A half-hour to learn Zig
Tenho um rascunho de livro jogado no meu note velho...