// Basic method structurepublic returnType MethodName(parameters){ // Method body return value;}// Examplepublic int Add(int a, int b){ return a + b;}// Void method (no return value)public void PrintMessage(string message){ Console.WriteLine(message);}
public void PublicMethod() { } // Accessible everywhereprivate void PrivateMethod() { } // Only within the classprotected void ProtectedMethod() { } // Within class and subclassesinternal void InternalMethod() { } // Within same assemblyprotected internal void Method() { } // Protected OR internalprivate protected void Method() { } // Protected AND internal
public void ModifyValue(int number){ number = 100; // Only modifies local copy}int x = 10;ModifyValue(x);Console.WriteLine(x); // Still 10 - original unchanged
Pass by reference - modifies the original variable:
public void ModifyReference(ref int number){ number = 100; // Modifies original variable}int x = 10;ModifyReference(ref x);Console.WriteLine(x); // 100 - original was modified
ref requires the variable to be initialized before passing and must be used at both declaration and call site.
public bool TryParse(string input, out int result){ if (int.TryParse(input, out result)) { return true; } result = 0; return false;}// Usageif (TryParse("123", out int value)){ Console.WriteLine($"Parsed: {value}");}// Inline declaration (C# 7.0+)if (TryParse("123", out var value)){ Console.WriteLine($"Parsed: {value}");}// Discard unused out parametersif (dict.TryGetValue(key, out _)) // Don't care about value{ Console.WriteLine("Key exists");}
public double Calculate(in Point3D point){ // point is passed by reference but cannot be modified return Math.Sqrt(point.X * point.X + point.Y * point.Y + point.Z * point.Z);}// Useful for large structs to avoid copyingpublic readonly struct Point3D{ public double X { get; init; } public double Y { get; init; } public double Z { get; init; }}
Use in parameters for large readonly structs to avoid copy overhead while preventing modification. For small structs (≤16 bytes), regular value parameters are fine.
public int FindIndex(int[] array, int target){ if (array == null || array.Length == 0) return -1; // Early return for invalid input for (int i = 0; i < array.Length; i++) { if (array[i] == target) return i; // Early return when found } return -1; // Not found}
Multiple methods with same name but different parameters:
public class Calculator{ // Overloaded methods public int Add(int a, int b) { return a + b; } public double Add(double a, double b) { return a + b; } public int Add(int a, int b, int c) { return a + b + c; } // Usage var calc = new Calculator(); int sum1 = calc.Add(1, 2); // Calls int version double sum2 = calc.Add(1.5, 2.5); // Calls double version int sum3 = calc.Add(1, 2, 3); // Calls three-parameter version}
Overload resolution is based on the number, types, and order of parameters - not the return type.
public int CalculateFactorial(int n){ // Local function - only accessible within this method int Factorial(int x) { if (x <= 1) return 1; return x * Factorial(x - 1); } if (n < 0) throw new ArgumentException("Must be non-negative", nameof(n)); return Factorial(n);}// Local function with captured variablespublic IEnumerable<int> GetMultiples(int factor, int count){ // Captures 'factor' and 'count' from outer scope IEnumerable<int> GenerateMultiples() { for (int i = 1; i <= count; i++) { yield return factor * i; } } return GenerateMultiples();}
Use local functions for helper logic that’s only used within one method. They can access variables from the outer scope (closure) and improve code organization.
Local functions that cannot capture outer variables:
public int Calculate(int x, int y){ // Static local function - cannot access x, y from outer scope static int Add(int a, int b) { return a + b; } return Add(x, y); // Must pass as parameters}
public class Example{ // Static method - belongs to type, not instance public static int Add(int a, int b) => a + b; // Virtual method - can be overridden in derived classes public virtual void Process() { } // Abstract method - must be implemented in derived classes public abstract void Execute(); // Override method - overrides base class virtual/abstract method public override void Process() { base.Process(); } // Sealed override - prevents further overriding public sealed override void Process() { } // Async method - returns Task or Task<T> public async Task<string> FetchDataAsync() { await Task.Delay(1000); return "Data"; }}
public static class StringExtensions{ // Extension method - note 'this' on first parameter public static bool IsValidEmail(this string email) { return email.Contains("@") && email.Contains("."); } public static string Truncate(this string value, int maxLength) { if (string.IsNullOrEmpty(value)) return value; return value.Length <= maxLength ? value : value.Substring(0, maxLength); }}// Usage - called as if they were instance methodsstring email = "[email protected]";bool valid = email.IsValidEmail(); // truestring text = "Hello World";string short = text.Truncate(5); // "Hello"
Extension methods must be defined in static classes and the first parameter must use the this modifier. They’re syntactic sugar for static method calls.
// Classic recursion - factorialpublic int Factorial(int n){ if (n <= 1) return 1; // Base case return n * Factorial(n - 1); // Recursive case}// Fibonacci sequencepublic int Fibonacci(int n){ if (n <= 1) return n; return Fibonacci(n - 1) + Fibonacci(n - 2);}// Tail recursion (optimizable)public int FactorialTailRecursive(int n, int accumulator = 1){ if (n <= 1) return accumulator; return FactorialTailRecursive(n - 1, n * accumulator);}
Recursion can cause stack overflow for deep call chains. The default stack size is ~1 MB. For deep recursion, consider iterative solutions or increasing stack size.
// Bad - method does too muchpublic void ProcessOrder(Order order){ ValidateOrder(order); CalculateTax(order); ApplyDiscount(order); SaveToDatabase(order); SendConfirmationEmail(order); UpdateInventory(order);}// Good - extract to smaller methodspublic void ProcessOrder(Order order){ ValidateOrder(order); CalculateOrderTotals(order); SaveOrder(order); NotifyCustomer(order); UpdateInventory(order);}
Use meaningful method names
// Badpublic void DoStuff(int x) { }public int Calc(int a, int b) { }// Goodpublic void ProcessPayment(decimal amount) { }public int CalculateTotalWithTax(int subtotal, decimal taxRate) { }
Validate parameters early
public void ProcessUser(User user, string action){ // Guard clauses at the start if (user == null) throw new ArgumentNullException(nameof(user)); if (string.IsNullOrWhiteSpace(action)) throw new ArgumentException("Action cannot be empty", nameof(action)); // Main logic user.PerformAction(action);}
Use expression-bodied members for simple methods
// Traditionalpublic int GetAge(){ return DateTime.Now.Year - BirthYear;}// Expression-bodied (cleaner for simple methods)public int GetAge() => DateTime.Now.Year - BirthYear;
Prefer out parameters or tuples over ref
// Instead of refpublic void GetCoordinates(ref int x, ref int y) { }// Use out or tuplepublic (int X, int Y) GetCoordinates() => (10, 20);
public class Server{ public string Host; public int Port; // Primary constructor - does the actual work public Server(string host, int port) { Host = host; Port = port; } // Chain to primary constructor using 'this' public Server() : this("localhost", 8080) { } // Another constructor chaining public Server(string host) : this(host, 8080) { }}// Usagevar server1 = new Server(); // Uses defaultsvar server2 = new Server("example.com"); // Custom hostvar server3 = new Server("example.com", 443); // All custom
Constructor chaining best practice: Chain all constructors to one primary constructor that does the actual initialization. This eliminates code duplication and makes refactoring easier.