C Sharp căn bản – Chương V : KẾ THỪA – ĐA HÌNH

0
(0)

Trong chương trước đã trình bày cách tạo ra những kiểu dữ liệu mới bằng việc xây dựng các lớp đối tượng. Tiếp theo chương này sẽ đưa chúng ta đi sâu vào mối quan hệ giữa những  đối  tượng  trong  thế  giới  thực  và  cách  mô  hình  hóa  những  quan  hệ  trong  xây  dựng chương trình. Chương 5 cũng giới thiệu khái niệm đặc biệt hóa (specialization) được cài đặt trong ngôn ngữ C# thông qua sự kế thừa (inheritance).

 

Khái niệm đa hình (polymorphism) cũng được trình bày trong chương 5, đây là khái niệm quan trọng trong lập trình hướng đối tượng. Khái niệm này cho phép các thể hiện của lớp có liên hệ với nhau có thể được xử lý theo một cách tổng quát.

Cuối cùng là phần trình bày về các lớp cô lập (sealed class) không được đặt biệt hóa, hay các lớp trừu tượng sử dụng trong đặc biệt hóa. Lớp đối tượng Object là gốc của tất cả các lớp cũng được thảo luận ở phần cuối chương.

 

Đặc biệt hóa và tổng quát hóa

 

Lớp và các thể hiện của lớp tức đối tượng tuy không tồn tại trong cùng một khối, nhưng chúng tồn tại trong một mạng lưới sự phụ thuộc và quan hệ lẫn nhau. Ví dụ như con người và xã hội động vật cùng sống trong một thế giới có quan hệ loài với nhau.

Quan hệ là một (is-a) là một sự đặc biệt hóa. Khi chúng ta nói rằng mèo là một loại động

vật có vú, có nghĩa là chúng ta đã nói rằng mèo là một trường hợp đặc biệt của loại động vật có vú. Nó có tất cả các đặc tính của bất cứ động vật có vú nào (như sinh ra con, có sữa mẹ và có lông…). Tuy nhiên, mèo có thêm các đặc tính riêng được xác định trong họ nhà mèo mà các họ động vật có vú khác không có được. Chó cũng là loại động vật có vú, chó cũng có tất cả các thuộc tính của động vật có vú, và riêng nó còn có thêm các thuộc tính riêng xác định họ loài chó mà khác với các thuộc tính đặc biệt của loài khác ví dụ như mèo chẳng hạn.

Quan hệ đặc biệt hóa và tổng quát hóa là hai mối quan hệ đối ngẫu và phân cấp với nhau. Chúng có quan hệ đối ngẫu vì đặc biệt được xem như là mặt ngược lại của tổng quát. Do đó, loài  chó  và  mèo  là  trường  hợp  đặc  biệt  của  động  vật  có  vú.  Ngược  lại  động  vật  có  vú  là trường hợp tổng quát từ các loài chó và mèo.

Mối quan hệ là phân cấp bởi vì chúng ta tạo ra một cây quan hệ, trong đó các trường hợp đặc biệt là những nhánh của trường hợp tổng quát. Trong cây phân cấp này nếu di chuyển lên trên  cùng  ta  sẽ  được  trường  hợp  tổng  quát  hóa,  và  ngược  lại  nếu  di  chuyển  xuống  ngược nhánh thì ta được trường hợp đặc biệt hóa. Ta có sơ đồ phân cấp minh họa cho loài chó, mèo và động vật có vú như trên:

 

 

 

ĐỘNG

VẬT CÓ VÚ

 

 

 

 

 

 

 

 

 

MÈO                                     CHÓ

 

 

 

 

 

 

Tương tự, khi chúng ta nói rằng ListBox và Button là những Window, ta phải chỉ ra những đặc tính và hành vi của những Window có trong cả hai lớp trên. Hay nói cách khác, Window là tổng quát hóa chia xẻ những thuộc tính của hai lớp ListBox và Button, trong khi đó mỗi trường hợp đặc biệt ListBox và Button sẽ có riêng những thuộc tính và hành vi đặc thù khác.

 

 

Ngôn ngữ mô hình hóa thống nhất (UML)

UML ( Unified Modeling Language) là ngôn ngữ chuẩn hóa để mô tả cho một hệ thống hoặc thương mại. Trong chương này sử dụng một số phần của mô hình UML để trình bày các biểu đồ quan hệ giữa các lớp.

Trong UML, những lớp được thể hiện như các khối hộp, tên của lớp được đặt trên cùng của khối hộp, và các phương thức hay các biến thành viên được đặt bên trong hộp.

Như trong hình 5.1, mô hình quan hệ tổng quát hóa và đặc biệt hóa được trình bày qua UML, ghi chú rằng mũi tên đi từ các lớp đặc biệt hóa đến lớp tổng quát hóa.

 

 

 

                                  Window

 

 

 

 

 

 

 

 

 

 

                                Button                                              List Box

 

 

 

Hình 5.2: Quan hệ giữa thành phần cửa sổ

Thông thường lưu ý rằng khi hai lớp chia xẻ chức năng với nhau, thì chúng được trích ra các phần chung và đưa vào lớp cơ sở chia xẻ. Điều này hết sức có lợi, vì nó cung cấp khả năng cao để sử dụng lại các mã nguồn chung và dễ dàng duy trì mã nguồn.

 

 

 

 

 

 

 

Window

 

 

 

 

Radio

Button

 

 

 

 

 

Hình 5.3 Dẫn xuất từ Window

Giả sử chúng ta bắt đầu tạo một loạt các lớp đối tượng theo hình vẽ 5.3 như bên trên. Sau

khi  làm  việc  với RadioButton,  CheckBox,  và  CommandButton một  thời  gian  ta  nhận  thấy chúng chia xẻ nhiều thuộc tính và hành vi đặc biệt hơn Window nhưng lại khá tổng quát cho cả ba lớp này. Như vậy ta có thể chia các thuộc tính và hành vi thành một nhóm lớp cơ sở riêng lấy tên là Button. Sau đó ta sắp xếp lại cấu trúc kế thừa như hình vẽ 5.4. Đây là ví dụ về cách tổng quát hóa được sử dụng để phát triển hướng đối tượng.

 

 

                    Window

 

 

 

 

 

 

 

 

                        Button             List Box

 

 

 

 

 

 

 

 

 

                                                                                                Check Box         Command

 

   

 

 

 

 

 

 

                       Radio

                        Button

 

 

