Methods
Definition
A method in C# is a code block containing a series of statements that perform a specific task. Methods promote code reuse, readability, and modularity by encapsulating logic into named, callable units.
// Basic method
public int Add(int a, int b)
{
return a + b;
}
Core Concepts
Method Declaration
A method consists of an access modifier, return type, name, parameter list, and body:
[access modifier] [modifiers] [return type] [name]([parameters])
{
// method body
}
public static double CalculateArea(double radius)
{
return Math.PI * radius * radius;
}
- Access modifiers:
public,private,protected,internal,protected internal,private protected - Modifiers:
static,async,abstract,virtual,override,sealed,new,unsafe,extern - Return type: any type, or
voidif no value is returned
Expression-Bodied Methods (C# 6+)
Single-expression methods can use => syntax for concise declarations:
public int Double(int x) => x * 2;
public string GetFullName(string first, string last) => $"{first} {last}";
// Works for void methods too
public void Log(string message) => Console.WriteLine(message);
Parameters
Value Parameters (default)
Arguments are passed by value — the method receives a copy of the argument:
public void Increment(int x)
{
x++; // Only modifies the local copy
}
int num = 5;
Increment(num);
// num is still 5
ref Parameters
The argument is passed by reference. The method can read and modify the original variable. The variable must be initialized before the call:
public void Double(ref int x)
{
x *= 2;
}
int num = 5;
Double(ref num);
// num is now 10
out Parameters
The method must assign a value before returning. The variable does not need to be initialized before the call:
public bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
// out variable declaration inline (C# 7+)
if (int.TryParse("42", out int parsed))
{
Console.WriteLine(parsed); // 42
}
in Parameters
The argument is passed by reference but is read-only inside the method. Prevents copying large structs while ensuring the method cannot modify the value:
public double ComputeDistance(in Point p)
{
// p.X = 10; // Compile error: cannot modify 'in' parameter
return Math.Sqrt(p.X * p.X + p.Y * p.Y);
}
var point = new Point(3, 4);
double dist = ComputeDistance(in point);
params Keyword
Allows a method to accept a variable number of arguments as an array:
public int Sum(params int[] numbers)
{
return numbers.Sum();
}
int total = Sum(1, 2, 3, 4, 5); // 15
int total2 = Sum([1, 2, 3]); // Also works with array
ref vs out vs in Comparison
| Feature | ref | out | in |
|---|---|---|---|
| Direction | Bidirectional | Output only | Input only |
| Must initialize before call | Yes | No | Yes |
| Must assign in method | No | Yes | No (read-only) |
| Method can read | Yes | Yes (but typically writes) | Yes |
| Method can write | Yes | Yes | No |
| Use case | Modify existing variable | Return multiple values | Avoid copying large structs |
Default Parameter Values
public void CreateUser(string name, string role = "User", bool isActive = true)
{
Console.WriteLine($"{name}, {role}, Active: {isActive}");
}
CreateUser("Alice"); // Alice, User, Active: True
CreateUser("Bob", "Admin"); // Bob, Admin, Active: True
CreateUser("Charlie", "Admin", false); // Charlie, Admin, Active: False
Default parameters must be placed after all non-default parameters in the method signature. Changing default values is a binary-breaking change — recompilation of calling code is required.
Named Arguments
Arguments can be passed by name, enabling you to skip optional parameters or improve readability:
CreateUser(name: "Alice", isActive: false);
CreateUser(isActive: true, role: "Admin", name: "Bob");
// Combine with defaults
CreateUser("Alice", isActive: false); // role uses default
Method Overloading
Multiple methods with the same name but different parameter signatures:
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
public int Add(int a, int b, int c) => a + b + c;
Rules:
- Overloads must differ in parameter count, types, or order
- Return type alone is not sufficient for overloading
ref/out/indifferences are valid for overloading (butrefvsoutis not — they are considered the same)
Optional Parameters vs Method Overloading
| Aspect | Optional Parameters | Method Overloading |
|---|---|---|
| Syntax | Single method with defaults | Multiple method definitions |
| Versioning | Risky — default changes are breaking | Safe — add new overloads |
| Readability | Cleaner for simple cases | Better for complex logic per variant |
| Performance | Same at runtime | Same at runtime |
Use optional parameters for simple cases with sensible defaults. Use overloading when different parameter combinations require fundamentally different logic.
Local Functions (C# 7+)
Methods declared inside another method. They can capture local variables from the enclosing scope:
public IEnumerable<int> GetEvens(IEnumerable<int> numbers)
{
return numbers.Where(IsEven);
// Local function — only accessible within GetEvens
bool IsEven(int n) => n % 2 == 0;
}
// Local function with captured variable
public void Process(string prefix)
{
int count = 0;
void LogItem(string item)
{
count++; // captures 'count' from enclosing scope
Console.WriteLine($"{prefix}{count}: {item}");
}
LogItem("Apple");
LogItem("Banana");
}
Static Methods vs Instance Methods
public class MathHelper
{
// Static — called on the type, not an instance
public static int Square(int x) => x * x;
// Instance — called on an object instance
public int Multiply(int a, int b) => a * b;
}
// Usage
int sq = MathHelper.Square(5); // Static call
var helper = new MathHelper();
int prod = helper.Multiply(3, 4); // Instance call
Extension Methods
Add methods to existing types without modifying them. Defined as static methods in a static class with the this keyword on the first parameter:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string? str)
{
return string.IsNullOrEmpty(str);
}
public static string Reverse(this string str)
{
return new string(str.ToCharArray().Reverse().ToArray());
}
}
// Usage — appears as instance method
string? name = null;
bool empty = name.IsNullOrEmpty(); // True
string reversed = "hello".Reverse(); // "olleh"
Extension methods cannot access private members of the type they extend. They are syntactic sugar — the compiler rewrites them as static method calls.
Recursion
A recursive method is one that calls itself. Every recursive method needs a base case (stopping condition) and a recursive case (which moves toward the base case).
// Factorial — classic recursion example
public int Factorial(int n)
{
if (n <= 1) return 1; // Base case
return n * Factorial(n - 1); // Recursive case
}
// Fibonacci
public int Fibonacci(int n)
{
if (n <= 1) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
The naive recursive Fibonacci has O(2^n) time complexity because it recalculates the same values repeatedly. Use memoization or an iterative approach for production code:
// Iterative Fibonacci — O(n)
public int FibonacciIterative(int n)
{
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++)
(a, b) = (b, a + b);
return b;
}
Common recursive patterns:
// Tree traversal
public int SumTree(TreeNode? node)
{
if (node is null) return 0; // Base case
return node.Value + SumTree(node.Left) + SumTree(node.Right);
}
// Directory traversal
public long GetDirectorySize(string path)
{
long size = Directory.GetFiles(path).Sum(f => new FileInfo(f).Length);
foreach (string dir in Directory.GetDirectories(path))
size += GetDirectorySize(dir); // Recursive call
return size;
}
// Binary search
public int BinarySearch(int[] arr, int target, int left, int right)
{
if (left > right) return -1; // Base case — not found
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] > target) return BinarySearch(arr, target, left, mid - 1);
return BinarySearch(arr, target, mid + 1, right);
}
Recursion vs iteration:
| Aspect | Recursion | Iteration |
|---|---|---|
| Readability | Cleaner for tree/graph problems | Simpler for linear processing |
| Stack usage | Each call adds a stack frame | Constant stack usage |
| Risk | Stack overflow for deep recursion | No stack overflow risk |
| Performance | Overhead per call (allocation) | Generally faster |
| When to use | Tree traversal, divide-and-conquer, backtracking | Loops, linear processing, tail-call patterns |
C# does not guarantee tail-call optimization. For deep recursion (thousands of levels), convert to an iterative approach using an explicit Stack<T> to avoid StackOverflowException.
Method Overloading Resolution
When multiple overloads exist, the compiler selects the best match using these rules (in priority order):
- Exact type match is preferred over implicit conversions
- Narrower types are preferred over wider types (e.g.,
intoverdouble) - Non-params overloads are preferred over
paramsoverloads - Non-generic overloads are preferred over generic overloads
- If ambiguity remains, a compiler error is produced
public void Print(int x) => Console.WriteLine($"int: {x}");
public void Print(double x) => Console.WriteLine($"double: {x}");
Print(5); // Calls Print(int) — exact match
Print(5.0); // Calls Print(double) — exact match
Print(5L); // Calls Print(double) — long converts to double
Code Examples
Complete Method Demo
public class UserService
{
// Method with ref parameter
public bool TryUpgrade(ref string role, string targetRole)
{
if (role == "Admin") return false;
role = targetRole;
return true;
}
// Method with out parameter
public bool TryGetUser(int id, out User? user)
{
user = id > 0 ? new User(id) : null;
return user is not null;
}
// Method with in parameter (avoid copying large struct)
public double CalculateScore(in UserProfile profile)
{
return profile.Points * profile.Multiplier;
}
// Method with params
public User[] GetUsersByIds(params int[] ids)
{
return ids.Select(id => new User(id)).ToArray();
}
}
When to Use
| Feature | When to Use |
|---|---|
Expression-bodied (=>) | Single-expression logic, getters, simple transformations |
ref parameters | When the method must modify the caller's variable |
out parameters | Returning multiple values without a wrapper type (prefer tuples in modern C#) |
in parameters | Passing large structs without copying (e.g., struct over 16 bytes) |
params | Methods accepting a variable number of arguments |
| Default parameters | APIs with sensible defaults that rarely change |
| Named arguments | Improving readability with multiple optional parameters |
| Local functions | Helper logic used only within one method |
| Extension methods | Adding utility methods to types you do not own |
Common Pitfalls
-
Overloading with optional parameters — Two overloads where one has a default value can cause ambiguity. For example,
void Log(string msg)andvoid Log(string msg, bool timestamp = true)— callingLog("hello")is ambiguous. -
ref returns with async — You cannot use
reforoutparameters withasyncmethods. The compiler generates a state machine, making it unsafe to pass references across await boundaries. -
params array allocation — Every call to a
paramsmethod allocates a new array on the heap. For performance-critical hot paths, consider providing explicit overloads for common argument counts. -
Default parameter versioning — Changing a default value is a binary-breaking change. Callers compiled against the old version continue using the old default. Use method overloading for public APIs.
-
Extension method namespace — Extension methods are only discoverable when their containing namespace is imported with
using. If the namespace is not imported, the methods will not appear as instance methods.
Key Takeaways
- Methods encapsulate reusable logic with a named, callable interface
- Use
reffor bidirectional reference passing,outfor output-only,infor read-only large structs - Expression-bodied members (
=>) make simple methods concise - Overloading differs by parameter signature — return type alone is not enough
- Extension methods add functionality to existing types via the
thiskeyword - Local functions encapsulate helper logic within a single method scope
- Prefer method overloading over optional parameters for public API versioning
Interview Questions
Q: What is the difference between ref and out?
A: Both pass arguments by reference. ref requires the variable to be initialized before the call and allows the method to optionally modify it. out does not require initialization before the call but the method must assign a value before returning. Semantically, ref means "I am giving you a value, you may change it" while out means "I expect you to give me a value."
Q: Can you overload a method by return type alone? A: No. C# does not allow method overloading based solely on return type. Overloads must differ in parameter count, types, or order. The compiler uses the argument list to resolve which overload to call — return type is not part of that decision.
Q: What are extension methods and how do you create one?
A: Extension methods allow you to add methods to existing types without modifying them. Create a static class, define a static method, and use the this keyword on the first parameter. The method then appears as an instance method on the extended type. Example: public static int WordCount(this string s) => s.Split().Length;
Q: What is the difference between method overloading and method overriding?
A: Overloading means multiple methods with the same name but different parameters in the same class. Overriding means a derived class provides a new implementation for a virtual or abstract method defined in the base class. Overloading is compile-time polymorphism; overriding is runtime polymorphism.