Nhũng tính năng thường được sử dụng trong C#

Bài viết này mình sẽ viết về C#, đây là ngôn ngữ yêu thích nhất của mình từ hồi đại học và gắn bó đến tận bây giờ dù bây giờ mình làm web thì mình làm việc nhiều hơn với Javascript. C# thì có rất nhiều điều để bàn đến, tuy nhiên trong bài viết này mình sẽ chủ yếu viết về những tính năng hay ho mà C# developer cần sử dụng rất nhiều. Những tính năng được đề cập trong bài viết sẽ không hoàn toàn là fundamentals như kiểu vòng lặp if else, cách sử dụng mảng hay generic class. Những gì được mình nhắc đến có thể nói là những chức năng hữu ích mà bạn rất nên biết và sử dụng thành thạo vì bạn gần như dùng đến hàng ngày.

Dưới đây là những tính năng được đề cập đến trong bài viết.

Để minh họa các tính năng này, mình sẽ tạo một Console App, nếu có thể các bạn nên tạo một Console App vừa đọc vừa copy để chạy thử để tiện theo dõi.

Automatically implemented properties

Một trong những điều mình thích ở C# chính là cú pháp ngắn gọn dễ hiểu, điển hình như automatically implemented properties. Trước khi có C# 3.0, các bạn sẽ cần phải khai báo như sau khi tạo class.

public class Subject
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

Điều này để đảm bảo tính chất encapsulation của OOP nhưng khi thuộc tính trong một class nhiều lên, sẽ khá là tốn công sức để viết code và code lặp cũng khá nhiều vì vậy trong C# 3.0 có một cách nhanh hơn rất nhiều. Thậm chí Visual Studio cũng gợi ý để sửa nhanh đoạn code trên. Bạn có thể đặt dấu nháy vào bất kì dòng nào ví dụ trong khối get set, và ấn Ctrl + . (dấu chấm). Khi đó sẽ có gợi ý sử dụng Use Auto Property. Đoạn code trên sẽ được tự động chuyển thành như sau.

public class Subject
{
    public string Name { get; set; }
}

Ngoài ra bạn còn có thể gán giá trị khởi tạo một cách nhanh chóng như sau

public class Subject
{
    public string Name { get; set; }
    public double Score { get; set; } = 0;
}

Trong trường hợp bạn chỉ muốn có read-only property, bạn chỉ việc bỏ set.

public class Subject
{
    public string Name { get; set; }
    public double Score { get; set; } = 0;
    public bool Mandatory { get; } = true;
}

Làm như vậy thì thuộc tính Mandatory không thể thay đổi, tuy nhiên bạn vẫn có thể thể thay đổi ở contructor.

public class Subject
{
    public Subject(bool mandatory = true)
    {
        Mandatory = mandatory;
    }
    public string Name { get; set; }
    public double Score { get; set; } = 0;
    public bool Mandatory { get; } = true;
}

Test thử bằng console app, giá trị nhận được sẽ là False.

class Program
{
    static void Main(string[] args)
    {
        Subject math = new Subject(false);
        Console.WriteLine("Mandatory: " + math.Mandatory);
    }
}
Mandatory: false

var keyword

C# cung cấp cho ta một cách thức khai báo biến mà không cần chỉ định rõ kiểu của biến. Trong C# cái này được gọi là type inference hoặc là implicit typing. Biến math trong đoạn code trên có thể được viết lại như sau.

var math = new Subject(false);

Viết như vậy không có nghĩa là biến math không có kiểu, cũng không phải là kiểu dynamic giống Javascript, kiểu của biến được trình biên dịch đối chiếu từ code. Trong ví dụ trên thì kiểu của biến dựa trên việc khởi tạo class Subject, do đó biến mang kiểu Subject.

String interpolation

Để hiển thị string theo ý muốn, trong C# ta có một số cách. Ví dụ ta muốn hiển thị một chuỗi như sau

Subject: Mathematics, Score: 8.5, Mandatory: False

Tao có thể nối chuỗi hoặc sử dụng string.Format()

Console.WriteLine("Subject: " + math.Name + ", Score: " + math.Score + ", Mandatory: " + math.Mandatory);
Console.WriteLine(string.Format("Subject: {0}, Score: {1}, Mandatory: {2}", math.Name, math.Score, math.Mandatory));

Với 2 cách trên ta đều nhận được kết quả giống nhau tuy nhiên C# cung cấp cho ta một cú pháp để xử lý chuỗi rất đơn giản và tường minh như sau

class Program
{
    static void Main(string[] args)
    {
        var math = new Subject(false);
        math.Name = "Mathematics";
        math.Score = 8.5;
        Console.WriteLine($"Subject: {math.Name}, Score: {math.Score}, Mandatory: {math.Mandatory}");
    }
}
Subject: Mathematics, Score: 8.5, Mandatory: False

Object and Collection Initializers

Một tính năng nữa của C# giúp ta thuận tiện hơn khi khởi tạo các giá trị mặc định của Object. Ở ví dụ trên thay vì ta khai báo

var math = new Subject(false);
math.Name = "Mathematics";
math.Score = 8.5;

ta có thể sử dụng cú pháp Object initializer như sau

class Program
{
    static void Main(string[] args)
    {
        var math = new Subject(false)
        {
            Name = "Mathematics",
            Score = 8.5
        };
               
        Console.WriteLine($"Subject: {math.Name}, Score: {math.Score}, Mandatory: {math.Mandatory}");
    }
}

Tương tự như vậy với collection initializer

var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

Ta sẽ rút gọn lại như sau

var numbers = new List<int> { 1, 2, 3 };

Anonymous Type

Trong C# ta còn có một kiểu gọi là Anonymous Type, bằng cách kết hợp Object Initializer và từ khóa var, ta có thể khai báo nhanh chóng như sau

var subjects = new[] {
    new { Name = "Mathematics", Score = 8.5 },
    new { Name = "Literature", Score = 7.0 },
    new { Name = "Physics", Score = 9.0 },
};

Mỗi đối tượng trong biến subjects là anonymous type. Nhưng không giống Javascript là mỗi đối tượng trong đó là dynamic type, C# vẫn là strong type. Trình biên dịch vẫn sẽ dựa vào code để suy ra kiểu dữ liệu. Bạn thử thay đổi một chút là chương trình sẽ báo lỗi ngay, ví dụ thêm vào một đối tượng khác kiểu.

var subjects = new[] {
    new { Name = "Mathematics", Score = 8.5 },
    new { Name = "Literature", Score = 7.0 },
    new { Name = "Physics", Score = 9.0 },
    new { Info = "Hacked", Result = false} // sẽ báo lỗi No best type found for implicitly-typed array
};

Extension Methods

Extension Method là một tính năng vô cùng tiện lợi để thêm phương thức vào các class mà bạn không thể chỉnh sửa trực tiếp, ví dụ như các class đó đã được đóng gói trong package. Ta tạo một class Student trong file Program.cs để minh họa, nhưng hãy giả sử như class Student này đến từ package của bên thứ ba và ta không có source code nên không có khả năng cập nhật phương thức cho nó.

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Subject> Subjects { get; set; }
}

Nếu muốn cập nhật thêm phương thức để tính điểm trung bình cho các môn ở trong thuộc tính Subjects cho class thì đó là lúc ta cần đến extension method. Ở đây, ta sẽ thêm một file mới như sau.

using System.Linq;
namespace CSharpEssentials
{
    public static class MyExtensionMethods
    {
        public static double Average(this Student student)
        {
            return student.Subjects.Select(x => x.Score).Average();
        }
    }
}

Class chứa extension method cần phải là static, trong trường hợp này namcespace là CSharpEssentials thì phương thức Average kia cũng chỉ dùng cho class có cùng namespace là CSharpEssentials . Average là extension method, cũng phải khai báo là static, sẽ nhận tham số có kiểu Student với từ khóa this đứng trước. Ta thử extension method với đoạn code sau.

static void Main(string[] args)
{
    var math = new Subject(false)
    {
        Name = "Mathematics",
        Score = 8.5
    };
    var literature = new Subject(false)
    {
        Name = "Literature",
        Score = 7.0
    };
    var physics = new Subject(false)
    {
        Name = "Physics",
        Score = 9.0
    };
    var student = new Student
    {
        Name = "John",
        Age = 20,
        Subjects = new List<Subject> { math, literature, physics }
    };
    var averageScore = student.CalculateAverage();
    Console.WriteLine($"Average score of John is: {averageScore}");
}
Average score of John is: 8.166666666666666

Null conditional và null coalescing operators

Đây là 2 tính năng rất hay giúp cho code C# vô cùng ngắn gọn và tường minh. Ta chỉnh sửa hàm main một chút như sau và đặt thuộc tính Subjects trong biến student là null để thử.

static void Main(string[] args)
{
    var math = new Subject(false)
    {
        Name = "Mathematics",
        Score = 8.5
    };
    var literature = new Subject(false)
    {
        Name = "Literature",
        Score = 7.0
    };
    var physics = new Subject(false)
    {
        Name = "Physics",
        Score = 9.0
    };
    var student = new Student
    {
        Name = "John",
        Age = 20,
        Subjects = null
    };
    string result;
    if (student.Subjects != null)
    {
        result = student.Subjects.FirstOrDefault().Name;
    }
    else
    {
        result = "[No name]";
    }
    Console.WriteLine($"First subject's score is {result}");
}

Khi Subjects bị null thì sẽ không thể thực hiện được hàm FirstOrDefault() để lấy phần tử đầu tiên của danh sách, nếu chạy qua đoạn code này chương trình sẽ bị lỗi. Do vậy ta phải dùng if/else để check null trước, nếu Subjects không bị null thì mới in ra phần tử đầu tiên còn nếu không thì hiện ra là [No name]. Đoạn code dài dòng từ dòng 28 đến dòng 39 có thể được giải quyết nhanh gọn như sau.

Console.WriteLine($"First subject's score is {student.Subjects?.FirstOrDefault().Name ?? "No name"}");

nameof expression

Trong nhiều trường hợp ta cần hiển thị label chính là tên của thuộc tính như trong ví dụ như sau.

static void Main(string[] args)
{
    var math = new Subject(false)
    {
        Name = "Mathematics",
        Score = 8.5
    };
    Console.WriteLine($"Name: {math.Name} - Score: {math.Score}");
}

Viết Name với Score là chuỗi string để hiển thị như vậy không có gì là sai, tuy nhiên giả sử ta thay đổi thuộc tính Score thành Mark hay Name thành tên gì đó khác thì ta lại cần sử chuỗi string cho tương ứng. C# cung cấp cho ta một cách đơn giản để không phải sửa đổi đó là lấy luôn tên thuộc tính để hiển thị, ta chỉ cần sửa lại như sau.

Console.WriteLine($"{nameof(math.Name)}: {math.Name} - ${nameof(math.Score)}: {math.Score}");

Sau này nếu thay đổi thuộc tính thì thi refactor code bằng Visual Studio, các thuộc tính sẽ được cập nhật tự động nhanh gọn và không tốn công sức.

Lambda expression

Phải nói lambda expression là một tính năng hay bậc nhất của C#, với cú pháp này, code được rút gọn và tường minh dễ hiểu đến cực đại, nó giúp abstract được nhiều kiến thức bên dưới, nhiều bạn không hoàn toàn hiểu hết những gì đằng sau nó nhưng vẫn có thể ứng dụng nó vào code hàng ngày.

Trước tiên ta thêm một hàm để lọc ra những subject được điểm cao, ta thêm một hàm vào file MyExtensionMethods trước đó, hàm này sẽ là extionsion method.

using System.Collections.Generic;
using System.Linq;
namespace CSharpEssentials
{
    public static class MyExtensionMethods
    {
        public static double CalculateAverage(this Student student)
        {
            return student.Subjects.Select(x => x.Score).Average();
        }
        public static IEnumerable<Subject> GoodSubjects(this List<Subject> subjects, double minimunScore)
        {
            foreach (var subject in subjects)
            {
                if (subject.Score >= 8)
                {
                    yield return subject;
                }
            }
        }
    }
}

Hàm GoodSubject ngoài tham số xác định đối tượng mở rộng, sẽ nhận tham số xác định điểm số tối tiểu để đạt điều kiện là môn học kết quả tốt. Sau khi kết thúc vòng lặp, kết quả nhận được là danh sách những subject có điểm số lớn hơn hoặc bằng tham số truyền vào. Trong hàm Main, ta dùng object initializer để khởi tạo một danh sách các subject để test hàm GoodSubject như sau. Ta truyền thàm số là 8 như vậy tất cả những môn học có điểm lớn hơn hoặc bằng 8 sẽ được hiển thị.

static void Main(string[] args)
{
    var subjectList = new List<Subject>
    {
        new Subject() { Name = "Mathematics", Score = 9.0 },
        new Subject() { Name = "Literature", Score = 6.0 },
        new Subject() { Name = "Physics", Score = 8.5 },
        new Subject() { Name = "Chemistry", Score = 8.0 },
        new Subject() { Name = "History", Score = 7.5 },
    };
    var selectedSubject = subjectList.GoodSubjects(8.0);
    foreach (var subject in selectedSubject)
    {
        Console.WriteLine(subject.Name);
    }
}

Và đây là kết quả nhận được.

Mathematics
Physics
Chemistry

