Mid-level 6 min · March 06, 2026

C# Data Types — Silent Truncation Breaks Your Math

Double-to-int casting dropped age 3.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • C# uses strong static typing: every variable's type is fixed at compile time
  • Value types (int, bool, struct) store data directly; reference types (class, string) store a memory address
  • Use decimal for money, not double — binary floating point causes rounding errors
  • int.Parse() throws on bad input; int.TryParse() returns false gracefully
  • var is shorthand, not dynamic — the type is still fixed at compile time
Plain-English First

Think of a variable like a labelled box in your bedroom. You decide what the box is for — shoes only, books only, or loose change — and that decision is the data type. Once you label a box 'shoes', you can't stuff a book in it without consequences. C# works exactly the same way: you name a box (the variable), tell the program what kind of thing goes inside (the data type), and from that point on the program holds you to it. That structure is what stops your program from accidentally putting a person's age where their name should be.

Every program ever written — from a simple calculator to a banking system — has one job in common: it has to remember things while it's running. It needs to remember a user's name, their account balance, whether they're logged in, and a hundred other details. Without a way to store and label that information, your code is just a series of commands that forgets everything the moment it moves on. Data types and variables are the very first tool that gives your program memory.

The problem they solve is precision. Imagine a hospital system that stores a patient's age as text instead of a number. Suddenly you can't calculate their dosage, you can't sort patients by age, and you can't do any arithmetic at all. Data types prevent that chaos by forcing you — and the compiler — to agree upfront on exactly what kind of information each piece of data is. C# is a strongly-typed language, which means it checks these rules before your code even runs, catching bugs at compile time rather than in production.

By the end of this article you'll know how to declare any variable in C#, choose the right data type for the job, understand the critical difference between value types and reference types, and avoid the three mistakes that trip up almost every beginner. You'll have runnable code you can drop straight into a project and a mental model that will stick with you for years.

What Is a Variable and How Do You Declare One in C#?

A variable is a named slot in your computer's memory that holds a value. You create one by writing the data type first, then the name you want to give it, and optionally an initial value. That three-part pattern — type, name, value — is the foundation of almost every line of C# you'll ever write.

The name you choose matters. Call it something that describes what it holds. playerScore tells you everything; s tells you nothing. C# names are case-sensitive, so PlayerScore and playerScore are two completely different variables — a fact that causes real bugs when you're not careful.

You can declare a variable and assign it later, or do both at once. If you declare without assigning, C# won't let you read it until you give it a value — this is the compiler protecting you from reading garbage memory. Think of it like the labelled box again: the box exists on your shelf, but you can't ship it until you actually put something inside.

C# also gives you the var keyword, which lets the compiler figure out the type from whatever you assign. It's not 'no type' — it's shorthand. The type is still locked in at compile time; you just don't have to write it out explicitly.

VariableDeclarations.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;

class VariableDeclarations
{
    static void Main()
    {
        // Explicit declaration: type is written out by hand
        // 'int' means whole numbers only — no decimals
        int playerScore = 0;

        // String holds text — always wrap text values in double quotes
        string playerName = "Alex";

        // bool holds only true or false — perfect for on/off flags
        bool isLoggedIn = true;

        // 'var' lets the compiler infer the type from the right-hand side
        // The compiler sees 3.14 and decides this is a 'double'
        var piApproximation = 3.14;

        // You can declare first and assign later
        // But you CANNOT read it before assigning — the compiler will refuse
        int livesRemaining;
        livesRemaining = 3; // assignment happens here, now it's safe to use

        // Printing the values to the console so we can see what's stored
        Console.WriteLine($"Player: {playerName}");
        Console.WriteLine($"Score: {playerScore}");
        Console.WriteLine($"Lives: {livesRemaining}");
        Console.WriteLine($"Logged in: {isLoggedIn}");
        Console.WriteLine($"Pi approx: {piApproximation}");
    }
}
Output
Player: Alex
Score: 0
Lives: 3
Logged in: True
Pi approx: 3.14
Pro Tip: Use var for long generic types, explicit types everywhere else
Reach for var when the type is obvious from the right-hand side (e.g. var message = "Hello") or when the type name is painfully long (e.g. Dictionary<string, List<int>>). For simple primitive types like int or bool, write the type explicitly — it makes your code easier to read for the next person, which is usually you, six months later.
Production Insight
A variable declared but not assigned before reading won't compile — that's C# protecting you. But if you declare with an initial value and later the value is not reassigned, consider marking it const or readonly.
The compiler enforces definite assignment for local variables, but fields default to their type's zero value unless you set them. That silent default can mask bugs when you think a field started as null.
Rule: always initialize local variables explicitly, and treat field defaults as a code smell — they make your intent unclear.
Key Takeaway
Declaration pattern: type + name + optional value
If you don't assign immediately, the compiler forces you to before reading — that's a feature, not a limitation.
Use var for readability, not laziness — your future self will thank you.

