- Khái niệm lớp
- Định nghĩa lớp
- Cú pháp, ví dụ
- Tạo và sử dụng đối tượng
- Hàm khởi tạo
- Quá tải phương thức
- Tìm hiểu tính đóng gói
- Thuộc tính trong lớp
Phần này bắt đầu nghiên cứu, triển khai các vấn đề kỹ thuật liên quan đến Lập trình hướng đối tượng.
Lập trình hướng đối tượng (Object-oriented programming - OOP), là kỹ thuật lập trình mà điều cốt yếu cần trừu tượng hóa các vấn đề thành các đối tượng (đối tượng có dữ liệu và các ứng xử). Kỹ thuật OOP có 4 tính chất
- Tính trừu tượng (abstraction) mô tả một cách tổng quát hóa (tập trung vào thông tin cần thiết) mà không chi tiết thông tin về các đối tượng, không gắn cứng với một đối tượng cụ thể cần mô tả (triển khai với interface, abstract)
- Tính đóng gói
(encapsulation) dữ liệu đối tượng cố gắng như nằm trong một
hộp đen, các thành phần khác bên ngoài đối tượng không trực tiếp tác động đến dữ liệu
(bên ngoài truy cập, tác động thông qua các phương thức public cho phép, qua setter, getter ...) - Tính đa hình (polymorphism) đối tượng ứng xử khác nhau tùy trường hợp cụ thể
- Tính kế thừa (inheritance) đặc tính của lớp được kế thừa từ một lớp khác
Khái niệm chung về lớp trong C#
Trong lập trình hướng đối tượng, lớp (class
) là một kiểu dữ liệu tham chiếu
nó định nghĩa một tập hợp các biến (trường dữ liệu, thuộc tính) và phương thức
(gọi chúng là các member - thành viên lớp).
Từ lớp đó sinh ra các đối tượng (object
), các đối tượng này còn gọi là
bản triển khai của lớp (instance of a class
),
mỗi đối tượng có giá trị dữ liệu cụ thể (lưu trong thành viên biến, thuộc tính).
Các phương thức (method) - định nghĩa ra các ứng xử của đối tượng - dựa theo dữ liệu của chúng.
Ví dụ: Một lớp mô tả các loại vũ khí, đặt tên là Vukhi
,
thì bên trong nó có thể định nghĩa các thành viên dữ liệu (biến) như: tên vũ khí,
độ sát thương, tầm ảnh hưởng ...
Cũng có thể định nghĩa các thành viên hàm (phương thức) để mô tả ứng xử của nó như:
Ten()
, TanCong()
...
Khi có định nghĩa lớp VuKhi
nghĩa là bạn có một kiểu dữ liệu tham chiếu mới,
bạn có thể tạo ra đối tượng cụ thể của nó
mà mỗi đối tượng có những dữ liệu khác nhau như sung_luc
, sung_may
...
Khai báo lớp, sử dụng lớp
Cú pháp, ví dụ khai báo lớp
Khi giải quyết các vấn đề thực tiễn, các vấn đề cần giải quyết bạn cần tìm cách trừu tượng hóa nó thành vấn đề tổng quát, như nó có các đặc tính gì, ứng xử của nó ra sao ... Từ đó mới có thể định nghĩa ra lớp để hiện thực hóa vấn đề trừu tượng được.
Cú pháp cơ bản như sau:
<Access Modifiers> class Class_Name { // khai báo các thành viên dữ liệu (thuộc tính, biến trường dữ liệu) // khai báo các thành viên hàm (phương thức) }
Trong đó Access Modifiers
áp dụng khai báo cho lớp có thể là: public
(không giới hạn truy cập)
hoặc internal
(mặc định nếu không khai báo, giới hạn truy cập trong cùng assembly - chương trình).
Nếu lớp con khai báo lồng trong một lớp khác còn có thể sử dụng private
(chỉ truy cập được
từ lớp chứa nó)
Khai báo và khởi tạo thành viên dữ liệu (biến, trường dữ liếu) giống với khai báo biến thông thường,
nhưng có thêm Access Modifiers
để quy định cấp độ truy cập. Đối với thành viên lớp (biến, thuộc tính, phương thức)
thì có thể áp dụng các access modifiers sau:
- public : không giới hạn phạm vi truy cập
- protected : chỉ truy cập trong nội bộ lớp hay các lớp kế thừa
- private : (mặc định) chỉ truy cập được từ các thành viên của lớp chứa nó
- internal : chỉ truy cập được trong cùng assembly (dll, exe)
- protected internal: truy cập được khi cùng assembly hoặc lớp kế thừa
- private protected: truy cập từ lớp chứa nó, lớp kế thừa nhưng phải cùng assembly
Trở lại ví dụ trên, định nghĩa lớp VuKhi
// File VuKhi.cs using System; namespace CS007_Class { public class VuKhi { /// Tên của vũ khí: Súng Lục, Súng Trường, Dao ... public string name = "Tên Vũ Khí"; /// Độ sát thương 10 cấp độ int dosatthuong = 0; /// Phương thức khởi tạo (được gọi khi toán tử new tạo đối tượng) /// tên phương thức trùng tên lớp, trường hợp này không tham số public VuKhi() { this.dosatthuong = 1; } /// Phương thức khởi tạo (được gọi khi toán tử new tạo đối tượng) /// tên phương thức trùng tên lớp, trường hợp này có tham số public VuKhi(string name, int dosatthuong) { this.name = name; SetDoSatThuong(dosatthuong); } /// Hàm này thiết lập độ sát thương public void SetDoSatThuong(int mucdo) { this.dosatthuong = mucdo; } // In ra: Tên vu khí: * * * * * * * * (bằng độ sát thương) public void TanCong() { Console.Write(name + ": \t"); for (int i = 0; i < dosatthuong; i++) { Console.Write(" * "); } Console.WriteLine(); } } }
Lớp VuKhi
trên mô tả chung về loại vũ khí, nó có tên lưu ở trường dữ liệu name
,
có độ sát thương lưu ở trường (biến) dosatthuong
, nó có các phương thức SetDoSatThuong
để thiết lập độ sát thước, TanCong()
trừu tượng hóa khi vú khí đó tấn công.
Tạo và sử dụng đối tượng
Giờ ta tiến hành tạo ra các đối tượng cụ thể, áp dụng VuKhi
.
Như trên đã nói, class
là một kiểu dữ liệu, nên để sử dụng nó có
thể khai báo biến với kiểu dữ liệu là tên lớp do bạn định nghĩa.
Để tạo ra đối tượng lớp thì dùng từ khóa new
với cú pháp new ClassName();
// Khai báo và khởi tạo đối tượng luôn var ob1 = new ClassName(); // Khai báo, sau đó khởi tạo ClassName ob2; ob2 = new ClassName();
Toán tử .
Sau khi đối tượng lớp (object) được tạo, bạn có thể truy cập đến các thuộc tính, trường dữ liệu và phương thức
của đối tượng đó bằng ký hiệu . theo quy tắc
object.tên_thuộc_tính
hay object.tên_phương_thức
Áp dụng:
static void Main(string[] args) { var sungluc = new VuKhi(); // Khai báo và khởi tạo sungluc.name = "SÚNG LỤC"; // Truy cập và gán thuộc tính sungluc.SetDoSatThuong(5); // Truy cập (gọi) phương thức VuKhi sungtruong = new VuKhi(); sungtruong.name = "SÚNG TRƯỜNG"; sungtruong.SetDoSatThuong(20); sungluc.TanCong(); // Gọi phương thức sungtruong.TanCong(); // Gọi phương thức //Kết quả chạy SÚNG LỤC: * * * * * SÚNG TRƯỜNG: * * * * * * * * * * * * * * * * * * * * }
Từ khóa this
Từ khóa this
dùng trong các phương thức của lớp, nó tham chiếu đến đối tượng
hiện tại sinh ra từ lớp. Sử dụng this
để tường minh, tránh sự không rõ ràng
khi truy cập thuộc tính, phương thức hoặc để lấy đối tượng lớp làm tham số cho các thành phần khác ...
Ví dụ, hàm SetDoSatThuong
, bạn có thể viết:
public void SetDoSatThuong(int dosatthuong) { this.dosatthuong = dosatthuong; }
Nếu viết thiếu this
thì là dosatthuong = dosatthuong
, làm cho khó
hiểu không biết là gán dosatthuong
từ tham số hàm cho dosatthuong
là dữ liệu của lớp. Các trường hợp khác, không gây ra sự bối rối kiểu này thì có thể bỏ this
(tuy nhiên nên sử dụng this ngay kể cả khi bỏ được nó để giúp ích quá trình bảo trì sau này)
Phương thức khởi tạo - Constructor
Phương thức khởi tạo là phương thức của lớp, nó được thi hành ngay khi đối tượng được tạo
(bởi toán tử new
), phương thức khởi tạo có tên trùng với tên của lớp,
không có kiểu trả về, bạn có thể tạo nhiều phương thức khởi tạo - các phương thức này đều cùng tên với
tên lớp nhưng tham số khác nhau. Lúc này khi khởi tạo đối tượng với toán tử new
tùy tham
số khởi tạo mà nó sẽ gọi phương thức khởi tạo tương ứng.
Ví dụ trên, lớp VuKhi
có hai phương thức khởi tạo:
public class VuKhi { /... public VuKhi() { this.dosatthuong = 1; } public VuKhi(string name, int dosatthuong) { this.name = name; SetDoSatThuong(dosatthuong); } //... }
Lúc này có thể sử như sau:
// Khởi tạo đối tượng, hàm tạo VuKhi() được gọi var sungluc = new VuKhi(); sungluc.name = "SÚNG LỤC"; sungluc.SetDoSatThuong(5); // Khởi tạo đối tượng, hàm tạo VuKhi(name, dosatthuong) được gọi VuKhi sungtruong = new VuKhi(name: "SÚNG TRƯỜNG", dosatthuong: 20);
Kết quả tương tự như ví dụ trên, việc sử dụng hàm khởi tạo đảm bảo dữ liệu của đối tượng bắt buộc phải khởi tạo ngay khi đối tượng đó được tạo - tránh việc sử dụng đối tượng mà dữ liệu không chính xác.
Quá tải (Overloading) phương thức
Kỹ thuật quá tải phương thức (Method Overloading) là cách thức triển khai khái niệm tính đa hình của lập trình hướng đối tượng. Quá tải phương thức là các phương thức có cùng tên nhưng tham số khác nhau (hàm có thể trả về kiểu dữ liệu khác nhau)
Tính đa hình (polymorphism) là cách ứng xử của đối tượng - ứng xử này là khác nhau tùy thuộc vào tình huống cụ thể.
Ví dụ lớp Console
của NET CORE
(mã nguồn
Console.cs) nó quá chồng một loạt phương thức ví dụ:
public static void WriteLine(); public static void WriteLine(bool value); public static void WriteLine(decimal value); public static void WriteLine(int value); ...
Điều này giúp cho bạn khi bạn gọi Console.Writeline(a)
, tùy thuộc vào kiểu dữ liệu
của a
mà một hàm WriteLine tương ứng được thi hành.
Ví dụ:
public class OverloadingExample { public static int Sum(int a, int b) { return a + b; } public static double Sum(double a, double b) { return a + b; } }
Lớp trên có hàm Sum
quá tải, tùy thuộc vào kiểu tham số mà hàm Sum cụ thể được gọi.
double a = 1; double b = 2; var c = OverloadingExample.Sum(a, b); // c = 3 có kiểu double
int a = 1; int b = 2; var c = OverloadingExample.Sum(a, b); // c = 3 nhưng có kiểu int
Chú ý: Khai báo hai hàm cùng tên, giống nhau hoàn toàn về tham số chỉ khác kiểu trả về sẽ gây lỗi.
Tìm hiểu tính đóng gói lập trình hướng đối tượng
Tính đóng gói mục đích hạn chế tối đa việc can thiệp trực tiếp vào dữ liệu, hoặc thi hành các tác vụ nội bổ của đối tượng. Nói cách khác, một đối tượng là hộp đen đối với các thành phần bên ngoài, nó chỉ cho phép bên ngoài tương tác với nó ở một số phương thức, thuộc tính, trường dữ liệu nhất định - hạn chế.
C# triển khai tính đóng gói này chính là sử dụng các Access Modifiers:
public
private
protected
internal
khi khai báo lớp, phương thức, thuộc tính, trường dữ liệu (biến).
public
thành viên có thể truy cập được bởi code bât kỳ đâu, ngoài đối tượng, không có hạn chế truy cập nào.private
phương thức, thuộc tính, trường khai báo với private chỉ có thể truy cập, gọi bởi các dòng code cùng lớp.protected
phương thức, thuộc tính, trường chỉ có thể truy cập, gọi bởi các dòng code cùng lớp hoặc các lớp kế thừa nó.internal
truy cập được bởi code ở cùng assembly (file).protected internal
truy cập được từ code assembly, hoặc lớp kế thừa nó ở assembly khác.private protected
truy cập được code khi cùng assembly trong cùng lớp, hoặc các lớp kế thừa nó.
Khi không chỉ rõ Modify thì mặc định là private
cho phương thức, thuộc tính, trường. Assembly là file exe, dll
Ví dụ:
class Student { private string Name; }
Khi sử dụng
var s = new Student(); s.Name = "ABC";
Biên dịch sẽ lỗi error CS0122: 'Student.Name' is inaccessible due to its protection level. Vì trường Name
là private
không thể truy cập bằng code bên ngoài lớp
như trên. Nhưng nếu thay bằng public
thì không lỗi.
Khi lập trình cố gắng tối đa ẩn thông tin ra bên ngoài lớp càng nhiều càng tốt để đảm bảo tính đóng gói của kỹ thuật lập trình OOP, nó giúp cho code dễ bảo trì và giám sát lỗi.
Thuộc tính trong lớp
Trường dữ liệu của lớp
Trường dữ liệu - khai báo như biến trong lớp, nó là thành viên của lớp, nó là biến. Trường dữ liệu có thể sử dụng bởi các phương thức trong lớp, hoặc nếu là public nó có thể truy cập từ bên ngoài, nhưng cách hay hơn để đảm bảo tính đóng gói khi cần truy cập thuộc tính hãy sử dụng phương thức, còn bản thân thuộc tính là private. Chúng ta đã sử dụng các trường dữ liệu ở những ví dụ trên.
Thuộc tính, bộ truy cập accessor setter/getter
Ngoài cách sử dụng trường dữ liệu, khai báo như biến ở phần trước, khai báo THUỘC TÍNH tương tự nhưng
nó có cơ chế accessor
(bộ truy cập), một cơ chế hết sức linh hoạt khi bạn
đọc / ghi dữ liệu vào thuộc tính. Hãy tìm hiểu qua một ví dụ sau:
class Student { private string name; // đây là trường dữ liệu }
Lớp này có một trường dữ liệu private là name
. Giờ ta
sẽ khai báo một thuộc tính có tên Name
với modify là public, thuộc tính này khi
đọc sẽ thi hành một đoạn code gọi là get
, khi ghi (gán) dữ liệu nó thi hành đoạn code
gọi là set
, thuộc tính Name
sẽ phối hợp cùng trường dữ liệu name
class Student { private string name; // Đây là trường dữ liệu public string Name // Đây là thuộc tính { // set thi hành khi gán, write // dữ liệu gán là value set { Console.WriteLine("Ghi dữ liệu <--" + value); name = value; } //get thi hành ghi đọc dữ liệu get { return "Tên là: " + name; } } }
Khi thực hiện
var s = new Student(); s.Name = "XYZ"; // set thi hành // In ra: Ghi dữ liệu <--XYZ // Và trường name giờ bằng XYZ Console.WriteLine(s.Name); // get được thi hành // In ra: Tên là: XYZ
Thuộc tính accessor
có thể khai báo thiếu set hoặc get, nếu thiếu set nó trở thành loại
chỉ đọc (readonly). Sử dụng set rất tiện lợi cho thao tác kiểm tra tính hợp lệ của dữ liệu khi gán, hoặc
tự động thực hiện một số tác vụ mỗi khi dữ liệu được gán.
Bạn có thể khai báo một thuộc tính tự động, nó hoạt động giống như trường dữ liệu.
public string Name {set; get;}
Bạn lưu ý khai báo như trên nó là thuộc tính, bạn vẫn có thể khai báo như là trường:
public string Name;
Tuy nhiên khi không là thuộc tính, thì sẽ thiếu đi một số tính năng ứng dụng cao cấp sau này mà C# hỗ trợ.
Source code: CS007_Class (Git), hoặc tải ex007