C# 9.0 có gì mới?

1

C# 9.0 được Microsoft công bố cùng với .NET 5.0 trong hội thảo Build 2020 vừa qua (từ 11-13 tháng 11 năm 2020). Sau đây chúng ta cùng điểm qua một số điểm mới của C# 9.0.

Bài viết này lược dịch lại nội dung từ bài viết trên .NET Blog của Mads Torgersen. Bạn có thể xem bài gốc tiếng Anh ở link sau: https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/.

Init-only property

Khi làm việc với C# bạn hẳn rất yêu thích cách thức khởi tạo object initializer. Đây là cách khởi tạo object rất uyển chuyển và dễ đọc.

Giả sử bạn có class Person như sau:

class Person {
    public string FirstName { get; set;}
    public string LastName { get; set; }
}

Bạn có thể khởi tạo object của Person bằng cú pháp object initializer như sau:

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

Tức là khi khởi tạo object, bạn gán giá trị cho từng property.

Với cách thức khởi tạo object này bạn không cần viết chuỗi hàm tạo – điều thường gặp trong C++ hay Java.

Cách thức khởi tạo này gây ra một phiền phức nếu bạn muốn vận dụng mô hình lập trình hàm: để khởi tạo object bạn bắt buộc phải có setter. Nhưng nếu có setter, object tạo ra từ class này lại trở thành một biến mutable – đi ngược lại yêu cầu của lập trình hàm.

Init-only property được tạo ra để giải quyết vấn đề này.

Nếu bạn chưa biết. Trong lập trình hàm (functional programming) yêu cầu sử dụng các biến immutable – loại biến được tạo ra nhưng sau đó không thay đổi giá trị nữa. Nó tương tự như khi bạn khai báo một biến readonly. Loại biến này trong C# còn được gọi là runtime constant.
Thực ra bạn cũng không xa lạ lắm với lập trình hàm. Nếu đã biết sử dụng LINQ là bạn đã lập trình hàm rồi đấy!

C# 9.0 cho phép bạn khai báo class Person theo cách mới như sau:

class Person {
    public string FirstName { get; init;}
    public string LastName { get; init; }
}

Để ý, ở vị trí của setter giờ là từ khóa init. Từ khóa này biến một property thành dạng init-only – loại property chỉ có thể gán giá trị một lần.

Giờ bạn vẫn có thể tiếp tục sử dụng object initializer cho Person như bình thường.

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

Tuy nhiên, nếu sau đó bạn thay đổi giá trị của các init-only property, compiler sẽ báo lỗi:

// hai lệnh sau sẽ bị compiler báo lỗi
person.FirstName = "Joe";
person.LastName = "Biden";

Tức là, biến person đã trở thành immutable (runtime constant).

Sử dụng init accessor và trường readonly

Với full property, bạn có thể sử dụng setter cùng với backing field để kiểm soát giá trị.

Khi sử dụng init-only property, bạn cũng có thể kết hợp với một backing field như vậy. Điều khác biệt là backing field giờ đây phải đặt cùng từ khóa readonly.

public class Person
{
    private readonly string firstName = "<unknown>";
    private readonly string lastName = "<unknown>";
    
