Free Handbook · Every example compiled & verified

Fundamentals

Variables, var, the built-in value types, conversions, operators, overflow, string formatting and nullable types: the C# every later module builds on.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 01 · what you'll be able to do

  • Read input with Console.ReadLine and print formatted output with Console.WriteLine and string interpolation
  • Declare variables and constants, and choose between int, long, double, decimal, bool and char for a value
  • Convert between types safely with casts, Parse, TryParse and Convert, and predict when data is lost
  • Predict the result of integer division, %, ++ and compound assignment, and catch overflow with checked
  • Use int? and the ?? operator for a value that may be missing
01

Top-level statements, Console and comments

Every example in this handbook is a top-level statements program: the statements at the top of Program.cs run in order, from the first line to the last, and the compiler wraps them in a hidden Main method for you (Getting Started shows the long form). The two methods you will use constantly are Console.WriteLine, which prints and then moves to a new line, and Console.Write, which prints and stays on the same line.

C#Program.cs
// A single-line comment: the compiler ignores it
Console.WriteLine("Order summary");
Console.Write("Items: ");      // no newline after this
Console.WriteLine(3);
Console.WriteLine();           // an empty line

/* A block comment can
   span several lines */
Console.WriteLine("Total: " + 42.5);
Console.WriteLine("Tab\tseparated and a \"quote\"");
Outputcompiled & run with real C#
Order summary
Items: 3

Total: 42.5
Tab	separated and a "quote"
Your turn

Print your name and city on one line using one Console.Write and one Console.WriteLine.

Console.ReadLine() reads one line the user typed and returns it as a string, without the Enter key. It returns null when there is no more input (for example, when input is piped from a file that has ended), so the type is string?. The program below reads two lines; the input it was run with is shown under the code.

C#Program.cs
Console.Write("Name: ");
string? name = Console.ReadLine();

Console.Write("Age: ");
string? ageText = Console.ReadLine();
int age = int.Parse(ageText ?? "0");   // text -> number

Console.WriteLine();
Console.WriteLine($"Hello, {name}. Next year you will be {age + 1}.");
Outputcompiled & run with real C#
Name: Age: 
Hello, Ada. Next year you will be 37.

Typed input is not echoed to stdout when it is piped in, which is why the prompts run together here. In a terminal you would see what you type after each prompt.

Your turn

Ask for a second number and print the sum of the two.

Three kinds of comment
// runs to the end of the line. /* ... */ spans lines. /// is an XML documentation comment placed above a type or method; IDEs show it on hover and the build can export it. Write comments that say why the code does something — the code already says what.
02

Variables, var and constants

A variable is a named, typed storage slot. C# is statically typed: the type is fixed when the variable is declared and the compiler checks every assignment against it. You can write the type out (int count = 0;) or let the compiler infer it with var (var count = 0;). var is still static typing — the compiler works out int from the right-hand side and the variable is an int forever.

C#Program.cs
int count = 3;
var price = 19.99;          // inferred: double
var name = "Keyboard";      // inferred: string
bool inStock = true;

count = count + 2;          // variables can change
Console.WriteLine($"{count} x {name} at {price}, in stock: {inStock}");

const double TaxRate = 0.18; // fixed at compile time
Console.WriteLine($"Tax on one: {price * TaxRate:F2}");

Console.WriteLine(price.GetType());
Console.WriteLine(name.GetType());
Outputcompiled & run with real C#
5 x Keyboard at 19.99, in stock: True
Tax on one: 3.60
System.Double
System.String
Your turn

Try TaxRate = 0.2; after the declaration and read the compiler error it gives.

  • Names start with a letter or _, are case-sensitive, and use camelCase for locals and PascalCase for constants, types and methods.
  • var needs an initial value (the compiler must see one to infer the type) and only works for local variables, not fields.
  • const values are baked into the compiled code and must be known at compile time: numbers, strings, bool, char. For a value computed at runtime that must not change after setup, a class uses a readonly field (Module 05).
Error you will hit

CS0165: using a variable before it has a value

C#
int total;
if (args.Length > 0) total = 10;
Console.WriteLine(total);
Program.cs(3,19): error CS0165: Use of unassigned local variable 'total'
Why the compiler said that

A local variable has no default value in C#. The compiler follows every path to line 3 and finds one (no arguments) where total was never assigned, so it refuses to compile rather than print garbage. This is called definite assignment.

