Skip to content

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.

Content is for learning and research only.