Type Casting & Boxing/Unboxing

Reviewed & published by Brayan K

Real programs constantly move data between types — a user types "42" as text and you need the number; a calculation produces a double and you need a whole count. This lesson covers safe and unsafe conversions, the difference between Parse and TryParse, and the subtle world of boxing and unboxing that trips up many intermediate developers.

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

Casting is like pouring liquid between containers. Pouring a small glass into a large jug (int → double) is always safe — it just fits. Pouring the big jug back into the small glass (double → int) might overflow, so C# makes you confirm with (int) that you accept spilling the extra. Boxing is putting that glass inside a sealed parcel (an object) so it can travel through a system that only handles parcels; unboxing is opening the parcel — and you must know exactly what glass to expect.

1. Implicit vs Explicit Casting

When a conversion can never lose data — a smaller type going into a larger one — C# does it for you automatically (implicit). When data could be lost, you must spell out the target type in parentheses (explicit), which is your signature accepting the consequences. The classic gotcha: (int) on a decimal truncates (chops the fraction), it does not round.

using System;

class Program
{
    static void Main()
    {
        // IMPLICIT cast — automatic, never loses data (small type -> big type)
        int    whole   = 7;
        double promoted = whole;     // int slots safely into a double
        long   wide    = whole;      // int -> long, also safe
        Console.WriteLine($"{promoted} and {wide}"); // 7 and 7

        // EXPLICIT cast — you must ask with (type); data MAY be lost.
        double pi      = 3.99;
        int    chopped = (int)pi;    // 3  — the .99 is TRUNCATED, not rounded
        Console.WriteLine($"(int)3.99 = {chopped}"); // (int)3.99 = 3

        // Truncation also throws away the sign-rounding you might expect:
        double negative = -2.7;
        Console.WriteLine($"(int)-2.7 = {(int)negative}"); // (int)-2.7 = -2

        // To ROUND properly, use Math.Round (banker's rounding by default):
        Console.WriteLine($"Round 3.99 = {Math.Round(pi)}"); // Round 3.99 = 4
    }
}

// ✅ Expected output:
//    7 and 7
//    (int)3.99 = 3
//    (int)-2.7 = -2
//    Round 3.99 = 4

2. Strings ↔ Numbers

Text and numbers are completely different types, so you can't cast between them with (int) — you convert. Going text → number uses Parse (throws on bad input) or TryParse (safe). Going number → text uses ToString, optionally with a format string like "C" or "F2". For anything a user typed, always reach for TryParse.

using System.Globalization;
using System;

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        // Text from a user is ALWAYS a string. Convert before doing maths.

        // int.Parse — works, but THROWS if the text isn't a valid number.
        string ageText = "42";
        int age = int.Parse(ageText);
        Console.WriteLine($"Parsed age: {age}"); // Parsed age: 42

        // TryParse — the SAFE version. Returns true/false, never crashes.
        string maybeNumber = "12x";
        if (int.TryParse(maybeNumber, out int value))
            Console.WriteLine($"Got {value}");
        else
            Console.WriteLine($"'{maybeNumber}' is not a number"); // this runs

        // Convert.ToInt32 handles null as 0 (Parse would throw on null).
        string nothing = null;
        Console.WriteLine($"Convert null -> {Convert.ToInt32(nothing)}"); // 0

        // Numbers -> text is easy: ToString(), optionally with a format.
        double price = 1234.5;
        Console.WriteLine(price.ToString("C"));   // £1,234.50
        Console.WriteLine(price.ToString("F2"));  // 1234.50
    }
}

// ✅ Expected output:
//    Parsed age: 42
//    '12x' is not a number
//    Convert null -> 0
//    £1,234.50
//    1234.50

Your turn. The two strings below came from a form. Convert each to the right number type, then run the calculation.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — these inputs arrived as text. Convert them safely.
        string quantityText = "6";
        string priceText    = "3.25";

        // 1) Convert quantityText to an int  (hint: int.Parse)
        int quantity = ___;        // 👉 int.Parse(quantityText)

        // 2) Convert priceText to a decimal  (hint: decimal.Parse)
        decimal unitPrice = ___;   // 👉 decimal.Parse(priceText)

        decimal total = quantity * unitPrice;
        Console.WriteLine($"{quantity} x {unitPrice:C} = {total:C}");

        // ✅ Expected output:  6 x £3.25 = £19.50
    }
}

3. Boxing & Unboxing

Because every type in C# ultimately derives from object, a value type like int can be stored in an object variable. Doing so boxes it: the value is copied onto the heap inside a little wrapper. Reading it back out unboxes it, and you must cast to the exact original type — unboxing an int as a long throws at runtime even though the number would fit.

using System;

class Program
{
    static void Main()
    {
        // BOXING: a value type is wrapped in an object on the heap.
        int number = 123;
        object boxed = number;     // implicit boxing — int -> object
        Console.WriteLine($"Boxed value: {boxed}"); // Boxed value: 123

        // UNBOXING: pull the value back out — needs an EXPLICIT cast
        // to the EXACT original type (int here, not long or double).
        int unboxed = (int)boxed;
        Console.WriteLine($"Unboxed + 1 = {unboxed + 1}"); // Unboxed + 1 = 124

        // ⚠️ Unboxing to the WRONG type throws InvalidCastException:
        // long wrong = (long)boxed;  // 💥 runtime error, even though 123 fits

        // The change after boxing does NOT travel back — boxing copies.
        number = 999;
        Console.WriteLine($"box still {boxed}, number now {number}");
        // box still 123, number now 999
    }
}

// ✅ Expected output:
//    Boxed value: 123
//    Unboxed + 1 = 124
//    box still 123, number now 999

🔎 Deep Dive: why boxing costs performance

Boxing isn't free. Each box is a heap allocation, and the garbage collector later has to clean it up. In a tight loop that boxes millions of values, this becomes a real bottleneck — the classic example was the old non-generic ArrayList, which stored everything as object and boxed every int you added.

This is exactly why generics exist. List<int> stores int values directly with no boxing, so it's both faster and type-safe. Whenever you see code converting values to object in a loop, suspect accidental boxing and prefer a generic collection.

Putting It Together: a Safe Input Summer

Here's the pattern you'll use constantly: take messy, possibly-invalid text and process only the valid parts without ever crashing.

using System;

class Program
{
    static void Main()
    {
        // A small, real pattern: validate user-style input before using it.
        string[] inputs = { "10", "twenty", "-3", "7.5", "42" };

        int sum = 0;
        foreach (string raw in inputs)
        {
            if (int.TryParse(raw, out int n))
            {
                sum += n;
                Console.WriteLine($"  ✓ '{raw}' -> {n}");
            }
            else
            {
                Console.WriteLine($"  ✗ '{raw}' skipped (not an integer)");
            }
        }
        Console.WriteLine($"Sum of valid integers: {sum}");
        // ✓ 10, ✗ twenty, ✓ -3, ✗ 7.5, ✓ 42  -> Sum: 49
    }
}

// ✅ Expected output:
//      ✓ '10' -> 10
//      ✗ 'twenty' skipped (not an integer)
//      ✓ '-3' -> -3
//      ✗ '7.5' skipped (not an integer)
//      ✓ '42' -> 42
//    Sum of valid integers: 49

🧩 Reorder Challenge

These six lines safely convert text to a number and print a doubled result. Put them in the order that compiles and prints Doubled: 50.

else
    Console.WriteLine("Not a number");
string text = "25";
if (int.TryParse(text, out int n))
    Console.WriteLine($"Doubled: {n * 2}");
// (these five lines go inside Main)
string text = "25";
if (int.TryParse(text, out int n))
    Console.WriteLine($"Doubled: {n * 2}");
else
    Console.WriteLine("Not a number");

Why: text must be declared before TryParse reads it. The if declares n via out, so the WriteLine using n must follow it. The else must come immediately after the if body — an else with no preceding if is a syntax error.

🧠 Quick Recall

1. What does this print?

double d = 7.9;
Console.WriteLine((int)d);

7 — casting double to int truncates the fraction; it does not round to 8.

2. What is the output?

object box = 5;
int x = (int)box;
Console.WriteLine(x + 10);

15 — 5 is boxed into box, then unboxed back to int as 5, and 5 + 10 = 15.

3. What prints here?

bool ok = int.TryParse("12abc", out int r);
Console.WriteLine($"{ok} {r}");

False 0 — "12abc" isn't a valid integer, so TryParse returns false and sets r to its default, 0.

Pro Tips

Common Errors (and the fix)

Mini-Challenge: Safe Average

Combine everything: parse only valid text, track a running total and count, and divide carefully to avoid integer division. The outline is in the comments.

using System;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: Safe average calculator
        // You're given scores as TEXT (some invalid). Build an average of the
        // valid ones only.
        //
        // 1. string[] scores = { "85", "ninety", "73", "100", "" };
        // 2. Loop through them; use int.TryParse to keep only real numbers.
        // 3. Track a running total (int) and a count (int) of valid scores.
        // 4. Print the average as a decimal with 1 dp: average:F1
        //    (cast carefully so you don't get integer division!)
        //
        // ✅ Example output:
        //    Valid scores: 3
        //    Average: 86.0

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

An implicit cast in C# happens automatically when:

  • The conversion can never lose data, e.g. int to double
  • You write the (type) syntax explicitly
  • Converting any number to a string
  • Whenever two types share a base class

Answer: The conversion can never lose data, e.g. int to double. Implicit casts are allowed only when no data can be lost, such as a smaller type (int) widening into a larger one (double or long).

What does the explicit cast (int)3.99 evaluate to?

  • 4
  • 3
  • 3.99
  • A compile error

Answer: 3. Casting a double to an int truncates (drops the fractional part); it does not round. So (int)3.99 is 3.

What is (int)(-2.7) in C#?

  • -3
  • -2
  • 2
  • 0

Answer: -2. Truncation chops the fractional part toward zero, so -2.7 becomes -2, not -3.

Which method should you use to convert user-supplied text to a number without risking a crash?

  • int.Parse
  • int.TryParse
  • (int) cast
  • Convert.ToDouble

Answer: int.TryParse. TryParse returns false on bad input instead of throwing, so user input can be handled gracefully. Parse throws a FormatException.

What does int.Parse("12x") do at runtime?

  • Returns 12
  • Returns 0
  • Throws a FormatException
  • Returns null

Answer: Throws a FormatException. Parse throws a FormatException when the text is not a valid integer. TryParse would instead return false.

After bool ok = int.TryParse("abc", out int r); what are ok and r?

  • true and 0
  • false and 0
  • false and -1
  • true and abc

Answer: false and 0. TryParse fails on "abc", so it returns false and sets the out variable to its default, 0.

What is boxing in C#?

  • Wrapping a value type inside an object on the heap
  • Converting a string to a number
  • Casting a double to an int
  • Splitting an array into segments

Answer: Wrapping a value type inside an object on the heap. Boxing copies a value type (like int) into an object on the heap so it can be treated as a reference type.

Given object boxed = 5; which unboxing line is valid at runtime?

  • long x = (long)boxed;
  • double x = (double)boxed;
  • int x = (int)boxed;
  • short x = (short)boxed;

Answer: int x = (int)boxed;. A boxed value must be unboxed to its exact original type. The 5 was boxed as an int, so only (int)boxed succeeds; the others throw InvalidCastException.

Why is boxing best avoided inside a tight loop?

  • It rounds numbers incorrectly
  • Each box is a heap allocation that adds GC pressure
  • It always throws an exception
  • It converts ints to strings

Answer: Each box is a heap allocation that adds GC pressure. Every box is a separate heap allocation the garbage collector must later clean up, which is slow in hot paths. Generics like List<int> avoid it.

To round 3.99 to the nearest whole number instead of truncating, you should use:

  • (int)3.99
  • Math.Round(3.99)
  • Math.Floor(3.99)
  • int.Parse("3.99")

Answer: Math.Round(3.99). Math.Round rounds to the nearest whole number (giving 4 here). An (int) cast and Math.Floor would both truncate to 3.

Continue this course

Frequently asked questions

What is the difference between implicit and explicit casting?

Implicit casting happens automatically when there is no risk of losing data, e.g. int to double. Explicit casting uses the (type) syntax and is required when data could be lost or truncated, e.g. double to int. The explicit syntax is you telling the compiler you accept the possible loss.

Should I use Parse or TryParse to convert a string to a number?

Use TryParse for any value that came from a user or external source. Parse throws a FormatException on bad input and crashes your program; TryParse returns false and lets you handle the failure gracefully without an exception.

What is boxing and unboxing in C#?

Boxing wraps a value type (like int) inside an object on the heap so it can be treated as a reference type. Unboxing extracts the value back out, and requires an explicit cast to the exact original type. Boxing allocates memory and is comparatively slow, so avoid it in hot loops — generics let you skip it entirely.

Why does (int)3.9 give 3 instead of 4?

Casting a double to an int truncates — it simply drops everything after the decimal point, it does not round. To round to the nearest whole number use Math.Round, and to always round up use Math.Ceiling.