Hình 5.4:  Cây quan hệ lớp cửa sổ

Trong mô hình UML trên được vẽ lại quan hệ giữa các lớp. Trong đó cả hai lớp Button và ListBox điều dẫn xuất từ lớp Window, trong đó Button có trường hợp đặc biệt là CheckBox và Command. Cuối cùng thì RadioButton được dẫn xuất từ CheckBox. Chúng ta cũng có thể nói rằng  RadioButton  là  một  CheckBox,  và  tiếp  tục  CheckBox  là  một  Button,  và  cuối  cùng Button là Window.

 

 

Sự  thiết kế trên không phải là duy nhất hay cách tốt nhất  để tổ  chức những đối tượng, nhưng đó là khởi điểm để hiểu về cách quan hệ giữa đối tượng với các đối tượng khác.

Sự kế thừa

 

Trong ngôn ngữ C#, quan hệ đặc biệt hóa được thực thi bằng cách sử dụng sự kế thừa. Đây không phải là cách duy nhất để thực thi đặc biệt hóa, nhưng nó là cách chung nhất và tự nhiên nhất để thực thi quan hệ này.

Trong  mô  hình  trước,  ta  có  thể  nói  ListBox  kế  thừa  hay  được  dẫn  xuất  từ  Window. Window được xem  như  là  lớp  cơ  sở,  và ListBox được  xem  như là lớp  dẫn xuất.  Như vậy, ListBox dẫn xuất tất cả các thuộc tính và hành vi từ lớp Window và thêm những phần đặc biệt riêng để xác nhận ListBox.

 

Thực thi kế thừa

 

Trong ngôn ngữ C# để tạo một lớp dẫn xuất từ một lớp ta thêm dấu hai chấm vào sau tên lớp dẫn xuất và trước tên lớp cơ sở:

public class ListBox : Window

Đoạn  lệnh  trên  khai  báo  một  lớp  mới  tên  là ListBox,  lớp  này  được  dẫn  xuất  từ Window. Chúng ta có thể đọc dấu hai chấm có thể được đọc như là “dẫn xuất từ”.

Lớp dẫn xuất sẽ kế thừa tất cả các thành viên của lớp cơ sở, bao gồm tất cả các phương thức

và biến thành viên của lớp cơ sở. Lớp dẫn xuất được tự do thực thi các phiên bản của một phương thức của lớp cơ sở. Lớp dẫn xuất cũng có thể tạo một phương thức mới bằng việc đánh dấu với từ khóa new. Ví dụ 5.1 sau minh họa việc tạo và sử dụng các lớp cơ sở và dẫn xuất.

Ví dụ 5.1: Sử dụng lớp dẫn xuất.

—————————————————————————–

 

using System;

 

public class Window

 

{

 

// Hàm khởi dựng lấy hai số nguyên chỉ

 

// đến vị trí của cửa sổ trên console public Window( int top, int left)

{

 

this.top = top;

 

this.left = left;

 

}

 

// mô phỏng vẽ cửa sổ public void DrawWindow()

{

 

 

Console.WriteLine(“Drawing Window at {0}, {1}”, top, left);

 

}

 

// Có hai biến thành viên private do đó

 

// hai biến này sẽ không thấy bên trong lớp

 

// dẫn xuất. private int top; private int left;

}

 

// ListBox dẫn xuất từ Window public class ListBox: Window

{

 

// Khởi dựng có tham số

 

public ListBox(int top, int left,

 

string theContents) : base(top, left) // gọi khởi dựng của lớp cơ sở

 

{

 

mListBoxContents = theContents;

 

}

 

// Tạo một phiên bản mới cho phương thức DrawWindow

 

// vì trong lớp dẫn xuất muốn thay đổi hành vi thực hiện

 

// bên trong phương thức này public new void DrawWindow()

{

 

base.DrawWindow();

 

Console.WriteLine(“ ListBox write: {0}”, mListBoxContents);

 

}

 

// biến thành viên private

 

private string mListBoxContents;

 

}

 

public class Tester

 

{

 

public static void Main()

 

{

 

// tạo đối tượng cho lớp cơ sở Window w = new Window(5, 10); w.DrawWindow();

// tạo đối tượng cho lớp dẫn xuất

 

ListBox lb = new ListBox( 20, 10, “Hello world!”);

 

lb.DrawWindow();

 

}

 

 

 

 

 

 

 

 

}

—————————————————————————–

Kết quả:

 

Drawing Window at: 5, 10

 

Drawing Window at: 20, 10

ListBox  write: Hello world!

—————————————————————————–

Ví dụ 5.1 bắt đầu với việc khai báo một lớp cơ sở tên Window. Lớp này thực thi một phương thức  khởi  dựng  và  một  phương  thức  đơn  giản DrawWindow.  Lớp  có  hai  biến  thành  viên private là top và left, hai biến này do khai báo là private nên chỉ sử dụng bên trong của lớp Window, các lớp dẫn xuất sẽ không truy cập được. ta sẽ bàn tiếp về ví dụ này trong phần tiếp theo.

 

Gọi phương thức khởi dựng của lớp cơ sở

 

Trong  ví  dụ  5.1,  một  lớp  mới  tên  là ListBox được  dẫn  xuất  từ  lớp  cơ  sở Window,  lớp ListBox có một phương thức khởi dựng lấy ba tham số. Trong phương thức khởi dựng của lớp dẫn xuất này có gọi phương thức khởi dựng của lớp cơ sở. Cách gọi được thực hiện bằng việc đặt dấu  hai chấm ngay sau phần khai báo danh  sách tham số  và tham chiếu đến lớp  cơ sở thông qua từ khóa base:

public ListBox( int theTop, int theLeft,

string theContents):

 

base( theTop, theLeft) // gọi khởi tạo lớp cơ sở

Bởi vì các lớp không được kế thừa các phương thức khởi dựng của lớp cơ sở, do đó lớp dẫn xuất phải thực thi phương thức khởi dựng riêng của nó. Và chỉ có thể sử dụng phương thức khởi dựng của lớp cơ sở thông qua việc gọi tường minh.

Một điều lưu ý trong ví dụ 5.1 là việc lớp ListBox thực thi một phiên bản mới của phương thức DrawWindow():

public new void DrawWindow()

Từ khóa new được sử dụng ở đây để chỉ ra rằng người lập trình đang tạo ra một phiên bản mới cho phương thức này bên trong lớp dẫn xuất.

Nếu lớp cơ sở có phương thức khởi dựng mặc định, thì lớp dẫn xuất không cần bắt buộc phải

gọi phương thức khởi dựng của lớp cơ sở một cách tường minh. Thay vào đó phương thức khởi dựng mặc định của lớp cơ sở sẽ được gọi một cách ngầm định. Tuy nhiên, nếu lớp cơ sở không  có  phương  thức  khởi  dựng  mặc  định,  thì  tất  cả  các  lớp  dẫn  xuất  của  nó  phải  gọi phương thức khởi dựng của lớp cơ sở một cách tường minh thông qua việc sử dụng từ khóa base.

 

 

Ghi chú: Cũng như thảo luận trong chương 4, nếu chúng ta không khai báo bất cứ phương thức khởi dựng nào, thì trình biên dịch sẽ tạo riêng một phương thức khởi dựng cho chúng

ta. Khi mà chúng ta viết riêng các phương thức khởi dựng hay là sử dụng phương thức khởi dựng mặc định do trình biên dịch cung cấp hay không thì phương thức khởi dựng mặc định không lấy một tham số nào hết. Tuy nhiên, lưu ý rằng khi ta tạo bất cứ phương thức khởi dựng nào thì trình biên dịch sẽ không cung cấp phương thức khởi dựng cho chúng ta.

 

Gọi phương thức của lớp cơ sở

 

Trong  ví  dụ  5.1,  phương  thức  DrawWindow()  của  lớp  ListBox  sẽ  làm  ẩn  và  thay  thế phương   thức   DrawWindow  của   lớp   cơ   sở   Window.   Khi   chúng   ta   gọi   phương   thức DrawWindow của một đối tượng của lớp ListBox thì phương thức ListBox.DrawWindow() sẽ được thực hiện, không phải phương thức Window.DrawWindow() của lớp cơ sở Window. Tuy nhiên, ta có thể gọi phương thức DrawWindow() của lớp cơ sở thông qua từ khóa base:

base.DrawWindow();  // gọi phương thức cơ sở

Từ khóa base chỉ đến lớp cơ sở cho đối tượng hiện hành.

 

Điều khiển truy xuất

 

Khả năng hiện hữu của một lớp và các thành viên của nó có thể được hạn chế thông qua việc  sử  dụng  các  bổ  sung  truy cập: public,  private,  protected,  internal,  và  protected internal.

Như  chúng  ta  đã  thấy, public cho  phép  một  thành  viên  có  thể  được  truy cập  bởi  một phương thức thành viên của những lớp khác. Trong khi đó private chỉ cho phép các phương thức thành viên trong lớp đó truy xuất. Từ khóa protected thì mở rộng thêm khả năng của private cho phép truy xuất từ các lớp dẫn xuất của lớp đó. Internal mở rộng khả năng cho phép bất cứ phương thức của lớp nào trong cùng một khối kết hợp (assembly) có thể truy xuất được. Một khối kết hợp được hiểu như là một khối chia xẻ và dùng lại trong CLR. Thông thường, khối này là tập hợp các tập tin vật lý được lưu trữ trong một thư mục bao gồm các tập

tin tài nguyên, chương trình thực thi theo ngôn ngữ IL,…

Từ khóa internal protected đi cùng với  nhau cho phép các thành viên của cùng một khối assembly hoặc các lớp dẫn xuất của nó có thể truy cập. Chúng ta có thể xem sự thiết kế này giống như là internal hay protected.

Các lớp cũng như những thành viên của lớp có thể được thiết kế với bất cứ mức độ truy xuất nào.  Một  lớp  thường  có  mức  độ  truy xuất  mở  rộng  hơn  cách  thành  viên  của  lớp,  còn  các thành viên thì mức độ truy xuất thường có nhiều hạn chế. Do đó, ta có thể định nghĩa một lớp MyClass như sau:

public class MyClass

 

{

 

 

//…

 

protected int myValue;

}

Như trên biến thành viên myValue được khai báo truy xuất protected mặc dù bản thân lớp được khai báo là public. Một lớp public là một lớp sẵn sàng cho bất cứ lớp nào khác muốn tương tác với nó. Đôi khi một lớp được tạo ra chỉ để trợ giúp cho những lớp khác trong một khối assemply, khi đó những lớp này nên được khai báo khóa internal hơn là khóa public.

Đa hình

 

Có hai cách thức khá mạnh để thực hiện việc kế thừa. Một là sử dụng lại mã nguồn, khi chúng ta tạo ra lớp ListBox, chúng ta có thể sử dụng lại một vài các thành phần trong lớp cơ sở như Window.

Tuy nhiên, cách sử dụng thứ hai chứng tỏ được sức mạnh to lớn của việc kế thừa đó là

tính đa hình (polymorphism). Theo tiếng Anh từ này được kết hợp từ poly là nhiều và morph

có nghĩa là form (hình thức). Do vậy, đa hình được hiểu như là khả năng sử dụng nhiều hình thức của một kiểu mà không cần phải quan tâm đến từng chi tiết.

Khi một tổng đài điện thoại gởi cho máy điện thoại của chúng ta một tín hiệu có cuộc gọi. Tổng đài không quan tâm đến điện thoại của ta là loại nào. Có thể ta đang dùng một điện thoại  cũ  dùng  motor  để  rung  chuông,  hay  là  một  điện  thoại  điện  tử  phát  ra  tiếng  nhạc  số. Hoàn toàn các thông tin về điện thoại của ta không có ý nghĩa gì với tổng đài, tổng đài chỉ biết một kiểu cơ bản là điện thoại mà thôi và diện thoại này sẽ biết cách báo chuông. Còn việc báo chuông như thế nào thì tổng đài không quan tâm. Tóm lại, tổng đài chỉ cần bảo điện thoại hãy làm điều gì đó để reng. Còn phần còn lại tức là cách thức reng là tùy thuộc vào từng loại điện thoại. Đây chính là tính đa hình.

 

Kiểu đa hình

 

Do  một  ListBox là  một  Window và  một  Button cũng  là  một  Window,  chúng  ta  mong muốn sử dụng cả hai kiểu dữ liệu này trong tình huống cả hai được gọi là Window. Ví dụ như trong một form giao diện trên MS Windows, form này chứa một tập các thể hiện của Window. Khi form được hiển thị, nó yêu cầu tất cả các thể hiện của Window tự thực hiện việc tô vẽ. Trong trường hợp này, form không muốn biết thành phần thể hiện là loại nào như Button, CheckBox,…,. Điều quan trọng là form kích hoạt toàn bộ tập hợp này tự thực hiện việc vẽ. Hay nói ngắn gọn là form muốn đối xử với những đối tượng Window này một cách đa hình.

 

