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.
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.
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.