Beginner

This beginner tutorial covers C#’s foundational syntax and object-oriented paradigm through 30 heavily annotated examples. Each example demonstrates core concepts with detailed inline comments explaining the imperative and OO approach.

Example 1: Hello World and C# Compilation

C# is a compiled language that targets the .NET runtime. You can run C# code through compilation to assemblies or use C# Interactive for REPL-style development.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[Source Code<br/>Program.cs]:::blue --> B[C# Compiler<br/>Roslyn]:::orange
    B --> C[IL Assembly<br/>Program.dll]:::teal
    C --> D[.NET Runtime<br/>Execution]:::orange

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#DE8F05,stroke:#000,color:#fff

Code:

// Example 1: Hello World and C# Compilation
Console.WriteLine("Hello, World!");  // => Outputs: Hello, World!
                                      // => Console.WriteLine prints to stdout with newline

Key Takeaway: C# uses Console.WriteLine for output. Statements end with semicolons. The language emphasizes statements and side effects.

Why It Matters: C# Interactive (csi) enables rapid prototyping similar to Python’s REPL, but with .NET’s type safety and performance. Enterprise applications use the compilation model for optimized production deployments, while developers use scripting for quick experiments and automation tasks.

Example 2: Variables with Type Inference (var)

C# supports type inference with the var keyword, allowing the compiler to deduce types from initialization expressions while maintaining static typing.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[var x = 10]:::blue --> B[Compiler Inference]:::orange
    B --> C[Type: int]:::teal
    B --> D[Mutable by default]:::orange

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#DE8F05,stroke:#000,color:#fff

Code:

// Example 2: Variables with Type Inference (var)
var x = 10;                  // => x is int (inferred from literal 10)
                             // => MUTABLE - can reassign x
                             // => Type determined at compile time

x = 20;                      // => REASSIGNMENT: x is now 20
                             // => No error - mutation is default in C#
                             // => Previous value 10 is lost

var name = "Alice";          // => name is string (inferred from string literal)
                             // => Compiler determines most specific type

Console.WriteLine(x);        // => Outputs: 20
                             // => Current value of x
Console.WriteLine(name);     // => Outputs: Alice

Key Takeaway: Use var for local variables when type is clear from initializer. Variables are mutable by default - reassignment is allowed.

Why It Matters: Mutability by default provides flexibility but introduces risks related to unexpected state changes. Understanding mutation patterns is critical for writing maintainable C# code, especially in multi-threaded applications.

Example 3: Explicit Type Declarations

While var provides convenience, explicit type declarations improve code clarity and document developer intent.

// Example 3: Explicit Type Declarations
int age = 30;                // => age is int (explicitly declared)
                             // => 32-bit signed integer
                             // => Range: -2,147,483,648 to 2,147,483,647

string name = "Bob";         // => name is string (explicitly declared)
                             // => Immutable UTF-16 character sequence
                             // => Reference type

double pi = 3.14159;         // => pi is double (64-bit IEEE 754)
                             // => Default for decimal literals

bool isValid = true;         // => isValid is bool (true or false)
                             // => Boolean values: true, false (lowercase)

Console.WriteLine($"{age} {name} {pi:F2} {isValid}");
                             // => String interpolation with $""
                             // => {age} embeds variable value
                             // => {pi:F2} formats to 2 decimal places
                             // => Outputs: 30 Bob 3.14 True

Key Takeaway: Explicit types document intent and improve readability at API boundaries. Use format specifiers in string interpolation for controlled output.

Why It Matters: Type declarations at public API boundaries prevent unintended type changes and improve IntelliSense documentation. String interpolation with format specifiers replaces verbose String.Format calls, improving code readability while maintaining type safety and performance.

Example 4: Nullable Reference Types

C# 8.0+ introduced nullable reference types to eliminate null reference exceptions through compile-time analysis.

// Example 4: Nullable Reference Types
string name = "Alice";       // => Non-nullable string
                             // => Cannot be assigned null (compiler warning)
                             // => Guaranteed to have a value

string? nullableName = null; // => Nullable string (? suffix)
                             // => Can be assigned null
                             // => Type: string? (nullable reference type)

int? nullableAge = null;     // => Nullable value type
                             // => int? is shorthand for Nullable<int>
                             // => Can hold int values OR null

nullableAge = 25;            // => Assign value to nullable int
                             // => nullableAge now holds 25 (not null)

if (nullableAge.HasValue)    // => HasValue checks if null
{                            // => true when value is present
    Console.WriteLine(nullableAge.Value);
                             // => .Value extracts int value
                             // => Outputs: 25
                             // => Throws if HasValue is false
}

int age = nullableAge ?? 0;  // => Null-coalescing operator ??
                             // => Returns left if not null, else right
                             // => age is 25 (nullableAge has value)
Console.WriteLine(age);      // => Outputs: 25

Key Takeaway: Use ? suffix for nullable types. Check HasValue before accessing .Value. Use ?? for default values.

Why It Matters: Nullable reference types eliminate null reference exceptions - Tony Hoare’s “billion-dollar mistake”. Codebases that enable nullable reference types see significant reductions in null-related production errors, making systems more reliable without runtime overhead.

Example 5: Control Flow - If Statements

C# if statements control execution flow based on boolean conditions. Unlike F#, they’re statements (not expressions) executed for side effects.

// Example 5: Control Flow - If Statements
int x = 10;                  // => x is 10 (positive integer)
                             // => Will be tested in conditional branches

if (x > 0)                   // => Condition: x > 0 evaluates to true
{                            // => Braces define code block
                             // => Required for multi-line blocks
    Console.WriteLine("Positive");
                             // => Executes when condition is true
                             // => Outputs: Positive
                             // => First matching branch wins
}
else if (x < 0)              // => Additional condition (not evaluated here)
{                            // => Would execute if x < 0
                             // => Skipped when previous condition matched
    Console.WriteLine("Negative");
                             // => Not executed (x is positive)
}
else                         // => Default case (not executed)
{                            // => Executes when neither above condition true
                             // => Catch-all for remaining cases
    Console.WriteLine("Zero");
                             // => Not executed (first condition was true)
}

// Ternary operator (expression form):
string description = x > 0 ? "positive" : "non-positive";
                             // => condition ? true-value : false-value
                             // => Returns "positive" (x > 0 is true)
                             // => description is "positive"
                             // => Ternary returns value (expression not statement)

Console.WriteLine(description);
                             // => Outputs: positive

Key Takeaway: If statements use braces for blocks. Ternary operator (? :) provides expression-based conditional.

Why It Matters: While if statements don’t return values like F# expressions, the ternary operator provides expression semantics when needed. Understanding when to use statements vs expressions improves code clarity - use ternary for simple value selection, if statements for complex multi-line logic.

Example 6: Switch Expressions (C# 8.0+)

Switch expressions provide pattern matching with expression semantics, replacing verbose switch statements.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[number input]:::blue --> B{Pattern Match}:::orange
    B -->|1| C[One]:::teal
    B -->|2| D[Two]:::teal
    B -->|_| E[Other]:::teal

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#029E73,stroke:#000,color:#fff
    style E fill:#029E73,stroke:#000,color:#fff

Code:

// Example 6: Switch Expressions (C# 8.0+)
int number = 2;              // => number is 2

string description = number switch  // => Switch expression returns value
{                            // => Pattern matching on number
    1 => "One",              // => Pattern: constant 1, result: "One"
                             // => => separates pattern from result
    2 => "Two",              // => Pattern: constant 2, result: "Two"
                             // => Matches current number value
    _ => "Other"             // => _ is discard pattern (wildcard/default)
                             // => Must be EXHAUSTIVE or compiler warns
};                           // => Semicolon ends expression statement
                             // => description is "Two" (matched pattern 2)

Console.WriteLine(description);
                             // => Outputs: Two

// Switch with guards:
string category = number switch
{                            // => Pattern matching with relational operators
    > 0 and < 10 => "Small",    // => Relational pattern with and
                                  // => Pattern: number > 0 AND number < 10
                                  // => Matches: number=2 satisfies both conditions
    >= 10 and < 100 => "Medium", // => Multiple conditions
                                  // => Not evaluated (previous pattern matched)
    >= 100 => "Large",           // => Single relational pattern
                                  // => Not evaluated
    _ => "Invalid"               // => Default case
                                  // => Not needed (previous pattern matched)
};                               // => category is "Small" (first match wins)
Console.WriteLine(category);     // => Outputs: Small

Key Takeaway: Switch expressions return values and support pattern matching. Use relational patterns (>, <, >=) and logical patterns (and, or).

Why It Matters: Switch expressions bring functional programming patterns to C#, eliminating temporary variables and improving readability. They reduce code complexity compared to equivalent if/else chains while maintaining compile-time exhaustiveness checking.

Example 7: Loops - For Loop

For loops enable iteration over sequences with index-based control, executing code blocks repeatedly.

// Example 7: Loops - For Loop
for (int i = 0; i < 5; i++)  // => Initializer: int i = 0
                             // => Condition: i < 5 (checked before each iteration)
                             // => Iterator: i++ (increments after each iteration)
{                            // => Loop body executes while condition is true
    Console.WriteLine(i);    // => Iteration 1: i=0, Outputs: 0
                             // => Iteration 2: i=1, Outputs: 1
                             // => Iteration 3: i=2, Outputs: 2
                             // => Iteration 4: i=3, Outputs: 3
                             // => Iteration 5: i=4, Outputs: 4
                             // => Iteration 6: i=5 (condition false, exits)
}

// Nested loop:
for (int i = 1; i <= 3; i++)
{                            // => Outer loop: i from 1 to 3
    for (int j = 1; j <= 2; j++)
    {                        // => Inner loop: j from 1 to 2
        Console.WriteLine($"i={i}, j={j}");
                             // => Outputs: i=1, j=1
                             // => Outputs: i=1, j=2
                             // => Outputs: i=2, j=1
                             // => Outputs: i=2, j=2
                             // => Outputs: i=3, j=1
                             // => Outputs: i=3, j=2
    }
}

Key Takeaway: For loops have initializer, condition, and iterator. Nested loops multiply iteration counts.

Why It Matters: For loops provide imperative iteration with fine-grained control over index variables. While LINQ and foreach offer more declarative alternatives for most cases, for loops remain optimal for algorithms requiring index manipulation (binary search, insertion sort) or backwards iteration.

Example 8: Loops - Foreach Loop

Foreach loops iterate over collections without explicit index management, providing cleaner syntax than for loops.

// Example 8: Loops - Foreach Loop
int[] numbers = { 1, 2, 3, 4, 5 };  // => Array initializer syntax
                                     // => numbers is int[] (mutable array)
                                     // => Length: 5 elements

foreach (int num in numbers)  // => Iterates over each element
{                             // => num takes value of current element
    Console.WriteLine(num);   // => Iteration 1: num=1, Outputs: 1
                              // => Iteration 2: num=2, Outputs: 2
                              // => Iteration 3: num=3, Outputs: 3
                              // => Iteration 4: num=4, Outputs: 4
                              // => Iteration 5: num=5, Outputs: 5
}

// Foreach with var:
foreach (var num in numbers)  // => Type inference: var becomes int
{                             // => num is int (inferred from array element type)
    Console.WriteLine(num * 2);
                              // => Doubles each element
                              // => Outputs: 2, 4, 6, 8, 10 (on separate lines)
}

Key Takeaway: Foreach iterates over IEnumerable collections. Element type can use var for inference.

Why It Matters: Foreach eliminates index-related bugs (off-by-one errors) and clearly expresses intent - “process each element” rather than “loop from 0 to N-1”. Foreach generates optimized code for arrays and Lists equivalent to manual indexing, with no performance penalty.

Example 9: Methods - Basic Syntax

Methods encapsulate reusable logic with parameters and return values. They’re the fundamental building block of C# code organization.

// Example 9: Methods - Basic Syntax
int Add(int x, int y)        // => Method signature: return type, name, parameters
{                            // => Parameters: x and y (both int)
    return x + y;            // => return keyword REQUIRED to return value
                             // => Returns sum of x and y
}                            // => Method type: (int, int) -> int

int result = Add(3, 5);      // => Method invocation with arguments 3 and 5
                             // => Calls Add method
                             // => result is 8 (3 + 5)
Console.WriteLine(result);   // => Outputs: 8

// Method with no return value (void):
void PrintMessage(string message)
{                            // => void means no return value
                             // => message is parameter (string type)
    Console.WriteLine($"Message: {message}");
                             // => Executes for side effect (printing)
                             // => $ enables string interpolation
}                            // => No return statement needed for void

PrintMessage("Hello");       // => Invocation with "Hello" argument
                             // => Outputs: Message: Hello

Key Takeaway: Methods use return to return values. void indicates no return value. Parameters declared with type annotations.

Why It Matters: Methods provide code reuse and abstraction boundaries. Unlike functional languages where functions are values, C# methods are class members (except local functions), making them the primary organizational unit for business logic in object-oriented designs.

Example 10: Lambda Expressions

Lambda expressions create anonymous functions inline, commonly used with LINQ and higher-order methods.

// Example 10: Lambda Expressions
Func<int, int> double = x => x * 2;
                             // => Lambda: parameter => expression
                             // => Func<int, int>: type of function (int -> int)
                             // => x is parameter (type inferred as int)
                             // => x * 2 is expression body
                             // => Creates inline function without separate method declaration

int result = double(5);      // => Invoke lambda like method
                             // => result is 10

// Multi-parameter lambda:
Func<int, int, int> add = (x, y) => x + y;
                             // => Parentheses REQUIRED for multiple parameters
                             // => Func<int, int, int>: (int, int) -> int
int sum = add(3, 5);         // => sum is 8

// Lambda with statement body:
Func<int, int> square = x =>
{                            // => Braces for multi-statement body
    int result = x * x;      // => Local variable in lambda
    return result;           // => return REQUIRED in statement body
};                           // => Semicolon ends variable declaration
int squared = square(4);     // => squared is 16

Console.WriteLine($"{result}, {sum}, {squared}");
                             // => Outputs: 10, 8, 16

Key Takeaway: Lambdas use param => expression or param => { statements } syntax. Declare with Func<> delegate types.

Why It Matters: Lambdas enable functional programming patterns in C# - LINQ queries, event handlers, and callbacks all use lambda syntax extensively. Modern C# codebases use lambdas extensively in LINQ operations, replacing verbose anonymous method syntax from C# 2.0.

Example 11: Classes and Objects

Classes define object templates with fields, properties, and methods. They’re C#’s primary abstraction mechanism.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[Person Class<br/>Template]:::blue --> B[alice Instance<br/>Name: Alice<br/>Age: 30]:::orange
    A --> C[bob Instance<br/>Name: Bob<br/>Age: 25]:::teal

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff

Code:

// Example 11: Classes and Objects
class Person                 // => Class definition (reference type)
{                            // => Classes are mutable by default
    public string Name;      // => Public field (direct access allowed)
                             // => Reference type (string)
    public int Age;          // => Value type field (int)
                             // => public allows access from anywhere
}                            // => Semicolon NOT required after class

Person alice = new Person(); // => Create instance with new keyword
                             // => alice is reference to Person object on heap
                             // => Fields initialized to defaults (null, 0)

alice.Name = "Alice";        // => Assign to Name field
                             // => alice.Name is now "Alice"
alice.Age = 30;              // => Assign to Age field
                             // => alice.Age is now 30

Person bob = new Person      // => Object initializer syntax (C# 3.0+)
{                            // => Braces contain initialization expressions
    Name = "Bob",            // => Initialize Name field to "Bob"
    Age = 25                 // => Initialize Age field to 25
};                           // => Semicolon ends statement
                             // => bob.Name="Bob", bob.Age=25

Console.WriteLine($"{alice.Name} is {alice.Age}");
                             // => String interpolation with $ prefix
                             // => Outputs: Alice is 30
Console.WriteLine($"{bob.Name} is {bob.Age}");
                             // => Outputs: Bob is 25

Key Takeaway: Classes use class Name { members } syntax. Create instances with new. Fields are mutable by default.

Why It Matters: Classes are reference types - multiple variables can reference the same object, enabling shared mutable state. This power comes with responsibility - many C# bugs relate to unexpected object mutations. Understanding reference semantics is critical for writing correct multi-threaded code.

Example 12: Properties with Get/Set

Properties provide controlled access to private fields through getters and setters, encapsulating implementation details.

// Example 12: Properties with Get/Set
class Person                 // => Person class with encapsulated fields
{
    private string _name;    // => Private field (backing field)
                             // => Convention: _ prefix for private fields
    private int _age;        // => Private int field

    public string Name       // => Public property
    {                        // => Provides controlled access to _name
        get { return _name; }
                             // => Getter: returns private field value
        set { _name = value; }
                             // => Setter: value is implicit parameter
                             // => Assigns to private field
    }

    public int Age           // => Public Age property
    {                        // => Provides validated access to _age
        get { return _age; }
                             // => Getter: returns _age value
        set                  // => Setter with validation logic
        {                    // => value is implicit parameter (int)
            if (value >= 0)  // => Check age is non-negative
            {                // => Valid ages only
                _age = value;
                             // => Only update if valid
            }                // => Invalid values ignored (silent failure)
        }
    }
}

Person alice = new Person();
                             // => Create Person instance
                             // => _name=null, _age=0 (defaults)
alice.Name = "Alice";        // => Calls Name setter with "Alice"
                             // => _name field becomes "Alice"
alice.Age = 30;              // => Calls Age setter with 30
                             // => Validation passes (30 >= 0)
                             // => _age field becomes 30

Console.WriteLine($"{alice.Name} is {alice.Age}");
                             // => Calls Name getter and Age getter
                             // => Outputs: Alice is 30

alice.Age = -5;              // => Calls setter with invalid value -5
                             // => Fails validation (-5 < 0)
                             // => _age unchanged (remains 30)
Console.WriteLine(alice.Age);// => Calls Age getter
                             // => Outputs: 30 (invalid value rejected)

Key Takeaway: Properties use get and set accessors. Setters receive value parameter implicitly. Enable validation and computed values.

Why It Matters: Properties maintain encapsulation while providing field-like syntax. They’re the .NET standard for data access - public fields violate conventions and break databinding, reflection, and serialization frameworks that expect property semantics.

Example 13: Auto-Implemented Properties

Auto-implemented properties eliminate boilerplate for simple getters and setters, improving code conciseness.

// Example 13: Auto-Implemented Properties
class Person                 // => Person class with auto-properties
{
    public string Name { get; set; }
                             // => Auto-implemented property
                             // => Compiler generates private backing field
                             // => public get and set accessors

    public int Age { get; set; }
                             // => Same pattern for Age property
                             // => Mutable through public setter

    public string Email { get; private set; }
                             // => Public getter, private setter
                             // => Can only be set within class
                             // => Immutable from outside class

    public Person(string email)
    {                        // => Constructor with email parameter
                             // => Required for Email initialization
        Email = email;       // => Private setter accessible here
                             // => Email can't be changed after construction
    }
}

Person alice = new Person("alice@example.com")
{                            // => Constructor + object initializer
                             // => Calls constructor first with email
    Name = "Alice",          // => Uses public Name setter
    Age = 30                 // => Uses public Age setter
};                           // => Semicolon ends statement
                             // => alice.Email is "alice@example.com"

Console.WriteLine($"{alice.Name}, {alice.Age}, {alice.Email}");
                             // => String interpolation with all properties
                             // => Outputs: Alice, 30, alice@example.com

// alice.Email = "new@example.com";  // => ERROR: setter is private
                             // => Compile error: Email setter not accessible

Key Takeaway: Auto-properties use { get; set; } syntax. Make setters private for immutability. Use in constructors for initialization.

Why It Matters: Auto-properties significantly reduce boilerplate compared to manual properties with backing fields. Private setters enable immutability patterns without readonly fields, supporting scenarios where initialization must occur in constructors rather than field initializers.

Example 14: Constructors

Constructors initialize object state when instances are created. They run before object is fully available.

// Example 14: Constructors
class Person                 // => Person class with multiple constructors
{
    public string Name { get; set; }
                             // => Auto-property for Name
    public int Age { get; set; }
                             // => Auto-property for Age

    public Person()          // => Default constructor (no parameters)
    {                        // => Called with: new Person()
        Name = "Unknown";    // => Sets default Name value
        Age = 0;             // => Sets default Age value
    }                        // => Provides safe defaults

    public Person(string name, int age)
    {                        // => Parameterized constructor
                             // => name is string parameter
                             // => age is int parameter
        Name = name;         // => Initialize Name from parameter
        Age = age;           // => Sets Age from parameter
    }                        // => Called with: new Person("Alice", 30)
}

Person person1 = new Person();
                             // => Calls default constructor (no arguments)
                             // => person1.Name="Unknown", person1.Age=0

Person person2 = new Person("Bob", 25);
                             // => Calls parameterized constructor
                             // => Passes "Bob" for name, 25 for age
                             // => person2.Name="Bob", person2.Age=25

Console.WriteLine($"{person1.Name}, {person1.Age}");
                             // => Accesses Name and Age properties
                             // => Outputs: Unknown, 0
Console.WriteLine($"{person2.Name}, {person2.Age}");
                             // => Outputs: Bob, 25

Key Takeaway: Constructors have same name as class, no return type. Multiple constructors provide initialization flexibility.

Why It Matters: Constructors guarantee invariants - required fields can be validated before objects become available. Dependency injection frameworks rely on constructor injection to provide dependencies, making constructors the standard initialization pattern in enterprise C# applications.

Example 15: Collections - List

List<T> is a generic, dynamically-sized collection - the most commonly used collection type in C#.

// Example 15: Collections - List<T>
List<int> numbers = new List<int>();
                             // => Generic list of integers
                             // => Type parameter: <int>
                             // => Initially empty (Count=0)

numbers.Add(1);              // => Append 1 to list
                             // => numbers is [1], Count=1
numbers.Add(2);              // => numbers is [1, 2], Count=2
numbers.Add(3);              // => numbers is [1, 2, 3], Count=3

int first = numbers[0];      // => Index access with [index]
                             // => first is 1 (zero-based indexing)

numbers[0] = 10;             // => MUTATION: update element at index 0
                             // => numbers is [10, 2, 3]

numbers.Remove(2);           // => Remove first occurrence of value 2
                             // => numbers is [10, 3], Count=2

// Collection initializer:
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
                             // => Initialize with values
                             // => names is ["Alice", "Bob", "Charlie"]

Console.WriteLine(string.Join(", ", numbers));
                             // => Join elements with separator
                             // => Outputs: 10, 3
Console.WriteLine(string.Join(", ", names));
                             // => Outputs: Alice, Bob, Charlie

Key Takeaway: List<T> provides dynamic sizing. Access elements with [index]. Initialize with { } syntax. Mutable by default.

Why It Matters: List is the default collection choice for most scenarios - it provides O(1) indexed access, O(1) amortized append, and full LINQ support. Understanding List vs Array vs IEnumerable tradeoffs is fundamental to C# performance optimization.

Example 16: Collections - Dictionary<TKey, TValue>

Dictionaries provide key-value storage with O(1) average lookup time, essential for caching and mapping scenarios.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A["Dictionary<string, int>"]:::blue
    A --> B["Alice → 30"]:::orange
    A --> C["Bob → 25"]:::teal
    A --> D["Charlie → 35"]:::orange

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#DE8F05,stroke:#000,color:#fff

Code:

// Example 16: Collections - Dictionary<TKey, TValue>
Dictionary<string, int> ages = new Dictionary<string, int>();
                             // => Generic dictionary: key=string, value=int
                             // => Type parameters: TKey=string, TValue=int
                             // => Initially empty (Count=0)

ages["Alice"] = 30;          // => Add key-value pair using indexer
                             // => ages has {"Alice": 30}, Count=1
ages["Bob"] = 25;            // => Add another pair
                             // => ages has {"Alice": 30, "Bob": 25}
ages["Charlie"] = 35;        // => Count=3

int aliceAge = ages["Alice"];// => Index with key (not integer index)
                             // => aliceAge is 30
                             // => Throws KeyNotFoundException if key missing

ages["Alice"] = 31;          // => MUTATION: update existing key
                             // => ages["Alice"] is now 31 (overwrites 30)

bool hasCharlie = ages.ContainsKey("Charlie");
                             // => Check if key exists (O(1) operation)
                             // => hasCharlie is true

// TryGetValue pattern (recommended):
if (ages.TryGetValue("Alice", out int age))
{                            // => Tries to get value for key "Alice"
                             // => If found: returns true, sets age to value
                             // => out parameter receives value
    Console.WriteLine($"Alice is {age}");
                             // => age is 31 (from dictionary)
                             // => Outputs: Alice is 31
}

// Collection initializer:
Dictionary<string, int> scores = new Dictionary<string, int>
{                            // => Initialize with key-value pairs
    ["Alice"] = 100,         // => Index initializer syntax
    ["Bob"] = 85             // => scores has {"Alice": 100, "Bob": 85}
};

Console.WriteLine($"Count: {ages.Count}");
                             // => Outputs: Count: 3

Key Takeaway: Dictionaries use [key] syntax for access. Use TryGetValue to avoid exceptions. Check ContainsKey before access.

Why It Matters: Dictionaries enable O(1) lookups critical for performance - caching, indexing, and mapping operations. TryGetValue pattern avoids exceptions in hot paths, improving performance by 10-100x compared to exception-based control flow.

Example 17: Collections - HashSet

HashSets store unique elements with O(1) add/contains operations, ideal for deduplication and membership testing.

// Example 17: Collections - HashSet<T>
HashSet<int> uniqueNumbers = new HashSet<int>();
                             // => HashSet of integers
                             // => Stores unique values only
                             // => Initially empty (Count=0)

uniqueNumbers.Add(1);        // => Add 1 to set
                             // => uniqueNumbers is {1}, Count=1
                             // => Returns true (element added)

bool added = uniqueNumbers.Add(1);
                             // => Try to add duplicate 1
                             // => Returns false (already exists)
                             // => uniqueNumbers still {1}, Count=1

uniqueNumbers.Add(2);        // => uniqueNumbers is {1, 2}
uniqueNumbers.Add(3);        // => uniqueNumbers is {1, 2, 3}

bool contains = uniqueNumbers.Contains(2);
                             // => O(1) membership test
                             // => contains is true

// Collection initializer:
HashSet<string> tags = new HashSet<string> { "c-sharp", "dotnet", "tutorial" };
                             // => Initialize with values
                             // => Duplicates automatically removed

tags.Add("c-sharp");         // => Duplicate, not added
                             // => tags still has 3 elements

Console.WriteLine($"Count: {uniqueNumbers.Count}");
                             // => Outputs: Count: 3
Console.WriteLine(string.Join(", ", uniqueNumbers));
                             // => Order not guaranteed in HashSet
                             // => Outputs: 1, 2, 3 (may vary)

Key Takeaway: HashSet guarantees uniqueness. Add returns bool indicating whether element was added. Order is not preserved.

Why It Matters: HashSets eliminate duplicates efficiently - processing 1 million elements with List.Distinct() takes seconds, while HashSet.Add loops take milliseconds. Use HashSets for deduplication, set operations (union, intersect), and fast membership testing.

Example 18: LINQ - Where (Filtering)

LINQ (Language Integrated Query) provides functional operations on collections through extension methods and query syntax.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[Source: 1,2,3,4,5,6,7,8,9,10]:::blue -->|Where n % 2 == 0| B[Filtered: 2,4,6,8,10]:::orange
    B -->|ToList| C[Result List]:::teal

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff

Code:

// Example 18: LINQ - Where (Filtering)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
                             // => Source collection: 1 through 10

IEnumerable<int> evens = numbers.Where(n => n % 2 == 0);
                             // => Where is extension method
                             // => Predicate lambda: n => n % 2 == 0
                             // => Returns IEnumerable<int> (lazy)
                             // => No filtering happens yet (deferred execution)

List<int> evensList = evens.ToList();
                             // => ToList() forces evaluation
                             // => Creates new list with results
                             // => evensList is [2, 4, 6, 8, 10]

// Method chaining:
IEnumerable<int> result = numbers
    .Where(n => n > 5)       // => Filter: keep elements > 5
                             // => Intermediate result: [6, 7, 8, 9, 10]
    .Where(n => n % 2 == 0); // => Filter: keep even numbers
                             // => Final result (lazy): [6, 8, 10]

Console.WriteLine(string.Join(", ", evensList));
                             // => Outputs: 2, 4, 6, 8, 10
Console.WriteLine(string.Join(", ", result));
                             // => Forces evaluation, Outputs: 6, 8, 10

Key Takeaway: Where filters with predicate lambda. Returns IEnumerable<T> (lazy). Chain multiple operations. Use ToList() to force evaluation.

Why It Matters: LINQ enables declarative data transformations - queries express WHAT to compute, not HOW. Deferred execution optimizes performance by eliminating intermediate collections, and LINQ-to-SQL/EF translates C# queries to database SQL, enabling type-safe database access.

Example 19: LINQ - Select (Mapping)

Select transforms each element in a collection, projecting to a new type or value.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[Source: 1,2,3,4,5]:::blue -->|Select n => n * 2| B[Transformed: 2,4,6,8,10]:::orange
    B -->|ToList| C[Result List]:::teal

    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff

Code:

// Example 19: LINQ - Select (Mapping)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
                             // => Source: [1, 2, 3, 4, 5]

IEnumerable<int> doubled = numbers.Select(n => n * 2);
                             // => Select transforms each element
                             // => Lambda: n => n * 2
                             // => Result (lazy): [2, 4, 6, 8, 10]

List<int> doubledList = doubled.ToList();
                             // => Force evaluation
                             // => doubledList is [2, 4, 6, 8, 10]

// Projection to anonymous type:
var people = new List<Person>
{                            // => List of Person objects
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};

var names = people.Select(p => p.Name).ToList();
                             // => Project to names only
                             // => Lambda: p => p.Name
                             // => names is ["Alice", "Bob"] (List<string>)

// Anonymous type projection:
var summary = people.Select(p => new { p.Name, IsAdult = p.Age >= 18 }).ToList();
                             // => Projects to anonymous type
                             // => Creates objects with Name and IsAdult properties
                             // => summary[0] is { Name = "Alice", IsAdult = true }
                             // => summary[1] is { Name = "Bob", IsAdult = true }

Console.WriteLine(string.Join(", ", doubledList));
                             // => Outputs: 2, 4, 6, 8, 10
Console.WriteLine(string.Join(", ", names));
                             // => Outputs: Alice, Bob
Console.WriteLine($"{summary[0].Name}: {summary[0].IsAdult}");
                             // => Outputs: Alice: True

Key Takeaway: Select maps elements with transformation lambda. Can project to same type, different type, or anonymous types.

Why It Matters: Select enables data transformation without loops - extract fields, compute derived values, reshape objects. LINQ-to-SQL/EF translates Select to SQL SELECT clauses, pushing projections to databases for performance (only requested columns transferred).

Example 20: LINQ - OrderBy/OrderByDescending

Ordering operations sort collections by key selectors, producing sorted sequences.

// Example 20: LINQ - OrderBy/OrderByDescending
List<int> numbers = new List<int> { 5, 2, 8, 1, 9, 3 };
                             // => Unsorted: [5, 2, 8, 1, 9, 3]

IEnumerable<int> sorted = numbers.OrderBy(n => n);
                             // => OrderBy with key selector: n => n (identity)
                             // => Sorts ascending by element value
                             // => Result (lazy): [1, 2, 3, 5, 8, 9]

List<int> sortedList = sorted.ToList();
                             // => Force evaluation
                             // => sortedList is [1, 2, 3, 5, 8, 9]

IEnumerable<int> descending = numbers.OrderByDescending(n => n);
                             // => Sort descending
                             // => Result: [9, 8, 5, 3, 2, 1]

// Ordering complex objects:
var people = new List<Person>
{
    new Person { Name = "Charlie", Age = 35 },
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};

var byAge = people.OrderBy(p => p.Age).ToList();
                             // => Sort by Age property (ascending)
                             // => byAge[0] is Bob (25)
                             // => byAge[1] is Alice (30)
                             // => byAge[2] is Charlie (35)

var byName = people.OrderBy(p => p.Name).ToList();
                             // => Sort by Name (alphabetically)
                             // => byName[0] is Alice
                             // => byName[1] is Bob
                             // => byName[2] is Charlie

Console.WriteLine(string.Join(", ", sortedList));
                             // => Outputs: 1, 2, 3, 5, 8, 9
Console.WriteLine(string.Join(", ", byAge.Select(p => p.Name)));
                             // => Outputs: Bob, Alice, Charlie

Key Takeaway: OrderBy sorts ascending, OrderByDescending descending. Key selector lambda chooses sort property.

Why It Matters: LINQ ordering uses stable sort algorithms - equal elements maintain original order. Combined with ThenBy for multi-level sorting, this enables complex sorting without manual comparers, improving code clarity.

Example 21: Exception Handling - Try-Catch

Try-catch blocks handle runtime errors gracefully, preventing application crashes and enabling error recovery.

// Example 21: Exception Handling - Try-Catch
try                          // => try block: code that might throw
{                            // => Protected region for error handling
    int[] numbers = { 1, 2, 3 };
                             // => Array with 3 elements (indices 0-2)
    Console.WriteLine(numbers[10]);
                             // => Index 10 out of bounds (array has 3 elements)
                             // => Throws IndexOutOfRangeException
                             // => Execution jumps to catch block
}
catch (IndexOutOfRangeException ex)
{                            // => Catches specific exception type
                             // => ex is exception object with details
    Console.WriteLine($"Error: {ex.Message}");
                             // => Access exception message property
                             // => Outputs: Error: Index was outside the bounds of the array.
}

// Multiple catch blocks:
try                          // => Second try block
{                            // => Protected region
    string? text = null;     // => Nullable string set to null
    Console.WriteLine(text.Length);
                             // => Null reference access (text is null)
                             // => Throws NullReferenceException
}
catch (NullReferenceException ex)
{                            // => Catches null reference exceptions specifically
                             // => More specific than Exception
    Console.WriteLine("Null reference caught");
                             // => Outputs: Null reference caught
}
catch (Exception ex)         // => Catches any exception (base class)
{                            // => More specific catches MUST come first
                             // => This is catch-all for other exceptions
    Console.WriteLine($"General error: {ex.Message}");
                             // => Not executed (specific catch handled it)
}

Key Takeaway: Use try-catch for error handling. Catch specific exceptions first, then general ones. Access exception details via ex object.

Why It Matters: Exceptions provide structured error handling superior to error codes. However, exceptions are expensive (1000x slower than normal flow) - use them for exceptional conditions only, not control flow. Reserve exceptions for errors, use return values for expected alternatives.

Example 22: Exception Handling - Finally

Finally blocks execute regardless of whether exceptions occur, ensuring cleanup code runs.

// Example 22: Exception Handling - Finally
void ProcessFile()           // => Method demonstrating finally block
{
    try                      // => Protected code block
    {
        Console.WriteLine("Opening file...");
                             // => Simulates file operation
                             // => Outputs: Opening file...
        throw new Exception("File error");
                             // => Simulated error (explicit throw)
                             // => Jumps to catch block
    }
    catch (Exception ex)     // => Catch block handles exception
    {                        // => ex contains exception details
        Console.WriteLine($"Error: {ex.Message}");
                             // => Outputs: Error: File error
    }
    finally                  // => Finally block ALWAYS executes
    {                        // => Runs after try or catch completes
                             // => Guaranteed execution for cleanup
        Console.WriteLine("Closing file...");
                             // => Cleanup code (close files, release locks)
                             // => Outputs: Closing file...
                             // => Executes even if exception thrown
    }
}

ProcessFile();               // => Calls method
                             // => Output order:
                             // => Opening file...
                             // => Error: File error
                             // => Closing file...

Key Takeaway: Finally blocks always execute - with or without exceptions. Use for cleanup (closing files, releasing locks).

Why It Matters: Finally guarantees cleanup runs even during exceptions, early returns, or control flow changes. Modern C# prefers using statements (automatic Dispose) over finally for resource cleanup, but finally remains essential for non-IDisposable cleanup scenarios.

Example 23: String Methods

Strings provide extensive manipulation methods for text processing, all returning new strings (immutability).

// Example 23: String Methods
string text = "Hello, World!";
                             // => Immutable string (reference type)

string upper = text.ToUpper();
                             // => Creates NEW string in uppercase
                             // => upper is "HELLO, WORLD!"
                             // => Original text unchanged

string lower = text.ToLower();
                             // => lower is "hello, world!"

string substring = text.Substring(0, 5);
                             // => Extract substring: start index 0, length 5
                             // => substring is "Hello"

string replaced = text.Replace("World", "C#");
                             // => Replace substring
                             // => replaced is "Hello, C#!"

string[] parts = text.Split(',');
                             // => Split by delimiter character
                             // => parts is ["Hello", " World!"] (string array)
                             // => Note: space remains in second element

string trimmed = "  spaces  ".Trim();
                             // => Remove leading/trailing whitespace
                             // => trimmed is "spaces"

bool contains = text.Contains("World");
                             // => Check if substring exists
                             // => contains is true

bool starts = text.StartsWith("Hello");
                             // => Check prefix
                             // => starts is true

Console.WriteLine(upper);    // => Outputs: HELLO, WORLD!
Console.WriteLine(substring);// => Outputs: Hello
Console.WriteLine(string.Join("|", parts));
                             // => Join array with separator
                             // => Outputs: Hello| World!

Key Takeaway: String methods return new strings (immutability). Common operations: ToUpper, ToLower, Substring, Replace, Split, Trim.

Why It Matters: Immutable strings prevent bugs from unexpected mutations and enable string interning (memory optimization). For extensive string building, use StringBuilder - repeated concatenation with + creates O(n²) temporary objects, while StringBuilder achieves O(n) performance.

Example 24: String Interpolation

String interpolation provides readable syntax for embedding expressions in strings, replacing String.Format.

// Example 24: String Interpolation
string name = "Alice";
int age = 30;
double salary = 75000.50;

string message = $"Name: {name}, Age: {age}";
                             // => $"..." enables interpolation
                             // => {name} embeds variable value
                             // => {age} embeds integer value
                             // => message is "Name: Alice, Age: 30"

string formatted = $"Salary: {salary:C}";
                             // => :C formats as currency
                             // => Uses current culture (e.g., $75,000.50)
                             // => formatted is "Salary: $75,000.50" (US culture)

string precision = $"Pi: {Math.PI:F2}";
                             // => :F2 formats float with 2 decimals
                             // => precision is "Pi: 3.14"

// Expression in interpolation:
string calculation = $"Sum: {2 + 3}";
                             // => Evaluates expression 2 + 3
                             // => calculation is "Sum: 5"

// Multi-line with verbatim strings:
string multiline = $@"Name: {name}
Age: {age}";                 // => @ enables multi-line strings
                             // => Preserves newlines
                             // => Outputs:
                             // => Name: Alice
                             // => Age: 30

Console.WriteLine(message);  // => Outputs: Name: Alice, Age: 30
Console.WriteLine(formatted);// => Outputs: Salary: $75,000.50
Console.WriteLine(precision);// => Outputs: Pi: 3.14

Key Takeaway: Use $"..." for string interpolation. Embed expressions with {expression}. Format with {value:format} specifiers.

Why It Matters: String interpolation improves readability compared to String.Format - expressions appear inline rather than separated as arguments. Compiler generates efficient code equivalent to String.Format, with no performance penalty.

Example 25: Value Types vs Reference Types

Understanding value vs reference semantics is critical for C# memory management and mutation behavior.

// Example 25: Value Types vs Reference Types

// VALUE TYPE: struct (stack-allocated)
int x = 10;                  // => Value type (int is struct)
                             // => Stored on stack
                             // => Contains actual value 10

int y = x;                   // => COPY: y gets copy of x's value
                             // => y is 10 (independent copy)
                             // => Value types create independent copies on assignment

x = 20;                      // => Modify x
                             // => x is 20, y remains 10 (no connection)
                             // => Demonstrates value type independence

Console.WriteLine($"x={x}, y={y}");
                             // => Outputs: x=20, y=10

// REFERENCE TYPE: class (heap-allocated)
class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Point p1 = new Point { X = 10, Y = 20 };
                             // => Reference type (class)
                             // => p1 holds REFERENCE to object on heap
                             // => Object has X=10, Y=20

Point p2 = p1;               // => REFERENCE COPY: p2 references SAME object
                             // => Both p1 and p2 point to same object
                             // => No object copying occurs

p1.X = 30;                   // => Modify through p1
                             // => Changes SHARED object
                             // => Both p1.X and p2.X are 30 (same object)

Console.WriteLine($"p1.X={p1.X}, p2.X={p2.X}");
                             // => Outputs: p1.X=30, p2.X=30

// VALUE TYPE: struct example
struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
}

PointStruct s1 = new PointStruct { X = 10, Y = 20 };
                             // => Value type (struct)
                             // => s1 stores actual data

PointStruct s2 = s1;         // => VALUE COPY: s2 gets copy of s1
                             // => s2 is independent copy

s1.X = 30;                   // => Modify s1
                             // => s1.X=30, s2.X remains 10 (separate copies)

Console.WriteLine($"s1.X={s1.X}, s2.X={s2.X}");
                             // => Outputs: s1.X=30, s2.X=10

Key Takeaway: Value types (structs, primitives) copy by value. Reference types (classes) copy by reference. Modifying references affects all variables referencing same object.

Why It Matters: Reference vs value semantics affects many C# memory bugs. Classes share state (reference types), enabling aliasing bugs where unexpected modifications occur. Structs provide value semantics but have boxing overhead when converted to interfaces or object.

Example 26: Null Conditional Operator (?.)

The null conditional operator safely accesses members on potentially null references, preventing NullReferenceException.

// Example 26: Null Conditional Operator (?.)
string? name = null;         // => Nullable string (can be null)

int? length = name?.Length;  // => ?. operator: null-conditional access
                             // => If name is null: returns null (no exception)
                             // => If name is not null: returns name.Length
                             // => length is null (name is null)

Console.WriteLine(length?.ToString() ?? "null");
                             // => ?. chains with ?? (null-coalescing)
                             // => Outputs: null

name = "Alice";              // => Assign non-null value
length = name?.Length;       // => name is not null
                             // => Returns name.Length (5)
                             // => length is 5 (int? with value)

Console.WriteLine(length);   // => Outputs: 5

// Chaining null conditionals:
class Person
{
    public string? Name { get; set; }
}

Person? person = null;
int? nameLength = person?.Name?.Length;
                             // => First ?. checks person is not null
                             // => If null: short-circuits, returns null
                             // => Second ?. checks Name is not null
                             // => nameLength is null (person is null)

person = new Person { Name = "Bob" };
nameLength = person?.Name?.Length;
                             // => person is not null, Name is "Bob"
                             // => Returns 3 (length of "Bob")
                             // => nameLength is 3

Console.WriteLine(nameLength ?? 0);
                             // => Outputs: 3

Key Takeaway: Use ?. for safe member access on nullable references. Returns null if reference is null, preventing exceptions. Chain multiple ?. operators.

Why It Matters: Null conditional operators eliminate many defensive null checks, improving code readability. Combined with nullable reference types (C# 8.0+), they enable null-safe code without verbose if-checks at every member access.

Example 27: Null Coalescing Operator (??)

The null coalescing operator provides default values when expressions evaluate to null.

// Example 27: Null Coalescing Operator (??)
string? name = null;

string displayName = name ?? "Unknown";
                             // => ?? operator: null-coalescing
                             // => If left is null: returns right
                             // => If left is not null: returns left
                             // => displayName is "Unknown" (name is null)

Console.WriteLine(displayName);
                             // => Outputs: Unknown

name = "Alice";
displayName = name ?? "Unknown";
                             // => name is not null ("Alice")
                             // => Returns "Alice"
                             // => displayName is "Alice"

Console.WriteLine(displayName);
                             // => Outputs: Alice

// Chaining null coalescing:
string? first = null;
string? second = null;
string? third = "Default";

string result = first ?? second ?? third ?? "Fallback";
                             // => Evaluates left-to-right
                             // => first is null, tries second
                             // => second is null, tries third
                             // => third is "Default", returns it
                             // => result is "Default"

Console.WriteLine(result);   // => Outputs: Default

// Null coalescing assignment (C# 8.0+):
string? value = null;
value ??= "Initialized";     // => ??= assigns only if left is null
                             // => value is null, assigns "Initialized"
                             // => value is now "Initialized"

value ??= "New Value";       // => value is not null
                             // => No assignment occurs
                             // => value remains "Initialized"

Console.WriteLine(value);    // => Outputs: Initialized

Key Takeaway: Use ?? to provide defaults for null values. Use ??= for null-conditional assignment. Both operators short-circuit evaluation.

Why It Matters: Null coalescing eliminates ternary operators for null checks (name != null ? name : "Unknown"), improving readability. Assignment form (??=) enables lazy initialization patterns without temporary variables.

Example 28: Collection Expressions (C# 12.0+)

Collection expressions provide uniform syntax for creating collections, replacing verbose initializer syntax.

// Example 28: Collection Expressions (C# 12.0+)

// Arrays:
int[] numbers = [1, 2, 3, 4, 5];
                             // => Collection expression syntax: [elements]
                             // => Creates int array
                             // => numbers is [1, 2, 3, 4, 5]

// Lists:
List<string> names = ["Alice", "Bob", "Charlie"];
                             // => Creates List<string>
                             // => names is ["Alice", "Bob", "Charlie"]

// Spread operator (..)
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = [..first, ..second];
                             // => .. spreads collection elements
                             // => combined is [1, 2, 3, 4, 5, 6]

// Mixed expressions:
int[] mixed = [0, ..first, 7, 8, ..second, 9];
                             // => Combine literals and spreads
                             // => mixed is [0, 1, 2, 3, 7, 8, 4, 5, 6, 9]

Console.WriteLine(string.Join(", ", numbers));
                             // => Outputs: 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", names));
                             // => Outputs: Alice, Bob, Charlie
Console.WriteLine(string.Join(", ", combined));
                             // => Outputs: 1, 2, 3, 4, 5, 6

Key Takeaway: Collection expressions use [elements] syntax. Spread operator .. expands collections inline. Works with arrays, lists, and other collection types.

Why It Matters: Collection expressions reduce initialization boilerplate and provide consistent syntax across collection types. Spread operator eliminates manual concatenation loops, improving readability while maintaining performance (compiler optimizes to efficient array operations).

Example 29: Primary Constructors (C# 12.0+)

Primary constructors define constructor parameters inline with class declarations, reducing boilerplate.

// Example 29: Primary Constructors (C# 12.0+)
class Person(string name, int age)
{                            // => Primary constructor parameters: name, age
                             // => Parameters available throughout class body
                             // => No explicit constructor block needed

    public string Name { get; } = name;
                             // => Initialize property from parameter
                             // => Name is read-only (no setter)
                             // => Captured from primary constructor

    public int Age { get; } = age;
                             // => Age initialized from parameter
                             // => Read-only property

    public void PrintInfo()  // => Method can access parameters
    {                        // => Primary constructor params in scope
        Console.WriteLine($"{name} is {age} years old");
                             // => Directly use primary constructor parameters
                             // => No need for fields
    }
}

Person alice = new Person("Alice", 30);
                             // => Create instance with primary constructor
                             // => Passes "Alice" for name, 30 for age
                             // => alice.Name="Alice", alice.Age=30

alice.PrintInfo();           // => Calls method
                             // => Outputs: Alice is 30 years old

Console.WriteLine($"{alice.Name}, {alice.Age}");
                             // => Outputs: Alice, 30

// Combining with base class:
class Employee(string name, int age, string department)
    : Person(name, age)      // => Call base primary constructor with forwarding
{                            // => Additional parameter: department
                             // => Inherits Name and Age from Person
    public string Department { get; } = department;
                             // => Initialize Department from parameter
}

Employee bob = new Employee("Bob", 25, "Engineering");
                             // => Calls Employee primary constructor
                             // => Forwards name/age to Person constructor
                             // => bob.Name="Bob", bob.Age=25, bob.Department="Engineering"

bob.PrintInfo();             // => Inherited from Person
                             // => Outputs: Bob is 25 years old
Console.WriteLine(bob.Department);
                             // => Outputs: Engineering

Key Takeaway: Primary constructors use class Name(params) syntax. Parameters available throughout class. Reduces property initialization boilerplate.

Why It Matters: Primary constructors eliminate repetitive constructor code (parameter-to-field assignments). They encourage immutability patterns by defaulting to readonly properties, improving code safety without sacrificing readability.

Example 30: Record Types

Records provide value-based equality, immutability by default, and concise syntax for data modeling.

// Example 30: Record Types
record Person(string Name, int Age);
                             // => Record with positional parameters
                             // => Generates: properties, constructor, equality, ToString
                             // => Properties are init-only (immutable after construction)

Person alice = new Person("Alice", 30);
                             // => Create record instance
                             // => alice.Name="Alice", alice.Age=30

Person bob = new Person("Bob", 25);

// Value-based equality:
Person alice2 = new Person("Alice", 30);
                             // => Different object, SAME values

bool areEqual = alice == alice2;
                             // => == compares VALUES (not references)
                             // => Both have Name="Alice", Age=30
                             // => areEqual is true (value equality)

Console.WriteLine(areEqual); // => Outputs: True

// With-expressions (non-destructive mutation):
Person alice31 = alice with { Age = 31 };
                             // => Creates NEW record copying alice
                             // => Updates Age to 31
                             // => alice unchanged (immutable)
                             // => alice31 has Name="Alice", Age=31

Console.WriteLine($"alice: {alice.Age}, alice31: {alice31.Age}");
                             // => Outputs: alice: 30, alice31: 31

// ToString override:
Console.WriteLine(alice);    // => Automatic ToString implementation
                             // => Outputs: Person { Name = Alice, Age = 30 }

// Deconstruction:
var (name, age) = alice;     // => Deconstruct record to tuple
                             // => name is "Alice", age is 30

Console.WriteLine($"{name}, {age}");
                             // => Outputs: Alice, 30

Key Takeaway: Records use record Name(params) syntax. Provide value equality, immutability, and with-expressions for copying. Ideal for data transfer objects.

Why It Matters: Records eliminate substantial boilerplate for data classes - no manual Equals, GetHashCode, ToString, or copy methods needed. Value equality makes records ideal for DTOs, API models, and domain entities, enabling easier testing (deep equality) and functional programming patterns (immutability).


Next Steps

Continue to Intermediate (Examples 31-60) to learn:

  • Async/await for asynchronous programming
  • Generics and generic constraints
  • Interfaces and polymorphism
  • LINQ advanced operations (GroupBy, Join, Aggregate)
  • Extension methods
  • Pattern matching (type patterns, property patterns)
  • Delegates and events
  • File I/O and serialization

These 30 beginner examples cover 0-40% of C#’s features, establishing the imperative and object-oriented foundation. The remaining 60% (intermediate and advanced) builds on these fundamentals with async patterns, generics, and advanced type system features.

Last updated