CAPÍTULO 8
Genéricos
Usamos argumentos genéricos cuando queremos crear funciones que puedan aceptar parámetros de distintos tipos. La sintaxis es la siguiente:
fn nombre_func ( T: type, x_a: T , … , ) tipo_devuelto //también puede ser T
o
fn nombre_func ( x_a: anytype , … , ) tipo_devuelto //también puede ser @TypeOf(x_a)
Este tipo de funciones, en realidad, tienen que ver con cómo el compilador de Zig reestructura el código fuente. Cuando definimos una función genérica y luego la usamos , el compilador la trata como una plantilla para generar una versión adaptada para cada tipo que aparece en las llamadas.
Ambas declaraciones especifican que el argumento x_a puede ser de diferentes tipos, según la llamada. Aún así, en el cuerpo de la función, hemos de tener en cuenta que al final operamos con un tipo concreto.
El inventor, entre otras cosas, estaba trabajando en una máquina voladora y en un arma nueva para el rey. La máquina voladora -por el riesgo que suponía probarla- seguía a medio hacer, pero el arma funcionaba casi a la perfección: al girar una manivela y soltarla, disparaba decenas de puntas de flecha. Eso sí, no paraba hasta agotar la carga.
load_ammo.zig |
|
const std = @import("std"); const print = std.debug.print; // puntas de flecha de distintos materiales const Proyectile = enum { Wood,Iron,Steel, Copper,Rock, }; // la ametralladora primitiva const MachineGun = struct { a_magazine: [100]Proyectile = undefined, l_ammo: std.ArrayListUnmanaged(Proyectile) = .{}, fn init(self: *MachineGun) void { self.l_ammo = std.ArrayListUnmanaged(Proyectile).initBuffer(&self.a_magazine); } // cargar diferentes cantidades / tipos de munición fn load_ammo(self: *MachineGun, x_ammo: anytype) void { // función anónima solo accesible desde este scope const load_single = struct { fn _(o_me: *MachineGun, x_bullet: anytype) void { if (o_me.l_ammo.capacity == o_me.l_ammo.items.len) print("No hay más sitio\n", .{}) else if (@TypeOf(x_bullet) == Proyectile) o_me.l_ammo.appendAssumeCapacity(x_bullet) else print("¡No puede disparar eso!\n", .{}); } }._; // si es un array cargamos todos los elementos switch (@typeInfo(@TypeOf(x_ammo))) { .array => for (x_ammo) |o_bullet| load_single(self, o_bullet), else => load_single(self, x_ammo), } } // dispara todos los proyectiles fn shoot(self: *MachineGun) void { while (self.l_ammo.items.len > 0) { const o_bullet = self.l_ammo.pop(); print("{any}\n", .{o_bullet}); } } }; pub fn main() !void { var o_mgun = MachineGun{}; o_mgun.init(); // cargar un proyectil o_mgun.load_ammo(Proyectile.Iron); // cargar algo que se encuentra en el suelo o_mgun.load_ammo("Algo que estaba en el suelo"); // cargar una banda de proyectiles o_mgun.load_ammo([_]Proyectile{ .Copper, .Rock, .Steel }); // disparar o_mgun.shoot(); } |
|
$ zig run load_ammo.zig |
|
zig run load_ammo.zig ¡No puede disparar eso! .Steel .Rock .Copper .Iron |
En este ejemplo vemos cómo, en la declaración de la función load_ammo, usamos x_ammo: anytype:
|
// cargar diferentes cantidades / tipos de munición fn load_ammo(self: *MachineGun, x_ammo: anytype) void { |
Hacemos uso de genéricos porque queremos permitir que se pueda añadir un único proyectil o varios a la vez representados por un array. También, podríamos definir de manera explícita el tipo como T y usarlo así:
|
fn load_ammo(self: *MachineGun, T: type, x_ammo: T) void { |
Dentro del cuerpo de la función es necesario comprobar el tipo de dato concreto que estamos recibiendo para poder operar correctamente. Para ello usamos la función @TypeOf - para obtener el tipo del valor y @typeInfo - para inspeccionar la estructura y saber si se trata de un array.
|
// si es un array cargamos todos los elementos switch (@typeInfo(@TypeOf(x_ammo))) { .array => for (x_ammo) |o_bullet| load_single(self, o_bullet), else => load_single(self, x_ammo), } |
Puedes encontrar información detallada sobre estas funciones puedes en https://ziglang.org/documentation/master/#Builtin-Functions o en el propio código fuente de Zig, en el fichero builtin.zig. Verás que @typeInfo devuelve un union:
|
pub const Type = union(enum) { type: void, void: void, bool: void, noreturn: void, int: Int, float: Float, pointer: Pointer, array: Array, ... |
En el capítulo 6 vimos que en un union solo un campo puede estar activo a la vez. Aquí detectamos si el campo activo es .array, iteramos sobre los datos que nos llegan y “cargamos” un proyectil por iteración.