Các tính năng nâng cao của C#
- 13-12-2023
- Toanngo92
- 0 Comments
Buổi này sẽ cung cấp cái nhìn sâu hơn vào những tính năng bổ sung của C#. Nó mô tả về việc trả về tham chiếu (ref returns), các biến địa phương tham chiếu (ref locals), cải tiến trong biến của chúng ta, bộ tuples, và phương thức Main() bất đồng bộ. Nó cũng giải thích về những cải tiến được thực hiện trong câu lệnh throw và giới thiệu thêm về thành viên dạng biểu thức. Buổi học này còn khám phá một số cải tiến mới trong C# 9.0.
Trong buổi học này, bạn sẽ học được:
- Mô tả về việc trả về tham chiếu và các biến địa phương tham chiếu (ref returns and ref locals).
- Giải thích về cải tiến trong biến out, tuples, phương thức Mai n() bất đồng bộ và các biểu thức throw.
- Mô tả về thành viên dạng biểu thức mới.
- Tổng quan về các cải tiến trong khớp mẫu (Pattern matching).
- Liệt kê các tính năng hoàn thiện trong C# 9.0.
- Nhận diện các cải tiến về hiệu suất và tương tác trong C# 9.0.
- Giải thích về việc hỗ trợ cho các bộ tạo mã code.
Mục lục
Các tính năng bổ sung của C#
Trong những phiên bản gần đây của C# từ phiên bản 7.0 trở đi, đã được giới thiệu nhiều tính năng mới rất hữu ích cho các nhà phát triển C#. Những tính năng này bao gồm các literal nhị phân, ngăn cách số, khớp mẫu, async main, tên phần tử tuple được suy luận, và nhiều tính năng khác nữa. Những tính năng này tập trung vào việc tăng hiệu suất, đơn giản hóa mã nguồn C#, và xử lý dữ liệu một cách linh hoạt hơn. Qua những tính năng này, những thay đổi lớn nhất đã được thực hiện đối với tuples và khớp mẫu. Trong khi những thay đổi đối với tuples đã làm cho việc có “nhiều kết quả” trở nên đơn giản hơn, thì những thay đổi đối với khớp mẫu đã giản lược mã nguồn phụ thuộc vào dữ liệu. Tất cả các tính năng nhỏ và lớn của các phiên bản gần đây của C# đều hợp tác với nhau để đảm bảo mã nguồn rõ ràng, hiệu quả và sản xuất hơn.
Nhiều cách chọn phiên bản ngôn ngữ hơn
Một trong những tính năng linh hoạt nhất của C# là khả năng làm việc với trình biên dịch theo phiên bản ngôn ngữ ưa thích. Một nhà phát triển có thể cấu hình trình biên dịch để hoạt động theo phiên bản ngôn ngữ dự định. Điều này tách biệt việc nâng cấp một công cụ với việc nâng cấp một phiên bản ngôn ngữ. Nói cách khác, người dùng cần chỉnh sửa cài đặt cấu hình dự án để chỉ định phiên bản ngôn ngữ mong muốn.
Lưu ý: Tính năng này có sẵn trong C# 7.2, Visual Studio 2017 trở đi và không có trong các phiên bản cũ hơn.
Hình bên dưới hiển thị cửa sổ các thuộc tính Dự Án.
Có ba cách khác ngoài cửa sổ thuộc tính dự án để chỉ định phiên bản ngôn ngữ sẽ được sử dụng, như sau:
- Sử dụng biểu tượng bóng đèn (light bulb icon).
- Sửa đổi tập tin .csproj
- Tạo tập tin .props
Trong Visual Studio 2017 trở đi, việc sử dụng biểu tượng bóng đèn có lẽ là cách đơn giản nhất để kích hoạt C# và sử dụng bất kỳ tính năng mới nào của nó. Môi trường phát triển tích hợp (IDE) tự động phát hiện người dùng muốn sử dụng phiên bản ngôn ngữ mới. Tương ứng, nó sau đó yêu cầu cập nhật dự án. Để sử dụng biểu tượng bóng đèn, người dùng có thể thử đặt một literal mặc định trong mã nguồn. Điều này sẽ kích hoạt biểu tượng bóng đèn để sửa mã, như được hiển thị trong hình bên dưới. Bằng cách nhấp vào biểu tượng này, người dùng có thể kích hoạt phiên bản mong muốn cho C#. Biểu tượng cũng cho phép nâng cấp tất cả các dự án C#. Hãy xem xét phương pháp này nếu có nhiều dự án cần nâng cấp, vì đây là cách tốt nhất để nâng cấp tất cả một lúc.
Sửa đổi tập tin .csproj
Các nhà phát triển có thể cập nhật tập tin .csproj
của dự án để thay đổi phiên bản ngôn ngữ một cách thủ công. Để làm điều này, họ phải thiết lập phần tử </LangVersion>
trong phần tử <PropertyGroup>
đầu tiên của tập tin. Đoạn mã bên dưới cho thấy tập tin đã được sửa đổi, các dòng in đậm chỉ ra những dòng được thêm vào.
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>ConsoleApp1</RootNamespace>
<AssemblyName>ConsoleApp1</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Người dùng cũng có thể chỉ rõ phiên bản như <LangVersion>7.1</LangVersion>
. Việc chỉ định phiên bản ngôn ngữ mong muốn trong tập tin .csproj
có nghĩa là xác định nó ở mức dự án. Điều này có nghĩa là mỗi dự án trong Visual Studio có thể sử dụng một phiên bản khác nhau của ngôn ngữ C# bằng cách sử dụng tập tin này. Nếu có nhiều thẻ PropertyGroup
trong tập tin .csproj
mỗi thẻ chỉ ra một cấu hình xây dựng khác nhau như release và debug, việc thiết lập thẻ LangVersion
bên trong mỗi thẻ là bắt buộc.
Trả về ref, ref Locals và Biến cải tiến out
Trong C#, một phương thức có thể chứa một tham số mà một nhà phát triển có thể truyền theo tham chiếu bằng cách sử dụng từ khóa ref
. Việc chỉ định từ khóa này là bắt buộc trong phương thức gọi cũng như trong định nghĩa của phương thức được gọi.
Hãy giả sử rằng một phương thức có ba tham số, trong đó tham số cuối cùng được truyền theo tham chiếu. Trong các phiên bản trước đây của C#, điều này được thực hiện bằng từ khóa
. Phương thức ref
Main()
là người gọi của phương thức này. Tuy nhiên, trước khi gọi phương thức này, phương thức Main()
khởi tạo tham số thứ ba. Do đó, việc khởi tạo một biến
trước khi xác định nó là một tham số là bắt buộc.ref
Trả về ref
Trong C# 7.2, từ khóa ref
đi kèm với một số khả năng nâng cao. Một nhà phát triển có thể sử dụng nó để trả về giá trị thông qua tham chiếu. Từ khóa này cũng có khả năng lưu trữ giá trị được truyền theo tham chiếu trong biến cục bộ, như được minh họa trong một ví dụ đơn giản trong đoạn mã bên dưới. Sự thay đổi này đối với từ khóa
làm cho việc quản lý các tình huống khi cần trả về tham chiếu để thực hiện thay thế nội dung một cách nhanh chóng.ref
class Program {
static void Main(string[] args) {
string[] writers = { "Emily George", "Lee Mein", "John Wash", "Sicily Wang" };
ref string writer2 = ref new Program().FindWriter(1,writers);
Console.WriteLine("Original writer: {0}", writer2);
Console.WriteLine();
writer2 = "Johan Muller";
Console.WriteLine("Replaced writer: {0}", writers[1]);
Console.ReadKey();
}
public ref string FindWriter(int num, string[] names) {
if (names.Length > 0)
return ref names[num];
throw new IndexOutOfRangeException($"{nameof (num)}
unavailable.");
}
}
Trong đoạn mã trên, phương thức FindWriter()
trả về một giá trị chuỗi sử dụng từ khóa ref
. Khi được gọi trong Main()
, từ khóa ref
lại được sử dụng để lưu trữ giá trị trong writer2
. Như vậy, nhà phát triển có thể sử dụng các tham chiếu sau này. Cải tiến này đã làm cho việc xử lý các yêu cầu mà tham chiếu cần được trả về theo một thứ tự cụ thể để thay thế một hoặc nhiều giá trị trở nên dễ dàng hơn.
Ref Locals
Cũng có thể sử dụng từ khóa ref
để định nghĩa những gì được gọi là ref locals. Trong định nghĩa này, từ khóa được chỉ định với một biến cục bộ. Loại mới này là một tính năng hữu ích của C#, cho phép lưu trữ các tham chiếu. Thông thường, nó được sử dụng với ref returns để có thể lưu trữ tham chiếu trong một biến cục bộ, như được minh họa trong đoạn mã bên dưới. Điều này giúp viết mã nguồn hiệu quả và hiệu suất hơn vì nó loại bỏ việc di chuyển các bản sao của giá trị giữa các phương thức.
using System;
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Input a string:");
char[] cseq = Console.ReadLine().ToCharArray();
Console.WriteLine($"Prior to replacing: {new string(cseq)}");
ref char cref = ref RetRefLocal.SeekCharRef('p', cseq);
cref = 'p';
Console.WriteLine($"Post replacing: {new string(cseq)}");
Console.ReadLine();
}
}
class RetRefLocal
{
public static ref char SeekCharRef(char val, char[] cSeq)
{
for (int k = 0; k < cSeq.Length; k++)
{
if (cSeq[k] == val)
{
return ref cSeq[k];
}
}
throw new IndexOutOfRangeException(val + " not found");
}
}
Trong đoạn mã trên, ref
được chỉ định ngay trước kiểu char
trong phương thức SeekCharRef()
. Điều này chỉ ra rằng phương thức sẽ trả về tham chiếu của biến. Khi được gọi trong phương thức Main()
, tham chiếu được trả về được lưu trữ trong cre
f, một biến cục bộ thuộc loại ref local. Trong kết quả của đoạn mã này, ký tự đầu tiên sẽ được thay thế bởi p
.
Các tính năng ref returns và ref locals hữu ích để thay thế các vị trí đặt trước hoặc đọc từ các cấu trúc dữ liệu lớn. Ví dụ, một ứng dụng có thể lưu trữ dữ liệu của nó trong một mảng các struct. Thông thường, điều này được thực hiện để loại bỏ các tạm dừng không mong muốn trong quá trình thu gom. Để đọc hoặc thay đổi dữ liệu đã lưu trữ, các phương thức liên quan có thể trả về một tham chiếu đến mảng này.
Mặc dù cả ref locals và ref returns đều là các tính năng hữu ích, nhưng có hai hạn chế được áp đặt. Thứ nhất, chỉ có thể trả về các tham chiếu an toàn, tức là những tham chiếu trỏ đến các trường của đối tượng và những tham chiếu được chuyển tới ứng dụng. Thứ hai, không thể sửa đổi ref locals để trỏ đến không gian lưu trữ khác, vì chúng được khởi tạo và giới hạn trong một vị trí bộ nhớ cố định.
Cải thiện biến out và huỷ bỏ
Từ khóa out
, giống như ref
, hữu ích để truyền một tham số theo tham chiếu. Tuy nhiên, trong bất kỳ phiên bản nào của C# trước phiên bản 7.0, việc khai báo biến trước khi chỉ định nó là một tham số out
là bắt buộc. Tương tự như các tham số ref
, việc sử dụng tham số out
liên quan đến việc chỉ định từ khóa out
trong phương thức gọi cũng như trong định nghĩa của phương thức được gọi. Mặc dù việc khai báo tham số out
được coi là bắt buộc, nhưng việc thực hiện có thể làm phức tạp. Điều này bởi vì một nhà phát triển phải chỉ ra loại đầy đủ như int length
hoặc string authorName
thay vì sử dụng từ khóa var
. Việc khai báo loại của các tham số này trong phương thức chính nó không thể thực hiện được vì sẽ gây lỗi biên dịch.
Tuy nhiên, trong C# 7.0 và các phiên bản sau đó, nhà phát triển có thể chỉ định kiểu dữ liệu của tất cả các tham số out
ngay tại chỗ thay vì khai báo chúng trước khi truyền làm tham số. Một ví dụ cơ bản về điều này được thể hiện trong đoạn mã bên dưới.
class BookApplication {
static void Main(string[] args) {
BookByOutArg(out string bName, out string bAuthor);
Console.WriteLine("Book: {0}, Author: {1}", bName, bAuthor);
Console.ReadKey();
}
static void BookByOutArg(out string name, out string author) {
name = "Harry Potter Part I";
author = "J. K. Rowling";
}
}
Trong đoạn mã trên, kiểu của mỗi biến được chỉ định như một tham số out
ngay sau từ khóa out
. Các tham số out
được giới hạn trong phạm vi của khối mà chúng được bao quanh. Do đó, dòng tiếp theo có thể tham chiếu đến chúng.
Vì các biến có từ khóa out
được khai báo trực tiếp là các tham số out
, trình biên dịch có thể xác định loại dữ liệu của chúng. Tuy nhiên, không được phép có bất kỳ phiên bản nạp chồng nào xung đột.
Điều này cho phép nhà phát triển khai báo chúng bằng từ khóa var
. Vì vậy, việc khai báo một tham số out
có thể là:
out var bookName;
Các tham số out
thường hữu ích trong khối try
mà trong đó giá trị trả về là true
và các tham số out
chứa kết quả thu được.
C# cho phép nhà phát triển loại bỏ giá trị trả về nếu không cần thiết. Ví dụ, nếu một nhà phát triển đã khai báo một biến cục bộ nhưng không sử dụng nó ở bất kỳ đâu, việc loại bỏ nó là hợp lý. Logic là, biến này không nên có tên hoặc giá trị. Điều này chính xác là chức năng của discard.
Thay vì sử dụng tên của biến, nhà phát triển chỉ cần chỉ định rằng biến này bị loại bỏ. Để làm điều này, nhà phát triển cần sử dụng ký hiệu gạch dưới _
, như được thể hiện trong đoạn mã bên dưới. Hãy đảm bảo có một dấu cách trước ký hiệu gạch dưới.
Một tham số discard là một tham số chỉ ghi, có nghĩa là không thể đọc hoặc lấy giá trị của nó. Nhà phát triển có thể loại bỏ bất kỳ số lượng biến nào, bao gồm cả các tham số out
.
Console.WriteLine("Specify a number:");
if int.TryParse(Console.ReadLine(), out int _)) {
Console.WriteLine("Given data is a number");
Console.ReadKey();
}
Trong đoạn mã này, khi người dùng chỉ định một giá trị số trong đầu vào, chương trình xử lý nó và hiển thị “Given data is a number.” Khi người dùng chỉ định một giá trị không phải số, nó được bỏ qua.
Cải Thiện Tuples
Trong nhiều tình huống, trở nên rõ ràng để trả về nhiều giá trị từ một phương thức duy nhất. Trong các phiên bản trước của C#, có một số cách để làm điều này như sử dụng các tham số out
, trả về một mảng danh sách và trả về một kiểu System.Tuple<T1, T2,...>
.
C# 7.1 đã thêm một cải tiến nhỏ cho tuples, được gọi là suy luận tên tuple. Tính năng mới này cho phép làm việc với tuples như là các kiểu giá trị. Nó cũng cho phép tuples suy ra tên của các phần tử từ các đầu vào. Ví dụ, một nhà phát triển có thể viết (comp.name, emp.age)
thay vì (name: comp.name, age: emp.age)
. Trình biên dịch có thể dễ dàng suy ra các tên này nên không cần phải đặt chúng một cách rõ ràng. Đoạn mã bên dưới định nghĩa hai phần tử của một tuple mà không có suy luận tên. Để đoạn mã này hoạt động, cần phải nhập một gói NuGet có tên System.ValueTuple
. Để chọn và cài đặt gói này, nhấp chuột phải vào References và chọn Manage NuGet Packages.
using System;
class Program
{
static void Main()
{
string ename = "Emy George";
int e_age = 30;
var empTuple = (ename, e_age);
Console.WriteLine(empTuple.Item1); // Emy George
Console.WriteLine(empTuple.Item2); // 30
Console.ReadKey();
}
}
Trong C#, có thể chỉ định tên các phần tử trong tuple một cách thủ công, như được thể hiện trong đoạn mã bên dưới.
string ename = "Eng George";
int e_age = 30;
var empTuple = (ename: ename, e_age: e_age);
Console.WriteLine(empTuple.ename); // Emy George
Console.WriteLine(empTuple.e_age); // 30
Trong đoạn mã này, phần tử được suy ra bằng một tên có ý nghĩa. Việc truy cập các phần tử riêng lẻ không cần phải dùng Item1, Item2, và cũng không cần sử dụng các chỉ số.
Từ phiên bản C# 7.1 trở đi, trình biên dịch có khả năng suy luận tên các phần tử trong tuple từ các biến cục bộ, thành viên có điều kiện đầy đủ và các thành viên khác như các thuộc tính. Đoạn mã bên dưới cho thấy cùng một mã của đoạn mã trên với tên các phần tử được suy luận.
string ename = "Eny George";
int e_age = 30;
var empTuple = (ename, e_age);
Console.WriteLine(empTuple.ename); // Emy George
Console.WriteLine(empTuple.e_age); // 30
Đoạn mã tiếp theo thể hiện tên các phần tử được suy luận thông qua một thuộc tính.
class Employee {
string ename;
int e_age;
static void Main() {
Employee emp = new Employee { ename = "Emy George", e_age = 30 };
var empTuple = (emp.ename, emp.e_age);
Console.WriteLine(empTuple.ename); // Hiển thị Emy George
Console.WriteLine(empTuple.e_age); // Hiển thị 30
Console.ReadLine();
}
}
Đoạn mã bên dưới thể hiện tên các phần tử được suy luận thông qua một thuộc tính có điều kiện null.
class Employee {
string ename;
int eage;
static void Main() {
Employee emp = null;
var empTuple = (emp?.ename, emp?.eage);
Console.WriteLine(empTuple.ename);
Console.WriteLine(empTuple.eage);
}
}
Các phần tử của một tuple là các trường công cộng và có thể thay đổi, trong khi các tuple là các kiểu giá trị. Hai tuple sẽ bằng nhau nếu các phần tử của chúng giống nhau theo cặp. Ngay cả mã hash code của hai tuple này và các phần tử của chúng cũng sẽ giống nhau. Do đó, các tuple hữu ích không chỉ để trả về nhiều giá trị mà còn thực hiện một số yêu cầu khác. Ví dụ, một nhà phát triển có thể sử dụng một tuple như một khóa nếu cần một từ điển với nhiều khóa. Tương tự, nếu yêu cầu là một danh sách với nhiều giá trị ở mỗi vị trí, việc định nghĩa một tuple ở mỗi vị trí là một lựa chọn lý tưởng.
Cải Thiện Hàm Asynchronized Main()
Mặc dù hàm Main()
không đồng bộ (asynchronous Main()) được dự định cho C# 7.0, nhưng nó chỉ có sẵn từ phiên bản C# 7.1 trở đi. Tính năng này làm cho việc sử dụng các phương thức không đồng bộ trở nên đơn giản hơn với sự trợ giúp của từ khóa async
và await
. Trước phiên bản này của C#, phương thức Main()
, là điểm vào của quá trình thực thi của chương trình, có các chữ ký sau đây:
public static void Main()
public static void Main(string[] args)
public static int Main(string[] args)
Trong các phiên bản C# cũ trước 7.1, chỉ có thể chờ đợi (await) một phương thức không đồng bộ nếu nó được gọi từ bên trong một phương thức không đồng bộ khác. Ví dụ, một ứng dụng console C# 7.0 không thể đánh dấu Main()
làm async
. Do đó, để sử dụng mã không đồng bộ, tùy chọn duy nhất là thêm một số mã để có thể hoạt động, như được thể hiện trong hình bên dưới.
Từ phiên bản C# 7.1 trở đi, có thể chờ đợi (await) các phương thức không đồng bộ bên trong phương thức Main()
trực tiếp. Bây giờ trách nhiệm của trình biên dịch là tạo ra mã bắt buộc để chương trình chạy đúng cách từ điểm vào của chương trình. Dưới đây là các chữ ký bổ sung mà phương thức Main()
hiện đã hỗ trợ cho mã không đồng bộ:
public static Task Main()
public static Task Main(string[] args)
public static Task<int> Main()
public static Task<int> Main(string[] args)
Có thể thấy rằng phương thức có hai loại trả về bổ sung, đó là Task
và Task<int>
. Lớp Task
đại diện cho một hành động chạy không đồng bộ và không có giá trị trả về. Việc kiểm tra trạng thái của một nhiệm vụ có thể thực hiện dễ dàng thông qua các thuộc tính Status
, IsCompleted
, IsCanceled
, và IsFaulted
của nó.
Khác với các phiên bản trước đó, từ C# 7.1 trở đi, hỗ trợ các kiểu tổng quát làm kiểu trả về của các phương thức không đồng bộ. Bây giờ nó hỗ trợ cả kiểu cấu trúc Task<T>
, giúp tránh việc cấp phát một đối tượng Task<T>
khi kết quả đã tồn tại trong chế độ await. Điều này có thể giảm đáng kể số lần cấp phát trong các kịch bản không đồng bộ để xử lý việc đệm. Nói một cách đơn giản, hiện nay có thể chỉ định phương thức Main()
của ứng dụng C# 7.1 là async, như được thể hiện trong hình bên dưới. Tính năng này giúp đơn giản hóa mã và loại bỏ mã boilerplate bổ sung.
Đoạn mã bên dưới thể hiện một đoạn mã khác sử dụng từ khóa await để viết mã một cách rõ ràng.
class TaskTest {
static async Task Main(string[] args) {
Console.WriteLine($"It is {System.DateTime.Now}");
await Task.Delay(3000);
Console.WriteLine($"It is {System.DateTime.Now}");
Console.ReadKey();
}
}
Trong đoạn mã trên, Task.Delay
trì hoãn việc thực thi của Console.WriteLine()
thứ hai trong suốt ba giây. Mã được viết gọn gàng, vì không cần phải gọi GetAwaiter().GetResult()
.
Đoạn mã bên dưới thể hiện việc sử dụng Task<int>
thông qua việc gọi một phương thức không đồng bộ trong Main()
.
class TaskTest
{
static async Task Main(string[] args)
{
var no = 6;
Console.WriteLine($"Factorial of {no}: {await AaFact(no)}");
Console.ReadKey();
return 0;
}
private static Task<int> AsFact(int num)
{
return Task.Run(() => Compute(num));
//Local function
int Compute(int p)
{
if (p == 1){
return 1;
}
else {
return p * Compute(p - 1);
}
}
}
}
Trong đoạn mã trên, một hàm cục bộ được định nghĩa để tính giai thừa của số 6. Kết quả là 720.
Cải thiện Biểu thức throw
Trong C#, câu lệnh throw
thường được sử dụng để bắt các ngoại lệ hoặc thực hiện một hành động nào đó giữa quá trình thực thi. Một câu lệnh throw
là một câu lệnh riêng biệt, điều này có nghĩa là không thể kết hợp nó với một câu lệnh hoặc biểu thức khác như đoạn mã bên dưới
if (num1 == null) {
throw new ArgumentNullException(nameof (num1));
}
Tuy nhiên, từ phiên bản C# 7.0 trở lên, có thể sử dụng câu lệnh throw
trong một biểu thức, tạo thành một biểu thức throw
. Nhà phát triển có thể sử dụng nó trong giữa một biểu thức như toán tử ba ngôi. Bây giờ có thể thêm ngoại lệ throw trong các biểu thức null-coalescing và điều kiện, cũng như trong các thành viên có thân hàm biểu thức, điều này giúp đơn giản hóa cú pháp tổng thể và giảm mã lệnh.
Với biểu thức throw
, một nhà phát triển có thể viết mã trong đoạn mã bên dưới một cách ngắn gọn hơn. Đoạn mã tiếp theo đây thể hiện điều này bằng cách sử dụng toán tử null-coalescing và gán.
myNum = num1 ?? throw new ArgumentNullException(nameof (num1));
Vì có thể sử dụng câu lệnh throw
trong biểu thức, nhà phát triển có thể sử dụng toán tử ?: để thay thế cho câu lệnh if...else
, như được thể hiện trong đoạn mã bên dưới đây.
return val < 10 ? val : throw new
ArgumentOutOfRangeException("Value has to be less than 10");
Thêm các thành viên Expression-bodied
C# 6.0 giới thiệu các thành viên có thân hàm biểu thức, thường là các phương thức. Tính năng này giúp đơn giản hóa cú pháp của các phương thức trong C#. Một thành viên có thân hàm biểu thức thay thế một khối mã bằng một câu lệnh đơn để thực thi. Trong một phương thức có thân hàm biểu thức, một nhà phát triển có thể định nghĩa cơ thể của nó dưới dạng biểu thức lambda. Tuy nhiên, cấu trúc này chỉ giới hạn trong phương thức.
Từ C# 7.0 trở đi, nhà phát triển có thể mở rộng tính năng này để bao gồm nhiều thành viên hơn như truy cập thuộc tính, hủy bỏ và các constructor.
Đoạn mã bên dưới cho thấy cách sử dụng tính năng này cho các phương thức, thuộc tính, cũng như hàm tạo trong C#.
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Using expression-bodied constructor and destructor:\n{new Demo().prodDetail()}");
Console.ReadLine();
var obj = new Demo();
obj.PriceListAdd(101, 480);
obj.PriceListAdd(102, 500);
obj.prodId = 101;
Console.WriteLine($"Using expression-bodied getters and setters");
Console.WriteLine($"The price of Product {obj.prodId} is:{obj.Price}");
obj.Price = 550;
Console.WriteLine($"The updated price of Product: {obj.prodId} is: {obj.Price}");
Console.ReadLine();
}
}
class Demo
{
public Dictionary<int, double> PriceList = new Dictionary<int, double>();
int prodId { get; set; } = 101;
string prodName { get; } = "Box Nails";
string prodCategory { get; } = "Nails";
double ProdPrice { get; }
public double Price
{
// Expression-bodied Getters and Setters
get => PriceList[prodId];
set => PriceList[prodId] = value;
}
// Expression-bodied constructor
public Demo() => prodPrice = 480;
// Expression-bodied destructor
~Demo() => Console.WriteLine("\nDestructor of Demo class");
public string ProdDetail() =>
$"Product Id: {ProdId}\nName: {ProdName}\nProduct Category: {ProdCategory}\nProduct Price: {ProdPrice}";
}
Trong đoạn mã trên, cú pháp thân hàm biểu thức được áp dụng vào một constructor, một destructor và các truy cập thuộc tính. Trong trường hợp của các truy cập thuộc tính, nhà phát triển có thể thực hiện một truy cập thuộc tính tự động để làm đơn giản hóa mã nguồn hơn. Tuy nhiên, điều này là một lựa chọn tốt hơn khi các thuộc tính yêu cầu một truy cập tự động (getter hoặc setter) rõ ràng. Tuy nhiên, cú pháp thân hàm biểu thức vẫn là một cách viết sạch và giảm số lượng các dòng mã.
Hình bên dưới cho thấy kết quả của đoạn mã trên.
Nhà phát triển cũng có thể sử dụng một phương thức có thân hàm biểu thức để gọi một phương thức bất đồng bộ trong Main()
, như được thể hiện trong đoạn mã bên dưới.
static async Task Main(string[] args) => WriteLine($"Factorial 6:{await AsFace (6)}");
Cải Tiến Matching Patterns
Pattern matching là cách để kiểm tra xem một nhóm cụ thể của ký tự, token hoặc thông tin có tồn tại trong thông tin được cung cấp hay không. Microsoft đầu tiên giới thiệu pattern matching trong C# 7.0 và sau đó thêm vào các mẫu khác. Pattern matching được sử dụng để quyết định xem các bản ghi nguồn của các ngôn ngữ cấp cao có đúng không. Nó cũng được sử dụng để tìm và thay thế một mẫu khớp với mẫu trong văn bản hoặc mã với một văn bản hoặc mã khác. Bất kỳ ứng dụng nào hỗ trợ chức năng tìm kiếm đều sử dụng một mẫu khớp.
Quá trình phát triển của Pattern Matching
Mẫu khớp các mẫu được giới thiệu trong C# 9.0.
Microsoft tiếp tục thêm các mẫu độc đáo vào mỗi bản cập nhật của C#. Tương tự, trong C# 9.0, Microsoft đã thêm các mẫu khớp khác nhau như mẫu theo loại, mẫu logic và mẫu quan hệ.
- Kiểu pattern – Mẫu theo loại được giới thiệu lần đầu trong C# 9.0 và được sử dụng để kiểm tra loại thời gian chạy của một biểu thức.
- Quan hệ – Như tên gọi của nó, nó khớp với mối quan hệ hoặc một biểu thức. Nói cách khác, mẫu quan hệ được sử dụng để so sánh kết quả của một biểu thức với một hằng số. Mã bên dưới hiển thị một ví dụ về mẫu quan hệ.
using System;
class Program
{
static void Main()
{
Console.WriteLine(Classify(50)); // Output: Too high
Console.WriteLine(Classify(double.NaN)); // Output: Unknown
Console.WriteLine(Classify(2.5)); // Output: Acceptable
static string Classify(double measurement) => measurement switch
}
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};
}
Chương trình trong đoạn mã trên mô tả mối quan hệ giữa các đầu ra. Bạn có thể sử dụng bất kỳ toán tử so sánh nào như ‘<‘, ‘>’, hoặc ‘=’ để tìm một phép so sánh.
Biểu thức hằng số có thể là một số nguyên, số thực, ký tự hoặc kiểu enum.
- Mẫu logic – Chúng kiểm tra xem một biểu thức có khớp với sự kết hợp logic của các mẫu hay không. ‘AND, OR và NOT’ là các toán tử logic được sử dụng để khớp. Các toán tử này tạo ra các mẫu logic như mẫu hội, mẫu phân, và phủ định.
- Mẫu hội – Đây là các mẫu ‘AND’. Chúng khớp khi cả hai mẫu đều khớp với biểu thức.
- Mẫu phân – Tương tự, mẫu phân là một mẫu ‘OR’. Chúng khớp khi một trong hai mẫu khớp với biểu thức.
Tính năng điều chỉnh và hoàn thiện
Ngoài các tính năng hiện có như kiểu bản ghi, setter chỉ khởi tạo, các câu lệnh cấp cao và những điều này, C# 9.0 còn bao gồm nhiều tính năng chính khác quan trọng để viết mã hiệu quả. Lưu ý rằng các tính năng mới được thêm vào không chỉ dễ hiểu mà còn rất ngắn gọn.
- Biểu thức new có kiểu mục tiêu
Khi kiểu không xác định, biểu thức new không đòi hỏi việc xác định kiểu cho các hàm tạo. Vì vậy, khi kiểu của đối tượng được tạo đã được biết trước, bạn có thể bỏ qua kiểu trong một biểu thức new.
Ví dụ
private List<List name> _name = new();
- Hàm ẩn danh tĩnh
Hàm hoặc phương thức ẩn danh tĩnh là cải tiến so với các phương thức ẩn danh trong C# 9.0. Khi một từ khóa static được áp dụng cho một lambda hoặc một phương thức ẩn danh, kết quả được biết đến như là một hàm ẩn danh tĩnh. Một hàm như vậy có thể tham chiếu đến các thành viên tĩnh từ phạm vi bên ngoài.
- Biểu thức điều kiện có kiểu mục tiêu
C# 9.0 cũng bao gồm một số cải tiến với biểu thức điều kiện có kiểu mục tiêu, cụ thể là cải tiến cho các câu lệnh ba ngôi. Nhánh của biểu thức ? .. :.. được phép có các loại khác nhau, miễn là cả hai loại chuyển đổi thành loại mục tiêu.
Xem xét một số ví dụ để hiểu rõ hơn. Đoạn mã bên dưới hiển thị mã được viết trong C# 8.0.
public abstract class Publication
{
}
public class Book : Publication
{
}
public class Journal : Publication
{
}
public class Program
{
static void Main(string[] args)
{
var flag = false;
Publication p = flag ? new Book() : new Journal();
Console.WriteLine("It is working!");
}
}
Đoạn mã bên trên xác định một lớp cơ sở có tên Publication
và hai lớp kế thừa Book
và Journal
. Trong Main
, chúng ta cố gắng kiểm tra giá trị của biến flag
và gán một đối tượng là một instance của Book
hoặc Journal
tùy thuộc vào việc flag
là true hoặc false tương ứng. Tuy nhiên, mã sẽ cho lỗi biên dịch trong C# 8.0 hoặc các phiên bản thấp hơn vì cả hai phía của biểu thức phải thuộc cùng một loại. Biểu thức điều kiện có kiểu mục tiêu là một tính năng có thể loại bỏ hạn chế này nhưng tính năng này không có sẵn trong C# 8.0 hoặc các phiên bản thấp hơn. Lỗi được hiển thị bởi trình biên dịch như sau:
Tính năng “biểu thức điều kiện có kiểu mục tiêu” không có sẵn trong C# 8.0
Feature 'target-typed conditional expression' is not available in C# 8.0. Please use language version 9.0 or greater.
Đoạn mã giống như vậy mà không có bất kỳ thay đổi nào khi được thực thi với .NET 5.0 và C# 9.0 hoạt động thành công. Điều này là do C# 9.0 hỗ trợ biểu thức điều kiện có kiểu mục tiêu, trong đó cả hai phía của biểu thức điều kiện có thể thuộc về các loại khác nhau.
- Hỗ trợ cải thiện cho trình tạo mã
Trình tạo mã là một tính năng trong C# đọc các thuộc tính hoặc các yếu tố mã khác bằng cách sử dụng API phân tích Roslyn (tên viết tắt của trình biên dịch .NET). Thông qua dữ liệu đó, nó tạo ra và thêm mã mới vào bộ biên dịch. Trình tạo nguồn không thể sửa đổi bất kỳ mã hiện có nào trong bộ biên dịch, chúng chỉ có thể thêm mã. Để tăng hiệu suất, C# 9.0 đã giới thiệu hai tính năng hỗ trợ mới cho trình tạo mã: cú pháp phương thức một phần và khởi tạo mô-đun.
Trước khi có C# 9.0, phương thức một phần là private và có một số hạn chế, như không cho phép các bộ điều khiển truy cập, giá trị trả về là void
hoặc tham số out
. Điều này có nghĩa là nhà phát triển phải chỉnh sửa mã được tạo ra bởi trình tạo mã không tuân theo những hạn chế này. Năng suất làm việc bị giảm và khả năng phát sinh lỗi tăng lên.
Với việc giới thiệu của C# 9.0, những hạn chế này đối với cú pháp phương thức một phần đã được loại bỏ.
Khởi tạo mô-đun là cải tiến thứ hai mới trong trình tạo mã. Khởi tạo mô-đun là kỹ thuật mà một phương thức được chỉ định như là một mô-đun khởi tạo. Một khởi tạo mô-đun là một phương thức chạy khi một hợp đồng được tải lần đầu tiên. Nó giống như một hàm xây dựng tĩnh trong C#, nhưng thay vì áp dụng cho một lớp, nó áp dụng cho toàn bộ hợp đồng.
Khởi tạo mô-đun có một số quy ước áp dụng, cụ thể là chúng phải là static, không có tham số và nên trả về void
. Ngoài ra, chúng không được phép là phương thức generic hoặc chứa trong một lớp generic.
Hiệu suất và Tương tác với Mã Nguyên Thủy (Interop)
Ngoài mã .NET và C# còn được gọi là mã quản lý, còn có các thư viện, APIs và mã khác không nằm dưới sự kiểm soát của .NET Framework hoặc CLR. Các thư viện, APIs và mã này được gọi là mã nguyên thủy hoặc không được quản lý.
Có một số tình huống mà mã quản lý phải tương tác với mã không quản lý. Microsoft cung cấp công nghệ để hỗ trợ việc ‘tương tác’ này giữa mã nguyên thủy và mã quản lý.
Thông qua điều này, nhà phát triển có thể truy cập các chức năng trong các thư viện nguyên thủy và thư viện COM hiện có mà không được tiết lộ bởi .NET Framework, tận dụng tốc độ và tiện lợi của .NET mà không cần viết lại mã hiện có.
C# 9.0 đã giới thiệu ba tính năng mới để cải thiện hiệu suất của một số thư viện cấp thấp và cung cấp hỗ trợ bổ sung cho tương tác nguyên thủy thông qua những tính năng này.
Dưới đây là các tính năng mới:
- Số nguyên kích thước nguyên thủy
- Con trỏ hàm
- Bỏ qua cờ
localsinit
Số nguyên kích thước nguyên thủy
Các số nguyên có kích thước cụ thể cho mỗi nền tảng được gọi là số nguyên kích thước nguyên thủy. Khi chạy trong quá trình 32 bit – đó là các số nguyên 32 bit. Trong quá trình 64 bit, đó là các số nguyên 64 bit. nint
và nuint
là các từ khóa ngữ cảnh được sử dụng để xác định các số nguyên kích thước nguyên thủy – các loại số nguyên có kích thước nguyên thủy đăng ký và không đăng ký.
nint
và nuint
được biểu diễn bằng các loại dữ liệu cơ bản (một loại dữ liệu cụ thể cho từng nền tảng được sử dụng để biểu diễn một con trỏ hoặc một thao tác) cụ thể là System.IntPtr
và System.UIntPtr
.
Đoạn mã bên dưới minh họa về các số nguyên kích thước nguyên thủy.
public static void Main()
{
int aa = 300;
nint bb = 300;
nint cc = bb + 1;
var type1 = typeof(nint); // hiển thị
var type2 = typeof(nuint); // hiển thị
long v = 100;
var type3 = (aa + v).GetType();
Console.WriteLine("{0}, {1}, {2}", type1, type2, type3); // hiển thị
}
Khi mã được thực thi, nó sẽ hiển thị:
System.IntPtr, System.UIntPtr, System.Int64
Con trỏ hàm
C# hỗ trợ việc sử dụng các phương thức. Mã không quản lý sử dụng thuật ngữ ‘hàm’ cho các quy trình con của họ. Một biến lưu địa chỉ bộ nhớ của một hàm được gọi là con trỏ hàm. Hàm sau đó có thể được gọi thông qua con trỏ hàm đó. Tương tự như trong cuộc gọi hàm bình thường, con trỏ hàm cũng có thể được triệu gọi và truyền đối số. Sử dụng cú pháp delegate* mới giới thiệu trong C# 9.0, con trỏ hàm có thể được khai báo dễ dàng.
Bỏ qua cờ localsinit
Tính năng này được sử dụng để chỉ cho trình biên dịch không phát sinh cờ initlocal
– Đây là một cờ cụ thể chỉ cho CLR phải khởi tạo bằng không tất cả các biến cục bộ và luôn là mặc định từ C# 1.0. Bây giờ trong C# 9.0, bạn có thể ghi đè hành vi mặc định đó bằng cách thông báo cho trình biên dịch ngừng khởi tạo bằng không cho các biến cục bộ.