Lập trình Hướng đối tượng
/// <summary>
/// tiêu đề
/// </summary>
public string Title { get; set; } = "A new book";
/// <summary>
/// nhà xuất bản
/// </summary>
public string Publisher { get; set; } = "Unknown publisher";
/// <summary>
/// năm xuất bản
/// </summary>
public int Year { get; set; } = 2018;
/// <summary>
/// thông tin mô tả
/// </summary>
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
var book = new Book();
// lệnh này lỗi, vì setter của Id là protected
// chỉ có thể gán giá trị cho Id từ trong class
// không thể gán giá trị từ ngoài class
//book.Id = 2;
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever";
WriteLine($"{book.Authors},{book.Title},-{book.Publisher},
{book.Year}");
ReadKey();
Trang 101
Trung tâm Tin học – Ngoại ngữ
}
}
}
Trong ví dụ trên bạn đã khai báo một class hoàn toàn sử dụng auto property mà không
có biến thành viên.
Bạn dễ dàng để ý thấy, việc khai báo auto property không khác biệt nhiều so với biến,
ngoại trừ cặp {get; set;} đứng sau tên.
Id, Authors, Title, Publisher và Year khi khai báo được gán sẵn giá trị đầu.
Description không được gán sẵn giá trị nên sẽ nhận giá trị mặc định (null) lúc khởi tạo
object. Tất cả các property này đều có thể tự do truy xuất trong client code (phương thức
Main).
Riêng Id đặc biệt hơn một chút là setter của nó để mức truy cập là protected. Điều
này dẫn tới là trong client code không thể gán giá trị cho Id, nhưng vẫn có thể đọc giá trị
của Id.
Bạn có thể thấy, mỗi property lúc khai báo sẽ chứa hai phương thức get và set. Tuy
nhiên, khi truy xuất qua tên object sẽ chỉ nhìn thấy duy nhất tên property tương tự như một
biến thành viên public bình thường.
Property sử dụng cấu trúc này thường được sử dụng để thay thế cho biến public.
Trong thân phương thức thành viên có thể sử dụng auto property giống hệt như sử dụng
biến thành viên.
3.5.3. Full property trong C#
Full property đủ có cách khai báo khác với auto property. Full property phải đi kèm
với backed field, nơi thực sự lưu trữ thông tin. Property sẽ kiểm soát giá trị dữ liệu trước
khi gán giá trị đó vào biến.
Cấu trúc để khai báo thuộc tính đầy đủ như sau:
Trang 102
Lập trình Hướng đối tượng
[private|protected] <tên-kiểu> <tên-biến-hỗ-trợ>;
[public|protected|private] <tên-kiểu> <tên-thuộc-tính>
{
[public|protected|private] get {
[thân-phương-thức];
return <tên-biến-hỗ-trợ>;
}
[public|protected|private] set {
[thân-phương-thức];
<tên-biến-hỗ-trợ> = value;
}
}
Khi sử dụng thuộc tính đầy đủ chúng ta gặp từ khóa value. Từ khóa này được sử dụng
như một biến chứa giá trị đang cần gán cho thuộc tính.
Để sử dụng cấu trúc đầy đủ, thông thường mỗi thuộc tính sẽ được tạo ra cùng một
biến thành viên private. Biến private lưu trữ thông tin, property làm nhiệm vụ kiểm soát
thông tin cho biến này.
Phương thức get sẽ trả giá trị của biến qua tên property; phương thức set cho phép
property nhận giá trị và gán lại vào biến. Biến thành viên private này được gọi
là trường/biến hỗ trợ (backed field).
Ví dụ:
namespace FullProperty
{
using static System.Console;
/// <summary>
/// sách điện tử
/// </summary>
internal class Book
{
public int Id { get; protected set; } = 1;
private string _authors = "Unknown author";
private string _title = "A new book";
private int _year = 2018;
Trang 103
Trung tâm Tin học – Ngoại ngữ
/// <summary>
/// tên tác giả/ nhóm tác giả
/// </summary>
public string Authors
{
get { return _authors; }
set { if (!string.IsNullOrEmpty(value)) { _authors = value; } }
}
/// <summary>
/// tiêu đề
/// </summary>
public string Title
{
get { return _title; }
set { if (!string.IsNullOrEmpty(value)) { _title = value; } }
}
/// <summary>
/// nhà xuất bản
/// </summary>
public string Publisher { get; set; } = "Unknown publisher";
/// <summary>
/// năm xuất bản
/// </summary>
public int Year
{
get { return _year; }
set { if (value > 0) _year = value; }
}
/// <summary>
/// thông tin mô tả
/// </summary>
public string Description { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
Trang 104
Lập trình Hướng đối tượng
var book = new Book();
// lệnh này lỗi, vì setter của Id là protected
// chỉ có thể gán giá trị cho Id từ trong class
// không thể gán giá trị từ ngoài class
//book.Id = 2;
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever";
WriteLine($"{book.Authors},{book.Title},-{book.Publisher},
{book.Year}");
ReadKey();
}
}
}
Trong ví dụ trên chúng ta đã điều chỉnh lớp Book để sử dụng full property. Hãy để ý
các trường Authors, Title và Year. Đây là 3 full property.
Ví dụ, property Authors giờ đây phải sử dụng kết hợp với biến backed field _authors.
Đây là nơi thực sự lưu giữ thông tin. Property Authors giờ đóng vai trò xuất/nhập/kiểm
soát dữ liệu cho biến _authors. Nhìn từ client code thì sẽ không thấy _authors mà chỉ thấy
Authors duy nhất. Thông qua Authors có thể đọc thông tin. Tuy nhiên, nếu gán chuỗi trống
hoặc chuỗi null cho Authors thì giá trị này không được gán cho _authors. Setter của Authors
kiểm soát việc gán thông tin này.
Full property không thể gán giá trị đầu như auto-property mà chỉ có thể gán giá trị
đầu cho biến backed field.
Tình huống tương tự cũng diễn ra với Title: không chấp nhận giá trị rỗng hoặc null.
Đối với Year: không chấp nhận giá trị âm.
Trang 105
Trung tâm Tin học – Ngoại ngữ
Như vậy, full property nên sử dụng cho các trường thông tin cần kiểm soát dữ liệu,
và sử dụng auto-property cho các trường thông tin cho phép truy xuất tự do.
Trong thân phương thức thành viên có thể sử dụng full property hoặc biến backed
field cho cùng mục đích. Hai cách sử dụng này không có gì khác biệt.
Thực tế, auto-property chỉ là một dạng viết tắt của full-property, trong đó biến backed
field được sinh tự động.
Để cho ngắn gọn, nếu thân của getter hoặc setter chỉ có 1 lệnh duy nhất thì có thể sử
dụng lối viết expression body như sau:
public string Authors
{
get => _authors;
// expression body, tương đương với get { return _authors;}
set { if (!string.IsNullOrEmpty(value)) { _authors = value; } }
}
3.6. Hàm dựng trong C# và khởi tạo đối tượng
Các class bạn xây dựng trong các bài học trước tự bản thân nó không có nhiều giá trị
với chương trình bởi vì class chỉ đơn thuần là mô tả kiểu dữ liệu. Để sử dụng class trong
chương trình C#, bạn cần khởi tạo đối tượng của nó.
Khởi tạo đối tượng trong C# là quá trình yêu cầu tạo ra một object của class tương
ứng trên vùng nhớ heap và lấy địa chỉ của object gán cho một biến.
Sau khi object được khởi tạo, bạn có thể truy xuất các thành viên của nó để phục vụ
cho mục đích của chương trình.
Để khởi tạo object trong C# sử dụng từ khóa new và lời gọi tới một trong số các hàm
tạo (constructor) của class tương tự như đối với struct.
Trang 106
Lập trình Hướng đối tượng
3.6.1. Xây dựng constructor cho class C#
Hàm tạo, về mặt hình thức, luôn có cùng tên với class và không có kiểu ra. Danh sách
tham số và thân hàm tương tự như các phương thức thành viên.
Hãy cùng thực hiện ví dụ sau để hiểu cách xây dựng hàm tạo của class.
namespace DefaultConstructor
{
using static System.Console;
internal class Book
{
private string _authors;
private string _title;
private int _year;
private string _publisher;
public Book() // đây là một hàm tạo của class Book
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
_year = 2019;
}
public Book(string author, string title, int year, string
publisher) // đây là hàm tạo có tham số
{
_authors = author;
_title = title;
_year = year;
_publisher = publisher;
}
public string Print()
{
return $"{_authors}, \"{_title}\", -{_publisher}, {_year}";
}
}
internal class Program
Trang 107
Trung tâm Tin học – Ngoại ngữ
{
private static void Main(string[] args)
{
ReadKey();
}
}
}
Trong ví dụ trên bạn đã xây dựng một class Book đơn giản. Trong class này chỉ có 4
biến thành viên private (_authors, _title, _year, _publisher), 1 phương thức thành viên
Print().
Bạn có thể để ý hai thành viên đặc biệt:
public Book() // đây là một hàm tạo của class Book
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
_year = 2019;
}
public Book(string author, string title, int year, string publisher)
// đây là hàm tạo có tham số
{
_authors = author;
_title = title;
_year = year;
_publisher = publisher;
}
Đây là hai constructor của class Book. Khi khởi tạo object với lệnh new, thực tế bạn
sẽ gọi tới một trong hai constructor này. Đây cũng là khối code đầu tiên được thực thi khi
khởi tạo object.
Mỗi constructor có thể chứa một access modifier (public, private, protected) như các
thành viên khác. Điều đặc biệt là tên của constructor phải trùng với tên class. Phía sau tên
của constructor là danh sách tham số, tương tự như đối với phương thức.
Trang 108
Lập trình Hướng đối tượng
Với vai trò đó, trong constructor thường đặt các lệnh để khởi tạo giá trị cho các thành
viên (như bạn đã làm).
Trong mỗi class C# không giới hạn số lượng constructor. Tuy nhiên, các constructor
không được phép có danh sách tham số trùng nhau. Nếu có nhiều constructor trong một
class, mỗi constructor được gọi là một overload (nạp chồng hàm tạo).
3.6.2. Khởi tạo object với constructor
Bây giờ trong phương thức Main hãy viết một số lệnh như sau:
private static void Main(string[] args)
{
var book1 = new Book();
WriteLine(book1.Print());
var book2 = new Book("Christian Nagel", "Professional C# 7 and
the .NET core 2.0", 2018, "Wrox");
WriteLine(book2.Print());
ReadKey();
}
Trong đó,
var book1 = new Book();
var book2 = new Book("Christian Nagel", "Professional C# 7 and the
.NET core 2.0", 2018, "Wrox");
là hai lệnh khởi tạo object của lớp Book, sử dụng hai constructor đã xây dựng.
Như vậy có thể thấy, lệnh khởi tạo object cần có từ khóa new và gọi tới một trong số
các constructor của class. Kết quả khởi tạo có thể gán cho một biến để sau tái sử dụng.
3.6.3. Khởi tạo object với property
C# cung cấp một cách khởi tạo object khác: sử dụng bộ khởi tạo (object initializer).
Cú pháp khởi tạo này sử dụng property và được đưa vào từ C# 3 (.NET framework 3.5).
Trang 109
Trung tâm Tin học – Ngoại ngữ
Hãy cùng thực hiện một ví dụ trước.
Thêm các property sau vào class Book:
public string Authors { get => _authors; set => _authors = value; }
public string Title { get => _title; set => _title = value; }
public int Year { get => _year; set => _year = value; }
Trong phương thức Main bổ sung các lệnh sau:
var book3 = new Book
{
Authors = "Christian Nagel",
Title = "Professional C# 7 and the .NET core 2.0",
Year = 2018
};
WriteLine(book3.Print());
Đây là cách khởi tạo với object initializer sử dụng property.
Trong cách khởi tạo này, chúng ta vẫn sử dụng lời gọi tới hàm tạo như bình thường,
tuy nhiên, chúng ta có thể kết hợp luôn việc gán giá trị cho các property trong cùng lệnh
khởi tạo theo quy tắc:
• Tất cả lệnh gán giá trị cho property phải đặt trong cặp dấu ngoặc nhọn;
• Mỗi lệnh gán viết tách nhau bởi một dấu phẩy;
• Phải kết thúc bằng dấu chấm phẩy, vì đây thực chất là một lệnh, không phải một
khối lệnh (code block) như bình thường;
• Không bắt buộc phải gán giá trị cho tất cả các thuộc tính.
Cách thức khởi tạo này đặc biệt phù hợp với các class chứa dữ liệu sử dụng property
cũng như khởi tạo danh sách. Hãy tưởng tượng nếu không có cách thức khởi tạo này, hàm
tạo phải có rất nhiều tham số đầu vào để có thể gán giá trị cho tất cả các thành viên. Một
hàm tạo với danh sách tham số quá dài nhìn rất cồng kềnh, khó nhớ thứ tự các tham số,
cũng như dễ gây lỗi khi truyền tham số.
Nếu sử dụng hàm tạo mặc định hoặc hàm tạo không tham số có thể bỏ cả cặp dấu
ngoặc tròn sau tên constructor.
Trang 110
Lập trình Hướng đối tượng
3.7. Một số vấn đề khác với constructor trong C#
3.7.1. Default constructor trong class C#
Hàm tạo là bắt buộc khi định nghĩa class. Tuy nhiên chương trình dịch của C# có khả
năng tự sinh hàm tạo cho class nếu nó không nhìn thấy định nghĩa hàm tạo nào trong
class. Loại hàm tạo này có tên gọi là hàm tạo mặc định (default constructor). Hàm tạo
mặc định không có tham số đầu vào.
Nếu trong khai báo class chúng ta tự viết một hàm tạo không có tham số đầu vào,
hàm tạo này không được gọi là hàm tạo mặc định nữa mà được gọi là hàm tạo không
tham số (parameter-less/zero-parameter constructor), vì nó không phải do chương trình
dịch của C# sinh ra.
Trong ví dụ trên, public Book() {...} là một hàm tạo không tham số nhưng nó
không phải là hàm tạo mặc định.
Một khi đã định nghĩa hàm tạo riêng trong class, C# compiler sẽ không tự sinh ra hàm
tạo mặc định nữa. Nghĩa là nếu bạn muốn gọi hàm tạo không tham số, bạn phải tự viết
thêm hàm tạo đó. Nếu không, quá trình dịch sẽ báo lỗi.
3.7.2. Chuỗi constructor trong class C#, constructor initializer
Hãy điều chỉnh lại class Book như sau:
internal class Book
{
private string _authors = "Unknown author";;
private string _title = "A new book";
private int _year = 2019;
private string _publisher = "Unknown publisher";
public Book()
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
Trang 111
Trung tâm Tin học – Ngoại ngữ
_year = 2019;
}
public Book(string author)
{
_authors = author;
}
public Book(string author, string title) : this(author)
{
_title = title;
}
public Book(string author, string title, int year): this(author,
title)
{
_year = year;
}
public Book(string author, string title, int year, string
publisher) : this(author, title, year)
{
_publisher = publisher;
}
public string Print()
{
return $"{_authors}, \"{_title}\", -{_publisher}, {_year}";
}
}
Những điều chỉnh trên sử dụng một khả năng đặc biệt của C#: constructor gọi lẫn
nhau. Khi cho các constructor gọi lẫn nhau như trên bạn có thể tạo ra một chuỗi constructor
với số lượng tham số tăng dần, đồng thời tận dụng được code của constructor xây dựng
trước đó.
Cấu trúc : this (...) như trên có tên gọi là constructor initializer, là loại cấu
trúc đặc biệt cho phép gọi đến constructor khác và truyền tham số phù hợp cho nó.
Trang 112
Lập trình Hướng đối tượng
Trong ví dụ trên, this(author) là lời gọi đến constructor Book(string author)
trước đó; this(author, title) là lời gọi đến Book(string author, string
title); v.v..
Constructor initializer luôn thực thi trước constructor gọi nó. Khi kết hợp tốt các
constructor initializer như trên, bạn có thể tạo ra một chuỗi constructor với số lượng tham
số tăng dần mà không cần viết lặp lại code của các constructor trước đó.
3.8. Vấn đề khởi tạo và sử dụng object
3.8.1. Quan hệ class và object
Có thể hình dung class giống như một bản thiết kế trên giấy của một ngôi nhà. Tự
bản thân bản thiết kế này không phải ngôi nhà. Và có bản thiết kế không có nghĩa là chúng
ta có ngôi nhà.
Chỉ khi bạn sử dụng bản thiết kế này để xây dựng được một/một số ngôi nhà cụ thể,
bản thiết kế đó mới có giá trị.
Quá trình sử dụng bản thiết kế để xây dựng ngôi nhà có thể xem như tương đương
với quá trình khởi tạo đối tượng (object initialization/instantiation) trong C#. Sau khi có
ngôi nhà, chúng ta mới có thể ở. Quá trình sử dụng ngôi nhà này tương đương với việc sử
dụng object để giải quyết các vấn đề của chương trình.
Trong lập trình hướng đối tượng có thể phân biệt ba giai đoạn:
• Xây dựng class: định nghĩa kiểu dữ liệu, tương tự như tạo bản thiết kế ngôi nhà;
• Khởi tạo object: khai báo và gán giá trị đầu cho biến, tương tự giai đoạn xây nhà
theo thiết kế;
• Sử dụng object: sử dụng biến trong các lệnh và biểu thức, tương tự khai thác ngôi
nhà.
Trang 113
Trung tâm Tin học – Ngoại ngữ
3.8.2. Khai báo và khởi tạo object
Việc khai báo một object thực hiện tương tự như khai báo biến thuộc các kiểu dữ liệu
cơ sở mà bạn đã biết. Tuy nhiên, việc khai báo đơn thuần như vậy không đủ để sử dụng
object, vì khi đó C# đơn giản gán cho object giá trị null – giá trị mặc định của object, mà
không thực sự cấp phát bộ nhớ cho object.
Việc truy xuất một object có giá trị null luôn luôn gây
lỗi NullReferenceException ("Object reference not set to an instance
of an object."). Ngoài ra, trình biên dịch của C# luôn bắt buộc các biến cục
bộ phải được khởi tạo (instantiation, initialization) trước khi sử dụng.
Khi khởi tạo, một object sẽ được tạo ra trong vùng nhớ heap. Nếu kết quả khởi tạo
gán cho một biến, địa chỉ của object sẽ được gán cho biến này. Bản thân địa chỉ của object
chỉ là một con số. Con số này lại được lưu trong stack của phương thức.
Do khởi tạo object thực chất là lời gọi tới hàm tạo, C# bắt buộc mỗi class phải có hàm
tạo.
3.8.3. Truy xuất các thành viên của object
Trong định nghĩa class, chúng ta đã biết ba loại thành viên là biến thành viên, đặc
tính, và phương thức. Khi một object được khai báo và khởi tạo, chúng ta có thể sử dụng
các thành viên này để thực sự chứa dữ liệu hoặc xử lý dữ liệu.
Việc truy xuất các thành viên chỉ có thể thực hiện thông qua tên object, không thể
thực hiện qua tên class (trừ thành viên static sẽ học sau).
Để phân biệt, người ta sử dụng thuật ngữ instance members (bao gồm instance
method, instance variable, instance property) để mô tả các thành viên của class mà chỉ
thực sự tồn tại sau khi khởi tạo object. Sự tồn tại của các thành viên này phụ thuộc vào
object (vốn cũng được gọi là một instance của class).
Trang 114
Lập trình Hướng đối tượng
Để truy xuất thành viên của object chúng ta sử dụng phép toán “.” với tên object.
Truy xuất phương thức thành viên đơn giản là một lời gọi phương thức từ một object nào
đó. Việc truy xuất phương thức thành viên cũng sử dụng cấu trúc tương tự:
Có sự khác biệt khi truy xuất thành viên của một object từ client code với việc truy
xuất trong nội bộ một class.
Client code là đoạn code nơi thực hiện khởi tạo và sử dụng object.
Nếu trong định nghĩa class, một thành viên được xác định là protected hoặc private
sẽ không thể truy xuất được từ client code mà chỉ có thể được truy xuất trong nội bộ class.
Chỉ những thành viên được xác định là public mới có thể được truy xuất từ client
code.
Khi truy xuất thành viên từ trong nội bộ của class thì không cần sử dụng phép toán
truy xuất thành viên. Tuy nhiên có một số tình huống đặc thù sẽ phải sử dụng đến từ khóa
this và phép toán truy xuất thành viên.
3.8.4. Từ khóa this
Giả sử trong constructor bạn đặt tên cho tham số như sau:
public Book(string _authors)
{
_authors = _authors;
// làm sao phân biệt _authors nào là member class,
// _authors nào là tham số???
}
Tham số _authors giờ trùng tên với biến thành viên _authors. Cách đặt tên này không
vi phạm gì trong C#. Vấn đề bây giờ là, làm sao để phân biệt _authors nào là member class,
_authors nào là tham số???
Trong những tình huống thế này, bạn có thể sử dụng từ khóa this để chỉ rõ đâu là
thành viên của class:
Trang 115
Trung tâm Tin học – Ngoại ngữ
this._authors = _authors;
Từ khóa this cho phép chỉ định chính bản thân object nơi đang thực thi code. Trong
ví dụ trên, this._authors báo hiệu rằng cần dùng chính biến thành viên _authors của
object đó. Điều này cũng có nghĩa là bạn có thể dùng this trước mọi thành viên của class.
Tuy nhiên, bạn nên hạn chế sử dụng this nếu có thể vì nó làm code nhìn phức tạp hơn.
Từ khóa this cũng có một tác dụng phụ khác khá tốt. Nếu bạn không nhớ hết các
thành viên của class, bạn có thể gõ this, dấu chấm, và chờ intellisense giúp liệt kê hết các
thành viên (non-static) của class đó.
Từ khóa this giúp liệt kê thành viên của class. Từ khóa this chỉ có tác dụng với các
thành viên bình thường (không có từ khóa static).
3.9. Lớp lồng nhau
Class được khai báo bên trong thân của một class khác được gọi là nested class (lớp
lồng nhau) hoặc inner class (lớp trong, lớp nội bộ); class chứa class đó được gọi là outer
class (lớp ngoài, lớp bao).
Nếu một class được khai báo trực tiếp trong namespace, class đó còn được gọi là class
cấp đỉnh (top level class).
Trang 116
Lập trình Hướng đối tượng
Trong tất cả các bài học từ trước đến giờ, các class bạn xây dựng đều thuộc loại này.
Nested class trong C# được sử dụng trong trường hợp thân của một class trở nên quá
lớn nhưng có chứa những logic tương đối độc lập. Khi đó các khối logic này có thể xem
xét tách thành các lớp nội bộ để dễ dàng hơn trong quản lý code của class chính.
Việc phân chia này đồng thời giúp gói gọn code tránh làm dự án bị phân tán bởi các
lớp nhỏ không được sử dụng bởi client code.
Ví dụ:
using System;
namespace NestedClass
{
internal class OuterClass
{
private string _str = "Outer class field";
private void OuterClassMethod() => Console.WriteLine("Outer
class method");
public void OuterMethodCallsPrivateInnerClassMethod()
{
PrivateInnerClass privateInner = new
PrivateInnerClass();
privateInner.Method();
}
public void OuterMethodsCallsPublicInnerClassMethod()
{
PublicInnerClass publicInner = new PublicInnerClass();
publicInner.Method();
}
public class PublicInnerClass
{
public void Method()
{
Console.WriteLine("Inner public class method");
OuterClass outer = new OuterClass();
Console.WriteLine(outer._str);
outer.OuterClassMethod();
}
}
private class PrivateInnerClass
Trang 117
Trung tâm Tin học – Ngoại ngữ
{
public void Method()
{
Console.WriteLine("Inner private class method");
OuterClass outer = new OuterClass();
Console.WriteLine(outer._str);
outer.OuterClassMethod();
}
}
}
internal class Program
{
private static void Main(string[] args)
{
OuterClass outer = new OuterClass();
outer.OuterMethodCallsPrivateInnerClassMethod();
outer.OuterMethodsCallsPublicInnerClassMethod();
OuterClass.PublicInnerClass inner = new
OuterClass.PublicInnerClass();
inner.Method();
Console.ReadKey();
}
}
}
Qua ví dụ trên có thể thấy, nested class cũng là một class hoàn toàn bình thường ngoại
trừ ba vấn đề:
• Nếu trong nested class khởi tạo object của class ngoài thì có thể truy cập cả vào
các thành viên private của object đó;
• Nested class có thể bị che giấu hoàn toàn khỏi client code (dùng từ khóa private);
• Sử dụng nested class (public) phải theo quy tắc
“Tên_lớp_ngoài.Tên_lớp_nội_bộ”.
Đặc điểm của nested class trong C#
Nested class có một số điểm khác biệt so với top-level class.
• Nested class có thêm từ khóa điều khiển truy cập private. Nếu một nested class
được định nghĩa với từ khóa private thì các lớp sibling của lớp ngoài (lớp khác
Trang 118
Lập trình Hướng đối tượng
cùng cấp độ với lớp ngoài) không nhìn thấy được nó. Chỉ code của lớp ngoài mới
sử dụng được nested class dạng private. Nếu không chỉ ra từ khóa điều khiển truy
cập nào, C# mặc định sử dụng private cho lớp nội bộ.
• Nested class có thể khởi tạo object của lớp ngoài và truy cập các thành viên private
của object lớp ngoài.
• Lớp nội bộ nếu được định nghĩa truy cập public thì lớp sibling của lớp ngoài cũng
có thể sử dụng nó như sử dụng các lớp bình thường. Khi đó, lớp sibling của lớp
ngoài sẽ dùng cấu trúc Tên_lớp_ngoài.Tên_lớp_trong để sử dụng lớp nội bộ.
Lưu ý: trong C# nên hạn chế sử dụng nested class. Việc sử dụng nested class không
hợp lý có thể dẫn đến những lỗi khó lường trước, đặc biệt là khi cho lớp trong và lớp ngoài
gọi lẫn nhau.
3.10. Nested types
Lớp lồng nhau như bạn đã xem xét ở trên là một trường hợp riêng của nested types
– đặc điểm của C# cho phép khai báo các kiểu dữ liệu khác bên trong một class hoặc struct.
Nested types trong C# thể hiện quan hệ “has-a” giữa class/struct ngoài với kiểu khai
báo bên trong nó.
C# cho phép khai báo tất cả các nhóm kiểu bạn đã biết (enum, class, struct, interface,
delegate) bên trong một class hoặc struct. Khi đó, các kiểu “bên trong” được gọi chung là
các nested type. Mỗi nested type có thể xem như “kiểu thành viên” của class hoặc struct,
tương tự như biến thành viên, phương thức thành viên hay đặc tính thành viên.
Về cấu trúc cú pháp, việc khai báo các kiểu nested không có gì khác biệt so với khi
nó được khai báo là kiểu cấp đỉnh (top-level type) trực thuộc namespace.
class OuterClass
{
public enum InnerEnum { ValueOne, ValueTwo, ValueThree }
// khai báo nested enum
private struct InnerStruct // khai báo nested struct private
{
public int AnIntMember = 10;
Trang 119
Trung tâm Tin học – Ngoại ngữ
}
}
Còn một lưu ý nữa: bản thân nested type lại có thể tiếp tục chứa nested type của riêng
nó với cấp độ lồng nhau không hạn chế. Tuy nhiên, đây là những tình huống sử dụng rất
hãn hữu. Việc đặt lớp lồng nhau nhiều cấp khiến code rắc rối và khó theo dõi.
Sử dụng nested type
Vậy khi nào và tại sao bạn nên xem xét sử dụng nested type thay vì khai báo kiểu như
bình thường (thuộc namespace)?
Thứ nhất, nested type cho phép kiểm soát truy cập tốt hơn. Bạn có thể sử dụng từ
khóa private để đặt kiểu dữ liệu nó hoàn toàn nội bộ. Trong khi đó, kiểu khai báo trong
namespace chỉ có hai mức truy cập: public và internal.
Thứ hai, do nested type cũng được xem là thành viên của class/struct, nó có thể truy
xuất các thành viên private/protected khác của class/struct chứa nó.
Thứ ba, nếu bạn xác định rằng một kiểu dữ liệu nào đó chỉ hữu dụng đối với 1 class
duy nhất hoặc không được sử dụng bởi client code, kiểu dữ liệu đó nên được khai báo làm
nested type.
Nested type được sử dụng từ client code thông qua tên của class/struct chứa nó giống
hệt như đối với nested class mà bạn đã xem xét ở phần trên. Việc sử dụng nội bộ bên trong
class/struct chứa kiểu đó không có gì khác biệt so với khi kiểu đó được khai báo trong
namespace.
Ví dụ, với class OuterClass như trên bạn có thể sử dụng các kiểu nested từ client code
như sau:
// sử dụng từ client code
var innerEnumValueOne = OuterClass.InnerEnum.ValueOne;
var innerStructObject = new OuterClass.InnerStruct();
Trang 120
Lập trình Hướng đối tượng
Nếu sử dụng trong nội bộ lớp OuterClass:
class OuterClass
{
public enum InnerEnum { ValueOne, ValueTwo, ValueThree }
// khai báo nested enum
private struct InnerStruct // khai báo nested struct private
{
public int AnIntMember = 10;
public ValueOne = InnerEnum.ValueOne;
}
public void Print(InnerStruct innerStruct)
{
Console.WriteLine(innerStruct.AnIntMember);
Console.WriteLine(innerStruct.ValueOne);
}
}
Như vậy, việc sử dụng nested type bên trong class chứa nó hoàn toàn không khác biệt
gì với các kiểu tương ứng khai báo ngoài class mà bạn đã biết.
3.11. Thành viên Static
3.12. Biến tĩnh (static field)
Trước khi đi vào nhu cầu tạo ra biến tĩnh, hãy cùng xây dựng một class như sau:
class Employee
{
public Employee(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Salary { get; set; } = "Not Enough";
}
Đây là một class đơn giản mô tả cho nhân viên công ty.
Trang 121
Trung tâm Tin học – Ngoại ngữ
Bài toán đặt ra là, mỗi khi khởi tạo một object của Employee, object đó sẽ được gán
một giá trị Id độc nhất. Ngoài ra, nếu liên tục tạo ra một loạt object mới (ví dụ, để lưu trong
một mảng), giá trị của Id sẽ tăng dần. Nghĩa là object tạo sau có Id bằng Id của object vừa
tạo trước cộng thêm 1. Nhu cầu tạo ra Id tự động như vậy rất giống với cách tăng tự động
giá trị khóa chính của bảng dữ liệu Sql Server.
Một giải pháp bạn chắc chắn sẽ nghĩ tới là tạo ra một biến đếm (counter) độc lập nào
đó (nằm ngoài class). Mỗi lần tạo ra object mới của Employee thì gán giá trị hiện thời của
counter cho Id, sau đó tăng counter thêm 1. Vấn đề khi đó nằm ở chỗ, bạn phải liên tục
truyền biến counter này đến cho bất kỳ chỗ nào thực hiện khởi tạo object.
Bây giờ hãy thực hiện thay đổi nhỏ sau trong class Employee:
class Employee
{
public Employee(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
Id = NextId;
NextId++;
}
public static int NextId;
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Salary { get; set; } = "Not Enough";
}
Bạn chỉ thêm lời khai báo:
public static int NextId;
Sau đó bổ sung thêm hai lệnh vào hàm dựng:
Id = NextId;
NextId++;
Trang 122
Lập trình Hướng đối tượng
Khai báo biến NextId ở trên là một lệnh khai báo biến tĩnh (static field). Dễ thấy,
biến tĩnh về mặt hình thức khác biệt với biến thành viên ở mỗi từ khóa static. Tuy nhiên,
tác dụng của nó lại hoàn toàn khác.
Biến tĩnh NextId được dùng chung trong mọi object của Employee. Nghĩa là dù bạn
khởi tạo bao nhiêu object đi nữa, tất cả các object đó đều có chung biến NextId này. Tức
là sự tồn tại của NextId thực sự độc lập với các object của Employee.
Đối với biến thành viên, nếu bạn khởi tạo một object, một biến thành viên mới sẽ
được tạo ra (có vùng nhớ riêng của nó). Biến thành viên đó sẽ được khởi tạo giá trị của
riêng nó.
Đối với biến tĩnh, trong lần truy xuất đầu tiên sẽ tạo ra vùng nhớ cho biến. Tất cả các
object tạo ra sau đó đều sử dụng chung vùng nhớ này. Do đó, biến tĩnh chỉ có 1 bản duy
nhất.
Bạn đã thấy sự tương đồng với ý tưởng tạo biến counter độc lập chưa ạ! Sự khác biệt
là, biến counter độc lập đó (NextId) giờ được đặt thẳng trong class. Tất cả các thành viên
của Employee tự động sử dụng được NextId. Bạn không cần phải mất công truyền qua
truyền lại nữa, cũng không cần tác động gì từ bên ngoài. Bằng cách này, bạn thậm chí còn
đếm được là hiện tại có bao nhiêu object của Employee đã được tạo ra.
Giờ hãy viết một số client code để thử nghiệm lớp Employee với trường static NextId:
using System;
class Program
{
static void Main(string[] args)
{
Employee.NextId = 1000000;
Employee employee1 = new Employee("Inigo", "Montoya");
Employee employee2 = new Employee("Princess", "Buttercup");
Console.WriteLine("{0} {1} ({2})", employee1.FirstName,
employee1.LastName, employee1.Id);
Console.WriteLine("{0} {1} ({2})", employee2.FirstName,
employee2.LastName, employee2.Id);
Trang 123
Trung tâm Tin học – Ngoại ngữ
Console.WriteLine($"NextId = { Employee.NextId }");
Console.ReadKey();
}
// ... khai báo của lớp Employee nằm đây, bỏ qua cho gọn code
}
Chạy thử chương trình bạn sẽ thấy object đầu tiên có Id = 1000000. Object thứ hai Id
= 1000001. Sau đó NextId lại tăng lên 1000002.
Về mặt cú pháp khai báo, biến static chỉ khác biệt duy nhất ở từ khóa static nằm trước
tên kiểu.
Khi sử dụng trong client code, bạn chỉ có thể truy xuất biến static thông qua tên class,
không phải thông qua tên object như đối với biến thành viên. Như trong ví dụ, bạn truy
xuất biến static NextId trong phương thức Main là Employee.NextId. Bạn không thể truy
xuất theo kiểu employee2.NextId được.
Chỉ khi nào bạn sử dụng biến tĩnh bên trong class, bạn có thể bỏ qua tên class. Khi
đó, về mặt hình thức, nó không có gì khác biệt với biến thành viên. Đây là tình huống bạn
đã sử dụng NextId bên trong constructor.
Lưu ý rằng, trong cùng một class không thể khai báo biến static và biến thành viên
trùng tên. Bạn hẳn sẽ thấy ngay, nếu dùng trong class, compiler sẽ không hiểu bạn muốn
dùng biến nào.
Trong cuộc sống bạn cũng có thể thấy những ví dụ tương đồng với biến thành viên
và biến static. Lấy ví dụ về khuôn đúc và sản phẩm đúc từ khuôn. Dễ hình dung, khuôn
đúc tương đương với class, còn sản phẩm đúc tương đương với object.
Mỗi sản phẩm đúc có những thông tin của riêng nó. Những thông tin riêng biệt này
tương tự như biến thành viên, vốn chứa thông tin về trạng thái và đặc điểm riêng của object
(sản phẩm đúc).
Trang 124
Lập trình Hướng đối tượng
Tuy nhiên, lại có những thông tin phải liên kết với chính khuôn đúc. Ví dụ số lượng
sản phẩm đã đúc từ khuôn, số series của sản phẩm tiếp theo, công suất đúc (bao nhiêu sản
phẩm mỗi giờ). Rõ ràng những thông tin này không liên quan đến từng sản phẩm cụ thể,
mà liên quan đến chính khuôn đúc.
Như vậy, dữ liệu không nhất thiết chỉ liên kết với object mà còn có thể liên kết với
chính class. Biến static chính là loại thông tin liên kết với bản thân class.
Về hằng thành viên của class C#.
Hằng thành viên của class C# có chút đặc biệt mà ít người để ý. Khi khai báo một
hằng thành viên, hằng đó sẽ được C# tự động coi là một thành viên static giống như biến
static. Sự khác biệt duy nhất là giá trị của nó phải được xác định ngay từ lúc khai báo và
sau này không thể thay đổi được nữa. Hằng thành viên khai báo giống hệt như biến thành
viên, ngoại trừ từ khóa const ở trước:
public const int MaxValue = 12345;
3.13. Phương thức tĩnh (static method)
Tương tự như biến tĩnh, phương thức tĩnh liên kết với bản thân class chứ không liên
kết với object của class đó. Do đó, cũng chỉ có 1 phiên bản duy nhất của phương thức đó
được tạo ra.
Về cú pháp khai báo, phương thức tĩnh khác biệt với phương thức thành viên duy
nhất ở từ khóa static đặt trước tên kiểu trả về.
Hãy cùng thực hiện ví dụ sau:
class ConsoleHelper
{
public ConsoleColor BackgroundColor { get; set; } =
ConsoleColor.Black;
public ConsoleColor ForegroundColor { get; set; } =
ConsoleColor.White;
public void WriteLine(object message)
Trang 125
Trung tâm Tin học – Ngoại ngữ
{
WriteLine(message, ForegroundColor, BackgroundColor);
}
public void WriteLine(object message,
ConsoleColor fgColor = ConsoleColor.White,
ConsoleColor bgColor = ConsoleColor.Black,
bool reset = true)
{
Console.ForegroundColor = fgColor;
Console.BackgroundColor = bgColor;
Console.WriteLine(message);
if (reset)
Console.ResetColor();
}
}
Ví dụ trên khai báo class ConsoleHelper với một phương thức static WriteLine, một
phương thức thành viên cùng tên WriteLine. Hai phương thức này cùng hỗ trợ viết thông
tin ra màn hình console nhưng có thể thiết lập màu nền và màu văn bản. Trong class này
cũng chứa hai property thành viên BackgroundColor và ForegroundColor.
Phương thức static được gọi trực tiếp từ tên class, so với phương thức thành viên
được gọi từ tên object. Do đó, để gọi phương thức static bạn không cần khởi tạo object.
static void Main(string[] args) ConsoleColor.Magenta,
{
// gọi phương thức tĩnh WriteLine
ConsoleHelper.WriteLine("Hello world!",
ConsoleColor.White);
// khởi tạo object và gọi phương thức thành viên WriteLine =
var helper = new ConsoleHelper { BackgroundColor
ConsoleColor.Black, ForegroundColor = ConsoleColor.Yellow };
helper.WriteLine("Hello again!");
Console.ReadKey();
}
Bởi vì phương thức static không được sử dụng qua object (và cũng không liên quan
gì đến object), bạn không được sử dụng từ khóa this bên trong phương thức static. Cũng
Trang 126
Lập trình Hướng đối tượng
vì lý do này, phương thức static không thể sử dụng được các thành viên bình thường (như
biến thành viên, phương thức thành viên) khác bên trong class.
Trong ví dụ trên, phương thức tĩnh WriteLine không thể sử dụng được
BackgroundColor và ForegroundColor vì đây là hai property thành viên.
Tuy nhiên, phương thức static vẫn có thể khởi tạo và sử dụng object của chính class
chứa nó. Ví dụ, lệnh khởi tạo sau là hoàn toàn hợp lệ bên trong thân của WriteLine:
var helper = new ConsoleHelper();
Ở chiều ngược lại, các phương thức thành viên hoàn toàn có thể sử dụng phương thức
tĩnh. Như trong ví dụ trên, phương thức thành viên WriteLine đã gọi phương thức static
WriteLine.
Nhìn chung, phương thức tĩnh có vai trò rất gần với phương thức toàn cục trong
C/C++. Sự khác biệt duy nhất là nó được quản lý tốt hơn.
Nếu một phương thức không sử dụng bất kỳ thành viên nào khác của class, bạn nên
đặt nó làm phương thức tĩnh. Cách làm này sẽ hiệu quả hơn so với đặt nó làm phương thức
thành viên vì sẽ chỉ có duy nhất 1 phiên bản của phương thức được tạo ra và quản lý. Nếu
để phương thức tương tự làm phương thức thành viên, mỗi object sẽ phải chứa một phiên
bản riêng của nó.
Intellisense của Visual studio có thể tự động phát hiện và gợi ý chuyển đổi phương
thức thành dạng static theo logic trên.
3.14. Hàm dựng tĩnh (static constructor)
Quay trở lại ví dụ minh họa của phần biến static. Bài toán đặt ra là: làm sao để gán
giá trị đầu của biến tĩnh NextId là một giá trị ngẫu nhiên nhưng không dùng client code?
Để tạo giá trị ngẫu nhiên, bạn có thể sử dụng class Random như sau:
Random randomGenerator = new Random();
NextId = randomGenerator.Next(101, 999);
Trang 127
Trung tâm Tin học – Ngoại ngữ
tức là bạn cần đến vài lệnh tính toán thì mới tạo ra được một giá trị ngẫu nhiên.
Hãy nghĩ đến một tình huống phức tạp hơn. Giả sử bạn lưu dữ liệu employee vào file.
Khi chương trình hoạt động, bạn cần lấy giá trị lớn nhất của Id đã lưu trong file để tiếp tục
tăng giá trị của Id theo quy luật chứ không muốn gán giá trị đầu ngẫu nhiên.
Để giải quyết tình huống khởi tạo giá trị đầu (mà phải tính toán phức tạp) cho biến
static, C# sử dụng hàm dựng tĩnh (static constructor).
Hãy bổ sung hàm dựng sau vào lớp Employee:
class Employee
{
static Employee()
{
Random randomGenerator = new Random();
NextId = randomGenerator.Next(101, 999);
}
// ...
public static int NextId = 42;
// ...
}
Đây là khai báo cho hàm dựng static của lớp Employee. Về cú pháp, nó khác hàm
dựng (constructor) bình thường ở từ khóa static đứng tên.
Hàm dựng static khác biệt với hàm dựng thông thường ở một số điểm:
• Hàm dựng không cần dùng từ khóa điều khiển truy cập. Lý do là vì hàm dựng
static không thể gọi từ client code. Hàm dựng này được chương trình tự động gọi
khi cần thiết. Chính xác hơn là nó được gọi khi sử dụng class lần đầu tiên trong
chương trình.
• Hàm dựng static không chấp nhận tham số. Lý do giống như ở trên: static
constructor được gọi tự động.
Hàm dựng static được sử dụng để khởi tạo giá trị cho các trường static của class, đặc
biệt là khi phải thực hiện những tính toán phức tạp.
Trang 128
Lập trình Hướng đối tượng
Nếu bạn đồng thời gán giá trị cho trường static lúc khởi tạo và trong hàm dựng static,
giá trị trong hàm dựng static sẽ là giá trị chính thức của trường static.
Nếu có thể, hãy gán giá trị đầu cho trường static lúc khai báo thay vì gán trong
constructor.
3.15. Đặc tính tĩnh (static property)
Như bạn đã biết, property thực chất là tổ hợp hai phương thức get-set để kiểm soát
xuất nhập giá trị cho một trường dữ liệu. Như vậy, nếu đã có biến static và phương thức
static thì cũng có khái niệm static property với cùng ý nghĩa: tổ hợp hai phương thức tĩnh
get – set để kiểm soát xuất nhập giá trị cho biến static.
Hãy bổ sung thêm đoạn code sau vào class Employee mà bạn đã xây dựng từ trước:
class Employee
{
// ...
public static int NextId
{
get {
return _nextId;
}
private set {
_nextId = value;
}
}
private static int _nextId = 42;
// ...
}
Đây là cách khai báo static property NextId để xuất nhập dữ liệu cho biến static
_nextId. Cách khai báo static property không có gì khác biệt so với property thành viên,
ngoại trừ từ khóa static.
Bạn thậm chí có thể khai báo auto static property như sau:
public static int NextId { get; private set; } = 42;
Trang 129
Trung tâm Tin học – Ngoại ngữ
Static property được sử dụng qua tên class giống như biến static và phương thức
static.
Tương tự như đặc tính thành viên, bạn nên sử dụng static property thay cho biến static
public.
3.16. Lớp tĩnh (static class)
Một số class được tạo ra nhưng không chứa bất kỳ biến thành viên nào. Lấy ví dụ,
nếu bạn muốn xây dựng một class chuyên thực hiện các hàm tính toán số học, bạn chẳng
cần biến thành viên nào cả. Hãy cùng thực hiện một class như vậy:
public static class MyMath
{
public static int Max(params int[] numbers)
{
if (numbers.Length == 0)
{
throw new ArgumentException("Không có giá trị để so
sánh", "numbers");
}
int max = numbers[0];
foreach (var number in numbers)
{
if (number > max)
{
max = number;
}
}
return max;
}
public static int Min(params int[] numbers)
{
if (numbers.Length == 0)
{
throw new ArgumentException("Không có giá trị để so sánh",
"numbers");
}
Trang 130
Lập trình Hướng đối tượng
int min = numbers[0];
foreach (var number in numbers)
{
if (number < min)
{
min = number;
}
}
return min;
}
}
Trong class này khai báo hai phương thức static Min và Max để tìm giá trị nhỏ nhất
và lớn nhất trong một mảng số nguyên.
Lớp MyMath không chứa bất kỳ biến hoặc phương thức thành viên thường nào. Do
đó, việc khởi tạo object của nó khá vô nghĩa. Do vậy nó được khai báo làm lớp static với
từ khóa static đứng trước từ khóa class như bạn đã thấy.
Từ khóa static đứng trước khai báo class có mấy tác dụng:
• Thứ nhất nó không cho phép khởi tạo object từ class này.
• Thứ hai, nó không cho phép khai báo bất kỳ thành viên thông thường nào mà chỉ
có thể khai báo các thành viên tĩnh.
Khi một class được đánh dấu là static, C# compiler tự động đánh dấu nó là abstract
và sealed, nghĩa là cấm khởi tạo object và không cho phép kế thừa.
Một đặc điểm nữa của lớp static là bạn có thể sử dụng cấu trúc using static để trực
tiếp truy xuất các thành viên static của class này mà không cần chỉ rõ tên class, nghĩa là
nếu ở đầu file code có lệnh using static MyMath; thì bạn có thể gọi các phương thức của
class này theo cách ngắn gọn Max(numbers); thay cho MyMath.Max(numbers);.
using static Console;
using static MyMath;
class Program
{
static void Main(string[] args)
Trang 131
Trung tâm Tin học – Ngoại ngữ
{
int[] numbers = new[] { 1, 2, 3, 4, 5, 6 };
// có thể gọi Max và Min theo cách ngắn gọn vì đã có using
static MyMath; ở trên
int max = Max(numbers);
int min = Min(numbers);
// có thể gọi WriteLine ngắn gọn vì đã có using static Console;
ở trên
WriteLine($"Max value: {max}");
WriteLine($"Min value: {min}");
ReadKey();
}
}
Lớp System.Console mà bạn đã biết cũng là một static class. Vì vậy, nếu đặt using
static System.Console; ở đầu file code thì có thể gọi các phương thức trong đó một cách
ngắn gọn: WriteLine("Hello world"); thay vì Console.WriteLine("Hello world");
3.17. Extension method
Extension method (phương thức mở rộng) là một tính năng rất thú vị của C# cho phép
“chèn” một phương thức của class này vào làm phương thức thành viên một class khác.
Để cho dễ hiểu, hãy tưởng tượng thế này. Lớp string mà bạn đã biết mặc dù cung cấp
rất nhiều phương thức hữu ích nhưng nếu bạn làm việc nhiều với giao diện console, hẳn
bạn sẽ muốn class này có một phương thức giúp xuất trực tiếp chuỗi ra console, thay vì
phải liên tục gọi đến Console.Write/WriteLine. Hoặc bạn cũng có thể muốn lớp này hỗ trợ
luôn việc chuyển đổi chuỗi thành các kiểu dữ liệu cơ sở quen thuộc, thay vì phải gọi
Parse/TryParse của kiểu đích.
Bắt đầu từ C# 3 bạn đã có thể thực hiện mong muốn trên bằng cách sử dụng extension
method. Extension method thực chất chỉ là một static method nằm trong một static class
cùng với một thay đổi nhỏ trong danh sách tham số.
Hãy cùng thực hiện một ví dụ nhỏ sau cho dễ hiểu.
Trang 132
Lập trình Hướng đối tượng
static class ExtensionMethods
{
public static void ToConsole(this string message)
{
Console.WriteLine(message);
}
public static void ToConsole(this string message, ConsoleColor
fgColor = ConsoleColor.White, ConsoleColor bgColor =
ConsoleColor.Black, bool reset = true)
{
Console.ForegroundColor = fgColor;
Console.BackgroundColor = bgColor;
Console.WriteLine(message);
if (reset) Console.ResetColor();
}
public static double ToDouble(this string number)
{
return double.TryParse(number, out double d) ? d :
double.NaN;
}
public static int ToInt(this string number)
{
return int.Parse(number);
}
}
Ví dụ trên xây dựng một static class ExtensionMethods với hai static method cùng
tên ToConsole. Hai phương thức này cùng thực hiện in một thông báo ra màn hình.
Overload thứ hai in ra có màu nền và màu văn bản. ToDouble và ToInt thực hiện chuyển
đổi chuỗi thành kiểu double và int.
Hãy để ý tham số thứ nhất của cả bốn phương thức. Chúng cùng có dang this string
<tên-biến>. Để ý thấy rằng, tham số này khác thường một chút ở từ khóa this. Chỉ một từ
khóa đó làm cho hai phương thức có năng lực đặc biệt: có thể gọi chúng từ một string bất
kỳ. Hãy cùng xem client code:
static void Main(string[] args)
Trang 133
Trung tâm Tin học – Ngoại ngữ
{
"Hello world!".ToConsole();
"Hello again!".ToConsole(ConsoleColor.Magenta);
int i = "2000".ToInt();
double d = "2000.0001".ToDouble();
Console.ReadKey();
}
Bạn đã thấy, ToConsole(), ToInt(), ToDouble() giờ được gọi thẳng từ object của
string, giống hệt như gọi các phương thức thành viên khác của string. Nếu chỉ nhìn lời gọi
phương thức ở client code, bạn không phân biệt được đâu là phương thức thành viên “xịn”
của class, đâu là extension method.
Dĩ nhiên, bạn hoàn toàn có thể gọi extension method như các static method thông
thường:
ExtensionMethods.ToConsole(“Hi SV He thong Thong tin.”);
Lưu ý, nếu bạn tạo một extension method có signature giống hệt như một phương
thức thành viên có sẵn của class, bạn sẽ không gọi nó qua object được mà chỉ có thể gọi
như phương thức static thông thường.
Nếu muốn tạo extension method cho kiểu nào, bạn đặt tên kiểu đó sau từ khóa this
của tham số đầu tiên. Trong ví dụ trên, bạn muốn tạo extension method cho lớp string thì
tham số đầu tiên phải là this string <tên-tham-số>. Bạn có thể sử dụng tham số này
trong thân method như bất kỳ tham số bình thường nào.
Bạn có thể tạo extension method cho cả các class có sẵn của C# cũng như class tự
xây dựng. Cách thực hiện không có gì khác biệt nhau.
Mặc dù extension method là một tính năng rất thú vị, bạn không nên lạm dụng nó,
nhất là khi áp dụng cho các class không phải do bạn xây dựng. Extension method có vấn
đề trong việc quản lý phiên bản (versioning). Đặc biệt không nên tạo extension method
cho kiểu object.
Trang 134
Lập trình Hướng đối tượng
CHƯƠNG 4. KẾ THỪA VÀ ĐA HÌNH TRONG C#
4.1. Kế thừa
Kế thừa (inheritance) là một công cụ rất mạnh trong lập trình hướng đối tượng cho
phép tạo ra các class mới từ một class đã có, và qua đó cho phép tái sử dụng code của class
đã có, giúp giảm thiểu việc lặp code giữa các class, dễ dàng bảo trì và giảm thời gian phát
triển.
Hãy cùng xem xét ví dụ sau:
using System;
namespace Inheritance
{
internal class Bird
{
private int _weight;
public int Weight
{
get => _weight;
set {
if (value > 0)
_weight = value;
}
}
public void Feed() => _weight += 10;
public Bird() => Console.WriteLine($"Bird created");
public Bird(int weight)
{
_weight = weight;
Console.WriteLine($"Bird created, {_weight} gr.");
}
public void Fly() => Console.WriteLine("Bird is flying");
}
internal class Parrot : Bird
{
public Parrot() => Console.WriteLine("Parrot created");
public Parrot(int weight) : base(weight) { }
public void Speak() => Console.WriteLine("Parrot is speaking");
}
internal class Cockatoo : Parrot
{
Trang 135
Trung tâm Tin học – Ngoại ngữ
public Cockatoo() => Console.WriteLine("Cockatoo created");
public void Dance() => Console.WriteLine("Cockatoo is
dancing");
}
internal class MainClass
{
private static void Main(string[] args)
{
Console.WriteLine("Bird:");
Bird bird = new Bird(50) { Weight = 100 };
bird.Feed();
Console.WriteLine($"Weight: {bird.Weight}");
bird.Fly();
Console.WriteLine("rnParrot:");
Parrot parrot = new Parrot(200);
parrot.Feed();
Console.WriteLine($"Weight: {parrot.Weight}");
parrot.Fly();
parrot.Speak();
Console.WriteLine("rnCockatoo:");
Cockatoo cockatoo = new Cockatoo() { Weight = 300 };
cockatoo.Feed();
Console.WriteLine($"Weight: {cockatoo.Weight}");
cockatoo.Fly();
cockatoo.Speak();
cockatoo.Dance();
Console.ReadKey();
}
}
}
Mối quan hệ giữa các class được thể hiện qua sơ đồ code:
Trang 136
Lập trình Hướng đối tượng
Code map thể hiện quan hệ giữa các class
Trong ví dụ trên chúng ta tạo ra ba class: Bird (chim), Parrot (vẹt) và Cockatoo (vẹt
châu Úc). Parrot kế thừa Bird; Cockatoo kế thừa Parrot. Quan hệ kế thừa này trong C#
được thể hiện bằng dấu hai chấm phân chia tên của class mới với tên của một class có sẵn:
• class Parrot : Bird
• class Cockatoo : Parrot
Khi chạy chương trình trên có thể nhận xét như sau:
Mỗi khi khởi tạo object của class con thì hàm tạo của class cha luôn được gọi trước.
Điều này có nghĩa là trước khi khởi tạo object của class con thì object của class cha được
khởi tạo, và do đó bản thân object con có chứa trong nó object cha. Object cha này được
truy cập từ object con thông qua từ khóa base. Thông qua từ khóa base cũng có thể truy
xuất các thành viên của lớp cha.
Hàm tạo không được kế thừa mà hàm tạo của lớp cha được gọi tự động (nếu là hàm
tạo không tham số) hoặc được gọi từ hàm tạo của lớp con (nếu là hàm tạo có tham số).
Hàm tạo của lớp cha được gọi bằng lệnh base(<danh sách tham số>), tương tự như gọi
phương thức (chỉ thay tên phương thức bằng từ khóa base).
Mặc dù lớp con không thể kế thừa thành viên private của lớp cha (tức là không thấy
và không thể trực tiếp sử dụng, như lớp Parrot không thể nhìn thấy và trực tiếp sử dụng
Trang 137
Trung tâm Tin học – Ngoại ngữ
biến thành viên _weight của Bird) nhưng qua phương thức/thuộc tính kế thừa của lớp cha
vẫn có thể gián tiếp sử dụng thành viên private này. Phương thức Feed và thuộc tính Weight
ở trên là ví dụ. Lý do là vì trong object con có cả object cha tồn tại. Lời gọi tới phương
thức kế thừa từ lớp cha thực chất là hoạt động với object cha này.
4.1.1. Đặc điểm của kế thừa
Lớp có sẵn mà từ đó tạo ra các lớp khác được gọi là lớp cha/lớp cơ sở; lớp mới xây
dựng trên cơ sở lớp cũ được gọi là lớp con/lớp dẫn xuất.
Để tiện lợi, trong một số trường hợp chúng ta sẽ sử dụng thuật ngữ tiếng Anh thay
thế: base class, parent class, super class (lớp cha/lớp cơ sở), derived class, child class,
subclass (lớp con, lớp dẫn xuất).
C# chỉ cho phép mỗi lớp con có một lớp cha trực tiếp (khác với C++ cho phép lớp có
nhiều lớp cha trực tiếp). Cách kế thừa này được gọi là kế thừa đơn (single inheritance).
Lớp con, đến lượt mình, lại có thể trở thành lớp cơ sở cho các lớp khác (tạm gọi vui
là lớp cháu J). Quá trình này có thể tiếp diễn với nhiều thế hệ lớp khác nhau, tạo ra một cấu
trúc phân cấp (class hierarchy) của các class có quan hệ kế thừa nhau.
Tất cả các lớp con, cháu, chắt, v.v. của một class gọi chung là các lớp hậu
duệ (descendant) của class đó; các lớp cha, ông, cụ, v.v. của một class được gọi chung là
các lớp tiền bối (ancestor) của nó.
Trong ví dụ trên, Bird là lớp cha của Parrot (Parrot là lớp con trực tiếp của Bird), còn
Parrot lại trở thành lớp cha của Cockatoo (Cockatoo là lớp con trực tiếp của Parrot, lớp con
gián tiếp của Bird). Bird => Parrot => Cockatoo tạo ra một cấu trúc phân cấp của các class.
Parrot, Cockatoo đều có thể gọi chung là các lớp hậu duệ của Bird.
Class được đánh dấu với từ khóa sealed không cho phép kế thừa. Hiểu đơn giản dòng
dõi class này đến đây là “tuyệt tự”.
Trang 138
Lập trình Hướng đối tượng
Khi một class kế thừa từ một class khác, nó thừa hưởng tất cả các thành viên của class
cha (kể cả những thành viên mà cha nó kế thừa từ ông), trừ những thành viên được đánh
dấu là private, hàm tạo, hàm hủy.
Trong ví dụ trên, Parrot thừa hưởng thuộc tính Weight, phương thức Feed và Fly của
Bird. Cockatoo sẽ thừa hưởng Weight, Feed và Fly (từ Bird), đồng thời thừa hưởng phương
thức Speak từ Parrot.
Tuy nhiên, Parrot (và Cockatoo) lại không kế thừa được biến thành viên _weight từ
Bird vì biến này để mức truy cập là private, cũng như không thể kế thừa các hàm tạo Bird()
và Bird(int). Tương tự, Cockatoo cũng không kế thừa được hàm tạo Parrot() và Parrot(int).
4.1.2. Lớp Object và kế thừa
Toàn bộ .NET framework được xây dựng dựa trên khái niệm “tất cả đều là object”,
vốn hoạt động trên cơ sở kế thừa.
Trong C#, mọi class đều là hậu duệ của lớp System.Object, kể cả khi không ghi
quan hệ kế thừa với lớp này.
Một số nguyên, số thực, biến logic, v.v. trong C# đều là object của các class tương
ứng (Int32, Double, Boolean) kế thừa từ lớp System.Object. Tuy nhiên, C# hỗ trợ để có
thể, ví dụ, gán giá trị số trực tiếp, thay thì phải khởi tạo object của lớp số nguyên tương
ứng.
Bởi vì mọi class trong C# đều kế thừa (trực tiếp hoặc gián tiếp) từ lớp System.Object,
bất kỳ class nào xây dựng xong đều có sẵn 4 phương thức: ToString, GetHashCode,
GetType, Equals. Đây là bốn phương thức của lớp Object. Như vậy, kế thừa là một cơ chế
tái sử dụng code để mở rộng ứng dụng.
Trong ví dụ trên, Bird, Parrot, Cockatoo đều là các hậu duệ của lớp System.Object,
vì vậy các class này đều thừa hưởng 4 phương thức kể trên.
Trang 139
Trung tâm Tin học – Ngoại ngữ
Để xem một class có những thành viên nào có thể dùng từ khóa this ở bên trong thân
bất kỳ phương thức nào như sau:
Như vậy, chúng ta thấy rằng, cơ chế kế thừa cho phép tái sử dụng code từ những class
sẵn có để tạo ra class mới một cách nhanh chóng. Mỗi class con là một bản mở rộng của
class cha bằng cách thêm vào những thành viên của riêng mình.
4.1.3. Quan hệ giữa kế thừa và đa hình
Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác nhau.
Trang 140
Lập trình Hướng đối tượng
Đa hình thiết lập mối quan hệ “là” (is-a relationship) giữa kiểu cơ sở và kiểu dẫn xuất.
Ví dụ, nếu chúng ta có lớp cơ sở Bird và lớp dẫn xuất Parrot thì một object của Parot cũng
là object của Bird, kiểu Parrot cũng là kiểu Bird (đương nhiên rồi, vẹt là chim mà!). Mối
quan hệ này nhìn rất giống như quan hệ kế thừa ở trên.
Trong khi đó, kế thừa liên quan chủ yếu đến tái sử dụng code: code của lớp con thừa
hưởng code của lớp cha. Một cách nói khác, đa hình liên quan tới quan hệ về ngữ nghĩa,
còn kế thừa liên quan tới cú pháp.
Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được đồng nhất,
thể hiện ở chỗ:
• Class con thừa hưởng các thành viên của class cha (kế thừa, tái sử dụng code);
• Một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là kiểu cơ sở có
thể dùng để thay thế cho kiểu dẫn xuất (đa hình).
Vì những lý do trên mà các lệnh khai báo và khởi tạo sau là hoàn toàn đúng:
Bird parrotTheBird = new Parrot();
Parrot cockatooTheParrot = new Cockatoo();
Bird cockatooTheBird = new Cockatoo();
Lệnh thứ nhất khai báo một object thuộc kiểu Bird nhưng được gán một object thuộc
kiểu Parrot.
Lệnh thứ hai khai báo biến thuộc kiểu Parrot nhưng được gán object thuộc kiểu
Cockatoo, kết quả như sau:
Trang 141
Trung tâm Tin học – Ngoại ngữ
Lệnh thứ ba khai báo biến thuộc kiểu Bird nhưng được gán object thuộc kiểu
Cockatoo với kết quả như sau:
Chúng ta cũng có thể để ý thấy rằng, các object của lớp con thực sự được khởi tạo
(các hàm tạo được gọi theo trật tự giống như đã gặp ở bài trước). Nhưng các object này lại
được tham chiếu tới từ các biến thuộc kiểu cha.
Trong những trường hợp này, object chỉ có thể sử dụng được những thành viên của
lớp cha. Ví dụ, object parrotTheBird ở trên chỉ có thể sử dụng các thành viên của lớp Bird
mà không biết về các thành viên mới của Parrot (như phương thức Speak).
Cơ chế quan hệ này kết hợp với ghi đè (overriding) và che giấu (hiding) cung cấp cho
người lập trình công cụ đặc biệt mạnh.
4.2. Che giấu phương thức
Như trên đã phân tích, class con thừa hưởng tất cả các thành viên mà lớp cha cho
phép. Vậy điều gì xảy ra nếu trong lớp con chúng ta định nghĩa một phương thức trùng với
phương thức nó kế thừa từ lớp cha?
Hãy tưởng tượng bây giờ chúng ta xây dựng lớp Chicken kế thừa từ Bird như sau:
class Chicken : Bird
Trang 142
Lập trình Hướng đối tượng
{
public void Fly()
{
Console.WriteLine("Chicken cannot fly");
}
}
Vì Chicken kế thừa Bird, nó cũng thừa hưởng phương thức Fly của Bird. Nhưng trong
lớp Chicken lại định nghĩa một phương thức Fly với mô tả giống hệt Fly của Bird.
Nếu để ý trong trình soạn thảo code, Intellisense của Visual Studio hiển thị như sau:
Cảnh báo của Visual Studio về che giấu phương thức
Thông báo này có ý nghĩa là phương thức Fly của lớp Chicken sẽ che đi phương thức
Fly của lớp Bird.
Trong những tình huống tương tự, C# tự động áp dụng cơ chế che giấu phương
thức (method hiding).
Để đảm bảo đây đúng là hành động mà người lập trình mong muốn, C# yêu cầu phải
ghi rõ từ khóa “new” trước khai báo phương thức như sau:
public new void Fly()
{
Console.WriteLine("Chicken cannot fly");
}
Trong tình huống này cả hai phương thức Fly đều cùng tồn tại trong object (của class
con) nhưng phụ thuộc vào loại biến tham chiếu tới (biến chứa địa chỉ) object (biến khai
báo thuộc kiểu con hay biến khai báo thuộc kiểu cha) sẽ quyết định sử dụng phương thức
nào:
Trang 143
Trung tâm Tin học – Ngoại ngữ
• Nếu biến chứa địa chỉ object được khai báo là kiểu cha, phương thức Fly của object
cha sẽ được gọi;
• Nếu biến chứa địa chỉ object được khai báo là kiểu con, phương thức Fly của object
con sẽ được gọi.
Trong ví dụ trên, nếu khai báo và khởi tạo như sau:
Chicken chicken = new Chicken();
chicken.Fly();
Kết quả thực hiện sẽ là:
Nếu khai báo và khởi tạo như sau:
Bird chicken = new Chicken();
chicken.Fly();
Kết quả thực hiện sẽ là:
Như vậy trong trường hợp này, phương thức Fly định nghĩa ở lớp con sẽ đơn giản là
“che” phương thức Fly mà nó kế thừa từ lớp cha. Cả hai cùng tồn tại trong cùng một object.
Kiểu của biến sẽ quyết định phương thức nào được gọi.
4.3. Ghi đè phương thức
Bây giờ hãy điều chỉnh lớp Bird, phương thức Fly như sau (thêm từ khóa virtual vào
trước khai báo phương thức):
Trang 144
Lập trình Hướng đối tượng
public virtual void Fly() => Console.WriteLine("Bird is flying");
Thay đổi phương thức Fly của lớp Chicken như sau (đổi từ khóa new thành override);
public override void Fly()
{
Console.WriteLine("Chicken cannot fly");
}
Đoạn code:
Chicken chicken = new Chicken();
chicken.Fly();
sẽ cho kết quả:
Đoạn code:
Bird chicken = new Chicken();
chicken.Fly();
cho kết quả:
Hai kết quả này giống nhau. Vậy điều gì đã xảy ra?
Đây là kết quả hoạt động của cơ chế ghi đè (overring) phương thức.
Trong cơ chế ghi đè, phương thức Fly của lớp cha (mà Chicken kế thừa) sẽ bị xóa bỏ
và thay thế bằng phương thức Fly mới định nghĩa trong lớp Chicken. Nói cách khác, trong
Trang 145
Trung tâm Tin học – Ngoại ngữ
object tạo ra từ Chicken giờ đây chỉ còn một phương thức Fly duy nhất. Do đó, bất kể biến
tham chiếu tới nó được khai báo là kiểu gì thì cũng chỉ truy xuất được phương thức Fly
này.
Để áp dụng được cơ chế ghi đè, cả lớp cha và lớp con cần phải phối hợp:
• Lớp cha phải cho phép phương thức được phép ghi đè bằng cách thêm từ khóa
virtual trước khai báo phương thức;
• Lớp con phải thông báo rõ việc ghi đè bằng cách thêm từ khóa override trước định
nghĩa phương thức.
Mặc định các phương thức của class không cho ghi đè mà chỉ cho phép che giấu.
Tuy nhiên, các phương thức Equals, GetHashCode, ToString của lớp tổ
tiên System.Object đều cho phép ghi đè ở lớp hậu duệ.
Để xác định những phương thức nào cho phép ghi đè, chỉ cần viết từ khóa override
trong thân class (bên ngoài phương thức).
Che dấu được sử dụng chủ yếu để đảm bảo tương thích ngược giữa các class. Cơ chế
này không được sử dụng nhiều trong thực tế.
Ở phía khác, ghi đè được sử dụng rất phổ biến cùng với đa hình giúp tạo ra một class
đại diện cho các biến thể khác nhau.
Trang 146
Lập trình Hướng đối tượng
4.4. Lớp trừu tượng và kế thừa
4.4.1. Lớp và trừu tượng hóa
Theo cách suy nghĩ hướng đối tượng, chúng ta phải trừu tượng hóa các đối tượng để
tạo ra class.
Ví dụ, từ việc phân tích nhiều chiếc bàn cụ thể chúng ta rút ra:
• Những chiếc bàn cụ thể phải có chân, dù là 3 chân, 4 chân hoặc nhiều chân hơn.
Như vậy, số chân là một đặc điểm chung của bàn.
• Mỗi chiếc bàn có thể sơn màu trắng, đỏ, vàng, v.v.. Vậy màu sắc cũng là một đặc
điểm chung.
• Mỗi chiếc bàn có thể to nhỏ khác nhau nhưng đều có một diện tích mặt để sử dụng.
Vậy diện tích bề mặt sử dụng cũng là một đặc điểm của bàn.
Qua phân tích này chúng ta thấy có 3 loại thông tin có thể dùng để mô tả cho một
chiếc bàn bất kỳ: số chân, màu sắc, diện tích mặt. Tuy nhiên, mỗi chiếc bàn cụ thể lại không
giống nhau, thể hiện ở giá trị cụ thể của số chân, màu sắc và diện tích mặt.
Như vậy, khi chúng ta mô tả “Bàn” bằng ba loại thông tin đại diện như trên, chúng ta
đã trừu tượng hóa từ những chiếc bàn cụ thể về một loại thông tin chung mô tả cho bàn.
Loại thông tin chung này chính là class, và từng chiếc bàn cụ thể là object. Class, do đó, là
dạng trừu tượng hóa, là mô tả chung của các đối tượng cụ thể.
Bây giờ chúng ta lại phân tích tiếp những chiếc ghế và tủ theo cách tương tự và lần
lượt thu được các lớp Ghế và Tủ.
Nếu chúng ta tiếp tục phân tích những điểm chung của Bàn, Ghế, và Tủ, chúng ta lại
có thể trừu tượng hóa một lần nữa để tạo ra lớp Nội thất.
Tuy nhiên, khi nói đến nội thất, chúng ta lại không thể đưa ra hình dung chính xác
của nó. Khác với khi nói đến Bàn chúng ta hình dung đại khái được một chiếc bàn.
Trang 147
Trung tâm Tin học – Ngoại ngữ
Như vậy, Nội thất là một loại trừu tượng hóa cấp độ cao hơn nữa. Nó không cho ra
hình dung cụ thể nào mà chỉ có thể được hình dung thông qua các class con cụ thể của nó
là Bàn, Ghế, hoặc Tủ. Loại class để mô tả nội thất như vậy trong lập trình hướng đối tượng
có tên gọi riêng: lớp trừu tượng (abstract class).
4.4.2. Lớp trừu tượng
Lớp trừu tượng (abstract class) là loại class có mức độ trừu tượng cao dùng làm khuôn
mẫu để tạo ra các class khác.
Như vậy, class bình thường là khuôn mẫu để tạo ra object (là những thực thể), còn
class trừu tượng lại dùng làm khuôn mẫu để tạo ra class khác. Sự khác biệt này dẫn đến
tình huống là lớp trừu tượng không được sử dụng để tạo ra object như class bình thường.
Trong C#, lớp trừu tượng được xây dựng bằng cách thêm từ khóa abstract vào trước
từ khóa class khi khai báo. Lớp trừu tượng không thể dùng để khởi tạo object mà chỉ đóng
vai trò lớp cơ sở để tạo ra các lớp dẫn xuất, là những trường hợp cụ thể hơn.
Ví dụ khai báo lớp trừu tượng Animal:
abstract class Animal // đây là một lớp trừu tượng
{
}
Lớp Animal không cho phép tạo object. Do đó, lệnh khởi tạo sau sẽ bị báo lỗi:
var animal = new Animal();
// lỗi! Lớp Animal không cho phép khởi tạo object
4.4.3. Phương thức trừu tượng
Một điểm rất mạnh của lớp trừu tượng là nó chứa bên trong các phương thức trừu
tượng (abstract method).
Phương thức trừu tượng (abstract method) là loại phương thức được khai báo trong
thân lớp trừu tượng với từ khóa abstract và không có thân phương thức. Ví dụ:
Trang 148
Lập trình Hướng đối tượng
abstract class Animal // đây là một lớp trừu tượng
{
public abstract void Eat();
// đây là khai báo phương thức trừu tượng Eat không có thân.
public abstract void Move();
// khai baos phương thức trừu tượng Move.
}
Nếu trong thân một class khai báo một phương thức trừu tượng thì class chứa nó bắt
buộc phải khai báo là abstract.
Một class kế thừa từ lớp trừu tượng này bắt buộc phải ghi đè tất cả phương thức trừu
tượng của class mà nó thừa kế.
class Dog : Animal
{
public override void Eat() => Console.WriteLine("I love bone!");
public override void Move() => Console.WriteLine("I walk on 4
feet");
}
Phương thức trừu tượng mặc định được đánh dấu virtual (cho phép ghi đè) nên bạn
không cần (không được) dùng từ khóa virtual trước phương thức abstract nữa.
Nếu không ghi đè đủ các phương thức abstract của lớp cha, lớp con bắt buộc cũng
phải đánh dấu là abstract:
abstract class Dog : Animal
// Dog không ghi đè hết phương thức abstract của Animal nên nó phải
đánh dấu abstract
{
public override void Eat() => Console.WriteLine("I love bone!");
}
Yêu cầu này làm cho lớp và phương thức trừu tượng trở thành một công cụ rất mạnh:
nó tạo ra một “bản hợp đồng” chứa danh sách các phương thức mà tất cả các lớp dẫn xuất
bắt buộc phải thực thi.
Trang 149
Trung tâm Tin học – Ngoại ngữ
Nói theo cách khác, lớp trừu tượng được sử dụng làm khuôn mẫu đề tạo ra class khác.
Để bắt các class dẫn xuất tuân thủ theo các quy tắc chung, trong lớp trừu tượng sử dụng
các phương thức trừu tượng với vai trò hợp đồng. Các class dẫn xuất từ lớp trừu tượng bắt
buộc phải tuân thủ hợp đồng khi kế thừa từ lớp trừu tượng bằng cách xây dựng các phương
án cụ thể của phương thức trừu tượng.
4.5. Interface trong C#
4.5.1. Quan hệ phụ thuộc giữa các class
Class B được coi là phụ thuộc chặt vào class A nếu class A được sử dụng trong code
của B (như tham chiếu tới A, nhận tham số kiểu A, khởi tạo object của A, khai báo biến
của A, v.v.).
Quan hệ phụ thuộc chặt này đơn giản khi sử dụng nhưng có thể gây ra nhiều hậu quả.
Quan hệ phụ thuộc chặt yêu cầu các lớp phụ thuộc phải xây dựng sau, dẫn tới không thể
phát triển song song các class. Quan hệ chặt cũng có thể gây khó khăn cho việc test các
class độc lập (vì chúng phụ thuộc vào nhau).
Để có thể phát triển song song hoặc dễ dàng thay thế class này bằng class khác, người
ta cần làm giảm sự phụ thuộc giữa các class, thay phụ thuộc chặt bằng phụ thuộc
lỏng (loosely-coupling).
Một công cụ rất mạnh thường được sử dụng để làm giảm sự phụ thuộc này là giao
diện (interface).
4.5.2. Khái niệm interface
Interface là một kiểu dữ liệu tương tự như class nhưng chỉ đưa ra mô tả (specification
/ declaration) của các thành viên mà không đưa ra phần thực thi (phần thân, body /
implementation). Phần thân của các phương thức sẽ phải được xây dựng trong các class
thực thi giao diện này.
Trang 150