C# Methods

Methods are fundamental building blocks in C# that encapsulate code logic, enabling code reuse, modularity, and organized structure. They define the behavior of objects and are essential for implementing functionality within classes and structs. Understanding methods is crucial for effective object-oriented programming in C#.

Table of Contents

  1. Introduction to Methods
  2. - What are Methods?
    - Purpose of Methods
    - Methods vs. Functions in C#
  3. Method Syntax
  4. - Access Modifiers
    - Return Type
    - Method Name
    - Parameters
    - Method Body
    - Example
  5. Types of Methods
  6. - Instance Methods
    - Static Methods
    - Extension Methods
    - Abstract Methods
    - Virtual and Override Methods
    - Sealed Methods
    - Partial Methods
  7. Parameters
  8. - Value Parameters
    - Reference Parameters (`ref`, `out`, `in`)
    - Optional Parameters
    - `params` Keyword
    - Named Arguments
  9. Return Types
  10. - `void` Methods
    - Returning Values
    - Using `out` and `ref` with Return Types
  11. Method Overloading
  12. - Definition
    - Rules for Overloading
    - Examples
  13. Method Overriding
  14. - Virtual Methods
    - Overriding Methods in Derived Classes
    - Abstract Methods
    - Example
  15. Generic Methods
  16. - Syntax
    - Benefits
    - Example
  17. Delegates and Lambda Expressions
  18. - Delegate Methods
    - Anonymous Methods
    - Lambda Expressions
    - LINQ Integration
  19. Asynchronous Methods
  20. - `async` and `await` Keywords
    - `Task` and `Task<T>` Return Types
    - Example
  21. Recursive Methods
  22. - Definition
    - Example
    - Tail Recursion
  23. Extension Methods
  24. - Syntax
    - Usage
    - Example
  25. Best Practices
  26. - Naming Conventions
    - Single Responsibility Principle
    - Keep Methods Short and Focused
    - Parameter Validation
    - Avoid Side Effects
  27. Common Mistakes
  28. - Incorrect Use of `ref`/`out`
    - Ignoring Return Values
    - Overusing Static Methods
    - Poor Naming
  29. Advanced Topics
  30. - Method Hiding (`new` Keyword)
    - Explicit Interface Implementation
    - Covariance and Contravariance in Methods
    - Optional Parameters vs. Method Overloading
    - Reflection and Methods
  31. Real-World Example
  32. - Scenario
    - Implementation
    - Explanation
  33. Summary

1. Introduction to Methods

What are Methods?

Methods in C# are blocks of code that perform specific tasks, can be invoked or called, and can return values. They are defined within classes or structs and are used to implement the behavior of objects.

Key Points:
- Encapsulation: Methods encapsulate code logic, promoting code reuse and modularity.
- Parameters and Return Values: Methods can accept inputs (parameters) and return outputs (return values).
- Access Control: Access modifiers determine the visibility and accessibility of methods.

Purpose of Methods

- Code Reusability: Avoids duplication by allowing the same code to be executed from multiple places.
- Organization: Groups related logic together, making code easier to manage and understand.
- Abstraction: Hides complex implementation details behind simple method calls.

Methods vs. Functions in C#

In C#, the term function is generally synonymous with method. However, technically:

- Methods: Defined within classes or structs.
- Functions: Standalone functions do not exist in C#; all functions are methods associated with a type.

Example:
public class Calculator
{
    // Method
    public int Add(int a, int b)
    {
        return a + b;
    }
}

2. Method Syntax

Understanding the syntax of methods is fundamental to defining and using them effectively in C#.

Components of a Method

1. Access Modifiers: Define the visibility of the method (`public`, `private`, `protected`, `internal`).
2. Return Type: Specifies the type of value the method returns (`void` if no value).
3. Method Name: Identifier for the method, following naming conventions.
4. Parameters: Inputs to the method, defined within parentheses.
5. Method Body: Contains the code to execute, enclosed in curly braces `{}`.

Syntax Breakdown

[AccessModifier] [ReturnType] [MethodName]([Parameters])
{
    // Method Body
}
- AccessModifier: Optional. Defaults to `private` if omitted.
- ReturnType: Mandatory. Defines the type of value returned.
- MethodName: Mandatory. Should be a verb or verb phrase.
- Parameters: Optional. Define inputs; multiple parameters separated by commas.

Example

public class MathOperations
{
    // Public method that adds two integers and returns the result
    public int Add(int a, int b)
    {
        return a + b;
    }

    // Private method that multiplies two integers
    private int Multiply(int a, int b)
    {
        return a * b;
    }

    // Method with no return value
    public void DisplaySum(int a, int b)
    {
        int sum = Add(a, b);
        Console.WriteLine($"Sum: {sum}");
    }
}

class Program
{
    static void Main()
    {
        MathOperations math = new MathOperations();
        math.DisplaySum(5, 3); // Output: Sum: 8

        // Directly accessing Multiply is not possible since it's private
        // int product = math.Multiply(5, 3); // Compile-time error
    }
}

Sample Output:
Sum: 8

Explanation:
- Add Method: Publicly accessible, takes two integers, and returns their sum.
- Multiply Method: Private, accessible only within `MathOperations` class.
- DisplaySum Method: Calls the `Add` method and displays the result.

3. Types of Methods

Methods in C# can be categorized based on various attributes such as their association with instances or classes, their ability to extend existing types, and their role in inheritance hierarchies.

Instance Methods

- Definition: Methods that operate on instances of a class.
- Access: Require creating an object of the class to be invoked.

- Example:
public class Person
{
    public string Name { get; set; }

    // Instance method
    public void Greet()
    {
        Console.WriteLine($"Hello, my name is {Name}.");
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person { Name = "Alice" };
        person.Greet(); // Output: Hello, my name is Alice.
    }
}

Static Methods

- Definition: Methods that belong to the class itself rather than any instance.
- Access: Invoked using the class name without creating an object.
- Use Cases: Utility functions, factory methods.
- Example:
public class MathUtilities
{
    // Static method
    public static int Square(int number)
    {
        return number * number;
    }
}

class Program
{
    static void Main()
    {
        int result = MathUtilities.Square(5);
        Console.WriteLine($"Square: {result}"); // Output: Square: 25
    }
}

Extension Methods

- Definition: Static methods that extend existing types without modifying their source code.
- Requirements: Defined in static classes and use the `this` keyword in the first parameter.
- Use Cases: Adding functionality to sealed classes or third-party libraries.

- Example:
public static class StringExtensions
{
    // Extension method for string type
    public static bool IsNullOrEmpty(this string str)
    {
        return string.IsNullOrEmpty(str);
    }
}

class Program
{
    static void Main()
    {
        string message = "";
        bool isEmpty = message.IsNullOrEmpty(); // Using extension method
        Console.WriteLine($"Is Empty: {isEmpty}"); // Output: Is Empty: True
    }
}

Abstract Methods

- Definition: Methods declared in abstract classes without an implementation.
- Purpose: Enforce that derived classes provide specific implementations.
- Requirements: Can only exist in abstract classes.

- Example:
public abstract class Shape
{
    // Abstract method
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }

    // Implementing abstract method
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

class Program
{
    static void Main()
    {
        Shape circle = new Circle { Radius = 3.0 };
        Console.WriteLine($"Area: {circle.CalculateArea()}"); // Output: Area: 28.274333882308138
    }
}

Virtual and Override Methods

- Virtual Methods:
- Definition: Methods that can be overridden in derived classes.
- Declaration: Use the `virtual` keyword.

- Example:
public class Animal
{
    // Virtual method
    public virtual void MakeSound()
    {
        Console.WriteLine("Some generic animal sound.");
    }
}

public class Dog : Animal
{
    // Overriding the virtual method
    public override void MakeSound()
    {
        Console.WriteLine("Bark!");
    }
}

class Program
{
    static void Main()
    {
        Animal genericAnimal = new Animal();
        genericAnimal.MakeSound(); // Output: Some generic animal sound.

        Animal dog = new Dog();
        dog.MakeSound(); // Output: Bark!
    }
}

- Override Methods:
- Definition: Methods in derived classes that provide a specific implementation of a virtual method.
- Declaration: Use the `override` keyword.

Sealed Methods

- Definition: Methods that cannot be overridden further in derived classes.
- Use Case: Prevent further modification of method behavior.
- Declaration: Use the `sealed` keyword in combination with `override`.

- Example:
public class BaseClass
{
    public virtual void Display()
    {
        Console.WriteLine("Base Display");
    }
}

public class DerivedClass : BaseClass
{
    // Sealed override method
    public sealed override void Display()
    {
        Console.WriteLine("Derived Display");
    }
}

public class FurtherDerivedClass : DerivedClass
{
    // Attempting to override Display will result in a compile-time error
    // public override void Display() { } // Error
}

class Program
{
    static void Main()
    {
        BaseClass obj = new DerivedClass();
        obj.Display(); // Output: Derived Display
    }
}

Partial Methods

- Definition: Methods that can be declared in one part of a partial class and optionally implemented in another part.
- Use Case: Allow code generation tools to add method hooks.
- Requirements: Must be declared within a partial class, return `void`, and cannot have access modifiers.

- Example:
// File: Person.Part1.cs
public partial class Person
{
    partial void OnNameChanged();

    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnNameChanged();
        }
    }
}

// File: Person.Part2.cs
public partial class Person
{
    // Implementing the partial method
    partial void OnNameChanged()
    {
        Console.WriteLine("Name has been changed.");
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "Alice"; // Output: Name has been changed.
    }
}

4. Parameters

Parameters allow methods to accept inputs, enabling dynamic and reusable code. C# provides various types of parameters to handle different scenarios.

Value Parameters

- Definition: Parameters passed by value; a copy of the data is made.
- Default Behavior: Most parameters are value parameters.

- Example:
public class Calculator
{
    public int Add(int a, int b)
    {
        a += 1; // Modifies local copy
        return a + b;
    }
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        int result = calc.Add(5, 3);
        Console.WriteLine($"Result: {result}"); // Output: Result: 9
    }
}

Explanation:
- The original `a` in `Main` remains unchanged.
- Only the local copy within `Add` is modified.

Reference Parameters (`ref`, `out`, `in`)

`ref` Keyword

- Definition: Passes arguments by reference, allowing methods to modify the caller's variable.
- Requirements: Variables must be initialized before being passed.

- Example:
public class Modifier
{
    public void Increment(ref int number)
    {
        number += 1;
    }
}

class Program
{
    static void Main()
    {
        int value = 5;
        Modifier mod = new Modifier();
        mod.Increment(ref value);
        Console.WriteLine($"Value after increment: {value}"); // Output: Value after increment: 6
    }
}

`out` Keyword

- Definition: Similar to `ref` but used when the method is expected to initialize the variable.
- Requirements: Variables do not need to be initialized before being passed.

- Example:
public class Parser
{
    public bool TryParseInt(string input, out int result)
    {
        return int.TryParse(input, out result);
    }
}

class Program
{
    static void Main()
    {
        Parser parser = new Parser();
        string input = "123";
        if(parser.TryParseInt(input, out int number))
        {
            Console.WriteLine($"Parsed Number: {number}"); // Output: Parsed Number: 123
        }
        else
        {
            Console.WriteLine("Invalid input.");
        }
    }
}

`in` Keyword (C# 7.2 and Later)

- Definition: Passes arguments by reference for read-only purposes.
- Use Case: Optimizes performance for large structures without allowing modification.

- Example:
public struct LargeStruct
{
    public int X;
    public int Y;
    // Assume more fields
}

public class Processor
{
    public void Display(in LargeStruct ls)
    {
        Console.WriteLine($"X: {ls.X}, Y: {ls.Y}");
        // ls.X = 10; // Compile-time error: cannot modify
    }
}

class Program
{
    static void Main()
    {
        LargeStruct ls = new LargeStruct { X = 5, Y = 10 };
        Processor proc = new Processor();
        proc.Display(in ls); // Output: X: 5, Y: 10
    }
}

Optional Parameters

- Definition: Parameters that have default values and can be omitted when calling the method.
- Syntax: Assign a default value in the method signature.

- Example:
public class Greeter
{
    public void Greet(string name = "Guest")
    {
        Console.WriteLine($"Hello, {name}!");
    }
}

class Program
{
    static void Main()
    {
        Greeter greeter = new Greeter();
        greeter.Greet(); // Output: Hello, Guest!
        greeter.Greet("Alice"); // Output: Hello, Alice!
    }
}

`params` Keyword

- Definition: Allows passing a variable number of arguments as an array.
- Use Case: Methods that need to accept zero or more arguments.

- Example:
public class MathOperations
{
    public int Sum(params int[] numbers)
    {
        return numbers.Sum();
    }
}

class Program
{
    static void Main()
    {
        MathOperations math = new MathOperations();
        Console.WriteLine(math.Sum(1, 2, 3)); // Output: 6
        Console.WriteLine(math.Sum(10, 20)); // Output: 30
        Console.WriteLine(math.Sum()); // Output: 0
    }
}

Named Arguments

- Definition: Specify arguments by the parameter name, allowing flexibility in the order of arguments.
- Use Case: Improves readability and allows skipping optional parameters.

- Example:
public class Logger
{
    public void Log(string message, string level = "Info", DateTime? timestamp = null)
    {
        Console.WriteLine($"[{level}] {timestamp?.ToString() ?? DateTime.Now.ToString()}: {message}");
    }
}

class Program
{
    static void Main()
    {
        Logger logger = new Logger();
        // Using named arguments to skip 'level'
        logger.Log(message: "System started."); // Output: [Info] [Current Timestamp]: System started.
        
        // Specifying 'level' and 'timestamp' out of order
        logger.Log(message: "An error occurred.", timestamp: DateTime.Now.AddMinutes(-5), level: "Error");
        // Output: [Error] [Timestamp 5 minutes ago]: An error occurred.
    }
}

5. Return Types

Methods can return values of any type, including built-in types, user-defined types, or `void` if no value is returned.

`void` Methods

- Definition: Methods that do not return a value.
- Use Case: Perform actions or operations without needing to provide feedback.

- Example:
public class Logger
{
    public void LogMessage(string message)
    {
        Console.WriteLine($"Log: {message}");
    }
}

class Program
{
    static void Main()
    {
        Logger logger = new Logger();
        logger.LogMessage("Application started."); // Output: Log: Application started.
    }
}

Returning Values

- Definition: Methods that return a value to the caller.
- Syntax: Specify the return type and use the `return` statement.

- Example:
public class Calculator
{
    public double Multiply(double a, double b)
    {
        return a * b;
    }
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        double product = calc.Multiply(4.5, 2.0);
        Console.WriteLine($"Product: {product}"); // Output: Product: 9
    }
}

Using `out` and `ref` with Return Types

- `out` Parameters: Allow methods to return multiple values.
- `ref` Parameters: Allow methods to modify the caller's variables and return values.

Example with `out` Parameter:
public class Parser
{
    public bool TryParseDouble(string input, out double result)
    {
        return double.TryParse(input, out result);
    }
}

class Program
{
    static void Main()
    {
        Parser parser = new Parser();
        string input = "123.45";
        if(parser.TryParseDouble(input, out double number))
        {
            Console.WriteLine($"Parsed Number: {number}"); // Output: Parsed Number: 123.45
        }
        else
        {
            Console.WriteLine("Invalid input.");
        }
    }
}

6. Method Overloading

Method Overloading allows multiple methods in the same class to have the same name but different parameters. It enhances code readability and usability by providing different ways to perform similar operations.

Definition

- Same Method Name: All overloaded methods share the same name.
- Different Parameters: Each method has a unique parameter list (different number, type, or order of parameters).

Rules for Overloading

1. Different Number of Parameters: Methods must differ in the number of parameters.
2. Different Types of Parameters: Methods can have the same number of parameters but different types.
3. Different Order of Parameters: Methods with parameters of different types can have the same number of parameters if their order differs.

Note: Overloading cannot be based solely on return type.

Examples

Overloading by Number of Parameters

public class Printer
{
    public void Print()
    {
        Console.WriteLine("Printing default document.");
    }

    public void Print(string document)
    {
        Console.WriteLine($"Printing document: {document}");
    }

    public void Print(string document, int copies)
    {
        Console.WriteLine($"Printing {copies} copies of {document}");
    }
}

class Program
{
    static void Main()
    {
        Printer printer = new Printer();
        printer.Print(); // Output: Printing default document.
        printer.Print("Report.pdf"); // Output: Printing document: Report.pdf
        printer.Print("Report.pdf", 3); // Output: Printing 3 copies of Report.pdf
    }
}

Overloading by Parameter Types

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b)
    {
        return a + b;
    }

    public string Add(string a, string b)
    {
        return a + b;
    }
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        Console.WriteLine(calc.Add(2, 3)); // Output: 5
        Console.WriteLine(calc.Add(2.5, 3.5)); // Output: 6
        Console.WriteLine(calc.Add("Hello, ", "World!")); // Output: Hello, World!
    }
}

Overloading by Parameter Order

public class Printer
{
    public void Print(string document, int copies)
    {
        Console.WriteLine($"Printing {copies} copies of {document}");
    }

    public void Print(int copies, string document)
    {
        Console.WriteLine($"Printing {copies} copies of {document} (different order)");
    }
}

class Program
{
    static void Main()
    {
        Printer printer = new Printer();
        printer.Print("Invoice.pdf", 2); // Output: Printing 2 copies of Invoice.pdf
        printer.Print(3, "Invoice.pdf"); // Output: Printing 3 copies of Invoice.pdf (different order)
    }
}

Important Considerations

- Ambiguity: Overloading methods too similarly can lead to confusion and compiler ambiguity.
- Readability: Ensure that overloaded methods are clearly distinguishable in their purpose and usage.

7. Method Overriding

Method Overriding allows a derived class to provide a specific implementation of a method that is already defined in its base class. This enables polymorphic behavior, where the method to invoke is determined at runtime based on the object's actual type.

Virtual Methods

- Definition: Methods in the base class marked with the `virtual` keyword that can be overridden in derived classes.
- Purpose: Provide a default implementation that derived classes can customize.

Example:
public class Animal
{
    // Virtual method
    public virtual void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

public class Dog : Animal
{
    // Overriding the virtual method
    public override void Speak()
    {
        Console.WriteLine("The dog barks.");
    }
}

public class Cat : Animal
{
    // Overriding the virtual method
    public override void Speak()
    {
        Console.WriteLine("The cat meows.");
    }
}

class Program
{
    static void Main()
    {
        List<Animal> animals = new List<Animal>
        {
            new Animal(),
            new Dog(),
            new Cat()
        };

        foreach(var animal in animals)
        {
            animal.Speak();
            // Output:
            // The animal makes a sound.
            // The dog barks.
            // The cat meows.
        }
    }
}

Abstract Methods

- Definition: Methods declared in an abstract class without an implementation, forcing derived classes to provide one.
- Purpose: Enforce that all non-abstract derived classes implement the method.

Example:
public abstract class Shape
{
    // Abstract method
    public abstract double CalculateArea();
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    // Implementing the abstract method
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    // Implementing the abstract method
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

class Program
{
    static void Main()
    {
        List<Shape> shapes = new List<Shape>
        {
            new Rectangle { Width = 5, Height = 3 },
            new Circle { Radius = 4 }
        };

        foreach(var shape in shapes)
        {
            Console.WriteLine($"Area: {shape.CalculateArea()}");
            // Output:
            // Area: 15
            // Area: 50.26548245743669
        }
    }
}

Sealed Methods

- Definition: Methods that cannot be overridden further in any derived classes.
- Usage: Prevents further modification of method behavior in the inheritance hierarchy.
- Syntax: Use the `sealed` keyword in conjunction with `override`.

Example:
public class BaseClass
{
    public virtual void Display()
    {
        Console.WriteLine("Base Display");
    }
}

public class DerivedClass : BaseClass
{
    // Sealed override method
    public sealed override void Display()
    {
        Console.WriteLine("Derived Display");
    }
}

public class FurtherDerivedClass : DerivedClass
{
    // Attempting to override Display will result in a compile-time error
    // public override void Display() { } // Error
}

class Program
{
    static void Main()
    {
        BaseClass obj = new DerivedClass();
        obj.Display(); // Output: Derived Display
    }
}

Partial Methods

- Definition: Methods that can be declared in one part of a partial class and optionally implemented in another part.
- Requirements: Must be `private` and return `void`. Can have `ref` but not `out` parameters.
- Use Case: Allow code generation tools to insert method hooks without affecting user code.

Example:
// File: Person.Part1.cs
public partial class Person
{
    partial void OnNameChanged();

    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnNameChanged();
        }
    }
}

// File: Person.Part2.cs
public partial class Person
{
    // Implementing the partial method
    partial void OnNameChanged()
    {
        Console.WriteLine($"Name changed to {name}");
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "Alice"; // Output: Name changed to Alice
    }
}

8. Generic Methods

Generic Methods allow methods to operate with different data types while providing type safety. They enable developers to write flexible and reusable code without sacrificing performance or type checking.

Syntax

public ReturnType MethodName<T>(T parameter)
{
    // Method body
}

- `<T>`: Type parameter placeholder.
- Constraints: Can be applied to restrict the types that can be used as arguments.

Example

public class Utilities
{
    // Generic method to display an array of any type
    public void DisplayArray<T>(T[] array)
    {
        foreach(var item in array)
        {
            Console.WriteLine(item);
        }
    }

    // Generic method with constraints
    public T FindMax<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }
}

class Program
{
    static void Main()
    {
        Utilities util = new Utilities();

        int[] numbers = { 1, 2, 3, 4, 5 };
        util.DisplayArray(numbers);
        // Output:
        // 1
        // 2
        // 3
        // 4
        // 5

        string[] words = { "apple", "banana", "cherry" };
        util.DisplayArray(words);
        // Output:
        // apple
        // banana
        // cherry

        int maxNumber = util.FindMax(10, 20);
        Console.WriteLine($"Max Number: {maxNumber}"); // Output: Max Number: 20

        string maxWord = util.FindMax("alpha", "beta");
        Console.WriteLine($"Max Word: {maxWord}"); // Output: Max Word: beta
    }
}
Explanation:
- `DisplayArray<T>` Method: Can accept arrays of any type (`int`, `string`, etc.) and display their elements.
- `FindMax<T>` Method: Finds the maximum of two values, requiring that `T` implements `IComparable<T>` for comparison.

Benefits of Generic Methods

- Type Safety: Errors are caught at compile-time rather than runtime.
- Reusability: Write once, use with multiple data types.
- Performance: Avoids the need for boxing/unboxing or type casting.

9. Delegates and Lambda Expressions

Delegates and lambda expressions provide powerful ways to pass methods as arguments, enabling functional programming paradigms and flexible code structures in C#.

Delegate Methods

- Definition: Delegates are type-safe function pointers that can reference methods with a specific signature.
- Usage: Pass methods as parameters, define callback mechanisms.

Example:
public delegate int Operation(int a, int b);

public class Calculator
{
    public int ExecuteOperation(int a, int b, Operation op)
    {
        return op(a, b);
    }
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();

        // Using a named method
        Operation add = Add;
        int sum = calc.ExecuteOperation(5, 3, add);
        Console.WriteLine($"Sum: {sum}"); // Output: Sum: 8

        // Using an anonymous method
        Operation multiply = delegate(int x, int y) { return x * y; };
        int product = calc.ExecuteOperation(5, 3, multiply);
        Console.WriteLine($"Product: {product}"); // Output: Product: 15
    }

    static int Add(int a, int b)
    {
        return a + b;
    }
}

Lambda Expressions

- Definition: Concise, inline expressions used to create delegates or expression tree types.
- Syntax: `(parameters) => expression` or `(parameters) => { statements; }`
- Usage: Simplify delegate usage, especially with LINQ.

Example:
public class Calculator
{
    public int ExecuteOperation(int a, int b, Func<int, int, int> op)
    {
        return op(a, b);
    }
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();

        // Using a lambda expression for addition
        Func<int, int, int> add = (x, y) => x + y;
        int sum = calc.ExecuteOperation(5, 3, add);
        Console.WriteLine($"Sum: {sum}"); // Output: Sum: 8

        // Using a lambda expression directly
        int product = calc.ExecuteOperation(5, 3, (x, y) => x * y);
        Console.WriteLine($"Product: {product}"); // Output: Product: 15
    }
}

LINQ Integration

Lambda expressions are extensively used in LINQ queries to define predicates, selectors, and aggregations.

Example:
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

        // Using a lambda expression with Where
        var evenNumbers = numbers.Where(n => n % 2 == 0);

        Console.WriteLine("Even Numbers:");
        foreach(var num in evenNumbers)
        {
            Console.WriteLine(num);
            // Output:
            // 2
            // 4
        }
    }
}
Explanation:
- Delegates: Allow methods to be passed as parameters, enabling flexible and reusable code.
- Lambda Expressions: Provide a shorthand way to define inline delegates, enhancing code conciseness.
- LINQ: Utilizes lambda expressions extensively for querying collections in a declarative manner.

10. Asynchronous Methods

Asynchronous methods enable non-blocking operations, improving application responsiveness and performance, especially in I/O-bound and long-running tasks.

`async` and `await` Keywords

- `async` Keyword: Marks a method as asynchronous, allowing the use of `await` within it.
- `await` Keyword: Pauses the method execution until the awaited task completes, without blocking the thread.

Syntax

public async Task<ReturnType> MethodName(parameters)
{
    // Asynchronous operations
    ReturnType result = await SomeAsyncOperation();
    return result;
}

Example

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WebFetcher
{
    private static readonly HttpClient client = new HttpClient();

    // Asynchronous method to fetch web content
    public async Task<string> FetchContentAsync(string url)
    {
        try
        {
            string content = await client.GetStringAsync(url);
            return content;
        }
        catch(Exception ex)
        {
            return $"Error: {ex.Message}";
        }
    }
}

