<< Home

Chapter 3: Types and Memory Model

C# uses a type system that blends clarity with safety. Some types live directly on the stack while others are managed on the heap by the runtime. Understanding this split helps you predict performance, memory behaviour, and the cost of certain operations. This chapter walks through the major categories, their rules, and the patterns they encourage.

Value types vs reference types

Value types store their data directly. When you assign one value type to another, the data is copied. Types such as int, double, bool, and most struct types belong to this category. They often live on the stack which keeps access fast and allocation predictable.

Reference types store a reference to data that lives elsewhere. Classes, arrays, delegates, and most objects fall into this group. When you assign a reference type variable to another variable you copy the reference rather than the underlying object. This means both variables point to the same data.

💡 You can think of value types as postcards and reference types as postcards containing directions to a house. Copying a postcard gives someone new directions but copying the house would be far more expensive.

Built-in numeric types and ranges

C# provides a family of numeric types arranged by size and purpose. These include signed and unsigned integers, floating point types, and high precision decimals. Each type has a defined range and size which helps the compiler catch accidental overflow or misuse.

A common subset includes byte, short, int, long, float, double, and decimal. Integers suit counting and indexing while floating point types handle scientific and graphics calculations. The decimal type provides extra precision for financial work.

Type Size Typical use
int 32 bits General integer work
long 64 bits Larger integer ranges
double 64 bits Floating point calculations
decimal 128 bits Financial precision

Choosing the correct numeric type is often a balance between range, performance, and precision.

Strings, chars, and immutability

Strings in C# are immutable. Once created, their contents cannot change. Operations that appear to modify a string actually create a new one. This feature helps avoid subtle bugs and keeps code predictable although it means repeated string manipulation can become expensive.

The char type represents a single UTF-16 code unit which is not always identical to a complete character since some Unicode characters require multiple units. When you need flexible text manipulation, consider types such as StringBuilder which can grow and change more efficiently.

Nullable value types

Value types normally cannot hold null. However, nullable value types allow you to represent the absence of a value by wrapping the type in ?. For example, int? represents an integer that may or may not be present. This is useful when values are optional or when working with data from external sources.

You can test nullable values using the HasValue property or by checking against null. When accessing the underlying value you use the Value property or the null coalescing operator to provide a fallback.

⚠️ Accessing Value when the nullable holds no data results in a runtime exception. Using the null coalescing operator is usually safer.

Records and with-expressions

Records provide a concise way to define immutable types that represent data. They automatically generate useful behaviour such as value-based equality and string representations. A record can define its properties in a compact form that makes intent easy to read.

With-expressions allow you to create a new record based on an existing one while changing specific properties. This approach keeps immutability intact and avoids the need for large constructors when only one or two fields change.

Span, ReadOnlySpan, and memory safety

Span<T> and ReadOnlySpan<T> provide views over contiguous memory areas without creating new arrays or copying data. They make high performance operations possible while keeping safety checks in place. These types can point into arrays, stack allocated memory, or even slices of larger structures.

Because they refer directly to memory regions the runtime restricts their usage so they cannot escape into long lived objects. This rule protects you from dangling references. In practice, Span<T> helps you write code that behaves like low level memory manipulation while retaining the guardrails of the managed environment.