The C# Built-In Data Types You'll Use Every Day

C# ships with a set of built-in data types that cover every common category of data. Each one maps to a .NET type under the hood, but C# gives them friendlier lowercase aliases — int instead of System.Int32, for example. They're identical; the alias is just less typing.

The most important split to understand is between whole numbers and decimal numbers. int holds whole numbers from about -2.1 billion to +2.1 billion. If you need decimals, you have three choices: float (7 digits of precision, uses an 'f' suffix), double (15-16 digits, the default for decimals), and decimal (28-29 digits, designed for money). For currency, always use decimalfloat and double use binary floating point, which can produce rounding errors that are catastrophic in financial software.

char holds a single character — one letter, one digit, one emoji. string holds any sequence of characters. bool holds true or false. long is like int but with a much bigger range — useful for things like Unix timestamps or row counts in enormous databases.

Choosing the right type isn't pedantry. It determines how much memory is used, what operations are legal, and what bugs you might accidentally introduce.

BuiltInDataTypes.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

class BuiltInDataTypes
{
    static void Main()
    {
        // INTEGER TYPES — whole numbers, no decimals
        byte playerLevel = 42;            // 0 to 255 — small non-negative numbers
        short temperatureCelsius = -18;   // -32,768 to 32,767
        int worldPopulationMillions = 8100; // most common integer type
        long distanceToStarInMeters = 9_460_730_472_580_800L; // 'L' suffix = long literal
        // Note: underscores in number literals are just for readability — C# ignores them

        // FLOATING POINT TYPES — numbers with a decimal point
        float bodyTemperatureF = 98.6f;   // 'f' suffix required for float literals
        double gravitationalConstant = 6.674e-11; // scientific notation works too
        decimal productPrice = 19.99m;    // 'm' suffix required — use for all money values

        // TEXT TYPES
        char bloodType = 'O';             // Single quotes for a single character
        string customerName = "Jordan";   // Double quotes for text sequences

        // BOOLEAN TYPE
        bool hasPremiumSubscription = false; // Only ever true or false

        // Showing the type AND value together using GetType()
        Console.WriteLine($"playerLevel is {playerLevel} (type: {playerLevel.GetType().Name})");
        Console.WriteLine($"productPrice is {productPrice:C} (type: {productPrice.GetType().Name})");
        Console.WriteLine($"customerName is '{customerName}' (type: {customerName.GetType().Name})");
        Console.WriteLine($"gravitationalConstant is {gravitationalConstant} (type: {gravitationalConstant.GetType().Name})");
        Console.WriteLine($"hasPremiumSubscription is {hasPremiumSubscription} (type: {hasPremiumSubscription.GetType().Name})");

        // Showing the minimum and maximum values built into each type
        Console.WriteLine($"\nMax int value: {int.MaxValue}");
        Console.WriteLine($"Max long value: {long.MaxValue}");
        Console.WriteLine($"Max decimal precision: {decimal.MaxValue}");
    }
}
Output
playerLevel is 42 (type: Byte)
productPrice is $19.99 (type: Decimal)
customerName is 'Jordan' (type: String)
gravitationalConstant is 6.674E-11 (type: Double)
hasPremiumSubscription is False (type: Boolean)
Max int value: 2147483647
Max long value: 9223372036854775807
Max decimal precision: 79228162514264337593543950335
Watch Out: Never use float or double for money
Try this in your head: what is 0.1 + 0.2? In maths, 0.3. In a double, it's 0.30000000000000004 — because floats and doubles store values in binary, and 0.1 can't be represented exactly in binary, just like 1/3 can't be written exactly in decimal. For any financial calculation, use decimal. It's slower, but it's exact. The extra milliseconds are worth more than a penny rounding error on a customer's invoice.
Production Insight
The char type is 2 bytes because C# uses UTF-16 internally. This means a single 'character' (like an emoji) may actually be two char values — code units. Use string for text, not arrays of char.
long is 8 bytes. If you're storing a value that fits in int, using long wastes 4 bytes per variable — in a large array, that's significant.
Rule: choose the smallest type that can hold your expected range. For counters, int is almost always right. Only reach for long when you have numbers over 2 billion or need to handle a database ID that could exceed that.
Key Takeaway
Match the type to the data's real-world nature.
Decimal for money. Double for science that can tolerate tiny errors. Int for counts.
The wrong type doesn't just waste memory — it introduces bugs that are hard to trace.

Value Types vs Reference Types — The Most Important Concept on This Page

This is the concept that separates people who debug their code confidently from people who stare at a screen wondering why their variable changed on its own. Every C# type is either a value type or a reference type, and the difference is about what gets copied when you assign one variable to another.

A value type stores the actual data directly. When you copy it to a new variable, you get a fresh independent copy. Change the copy, the original is untouched. All the numeric types, bool, char, and struct types are value types. Think of it like writing a phone number on two separate sticky notes — if you scribble on one, the other is fine.

A reference type stores the address of the data, not the data itself. When you 'copy' a reference type, both variables now point at the same object in memory. Change it through one variable, and the other variable reflects the change too — because they're both looking at the same thing. string, arrays, and classes are reference types. Think of it like two people both holding a map to the same house. If someone repaints that house, both maps now lead to a painted house.

string in C# is technically a reference type, but it behaves like a value type because strings are immutable — once created, a string's value never changes. When you 'modify' a string, C# actually creates a brand new string in memory.

ValueVsReferenceTypes.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;

// A simple class — classes are always reference types
class ShoppingCart
{
    public int ItemCount; // public field so we can access it directly in this example
}

class ValueVsReferenceTypes
{
    static void Main()
    {
        // ── VALUE TYPE DEMO ──────────────────────────────────────────
        int originalScore = 100;
        int backupScore = originalScore; // A completely independent COPY is made

        backupScore = 999; // We change only the copy

        // The original is untouched — each int lives in its own memory slot
        Console.WriteLine($"originalScore: {originalScore}"); // still 100
        Console.WriteLine($"backupScore:   {backupScore}");   // 999

        Console.WriteLine();

        // ── REFERENCE TYPE DEMO ──────────────────────────────────────
        ShoppingCart customerCart = new ShoppingCart();
        customerCart.ItemCount = 3;

        // This does NOT copy the cart — it copies the ADDRESS of the cart
        // Both variables now point at the SAME object in memory
        ShoppingCart checkoutCart = customerCart;

        checkoutCart.ItemCount = 7; // We change it through checkoutCart...

        // ...but customerCart reflects the change because they share one object
        Console.WriteLine($"customerCart.ItemCount: {customerCart.ItemCount}"); // 7 !
        Console.WriteLine($"checkoutCart.ItemCount: {checkoutCart.ItemCount}"); // 7

        Console.WriteLine();

        // ── STRING IMMUTABILITY DEMO ─────────────────────────────────
        string originalGreeting = "Hello";
        string modifiedGreeting = originalGreeting;

        // This looks like we're changing modifiedGreeting in place
        // but C# actually creates a BRAND NEW string and points modifiedGreeting at it
        modifiedGreeting = modifiedGreeting + ", World";

        Console.WriteLine($"originalGreeting: {originalGreeting}"); // still "Hello"
        Console.WriteLine($"modifiedGreeting: {modifiedGreeting}"); // "Hello, World"
    }
}
Output
originalScore: 100
backupScore: 999
customerCart.ItemCount: 7
checkoutCart.ItemCount: 7
originalGreeting: Hello
modifiedGreeting: Hello, World
Interview Gold: Stack vs Heap
Value types are (usually) stored on the stack — fast, automatically cleaned up when a method returns. Reference types live on the heap — managed by the garbage collector. Interviewers love asking 'where does a value type live?' The technically precise answer is: value types live wherever they're declared. A value type inside a class instance lives on the heap alongside that instance. But the core rule — value types copy data, reference types copy addresses — always holds true regardless of where they're stored.
Production Insight
The biggest production failure I've seen from reference type confusion: a caching layer where a single List<T> was passed around as a singleton reference. Multiple threads added items, and the list grew without bound — memory leak disguised as 'data loss'. The developer assumed each thread got its own copy.
Another classic: storing a List<int> as a return value from a method that actually returned the same list instance every time. Callers mutated it concurrently.
Rule: if you return an object from a method, consider whether the caller should be able to mutate it. Either return a copy (use .ToList() for lists) or return an immutable wrapper (AsReadOnly()).
Key Takeaway
Value types = independent copies when assigned.
Reference types = shared pointers when assigned.
String is immutable — it only look like a value type, but it's a reference type with training wheels.

