5 lỗi sai cơ bản thường gặp trong C#

538

Bài viết được sự cho phép bởi tác giả Vũ Thành Nam

#1 Sử dụng kiểu dữ liệu tham chiếu như một kiểu nguyên thuỷ hay kiểu giá trị

Trong lập trình C++ hay nhiều ngôn ngữ khác có sử dụng kiểu dữ liệu tham chiếu, tức là các giá trị cửa kiểu dữ liệu đó tham chiều tới cùng một vùng nhớ. Để hiểu về kiểu dữ liệu nguyên thuỷ (value type hay primitive type) hay kiểu dữ liệu tham chiếu (reference type) bạn cần hiểu đôi chút về vùng nhớ trong đó có heap và stack. Kiểu dữ liệu tham chiếu tuy các giá trị được lưu trên stack nhưng để truy vấn nó thì lại gọi theo heap. Hay tưởng tượng 1 ngôi nhà có địa chỉ và khi gửi thư ta thưởng gửi tới địa chỉ 123/4 chẳng hạn.

Các kiểu int, long, bool, float là kiểu dữ liệu nguyên thuỷ hay kiểu giá trị. Còn object là một loại kiểu dữ liệu tham chiếu nên khi bạn gán giá trị hãy chú ý đến sự tham chiếu của nó nhé. Nhưng Nhưng Nhưng! Đừng tưởng cứ object là tất tần tật như nhau…

Ví dụ:

  Point point1 = new Point(20, 30);
  Point point2 = point1;
  point2.X = 50;
  Console.WriteLine(point1.X);       // 20 (does this surprise you?)
  Console.WriteLine(point2.X);       // 50
  
  Pen pen1 = new Pen(Color.Black);
  Pen pen2 = pen1;
  pen2.Color = Color.Blue;
  Console.WriteLine(pen1.Color);     // Blue (or does this surprise you?)
  Console.WriteLine(pen2.Color);     // Blue

Ví dụ trên thì Point và Pen là object được tạo ra bằng cùng 1 cách, nhưng giá trị của point1 không đổi khi gán một giá trị mới từ point2, trong khi giá trị pen1 lại bị thay đổi khi gán giá trị color mới được gán từ pen2. Chúng ta có thể nhận thấy, point1 và point2 chứa giá trị sao chép đối tượng Point, trong khi pen1 và pen2 lại chỉ chứa tham chiểu tương đồng của đổi tượng Pen.

Đi sâu hơn (f12 sẽ thấy) thì Point là struct (kiểu giá trị) còn Pen là class (kiểu tham chiếu chính gốc đấy nhé).

#2 Hiểu sai về giá trị mặc định khi biến không được khởi tạo giá trị ban đầu

Trong C# kiểu dữ liệu nguyên thuỷ hay kiểu giá trị không thể null. Nhớ nhé khi khai báo 1 object chưa được gán giá trị khởi tạo thì nó là null, nhưng đổi với kiểu giá trị thì nó không null đâu đừng có mà check null biến có kiểu giá trị làm gì.

class Program 
{
      static Point point1;
      static Pen pen1;
      static void Main(string[] args) 
      {
          Console.WriteLine(pen1 == null);      // True
          Console.WriteLine(point1 == null);    // False (huh?)
      }
}

Như đề cập ở trên Point là struct nó không có giá trị mặc định là null mà là (0,0)

Các đạik C# thường dùng hàm extension IsEmpty để check cho các đa số (không phải tất cả) biến kiểu giá trị nhé.

  So sánh C# và C++ - Nên học ngôn ngữ nào?

  Học gì để trở thành C# developer? Những kiến thức quan trọng

#3 Sử dụng sai hay hiểu sai hàm string comparison

Có nhiều cách để so sánh chuỗi string trong C# và đa số khi so sánh lại sử dụng toán tử == Nó thực sự là một phương thức không hề an toàn một chút nào chính bỏi vì nó không hề tường minh khi bạn muốn so sánh chuỗi đi kèm type. Đào sau hơn xem thử có 2 cách so sánh chuỗi trong C# với Equals.

public bool Equals(string value);
public bool Equals(string value, StringComparison comparisonType);

Dấu == chính là ngầm định của kiểu so sánh Equals đầu tiên không sử dụng StringComparison. Thật chẳng có gì xảy ra khi so sánh chuỗi cơ bản các ký tự alphabet, số hay ký tự đặc biệt thông dụng. Tuy nhiên nếu bạn cần dựa trên ngôn ngữ cài đặt trên môi trường chạy (hệ điều hành chẳng hạn) thì một số ký tự sẽ được coi là khác nhau nếu chuyển đổi môi trường khác nhau (CurrentCultrure) nó khác nhau. Chính vì vậy khi so sánh chuỗi sẽ an toàn hơn nếu sử dụng hàm Equals bao gồm comparisonType. Ví dụ dưới đây cho thấy giá trị so sánh sẽ khác nhau tuỳ thuộc vào option của comparisonType.

string s = "strasse";