Hàm vừa rồi chỉ nhận tham số là một giá trị nhất định, sau đó lọc ra các kết quả có điểm số lớn hơn giá trị đó, vậy nên ta không thể đòi hỏi hơn nếu muốn hàm đó lọc ra subject có score chính xác là 8 hoặc có điều kiện khác là score nhỏ hơn giá trị nào đó, nếu muốn thì ta phải sửa đoạn code trong hàm GoodSubjects. Bây giờ ta sẽ sửa hàm đó để nó tổng quát hơn.

using System;
using System.Collections.Generic;
using System.Linq;
namespace CSharpEssentials
{
    public static class MyExtensionMethods
    {
        public static double CalculateAverage(this Student student)
        {
            return student.Subjects.Select(x => x.Score).Average();
        }
        public static IEnumerable<Subject> GetSubjects(this List<Subject> subjects, Func<Subject, bool> condition)
        {
            foreach (var subject in subjects)
            {
                if (condition(subject))
                {
                    yield return subject;
                }
            }
        }
    }
}

Thay vì là hàm GoodSubject chỉ lấy các kết quả tốt (lớn hơn một giá trị nhất định truyền vào), ta sửa thành GetSubject với tham số là một hàm điều kiện, mục đích là lấy các subject theo một điều kiện nhất định, tham số sẽ được thay bằng một hàm, sau này ta sẽ truyền điều kiện qua hàm này. Func bản chất chính là một delegate, việc truyền hàm thông qua tham số trong Javascipt là điều hiển nhiên nhưng trong C# thì cần thông qua delegate. Delegate cũng là một khái niệm cơ bản nhưng không quá dễ hiểu và nhiều người bỏ qua, nếu bạn không hiểu delegate là gì thì cần tìm hiểu trước khi xem tiếp. Còn nếu ai đã lập trình Javascript thì sẽ không khó hiểu trong trường hợp này. Trong vòng for ta sẽ lấy các subject thỏa mãn điều kiện được truyền vào.

Trong hàm main ta sẽ viết hàm điều kiện để lấy các subject có điểm lớn hơn 8, GetGoodSubject như sau. Sau đó hàm GetSubject nhận tham số là hàm GetGoodSubject, khi đó kết quả ta nhận được giống hệt như trước đó.





Tuy nhiên điểm khác biệt lúc này là hàm extension method ta viết kia đã trở nên tổng quát hơn, và tiếp nhận được nhiều điều kiện logic hơn. Ta thử viết thêm một hàm để lấy môn học có kết quả bằng 8 ra. Lúc này ta chỉ việc thay tham số ở hàm tổng quát GetSubjects bằng hàm GetSubject8 là được.

static void Main(string[] args)
{
    var subjectList = new List<Subject>
    {
        new Subject() { Name = "Mathematics", Score = 9.0 },
        new Subject() { Name = "Literature", Score = 6.0 },
        new Subject() { Name = "Physics", Score = 8.5 },
        new Subject() { Name = "Chemistry", Score = 8.0 },
        new Subject() { Name = "History", Score = 7.5 },
    };
    static bool GetGoodSubject(Subject s)
    {
        return s.Score >= 8.0;
    }
    static bool GetSubject8(Subject s)
    {
        return s.Score == 8.0;
    }
    
    var selectedSubject = subjectList.GetSubjects(GetSubject8);
    foreach (var subject in selectedSubject)
    {
        Console.WriteLine(subject.Name);
    }
}

Kết quả ta nhận được là

Chemistry

Ta thấy hàm ban đầu giờ đa năng hơn rất nhiều, ta dễ dàng thay đổi điều kiện của nó chỉ bằng việc thay đổi tham số mà không phải lo thay đổi code bên trong hàm đó. Và đỉnh cao của C# giúp code vô cùng ngắn gọn và tường minh đó là việc sử dụng lambda expression. Hãy xóa 2 hàm GetGoodSubject và GetSubject 8 đi, ta không cần nó nữa mà hãy thay bằng cú pháp lambda.

static void Main(string[] args)
{
    var subjectList = new List<Subject>
    {
        new Subject() { Name = "Mathematics", Score = 9.0 },
        new Subject() { Name = "Literature", Score = 6.0 },
        new Subject() { Name = "Physics", Score = 8.5 },
        new Subject() { Name = "Chemistry", Score = 8.0 },
        new Subject() { Name = "History", Score = 7.5 },
    };
    
    var selectedSubject = subjectList.GetSubjects(x => x.Score >= 8.0); 
    // hoặc là x => x.Score == 8.0
    foreach (var subject in selectedSubject)
    {
        Console.WriteLine(subject.Name);
    }
}

