Skip to content

Zig Variables and Constants

This chapter will explore the use of variables and constants in Zig, including their differences, best practices, and advanced usage.

Basic Concepts of Variables and Constants

Constants (const)

Constants are immutable values that cannot be modified once assigned:

zig
const std = @import("std");

pub fn main() void {
    const name = "张三";
    const age = 25;
    const pi = 3.14159;

    std.debug.print("Name: {s}, Age: {}, π = {d:.5}\n", .{ name, age, pi });

    // The following code will cause a compilation error:
    // name = "李四"; // Error: Cannot modify constant
    // age = 26;     // Error: Cannot modify constant
}

Variables (var)

Variables are mutable values that can be modified at runtime:

zig
const std = @import("std");

pub fn main() void {
    var counter = 0;
    var message = "Start";

    std.debug.print("{s}: {}\n", .{ message, counter });

    // Modify variable values
    counter = 10;
    message = "Updated";

    std.debug.print("{s}: {}\n", .{ message, counter });
}

Compile-time Constants vs Runtime Constants

Compile-time Constants

Compile-time constants are determined at compile time:

zig
const std = @import("std");

// Compile-time constants
const BUFFER_SIZE = 1024;
const VERSION = "1.0.0";
const MAX_USERS = 100;

// Compile-time calculations
const TOTAL_MEMORY = BUFFER_SIZE * MAX_USERS;

pub fn main() void {
    std.debug.print("Buffer size: {}\n", .{BUFFER_SIZE});
    std.debug.print("Version: {s}\n", .{VERSION});
    std.debug.print("Max users: {}\n", .{MAX_USERS});
    std.debug.print("Total memory required: {} bytes\n", .{TOTAL_MEMORY});
}

Runtime Constants

Runtime constants have values determined at runtime but cannot be modified afterwards:

zig
const std = @import("std");

fn getCurrentTime() i64 {
    return 1640995200; // Simulate getting current time
}

pub fn main() void {
    // Runtime constants
    const start_time = getCurrentTime();
    const user_input = "Hello, World!";

    std.debug.print("Program start time: {}\n", .{start_time});
    std.debug.print("User input: {s}\n", .{user_input});

    // start_time value is determined at runtime, but cannot be modified afterwards
}

Mutability of Variables

Fully Mutable Variables

zig
const std = @import("std");

pub fn main() void {
    var number = 42;
    var text = "Initial text";
    var flag = true;

    std.debug.print("Initial values - Number: {}, Text: {s}, Flag: {}\n",
                    .{ number, text, flag });

    // Modify all variables
    number = 100;
    text = "Updated text";
    flag = false;

    std.debug.print("Updated values - Number: {}, Text: {s}, Flag: {}\n",
                    .{ number, text, flag });
}

Partial Mutability

zig
const std = @import("std");

const Person = struct {
    name: []const u8,
    age: u32,
};

pub fn main() void {
    // Struct variable, fields can be modified
    var person = Person{
        .name = "王五",
        .age = 30,
    };

    std.debug.print("Initial: {s}, {}\n", .{ person.name, person.age });

    // Can modify fields
    person.age = 31;
    // person.name = "赵六"; // Note: name is []const u8, cannot modify content

    std.debug.print("Updated: {s}, {}\n", .{ person.name, person.age });
}

Mutability of Arrays and Slices

Mutable Arrays

zig
const std = @import("std");

pub fn main() void {
    // Mutable array
    var numbers = [_]i32{ 1, 2, 3, 4, 5 };

    std.debug.print("Original array: ");
    for (numbers) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n");

    // Modify array elements
    numbers[0] = 10;
    numbers[4] = 50;

    std.debug.print("Modified array: ");
    for (numbers) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n");
}

Constant Arrays

zig
const std = @import("std");

pub fn main() void {
    // Constant array
    const fruits = [_][]const u8{ "苹果", "香蕉", "橙子" };

    std.debug.print("Fruit list: ");
    for (fruits) |fruit| {
        std.debug.print("{s} ", .{fruit});
    }
    std.debug.print("\n");

    // The following code will cause compilation error:
    // fruits[0] = "葡萄"; // Error: Cannot modify constant array
}

Slice Mutability

zig
const std = @import("std");

pub fn main() void {
    var array = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

    // Mutable slice
    var mutable_slice: []i32 = array[2..7];

    // Read-only slice
    const readonly_slice: []const i32 = array[0..5];

    std.debug.print("Original array: ");
for (array) |val| {
        std.debug.print("{} ", .{val});
}
std.debug.print("\n");

    // Modify through mutable slice
mutable_slice[0] = 100;
mutable_slice[2] = 200;

std.debug.print("Modified: ");
for (array) |val| {
    std.debug.print("{} ", .{val});
}
std.debug.print("\n");

std.debug.print("Read-only slice: ");
for (readonly_slice) |val| {
    std.debug.print("{} ", .{val});
}
std.debug.print("\n");
}

