Chuyển tới nội dung chính

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 void if 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

Featurerefoutin
DirectionBidirectionalOutput onlyInput only
Must initialize before callYesNoYes
Must assign in methodNoYesNo (read-only)
Method can readYesYes (but typically writes)Yes
Method can writeYesYesNo
Use caseModify existing variableReturn multiple valuesAvoid 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
cảnh báo

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/in differences are valid for overloading (but ref vs out is not — they are considered the same)

Optional Parameters vs Method Overloading

AspectOptional ParametersMethod Overloading
SyntaxSingle method with defaultsMultiple method definitions
VersioningRisky — default changes are breakingSafe — add new overloads
ReadabilityCleaner for simple casesBetter for complex logic per variant
PerformanceSame at runtimeSame at runtime
mẹo

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"
cảnh báo

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);
}
Fibonacci is exponential without memoization

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:

AspectRecursionIteration
ReadabilityCleaner for tree/graph problemsSimpler for linear processing
Stack usageEach call adds a stack frameConstant stack usage
RiskStack overflow for deep recursionNo stack overflow risk
PerformanceOverhead per call (allocation)Generally faster
When to useTree traversal, divide-and-conquer, backtrackingLoops, linear processing, tail-call patterns
Stack overflow protection

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):

  1. Exact type match is preferred over implicit conversions
  2. Narrower types are preferred over wider types (e.g., int over double)
  3. Non-params overloads are preferred over params overloads
  4. Non-generic overloads are preferred over generic overloads
  5. 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

FeatureWhen to Use
Expression-bodied (=>)Single-expression logic, getters, simple transformations
ref parametersWhen the method must modify the caller's variable
out parametersReturning multiple values without a wrapper type (prefer tuples in modern C#)
in parametersPassing large structs without copying (e.g., struct over 16 bytes)
paramsMethods accepting a variable number of arguments
Default parametersAPIs with sensible defaults that rarely change
Named argumentsImproving readability with multiple optional parameters
Local functionsHelper logic used only within one method
Extension methodsAdding utility methods to types you do not own

Common Pitfalls

  1. Overloading with optional parameters — Two overloads where one has a default value can cause ambiguity. For example, void Log(string msg) and void Log(string msg, bool timestamp = true) — calling Log("hello") is ambiguous.

  2. ref returns with async — You cannot use ref or out parameters with async methods. The compiler generates a state machine, making it unsafe to pass references across await boundaries.

  3. params array allocation — Every call to a params method allocates a new array on the heap. For performance-critical hot paths, consider providing explicit overloads for common argument counts.

  4. 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.

  5. 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 ref for bidirectional reference passing, out for output-only, in for 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 this keyword
  • 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.

References