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:

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

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:

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

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

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

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.