Xây dựng các lớp
đối tượng là khả năng để tạo ra những kiểu dữ liệu mới, một đặc tính quan trọng của ngôn ngữ lập trình hướng đối tượng.
Chúng ta có thể xây dựng những kiểu dữ liệu mới trong
ngôn ngữ C# bằng cách khai báo và định nghĩa những lớp. Thể hiện của một lớp được gọi là những
đối tượng (object). Những đối tượng
này được tạo trong bộ nhớ khi
chương trình được thực hiện.
Sự khác nhau giữa
một lớp và một đối
tượng cũng giống như sự khác nhau giữa
khái niệm giữa loài mèo và một con mèo Mun đang nằm bên chân của ta. Chúng ta không thể đụng chạm hay đùa giỡn với khái niệm
mèo nhưng có thể thực hiện điều
đó được với mèo Mun, nó là một thực thể sống động, chứ không
trừu tượng như khái niệm họ loài mèo.
Một họ mèo mô tả những con mèo có các đặc tính: có trọng
lượng, có chiều cao, màu mắt, màu lông,...chúng cũng có hành động như là ăn ngủ, leo trèo,...một con mèo, ví dụ như mèo Mun chẳng hạn, nó cũng có trọng lượng
xác định là 5 kg,
chiều cao 15 cm, màu mắt đen, lông đen...Nó
cũng có những khả năng như ăn ngủ leo trèo,..
Lợi ích to lớn của những lớp trong
ngôn ngữ lập trình là khả năng đóng gói các thuộc tính và tính chất của một thực thể trong một khối đơn, tự có nghĩa, tự khả năng
duy trì . Ví dụ khi chúng
ta muốn sắp nội dung những
thể hiện hay đối tượng của lớp điều
khiển ListBox trên
Windows, chỉ cần gọi các đối
tượng này thì chúng
sẽ tự sắp xếp, còn việc chúng làm ra sao thì ta không quan
tâm, và cũng chỉ cần biết bấy nhiêu
đó thôi.
Đóng gói cùng với đa hình (polymorphism) và kế thừa
(inheritance) là các thuộc
tính chính yếu của bất kỳ một ngôn ngữ lập trình
hướng đối tượng nào.
Để định
nghĩa một kiểu dữ liệu mới hay một lớp đầu tiên phải khai báo rồi
sau đó mới định nghĩa các thuộc tính và phương thức của kiểu dữ liệu đó. Khai báo một lớp bằng cách sử dụng từ khoá class. Cú pháp đầy đủ của khai báo
một lớp như sau:
[Thuộc
tính] [Bổ sung truy cập] class
<Định danh lớp> [: Lớp cơ sở]
{
<Phần
thân của lớp: bao gồm định nghĩa các thuộc
tính và phương thức hành động >
}
Thành phần thuộc tính của đối
tượng sẽ được trình bày chi tiết trong phần
sau, còn thành phần bổ sung truy cập cũng sẽ được
trình bày tiếp ngay mục dưới. Định danh lớp chính là tên của lớp do
người xây dựng chương trình tạo ra. Lớp cơ sở
là lớp mà đối tượng sẽ kế thừa để phát
triển ta sẽ bàn sau. Tất cả các thành
viên của lớp được định nghĩa bên trong thân của lớp, phần thân này sẽ được
bao bọc bởi hai dấu ({}).
Trong C#, mọi chuyện
đều xảy ra trong một lớp.
Như các ví dụ mà
chúng ta đã tìm hiểu,
các hàm điều
được
đưa vào trong một lớp, kể cả hàm đầu
vào của chương
trình (hàm Main()):
public
class Tester
{
public static int Main()
{
//....
}
}
Điều cần nói ở đây là chúng ta chưa tạo bất cứ thể hiện nào của lớp, tức là tạo
đối tượng cho lớp Tester. Điều gì khác nhau giữa một lớp và thể hiện của lớp? để trả lới cho câu hỏi
này chúng ta bắt đầu xem xét sự khác nhau giữa
kiểu dữ liệu int và một
biến kiểu int . Ta có viết như sau:
int var1 = 10;
tuy nhiên ta không thể viết
được
int = 10;
Ta không thể gán giá trị
cho một kiểu dữ liệu, thay
vào đó ta chỉ
được gán dữ liệu cho một
đối tượng của kiểu dữ lịêu đó, trong trường hợp trên
đối tượng là biến var1.
Khi chúng ta tạo một lớp mới, đó chính là việc định
nghĩa các thuộc tính
và hành vi của tất cả các đối
tượng của lớp. Giả sử chúng ta đang lập trình để tạo các điều
khiển trong các ứng dụng trên Windows, các điều khiển này
giúp cho người dùng
tương tác tốt với Windows, như là ListBox,
TextBox, ComboBox,...Một
trong những điều khiển thông dụng
là ListBox, điều khiển này cung cấp một danh
sách liệt kê các mục chọn và cho phép người dùng chọn
các mục tin trong
đó.
ListBox này cũng có các thuộc tính khác nhau nhu: chiều cao, bề dày, vị trí,
và màu sắc thể hiện và các hành vi của chúng như: chúng có thể thêm bới mục tin, sắp xếp,...
Ngôn ngữ lập trình
hướng đối tượng cho phép chúng ta tạo kiểu dữ liệu mới là lớp ListBox, lớp này bao bọc
các thuộc tính cũng
như khả năng
như: các thuộc tính
height, width, location, color, các phương thức hay hành vi như Add(), Remove(), Sort(),...
Chúng ta không thể gán dữ liệu cho kiểu ListBox, thay vào đó đầu tiên ta phải
tạo một đối tượng
cho lớp đó:
ListBox myListBox;
Một khi chúng ta đã tạo một thể hiện của lớp ListBox thì ta có thể gán dữ liệu cho thể hiện đó. Tuy nhiên đoạn lệnh trên
chưa thể tạo đối tượng
trong bộ nhớ được, ta sẽ bàn
tiếp. Bây giờ ta sẽ tìm hiểu cách
tạo một lớp và tạo các
thể hiện thông qua ví dụ minh họa 4.1. Ví dụ
này tạo một lớp có chức
năng hiểu thị thời gian trong một ngày. Lớp
này có hành vi thể hiện ngày, tháng, năm, giờ, phút, giây hiện hành. Để làm được điều trên thì lớp
này có 6 thuộc tính hay
còn gọi là biến thành viên, cùng với một phương thức như sau:
//Tạo
một lớp Thoigian đơn giản như sau:
------------------------------------------------------------------
using
System;
public
class ThoiGian
{
public void ThoiGianHienHanh()
{
Console.WriteLine(“Hien thi thoi gian hien hanh”);
}
//Các biến thành viên
int Nam;
int Thang;
int Ngay;
int Gio;
int Phut;
int Giay;
}
public
class Tester
{
static void Main()
{
ThoiGian t = new ThoiGian();
t.ThoiGianHienHanh();
}
}
------------------------------------------------------------------
Kết
quả:
Hien
thi thoi gian hien hanh
------------------------------------------------------------------
Lớp ThoiGian chỉ
có một phương
thức chính là hàm
ThoiGianHienHanh(), phần
thân của phương
thức này được định nghĩa bên trong của lớp
ThoiGian. Điều này
khác với ngôn ngữ C++, C# không đòi hỏi phải khai báo trước khi định
nghĩa một
phương thức, và
cũng không hỗ trợ việc khai báo phương thức trong một tập tin và sau đó định nghĩa ở một tập tin
khác. C# không có các tập
tin tiêu đề, do vậy tất cả các
phương thức
được định nghĩa hoàn toàn bên trong của lớp. Phần cuối của định
nghĩa lớp là phần khai báo các biến thành viên: Nam, Thang, Ngay, Gio,
Phut, va Giay.
Sau khi định nghĩa xong lớp ThoiGian thì tiếp theo là phần
định nghĩa lớp Tester, lớp này có chứa
một hàm khá thân thiện với chúng ta là hàm Main(). Bên trong hàm Main có một thể hiện của lớp ThoiGian được tạo ra và
gán giá trị cho đối tượng t. Bởi vì t
là thể hiện của đối
tượng ThoiGian, nên
hàm Main() có thể sử dụng phương thức của t:
t.ThoiGianHienHanh();
Thuộc tính truy cập
quyết định khả năng các phương thức của lớp bao gồm việc các
phương thức của lớp khác có thể
nhìn thấy và sử dụng các biến
thành viên hay những
phương thức bên
trong lớp. Bảng 1.13 tóm tắt các
thuộc tính truy cập của một lớp trong C#.
Bảng 1.13. Thuộc tính
truy cập
Thuộc
tính
|
Giới hạn
truy cập
|
public
|
Không hạn chế. Những thành viên
được đánh dấu public có thể được
dùng bởi bất kì các phương thức của lớp
bao gồm những lớp khác.
|
private
|
Thành viên trong một lớp A được
đánh dấu là
private thì chỉ được truy cập bởi
các phương thức của lớp A.
|
protected
|
Thành viên trong lớp A được
đánh dấu là
protected thì chỉ được các phương thức
bên trong lớp A và những phương thức dẫn xuất
từ lớp A truy cập.
|
internal
|
Thành viên trong lớp A được
đánh dấu là
internal thì được truy cập bởi những
phương thức của bất cứ lớp nào trong
cùng khối hợp ngữ với A.
|
protected
internal
|
Thành viên trong lớp A được
đánh dấu là
protected internal được truy cập bởi các
phương thức của lớp A, các phương thức
của lớp dẫn xuất của A, và bất cứ lớp
nào trong cùng khối hợp ngữ của A.
|
Mong muốn chung là thiết kế các biến thành viên của lớp ở thuộc tính private. Khi đó chỉ có phương thức thành viên của lớp truy cập
được giá trị của biến. C# xem
thuộc tính private là mặc định nên trong ví dụ trên ta không khai báo thuộc tính truy cập
cho 6 biến nên mặc định chúng là private, tương tự:
//Các
biến thành viên
private
int Nam;
private
int Thang;
private
int Ngay;
private
int Gio;
private
int Phut;
private
int Giay;
Do lớp Tester và phương thức thành viên ThoiGianHienHanh của lớp ThoiGian
được khai báo
là public nên bất kỳ lớp nào cũng có thể truy cập
được.
Trong các ngôn ngữ lập trình thì
tham số và đối mục được xem là như nhau, cũng tương tự khi đang nói về ngôn ngữ hướng
đối tượng thì ta gọi một hàm là một phương thức hay hành vi. Tất cả các tên này đều tương đồng với nhau.
Một phương thức có thể lấy bất kỳ số lượng tham số
nào, các tham số này theo sau bởi tên của phương thức và được bao bọc bên
trong dấu ngoặc tròn (). Mỗi tham số phải khai báo kèm với kiểu dữ liệu. ví dụ ta có một
khai báo định
nghĩa một
phương thức có
tên là Method, phương thức không trả về giá trị nào cả (khai
báo giá trị trả về là void), và có hai tham số là một kiểu int và button:
void
Method (int param1, button param2)
{
//...
}
Bên trong thân của phương thức, các tham số
này được xem
như những biến cục bộ, giống như là ta khai báo biến bên trong phương thức và khởi tạo giá trị bằng giá trị của tham số truyền vào.
Ví dụ sau minh họa việc truyền tham số vào một
phương thức,
trong trường hợp này thì hai tham số của kiểu là int
và float.
using
System;
public
class Class1
{
public void SomeMethod(int p1, float p2)
{
Console.WriteLine(“Ham nhan duoc hai tham so: {0} va
{1}”, p1,p2);
}
}
public
class Tester
{
static void Main()
{
int var1 = 5;
float var2 = 10.5f;
Class1 c = new Class1();
c.SomeMethod(var1, var2 );
}
}
------------------------------------------------------------------
Kết
quả:
Ham
nhan duoc hai tham so: 5 va 10.5
------------------------------------------------------------------
Phương thức SomeMethod sẽ lấy hai tham
số int và float rồi hiển thị chúng ta
màn hình bằng việc dùng hàm Console.WriteLine(). Những tham số này có tên là p1 và p2 được xem như là biến cục bộ bên trong của phương thức.
Trong phương thức gọi Main, có hai biến cục bộ được tạo ra là
var1 và var2. Khi hai biến
này được truyền cho phương thức SomeMethod thì chúng được ánh xạ thành hai tham số p1 và p2 theo thứ tự danh sách
biến đưa vào.
Trong phần trước đã đề cập đến sự khác nhau giữa
kiểu dữ liệu giá trị và
kiểu dữ liệu tham chiếu.
Những kiểu dữ liệu chuẩn của C# như int, char, float,… là những kiểu dữ liệu giá trị, và
các biến được tạo ra từ các kiểu dữ liệu này
được lưu
trên stack. Tuy nhiên, với
các đối tượng kiểu dữ liệu tham chiếu thì được tạo ra trên
heap, sử dụng từ khóa new để tạo một đối tượng:
ThoiGian t = new ThoiGian();
t thật sự không chứa giá trị của đối tượng ThoiGian, nó chỉ chứa địa chỉ của đối tượng được tạo ra trên
heap, do vậy t chỉ chứa tham chiếu
đến một đối tượng
mà thôi.
Thử xem lại ví dụ minh họa ở trên, câu lệnh tạo một đối
tượng cho lớp ThoiGian tương tự như việc gọi thực hiện một
phương thức:
ThoiGian t = new ThoiGian();
Đúng như vậy, một
phương thức sẽ được gọi thực hiện khi chúng ta tạo một đối tượng. Phương thức này được gọi là bộ khởi dựng
(constructor). Các phương thức này được định
nghĩa khi xây dựng lớp, nếu ta không tạo
ra thì CLR sẽ thay mặt chúng ta tạo phương thức khởi dựng một cách mặc
định. Chức năng của bộ khởi dựng là tạo ra
đối tượng được xác định bởi một lớp và đặt
trạng thái này hợp lệ. Trước
khi bộ khởi dựng được thực hiện thì đối tượng
chưa được
cấp phát trong bộ nhớ. Sau khi bộ
khởi dựng thực hiện hoàn thành
thì bộ nhớ sẽ lưu giữ
một thể hiện hợp lệ của lớp vừa khai báo.
Lớp ThoiGian trong ví dụ trên không định nghĩa bộ khởi dựng. Do không định nghĩa nên trình biên dịch sẽ cung cấp một bộ khởi dựng cho chúng ta. Phương thức khởi dựng mặc định được tạo ra cho một đối tượng sẽ không thực hiện bất cứ hành động
nào, tức là bên trong
thân của phương
thức rỗng. Các biến thành viên được khởi tạo các giá trị thường
như thuộc tính
nguyên có giá trị là 0 và
chuỗi thì khởi tạo rỗng,..Bảng 4.2 sau tóm tắt các giá trị mặc định được gán cho các kiểu dữ liệu cơ
bản.
Bảng 1.14. Giá trị mặc định của kiểu dữ
liệu cơ bản
Kiểu dữ liệu
|
Giá trị mặc định
|
int, long, byte,…
|
0
|
bool
|
false
|
char
|
‘\0’ (null)
|
enum
|
0
|
reference
|
null
|
Thường thường,
khi muốn định nghĩa một phương thức khởi dựng riêng
ta phải cung cấp các tham số để hàm
khởi dựng có thể khởi tạo các giá trị khác ngoài giá trị mặc định cho các đối tượng.
Để định
nghĩa một bộ khởi dựng riêng
ta phải khai báo một phương thức có tên giống như tên lớp đã khai báo. Phương thức khởi dựng không có giá trị trả về và
được khai báo
là public. Nếu
phương thức khởi dựng này được truyền tham
số thì phải khai báo danh sách tham số giống như khai báo với bất kỳ phương thức nào trong một lớp. Ví dụ sau minh họa một bộ khởi dựng riêng, phương phức khởi dựng này sẽ nhận một tham số là một đối
tượng kiểu DateTime do C# cung cấp.
Ví
dụ: Định nghĩa một bộ khởi dựng.
------------------------------------------------------------------
using
System;
public
class ThoiGian
{
public void ThoiGianHienHanh()
{
Console.WriteLine(“ Thoi gian hien
hanh la : {0}/{1}/{2} {3}:{4}:{5}”, Ngay, Thang, Nam, Gio, Phut, Giay);
}
// Hàm khởi dựng
public ThoiGian( System.DateTime
dt )
{
Nam = dt.Year;
Thang = dt.Month;
Ngay = dt.Day;
Gio = dt.Hour;
Phut = dt.Minute;
Giay = dt.Second;
}
//Biến thành viên
private int Nam;
int Thang; int Ngay; int Gio; int
Phut; int Giay;
}
public class Tester
{
static void Main()
{
System.DateTime currentTime = System.DateTime.Now;
ThoiGian t = new ThoiGian( currentTime ); t.ThoiGianHienHanh();
}
}
------------------------------------------------------------------
Kết
quả:
Thoi
gian hien hanh la: 5/6/2002 9:10:20
------------------------------------------------------------------
Trong ví dụ trên phương thức khởi dựng lấy một đối tượng DateTime và khởi tạo tất cả các biến
thành viên dựa trên giá
trị của đối tượng
này. Khi phương thức này thực hiện xong, một đối
tượng ThoiGian
được tạo ra và các biến của đối
tượng cũng
đã được khởi tạo. Hàm ThoiGianHienHanh được gọi trong
hàm Main() sẽ hiển thị giá trị thời gian lúc đối tượng được tạo ra.
Chúng ta thử bỏ một số lệnh khởi tạo trong phương thức khởi dựng và cho thực hiện
chương trình lại
thì các biến không
được khởi tạo sẽ có giá trị mặc định là
0, do là biến nguyên. Một biến thành viên kiểu nguyên sẽ
được thiết lập giá trị là 0
nếu chúng ta không gán nó
trong phương thức khởi dựng. Chú ý rằng kiểu dữ liệu giá trị
không thể không
được khởi tạo, nếu ta
không khởi tạo thì trình biên dịch sẽ cung cấp các
giá trị mặc định theo bảng
4.2.
Ngoài ra trong chương trình 4.3
trên có sử dụng đối tượng của lớp DateTime, lớp
DateTime này được cung cấp bởi thư viện System, lớp
này cũng cung cấp
các biến thành viên
public như: Year, Month, Day, Hour, Minute, và Second tương tự như lớp ThoiGian của
chúng ta.
Thêm vào đó là lớp này có đưa ra một phương thức thành viên tĩnh tên là Now, phương thức Now sẽ trả về một tham chiếu
đến một thể hiện của một đối
tượng DateTime
được khởi tạo với thời gian hiện hành.
Theo như trên khi lệnh :
System.DataTime currentTime =
System.DateTime.Now();
được thực hiện thì phương thức tĩnh Now() sẽ tạo ra một
đối tượng DateTime trên bộ nhớ heap và trả về một tham chiếu
và tham chiếu này
được gán cho biến đối tượng
currentTime. Sau khi đối tượng
currentTime được
tạo thì câu lệnh tiếp theo sẽ thực hiện việc truyền đối tượng
currentTime cho phương thức khởi dựng để tạo một đối tượng
ThoiGian:
ThoiGian t = new ThoiGian( currentTime );
Bên trong phương thức khởi dựng này
tham số dt sẽ tham chiếu đến
đối tượng DateTime là đối tượng vừa tạo mà currentTime cũng tham chiếu. Nói cách khác lúc này tham số dt và currentTime cùng tham chiếu đến một đối tượng DateTime trong bộ nhớ. Nhờ vậy phương thức khởi dựng ThoiGian có thể truy cập được các biến
thành viên public của
đối tượng DateTime được tạo trong hàm Main().
Có một sự nhấn mạnh ở đây
là đối tượng DateTime được truyền cho bộ dựng ThoiGian chính là đối tượng đã được tạo trong
hàm Main và là kiểu dữ liệu tham chiếu.
Do vậy khi thực hiện truyền tham
số là một kiểu dữ liệu tham chiếu thì con trỏ
được ánh xạ qua chứ hoàn toàn không có một đối
tượng nào
được sao chép lại.
Khởi
tạo biến thành viên
Các biến thành viên có thể được
khởi tạo trực tiếp khi
khai báo trong quá trình khởi tạo, thay vì
phải thực hiện việc khởi tạo các biến
trong bộ khởi dựng. Để thực hiện việc khởi tạo chỉ việc sử dụng phép gán giá trị cho một biến:
private int Giay = 30; // Khởi tạo
Việc khởi tạo biến thành viên sẽ rất có ý
nghĩa, vì khi xác định giá trị khởi tạo như vậy
thì biến sẽ không nhận giá trị mặc định mà trình biên dịch cung cấp.
Khi đó nếu các biến này không được gán lại trong các phương thức khởi dựng thì nó sẽ có giá trị mà
ta đã khởi tạo. Ví dụ sau minh họa
việc khởi tạo biến thành
viên khi khai báo. Trong ví dụ này sẽ có hai
bộ dựng ngoài bộ dựng mặc định mà trình biên dịch cung cấp, một bộ dựng thực hiện việc gán giá
trị cho tất cả các biến
thành viên, còn bộ dựng thứ hai thì cũng tương tự nhưng sẽ
không gán giá trị cho biến Giay.
Ví dụ: Minh hoạ sử
dụng khởi tạo biến thành viên.
------------------------------------------------------------------
public class ThoiGian
{
public void ThoiGianHienHanh()
{
System.DateTime now =
System.DateTime.Now; System.Console.WriteLine(“\n Hien tai: \t {0}/{1}/{2}
{3}:{4}:{5}”,
now.Day, now.Month, now.Year,
now.Hour, now.Minute, now.Second); System.Console.WriteLine(“ Thoi Gian:\t
{0}/{1}/{2} {3}:{4}:{5}”,
Ngay, Thang, Nam, Gio, Phut,
Giay);
}
public ThoiGian( System.DateTime
dt)
{
Nam = dt.Year;
Thang = dt.Month;
Ngay = dt.Day;
Gio = dt.Hour;
Phut = dt.Minute;
Giay = dt.Second; // có gán cho biến thành viên Giay
}
public ThoiGian(int Year, int
Month, int Date, int Hour, int Minute)
{
Nam = Year;
Thang = Month;
Ngay = Date;
Gio = Hour;
Phut = Minute;
}
private int Nam;
private int Thang;
private int Ngay;
private int Gio;
private int Phut;
private int Giay = 30 ; // biến
được khởi tạo.
}
public class Tester
{
static void Main()
{
System.DateTime currentTime =
System.DateTime.Now;
ThoiGian t1 = new
ThoiGian(currentTime );
t1.ThoiGianHienHanh();
ThoiGian t2 = new
ThoiGian(2001,7,3,10,5);
t2.ThoiGianHienHanh();
}
}
------------------------------------------------------------------
Kết quả:
Hien tai:
|
5/6/2002
|
10:15:5
|
Thoi Gian:
|
5/6/2002
|
10:15:5
|
Hien
|
tai:
|
5/6/2002
|
10:15:5
|
Thoi
|
Gian:
|
3/7/2001
|
10:5:30
|
------------------------------------------------------------------
Nếu không khởi tạo giá trị của biến thành viên thì bộ khởi dựng mặc định sẽ khởi tạo giá trị là 0
mặc định cho biến thành viên có kiểu nguyên. Tuy nhiên, trong trường hợp này biến thành viên Giay được khởi tạo giá trị 30:
Giay
= 30; // Khởi tạo
Trong trường hợp bộ khởi tạo thứ hai không truyền giá trị cho biến Giay
nên biến này vẫn lấy giá trị mà
ta đã khởi tạo ban đầu là 30:
ThoiGian
t2 = new ThoiGian(2001, 7, 3, 10, 5); t2.ThoiGianHienHanh();
Ngược lại, nếu một giá trị
được gán cho biến Giay như trong bộ khởi tạo thứ nhất thì giá trị
mới này sẽ được chồng lên
giá trị khởi tạo.
Trong ví dụ trên lần
đầu tiên tạo đối tượng
ThoiGian do ta truyền vào
đối tượng DateTime nên hàm khởi dựng thứ nhất được thực hiện, hàm
này sẽ gán giá trị 5 cho biến Giay. Còn khi tạo đối
tượng ThoiGian thứ hai, hàm khởi dựng thứ hai được thực hiện, hàm
này không gán giá trị cho
biến Giay nên biến này vẫn còn lưu giữ lại giá trị 30 khi khởi tạo ban
đầu.
Bộ
khởi dựng sao chép
Bộ khởi dựng sao chép thực hiện việc tạo một đối
tượng mới bằng cách sao chép tất cả các biến từ một đối tượng đã có và cùng một kiểu dữ liệu. Ví dụ chúng
ta muốn đưa một đối tượng
ThoiGian vào bộ khởi dựng lớp
ThoiGian để tạo một đối
tượng ThoiGian mới có cùng giá trị với đối
tượng ThoiGian
cũ. Hai đối
tượng này hoàn toàn
khác nhau và chỉ giống nhau ở giá trị biến thành viên sao khi khởi dựng.
Ngôn ngữ C# không cung cấp bộ khởi dựng sao chép, do đó chúng ta phải tự tạo ra. Việc sao chép các thành phần từ một đối tượng
ban đầu cho một đối tượng mới như sau:
public
ThoiGian(ThoiGian tg)
{
Nam = tg.Nam;
Thang = tg.Thang;
Ngay = tg.Ngay;
Gio = tg.Gio;
Phut = tg.Phut;
Giay = tg.Giay;
}
Khi đó ta có thể sao chép từ một đối tượng
ThoiGian đã hiện hữu như sau:
ThoiGian
t2 = new ThoiGian( t1 );
Trong đó t1 là đối tượng ThoiGian đã tồn tại, sau khi
lệnh trên thực hiện xong thì đối tượng
t2 được tạo ra như bản sao của đối
tượng t1.
Từ khóa this được dùng để
tham chiếu đến thể hiện hiện hành của một đối tượng. Tham chiếu
this này được
xem là con trỏ ẩn đến tất các
phương thức
không có thuộc tính
tĩnh trong một lớp. Mỗi phương thức có thể tham
chiếu đến những phương thức khác và các biến thành viên thông qua tham chiếu this này.
Tham chiếu this này được sử dụng thường xuyên theo ba cách:
- Sử dụng khi các biến
thành viên bị che lấp bởi tham số
đưa vào, như trường hợp sau:
public
void SetYear(int Nam)
{
this.Nam = Nam;
}
Như trong đoạn mã trên phương thức SetYear sẽ thiết lập giá trị của biến thành viên Nam, tuy nhiên do tham số đưa vào có tên là Nam,
trùng với biến thành viên, nên ta phải dùng tham chiếu this để xác định
rõ các biến thành viên và
tham số được truyền vào. Khi đó this.Nam chỉ đến biến thành viên của đối tượng,
trong khi Nam chỉ đến tham số.
- Sử dụng tham chiếu
this để truyền đối tượng
hiện hành vào một tham số của một phương thức của đối
tượng khác:
public
void Method1(OtherClass otherObject)
{
//Sử dụng tham chiếu this để
truyền tham số là bản
//thân đối tượng đang thực
hiện.
otherObject.SetObject(this);
}
Như trên cho thấy khi cần truyền một tham số là
chính bản thân của đối tượng
đang thực hiện thì ta bắt buộc phải dùng tham chiếu this để truyền.
- Cách thứ ba sử dụng tham chiếu this là mảng
chỉ mục (indexer).
Những thuộc tính
và phương thức
trong một lớp có thể là những
thành viên thể hiện (instance members) hay những thành viên tĩnh (static
members). Những thành
viên thể hiện hay thành viên của đối tượng
liên quan đến thể hiện của một kiểu dữ liệu. Trong khi thành viên tĩnh
được xem
như một phần của lớp. Chúng
ta có thể truy cập đến thành viên tĩnh của một lớp thông qua tên lớp đã được khai báo. Ví dụ chúng ta có một lớp tên là Button và có hai thể hiện của lớp tên là btnUpdate và btnDelete. Và giả sử lớp Button này có một phương thức tĩnh là Show(). Để truy cập phương thức tĩnh này ta viết :
Button.Show();
Đúng hơn là viết:
btnUpdate.Show();
@Ghi chú: Trong ngôn ngữ C# không cho phép truy cập đến các
phương thức
tĩnh và các biến
thành viên tĩnh thông qua một thể hiện, nếu chúng ta cố
làm điều đó thì
trình biên dịch C# sẽ báo lỗi, điều
này khác với ngôn ngữ C++.
Trong một số ngôn ngữ thì có sự phân chia giữa
phương thức của lớp và các phương thức khác (toàn cục) tồn tại bên ngoài không phụ thuộc bất cứ một lớp nào. Tuy
nhiên, điều này
không cho phép trong C#, ngôn ngữ C# không cho phép tạo các phương thức bên ngoài của
lớp, nhưng ta có thể tạo được các phương thức giống
như vậy bằng cách tạo các phương thức tĩnh bên trong một lớp.
Phương thức tĩnh hoạt động ít
nhiều giống như phương thức toàn cục, ta truy cập
phương thức này
mà không cần phải tạo bất cứ thể hiện hay
đối tượng của lớp chứa phương thức toàn cục. Tuy nhiên, lợi ích của
phương thức
tĩnh vượt xa
phương thức
toàn cục vì
phương thức
tĩnh được
bao bọc trong phạm vi của một lớp nơi nó được định nghĩa, do vậy ta sẽ không
gặp tình trạng lộn xộn giữa các phương thức trùng tên do chúng được đặt trong namespace.
@Ghi chú: Chúng ta không nên bị cám dỗ bởi việc tạo ra một lớp chứa toàn bộ các phương thức linh tinh. Điều này có thể tiện cho công
việc lập trình nhưng sẽ điều không mong muốn và giảm tính
ý nghĩa của việc thiết kế hướng đối tượng.
Vì đặc tính của việc tạo các
đối tượng là xây dựng các phương thức và hành vi xung quanh các thuộc tính hay dữ
liệu của đối tượng.
Như chúng ta đã biết phương thức Main() là một phương thức tĩnh. Phương tĩnh được xem như là phần hoạt động của lớp hơn là của thể hiện một lớp. Chúng
cũng không cần có một tham chiếu this hay bất
cứ thể hiện nào tham chiếu tới.
Phương thức tĩnh không thể truy cập trực tiếp đến các
thành viên không có tính chất tĩnh (nonstatic). Như vậy Main() không thể gọi một phương thức không tĩnh bên trong lớp. Ta xem lại đoạn
chương trình minh họa:
using
System;
public
class Class1
{
public void SomeMethod(int p1, float p2)
{
Console.WriteLine(“Ham nhan duoc hai tham so: {0} va
{1}”, p1,p2);
}
}
public class Tester
{
static void Main()
{
int var1 = 5;
float var2 = 10.5f;
Class1 c = new Class1();
c.SomeMethod( var1, var2 );
}
}
Phương thức SomeMethod() là phương thức không tĩnh của lớp Class1,
do đó để truy cập được phương thức của lớp này cấn phải tạo một thể hiện là một đối tượng
cho lớp Class1.
Sử dụng bộ
khởi dựng tĩnh
Nếu một lớp khai báo một bộ khởi tạo tĩnh (static constructor), thì được đảm bảo rằng phương thức khởi dựng
tĩnh này sẽ
được thực hiện trước bất cứ thể hiện nào của lớp
được tạo ra.
@Ghi chú: Chúng ta không thể điều khiển chính xác khi nào thì
phương thức khởi dựng tĩnh được thực hiện. Tuy nhiên ta biết chắc rằng nó sẽ được thực hiện sau khi chương trình chạy và trước bất kì biến đối tượng
nào được tạo ra.
Theo ví dụ trên có thể
thêm một bộ khởi dựng
tĩnh cho lớp
ThoiGian:
static
ThoiGian()
{
Ten = “Thoi gian”;
}
Lưu ý rằng ở đây
không có bất cứ thuộc tính truy cập
nào như public trước bộ khởi dựng tĩnh. Thuộc tính truy cập
không cho phép theo sau một phương thức khởi dựng tĩnh. Do phương thức tĩnh nên không thể truy cập bất cứ biến thành viên không thuộc loại
tĩnh, vì vậy biến thành viên Name bên trên cũng
phải được khai báo là tĩnh:
private static string Ten;
Cuối cùng ta thêm một dòng vào phương thức ThoiGianHienHanh() của lớp
ThoiGian:
public
void ThoiGianHienHanh()
{
System.Console.WriteLine(“
Ten: {0}”, Ten); System.Console.WriteLine(“ Thoi Gian:\t {0}/{1}/{2}
{3}:{4}:{5}”, Ngay, Thang, Nam, Gio, Phut, Giay);
}
Sau khi thay đổi ta biên dịch
và chạy chương
trình được kết quả sau:
Ten:
Thoi Gian
Thoi
Gian: 5/6/2002 18:35:20
Mặc dù chương trình thực hiện tốt, nhưng không cần thiết phải tạo ra bộ khởi dựng tĩnh để phục vụ cho mục đích này. Thay vào đó ta
có thể dùng chức năng khởi tạo biến thành
viên như sau:
private
static string Ten = “Thoi Gian”;
Tuy nhiên, bộ khởi tạo tĩnh có hữu dụng khi chúng ta cần cài đặt
một số công việc mà không thể
thực hiện được thông qua chức năng khởi dựng và công việc
cài đặt này chỉ được thực hiện duy nhất một lần.
Sử dụng bộ
khởi dựng private
Như đã nói ngôn ngữ C# không có phương thức toàn cục và hằng số toàn cục. Do vậy chúng
ta có thể tạo ra những lớp tiện ích nhỏ chỉ để chứa các phương thức tĩnh. Cách thực hiện này
luôn có hai mặt tốt và không tốt. Nếu chúng
ta tạo một lớp tiện ích
như vậy và không muốn bất cứ một thể hiện nào
được tạo ra. Để ngăn ngừa
việc tạo bất cứ thể hiện của lớp ta tạo ra bộ khởi dựng không có tham số và không làm gì cả, tức là bên
trong thân của
phương thức rỗng, và thêm vào đó
phương thức này
được đánh
dầu là private. Do không
có bộ khởi dựng public, nên không thể tạo ra bất cứ thể hiện nào của lớp.
Sử dụng các thuộc
tính tĩnh
Một vấn đề đặt ra là làm sao kiểm soát được số thể hiện của một lớp được tạo ra khi
thực hiện chương trình. Vì hoàn
toàn ta không thể tạo được biến toàn cục
để làm công việc đếm số thể hiện của một lớp.
Thông thường các biến
thành viên tĩnh được dùng để
đếm số thể hiện đã
được
được tạo ra của một lớp.
Thông thường khi xây dựng
các lớp, ta có mong muốn là tạo ra nhiều hàm
có cùng tên. Cũng như hầu hết trong
các ví dụ trước thì các lớp điều có
nhiều hơn một phương thức khởi dựng.
Như trong lớp Time
có các phương thức khởi dựng nhận các tham số
khác nhau, như tham số là đối
tượng DateTime, hay
tham số có thể được tùy chọn
để thiết lập các giá trị
của các biến thành viên thông qua các tham số nguyên. Tóm lại khi xây dựng nhiều các phương thức cùng tên nhưng nhận các tham số khác nhau. Chức năng này được gọi là nạp chồng phương thức.
Một ký hiệu
(signature) của một phương thức được định nghĩa như tên của phương thức cùng với
danh sách tham số của phương thức. Hai phương thức khác nhau khi ký hiệu của chúng khác là khác nhau tức là khác nhau khi tên phương thức khác nhau hay danh sách tham số khác nhau. Danh sách tham số được
xem là khác nhau bởi số lượng các tham số
hoặc là kiểu dữ liệu của tham số. Ví dụ
đoạn mã sau,
phương thức thứ nhất khác phương thức thứ hai do số lượng tham số
khác nhau. Phương thức thứ hai khác
phương thức thứ ba do kiểu dữ liệu tham số khác nhau:
void
myMethod( int p1 );
void
myMethod( int p1, int p2 );
void
myMethod( int p1, string p2 );
Một lớp có thể có bất cứ số lượng phương thức nào, nhưng mỗi phương thức trong lớp phải có ký hiệu khác với tất cả các phương thức thành viên còn lại của lớp.
Thuộc tính là khái niệm cho phép truy cập trạng thái của lớp thay vì thông qua truy cập trực tiếp các biến thành viên, nó sẽ đựơc
thay thế bằng việc thực thi
truy cập thông qua
phương thức của lớp.
Đây thật sự là một điều lý tưởng.
Các thành phần bên ngoài
(client) muốn truy cập trạng thái của một đối tượng
và không muốn làm việc với những
phương thức.
Tuy nhiên, người thiết kế lớp muốn dấu trạng thái
bên trong của lớp mà anh ta xây dựng, và cung cấp một cách
gián tiếp thông qua một phương thức.
Thuộc tính là một
đặc tính mới được giới thiệu trong
ngôn ngữ C#. Đặc tính này cung cấp khả năng bảo
vệ các trường dữ liệu bên
trong một lớp bằng việc đọc và viết chúng thông qua thuộc tính. Trong ngôn ngữ khác, điều này có thể
được thực hiện thông qua việc tạo các
phương thức lấy dữ liệu (getter
method) và phương thức thiết lập dữ liệu (setter
method).
Thuộc tính được thiết kế nhắm vào hai mục
đích: cung cấp một giao diện đơn cho phép truy cập các biến
thành viên, Tuy nhiên cách thức thực thi
truy cập giống như phương thức trong đó các dữ liệu được che dấu,
đảm bảo cho yêu cầu thiết kế hướng đối
tượng. Để hiểu rõ đặc
tính này ta sẽ xem ví dụ bên dưới:
Ví
dụ: Sử dụng thuộc tính.
------------------------------------------------------------------
using
System;
public
class Time
{
public void DisplayCurrentTime()
{
Console.WriteLine(“Time\t:
{0}/{1}/{2} {3}:{4}:{5}”,
date, month, year, hour, minute,
second);
}
public Time( System.DateTime dt)
{
year = dt.Year;
month = dt.Month;
date = dt.Day;
hour = dt.Hour;
minute = dt.Minute;
second = dt.Second;
}
public int Hour
{
get
{
return hour;
}
set
{
hour = value;
}
}
//Biến thành viên
private int year; private int
month; private int date;
private int hour; private int
minute; private int second;
}
public class Tester
{
static void Main()
{
System.DateTime currentTime =
System.DateTime.Now;
Time t = new Time( currentTime );
t.DisplayCurrentTime();
// Lấy dữ liệu từ
thuộc tính Hour
int theHour = t.Hour;
Console.WriteLine(“ Retrieved the
hour: {0}”, theHour);
theHour++;
t.Hour = theHour;
Console.WriteLine(“Updated the
hour: {0}”, theHour);
}
}
------------------------------------------------------------------
:
Kết quả:
Time : 2/1/2003 17:55:1
Retrieved
the hour: 17
Updated
the hour: 18
------------------------------------------------------------------
Để khai báo thuộc
tính, đầu tiên là
khai báo tên thuộc tính
để truy cập, tiếp theo là phần
thân định nghĩa
thuộc tính nằm trong cập dấu ({}).
Bên trong thân của thuộc tính là khai báo hai bộ truy cập lấy và thiết lập dữ liệu:
public
int Hour
{
get
{
return hour;
}
set
{
hour = value;
}
}
Mỗi bộ truy cập được khai báo riêng biệt để làm hai công việc khác nhau là lấy hay thiết lập giá trị cho thuộc
tính. Giá trị thuộc tính có thể được
lưu trong cơ sở
dữ liệu, khi đó trong phần thân của bộ truy cập sẽ thực hiện các công việc tương tác với cơ sở dữ lịêu. Hoặc là
giá trị thuộc tính được lưu trữ trong các biến
thành viên của lớp như trong ví dụ:
private int hour;
Truy cập lấy dữ
liệu (get accessor)
Phần khai báo tương tự như một
phương thức của lớp dùng để
trả về một đối
tượng có kiểu dữ liệu của thuộc tính. Trong ví dụ trên, bộ truy
cập lấy dữ liệu get của thuộc tính Hour cũng tương tự như một
phương thức trả về một giá trị int. Nó trả về giá trị của biến thành
viên hour nơi mà giá trị của thuộc tính Hour lưu trữ:
get
{
return hour;
}
Trong ví dụ này, một biến thành viên cục bộ được
trả về, nhưng nó cũng có thể truy cập dễ dàng một giá trị nguyên từ
cơ sở dữ lịêu, hay thực
hiện việc tính toán tùy ý.
Bất cứ khi nào
chúng ta tham chiếu
đến một thuộc tính hay là gán giá trị thuộc tính
cho một biến thì bộ truy cập lấy dữ liệu get sẽ được thực hiện để đọc giá
trị của thuộc tính:
Time
t = new Time( currentTime );
int
theHour = t.Hour;
Khi lệnh thứ hai
được thực hiện thì giá trị
của thuộc tính sẽ được
trả về, tức là bộ truy cập lấy dữ lịêu get sẽ được
thực hiện và kết quả là giá
trị của thuộc tính được gán cho biến
cục bộ theHour.
Bộ truy cập thiết
lập dữ liệu ( set accessor)
Bộ truy cập này
sẽ thiết lập một giá trị mới cho thuộc
tính và tương tự như một
phương thức trả về một giá trị void. Khi định nghĩa bộ truy cập thiết lập dữ lịêu chúng ta
phải sử dụng từ khóa
value để đại diện cho tham số
được truyền vào và được lưu trữ bởi thuộc tính:
set
{
hour = value;
}
Như đã nói trước, do ta đang khai báo thuộc tính lưu trữ dưới dạng biến thành viên nên trong phần thân của bộ truy cập ta chỉ sử dụng biến thành viên mà thôi. Bộ truy cập thiết lập hoàn toàn cho phép chúng ta có thể viết giá trị vào trong cơ sở dữ lịêu hay cập nhật bất cứ biến thành viên nào khác của lớp nếu cần thiết.
Khi chúng ta gán một giá trị cho
thuộc tính thì bộ truy cập thiết lập dữ liệu set sẽ được tự động thực hiện và một tham số ngầm định sẽ được
tạo ra để lưu giá trị mà ta muốn gán:
theHour++;
t.Hour
= theHour;
Lợi ích của
hướng tiếp cận này cho phép các thành phần bên ngoài (client) có thể tương tác với thuộc tính một cách trực tiếp, mà
không phải hy sinh việc che dấu dữ lịêu cũng như đặc tính đóng gói dữ lịêu trong thiết
kế hướng đối tượng.