ざっくりZig - 配列とタプル

2024/04/04に公開

配列

配列(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[外側の配列での要素の位置][内側の配列での要素の位置]とします。

2次元配列の例
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の配列で表されます。

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 一覧


脚注
  1. 公式リファレンス Source Encoding ↩︎

Discussion