Phương thức đa hình

 

Để tạo một phương thức hỗ tính đa hình, chúng ta cần phải khai báo khóa virtual trong phương  thức  của  lớp  cơ  sở.  Ví  dụ,  để  chỉ  định  rằng  phương  thức DrawWindow()  của  lớp Window trong ví dụ 5.1 là đa hình, đơn giản là ta thêm từ khóa virtual vào khai báo như sau:

public virtual void DrawWindow()

Lúc này thì các lớp dẫn xuất được tự do thực thi các cách xử riêng của mình trong phiên bản  mới  của  phương  thức  DrawWindow().  Để  làm  được  điều  này  chỉ  cần  thêm  từ  khóa

override để chồng lên phương thức ảo DrawWindow() của lớp cơ sở. Sau đó thêm các đoạn mã nguồn mới vào phương thức viết chồng này.

Trong ví dụ minh họa 5.2 sau, lớp ListBox dẫn xụất từ lớp Window và thực thi một phiên bản riêng của phương thức DrawWindow():

public override void DrawWindow()

 

{

 

base.DrawWindow();

 

Console.WriteLine(“Writing string to the listbox: {0}”, listBoxContents);

 

}

 

Từ khóa override bảo với trình biên dịch rằng lớp này thực hiện việc phủ quyết lại phương thức  DrawWindow()  của  lớp  cơ  sở.  Tương  tự  như  vậy  ta  có  thể  thực  hiện  việc  phủ  quyết phương thức này trong một lớp dẫn xuất  khác như Button, lớp này cũng được dẫn xuất từ Window.

Trong  phần  thân  của  ví  dụ  5.2,  đầu  tiên  ta  tạo  ra  ba  đối  tượng,  đối  tượng  thứ  nhất  của Window, đối tượng thứ hai của lớp ListBox và đối tượng cuối cùng của lớp Button. Sau đó ta thực hiện việc gọi phương thức DrawWindow() cho mỗi đối tượng sau:

Window  win = new Window( 1, 2 );

 

ListBox  lb = new ListBox( 3, 4, “Stand alone list box”); Button  b = new Button( 5, 6 );

win.DrawWindow(); lb.DrawWindow(); b.DrawWindow();

Đoạn chương trình trên thực hiện các công việc như yêu cầu của chúng ta, là từng đối tượng

thực hiện công việc tô vẽ của nó. Tuy nhiên, cho đến lúc này thì chưa có bất cứ sự đa hình nào được thực thi. Mọi chuyện vẫn bình thường cho đến khi ta muốn tạo ra một mảng các đối tượng Window,  bởi vì ListBox cũng là một Window nên ta có thể tự do đặt một đối tượng ListBox vào vị trí của một đối tượng Window trong mảng trên. Và tương tự ta cũng có thể đặt một đối tượng Button vào bất cứ vị trí nào trong mảng các đối tượng Window, vì một Button cũng là một Window.

Window[]  winArray = new Window[3];

 

winArray[0] = new Window( 1, 2 );

 

winArray[1] = new ListBox( 3, 4, “List box is array”);

 

winArray[2] = new Button( 5, 6 );

Chuyện  gì  xảy  ra  khi  chúng  ta  gọi  phương  thức DrawWindow()  cho  từng  đối  tượng  trong mảng winArray.

for( int i = 0; i < 3 ; i++)

 

{

 

 

winArray[i].DrawWindow();

 

 

 

 

 

 

 

}

Trình biên dịch điều biết rằng có ba đối tượng Windows trong mảng và phải thực hiện việc

gọi  phương  thức  DrawWindow()  cho  các  đối  tượng  này.  Nếu  chúng  ta  không  đánh  dấu phương  thức DrawWindow()  trong  lớp Window là  virtual thì  phương  thức DrawWindow() trong lớp Window sẽ được gọi ba lần. Tuy nhiên do chúng ta đã đánh dấu phương thức này ảo ở lớp cơ sở và thực thi việc phủ quyết phương thức này ỏ các lớp dẫn xuất.

Khi ta gọi phương thức DrawWindow trong mảng, trình biên dịch sẽ dò ra được chính xác kiểu dữ liệu nào được thực thi trong mảng khi đó có ba kiểu sẽ được thực thi là một Window, một ListBox, và một Button. Và trình biên dịch sẽ gọi chính xác phương thức của từng đối tượng. Đây là điều cốt lõi và tinh hoa của tính chất đa hình. Đoạn chương trình hoàn chỉnh

5.2 minh họa cho sự thực thi tính chất đa hình.

Ví dụ 5.2: Sử dụng phương thức ảo.

—————————————————————————–

 

using System;

 

public class Window

 

{

 

public Window( int top, int left )

 

{

 

this.top = top;

 

this.left = left;

 

}

 

// phương thức được khai báo ảo public virtual void DrawWindow()

{

 

Console.WriteLine( “Window: drawing window at {0}, {1}”, top, left );

 

}

 

// biến thành viên của lớp protected int top; protected int left;

}

 

public class ListBox : Window

 

{

 

// phương thức khởi dựng có tham số

 

public ListBox(  int top, int left,      string contents ): base( top, left)

 

{

 

listBoxContents = contents;

 

}

 

 

// thực hiện việc phủ quyết phương thức DrawWindow

 

 

 

 

 

 

 

public override void DrawWindow()

 

{

 

base.DrawWindow();

 

Console.WriteLine(“ Writing string to the listbox: {0}”, listBoxContents);

 

}

 

// biến thành viên của ListBox private string listBoxContents;

}

 

public class Button : Window

 

{

 

public Button( int top, int left) : base( top, left )

 

{

 

}

 

// phủ quyết phương thức DrawWindow của lớp cơ sở public override void DrawWindow()

{

 

Console.WriteLine(“ Drawing a button at {0}: {1}”, top, left);

 

}

 

}

 

public class Tester

 

{

 

static void Main()

 

{

 

Window win = new Window(1,2);

 

ListBox lb = new ListBox( 3, 4, “ Stand alone list box”); Button b = new Button( 5, 6 );

win.DrawWindow(); lb.DrawWindow(); b.DrawWindow();

Window[]  winArray = new Window[3];

 

winArray[0] = new Window( 1, 2 );

 

winArray[1] = new ListBox( 3, 4, “List box is array”);

 

winArray[2] = new Button( 5, 6 );

 

for( int i = 0; i < 3; i++)

 

{

 

winArray[i].DrawWindow();

 

}

 

}

 

 

 

 

 

 

 

 

}

—————————————————————————–

Kết quả:

 

Window: drawing window at 1: 2

 

Window: drawing window at 3: 4

Writing string to the listbox: Stand alone list box

 

Drawing a button at 5: 6

 

Window: drawing Window at 1: 2

 

Window: drawing window at 3: 4

Writing string to the listbox: List box is array

 

Drawing a button at 5: 6

—————————————————————————–

Lưu ý trong suốt ví dụ này, chúng ta đánh dấu một phương thức phủ quyết mới với từ khóa phủ quyết override:

public override void DrawWindow()

Lúc này trình biên dịch biết cách sử dụng phương thức phủ quyết khi gặp đối tượng mang hình thức đa hình. Trình biên dịch chịu trách nhiệm trong việc phân ra kiểu dữ liệu thật của đối tượng để sau này xử lý. Do đó phương thức ListBox.DrawWindow() sẽ được gọi khi một đối tượng Window tham chiếu đến một đối tượng thật sự là ListBox.

Ghi chú: Chúng ta phải chỉ định rõ ràng với từ khóa override khi khai báo một phương thức phủ quyết phương thức ảo của lớp cơ sở. Điều này dễ lầm lẫn với người lập trình C++

vì từ khóa này trong C++ có thể bỏ qua mà trình biên dịch C++ vẫn hiểu.

 

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ở Window 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 ListBox và RadioButton đươ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ở Window mua được của công ty A làm lớp cơ sở cho hai 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 Window 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 Sort( ) vào lớp ListBox:

 

public class ListBox : Window

 

{

 

public virtual void Sort( ) {….}

 

 

 

 

 

 

}

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ở Window, đưa ra phiên bản thứ hai của lớp Window. 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 Sort( ) vào lớp cơ sở Window:

public class Window

 

{

 

//……..

 

public virtual void Sort( ) {….}

 

}

 

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 Sort() trong lớp Window 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 ListBox. Trình biên dịch có thể gọi phương thức Sort( ) trong lớp ListBox khi  chúng ta có  ý định  gọi  phương  thức  Sort(  )  trong Window.  Trong ngôn  ngữ Java, nếu phương thức Sort( ) trong Window có kiểu trả về khác kiểu trả về của phương thức Sort( ) trong lớp ListBox 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 Sort( ) được trình bày trong lớp Window, thì khi thực hiện hành vi của lớp Listbox 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:

 

…\class1.cs(54, 24): warning CS0114: ‘ListBox.Sort( )’ hides inherited member ‘Window.Sort()’.

To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

Để 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  ListBox.Sort(  )  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 Window:

public class ListBox : Window

 

{

 

public new virtual  Sort( ) {….}

 

}

 

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 Window, 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 ListBox : Window

 

{

 

 

public override void Sort( ) {…}

 

 

}

 

Lớp trừu tượng

 

Mỗi  lớp  con  của  lớp Window nên  thực  thi  một  phương  thức DrawWindow()  cho  riêng mình. Tuy nhiên điều này không thực sự đòi hỏi phải thực hiện một cách bắt buộc. Để yêu cầu các lớp con (lớp dẫn xuất) phải thực thi một phương thức của lớp cơ sở, chúng ta phải thiết kế một phương thức một cách 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.

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 DrawWindow()  như  là  trừu  tượng  trong  lớp Window, 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 DrawWindow(). 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 void DrawWindow( );

Do phương thức không cần phần thực thi, nên không có dấu ({}) mà chỉ có dấu chấm phẩy (;)

sau phương thức. Như thế với phương thức DrawWindow() đượ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 Window ta có thể khai báo là lớp trừu tượng như sau:

abstract public void Window

Ví  dụ  5.3  sau  minh  họa  việc  tạo  lớp  Window  trừu  tượng  và  phương  thức  trừu  tượng

DrawWindow() của lớp Window.

Ví dụ 5.3: Sử dụng phương thức và lớp trừu tượng.

—————————————————————————–

 

using System;

 

abstract public class Window

 

{

 

// hàm khởi dựng lấy hai tham số public Window( int top, int left)

{

 

 

 

 

 

 

this.top = top;

 

this.left = left;

 

}

 

// phương thức trừu tượng minh họa việc

 

// vẽ ra cửa sổ

 

abstract public void DrawWindow();

 

// biến thành viên protected protected int top;

protected int left;

 

}

 

// lớp ListBox dẫn xuất từ lớp Window public class ListBox : Window

{

 

// hàm khởi dựng lấy ba tham số

 

public ListBox( int top, int left, string contents) : base( top, left)

 

{

 

listBoxContents = contents;

 

}

 

// phủ quyết phương thức trừu tượng DrawWindow()

 

public override void DrawWindow( )

 

{

 

Console.WriteLine(“Writing string to the listbox: {0}”, listBoxContents);

 

}

 

// biến private của lớp

 

private string listBoxContents;

 

}

 

// lớp Button dẫn xuất từ lớp Window public class Button : Window

{

 

// hàm khởi tạo nhận hai tham số

 

public Button( int top, int left) : base( top, left)

 

{

 

}

 

// thực thi phương thức trừu tượng public override void DrawWindow()

{

 

Console.WriteLine(“Drawing button at {0}, {1}\n”, top, left);

 

}

 

 

 

 

 

 

 

 

}

 

public class Tester

 

{

 

static void Main()

 

{

 

Window[] winArray = new Window[3];

 

winArray[0] = new ListBox( 1, 2, “First List Box”); winArray[1] = new ListBox( 3, 4, “Second List Box”); winArray[2] = new Button( 5, 6);

for( int i=0; i <3 ; i++)

 

{

 

winArray[i].DrawWindow( );

 

}

 

}

 

}

 

—————————————————————————–

Trong ví dụ 5.3, lớp Window được khai báo là lớp trừu tượng và do vậy nên chúng ta không

thể tạo bất cứ thể hiện nào của lớp Window. Nếu chúng ta thay thế thành viên đầu tiên của mảng:

winArray[0] = new ListBox( 1, 2, “First List Box”);

bằng câu lệnh sau:

 

winArray[0] = new Window( 1, 2);

Thì trình biên dịch sẽ báo một lỗi như sau:

 

Cannot create an instance of the abstract class or interface ‘Window’

Chúng ta có thể tạo được các thể hiện của lớp ListBox và Button, bởi vì hai lớp này đã phủ quyết phương thức trừu tượng. Hay có thể nói hai lớp này đã được xác định (ngược với lớp trừu tượng).

 

Hạn chế của lớp trừu tượng

 

Mặc dù chúng ta đã thiết kế phương thức DrawWindow() như một lớp 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 ListBox như lớp DropDownListBox, thì lớp này không được hỗ trợ để thực thi phương thức DrawWindow( ) cho riêng nó.

Ghi chú: Khác với ngôn ngữ C++, trong C# phương thức Window.DrawWindow( )  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 DrawWindow() 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 Window 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 Window, 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 Window.

Ý 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 “Window” trừu  tượng,  và nó  sẽ  được biểu lộ trong các thể  hiện xác định của Windows, như là Button, ListBox, Frame,…

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 8 nói về giao diện.

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 Window trong ví dụ 5.3 được thay đổi từ khóa abstract bằng từ khóa sealed (cũng có thể loại bỏ từ khóa trong khai báo của phương thức DrawWindow()). 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:

 

‘ListBox’ cannot inherit from sealed class ‘Window’

Đâ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.

 

Gốc của tất cả các lớp: 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

ToString( )

Cung cấp chuỗi thể hiện 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 5.1: Tóm tắt các phương thức của lớp Object.

Ví dụ 5.4 sau minh họa việc sử dụng phương thức ToString( ) thừa kế từ lớp Object.

Ví dụ 5.4: Thừa kế từ Object.

—————————————————————————–

 

using System;

 

public class SomeClass

 

{

 

public SomeClass( int val )

 

{

 

value = val;

 

}

 

// phủ quyết phương thức ToString của lớp Object public virtual string ToString()

{

 

return value.ToString();

 

}

 

// biến thành viên private lưu giá trị private int value;

}

 

public class Tester

 

{

 

static void Main( )

 

{

 

 

int i = 5;

 

Console.WriteLine(“The value of i is: {0}”, i.ToString()); SomeClass s = new SomeClass(7); Console.WriteLine(“The value of s is {0}”, s.ToString());

 

 

 

Console.WriteLine(“The value of 5 is {0}”,5.ToString());

 

}

 

}

 

—————————————————————————– Kết quả:

The value of i is: 5

 

The value of s is 7

 

The value of 5 is 5

—————————————————————————–

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.

Lớp SomeClass trong  ví  dụ  trên  thực  hiện  việc  phủ  quyết phương  thức 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 SomeClass, phương thức của lớp cơ sở sẽ được thực thi, và kết quả xuất ra sẽ có thay đổi như sau:

 

The value of s is SomeClass

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ể hiện.

Các lớp không cần phải khai báo tường minh việc dẫn xuất  từ lớp Object, việc kế thừa sẽ được đưa vào một cách ngầm định. Như lớp SomeClass trên ta không khai báo bất cứ dẫn xuất của lớp nào nhưng C# sẽ tự động đưa lớp Object thành lớp dẫn xuất. Do đó ta mới có thể phủ quyết phương thức ToString() của lớp Object.

Boxing và Unboxing dữ liệu

 

Boxing và unboxing là những xử lý cho phép kiểu dữ liệu giá trị (như int, long,…) được

đối xử như kiểu dữ liệu tham chiếu (các đối tượng). Một giá trị được đưa vào bên trong của

đối tượng, được gọi là Boxing. Trường hợp ngược lại, Unboxing sẽ chuyển từ đối tượng ra một giá trị. Xử lý này đã cho phép chúng ta gọi phương thức ToString( ) trên kiểu dữ liệu int trong ví dụ 5.4.

 

Boxing được thực hiện ngầm định

Boxing là một sự chuyển đổi ngầm định của một kiểu dữ liệu giá trị sang kiểu dữ liệu tham chiếu là đối tượng. Boxing một giá trị bằng cách tạo ra một thể hiển của đối tượng cần dùng và sao chép giá trị trên vào đối tượng mới tạo. Ta có hình vẽ sau minh họa quá trình Boxing một số nguyên.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Hình 5.5: Boxing số nguyên.

Boxing được thực hiện ngầm định khi chúng ta đặt một kiểu giá trị vào một tham chiếu đang chờ đợi và giá trị sẽ được đưa vào đối tượng một cách tự động ngầm định. Ví dụ, nếu chúng ta gán một kiểu dư liệu cơ bản như kiểu nguyên int vào một biến kiểu Object (điều này hoàn toàn  hợp  lệ  vì  kiểu  int được dẫn  xuất  từ lớp Object)  thì  giá trị  này sẽ  được đưa  vào  biến Object, như minh họa sau:

using System;

 

class Boxing

 

{

 

public static void Main()

 

{

 

int i = 123;

 

Console.WriteLine(“The object value = {0}”, i);

 

}

 

}

 

Unboxing phải được thực hiện tường minh

Việc đưa một giá trị vào một đối tượng được thực hiện một cách ngầm định. Và sự thực hiện ngược lại, unboxing, tức là đưa từ một đối tượng ra một giá trị phải được thực hiện một cách tường minh. Chúng ta phải thiết lập theo hai bước sau:

Phải chắc chắn rằng đối tượng đã boxing đúng kiểu giá trị đưa ra. Sao chép giá trị từ thể hiện hay đối tượng vào biến kịểu giá trị.

 

 

 

 

 

 

Hình 5.6: Unboxing sau khi thực hiện Boxing.

Để thực hiện unboxing thành công, thì đối tượng được unboxing phải được tham chiếu đến một đối tượng, và đối tượng này đã được tạo ra bằng việc boxing một giá trị cùng với kiểu của giá trị được đưa ra. Boxing và Unboxing được minh họa trong ví dụ 5.5.

Ví dụ 5.5: Boxing và Unboxing.

—————————————————————————–

 

using System;

 

public class UnboxingTest

 

{

 

public static void Main()

 

{

 

 

int i = 123;

 

// Boxing object o = i;

// Unboxing phải được tường minh int k = (int) o; Console.WriteLine(“k: {0}”, k);

 

 

}

 

}

 

