C# Variables & Data Types

C# Variables & Data Types

C# is a statically-typed and type-safe language. This means every variable and constant has a type, and the compiler ensures that you don't perform operations that are incompatible with those types.


The Two Pillars: Value Types vs. Reference Types

Understanding the difference between these two is fundamental to mastering C# memory management.

1. Value Types (Stored on the Stack)

Value types hold their data directly within the memory allocated to them. They are typically stored on the Stack, which is a "Last-In, First-Out" (LIFO) memory structure that is incredibly fast because the CPU manages it automatically as variables go in and out of scope.

Built-in Primitive Types

TypeSizeRangeDefault
sbyte / byte1 byte-128 to 127 / 0 to 2550
short / ushort2 bytes±32,767 / 0 to 65,5350
int / uint4 bytes±2.1 billion / 0 to 4.2 billion0
long / ulong8 bytesExtremely large / 0 to huge0L
float4 bytes7 decimal digits precision0.0f
double8 bytes15-16 decimal digits precision0.0d
decimal16 bytes28-29 decimal digits (Financial)0.0m
bool1 bytetrue or falsefalse
char2 bytesSingle Unicode character\0

Custom Value Types: Structs

A struct is a value type that is typically used for small, lightweight objects that act like primitive values (e.g., a Point, a Color, or a ComplexNumber).

  • Memory: Unlike classes, structs do not require "garbage collection" because they are destroyed immediately when the function they were created in finishes.
  • Copying: When you assign one struct to another, the entire "blob" of data is copied.
public struct Point {
    public int X;
    public int Y;
}

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // p2 is a complete copy of p1
p2.X = 99;     // p1.X is still 10

Named Constants: Enums

An enum is a distinct value type that consists of a set of named constants. By default, the underlying type of each element in the enum is int.

  • Underlying Type: You can change the underlying type to byte or long if needed.
  • Readability: Enums make code much more readable than using "magic numbers."
enum ProjectStatus : byte {
    Draft = 0,
    InProgress = 1,
    UnderReview = 2,
    Completed = 3
}

ProjectStatus current = ProjectStatus.InProgress;

Memory Lifecycle

Because value types live on the stack, they are automatically cleaned up the moment the code execution leaves the block (curly braces { }) where they were defined. This makes them highly efficient for short-lived data.

Key Behavior Summary:

int a = 5;
int b = a; // 'b' is a brand new location in memory with the value 5
b = 10;    // 'a' is untouched.

2. Reference Types (Stored on the Heap)

Reference types do not hold the data itself. Instead, they store a memory address (a reference) that points to a location on the Managed Heap. The Heap is a large, shared pool of memory used for objects that need a longer or more flexible lifecycle.

Common Reference Types

  • Classes: The blueprint for objects (e.g., string, object, and custom classes).
  • Interfaces: Contracts that define "what" a class can do, but not "how."
  • Delegates: References to methods, often used for events and callbacks.
  • Arrays: Even if the elements are value types (like int[]), the array itself is an object on the heap.
  • Records: A modern C# feature (C# 9.0+) for immutable data-centric objects.

Memory Management: The Garbage Collector (GC)

Unlike value types, which are cleaned up immediately when they go out of scope, reference types stay on the heap until the Garbage Collector determines they are no longer being used. The GC runs periodically in the background, identifying "dead" objects and reclaiming their memory.

The Special Case: string

While string is a reference type, it has a few unique behaviors:

  1. Immutability: Once a string is created, it cannot be changed. Any operation that seems to "modify" a string actually creates a brand new string in memory.
  2. Equality: When you compare two strings using ==, C# compares their content rather than their memory address, making them feel like value types.

The Role of null

Because reference types store a memory address, they can be set to null, meaning they "point to nothing." Attempting to access a member of a null reference results in the infamous NullReferenceException.

string name = null; // Valid for reference types
// Console.WriteLine(name.Length); // This would crash the program!

Boxing and Unboxing: The Bridge Between Types

Boxing is the process of converting a Value Type into a Reference Type so it can be stored on the heap. Unboxing is the reverse.

  1. Boxing: The runtime "wraps" the value type inside an object instance and moves it to the heap.
  2. Unboxing: The runtime "extracts" the value type from the object and moves it back to the stack.
int i = 123;      // Value Type (Stack)
object o = i;     // Boxing: i is copied to the Heap
int j = (int)o;   // Unboxing: the value is copied back to the Stack

Performance Note: Boxing and unboxing are computationally expensive. Modern C# uses Generics to avoid boxing in almost all scenarios.


Modern Reference Types: Records (C# 9.0+)

A record is a special kind of class that provides built-in functionality for encapsulating data. They are designed to be Immutable by default and provide Value-based Equality (meaning two different record objects are equal if their data is the same).

public record Person(string FirstName, string LastName);

var person1 = new Person("Jane", "Doe");
var person2 = new Person("Jane", "Doe");

Console.WriteLine(person1 == person2); // True (Value-based equality)

Memory Comparison Summary

FeatureValue TypesReference Types
LocationStackHeap (Reference on Stack)
AssignmentCopies the dataCopies the reference (address)
Default0, false, \0null
CleanupAutomatic (Instant)Garbage Collector (Periodically)

Key Behavior Summary:

class Student { public string Name; }

Student s1 = new Student { Name = "Alice" };
Student s2 = s1; // s2 points to the EXACT SAME student as s1
s2.Name = "Bob"; // s1.Name is now also "Bob"!

Nullable Types

Value types cannot normally be null. However, C# provides Nullable Types using the ? syntax.

int? age = null; // Valid
if (age.HasValue) 
{
    Console.WriteLine(age.Value);
}

Generics: Type-Safe Templates

Generics allow you to write code that works with any data type while maintaining type safety and performance.

Instead of writing a different class for int and string, you use a placeholder <T>:

public class Box<T> 
{
    public T Content { get; set; }
}

var intBox = new Box<int> { Content = 123 };
var strBox = new Box<string> { Content = "Hello" };

Collections & Data Structures

C# provides a rich set of collections in the System.Collections.Generic namespace.

1. Arrays (Fixed Size)

The most basic collection. Once created, its size cannot change.

string[] fruits = { "Apple", "Banana", "Cherry" };

2. List (Dynamic Size)

The most commonly used collection. It grows automatically as you add items.

List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
Console.WriteLine(names.Count); // 2

3. Dictionary<TKey, TValue>

A collection of key-value pairs for fast lookups.

Dictionary<string, string> capitals = new Dictionary<string, string>();
capitals["USA"] = "Washington D.C.";
capitals["France"] = "Paris";

Enumerables and IEnumerable<T>

IEnumerable<T> is the most important interface in C# collections. It represents a collection of objects that can be iterated over (one by one).

The Power of IEnumerable:

  1. Read-Only: It provides a forward-only view of data.
  2. Lazy Evaluation: It doesn't necessarily hold all data in memory at once; it can fetch items one at a time.
  3. LINQ Foundation: This interface is what allows you to use LINQ (Language Integrated Query) to filter and transform data.
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// Any collection (List, Array, etc.) can be treated as an IEnumerable
foreach (var num in numbers) 
{
    Console.WriteLine(num);
}

Advanced Syntax: var and Type Inference

The var keyword tells the compiler to figure out the type based on the right-hand side.

var scores = new List<int>(); // Compiler knows this is List<int>

Note: var is still statically typed. Once inferred, the type cannot change.