Container generik

Mendefinisikan satu container yang melayani semua tipe.

Daftar isi

Fungsi generik dengan parameter comptime mengembalikan nilai, sedangkan container generik mengembalikan tipe. Pola ini mewujudkan satu definisi yang melayani semua tipe, misalnya satu daftar untuk u8 dan satu daftar untuk f64 tanpa menyalin kode secara manual. Topik ini membahas fungsi yang mengembalikan type, instansiasi container pada satu program, serta kaitan pola ini dengan pustaka standar Zig. Hasil akhirnya adalah kemampuan menulis tipe bentukan yang dapat diisi tipe apa pun secara eksplisit.

Konsep#

Fungsi yang mengembalikan type#

Container generik ditulis sebagai fungsi yang parameternya bertipe comptime T: type dan tubuhnya mengembalikan type. Di dalam tubuh, struct didefinisikan dengan field yang mengacu pada T, kemudian dikembalikan sebagai hasil fungsi. Pemanggilan Daftar(u8) bukan pemanggilan biasa, melainkan permintaan kepada kompilator untuk membentuk tipe struct baru dengan T diganti u8.

Penanda const Self = @This() berada di dalam struct hasil. @This() mengembalikan container yang melingkupi deklarasi tersebut, yaitu struct yang baru dibentuk, dan memberinya nama Self agar metode dapat menyebut tipenya sendiri tanpa tahu tipe T yang dipakai pemanggil. Nama Self bukan kata kunci, tetapi konvensi yang lazim dipakai pustaka standar untuk keperluan yang sama.

Nama Daftar ditulis dengan huruf kapital mengikuti konvensi penamaan tipe pada Zig, karena hasil fungsi ini memang sebuah tipe. Fungsi semacam ini pada dasarnya adalah perhitungan pada tingkat tipe, yaitu argumennya wajib konstanta waktu kompilasi dan hasilnya juga diketahui saat kompilasi. Parameter yang diterimanya tidak terbatas satu, sehingga fungsi boleh menerima tipe elemen beserta lebar atau konfigurasi lain, dan setiap kombinasi argumen membentuk tipe hasil yang berbeda.

Contoh Lengkap#

Instansiasi Daftar(u8) dan Daftar(f64)#

Program berikut mendefinisikan satu container Daftar dengan kapasitas tetap, lengkap dengan metode tambah dan ambil. Dua instansiasi dipakai berdampingan dalam satu program, yaitu Daftar(u8) untuk hitungan sederhana dan Daftar(f64) untuk pengukuran desimal.

zig daftar-generik.zig
const std = @import("std");

const KAPASITAS = 8;

fn Daftar(comptime T: type) type {
    return struct {
        const Self = @This();

        items: [KAPASITAS]T = undefined,
        panjang: usize = 0,

        fn tambah(self: *Self, nilai: T) void {
            self.items[self.panjang] = nilai;
            self.panjang += 1;
        }

        fn ambil(self: Self, indeks: usize) T {
            return self.items[indeks];
        }
    };
}

pub fn main() void {
    var hitungan = Daftar(u8){};
    hitungan.tambah(7);
    hitungan.tambah(13);
    hitungan.tambah(5);

    var pengukuran = Daftar(f64){};
    pengukuran.tambah(22.5);
    pengukuran.tambah(19.0);

    std.debug.print("hitungan: {d}, {d}, {d}\n", .{ hitungan.ambil(0), hitungan.ambil(1), hitungan.ambil(2) });
    std.debug.print("pengukuran: {d}, {d}\n", .{ pengukuran.ambil(0), pengukuran.ambil(1) });
    std.debug.print("panjang pengukuran = {d}\n", .{pengukuran.panjang});
    std.debug.print("ukuran Daftar(u8) = {d} byte\n", .{@sizeOf(Daftar(u8))});
    std.debug.print("ukuran Daftar(f64) = {d} byte\n", .{@sizeOf(Daftar(f64))});
}
text Keluaran
hitungan: 7, 13, 5
pengukuran: 22.5, 19
panjang pengukuran = 2
ukuran Daftar(u8) = 16 byte
ukuran Daftar(f64) = 72 byte

Dua instansiasi berperilaku sebagai dua tipe yang benar-benar berbeda. Field items pada Daftar(u8) menyimpan delapan byte, sedangkan Daftar(f64) menyimpan delapan bilangan desimal, sehingga @sizeOf melaporkan 16 byte berbanding 72 byte setelah ditambah field panjang. Karena tipe berbeda, hitungan.tambah(22.5) akan ditolak kompilator, dan pemisahan ini terjadi tanpa satu baris duplikasi pada kode sumber.

