目的
この記事では、Zigのデザイン原則(明示的アロケータ、comptime)を理解し、実践的な設計ができるようになることを目指します。
前提知識:
- Beginner編の内容(基本構文、エラー処理の基礎、構造体、defer/errdefer)
この記事で学ぶこと:
- エラー処理の高度なテクニック
- メモリアロケータの概念と使い分け
- comptime による強力なメタプログラミング
- タグ付きユニオン(Rustのenum相当)
- ポインタとスライスの深い理解
- 動的データ構造(ArrayList、HashMap)
- パッケージとモジュールの構成
1. エラー処理の深掘り
エラーセットの組み合わせ
const std = @import("std");
const FileError = error{
NotFound,
PermissionDenied,
AlreadyExists,
};
const NetworkError = error{
ConnectionRefused,
Timeout,
InvalidResponse,
};
// エラーセットを結合(|| 演算子)
const AppError = FileError || NetworkError;
fn readFromNetwork(url: []const u8) AppError![]const u8 {
if (std.mem.eql(u8, url, "bad")) {
return error.ConnectionRefused;
}
if (std.mem.eql(u8, url, "missing")) {
return error.NotFound;
}
return "data";
}
pub fn main() !void {
const result = readFromNetwork("good") catch |err| {
switch (err) {
error.NotFound => std.debug.print("File not found\n", .{}),
error.ConnectionRefused => std.debug.print("Connection refused\n", .{}),
error.Timeout => std.debug.print("Timeout\n", .{}),
else => std.debug.print("Other error: {}\n", .{err}),
}
return;
};
std.debug.print("Result: {s}\n", .{result});
}
エラー推論(anyerror)
// anyerror は全てのエラーを含む
fn mightFail() anyerror!i32 {
return error.UnknownError;
}
// エラーセットを推論(!T)
fn autoInfer() !i32 {
if (false) return error.Failed;
if (false) return error.AnotherError;
return 42;
}
// 推論されたエラーセット: error{Failed, AnotherError}
カスタムエラーの設計
const std = @import("std");
// レイヤーごとにエラーを定義
const DatabaseError = error{
ConnectionFailed,
QueryFailed,
TransactionFailed,
};
const ValidationError = error{
InvalidEmail,
InvalidPassword,
UsernameTooShort,
};
const AuthError = DatabaseError || ValidationError;
const User = struct {
id: u32,
username: []const u8,
email: []const u8,
};
fn validateUsername(username: []const u8) ValidationError!void {
if (username.len < 3) {
return error.UsernameTooShort;
}
}
fn validateEmail(email: []const u8) ValidationError!void {
if (std.mem.indexOf(u8, email, "@") == null) {
return error.InvalidEmail;
}
}
fn createUser(username: []const u8, email: []const u8) AuthError!User {
// バリデーション(エラーは自動で伝播)
try validateUsername(username);
try validateEmail(email);
// データベース操作をシミュレート
if (std.mem.eql(u8, username, "error")) {
return error.ConnectionFailed;
}
return User{
.id = 1,
.username = username,
.email = email,
};
}
pub fn main() !void {
const user = createUser("alice", "alice@example.com") catch |err| {
switch (err) {
error.UsernameTooShort => {
std.debug.print("Username must be at least 3 characters\n", .{});
},
error.InvalidEmail => {
std.debug.print("Invalid email format\n", .{});
},
error.ConnectionFailed => {
std.debug.print("Database connection failed\n", .{});
},
else => {
std.debug.print("Unknown error: {}\n", .{err});
},
}
return;
};
std.debug.print("Created user: {s}\n", .{user.username});
}
errdefer の実践パターン
const std = @import("std");
fn processWithCleanup(allocator: std.mem.Allocator) !void {
// リソース1を確保
const buffer1 = try allocator.alloc(u8, 1024);
errdefer allocator.free(buffer1); // エラー時にクリーンアップ
// リソース2を確保
const buffer2 = try allocator.alloc(u8, 2048);
errdefer allocator.free(buffer2); // エラー時にクリーンアップ
// エラーが起きるかもしれない処理
if (false) return error.ProcessingFailed;
// 成功時はdeferで解放
defer {
allocator.free(buffer1);
allocator.free(buffer2);
}
std.debug.print("Processing succeeded\n", .{});
}
Rustとの比較:
// Rust: Result<T, E> と thiserror
#[derive(Error, Debug)]
enum AppError {
#[error("File error: {0}")]
File(#[from] std::io::Error),
#[error("Network error: {0}")]
Network(String),
}
// Zig: エラーセットの組み合わせ
const FileError = error{NotFound, PermissionDenied};
const NetworkError = error{ConnectionRefused, Timeout};
const AppError = FileError || NetworkError;
学習ポイント:
-
||でエラーセットを結合 -
anyerrorは全エラーを含む - エラーは型として推論される
- レイヤーごとにエラーを定義
-
errdeferでリソース管理
2. メモリアロケータの概念
なぜアロケータが必要か
Zigには以下がありません:
- ガーベージコレクタ
- 暗黙的なメモリ管理
- グローバルアロケータ
代わりに:明示的にアロケータを渡す
const std = @import("std");
pub fn main() !void {
// アロケータを明示的に選択
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// メモリ確保
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
std.debug.print("Allocated {} bytes\n", .{buffer.len});
}
std.mem.Allocator インターフェース
// Allocator は構造体(Rustのトレイトに相当)
pub const Allocator = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
alloc: *const fn(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8,
resize: *const fn(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool,
free: *const fn(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void,
};
// 使いやすいラッパー関数
pub fn alloc(self: Allocator, comptime T: type, n: usize) ![]T { ... }
pub fn free(self: Allocator, memory: anytype) void { ... }
};
標準アロケータの種類
const std = @import("std");
pub fn main() !void {
// 1. page_allocator(システムアロケータ)
// - 直接OSからメモリを取得
// - 遅いが、シンプル
const page_alloc = std.heap.page_allocator;
// 2. GeneralPurposeAllocator(汎用アロケータ)
// - メモリリークを検出
// - デバッグに最適
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const gpa_alloc = gpa.allocator();
// 3. ArenaAllocator(アリーナアロケータ)
// - まとめて解放
// - 一時的なメモリに最適
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // 全てまとめて解放
const arena_alloc = arena.allocator();
// 4. FixedBufferAllocator(固定バッファアロケータ)
// - スタック上のバッファを使用
// - OSコールなし
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const fba_alloc = fba.allocator();
// 使用例
const data1 = try page_alloc.alloc(u8, 100);
defer page_alloc.free(data1);
const data2 = try arena_alloc.alloc(u8, 200);
// arena.deinit() で自動解放されるので個別のfreeは不要
std.debug.print("Allocated successfully\n", .{});
}
アロケータの渡し方
const std = @import("std");
// 関数にアロケータを渡す
fn processData(allocator: std.mem.Allocator, size: usize) ![]u8 {
const buffer = try allocator.alloc(u8, size);
// 処理...
return buffer;
}
// 構造体にアロケータを保持
const DataProcessor = struct {
allocator: std.mem.Allocator,
buffer: []u8,
pub fn init(allocator: std.mem.Allocator, size: usize) !DataProcessor {
const buffer = try allocator.alloc(u8, size);
return DataProcessor{
.allocator = allocator,
.buffer = buffer,
};
}
pub fn deinit(self: *DataProcessor) void {
self.allocator.free(self.buffer);
}
pub fn process(self: *DataProcessor) void {
std.debug.print("Processing {} bytes\n", .{self.buffer.len});
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// パターン1: 関数に渡す
const data = try processData(allocator, 1024);
defer allocator.free(data);
// パターン2: 構造体に保持
var processor = try DataProcessor.init(allocator, 2048);
defer processor.deinit();
processor.process();
}
Rustとの比較:
// Rust: 所有権システム(暗黙的)
let vec = Vec::new(); // グローバルアロケータ
let data = vec![1, 2, 3]; // 自動管理
// スコープ終了でDrop
// Zig: 明示的アロケータ
var list = std.ArrayList(i32).init(allocator); // 明示的
defer list.deinit(); // 明示的解放
学習ポイント:
- アロケータは明示的に渡す
-
std.mem.Allocatorはインターフェース - 用途に応じてアロケータを使い分け
- ArenaAllocator は一時的なメモリに便利
- Rustの所有権とは異なるアプローチ
3. ポインタとスライス
シングルアイテムポインタ
const std = @import("std");
pub fn main() void {
var x: i32 = 42;
// ポインタの作成
const ptr: *i32 = &x;
const const_ptr: *const i32 = &x;
// 参照外し
std.debug.print("Value: {}\n", .{ptr.*});
// ポインタ経由で変更
ptr.* = 100;
std.debug.print("New value: {}\n", .{x});
// const ポインタは変更不可
// const_ptr.* = 200; // ❌ エラー
}
マルチアイテムポインタ
pub fn main() void {
const array = [_]i32{ 1, 2, 3, 4, 5 };
// マルチアイテムポインタ(長さ情報なし)
const ptr: [*]const i32 = &array;
// インデックスでアクセス
std.debug.print("ptr[0] = {}\n", .{ptr[0]});
std.debug.print("ptr[1] = {}\n", .{ptr[1]});
// 長さは自分で管理する必要がある
for (0..array.len) |i| {
std.debug.print("ptr[{}] = {}\n", .{i, ptr[i]});
}
}
スライスの内部構造
pub fn main() void {
const array = [_]i32{ 1, 2, 3, 4, 5 };
// スライス = ポインタ + 長さ
const slice: []const i32 = &array;
// スライスの構造
std.debug.print("ptr: {*}\n", .{slice.ptr});
std.debug.print("len: {}\n", .{slice.len});
// スライスからポインタを取得
const ptr: [*]const i32 = slice.ptr;
std.debug.print("ptr[0] = {}\n", .{ptr[0]});
}
ポインタとスライスの変換
pub fn main() void {
var array = [_]i32{ 1, 2, 3, 4, 5 };
// 配列 → スライス
const slice: []i32 = &array;
// スライス → ポインタ
const ptr: [*]i32 = slice.ptr;
// ポインタ → スライス(長さを指定)
const new_slice: []i32 = ptr[0..5];
// シングルアイテムポインタ → スライス(長さ1)
var single: i32 = 42;
const single_ptr: *i32 = &single;
const single_slice: []i32 = single_ptr[0..1];
std.debug.print("single_slice[0] = {}\n", .{single_slice[0]});
}
ポインタの実践例
const std = @import("std");
// 関数外の値を変更
fn increment(ptr: *i32) void {
ptr.* += 1;
}
// 配列を操作
fn fillArray(array: []i32, value: i32) void {
for (array) |*item| {
item.* = value;
}
}
// スライスを返す
fn getSubslice(data: []const u8, start: usize, end: usize) []const u8 {
return data[start..end];
}
pub fn main() void {
var x: i32 = 10;
increment(&x);
std.debug.print("x = {}\n", .{x}); // 11
var array = [_]i32{0} ** 5;
fillArray(&array, 42);
std.debug.print("array = {any}\n", .{array}); // [42, 42, 42, 42, 42]
const data = "Hello, Zig!";
const sub = getSubslice(data, 0, 5);
std.debug.print("sub = {s}\n", .{sub}); // "Hello"
}
Rustとの比較:
// Rust: 借用
fn increment(x: &mut i32) {
*x += 1;
}
let mut x = 10;
increment(&mut x);
// Zig: ポインタ
fn increment(ptr: *i32) void {
ptr.* += 1;
}
var x: i32 = 10;
increment(&x);
学習ポイント:
-
T: シングルアイテムポインタ -
[*]T: マルチアイテムポインタ(長さなし) -
[]T: スライス(ポインタ + 長さ) - ポインタは明示的な参照外し(
.*) - Rustの借用より低レベル
4. ユニオンとタグ付きユニオン
基本的なユニオン
const std = @import("std");
// 通常のユニオン(タグなし)
const Value = union {
int: i32,
float: f64,
boolean: bool,
};
pub fn main() void {
var value = Value{ .int = 42 };
// どのフィールドがアクティブかはプログラマが管理
std.debug.print("int: {}\n", .{value.int});
// 別のフィールドに変更
value = Value{ .float = 3.14 };
std.debug.print("float: {}\n", .{value.float});
// 間違ったフィールドにアクセスすると未定義動作
// std.debug.print("int: {}\n", .{value.int}); // ❌ UB
}
タグ付きユニオン(Rustの enum 相当)
const std = @import("std");
// タグ付きユニオン
const Result = union(enum) {
ok: i32,
err: []const u8,
pub fn isOk(self: Result) bool {
return switch (self) {
.ok => true,
.err => false,
};
}
};
pub fn main() void {
const success = Result{ .ok = 42 };
const failure = Result{ .err = "Something went wrong" };
// switch でパターンマッチング
switch (success) {
.ok => |value| std.debug.print("Success: {}\n", .{value}),
.err => |msg| std.debug.print("Error: {s}\n", .{msg}),
}
switch (failure) {
.ok => |value| std.debug.print("Success: {}\n", .{value}),
.err => |msg| std.debug.print("Error: {s}\n", .{msg}),
}
std.debug.print("success.isOk() = {}\n", .{success.isOk()});
std.debug.print("failure.isOk() = {}\n", .{failure.isOk()});
}
複雑なタグ付きユニオン
const std = @import("std");
const Message = union(enum) {
quit,
move: struct { x: i32, y: i32 },
write: []const u8,
change_color: struct { r: u8, g: u8, b: u8 },
pub fn handle(self: Message) void {
switch (self) {
.quit => std.debug.print("Quitting\n", .{}),
.move => |pos| std.debug.print("Moving to ({}, {})\n", .{pos.x, pos.y}),
.write => |text| std.debug.print("Writing: {s}\n", .{text}),
.change_color => |color| std.debug.print("Color: RGB({}, {}, {})\n", .{color.r, color.g, color.b}),
}
}
};
pub fn main() void {
const messages = [_]Message{
Message.quit,
Message{ .move = .{ .x = 10, .y = 20 } },
Message{ .write = "Hello" },
Message{ .change_color = .{ .r = 255, .g = 0, .b = 0 } },
};
for (messages) |msg| {
msg.handle();
}
}
Rustの enum との比較
// Rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
impl Message {
fn handle(&self) {
match self {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: RGB({}, {}, {})", r, g, b),
}
}
}
// Zig
const Message = union(enum) {
quit,
move: struct { x: i32, y: i32 },
write: []const u8,
change_color: struct { r: u8, g: u8, b: u8 },
pub fn handle(self: Message) void {
switch (self) {
.quit => std.debug.print("Quitting\n", .{}),
.move => |pos| std.debug.print("Moving to ({}, {})\n", .{pos.x, pos.y}),
.write => |text| std.debug.print("Writing: {s}\n", .{text}),
.change_color => |color| std.debug.print("Color: RGB({}, {}, {})\n", .{color.r, color.g, color.b}),
}
}
};
学習ポイント:
-
unionは複数の型のいずれか -
union(enum)でタグ付きユニオン -
switchでパターンマッチング - Rustの
enumとほぼ同等 -
|variable|で値を取り出す
5. enum (列挙型)
enum の基本
const std = @import("std");
const Direction = enum {
north,
south,
east,
west,
pub fn opposite(self: Direction) Direction {
return switch (self) {
.north => .south,
.south => .north,
.east => .west,
.west => .east,
};
}
};
pub fn main() void {
const dir = Direction.north;
std.debug.print("Direction: {}\n", .{dir});
std.debug.print("Opposite: {}\n", .{dir.opposite()});
}
整数値の割り当て
const std = @import("std");
const StatusCode = enum(u16) {
ok = 200,
created = 201,
bad_request = 400,
not_found = 404,
internal_error = 500,
};
pub fn main() void {
const code = StatusCode.ok;
// enum → 整数
const value: u16 = @intFromEnum(code);
std.debug.print("Code: {}\n", .{value}); // 200
// 整数 → enum
const from_int = @as(StatusCode, @enumFromInt(404));
std.debug.print("Status: {}\n", .{from_int}); // not_found
}
switch での使用
const std = @import("std");
const Color = enum {
red,
green,
blue,
yellow,
pub fn toRGB(self: Color) struct { r: u8, g: u8, b: u8 } {
return switch (self) {
.red => .{ .r = 255, .g = 0, .b = 0 },
.green => .{ .r = 0, .g = 255, .b = 0 },
.blue => .{ .r = 0, .g = 0, .b = 255 },
.yellow => .{ .r = 255, .g = 255, .b = 0 },
};
}
};
pub fn main() void {
const color = Color.red;
const rgb = color.toRGB();
std.debug.print("RGB: ({}, {}, {})\n", .{rgb.r, rgb.g, rgb.b});
}
学習ポイント:
-
enumは名前付き定数 - 整数値を割り当て可能
-
@intFromEnumと@enumFromIntで変換 -
switchで網羅性チェック
6. comptime の深堀り
comptime 変数と関数
const std = @import("std");
// コンパイル時に計算
comptime {
var sum: i32 = 0;
var i: i32 = 0;
while (i < 10) : (i += 1) {
sum += i;
}
std.debug.assert(sum == 45);
}
// コンパイル時関数
fn fibonacci(n: comptime_int) comptime_int {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn main() void {
// コンパイル時に計算(実行時コストゼロ)
const fib10 = comptime fibonacci(10);
std.debug.print("fibonacci(10) = {}\n", .{fib10});
}
inline for / while
pub fn main() void {
// inline for: ループをアンロール
inline for (0..5) |i| {
std.debug.print("{} ", .{i});
}
std.debug.print("\n", .{});
// 配列の各要素に対して実行
const types = [_]type{ i8, i16, i32, i64 };
inline for (types) |T| {
std.debug.print("Size of {}: {}\n", .{T, @sizeOf(T)});
}
}
型生成
const std = @import("std");
// 型を返す関数
fn Pair(comptime T: type) type {
return struct {
first: T,
second: T,
pub fn init(first: T, second: T) @This() {
return .{ .first = first, .second = second };
}
pub fn swap(self: *@This()) void {
const temp = self.first;
self.first = self.second;
self.second = temp;
}
};
}
pub fn main() void {
var pair_int = Pair(i32).init(10, 20);
std.debug.print("Before: ({}, {})\n", .{pair_int.first, pair_int.second});
pair_int.swap();
std.debug.print("After: ({}, {})\n", .{pair_int.first, pair_int.second});
var pair_str = Pair([]const u8).init("hello", "world");
std.debug.print("Strings: ({s}, {s})\n", .{pair_str.first, pair_str.second});
}
コンパイル時実行の活用
const std = @import("std");
// 文字列の長さをコンパイル時に計算
fn compileTimeStrLen(comptime str: []const u8) comptime_int {
return str.len;
}
// 配列を生成
fn generateArray(comptime size: usize, comptime value: i32) [size]i32 {
var array: [size]i32 = undefined;
for (&array) |*item| {
item.* = value;
}
return array;
}
pub fn main() void {
// コンパイル時に長さを計算
const len = comptime compileTimeStrLen("Hello, Zig!");
std.debug.print("Length: {}\n", .{len});
// コンパイル時に配列を生成
const array = comptime generateArray(5, 42);
std.debug.print("Array: {any}\n", .{array});
}
Rustのマクロとの比較:
// Rust: マクロ
macro_rules! create_array {
($size:expr, $val:expr) => {
[$val; $size]
};
}
let array = create_array!(5, 42);
// Zig: comptime関数
fn generateArray(comptime size: usize, comptime value: i32) [size]i32 {
var array: [size]i32 = undefined;
for (&array) |*item| {
item.* = value;
}
return array;
}
const array = comptime generateArray(5, 42);
学習ポイント:
-
comptimeでコンパイル時実行 -
inlineでループアンロール - 型を生成する関数
- Rustのマクロより強力
7. ジェネリクス
anytype による型推論
const std = @import("std");
// anytype: 呼び出し時に型推論
fn max(a: anytype, b: anytype) @TypeOf(a, b) {
return if (a > b) a else b;
}
fn print(value: anytype) void {
std.debug.print("Value: {any}\n", .{value});
}
pub fn main() void {
const x = max(10, 20);
const y = max(1.5, 2.5);
std.debug.print("max(10, 20) = {}\n", .{x});
std.debug.print("max(1.5, 2.5) = {}\n", .{y});
print(42);
print("hello");
print(true);
}
comptime type パラメータ
const std = @import("std");
// 明示的な型パラメータ
fn swap(comptime T: type, a: *T, b: *T) void {
const temp = a.*;
a.* = b.*;
b.* = temp;
}
fn sum(comptime T: type, values: []const T) T {
var result: T = 0;
for (values) |value| {
result += value;
}
return result;
}
pub fn main() void {
var x: i32 = 10;
var y: i32 = 20;
swap(i32, &x, &y);
std.debug.print("x = {}, y = {}\n", .{x, y});
const numbers = [_]i32{ 1, 2, 3, 4, 5 };
const total = sum(i32, &numbers);
std.debug.print("Sum: {}\n", .{total});
}
型制約の実現
const std = @import("std");
// 型が特定のメソッドを持つことを要求
fn printable(comptime T: type) bool {
return @hasDecl(T, "print");
}
fn callPrint(value: anytype) void {
comptime {
if (!printable(@TypeOf(value))) {
@compileError("Type must have a 'print' method");
}
}
value.print();
}
const PrintableInt = struct {
value: i32,
pub fn print(self: PrintableInt) void {
std.debug.print("Value: {}\n", .{self.value});
}
};
pub fn main() void {
const p = PrintableInt{ .value = 42 };
callPrint(p);
// コンパイルエラー: 'print' メソッドがない
// callPrint(42);
}
ジェネリック構造体
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.items = std.ArrayList(T).init(allocator),
};
}
pub fn deinit(self: *@This()) void {
self.items.deinit();
}
pub fn push(self: *@This(), item: T) !void {
try self.items.append(item);
}
pub fn pop(self: *@This()) ?T {
if (self.items.items.len == 0) return null;
return self.items.pop();
}
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var stack = Stack(i32).init(allocator);
defer stack.deinit();
try stack.push(10);
try stack.push(20);
try stack.push(30);
while (stack.pop()) |value| {
std.debug.print("Popped: {}\n", .{value});
}
}
Rustのトレイト境界との比較:
// Rust: トレイト境界
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Zig: anytype + comptime
fn max(a: anytype, b: anytype) @TypeOf(a, b) {
return if (a > b) a else b;
}
学習ポイント:
-
anytypeで型推論 -
comptime T: typeで明示的な型パラメータ -
@hasDeclで型制約を実現 - Rustのトレイトより柔軟だが、型安全性は低い
8. 動的データ構造(ArrayList)
ArrayList
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ArrayList の作成
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
// 要素の追加
try list.append(10);
try list.append(20);
try list.append(30);
std.debug.print("Length: {}\n", .{list.items.len});
// 要素へのアクセス
for (list.items, 0..) |item, i| {
std.debug.print("[{}] = {}\n", .{i, item});
}
// 要素の削除
_ = list.pop();
// 挿入
try list.insert(1, 15);
// クリア
list.clearRetainingCapacity();
}
HashMap
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// HashMap の作成
var map = std.AutoHashMap([]const u8, i32).init(allocator);
defer map.deinit();
// 要素の追加
try map.put("alice", 30);
try map.put("bob", 25);
try map.put("charlie", 35);
// 要素の取得
if (map.get("alice")) |age| {
std.debug.print("Alice is {} years old\n", .{age});
}
// 要素の存在確認
if (map.contains("bob")) {
std.debug.print("Bob exists\n", .{});
}
// イテレート
var iter = map.iterator();
while (iter.next()) |entry| {
std.debug.print("{s}: {}\n", .{entry.key_ptr.*, entry.value_ptr.*});
}
// 削除
_ = map.remove("charlie");
}
アロケータの実践
const std = @import("std");
fn processData(allocator: std.mem.Allocator) !void {
// 一時的なデータ構造
var list = std.ArrayList([]const u8).init(allocator);
defer list.deinit();
try list.append("hello");
try list.append("world");
try list.append("zig");
for (list.items) |item| {
std.debug.print("{s}\n", .{item});
}
}
pub fn main() !void {
// ArenaAllocator で一括管理
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // 全てまとめて解放
const allocator = arena.allocator();
try processData(allocator);
// deinit 忘れても arena.deinit() で解放される
}
カスタムデータ構造
const std = @import("std");
const Queue = struct {
items: std.ArrayList(i32),
pub fn init(allocator: std.mem.Allocator) Queue {
return Queue{
.items = std.ArrayList(i32).init(allocator),
};
}
pub fn deinit(self: *Queue) void {
self.items.deinit();
}
pub fn enqueue(self: *Queue, value: i32) !void {
try self.items.append(value);
}
pub fn dequeue(self: *Queue) ?i32 {
if (self.items.items.len == 0) return null;
return self.items.orderedRemove(0);
}
pub fn isEmpty(self: Queue) bool {
return self.items.items.len == 0;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var queue = Queue.init(allocator);
defer queue.deinit();
try queue.enqueue(1);
try queue.enqueue(2);
try queue.enqueue(3);
while (queue.dequeue()) |value| {
std.debug.print("Dequeued: {}\n", .{value});
}
}
Rustとの比較:
// Rust: Vec と HashMap
let mut vec = Vec::new();
vec.push(10);
let mut map = HashMap::new();
map.insert("key", "value");
// Zig: ArrayList と HashMap
var list = std.ArrayList(i32).init(allocator);
try list.append(10);
var map = std.AutoHashMap([]const u8, []const u8).init(allocator);
try map.put("key", "value");
学習ポイント:
-
ArrayListは動的配列 -
AutoHashMapはハッシュマップ - 全ての動的データ構造にアロケータが必要
-
deferで確実に解放
9. パッケージとモジュール
@import の詳細
// src/math.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn multiply(a: i32, b: i32) i32 {
return a * b;
}
const PI = 3.14159; // プライベート
pub const E = 2.71828; // 公開
// src/main.zig
const std = @import("std");
const math = @import("math.zig");
pub fn main() void {
const sum = math.add(10, 20);
const product = math.multiply(5, 6);
std.debug.print("Sum: {}\n", .{sum});
std.debug.print("Product: {}\n", .{product});
std.debug.print("E: {}\n", .{math.E});
// math.PI は使えない(プライベート)
}
pub による公開制御
// src/user.zig
const std = @import("std");
pub const User = struct {
id: u32,
name: []const u8,
email: []const u8,
password_hash: []const u8, // プライベートフィールド
// 公開コンストラクタ
pub fn init(id: u32, name: []const u8, email: []const u8) User {
return User{
.id = id,
.name = name,
.email = email,
.password_hash = "", // 仮の値
};
}
// 公開メソッド
pub fn getName(self: User) []const u8 {
return self.name;
}
// プライベートメソッド
fn hashPassword(password: []const u8) []const u8 {
// パスワードをハッシュ化
return password; // 簡略化
}
};
// 公開関数
pub fn validateEmail(email: []const u8) bool {
return std.mem.indexOf(u8, email, "@") != null;
}
// プライベート関数
fn internalHelper() void {
// ...
}
// src/main.zig
const std = @import("std");
const user_mod = @import("user.zig");
pub fn main() void {
const user = user_mod.User.init(1, "Alice", "alice@example.com");
std.debug.print("Name: {s}\n", .{user.getName()});
// user.password_hash は直接アクセスできない
// user_mod.internalHelper() は呼べない
if (user_mod.validateEmail("test@example.com")) {
std.debug.print("Valid email\n", .{});
}
}
パッケージ構成
my-project/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
├── models/
│ ├── user.zig
│ └── post.zig
├── services/
│ ├── auth.zig
│ └── database.zig
└── utils/
└── helpers.zig
// src/models/user.zig
pub const User = struct {
id: u32,
name: []const u8,
};
// src/main.zig
const std = @import("std");
const User = @import("models/user.zig").User;
pub fn main() void {
const user = User{ .id = 1, .name = "Alice" };
std.debug.print("User: {s}\n", .{user.name});
}
build.zig.zon(依存関係)
// build.zig.zon
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.@"some-package" = .{
.url = "https://github.com/user/some-package/archive/main.tar.gz",
.hash = "1220...",
},
},
}
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-project",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// 依存パッケージを追加
const some_package = b.dependency("some-package", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("some-package", some_package.module("some-package"));
b.installArtifact(exe);
}
学習ポイント:
-
@importでモジュールをインポート -
pubで公開制御 - ディレクトリ構成は自由
-
build.zig.zonで依存管理
10. よく使う標準ライブラリ
std.mem (メモリ操作)
const std = @import("std");
pub fn main() !void {
// メモリのコピー
var src = [_]u8{ 1, 2, 3, 4, 5 };
var dst: [5]u8 = undefined;
@memcpy(&dst, &src);
// メモリの比較
const equal = std.mem.eql(u8, &src, &dst);
std.debug.print("Equal: {}\n", .{equal});
// 文字列の操作
const text = "Hello, Zig!";
// 文字列の検索
if (std.mem.indexOf(u8, text, "Zig")) |index| {
std.debug.print("Found at index {}\n", .{index});
}
// 文字列の分割
var iter = std.mem.splitSequence(u8, text, ", ");
while (iter.next()) |part| {
std.debug.print("Part: {s}\n", .{part});
}
// トリム
const whitespace = " hello ";
const trimmed = std.mem.trim(u8, whitespace, " ");
std.debug.print("Trimmed: '{s}'\n", .{trimmed});
}
std.fs (ファイル操作)
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ファイルの書き込み
const file = try std.fs.cwd().createFile("test.txt", .{});
defer file.close();
try file.writeAll("Hello, Zig!\n");
try file.writeAll("This is a test.\n");
// ファイルの読み込み
const content = try std.fs.cwd().readFileAlloc(
allocator,
"test.txt",
1024 * 1024, // 最大サイズ
);
defer allocator.free(content);
std.debug.print("Content:\n{s}\n", .{content});
// ディレクトリの作成
try std.fs.cwd().makeDir("test_dir");
// ディレクトリの削除
try std.fs.cwd().deleteDir("test_dir");
// ファイルの削除
try std.fs.cwd().deleteFile("test.txt");
}
std.json (JSON処理)
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
email: []const u8,
active: bool,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSONのシリアライズ
const user = User{
.id = 1,
.name = "Alice",
.email = "alice@example.com",
.active = true,
};
var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();
try std.json.stringify(user, .{}, buffer.writer());
std.debug.print("JSON: {s}\n", .{buffer.items});
// JSONのデシリアライズ
const json_text =
\\{
\\ "id": 2,
\\ "name": "Bob",
\\ "email": "bob@example.com",
\\ "active": false
\\}
;
const parsed = try std.json.parseFromSlice(
User,
allocator,
json_text,
.{},
);
defer parsed.deinit();
std.debug.print("Parsed: {s}\n", .{parsed.value.name});
}
実践的な例:CLIツール
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// コマンドライン引数を取得
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
std.debug.print("Usage: {s} <filename>\n", .{args[0]});
return;
}
const filename = args[1];
// ファイルを読み込み
const content = std.fs.cwd().readFileAlloc(
allocator,
filename,
1024 * 1024,
) catch |err| {
std.debug.print("Error reading file: {}\n", .{err});
return;
};
defer allocator.free(content);
// 行数と文字数をカウント
var line_count: usize = 0;
var char_count: usize = content.len;
for (content) |c| {
if (c == '\n') line_count += 1;
}
std.debug.print("Lines: {}\n", .{line_count});
std.debug.print("Characters: {}\n", .{char_count});
}
学習ポイント:
-
std.memでメモリ操作 -
std.fsでファイル操作 -
std.jsonでJSON処理 - 標準ライブラリは豊富
11. まとめ
Intermediate編で学んだこと
✅ エラー処理の深掘り
- エラーセットの組み合わせ(
||) - エラー推論(
anyerror) - カスタムエラーの設計
-
errdeferの実践
✅ メモリアロケータ
- アロケータの概念
- 標準アロケータ(page, GPA, Arena, FixedBuffer)
- 明示的なメモリ管理
- Rustとの違い
✅ ポインタとスライス
- シングルアイテムポインタ(
T) - マルチアイテムポインタ(
[*]T) - スライスの内部構造
- ポインタの実践
✅ タグ付きユニオン
-
union(enum)の使い方 - パターンマッチング
- Rustの
enumとの比較
✅ comptime
- コンパイル時計算
- 型生成
- ジェネリクス
- メタプログラミング
✅ 動的データ構造
ArrayListHashMap- アロケータの実践
✅ パッケージとモジュール
@import-
pubによる公開制御 build.zig.zon
Rust vs Zig の理解
| 概念 | Rust | Zig |
|---|---|---|
| メモリ管理 | 所有権(暗黙的) | アロケータ(明示的) |
| エラー | Result | error union (!T) |
| ジェネリクス | トレイト境界 | comptime + anytype |
| enum | enum + match | union(enum) + switch |
| ポインタ | &T, &mut T | *T, *const T |
| マクロ | macro_rules!, proc_macro | comptime |
実践的なCLIツールの例
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 引数の処理
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// ファイル操作
var list = std.ArrayList([]const u8).init(allocator);
defer list.deinit();
// JSON処理
const json_text = try std.json.stringifyAlloc(
allocator,
list.items,
.{},
);
defer allocator.free(json_text);
std.debug.print("{s}\n", .{json_text});
}
公式リソース: