By the end of this session, you will be able to:
StringBuildernull safely using nullable types and null operators| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | Methods: definition & overloading | Theory + Demo | 25 min |
| 0:25 | Strings & StringBuilder | Theory + Demo | 20 min |
| 0:45 | Null & nullable types | Theory + Demo | 15 min |
| 1:00 | Break | — | 5 min |
| 1:05 | Exception handling | Theory + Demo | 25 min |
| 1:30 | Exercises | Exercise | 30 min |
ref and out parametersmodifier returnType Name(params)void methods perform an action and return nothing=>) for single-expression methodsstatic int Add(int a, int b)
{
return a + b;
}
int result = Add(3, 4); // 7
// void — no return value
static void PrintLine(string message)
{
Console.WriteLine(message);
}
// Expression-bodied shorthand
static int Square(int n) => n * n;
static void Greet(string name)
=> Console.WriteLine($"Hello, {name}!");
Console.WriteLine — it has overloads for string, int, object, and more.
static int Add(int a, int b)
=> a + b;
static double Add(double a, double b)
=> a + b;
static int Add(int a, int b, int c)
=> a + b + c;
// Compiler picks the correct version:
Add(1, 2); // int overload
Add(1.5, 2.5); // double overload
Add(1, 2, 3); // three-arg overload
static void CreateUser(
string name,
bool isAdmin = false,
int age = 18)
{
Console.WriteLine(
$"{name}, admin: {isAdmin}, age: {age}");
}
CreateUser("Alice");
// isAdmin=false, age=18
CreateUser("Bob", isAdmin: true);
// skip age, set isAdmin by name
CreateUser("Carol", age: 25);
// skip isAdmin, set age by name
// ref: variable must be initialized before passing
static void Double(ref int value) => value *= 2;
int x = 5;
Double(ref x); // x is now 10
// out: method is responsible for assigning the value
static bool TryDivide(int a, int b, out double result)
{
if (b == 0) { result = 0; return false; }
result = (double)a / b;
return true;
}
if (TryDivide(10, 2, out double quotient))
Console.WriteLine(quotient); // 5
out is exactly how int.TryParse(s, out int value) works — now you know why the syntax looks that way.
Trim() return a new value — assign it back or it is loststring.Join to combine collections into a single strings.Trim() does nothing if you don't assign the result: s = s.Trim();
string s = " Hello, World! ";
s.Length // 17
s.Trim() // "Hello, World!"
s.ToUpper() // " HELLO, WORLD! "
s.Contains("World") // true
s.StartsWith(" Hello") // true
s.Replace("World", "C#") // " Hello, C#! "
s.Substring(7, 5) // "World"
s.IndexOf("World") // 9
s.Split(',') // [" Hello", " World! "]
string.Join(", ",
new[] { "a", "b", "c" }) // "a, b, c"
Case-insensitive comparison
string a = "hello";
string b = "HELLO";
bool eq = a == b; // false
bool eqIgnore = string.Equals(
a, b,
StringComparison.OrdinalIgnoreCase);
// true
OrdinalIgnoreCase for identifiers and API parameters; use CurrentCultureIgnoreCase only for user-facing text.
StringBuilder — avoid string thrashing in loops
var sb = new StringBuilder();
for (int i = 1; i <= 100; i++)
{
sb.Append(i);
if (i < 100) sb.Append(", ");
}
string result = sb.ToString();
// "1, 2, 3, ..., 100"
Each + on a string allocates a new object. StringBuilder mutates a buffer internally — use it whenever building strings in a loop.
NullReferenceException.int?)???.string, objects) can be null by defaultint, bool) cannot — add ? to allow null?? provides a fallback when the left side is null?. accesses a member only if not null — returns null instead of throwing// Nullable value type
int? age = null;
bool? isVerified = null;
// ?? — null-coalescing
int? userAge = null;
int display = userAge ?? 0; // 0
// ?. — null-conditional
string? name = null;
int? length = name?.Length; // null
string? upper = name?.ToUpper(); // null
// Chain both
int len = user?.Address?.PostalCode?.Length ?? 0;
is not null is preferred — it reads as plain English and is pattern-match awareif (name != null) block, the compiler knows name is non-null (flow analysis)<Nullable>enable</Nullable> in your project file to get compiler warnings whenever you dereference a potentially-null reference.
string? name = GetName();
// Classic null check
if (name != null)
{
Console.WriteLine(name.Length);
}
// C# 9+ pattern syntax (preferred)
if (name is not null)
{
Console.WriteLine(name.Length);
}
// Inline guard with ??
string display = name ?? "Unknown";
try / catch / finallythrowtry
{
int result = 10 / int.Parse(Console.ReadLine());
Console.WriteLine(result);
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero.");
}
catch (FormatException ex)
{
Console.WriteLine($"Invalid input: {ex.Message}");
}
finally
{
// Always runs — use to release resources
Console.WriteLine("Done.");
}
finally runs whether an exception was thrown or not — ideal for cleanup (closing files, releasing locks).
Exception at the top swallows everything, including bugs you want to seethrow new ... to signal a precondition violationnameof(b) keeps the parameter name refactor-safe// Correct catch order
catch (FormatException ex) // specific
catch (OverflowException ex) // specific
catch (Exception ex) // general — last!
// Throwing with a meaningful message
static int Divide(int a, int b)
{
if (b == 0)
throw new ArgumentException(
"Divisor cannot be zero.",
nameof(b));
return a / b;
}
throw; — it preserves the original stack tracethrow ex; resets the stack trace, making debugging much harderthrow ex; — always use bare throw; to re-throw.
try
{
// ... operation that may throw
}
catch (Exception ex)
{
Console.WriteLine(
$"Logging: {ex.Message}");
throw; // preserves stack trace
// NOT: throw ex;
}
public class InsufficientFundsException : Exception
{
public decimal Amount { get; }
public InsufficientFundsException(decimal amount)
: base($"Insufficient funds. Attempted to withdraw {amount:C}.")
{
Amount = amount;
}
}
// Throwing
throw new InsufficientFundsException(500m);
// Catching
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Failed: {ex.Message}");
Console.WriteLine($"Amount: {ex.Amount}");
}
Custom exceptions become essential in Session 10 — every API error will map to a specific exception type.
throw; to re-throw — never throw ex;. The stack trace is your only map to the crime scene."StringBuilder in loops?? provides a fallback; ?. short-circuits null dereferences safelythrow; preserves the stack traceNext session: Session 05 — OOP I: Classes, Encapsulation & Inheritance
base keywordvirtual and overrideBuild a safe calculator console app that uses methods, strings, nullable types, and exception handling.
double? SafeCalculate(string expression) that parses a string like "10 / 2" and returns the result, or null if the operation is invalid+, -, *, /InvalidExpressionException (with the original input as a property) when the format is unrecognizedMain, loop until the user types "exit", printing each result or an error message — never let an uncaught exception crash the appStringBuilder to produce a summary of all calculations at the endhistory command that prints all previous calculations using your StringBuilder summary at any point during the session, not just on exit.