Slices

Slices

        Los slices (cortes o segmentos) son como una vista sobre un array. Para poder declarar un slice, necesitamos tener previamente un array:


const  nombre = a_existente[índice_inicial .. índice_final];


        También, podemos omitir el índice final - en ese caso el slice irá hasta el final del array. Vamos a ver cómo definir un slice en el ejemplo siguiente:

slice.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

  const a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 };

  const z_range = a_nums[2..5];

  print("Slice z_range: \n\t {any}\n", .{z_range});

  print("\tElemento 1 del slice: {}\n", .{z_range[0]});

  print("\tElemento 2 del slice: {}\n", .{z_range[1]});

  print("\tElemento 3 del slice: {}\n", .{z_range[2]});

}

$ zig run slice.zig

Slice z_range:

         { 3, 14, 35 }

        Elemento 1 del slice: 3

        Elemento 2 del slice: 14

        Elemento 3 del slice: 35

Slices en memoria

Los slices apuntan a los datos de un array. Esos datos no son suyos.  Un array, tiene siempre definida su longitud en cuanto escribimos y compilamos el código en Zig para ejecutarlo. Sin embargo, un slice puede tener una longitud distinta dependiendo de la ejecución.

Un array a_nums lo podemos representar con el siguiente esquema:

11

21

3

14

35

8

índices:

0

1

2

3

4

5

El slice z_range lo hemos definido sobre el array, empezando en el índice 2 y acabando en el 5:

z_range

índices slice:

0

1

2

11

21

3

14

35

8

índices:

0

1

2

3

4

5

Como puedes ver de la ejecución del código:

        Los valores que en el array a_nums están en los índices 2,3 y 4, en el nuevo slice z_range están bajo los índices 0, 1 y 2. Tenemos, por tanto, dos constantes apuntando exactamente a los mismos valores en memoria.

        Esto quiere decir que si el array a_nums es una variable y cambiamos alguno de  sus valores, el slice que apunta a esa zona del array en cuestión, mostrará también los cambios:

slice_array_mutable.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

    var a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 };

    // creamos un segmento apuntando a los índices: 2,3,4

    const z_range = a_nums[2..5];

    print("Slice z_range: \n\t {any}\n", .{z_range});

    print("\tElemento 1 del slice: {}\n", .{z_range[0]});

    // cambiamos el primer elemento del slice

    z_range[0] = 55;

    print("\tElemento 1 del slice cambiado: {}\n", .{z_range[0]});

    // cambiamos el elemento del índice 3 del array (segundo del slice)

    a_nums[3] = 13;

    print("\tElemento 2 del slice: {}\n", .{z_range[1]});

    // cambiamos el elemento del índice 4 del array (tercero del slice)

    a_nums[4] = 42;

    print("\tElemento 3 del slice: {}\n", .{z_range[2]});

    print("\nSlice z_range, después de los cambios: \n\t {any}\n", .{z_range});

    print("\nArray a_nums, después de los cambios: \n\t {any}\n", .{a_nums});

}

$ zig run slice_array_mutable.zig

Slice z_range:

         { 3, 14, 35 }

        Elemento 1 del slice: 3

        Elemento 1 del slice cambiado: 55

        Elemento 2 del slice: 13

        Elemento 3 del slice: 42

Slice z_range, después de los cambios:

         { 55, 13, 42 }

Array a_nums, después de los cambios:

         { 11, 21, 55, 13, 42, 8 }

En este caso el array a_nums, al ser una variable, nos permite cambiar sus valores.

Aunque z_range haya sido declarado como una constante, apunta a la misma zona de memoria: los cambios hechos sobre los elementos del array original se reflejan en el slice automáticamente. También podemos modificar los valores de esos elementos directamente a través del propio slice como ocurre con:

  // cambiamos el primer elemento del slice

  z_range[0] = 55;

        Es fundamental comprender que los slices no son copias del array sino vistas activas sobre él. Esto nos será útil para crear funciones que aceptan segmentos de un array, sin copiar nada innecesario.

Ahora, dirás: “pero un momento, si el slice es constante ¿cómo es que podemos cambiar los valores a través del slice?” 

Verás: un slice constante no significa que los valores a los que apunta no se pueden cambiar - eso depende del array subyacente. Un slice constante significa que una vez declarado, su rango es fijo: ya no se puede mover su principio ni su final.

Un slice variable sí puede cambiar dónde empieza y dónde acaba pero respetando la longitud original del slice: No puedes cambiar su tamaño.

slice_mutable.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

    var a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 };

    // creamos un segmento apuntando a los índices: 2,3,4

    var z_range = a_nums[2..5];

    print("Slice z_range: \n\t {any}\n", .{z_range});

    print("\tElemento 1 del slice: {}\n", .{z_range[0]});

    // ahora slice apunta a los índices: 0,1,2

    z_range = a_nums[0..3];

    print("Slice z_range: \n\t {any}\n", .{z_range});

    print("\tElemento 1 del slice: {}\n", .{z_range[0]});

}

$ zig run slice_mutable.zig

Slice z_range:

         { 3, 14, 35 }

        Elemento 1 del slice: 3

Slice z_range:

         { 11, 21, 3 }

        Elemento 1 del slice: 11

Aquí, el slice z_range es mutable, así que podemos hacer que apunte a otro rango del array original. Pero en este caso, en ningún momento hemos modificado el array original ni creado un slice nuevo.

Longitud de los slices