class Program
{
    static async Task Main()
    {
        WebFetcher fetcher = new WebFetcher();
        string url = "https://www.example.com";
        string content = await fetcher.FetchContentAsync(url);
        Console.WriteLine(content);
    }
}
Explanation:
- `FetchContentAsync` Method: Asynchronously fetches the content of a given URL.
- `await client.GetStringAsync(url)`: Waits for the HTTP GET request to complete without blocking the main thread.
- `async Task<string>`: Indicates that the method is asynchronous and returns a `Task` that resolves to a `string`.

Return Types for Asynchronous Methods

- `Task`: Represents an asynchronous operation that does not return a value.
- `Task<T>`: Represents an asynchronous operation that returns a value of type `T`.
- `ValueTask` and `ValueTask<T>`: Optimized for scenarios where methods may complete synchronously.

Example with `Task`:
public async Task PerformOperationAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("Operation Completed.");
}

class Program
{
    static async Task Main()
    {
        await PerformOperationAsync(); // Output after 1 second: Operation Completed.
    }
}

Best Practices

- Naming Convention: Append `Async` to method names (e.g., `FetchDataAsync`).
- Avoid Blocking Calls: Use `await` instead of blocking methods like `.Result` or `.Wait()`.
- Exception Handling: Use try-catch blocks to handle exceptions in asynchronous methods.
- ConfigureAwait: Use `ConfigureAwait(false)` in library code to avoid deadlocks.

11. Recursive Methods

Recursive Methods are methods that call themselves to solve a problem by breaking it down into smaller, more manageable sub-problems. They are particularly useful for tasks that can be defined in terms of similar subtasks.

Definition

- Base Case: The condition under which the recursion stops.
- Recursive Case: The part of the method that includes the recursive call.

Example: Calculating Factorial

public class MathOperations
{
    // Recursive method to calculate factorial
    public long Factorial(int n)
    {
        if(n < 0)
            throw new ArgumentException("Negative numbers are not allowed.");

        if(n == 0 || n == 1)
            return 1; // Base case

        return n * Factorial(n - 1); // Recursive call
    }
}

class Program
{
    static void Main()
    {
        MathOperations math = new MathOperations();
        int number = 5;
        long result = math.Factorial(number);
        Console.WriteLine($"Factorial of {number} is {result}"); // Output: Factorial of 5 is 120
    }
}

Example: Fibonacci Sequence

public class MathOperations
{
    // Recursive method to calculate nth Fibonacci number
    public int Fibonacci(int n)
    {
        if(n < 0)
            throw new ArgumentException("Negative numbers are not allowed.");

        if(n == 0) return 0; // Base case
        if(n == 1) return 1; // Base case

        return Fibonacci(n - 1) + Fibonacci(n - 2); // Recursive call
    }
}

class Program
{
    static void Main()
    {
        MathOperations math = new MathOperations();
        int position = 6;
        int fib = math.Fibonacci(position);
        Console.WriteLine($"Fibonacci number at position {position} is {fib}"); // Output: Fibonacci number at position 6 is 8
    }
}

Tail Recursion

- Definition: A form of recursion where the recursive call is the last operation in the method.
- Benefits: Can be optimized by the compiler to prevent stack overflow.
- Note: C# does not currently support tail call optimization, so deep recursion can still lead to stack overflow.

Example:
public class MathOperations
{
    // Tail-recursive method to calculate factorial
    public long Factorial(int n, long accumulator = 1)
    {
        if(n < 0)
            throw new ArgumentException("Negative numbers are not allowed.");

        if(n == 0 || n == 1)
            return accumulator; // Base case

        return Factorial(n - 1, n * accumulator); // Tail-recursive call
    }
}

class Program
{
    static void Main()
    {
        MathOperations math = new MathOperations();
        int number = 5;
        long result = math.Factorial(number);
        Console.WriteLine($"Factorial of {number} is {result}"); // Output: Factorial of 5 is 120
    }
}

12. Extension Methods

Extension Methods allow developers to add new methods to existing types without modifying their source code or creating derived types. They are a powerful feature for enhancing the functionality of classes, especially those from external libraries.

Syntax

- Defined in Static Classes: Extension methods must reside in a static class.
- First Parameter: Uses the `this` keyword followed by the type to extend.
- Static Method: The method itself must be static.

Example: Adding an Extension Method to `string`
public static class StringExtensions
{
    // Extension method to check if a string is a palindrome
    public static bool IsPalindrome(this string str)
    {
        if(string.IsNullOrEmpty(str))
            return false;

        int left = 0;
        int right = str.Length - 1;

        while(left < right)
        {
            if(char.ToLower(str[left]) != char.ToLower(str[right]))
                return false;
            left++;
            right--;
        }

        return true;
    }
}

class Program
{
    static void Main()
    {
        string word1 = "Radar";
        string word2 = "Hello";

        Console.WriteLine($"{word1} is palindrome: {word1.IsPalindrome()}"); // Output: Radar is palindrome: True
        Console.WriteLine($"{word2} is palindrome: {word2.IsPalindrome()}"); // Output: Hello is palindrome: False
    }
}

Usage with LINQ

Extension methods are extensively used in LINQ to provide query capabilities on collections.

Example: Using LINQ Extension Methods
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

        // Using the Count extension method
        int count = numbers.Count(n => n > 2);
        Console.WriteLine($"Numbers greater than 2: {count}"); // Output: Numbers greater than 2: 3

        // Using the Sum extension method
        int sum = numbers.Sum();
        Console.WriteLine($"Sum of numbers: {sum}"); // Output: Sum of numbers: 15
    }
}

Benefits of Extension Methods

- Enhance Existing Types: Add functionality without altering original classes.
- Improve Readability: Chainable methods lead to more fluent code.
- Maintainability: Keep related methods organized and accessible.

Best Practices

- Meaningful Names: Ensure that extension methods have clear and descriptive names.
- Avoid Overuse: Do not clutter types with excessive extension methods.
- Namespace Organization: Place extension methods in appropriate namespaces to control their visibility.

13. Best Practices

Adhering to best practices when defining and using methods ensures that your code is efficient, readable, and maintainable.

Naming Conventions

- Method Names: Use PascalCase and verbs or verb phrases (e.g., `CalculateTotal`, `GetUser`).
- Consistency: Follow consistent naming across the codebase for clarity.

Example:
public class OrderProcessor
{
    public void ProcessOrder(Order order) { /* ... */ }
    public bool ValidateOrder(Order order) { /* ... */ }
}

Single Responsibility Principle

- Definition: Each method should have one responsibility or perform a single task.
- Benefit: Enhances modularity and makes methods easier to test and maintain.

Example:
public class ReportGenerator
{
    // Single responsibility: Generate report data
    public ReportData GenerateData() { /* ... */ }

    // Single responsibility: Format report
    public string FormatReport(ReportData data) { /* ... */ }
}

Keep Methods Short and Focused

- Guideline: Aim for methods that fit within one screen view, promoting clarity.
- Benefit: Easier to understand, debug, and modify.

Parameter Validation

- Importance: Validate input parameters to prevent unexpected behavior and errors.
- Implementation: Use `if` statements, exceptions, or guard clauses.

Example:
public class UserService
{
    public void CreateUser(string username, string password)
    {
        if(string.IsNullOrWhiteSpace(username))
            throw new ArgumentException("Username cannot be empty.", nameof(username));
        if(password.Length < 6)
            throw new ArgumentException("Password must be at least 6 characters long.", nameof(password));

        // Proceed with user creation
    }
}

Avoid Side Effects

- Definition: Methods should avoid unintended modifications to external states.
- Benefit: Enhances predictability and reliability of code.

Example:
public class Calculator
{
    // Side-effect-free method
    public int Add(int a, int b)
    {
        return a + b;
    }

    // Method with side effect (not recommended)
    public void AddAndStore(int a, int b)
    {
        this.LastResult = a + b;
    }

    public int LastResult { get; private set; }
}

Use `async` and `await` Appropriately

- Guideline: Use asynchronous methods for I/O-bound and long-running operations to keep applications responsive.
- Avoid: Overusing `async` for CPU-bound tasks where synchronous methods suffice.

Leverage Extension Methods Judiciously

- Guideline: Use extension methods to add meaningful functionality to existing types without cluttering them.
- Avoid: Creating too many extension methods that can confuse users of the type.

14. Common Mistakes

Avoiding common pitfalls when working with methods ensures robust and error-free code.

Incorrect Use of `ref`/`out`

- Issue: Misusing `ref` and `out` can lead to unexpected behavior and hard-to-track bugs.
- Solution: Use `ref` only when necessary to modify the caller's variable. Use `out` for methods that need to return multiple values.

Mistake Example:
public void SetValue(ref int number)
{
    number = 10;
}

class Program
{
    static void Main()
    {
        int value; // Uninitialized
        SetValue(ref value); // Compile-time error: Use of unassigned local variable
    }
}

Correction:
public void SetValue(out int number)
{
    number = 10;
}

class Program
{
    static void Main()
    {
        int value; // No need to initialize
        SetValue(out value);
        Console.WriteLine(value); // Output: 10
    }
}

Ignoring Return Values

- Issue: Methods that return values are sometimes called without using the returned data, leading to wasted computations.
- Solution: Always handle return values appropriately, whether by using them or explicitly ignoring them.

Mistake Example:
public int CalculateSum(int a, int b)
{
    return a + b;
}

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        calc.CalculateSum(5, 3); // Return value ignored
    }
}

Correction:
int sum = calc.CalculateSum(5, 3);
Console.WriteLine($"Sum: {sum}"); // Output: Sum: 8

Overusing Static Methods

- Issue: Excessive use of static methods can lead to code that is difficult to test and maintain.
- Solution: Use static methods for utility functions that do not require instance state. Prefer instance methods when behavior depends on object state.

Poor Naming

- Issue: Methods with unclear or misleading names can confuse developers and make the codebase harder to navigate.
- Solution: Use descriptive and consistent naming conventions that reflect the method's purpose.

Mistake Example:
public class DataHandler
{
    public void DoIt()
    {
        // Performs data processing
    }
}

Correction:
public class DataHandler
{
    public void ProcessData()
    {
        // Performs data processing
    }
}

Overcomplicating Method Logic

- Issue: Methods that try to perform too many tasks can become hard to understand and maintain.
- Solution: Adhere to the Single Responsibility Principle by ensuring each method handles a single task or closely related tasks.

Mistake Example:
public void ManageUser(string username, string password)
{
    ValidateUser(username, password);
    SaveToDatabase(username, password);
    SendWelcomeEmail(username);
    LogActivity(username);
}

Correction:
public void ManageUser(string username, string password)
{
    ValidateUser(username, password);
    SaveToDatabase(username, password);
    SendWelcomeEmail(username);
    LogActivity(username);
}

private void ValidateUser(string username, string password) { /* ... */ }
private void SaveToDatabase(string username, string password) { /* ... */ }
private void SendWelcomeEmail(string username) { /* ... */ }
private void LogActivity(string username) { /* ... */ }

Ignoring Exception Handling

- Issue: Failing to handle exceptions within methods can lead to unhandled exceptions and application crashes.
- Solution: Implement appropriate exception handling using try-catch blocks and consider using custom exceptions for clarity.

Mistake Example:
public void ReadFile(string path)
{
    string content = File.ReadAllText(path); // May throw IOException
    Console.WriteLine(content);
}

class Program
{
    static void Main()
    {
        ReadFile("nonexistent.txt"); // Unhandled exception
    }
}

Correction:
public void ReadFile(string path)
{
    try
    {
        string content = File.ReadAllText(path);
        Console.WriteLine(content);
    }
    catch(IOException ex)
    {
        Console.WriteLine($"Error reading file: {ex.Message}");
    }
}

class Program
{
    static void Main()
    {
        ReadFile("nonexistent.txt"); // Output: Error reading file: ...
    }
}

15. Advanced Topics

Exploring advanced concepts related to methods can further enhance your C# programming skills and enable the creation of more sophisticated and efficient code.

Method Hiding (`new` Keyword)

- Definition: Hides a method in the base class with a new implementation in the derived class.
- Use Case: When a derived class needs to provide a different implementation without overriding.
- Caution: Can lead to confusion and should be used judiciously.

Example:
public class BaseClass
{
    public void Display()
    {
        Console.WriteLine("Base Display");
    }
}

public class DerivedClass : BaseClass
{
    // Hiding the base method
    public new void Display()
    {
        Console.WriteLine("Derived Display");
    }
}

class Program
{
    static void Main()
    {
        BaseClass baseObj = new BaseClass();
        baseObj.Display(); // Output: Base Display

        DerivedClass derivedObj = new DerivedClass();
        derivedObj.Display(); // Output: Derived Display

        BaseClass polymorphicObj = new DerivedClass();
        polymorphicObj.Display(); // Output: Base Display
    }
}

Explicit Interface Implementation

- Definition: Implementing interface members explicitly, making them accessible only through the interface type.
- Use Case: Avoid naming conflicts or when interface members should not be part of the public API.

Example:
public interface ILogger
{
    void Log(string message);
}

public class FileLogger : ILogger
{
    // Explicit interface implementation
    void ILogger.Log(string message)
    {
        Console.WriteLine($"Logging to file: {message}");
    }

    // Additional public method
    public void LogToConsole(string message)
    {
        Console.WriteLine($"Logging to console: {message}");
    }
}

class Program
{
    static void Main()
    {
        FileLogger logger = new FileLogger();
        logger.LogToConsole("Hello, World!"); // Output: Logging to console: Hello, World!

        // logger.Log("Hello"); // Compile-time error

        ILogger iLogger = logger;
        iLogger.Log("Hello"); // Output: Logging to file: Hello
    }
}

Covariance and Contravariance in Methods

- Covariance: Allows a method to return a more derived type than specified by the return type of the delegate.
- Contravariance: Allows a method to accept parameters of less derived types than specified by the delegate.

Example: Covariance
public class Animal { }
public class Dog : Animal { }

public delegate Animal AnimalFactory();

public class DogFactory
{
    public Dog CreateDog()
    {
        return new Dog();
    }
}

class Program
{
    static void Main()
    {
        DogFactory factory = new DogFactory();
        AnimalFactory animalFactory = factory.CreateDog; // Covariance
        Animal animal = animalFactory();
        Console.WriteLine(animal.GetType().Name); // Output: Dog
    }
}

Example: Contravariance
public delegate void AnimalHandler(Animal animal);

public class DogHandler
{
    public void HandleDog(Dog dog)
    {
        Console.WriteLine("Handling dog.");
    }
}

class Program
{
    static void Main()
    {
        DogHandler handler = new DogHandler();
        AnimalHandler animalHandler = handler.HandleDog; // Contravariance
        animalHandler(new Dog()); // Output: Handling dog.
    }
}

Optional Parameters vs. Method Overloading

- Optional Parameters: Provide default values, allowing methods to be called with fewer arguments.
- Method Overloading: Define multiple methods with the same name but different parameters.

Comparison:
- Flexibility: Overloading allows entirely different implementations, while optional parameters are for default behaviors.
- Versioning: Adding optional parameters can break binary compatibility, while overloading is safer in this regard.

Example:
public class Logger
{
    // Using optional parameters
    public void Log(string message, string level = "Info")
    {
        Console.WriteLine($"[{level}] {message}");
    }

    // Using method overloading
    public void Log(string message)
    {
        Log(message, "Info");
    }

    public void Log(string message, string level, DateTime timestamp)
    {
        Console.WriteLine($"[{level}] {timestamp}: {message}");
    }
}

Reflection and Methods

- Definition: Reflection allows inspection and invocation of methods at runtime.
- Use Case: Dynamic method invocation, plugins, serialization frameworks.

Example: Invoking a Method Using Reflection
using System;
using System.Reflection;

public class Greeter
{
    public void Greet(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
}

class Program
{
    static void Main()
    {
        Greeter greeter = new Greeter();
        Type type = typeof(Greeter);
        MethodInfo method = type.GetMethod("Greet");
        method.Invoke(greeter, new object[] { "Alice" }); // Output: Hello, Alice!
    }
}
Caution: Reflection can impact performance and should be used judiciously.

16. Real-World Example

Scenario: Developing a `Library` system that manages books, authors, and patrons. The system should allow adding books, searching for books, and managing patron interactions.

Requirements

1. Add New Books: Add books to the library with details like title, author, and ISBN.
2. Search Books: Search for books by title or author.
3. Borrow Books: Allow patrons to borrow available books.
4. Return Books: Manage the return of borrowed books.
5. Logging: Log all operations for auditing purposes.

Implementation

using System;
using System.Collections.Generic;
using System.Linq;

namespace LibrarySystem
{
    // Book class
    public class Book
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string ISBN { get; set; }
        public bool IsBorrowed { get; set; }

        public Book(string title, string author, string isbn)
        {
            Title = title;
            Author = author;
            ISBN = isbn;
            IsBorrowed = false;
        }

        public void Borrow()
        {
            if(IsBorrowed)
                throw new InvalidOperationException("Book is already borrowed.");
            IsBorrowed = true;
        }

        public void Return()
        {
            if(!IsBorrowed)
                throw new InvalidOperationException("Book was not borrowed.");
            IsBorrowed = false;
        }
    }

    // Patron class
    public class Patron
    {
        public string Name { get; set; }
        public List<Book> BorrowedBooks { get; set; }

        public Patron(string name)
        {
            Name = name;
            BorrowedBooks = new List<Book>();
        }

        public void BorrowBook(Book book)
        {
            book.Borrow();
            BorrowedBooks.Add(book);
        }

        public void ReturnBook(Book book)
        {
            book.Return();
            BorrowedBooks.Remove(book);
        }
    }

    // Library class
    public class Library
    {
        private List<Book> books;
        private List<Patron> patrons;

        public Library()
        {
            books = new List<Book>();
            patrons = new List<Patron>();
        }

        // Method to add a new book
        public void AddBook(string title, string author, string isbn)
        {
            if(books.Any(b => b.ISBN == isbn))
                throw new ArgumentException("A book with the same ISBN already exists.");
            books.Add(new Book(title, author, isbn));
            Logger.Log($"Added book: {title} by {author}, ISBN: {isbn}");
        }

