C# Data Types — Silent Truncation Breaks Your Math
Double-to-int casting dropped age 3.
- 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
decimalfor money, notdouble— binary floating point causes rounding errors int.Parse()throws on bad input;int.TryParse()returns false gracefullyvaris shorthand, not dynamic — the type is still fixed at compile time
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.
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.const or readonly.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 decimal — float 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.
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.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.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.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.
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.List<int> as a return value from a method that actually returned the same list instance every time. Callers mutated it concurrently..ToList() for lists) or return an immutable wrapper (AsReadOnly()).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.
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.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.double.Parse without specifying CultureInfo.InvariantCulture. European orders with comma decimals crashed. The fix was adding CultureInfo.InvariantCulture to every parse call.CultureInfo.InvariantCulture for data interchange formats.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.
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.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.HasValue before accessing Value. Consider using the ?? operator with a safe default that makes sense for your domain.int?, bool?) allow representing 'unknown' without magic numbers.How a Wrong Data Type Cost a Hospital System $2M
double to handle 'half years' for infant ages. The pharmacy system used truncation instead of rounding when converting to a whole number.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.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.- Choose the most restrictive type that can hold your data —
intfor whole numbers,decimalfor money, neverdoublefor 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.
InvalidCastException when casting between typesis or as before casting. For value types, use Convert.ToInt32() instead of direct cast when you expect a conversion, not a simple type check.FormatException from int.Parse(), decimal.Parse(), etc.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.Object.ReferenceEquals() to confirm aliasing.(int)Math.Round(value) if you need rounding, or change the variable type to match the precision you need.Key takeaways
decimal for any monetary value without exceptionfloat and double use binary floating point and will silently introduce rounding errors into financial calculations.int.TryParse() instead of int.Parse() when the input comes from anywhere outside your codeParse throws an exception on bad input while TryParse returns false, letting you handle the error gracefully.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 patternsUsing double for currency calculations
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
System.FormatException the moment a user types a letter, a space, or leaves a field empty.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
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
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
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 Questions on This Topic
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?
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.Frequently Asked Questions
That's C# Basics. Mark it forged?
6 min read · try the examples if you haven't