Variables and Data Types
🎯 What You'll Learn
- What variables are and why every program needs them
- All the primitive data types in C# (int, double, decimal, bool, char, string)
- How to declare, initialize, and use variables
- Type inference with the
varkeyword - Constants and why immutability matters
- The critical difference between value types and reference types
- Nullable types and the billion-dollar mistake
- Converting between types (casting, parsing, and conversion methods)
What is a Variable?
Imagine you're doing math homework. You write down a number, use it in a calculation, then need to refer back to it later. In real life, you might circle it or write "let x = 5." In programming, we do exactly the same thing—we give names to pieces of data so we can refer to them later.
A variable is a named container that holds a value. The value can change (vary) during the program's execution—hence the name "variable."
Every variable in C# has three essential properties:
- Name — How you refer to it in code (like
ageorcustomerName) - Type — What kind of data it holds (number, text, true/false, etc.)
- Value — The actual data stored in it
Declaring and Initializing Variables
To create a variable in C#, you need to declare it—tell the compiler its type and name. You can optionally initialize it by giving it a starting value.
Basic Syntax
// Declaration only (no initial value)
int age;
// Declaration with initialization (recommended)
int age = 25;
// You can also assign a value later
int score;
score = 100; // Assignment (not declaration)
// Multiple variables of the same type
int x = 10, y = 20, z = 30;
The pattern is always: type name = value;
In C#, you cannot use a local variable before assigning it a value. The compiler will give you an error: "Use of unassigned local variable." Always initialize your variables.
Naming Rules and Conventions
Variable names in C# must follow certain rules:
- Must start with a letter or underscore (
_) - Can contain letters, digits, and underscores
- Cannot be a C# keyword (like
int,class,if) - Are case-sensitive (
Ageandageare different variables)
Beyond the rules, C# has strong conventions (not enforced, but expected):
// Local variables and parameters: camelCase
int customerAge = 30;
string firstName = "Akwasi";
decimal totalPrice = 99.99m;
// Private fields: _camelCase (underscore prefix)
private int _count;
private string _connectionString;
// Constants: PascalCase
const int MaxRetries = 3;
const string DefaultCurrency = "GHS";
// BAD: Avoid these
int x; // Too short, meaningless
int NUMBER; // ALL CAPS is not C# style
int customer_age; // snake_case is not C# style
Choose names that reveal intent. elapsedTimeInDays is better than
d. customerCount is better than n.
Your future self (and teammates) will thank you.
Primitive Data Types
C# has several built-in primitive types (also called "simple types"). These are the fundamental building blocks for storing data.
Integer Types (Whole Numbers)
Integers are whole numbers without decimal points. C# offers several sizes depending on how large your numbers need to be:
| Type | Size | Range | Use Case |
|---|---|---|---|
byte |
1 byte | 0 to 255 | Small positive numbers, file data |
short |
2 bytes | -32,768 to 32,767 | Rarely used today |
int |
4 bytes | ±2.1 billion | Default choice for integers |
long |
8 bytes | ±9.2 quintillion | Very large numbers, timestamps |
byte red = 255; // Color component (0-255)
short temperature = -40; // Rarely needed
int population = 31_000_000; // Ghana's population (underscores for readability!)
long distanceToSun = 149_597_870_700L; // Meters (note the L suffix)
// int is the default - use it unless you have a specific reason not to
int age = 25;
int quantity = 100;
int year = 2024;
C# allows underscores (_) in numeric literals for readability.
1_000_000 is the same as 1000000 but much easier to read.
Use them for large numbers!
Floating-Point Types (Decimal Numbers)
When you need decimal points, you have three choices:
| Type | Size | Precision | Use Case |
|---|---|---|---|
float |
4 bytes | ~7 digits | Graphics, games (when precision matters less) |
double |
8 bytes | ~15-16 digits | Default for decimals, scientific calculations |
decimal |
16 bytes | 28-29 digits | Money and financial calculations |
float temperature = 36.6f; // Note the 'f' suffix
double pi = 3.14159265358979; // Default decimal type
decimal price = 19.99m; // Note the 'm' suffix (m for money)
decimal salary = 5000.00m; // Always use decimal for currency
// Why decimal for money? Because float/double can have tiny errors:
double badMath = 0.1 + 0.2; // Equals 0.30000000000000004 (!)
decimal goodMath = 0.1m + 0.2m; // Equals exactly 0.3
Never use float or double for financial
calculations. They use binary representation which can't precisely represent some
decimal fractions (like 0.1). Use decimal for any monetary values—prices,
salaries, account balances, taxes. This is especially important for your ERP work!
Boolean Type (True/False)
The bool type represents a logical value that can only be true
or false. It's named after mathematician George Boole.
bool isActive = true;
bool hasPermission = false;
bool isLoggedIn = true;
// Booleans are often the result of comparisons
int age = 25;
bool isAdult = age >= 18; // true
bool isTeenager = age >= 13 && age <= 19; // false
// Used heavily in conditions (we'll cover this in Control Flow)
if (isActive)
{
Console.WriteLine("The account is active");
}
Character Type
The char type holds a single Unicode character. Characters are enclosed
in single quotes.
char letter = 'A';
char digit = '7';
char symbol = '@';
char newLine = '\n'; // Escape sequence for new line
char tab = '\t'; // Escape sequence for tab
char heart = '♥'; // Unicode characters work!
char ghanaFlag = '\u0047'; // Unicode escape (this is 'G')
String Type (Text)
The string type holds text—a sequence of characters. Strings are enclosed
in double quotes. Despite being extremely common, string is technically
a reference type (more on this later), but it behaves like a primitive in many ways.
string name = "Akwasi";
string greeting = "Hello, World!";
string empty = ""; // Empty string (not null!)
string nothing = null; // No string at all (be careful!)
// String concatenation (joining strings)
string fullName = "Akwasi" + " " + "Asante"; // "Akwasi Asante"
// String interpolation (modern, preferred way)
string firstName = "Akwasi";
int age = 30;
string message = $"My name is {firstName} and I am {age} years old.";
// Result: "My name is Akwasi and I am 30 years old."
// Verbatim strings (ignore escape sequences, great for paths)
string path = @"C:\Users\Akwasi\Documents\file.txt";
// Without @, you'd need: "C:\\Users\\Akwasi\\Documents\\file.txt"
// Raw string literals (C# 11+) - great for multi-line and special characters
string json = """
{
"name": "Akwasi",
"age": 30
}
""";
Always use string interpolation ($"Hello {name}") instead of concatenation
("Hello " + name). It's more readable, easier to maintain, and performs
better in most cases. The $ prefix enables the {} placeholders.
Complete Type Reference
Here's a comprehensive reference of all C# primitive types:
| Category | Type | Size | Default Value | Example |
|---|---|---|---|---|
| Integer | byte |
1 byte | 0 | byte b = 255; |
short |
2 bytes | 0 | short s = -100; |
|
int |
4 bytes | 0 | int i = 42; |
|
long |
8 bytes | 0L | long l = 123L; |
|
| Floating | float |
4 bytes | 0.0f | float f = 3.14f; |
double |
8 bytes | 0.0d | double d = 3.14; |
|
decimal |
16 bytes | 0.0m | decimal m = 19.99m; |
|
| Boolean | bool |
1 byte | false | bool b = true; |
| Character | char |
2 bytes | '\0' | char c = 'A'; |
| Text | string |
varies | null | string s = "Hi"; |
Type Inference with var
C# can often figure out the type from the value you're assigning. The var
keyword lets the compiler infer the type automatically:
// Explicit typing
int count = 10;
string name = "Akwasi";
double price = 29.99;
// Same thing with var (compiler infers the type)
var count = 10; // Compiler knows this is int
var name = "Akwasi"; // Compiler knows this is string
var price = 29.99; // Compiler knows this is double
// The variable is STILL strongly typed!
var age = 25;
age = "twenty-five"; // ERROR! age is an int, can't assign string
// var REQUIRES initialization (compiler needs to see the value)
var mystery; // ERROR! Can't infer type without a value
var is not like JavaScript's var or Python's
variables. The type is determined at compile time and cannot change. It's just
shorthand—the compiler fills in the explicit type for you.
When to use var:
- When the type is obvious from the right side:
var name = "Akwasi"; - When the type name is very long:
var customers = new Dictionary<string, List<Customer>>(); - In LINQ queries and anonymous types (we'll see this later)
When to use explicit types:
- When the type isn't obvious:
int result = CalculateSomething(); - When you want to be specific:
decimal price = 19.99m;(not double) - When teaching or in documentation
Constants: Values That Never Change
Sometimes you have values that should never change during program execution—mathematical constants, configuration values, magic numbers that have meaning. C# provides two ways to create immutable values.
const: Compile-Time Constants
const double Pi = 3.14159265358979;
const int MaxRetries = 3;
const string CompanyName = "Adullam Solutions";
const decimal VatRate = 0.125m; // 12.5% VAT in Ghana
// Constants must be assigned at declaration
const int Value; // ERROR! Must have a value
// Constants cannot be changed
Pi = 3.0; // ERROR! Cannot modify a constant
// Constants can only be primitive types or strings
const DateTime StartDate = ...; // ERROR! DateTime is not allowed
readonly: Runtime Constants
readonly fields can be assigned at declaration or in the constructor,
but never changed after that. They're more flexible than const.
class Configuration
{
// Can be assigned at declaration
public readonly string Version = "1.0.0";
// Can be assigned in constructor
public readonly DateTime StartupTime;
public readonly string MachineName;
public Configuration()
{
StartupTime = DateTime.Now; // Set at runtime
MachineName = Environment.MachineName; // Set at runtime
}
}
| Feature | const |
readonly |
|---|---|---|
| When assigned | Compile time only | Declaration or constructor |
| Allowed types | Primitives and strings only | Any type |
| Can use runtime values | No | Yes |
| Memory | Substituted at compile time | Stored as a field |
Value Types vs Reference Types
This is one of the most important concepts in C#. Understanding the difference will help you avoid subtle bugs and write more efficient code.
Value Types: Stored Directly
Value types store their data directly in the variable. When you copy a value type, you get an independent copy.
int a = 10;
int b = a; // b gets a COPY of a's value
b = 20; // Changing b does NOT affect a
Console.WriteLine(a); // Output: 10 (unchanged)
Console.WriteLine(b); // Output: 20
Value types include: int, double, decimal,
bool, char, struct, and enum.
Reference Types: Stored Indirectly
Reference types store a reference (pointer) to where the data actually lives. When you copy a reference type, both variables point to the same data.
int[] a = { 1, 2, 3 }; // Array is a reference type
int[] b = a; // b points to the SAME array as a
b[0] = 99; // Changing through b...
Console.WriteLine(a[0]); // Output: 99 (a is affected!)
Console.WriteLine(b[0]); // Output: 99
Reference types include: string (special case), object,
arrays, class, and delegates.
The Stack and the Heap
Behind the scenes, value types and reference types are stored in different memory areas:
| Stack | Heap | |
|---|---|---|
| What's stored | Value types, references (pointers) | Reference type data (objects) |
| Speed | Very fast | Slower |
| Size | Limited (1 MB default) | Large (limited by system RAM) |
| Management | Automatic (LIFO) | Garbage collected |
| Lifetime | Until scope ends | Until no references remain |
Understanding value vs reference types helps you:
- Predict when modifying one variable affects another
- Understand method parameter behavior (passing by value vs reference)
- Avoid bugs when working with collections and objects
- Write more memory-efficient code
Nullable Types: Handling "No Value"
Sometimes a variable legitimately has no value. A customer might not have provided their phone number. A product might not have a discount. How do we represent "nothing"?
The Problem with Null
Reference types can be null (meaning "no object"), but value types cannot:
string name = null; // OK - string is a reference type
int age = null; // ERROR! int cannot be null
But what if a customer's age is unknown? Using 0 or -1 as
"no value" is a hack that leads to bugs.
Nullable Value Types
C# solves this with nullable value types, written as T?:
int? age = null; // OK! int? can be null
double? discount = null; // No discount applied
bool? isVerified = null; // Unknown verification status
DateTime? deletedAt = null; // Not deleted yet
// Checking if a nullable has a value
if (age.HasValue)
{
Console.WriteLine($"Age is {age.Value}");
}
else
{
Console.WriteLine("Age is unknown");
}
// Shorthand: null-conditional and null-coalescing
int displayAge = age ?? 0; // Use 0 if age is null
int displayAge2 = age ?? default; // Use default (0) if null
// Real-world example: ERP inventory
decimal? lastPurchasePrice = null; // New product, never purchased
int? reorderLevel = 10; // Has a value
DateTime? expiryDate = null; // Non-perishable item
Tony Hoare, inventor of null references, called it his "billion-dollar mistake" because null-related bugs have caused countless errors over the decades. Modern C# has nullable reference types (enabled by default in new projects) that help you track where null is allowed. We'll cover this more in later sections.
Type Conversion
Often you need to convert data from one type to another. C# provides several ways to do this, each with different safety characteristics.
Implicit Conversion (Safe, Automatic)
When there's no risk of data loss, C# converts automatically:
int myInt = 100;
long myLong = myInt; // OK! int fits perfectly in long
double myDouble = myInt; // OK! int fits perfectly in double
float myFloat = 3.14f;
double myDouble2 = myFloat; // OK! float fits in double
The rule: smaller types can be implicitly converted to larger types (widening).
Explicit Conversion / Casting (Potentially Unsafe)
When data might be lost, you must explicitly cast:
double myDouble = 9.78;
int myInt = (int)myDouble; // Cast required! myInt = 9 (decimal truncated)
long bigNumber = 3_000_000_000L;
int smallNumber = (int)bigNumber; // DANGER! Overflow - wrong result!
// Use checked to detect overflow
checked
{
int safe = (int)bigNumber; // Throws OverflowException
}
Parsing Strings to Numbers
Converting strings to numbers is extremely common—user input, file data, API responses all come as strings:
string input = "42";
// Parse - throws exception if invalid
int number1 = int.Parse(input); // 42
int number2 = int.Parse("not a number"); // THROWS FormatException!
// TryParse - returns bool, safer for user input
if (int.TryParse(input, out int result))
{
Console.WriteLine($"Parsed: {result}");
}
else
{
Console.WriteLine("Invalid number");
}
// Converting other types
double price = double.Parse("19.99");
decimal amount = decimal.Parse("1234.56");
bool flag = bool.Parse("true");
DateTime date = DateTime.Parse("2024-01-15");
When parsing data from users, files, or external sources, always use TryParse.
It won't crash your program if the input is invalid. Reserve Parse for
data you're certain is valid.
The Convert Class
The Convert class provides another way to convert between types:
string text = "123";
int number = Convert.ToInt32(text);
double dbl = Convert.ToDouble(text);
bool flag = Convert.ToBoolean(1); // true (non-zero = true)
// Convert handles null gracefully (returns default)
string nullString = null;
int fromNull = Convert.ToInt32(nullString); // Returns 0, doesn't throw
ToString(): Converting Anything to String
Every object in C# has a ToString() method:
int number = 42;
string text = number.ToString(); // "42"
double price = 1234.56;
string formatted = price.ToString("C"); // "$1,234.56" (currency)
string twoDecimals = price.ToString("F2"); // "1234.56" (2 decimal places)
DateTime now = DateTime.Now;
string dateStr = now.ToString("yyyy-MM-dd"); // "2024-01-15"
Putting It All Together: A Practical Example
Let's use everything we've learned in a realistic scenario—a product in our InvenTrack system:
// InvenTrack Product Variables
// Identifiers
int productId = 1001;
string sku = "WIDGET-001";
string name = "Premium Widget";
string description = "A high-quality widget for all your widget needs.";
// Pricing (always use decimal for money!)
decimal costPrice = 45.00m;
decimal sellingPrice = 79.99m;
decimal? discountPrice = null; // No discount currently
// Inventory
int quantityOnHand = 150;
int reorderLevel = 25;
int? maxStockLevel = 500; // Optional maximum
// Status flags
bool isActive = true;
bool isTaxable = true;
bool trackInventory = true;
// Dates
DateTime createdAt = DateTime.Now;
DateTime? lastSoldAt = null; // Never sold yet
DateTime? expiryDate = null; // Non-perishable
// Physical properties
double weight = 0.5; // kg
double length = 10.0; // cm
double width = 5.0; // cm
double height = 2.5; // cm
// Constants for calculations
const decimal VatRate = 0.125m; // 12.5% Ghana VAT
// Calculated values
decimal effectivePrice = discountPrice ?? sellingPrice;
decimal vatAmount = effectivePrice * VatRate;
decimal priceWithVat = effectivePrice + vatAmount;
decimal profitMargin = sellingPrice - costPrice;
bool needsReorder = quantityOnHand <= reorderLevel;
// Output
Console.WriteLine($"Product: {name} ({sku})");
Console.WriteLine($"Price: {priceWithVat:C} (incl. VAT)");
Console.WriteLine($"In Stock: {quantityOnHand} units");
Console.WriteLine($"Needs Reorder: {needsReorder}");
Key Takeaways
- Variables are named containers with a type, name, and value
- Use
intfor whole numbers,doublefor decimals,decimalfor money boolholds true/false,charholds single characters,stringholds textvarlets the compiler infer the type (still statically typed!)- Use
constfor compile-time constants,readonlyfor runtime constants - Value types store data directly; reference types store references
- Use
T?(nullable types) when a value might legitimately not exist - Use
TryParsefor safe string-to-number conversion
You now understand how to store data in C#. In the next section, we'll learn how to manipulate that data using operators and expressions—arithmetic, comparisons, logical operations, and more.
What's Next?
In the next section, Operators and Expressions, we'll learn how to perform calculations, make comparisons, and combine conditions. You'll see how to turn raw data into meaningful results.