第10章: 構造体と列挙型

学習目標

この章を終えると、以下ができるようになります:

  • 構造体を定義し、メソッドを実装できる
  • パック構造体でメモリ効率を向上できる
  • 列挙型とタグ付きユニオンを使ってデータをモデリングできる
  • ジェネリック構造体を作成できる
  • 構造体の基本

    構造体の定義

    構造体は、関連するデータをまとめるための基本的なデータ構造です。

    const std = @import("std");
    
    const Point = struct {
        x: f32,
        y: f32,
    };
    
    const Rectangle = struct {
        top_left: Point,
        width: f32,
        height: f32,
    };
    
    pub fn example() void {
        const point = Point{ .x = 10.0, .y = 20.0 };
        std.debug.print("Point: ({d:.1}, {d:.1})\n", .{point.x, point.y});
    
        const rect = Rectangle{
            .top_left = Point{ .x = 0.0, .y = 0.0 },
            .width = 100.0,
            .height = 50.0,
        };
        std.debug.print("Rectangle: ({d:.1}, {d:.1}), {d:.1}x{d:.1}\n",
            .{rect.top_left.x, rect.top_left.y, rect.width, rect.height});
    }
    

    メソッドの定義

    構造体にメソッド(関数)を追加できます。

    const std = @import("std");
    
    const Circle = struct {
        center: Point,
        radius: f32,
    
        // メソッド: 面積を計算
        pub fn area(self: Circle) f32 {
            return std.math.pi * self.radius * self.radius;
        }
    
        // メソッド: 円周を計算
        pub fn circumference(self: Circle) f32 {
            return 2.0 * std.math.pi * self.radius;
        }
    
        // メソッド: 点が円の内部にあるか判定
        pub fn contains(self: Circle, point: Point) bool {
            const dx = point.x - self.center.x;
            const dy = point.y - self.center.y;
            const distance = std.math.sqrt(dx * dx + dy * dy);
            return distance <= self.radius;
        }
    };
    
    const Point = struct {
        x: f32,
        y: f32,
    };
    
    pub fn example() void {
        const circle = Circle{
            .center = Point{ .x = 0.0, .y = 0.0 },
            .radius = 10.0,
        };
    
        std.debug.print("Area: {d:.2}\n", .{circle.area()});
        std.debug.print("Circumference: {d:.2}\n", .{circle.circumference()});
    
        const p1 = Point{ .x = 5.0, .y = 5.0 };
        const p2 = Point{ .x = 15.0, .y = 15.0 };
    
        std.debug.print("Point (5, 5) inside: {}\n", .{circle.contains(p1)});
        std.debug.print("Point (15, 15) inside: {}\n", .{circle.contains(p2)});
    }
    

    デフォルト値

    構造体のフィールドにデフォルト値を設定できます。

    const std = @import("std");
    
    const Config = struct {
        host: []const u8 = "localhost",
        port: u16 = 8080,
        timeout_ms: u32 = 5000,
        debug: bool = false,
    };
    
    pub fn example() void {
        // すべてデフォルト値
        const config1 = Config{};
        std.debug.print("Config1: {s}:{}, timeout={}, debug={}\n",
            .{config1.host, config1.port, config1.timeout_ms, config1.debug});
    
        // 一部を上書き
        const config2 = Config{
            .port = 3000,
            .debug = true,
        };
        std.debug.print("Config2: {s}:{}, timeout={}, debug={}\n",
            .{config2.host, config2.port, config2.timeout_ms, config2.debug});
    }
    

    コンストラクタパターン

    構造体の初期化を簡潔にするコンストラクタ関数を定義できます。

    const std = @import("std");
    
    const User = struct {
        id: u32,
        name: []const u8,
        email: []const u8,
        active: bool,
    
        // コンストラクタ
        pub fn init(id: u32, name: []const u8, email: []const u8) User {
            return User{
                .id = id,
                .name = name,
                .email = email,
                .active = true,
            };
        }
    
        // ビルダーパターン
        pub fn setActive(self: User, active: bool) User {
            var new_user = self;
            new_user.active = active;
            return new_user;
        }
    
        pub fn print(self: User) void {
            std.debug.print("User #{}: {s} <{s}> [{}]\n",
                .{self.id, self.name, self.email, if (self.active) "active" else "inactive"});
        }
    };
    
    pub fn example() void {
        const user1 = User.init(1, "Alice", "alice@example.com");
        user1.print();
    
        const user2 = user1.setActive(false);
        user2.print();
    }
    

    パック構造体

    基本的なパック構造体

    パック構造体は、メモリレイアウトが保証された構造体です。

    const std = @import("std");
    
    const Flags = packed struct {
        read: bool,
        write: bool,
        execute: bool,
        reserved: u5,
    };
    
    pub fn example() void {
        const flags = Flags{
            .read = true,
            .write = true,
            .execute = false,
            .reserved = 0,
        };
    
        std.debug.print("Size: {} bytes\n", .{@sizeOf(Flags)});
        std.debug.print("Read: {}, Write: {}, Execute: {}\n",
            .{flags.read, flags.write, flags.execute});
    
        // ビットパターンとして扱える
        const bits: u8 = @bitCast(flags);
        std.debug.print("Bit pattern: 0b{b:0>8}\n", .{bits});
    }
    

    ビットフィールドの活用

    const std = @import("std");
    
    const Color = packed struct {
        r: u8,
        g: u8,
        b: u8,
        a: u8,
    
        pub fn init(r: u8, g: u8, b: u8, a: u8) Color {
            return Color{ .r = r, .g = g, .b = b, .a = a };
        }
    
        pub fn toU32(self: Color) u32 {
            return @bitCast(self);
        }
    
        pub fn fromU32(value: u32) Color {
            return @bitCast(value);
        }
    };
    
    pub fn example() void {
        const red = Color.init(255, 0, 0, 255);
        std.debug.print("Red: R={}, G={}, B={}, A={}\n",
            .{red.r, red.g, red.b, red.a});
    
        const value = red.toU32();
        std.debug.print("As u32: 0x{x:0>8}\n", .{value});
    
        const restored = Color.fromU32(value);
        std.debug.print("Restored: R={}, G={}, B={}, A={}\n",
            .{restored.r, restored.g, restored.b, restored.a});
    }
    

    ハードウェアレジスタのモデリング

    const std = @import("std");
    
    const StatusRegister = packed struct {
        carry: u1,
        zero: u1,
        interrupt_disable: u1,
        decimal_mode: u1,
        break_command: u1,
        unused: u1,
        overflow: u1,
        negative: u1,
    
        pub fn init() StatusRegister {
            return @bitCast(@as(u8, 0x00));
        }
    
        pub fn setFlag(self: *StatusRegister, flag: Flag, value: bool) void {
            switch (flag) {
                .Carry => self.carry = @intFromBool(value),
                .Zero => self.zero = @intFromBool(value),
                .Overflow => self.overflow = @intFromBool(value),
                .Negative => self.negative = @intFromBool(value),
            }
        }
    
        pub fn getFlag(self: StatusRegister, flag: Flag) bool {
            return switch (flag) {
                .Carry => self.carry == 1,
                .Zero => self.zero == 1,
                .Overflow => self.overflow == 1,
                .Negative => self.negative == 1,
            };
        }
    };
    
    const Flag = enum {
        Carry,
        Zero,
        Overflow,
        Negative,
    };
    
    pub fn example() void {
        var status = StatusRegister.init();
    
        status.setFlag(.Zero, true);
        status.setFlag(.Carry, true);
    
        std.debug.print("Zero flag: {}\n", .{status.getFlag(.Zero)});
        std.debug.print("Carry flag: {}\n", .{status.getFlag(.Carry)});
        std.debug.print("Negative flag: {}\n", .{status.getFlag(.Negative)});
    }
    

    列挙型

    基本的な列挙型

    列挙型は、関連する定数の集合を定義します。

    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 rotate90(self: Direction) Direction {
            return switch (self) {
                .North => .East,
                .East => .South,
                .South => .West,
                .West => .North,
            };
        }
    };
    
    pub fn example() void {
        const dir = Direction.North;
        std.debug.print("Direction: {}\n", .{dir});
        std.debug.print("Opposite: {}\n", .{dir.opposite()});
        std.debug.print("Rotate 90°: {}\n", .{dir.rotate90()});
    }
    

    列挙型に値を付与

    const std = @import("std");
    
    const HttpStatus = enum(u16) {
        Ok = 200,
        Created = 201,
        NoContent = 204,
        BadRequest = 400,
        Unauthorized = 401,
        Forbidden = 403,
        NotFound = 404,
        InternalServerError = 500,
    
        pub fn isSuccess(self: HttpStatus) bool {
            return @intFromEnum(self) >= 200 and @intFromEnum(self) < 300;
        }
    
        pub fn isError(self: HttpStatus) bool {
            return @intFromEnum(self) >= 400;
        }
    };
    
    pub fn example() void {
        const status1 = HttpStatus.Ok;
        const status2 = HttpStatus.NotFound;
    
        std.debug.print("Status {}: success={}, error={}\n",
            .{@intFromEnum(status1), status1.isSuccess(), status1.isError()});
        std.debug.print("Status {}: success={}, error={}\n",
            .{@intFromEnum(status2), status2.isSuccess(), status2.isError()});
    }
    

    列挙型のイテレーション

    const std = @import("std");
    
    const Color = enum {
        Red,
        Green,
        Blue,
        Yellow,
        Magenta,
        Cyan,
    
        pub fn toRGB(self: Color) [3]u8 {
            return switch (self) {
                .Red => [3]u8{ 255, 0, 0 },
                .Green => [3]u8{ 0, 255, 0 },
                .Blue => [3]u8{ 0, 0, 255 },
                .Yellow => [3]u8{ 255, 255, 0 },
                .Magenta => [3]u8{ 255, 0, 255 },
                .Cyan => [3]u8{ 0, 255, 255 },
            };
        }
    };
    
    pub fn example() void {
        // すべての列挙値を取得
        const colors = std.meta.fields(Color);
    
        inline for (colors) |color_info| {
            const color = @field(Color, color_info.name);
            const rgb = color.toRGB();
            std.debug.print("{s}: RGB({}, {}, {})\n",
                .{color_info.name, rgb[0], rgb[1], rgb[2]});
        }
    }
    

    タグ付きユニオン

    基本的なタグ付きユニオン

    タグ付きユニオンは、複数の型のうち1つを保持できる型です。

    const std = @import("std");
    
    const Value = union(enum) {
        integer: i32,
        float: f64,
        boolean: bool,
        string: []const u8,
    
        pub fn print(self: Value) void {
            switch (self) {
                .integer => |val| std.debug.print("Integer: {}\n", .{val}),
                .float => |val| std.debug.print("Float: {d:.2}\n", .{val}),
                .boolean => |val| std.debug.print("Boolean: {}\n", .{val}),
                .string => |val| std.debug.print("String: {s}\n", .{val}),
            }
        }
    };
    
    pub fn example() void {
        const values = [_]Value{
            Value{ .integer = 42 },
            Value{ .float = 3.14 },
            Value{ .boolean = true },
            Value{ .string = "Hello" },
        };
    
        for (values) |val| {
            val.print();
        }
    }
    

    ペイロード付きタグ付きユニオン

    const std = @import("std");
    
    const Message = union(enum) {
        Quit,
        Move: struct { x: i32, y: i32 },
        Write: []const u8,
        ChangeColor: 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}),
                .ChangeColor => |color| std.debug.print(
                    "Changing color to RGB({}, {}, {})\n",
                    .{color.r, color.g, color.b}
                ),
            }
        }
    };
    
    pub fn example() void {
        const messages = [_]Message{
            Message.Quit,
            Message{ .Move = .{ .x = 10, .y = 20 } },
            Message{ .Write = "Hello, World!" },
            Message{ .ChangeColor = .{ .r = 255, .g = 0, .b = 0 } },
        };
    
        for (messages) |msg| {
            msg.handle();
        }
    }
    

    Result型のモデリング

    const std = @import("std");
    
    fn Result(comptime T: type, comptime E: type) type {
        return union(enum) {
            Ok: T,
            Err: E,
    
            pub fn isOk(self: @This()) bool {
                return self == .Ok;
            }
    
            pub fn isErr(self: @This()) bool {
                return self == .Err;
            }
    
            pub fn unwrap(self: @This()) T {
                return switch (self) {
                    .Ok => |val| val,
                    .Err => |err| {
                        std.debug.print("Called unwrap on Err: {}\n", .{err});
                        @panic("unwrap on Err");
                    },
                };
            }
    
            pub fn unwrapOr(self: @This(), default: T) T {
                return switch (self) {
                    .Ok => |val| val,
                    .Err => default,
                };
            }
        };
    }
    
    const ParseError = error{
        InvalidFormat,
        OutOfRange,
    };
    
    fn parseInt(str: []const u8) Result(i32, ParseError) {
        _ = str;
        // 簡略化のため固定値を返す
        return Result(i32, ParseError){ .Ok = 42 };
    }
    
    pub fn example() void {
        const result1 = parseInt("123");
        if (result1.isOk()) {
            std.debug.print("Parsed: {}\n", .{result1.unwrap()});
        }
    
        const result2 = Result(i32, ParseError){ .Err = ParseError.InvalidFormat };
        const value = result2.unwrapOr(0);
        std.debug.print("Default value: {}\n", .{value});
    }
    

    ジェネリック構造体

    基本的なジェネリック構造体

    const std = @import("std");
    
    fn Stack(comptime T: type) type {
        return struct {
            items: []T,
            len: usize,
            allocator: std.mem.Allocator,
    
            const Self = @This();
    
            pub fn init(allocator: std.mem.Allocator, capacity: usize) !Self {
                const items = try allocator.alloc(T, capacity);
                return Self{
                    .items = items,
                    .len = 0,
                    .allocator = allocator,
                };
            }
    
            pub fn deinit(self: *Self) void {
                self.allocator.free(self.items);
            }
    
            pub fn push(self: *Self, item: T) !void {
                if (self.len >= self.items.len) {
                    return error.StackOverflow;
                }
                self.items[self.len] = item;
                self.len += 1;
            }
    
            pub fn pop(self: *Self) ?T {
                if (self.len == 0) {
                    return null;
                }
                self.len -= 1;
                return self.items[self.len];
            }
    
            pub fn peek(self: *Self) ?T {
                if (self.len == 0) {
                    return null;
                }
                return self.items[self.len - 1];
            }
        };
    }
    
    pub fn example() !void {
        const allocator = std.heap.page_allocator;
    
        var int_stack = try Stack(i32).init(allocator, 10);
        defer int_stack.deinit();
    
        try int_stack.push(1);
        try int_stack.push(2);
        try int_stack.push(3);
    
        std.debug.print("Pop: {?}\n", .{int_stack.pop()});
        std.debug.print("Peek: {?}\n", .{int_stack.peek()});
        std.debug.print("Pop: {?}\n", .{int_stack.pop()});
    }
    

    ジェネリックなOption型

    const std = @import("std");
    
    fn Option(comptime T: type) type {
        return union(enum) {
            Some: T,
            None,
    
            const Self = @This();
    
            pub fn isSome(self: Self) bool {
                return self == .Some;
            }
    
            pub fn isNone(self: Self) bool {
                return self == .None;
            }
    
            pub fn unwrap(self: Self) T {
                return switch (self) {
                    .Some => |val| val,
                    .None => @panic("Called unwrap on None"),
                };
            }
    
            pub fn unwrapOr(self: Self, default: T) T {
                return switch (self) {
                    .Some => |val| val,
                    .None => default,
                };
            }
    
            pub fn map(self: Self, comptime R: type, func: fn(T) R) Option(R) {
                return switch (self) {
                    .Some => |val| Option(R){ .Some = func(val) },
                    .None => Option(R).None,
                };
            }
        };
    }
    
    pub fn example() void {
        const opt1 = Option(i32){ .Some = 42 };
        const opt2 = Option(i32).None;
    
        std.debug.print("opt1 is some: {}\n", .{opt1.isSome()});
        std.debug.print("opt2 is none: {}\n", .{opt2.isNone()});
    
        const value1 = opt1.unwrapOr(0);
        const value2 = opt2.unwrapOr(0);
    
        std.debug.print("opt1 value: {}\n", .{value1});
        std.debug.print("opt2 value: {}\n", .{value2});
    
        // map関数の使用
        const double = struct {
            fn func(x: i32) i32 {
                return x * 2;
            }
        }.func;
    
        const opt3 = opt1.map(i32, double);
        std.debug.print("Mapped value: {}\n", .{opt3.unwrap()});
    }
    

    まとめ

    この章では、構造体と列挙型について学びました:

  • 構造体: データとメソッドのカプセル化
  • パック構造体: メモリレイアウトの制御
  • 列挙型: 関連する定数の定義
  • タグ付きユニオン: 複数の型を統一的に扱う
  • ジェネリック構造体: 型パラメータを使った汎用的な実装
  • 次の章では、コンパイル時計算(comptime)について学びます。

    参考資料

  • Zig Language Reference - Structs: https://ziglang.org/documentation/master/#struct
  • Zig Language Reference - Enums: https://ziglang.org/documentation/master/#enum
  • Zig Language Reference - Unions: https://ziglang.org/documentation/master/#union
  • Ziglearn - Advanced Data Structures: https://ziglearn.org/chapter-2/