Data Types
Reviewed & published by Brayan K
You already met a handful of types in the Variables lesson. Now we'll go deeper: the full family of numeric types, how text and booleans are stored, the all-important difference between value types and reference types, and how to pick the right type every single time. Choosing well here prevents whole categories of bugs later.
Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- The integer family: byte, short, int, long (and when to use each)
- Floating point vs decimal: float, double, decimal
- How char and string store text, including Unicode
- Value types vs reference types — and why it matters
- Default values, MinValue and MaxValue
- Numeric literal suffixes (m, f, L) and underscore separators
💡 Real-World Analogy
Think of data types as shipping boxes of different sizes. A tiny jewellery box (byte) is cheap but only fits something small (0–255). A standard parcel box (int) fits almost anything you'll ship day to day. A freight crate (long) is for the rare huge item. You could ship a ring in a freight crate, but it wastes space — and you can't cram a fridge into a jewellery box. Picking the right-sized box for the job is exactly what choosing a type is.
1. The Numeric Types
C# splits numbers into integers (no decimal point) and real numbers (with a decimal point). Within each group there are several sizes, trading memory for range or precision. You'll use int for nearly all whole numbers, double for general decimals, and decimal for money. Run the example and watch how the suffixes change the type.
using System;
class Program
{
static void Main()
{
// === Integer (whole-number) types ===
byte age = 200; // 0 to 255 (1 byte)
short year = 2026; // -32,768 to 32,767 (2 bytes)
int views = 1_500_000; // ±2.1 billion (4 bytes)
long bigCount = 9_000_000_000;// huge whole numbers (8 bytes)
// Underscores are just visual separators — they're ignored.
Console.WriteLine($"byte: {age}"); // byte: 200
Console.WriteLine($"short: {year}"); // short: 2026
Console.WriteLine($"int: {views}"); // int: 1500000
Console.WriteLine($"long: {bigCount}"); // long: 9000000000
// === Floating-point & exact decimal types ===
float ratio = 1.5f; // ~7 digits precision (suffix f)
double average = 3.14159265; // ~15 digits (default)
decimal price = 19.99m; // exact base-10 (suffix m)
Console.WriteLine($"float: {ratio}"); // float: 1.5
Console.WriteLine($"double: {average}"); // double: 3.14159265
Console.WriteLine($"decimal: {price}"); // decimal: 19.99
}
}
// ✅ Expected output:
// byte: 200
// short: 2026
// int: 1500000
// long: 9000000000
// float: 1.5
// double: 3.14159265
// decimal: 19.99📊 Numeric Type Cheat Sheet
| Type | Size | Range / Precision | Use for |
|---|---|---|---|
| byte | 1 byte | 0 to 255 | Raw bytes, colour channels |
| short | 2 bytes | ±32,767 | Memory-tight small numbers |
| int | 4 bytes | ±2.1 billion | The default whole number |
| long | 8 bytes | ±9.2 quintillion | IDs, file sizes, ticks |
| float | 4 bytes | ~7 digits | Graphics, games (speed) |
| double | 8 bytes | ~15 digits | Science, averages |
| decimal | 16 bytes | ~28 digits, exact | Money & finance |
2. Text (char, string) and bool
A char holds exactly one character in 'single quotes'; a string holds any amount of text in "double quotes". A bool is the simplest type of all — it is only ever true or false. One neat detail: a char is really just a number under the hood (its Unicode code point), which is why it can implicitly become an int.
using System;
class Program
{
static void Main()
{
// char -> a SINGLE Unicode character in 'single quotes'
char initial = 'B';
char symbol = '\u00A3'; // the £ pound sign by its Unicode code
// string -> a sequence of characters in "double quotes"
string fullName = "Brenda Sellers";
// bool -> only ever true or false (lowercase keywords)
bool isActive = true;
bool isExpired = false;
Console.WriteLine($"Initial: {initial}"); // Initial: B
Console.WriteLine($"Symbol: {symbol}"); // Symbol: £
Console.WriteLine($"Name: {fullName}"); // Name: Brenda Sellers
Console.WriteLine($"Active: {isActive}"); // Active: True
Console.WriteLine($"Expired: {isExpired}");// Expired: False
// A char is secretly a number — its Unicode code point.
int code = initial; // implicit char -> int
Console.WriteLine($"'B' has code {code}"); // 'B' has code 66
}
}
// ✅ Expected output:
// Initial: B
// Symbol: £
// Name: Brenda Sellers
// Active: True
// Expired: False
// 'B' has code 66Your turn. For each variable below, replace the ___ with the correct type keyword. The comments tell you the answer if you get stuck — but try first.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — pick the RIGHT type for each value, then fill the ___.
// 1) A person's age in whole years
___ age = 34; // 👉 int
// 2) A bank balance that must be exact to the penny
___ balance = 1052.40m; // 👉 decimal (note the m suffix!)
// 3) A single grade letter
___ grade = 'A'; // 👉 char (single quotes)
// 4) Whether the account is verified
___ verified = true; // 👉 bool
Console.WriteLine($"Age {age}, balance {balance:C}, grade {grade}, verified {verified}");
// ✅ Expected: Age 34, balance £1,052.40, grade A, verified True
}
}3. Value Types vs Reference Types
This is the single most important idea in the lesson. Value types (all the numbers, bool, char, and structs) store their data directly — copying one makes an independent copy. Reference types (string, arrays, and every class) store a reference that points to the real data elsewhere, so copying the variable just copies the pointer, and both variables can see the same object. Misunderstanding this causes "why did my other variable change?!" bugs.
using System;
class Program
{
static void Main()
{
// VALUE type: copying copies the DATA. The two are independent.
int a = 10;
int b = a; // b gets its own copy of 10
b = 99; // changing b does NOT touch a
Console.WriteLine($"a = {a}, b = {b}"); // a = 10, b = 99
// REFERENCE type: copying copies the POINTER. Both see one array.
int[] first = { 1, 2, 3 };
int[] second = first; // second points at the SAME array
second[0] = 99; // mutating through second...
Console.WriteLine($"first[0] = {first[0]}"); // first[0] = 99 (!)
// 'string' is a reference type but IMMUTABLE, so it feels like a value.
string name = "abc";
string copy = name;
copy = copy.ToUpper(); // makes a NEW string, original unchanged
Console.WriteLine($"name = {name}, copy = {copy}"); // name = abc, copy = ABC
}
}
// ✅ Expected output:
// a = 10, b = 99
// first[0] = 99
// name = abc, copy = ABC🔎 Deep Dive: defaults, limits, and overflow
Every type has a default value used when a field isn't assigned: 0 for numbers, false for bool, and null for reference types. You can read it with default(int). Each numeric type also exposes its limits via int.MinValue and int.MaxValue.
using System;
class Program
{
static void Main()
{
// Every type has a 'default' value used when nothing is assigned.
Console.WriteLine($"default(int): {default(int)}"); // 0
Console.WriteLine($"default(double): {default(double)}"); // 0
Console.WriteLine($"default(bool): {default(bool)}"); // False
Console.WriteLine($"default(char): [{default(char)}]"); // [ ] the null char
// For string (a reference type) the default is null -> prints empty:
string s = default;
Console.WriteLine($"default(string): [{s}]"); // []
// MinValue / MaxValue tell you the limits of a numeric type.
Console.WriteLine($"int range: {int.MinValue} .. {int.MaxValue}");
// int range: -2147483648 .. 2147483647
Console.WriteLine($"byte max: {byte.MaxValue}"); // 255
}
}
// ✅ Expected output:
// default(int): 0
// default(double): 0
// default(bool): False
// default(char): [␀]
// default(string): []
// int range: -2147483648 .. 2147483647
// byte max: 255Push a value past int.MaxValue and it silently overflows (wraps to a negative number) by default. Wrap arithmetic in a checked { ... } block to make overflow throw an exception instead of failing silently.
🧩 Reorder Challenge
These five lines were shuffled. Put them in the order that compiles and prints Total: £40.00. Think about what must exist before it can be used.
Console.WriteLine($"Total: {total:C}");
decimal unitPrice = 10.00m;
decimal total = unitPrice * qty;
int qty = 4;
// (these four lines go inside Main)int qty = 4;
decimal unitPrice = 10.00m;
decimal total = unitPrice * qty;
Console.WriteLine($"Total: {total:C}");Why: a variable must be declared before it is read. total depends on both qty and unitPrice, so those two must come first. The WriteLine uses total, so it must come last. Note int * decimal safely promotes to decimal, which is why the multiplication is allowed.
🧠 Quick Recall
1. What does this print?
char c = 'A';
int n = c;
Console.WriteLine(n);65 — a char implicitly converts to its Unicode code point, and 'A' is 65.
2. What is the output?
int x = 5;
int y = x;
y = 20;
Console.WriteLine($"{x} {y}");5 20 — int is a value type, so y got an independent copy. Changing y never touches x.
3. What prints here?
Console.WriteLine(default(bool));
Console.WriteLine(default(int));False then 0 — the default for bool is false and for int is 0.
Pro Tips
- 💡 Default to int for whole numbers and double for decimals unless you have a specific reason to change.
- 💡 Always decimal for money. Never store currency in double — binary rounding will lose pennies.
- 💡 Use underscores in big literals: 1_000_000 is far easier to read than 1000000.
- 💡 Remember: string is immutable. Methods like Trim() return a new string; they never change the original.
Common Errors (and the fix)
- "CS0266: Cannot implicitly convert 'long' to 'int'": the value is too big for int. Use long, or cast if you're certain it fits.
- Assigning 19.99 to a decimal: a bare decimal literal is a double. You must write 19.99m.
- Surprised by overflow: byte b = 256; won't even compile, but byte b = (byte)256; wraps to 0. Pick a bigger type.
- Mutating a shared array by accident: two variables pointing to one array means a change through one is visible through the other — clone with (int[])arr.Clone() if you need a separate copy.
Mini-Challenge: Product Line
Model a product using the right type for every field. The outline is in the comments — declare the variables, compute inStock from stockCount, and print the two-line summary.
using System;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: Product line for an online shop
// Choose the BEST type for each piece of data, then print a summary.
//
// 1. productName -> the text "Wireless Mouse"
// 2. sku -> a single category letter, e.g. 'E' (electronics)
// 3. stockCount -> a whole number, e.g. 47
// 4. price -> money, e.g. 24.99 (must be exact!)
// 5. inStock -> true/false depending on stockCount > 0
//
// ✅ Example output:
// Wireless Mouse [E]
// 47 in stock at £24.99 — Available: True
// your code here
}
}🎉 Lesson Complete
- ✅ Integers come in sizes: byte, short, int, long — default to int
- ✅ Real numbers: float, double (general) and decimal (money, exact)
- ✅ char is one character; string is text; bool is true/false
- ✅ Value types copy data; reference types copy a pointer to shared data
- ✅ Every type has a default, and numeric types expose MinValue/MaxValue
Practice quiz
Which integer type is the recommended default for whole numbers?
- byte
- short
- int
- long
Answer: int. int (4 bytes, about ±2.1 billion) is the natural default for whole numbers on modern hardware.
What is the default value of an int field?
- 0
- null
- -1
- 1
Answer: 0. Numeric types default to 0; default(int) is 0.
What is the default value of a bool?
- true
- 0
- null
- false
Answer: false. A bool defaults to false; default(bool) prints False.
Which group does string belong to?
- Value type
- Reference type
- Numeric type
- Enum
Answer: Reference type. string is a class, so it is a reference type — but it is immutable, which makes it feel value-like.
Copying a value type (like int b = a;) does what?
- Copies the data into an independent variable
- Copies a pointer so both share data
- Throws an error
- Makes b reference a
Answer: Copies the data into an independent variable. Value types copy the data, so b is independent — changing b never affects a.
After int[] second = first; changing second[0] also changes first[0]. Why?
- Arrays are value types
- first was copied
- Both variables reference the same array on the heap
- second is read-only
Answer: Both variables reference the same array on the heap. Arrays are reference types, so the assignment copies the pointer — both names point at one array.
What is the largest value a byte can hold?
- 127
- 255
- 1000
- 65535
Answer: 255. A byte holds 0 to 255 (one byte).
What is the result of int n = 'A'; where 'A' has code point 65?
- It won't compile
- 0
- An 'A' character
- 65
Answer: 65. A char implicitly converts to its Unicode code point, so n becomes 65.
Which type is best for an exact bank balance to the penny?
- double
- decimal
- float
- int
Answer: decimal. decimal stores base-10 amounts exactly, so it is correct for money; double can drift.
What do underscores in a literal like 1_500_000 do?
- Multiply the number
- Act as decimal points
- Are visual separators that are ignored
- Cause a compile error
Answer: Are visual separators that are ignored. Underscores are purely visual digit separators and are ignored by the compiler.
Continue this course
- Previous: Variables & Data Types
- Next: Type Casting & Boxing — Implicit/explicit casts, Convert, and boxing/unboxing
- Quick reference: C# cheat sheet
Frequently asked questions
What is the difference between a value type and a reference type in C#?
Value types (int, double, bool, char, struct, enum) store their data directly and live on the stack when local. Reference types (string, arrays, classes) store a reference that points to data on the managed heap. Copying a value type copies the data; copying a reference type copies the pointer, so two variables can see the same object.
Why is string a reference type but behaves like a value?
string is a class (reference type) but it is immutable, so once created it never changes. Methods like ToUpper() return a brand-new string. Because you can never mutate it in place, it feels like a value type even though it lives on the heap.
Should I use int or long for whole numbers?
Use int by default — it covers roughly -2.1 billion to +2.1 billion and is the natural integer size on modern hardware. Reach for long only when you genuinely need values larger than that, such as file sizes in bytes, timestamps in ticks, or database IDs.
What does the m, f, or d suffix on a number mean?
Numeric literals default to int (whole) or double (decimal point). The suffix forces a type: 10m is a decimal, 10f is a float, 10d is a double, 10L is a long, 10u is uint. The m suffix is the one you will use most because money should always be decimal.