© 2020, Developed by Hieu Dev

Các mẹo lập trình C# giúp năng cao chất lượng và hiệu suất

Trong bài viết này, chúng ta sẽ tìm hiểu một số mẹo và thủ thuật C# hữu ích về cách cải thiện chất lượng và hiệu suất code của chúng ta. Sau đó, chúng ta sẽ thấy một số lưu ý khi nói đến việc xử lý ngoại lệ mà chúng ta nên biết.

Các mẹo lập trình C# giúp năng cao chất lượng và hiệu suất

Null-Check

Trong các dự án, ta thực hiện kiểm tra null khá thường xuyên, và cách phổ biến nhất mà chúng ta thường làm là:

var product = GetProduct();

if (product == null)
{
    // ...
}

Cách tổ chức code này có vẻ rất bình thường nhưng bạn có biết vấn đề với cách tiếp cận này là gì không? Toán tử == có thể bị ghi đè và không có đảm bảo chặt chẽ rằng việc so sánh một đối tượng với null sẽ tạo ra kết quả mà chúng ta mong đợi. Thay vào đó, ta nên dùng toán tử mới được giới thiệu trong C# 7, đó là toán tử is.

Đây là cách chúng ta có thể thực hiện kiểm tra null với toán tử is như sau:

var product = GetProduct();

if (product is null)
{
    // ...
}

Toán tử is sẽ luôn đánh giá true trước tiên xem đối tượng được chỉ định có phải là không null. Nó cũng là một cách viết kiểm tra null rõ ràng hơn vì nó đọc giống như một câu.

Bắt đầu với C#9, bạn có thể sử dụng phủ định để thực hiện kiểm tra null như sau:

var product = GetProduct();

if (product is not null)
{
    // ...
}

Reduce Nesting

Nesting là khi code nằm giữa hai hoặc nhiều hơn hai dấu ngoặc nhọn. Một ví dụ đơn giản về nesting là phần thân của một phương thức, trường hợp này ta sẽ gặp ở bất kỳ ngôn ngữ nào, và ta thấy rằng quá nhiều dấu ngoặc lồng ghép nhau thì code sẽ rất rối.

Tại sao khi code quá nhiều nesting sẽ không tốt? Thông thường, một hoặc hai cấp độ nesting sẽ không có vấn đề gì. Tuy nhiên, chúng ta càng có nhiều cấp độ lồng vào nhau, thì việc đọc code càng trở nên khó khăn hơn và các lỗi sẽ trở nên khó kiểm soát hơn.

Nhưng chúng ta có thể cải thiện một cách dễ dàng. Hôm nay mình sẽ hướng dẫn đến bạn một số tips C# để giảm nesting trong bộ code của bạn.

Chúng ta hãy xem một ví dụ trong đó chúng ta có một câu lệnh if-else. Bên trong câu lệnh if, chúng ta trả về một số giá trị:

Product PurchaseProduct(int id)
{
    var product = GetProduct(id);

    if (product.Quantity > 0)
    {
        product.Quantity--;

        return product;
    }
    else
    {
        SendOutOfStockNotification(product);

        return null;
    }
}

Trong những trường hợp như vậy, ta có thể xoá những câu lệnh else và giảm cấp độ nesting một cách hiệu quả như sau:

Product PurchaseProduct(int id)
{
    var product = GetProduct(id);
    if (product.Quantity > 0)
    {
        product.Quantity--;
        return product;
    }
    SendOutOfStockNotification(product);
    return null;
}

Đôi khi, chúng ta cần đảm bảo điều kiện được đáp ứng trước khi thực hiện câu lệnh bên trong. Điều này thường dẫn đến nhiều cấp độ nesting vào nhau và mã khó đọc hơn, ví dụ như:

bool IsProductInStock(int id)
{
    var product = GetProduct(id);
    if (product is not null)
    {
        if (product.Quantity > 0)
        {
            return true;
        }
    }
    return false;
}

Để giảm nesting đoạn code trên, bạn có thể làm như sau:
 
bool IsProductInStock(int id)
{
    var product = GetProduct(id);
    if (product is null)
    {
        return false;
    }
    if (product.Quantity <= 0)
    {
        return false;
    }
    return true;
}


Trên là một ví dụ về nguyên tắc early return, cụ thể là chúng ta nên trả về từ một phương thức càng sớm càng tốt. Trong trường hợp này, trước tiên ta kiểm tra xem product có null không và trả lại false nếu có. Sau đó, ta kiểm tra xem số lượng có nhỏ hơn hoặc bằng 0 hay không và trả lại false nếu đúng. Nếu không, sản phẩm không null và số lượng lớn hơn 0, vì vậy trả lại true

