1. Giới thiệu:
Stack stack = new Stack();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack) Console.Write("{0} ", i);
Output: 0 1 2 3 4 5 6 7 8 9
Trong đoạn code trên, bạn có thể thấy một kiểu khai báo mới: "Stack",
đó chính là generics. Nhờ nó, trình dịch sẽ buộc kiểu của dữ liệu bạn
đưa vào stack qua hàm Push() phải phù hợp kiểu đã định (ở đây là int).
Cái này giống với template trong STL. Generics có thể áp dụng cho class,
struct, interface, delegate, và methods.
Iterator được cải tiến để có thể làm việc được với generics. Trong dòng
lệnh cuối cùng ở trên, lệnh foreach đã được viết rất đơn giản vì biến i
có kiểu phù hợp với stack.
2. Mục đích:
Các
generic trong C# là các cấu trúc mà cho phép bạn dùng lại các tính năng
đã được định nghĩa cho các kiểu dữ liêu khác nhau trong C#.
Trong
một chương trình C# mà sử dụng một mảng kiểu Object để lưu trữ tên một
tập hợp của các sinh viên. Tên được đọc từ người dùng do các kiểu giá
trị đều được cho phép chuyển đổi về kiểu object. Trong trường hợp này
khi người dùng nhập vào một giá trị kiểu numeric (kiểu số) thì trình
biên dịch cũng không có bất kỳ sự kiểm tra nào.
Để
đảm bảo đúng kiểu dữ liệu, C# cung cấp generics, nó là một tính năng
được tích hợp trong C# mà cho phép bạn định nghĩa được kiểu dữ liệu mẫu
gốc trên nó các kiểu dữ liệu được kiểm tra chuẩn kiểu dữ liệu sau này.
3. Các namespace cho Generics.
Các
generic là một loại cấu trúc dữ liệu mà có thể làm việc với các kiểu
giá trị cũng như các kiểu tham chiếu. Bạn có thể định nghĩa một class,
interface, structure and delegate như một kiểu dư liệu trong C#.
Namespace System.Collections.ObjectModel cho phép bạn tạo động và tập
hợp các generic duy nhất có khả năng đọc. Namespace
System.Collections.Generic gồm các class và interface cho phép bạn định
nghĩa tập hợp các generic có thể sửa đổi.
4. Việc tạo các generic:
Một
sự khai báo các generic luôn luôn chấp nhận một tham số kiểu. Nó là một
phần nắm giữ kiểu dữ liệu được trả về. Kiểu được chỉ định duy nhất một
kiểu generic được nói tới hoặc hàm khởi tạo nó là một kiểu trong chương
trình.
Cấu trúc:
6. Tác dụng của Generic:
Generic
đảm bảo chuẩn kiểu dữ liệu lúc biên dịch. Generics cho phép bạn dùng
lại mã trong một nền tảng mà không có khuôn mẫu hoặc chuyển đổi. Một sự
định nghĩa kiểu generic là khả năng dùng lai với các kiểu dữ liệu khác
nhau nhưng không chấp nhận các giá trị của một kiểu đơn cùng một thời
gian.
Một số tính năng của Generic:
Ø Tăng khả năng thực thi vì không mất thời gian và bộ nhớ cho việc casting và boxing khi tạo một generic.
Ø Đảm bảo kiểu mẫu lập trình mạnh
Ø Hạn chế các lỗi run-time mà có thể xảy ra trong quá trình boxing và casting.
6. Các class Generic:
Các
class Generic định nghĩa các tính năng mà có thể dùng cho bất cứ kiểu
dữ liệu nào.Các class là được khai báo với việc khai báo một class sau
đó là tham số kiểu được bao trong hai dấu < >. Lúc khai báo class
generic bạn có thể gán thêm một số hạn chế hoặc điều kiện cho tham số
kiểu bằng viẹc sử dụng từ khóa where. Hơn nữa việc gán thêm hạn chế và các điều kiện vào tham số kiểu là tùy ý.
Các
class Genric có thể được lồng với các class generic hoặc không generic
khác, Hơn nữa bất kỳ class được lồng trong một class generic khác và
khác tham số kiểu thì tham số kiểu của class bên ngoài sẽ được gán lại
cho class lồng bên trong.
7. Các điều kiện của tham số kiểu:
Bạn
có thể gán các điều kiện cho tham số kiểu lúc khai báo một kiểu
genneric. Một ép buộc là một là một hạn chế được áp đạt lên các kiểu dữ
liệu của tham số kiểu. Các ép buộc được chỉ định bằng cách chỉ định từ
khóa where. Nó được sử dụng khi người lập trình muốn giới hạn kiểu dữ
liệu trên tham số kiểu để đảm bảo sự toàn vẹn và tin cậy của dữ liệu
trong một tập hợp.
8. Việc kế thừa các class Generic:
Ø Một
class generic có thể kế thừa từ bất cứ class generic hoặc không phải
class generic nào trong C#. Khi đó một class generic đóng vai trò cả hai
giống như một class cơ sở hoặc class dẫn xuất.
Ø Thực hiện các thao tác bình thường như thao tác với các class kế thừa nhau thông thường.
9. Các phương thức Generic.
Ø Các
phương thức generic sử lý các dữ liệu của các kiểu dữ kiệu là duy nhất
khi các biến lưu trữ kiểu dữ liệu này, Một generic được khai báo với
danh sách tham số kiểu được bao trong 2 dấu < >.
Ø Việc định nghĩa các phương thức với các tham số kiểu cho phép bạn gọi tới phương thức với các kiểu khác nhau mọi lúc.
Ø Bạn
có thể khi báo một generic class bên trong một class là generic class
hoặc class thông thương. Khi bạn khai báo một phương thức được khai báo
trong một class generic đươc khai báo thì thân của phương thức thể hiện
cả hai tham số kiẻu của phương thức và của class.
10. Các interface Generic.
Ø Các
interface Generic là được dùng cho tập hợp các generic hoặc các class
generic việc hiển thị các thành phần trong tập hợp. Bạn có thể sử dụng
các class generic với các interface ngăn chặn các toán tử boxing và
casting trên các kiểu giá trị.
Ø Các
class interface co thể thực thi các giao diện generic bằng việc thông
qua sự trả về của các tham số được chỉ định trong interface. Tương tự
thế các class generic cũng thực thi sự kế thừa.
11. Mục đích của Iterator.
Ø Trong
một hoàn cảnh mà có một người muốn nhớ một cuốn sách của 100 trang.
Nhiệm vụ cuối cùng của người này là nhắc lại mỗi trang trong 100 trang
đó.
Ø Tương
tự người này phải nhắc lại số trang, một iterator trong C# hỗ trợ việc
đi tắt thông qua một danh sách các giá trị hoặc một tập hợp. nó là một
khối của đoạn code khi mà dùng vòng lặp foreach gọi tới một tập hợp các
giá trị theo một khuôn mẫu có tuần tự.
Ø Ví dụ như một người lập trình lưu trữ các giá trị thông qua sự tuần tự các giá trị sử dụng ỉterator để so sánh các giá trị.
12. Iterator:
Ø Một
iterator không phải là một thành phần dữ liệu nhưng nó là một cách của
việc truy suất đến các phần tử. Nó có thể là một phương thức một khả
năng truy suất get hoặc một một toán tử mà cho phép bạn đánh dấu các giá
trị trong một tập hợp. Các Iterator là chỉ định cái cách mà các giá trị
được sinh ra khi mà vòng lặp foreach truy suất đến các phần tử trong
một tập hợp.
Ø Chúng giữ lại một phần các phần tử trong tập hợp sau đó nó có thể lấy về các giá trị này khi cần đến.
13. Tác dụng:
Ø Cung cấp một sự đơn giản và nhanh hơn theo cách nhắc lại các giá trị trong tập hợp.
Ø Giảm bớt sự phức tạp của việc cung cấp một sự liệt kê cho một tập hợp.
Ø Iterator có thể trả về một lượng lơn các giá trị.
Ø Iterator có thể được đánh giá và trả về duy nhất các giá trị mà nó cần thiết.
Ø Iterator có thể trả về các giá trị mà không tốn bộ nhớ bằng việc gọi tới duy nhất một giá trị trong danh sách.
14. Sự thực thi:
Ø Iterator có thể được thực hiện bằng phương thức GetEnumerator() mà trả về một tham chiếu tới interface IEnumerator.
Ø Khối
iterator sử dụng từ khóa yield. Từ khóa yield return là trả về các giá
trị khi đó từ khóa yield break là kết thúc của sử lý iterator.
Ø Một
cách khác để tạo là bằng việc tạo một phương thức kiểu trả về là
interface IEnumerable. Đây được gọi là một iterator có tên.Iterator có
tên chấp nhận tham số mà được sử dụng cho việc quản lý điểm bắt đầu và
điểm kết thúc vủa vòng lặp foreach.
Bài thực hành số 14
Generic và Iterator
1. Generic
Tóm tắt lý thuyết: Lập
trình generic tiết kiệm được khá khá thời gian lập trình, và tính tái
sử dụng code rất cao. Trong C#, bạn có thể lập trình Generic với Class,
Struct, Function.
Generics rất hữu ích vì
nó cung cấp một khả năng mạnh mẽ để kiểm tra kiểu dữ liệu trong lúc
biên dịch, sử dụng ít các phép ép kiểu giữ các kiểu dữ liệu khác nhau,
giảm việc ép kiểu lúc chạy.
1.1 Tại sao dùng generics?
Nếu
không có generics, để mô tả dữ liệu không xác định (dữ liệu có kiểu
tổng quát nào đó), chúng ta phải dùng kiểu object. Ví dụ, lớp Stack chứa
dữ liệu là một mảng các object, và nó có hai phương thức Push và Pop,
sử dụng object như là kiểu dữ liệu đầu vào.
Mặc
dù chúng ta dùng kiểu object để lưu trữ dữ liệu một cách rất mềm dẻo,
tuy nhiên lại xuất hiện một số hạn chế. Ví dụ, chúng ta có thể đẩy vào
Stack một giá trị thuộc kiểu bất kì, ví dụ Customer. Khi đối tượng này
được lấy ra ví dụ bằng lệnh Pop, chúng ta bắt buộc phải ép kiểu trở lại,
điều này làm giảm hiệu năng của hệ thống do xuất hiện các phép kiểm tra
kiểu dữ liệu.
Nếu giá trị truyền vào là kiểu tham trị (value type), nó sẽ được tự động gọi lại (boxed). Khi lấy lại, giá trị này sẽ được tự động mở gói (unboxed), và đây là phép ép kiểu không an toàn (boxing và unboxing - khái niệm?)
Tất cả các
phép boxing và unboxing, ngoài việc kiểm tra kiểu dữ liệu, còn mất các
phép toán khởi tạo bộ nhớ động cho việc box/unbox.
Một vấn đề
nữa là chúng ta có thể chuyển kiểu nhầm. Ví dụ, bạn lưu trữ vào Stack
một đối tượng Customer, nhưng lại lấy ra một string, khi đó ngoại lệ
InvalidCastException sẽ được ném ra, do chúng ta không thể chuyển
Customer về string được.
Mọi việc sẽ ổn hơn nếu chúng ta có thể chỉ đích xác kiểu dữ liệu mà Stack lưu trữ. Generics cho phép chúng ta làm điều đó
1.2 Tạo và sử dụng Generics
Ví
dụ sau đây mô tả cách tạo một lớp generic Stack nhận kiểu dữ liệu T như
là tham số đầu vào. Kiểu này được đặt trong cặp dấu <> đi ngay
đăng sau tên lớp. Thay vì chuyện cần có phép ép kiểu từ kiểu object, mỗi
thể hiện Stack chỉ làm việc với kiểu dữ liệu được chỉ định ra lúc tạo,
và không cần đến phép ép kiểu. T hoạt động như là thành phần tạm, cho
đến khi một thể hiện nào đó của Stack được tạo ra
Khi lớp Stack được dùng, một kiểu dữ liệu cụ thể nào đó sẽ thay thế T. Xem ví dụ sau:
Kiểu Stack
được gọi là kiểu đã khởi tạo (constructed type). Trong kiểu này, tất cả
chỗ nào xuất hiện T đều được thay thế bởi int. Lúc đó mảng sẽ lưu trữ là
mảng các số nguyên, chứ không phải là mảng các đối tượng, và sẽ hiệu
quả hơn nhiều so với việc lưu trữ và xử lí mảng đối tượng. Tất nhiên,
các hàm Push và Pop của Stack cũng hoạt động với kiểu dữ liệu int, cho
phép kiểm soát lỗi không tương thích kiểu lúc biên dịch, và tránh được
phép ép kiểu lúc chạy chương trình.
Generics cho phép kiểm tra kiểu ngay khi biên dịch, có nghĩa là chúng ta không thể nhầm lẫn trong việc xử lí dữ liệu được,. Tất cả những lỗi này sẽ được thông báo ngay bởi bộ biên dịch. Hãy xem ví dụ sau.
Generics cho phép kiểm tra kiểu ngay khi biên dịch, có nghĩa là chúng ta không thể nhầm lẫn trong việc xử lí dữ liệu được,. Tất cả những lỗi này sẽ được thông báo ngay bởi bộ biên dịch. Hãy xem ví dụ sau.
1: Stack stack = new Stack();
2: stack.Push(new Customer());
3: Customer c = stack.Pop();
4: stack.Push(3); // Type mismatch error
5: int x = stack.Pop(); // Type mismatch error
Generics có thể sử dụng nhiều kiểu dữ liệu đầu
vào. Lớp Stack dùng một kiểu dữ liệu đầu vào, nhưng lớp Dictionary lại
có hai kiểu dữ liệu đầu vào, một dành cho kiểu dữ liệu của khóa, và một
dành cho kiểu dữ liệu của giá trị khóa.
1: public class Dictionary
2: {
3: public void Add(K key, V value) {...}
4: public V this[K key] {...}
5:}
Tất nhiên, khi sử dụng chúng ta phải cung cấp cả hai kiểu dữ liệu này
Tất nhiên, khi sử dụng chúng ta phải cung cấp cả hai kiểu dữ liệu này
Ví dụ
Ràng buộc dữ liệu
Nói
chung, các lớp generic sẽ thực hiện nhiều hành động hơn là chỉ lưu trữ
dữ liệu. Thực tế là các lớp generic sẽ muốn triệu gọi các phương thức có
liên quan đến việc xử lí đối tượng có kiểu với tham số generic truyền
vào. Xem ví dụ sau:
Vì
kiểu dữ liệu của K có thể là bất cứ kiểu gì, do đó chúng ta chỉ có thể
dùng phương thức có trong Object, nghĩa là lớp nào cũng hỗ trợ, ví dụ
như các hàm Equals, GetHashCode, và ToString; nếu chúng ta dùng hàm khác
(trong ví dụ trên là CompareTo) thì không thể đảm bảo rằng kiểu nào của
K cũng hỗ trợ, nên sẽ có lỗi biên dịch. Tất nhiên, nếu muốn thì lúc này
phải ép kiểu, ví dụ như sau:
Tới
đây, vấn đề cũ lại xảy ra: mất thêm chi phí ép kiểu và có thể có ngoại
lại ném ra nếu trong trường hợp chúng ta không ép kiểu được.
Để
giải quyết vấn đề này, C# cung cấp 1 cách kiểu tra kiểu tại thời điểm
design, đồng thới làm giảm bớt ép kiểu, đó là danh sách các ràng buộc
(constraints) đối với mỗi loại tham số. Mỗi
kiểu tham số đầu vào sẽ phải thỏa mãn các ràng buộc nào đó để mà có thể
trở thành kiểu tham số thực. Tham số này được cung cấp bằng từ khóa
where, theo sau bởi tên kiểu và dấu hai chấm, đến kiểu lớp, kiểu giao
tiếp ràng buộc, cách nhau bởi dấu phảy
Cú pháp where
Chú ý :
Chúng
ta có thể chỉ định nhiều ràng buộc về giao tiếp và kiểu, nhưng chỉ có 1
ràng buộc bề lớp. Các ràng buộc kiểu khác nhau cách nhau bởi mệnh đề
where. Ví dụ dưới đây, kiểu K có 2 ràng buộc giao tiếp, trong khi kiểu E
có 1 ràng buộc lớp và 1 ràng buộc khởi tạo.
Ràng
buộc khởi tạo new() ở ví dụ trên chỉ ra rằng kiểu truyền vào cho E phải
có hàm tạo không tham số, public, do đó cho phép lớp generic có thể gọi
hàm tạo này để tạo thể hiện cho lớp. phần này tra ở Help.
Phương thức generic
Trong
một số trường hợp thì chúng ta không cần kiểu generic cho 1 lớp, mà chỉ
cần cho một phương thức nào thôi. Trong trường hợp này thì chúng ta sẽ
dùng phương thức generic. Ví dụ, với lớp Stack ở trên, một thao tác hay
làm đó là đẩy vào trong stack nhiều phần tử 1 lúc, nghĩa là trong 1 lời
gọi hàm sẽ thao tác với nhiều phần tử. Chẳng hạn, để tạo một Stack,
chúng ta muốn rằng sẽ đẩy vào 1 mảng các số nguyên, vậy phương thức sẽ
như sau:
Lời gọi tương ứng sẽ là :
Tuy
nhiên, cách làm này chỉ đúng với Stack. Bây giờ, chúng ta sẽ phải viết
một phương thức generic, sử dụng chung kiểu dữ liệu T đối với lớp
generic. Đoạn mã như sau:
Lời gọi hàm tương ứng là
Lập trình Generic với Struct.
Nhìn chung, ko có gì khác biệt trong cách lập trình Generic Struct và Generic Class, ví dụ sau là Struct
Couple:
Ví dụ
2. Giới thiệu Iterator
Câu lệnh foreach của C# được dùng để duyệt lặp
đi lặp lại qua những phần tử trong một tập liệt kê được. Để trở thành
một tập liệt kê được, tập đó phải phải có phương thức GetEnumerator
trả về enumerator (toán tử liệt kê). Nói chung, trên thực tế toán tử
liệt kê rất khó cài đặt, thế nhưng công việc này sẽ đơn giản hơn với
việc dùng iterator.
Một iterator là một khối lệnh cho phép yield (tạo ra, sinh ra) một dãy các giá trị đã được sắp xếp. Một iterator được phân biệt với khối lệnh bình thường bằng một hay nhiều từ khóa yield:
Một iterator là một khối lệnh cho phép yield (tạo ra, sinh ra) một dãy các giá trị đã được sắp xếp. Một iterator được phân biệt với khối lệnh bình thường bằng một hay nhiều từ khóa yield:
Ø Lệnh yield return chuyển đến giá trị tiếp theo của iteration
Ø Lệnh yield break chỉ ra iterator đã hoàn thành.
Một iterator có
thể được dùng như là thân hàm hoặc là giá trị trả về của hàm trong lớp
có cài đặt giao tiếp enumerator hoặc enumerable.
Ø Giao tiếp enumerator là System.Collections.IEnumerator và được thiết lập từ System.Collections.Generic.IEnumerator.
Ø Giao tiếp enumerable là System.Collections.IEnumerable và được thiết lập từ System.Collections.Generic.IEnumerable.
Ø Một điều quan
trọng đó là một iterator không phải là một thành viên của lớp, nhưng đó
là một cách cài đặt hàm thành viên. Một thành viên được cài đặt thông
qua iterator có thể được viết đề bởi các thành viên khác mà có hay không
có sự cài đặt với iterator.
Ø Lớp Stack dưới đây cài đặt giao tiếp GetEnumerator bằng cách sử dụng iterator. Iterator này cho phép duyệt stack từ đỉnh về đáy.
Khi đó, chúng ta sẽ sử dụng được lệnh foreach đối với Stack. Xem đoạn lệnh minh họa sau:
Kết quả đầu ra là: 9 8 7 6 5 4 3 2 1 0
Lệnh
foreach sẽ gọi phương thức GetEnumerator để lấy về một toán tử liệt kê.
Tất cả các hàm này đều không có tham số truyền vào, mặc dù có thể có
nhiều hàm như vậy. Trong trường hợp đó, chúng ta hiểu như là lớp sẽ cho
phép liệt kê các thành phần của nó theo các thứ tự (hướng) liệt kê khác
nhau. Xem ví dụ dưới đây: