AgentSkillsCN

zig-fundamentals

当用户询问“Zig 中的 comptime 是如何工作的”、“请解释 Zig 的错误处理机制”、“Zig 的可选类型有哪些”、“如何在 Zig 中使用 defer”、“Zig 的基础类型有哪些”等疑问时,或在一般情况下对 Zig 语言的基础概念展开探讨时,应使用此技能。

SKILL.md
--- frontmatter
name: zig-fundamentals
description: This skill should be used when the user asks questions like "how does comptime work in Zig", "explain Zig error handling", "what are Zig optionals", "how do I use defer in Zig", "what are Zig's basic types", or generally asks about fundamental Zig language concepts for programmers experienced in other languages.
version: 0.1.0

Zig Fundamentals

Overview

This skill provides essential Zig language concepts for experienced programmers transitioning from other languages. It focuses on Zig's distinctive features: comptime (compile-time execution), error handling, optionals, and memory control primitives.

Target Zig version: 0.15.2

Core Concepts

Comptime: Compile-Time Execution

Comptime is Zig's most distinctive feature, enabling code execution during compilation.

What it does:

  • Executes arbitrary Zig code at compile time
  • Generates code, types, and data structures before runtime
  • Eliminates need for macros, preprocessor, or code generation tools

Common use cases:

  1. Generic functions and types:

    zig
    fn max(comptime T: type, a: T, b: T) T {
        return if (a > b) a else b;
    }
    

    The comptime T: type means T is determined at compile time, creating monomorphized versions.

  2. Comptime computation:

    zig
    const array_size = comptime fibonacci(10);  // Computed at compile time
    var buffer: [array_size]u8 = undefined;
    
  3. Type introspection and generation:

    zig
    fn printFields(comptime T: type) void {
        inline for (@typeInfo(T).Struct.fields) |field| {
            std.debug.print("{s}\n", .{field.name});
        }
    }
    

Key points:

  • Any expression can be forced to compile-time with comptime keyword
  • Parameters marked comptime must be known at compile time
  • Use inline for to unroll loops at compile time
  • Access type information with @typeInfo(T)

For more detailed patterns, see references/comptime-patterns.md.

Error Handling

Zig uses explicit error handling without exceptions. Errors are values, not control flow.

Error unions:

zig
const FileError = error{ FileNotFound, PermissionDenied };

fn readFile(path: []const u8) FileError![]u8 {
    // Returns either error or []u8
    if (file_not_exist) return error.FileNotFound;
    return file_contents;
}

The ! operator:

  • !T means "error union" - either an error or value of type T
  • anyerror!T allows any error type

Error handling patterns:

  1. try - propagate errors:

    zig
    const contents = try readFile("data.txt");
    // If error, returns immediately from current function
    
  2. catch - handle errors:

    zig
    const contents = readFile("data.txt") catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    
  3. catch with default value:

    zig
    const contents = readFile("data.txt") catch "default content";
    
  4. errdefer - cleanup on error:

    zig
    fn allocateResource() !*Resource {
        const resource = try allocate();
        errdefer deallocate(resource);  // Only runs if function returns error
    
        try initialize(resource);
        return resource;
    }
    

Key principles:

  • Errors are compile-time known sets of values
  • No hidden control flow (no exceptions)
  • try is syntactic sugar for catch |err| return err
  • Always handle or propagate errors explicitly

Optionals

Optionals represent values that may or may not exist, replacing null pointers safely.

Syntax:

zig
const maybe_value: ?i32 = null;  // Optional i32
const has_value: ?i32 = 42;      // Contains value

Checking and unwrapping:

  1. if unwrapping:

    zig
    if (maybe_value) |value| {
        // value is i32 here, not ?i32
        std.debug.print("Value: {}\n", .{value});
    } else {
        std.debug.print("No value\n", .{});
    }
    
  2. orelse - provide default:

    zig
    const value = maybe_value orelse 0;  // Use 0 if null
    
  3. orelse - early return:

    zig
    const value = maybe_value orelse return;  // Return if null
    
  4. .? - assert non-null (unsafe):

    zig
    const value = maybe_value.?;  // Panics if null
    

Combining with errors:

zig
fn findUser(id: u32) !?User {
    // Can return error OR null OR User
    if (database_error) return error.DatabaseDown;
    if (user_not_found) return null;
    return user;
}

// Usage:
const user = try findUser(123) orelse return error.UserNotFound;

Key distinction from other languages:

  • null only exists for optional types (?T)
  • Regular pointers cannot be null in Zig
  • Optional pointers have same size as regular pointers (null is optimized)

Defer: Guaranteed Cleanup

defer executes code when scope exits, regardless of how it exits (return, error, etc.).

Basic usage:

zig
fn processFile(path: []const u8) !void {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();  // Guaranteed to run when function exits

    // Work with file...
    // file.close() automatically called
}

Multiple defers execute in reverse order (LIFO):