En el ejemplo siguiente imaginamos que en un juego hay 8 mejoras posibles para un arma, un escudo, o una nave.

Definimos esas mejoras con el array a_powerups. Dependiendo de los puntos de experiencia  acumulados por el  jugador, se pueden elegir diferentes mejoras usando un slice:

slice_length.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

    const a_powerups = [_]u8{ 1, 2, 3, 4, 5, 7, 8 };

    // el jugador tiene estos puntos de experiencia

    const n_exp = 3000;

    // dependiendo de los puntos de experiencia

    // los powerups disponibles son distintos

    const z_available_powerups = if (n_exp > 2000) 

            a_powerups[0..] 

        else 

            a_powerups[0..3];

    print("Con tus puntos de experiencia: {}\nPuedes elegir estos powerups: \n\t {any}\n",

    .{ n_exp, z_available_powerups });

}

$ zig run slice_length.zig

Con tus puntos de experiencia: 3000

Puedes elegir estos powerups:

         { 1, 2, 3, 4, 5, 7 }

Si cambiamos el valor n_exp a 1000 por ejemplo:

 const n_exp = 1000;

$ zig run slice_length.zig

Con tus puntos de experiencia: 1000

Puedes elegir estos powerups:

         { 1, 2, 3 }

En este caso, el slice z_available_powerups tienee diferente contenido y otra longitud dependiendo de los datos de entrada, por lo tanto Zig no puede conocer esa longitud en tiempo de compilación.

Repetir y concatenar arrays (o slices)

Podemos repetir un array o un slice usando el operador ** seguido del número de veces que queremos repetirlo:

  const a_x = [_]u8{48}; // array de un solo elemento (char ‘0’)

  const a_pad = a_x ** 6; // repetimos el array 6 veces

 

  // a_pad = .{‘0’,’0’,’0’,’0’,’0’,’0’ }

Para concatenar arrays o slices usamos el operador ++

  const s_x = "Hola" ++ " " ++  "programador" 

  // “Hola programador”

Los datos que repetimos o concatenamos tienen que ser conocidos en tiempo de compilación.

Imagina que tenemos que mostrar una puntuación en una máquina arcade donde el resultado siempre se pone con 8 dígitos. Tendremos que rellenar los dígitos que quedan libres a la izquierda con 0.s

padded_score.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

    const n_score = 10007;

    const N_WIDTH = 8;

    // obtenemos cuántos 10 multiplican el score

    const n_has_zeros = std.math.log10(n_score);

    // calculamos con cuántos 0.s a la izquierda rellenar el score

    const n_pad_with = N_WIDTH - n_has_zeros - 1;

    const z_zero = "0"; // cadena (slice en Zig) con un solo ‘0’

    // obtenemos los ceros a la izquierda como cadena

    // repitiendo el slice “0” hasta cubrir los espacios libres

    const s_score_pad = z_zero ** n_pad_with;

    const s_resp: []const u8 = std.fmt.comptimePrint("{s}{}", 

      .{ s_score_pad, n_score });

    // por último concatenamos dos slices (strings) usando ++

    print("{s}: \n", .{"SCORE: " ++ s_resp});

}

$ zig run padded_score.zig

SCORE: 00010007

En el código notarás que usamos la función std.fmt.comptimePrint

 const s_resp = std.fmt.comptimePrint("{s}{}", .{ s_score_pad, n_score })

Es una función de la librería estándar de Zig, parecida a std.debug.print. Esta en vez de imprimir la cadena interpolada, sólo la devuelve como resultado.

Pasar slices a funciones

Los slices son muy útiles para pasar datos a funciones:

En este ejemplo, un jugador consigue un cierto nivel en el uso de armas. El juego le informa sobre las armas que puede usar:

slice_fun.zig

const std = @import("std");

const print = std.debug.print;

fn show_weapons(z_pows: []const u8) void {

    const n_what: u8 = z_pows[0];

    const s_weapon = switch (n_what) {

        0 => "Piedra",

        1 => "Papel",

        2 => "Tijera",

        3 => "Weight Gain 3000",

        4 => "Yo lo llamo \"láser\"",

        else => "no vale!",

    };

    print("\t{s}\n", .{s_weapon});

    if (z_pows.len > 1) {

        show_weapons(z_pows[1..]);

    }

}

pub fn main() void {

    // armas disponibles

    const a_weapon_codes = [_]u8{

        0,

        1,

        2,

        3,

        4,

        // ...

    };

    // el jugador tiene este nivel en manejo de las armas

    const n_weapons_lvl = 5;

    // qué armas podrá usar con el nivel conseguido

    const z_can_use = if (n_weapons_lvl > 4) 

            a_weapon_codes[0..] 

        else 

            a_weapon_codes[0..3];

    print("\nCon el nivel {} puedes usar estas armas:\n", .{n_weapons_lvl});

    show_weapons(z_can_use);

}

$ zig run slice_fun.zig

Con el nivel 5 puedes usar estas armas:

        Piedra

        Papel

        Tijera

        Weight Gain 3000

        Yo lo llamo “láser”

Viendo este último ejemplo del capítulo, quizá te preguntes: “¿Qué es esa función tan extraña que se llama a sí misma para listar un arma trás otra?”. Es una función recursiva. Pero no es la única manera de procesar los datos en secuencia. De eso trata el próximo capítulo.

Arrays mutables
Resumen del capítulo
© 2025 Zen of Zig