Un opcional en Zig es un tipo que puede contener un valor o no contener nada. Tony Hoare, creador de la referencia nula (null), la describió décadas después como “el error de los mil millones de dólares”.
En muchos lenguajes, referencias y punteros han podido ser null sin que el tipo lo distinga. Si olvidamos una comprobación, el fallo aparece durante la ejecución como una NullPointerException o un acceso inválido.
Zig obliga a reflejar esa posibilidad en el tipo y a manejarla antes de usar el valor.
En Zig, un tipo normal NUNCA puede ser nulo.
var numero: i32 = null; // ERROR: expected type 'i32', found 'null'
var ptr: *u8 = null; // ERROR: expected type '*u8', found 'null'Si quieres que algo pueda “no tener valor”, debes declararlo explícitamente como Opcional anteponiendo un interrogante ? al tipo.
Declaración de opcionales (?T)
Un opcional ?T es, conceptualmente, una caja que puede contener un valor de tipo T o estar vacía (null).
// Puede contener un entero o null
var posible_edad: ?i32 = null;
posible_edad = 25; // Ahora tiene valor
posible_edad = null; // Vuelve a estar vacíoLo brillante es que ?i32 y i32 son tipos diferentes. No puedes sumarlos ni mezclarlos directamente. El compilador te obliga a comprobar si hay valor antes de usarlo.
const a: ?i32 = 10;
const b: i32 = 5;
// ERROR: operator + not allowed for type '?i32'
// const suma = a + b; Zig te está diciendo: “Oye, a podría estar vacío. Dime qué hago en ese caso antes de sumar”.
Desempaquetado seguro
Para usar el valor de un opcional, tenemos que “desempaquetarlo” (unwrap). Zig nos ofrece tres formas principales de hacerlo.
Captura de valor con if
Esta es la estructura más común en Zig. Usamos if (opcional) |nombre_variable|.
const input: ?i32 = obtenerValorDelUsuario();
if (input) |valor| {
// Dentro de este bloque, 'valor' es de tipo i32 (no ?i32)
// Es seguro usarlo.
std.debug.print("El usuario introdujo: {d}\n", .{valor});
} else {
std.debug.print("El usuario no introdujo nada\n", .{});
}Es elegante porque combina la comprobación (!= null) y la extracción del valor en una sola línea.
Valores por defecto con orelse
A veces solo queremos decir: “Usa este valor, y si es null, usa este otro por defecto”.
const config_usuario: ?u32 = null;
// Si config_usuario es null, usamos 8080
const puerto = config_usuario orelse 8080; orelse también permite flujo de control. Podemos hacer return o break si el valor falta.
// Si es null, salimos de la función inmediatamente
const dato = obtenerDato() orelse return error.FalloGrave;Desempaquetado forzoso (.?)
A veces, nosotros (como programadores) sabemos que un valor no puede ser null debido a la lógica del programa, aunque el tipo diga que sí.
En esos casos, podemos usar .? para forzar la extracción.
const numero: ?i32 = 10;
const valor = numero.?; // Obtenemos el 10¿Qué pasa si te equivocas?
Usar .? cuando el valor es null es comportamiento ilegal. En Debug y ReleaseSafe, Zig lo detecta y lanza un panic; ReleaseFast y ReleaseSmall no tienen por qué conservar esa comprobación.
Punteros opcionales (?*T)
Si tenemos un opcional de un entero ?u8:
- Necesitamos 8 bits para el número.
- Necesitamos 1 bit extra (booleano) para saber si es null o no.
- Debido al alineamiento de memoria, esto suele ocupar más espacio del esperado.
Para los punteros que no permiten la dirección cero, Zig usa una representación más compacta.
Sabemos que la dirección de memoria 0 nunca es válida. Zig aprovecha esto.
En un ?*T, Zig usa la dirección 0 para representar null.
Esto significa que un ?*T ocupa lo mismo que un *T.
- Sin una etiqueta adicional: la propia dirección cero representa
null. - Comprobación explícita: el tipo obliga a desempaquetar el puntero antes de usarlo como
*T.
// En C, esto sería un puntero que podría ser NULL (peligroso)
// En Zig, es explícito y seguro.
const Nodo = struct {
valor: i32,
siguiente: ?*Nodo, // Puntero al siguiente nodo o null
};Bucles con opcionales
Los opcionales se llevan muy bien con los bucles while. Podemos usar la sintaxis de captura para iterar mientras una función devuelva valor y parar cuando devuelva null.
var i: u32 = 0;
fn iterador() ?u32 {
if (i >= 3) return null;
i += 1;
return i;
}
pub fn main() void {
// Ejecuta el bucle mientras iterador() devuelva algo distinto de null
while (iterador()) |num| {
std.debug.print("Número: {d}\n", .{num});
}
}