5.5 - Extension Methods
Tóm tắt: Extension method cho phép "thêm" phương thức vào một kiểu bạn không sở hữu —
string,DateTime,IEnumerable. Thực chất nó chỉ là phương thức static với cú pháp gọi đẹp hơn, và hai hệ quả của điều đó hay làm người ta bất ngờ: phương thức của chính class luôn được ưu tiên hơn extension cùng tên, và gọi được trên một tham chiếunullmà không némNullReferenceException— vì không có lời gọi trên instance nào cả. Toàn bộ LINQ được xây trên cơ chế này. Bài này cũng nói về mặt trái: extension đặt sai chỗ làm code khó lần nguồn gốc.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết extension method đúng cú pháp và đặt nó ở namespace hợp lý.
- Giải thích độ ưu tiên khi tên trùng với phương thức của class.
- Dự đoán đúng hành vi khi gọi extension trên
null. - Nhận ra ba dấu hiệu lạm dụng extension method.
- Dùng extension để viết
IServiceCollectionvàIApplicationBuildergọn hơn. - Đọc và viết extension property bằng khối
extensionmới của C# 14.
Cần biết trước: bài 5.3 (LINQ), bài 4.2 (đóng gói), bài 4.6 (internal).
Nội dung bài học
5.5.1 — Cú pháp và bản chất
public static class StringExtensions // phải static
{
public static bool IsValidEmail(this string? value) // this ở tham số ĐẦU
=> !string.IsNullOrWhiteSpace(value)
&& value.Contains('@')
&& value.IndexOf('@') < value.LastIndexOf('.');
}
// Gọi như phương thức của string
if (input.IsValidEmail()) { }
// Trình biên dịch dịch thành
if (StringExtensions.IsValidEmail(input)) { }Ba điều kiện bắt buộc: class phải static, phương thức phải static, tham số đầu tiên phải có từ khoá this.
Điểm cuối cùng — nó chỉ là đường cú pháp — giải thích mọi hành vi còn lại của extension method.
5.5.2 — Độ ưu tiên: class luôn thắng
public class Order
{
public string Describe() => "từ class";
}
public static class OrderExtensions
{
public static string Describe(this Order o) => "từ extension";
}
new Order().Describe(); // "từ class"Phương thức của chính class luôn được ưu tiên. Extension chỉ được xét khi trình biên dịch không tìm thấy phương thức phù hợp trên kiểu đó.
Hệ quả thực tế đáng lưu ý: nếu tác giả thư viện thêm một phương thức trùng tên vào class ở phiên bản sau, extension của bạn âm thầm ngừng được gọi. Code vẫn biên dịch, hành vi đổi. Đây là lý do nên tránh đặt tên extension trùng với tên có khả năng xuất hiện trong chính kiểu đó.
Khi có nhiều extension cùng tên từ nhiều namespace, trình biên dịch báo lỗi mơ hồ và bạn phải gọi tường minh dạng static.
Trình biên dịch chọn phương thức theo thứ tự này:
5.5.3 — Gọi được trên null
public static bool IsEmpty(this string? s) => string.IsNullOrEmpty(s);
string? name = null;
bool result = name.IsEmpty(); // TRUE — không ném ngoại lệVì đây thực chất là StringExtensions.IsEmpty(null) — một lời gọi static với đối số null — nên không có lời gọi trên instance nào để mà NullReferenceException.
Điều này vừa hữu ích vừa nguy hiểm:
// Hữu ích — viết được hàm kiểm tra null rất gọn
public static bool HasValue(this string? s) => !string.IsNullOrWhiteSpace(s);
// Nguy hiểm — trông như gọi trên instance, người đọc tưởng đã chắc chắn khác null
public static string NormalizeKey(this string s) => s.Trim().ToLowerInvariant();
name.NormalizeKey(); // NỔ, nhưng nổ BÊN TRONG extension chứ không phải ở đâyStack trace khi name là null cho thấy dòng đầu tiên nằm trong extension:
at StringExtensions.NormalizeKey(String s) in ...\Program.cs:line 41
at Program.<Main>$(String[] args) in ...\Program.cs:line 9Một cái bẫy khác liên quan tới mục 5.5.2: nếu đặt tên extension là Normalize, nó không bao giờ được gọi, vì string đã có sẵn phương thức Normalize() (chuẩn hoá Unicode) và phương thức của class luôn thắng. name.Normalize() khi đó gọi string.Normalize() và ném NullReferenceException ngay tại dòng gọi.
Quy tắc: nếu extension có thể nhận null, hãy khai tham số là string? và xử lý tường minh. Nếu không, khai string và thêm ArgumentNullException.ThrowIfNull(s) ở đầu để thông báo lỗi nêu đúng tên tham số. Với nullable reference types bật sẵn (mặc định từ .NET 6), trình biên dịch còn cảnh báo CS8604 khi bạn truyền biến string? vào extension nhận string (bài 5.6).
5.5.4 — LINQ là extension method
// Toàn bộ LINQ nằm trong System.Linq.Enumerable
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)Đây là lý do cần using System.Linq mới thấy Where, Select. Project tạo từ template .NET 6 trở lên bật implicit usings, tự thêm using System.Linq cùng vài namespace phổ biến, nên bạn ít khi phải gõ dòng này. Nhưng extension của bạn đặt trong namespace riêng thì vẫn phải using namespace đó; không có using thì extension không nằm trong phạm vi và trình biên dịch báo không tìm thấy phương thức (CS1061).
Và đây cũng là lý do LINQ hoạt động với mọi thứ cài đặt IEnumerable<T> — kể cả kiểu do bạn viết, mà không cần Microsoft biết trước về nó. Đó chính là bài toán extension method sinh ra để giải: mở rộng một interface đã phát hành mà không sửa nó.
5.5.5 — Nơi extension toả sáng trong ASP.NET Core
Mục này là xem trước: builder.Services, AddScoped là cách đăng ký service vào DI container, học ở Module 7. Điều cần thấy ở đây là extension method gom các dòng đăng ký lại thành một lời gọi có tên.
// Không có extension — Program.cs phình ra hàng trăm dòng
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<ICustomerRepository, SqlCustomerRepository>();
// ... 40 dòng nữa ...
// Có extension — mỗi tầng tự khai báo phần của mình
builder.Services
.AddInfrastructure(builder.Configuration)
.AddApplication()
.AddApiVersioning();public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(config.GetConnectionString("Default")));
services.AddScoped<IOrderRepository, SqlOrderRepository>();
return services; // trả về để nối chuỗi
}
}Hai chi tiết đáng chú ý: trả về chính đối tượng để nối chuỗi được, và đặt lớp extension trong cùng project với thứ nó đăng ký — nhờ vậy Program.cs không cần biết SqlOrderRepository tồn tại, đúng tinh thần internal ở bài 4.6.
5.5.6 — Ba dấu hiệu lạm dụng
1. Extension cho kiểu bạn sở hữu.
// Bạn viết ra Order — vậy thêm phương thức vào thẳng class
public static decimal GetDiscount(this Order order) => /* ... */;Extension không truy cập được thành viên private, nên logic nghiệp vụ nằm ở đó sẽ chỉ dùng được dữ liệu công khai — đẩy bạn về phía anemic model.
2. Extension chứa logic nghiệp vụ nặng.
// Lấy dữ liệu từ database bên trong một extension của string?
public static Customer? ToCustomer(this string email) { /* truy vấn DB */ }Extension nên là hàm thuần, biến đổi dữ liệu. Có phụ thuộc thì nó phải là một service nhận qua constructor.
3. Extension trong namespace quá rộng.
namespace System; // đổ mọi extension vào đâyMọi file trong solution tự động thấy chúng, kể cả nơi không cần. Nên đặt trong namespace của chính module — MyApp.Orders.Extensions — để người dùng chọn using một cách có ý thức.
Phép thử đơn giản: "Người đọc code này có đoán được phương thức đó đến từ đâu không?" Nếu không, extension đang gây hại nhiều hơn giúp.
5.5.7 — C# 14: khối extension
Cú pháp this ở trên chỉ viết được extension method. C# 14 (đi kèm .NET 10) thêm khối extension, cho phép viết cả extension property và extension static:
// Customer.cs
public class Customer
{
public string Name { get; set; } = "";
public decimal TotalRevenue { get; set; }
}
// CustomerExtensions.cs
public static class CustomerExtensions
{
extension(Customer c) // c là "this" của các thành viên bên trong
{
public bool IsVip => c.TotalRevenue > 50_000_000m; // extension property
}
extension(Customer) // không đặt tên: dành cho thành viên static
{
public static Customer FromName(string name) => new() { Name = name };
}
}// Program.cs
var c = new Customer { Name = "An", TotalRevenue = 60_000_000m };
Console.WriteLine(c.IsVip); // True — gọi như property của Customer
Console.WriteLine(Customer.FromName("Bình").Name); // Bình — gọi như static của CustomerDán vào một file Program.cs thì khối Program.cs đặt lên đầu, khối khai báo đặt xuống cuối (C# bắt buộc, nếu không sẽ báo lỗi CS8803).
Mọi quy tắc của mục này vẫn đúng với cú pháp mới: nó vẫn là thành viên static được trình biên dịch dịch lại, class vẫn thắng khi trùng tên. Cú pháp this cũ vẫn hợp lệ và là thứ bạn gặp nhiều nhất trong code hiện có cùng tài liệu; dùng khối extension khi cần property hoặc static. (Ví dụ trên chỉ để minh hoạ cú pháp: Customer là kiểu của bạn, nên theo mục 5.5.6 thì IsVip nên nằm thẳng trong class.)
5.5.8 — Rà lại code của bạn
Danh sách rà soát extension method
- Chỉ viết extension cho kiểu bạn KHÔNG sở hữu.
- Extension là hàm thuần, không gọi database hay HTTP bên trong.
- Extension nhận null đều khai tham số nullable và xử lý tường minh.
- Không đặt extension trong namespace System hay namespace gốc của dự án.
- Tên extension không trùng với tên có khả năng xuất hiện trong chính kiểu đó.
- Extension đăng ký DI trả về chính đối tượng để nối chuỗi.
- Lớp extension đăng ký DI nằm cùng project với thứ nó đăng ký.
Bài tập áp dụng
Bài 1 — Chứng minh độ ưu tiên: class luôn thắng
Tạo một class có phương thức Describe() và một extension cùng tên. Gọi và ghi kết quả. Sau đó xoá phương thức trong class và gọi lại.
Tiêu chí hoàn thành: bạn nêu được hậu quả của quy tắc này khi nâng cấp một thư viện.
Gợi ý và lời giải — Bài 1
Gợi ý. Trình biên dịch tìm phương thức theo thứ tự: trước hết trong chính kiểu đó và các lớp cha, chỉ khi không tìm thấy mới tìm tới extension method.
Lời giải.
// Customer.cs
public class Customer
{
public string Describe() => "từ class";
}
// CustomerExtensions.cs
public static class CustomerExtensions
{
public static string Describe(this Customer c) => "từ extension";
}// Program.cs
var c = new Customer();
Console.WriteLine(c.Describe()); // "từ class"Xoá phương thức trong class, giữ nguyên extension:
Console.WriteLine(c.Describe()); // "từ extension"Không có cảnh báo nào ở cả hai trường hợp. Trình biên dịch lặng lẽ chọn, và bạn không biết nó đã chọn cái nào nếu không đọc kỹ.
Hậu quả khi nâng cấp thư viện — đây là phần quan trọng. Giả sử bạn viết một extension để bù cho thứ thư viện chưa có:
// Thư viện version 1.0 chưa có phương thức này
public static class SdkExtensions
{
// Task<T?> là kết quả của phương thức bất đồng bộ (Module 6)
public static Task<T?> GetAsync<T>(this ISdkClient c, string id)
=> /* cài đặt riêng của bạn, có xử lý lỗi và retry */;
}Nâng thư viện lên 2.0, và bản mới thêm một phương thức GetAsync vào chính ISdkClient. Kể từ lúc đó:
- Mọi lời gọi
client.GetAsync(...)âm thầm chuyển sang dùng bản của thư viện. - Extension của bạn — với phần xử lý lỗi và thử lại — không bao giờ được gọi nữa.
- Code vẫn biên dịch sạch. Không cảnh báo, không lỗi.
- Hành vi đổi: có thể khác về xử lý lỗi, khác về giá trị trả về khi không tìm thấy, khác về việc có thử lại hay không.
Đây là một trong những cách đổi hành vi khó phát hiện nhất, vì không có gì trong code của bạn thay đổi.
Ba cách phòng:
- Đặt tên extension khác với tên có thể xuất hiện trong thư viện. Thêm tiền tố của dự án, ví dụ
CrmGetAsync. Xấu hơn nhưng an toàn. - Viết một bài kiểm thử khẳng định hành vi, không chỉ khẳng định kết quả. Nếu extension của bạn có thử lại, hãy kiểm thử rằng nó thử lại thật.
- Đọc ghi chú phát hành khi nâng phiên bản chính. Phương thức mới thêm vào interface là thay đổi đáng chú ý, dù về mặt kỹ thuật nó không phá vỡ biên dịch.
Một biến thể tinh vi hơn: default interface method. Từ C# 8, interface có thể có phương thức với cài đặt sẵn. Quy tắc ưu tiên lúc đó phức tạp hơn — cài đặt trong lớp thắng cài đặt mặc định của interface, và extension method vẫn xếp sau cùng. Khi cả ba cùng tồn tại, việc suy ra cái nào được gọi trở nên khó, và đó là lý do nên tránh để ba thứ cùng tên tồn tại.
Bài 2 — Gọi extension trên null
Viết một extension nhận string? và một extension nhận string. Gọi cả hai với biến null. Ghi lại cái nào chạy được và cái nào ném ở đâu.
Tiêu chí hoàn thành: bạn giải thích được vì sao gọi phương thức trên null lại không ném NullReferenceException.
Gợi ý và lời giải — Bài 2
Gợi ý. Extension method chỉ là cú pháp đường cho một phương thức tĩnh. Hãy viết lại lời gọi dưới dạng phương thức tĩnh thông thường và câu trả lời sẽ rõ.
Lời giải.
// StringExtensions.cs
public static class StringExtensions
{
public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
public static int GetLength(this string s) => s.Length;
}// Program.cs
string? x = null;
Console.WriteLine(x.IsBlank()); // True — CHẠY BÌNH THƯỜNG
Console.WriteLine(x.GetLength()); // NullReferenceException (kèm cảnh báo CS8604 lúc biên dịch)Vì sao cái thứ nhất chạy được. Trình biên dịch dịch lời gọi extension thành lời gọi phương thức tĩnh:
StringExtensions.IsBlank(x); // x = null là một tham số hợp lệ
StringExtensions.GetLength(x); // cũng gọi được, nhưng bên trong s.Length némVì đây là phương thức tĩnh, runtime không cần tìm bảng phương thức ảo trên đối tượng, nên không có phép giải tham chiếu nào xảy ra tại điểm gọi. null chỉ đơn giản là giá trị của tham số đầu tiên.
Ngoại lệ ở GetLength không phát sinh tại x.GetLength() mà phát sinh bên trong thân hàm, ở dòng s.Length. Stack trace chạy thật cho thấy dòng đầu là at StringExtensions.GetLength(String s), dòng thứ hai mới là chỗ gọi. Chi tiết này quan trọng khi đọc dấu vết ngăn xếp.
Ứng dụng hữu ích — viết API dễ chịu hơn:
public static bool HasValue(this string? s) => !string.IsNullOrWhiteSpace(s);
public static IReadOnlyList<T> OrEmpty<T>(this IEnumerable<T>? source)
=> source?.ToList() ?? [];
// Dùng được mà không cần kiểm tra null trước
if (customer.Note.HasValue()) { }
foreach (var x in maybeNullList.OrEmpty()) { }Cái bẫy — người đọc không biết nó an toàn. Nhìn customer.Note.HasValue(), không ai đoán được đây là extension chấp nhận null. Họ sẽ tưởng Note chắc chắn khác null và viết customer.Note.Trim() ngay dòng dưới — dòng đó thì ném thật.
Vì vậy hãy đánh dấu rõ ràng bằng kiểu tham số và bằng thuộc tính:
using System.Diagnostics.CodeAnalysis; // NotNullWhen
public static bool HasValue([NotNullWhen(true)] this string? s)
=> !string.IsNullOrWhiteSpace(s);[NotNullWhen(true)] nói với trình biên dịch: khi hàm trả true thì s chắc chắn khác null. Nhờ vậy:
if (customer.Note.HasValue())
{
var x = customer.Note.Trim(); // không còn cảnh báo — trình biên dịch đã biết
}Đây là cách dạy trình biên dịch hiểu hàm của bạn, chủ đề chính của bài 5.6.
Một lưu ý khi bật Nullable Reference Types. Extension nhận string (không có dấu chấm hỏi) mà bị gọi trên một biến string? sẽ nhận cảnh báo CS8604. Đó là điều tốt — cảnh báo đúng chỗ. Đừng dập nó bằng toán tử !; hãy đổi tham số thành string? nếu hàm thật sự xử lý được null, hoặc kiểm tra null trước khi gọi.
Bài 3 — Dọn Program.cs bằng extension method
Program.cs của một API CRM dài 94 dòng, phần lớn là đăng ký dịch vụ. Phần đầu của nó in trong lời giải ("trước"). Phác thảo cách gom các dòng đăng ký theo tầng thành các extension AddXxx. Bài này đọc trước kiến thức DI (Module 7) và ASP.NET Core (Module 8): chưa cần chạy được, chỉ cần viết được chữ ký các extension và nói được dòng nào đi vào extension nào.
Tiêu chí hoàn thành: Program.cs mới dưới 40 dòng, và bạn nêu được lợi ích kiến trúc ngoài chuyện ngắn hơn.
Gợi ý và lời giải — Bài 3
Gợi ý. Nhóm các đăng ký theo tầng hoặc theo tính năng, không theo loại kỹ thuật. Mỗi nhóm thành một extension đặt trong đúng dự án mà nó thuộc về.
Lời giải — trước, 94 dòng:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<CrmDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Crm")));
builder.Services.AddScoped<ICustomerRepository, EfCustomerRepository>();
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<ILeadRepository, EfLeadRepository>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
builder.Services.AddStackExchangeRedisCache(o => { /* ... */ });
builder.Services.AddAuthentication(/* 12 dòng */);
builder.Services.AddAuthorization(/* 8 dòng */);
builder.Services.AddOpenApi(/* 15 dòng cấu hình tài liệu API */);
// ... còn 40 dòng nữaSau, 18 dòng:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddCrmDomain()
.AddCrmInfrastructure(builder.Configuration)
.AddCrmAuthentication(builder.Configuration)
.AddCrmApi();
var app = builder.Build();
app.UseCrmPipeline();
app.MapControllers();
app.Run();Với mỗi extension đặt trong đúng dự án mà nó cấu hình:
// Crm.Infrastructure/DependencyInjection.cs
namespace Microsoft.Extensions.DependencyInjection; // để không phải thêm using
public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddCrmInfrastructure(
this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<CrmDbContext>(o =>
o.UseNpgsql(config.GetConnectionString("Crm")));
services.AddScoped<ICustomerRepository, EfCustomerRepository>();
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddScoped<ILeadRepository, EfLeadRepository>();
services.AddSingleton<IEmailSender, SmtpEmailSender>();
return services; // trả về để nối chuỗi
}
}94 xuống 18 dòng. Nhưng lợi ích kiến trúc mới là phần chính:
Mỗi dự án tự khai báo cách đăng ký của nó. Thêm một repository vào
Crm.Infrastructurechỉ cần sửa file trong chính dự án đó —Program.csvà các dự án khác không đụng tới. Đây cũng là cách để dự án hạ tầng không lộ các lớp cài đặt ra ngoài, đúng như bài 4.6 đã nêu: chúng có thể làinternal, chỉ extension method làpublic.Program.csthành bản đồ hệ thống. Đọc 18 dòng là biết ứng dụng gồm những phần nào. Đọc 94 dòng đăng ký thì không.Giảm xung đột Git. Với
Program.csdài, mọi người thêm dịch vụ đều sửa cùng một file, nên xung đột liên tục — đúng vấn đề ở bài 3.7. Tách ra thì mỗi người sửa file của tầng mình.
Hai quy ước nên theo:
- Đặt namespace là
Microsoft.Extensions.DependencyInjection. Nhờ vậyProgram.cskhông cần thêmusingcho từng dự án — cùng cách mà chính Microsoft làm với các gói của họ. - Luôn
return services. Cho phép nối chuỗi, và đó là quy ước mà mọi thư viện .NET đều theo.
Áp dụng cho cả đường ống middleware:
public static WebApplication UseCrmPipeline(this WebApplication app)
{
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("Default");
app.UseAuthentication();
app.UseAuthorization();
return app;
}Lưu ý ở đây thứ tự vẫn quan trọng như bài 8.2 trình bày — gom vào extension không làm thay đổi điều đó, chỉ đưa nó vào một chỗ duy nhất để dễ rà soát và ghi chú lý do.
Tự kiểm tra
Extension method thực chất là gì?
Là một phương thức static trong một class static, với tham số đầu tiên có từ khoá this. Trình biên dịch dịch lời gọi kiểu input.IsValidEmail thành StringExtensions.IsValidEmail(input). Nó chỉ là đường cú pháp, và điều đó giải thích mọi hành vi còn lại của nó.
Khi extension trùng tên với phương thức của class thì sao?
Phương thức của class luôn thắng; extension chỉ được xét khi không tìm thấy phương thức phù hợp trên kiểu đó. Hệ quả cần cảnh giác: nếu tác giả thư viện thêm phương thức trùng tên ở phiên bản sau, extension của bạn âm thầm ngừng được gọi — code vẫn biên dịch nhưng hành vi đổi.
Vì sao gọi extension trên biến null lại không ném ngoại lệ?
Vì đó thực chất là một lời gọi static với đối số null, không có lời gọi trên instance nào cả. Điều này hữu ích để viết hàm kiểm tra null gọn gàng, nhưng cũng nguy hiểm vì người đọc tưởng đã chắc chắn khác null. Nếu extension có thể nhận null thì nên khai tham số là kiểu nullable và xử lý tường minh.
Vì sao LINQ cần using System.Linq?
Vì toàn bộ LINQ là extension method nằm trong lớp System.Linq.Enumerable. Không có using thì chúng không nằm trong phạm vi và trình biên dịch báo không tìm thấy phương thức. Đây cũng là lý do LINQ hoạt động với mọi kiểu cài đặt IEnumerable, kể cả kiểu bạn tự viết.
Vì sao không nên viết extension cho kiểu mình sở hữu?
Vì extension không truy cập được thành viên private, nên logic đặt ở đó chỉ dùng được dữ liệu công khai, đẩy bạn về phía anemic model. Kiểu của chính bạn thì thêm phương thức vào thẳng class, nơi nó truy cập được trạng thái nội bộ và giữ được bất biến.
Nên đặt extension ở namespace nào?
Namespace của chính module đó, ví dụ MyApp.Orders.Extensions, để người dùng chọn using một cách có ý thức. Đặt vào namespace System hay namespace gốc khiến mọi file trong solution tự động thấy chúng, kể cả nơi không cần, và người đọc không đoán được phương thức đến từ đâu.
C# 14 thêm gì cho extension?
Khối extension(Kiểu tham số) đặt trong lớp static, cho phép viết extension property và extension static, điều cú pháp this cũ không làm được. Bản chất không đổi: vẫn là thành viên static được trình biên dịch dịch lại, và phương thức của class vẫn thắng khi trùng tên. Cú pháp this cũ vẫn hợp lệ.
Kết luận
Ba điều đáng nhớ nhất:
- Chỉ là phương thức static với cú pháp đẹp. Mọi hành vi lạ của extension đều suy ra từ câu này.
- Chỉ viết cho kiểu bạn không sở hữu. Kiểu của mình thì thêm phương thức vào thẳng class.
- Phép thử: người đọc có đoán được nó đến từ đâu không? Không đoán được thì extension đang gây hại.
Tham khảo
- Extension methods
- LINQ overview — ví dụ lớn nhất của extension method
- Standard query operators
- Framework design guidelines — khi nào nên dùng extension
Điều hướng
- Bài trước: 5.4 — Delegates và Events
- Bài tiếp theo: 5.6 — Nullable Reference Types (C# 8+)
- Về module: Trang mục lục