Các tính năng mới của C# 7.0

Phiên bản C# 7.0 đã được phát hành cùng với Visual Studio 2017. Phiên bản này có một số cải tiến thú vị so với phiên bản C# 6.0. Bài viết này sẽ trình bày các tính năng mới của C# 7.0.

Biến out trong C#

Cú pháp hiện đang hỗ trợ các tham số out đã được cải tiến trong phiên bản này. Bây giờ bạn có thể khai báo các biến out ngay trong danh sách đối số khi gọi phương thức, thay vì viết một câu lệnh khai báo riêng:

if (int.TryParse(input, out int result))
{
    Console.WriteLine(result);
}
else
{
    Console.WriteLine("Could not parse input");
}

Bạn có thể muốn chỉ định kiểu dữ liệu rõ ràng cho biến out, như đã trình bày ở ví dụ trên. Tuy nhiên, phiên bản này cũng hỗ trợ sử dụng biến cục bộ được định kiểu ngầm - sử dụng từ khóa var như dưới đây:

if (int.TryParse(input, out var answer))
{
    Console.WriteLine(answer);
}
else
{
    Console.WriteLine("Could not parse input");
}
  • Mã dễ đọc hơn: Bạn khai báo biến out nơi bạn sử dụng nó, không cần phải khai báo biến trước ở một dòng khác.
  • Không cần gán giá trị ban đầu: Bằng cách khai báo biến out khi gọi phương thức, bạn không thể vô tình sử dụng nó trước khi nó được định nghĩa.

Tuple trong C#

Đôi khi bạn viết các phương thức cần một cấu trúc dữ liệu đơn giản chứa nhiều phần tử dữ liệu mà bạn không muốn định nghĩa một lớp mới để lưu trữ các phần tử dữ liệu này.

Để hỗ trợ kịch bản này, các lớp tuple đã được thêm vào C#. Tuple là cấu trúc dữ liệu nhẹ chứa nhiều trường để đại diện cho các thành viên dữ liệu. Các trường không được xác thực và bạn cũng không thể định nghĩa phương thức của riêng mình

Lưu ý: Tuples đã có sẵn trước C# 7.0, nhưng chúng không hiệu quả. Điều này có nghĩa là các phần tử tuple chỉ có thể được tham chiếu như Item1, Item2v.v. C# 7.0 cho phép sử dụng các tên cho các trường của tuple bằng cách sử dụng những kiểu khai báo mới hiệu quả hơn.

Bạn có thể tạo một tuple bằng cách gán một giá trị cho mỗi thành viên và tùy ý cung cấp tên cho từng thành viên của tuple như sau:

(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

Tuple namedLetters chứa các trường được gọi là AlphaBeta. Những tên đó chỉ tồn tại ở thời gian biên dịch và không được bảo tồn.

Ngoài ra, bạn cũng có thể chỉ định tên của các trường ở phía bên phải của khai báo:

var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

Có thể đôi khi bạn muốn thay đổi tên các thành viên của một tuple được trả về từ một phương thức. Bạn có thể làm điều đó bằng cách khai báo các biến riêng biệt cho từng giá trị trong tuple. Việc làm này được gọi là giải cấu trúc (deconstructing) tuple:

(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

Bạn cũng có thể cung cấp giải cấu trúc tương tự cho bất kỳ kiểu dữ liệu nào trong .NET.

Bạn viết một phương thức Deconstruct như một thành viên của lớp. Phương thức Deconstruct đó cung cấp một tập hợp các đối số out cho mỗi thuộc tính bạn muốn trích xuất. Hãy xem lớp Point này cung cấp một phương thức giải cấu trúc để trích xuất tọa độ XY:

public class Point
{
   public Point(double x, double y) 
	   => (X, Y) = (x, y);

   public double X { get; }
   public double Y { get; }

   public void Deconstruct(out double x, out double y) =>
	   (x, y) = (X, Y);
}

Bạn có thể trích xuất các trường riêng lẻ bằng cách gán một biến Point cho một tuple như sau:

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

Tìm hiểu thêm về lớp Tuple trong C# tại đây:

Lớp Tuple trong C# | Comdy
Tuple và ValueTuple là hai cấu trúc dữ liệu rất tiện lợi trong C#. Bài viết sẽ hướng dẫn bạn các sử dụng chúng.

Loại bỏ trong C#

Thông thường khi giải cấu trúc (deconstructing) một tuple hoặc gọi một phương thức với các tham số out, bạn buộc phải định nghĩa một số biến có giá trị mà bạn không quan tâm và không có ý định sử dụng.

C# đã bổ sung loại bỏ (discards) để xử lý tình huống này. Loại bỏ là một biến chỉ ghi (write-only) có tên là _(ký tự gạch dưới); bạn có thể gán tất cả các giá trị mà bạn định loại bỏ cho một biến duy nhất.

Một loại bỏ giống như một biến không được gán; ngoài câu lệnh gán, loại bỏ không thể được sử dụng trong mã.

Loại bỏ được hỗ trợ trong các trường hợp sau:

  • Khi giải cấu trúc tuple hoặc kiểu dữ liệu do người dùng xác định.
  • Khi gọi phương thức với tham số out.
  • Trong một hoạt động khớp mẫu với các câu lệnh isswitch.

Ví dụ sau định nghĩa một phương thức QueryCityDataForYears trả về tuple chứa dữ liệu cho một thành phố trong hai năm khác nhau. Gọi phương thức trong ví dụ chỉ liên quan đến hai giá trị dân số được trả về bởi phương thức và do đó coi các giá trị còn lại trong tuple là loại bỏ khi nó giải cấu trúc tuple.

using System;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }
   
    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;
      
        if (name == "New York City")
        {
            area = 468.48; 
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}

Đây là kết quả khi biên dịch và thực thi chương trình trên:

Population change, 1960 to 2010: 393,149

Khớp mẫu trong C#

Khớp mẫu (pattern matching) là một tính năng cho phép bạn triển khai phương thức trên các thuộc tính khác với kiểu của đối tượng. Có lẽ bạn đã quen với việc triển khai phương thức dựa trên kiểu của đối tượng.

Trong lập trình hướng đối tượng, các phương thức virtualoverride cung cấp cú pháp để thực hiện triển khai phương thức dựa trên kiểu của đối tượng. Các lớp cơ sở và lớp dẫn xuất cung cấp các triển khai khác nhau.

Các biểu thức khớp mẫu mở rộng khái niệm này để bạn có thể dễ dàng triển khai các biểu thức cho các kiểu dữ liệu và các thành phần dữ liệu không cần sử dụng kế thừa.

Khớp mẫu hỗ trợ biểu thức is và biểu thức switch. Mỗi cái cho phép kiểm tra một đối tượng và các thuộc tính của nó để xác định xem đối tượng đó có thỏa mãn mẫu hay không. Bạn sử dụng từ khóa when để chỉ định các quy tắc bổ sung cho mẫu.

Khớp mẫu cho toán tử is rất quen thuộc vì nó sử dụng toán tử is để truy vấn một đối tượng về kiểu dữ liệu của nó và gán kết quả trong một chỉ dẫn. Đoạn mã sau kiểm tra xem một biến có phải kiểu int không và nếu có, sẽ cộng nó vào tổng hiện tại:

if (input is int count)
{
    sum += count;
}

Ví dụ nhỏ trên cho thấy các cải tiến cho biểu thức is. Bạn có thể kiểm tra các kiểu giá trị cũng như các kiểu tham chiếu và bạn có thể gán kết quả thành công cho một biến mới của kiểu chính xác.

Khớp mẫu cho toán tử switch cũng rất quen thuộc, dựa trên câu lệnh switch có sẵn trong C#. Câu lệnh chuyển đổi được cập nhật có một số cấu trúc mới:

  • Kiểu dữ liệu chính của một biểu thức switch không còn bị giới hạn trong kiểu giá trị, kiểu Enum, kiểu string hoặc kiểu nullable tương ứng với các kiểu dữ liệu đó. Bây giờ bất kỳ kiểu dữ liệu nào cũng có thể được sử dụng.
  • Bạn có thể kiểm tra kiểu dữ liệu của biểu thức switch trong mỗi case. Như với biểu thức is, bạn có thể gán một biến mới cho kiểu đó.
  • Bạn có thể thêm một mệnh đề when để kiểm tra thêm các điều kiện về biến đó.
  • Thứ tự của các case rất quan trọng vì nhánh đầu tiên khớp sẽ được thực thi và những nhánh khác sẽ bị bỏ qua.

Đoạn mã sau minh họa các tính năng mới này:

public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
    int sum = 0;
    foreach (var i in sequence)
    {
        switch (i)
        {
            case 0:
                break;
            case IEnumerable<int> childSequence:
            {
                foreach(var item in childSequence)
                    sum += (item > 0) ? item : 0;
                break;
            }
            case int n when n > 0:
                sum += n;
                break;
            case null:
                throw new NullReferenceException("Null found in sequence");
            default:
                throw new InvalidOperationException("Unrecognized type");
        }
    }
    return sum;
}
  • case 0: là mẫu hằng số quen thuộc.
  • case IEnumerable<int> childSequence: là mẫu kiểu dữ liệu.
  • case int n when n > 0: là mẫu kiểu dữ liệu với một điều kiện when bổ sung.
  • case null: là mẫu null.
  • default: là trường hợp mặc định quen thuộc.

Trả về ref trong C#

