String

Memahami string sebagai slice u8 dan pola sentinel berakhiran null.

Daftar isi

String pada Zig bukan tipe tersendiri, melainkan deretan byte u8 yang dipandang sebagai teks. Literal string membawa penanda akhir bernilai nol yang disebut sentinel, sedangkan fungsi pustaka standar bekerja di atas slice []const u8 yang hanya dapat dibaca. Topik ini membahas tipe literal string, konversinya menjadi slice, fungsi perbandingan dan pencarian bawaan, serta perbedaan panjang byte dan jumlah karakter pada teks UTF-8. Seluruhnya ditutup dengan program penghitung kata sederhana yang dijalankan dengan Zig 0.17.0.

Konsep#

Literal string dan sentinelnya#

Setiap literal string bertipe *const [N:0]u8, yaitu pointer ke array N byte yang diakhiri sentinel 0. Sentinel tidak dihitung pada N, sehingga literal "pustaka" bertipe *const [7:0]u8 dan menempati delapan byte di memori beserta sentinelnya. Pola sentinel ini mewarisi konvensi null-terminated string dari dunia C, tetapi Zig menyimpan panjangnya pula pada tipe literal sehingga panjangnya diketahui tanpa menghitung. Nilai 0 pada ujungnya bukan karakter, melainkan penanda murni yang tidak dihitung sebagai isi teks.

Program berikut menampilkan tipe literal secara langsung, mengonversinya menjadi pointer ber-sentinel, lalu mengukur panjangnya dengan std.mem.span. Seluruh nilai cetaknya berasal dari waktu kompilasi maupun pemeriksaan runtime, sehingga tipe yang ditampilkan adalah tipe yang benar-benar dipegang oleh kompiler Zig 0.17.0.

zig fungsi-string.zig
const std = @import("std");

pub fn main() void {
    const literal = "pustaka";
    std.debug.print("Tipe literal: {s}\n", .{@typeName(@TypeOf(literal))});

    const ptr: [*:0]const u8 = literal;
    const span = std.mem.span(ptr);
    std.debug.print("Tipe hasil span: {s}\n", .{@typeName(@TypeOf(span))});
    std.debug.print("Panjang span: {d}\n", .{span.len});
    std.debug.print("Sentinel: {d}\n", .{span[span.len]});

    const a: []const u8 = "sensor";
    const b: []const u8 = "Sensor";
    std.debug.print("eql: {}\n", .{std.mem.eql(u8, a, b)});
    std.debug.print("startsWith: {}\n", .{std.mem.startsWith(u8, a, "sen")});
    std.debug.print("indexOfScalar: {?}\n", .{std.mem.indexOfScalar(u8, a, 's')});
}
text Keluaran
Tipe literal: *const [7:0]u8
Tipe hasil span: [:0]const u8
Panjang span: 7
Sentinel: 0
eql: false
startsWith: true
indexOfScalar: 0

Jalankan program tersebut dengan perintah berikut.

bash
# menjalankan program fungsi string
zig run fungsi-string.zig
text Keluaran
Tipe literal: *const [7:0]u8
Tipe hasil span: [:0]const u8
Panjang span: 7
Sentinel: 0
eql: false
startsWith: true
indexOfScalar: 0

Tipe literal "pustaka" terbaca persis sebagai *const [7:0]u8, dan penugasan pada ptr menunjukkan bahwa literal dapat dilihat sebagai pointer [*:0]const u8 yang panjangnya belum diketahui. Fungsi std.mem.span mengubah pointer semacam itu menjadi slice [:0]const u8 dengan panjang 7, dan elemen pada indeks span.len terbaca bernilai 0, yaitu sentinelnya sendiri.

Mengubah literal menjadi slice biasa#