zig
defer std.debug.print("Third\n", .{});
defer std.debug.print("Second\n", .{});
defer std.debug.print("First\n", .{});
// Prints: First, Second, Third

errdefer - cleanup only on error:

zig
fn createWidget() !*Widget {
    const widget = try allocator.create(Widget);
    errdefer allocator.destroy(widget);  // Only if function errors

    try widget.initialize();  // If this fails, widget is destroyed
    return widget;
}

Key patterns:

  • Use defer immediately after acquiring resource
  • errdefer for partial initialization cleanup
  • Defers execute in reverse order of declaration
  • Scope-based, not function-based (works in blocks)

Basic Types and Values

Integer types:

  • Explicit bit sizes: i8, u8, i16, u16, i32, u32, i64, u64, i128, u128
  • Platform-dependent: isize, usize (pointer-sized integers)
  • Arbitrary bit width: i7, u24, etc. (any bit count)

Float types:

  • f16, f32, f64, f80, f128

Boolean:

  • bool - only true or false
  • No implicit integer conversion

Arrays and slices:

zig
const array: [5]i32 = .{ 1, 2, 3, 4, 5 };     // Fixed-size array
const slice: []const i32 = &array;             // Slice references array

Struct initialization:

zig
const Point = struct {
    x: f32,
    y: f32,
};

const point = Point{ .x = 1.0, .y = 2.0 };

Anonymous struct literals:

zig
const point = .{ .x = 1.0, .y = 2.0 };  // Type inferred

Undefined and initialization:

  • undefined - uninitialized memory (performance optimization)
  • Use for arrays/buffers that will be filled immediately
  • Debug builds catch usage of undefined values

Control Flow Differences

if is an expression:

zig
const value = if (condition) 42 else 0;

switch is exhaustive:

zig
const value: i32 = 2;
const result = switch (value) {
    1 => "one",
    2 => "two",
    3, 4, 5 => "three to five",
    else => "other",
};

while with continue expressions:

zig
var i: usize = 0;
while (i < 10) : (i += 1) {  // Continue expression runs each iteration
    // Loop body
}

for for arrays and slices:

zig
for (items) |item| {
    std.debug.print("{}\n", .{item});
}

// With index:
for (items, 0..) |item, i| {
    std.debug.print("{}: {}\n", .{ i, item });
}

Working with Zig Code

Reading Zig Code

When examining Zig code:

  1. Look for comptime - indicates compile-time logic
  2. Check error types - functions with ! return errors
  3. Identify optionals - ?T types may be null
  4. Find defers - locate resource cleanup
  5. Read parameter types - comptime params vs runtime params

Common Patterns

Result type (error + optional):

zig
fn operation() !?Value {
    // Returns: error, null, or Value
}

Allocator parameter:

zig
fn createThing(allocator: std.mem.Allocator) !Thing {
    const data = try allocator.alloc(u8, size);
    // allocator passed explicitly, no hidden allocation
}

Payload capture:

zig
if (maybe_value) |value| {
    // Use value
}

if (result) |value| {
    // Success case
} else |err| {
    // Error case
}

Transitioning from Other Languages

From C/C++:

  • No implicit casts - use explicit conversion (@intCast, etc.)
  • No undefined behavior in safe mode (checked in Debug/ReleaseSafe)
  • No hidden control flow (no exceptions, no operator overloading)
  • Comptime replaces templates/macros

From Rust:

  • No borrow checker - manual memory management with allocators
  • Explicit allocator passing instead of implicit lifetimes
  • Error unions instead of Result<T, E> enum
  • Optionals instead of Option<T> enum

From Go/Java/Python:

  • No garbage collector - manual memory management
  • Explicit error handling, no exceptions
  • No hidden allocations - all memory explicit
  • Value types by default, not references

Additional Resources

Reference Files

For detailed examples and advanced patterns:

  • references/comptime-patterns.md - Advanced comptime techniques, type introspection, generic patterns
  • references/error-patterns.md - Comprehensive error handling patterns, error sets, custom errors

Official Documentation

Quick Reference

ConceptSyntaxPurpose
Comptime parametercomptime T: typeGeneric/compile-time value
Error union!TFunction may return error or T
Optional?TValue may be null
Unwrap optionalvalue orelse defaultProvide default if null
Try errortry exprPropagate error or unwrap value
Catch errorexpr catch handlerHandle error case
Defer cleanupdefer exprExecute when scope exits
Error defererrdefer exprExecute only on error
Type introspection@typeInfo(T)Get compile-time type info

Tips for Success

  1. Embrace explicit error handling - no hidden exceptions improves reliability
  2. Use comptime for generics - replaces C++ templates with readable code
  3. Leverage defer for cleanup - simpler than try/finally or RAII
  4. Pass allocators explicitly - makes memory allocation visible
  5. Read the standard library - idiomatic patterns and common utilities
  6. Use Debug build mode - catches undefined behavior and safety violations

When working with Zig code, focus on understanding the explicit flow: where errors originate, how memory is allocated, and what executes at compile time versus runtime.