Back to Blog
C#

C# Built-in Data Types: Selection Guide

c# built in data types: A practical guide to C# built-in data types: ranges, defaults, memory behavior, and how to choose the right type for your variables.

C# data typesvalue typesreference typesnumeric typestype conversion
C# code snippet showing variable declarations with different built-in data types like int, double, and decimal, with a visual representation of memory allocation.

c# built in data types requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you declare a variable in C#, you must pick a type from the set of built-in data types. That choice affects memory usage, range, precision, and runtime behavior. Understanding what each type actually stores is the first step toward writing code that behaves predictably under real workloads.

The Built-in Value Types in C#

C# defines a set of value types that are directly supported by the runtime. These are the numeric types, bool, and char. Each one maps to a specific storage size and a defined range of representable values.

The integer types are the most common. byte and sbyte are 8-bit, short and ushort are 16-bit, int and uint are 32-bit, and long and ulong are 64-bit. The signed versions use two's complement representation, so they can store negative values, while the unsigned versions cannot. For most application logic, int is the default choice because it balances range with performance on modern hardware.

Floating-point types are float and double. They follow the IEEE 754 standard and are designed for approximate arithmetic. float uses 32 bits and double uses 64 bits. The decimal type is different: it is a 128-bit value type that stores exact decimal representation, often used for financial calculations where rounding errors from binary floating point are unacceptable.

int count = 42; long fileSize = 1024L * 1024 * 1024; double ratio = 0.75; decimal price = 19.99m; bool isActive = true; char grade = 'A';

Each of these types has a MinValue and MaxValue constant that defines its range. For example, int.MinValue is -2,147,483,648 and int.MaxValue is 2,147,483,647. Using these constants in validation logic prevents magic numbers and makes the intent clear.

Reference Types That Ship with the Runtime

The built-in reference types are object, string, and dynamic. object is the ultimate base class for all types, value or reference. When you assign a value type to an object variable, boxing occurs, which copies the value onto the heap and wraps it in a reference. This has a runtime cost and should be avoided in hot paths.

string is a reference type, but it behaves like a value type in many ways. It is immutable, so every modification creates a new instance. Comparing strings with == compares the content, not the reference, because the operator is overloaded. This is a common source of confusion for developers coming from languages like Java.

dynamic bypasses compile-time type checking. The actual type is resolved at runtime. This can simplify interop with dynamic languages or COM, but it removes the safety net of the compiler and can lead to runtime exceptions if the member does not exist.

object boxed = 42; // boxing string name = "C#"; dynamic value = GetValue(); // type resolved at runtime

When you use dynamic, the compiler emits code that performs reflection and dispatch at runtime. This is slower than direct calls and should be limited to scenarios where you genuinely need late binding.

Default Values and the default Keyword

Every built-in type has a default value. Value types default to zero, bool defaults to false, and reference types default to null. This matters when you declare a field or an array element without explicit initialization.

int[] numbers = new int[3]; // all elements are 0 bool[] flags = new bool[2]; // all elements are false string text = null; // default for reference types

The default keyword can be used to obtain the default value of any type, which is useful in generic code:

T GetDefault<T>() => default(T);

For value types, default(T) returns the zeroed value. For reference types, it returns null. Knowing the default is critical when you read uninitialized fields or array slots. The C# compiler does not allow using a local variable before it is assigned, but fields and array elements are always initialized to their default.

Choosing Between int, long, and decimal

The choice between integer types depends on the range of values you expect. int is sufficient for most counters, IDs, and indexes. Use long when you need to store values larger than 2.1 billion, such as file sizes or timestamps. short and byte are rarely beneficial unless you are working with binary formats or optimizing memory in a large array.

For non-integer numbers, the choice between double and decimal is more nuanced. double is faster and has a wider range, but it stores values as binary fractions. This means that 0.1 is not represented exactly, and repeated arithmetic can accumulate small errors. decimal stores digits in base 10, so it represents values like 0.1 exactly, but it is slower and has a smaller range.

Aspectdoubledecimal
Storage size64 bits128 bits
Precision15-17 significant digits28-29 significant digits
Exact decimal?NoYes
Typical useScientific, graphicsFinancial, monetary

Use decimal for money, tax calculations, or any value where rounding errors cannot be tolerated. Use double for measurements, physics, or graphics where the range and speed matter more than exact representation.

Memory Behavior: Stack vs Heap

Value types are stored on the stack when they are local variables, and inline within the containing object when they are fields. Reference types are always stored on the heap, and the variable holds a reference to that location. This distinction affects memory layout and garbage collection pressure.

When you pass a value type to a method, a copy is made. Changes inside the method do not affect the original variable unless you use the ref or out keyword. Reference types are passed by reference, so the method can modify the object's state.

void ModifyValue(int x) => x = 10; void ModifyObject(StringBuilder sb) => sb.Append("changed");

Large value types, such as a decimal or a custom struct with many fields, can cause stack allocations and copying overhead. In performance-sensitive code, consider whether a reference type or a smaller value type would be more appropriate.

Common Pitfalls with Numeric Types

Overflow is a classic issue. Arithmetic on integer types wraps around by default in an unchecked context. For example, int.MaxValue + 1 becomes int.MinValue. You can enable checked arithmetic to throw an OverflowException instead:

int a = int.MaxValue; int b = checked(a + 1); // throws OverflowException

Floating-point precision is another trap. Comparing two double values with == can fail because of tiny rounding differences. Use a tolerance or a fixed number of decimal places instead.

double x = 0.1 + 0.2; double y = 0.3; bool equal = Math.Abs(x - y) < 1e-9;

decimal avoids this problem, but it is not immune to overflow. decimal.MaxValue is about 7.9e28, which is large but still finite. Dividing by zero throws DivideByZeroException for all numeric types, but floating-point division by zero yields Infinity or NaN instead of throwing.

Converting Between Built-in Types

Implicit conversions happen automatically when the target type can represent every value of the source type. For example, int can be implicitly converted to long, float, double, or decimal. Explicit conversions are required when data could be lost, such as double to int or long to int.

int i = 100; long l = i; // implicit long big = 10000000000; int narrowed = (int)big; // explicit, may overflow

For numeric conversions, the Convert class provides methods that throw exceptions on overflow. The checked keyword also applies to explicit casts and raises an OverflowException if the value does not fit.

int result = checked((int)big); // throws if out of range

String conversions are handled by int.Parse, decimal.Parse, or the TryParse variants. TryParse is preferred when the input might be invalid because it avoids exceptions.

if (int.TryParse(input, out int parsed)) { // use parsed }

When converting between numeric types, be aware of the sign and range. Casting a negative int to uint will wrap around, and casting a double to int truncates toward zero. These behaviors are defined by the language and should be handled explicitly when they matter.

c# built in data types: Practical Usage and Code Examples | RYUSLOG DEV