C# - C Sharp: Đa hình (Polymorphism)


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Giới thiệu

Trong thực tế, những động vật như tắc kè hoa chẳng hạn có khả năng thay đổi màu sắc dựa trên môi trường. Một người nào đó có thể đóng các vai trò khác nhau trong cuộc sống hàng ngày của anh ta, như là cha, con, chồng. Như vậy thì trong những tình huống khác nhau thì anh ta cũng hành xử khác nhau. Tương tự như vậy, C# cung cấp đặc điểm gọi là Đa hình (Polymorphism) trong đó một đối tượng có thể có những hành xử khác nhau dựa trên bối cảnh mà nó được sử dụng.

Từ 'polymorph' là sự kết hợp của hai từ, 'poly' nghĩa là 'many' và 'morph' nghĩa là 'forms'. Vì thế, 'polymorphism' là 'đa hình', nó tham chiếu tới một đối tượng mà có thể có nhiều dạng. Nguyên tắc này cũng có thể áp dụng cho các lớp con của một lớp mà có thể định nghĩa các hành vi đặc trưng của chính chúng cũng như dẫn xuất một số chức năng tương tự của lớp cha. Khái niệm ghi đè phương thức là một ví dụ về đa hình trong lập trình hướng đối tượng trong đó cùng một phương thức nhưng lại cư xử khác nhau giữa lớp cha và lớp con.

Hiểu rõ về liên kết tĩnh và liên kết động

Khi trình biên dịch giải quyết liên kết của các phương thức và các lời gọi phương thức lúc biên dịch, nó được gọi là liên kết tĩnh hay liên kết sớm. Nếu trình biên dịch giải quyết các lời gọi phương thức và các liên kết trong thời gian chạy (runtime), thì nó được gọi là liên kết động hoặc liên kết muộn. Tất cả các lời gọi phương thức tĩnh đều được giải quyết lúc biên dịch và do đó, liên kết tĩnh được thực hiện cho tất cả các lời gọi phương thức tĩnh. Các lời gọi phương thức thể hiện luôn luôn được giải quyết lúc thực thi chương trình.

Các phương thức tĩnh là các phương thức lớp và được truy cập bằng cách sử dụng tên của chính lớp đó. Việc sử dụng phương thức tĩnh là được khuyến khích, bởi vì các tham chiếu đối tượng không được yêu cầu để truy cập chúng và do đó, các lời gọi phương thức tĩnh được giải quyết trong quá trình biên dịch chính chúng. Đó cũng là lý do vì sao mà các phương thức tĩnh không được ghi đè.

Tương tự như vậy, C# không cho phép có hành vi đa hình trong các biến của một lớp bất kỳ. Do đó, việc truy cập đến tất cả các biến cũng là theo liên kết tĩnh.

Một số điểm khác biệt quan trọng giữa liên kết tĩnh và liên kết động được thể hiện như bảng dưới đây.

Liên kết tĩnh Liên kết động
Liên kết tĩnh xảy ra khi biên dịch. Liên kết động xảy ra khi thực thi.
Các phương thức và biến private, static, và final sử dụng liên kết tĩnh và được quy định bởi trình biên dịch. Các phương thức trừu tượng được quy định lúc thực thi và dựa trên đối tượng thực thi.
Liên kết tĩnh sử dụng thông tin kiểu đối tượng để liên kết, đó là kiểu của lớp. Liên kết động sử dụng kiểu tham chiếu để giải quyết liên kết.
Các phương thức được tải chồng được quy định sử dụng liên kết tĩnh. Các phương thức được ghi đè được quy định sử dụng liên kết động.

Đoạn mã 1 thể hiện một ví dụ về liên kết tĩnh.

Đoạn mã 1:

namespace ConsoleApp1
{
 class Employee
 {
  string id; // biến để lưu mã nhân viên
  string name; // lưu tên nhân viên
  float salary; // lưu lương
  float commission; // lưu hoa hồng
  
  /* hàm tạo để khởi tạo giá trị ban đầu cho các trường */
  public Employee(string id, string name, float salary)
  {
   this.id = id;
   this.name = name;
   this.salary = salary;
  }

  /* tính hoa hồng dựa trên doanh số bán hàng */
  public void CalCommission(float sales)
  {
   if (sales > 10000)
    commission = salary * 20 / 100;
   else
    commission = 0;
  }

  /* tải chồng chương thức. Tính hoa hồng dựa trên thời gian vượt giờ */
  public void Commission(int overtime)
  {
   if (overtime > 8)
    commission = salary / 30;
   else
    commission = 0;
  }

  /* hiển thị thông tin chi tiết nhân viên */
  public void EmployeeDetail()
  {
   Console.WriteLine($"ID: {id}");
   Console.WriteLine($"Name: {name}");
   Console.WriteLine($"Salary: {salary}");
   Console.WriteLine($"Commission: {commission}");
  }
 }

 /* định nghĩa lớp TestEmployee */
 class TestEmployee
 {
  static void Main(string[] args)
  {
   // tạo đối tượng của lớp NhanVien
   Employee objEmp = new Employee("EMP001", "Phan Ba Sang", 5000000);

   // gọi phương thức CalCommission() với đối số kiểu float
   objEmp.CalCommission(3000000F);

   // in ra thông tin chi tiết nhân viên
   objEmp.EmployeeDetail();
  }
 }
}

Kết quả thực thi:

ID: EMP001
Name: Phan Ba Sang
Salary: 5000000
Commission: 1000000

Đoạn mã 1 cho thấy lớp Employee có bốn trường. Hàm tạo được sử dụng để khởi tạo các trường với các giá trị nhận được. Lớp cũng bao gồm hai phương thức cùng tên CalCommission(). Phương thức thứ nhất tính hoa hồng dựa trên doanh số bán hàng được thực hiện bởi nhân viên và phương thức thứ hai tính toán hoa hồng dựa trên số giờ làm việc ngoài giờ của nhân viên. Phương thức EmployeeDetail() được dùng để in ra chi tiết thông tin nhân viên.

Một lớp khác là lớp TestEmployee được tạo chứa phương thức Main(). Bên trong phương thức Main(), đối tượng objEmp của lớp Employee được tạo và hàm tạo có tham số được gọi với các đối số khác nhau. Tiếp theo, phương thức CalCommission() được gọi với đối số là 3000000F. Khi phương thức CalCommission() được thực thi thì phương thức tương ứng với đối số có kiểu float được gọi đến bởi vì nó được quy định trong quá trình biên dịch dựa trên kiểu của biến. Sau cùng, phương thức EmployeeDetail() được gọi để in ra chi tiết thông tin của nhân viên.

Đoạn mã 2 thể hiện một ví dụ về liên kết động.

Đoạn mã 2:

namespace ConsoleApp1
{
 class Employee
 {
  string id; // biến để lưu mã nhân viên
  string name; // lưu tên nhân viên
  float salary; // lưu lương
  float commission; // lưu hoa hồng
  
  /* hàm tạo để khởi tạo giá trị ban đầu cho các trường */
  public Employee(string id, string name, float salary)
  {
   this.id = id;
   this.name = name;
   this.salary = salary;
  }

  /* tính hoa hồng dựa trên doanh số bán hàng */
  public void CalCommission(float sales)
  {
   if (sales > 10000)
    commission = salary * 20 / 100;
   else
    commission = 0;
  }

  /* tải chồng chương thức. Tính hoa hồng dựa trên thời gian vượt giờ */
  public void Commission(int overtime)
  {
   if (overtime > 8)
    commission = salary / 30;
   else
    commission = 0;
  }

  /* hiển thị thông tin chi tiết nhân viên */
  public void EmployeeDetail()
  {
   Console.WriteLine($"ID: {id}");
   Console.WriteLine($"Name: {name}");
   Console.WriteLine($"Salary: {salary}");
   Console.WriteLine($"Commission: {commission}");
  }
 }

 class PartTimeEmployee : Employee
 {
  // biến của lớp con
  string workShift; // biến này dùng để lưu trữ thông tin chuyển đổi ca làm

  /* hàm tạo có tham số để khởi tạo các giá trị dựa trên giá trị nhập vào từ người dùng */
  public PartTimeEmployee(string id, string name, float salary, string workShift):
   base(id, name, salary)
  {
   // gọi hàm tạo lớp cha
   this.workShift = workShift;
  }

  /* ghi đè phương thức để hiển thị thông tin chi tiết của nhân viên */
  public new void EmployeeDetail()
  {
   CalCommission(12); // gọi phương thức đã thừa kế
   base.EmployeeDetail(); // gọi phương thức hiển thị của lớp cha
   Console.WriteLine($"Work shift: {workShift}");
  }
 }

 /* Thay đổi định nghĩa lớp TestNhanVien */
 class TestEmployee
 {
  static void Main(string[] args)
  {
   // tạo đối tượng lớp NhanVien
   Employee objEmp = new Employee("EMP001", "Dinh Thi Kim Ngan", 4000000);

   objEmp.CalCommission(3000000F); // tính hoa hồng

   objEmp.EmployeeDetail(); // in thông tin chi tiết của objEmp

   Console.WriteLine("-------------------------");
   /* tạo biến tham chiếu của lớp NhanVien nhưng lại tham chiếu
   đến đối tượng của lớp NhanVienPartTime */
   PartTimeEmployee objEmp1 = new PartTimeEmployee("EMP002", "Bui Quynh Hoa", 3000000, "Ca sang");
   objEmp1.EmployeeDetail(); // in thông tin chi tiết của objEmp1
  }
 }
}

Kết quả thực thi:

ID: EMP001
Name: Dinh Thi Kim Ngan
Salary: 4000000
Commission: 800000
-------------------------
ID: EMP002
Name: Bui Quynh Hoa
Salary: 3000000
Commission: 0
Work shift: Ca sang

Đoạn mã 2 hiển thị lớp PartTimeEmployee thừa kế từ lớp Employee. Lớp này có biến riêng của nó là workShift để chỉ ra rằng nhân viên làm ca ngày hay ca đêm. Hàm tạo của lớp PartTimeEmployee gọi hàm tạo của lớp cha sử dụng từ khóa base để khởi tạo các thuộc tính chung của nhân viên. Ngoài ra, nó cũng khởi tạo cho biến workShift.

Lớp con ghi đè phương thức EmployeeDetail(). Bên trong phương thức được ghi đè, phương thức CalCommission() được gọi đến với một đối số kiểu nguyên. Nó sẽ tính hoa hồng dựa trên giờ làm thêm. Tiếp theo, phương thức EmployeeDetail() của lớp cha được gọi để hiển thị những thông tin cơ bản của nhân viên cũng như chi tiết về workShift.

Lớp TestEmployee được sửa đổi trong đó tạo một đối tượng khác là objEmp1 của lớp Employee. Tuy nhiên, đối tượng được gán tham chiếu của lớp PartTimeEmployee hàm tạo được gọi là hàm tạo bốn tham số. Sau đó, phương thức EmployeeDetail() được gọi để in ra chi tiết nhân viên.

Chú ý rằng đầu ra của nhân viên mã "EMP002" cũng thể hiện chi tiết của biến workShift. Điều này chỉ ra rằng phương thức EmployeeDetail() của lớp con PartTimeEmployee được gọi mặc dù kiểu của đối  tượng objEmp1 là Employee. Đó là bởi vì, trong quá trình tạo nó lưu trữ tham chiếu của lớp PartTimeEmployee.

Đây là liên kết động, đó là lời gọi phương thức được quy định cho đối tượng lúc thực thi dựa trên tham chiếu được gán cho đối tượng.

Sự khác nhau giữa kiểu tham chiếu và kiểu đối tượng

Trong đoạn mã 2, kiểu của của đối tượng objEmp1 là Employee. Điều này có nghĩa là đối tượng sẽ có tất cả các đặc điểm của lớp Employee. Tuy nhiên, tham chiếu được gán cho đối tượng của lớp PartTimeEmployee. Điều này có nghĩa là đối tượng sẽ liên kết với các thành phần của lớp PartTimeEmployee trong quá trình chạy. Trong trường hợp này, kiểu đối tượng là Employee và kiểu tham chiếu là PartTimeEmployee. Điều này chỉ có thể xảy ra khi các lớp có mối liên quan theo quan hệ cha-con.

C# cho phép gán một thể hiện của lớp con cho lớp cha của nó. Điều này gọi là boxing.

Ví dụ,

PartTimeEmployee objPTE = new PartTimeEmployee();
Employee objEmp = objPTE; // boxing

Trong khi boxing một đối tượng, thì đối tượng con objPTE được gán trực tiếp cho đối tượng objEmp của lớp cha. Tuy nhiên, đối tượng cha không thể truy cập các thành phần riêng của đối tượng con và không có sẵn trong lớp cha.

C# cũng cho phép ép kiểu tham chiếu cha trở về kiểu con. Điều này là bởi cha tham chiếu một đối tượng có kiểu con. Ép kiểu một đối tượng cha sang kiểu con được gọi là boxing bởi vì một đối tượng có quyền được ép kiểu sang một lớp nhỏ hơn trong cấu trúc phân cấp. Tuy nhiên, unboxing yêu cầu ép kiểu tường minh bằng cách xác định tên lớp con trong cặp ngoặc tròn. Ví dụ,

PartTimeEmployee objPTE = (PartTimeEmployee) objEmployee; // unboxing

Lời gọi phương thức ảo

Trong đoạn mã 2, trong quá trình thực thi câu lệnh Employee objEmp1= new PartTimeEmployee(…);, kiểu runtime (thời gian thực thi) của đối tượng Employee được xác định. Trình biên dịch không phát sinh lỗi bởi vì lớp Employee cũng có phương thức EmployeeDetail(). Tại thời điểm chạy, phương thức đã thực thi được tham chiếu từ đối tượng lớp PartTimeEmployee. Khía cạnh này của đa hình gọi là lời gọi phương thức ảo.

Sự khác nhau ở đây là giữa trình biên dịch và thời gian thực thi. Trình biên dịch kiểm tra khả năng truy cập của mỗi phương thức và biến thể hiện dựa trên định nghĩa lớp, trong khi đó hành vi liên quan đến một đối tượng được xác định tại thời gian thực thi.

Đây là một khía cạnh quan trọng của đa hình trong đó hành vi của đối tượng được xác định tại thời điểm thực thi dựa trên tham chiếu được truyền tới nó.

Ở đây, vì đối tượng được tạo là thuộc về lớp PartTimeEmployee, nên phương thức EmployeeDetail() của PartTimeEmployee được gọi mặc dù đối tượng có kiểu Employee. Điều này được tham chiếu như là lời gọi phương thức ảo và phương thức được tham chiếu tới như là phương thức ảo.

Trong C#, tất cả các phương thức đều hành xử theo cách này, theo đó một phương thức được ghi đè trong lớp con được gọi tại thời điểm thực thi không phân biệt kiểu tham chiếu được sử dụng trong mã nguồn lúc biên dịch. Trong các ngôn ngữ khác như C++, ta cũng có thể đạt được điều tương tự bằng cách sử dụng từ khóa virtual.

» Tiếp: Bài tập cơ bản
« Trước: Từ khóa base
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!