LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 920 min read

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
Seeing the difference in practice
// Value type — copying creates an independent copy
int a = 10;
int b = a; // b is a separate copy
b = 20;
Console.WriteLine(a); // 10 — unaffected by changing b
// Reference type — copying shares the same object
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // arr2 points to the SAME array
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 99 — arr1 sees the change too, since they're the same array!
A Genuinely Common Source of Bugs

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.

Value type declared
↓
Stored directly on the stack
↓
Method returns
↓
Stack memory automatically reclaimed instantly
Fun Fact

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.

TypeSizeRangeTypical Use
sbyte8-bit signed-128 to 127Rarely used directly
byte8-bit unsigned0 to 255Raw binary data, image pixel values
short16-bit signed-32,768 to 32,767Rarely used directly
ushort16-bit unsigned0 to 65,535Rarely used directly
int32-bit signed≈ -2.1 billion to 2.1 billionThe default choice for whole numbers
uint32-bit unsigned0 to ≈ 4.2 billionWhen negative values are impossible by definition
long64-bit signed≈ ±9.2 quintillionVery large counts, timestamps, file sizes
ulong64-bit unsigned0 to ≈ 18.4 quintillionVery large non-negative counts
int population = 8_000_000_000; // digit separators (_) improve readability — 8 billion
long fileSizeInBytes = 5_000_000_000L; // 'L' suffix marks this as a long literal
byte redChannel = 255; // a single color channel, 0-255
Fun Fact

The 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

TypeSizeExamplePrecision
float32-bit3.14f~6-9 significant digits
double64-bit3.14159~15-17 significant digits (the default)
decimal128-bit19.99m28-29 significant digits, exact for base-10 values
Use decimal for Money

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

Seeing the floating-point rounding problem directly
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 expected

bool 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 below
Fun Fact

A 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 string
Output

Click 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 string
Fun Fact

This 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 this
Console.WriteLine(x.GetType().Name); // Int32
Fun Fact

int, 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

Using double for currency calculations

Floating-point binary representation introduces tiny rounding errors — always use decimal for money, prices, and financial totals.

Assuming reference type assignment copies the data

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.

Forgetting the m suffix on a decimal literal

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.

Choosing int for everything without considering range

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.

Next Lesson →

Type Conversion