ざっくりZig - 配列とタプル
配列
配列(Array)は同じ型の複数のデータを同時に格納することを表す型です。格納された個別のデータを要素(element)といいます。型名は[要素の数]要素の型
とします。要素の数が確定しない場合は[]u8
のように要素の数を省略することもあります。オプションの場合は先頭に?
をつけます。
// 配列にデータを格納(型は[3]u8)
const a = [_]u8{ 3, 5, 2 }; // [3]u8{ 3, 5, 2 } と要素の数を明示してもよい
// 配列に格納されたデータを出力
try stdout.print("a[0] = {}, a[1] = {}, a[2] = {}\n", .{ a[0], a[1], a[2] });
// 配列の長さ
try stdout.print("a.len = {}\n", .{a.len});
// 結果
a[0] = 3, a[1] = 5, a[2] = 2
a.len = 3
const a = [_]u8{ 3, 5, 2 }
はu8型の値3, 5, 2
のそれぞれをa
という配列に格納することを表します。配列の型は[3]u8
となります。配列の先頭の要素をa[0]
とし、それ以降をa[1], a[2], ...
とします。
[0], [1], ...
の部分は添え字(index)といいます。末尾の要素の添え字は[要素の数 - 1]
となります。そして配列の要素数を配列の長さといいa.len
と表します。なお、要素のない配列は[_]u8{}
とします。このときの型は[0]u8
となります。
配列とfor
配列とforを組み合わせると、配列のそれぞれの要素を用いた繰り返し処理ができます。
// 合計の計算用(初期設定)
var s: u8 = 0;
try stdout.print("s = {}\n", .{s});
// aの各要素をvに代入して{...}内の処理を行う
for (a) |v| {
try stdout.print("{} + {} = {}\n", .{ s, v, s + v });
s += v; // aの各要素の値を加える
try stdout.print("s = {}\n", .{s}); // 1回の処理ごとの合計
}
// 結果
s = 0 // sの初期設定は0
0 + 3 = 3 // a[0]の値をsに加算
s = 3
3 + 5 = 8 // a[1]の値をsに加算
s = 8
8 + 2 = 10 // a[2]の値をsに加算
s = 10 // aの各要素の合計
for
では配列の次に添え字(要素の位置)用の変数を定義し、繰り返し処理中にそれを使用することもできます。
for(a, 0..) |v, i| { // v: aの各要素の値, i: 配列の添え字(要素の位置)
try stdout.print("a[{}] = {}\n", .{i, v});
}
// 結果
a[0] = 3
a[1] = 5
a[2] = 2
配列の演算子
配列に使われる演算子は++
と**
があります。++
は配列どうしの結合、**
は同じ構成の配列を複数繰り返す結合を表します。これらの両辺はどちらもコンパイル時点で内容が確定していなくてはなりません。
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const a = [_]u8 { 3, 5, 2 };
const b = [_]u8 { 8, 6 };
const c = a ++ b; // a と bを結合 - { 3, 5, 2 } ++ { 8, 6 }
const d = c ** 2; // cの内容 { 3, 5, 2, 8, 6 } を2つ繰り返す
try stdout.print("a = {any}\n", .{a}); // {any}で配列の全要素を出力
try stdout.print("b = {any}\n", .{b});
try stdout.print("c = {any}\n", .{c});
try stdout.print("d = {any}\n", .{d});
}
// 結果
a = { 3, 5, 2 }
b = { 8, 6 }
c = { 3, 5, 2, 8, 6 }
d = { 3, 5, 2, 8, 6, 3, 5, 2, 8, 6 }
配列の次元
配列の要素が単純な値のみのものを1次元配列といいます。配列の要素がさらに配列になっているものを2次元配列といいます。ここでは要素となる配列を「内側の配列」といい、それを持つ配列を「外側の配列」ということにします。
2次元配列の型名は[外側の配列の長さ][内側の配列の長さ]要素の型名
となります。内側の配列はすべて要素数が同じでなくてはなりません。要素数が確定しない場合は[][]u8
などとすることもあります。
配列の各要素はa[外側の配列での要素の位置][内側の配列での要素の位置]
とします。
const a = [2][3]u8{ // 外側の配列
[_]u8{ 3, 5, 2 }, // 内側の配列1
[_]u8{ 6, 8, 4 } // 内側の配列2
};
for (a, 0..) |row, i| {
for (row, 0..) |cell, j| {
print("a[{}][{}] = {}\n", .{ i, j, cell });
}
}
// 結果
a[0][0] = 3
a[0][1] = 5
a[0][2] = 2
a[1][0] = 6
a[1][1] = 8
a[1][2] = 4
内側の配列の要素がさらに配列となるものは3次元配列となり、それ以上の次元も定義できます。こうした2次元以上の配列は多次元配列ということもあります。
配列と文字列
ZigではソースコードをUTF-8で記述することになっています[1]。UTF-8は1~複数バイトの値で1文字を表します。文字列はそれらが複数並ぶものであるため、文字列はu8
の配列で表されます。
const lang = [_]u8 { 'Z', 'i', 'g' }; // '文字'は文字コード(1バイトのみ対応)
try stdout.print("{s}\n", .{ lang }); // "Zig"と出力される
とはいえ、文字列を毎回1文字ずつ分解して定義するのは面倒です。通常は"Zig"
のようにするでしょう。ただ、このときの型はちょっと特殊になります。
const lang1 = [_]u8{'Z', 'i', 'g'};
const lang2 = "Zig".*;
const lang3 = "Zig";
const lang4: [:0]const u8 = lang3;
// 変数の値、文字列の長さ、変数の型を出力
try stdout.print("lang1: {s}, {}, {}\n", .{ lang1, lang1.len, @TypeOf(lang1) });
try stdout.print("lang2: {s}, {}, {}\n", .{ lang2, lang2.len, @TypeOf(lang2) });
try stdout.print("lang3: {s}, {}, {}\n", .{ lang3, lang3.len, @TypeOf(lang3) });
try stdout.print("lang4: {s}, {}, {}\n", .{ lang4, lang4.len, @TypeOf(lang4) });
// 結果
lang1: Zig, 3, [3]u8
lang2: Zig, 3, [3:0]u8
lang3: Zig, 3, *const [3:0]u8
lang4: Zig, 3, [:0]const u8
lang1
の型は[3]u8
ですが、lang2
からlang4
まではこれと異なる型になっています。len
で表される文字列の長さはいずれも3
になっています。
lang2
の型である[3:0]u8
は末尾にデータの終端を表す0
という値が1バイト追加されたu8
配列のことです。この1バイトは番兵(Sentinel)といいます。文字列の長さlang2.len
としては現れませんが、lang2[3]
の値は0
に設定されます。これはC言語と同じデータの並びです。
そしてlang3
の型である*const [3:0]u8
は[3:0]u8
が記憶されている場所を表すメモリアドレスが格納されたポインタ(Pointer)を表しています。const
は固定値を意味します。文字列をはじめとしたバイトデータは容量が大きくなる可能性があるため、プログラム内でデータのやり取りをする際、データそのものではなく容量が少ないポインタで扱うことがよくあります。
最後のlang4
の型は[:0]const u8
です。これは末尾に0が追加されたデータの先頭位置を表すポインタとデータの長さを持つスライス(Slice)というものです。これはデータの一部分を取得するときによく使われます。const
はやはり固定値ということです。
このように、文字列はu8
の配列で格納されるものの、実際に扱う場合はさまざまな型を利用できるため、用途に合わせて使い分けるようにします。
配列の初期設定(要素の値がすべて0)
配列の要素をすべて0に初期設定するときはstd.mem.zeroes(型名)
を利用できます。
// 配列の初期設定(要素がすべて0)
var a = std.mem.zeroes([3:0]u8);
print("a = {any}, a.len = {}, @TypeOf(a) = {}\n", .{ a, a.len, @TypeOf(a) });
a[0] = 'Z';
a[1] = 'i';
a[2] = 'g';
print("a = {s}, a[3] = {}\n", .{ a, a[3] });
// 結果
a = { 0, 0, 0 }, a.len = 3, @TypeOf(a) = [3:0]u8
a = Zig, a[3] = 0
.{...}による代入
これまで配列を変数に代入するときはconst a = [_]u8{...}
など右辺で要素の型を示していました。しかし、左辺で型を示すときはconst a: [3]u8 = .{1, 2, 3};
のように右辺を.{...}
として代入することもできます。ただしこの場合は必ず配列の長さを示さなくてはなりません。
const ca: [3]u8 = .{ 1, 2, 3 };
var va: [3]u8 = undefined;
va = .{ 1, 2, 3 };
タプル
タプル(Tuple)は見た目は配列に似ていますが、要素の型を問わずさまざまな値をひとまとめに扱えます。代入は.{...}
で行いますが、変数の型が明示されません。
const tuple1 = .{ 1234, 12.24, "abcd", false };
そしてそれぞれの要素はtuple1[0], tuple1[1], ...
で表します。
try stdout.print("tuple1[0] = {}\n", .{tuple1[0]});
try stdout.print("tuple1[1] = {d}\n", .{tuple1[1]});
try stdout.print("tuple1[2] = {s}\n", .{tuple1[2]});
try stdout.print("tuple1[3] = {}\n", .{tuple1[3]});
tuple1[0] = 1234
tuple1[1] = 12.24
tuple1[2] = abcd
tuple1[3] = false
要素の指定にはtuple1.@"0", tuple2.@"1", ...
という方法もあります。
try stdout.print("tuple1.@\"0\" = {}\n", .{tuple1.@"0"});
try stdout.print("tuple1.@\"1\" = {d}\n", .{tuple1.@"1"});
try stdout.print("tuple1.@\"2\" = {s}\n", .{tuple1.@"2"});
try stdout.print("tuple1.@\"3\" = {}\n", .{tuple1.@"3"});
tuple1.@"0" = 1234
tuple1.@"1" = 12.24
tuple1.@"2" = abcd
tuple1.@"3" = false
++
や**
といった演算子も対応しています。
const tuple1 = ...; // 上記に同じ
const tuple2 = .{ "efgh", true, 5678, 56.78 };
const tuple12 = tuple1 ++ tuple2; // .{ 1234, 12.24, "abcd", false, "efgh", true, 5678, 56.78 }
const tuple3 = .{ "ijkl", 0x9abc } ** 3; // .{ "ijkl", 0x9abc, "ijkl", 0x9abc, "ijkl", 0x9abc }
まとめ
-
配列(Array)は同じ型の要素を複数格納し、型名は
[要素の数]要素の型
となる -
配列
a
の要素はa[0], a[1], ...
のように表し、配列の長さはa.len
とする -
配列とforを組み合わせると各要素の値を利用した繰り返し処理を実行できる
-
++
は配列の結合、**
は配列の同じ内容を複数繰り返す -
要素が単純な値のみのものを1次元配列といい、要素が配列になっているものを2次元配列という
型名は[外側の配列の長さ][内側の配列の長さ]要素の型名
となり、配列の各要素はa[外側の配列での要素の位置][内側の配列での要素の位置]
で表す
さらにその要素も配列の場合は3次元となり、それ以上の次元も定義可能
2次元以上の配列を多次元配列という -
文字列は
u8
の配列で表されるが、番兵(Sentinel)つき配列、ポインタ(Pointer)、スライス(Slice)といった型で扱うこともできる -
配列の初期設定は
std.mem.zeroes(型名)
でできる -
配列の代入は、左辺で型が明示したとき
.{...}
で代入できる
ただし配列の長さを明示されていなくてはならない -
タプル(Tuple)は配列と同様
.{...}
で代入できるが要素の型は揃っていなくてもよい -
タプルの要素は
tuple[0], tuple[1], ...
やtuple.@"0", tuple.@"1", ...
で表す -
タプルは
++
や**
に対応している
共用型(union)と列挙型(enum) >
< 繰り返しの応用 (for, while, else, break, continue, オプション, ラベル)
ざっくりZig 一覧
-
公式リファレンス Source Encoding ↩︎
Discussion