Kiểu dữ liệu Generics và Iterators trong C#
- 06-12-2023
- Toanngo92
- 0 Comments
Generics là cấu trúc dữ liệu cho phép bạn tái sử dụng cùng một đoạn mã cho các loại dữ liệu khác nhau như lớp, giao diện, và những loại khác. Generics thường được sử dụng một cách hiệu quả khi làm việc với mảng và các bộ sưu tập có trình duyệt enumerator. Iterators là các đoạn mã có khả năng lặp lại qua các giá trị của bộ sưu tập.
Chúng ta sẽ tìm hiểu về:
- Định nghĩa và mô tả về Generics
- Giải thích cách tạo và sử dụng Generics
- Giải thích về iterators
Mục lục
Generics
Generics là một dạng cấu trúc dữ liệu tham số hóa có thể làm việc với cả các loại giá trị và tham chiếu. Bạn có thể định nghĩa một lớp, giao diện, cấu trúc, phương thức hoặc một delegate như một kiểu generic trong C#.
Xem xét một chương trình C# sử dụng biến mảng kiểu Object
để lưu trữ một tập hợp tên sinh viên. Các tên được đọc từ bảng điều khiển dưới dạng kiểu giá trị và được boxed để có thể lưu trữ mỗi tên như kiểu Object
. Trong trường hợp này, trình biên dịch không thể xác minh dữ liệu được lưu trữ so với kiểu dữ liệu của nó vì nó cho phép bạn ép kiểu sang Object
. Nếu bạn nhập dữ liệu số, nó sẽ được chấp nhận mà không cần bất kỳ xác minh nào.
Để đảm bảo an toàn về kiểu dữ liệu, C# giới thiệu generics, có nhiều tính năng bao gồm khả năng định nghĩa các mẫu kiểu chung dựa trên đó kiểu có thể được tạo ra sau này.
Namespaces, Classes, and Interfaces cho Generics
Có một số namespace trong .NET Framework hỗ trợ việc tạo và sử dụng generics.
Namespace System.Collections.Object
Model cho phép bạn tạo các bộ sưu tập generic có tính động và chỉ đọc. Namespace System.Collections.Generic
bao gồm các lớp và giao diện cho phép bạn định nghĩa các bộ sưu tập generic tùy chỉnh.
- Các Lớp (Classes)
Namespace System.Collections.Generic
bao gồm các lớp cho phép bạn tạo các bộ sưu tập an toàn về kiểu dữ liệu. Bảng bên dưới liệt kê một số lớp phổ biến của namespace System.Collections.Generic
.
Lớp | Mô tả |
Comparer | Là một lớp trừu tượng cho phép bạn tạo một bộ sưu tập generic bằng cách thực hiện các chức năng của giao diện Comparer. |
Dictionary.KeyCollection | Chứa các khóa có mặt trong thể hiện của lớp Dictionary. |
Dictionary.ValueCollection | Chứa các giá trị có mặt trong thể hiện của lớp Dictionary. |
EqualityComparer | Là một lớp trừu tượng cho phép bạn tạo một bộ sưu tập generic bằng cách thực hiện các chức năng của giao diện EqualityComparer. |
- Giao diện (Interfaces)
Namespace System.Collections.Generic
bao gồm các giao diện cho phép bạn tạo ra các bộ sưu tập an toàn về kiểu dữ liệu.
Bảng bên dưới liệt kê danh sách các giao diện của Namespace System.Collections.Generic
Giao diện | Mô tả |
IComparer | Định nghĩa phương thức generic Compare() so sánh các giá trị trong bộ sưu tập |
IEnumerable | Định nghĩa phương thức generic GetEnumerator() duyệt qua một bộ sưu tập |
IEqualityComparer | Bao gồm các phương thức kiểm tra sự bằng nhau giữa hai đối tượng |
Namespace System.Collections.ObjectModel
Namespace System.Collections.ObjectModel
bao gồm các lớp được sử dụng để tạo ra các bộ sưu tập generic tùy chỉnh.
Bảng bên dưới hiển thị các lớp được chứa trong Namespace System.Collections.ObjectModel
Lớp | Mô tả |
Collection<T> | Cung cấp lớp cơ sở cho các bộ sưu tập generic |
Dictionary<TKey, TValue> | Cung cấp một lớp trừu tượng cho một bộ sưu tập trong đó các khóa liên kết với các giá trị |
ReadOnlyCollection<T> | Là một lớp cơ sở generic chỉ đọc ngăn chặn việc sửa đổi bộ sưu tập |
Đoạn mã bên dưới minh họa việc sử dụng lớp ReadOnlyCollection
.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
class ReadOnly
{
static void Main(string[] args)
{
List<string> objList = new List<string>();
objList.Add("Francis");
objList.Add("James");
objList.Add("Baptista");
objList.Add("Michael");
ReadOnlyCollection<string> objReadOnly = new ReadOnlyCollection<string>(objList);
Console.WriteLine("Values stored in the readonly collection");
foreach (string str in objReadOnly)
{
Console.WriteLine(str);
}
Console.WriteLine();
Console.WriteLine("Total number of elements in the readonly collection: " + objReadOnly.Count);
if (objList.Contains("Francis"))
{
objList.Insert(2, "Peter");
Console.WriteLine("\nValues stored in the list after modification");
foreach (string str in objReadOnly)
{
Console.WriteLine(str);
}
string[] array = new string[objReadOnly.Count * 2];
objReadOnly.CopyTo(array, 5);
Console.WriteLine("\nTotal number of values that can be stored in the new array: " + array.Length);
Console.WriteLine("Values in the new array");
foreach (string str in array)
{
if (str == null)
{
Console.WriteLine("null");
}
else
{
Console.WriteLine(str);
}
}
}
}
}
Trong đoạn mã trên phương thức Main()
của lớp ReadOnly
tạo một thể hiện của lớp List
. Phương thức Add()
này chèn các phần tử vào thể hiện của lớp List
.
Một thể hiện của lớp ReadOnlyCollection
với kiểu string
được tạo ra và các phần tử lưu trữ trong thể hiện của lớp List
được sao chép sang thể hiện của lớp ReadOnlyCollection
. Phương thức Contains()
kiểm tra xem lớp List
có chứa phần tử cụ thể không. Nếu lớp List
chứa phần tử cụ thể, ví dụ ‘Francis’, thì phần tử mới ‘Peter’ sẽ được chèn vào vị trí chỉ định, là vị trí thứ 2. Mã tạo một biến mảng có kích thước gấp đôi của lớp ReadOnlyCollection
. Phương thức CopyTo()
sao chép các phần tử từ lớp ReadOnlyCollection
vào biến mảng từ vị trí thứ năm trở đi.
Hình bên dưới hiển thị kết quả đầu ra của đoạn mã bên trên.
Tạo Ra Các Kiểu Generic
Một khai báo generic luôn chấp nhận một tham số kiểu, là một chỗ trỏ cho kiểu dữ liệu cần thiết. Kiểu được chỉ định chỉ khi một kiểu generic được tham chiếu hoặc được xây dựng như một kiểu trong chương trình.
Quá trình tạo ra một kiểu generic bắt đầu với một định nghĩa kiểu generic chứa các tham số kiểu. Định nghĩa hoạt động như một bản thiết kế. Sau đó, một kiểu generic được xây dựng từ định nghĩa bằng cách chỉ định các kiểu thực tế như các đối số kiểu generic, sẽ thay thế cho các tham số kiểu hoặc những chỗ trỏ.
Các Lợi Ích
Generics đảm bảo an toàn kiểu tại thời điểm biên dịch. Generics cho phép bạn tái sử dụng mã một cách an toàn mà không cần ép kiểu hoặc boxing. Một định nghĩa kiểu generic có thể tái sử dụng với các loại khác nhau nhưng chỉ có thể chấp nhận các giá trị của một kiểu duy nhất vào một thời điểm. Ngoài khả năng tái sử dụng, còn có nhiều lợi ích khác khi sử dụng generics.
Các lợi ích đó là:
- Cải thiện hiệu suất vì sử dụng bộ nhớ thấp do không cần thực hiện các thao tác ép kiểu (casting) hoặc đóng gói (boxing) khi tạo ra một kiểu generic.
- Đảm bảo mô hình lập trình kiểu mạnh (strongly-typed programming).
- Giảm thiểu các lỗi thời gian chạy có thể xảy ra do việc ép kiểu (casting) hoặc đóng gói (boxing).
Tạo và Sử Dụng Generics
Các kiểu generic không chỉ hạn chế trong các lớp mà còn có thể bao gồm cả các giao diện và delegates. Phần này xem xét về các lớp, phương thức, giao diện, delegates generic, và các khái niệm tương tự.
Các Lớp Generic
Các lớp generic xác định các chức năng có thể được sử dụng cho bất kỳ loại dữ liệu nào. Các lớp generic được khai báo với một lệnh khai báo lớp theo sau bởi một tham số kiểu parameter
được bao quanh bởi dấu ngoặc nhọn. Trong quá trình khai báo một lớp generic, bạn có thể áp dụng một số hạn chế hoặc ràng buộc cho các tham số kiểu bằng cách sử dụng từ khóa where. Tuy nhiên, việc áp dụng ràng buộc cho các tham số kiểu là tùy chọn.
Do đó, khi tạo ra một lớp generic, bạn cần tổng quát hóa các loại dữ liệu thành tham số kiểu và tùy chọn xác định ràng buộc áp dụng cho tham số kiểu.
Dưới đây là cú pháp được sử dụng để tạo ra một lớp generic:
<access_modifier> class <ClassName> <<type_parameter_list>> [where <type_parameter_constraint_clause>]
trong đó,
access_modifier
: Xác định phạm vi của lớp generic. Đây là tùy chọn.ClassName
: Là tên của lớp generic mới sẽ được tạo ra.- <
type_parameter
>: Được sử dụng như một chỗ trỏ cho kiểu dữ liệu thực tế. type_parameter_constraint_clause
: Một lớp hoặc giao diện tùy chọn được áp dụng cho tham số kiểu bằng từ khóa where.
Đoạn mã bên dưới tạo ra một lớp generic có thể được sử dụng cho bất kỳ loại dữ liệu cụ thể nào.
using System;
using System.Collections.Generic;
class General<T>
{
T[] values;
int _counter = 0;
public General(int max)
{
values = new T[max];
}
public void Add(Tval)
{
if (_counter < values.Length)
{
values[_counter] = val;
_counter++;
}
}
public void Display()
{
Console.WriteLine("Constructed Class of type: " + typeof(T));
Console.WriteLine("Values stored in the object of constructed class are: ");
for (int i = 0; i < values.Length; i++)
{
Console.WriteLine(values[i]);
}
}
}
class Students
{
static void Main(string[] args)
{
General<string> objGeneral = new General<string>(3);
objGeneral.Add("John");
objGeneral.Add("Patrick");
objGeneral.Display();
General<int> objGeneral2 = new General<int>(2);
objGeneral2.Add(200);
objGeneral2.Add(35);
objGeneral2.Display();
}
}
Trong đoạn mã trên, một định nghĩa lớp generic cho General được tạo ra, chấp nhận một tham số kiểu T.
Lớp generic khai báo một hàm tạo có tham số với giá trị int
. Phương thức Add()
nhận tham số cùng loại với lớp generic. Phương thức Display()
hiển thị kiểu giá trị được chỉ định bởi kiểu tham số và các giá trị do người dùng cung cấp thông qua đối tượng. Phương thức Main()
của lớp Students
tạo một thể hiện của lớp generic General
bằng cách cung cấp giá trị tham số kiểu string
và giá trị total
cần lưu trữ là 3. Thể hiện này gọi phương thức Add()
chứa giá trị là tên của sinh viên. Các tên này được hiển thị bằng cách gọi phương thức Display()
. Sau đó, một đối tượng khác được tạo ra với một loại dữ liệu khác, int
, dựa trên cùng một định nghĩa lớp. Định nghĩa lớp là generic, chúng ta không cần phải thay đổi mã hiện tại, mà có thể tái sử dụng cùng một mã cho một loại dữ liệu int
. Do đó, sử dụng cùng một định nghĩa lớp generic, chúng ta có thể tạo ra hai danh sách dữ liệu khác nhau.
Kết quả:
Constructed Class is of type: System String
Values stored in the object of constructed class are:
John
Patrick
Constructed Class is of type: System.Int32
Values stored in the object of constructed class are:
200
35
Ghi chú: Các lớp generic có thể được lồng trong các lớp generic khác hoặc không. Tuy nhiên, bất kỳ lớp nào lồng trong một lớp generic đều là một lớp generic vì các kiểu tham số của lớp bên ngoài được cung cấp cho lớp lồng.
Ràng buộc trên kiểu tham số
Bạn có thể áp dụng ràng buộc trên kiểu tham số trong quá trình khai báo một kiểu generic. Ràng buộc là một hạn chế được áp dụng lên kiểu dữ liệu của kiểu tham số. Ràng buộc được chỉ định bằng từ khóa where. Chúng được sử dụng khi người lập trình muốn hạn chế các loại dữ liệu của kiểu tham số để đảm bảo tính nhất quán và đáng tin cậy của dữ liệu trong một bộ sưu tập.
Bảng bên dưới liệt kê các loại ràng buộc có thể áp dụng cho kiểu tham số.
Ràng buộc | Mô tả |
T: struct | Xác định rằng tham số kiểu phải là kiểu giá trị, trừ giá trị null |
T: class | Xác định rằng tham số kiểu phải là kiểu tham chiếu như một lớp, giao diện hoặc delegate |
T: new() | Xác định rằng tham số kiểu phải có một constructor mà không có bất kỳ tham số nào và có thể được gọi công khai |
T: <base class name> | Xác định rằng tham số kiểu phải là lớp cha hoặc phải kế thừa từ một lớp cha |
T: <interface name> | Xác định rằng tham số kiểu phải là một giao diện hoặc phải kế thừa từ một giao diện |
Đoạn mã bên dưới tạo ra một lớp generic sử dụng ràng buộc lớp.
using System;
using System.Collections.Generic;
class Employee
{
string _empName;
int _empID;
public Employee(string name, int num)
{
_empName = name;
_empID = num;
}
public string Name
{
get { return _empName; }
}
public int ID
{
get { return _empID; }
}
}
class GenericList<T> where T : Employee
{
T[] _name = new T[3];
int _counter = 0;
public void Add(Tval)
{
if (_counter < _name.Length)
{
_name[_counter] = val;
_counter++;
}
}
public void Display()
{
for (int i = 0; i < _counter; i++)
{
Console.WriteLine(_name[i].Name + ", " + _name[i].ID);
}
}
}
class ClassConstraintDemo
{
static void Main(string[] args)
{
GenericList<Employee> objList = new GenericList<Employee>();
objList.Add(new Employee("John", 100));
objList.Add(new Employee("James", 200));
objList.Add(new Employee("Patrick", 300));
objList.Display();
}
}
Trong đoạn mã trên, lớp GenericList được tạo ra chấp nhận một tham số kiểu T.
Kiểu tham số này được áp dụng với ràng buộc lớp, có nghĩa là kiểu tham số có thể chỉ bao gồm chi tiết của kiểu Employee. Lớp generic tạo một biến mảng với tham số kiểu T
, điều này có nghĩa là nó có thể chứa các giá trị kiểu Employee. Phương thức Add()
bao gồm tham số ‘val’, sẽ chứa các giá trị được thiết lập trong phương thức Main()
. Vì tham số kiểu phải thuộc loại Employee, hàm khởi tạo được gọi khi thiết lập các giá trị trong phương thức Main()
.
Kết quả:
John, 100
James, 200
Patrich, 300
Ghi chú: Khi bạn sử dụng một loại cụ thể như một ràng buộc, loại được sử dụng như một ràng buộc phải có phạm vi truy cập lớn hơn so với kiểu generic sẽ sử dụng ràng buộc đó.
Kế thừa từ các Lớp Generic
Một lớp generic có thể được kế thừa như bất kỳ lớp không generic nào khác trong C#. Do đó, một lớp generic có thể hoạt động như một lớp cơ sở hoặc một lớp dẫn xuất.
Khi kế thừa một lớp generic trong một lớp generic khác, bạn có thể sử dụng tham số kiểu generic của lớp cơ sở thay vì truyền kiểu dữ liệu của tham số. Tuy nhiên, khi kế thừa một lớp generic trong một lớp không generic, bạn phải cung cấp kiểu dữ liệu của tham số thay vì tham số kiểu generic của lớp cơ sở. Các ràng buộc được áp đặt ở cấp độ lớp cơ sở phải được bao gồm trong lớp generic dẫn xuất.
Hình bên dưới hiển thị một lớp generic như là một lớp cơ sở.
Cú pháp dưới đây được sử dụng để kế thừa một lớp generic từ một lớp generic hiện có:
<access_modifier>class<<BaseClass><<generic type parameter>>{}
<access_modifier>class<DerivedClass>:<BaseClass><<generic type parameter>>{}
trong đó:
access_modifier
: Xác định phạm vi của lớp generic.BaseClass
: Là lớp cơ sở generic.<generic_type_parameter>
: Là một placeholder cho kiểu dữ liệu được chỉ định.DerivedClass
: Là lớp generic dẫn xuất.
Cú pháp dưới đây được dùng để Kế thừa một lớp không generic từ một lớp generic
<access_modifier> class <BaseClass> <<generic_type_parameter>> {}
<access_modifier> class <DerivedClass> : <BaseClass><<type_parameter_value>> {}
trong đó,
<type_parameter_value>
: Có thể là một kiểu dữ liệu như int
, string
, hoặc float
.
Phương thức Generic
Các phương thức generic xử lý các giá trị có kiểu dữ liệu chỉ được biết khi truy cập các biến lưu trữ các giá trị này. Một phương thức generic được khai báo với danh sách tham số kiểu generic được bao quanh bởi dấu ngoặc đơn. Việc định nghĩa phương thức với tham số kiểu cho phép bạn gọi phương thức với một kiểu khác nhau mỗi lần. Bạn có thể khai báo một phương thức generic trong các khai báo lớp generic hoặc không generic. Khi bạn khai báo một phương thức generic trong một khai báo lớp generic, thân của phương thức tham chiếu đến các tham số kiểu của cả phương thức và khai báo lớp.
Các từ khóa có thể được sử dụng khi khai báo các phương thức generic:
virtual
: Các phương thức generic khai báo với từ khóa virtual có thể bị ghi đè trong lớp dẫn xuất.override
: Phương thức generic được khai báo với từ khóa override ghi đè phương thức của lớp cơ sở.abstract
: Phương thức generic được khai báo với từ khóa abstract chỉ chứa phần khai báo của phương thức, thường được thực hiện trong một lớp dẫn xuất.
Cú pháp sử dụng cho việc khai báo một phương thức generic:
<access_modifier> <return_type> <Method_name> <<type_parameter_list>>()
trong đó,
access_modifier
: Xác định phạm vi của phương thức.return_type
: Xác định kiểu dữ liệu mà phương thức generic sẽ trả về.Method_name
: Tên của phương thức generic.<type_parameter_list>
: Là một placeholder cho kiểu dữ liệu thực tế.
Đoạn mã bên dưới tạo ra một phương thức generic trong một lớp không generic.
using System;
using System.Collections.Generic;
class SwapNumbers {
static void Swap<T>(ref T valOne, ref T valTwo) {
T temp = valOne;
valOne = valTwo;
valTwo = temp;
}
static void Main(string[] args) {
int numOne = 23;
int numTwo = 45;
Console.WriteLine("Values before swapping: " + numOne + " & " + numTwo);
Swap<int>(ref numOne, ref numTwo);
Console.WriteLine("Values after swapping: " + numOne + " & " + numTwo);
}
}
Trong đoạn mã trên, lớp SwapNumbers
bao gồm một phương thức generic Swap()
nhận một tham số kiểu T trong dấu ngoặc kép và hai tham số trong ngoặc đơn của kiểu T. Phương thức Swap()
tạo ra một biến temp
kiểu T được gán giá trị từ biến valOne
. Phương thức Main()
hiển thị các giá trị được khởi tạo trong các biến và gọi phương thức Swap()
bằng cách cung cấp kiểu int trong dấu ngoặc kép. Điều này sẽ thay thế cho tham số kiểu trong định nghĩa phương thức generic và hiển thị các giá trị được hoán đổi trong các biến.
Kết quả:
Values before swapping: 23 & 45
Values after swapping: 45 & 23
Giao diện Generic
Các giao diện generic hữu ích cho các bộ sưu tập generic hoặc các lớp generic đại diện cho các mục trong bộ sưu tập. Bạn có thể sử dụng các lớp generic với các giao diện generic để tránh các hoạt động boxing và unboxing trên các kiểu dữ liệu giá trị.
Các lớp generic có thể thực hiện các giao diện generic bằng cách truyền các tham số cần thiết được chỉ định trong giao diện. Tương tự như các lớp generic, các giao diện generic cũng triển khai tính kế thừa.
Cú pháp để khai báo một giao diện tương tự như cú pháp cho việc khai báo lớp. Dưới đây là cú pháp được sử dụng để tạo ra một giao diện generic:
<access_modifier> interface <InterfaceName> <<type_parameter_list>> [where <type_parameter_constraint_clause>]
trong đó,
access_modifier
: Xác định phạm vi của giao diện generic.InterfaceName
: Là tên của giao diện generic mới.<type_parameter_list>
: Được sử dụng như một placeholder cho kiểu dữ liệu thực tế.type_parameter_constraint_clause
: Một lớp tùy chọn hoặc một giao diện được áp dụng cho tham số kiểu với từ khóawhere
.
Đoạn mã bên dưới tạo ra một giao diện generic được thực hiện bởi lớp không generic.
using System;
using System.Collections.Generic;
interface IMaths<T>
{
T Addition(T valOne, T valTwo);
T Subtraction(T valOne, T valTwo);
}
class Numbers : IMaths<int>
{
public int Addition(int valOne, int valTwo)
{
return valOne + valTwo;
}
public int Subtraction(int valOne, int valTwo)
{
if (valOne > valTwo)
{
return valOne - valTwo;
}
else
{
return valTwo - valOne;
}
}
}
class Program
{
static void Main(string[] args)
{
int numOne = 23;
int numTwo = 45;
Numbers objInterface = new Numbers();
Console.Write("Addition of two integer values is: ");
Console.WriteLine(objInterface.Addition(numOne, numTwo));
Console.Write("Subtraction of two integer values is: ");
Console.WriteLine(objInterface.Subtraction(numOne, numTwo));
}
}
Trong đoạn mã trên, giao diện generic IMaths lấy một tham số kiểu T
và khai báo hai phương thức kiểu T
. Lớp Numbers thực hiện giao diện IMaths bằng cách cung cấp kiểu int
trong dấu ngoặc nhọn và thực hiện hai phương thức được khai báo trong giao diện generic. Phương thức Main()
tạo một thể hiện của lớp Numbers và hiển thị phép cộng và phép trừ của hai số.
Kết quả:
Addition of two integer values is: 68
Subtraction of two integer values is: 22
Ràng buộc Giao diện Generic
Bạn có thể chỉ định một giao diện làm ràng buộc trên một tham số kiểu. Điều này cho phép bạn sử dụng các thành viên của giao diện trong lớp generic. Ngoài ra, nó đảm bảo rằng chỉ có các loại thực hiện giao diện được sử dụng.
Bạn cũng có thể chỉ định nhiều giao diện làm ràng buộc trên một tham số kiểu duy nhất. Đoạn mã bên dưới tạo một giao diện generic được sử dụng làm ràng buộc trong một lớp generic.
using System;
using System.Collections.Generic;
interface IDetails
{
void GetDetails();
}
class Student : IDetails
{
string _studName;
int _studID;
public Student(string name, int num)
{
_studName = name;
_studID = num;
}
public void GetDetails()
{
Console.WriteLine(_studID + "\t" + _studName);
}
}
class GenericList<T> where T : IDetails
{
T[] _values = new T[3];
int _counter = 0;
public void Add(T val)
{
if (_counter < 3)
{
_values[_counter] = val;
_counter++;
}
}
public void Display()
{
for (int i = 0; i < 3; i++)
{
_values[i].GetDetails();
}
}
}
class InterfaceConstraintDemo
{
static void Main(string[] args)
{
GenericList<Student> objList = new GenericList<Student>();
objList.Add(new Student("Wilson", 120));
objList.Add(new Student("Jack", 130));
objList.Add(new Student("Peter", 140));
objList.Display();
}
}
Trong đoạn mã trên, một giao diện IDetails
khai báo một phương thức GetDetails()
. Lớp Student thực hiện giao diện IDetails
. Lớp GenericList được tạo ra, nhận một tham số kiểu T
. Tham số kiểu này được áp dụng ràng buộc của giao diện, có nghĩa là tham số kiểu chỉ có thể bao gồm chi tiết của loại IDetails
. Phương thức Main()
tạo một thể hiện của lớp GenericList bằng cách truyền giá trị tham số kiểu là Student
, vì lớp Student
thực hiện giao diện IDetails
.
Hình bên dưới tạo một giao diện generic.
Generic Delegates trong C#
Delegate là các kiểu tham chiếu gói một tham chiếu tới một phương thức có chữ ký và kiểu trả về. Tương tự như các lớp, giao diện và cấu trúc, phương thức người dùng tự định nghĩa và delegate cũng có thể được khai báo như là generic. Một generic delegate có thể được sử dụng để tham chiếu đến nhiều phương thức trong một lớp với các loại tham số khác nhau. Tuy nhiên, số lượng tham số của delegate và các phương thức tham chiếu phải là giống nhau. Cú pháp để khai báo một generic delegate tương tự như khi khai báo một generic method, nơi danh sách type parameter được chỉ định sau tên của delegate.
Dưới đây là cú pháp được sử dụng để khai báo một generic delegate:
delegate <return_type> <DelegateName> <type parameter list> (<argument_list>);
trong đó,
return_type
: Xác định kiểu giá trị mà delegate sẽ trả về.DelegateName
: Là tên của generic delegate.type parameter list
: Được sử dụng như một chỗ đặt dấu giữ chỗ cho kiểu dữ liệu thực tế.argument_list
: Xác định danh sách tham số trong delegate.
Đoạn mã bên dưới đây khai báo một generic delegate.
using System;
delegate T DelMath<T>(T val);
class Numbers
{
static int NumberType(int num)
{
if (num % 2 == 0)
return 1;
else
return 0;
}
static float NumberType(float num)
{
return num % 2.5F;
}
public static void Main(string[] args)
{
DelMath<int> objDel = NumberType;
DelMath<float> objDel2 = NumberType;
Console.WriteLine(objDel(10));
Console.WriteLine(objDel2(108.756F));
}
}
Trong đoạn mã trên, một generic delegate được khai báo trong lớp Numbers. Trong phương thức Main()
của lớp, một đối tượng của delegate được tạo ra, đang tham chiếu đến phương thức NumberType() và nhận tham số kiểu int
. Một giá trị số nguyên được truyền vào phương thức, chỉ hiển thị giá trị nếu nó là số chẵn. Một đối tượng khác của delegate được tạo trong phương thức Main()
, đang tham chiếu đến phương thức NumberType() và nhận tham số kiểu Float
. Một giá trị Float
được truyền vào phương thức, hiển thị phần dư của phép chia. Do đó, generic delegate có thể được sử dụng cho các phương thức quá tải.
Hình bên dưới khai báo một generic delegate.
Nạp chồng Phương thức sử dụng kiểu Tham số
Các phương thức của một lớp generic sử dụng tham số kiểu generic có thể được nạp chồng. Người lập trình có thể nạp chồng các phương thức sử dụng tham số kiểu bằng cách thay đổi kiểu hoặc số lượng tham số. Tuy nhiên, sự khác biệt về kiểu không dựa trên tham số kiểu generic, mà dựa trên kiểu dữ liệu của tham số được truyền.
Đoạn mã bên dưới thể hiện cách nạp chồng các phương thức sử dụng kiểu tham số.
using System;
using System.Collections;
using System.Collections.Generic;
class General<T, U>
{
T _valOne;
U _valTwo;
public void AcceptValues(T item)
{
_valOne = item;
}
public void AcceptValues(U item)
{
_valTwo = item;
}
public void Display()
{
Console.Write(_valOne + "\t" + _valTwo);
}
}
class MethodOverload
{
static void Main(string[] args)
{
General<int, string> objGenOne = new General<int, string>();
objGenOne.AcceptValues(10);
objGenOne.AcceptValues("Smith");
Console.WriteLine("ID\tName\tDesignation\tSalary");
objGenOne.Display();
General<string, float> objGenTwo = new General<string, float>();
objGenTwo.AcceptValues("Mechanic");
objGenTwo.AcceptValues(2500);
Console.Write("\t");
objGenTwo.Display();
Console.WriteLine();
}
}
Trong đoạn mã trên, lớp General có hai phương thức nạp chồng với các tham số kiểu khác nhau. Trong phương thức Main()
, một thể hiện của lớp General được tạo ra. Lớp được khởi tạo bằng cách xác định kiểu dữ liệu cho các tham số generic T
và U
là string
và int
tương ứng. Các phương thức nạp chồng được gọi bằng cách xác định các giá trị phù hợp. Các phương thức này lưu các giá trị này vào các biến tương ứng được định nghĩa trong lớp General. Các giá trị này chỉ ra ID và tên của nhân viên.
Một thể hiện khác của lớp General được tạo ra, xác định kiểu dữ liệu mà lớp có thể chứa là string
và float
. Các phương thức nạp chồng được gọi bằng cách xác định các giá trị phù hợp. Các phương thức này lưu các giá trị này vào các biến tương ứng được định nghĩa trong lớp General. Các giá trị này chỉ ra chức vụ và mức lương của nhân viên.
Hình bên dưới hiển thị đầu ra của việc sử dụng các phương thức nạp chồng với các tham số kiểu.
Ghi đè Phương thức Ảo trong Lớp Generic
Các phương thức trong các lớp generic có thể được ghi đè giống như trong bất kỳ lớp không generic nào. Để ghi đè một phương thức trong lớp generic, phương thức trong lớp cơ sở phải được khai báo là virtual
, và phương thức này có thể được ghi đè trong lớp dẫn xuất bằng cách sử dụng từ khóa override
.
Đoạn mã bên dưới thể hiện cách ghi đè các phương thức ảo của một lớp generic.
using System;
using System.Collections;
using System.Collections.Generic;
class GeneralList<T>
{
protected T ItemOne;
public GeneralList(T valOne)
{
ItemOne = valOne;
}
public virtual T GetValue()
{
return ItemOne;
}
}
class Student<T> : GeneralList<T>
{
public T Value;
public Student(T valOne, T valTwo) : base(valOne)
{
Value = valTwo;
}
public override T GetValue()
{
Console.Write(base.GetValue() + "\t\t");
return Value;
}
}
class StudentList
{
public static void Main()
{
Student<string> objStudent = new Student<string>("Patrick", "Male");
Console.WriteLine("Name\t\tSex");
Console.WriteLine(objStudent.GetValue());
}
}
Trong đoạn mã trên, lớp GeneralList
bao gồm một constructor gán tên của sinh viên. Phương thức GetValue()
của lớp GeneralList
được ghi đè trong lớp Student
. Constructor của lớp Student
gọi constructor của lớp cơ sở bằng từ khóa base và gán giới tính của sinh viên được chỉ định. Phương thức GetValue()
của lớp dẫn xuất trả về giới tính của sinh viên. Tên của sinh viên được lấy bằng cách sử dụng từ khóa base
để gọi phương thức GetValue()
của lớp cơ sở. Lớp StudentList
tạo một thể hiện của lớp Student
. Thể hiện này gọi phương thức GetValue()
của lớp dẫn xuất, sau đó gọi phương thức GetValue()
của lớp cơ sở bằng cách sử dụng từ khóa base
.
Hình bên dưới hiển thị đầu ra khi sử dụng các phương thức ảo được ghi đè trong lớp generic.
Iterators trong C#
Hãy tưởng tượng một tình huống trong đó một người đang cố gắng ghi nhớ một cuốn sách gồm 100 trang. Để hoàn thành nhiệm vụ, người đó phải lặp lại từng trang trong số 100 trang đó.
Tương tự như người đó lặp lại từng trang, ở trong ngôn ngữ lập trình C#, iterator được sử dụng để duyệt qua một danh sách giá trị hoặc một bộ sưu tập. Đó là một đoạn mã sử dụng vòng lặp Foreach
để tham chiếu đến một bộ sưu tập các giá trị theo thứ tự tuần tự. Ví dụ, hãy xem xét một tập hợp các giá trị cần được sắp xếp. Để triển khai logic một cách thủ công, lập trình viên có thể lặp qua từng giá trị theo thứ tự bằng iterators để so sánh chúng.
Hình bên dưới minh họa những tương tự này.
Một iterator không phải là thành viên dữ liệu mà là một cách để truy cập các thành viên. Nó có thể là một phương thức, một truy cập được nhận hoặc một toán tử cho phép bạn duyệt qua các giá trị trong một bộ sưu tập. Iterator chỉ định cách giá trị được tạo ra khi câu lệnh foreach
truy cập các phần tử trong một bộ sưu tập. Chúng theo dõi các phần tử trong bộ sưu tập, để bạn có thể truy xuất các giá trị này nếu cần thiết.
Ví dụ, hãy xem xét một mảng có sáu phần tử, nơi iterator có thể trả về tất cả các phần tử trong mảng một cách tuần tự.
Lợi ích
Đối với một lớp hoạt động giống như một bộ sưu tập, việc sử dụng iterators để duyệt qua các giá trị của bộ sưu tập với câu lệnh foreach
là lựa chọn tốt. Bằng cách làm điều này, bạn có thể nhận được các lợi ích sau:
- Iterators cung cấp một cách duyệt qua các giá trị của bộ sưu tập đơn giản và nhanh chóng.
- Iterators giảm độ phức tạp của việc cung cấp một bộ liệt kê cho một bộ sưu tập.
- Iterators có thể trả về một lượng lớn giá trị.
- Iterators có thể được sử dụng để đánh giá và trả về chỉ các giá trị cần thiết.
- Iterators có thể trả về giá trị mà không tiêu tốn bộ nhớ bằng cách tham chiếu từng giá trị trong danh sách.
Thực hiện
Iterator có thể được tạo ra bằng cách triển khai phương thức GetEnumerator()
trả về một tham chiếu của giao diện IEnumerator
. Khối iterator sử dụng từ khóa yield
để cung cấp các giá trị cho thể hiện của enumerator hoặc kết thúc quá trình lặp. Lệnh yield
return trả về các giá trị, trong khi lệnh yield
break kết thúc quá trình lặp. Khi điều khiển chương trình đến lệnh yield
return, vị trí hiện tại được lưu trữ và lần gọi iterator tiếp theo, thực thi bắt đầu từ vị trí đã lưu.
Đoạn mã bên dưới mô tả việc sử dụng iterators để duyệt qua các giá trị của một bộ sưu tập.
using System;
using System.Collections;
class Department : IEnumerable
{
string[] departmentNames = { "Marketing", "Finance", "Information Technology", "Human Resources" };
public IEnumerator GetEnumerator()
{
for (int i = 0; i < departmentNames.Length; i++)
{
yield return departmentNames[i];
}
}
static void Main(string[] args)
{
Department objDepartment = new Department();
Console.WriteLine("Department Names");
Console.WriteLine();
foreach (string str in objDepartment)
{
Console.WriteLine(str);
}
}
}
Trong đoạn mã trên, lớp Department triển khai giao diện IEnunerable
. Lớp Department bao gồm một biến mảng lưu trữ các tên phòng ban và một phương thức GetEnunerator()
, chứa vòng lặp for
. Vòng lặp for
trả về các tên phòng ban tại mỗi vị trí chỉ mục trong biến mảng. Khối mã này trong phương thức GetEnumcrator()
bao gồm iterator trong ví dụ này. Phương thức Main()
tạo một thể hiện của lớp Department và chứa một vòng lặp foreach
hiển thị các tên phòng ban.
Hình bên dưới mô tả việc sử dụng iterators.
Ghi chú: Khi trình biên dịch C# gặp một iterator, nó mặc định gọi các phương thức Current
, MoveNext
, và Dispose
từ giao diện IEnumerable
. Những phương thức này được sử dụng để duyệt qua dữ liệu trong bộ sưu tập.
Iterators Generic
C# cho phép các lập trình viên tạo ra các iterator generic. Các iterator generic được tạo ra bằng cách trả về một đối tượng từ giao diện Enumerator<T>
hoặc IEnumerable<T>
generic. Chúng được sử dụng để lặp qua các giá trị của bất kỳ loại giá trị nào.
Đoạn mã bên dưới mô tả cách tạo ra một iterator generic để duyệt qua các giá trị của bất kỳ loại nào.
using System;
using System.Collections.Generic;
class GenericDepartment<T>{
T[] items;
public GenericDepartment(T[] val)
{
items = val;
}
public IEnumerator<T> GetEnumerator()
{
foreach (T value in items)
{
yield return value;
}
}
}
class GenericIterator
{
static void Main(string[] args)
{
string[] departmentNames = { "Marketing", "Finance", "Human Resources", "Information Technology", "HumanResources" };
GenericDepartment<string> objGeneralName = new GenericDepartment<string>(departmentNames);
foreach (string val in objGeneralName)
{
Console.Write(val + "\t");
}
int[] departmentID = { 101, 110, 210, 220 };
GenericDepartment<int> objGeneralID = new GenericDepartment<int>(departmentID);
Console.WriteLine();
foreach (int val in objGeneralID)
{
Console.Write(val + "\t\t");
}
Console.WriteLine();
}
}
Hình bên dưới tạo một iterator generic
Trong đoạn mã trên, lớp generic GenericDepartment được tạo với tham số kiểu generic T
. Lớp này khai báo một biến mảng và bao gồm một hàm tạo có tham số gán các giá trị cho biến mảng này. Trong lớp generic GenericDepartment, phương thức GetEnumerator()
trả về một kiểu generic của giao diện IEnumerator
. Phương thức này trả về các phần tử được lưu trữ trong biến mảng, sử dụng câu lệnh yield
. Trong lớp GenericIterator, một thể hiện của lớp GenericDepartment được tạo ra tham chiếu đến các tên phòng ban khác nhau trong mảng. Một đối tượng khác của lớp GenericDepartment được tạo ra, tham chiếu đến các mã phòng ban khác nhau trong mảng.
Thực hiện Iterators Named
Một cách khác để tạo iterators là bằng cách tạo ra một phương thức, có kiểu trả về là giao diện IEnumerable
. Điều này được gọi là iterator named. Iterator có tên có thể chấp nhận các tham số có thể được sử dụng để quản lý điểm bắt đầu và kết thúc của một vòng lặp foreach
. Kỹ thuật linh hoạt này cho phép bạn lấy các giá trị cần thiết từ bộ sưu tập.
Cú pháp sau tạo ra một iterator named:
<access_modifier> IEnumerable <IteratorName> (<parameter list>) {}
trong đó,
access_modifier
: Xác định phạm vi của iterator có tên.IteratorName
: Là tên của phương thức iterator.parameter list
: Xác định không hoặc nhiều tham số để truyền vào phương thức iterator.
Đoạn mã bên dưới thể hiện cách tạo ra một iterator named.
using System;
class NamedIterators {
string[] cars = { "Ferrari", "Mercedes", "BMW", "Toyota", "Nissan" };
public IEnumerable GetCarNames() {
for (int i = 0; i < cars.Length; i++){
yield return cars[i];
}
}
static void Main(string[] args) {
NamedIterators objIterator = new NamedIterators();
foreach (string str in objIterator.GetCarNames()){
Console.WriteLine(str);
}
}
}
Trong đoạn mã trên, lớp NamedIterators bao gồm một biến mảng và một phương thức GetCarNames(), có kiểu trả về là IEnumerable
. Vòng lặp for
duyệt qua các giá trị trong biến mảng. Phương thức Main()
tạo một thể hiện của lớp NamedIterators và thể hiện này được sử dụng trong vòng lặp foreach
để hiển thị tên của các xe từ biến mảng.
Kết quả:
Ferrari
Mercedes
BMW
Toyota
Nissan