Value type và reference type trong C#

(Cập nhật: Trong C# in depth (2nd edition) có một phần thảo luận chi tiết về vấn đề này, nếu bạn quan tâm thì nên tham khảo, đừng đọc phần dưới.)

Hôm nọ vào diễn đàn nào đấy thấy giải thích sự khác nhau giữa value type và reference type trong C#. Đại loại là biến value type nằm trên stack, biến reference type nằm trên heap. Có lẽ suy nghĩ này ảnh hưởng từ C/C++ gì đó chăng, nhưng nói chung là không chính xác. Về các đặc điểm của value type và reference type, MSDN đã trình bày đầy đủ, nên ở đây không lặp lại nữa. Quay lại chuyện stack và heap. Giả sử như ý trên đúng, tức là object tạo từ class nằm trên heap. Nếu object này có field int là value type, thì field này nằm ở đâu? Theo như ý trên thì nó nằm ở stack. Object nằm trên heap mà field nằm ở stack thì cũng hơi lạ nhỉ.

Thực ra, tôi nghĩ cái ý tưởng về stack và heap không đúng mà cũng chẳng sai. Vấn đề là, nó thuộc về hiện thực của CLR, và chúng ta không thể và cũng không cần biết chuyện nó lưu trữ các giá trị như thế nào. Việc chúng ta cần quan tâm chỉ là các tính chất và hành vi của chúng mà thôi.

Không phải chỉ C#, ngay cả với C++ thì việc sử dụng khái niệm stack và heap cũng là không cần thiết và có vẻ không chuẩn. Cụ thể thế nào thì các bạn có thể tìm hiểu thêm các khái niệm automatic storage, static storage, and dynamic storage (chẳng hạn, trong quyển C++ primer plus, đây cũng là một quyển rất tốt về C++).

C, C++: toán tử ++ và sequence point và side effect

Chắc ai cũng biết toán tử ++ trong C++ có hai phiên bản:

  • a++ tăng a thêm 1 (nếu a là int), trả về giá trị hiện tại (trước khi tăng) của a,
  • ++a tăng a thêm 1 (nếu a là int), trả về giá trị sau khi tăng của a.

Vậy cho int a = 1 thì 1 + a++ + a++; sẽ cho ra giá trị bao nhiêu? Dễ quá nhỉ, cứ áp dụng quy tắc trên là xong. Đây là một dạng câu hỏi phổ biến mà một số sách C, C++, v.v và một số giáo viên vẫn hay đố.

Tuy nhiên, thực tế dù bạn có áp dụng quy tắc trên, hay chạy thử bằng trình dịch thì kết quả mà bạn có vẫn luôn là một câu trả lời sai. Bởi vì theo đặc tả của C++, chỉ tại các sequence point, ta mới có thể đảm bảo là các side effect (hiệu ứng lề) của biểu thức đã được thực thi, và thứ tự thực thi thường không được đảm bảo. Chẳng hạn:

  • f() + g();: + không phải là sequence point, ; là sequence point. Không thể chắc chắn được là f() hay g() được thực thi trước.
  • f() + g(a++);: Không thể chắc chắn f() hay g(a++) được thực thi trước, nghĩa là không thể chắc chắn f() hay a++ được thực thi trược, nên nếu trong f có thể truy xuất được a thì giá trị của a sẽ là không xác định.
  • Tình huống ban đầu, 1 + a++ + a++;: Tất cả những gì ta có thể chắc chắn là a sẽ được tăng lên 1 hai lần, việc tăng này có thể xảy ra trước hoặc sau bất kì phép + nào trong biểu thức, nên giá trị của biểu thức này là không xác định, và phụ thuộc vào từng trình dịch cụ thể.

Ta kết luận được gì?

  • Đặc tả của các ngôn ngữ lập trình luôn phức tạp hơn ta tưởng rất nhiều, và những người soạn ra nó thực sự là các language lawyer.
  • Đừng cố lồng ghép các biểu thức như trên, bên cạnh việc không xác định giá trị nó còn làm cho mã nguồn trở nên vô cùng rối rắm.
  • Hạn chế side effect, trả về kết quả nên là công việc duy nhất mà một function phải làm. Đây cũng là một nguyên tắc mà lập trình hàm cổ vũ.
  • Đừng đố người khác những biểu thức như thế này :D.

Cyclic dependency trong C++

Xét trường hợp này:

/* ====== x.h ====== */ 
#include “y.h” 
class X 
{ 
  Y *m_y; 
  ... 
};

/* ====== y.h ====== */ 
#include “x.h” 
class X; 
class Y 
{ 
  X *m_x; 
  ... 
};

Dễ thấy muốn có x.h phải có y.h, mà muốn có y.h thì lại cần đến x.h. Trừ khi có khả năng xử lý cyclic dependency (như kiểu của Java), trình biên dịch sẽ báo lỗi.

Cách xử lí:

/* ====== x.h ====== */ 
// Forward declaration of Y for cyclic dependency 
class Y; 
class X 
{ 
  Y *m_y; 
  ... 
};

/* ====== y.h ====== */ 
// Forward declaration of X for cyclic dependency 
class X; 
class Y 
{ 
  X *m_x; 
  ... 
};

Đây gọi là forward declaration. Nó giúp giải quyết được cyclic dependency kiểu trên, tuy nhiên, có những dạng cyclic dependency không thể xử lí bằng forward declaration được.

Bạn có thể đọc thêm một số nguyên tắc khác ở đây.