解答3: 基本構文と変数の理解
概要
この解答では、Zigの基本構文、変数宣言、プリミティブ型、型変換、演算子について実践的に学びます。型システムを正しく理解することは、安全で効率的なZigプログラミングの基礎となります。
Part 1: 型の理解 - 解答
const std = @import("std");
pub fn main() void {
std.debug.print("=== Signed Integers ===\n", .{});
// 1. 符号付き整数(i8, i16, i32, i64)
const i8_min: i8 = -128;
const i8_max: i8 = 127;
std.debug.print("i8: {} to {}\n", .{i8_min, i8_max});
const i16_min: i16 = -32768;
const i16_max: i16 = 32767;
std.debug.print("i16: {} to {}\n", .{i16_min, i16_max});
const i32_min: i32 = -2147483648;
const i32_max: i32 = 2147483647;
std.debug.print("i32: {} to {}\n", .{i32_min, i32_max});
const i64_min: i64 = -9223372036854775808;
const i64_max: i64 = 9223372036854775807;
std.debug.print("i64: {} to {}\n", .{i64_min, i64_max});
std.debug.print("\n=== Unsigned Integers ===\n", .{});
// 2. 符号なし整数(u8, u16, u32, u64)
const u8_max: u8 = 255;
std.debug.print("u8: 0 to {}\n", .{u8_max});
const u16_max: u16 = 65535;
std.debug.print("u16: 0 to {}\n", .{u16_max});
const u32_max: u32 = 4294967295;
std.debug.print("u32: 0 to {}\n", .{u32_max});
const u64_max: u64 = 18446744073709551615;
std.debug.print("u64: 0 to {}\n", .{u64_max});
std.debug.print("\n=== Floating Point ===\n", .{});
// 3. 浮動小数点(f32, f64)
const pi_f32: f32 = 3.14159265;
const pi_f64: f64 = 3.14159265358979323846;
std.debug.print("pi (f32): {d:.2}\n", .{pi_f32});
std.debug.print("pi (f64): {d:.8}\n", .{pi_f64});
std.debug.print("\n=== Boolean ===\n", .{});
// 4. ブール型
const is_true: bool = true;
const is_false: bool = false;
std.debug.print("true and false = {}\n", .{is_true and is_false});
std.debug.print("true or false = {}\n", .{is_true or is_false});
std.debug.print("not true = {}\n", .{!is_true});
std.debug.print("\n=== Characters ===\n", .{});
// 5. 文字型
const ascii_char: u8 = 'A';
const unicode_char: u21 = 'あ';
const emoji: u21 = '😀';
std.debug.print("ASCII: {c} (code: {})\n", .{ascii_char, ascii_char});
std.debug.print("Unicode: {u} (code: {})\n", .{unicode_char, unicode_char});
std.debug.print("Emoji: {u} (code: {})\n", .{emoji, emoji});
}
実行結果
=== Signed Integers ===
i8: -128 to 127
i16: -32768 to 32767
i32: -2147483648 to 2147483647
i64: -9223372036854775808 to 9223372036854775807
=== Unsigned Integers ===
u8: 0 to 255
u16: 0 to 65535
u32: 0 to 4294967295
u64: 0 to 18446744073709551615
=== Floating Point ===
pi (f32): 3.14
pi (f64): 3.14159265
=== Boolean ===
true and false = false
true or false = true
not true = false
=== Characters ===
ASCII: A (code: 65)
Unicode: あ (code: 12354)
Emoji: 😀 (code: 128512)
ポイント解説
- 整数型の選択: 用途に応じて適切なサイズを選ぶ
- 浮動小数点の精度:
- 文字の扱い:
Part 2: 型変換 - 解答
const std = @import("std");
// 整数から浮動小数点への変換関数
fn intToFloat(value: i32) f64 {
return @floatFromInt(value);
}
// 浮動小数点から整数への変換関数(切り捨て)
fn floatToInt(value: f64) i32 {
return @intFromFloat(value);
}
// 小さい整数型から大きい整数型への変換
fn widenInt(value: i16) i32 {
return @intCast(value);
}
// 大きい整数型から小さい整数型への変換(安全性チェック付き)
fn narrowInt(value: i32) !i16 {
if (value > std.math.maxInt(i16) or value < std.math.minInt(i16)) {
return error.Overflow;
}
return @intCast(value);
}
pub fn main() !void {
std.debug.print("=== Type Conversion Tests ===\n", .{});
// テスト1: intToFloat
const int_val: i32 = 42;
const float_result = intToFloat(int_val);
std.debug.print("intToFloat({}) = {d:.1}\n", .{int_val, float_result});
// テスト2: floatToInt
const float_val: f64 = 3.14159;
const int_result = floatToInt(float_val);
std.debug.print("floatToInt({d:.5}) = {}\n", .{float_val, int_result});
// テスト3: widenInt
const small_int: i16 = 100;
const large_int = widenInt(small_int);
std.debug.print("widenInt({}) = {}\n", .{small_int, large_int});
// テスト4: narrowInt(成功ケース)
const valid_val: i32 = 100;
const narrow_result = try narrowInt(valid_val);
std.debug.print("narrowInt({}) = {}\n", .{valid_val, narrow_result});
// テスト5: narrowInt(エラーケース)
const invalid_val: i32 = 40000;
narrowInt(invalid_val) catch |err| {
std.debug.print("narrowInt({}) failed: {}\n", .{invalid_val, err});
};
}
実行結果
=== Type Conversion Tests ===
intToFloat(42) = 42.0
floatToInt(3.14159) = 3
widenInt(100) = 100
narrowInt(100) = 100
narrowInt(40000) failed: error.Overflow
型変換のベストプラクティス
- 明示的な変換: Zigは暗黙的な型変換を行わないため、常に明示的に変換する
- 範囲チェック: 小さい型への変換時は範囲チェックを実装
- 精度の損失: 浮動小数点から整数への変換では小数部が切り捨てられる
- 符号の扱い: 符号付きと符号なしの変換には特に注意
Part 3: 演算子の活用 - 解答
const std = @import("std");
// 基本的な算術演算を行う関数
fn arithmetic(a: i32, b: i32) void {
const add = a + b;
const sub = a - b;
const mul = a * b;
const div = @divTrunc(a, b);
const mod = @mod(a, b);
std.debug.print("Addition: {} + {} = {}\n", .{a, b, add});
std.debug.print("Subtraction: {} - {} = {}\n", .{a, b, sub});
std.debug.print("Multiplication: {} * {} = {}\n", .{a, b, mul});
std.debug.print("Division: {} / {} = {}\n", .{a, b, div});
std.debug.print("Modulo: {} % {} = {}\n", .{a, b, mod});
}
// ビット演算を行う関数
fn bitwise(a: u8, b: u8) void {
const and_result = a & b;
const or_result = a | b;
const xor_result = a ^ b;
const not_result = ~a;
const left_shift = a << 2;
const right_shift = a >> 2;
std.debug.print("a = 0b{b:08} ({})\n", .{a, a});
std.debug.print("b = 0b{b:08} ({})\n", .{b, b});
std.debug.print("AND: 0b{b:08} ({})\n", .{and_result, and_result});
std.debug.print("OR: 0b{b:08} ({})\n", .{or_result, or_result});
std.debug.print("XOR: 0b{b:08} ({})\n", .{xor_result, xor_result});
std.debug.print("NOT: 0b{b:08} ({})\n", .{not_result, not_result});
std.debug.print("Left shift (<<2): 0b{b:08} ({})\n", .{left_shift, left_shift});
std.debug.print("Right shift (>>2): 0b{b:08} ({})\n", .{right_shift, right_shift});
}
// 比較演算を行う関数
fn comparison(a: i32, b: i32) void {
std.debug.print("{} == {} : {}\n", .{a, b, a == b});
std.debug.print("{} != {} : {}\n", .{a, b, a != b});
std.debug.print("{} < {} : {}\n", .{a, b, a < b});
std.debug.print("{} <= {} : {}\n", .{a, b, a <= b});
std.debug.print("{} > {} : {}\n", .{a, b, a > b});
std.debug.print("{} >= {} : {}\n", .{a, b, a >= b});
}
// オーバーフロー対策の演算
fn overflowSafe() void {
var x: u8 = 255;
// ラップアラウンド加算
x +%= 1;
std.debug.print("Wrapping: 255 +%%= 1 = {}\n", .{x});
// 飽和加算
var y: u8 = 255;
y +|= 1;
std.debug.print("Saturating: 255 +|= 1 = {}\n", .{y});
// ラップアラウンド減算
var z: u8 = 0;
z -%= 1;
std.debug.print("Wrapping: 0 -%%= 1 = {}\n", .{z});
// 飽和減算
var w: u8 = 0;
w -|= 1;
std.debug.print("Saturating: 0 -|= 1 = {}\n", .{w});
}
pub fn main() void {
std.debug.print("=== Arithmetic Operations ===\n", .{});
arithmetic(10, 3);
std.debug.print("\n=== Bitwise Operations ===\n", .{});
bitwise(0b1100, 0b1010);
std.debug.print("\n=== Comparison Operations ===\n", .{});
comparison(10, 20);
std.debug.print("\n=== Overflow-Safe Operations ===\n", .{});
overflowSafe();
}
実行結果
=== Arithmetic Operations ===
Addition: 10 + 3 = 13
Subtraction: 10 - 3 = 7
Multiplication: 10 * 3 = 30
Division: 10 / 3 = 3
Modulo: 10 % 3 = 1
=== Bitwise Operations ===
a = 0b00001100 (12)
b = 0b00001010 (10)
AND: 0b00001000 (8)
OR: 0b00001110 (14)
XOR: 0b00000110 (6)
NOT: 0b11110011 (243)
Left shift (<<2): 0b00110000 (48)
Right shift (>>2): 0b00000011 (3)
=== Comparison Operations ===
10 == 20 : false
10 != 20 : true
10 < 20 : true
10 <= 20 : true
10 > 20 : false
10 >= 20 : false
=== Overflow-Safe Operations ===
Wrapping: 255 +%= 1 = 0
Saturating: 255 +|= 1 = 255
Wrapping: 0 -%= 1 = 255
Saturating: 0 -|= 1 = 0
演算子の種類と用途
+, -, *: 基本的な演算
- @divTrunc: 切り捨て除算
- @mod: 剰余演算- ビット演算子:
&: AND(両方のビットが1の場合に1)
- |: OR(どちらかのビットが1の場合に1)
- ^: XOR(ビットが異なる場合に1)
- ~: NOT(ビットを反転)
- <<, >>: シフト演算- オーバーフロー対策演算子:
+%: ラップアラウンド加算
- +|: 飽和加算
- -%: ラップアラウンド減算
- -|: 飽和減算Part 4: 実践プログラム - 解答
const std = @import("std");
// 摂氏から華氏への変換
fn celsiusToFahrenheit(celsius: f64) f64 {
return celsius * 9.0 / 5.0 + 32.0;
}
// 華氏から摂氏への変換
fn fahrenheitToCelsius(fahrenheit: f64) f64 {
return (fahrenheit - 32.0) * 5.0 / 9.0;
}
// 摂氏からケルビンへの変換
fn celsiusToKelvin(celsius: f64) f64 {
return celsius + 273.15;
}
// ケルビンから摂氏への変換
fn kelvinToCelsius(kelvin: f64) f64 {
return kelvin - 273.15;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: {s} <temperature> <unit>\n", .{args[0]});
std.debug.print("Units: C (Celsius), F (Fahrenheit), K (Kelvin)\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print(" {s} 100 C\n", .{args[0]});
std.debug.print(" {s} 32 F\n", .{args[0]});
std.debug.print(" {s} 273.15 K\n", .{args[0]});
return;
}
const temp = try std.fmt.parseFloat(f64, args[1]);
const unit = args[2][0];
switch (unit) {
'C', 'c' => {
const f = celsiusToFahrenheit(temp);
const k = celsiusToKelvin(temp);
std.debug.print("{d:.2}°C = {d:.2}°F = {d:.2}K\n", .{temp, f, k});
},
'F', 'f' => {
const c = fahrenheitToCelsius(temp);
const k = celsiusToKelvin(c);
std.debug.print("{d:.2}°F = {d:.2}°C = {d:.2}K\n", .{temp, c, k});
},
'K', 'k' => {
const c = kelvinToCelsius(temp);
const f = celsiusToFahrenheit(c);
std.debug.print("{d:.2}K = {d:.2}°C = {d:.2}°F\n", .{temp, c, f});
},
else => {
std.debug.print("Unknown unit: {c}\n", .{unit});
std.debug.print("Supported units: C, F, K\n", .{});
return error.InvalidUnit;
},
}
}
実行例
$ zig run temperature.zig -- 100 C
100.00°C = 212.00°F = 373.15K
$ zig run temperature.zig -- 32 F
32.00°F = 0.00°C = 273.15K
$ zig run temperature.zig -- 273.15 K
273.15K = 0.00°C = 32.00°F
$ zig run temperature.zig -- 0 C
0.00°C = 32.00°F = 273.15K
温度変換の公式
- 摂氏 ↔ 華氏:
F = C × 9/5 + 32
- F → C: C = (F - 32) × 5/9- 摂氏 ↔ ケルビン:
K = C + 273.15
- K → C: C = K - 273.15ボーナス課題 - 解答
Bonus 1: 任意ビット幅整数
const std = @import("std");
pub fn main() void {
std.debug.print("=== Arbitrary Bit Width Integers ===\n", .{});
// 3ビット符号付き整数(-4 to 3)
const i3_max: i3 = 3;
const i3_min: i3 = -4;
std.debug.print("i3 range: {} to {}\n", .{i3_min, i3_max});
// 5ビット符号なし整数(0 to 31)
const u5_max: u5 = 31;
std.debug.print("u5 range: 0 to {}\n", .{u5_max});
// 7ビット符号なし整数
const u7_max: u7 = 127;
std.debug.print("u7 range: 0 to {}\n", .{u7_max});
std.debug.print("\n=== Bit Flags Example ===\n", .{});
// ビットフラグとして使用
const READ: u4 = 0b0001;
const WRITE: u4 = 0b0010;
const EXECUTE: u4 = 0b0100;
const ADMIN: u4 = 0b1000;
var permissions: u4 = READ | WRITE;
std.debug.print("Initial permissions: 0b{b:04}\n", .{permissions});
// 実行権限を追加
permissions |= EXECUTE;
std.debug.print("After adding EXECUTE: 0b{b:04}\n", .{permissions});
// 権限チェック
const has_read = (permissions & READ) != 0;
const has_write = (permissions & WRITE) != 0;
const has_execute = (permissions & EXECUTE) != 0;
const has_admin = (permissions & ADMIN) != 0;
std.debug.print("Permissions:\n", .{});
std.debug.print(" READ: {}\n", .{has_read});
std.debug.print(" WRITE: {}\n", .{has_write});
std.debug.print(" EXECUTE: {}\n", .{has_execute});
std.debug.print(" ADMIN: {}\n", .{has_admin});
}
Bonus 2: comptime型推論
const std = @import("std");
// 任意の数値型の最大値を返すコンパイル時関数
fn maxValue(comptime T: type) T {
const type_info = @typeInfo(T);
return switch (type_info) {
.Int => |int_info| blk: {
if (int_info.signedness == .unsigned) {
// 符号なし整数の最大値: 2^bits - 1
break :blk (@as(T, 1) << @intCast(int_info.bits)) -% 1;
} else {
// 符号付き整数の最大値: 2^(bits-1) - 1
break :blk (@as(T, 1) << @intCast(int_info.bits - 1)) -% 1;
}
},
else => @compileError("maxValue は整数型のみサポートします"),
};
}
// 任意の数値型の最小値を返すコンパイル時関数
fn minValue(comptime T: type) T {
const type_info = @typeInfo(T);
return switch (type_info) {
.Int => |int_info| blk: {
if (int_info.signedness == .unsigned) {
// 符号なし整数の最小値: 0
break :blk 0;
} else {
// 符号付き整数の最小値: -2^(bits-1)
break :blk -(@as(T, 1) << @intCast(int_info.bits - 1));
}
},
else => @compileError("minValue は整数型のみサポートします"),
};
}
pub fn main() void {
// テスト
const u8_max = maxValue(u8);
const i8_max = maxValue(i8);
const i8_min = minValue(i8);
std.debug.print("u8 max: {}\n", .{u8_max});
std.debug.print("i8 range: {} to {}\n", .{i8_min, i8_max});
// さまざまな型でテスト
const u16_max = maxValue(u16);
const i32_min = minValue(i32);
const i32_max = maxValue(i32);
std.debug.print("u16 max: {}\n", .{u16_max});
std.debug.print("i32 range: {} to {}\n", .{i32_min, i32_max});
// 任意ビット幅でもテスト
const u5_max = maxValue(u5);
const i3_min = minValue(i3);
const i3_max = maxValue(i3);
std.debug.print("u5 max: {}\n", .{u5_max});
std.debug.print("i3 range: {} to {}\n", .{i3_min, i3_max});
}
Bonus 3: ビット操作ユーティリティ
const std = @import("std");
// nビット目を設定
fn setBit(value: u32, bit: u5) u32 {
return value | (@as(u32, 1) << bit);
}
// nビット目をクリア
fn clearBit(value: u32, bit: u5) u32 {
return value & ~(@as(u32, 1) << bit);
}
// nビット目をトグル
fn toggleBit(value: u32, bit: u5) u32 {
return value ^ (@as(u32, 1) << bit);
}
// nビット目をチェック
fn checkBit(value: u32, bit: u5) bool {
return (value & (@as(u32, 1) << bit)) != 0;
}
// 立っているビットの数を数える
fn countBits(value: u32) u6 {
var count: u6 = 0;
var v = value;
while (v != 0) : (v >>= 1) {
if ((v & 1) != 0) {
count += 1;
}
}
return count;
}
// バイトオーダーを反転(エンディアン変換)
fn reverseBytes(value: u32) u32 {
return ((value & 0xFF000000) >> 24) |
((value & 0x00FF0000) >> 8) |
((value & 0x0000FF00) << 8) |
((value & 0x000000FF) << 24);
}
pub fn main() void {
var x: u32 = 0;
std.debug.print("=== Bit Manipulation ===\n", .{});
// ビット操作のテスト
x = setBit(x, 0); // ビット0を設定
x = setBit(x, 3); // ビット3を設定
std.debug.print("After setting bits 0 and 3: 0b{b:08} ({})\n", .{x, x});
x = toggleBit(x, 1); // ビット1をトグル
std.debug.print("After toggling bit 1: 0b{b:08} ({})\n", .{x, x});
const is_set = checkBit(x, 3);
std.debug.print("Bit 3 is set: {}\n", .{is_set});
const num_bits = countBits(x);
std.debug.print("Number of set bits: {}\n", .{num_bits});
x = clearBit(x, 0);
std.debug.print("After clearing bit 0: 0b{b:08} ({})\n", .{x, x});
std.debug.print("\n=== Endian Conversion ===\n", .{});
// エンディアン変換
const original: u32 = 0x12345678;
const reversed = reverseBytes(original);
std.debug.print("Original: 0x{x:08}\n", .{original});
std.debug.print("Reversed: 0x{x:08}\n", .{reversed});
}
Bonus 4: 数値フォーマッター
const std = @import("std");
// 数値を2進数文字列に変換
fn toBinaryString(allocator: std.mem.Allocator, value: u32) ![]u8 {
return try std.fmt.allocPrint(allocator, "0b{b}", .{value});
}
// 数値を16進数文字列に変換
fn toHexString(allocator: std.mem.Allocator, value: u32) ![]u8 {
return try std.fmt.allocPrint(allocator, "0x{x:08}", .{value});
}
// 数値をカンマ区切りに変換
fn toCommaString(allocator: std.mem.Allocator, value: u64) ![]u8 {
var result = std.ArrayList(u8).init(allocator);
defer result.deinit();
const str = try std.fmt.allocPrint(allocator, "{}", .{value});
defer allocator.free(str);
var count: usize = 0;
var i: usize = str.len;
while (i > 0) {
i -= 1;
if (count > 0 and count % 3 == 0) {
try result.insert(0, ',');
}
try result.insert(0, str[i]);
count += 1;
}
return try result.toOwnedSlice();
}
// バイトサイズを人間が読みやすい形式に変換
fn toHumanSize(allocator: std.mem.Allocator, bytes: u64) ![]u8 {
const units = [_][]const u8{ "B", "KB", "MB", "GB", "TB" };
var size = @as(f64, @floatFromInt(bytes));
var unit_index: usize = 0;
while (size >= 1024.0 and unit_index < units.len - 1) {
size /= 1024.0;
unit_index += 1;
}
return try std.fmt.allocPrint(allocator, "{d:.2} {s}", .{size, units[unit_index]});
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
std.debug.print("=== Number Formatting ===\n", .{});
// テスト
const value: u32 = 255;
const binary = try toBinaryString(allocator, value);
defer allocator.free(binary);
std.debug.print("Binary: {s}\n", .{binary});
const hex = try toHexString(allocator, value);
defer allocator.free(hex);
std.debug.print("Hex: {s}\n", .{hex});
const large_num: u64 = 1234567890;
const comma = try toCommaString(allocator, large_num);
defer allocator.free(comma);
std.debug.print("Comma: {s}\n", .{comma});
const size: u64 = 1024 * 1024 * 512; // 512 MB
const human = try toHumanSize(allocator, size);
defer allocator.free(human);
std.debug.print("Size: {s}\n", .{human});
// 追加テスト
const sizes = [_]u64{ 1023, 1024, 1048576, 1073741824, 1099511627776 };
std.debug.print("\n=== Various Sizes ===\n", .{});
for (sizes) |s| {
const h = try toHumanSize(allocator, s);
defer allocator.free(h);
std.debug.print("{} bytes = {s}\n", .{s, h});
}
}
ポイント解説
1. 型の選択
適切な型を選択することで、メモリ効率とパフォーマンスが向上します:
- 整数型: 値の範囲に応じて最小のサイズを選ぶ
- 浮動小数点: 必要な精度に応じてf32またはf64を選択
- 任意ビット幅: ビットフィールドや特殊な用途に活用
- 範囲チェック: 小さい型への変換時は必ずチェック
- 精度の損失: 浮動小数点から整数への変換
- 符号の扱い: 符号付きと符号なしの変換
- フラグ管理: 複数のブール値を1つの整数で管理
- 最適化: ビット演算は高速
- 低レベル制御: ハードウェアレジスタの操作
2. 型変換の安全性
型変換時の注意点:
3. ビット操作の活用
ビット操作は以下の用途で有効:
よくある間違い
1. 暗黙的な型変換を期待
// 間違い
const x: i32 = 10;
const y: i64 = x; // エラー!
// 正しい
const y: i64 = @intCast(x);
2. オーバーフローを考慮しない
// 間違い(デバッグビルドでパニック)
var x: u8 = 255;
x += 1;
// 正しい
x +%= 1; // ラップアラウンド
3. ビット演算の優先順位
// 間違い
if (value & 0x80 == 0x80) // == の方が優先順位が高い
// 正しい
if ((value & 0x80) == 0x80)
発展課題
1. 固定小数点演算
整数を使った固定小数点演算ライブラリを実装してください。
2. ビットフィールド管理
packed structを使った効率的なビットフィールド管理システムを作成してください。
3. 型安全な単位変換
距離、重さ、時間などの物理単位を型安全に扱うライブラリを実装してください。
まとめ
この課題を通じて、Zigの型システムと基本構文について深く理解できました。重要なポイント:
これらの基礎を習得することで、より高度なZigプログラミングへの準備が整いました。