    public string FirstName 
    { 
        get => firstName; 
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
    }
    public string LastName 
    { 
        get => lastName; 
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

Từ khóa readonly đảm bảo rằng backing field chỉ được gán giá trị một lần. Khi này backing field của init-only property sẽ trở thành một runtime constant – một biến immutable (giá trị bất biến).

Như vậy, sử dụng init accessor và trường readonly giúp bạn kiểm soát giá trị gán cho init-only property, trong đó trường readonly đóng vai trò backing field.

Như vậy, có thể hình dung init property cũng có hai dạng, tượng tự như property thông thường: auto-implement init property và full init property.

Kiểu bản ghi record

Đây cũng là một cập nhật nữa để đưa C# tiến gần đến với các ngôn ngữ lập trình hàm.

Xu hướng (paradigm) chủ đạo trong C# là lập trình hướng đối tượng, theo đó mỗi object có định danh riêng và chứa dữ liệu biến đổi theo thời gian. Các object được so sánh với nhau theo địa chỉ.

Hai đặc điểm này khiến việc sử dụng class trong lập trình hàm có nhiều bất cập.

Lập trình hàm mong muốn dữ liệu ở dạng bất biến (immutable) và được so sánh theo giá trị (giống như kiểu struct). Tính bất biến thể hiện rằng object sau khi tạo ra thì các trường của nó không thay đổi nữa. So sánh theo giá trị nghĩa là hai object được xem là bằng nhau khi giá trị các trường của nó giống nhau.

Kiểu dữ liệu bất biến quen thuộc nhất trong C# là string.

C# 9.0 đưa vào một kiểu dữ liệu mới đáp ứng các yêu cầu trên: record. Có thể hình dung record như một dạng lai giữa class và struct.

public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Trong ví dụ trên chúng ta khai báo Person là một record với hai init-only property.

Các object của Person giờ có hai đặc điểm quan trọng cho lập trình hàm: bất biến và so sánh theo giá trị.

var p1 = new Person { FirstName = "Joe", LastName = "Biden" };
// hai lệnh sau KHÔNG hợp lệ, bạn không thể thay đổi giá trị của p1 sau khi được khởi tạo
p1.FirstName = "Donald";
p1.LastName = "Trump";
var p2 = new Person { FirstName = "Joe", LastName = "Biden" };
Console.WriteLine(Equals(p1, p2)); // cho kết quả true, mặc dù là hai object khác nhau

Phương thức Equals (kế thừa từ Object) hoặc phương thức tĩnh object.Equals() khi gọi trên các object của record sẽ so sánh chúng theo giá trị (giống như với struct).

Nếu cần so sánh object của record theo địa chỉ (giống object từ class), bạn cần dùng phương thức ReferenceEquals (hoặc object.ReferenceEquals).

Nếu thay từ khóa record bằng class, phép so sánh Equals(p1, p2) sẽ cho giá trị false vì đây là hai object khác nhau. Tức là object tạo từ class được so sánh theo địa chỉ. Nếu thay record bằng struct, phép so sánh Equals(p1, p2) vẫn cho giá trị true do object của struct được so sánh theo giá trị.

Như vậy, record có thể hình dung là một class nhưng có “biểu hiện” giống như struct.

Do vẫn là class, record trong C# 9 có đầy đủ các đặc điểm khác của class (là kiểu tham chiếu, có thể kế thừa).

public record Student : Person
{
    public int ID;
}

Positional record

Theo cách thông thường, bạn tạo class (và record) với constructor và deconstructor như sau:

public record Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; }
    // constructor
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    // deconstructor
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

Khi này bạn có thể tạo object và trích dữ liệu của object như sau:

var person = new Person("Mads", "Torgersen"); // positional construction
var (f, l) = person;                        // positional deconstruction
// f sẽ nhận giá trị "Mads", l sẽ nhận giá trị "Torgersen"

Deconstructor (chú ý phân biệt với desctructor) là khái niệm xuất hiện từ C# 7 cho phép trích giá trị từ một object và gán vào một biến kiểu tuple.

C# cung cấp một cách khác nhanh chóng hơn để tạo ra record:

public record Student(string FirstName, string LastName);

Cách thức tạo record này rất giống data class của Kotlin.

Cú pháp trên tự động tạo ra một record Student giống hệt như cách thức “truyền thống”.

Theo cú pháp positional record sẽ luôn tạo ra auto property. Nếu cần kiểm soát dữ liệu, bạn có thể chuyển chúng thành full property theo cách sau:

public record Person(string FirstName, string LastName)
{
    protected string FirstName { get; init; } = FirstName; 
}

Khi này FirstName sẽ là một full property, trong khi LastName vẫn là auto property.

Nếu positional record kế thừa từ một recored khác, bạn có thể gọi constructor của record cha như sau:

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

Biểu thức with

Khi làm việc với dữ liệu bất biến bạn sẽ thường xuyên phải tạo giá trị mới từ giá trị có sẵn (thay vì cập nhật giá trị có sẵn). Đây cũng là cách thể hiện trạng thái mới của dữ liệu trong lập trình hàm.

Ví dụ, nếu muốn thay đổi tên của một người, trong lập trình hướng đối tượng bạn cập nhật giá trị mới cho FirstName và LastName là xong. Tuy nhiên, nếu bạn sử dụng record và init-only property, bạn không thể gán giá trị mới cho các property này. Chúng là những giá trị bất biến. Trong trường hợp này bạn cần tạo object mới dựa trên object có sẵn và gán giá trị mới cho FirstName và LastName.

Thực ra cách làm việc này bạn đã gặp khi sử dụng kiểu string.

Để tiện lợi cho việc tạo object mới từ object cũ, C# 9 đưa vào biểu thức with. Cách sử dụng biểu thức with như sau:

// tạo object person
var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
// tạo object mới nhưng chỉ có giá trị LastName khác
var otherPerson = person with { LastName = "Torgersen" };

Trong ví dụ trên, đầu tiên bạn tạo object person với FirstName là “Mads”, LastName là “Nielsen”. Sau đó bạn tạo object otherPerson. Tuy nhiên, otherPerson có cùng giá trị FirstName với person, chỉ có LastName thay đổi.

Biểu thức with trong trường hợp này cho phép bạn nhanh chóng tạo ra một object mới dựa trên giá trị của một object có sẵn và chỉ thay đổi property nào cần thiết. Giá trị các property còn lại được sao chép sang.

Logic ở đây rất đơn giản: toàn bộ dữ liệu của object cũ được sao chép sang object mới và chỉ cập nhật giá trị của propery được chỉ định. Do vậy, để sử dụng biểu thức with, các property tương ứng phải có set hoặc init.

Top-level programs

Bạn nào mới học lập trình C# cũng đều trải qua chương trình Hello world quen thuộc:

using System;
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

C# 9 cho phép người mới học viết chương trình Hello world theo cách đơn giản hơn nữa:

using System;
Console.WriteLine("Hello World!");

Nghĩa là giờ đây bạn hoàn toàn có thể viết một chương trình C# như sau:

using static System.Console;
using System.Threading.Tasks;
WriteLine(args[0]);
await Task.Delay(1000);
return 0;

Bạn có thể để ý: không namespace, không class, không static void Main(). Một chương trình C# giờ nhìn hoàn toàn khác lạ, giống như một ngôn ngữ script nào đó.

Tuy nhiên, khi viết theo cách này, tất cả lệnh phải được viết sau khối using và trước bất kỳ lệnh khai báo kiểu/namespace nào. Bạn cũng chỉ được phép sử dụng lối viết này trong một file, nghĩa là file code thứ hai của project sẽ phải viết theo cách “thông thường”.

Hãy để ý trong lệnh WriteLine(args[0]); bạn có thể truy xuất mảng args mặc dù bạn không khai báo nó. Đây là một tham số “magic” giúp bạn truy xuất dữ liệu truyền từ giao diện dòng lệnh, giống hệ như khi bạn sử dụng static void Main(string[] args).

Nhìn chung khi sử dụng lối viết này bạn nên hình dung là phương thức Main sẽ được compiler tự động tạo ra từ code bạn viết.

Target typing và biểu thức new

Khởi tạo object trong C# vốn đã rất đơn giản và tiện lợi. Bạn có thể gọi constructor thông thường hoặc sử dụng object initializer.

C# 9 bổ sung thêm một cách khởi tạo nữa: target typing.

Hãy xem ví dụ sau:

Point p1 = new Point(3, 5); // cách thông thường
Point p2 = new (3, 5); // target typing

Trong cách khởi tạo mới này bạn không cần lặp lại tên kiểu Point ở vế phải nữa.

Lối khởi tạo mới này rất hữu dụng khi dùng với mảng và danh sách:

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) }; 

Từ khóa new trong trường hợp này hoạt động như một biểu thức và cũng được gọi là biểu thức new (new expression).

+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạn!

Kết luận

C# 9 đem đến nhiều cập nhật giúp ngôn ngữ phát triển theo hướng lập trình hàm, cô đọng và súc tích hơn.

Những gì điểm qua trên đây là những cập nhật dễ nhận biết nhất. Bạn có thể xem chi tiết tất cả các cập nhật của C#9 từ tài liệu chính thức https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-9.

Theo dõi
Thông báo của
guest

1 Thảo luận
Cũ nhất
Mới nhất
Phản hồi nội tuyến
Xem tất cả bình luận
Cường

Mấy nay không thấy admin ra bài mới về .NET nữa 🙁
Mấy bài của admin hay quá, rất chi tiết và dễ hiểu