Chúng ta có thể tối ưu hóa điều này hơn nữa bằng cách kết hợp hai câu lệnh if thành một câu lệnh duy nhất với code như sau:

 
bool IsProductInStock(int id)
{
    var product = GetProduct(id);
    if (product is null || product.Quantity <= 0)
    {
        return false;
    }
    return true;
}

Using Declarations

Các bạn có thể thấy, ta quá quen khi sử dụng quá nhiều nesting khi sử dụng câu lệnh using, ví dụ như:

using (var streamReader = new StreamReader("..."))
{
    string content = streamReader.ReadToEnd();
}

Từ C#8 trở lên, bạn có thể dễ dàng sử dụng câu lệnh using như sau:
 
using var streamReader = new StreamReader("...");
string content = streamReader.ReadToEnd();


Nhưng khi sử dụng, bạn nên lưu ý rằng câu lệnh using chỉ sử dụng ở block-level scope.


Logical Expression

C# 9 đã giới thiệu các logical patterns mới mà chúng ta có thể sử dụng để cải thiện các biểu thức logic trở nên dễ hiểu dễ đọc hơn và bớt rối hơn. 

Ví dụ, chúng ta sẽ viết một hàm để kiểm tra xem một ký tự được chỉ định có phải là một chữ cái hay không:

