C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
83: Value Types, Boxing, and Unboxing
I've seen this exact bug trip up plenty of junior devs during their first few weeks on a production codebase. It usually happens when someone is trying to be "flexible" with their data types by using object. Take a look at this snippet:
object myScore = 100; // This is an int
double finalScore = (double)myScore;
Console.WriteLine(finalScore);
At first glance, this looks perfectly fine. An int can easily be converted to a double, right? But if you run this, C# will throw an InvalidCastException. It doesn't matter that the numbers are compatible; the runtime is suddenly very picky about what you're doing.
The Mystery of the InvalidCastException
The problem here is that myScore isn't actually an integer anymore—it's an object. When we assigned 100 to it, C# performed something called boxing. It took the value 100, wrapped it in a little "box" on the heap, and gave us a reference to that box.
When we try to cast it back using (double)myScore, we are unboxing. Here is the golden rule of unboxing: you must unbox the value to its exact original type. You cannot unbox an int directly into a double, even though you can cast a raw int to a double. The runtime looks inside the box, sees an int, sees you asking for a double, and decides to crash rather than guess what you want.
Unboxing to the Original Type
To fix this, you have to unbox the value to its original type first, and then perform the conversion. I usually handle it like this:
object myScore = 100;
// Unbox to int first, then C# handles the implicit conversion to double
double finalScore = (int)myScore;
Console.WriteLine(finalScore); // Now it works!
Alternatively, if you aren't 100% sure what's in the box, you can use the as operator (though that only works for reference types) or, more appropriately for value types, the is pattern matching introduced in newer versions of C#:
if (myScore is int intValue)
{
double finalScore = intValue;
Console.WriteLine(finalScore);
}
What's Actually Happening Under the Hood
To really get a handle on this, you need to understand the split between Value Types and Reference Types. Value types (like int, bool, double, and structs) usually live on the stack. They are fast, lightweight, and cleaned up the moment the method they are in finishes executing.
Reference types (like string, class, and object) live on the heap. They require more overhead because the Garbage Collector (GC) has to track them and clean them up later.
- Boxing: This is the process of converting a value type to the type
object. The CLR allocates a new object on the heap and copies the value into it. - Unboxing: This is extracting the value type from the object. The CLR checks the type in the box to make sure it matches the requested type and then copies the value back to the stack.
The Hidden Cost of Boxing
You might be wondering, "Why not just use object for everything to keep it flexible?" Because boxing is expensive. I've spent hours profiling applications only to find that a tight loop was boxing integers millions of times per second, causing the Garbage Collector to run constantly and freezing the UI.
Every time you box a value, you're allocating memory on the heap. If you do this in a loop, you're creating a mountain of temporary objects that the GC has to climb. This is exactly why Generics (which we'll cover in detail later) were invented. Generics allow us to write flexible code without the performance penalty of boxing. If you find yourself boxing and unboxing frequently, it's usually a sign that your data structure needs a rethink.
📋 Practical Task
Exercise: Building a Type-Safe Configuration Registry
You are building a simple configuration system that stores settings of various types (integers, booleans, and strings) using a Dictionary<string, object>. Your goal is to implement a retrieval method that handles unboxing safely to avoid the InvalidCastException we discussed.
Requirements:
- Create a class called
ConfigRegistry. - Add a private field:
Dictionary<string, object> _settings = new Dictionary<string, object>(); - Implement a method
void SetValue(string key, object value)to add items to the dictionary. - Implement a generic method
T GetValue<T>(string key). This method should:- Check if the key exists in the dictionary.
- If it exists, attempt to cast/unbox the
objectto typeT. - If the cast fails or the key doesn't exist, return the
defaultvalue for typeT.
Test Case:
var registry = new ConfigRegistry();
registry.SetValue("Timeout", 30); // Boxed int
registry.SetValue("Enabled", true); // Boxed bool
int timeout = registry.GetValue<int>("Timeout"); // Should return 30
bool enabled = registry.GetValue<bool>("Enabled"); // Should return true
int missing = registry.GetValue<int>("Volume"); // Should return 0 (default for int)
string wrongType = registry.GetValue<string>("Timeout"); // Should return null (default for string) instead of crashingThere are no comments for now.