#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:
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:
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:
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:
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.