Bài 6.10. C# 12 có gì mới?
Nội dung bài học
- Giới thiệu
- Primary constructor
- Collection expression
- Tham số mặc định trong biểu thức lambda
- Bí danh của kiểu bất kỳ
- Mảng inline
- Thuộc tính thử nghiệm
Giới thiệu
- C# 12 được công bố cùng với .NET 8 vào tháng 11 năm 2023. Ta có thể sử dụng phiên bản này bằng cách tải phiên bản Visual Studio mới nhất hoặc Visual Studio Code C# Dev Kit.
- Các tính năng mới thú vị được giới thiệu nhằm cải thiện hiệu quả lập trình và đơn giản hóa code.
- Sau đây là một số các tính năng chính đó:
- Phương thức khởi tạo chính.
- Biểu thức tập hợp.
- Mảng trên 1 dòng.
- Tham số mặc định trong biểu thức lambda.
- Tham số ref readonly
- Bí danh của kiểu bất kỳ
- Thuộc tính thử nghiệm
- Interceptor: một tính năng preview cho phép ta chặn các lời gọi tới phương thức.
- Các phần tiếp theo ta sẽ tìm hiểu một số tính năng chính đã được giới thiệu và liệt kê ở trên nhé.
Primary constructor
- Phương thức khởi tạo chính(primary constructor) là kĩ thuật cho phép ta khai báo tham số của constructor trực tiếp ở trong khai báo của lớp hoặc struct.
- Trước C# 12 thì phương thức khởi tạo chính chỉ có hiệu lực với kiểu record. Từ C# 12 ta có thể sử dụng phương thức khởi tạo chính cho tất cả các loại class và struct.
- Phạm vi tham số của phương thức khởi tạo chính là trong toàn bộ phần thân của một class. Nhằm đảm bảo chắc chắn rằng tất cả các tham số của primary constructor được gán, tất cả các constructor được khai báo tường minh đều phải gọi tới constructor chính qua cú pháp this(params). Trong đó params là các đối số tương ứng vói tham số của primary constructor.
- Bổ sung primary constructor vào một lớp sẽ ngăn cấm trình biên dịch khai báo một constructor ngầm định không tham số.
- Trong struct, constructor ngầm định không tham số có nhiệm vụ khởi tạo giá trị cho tất cả các trường dữ liệu bao gồm cả tham số của phương thức khởi tạo chính.
- Sau đây là một số quy tắc phân biệt tham số của primary constructor:
- Tham số của primary constructor có thể không được lưu lại nếu chúng không cần thiết.
- Các tham số này không phải là một thành phần của class. Do đó giả định ta có tham số x của primary constructor thì ta không thể truy cập qua cú pháp this.x.
- Các tham số có thể được gán giá trị.
- Các tham số không thể trở thành thuộc tính nếu nó không nằm trong kiểu record.
- Khi nào thì sử dụng primary constructor?
- Dùng làm đối số cho lời gọi tới constructor của lớp cha qua base().
- Để khởi tạo các trường hoặc thuộc tính của lớp.
- Dùng để tham chiếu tới tham số của constructor trong một thành phần khác static của lớp.
- Sau đây là hình ảnh minh họa của primary constructor:
internal class Circle(double r) // hàm khởi tạo chính
{
public double Perimeter { get; } = 2 * Math.PI * r;
public double Area { get; } = Math.PI * r * r;
}
- Đoạn code trên sử dụng tham số trong hàm khởi tạo chính để tính toán giá trị cho chu vi và diện tích của đường tròn bán kính r.
- Đoạn code trên có thể viết lại theo cách truyền thống như sau:
internal class Circle
{
public double Perimeter { get; }
public double Area { get; }
public Circle(double r) // constructor 1 tham số
{
Perimeter = 2 * Math.PI * r;
Area = Math.PI * r * r;
}
}
- Như vậy ta thấy rằng primary constructor giúp code của ta ngắn gọn và dễ dàng khởi tạo giá trị cho các trường, thuộc tính trong một lớp.
- Bây giờ, giả định rằng ta muốn co kéo để phóng to hoặc thu nhỏ đường tròn, ta có thể thay đổi giá trị của tham số trong primary constructor như sau:
namespace CSharp12
{
internal class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Circle circle = new Circle();
Console.WriteLine("==> Dữ liệu gốc của đường tròn: ");
ShowInfo(circle);
Console.WriteLine("==> Sau khi resize: ");
circle.Resize(5);
ShowInfo(circle);
}
static void ShowInfo(Circle circle)
{
// hiển thị kết quả, làm tròn đến hai chữ số phần thập phân
Console.WriteLine($"Bán kính đường tròn: {circle.Radius:N2}");
Console.WriteLine($"Chu vi đường tròn: {circle.Perimeter:N2}");
Console.WriteLine($"Diện tích đường tròn: {circle.Area:N2}");
}
}
internal class Circle(double r) // hàm khởi tạo chính
{
public double Radius { get; set; }
public double Perimeter => 2 * Math.PI * r;
public double Area => Math.PI * r * r;
public Circle() : this(1)
{
Radius = 1;
}
public void Resize(double deltaR) // thay đổi kích thước đường tròn
{
r += deltaR;
Radius = r;
}
}
}
==> Dữ liệu gốc của đường tròn:
Bán kính đường tròn: 1.00
Chu vi đường tròn: 6.28
Diện tích đường tròn: 3.14
==> Sau khi resize:
Bán kính đường tròn: 6.00
Chu vi đường tròn: 37.70
Diện tích đường tròn: 113.10
- Như vậy, khi giá trị tham số của primary constructor thay đổi, các trường, thuộc tính đi kèm có sử dụng tới tham số của primary constructor cũng sẽ được tính toán lại. Kết quả được phản ánh trong ví dụ trên.
Collection expression
- Biểu thức tập hợp giới thiệu một cú pháp ngắn gọn để tạo các giá trị phổ biến của tập hợp. Nếu muốn nhúng các tập hợp khác vào các giá trị này, ta sử dụng phần tử ..e.
- Ví dụ sau minh họa việc tạo lập các tập hợp với collection expression:
// tạo mảng int:
int[] a = [1, 2, 3, 4, 5, 6, 7, 8];
// tạo một list các string:
List<string> b = ["one", "two", "three", "four", "five"];
// tạo một span:
Span<char> c = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'];
// tạo mảng zigzag 2D:
int[][] twoD = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
// tạo mảng zigzag 2D từ các biến:
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[][] twoDFromVariables = [row0, row1, row2];
// ví dụ tạo mảng 1d mới từ các mảng 1d:
int[] single = [.. row0, .. row1, .. row2];
// ta có mảng single với các phần tử: 1, 2, 3, 4, 5, 6, 7, 8, 9
- Ta có thể sử dụng biểu thức tập hợp tại bất cứ đâu ta cần một tập hợp của các phần tử. Có thể sử dụng biểu thức dạng này để khởi tạo giá trị cho tập hợp cũng như truyền nó vào làm đối số của lời gọi phương thức có nhận vào tham số kiểu tập hợp.
- Thêm một ví dụ khác truyền biểu thức tập hợp vào lời gọi phương thức:
static void Main(string[] args)
{
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];
// truyền biểu thức tập hợp vào lời gọi method
ShowElements([.. single]);
}
static void ShowElements(int[] arr)
{
foreach(var element in arr)
{
Console.Write($"{element}, ");
}
Console.WriteLine();
}
// kết quả:
1, 2, 3, 4, 5, 6, 7, 8, 9,
Tham số mặc định trong biểu thức lambda
- Bạn giờ đây có thể định nghĩa các giá trị mặc định cho tham số trong biểu thức lambda. Cú pháp và quy tắc giống hệt như quy tắc cho tham số mặc định của phương thức.
- Ví dụ:
static void Main(string[] args)
{
// sử dụng tham số mặc định cho biểu thức lambda
var IncrementBy = (int source, int increment = 1) => source + increment;
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];
Console.WriteLine(IncrementBy(100)); // 101
Console.WriteLine(IncrementBy(199)); // 200
Console.WriteLine(IncrementBy(199, 5)); // 204
}
Bí danh của kiểu bất kỳ
- Bạn có thể sử dụng bí danh cho kiểu bất kỳ với chỉ thị using.
- Ví dụ sau đây minh họa đặt bí danh cho kiểu Console và int[]:
using X = System.Console;
using intArray = int[];
namespace CSharp12
{
internal class Program
{
static void Main(string[] args)
{
// sử dụng tham số mặc định cho biểu thức lambda
var IncrementBy = (int source, int increment = 1) => source + increment;
intArray row0 = [1, 2, 3];
intArray row1 = [4, 5, 6];
intArray row2 = [7, 8, 9];
intArray single = [.. row0, .. row1, .. row2];
X.WriteLine(IncrementBy(100)); // 101
X.WriteLine(IncrementBy(199)); // 200
X.WriteLine(IncrementBy(199, 5)); // 204
}
}
}
101
200
204
Mảng inline
- Mảng inline được sử dụng bởi team runtime và các tác giả thư viện mã nguồn nhằm tăng hiệu năng trong ứng dụng của ta.
- Mảng inline cho phép lập trình viên tạo một mảng với kích thước cố định trong kiểu struct.
- Ví dụ sau sẽ minh họa về mảng inline:
namespace CSharp12
{
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
private int _element;
}
internal class Program
{
static void Main(string[] args)
{
// sử dụng mảng inline như mảng thông thường:
var buffer = new Buffer();
for (int i = 0; i < 10; i++)
{
buffer[i] = i * 100;
}
foreach(var item in buffer)
{
Console.WriteLine(item);
}
}
}
}
0
100
200
300
400
500
600
700
800
900
Thuộc tính thử nghiệm
- Phần này liên quan đến tạo mã cho thư viện. Với trình độ hiện tại bạn chỉ cần biết đơn giản rằng các kiểu, phương thức, mã máy(assembly) có thể được gắn nhãn System.Diagnostics.CodeAnalysis.ExperimentalAttribute để chỉ ra rằng đây là một tính năng đang trong quá trình thử nghiệm.
- Trình biên dịch sẽ hiện cảnh báo nếu ta truy cập vào một thành phần gắn nhãn ExperimentalAttribute. Tất cả các kiểu trong file mã máy gắn nhãn thuộc tính Experimental đều là các thành phần thử nghiệm.
