Zig Data Types
Zig has a rich and precise type system. This chapter will详细介绍 the various data types in Zig.
Integer Types
Signed Integers
Zig provides multiple sizes of signed integer types:
zig
const std = @import("std");
pub fn main() void {
const i8_val: i8 = -128; // 8-bit signed integer (-128 to 127)
const i16_val: i16 = -32768; // 16-bit signed integer
const i32_val: i32 = -2147483648; // 32-bit signed integer
const i64_val: i64 = -9223372036854775808; // 64-bit signed integer
std.debug.print("i8: {}, i16: {}, i32: {}, i64: {}\n",
.{ i8_val, i16_val, i32_val, i64_val });
}Unsigned Integers
zig
const std = @import("std");
pub fn main() void {
const u8_val: u8 = 255; // 8-bit unsigned integer (0 to 255)
const u16_val: u16 = 65535; // 16-bit unsigned integer
const u32_val: u32 = 4294967295; // 32-bit unsigned integer
const u64_val: u64 = 18446744073709551615; // 64-bit unsigned integer
std.debug.print("u8: {}, u16: {}, u32: {}, u64: {}\n",
.{ u8_val, u16_val, u32_val, u64_val });
}Arbitrary Bit-width Integers
Zig supports integer types of arbitrary bit widths:
zig
const std = @import("std");
pub fn main() void {
const i3_val: i3 = -4; // 3-bit signed integer (-4 to 3)
const u3_val: u3 = 7; // 3-bit unsigned integer (0 to 7)
const i12_val: i12 = -2048; // 12-bit signed integer
const u24_val: u24 = 16777215; // 24-bit unsigned integer
std.debug.print("i3: {}, u3: {}, i12: {}, u24: {}\n",
.{ i3_val, u3_val, i12_val, u24_val });
}Special Integer Types
zig
const std = @import("std");
pub fn main() void {
// isize and usize: integers same size as pointers
const isize_val: isize = -1000;
const usize_val: usize = 1000;
// c_int, c_uint: integer types compatible with C
const c_int_val: c_int = 42;
const c_uint_val: c_uint = 42;
std.debug.print("isize: {}, usize: {}\n", .{ isize_val, usize_val });
std.debug.print("c_int: {}, c_uint: {}\n", .{ c_int_val, c_uint_val });
}Floating-point Types
Standard Floating-point Numbers
zig
const std = @import("std");
pub fn main() void {
const f16_val: f16 = 3.14; // 16-bit floating point (half precision)
const f32_val: f32 = 3.14159; // 32-bit floating point (single precision)
const f64_val: f64 = 3.141592653589793; // 64-bit floating point (double precision)
const f128_val: f128 = 3.1415926535897932384626433832795; // 128-bit floating point
std.debug.print("f16: {d:.2}\n", .{f16_val});
std.debug.print("f32: {d:.5}\n", .{f32_val});
std.debug.print("f64: {d:.15}\n", .{f64_val});
std.debug.print("f128: {d:.30}\n", .{f128_val});
}C-compatible Floating-point
zig
const std = @import("std");
pub fn main() void {
const c_longdouble_val: c_longdouble = 3.14159;
std.debug.print("c_longdouble: {d:.5}\n", .{c_longdouble_val});
}Boolean Type
zig
const std = @import("std");
pub fn main() void {
const is_true: bool = true;
const is_false: bool = false;
// Boolean operations
const and_result = is_true and is_false; // false
const or_result = is_true or is_false; // true
const not_result = !is_true; // false
std.debug.print("true: {}, false: {}\n", .{ is_true, is_false });
std.debug.print("and: {}, or: {}, not: {}\n", .{ and_result, or_result, not_result });
}Character and String Types
Character Type
In Zig, characters are actually integers:
zig
const std = @import("std");
pub fn main() void {
const char_a: u8 = 'A'; // ASCII character
const char_unicode: u21 = '中'; // Unicode character
std.debug.print("ASCII character 'A': {} (value: {})\n", .{ char_a, char_a });
std.debug.print("Unicode character '中': {u} (value: {})\n", .{ char_unicode, char_unicode });
}String Type
zig
const std = @import("std");
pub fn main() void {
// String literal type is *const [N:0]u8
const string_literal = "Hello, 世界!";
// String slice
const string_slice: []const u8 = "Hello, Zig!";
// Mutable string (character array)
var mutable_string = [_]u8{'H', 'e', 'l', 'l', 'o'};
std.debug.print("String literal: {s}\n", .{string_literal});
std.debug.print("String slice: {s}\n", .{string_slice});
std.debug.print("Mutable string: {s}\n", .{mutable_string});
// Modify mutable string
mutable_string[0] = 'h';
std.debug.print("After modification: {s}\n", .{mutable_string});
}Array Types
Fixed-size Arrays
zig
const std = @import("std");
pub fn main() void {
// Explicit size array
const numbers: [5]i32 = [5]i32{ 1, 2, 3, 4, 5 };
// Inferred size array
const fruits = [_][]const u8{ "苹果", "香蕉", "橙子" };
// Repeated initialization
const zeros = [_]i32{0} ** 10; // 10 zeros
std.debug.print("Number array length: {}\n", .{numbers.len});
std.debug.print("Fruit array: ");
for (fruits) |fruit| {
std.debug.print("{s} ", .{fruit});
}
std.debug.print("\n");
std.debug.print("First 5 zeros: ");
for (zeros[0..5]) |zero| {
std.debug.print("{} ", .{zero});
}
std.debug.print("\n");
}Multidimensional Arrays
zig
const std = @import("std");
pub fn main() void {
// 2D array
const matrix: [3][3]i32 = [3][3]i32{
[3]i32{ 1, 2, 3 },
[3]i32{ 4, 5, 6 },
[3]i32{ 7, 8, 9 },
};
std.debug.print("3x3 matrix:\n");
for (matrix) |row| {
for (row) |val| {
std.debug.print("{} ", .{val});
}
std.debug.print("\n");
}
}Slice Types
zig
const std = @import("std");
pub fn main() void {
var array = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Slice is a reference to a part of the array
const slice1: []i32 = array[2..7]; // Indices 2 to 6
const slice2: []i32 = array[0..]; // From start to end
const slice3: []const i32 = array[5..8]; // Read-only slice
std.debug.print("Original array: ");
for (array) |val| {
std.debug.print("{} ", .{val});
}
std.debug.print("\n");
std.debug.print("Slice1 [2..7]: ");
for (slice1) |val| {
std.debug.print("{} ", .{val});
}
std.debug.print("\n");
std.debug.print("Slice2 [0..]: ");
for (slice2) |val| {
std.debug.print("{} ", .{val});
}
std.debug.print("\n");
// Modifying slice affects original array
slice1[0] = 100;
std.debug.print("Array after modification: ");
for (array) |val| {
std.debug.print("{} ", .{val});
}
std.debug.print("\n");
}Pointer Types
Single-item Pointers
zig
const std = @import("std");
pub fn main() void {
var number: i32 = 42;
// Single-item pointer
const ptr: *i32 = &number;
const const_ptr: *const i32 = &number;
std.debug.print("Original value: {}\n", .{number});
std.debug.print("Access via pointer: {}\n", .{ptr.*});
// Modify value through pointer
ptr.* = 100;
std.debug.print("After modification: {}\n", .{number});
}Multi-item Pointers
zig
const std = @import("std");
pub fn main() void {
var array = [_]i32{ 1, 2, 3, 4, 5 };
// Multi-item pointer
const multi_ptr: [*]i32 = &array;
std.debug.print("Access via multi-item pointer:\n");
var i: usize = 0;
while (i < array.len) : (i += 1) {
std.debug.print("array[{}] = {}\n", .{ i, multi_ptr[i] });
}
}C Pointers
zig
const std = @import("std");
pub fn main() void {
var number: i32 = 42;
// C pointer (can be null)
var c_ptr: ?*i32 = &number;
var null_ptr: ?*i32 = null;
if (c_ptr) |ptr| {
std.debug.print("C pointer value: {}\n", .{ptr.*});
}
if (null_ptr) |ptr| {
std.debug.print("Will not execute\n", .{});
} else {
std.debug.print("null_ptr is null\n", .{});
}
}Optional Types
zig
const std = @import("std");
pub fn main() void {
// Optional type can be value or null
var maybe_number: ?i32 = 42;
var maybe_null: ?i32 = null;
// Use if to unwrap optional value
if (maybe_number) |num| {
std.debug.print("Number is: {}\n", .{num});
} else {
std.debug.print("No number\n", .{});
}
if (maybe_null) |num| {
std.debug.print("Will not execute\n", .{});
} else {
std.debug.print("maybe_null is null\n", .{});
}
// Use orelse to provide default value
const number = maybe_null orelse 0;
std.debug.print("Using default value: {}\n", .{number});
}Error Union Types
zig
const std = @import("std");
// Define error set
const MathError = error{
DivisionByZero,
Overflow,
};
fn divide(a: i32, b: i32) MathError!i32 {
if (b == 0) return MathError.DivisionByZero;
return @divTrunc(a, b);
}
pub fn main() void {
// Error union type
const result1 = divide(10, 2);
const result2 = divide(10, 0);
// Handle success case
if (result1) |value| {
std.debug.print("10 / 2 = {}\n", .{value});
} else |err| {
std.debug.print("Error: {}\n", .{err});
}
// Handle error case
if (result2) |value| {
std.debug.print("10 / 0 = {}\n", .{value});
} else |err| {
std.debug.print("Error: {}\n", .{err});
}
// Use catch to provide default value
const safe_result = divide(10, 0) catch 0;
std.debug.print("Safe division result: {}\n", .{safe_result});
}Struct Types
zig
const std = @import("std");
const Point = struct {
x: f32,
y: f32,
// Struct method
pub fn distance(self: Point, other: Point) f32 {
const dx = self.x - other.x;
const dy = self.y - other.y;
return @sqrt(dx * dx + dy * dy);
}
};
pub fn main() void {
const p1 = Point{ .x = 0.0, .y = 0.0 };
const p2 = Point{ .x = 3.0, .y = 4.0 };
std.debug.print("Point 1: ({d:.1}, {d:.1})\n", .{ p1.x, p1.y });
std.debug.print("Point 2: ({d:.1}, {d:.1})\n", .{ p2.x, p2.y });
std.debug.print("Distance: {d:.2}\n", .{p1.distance(p2)});
}Enum Types
Simple Enums
zig
const std = @import("std");
const Color = enum {
Red,
Green,
Blue,
};
pub fn main() void {
const my_color = Color.Red;
switch (my_color) {
Color.Red => std.debug.print("Color is Red\n", .{}),
Color.Green => std.debug.print("Color is Green\n", .{}),
Color.Blue => std.debug.print("Color is Blue\n", .{}),
}
}Enums with Values
zig
const std = @import("std");
const Status = enum(u8) {
Ok = 0,
Error = 1,
Pending = 2,
pub fn toString(self: Status) []const u8 {
return switch (self) {
.Ok => "Success",
.Error => "Error",
.Pending => "Pending",
};
}
};
pub fn main() void {
const status = Status.Ok;
std.debug.print("Status: {} ({})\n", .{ status, status.toString() });
std.debug.print("Status value: {}\n", .{@intFromEnum(status)});
}Union Types
Tagged Unions
zig
const std = @import("std");
const Value = union(enum) {
Integer: i32,
Float: f64,
String: []const u8,
Boolean: bool,
};
pub fn main() void {
const values = [_]Value{
Value{ .Integer = 42 },
Value{ .Float = 3.14 },
Value{ .String = "Hello" },
Value{ .Boolean = true },
};
for (values) |value| {
switch (value) {
.Integer => |int| std.debug.print("Integer: {}\n", .{int}),
.Float => |float| std.debug.print("Float: {d:.2}\n", .{float}),
.String => |string| std.debug.print("String: {s}\n", .{string}),
.Boolean => |boolean| std.debug.print("Boolean: {}\n", .{boolean}),
}
}
}Function Types
zig
const std = @import("std");
// Function type definition
const BinaryOp = fn (i32, i32) i32;
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn multiply(a: i32, b: i32) i32 {
return a * b;
}
fn calculate(op: BinaryOp, a: i32, b: i32) i32 {
return op(a, b);
}
pub fn main() void {
const result1 = calculate(add, 5, 3);
const result2 = calculate(multiply, 5, 3);
std.debug.print("5 + 3 = {}\n", .{result1});
std.debug.print("5 * 3 = {}\n", .{result2});
}Type Conversion
Explicit Type Conversion
zig
const std = @import("std");
pub fn main() void {
const int_val: i32 = 42;
const float_val: f64 = 3.14;
// Integer conversion
const u32_val: u32 = @intCast(int_val);
const i64_val: i64 = @intCast(int_val);
// Float conversion
const f32_val: f32 = @floatCast(float_val);
const int_from_float: i32 = @intFromFloat(float_val);
const float_from_int: f64 = @floatFromInt(int_val);
std.debug.print("Original integer: {}\n", .{int_val});
std.debug.print("Converted to u32: {}\n", .{u32_val});
std.debug.print("Converted to i64: {}\n", .{i64_val});
std.debug.print("Original float: {d:.2}\n", .{float_val});
std.debug.print("Converted to f32: {d:.2}\n", .{f32_val});
std.debug.print("Converted to integer: {}\n", .{int_from_float});
std.debug.print("Integer to float: {d:.2}\n", .{float_from_int});
}Bit Casting
zig
const std = @import("std");
pub fn main() void {
const float_val: f32 = 3.14;
const int_bits: u32 = @bitCast(float_val);
const back_to_float: f32 = @bitCast(int_bits);
std.debug.print("Float: {d:.2}\n", .{float_val});
std.debug.print("Bit representation: 0x{X}\n", .{int_bits});
std.debug.print("Converted back to float: {d:.2}\n", .{back_to_float});
}Compile-time Types
comptime_int and comptime_float
zig
const std = @import("std");
pub fn main() void {
// Compile-time integers can be of arbitrary size
const big_number = 123456789012345678901234567890;
// Compile-time floats have infinite precision
const precise_pi = 3.1415926535897932384626433832795028841971693993751;
// Runtime requires specific type
const runtime_int: i64 = big_number % 1000000;
const runtime_float: f64 = precise_pi;
std.debug.print("Last 6 digits of big integer: {}\n", .{runtime_int});
std.debug.print("π (f64 precision): {d:.15}\n", .{runtime_float});
}Type Information and Reflection
zig
const std = @import("std");
const Person = struct {
name: []const u8,
age: u32,
};
pub fn main() void {
// Get type information
const person_type = @TypeOf(Person{});
const int_type = @TypeOf(@as(i32, 0));
std.debug.print("Person type: {}\n", .{person_type});
std.debug.print("i32 type: {}\n", .{int_type});
// Type size
std.debug.print("Person size: {} bytes\n", .{@sizeOf(Person)});
std.debug.print("i32 size: {} bytes\n", .{@sizeOf(i32)});
// Type alignment
std.debug.print("Person alignment: {} bytes\n", .{@alignOf(Person)});
std.debug.print("i32 alignment: {} bytes\n", .{@alignOf(i32)});
}Summary
This chapter provided a detailed introduction to Zig's type system:
- ✅ Integer types: signed, unsigned, arbitrary bit-width
- ✅ Floating-point types: different precision floats
- ✅ Boolean and string types
- ✅ Array, slice, and pointer types
- ✅ Optional types and error union types
- ✅ Struct, enum, and union types
- ✅ Function types and type conversion
- ✅ Compile-time types and type reflection
Zig's type system is both powerful and flexible, providing a solid foundation for writing safe and efficient code. In the next chapter, we'll learn how to use these types to declare and manipulate variables and constants.