Type Conversion — Safely Moving Data Between Types

Sometimes you have data in one type and you need it in another. A user types their age into a text box — that comes in as a string. You need to do maths with it — that requires an int. This is type conversion, and C# gives you three ways to do it.

Implicit conversion happens automatically when C# can guarantee no data will be lost. Going from int to long is safe — a long can hold everything an int can, and more. Going from float to double is safe for the same reason. C# just does it for you without complaining.

Explicit conversion (casting) is when you force the conversion and accept that data might be lost. Going from double to int chops off the decimal part — 9.9 becomes 9, not 10. You signal this intent by putting the target type in brackets before the value: (int)someDouble. The compiler won't do this silently because you'd lose data without knowing it.

Parsing is for converting strings to numeric types. int.Parse("42") works great when the string is definitely a valid number. int.TryParse("42", out int result) is the safer version — it returns false instead of throwing an exception if the string is something unexpected like "hello" or an empty field from a form. In real applications, always use TryParse for user input.

TypeConversion.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;

class TypeConversion
{
    static void Main()
    {
        // ── IMPLICIT CONVERSION ──────────────────────────────────────
        // int fits perfectly inside a long — no data can be lost, so C# allows it silently
        int monthsInYear = 12;
        long secondsInYear = monthsInYear * 30L * 24L * 60L * 60L; // int auto-promotes to long
        Console.WriteLine($"Seconds in a year (approx): {secondsInYear}");

        // ── EXPLICIT CONVERSION (CASTING) ────────────────────────────
        double exactAverageScore = 87.9;
        // We MUST cast explicitly because going from double to int loses the decimal
        // The cast does NOT round — it truncates (chops off) the decimal part
        int roundedScore = (int)exactAverageScore;
        Console.WriteLine($"Exact score: {exactAverageScore}");
        Console.WriteLine($"After casting to int: {roundedScore}"); // 87, not 88!

        // If you want proper rounding, use Math.Round before casting
        int properlyRoundedScore = (int)Math.Round(exactAverageScore);
        Console.WriteLine($"Properly rounded: {properlyRoundedScore}"); // 88

        Console.WriteLine();

        // ── PARSING — converting strings to numbers ──────────────────
        string userInputAge = "29";     // Imagine this came from a text box
        int parsedAge = int.Parse(userInputAge); // Works, but throws if input is invalid
        Console.WriteLine($"Parsed age: {parsedAge} (type: {parsedAge.GetType().Name})");

        // ── TRYPARSE — the safe, production-ready approach ───────────
        string suspiciousInput = "twenty"; // User typed a word instead of a number

        // TryParse returns true/false and writes the result into 'parsedValue' via 'out'
        bool parseSucceeded = int.TryParse(suspiciousInput, out int parsedValue);

        if (parseSucceeded)
        {
            Console.WriteLine($"Parsed successfully: {parsedValue}");
        }
        else
        {
            // This branch runs — no exception thrown, we handle it gracefully
            Console.WriteLine($"Could not parse '{suspiciousInput}' as an integer. Please enter digits only.");
        }

        // ── Convert class — another option for common conversions ─────
        double pi = 3.14159;
        string piAsText = Convert.ToString(pi); // Converts double to string
        Console.WriteLine($"Pi as a string: '{piAsText}'");
    }
}
Output
Seconds in a year (approx): 31104000
Exact score: 87.9
After casting to int: 87
Properly rounded: 88
Parsed age: 29 (type: Int32)
Could not parse 'twenty' as an integer. Please enter digits only.
Pi as a string: '3.14159'
Watch Out: int.Parse throws, TryParse doesn't
If you call int.Parse("abc") your program throws a FormatException and crashes unless you've wrapped it in a try-catch. In any situation where the input comes from a user, a file, or a network — in other words, anywhere you don't control — use int.TryParse instead. It's not slower, it's not harder to write, and it's the difference between a robust application and one that crashes the moment a user makes a typo.
Production Insight
The Convert class uses the current culture for decimal separators. In a French culture, 1,5 is valid, but in English it's not. If your code runs on a server with a different culture than expected, Convert.ToDouble("1.5") throws an exception.
A production outage I worked: a global e-commerce site used double.Parse without specifying CultureInfo.InvariantCulture. European orders with comma decimals crashed. The fix was adding CultureInfo.InvariantCulture to every parse call.
Rule: always specify an explicit culture when parsing user input. Use CultureInfo.InvariantCulture for data interchange formats.
Key Takeaway
Implicit = safe, explicit = possible data loss, parse = string to number.
TryParse is the production-safe choice — never Parse unless you control the input 100%.
Culture kills: always specify InvariantCulture for machine-to-machine data.

