Tính kế thừa trong C#

Tính kế thừa (Inheritance) là một trong bốn trụ cột quan trọng của lập trình hướng đối tượng (OOP).

  • Tính đóng gói (Encapsulation).
  • Tính kế thừa (Inheritance).
  • Tính đa hình (Polymorphirsm).
  • Tính trừu tượng (Abstraction).

Nếu chưa hiểu rõ về lập trình hướng đối tượng thì bạn nên tham khảo bài viết dưới đây trước:

Lập trình hướng đối tượng (OOP) trong C# | Comdy
Lập trình hướng đối tượng (OOP) trong C# là gì? 5 khái niệm và 4 tính chất quan trọng của lập trình hướng đối tượng là gì?

Tính kế thừa là khả năng tạo một lớp mới dựa trên một lớp có sẵn. Lớp có sẵn là lớp cha, lớp mới là lớp con và lớp con thừa kế các thành phần được định nghĩa ở lớp cha.

Khi tạo một lớp, thay vì viết các thành viên hoàn toàn mới lập trình viên có thể chỉ định rằng lớp mới sẽ kế thừa các thành viên của một lớp hiện có. Lớp hiện có này được gọi là lớp cơ sở và lớp mới được gọi là lớp dẫn xuất.

Ý tưởng về sự kế thừa thực hiện mối quan hệ IS-A (là một). Ví dụ, động vật có vú là một động vật, chó là một động vật có vú do đó chó cũng là một động vật, v.v.

Các lớp cơ sở và dẫn xuất

Một lớp có thể kế thừa các thành viên của một lớp hiện có.  Lớp hiện có này được gọi là lớp cơ sở và lớp mới được gọi là lớp dẫn xuất.

Cú pháp được sử dụng trong C# để tạo các lớp dẫn xuất như sau:

<acess-specifier> class <base_class> 
{
   ...
}

class <derived_class> : <base_class> 
{
   ...
}

Hãy xem xét một hình dạng lớp cơ sở và hình chữ nhật của lớp dẫn xuất của nó:

using System;

namespace InheritanceApplication 
{
   class Shape 
   {
      protected int width;
      protected int height;
      
      public void SetWidth(int w) 
      {
         width = w;
      }
      public void SetHeight(int h) 
      {
         height = h;
      }
   }

   // Derived class
   class Rectangle: Shape 
   {
      public int GetArea() 
      { 
         return (width * height); 
      }
   }
   class RectangleTester 
   {
      static void Main(string[] args) 
      {
         Rectangle rectangle = new Rectangle();
         rectangle.SetWidth(5);
         rectangle.SetHeight(7);

         // Print the area of the object.
         Console.WriteLine("Total area: {0}",  rectangle.GetArea());
         Console.ReadKey();
      }
   }
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Total area: 35

Khởi tạo lớp cơ sở

Lớp dẫn xuất kế thừa các trường và các phương thức thành viên của lớp cơ sở. Do đó, đối tượng lớp cơ sở nên được tạo trước khi lớp dẫn xuất được tạo. Bạn có thể cung cấp chỉ thị khởi tạo lớp cơ sở trong phương thức khởi tạo của lớp dẫn xuất.

Chương trình sau đây chứng minh điều này:

using System;

namespace RectangleApplication 
{
   class Rectangle 
   {      
      protected double length;
      protected double width;
      
      public Rectangle(double l, double w)
      {
         length = l;
         width = w;
      }
      public double GetArea()
      {
         return length * width;
      }
      public void Display() 
      {
         Console.WriteLine("Length: {0}", length);
         Console.WriteLine("Width: {0}", width);
         Console.WriteLine("Area: {0}", GetArea());
      }
   }  
   class Tabletop : Rectangle 
   {
      public Tabletop(double l, double w) : base(l, w) 
      { }
      
      public double GetCost() 
      {
         return GetArea() * 70;
      }
      
      public void Display() 
      {
         base.Display();
         Console.WriteLine("Cost: {0}", GetCost());
      }
   }
   class ExecuteRectangle 
   {
      static void Main(string[] args) 
      {
         Tabletop tabletop = new Tabletop(4.5, 7.5);
         tabletop.Display();
         Console.ReadLine();
      }
   }
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Length: 4.5
Width: 7.5
Area: 33.75
Cost: 2362.5

Phạm vi kế thừa

Không phải tất cả các thành viên của lớp cơ sở đều được kế thừa bởi lớp dẫn xuất. Các thành viên sau đây không được kế thừa:

  • Static constructor - được sử dụng để khởi tạo giá trị cho các trường static.
  • Instance constructor - phương thức khởi tạo có đối số để khởi tạo giá trị cho các trường của lớp.
  • Destructor - phương thức hủy, được sử dụng để thực hiện bất kỳ hoạt động dọn dẹp cuối cùng cần thiết nào khi một thể hiện của lớp đang được bộ thu gom rác thu thập.

Những thành viên khác của lớp cơ sở đều được kế thừa bởi lớp dẫn xuất. Tuy nhiên lớp dẫn xuất có truy cập được hay không thì phụ thuộc vào chỉ thị truy cập của những thành viên này:

  • Các thành viên private của lớp cơ sở chỉ có thể truy cập được khi lớp dẫn xuất được lồng trong lớp cơ sở. Hãy xem ví dụ sau:
using System;

public class A 
{
   private int value = 10;

   public class B : A
   {
       public int GetValue()
       {
           return this.value;
       }     
   }
}

public class Example
{
    public static void Main(string[] args)
    {
        var b = new A.B();
        Console.WriteLine(b.GetValue());
    }
}

Kết quả khi biên dịch và thực thi chương trình như sau:

10
  • Các thành viên protected chỉ có thể truy cập trong lớp dẫn xuất.
  • Các thành viên internal có thể được truy cập nếu lớp dẫn xuất ở cùng assembly với lớp cơ sở.
  • Các thành viên public có thể được truy cập trong lớp dẫn xuất. Các thành viên được kế thừa này có thể được gọi như chúng được định nghĩa ở trong lớp dẫn xuất. Hãy xem ví dụ sau:
public class A
{
    public void Method1()
    {
        Console.WriteLine("This is method 1");
    }
}

public class B : A
{ }

public class Example
{
    public static void Main()
    {
        B b = new B();
        b.Method1();
    }
}

Kết quả khi biên dịch và chạy chương trình:

This is method 1

Đa kế thừa trong C#

C# không hỗ trợ đa kế thừa. Nhưng C# cho phép kế thừa một class và triển khai (implement) nhiều interface. Chương trình sau đây chứng minh điều này:

using System;

namespace InheritanceApplication 
{
   class Shape 
   {
      protected int width;
      protected int height;
      
      public void SetWidth(int w) 
      {
         width = w;
      }
      public void SetHeight(int h) 
      {
         height = h;
      }
   }

   public interface IPaintCost 
   {
      int GetPaintCost(int area);
   }

   public interface ICleanCost 
   {
      int GetCleanCost(int area);
   }
   
   // Derived class
   class Rectangle : Shape, IPaintCost, ICleanCost
   {
      public int GetArea() 
      {
         return (width * height);
      }
      public int GetPaintCost(int area) 
      {
         return area * 70;
      }
      public int GetCleanCost(int area) 
      {
         return area * 20;
      }
   }
   
   class RectangleTester 
   {
      static void Main(string[] args) 
      {
         Rectangle rectangle = new Rectangle();         
         rectangle.SetWidth(5);
         rectangle.SetHeight(7);
         
         int area = rectangle.GetArea();
         
         Console.WriteLine("Total area: {0}",  area);
         Console.WriteLine("Total paint cost: ${0}", rectangle.GetPaintCost(area));
         Console.WriteLine("Total clean cost: ${0}", rectangle.GetCleanCost(area));
         Console.ReadKey();
      }
   }
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Total area: 35
Total paint cost: $2450
Total clean cost: $700

Abstract Class trong C#

Sử dụng từ khóa abstract để khai báo abstract class trong C#. Abstract class có những đặc điểm sau:

  • Không thể tạo thể hiện cho abstract class.
  • Một abstract class có thể chứa các phương thức thông thường, phương thức ảo (virtual method) và phương thức trừu tượng (abstract method).
  • Không thể sử dụng từ khóa sealed cho abstract class vì nó ngăn không cho một lớp dẫn xuất kế thừa abstract class, trong khi đó abstract class cần phải được kế thừa vì không tạo thể hiện từ abstract class được.

Ví dụ về kế thừa abstract class trong C#:

abstract class BaseClass   // Abstract class
{
    protected int _x = 100;
    protected int _y = 150;
    public abstract void AbstractMethod();   // Abstract method
}

class DerivedClass : BaseClass
{
    public override void AbstractMethod()
    {
        _x++;
        _y++;
    }

    static void Main()
    {
        var obj = new DerivedClass();
        obj.AbstractMethod();
        Console.WriteLine($"x = {obj.X}, y = {obj.Y}");
    }
}

Kết quả khi biên dịch và thực thi chương trình:

x = 101, y = 151


Bài viết liên quan:

Hướng dẫn này sẽ giúp bạn tìm hiểu về đọc ghi file (File I/O) trong C# và sử dụng các lớp tiện ích để đọc ghi file.

Reflection trong C#

  • 6 min read

Reflection trong C# là gì? Ứng dụng của Reflection trong C#. Cách khai báo và sử dụng Reflection trong C#.

Attribute trong C#

  • 7 min read

Attribute trong C# là gì? Có những loại attribute nào trong C#? Làm sao để sử dụng attribute trong C#.