Pembagian penerima metode mengikuti konvensi metode struct biasa. Metode tambah menerima *Self karena mengubah isi struct, sedangkan ambil menerima salinan Self karena hanya membaca keadaan. Pemanggilan dengan sintaks titik pada kedua metode dibongkar menjadi panggilan fungsi biasa pada container hasil instansiasi, sehingga tidak ada mekanisme tambahan yang bekerja khusus untuk container generik.

bash
# jalankan program dari direktori topik
zig run daftar-generik.zig
text Keluaran
hitungan: 7, 13, 5
pengukuran: 22.5, 19
panjang pengukuran = 2
ukuran Daftar(u8) = 16 byte
ukuran Daftar(f64) = 72 byte

Pola yang sama pada pustaka standar#

Pustaka standar menerapkan pola yang sama pada tipe yang dipakai sehari-hari. std.ArrayList adalah container generik yang menampung koleksi yang tumbuh, dan std.ArrayList(u8) merupakan tipe hasil instansiasinya, bukan nama tipe tunggal. Pembahasan ArrayList pada modul memori mengulas pemakaiannya secara lengkap, sedangkan fragmen berikut hanya menunjukkan bentuk deklarasinya.

zig Contoh: instansiasi std.ArrayList
// tidak-dijalankan
var daftar: std.ArrayList(u8) = .empty;
try daftar.append(allocator, 7); // allocator disertakan pada tiap metode
std.debug.print("{d}\n", .{daftar.items.len});

Persamaan dengan Daftar terletak pada bentuk dasarnya, yaitu fungsi dengan parameter tipe yang mengembalikan struct berisi field dan metode. Perbedaannya terletak pada pengelolaan memori, karena ArrayList memperoleh memori melalui Allocator yang diteruskan pada setiap metode, sedangkan Daftar memakai array berkapasitas tetap. Pola container generik tetap sama di kedua sisi, sehingga pemahaman pada container buatan sendiri langsung terpakai ketika membaca kode pustaka standar.

Pemilihan antara kapasitas tetap dan alokasi dinamis merupakan keputusan desain. Kapasitas tetap menghilangkan ketergantungan pada allocator, membuat batas ukuran terbaca dari deklarasinya, dan menghindari kegagalan alokasi, sedangkan koleksi yang tumbuh menuntut kebebasan ukuran dengan biaya pengelolaan memori yang eksplisit. Struktur Daftar pada topik ini sengaja dibuat sederhana agar fokusnya tertuju pada mekanisme generiknya.

Kesalahan umum#

Kesalahan pada topik ini menyangkut kapasitas dan anggapan tentang tipe hasil. Tiga kasus berikut paling sering muncul.

  1. Mengisi Daftar melebihi kapasitasnya. Metode tambah pada program di atas tidak memeriksa batas, sehingga pengisian kesembilan menghentikan program dengan panic index out of bounds: index 8, len 8. Periksa panjang terhadap items.len di awal metode dan laporkan error secara eksplisit, atau pilih kapasitas yang cukup sejak awal.
  2. Menganggap Daftar(u8) dan Daftar(f64) sebagai tipe yang saling dapat dipertukarkan. Keduanya lahir dari definisi yang sama, tetapi kompilator memperlakukannya sebagai dua tipe berbeda, sehingga pemberian nilai antar instansiasi ditolak. Teruskan tipenya sebagai parameter comptime T: type pada fungsi yang perlu menerima keduanya.
  3. Menginstansiasi container dengan nilai yang baru diketahui saat berjalan. Ekspresi semacam Daftar(ukuran) dengan ukuran berupa variabel biasa ditolak karena parameter tipe menuntut konstanta waktu kompilasi. Gunakan konstanta atau literal untuk argumennya, dan serahkan penanganan ukuran dinamis kepada koleksi yang memakai allocator.

Ringkasan#

  • Container generik ditulis sebagai fungsi fn Nama(comptime T: type) type yang mengembalikan struct dengan field dan metode bertipe T.
  • Penanda const Self = @This() memberi nama pada struct hasil sehingga metode dapat merujuk tipenya sendiri.
  • Setiap instansiasi seperti Daftar(u8) dan Daftar(f64) merupakan tipe berbeda dengan ukuran berbeda, dan pemberian nilai antar instansiasi ditolak kompilator.
  • std.ArrayList menerapkan pola yang sama dengan pengelolaan memori eksplisit melalui Allocator.

Referensi#