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

Methods

Định nghĩa (Definition)

Một phương thức (Method) trong C# là một khối mã chứa một loạt các câu lệnh thực hiện một tác vụ cụ thể. Phương thức thúc đẩy tái sử dụng mã, khả năng đọc và tính mô-đun bằng cách đóng gói logic thành các đơn vị có tên và có thể gọi.

// Basic method
public int Add(int a, int b)
{
return a + b;
}

Các khái niệm cốt lõi (Core Concepts)

Khai báo phương thức (Method Declaration)

Một phương thức bao gồm bổ từ truy cập (Access Modifier), kiểu trả về (Return Type), tên, danh sách tham số (Parameters) và thân phương thức:

[access modifier] [modifiers] [return type] [name]([parameters])
{
// method body
}

public static double CalculateArea(double radius)
{
return Math.PI * radius * radius;
}
  • Bổ từ truy cập (Access Modifiers): public, private, protected, internal, protected internal, private protected
  • Bổ từ (Modifiers): static, async, abstract, virtual, override, sealed, new, unsafe, extern
  • Kiểu trả về (Return Type): bất kỳ kiểu nào, hoặc void nếu không trả về giá trị

Phương thức thân biểu thức (Expression-Bodied Methods) (C# 6+)

Các phương thức biểu thức đơn có thể sử dụng cú pháp => cho khai báo ngắn gọn:

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

Tham số (Parameters)

Tham số giá trị (Value Parameters) - mặc định

Đối số (Arguments) được truyền theo giá trị — phương thức nhận một bản sao của đối số:

public void Increment(int x)
{
x++; // Only modifies the local copy
}

int num = 5;
Increment(num);
// num is still 5

Tham số ref

Đối số được truyền theo tham chiếu (Reference). Phương thức có thể đọc và sửa đổi biến gốc. Biến phải được khởi tạo trước khi gọi:

public void Double(ref int x)
{
x *= 2;
}

int num = 5;
Double(ref num);
// num is now 10

Tham số out

Phương thức phải gán giá trị trước khi trả về. Biến không cần khởi tạo trước khi gọi:

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
}

Tham số in

Đối số được truyền theo tham chiếu nhưng chỉ đọc (Read-only) bên trong phương thức. Ngăn chặn việc sao chép struct lớn (Large Structs) trong khi đảm bảo phương thức không thể sửa đổi giá trị:

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

Từ khóa params

Cho phép phương thức chấp nhận số lượng đối số thay đổi (Variable Number of Arguments) dưới dạng mảng:

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

So sánh ref vs out vs in

Tính năngrefoutin
HướngHai chiều (Bidirectional)Chỉ đầu ra (Output Only)Chỉ đầu vào (Input Only)
Phải khởi tạo trước khi gọiKhông
Phải gán trong phương thứcKhôngKhông (chỉ đọc)
Phương thức có thể đọcCó (nhưng thường ghi)
Phương thức có thể ghiKhông
Trường hợp sử dụngSửa đổi biến hiện cóTrả về nhiều giá trịTránh sao chép struct lớn