The fix

Give the variable a value on every path — usually by initialising it where you declare it.

C#
int total = 0;
if (args.Length > 0) total = 10;
Console.WriteLine(total);
When to use var
Most C# teams use var when the type is obvious from the right-hand side (var list = new List<int>();) and write the type when it is not (int retries = GetRetryCount();). It is a readability choice, never a performance one — both compile to the same code.
03

Built-in types: int, long, double, decimal, bool, char and string

C# has a small set of built-in value types. A value type holds its data directly, so copying one copies the value. string is a reference type (the variable points at text stored elsewhere), but it behaves like a value in everyday code because strings cannot be changed — Module 03 covers why.

TypeSizeRange / precisionLiteralUse for
int32 bits-2,147,483,648 to 2,147,483,64742counts, indexes, the default whole number
long64 bitsabout ±9.2 quintillion42Lids, timestamps, file sizes
double64 bits~15-17 significant digits, binary3.14measurements, science, graphics
float32 bits~6-9 digits3.14fgames and GPUs, rarely elsewhere
decimal128 bits28-29 digits, base 103.14mmoney and anything a person adds up
bool1 bytetrue or falsetrueflags and conditions
char16 bitsone UTF-16 code unit'A'single characters
stringreferenceany length of text"hi"text
C#Program.cs
Console.WriteLine($"int:  {int.MinValue} .. {int.MaxValue}");
Console.WriteLine($"long max: {long.MaxValue}");

long population = 8_100_000_000;   // _ separates digits for reading
Console.WriteLine(population);

char grade = 'B';
Console.WriteLine(grade + 1);          // char is a number underneath
Console.WriteLine((char)(grade + 1));  // cast back to a char

string city = "Pune";
Console.WriteLine($"{city} has {city.Length} letters, first is {city[0]}");
Outputcompiled & run with real C#
int:  -2147483648 .. 2147483647
long max: 9223372036854775807
8100000000
67
C
Pune has 4 letters, first is P

double versus decimal: why money is decimal

double stores numbers in binary, and most decimal fractions — 0.1, 0.2 — have no exact binary form, just as 1/3 has no exact decimal form. The tiny error is fine for a physics simulation and wrong for an invoice. decimal stores base-10 digits, so 0.1 is exactly 0.1. It is slower and uses more memory, which matters only in tight numeric loops.

C#Program.cs
double d = 0.1 + 0.2;
decimal m = 0.1m + 0.2m;

Console.WriteLine(d);
Console.WriteLine(m);
Console.WriteLine(d == 0.3);
Console.WriteLine(m == 0.3m);

// adding one paisa a hundred thousand times
double dTotal = 0;
decimal mTotal = 0;
for (int i = 0; i < 100_000; i++) { dTotal += 0.01; mTotal += 0.01m; }
Console.WriteLine(dTotal);
Console.WriteLine(mTotal);
Outputcompiled & run with real C#
0.30000000000000004
0.3
False
True
999.9999999992356
1000.00
Your turn

Change the loop to add 0.25 instead. Does double drift now? (0.25 is a power of two, so it is exact in binary.)

In real jobs
Payment, billing and accounting code in .NET uses decimal for amounts — a code reviewer will flag double price on sight. Never compare two double values with ==; check that the difference is smaller than a tolerance, such as Math.Abs(a - b) < 1e-9.
04

Type conversion: implicit, casts, Parse, TryParse and Convert

C# converts between numeric types implicitly only when no information can be lost: int to long, int to double. Going the other way you must write an explicit cast — (int)price — which tells the compiler "I know this may lose data". A cast from double to int truncates toward zero; it does not round.

C#Program.cs
int items = 7;
long big = items;          // implicit: int fits in long
double ratio = items;      // implicit: int fits in double
Console.WriteLine($"{big} {ratio}");

double price = 9.99;
int whole = (int)price;    // explicit: drops the fraction
Console.WriteLine(whole);
Console.WriteLine((int)-9.99);

Console.WriteLine(Math.Round(9.99));
Console.WriteLine(Math.Round(2.5));    // banker's rounding: to even
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));
Outputcompiled & run with real C#
7 7
9
-9
10
2
3

Math.Round rounds halves to the nearest even number by default, so 2.5 becomes 2. Pass MidpointRounding.AwayFromZero for school-style rounding.

Error you will hit