Mutability of Pointers

Pointers to Mutable Data

zig
const std = @import("std");

pub fn main() void {
    var number: i32 = 42;

    // Mutable pointer to mutable data
    var ptr: *i32 = &number;

    std.debug.print("Original value: {}\n", .{number});
    std.debug.print("Through pointer: {}\n", .{ptr.*});

    // Modify value through pointer
    ptr.* = 100;
    std.debug.print("Modified: {}\n", .{number});

    // Pointer itself can change target
    var another_number: i32 = 200;
    ptr = &another_number;
    std.debug.print("New pointer: {}\n", .{ptr.*});
}

Pointers to Constant Data

zig
const std = @import("std");

pub fn main() void {
    const number: i32 = 42;

    // Pointer to constant data
    const const_ptr: *const i32 = &number;

    std.debug.print("Constant value: {}\n", .{const_ptr.*});

    // The following code will cause compilation error:
    // const_ptr.* = 100; // Error: Cannot modify data through constant pointer
}

Global Variables and Constants

Global Constants

zig
const std = @import("std");

// Global constants
const PROGRAM_NAME = "My Program";
const VERSION_MAJOR = 1;
const VERSION_MINOR = 0;
const VERSION_PATCH = 0;

// Calculated global constants
const VERSION_STRING = std.fmt.comptimePrint("{}.{}.{}", .{ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH });

pub fn main() void {
    std.debug.print("Program name: {s}\n", .{PROGRAM_NAME});
    std.debug.print("Version: {s}\n", .{VERSION_STRING});
}

Global Variables

zig
const std = @import("std");

// Global variables (use with caution)
var global_counter: i32 = 0;
var global_message: []const u8 = "Initial message";

fn incrementCounter() void {
    global_counter += 1;
}

fn updateMessage(new_message: []const u8) void {
    global_message = new_message;
}

pub fn main() void {
    std.debug.print("Initial state - Counter: {}, Message: {s}\n",
                    .{ global_counter, global_message });

    incrementCounter();
    updateMessage("Updated message");

    std.debug.print("Updated state - Counter: {}, Message: {s}\n",
                    .{ global_counter, global_message });
}

Thread-local Variables

zig
const std = @import("std");

// Thread-local variables
threadlocal var thread_counter: i32 = 0;

fn incrementThreadCounter() void {
    thread_counter += 1;
}

pub fn main() void {
    std.debug.print("Initial thread counter: {}\n", .{thread_counter});

    incrementThreadCounter();
    incrementThreadCounter();

    std.debug.print("Final thread counter: {}\n", .{thread_counter});
}

Variable Initialization Patterns

Lazy Initialization

zig
const std = @import("std");

fn expensiveCalculation() i32 {
    std.debug.print("Performing expensive calculation...\n", .{});
    return 42;
}

pub fn main() void {
    // Declare but don't initialize
    var result: i32 = undefined;
    var should_calculate = true;

    if (should_calculate) {
        result = expensiveCalculation();
    } else {
        result = 0;
    }

    std.debug.print("Result: {}\n", .{result});
}

Conditional Initialization

zig
const std = @import("std");

pub fn main() void {
    const debug_mode = true;

    // Conditional initialization
    const log_level = if (debug_mode) "DEBUG" else "INFO";
    const buffer_size = if (debug_mode) 1024 else 512;

    std.debug.print("Log level: {s}\n", .{log_level});
    std.debug.print("Buffer size: {}\n", .{buffer_size});
}

Variable Lifecycle

Stack Variables

zig
const std = @import("std");

fn createStackVariable() void {
    var local_var = 42; // Stack variable
    std.debug.print("Stack variable: {}\n", .{local_var});
    // local_var is automatically destroyed at end of function
}

pub fn main() void {
    createStackVariable();
    // local_var no longer exists
}

Static Variables

zig
const std = @import("std");

fn getStaticCounter() *i32 {
    // Static variable, exists throughout program runtime
    const static = struct {
        var counter: i32 = 0;
    };

    static.counter += 1;
    return &static.counter;
}

pub fn main() void {
    const counter1 = getStaticCounter();
    const counter2 = getStaticCounter();
    const counter3 = getStaticCounter();

    std.debug.print("First call: {}\n", .{ counter1.* });
    std.debug.print("Second call: {}\n", .{ counter2.* });
    std.debug.print("Third call: {}\n", .{ counter3.* });
}

