Variables and Data Types
In this lesson, we'll explore how Zig handles variables and its type system. You'll learn about variable declaration, mutability, type inference, and the various data types available in Zig.
What You'll Learn
- How to declare variables and constants in Zig
- The difference between
constandvar - Zig's basic data types and how to use them
- Type inference and explicit type annotations
- How to work with different numeric types
Variables and Mutability
In Zig, you declare variables using const or var. The choice between them determines whether the variable is immutable or mutable:
const std = @import("std");
pub fn main() !void {
// Immutable variable (recommended when possible)
const x: i32 = 5;
std.debug.print("The value of x is: {}\n", .{x});
// This would cause a compile error:
// x = 6; // error: cannot assign to constant
// Mutable variable
var y: i32 = 5;
std.debug.print("The value of y is: {}\n", .{y});
y = 6; // This works!
std.debug.print("The value of y is now: {}\n", .{y});
}
Best Practice: Use
constby default and only usevarwhen you need to modify a value. This makes your code more predictable and easier to reason about.
Type Inference
Zig can often infer types from the value you assign, but you can also be explicit:
const std = @import("std");
pub fn main() !void {
// Type inference - Zig figures out the type
const x = 42; // i32 (inferred as comptime_int, then i32)
const y = 3.14; // f64 (inferred as comptime_float, then f64)
const name = "Zig"; // []const u8 (string slice)
// Explicit type annotation
const z: u8 = 42; // Explicitly an 8-bit unsigned integer
const pi: f32 = 3.14; // Explicitly a 32-bit float
std.debug.print("x={}, y={d:.2}, name={s}, z={}, pi={d:.2}\n", .{x, y, name, z, pi});
}
Integer Types
Zig provides a comprehensive set of integer types with explicit sizes:
const std = @import("std");
pub fn main() !void {
// Signed integers (i8, i16, i32, i64, i128)
const small: i8 = -128; // Range: -128 to 127
const medium: i32 = -2_147_483_648; // Use underscores for readability
const large: i64 = 9_223_372_036_854_775_807;
// Unsigned integers (u8, u16, u32, u64, u128)
const byte: u8 = 255; // Range: 0 to 255
const word: u16 = 65535;
const dword: u32 = 4_294_967_295;
// Special integer types
const ptr_sized: usize = 1024; // Size of a pointer (32/64-bit depending on platform)
const signed_ptr: isize = -1024; // Signed version of usize
std.debug.print("Signed: {}, {}, {}\n", .{small, medium, large});
std.debug.print("Unsigned: {}, {}, {}\n", .{byte, word, dword});
std.debug.print("Pointer-sized: {}, {}\n", .{ptr_sized, signed_ptr});
}
Floating-Point Types
Zig supports standard floating-point types:
const std = @import("std");
pub fn main() !void {
// f16 - 16-bit float (half precision)
const half: f16 = 1.5;
// f32 - 32-bit float (single precision)
const single: f32 = 3.14159;
// f64 - 64-bit float (double precision, default)
const double: f64 = 3.141592653589793;
// f128 - 128-bit float (quad precision)
const quad: f128 = 3.141592653589793238462643383279502884197;
std.debug.print("f16: {d:.2}\n", .{half});
std.debug.print("f32: {d:.5}\n", .{single});
std.debug.print("f64: {d:.15}\n", .{double});
}
Boolean Type
The boolean type in Zig is straightforward:
const std = @import("std");
pub fn main() !void {
const is_learning: bool = true;
const is_expert: bool = false;
std.debug.print("Learning Zig: {}\n", .{is_learning});
std.debug.print("Expert yet: {}\n", .{is_expert});
// Boolean operations
const both = is_learning and is_expert;
const either = is_learning or is_expert;
const not_expert = !is_expert;
std.debug.print("Both: {}, Either: {}, Not expert: {}\n", .{both, either, not_expert});
}
Type Conversion
Zig requires explicit type conversions for safety. There are no implicit conversions:
const std = @import("std");
pub fn main() !void {
// Explicit type conversion using @as
const x: i32 = 42;
const y: f64 = @as(f64, @floatFromInt(x));
std.debug.print("Integer: {}\n", .{x});
std.debug.print("Converted to float: {d:.2}\n", .{y});
// Converting between integer types
const big: i32 = 1000;
const small: i16 = @intCast(big); // Use @intCast for integer conversions
std.debug.print("i32 to i16: {}\n", .{small});
// Converting float to integer (truncates)
const float_val: f64 = 42.9;
const int_val: i32 = @intFromFloat(float_val);
std.debug.print("Float {d:.1} to int: {}\n", .{float_val, int_val});
// Try changing these values and conversions!
}
Undefined and Null
Zig has special values for uninitialized and optional data:
const std = @import("std");
pub fn main() !void {
// Undefined - uninitialized value (use with caution)
var uninitialized: i32 = undefined;
uninitialized = 42; // Must initialize before use
std.debug.print("Initialized value: {}\n", .{uninitialized});
// Optional types - can be null
var maybe_number: ?i32 = null;
std.debug.print("Optional is null: {}\n", .{maybe_number == null});
maybe_number = 42;
std.debug.print("Optional has value: {?}\n", .{maybe_number});
// Unwrap optional with if
if (maybe_number) |value| {
std.debug.print("The value is: {}\n", .{value});
}
}
Practice Exercises
Try these exercises in the playground above:
- Create variables of different integer types and perform arithmetic operations
- Declare a mutable variable, change its value multiple times, and print each change
- Create an optional value, set it to
null, then to a value, and print both states - Convert between different numeric types using the conversion functions
- Create boolean variables and practice logical operations (
and,or,not)
Key Takeaways
- Use
constfor immutable variables andvarfor mutable ones - Zig has explicit integer types with specific bit sizes (i8, u32, etc.)
- Type conversions must be explicit using built-in functions
- Optional types use
?to represent values that might be null - The
undefinedvalue represents uninitialized data
Next Steps
Now that you understand variables and types, let's explore how to create reusable code with functions in the next lesson.
Ready to continue? Let's learn about Functions!