Pada kebanyakan API pustaka standar, teks diteruskan sebagai []const u8, yaitu slice byte yang hanya dapat dibaca. Penugasan const teks: []const u8 = literal; cukup untuk mengubah literal menjadi slice biasa tanpa fungsi tambahan. Slice hasilnya membawa panjang N tanpa sentinel, dan indeksnya berkisar dari 0 sampai N - 1. Konversi ini berlangsung tanpa menyalin byte, sehingga slice tetap merujuk data literal yang tersimpan pada bagian baca-saja program.

Fungsi std.mem.span menjadi penting ketika panjang teks tidak diketahui sejak awal, misalnya pada pointer [*:0]const u8 yang datang dari pustaka C. Fungsi tersebut membaca sentinel untuk menghitung panjang, lalu mengembalikan slice [:0]const u8 yang tetap menyertakan sentinelnya. Dengan begitu, hasilnya tetap dapat diteruskan ke API yang mengharapkan teks ber-sentinel maupun ke API yang hanya membutuhkan slice biasa. Program sebelumnya memperlihatkan kedua bentuk hasil itu secara berurutan, lengkap dengan tipe yang dicetak oleh @typeName.

Perbandingan dan pencarian#

Pustaka standar menyediakan fungsi perbandingan dan pencarian di dalam namespace std.mem. Fungsi std.mem.eql membandingkan isi dua slice byte dan mengembalikan true hanya apabila panjang dan seluruh isinya sama. Pada program sebelumnya, "sensor" dan "Sensor" dinyatakan tidak sama karena huruf besar dan huruf kecil diperlakukan berbeda. Fungsi std.mem.startsWith memeriksa awalan, dan hasilnya true untuk "sensor" dengan awalan "sen".

Fungsi std.mem.indexOfScalar mencari satu byte dan mengembalikan optional, yaitu nilai yang mungkin tidak ada. Hasilnya adalah indeks kemunculan pertama, misalnya 0 untuk karakter s pada "sensor", dan null apabila karakter tersebut tidak ditemukan. Optional wajib dibuka secara eksplisit sebelum dipakai sebagai indeks, sehingga kegagalan pencarian tidak pernah berubah menjadi akses yang tidak sah secara diam-diam. Fungsi pencarian lain seperti std.mem.indexOf bekerja dengan pola yang sama untuk rentang byte yang lebih panjang.

Perbandingan dilakukan terhadap byte secara langsung tanpa menafsirkan penyandiannya, sehingga std.mem.eql menganggap dua teks sama hanya apabila seluruh byte-nya identik. Perilaku ini membuat hasil perbandingan dapat diprediksi untuk teks apa pun, termasuk teks yang memuat UTF-8. Apabila perbandingan yang tidak membedakan huruf besar dan kecil benar-benar diperlukan, kedua teks perlu disamakan bentuknya terlebih dahulu sebelum dibandingkan.

Contoh Lengkap#

Penghitung kata sederhana#

Program berikut menghitung jumlah kata pada sebuah kalimat dengan berpindah-pindah keadaan antara di dalam kata dan di luar kata.

zig kata.zig
const std = @import("std");

pub fn main() void {
    const kalimat = "zig menghitung kata dalam kalimat";
    const teks: []const u8 = kalimat;

    var kata: usize = 0;
    var di_dalam_kata = false;

    for (teks) |c| {
        if (c == ' ') {
            di_dalam_kata = false;
        } else if (!di_dalam_kata) {
            di_dalam_kata = true;
            kata += 1;
        }
    }

    std.debug.print("Kalimat: {s}\n", .{teks});
    std.debug.print("Jumlah kata: {d}\n", .{kata});
    std.debug.print("Panjang byte: {d}\n", .{teks.len});
}
text Keluaran
Kalimat: zig menghitung kata dalam kalimat
Jumlah kata: 5
Panjang byte: 33

