Skip to content

C# - Data Types

In C#, a data type specifies the type of data that a variable can hold, such as a number, text, or a true/false value. As a statically-typed and type-safe language, C# requires you to declare a variable’s type before you use it. This helps catch errors at compile-time, leading to more robust and reliable software.

C# data types are broadly categorized into two main kinds: Value Types and Reference Types.

Value types directly contain their data and are typically stored on the stack, which is a fast region of memory for short-lived data. When you assign a value-type variable to another, a copy of the value is made.

Common value types include:

  • Integral types: int, long, byte, short (for whole numbers).
  • Floating-point types: double, float, decimal (for numbers with fractions).
  • Other types: bool (true/false), char (a single character), enum (a set of named constants), and struct (a user-defined value type).
TypeSize (Bytes)Common Use Case
int4General purpose integer (e.g., age, count)
long8Very large integers (e.g., database IDs)
double8General purpose floating-point (e.g., scientific calculations)
decimal16High-precision financial calculations (e.g., money)
int age = 30;
double pi = 3.14159;
decimal price = 19.99m; // 'm' suffix for decimal
long population = 8_000_000_000L; // 'L' suffix for long, '_' for readability
bool isComplete = true;
char grade = 'A';

Reference types do not store the actual data directly. Instead, they store a reference (or a memory address) to the data, which is located on the heap, a memory region for longer-lived objects. When you assign a reference-type variable to another, only the reference is copied, not the object itself. Both variables then point to the same object.

Common reference types include:

  • string: A sequence of characters.
  • object: The ultimate base class for all types in .NET.
  • Arrays: A collection of elements of the same type.
  • Classes, Interfaces, Delegates, Records: User-defined reference types.
// s1 holds a reference to an object on the heap.
string s1 = "Hello";
// s2 now holds a copy of the reference, pointing to the SAME object.
string s2 = s1;
// Note: string is a special case; it's immutable. Operations that seem
// to modify a string actually create a new string object.
// A standard class example
var person1 = new Person { Name = "John" };
var person2 = person1; // Both refer to the same Person object
person2.Name = "Jane";
Console.WriteLine(person1.Name); // Output: Jane

The var keyword lets the compiler infer the type of a variable from the value it is initialized with. This can make code more readable by reducing redundancy.

Important: var does not mean the variable is dynamically typed (like in JavaScript). The type is still determined at compile-time and cannot be changed later. It’s just syntactic sugar.

var message = "This is a string"; // Compiler infers string
var count = 100; // Compiler infers int
var prices = new List<decimal>(); // Compiler infers List<decimal>

By default, reference types can be null, while value types cannot. C# provides features to handle nullability safely.

  • Nullable Value Types: To make a value type nullable, append a ? to its type declaration. Example: int? age = null;.
  • Nullable Reference Types (NRTs): A modern C# feature (enabled by default in new projects) that helps you avoid the dreaded NullReferenceException. The compiler warns you if you try to use a reference type that might be null without checking it first.
// NRTs enabled in project file: <Nullable>enable</Nullable>
string nonNullableName = "World"; // Cannot be null
string? nullableName = null; // Can be null
Console.WriteLine($"Hello, {nonNullableName.ToUpper()}"); // Safe
// Compiler warning: nullableName may be null here.
// Console.WriteLine($"Hello, {nullableName.ToUpper()}");
// Proper check:
if (nullableName != null)
{
Console.WriteLine($"Hello, {nullableName.ToUpper()}");
}

Converting data from one type to another is a common task.

  • Implicit Conversion (Safe): Happens automatically when no data can be lost. int to double is safe.
  • Explicit Conversion (Casting): Required when data might be lost. You must use the cast operator (type). double to int is explicit because the fractional part is lost.
  • Helper Methods (Safest): The Convert class and TryParse methods are the best way to handle conversions from external sources like user input or files.
// 1. Implicit
int myInt = 10;
double myDouble = myInt; // Safe, no data loss
// 2. Explicit (Casting)
double anotherDouble = 9.78;
int anotherInt = (int)anotherDouble; // Explicitly cast, result is 9
// 3. Helper Methods (Best Practice)
string userInput = "123";
if (int.TryParse(userInput, out int parsedValue))
{
Console.WriteLine($"Successfully parsed: {parsedValue}");
}
else
{
Console.WriteLine("Invalid number format.");
}

Pointer types are a feature inherited from C/C++. They store the memory address of a variable. This is an advanced feature and is rarely used in typical C# application code. It’s mainly for performance-critical scenarios or for interoperating with unmanaged code (like C++ libraries).

Warning: Code using pointers must be marked with the unsafe keyword and may require a specific compiler setting (<AllowUnsafeBlocks>true</AllowUnsafeBlocks> in the .csproj file). Use with extreme caution.

public class UnsafeExample
{
public static unsafe void Main()
{
int number = 20;
// The '&' operator gets the memory address of the variable
int* pointerToNumber = &number;
Console.WriteLine($"Value of number: {number}");
Console.WriteLine($"Address of number: {(long)pointerToNumber}");
Console.WriteLine($"Value via pointer: {*pointerToNumber}");
// Modify the value using the pointer
*pointerToNumber = 30;
Console.WriteLine($"New value of number: {number}");
}
}