CS0266: assigning a double to an int without a cast

C#
double price = 9.99;
int whole = price;
Console.WriteLine(whole);
Program.cs(2,13): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)
Why the compiler said that

An int cannot hold 0.99, so this conversion loses data. C# makes you say so explicitly instead of silently dropping it. The message even tells you an explicit conversion exists.

The fix

Cast if truncation is what you want, or round first if it is not.

C#
double price = 9.99;
int whole = (int)price;               // 9
int rounded = (int)Math.Round(price); // 10
Console.WriteLine($"{whole} {rounded}");

Text to numbers

A cast cannot turn text into a number — (int)"42" does not compile. Use int.Parse when the text must be valid (it throws if not), int.TryParse when it might not be (user input, files, network data), and Convert.ToInt32 when converting between many types, including null (which it turns into 0). Every numeric type has the same trio: double.Parse, decimal.TryParse and so on.

C#Program.cs
using System.Globalization;

int a = int.Parse("42");
Console.WriteLine(a + 1);

foreach (string text in new[] { "17", "12a", "" })
{
    if (int.TryParse(text, out int n))
        Console.WriteLine($"'{text}' -> {n}");
    else
        Console.WriteLine($"'{text}' is not a number");
}

// Parse decimals with a fixed culture so "." always means the decimal point
decimal amount = decimal.Parse("1499.50", CultureInfo.InvariantCulture);
Console.WriteLine(amount * 2);

Console.WriteLine(Convert.ToInt32("99"));
Console.WriteLine(Convert.ToInt32(9.5));   // Convert rounds (to even)
Console.WriteLine(Convert.ToInt32(null as string));
Console.WriteLine(Convert.ToBoolean("true"));
Console.WriteLine(42.ToString() + "!");
Outputcompiled & run with real C#
43
'17' -> 17
'12a' is not a number
'' is not a number
2999.00
99
10
0
True
42!
Your turn

Read a number with Console.ReadLine() and keep asking in a loop until int.TryParse succeeds.

Error you will hit

CS0029: a string is not an int

C#
int age = "42";
Console.WriteLine(age);
Program.cs(1,11): error CS0029: Cannot implicitly convert type 'string' to 'int'
Why the compiler said that

"42" is text that happens to contain digits. There is no implicit or explicit conversion from string to int, because the text might be "forty-two". CS0029 is the general "these two types are not compatible" error.

The fix

Remove the quotes if it is a literal, or parse it if it really is text.

C#
int age = 42;
int parsed = int.Parse("42");
Console.WriteLine(age + parsed);
Error you will hit

FormatException: Parse on text that is not a number

C#
string input = "12a";
int n = int.Parse(input);
Console.WriteLine(n * 2);
Unhandled exception. System.FormatException: The input string '12a' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

This one compiles — the compiler cannot know what a string will contain at runtime. int.Parse throws when the text is not a valid integer. Read the stack trace from the top: the exception type and message, then the frames, ending in your code at line 2.

The fix

For any input you do not control, use TryParse, which returns false instead of throwing.

C#
string input = "12a";
if (int.TryParse(input, out int n))
    Console.WriteLine(n * 2);
else
    Console.WriteLine($"'{input}' is not a whole number");
05

Operators: division, remainder, compound assignment and ++

The arithmetic operators are + - * / %. The one that surprises everyone: when both operands are integers, / does integer division and throws the fraction away. Make either side a double or decimal to get a fractional result. % is the remainder, and its sign follows the left operand.

C#Program.cs
Console.WriteLine(7 / 2);        // int / int -> int
Console.WriteLine(7 / 2.0);      // int / double -> double
Console.WriteLine((double)7 / 2);
Console.WriteLine(-7 / 2);       // truncates toward zero

Console.WriteLine(7 % 3);
Console.WriteLine(-7 % 3);       // sign of the left side
Console.WriteLine(10 % 2 == 0 ? "even" : "odd");

int minutes = 135;
Console.WriteLine($"{minutes / 60}h {minutes % 60}m");

int score = 10;
score += 5;   // score = score + 5
score *= 2;   // score = score * 2
score -= 1;
score /= 3;
Console.WriteLine(score);
Outputcompiled & run with real C#
3
3.5
3.5
-3
1
-1
even
2h 15m
9
Your turn

Convert 4000 seconds into hours, minutes and seconds using only / and %.