Constant Expressions

Compile-time Calculations

zig
const std = @import("std");

// Compile-time constant expressions
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = 24;

const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
const SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;

// Compile-time string operations
const GREETING = "Hello";
const TARGET = "World";
const MESSAGE = GREETING ++ ", " ++ TARGET ++ "!";

pub fn main() void {
    std.debug.print("Seconds in a day: {}\n", .{ SECONDS_PER_DAY });
    std.debug.print("Message: {s}\n", .{MESSAGE });
}

Compile-time Functions

zig
const std = @import("std");

// Compile-time functions
fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Compile-time calculate Fibonacci numbers
const FIB_10 = comptime fibonacci(10);
const FIB_15 = comptime fibonacci(15);

pub fn main() void {
    std.debug.print("Fibonacci #10: {}\n", .{FIB_10});
    std.debug.print("Fibonacci #15: {}\n", .{FIB_15});
}

Best Practices for Variables and Constants

1. Prefer Constants

zig
const std = @import("std");

pub fn main() void {
    // ✅ Good practice: Default to const
    const user_name = "User";
    const max_attempts = 3;
    const timeout_ms = 5000;

    // Only use var when modification is needed
    var current_attempts = 0;
    var is_connected = false;
}

2. Clear Naming Conventions

zig
const std = @import("std");

// Global constants: SCREAMING_SNAKE_CASE
const MAX_BUFFER_SIZE = 4096;
const DEFAULT_TIMEOUT = 30;

pub fn main() void {
    // Local variables and constants: snake_case
    const user_id = 12345;
    var connection_count = 0;

    // Types: PascalCase
    const Point = struct {
        x: f32,
        y: f32,
    };

    const origin = Point{ .x = 0.0, .y = 0.0 };

    std.debug.print("User ID: {}, Connections: {}\n", .{ user_id, connection_count });
}

3. Appropriate Scopes

zig
const std = @import("std");

pub fn main() void {
    const global_config = "Global configuration";

    // Limit variable scope
    {
        const local_temp = "Temporary data";
        var processing_flag = true;

        // Use temporary variables within this block
        if (processing_flag) {
            std.debug.print("Processing: {s}\n", .{local_temp});
            processing_flag = false;
        }
    }

    // local_temp and processing_flag not accessible here
    std.debug.print("Global config: {s}\n", .{global_config});
}

4. Avoid Global Mutable State

zig
const std = @import("std");

// ❌ Avoid: Global mutable variables
// var global_state = 0;

// ✅ Better: Use struct to encapsulate state
const AppState = struct {
    counter: i32,
    message: []const u8,

    pub fn init() AppState {
        return AppState{
            .counter = 0,
            .message = "Initial state",
        };
    }

pub fn increment(self: *AppState) void {
    self.counter += 1;
}

pub fn main() void {
    var app_state = AppState.init();

    std.debug.print("Initial: {}, {s}\n", .{ app_state.counter, app_state.message });

    app_state.increment();
    app_state.message = "Updated state";

    std.debug.print("Updated: {}, {s}\n", .{ app_state.counter, app_state.message });
}

Practice Exercises

Exercise 1: Configuration Management

zig
const std = @import("std");

// TODO: Define application configuration constants
// - Application name
// - Version number
// - Default port
// - Maximum connections

// TODO: Define a configuration struct with mutable runtime configuration

pub fn main() void {
    // TODO: Create configuration instance
    // TODO: Modify runtime configuration
    // TODO: Print all configuration information
}

Exercise 2: Counter System

zig
const std = @import("std");

// TODO: Implement a counter system
// - Support increment, decrement, reset operations
// - Max and min value limits
// - Record operation history

const Counter = struct {
    // TODO: Define fields

    pub fn init(min_val: i32, max_val: i32) Counter {
        // TODO: Implement initialization
    }

    pub fn increment(self: *Counter) bool {
        // TODO: Implement increment operation
    }

    pub fn decrement(self: *Counter) bool {
        // TODO: Implement decrement operation
    }

    pub fn reset(self: *Counter) void {
        // TODO: Implement reset operation
    };
};

pub fn main() void {
    // TODO: Test counter system
}

Summary

This chapter explored the use of variables and constants in Zig:

  • ✅ Differences between var and const
  • ✅ Compile-time vs runtime constants
  • ✅ Mutability of different data types
  • ✅ Use of global variables and thread-local variables
  • ✅ Variable lifecycles and scopes
  • ✅ Constant expressions and compile-time calculations
  • ✅ Best practices and code style

Understanding correct usage of variables and constants is fundamental to writing high-quality Zig code. In the next chapter, we'll learn about loops in Zig.

Content is for learning and research only.