C# is a strongly-typed language with two categories of types: value types and reference types. Understanding this distinction is crucial for effective programming in C#.
Value Types
Stored directly in memory (stack) and contain their data. When you assign a value type to another variable, a copy of the value is created.
Characteristics of Value Types
- Stored on the stack
- Memory is allocated when declared
- Memory is deallocated when they go out of scope
- Assignment creates a copy of the value
- Cannot be null (unless nullable)
Common Value Types
Integral Types
-
int(32-bit signed integer) -
long(64-bit signed integer) -
short(16-bit signed integer) -
byte(8-bit unsigned integer) -
sbyte(8-bit signed integer) -
uint(32-bit unsigned integer) -
ulong(64-bit unsigned integer) -
ushort(16-bit unsigned integer)
Floating-Point Types
-
float(32-bit floating-point) -
double(64-bit floating-point) -
decimal(128-bit decimal)
Other Value Types
-
bool(Boolean) -
char(Unicode character) -
struct(Custom value types) -
enum(Enumeration types)
// Value type examples
int age = 25; // Stored directly on stack
double salary = 50000.50;
bool isActive = true;
char grade = 'A';
// Assignment creates a copy
int original = 10;
int copy = original; // copy gets its own value
copy = 20; // original remains 10
Reference Types
Stored as references to memory locations (heap). The variable contains a reference (pointer) to the actual data, not the data itself.
Characteristics of Reference Types
- Stored on the heap
- Memory managed by garbage collector
- Assignment copies the reference, not the data
- Can be null
- Multiple variables can reference the same object
Common Reference Types
-
string(immutable sequence of characters) -
object(base type for all types) -
class(custom reference types) -
interface(contracts for classes) -
delegate(function pointers) -
array(collections of elements)
// Reference type examples
string name = "John"; // Reference to string on heap
int[] numbers = {1, 2, 3}; // Reference to array on heap
object obj = new object();
// Assignment copies the reference
string original = "Hello";
string reference = original; // Both point to same string
reference = "World"; // original still "Hello"
// But with mutable objects:
int[] array1 = {1, 2, 3};
int[] array2 = array1; // Both reference same array
array2[0] = 99; // array1[0] is also 99
Boxing and Unboxing
Boxing
Converting a value type to a reference type (object). This creates a copy of the value on the heap.
int value = 42;
object boxed = value; // Boxing: int -> object
Unboxing
Converting a reference type back to a value type. Requires explicit casting.
object boxed = 42;
int unboxed = (int)boxed; // Unboxing: object -> int
Performance Implications
Boxing and unboxing have performance costs:
- Memory allocation on heap
- Type checking at runtime
- Potential for InvalidCastException
// Avoid unnecessary boxing
ArrayList list = new ArrayList(); // Old generic collection
list.Add(42); // Boxing occurs here
// Use generic collections instead
List<int> genericList = new List<int>();
genericList.Add(42); // No boxing
Type Safety
C# is strongly typed, meaning:
- Variables must be declared with a type
- Type checking occurs at compile time
- Invalid operations are caught before runtime
- Type conversions must be explicit (when narrowing)
// Compile-time type checking
int number = 42;
// number = "text"; // Compilation error
// Explicit conversion required
double precise = 123.45;
int rounded = (int)precise; // Explicit cast
The object Type
object is the base type for all types in C#. Any type can be assigned to an object variable.
object anything = 42; // int -> object
anything = "text"; // string -> object
anything = new int[3]; // array -> object
// To use the actual type, cast back
string text = (string)anything;
Dynamic Typing with dynamic
C# also supports dynamic typing with the dynamic keyword. Type checking is deferred until runtime.
dynamic flexible = 42;
flexible = "text"; // OK at compile time
flexible = new object(); // OK at compile time
// Runtime type checking
Console.WriteLine(flexible.Length); // May throw exception
When to Use dynamic
- Interoperating with dynamic languages
- Working with COM objects
- Reflection scenarios
- When type is unknown at compile time
Nullable Value Types
Value types cannot normally be null, but with nullable types, they can represent missing or undefined values.
int? nullableInt = null; // Nullable int
double? nullableDouble = 5.5;
bool? nullableBool = null;
// Checking for null
if (nullableInt.HasValue)
{
int value = nullableInt.Value;
}
// Null coalescing operator
int defaultValue = nullableInt ?? 0;
Type Inference with var
C# can infer types from initialization expressions using the var keyword.
var number = 42; // int
var text = "hello"; // string
var list = new List<int>(); // List<int>
// Still strongly typed
number = "text"; // Compilation error
Best Practices
1. Prefer Value Types for Small, Immutable Data
// Good for value types
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
2. Use Reference Types for Large or Mutable Data
// Good for reference types
public class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
3. Avoid Unnecessary Boxing
// Bad - causes boxing
ArrayList mixed = new ArrayList();
mixed.Add(42);
mixed.Add("text");
// Good - no boxing
List<int> numbers = new List<int>();
List<string> texts = new List<string>();
4. Use var When Type is Obvious
// Good uses of var
var result = CalculateTotal();
var query = from c in customers where c.IsActive select c;
// Prefer explicit typing when clarity matters
Dictionary<string, List<int>> data = new();
Common Mistakes
1. Treating Reference Types as Value Types
// Mistake
Point p1 = new Point(1, 2);
Point p2 = p1; // Copies reference, not value
p2.X = 5; // Modifies both p1 and p2
2. Ignoring Null Reference Exceptions
string text = null;
int length = text.Length; // NullReferenceException
3. Unnecessary Boxing in Collections
// Avoid
ArrayList list = new ArrayList();
for (int i = 0; i < 1000; i++)
{
list.Add(i); // Boxing occurs 1000 times
}
Summary
Understanding C#'s type system is fundamental to writing efficient and correct code. Value types are stored directly and copied by value, while reference types are stored on the heap and copied by reference. Choose the appropriate type category based on your data's characteristics and usage patterns. Be mindful of boxing/unboxing performance implications and use nullable types when you need to represent missing values for value types.
Top comments (0)