++ adds one and -- subtracts one. Written before the variable (++i) the expression gives the new value; written after (i++) it gives the old value and then increments. On a line of its own the two are identical — the difference only shows when the result is used inside a bigger expression.

VisualizePrefix versus postfixStep 1 / 5
int i = 5;
int a = i++;
int b = ++i;
int c = i-- + 10;
Console.WriteLine($"{a} {b} {c} {i}");
Line 1

i starts at 5.

Variables now
i5
All 5 steps as a table
StepLineWhat happenedVariables now
11i starts at 5.i = 5
22Postfix: the expression is the old value 5, then i becomes 6.i = 6 a = 5
33Prefix: i becomes 7 first, and the expression is the new value 7.i = 7 a = 5 b = 7
44Postfix decrement: uses the old 7 (so 7 + 10 = 17), then i becomes 6.i = 6 a = 5 b = 7 c = 17
55Prints all four.
GroupOperatorsNotes
Comparison== != < > <= >=produce a bool
Logical&& || !&& and || short-circuit: the right side runs only if needed
Precedence* / % before + - before comparisons before && before ||add parentheses whenever you have to think about it
Null?? ??= ?.covered in the nullable lesson below
06

Overflow and the checked keyword

An int has 32 bits. Add 1 to int.MaxValue and the result does not fit, so by default C# wraps around to int.MinValue without any error — the bits simply roll over, like a car odometer. This is fast, and it is almost never what you want. Wrap an expression in checked(...) (or a block in checked { }) and overflow throws an OverflowException instead.

C#Program.cs
int max = int.MaxValue;
int wrapped = max + 1;          // unchecked by default
Console.WriteLine(wrapped);

long safe = (long)max + 1;      // widen first, then add
Console.WriteLine(safe);

try
{
    int boom = checked(max + 1);
    Console.WriteLine(boom);
}
catch (OverflowException)
{
    Console.WriteLine("checked caught the overflow");
}

byte b = 250;
b += 10;                        // byte also wraps: 260 - 256
Console.WriteLine(b);
Outputcompiled & run with real C#
-2147483648
2147483648
checked caught the overflow
4

try/catch is covered properly in Module 07; here it only stops the example from crashing.

Error you will hit

OverflowException in a checked expression

C#
int max = int.MaxValue;
int next = checked(max + 1);
Console.WriteLine(next);
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

checked turned a silent wrap into an exception, which is exactly its job: a crash with a line number is far easier to find than a negative bank balance three screens later.

The fix

Use a type big enough for the result (long, or decimal for money), and keep checked where overflow would mean a bug.

C#
int max = int.MaxValue;
long next = (long)max + 1;
Console.WriteLine(next);
Turn it on project-wide
Setting <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> in the .csproj makes all integer arithmetic checked; unchecked(...) then opts out where wrapping is intended (hash codes, checksums). Constant expressions such as int.MaxValue + 1 written with literals are checked by the compiler anyway and fail with CS0220.
07

String interpolation and formatting

Prefix a string with $ and anything inside { } is evaluated and inserted. After the expression you can add a format specifier after a colon ({price:F2}) and an alignment after a comma ({name,-10} pads to 10 characters, left-aligned; a positive number right-aligns). This replaces most string concatenation with +.

C#Program.cs
string item = "Mouse";
decimal price = 749.5m;
int qty = 3;
double ratio = 0.4567;

Console.WriteLine($"{qty} x {item} = {price * qty}");
Console.WriteLine($"Two decimals: {price:F2}");
Console.WriteLine($"Percent: {ratio:P1}");
Console.WriteLine($"Padded: [{qty:D4}]");
Console.WriteLine($"Hex: {255:X}");
Console.WriteLine($"Maths inside: {qty * 2 + 1}");
Console.WriteLine($"Braces: {{literal}}");

// alignment: a small table
Console.WriteLine($"{"Item",-8}|{"Price",8}");
Console.WriteLine($"{item,-8}|{price,8:F2}");
Console.WriteLine($"{"Cable",-8}|{99m,8:F2}");
Outputcompiled & run with real C#
3 x Mouse = 2248.5
Two decimals: 749.50
Percent: 45.7%
Padded: [0003]
Hex: FF
Maths inside: 7
Braces: {literal}
Item    |   Price
Mouse   |  749.50
Cable   |   99.00
Your turn

