C# - C Sharp: Tạo và sử dụng generic

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Trong C# ta có thể áp dụng generic cho cả lớp, interface, phương thức và delegate. Phần này sẽ ta sẽ tìm hiểu cách tạo và sử dụng những generic này.

Lớp generic

Các lớp generic định nghĩa những chức năng mà có thể được sử dụng cho bất kỳ loại dữ liệu nào, chúng được khai báo bằng cách sử dụng từ khóa class và theo sau là một tên lớp, phía sau đó là cặp ngoặc nhọn và bên trong có chứa tham số kiểu. Khi khai báo lớp generic ta nên đưa ra giới hạn hay ràng buộc cho tham số kiểu bằng cách sử dụng từ khóa where. Lưu ý rằng đây chỉ là một tuỳ chọn, vì vậy, khi tạo loại lớp này bạn phải khái quát hoá các kiểu dữ liệu vào phần tham số kiểu và tuỳ chọn này sẽ quyết định ràng buộc để áp dụng trên tham số kiểu.

Cú pháp:

Bổ_từ_truy_cập class Tên_lớp<Danh_sách_tham_số_kiểu> [where Mệnh_đề_ràng_buộc_tham_số_kiểu]

, trong đó:

Bổ_từ_truy_cập: dùng để xác định tầm vực của lớp generic (đây là một tuỳ chọn).

Danh_sách_tham_số_kiểu: được dùng tạo nơi giữ chỗ cho kiểu dữ liệu thực sự sau này.

Mệnh_đề_ràng_buộc_tham_số_kiểu: đây là một tuỳ chọn, dùng để thiết lập ràng buộc tham số kiểu, có thể kiểu là class (không được là sealed class) hoặc interface áp dụng cho tham số kiểu cùng với từ khoá where (ví dụ như where T : EIDemo).

Ví dụ:

using System;

namespace Demo
{
  class ChungChung<T>
  {
    T[] giaTri;
    int _boDem = 0;
    public ChungChung(int max)
    {
      giaTri = new T[max];
    }
    public void Them(T val)
    {
      if (_boDem < giaTri.Length)
      {
        giaTri[_boDem] = val;
        _boDem++;
      }
    }
    public void HienThi()
    {
      Console.WriteLine("Kieu cua lop co cau truc la: " + typeof(T));
      Console.WriteLine("Cac gia tri luu trong doi tuong cua lop co cau truc la:");
      for (int i = 0; i < giaTri.Length; i++)
      {
        Console.WriteLine(giaTri[i]);
      }
    }
  }
  class SinhVien
  {
    static void Main(string[] args)
    {
      ChungChung<string> objCC1 = new ChungChung<string>(3);
      objCC1.Them("Phuong");
      objCC1.Them("Binh");
      objCC1.HienThi();
      ChungChung<int> objCC2 = new ChungChung<int>(2);
      objCC2.Them(325);
      objCC2.Them(57);
      objCC2.HienThi();
    }
  }
}

Phân tích ví dụ trên:

Trước tiên định nghĩa một lớp generic có tên ChungChung và có tham số kiểu là T; bên trong lớp này có một hàm tạo với một tham số kiểu int; phương thức Them() có một tham số cùng kiểu với lớp generic; phương thức Display() hiển thị kiểu giá trị được xác định thông qua tham số kiểu và các giá trị được cung cấp bởi người dùng thông qua các đối tượng; phương thức Main() của lớp SinhVien tạo một thể hiện có tên objGeneral của lớp ChungChung bằng cách cung cấp giá trị tham số kiểu là string và tổng số các giá trị được lưu trữ là 3; thể hiện objCC1 gọi phương thức Them() và truyền đi tên của các sinh viên và các tên sinh viên sẽ được hiển thị thông qua lời gọi phương thức HienThi(). Sau đó, thể hiện objCC2 được tạo với tham số kiểu là int và khởi tạo tổng số giá trị lưu trữ là 2. Đến đây bạn sẽ thấy một điều rằng ta không cần phải thay đổi mã lệnh để phù hợp với kiểu dữ liệu mới khi ta sử dụng lớp generic.