Tính năng này cho phép các thuật toán sử dụng và trả về các tham chiếu đến các biến được định nghĩa ở nơi khác.

Một ví dụ là làm việc với ma trận lớn và tìm một vị trí duy nhất theo điều kiện. Phương thức sau đây trả về một tham chiếu đến giá trị trong bộ lưu trữ của ma trận:

public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
    {
        for (int j = 0; j < matrix.GetLength(1); j++)
        {
            if (predicate(matrix[i, j]))
            {
                return ref matrix[i, j];
            }
        }
    }
    throw new InvalidOperationException("Not found");
}

Bạn có thể khai báo giá trị trả về dưới dạng ref và sửa đổi giá trị đó trong ma trận, như ví dụ sau:

ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

Ngôn ngữ C# có một số quy tắc bảo vệ để tránh việc bạn lạm dụng trả về ref như sau:

  • Bạn phải thêm từ khóa ref vào chữ ký phương thức và cho tất cả các câu lệnh return trong một phương thức.
  • Câu lệnh return ref có thể được gán cho một biến giá trị hoặc một biến ref: Người gọi kiểm soát xem giá trị trả về có được sao chép hay không. Việc bỏ qua từ khóa ref khi gán giá trị trả về cho biết rằng người gọi muốn có một bản sao của giá trị, chứ không phải tham chiếu đến giá trị trong bộ lưu trữ.
  • Bạn không thể gán giá trị trả về của một phương thức chuẩn cho một biến ref cục bộ.
  • Bạn không thể trả về một biến ref có vòng đời chỉ tồn tại trong phương thức: Điều đó có nghĩa là bạn không thể trả về một tham chiếu đến một biến cục bộ hoặc một biến có phạm vi tương tự.
  • Trả về ref không thể được sử dụng với các phương thức async: Trình biên dịch không thể biết nếu biến được tham chiếu đã được đặt thành giá trị cuối cùng của nó khi phương thức async trả về.

Việc bổ sung trả về ref cho phép các thuật toán hiệu quả hơn bằng cách tránh sao chép các giá trị hoặc thực hiện tính toán nhiều lần.

Hàm cục bộ

Nhiều lớp có thiết kế các phương thức private được gọi từ chỉ một phương thức khác. Các phương thức private bổ sung này giữ cho mỗi phương thức có kích thước nhỏ và tập trung.

Các hàm cục bộ (local functions) cho phép bạn khai báo các phương thức bên trong ngữ cảnh của một phương thức khác. Các hàm cục bộ giúp người đọc dễ dàng thấy rằng phương thức cục bộ chỉ được gọi từ ngữ cảnh mà nó được khai báo.

Có hai trường hợp sử dụng phổ biến cho các hàm cục bộ: phương thức lặp public và phương thức không đồng bộ public. Cả hai loại phương thức đều tạo mã thông báo lỗi muộn hơn mong đợi của các lập trình viên.

Ví dụ sau đây cho thấy việc tách xác thực tham số khỏi việc thực hiện iterator bằng hàm cục bộ:

public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
    if (start < 'a' || start > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    }
    
    if (end < 'a' || end > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
    }

    if (end <= start)
    {
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
    }

    return alphabetSubsetImplementation();

    IEnumerable<char> alphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
            yield return c;
    }
}

Kỹ thuật tương tự có thể được sử dụng với các phương thức async để đảm bảo rằng các ngoại lệ phát sinh từ xác thực đối số được đưa ra trước khi công việc không đồng bộ bắt đầu:

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
    {
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    }
        
    if (index < 0)
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    }
        
    if (string.IsNullOrWhiteSpace(name))
    {
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
    }
    
    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}
Lưu ý: Một số thiết kế được hỗ trợ bởi các chức năng cục bộ cũng có thể được thực hiện bằng cách sử dụng biểu thức lambda.

Biểu thức thân thành viên

C# 6 đã giới thiệu biểu thức thân thành viên (expression-bodied member) cho các phương thức và các thuộc tính chỉ đọc. C# 7.0 mở rộng các thành viên được phép có thể được thực hiện dưới dạng biểu thức. Trong C# 7.0, bạn có thể sử dụng biểu thức thân thành viên cho constructor, finalizers , và bộ truy xuất getset vào các thuộc tínhchỉ mục.

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}


Bài viết liên quan:

Hướng dẫn này sẽ giúp bạn tìm hiểu về đọc ghi file (File I/O) trong C# và sử dụng các lớp tiện ích để đọc ghi file.

Reflection trong C#

  • 6 min read

Reflection trong C# là gì? Ứng dụng của Reflection trong C#. Cách khai báo và sử dụng Reflection trong C#.

Attribute trong C#

  • 7 min read

Attribute trong C# là gì? Có những loại attribute nào trong C#? Làm sao để sử dụng attribute trong C#.