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
voidnế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ăng | ref | out | in |
|---|---|---|---|
| Hướng | Hai chiều (Bidirectional) | Chỉ đầu ra (Output Only) | Chỉ đầu vào (Input Only) |
| Phải khởi tạo trước khi gọi | Có | Không | Có |
| Phải gán trong phương thức | Không | Có | Không (chỉ đọc) |
| Phương thức có thể đọc | Có | Có (nhưng thường ghi) | Có |
| Phương thức có thể ghi | Có | Có | Không |
| Trường hợp sử dụng | Sử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
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/inlà hợp lệ để nạp chồng (nhưngrefvsoutthì 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ạnh | Tham số tùy chọn (Optional Parameters) | Nạp chồng phương thức (Method Overloading) |
|---|---|---|
| Cú pháp | Một phương thức với giá trị mặc định | Nhiề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 đọc | Gọn hơn cho trường hợp đơn giản | Tốt hơn cho logic phức tạp theo từng biến thể |
| Hiệu năng | Giống nhau trong thời gian chạy | Giống nhau trong thời gian chạy |
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"
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 đệ 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 đọc | Rõ 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 Stack | Mỗi cuộc gọi thêm một stack frame | Sử dụng stack không đổi |
| Rủi ro | Tràn stack (Stack Overflow) cho đệ quy sâu | Không có rủi ro tràn stack |
| Hiệu năng | Overhead mỗi cuộc gọi (cấp phát) | Nói chung nhanh hơn |
| Khi nào sử dụng | Duyệ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 |
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):
- Khớp kiểu chính xác (Exact Type Match) được ưu tiên hơn chuyển đổi ẩn (Implicit Conversions)
- Các kiểu hẹp hơn (Narrower) được ưu tiên hơn kiểu rộng hơn (ví dụ:
inthơndouble) - Nạp chồng không phải params được ưu tiên hơn nạp chồng
params - Nạp chồng không generic (Non-Generic) được ưu tiên hơn nạp chồng generic
- 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ăng | Khi 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ố ref | Khi phương thức phải sửa đổi biến của người gọi |
Tham số out | Trả 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ố in | Truyền struct lớn mà không sao chép (ví dụ: struct trên 16 bytes) |
params | Phương thức chấp nhận số lượng đối số thay đổi |
| Tham số mặc định | API 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)
-
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)vàvoid Log(string msg, bool timestamp = true)— gọiLog("hello")là mơ hồ. -
ref returns với async — Bạn không thể sử dụng tham số
refhoặcoutvới phương thứcasync. 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. -
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. -
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.
-
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
refcho truyền tham chiếu hai chiều,outcho chỉ đầu ra,incho 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 ref và out 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).