Giá trị tham số mặc định (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

Tham số mặc định phải được đặt sau tất cả tham số không mặc định trong chữ ký phương thức. Thay đổi giá trị mặc định là một thay đổi phá vỡ nhị phân (Binary-Breaking Change) — cần biên dịch lại mã gọi.

Đối số có tên (Named Arguments)

Đối số có thể được truyền theo tên, cho phép bạn bỏ qua tham số tùy chọn hoặc cải thiện khả năng đọc:

CreateUser(name: "Alice", isActive: false);
CreateUser(isActive: true, role: "Admin", name: "Bob");

// Combine with defaults
CreateUser("Alice", isActive: false); // role uses default

Nạp chồng phương thức (Method Overloading)

Nhiều phương thức có cùng tên nhưng chữ ký tham số (Parameter Signatures) khác nhau:

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;

Quy tắc:

  • Các nạp chồng phải khác nhau về số lượng, kiểu hoặc thứ tự tham số
  • Chỉ kiểu trả về (Return Type) alone là không đủ để nạp chồng
  • Khác biệt ref/out/in là hợp lệ để nạp chồng (nhưng ref vs out thì không — chúng được coi là giống nhau)

Tham số tùy chọn vs Nạp chồng phương thức (Optional Parameters vs Method Overloading)

Khía cạnhTham số tùy chọn (Optional Parameters)Nạp chồng phương thức (Method Overloading)
Cú phápMột phương thức với giá trị mặc địnhNhiều định nghĩa phương thức
Phiên bản (Versioning)Rủi ro — thay đổi mặc định là phá vỡAn toàn — thêm nạp chồng mới
Khả năng đọcGọn hơn cho trường hợp đơn giảnTốt hơn cho logic phức tạp theo từng biến thể
Hiệu năngGiống nhau trong thời gian chạyGiống nhau trong thời gian chạy
mẹo

Sử dụng tham số tùy chọn (Optional Parameters) cho các trường hợp đơn giản với giá trị mặc định hợp lý. Sử dụng nạp chồng (Overloading) khi các kết hợp tham số khác nhau yêu cầu logic cơ bản khác nhau.

Hàm cục bộ (Local Functions) (C# 7+)

Phương thức được khai báo bên trong một phương thức khác. Chúng có thể captures biến cục bộ (Local Variables) từ phạm vi bao quanh:

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");
}

Phương thức tĩnh vs Phương thức thực thể (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

Phương thức mở rộng (Extension Methods)

Thêm phương thức vào các kiểu hiện có mà không sửa đổi chúng. Được định nghĩa là phương thức static trong lớp static với từ khóa this trên tham số đầu tiên:

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

Phương thức mở rộng (Extension Methods) không thể truy cập thành viên riêng (Private Members) của kiểu mà chúng mở rộng. Chúng là đường cú pháp (Syntactic Sugar) — trình biên dịch viết lại chúng thành các cuộc gọi phương thức tĩnh.

Đệ quy (Recursion)

Một phương thức đệ quy (Recursive Method) là phương thức gọi chính nó. Mọi phương thức đệ quy cần một trường hợp cơ sở (Base Case) (điều kiện dừng) và một trường hợp đệ quy (Recursive Case) (tiến về trường hợp cơ sở).

// 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 theo cấp số nhân khi không có ghi nhớ (Memoization)

Fibonacci đệ quy ngây ngô có độ phức tạp thời gian O(2^n) vì nó tính toán lại các giá trị giống nhau nhiều lần. Sử dụng ghi nhớ (Memoization) hoặc cách tiếp cận lặp (Iterative) cho mã sản xuất:

// 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;
}

Các mẫu đệ quy phổ biến:

// 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);
}

Đệ quy (Recursion) vs Lặp (Iteration):

Khía cạnhĐệ quy (Recursion)Lặp (Iteration)
Khả năng đọcRõ hơn cho bài toán cây/đồ thịĐơn giản hơn cho xử lý tuyến tính
Sử dụng StackMỗi cuộc gọi thêm một stack frameSử dụng stack không đổi
Rủi roTràn stack (Stack Overflow) cho đệ quy sâuKhông có rủi ro tràn stack
Hiệu năngOverhead mỗi cuộc gọi (cấp phát)Nói chung nhanh hơn
Khi nào sử dụngDuyệt cây (Tree Traversal), chia để trị (Divide-and-Conquer), quay lui (Backtracking)Vòng lặp, xử lý tuyến tính, mẫu tail-call
Bảo vệ tràn stack (Stack Overflow Protection)

C# không đảm bảo tối ưu hóa tail-call. Đối với đệ quy sâu (hàng nghìn cấp), chuyển sang cách tiếp cận lặp (Iterative) sử dụng Stack<T> rõ ràng để tránh StackOverflowException.

Giải quyết nạp chồng phương thức (Method Overloading Resolution)

Khi có nhiều nạp chồng, trình biên dịch chọn khớp tốt nhất sử dụng các quy tắc sau (theo thứ tự ưu tiên):

  1. Khớp kiểu chính xác (Exact Type Match) được ưu tiên hơn chuyển đổi ẩn (Implicit Conversions)
  2. Các kiểu hẹp hơn (Narrower) được ưu tiên hơn kiểu rộng hơn (ví dụ: int hơn double)
  3. Nạp chồng không phải params được ưu tiên hơn nạp chồng params
  4. Nạp chồng không generic (Non-Generic) được ưu tiên hơn nạp chồng generic
  5. Nếu vẫn mơ hồ, lỗi trình biên dịch sẽ được đưa ra
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

Ví dụ mã (Code Examples)

Demo phương thức hoàn chỉnh (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();
}
}

Khi nào sử dụng (When to Use)

Tính năngKhi nào sử dụng
Thân biểu thức (Expression-bodied - =>)Logic biểu thức đơn, getter, phép biến đổi đơn giản
Tham số refKhi phương thức phải sửa đổi biến của người gọi
Tham số outTrả về nhiều giá trị mà không cần kiểu bao (Wrapper Type) (ưu tiên tuple trong C# hiện đại)
Tham số inTruyền struct lớn mà không sao chép (ví dụ: struct trên 16 bytes)
paramsPhương thức chấp nhận số lượng đối số thay đổi
Tham số mặc địnhAPI với giá trị mặc định hợp lý hiếm khi thay đổi
Đối số có tên (Named Arguments)Cải thiện khả năng đọc với nhiều tham số tùy chọn
Hàm cục bộ (Local Functions)Logic trợ giúp chỉ sử dụng trong một phương thức
Phương thức mở rộng (Extension Methods)Thêm phương thức tiện ích vào kiểu bạn không sở hữu

Lỗi thường gặp (Common Pitfalls)

  1. Nạp chồng với tham số tùy chọn — Hai nạp chồng trong đó một cái có giá trị mặc định có thể gây mơ hồ. Ví dụ: void Log(string msg)void Log(string msg, bool timestamp = true) — gọi Log("hello") là mơ hồ.

  2. ref returns với async — Bạn không thể sử dụng tham số ref hoặc out với phương thức async. Trình biên dịch tạo state machine, khiến việc truyền tham chiếu qua ranh giới await không an toàn.

  3. Cấp phát mảng params — Mọi cuộc gọi phương thức params đều cấp phát mảng mới trên Heap. Đối với các đường dẫn nóng (Hot Paths) quan trọng hiệu năng, cân nhắc cung cấp nạp chồng rõ ràng cho các số lượng đối số phổ biến.

  4. Phiên bản tham số mặc định — Thay đổi giá trị mặc định là thay đổi phá vỡ nhị phân (Binary-Breaking Change). Mã gọi được biên dịch với phiên bản cũ sẽ tiếp tục sử dụng mặc định cũ. Sử dụng nạp chồng phương thức cho API công khai.

  5. Không gian tên phương thức mở rộng — Phương thức mở rộng chỉ có thể phát hiện khi không gian tên (Namespace) chứa chúng được nhập bằng using. Nếu không gian tên không được nhập, các phương thức sẽ không xuất hiện dưới dạng phương thức thực thể.

Tóm tắt要点 (Key Takeaways)

  • Phương thức đóng gói logic tái sử dụng với giao diện có tên và có thể gọi
  • Sử dụng ref cho truyền tham chiếu hai chiều, out cho chỉ đầu ra, in cho struct lớn chỉ đọc
  • Thành viên thân biểu thức (Expression-Bodied Members) (=>) làm cho phương thức đơn giản ngắn gọn
  • Nạp chồng (Overloading) khác theo chữ ký tham số — chỉ kiểu trả về là không đủ
  • Phương thức mở rộng (Extension Methods) thêm chức năng vào kiểu hiện có qua từ khóa this
  • Hàm cục bộ (Local Functions) đóng gói logic trợ giúp trong phạm vi một phương thức duy nhất
  • Ưu tiên nạp chồng phương thức (Method Overloading) hơn tham số tùy chọn cho phiên bản API công khai

Câu hỏi phỏng vấn (Interview Questions)

Q: Sự khác biệt giữa refout là gì? A: Cả hai đều truyền đối số theo tham chiếu. ref yêu cầu biến được khởi tạo trước khi gọi và cho phép phương thức tùy chọn sửa đổi nó. out không yêu cầu khởi tạo trước khi gọi nhưng phương thức phải gán giá trị trước khi trả về. Về mặt ngữ nghĩa, ref có nghĩa là "tôi đang cho bạn một giá trị, bạn có thể thay đổi nó" trong khi out có nghĩa là "tôi kỳ vọng bạn cho tôi một giá trị."

Q: Bạn có thể nạp chồng phương thức chỉ bằng kiểu trả về (Return Type) không? A: Không. C# không cho phép nạp chồng phương thức chỉ dựa trên kiểu trả về. Các nạp chồng phải khác nhau về số lượng tham số, kiểu hoặc thứ tự. Trình biên dịch sử dụng danh sách đối số để giải quyết nạp chồng nào sẽ gọi — kiểu trả về không phải một phần của quyết định đó.

Q: Phương thức mở rộng (Extension Methods) là gì và cách tạo? A: Phương thức mở rộng cho phép bạn thêm phương thức vào kiểu hiện có mà không sửa đổi chúng. Tạo một lớp static, định nghĩa một phương thức static và sử dụng từ khóa this trên tham số đầu tiên. Phương thức sau đó xuất hiện dưới dạng phương thức thực thể trên kiểu được mở rộng. Ví dụ: public static int WordCount(this string s) => s.Split().Length;

Q: Sự khác biệt giữa nạp chồng phương thức (Method Overloading) và ghi đè phương thức (Method Overriding) là gì? A: Nạp chồng có nghĩa là nhiều phương thức cùng tên nhưng tham số khác nhau trong cùng một lớp. Ghi đè có nghĩa là lớp dẫn xuất cung cấp triển khai mới cho phương thức virtual hoặc abstract được định nghĩa trong lớp cơ sở. Nạp chồng là đa hình thời điểm biên dịch (Compile-Time Polymorphism); ghi đè là đa hình thời gian chạy (Runtime Polymorphism).

Tài liệu tham khảo (References)