Data Types
Explore C#'s built-in value types and reference types, and the difference between them.
Value Types vs Reference Types
C#'s types fall into two fundamental categories, and understanding this distinction deeply will save you from a whole class of confusing bugs later. Value types (like int, double, bool, and structs) store their actual data directly wherever the variable lives, and are copied by value — copying one creates a completely independent duplicate. Reference types (like string, arrays, and classes) store only a reference (essentially a memory address) to data that actually lives elsewhere on the heap, and are copied by reference — copying one just copies the address, so both variables point at the exact same underlying data.
Value Types
- int, double, bool, char, structs
- Typically stored on the stack
- Copying creates an independent copy
- Cannot be null (unless explicitly made nullable)
Reference Types
- string, arrays, classes, interfaces
- The actual data lives on the heap
- Copying shares the same underlying data
- Can be null by default
// Value type — copying creates an independent copyint a = 10;int b = a; // b is a separate copyb = 20;Console.WriteLine(a); // 10 — unaffected by changing b
// Reference type — copying shares the same objectint[] arr1 = { 1, 2, 3 };int[] arr2 = arr1; // arr2 points to the SAME arrayarr2[0] = 99;Console.WriteLine(arr1[0]); // 99 — arr1 sees the change too, since they're the same array!That last example — where modifying arr2 also appears to change arr1 — surprises many beginners coming from languages that copy everything by value. Once you internalize that arrays, and later custom classes, are reference types, this behavior becomes predictable rather than mysterious.
Stack vs Heap: A Closer Look
The stack is a small, extremely fast region of memory that stores value types and manages method call frames — it grows and shrinks automatically as methods are called and return, in strict last-in-first-out order. The heap is a much larger pool of memory where reference type data actually lives, managed by the garbage collector, which periodically reclaims memory for objects nothing references anymore.
This is a simplification that holds true for local variables, but it's worth knowing the full picture eventually: a value type that is itself a field inside a class (a reference type) actually lives on the heap too, embedded directly within that object's memory — "value types go on the stack" is a helpful mental model for now, not an absolute law of C#.
Full Integer Type Range
C# offers several integer sizes, each trading off memory usage against the range of values it can hold — useful to know when working with anything memory-sensitive, or interfacing with external data formats that specify an exact byte width.
| Type | Size | Range | Typical Use |
|---|---|---|---|
| sbyte | 8-bit signed | -128 to 127 | Rarely used directly |
| byte | 8-bit unsigned | 0 to 255 | Raw binary data, image pixel values |
| short | 16-bit signed | -32,768 to 32,767 | Rarely used directly |
| ushort | 16-bit unsigned | 0 to 65,535 | Rarely used directly |
| int | 32-bit signed | ≈ -2.1 billion to 2.1 billion | The default choice for whole numbers |
| uint | 32-bit unsigned | 0 to ≈ 4.2 billion | When negative values are impossible by definition |
| long | 64-bit signed | ≈ ±9.2 quintillion | Very large counts, timestamps, file sizes |
| ulong | 64-bit unsigned | 0 to ≈ 18.4 quintillion | Very large non-negative counts |
int population = 8_000_000_000; // digit separators (_) improve readability — 8 billionlong fileSizeInBytes = 5_000_000_000L; // 'L' suffix marks this as a long literalbyte redChannel = 255; // a single color channel, 0-255The underscore digit separator (8_000_000_000) was introduced in C# 7.0 (2017) purely for human readability — the compiler strips them out entirely, so 8_000_000_000 and 8000000000 compile to the exact same number.
Floating-Point Types
| Type | Size | Example | Precision |
|---|---|---|---|
| float | 32-bit | 3.14f | ~6-9 significant digits |
| double | 64-bit | 3.14159 | ~15-17 significant digits (the default) |
| decimal | 128-bit | 19.99m | 28-29 significant digits, exact for base-10 values |
`double` and `float` represent numbers in binary, which cannot exactly represent many common decimal fractions (0.1 in binary is actually a long repeating value) — this can introduce tiny rounding errors after repeated calculations. `decimal` is specifically designed to represent base-10 values exactly, which is why it is the standard choice for currency and financial calculations across the entire .NET ecosystem.
double d = 0.1 + 0.2;Console.WriteLine(d); // 0.30000000000000004 — NOT exactly 0.3!
decimal dec = 0.1m + 0.2m;Console.WriteLine(dec); // 0.3 — exact, as expectedbool and char
bool holds exactly two values, true or false, and is what every conditional statement (covered in an upcoming lesson) ultimately evaluates. char holds a single Unicode character — not just basic English letters, but characters from virtually any writing system in the world.
bool isLoggedIn = true;char grade = 'A';char currencySymbol = '€';char emoji = '🙂'; // Note: some emoji actually need two chars (a "surrogate pair") — covered as an edge case belowA C# char is internally a 16-bit UTF-16 code unit — this covers the vast majority of characters in active use worldwide, but some rarer characters and many emoji actually require two chars combined (a "surrogate pair") to represent a single visible symbol, which is why iterating a string character-by-character can occasionally split an emoji in two.
string
Despite being a reference type, `string` in C# behaves as immutable — once created, its value never changes for the entire lifetime of that string object. Any operation that appears to "modify" a string (like concatenation) actually creates and returns an entirely new string, leaving the original untouched.
string greeting = "Hello";string name = "World";string message = $"{greeting}, {name}!";Console.WriteLine(message);
string original = "cat";string upper = original.ToUpper();Console.WriteLine(original); // "cat" — unchanged!Console.WriteLine(upper); // "CAT" — a brand new stringClick Run to see what this code prints.
String Interning
.NET automatically "interns" string literals — identical literal strings across your entire program are stored only once in memory and share the same reference, purely as a memory optimization, since strings are so common and often repeated.
string a = "hello";string b = "hello";Console.WriteLine(object.ReferenceEquals(a, b)); // True — both literals point to the SAME interned stringThis interning behavior is invisible in everyday code since strings behave as immutable regardless — you never need to think about it for correctness, only if you're specifically debugging memory usage or, very rarely, using == vs ReferenceEquals in an unusual way.
Checking a Value's Type
The GetType() method (available on every value, since all C# types ultimately derive from object) reports a variable's exact runtime type — useful for debugging or logging.
int x = 42;Console.WriteLine(x.GetType()); // System.Int32 — "int" is just a shorter alias for thisConsole.WriteLine(x.GetType().Name); // Int32int, double, bool, and every other "built-in" C# type name is actually just a language-level alias for a full .NET type — int is System.Int32, string is System.String, bool is System.Boolean. Both forms are 100% interchangeable and compile to identical code; int and System.Int32 are not just similar, they are literally the same type.
Common Beginner Mistakes
Floating-point binary representation introduces tiny rounding errors — always use decimal for money, prices, and financial totals.
array2 = array1 does not create a new array — both variables now point at the identical underlying array, and changes through either are visible through both.
decimal price = 19.99; is actually a compile error — a plain 19.99 literal is inferred as double, and C# won't implicitly narrow it to decimal; you need 19.99m.
A value that could genuinely exceed ~2.1 billion (like a running total of page views across a large site) needs long, not int, or it will silently overflow.
FAQs
`double` is the default, most commonly used, and more precise; `float` uses half the memory but with less precision — mostly chosen in graphics, game development, or other performance/memory-critical code where the reduced precision is an acceptable tradeoff.
Yes — "reference type" describes how it's stored and copied (a pointer to heap data), while "immutable" describes that its value can never be changed after creation. These are two independent properties, and string happens to have both.
By default, it silently "overflows" and wraps around to a very negative number, without throwing an error — wrapping the calculation in a checked { } block makes C# throw an OverflowException instead, which is often the safer choice for critical calculations.
Yes, covered in full in the very next lesson on type conversion — some conversions happen automatically (like int to double), while others require an explicit cast (like double to int).
Key Takeaways
- Value types (int, double, bool, structs) copy independently; reference types (string, arrays, classes) copy by shared reference.
- Use decimal for money and any calculation requiring exact base-10 precision; double/float for general-purpose math and performance-sensitive code.
- string is a reference type but behaves as immutable — every "modification" actually produces a new string object.
- Built-in type names like int and string are just aliases for full .NET types (System.Int32, System.String) — both forms are 100% interchangeable.
Summary
Understanding value versus reference semantics deeply — not just memorizing which types are which — will pay off throughout the rest of this course, especially once you reach classes, objects, and collections. Next, you'll learn how to convert between these different types safely.