C# 14 — Những Tính Năng Đột Phá Trong .NET 10
Posted on: 4/23/2026 4:11:58 AM
Table of contents
- 1. Extension Members — Cuộc cách mạng Extension
- 2. Field Keyword — Tạm biệt Backing Field thủ công
- 3. Null-Conditional Assignment — Gán giá trị an toàn
- 4. Implicit Span Conversions — Hiệu năng đẳng cấp
- 5. Lambda Parameters với Modifiers
- 6. Partial Events và Constructors
- 7. User-Defined Compound Assignment
- 8. Unbound Generic Types trong nameof
- Tổng hợp: Bảng so sánh trước và sau C# 14
- Thực hành: Refactor một Service thực tế
- Lộ trình áp dụng
- Tài liệu tham khảo
Nếu bạn đang dùng .NET và vẫn viết extension method kiểu cũ với this parameter, hoặc phải khai báo backing field thủ công mỗi khi cần validate trong setter — thì C# 14 sẽ thay đổi hoàn toàn cách bạn code. Phiên bản mới nhất của ngôn ngữ, đi kèm .NET 10 LTS, mang đến những cải tiến lớn về cú pháp, hiệu năng và khả năng biểu đạt mà mọi .NET developer đều nên biết.
1. Extension Members — Cuộc cách mạng Extension
Đây là tính năng được chờ đợi nhất trong C# 14. Từ trước đến nay, extension method là cách duy nhất để "gắn" thêm chức năng vào một type có sẵn. Nhưng bạn không thể viết extension property, extension operator, hay extension static member. C# 14 thay đổi hoàn toàn điều này với cú pháp extension block mới.
Điểm nhấn
Extension members trong C# 14 tương thích hoàn toàn về source và binary với extension method hiện có. Bạn có thể migrate dần mà không cần recompile code phụ thuộc.
Cú pháp mới
public static class EnumerableExtensions
{
// Extension block cho instance members
extension<TSource>(IEnumerable<TSource> source)
{
// Extension property — HOÀN TOÀN MỚI!
public bool IsEmpty => !source.Any();
// Extension method — cú pháp mới, gọn hơn
public IEnumerable<TSource> WhereNotNull()
=> source.Where(x => x is not null);
}
// Extension block cho static members
extension<TSource>(IEnumerable<TSource>)
{
// Static extension property
public static IEnumerable<TSource> Empty
=> Enumerable.Empty<TSource>();
// User-defined operator — CỰC KỲ MẠNH
public static IEnumerable<TSource> operator +(
IEnumerable<TSource> left,
IEnumerable<TSource> right)
=> left.Concat(right);
}
}
Sử dụng
var numbers = new List<int> { 1, 2, 3 };
// Extension property — gọi như property thường
if (!numbers.IsEmpty)
{
Console.WriteLine($"Có {numbers.Count} phần tử");
}
// Extension operator — nối hai collection bằng +
var combined = numbers + new[] { 4, 5, 6 };
// combined = [1, 2, 3, 4, 5, 6]
graph LR
A["C# 3.0
Extension Methods"] --> B["C# 14
Extension Members"]
B --> C["Extension
Properties"]
B --> D["Extension
Operators"]
B --> E["Static Extension
Members"]
B --> F["Extension
Methods v2"]
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style D fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style E fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style F fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
2. Field Keyword — Tạm biệt Backing Field thủ công
Bao nhiêu lần bạn phải khai báo một private field chỉ để thêm chút logic validate trong setter? Với từ khóa field mới, compiler sẽ tự tạo backing field cho bạn.
Trước C# 14
private string _name = "";
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
private int _age;
public int Age
{
get => _age;
set => _age = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
Với C# 14
public string Name
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
public int Age
{
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
Mẹo hay
Nếu class bạn đã có một biến tên field từ trước, dùng @field hoặc this.field để phân biệt với từ khóa mới. Compiler sẽ cảnh báo nếu phát hiện trùng tên.
3. Null-Conditional Assignment — Gán giá trị an toàn
Toán tử ?. và ?[] giờ đây có thể đứng ở bên trái phép gán. Đây là thay đổi nhỏ nhưng cực kỳ hữu ích trong code hàng ngày.
Trước C# 14
if (customer is not null)
{
customer.Order = GetCurrentOrder();
}
if (customer is not null)
{
customer.Total += CalculateDiscount();
}
if (settings is not null)
{
settings["theme"] = "dark";
}
Với C# 14
customer?.Order = GetCurrentOrder();
customer?.Total += CalculateDiscount();
settings?["theme"] = "dark";
Vế phải chỉ được evaluate khi vế trái không null. Nếu customer là null, GetCurrentOrder() sẽ không bao giờ được gọi — tránh side effect không mong muốn.
| Pattern | C# 13 | C# 14 |
|---|---|---|
| Gán property có null check | if (x != null) x.P = v; | x?.P = v; |
| Compound assignment | if (x != null) x.P += v; | x?.P += v; |
| Indexer assignment | if (x != null) x[i] = v; | x?[i] = v; |
| Increment/Decrement | if (x != null) x.P++; | Không hỗ trợ (++/--) |
4. Implicit Span Conversions — Hiệu năng đẳng cấp
Span<T> và ReadOnlySpan<T> là nền tảng của high-performance .NET. C# 14 nâng chúng lên thành "first-class citizen" với các implicit conversion tự động — giảm đáng kể code ceremony khi làm việc với memory.
Trước C# 14
string line = Console.ReadLine()!;
ReadOnlySpan<char> key = line.AsSpan(0, 5); // phải gọi AsSpan()
ProcessKey(key);
byte[] buffer = GetBuffer();
Span<byte> slice = buffer.AsSpan(0, 8); // lại phải AsSpan()
ProcessSlice(slice);
Với C# 14
string line = Console.ReadLine()!;
ProcessKey(line[..5]); // implicit conversion tự động!
byte[] buffer = GetBuffer();
ProcessSlice(buffer[..8]); // không cần AsSpan()
Tại sao quan trọng?
Implicit span conversion không chỉ giảm code boilerplate — nó còn giúp compiler thực hiện tối ưu tốt hơn: ít biến tạm, ít bounds check, và aggressive inlining hiệu quả hơn. Đây là nền tảng để .NET 10 đạt được những cải thiện hiệu năng ấn tượng ở tầng BCL.
5. Lambda Parameters với Modifiers
Trước C# 14, nếu muốn dùng out, ref, in trong lambda, bạn phải khai báo kiểu cho tất cả parameters. Giờ không cần nữa.
Trước C# 14
// Phải khai báo TOÀN BỘ kiểu
TryParse<int> parse = (string text, out int result)
=> int.TryParse(text, out result);
// Dù chỉ cần modifier cho 1 param
Func<ref int, int> doubler = (ref int x) => x *= 2;
Với C# 14
// Giữ implicit typing, chỉ thêm modifier
TryParse<int> parse = (text, out result)
=> int.TryParse(text, out result);
ReadOnlySpan<int> data = [1, 2, 3];
ProcessSpan((scoped span) => span.Length);
6. Partial Events và Constructors
C# đã hỗ trợ partial method từ lâu. C# 14 mở rộng khái niệm này cho constructor và event — cực kỳ hữu ích khi kết hợp với source generators.
// File 1: Defining declaration (có thể do source generator tạo)
public partial class OrderViewModel(int orderId, string customerName)
{
public partial event EventHandler? OrderChanged;
}
// File 2: Implementing declaration (code của developer)
public partial class OrderViewModel
{
private EventHandler? _orderChanged;
public partial event EventHandler? OrderChanged
{
add => _orderChanged += value;
remove => _orderChanged -= value;
}
// Partial constructor — chạy sau primary constructor
public OrderViewModel
{
LoadOrderDetails();
ValidateCustomer();
}
}
Kết hợp Source Generator
Partial constructor đặc biệt mạnh khi dùng với MVVM source generators (CommunityToolkit.Mvvm, ReactiveUI.SourceGenerators). Generator khai báo defining declaration, developer viết logic khởi tạo trong implementing declaration — mỗi bên lo phần của mình.
7. User-Defined Compound Assignment
Trước C# 14, khi bạn viết a += b, compiler dịch thành a = a + b — tạo một object tạm không cần thiết. Giờ bạn có thể định nghĩa += trực tiếp để thực hiện in-place mutation.
public struct Vector3(float x, float y, float z)
{
public float X { get; private set; } = x;
public float Y { get; private set; } = y;
public float Z { get; private set; } = z;
// Operator + truyền thống — tạo instance mới
public static Vector3 operator +(Vector3 left, Vector3 right)
=> new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
// Compound assignment += — in-place, KHÔNG tạo instance mới
public void operator +=(Vector3 other)
{
X += other.X;
Y += other.Y;
Z += other.Z;
}
}
Lưu ý hiệu năng
User-defined compound assignment đặc biệt có ý nghĩa với struct lớn hoặc trong tight loop xử lý số học, game engine, physics simulation — nơi mà việc tạo bản copy tạm ảnh hưởng trực tiếp đến GC pressure và cache locality.
8. Unbound Generic Types trong nameof
Thay đổi nhỏ nhưng tiện lợi: nameof giờ chấp nhận generic type không cần đóng kiểu.
// C# 13 — phải chọn 1 kiểu cụ thể (arbitrary)
string name1 = nameof(List<int>); // "List"
string name2 = nameof(Dictionary<string, object>); // "Dictionary"
// C# 14 — dùng unbound generic
string name3 = nameof(List<>); // "List"
string name4 = nameof(Dictionary<,>); // "Dictionary"
Tổng hợp: Bảng so sánh trước và sau C# 14
| Tính năng | Trước C# 14 | C# 14 | Lợi ích chính |
|---|---|---|---|
| Extension property | Không thể | extension(T source) { public P ... } | API tự nhiên hơn |
| Extension operator | Không thể | operator +(T left, T right) | DSL mạnh mẽ |
| Property validation | Khai báo backing field | Từ khóa field | Giảm boilerplate |
| Null-safe assignment | if (x != null) x.P = v; | x?.P = v; | Code ngắn gọn |
| Span conversion | arr.AsSpan(0, n) | arr[..n] implicit | Hiệu năng + gọn |
| Lambda modifiers | Phải khai báo full type | Implicit type + modifier | Ít ceremony |
| Partial constructor | Không thể | partial constructor | Source gen friendly |
| Compound assignment | Luôn tạo bản copy | In-place += | Zero-copy mutation |
graph TB
subgraph "C# 14 Feature Categories"
A["Biểu đạt
(Expressiveness)"]
B["Hiệu năng
(Performance)"]
C["Công cụ
(Tooling)"]
end
A --> A1["Extension Members"]
A --> A2["Null-Conditional
Assignment"]
A --> A3["field Keyword"]
B --> B1["Implicit Span
Conversions"]
B --> B2["Compound
Assignment"]
C --> C1["Partial Events
& Constructors"]
C --> C2["Lambda
Modifiers"]
C --> C3["nameof
Unbound Generic"]
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#2c3e50,stroke:#fff,color:#fff
style C fill:#4CAF50,stroke:#fff,color:#fff
style A1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style A2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style A3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B1 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style B2 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style C1 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style C2 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style C3 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
Thực hành: Refactor một Service thực tế
Hãy xem C# 14 thay đổi code production như thế nào qua một ví dụ OrderService:
// === TRƯỚC C# 14 ===
public static class OrderExtensions
{
public static decimal GetTotal(this Order order)
=> order.Items.Sum(i => i.Price * i.Quantity);
public static bool HasDiscount(this Order order)
=> order.DiscountCode is not null;
}
public class OrderService
{
private string _status = "pending";
public string Status
{
get => _status;
set => _status = value ?? throw new ArgumentNullException(nameof(value));
}
public void ApplyDiscount(Order? order, decimal amount)
{
if (order is not null)
{
order.Discount += amount;
}
}
}
// === SAU C# 14 ===
public static class OrderExtensions
{
extension(Order order)
{
// Extension property thay vì method
public decimal Total => order.Items.Sum(i => i.Price * i.Quantity);
public bool HasDiscount => order.DiscountCode is not null;
}
}
public class OrderService
{
// field keyword — không cần backing field
public string Status
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
public void ApplyDiscount(Order? order, decimal amount)
{
// Null-conditional assignment
order?.Discount += amount;
}
}
Lộ trình áp dụng
<LangVersion>14</LangVersion> hoặc latest trong .csproj.field keyword — ít rủi ro nhất, áp dụng ngay cho mọi property có validate logic. Dùng Roslyn analyzer để tìm backing field thừa.if (x != null) x.P = v; và thay bằng x?.P = v;. IDE sẽ suggest tự động.Tài liệu tham khảo
Tối ưu SQL Server cho .NET Developer: Execution Plans, Index Strategy và Query Store
Rate Limiting — Kiểm Soát Lưu Lượng API Trong Hệ Thống Phân Tán
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.