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
| Type | Size | Range | Default |
|---|---|---|---|
sbyte / byte | 1 byte | -128 to 127 / 0 to 255 | 0 |
short / ushort | 2 bytes | ±32,767 / 0 to 65,535 | 0 |
int / uint | 4 bytes | ±2.1 billion / 0 to 4.2 billion | 0 |
long / ulong | 8 bytes | Extremely large / 0 to huge | 0L |
float | 4 bytes | 7 decimal digits precision | 0.0f |
double | 8 bytes | 15-16 decimal digits precision | 0.0d |
decimal | 16 bytes | 28-29 decimal digits (Financial) | 0.0m |
bool | 1 byte | true or false | false |
char | 2 bytes | Single 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
byteorlongif 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:
- 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.
- 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.
- Boxing: The runtime "wraps" the value type inside an
objectinstance and moves it to the heap. - 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
| Feature | Value Types | Reference Types |
|---|---|---|
| Location | Stack | Heap (Reference on Stack) |
| Assignment | Copies the data | Copies the reference (address) |
| Default | 0, false, \0 | null |
| Cleanup | Automatic (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:
- Read-Only: It provides a forward-only view of data.
- Lazy Evaluation: It doesn't necessarily hold all data in memory at once; it can fetch items one at a time.
- 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.