Introduction
Welcome to Module 3 of our Complete C# Course: From Beginner to Advanced! After mastering control flow and operators in Module 2, it’s time to make your C# programs more modular and resilient with methods and exception handling. In this module, we’ll cover defining and calling methods, method overloading, parameter passing techniques (ref, out, optional), exception handling with try, catch, finally, and throw, creating custom exceptions, and basic debugging techniques. We’ll build an interactive expense tracker app to manage expenses with robust error handling, applying these concepts in a real-world context. With detailed examples, best practices, and pros/cons, you’ll learn to write clean, reliable C# code. Let’s get started!
1. Defining and Calling Methods
Methods are reusable blocks of code that perform specific tasks, improving modularity and readability.
Syntax
Return Type: Specifies what the method returns (e.g., int, void).
Parameters: Inputs to the method.
Access Modifier: public, private, etc.
Example: Calculate Expense Total
using System;
namespace ExpenseTracker
{
class Program
{
// Method definition
static double CalculateTotal(double amount, double taxRate)
{
return amount + (amount * taxRate);
}
static void Main(string[] args)
{
double amount = 50.0;
double taxRate = 0.08;
double total = CalculateTotal(amount, taxRate); // Method call
Console.WriteLine($"Total with tax: {total:C}");
}
}
}
Real-World Use: Calculating totals in financial apps or processing user inputs.
Pros:
Promotes code reuse and modularity.
Improves readability by encapsulating logic.
Cons:
Overuse of methods can lead to excessive abstraction.
Parameter passing adds overhead for simple tasks.
Best Practices:
Use clear, descriptive method names (e.g., CalculateTotal vs. Calc).
Keep methods single-purpose (Single Responsibility Principle).
Use void for methods without return values.
Alternatives:
Lambda expressions for inline logic.
Extension methods for adding functionality to existing types.
2. Method Overloading
Method overloading allows multiple methods with the same name but different parameter lists.
Example: Flexible Expense Calculator
using System;
namespace ExpenseTracker
{
class Program
{
// Overloaded methods
static double CalculateTotal(double amount)
{
return amount + (amount * 0.08); // Default tax rate
}
static double CalculateTotal(double amount, double taxRate)
{
return amount + (amount * taxRate);
}
static double CalculateTotal(double amount, double taxRate, double discount)
{
double subtotal = amount - discount;
return subtotal + (subtotal * taxRate);
}
static void Main(string[] args)
{
Console.WriteLine($"Default tax: {CalculateTotal(50.0):C}"); // $54.00
Console.WriteLine($"Custom tax: {CalculateTotal(50.0, 0.1):C}"); // $55.00
Console.WriteLine($"With discount: {CalculateTotal(50.0, 0.1, 5.0):C}"); // $49.50
}
}
}
Real-World Use: Offering multiple ways to calculate prices (e.g., with/without discounts) in e-commerce.
Pros:
Enhances flexibility without duplicating method names.
Improves API usability.
Cons:
Can confuse developers if signatures are too similar.
Increases code complexity.
Best Practices:
Ensure overloaded methods have distinct purposes.
Use default parameters instead of overloading when possible.
Alternatives:
Optional parameters (covered below).
Separate method names for clarity.
3. Passing Parameters (ref, out, optional)
C# supports different parameter-passing mechanisms:
By Value: Default; copies the value.
ref: Passes a reference; changes affect the original variable.
out: Passes a reference; must be assigned in the method.
Optional: Parameters with default values.
Example: Expense Validation
using System;
namespace ExpenseTracker
{
class Program
{
static bool ValidateAmount(double amount, ref string errorMessage, out double adjustedAmount)
{
adjustedAmount = amount;
if (amount < 0)
{
errorMessage = "Amount cannot be negative.";
return false;
}
if (amount > 1000)
{
errorMessage = "Amount exceeds maximum limit.";
adjustedAmount = 1000; // Cap the amount
return false;
}
errorMessage = string.Empty;
return true;
}
static double CalculateTotal(double amount, double taxRate = 0.08) // Optional parameter
{
return amount + (amount * taxRate);
}
static void Main(string[] args)
{
double amount = 1500;
string error = string.Empty;
double adjusted;
if (ValidateAmount(amount, ref error, out adjusted))
{
Console.WriteLine($"Total: {CalculateTotal(adjusted):C}");
}
else
{
Console.WriteLine($"Error: {error}, Adjusted: {adjusted:C}");
}
// Using optional parameter
Console.WriteLine($"Default tax: {CalculateTotal(50.0):C}");
}
}
}
Real-World Use: Validating inputs in forms or adjusting values in financial calculations.
Pros:
ref allows bidirectional data flow.
out ensures output values are set.
Optional parameters reduce overloading needs.
Cons:
ref/out can make code harder to follow.
Optional parameters may hide default behavior.
Best Practices:
Use ref only when necessary (e.g., modifying existing variables).
Use out for methods returning multiple values.
Document default values for optional parameters.
Alternatives:
Tuples or records for returning multiple values.
Method overloading instead of optional parameters.
4. Exception Handling (try, catch, finally, throw)
Exception handling manages runtime errors gracefully.
Syntax
try: Contains code that might throw an exception.
catch: Handles specific or general exceptions.
finally: Runs regardless of exception (e.g., cleanup).
throw: Explicitly throws an exception.
Example: Safe File Processing
using System;
namespace ExpenseTracker
{
class Program
{
static void ProcessExpense(string amountText)
{
try
{
double amount = double.Parse(amountText);
if (amount < 0)
throw new ArgumentException("Amount cannot be negative.");
Console.WriteLine($"Processed amount: {amount:C}");
}
catch (FormatException)
{
Console.WriteLine("Error: Invalid number format.");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
Console.WriteLine("Processing complete.");
}
}
static void Main(string[] args)
{
ProcessExpense("100"); // Processed amount: $100.00
ProcessExpense("-50"); // Error: Amount cannot be negative.
ProcessExpense("abc"); // Error: Invalid number format.
}
}
}
Real-World Use: Handling invalid inputs in a banking app or logging errors in a service.
Pros:
Prevents crashes from unexpected errors.
finally ensures cleanup (e.g., closing files).
throw allows custom error signaling.
Cons:
Overuse of try/catch can obscure logic.
Catching general Exception may hide bugs.
Best Practices:
Catch specific exceptions (e.g., FormatException).
Use finally for resource cleanup.
Avoid empty catch blocks.
Alternatives:
Validation checks to prevent exceptions.
Result objects (e.g., Result<T>) for error handling.
5. Custom Exceptions
Custom exceptions define specific error types for your application.
Example: Expense Exception
using System;
namespace ExpenseTracker
{
// Custom exception
class InvalidExpenseException : Exception
{
public InvalidExpenseException(string message) : base(message) { }
public InvalidExpenseException(string message, Exception inner) : base(message, inner) { }
}
class Program
{
static void ProcessExpense(double amount)
{
try
{
if (amount < 0)
throw new InvalidExpenseException("Amount cannot be negative.");
if (amount > 10000)
throw new InvalidExpenseException("Amount exceeds maximum limit.");
Console.WriteLine($"Processed: {amount:C}");
}
catch (InvalidExpenseException ex)
{
Console.WriteLine($"Expense Error: {ex.Message}");
}
}
static void Main(string[] args)
{
ProcessExpense(100); // Processed: $100.00
ProcessExpense(-50); // Expense Error: Amount cannot be negative.
ProcessExpense(15000); // Expense Error: Amount exceeds maximum limit.
}
}
}
Real-World Use: Defining domain-specific errors in business applications.
Pros:
Improves error specificity and handling.
Enhances code documentation.
Cons:
Adds complexity for small projects.
Requires consistent usage across codebase.
Best Practices:
Inherit from Exception or a specific base exception.
Use descriptive names (e.g., InvalidExpenseException).
Include inner exceptions for context.
Alternatives:
Built-in exceptions (e.g., ArgumentException).
Error codes or result objects.
6. Debugging Basics
Debugging identifies and fixes errors using tools like Visual Studio’s debugger.
Techniques
Breakpoints: Pause execution to inspect variables.
Watch: Monitor variable values.
Call Stack: Trace method calls.
Step Into/Over: Navigate code execution.
Example: Debugging a Method
using System;
namespace ExpenseTracker
{
class Program
{
static double CalculateTotal(double amount, double taxRate)
{
// Set breakpoint here to inspect amount and taxRate
double tax = amount * taxRate;
double total = amount + tax;
return total;
}
static void Main(string[] args)
{
Console.Write("Enter amount: ");
double amount;
if (!double.TryParse(Console.ReadLine(), out amount))
{
Console.WriteLine("Invalid amount!");
return;
}
double total = CalculateTotal(amount, 0.08);
Console.WriteLine($"Total: {total:C}");
}
}
}
Debugging Steps (Visual Studio):
Set a breakpoint on double tax = amount * taxRate;.
Run in Debug mode (F5).
Inspect amount and taxRate in the Watch window.
Step through code with F10 (Step Over) or F11 (Step Into).
Check the Call Stack to trace execution.
Real-World Use: Diagnosing incorrect calculations in a payroll system.
Pros:
Breakpoints pinpoint issues quickly.
Visual Studio’s debugger is powerful and integrated.
Cons:
Debugging complex async code can be challenging.
Over-reliance on debugging may skip proper testing.
Best Practices:
Use breakpoints for specific issues; avoid excessive logging.
Combine with unit tests for robust error checking.
Log meaningful messages for production debugging.
Alternatives:
Console logging for quick checks.
Unit tests to catch errors early.
Tools like Rider or VS Code debugger.
Interactive Example: Expense Tracker App
Let’s build a console-based expense tracker app that applies methods, exception handling, and debugging.
using System;
using System.Collections.Generic;
namespace ExpenseTracker
{
// Custom exception
class InvalidExpenseException : Exception
{
public InvalidExpenseException(string message) : base(message) { }
}
class Expense
{
public double Amount { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public Expense(double amount, string description)
{
Amount = amount;
Description = description;
Date = DateTime.Now;
}
}
class ExpenseManager
{
private List<Expense> expenses = new List<Expense>();
private const double MaxAmount = 10000;
/// <summary>
/// Adds an expense with validation.
/// </summary>
public void AddExpense(double amount, string description, out string errorMessage)
{
errorMessage = string.Empty;
try
{
if (string.IsNullOrWhiteSpace(description))
throw new InvalidExpenseException("Description cannot be empty.");
if (amount <= 0)
throw new InvalidExpenseException("Amount must be positive.");
if (amount > MaxAmount)
throw new InvalidExpenseException($"Amount exceeds limit of {MaxAmount:C}.");
expenses.Add(new Expense(amount, description));
}
catch (InvalidExpenseException ex)
{
errorMessage = ex.Message;
}
}
/// <summary>
/// Calculates total expenses with optional tax rate.
/// </summary>
public double CalculateTotal(double taxRate = 0.08)
{
double total = 0;
foreach (var expense in expenses)
{
total += expense.Amount + (expense.Amount * taxRate);
}
return total;
}
/// <summary>
/// Displays all expenses.
/// </summary>
public void DisplayExpenses()
{
if (expenses.Count == 0)
{
Console.WriteLine("No expenses recorded.");
return;
}
foreach (var expense in expenses)
{
Console.WriteLine($"{expense.Date:MM/dd/yyyy}: {expense.Description} - {expense.Amount:C}");
}
}
}
class Program
{
static void Main(string[] args)
{
ExpenseManager manager = new ExpenseManager();
while (true)
{
Console.WriteLine("\nExpense Tracker");
Console.WriteLine("1. Add Expense");
Console.WriteLine("2. View Expenses");
Console.WriteLine("3. Calculate Total");
Console.WriteLine("4. Exit");
Console.Write("Choose an option: ");
string choice = Console.ReadLine();
if (choice == "4") break;
switch (choice)
{
case "1":
Console.Write("Enter description: ");
string desc = Console.ReadLine();
Console.Write("Enter amount: ");
if (!double.TryParse(Console.ReadLine(), out double amount))
{
Console.WriteLine("Invalid amount!");
continue;
}
string error;
manager.AddExpense(amount, desc, out error);
if (!string.IsNullOrEmpty(error))
Console.WriteLine($"Error: {error}");
else
Console.WriteLine("Expense added successfully.");
break;
case "2":
manager.DisplayExpenses();
break;
case "3":
double total = manager.CalculateTotal();
Console.WriteLine($"Total with tax: {total:C}");
break;
default:
Console.WriteLine("Invalid option!");
break;
}
}
}
}
}
How It Works:
Methods: AddExpense, CalculateTotal, DisplayExpenses encapsulate logic.
Overloading: CalculateTotal uses optional parameters.
Parameters: Uses out for error messages in AddExpense.
Exception Handling: Custom InvalidExpenseException for validation errors.
Debugging: Set breakpoints in AddExpense to inspect amount and description.
Why It’s Useful: Mimics expense tracking in personal finance or business apps.
Setup: Create a new Console App in Visual Studio or run dotnet new console.
Best Standards for Module 3
Methods: Use clear, single-purpose methods with descriptive names.
Overloading: Ensure distinct parameter lists; prefer optional parameters for simple cases.
Parameters: Use ref/out sparingly; prefer tuples or return objects for multiple values.
Exception Handling: Catch specific exceptions; use finally for cleanup.
Custom Exceptions: Create for domain-specific errors with clear messages.
Debugging: Combine breakpoints, watches, and logging for effective debugging.
Conclusion
You’ve just mastered methods and exception handling in C#! By learning to define, call, and overload methods, use ref, out, and optional parameters, handle exceptions with try/catch, create custom exceptions, and debug effectively, you’re ready to build robust, modular applications. The expense tracker app showcases how these concepts work together in a real-world scenario.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam