コーディング · プログラミング言語
Zig入門:初心者向けチュートリアル
結論
Zigは、Cの現代的な代替を目指した、汎用のシステムプログラミング言語兼ツールチェーンです。隠れた制御フローも、ガベージコレクターも、隠れたメモリ確保もありません。アロケーターを明示的に渡し、エラーはエラー共用体とtryで扱い、後始末はdeferで行います。コンパイル時のコード実行(comptime)が、マクロとジェネリクスに代わります。ファイルはzig run hello.zigで実行します。Zigはまだバージョン1.0より前なので、言語と標準ライブラリの細部はリリースごとに変わります。
Zigとは何か、誰が使うのか
ZigはAndrew Kelleyが作り、非営利団体のZig Software Foundationが開発しています。Cと同じ領域、つまりOSの構成要素、組み込みファームウェア、ゲームエンジン、データベース、高速なコマンドラインツールを対象にしています。ZigコンパイラはCやC++のコードもコンパイルでき(zig cc)、多くのターゲットへクロスコンパイルもできるため、Cのツールチェーンとしてだけ使うプロジェクトもあります。
Zigで書かれた有名なプロジェクトには、JavaScriptランタイムのBun、金融データベースのTigerBeetle、ターミナルエミュレーターのGhosttyがあります。
Zigのインストールとhello, worldの実行
ziglang.org/downloadから自分のシステム向けのビルド済みアーカイブをダウンロードし、展開して、フォルダーをPATHに追加するか、システムのパッケージマネージャーでZigをインストールします。別のインストーラーやランタイムはありません。
// hello.zig
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}zig run hello.zig # compile and run
# Hello, world!
zig build-exe hello.zig # produce an executable named hello
zig init # create a project with build.zigstd.debug.printは標準エラー出力に書き込み、最近のどのバージョンでも最も簡単な出力方法です。2番目の引数は、書式のプレースホルダーに渡す値のタプルで、.{}は空のタプルです。標準出力に書き込むAPIはリリースごとに変わっているので、お使いのバージョンの標準ライブラリのドキュメントを確認してください。
変数と基本の型
constは変わらない値を、varは変更可能な変数を宣言します。コンパイラは厳格で、使われていないローカル変数はエラーになり、一度も変更されないvarもエラーになります。そのため、デフォルトではconstを使いましょう。
| 型 | 例 | 補足 |
|---|---|---|
| 符号付き整数 | i8, i32, i64 | i7のように、任意のビット幅を使えます |
| 符号なし整数 | u8, u32, usize | usizeはポインターと同じサイズで、長さや添字に使います |
| 浮動小数点数 | f32, f64 | IEEE 754 |
| bool | true, false | 整数からの暗黙の変換はありません |
| 配列 | [3]u8 | 長さが固定で、コンパイル時にわかります |
| スライス | []const u8 | ポインターと長さの組。文字列はバイトのスライスです |
| オプショナル | ?i32 | 値またはnull |
| エラー共用体 | !u32 | 値またはエラー |
const std = @import("std");
pub fn main() void {
const age: u32 = 30;
var count: i32 = 0;
count += 1;
const name = "Ana"; // string literal, coerces to []const u8
std.debug.print("{s} is {d}, count = {d}\n", .{ name, age, count });
var maybe: ?i32 = null;
maybe = 5;
const value = maybe orelse 0;
std.debug.print("value = {d}\n", .{value});
}書式のプレースホルダーは、出力のしかたを指定します。数値は{d}、文字列は{s}、1バイトを文字として出力するには{c}、任意の値のデバッグ出力には{any}です。
制御構文
if、switch、while、forは、どれも式として使えます。switchは取りうるすべての値を網羅しなければならないので、たいていはelseの分岐を加えます。
const std = @import("std");
pub fn main() void {
const n: i32 = -2;
const sign: []const u8 = if (n > 0) "positive" else if (n < 0) "negative" else "zero";
const score: u8 = 85;
const letter: u8 = switch (score) {
90...100 => 'A',
80...89 => 'B',
else => 'C',
};
std.debug.print("{s} {c}\n", .{ sign, letter }); // negative B
var i: u32 = 0;
while (i < 3) : (i += 1) {
std.debug.print("{d} ", .{i}); // 0 1 2
}
const items = [_]u8{ 10, 20, 30 };
for (items, 0..) |item, index| {
std.debug.print("\n{d}: {d}", .{ index, item });
}
std.debug.print("\n", .{});
}[_]u8{ ... }と書くと、コンパイラが配列の長さを数えてくれます。for (items, 0..)は、要素とカウンターを同時にたどります。縦線の間の値はキャプチャと呼ばれます。
関数とcomptime
関数は、パラメーターの型と戻り値の型を宣言します。引数は関数の内部では変更できません。ジェネリックなコードはcomptimeパラメーターで書きます。型はコンパイル時に存在する値なので、関数は型を引数として受け取れます。
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
pub fn main() void {
std.debug.print("{d}\n", .{add(2, 3)}); // 5
std.debug.print("{d}\n", .{max(i32, 3, 7)}); // 7
std.debug.print("{d}\n", .{max(f64, 1.5, 0.5)}); // 1.5
}コンパイラは、maxが呼ばれた型ごとに、別々のバージョンを作ります。標準ライブラリのジェネリックなデータ構造も同じ仕組みで作られています。型を受け取り、新しい構造体型を返す関数です。
エラー共用体・try・deferによるエラー処理
エラーは値です。戻り値の型!u32は「u32またはエラー」を意味します。try exprはエラーがあれば呼び出し元にそのエラーを返し、catchはその場で処理します。エラーを黙って無視することはできません。
const std = @import("std");
fn parseAge(s: []const u8) !u32 {
const n = try std.fmt.parseInt(i32, s, 10);
if (n < 0) return error.Negative;
return @intCast(n);
}
pub fn main() void {
const age = parseAge("-5") catch |err| {
std.debug.print("error: {s}\n", .{@errorName(err)});
return;
};
std.debug.print("age {d}\n", .{age});
}
// error: Negativedeferは現在のブロックを抜けるときに文を実行し、errdeferはブロックがエラーで抜けるときだけ実行します。これにより、後始末のコードを、リソースを取得するコードの隣に置けます。
メモリとアロケーター
Zigには、ガベージコレクターも、隠れたグローバルなアロケーターもありません。ヒープメモリを必要とする関数はstd.mem.Allocatorパラメーターを受け取るので、メモリをどこから取るか(ページアロケーター、アリーナ、固定バッファ、リークを検出するデバッグ用アロケーターなど)は呼び出し側が決めます。
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const buf = try allocator.alloc(u32, 5);
defer allocator.free(buf);
for (buf, 0..) |*slot, i| {
slot.* = @intCast(i * i);
}
std.debug.print("{any}\n", .{buf}); // { 0, 1, 4, 9, 16 }
}|*slot|は各要素へのポインターをキャプチャするので、ループから書き込めます。slot.*はポインターを参照外しします。DebugとReleaseSafeのビルドでは、整数のオーバーフローや範囲外の添字アクセスは、未定義動作ではなく、わかりやすいパニックでプログラムを止めます。
小さな完成プログラム:数値の平均
const std = @import("std");
fn average(numbers: []const i64) ?f64 {
if (numbers.len == 0) return null;
var sum: i64 = 0;
for (numbers) |n| sum += n;
const total: f64 = @floatFromInt(sum);
const len: f64 = @floatFromInt(numbers.len);
return total / len;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const buf = try allocator.alloc(i64, 16);
defer allocator.free(buf);
var it = std.mem.tokenizeScalar(u8, "4 8 15 16 23 42", ' ');
var count: usize = 0;
while (it.next()) |word| : (count += 1) {
buf[count] = try std.fmt.parseInt(i64, word, 10);
}
if (average(buf[0..count])) |avg| {
std.debug.print("count={d} avg={d:.2}\n", .{ count, avg });
}
}
// count=6 avg=18.00このプログラムは、文字列を空白で分割し、各部分をtryで解析し、数値をdeferで解放するヒープメモリに保存して、空のリストではnullになるオプショナルの平均を返します。
次に学ぶこと
- ziglang.org/documentationにある言語リファレンス。インストールしたバージョンのページを選びます。
- ziglang.orgのLearnセクション。「Getting Started」と言語の概要があります。
- 壊れた小さなプログラムを1つずつ直していくZiglingsは、人気のあるコミュニティの練習問題集です。
- 標準ライブラリのソースを読む。Zigで書かれており、現在のAPIの最良のリファレンスです。
よくある質問
Zigは本番で使える段階ですか?
本番でZigを使っている企業もありますが、言語は1.0より前なので、コンパイラをアップグレードすると互換性のない変更が入るものと想定し、その対応の時間を見込んでおく必要があります。
ZigはCやRustの代わりになりますか?
ZigはCに最も近く、手動のメモリ管理に、より良い安全性チェック、エラー処理、コンパイル時のコードを加えたものです。Rustは所有権システムと借用チェッカーでメモリ安全性を保証しますが、Zigにはそれがありません。
ZigからCライブラリを使えますか?
はい。ZigはCのヘッダーファイルをインポートしてC関数を直接呼び出せ、ZigコンパイラはCやC++のソースファイルもビルドできるため、言語を混ぜるのは簡単です。
Zigのcomptimeとはどういう意味ですか?
comptimeは、コンパイル中に評価されるコードやパラメーターを示します。Zigは、別のマクロ言語の代わりに、これをジェネリクス、定数テーブル、コンパイル時チェックに使います。