Phương thức (method) là một khối code có tên, nhận đầu vào (tham số), thực hiện công việc của nó, và có thể trả lại một kết quả (giá trị trả về). Trong C#, mọi phương thức đều nằm bên trong một kiểu; không có hàm đứng độc lập.
Đầu ra:
Headphones 48.00
Cable 11.40
Cấu tạo của một phương thức
public static decimal PriceWithTax(decimal net, decimal taxRate)
{
return net * (1 + taxRate);
}
- Access modifier (
public,private, ...): ai được phép gọi nó. Nếu bỏ trống, phương thức làprivatetrong kiểu của nó. static: phương thức thuộc về kiểu, không thuộc về một đối tượng. Xem thêm bên dưới.- Kiểu trả về: kiểu của giá trị được trả về, hoặc
voidnếu không trả về gì. - Tên: theo quy ước viết PascalCase, thường là một động từ (
Calculate,SendEmail,TryParse). - Danh sách tham số: không hoặc nhiều cặp
type name. Mỗi tham số là một biến cục bộ được khởi tạo từ đối số của nơi gọi. - Thân phương thức: các câu lệnh nằm giữa hai dấu ngoặc nhọn.
Tên cộng với kiểu của các tham số (và mọi modifier ref, out hoặc in) tạo thành chữ ký (signature) của phương thức. Kiểu trả về không thuộc chữ ký, điều này quan trọng khi overload.
Giá trị trả về và void
return kết thúc phương thức ngay lập tức và trả lại một giá trị. Phương thức có kiểu trả về phải trả về một giá trị đúng kiểu đó trên mọi nhánh:
static string Grade(int score)
{
if (score >= 50)
return "pass";
} // error CS0161: 'Program.Grade(int)': not all code paths return a value
Trình biên dịch kiểm tra từng nhánh. Hãy thêm return "fail"; còn thiếu, hoặc một throw nếu việc chạy đến điểm đó là bug.
Phương thức void không trả về gì. Nó vẫn có thể dùng return; trơn để thoát sớm:
Đầu ra:
ana@mail.com: newsletter sent
ben@mail.com: not subscribed, skipped
False
True
Phương thức dạng biểu thức
Khi thân phương thức chỉ là một biểu thức, => thay cho dấu ngoặc nhọn và return:
static decimal Discount(decimal price) => price * 0.1m;
static bool IsAdult(int age) => age >= 18;
static void Log(string message) => Console.WriteLine($"[log] {message}");
Đây vẫn là cùng một phương thức, chỉ viết ngắn hơn. Cách này cũng dùng được cho phương thức void, miễn là thân là một biểu thức (một lời gọi, một phép gán).
Tham số là bản sao
Mặc định, đối số được truyền theo giá trị: tham số nhận một bản sao của đối số. Gán cho tham số bên trong phương thức không ảnh hưởng đến biến của nơi gọi:
Đầu ra:
inside: 42
after: 21
scores: 70, 85, 100
List bị thay đổi vì với kiểu tham chiếu, giá trị được sao chép chính là tham chiếu: phương thức và nơi gọi cùng trỏ đến một đối tượng list. Gán lại scores = new List<int>() bên trong phương thức sẽ không ảnh hưởng đến nơi gọi. Để phương thức gán được cho biến của nơi gọi, dùng ref hoặc out; để đặt giá trị mặc định cho tham số và gọi theo tên, dùng tham số tùy chọn và đối số có tên.
Trả về nhiều giá trị bằng tuple
Từ C# 7.0, phương thức có thể trả về một tuple với các phần tử có tên, và nơi gọi có thể tách nó thành các biến riêng:
Đầu ra:
min 15, max 27, avg 21.0
range 12
_ bỏ đi phần tử bạn không cần. Với kết quả dùng ở nhiều nơi, một lớp nhỏ hoặc record với các thuộc tính có tên mô tả cấu trúc rõ hơn tuple.
Overload
Nhiều phương thức có thể dùng chung một tên nếu danh sách tham số khác nhau về số lượng, kiểu hoặc thứ tự các kiểu. Trình biên dịch chọn phương thức khớp nhất cho mỗi lời gọi:
Đầu ra:
12.57
13.50
25
Area(5) chọn phiên bản int vì khớp chính xác; Area(2.0) chọn phiên bản double. Các phương thức không thể chỉ khác nhau ở kiểu trả về: hai phương thức Area(int) trả về int và double là lỗi CS0111, Type 'Program' already defines a member called 'Area' with the same parameter types, vì một lời gọi như Area(5) không thể phân biệt được chúng.
Một lời gọi không overload nào chấp nhận cũng lỗi lúc biên dịch: Area(1, 2, 3) là CS1501, No overload for method 'Area' takes 3 arguments, còn Area("big") là CS1503, Argument 1: cannot convert from 'string' to 'double'.
params: số lượng đối số tùy ý
Tham số mảng params nhận bao nhiêu đối số cũng được, kể cả không có đối số nào. Trình biên dịch tạo mảng giúp bạn:
Đầu ra:
empty cart: 0 item(s), 0
one item: 1 item(s), 9.99
three items: 3 item(s), 19.75
from array: 2 item(s), 50
Quy tắc: params phải là tham số cuối cùng (nếu không sẽ gặp CS0231), mỗi phương thức có tối đa một, và truyền vào một mảng có sẵn thì mảng đó được chuyển nguyên vẹn. Console.WriteLine("{0} and {1}", a, b) và string.Format là các phương thức params object[]. C# 13 mở rộng params cho các kiểu collection khác như List<T> và ReadOnlySpan<T>; trước đó chỉ cho phép mảng.
Phương thức static và instance
Phương thức static thuộc về kiểu và được gọi qua tên kiểu: Math.Max(3, 7), Program.Total(...). Phương thức instance thuộc về một đối tượng và có thể dùng các field của đối tượng đó:
Đầu ra:
12
Gọi một phương thức instance từ một phương thức static mà không có đối tượng là lỗi phổ biến nhất của người mới học C#:
class Program
{
void Greet() => Console.WriteLine("hi");
static void Main()
{
Greet();
// error CS0120: An object reference is required for the non-static field, method, or property 'Program.Greet()'
}
}
Hãy đánh dấu Greet là static, hoặc tạo một đối tượng rồi gọi trên đó: new Program().Greet();. Trang về static trình bày kỹ hơn về thành viên static và lớp static.
Phương thức Main
Main là nơi chương trình bắt đầu. Nó phải là static, và các chữ ký sau đều hợp lệ:
static void Main() { }
static void Main(string[] args) { } // command-line arguments
static int Main(string[] args) { return 0; } // exit code for the shell
static async Task Main() { } // C# 7.1 and later
Dạng async Task Main cho phép điểm bắt đầu dùng await trực tiếp (xem async và await). Từ C# 9, một tệp có thể chứa top-level statement thay thế: code hoàn toàn không có lớp hay Main, như trong một dự án dotnet new console mới. Trình biên dịch sinh lớp và phương thức Main bao quanh nó, nên đây vẫn là cùng một thứ được viết ngắn hơn; args vẫn dùng được.
Đệ quy
Một phương thức có thể tự gọi chính nó. Mỗi lời gọi có tham số và biến cục bộ riêng, và phải có một trường hợp cơ sở để dừng chuỗi gọi:
Đầu ra:
120
2432902008176640000
35
Không có trường hợp cơ sở, hoặc với đầu vào quá sâu, mỗi lời gọi thêm một stack frame cho đến khi chương trình chết với StackOverflowException. Ngoại lệ này không bắt được, nên với đầu vào có độ sâu không biết trước, hãy viết lại đệ quy thành vòng lặp.
Hàm cục bộ
C# 7.0 giới thiệu hàm cục bộ (local function): phương thức khai báo bên trong một phương thức khác và chỉ thấy được ở đó. Chúng có thể dùng biến của phương thức bao quanh:
// C# 7.0 and later
static int CountValid(string[] codes)
{
int count = 0;
foreach (string code in codes)
{
if (IsValid(code)) count++;
}
return count;
bool IsValid(string c) => c.Length == 6 && char.IsLetter(c[0]);
}
Chúng giữ một hàm phụ ngay cạnh nơi gọi duy nhất của nó mà không lộ ra cho phần còn lại của lớp. Từ C# 8 chúng có thể được đánh dấu static để cấm capture biến bên ngoài. Khi không dùng được hàm cục bộ, một phương thức private static trong cùng lớp làm được việc tương tự, như trong các ví dụ khác trên trang này.
Câu hỏi thường gặp
Phương thức và hàm trong C# khác nhau thế nào?
Trong C#, mọi hàm đều được khai báo bên trong một lớp, struct hoặc record, và được gọi là phương thức. Không có hàm đứng độc lập. Ngay cả top-level statement (C# 9) và hàm cục bộ (C# 7) cũng được biên dịch thành phương thức của một kiểu được sinh ra hoặc kiểu bao quanh. "Hàm" chỉ là cách gọi thân mật, và dùng cho lambda và delegate.
params trong C# dùng để làm gì?
params cho phép một phương thức nhận số lượng đối số tùy ý cùng một kiểu: static int Sum(params int[] numbers) có thể gọi bằng Sum(), Sum(4), Sum(4, 5, 6) hoặc Sum(existingArray). Trình biên dịch gói các đối số vào một mảng. Tham số params phải đứng cuối cùng, và mỗi phương thức chỉ có tối đa một tham số như vậy.
Phương thức Main trong C# là gì?
Main là điểm bắt đầu của chương trình: phương thức mà runtime gọi đầu tiên. Nó phải là static, có thể trả về void hoặc int (mã thoát), và có thể nhận string[] args cho các đối số dòng lệnh. Từ C# 7.1 nó còn có thể là async Task Main, và từ C# 9 top-level statement cho phép trình biên dịch tự sinh nó cho bạn.
Làm sao trả về nhiều giá trị từ một phương thức C#?
Trả về một tuple: static (int Min, int Max) Range(int[] values) và return (min, max);. Nơi gọi có thể đọc result.Min hoặc tách ra bằng var (lo, hi) = Range(values);. Với những giá trị luôn đi cùng nhau khắp code, một lớp hoặc record nhỏ sẽ rõ ràng hơn; tham số out là cách làm cũ hơn.
Tại sao gặp lỗi "An object reference is required for the non-static field, method, or property"?
Một phương thức static như Main đã gọi trực tiếp một phương thức instance. Phương thức static thuộc về kiểu và không có đối tượng nào để gọi lên. Hãy đánh dấu phương thức được gọi là static, hoặc tạo một đối tượng trước: var app = new Program(); app.Greet();.
"not all code paths return a value" nghĩa là gì?
Lỗi CS0161: một phương thức có kiểu trả về nhưng có ít nhất một nhánh chạy đến dấu ngoặc nhọn đóng mà không gặp return. Thường là một if trả về giá trị nhưng sau đó không có else hay return cuối cùng. Hãy thêm một return (hoặc throw) ở cuối phương thức.