Setiap transisi dari spasi ke karakter selain spasi menandai awal kata baru, sehingga penghitung bertambah lima kali untuk kalimat tersebut. Perulangan for (teks) |c| mengunjungi setiap byte, dan penugasan const teks: []const u8 = kalimat; menunjukkan bahwa literal string dapat langsung dipakai sebagai slice. Properti teks.len melaporkan 33 byte, yang pada teks ASCII sama dengan jumlah karakternya. Kesetaraan itu tidak berlaku lagi pada teks UTF-8 yang memuat karakter multibyte.

Definisi kata pada program ini sederhana, yaitu rentang byte yang dipisahkan spasi, sehingga tanda baca yang menempel tetap dihitung sebagai bagian dari kata. Pemisah lain seperti tab dan baris baru dapat ditambahkan pada kondisi yang sama apabila kalimatnya berasal dari sumber yang lebih lebar. Pendekatan berpindah keadaan semacam ini menjadi fondasi pengurai teks yang lebih lengkap, dan tidak memerlukan alokasi memori sama sekali karena seluruhnya bekerja atas slice yang sudah ada.

UTF-8 dan panjang teks#

Zig tidak mengasumsikan penyandian tertentu pada []const u8, dan literal string ditulis sebagai UTF-8. Satu karakter dapat menempati lebih dari satu byte, sehingga len menyatakan panjang byte dan bukan jumlah karakter. Program berikut membandingkan keduanya pada kata yang memuat huruf beraksen.

zig utf8.zig
const std = @import("std");

pub fn main() void {
    const beraksen = "café";

    std.debug.print("Kata: {s}\n", .{beraksen});
    std.debug.print("Panjang byte: {d}\n", .{beraksen.len});
    const jumlah = std.unicode.utf8CountCodepoints(beraksen) catch 0;
    std.debug.print("Jumlah kode: {d}\n", .{jumlah});
}
text Keluaran
Kata: café
Panjang byte: 5
Jumlah kode: 4

Kata "café" menempati lima byte karena huruf é disandikan dua byte, sedangkan jumlah code point (titik kode) Unicode-nya empat. Fungsi std.unicode.utf8CountCodepoints menghitung jumlah titik kode dan mengembalikan error union karena deretan byte apa pun bisa jadi bukan UTF-8 yang sah, sehingga contoh menyertakan catch 0 sebagai penanganan paling sederhana. Ketika program perlu memotong atau mengindeks teks UTF-8, pemotongan pada batas byte apa pun dapat membelah karakter, sehingga operasi semacam itu dilakukan terhadap batas titik kode yang sah.

Kesalahan umum#

Kesalahan pertama berupa memperlakukan []u8 dan []const u8 sebagai tipe yang saling dipertukarkan. Literal string bertipe []const u8 dan tidak dapat ditulis, sedangkan buffer yang dialokasikan bertipe []u8. Menyimpan literal pada pengikatan var tidak membuatnya dapat diubah, karena kekonstannya melekat pada data, bukan pada pengikatnya.

Kesalahan kedua berupa menghitung panjang teks berbahasa lain sebagai banyaknya karakter. Properti .len menghitung byte, sehingga teks UTF-8 dengan karakter multi-byte melaporkan angka yang lebih besar daripada banyaknya titik kodenya. Bila banyaknya karakter diperlukan, hitung titik kodenya dengan iterasi Unicode seperti pada contoh UTF-8 topik ini.

Ringkasan#

  • Literal string bertipe *const [N:0]u8 dengan sentinel nol, sedangkan API pustaka standar menerima teks sebagai []const u8 yang hanya dapat dibaca.
  • std.mem.span mengubah pointer ber-sentinel [*:0]const u8 menjadi slice [:0]const u8 dengan panjang yang dihitung dari sentinelnya.
  • Perbandingan dan pencarian memakai std.mem.eql, std.mem.startsWith, dan std.mem.indexOfScalar, yang hasil pencariannya berupa optional.
  • Properti len menyatakan panjang byte, sementara jumlah karakter UTF-8 dihitung dengan std.unicode.utf8CountCodepoints dan keduanya dapat berbeda.

Referensi#