bool IsLetter(char ch) => (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');

Đây là cách code điển hình sử dụng để kiểm tra một ký tự được chỉ định có phải là một chữ cái hay không, mặc dù viết hơi rườm rà vì phải lặp lại tham số cho mỗi lần kiểm tra.

Với các logical pattern mới như and và or, chúng ta có thể viết lại hàm trước đó như sau:

bool IsLetter(char ch) => ch is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

Cách code này dễ đọc hơn nhiều vì nó gần như đọc giống như một câu. Chúng ta cũng không cần chỉ định tham số ký tự nhiều lần.

Loại bỏ if-else khi set giá trị boolean

Chúng ta thường gặp một tình huống trong code khi cần trả về một giá trị bool từ một hàm, ví dụ như sau:

bool IsInStock(Product product)
{
    if (product.Quantity > 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Mặc dù cách tiếp cận này không sai, nhưng chúng ta phải tự hỏi liệu chúng ta có cần câu lệnh if ngay từ đầu hay không. Vì chúng ta đã có một biểu thức logic bên trong câu lệnh if, chúng ta có thể đơn giản hóa phương pháp này bằng cách chỉ cần trả về giá trị của biểu thức logic đó:

bool IsInStock(Product product)
{
    return product.Quantity > 0;
}

Chúng ta đã code tối giản hơn nhiều để đạt được kết quả tương tự. Chúng ta có thể đơn giản hóa thêm phương pháp trước đó bằng cách sử dụng expression body như sau:

bool IsInStock(Product product) => product.Quantity > 0;

Switch Statements

Câu lệnh switch có thể rất hữu ích khi chúng ta muốn đánh giá một số đối tượng và dựa trên các giá trị có thể trả về một kết quả khác.

Ví dụ, ta hãy viết một câu lệnh chuyển đổi để kiểm tra xem ngày hiện tại có phải là ngày cuối tuần hay không:

switch (DateTime.Now.DayOfWeek)
{
    case DayOfWeek.Monday:
        return "Not Weekend";
    case DayOfWeek.Tuesday:
        return "Not Weekend";
    case DayOfWeek.Wednesday:
        return "Not Weekend";
    case DayOfWeek.Thursday:
        return "Not Weekend";
    case DayOfWeek.Friday:
        return "Not Weekend";
    case DayOfWeek.Saturday:
        return "Weekend";
    case DayOfWeek.Sunday:
        return "Weekend";
    default:
        throw new ArgumentOutOfRangeException();
}


Như chúng ta thấy, chúng ta phải viết rất nhiều code và chúng lại lặp lại với nhau. Chúng ta có thể thu gọn bằng cách kết hợp tất cả các câu lệnh case để trả về cùng một kết quả:
 
switch (DateTime.Now.DayOfWeek)
{
    case DayOfWeek.Monday:
    case DayOfWeek.Tuesday:
    case DayOfWeek.Wednesday:
    case DayOfWeek.Thursday:
    case DayOfWeek.Friday:
        return "Not Weekend";
    case DayOfWeek.Saturday:
    case DayOfWeek.Sunday:
        return "Weekend";
    default:
        throw new ArgumentOutOfRangeException();
}

Như trên, bạn có thể thấy code của chúng ta được tối giản đáng kể. Nhưng khi bắt đầu với C#8 trở đi, các biểu thức switch sẽ giúp chúng ta cải thiện code trở nên tối giản hơn nữa bằng cách biến đổi câu lệnh switch trước đó thành một biểu thức switch:
 
DateTime.Now.DayOfWeek switch
{
    DayOfWeek.Monday => "Not Weekend",
    DayOfWeek.Tuesday => "Not Weekend",
    DayOfWeek.Wednesday => "Not Weekend",
    DayOfWeek.Thursday => "Not Weekend",
    DayOfWeek.Friday => "Not Weekend",
    DayOfWeek.Saturday => "Weekend",
    DayOfWeek.Sunday  => "Weekend",
    _ => throw new ArgumentOutOfRangeException()
}

Nhưng ở C#9, chúng ta cũng có thể sử dụng các logical pattern trong các biểu thức switch. Trong trường hợp này, chúng ta có thể tối giản tiếp các code trên bằng cách sử dụng or như sau:
 
DateTime.Now.DayOfWeek switch
{
    DayOfWeek.Monday or DayOfWeek.Tuesday or DayOfWeek.Wednesday or DayOfWeek.Thursday or DayOfWeek.Friday => "Not Weekend",
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    _ => throw new ArgumentOutOfRangeException()
}

Và tiếp theo là "trùm cuối", giới thiệu bạn một cách tối giản hơn nữa khi sử dụng logical pattern not:
 
DateTime.Now.DayOfWeek switch
{
    not (DayOfWeek.Saturday or DayOfWeek.Sunday) => "Not Weekend",
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    _ => throw new ArgumentOutOfRangeException()
}

Filter Exceptions

Giả sử bạn có gặp tình huống khi bạn cần phải xử lý một ngoại lệ. Chúng ta thường gặp một trường hợp như thế này, trong đó chúng ta phải thực hiện các logic xử lý ngoại lệ khác nhau dựa trên một số điều kiện.

Giả sử chúng ta muốn xử lý HttpRequestException theo một cách khi đó StatusCode400(Bad Request) hay khi gặp StatusCode404 (Not Found). Cách tiếp cận đơn giản sẽ là bắt ngoại lệ và sau đó viết một câu lệnh if để kiểm tra một điều kiện:

try
{
    await GetBlogsFromApi();
}
catch (HttpRequestException e)
{
    if (e.StatusCode == HttpStatusCode.BadRequest)
    {
        HandleBadRequest(e);
    }
    else if (e.StatusCode == HttpStatusCode.NotFound)
    {
        HandleNotFound(e);
    }
}

Cách viết như trên không sai, nhưng nó lại không được quá clean, và quá nhiều nesting được giới thiệu trên. Thay vì cách viết trên, bạn có thể viết như sau:
 
try
{
    await GetBlogsFromApi();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.BadRequest)
{
    HandleBadRequest(e);
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
    HandleNotFound(e);
}


Return Empty Collections

Thông thường, chúng ta có các phương thức trả về một collection. Tuy nhiên, chúng ta nên return lại những gì trong trường hợp các điều kiện tiên quyết không được đáp ứng? Thường thì chúng ta trả về các giá trị null, chẳng hạn như:

IEnumerable<Product> GetProductsByCategory(string category)
{
    if (string.IsNullOrWhiteSpace(category))
    {
        return null;
    }
    var products = _dbContext.Products.Where(p => p.Category == category).ToList();
    return products;
}

Thực tế cách làm này không tốt vì buộc chúng ta phải gọi và kiểm tra kết quả null.  Một cách tiếp cận tốt hơn sẽ là chỉ trả về một collection rỗng như sau:

IEnumerable<Product> GetProductsByCategory(string category)
{
    if (string.IsNullOrWhiteSpace(category))
    {
        return Enumerable.Empty<Product>();
    }
    var products = _dbContext.Products.Where(p => p.Category == category).ToList();
    return products;
}

Như bạn có thể thấy, sử dụng Array.EmptyEnumerable.Empty là cách hiệu quả để trả về một collection rỗng từ một phương thức. 

Lời kết

Và đây, chúng chính là những mẹo lập trình C# giúp năng cao chất lượng và hiệu suất của bạn. Trong thực tế, còn rất nhiều cách để tối ưu, và mình sẽ tổng hợp và giới thiệu các bạn trong các phần tiếp theo.

Như các bạn có thể thấy, các phiên bản C# về sau này, mỗi bạn cập nhật luôn tạo ra những cách cải thiện và tối ưu hiệu năng đáng kể. Vì thế trong những bài tới mình sẽ giới thiệu các bạn phiên bản C# mới nhất với nhứng hiệu quả vượt trội.

Tham khảo: https://code-maze.com/c-tips-to-improve-code-quality-and-performance

Mong bài viết hữu ích, chúc các bạn thành công!
Hieu Ho.

Đăng nhận xét

Mới hơn Cũ hơn

TOGETHER

WE GROW