Thừa kế và đa hình trong C#

Xem thêm các chuyên mục:

1- Giới thiệu

Thừa kế và đa hình - đây là một khái niệm vô cùng quan trọng trong CSharp. Mà bạn bắt buộc phải hiểu nó.

2- Lớp, đối tượng và Constructor

Bạn cần hiểu một cách rạch ròi về class, cấu tử (constructor) và đối tượng trước khi bắt đầu tìm hiểu quan hệ thừa kế trong CSharp. Chúng ta xem lớp Person, mô tả một con người với các thông tin liên quan.
Person.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class Person
    {
        // Trường name - thông tin tên người
        public String Name;

        // Trường bornYear - thông tin năm sinh
        public int BornYear;

        // Nơi sinh
        public String PlaceOfBirth;

        // Constructor  này có 3 tham số. 
        // Mục đích để khởi tạo các giá trị cho các trường của Person.
        // Chỉ định rõ tên, năm sinh, nơi sinh.
        public Person(String Name, int BornYear, String PlaceOfBirth)
        {
            this.Name = Name;
            this.BornYear = BornYear;
            this.PlaceOfBirth = PlaceOfBirth;
        }

        // Constructor này có 2 tham số. 
        // Mục đích khởi tạo giá trị cho 2 trường tên và năm sinh cho Person.
        // Nơi sinh không được khởi tạo.
        public Person(String Name, int BornYear)
        {
            this.Name = Name;
            this.BornYear = BornYear;
        }

    }
}
PersonDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class PersonDemo
    {

        static void Main(string[] args)
        {

            // Đối tượng: Thomas Edison.
            // Được tạo ra bởi Constructor có 2 tham số của lớp Person.
            Person edison = new Person("Thomas Edison", 1847);

            Console.WriteLine("Info:");
            Console.WriteLine("Name: " + edison.Name);
            Console.WriteLine("Born Year: " + edison.BornYear);
            Console.WriteLine("Place Of Birth: " + edison.PlaceOfBirth);

            // Đối tượng: Bill Gates
            // Được tạo ra bởi Constructor có 3 tham số của lớp Person.
            Person billGates = new Person("Bill Gate", 1955, "Seattle, Washington");

            Console.WriteLine("-----------------------------------");

            Console.WriteLine("Info:");
            Console.WriteLine("Name: " + billGates.Name);
            Console.WriteLine("Born Year: " + billGates.BornYear);
            Console.WriteLine("Place Of Birth: " + billGates.PlaceOfBirth);

            Console.ReadLine();
        }
    }
}
Kết quả chạy lớp PersonDemo:

Phân biệt Lớp, Constructor và đối tượng:

Lớp Person mô phỏng một lớp người, nó là một thứ gì đó trừu tượng, nhưng nó có các trường để mang thông tin, trong ví dụ trên là tên, năm sinh, nơi sinh.
Constructor (Cấu tử).
  • Constructor luôn có tên giống tên của lớp.
  • Một lớp có thể có một hoặc nhiều Constructor.
  • Constructor có thể có hoặc không có tham số, Constructor không có tham số còn gọi là Constructor mặc định.
  • Constructor  được sử dụng để tạo ra một đối tượng của lớp đó.
Như vậy lớp Person (Mô tả lớp người) là thứ trừu tượng, nhưng khi chỉ rõ vào bạn hoặc tôi thì đó là 2 đối tượng thuộc lớp Person. Và Constructor là một phương thức đặc biệt để tạo ra đối tượng, Constructor sẽ gán các giá trị vào các trường (field) của đối tượng được tạo ra..
Đây là hình ảnh minh họa các trường (field) của lớp được gán giá trị thế nào, khi bạn tạo một đối tượng từ Constructor.

3- Thừa kế trong CSharp

Chúng ta cần một vài lớp tham gia vào các ví dụ.
  • Animal: Lớp mô phỏng một động vật.
  • Duck: Lớp mô phỏng con vịt, là một lớp con của Animal.
  • Cat: Lớp mô phỏng con mèo, là một lớp con của Animal
  • Mouse: Lớp mô phỏng con chuột, là một lớp con của Animal.