// outputs False:
Console.WriteLine(s == "straße");
Console.WriteLine(s.Equals("straße"));
Console.WriteLine(s.Equals("straße", StringComparison.Ordinal));
Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture));        
Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase));

// outputs True:
Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture));
Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));

Không chỉ có phương thức Equals, ngay cả phương thức Compare cũng nên dùng comparisonType nhé. Chỉ dùng ==, >, <, <=, >= đối với so sánh chuỗi khá có thể sẽ gặp rủi ro vào một ngày đẹp trời đó.

Tham khảo việc làm lập trình C hấp dẫn trên TopDev!

#4 Sử dụng vòng lặp thay vì khai báo liệt kê (LINQ) trong collections (kiểu tập hợp)

C# 3.0 mang đến cho chúng ta LINQ (Language Integrated Query) với rất nhiều tiện ích khi làm việc với tập hợp khi truy vấn (queries) và liệt kê chúng. Nhưng đừng nên lạm dụng chúng nhé. LINQ được sinh ra với ý nghĩa đơn giản hoá và làm tương đồng với các hình thức truy vấn database và nó chỉ nên làm việc khi truy vấn database.

Có người cho rằng chúng chẳng làm sai kết quả mà lại gọn gàng dễ hiểu (theo mình cũng chẳng dễ hiểu đâu). Ví dụ:

decimal total = 0;
foreach (Account account in myAccounts) 
{
	if (account.Status == "active") 
	{
		total += account.Balance;
	}
}

//Or

decimal total = (from account in myAccounts
				where account.Status == "active"
				select account.Balance).Sum();

Rồi để xem nhé, với LINQ kìa nó lấy lên 1 danh sách các balance (1 lần lặp) sau đó duyệt chung và tính tổng (1 lần lặp trong Sum á) vậỵ là 2 lần lặp rồi nhá. Code ngắn vậy thôi nhưng sao nó đang làm tăng độ phức tạp lên kìa, trong khi bên trên ta chỉ có mỗi vòng lặp xử lý. Thêm vào đó LINQ không thể (đúng hơn là rất khó) debug khi gắn logic vào nó. Không tin hả! thử cộng trừ nhân chia trong LINQ và trace kết quả coi. Hãy đơn giản hoá nó và trả nó về với mục đích chính của nó nhé. Đáng cân nhắc phải không?

#5 Những lỗi cơ bản cần cân nhắc trước khi làm việc với LINQ

LINQ làm việc rất tốt đối với việc xử lý đa tiến trình (task) trong tập hợp (collections) liệu rằng chúng có lưu trên bộ nhớ của các đối tượng (objects) bảng cơ sở dữ liệu (database tables) hay xml… Đời không phải lúc nào cũng hoàn hảo, nếu có lỗi trong quá trình truy vấn thì sao, liệu rằng nó sẽ văng (throw) ra exception hay trả về một kết quả khác sai hoàn toàn. Chẳng phải bạn luôn thích try catch trong mọi hàm hay sao 😀

decimal total = (from account in myAccounts
                       where account.Status == "active"
                       select account.Balance).Sum();

Điều gì xảy ra nếu account.Status là “Active” (chữ A lại viết hoa mới đểu), vâng thật tuyệt vời khi myAccounts được lưu trong đối tượng DbSet (phía hạ tầng database) sau khi truy vấn thông qua biểu thức sẽ vẫn khớp với phần tử đó. Tuy nhiên nếu myAccounts lại được lưu trong bộ nhớ đệm của mảng, nó sẽ trả về một giá trị khác đi. Khoan đã nào, ở đây có 2 vấn đề cần được làm rõ. Thứ nhất tại sao không dùng Equals với comparisonType như ở trên nói mà lại dùng ==. Thứ 2, nếu dùng một hàm extension methods ở chỗ điều kiện kia (ví dụ account.Status.LowerCase()  == “active”) và trong hàm đó nó lăn ra chết 😀 (tức là throw exception đó) thì điều gì xảy ra đối với hàm Sum.

Câu trả lời cho điều thứ nhất là do LINQ là ngôn ngữ translate thành câu lệnh TSQL nên việc dùng == ở trường hợp này lại là một cách chính xác. Và cũng chính do nó tuân theo toán tử TSQL nên mọi Extension method đều không dùng được trong LINQ. Nên điều thứ 2 ở trên không thực hiện được đâu trừ khi đổi sang dạng lambda với extension mà .Net hỗ trợ sẵn khi đó nó mới tuân theo C# rule. Tham khảo tại đây: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable?redirectedfrom=MSDN&view=netframework-4.7.2  và https://www.c-sharpcorner.com/article/linq-extension-methods/

Nói tóm lại là LINQ là ngôn ngữ chuyển đổi thành TSQL nó tuân theo luật rừng của TSQL nên khi đúng cần chú ý nhé!

Bài viết gốc được đăng tải tại ntechdevelopers.com

Xem thêm:

Tham khảo ngay việc làm IT mọi cấp độ trên TopDev!