Từ khóa new và override
Trong ngôn ngữ C#, người lập trình có thể quyết định phủ quyết một phương thức ảo bằng cách khai báo tường minh từ khóa override. Điều này giúp cho ta đưa ra một phiên bản mới của chương trình và sự thay đổi của lớp cơ sở sẽ không làm ảnh hưởng đến chương trình viết trong các lớp dẫn xuất. Việc yêu cầu sử dụng từ khóa
override sẽ giúp ta ngăn ngừa vấn đề này.
Bây giờ ta thử bàn về vấn đề này, giả sử lớp cơ sở DongVat của ví dụ trước được viết bởi một công ty A. Cũng giả sử rằng lớp ConMeo đươc viết từ những người lập trình của công ty B và họ dùng lớp cơ sở DongVat mua được của công ty A làm lớp cơ sở cho lớp trên. Người lập trình trong công ty B không có hoặc có rất ít sự kiểm soát về những thay đổi trong tương lai với lớp DongVat do công ty A phát triển.
Khi nhóm lập trình của công ty B quyết định thêm một phương thức Keu( ) vào lớp ConMeo:
public virtual void Keu(string tiengkeu)
{
Console.WriteLine("Con {0} keu {1}", ten, tiengkeu);
}
Việc thêm vào vẫn bình thường cho đến khi công ty A, tác giả của lớp cơ sở DongVat, đưa ra phiên bản thứ hai của lớp DongVat. Và trong phiên bản mới này những người lập trình của công ty A đã thêm một phương thức Keu( ) vào lớp cơ sở DongVat:
public class Window
//……..
public virtual void Keu(string tiengkeu)
{//………. }
}
Trong các ngôn ngữ lập trình hướng đối tượng khác như C++, phương thức ảo mới Keu() trong lớp D ong Va t bây giờ sẽ hành động giống như là một phương thức cơ sở cho phương thức ảo trong lớp ConMeo. Trình biên dịch có thể gọi phương thức Keu( ) trong lớp ConMeo khi chúng ta có ý định gọi phương thức Keu( ) trong D ong V a t. Trong ngôn ngữ Java, nếu phương thức K e u( ) trong DongVat có kiểu trả về khác kiểu trả về của phương thức Keu( ) trong lớp ConMeo thì sẽ được báo lỗi là phương thức phủ quyết không hợp lệ.
Ngôn ngữ C# ngăn ngừa sự lẫn lộn này, trong C# một phương thức ảo thì được xem như là gốc rễ của sự phân phối ảo. Do vậy, một khi C# tìm thấy một phương thức khai báo là ảo thì nó sẽ không thực hiện bất cứ việc tìm kiếm nào trên cây phân cấp kế thừa. Nếu một phương thức ảo Keu( ) được trình bày trong lớp DongVat, thì khi thực hiện hành vi của lớp ConMeo không thay đổi.
Tuy nhiên khi biên dịch lại, thì trình biên dịch sẽ đưa ra một cảnh báo giống như sau:
…. 'LTHDTC.ConMeo.Keu(string)' hides inherited member
'LTHDTC.DongVat.Keu(string)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
C:\Tai lieu giang day\Lap trinh huong doi tuong\Vi
du\LTHDTC\LTHDTC\DongVat.cs 53 25 LTHDTC
Để loại bỏ cảnh báo này, người lập trình phải chỉ rõ ý định của anh ta. Anh ta có thể đánh dấu phương thức ConMeo.Keu( ) với từ khóa là new, và nó không phải phủ quyết của bất cứ phương thức ảo nào trong lớp DongVat:
public class ConMeo : DongVat
{
public new virtual Keu( ) {….}
Việc thực hiện khai báo trên sẽ loại bỏ được cảnh báo. Mặc khác nếu người lập trình muốn phủ quyết một phương thức trong DongVat, thì anh ta cần thiết phải dùng từ khóa override để khai báo một cách tường minh:
Public class ConMeo:DongVat
{
//……
Public override Keu(){………..}
Bà i 17 Kế thừa và đa hình(3) 17.1 Lớp trừu tượng
17.1.1. Xây dựng lớp cơ sở trừu tượng
Ta có một lớp Hinh và các lớp HinhChuNhat, HinhTron, HinhVuong kế thừa từ lớp
Hinh, lớp hình có các phương thức tính chu vi, tính diện tích, Vẽ. Nhưng với mỗi một loại hình thì có cách tính diện tích, chu vi, vẽ là khác nhau. Nghĩa là với các phương thức trên thì mỗi một lớp kế thừa từ lớp Hinh đòi hỏi phải thực thi một phương thức cho riêng mình. Tuy nhiên nếu như chúng ta khai báo các phương thức trên trong lớp Hinh là phương thức ảo thì điều này không thực sự đòi hỏi các lớp kế thừa phải ghi đè các phương thức trên một cách bắt buộc. Để yêu cầu các lớp dẫn xuất bắt buộc phải thực thi một phương thức của lớp cơ sở thì phương thức đó chúng ta phải khai báo là phương thức trừu tượng.
Một phương thức trừu tượng không có sự thực thi. Phương thức này chỉ đơn giản tạo ra một tên phương thức và ký hiệu của phương thức, phương thức này sẽ được thực thi ở các lớp dẫn xuất. Một lớp mà chứa ít nhất một phương thức trừu tượng thì lớp đó phải là lớp trừu tượng.
Những lớp trừu tượng được thiết lập như là cơ sở cho những lớp dẫn xuất, nhưng việc tạo các thể hiện hay các đối tượng cho các lớp trừu tượng được xem là không hợp lệ. Một khi chúng ta khai báo một phương thức là trừu tượng, thì chúng ta phải ngăn cấm bất cứ việc tạo thể hiện cho lớp này.
Do vậy, nếu chúng ta thiết kế phương thức C h u V i () như là trừu tượng trong lớp Hinh, chúng ta có thể dẫn xuất từ lớp này, nhưng ta không thể tạo bất cứ đối tượng cho lớp này. Khi đó mỗi lớp dẫn xuất phải thực thi phương thức ChuVi(). Nếu lớp dẫn xuất không thực thi phương thức trừu tượng của lớp cơ sở thì lớp dẫn xuất đó cũng là lớp trừu tượng, và ta cũng không thể tạo các thể hiện của lớp này được.
Phương thức trừu tượng được thiết lập bằng cách thêm từ khóa abstract vào đầu của phần định nghĩa phương thức, cú pháp thực hiện như sau:
abstract public double ChuVi();
phẩy (;) sau phương thức. Như thế với phương thức ChuVi () được thiết kế là trừu tượng thì chỉ cần câu lệnh trên là đủ.
Nếu một hay nhiều phương thức được khai báo là trừu tượng, thì phần định nghĩa lớp phải được khai báo là abstract, với lớp Hinh ta có thể khai báo là lớp trừu tượng như sau:
abstract class Hinh
{
//……..
}
Ví dụ : Phương thức trừu tượng
class Diem {
private int x; private int y;
public Diem(int x, int y) { this.x = x; this.y = y; } public int X { set { x = value; } get { return x; } }
public int Y { get { return y; } set { y = value; } }
public void Draw() {
System.Console.WriteLine("Toa do x {0}, y {1}", x, y); }
public double KhoangCach(Diem d) {
return Math.Sqrt(Math.Pow((x - d.x), 2) + Math.Pow((y - d.y), 2)); }
public double KhoangCach(int x, int y) {
return Math.Sqrt(Math.Pow((x - this.x), 2) + Math.Pow((y - this.y), 2)); }
}
// Vì lớp có phương thức trừu tượng, nên lớp cũng phải khai báo là lớp trừu tượng abstract class Hinh
{
// Phương thức trừu tượng abstract public void Draw(); abstract public double DienTich(); abstract public double ChuVi(); public void thu()
{
} } }
17.1.2. Kế thừa từ lớp trừu tượng
Khi một lớp được dẫn xuất từ một lớp trừu tượng thì lớp đó được kế thừa tất cả các thành phần không phải là private của lớp cơ sở. Trong đó có cả các thành phần là trừu tượng và không trừu tượng, khi đó lớp dẫn xuất bắt buộc phải ghi đè tất cả các phương thức trừu tượng bằng từ khóa override. Nếu lớp dẫn xuất không ghi đè hết các phương thức trừu tượng thì lớp dẫn xuất sẽ là lớp trừu tượng.
Ví dụ : Xây dựng lớp kế thừa từ lớp trừu tượng. class HinhTron : Hinh
{
private Diem tam; private double bankinh;
public HinhTron(Diem tam, double bankinh) {
this.tam = tam;
this.bankinh = bankinh; }
public HinhTron(int x, int y, double bankinh) {
tam = new Diem(x, y); this.bankinh = bankinh; }
// Thực thi phương thức trừu tượng trong lớp cơ sở public override void Draw()
{
System.Console.Write("Duong tron co:\n Toa do tam"); tam.Draw();
System.Console.WriteLine("Ban kinh: {0}", bankinh); }
public override double ChuVi() {
return 2 * Math.PI * bankinh; }
// Thực thi phương thức trừu tượng trong lớp cơ sở
public override double DienTich() {
return Math.PI * bankinh * bankinh; }
}
class HinhChuNhat : Hinh {
protected double a, b;
public HinhChuNhat(double a, double b) {
if ((a <= 0) | (b <= 0)) {
throw new ArithmeticException("Canh cua hinh chu nhat phai lon hon 0"); } else { this.a = a; this.b = b; } }
public override void Draw() {
System.Console.WriteLine("Hinh chu nhat co hai canh \na= {0}\nb= {1}", a, b); }
{
return (a + b) * 2; }
public override double DienTich() {
return (a * b); }
}
class HinhVuong : HinhChuNhat {
public HinhVuong(double a) : base(a, a)
{ }
public override void Draw() {
System.Console.WriteLine("Hinh vuong co canh la {0}", a); base.Draw();
} }
Khi đó ta có thể sử dụng các lớp trên để khai báo như sau :
public static void Main() {
Hinh h ;
HinhTron ht = new HinhTron(5, 7, 16); HinhChuNhat hcn = new HinhChuNhat(5, 8); HinhVuong hv = new HinhVuong(8);
h = ht; System.Console.WriteLine(h.ChuVi()); h = hcn; System.Console.WriteLine(h.ChuVi()); h = hv; System.Console.WriteLine(h.ChuVi()); }
Khi đó đoạn chương trình trên hoàn toàn chạy được không gặp bất cứ một lỗi nào cả. Nhưng nếu ta thay câu lệnh Hinh h bằng câu lệnh Hinh h = new Hinh() ; thì trình biên dịch sẽ báo lỗi như sau :
Cannot create an instance of the abstract class or interface Hinh ……….
Quay trở lại tính đa hình mà chúng ta đã bàn trước đây, chúng ta muốn các loại đối tượng khác nhau khi thực hiện một hành động cùng một cách thức với nhau. Như trong chương trình Main trên thì biến đối tượng Hinh h được gán cho đối tượng HinhTron,
HinhChuNhat, HinhVuong, như vậy khi gọi các phương thức thì trình biên dịch sẽ kiểm tra xem đối tượng h là đối tượng nào để gọi các phương thức tương ứng.
Sau đây là một ví dụ mà tính đa hình được thể hiện rất rõ.
public static void Main() {
Hinh h;
HinhTron ht = new HinhTron(5, 7, 16); HinhChuNhat hcn = new HinhChuNhat(5, 8); HinhVuong hv = new HinhVuong(8);
ThaoTacHinh(ht); ThaoTacHinh(hv);
ThaoTacHinh(new HinhChuNhat(5, 8)); ThaoTacHinh(new HinhTron(5, 6, 7)); }
public static void ThaoTacHinh(Hinh h) {
System.Console.WriteLine(h.ToString()); h.Draw();
System.Console.WriteLine("Chu vi: {0}", h.ChuVi()); System.Console.WriteLine("Dien tich: " + h.DienTich()); }
Như vậy là thay vì phải xây dựng các phương thức khác nhau cho các kiểu đối tượng hình khác nhau, ta có thể xây dựng một phương thức mà đối số truyền vào là lớp cha của các lớp đó. Như vậy khi gọi thì ta có thể truyền vào các biến đối tượng mà kế thừa từ lớp cha đó. Như vậy chúng ta thấy rõ ràng rằng dù là đối tượng hình gì thì khi muốn thực
hiện các thao tác ta đều có một cách thức chung là gọi phương thức ThaoTacHinh và truyền vào đối tượng Hình cần thực hiện. Đó chính là tính đa hình.
17.1.3. Hạn chế của lớp trừu tượng
Mặc dù chúng ta đã thiết kế phương thức ChuVi() như một phương thức trừu tượng để hỗ trợ cho tất cả các lớp dẫn xuất được thực thi riêng, nhưng điều này có một số hạn chế. Nếu chúng ta dẫn xuất một lớp từ lớp HinhChuNhat như lớp HinhVuong, thì lớp này không được hỗ trợ để thực thi phương thức ChuVi( ) cho riêng nó.
Ghi chú: Khác với ngôn ngữ C++, trong C# phương thức Hinh.ChuVi( ) không thể cung cấp một sự thực thi, do đó chúng ta sẽ không thể lấy được lợi ích của phương thức ChuVi() bình thường dùng để chia xẻ bởi các lớp dẫn xuất.
Cuối cùng những lớp trừu tượng không có sự thực thi căn bản; chúng thể hiện ý tưởng về một sự trừu tượng, điều này thiết lập một sự giao ước cho tất cả các lớp dẫn xuất. Nói cách khác các lớp trừu tượng mô tả một phương thức chung của tất cả các lớp được thực thi một cách trừu tượng.
Ý tưởng của lớp trừu tượng Hinh thể hiện những thuộc tính chung cùng với những hành vi của tất cả các Hình, thậm chí ngay cả khi ta không có ý định tạo thể hiện của chính lớp trừu tượng Hinh.
Ý nghĩa của một lớp trừu tượng được bao hàm trong chính từ “trừu tượng”. Lớp này dùng để thực thi một “Hinh” trừu tượng, và nó sẽ được biểu lộ trong các thể hiện xác định của Hinh, như là HinhTron, HinhVuong, HinhChuNhat…
Các lớp trừu tượng không thể thực thi được, chỉ có những lớp xác thực tức là những lớp dẫn xuất từ lớp trừu tượng này mới có thể thực thi hay tạo thể hiện. Một sự thay đổi việc sử dụng trừu tượng là định nghĩa một giao diện (interface), phần này sẽ được trình bày trong Chương tiếp theo
Một số chú ý:
Không thể tạo đối tượng của lớp trừu tượng
Một lớp chứa một phương thức trừu tượng thì lớp đó phải là lớp trừu tượng
Lớp dẫn xuất không hiện thực tất cả các phương thức trừu tượng của lớp cơ sở thì lớp đó cũng phải là lớp trừu tượng
Để khai báo phương thức trừu tượng ta sử dụng từ khoá abstract
Phương thức trừu tượng không có phần thân
Phương thức trừu tượng không thể là private
Phương thức trừu tượng mặc định cũng là phương thức ảo
Phương thức tĩnh không thể là phương thức trừu tượng
Lớp dẫn xuất kề thừa phương thức trừu tượng thì phải ghi đè nó.
Phương thức trừu tượng có thể ghi đè phương thức của lớp cơ sở được khai báo là phương thức ảo
Phương thức trừu tượng có thể ghi đè phương thức của lớp cơ sở đã đã được ghi đè
17.2. Lớp cô lập (sealed class)
Ngược với các lớp trừu tượng là các lớp cô lập. Một lớp trừu tượng được thiết kế cho các lớp dẫn xuất và cung cấp các khuôn mẫu cho các lớp con theo sau. Trong khi một lớp cô lập thì không cho phép các lớp dẫn xuất từ nó. Để khai báo một lớp cô lập ta dùng từ khóa sealed đặt trước khai báo của lớp không cho phép dẫn xuất. Hầu hết các lớp thường được đánh dấu sealed nhằm ngăn chặn các tai nạn do sự kế
thừa gây ra.
Nếu khai báo của lớp Hinh trong ví dụ 3.2 được thay đổi từ khóa abstract bằng từ khóa sealed (cũng có thể loại bỏ từ khóa trong các khai báo của phương thức ChuVi(), DienTich(),…). Chương trình sẽ bị lỗi khi biên dịch. Nếu chúng ta cố thử biên dịch chương trình thì sẽ nhận được lỗi từ trình biên dịch:
‘HinhTron’ cannot inherit from sealed class ‘Hinh’
Đây chỉ là một lỗi trong số những lỗi như ta không thể tạo một phương thức thành viên
protected trong một lớp khai báo là sealed.
17.3. Lớp Object
Tất cả các lớp của ngôn ngữ C# của bất cứ kiểu dữ liệu nào thì cũng được dẫn xuất từ lớp System.Object. Thú vị là bao gồm cả các kiểu dữ liệu giá trị.
Một lớp cơ sở là cha trực tiếp của một lớp dẫn xuất. Lớp dẫn xuất này cũng có thể làm cơ sở cho các lớp dẫn xuất xa hơn nữa, việc dẫn xuất này sẽ tạo ra một cây thừa kế hay một kiến trúc phân cấp. Lớp gốc là lớp nằm ở trên cùng cây phân cấp thừa kế, còn các lớp dẫn xuất thì nằm bên dưới. Trong ngôn ngữ C#, lớp gốc là lớp Object, lớp này nằm trên cùng trong cây phân cấp các lớp.
Lớp Object cung cấp một số các phương thức dùng cho các lớp dẫn xuất có thể thực hiện việc phủ quyết. Những phương thức này bao gồm Equals() kiểm tra xem hai đối tượng có giống nhau hay không. Phương thức GetType() trả về kiểu của đối tượng. Và phương thức ToString() trả về một chuỗi thể hiện lớp hiện hành. Sau đây là bảng tóm tắt các phương thức của lớp Object.
Phương thức Chức năng
Equal( ) So sánh bằng nhau giữa hai đối tượng
GetHashCode( ) Cho phép những đối tượng cung cấp riêng những hàm băm cho sử dụng tập hợp.
GetType( ) Cung cấp kiểu của đối tượng
Finalize( ) Dọn dẹp các tài nguyên
MemberwiseClone( ) Tạo một bản sao từ đối tượng.
Bảng 3.1: Tóm tắt các phương thức của lớp Object.
Ví dụ: Sau minh họa việc sử dụng phương thức ToString( ) thừa kế từ lớp Object. class HinhChuNhat
{
protected double a, b;
public HinhChuNhat(double a, double b) {
if ((a <= 0) | (b <= 0)) {
throw new ArithmeticException("Canh cua hinh chu nhat phai lon hon 0"); } else { this.a = a; this.b = b; } }
public override string ToString() {
return "Hinh chu nhat"; }
}
Trong tài liệu của lớp Object phương thức ToString() được khai báo như sau:
public virtual string ToString();
Đây là phương thức ảo public, phương thức này trả về một chuỗi và không nhận tham số. Tất cả kiểu dữ liệu được xây dựng sẵn, như kiểu int, dẫn xuất từ lớp Object nên nó cũng có thể thực thi các phương thức của lớp Object.
ToString(), do đó phương thức này sẽ trả về giá trị có nghĩa. Nếu chúng ta không phủ quyết phương thức ToString() trong lớp HinhChuNhat, phương thức của lớp cơ sở sẽ được thực thi
Như chúng ta thấy, hành vi mặc định đã trả về một chuỗi chính là tên của lớp đang thể