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
varandconst - ✅ 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.