Về bản chất cú pháp x => x.Score >= 8.0 chính là logic của hàm GetGoodSubject ban đầu khi x là tham số s truyền vào, mũi tên thể hiện return ra điều kiện logic x.Score >=8.0, giá trị trả về kiểu bool giống với hàm GetGoodSubject trong lambda expression sẽ dựa vào chính điều kiện logic. Kết quả cuối cùng bạn nhật được hoàn toàn giống trước đó.

Trong phần này mình giải thích thiên về lợi ích mà lambda expression mang lại, hơn là nói về lambda expression là gì. Nếu bạn cảm thấy khó hiểu, ban rất nên tìm hiểu thêm vì đây là kỹ thuật được sử dụng rất rất nhiều.

async/await keywords

Trước đây, lập trình bất đồng bộ trong C# khá là mệt mỏi với nhiều kiến thức về luồng cần phải nắm. Tuy nhiên từ khi từ khóa async và await ra đời thì vấn đề trở nên đơn giản hơn rất nhiều. Để minh họa cho lợi ich lập trình bất đồng bộ nói chung và từ khóa async/await nói riêng, bạn hãy thêm một hàm RunForLongTime vào file MyExtentionMethods như sau. Hàm này chẳng làm gì ngoài việc ngưng hệ thống 30 giây rồi trả về một chuỗi string. Mục đích để minh họa cho một tác vụ tốn nhiều thời gian.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CSharpEssentials
{
    public static class MyExtensionMethods
    {
        // ...
        public static string RunForLongTime(this Student student)
        {
            System.Threading.Thread.Sleep(30000);
            return "I have run for 30 seconds!";
        }
    }
}

Hàm Main dưới đây sẽ thực thi hàm RunForLongTime mà sẽ dừng hình mất 30s sau đó mới cho phép người dùng nhập dữ liệu, rồi cuối cùng mới in ra dữ liệu nhập vào và dòng thông báo từ hàm RunForLongTime.

static void Main(string[] args)
{
    var student = new Student();
    var notification = student.RunForLongTime();
    Console.Write("Type your name please: ");
    var name = Console.ReadLine();
    Console.WriteLine($"Hi, I am {name}.");
    Console.WriteLine(notification);
}
Type your name please: Duc
Hi, I am Duc.
I have run for 30 seconds!

Tương tự nếu như thực hiện một tác vụ mất nhiều thời gian mà block giao diện bắt user phải đợi trên trang web hay trên mobile thì về mặt UX là không tốt. Lúc này ta cần đến lập trình bất đồng bộ. Ta sẽ cập nhật hàm RunForLongTime với từ khóa async, đồng thời kiểu trả về là một Task.

public static async Task<string> RunForLongTime(this Student student)
{
    await Task.Run(() => System.Threading.Thread.Sleep(30000));
    return "I have run for 30 seconds!";
}

Trong hàm main lúc này, chương trình sẽ chạy qua thực hiện hàm RunForLongTime nhưng không dừng lại chờ mà tiếp tục chạy tiếp để người dùng nhập dữ liệu, người dùng có thể thực hiện thao tác ngay trong lúc 30s của tác vụ kia vẫn chạy. Cuối cùng lệnh notification.Wait() để đảm bảo rằng nếu người dùng nhập dữ liệu sớm trước 30s thì chương trình vẫn sẽ đợi đến khi hàm RunForLongTime được hoàn thành. Kết quả cuối cùng vẫn giống trước đó nhưng UX đã được cải thiện.

static void Main(string[] args)
{
    var student = new Student();
    var notification = student.RunForLongTime();
    Console.Write("Type your name please: ");
    var name = Console.ReadLine();
    Console.WriteLine($"Hi, I am {name}.");
    notification.Wait();
    Console.WriteLine(notification.Result);
}

Summary

Trong bài viết này, mình đã đề cập đến những tính năng theo mình là rất hay của C# mà có lẽ không có C# developer nào lại không dùng. C# là một ngôn ngữ mạnh mẽ và linh hoạt nên với một vấn đề sẽ có những cách viết code để giải quyết khác nhau, tuy nhiên những tính năng mình vừa đề cập thì bạn có thể thấy ở bất kỳ một project C# nào. Do vậy nắm vững những tính năng trên sẽ giúp bạn dễ dàng hơn trong công việc của mình.

Leave a Reply

Your email address will not be published. Required fields are marked *