Add a third row and a total line whose price column lines up with the others.

SpecifierExampleOutput
F2 fixed-point{3.14159:F2}3.14
N0 with group separators{12345:N0}12,345 (separators follow the culture)
P1 percent{0.256:P1}25.6%
D5 zero-padded integer{42:D5}00042
X hexadecimal{255:X}FF
E2 scientific{1234.5:E2}1.23E+003
Formatting depends on the culture
C (currency), N and even the decimal point change with the machine's regional settings: the same code prints 1,234.50 in the US and 1.234,50 in Germany. For files, logs and APIs that other programs read, format with CultureInfo.InvariantCulture: price.ToString("F2", CultureInfo.InvariantCulture).
08

Nullable value types: int? and ??

An int always holds a number; it can never be null. Sometimes "no value" is a real answer — a customer who has not given their age, a sensor that has not reported. Add ? to any value type to get a nullable value type: int? holds either an int or null. HasValue tells you which, and the null-coalescing operator ?? supplies a fallback.

C#Program.cs
int? age = null;
Console.WriteLine(age.HasValue);
Console.WriteLine(age ?? -1);          // fallback when null

age = 36;
Console.WriteLine(age.HasValue);
Console.WriteLine(age.Value + 1);

int? missing = null;
Console.WriteLine(missing + 5 == null); // null in, null out

int? retries = null;
retries ??= 3;                          // assign only if null
Console.WriteLine(retries);

if (age is int years)                   // test and unwrap in one step
    Console.WriteLine($"{years} years");
Outputcompiled & run with real C#
False
-1
True
37
True
3
36 years
Your turn

Parse "abc" with int.TryParse, store the result in an int? that is null on failure, and print it with ?? 0.

Error you will hit

CS0037: null in a plain int

C#
int stock = null;
Console.WriteLine(stock);
Program.cs(1,13): error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
Why the compiler said that

A plain int has no spare state to mean "nothing". Only a nullable type can hold null.

The fix

Declare it as int? if "no value" is valid, or give it a real starting value.

C#
int? stock = null;
Console.WriteLine(stock ?? 0);
Error you will hit

InvalidOperationException: .Value on a null int?

C#
int? discount = null;
int value = discount.Value;
Console.WriteLine(value);
Program.cs(2,13): warning CS8629: Nullable value type may be null.
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
   at System.Nullable`1.get_Value()
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

The compiler warned (CS8629) but a warning does not stop the build. At runtime .Value on an empty nullable throws. Treat nullable warnings as errors — they point at exactly this crash.

The fix

Check first, or use ?? or GetValueOrDefault().

C#
int? discount = null;
int value = discount ?? 0;
Console.WriteLine(value);
Top-level statements
Code written directly in Program.cs; the compiler wraps it in a hidden Main method.
var
Asks the compiler to infer a local variable's type from its initial value. Still statically typed.
Value type
A type whose variable holds the data itself (int, double, bool, char, structs); copying copies the value.
decimal
A 128-bit base-10 number type that represents values like 0.1 exactly; the standard for money.
Explicit cast
A conversion written as (int)x that may lose data; required when no implicit conversion exists.
TryParse
Converts text to a number and returns false instead of throwing when the text is invalid.
Integer division
Division of two integers, which discards the fractional part: 7 / 2 is 3.
checked
Makes integer overflow throw OverflowException instead of silently wrapping around.
Nullable value type
T? for a value type T, such as int?: holds a value or null.
Null-coalescing operator
a ?? b evaluates to a unless it is null, otherwise b.
Quick check

What does Console.WriteLine(7 / 2 * 2.0); print?

Quick check

Which type should hold a product's price in an online shop?

Frequently asked questions

What is the difference between var and a written type in C#?
None at runtime. var tells the compiler to infer the type from the value you assign, and the variable keeps that type for its whole life. It is a readability choice: use var when the type is obvious from the right-hand side and write the type when it is not.
Should I use double or decimal in C#?
Use decimal for money and any quantity people add up and expect to be exact, because it stores base-10 digits. Use double for measurements, science and graphics, where speed matters and tiny binary rounding errors do not.
What is the difference between int.Parse and int.TryParse?
int.Parse returns the number or throws FormatException if the text is not a valid integer. int.TryParse returns true or false and gives the number through an out parameter, so it never throws. Use TryParse for any input you do not control.

Finish the C# handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.