Lập trình Hướng đối tượng
Một cách gần đúng, interface gần giống như một abstract class trong đó tất cả các
phương thức của class đều được đánh dấu là abstract.
Cũng giống như abstract class, interface không thể dùng để khởi tạo object mà chỉ để
các lớp cụ thể “kế thừa”. Khi một class “kế thừa” từ một interface, nó bắt buộc phải cung
cấp phần thực thi cho tất cả các thành viên của interface (tương tự như phải thực thi tất cả
các thành viên abstract).
Interface tạo ra một bản “hợp đồng” mô tả những gì cần phải làm mà các class thực
thi interface đó phải tuân thủ theo. Khi đó, các class phối hợp với nhau thông qua bản hợp
đồng này mà không cần biết đến nhau nữa (làm mất quan hệ chặt).
Vì đặc điểm đó, interface trở thành một công cụ đặc biệt mạnh giúp tạo ra mối quan
hệ lỏng giữa các class, qua đó giúp phát triển và test các thành phần (class) một cách độc
lập.
Khi sử dụng interface vẫn phải thực hiện khởi tạo object của một class cụ thể thực thi
interface này. Thao tác khởi tạo này thực hiện ở một class trung gian.
Ví dụ:
using System;
namespace ConsoleApp
{
internal interface IPet
// khai báo interface với hai phương thức
{
void Feed(); // mô tả phương thức (không có thân)
void Sound();
}
internal interface IBird
// khai báo interface với ba phương thức
{
void Fly();
void Sound();
void Feed();
}
Trang 151
Trung tâm Tin học – Ngoại ngữ
internal class Cat : IPet // Cat thực thi IPet
{
public Cat() => Console.WriteLine("I'm a cat. ");
// thực thi cho phương thức Feed và Sound
// hai phương thức này thực thi theo kiểu implicit
public void Feed() => Console.WriteLine("Fish, please!");
public void Sound() => Console.WriteLine("Meow meow!");
}
internal class Dog : IPet // Dog thực thi IPet
{
public Dog() => Console.WriteLine("I'm a dog. ");
// cả hai phương thức Feed và Sound thực thi kiểu explicit.
// Object của Dog không thể gọi hai phương thức này.
// Hai phương thức này chỉ có thể gọi qua giao diện IPet
void IPet.Feed() => Console.WriteLine("Bone, please!");
void IPet.Sound() => Console.WriteLine("Woof woof!");
}
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao
diện
{
public Parrot() => Console.WriteLine("I'm a parrot. ");
// hai phương thức này thực thi kiểu implicit, do đó
// có thể gọi từ object của Parrot
public void Feed() => Console.WriteLine("Nut, please!");
public void Fly() => Console.WriteLine("Yeah, I can fly!");
// hai phương thức này thực thi kiểu explicit, do đó
// không thể gọi từ object của Parrot
// mà chỉ có thể gọi qua giao diện IPet hoặc IBird
void IPet.Sound() => Console.WriteLine("I can speak!");
void IBird.Sound() => Console.WriteLine("I can sing, too!");
}
internal class BirdLover
{
private IBird _bird;
public BirdLover(IBird bird) => _bird = bird;
public void Play()
{
// _bird có thể gọi đủ các phương thức của IBird
Console.Write("Fly ...");
_bird.Fly();
Console.Write("Say something ...");
_bird.Sound();
Console.Write("What do you like to eat? ");
Trang 152
Lập trình Hướng đối tượng
_bird.Feed();
}
}
internal class PetLover
{
private IPet _pet;
public PetLover(IPet pet) => _pet = pet;
public PetLover() { }
public void Play()
{
// _pet có thể gọi đủ các phương thức của IPet
Console.Write("What do you like to eat? ");
_pet.Feed();
Console.Write("Now say something ... ");
_pet.Sound();
}
}
internal class _18_interface
{
private static void Main()
{
IPet pet = new Dog();
PetLover petLover = new PetLover(pet);
petLover.Play();
petLover = new PetLover(new Parrot());
petLover.Play();
BirdLover birdLover = new BirdLover(new Parrot());
birdLover.Play();
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
cat.Feed(); cat.Sound();
IPet cat2 = new Cat();
// cat2 có thể gọi Feed và Sound
cat2.Feed(); cat2.Sound();
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi
được Sound
parrot.Feed(); parrot.Fly();
IBird parrot2 = new Parrot();
// (gọi qua giao diện) parrot2 gọi được đủ 3 phương thức của
IBird
parrot2.Feed(); parrot2.Fly(); parrot2.Sound();
// dog không gọi được phương thức nào (gọi qua object) do
Trang 153
Trung tâm Tin học – Ngoại ngữ
} // cả hai phương thức của Dog đều thực hiện kiểu explicit
Dog dog = new Dog();
4.5.3. IPet dog2 = new Dog();
// gọi qua giao diện: dog2 gọi được cả Feed và Sound
dog2.Feed(); dog2.Sound();
Console.ReadKey();
}
}
Kỹ thuật lập trình với Interface
Khai báo kiểu interface
Trong ví dụ trên chúng ta xây dựng hai interface: IPet và IBird
• IPet
• IBird
internal interface IPet // khai báo interface với hai phương thức
{
void Feed(); // mô tả phương thức (không có thân)
void Sound();
}
Interface được khai báo với từ khóa interface và danh sách mô tả các phương thức,
đặc tính hoặc biến thành viên.
Một interface có thể được sử dụng nội bộ trong project, hoặc được sử dụng bởi các
project khác. Trong tình huống thứ nhất (mặc định), interface sử dụng từ khóa điểu khiển
truy cập internal (tương tự class), và do đó có thể không cần viết từ khóa internal. Trong
tình huống thứ hai sử dụng từ khóa public.
Interface là một kiểu dữ liệu cùng cấp độ với class, do đó có thể được khai báo trực
tiếp trong không gian tên hoặc trong phạm vi của class khác. Tên của interface được đặt
giống quy ước tên class nhưng có thêm chữ “I” đứng trước. Như ví dụ trên, tên hai interface
lần lượt là IPet, IBird.
Trang 154
Lập trình Hướng đối tượng
Trong interface chỉ có các mô tả, không có thân phương thức. Mô tả phương thức
không có từ khóa điều khiển truy cập (tức là không có public, private, protected trước các
mô tả).
4.5.4. Thực thi interface
Mặc dù interface là một kiểu dữ liệu nhưng tự bản thân nó không có khả năng sinh ra
object mà chỉ có thể tạo ra biến tham chiếu đến object của các class khác tuân thủ theo quy
định của interface.
Interface được sử dụng làm khuôn mẫu để sinh ra các class khác (gần giống như lớp
abstract). Việc tạo ra một class trên cơ sở khuôn mẫu của interface gọi là thực thi interface.
Cấu trúc cú pháp để một class thực thi một interface như sau:
internal class Cat : IPet // Cat thực thi IPet
internal class Dog : IPet // Dog thực thi IPet
Một class cũng có thể thực thi nhiều interface:
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao
diện
Khi một class thực thi một hoặc nhiều interface, nó có nghĩa vụ phải xây dựng tất cả
các thành viên được mô tả trong interface. Visual Studio hỗ trợ bằng cách đánh dấu lỗi cú
pháp (gạch chân đỏ) nếu class chưa xây dựng đủ các thành viên của interface theo yêu cầu.
Có hai cách thức thực thi các thành viên của interface: implicit và explicit.
Trong cách thực thi implicit không chỉ rõ là phương thức được thực thi thuộc về
interface nào; ngược lại, cách thực thi explicit phải chỉ rõ phương thức đang thực thi thuộc
về interface nào.
Lớp Cat ở đây hoàn toàn áp dụng cách thực thư implicit.
internal class Cat : IPet // Cat thực thi IPet
{
Trang 155
Trung tâm Tin học – Ngoại ngữ
public Cat() => Console.WriteLine("I'm a cat. ");
// thực thi cho phương thức Feed và Sound
// hai phương thức này thực thi theo kiểu implicit
public void Feed() => Console.WriteLine("Fish, please!");
public void Sound() => Console.WriteLine("Meow meow!");
}
Lớp Dog lại hoàn toàn thực thi kiểu explicit. Mỗi phương thức khi thực thi phải chỉ
rõ nó thuộc interface nào.
internal class Dog : IPet // Dog thực thi IPet
{
public Dog() => Console.WriteLine("I'm a dog. ");
// cả hai phương thức Feed và Sound thực thi kiểu explicit.
// Object của Dog không thể gọi hai phương thức này.
// Hai phương thức này chỉ có thể gọi qua giao diện IPet
void IPet.Feed() => Console.WriteLine("Bone, please!");
void IPet.Sound() => Console.WriteLine("Woof woof!");
}
internal class Dog : IPet // Dog thực thi IPet
{
public Dog() => Console.WriteLine("I'm a dog. ");
// cả hai phương thức Feed và Sound thực thi kiểu explicit.
// Object của Dog không thể gọi hai phương thức này.
// Hai phương thức này chỉ có thể gọi qua giao diện IPet
void IPet.Feed() => Console.WriteLine("Bone, please!");
void IPet.Sound() => Console.WriteLine("Woof woof!");
}
Lớp Parrot áp dụng cả implicit và explicit
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao
diện
{
public Parrot() => Console.WriteLine("I'm a parrot. ");
// hai phương thức này thực thi kiểu implicit, do đó
// có thể gọi từ object của Parrot
public void Feed() => Console.WriteLine("Nut, please!");
public void Fly() => Console.WriteLine("Yeah, I can fly!");
// hai phương thức này thực thi kiểu explicit, do đó
// không thể gọi từ object của Parrot
// mà chỉ có thể gọi qua giao diện IPet hoặc IBird
Trang 156
Lập trình Hướng đối tượng
void IPet.Sound() => Console.WriteLine("I can speak!");
void IBird.Sound() => Console.WriteLine("I can sing, too!");
}
Nếu phương thức được thực thi theo kiểu explicit thì không được phép sử dụng từ
khóa điều khiển truy cập.
Sự khác biệt lớn nhất giữa implicit và explicit thể hiện ở việc sử dụng object của
class.
4.5.5. Sử dụng interface
Kiểu interface
Interface có thể sử dụng như một kiểu dữ liệu để khai báo biến. Biến của interface
cho phép gọi các thành viên của interface giống như một object bình thường của class.
• BirdLover
• PetLover
internal class BirdLover
{
private IBird _bird;
public BirdLover(IBird bird) => _bird = bird;
public void Play()
{
// _bird có thể gọi đủ các phương thức của IBird
Console.Write("Fly ...");
_bird.Fly();
Console.Write("Say something ...");
_bird.Sound();
Console.Write("What do you like to eat? ");
_bird.Feed();
}
}
Trong hai class BirdLover và PetLover chúng ta sử dụng hai biến _bird và _pet giống
như một object bình thường.
Trang 157
Trung tâm Tin học – Ngoại ngữ
Tuy nhiên, biến của interface bắt buộc phải tham chiếu tới một object thực sự. Như
trong hai lớp trên, object của class được truyền qua tham số của hàm tạo. Nếu không cho
biến của interface tham chiếu tới một object thực sự, khi chạy chương trình sẽ gặp lỗi
‘Object reference not set to an instance of an object’ ở các lời gọi hàm hoặc truy xuất thành
viên.
Ví dụ, lệnh sau sẽ báo lỗi khi chạy:
PetLover petLover2 = new PetLover();
petLover2.Play();
Ở đây chúng ta sử dụng constructor không tham số của lớp PetLover (nghĩa là không
truyền object nào để gán cho biến _pet. Chương trình sẽ báo lỗi ở lời gọi _pet.Feed() vì
_pet không hề tham chiếu tới một object nào.
Khởi tạo object
Interface có thể dùng để khai báo biến (như ở trên) nhưng không thể tự khởi tạo
object. Biến kiểu interface chỉ có thể tham chiếu tới object của class thực thi interface đó.
IPet pet = new Dog();
PetLover petLover = new PetLover(pet);
petLover.Play();
petLover = new PetLover(new Parrot());
petLover.Play();
BirdLover birdLover = new BirdLover(new Parrot());
birdLover.Play();
Nói một cách khác, chúng ta cần sử dụng một class cụ thể thực thi interface để khởi
tạo object rồi gán object đó cho biến interface.
Đối với các class thực thi interface phụ thuộc vào cách thực thi (explicit hay implicit),
có sự khác biệt khi sử dụng object của các class này:
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
Trang 158
Lập trình Hướng đối tượng
cat.Feed(); cat.Sound();
IPet cat2 = new Cat();
// cat2 có thể gọi Feed và Sound
cat2.Feed(); cat2.Sound();
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi được
Sound
parrot.Feed(); parrot.Fly();
IBird parrot2 = new Parrot();
// (gọi qua giao diện) parrot2 gọi được đủ 3 phương thức của IBird
parrot2.Feed(); parrot2.Fly(); parrot2.Sound();
// dog không gọi được phương thức nào (gọi qua object) do
// cả hai phương thức của Dog đều thực hiện kiểu explicit
Dog dog = new Dog();
IPet dog2 = new Dog();
// gọi qua giao diện: dog2 gọi được cả Feed và Sound
dog2.Feed(); dog2.Sound();
Việc thực thi implicit tạo cho object của class khả năng sử dụng các phương thức như
class bình thường:
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
cat.Feed(); cat.Sound();
Những phương thức nào được thực thi kiểu explicit thì không thể gọi được trên object:
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi được
Sound
parrot.Feed(); parrot.Fly();
// dog không gọi được phương thức nào (gọi qua object) do
// cả hai phương thức của Dog đều thực hiện kiểu explicit
Dog dog = new Dog();
Vì có sự khác biệt giữa hai cách sử dụng object (parrot và parrot2)
Parrot parrot = new Parrot(); parrot.Feed(); parrot.Fly()
Trang 159
Trung tâm Tin học – Ngoại ngữ
IBird parrot2 = new Parrot(); parrot2.Feed(); parrot2.Fly();
parrot2.Sound();
Chúng ta gọi cách sử dụng thứ nhất là “gọi qua object”, cách sử dụng thứ hai gọi là
“gọi qua interface”.
Trong ví dụ trên, sự phụ thuộc giữa các class được thể hiện qua sơ đồ code sau.
4.6. Partial class trong C#
4.6.1. Khái niệm partial class
Trong các bài học từ đầu đến giờ, bạn xây dựng mỗi class trong một file đặt trùng tên
với class. Đây là cách thức tổ chức mã nguồn của class bình thường và phổ biến trong tất
cả các ngôn ngữ lập trình hướng đối tượng.
Giờ hãy hình dung một số tình huống khác.
Trang 160
Lập trình Hướng đối tượng
Visual studio có khả năng sinh code tự động để tạo ra nhiều class khác nhau. Ví dụ,
nếu bạn học lập trình với windows forms sẽ thấy, khi đặt một điều khiển lên form, trình
thiết kế của Visual studio sẽ tự động sinh code tương ứng. Vậy giờ nếu bạn tự viết thêm
code của mình vào file code được sinh tự động đó, code của bạn có thể bị mất đi nếu form
thay đổi (vì bạn không thể kiểm soát việc sinh code tự động). Ngoài ra, code sinh tự động
thường rất phức tạp. Bạn rất khó theo dõi các code sinh tự động này.
Đây là tình hình khi lập trình windows forms trong C# 1.0, khi chưa có khái niệm
partial class.
Visual studio cũng có một công cụ riêng giúp sinh code tự động theo mẫu, gọi là T4
Text Template. Bộ sinh code tự động này được sử dụng rất nhiều, ví dụ, cho asp.net, entity
framework. Nếu viết code vào file code sinh tự động, mỗi lần chạy lại bạn có thể mất hết
code của mình.
Từ những tình huống trên dẫn đến một nhu cầu đặc biệt: xây dựng MỘT class trên
NHIỀU file vật lý. Điều này giúp giải quyết những vấn đề vừa nêu. Phần sinh tự động đặt
ở một file độc lập. Phần bạn tự viết nằm trên một file khác. Thay đổi anh này không ảnh
hưởng đến anh kia. C# gọi loại class xây dựng trên nhiều file vật lý như vậy là partial class.
Partial class là một tính năng của C# cho phép định nghĩa một class trên nhiều file
vật lý khác nhau. C# compiler sẽ tự động ghép nối các file mã nguồn này trong quá trình
biên dịch.
Partial class là tính năng phục vụ cho các công cụ hỗ trợ thiết kế và sinh code tự động.
4.6.2. Sử dụng partial class trong C# project
Hãy cùng thực hiện ví dụ sau. Thêm project tên PartialClass vào solution. Thêm hai
file mã nguồn Student.Model.cs và Student.Methods.cs vào project.
Trang 161
Trung tâm Tin học – Ngoại ngữ
Lần lượt viết code cho các file như sau:
• Student.Model.cs
• Studen.Methods.cs
• Program.cs
using System;
namespace PartialClass
{
public partial class Student
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string Major { get; set; }
public string Specialization { get; set; }
}
}
Bạn để ý một số vấn đề sau:
Hai file mã nguồn Student.Model.cs và Student.Methods.cs đều chứa khai báo class
Student trong cùng namespace PartialClass. Rõ ràng, điều này nhẽ ra phải gây ra xung đột
định danh (name conflict). Tuy nhiên, khi bạn dịch chương trình sẽ thành công.
Vấn đề là ở chỗ, trước từ khóa class bạn gặp thêm từ khóa partial. Từ khóa này báo
cho C# compiler rằng hai khai báo này thuộc về cùng một class Student. Chỉ là code được
đặt trên hai file mã nguồn khác nhau. Khi dịch chương trình, C# compiler sẽ tự động ghép
nối chúng lại thành một class duy nhất.
Trang 162
Lập trình Hướng đối tượng
Do C# hiểu rằng hai khai báo thuộc về một class duy nhất, trong client code (phương
thức Main), bạn sử dụng được những thành viên khai báo trên cả hai file. Nói cách khác,
client code không phân biệt đây là partial class hay class thông thường.
4.6.3. Một số vấn đề khi sử dụng partial class trong C#
Như vậy có thể thấy, việc khai báo và sử dụng partial class thực ra rất đơn giản về cú
pháp cũng như ý tưởng:
• Nếu trong class có nhiều thành phần khác nhau nhưng bắt buộc phải thuộc về 1
class, bạn có thể sử dụng partial class để tách các phần đó sang các file riêng rẽ.
Ví dụ, phần thiết kế của form và xử lý sự kiện vốn có bản chất khác nhau nhưng
phải thuộc về cùng class. Visual studio tự động tạo form làm partial class.
• Nếu trong một class có những thành phần code thường biến động theo quá trình
phát triển và có những thành phần ổn định thì cũng có thể xem xét tách chúng ra
các file khác nhau để dễ quản lý. Trong đó, thành phần biến động ra một file riêng,
thành phần ổn định để lại một file riêng. Ví dụ, các thành phần field, property,
constructor thường cố định. Trong khi đó các member method thường biến động
nhiều hơn. Bạn có thể tách code thành hai phần riêng biệt.
• Nếu bạn sử dụng công cụ sinh code tự động, hãy để class đó làm partial class. Biết
đâu về sau bạn cần bổ sung code tự viết.
Ngoài ra, khi sử dụng partial class cần lưu ý các vấn đề sau:
Thứ nhất, các file mã nguồn của partial class phải nằm trong cùng một assembly (cùng
trong một project). Nếu bạn viết thư viện class và biên dịch để người khác sử dụng, trong
đó có xây dựng một partial class. Người dùng class đó không thể dùng cơ chế partial class
để mở rộng tiếp partial class của bạn được. Lý do rất đơn giản, partial class chỉ là tách code
ra nhiều file riêng rẽ để sau compiler tự mình gom lại. Một partial class đã được biên dịch
thì compiler không thể gom vào cùng code khác được nữa.
Thứ hai, trong mỗi phần code của partial class, bạn không được khai báo các thành
viên trùng nhau. Lý do là, mặc dù trải rộng trên nhiều file mã nguồn khác nhau nhưng các
Trang 163
Trung tâm Tin học – Ngoại ngữ
phần của partial class thuộc về cùng một class. C# không cho phép khai báo cùng một
thành viên nhiều lần.
Thứ ba, mỗi phần của class bắt buộc phải có đủ hai từ khóa partial class. Tuy nhiên,
modifier (public | internal) thì chỉ cần viết một lần. Lớp cơ sở hoặc interface mà class này
thừa kế cũng chỉ cần viết một lần.
4.7. Partial method trong C#
4.7.1. Khái niệm partial method
Trong partial class có thể chứa một loại thành viên đặc biệt mà class thông thường
không có: partial method. Tương tự như partial class, partial method cũng hướng tới hỗ
trợ sinh code tự động. Partial method xuất hiện từ C# 3.0 và được xem như thành phần mở
rộng cho partial class (xuất hiện từ C# 2.0).
Partial method cho phép code sinh tự động gọi phương thức nhưng không nhất thiết
phải xây dựng (implement) phương thức đó. Do vậy, partial method chỉ chứa signature (mô
tả) mà không có phần implementation (phần thân, phần thực thi). Nếu không tìm thấy phần
thực thi của partial method, compiler sẽ bỏ qua lệnh gọi partial method.
Người ta cũng thường gọi lời gọi partial method là hook. Hook nếu gắn với phần thực
thi sẽ được gọi như phương thức bình thường. Nếu không có phần thực thi, hook sẽ được
compiler bỏ qua.
Cơ chế trên giúp giữ khối lượng code nhỏ nhưng vẫn đảm bảo tính linh hoạt.
4.7.2. Sử dụng partial method
Hãy cùng thực hiện một ví dụ với partial method trong partial class. Chúng ta lặp lại
ví dụ trên nhưng với một số thay đổi nhỏ:
• Student.Model.cs
• Studen.Methods.cs
• Program.cs
Trang 164
Lập trình Hướng đối tượng
using System;
namespace PartialMethod
{
public partial class Student
{
private string _firstName;
private string _lastName;
partial void OnSettingFirstName(string value);
partial void OnSettingLastName(string value);
public int Id { get; set; }
public string FirstName
{
get => _firstName;
set {
OnSettingFirstName(value);
_firstName = value;
}
}
public string LastName
{
get => _lastName;
set {
OnSettingLastName(value);
_lastName = value;
}
}
public DateTime DateOfBirth { get; set; }
public string Major { get; set; }
public string Specialization { get; set; }
}
}
Khi dịch và chạy thử, chương trình sẽ báo lỗi khi gặp lệnh gán chuỗi rỗng cho
FirstName khi khởi tạo object student.
Trong ví dụ trên, ở file Student.Model.cs chúng ta đã chuyển FirstName và LastName
thành full property với hai backed field lần lượt là _firstName và _lastName.
Trang 165
Trung tâm Tin học – Ngoại ngữ
Bạn khai báo hai partial method OnSettingFirstName và OnSettingLastName:
partial void OnSettingFirstName(string value);
partial void OnSettingLastName(string value);
Bạn gọi hai partial method trong setter của hai property tương ứng:
public string FirstName
{
get => _firstName; set {
OnSettingFirstName(value);
_firstName = value;
}
}
public string LastName
{
get => _lastName; set {
OnSettingLastName(value);
_lastName = value;
}
}
Trong file Student.Methods.cs bạn thực thi hai partial method này:
private void CheckName(string value)
{
var temp = value.Trim();
if (string.IsNullOrEmpty(temp) || temp.Contains(" "))
throw new System.Exception("Tên sai quy cách");
}
partial void OnSettingFirstName(string value)
{
CheckName(value);
}
partial void OnSettingLastName(string value)
{
CheckName(value);
}
Logic của các phương thức này rất đơn giản: nếu giá trị gán cho FirstName hoặc
LastName chứa dấu cách, hoặc là xâu rỗng thì phát ra exception. Vì lí do này, nếu bạn chạy
client code như trên thì chương trình sẽ báo lỗi và mở lại giao diện code ngay.
Trang 166
Lập trình Hướng đối tượng
Trong ví dụ trên, giả sử Student.Model.cs được sinh tự động. Rõ ràng, bạn không
muốn gán cứng logic kiểm tra tính hợp lệ của FirstName và LastName mà muốn để cho
người dùng tự mình thực hiện logic riêng.
Do đó, bạn khai báo hai partial method và đặt sẵn hai hook (hai lời gọi partial method)
ở những vị trí phù hợp. Phần thân của partial method (chứa logic kiểm tra giá trị của
FirstName và LastName) để dành cho người lập trình tự viết trong file Student.Methods.cs.
Nếu người lập trình không tự viết phần thực thi thì hook không có ý nghĩa. Compiler
bỏ qua lời gọi hook kia. Nếu người lập trình viết phần thực thi, hook sẽ được thực thi như
phương thức bình thường.
4.7.3. Các lưu ý khi sử dụng partial method
Partial method chỉ có thể sử dụng bên trong partial class. Không thể sử dụng partial
method trong class thông thường.
Partial method bao gồm ba phần (và thường nằm trong các file khác nhau): phần khai
báo, phần sử dụng (gọi phương thức), phần thực thi.
Khai báo partial method bắt buộc phải bắt đầu bằng từ khóa partial, kết thúc là dấu
chấm phẩy sau danh sách tham số và không có thân.
Lời gọi partial method không có gì khác biệt với member method thông thường.
Phần thực thi thường đặt trong một file code khác. Phần thực thi giống hệt như xây
dựng một phương thức thông thường nhưng có từ khóa partial ở đầu. Ngoài ra, phần thực
thi và phần khai báo phải có signature giống hệt nhau.
Partial method bắt buộc phải có return type là void và không được có tham số out.
Tuy nhiên, tham số ref vẫn sử dụng được nếu bạn cần giữ lại thay đổi của tham số. Partial
method không được sử dụng access modifier như public, private, protected. Nó cũng không
được sử dụng các modifier khác như virtual, abstract, sealed, v.v..
Trang 167
Trung tâm Tin học – Ngoại ngữ
Nếu không tìm thấy phần thực thi, compiler sẽ bỏ qua lời gọi partial method.
Trang 168
Lập trình Hướng đối tượng
CHƯƠNG 5. NẠP CHỒNG TOÁN TỬ
5.1.1. Khái niệm nạp chồng toán tử
Đối với các kiểu dữ liệu số, C# định nghĩa sẵn một số phép toán như các phép toán
số học, phép toán so sánh, phép toán tăng giảm. Đối với kiểu string, như chúng ta đã biết,
được định sẵn phép toán cộng xâu.
Tuy nhiên, các kiểu dữ liệu (class) do người dùng định nghĩa lại không thể sử dụng
ngay các phép toán đó được.
Ví dụ, nếu người dùng định nghĩa kiểu số phức, các phép toán cơ bản trên kiểu số
phức lại không thể thực hiện được ngay, mặc dù về mặt toán học các phép toán đối với số
phức không có gì khác biệt với kiểu số được C# định nghĩa.
Để giải quyết những vấn đề tương tự, C# cho phép nạp chồng toán tử, tức là cho
phép định nghĩa lại những phép toán đã có với các kiểu dữ liệu do người dùng xây dựng.
Nạp chồng phương thức (method overloading) cùng với nạp chồng toán tử (operator
overloading) là hai hiện tượng thuộc về nguyên lý đa hình tĩnh (static polymorphism).
5.1.2. Cách nạp chồng toán tử
Hãy cùng thực hiện và phân tích ví dụ sau để hiểu cách nạp chồng toán tử. Chú ý xem
xét cú pháp nạp chồng đối với mỗi toán tử.
Ví dụ:
using System;
namespace OperatorOverload
{
/// <summary>
/// lớp biểu diễn hình hộp
/// </summary>
internal class Box
{
public double Length { get; set; }
public double Breadth { get; set; }
Trang 169
Trung tâm Tin học – Ngoại ngữ
public double Height { get; set; }
public Box() { }
public Box(double length, double breadth, double height)
{
Length = Length;
Breadth = breadth;
Height = height;
}
/// <summary>
/// tính thể tích khối hộp
/// </summary>
public double Volume => Length * Breadth * Height;
// nạp chồng phép cộng
public static Box operator +(Box b, Box c)
{
Box box = new Box
{
Length = b.Length + c.Length,
Breadth = b.Breadth + c.Breadth,
Height = b.Height + c.Height
};
return box;
}
// nạp chồng phép so sánh bằng
public static bool operator ==(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length == rhs.Length && lhs.Height == rhs.Height
&& lhs.Breadth == rhs.Breadth)
{
status = true;
}
return status;
}
// nạp chồng phép so sánh khác
public static bool operator !=(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length != rhs.Length || lhs.Height != rhs.Height
|| lhs.Breadth != rhs.Breadth)
{
status = true;
}
Trang 170
Lập trình Hướng đối tượng
return status;
}
// nạp chồng phép so sánh nhỏ hơn
public static bool operator <(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length < rhs.Length && lhs.Height < rhs.Height
&& lhs.Breadth < rhs.Breadth)
{
status = true;
}
return status;
}
// nạp chồng phép so sánh lớn hơn
public static bool operator >(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length > rhs.Length && lhs.Height >
rhs.Height && lhs.Breadth > rhs.Breadth)
{
status = true;
}
return status;
}
public override string ToString()
{
return string.Format("({0}, {1}, {2})", Length, Breadth,
Height);
}
}
internal class Program
{
private static void Main(string[] args)
{
Box Box1 = new Box(6, 7, 5);
Box Box2 = new Box(12, 13, 10);
Box Box3 = new Box();
Box Box4 = new Box();
/* phép cộng hai hình hộp cho ra hình hộp khác có kích
thước
* bằng tổng kích thước của hai hộp */
Box3 = Box1 + Box2;
Console.WriteLine("Box 3: {0}", Box3.ToString());
Trang 171
Trung tâm Tin học – Ngoại ngữ
Console.WriteLine("Volume of Box3 : {0}", Box3.Volume);
// so sánh hai hình hộp
if (Box1 > Box2)
Console.WriteLine("Box1 lớn hơn Box2");
else
Console.WriteLine("Box1 không lớn hơn Box2");
if (Box3 == Box4)
Console.WriteLine("Box3 bằng Box4");
else
Console.WriteLine("Box3 không bằng Box4");
Console.ReadKey();
}
}
}
Trong ví dụ trên chúng ta đã thực hiện nạp chồng cho phép toán cộng (+), các phép
so sánh (bằng ==, khác !=, lớn hơn >, nhỏ hơn <).
Cú pháp khai báo này được tổng hợp lại dưới đây:
public static Box operator +(Box b, Box c) {...}
public static bool operator ==(Box lhs, Box rhs) {...}
public static bool operator !=(Box lhs, Box rhs) {...}
public static bool operator <(Box lhs, Box rhs) {...}
public static bool operator >(Box lhs, Box rhs) {...}
Nếu để ý kỹ hơn nữa chúng ta thấy, đây đều là các phép toán binary. Cách nạp chồng
các phép toán này có cùng một cú pháp.
Mỗi loại phép toán sẽ có cách nạp chồng riêng. Tuy nhiên, cú pháp chung là:
public static <return_type> operator <operator>(<parameters>) { ... }
Các toán tử có thể nạp chồng: +, -, !, ~, ++, –, +, -, *, /, %, ==, !=, <, >, <=, >=
Ngoài ra phép toán indexer cũng là một phép toán có thể nạp chồng.
5.1.3. Một số lưu ý khi nạp thực hiện chồng toán tử
Các phép toán chia làm ba loại: unary (chỉ cần một toán hạng, như phép toán tăng
++, phép toán giảm –), binary (cần hai toán hạng, như các phép toán +,-,*,/), ternary (cần
Trang 172
Lập trình Hướng đối tượng
ba toán hạng, như phép toán điều kiện ?). Do đó, khi nạp chồng phép toán nào thì phải cung
cấp đủ lượng tham số phù hợp. Ví dụ, khi nạp chồng phép toán binary (như +, -) thì phải
cấp 2 tham số như đã làm ở trên.
Phép toán tăng giảm (++, –) thuộc loại unary nên trong danh sách tham số chỉ cần 1
tham số. Các phép toán này cũng không có giới hạn gì khi nạp chồng. Cùng ví dụ với lớp
Box trên:
public static Box operator ++ (Box b)
{
return new Box(b.Length++, b.Breadth++, b.Height++);
}
Các phép toán số học (+, – *, /, %) không đặt ra giới hạn gì khi nạp chồng. Bạn chỉ
cần tuân thủ đúng cú pháp như trên là được.
Bạn thậm chí có thể nạp chồng cùng một phép toán nhiều lần. Ví dụ, bạn hoàn toàn
có thể thêm nạp chồng phép + một lần nữa như sau:
public static Box operator +(Box b, double size)
{
return new Box(b.Length += size, b.Breadth += size, b.Height + size);
}
Ở đây bạn đã nạp chồng phép cộng Box với một số thực. Điều kiện để nạp chồng
phép toán nhiều lần là danh sách tham số của mỗi lần nạp chồng phải khác nhau.
Đối với các phép toán so sánh, bạn phải thực hiện nạp chồng cả cặp. Nghĩa là, nếu
nạp chồng phép so sánh bằng == thì đồng thời phải nạp chồng cả phép so sánh khác !=;
nếu nạp chồng phép so sánh hơn > thì phải nạp chồng cả phép so sánh kém <.
Các phép gán (+=, -=, v.v.) không cho phép nạp chồng trực tiếp. Tuy nhiên, nếu bạn
đã nạp chồng phép +,-, v.v. thì các phép toán này tự nhiên sẽ được nạp chồng. Ví dụ, nếu
bạn đã nạp chồng phép cộng Box với 1 số như trên thì hoàn toàn có thể gọi lệnh
var Box5 = Box4 += 5; // phép cộng gán với số
Trang 173
Trung tâm Tin học – Ngoại ngữ
Riêng phép toán indexer có cách thực hiện nạp chồng riêng dưới đây.
5.1.4. Nạp chồng phép toán indexer
Indexer là một phép toán giúp client code sử dụng object tương tự như khi sử dụng
mảng. Indexer thường được sử dụng với với các kiểu dữ liệu chứa trong nó một tập hợp dữ
liệu (collection hoặc array). Indexer giúp đơn giản hóa việc sử dụng ở client code.
Trước khi xem cú pháp nạp chồng toán tử indexer, hãy cùng thực hiện ví dụ sau đây:
namespace IndexerOverload
{
using static System.Console;
class Program
{
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
ReadLine();
}
}
class Vector
{
private double[] _components;
public Vector(int dimension)
{
_components = new double[dimension];
}
public Vector(params double[] components)
{
_components = components;
}
public override string ToString()
{
return $"({string.Join(", ", _components)})";
}
}
}
Trang 174
Lập trình Hướng đối tượng
Ví dụ này xây dựng một class Vector đơn giản dành cho vector n-chiều. Cả vector
được lưu trong một mảng private _components (mỗi phần tử của mảng là kích thước một
chiều của vector). Class này có 2 overload cho constructor, một cái nhận số chiều làm tham
số, một cái nhận mảng double làm tham số.
Bạn có muốn truy xuất giá trị từng chiều của vector này như truy xuất phần tử của
mảng không? Tức là viết kiểu vector[0], vector[1], v.v., trong đó 0, 1, là chỉ số chiều.
5.1.5. Cú pháp nạp chồng indexer
Cú pháp nạp chồng phép toán indexer cho class gần giống property, trong đó phải có
ít nhất một trong hai phương thức get/set, dùng để trả lại giá trị và gán giá trị. Khác biệt
duy nhất ở chỗ indexer bắt buộc sử dụng từ khóa this với cặp dấu ngoặc vuông. Biến làm
khóa phải đặt trong cặp dấu ngoặc vuông.
public TValue this[TKey key]
{
get{ }
set{ }
}
Trong đó:
• TValue là kiểu dữ liệu trả về, TKey là kiểu dữ liệu của khóa;
• số lượng khóa có thể nhiều hơn 1;
• kiểu của khóa có thể là bất kỳ kiểu dữ liệu nào (không nhất thiết là số hoặc chuỗi);
• phương thức get và set hoạt động giống như đối với thuộc tính.
5.1.6. Nạp chồng toán tử indexer cho lớp Vector
Thêm đoạn code sau vào class Vector:
public double this[int index]
{
get => (index < _components.Length) ? _components[index] :
double.NaN;
set { if (index < _components.Length) _components[index] = value;
}
}
Trang 175
Trung tâm Tin học – Ngoại ngữ
Cấu trúc này nhìn giống hệt như full property, ngoại trừ tên gọi this[int index].
Đoạn code này đã thực hiện nạp chồng toán tử indexer cho lớp Vector.
Logic hoạt động của getter rất đơn giản. Nếu index nhỏ hơn số phần tử của mảng thì
trả lại giá trị tương ứng index, nếu không thì trả về giá trị NaN (Not a Number). Setter chỉ
gán giá trị cho phần tử tương ứng của mảng.
Nếu phương thức get/set chỉ chứa một lệnh duy nhất có thể sử dụng cú pháp
“expression body” cho ngắn gọn. Trong code của getter ở trên chúng ta đã sử dụng cấu trúc
này.
Expression body là một lối viết xuất hiện từ C# 6: nếu thân của phương thức chỉ chứa
một lệnh duy nhất có thể sử dụng cấu trúc như sau để viết:
Tên_phương_thức() => lệnh;
Từ C# 7 có thể sử dụng expression body cho cả phương thức get và set của property.
Bây giờ bạn có thể truy xuất từng chiều của vector như sau:
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
var x = vector1[0];
var y = vector1[1];
var z = vector1[2];
WriteLine($"Vector components: x = {x}, y = {y}, z = {z}");
vector1[2] = 30;
vector1[1] = 20;
vector1[0] = 10;
WriteLine($"vector 1: {vector1}");
ReadLine();
}
Dưới đây là code đầy đủ của ví dụ trên:
Trang 176
Lập trình Hướng đối tượng
namespace IndexerOverload
{
using static System.Console;
class Program
{
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
var x = vector1[0];
var y = vector1[1];
var z = vector1[2];
WriteLine($"Vector components: x = {x}, y = {y}, z = {z}");
vector1[2] = 30;
vector1[1] = 20;
vector1[0] = 10;
WriteLine($"vector 1: {vector1}");
ReadLine();
}
}
class Vector :
{ =
public double this[int index]
{
get => (index < _components.Length) ? _components[index]
double.NaN;
set { if (index < _components.Length) _components[index]
value; }
}
private double[] _components;
public Vector(int dimension)
{
_components = new double[dimension];
}
public Vector(params double[] components)
{
_components = components;
}
Trang 177
Trung tâm Tin học – Ngoại ngữ
public override string ToString()
{
return $"({string.Join(", ", _components)})";
}
}
}
Trang 178
Lập trình Hướng đối tượng
CHƯƠNG 6. XỬ LÝ NGOẠI LỆ
6.1. Khái niệm exception
Ngoại lệ (exception) là những tình huống mà chương trình không thể thực hiện được
lệnh theo yêu cầu.
C# (và các ngôn ngữ lập trình khác) cung cấp các công cụ đặc biệt để sử dụng trong
những tình huống tương tự, bao gồm: thông báo ngoại lệ, bắt và xử lý ngoại lệ.
Lệnh biểu diễn với từ khóa throw ở trên là thông báo ngoại lệ (hay còn gọi là thông
báo lỗi).
Trong C# (và .NET framework), cách thức đơn giản nhất để phát ra thông báo ngoại
lệ là sử dụng lớp Exception với từ khóa throw theo cấu trúc:
throw new Exception(“thông tin về lỗi”);
Cấu trúc này khởi tạo một object của lớp Exception và gửi object này cho cơ chế
thông báo lỗi của .NET framework. Khi gọi lệnh throw, luồng điều khiển của chương trình
sẽ thay đổi.
Exception là lớp mô tả ngoại lệ cơ bản nhất trong .NET. Khi học đến phần kế thừa
chúng ta có thêm khả năng để tạo ra các lớp thông báo ngoại lệ riêng của mình. Các lớp
thông báo ngoại lệ do người dùng định nghĩa có khả năng đóng gói thêm nhiều thông tin
khác giúp ích cho quá trình dò lỗi.
6.1.1. Cơ chế xử lý exception trong C#
Khi một ngoại lệ được phát ra ở một vị trí bất kỳ trong chương trình, việc thực thi của
chương trình sẽ dừng lại. Nếu chương trình đang chạy ở chế độ Debug, trình soạn thảo
code sẽ được mở ra và đoạn code bị lỗi sẽ được đánh dấu giúp cho người lập trình xác định
vị trí và nguyên nhân gây lỗi.
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh chưa tồn
tại.
Trang 179
Trung tâm Tin học – Ngoại ngữ
Nếu chương trình chạy ở chế độ Release, chương trình sẽ bị dừng lại và cơ chế xử lý
ngoại lệ mặc định của .NET framework sẽ được kích hoạt để hiển thị lỗi. Chương trình
được dịch ở chế độ này sẽ không chạy được ở chế độ Debug nữa.
Đây là cơ chế bắt và xử lý lỗi mặc định của .NET framework đối với ứng dụng
console. Đối với ứng dụng windows form, giao diện bắt và xử lý lỗi có khác biệt.
Tuy nhiên, cơ chế thông báo lỗi mặc định của .NET framework tương đối không thân
thiện với người dùng.
.NET cung cấp cho các chương trình tính năng bắt và xử lý ngoại lệ để tự mình xác
định xem khi xảy ra lỗi (ngoại lệ) thì sẽ làm gì.
6.1.2. Kỹ thuật xử lý ngoại lệ (Exception Handling) trong C#
Chúng ta đã nhắc đến khái niệm ngoại lệ (exception) và xem xét cách thức đơn giản
nhất để phát thông báo ngoại lệ bằng lệnh throw và lớp Exception. Exception là một cơ
chế rất mạnh trong .NET giúp phát hiện lỗi logic trong chương trình ở giai đoạn Runtime.
6.1.3. Exception và chế độ Debug trong C#
Khi chạy chương trình ở chế độ debug, nếu phát sinh ngoại lệ, Visual Studio sẽ mở
file mã nguồn ở đúng vị trí lỗi cùng với thông báo cụ thể. Qua đó, chúng ta có thể xác định
nguồn gốc của lỗi và đưa ra cách giải quyết.
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh chưa tồn
tại.
Trang 180
Lập trình Hướng đối tượng
Ngoại lệ ở chế độ chạy debug
Đây là cơ chế bắt và xử lý lỗi ở chế độ Debug. Chương trình chúng ta viết từ đầu dự
án đến giờ đều dịch và chạy ở chế độ Debug.
.NET framework cung cấp nhiều lớp hỗ trợ thông báo lỗi kế thừa từ lớp Exception
với các thông tin chi tiết hơn về lỗi có thể gặp phải. Nếu một phương thức nào đó có khả
năng phát sinh lỗi, Visual Studio sẽ hiển thị danh sách các loại lỗi có thể gặp phải.
Ví dụ, đối với phương thức OpenRead của lớp File có thể phát sinh 8 loại Exception
khác nhau như lỗi vào ra (IOException), lỗi không tìm thấy file (FileNotFoundException),
lỗi không tìm thấy thư mục (DirectoryNotFoundException), v.v..
Danh sách các loại ngoại lệ này được thể hiện bằng các class khác nhau sẽ được sử
dụng nếu tình huống lỗi tương ứng phát sinh. Khi đặt con trỏ chuột lên tên phương thức
này chúng ta sẽ xem được danh sách các lớp chứa thông tin về ngoại lệ của phương thức:
Các ngoại lệ có thể phát sinh khi sử dụng phương thức OpenRead
Trang 181
Trung tâm Tin học – Ngoại ngữ
Thông tin này có nghĩa là, nếu phát sinh bất kỳ exception nào trong 8 loại exception
có thể xảy ra khi thực hiện phương thức này, lệnh throw sẽ được gọi cùng với một object
của loại exception tương ứng.
Ví dụ, nếu cung cấp một file không tồn tại, phương thức OpenRead không thể làm gì
được và sẽ phát lệnh throw new FileNotFoundException(), tương tự như cách chúng ta tự
phát ra thông báo ngoại lệ ở trên.
6.1.4. Xử lý ngoại lệ ở chế độ Release
Một chương trình trước khi đem triển khai cho người dùng cuối phải được dịch ở chế
độ Release. Chương trình được dịch ở chế độ này sẽ không chạy được ở chế độ Debug nữa.
Như chúng ta thấy, cơ chế bắt và xử lý lỗi mặc định của .NET framework tương đối
không thân thiện với người dùng.
.NET cũng cung cấp cho các chương trình tính năng bắt và xử lý ngoại lệ (Exception
Handling) để tự mình xác định hoạt động của chương trình khi xảy ra lỗi (ngoại lệ), tránh
phải sử dụng cơ chế bắt và xử lý lỗi mặc định.
6.2. Cấu trúc try – catch - finally
Cơ chế bắt và xử lý ngoại lệ sử dụng cấu trúc cú pháp sau:
try { <code có khả năng gây lỗi viết trong block này> }
catch(<loại lỗi 1> object1) { <hành động khi xảy ra lỗi> }
catch(<loại lỗi 2> object2) { <hành động khi xảy ra lỗi> }
… // có thể kết hợp nhiều khối catch nữa ở đây
finally{ <hành động sẽ thực hiện cả khi có lỗi hay không có lỗi> }
Cấu trúc này có 3 khối code:
Khối “try”: chứa các đoạn code có khả năng gây lỗi;
Các khối “catch”: dùng để bắt từng loại lỗi cụ thể.
Trang 182
Lập trình Hướng đối tượng
Khi xảy ra lỗi, khối này sẽ bắt object của lớp exception tương ứng (mà ở phần phát
ngoại lệ tạo ra cùng lệnh throw) và thực thi đoạn code tương ứng. Trong đoạn code này có
thể sử dụng các object chứa thông tin ngoại lệ mà khối catch này bắt được.
Chúng ta đã biết các lớp ngoại lệ kế thừa nhau tạo thành một cấu trúc phân cấp với
lớp Exception ở gốc. Nếu khối catch được chỉ định bắt loại lỗi cha, nó đồng thời bắt tất cả
các loại lỗi con kế thừa từ lớp cha đó. Nếu chúng ta bắt lỗi thuộc loại cao nhất là Exception
thì cũng đồng thời bắt tất cả các loại lỗi có thể phát sinh trong chương trình.
Khối “finally”: không bắt buộc. Nếu có mặt khối này, dù có xảy ra ngoại lệ hay không
thì các lệnh trong khối code này đều sẽ được thực hiện.
Trang 183