Như đã biết ở phần trước, Constructor của lớp được sử dụng để tạo ra một đối tượng, và gán giá trị cho các trường (field).
Constructor của lớp con bao giờ cũng gọi tới một Constructor của lớp cha để khởi tạo giá trị cho các trường của lớp cha, sau đó nó mới khởi tạo giá trị cho các trường của nó.
Hãy xem ví dụ:
Animal.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    public abstract class Animal
    {
        // Đây là Trường Name (Tên).
        // ví dụ Mèo Tom, Chuột Jerry.
        public string Name;

        // Constructor mặc định.
        public Animal()
        {
            Console.WriteLine("- Animal()");
        }

        public Animal(string Name)
        {
            // Gán giá trị cho trường Name.
            this.Name = Name;
            Console.WriteLine("- Animal(string)");
        }

        // Move(): Con vật di chuyển. 
        // virtual: Cho phép lớp con có thể ghi đè (override) phương thức này.
        public virtual void Move()
        {
            Console.WriteLine("Animal Move");
        }

        public void Sleep()
        {
            Console.WriteLine("Sleep");
        }

    }
}
Cat là lớp con thừa kế từ lớp Animal, nó cũng có các trường của mình.
Cat.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    public class Cat : Animal
    {

        public int Age;
        public int Height;

        // Đây là một Constructor có 3 tham số của lớp Cat.
        // Sử dụng :base(name) để gọi đến Constructor của lớp cha: Animal(string).
        // Các trường của lớp cha sẽ được gán giá trị.
        // Sau đó các trường của lớp này mới được gán giá trị.
        public Cat(string name, int Age, int Height)
            : base(name)
        {
            this.Age = Age;
            this.Height = Height;
            Console.WriteLine("- Cat(string,int,int)");
        }

        // Constructor này gọi tới Constructor tử mặc định (Không tham số) của lớp cha.
        public Cat(int Age, int Height)
            : base()
        {
            this.Age = Age;
            this.Height = Height;
            Console.WriteLine("- Cat(int,int)");
        }

        public void Say()
        {
            Console.WriteLine("Meo");
        }

        // Ghi đè phương thức Move() của lớp cha (Animal).
        // Viết lại phương thức này, 
        // để mô tả chính xác hành vi di chuyển của loài Mèo. 
        public override void Move()
        {
            Console.WriteLine("Cat Move ...");
        }
    }
}
CatTest.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class CatTest
    {
        static void Main(string[] args)
        {

            Console.WriteLine("Create Cat object from Cat(string,int,int)");

            // Khởi tạo một đối tượng Cat từ cấu tử 3 tham số.
            // Trường Name của Animal sẽ được gán giá trị "Tom".
            // Trường Age của Cat sẽ được gán giá trị 3
            // Trường Height của Cat sẽ được gán giá trị 20.
            Cat tom = new Cat("Tom",3, 20);

            Console.WriteLine("------");
            
            Console.WriteLine("Name = {0}", tom.Name);
            Console.WriteLine("Age = {0}", tom.Age);
            Console.WriteLine("Height = {0}", tom.Height);

            Console.WriteLine("------");

            // Gọi phương thức được thừa kế từ lớp Animal.
            tom.Sleep();

            // Gọi phương thức Say() (của lớp Cat)
            tom.Say();


            Console.ReadLine();
        }
    }
}
Kết quả chạy lớp CatTest:
Điều gì đã xẩy ra khi bạn khởi tạo một đối tượng từ một Constructor? Nó sẽ gọi lên một Constructor của lớp cha như thế nào? Bạn hãy xem hình minh họa dưới đây:
Với hình minh họa trên bạn thấy rằng, Constructor của lớp cha bao giờ cũng được gọi trước Constructor của lớp con, nó sẽ gán giá trị cho các trường của lớp cha trước, sau đó các trường của lớp con mới được gán giá trị.
Khi bạn viết một Constructor và không chỉ rõ nó dựa trên (base) Constructor nào của lớp cha, CSharp mặc định hiểu là Constructor đó dựa trên Constructor mặc định của lớp cha.
 