Lưu ý: Lớp generic có thể được lồng bên trong một lớp generic hoặc không generic khác, và bất kỳ lớp nào lồng trong một lớp generic thì bản thân nó được coi là lớp generic vì các tham số kiểu của lớp bên ngoài nó cũng áp dụng được cho nó.

Ràng buộc cho các tham số kiểu

Ta có thể sử dụng các ràng buộc cho kiểu tham số khi khai báo kiểu generic. Mỗi ràng buộc sẽ là một giới hạn áp đặt cho kiểu dữ liệu của tham số kiểu. Ràng buộc được tạo thông qua sử dụng từ khoá where và việc thiết lập ràng buộc cho tham số kiểu sẽ đảm bảo tính nhất quán và độ tin cậy của dữ liệu trong tập hợp.

Bảng dưới đây sẽ liệt kê các loại ràng buộc ta có thể được áp dụng cho tham số kiểu (giả sử T là tên của tham số kiểu):

Ràng buộc Mô tả
T : struct Tham số kiểu phải là một kiểu giá trị (value type) và không được null (ví dụ như kiểu string là không được)
T : class Tham số kiểu phải là một kiểu tham chiếu (reference type), ví dụ như class, interface, delegate, array
T : new() Tham số kiểu phải bao gồm một hàm tạo không tham số và có bổ từ truy cập là public
T : Tên_lớp Tham số kiểu phải là một lớp cơ sở hoặc dược thừa kế từ lớp cơ sở
T : Tên_interface Tham số kiểu phải là một interface hoặc được thừa kế từ interface
T: U Tham số kiểu được cung cấp cho T phải là hoặc xuất phát từ tham số được cung cấp cho U. Trong ngữ cảnh có thể null, nếu U là loại tham chiếu không thể null, thì T phải là loại tham chiếu không thể null. Nếu U là loại tham chiếu có thể null, thì T có thể là null hoặc không thể null

Ví dụ dưới đây sẽ tạo một lớp generic với ràng buộc tham số kiểu là một class:

using System;

namespace Demo
{
  class NhanVien1
  {
    string _tenNV;
    int _idNV;
    public NhanVien1(string ten, int id)
    {
      _tenNV = ten;
      _idNV = id;
    }
    public string Ten
    {
      get
      {
        return _tenNV;
      }
    }
    public int ID
    {
      get
      {
        return _idNV;
      }
    }
  }
  class Demo<T> where T : NhanVien1
    {
    T[] _ten = new T[3];
    int _boDem = 0;
  public void Them(T val)
    {
    _ten[_boDem] = val;
    _boDem++;
    }
  public void HienThi()
    {
    for (int i = 0; i < _boDem; i++)
    {
    Console.WriteLine(_ten[i].Ten + ", " + _ten[i].ID);
    }
    }
    }
  class TestDemo
  {
    static void Main(string[] args)
    {
      Demo<NhanVien1> objDemo = new Demo<NhanVien1>();
      objDemo.Them(new NhanVien1("Lan", 123));
      objDemo.Them(new NhanVien1("Long", 456));
      objDemo.Them(new NhanVien1("Minh", 789));
      objDemo.HienThi();
    }
  }
}

Trong ví dụ trên, lớp Demo được tạo với tham số kiểu là T và ràng buộc tham số kiểu cho T có kiểu class là lớp Employee; lớp Demo tạo một biến mảng kiểu T, tức là kiểu NhanVien; phương thức Them() của lớp Demo có một tham số cũng là kiểu T và nó sẽ nhận lại giá trị từ lời gọi phương thức Them() ở phương thức Main(), điều này có nghĩa tham số của phương thức Them()Main() phải có kiểu NhanVien và hàm tạo của Demo được gọi ngay trong tham số của phương thức Them() của Main(). Dưới đây là kết quả của ví dụ:

Lan, 123
Long, 456
Minh, 789

Chú ý: Khi ta sử dụng một kiểu cụ thể nào đó làm ràng buộc thì kiểu đó cần phải có mức truy cập cao hơn kiểu generic mã sẽ được áp dụng ràng buộc.

