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 const and var
  • 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 const by default and only use var when 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:

  1. Create variables of different integer types and perform arithmetic operations
  2. Declare a mutable variable, change its value multiple times, and print each change
  3. Create an optional value, set it to null, then to a value, and print both states
  4. Convert between different numeric types using the conversion functions
  5. Create boolean variables and practice logical operations (and, or, not)

Key Takeaways

  • Use const for immutable variables and var for 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 undefined value 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!

Variables and Data Types | LearningZig.org