1. Giới thiệu
Trong thế giới lập trình hướng đối tượng, tụi mình thường gặp tình huống cần thêm chức năng mới cho các class đã có sẵn. Đôi khi mình không thể đụng vào mã nguồn của các class đó (ví dụ như các kiểu chuẩn .NET, class từ thư viện bên ngoài hoặc code được sinh tự động).
Những lúc như vậy, Extension Methods xuất hiện như một tính năng cực mạnh và tinh tế của C#, cho phép bạn "giả vờ" thêm phương thức mới vào các kiểu đã có mà không cần sửa trực tiếp chúng.
Extension methods là cú pháp sugar trong C#, cho phép bạn gọi static method như thể nó là instance method của kiểu đó. Nhìn và dùng cứ như là nó luôn là một phần của kiểu mở rộng, dù thực ra không phải vậy.
Giả sử bạn cần một phương thức để viết hoa chữ cái đầu tiên của chuỗi:
public static class StringUtil
{
public static string CapitalizeFirstLetter(string str)
{
if (string.IsNullOrEmpty(str)) return str;
return char.ToUpper(str[0]) + str.Substring(1);
}
}
// sử dụng
string hello = StringUtil.CapitalizeFirstLetter("xin chào"); // "Xin chào"
Console.WriteLine(hello);
Code thì vẫn là code thôi, nhưng C# cho phép viết gọn hơn nhiều.
Dòng này
string hello = StringUtil.CapitalizeFirstLetter("xin chào"); // "Xin chào"
có thể viết như này:
string hello = "xin chào".CapitalizeFirstLetter(); // "Xin chào"
Và giờ mình sẽ giải thích tại sao lại được như vậy.
2. Extension Methods hoạt động như thế nào
Extension Methods nhìn thì có vẻ "ảo diệu", nhưng thực ra nó khá đơn giản. Extension method thực chất chỉ là static method bình thường, khai báo trong static class, nhưng có một điểm khác biệt quan trọng: tham số đầu tiên của nó phải có từ khóa this. Chính tham số này chỉ ra kiểu nào sẽ được "mở rộng" bởi method đó.
Xem ví dụ mở rộng kiểu string nhé:
namespace MyProject.Extensions
{
// 1. Extension Methods phải được khai báo trong static class.
public static class StringExtensions
{
// 2. Method phải là static.
// 3. Tham số đầu tiên phải có từ khóa 'this'.
public static string CapitalizeFirstLetter(this string str)
{
if (string.IsNullOrEmpty(str))
return str;
// Dùng String.ToUpper và String.Substring để tạo chuỗi mới
return char.ToUpper(str[0]) + str.Substring(1);
}
// Thêm một cái nữa cho vui:
public static int WordCount(this string text)
{
if (string.IsNullOrEmpty(text))
return 0;
// Đếm từ đơn giản bằng cách tách theo dấu cách
return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
Để dùng các method này, bạn chỉ cần import namespace MyProject.Extensions trong file của bạn:
using MyProject.Extensions; // Quan trọng: import namespace chứa Extension Methods
string hello = "xin chào, thế giới!";
Console.WriteLine(hello.CapitalizeFirstLetter()); // In ra: "Xin chào, thế giới!"
string sentence = "Đây là chuỗi kiểm tra để đếm số từ.";
Console.WriteLine($"Số từ: {sentence.WordCount()}"); // In ra: "Số từ: 7"
string emptyString = "";
Console.WriteLine(emptyString.CapitalizeFirstLetter()); // In ra: ""
Lưu ý: Nếu không có từ khóa this trước tham số đầu tiên string str, CapitalizeFirstLetter chỉ là static method bình thường của class StringExtensions, và bạn sẽ phải gọi nó như StringExtensions.CapitalizeFirstLetter("xin chào"). Chính this biến nó thành Extension Method, cho phép gọi kiểu hướng đối tượng.
3. Cú pháp Extension Methods
Để tạo và dùng Extension Methods, làm theo các bước sau:
- Tạo static class: Tất cả Extension Methods phải nằm trong static class. Tên class có thể tùy ý, nhưng thường đặt là [TypeName]Extensions (ví dụ StringExtensions, ListExtensions, DateTimeExtensions).
- Khai báo static method: Trong static class, viết static method như bình thường.
- Thêm từ khóa this vào tham số đầu: Quan trọng nhất là tham số đầu tiên phải có từ khóa this, theo sau là kiểu bạn muốn mở rộng. Tham số này chính là instance mà bạn sẽ gọi Extension Method trên đó.
- Import namespace: Trong file bạn muốn dùng Extension Method, nhớ import namespace chứa static class đó (using). Nếu không, compiler sẽ không tìm thấy method mở rộng của bạn.
- Dùng như method bình thường: Giờ bạn có thể gọi Extension Method như thể nó là instance method của object thuộc kiểu đó.
4. "Ảo diệu" thực ra là gì?
Cái gọi là "ảo diệu" của Extension Methods là nhờ compiler C#. Khi chạy chương trình, Extension Methods không thực sự là method của class. Thay vào đó, compiler C# sẽ chuyển lời gọi Extension Method thành static call bình thường.
Khi bạn viết obj.Method(args), với Method là Extension Method, compiler sẽ dịch thành ContainingClass.Method(obj, args). Việc này diễn ra ngầm, tạo cảm giác như method là một phần của class.
Xử lý xung đột: Extension Methods vs Method gốc
Nếu class đã có method "gốc" (instance method) trùng tên và signature với Extension Method thì sao?
public class MyClass
{
public void DoSomething(int value)
{
Console.WriteLine($"Method gốc MyClass.DoSomething: {value}");
}
}
public static class MyClassExtensions
{
public static void DoSomething(this MyClass instance, int value)
{
Console.WriteLine($"Extension Method MyClassExtensions.DoSomething: {value}");
}
}
// Sử dụng:
MyClass obj = new MyClass();
obj.DoSomething(10); // Sẽ gọi method gốc MyClass.DoSomething: 10
Quy tắc ưu tiên: Nếu class đã có instance method trùng tên và signature (số lượng và kiểu tham số), luôn luôn gọi method gốc, không phải Extension Method. Extension Methods có ưu tiên thấp hơn khi trùng tên.
Giới hạn của Extension Methods
- Không override được method đã có: Extension Methods không tham gia vào tính đa hình (polymorphism).
- Không thêm được field, property hay event mới: Chỉ thêm được method thôi. Không thể dùng để thêm trạng thái mới cho kiểu đã có.
- Không mở rộng static class: Tham số đầu this luôn phải là instance (object) của kiểu.
- Không mở rộng field hay property: Chỉ mở rộng được kiểu thôi.
- Không truy cập được member private hay protected: Extension Methods chỉ làm việc với member public của kiểu mở rộng.
Tuy nhiên, bạn có thể thêm Extension Methods cho:
- Interface: Cực kỳ mạnh nhé! Bạn có thể thêm method cho tất cả class implement một interface nào đó mà không cần sửa interface hay từng class. Ví dụ, mọi class implement IEnumerable<T> đều "nhận" được Extension Methods từ LINQ.
- Kiểu object: Cho phép tạo Extension Methods dùng được cho bất kỳ object nào trong C#. Nhưng nhớ cẩn thận, kẻo làm bẩn global namespace và gây xung đột tên.
5. Ví dụ thực tế và Best Practices
Extension Methods không chỉ là lý thuyết, mà là công cụ cực mạnh cho dev mỗi ngày. Dưới đây là vài tình huống thực tế:
1. Mở rộng DateTime:
Thêm các method tiện cho xử lý ngày tháng, rất hay dùng trong business logic.
using System;
namespace MyProject.TimeHelpers
{
public static class DateTimeExtensions
{
public static bool IsWeekend(this DateTime date)
{
return date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;
}
public static bool IsWeekday(this DateTime date)
{
return !date.IsWeekend(); // Dùng Extension Method đã có!
}
public static DateTime NextDay(this DateTime date)
{
return date.AddDays(1);
}
public static DateTime AddBusinessDays(this DateTime date, int days)
{
DateTime newDate = date;
while (days > 0)
{
newDate = newDate.NextDay();
if (newDate.IsWeekday())
{
days--;
}
}
return newDate;
}
}
}
Sử dụng:
using MyProject.TimeHelpers;
DateTime today = DateTime.Now;
if (today.IsWeekend())
{
Console.WriteLine("Hôm nay là cuối tuần!");
}
else
{
Console.WriteLine("Hôm nay là ngày trong tuần.");
}
DateTime tomorrow = today.NextDay();
Console.WriteLine($"Ngày mai: {tomorrow.ToShortDateString()}");
DateTime futureDate = today.AddBusinessDays(5);
Console.WriteLine($"Sau 5 ngày làm việc sẽ là: {futureDate.ToShortDateString()}");
2. Mở rộng kiểu tự định nghĩa (ví dụ Dog):
Ngay cả khi bạn có quyền sửa class, Extension Methods vẫn hữu ích để tách logic phụ hoặc tuân thủ nguyên tắc trách nhiệm đơn (Single Responsibility Principle).
namespace MyProject.Animals
{
public class Dog
{
public string Name { get; set; }
public int Age { get; set; }
public string Breed { get; set; }
}
public static class DogExtensions
{
public static bool IsPuppy(this Dog dog)
{
// Xác định chó con
return dog.Age < 2;
}
public static string GetAgeCategory(this Dog dog)
{
if (dog.Age < 1) return "Chó con";
if (dog.Age < 7) return "Chó trưởng thành";
return "Chó già";
}
public static void Bark(this Dog dog)
{
Console.WriteLine($"{dog.Name} nói: Gâu! Gâu!");
}
}
}
Sử dụng:
using MyProject.Animals;
var sharik = new Dog { Name = "Sharik", Age = 1, Breed = "Cho ta" };
if (sharik.IsPuppy())
{
Console.WriteLine($"{sharik.Name} — vẫn là chó con!"); // Sharik — vẫn là chó con!
}
Console.WriteLine($"{sharik.Name} thuộc loại: {sharik.GetAgeCategory()}"); // Sharik thuộc loại: Chó con
sharik.Bark(); // Sharik nói: Gâu! Gâu!
var rex = new Dog { Name = "Rex", Age = 5, Breed = "Chó becgie" };
Console.WriteLine($"{rex.Name} thuộc loại: {rex.GetAgeCategory()}"); // Rex thuộc loại: Chó trưởng thành
Best Practices:
Đặt tên có ý nghĩa: Tên class mở rộng (StringExtensions, ListExtensions) và method nên rõ ràng, thể hiện đúng chức năng.
Đặt vào namespace hợp lý: Gom Extension Methods vào namespace phù hợp với mục đích, tránh làm loãng global namespace.
Dùng hợp lý: Đừng lạm dụng Extension Methods. Nếu bạn có thể sửa class và thêm method trực tiếp, có thể đó là giải pháp sạch hơn. Extension Methods hợp nhất cho việc thêm chức năng không xâm lấn.
Tránh trùng tên: Nhớ quy tắc ưu tiên method gốc. Nếu Extension Method trùng tên và signature với method đã có, nó sẽ không được gọi.
Method nên rõ ràng: Extension Methods nên làm một việc cụ thể, đừng viết quá phức tạp.
Dễ test: Extension Methods là static method bình thường nên rất dễ test.
GO TO FULL VERSION