Thừa kế lớp generic

Lớp generic có thể được thừa kế giống như các lớp khác trong C#, điều này có nghĩa rằng lớp generic có thể đóng vai trò là lớp cơ sở hoặc lớp dẫn xuất.

Ta có quyền thừa kế các tham số kiểu từ lớp cha là lớp generic, điều này sẽ tránh được phải truyền đối số với kiểu dữ liệu cụ thể cho tham số. Lưu ý là nếu lớp dẫn xuất là lớp không generic thì trong lớp dẫn xuất ta phải cung cấp kiểu dữ liệu của tham số để thế vào tham số kiểu generic của lớp cơ sở, còn nếu lớp dẫn xuất là lớp generic thì ràng buộc áp đặt tại lớp cơ sở phải được đưa vào trong lớp generic dẫn xuất đó.

· Ví dụ khai báo một lớp generic thừa kế từ lớp generic khác:

// Generic thừ kế Generic
public class SinhVien<T>
{
}
public class DiemSo<T> : SinhVien<T>
{
}

· Ví dụ khai báo một lớp không phải generic thừa kế từ lớp generic:

// Non-Generic thừa kế Generic
public class Student<T>
{
}
public class Mark : Student<string>
{
}          

Phương thức generic

Chúng xử lý các dữ liệu trong đó kiểu của chúng chỉ được biết khi truy cập vào các biến chứa những dữ liệu đó, chúng được khai báo với các tham số kiểu nằm trong cặp ngoặc nhọn. Việc định nghĩa các phương thức như vậy sẽ giúp ta có thể truyền các kiểu dữ liệu khác nhau mỗi lần gọi. Ta có thể khai báo phương thức generic trong một lớp generic hoặc non-generic, và khi chúng được khai báo bên trong lớp generic thì phần thân của chúng sẽ tham chiếu được đến cả các tham số kiểu của chúng lẫn của lớp chứa chúng.

Phương thức generic có thể được khai báo cùng với những từ khoá sau:

virtual: Từ khoá này sẽ giúp phương thức được ghi đè trong lớp dẫn xuất.

override: Từ khoá này sẽ giúp phương thức ghi đè phương thức của lớp cơ sở.

abstract: Phương thức chứa từ khoá này chỉ có thể được khai báo mà không được định nghĩa (không có phần thân).

Cú pháp khai báo phương thức generic:

Bổ_từ_truy_cập Kiểu_trả_về Tên_phương_thức<Danh_sách_tham_số_kiểu>(Danh_sách_tham_số)

Ví dụ:

using System;

namespace Demo
{
  class HoanViSo
  {
    static void HoanVi<T>(ref T so1, ref T so2)
    {
      T tam = so1;
      so1 = so2;
      so2 = tam;
    }
    static void Main(string[] args)
    {
      int so1 = 67;
      int so2 = 89;
      Console.WriteLine("Cac gia tri truoc khi hoan vi, so1 = " + so1 + ", so2 = " + so2);
      HoanVi<int>(ref so1, ref so2);
      Console.WriteLine("Sau khi hoan vi, so1 = " + so1 + ", so2 = " + so2);
    }
  }
}

Giải thích ví dụ: Lớp HoanViSo có phương thức HoanVi() với tham số kiểu là T đặt trong cặp ngoặc nhọn và có hai đối số cũng có kiểu là T. Trong phương thức HoanVi() tạo một biến tên tam cũng có kiểu T và được gán giá trị của biến so1; phương thức Main() khai báo hai biến so1so2 có cùng kiểu int và khởi tạo giá trị so1 = 67so2 = 89, sau đó gọi đến phương thức HoanVi() và truyền đi kiểu int trong cặp ngoặc nhọn và hai tham chiếu của hai biến so1so2.

Interface generic

Loại interface này rất hữu ích đối với các lớp hoặc tập lớp generic trong việc thể hiện các mục trong tập hợp. Ta có thể sử dụng các lớp và interface generic để tránh các hoạt động boxingunboxing trên các kiểu dữ liệu. Các lớp generic có thể thực thi các interface generic bằng cách truyền các tham số cụ thể tới interface. Interface generic cũng có thể thừa kế.

Cú pháp khai báo interface generic:

interface Tên_interface <Danh_sách_tham_số_kiểu> [where Mệnh_đề ràng_buộc_tham_số_kiểu]
{
}

Ví dụ:

using System;

namespace Demo
{
  interface IToanHoc<T>
  {
    T Cong(T valOne, T valTwo);
    T Tru(T valOne, T valTwo);
  }
  
  class ConSo : IToanHoc<int>
  {
    public int Cong(int so1, int so2)
    {
      return so1 + so2;
    }
    public int Tru(int so1, int so2)
    {
      if (so1 > so2)
      {
        return (so1 - so2);
      }
      else
      {
        return (so2 - so1);
      }
    }
  
    static void Main(string[] args)
    {
      int so1 = 23;
      int so2 = 45;
      ConSo objCS = new ConSo();
      Console.Write("Cong {0} voi {1} duoc: ", so1, so2);
      Console.WriteLine(objCS.Cong(so1, so2));
      Console.Write("Tru {0} cho {1} duoc: ", so1, so2);
      Console.WriteLine(objCS.Tru(so1, so2));
    }
  }
}

Giải thích ví dụ: Trong interface generic IToanHoc với tham số kiểu là T khai báo hai phương thức Cong() và Tru(), mỗi phương thức cũng đều có hai tham số có kiểu là T. Lớp ConSo thực thi IToanHoc bằng cách cung cấp kiểu int trong cặp ngoặc nhọn và thực thi hai phương thức của interface này. Phương thức Main() tạo một thể hiện của lớp ConSo và gọi hai phương thức Cong()Tru() để thực hiện các phép tính cộng và trừ. Kết quả của ví dụ như sau:

Cong 23 voi 45 duoc: 68
Tru 23 voi 45 duoc: 22

Ràng buộc tham số kiểu là interface generic

Ta có thể định nghĩa interface như là một tham số kiểu và điều này sẽ cho phép ta sử dụng các thành phần của interface trong một lớp generic. Mặt khác thì điều này cũng đảm bảo rằng chỉ có kiểu là interface mới được sử dụng trong lớp. Một lớp có quyền có nhiều tham số kiểu là interface. Ví dụ:

using System;

namespace Demo
{
  interface IChiTiet
  {
    void ChiTiet();
  }
  
  class SinhVien : IChiTiet
  {
    string _ten;
    int _id;
    public SinhVien(string ten, int id)
    {
      _ten = ten;
      _id = id;
    }
    public void ChiTiet()
    {
      Console.WriteLine(_id + "\t" + _ten);
    }
  }
  
  class ThuNghiem<T> where T : IChiTiet
  {
    T[] _giaTri = new T[3];
    int _boDem = 0;
    public void Them(T val)
    {
      _giaTri[_boDem] = val;
      _boDem++;
    }
    public void HienThi()
    {
      for (int i = 0; i < 3; i++)
      {
        _giaTri[i].ChiTiet();
      }
    }
  }
  
  class Demo
  {
    static void Main(string[] args)
    {
      ThuNghiem<SinhVien> objTN = new ThuNghiem<SinhVien>();
      objTN.Them(new SinhVien("Xuan Truong", 123));
      objTN.Them(new SinhVien("Ngoc Anh", 456));
      objTN.Them(new SinhVien("Dinh Tung", 789));
      objTN.HienThi();
    }
  }
}

Giải thích ví dụ: Trong interface IChiTiet khai báo phương thức ChiTiet(). Lớp SinhVien thực thi interface IChiTiet. Lớp ThuNghiem được tạo với tham số kiểu là T với ràng buộc tham số kiểu là IChiTiet, điều này có nghĩa rằng tham số kiểu chỉ có thể là IChiTiet. Phương thức Main() tạo một thể hiện của ThuNghiem bằng cách truyền giá trị của tham số kiểu là SinhVien vì lớp này thực thi interface IChiTiet.

» Tiếp: Iterator
« Trước: Generic
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!