        // Method to search books by title
        public List<Book> SearchByTitle(string title)
        {
            var results = books.Where(b => b.Title.IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
            Logger.Log($"Searched for books with title containing '{title}'. Found {results.Count} books.");
            return results;
        }

        // Method to search books by author
        public List<Book> SearchByAuthor(string author)
        {
            var results = books.Where(b => b.Author.IndexOf(author, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
            Logger.Log($"Searched for books by author '{author}'. Found {results.Count} books.");
            return results;
        }

        // Method to register a new patron
        public void RegisterPatron(string name)
        {
            if(patrons.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
                throw new ArgumentException("A patron with the same name already exists.");
            patrons.Add(new Patron(name));
            Logger.Log($"Registered patron: {name}");
        }

        // Method to borrow a book
        public void BorrowBook(string patronName, string isbn)
        {
            Patron patron = patrons.FirstOrDefault(p => p.Name.Equals(patronName, StringComparison.OrdinalIgnoreCase));
            if(patron == null)
                throw new ArgumentException("Patron not found.");

            Book book = books.FirstOrDefault(b => b.ISBN == isbn);
            if(book == null)
                throw new ArgumentException("Book not found.");

            patron.BorrowBook(book);
            Logger.Log($"{patronName} borrowed book: {book.Title}, ISBN: {isbn}");
        }

        // Method to return a book
        public void ReturnBook(string patronName, string isbn)
        {
            Patron patron = patrons.FirstOrDefault(p => p.Name.Equals(patronName, StringComparison.OrdinalIgnoreCase));
            if(patron == null)
                throw new ArgumentException("Patron not found.");

            Book book = patron.BorrowedBooks.FirstOrDefault(b => b.ISBN == isbn);
            if(book == null)
                throw new ArgumentException("This patron did not borrow this book.");

            patron.ReturnBook(book);
            Logger.Log($"{patronName} returned book: {book.Title}, ISBN: {isbn}");
        }

        // Method to display all books
        public void DisplayAllBooks()
        {
            Console.WriteLine("Library Books:");
            foreach(var book in books)
            {
                Console.WriteLine($"- {book.Title} by {book.Author} (ISBN: {book.ISBN}) - {(book.IsBorrowed ? "Borrowed" : "Available")}");
            }
            Logger.Log("Displayed all books.");
        }
    }

    // Logger class for logging operations
    public static class Logger
    {
        public static void Log(string message)
        {
            // For simplicity, logging to console. In real applications, log to files or logging systems.
            Console.WriteLine($"[LOG] {DateTime.Now}: {message}");
        }
    }

    class Program
    {
        static void Main()
        {
            Library library = new Library();

            try
            {
                // Adding books
                library.AddBook("1984", "George Orwell", "ISBN001");
                library.AddBook("To Kill a Mockingbird", "Harper Lee", "ISBN002");
                library.AddBook("The Great Gatsby", "F. Scott Fitzgerald", "ISBN003");

                // Registering patrons
                library.RegisterPatron("Alice");
                library.RegisterPatron("Bob");

                // Displaying all books
                library.DisplayAllBooks();

                // Borrowing books
                library.BorrowBook("Alice", "ISBN001");
                library.BorrowBook("Bob", "ISBN003");

                // Displaying all books after borrowing
                library.DisplayAllBooks();

                // Returning books
                library.ReturnBook("Alice", "ISBN001");

                // Displaying all books after returning
                library.DisplayAllBooks();

                // Searching for books
                var searchResults = library.SearchByAuthor("Harper Lee");
                Console.WriteLine("\nSearch Results by Author 'Harper Lee':");
                foreach(var book in searchResults)
                {
                    Console.WriteLine($"- {book.Title} (ISBN: {book.ISBN}) - {(book.IsBorrowed ? "Borrowed" : "Available")}");
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}

Sample Output:
[LOG] 4/27/2024 10:15:30 AM: Added book: 1984 by George Orwell, ISBN: ISBN001
[LOG] 4/27/2024 10:15:30 AM: Added book: To Kill a Mockingbird by Harper Lee, ISBN: ISBN002
[LOG] 4/27/2024 10:15:30 AM: Added book: The Great Gatsby by F. Scott Fitzgerald, ISBN: ISBN003
[LOG] 4/27/2024 10:15:30 AM: Registered patron: Alice
[LOG] 4/27/2024 10:15:30 AM: Registered patron: Bob
Library Books:
- 1984 by George Orwell (ISBN: ISBN001) - Available
- To Kill a Mockingbird by Harper Lee (ISBN: ISBN002) - Available
- The Great Gatsby by F. Scott Fitzgerald (ISBN: ISBN003) - Available
[LOG] Displayed all books.
[LOG] 4/27/2024 10:15:30 AM: Alice borrowed book: 1984, ISBN: ISBN001
[LOG] 4/27/2024 10:15:30 AM: Bob borrowed book: The Great Gatsby, ISBN: ISBN003
Library Books:
- 1984 by George Orwell (ISBN: ISBN001) - Borrowed
- To Kill a Mockingbird by Harper Lee (ISBN: ISBN002) - Available
- The Great Gatsby by F. Scott Fitzgerald (ISBN: ISBN003) - Borrowed
[LOG] Displayed all books.
[LOG] 4/27/2024 10:15:30 AM: Alice returned book: 1984, ISBN: ISBN001
Library Books:
- 1984 by George Orwell (ISBN: ISBN001) - Available
- To Kill a Mockingbird by Harper Lee (ISBN: ISBN002) - Available
- The Great Gatsby by F. Scott Fitzgerald (ISBN: ISBN003) - Borrowed
[LOG] Displayed all books.
Search Results by Author 'Harper Lee':
- To Kill a Mockingbird (ISBN: ISBN002) - Available

Explanation:
- Classes:
- Book: Represents a book with properties like `Title`, `Author`, `ISBN`, and `IsBorrowed`. Methods `Borrow` and `Return` manage the borrowing state.
- Patron: Represents a library patron with a `Name` and a list of `BorrowedBooks`. Methods `BorrowBook` and `ReturnBook` handle book transactions.
- Library: Manages collections of `books` and `patrons`. Provides methods to add books, search books, register patrons, borrow and return books, and display all books.
- Logger: Static class for logging operations. In this example, logs are printed to the console.

- Method Usage:
- Adding Books: `AddBook` method adds new books to the library.
- Registering Patrons: `RegisterPatron` method registers new library members.
- Borrowing Books: `BorrowBook` allows patrons to borrow available books.
- Returning Books: `ReturnBook` allows patrons to return borrowed books.
- Searching Books: `SearchByAuthor` and `SearchByTitle` methods facilitate finding books based on criteria.
- Displaying Books: `DisplayAllBooks` method lists all books along with their availability.

- Logging: Each significant operation is logged using the `Logger.Log` method for auditing and tracking purposes.

- Error Handling: The `try-catch` block in `Main` handles exceptions such as duplicate ISBNs, non-existent patrons, or invalid operations like borrowing an already borrowed book.

- Output: The sample output demonstrates the flow of adding, borrowing, returning, and searching books, along with corresponding log entries.

17. Summary

Methods in C# are essential for defining the behavior and functionalities of classes and structs. They enable code reuse, modularity, and organized structures, making complex applications manageable and maintainable.

Key Takeaways:
- Encapsulation and Reusability: Methods encapsulate logic and allow code to be reused across different parts of an application.

- Flexibility: Through features like overloading, overriding, generics, and extension methods, methods provide flexible and powerful ways to implement functionality.

- Asynchronous Programming: Asynchronous methods (`async`/`await`) enhance application responsiveness and performance by handling long-running tasks without blocking threads.

- Best Practices: Adhering to naming conventions, single responsibility principle, concise methods, and proper parameter validation leads to clean and maintainable code.

- Common Pitfalls: Avoiding mistakes like incorrect use of `ref`/`out`, ignoring return values, overusing static methods, and poor naming ensures robust and error-free applications.

- Advanced Concepts: Understanding method hiding, explicit interface implementation, covariance and contravariance, and reflection can unlock advanced programming techniques.

- Real-World Applications: Practical examples, such as the library management system, demonstrate how methods orchestrate complex operations and interactions within an application.

Previous: C# Operators | Next: C# Pass by Value vs Pass by Reference

<
>