—————————————————————————–

Ví dụ 5.5 tạo một số nguyên i và thực hiện boxing ngầm định khi i được gán cho một đối tượng o. Sau đó giá trị được unboxing một cách tường minh và gán đến một biến nguyên int mới, và cuối cùng giá trị được hiển thị.

Thông thường, chúng ta sẽ bao bọc các hoạt động unboxing trong khối try, sẽ được trình bày trong Chương 13. Nếu một đối tượng được Unboxing là null hay là tham chiếu đến một đối tượng có kiểu dữ liệu khác, một InvalidCastException sẽ được phát sinh.

Các lớp lồng nhau

 

Các lớp chứa những thành viên, và những thành viên này có thể là một lớp khác có kiểu

do người dùng định nghĩa (user-defined type). Do vậy, một lớp Button có thể có một thành viên của kiểu Location, và kiểu Location này chứa thành viên của kiểu dữ liệu Point. Cuối cùng, Point có thể chứa chứa thành viên của kiểu int.

Cho đến lúc này, các lớp được tạo ra chỉ để dùng cho các lớp bên ngoài, và chức năng của các lớp đó như là lớp trợ giúp (helper class). Chúng ta có thể định nghĩa một lớp trợ giúp bên trong  các  lớp  ngoài  (outer  class).  Các  lớp  được  định  nghĩa  bên  trong  gọi  là  các  lớp  lồng (nested class), và lớp chứa được gọi đơn giản là lớp ngoài.

Những lớp lồng bên trong có lợi là có khả năng truy cập đến tất cả các thành viên của lớp ngoài. Một phương thức của lớp lồng có thể truy cập đến biến thành viên private của lớp ngoài.

Hơn nữa, lớp lồng bên trong có thể ẩn đối với tất cả các lớp khác, lớp lồng có thể là private cho lớp ngoài.

Cuối cùng, một lớp làm lồng bên trong là public và được truy cập bên trong phạm vi của lớp ngoài. Nếu một lớp Outer là lớp ngoài, và lớp Nested là lớp public lồng bên trong lớp Outer, chúng  ta  có  thể  tham  chiếu  đến  lớp Tested như  Outer.Nested,  khi  đó  lớp  bên  ngoài  hành động ít nhiều giống như một namespace hay một phạm vi.

Ghi chú: Đối với người lập trình Java, lớp lồng nhau trong C# thì giống như những lớp nội  static (static  inner)  trong  Java.  Không  có  sự  tương  ứng  trong  C#  với  những  lớp  nội nonstatic (nonstatic inner) trong Java.

Ví dụ 5.6 sau sẽ thêm một lớp lồng vào lớp Fraction tên là FractionArtist. Chức năng của lớpFractionArtis là vẽ một phân số ra màn hình. Trong ví dụ này, việc vẽ sẽ được thay thế bằng sử dụng hàm WriteLine xuất ra màn hình console.

Ví dụ 5.6: Sử dụng lớp lồng nhau.

—————————————————————————–

 

using System;

 

using System.Text;

 

 

 

 

 

 

public class Fraction

 

{

 

public Fraction( int numerator, int denominator)

 

{

 

this.numerator = numerator;

 

this.denominator = denominator;

 

}

 

public override string ToString()

 

{

 

StringBuilder s = new StringBuilder(); s.AppendFormat(“{0}/{1}”,numerator, denominator); return s.ToString();

}

 

internal class FractionArtist

 

{

 

public void Draw( Fraction f)

 

{

 

Console.WriteLine(“Drawing the numerator {0}”, f.numerator); Console.WriteLine(“Drawing the denominator {0}”, f.denominator);

}

 

}

 

// biến thành viên private private int numerator; private int denominator;

}

 

public class Tester

 

{

 

static void Main()

 

{

 

Fraction f1 = new Fraction( 3, 4); Console.WriteLine(“f1: {0}”, f1.ToString());

Fraction.FractionArtist  fa = new Fraction.FractionArtist();

 

fa.Draw( f1 );

 

}

 

}

 

—————————————————————————–

Lớp Fraction trên nói chung là không có gì thay đổi ngoại trừ việc thêm một lớp lồng bên trong và lược đi một số phương thức không thích hợp trong ví dụ này. Lớp lồng bên trong FractionArtist chỉ cung cấp một phương thức thành viên duy nhất, phương thức Draw(). Điều thú  vị  trong  phương  thức  Draw() truy  cập  dữ  liệu  thành  viên  private  là  f.numerator và f.denominator.   Hai   viến   thành   viên   private  này   sẽ   không   cho   phép   truy   cập   nếu FractionArtist không phải là lớp lồng bên trong của lớp Fraction.

Lưu ý là trong hàm Main() khi khai báo một thể hiện của lớp lồng bên trong, chúng ta phải xác nhận tên của lớp bên ngoài, tức là lớp Fraction:

Fraction.FractionArtist   fa = new Fraction.FractionArtist();

Thậm chí khi lớp FractionArtist là public, thì phạm vị của lớp này vẫn nằm bên trong của lớp

Fraction.

Câu hỏi và trả lời

Câu hỏi 1: Có cần thiết phải chỉ định từ khóa override trong phương thức phủ quyết của lớp dẫn xuất hay không?

Trả lời 1: Có, chúng ta phải khai báo rõ ràng từ khóa override với phương thức phủ quyết phương thức ảo (của lớp cơ sở ) bên trong lớp dẫn xuất.

Câuhỏi 2: Lớp trừu tượng là thế nào? Có thể tạo đối tượng cho lớp trừu tượng hay không?

Trả lời 2: Lớp trừu tượng không có sự thực thi, các phương thức của nó được tạo ra chỉ là hình thức, tức là chỉ có khai báo, do vậy phần định nghĩa bắt buộc phải được thực hiện ở các

lớp dẫn xuất từ lớp trừu tượng này. Do chỉ là lớp trừu tượng, không có sự thực thi nên chúng

ta không thể tạo thể hiện hay tạo đối tượng cho lớp trừu tượng này.

Câuhỏi 3: Có phải khi tạo một lớp thì phải kế thừa từ một lớp nào không?

Trả lời 3: Không nhất thiết như vậy, tuy nhiên trong C#, thì tất cả các lớp được tạo điều phải dẫn xuất từ lớp Object. Cho dù chúng có được khai báo tường minh hay không. Do đó Object

là lớp gốc của tất cả các lớp được xây dựng trong C#. Một điều thú vị là các kiểu dữ liệu giá

trị như kiểu nguyên, thực, ký tự cũng được dẫn xuất từ Object. 

Câuhỏi 4: Lớp lồng bên trong một lớp là như thế nào?

Trả lời 4:  Lớp  lồng  bên  trong  một  lớp  hay còn  gọi  là  lớp  nội  được  khai  báo  với  từ  khóa internal, chứa bên trong phạm vi của một lớp. Lớp nội có thể truy cập được các thành viên private của lớp mà nó chứa bên trong

Câuhỏi 5: Có thể kế thừa từ một lớp cơ sở được viết trong ngôn ngữ khác ngôn ngữ C#?

Trả lời 5: Được, một trong những đặc tính của .NET là các lớp có thể kế thừa từ các lớp được viết từ ngôn ngữ khác. Do vậy, trong C# ta có thể kế thừa một lớp được viết từ ngôn ngữ khác của .NET. Và những ngôn ngữ khác cũng có thể kế thừa từ các lớp C# mà ta tạo ra.

Câu hỏi thêm

Câuhỏi 1: Sự đặt biệt hóa được sử dụng trong C# thông qua tính gì?

Câuhỏi 2: Khái niệm đa hình là gì? Khi nào thì cần sử dụng tính đa hình?

Câu hỏi 3:  Hãy  xây  dựng  cây  phân  cấp  các  lớp  đối  tượng  sau:  Xe_Toyota,  Xe_Dream, Xe_Spacy, Xe_BMW, Xe_Fiat, Xe_DuLich, Xe_May, Xe?

Câuhỏi 4: Từ khóa new được sử dụng làm gì trong các lớp?

Câu hỏi 5: Một phương thức ảo trong lớp cơ sở có nhất thiết phải được phủ quyết trong lớp dẫn xuất hay không?

Câu hỏi 6: Lớp trừu tượng có cần thiết phải xây dựng hay không? Hãy cho một ví dụ về một lớp trừu tượng cho một số lớp.

Câu hỏi 7:  Lớp  cô  lập  là  gì?  Có  thể  khai  báo  protected  cho  các  thành  viên  của  nó  được không?

Câu hỏi 8: Lớp Object cung cấp những phương thức nào mà các lớp khác thường xuyên kế thừa để sử dụng.

Câuhỏi 9: Thế nào là boxing và unboxing? Hãy cho biết hai ví dụ về quá trình này?

 

Bài tập

 

Bài tập 1: Hãy mở rộng ví dụ trong chương xây dựng thêm các đối tượng khác kế thừa lớp

Window như: Label, TextBox, Scrollbar, toolbar, menu,…

Bài tập2: Hãy xây dựng các lớp đối tượng trong câu hỏi 3, thiết lập các quan hệ kế thừa dựa trên cây kế thừa mà bạn xây dựng. Mỗi đối tượng chỉ cần một thuộc tính là myNane để cho biết tên của nó (như Xe_Toyota thì myName là “Toi la Toyota”…). Các đối tượng có phương thức Who() cho biết giá trị myName của nó. Hãy thực thi sự đa hình trên các lớp đó. Cuối cùng tạo một lớp Tester với hàm Main() để tạo một mảng các đối tượng Xe, đưa từng  đối tượng cụ thể vào mảng đối tượng Xe, sau đó cho lặp từng đối tượng trong mảng để nó tự giới thiệu tên (bằng cách gọi hàm Who() của từng đối tượng).

Bài tập 3: Xây dựng các lớp đối tượng hình học như: điểm, đoạn thẳng, đường tròn, hình chữ nhật, hình vuông, tam giác, hình bình hành, hình thoi. Mỗi lớp có các thuộc tính riêng để xác định được hình vẽ biểu diễn của nó như đoạn thẳng thì có điểm đầu, điểm cuối…. Mỗi lớp thực thi một phương thức Draw() phủ quyết Draw() của lớp cơ sở gốc của các hình mà nó dẫn xuất. Hãy xây dựng lớp cơ sở của các lớp trên và thực thi đa hình với phương thức Draw(). Sau đó tạo lớp Tester cùng với hàm Main() để thử nghiệm đa hình giống như bài tập 2 ở trên.

Bài tập 4: Chương trình sau đây có lỗi. Hãy sửa lỗi biên dịch và chạy chương trình. Cho biết lệnh nào gây ra lỗi. Và nguyên nhân gây ra lỗi?

—————————————————————————–

 

using System;

 

abstract public class Animal

 

{

 

public Animal(string name)

 

{

 

this.name = name;

 

}

 

 

 

 

 

 

// phương thức trừu tượng minh họa việc

 

// đưa tên của đối tượng abstract public void Who();

// biến thành viên protected protected string name;

}

 

// lớp Dog dẫn xuất từ lớp Animal public class Dog : Animal

{

 

// hàm khởi dựng lấy hai tham số

 

public Dog(string name, string color) : base(name)

 

{

 

this.color = color;

 

}

 

// phủ quyết phương thức trừu tượng Who()

 

public override void Who( )

 

{

 

Console.WriteLine(“Gu gu! Toi la {0} co mau long {1}”, name, color);

 

}

 

// biến private của lớp private string color;

}

 

public class Cat : Animal

 

{

 

// hàm khởi dựng lấy hai tham số

 

public Cat(string name, int weight) : base(name)

 

{

 

this.weight = weight;

 

}

 

// phủ quyết phương thức trừu tượng Who()

 

public override void Who( )

 

{

 

Console.WriteLine(“Meo meo! Toi la {0} can nang {1}”, name, weight);

 

}

 

// biến private của lớp private int weight;

}

 

 

public class Tester

 

 

 

 

 

 

 

{

 

static void Main()

 

{

 

Animal[] Arr = new Animal[3];

 

Arr[0] = new Dog(“Lu Lu”, “Vang”); Arr[1] = new Cat(“Mun”, 5);

Arr[2] = new Animal(“Noname”);

 

for( int i=0; i <3 ; i++)

 

{ Arr[i].Who();

}

 

}

 

}

 

—————————————————————————–

 

Hết chương 5

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Related posts

GT C Sharp cơ bản – Bài 30 : Chương 29:  So sánh C# với các ngôn ngữ khác

GT C Sharp cơ bản – Bài 29 : Chương 28: Dòng lệnh

GT C Sharp cơ bản – Bài 28 : Chương 27: Tính tương hoạt