
Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC
Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 1
Lập trình C#
Dịch từ cuốn sách Beginning C Sharp Game Programming
Phần 3: Giới thiệu về Lớp
Gía trị với Tham chiếu
Có những cách khác nhau để một trình biên dịch nói về dữ liệu, và C# có hai cách. Mọi kiểu dữ liệu
trong C# đều rơi vào một trong hai loại:
Kiểu giá trị (Value type)
Kiểu tham chiếu (Reference type)
Tôi sẽ giải thích các điểm khác nhau của mỗi loại trong những mục sau.
Kiểu dữ liệu
Một kiểu giá trị thường là một miếng nhỏ của dữ liệu mà hệ thống dành rất ít thời gian để sắp xếp.
Bạn đã sử dụng kiểu giá trị trong Phần 2 với tất cả các kiểu dữ liệu số được xây dựng sẵn. Mọi thứ
được liệt kê trong bảng 2.1 – như là int, float và vâng vâng – là một kiểu giá trị.
Ghi chú
Kiểu giá trị được tạo trên ngăn xếp hệ thống (system stack). Bạn không cần thiết để biết nó là
gì, nhưng nếu bạn cảm thấy thú vị, tôi khuyên bạn rằng nên tự nghiên cứu nó. Chủ đề này
vượt ra ngoài phạm vi của quyển sách này, vì vậy tôi không đủ chỗ để giải thích nó ở đây,
nhưng nó sẽ giúp bạn hiểu chính xác làm sao máy vi tính hoạt động, nó sẽ ảnh hưởng đến
việc tạo chương trình của bạn nhanh hơn và hiệu quả hơn.
Kiểu giá trị đơn giản và rất minh bạch để sử dụng, như là đoạn mã sau:
int x = 10, y = 20;
x = y; // Giá trị của y được chép vào x
y = 10; // y được gán bằng 10
Bên cạnh những kiểu đượcdựng sẵn, cấu trúc (structure) cũng là một kiểu giá trị, nhưng tôi sẽ đề cập
về sau trong phần này.
Kiểu tham chiếu
Kiểu tham chiếu hoàn toàn khác biệt so với kiểu giá trị. Lớp, khác với cấu trúc, luôn luôn là kiểu tham
chiếu. Kiểu tham chiếu, thay vì lưu dữ liệu một cách trực tiếp, nó lại lưu một địa chỉ, và địa chỉ đó trỏ
tới dữ liệu thật trong nơi nào đó của máy vi tính. Xem hình 3.1.
Khai báo một Kiểu Tham chiếu
Một trong những điểm khác nhau lớn nhất giữa kiểu giá trị và tham chiếu là cách mà bạn khai báo cho
nó. Một kiểu tham chiếu phải được tạo bởi từ khóa new (giả sử như chúng ta có một lớp tên Foo):
Foo x = new Foo();

Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC
Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 2
Trông có vẻ nhiều việc cần làm, nhưng bạn sẽ quen với nó. Cơ bản, đoạn mã thực hiện hai công việc.
Đó là:
1. Tạo một kiểu tham chiếu mới tên x, và
2. Tạo một đối tượng Foo mới trong đống dữ liệu và trỏ x đến đó.
Hình 3.1 Kiểu giá trị được lưu trực tiếp, trong khi kiểu tham chiếu lưu một địa chỉ trỏ tới dữ liệu thật.
Ghi chú
Đống dữ liệu (Heap) là một phần khác của máy vi tính để lưu trữ dữ liệu. Tôi không có đủ
chỗ để giải thích nó ở đây; đó chỉ là một vài thứ khác bạn nên nghiên cứu cho riêng bạn nên
bạn cảm thấy thú vị.
Đương nhiên, bạn không cần phải thực hiện chúng một lượt. Bạn có thể dễ dàng tách chúng ra như thế
này:
Foo x;
x = new Foo();
Nó tùy thuộc vào bạn.
Chơi đùa với các Tham chiếu
Và bây giờ là lúc để chơi đùa với các tham chiếu, nó là thứ mà bạn chưa gặp trước đây. Không may
cho bạn, các tham chiếu không hoạt động theo cách của các kiểu dữ liệu, mà nó có thể gây bối rối một
chút.
Đây là phần mà các tham chiếu có vẻ hơi khó hiểu một chút. Bạn hoàn toàn phải nhớ điều này mọi lúc
môi nơi khi sử dụng các tham chiếu, còn không chươn trình của bạn sẽ trở thành một mớ hỗn độn. Ví
dụ, hãy thử đoán xem đoạn mã này làm gì:
Foo x = new Foo();
Foo y = new Foo();
y = x;
// Thay đổi y ở đây
Bạn có nghĩ sau khi đọan mã này thực thi, x giữ nguyên trạng thái ban đầu và y thay đổi, đúng
không ? Sai !!! Chúng cùng thay đổi. Hãy nghe tôi – có một chút khó khăn để nhìn nhận nó, nhưng nó
có nghĩa. Sơ đồ có thể giúp, nhìn hình 3.2.
int x
10
Int32 y
địa chỉ của
dữ liệu
10
Ki
ể
u giá tr
ị
Kiểu tham chiếu

Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC
Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 3
Hình 3.2 Gán x cho y làm y trỏ tới dữ liệu của x, và không thực sự sao chép giá trị như bạn mong đợi.
Cơ bản, dòng này thật sự đã làm rối tung mọi thứ lên:
y = x;
Điều gì thật sự đã hoàn thành? Bạn muốn sao chép giá trị từ x sang y, nhưng điều đó đã không xảy ra.
Thay vào đó, vì chúng là những kiểu tham chiếu, máy vi tính sẽ trỏ y vào cùng dữ liệu mà x trỏ vào.
Vì vậy x và y bây giờ trỏ đến cùng một dữ liệu trong bộ nhớ, và thực hiện thao tác nào trên y sẽ làm
giống vậy đối với x.
Bộ thu gom rác
Trong ví dụ hình 3.2, bạn có thể ghi nhận rằng y đã giành lấy một vùng nhớ, và sau đó nó bị bỏ qua
khi gán x cho y. Vậy điều gì sẽ xảy ra đối với vùng nhớ mà y trỏ tới trước đó?
Trong những ngôn ngữ cũ, như C, vùng nhớ đó sẽ bị mất mãi mãi. Bạn sẽ tạo cái gọi là con trỏ treo
(dangling pointer), là các con trỏ giống các tham chiếu; máy vi tính biết rằng vùng nhớ đã được sử
dụng, nhưng chương trình của bạn sẽ quên nó ở đâu, và bạn sẽ không bao giờ có thể lấy lại vùng nhớ
đó được nữa trừ khi bạn tắt chương trình đi.
C# giải quyết được vấn đề này bởi bộ thu gom rác. Mỗi lần bạn tạo một mảnh dữ liệu trong C#, .NET
runtime sẽ theo dõi chương trình của bạn trỏ đến dữ liệu đó bao nhiêu lần, và nếu số lần đó về 0, thì
bộ thu gom rác sẽ phát hiện và giải phóng vùng nhớ đó để sử dụng cho thứ khác.
Không thể có rò rỉ bộ nhớ trong C#.
null
Có một giá trị đặc biệt mà bạn có thể sử dụng với kiểm tham chiếu; nó gọi là null. Giá trị null có
nghĩa là “chẳng có gì”. Nếu bạn đặt một tham chiếu đến null, thì bạn đang nói với máy vi tính rằng
tham chiếu đó chẳng trỏ tới đâu. Những ngôn ngữ cũ hơn sử dụng giá trị 0 để biểu thị nó, nhưng
null thì dễ đọc hơn.
x
y
Foo x = new Foo();
Foo y = new Foo();
dữ liệu
dữ liệu
x
y
y = x;
dữ liệu
dữ liệu
x
y
// Thay đổi y
dữ liệu
thay đổi
dữ liệu

Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC
Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 4
Cơ bản về Cấu trúc (Structure) và Lớp (Class)
Trong những ngày đầu của ngôn ngữ máy tính, ngôn ngữ lập trình khá đơn giản, và bạn có thể chỉ tạo
được một số lượng biến giới hạn. Điều này rõ ràng làm cho chương trình rất hạn chế và khá tồi tệ. Ví
dụ, bạn sẽ tạo ra chương trình như thế này trong những ngôn ngữ lập trình cũ:
int SpaceshipArmor;
int SpaceshipPower;
int SpaceshipFuel;
int EnemyArmor;
int EnemyPower;
int EnemyFuel;
Điều này sẽ là rối tung mọi thứ rất nhanh, và làm cho nó rất khó để quản lý mã của bạn.
Sử dụng lớp và cấu trúc sẽ làm cho cuộc sống của bạn đơn giản hơn khi đóng gói các dữ liệu đó vào
những gói dữ liệu “rất dễ sử dụng”.
Tạo ra Lớp và Cấu trúc
Về cơ bản, ý tưởng đằng sau lớp và cấu trúc là tạo ra kiểu đối tượng cho riêng bạn bằng những đối
tượng đã có sẵn. Một cấu trúc (structure) là một kiểu dữ liệu mà có thể giữ các dữ liệu khác ở bên
trong, cho phép bạn xây dựng kiểu dữ liệu của riêng bạn. Ví dụ, đây là một cấu trúc mô tả một đối
tượng tàu vũ trụ đơn giản trong C#:
struct Spaceship
{
public int fuel;
public int armor;
public int power;
}
Ghi chú
Từ khóa public nói cho trình biên dịch rằng bất kỳ hàm nào ở bất cứ đâu đầu có thể truy cập
dữ liệu bên trong một cấu trúc. Bạn không phải lo về việc này; tôi sẽ đi sâu hơn vào điều này
trong những mục sau. Nếu public bị bỏ đi, thì máy vi tính sẽ xem rằng bạn không muốn thứ gì
bên ngoài truy cập nó.
Ghi chú
Để tạo một lớp, đơn giản hãy thay từ khóa struct thành class trong ví dụ trước.
Và bậy giờ, bên trong chương trình của bạn, bạn có thể tạo biến tàu vũ trụ cho riêng bạn:
Spaceship player;
Spaceship enemy;
player.fuel = 100;
enemy.fuel = 100;
Điều này đơn giản, phải không?
Sự khác nhau giữa Cấu trúc và Lớp
Trong C#, có một vài điều khác nhau cơ bản giữa cấu trúc và lớp. Cấu trúc thường đơn giản và không
có nhiều thứ phức tạp bên trong chúng. Cấu trúc thường nhỏ hơn lớp, và C# luôn tạo cấu trúc là kiểu
giá trị (nghĩa là chúng sẽ luôn luôn tạo trong ngăn xếp).

Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC
Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 5
Lớp, khác với cấu trúc, luôn luôn là kiểu tham chiếu, và luôn được tạo trên một đống (heap), thay vì
trên ngăn xếp. Lớp có nhiều thứ mà cấu trúc không có, tôi sẽ giải thích những thứ này khi chúng ta
gặp nó.
Đưa Hàm (Function) và Lớp và Cấu trúc
Lớp và cấu trúc không chỉ có tính năng lưu trữ dữ liệu, mà chúng còn có thể thực hiện một vài phép
tính nữa, nếu bạn cho chúng khả năng đó. Ví dụ, bạn muốn đặt lại mọi dữ liệu của tàu thành 100 một
cách nhanh chóng; nếu không có một hàm, nó trông giống đoạn mã sau:
player.fuel = 100;
player.armor = 100;
player.power = 100;
Rõ ràng, điều này không phải là thứ bạn chỉ làm một lần, vì vậy tại sao thay vào đó không cho nó vào
một hàm, nên lớp Spaceship sẽ trông như thế này (phần mới được tô đậm):
struct Spaceship
{
public int fuel;
public int armor;
public int power;
public void Recharge()
{
fuel = 100;
armor = 100;
power = 100;
}
}
Bây giờ bạn có thể chỉ cần gọi hàm Recharge trên tàu vũ trụ của bạn khi bạn muốn đặt lại tất cả cái
biến:
player.Recharge();
Trả về giá trị
Các hàm không chỉ trhực hiện các tác vụ, mà chúng còn có thể trả về giá trị. Ví dụ, hãy nói bạn có
một con tàu vũ trụ; bạn biết nhiên liệu và năng lượng nó có, nhưng bạn không thực sự chắc chắn về
thời gian mà năng lượng cung cấp sẽ hết. Để tính toán nó, bạn cần thiết lập một công thức – hãy nói
rằng mỗi đơn vị năng lượng duy trì được hai giờ; và bạn muốn tìm xem còn bao nhiêu thời gian với số
năng lượng còn lại, bạn sẽ làm điều gì đó như thế này:
int hoursleft = player.power * 2;
Đó là một cách để giải quyết vấn đề, nhưng đó không thực sự là một giải pháp tốt. Về sau, bạn có thể
đề nghị mỗi đơn vị năng lượng tương đương với ba giờ thay vì hai. Để thay đổi điều này, bạn phải dò
hết lại mã của bạn và tìm xem chỗ nào bạn sử dụng số 2 và thay nó bằng số 3. Không vui chút nào.
Vì vậy, hãy đưng quá trình này vào một hàm!
int HoursofPowerLeft()
{ return power * 2;
}