Nullable Value Types — Handling Missing Data Without Magic Numbers

What do you store in an int variable when the value is unknown? A patient hasn't had their temperature taken yet — should you store 0? That's a valid temperature. -1? That's a magic number that could look like a legitimate value. C# solves this with nullable value types: append ? to the type, like int? or bool?. This makes the variable able to hold either a value or null.

Nullable types are wrappers around the underlying value type. They add a HasValue property (true if a value is set) and a Value property to access the underlying value. You can also use the null-coalescing operator ?? to provide a default when null: int actual = maybeInt ?? 0.

Nullable types are not reference types — they're still value types (structs). The ? is syntactic sugar for Nullable<T>. This means they still get copied by value when assigned. And they come with a performance cost: boxing/unboxing if used with non-generic collections like ArrayList. Always prefer generic collections (List<int?>) to avoid that.

Nullable reference types are a different feature (introduced in C# 8.0) — they use the same ? syntax but work on reference types (string?, object?). The goal is to catch null dereferences at compile time. They're annotations, not wrappers — the underlying variable is still just a reference that can be null at runtime.

NullableValueTypes.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;

class NullableValueTypes
{
    static void Main()
    {
        // ── DECLARING A NULLABLE VALUE TYPE ──────────────────────────
        int? temperature = null; // 'int?' is equivalent to 'Nullable<int>'
        int? age = 25;           // Can hold a normal int value too

        // ── CHECKING FOR VALUE ───────────────────────────────────────
        if (temperature.HasValue)
        {
            Console.WriteLine($"Temperature: {temperature.Value}°C");
        }
        else
        {
            Console.WriteLine("Temperature not recorded.");
        }

        // ── NULL-COALESCING OPERATOR ────────────────────────────────
        int displayTemperature = temperature ?? 20; // default 20 if null
        Console.WriteLine($"Display: {displayTemperature}°C");

        // ── WORKING WITH GENERIC COLLECTIONS ─────────────────────────
        var readings = new int?[] { null, 36, 37, null, 39 };
        int sum = 0;
        int count = 0;
        foreach (var r in readings)
        {
            if (r.HasValue)
            {
                sum += r.Value;
                count++;
            }
        }
        double average = count > 0 ? (double)sum / count : 0;
        Console.WriteLine($"Average of recorded readings: {average:F1}°C");

        // ── COMPARISON WITH DEFAULT VALUE ───────────────────────────
        // default(int?) is null, not 0
        int? defaultNullable = default;
        int defaultInt = default;
        Console.WriteLine($"default(int?) is null: {defaultNullable == null}");
        Console.WriteLine($"default(int) is 0: {defaultInt == 0}");
    }
}
Output
Temperature not recorded.
Display: 20°C
Average of recorded readings: 37.3°C
default(int?) is null: True
default(int) is 0: True
Performance Tip: Avoid boxing with nullable types
If you ever need to put a nullable int into a non-generic collection like ArrayList, the nullable struct itself will be boxed (allocated on the heap). That's fine for small collections, but if you're storing millions of records, that allocation overhead can become significant. Always use List<int?> or an array — generics preserve the value type semantics without boxing.
Production Insight
A common pattern: using int? for database columns that allow NULL. But be careful — when you read data from the database through an ORM like Entity Framework Core, a NULL column is mapped to null for nullable types. If you then pass that value to a method expecting an int, you get an InvalidOperationException if you access .Value without checking HasValue.
I've seen a microservice crash because a database migration added a NOT NULL constraint to a column that was previously optional — the ORM started returning 0 instead of null, and the code handling the null case never executed, leading to incorrect business logic.
Rule: when using nullable types from a data source, always check HasValue before accessing Value. Consider using the ?? operator with a safe default that makes sense for your domain.
Key Takeaway
Nullable value types (int?, bool?) allow representing 'unknown' without magic numbers.
They're value types — copying creates independent instances.
Always check HasValue or use ?? — never assume a nullable has a value.
● Production incidentPOST-MORTEMseverity: high

How a Wrong Data Type Cost a Hospital System $2M

Symptom
Pediatric dosage calculations produced inconsistent results. For example, a 3.2-year-old child would sometimes get a dosage for a 3-year-old, sometimes for a 4-year-old. Nurses started overriding the system manually.
Assumption
The developer assumed age should be stored as a double to handle 'half years' for infant ages. The pharmacy system used truncation instead of rounding when converting to a whole number.
Root cause
double to int conversion through truncation (casting) caused the fractional part to be dropped silently. Age 3.7 became 3, age 3.2 also became 3 — inconsistent behavior depending on how the value was originally calculated.
Fix
Changed the data type from double to int and moved the fractional-year handling to a separate boolean field 'IsInfant' with exact month-based calculation. One line: double ageInYears to int ageInYears.
Key lesson
  • Choose the most restrictive type that can hold your data — int for whole numbers, decimal for money, never double for identifiers or counts.
  • When converting between types, be explicit about rounding direction (Math.Floor, Math.Ceiling, Math.Round) — never rely on implicit truncation.
  • Store domain data in the type that matches its real-world meaning. Age is a whole number; fractional age is a derived calculation, not a stored value.
Production debug guideThree most common type errors and how to find them fast3 entries
Symptom · 01
Runtime InvalidCastException when casting between types
Fix
Check if the cast is legal. Use is or as before casting. For value types, use Convert.ToInt32() instead of direct cast when you expect a conversion, not a simple type check.
Symptom · 02
Unexpected FormatException from int.Parse(), decimal.Parse(), etc.
Fix
Replace all Parse calls with TryParse for any input that could be malformed. Add a watch on the input string — check for whitespace, null, or unexpected culture-specific decimal separators.
Symptom · 03
Variables 'changing themselves' — reference type aliasing
Fix
Set a breakpoint on assignment. Inspect the variable you think is independent — if it's a reference type, check whether it points to the same object ID. Use Object.ReferenceEquals() to confirm aliasing.
★ Quick Debug Cheat Sheet: Type ProblemsWhen your app behaves oddly because of type issues, run these commands first to isolate the problem.
Value truncated or rounded unexpectedly
Immediate action
Check if an explicit cast (`(int)`) is applied. The cast truncates, not rounds.
Commands
Console.WriteLine($"Original: {originalValue}, Cast: {(int)originalValue}");
Console.WriteLine($"Rounded: {Math.Round(originalValue)}");
Fix now
Replace the cast with (int)Math.Round(value) if you need rounding, or change the variable type to match the precision you need.
"Object reference not set to an instance of an object" on a value type+
Immediate action
Check if the variable is a nullable type (`int?`, `bool?`) and hasn't been assigned.
Commands
if (myNullable.HasValue) { /* safe */ }
int nonNullable = myNullable ?? defaultValue;
Fix now
Always check .HasValue before accessing a nullable value type, or use the null-coalescing operator ?? to provide a default.
Money calculations show $19.999999999999996+
Immediate action
Check if the field type is `double` or `float` instead of `decimal`.
Commands
Console.WriteLine($"Type: {price.GetType().Name}");
decimal correctPrice = 19.99m; // suffix 'm' ensures decimal
Fix now
Change the variable type to decimal and add m suffix to all numeric literals. Recalculate with decimal arithmetic.
C# Built-in Data Types Comparison
TypeCategorySize (bytes)Range / PrecisionTypical Use Case
boolValue (struct)1true or false onlyFlags, conditions, toggles
byteValue (struct)10 to 255Raw binary data, pixel values
intValue (struct)4-2,147,483,648 to 2,147,483,647Counts, indexes, ages — everyday whole numbers
longValue (struct)8±9.2 × 10^18Timestamps, large IDs, file sizes in bytes
floatValue (struct)4~7 significant digitsGame graphics, sensors — where speed beats precision
doubleValue (struct)8~15-16 significant digitsScientific calculations, general decimal maths
decimalValue (struct)16~28-29 significant digitsCurrency, financial data — where exactness is mandatory
charValue (struct)2One Unicode character (U+0000 to U+FFFF)Single characters, character parsing
stringReference (class)VariableUp to ~2 billion charactersNames, messages, any text
objectReference (class)VariableCan hold any type (boxing/unboxing applies)Generic containers, reflection, legacy APIs

Key takeaways

1
Every variable in C# has a fixed type set at compile time
C# is strongly typed, which means type errors are caught before your program runs, not after it crashes in production.
2
Use decimal for any monetary value without exception
float and double use binary floating point and will silently introduce rounding errors into financial calculations.
3
Value types (int, bool, struct, etc.) copy their data when assigned; reference types (class, string, array) copy only the memory address
accidentally sharing a reference type between two variables is one of the most common sources of hard-to-find bugs.
4
Always use int.TryParse() instead of int.Parse() when the input comes from anywhere outside your code
user forms, files, APIs — because Parse throws an exception on bad input while TryParse returns false, letting you handle the error gracefully.
5
Nullable value types (int?, bool?) let you model 'unknown' without magic numbers. Always check HasValue or use the ?? operator before accessing the value.

Common mistakes to avoid

5 patterns
×

Using double for currency calculations

Symptom
You get results like $19.999999999999996 instead of $20.00 when adding prices, and totals drift over time.
Fix
Always declare money variables as decimal and use the 'm' suffix on literals, e.g. decimal price = 9.99m;. The decimal type uses base-10 arithmetic internally, eliminating the binary rounding errors that plague float and double.
×

Calling int.Parse() on user input without validation

Symptom
Your program crashes with an unhandled System.FormatException the moment a user types a letter, a space, or leaves a field empty.
Fix
Replace int.Parse(input) with int.TryParse(input, out int result) and check the boolean return value before using result. This keeps your application alive and lets you show the user a friendly error message instead.
×

Assuming that assigning a class (reference type) to a new variable creates an independent copy

Symptom
You 'copy' an object, modify the copy, and the original changes too — which looks like a ghost is editing your data.
Fix
Understand that assigning a reference type copies the pointer, not the object. To get a true independent copy you need to either implement a Clone() method, use a copy constructor, or switch to an immutable design. For simple data containers, consider using a struct (value type) instead of a class so copies are automatic.
×

Treating `string` as a mutable reference type and expecting changes to propagate

Symptom
You modify a string parameter inside a method and expect the caller to see the change — but it doesn't happen.
Fix
Remember strings are immutable: every modification creates a new string. If you need the caller to see the change, either return the new string or pass it with ref (but then you're passing a reference to the reference). Better to simply return the modified string.
×

Forgetting culture when parsing or formatting numbers

Symptom
A European user enters '1,5' as a decimal number, and your app crashes or produces the wrong value because it expected a dot.
Fix
Use CultureInfo.InvariantCulture for data interchange (files, APIs, databases) and respect the user's culture for display. When parsing user input, use the culture from the current thread or the user's locale. Example: decimal.Parse(userInput, CultureInfo.CurrentCulture).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a value type and a reference type in C#? ...
Q02SENIOR
Why would you use decimal instead of double when working with financial ...
Q03JUNIOR
What does the var keyword actually do in C#? Does it create a dynamicall...
Q04SENIOR
Explain the concept of boxing and unboxing in C#. When does it happen an...
Q01 of 04JUNIOR

What is the difference between a value type and a reference type in C#? Can you give an example of each and explain what happens in memory when you assign one to another variable?

ANSWER
A value type stores its data directly in the variable's memory location. When you assign a value type to another variable, a full copy of the data is made. Examples: int, bool, struct. In memory, value types are often allocated on the stack (when local) or inline within objects on the heap. A reference type stores a memory address pointing to the actual data. When you assign a reference type, the address is copied, so both variables point to the same object. Examples: class, string, array. The object itself lives on the heap. For strings, the immutability means the shared reference behaves safely — modifications create new strings rather than changing the shared object.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between int and long in C#?
02
What does the var keyword do in C# and is it bad practice?
03
Why is string a reference type but it behaves like a value type in C#?
04
What is the difference between `int?` and `int`?
05
How do I convert a string to a number safely in C#?
🔥

That's C# Basics. Mark it forged?

6 min read · try the examples if you haven't

Previous
Introduction to C#
2 / 11 · C# Basics
Next
Control Flow in C#