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++).

Advertisements

Tóm lại polymorphism trong OOP là gì nhỉ?

(Bài này khá linh tinh và có thể chứa thông tin không chính xác. Nếu bạn mới học lập trình thì không nên đọc.)

Khi chưa tìm hiểu kĩ về polymorphism, tôi có cảm giác polymorphism là một cái gì đó khá mơ hồ. Nói chung thì các sách khi để cập đến vấn đề này đều dùng ví dụ minh họa hơn là định nghĩa chính xác. Với những tài liệu có định nghĩa, thì có vẻ như đánh đồng polymorphism và overriding. Tuy nhiên, trong khi với những ngôn ngữ như Java các method đều là virtual method thì cũng có những ngôn ngữ như C# trong đó virtual method và overriding phải được phát biểu một cách rõ ràng. Tuy nhiên, như ta đã biết, polymorphism cùng với encapsulation và inheritance thường được giới thiệu như là ba cột trụ (pillar) của OOP. Nếu polymorphism đồng nghĩa với overriding và virtual method thì tại sao những ngôn ngữ như C# lại có vẻ như hạn chế cột trụ này? (Nói ngoài lề là thực ra việc đòi hỏi phát biểu virtual method và overriding một cách rõ ràng trong C# có cái lí của nó, chứ không phải là để hạn chế.)

Thực tế là, khi đọc các tài liệu mang tính lí thuyết hơn, bạn sẽ thấy polymorphism không phải là một khái niệm của riêng OOP, càng không đồng nghĩa với overriding. Mặc dù không phải tất cả các tài liệu đều viết giống nhau, nhưng nhìn chung có thể định nghĩa polymorphism là tính chất cho phép các cấu trúc gồm hàm và kiểu dữ liệu có thể làm việc với nhiều kiểu dữ liệu khác nhau. Có ba dạng polymorphism:

  • Parametric polymorphism: Các type liên quan đến cấu trúc được cung cấp dưới dạng biểu thức trong đó type là các biến. Chẳng hạn, C++ template chính là một cơ chế hỗ trợ parametric polymorphism trong ngôn ngữ này. Tất nhiên, parametric polymorphism có thể có trong các ngôn ngữ không hỗ trợ OOP.
  • Ad hoc polymorphism: Thuật ngữ này dùng để chỉ overloading. Ví dụ phổ biến là toán tử + có thể là một function dạng int * int –> int, cũng có thể là một function dạng string * string –> string. Một số người không đồng ý xem overloading là một dang polymorphism.
  • Subtype polymorphism: Đây là dạng polymorphism mà thuật ngữ “polymorphism” trong OOP chỉ tới. Với subtype polymorphism, một method có thể áp dụng cho nhiều type khác nhau thông qua subtyping. Cụ thể, nếu một method áp dụng được cho type T, nó sẽ áp dụng được cho subtype của T. Quan hệ subtype thường được thiết lập thông qua inheritance. Chú ý là, định nghĩa này không đề cập đến overriding hay virtual method gì cả. Một ngôn ngữ OOP có thể không hỗ trợ overriding mà vẫn thỏa mãn subtype polymorphism.

Một điểm đáng chú ý là trừ Wikipedia, tôi chưa thấy có nguồn nào coi overriding là một dạng polymorphism cả.

Tóm lại nên định nghĩa polymorphism như thế nào? Về mặt lương tâm mà nói thì có lẽ cần trình bày dựa theo lí thuyết về ngôn ngữ lập trình. Về mặt cảm giác, tôi nghĩ định nghĩa polymorphism trong OOP bằng subtype polymorphism tuy trông thì phức tạp nhưng lại đơn giản, mạch lạc, ít mơ hồ hơn định nghĩa thông qua overriding (đương nhiên kéo theo người học dễ hiểu, dễ nhớ, dễ hệ thống hơn). Về thực tế thì vì định nghĩa về polymorphism trong OOP thông qua overriding (và kéo theo hiểu rằng polymorphism là đặc trưng của OOP) đã quá phổ biến, mà hiểu sao thì tóm lại vẫn là biết lập trình, nên có lẽ cũng không cần bận tâm đến polymorphism thực sự là cái gì nữa :D.

Các bạn có thể tham khảo thêm trong Concepts in programming languages và Programming languages pragmatics.

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.