// Constructor  này không chỉ rõ nó dựa trên (base) Constructor nào của lớp cha.
public Cat(int Age, int Height)
{

}

// Nó sẽ tương đương với:
public Cat(int Age, int Height) : base()
{

}
Một Constructor có thể gọi tới một Constructor khác trong cùng một lớp bằng cách sử dụng :this.
private int Weight;

// Constructor mặc định (Không có tham số).
// Nó gọi tới constructor Mouse(int).
public Mouse()    : this(100)
{
}

// Đây là constructor có 1 tham số.
// Không chỉ rõ :base
// Nghĩa là nó dựa trên (base) Constructor mặc định của lớp cha.
public Mouse(int Weight)
{            
   this.Weight = Weight;
}
Mouse.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    public class Mouse : Animal
    {

        private int Weight;

        // Constructor mặc định (Không có tham số).
        // Gọi tới Mouse(int)
        public Mouse()
            : this(100)
        {
        }

        // Constructor này có 1 tham số.
        // Và không ghi rõ :base
        // Nghĩa là nó base (dựa trên) Constructor mặc định của lớp cha.
        public Mouse(int Weight)
        {            
            this.Weight = Weight;
        }

        // Constructor này có 2 tham số.
        public Mouse(String name, int Weight)
            : base(name)
        {
            this.Weight = Weight;
        }

        public int GetWeight()
        {
            return Weight;
        }

        public void SetWeight(int Weight)
        {
            this.Weight = Weight;
        }

 
    }
}
Sử dụng toán tử 'is' bạn có thể kiểm tra một đối tượng có phải là kiểu của một class nào đó hay không. Hãy xem ví dụ dưới đây:
IsOperatorDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class IsOperatorDemo
    {
        static void Main(string[] args)
        {
            // Khởi tạo một đối tượng Animal.
            // Animal là lớp trừu tượng, 
            // Vì vây bạn không thể tạo đối tượng từ Constructor của Animal. 
            Animal tom = new Cat("Tom", 3, 20);

            Console.WriteLine("Animal Sleep:");

            // Gọi phương thức Sleep() của Animal
            tom.Sleep();

            // Sử dụng toán tử 'is' để kiểm tra xem
            // một đối tượng có phải kiểu nào đó không. 
            bool isMouse = tom is Mouse;// false

            Console.WriteLine("Tom is mouse? " + isMouse);

            bool isCat = tom is Cat; // true
            Console.WriteLine("Tom is cat? " + isCat);

            bool isAnimal = tom is Animal; // true
            Console.WriteLine("Tom is animal? " + isAnimal);

            Console.ReadLine();
        }
    }
}
Chạy ví dụ:

Ép kiểu (cast) trong CSharp.

CastDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class CastDemo
    {
        static void Main(string[] args)
        {
            // Gọi phương thức, nó trả về một con vật ngẫu nhiên.
            Animal animal = GetRandomAnimal();

            if (animal is Cat)
            {
                Console.WriteLine("Your animal is Cat");

                // Ép kiểu (cast) thành đối tượng Cat.
                Cat cat = (Cat)animal;

                // Và truy cập vào trường Height của đối tượng Cat.
                Console.WriteLine("Cat height: " + cat.Height);
            }
            else if (animal is Mouse)
            {
                Console.WriteLine("Your animal is Mouse");

                // Ép kiểu (cast) thành đối tượng Mouse.
                Mouse mouse = (Mouse)animal;

                // Và gọi một phương thức của lớp Mouse.
                Console.WriteLine("Mouse weight: " + mouse.GetWeight());
            }

            Console.ReadLine();
        }

        // Method trả về ngẫu nhiên một con vật.
        public static Animal GetRandomAnimal()
        {
            // Trả về số ngẫu nhiên nằm giữa 0 và 9 (0,...9) 
            int random = new Random().Next(0, 10);

            Console.WriteLine("random = " + random);

            Animal animal = null;
            if (random < 5)
            {
                Console.WriteLine("Create a Cat");
                animal = new Cat("Tom", 3, 20);
            }
            else
            {
                Console.WriteLine("Create a Mouse");
                animal = new Mouse("Jerry", 5);
            }
            return animal;
        }
    }
}
Chạy ví dụ:

4- Đa hình trong CSharp

Đa hình (Polymorphism) từ này có nghĩa là có nhiều hình thức. Trong mô hình lập trình hướng đối tượng, đa hình thường được diễn tả như là "một giao diện, nhưng nhiều chức năng".

Đa hình có thể là tĩnh hoặc động. Trong đa hình tĩnh, phản ứng với một chức năng được xác định tại thời gian biên dịch. Trong đa hình động, nó được quyết định tại thời gian chạy (run-time).
Bạn có một con mèo nguồn gốc châu Á (AsianCat), bạn có thể nói nó là một con mèo (Cat) hoặc nói nó là một con vật (Animal) đó là một khía cạnh của từ đa hình.

Hoặc một ví dụ khác: Trên lý lịch của bạn ghi rằng bạn là một người châu Á, trong khi đó bạn thực tế là một người Việt Nam. Và tôi cũng có thể nói rằng bạn là người trái đất.

Ví dụ dưới đây cho bạn thấy cách hành xử giữa khai báo và thực tế:
PolymorphismCatDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class PolymorphismCatDemo
    {
        static void Main(string[] args)
        {
            // Bạn khai báo một đối tượng Animal (Động vật).
            // Bằng cách tạo nó bởi Constructor của lớp Cat. 
            // ** Đối tượng 'tom' khai báo là Animal
            // vì vậy nó chỉ có thể gọi các phương thức của Animal.
            Animal tom = new Cat("Tom", 3, 20); 

            // Gọi phương thức Sleep. 
            tom.Sleep();

            // - Move() là phương thức được định nghĩa trong lớp Animal.
            // - Move() được ghi đè (override) trong lớp Cat.
            // 'tom' thực tế là Cat, mặc dù đang được khai báo là Animial.
            // ==> Nó sẽ gọi phương thức Move() định nghĩa trong lớp Cat. 
            tom.Move(); // ==> Cat Move.


            Console.ReadLine();
        }
    }
}
Bạn có 2 cách để viết lại một phương thức được định nghĩa ở lớp cha, bằng cách sử dụng từ khóa override hoặc new. Và chúng rất khác nhau. Hãy xem hình minh họa dưới đây:
Duck.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
   class Duck : Animal
   {

       public Duck(string name)
           : base(name)
       {
           Console.WriteLine("- Duck(string)");
       }


       public new void Move()
       {
           Console.WriteLine("Duck Move..");
       }
   }
}
InheritanceDuckDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InheritancePolymorphism
{
    class InheritanceDuckDemo
    {
        static void Main(string[] args)
        {
            // Bạn khai báo một đối tượng Animal.
            // Nhưng tạo nó bằng Constructor của lớp Duck. 
            Animal donald = new Duck("Donald");

            // Gọi phương thức Sleep() (Được định nghĩa trong lớp Animal).
            donald.Sleep();

            // - Move() là phương thức được định nghĩa trong lớp Animal.
            // - Move() được "viết lại" với từ khóa 'new' trong lớp Duck. 
            //  ('new' Move() chỉ được dùng cho các đối tượng được khai báo là Duck hoặc lớp con của Duck).
            // 'donald' is declared as 'Animal', not 'Duck'.
            donald.Move(); // ==> Animal Move.


            Console.ReadLine();
        }
    }
}
Chạy ví dụ:

Xem thêm các chuyên mục: