Every piece of data in your application has a type — and choosing the right one matters more than you think.
Imagine you're building a warehouse management system.
You need to store:
You can't store a price as a whole number — you'd lose the cents. You can't store a name as a number. And you can't store "yes/no" as text without wasting memory and making comparisons slow.
C# built-in data types solve this problem. They give you precise, efficient tools to represent every kind of data your application needs to handle.
Data types are the categories of data that your program can work with. They tell the computer:
C# provides a set of built-in data types — predefined types that map directly to underlying types in the Common Type System (CTS) of the .NET platform. These are the atomic building blocks from which every more complex type (classes, structs, records, collections) is composed.
In C#, everything is an object — but the built-in types give you direct, efficient access to the primitive data representations that the runtime understands natively.
Without data types, a computer has no way to know:
For example, the byte pattern 01000001 could mean:
Without a type, the computer can't distinguish between these interpretations.
C# provides a rich set of built-in types that give meaning to data and enable the compiler to catch errors before your code ever runs.
C# is a type-safe language. This means the compiler can verify that you're using data correctly — preventing entire categories of bugs that plague dynamically-typed languages.
C# built-in data types divide into two fundamental categories:
| C# Keyword | .NET Type | Size | Range / Purpose | Category |
|---|---|---|---|---|
sbyte | System.SByte | 8 bits | -128 to 127 | Integral |
byte | System.Byte | 8 bits | 0 to 255 | Integral |
short | System.Int16 | 16 bits | -32,768 to 32,767 | Integral |
ushort | System.UInt16 | 16 bits | 0 to 65,535 | Integral |
int | System.Int32 | 32 bits | -2.1 billion to 2.1 billion | Integral |
uint | System.UInt32 | 32 bits | 0 to 4.2 billion | Integral |
long | System.Int64 | 64 bits | -9.2 quintillion to 9.2 quintillion | Integral |
ulong | System.UInt64 | 64 bits | 0 to 18.4 quintillion | Integral |
Int128 | System.Int128 | 128 bits | ±1.7×10³⁸ — .NET 7+ | Integral |
UInt128 | System.UInt128 | 128 bits | 0 to 3.4×10³⁸ — .NET 7+ | Integral |
nint | System.IntPtr | 32/64 bits | Native-sized signed integer | Integral |
nuint | System.UIntPtr | 32/64 bits | Native-sized unsigned integer | Integral |
float | System.Single | 32 bits | ±3.4×10³⁸, ~7 digits precision | Floating-point |
double | System.Double | 64 bits | ±1.7×10³⁰⁸, ~15-17 digits precision | Floating-point |
Half | System.Half | 16 bits | ±65,504, ~3 digits precision — .NET 5+ | Floating-point |
decimal | System.Decimal | 128 bits | ±7.9×10²⁸, 28-29 digits precision | Decimal |
bool | System.Boolean | 8 bits | true or false | Boolean |
char | System.Char | 16 bits | Unicode character (U+0000 to U+FFFF) | Character |
string | System.String | Variable | Sequence of Unicode characters | Reference |
object | System.Object | Variable | Base type of all types | Reference |
DateOnly | System.DateOnly | — | Calendar date without time — .NET 6+ | Struct |
TimeOnly | System.TimeOnly | — | Time of day without date — .NET 6+ | Struct |
Let's trace what happens when you declare and use a variable:
int age;
age = 25;
00000000 00000000 00000000 00011001int newAge = age + 5; // 30
Here's a beginner-friendly example using the most common types:
// Whole numbers int itemCount = 42; // Decimal numbers (money) decimal itemPrice = 19.99m; // True / False bool isInStock = true; // Single character char rating = 'A'; // Text string itemName = "Mechanical Keyboard"; // C# 14 / .NET 10: target-typed new expression decimal total = new(839.58); // Modern additions DateOnly orderDate = new(2025, 1, 15); TimeOnly orderTime = new(14, 30, 0); Half precisionValue = (Half)0.5;
Let's see how these types work in a banking application:
public class BankAccount { // Account holder (text) public string HolderName { get; set; } // Account number (up to 19 digits) public long AccountNumber { get; set; } // Balance (money — must use decimal, NOT double) public decimal Balance { get; private set; } // Is the account active? (true/false) public bool IsActive { get; set; } // Date account was opened public DateOnly OpenedDate { get; set; } // Transaction type (enum uses int as underlying type) public enum TransactionType { Deposit = 0, Withdrawal = 1, Transfer = 2 } }
Why decimal for money? Because double can introduce tiny rounding errors (0.1 + 0.2 ≠ 0.3 exactly in binary). For financial calculations, decimal preserves exact decimal precision.
Think of data types like different containers in a kitchen:
| C# Type | Kitchen Container | Why It Fits |
|---|---|---|
int | Measuring cup for whole items | Counts items (3 eggs, not 3.5 eggs) |
decimal | Precise kitchen scale | Money needs exact precision (no rounding) |
double | Thermometer | Approximate measurements are fine (temperature) |
bool | On/Off switch | Only two states: yes or no |
char | Single alphabet letter | One character at a time |
string | Notepad | Multiple characters together (text) |
Just as you wouldn't use a kitchen scale to measure a cup of flour (wrong tool), you wouldn't use a double to store money (wrong precision).
Let's look at what happens in memory when you declare different types:
int x = 10; int y = x; // COPIES the value y = 20; // x is still 10 Memory: x → [ 10 ] y → [ 20 ] (separate copy)
string a = "Hello"; string b = a; // COPIES the reference Memory: a → [ pointer to "Hello" ] b → [ pointer to "Hello" ] (same data!)
This difference matters. With value types, modifying one variable doesn't affect another. With reference types, two variables can point to the same underlying data.
This is one of the most common sources of confusion for beginners:
float — 32 bits, ~7 digits precision. Used for graphics, games, and scientific data where memory matters and small errors are acceptable.
double — 64 bits, ~15-17 digits precision. The default for non-money decimal values. Used for calculations where precision matters but exact decimal representation isn't required.
decimal — 128 bits, 28-29 digits precision. Used for money because it accurately represents base-10 fractions like 0.01 (which double cannot do perfectly).
double x = 0.1 + 0.2; // 0.30000000000000004 (NOT 0.3) decimal y = 0.1m + 0.2m; // 0.3 (exactly) Console.WriteLine(x == 0.3); // False! Console.WriteLine(y == 0.3m); // True
Wrong: double price = 19.99;
Correct: decimal price = 19.99m;
Money requires exact decimal representation. double can introduce rounding errors that accumulate over many transactions.
// This is a COMPILE ERROR decimal price = 19.99; // Cannot implicitly convert double to decimal // This is correct decimal price = 19.99m; //
If your application stores user IDs, order numbers, or timestamps that can exceed 2.1 billion, use long instead of int. This is especially common in databases and APIs.
| Scenario | Use This Type | Reason |
|---|---|---|
| Counting items, IDs, ages | int | 32 bits, sufficient for most counts |
| Large counts (users, transactions) | long | 64 bits, won't overflow easily |
| Money, prices, financial data | decimal | Exact decimal precision |
| Scientific calculations, measurements | double | Performance, good precision |
| Game graphics, ML inference (low precision) | Half | 16-bit, fast on GPUs |
| Yes/no, flags, toggles | bool | Only two states |
| Single character, parsing | char | One Unicode character |
| Names, emails, URLs, messages | string | Sequence of characters |
| Calendar dates (no time) | DateOnly | Clear intent, no timezone confusion |
| Time-of-day (no date) | TimeOnly | Clear intent, no date confusion |
| Huge integers (cryptography, big data) | Int128 | 128-bit precision |
| Interop with native code | nint / nuint | Matches CPU word size |
C# allows you to use var to let the compiler infer the type:
var count = 42; // inferred as int var name = "Layerbit"; // inferred as string var price = 19.99m; // inferred as decimal (m suffix)
Good practice: Use explicit types when the type is not obvious from the context. Use var when the type is clearly evident from the right side.
int (or long for large values)decimal (NEVER double)doubleboolstringDateOnlyTimeOnlyHalf — 16-bit floating point for GPU/ML workloadsInt128 / UInt128 — 128-bit integersnint / nuint — native-sized integers for interopDateOnly / TimeOnly — precise date/time separationnew expressions — decimal x = new(19.99m);You've seen how data types work, how to choose the right one, and how value types and reference types differ. Let's see if you can apply this knowledge.
1. You're building a banking application that needs to calculate interest on customer accounts. Which data type should you use for storing the account balance?
Correct: C
Why C is correct: decimal is the correct choice for financial data because it accurately represents base-10 fractions (like 0.01) without floating-point rounding errors. When dealing with money, exact precision is non-negotiable — a bank must be able to calculate $0.10 + $0.20 and get exactly $0.30, every time.
Why A is incorrect: double uses binary floating-point representation, which cannot exactly represent many decimal values. For example, 0.1 + 0.2 with double equals approximately 0.30000000000000004, not exactly 0.3. Over millions of transactions, these tiny errors accumulate.
Why B is incorrect: float has even less precision than double (only ~7 decimal digits vs ~15-17). Using float for money would introduce larger rounding errors than double.
Why D is incorrect: long stores whole integers only. Account balances need decimal places (cents), so long cannot represent $19.99.
Reinforcement: This is a fundamental rule in .NET development: never use double or float for money. Always use decimal. Microsoft's own guidelines explicitly recommend decimal for financial calculations.
2. What is the key difference between value types (like int) and reference types (like string) in C#?
Correct: B
Why B is correct: This is the fundamental difference. When you declare an int x = 10, the variable x directly contains the value 10 in memory. When you declare a string s = "Hello", the variable s contains a reference (pointer) to where the string "Hello" is actually stored on the heap.
Why A is incorrect: While value types can be more efficient for small data (no heap allocation, no pointer dereferencing), they're not universally faster. Large value types (like big structs) can be slower when copied frequently. Reference types avoid copying large data by sharing references.
Why C is incorrect: This is backwards. Reference types CAN be null by default. Value types CANNOT be null unless they are explicitly made nullable using Nullable<T> or the ? operator (e.g., int? x = null;).
Why D is incorrect: They behave very differently. Assigning one value type variable to another creates a copy. Assigning one reference type variable to another creates two references to the same data.
Reinforcement: Understanding this difference is crucial for avoiding bugs. When you pass a value type to a method, the method receives a copy — changes inside the method don't affect the original. When you pass a reference type, changes inside the method DO affect the original data.
3. In .NET 10 / C# 14, which new built-in type should you use to store a 16-bit floating-point number for GPU or machine learning workloads where memory bandwidth is critical?
Correct: C
Why C is correct: System.Half (keyword Half) is a 16-bit floating-point type introduced in .NET 5. It's specifically designed for scenarios where memory bandwidth is critical — such as GPU computing, machine learning inference, and graphics processing — because it uses half the memory of float (32 bits) and quarter of double (64 bits).
Why A is incorrect: float is 32 bits (4 bytes). While float is used in graphics and games, Half provides an additional layer of memory savings when precision requirements are lower.
Why B is incorrect: double is 64 bits (8 bytes). This is actually the OPPOSITE of what you want when memory bandwidth is critical — double uses MORE memory, not less.
Why D is incorrect: decimal is 128 bits (16 bytes) and is designed for financial precision, not for high-performance computing workloads. It would be the worst choice for GPU/ML scenarios.
Reinforcement: Modern .NET includes specialized types for modern workloads. Half, Int128, and nint are all examples of how .NET has evolved to meet the demands of contemporary computing — from machine learning to cryptography to native interoperability.
4. A developer writes the following code:
int x = 10;
int y = x;
y = 20;
Console.WriteLine(x);
What is the output?
Correct: A
Why A is correct: int is a value type. When you write int y = x;, C# makes a copy of the value 10 and stores it in y. When you then change y to 20, you're only modifying the copy. The original variable x still contains 10.
Why B is incorrect: This would be the output if int were a reference type (where y would point to the same memory as x). But int is a value type, so modifying y doesn't affect x.
Why C is incorrect: NullReferenceException occurs when you try to access a member of a reference type that is null. int is a value type and cannot be null by default, so no exception will occur.
Why D is incorrect: This is valid C# code. It compiles without any errors.
Reinforcement: This example demonstrates the copy semantics of value types. If you had used a reference type (like a custom class), both variables would point to the same object, and modifying one would affect the other.
5. You need to store the date "March 15, 2025" for a user's birthday. You don't need to store any time information (like 10:30 AM). Which type should you use?
Correct: B
Why B is correct: DateOnly (introduced in .NET 6) stores exactly what its name says — only the date, without any time component. This makes your intent clear: "This is a date, not a timestamp." It also avoids the problem of having a meaningless 00:00:00 time attached to a date.
Why A is incorrect: DateTime always includes a time component, even if you set it to midnight. Using DateTime for a birthday would store an unnecessary time value (like 00:00:00) that could cause confusion or timezone issues.
Why C is incorrect: string could store the date as text ("March 15, 2025"), but you lose all type safety and date-specific operations. You can't easily compare dates, sort them, or calculate age from a string without parsing.
Why D is incorrect: TimeOnly stores only the time (like 10:30 AM), not the date. It's the opposite of what you need — you want the date without the time.
Reinforcement: The introduction of DateOnly and TimeOnly in .NET 6 represented a philosophical shift in how .NET handles temporal data. They encourage you to think precisely about what you're storing: a date, a time, or both together (as DateTime). This precision leads to fewer bugs and clearer code.
You've built a solid foundation in understanding C# built-in data types!
dotnetmadeeasy.com — Learn C# and .NET, the right way.