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

💡 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

TypeSizeRange / PrecisionUse for
byte1 byte0 to 255Raw bytes, colour channels
short2 bytes±32,767Memory-tight small numbers
int4 bytes±2.1 billionThe default whole number
long8 bytes±9.2 quintillionIDs, file sizes, ticks
float4 bytes~7 digitsGraphics, games (speed)
double8 bytes~15 digitsScience, averages
decimal16 bytes~28 digits, exactMoney & 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 66

Your 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:  255

Push 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

Common Errors (and the fix)

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

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

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.