Tài liệu lập trình C# | Đại học Kỹ thuật - Công nghệ Cần Thơ

Tài liệu lập trình C# | Đại học Kỹ thuật - Công nghệ Cần Thơ. Tài liệu được biên soạn dưới dạng file PDF gồm 123 trang, giúp bạn tham khảo, ôn tập và đạt kết quả cao trong kì thi sắp tới. Mời bạn đọc đón xem!

Môn:

Lập trình C# 3 tài liệu

Thông tin:
123 trang 7 tháng trước

Bình luận

Vui lòng đăng nhập hoặc đăng ký để gửi bình luận.

Tài liệu lập trình C# | Đại học Kỹ thuật - Công nghệ Cần Thơ

Tài liệu lập trình C# | Đại học Kỹ thuật - Công nghệ Cần Thơ. Tài liệu được biên soạn dưới dạng file PDF gồm 123 trang, giúp bạn tham khảo, ôn tập và đạt kết quả cao trong kì thi sắp tới. Mời bạn đọc đón xem!

101 51 lượt tải Tải xuống
TRƯỜNG ĐẠI HỌC KỸ THUẬT – CÔNG NGHỆ
CẦN THƠ
KHOA CÔNG NGHỆ THÔNG TIN
CHUYÊN NGÀNH HỆ THỐNG THÔNG TIN
CẦN THƠ 2021
TÀI LIỆU LẬP TRÌNH C#
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 2
TIN HỌC ĐẠI CƯƠNG
I. Cấu trúc cơ bản của một chương trình
1. Using
Cú pháp:
Dùng để chỉ những thư viện được dùng trong chương trình.
Thư viện là một tập các phương thức, kiểu dữ liệu nào đó được tạo ra nhằm hỗ trợ cho
việc lập trình được nhanh chóng và hiệu quả hơn.
Ví dụ: khai báo một số thư viện
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
2. Namespace
Cú pháp:
Namespace <tên namespace>
{
//các thành ph n bên trong namespace bao g m các l p, enum, delegate ho c
//các namespace con
}
Dùng để báo cho trình biên dịch biết các thành phần bên trong khối {} ngay bên
dưới namespace thuộc vào chính namespace đó.
3. Class
Cú pháp:
Dùng để báo cho trình biên dịch biết là những thành phần trong khối {} ngay
sau tên lớp thuộc vào chính lớp đó.
Ví dụ:
class Program
{
static void Main(string[] args)
{
}
}
Phương thức Main bên trong khối {} của lớp Program nên phương thức này
thuộc lớp Program.
4. Hàm (phương thức) Main
Main là hàm được tạo sẵn khi tạo project với cấu trúc:
Main là hàm chính của chương trình. Mỗi khi trình biên dịch dịch chương trình
ra sẽ đi vào hàm main đầu tiên để bắt đầu vong đời của chương trình. Code sẽ được
viết bên trong khối {} của hàm main.
5. Comment (chú thích)
Có 3 cách để comment code trong Visual Studio:
using <tên thư viện>
class <tên class>
static void Main(string[] args)
{
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 3
- Sử dụng ký tự //: bất kì ký tự nào phía sau // đều không được dịch. Sau dòng //,
dòng tiếp theo sẽ không còn comment nữa.
VD: //comment cho một dòng code…
- Sử dụng ký tự /**/: bất kì đoạn code hay chữ nào nằm trong khối /**/ đều tính là
comment, kể cả xuống dòng.
VD: /* đoạn này không được dịch
xuống dòng như vậy cũng không dịch*/
- Sử dụng ký tự ///: khi gõ ký tự này trên namespace, class, method thì visual studio
sẽ tự sinh ra một đoạn comment như sau:
/// <summary>/// B n có th ghi b t kỳ trong này///
</summary>/// <param name="args"></param>
- Phím tắt comment nhanh
Crtl + k+c: đóng comment
Crtl + k+u: mở comment
II. Nhập xuất cơ bản trong C# Console Application
1. Cấu trúc cơ bản của các lệnh nhập xuất
a. Console.Write(<giá trị cần in ra màn hình>);
In giá trị ra màn hình console nhưng không đưa con trỏ xuống dòng. Giá trị này có
thể là một ký tự, một chuỗi, một giá trị có thể chuyển về kiểu chuỗi.
VD:
static void Main(string[] args)
{
Console.Write(“xin chào!”); // in ra dòng ch “xin chào!”
}
b. Console.WriteLine(<giá trị cần in ra màn hình>);
Tương tự như Console.Write(). Nhưng khi in kết quả ra màn hình, nó sẽ tự động
đưa con trỏ xuống dòng.
Ngoài ra, còn có nhiều cách để xuống dòng như:
Sử dụng ký tự “\n” trong chuỗi in ra màn hình thì trình biên dịch sẽ tự đổi nó
thành ký tự xuống dòng.
Console.WriteLine(“hé lô”) = Console.Write(“hé lô \n”)
Sử dụng lệnh xuống dòng Enviroment.NewLine. VD
Console.Write(Enviroment.NewLine);
(cách này khá dài nên rất ít sử dụng. Chủ yếu là Console.WriteLine hoặc “\n”).
Cộng dồn chuỗi in ra màn hình
{
int a = 5; // khai báo biến kiểu nguyên có tên là a và khởi tạo giá trị là 5.
Console.Write("a = "); // In ra màn hình giá trị "a = ".
Console.Write(a); // In ra giá trị của a là 5
// K t qu màn hình là: a = 5ế
}
Có thể viết gọn lại là: Console.Write(“a= ”+5); // vẫn in ra màn hình a=5
In ra giá trị của biến
Cộng dồn là một cách in ra giá trị của biến. Ngoài ra, cũng có thể chỉ định vị trí in
ra giá trị của biến trong chuỗi bằng cú pháp {<số đếm>}.
Ví dụ:
int a = 5; // khai báo biến kiểu nguyên có tên là a và khởi tạo giá trị là 5.
Console.Write("a = {0}", a); // In ra màn hình giá trị "a = 5".
Cú pháp:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 4
Console.Write(“{0}{1}{2}{…}”,<giá tr 0>,<giá tr 1>,<giá tr 2>,
<giá tr n>);
Trong đó: <giá trị 0> sẽ được điền tương ứng vào vị trí số 0 tương tự cho các vị
trí còn lại. Với hai cách trên, ta có thể thao tác biến hóa làm cho code trở nên gọn gàng,
trực quan hơn.
c. Console.Read();
Đọc một ký tự từ bàn phím và trả về kiểu số nguyên là mã ASCII của ký tự đó.
Chú ý: lệnh này không đọc được các phím chức năng như ctrl, shift, Alt, tab,…
Ví dụ:
static void Main(string[] args)
{
Console.WriteLine(Console.Read());
// đọc 1 ký tự từ bàn phím bằng lệnh Console.Read() sau đó in ra ký tự vừa đọc
Console.ReadKey();
//lệnh này có mục đích dừng màn hình để xem kết quả.
}
Khi nhập a thì màn hình sẽ in ra số 97 (mã ASCII của ký tự a).
d. Console.ReadLine();
Đọc dữ liệu từ bàn phím cho đến khi gặp ký tự xuống dòng thì dừng (nói cách khác
đọc cho đến khi nhấn enter thì dừng). Giá trị đọc được luôn là một chuỗi.
e. Console.ReadKey(< tham số kiểu bool >);
- Lệnh này dùng để đọc một ký tự từ bàn phím nhưng trả về kiểu ConsoleKeyInfo (là
một kiểu dữ liệu có cấu trúc được định nghĩa sẵn để chứa những ký tự của bàn
phím bao gồm các phím chức năng).
- Tham số kiểu bool bao gồm 2 giá trị: true hoặc false. Nếu truyền vào true thì phím
được ấn sẽ không hiển thị lên màn hình console mà được đọc ngầm. Ngược lại thì
phím được ấn sẽ hiển tị lên màn hình console. Nếu không truyền vào tham số sẽ
được mặc định là false.
III. Biến trong C#
1. Biến là gì? Tại sao phải dùng biến
- Biến là một giá trị dữ liệu có thể thay đổi được. Là tên gọi tham chiếu đến vùng
nhớ nào đó trong bộ nhớ. Là thành phần cốt lõi của ngôn ngữ lập trình.
- Lưu trữ dữ liệu và tái sử dụng. Thao tác với bộ nhớ một cách dễ dàng:
Cấu trúc của bộ nhớ bao gồm nhiều ô nhớ liên tiếp nhau, mỗi ô nhớ có một địa
chỉ riêng (địa chỉ ô nhớ thường là mã hex – thập lục phân).
Khi muốn sử dụng ô nhớ nào (cấp phát, hủy, lấy giá trị, …) thì phải thông qua
địa chỉ của chúng. Điều này làm cho việc lập trình khó khăn hơn.
Thay vào đó, có thể khai báo một biến và cho nó tham chiếu đến ô nhớ cần
quản lý. Khi sử dụng sẽ dùng tên biến đã đặt chứ không cần dùng địa chỉ của ô
nhớ đó.
2. Khai báo biến
Cú pháp:
- Trong đó:
<kiểu dữ liệu> có thể là kiểu dữ liệu cơ bản hay kiểu dữ liệu cấu trúc,…
<tên biến> là tên do người dùng đặt. Phải tuân thủ quy tắc đặt tên.
Khai báo:
Kiểu dữ liệu là: int, string
Tên biến là:
<kiểu dữ liệu> <tên biến>;
int BienKieuSoNguyen;
string BienKieuChuoi;
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 5
BienKieuSoNguyen
BienKieuChuoi
Để sử dụng biến ta cần phải gán giá trị cho biến. Có 2 cách:
Khởi tạo giá trị lúc khai báo:
Gán giá trị theo cú pháp: <tên biến> = <giá trị>;
Khi bạn muốn gọi một biến ra để lấy giá trị thì chỉ cần gọi tên là được. VD:
3. Quy tắc đặt tên biến
Một số quy tắc đặt tên biến cũng như các định danh khác:
Tên biến là một chuỗi ký tự liên kết (không có khoản trắng) và không chứa ký
tự đặc biệt
Tên biến không được đặt bằng tiếng Việt có dấu.
Tên không được bắt đầu bằng số.
Tên biến không được trùng nhau.
Tên biến không được trùng với từ khóa
DANH SÁCH CÁC TỪ KHÓA TRONG C#
** Ngoài ra còn có một số quy tắc khác:
- Quy tắc Lạc Đà: viết thường từ đầu tiên và viết hoa chữ cái đầu tiên của mỗi
từ. Ví dụ: tenBien, kyTu,…
- Quy tắc Pascal: viết hoa chữ cái đầu tiên của mỗi từ. Ví dụ: TenBien, KyTu,
int BienKieuSoNguyen = 10;
string BienKieuChuoi = “hello”;
BienKieuSoNguyen = 9;
BienKieuKyTu = “H”;
int a = 1, b = 2 ;
int c = a + b;
// Bi n a và bi n b đ c g i đ l y giá tr sau đó c ngế ế ượ
//chúng l i r i gán cho bi n c. ế
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 6
Lưu ý:
- Tên biến phải ngắn gọn dễ hiểu, thể hiện rõ mục đích của biến (Name, Tuoi,
GioiTinh,…).
- Không đặt tên biến bằng một ký tự như a, b, c,… như vậy khi người khác
đọc sẽ gây không hiểu biến này dùng để làm gì.
- C# có phân biệt chữ hoa chữ thường. VD biến a <> biến A
IV.Kiểu dữ liệu trong C#
1. Định nghĩa
- Kiểu dữ liệu là tập hợp các nhóm dữ liệu có cùng đặc tính, cách lưu trữ và thao
tác xử lý trên từng dững liệu đó.
- Là 1 tính hiệu để trình biên dịch biết kích thước của một biến và khả năng của nó.
- Là thành phần cốt lõi của ngôn ngữ lập trình
Phải có kiểu dữ liệu để nhận biết kích thước và khả năng của một biến. Nhằm mục
đích phân loại dữ liệu. Nếu không có kiểu dữ liệu sẽ rất khó xử lý vì không biết
biến này kiểu chuỗi hay kiểu số nguyên hay số thực, …
2. Phân loại
a. Kiểu dữ liệu giá trị (value):
- Một biến khi khai báo kiểu dữ liệu giá trị thì vùng nhớ của biến đó sẽ chứa giá
trị của kiểu dữ liệu được lưu trữ trong bộ nhớ Stack.
- Một số kiểu dữ liệu thuộc kiểu giá trị: bool, byte, char, decimal, double, fload,
int, long, short, struct, …
b. Kiểu dữ liệu tham chiếu:
- Một biến khi khai báo kiểu dữ liệu tham chiếu thì vùng nhớ của biến đó chỉ chứa
địa chỉ của đối tượng dữ liệu và lưu trong bộ nhớ Stack. Đối tượng dữ liệu thực
sự được lưu trong bộ nhớ Heap
- Một số kiểu dữ liệu thuộc kiểu tham chiếu: object, dynamic, string và tất cả kiểu
dữ liệu do người dùng định nghĩa.
Stack và Heap đều là bộ nhớ trên RAM nhưng cách tổ chức và quản lý dữ
liệu cũng như sử dụng thì khác nhau:
Stack:
Vùng nhớ được cấp phát khi chương trình biên dịch.
Được sử dụng cho việc thực thi thread, khi gọi hàm, các biến cục bộ kiểu
giá trị và tự động giải phóng khi không còn sử dụng nữa.
Kích thước vùng nhớ của Stack là cố định và chúng ta không thể thay đổi.
Khi vùng hwos này không còn đủ dùng thì sẽ gây ra hiện tượng tràn bộ
nhớ (stack overlow). Hiện tượng này xảy ra khi nhiều hàm lồng vào nhau
hoặc gọi đệ quy nhiều lần dẫn tới không đủ vùng nhớ.
Heap:
Vùng nhớ được cấp phát khi chạy chương trình.
Vùng nhớ Heap được dùng cho cấp phát bộ nhớ động (cấp phát thông qua
toán tử new).
Bình thường vùng nhwos Heap do người dùng tự giải phóng nhwung trong
C# điều này được hỗ trợ mạnh mẽ bởi bộ tự động thu gom rác (Garbage
Collection). Vì thế, việc thực hiện vùng nhớ sẽ được giải phóng tự động.
Kích thước vùng nhớ Heap có thể thay đổi được. Khi không đủ vùng nhớ
để cấp phát thì hệ điều hành sẽ tự động tăng kích thước vùng nhớ Heap
lên.
Ý nghĩa của một số kiểu dữ liệu cơ bản
Nhóm Kiểu dữ
liệu
Kích thước Ý nghĩa
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 7
Kiểu số
nguyên
Byte
1
Số nguyên dương không dấu từ 0 – 255
Sbyte
1
Số nguyên có dấu từ -128 - 127
Short
2
Số nguyên từ -32,768 – 32,767
ushort
2
Số nguyên không dấu từ 0 – 65,535
Int
4
Số nguyên từ -2,147,483,647 – 2,147,483,647
Uint
4
Số nguyên không dấu có giá trị từ 0 đến 4,294,967,295
Long
8
Số nguyên có giá trị từ -9,223,370,036,854,775,808 đến
9,223,370,036,854,775,807
ulong
8
Số nguyên có giá trị từ 0 đến
18,446,744,073,709,551,615
Kiểu ký tự Char
2
Chứa một ký tự Unicode
Kiểu logic bool
1
Chứa 1 trong 2 giá trị logic true hoặc false
Kiểu số
thực
Float
4
Kiểu số thực dấu chấm động có giá trị dao động
từ 3.4E – 38 đến 3.4E + 38, với 7 chữ số có nghĩa
Double
8
Kiểu số thực dấu chấm động có giá trị dao động từ 1.7E –
308 đến 1.7E + 308, với 15, 16 chữ số có nghĩa
decimal
8
Có độ chính xác đến 28 con số và giá trị thập phân, được
dùng trong tính toán tài chính
Khác với kiểu dữ liệu trên, string là kiểu dữ liệu tham chiếu dùng để
lưu chuỗi ký tự.
Kiểu dữ liệu có miền giá trị lớn hơn sẽ chứa được kiểu dữ liệu có
miền giá trị nhỏ hơn để có thể gán giá trị qua biến kiểu dữ liệu lớn
hơn.
Giá trị kiểu char nằm trong dấu ‘ ’.
Giá trị kiểu string nằm trong dấu “ ”.
Giá trị của biến kiểu fload phải có chữ F hoặc f làm hậu tố.
Giá trị của biến kiểu decimal phải có chữ M hoặc m làm hậu tố.
Trừ kiểu string, tất cả các kiểu trên đều không được chứa giá trị null.
VÍ DỤ:
static void Main(string[] args)
{
// Ki u s nguyên
byte BienByte = 10;
short BienShort = 10;
int BienInt = 10;
long BienLong = 10;
// Ki u s th c
float BienFloat = 10.9f;
// Giá tr c a bi n ki u float ph i có h u t f ho c F. ế
double BienDouble = 10.9;
// Giá tr c a bi n ki u double không c n h u t . ế
decimal BienDecimal = 10.9m;
// Giá tr c a bi n ki u decimal ph i có h u t m. ế
// Ki u ký t và ki u chu i
char BienChar = 'K';
// Giá tr c a bi n ki u ký t n m trong d u '' (nháy đ n). ế ơ
string BienString = "Kteam";
// Giá tr c a bi n ki u chu i n m trong d u "" (nháy kép). ế
Console.ReadKey();
}
V. Toán tử trong C#
1. Toán tử là gì
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 8
Toán tử là một công cụ có thể thao tác với dữ liệu. Một toán tử là một ký hiệu
dùng để đại diện cho một thao tác cụ thể được thực hiện trên dữ liệu.
Có 6 loại toán tử cơ bản: toán tử toán học, toán tử quan hệ, toán tử logic, toản tử
khởi tạo và gán, toán tử so sánh trên bit, toán tử khác.
a. Toán tử toán học
Giả sử biến a =10 và biến b = 9
Toán tử Mô tả Ví dụ
+ Thực hiện cộng hai toán hạng
a + b = 19
- Thực hiện trừ hai toán hạng
a – b = 1
* Thực hiện nhân hai toán hạng
a * b = 90
/ Chia lấy phần nguyên 2 số nguyên. Ngược lại thì chia bình thường
a / b = 1
% Chia lấy phần
a % b = 1
++ Tăng giá trị lên 1 đơn vị
a++ = 11
-- Giảm giá trị xuống 1 đơn vị
a-- = 9
Lưu ý: đối với toán tử ++ và – cần phân biệt a++ và ++a (a-- và --a):
a++ là sử dụng giá trị của biến a để thực hiện biểu thức rồi mới thực hiện
tăng lên 1 đơn vị cho a (tương tự a--).
++a là tăng giá trị biến a lên một đơn vị rồi mới sử dụng biến a để thực
hiện biểu thức. (--a).
Ví dụ:
static void Main(string[] args)
{
int i = 5, j = 5;
Console.WriteLine(i++); //S d ng giá tr i đ in ra r i m i tăng i
Console.WriteLine(++j); //Tăng j lên r i m i in giá tr j ra màn hình
Console.ReadKey();
} 5
Kết quả khi chạy chương trình là: 6
b. Toán tử quan hệ
Giả sử biến a có giá trị bằng 10 và biến b có giá trị bằng 9:
Lưu ý:
1- Các toán tử quan hệ này chỉ áp dụng cho số hoặc ký tự.
2- Hai toán hạng hai bên phải cùng loại (cùng số hoặc cùng chữ).
3- Bản chất của việc so sánh hai ký tự khác nhau là so sánh mã ASCII của các
ký tự đó.
Toán tử Mô tả Ví dụ
== So sánh 2 toán hạng có bằng nhau hay không. Nếu bằng thì trả
về true nếu không bằng thì trả về false
a == b sẽ trả
về false
!= So sánh 2 toán hạng có bằng nhau hay không. Nếu không bằng thì trả
về true nếu bằng thì trả về false
a != b sẽ trả
về true
> So sánh 2 toán hạng bên trái có lớn hơn toán hạng bên phải hay
không. Nếu lớn hơn thì trả về true nếu không lớn hơn thì trả về false
a > b sẽ trả
về true
< So sánh 2 toán hạng bên trái có nhỏ hơn toán hạng bên phải hay
không. Nếu nhỏ hơn thì trả về true nếu không nhỏ hơn thì trả
về false
a < b sẽ trả
về false
>= So sánh 2 toán hạng bên trái có lớn hơn hoặc bằng toán hạng bên
phải hay không. Nếu lớn hơn hoặc bằng thì trả về true nếu nhỏ
hơn thì trả về false
a >= b sẽ trả
về true
<= So sánh 2 toán hạng có nhỏ hơn hoặc bằng hay không. Nếu nhỏ hơn
hoặc bằng thì trả về true nếu lớn hơn thì trả về false
a <= b sẽ trả
về false
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 9
4- Không nên sử dụng các toán tử trên để so sánh các chuỗi với nhau vì bản
chất việc so sánh chuỗi là so sánh từng ký tự tương ứng với nhau và so sánh
ASCII của ký tự đó. Như vậy, ký tự ‘K’ != ‘k’. Để so sánh hai chuỗi
người ta thường dùng hàm so sánh chuỗi đã được hỗ trợ sẵn.
c. Toán tử logic
Giả sử mệnh đề A đúng mệnh đề B sai:
Toán tử Mô tả Ví dụ
&& Hay còn gọi là toán tử logic AND (và). Trả về true nếu tất cả toán
hạng đều mang giá trị true. Và trả về false nếu có ít nhất 1 toán
hạng mang giá trị false.
A && B kết quả
false
|| Hay còn gọi là toán tử logic OR (hoặc). Trả về true nếu có ít nhất
1 toán hạng mang giá trị true. Và trả về false nếu tất cả toán hạng
đều mang giá trị false.
A || B kết quả
true.
! Hay còn gọi là toán tử logic NOT (phủ định). Có chức năng đảo
ngược trạng thái logic của toán hạng. Nếu toán hạng đang mang giá
trị true thì kết quả sẽ là false và ngược lại.
!A kết quả
false
Lưu ý:
o Các toán tử &&|| có thể áp dụng đồng thời nhiều toán hạng, ví dụ như:
A && B && C || D || K (Thứ tự thực hiện sẽ được trình bày ở phần sau).
o Các toán hạng trong biểu thức chứa toán tử logic phải trả về true hoặc false.
d. Toán tử khởi tạo và gán
Toán tử khởi tạo và gán thường được sử dụng nhằm mục đích lưu lại giá trị
cho một biến nào đó. Một số toán tử khởi tạo và gán hay được sử dụng:
Toán tử Mô tả Ví dụ
= Gán giá trị của toán hạng bên phải cho toán hạng bên trái. K = 10 sẽ gán 10 cho
biến K
+= Lấy toán hạng bên trái cộng toán hạng bên phải sau đó gán
kết quả lại cho toán hạng bên trái.
K += 1 tương đương
với K = K + 1
-= Lấy toán hạng bên trái trừ toán hạng bên phải sau đó gán
kết quả lại cho toán hạng bên trái.
K -= 1 tương đương
với K = K – 1
*= Lấy toán hạng bên trái nhân toán hạng bên phải sau đó gán
kết quả lại cho toán hạng bên trái.
K *= 1 tương đương
với K = K * 1
/= Lấy toán hạng bên trái chia lấy phần nguyên với toán
hạng bên phải sau đó gán kết quả lại cho toán hạng bên trái.
K /= 1 tương đương
với K = K / 1
%= Lấy toán hạng bên trái chia lấy dư với toán hạng bên
phải sau đó gán kết quả lại cho toán hạng bên trái.
K %= 1 tương đương
với K = K % 1
Một số lưu ý khi sử dụng các toán tử trên:
1- Toán tử bên trái thường là một biến, còn toán tử bên phải có thể là biến, có
thể là biểu thức đều được.
2- Một phép toán gán hoặc khởi tạo có thể được sử dụng như là toán hạng bên
phải cho một phép gán hoặc khởi tạo khác.
Ví dụ:
int H, K, T;
H = K = T = 10;
Console.WriteLine(" H = {0}, K = {1}, T = {2}", H, K, T);
H += K = T = 5;
Console.WriteLine(" H = {0}, K = {1}, T = {2}", H, K, T);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 10
Phép toán H =K =T = 10 sẽ thực hiện gán 10 cho biến T sau đó gán giá trị
biến T cho biến K sau đó gán giá trị biến K cho biến H. Như vậy ta được cả 3
biến H K T đều có giá trị bằng 10.
Phép toán H += K = T = 5 sẽ thực hiện gán 5 cho biến T sau đó gán giá trị biến
T cho biến K sau đó lấy H + K gán kết quả cho biến H. Cuối cùng ta được H =
15, K = 5, T = 5.
e. Toán tử so sánh trên bit
Các toán tử so sánh trên bit cũng ít gặp nên mình chỉ giới thiệu qua cho các bạn
tham khảo thôi chứ chúng ta không giới thiệu rõ phần này.
Giả sử a có giá trị bằng 10 và b có giá trị bằng 9. Giá trị biến a đổi ra nhị phân là
1010 và giá trị biến b đổi ra nhị phân là 1001.
Toán tử Mô tả Ví dụ
& Sao chép bit 1 tới kết quả nếu nó tồn tại trong cả hai toán
hạng tại vị trí tương ứng, ngược lại thì bit kết quả bằng 0
a&b sẽ cho kết quả là 1000
tương đương với số 8 trong
hệ thập phân
| Sao chép bit 1 tới kết quả nếu nó tồn tại ở một trong
hai toán hạng tại vị trí tương ứng, ngược lại thì bit kết quả
bằng 0
a|b sẽ cho kết quả 1011
tương đương với số 11
trong hệ thập phân
^ Sao chép bit 1 tới kết quả nếu nó chỉ tồn tại ở một toán
hạng tại vị trí tương ứng, ngược lại thì bit kết quả bằng 0
a^b sẽ cho kết quả 0011
tương đương với số 3 trong
hệ thập phân
~ Dùng để đảo bit 0 thành 1 và ngược lại 1 thành 0 ~a sẽ cho kết quả 0101
<< Dịch trái n bit. Giá trị toán hạng bên trái sẽ được dịch
trái n bit với n được xác định bởi toán hạng bên phải
a<<2 sẽ cho kết quả
101000
>> Dịch phải n bit. Giá trị toán hạng bên trái sẽ được dịch
phải n bit với n được xác định bởi toán hạng bên phải
a>>2 sẽ cho kết quả 0010
Ngoài những toán tử trên, vẫn còn nhiều toán tử khác cũng được hay sử dụng
Toán tử Mô tả Ví dụ
sizeof() Trả về kích cỡ của một kiểu dữ liệu sizeof(int) sẽ trả về 4
typeof() Trả về kiểu của một lớp typeof(string) sẽ trả
về System.String
new Cấp phát vùng nhớ mới, áp dụng cho kiểu dữ liệu tham
chiếu
DateTime dt
= new DateTime()
is Xác định đối tượng có phải là một kiểu cụ thể nào đó hay
không. Nếu đúng sẽ trả về true ngược lại trả về false
as Ép kiểu mà không gây ra lỗi. Nếu ép kiểu không thành
công sẽ trả về null
? : Được gọi là toán tử 3 ngôi. Tương đương với cấu trúc điều
kiện Cú pháp:
(toán hạng 1) ? (toán hạng 2) : (toán hạng 3)
Ý nghĩa: trả về toán hạng 2 nếu toán hạng 1 là true và
ngược lại trả về toán hạng 3
(1 < 2) ? 1 : 0
kết quả là 1 vì toán hạng
1 là (1 < 2) là đúng nên trả
về toán hạng 2 là 1
, Sử dụng toán tử “,” để kết nối nhiều biểu thức lại với
nhau.
(t = 5, 2) sẽ duyệt qua biểu
thức 1 là t = 5, thực hiện
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 11
Cú pháp:
(biểu thức 1, biểu thức 2)
Ý nghĩa: Duyệt qua biểu thức 1 sau đó duyệt qua biểu
thức 2 và trả về giá trị của biểu thức 2
gán 5 cho t sau đó duyệt
qua biểu thức 2 là 2, cuối
cùng trả về giá trị là 2
2. Ví dụ chương trình sử dụng toán tử
Ví d 1: các phép toán c b nơ
static void Main(string[] args)
{
int a, b, c;
a = b = (c = 9) + 1; // khởi tạo giá trị: a = 10, b = 10, c = 9
a += b; // tương đương a = a + b
b = c++; // thực hiện gán giá trị c cho biến b sau đó thực hiện c = c + 1
--c; // thực hiện c = c - 1
Console.WriteLine(" a = {0}, b = {1}, c = {2}", a, b, c);
Console.ReadKey();
}
Ví d 2: k t h p các phép toán đ vi t ch ng trình ki m tra s ế ế ươ
nh p vào là s l .
static void Main(string[] args)
{
string strSoNguyen; // Biến chứa dữ liệu nhập vào từ bàn phím
int SoNguyen; // Biến chứa số nhập vào từ bàn phím
string KetQua; // Biến chứa kết quả kiểm tra số vừa nhập là chẵn hay lẻ
strSoNguyen = Console.ReadLine();
// Đọc dữ liệu nhập vào từ bàn phím (dữ liệu chuỗi) sau đó gán giá trị vào biến strSoNguyen
SoNguyen = Int32.Parse(strSoNguyen);
// Ép kiểu dữ liệu vừa nhập vào (dạng chuỗi) sang dạng số rồi gán giá trị vào biến SoNguyen
KetQua = (SoNguyen % 2 == 0) ? "so chan" : "so le";
// Sử dụng toán tử 3 ngôi để kiểm tra số chẵn lẻ
Console.WriteLine("{0} la {1}", SoNguyen, KetQua);
// In kết quả ra màn hình
Console.ReadKey();
}
Đầu tiên ta có 3 biến:
strSoNguyen: Chứa dữ liệu nhập vào từ bàn phím. Vì dữ liệu nhập vào từ bàn
phím mặc định là dạng chuỗi nên cần biến kiểu chuỗi để chứa giá trị.
SoNguyen: Chứa dữ liệu nhập vào từ bàn phím ở dạng số. Từ dữ liệu dạng chuỗi
của biến strSoNguyen ta ép kiểu sang kiểu số để dễ xử lý
KetQua: Chứa kết quả kiểm tra số vừa nhập là chẵn hay lẻ. Kết quả này ở dạng
chuỗi để có thể in ra màn hình luôn.
Tiếp theo ta nhận kết quả nhập từ bàn phím bằng lệnh Console.ReadLine() rồi gán
giá trị cho biến strSoNguyen.
Ép kiểu kết quả vừa nhập sang dạng số rồi gán giá trị vào biến SoNguyen.
Sử dụng toán tử 3 ngôi kiểm tra xem số vừa nhập chia hết cho 2 hay
không (nếu chia hết cho 2 thì phép chia lấy với 2 sẽ cho kết quả 0 biểu
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 12
thức SoNguyen % 2 == 0 s trả về true ngược lại sẽ trả về false). Nếu chia hết thì
trả về chuỗi “so chan” ngược lại trả về chuỗi “so le”..
VI. Hằng trong C#
1. Định nghĩa hằng
K/n: hằng là một biến những giá trị không thay đổi trong suốt chương trình. Hằng
bắt buộc phải khởi tạo hoặc khai báo.
Mục đích: Nhằm ngăn chặn việc gán giá trị khác vào biến. Hằng làm cho chương
trình dễ đọc hơn bằng cách biến những con số vô cảm thành những tên có nghĩa. Giúp
cho chương trình dễ nâng cấp, dễ sửa chữa, dễ tránh lỗi hơn. Nếu vô ý gán giá trị cho
một biến hằng ở đâu thì trình biên dịch sẽ báo lỗi ngay lập tức.
2. Có mấy loại hằng -- có 3 loại hằng
a. Giá trị hằng
Ta có câu lệnh gán sau: x = 10;
10 là giá trị hằng. Giá trị 10 luôn là 10 và không thể gán giá trị khác cho
10.
b. Biểu tượng hằng
Việc gán một tên cho giá trị hằng được xem là một biểu tượng hằng.
Xét lại câu lệnh: x = 10; thì x được xem là biểu tượng hằng
Cú pháp:
VD: const int a = 10;
Lưu ý:
Phải có từ khóa ‘const’ trước khai báo và phải khởi tạo giá trị ngay khi khai báo
c. Kiểu liệt kê
Kiểu liệt kê là tập hợp các tên hằng có giá trị không thay đổi (bài ENUM sẽ nói
rõ).
3. Cách sử dụng hằng
Vì bản chất hằng cũng là một biến nhưng giá trị không thay đổi nên hằng được sử
dụng tương tự như biến.
Một số lưu ý khi sử dụng hằng:
Hằng bắt buộc phải được khởi tạo giá trị ngay khi khai báo và không được thay
đổi giá trị trong suốt chương trình.
Giá trị của hằng được tính toán vào lúc biên dịch nên không thể gán trực tiếp giá
trị của biến vào hằng. Nếu muốn làm điều đó thì phải sử dụng từ khóa readonly
trước khai báo biến (readonly là từ khóa chỉ cho trình biên dịch biết rằng biến này
chỉ được đọc, lấy giá trị chứ không được gán giá trị)
Hằng bao giờ cũng static nhưng ta không được đưa từ khóa này vào khai báo
hằng.
VII.Ép kiểu trong C#
1. Khái niệm ép kiểu
Ép kiểu là biến đổi dữ liệu thuộc kiểu dữ liệu này thành kiểu dữ liệu khác. Để
chuyển dũ liệu sang một kiểu dữ liệu mong muốn phục vụ cho việc thao tác và xử lý.
Đưa dữ liệu về định dạng mà mình mong muốn (ví dụ chuyển từ kiểu số nguyên sang
kiểu chuỗi).
2. Có mấy loại ép kiểu? có 4 loại:
a. Chuyển đổi kiểu ngầm định (implicit)
Việc chuyển đổi này được thực hiện bởi trình biên dịch và chúng ta không
cần tác động gì.
Const <ki u d li u> <tên bi n> = <giá tr h ng>; ế
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 13
Được thực hiện khi chuyển kiểu từ:
Kiểu dữ liệu nhỏ sang kiểu dữ liệu lớn hơn
Chuyển từ lớp con đến lớp cha.
Ví dụ:
int intValue = 10;
long longValue = intValue;
// kiểu long có miền giá trị lớn hơn kiểu int nên có thể chuyển từ int sang long
float floatValue = 10.9f;
double doubleValue = floatValue;
//kiểu double có miền gái trị lớn hơn kiểu float nên có thể chuyển từ float sang double
b. Chuyển đổi kiểu tương minh (explicit)
Là việc chuyển kiểu một cách ro ràng và dùng từ kháo chỉ định chứ không
dùng phương thức.
Một số đặc điểm của chuyển đổi kiểu tường minh:
Thường được dùng để chuyển đổi giữa các kiểu dữ liệu có tính chất
tương tự nhau.
Hỗ trợ trong việc boxing và unboxing đổi tượng.
Cú pháp ngắn gọn do không sử dụng phương thức.
Chỉ khi chúng ta biết rõ biến đó có thuộc kiểu dữ liệu nào mới thực
hiện chuyển đổi. Ngược lại, có thể lỗi khi chạy chương trình.
Cú pháp:
<kiểu dữ liệu> là kiểu dữ liệu mình muốn chuyển sang.
<biến ép kiểu> là biến chứa dữ liệu cần ép kiểu.
Phải có cặp dấu ngoặc tròn.
Ý nghĩa:
Ép kiểu của <biến cần ép kiểu> về <kiểu dũ liệu> nếu thành công sẽ trả ra giá
trị kết quả. Ngược lại sẽ báo lỗi. Đặc biệt với số:
Ta có thực hiện ép kiểu dữ liệu lớn hơn về kiểu dữ liệu nhỏ hơn mà
không báo lỗi
Nếu dữ liệu cần ép kiểu vượt quá miền giá trị của kiểu dữ liệu muốn
ép kiểu về chương trình sẽ tự cắt bit cho phù hợp với khả năng chứa
kiểu dữ liệu đó (cắt từ bên trái qua).
Ví dụ:
int i = 300; // 300 có mã nhị phân là 100101100
byte b = (byte)i;
/* do kiểu byte có giới hạn đến giá trị 255 thôi nên không thể chứa số 300 được mà kiểu byte có
kích thước là 1 bytes tương đương 8 bit. Như vậy ta cần cắt mã nhị phân của số 300 về còn 8 bit là
được. Mã nhị phân 300 là 100101100 cắt từ trái qua 1 bit ta được 00101100 (đủ 8 bit) tương đương với
số 44. Cuối cùng biến b sẽ mang giá trị là 44.*/
Console.WriteLine(" i = " + i);
Console.WriteLine(" b = " + b);
VD: Nếu muốn thực hiện chia lấy phần nguyên củ 2 toán hạng đều là số
nguyên. Thì để được kết quả chính xác ta sẽ ép kiểu 1 trong 2 roán hạng đó về số
thực.
double d = 2 / 3;
// kết quả ra 0 vì 2 và 3 đều là số nguyên nên thực hiện 2 chia lấy phần nguyên với 3 được 0
double k = (double)2 / 3;
(<ki u d li u>) <bi n c n ép ế
ki u>
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 14
// Ép kiểu số 2 từ kiểu nguyên sang kiểu số thực. Như vậy kết quả phép chia sẽ ra số thực
double t = 1.0 * 2 / 3;
// Thực hiện nhân 1.0 với 2 mục đích để biến số 2 (số nguyên) thành 2.0 (số thực)
Console.WriteLine("d = {0} \n k = {1} \n t = {2}", d,k, t);
c. Sử dụng phương thức hỗ trợ sẵn
Phương thức Parse(), TryParse()
Parse()
Cú pháp:
<kiểu dữ liệu> là kiểu dữ liệu cơ bản mình muốn chuyển đổi sang.
<dữ liệu cần chuyển đổi> là dữ liệu thuộc kiểu chuỗi, có thể là biến kiểu chuỗi
hoặc giá trị hằng kiểu chuỗi.
Ý nghĩa:
Chuyển đổi một chuỗi sang một kiểu dữ liệu cơ bản tương ứng.
Phương thức trả về giá trị kết quả chuyển kiểu nếu chuyển kiểu thành công.
Ngược lại sẽ báo lỗi chương trình.
Ví dụ:
string stringValue = "10";
int intValue = int.Parse(stringValue);
// Chuyển chuỗi stringValue sang kiểu int và lưu giá trị vào biến intValue - Kết quả intValue = 10
double HowKteam = double.Parse("10.9");
// Chuyển chuỗi giá trị hằng "10.9" sang kiểu int và lưu giá trị vào biến HowKteam - Kết quả
HowKteam = 10.9
TryParse()
Cú pháp:
<kiểu dữ liệu>.TryParse(<dữ liệu cần chuyển đổi>, out <biến chứa kết quả>);
<kiểu dữ liệu> là kiểu dữ liệu cơ bản mình muốn chuyển đổi sang.
<dữ liệu cần chuyển đổi> là dữ liệu thuộc kiểu chuỗi, có thể là biến kiểu chuỗi
hoặc giá trị hằng kiểu chuỗi.
<biến chứa kết quả> là biến mà bạn muốn lưu giá trị kết quả sau khi chuyển kiểu
thành công. Từ khóa out là từ khóa bắt buộc phải có.
Ý nghĩa:
Chuyển một chuỗi sang một kiểu dữ liệu cơ bản tương ứng.
Phương thức sẽ trả về true nếu chuyển kiểu thành công và giá trị kết quả chuyển
kiểu sẽ lưu vào <biến chứa kết quả>. Ngược lại sẽ trả về false và <biến chứa kết
quả> sẽ mang giá trị 0.
Ví dụ:
int Result;
// Biến chứa giá trị kết quả khi ép kiểu thành công
bool isSuccess;
// Biến kiểm tra việc ép kiểu có thành công hay không
string Data1 = "10", Data2 = "Kteam";
// Dữ liệu cần ép kiểu
isSuccess = int.TryParse(Data1, out Result);
// Thử ép kiểu Data1 về int nếu thành công thì Result sẽ chứa giá trị kết quả ép kiểu và
isSuccess sẽ mang giá trị true. Ngược lại Result sẽ mang giá trị 0 và isSuccess mang giá trị
false
Console.Write(isSuccess == true ? " Success !" : " Failed !");
// Sử dụng toán tử 3 ngôi để in ra màn hình việc ép kiểu đã thành công hay thất bại.
Console.WriteLine(" Result = " + Result);
// In giá trị Result ra màn hình
isSuccess = int.TryParse(Data2, out Result);
<ki u d li u>. Parse(<d li u c n chuy n
đ i> );
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 15
// Tương tự như trên nhưng thao tác với Data2
Console.Write(isSuccess == true ? " Success !" : " Failed !");
// Tương tự như trên
Console.WriteLine(" Result = " + Result);
// Tương tự như trên
Vì Data1 có thể ép kiểu về int nên kết quả thành công và in giá trị ra. Còn Data2
không thể ép kiểu về kiểu int nên kết quả không thành công và in ra giá trị 0.
Lưu ý khi sử dụng Parse() và TryParse():
Tham số truyền vào phải là một chuỗi.
Cả 2 phương thức được gọi thông qua tên kiểu dữ liệu.
Parse() trả về giá trị kết quả ép kiểu nếu ép kiểu không thành công thì sẽ báo lỗi.
Còn TryParse() trả về xem ép kiểu có thành công hay không, giá trị kết quả ép
kiểu sẽ nằm trong <biến chứa kết quả>.
Ngoài TryParse() ra thì vẫn có một cách ép kiểu không báo lỗi chương trình. Đó là
sử dụng toán tử as:
- Toán tử as dùng để “Ép kiểu mà không gây ra lỗi. Nếu ép kiểu không
thành công sẽ trả về null”.
- Chỉ áp dụng cho việc chuyển kiểu giữa các kiểu tham chiếu tương thích
(thường là các kiểu có quan hệ kế thừa) hoặc các kiểu nullable (là các kiểu
có thể chứa giá trị null).
Lớp hỗ trợ sẵn (Convert)
Convert là lớp tiện ích được C# hỗ trợ sẵn cung cấp cho chúng ta nhiều phương
thức chuyển đổi kiểu dữ liệu.
Các đặc điểm của các phương thức trong lớp Convert:
Tham số truyền vào của các phương thức không nhất thiết là chuỗi (có thể
là int, bool, . . .).
Nếu tham số truyền vào là null thì các phương thức sẽ trả về giá trị mặc
định của kiểu dữ liệu.
Các trường hợp tham số truyền vào sai định dạng hoặc vượt quá giới hạn
thì chương trình sẽ báo lỗi như phương thức Parse().
Một số phương thức chuyển kiểu mà Convert có:
Các phương thức chuyển kiểu trong lớp Convert đều có thể nhận các kiểu
dữ liệu cơ bản (int, bool, byte, . . .) làm tham số truyền vào.
Ở bảng trên mình chỉ liệt kê những phương thức hay dùng. Để tìm hiểu
thêm các phương thức chuyển kiểu khác các bạn có thể sử dụng tính năng
gợi ý của Visual Studio:
Đầu tiên các bạn gõ “Convert.” (lưu ý dấu chấm liền sau Convert)
Visual Studio sẽ hiển thị ra tất cả các phương thức có trong lớp Convert.
Giờ bạn chỉ việc lựa chọn phương thức muốn sử dụng là được.
d. Người dùng tự định nghĩa kiểu chuyển đổi
Khi chúng ta tạo ra một kiểu dữ liệu mới chúng ta cũng cần định nghĩa các
chuyển đổi kiểu dữ liệu từ kiểu cơ bản sang kiểu tự định nghĩa và ngược lại.
VIII. Cấu trúc rẽ nhánh If else trong C#
1. Cấu trúc if … else … dạng thiếu
Cú pháp:
- if là từ khóa bắt buộc
- <biểu thức điêu kiện> là biểu thức dạng boolean (trả về true or false).
if ([biểu thức điều kiện]) <câu lệnh thực hiện>
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 16
- <câu lệnh thực hiện> là câu lệnh muốn thực hiện nếu <biểu thức điều kiện>
là đúng.
Ý nghĩa:
Nếu <biểu thức điều kiện> trả về true thì thực hiện <câu lệnh thực hiện> ngược lại
thì không làm gì cả.
Ví dụ:
string K = “Kteam”;
if (K == “Kteam”);
// biểu thức điều kiện sử dụng toán tử == để sánh xem giá trị biến K có bằng
Kteam hay không. Nếu bằng thì trả về true ngược lại trả về false
Console.WriteLine(“đúng”);
// in ra man hình chữ “đúng” nếu biểu thức trên là đúng.
2. Cấu trúc if … else … dạng đủ
Cú pháp:
- if else là từ khóa bắt buộc.
- <biểu thức điều kiện> là biểu thức dạng boolean.
- <câu lệnh thực hiện 1> là câu lệnh muốn thực hiện nếu điều kiện đúng
- <câu lệnh thực hiện 2> là câu lệnh muốn thực hiện nếu điều kiện sai.
Ý nghĩa:
Nếu <biểu thức điều kiện > trả về true thì thực hiện <câu lệnh thực hiện 1>, ngược
lại <câu lệnh thực hiện 2>.
Ví dụ:
string K = "Kteam";
if (K == "Kteam") // Nếu giá trị K bằng “Kteam” thì
Console.WriteLine("Free Education");
// In ra màn hình “Free Education”
else // Ngược lại thì
Console.WriteLine("Connecting to HowKteam. . .");
// In ra màn hình “Connecting to HowKteam. . .”
3. Một số lưu ý khi sử dụng
<biểu thức điều kiện> có thể chứa nhiều biểu thức con bên trong và các biểu thức
con liên kết với nhau bằng các toán tử quan hệ nhưng tất cả phải trả về kiểu boolean.
Nếu muốn thực hiện nhiều câu lệnh thì có thể nhóm chúng vào trong cặp dấu { }.
Ví dụ:
if <biểu thức điều kiện>
{
<câu lệnh thực hiện>;
}
Trong câu lệnh có thể chứa một câu lệnh điều kiện con nữa.
Ví dụ:
if <bi u th c đi u ki n 1>
{
if <bi u th c đi u ki n 2>
{
<câu l nh th c hi n 1>;
if <biểu thức điều kiện>
<câu lệnh thực hiện 1>;
else
<câu lệnh thực hiện 2>;
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 17
}
elsse
{
<câu l nh th c hi n 2>;
}
<câu l nh th c hi n 3>;
}
else
{
if (bi u th c đi u ki n 3)
{
<câu l nh th c hi n 4>;
}
if (bi u th c đi u ki n 4)
{
<câu l nh th c hi n 5>;
}
<câu l nh th c hi n 6>;
}
Các biểu thức điều kiện được kiểm tra từ trên xuống dưới và không kiểm tra lại.
Nếu biểu thức điều kiện đang kiểm tra trả về true thì:
- Thực hiện khối lệnh trong đó.
- Thoát khỏi cấu trúc
- Không kiểm tra các điều kiện còn lại.
So với toán tử 3 ngôi thì:
- Câu lệnh điều kiện nhìn trực quan hơn và có thẻ thực hiện nhiều câu lệnh hơn.
- Nhưng nếu chỉ thực hiện một câu lệnh điều kiện đơn giản thì dùng toán tử 3
ngôi sẽ làm code ngắn gọn và viết nhanh hơn.
VD: Phương trình Ax+B=0
Console.Write(" Moi nhap so A: ");
strA = Console.ReadLine();
Console.Write(" Moi nhap so B: ");
strB = Console.ReadLine();
if (int.TryParse(strA, out A) == false ||
int.TryParse(strB, out B) == false)
// kiểm tra người dùng có thực sự nhập số nguyên vào hay không. Nếu ép kiểu
thành công sẽ trả về true, ngược lại trả về false
{
Console.WriteLine (“Du lieu nhap sai !”);
return; // Lệnh này tạm hiểu là dừng và thoát chương trình mà không
thực //hiện những câu lệnh sau nó nữa. Sẽ được tìm hiểu chi tiết trong bài 16
Hàm
}
else
{
Console.WriteLine("\n Phuong trinh cua ban vua nhap la:
{0}x + {1} = 0", A, B);
if (A == 0)
{
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 18
Console.WriteLine("\n Phuong trinh co vo so nghiem !");
}
else if (B == 0)
{
Console.WriteLine("\n Phuong trinh co nghiem x = 0");
}
else
{
Nghiem = (double)-B / A; // Ép kiểu để cho ra kết quả chính xác
Console.WriteLine("\n Phuong trinh co nghiem x = {0}",
Nghiem);
}
}
IX. Cấu trúc rẽ nhánh Switch case trong C#
1. Cấu trúc switch case dạng thiếu
Cú pháp:
Trong đó:
Switch case là từ khóa bắt buộc.
Break là một lệnh nhảy
- Ý nghĩa của nó là thoát ra khỏi cấu trúc, vong lặp chứ nó.
- Ngoài break vẫn còn lệnh nhảy khác như goto nhưng ít khi được sử dụng.
<biểu thức> phải là biểu thức trả về kết quả kiểu:
- Số nguyên (int, long, byte, …)
- Ký tự hoặc chuỗi (char, string)
- Kiểu liệt kê (enum)
<giá trị thứ i> với i = 1..n là giá trị muốn so sánh với giá trị của <biểu
thức>.
<câu lệnh thứ i> với I = 1..n là giá trị muốn thực hiện khi <giá trị thứ i>
tương ứng với giá trị của biểu thức <biểu thức>
Ý nghĩa: duyệt lần lượt từ tên xuống dưới và kiểm tra xem gá trị của <biểu thức> có
bằng với <giá trị thứ i> đang xét hay không. Nếu bằng thì thức hiện <câu lệnh thứ i>
tương ứng.
Lưu ý:
<giá trị thứ i> phải có kiểu dữ liệu giống kiểu dữ liệu của giá trị của biểu thức.
<câu lệnh thứ i> có thể gồm nhiều câu lệnh và không nhất thiết phải đặt trong
cặp dấu ngoặc nhọn { } nhưng tốt hơn nên đặt trong dấu { } để code được rõ
ràng hơn.
Nếu case đang xét không rỗng (có lệnh để thực hiện) thì bắt buộc phải có lệnh
nhảy (break) sau đó.
Ví dụ:
int k = 8;
switch (k)
{
switch (<biểu thức>)
{
case <giá trị thứ 1>:<câu lệnh thứ 1>;
break;
… …
case <giá trị thứ n>:<câu lệnh thứ n>;
break;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 19
case 3:
Console.WriteLine("HowKteam");
break; //Vì case này có lệnh thực hiện nên phải có lệnh break
case 9: //case này rỗng (không có lệnh thực hiện) nên không cần lệnh break
case 10:
Console.WriteLine("Free Education");
break;
}
Lưu đồ sau sẽ minh họa cách thức hoạt động của cấu trúc switch case dạng thiếu:
Chú ý là trường hợp không có lệnh break đồng nghĩa case đó rỗng.
Đối với case cuối cùng dù có câu lệnh để thục hiện hay không vẫn phải có
lệnh break để thoát khỏi cấu trúc.
Ví dụ:
int k = 10;
switch (k) // giá trị biểu thức là giá trị của biến k (kiểu số nguyên)
{
case 3: // các giá trị so sánh cũng là kiểu số nguyên
Console.WriteLine("HowKteam"); // lệnh thực hiện nếu k = 3
break; // lệnh thoát ra khỏi cấu trúc
case 9:
Console.WriteLine("Kteam"); // tương tự
break;
case 10:
Console.WriteLine("Free Education"); // tương tự
break;
}
2. Cấu trúc switch case dạng đủ
switch (<bi u th c>)
{
case <giá tr th 1>: <câu l nh th 1>;
break;
case <giá tr th 2>: <câu l nh th 2>;
break;
. . .
case <giá tr th n>: <câu l nh th n>;
break;
default: <câu l nh m c đ nh>;
break;
}
Vào cấu trúc
Biểu thức
<Câu lệnh thứ 1>
<Câu lệnh thứ 2>
Bằng <giá trị 2>
Bằng <giá trị 1>
Không có lệnh break
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 20
Cú pháp:
Trong đó:
switch, case, default là từ khóa bắt buộc.
<biểu thức> phải là biểu thức trả về kết quả kiểu:
- Số nguyên (int, long, byte, . . .)
- Ký tự hoặc chuỗi (char, string). Kiểu liệt kê (enum)
<giá trị thứ i> với i = 1..n là giá trị muốn so sánh với giá trị của <biểu thức>.
<câu lệnh thứ i> với i = 1..n là câu lệnh muốn thực hiện khi <giá trị thứ i> tương
ứng bằng với giá trị của <biểu thức>.
<câu lệnh mặc định> là câu lệnh sẽ được thực hiện nếu giá trị <biểu thức> không
bằng với <giá trị thứ i> nào.
Ý nghĩa: duyệt lần lượt từ trên xuống dưới và kiểm tra xem giá trị của <biểu
thức> có bằng với <giá trị thứ i> đang xét không. Nếu bằng thì thực hiện <câu lệnh
thứ i> tương ứng. Nếu không bằng các <giá trị thứ i> thì sẽ thực hiện <câu lệnh mặc
định>.
Về cơ bản cách thức hoạt động của 2 cấu trúc switch. . . case dạng đủ và dạng
thiếu là như nhau, chỉ khác nhau ở một diểm là dạng đủ có thêm dòng default. . . xem
lại lưu ý của dạng thiếu để tránh mắc lỗi.
Ví dụ:
int k = 8;
switch (k)
{
case 3:
Console.WriteLine("HowKteam");
break;
case 9:
Console.WriteLine("Kteam");
break;
case 10:
Console.WriteLine("Free Education");
break;
default: // Nếu không thỏa các trường hợp trên sẽ thực hiện lệnh sau đây
Console.WriteLine("Connecting to
HowKteam. . .");
break;
}
K t qu : Connecting to HowKteam. . .ế
Vì không tìm thấy case nào có giá trị bằng với giá trị biến k nên sẽ thực hiện câu
lệnh trong default. Do đó màn hình in ra “Connecting to HowKteam…”.
Lưu đồ minh họa cách hoạt động của cấu trúc switch … case … default…
Thoát
cấu trúc
<Câu lệnh thứ n>
<Câu lệnh mặc
định>
Không có lệnh break
Bằng <giá trị n>
Không có lệnh break
. . .
Không có lệnh break
109
0xfff
Heap
stack
0xcc
c
boxing
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 21
X. Kiểu dữ liệu Object trong C#
1. Khái niệm về kiểu dữ liệu object
Kiểu dữ liệu object
Là một kiểu dữ liệu cơ bản của tất cả các kiểu dữ liệu trong .NET.
Mọi kiểu dữ liệu đều được kế thừa từ System.Object.
Thuộc kiểu dữ liệu tham chiếu.
Kiểu dữ liệu object cung cấp một số phương thức ảo cho phép overload để sử dụng.
Một số phương thức củ object:
Phương thức Ý nghĩa
ToString()
Trả về kiểu chuỗi của đối tượng (chuyển từ kiểu dữ liệu khác về chuỗi)
GetHashCode(
)
Trả về mã băm của đối tượng.
Equals()
So sánh 2 đối tượng và trả về true khi 2 đối tượng có giá trị bằng nhau,
ngược lại trả về false.
GetType()
Trả về kiểu dữ liệu của đối tượng
2. Boxing và unboxing
Boxing là quá trình chuyển dữ liệu từ kiểu dữ liệu giá trị về kiểu dữ liệu tham
chiếu.
Quá trình boxing:
- Khởi tạo một đối tượng trong vùng nhớ heap.
- Coppy giá trị của biến có kiểu dữ liệu vào đối tượng này.
Quá trình boxing được thực hiện ngầm định.
Mỗi kiểu dữ liệu đều kế thừa từ lớp System.Object nên có thể gán giá trị của
kiểu dẫn xuất cho kiểu dữ liệu cha. Trong ví dụ là biến kiểu int gán giá trị
cho biến kiểu object
Ví dụ:
// khởi tạo một biến Value kiểu int (kiểu dữ liệu giá trị)
int Value = 109;
/* thực hiện boxing bằng cách:
Khởi tạo đối tượng ObjectValue kiểu object
Gán giá trị của biến Value vào ObjectValue */
object ObjectValue = Value;
Unboxing làquá trình ngược lại với boxing, tức là đưa dữu liệu từ kiểu tham chiếu
về kiểu dữ liệu giá trị.
Unboxing được thực hiện tường minh và thông qua cách ép kiểu tường minh.
Phải chắc chắn rằng đối tượng cần boxing thuộc đúng kiểu dữ liệu đưa ra. Nếu
không việc unboxing sẽ báo lỗi chương trình.
Quá trình unboxing:
Kiểm tra xem đối tượng cần unboxing thuộc đúng kiểu dữ liệu đưa ra hay không.
Nếu đúng thì thực hiện coppy giá trị của đối tượng sang biến dữ liệu kiểu giá trị.
Ngược lại thì thông báo lỗi.
Ví dụ:
int Value = 109;
// boxing
object ObjectValue = Value;
/* thực hiện unboxing bằng cách:
Khởi tạo đối tượng ObjectValue thấy thuộc đúng kiểu int.
Gán giá trị của ObjectValue vào biến NewValue bằng cách ép kiểu tường minh.
Biến NewValue sẽ mang giá trị là 109 */
int NewValue = (int)ObjectValue;
109
0xfff
109
Unboxing
int NewValue = (int)ObjectValue;
Object ObjectValue = Value;
int Value = 109;
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 22
Từ khóa var
Var là từ khóa hỗ trợ khai báo biến mà không cần kiểu dữ liệu, kiểu dữ liệu sẽ được
xác định khi gán giá trị cho biến, lúc đó chuowg trình sẽ tự ép kiểu cho biến.
Những lưu ý khi sử dụng var:
- Bắt buộc phải gán giá trị ngay khi khởi tạo biến và không thể khởi tạo giá trị
null cho biến var.
- Var chỉ là từ khóa dùng để khai báo biến không phải là một kiểu dữ liệu.
Khai báo biến bằng từ khóa var thường được dùng trong:
- Duyệt mảng bằng foreach.
- Truy vấn LinQ.
Kết quả khi chạy chương trình
XI. Từ khóa Dynamic trong C#
1. Từ khóa dynamic
Từ khóa dynamic là từ khóa dùng để khai báo kiểu dynamic. Kiểu dynamic là
một khái niệm mới được đưa vào C# 4.0.
Cú pháp:
// Lỗi vì chưa khởi tạo giá trị cho biến varInt.
var varInt;
// Lỗi vì không được khởi tạo giá trị null cho biến
varString.
var varString = null;
// Lỗi vì phải khởi tạo giá trị ngay khi khai báo
var varLong;
varLong = 109;
// Khai báo đúng!
var varBool = true;
/* Vì biến StringVariable được khởi tạo giá trị kiểu chuỗi
* nên trình biên dịch sẽ hiểu biến này như là biến kiểu
string.*/
var varString = "HowKteam";
// Khai báo tường minh biến kiểu string
string Content = "HowKteam";
// In giá trị của biến StringVariable và biến Content
Console.WriteLine(varString);
Console.WriteLine(Content);
Dynamic <tên bi n>;ế
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 23
Trong đó:
- Dynamic là từ khóa.
- <tên biến> là tên do người dùng đặt.
Đặc điểm:
Các đối tượng thuộc kiểu dynamic sẽ không xác định được kiểu cho đến khi
chương trình thực thi. Tức là trình biên dịch sẽ bỏ qua tất cả lỗi về cú pháp, việc
kiểm tra này sẽ thực hiện khi chương trình thực thi.
Khi chạy chương trình sẽ nhận được các lỗi sau:
- Hỗ trợ dynamic programming (lập trình động) cho ngôn ngữ lập trình sử
dụng kiểu dữ liệu tĩnh như C#.
- Giúp cải thiện khả năng tương thích với các ngôn ngữ lập trình và nền tảng
(frameworks) động.
- Giúp việc code đơn giản và nhanh hơn.
- Có thể ép kiểu qua lại với các kiểu dữ liệu khác một cách bình thường.
Ví dụ chương trình sử dụng dynamic:
// Khai báo biến StringValue kiểu dynamic và khởi tạo giá trị là một chuỗi kiểu string
dynamic StringValue = "HowKteam";
/* Chúng ta biết rằng kiểu chuỗi không hỗ trợ toán tử ++
* Nhưng câu lệnh StringValue++ vẫn không báo lỗi là do ở thời điểm hiện tại trình biên dịch vẫn
chưa xác định kiểu dữ liệu cho biến StringValue
* Khi chạy chương trình thì lúc này C# mới phát hiện biến StringValue là kiểu string và không thể
thực hiện toán tử ++ lúc đó sẽ xuất hiện lỗi*/
StringValue++;
// Khai báo 2 biến Name và Mission kiểu string và khởi tạo giá trị.
string Name = "HowKteam ";
string Mission = "Free Education";
/* Thực hiện gán 1 biến kiểu string cho biến kiểu dynamic bằng cách ép kiểu ngầm định
(implicit)
* Sau phép gán này DynamicValue chứa "Free Education" nhưng kiểu dữ liệu của nó chưa được
xác định.*/
dynamic DynamicName = Name;
// Thực hiện cộng chuỗi và in ra màn hình bình thường
Console.WriteLine("Mission of " + DynamicName + " is " +
Mission);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 24
Kết quả:
2. Phân biệt object, var, dynamic
Về khái niệm thì:
Object là kiểu dữ liệu cơ bản của tất cả kiểu dữ liệu trong C#.
Var là một từ khóa để khai báo một cách ngầm định kiểu dữ liệu và kiểu
anonymous (kiểu anonymous sẽ được trình bày ở những bài sau).
Dynamic là một từ khóa để khai báo kiểu dynamic. Kiểu dynamic cũng có thể
tương tác với mọi kiểu dữ liệu nhưng khác object, biến kiểu dynamic chỉ được
xác định kiểu dữ liệu khi chương trình thực thi.
Chúng ta cùng phân biệt object, var và dynamic qua bảng tổng hợp sau
Đặc điểm Object Var Dynamic
Là một kiểu dữ liệu Phải Về bản chất thì var và dynamic đều là
từ khóa không phải kiểu dữ liệu
Phải khởi tạo giá trị
khi khai báo
Không bắt buộc Bắt buộc Không bắt buộc
Sử dụng để làm kiểu
trả về hoặc tham số
cho hàm
Không
Có khả năng ép kiểu
qua lại với các kiểu
dữ liệu khác
Không
Thời điểm xác định
kiểu dữ liệu thực sự
Là một kiểu dữ liệu
nên không cần xác
định gì nữa
Xác định ngay tại
khai báo thông qua
giá trị được gán vào
Xác định khi
chương trình
thực thi
XII.Vòng lập For trong C#
1. Cú pháp
Trong đó:
- Các phần [khởi tạo]; [điều kiện lăp]; [bước lặp lại] hoàn toàn có thể trống.
for ([Kh i t o]; [Đi u ki n l p]; [B c l p ướ
l i])
{
// Kh i l nh đ c l p l i. Có th b tr ng ượ
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 25
- Mỗi đoạn [khởi tạo]; hay [điều kiện lặp]; [bước lặp lại] là một câu lệnh riêng.
Tiến trình:
Ban đầu trình biên dịch sẽ di vào phần khởi tạo chạy đoạn lệnh khởi tạo.
Tiếp theo kiểm tra điều kiện lặp. Rồi thực hiện khối code bên trong vòng lặp for.
Khi đến ký hiệu } thì sẽ quay lên bước lặp lại.
Sau đó lại kiểm tra điều kiện lặp rồi tiếp tục thực hiện đoạn code trong khối lệnh.
Đến khi điều kiện lặp không còn thõa mãn thì sẽ kết thúc vòng lặp for.
Trường hợp khác:
Trong đó:
o Vòng lặp for này trở thành vòng lặp vô tận.
o Lưu ý dấu ; vẫn phải có.
2. Khởi tạo
Khi bắt đầu vào đoạn code của vòng lặp for, đoạn lệnh này sẽ được chạy đầu tiên.
Và chỉ được gọi duy nhất một lần trong vòng đời của vòng lặp for.
Ví dụ:
- Kết quả màn hình xuất ra một loạt giá trị 0 vì i = 0 được khởi tạo tại phần khởi tạo
của vòng lặp for và vòng lặp for này không có Điều kiện lặp nên chương trình sẽ
chạy vô tận.
- Ở trường hợp này i được gọi là biến đếm (thuật ngữ lập trình dùng cho một biến
có tác dụng tăng giá trị lên mỗi lần lặp lại).
Ví dụ 2: Chúng ta không nhất thiết phải khai báo môt biến ngay tại vị trí khởi tạo.
Ta có thể chỉ gán giá trị hoặc không làm gì cả (bỏ trống).
for (; ;) // l u ý d u ;ư
{
// Kh i l nh đ c l p l i. Có th b tr ng ượ
}
static void Main(string[] args)
{
// i sẽ được khởi tạo lần đầu tiên tại vòng lặp for
// khi vòng đời của vòng lặp for kết thúc bộ nhớ của biến i sẽ được giải phóng
// hay nói cách khác i là biến cục bộ của vòng lặp for
for (int i = 0; ; )
{
Console.WriteLine(i);
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
// i được gán giá trị bằng 0
// i lúc này không phải biến cục bộ của
vòng lặp for
// i thuộc hàm main
for (i = 0; ; )
{
Console.WriteLine(i);
}
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 26
Ví dụ 3: chỉ có thể có duy nhất 1 câu lệnh khởi tạo trong vòng lặp
Hay
3. Điều kiện lặp
Điều kiện lặp là một biểu thức logic với kết quả trả về bắt buộc là true hoặc false
(có thể bỏ trống sẽ trả về kết quả là true).
Điều kiện lặp là dòng lệnh thứ 2 vòng for sẽ chạy vào khi chạy lần đầu tiên (Khởi
tạo chạy trước). Từ lần lặp thứ 2 của vòng for, Điều kiện lặp cũng là dòng lệnh thứ 2
được chạy (sau bước lặp lại). (Cứ nhớ là luôn đứng thứ 2).
Khi câu điều kiện lặp không thỏa mãn (kết quả false) thì vòng lặp for kết thúc
Có thể thấy điều kiện lặp của vòng lập này luôn là true, nên vòng lặp sẽ lặp vô
tận.
Để giải quyết vấn đề này và cho vòng lặp kết thúc khi thỏa mãn điều kiện lặp.
Chúng ta thêm một đoạn code i++; ngay dưới đoạn code Console.WriteLine(i).
for static void Main(string[] args)
{
int i;
// lỗi vì chỉ được phép có duy nhất một dòng lệnh khởi tạo trong vòng lặp for
for (i = 0, int j = 0; ; )
{
Console.WriteLine(i);
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
// lỗi vì chỉ được phép có duy nhất một dòng lệnh khởi tạo trong vòng lặp for
for (i = 0; int j = 0; ; )
{
Console.WriteLine(i);
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
// vòng lặp for này vẫn lặp vô tận vì không bao giờ
//thỏa mãn điều kiên dừng
// i luôn == 0
// Điều kiện lặp luôn là true
for (i = 0; i < 10;)
{
Console.WriteLine(i);
}
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 27
Kết quả màn hình xuất ra các giá trị số nguyên từ 0 đến 9 (10 lần). Chứng tỏ vòng
lặp đã kết thúc sau 10 lần lặp (không còn lặp vô tận).
Lưu ý:
- Giá trị in ra từ 0 đến 9 chứ không phải đến 10. Vì Điều kiện lặp là i < 10 (10 ==
10 nên câu điều kiện là false và kết thúc vòng lặp. Vẫn thỏa mãn lặp 10 lần).
- Sau mỗi lần lặp giá trị i lại tăng lên 1 đơn vị. Sau 11 lần thì giá trị i == 10, không
còn thỏa mãn Điều kiện lặp nữa nên vòng lặp kết thúc.
- Các bạn có thể xem bảng thử dưới đây:
Lần 1 2 3 4 5 6 7 8 9 10 11
i 0 1 2 3 4 5 6 7 8 9 10
i<1
0
TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRU
E
FALSE
Bạn hoàn toàn có thể để giá trị true hoặc false vào phần điều kiện lặp (bỏ trống
mặc định là true). Hoặc một biểu thức logic phức tạp nhưng kết quả cuối cùng trả
về là true hoặc false.
Hay
static void Main(string[] args)
{
int i;
for (i = 0; i < 10;)
{
Console.WriteLine(i);
i++;
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
for (i = 0; (i % 3 == 0) && (i < 10);)
{
Console.WriteLine(i);
i++;
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
for (i = 0; true;)
{
Console.WriteLine(i);
i++;
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
for (i = 0; false;)
{
Console.WriteLine(i);
i++;
}
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 28
4. Bước lặp lại
Như ví dụ trên ta thấy. Mỗi lần muốn tăng giá trị của i ta phải dùng môt đoạn
lệnh i++ ; ở cuối khối lệnh. Vậy trường hợp bất cứ khi nào lặp lại ta cũng cần thực
thi đoạn lệnh i++ ; thì sao? Để tiện hơn cho việc code. Chúng ta có một phần tiếp
theo để tìm hiểu. Đó là bước lặp lại.
Xét đoạn code sau:
Chúng ta có thể thực hiện nhiều bước lặp
static void Main(string[] args)
{
int i;
for (i = 0; i < 10; i++)
{
Console.WriteLine(i)
}
Console.ReadKey();
}
Ta có thể viết gọn lại bằng cách đưa
i++; vào phần bước lặp lại của for
static void Main(string[] args)
{
int i;
for (i = 0; i < 10;)
{
Console.WriteLine(i);
i++;
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
int j = 0;
for (i = 0; i < 10; i++, j += 3,
Console.WriteLine("Tăng"))
{
Console.WriteLine(i);
}
Console.ReadKey();
}
Ta thấy đoạn i++j += 3 được
cách nhau bởi dấu phẩy (,)
Với mỗi đoạn lệnh trong bước
lặp. Chúng đươc phân cách nhau bởi
dấu phẩy (,).
Lưu ý: Đoạn code trong bước lặp
còn có thể thêm
cả Console.WriteLine("Tăng") vào
(khuyến cáo không nên). Nhưng
không thể thực hiện đoạn code có
chứa từ khóa (như if, for …).
static void Main(string[] args)
{
int i;
int j = 0;
for (i = 0; i < 10; i++, j += 3)
{
Console.WriteLine(i);
}
Console.ReadKey();
}
static void Main(string[] args)
{
int i;
int j = 0;
// lỗi đoạn , if (i % 2 == 0) j += 3
for (i = 0; i < 10; i++, if (i % 2 == 0) j += 3)
{
Console.WriteLine(i);
}
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 29
Không thể thêm câu điều kiện
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 30
XIII. Vòng lập While trong C#
Cú pháp:
- Điều kiện lặp là một biểu thức logic bắt buộc phải có với kết quả trả về bắt
buộc là true hoặc false.
- Từ khóa while biểu thị đây là vòng lặp while. Các câu lệnh trong khối lệnh sẽ
được lặp lại đến khi không còn thỏa mãn điều kiện lặp sẽ kết thúc vòng lặp
while.
- Tiến trình:
Đầu tiên: trình biên dịch sẽ đi vào dòng while (<điều kiện lặp>). Kiểm tra
điều kiện lặp có thỏa mãn hay không. Nếu tất cả là true thì sẽ đi vào bên trong
thực hiện khối code. Quá trình chỉ kết thúc khi điều kiện lặp là false.
Điều kiện lặp luôn bằng true. Thì vòng lặp while sẽ lặp vô tận.
Điều kiện luôn bằng false thì vòng lặp không thực thi.
Các ví dụ sử dụng while:
1- In ra một ma trận số:
Kết quả của
chương
trình:
while (<Đi u ki n l p>)
{
// kh i l nh l p l i
}
static void Main(string[] args)
{
int countLoop = 0;
int countLoopTime = 0;
int valueNum = 10;
int loopTime = 5;
// Vẽ từ trên xuống LoopTime lần
while (countLoopTime < loopTime)
{
// reset lại biến countLoop về 0 để viết lại từ đầu
countLoop = 0;
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum)
{
// in ra giá trị của countLoop trong 8 vị trí
Console.Write("{0,8}", countLoop);
// tăng giá trị của biến countLoop lên một đơn vị
countLoop++;
}
// Mỗi khi hoàn thành một vòng lặp nhỏ thì lại xuống dòng chuẩn vị vẽ lần tiếp theo
Console.WriteLine();
// tăng giá trị countLoopTime lên một đơn vị
countLoopTime++;
}
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 31
2- In ra một ma trận số có giá trị ngẫu nhiên:
Kết quả chương trình
XIV. Vòng lập Do While trong C#
Cú pháp:
static void Main(string[] args)
{
int countLoop = 0;
int countLoopTime = 0;
int valueNum = 10;
int loopTime = 5;
int minRandomValue = 0;
int maxRandomValue = 100;
Random rand = new Random();
// Vẽ từ trên xuống LoopTime lần
while (countLoopTime < loopTime)
{
// reset lại biến countLoop về 0 để viết lại từ đầu
countLoop = 0;
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum)
{
// giá trị sinh ngẫu nhiên trong khoảng [minRandomValue . .. maxRandomValue - 1]
int showValue = rand.Next(minRandomValue,
maxRandomValue);
// in ra giá trị của showValue trong 8 vị trí
Console.Write("{0,8}", showValue);
// tăng giá trị của biến countLoop lên một đơn vị
countLoop++;
}
// Mỗi khi hoàn thành một vòng lặp nhỏ thì lại xuống dòng chuẩn vị vẽ lần tiếp theo
Console.WriteLine();
// tăng giá trị countLoopTime lên một đơn vị
countLoopTime++;
}
Console.ReadKey();
}
do
{
// kh i l nh l p lai
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 32
- Điều kiện lặp: là một biểu thức logic bắt buộc phải có với kết quả trả về bắt buộc
là true hoặc false.
- Từ khóa do while biểu thị đây là một vòng lặp do while. Các câu lệnh trong khối
lệnh được lặp lại đến khi không còn thỏa mãn điều kiện sẽ kết thúc vòng lặp do
while.
- Tiến trình:
o Đầu tiên trình biên dịch sẽ đi vào dòng do và thực hiện khối lệnh bên trong.
Sau đó khi gặp ký tự } sẽ kiểm tra điều kiện lặp thỏa mãn hay không. Nếu
kết quả là true thì sẽ quay lại ký tự { thực hiện khói code. Quá trình chỉ kết
thúc khi điều kiện lặp là false.
o Điều kiện lwpj luôn bằng true thì vòng lặp while sẽ trở thành vòng lặp vô
tận.
o Điều kiện lặp luôn bằng false thì vòng lặp không được thực thi.
Lưu ý: vòng lặp do while sẽ thực hiện câu lệnh trong khối code xong rồi mới
kiểm tra điều kiện lặp. Cuối vòng lặp do while có dấu ; ở cuối.
static void Main(string[] args)
{
int countLoop = 0;
int countLoopTime = 0;
int valueNum = 10;
int loopTime = 5;
// Vẽ từ trên xuống LoopTime lần
do
{
countLoop = 0; // reset lại biến countLoop về 0 để viết lại từ đầu
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum)
{
Console.Write("{0,8}", countLoop);// in giá trị của countLoop trong 8 vị trí
countLoop++;// tăng giá trị của biến countLoop lên một đơn vị
}
// Mỗi khi hoàn thành một vòng lặp nhỏ thì xuống dòng chuẩn vị vẽ lần tiếp theo
Console.WriteLine();
countLoopTime++;// tăng giá trị countLoopTime lên một đơn vị
} while (countLoopTime < loopTime) ;
Console.ReadKey();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 33
XV. Cấu trúc hàm cơ bản trong C#
1. Cú pháp khai báo hàm
Trong đó:
[từ khóa 1], [từ khóa 2], [từ khóa n] là các từ khóa như: public, static, read
only,… và có thể không điền.
Kiểu dữ liệu trả về như: từ khóa void hay mọi kiểu dữ liệu như int, long,
bool, SinhVien,…
Tên hàm:
- Là tên gọi của hàm.
- Tên có thể đặt tùy ý nhưng nên đặt tên theo quy tắc đặt tên để có sự đồng
bộ ngầm định giữa các lập trình viên dễ tìm và dễ nhớ.
- Xem cách khởi tạo hàm giống khởi tạo biến ở chỗ. Đều cần kiễu dữ liệu
và tên. Có thể có các từ khóa. Tên tái sử dụng hàm ở nơi mong muốn.
Parametter là tham số truyền vào để sử dụng nội bộ trong hàm. Cấu trúc
khởi tạo như một biến thông thường. Có thể không điền.
Hàm chỉ được khai báo trong class.
Lưu ý: mọi hàm đều phải có cặp ngoặc nhọn {} biểu thị là một khối lệnh. Mọi
dòng code xử lý của hàm đều được viết bên trong cặp ngoặc nhọn {}. Không thể
khai báo một hàm trong một hàm khác theo cách thông thường.
Một hàm cơ bản hay thấy với cấu trúc bắt buộc phải có trong lập trình là hàm
Main:
static void Main(string[] args)
{
}
Static là từ khóa. Có thể không sử dụng được. Nhưng ở hàm main thì phải có.
Void là kiểu trả về. với hàm có kiểu trả về là void thì sẽ không cần từ khóa
return. Hoặc có nhưng chỉ đơn giản là ghi return.
Main là tên hàm. có thể đặt tùy ý. Nhưng ở trường hợp này thì bắt buộc phải là
main vì mỗi chương trình đều cần hàm main.
String[] args là parametter truyền từ bên ngoài vào để sử dụng hàm. co thể
không có cũng được. Nhưng ở trương hợp hàm main là bắt buộc phải có.
2. Hàm void
Một số lưu ý về sau: vì chúng ta đang viết code trên nền consle c#. Bắt buộc
phải có hàm main. Nhưng hàm mian lại có từ khóa static. Nên để trong hàm main
có thể sử dụng các hàm mà ta viết ra thì các hàm đó cũng phải có từ static.
Khi sử dụng hàm ta sẽ gọi lại tên hàm kèm theo dấu () biểu thị đó là một hàm.
sau này nếu có parametter thì sẽ thêm giá trị vào bên trong dấu ().
Chúng ta có thể gọi lại nhiều lần.
[Từ khóa 1] [Từ khóa 2] [Từ khóa n] <Kiểu dữ liệu trả về> <Tên hàm>([Parameter]){ }
void Demo()
{
// some code
return;
}
void Demo()
{
// some code
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 34
3. Hàm có kiểu trả về khác void
Với hàm có kiểu trả về khác void. Trong thân hàm bắt buộc phải có dòng return
<Giá trị trả về>;
Giá trị trả về phải có kiểu dữ liệu tương ứng với Kiểu dữ liệu trả về khi khai báo
hàm.
4. Parametter
Muốn thực hiện tính tổng hai số nhiều lần. Để tạo sự linh hoạt cho hàm thì
chúng ta sẽ tìm hiểu thêm về parameter:
Có thể hiểu đơn giản parametter là
Tập hợp một hay nhiều biến chứa các giá trị cần thiết để thao tác trong hàm.
Các giá trị của các biến này là những giá trị mà người dùng truyền vào khi gọi
hàm đó.
Khai báo một parametter cũng giống như khai báo biến. Khi khai báo nhiều
parametter thì các khai báo phải cách nhau bởi dấu “,”
static void Main(string[] args)
{
Demo(); // Gọi lại hàm để sử dụng
Console.ReadKey();
}
static void Main(string[] args)
{
Console.WriteLine(ReturnANumber());
Console.ReadKey();
}
/// <summary>
/// Hàm trả về giá trị số nguyên 5 thông qua tên hàm
/// Lưu ý giá trị trả về phải cùng kiểu dữ liệu với kiểu trả về của hàm
/// Ở đây là kiểu int
/// </summary>
/// <returns></returns>
static int ReturnANumber()
{
// b t bu c ph i có c u trúc return trong thân hàm
return 5;
}
Kết quả bằng: 5
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 35
Cho người dùng truyền vào 2 số họ muốn tính tổng vào 2 biến.
Từ đó ta chỉ cần tính tổng giá trị 2 biến đó rồi trả kết quả về cho người dùng.
Các khai báo int firstNumber, int secondNumber là các khai báo
parametter. Với khai báo này ta hiểu rằng muốn sử dụng hàm này thì cần
truyền vào 2 giá trị kiểu int.
Các parametter được xem như các biến cục bộ có phạm vị sử dụng trong
hàm
Các parametter được khởi tạo ngay khi gọi hàm và được hủy khi kết thúc
gọi hàm.
Số lượng parameter là không giới hạn.
Khi sử dụng hàm phải truyền vào đủ và đúng parameter. (Đủ số lượng, đúng
kiểu dữ liệu và đúng thứ tự như khai báo)
Có thể khai báo các parameter với các kiểu dữ liệu khác nhau.
Hàm sử dụng sẽ tạo ra các bản sao của parameter truyền vào trên RAM. Sau
đó dùng những bản sao đó để xử lý dữ liệu. Cho nên kết thúc lời gọi hàm
giá trị của các parameter sẽ không bị thay đổi.
Ví dụ về các paremetter với kiểu dữ liệu khác nhau:
static void Main(string[] args)
{
// khi sử dụng hàm phải truyền đúng số lượng, thứ tự parameter vào như khai báo của hàm
// đồng thời kiểu dữ liệu truyền vào của parameter phải trùng khớp với khai báo của hàm
Console.WriteLine(SumTwoNumber(5, 10));
Console.ReadKey();
}
/// <summary>
/// hàm trả ra kết quả tổng của 2 số firstNumber và secondNumber
/// </summary>
/// <param name="firstNumber"></param>
/// <param name="secondNumber"></param>
/// <returns></returns>
static int SumTwoNumber(int firstNumber, int secondNumber)
{
return firstNumber + secondNumber;
}
Kết quả là: 15
static void Main(string[] args)
{
PrintSomeThing("K9", 22);
PrintSomeThing("HowKteam.com", 1);
Console.ReadKey();
}
static void PrintSomeThing(string name, int age)
{
// in ra màn hình tên và tuổi được truyền vào
Console.WriteLine("This is {0}, {1} years old.", name, age);
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 36
XVI. Biến toàn cục và biến cục bộ trong C#
Biến toàn cục là biến được khai báo ở phân cấp cao hơn vị trí đang xác định.
Biến cục bộ là biến được khai báo ở cùng phân cấp tại vị trí đang xác định.
Vòng đời của biến toàn cục và biến cục bộ bắt đầu khi khối lệnh chứa nó bắt đầu
(khối lệnh bắt đầu bằng dấu “{“) và kết thúc khi khối lệnh chứa nó kết thúc (khối lệnh
kết thúc bằng dấu “}”).
Biến cục bộ được ưu tiên sử dụng hơn biến toàn cục trong trường hợp 2 biến này
trùng tên.Ví dụ biến toàn cục:
Lưu ý:
Parameter chính là một biến cục bộ.
Biến cục bộ có phạm vi sử dụng bên trong cặp dấu ngoặc nhọn { }.
XVII. Từ khóa ref và out trong C#
Ta xét ví dụ sau:
Kết quả màn hình in ra hai giá trị 5. Vì sau khi kết thúc hàm IncreaseValue giá trị
của value vẫn không thay đổi.
Với mong muốn giá trị của value sẽ thay đổi sau khi kết thúc lời gọi
hàm IncreaseValue thì chúng ta sẽ thêm từ khóa ref phía trước kiểu dữ liệu của
class Program
{
// biến toàn cục của các hàm nằm trong class
Program
// biến cục bộ của class Program
static int value = 5;
static void Main(string[] args)
{
// in ra màn hình biến toàn cục
Console.WriteLine(value);
// thay đổi giá trị của value
value = 10;
// kết quả gọi hàm này sẽ không thay đổi vì ưu tiên
biến cục bộ hơn
PrintSomeThing();
Console.ReadKey();
}
static void PrintSomeThing()
{
int value = 9;
// in ra màn hình biến toàn cục
Console.WriteLine(value);
}
}
class Program
{
// biến toàn cục của các hàm nằm
trong class Program
// biến cục bộ của class Program
static int value = 5;
static void Main(string[] args)
{
// in ra màn hình biến toàn cục
Console.WriteLine(value);
PrintSomeThing();
Console.ReadKey();
}
static void PrintSomeThing()
{
// in ra màn hình biến toán cục
Console.WriteLine(value);
}
}
static void Main(string[] args)
{
int value = 5;
Console.WriteLine("Value before increase: {0}", value);
IncreaseValue(value);
Console.WriteLine("Value after increase: {0}", value);
Console.ReadKey();
}
static void IncreaseValue(int value)
{
value++;
}
static void Main(string[] args)
{
int value = 5;
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 37
parameter mong muốn thay đổi giá trị khi khai o m. Đồng thời phải tm t
khóa ref ngay tc biến parameter truyền o khi s dụng m.
Kết quả in ra màn hình giá trị 5 và 6. Do giá trị của value đã thay đổi sau khi kết thúc
lời gọi hàm IncreaseValue.
Lưu ý:
Từ khóa ref phải có trước tên parametter của hàm và trước tên biến truyền vào khi
gọi hàm sử dụng.
Truyền parameter có từ khóa ref bắt buộc phải là một biến (không thể truyền vào
một hằng vì hằng là giá trị không thay đổi).
Có thể có một hoặc nhiều parameter với từ khóa ref trong lời khai báo hàm.
Biến truyền vào có từ khóa ref thì phải được khởi tạo giá trị trước khi truyền vào.
Hàm sử dụng sẽ thao tác trực tiếp với vùng nhớ của các parameter trên RAM.
Cho nên kết thúc lời gọi hàm giá trị các parameter sẽ bị thay đổi.
Từ khóa out
Từ khóa out cũng tương tự từ khóa ref. Đó là:
- Vùng nhớ của các parameter sẽ được hàm sử dụng thao tác trực tiếp, dẫn đến khi
kết thúc lời gọi hàm giá trị của các parametter có thể bị thay đổi.
- Phải có từ khóa out trước tên parameter của hàm và trước tên biến truyền vào
khi gọi hàm sử dụng.
Nhưng có một sự khác biệt đó là:
- Biến truyền vào có từ khóa out sẽ không cần khởi tạo giá trị ban đầu.
- Parameter đó chỉ như một thùng chứa kết quả trả về khi kết thúc gọi hàm.
- Đồng thời parameter đó phải được khởi tạo ngay bên trong lời gọi hàm.
Trong thân hàm IncreaseValue bắt buộc phải khởi tạo giá trị cho biến value. Kết quả
màn hình xuất ra giá trị 5 ban đầu và 1 là kết quả cuối cùng của biến value sau khi kết
thúc lời gọi hàm IncreaseValue.
XVIII. Mảng 1 chiều trong C#
Khái niệm về mảng
Mảng là
- Tập hợp các đối tượng có cùng kiểu dữ liệu.
- Mỗi đối tượng trong mảng được gọi là một phần tử.
- Các phần tử phân biệt với nhau bằng chỉ số phần tử. Trong C# chỉ số phần tử là
các số nguyên không âm và bắt đầu từ 0 1 2 3…
Đặc điểm của mảng:
- Các phần tử trong mảng dùng chung một tên và được truy xuất thông qua chỉ số
phần tử.
- Một mảng cần có giới hạn số phần tử mà mảng có thể chứa.
- Phải cấp phát vùng nhớ mới có thể sử dụng mảng.
- Vị trí ô nhớ của các phần tử trong mảng được cấp phát liền kề nhau.
static void Main(string[] args)
{
int value = 5;
Console.WriteLine("Value before increase: {0}", value);
IncreaseValue(out value);
Console.WriteLine("Value after increase: {0}", value);
Console.ReadKey();
}
static void IncreaseValue(out int value)
{
Value = 0;
value++;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 38
Những lợi ích khi sử dụng mảng:
- Gom nhóm các đối tượng có chung tính chất lại với nhau giúp code gọn gàng
hơn.
- Để thao tác, dễ quản lý, nâng cấp sửa chữa.
- Dễ dàng áp dụng các cấu trúc lặp vào để xử lý dữ liệu.
Cú pháp:
Trong đó:
- <kiểu dữ liệu> là kiểu dữ liệu của các phần tử trong mảng.
- Cặp dấu [] là ký hiệu cho khai báo mảng 1 chiều.
- <tên mảng> là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng. Cấp
phát vùng nhớ:
- Được thực hiện thông qua toán tử new
- Lưu ý là khi cấp phát vùng nhớ cho mảng 1 chiều ta cần chỉ ra số phần tử tối đa
của mảng.
- Sau khi mảng được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá
trị mặc định:
o Đối với số nguyên là 0
o Đối với số thực là 0.0
o Đối với kiểu ký tự là ‘’ (ký tự rỗng)
o Đối với kiểu tham chiếu là null
- Chúng ta có thể khởi tạo giá trị khác mà chúng ta mong muốn ngay khi cấp phát
vùng nhớ bằng cú pháp sau:
- Các giá trị khởi tạo nằm trong cặp dấu ngoặc ngọn {} và cách nhau bởi dấu
phẩy.
- Chúng ta không cần cung cấp số phần tử tối đa mà trình biên dịch sẽ tự đếm
xem bạn đã khởi tạo bao nhiêu giá trị và xem nó như số phần tử tối đa. Vì thế
dù việc khai báo số phần tử tối đa không lỗi nhưng trong trường hợp này nó
không có ý nghĩa lắm!
Khởi tạo giá trị
Cú pháp:
Ví dụ:
int[] IntArray = { 3, 9, 10 };
Về bản chất thì cách này trình biên dịch vẫn xem xét số phần tử khởi tạo và cấp phát
vùng nhớ cho biến mảng sau đó thực khởi tạo giá trị cho các phần tử trong mảng.
Nhưng cách viết này có vẻ nhanh và gọn hơn so với cách cấp phát vùng nhớ rồi mới
khởi tạo giá trị.
Tóm lại ta có 3 cách khai báo và khởi tạo sau:
Khai báo và cấp phát vùng nhớ
string[] Array = new string[3];
Khai báo, cấp phát và khởi tạo giá trị cho mảng
string[] Kteam = new string[] { "HowKteam","Free Education" };
Khởi tạo giá trị cho mảng
<kiểu dữ liệu> [] <tên mảng>;
<kiểu dữ liệu>[] <tên mảng> = new <kiểu dữ liệu>[] { <giá trị 1>, …, <giá trị n> };
<kiểu dữ liệu>[] <tên mảng> = { <giá trị 1>, …, <giá trị n> };
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 39
int[] IntArray = { 3, 9, 10 };
Sử dụng mảng
Kiểu mảng có thể dùng làm:
- Kiểu dữ liệu cho biến.
- Kiểu trả về cho hàm.
- Tham số truyền vào cho hàm.
Các phần tử của mảng được truy xuất thông qua chỉ số phần tử và cặp dấu []. Có thể
xem các phần tử của mảng như là các biến đơn và thao tác như thao tác với biến bình
thường.
Tên thuộc tính hoặc
phương thức
Ý nghĩa
Length Thuộc tính trả về số nguyên kiểu int là số phần tử tối đa của mảng
LongLength Thuộc tính trả về số nguyên kiểu long là số phần tử tối đa của mảng
GetLength(<số
chiều>)
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác định. Lưu ý
chiều của mảng là các số nguyên và được đánh số từ 0. Cho nên đối với
mảng 1 chiều thì số chiều là 0.
GetLongLength(<số
chiều>)
Tương tự GetLength nhưng trả về số nguyên kiểu long
Sort() Phương thức thực hiện sắp xếp mảng theo một thứ tự
Clear() Phương thức xóa hết dữ liệu trong mảng và đưa về giá trị mặc định của
kiểu. Lưu ý là chỉ xóa giá trị, vùng nhớ vẫn còn đó và có thể tiếp tục sử
dụng mà không cần cấp phát.
Copy() Thực hiện copy giá trị của mảng ra một vùng nhớ mới (phép gán thông
thường thì 2 đối tượng sẽ dùng chung vùng nhớ rất nguy hiểm vì đối
tượng này thay đổi dẫn đến đối tượng kia cũng thay đổi)
Reverse() Phương thức thực hiện đảo ngược thứ tự của mảng 1 chiều
Ý tưởng:
o Để truy xuất đến các phần tử của mảng cần thông qua chỉ số phần tử.
o Mà chỉ số phần tử là các số nguyên tăng dần.
o Từ đó ta có thể tận dụng vòng lặp để tăng giá trị 1 biến lên rồi xem biến đó như
là chỉ số phần tử của mảng.
dụ:
// Khai báo, cấp phát và khởi tạo mảng kiểu string và tên là Kteam
string[] Kteam = new string[] { "HowKteam", "Free Education" };
/*
* Vì chỉ số phần tử được đánh số từ 0 nên muốn truy xuất đến phần tử thứ 2 của mảng
thì chỉ số phần tử là 1
*/
Console.WriteLine(Kteam[1]);
int[] Kteam = new int[3];
for (int i = 0; i < 3; i++)
// Vì các phần tử có chỉ số là 0 1 2 nên điều kiện dừng là i < 3
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 40
Mọi thứ đều suôn sẻ cho đến một ngày vì lí do nào đó bạn cần nâng cấp
mảng Kteam lên 10 phần tử. Khi đó vấn đề sẽ xuất hiện, bạn phải đi thay đổi tất cả
những chỗ nào liên quan đến số phần tử từ 3 thành 10 hết. Như vậy mỗi lần có thay
đổi về số phần tử bạn đều phải làm lại những việc đó. Vậy tại sao ta không tận dụng
một thuộc tính vừa được học xong để giải quyết?
Với cách duyệt mảng như thế này bạn đã có thể làm mọi thứ với mảng từ nhập xuất
giá trị cho mảng đến tính toán phức tạp.
Chương trình ví dụ: tính năm âm lịch từ năm dương lịch đã nhập
Ý tưởng:
Chúng ta thấy các case là các giá trị nguyên không âm và tăng dần làm ta liên tưởng
tới chỉ số của mảng.
Như vậy nếu như ta tạo ra 1 mảng có số phần tử bằng số case và giá trị của phần tử
tại chỉ số thứ i sẽ tương đương với giá trị của case i.
Việc còn lại chỉ là tra cứu theo chỉ số của mảng nữa là xong!
Chương trình mẫu:
int[] Kteam = new int[3];
/*
* Thay số 3 thành thuộc tính Length.
* Bây giờ bạn có thay đổi số phần tử thì chỉ cần thay đổi ở khai báo thôi là xong!
*/
for (int i = 0; i < Kteam.Length; i++)
{
// Do something
}
int Year; // Biến chứa giá trị năm cần tính.
// Mảng Can chứa các giá trị can tương ứng theo bảng can
string[] Can = { "Canh", "Tan", "Nham", "Quy", "Giap",
"At", "Binh", "Dinh", "Mau", "Ky" };
// Mảng Chi chứa các giá trị chi tương ứng theo bảng chi
string[] Chi = { "Than", "Dau", "Tuat", "Hoi", "Ty", "Suu",
"Dan", "Meo", "Thin", "Ty", "Ngo", "Mui" };
Console.Write(" Moi ban nhap mot nam bat ky: ");
// Nhập năm dương lịch và ép kiểu về kiểu số nguyên
Year = Int32.Parse(Console.ReadLine());
/*
* Vì kết quả phép chia lấy dư của Year%10 hoặc Year%12 sẽ cho ra số nguyên
* Nên ta sẽ dùng nó làm chỉ số phần tử để tra cứu ra giá trị can chi tương ứng. Thay vì
dùng cách cũ là switch case
* Như vậy cách này vừa đơn giản vừa dễ hiểu, code rất ít sẽ với cách dùng switch case
*/
Console.WriteLine("Nam {0} co nam am lich la: {1} {2}",
Year, Can[Year%10], Chi[Year%12]);
// Nối Can và Chi lại để được năm âm lịch
Console.ReadLine();
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 41
XIX. Mảng 2 chiều trong C#
Khai báo mảng 2 chiều
Mảng 2 chiều được hình dung như một bảng có m dòng và n cột với một số đặc trưng
sau:
- Mảng 2 chiều mang những đặc trưng cơ bản của một mảng bình thường.
-
- Các phần tử trong mảng 2 chiều được truy xuất thông qua 2 chỉ số phần tử (tạm
gọi là chỉ số dòng và chỉ số cột)
- Hình ảnh minh họa mảng 2: Giả sử ta có mảng A có 5 dòng và 8 cột.
Các dòng và các cột được đánh số từ 0 và tăng dần. Mỗi phần tử là giao nhau của
dòng và cột tương ứng đồng thời ta sử dụng chỉ số dòng cột đó để truy xuất đến phần tử
của mảng 2 chiều.
Ví dụ: A[1, 2] là cách truy xuất đến phần tử ở dòng thứ 2 cột thứ 3 (do chỉ số được
đánh số từ 0)
Cú pháp:
Trong đó:
<kiểu dữ liệu> là kiểu dữ liệu của các phần tử trong mảng.
Cặp dấu [ , ] là ký hiệu cho khai báo mảng hai chiểu.
<tên mảng> là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến.
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng.
Cấp phát vùng nhớ
Được thực hiện thông qua toán tử new. Lưu ý khi cấp phát vùng nhớ cho mảng 2
chiều ta cần chỉ ra số dòng và số cột tối đa của mảng
Ví dụ:
Sau khi mảng được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá trị
mặc định:
- Đổi với số nguyên là 0
- Đối với số thực là 0.0
- Đối với kiểu ký tự là ‘ ’ (ký tự rỗng)
- Đối với kiểu tham chiếu là null
<ki u d li u> [ , ] <tên
m ng>;
/* * Khai báo mảng 2 chiều kiểu string và có tên là Kteam.
* Sau đó thực hiện cấp phát vùng nhớ với số dòng là 2 và số cột là 3. */
string[,] SinhVien = new string[2, 3];
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 42
- Chúng ta có thể tạo giá trị các mà chúng ta mong muốn ngay khi cấp phát vùng
nhớ bằng cú pháp sau:
- Vì đây là mảng 2 chiều nên chúng ta sẽ không khởi tạo giống mảng 1 chiều mà
phải khởi tạo giá trị theo từng dòng một. xem từng dòng là 1 mảng 1 chiều và
khởi tạo như mảng một chiều.
- Các giá trị khởi tạo nằm trong một dấu ngoặc nhọn {} và cách nhau bởi dấu
phẩy.
- Chúng ta không cần cung cấp số dòng và số cột tối đa mà trình biên dịch sẽ tự
đếm xem bạn đã khởi tạo bao nhiêu dòng và mỗi dòng bao nhiêu giá trị rồi xem
nó như số dòng số cột tối đa.
Khởi tạo giá trị
Cú pháp:
Ví dụ:
Về bản chất thì cách này trình biên dịch vẫn xem xét số phần tử khởi tạo và cấp phát vùng nhớ
cho biến mảng sau đó thực khởi tạo giá trị cho các phần tử trong mảng. nhưng cách viết này có
vẻ nhanh và gọn hơn so với cách cấp phát vùng nhớ rồi mới khởi tạo giá trị.
Tóm lại, cũng như mảng một chiều, mảng 2 chiều cũng có 3 cách khai báo và khởi tạo:
- Khai báo và cấp phát vùng nhớ
string[,] Array = new string[2, 3];
- Khai báo, cấp phát và khởi tạo giá trị cho mảng
- Khởi tạo giá trị cho mảng
<ki u d li u>[,] <tên m ng> = new <ki u d li u>[]
{
{<giá tr dòng 1 c t 1>,…,<giá tr dòng 1 c t n>},
{<giá tr dòng m c t 1>,…,<giá tr dòng m c t n>}
};
<ki u d li u>[,] <tên m ng> =
{
{<giá tr dòng 1 c t 1>,…,<giá tr dòng 1 c t n>},
{<giá tr dòng m c t 1>,…,<giá tr dòng m c t n>}
};
int[,] IntArray =
{
{1, 2}, {3, 4}, {5, 6}
};
string[,] Kteam = new string[,]
{
{ "HowKteam", "Free Education" },
{ “HowKteam.com”, “Share to be better” }
};
int[] IntArray =
{
{1, 2}, {3, 4}, {5, 6}
};
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 43
Sử dụng ảng 2 chiều:
Tương tự như mảng 1 chiều, kiểu mảng 2 chiều cũng có thể dùng làm:
- Kiểu dữ liệu cho biến.
- Kiểu trả về cho hàm.
- Tham số truyền vào cho hàm.
o Các phần tử của mảng được truy xuất thông qua chỉ số dòng và chỉ số cột
(chỉ số dòng viết trước, chỉ số cột viết sau ngăn cách nhau bởi dấu ,) và cặp
dấu []. Có thể xem các phần tử của mảng như là các biến đơn và thao tác
như thao tác với biến bình thường.
Một số thuộc tính của mảng 2 chiều:
Tên thuộc tính
phương thức
Ý nghĩa
Length Thuộc tính sẽ trả về số nguyên kiểu int là số phần tử tối đa của mảng
(số phần tử của mảng 2 chiều là tích sdòng số cột của mảng)
Longlength Tương tự như thuộc tính Length nhưng trả về số nguyên kiểu long
GetLength
(<số nhiều>)
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác đnh. Lưu
ý chiều của mảng là các số nguyên và được đánh số từ 0. Đối với
mảng 2 chiều thì GetLength (0) là độ dài đầu tiên tương ứng với số
dòng và GetLength (1) là độ dài chiều thứ 2 tương ứng số cột.
GetLongLength
(<số nhiều>)
Tương tự GetLength nhưng trả về số nguyên kiểu long
Clone() Thực hiện coppy giá trị của mảng ra một vùng nhớ mới (phép gán
thông thường thì 2 đối tượng này thay đổi dẫn đến đối tượng kia cũng
thay đổi)
Cách duyệt mảng 2 chiều:
Ý tưởng: ơng tự như ý tưởng duyệt mảng 1 chiều.
Nhưng do mảng 2 chiều có 2 chỉ số là chỉ số dòng và chỉ số cột nên chúng ta cần 2
vòng lặp lồng vào nhau.
Ví dụ:
Cách duyệt này sẽ duyệt tuần tự các dòng trong mảng 2 chiều, ở mỗi dòng sẽ duyệt từ
đầu dòng đến cuối dòng. Bạn hoàn toàn có thể duyệt theo ý mình bằng cách thay đổi
giá trị trong vòng lặp.
Ví dụ sau sẽ duyệt theo cột. Tức là duyệt tuần tự từng cột rồi ở mỗi cột duyệt từ trên
xuống dưới.
int[,] IntArray = new int [9, 10];
/*Sử dụng 2 vòng for lồng vào nhau để duyệt mảng 2 chiều
* Vòng lặp ngoài là vòng lặp duyệt mỗi dòng của của mảng 2 chiều
* Với mỗi dòng thì vòng lặp trong là vòng lặp duyệt các phần tử trên dòng đó
(duyệt từng cột trên dòng hiện tại)*/
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 10; j++)
{
/* Với cách duyệt này thì IntArray[i, j] sẽ là phần tử hiện tại mình đang xét
* Code xử lý sẽ viết ở đây */
}
}
/* Duyệt mảng 2 chiều theo cột
* Các bạn để ý sự thay đổi trong 2 vòng lặp*/
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 9; j++)
{
/* Lưu ý là các phần tử được truy xuất là IntArray[j, i] thay vì IntArray[i, j]
* Code xử lý */
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 44
Để ý là cả 2 cách duyệt mình đều vi phạm 1 điều mà ở phần duyệt mảng 1 chiều của
bài trước mình đã trình bày. Đó là nên sử dụng hàm trả về số dòng, số cột thay vì viết
cứng một con số cụ thể.
Lúc này hàm GetLength(<số chiều>) thực sự phát huy tác dụng. Các bạn cùng xem
nhé:
Ví dụ chương trình sử dụng mảng 2 chiều
Ta thử xét 1 ví dụ đơn giản đó là viết chương trình cho phép nhập vào giá trị số
nguyên cho 1 mảng 2 chiều bất kỳ sau đó in ra màn hình mảng đã nhập kèm theo tổng
tất cả các giá trị mảng trong.
Chương trình minh họa:
/*Như đã trình bày ở phần trước thì: GetLength(0) sẽ trả về số dòng của
mảng 2 chiềuGetLength(1) sẽ trả về số cột của mảng 2 chiều*/
for (int i = 0; i < IntArray.GetLength(0); i++)
{
for (int j = 0; j < IntArray.GetLength(1); j++)
{
// Code x
}
}
Console.Write(" Moi ban nhap so dong cua mang: ");
int Rows = int.Parse(Console.ReadLine());
Console.Write(" Moi ban nhap so cot cua mang: ");
int Columns = int.Parse(Console.ReadLine());
// Tạo 1 mảng 2 chiều với số dòng và số cột đã nhập
int[,] IntArray = new int[Rows, Columns];
/* Duyệt mảng để nhập giá trị cho các phần tử
* Ở đây mình muốn minh họa cách sử dụng mảng nên mình bỏ qua các bước kiểm tra dữ liệu
mà ép kiểu trực tiếp
* Điều này có thể gây lỗi khi nhập sai nên các bạn hãy cải tiến chương trình này cho đầy đủ
nhé! */
for (int i = 0; i < IntArray.GetLength(0); i++)
{
for (int j = 0; j < IntArray.GetLength(1); j++)
{
Console.Write(" Moi ban nhap phan tu IntArray[{0}, {1}] = ", i, j);
IntArray[i, j] = int.Parse(Console.ReadLine());
}
}
/* In mảng 2 chiều đã nhập ra màn hình
* Để tính tổng các giá trị trong mảng ta chỉ cần duyệt qua các phần tử và cộng chúng lại với
nhau
* Tận dụng lúc duyệt mảng để in giá trị ta sẽ thực hiện tính tổng luôn để tránh phải duyệt lại
mảng thêm lần nữa. */
int Sum = 0;
Console.WriteLine("\n Mang ban vua nhap la: ");
for (int i = 0; i < IntArray.GetLength(0); i++)
{
for (int j = 0; j < IntArray.GetLength(1); j++)
{
Console.Write(IntArray[i, j] + " ");
Sum = Sum + IntArray[i, j];
}
// Sau khi in xong mỗi dòng ta thực hiện xuống dòng rồi mới in tiếp
Console.WriteLine();
}
Console.WriteLine(" Tong cac gia tri trong mang: " + Sum);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 45
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 46
XX.Mảng nhiều chiều trong lập trình C# cơ bản
Mảng 3 chiều trong C#
Nếu như mảng 2 chiều được hình dung như một ma trận MxN thì mảng 3 chiều
được hình dung như một hình hộp chữ nhật có kích thước MxNxP
Cú pháp:
Trong đó:
<kiểu dữ liệu> là kiểu dữ liệu của các phần tử trong mảng.
Cặp dấu [ , , ] là kí hiệu cho khai báo mảng 3 chiều.
<tên mảng> là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến.
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng.
Cấp phát vùng nhớ
Sau khi được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá trị mặc
định:
Đối với số nguyên là 0
Đối với số thực là 0.0
Đối với kiểu ký tự là ‘ ’
Đối với tham chiếu là null
Chúng ta có thể khởi tạo giá trị khác mà chúng ta mong muốn ngay khi cấp phát
vùng nhớ bằng cú pháp:
<ki u d li u> [ , , ] <tên m ng>;
/*
* Khai báo m ng 3 chi u ki u string và có
tên là Kteam.
* Sau đó th c hi n c p phát vùng nh v i 3
ch s l n l t là 2, 2, 3. ượ
*/
string[ , , ] Kteam = new string[2, 2, 3];
<kiểu dữ liệu>[ , , ] <tên mảng> = new <kiểu dữ liệu>[ , , ]
{
{
{<giá trị tại vị trí 0, 0, 0>, …, <giá trị tại vị trí (0,0,p)>},
{<giá trị tại vị trí 0, n, 0>, …, <giá trị tại vị trí (0,n,p)>}
}
{
{< giá trị tại vị trí m, 0, 0>, …, <giá trị tại vị trí (m,0,p)>},
{<giá trị tại vị trí m, n, 0>, …, <giá trị tại vị trí (m,n,p)>}
}
}
Khởi
tạo một
mảng 2
chiều
Khởi tạo
1 mảng 1
chiều mà
mỗi phần
tử là
mảng 2
chiều
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 47
Ví dụ:
Với khai báo trên thì mảng 3 chiều sẽ là mảng 3 chiều với số phần tử tối đa của các
chiều lần lượt là 2 2 3.
Khai báo và cấp phát vùng nhớ
Khai báo, cấp phát và khởi tạo giá trị cho mảng
Sử dụng mảng 3 chiều
Các phần tử của mảng được truy xuất thông qua 3 chỉ số ngăn cách nhau bởi dấu phẩy
và nằm trong cặp dấu [ ].
Tên thuộc tính hoặc
phương thc
Ý nghĩa
Length Thuộc tính tr về snguyên kiu int là sphần t tối đa của mảng (s
phần t ca mng 3 chiu là tích 3 ch scủa mảng)
LongLength Tương t n thuộc tính Length nng tr về snguyên kiểu long
GetLength
(<schiu>)
Tr v snguyên kiu int là sphần t trong chiều đã xác định. Lưu
ý chiu của mng là các snguyên đưc đánh st 0. Đối với
mng 3 chiu thì GetLength(0) là độ i chiều đầu tiên
GetLength(1) là độ i chiu th 2 GetLength(2) là độ dài chiều
thứ 3.
GetLongLength
(<schiu>)
Tương t GetLength nng tr về snguyên kiểu long
Rank Thuộc tính tr về snguyên int là schiu của mảng
Clone() Thực hin copy giá tr của mng ra mt vùng nhớ mi (phép gán
int[,,] Mang3Chieu = new int[,,]
{
{
{1, 2, 3},
{4, 5, 6}
},
{
{7, 8, 9},
{10, 11, 12}
}
};
string[ , , ] Array = new string[2, 2, 3];
Khởi
tạo
một
mảng
2
chiều
<kiểu dữ liệu>[ , , ] <tên mảng> =
{
{
{<giá trị tại vị trí 0, 0, 0>, …, <giá trị tại vị trí (0,0,p)>},
{<giá trị tại vị trí 0, n, 0>, …, <giá trị tại vị trí (0,n,p)>}
}
{
{< giá trị tại vị trí m, 0, 0>, …, <giá trị tại vị trí (m,0,p)>},
{<giá trị tại vị trí m, n, 0>, …, <giá trị tại vị trí (m,n,p)>}
}
}
Khởi tạo
1 mảng
1 chiều
mà mỗi
phần tử
là một
mảng 2
chiều
// Khai báo, cấp phát và khởi tạo mảng 3 chiều kiểu int và tên là Mang3Chieu
int[,,] Mang3Chieu = new int[,,]
{
{
{1, 2, 3},
{4, 5, 6}
},
{
{7, 8, 9},
{10, 11, 12}
}
};
//Truy xuất đến phần tử có các chỉ số lần lượt là 1 1 2
Console.WriteLine(Mang3Chieu[1, 1, 2]);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 48
thông thường thì 2 đối tượng sẽ dùng chung vùng nhớ rất nguy hiểm
vì đối tượng này thay đổi dẫn đến đối tượng kia cũng thay đổi)
Cách duyệt mảng 3 chiều
Ý tưởng:
Tương tự như ý tưởng duyệt mảng 2 chiều.
Nhưng do mảng 3 chiều có 3 chỉ số nên chúng ta cần 3 vòng lặp lồng vào nhau.
Ví dụ:
Cách duyệt này sẽ duyệt tuần tự các chiều trong mảng 3 chiều, ở mỗi chiều sẽ duyệt
từ đầu đến cuối. Bạn hoàn toàn có thể duyệt theo ý mình bằng cách thay đổi giá trị trong
vòng lặp.
Vì mảng 3 chiều cũng ít được sử dụng nên mình chỉ giới thiệu qua thôi chứ không đi
quá chi tiết. Với kiến thức cơ bản mình đã cung cấp các bạn hoàn toàn có thể tự tìm hiểu
thêm khi cần thiết.
Lớp array trong C# là lớp cơ sở cho mọi mảng, một mảng bất kỳ đều được kế thừa
từ lớp này
Tên thuộc tính Ý nghĩa
Length Thuộc tính trả về số nguyên kiểu int là số phần tử tối đa của
mảng.
LongLength Tương tự như thuộc tính Length nhưng trả về số nguyên
kiểu long
Rank Thuộc tính trả về số nguyên int là số chiều của mảng
Một số phương thức trong lớp array:
Tên phương thức Ý nghĩa
Clear() Thiết lập lại giá trị mặc đnh co tất cả các phần tử trong mảng.
GetLength
(<số chiều>)
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác định.
Lưu ý chiều của mảng là các số nguyên và được đánh số từ 0.
GetLongLength
(<số chiều>)
Tương tự GetLength nhưng trả về số nguyên kiểu long
GetValue(<vị trí>) Trả về giá trị 1 phần tử của mảng tại vị trí truyền vào. Nếu là
mảng nhiều chiều thì có thể truyền vào danh sách các chỉ số.
Reverse(<tên mảng>) Đảo ngược các giá trị của mảng 1 chiều.
Sort(<tên mảng>) Sắp xếp các phần tử của mảng 1 chiều.
IndexOf(<tên mảng>,
<phần tử cần tìm>)
Tìm kiếm 1 phần tử có tồn tại trong mảng hay không. Nếu có thì
trả về vị trí xuất hiện đầu tiên trong mảng ngược lại sẽ trả về 0.
int[, ,] IntArray = new int[3, 9, 10];
/* Sử dụng 3 vòng for lồng vào nhau để duyệt mảng 3 chiều
* Vòng lặp đầu tiên là vòng lặp duyệt các phần tử của chiều đầu tiên.
* Vòng lặp thứ 2 là vòng lặp duyệt các phần tử của chiều th2.
* Vòng lặp thứ 3 là vòng lặp duyệt các phần tử của chiều th3. */
for (int i = 0; i < IntArray.GetLength(0); i++)
{
for (int j = 0; j < IntArray.GetLength(1); j++)
{
for (int k = 0; k < IntArray.GetLength(2); k++)
{
/* cách duyệt này thì IntArray[i, j, k] sẽ là phần tử hiện tại mình đang xét
* Code xử lý sẽ viết ở đây*/
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 49
Đây chỉ là một số thuộc tính và phương thức tiêu biểu. Ngoài ra còn nhiều phương
thức, thuộc tính khác các bạn có thể tự khám phá.
Lưu ý là các phương thức Sort, Reverse, IndexOf được gọi thông qua tên lớp. Tức
là muốn sử dụng các phương thức trên ta sẽ dùng cú pháp:
Array.Sort(<tên mảng>)
Array.Reverse(<tên mảng>)
Array.IndexOf(<tên mảng>, <phần tử cần tìm>)
Nhờ sự hỗ trợ của các phương thức trên mà ta có thể dễ dàng thao tác với dữ liệu
mảng như sắp xếp, đảo ngược, . . . mà không cần phải viết chi tiết ra giải thuật như thế
nào.
Sau đây là một vài ví dụ ứng dụng lớp Array:
/*Khai báo và khởi tạo mảng 1 chiều tên IntArray có 4 phần tử chưa được sắp xếp*/
int[] IntArray = { 5, 2, 1, 3 };
/* Thực hiện câu lệnh sắp xếp mảng
* Ở đây mặc định là sẽ sắp xếp tăng dần.
* Nếu bạn muốn sắp xếp giảm dần có thể tận dụng phương thức đảo ngược các giá trị mảng
để được mảng giảm dần*/
Array.Sort(IntArray);
Console.WriteLine(" Mang da sap xep tang dan: ");
for (int i = 0; i < IntArray.Length; i++)
{
Console.Write("\t" + IntArray[i]);
}
Console.WriteLine();
/* Đảo ngược các phần tử của mảng để được 1 mảng giảm dần */
Array.Reverse(IntArray);
Console.WriteLine(" Mang da sap xep giam dan: ");
for (int i = 0; i < IntArray.Length; i++)
{
Console.Write("\t" + IntArray[i]);
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 50
XXI. Vòng lập foreach trong C# cơ bản
Cấu trúc lập foreach cho phép chúng ta duyệt 1 mảng hoặc 1 tập hợp
Một số đặc trưng của foreach:
Foreach không duyệt mảng hoặc tập hợp thông qua chỉ số phần tử như cấu trúc lặp for
Foreach duyệt tuần tự các phần tử trong mảng hoặc tập hợp
Foreach chỉ dùng để duyệt mảng hoặc tập hợp ngoài ra không thể làm gì khác
Cú pháp:
Trong đó:
Các từ khoá foreach, in là từ khoá bắt buộc.
< kiểu dữ liệu> là kiểu dữ liệu của các phần tử trong mảng hoặc tập hợp.
< tên biến tạm> là tên 1 biến tạm đại diện cho phần tử đang xét khi duyệt mảng
hoặc tập hợp.
<tên mảng hoặc tập hợp> là tên của mảng hoặc tập hợp cần duyệt.
Nguyên tắc hoạt động
Foreach cũng có nguyên tắc hoạt động tương tự như các cấu trúc lặp khác cụ thể như
sau:
Ở vòng lặp đầu tiên sẽ gán giá trị của phần tử đầu tiên trong mảng vào biến tạm.
Thực hiện khối lệnh bên trong vòng lặp foreach.
Qua mỗi vòng lặp tiếp theo sẽ thực hiện kiểm tra xem đã duyệt hết mảng hoặc tập
hợp chưa. Nếu chưa thì tiếp gán giá trị của phần tử hiện tại vào biến tạm và tiếp tục
thực hiện khối lệnh bên trong.
Nếu đã duyệt qua hết các phần tử thì vòng lặp sẽ kết thúc.
Qua nguyên tắc hoạt động trên ta có thể thấy:
Biến tạm trong vòng lặp foreach sẽ tương đương với phần tử i trong cách duyệt
của vòng lặp for
Qua mỗi bước lặp ta chỉ có thể thao tác với giá trị của phần tử đang xét mà không
thể tương tác với các phần tử đứng trước nó hay đứng sau nó
Bằng cách duyệt của foreach ta không thể thay đổi giá trị của các phần tử vì lúc
này giá trị của nó đã được sao chép ra một 1 biến tạm và ta chỉ có thể thao tác với biến
tạm.
Thậm chí việc thay đổi giá trị của biến tạm cũng không được phép. Nếu ta cố làm
điều đó thì sẽ gặp lỗi sau:
Sử dụng foreach trong C#
Trong C#, có những danh sách, tập hợp mà ta không thể truy xuất đến các phần tử của
nó thông qua chỉ số phần tử được (ví dụ như kiểu List, hoặc các collection, generic).
Trong trường hợp như vậy, để duyệt các danh sách, tập hợp có tính chất như trên
thì foreach là lựa chọn tốt nhất.
foreach (<kiểu dữ liệu> <tên biến tạm> in <tên mảng hoặc tập hợp>)
{
// Code x
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 51
Chúng ta sẽ tìm hiểu sức mạnh của foreach qua các bài học sau. Còn trong bài học
này mình chỉ ví dụ đơn giản để các bạn có thể nắm cú pháp cũng như cách sử
dụng foreach.
Xét chương trình sau:
Có lẽ chúng ta đã không mấy xa lạ với đoạn chương trình trên. Đoạn chương trình
trên sẽ duyệt mảng để in ra các giá trị của mảng và tính tổng các phần tử trong mảng.
Nhưng thay vì sử dụng for thì mình sử dụng foreach để các bạn có thể thấy được sự
tương đồng giữa các thành phần trong cấu trúc foreach và cấu trúc vòng lặp for.
Kết quả khi chạy đoạn chương trình trên:
Ví dụ thứ 2: ta thử sử dụng foreach để duyệt mảng jagged
Ta có thể thấy cách duyệt foreach ngắn gọn hơn nhiều so với cách duyệt bằng vòng
lặp for thông thường.
Ta cũng chả quan tâm đến việc phải xử lý độ dài mảng hay chỉ số phần tử để truy
xuất 1 phần tử nào đó.
/*
* Khai báo mảng 1 chiều IntArray và khởi tạo giá trị.
* Các bạn có thể xem lại cú pháp này ở bài Mảng 1 chiều trong C#
* Khai báo 1 biến Sum để chứa giá trị tổng các phần tử trong mảng IntArray.
*/
int[] IntArray = { 1, 5, 2, 4, 6 };
int Sum = 0;
/*
* Sử dụng foreach để duyệt mảng và in giá trị của các phần tử trong mảng.
* Đồng thời tận dụng vòng lặp để tính tổng các phần tử trong mảng.
*/
foreach (int item in IntArray)
{
Console.Write("\t" + item);
Sum += item;
}
Console.WriteLine("\n Sum = " + Sum);
/* Khai báo 1 mảng jagged tên là JaggedArray và khởi tạo giá trị.
* Các bạn có thể xem lại cú pháp khai báo này ở bài Mảng nhiều chiều trong C#.*/
int[][] JaggedArray =
{
new int[] { 1, 2, 3 },
new int[] { 5, 2, 4, 1, 6},
new int[] { 7, 3, 4, 2, 1, 5, 9, 8}
};
/*Tương tự như dùng for, ta cũng dùng 2 vòng foreach lồng vào nhau để duyệt mảng. */
foreach (int[] Element in JaggedArray)
{
foreach (int item in Element)
{
Console.Write(item + " ");
}
Console.WriteLine();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 52
Kết quả khi chạy chương trình trên
So sánh for và foreach
Foreach mang trong mình một số ưu điểm như:
Câu lệnh ngắn gọn, sẽ sử dụng.
Rất có ích khi duyệt danh sách, tập hợp mà không thể truy xuất thông qua chỉ số
phần tử.
Duyệt các danh sách, tập hợp có số phần tử không xác định hoặc số phần tử thay
đổi liên tục.
Mặc dù có nhiều ưu điểm nhưng không hẳn là foreach hơn hẵn for.
Tiêu chí For Foreach
Khả năng truy xuất phần tử Truy xuất ngẫu nhiên (có thể gọi
bất kỳ phần tử nào trong mảng
để sử dụng)
Truy xuất tuần tự (chỉ sử
dụng được giá trị phần tử
đang xét)
Thay đổi được giá trị của các phần
tử
Không
Duyệt mảng, tập hợp khi không biết
được số phần tử của mảng, tập hợp
Không
Hiệu suất (tốc độ xử lý) (*) Đối với mảng, danh sách hoặc
tập hợp có khả năng truy xuất
ngẫu nhiên thì for sẽ chiếm ưu
thế
Đối với mảng, danh sách
hoặc tập hợp không có
khả năng truy xuất ngẫu
nhiên thì foreach chiếm
ưu thế
Nhìn chung hiệu suất của for và foreach còn phụ thuộc vào cấu trúc dữ liệu đang xét
cho nên việc so sánh này chỉ mang tính chất tham khảo.
Sau đây là 2 đoạn chương trình kiểm tra tốc độ của for và foreach đối với 2 cấu trúc
dữ liệu là mảng 1 chiều (có khả năng truy xuất ngẫu nhiên) và danh sách liên
kết LinkedList (không có khả năng truy xuất ngẫu nhiên):
Đầu tiên là mảng 1 chiều:
Đoạn chương trình mình thực hiện:
- Khai báo 1 mảng 1 chiều có 20 triệu phần tử (khai báo số phần tử lớn để có thể
thấy được sự chêch lệch về tốc độ)
- Lần lượt dùng for, foreach để duyệt mảng đó và thực hiện 1 câu lệnh nào đó.
- Cuối cùng là xuất ra thời gian thực thi của từng trường hợp dưới dạng giây và
mili giây.
Dựa vào kết quả ta có thể thấy được sự chêch lệch nhỏ về tốc độ, nếu kiểm tra với
số phần tử lớn hơn hoặc cấu trúc dữ liệu phức tạp hơn thì chêch lch này càng lớn.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 53
Tiếp thao là danh sách liên kết LinkedList
Chương trình mình thực hiện:
o Tạo 1 LinkedList và thêm vào 100000 phần tử.
o Sau đó lần lượt dùng for, foreach duyệt LinkedList trên và tính tổng
giá trị các phần tử trong mảng.
o Cuối cùng in ra thời gian chạy của for, foreach.
Kết quả khi chạy chương trình trên là:
Dựa vào kết quả chạy ta thấy sự chênh lệch là quá lớn, rõ ràng đối với cấu trúc dữ
liệu phức tạp, không hỗ trợ truy xuất thông qua chỉ số phần tử nữa thì foreach chiếm
ưu thế.
Tùy vào từng trường hợp mà ta nên dùng for hay dùng foreach. Không nên lạm dụng
1 thứ quá nhiều
/* Kiểm tra tốc độ của for */
* Sử dụng 1 cái đồng hồ để đo thời gian chạy của 2 vòng lặp for và foreach
* Ở đây mình chỉ kiểm tra tốc độ chứ không tập trung giải thích cú pháp
* Các bạn có thể tìm hiểu thêm. */
Stopwatch start = new Stopwatch();
start.Start();
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 54
// Khai báo 1 LinkedList chưa các số nguyên int và khởi tạo giá trị cho nó.
LinkedList<int> list = new LinkedList<int>();
for (int i = 0; i < 100000; i++)
{
list.AddLast(i);
}
/* Kiểm tra tốc độ của for */
Stopwatch st = new Stopwatch();
int s1 = 0, length = list.Count;
st.Start();
for (int i = 0; i < length; i++)
{
/*Vì LinkedList không thể truy xuất thông qua chỉ số phần tử
* nên mình phải sử dụng 1 phương thức hỗ trợ làm điều này.
* Và đây chính là sự hạn chế của for đối với các cấu trúc dữ liệu tương tự như danh sách liên
kết này. */
s1 += list.ElementAt(i);
}
st.Stop();
/* Kiểm tra tốc độ của foreach */
Stopwatch st2 = new Stopwatch();
int s2 = 0;
st2.Start();
foreach (int item in list)
{ //Vì foreach không quan tâm đến chỉ số phần tử nên code viết rất ngắn gọn
s2 += item;
}
st2.Stop();
/* In ra giá trị tính tổng giá trị các phần tử khi duyệt bằng for và foreach để chắc chắn rằng cả 2
đều chạy đúng */
Console.WriteLine(" s1 = {0} s2 = {1}", s1, s2);
Console.WriteLine(" Thoi gian chay cua for = {0} giay {1} mili
giay", st.Elapsed.Seconds, st.Elapsed.Milliseconds);
Console.WriteLine(" Thoi gian chay cua foreach = {0} giay {1}
mini giay", st2.Elapsed.Seconds, st2.Elapsed.Milliseconds);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 55
XXII. Lớp string trong C# cơ bản
String là một kiểu dữ liệu tham chiếu được dùng để lưu trữ chuỗi ký tự. Vì là một
kiểu dữ liệu nên cách khai báo và sử dụng hoàn toàn tương tự các kiểu dữ liệu khác.
Hôm nay mình sẽ không bàn đến khai báo của nó nữa mà đi sâu vào các thuộc tính
và phương thức mà lớp String hỗ trợ.
Thuộc tính trong lớp string:
Tên thuộc tính Ý nghĩa
Length Trả về một số nguyên kiểu int là độ dài của chuỗi
Một số phương thức thường dùng trong lớp string:
Tên phương thức Ý nghĩa Ghi chú
String.compare(string
strA, string strB)
So sanh 2 chuỗi strA và strB
có bằng nhau không. Nếu bằng
nhau thì tả về 0,
Nếu strA>strB thì trả về 1,
ngược lại -1.
Có thể gọi phương thức so sánh này
thông qua tên biến:
<tên biến>.CompareTo(string strB)
String.concat(string
strA, string strB)
Nối 2 chuỗi strA và strB thành
1 chuỗi
Tương tự phép cộng chuỗi bằng toán
tử cộng.
IndexOf(char Value) Trả về 1 số nguyên kiểu int là
vị trí xuất hiên đầu tiên của ký
tự value trong chuỗi
Nếu không tìm thấy thì phương thức
tả về -1
Insert(int strartIndex,
string value)
Trả về một chuỗi mới trong đó
bao gồm chuỗi cũ đã chèn
thêm chuỗi value tại vị trí
strartIndex
String.IsNullOrEmpty(
string value)
Kiểm tra chuỗi vakue có phải
chuỗi null hay không. Nếu
đúng trả về true, ngược lại
false.
LastIndexOf(char
value)
Trả về một số nguyên kiểu int
là vị trí xuất hiện cuối cùng
của ký tự value trong chuỗi
Remove(int
strartIndex, int count)
Trả về một chuỗi mới đã gỡ
count ký tự từ vị trí strartIndex
trong chuỗi ban đầu
Replace(char oldValue,
char newValue)
Trả về một chuỗi mới đã thay
thế các ký tự oldValue bằng ký
tự newValue trong chuỗi ban
đầu
Split(char value) Trả về một mảng các chuỗi
được cắt ra từ chuỗi ban đầu
tại những nơi có ký tự value
Phương thức này có thể truyền vào
nhiếu ký tự khác nhau. Khi đó
phương thức sẽ thực hiện cắt theo
từng ký tự đã truyền vào.
Subtring(int
strartIndex, int length)
Trả về 1 chuỗi mới được cắt từ
vị trí strarIndex với số ký tự
cắt là length từ chuỗi ban đầu
Nếu gọi subtring mà chỉ truyền vào
giá trị startIndex thì mặc định
phương thức sẽ cắt từ vị trí startIndex
đến cuối chuỗi
Lưu ý:
Các phương thức mà mình có ghi String phía trước là các phương thức gọi thông
qua tên lớp. Các phương thức còn lại được gọi thông qua đối tượng.
Các phương thức khi gọi sẽ tạo ra đối tượng mới rồi thao tác trên đối tượng đó chứ
không thao tác trực tiếp với đối tượng đang xét. Vì thế nếu như gọi.
string a = "HowKteam";
a.Substring(3, 1);
Thì biến a sau khi thực hiện lệnh Subtring vẫn mang giá trị “HowKteam”.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 56
Nếu muốn biến a mang giá trị mới khi thực hiện Substring thì phải gán ngược lại giá
trị mới đó cho biến a: a = a.Subtring(3, 1);
Có thể xem 1 chuỗi là 1 mảng các ký tự. Như vậy hoàn toàn có thể truy xuất đến
từng ký tự như truy xuất đền phần tử của mảng.
Ở trên chỉ là các phương thức hay dùng ngoài ra còn rất nhiều phương thức.
Ứng dụng lớp string vào việc xử lý chuỗi
Thực hiện chuẩn hóa 1 chuỗi họ tên của người dùng với các yêu cầu
Cắt bỏ hết các khoảng trắng dư ở đầu cuối chuỗi. Các từ cách nhau một khoảng
trắng nếu phát hiện có nhiều hơn 1 khoảng trắng thì thực hiện cắt bỏ.
Viết hoa chữ cái đầu tiên của mỗi từ, các chữ cái tiếp theo thì viết thường.
Ý tưởng
Cắt khoảng trắng dư ở đầu và cuối chuỗi thì ta có thể sử dụng phương thức Trim.
Khoảng trắng ở giữa thì ta sẽ duyệt cả chuỗi nếu phát hiện có 2 khoảng trắng thì
thay thế nó bằng 1 một khoảng trắng. Để làm điều này ta có thể dùng:
- IndexOf để phát hiện khoảng trắng.
- Replace để thay thế 2 khoảng trắng thành 1 khoảng trắng.
Viết hoa chữ cái đầu và viết thường các chữ cái còn lại thì ta có thể cắt chuỗi họ tên
ra thành các từ và ứng với mỗi từ ta thực hiện như yêu cầu đề bài. Để làm điều này ta
có thể sử dụng:
- Split để cắt ra các từ.
- Substring để cắt ra các chữ cái mong muốn.
- ToUpper để viết hoa và ToLower để viết thường.
Code tham khảo
/* Khai báo 1 biến kiểu chuỗi tên là FullName
* Khai báo 1 biến Result chứa kết quả chuẩn hoá chuỗi.
* Giá trị biến FullName được nhập từ bàn phím. */
string FullName;
string Result = "";
Console.Write(" Moi ban nhap ho va ten: ");
FullName = Console.ReadLine();
/* Cắt các khoảng trắng dư ở đầu và cuối chuỗi */
FullName = FullName.Trim();
/* Trong khi còn tìm thấy 2 khoảng trắng
* thì thực hiện thay thế 2 khoảng trắng bằng 1 khoảng trắng*/
while (FullName.IndexOf(" ") != -1)
{
FullName = FullName.Replace(" ", " ");
}
/* Cắt chuỗi họ tên ra thành mảng các từ.
* Sau đó duyệt mảng để chuẩn hoá từng từ một.
* Khi duyệt mỗi từ ta thực hiện cắt ra chữ cái đầu trên và lưu trong biến FirstChar
* Cắt các chữ cái còn lại và lưu trong biến OtherChar.
* Thực hiện viết hoa chữ cái đầu và viết thường các chữ cái còn lại.
* Cuối cùng là lưu chữ vừa chuẩn hoá vào biến Result. */
string[] SubName = FullName.Split(' ');
for (int i = 0; i < SubName.Length; i++)
{
string FirstChar = SubName[i].Substring(0, 1);
string OtherChar = SubName[i].Substring(1);
SubName[i] = FirstChar.ToUpper() + OtherChar.ToLower();
Result += SubName[i] + " ";
}
Console.WriteLine(" Ho ten cua ban la: " + Result);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 57
Kết quả khi chạy chương trình:
Chương trình trên vẫn còn 1 lỗi nhỏ đó là nếu như không nhấn phím space để tạo
khoảng trắng mà nhấn phím Tab thì chương trình sẽ không ra kết quả như ý. Hãy thử
vận dụng kiến thức đã học để giải quyết xem sao!
Lớp StringBuilder
Đặc điểm
Lớp StringBuilder được .NET xây dựng sẵn giúp chúng ta thao tác trực tiếp với
chuỗi gốc và giúp tiết kiệm bộ nhớ hơn so với lớp String.
Đặc điểm của StringBuilder là:
- Cho phép thao tác trực tiếp trên chuỗi ban đầu.
- Có khả năng tự mở rộng vùng nhớ khi cần thiết.
- Không cho phép lớp khác kế thừa
Từ 2 đặc điểm này đã làm nổi bật lên ưu điểm của StringBuilder so với String đó là
ít tốn bộ nhớ. Cụ thể qua ví dụ sau:
Ở 2 dòng lệnh có thể thấy bộ nhớ sẽ được lưu trữ như sau:
- Đầu tiên tạo 1 vùng nhớ đối tượng kiểu string tên là Value.
- Tạo 1 vùng nhớ chứa giá trị “Kteam”.
- Khi thực hiện toán tử cộng trên 2 chuỗi sẽ tạo ra 1 vùng nhớ nữa để chứa
giá trị chuỗi mới sau khi cộng.
- Cuối cùng là phép gán sẽ thực hiện trỏ đối tượng Value sang vùng nhớ chứa
chuỗi kết quả của phép cộng.
Như vậy ta thấy sẽ có 1 vùng nhớ không sử dụng nhưng vẫn còn nằm trong bộ nhớ,
đó là vùng nhớ chứa giá trị “How” – giá trị ban đầu của biến Value.
Đối với StringBuilder thì khác:
Ở 2 câu lệnh trên bộ nhớ sẽ lưu trữ như sau:
- Tạo một vùng nhớ cho đối tượng MutableValue chứa giá trị “How”.
- Tạo một vùng nhớ chứa giá trị “Kteam”.
- Mở rộng vùng nhớ của MutableValue để nối chuỗi “Kteam” vào sau chuỗi
“How”.
Rõ ràng là ta không tạo ra quá nhiều vùng nhớ và cũng không lãng phí bất cứ vùng
nhớ nào.
Sử dụng
Cách khởi tạo 1 đối tượng StringBuilder có đôi chút khác so với String.
Cú pháp:
Khởi tạo một đối tượng rỗng:
Khởi tạo một đối tượng chứa 1 chuỗi cho trước:
string Value = "How";
Value = Value + "Kteam";
StringBuilder MutableValue = new StringBuilder("How");
MutableValue.Append("Kteam");
StringBuilder <tên bi n> = new StringBuilder()ế ;
StringBuilder <tên bi n> = new StringBuilder(<chu i giá tr >);ế
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 58
Trong lớp StringBuilder có các phương thức như: Remove, Insert, Replace được sử
dụng hoàn toàn giống như lớp String.
Chỉ có vài phương thức mới các bạn cần chú ý:
Tên phương thức Ý nghĩa
Append(string Value) Nỗi chuỗi Value vào sau chuỗi ban đầu
Clear() Xáo toàn bộ nội dung trong đối tượng (lưu ý không phải xóa vùng
nhớ của đối tượng)
ToString() Chuyển đổi đối tượng kiểu StringBuider sang kiểu String
Lưu ý:
Các bạn nhớ đây là đối tượng kiểu StringBuilder nên thao tác với chuỗi như gán,
nối chuỗi, . . . phải thông qua các phương thức chứ không thể thực hiện trực tiếp được.
Giữa String và StringBuilder đều có cái hay riêng của nó. Tuỳ vào từng yêu cầu của
bài toán mà nên sử dụng cho hợp lý, tránh lạm dụng quá nhiều 1 kiểu:
- Thông thường đối với các bài toán đòi hỏi thao tác nhiều với chuỗi gốc như cộng
chuỗi, chèn chuỗi, xoá bỏ một số ký tự, . . . thì nên sử dụng StringBuilder để tối
ưu bộ nhớ.
- Còn lại thì nên sử dụng String để việc thao tác thuận tiện hơn.
XXIII. Struct trong C# cơ bản
Struct là một kiểu dữ liệu có cấu trúc, được kết hợp từ các kiểu dữ liệu nguyên thuỷ
do người lập trình định nghĩa để thuận tiện trong việc quản lý dữ liệu và lập trình.
Xét bài toán sau:
Ta cần lưu trữ thông tin của 10 sinh viên với mỗi sinh viên gồm có các thông tin như
- Mã số.
- Họ tên.
- Nơi sinh.
- CMND.
Khi đó, để lưu thông tin của 1 sinh viên ta cần 4 biến chứa 4 thông tin trên. Nếu
muốn lưu thông tin 10 sinh viên thì cần 40 biến. Chắc không quá nhiều, nhưng nếu
muốn lưu thông tin của 1000, 10000 sinh viên thì sao?
Số lượng biến lúc này rất nhiều khiến cho code dài dòng khó thao tác, khó kiểm
soát.
Từ đó mới đưa ra khái niệm kiểu dữ liệu có cấu trúc để giải quyết vấn đề trên.
Ý tưởng là đóng gói các thông tin đó vào 1 đối tượng duy nhất. Như vậy thay vì
phải khai báo 40 biến thì ta chỉ cần khai báo 1 mảng 10 phần tử mà mỗi phần tử có kiểu
dữ liệu ta đã định nghĩa.
Đặc điểm của struct
Là một kiểu dữ liệu tham trị.
Dùng để đóng gói các trường dữ liệu khác nhau nhưng có liên quan đến nhau.
Bên trong struct ngoài các biến có kiểu dữ liệu cơ bản còn có các phương thức, các
struct khác.
Muốn sử dụng phải khởi tạo cấp phát vùng nhớ cho đối tượng thông qua toán tử
new.
Struct không được phép kế thừa.
Khai báo và sử dụng struct
Khai báo
Cú pháp:
Trong đó:
<tên struct> là tên kiểu dữ liệu do mình
tự đặt và tuân thủ theo quy tắc đặt tên.
<danh sách các biến> là danh sách các
biến thành phần được khai báo như khai
báo biến bình thường.
struct <tên struct>
{
public <danh sách các bi n>;ế
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 59
Từ khoá public là từ khoá chỉ định phạm vi truy. Trong ngữ cảnh hiện tại thì có thể
hiểu từ khoá này giúp cho người khác có thể truy xuất được để sử dụng.
Ví dụ:
Với khai báo này ta đã có 1 kiểu dữ liệu mới tên là SinhVien. Và có thể khai báo
biến, sử dụng nó như sử dụng các kiểu dữ liệu khác.
Nếu như kiểu int có thể chứa số nguyên, kiểu double có thể chứa số thực thì kiểu
SinhVien vừa khai báo có thể chứa 5 trường thông tin con là MaSo, HoTen, DiemToan,
DiemLy, DiemVan.
Lưu ý: bên trong vẫn còn 2 khai báo chưa được nhắc đến đó là:
- Constructor (hàm khởi tạo).
- Các phương thức mà mình muốn cung cấp để hỗ trợ người dùng khi thao tác
với dữ liệu bên trong struct.
Sử dụng:
Ta có thể truy xuất đến từng thành phần dữ liệu của struct thông qua toán tử “.”
Kèm theo tên thành phần muốn truy xuất.
Xét bài toán sau: Viết chương trình lưu trữ thông tin của sinh viên bao gồm: mã
số, họ tên, điểm toán, điểm lý, điểm văn. Thực hiện nhập thông tin cho 1 sinh viên và
tính điểm trung bình theo công thức (toán + lý + văn)/3.
Chương trình tham khảo:
struct SinhVien
{
public int MaSo;
public string HoTen;
public double DiemToan;
public double DiemLy;
public double DiemVan;
}
struct SinhVien
{
public int MaSo;
public string HoTen;
public double DiemToan;
public double DiemLy;
public double DiemVan;
}
static void NhapThongTinSinhVien(out SinhVien SV)
{
Console.Write(" Ma so: ");
SV.MaSo = int.Parse(Console.ReadLine());
Console.Write(" Ho ten: ");
SV.HoTen = Console.ReadLine();
Console.Write(" Diem toan: ");
SV.DiemToan = Double.Parse(Console.ReadLine());
Console.Write(" Diem ly: ");
SV.DiemLy = Double.Parse(Console.ReadLine());
Console.Write(" Diem van: ");
SV.DiemVan = Double.Parse(Console.ReadLine());
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 60
Kết quả chương trình:
Trong chương trình trên ta có thể thấy:
Kiểu dữ liệu SinhVien có thể dùng làm kiểu dữ liệu cho biến, parametter
cho phương thức. Ngoài ra còn có thể làm kiểu trả về cho phương thức.
Các thành phần dữ liệu bên trong được truy xuất thông qua dấu “.”
Vì struct là kiểu tham trị nên khi truyền vào các phương thức thì giá trị của
nó sau khi kết thúc phương thức sẽ không thay đổi. Do đó cần sử dụng từ
khoá out để có thể cập nhật giá trị thay đổi khi kết thúc phương thức.
static void XuatThongTinSinhVien(SinhVien SV)
{
Console.WriteLine(" Ma so: " + SV.MaSo);
Console.WriteLine(" Ho ten: " + SV.HoTen);
Console.WriteLine(" Diem toan: " + SV.DiemToan);
Console.WriteLine(" Diem ly: " + SV.DiemLy);
Console.WriteLine(" Diem van: " + SV.DiemVan);
}
static double DiemTBSinhVien(SinhVien SV)
{
return (SV.DiemToan + SV.DiemLy + SV.DiemVan) / 3;
}
static void Main(string[] args)
{
/** Khai báo 1 kiểu dữ liệu SinhVien với các trường thông tin như đề bài.
* Khai báo và khởi tạo 1 đối tượng SV1 kiểu SinhVien.*/
SinhVien SV1 = new SinhVien();
Console.WriteLine(" Nhap thong tin sinh vien: ");
/** Đây là hàm hỗ trợ nhập thông tin sinh viên.
* Sử dụng từ khoá out để có thể cập nhật giá trị nhập được ra biến SV1 bên ngoài
* khi kết thúc gọi hàm (có thể xem lại bài Hàm trong C#).*/
NhapThongTinSinhVien(out SV1);
Console.WriteLine("*********");
Console.WriteLine(" Thong tin sinh vien vua nhap la: ");
XuatThongTinSinhVien(SV1);
Console.WriteLine(" Diem TB cua sinh vien la: " +
DiemTBSinhVien(SV1));
Console.ReadLine();
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 61
XXIV. Enum trong C#
Enum là từ khoá dùng để khai báo một kiểu liệt kê (Enumeration). Kiểu liệt kê là
một tập hợp các hằng số do người dùng tự định nghĩa.
Nói cách khác, enum là cách mà C# hỗ trợ người dùng gom nhóm các hằng số lại
với nhau và có chung một tên gọi (thường các hằng số này sẽ có liên quan với nhau ví
dụ như các trạng thái của 1 sự vật, các tính chất của 1 sự vật, . . .)
Đặc điểm của enum:
Là một kiểu dữ liệu tham trị
Enum không được phép kế thừa
Khai báo sử dụng Enum
Cú pháp:
Trong đó:
<tên enum> là tên kiểu liệt kê do mình
tự đặt và tuân thủ theo quy tắc đặt tên.
<danh sách các biểu tượng hằng> là
danh sách các biểu tượng hằng thành phần
mỗi biểu tượng hằng cách nhau bằng dấu “ , ”.
Ví dụ:
Với khai báo này ta đã có 1 kiểu liệt kê tên là Color.
Về bản chất, các biểu tượng hằng RED, BLUE, YELLOW này
đại diện cho các số nguyên lần lượt là 0, 1, 2.
Như vậy, nếu như chúng ta sử dụng cách khai báo hằng bình thường thì ta có thể
khai báo như sau:
Lưu ý:
Ta hoàn toàn có thể quy định giá trị cho từng biểu tượng
hằng bằng cách trực tiếp khi khai báo. Ví dụ:
Khi đó các biểu tượng hằng RED, BLUE, YELLOW sẽ đại
diện cho các số nguyên lần lượt là 2, 4, 6
Nếu ta không quy định giá trị cho các biểu tượng hằng thì
giá trị của biểu tượng hằng đầu tiên sẽ mặc định là 0 và tăng
dần cho các biểu tượng hằng tiếp theo.
Sử dụng
Ta có thể truy xuất đến từng biểu tượng hằng của enum thông qua toán tử “.” Kèm
theo tên biểu tượng hằng muốn truy xuất.
Ví dụ: Corlor.RED;
Lưu ý:
Mặc dù bản chất các biểu tượng hằng là đại diện cho các số nguyên nhưng bạn
không thể so sánh trực tiếp chúng với các số nguyên được mà phải ép kiểu. Ví dụ:
enum <tên enum>
{
<danh sách các biểu tượng hằng>
}
enum Color
{
RED,
BLUE,
YELLOW
}
public const int RED = 0;
public const int BLUE = 1;
public const int YELLOW = 2;
enum Color
{
RED = 2,
BLUE = 4,
YELLOW = 6,
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 62
Để chương trình không báo lỗi ta có thể ép kiểu biểu tượng hằng RED về kiểu int.
Choose == (int)Color.RED
Chúng ta cũng có thể ép kiểu ngược lại từ số nguyên sang kiểu liệt kê.
Ví dụ:
Color Background = (Color)2; // Background s có giá tr là Color.YELLOW
Khi khai báo 1 biến nào đó, các lập trình viên thường cố gắng xây dựng 1 tập các giá
trị của biến đó (nếu có thể) và gom nhóm chúng bằng enum. Điều này rất thường gặp
trong các bộ thư viện của C# và là sự khác biệt giữa C# và Java. Sự khác biệt này có
ảnh hưởng gì đến việc lập trình? Câu hỏi này sẽ được trả lời ngày sau đây.
Sau khi xem qua cách khai báo và sử dụng enum ta có thể thấy rằng enum có những
ưu điểm sau đây:
Chính vì được sử dụng với mục đích gom nhóm các hằng có liên quan với nhau
thành 1 tên duy nhất nên khi sử dụng bạn không cần phải nhớ chính xác tên hằng mà
chỉ cần nhớ tên enum chứa nó là đủ việc còn lại đã có visual studio hỗ trợ.
Chỉ cần gõ tên enum và dấu “.” Visual studio đã liệt kê
sẵn danh sách các biểu tượng hằng bên trong nó. Điều
này giúp cho việc lập trình dễ dàng hơn nhiều.
Hơn thế nữa visual studio còn hỗ trợ giúp bạn tìm ra
tên enum phù hợp với biến đang cần gán giá trị.
XXV. Regular Expression trong C#
Một số lớp hỗ trợ regular expression
Khi áp dụng một biểu thức quy tắc lên một chuỗi mẫu nào đó thì kết quả trả về có
thể là nhiều chuỗi con thoả mãn biểu thức quy tắc trên. Khi đó các chuỗi con sẽ được
lưu vào trong 1 tập hợp có tên là MatchCollection, mỗi phần tử trong tập hợp là 1 biến
có kiểu Match.
MatchCollection là 1 kiểu tập hợp chứa danh sách các đối tượng kiểu Match. Vì đây
cũng là 1 tập hợp bình thường nên có thể thao tác như các tập hợp khác.
Ví dụ sử dụng Match và MatchCollection:
Cho chuỗi gốc là “-howkteam.com 10092016-”. Giả sử bạn muốn lấy ra tất cả các
số trong chuỗi. Vậy pattern của chúng ta đơn giản chỉ là “\d”.
Nếu bạn chỉ viết code đơn gian thế này:
Kết quả là 1
enum Color
{
RED,
BLUE,
YELLOW
}
int Choose = int.Parse(Console.ReadLine());
if (Choose == Color.RED)// lỗi vì không thể so sánh trực tiếp 1 enum với 1 số nguyên
{
Console.WriteLine("Ban vua chon mau do");
}
Regex reg = new Regex(@"\d");
Match result = reg.Match("-howkteam.com
10092016-");
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 63
Một đối tượng kiểu Match sẽ chứa 1 chuỗi con kết quả, để xem chuỗi con kết quả
này ta sẽ gọi phương thức ToString(). Ngoài ra nó cũng có các thuộc tính và phương
thức khác như:
Chúng ta mong muốn lấy ra tất cả các số chứ không phải 1 số thế này!
Kết quả có vẻ như ý rồi nhưng ta có thể thấy code không mấy gọn gàng cũng như
mất thời gian xử lý ở chỗ hàm NextMatch().
Đến đây ta thử sử dụng MatchCollection đã được .NET hỗ trợ sẵn xem thế nào:
Regex reg = new Regex(@"\d");
// Tạo 1 đối tượng Regex chứa pattern của mình
Match result = reg.Match("-howkteam.com 10092016-");
// Tạo 1 đối tượng Match để chứa kết quả.
do
{
Console.WriteLine(result.ToString());
result = result.NextMatch();
// Chuyển qua kết quả trùng khớp kế tiếp
}
while (result != Match.Empty);// Kiểm tra xem đã hết kết quả trùng khớp chưa
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 64
Để ý là lúc này ta dùng hàm Matches()
để lấy kết quả chứ không phải Match().
Matches() trả về 1 tập hợp các đối tượng
Match (MatchCollection) nên ta có thể
dùng foreach để duyệt tập hợp
Kết quả khi chạy vẫn giống như cách
trên:
Lưu ý:
Nếu như không tìm thấy chuỗi con
phù hợp thì sẽ trả về 1 đối tượng Match có giá trị rỗng chứ không phải null. Khi đó
để kiểm tra có giá trị hay không ta sẽ sử dụng:
Group và GroupCollection
Group trong Regular Expression là chỉ cách ta gom nhóm các biểu thức lại thành
cụm và có thể đặt tên cho nhóm để dễ quản lý và thao tác.
Lớp Group là 1 lớp đại diện cho 1 gom nhóm trong biểu thức. Có 1 điểm chúng ta
nên biết là lớp Group là lớp cha của lớp Match!
Tại sao lại có khái niệm này? – Bởi vì:
Trong 1 kết quả trùng khớp sẽ có thể chứa nhiều thông tin khác nhau và ta mong
muốn các thể lấy ra từng thành phần nhỏ trong đó mà không phải dùng thêm 1 biểu thức
chính quy nào nữa.
Và trong biểu thức chính quy ban đầu ta sẽ gom nhóm các thành phần con thành
các group và đặt tên cho chúng như vậy khi lấy được 1 chuỗi kết quả ta muốn lấy các
thành phần con bên trong đó thì ta chỉ cần gọi chúng thông qua tên đã đặt.
Cú pháp:
(?<tên group>)
Trong đó:
( ): là cú pháp gom nhóm các biểu thức
?<tên group>: là cú pháp đặt tên cho group. Bạn có thể không đặt tên cho group
cũng được. Lưu ý là tên group bạn phải viết liền không dấu và nên tuân theo quy tắc đặt
tên.
Ví dụ:
Để lấy ra chuỗi giờ phút giây ta có thể làm như sau:
“\d+:\d+:\d+”
Nhưng nếu bây giờ mình muốn lấy ra giờ, phút, giây riêng để xử lý thì phải làm
sao?
Nếu viết 3 biểu thức thì khá dài dòng. Khi đó ta sử dụng group và đặt tên cho chúng
như sau:
(?<hours>\d+):(?<minutes>\d+):(?<seconds>\d+)
Trong đó “hours”, “minutes”, “seconds” là tên do mình đặt cho 3 group.
Regex reg = new Regex(@"\d");
foreach (Match item in reg.Matches("-howkteam.com
10092016-"))
{
Console.WriteLine(item.ToString());
}
if (result == Match.Empty)
{
// Câu l nh
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 65
Để lấy ra danh sách các gom nhóm trong 1 chuỗi con kết quả ta dùng thuộc tính
Groups” trong lớp Match. Thuộc tính này trả về 1 GroupCollection. GroupCollection
là 1 lớp chứa danh sách các gom nhóm trong biểu thức, mỗi phần tử của danh sách là 1
đối tượng kiểu Group.
Ví dụ sử dụng Group và GroupCollection:
Kết quả khi chạy chương trình
trên:
.NET đã hỗ trợ chúng ta truy xuất
các phần tử trong danh sách
GroupCollection thông qua chỉ số phần
tử là tên các group chúng ta đã đặt trong biểu thức ở trên.
Capture và CaptureCollection
Mỗi khi tìm thấy bất kỳ 1 chuỗi con nào (bao gồm cả các group) thì C# sẽ bắt nó lại
và lưu vào 1 đối tượng có kiểu Capture. Và danh sách tất cả các Capture chính là 1
CaptureCollection.
Một điểm cần biết nữa là Capture là lớp cha của lớp Group!
Tại sao lại có lớp Capture này? – Câu trả lời sẽ nằm trong tình huống sau:
Cho chuỗi sau “10:30:15 IBM 192.168.1.2 INTEL” hãy viết biểu thức lấy ra giờ
phút giây, địa chỉ ip và tên công ty.
Lúc này ta sẽ có biểu thức sau:
“(?<times>(\d|:)+)\s(?<company>\S+)\s(?<ip>(\d|\.)+)\s(?<company>\S+)”
Lưu ý: là mình sẽ không tập trung vào giải thích chi tiết biểu thức mà tập trung vào
cách sử dụng các lớp.
Ở đây mình có 2 tên công ty bên trong nên mình đặt chung 1 tên group là company
với mong muốn lấy ra 2 tên công ty từ group.
Chương trình kiểm tra:
// Tạo 1 biểu thức
Regex re = new Regex(@"(?<hours>\d+):(?<minutes>\d+):(?
<seconds>\d+)");
/* Duyệt qua các kết quả trùng khớp
* Lấy ra giá trị các group thông qua chỉ số phần tử là tên các group đã đặt trong biểu thức*/
foreach (Match item in re.Matches("30/04/2017 10:15:12
192.168.1.2"))
{
Console.WriteLine(" Match: " + item.ToString());
Console.WriteLine(" Hours: " + item.Groups["hours"]);
Console.WriteLine(" Minutes: " + item.Groups["minutes"]);
Console.WriteLine(" Seconds: " + item.Groups["seconds"]);
}
Regex RE = new Regex(@"(?<times>(\d|:)+)\s" + @"(?<company>\S+)\s"
+ @"(?<ip>(\d|\.)+)\s" + @"(?<company>\S+)");
foreach (Match item in RE.Matches("10:30:15 IBM 192.168.1.2
INTEL"))
{
Console.WriteLine(" time: " + item.Groups["times"]);
Console.WriteLine(" company: " + item.Groups["company"]);
Console.WriteLine(" ip: " + item.Groups["ip"]);
Console.WriteLine(" company: " + item.Groups["company"]);
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 66
Nhưng khi chạy chương trình kết quả lại như thế này:
Ta thấy có tới 2 công ty thoả mãn là INTEL và IBM nhưng chương trình chỉ in ra
được INTEL. Chúng ta có thể kiểm tra bằng phần mềm RegEx Tester để chắc rằng biểu
thức mình viết đúng:
Các bạn hãy để ý chỗ Group 4 (company) trong hình. Rõ ràng ta lấy ra được 2 giá
trị nhưng chỉ hiển thị được giá trị sau cùng.
Đến đây có bạn sẽ nói rằng tại sao không đổi tên group khác đi là xong? Câu trả lời
là trong ví dụ trên mình chỉ có 2 công ty nhưng giả sử có đến 100 công ty trong chuỗi
thì sao? Bạn phải đặt 100 biến khác nhau?
Vì thế chúng ta sẽ tận dụng đặc điểm của Capture và sử dụng chúng để giải quyết.
Chương trình sẽ như sau:
Ở đây để lấy ra danh sách các Capture (CaptureCollection) ta sử dụng thuộc tính
Captures.
Kết quả khi chạy đoạn code trên:
Regex RE = new Regex(@"(?<times>(\d|:)+)\s" + @"(?<company>\S+)\s" +
@"(?<ip>(\d|\.)+)\s" + @"(?<company>\S+)");
foreach (Match item in RE.Matches("10:30:15 IBM 192.168.1.2 INTEL"))
{
Console.WriteLine(" time: " + item.Groups["times"]);
Console.WriteLine(" ip: " + item.Groups["ip"]);
Console.Write(" company: ");
/*Lấy ra tất cả các capture bắt được trong group company và duyệt lần lượt chúng
* Sau đó ta có thể sử dụng hàm ToString() hoặc thuộc tính Value để lấy giá trị của Capture
*/
foreach (Capture i in item.Groups["company"].Captures)
{
Console.Write(i.ToString() + " ");
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 67
Bảng các ký hiệu Regular Expression thông dụng:
Regular Expression hay tiếng Việt được gọi là Biểu thức chính quy, là một cấu trúc
rất mạnh để mô tả một chuỗi theo cách thống nhất chung.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 68
Regular Expression bao gồm tập hợp các ký tự, toán tử hay ký hiệu toán học nhằm
biểu thị một chuỗi theo cấu trúc chung mà mọi người học theo. Có thể xem Regular
Expression như một loại tiếng lóng dùng chung trong lập trình.
Bạn có thể trích lọc một hay nhiều chuỗi có cấu trúc chung từ một đoạn văn bản
hay một chuỗi ra.
Bạn có thể tìm kiếm, thay đổi nội dung của chuỗi một cách dễ dàng. Thay vì phải
ngồi cắt chuỗi mỏi mệt như trước đây.
Từ đây, với Regular Expression. Bạn hoàn toàn có thể trích lọc dữ liệu từ các đoạn
html theo ý.
Các ký hiệu của Regular Expression
Trước khi tìm hiểu về các ký hiệu chúng ta cùng xem qua một ví dụ mẫu để nắm
được cấu trúc chung của Regular Expression
Chuỗi mẫu:
-howkteam.com-10092016-
Pattern:
\d{8}
Kết quả tìm kiếm:
10092016
Một cách nhìn khác:
-howkteam.com-10092016-
Hay một cách dễ nhớ
-howkteam.com-\d{8}-
Trong đó
\d là ký hiệu biểu thị cho số
{8} là biểu thị ký tự trước đó xuất hiện 8
lần
Vậy có thể đọc câu pattern này là: Tìm ra
chuỗi con là dãy 8 số liên tiếp nhau.
>>> kết quả là trùng khớp.
Nếu bạn thay số 8 thành số 6 tức là
pattern thành :
\d{6}
Thì kết quả sẽ thành 100920 hay
-howkteam.com-10092016-
Hay một cách dễ nhớ
-howkteam.com-\d{6}16-
Hay có thể mường tượng Regular
Expression này như string.Format
Từ đây bạn có thể nhận thấy. Regular Expression bản chất là tìm ra một hoặc nhiều
chuỗi con thỏa mãn cấu trúc chung được định ra. (bảng kí hiệu)
Pattern .
Đại diện cho một ký tự bất kỳ. Ngoại trừ ký hiệu \n
Chuỗi mẫu:
-howkteam.com-10092016-
Pattern:
.
Kết quả:
Dsach các ký t xu t hi n trong chu i m u:{,h,o,w,k,...,1,6,-}
Cách nhìn khác:
Lấy ra từng từ bên trong chuỗi mẫu.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 69
Hình minh họa:
Pattern \d
Đại diện cho ký tự số. Tương đương pattern [0-9].
Chuỗi mẫu:
-howkteam.com-10092016-
Pattern:
\d
Kết quả:
Danh sách các số xuất hiện trong chuỗi mẫu: {1,0,0,9,2,0,1,6}
Cách nhìn khác:
Lấy ra từng số bên trong chuỗi mẫu.
Hình minh họa:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 70
Pattern \D
Ký tự không phải số. Hay có thể hiểu là phủ định của \d
Chuỗi mẫu:
-howkteam.com-10092016-
Pattern:
\D
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 71
Pattern \s
Ký tự khoảng trắng. Tương đương [\f\n\r\t\v]
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
com\s1009
Kết quả:
Pattern \S
Ký tự không phải khoảng trắng. Tương đương phủ định của \s hay tương
đương [^\f\n\r\t\v]
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
\S
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 72
Pattern \w
Ký tự word (gồm chữ cái và chữ số, dấu gạch dưới _ ) tương đương [a-zA-Z_0-9]
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
\w
Kết quả:
Pattern \W
Ký tự không phải word. ơng đương phủ định của \w hay [^a-zA-Z_0-9]
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
\W
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 73
Pattern ^
Bắt đầu một chuỗi hay một dòng
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
^-howkteam
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 74
Pattern $
Kết thúc một chuỗi hay một dòng
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
16-$
Kết quả:
Pattern \A
Bắt đầu môt chuỗi
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
\A-howkteam
Kết quả:
Pattern \z
Kết thúc một chuỗi
Chuỗi mẫu:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 75
-howkteam.com 10092016-
Pattern:
16-\z
Kết quả:
Pattern |
Ký tự so trùng tương đương với or. Dùng để kết hợp nhiều điều kiện.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
k|h|9
Kết quả:
Pattern [abc]
Khớp với một ký tự nằm trong nhóm này là a hay b hay c đều được.
Chuỗi mẫu:
-howkteam.com 10092016-
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 76
Pattern:
[k9h]
Kết quả:
Pattern [a-z]
Khớp với ký tự trong khoảng a đến z. az là ký tự hiện hữu trong bảng ASCII.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
[1-8]
Kết quả:
Pattern [^abc]
Không trùng bất kỳ ký tự a b hay c. Tương đương phủ định của [abc]
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
[^1-8]
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 77
Kết quả:
Pattern ()
Xác định một group. Một biểu thức đơn lẻ trong pattern.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
([0-3])|([5-7])
Lấy ra tất cả các số trong khoảng [0-3] hoặc trong khoảng [5-7]
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 78
Pattern ?
Khớp với từ đứng trước xuất hiện 0 hay 1 lần.
Chuỗi mẫu:
-howkteam.com 10092016-
Lấy ra tất cả số 1 xuất hiện 0 hay 1 lần
Pattern:
1?
Kết quả:
Pattern *
Khớp với từ đứng trước 0 lần trở lên.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
1*
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 79
Pattern +
Khớp với từ đứng trước 1 lần trở lên.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
1+
Kết quả:
Pattern {n}
Với n là số. Khớp với từ đứng trước xuất hiện đúng n lần.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
0{2}
Lấy ra các kết quả là một cặp số 0 liền nhau.
Hãy thử với n là một giá trị số khác nhé.
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 80
Pattern {n,}
Với n là số. Khớp với từ đứng trước xuất hiện đúng n lần trở lên.
Chuỗi mẫu:
-howkteam.com 10092016-
Pattern:
0{1,}
Kết quả:
Pattern {m,n}
Với m và n là số. Khớp với từ đứng trước xuất hiện từ m đến n lần.
Chuỗi mẫu:
-howkteam.com 100010092016-
Pattern:
0{1,2}
Kết quả:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 81
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 82
Phần nâng cao
Collections
Các lớp hỗ trợ lưu trữ, quản lý và thao tác với các đối tượng một cách có thứ tự.
Các lớp này nằm trong namespace System.Collections.
Một số đặc điểm của Collections
Là một mảng có kích thước động:
Không cần khai báo kích thước khi khởi tạo.
Có thể tăng giảm số lượng phần tử trong mảng một cách linh hoạt.
Có thể lưu trữ một tập hợp đối tượng thuộc nhiều kiểu khác nhau.
Hỗ trợ rất nhiều phương thức để thao tác với tập hợp như: tìm kiếm, sắp xếp, đảo
ngược…
Mỗi collections được tổ chức thành một lớp nên cần khởi tạo đối tượng trước khi sử
dụng.
Khi nào sử dụng Collection?
Chúng ta đã từng tìm hiểu một kiểu dữ liệu dùng để quản lý danh sách đối tượng đó là
kiểu mảng. Vậy Collections có gì hay hơn mảng? Khi nào dùng mảng và khi nào dùng
Collections?
Đầu tiên là những điểm mạnh của Collections
Bên trong Collections có nhiều lớp đa dạng hỗ trợ cho từng mục đích khác nhau.
Nếu như mảng chỉ có thể truy xuất phần tử thông qua chỉ số thì các Collections có thế
truy xuất thông qua chỉ số hoặc thông qua key.
Đối với danh sách cần thao tác tìm kiếm nhiều thì Collections cũng có lớp hỗ trợ
giúp việc tìm kiếm nhanh hơn nhiều so với mảng nguyên thuỷ.
Trong trường hợp danh sách cần thay đổi số lượng phần tử liên tục (thêm hoặc
xoá phần tử) thì Collections cũng hỗ trợ sẵn.
Ngoài ra trong namespace System.Collections còn hỗ trợ sẵn 2 cấu trúc dữ liệu
kinh điển đó là STACK QUEUE nên chỉ cần lấy ra sử dụng mà không cần cài đặt
lại.
Vậy khi nào sử dụng mảng khi nào sử dụng Collections?
Theo lời khuyên từ Microsoft thì:
Mảng thường được dùng để làm việc với một số lượng cố định các đối
tượng strongly-typed (có thể hiểu đơn giản, strongly-typed là kiểu dữ liệu không bị
thay đổi một cách đột ngột, tường minh).
Collections cung cấp một cách linh hoạt hơn để làm việc với danh sách. Ta có thể
tăng giảm số lượng phần tử một cách tự động. Một số Collections còn hỗ trợ lưu trữ
danh sách dưới dạng Key – Value giúp truy xuất, tìm kiếm một cách nhanh chóng.
Một số Collections thông dụng
Một số lớp Collections được sử dụng phổ biến:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 83
Arraylist trong C# là một collection giúp lưu trưc quản lý một danh sách các đối tượng theo
kiểu mảng (truy cập các phần tử bên trong thông qua chỉ số index)
Rất giống mảng các object nhưng có thể thêm hoặc xóa các phần tử một cách linh hoạt
và có thể tự điều chỉnh kích cỡ một cách tự động.
Để sử dụng các Collection trong .NET ta cần thêm thư viện System.Collection bằng câu
lệnh: using System.Collection;
Vì đây là ArrayList là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng
toán tử new:
// kh i t o ArrayList r ng
ArrayList MyArray = new ArrayList();
Ngoài ra, bạn cũng có thể khởi tạo 1 ArrayList chứa các phần tử được sao chép từ 1
Collection khác:
/* kh i t o 1 ArrayList có kích th c b ng v i MyArray2. ướ
* sao chép toàn b ph n t trong MyArray2 vào MyArray3.*/
Arraylist MyArray3 = new ArrayList(MyArray2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong ArrayList
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong ArrayList.
Capacity Trả về 1 số nguyên cho biết số phần tử mà ArrayList có thể
chứa (sức chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự
động tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho ArrayList.
Một số phương thức thông dụng trong ArrayList:
TÊN PHƯƠNG THỨC Ý NGHĨA
Add(object Value) Thêm đối tượng Value vào cuối ArrayList.
LỚP MÔ TẢ
ArrayList Lớp cho phép lưu trữ và quản lý các phần tử giống mảng.
Tuy nhiên, không giống như trong mảng, ta có thể thêm hoặc xoá phần
tử một cách linh hoạt
và có thể tự điều chỉnh kích cỡ một cách tự động.
HashTable Lớp lưu trữ dữ liệu dưới dạng cặp Key – Value.
Khi đó ta sẽ truy xuất các phần tử trong danh sách này thông qua Key
(thay vì thông qua chỉ số phần tử như mảng nh thường).
SortedList Là s kêt hợp của ArrayList HashTable. Tức là dữ liệu sẽ lưu dưới
dạng Key – Value.
Ta có thể truy xuất các phần tử trong danh sách thông qua Key hoặc
thông qua chỉ số phần tử.
Đặc biệt là các phần tử trong danh sách này luôn được sắp xếp theo giá
trị của Key.
Stack Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc LIFO (Last In
First Out).
Queue Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc FIFO (First In
First Out).
BitArray Lớp cho phép lưu trữ và quản lý một danh sách các bit.
Giống mảng các phần tử kiểu bool với true biểu thị cho
bit 1 và false biểu thị cho bit 0.
Ngoài ra BitArray còn hỗ trợ một số phương thức cho việc tính toán
trên bit.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 84
AddRange
(ICollection ListObject)
Thêm danh sách phần tử ListObject vào cuối ArrayList.
BinarySearch
(object Value)
Tìm kiếm đối tượng Value trong ArrayList theo thuật toán tìm kiếm
nhị phân.
Nếu tìm thấy sẽ trả về vị trí của phần tử ngược lại trả về giá trị âm.
Lưu ý:ArrayList phải được sắp xếp trước khi sử dụng hàm.
Clear() Xoá tất cả các phần tử trong ArrayList.
Clone() Tạo 1 bản sao từ ArrayList hiện tại.
Contains(object Value) Kiểm tra đối tượng Value có tồn tại trong ArrayList hay không.
GetRange
(int StartIndex,
int EndIndex)
Trả về 1 ArrayList bao gồm các phần tử từ vị trí StartIndex đến
EndIndex trong ArrayList ban đầu.
IndexOf(object Value) Trả về vị trí đầu tiên xuất hiện đối tượng Value trong ArrayList.
Nếu không tìm thấy sẽ trả về -1.
Insert(int Index,
object Value)
Chèn đối tượng Value vào vị trí Index trong ArrayList.
InsertRange
(int Index, ICollection List
Object)
Chèn danh sách phần tử ListObject vào vị
trí Index trong ArrayList.
LastIndexOf(object Value) Trả về vị trí xuất hiện cuối cùng của đối tượng Value trong
ArrayList . Nếu không sẽ trả về -1.
Remove(object Value) Xoá đối tượng Value xuất hiện đầu tiên trong ArrayList.
Reverse() Đảo ngược tất cả phần tử trong ArrayList.
Sort() Sắp xếp các phần tử trong ArrayList theo thứ tự tăng dần.
ToArray() Trả về 1 mảng các object chứa các phần tử được sao chép
từ ArrayList.
Những phương thức trên này cũng khá đơn giản nên sẽ không tập trung vào chúng.
Thay vào đó là hướng dẫn những thứ hay hơn.
Phương thức Sort() sẽ thực hiện sắp xếp danh sách theo thứ tự tăng dần. Vậy nếu danh
sách gồm các đối tượng mà mỗi đối tượng là 1 lớp có nhiều thuộc tính thì hàm Sort này
biết sắp xếp tăng dần theo thuộc tính nào?
Để trả lời câu hỏi này ta cần biết thêm là còn 1 hàm Sort nữa được hỗ trợ sẵn
trong ArrayList có cú pháp như sau:
Sort ( IComparer comparer)
Công dụng:
Hàm này cho phép người dùng tự định nghĩa cách sắp xếp theo ý mình.
Tham số truyền vào là 1 lớp có kế thừa từ interface IComparer .
Interface IComparer chứa 1 phương thức duy nhất là:
int Comparer (object x, object y).
Phương thức này sẽ trả về 3 giá trị:
Bé hơn 0 nếu x < y.
Lớn hơn 0 nếu x > y.
Bằng 0 nếu x = y.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 85
Từ những quy định về inferface này ,chỉ cần khai báo 1 lớp kế thừa
interface IComparer và định nghĩa nội dụng cho phương thức Comparer có giá trị trả về
theo những quy định trên.
Ví dụ: đầu tiên, có 1 lớp person đại diện cho 1 con người gồm 2 thông tin Name và Age
Tiếp theo định nghĩa 1 lớp kế thừa interface ICompare và override lại phương thức
Compare trong interface này. Hàm Compare sẽ so sánh dựa trên thuộc tính tuổi tăng dần.
Cuối cùng là chương trình chính thử tạo 1 ArrayList và thêm 1 vài đối
tượng Person vào sau đó gọi hàm Sort() và xem kết quả.
Kết quả
Hashtable trong C#:
ArrayList arrPersons = new ArrayList();// Tạo 1 danh sách kiểu ArrayList rỗng
// Thêm 3 Person vào danh sách
arrPersons.Add(new Person("Nguyen Van A", 18));
arrPersons.Add(new Person("Nguyen Van B", 25));
arrPersons.Add(new Person("Nguyen Van C", 20));
Console.WriteLine("Danh sach Person ban dau: ");// In danh sách Person ban đầu ra.
foreach (Person item in arrPersons)
{
Console.WriteLine(item.ToString());
}
/* Thực hiện sắp xếp danh sách Person theo tiêu chí đã được định nghĩa
* trong phương thức Compare của lớp SortPerson (tuổi tăng dần). */
arrPersons.Sort(new SortPersons());
// In danh sách Person đã được sắp xếp ra màn hình.
Console.WriteLine();
Console.WriteLine("Danh sach Person da duoc sap xep theo tuoi tang dan: ");
foreach (Person item in arrPersons)
{
Console.WriteLine(item.ToString());
}
public class SortPersons : IComparer
{
public class Person
{
private string name;
private int age;
public string Name
/// <summary>
/// Override phương thức ToString để
khi cần có thể in thông tin của object
ra cho nhanh.
/// </summary>
/// <summary>
/// Tạo 1 constructor có tham số để
tiện cho việc khởi tạo nhanh đối
tượng Person với các giá trị cho sẵn.
/// </summary>
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 86
Là một Collections lưu trữ dữ liệu dưới dạng cặp Key - Value.
Key đại diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá
trị tương ứng của khoá đó.
Sẽ dử dụng Key để truy cập đến Value tương ứng.
Key Value đều là kiểu object nên ta có thể lưu trữ được mọi kiểu dữ liệu từ những
kiểu cơ sở đến kiểu phức tạp (class).
Nếu các Key của Hashtable là các số nguyên tăng dần từ 0 thì Hashtable nhìn trông
giống ArrayList (chỉ là giống về bề ngoài ti chứ n trong rất khác nhau).
Hashtable là 1 Collections nên để s dụng ta cn tm t
vin System.Collections bằng câu lnh:
using System.Collections;
Vì Hashtable là một lp nên tc khi s dụng ta cn khởi to vùng nhớ bằng tn
t new:
// kh i t o 1 Hashtable r ng
Hashtable MyHash = new Hashtable();
Cũng có chỉ định sc chứa (Capacity) ngay lúc khởi to bằng cách thông
qua constructor đưc hỗ trợ sn:
// khởi to 1 Hashtable chỉ định Capacity ban đầu là 5
Hashtable MyHash2 = new Hashtable(5);
Ngoài ra, cũng có thể khở to 1 Hashtable chứa các phần t đưc sao cp t 1
Hashtable khác:
/* Khởi to 1 Hashtable có ch tc bằng với MyHash2.
* Sao cp tn độ phần t trong MyHash2 o MyHash3. */
Hashtable MyHash3 = new Hashtable(MyHash2);
Một sthuộc nh phương thức htr sẵn trong Hashtabe:
Một số thuộc tính thông dụng trong Hashtable:
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong Hashtable.
Keys Trả về 1 danh sách chứa các Key trong Hashtable.
Values Trả về 1 danh sách chứa các Value trong Hashtable.
Một số phương thức thông dụng trong Hashtable:
TÊN PHƯƠNG THỨC Ý NGHĨA
Add(object Key, object Value) Thêm 1 cặp Key - Value vào Hashtable.
Clear() X tất cả các phần tử trong Hashtable.
Clone() Tạo 1 bản sao từ Hashtable hiện tại.
ContainsKey(object Key) Kiểm tra đối tượng Key có tồn tại trong Hashtable hay
không.
ContainsValue(object Value) Kiểm tra đối tượng Value có tồn tại trong Hashtable hay
không.
CopyTo(Array array, int Index) Thực hiện sao chép tất cả phần tử trong Hashtable sang
mảng một chiều array từ vị trí Index của array.
Lưu ý: array phải là mảng các object hoặc mảng
các DictionaryEntry.
Remove(object Key) Xoá đối tượng có Key xuất hiện đầu tiên
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 87
trong Hashtable.
Một số lưu ý về Hashtable
Mỗi một phần tử trong Hashtable (bao gồm 1 cặp Key - Value) được C# định nghĩa là 1
đối tượng có kiểu DictionaryEntry. Trong DictionaryEntry có 2 thuộc tính chính:
Key: trả về giá trị Key của phần tử hiện tại.
Value: trả về giá trị Value của phần tử hiện tại.
Ví dụ: thử dùng foreach duyệt 1 Hashtable và in ra giá trị Key – Value của mỗi phần tử:
Việc truy xuất các phần tử trong Hashtable giống như truy xuất các phần tử mảng nhưng
thông qua Key. Ví dụ:
Console.WriteLine(MyHash["One"]);
Trong đó:
MyHash là tên của Hashtable.
One là tên Key cần truy xuất.
MyHash["One"] sẽ lấy ra giá trị Value tương ứng với Key trên.
Nên cẩn thận khi truy xuất các phần tử trong Hashtable thông qua Key:
Nếu thực hiện lấy giá trị 1 phần tử trong Hashtable với Key không tồn tại thì sẽ ra giá
trị null và không báo lỗi.
Nếu thực hiện gán giá trị cho 1 phần tử trong Hashtable tại vị trí Key không tồn tại
thì Hashtable sẽ tự thêm 1 phần tử mới với Key và Value như trên. Điều này có thể làm
phát sinh thêm các phần tử không mong muốn trong danh sách.
Ví dụ:
SortedList
// Tạo một Hashtable đơn giản với 3 phần tử
Hashtable hash = new Hashtable();
hash.Add("K", "Kteam");
hash.Add("H", "HowKteam");
hash.Add("FE", "Free Education");
/* * Duyệt qua các phần tử trong Hashtable.
* Vì mỗi phần tử là 1 DictionaryEntry nên ta chỉ định kiểu dữ liệu cho item là DictionaryEntry luôn.
* Thử in ra màn hình cặp Key - Value của mỗi phần tử được duyệt. */
foreach (DictionaryEntry item in hash)
{
Console.WriteLine(item.Key + "\t" + item.Value);
}
Console.WriteLine("Key 'VT' is not exists");
}
// Thử in ra số phần tử ban đầu của Hashtable
Console.WriteLine("\nCount: " + hash.Count);
foreach (DictionaryEntry item in hash)
{
Console.WriteLine(item.Key + "\t" + item.Value);
// In ra màn hình giá trị Value trong 1 Key không tồn tại.
Console.WriteLine(hash["VT"]);
// Để chắc chắn là null ta thử kiểm tra bằng điều kiện if.
if (hash["VT"] == null)
{
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 88
SortedList cũng là một Collections lưu trữ dữ liệu dưới dạng cặp Key - Value. Key đại
diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá trị tương ứng của
khoá đó.
Đặc điểm của SortedList
Là 1 Hashtable nhưng các giá trị được sắp xếp theo Key. Việc sắp xếp này được thực
hiện một cách tự động mỗi khi thêm 1 phần tử mới vào SortedList.
Có thể truy xuất đến các phần tử trong SortedListthông qua Key(như Hashtable)
hoặc thông qua chỉ số phần tử (như ArrayList).
SortedList chính là sự kết hợp giữa ArrayList với Hashtable .
Do SortedList cũng là 1 Collections nên để sử dụng ta cần thêm thư
viện System.Collections bằng câu lệnh:
using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// kh i t o 1 SortedList r ng
SortedList MySL = new SortedList();
Cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
// kh i t o 1 SortedList và ch đ nh Capacity ban đ u là 5
SortedList MySL2 = new SortedList(5);
Cũng có thể khởi tạo 1 SortedList chứa các phần tử được sao chép từ
một SortedList khác:
/*Kh i t o 1 SortedList có kích th c b ng v i MySL2. ướ
* Sao chép toàn đ ph n t trong MySL2 vào MySL3. */
SortedList MySL3 = new SortedList(MySL2);
Vì các phần tử của SortedList được sắp xếp tự động theo Key nên ta cũng có thể chỉ ra
cách sắp xếp do mình tự định nghĩa thông qua constructor có sẵn:
/** đ nh nghĩa 1 l p PersonComparer có th c thi 1 interface IComparer
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 89
* Sau đó override l i ph ng th c Compare. ươ
* S d ng l p trên đ truy n vào constructor c a SortedList.
*/
SortedList MySL4 = new SortedList(new PersonComparer());
Ngoài ra cũng có thể khởi tạo 1 SortedList chứa các phần tử được sao chép từ
1 SortedList khác đồng thời sắp xếp lại các phần tử theo 1 cách sắp xếp khác:
/* * Tạo 1 SortedList mới và sao chép các phần tử từ MySL3 đồng thời sắp xếp các phần tử lại
* theo cách sắp xếp được định nghĩa trong lớp PersonComparer. */
SortedList MySL5 = new SortedList(MySL3, new PersonComparer());
Một số thuộc tính và phương thức hỗ trợ sẵn trong SortedList
Vì SortedList là sự kết hợp giữa ArrayList và Hashtable nên nó sẽ mang các thuộc tính,
phương thức giống 2 Collections trên và một vài phương thức mới.
Một số thuộc tính thông dụng trong SortedList:
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong SortedList .
Capacity Trả về 1 số nguyên cho biết số phần tử mà SortedList có thể chứa (sức
chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự
động tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho SortedList.
Keys Trả về 1 danh sách chứa các Key trong SortedList.
Values Trả về 1 danh sách chứa các Value trong SortedList.
Một số phương thức thông dụng trong SortedList:
TÊN PHƯƠNG THỨC Ý NGHĨA
Add(object Key, object Value) Thêm 1 cặp Key - Value vào SortedList.
Clear() Xoá tất cả các phần tử trong SortedList.
Clone() Tạo 1 bản sao từ SortedList hiện tại.
ContainsKey(object Key) Kiểm tra đối tượng Key có tồn tại trong SortedList hay
không.
ContainsValue(object Value) Kiểm tra đối tượng Value có tồn tại trong SortedList hay
không.
CopyTo(Array array, int Index) Thực hiện sao chép tất cả phần tử trong SortedList sang mảng
một chiều array từ vị trí Index của array.
Lưu ý: array phải là mảng các object hoặc mảng
các DictionaryEntry.
GetByIndex(int Index) Trả về giá trị Value tại vị trí Index trong SortedList.
GetKey(int Index) Trả về giá trị Key tại vị trí Index trong SortedList.
GetKeyList() Trả về 1 List các Key trong SortedList.
GetValueList() Trả về 1 List các Value trong SortedList.
IndexOfKey(object Key) Trả về 1 số nguyên là chỉ số phần tử của 1 Key trong
SortedList
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 90
Remove(object Key) Xoá đối tượng có Key xuất hiện đầu tiên trong SortedList.
RemoveAt(int Index) Xoá đối tượng tại vị trí Index trong SortedList.
SetByIndex
(int Index, object Value)
Gán giá trị Value mới tại vị trí Index trong SortedList.
Về cách sử dụng thì thao tác hoàn toàn giống với Hashtable .
Một số lưu ý
Nếu bạn muốn các giá trị Key là các đối tượng thuộc 1 lớp nào đó thì bạn phải định
nghĩa cách so sánh đối tượng đó. Nếu không chương trình sẽ báo lỗi vì nó không biết phải
sắp xếp các Key này như thế nào. Ví dụ đoạn chương trình sau:
Khi chạy chương trình trên sẽ nhận được lỗi sau:
Để khắc phục điều này ta có thể định nghĩa 1 lớp thực thi interface IComparer và định
nghĩa cách sắp xếp trong hàm Comparer:
Sau đó sử dụng constructor của SortedList để truyền lớp này vào:
/// <summary>
/// Định nghĩa 1 lớp thực thi interface IComparer.
/// override phương thức Comparer và định nghĩa cách sắp xếp trong đó.
/// Chi tiết bạn có thể xem lại bài ArrayList trong C#.
/// </summary>
class PersonComparer : Icomparer
{
public int Compare(object x, object y)
{
Person a = x as Person;
Person b = y as Person;
if (a == null || b == null)
{ throw new InvalidOperationException(); }
else
{
if (a.Age > b.Age)
{ return 1; }
else if (a.Age == b.Age)
{ return 0; }
else
{ return -1; }
}
}
}
SortedList MySL6 = new SortedList();
MySL6.Add(new Person("HowKteam", 20), 10);
MySL6.Add(new Person("Kteam", 2), 15);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 91
// t o 1 SortedList và truy n vào cách s p x p các Key trong ế
SortedList SortedList MySL6 = new SortedList(new PersonComparer());
Khi đó chương trình sẽ căn cứ vào hàm Comparer để sắp xếp các Key.
Khi các Key hoặc Value là các đối tượng thuộc 1 lớp nào đó thì thì ta nên override lại
phương thức ToString để việc in ra Key và Value không bị lỗi:
Ví dụ với đoạn chương trình trên khi chưa override phương thức ToString thì kết quả
hiển thị là:
Ta thử override phương thức ToString trong lớp Person:
Stack
Stack (hay còn gọi là ngăn xếp) là một cấu trúc dữ liệu hoạt động theo nguyên lý LIFO
(Last In First Out). Người ta hay gọi nó là ngăn xếp bởi vì nó hoạt động giống như ngăn
xếp trong thực tế vậy.
Giả sử bạn xếp chồng các chiếc đĩa lên nhau.
Khi đó chiếc đĩa được xếp đầu tiên sẽ nằm dưới cùng và chiếc đĩa được xếp sau cùng sẽ
nằm trên đầu. Nếu bạn muốn lấy đĩa ra sử dụng chắc chắn bạn sẽ lấy chiếc đĩa nằm trên
cùng ra. Đây chính là nguyên lý Last In First Out (vào cuối ra đầu) của Stack.
Quay lại vấn đề lập trình, hãy tưởng tượng cả chồng đĩa đó chính là 1 danh sách (Stack),
các chiếc đĩa là các phần tử của danh sách. Thế là ta đã có cấu trúc dữ liệu Stack trong lập
trình rồi.
Trong C#, Stack là một Collections đại diện cho một danh sách hoạt động theo nguyên
LIFO.
Vì C# đã hỗ trợ sẵn cấu trúc dữ liệu Stack rồi nên chỉ tìm hiểu cách sử dụng thôi.
Đặc điểm của Stack
Là một danh sách lưu trữ các đối tượng nhưng không thể truy cập các phần tử thông qua
chỉ số phần tử được.
Hành động thêm phần tử vào Stack được gọi là Push (đẩy vào).
Hành động lấy phần tử ra khỏi Stack được gọi là Pop (đẩy ra). Và luôn luôn lấy ra phần
tử được thêm vào cuối cùng.
Do stack cũng là 1 collection nên khi sử dụng cần thêm thư viện System.Collection.
using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// kh i t o 1 Stack r ng
Stack MyStack = new Stack();
Cũng có thể chỉ định sức chứa(Capacity) ngay lúc khởi tạo bằng cách thông qua
construction được hỗ trợ sẵn:
// khởi tạo 1 Stack và chỉ định sức chứa ban đầu là 5
Stack MyStack = new Stack(5);
public override string ToString()
{
return Name + " : " + Age;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 92
Bạn cũng có thể khởi tạo 1 Stack chứa các phần tử được sao chép từ một danh sách
khác:
Một số thuộc tính và phương thức hỗ trợ sẵn trong Stack
Một số thuộc tính thông dụng trong Stack:
TÊN THUỘC TÍNH Ý NGHĨA
Count
Trả về 1 số nguyên là số phần tử hiện
trong Stack.
Một số phương thức thông dụng trong Stack:
TÊN PHƯƠNG THỨC Ý NGHĨA
Clear() Xoá tất cả các phần tử trong Stack.
Clone() Tạo 1 bản sao từ Stack hiện tại.
Contains (object Value) Kiểm tra đối tượng Value có tồn tại trong Stack hay không.
CopyTo
(Array array, int Index)
Sao chép tất cả phần tử trong Stack sang mảng một chiều array
từ vị trí Index của Array
Peek() Trả về giá trị của đối tượng tại vị trí trên
cùng trong Stack (phần tử thêm vào cuối cùng)
nhưng không xoá phần tử khỏi Stack.
Pop() Trả về giá trị của đối tượng tại vị trí trên cùng trong Stack
(phần tử thêm vào cuối cùng) đồng thời xóa phần tử khỏi Stack
Push(object Value) Thêm một phần tử có giá trị Value vào vị trí trên cùng
trong Stack.
ToArray() Tạo ra 1 mảng các object chứa tất cả các phần tử trong Stack và
trả về mảng đó.
Một số ví dụ về sử dụng Stack
// khởi tạo 1 mảng bất kỳ
ArrayList MyArray = new ArrayList();
MyArray.Add(5);
MyArray.Add(9);
MyArray.Add(10);
// Khởi tạo 1 Stack và sao chép giá trị của các phần tử từ MyArray vào Stack.
Stack MyStack3 = new Stack(MyArray);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 93
Queue
Queue (hay còn gọi là hàng đợi) là một cấu trúc dữ liệu hoạt động theo nguyên
FIFO (First In First Out). Người ta hay gọi nó là hàng đợi bởi vì nó hoạt động giống
như việc xếp hàng đợi chờ gì đó trong thực tế vậy.
Giả sử ta đến xếp hàng để mua vé như hình dưới:
Khi đó người xếp hàng đầu tiên sẽ là người mua được vé đầu tiên. Đây chính là nguyên
First In First Out (vào trước ra trước) của Queue.
Hãy tưởng tượng những người đứng xếp hàng đó chính là 1 danh sách (Queue), mỗi
người là 1 phần tử của danh sách. Thế là ta đã có cấu trúc dữ liệu Queue trong lập trình
rồi đó.
Trong C#, Queue là một Collections đại diện cho một danh sách hoạt động theo nguyên
FIFO đã trình bày ở trên.
Vì C# đã hỗ trợ sẵn cấu trúc dữ liệu Queue rồi nên chỉ tìm hiểu cách sử dụng nó thôi.
Đặc điểm của Queue
// Tạo 1 Stack rỗng
Stack MyStack4 = new Stack();
// Thực hiện thêm vài phần tử vào Stack thông qua hàm Push.
MyStack4.Push("Education");
MyStack4.Push("Free");
MyStack4.Push("HowKteam");
// Thử sử dụng các phương thức của Stack.
Console.WriteLine(" So phan tu hien tai cua Stack la: {0}", MyStack4.Count);
// Lưu ý ở đây ta chỉ muốn xem giá trị mà không muốn nó khỏi Stack thì ta sẽ dùng Peek.
Console.WriteLine(" Phan tu dau cua Stack la: {0}", MyStack4.Peek());
// Thử kiểm tra lại số phần tử để chắc chắn rằng hàm Peek không xoá phần tử ra khỏi Stack.
Console.WriteLine(" So phan tu cua Stack sau khi goi ham Peek: {0}",
MyStack4.Count);
// Thực hiện xoá các phần tử ra khỏi Stack.
Console.WriteLine(" Popping...");
int Length = MyStack4.Count;
for (int i = 0; i < Length; i++)
{
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 94
Là một danh sách lưu trữ các đối tượng nhưng không thể truy cập các phần tử thông qua
chỉ số phần tử được.
Hành động thêm phần tử vào Queue được gọi là Enqueue (xếp hàng).
Hành động lấy phần tử ra khỏi Queue được gọi là Dequeue (ra khỏi hàng). Và luôn luôn
lấy ra phần tử được thêm vào đầu tiên.
Queue rất giống Stack chỉ khác ở nguyên lý hoạt động thôi nên Stack có gì Queue sẽ có
cái tương tự như vậy.
Do Queue cũng là 1 collection nên khi sử dụng cần thêm thư viện System.Collection.
using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// kh i t o 1 Queue r ng
Queue MyQueue = new Queue();
Cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
// khởi tạo 1 Queue và chỉ định sức chứa ban đầu là 5
Queue MyQueue2 = new Queue(5);
Cũng có thể khởi tạo 1 Queue chứa các phần tử được sao chép từ một danh sách khác:
// khởi tạo 1 mảng bất kỳ
ArrayList MyArray = new ArrayList();
MyArray.Add(5);
MyArray.Add(9);
MyArray.Add(10);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 95
// Khởi tạo 1 Queue và sao chép giá trị của các phần tử từ MyArray vào Queue.
Queue MyQueue3 = new Queue(MyArray);
Một số phương thức và thuộc tính thoog dụng của Queue
Một số thuộc tính thông dụng trong Queue:
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong Queue .
Một số phương thức thông dụng trong Queue:
TÊN PHƯƠNG THỨC Ý NGHĨA
Clear() Xoá tất cả các phần tử trong Queue .
Clone() Tạo 1 bản sao từ Queue hiện tại.
Contains (object Value) Kiểm tra đối tượng Value có tồn tại trong Queue hay không.
CopyTo
(Array array, int Index)
Thực hiện sao chép tất cả phần tử trong Queue sang mảng một chiều
array từ vị trí Index của array.
Enqueue () Trả về giá trị của đối tượng tại vị trí đầu trong Queue (phần tử thêm
vào đầu tiên)
nhưng không xoá phần tử khỏi Queue.
Dequeue() Trả về giá trị của đối tượng tại vị trí đầu trong Queue(phần tử thêm
vào đầu tiên)
đồng thời xoá phần tử khỏi Queue.
Push(object Value) Thêm một phần tử có giá trị Value vào đầu Queue.
ToArray() Tạo ra 1 mảng các object chứa tất cả các phần tử trong Queue và trả
về mảng đó.
Một ví dụ đơn giản về sử dụng Queue
Cách sử dụng Queue hoàn toàn tương tự cách sử dụng Stack. Một ví dụ đơn giản về sử
dụng Queue:
// Tạo 1 Queue rỗng
Queue MyQueue4 = new Queue();
// Thực hiện thêm vài phần tử vào Queue thông qua hàm Enqueue.
MyQueue4.Enqueue("HowKteam");
MyQueue4.Enqueue("Free");
MyQueue4.Enqueue("Education");
// Thử sử dụng các phương thức của Queue.
Console.WriteLine(" So phan tu hien tai cua Queue la: {0}",
MyQueue4.Count);
// Lưu ý ở đây ta chỉ muốn xem giá trị mà không muốn nó khỏi Queue thì ta sẽ dùng Peek.
Console.WriteLine(" Phan tu dau cua Queue la: {0}", MyQueue4.Peek());
// Thử kiểm tra lại số phần tử để chắc chắn rằng hàm Peek không xoá phần tử ra khỏi Queue.
Console.WriteLine(" So phan tu cua Queue sau khi goi ham Peek: {0}",
MyQueue4.Count);
// Thực hiện xoá các phần tử ra khỏi Queue thông qua hàm Dequeue.
Console.WriteLine(" Popping...");
int Length = MyQueue4.Count;
for (int i = 0; i < Length; i++)
{
Console.Write(" " + MyQueue4.Dequeue());
}
Console.WriteLine();
// Kiểm tra lại số phần tử của Queue sau khi Pop
Console.WriteLine(" So phan tu cua Queue sau khi Pop la: {0}",
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 96
Kết quả:
BitArray
BitArray là một Collections giúp quản lý, lưu trữ một danh sách các bit (0 hoặc 1),
được biểu diễn như kiểu Boolean (kiểu luận lý). Trong đó true biểu thị cho
bit 1 và false biểu thị cho bit 0.
Nó được sử dụng khi ta cần lưu trữ danh sách các bit mà chưa biết trước số lượng. Ta có
thể truy cập đến các phần tử trong BitArray thông qua chỉ số như ArrayList.
Sẽ có thắc mắc sao không dùng mảng các đối tượng kiểu bool mà lại dùng BitArray?
Thì câu trả lời là BitArray giúp tiết kiệm bộ nhớ hơn rất nhiều:
Mặc dù kiểu bool chỉ lưu 2 giá trị true hoặc false nhưng lại tốn đến 1 bytes cho mỗi
biến kiểu bool.
Trong khi đó mỗi phần tử trong BitArray tốn đúng 1 bit để lưu trữ.
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new.
Lưu ý: là ta không thể khởi tạo 1 BitArray rỗng!
Đầu tiên có thể khởi tạo BitArray và cho biết số phần tử ban đầu của BitArray:
/* Khởi tạo 1 BitArray có 10 phần tử.
* Mỗi phần tử có giá trị mặc định 0 (false).*/
BitArray MyBA = new BitArray(10);
Nếu bạn không muốn giá trị mặc định là false thì bạn có thể chỉ định giá trị mặc
định thông qua constructor:
/* Khởi tạo 1 BitArray có 10 phần tử.
* Mỗi phần tử có giá trị mặc định 1 (true). */
BitArray MyBA2 = new BitArray(10, true);
Có thể khởi tạo một BitArray từ một mảng bool có sẵn:
/* Khởi tạo 1 BitArray từ một mảng bool có sẵn. */
bool[] MyBools = new bool[5] { true, false, true, true, false };
BitArray MyBA3 = new BitArray(MyBools); // 1 0 1 1 0
Hoặc khởi tạo một BitArray từ một mảng byte có sẵn:
/** Khởi tạo 1 BitArray từ một mảng byte có sẵn. */
byte[] MyBytes = new byte[5] { 1, 2, 3, 4, 5 };
BitArray MyBA4 = new BitArray(MyBytes);
Đối với trường hợp này ta có thể cách lưu trữ của nó như sau:
Đầu tiên ta đã biết 1 byte = 8 bits.
Khi đó trình biên dịch sẽ chuyển các số kiểu byte sang dạng 8 bits và lưu lần lượt
vào BitArray. Như vậy trường hợp trên sẽ có 5 số kiểu byte tương đương với 40 bits sẽ
được lưu vào BitArray.
Thử in giá trị các phần tử BitArray trên để kiểm chứng:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 97
Để dễ nhìn thì cứ in được 8 bits thì mình
sẽ xuống dòng. Lưu ý đây chỉ là cách hiển
thị lên màn hình thôi chứ bên trong vẫn là 1
mảng các bit duy nhất:
Gợi ý: Có thể thử đổi các bit trên xem có
giống 5 số kiểu byte ban đầu không!
Tương tự, cũng có thể khởi tạo một BitArray từ một mảng các số nguyên int có sẵn:
/* Khởi tạo 1 BitArray từ một mảng int có sẵn.*/
int[] MyInts = new int[5] { 1, 2, 3, 4, 5 };
BitArray MyBA5 = new BitArray(MyInts);
Trường hợp này tương tự như trên nhưng
lúc này kiểu int chiếm 4 bytes nên ta sẽ
đổi 4 bytes = 32 bits. Và Trình biên dịch sẽ
chuyển mỗi số nguyên int sang 32 bits và
lưu vào ArrayList:
Một số thuộc tính và phương thức hỗ trợ sẵn trong BitArray
Một số thuộc tính thông dụng trong BitArray:
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong BitArray.
Length Trả về 1 số nguyên là số phần tử hiện có trong BitArray.
Đồng thời có thể thay đổi kích thước của BitArray bằng cách gán giá trị
mới cho thuộc tính này.
Một số phương thức thông dụng trong BitArray:
TÊN PHƯƠNG THỨC Ý NGHĨA
And(BitArrayValue) Thực hiện phép toán AND bit giữa dãy bit hiện tại với dãy bit Value
và trả về 1 BitArray là kết quả của phép toán trên.
Clone() Tạo 1 bản sao từ BitArray hiện tại.
CopyTo
(Array array, int Index)
Thực hiện sao chép tất cả phần tử trong BitArray sang
mảng một chiều array từ vị trí Index của array.
Get(int Index) Trả về giá trị của bit tại vị trí Index trong BitArray.
Not() Trả về 1 BitArray là kết quả của phép toán NOT trên dãy bit hiện tại.
Or(BitArray Value) Trả về 1 BitArray là kết quả của phép toán OR giữa dãy bit hiện tại với
dãy bit Value.
Set(int Index, bool Value) Gán giá trị cho bit tại v trí Index với giá trị mới là Value.
SetAll(bool Value) Gán giá trị cho toàn bộ các bit trong BitArray với giá trị mới là Value.
Xor(BitArray Value) Trả về 1 BitArray là kết quả của phép toán XOR giữa dãy bit hiện tại
với dãy bit Value.
Lưu ý:
Các phép toán AND, OR, NOT, XOR phải được thực hiện trên 2 BitArray cùng
độ dài nếu không sẽ báo lỗi.
Các phép toán AND, OR, NOT, XOR sẽ làm thay đổi cả BitArray gọi nó. Ví dụ:
BitArray A = new BitArray(5);
BitArray B = new BitArray(5, true);
A.And(B);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 98
Thì kết quả của phép AND sẽ được cập nhật giá trị vào BitArray A.
Một ví dụ đơn giản về sử dụng BitArray
Một chương trình đơn giản về BitArray:
Generic trong C#
Template được dùng để tạo các lớp, các hàm mà không cần quan tâm đến đối số kiểu dữ
liệu là gì. Template được đưa ra nhằm mục đích tăng tính tái sử dụng lại mã nguồn.
Generic cho phép định nghĩa 1 hàm, một lớp mà không cần chỉ ra đối số kiểu dữ liệu là
gì. Tùy vào kiểu dữ liệu mà người dùng truyền vào thì nó sẽ hoạt động theo kiểu dữ liệu
đó.
Ví dụ: hàm hoán đổi giá trị 2 số nguyên:
// Khởi tạo 1 BitArray từ mảng bool có sẵn
bool[] MyBool2 = new bool[5] { true, false, true, true, false };
BitArray MyBA6 = new BitArray(MyBool2);
// Khởi tạo 1 BitArray có 2 phần tử và có giá trị mặc định là 1 (true)
bool[] MyBool3 = new bool[] { false, true, true, false, false };
BitArray MyBA7 = new BitArray(MyBool3);
Console.Write(" Gia tri cua MyBA6: ");
PrintBits(MyBA6, 5);
Console.Write(" Gia tri cua MyBA7: ");
PrintBits(MyBA7, 5);
Console.WriteLine(" Thuc hien cac phep toan AND, OR, NOT, XOR tren MyBA6 va
MyBA7: ");
// thực hiện sao chép giá trị của MyBA6 ra để không làm thay đổi nó
BitArray AndBit = MyBA6.Clone() as BitArray;
AndBit.And(MyBA7);
Console.Write(" Ket qua cua phep toan AND: ");
PrintBits(AndBit, 5);
BitArray OrBit = MyBA6.Clone() as BitArray;
OrBit.Or(MyBA7);
Console.Write(" Ket qua cua phep toan OR: ");
PrintBits(OrBit, 5);
BitArray XorBit = MyBA6.Clone() as BitArray;
XorBit.Xor(MyBA7);
Console.Write(" Ket qua cua phep toan XOR: ");
PrintBits(XorBit, 5);
BitArray NotBit = MyBA6.Clone() as BitArray;
NotBit.Not();
Console.Write(" Ket qua cua phep toan NOT tren MyBA6: ");
PrintBits(NotBit, 5);
Public static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 99
Mọi thứ đều hoạt động tốt cho tới khi hoán đổi 2 số thực. khi đó phải viết lại 1 hàm
Swap mới với kiểu dưc liệu của tham số truyền vào là kiểu số thực.
Mặc dù thao tác giống nhau nhưng phải viết hàm 2 lần. Chính vì vậy, Generic ra đời để
giúp giảm thiểu việc code và tăng tính năng sử dụng.
Nếu sử dụng Generic ta viết như sau:
Chỉ cần đặt 1 chữ cái nào đó thay cho kiểu dữ liệu và khi gọi hàm ta chỉ ra kiểu dữ liệu
đang thao tác là gì. Ví dụ:
Khi gọi cú pháp bên dưới thì hàm Swap sẽ chạy và thay ký tự T thành kiểu dữ
liệu int tương ứng.
Phía trên là Generic cho phương thức. Tiếp theo là Generic cho lớp cũng tương tự.
int a = 5, b = 7;
double c = 1.2, d = 5.6;
Swap<int>(ref a, ref b);
Swap<double>(ref c, ref d);
public class MyGeneric<T>
{
private T[] items;
public T[] Items
{
get { return items; }
}
public MyGeneric(int Size)
{
items = new T[Size];
}
public T GetByIndex(int Index)
{
// Nếu index vượt ra khỏi chỉ số phần tử của mảng thì ném ra ngoại lệ
if (Index < 0 || Index >= items.Length)
{
throw new IndexOutOfRangeException();
}
else
{
return items[Index];
}
}
Swap<int>(ref a, ref b)
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 100
Thay vì viết một lớp cho kiểu int, long, double gì đó thì giờ thay nó bằng T còn lại thao
tác như bình thường.
Đến khi sử dụng thì ta truyền kiểu dữ liệu thích hợp vào. Ví dụ:
// Kh i t o 1 m ng s nguyên ki u int có 5 ph n t
MyGeneric<int> MyG = new MyGeneric<int>(5);
MyG.SetItemValue(0, 10);
Khi bạn khai báo cú pháp bên dưới thì trình biên dịch sẽ hiểu T trong lớp MyGeneric là
kiểu int và thay thế toàn bộ chữ cái T trong lớp thành int sau đó thực thi.
MyGeneric<int>
Đặc điểm của Generic
Giúp định nghĩa một thao tác dữ liệu với kiểu dữ liệu chung nhất nhìn hạn chế viết code
và tái sử dụng.
Ứng dụng phổ biến nhất của Generic là tạo ra các Generic Collections.
c Collections phổ biến thì giá trị lưu trữ bên trong đều là object.
Điều này gây rất nhiều khó khăn nếu như muốn quản lý 1 danh sách có cùng kiểu.
Vì object có thể chứa được mọi kiểu dữ liệu nên ta khó kiểm soát rằng việc thêm
phần tử có phải cùng kiểu dữ liệu ta mong muốn hay không.
Từ đó Generic Collections ra đời để giúp ta vừa có thể sử dụng được các Collections
vừa có thể hạn chế lỗi xảy ra trong quá trình thực thi.
Ngoài ra, Generic còn giúp hạn chế truy cập nếu như không truyền đúng kiểu dữ liệu.
Một số loại Generic Collections thông dụng
Các Generic Collections đều được xây dựng bắt nguồn từ 1 Collections nào đó có sẵn.
Vì thế với mỗi Collections đã học sẽ có một Generic tương ứng.
Một số Generic Collections được sử dụng phổ biến:
LỚP MÔ TẢ
List<T> Là một Collections giúp lưu trữ các phần tử liên tiếp (giống mảng)
nhưng có khả năng tự mở rộng kích thước.
Generic Collections này là sự thay thế cho ArrayList đã học.
Dictionary
<Tkey, TValue>
Lớp lưu trữ dữ liệu dưới dạng cặp Key – Value.
Khi đó ta sẽ truy xuất các phần tử trong danh sách này thông qua Key
(thay vì thông qua chỉ số phần tử như mảng bình thường).
Generic Collections này là sự thay thế cho Hashtable đã học.
SortedDictionary
<Tkey, TValue>
Là sự kêt hợp của List và Dictionary. Tức là dữ liệu sẽ lưu dưới dạng Key
– Value. Ta có thể truy xuất các phần tử trong danh sách thông
qua Key hoặc thông qua chỉ số phần tử.
public void SetItemValue(int Index, T Value)
{
if (Index < 0 || Index >= items.Length)
{
throw new IndexOutOfRangeException();
}
else
{
items[Index] = Value;
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 101
Đặc biệt là các phần tử trong danh sách này luôn được sắp xếp theo giá trị
của Key.
Generic Collections này là sự thay thế cho SortedList đã học.
Stack<T> Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc LIFO (Last In First
Out).
Generic Collections này là sự thay thế cho Stack đã học.
Queue<T> Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc FIFO (First In First
Out).
Generic Collections là sự thay thế cho Queue đã học.
List
List là 1 Generic Collections đưa ra như một sự thay thế ArrayList vì thế về khái niệm
cũng như sử dụng nó hoàn toàn giống với ArrayList.
List trong C# là một Generic Collections giúp lưu trữ và quản lý một danh sách các đối
tượng theo kiểu mảng (truy cập các phần tử bên trong thông qua chỉ số index).
Để sử dụng các Collection trong .NET ta cần thêm thư viện System.Collection.Generic
bằng câu lệnh.
using System.Collections.Generic;
Vì List là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// kh i t o 1 List các s nguyên r ng
List<int> MyList = new List<int>();
Có thể chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông qua constructor
được hỗ trợ sẵn:
// khởi tạo 1 List các số nguyên và chỉ định Capacity ban đầu là 5
List<int> MyList2 = new List<int>(5);
Ngoài ra bạn cũng có thể khởi tạo 1 List chứa các phần tử được sao chép từ một Generic
Collections khác (lưu ý là có cùng kiểu dữ liệu truyền vào):
/*
* Kh i t o 1 List s nguyên có kích th c b ng v i MyList2. ướ
* Sao chép toàn đ ph n t trong MyList2 vào MyList3.
*/
List<int> MyList3 = new List<int>(MyList2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong List
Một số thuộc tính thông dụng trong List:
TÊN THUỘC TÍNH
Ý NGHĨA
Count
Trả về 1 số nguyên là số phần tử hiện có trong List.
Capacity Trả về 1 số nguyên cho biết số phần tử mà List có thể chứa (sức chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự động
tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho List.
Một số phương thức thông dụng trong List:
TÊN PHƯƠNG THỨC Ý NGHĨA
Add(object Value) Thêm đối tượng Value vào cuối List.
AddRange Thêm danh sách phần tử ListObject vào cuối List.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 102
(ICollection ListObject)
BinarySearch
(object Value)
m kiếm đối tượng Value trong List theo thuật toán tìm kiếm nhị
phân.
Nếu tìm thấy sẽ trả về vị trí của phần tử ngược lại trả về giá trị âm.
Lưu ý: List phải được sắp xếp trước khi sử dụng hàm.
Clear() Xoá tất cả các phần tử trong List.
Contains(T Value) Kiểm tra đối tượng Value có tồn tại trong List hay không.
CopyTo
(T[] array, int Index)
Thực hiện sao chép tất cả phần tử trong List sang mảng một chiều
array từ vị trí Index của array.
Lưu ý: array phải là mảng kiểu T tương ứng.
IndexOf(T Value) Trả về vị trí đầu tiên xuất hiện đối tượng Value trong List.
Nếu không tìm thấy sẽ trả về -1.
Insert
(int Index, T Value)
Chèn đối tượng Value vào vị trí Index trong List.
InsertRange
(int Index, IEnumerable<T>
ListObject)
Chèn danh sách phần tử ListObject vào vị trí Index trong List.
LastIndexOf(T Value) Trả về vị trí xuất hiện cuối cùng của đối tượng Value trong List.
Nếu không tìm thấy sẽ trả về -1.
Remove(T Value) Xoá đối tượng Value xuất hiện đầu tiên trong List.
Reverse() Đảo ngược tất cả phần tử trong List.
Sort() Sắp xếp các phần tử trong List theo thứ tự tăng dần.
ToArray() Trả về 1 mảng kiểu T chứa các phần tử được sao chép từ List.
Sử dụng List tương tự như sử dụng ArrayList. Một ví dụ đơn giản về sử dụng List:
List<string> MyList4 = new List<string>();//Tạo List kiểu string và thêm 2 phần tử vào List.
MyList4.Add("Free"); MyList4.Add("Education");
Console.WriteLine(" List ban dau: ");// In giá trị các phần tử trong List
Console.WriteLine("So luong phan tu trong List la:{0}", MyList4.Count);
foreach (string item in MyList4)
{
Console.Write(" " + item);
}
Console.WriteLine();
MyList4.Insert(0, "HowKteam");// Chèn 1 phần tử vào đầu List
// In lại giá trị các phần tử trong List để xem đã chèn được hay chưa
Console.WriteLine(" List sau khi insert: ");
Console.WriteLine("So luong phan tu trong List la:{0}", MyList4.Count);
foreach (string item in MyList4)
{
Console.Write(" " + item);
}
Console.WriteLine();
// Kiểm tra 1 phần tử có tồn tại trong List hay không
bool isExists = MyList4.Contains("Kteam");
if (isExists == false)
{
Console.WriteLine(" Khong tim thay chuoi Kteam trong List");
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 103
Dictionary
Tương tự như List, Dictionary chính là sự thay thế cho Collections Hashtable. Cho nên
về khái niệm hay sử dụng thì Dictionary đều sẽ giống Hashtable.
Dictionary trong C# là một Collections lưu trữ dữ liệu dưới dạng cặp Key -
Value. Key đại diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá
trị tương ứng của khoá đó. Ta sẽ dử dụng Key để truy cập đến Value tương ứng.
Do Dictionary là 1 Generic Collections nên để sử dụng ta cần thêm thư
viện System.Collections.Generic bằng câu lệnh:
using System.Collections.Generic;
Vì Dictionary là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán
tử new:
// khởi tạo 1 Dictionary rỗng với Key và Value đều có kiểu dữ liệu là chuỗi.
Dictionary<string, string> MyHash = new Dictionary<string, string>();
Bạn cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
/* khởi tạo 1 Dictionary với Key và Value có kiểu chuỗi
* đồng thời chỉ định Capacity ban đầu là 5 */
Dictionary<string, string> MyDic2 = new Dictionary<string, string>(5);
Ngoài ra bạn cũng có thể khởi tạo 1 Dictionary chứa các phần tử được sao chép từ một
Dictionary khác:
/*
* Khởi tạo 1 Dictionary có kích thước bằng với MyDic2.
* Sao chép toàn độ phần tử trong MyDic2 vào MyDic3.
*/
Dictionary<string, string> MyDic3 = new Dictionary<string, string>(MyDic2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong Dictionary
Một số thuộc tính thông dụng trong Dictionary:
TÊN THUỘC TÍNH Ý NGHĨA
Count Trả về 1 số nguyên là số phần tử hiện có trong Dictionary.
Keys Trả về 1 danh sách chứa các Key trong Dictionary.
Values Trả về 1 danh sách chứa các Value trong Dictionary.
Một số phương thức thông dụng trong Dictionary:
TÊN PHƯƠNG THỨC Ý NGHĨA
Add(TKey Key, TValue Value) Thêm 1 cặp Key - Value vào Dictionary.
Clear() Xoá tất cả các phần tử trong Dictionary.
ContainsKey(TKey Key) Kiểm tra đối tượng Key có tồn tại trong Dictionary hay
không.
ContainsValue(TValue Value) Kiểm tra đối tượng Value có tồn tại trong Dictionary hay
không.
Remove(TKey Key) Xoá đối tượng có Key xuất hiện đầu tiên trong Dictionary.
TryGetValue
(TKey Key, TValue Value)
Kiểm tra Key có tồn tại hay không.
Nếu có sẽ trả về true đồng thời trả về giá trị Value tương ứng
qua biến Value.
Ngược lại trả về false.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 104
Một số lưu ý về Dictionary:
Mỗi một phần tử trong Dictionary (bao gồm 1 cặp Key - Value) được C# định nghĩa là
1 đối tượng có kiểu:
KeyValuePair<TKey, TValue>
Trong đó, có 2 thuộc tính chính:
Key: trả về giá trị Key của phần tử hiện tại.
Value: trả về giá trị Value của phần tử hiện tại.
Điều này tương tự như DictionaryEntry trong Hashtable. Vì thế cách sử dụng cũng
tương tự. Ví dụ mình thử dùng foreach duyệt 1 Dictionary và in ra giá trị Key – Value của
mỗi phần tử:
Việc truy xuất các phần tử trong Dictionary giống như truy xuất các phần tử mảng
nhưng thông qua Key.
Ví dụ:
Console.WriteLine(MyDic4["FE"]);
Trong đó:
MyDic4 là tên của Dictionary.
"FE" là tên Key cần truy xuất. Lưu ý là phải cùng kiểu dữ liệu TKey đã được chỉ
định lúc khởi tạo Dictionary.
MyHash["FE"] sẽ lấy ra giá trị Value tương ứng với Key trên.
Khi thao tác với Hashtable nếu như truy xuất đến phần tử có Key không tồn tại sẽ
không báo lỗi nhưng với Dictionary thì không phải vậy. Nếu ta truy xuất đến Key không
tồn tại sẽ nhận được lỗi sau:
Khác biệt giữa Dictionary và HashTable trong C#
HASHTABLE DICTIONARY
// Tạo 1 Dictionary đơn giản và thêm vào 3 phần tử.
Dictionary<string, string> MyDic4 = new Dictionary<string, string>();
MyDic4.Add("FE", "Free Education");
MyDic4.Add("K", "Kteam");
MyDic4.Add("HK", "HowKteam");
/*Duyệt qua các phần tử trong Dictionary.
* Vì mỗi phần tử là 1 KeyValuePair nên ta chỉ định kiểu dữ liệu cho item là KeyValuePair
* Thử in ra màn hình cặp Key - Value của mỗi phần tử được duyệt.*/
foreach (KeyValuePair<string, string> item in MyDic4)
{
Console.WriteLine(item.Key + "\t" + item.Value);
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 105
Threadsafe - Hỗ trợ multi threading không
đụng độ tài nguyên
Không hỗ trợ.
Cặp Key - Value lưu kiểu object Phải xác định cụ thể kiểu dữ liệu của cặp Key - value
Truy xuất phần tử không tồn tại trong
Hashtable sẽ không báo lỗi suy ra return null
Truy xuất phần tử không tồn tại trong Dictionary sẽ
báo lỗi
Hiệu quả cho dữ liệu lớn Không hiệu quả cho dữ liệu lớn
Các phần tử được sắp xếp lại mỗi khi thêm
hoặc xóa các phần tử trong Hashtable.
Các phần tử nằm theo thứ tự được thêm vào.
Tìm kiếm nhanh hơn. Tìm kiếm chậm hơn.
Tuple
Tuple là một kiểu dữ liệu có cấu trúc, giúp lưu trữ các dữ liệu phức tạp mà không cần
phải tạo ra một struct hay class mới.
C# cung cấp cho chúng ta: 8 lớp generic
public class Tuple <T1>
public class Tuple <T1, T2>
public class Tuple <T1, T2, T3>
public class Tuple <T1, T2, T3, T4>
public class Tuple <T1, T2, T3, T4, T5>
public class Tuple <T1, T2, T3, T4, T5, T6>
public class Tuple <T1, T2, T3, T4, T5, T6, T7>
public class Tuple <T1, T2, T3, T4, T5, T6, T7, TRest>
Mỗi lớp Tuple<> đã được định nghĩa sẵn các Property có tên Item1, Item2, Item3,…
tương ứng với các kiểu dữ liệu T1, T2, T3,… được truyền vào. Hình ảnh sau là một ví dụ:
Đây là hình ảnh lớp Tuple<T1, T2>, khi ta truyền 2 kiểu dữ liệu vào T1, T2 thì trong
lớp này sẽ có 2 Property Item1, Item2 có kiểu dữ liệu tương ứng.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 106
Một static class Tuple. Trong lớp sẽ chứa các hàm Create<> nhằm khởi tạo
một Tuple:
Mỗi hàm Create sẽ tương ứng tạo 1 đối tượng kiểu generic Tuple.
Sử dụng Tuple như thế nào khi đã có struct và class?
Trường hợp đầu tiên là khi viết một phương thức và muốn trả về 1 lúc nhiều giá trị.
Lúc này Tuple sẽ dễ dàng giải quyết mà không cần phải tạo thêm struct hay class.
Trong nhiều trường hợp khác muốn tạo nhanh 1 đối tượng với 1 vài thuộc tính và chỉ
sử dụng 1 lần thôi thì việc dùng struct hoặc class là rất lãng phí, làm chương trình trở
nên dài dòng hơn. Khi đó Tuple được sử dụng như một phương án thay thế tốt hơn vì
có sẵn rồi giờ lấy ra dùng thôi không cần khai báo gì nữa.
Ngoài ra, Tuple đã override sẵn:
o Phương thức Equals (phương thức dùng để so sánh 2 đối tượng).
o Phương thức ToString (Phương thức chuyển giá trị đối tượng sang chuỗi).
o Phương thức GetHashCode (Phương thức trả về mã băm của một đối tượng,
dùng để hỗ trợ so sánh 2 đối tượng).
Từ đó chúng ta chỉ việc sử dụng mà không cần phải viết lại những phương thức này.
Cách sử dụng Tuple
Đầu tiên là khởi tạo một đối tượng Tuplechúng ta có 2 cách:
Cách 1: Thông qua phương thức Create trong lớp Tuple:
// Khởi tạo Tuple thông qua phương thức Create
var MyTuple = Tuple.Create<int, string>(1, "HowKteam");
Ví dụ trên có nghĩa là tạo ra 1 đối tượng (Tuple) có 2 thuộc tính bên trong:
Thuộc tính thứ nhất (Item1) có kiểu dữ liệu là int và khởi tạo giá trị là 1.
Thuộc tính thứ hai (Item2) có kiểu dữ liệu là string và khởi tạo giá trị là
HowKteam”.
Cách 2: Thông qua Constructor của các lớp Generic:
// Kh i t o Tuple thông qua constructor c a các l p generic
var MyTuple2 = new Tuple<int, string>(2, "Kteam");
Ví dụ này cũng được hiểu tương tự như trên.
Khi bạn khởi tạo Tuple có bao nhiêu kiểu dữ liệu thì sẽ có bấy nhiêu thuộc
tính Item tương ứng. Hình dưới đây là minh hoạ cho Tuple<T1, T2>:
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 107
Ví dụ: in ra giá trị các thuộc tính đã khởi tạo ở trên:
// Lấy giá trị bên trong Tuple
Console.WriteLine(" ID: {0}, Name: {1}",MyTuple.Item1, MyTuple.Item2);
Kết quả: khi chạy chương trình là:
Lúc này đã hiểu Tuple cũng chỉ là một lớp bình thường đã được định nghĩa sẵn. Vì vậy
để giải quyết bài toán trả về nhiều giá trị cùng một lúc, đơn giản chỉ cần khai báo kiểu trả
về là 1 Tuple.
Ví dụ về tuple
Xét ví dụ sau: Viết chương trình trả về 3 giá trị là ngày, tháng, năm hiện tại của hệ
thống.
Đầu tiên mình sẽ viết phương thức trả về 3 giá trị ngày, tháng, năm:
Trong chương trình chính ta gọi và sử dụng như sau:
Có thể thử không gọi Item1, Item2, Item3 mà sử dụng phương thức ToString() để xem
kết quả thế nào nhé!
Lưu ý: Các thuộc tính Item1, Item2, Item3,… là các thuộc tính chỉ đọc nghĩa là chỉ có
thể gọi ra để lấy giá trị chứ không thể gán giá trị cho chúng được.
ICollection
ICollection là một interface trong bộ các interface được định nghĩa sẵn của .NET
Framework.
Trước khi vào nội dung chính chúng ta cùng tìm hiểu tại sao .NET lại định nghĩa sẵn
nhiều interface đến như vậy.
/// <summary>
/// Phương thức trả về 1 Tuple có 3 thuộc tính (cả 3 đều có kiểu dữ liệu là int)
/// </summary>
/// <returns></returns>
static Tuple<int, int, int> GetCurrentDayMonthYear()
{
DateTime now = DateTime.Now; // lấy ngày giờ hiện tại của hệ thống.
/* Sử dụng Constructor của Tuple<> để trả về hoặc có thể sử dụng phương thức Create đã trình
bày ở trên. */
return new Tuple<int, int, int>(now.Day, now.Month, now.Year);
}
/*
* Mình dùng var để C# tự nhận diện kiểu dữ liệu.
* Bạn có thể khai báo tường minh kiểu dữ liệu là Tuple<int, int, int>
*/
var now = GetCurrentDayMonthYear();
Console.WriteLine(" Day: {0}, Month: {1}, Year: {2}", now.Item1,
now.Item2, now.Item3);
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 108
Đưa ra những chuẩn mực chung, mỗi interface thể hiện cho một hoặc một vài tính chất
nào đó.
Khi bạn muốn cấu trúc dữ liệu của mình có tính chất nào đó (ví dụ như bạn muốn cấu
trúc dữ liệu mình là 1 dạng collection, hay cấu trúc dữ liệu của mình có khả năng duyệt
bằng từ khoá foreach,…) thì bạn chỉ cần thực thi đúng interface đã được cung cấp sẵn là
được.
Ngoài ra, việc định nghĩa sẵn các interface như là một cách để lập trình viên có
thể tuỳ chỉnh một số chức năng bên trong .NET.
Phân tích 1 chút. Ban đầu hàm Sort trong lớp ArrayList chỉ thực hiện sắp xếp
tăng dần (theo số hoặc theo bảng chữ cái) và để ý kỹ bạn sẽ thấy điều này:
Hàm Sort có thể nhận vào 1 đối tượng có thực thi interface IComparer. Đây chính là
chìa khoá để chúng ta có thể tuỳ chỉnh code.
Để thực hiện sắp xếp cho dù là thuật toán nào thì người ta cũng cần so sánh 2 đối tượng
xem đối tượng nào bé hơn thì đứng trước. Và việc so sánh này được viết bên trong 1 hàm
tên là Compare.
Trong hàm Sort mặc định thì hàm Compare chỉ so sánh được số hoặc chữ. Vậy nếu như
ta có thể tuỳ chỉnh hàm Compare này và đưa ra cách so sánh riêng của mình thì danh sách
sẽ được sắp xếp theo ý của mình. Và cách tuỳ chỉnh hàm Compare chính là tạo một đối
tượng có thực thi interface IComaparer.
Mô phỏng lại nội dung bên trong hàm Sort để hình dung được tại sao truyền vào 1 đối
tượng thực thi interface IComparer thì có thể thay đổi được cách sắp xếp:
Để ý dòng if thôi là đủ. Rõ ràng họ không quan tâm hàm Compare viết gì trong đó họ
chỉ cần có hàm đó để họ gọi là được. Và để đảm bảo đối tượng của mình có
hàm Compare thì cái này do interface IComparer ràng buộc.
public void Sort(IComparer comparer)
{
/* Mình sử dụng thuật toán sắp xếp đơn giản nhất nhé.
* Còn thực sự hàm Sort của .NET sử dụng thuật toán khác nha. */
int count = array.Count;
for (int i = 0; i < count - 1; i++)
{
for (int j = i + 1; j < count; j++)
{
/* Nếu phần tử i bé hơn phần tử j thì thực hiện hoán đổi vị trí 2 phần tử này */
if (comparer.Compare(array[i], array[j]) > 0)
{
object tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 109
ICollection là 1 interface có ý nghĩa “xác định kích thước, enumerator (bộ liệt kê) và
những phương thức đồng bộ cho những tập hợp không phải kiểu generic” hay nói
ngắn gọn đây là 1 interface thể hiện tính chất của 1 collection.
Interface ICollection yêu cầu chúng ta thực thi những Property, phương thức sau:
Count: trả về số lượng phần tử của tập hợp.
IsSynchronized SyncRoot: 2 property để làm cho thao tác đa luồng với tập hợp an
toàn hơn.
CopyTo(Array array, int index): phương thức thực hiện copy tập hợp ra 1 mảng, bắt
đầu từ vị trí index trong tập hợp.
GetEnumerator(): Trả về 1 đối tượng kiểu IEnumerator.
Thực thi interface ICollection
Chúng ta sẽ cùng tổ chức 1 Collection đơn giản giống ArrayList bằng cách thực thi các
interface cần thiết. Ví dụ này sẽ được thực hiện xuyên suốt cho các bài sau về interface.
public class MyArrayList : ICollection
{
private object[] lstObj; // mảng giá trị
private int count; // số lượng phần tử
private const int MAXCOUNT = 100; // số lượng phần tử tối đa
public MyArrayList()
{ count = -1;
lstObj = new object[MAXCOUNT];
}
public MyArrayList(int count)
{ this.count = count;
lstObj = new object[count]; }
public MyArrayList(Array array)
{ array.CopyTo(lstObj, 0);
count = array.Length; }
public void CopyTo(Array array, int index)
{
// thực hiện copy các phần tử trong lstObj từ vị trí index đến cuối sang mảng array.
lstObj.CopyTo(array, index);
}
public int Count
{
get { return count; }
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 110
To mt lp MyArrayList thực thi interface ICollection. Trong lp MyArrayList có 1
mng các object 1 biến lưu trữ slưng phần t của mng. Sau đó thực hin viết li
những property hoặc phương thức mà chúng ta biết bao gồm property Count, phương
thức CopyTo() một s constructor cơ bản. Vì đây ca phải là 1 collection hoàn thin
nên không thể chạy cơng tnh xem thử đưc.
Delegate
Delegate là mt biến kiu tham chiếu (references) chứa tam chiếu ti một phương thức.
Tham chiếu của Delegate có thể thay đổi runtime (khi cơng trình thực thi).
Delegate thường được dùng để triển khai các phương thức hoặc sự kiện call-back.
Cứ hiểu Delegate là một biến bình thường, biến này chứa hàm cần gọi. Sau này lôi ra
sài như hàm bình thường. Giá trị của biến Delegate lúc này là tham chiếu đến hàm. Có thể
thay đổi runtime khi chương trình đang chạy.
Delegate được dẫn xuất từ lớp System.Delegate trong C#
Khai báo Delegate trong C#
Khai báo Delegate trong C# sẽ tương tự như khai báo một biến. Nhưng cần thêm t
khóa Delegate để xác định đây là một Delegate. Đồng thời vì Delegate là để tham chiếu
đến một hàm, nên cũng cần khai báo kèm kiểu dữ liệu trả về của và tham số đầu
vào của Delegate tương ứng với hàm tham chiếu.
Công thức:
delegate <ki u tr v > < tên delegate> (<danh sách tha số nếu có>);
Ví dụ:
delegate int MyDelegate(string s);
Lưu ý: Chữ delegate viết thường
Lúc này chúng ta đã tạo một Delegate có tên là MyDelegate. MyDelegate có kiểu trả về
là int, một tham số đầu vào là string.
MyDelegate lúc này có thể dùng làm kiểu dữ liệu cho mọi Delegate tới hàm tương ứng
kiểu trả về và tham số đầu vào.
Khởi tạo và sử dụng Delegate trong C#
Khi kiểu Delegate được khai báo, đối tượng Delegate phải được tạo với từ khóa new và
được tham chiếu đến một phương thức cụ thể. Phương thức này phải cùng kiểu trả về
và tham số đầu vào với Delegate đã tạo.
Khi tạo một Delegate, tham số được truyền với biểu thức new được viết tương tự như
một lời gọi phương thức, nhưng không có tham số tới phương thức đó. Tức là chỉ
truyền tên hàm vào thôi. Delegate sẽ tự nhận định hàm được đưa vào có cùng kiểu dữ liệu
trả ra và cùng tham số đầu vào hay không.
Ví dụ:
class Program
{
delegate int MyDelegate(string s);
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
MyDelegate convertToInt = new MyDelegate(ConvertStringToInt);
string numberSTR = "35";
int valueConverted = convertToInt(numberSTR);
Console.WriteLine("Giá tr đã convert thành int: "+ valueConverted);
Console.ReadLine();
}
static int ConvertStringToInt(string stringValue)
{
int valueInt = 0;
Int32.TryParse(stringValue, out valueInt);
Console.WriteLine("Đã ép ki u d li u thành công");
return valueInt;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 111
Để hiểu rõ hơn về đoạn code trên:
Ở đây tạo một hàm ConvertStringToInt làm nhiệm vụ là chuyển kiểu dữ liệu của
một số từ string sang int.
Sử dụng Delegate bằng cách tạo một biến convertToInt có kiểu dữ liệu
là MyDelegate. convertToInt này mình new MyDelegate với tham số đầu vào là tên
hàm ConvertStringToInt (lưu ý chỉ tên hàm thôi).
Có biến numberSTR kiểu string khởi tạo giá trị là 35.
Tạo một biến valueConverted kiểu int khởi tạo nó bằng kết quả
gọi Delegate convertToInt với tham số truyền vào Delegate là biến numberSTR.
Kết quả xuất ra màn hình Console là số 35.
Nhận thấy Delegate convertToInt sử dụng tương tự như một hàm bình thường.
Do MyDelegate đã khởi tạo đồng nhất kiểu dữ liệu trả về và tham số đầu vào với
hàm ConvertStringToInt nên convertToInt mới thỏa mãn điều kiện khởi tạo và sử dụng của
hàm ConvertStringToInt này.
Vì sao cần Delegate? Khi bạn cần dùng một hàm như một biến ví dụ như tham số
truyền vào của một hàm, hàm call-back, event…
Multicast(đa hướng) một Delegate trong C#
Khi bạn cần thực hiện một chuỗi hàm với cùng kiểu trả về và cùng tham số đầu
vào mà không muốn gọi nhiều hàm tuần tự (chỉ gọi 1 hàm 1 lần duy nhất). Lúc này bạn
sẽ cần dùng đến Multicast Delegate.
Bản chất có thể làm một chuỗi Delegate cùng kiểu Delegate bằng cách dùng toán tử +.
Lúc này khi gọi Delegate sẽ thực hiện tuần từ các Delegate được cộng vào với nhau.
Có thể loại bỏ Delegate trong multicast bằng toán tử -.
Ví dụ:
class Program
{
delegate int MyDelegate(string s);
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
MyDelegate convertToInt = new MyDelegate(ConvertStringToInt);
MyDelegate showString = new MyDelegate(ShowString);
MyDelegate multicast = convertToInt + showString;
string numberSTR = "35";
int valueConverted = convertToInt(numberSTR);
Console.WriteLine("Giá tr đã convert thành int: " + valueConverted);
Console.WriteLine("K t qu khi g i multicast Delegate");ế
multicast(numberSTR);
Console.ReadLine();
}
static int ConvertStringToInt(string stringValue)
{
int valueInt = 0;
Int32.TryParse(stringValue, out valueInt);
Console.WriteLine("Đã ép ki u d li u thành công");
return valueInt;
}
static int ShowString(string stringValue)
{
Console.WriteLine(stringValue);
return 0;
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 112
Dùng lại ví dụ của phần trước.
Tạo thêm hàm ShowString với mục dích là xuất ra màn hình Console chuỗi truyền vào.
Mình tạo thêm 2 Delegate là showString tham chiếu tới hàm ShowString và multicast là
kết quả cộng của 2 Delegate convertToInt và showString .
Gọi Delegate multicast để thực hiện 1 lần 2 delegate tuần tự và convertToInt và
showString.
Console.WriteLine("K t qu khi g i multicast Delegate");ế
multicast(numberSTR);
Khi cần loại bỏ Delegate trong multicast bạn chỉ việc trừ Delegate ra
multicast = multicast - showString;
Dùng Delegate cho call-back function
Như đã nói ở trên, Delegate cũng là một biến. Vậy nên, có thể truyền Delegate vào hàm
làm parameter như biến bình thường. Lúc này Delegate này sẽ được gọi là call-back
function. Mục đích của việc này là hàm nhận call-back function là param có thể
gọi Delegate được đưa vào khi nào cần như ví dụ sau:
Như đã thấy, việc đã sử dụng Delegate làm call-back function thành công.
Ý nghĩa: mỗi khi người dùng nhập vào tên của mình thì sẽ gọi delegate Showstring để
hiền thị tên người dùng vừa nhập vào ra màn hình console. Vậy lúc này
hàm ShowString này hoàn toàn có thể được định nghĩa do người dùng mà không cần can
thiệp vào code của hàm NhapVaShowTen.
Event
Event Delegate với mục đích để cho lớp khác hoặc đối tượng cha của đối tượng
hiện tại ủy thác(định nghĩa) hàm vào trong đó.
Mục đích chính của chuyện này là để thông báo lên cho đối tượng cha biết mà xử lý.
Khai báo Event trong C#
Khai báo Event trong C# sẽ tương tự như khai báo một biến. Nhưng biến này sẽ nhận
kiểu dữ liệu là Delegate đã được tạo trước đó. Cần có từ khóa event để chương trình biết
đây là một biến event.
Công thức:
event <Ki u delegate> <tên event> ;
delegate int MyDelegate(string s);
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
MyDelegate showString = new MyDelegate(ShowString);
NhapVaShowTen(showString);
Console.ReadLine();
}
static void NhapVaShowTen(MyDelegate showTen)
{
Console.WriteLine("M i nh p tên c a b n:");
string ten = Console.ReadLine();
showTen(ten);
}
static int ShowString(string stringValue)
{
Console.WriteLine(stringValue);
return 0;
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 113
Ví dụ:
event UpdateNameHandler NameChanged;
Ví dụ code đầy đủ:
Lưu ý: Chữ event viết thường
Mục đích là mình mong muốn mỗi khi Name của class HocSinh thay đổi mình sẽ biết
và có thể code xử lý tương ứng.
Ta tạo một class HocSinh có filed là _Name kiểu dữ liệu là string. Được đóng gói thành
property Name.(Bạn để ý rằng mình cố ý tạo property và filed cùng tên nhưng khác nhau là
filed có dấu _ phía trước tên. Như vậy vừa dễ quản lý vừa tiện cho việc code)
Lúc này chúng ta đã tạo một Delegate có tên là UpdateNameHandler cùng cấp
với class Program và class HocSinh . UpdateNameHandler có kiểu trả về là void, một tham
số đầu vào là string. Ta cần tạo Delegate ở phạm vi này là vì muốn có thể được dùng cả
trong class HocSinh và class Program.
Ta tạo event NameChanged thuộc class HocSinh lúc này có kiểu dữ liệu là
Delegate UpdateNameHandler . Lưu ý cách đặt tên event thể hiện được tính chất là Name
Changed(đã thay đổi). Lưu ý: event phải public.
Khởi tạo và sử dụng Delegate trong C#
Event phải được ủy thác từ đối tượng cha của đối tượng chứa event . Bằng
cách += hàm Delegate tương ứng vào event của đối tượng(Tương tự có thể loại bỏ bằng
cách -=).
Ví dụ:
HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged;
Hàm Hs_NameChanged lúc này được tự tạo ra bằng cách gõ cú pháp sau và nhấn phím
tab một lần:
hs.NameChanged +=
Hàm Hs_NameChanged lúc này được tự tạo ra bên dưới hàm Main với kiểu trả về và
tham số đầu vào tương ứng với Delegate UpdateNameHandler.
Hoặc bạn cũng có thể dùng anonymous method trong tình huống này:
namespace Event_Voi_Delegate
{
public delegate void UpdateNameHandler(string name);
class Program
{
static void Main(string[] args)
{
}
}
public class HocSinh
{
public event UpdateNameHandler NameChanged;
private string _Name;
public string Name
{
get => _Name;
set
{
_Name = value;
}
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 114
hs.NameChanged += (name) =>
{
Console.WriteLine("Tên m i: " + name);
};
Trong phương thức set của property Name. Mình sẽ gọi event NameChanged. Nhưng
nếu như NameChanged chưa từng được ủy thác thì khi gọi sẽ bị null exception. Nên mình
sẽ kiểm tra NameChanged có null hay không? Nếu không null thì sẽ gọi event như hàm và
truyền param tương ứng vào là Name.
Code hoàn chỉnh:
Mỗi khi set lại giá trị cho hs.Name thì hàm Hs_NameChanged đã được gọi ngay sau đó.
namespace Event_Voi_Delegate
{
public delegate void UpdateNameHandler(string name);
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged;
hs.Name = "Kteam";
Console.WriteLine("Tên t class: " + hs.Name);
hs.Name = "HowKteam.com";
Console.WriteLine("Tên t class: " + hs.Name);
Console.ReadLine();
}
private static void Hs_NameChanged(string name)
{
Console.WriteLine("Tên m i: " + name);
}
}
public class HocSinh
{
public event UpdateNameHandler NameChanged;
private string _Name;
public string Name
{
get => _Name;
set
{
_Name = value;
if(NameChanged != null)
{
NameChanged(Name);
}
}
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 115
vent chuẩn .Net
Event chuẩn .Netevent với Delegate nhưng thỏa mãn các điều kiện:
Delegate có kiểu trả về là void
Delegate có hai tham số, tham số thứ nhất có kiểu dữ liệu là object, tham số thứ hai có
kiểu EventArgs. object chính là đối tượng phát sinh sự kiện, EventArgs chính
là class giữ thông tin mà đối tượng gửi kèm trong quá trình phát sinh sự kiện.
Lúc này thay vì chúng ta dùng Delegate do chúng ta tự tạo thì .Net có sẵn Delegate tên
là EventHandler theo chuẩn ở trên.
Vậy các event có sẵn trong .Net như Console.CancelKeyPress hay nhiều event khác nữa
Các event này thường được đánh ký hiệu là tia sét:
Cách dùng Event chuẩn .Net trong C#
Với mong muốn tạo ra một class HocSinh để quản lý Tên của học sinh. Vì muốn biết
khi nào tên của học sinh thay đổi sẽ ghi lại thành log sau này đối chiếu lịch sử thay đổi này.
Chúng ta sẽ tạo một Event NameChanged với Delegate là EventHandler có sẵn của .Net và
ủy thác việc ghi log lại.
Mình cũng đóng gói event mình sẽ tạo. Nhưng với event thay vì get set thì sẽ
là add và remove. Đồng thời tạo hàm OnNameChanged để thông báo event đã có sự thay
đổi.
Lưu ý: sử dụng _NameChanged chứ không phải nameChanged.
filed_NameChanged chính là event thực sự. Còn event NameChanged chính là event nhận
là ủy thác add và remove mà thôi.
Chúng ta truyền mặc định this vào tham số đầu tiên để thể hiện class HocSinh gọi event
này, tạo một đối tượng EventArgs vào tham số thứ hai để thỏa mãn đầy đủ tham số cho
Delegate EventHandler. Sau này, bạn có thể dùng các tham số này ở phần sau.
Mỗi khi có sự thay đổi tên của học sinh thì event NameChanged được thông báo lên.
Vậy là chúng ta có thể biết để ghi ra màn hình cho biết tên học sinh đã thay đổi.
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 116
Dùng Event chuẩn .Net với tham số truyền vào
Chúng ta đã biết khi nào tên của học sinh thay đổi và thông báo. Nhưng không thể biết
học sinh đó đã đổi tên thành gì. Vậy để có thể biết được giá trị sau khi cập nhật là
gì? Chúng ta sẽ dùng tới tham số thứ hai đó là EventArgs.
Vì tham số thứ hai chỉ cần có kiểu dữ liệu là EventArgs nên chúng ta sẽ tạo một class
mới là NameChangedEventArgs kết thừa lại EventArgs.
Class này sẽ có Constructor với tham số đầu vào là tên mới của học sinh. Và có thuộc
tính là Name public ra.
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged;
hs.Name = "Tên l n 1";
hs.Name = "Tên l n 2";
hs.Name = "Tên cu i";
Console.ReadLine();
}
private static void Hs_NameChanged(object sender, EventArgs e)
{
Console.WriteLine("Tên có thay đ i.");
}
}
public class HocSinh
{
private string _Name;
public string Name
{
get => _Name;
set
{
_Name = value;
OnNameChanged();
}
}
private event EventHandler _NameChanged;
public event EventHandler NameChanged
{
add
{
_NameChanged += value;
}
remove
{
_NameChanged -= value;
}
}
void OnNameChanged()
{
if(_NameChanged != null)
{
_NameChanged(this, new EventArgs());
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 117
Ngay tại vị trí khai báo event NameChanged và _NameChanged. Chúng ta sẽ
thêm generic NameChangedEventArgs vào sau EventHandler để thông báo rằng mình sẽ
dùng kiểu dữ liệu NameChangedEventArgs thay cho EventArgs.
Khi thông báo event được thực hiện trong hàm OnNameChanged. Chúng ta sẽ
thay new EventArgs thành new NameChangedEventArgs đồng thời truyền tên mới của học
sinh vào. Hàm OnNameChanged cũng phải thêm tham số đầu vào kiểu dữ liệu là string để
có thể truyền tên mới của học sinh vào. Đồng thời cũng phải truyền value vào khi gọi
hàm OnNameChanged tại hàm set của Name.
Phía ủy thác event cũng phải thay đổi EventArgs thành NameChangedEventArgs có thể
lấy tên này ra dùng thông qua tham số thứ hai đang có tên là e. chúng ta sẽ thông báo ra
nên mới của học sinh là gì bằng cách cộng thêm chuỗi e.Name.
class Program
{
static void Main(string[] args)
{ Console.OutputEncoding = Encoding.Unicode;
HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged;
hs.Name = "Tên l n 1"; hs.Name = "Tên l n 2";
hs.Name = "Tên cu i"; Console.ReadLine();
}
private static void Hs_NameChanged(object sender, NameChangedEventArgs e)
{
Console.WriteLine("Tên có thay đ i: " + e.Name);
}
}
public class HocSinh
{
private string _Name;
public string Name
{
get => _Name;
set
{ _Name = value; OnNameChanged(value); }
}
private event EventHandler<NameChangedEventArgs> _NameChanged;
public event EventHandler<NameChangedEventArgs> NameChanged
{
add
{ _NameChanged += value; }
remove
{ _NameChanged -= value; }
}
void OnNameChanged(string name)
{
if(_NameChanged != null)
{ _NameChanged(this, new NameChangedEventArgs(name)); }
}
}
public class NameChangedEventArgs : EventArgs
{
public string Name { get; set; }
public NameChangedEventArgs(string name)
{
Name = name;
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 118
Multi threading là gì? Vì sao cần Multi threading
Multi threading thể hiểu là một kỹ thuật để cùng một lúc xử lý nhiều tác vụ. Bản
chất chương trình C# được tạo ra sẽ có hai luồng chạy chính. Luồng thứ nhất
là MainThread(luồng chính của chương trình mặc định là hàm Main) và UIThread(luồng
cập nhật giao diện).
Các code xử lý mà chúng ta code chính là nằm trên luồng MainThread. Và chương
trình sẽ thực hiện tuần tự từng dòng code từ trên đi xuống. Nên nếu chúng ta có 3 hàm
xử lý cần chạy cùng lúc là không thể. Vì chương trình phải đợi hàm trước đó xử lý
xong mới đến hàm kế tiếp.
Vậy để giải quyết việc cùng một lúc, có thể xử lý nhiều tác vụ, multi threading ra đời.
Trong C# chúng ta có thể dùng Thread để thực hiện đa luồng.(Cứ tưởng tượng rằng
chúng ta sẽ làm cho chương trình không chỉ còn 2 luồng chạy cơ bản song song mà có
thêm n luồng do chúng ta tạo ra và chạy song song nhau)
Cách dùng Thread trong C#
Class Thread nằm trong thư viện System.Threading của .Net. Để có thể sử
dụng Thread thì chúng ta sẽ using System.Threading.
Chúng ta tạo một hàm DemoThread với mục đích là giả lập lại một hàm xử lý mất thời
gian 5 giây. Ở đây mình dùng Thread.Sleep để mô phỏng lại việc sẽ tiêu tốn một giây cho
mỗi lần lặp xử lý với 5 lần lặp. Thread.Sleep có nhiệm vụ làm cho luồng hiện tại ngủ theo
thời gian được cài đặt.
Kết quả có thể thấy màn hình Console sẽ in ra giá trị của I sau mỗi 1 giây. Sau 5 giây
thì hoàn thành. Vậy nếu chúng ta gọi 3 hàm DemoThread này liên tục sẽ tiêu tốn 15s để
hoàn thành chương trình.
Bây giờ chúng ta sẽ cho mỗi hàm demo vào một luồng riêng biệt để thực hiện. Lúc này
tốc độ sẽ tăng lên đáng kể vì 3 luồng chạy song song nhau. Chúng ta dùng cách tạo và
gọi Thread và sử dụng tối ưu nhất để bạn dễ tiếp cận. Bạn có thể tìm hiểu thêm
về ThreadStart nếu muốn.
Vì muốn biết giá trị của i được in ra từ hàm nào nên mình truyền thêm tham số cho
hàm DemoThread và in kèm giá trị đó.
Thread chỉ bắt đầu chạy khi bạn gọi hàm Start.
Sau khi thực hiện thì 5s sau chương trình đã hoàn thành.
class Program
{
static void Main(string[] args)
{
DemoThread();
DemoThread();
DemoThread();
Console.ReadLine();
}
static void DemoThread()
{
// Thực hiện vòng lặp 5 lần. Mỗi lần tốn 1 giây
for (int i = 0; i < 5; i++)
{
// Làm gì đó tốn 1s. Dùng Thread.Sleep để luồng hiện tại ngủ theo thời gian được cài đặt.
// Mục đích để giả lập độ trễ của code xử lý
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine(i);
}
}
}
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 119
Hiện tại bạn thấy kết quả in ra vẫn theo một trật tự vì mình dùng Thread.Sleep bên trong
hàm DemoThread. Bây giờ mình sẽ thay đổi hàm này theo hướng không sleep nữa và cho
tăng số lần lặp lên.
class Program
{
static void Main(string[] args)
{
/* Tạo một Thread t với anonymous function và gọi hàm DemoThread bên trong
* Thread chỉ bắt đầu chạy khi gọi hàm Start
* Bạn có thể thực hiện một hàm hay nhiều dòng code ở bên trong anonymous function này */
Thread t = new Thread(()=> {
DemoThread("Thread 1");
});
t.Start();
Thread t2 = new Thread(() => {
DemoThread("Thread 2");
});
t2.Start();
Thread t3 = new Thread(() => {
DemoThread("Thread 3");
});
t3.Start();
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 120
Bạn nhận thấy rằng thứ tự thực hiện của 3 luồng này lộn xộn và giá trị i in ra cũng vậy.
Lý do là vì 3 luồng này đang chạy song song độc lập nhau.
Cách dùng Multi Threading trong C#
Vậy là bạn đã có thể tắc tốc độ xử lý chương trình của mình bằng Thread. Nhưng mình
còn có thể tăng tốc độ xử lý lên nhiều nữa bằng kỹ thuật Multi Threading.
Vậy bây giờ mình muốn thực hiện gọi hàm DemoThread với 5 lần thì mình bắt buộc
phải dùng vòng lặp thay vì tạo tay 5 biến Thread để Start.
class Program
{
static void Main(string[] args)
{
/* Tạo một Thread t với anonymous function và gọi hàm DemoThread bên trong
* Thread chỉ bắt đầu chạy khi gọi hàm Start
* Bạn có thể thực hiện một hàm hay nhiều dòng code ở bên trong anonymous function này */
Thread t = new Thread(()=> {
DemoThread("Thread 1");
});
t.Start();
Thread t2 = new Thread(() => {
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 121
Ý tưởng đơn giản là tạo một vòng lặp 5 lần. Mỗi lần tạo ra một Thread mới và đưa biến
i vào hàm DemoThread.
Bạn có thể nận thấy. Thiếu đâu Thread 1 và Thread 3.
Lý do là vì Thread không Start ngay khi bạn gọi hàm Start. Mà hệ điều hành sẽ tự điều
phối sao cho hợp lý để tối ưu tài nguyên. Trong code bạn đưa vào là biến i. Có
thể DemoThread được Start ở lần lặp sau đó, suy ra biến i lúc này có giá trị là 2. Nên
thành ra Thread 2 được thực hiện tới 2 lần.
Để giải quyết tình trạng Missed Thread này. Bạn có thể dùng foreach hoặc tạo một biến
tạm để lưu giá trị i bên ngoài Thread rồi dùng biến đó thay cho i.
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
int tempI = i;
Thread t = new Thread(() => {
DemoThread("Thread " + tempI);
});
t.Start();
}
Console.ReadLine();
}
static void DemoThread(string threadIndex)
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine(threadIndex + " - " + i);
}
}
}
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(() => {
DemoThread("Thread " + i);
});
t.Start();
}
Console.ReadLine();
}
static void DemoThread(string threadIndex)
{
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 122
T R Ư N G Đ I H C K T H U T C Ô N G N G H C N T H Ơ | 123
Lưu ý khi dùng Thread trong C#
Nếu Thread của bạn xử lý nhiều hoặc lặp vô tận. Bạn nên set thuộc
tính IsBackground cho Thread là true để khi chương trình của bạn tắt, Thread của bạn cũng
sẽ được giải phóng. Còn nếu IsBackground = false. Thì chương trình của bạn phải
đợi Thread này chạy xong thì mới có thể kết thúc.
t.IsBackground = true;
Nếu bạn code MultiThreading ở các ngôn ngữ khác như Winform hay WPF thì nên đưa
code xử lý liên quan đến giao diện vào trong Invoke để tránh đụng độ tài nguyên khi sử
dụng.(cấu trúc Invoke có thể khác một chút tùy vào ngôn ngữ bạn dùng)
Invoke trong Winform:
Invoke trong WPF:
// Invoke cần một đối tượng giao diện để Invoke. Ở đây mình dùng this là form hiện tại.
// Bạn có thể dùng control bất kỳ thay thế
this.Invoke(new Action(()=> {
// code của bạn
}));
// Invoke cần một đối tượng giao diện để Invoke.
// Ở đây mình dùng this là Window hiện tại.
// Bạn có thể dùng control bất kỳ thay thế
// WPF cần Dispathcer để Invoke
this.Dispatcher.Invoke(new Action(()=> {
// code của bạn
}));
| 1/123

Preview text:

TRƯỜNG ĐẠI HỌC KỸ THUẬT – CÔNG NGHỆ CẦN THƠ
TÀI LIỆU LẬP TRÌNH C#
KHOA CÔNG NGHỆ THÔNG TIN
CHUYÊN NGÀNH HỆ THỐNG THÔNG TIN CẦN THƠ 2021
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 2
TIN HỌC ĐẠI CƯƠNG
I. Cấu trúc cơ bản của một chương trình 1. Using Cú pháp: using
Dùng để chỉ những thư viện được dùng trong chương trình.
Thư viện là một tập các phương thức, kiểu dữ liệu nào đó được tạo ra nhằm hỗ trợ cho
việc lập trình được nhanh chóng và hiệu quả hơn.
Ví dụ: khai báo một số thư viện using System;
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; 2. Namespace Cú pháp: Namespace { //các thành ph n
ầ bên trong namespace bao g m ồ các l p ớ , enum, delegate ho c ặ //các namespace con }
Dùng để báo cho trình biên dịch biết các thành phần bên trong khối {} ngay bên
dưới namespace thuộc vào chính namespace đó. 3. Class Cú pháp: class
Dùng để báo cho trình biên dịch biết là những thành phần trong khối {} ngay
sau tên lớp thuộc vào chính lớp đó. Ví dụ: class Program {
static void Main(string[] args) { } }
 Phương thức Main bên trong khối {} của lớp Program nên phương thức này thuộc lớp Program.
4. Hàm (phương thức) Main
Main là hàm được tạo sẵn khi tạo project với cấu trúc:
static void Main(string[] args) { }
Main là hàm chính của chương trình. Mỗi khi trình biên dịch dịch chương trình
ra sẽ đi vào hàm main đầu tiên để bắt đầu vong đời của chương trình. Code sẽ được
viết bên trong khối {} của hàm main.
5. Comment (chú thích)
Có 3 cách để comment code trong Visual Studio:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 3 -
Sử dụng ký tự //: bất kì ký tự nào phía sau // đều không được dịch. Sau dòng //,
dòng tiếp theo sẽ không còn comment nữa.
VD: //comment cho một dòng code… -
Sử dụng ký tự /**/: bất kì đoạn code hay chữ nào nằm trong khối /**/ đều tính là
comment, kể cả xuống dòng.
VD: /* đoạn này không được dịch
xuống dòng như vậy cũng không dịch*/ -
Sử dụng ký tự ///: khi gõ ký tự này trên namespace, class, method thì visual studio
sẽ tự sinh ra một đoạn comment như sau: /// /// B n ạ có th ể ghi b t ấ kỳ trong này/// /// - Phím tắt comment nhanh Crtl + k+c: đóng comment Crtl + k+u: mở comment
II. Nhập xuất cơ bản trong C# Console Application
1. Cấu trúc cơ bản của các lệnh nhập xuất a. C onsole.Write();
 In giá trị ra màn hình console nhưng không đưa con trỏ xuống dòng. Giá trị này có
thể là một ký tự, một chuỗi, một giá trị có thể chuyển về kiểu chuỗi. VD:
static void Main(string[] args) {
Console.Write(“xin chào!”); // in ra dòng ch ữ “xin chào!” } b. C onsole.WriteLine();
 Tương tự như Console.Write(). Nhưng khi in kết quả ra màn hình, nó sẽ tự động đưa con trỏ xuống dòng.
Ngoài ra, còn có nhiều cách để xuống dòng như: 
Sử dụng ký tự “\n” trong chuỗi in ra màn hình thì trình biên dịch sẽ tự đổi nó
thành ký tự xuống dòng.
Console.WriteLine(“hé lô”) = Console.Write(“hé lô \n”) 
Sử dụng lệnh xuống dòng Enviroment.NewLine. VD
Console.Write(Enviroment.NewLine);
(cách này khá dài nên rất ít sử dụng. Chủ yếu là Console.WriteLine hoặc “\n”).
Cộng dồn chuỗi in ra màn hình {
int a = 5; // khai báo biến kiểu nguyên có tên là a và khởi tạo giá trị là 5.
Console.Write("a = "); // In ra màn hình giá trị "a = ".
Console.Write(a); // In ra giá trị của a là 5 // K t ế qu ả màn hình là: a = 5 }
Có thể viết gọn lại là: Console.Write(“a= ”+5); // vẫn in ra màn hình a=5
In ra giá trị của biến
Cộng dồn là một cách in ra giá trị của biến. Ngoài ra, cũng có thể chỉ định vị trí in
ra giá trị của biến trong chuỗi bằng cú pháp {}. Ví dụ:
int a = 5; // khai báo biến kiểu nguyên có tên là a và khởi tạo giá trị là 5.
Console.Write("a = {0}", a); // In ra màn hình giá trị "a = 5". Cú pháp:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 4
Console.Write(“{0}{1}{2}{…}”,ị 0>,ị 1>,ị 2>, ị n>);
Trong đó: sẽ được điền tương ứng vào vị trí số 0 tương tự cho các vị
trí còn lại. Với hai cách trên, ta có thể thao tác biến hóa làm cho code trở nên gọn gàng, trực quan hơn. c. C onsole.Read();
Đọc một ký tự từ bàn phím và trả về kiểu số nguyên là mã ASCII của ký tự đó.
Chú ý: lệnh này không đọc được các phím chức năng như ctrl, shift, Alt, tab,… Ví dụ:
static void Main(string[] args) {
Console.WriteLine(Console.Read());
// đọc 1 ký tự từ bàn phím bằng lệnh Console.Read() sau đó in ra ký tự vừa đọc Console.ReadKey();
//lệnh này có mục đích dừng màn hình để xem kết quả. }
Khi nhập a thì màn hình sẽ in ra số 97 (mã ASCII của ký tự a). d. C onsole.ReadLine();
Đọc dữ liệu từ bàn phím cho đến khi gặp ký tự xuống dòng thì dừng (nói cách khác
đọc cho đến khi nhấn enter thì dừng). Giá trị đọc được luôn là một chuỗi. e. C
onsole.ReadKey(< t ham số kiểu bool > );
- Lệnh này dùng để đọc một ký tự từ bàn phím nhưng trả về kiểu ConsoleKeyInfo (là
một kiểu dữ liệu có cấu trúc được định nghĩa sẵn để chứa những ký tự của bàn
phím bao gồm các phím chức năng).
- Tham số kiểu bool bao gồm 2 giá trị: true hoặc false. Nếu truyền vào true thì phím
được ấn sẽ không hiển thị lên màn hình console mà được đọc ngầm. Ngược lại thì
phím được ấn sẽ hiển tị lên màn hình console. Nếu không truyền vào tham số sẽ
được mặc định là false. III. Biến trong C#
1. Biến là gì? Tại sao phải dùng biến -
Biến là một giá trị dữ liệu có thể thay đổi được. Là tên gọi tham chiếu đến vùng
nhớ nào đó trong bộ nhớ. Là thành phần cốt lõi của ngôn ngữ lập trình. -
Lưu trữ dữ liệu và tái sử dụng. Thao tác với bộ nhớ một cách dễ dàng: 
Cấu trúc của bộ nhớ bao gồm nhiều ô nhớ liên tiếp nhau, mỗi ô nhớ có một địa
chỉ riêng (địa chỉ ô nhớ thường là mã hex – thập lục phân). 
Khi muốn sử dụng ô nhớ nào (cấp phát, hủy, lấy giá trị, …) thì phải thông qua
địa chỉ của chúng. Điều này làm cho việc lập trình khó khăn hơn. 
Thay vào đó, có thể khai báo một biến và cho nó tham chiếu đến ô nhớ cần
quản lý. Khi sử dụng sẽ dùng tên biến đã đặt chứ không cần dùng địa chỉ của ô nhớ đó. 2. Khai báo biến Cú pháp:
<kiểu dữ liệu> <tên biến>; - Trong đó:
có thể là kiểu dữ liệu cơ bản hay kiểu dữ liệu cấu trúc,…
là tên do người dùng đặt. Phải tuân thủ quy tắc đặt tên. Khai báo:
Kiểu dữ liệu là: int, string int BienKieuSoNguyen; Tên biến là: string BienKieuChuoi;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 5 BienKieuSoNguyen BienKieuChuoi
Để sử dụng biến ta cần phải gán giá trị cho biến. Có 2 cách: 
Khởi tạo giá trị lúc khai báo: int BienKieuSoNguyen = 10;
string BienKieuChuoi = “hello”; 
Gán giá trị theo cú pháp: = ; BienKieuSoNguyen = 9; BienKieuKyTu = “H”;
Khi bạn muốn gọi một biến ra để lấy giá trị thì chỉ cần gọi tên là được. VD: int a = 1, b = 2 ; int c = a + b; // Bi n ế a và bi n ế b đư c ợ g i ọ đ ể l y ấ giá tr ị sau đó c n ộ g //chúng l i ạ r i ồ gán cho bi n ế c.
3. Quy tắc đặt tên biến
Một số quy tắc đặt tên biến cũng như các định danh khác: 
Tên biến là một chuỗi ký tự liên kết (không có khoản trắng) và không chứa ký tự đặc biệt 
Tên biến không được đặt bằng tiếng Việt có dấu. 
Tên không được bắt đầu bằng số. 
Tên biến không được trùng nhau. 
Tên biến không được trùng với từ khóa
DANH SÁCH CÁC TỪ KHÓA TRONG C#
** Ngoài ra còn có một số quy tắc khác:
- Quy tắc Lạc Đà: viết thường từ đầu tiên và viết hoa chữ cái đầu tiên của mỗi
từ. Ví dụ: tenBien, kyTu,…
- Quy tắc Pascal: viết hoa chữ cái đầu tiên của mỗi từ. Ví dụ: TenBien, KyTu, …
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 6 Lưu ý:
- Tên biến phải ngắn gọn dễ hiểu, thể hiện rõ mục đích của biến (Name, Tuoi, GioiTinh,…).
- Không đặt tên biến bằng một ký tự như a, b, c,… như vậy khi người khác
đọc sẽ gây không hiểu biến này dùng để làm gì.
- C# có phân biệt chữ hoa chữ thường. VD biến a <> biến A …
IV.Kiểu dữ liệu trong C# 1. Định nghĩa
- Kiểu dữ liệu là tập hợp các nhóm dữ liệu có cùng đặc tính, cách lưu trữ và thao
tác xử lý trên từng dững liệu đó.
- Là 1 tính hiệu để trình biên dịch biết kích thước của một biến và khả năng của nó.
- Là thành phần cốt lõi của ngôn ngữ lập trình
 Phải có kiểu dữ liệu để nhận biết kích thước và khả năng của một biến. Nhằm mục
đích phân loại dữ liệu. Nếu không có kiểu dữ liệu sẽ rất khó xử lý vì không biết
biến này kiểu chuỗi hay kiểu số nguyên hay số thực, … 2. Phân loại
a. Kiểu dữ liệu giá trị (value):
- Một biến khi khai báo kiểu dữ liệu giá trị thì vùng nhớ của biến đó sẽ chứa giá
trị của kiểu dữ liệu được lưu trữ trong bộ nhớ Stack.
- Một số kiểu dữ liệu thuộc kiểu giá trị: bool, byte, char, decimal, double, fload, int, long, short, struct, …
b. Kiểu dữ liệu tham chiếu:
- Một biến khi khai báo kiểu dữ liệu tham chiếu thì vùng nhớ của biến đó chỉ chứa
địa chỉ của đối tượng dữ liệu và lưu trong bộ nhớ Stack. Đối tượng dữ liệu thực
sự được lưu trong bộ nhớ Heap
- Một số kiểu dữ liệu thuộc kiểu tham chiếu: object, dynamic, string và tất cả kiểu
dữ liệu do người dùng định nghĩa.
Stack và Heap đều là bộ nhớ trên RAM nhưng cách tổ chức và quản lý dữ
liệu cũng như sử dụng thì khác nhau: Stack:
Vùng nhớ được cấp phát khi chương trình biên dịch. 
Được sử dụng cho việc thực thi thread, khi gọi hàm, các biến cục bộ kiểu
giá trị và tự động giải phóng khi không còn sử dụng nữa. 
Kích thước vùng nhớ của Stack là cố định và chúng ta không thể thay đổi. 
Khi vùng hwos này không còn đủ dùng thì sẽ gây ra hiện tượng tràn bộ
nhớ (stack overlow). Hiện tượng này xảy ra khi nhiều hàm lồng vào nhau
hoặc gọi đệ quy nhiều lần dẫn tới không đủ vùng nhớ. Heap:
Vùng nhớ được cấp phát khi chạy chương trình. 
Vùng nhớ Heap được dùng cho cấp phát bộ nhớ động (cấp phát thông qua toán tử new). 
Bình thường vùng nhwos Heap do người dùng tự giải phóng nhwung trong
C# điều này được hỗ trợ mạnh mẽ bởi bộ tự động thu gom rác (Garbage
Collection). Vì thế, việc thực hiện vùng nhớ sẽ được giải phóng tự động. 
Kích thước vùng nhớ Heap có thể thay đổi được. Khi không đủ vùng nhớ
để cấp phát thì hệ điều hành sẽ tự động tăng kích thước vùng nhớ Heap lên.
Ý nghĩa của một số kiểu dữ liệu cơ bản Nhóm Kiểu dữ Kích thước Ý nghĩa liệu
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 7 Kiểu số Byte 1
Số nguyên dương không dấu từ 0 – 255 nguyên Sbyte 1
Số nguyên có dấu từ -128 - 127 Short 2
Số nguyên từ -32,768 – 32,767 ushort 2
Số nguyên không dấu từ 0 – 65,535 Int 4
Số nguyên từ -2,147,483,647 – 2,147,483,647 Uint 4
Số nguyên không dấu có giá trị từ 0 đến 4,294,967,295 Long 8
Số nguyên có giá trị từ -9,223,370,036,854,775,808 đến 9,223,370,036,854,775,807 ulong 8
Số nguyên có giá trị từ 0 đến 18,446,744,073,709,551,615 Kiểu ký tự Char 2 Chứa một ký tự Unicode Kiểu logic bool 1
Chứa 1 trong 2 giá trị logic true hoặc false Kiểu số Float 4
Kiểu số thực dấu chấm động có giá trị dao động thực
từ 3.4E – 38 đến 3.4E + 38, với 7 chữ số có nghĩa Double 8
Kiểu số thực dấu chấm động có giá trị dao động từ 1.7E –
308 đến 1.7E + 308, với 15, 16 chữ số có nghĩa decimal 8
Có độ chính xác đến 28 con số và giá trị thập phân, được
dùng trong tính toán tài chính
Khác với kiểu dữ liệu trên, string là kiểu dữ liệu tham chiếu dùng để lưu chuỗi ký tự.
Kiểu dữ liệu có miền giá trị lớn hơn sẽ chứa được kiểu dữ liệu có
miền giá trị nhỏ hơn để có thể gán giá trị qua biến kiểu dữ liệu lớn hơn. 
Giá trị kiểu char nằm trong dấu ‘ ’. 
Giá trị kiểu string nằm trong dấu “ ”. 
Giá trị của biến kiểu fload phải có chữ F hoặc f làm hậu tố. 
Giá trị của biến kiểu decimal phải có chữ M hoặc m làm hậu tố. 
Trừ kiểu string, tất cả các kiểu trên đều không được chứa giá trị null. VÍ DỤ:
static void Main(string[] args) { / / Ki u ể s ố n guyên byte BienByte = 10; short BienShort = 10; int BienInt = 10; long BienLong = 10; // Ki u ể s ố th c float BienFloat = 10.9f; // Giá tr ị c a ủ bi n ế ki u ể float ph i ả có h u ậ t ố f ho c ặ F. double BienDouble = 10.9; // Giá tr ị c a ủ bi n ế ki u ể double không c n ầ h u ậ t . ố decimal BienDecimal = 10.9m; // Giá tr ị c a ủ bi n ế ki u ể decimal ph i ả có h u ậ t ố m. // Ki u ể ký t ự và ki u ể chu i char BienChar = 'K'; // Giá tr ị c a ủ bi n ế ki u ể ký t ự n m ằ trong d u ấ '' (nháy đ n ơ ). string BienString = "Kteam"; // Giá tr ị c a ủ bi n ế ki u ể chu i ỗ n m ằ trong d u ấ "" (nháy kép). Console.ReadKey(); } V. Toán tử trong C# 1. Toán tử là gì
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 8
Toán tử là một công cụ có thể thao tác với dữ liệu. Một toán tử là một ký hiệu
dùng để đại diện cho một thao tác cụ thể được thực hiện trên dữ liệu.
Có 6 loại toán tử cơ bản: toán tử toán học, toán tử quan hệ, toán tử logic, toản tử
khởi tạo và gán, toán tử so sánh trên bit, toán tử khác. a. T oán tử toán học
Giả sử biến a =10 và biến b = 9 Toán tử Mô tả Ví dụ +
Thực hiện cộng hai toán hạng a + b = 19 -
Thực hiện trừ hai toán hạng a – b = 1 *
Thực hiện nhân hai toán hạng a * b = 90 /
Chia lấy phần nguyên 2 số nguyên. Ngược lại thì chia bình thường a / b = 1 % Chia lấy phần a % b = 1 ++
Tăng giá trị lên 1 đơn vị a++ = 11 --
Giảm giá trị xuống 1 đơn vị a-- = 9
Lưu ý: đối với toán tử ++ và – cần phân biệt a++ và ++a (a-- và --a): 
a++ là sử dụng giá trị của biến a để thực hiện biểu thức rồi mới thực hiện
tăng lên 1 đơn vị cho a (tương tự a--). 
++a là tăng giá trị biến a lên một đơn vị rồi mới sử dụng biến a để thực hiện biểu thức. (--a). Ví dụ:
static void Main(string[] args) { int i = 5, j = 5;
Console.WriteLine(i++);
//Sử dụng giá tr ị i đ ể in ra r i ồ m i ớ tăng i
Console.WriteLine(++j); //Tăng j lên r i ồ mới in giá tr ị j ra màn hình Console.ReadKey(); } 5
 Kết quả khi chạy chương trình là: 6 b. T oán tử quan hệ
Giả sử biến a có giá trị bằng 10 và biến b có giá trị bằng 9: Lưu ý: Toán tử Mô tả Ví dụ ==
So sánh 2 toán hạng có bằng nhau hay không. Nếu bằng thì trả a == b sẽ trả
về true nếu không bằng thì trả về false về false !=
So sánh 2 toán hạng có bằng nhau hay không. Nếu không bằng thì trả a != b sẽ trả
về true nếu bằng thì trả về false về true >
So sánh 2 toán hạng bên trái có lớn hơn toán hạng bên phải hay a > b sẽ trả
không. Nếu lớn hơn thì trả về true nếu không lớn hơn thì trả về false về true <
So sánh 2 toán hạng bên trái có nhỏ hơn toán hạng bên phải hay a < b sẽ trả
không. Nếu nhỏ hơn thì trả về true nếu không nhỏ hơn thì trả về false về false >=
So sánh 2 toán hạng bên trái có lớn hơn hoặc bằng toán hạng bên a >= b sẽ trả
phải hay không. Nếu lớn hơn hoặc bằng thì trả về true nếu nhỏ về true
hơn thì trả về false <=
So sánh 2 toán hạng có nhỏ hơn hoặc bằng hay không. Nếu nhỏ hơn a <= b sẽ trả
hoặc bằng thì trả về true nếu lớn hơn thì trả về false về false
1- Các toán tử quan hệ này chỉ áp dụng cho số hoặc ký tự.
2- Hai toán hạng hai bên phải cùng loại (cùng số hoặc cùng chữ).
3- Bản chất của việc so sánh hai ký tự khác nhau là so sánh mã ASCII của các ký tự đó.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 9
4- Không nên sử dụng các toán tử trên để so sánh các chuỗi với nhau vì bản
chất việc so sánh chuỗi là so sánh từng ký tự tương ứng với nhau và so sánh
mã ASCII của ký tự đó. Như vậy, ký tự ‘K’ != ‘k’. Để so sánh hai chuỗi
người ta thường dùng hàm so sánh chuỗi đã được hỗ trợ sẵn. c. T oán tử logic
Giả sử mệnh đề A đúng mệnh đề B sai: Toán tử Mô tả Ví dụ &&
Hay còn gọi là toán tử logic AND (và). Trả về true nếu tất cả toán A && B kết quả
hạng đều mang giá trị true. Và trả về false nếu có ít nhất 1 toán là false
hạng mang giá trị false. ||
Hay còn gọi là toán tử logic OR (hoặc). Trả về true nếu có ít nhất A || B kết quả
1 toán hạng mang giá trị true. Và trả về false nếu tất cả toán hạng là true.
đều mang giá trị false. !
Hay còn gọi là toán tử logic NOT (phủ định). Có chức năng đảo !A kết quả
ngược trạng thái logic của toán hạng. Nếu toán hạng đang mang giá là false
trị true thì kết quả sẽ là false và ngược lại. Lưu ý: o
Các toán tử &&| có thể áp dụng đồng thời nhiều toán hạng, ví dụ như:
A && B && C || D || K (Thứ tự thực hiện sẽ được trình bày ở phần sau). o
Các toán hạng trong biểu thức chứa toán tử logic phải trả về true hoặc false. d. T
oán tử khởi tạo và gán
Toán tử khởi tạo và gán thường được sử dụng nhằm mục đích lưu lại giá trị
cho một biến nào đó. Một số toán tử khởi tạo và gán hay được sử dụng: Toán tử Mô tả Ví dụ =
Gán giá trị của toán hạng bên phải cho toán hạng bên trái. K = 10 sẽ gán 10 cho biến K +=
Lấy toán hạng bên trái cộng toán hạng bên phải sau đó gán K += 1 tương đương
kết quả lại cho toán hạng bên trái. với K = K + 1 -=
Lấy toán hạng bên trái trừ toán hạng bên phải sau đó gán K -= 1 tương đương
kết quả lại cho toán hạng bên trái. với K = K – 1 *=
Lấy toán hạng bên trái nhân toán hạng bên phải sau đó gán K *= 1 tương đương
kết quả lại cho toán hạng bên trái. với K = K * 1 /=
Lấy toán hạng bên trái chia lấy phần nguyên với toán K /= 1 tương đương
hạng bên phải sau đó gán kết quả lại cho toán hạng bên trái. với K = K / 1 %=
Lấy toán hạng bên trái chia lấy dư với toán hạng bên K %= 1 tương đương
phải sau đó gán kết quả lại cho toán hạng bên trái. với K = K % 1
Một số lưu ý khi sử dụng các toán tử trên:
1- Toán tử bên trái thường là một biến, còn toán tử bên phải có thể là biến, có
thể là biểu thức đều được.
2- Một phép toán gán hoặc khởi tạo có thể được sử dụng như là toán hạng bên
phải cho một phép gán hoặc khởi tạo khác. Ví dụ: int H, K, T; H = K = T = 10;
Console.WriteLine(" H = {0}, K = {1}, T = {2}", H, K, T); H += K = T = 5;
Console.WriteLine(" H = {0}, K = {1}, T = {2}", H, K, T);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 10
Phép toán H =K =T = 10 sẽ thực hiện gán 10 cho biến T sau đó gán giá trị
biến T cho biến K sau đó gán giá trị biến K cho biến H. Như vậy ta được cả 3
biến H K T đều có giá trị bằng 10. 
Phép toán H += K = T = 5 sẽ thực hiện gán 5 cho biến T sau đó gán giá trị biến
T cho biến K sau đó lấy H + K gán kết quả cho biến H. Cuối cùng ta được H = 15, K = 5, T = 5. e. T
oán tử so sánh trên bit
Các toán tử so sánh trên bit cũng ít gặp nên mình chỉ giới thiệu qua cho các bạn
tham khảo thôi chứ chúng ta không giới thiệu rõ phần này.
Giả sử a có giá trị bằng 10 và b có giá trị bằng 9. Giá trị biến a đổi ra nhị phân là
1010 và giá trị biến b đổi ra nhị phân là 1001. Toán tử Mô tả Ví dụ &
Sao chép bit 1 tới kết quả nếu nó tồn tại trong cả hai toán
a&b sẽ cho kết quả là 1000
hạng tại vị trí tương ứng, ngược lại thì bit kết quả bằng 0
tương đương với số 8 trong hệ thập phân |
Sao chép bit 1 tới kết quả nếu nó tồn tại ở một trong
a|b sẽ cho kết quả 1011
hai toán hạng tại vị trí tương ứng, ngược lại thì bit kết quả tương đương với số 11 bằng 0 trong hệ thập phân ^
Sao chép bit 1 tới kết quả nếu nó chỉ tồn tại ở một toán
a^b sẽ cho kết quả 0011
hạng tại vị trí tương ứng, ngược lại thì bit kết quả bằng 0
tương đương với số 3 trong hệ thập phân ~
Dùng để đảo bit 0 thành 1 và ngược lại 1 thành 0
~a sẽ cho kết quả 0101 <<
Dịch trái n bit. Giá trị toán hạng bên trái sẽ được dịch
a<<2 sẽ cho kết quả
trái n bit với n được xác định bởi toán hạng bên phải 101000 >>
Dịch phải n bit. Giá trị toán hạng bên trái sẽ được dịch
a>>2 sẽ cho kết quả 0010
phải n bit với n được xác định bởi toán hạng bên phải
Ngoài những toán tử trên, vẫn còn nhiều toán tử khác cũng được hay sử dụng Toán tử Mô tả Ví dụ sizeof()
Trả về kích cỡ của một kiểu dữ liệu sizeof(int) sẽ trả về 4 typeof()
Trả về kiểu của một lớp typeof(string) sẽ trả về System.String new
Cấp phát vùng nhớ mới, áp dụng cho kiểu dữ liệu tham DateTime dt chiếu = new DateTime() is
Xác định đối tượng có phải là một kiểu cụ thể nào đó hay
không. Nếu đúng sẽ trả về true ngược lại trả về false as
Ép kiểu mà không gây ra lỗi. Nếu ép kiểu không thành công sẽ trả về null ? :
Được gọi là toán tử 3 ngôi. Tương đương với cấu trúc điều (1 < 2) ? 1 : 0 kiện Cú pháp:
kết quả là 1 vì toán hạng
(toán hạng 1) ? (toán hạng 2) : (toán hạng 3)
1 là (1 < 2) là đúng nên trả
Ý nghĩa: trả về toán hạng 2 nếu toán hạng 1 là true và về toán hạng 2 là 1
ngược lại trả về toán hạng 3 ,
Sử dụng toán tử “,” để kết nối nhiều biểu thức lại với
(t = 5, 2) sẽ duyệt qua biểu nhau.
thức 1 là t = 5, thực hiện
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 11 Cú pháp: gán 5 cho t sau đó duyệt
(biểu thức 1, biểu thức 2)
qua biểu thức 2 là 2, cuối
Ý nghĩa: Duyệt qua biểu thức 1 sau đó duyệt qua biểu
cùng trả về giá trị là 2
thức 2 và trả về giá trị của biểu thức 2
2. Ví dụ chương trình sử dụng toán tử
Ví dụ 1: các phép toán c ơ b n ả
static void Main(string[] args) { int a, b, c;
a = b = (c = 9) + 1;
// khởi tạo giá trị: a = 10, b = 10, c = 9
a += b; // tương đương a = a + b
b = c++; // thực hiện gán giá trị c cho biến b sau đó thực hiện c = c + 1
--c; // thực hiện c = c - 1
Console.WriteLine(" a = {0}, b = {1}, c = {2}", a, b, c); Console.ReadKey(); } Ví dụ 2: kết h p ợ các phép toán đ ể viết chư n ơ g trình ki m ể tra s ố nhập vào là s ố l . ẻ
static void Main(string[] args) {
string strSoNguyen;
// Biến chứa dữ liệu nhập vào từ bàn phím
int SoNguyen; // Biến chứa số nhập vào từ bàn phím
string KetQua; // Biến chứa kết quả kiểm tra số vừa nhập là chẵn hay lẻ
strSoNguyen = Console.ReadLine();
// Đọc dữ liệu nhập vào từ bàn phím (dữ liệu chuỗi) sau đó gán giá trị vào biến strSoNguyen
SoNguyen = Int32.Parse(strSoNguyen);
// Ép kiểu dữ liệu vừa nhập vào (dạng chuỗi) sang dạng số rồi gán giá trị vào biến SoNguyen
KetQua = (SoNguyen % 2 == 0) ? "so chan" : "so le";
// Sử dụng toán tử 3 ngôi để kiểm tra số chẵn lẻ
Console.WriteLine("{0} la {1}", SoNguyen, KetQua);
// In kết quả ra màn hình Console.ReadKey(); } Đầu tiên ta có 3 biến:
strSoNguyen: Chứa dữ liệu nhập vào từ bàn phím. Vì dữ liệu nhập vào từ bàn
phím mặc định là dạng chuỗi nên cần biến kiểu chuỗi để chứa giá trị.
SoNguyen: Chứa dữ liệu nhập vào từ bàn phím ở dạng số. Từ dữ liệu dạng chuỗi
của biến strSoNguyen ta ép kiểu sang kiểu số để dễ xử lý
KetQua: Chứa kết quả kiểm tra số vừa nhập là chẵn hay lẻ. Kết quả này ở dạng
chuỗi để có thể in ra màn hình luôn.
Tiếp theo ta nhận kết quả nhập từ bàn phím bằng lệnh Console.ReadLine() rồi gán
giá trị cho biến strSoNguyen.
Ép kiểu kết quả vừa nhập sang dạng số rồi gán giá trị vào biến SoNguyen.
Sử dụng toán tử 3 ngôi kiểm tra xem số vừa nhập có chia hết cho 2 hay
không (nếu chia hết cho 2 thì phép chia lấy dư với 2 sẽ cho kết quả là 0 và biểu
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 12
thức SoNguyen % 2 == 0 sẽ trả về true ngược lại sẽ trả về false). Nếu chia hết thì
trả về chuỗi “so chan” ngược lại trả về chuỗi “so le”.. VI. Hằng trong C#
1. Định nghĩa hằng
K/n: hằng là một biến những giá trị không thay đổi trong suốt chương trình. Hằng
bắt buộc phải khởi tạo hoặc khai báo.
Mục đích: Nhằm ngăn chặn việc gán giá trị khác vào biến. Hằng làm cho chương
trình dễ đọc hơn bằng cách biến những con số vô cảm thành những tên có nghĩa. Giúp
cho chương trình dễ nâng cấp, dễ sửa chữa, dễ tránh lỗi hơn. Nếu vô ý gán giá trị cho
một biến hằng ở đâu thì trình biên dịch sẽ báo lỗi ngay lập tức.
2. Có mấy loại hằng -- có 3 loại hằng a. Giá trị hằng
Ta có câu lệnh gán sau: x = 10;
 10 là giá trị hằng. Giá trị 10 luôn là 10 và không thể gán giá trị khác cho 10.
b. Biểu tượng hằng
Việc gán một tên cho giá trị hằng được xem là một biểu tượng hằng.
Xét lại câu lệnh: x = 10; thì x được xem là biểu tượng hằng Cú pháp: VD: const int a = 10; Const ể d ữ li u ệ > ế > = ị h n ằ g>; Lưu ý:
Phải có từ khóa ‘const’ trước khai báo và phải khởi tạo giá trị ngay khi khai báo c. Kiểu liệt kê
Kiểu liệt kê là tập hợp các tên hằng có giá trị không thay đổi (bài ENUM sẽ nói rõ).
3. Cách sử dụng hằng
Vì bản chất hằng cũng là một biến nhưng giá trị không thay đổi nên hằng được sử
dụng tương tự như biến.  M
ột số lưu ý khi sử dụng hằng:
Hằng bắt buộc phải được khởi tạo giá trị ngay khi khai báo và không được thay
đổi giá trị trong suốt chương trình. 
Giá trị của hằng được tính toán vào lúc biên dịch nên không thể gán trực tiếp giá
trị của biến vào hằng. Nếu muốn làm điều đó thì phải sử dụng từ khóa readonly
trước khai báo biến (readonly là từ khóa chỉ cho trình biên dịch biết rằng biến này
chỉ được đọc, lấy giá trị chứ không được gán giá trị) 
Hằng bao giờ cũng static nhưng ta không được đưa từ khóa này vào khai báo hằng. VII.Ép kiểu trong C#
1. Khái niệm ép kiểu
Ép kiểu là biến đổi dữ liệu thuộc kiểu dữ liệu này thành kiểu dữ liệu khác. Để
chuyển dũ liệu sang một kiểu dữ liệu mong muốn phục vụ cho việc thao tác và xử lý.
Đưa dữ liệu về định dạng mà mình mong muốn (ví dụ chuyển từ kiểu số nguyên sang kiểu chuỗi).
2. Có mấy loại ép kiểu? có 4 loại:
a. Chuyển đổi kiểu ngầm định (implicit)
Việc chuyển đổi này được thực hiện bởi trình biên dịch và chúng ta không cần tác động gì.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 13
Được thực hiện khi chuyển kiểu từ: 
Kiểu dữ liệu nhỏ sang kiểu dữ liệu lớn hơn 
Chuyển từ lớp con đến lớp cha. Ví dụ: int intValue = 10; long longValue = intValue;
// kiểu long có miền giá trị lớn hơn kiểu int nên có thể chuyển từ int sang long float floatValue = 10.9f;
double doubleValue = floatValue;
//kiểu double có miền gái trị lớn hơn kiểu float nên có thể chuyển từ float sang double
b. Chuyển đổi kiểu tương minh (explicit)
Là việc chuyển kiểu một cách ro ràng và dùng từ kháo chỉ định chứ không dùng phương thức.
Một số đặc điểm của chuyển đổi kiểu tường minh: 
Thường được dùng để chuyển đổi giữa các kiểu dữ liệu có tính chất tương tự nhau. 
Hỗ trợ trong việc boxing và unboxing đổi tượng. 
Cú pháp ngắn gọn do không sử dụng phương thức. 
Chỉ khi chúng ta biết rõ biến đó có thuộc kiểu dữ liệu nào mới thực
hiện chuyển đổi. Ngược lại, có thể lỗi khi chạy chương trình. Cú pháp: (ể d ữ li u ệ >) ế c n ầ ép kiểu> 
là kiểu dữ liệu mình muốn chuyển sang. 
là biến chứa dữ liệu cần ép kiểu. 
Phải có cặp dấu ngoặc tròn. Ý nghĩa:
Ép kiểu của về nếu thành công sẽ trả ra giá
trị kết quả. Ngược lại sẽ báo lỗi. Đặc biệt với số: 
Ta có thực hiện ép kiểu dữ liệu lớn hơn về kiểu dữ liệu nhỏ hơn mà không báo lỗi 
Nếu dữ liệu cần ép kiểu vượt quá miền giá trị của kiểu dữ liệu muốn
ép kiểu về chương trình sẽ tự cắt bit cho phù hợp với khả năng chứa
kiểu dữ liệu đó (cắt từ bên trái qua). Ví dụ:
int i = 300; // 300 có mã nhị phân là 100101100 byte b = (byte)i;
/* do kiểu byte có giới hạn đến giá trị 255 thôi nên không thể chứa số 300 được mà kiểu byte có
kích thước là 1 bytes tương đương 8 bit. Như vậy ta cần cắt mã nhị phân của số 300 về còn 8 bit là
được. Mã nhị phân 300 là 100101100 cắt từ trái qua 1 bit ta được 00101100 (đủ 8 bit) tương đương với
số 44. Cuối cùng biến b sẽ mang giá trị là 44.*/
Console.WriteLine(" i = " + i);
Console.WriteLine(" b = " + b);
VD: Nếu muốn thực hiện chia lấy phần nguyên củ 2 toán hạng đều là số
nguyên. Thì để được kết quả chính xác ta sẽ ép kiểu 1 trong 2 roán hạng đó về số thực. double d = 2 / 3;
// kết quả ra 0 vì 2 và 3 đều là số nguyên nên thực hiện 2 chia lấy phần nguyên với 3 được 0 double k = (double)2 / 3;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 14
// Ép kiểu số 2 từ kiểu nguyên sang kiểu số thực. Như vậy kết quả phép chia sẽ ra số thực double t = 1.0 * 2 / 3;
// Thực hiện nhân 1.0 với 2 mục đích để biến số 2 (số nguyên) thành 2.0 (số thực)
Console.WriteLine("d = {0} \n k = {1} \n t = {2}", d,k, t);
c. Sử dụng phương thức hỗ trợ sẵn
Phương thức Parse(), TryParse() Parse() Cú pháp:
là kiểu dữ liệu cơ bản mình muốn chuyển đổi sang.
ể dữ liệu>.Parse(ữ li u ệ c n ầ chuy n ể đ i ổ >); 
là dữ liệu thuộc kiểu chuỗi, có thể là biến kiểu chuỗi
hoặc giá trị hằng kiểu chuỗi. Ý nghĩa:
Chuyển đổi một chuỗi sang một kiểu dữ liệu cơ bản tương ứng. 
Phương thức trả về giá trị kết quả chuyển kiểu nếu chuyển kiểu thành công.
Ngược lại sẽ báo lỗi chương trình. Ví dụ: string stringValue = "10";
int intValue = int.Parse(stringValue);
// Chuyển chuỗi stringValue sang kiểu int và lưu giá trị vào biến intValue - Kết quả intValue = 10
double HowKteam = double.Parse("10.9");
// Chuyển chuỗi giá trị hằng "10.9" sang kiểu int và lưu giá trị vào biến HowKteam - Kết quả HowKteam = 10.9 TryParse() Cú pháp: .TryParse(, out );
 là kiểu dữ liệu cơ bản mình muốn chuyển đổi sang.
 là dữ liệu thuộc kiểu chuỗi, có thể là biến kiểu chuỗi
hoặc giá trị hằng kiểu chuỗi.
 là biến mà bạn muốn lưu giá trị kết quả sau khi chuyển kiểu
thành công. Từ khóa out là từ khóa bắt buộc phải có. Ý nghĩa:
 Chuyển một chuỗi sang một kiểu dữ liệu cơ bản tương ứng.
 Phương thức sẽ trả về true nếu chuyển kiểu thành công và giá trị kết quả chuyển
kiểu sẽ lưu vào . Ngược lại sẽ trả về false và quả> sẽ mang giá trị 0. Ví dụ: int Result;
// Biến chứa giá trị kết quả khi ép kiểu thành công bool isSuccess;
// Biến kiểm tra việc ép kiểu có thành công hay không
string Data1 = "10", Data2 = "Kteam";
// Dữ liệu cần ép kiểu
isSuccess = int.TryParse(Data1, out Result);
// Thử ép kiểu Data1 về int nếu thành công thì Result sẽ chứa giá trị kết quả ép kiểu và
isSuccess sẽ mang giá trị true. Ngược lại Result sẽ mang giá trị 0 và isSuccess mang giá trị false
Console.Write(isSuccess == true ? " Success !" : " Failed !");
// Sử dụng toán tử 3 ngôi để in ra màn hình việc ép kiểu đã thành công hay thất bại.
Console.WriteLine(" Result = " + Result);
// In giá trị Result ra màn hình
isSuccess = int.TryParse(Data2, out Result);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 15
// Tương tự như trên nhưng thao tác với Data2
Console.Write(isSuccess == true ? " Success !" : " Failed !"); // Tương tự như trên
Console.WriteLine(" Result = " + Result); // Tương tự như trên
Vì Data1 có thể ép kiểu về int nên kết quả thành công và in giá trị ra. Còn Data2
không thể ép kiểu về kiểu int nên kết quả không thành công và in ra giá trị 0.
Lưu ý khi sử dụng Parse() và TryParse():
 Tham số truyền vào phải là một chuỗi.
 Cả 2 phương thức được gọi thông qua tên kiểu dữ liệu.
 Parse() trả về giá trị kết quả ép kiểu nếu ép kiểu không thành công thì sẽ báo lỗi.
Còn TryParse() trả về xem ép kiểu có thành công hay không, giá trị kết quả ép kiểu sẽ nằm trong .
 Ngoài TryParse() ra thì vẫn có một cách ép kiểu không báo lỗi chương trình. Đó là sử dụng toán tử as:
- Toán tử as dùng để “Ép kiểu mà không gây ra lỗi. Nếu ép kiểu không
thành công sẽ trả về null”.
- Chỉ áp dụng cho việc chuyển kiểu giữa các kiểu tham chiếu tương thích
(thường là các kiểu có quan hệ kế thừa) hoặc các kiểu nullable (là các kiểu
có thể chứa giá trị null).
Lớp hỗ trợ sẵn (Convert)
Convert là lớp tiện ích được C# hỗ trợ sẵn cung cấp cho chúng ta nhiều phương
thức chuyển đổi kiểu dữ liệu.
Các đặc điểm của các phương thức trong lớp Convert: 
Tham số truyền vào của các phương thức không nhất thiết là chuỗi (có thể là int, bool, . . .). 
Nếu tham số truyền vào là null thì các phương thức sẽ trả về giá trị mặc
định của kiểu dữ liệu. 
Các trường hợp tham số truyền vào sai định dạng hoặc vượt quá giới hạn
thì chương trình sẽ báo lỗi như phương thức Parse().
Một số phương thức chuyển kiểu mà Convert có: 
Các phương thức chuyển kiểu trong lớp Convert đều có thể nhận các kiểu
dữ liệu cơ bản (int, bool, byte, . . .) làm tham số truyền vào. 
Ở bảng trên mình chỉ liệt kê những phương thức hay dùng. Để tìm hiểu
thêm các phương thức chuyển kiểu khác các bạn có thể sử dụng tính năng gợi ý của Visual Studio: 
Đầu tiên các bạn gõ “Convert.” (lưu ý dấu chấm liền sau Convert) 
Visual Studio sẽ hiển thị ra tất cả các phương thức có trong lớp Convert.
Giờ bạn chỉ việc lựa chọn phương thức muốn sử dụng là được.
d. Người dùng tự định nghĩa kiểu chuyển đổi
Khi chúng ta tạo ra một kiểu dữ liệu mới chúng ta cũng cần định nghĩa các
chuyển đổi kiểu dữ liệu từ kiểu cơ bản sang kiểu tự định nghĩa và ngược lại.
VIII. Cấu trúc rẽ nhánh If else trong C#
1. Cấu trúc if … else … dạng thiếu Cú pháp:
if ([biểu thức điều kiện]) - if là từ khóa bắt buộc -
là biểu thức dạng boolean (trả về true or false).
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 16 -
là câu lệnh muốn thực hiện nếu là đúng. Ý nghĩa:
Nếu trả về true thì thực hiện ngược lại thì không làm gì cả. Ví dụ: string K = “Kteam”; if (K == “Kteam”);
// biểu thức điều kiện sử dụng toán tử == để sánh xem giá trị biến K có bằng
Kteam hay không. Nếu bằng thì trả về true ngược lại trả về false
Console.WriteLine(“đúng”);
//
in ra man hình chữ “đúng” nếu biểu thức trên là đúng.
2. Cấu trúc if … else … dạng đủ Cú pháp: if ; else ; -
if else là từ khóa bắt buộc. -
<biểu thức điều kiện> là biểu thức dạng boolean. -
<câu lệnh thực hiện 1> là câu lệnh muốn thực hiện nếu điều kiện đúng -
là câu lệnh muốn thực hiện nếu điều kiện sai. Ý nghĩa:
Nếu trả về true thì thực hiện , ngược lại . Ví dụ: string K = "Kteam";
if (K == "Kteam") // Nếu giá trị K bằng “Kteam” thì
Console.WriteLine("Free Education");
// In ra màn hình “Free Education”
else // Ngược lại thì
Console.WriteLine("Connecting to HowKteam. . .");
// In ra màn hình “Connecting to HowKteam. . .”
3. Một số lưu ý khi sử dụng
có thể chứa nhiều biểu thức con bên trong và các biểu thức
con liên kết với nhau bằng các toán tử quan hệ nhưng tất cả phải trả về kiểu boolean.
Nếu muốn thực hiện nhiều câu lệnh thì có thể nhóm chúng vào trong cặp dấu { }. Ví dụ: if { ; }
Trong câu lệnh có thể chứa một câu lệnh điều kiện con nữa. Ví dụ: if ể th c ứ đi u ề ki n ệ 1> { if ể th c ứ đi u ề ki n ệ 2> { ệ h th c ự hi n ệ 1>;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 17 } elsse { ệ h th c ự hi n ệ 2>; } ệ h th c ự hi n ệ 3>; } else { if (bi u ể th c ứ đi u ề ki n ệ 3) { ệ h th c ự hi n ệ 4>; } if (bi u ể th c ứ đi u ề ki n ệ 4) { ệ h th c ự hi n ệ 5>; } ệ h th c ự hi n ệ 6>; }
Các biểu thức điều kiện được kiểm tra từ trên xuống dưới và không kiểm tra lại.
Nếu biểu thức điều kiện đang kiểm tra trả về true thì: -
Thực hiện khối lệnh trong đó. - Thoát khỏi cấu trúc -
Không kiểm tra các điều kiện còn lại.
So với toán tử 3 ngôi thì: -
Câu lệnh điều kiện nhìn trực quan hơn và có thẻ thực hiện nhiều câu lệnh hơn. -
Nhưng nếu chỉ thực hiện một câu lệnh điều kiện đơn giản thì dùng toán tử 3
ngôi sẽ làm code ngắn gọn và viết nhanh hơn. VD: Phương trình Ax+B=0
Console.Write(" Moi nhap so A: "); strA = Console.ReadLine();
Console.Write(" Moi nhap so B: "); strB = Console.ReadLine();
if (int.TryParse(strA, out A) == false ||
int.TryParse(strB, out B) == false)
// kiểm tra người dùng có thực sự nhập số nguyên vào hay không. Nếu ép kiểu
thành công sẽ trả về true, ngược lại trả về false {
Console.WriteLine (“Du lieu nhap sai !”);
return; // Lệnh này tạm hiểu là dừng và thoát chương trình mà không
thực //hiện những câu lệnh sau nó nữa. Sẽ được tìm hiểu chi tiết trong bài 16 Hàm } else {
Console.WriteLine("\n Phuong trinh cua ban vua nhap la: {0}x + {1} = 0", A, B); if (A == 0) {
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 18
Console.WriteLine("\n Phuong trinh co vo so nghiem !"); }
else if (B == 0) {
Console.WriteLine("\n Phuong trinh co nghiem x = 0"); } else {
Nghiem = (double)-B / A; // Ép kiểu để cho ra kết quả chính xác
Console.WriteLine("\n Phuong trinh co nghiem x = {0}", Nghiem); } }
IX. Cấu trúc rẽ nhánh Switch case trong C#
1. Cấu trúc switch case dạng thiếu Cú pháp: switch () { case :; break; … … case :; break; } Trong đó: 
Switch case là từ khóa bắt buộc.  Break là một lệnh nhảy -
Ý nghĩa của nó là thoát ra khỏi cấu trúc, vong lặp chứ nó. -
Ngoài break vẫn còn lệnh nhảy khác như goto nhưng ít khi được sử dụng. 
phải là biểu thức trả về kết quả kiểu:
- Số nguyên (int, long, byte, …)
- Ký tự hoặc chuỗi (char, string) - Kiểu liệt kê (enum) 
với i = 1..n là giá trị muốn so sánh với giá trị của thức>. 
với I = 1..n là giá trị muốn thực hiện khi
tương ứng với giá trị của biểu thức
Ý nghĩa: duyệt lần lượt từ tên xuống dưới và kiểm tra xem gá trị của có
bằng với đang xét hay không. Nếu bằng thì thức hiện tương ứng. Lưu ý:
phải có kiểu dữ liệu giống kiểu dữ liệu của giá trị của biểu thức. 
có thể gồm nhiều câu lệnh và không nhất thiết phải đặt trong
cặp dấu ngoặc nhọn { } nhưng tốt hơn nên đặt trong dấu { } để code được rõ ràng hơn. 
Nếu case đang xét không rỗng (có lệnh để thực hiện) thì bắt buộc phải có lệnh nhảy (break) sau đó. Ví dụ: int k = 8; switch (k) {
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 19 case 3:
Console.WriteLine("HowKteam");
break; //Vì case này có lệnh thực hiện nên phải có lệnh break
case 9: //case này rỗng (không có lệnh thực hiện) nên không cần lệnh break case 10:
Console.WriteLine("Free Education"); break; }
Lưu đồ sau sẽ minh họa cách thức hoạt động của cấu trúc switch case dạng thiếu: 
Chú ý là trường hợp không có lệnh break đồng nghĩa case đó rỗng. 
Đối với case cuối cùng dù có câu lệnh để thục hiện hay không vẫn phải có
lệnh break để thoát khỏi cấu trúc. Ví dụ: int k = 10;
switch (k) // giá trị biểu thức là giá trị của biến k (kiểu số nguyên) {
case 3: // các giá trị so sánh cũng là kiểu số nguyên
Console.WriteLine("HowKteam"); // lệnh thực hiện nếu k = 3
break; // lệnh thoát ra khỏi cấu trúc case 9:
Console.WriteLine("Kteam"); // tương tự break; case 10:
Console.WriteLine("Free Education"); // tương tự break; }
2. Cấu trúc switch case dạng đủ switch (ứ >) { case : ệ h th ứ 1>; break; case : ệ h th ứ 2>; break; . . . case ị th ứ n>: ệ h th ứ n>; break; default: ệ h m c ặ đ n ị h>; break; }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 20 Cú pháp: Trong đó: 
switch, case, default là từ khóa bắt buộc. 
phải là biểu thức trả về kết quả kiểu: -
Số nguyên (int, long, byte, . . .) -
Ký tự hoặc chuỗi (char, string). Kiểu liệt kê (enum) 
với i = 1..n là giá trị muốn so sánh với giá trị của . 
với i = 1..n là câu lệnh muốn thực hiện khi tương
ứng bằng với giá trị của . 
là câu lệnh sẽ được thực hiện nếu giá trị không bằng với nào.
Ý nghĩa: duyệt lần lượt từ trên xuống dưới và kiểm tra xem giá trị của thức> có bằng với đang xét không. Nếu bằng thì thực hiện thứ i> tương ứng. Nếu không bằng các thì sẽ thực hiện định>.
Về cơ bản cách thức hoạt động của 2 cấu trúc switch. . . case dạng đủ và dạng
thiếu là như nhau, chỉ khác nhau ở một diểm là dạng đủ có thêm dòng default. . . xem
lại lưu ý của dạng thiếu để tránh mắc lỗi. Ví dụ: int k = 8; switch (k) { case 3:
Console.WriteLine("HowKteam"); break; case 9: Console.WriteLine("Kteam"); break; case 10:
Console.WriteLine("Free Education"); break;
default: // Nếu không thỏa các trường hợp trên sẽ thực hiện lệnh sau đây
Console.WriteLine("Connecting to HowKteam. . ."); break; }
 Kết quả: Connecting to HowKteam. . .
Vì không tìm thấy case nào có giá trị bằng với giá trị biến k nên sẽ thực hiện câu
lệnh trong default. Do đó màn hình in ra “Connecting to HowKteam…”.
Lưu đồ minh họa cách hoạt động của cấu trúc switch … case … default… Vào cấu trúc Biểu thức Bằng Không có lệnh break Bằng Không có lệnh break
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ. . .
T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 21
X. Kiểu dữ liệu Object tr Không c ong C# ó lệnh break 1.
Khái niệm về kiểu dữ liệu object Kiểu dữ liệu object Là một B kiểằng < u dữ lgi iệá trị u c n>
ơ bản của tất cả các kiểu dữ liệu trong .NET.
Mọi kiểu dữ liệu đều được kế thừa từ System.Object.
Thuộc kiểu dữ liệu tham chiếu.
Kiểu dữ liệu object cung cấp một số phương thức ảo cho phép overload để sử dụng. Không có lệnh break
Một số phương thức củ object: Phương thức Ý nghĩa ToString()
Trả về kiểu chuỗi của < Câ đối u l tư ệ ợ nh m ng (c ặc
huyển từ kiểu dữ liệu khác về chuỗi) Thoát
GetHashCode( Trả về mã băm của đối tượđịnh> ng. cấu trúc ) Equals()
So sánh 2 đối tượng và trả về true khi 2 đối tượng có giá trị bằng nhau,
ngược lại trả về false. GetType()
Trả về kiểu dữ liệu của đối tượng 2. Boxing và unboxing
Boxing là quá trình chuyển dữ liệu từ kiểu dữ liệu giá trị về kiểu dữ liệu tham chiếu.  Quá trình boxing:
- Khởi tạo một đối tượng trong vùng nhớ heap.
- Coppy giá trị của biến có kiểu dữ liệu vào đối tượng này. 
Quá trình boxing được thực hiện ngầm định. 
Mỗi kiểu dữ liệu đều kế thừa từ lớp System.Object nên có thể gán giá trị của
kiểu dẫn xuất cho kiểu dữ liệu cha. Trong ví dụ là biến kiểu int gán giá trị cho biến kiểu object Ví dụ:
// khởi tạo một biến Value kiểu int (kiểu dữ liệu giá trị) int Value = 109;
/* thực hiện boxing bằng cách:
Khởi tạo đối tượng ObjectValue kiểu object
Gán giá trị của biến Value vào ObjectValue */ object ObjectValue = Value;
Unboxing làquá trình ngược lại với boxing, tức là đưa dữu liệu từ kiểu tham chiếu
về kiểu dữ liệu giá trị.
Unboxing được thực hiện tường minh và thông qua cách ép kiểu tường minh.
Phải chắc chắn rằng đối tượng cần boxing thuộc đúng kiểu dữ liệu đưa ra. Nếu
không việc unboxing sẽ báo lỗi chương trình. Quá trình unboxing:
Kiểm tra xem đối tượng cần unboxing thuộc đúng kiểu dữ liệu đưa ra hay không.
Nếu đúng thì thực hiện coppy giá trị của đối tượng sang biến dữ liệu kiểu giá trị.
Ngược lại thì thông báo lỗi. Ví dụ: int Value = 109; // boxing object ObjectValue = Value;
/* thực hiện unboxing bằng cách:
Khởi tạo đối tượng ObjectValue thấy thuộc đúng kiểu int.
Gán giá trị của ObjectValue vào biến NewValue bằng cách ép kiểu tường minh.
Biến NewValue sẽ mang giá trị là 109 */
int NewValue = (int)ObjectValue; stack Heap 0xcc c 109 boxing 0xfff 0xfff 109 109 Unboxing
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 22 Từ khóa var int Value = 109;
Var là từ khóa hỗ trợ khai báo biến mà không cần kiểu dữ liệu, kiểu dữ liệu sẽ được
xác định khi gán giá trị cho biến, lúc đó chuowg trình sẽ tự ép kiểu cho biến. Object ObjectValue = Value;
Những lưu ý khi sử dụng var: -
Bắt buộc phải gán giá trị ngay khi khởi tạo biến và không thể khởi tạo giá trị null cho biến int
var .NewValue = (int)ObjectValue; -
Var chỉ là từ khóa dùng để khai báo biến không phải là một kiểu dữ liệu.
// Lỗi vì chưa khởi tạo giá trị cho biến varInt. var varInt;
// Lỗi vì không được khởi tạo giá trị null cho biến varString. var varString = null;
// Lỗi vì phải khởi tạo giá trị ngay khi khai báo var varLong; varLong = 109; // Khai báo đúng! var varBool = true;
Khai báo biến bằng từ khóa var thường được dùng trong: - Duyệt mảng bằng foreach. - Truy vấn LinQ.
/* Vì biến StringVariable được khởi tạo giá trị kiểu chuỗi
* nên trình biên dịch sẽ hiểu biến này như là biến kiểu string.*/ var varString = "HowKteam";
// Khai báo tường minh biến kiểu string string Content = "HowKteam";
// In giá trị của biến StringVariable và biến Content Console.WriteLine(varString); Console.WriteLine(Content);
Kết quả khi chạy chương trình
XI. Từ khóa Dynamic trong C# 1. Từ khóa dynamic
Từ khóa dynamic là từ khóa dùng để khai báo kiểu dynamic. Kiểu dynamic là
một khái niệm mới được đưa vào C# 4.0. Cú pháp: Dynamic ế >;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 23 Trong đó: - Dynamic là từ khóa. -
là tên do người dùng đặt. Đặc điểm:
Các đối tượng thuộc kiểu dynamic sẽ không xác định được kiểu cho đến khi
chương trình thực thi. Tức là trình biên dịch sẽ bỏ qua tất cả lỗi về cú pháp, việc
kiểm tra này sẽ thực hiện khi chương trình thực thi.
// Khai báo biến StringValue kiểu dynamic và khởi tạo giá trị là một chuỗi kiểu string
dynamic StringValue = "HowKteam";
/* Chúng ta biết rằng kiểu chuỗi không hỗ trợ toán tử ++
* Nhưng câu lệnh StringValue++ vẫn không báo lỗi là do ở thời điểm hiện tại trình biên dịch vẫn
chưa xác định kiểu dữ liệu cho biến StringValue
* Khi chạy chương trình thì lúc này C# mới phát hiện biến StringValue là kiểu string và không thể
thực hiện toán tử ++ lúc đó sẽ xuất hiện lỗi*/ StringValue++;
Khi chạy chương trình sẽ nhận được các lỗi sau:
- Hỗ trợ dynamic programming (lập trình động) cho ngôn ngữ lập trình sử
dụng kiểu dữ liệu tĩnh như C#.
- Giúp cải thiện khả năng tương thích với các ngôn ngữ lập trình và nền tảng (frameworks) động.
- Giúp việc code đơn giản và nhanh hơn.
- Có thể ép kiểu qua lại với các kiểu dữ liệu khác một cách bình thường.
Ví dụ chương trình sử dụng dynamic:
// Khai báo 2 biến Name và Mission kiểu string và khởi tạo giá trị. string Name = "HowKteam ";
string Mission = "Free Education";
/* Thực hiện gán 1 biến kiểu string cho biến kiểu dynamic bằng cách ép kiểu ngầm định (implicit)
* Sau phép gán này DynamicValue chứa "Free Education" nhưng kiểu dữ liệu của nó chưa được xác định.*/ dynamic DynamicName = Name;
// Thực hiện cộng chuỗi và in ra màn hình bình thường
Console.WriteLine("Mission of " + DynamicName + " is " + Mission);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 24 Kết quả:
2. Phân biệt object, var, dynamic Về khái niệm thì: 
Object là kiểu dữ liệu cơ bản của tất cả kiểu dữ liệu trong C#. 
Var là một từ khóa để khai báo một cách ngầm định kiểu dữ liệu và kiểu
anonymous (kiểu anonymous sẽ được trình bày ở những bài sau). 
Dynamic là một từ khóa để khai báo kiểu dynamic. Kiểu dynamic cũng có thể
tương tác với mọi kiểu dữ liệu nhưng khác object, biến kiểu dynamic chỉ được
xác định kiểu dữ liệu khi chương trình thực thi.
Chúng ta cùng phân biệt object, var và dynamic qua bảng tổng hợp sau Đặc điểm Object Var Dynamic Là một kiểu dữ liệu Phải
Về bản chất thì var và dynamic đều là
từ khóa không phải kiểu dữ liệu Phải khởi tạo giá trị Không bắt buộc Bắt buộc Không bắt buộc khi khai báo Sử dụng để làm kiểu Có Không Có trả về hoặc tham số cho hàm Có khả năng ép kiểu Có Không Có qua lại với các kiểu dữ liệu khác Thời điểm xác định
Là một kiểu dữ liệu Xác định ngay tại Xác định khi kiểu dữ liệu thực sự
nên không cần xác khai báo thông qua chương trình định gì nữa giá trị được gán vào thực thi
XII.Vòng lập For trong C# 1. Cú pháp for ([Kh i ở t o ạ ]; [Đi u ề ki n ệ l p ặ ]; [Bư c ớ l p ặ lại]) { // Kh i ố l n ệ h đư c ợ l p ặ l i ạ . Có th ể b ỏ tr n ố g } Trong đó:
- Các phần [khởi tạo]; [điều kiện lăp]; [bước lặp lại] hoàn toàn có thể trống.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 25
- Mỗi đoạn [khởi tạo]; hay [điều kiện lặp]; [bước lặp lại] là một câu lệnh riêng. Tiến trình:
 Ban đầu trình biên dịch sẽ di vào phần khởi tạo chạy đoạn lệnh khởi tạo.
 Tiếp theo kiểm tra điều kiện lặp. Rồi thực hiện khối code bên trong vòng lặp for.
Khi đến ký hiệu } thì sẽ quay lên bước lặp lại.
 Sau đó lại kiểm tra điều kiện lặp rồi tiếp tục thực hiện đoạn code trong khối lệnh.
Đến khi điều kiện lặp không còn thõa mãn thì sẽ kết thúc vòng lặp for.  Trường hợp khác: for (; ;) // l u ư ý d u ấ ; { // Kh i ố l n ệ h đư c ợ l p ặ l i ạ . Có th ể b ỏ tr n ố g } Trong đó:
o Vòng lặp for này trở thành vòng lặp vô tận.
o Lưu ý dấu ; vẫn phải có. 2. Khởi tạo
Khi bắt đầu vào đoạn code của vòng lặp for, đoạn lệnh này sẽ được chạy đầu tiên.
Và chỉ được gọi duy nhất một lần trong vòng đời của vòng lặp for. Ví dụ:
static void Main(string[] args) {
// i sẽ được khởi tạo lần đầu tiên tại vòng lặp for
// khi vòng đời của vòng lặp for kết thúc bộ nhớ của biến i sẽ được giải phóng
// hay nói cách khác i là biến cục bộ của vòng lặp for for (int i = 0; ; ) { Console.WriteLine(i); } Console.ReadKey(); }
- Kết quả màn hình xuất ra một loạt giá trị 0 vì i = 0 được khởi tạo tại phần khởi tạo
của vòng lặp for và vòng lặp for này không có Điều kiện lặp nên chương trình sẽ chạy vô tận.
- Ở trường hợp này i được gọi là biến đếm (thuật ngữ lập trình dùng cho một biến
có tác dụng tăng giá trị lên mỗi lần lặp lại).
Ví dụ 2: Chúng ta không nhất thiết phải khai báo môt biến ngay tại vị trí khởi tạo.
Ta có thể chỉ gán giá trị hoặc không làm gì cả (bỏ trống).
static void Main(string[] args) { int i;
// i được gán giá trị bằng 0
// i lúc này không phải biến cục bộ của vòng lặp for // i thuộc hàm main for (i = 0; ; ) { Console.WriteLine(i); } Console.ReadKey(); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 26
Ví dụ 3: chỉ có thể có duy nhất 1 câu lệnh khởi tạo trong vòng lặp
for static void Main(string[] args) { int i;
// lỗi vì chỉ được phép có duy nhất một dòng lệnh khởi tạo trong vòng lặp for for (i = 0, int j = 0; ; ) { Console.WriteLine(i); } Console.ReadKey(); } Hay
static void Main(string[] args) { int i;
// lỗi vì chỉ được phép có duy nhất một dòng lệnh khởi tạo trong vòng lặp for for (i = 0; int j = 0; ; ) { Console.WriteLine(i); } Console.ReadKey(); } 3. Điều kiện lặp
Điều kiện lặp là một biểu thức logic với kết quả trả về bắt buộc là true hoặc false
(có thể bỏ trống sẽ trả về kết quả là true).
Điều kiện lặp là dòng lệnh thứ 2 vòng for sẽ chạy vào khi chạy lần đầu tiên (Khởi
tạo chạy trước). Từ lần lặp thứ 2 của vòng for, Điều kiện lặp cũng là dòng lệnh thứ 2
được chạy (sau bước lặp lại). (Cứ nhớ là luôn đứng thứ 2).
Khi câu điều kiện lặp không thỏa mãn (kết quả false) thì vòng lặp for kết thúc
Có thể thấy điều kiện lặp của vòng lập này luôn là true, nên vòng lặp sẽ lặp vô
static void Main(string[] args) { int i;
// vòng lặp for này vẫn lặp vô tận vì không bao giờ
//thỏa mãn điều kiên dừng // i luôn == 0
// Điều kiện lặp luôn là true for (i = 0; i < 10;) { Console.WriteLine(i); } Console.ReadKey(); } tận.
Để giải quyết vấn đề này và cho vòng lặp kết thúc khi thỏa mãn điều kiện lặp.
Chúng ta thêm một đoạn code i++; ngay dưới đoạn code Console.WriteLine(i).
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 27
Kết quả màn hình xuất ra các giá trị số nguyên từ 0 đến 9 (10 lần). Chứng tỏ vòng
lặp đã kết thúc sau 10 lần lặp (không còn lặp vô tận).
static void Main(string[] args) { int i; for (i = 0; i < 10;) { Console.WriteLine(i); i++; } Console.ReadKey(); } Lưu ý:
- Giá trị in ra từ 0 đến 9 chứ không phải đến 10. Vì Điều kiện lặp là i < 10 (10 ==
10 nên câu điều kiện là false và kết thúc vòng lặp. Vẫn thỏa mãn lặp 10 lần).
- Sau mỗi lần lặp giá trị i lại tăng lên 1 đơn vị. Sau 11 lần thì giá trị i == 10, không
còn thỏa mãn Điều kiện lặp nữa nên vòng lặp kết thúc.
- Các bạn có thể xem bảng thử dưới đây: Lần 1 2 3 4 5 6 7 8 9 10 11 i 0 1 2 3 4 5 6 7 8 9 10 i<1 TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRU FALSE E 0
Bạn hoàn toàn có thể để giá trị true hoặc false vào phần điều kiện lặp (bỏ trống
mặc định là true). Hoặc một biểu thức logic phức tạp nhưng kết quả cuối cùng trả về là true hoặc false.
static void Main(string[] args) { int i;
for (i = 0; (i % 3 == 0) && (i < 10);) { Console.WriteLine(i); i++; } Console.ReadKey(); } Hay
static void Main(string[] args)
static void Main(string[] args) { { int i; int i; for (i = 0; false;) for (i = 0; true;) { { Console.WriteLine(i); Console.WriteLine(i); i++; i++; } } Console.ReadKey(); Console.ReadKey(); } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 28 4. Bước lặp lại
Như ví dụ trên ta thấy. Mỗi lần muốn tăng giá trị của i ta phải dùng môt đoạn
lệnh i++ ; ở cuối khối lệnh. Vậy trường hợp bất cứ khi nào lặp lại ta cũng cần thực
thi đoạn lệnh i++ ; thì sao? Để tiện hơn cho việc code. Chúng ta có một phần tiếp
theo để tìm hiểu. Đó là bước lặp lại. Xét đoạn code sau:
Ta có thể viết gọn lại bằng cách đưa
i++; vào phần bước lặp lại của for
static void Main(string[] args) {
static void Main(string[] args) int i; { for (i = 0; i < 10;) int i; { for (i = 0; i < 10; i++) Console.WriteLine(i); { i++; Console.WriteLine(i) } } Console.ReadKey(); Console.ReadKey(); } }
Chúng ta có thể thực hiện nhiều bước lặp
static void Main(string[] args) 
Ta thấy đoạn i++j += 3 được {
cách nhau bởi dấu phẩy (,) int i; 
Với mỗi đoạn lệnh trong bước int j = 0;
lặp. Chúng đươc phân cách nhau bởi dấu phẩy (,).
for (i = 0; i < 10; i++, j += 3)
Lưu ý: Đoạn code trong bước lặp { còn có thể thêm Console.WriteLine(i);
cả Console.WriteLine("Tăng") vào }
(khuyến cáo không nên). Nhưng Console.ReadKey();
không thể thực hiện đoạn code có }
chứa từ khóa (như if, for …).
static void Main(string[] args)
static void Main(string[] args) { { int i; int i; int j = 0; int j = 0;
// lỗi đoạn , if (i % 2 == 0) j += 3
for (i = 0; i < 10; i++, if (i % 2 == 0) j += 3)
for (i = 0; i < 10; i++, j += 3, { Console.WriteLine("Tăng")) Console.WriteLine(i); { } Console.WriteLine(i); Console.ReadKey(); } } Console.ReadKey(); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 29
Không thể thêm câu điều kiện
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 30 XIII.
Vòng lập While trong C# Cú pháp: while (<Đi u ề ki n ệ l p ặ >) { // kh i ố l n ệ h l p ặ l i ạ } -
Điều kiện lặp là một biểu thức logic bắt buộc phải có với kết quả trả về bắt buộc là true hoặc false. -
Từ khóa while biểu thị đây là vòng lặp while. Các câu lệnh trong khối lệnh sẽ
được lặp lại đến khi không còn thỏa mãn điều kiện lặp sẽ kết thúc vòng lặp while. - Tiến trình:
Đầu tiên: trình biên dịch sẽ đi vào dòng while (<điều kiện lặp>). Kiểm tra
điều kiện lặp có thỏa mãn hay không. Nếu tất cả là true thì sẽ đi vào bên trong
thực hiện khối code. Quá trình chỉ kết thúc khi điều kiện lặp là false.
Điều kiện lặp luôn bằng true. Thì vòng lặp while sẽ lặp vô tận.
Điều kiện luôn bằng false thì vòng lặp không thực thi.
Các ví dụ sử dụng while: 1- In ra một ma trận số:
static void Main(string[] args) { int countLoop = 0; int countLoopTime = 0; int valueNum = 10; int loopTime = 5;
// Vẽ từ trên xuống LoopTime lần
while (countLoopTime < loopTime) {
// reset lại biến countLoop về 0 để viết lại từ đầu countLoop = 0;
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum) {
// in ra giá trị của countLoop trong 8 vị trí
Console.Write("{0,8}", countLoop);
// tăng giá trị của biến countLoop lên một đơn vị countLoop++; }
// Mỗi khi hoàn thành một vòng lặp nhỏ thì lại xuống dòng chuẩn vị vẽ lần tiếp theo Console.WriteLine();
// tăng giá trị countLoopTime lên một đơn vị countLoopTime++; } Console.ReadKey(); } Kết quả của chương trình:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 31
2- In ra một ma trận số có giá trị ngẫu nhiên:
static void Main(string[] args) { int countLoop = 0; int countLoopTime = 0; int valueNum = 10; int loopTime = 5; int minRandomValue = 0; int maxRandomValue = 100; Random rand = new Random();
// Vẽ từ trên xuống LoopTime lần
while (countLoopTime < loopTime) {
// reset lại biến countLoop về 0 để viết lại từ đầu countLoop = 0;
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum) {
// giá trị sinh ngẫu nhiên trong khoảng [minRandomValue . .. maxRandomValue - 1]
int showValue = rand.Next(minRandomValue, maxRandomValue);
// in ra giá trị của showValue trong 8 vị trí
Console.Write("{0,8}", showValue);
// tăng giá trị của biến countLoop lên một đơn vị countLoop++; }
// Mỗi khi hoàn thành một vòng lặp nhỏ thì lại xuống dòng chuẩn vị vẽ lần tiếp theo Console.WriteLine();
// tăng giá trị countLoopTime lên một đơn vị countLoopTime++; } Console.ReadKey(); }
Kết quả chương trình XIV.
Vòng lập Do While trong C# Cú pháp: do { // kh i ố l n ệ h l p ặ lai
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 32 -
Điều kiện lặp: là một biểu thức logic bắt buộc phải có với kết quả trả về bắt buộc là true hoặc false. -
Từ khóa do while biểu thị đây là một vòng lặp do while. Các câu lệnh trong khối
lệnh được lặp lại đến khi không còn thỏa mãn điều kiện sẽ kết thúc vòng lặp do while. - Tiến trình: o
Đầu tiên trình biên dịch sẽ đi vào dòng do và thực hiện khối lệnh bên trong.
Sau đó khi gặp ký tự } sẽ kiểm tra điều kiện lặp thỏa mãn hay không. Nếu
kết quả là true thì sẽ quay lại ký tự { thực hiện khói code. Quá trình chỉ kết
thúc khi điều kiện lặp là false. o
Điều kiện lwpj luôn bằng true thì vòng lặp while sẽ trở thành vòng lặp vô tận. o
Điều kiện lặp luôn bằng false thì vòng lặp không được thực thi. 
Lưu ý: vòng lặp do while sẽ thực hiện câu lệnh trong khối code xong rồi mới
kiểm tra điều kiện lặp. Cuối vòng lặp do while có dấu ; ở cuối.
static void Main(string[] args) { int countLoop = 0; int countLoopTime = 0; int valueNum = 10; int loopTime = 5;
// Vẽ từ trên xuống LoopTime lần do {
countLoop = 0; // reset lại biến countLoop về 0 để viết lại từ đầu
// vẽ từ trái qua valueNum lần
while (countLoop < valueNum) {
Console.Write("{0,8}", countLoop);// in giá trị của countLoop trong 8 vị trí
countLoop++;// tăng giá trị của biến countLoop lên một đơn vị }
// Mỗi khi hoàn thành một vòng lặp nhỏ thì xuống dòng chuẩn vị vẽ lần tiếp theo Console.WriteLine();
countLoopTime++;// tăng giá trị countLoopTime lên một đơn vị
} while (countLoopTime < loopTime) ; Console.ReadKey(); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 33
XV. Cấu trúc hàm cơ bản trong C#
1. Cú pháp khai báo hàm
[Từ khóa 1] [Từ khóa 2] [Từ khóa n] ([Parameter]){ } Trong đó: 
[từ khóa 1], [từ khóa 2], [từ khóa n] là các từ khóa như: public, static, read
only,… và có thể không điền. 
Kiểu dữ liệu trả về như: từ khóa void hay mọi kiểu dữ liệu như int, long, bool, SinhVien,…  Tên hàm: - Là tên gọi của hàm.
- Tên có thể đặt tùy ý nhưng nên đặt tên theo quy tắc đặt tên để có sự đồng
bộ ngầm định giữa các lập trình viên dễ tìm và dễ nhớ.
- Xem cách khởi tạo hàm giống khởi tạo biến ở chỗ. Đều cần kiễu dữ liệu
và tên. Có thể có các từ khóa. Tên tái sử dụng hàm ở nơi mong muốn. 
Parametter là tham số truyền vào để sử dụng nội bộ trong hàm. Cấu trúc
khởi tạo như một biến thông thường. Có thể không điền. 
Hàm chỉ được khai báo trong class.
Lưu ý: mọi hàm đều phải có cặp ngoặc nhọn {} biểu thị là một khối lệnh. Mọi
dòng code xử lý của hàm đều được viết bên trong cặp ngoặc nhọn {}. Không thể
khai báo một hàm trong một hàm khác theo cách thông thường.
Một hàm cơ bản hay thấy với cấu trúc bắt buộc phải có trong lập trình là hàm Main:
static void Main(string[] args) { }
Static là từ khóa. Có thể không sử dụng được. Nhưng ở hàm main thì phải có.
Void là kiểu trả về. với hàm có kiểu trả về là void thì sẽ không cần từ khóa
return. Hoặc có nhưng chỉ đơn giản là ghi return.
Main là tên hàm. có thể đặt tùy ý. Nhưng ở trường hợp này thì bắt buộc phải là
main vì mỗi chương trình đều cần hàm main.
String[] args là parametter truyền từ bên ngoài vào để sử dụng hàm. co thể
không có cũng được. Nhưng ở trương hợp hàm main là bắt buộc phải có. 2. Hàm void void Demo() void Demo() { { // some code // some code return; } }
Một số lưu ý về sau: vì chúng ta đang viết code trên nền consle c#. Bắt buộc
phải có hàm main. Nhưng hàm mian lại có từ khóa static. Nên để trong hàm main
có thể sử dụng các hàm mà ta viết ra thì các hàm đó cũng phải có từ static.
Khi sử dụng hàm ta sẽ gọi lại tên hàm kèm theo dấu () biểu thị đó là một hàm.
sau này nếu có parametter thì sẽ thêm giá trị vào bên trong dấu ().
Chúng ta có thể gọi lại nhiều lần.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 34
static void Main(string[] args) {
Demo(); // Gọi lại hàm để sử dụng Console.ReadKey(); }
3. Hàm có kiểu trả về khác void
Với hàm có kiểu trả về khác void. Trong thân hàm bắt buộc phải có dòng return ;
Giá trị trả về phải có kiểu dữ liệu tương ứng với Kiểu dữ liệu trả về khi khai báo hàm.
static void Main(string[] args) {
Console.WriteLine(ReturnANumber()); Console.ReadKey(); } ///
/// Hàm trả về giá trị số nguyên 5 thông qua tên hàm
/// Lưu ý giá trị trả về phải cùng kiểu dữ liệu với kiểu trả về của hàm /// Ở đây là kiểu int /// /// static int ReturnANumber() { // b t ắ bu c ộ ph i ả có c u
ấ trúc return trong thân hàm return 5; }  Kết quả bằng: 5 4. Parametter
Muốn thực hiện tính tổng hai số nhiều lần. Để tạo sự linh hoạt cho hàm thì
chúng ta sẽ tìm hiểu thêm về parameter:
Có thể hiểu đơn giản parametter là
Tập hợp một hay nhiều biến chứa các giá trị cần thiết để thao tác trong hàm.
Các giá trị của các biến này là những giá trị mà người dùng truyền vào khi gọi hàm đó.
Khai báo một parametter cũng giống như khai báo biến. Khi khai báo nhiều
parametter thì các khai báo phải cách nhau bởi dấu “,”
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 35
Cho người dùng truyền vào 2 số họ muốn tính tổng vào 2 biến.
Từ đó ta chỉ cần tính tổng giá trị 2 biến đó rồi trả kết quả về cho người dùng.
static void Main(string[] args) {
// khi sử dụng hàm phải truyền đúng số lượng, thứ tự parameter vào như khai báo của hàm
// đồng thời kiểu dữ liệu truyền vào của parameter phải trùng khớp với khai báo của hàm
Console.WriteLine(SumTwoNumber(5, 10)); Console.ReadKey(); } ///
/// hàm trả ra kết quả tổng của 2 số firstNumber và secondNumber /// /// /// ///
static int SumTwoNumber(int firstNumber, int secondNumber) {
return firstNumber + secondNumber; }  Kết quả là: 15 
Các khai báo int firstNumber, int secondNumber là các khai báo
parametter. Với khai báo này ta hiểu rằng muốn sử dụng hàm này thì cần
truyền vào 2 giá trị kiểu int. 
Các parametter được xem như các biến cục bộ có phạm vị sử dụng trong hàm 
Các parametter được khởi tạo ngay khi gọi hàm và được hủy khi kết thúc gọi hàm. 
Số lượng parameter là không giới hạn. 
Khi sử dụng hàm phải truyền vào đủ và đúng parameter. (Đủ số lượng, đúng
kiểu dữ liệu và đúng thứ tự như khai báo) 
Có thể khai báo các parameter với các kiểu dữ liệu khác nhau. 
Hàm sử dụng sẽ tạo ra các bản sao của parameter truyền vào trên RAM. Sau
đó dùng những bản sao đó để xử lý dữ liệu. Cho nên kết thúc lời gọi hàm
giá trị của các parameter sẽ không bị thay đổi.
Ví dụ về các paremetter với kiểu dữ liệu khác nhau:
static void Main(string[] args) { PrintSomeThing("K9", 22);
PrintSomeThing("HowKteam.com", 1); Console.ReadKey(); }
static void PrintSomeThing(string name, int age) {
// in ra màn hình tên và tuổi được truyền vào
Console.WriteLine("This is {0}, {1} years old.", name, age); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 36 XVI.
Biến toàn cục và biến cục bộ trong C#
Biến toàn cục là biến được khai báo ở phân cấp cao hơn vị trí đang xác định.
Biến cục bộ là biến được khai báo ở cùng phân cấp tại vị trí đang xác định.
Vòng đời của biến toàn cục và biến cục bộ bắt đầu khi khối lệnh chứa nó bắt đầu
(khối lệnh bắt đầu bằng dấu “{“) và kết thúc khi khối lệnh chứa nó kết thúc (khối lệnh
kết thúc bằng dấu “}”).
Biến cục bộ được ưu tiên sử dụng hơn biến toàn cục trong trường hợp 2 biến này
trùng tên.Ví dụ biến toàn cục: class Program class Program { {
// biến toàn cục của các hàm nằm trong class
// biến toàn cục của các hàm nằm Program trong class Program
// biến cục bộ của class Program
// biến cục bộ của class Program static int value = 5; static int value = 5;
static void Main(string[] args)
static void Main(string[] args) { {
// in ra màn hình biến toàn cục
// in ra màn hình biến toàn cục Console.WriteLine(value); Console.WriteLine(value);
// thay đổi giá trị của value PrintSomeThing(); value = 10; Console.ReadKey();
// kết quả gọi hàm này sẽ không thay đổi vì ưu tiên biến cục bộ hơn } PrintSomeThing(); static void PrintSomeThing() Console.ReadKey(); { }
// in ra màn hình biến toán cục static void PrintSomeThing() Console.WriteLine(value); { } int value = 9; }
// in ra màn hình biến toàn cục Console.WriteLine(value); } } Lưu ý:
Parameter chính là một biến cục bộ. 
Biến cục bộ có phạm vi sử dụng bên trong cặp dấu ngoặc nhọn { }. XVII.
Từ khóa ref và out trong C# Ta xét ví dụ sau:
static void Main(string[] args) { int value = 5;
Console.WriteLine("Value before increase: {0}", value); IncreaseValue(value);
Console.WriteLine("Value after increase: {0}", value); Console.ReadKey(); }
static void IncreaseValue(int value) { value++; }
Kết quả màn hình in ra hai giá trị 5. Vì sau khi kết thúc hàm IncreaseValue giá trị
của value vẫn không thay đổi.
Với mong muốn giá trị của value sẽ thay đổi sau khi kết thúc lời gọi
hàm IncreaseValue thì chúng ta sẽ thêm từ khóa ref phía trước kiểu dữ liệu của
static void Main(string[] args) { int value = 5;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 37
parameter mong muốn thay đổi giá trị khi khai báo hàm. Đồng thời phải thêm từ
khóa ref ngay trước biến parameter truyền vào khi sử dụng hàm.
Kết quả in ra màn hình giá trị 5 và 6. Do giá trị của value đã thay đổi sau khi kết thúc
lời gọi hàm IncreaseValue. Lưu ý:
 Từ khóa ref phải có trước tên parametter của hàm và trước tên biến truyền vào khi gọi hàm sử dụng.
 Truyền parameter có từ khóa ref bắt buộc phải là một biến (không thể truyền vào
một hằng vì hằng là giá trị không thay đổi).
 Có thể có một hoặc nhiều parameter với từ khóa ref trong lời khai báo hàm.
 Biến truyền vào có từ khóa ref thì phải được khởi tạo giá trị trước khi truyền vào.
 Hàm sử dụng sẽ thao tác trực tiếp với vùng nhớ của các parameter trên RAM.
Cho nên kết thúc lời gọi hàm giá trị các parameter sẽ bị thay đổi. Từ khóa out
Từ khóa out cũng tương tự từ khóa ref. Đó là:
- Vùng nhớ của các parameter sẽ được hàm sử dụng thao tác trực tiếp, dẫn đến khi
kết thúc lời gọi hàm giá trị của các parametter có thể bị thay đổi.
- Phải có từ khóa out trước tên parameter của hàm và trước tên biến truyền vào khi gọi hàm sử dụng.
Nhưng có một sự khác biệt đó là:
- Biến truyền vào có từ khóa out sẽ không cần khởi tạo giá trị ban đầu.
- Parameter đó chỉ như một thùng chứa kết quả trả về khi kết thúc gọi hàm.
- Đồng thời parameter đó phải được khởi tạo ngay bên trong lời gọi hàm.
static void Main(string[] args) { int value = 5;
Console.WriteLine("Value before increase: {0}", value); IncreaseValue(out value);
Console.WriteLine("Value after increase: {0}", value); Console.ReadKey(); }
static void IncreaseValue(out int value) { Value = 0; value++; }
Trong thân hàm IncreaseValue bắt buộc phải khởi tạo giá trị cho biến value. Kết quả
màn hình xuất ra giá trị 5 ban đầu và 1 là kết quả cuối cùng của biến value sau khi kết
thúc lời gọi hàm IncreaseValue. XVIII.
Mảng 1 chiều trong C# Khái niệm về mảng Mảng là -
Tập hợp các đối tượng có cùng kiểu dữ liệu. -
Mỗi đối tượng trong mảng được gọi là một phần tử. -
Các phần tử phân biệt với nhau bằng chỉ số phần tử. Trong C# chỉ số phần tử là
các số nguyên không âm và bắt đầu từ 0 1 2 3… Đặc điểm của mảng: -
Các phần tử trong mảng dùng chung một tên và được truy xuất thông qua chỉ số phần tử. -
Một mảng cần có giới hạn số phần tử mà mảng có thể chứa. -
Phải cấp phát vùng nhớ mới có thể sử dụng mảng. -
Vị trí ô nhớ của các phần tử trong mảng được cấp phát liền kề nhau.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 38
Những lợi ích khi sử dụng mảng: -
Gom nhóm các đối tượng có chung tính chất lại với nhau giúp code gọn gàng hơn. -
Để thao tác, dễ quản lý, nâng cấp sửa chữa. -
Dễ dàng áp dụng các cấu trúc lặp vào để xử lý dữ liệu. Cú pháp: [] ; Trong đó: -
là kiểu dữ liệu của các phần tử trong mảng. -
Cặp dấu [] là ký hiệu cho khai báo mảng 1 chiều. -
là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng. Cấp phát vùng nhớ: -
Được thực hiện thông qua toán tử new -
Lưu ý là khi cấp phát vùng nhớ cho mảng 1 chiều ta cần chỉ ra số phần tử tối đa của mảng. -
Sau khi mảng được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá trị mặc định: o
Đối với số nguyên là 0 o
Đối với số thực là 0.0 o
Đối với kiểu ký tự là ‘’ (ký tự rỗng) o
Đối với kiểu tham chiếu là null -
Chúng ta có thể khởi tạo giá trị khác mà chúng ta mong muốn ngay khi cấp phát
vùng nhớ bằng cú pháp sau: [] = new [] { , …, }; -
Các giá trị khởi tạo nằm trong cặp dấu ngoặc ngọn {} và cách nhau bởi dấu phẩy. -
Chúng ta không cần cung cấp số phần tử tối đa mà trình biên dịch sẽ tự đếm
xem bạn đã khởi tạo bao nhiêu giá trị và xem nó như số phần tử tối đa. Vì thế
dù việc khai báo số phần tử tối đa không lỗi nhưng trong trường hợp này nó không có ý nghĩa lắm! Khởi tạo giá trị Cú pháp: [] = { , …, }; Ví dụ: int[] IntArray = { 3, 9, 10 };
Về bản chất thì cách này trình biên dịch vẫn xem xét số phần tử khởi tạo và cấp phát
vùng nhớ cho biến mảng sau đó thực khởi tạo giá trị cho các phần tử trong mảng.
Nhưng cách viết này có vẻ nhanh và gọn hơn so với cách cấp phát vùng nhớ rồi mới khởi tạo giá trị.
Tóm lại ta có 3 cách khai báo và khởi tạo sau: 
Khai báo và cấp phát vùng nhớ
string[] Array = new string[3]; 
Khai báo, cấp phát và khởi tạo giá trị cho mảng
string[] Kteam = new string[] { "HowKteam","Free Education" }; 
Khởi tạo giá trị cho mảng
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 39 int[] IntArray = { 3, 9, 10 }; Sử dụng mảng
Kiểu mảng có thể dùng làm: - Kiểu dữ liệu cho biến. - Kiểu trả về cho hàm. -
Tham số truyền vào cho hàm.
Các phần tử của mảng được truy xuất thông qua chỉ số phần tử và cặp dấu []. Có thể
xem các phần tử của mảng như là các biến đơn và thao tác như thao tác với biến bình thường.
// Khai báo, cấp phát và khởi tạo mảng kiểu string và tên là Kteam
string[] Kteam = new string[] { "HowKteam", "Free Education" }; /*
* Vì chỉ số phần tử được đánh số từ 0 nên muốn truy xuất đến phần tử thứ 2 của mảng
thì chỉ số phần tử là 1 */ Console.WriteLine(Kteam[1]);
Tên thuộc tính hoặc Ý nghĩa phương thức Length
Thuộc tính trả về số nguyên kiểu int là số phần tử tối đa của mảng LongLength
Thuộc tính trả về số nguyên kiểu long là số phần tử tối đa của mảng GetLength(
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác định. Lưu ý chiều>)
chiều của mảng là các số nguyên và được đánh số từ 0. Cho nên đối với
mảng 1 chiều thì số chiều là 0. GetLongLength(
Tương tự GetLength nhưng trả về số nguyên kiểu long chiều>) Sort()
Phương thức thực hiện sắp xếp mảng theo một thứ tự Clear()
Phương thức xóa hết dữ liệu trong mảng và đưa về giá trị mặc định của
kiểu. Lưu ý là chỉ xóa giá trị, vùng nhớ vẫn còn đó và có thể tiếp tục sử
dụng mà không cần cấp phát. Copy()
Thực hiện copy giá trị của mảng ra một vùng nhớ mới (phép gán thông
thường thì 2 đối tượng sẽ dùng chung vùng nhớ rất nguy hiểm vì đối
tượng này thay đổi dẫn đến đối tượng kia cũng thay đổi) Reverse()
Phương thức thực hiện đảo ngược thứ tự của mảng 1 chiều  Ý tưởng: o
Để truy xuất đến các phần tử của mảng cần thông qua chỉ số phần tử. o
Mà chỉ số phần tử là các số nguyên tăng dần. o
Từ đó ta có thể tận dụng vòng lặp để tăng giá trị 1 biến lên rồi xem biến đó như
là chỉ số phần tử của mảng.  Ví dụ: int[] Kteam = new int[3];
for (int i = 0; i < 3; i++)
// Vì các phần tử có chỉ số là 0 1 2 nên điều kiện dừng là i < 3 {
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 40
Mọi thứ đều suôn sẻ cho đến một ngày vì lí do nào đó bạn cần nâng cấp
mảng Kteam lên 10 phần tử. Khi đó vấn đề sẽ xuất hiện, bạn phải đi thay đổi tất cả
những chỗ nào liên quan đến số phần tử từ 3 thành 10 hết. Như vậy mỗi lần có thay
đổi về số phần tử bạn đều phải làm lại những việc đó. Vậy tại sao ta không tận dụng
một thuộc tính vừa được học xong để giải quyết?
Với cách duyệt mảng như thế này bạn đã có thể làm mọi thứ với mảng từ nhập xuất
giá trị cho mảng đến tính toán phức tạp.
Chương trình ví dụ: tính năm âm lịch từ năm dương lịch đã nhập Ý tưởng: int[] Kteam = new int[3]; /*
* Thay số 3 thành thuộc tính Length.
* Bây giờ bạn có thay đổi số phần tử thì chỉ cần thay đổi ở khai báo thôi là xong! */
for (int i = 0; i < Kteam.Length; i++) { // Do something }
Chúng ta thấy các case là các giá trị nguyên không âm và tăng dần làm ta liên tưởng tới chỉ số của mảng.
Như vậy nếu như ta tạo ra 1 mảng có số phần tử bằng số case và giá trị của phần tử
tại chỉ số thứ i sẽ tương đương với giá trị của case i.
Việc còn lại chỉ là tra cứu theo chỉ số của mảng nữa là xong! Chương trình mẫu:
int Year; // Biến chứa giá trị năm cần tính.
// Mảng Can chứa các giá trị can tương ứng theo bảng can
string[] Can = { "Canh", "Tan", "Nham", "Quy", "Giap",
"At", "Binh", "Dinh", "Mau", "Ky" };
// Mảng Chi chứa các giá trị chi tương ứng theo bảng chi
string[] Chi = { "Than", "Dau", "Tuat", "Hoi", "Ty", "Suu",
"Dan", "Meo", "Thin", "Ty", "Ngo", "Mui" };
Console.Write(" Moi ban nhap mot nam bat ky: ");
// Nhập năm dương lịch và ép kiểu về kiểu số nguyên
Year = Int32.Parse(Console.ReadLine()); /*
* Vì kết quả phép chia lấy dư của Year%10 hoặc Year%12 sẽ cho ra số nguyên
* Nên ta sẽ dùng nó làm chỉ số phần tử để tra cứu ra giá trị can chi tương ứng. Thay vì
dùng cách cũ là switch case
* Như vậy cách này vừa đơn giản vừa dễ hiểu, code rất ít sẽ với cách dùng switch case */
Console.WriteLine("Nam {0} co nam am lich la: {1} {2}",
Year, Can[Year%10], Chi[Year%12]);
// Nối Can và Chi lại để được năm âm lịch Console.ReadLine();
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 41 XIX.
Mảng 2 chiều trong C# Khai báo mảng 2 chiều
Mảng 2 chiều được hình dung như một bảng có m dòng và n cột với một số đặc trưng sau:
- Mảng 2 chiều mang những đặc trưng cơ bản của một mảng bình thường. -
-
Các phần tử trong mảng 2 chiều được truy xuất thông qua 2 chỉ số phần tử (tạm
gọi là chỉ số dòng và chỉ số cột)
- Hình ảnh minh họa mảng 2: Giả sử ta có mảng A có 5 dòng và 8 cột.
Các dòng và các cột được đánh số từ 0 và tăng dần. Mỗi phần tử là giao nhau của
dòng và cột tương ứng đồng thời ta sử dụng chỉ số dòng cột đó để truy xuất đến phần tử của mảng 2 chiều.
Ví dụ: A[1, 2] là cách truy xuất đến phần tử ở dòng thứ 2 cột thứ 3 (do chỉ số được đánh số từ 0) Cú pháp: ể d ữ li u ệ > [ , ] mảng>; Trong đó:
là kiểu dữ liệu của các phần tử trong mảng.
Cặp dấu [ , ] là ký hiệu cho khai báo mảng hai chiểu.
là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến.
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng. Cấp phát vùng nhớ
Được thực hiện thông qua toán tử new. Lưu ý khi cấp phát vùng nhớ cho mảng 2
chiều ta cần chỉ ra số dòng và số cột tối đa của mảng Ví dụ:
/* * Khai báo mảng 2 chiều kiểu string và có tên là Kteam.
* Sau đó thực hiện cấp phát vùng nhớ với số dòng là 2 và số cột là 3. */
string[,] SinhVien = new string[2, 3];
Sau khi mảng được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá trị mặc định:
- Đổi với số nguyên là 0
- Đối với số thực là 0.0
- Đối với kiểu ký tự là ‘ ’ (ký tự rỗng)
- Đối với kiểu tham chiếu là null
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 42 -
Chúng ta có thể tạo giá trị các mà chúng ta mong muốn ngay khi cấp phát vùng nhớ bằng cú pháp sau:
ệ >[,] ả g> = new ể d ữ li u ệ >[] { {ị dòng 1 c t ộ 1>,…,ị dòng 1 c t ộ n>}, … {ị dòng m c t ộ 1>,…,ị dòng m c t ộ n>} }; -
Vì đây là mảng 2 chiều nên chúng ta sẽ không khởi tạo giống mảng 1 chiều mà
phải khởi tạo giá trị theo từng dòng một. xem từng dòng là 1 mảng 1 chiều và
khởi tạo như mảng một chiều. -
Các giá trị khởi tạo nằm trong một dấu ngoặc nhọn {} và cách nhau bởi dấu phẩy. -
Chúng ta không cần cung cấp số dòng và số cột tối đa mà trình biên dịch sẽ tự
đếm xem bạn đã khởi tạo bao nhiêu dòng và mỗi dòng bao nhiêu giá trị rồi xem
nó như số dòng số cột tối đa. Khởi tạo giá trị ữ li u ệ >[,] ả g> = { {ị dòng 1 c t ộ 1>,…,ị dòng 1 c t ộ n>}, … {ị dòng m c t ộ 1>,…,ị dòng m c t ộ n>} }; Cú pháp: Ví dụ: int[,] IntArray = { {1, 2}, {3, 4}, {5, 6} };
Về bản chất thì cách này trình biên dịch vẫn xem xét số phần tử khởi tạo và cấp phát vùng nhớ
cho biến mảng sau đó thực khởi tạo giá trị cho các phần tử trong mảng. nhưng cách viết này có
vẻ nhanh và gọn hơn so với cách cấp phát vùng nhớ rồi mới khởi tạo giá trị.
Tóm lại, cũng như mảng một chiều, mảng 2 chiều cũng có 3 cách khai báo và khởi tạo:
- Khai báo và cấp phát vùng nhớ
string[,] Array = new string[2, 3];
- Khai báo, cấp phát và khởi tạo giá trị cho mảng
string[,] Kteam = new string[,] {
{ "HowKteam", "Free Education" },
{ “HowKteam.com”, “Share to be better” } };
- Khởi tạo giá trị cho mảng int[] IntArray = { {1, 2}, {3, 4}, {5, 6} };
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 43
Sử dụng ảng 2 chiều:
Tương tự như mảng 1 chiều, kiểu mảng 2 chiều cũng có thể dùng làm: - Kiểu dữ liệu cho biến. - Kiểu trả về cho hàm. -
Tham số truyền vào cho hàm. o
Các phần tử của mảng được truy xuất thông qua chỉ số dòng và chỉ số cột
(chỉ số dòng viết trước, chỉ số cột viết sau ngăn cách nhau bởi dấu ,) và cặp
dấu []. Có thể xem các phần tử của mảng như là các biến đơn và thao tác
như thao tác với biến bình thường.
Một số thuộc tính của mảng 2 chiều: Tên thuộc tính Ý nghĩa phương thức Length
Thuộc tính sẽ trả về số nguyên kiểu int là số phần tử tối đa của mảng
(số phần tử của mảng 2 chiều là tích số dòng số cột của mảng) Longlength
Tương tự như thuộc tính Length nhưng trả về số nguyên kiểu long GetLength
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác định. Lưu ()
ý chiều của mảng là các số nguyên và được đánh số từ 0. Đối với
mảng 2 chiều thì GetLength (0) là độ dài đầu tiên tương ứng với số
dòng và GetLength (1) là độ dài chiều thứ 2 tương ứng số cột. GetLongLength
Tương tự GetLength nhưng trả về số nguyên kiểu long () Clone()
Thực hiện coppy giá trị của mảng ra một vùng nhớ mới (phép gán
thông thường thì 2 đối tượng này thay đổi dẫn đến đối tượng kia cũng thay đổi)
Cách duyệt mảng 2 chiều:
Ý tưởng: Tương tự như ý tưởng duyệt mảng 1 chiều.
Nhưng do mảng 2 chiều có 2 chỉ số là chỉ số dòng và chỉ số cột nên chúng ta cần 2 vòng lặp lồng vào nhau. Ví dụ:
int[,] IntArray = new int [9, 10];
/*Sử dụng 2 vòng for lồng vào nhau để duyệt mảng 2 chiều
* Vòng lặp ngoài là vòng lặp duyệt mỗi dòng của của mảng 2 chiều
* Với mỗi dòng thì vòng lặp trong là vòng lặp duyệt các phần tử trên dòng đó
(duyệt từng cột trên dòng hiện tại)*/
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 10; j++) {
/* Với cách duyệt này thì IntArray[i, j] sẽ là phần tử hiện tại mình đang xét
* Code xử lý sẽ viết ở đây */ } }
Cách duyệt này sẽ duyệt tuần tự các dòng trong mảng 2 chiều, ở mỗi dòng sẽ duyệt từ
đầu dòng đến cuối dòng. Bạn hoàn toàn có thể duyệt theo ý mình bằng cách thay đổi giá trị trong vòng lặp.
Ví dụ sau sẽ duyệt theo cột. Tức là duyệt tuần tự từng cột rồi ở mỗi cột duyệt từ trên xuống dưới.
/* Duyệt mảng 2 chiều theo cột
* Các bạn để ý sự thay đổi trong 2 vòng lặp*/
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
/* Lưu ý là các phần tử được truy xuất là IntArray[j, i] thay vì IntArray[i, j] * Code xử lý */ } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 44
Để ý là cả 2 cách duyệt mình đều vi phạm 1 điều mà ở phần duyệt mảng 1 chiều của
bài trước mình đã trình bày. Đó là nên sử dụng hàm trả về số dòng, số cột thay vì viết
cứng một con số cụ thể.
Lúc này hàm GetLength() thực sự phát huy tác dụng. Các bạn cùng xem nhé:
/*Như đã trình bày ở phần trước thì: GetLength(0) sẽ trả về số dòng của
mảng 2 chiềuGetLength(1) sẽ trả về số cột của mảng 2 chiều*/
for (int i = 0; i < IntArray.GetLength(0); i++) {
for (int j = 0; j < IntArray.GetLength(1); j++) { // Code xử lý } }
Ví dụ chương trình sử dụng mảng 2 chiều
Ta thử xét 1 ví dụ đơn giản đó là viết chương trình cho phép nhập vào giá trị số
nguyên cho 1 mảng 2 chiều bất kỳ sau đó in ra màn hình mảng đã nhập kèm theo tổng
tất cả các giá trị mảng trong. Chương trình minh họa:
Console.Write(" Moi ban nhap so dong cua mang: ");
int Rows = int.Parse(Console.ReadLine());
Console.Write(" Moi ban nhap so cot cua mang: ");
int Columns = int.Parse(Console.ReadLine());
// Tạo 1 mảng 2 chiều với số dòng và số cột đã nhập
int[,] IntArray = new int[Rows, Columns];
/* Duyệt mảng để nhập giá trị cho các phần tử
* Ở đây mình muốn minh họa cách sử dụng mảng nên mình bỏ qua các bước kiểm tra dữ liệu mà ép kiểu trực tiếp
* Điều này có thể gây lỗi khi nhập sai nên các bạn hãy cải tiến chương trình này cho đầy đủ nhé! */
for (int i = 0; i < IntArray.GetLength(0); i++) {
for (int j = 0; j < IntArray.GetLength(1); j++) {
Console.Write(" Moi ban nhap phan tu IntArray[{0}, {1}] = ", i, j);
IntArray[i, j] = int.Parse(Console.ReadLine()); } }
/* In mảng 2 chiều đã nhập ra màn hình
* Để tính tổng các giá trị trong mảng ta chỉ cần duyệt qua các phần tử và cộng chúng lại với nhau
* Tận dụng lúc duyệt mảng để in giá trị ta sẽ thực hiện tính tổng luôn để tránh phải duyệt lại mảng thêm lần nữa. */ int Sum = 0;
Console.WriteLine("\n Mang ban vua nhap la: ");
for (int i = 0; i < IntArray.GetLength(0); i++) {
for (int j = 0; j < IntArray.GetLength(1); j++) {
Console.Write(IntArray[i, j] + " "); Sum = Sum + IntArray[i, j]; }
// Sau khi in xong mỗi dòng ta thực hiện xuống dòng rồi mới in tiếp Console.WriteLine(); }
Console.WriteLine(" Tong cac gia tri trong mang: " + Sum);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 45 Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 46
XX. Mảng nhiều chiều trong lập trình C# cơ bản Mảng 3 chiều trong C#
Nếu như mảng 2 chiều được hình dung như một ma trận MxN thì mảng 3 chiều
được hình dung như một hình hộp chữ nhật có kích thước MxNxP Cú pháp: ể d ữ li u ệ > [ , , ] ả g>; Trong đó:
là kiểu dữ liệu của các phần tử trong mảng.
Cặp dấu [ , , ] là kí hiệu cho khai báo mảng 3 chiều.
là tên của mảng, cách đặt tên mảng cũng như cách đặt tên biến.
Để sử dụng được mảng ta phải khởi tạo giá trị hoặc cấp phát vùng nhớ cho mảng. Cấp phát vùng nhớ /* * Khai báo m n ả g 3 chi u ề ki u ể string và có tên là Kteam. * Sau đó th c ự hi n ệ c p ấ phát vùng nh ớ v i ớ 3 chỉ s ố l n ầ lư t ợ là 2, 2, 3. */
string[ , , ] Kteam = new string[2, 2, 3];
Sau khi được cấp phát vùng nhớ thì các phần tử trong mảng sẽ mang giá trị mặc định:
Đối với số nguyên là 0
Đối với số thực là 0.0
Đối với kiểu ký tự là ‘ ’
Đối với tham chiếu là null
Chúng ta có thể khởi tạo giá trị khác mà chúng ta mong muốn ngay khi cấp phát vùng nhớ bằng cú pháp: [ , , ] = new [ , , ] { { {, …, }, Khởi … tạo một Khởi tạo {, …, } mảng 2 1 mảng 1 chiều chiều mà } mỗi phần … tử là { mảng 2
{< giá trị tại vị trí m, 0, 0>, …, }, chiều … {, …, } } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 47 Ví dụ:
int[,,] Mang3Chieu = new int[,,] { { {1, 2, 3}, {4, 5, 6} }, { {7, 8, 9}, {10, 11, 12} } };
Với khai báo trên thì mảng 3 chiều sẽ là mảng 3 chiều với số phần tử tối đa của các
chiều lần lượt là 2 2 3.
Khai báo và cấp phát vùng nhớ
string[ , , ] Array = new string[2, 2, 3];
Khai báo, cấp phát và khởi tạo giá trị cho mảng [ , , ] = { { Khởi {, …, }, tạo Khởi tạo một 1 mảng … mảng 1 chiều {, …, } 2 mà mỗi } chiều phần tử … là một mảng 2 { chiều
{< giá trị tại vị trí m, 0, 0>, …, }, … {, …, } } }
Sử dụng mảng 3 chiều
Các phần tử của mảng được truy xuất thông qua 3 chỉ số ngăn cách nhau bởi dấu phẩy
và nằm trong cặp dấu [ ]. Tên thuộc tính hoặc Ý nghĩa ph ư // ơ ng K t haih ức
báo, cấp phát và khởi tạo mảng 3 chiều kiểu int và tên là Mang3Chieu i L nt e [ ng ,,] th Mang3Chieu T = huộc new itính
nt[,, t]rả về số nguyên kiểu int là số phần tử tối đa của mảng (số {
phần tử của mảng 3 chiều là tích 3 chỉ số của mảng) LongL e ngt h {
Tương tự như thuộc tính Length nhưng trả về số nguyên kiểu long G e t L e ngt h {1, 2, T
3},rả về số nguyên kiểu int là số phần tử trong chiều đã xác định. Lưu (< s ố c h i ề u> ) {4, 5, ý
6} chiều của mảng là các số nguyên và được đánh số từ 0. Đối với },
mảng 3 chiều thì GetLength(0) là độ dài chiều đầu tiên và {
GetLength(1) là độ dài chiều thứ 2 và GetLength(2) là độ dài chiều {7, 8, 9}, thứ 3. {10, 11, 12} GetL ongL e ngt } h
Tương tự GetLength nhưng trả về số nguyên kiểu long (< s ố c h i ề u> }; ) // R Tr ank uy xuất đến phầ T n huộc tử c ó tí c nh ác t c rả hỉ về s ố s l ố ầ nguyê n lượt l n à in 1 t 1 là 2 s ố chiều của mảng Clone C () onsole.Write T L hự i c ne hi (Mện a copy ng3Chgiieá trị của mả
u[1, 1, 2]);ng ra một vùng nhớ mới (phép gán
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 48
thông thường thì 2 đối tượng sẽ dùng chung vùng nhớ rất nguy hiểm
vì đối tượng này thay đổi dẫn đến đối tượng kia cũng thay đổi)
Cách duyệt mảng 3 chiều Ý tưởng:
Tương tự như ý tưởng duyệt mảng 2 chiều.
Nhưng do mảng 3 chiều có 3 chỉ số nên chúng ta cần 3 vòng lặp lồng vào nhau. Ví dụ:
int[, ,] IntArray = new int[3, 9, 10];
/* Sử dụng 3 vòng for lồng vào nhau để duyệt mảng 3 chiều
* Vòng lặp đầu tiên là vòng lặp duyệt các phần tử của chiều đầu tiên.
* Vòng lặp thứ 2 là vòng lặp duyệt các phần tử của chiều thứ 2.
* Vòng lặp thứ 3 là vòng lặp duyệt các phần tử của chiều thứ 3. */
for (int i = 0; i < IntArray.GetLength(0); i++) {
for (int j = 0; j < IntArray.GetLength(1); j++) {
for (int k = 0; k < IntArray.GetLength(2); k++) {
/* cách duyệt này thì IntArray[i, j, k] sẽ là phần tử hiện tại mình đang xét
* Code xử lý sẽ viết ở đây*/ } } }
Cách duyệt này sẽ duyệt tuần tự các chiều trong mảng 3 chiều, ở mỗi chiều sẽ duyệt
từ đầu đến cuối. Bạn hoàn toàn có thể duyệt theo ý mình bằng cách thay đổi giá trị trong vòng lặp.
Vì mảng 3 chiều cũng ít được sử dụng nên mình chỉ giới thiệu qua thôi chứ không đi
quá chi tiết. Với kiến thức cơ bản mình đã cung cấp các bạn hoàn toàn có thể tự tìm hiểu thêm khi cần thiết.
Lớp array trong C# là lớp cơ sở cho mọi mảng, một mảng bất kỳ đều được kế thừa từ lớp này Tên thuộc tính Ý nghĩa Length
Thuộc tính trả về số nguyên kiểu int là số phần tử tối đa của mảng. LongLength
Tương tự như thuộc tính Length nhưng trả về số nguyên kiểu long Rank
Thuộc tính trả về số nguyên int là số chiều của mảng
Một số phương thức trong lớp array: Tên phương thức Ý nghĩa Clear()
Thiết lập lại giá trị mặc định co tất cả các phần tử trong mảng. GetLength
Trả về số nguyên kiểu int là số phần tử trong chiều đã xác định. ()
Lưu ý chiều của mảng là các số nguyên và được đánh số từ 0. GetLongLength
Tương tự GetLength nhưng trả về số nguyên kiểu long () GetValue()
Trả về giá trị 1 phần tử của mảng tại vị trí truyền vào. Nếu là
mảng nhiều chiều thì có thể truyền vào danh sách các chỉ số. Reverse()
Đảo ngược các giá trị của mảng 1 chiều. Sort()
Sắp xếp các phần tử của mảng 1 chiều. IndexOf(,
Tìm kiếm 1 phần tử có tồn tại trong mảng hay không. Nếu có thì )
trả về vị trí xuất hiện đầu tiên trong mảng ngược lại sẽ trả về 0.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 49
Đây chỉ là một số thuộc tính và phương thức tiêu biểu. Ngoài ra còn nhiều phương
thức, thuộc tính khác các bạn có thể tự khám phá.
Lưu ý là các phương thức Sort, Reverse, IndexOf được gọi thông qua tên lớp. Tức
là muốn sử dụng các phương thức trên ta sẽ dùng cú pháp:  Array.Sort()  Array.Reverse()  Array.IndexOf(, )
Nhờ sự hỗ trợ của các phương thức trên mà ta có thể dễ dàng thao tác với dữ liệu
mảng như sắp xếp, đảo ngược, . . . mà không cần phải viết chi tiết ra giải thuật như thế nào.
Sau đây là một vài ví dụ ứng dụng lớp Array:
/*Khai báo và khởi tạo mảng 1 chiều tên IntArray có 4 phần tử chưa được sắp xếp*/
int[] IntArray = { 5, 2, 1, 3 };
/* Thực hiện câu lệnh sắp xếp mảng
* Ở đây mặc định là sẽ sắp xếp tăng dần.
* Nếu bạn muốn sắp xếp giảm dần có thể tận dụng phương thức đảo ngược các giá trị mảng
để được mảng giảm dần*/ Array.Sort(IntArray);
Console.WriteLine(" Mang da sap xep tang dan: ");
for (int i = 0; i < IntArray.Length; i++) {
Console.Write("\t" + IntArray[i]); } Console.WriteLine();
/* Đảo ngược các phần tử của mảng để được 1 mảng giảm dần */ Array.Reverse(IntArray);
Console.WriteLine(" Mang da sap xep giam dan: ");
for (int i = 0; i < IntArray.Length; i++) {
Console.Write("\t" + IntArray[i]); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 50 XXI.
Vòng lập foreach trong C# cơ bản
Cấu trúc lập foreach cho phép chúng ta duyệt 1 mảng hoặc 1 tập hợp
Một số đặc trưng của foreach:
Foreach không duyệt mảng hoặc tập hợp thông qua chỉ số phần tử như cấu trúc lặp for
Foreach duyệt tuần tự các phần tử trong mảng hoặc tập hợp
Foreach chỉ dùng để duyệt mảng hoặc tập hợp ngoài ra không thể làm gì khác Cú pháp:
foreach (<kiểu dữ liệu> <tên biến tạm> in <tên mảng hoặc tập hợp>) { // Code x ử lý } Trong đó: 
Các từ khoá foreach, in là từ khoá bắt buộc. 
< kiểu dữ liệu> là kiểu dữ liệu của các phần tử trong mảng hoặc tập hợp. 
< tên biến tạm> là tên 1 biến tạm đại diện cho phần tử đang xét khi duyệt mảng hoặc tập hợp. 
là tên của mảng hoặc tập hợp cần duyệt.
Nguyên tắc hoạt động
Foreach cũng có nguyên tắc hoạt động tương tự như các cấu trúc lặp khác cụ thể như sau: 
Ở vòng lặp đầu tiên sẽ gán giá trị của phần tử đầu tiên trong mảng vào biến tạm. 
Thực hiện khối lệnh bên trong vòng lặp foreach. 
Qua mỗi vòng lặp tiếp theo sẽ thực hiện kiểm tra xem đã duyệt hết mảng hoặc tập
hợp chưa. Nếu chưa thì tiếp gán giá trị của phần tử hiện tại vào biến tạm và tiếp tục
thực hiện khối lệnh bên trong. 
Nếu đã duyệt qua hết các phần tử thì vòng lặp sẽ kết thúc.
Qua nguyên tắc hoạt động trên ta có thể thấy: 
Biến tạm trong vòng lặp foreach sẽ tương đương với phần tử i trong cách duyệt của vòng lặp for 
Qua mỗi bước lặp ta chỉ có thể thao tác với giá trị của phần tử đang xét mà không
thể tương tác với các phần tử đứng trước nó hay đứng sau nó 
Bằng cách duyệt của foreach ta không thể thay đổi giá trị của các phần tử vì lúc
này giá trị của nó đã được sao chép ra một 1 biến tạm và ta chỉ có thể thao tác với biến tạm. 
Thậm chí việc thay đổi giá trị của biến tạm cũng không được phép. Nếu ta cố làm
điều đó thì sẽ gặp lỗi sau:
Sử dụng foreach trong C#
Trong C#, có những danh sách, tập hợp mà ta không thể truy xuất đến các phần tử của
nó thông qua chỉ số phần tử được (ví dụ như kiểu List, hoặc các collection, generic).
Trong trường hợp như vậy, để duyệt các danh sách, tập hợp có tính chất như trên
thì foreach là lựa chọn tốt nhất.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 51
Chúng ta sẽ tìm hiểu sức mạnh của foreach qua các bài học sau. Còn trong bài học
này mình chỉ ví dụ đơn giản để các bạn có thể nắm cú pháp cũng như cách sử dụng foreach. Xét chương trình sau: /*
* Khai báo mảng 1 chiều IntArray và khởi tạo giá trị.
* Các bạn có thể xem lại cú pháp này ở bài Mảng 1 chiều trong C#
* Khai báo 1 biến Sum để chứa giá trị tổng các phần tử trong mảng IntArray. */
int[] IntArray = { 1, 5, 2, 4, 6 }; int Sum = 0; /*
* Sử dụng foreach để duyệt mảng và in giá trị của các phần tử trong mảng.
* Đồng thời tận dụng vòng lặp để tính tổng các phần tử trong mảng. */ foreach (int item in IntArray) { Console.Write("\t" + item); Sum += item; }
Console.WriteLine("\n Sum = " + Sum);
Có lẽ chúng ta đã không mấy xa lạ với đoạn chương trình trên. Đoạn chương trình
trên sẽ duyệt mảng để in ra các giá trị của mảng và tính tổng các phần tử trong mảng.
Nhưng thay vì sử dụng for thì mình sử dụng foreach để các bạn có thể thấy được sự
tương đồng giữa các thành phần trong cấu trúc foreach và cấu trúc vòng lặp for.
Kết quả khi chạy đoạn chương trình trên:
Ví dụ thứ 2: ta thử sử dụng foreach để duyệt mảng jagged
/* Khai báo 1 mảng jagged tên là JaggedArray và khởi tạo giá trị.
* Các bạn có thể xem lại cú pháp khai báo này ở bài Mảng nhiều chiều trong C#.*/ int[][] JaggedArray = { new int[] { 1, 2, 3 }, new int[] { 5, 2, 4, 1, 6},
new int[] { 7, 3, 4, 2, 1, 5, 9, 8} };
/*Tương tự như dùng for, ta cũng dùng 2 vòng foreach lồng vào nhau để duyệt mảng. */
foreach (int[] Element in JaggedArray) { foreach (int item in Element) { Console.Write(item + " "); } Console.WriteLine(); }
Ta có thể thấy cách duyệt foreach ngắn gọn hơn nhiều so với cách duyệt bằng vòng lặp for thông thường.
Ta cũng chả quan tâm đến việc phải xử lý độ dài mảng hay chỉ số phần tử để truy
xuất 1 phần tử nào đó.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 52
Kết quả khi chạy chương trình trên
So sánh for và foreach
Foreach mang trong mình một số ưu điểm như: 
Câu lệnh ngắn gọn, sẽ sử dụng. 
Rất có ích khi duyệt danh sách, tập hợp mà không thể truy xuất thông qua chỉ số phần tử. 
Duyệt các danh sách, tập hợp có số phần tử không xác định hoặc số phần tử thay đổi liên tục.
Mặc dù có nhiều ưu điểm nhưng không hẳn là foreach hơn hẵn for. Tiêu chí For Foreach
Khả năng truy xuất phần tử
Truy xuất ngẫu nhiên (có thể gọi
Truy xuất tuần tự (chỉ sử
bất kỳ phần tử nào trong mảng
dụng được giá trị phần tử để sử dụng) đang xét)
Thay đổi được giá trị của các phần Có Không tử
Duyệt mảng, tập hợp khi không biết Không Có
được số phần tử của mảng, tập hợp
Hiệu suất (tốc độ xử lý) (*)
Đối với mảng, danh sách hoặc
Đối với mảng, danh sách
tập hợp có khả năng truy xuất hoặc tập hợp không có
ngẫu nhiên thì for sẽ chiếm ưu khả năng truy xuất ngẫu thế nhiên thì foreach chiếm ưu thế
Nhìn chung hiệu suất của for và foreach còn phụ thuộc vào cấu trúc dữ liệu đang xét
cho nên việc so sánh này chỉ mang tính chất tham khảo.
Sau đây là 2 đoạn chương trình kiểm tra tốc độ của for và foreach đối với 2 cấu trúc dữ liệu là m
ảng 1 chiều (có khả năng truy xuất ngẫu nhiên) và danh sách liên
kết LinkedList (không có khả năng truy xuất ngẫu nhiên):
Đầu tiên là mảng 1 chiều:
Đoạn chương trình mình thực hiện: -
Khai báo 1 mảng 1 chiều có 20 triệu phần tử (khai báo số phần tử lớn để có thể
thấy được sự chêch lệch về tốc độ) -
Lần lượt dùng for, foreach để duyệt mảng đó và thực hiện 1 câu lệnh nào đó. -
Cuối cùng là xuất ra thời gian thực thi của từng trường hợp dưới dạng giây và mili giây.
 Dựa vào kết quả ta có thể thấy được sự chêch lệch nhỏ về tốc độ, nếu kiểm tra với
số phần tử lớn hơn hoặc cấu trúc dữ liệu phức tạp hơn thì chêch lệch này càng lớn.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 53
/* Kiểm tra tốc độ của for */
* Sử dụng 1 cái đồng hồ để đo thời gian chạy của 2 vòng lặp for và foreach
* Ở đây mình chỉ kiểm tra tốc độ chứ không tập trung giải thích cú pháp
* Các bạn có thể tìm hiểu thêm. */
Stopwatch start = new Stopwatch(); start.Start();
Tiếp thao là danh sách liên kết LinkedList
 Chương trình mình thực hiện: o
Tạo 1 LinkedList và thêm vào 100000 phần tử. o
Sau đó lần lượt dùng for, foreach duyệt LinkedList trên và tính tổng
giá trị các phần tử trong mảng. o
Cuối cùng in ra thời gian chạy của for, foreach.
 Kết quả khi chạy chương trình trên là:
Dựa vào kết quả chạy ta thấy sự chênh lệch là quá lớn, rõ ràng đối với cấu trúc dữ
liệu phức tạp, không hỗ trợ truy xuất thông qua chỉ số phần tử nữa thì foreach chiếm ưu thế.
Tùy vào từng trường hợp mà ta nên dùng for hay dùng foreach. Không nên lạm dụng 1 thứ quá nhiều
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 54
// Khai báo 1 LinkedList chưa các số nguyên int và khởi tạo giá trị cho nó.
LinkedList list = new LinkedList();
for (int i = 0; i < 100000; i++) { list.AddLast(i); }
/* Kiểm tra tốc độ của for */
Stopwatch st = new Stopwatch();
int s1 = 0, length = list.Count; st.Start();
for (int i = 0; i < length; i++) {
/*Vì LinkedList không thể truy xuất thông qua chỉ số phần tử
* nên mình phải sử dụng 1 phương thức hỗ trợ làm điều này.
* Và đây chính là sự hạn chế của for đối với các cấu trúc dữ liệu tương tự như danh sách liên kết này. */ s1 += list.ElementAt(i); } st.Stop();
/* Kiểm tra tốc độ của foreach */
Stopwatch st2 = new Stopwatch(); int s2 = 0; st2.Start(); foreach (int item in list)
{ //Vì foreach không quan tâm đến chỉ số phần tử nên code viết rất ngắn gọn s2 += item; } st2.Stop();
/* In ra giá trị tính tổng giá trị các phần tử khi duyệt bằng for và foreach để chắc chắn rằng cả 2 đều chạy đúng */
Console.WriteLine(" s1 = {0} s2 = {1}", s1, s2);
Console.WriteLine(" Thoi gian chay cua for = {0} giay {1} mili
giay", st.Elapsed.Seconds, st.Elapsed.Milliseconds);
Console.WriteLine(" Thoi gian chay cua foreach = {0} giay {1}
mini giay", st2.Elapsed.Seconds, st2.Elapsed.Milliseconds);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 55 XXII.
Lớp string trong C# cơ bản
String là một kiểu dữ liệu tham chiếu được dùng để lưu trữ chuỗi ký tự. Vì là một
kiểu dữ liệu nên cách khai báo và sử dụng hoàn toàn tương tự các kiểu dữ liệu khác.
Hôm nay mình sẽ không bàn đến khai báo của nó nữa mà đi sâu vào các thuộc tính
và phương thức mà lớp String hỗ trợ.
Thuộc tính trong lớp string: Tên thuộc tính Ý nghĩa Length
Trả về một số nguyên kiểu int là độ dài của chuỗi
Một số phương thức thường dùng trong lớp string: Tên phương thức Ý nghĩa Ghi chú String.compare(string
So sanh 2 chuỗi strA và strB
Có thể gọi phương thức so sánh này strA, string strB)
có bằng nhau không. Nếu bằng thông qua tên biến: nhau thì tả về 0, .CompareTo(string strB)
Nếu strA>strB thì trả về 1, ngược lại -1. String.concat(string
Nối 2 chuỗi strA và strB thành
Tương tự phép cộng chuỗi bằng toán strA, string strB) 1 chuỗi tử cộng. IndexOf(char Value)
Trả về 1 số nguyên kiểu int là
Nếu không tìm thấy thì phương thức
vị trí xuất hiên đầu tiên của ký tả về -1 tự value trong chuỗi Insert(int strartIndex,
Trả về một chuỗi mới trong đó string value)
bao gồm chuỗi cũ đã chèn
thêm chuỗi value tại vị trí strartIndex String.IsNullOrEmpty(
Kiểm tra chuỗi vakue có phải string value)
chuỗi null hay không. Nếu
đúng trả về true, ngược lại false. LastIndexOf(char
Trả về một số nguyên kiểu int value)
là vị trí xuất hiện cuối cùng
của ký tự value trong chuỗi Remove(int
Trả về một chuỗi mới đã gỡ strartIndex, int count)
count ký tự từ vị trí strartIndex trong chuỗi ban đầu Replace(char oldValue,
Trả về một chuỗi mới đã thay char newValue)
thế các ký tự oldValue bằng ký
tự newValue trong chuỗi ban đầu Split(char value)
Trả về một mảng các chuỗi
Phương thức này có thể truyền vào
được cắt ra từ chuỗi ban đầu
nhiếu ký tự khác nhau. Khi đó
tại những nơi có ký tự value
phương thức sẽ thực hiện cắt theo
từng ký tự đã truyền vào. Subtring(int
Trả về 1 chuỗi mới được cắt từ
Nếu gọi subtring mà chỉ truyền vào strartIndex, int length)
vị trí strarIndex với số ký tự
giá trị startIndex thì mặc định
cắt là length từ chuỗi ban đầu
phương thức sẽ cắt từ vị trí startIndex đến cuối chuỗi Lưu ý:
Các phương thức mà mình có ghi String phía trước là các phương thức gọi thông
qua tên lớp. Các phương thức còn lại được gọi thông qua đối tượng.
Các phương thức khi gọi sẽ tạo ra đối tượng mới rồi thao tác trên đối tượng đó chứ
không thao tác trực tiếp với đối tượng đang xét. Vì thế nếu như gọi. string a = "HowKteam"; a.Substring(3, 1);
Thì biến a sau khi thực hiện lệnh Subtring vẫn mang giá trị “HowKteam”.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 56
Nếu muốn biến a mang giá trị mới khi thực hiện Substring thì phải gán ngược lại giá
trị mới đó cho biến a: a = a.Subtring(3, 1);
Có thể xem 1 chuỗi là 1 mảng các ký tự. Như vậy hoàn toàn có thể truy xuất đến
từng ký tự như truy xuất đền phần tử của mảng.
Ở trên chỉ là các phương thức hay dùng ngoài ra còn rất nhiều phương thức.
Ứng dụng lớp string vào việc xử lý chuỗi
 Thực hiện chuẩn hóa 1 chuỗi họ tên của người dùng với các yêu cầu
Cắt bỏ hết các khoảng trắng dư ở đầu cuối chuỗi. Các từ cách nhau một khoảng
trắng nếu phát hiện có nhiều hơn 1 khoảng trắng thì thực hiện cắt bỏ.
Viết hoa chữ cái đầu tiên của mỗi từ, các chữ cái tiếp theo thì viết thường. Ý tưởng
Cắt khoảng trắng dư ở đầu và cuối chuỗi thì ta có thể sử dụng phương thức Trim.
Khoảng trắng ở giữa thì ta sẽ duyệt cả chuỗi nếu phát hiện có 2 khoảng trắng thì
thay thế nó bằng 1 một khoảng trắng. Để làm điều này ta có thể dùng:
- IndexOf để phát hiện khoảng trắng.
- Replace để thay thế 2 khoảng trắng thành 1 khoảng trắng.
Viết hoa chữ cái đầu và viết thường các chữ cái còn lại thì ta có thể cắt chuỗi họ tên
ra thành các từ và ứng với mỗi từ ta thực hiện như yêu cầu đề bài. Để làm điều này ta có thể sử dụng:
- Split để cắt ra các từ.
- Substring để cắt ra các chữ cái mong muốn.
- ToUpper để viết hoa và ToLower để viết thường. Code tham khảo
/* Khai báo 1 biến kiểu chuỗi tên là FullName
* Khai báo 1 biến Result chứa kết quả chuẩn hoá chuỗi.
* Giá trị biến FullName được nhập từ bàn phím. */ string FullName; string Result = "";
Console.Write(" Moi ban nhap ho va ten: "); FullName = Console.ReadLine();
/* Cắt các khoảng trắng dư ở đầu và cuối chuỗi */ FullName = FullName.Trim();
/* Trong khi còn tìm thấy 2 khoảng trắng
* thì thực hiện thay thế 2 khoảng trắng bằng 1 khoảng trắng*/
while (FullName.IndexOf(" ") != -1) {
FullName = FullName.Replace(" ", " "); }
/* Cắt chuỗi họ tên ra thành mảng các từ.
* Sau đó duyệt mảng để chuẩn hoá từng từ một.
* Khi duyệt mỗi từ ta thực hiện cắt ra chữ cái đầu trên và lưu trong biến FirstChar
* Cắt các chữ cái còn lại và lưu trong biến OtherChar.
* Thực hiện viết hoa chữ cái đầu và viết thường các chữ cái còn lại.
* Cuối cùng là lưu chữ vừa chuẩn hoá vào biến Result. */
string[] SubName = FullName.Split(' ');
for (int i = 0; i < SubName.Length; i++) {
string FirstChar = SubName[i].Substring(0, 1);
string OtherChar = SubName[i].Substring(1);
SubName[i] = FirstChar.ToUpper() + OtherChar.ToLower(); Result += SubName[i] + " "; }
Console.WriteLine(" Ho ten cua ban la: " + Result);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 57
Kết quả khi chạy chương trình:
Chương trình trên vẫn còn 1 lỗi nhỏ đó là nếu như không nhấn phím space để tạo
khoảng trắng mà nhấn phím Tab thì chương trình sẽ không ra kết quả như ý. Hãy thử
vận dụng kiến thức đã học để giải quyết xem sao! Lớp StringBuilder Đặc điểm
Lớp StringBuilder được .NET xây dựng sẵn giúp chúng ta thao tác trực tiếp với
chuỗi gốc và giúp tiết kiệm bộ nhớ hơn so với lớp String.
Đặc điểm của StringBuilder là: -
Cho phép thao tác trực tiếp trên chuỗi ban đầu. -
Có khả năng tự mở rộng vùng nhớ khi cần thiết. -
Không cho phép lớp khác kế thừa
Từ 2 đặc điểm này đã làm nổi bật lên ưu điểm của StringBuilder so với String đó là
ít tốn bộ nhớ. Cụ thể qua ví dụ sau: string Value = "How"; Value = Value + "Kteam";
Ở 2 dòng lệnh có thể thấy bộ nhớ sẽ được lưu trữ như sau: -
Đầu tiên tạo 1 vùng nhớ đối tượng kiểu string tên là Value. -
Tạo 1 vùng nhớ chứa giá trị “Kteam”. -
Khi thực hiện toán tử cộng trên 2 chuỗi sẽ tạo ra 1 vùng nhớ nữa để chứa
giá trị chuỗi mới sau khi cộng. -
Cuối cùng là phép gán sẽ thực hiện trỏ đối tượng Value sang vùng nhớ chứa
chuỗi kết quả của phép cộng.
Như vậy ta thấy sẽ có 1 vùng nhớ không sử dụng nhưng vẫn còn nằm trong bộ nhớ,
đó là vùng nhớ chứa giá trị “How” – giá trị ban đầu của biến Value.
Đối với StringBuilder thì khác:
StringBuilder MutableValue = new StringBuilder("How"); MutableValue.Append("Kteam");
Ở 2 câu lệnh trên bộ nhớ sẽ lưu trữ như sau:
- Tạo một vùng nhớ cho đối tượng MutableValue chứa giá trị “How”.
- Tạo một vùng nhớ chứa giá trị “Kteam”.
- Mở rộng vùng nhớ của MutableValue để nối chuỗi “Kteam” vào sau chuỗi “How”.
Rõ ràng là ta không tạo ra quá nhiều vùng nhớ và cũng không lãng phí bất cứ vùng nhớ nào. Sử dụng
Cách khởi tạo 1 đối tượng StringBuilder có đôi chút khác so với String. Cú pháp:
Khởi tạo một đối tượng rỗng:
StringBuilder ế > = new StringBuilder();
Khởi tạo một đối tượng chứa 1 chuỗi cho trước:
StringBuilder ế > = new StringBuilder(ỗ giá tr > ị );
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 58
Trong lớp StringBuilder có các phương thức như: Remove, Insert, Replace được sử
dụng hoàn toàn giống như lớp String.
Chỉ có vài phương thức mới các bạn cần chú ý: Tên phương thức Ý nghĩa Append(string Value)
Nỗi chuỗi Value vào sau chuỗi ban đầu Clear()
Xáo toàn bộ nội dung trong đối tượng (lưu ý không phải xóa vùng nhớ của đối tượng) ToString()
Chuyển đổi đối tượng kiểu StringBuider sang kiểu String Lưu ý:
Các bạn nhớ đây là đối tượng kiểu StringBuilder nên thao tác với chuỗi như gán,
nối chuỗi, . . . phải thông qua các phương thức chứ không thể thực hiện trực tiếp được.
Giữa String và StringBuilder đều có cái hay riêng của nó. Tuỳ vào từng yêu cầu của
bài toán mà nên sử dụng cho hợp lý, tránh lạm dụng quá nhiều 1 kiểu:
- Thông thường đối với các bài toán đòi hỏi thao tác nhiều với chuỗi gốc như cộng
chuỗi, chèn chuỗi, xoá bỏ một số ký tự, . . . thì nên sử dụng StringBuilder để tối ưu bộ nhớ.
- Còn lại thì nên sử dụng String để việc thao tác thuận tiện hơn. XXIII.
Struct trong C# cơ bản
Struct là một kiểu dữ liệu có cấu trúc, được kết hợp từ các kiểu dữ liệu nguyên thuỷ
do người lập trình định nghĩa để thuận tiện trong việc quản lý dữ liệu và lập trình. Xét bài toán sau:
Ta cần lưu trữ thông tin của 10 sinh viên với mỗi sinh viên gồm có các thông tin như - Mã số. - Họ tên. - Nơi sinh. - CMND.
Khi đó, để lưu thông tin của 1 sinh viên ta cần 4 biến chứa 4 thông tin trên. Nếu
muốn lưu thông tin 10 sinh viên thì cần 40 biến. Chắc không quá nhiều, nhưng nếu
muốn lưu thông tin của 1000, 10000 sinh viên thì sao?
Số lượng biến lúc này rất nhiều khiến cho code dài dòng khó thao tác, khó kiểm soát.
Từ đó mới đưa ra khái niệm kiểu dữ liệu có cấu trúc để giải quyết vấn đề trên.
Ý tưởng là đóng gói các thông tin đó vào 1 đối tượng duy nhất. Như vậy thay vì
phải khai báo 40 biến thì ta chỉ cần khai báo 1 mảng 10 phần tử mà mỗi phần tử có kiểu
dữ liệu ta đã định nghĩa. Đặc điểm của struct
Là một kiểu dữ liệu tham trị.
Dùng để đóng gói các trường dữ liệu khác nhau nhưng có liên quan đến nhau.
Bên trong struct ngoài các biến có kiểu dữ liệu cơ bản còn có các phương thức, các struct khác.
Muốn sử dụng phải khởi tạo cấp phát vùng nhớ cho đối tượng thông qua toán tử new.
Struct không được phép kế thừa.
Khai báo và sử dụng struct Khai báo Cú pháp: Trong đó: struct
là tên kiểu dữ liệu do mình {
tự đặt và tuân thủ theo quy tắc đặt tên. public ; là danh sách các }
biến thành phần được khai báo như khai báo biến bình thường.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 59
Từ khoá public là từ khoá chỉ định phạm vi truy. Trong ngữ cảnh hiện tại thì có thể
hiểu từ khoá này giúp cho người khác có thể truy xuất được để sử dụng. Ví dụ: struct SinhVien { public int MaSo; public string HoTen; public double DiemToan; public double DiemLy; public double DiemVan; }
Với khai báo này ta đã có 1 kiểu dữ liệu mới tên là SinhVien. Và có thể khai báo
biến, sử dụng nó như sử dụng các kiểu dữ liệu khác.
Nếu như kiểu int có thể chứa số nguyên, kiểu double có thể chứa số thực thì kiểu
SinhVien vừa khai báo có thể chứa 5 trường thông tin con là MaSo, HoTen, DiemToan, DiemLy, DiemVan.
Lưu ý: bên trong vẫn còn 2 khai báo chưa được nhắc đến đó là: -
Constructor (hàm khởi tạo). -
Các phương thức mà mình muốn cung cấp để hỗ trợ người dùng khi thao tác
với dữ liệu bên trong struct. Sử dụng:
Ta có thể truy xuất đến từng thành phần dữ liệu của struct thông qua toán tử “.”
Kèm theo tên thành phần muốn truy xuất.
Xét bài toán sau: Viết chương trình lưu trữ thông tin của sinh viên bao gồm: mã
số, họ tên, điểm toán, điểm lý, điểm văn. Thực hiện nhập thông tin cho 1 sinh viên và
tính điểm trung bình theo công thức (toán + lý + văn)/3.
Chương trình tham khảo: struct SinhVien { public int MaSo; public string HoTen; public double DiemToan; public double DiemLy; public double DiemVan; }
static void NhapThongTinSinhVien(out SinhVien SV) { Console.Write(" Ma so: ");
SV.MaSo = int.Parse(Console.ReadLine()); Console.Write(" Ho ten: "); SV.HoTen = Console.ReadLine(); Console.Write(" Diem toan: ");
SV.DiemToan = Double.Parse(Console.ReadLine()); Console.Write(" Diem ly: ");
SV.DiemLy = Double.Parse(Console.ReadLine()); Console.Write(" Diem van: ");
SV.DiemVan = Double.Parse(Console.ReadLine()); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 60
static void XuatThongTinSinhVien(SinhVien SV) {
Console.WriteLine(" Ma so: " + SV.MaSo);
Console.WriteLine(" Ho ten: " + SV.HoTen);
Console.WriteLine(" Diem toan: " + SV.DiemToan);
Console.WriteLine(" Diem ly: " + SV.DiemLy);
Console.WriteLine(" Diem van: " + SV.DiemVan); }
static double DiemTBSinhVien(SinhVien SV) {
return (SV.DiemToan + SV.DiemLy + SV.DiemVan) / 3; }
static void Main(string[] args) {
/** Khai báo 1 kiểu dữ liệu SinhVien với các trường thông tin như đề bài.
* Khai báo và khởi tạo 1 đối tượng SV1 kiểu SinhVien.*/ SinhVien SV1 = new SinhVien();
Console.WriteLine(" Nhap thong tin sinh vien: ");
/** Đây là hàm hỗ trợ nhập thông tin sinh viên.
* Sử dụng từ khoá out để có thể cập nhật giá trị nhập được ra biến SV1 bên ngoài
* khi kết thúc gọi hàm (có thể xem lại bài Hàm trong C#).*/ NhapThongTinSinhVien(out SV1);
Console.WriteLine("*********");
Console.WriteLine(" Thong tin sinh vien vua nhap la: "); XuatThongTinSinhVien(SV1);
Console.WriteLine(" Diem TB cua sinh vien la: " + DiemTBSinhVien(SV1)); Console.ReadLine(); } Kết quả chương trình:
Trong chương trình trên ta có thể thấy: 
Kiểu dữ liệu SinhVien có thể dùng làm kiểu dữ liệu cho biến, parametter
cho phương thức. Ngoài ra còn có thể làm kiểu trả về cho phương thức. 
Các thành phần dữ liệu bên trong được truy xuất thông qua dấu “.” 
Vì struct là kiểu tham trị nên khi truyền vào các phương thức thì giá trị của
nó sau khi kết thúc phương thức sẽ không thay đổi. Do đó cần sử dụng từ
khoá out để có thể cập nhật giá trị thay đổi khi kết thúc phương thức.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 61 XXIV. Enum trong C#
Enum là từ khoá dùng để khai báo một kiểu liệt kê (Enumeration). Kiểu liệt kê là
một tập hợp các hằng số do người dùng tự định nghĩa.
Nói cách khác, enum là cách mà C# hỗ trợ người dùng gom nhóm các hằng số lại
với nhau và có chung một tên gọi (thường các hằng số này sẽ có liên quan với nhau ví
dụ như các trạng thái của 1 sự vật, các tính chất của 1 sự vật, . . .) Đặc điểm của enum:
Là một kiểu dữ liệu tham trị
Enum không được phép kế thừa
Khai báo sử dụng Enum Cú pháp: Trong đó: enum
là tên kiểu liệt kê do mình {
tự đặt và tuân thủ theo quy tắc đặt tên. là }
danh sách các biểu tượng hằng thành phần
mỗi biểu tượng hằng cách nhau bằng dấu “ , ”. Ví dụ: enum Color
Với khai báo này ta đã có 1 kiểu liệt kê tên là Color. {
Về bản chất, các biểu tượng hằng RED, BLUE, YELLOW này RED,
đại diện cho các số nguyên lần lượt là 0, 1, 2. BLUE, YELLOW }
Như vậy, nếu như chúng ta sử dụng cách khai báo hằng bình thường thì ta có thể khai báo như sau: public const int RED = 0; public const int BLUE = 1; public const int YELLOW = 2; Lưu ý:
Ta hoàn toàn có thể quy định giá trị cho từng biểu tượng enum Color
hằng bằng cách trực tiếp khi khai báo. Ví dụ: {
Khi đó các biểu tượng hằng RED, BLUE, YELLOW sẽ đại RED = 2,
diện cho các số nguyên lần lượt là 2, 4, 6 BLUE = 4,
Nếu ta không quy định giá trị cho các biểu tượng hằng thì YELLOW = 6,
giá trị của biểu tượng hằng đầu tiên sẽ mặc định là 0 và tăng }
dần cho các biểu tượng hằng tiếp theo. Sử dụng
Ta có thể truy xuất đến từng biểu tượng hằng của enum thông qua toán tử “.” Kèm
theo tên biểu tượng hằng muốn truy xuất. Ví dụ: Corlor.RED; Lưu ý:
Mặc dù bản chất các biểu tượng hằng là đại diện cho các số nguyên nhưng bạn
không thể so sánh trực tiếp chúng với các số nguyên được mà phải ép kiểu. Ví dụ:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 62 enum Color { RED, BLUE, YELLOW }
int Choose = int.Parse(Console.ReadLine());
if (Choose == Color.RED)// lỗi vì không thể so sánh trực tiếp 1 enum với 1 số nguyên {
Console.WriteLine("Ban vua chon mau do"); }
Để chương trình không báo lỗi ta có thể ép kiểu biểu tượng hằng RED về kiểu int. Choose == (int)Color.RED
Chúng ta cũng có thể ép kiểu ngược lại từ số nguyên sang kiểu liệt kê. Ví dụ:
Color Background = (Color)2; // Background s ẽ có giá tr ị là Color.YELLOW
Khi khai báo 1 biến nào đó, các lập trình viên thường cố gắng xây dựng 1 tập các giá
trị của biến đó (nếu có thể) và gom nhóm chúng bằng enum. Điều này rất thường gặp
trong các bộ thư viện của C# và là sự khác biệt giữa C# và Java. Sự khác biệt này có
ảnh hưởng gì đến việc lập trình? Câu hỏi này sẽ được trả lời ngày sau đây.
Sau khi xem qua cách khai báo và sử dụng enum ta có thể thấy rằng enum có những ưu điểm sau đây:
Chính vì được sử dụng với mục đích gom nhóm các hằng có liên quan với nhau
thành 1 tên duy nhất nên khi sử dụng bạn không cần phải nhớ chính xác tên hằng mà
chỉ cần nhớ tên enum chứa nó là đủ việc còn lại đã có visual studio hỗ trợ.
Chỉ cần gõ tên enum và dấu “.” Visual studio đã liệt kê
sẵn danh sách các biểu tượng hằng bên trong nó. Điều
này giúp cho việc lập trình dễ dàng hơn nhiều.
Hơn thế nữa visual studio còn hỗ trợ giúp bạn tìm ra
tên enum phù hợp với biến đang cần gán giá trị. XXV.
Regular Expression trong C#
Một số lớp hỗ trợ regular expression
Khi áp dụng một biểu thức quy tắc lên một chuỗi mẫu nào đó thì kết quả trả về có
thể là nhiều chuỗi con thoả mãn biểu thức quy tắc trên. Khi đó các chuỗi con sẽ được
lưu vào trong 1 tập hợp có tên là MatchCollection, mỗi phần tử trong tập hợp là 1 biến có kiểu Match.
MatchCollection là 1 kiểu tập hợp chứa danh sách các đối tượng kiểu Match. Vì đây
cũng là 1 tập hợp bình thường nên có thể thao tác như các tập hợp khác.
Ví dụ sử dụng Match và MatchCollection:
Cho chuỗi gốc là “-howkteam.com 10092016-”. Giả sử bạn muốn lấy ra tất cả các
số trong chuỗi. Vậy pattern của chúng ta đơn giản chỉ là “\d”.
Nếu bạn chỉ viết code đơn gian thế này: Regex reg = new Regex(@"\d");
Match result = reg.Match("-howkteam.com 10092016-");  Kết quả là 1
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 63
Một đối tượng kiểu Match sẽ chứa 1 chuỗi con kết quả, để xem chuỗi con kết quả
này ta sẽ gọi phương thức ToString(). Ngoài ra nó cũng có các thuộc tính và phương thức khác như:
Chúng ta mong muốn lấy ra tất cả các số chứ không phải 1 số thế này! Regex reg = new Regex(@"\d");
// Tạo 1 đối tượng Regex chứa pattern của mình
Match result = reg.Match("-howkteam.com 10092016-");
// Tạo 1 đối tượng Match để chứa kết quả. do {
Console.WriteLine(result.ToString()); result = result.NextMatch();
// Chuyển qua kết quả trùng khớp kế tiếp }
while (result != Match.Empty);// Kiểm tra xem đã hết kết quả trùng khớp chưa
Kết quả có vẻ như ý rồi nhưng ta có thể thấy code không mấy gọn gàng cũng như
mất thời gian xử lý ở chỗ hàm NextMatch().
Đến đây ta thử sử dụng MatchCollection đã được .NET hỗ trợ sẵn xem thế nào:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 64 Regex reg = new Regex(@"\d");
foreach (Match item in reg.Matches("-howkteam.com 10092016-")) {
Console.WriteLine(item.ToString()); }
Để ý là lúc này ta dùng hàm Matches()
để lấy kết quả chứ không phải Match().
Matches() trả về 1 tập hợp các đối tượng
Match (MatchCollection) nên ta có thể
dùng foreach để duyệt tập hợp
Kết quả khi chạy vẫn giống như cách trên: Lưu ý:
Nếu như không tìm thấy chuỗi con
phù hợp thì sẽ trả về 1 đối tượng Match có giá trị rỗng chứ không phải null. Khi đó if (result == Match.Empty) { // Câu l n ệ h }
để kiểm tra có giá trị hay không ta sẽ sử dụng:
Group và GroupCollection
Group trong Regular Expression là chỉ cách ta gom nhóm các biểu thức lại thành
cụm và có thể đặt tên cho nhóm để dễ quản lý và thao tác.
Lớp Group là 1 lớp đại diện cho 1 gom nhóm trong biểu thức. Có 1 điểm chúng ta
nên biết là lớp Group là lớp cha của lớp Match!
Tại sao lại có khái niệm này? – Bởi vì:
Trong 1 kết quả trùng khớp sẽ có thể chứa nhiều thông tin khác nhau và ta mong
muốn các thể lấy ra từng thành phần nhỏ trong đó mà không phải dùng thêm 1 biểu thức chính quy nào nữa.
Và trong biểu thức chính quy ban đầu ta sẽ gom nhóm các thành phần con thành
các group và đặt tên cho chúng như vậy khi lấy được 1 chuỗi kết quả ta muốn lấy các
thành phần con bên trong đó thì ta chỉ cần gọi chúng thông qua tên đã đặt. Cú pháp:
(?<tên group>) Trong đó:
( ): là cú pháp gom nhóm các biểu thức
?: là cú pháp đặt tên cho group. Bạn có thể không đặt tên cho group
cũng được. Lưu ý là tên group bạn phải viết liền không dấu và nên tuân theo quy tắc đặt tên. Ví dụ:
Để lấy ra chuỗi giờ phút giây ta có thể làm như sau: “\d+:\d+:\d+”
Nhưng nếu bây giờ mình muốn lấy ra giờ, phút, giây riêng để xử lý thì phải làm sao?
Nếu viết 3 biểu thức thì khá dài dòng. Khi đó ta sử dụng group và đặt tên cho chúng như sau: (?\d+):(?\d+):(?\d+)
Trong đó “hours”, “minutes”, “seconds” là tên do mình đặt cho 3 group.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 65
Để lấy ra danh sách các gom nhóm trong 1 chuỗi con kết quả ta dùng thuộc tính
Groups” trong lớp Match. Thuộc tính này trả về 1 GroupCollection. GroupCollection
là 1 lớp chứa danh sách các gom nhóm trong biểu thức, mỗi phần tử của danh sách là 1 đối tượng kiểu Group.
Ví dụ sử dụng Group và GroupCollection: // Tạo 1 biểu thức
Regex re = new Regex(@"(?\d+):(?\d+):(? \d+)");
/* Duyệt qua các kết quả trùng khớp
* Lấy ra giá trị các group thông qua chỉ số phần tử là tên các group đã đặt trong biểu thức*/
foreach (Match item in re.Matches("30/04/2017 10:15:12 192.168.1.2")) {
Console.WriteLine(" Match: " + item.ToString());
Console.WriteLine(" Hours: " + item.Groups["hours"]);
Console.WriteLine(" Minutes: " + item.Groups["minutes"]);
Console.WriteLine(" Seconds: " + item.Groups["seconds"]); }
Kết quả khi chạy chương trình trên:
.NET đã hỗ trợ chúng ta truy xuất
các phần tử trong danh sách
GroupCollection thông qua chỉ số phần
tử là tên các group chúng ta đã đặt trong biểu thức ở trên.
Capture và CaptureCollection
Mỗi khi tìm thấy bất kỳ 1 chuỗi con nào (bao gồm cả các group) thì C# sẽ bắt nó lại
và lưu vào 1 đối tượng có kiểu Capture. Và danh sách tất cả các Capture chính là 1 CaptureCollection.
Một điểm cần biết nữa là Capture là lớp cha của lớp Group!
Tại sao lại có lớp Capture này? – Câu trả lời sẽ nằm trong tình huống sau:
Cho chuỗi sau “10:30:15 IBM 192.168.1.2 INTEL” hãy viết biểu thức lấy ra giờ
phút giây, địa chỉ ip và tên công ty.
Lúc này ta sẽ có biểu thức sau:
“(?(\d|:)+)\s(?\S+)\s(?(\d|\.)+)\s(?\S+)”
Lưu ý: là mình sẽ không tập trung vào giải thích chi tiết biểu thức mà tập trung vào cách sử dụng các lớp.
Ở đây mình có 2 tên công ty bên trong nên mình đặt chung 1 tên group là company
với mong muốn lấy ra 2 tên công ty từ group. Chương trình kiểm tra:
Regex RE = new Regex(@"(?(\d|:)+)\s" + @"(?\S+)\s"
+ @"(?(\d|\.)+)\s" + @"(?\S+)");
foreach (Match item in RE.Matches("10:30:15 IBM 192.168.1.2 INTEL")) {
Console.WriteLine(" time: " + item.Groups["times"]);
Console.WriteLine(" company: " + item.Groups["company"]);
Console.WriteLine(" ip: " + item.Groups["ip"]);
Console.WriteLine(" company: " + item.Groups["company"]); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 66
Nhưng khi chạy chương trình kết quả lại như thế này:
Ta thấy có tới 2 công ty thoả mãn là INTEL và IBM nhưng chương trình chỉ in ra
được INTEL. Chúng ta có thể kiểm tra bằng phần mềm RegEx Tester để chắc rằng biểu thức mình viết đúng:
Các bạn hãy để ý chỗ Group 4 (company) trong hình. Rõ ràng ta lấy ra được 2 giá
trị nhưng chỉ hiển thị được giá trị sau cùng.
Đến đây có bạn sẽ nói rằng tại sao không đổi tên group khác đi là xong? Câu trả lời
là trong ví dụ trên mình chỉ có 2 công ty nhưng giả sử có đến 100 công ty trong chuỗi
thì sao? Bạn phải đặt 100 biến khác nhau?
Vì thế chúng ta sẽ tận dụng đặc điểm của Capture và sử dụng chúng để giải quyết. Chương trình sẽ như sau:
Regex RE = new Regex(@"(?(\d|:)+)\s" + @"(?\S+)\s" + @"(?(\d|\.)+)\s" + @"(?\S+)");
foreach (Match item in RE.Matches("10:30:15 IBM 192.168.1.2 INTEL")) {
Console.WriteLine(" time: " + item.Groups["times"]);
Console.WriteLine(" ip: " + item.Groups["ip"]); Console.Write(" company: ");
/*Lấy ra tất cả các capture bắt được trong group company và duyệt lần lượt chúng
* Sau đó ta có thể sử dụng hàm ToString() hoặc thuộc tính Value để lấy giá trị của Capture */
foreach (Capture i in item.Groups["company"].Captures) {
Console.Write(i.ToString() + " "); } }
Ở đây để lấy ra danh sách các Capture (CaptureCollection) ta sử dụng thuộc tính Captures.
Kết quả khi chạy đoạn code trên:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 67
Bảng các ký hiệu Regular Expression thông dụng:
Regular Expression hay tiếng Việt được gọi là Biểu thức chính quy, là một cấu trúc
rất mạnh để mô tả một chuỗi theo cách thống nhất chung.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 68
Regular Expression bao gồm tập hợp các ký tự, toán tử hay ký hiệu toán học nhằm
biểu thị một chuỗi theo cấu trúc chung mà mọi người học theo. Có thể xem Regular
Expression như một loại tiếng lóng dùng chung trong lập trình.
Bạn có thể trích lọc một hay nhiều chuỗi có cấu trúc chung từ một đoạn văn bản hay một chuỗi ra.
Bạn có thể tìm kiếm, thay đổi nội dung của chuỗi một cách dễ dàng. Thay vì phải
ngồi cắt chuỗi mỏi mệt như trước đây.
Từ đây, với Regular Expression. Bạn hoàn toàn có thể trích lọc dữ liệu từ các đoạn html theo ý.
Các ký hiệu của Regular Expression
Trước khi tìm hiểu về các ký hiệu chúng ta cùng xem qua một ví dụ mẫu để nắm
được cấu trúc chung của Regular Expression Chuỗi mẫu:
{8} là biểu thị ký tự trước đó xuất hiện 8 -howkteam.com-10092016- lần
Vậy có thể đọc câu pattern này là: Tìm ra Pattern:
chuỗi con là dãy 8 số liên tiếp nhau. \d{8}
>>> kết quả là trùng khớp. Kết quả tìm kiếm:
Nếu bạn thay số 8 thành số 6 tức là 10092016 pattern thành : \d{6} Một cách nhìn khác: -howkteam.com-10092016-
Thì kết quả sẽ thành 100920 hay -howkteam.com-10092016- Hay một cách dễ nhớ -howkteam.com-\d{8}- Hay một cách dễ nhớ -howkteam.com-\d{6}16- Trong đó
\d là ký hiệu biểu thị cho số
Hay có thể mường tượng Regular
Expression này như string.Format
Từ đây bạn có thể nhận thấy. Regular Expression bản chất là tìm ra một hoặc nhiều
chuỗi con thỏa mãn cấu trúc chung được định ra. (bảng kí hiệu) Pattern .
Đại diện cho một ký tự bất kỳ. Ngoại trừ ký hiệu \n Chuỗi mẫu: -howkteam.com-10092016- Pattern: . Kết quả: Dsach các ký t ự xu t ấ hi n ệ trong chu i ỗ m u ẫ :{,h,o,w,k,...,1,6,-} Cách nhìn khác:
Lấy ra từng từ bên trong chuỗi mẫu.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 69 Hình minh họa: Pattern \d
Đại diện cho ký tự số. Tương đương pattern [0-9]. Chuỗi mẫu: -howkteam.com-10092016- Pattern: \d Kết quả:
Danh sách các số xuất hiện trong chuỗi mẫu: {1,0,0,9,2,0,1,6} Cách nhìn khác:
Lấy ra từng số bên trong chuỗi mẫu. Hình minh họa:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 70 Pattern \D
Ký tự không phải số. Hay có thể hiểu là phủ định của \d Chuỗi mẫu: -howkteam.com-10092016- Pattern: \D Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 71 Pattern \s
Ký tự khoảng trắng. Tương đương [\f\n\r\t\v] Chuỗi mẫu: -howkteam.com 10092016- Pattern: com\s1009 Kết quả: Pattern \S
Ký tự không phải khoảng trắng. Tương đương phủ định của \s hay tương đương [^\f\n\r\t\v] Chuỗi mẫu: -howkteam.com 10092016- Pattern: \S Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 72 Pattern \w
Ký tự word (gồm chữ cái và chữ số, dấu gạch dưới _ ) tương đương [a-zA-Z_0-9] Chuỗi mẫu: -howkteam.com 10092016- Pattern: \w Kết quả: Pattern \W
Ký tự không phải word. Tương đương phủ định của \w hay [^a-zA-Z_0-9] Chuỗi mẫu: -howkteam.com 10092016- Pattern: \W Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 73 Pattern ^
Bắt đầu một chuỗi hay một dòng Chuỗi mẫu: -howkteam.com 10092016- Pattern: ^-howkteam Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 74 Pattern $
Kết thúc một chuỗi hay một dòng Chuỗi mẫu: -howkteam.com 10092016- Pattern: 16-$ Kết quả: Pattern \A Bắt đầu môt chuỗi Chuỗi mẫu: -howkteam.com 10092016- Pattern: \A-howkteam Kết quả: Pattern \z Kết thúc một chuỗi Chuỗi mẫu:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 75 -howkteam.com 10092016- Pattern: 16-\z Kết quả: Pattern |
Ký tự so trùng tương đương với or. Dùng để kết hợp nhiều điều kiện. Chuỗi mẫu: -howkteam.com 10092016- Pattern: k|h|9 Kết quả: Pattern [abc]
Khớp với một ký tự nằm trong nhóm này là a hay b hay c đều được. Chuỗi mẫu: -howkteam.com 10092016-
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 76 Pattern: [k9h] Kết quả: Pattern [a-z]
Khớp với ký tự trong khoảng a đến z. az là ký tự hiện hữu trong bảng ASCII. Chuỗi mẫu: -howkteam.com 10092016- Pattern: [1-8] Kết quả: Pattern [^abc]
Không trùng bất kỳ ký tự a b hay c. Tương đương phủ định của [abc] Chuỗi mẫu: -howkteam.com 10092016- Pattern: [^1-8]
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 77 Kết quả: Pattern ()
Xác định một group. Một biểu thức đơn lẻ trong pattern. Chuỗi mẫu: -howkteam.com 10092016- Pattern: ([0-3])|([5-7])
Lấy ra tất cả các số trong khoảng [0-3] hoặc trong khoảng [5-7] Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 78 Pattern ?
Khớp với từ đứng trước xuất hiện 0 hay 1 lần. Chuỗi mẫu: -howkteam.com 10092016-
Lấy ra tất cả số 1 xuất hiện 0 hay 1 lần Pattern: 1? Kết quả: Pattern *
Khớp với từ đứng trước 0 lần trở lên. Chuỗi mẫu: -howkteam.com 10092016- Pattern: 1* Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 79 Pattern +
Khớp với từ đứng trước 1 lần trở lên. Chuỗi mẫu: -howkteam.com 10092016- Pattern: 1+ Kết quả: Pattern {n}
Với n là số. Khớp với từ đứng trước xuất hiện đúng n lần. Chuỗi mẫu: -howkteam.com 10092016- Pattern: 0{2}
Lấy ra các kết quả là một cặp số 0 liền nhau.
Hãy thử với n là một giá trị số khác nhé. Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 80 Pattern {n,}
Với n là số. Khớp với từ đứng trước xuất hiện đúng n lần trở lên. Chuỗi mẫu: -howkteam.com 10092016- Pattern: 0{1,} Kết quả: Pattern {m,n}
Với m và n là số. Khớp với từ đứng trước xuất hiện từ m đến n lần. Chuỗi mẫu: -howkteam.com 100010092016- Pattern: 0{1,2} Kết quả:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 81
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 82 Phần nâng cao Collections
Các lớp hỗ trợ lưu trữ, quản lý và thao tác với các đối tượng một cách có thứ tự.
Các lớp này nằm trong namespace System.Collections.
Một số đặc điểm của Collections
Là một mảng có kích thước động: 
Không cần khai báo kích thước khi khởi tạo. 
Có thể tăng giảm số lượng phần tử trong mảng một cách linh hoạt.
Có thể lưu trữ một tập hợp đối tượng thuộc nhiều kiểu khác nhau.
Hỗ trợ rất nhiều phương thức để thao tác với tập hợp như: tìm kiếm, sắp xếp, đảo ngược…
Mỗi collections được tổ chức thành một lớp nên cần khởi tạo đối tượng trước khi sử dụng.
Khi nào sử dụng Collection?
Chúng ta đã từng tìm hiểu một kiểu dữ liệu dùng để quản lý danh sách đối tượng đó là
kiểu mảng. Vậy Collections có gì hay hơn mảng? Khi nào dùng mảng và khi nào dùng Collections?
Đầu tiên là những điểm mạnh của Collections
Bên trong Collections có nhiều lớp đa dạng hỗ trợ cho từng mục đích khác nhau.
Nếu như mảng chỉ có thể truy xuất phần tử thông qua chỉ số thì các Collections có thế
truy xuất thông qua chỉ số hoặc thông qua key. 
Đối với danh sách cần thao tác tìm kiếm nhiều thì Collections cũng có lớp hỗ trợ
giúp việc tìm kiếm nhanh hơn nhiều so với mảng nguyên thuỷ. 
Trong trường hợp danh sách cần thay đổi số lượng phần tử liên tục (thêm hoặc
xoá phần tử) thì Collections cũng hỗ trợ sẵn. 
Ngoài ra trong namespace System.Collections còn hỗ trợ sẵn 2 cấu trúc dữ liệu kinh điển đó là S TACK và Q
UEUE nên chỉ cần lấy ra sử dụng mà không cần cài đặt lại.
Vậy khi nào sử dụng mảng khi nào sử dụng Collections?
Theo lời khuyên từ Microsoft thì: 
Mảng thường được dùng để làm việc với một số lượng cố định các đối
tượng strongly-typed (có thể hiểu đơn giản, strongly-typed là kiểu dữ liệu không bị
thay đổi một cách đột ngột, tường minh). 
Collections cung cấp một cách linh hoạt hơn để làm việc với danh sách. Ta có thể
tăng giảm số lượng phần tử một cách tự động. Một số Collections còn hỗ trợ lưu trữ
danh sách dưới dạng Key – Value giúp truy xuất, tìm kiếm một cách nhanh chóng.
Một số Collections thông dụng
Một số lớp Collections được sử dụng phổ biến:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 83 LỚP MÔ TẢ ArrayList
Lớp cho phép lưu trữ và quản lý các phần tử giống mảng.
Tuy nhiên, không giống như trong mảng, ta có thể thêm hoặc xoá phần tử một cách linh hoạt
và có thể tự điều chỉnh kích cỡ một cách tự động. HashTable
Lớp lưu trữ dữ liệu dưới dạng cặp Key – Value.
Khi đó ta sẽ truy xuất các phần tử trong danh sách này thông qua Key
(thay vì thông qua chỉ số phần tử như mảng bình thường). SortedList Là sự kêt hợp của A
rrayList và HashTable. Tức là dữ liệu sẽ lưu dưới dạng Key – Value.
Ta có thể truy xuất các phần tử trong danh sách thông qua Key hoặc
thông qua chỉ số phần tử.
Đặc biệt là các phần tử trong danh sách này luôn được sắp xếp theo giá trị của Key. Stack
Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc LIFO (Last In First Out). Queue
Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc FIFO (First In First Out). BitArray
Lớp cho phép lưu trữ và quản lý một danh sách các bit.
Giống mảng các phần tử kiểu bool với true biểu thị cho
bit 1 và false biểu thị cho bit 0.
Ngoài ra BitArray còn hỗ trợ một số phương thức cho việc tính toán trên bit.
Arraylist trong C# là một collection giúp lưu trưc quản lý một danh sách các đối tượng theo
kiểu mảng (truy cập các phần tử bên trong thông qua chỉ số index)
Rất giống mảng các object nhưng có thể thêm hoặc xóa các phần tử một cách linh hoạt
và có thể tự điều chỉnh kích cỡ một cách tự động.
Để sử dụng các Collection trong .NET ta cần thêm thư viện System.Collection bằng câu
lệnh: using System.Collection;
Vì đây là ArrayList là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new: // kh i ở t o ạ ArrayList r n ỗ g
ArrayList MyArray = new ArrayList();
Ngoài ra, bạn cũng có thể khởi tạo 1 ArrayList chứa các phần tử được sao chép từ 1 Collection khác: /* kh i ở t o
ạ 1 ArrayList có kích thư c ớ b n ằ g v i ớ MyArray2. * sao chép toàn b ộ ph n ầ t
ử trong MyArray2 vào MyArray3.*/
Arraylist MyArray3 = new ArrayList(MyArray2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong ArrayList TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong ArrayList. Capacity
Trả về 1 số nguyên cho biết số phần tử mà ArrayList có thể chứa (sức chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự động tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho ArrayList.
Một số phương thức thông dụng trong ArrayList: TÊN PHƯƠNG THỨC Ý NGHĨA Add(object Value)
Thêm đối tượng Value vào cuối ArrayList.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 84 AddRange
Thêm danh sách phần tử ListObject vào cuối ArrayList. (ICollection ListObject) BinarySearch
Tìm kiếm đối tượng Value trong ArrayList theo thuật toán tìm kiếm (object Value) nhị phân.
Nếu tìm thấy sẽ trả về vị trí của phần tử ngược lại trả về giá trị âm.
Lưu ý: là ArrayList phải được sắp xếp trước khi sử dụng hàm. Clear()
Xoá tất cả các phần tử trong ArrayList. Clone()
Tạo 1 bản sao từ ArrayList hiện tại. Contains(object Value)
Kiểm tra đối tượng Value có tồn tại trong ArrayList hay không. GetRange
Trả về 1 ArrayList bao gồm các phần tử từ vị trí StartIndex đến (int StartIndex,
EndIndex trong ArrayList ban đầu. int EndIndex) IndexOf(object Value)
Trả về vị trí đầu tiên xuất hiện đối tượng Value trong ArrayList.
Nếu không tìm thấy sẽ trả về -1. Insert(int Index,
Chèn đối tượng Value vào vị trí Index trong ArrayList. object Value) InsertRange
Chèn danh sách phần tử ListObject vào vị (int Index, ICollection List
trí Index trong ArrayList. Object)
LastIndexOf(object Value) Trả về vị trí xuất hiện cuối cùng của đối tượng Value trong
ArrayList . Nếu không sẽ trả về -1. Remove(object Value)
Xoá đối tượng Value xuất hiện đầu tiên trong ArrayList. Reverse()
Đảo ngược tất cả phần tử trong ArrayList. Sort()
Sắp xếp các phần tử trong ArrayList theo thứ tự tăng dần. ToArray()
Trả về 1 mảng các object chứa các phần tử được sao chép từ ArrayList.
Những phương thức trên này cũng khá đơn giản nên sẽ không tập trung vào chúng.
Thay vào đó là hướng dẫn những thứ hay hơn.
Phương thức Sort() sẽ thực hiện sắp xếp danh sách theo thứ tự tăng dần. Vậy nếu danh
sách gồm các đối tượng mà mỗi đối tượng là 1 lớp có nhiều thuộc tính thì hàm Sort này
biết sắp xếp tăng dần theo thuộc tính nào?
Để trả lời câu hỏi này ta cần biết thêm là còn 1 hàm Sort nữa được hỗ trợ sẵn
trong ArrayList có cú pháp như sau:
Sort ( IComparer comparer) Công dụng:
Hàm này cho phép người dùng tự định nghĩa cách sắp xếp theo ý mình. 
Tham số truyền vào là 1 lớp có kế thừa từ interface IComparer . 
Interface IComparer chứa 1 phương thức duy nhất là:
int Comparer (object x, object y).
Phương thức này sẽ trả về 3 giá trị:  Bé hơn 0 nếu x < y.  Lớn hơn 0 nếu x > y.  Bằng 0 nếu x = y.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 85
Từ những quy định về inferface này ,chỉ cần khai báo 1 lớp kế thừa
interface IComparer và định nghĩa nội dụng cho phương thức Comparer có giá trị trả về
theo những quy định trên.
Ví dụ: đầu tiên, có 1 lớp person đại diện cho 1 con người gồm 2 thông tin Name và Age public class Person /// /// {
/// Override phương thức ToString để
/// Tạo 1 constructor có tham số để private string name;
khi cần có thể in thông tin của object
tiện cho việc khởi tạo nhanh đối
public class SortPersons : IComparer private int age; ra cho nhanh.
tượng Person với các giá trị cho sẵn. { public string Name /// ///
Tiếp theo định nghĩa 1 lớp kế thừa interface ICompare và override lại phương thức
Compare trong interface này. Hàm Compare sẽ so sánh dựa trên thuộc tính tuổi tăng dần.
Cuối cùng là chương trình chính thử tạo 1 ArrayList và thêm 1 vài đối t A ư r ợ r ng ay L P i e s r t s on ar r và Pe o r s s a o u đó gọi ns = new hà A m rr S ayor Li t() st và () xe ;// m Tạ kết quả o 1 danh . sách kiểu ArrayList rỗng
// Thêm 3 Person vào danh sách arr K P ế e tr quả
sons.Add(new Person("Nguyen Van A", 18));
arrPersons.Add(new Person("Nguyen Van B", 25));
arrPersons.Add(new Person("Nguyen Van C", 20));
Console.WriteLine("Danh sach Person ban dau: ");// In danh sách Person ban đầu ra.
foreach (Person item in arrPersons) {
Console.WriteLine(item.ToString()); }
/* Thực hiện sắp xếp danh sách Person theo tiêu chí đã được định nghĩa
* trong phương thức Compare của lớp SortPerson (tuổi tăng dần). */
arrPersons.Sort(new SortPersons());
// In danh sách Person đã được sắp xếp ra màn hình. Console.WriteLine(); Con H s as ol h e.ta W b ri le t t e rong C Line(" #:
Danh sach Person da duoc sap xep theo tuoi tang dan: ");
foreach (Person item in arrPersons) {
Console.WriteLine(item.ToString()); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 86
Là một Collections lưu trữ dữ liệu dưới dạng cặp Key - Value. 
Key đại diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá
trị tương ứng của khoá đó. 
Sẽ dử dụng Key để truy cập đến Value tương ứng.
Key Value đều là kiểu object nên ta có thể lưu trữ được mọi kiểu dữ liệu từ những
kiểu cơ sở đến kiểu phức tạp (class).
Nếu các Key của Hashtable là các số nguyên tăng dần từ 0 thì Hashtable nhìn trông giống A
rrayList (chỉ là giống về bề ngoài thôi chứ bên trong rất khác nhau).
 Hashtable là 1 Collections nên để sử dụng ta cần thêm thư
viện System.Collections bằng câu lệnh: using System.Collections;
Vì Hashtable là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new: // kh i ở t o ạ 1 Hashtable r n ỗ g
Hashtable MyHash = new Hashtable();
Cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
// khởi tạo 1 Hashtable và chỉ định Capacity ban đầu là 5
Hashtable MyHash2 = new Hashtable(5);
Ngoài ra, cũng có thể khở tạo 1 Hashtable chứa các phần tử được sao chép từ 1 Hashtable khác:
/* Khởi tạo 1 Hashtable có kích thước bằng với MyHash2.
* Sao chép toàn độ phần tử trong MyHash2 vào MyHash3. */
Hashtable MyHash3 = new Hashtable(MyHash2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong Hashtabe:
Một số thuộc tính thông dụng trong Hashtable:
TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong Hashtable. Keys
Trả về 1 danh sách chứa các Key trong Hashtable. Values
Trả về 1 danh sách chứa các Value trong Hashtable.
Một số phương thức thông dụng trong Hashtable: TÊN PHƯƠNG THỨC Ý NGHĨA Add(object Key, object Value)
Thêm 1 cặp Key - Value vào Hashtable. Clear()
Xoá tất cả các phần tử trong Hashtable. Clone()
Tạo 1 bản sao từ Hashtable hiện tại. ContainsKey(object Key)
Kiểm tra đối tượng Key có tồn tại trong Hashtable hay không. ContainsValue(object Value)
Kiểm tra đối tượng Value có tồn tại trong Hashtable hay không.
CopyTo(Array array, int Index) Thực hiện sao chép tất cả phần tử trong Hashtable sang
mảng một chiều array từ vị trí Index của array.
Lưu ý: array phải là mảng các object hoặc mảng các DictionaryEntry. Remove(object Key)
Xoá đối tượng có Key xuất hiện đầu tiên
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 87 trong Hashtable.
Một số lưu ý về Hashtable
Mỗi một phần tử trong Hashtable (bao gồm 1 cặp Key - Value) được C# định nghĩa là 1
đối tượng có kiểu DictionaryEntry. Trong DictionaryEntry có 2 thuộc tính chính: 
Key: trả về giá trị Key của phần tử hiện tại. 
Value: trả về giá trị Value của phần tử hiện tại.
Ví dụ: thử dùng foreach duyệt 1 Hashtable và in ra giá trị Key – Value của mỗi phần tử:
Việc truy xuất các phần tử trong Hashtable giống như truy xuất các phần tử mảng nhưng thông qua Key. Ví dụ:
// Tạo một Hashtable đơn giản với 3 phần tử
Hashtable hash = new Hashtable(); hash.Add("K", "Kteam"); hash.Add("H", "HowKteam");
hash.Add("FE", "Free Education");
/* * Duyệt qua các phần tử trong Hashtable.
* Vì mỗi phần tử là 1 DictionaryEntry nên ta chỉ định kiểu dữ liệu cho item là DictionaryEntry luôn.
* Thử in ra màn hình cặp Key - Value của mỗi phần tử được duyệt. */
foreach (DictionaryEntry item in hash) {
Console.WriteLine(item.Key + "\t" + item.Value); }
Console.WriteLine(MyHash["One"]); Trong đó:
 MyHash là tên của Hashtable.
 One là tên Key cần truy xuất.
 MyHash["One"] sẽ lấy ra giá trị Value tương ứng với Key trên.
Nên cẩn thận khi truy xuất các phần tử trong Hashtable thông qua Key:
 Nếu thực hiện lấy giá trị 1 phần tử trong Hashtable với Key không tồn tại thì sẽ ra giá
trị null và không báo lỗi.
 Nếu thực hiện gán giá trị cho 1 phần tử trong Hashtable tại vị trí Key không tồn tại
thì Hashtable sẽ tự thêm 1 phần tử mới với Key và Value như trên. Điều này có thể làm
phát sinh thêm các phần tử không mong muốn trong danh sách.
// In ra màn hình giá trị Value trong 1 Key không tồn tại. Console.WriteLine(hash["VT"]);
// Để chắc chắn là null ta thử kiểm tra bằng điều kiện if. if (hash["VT"] == null) { Ví dụ:
Console.WriteLine("Key 'VT' is not exists"); } SortedList
// Thử in ra số phần tử ban đầu của Hashtable
Console.WriteLine("\nCount: " + hash.Count);
foreach (DictionaryEntry item in hash) {
Console.WriteLine(item.Key + "\t" + item.Value);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 88
SortedList cũng là một Collections lưu trữ dữ liệu dưới dạng cặp Key - Value. Key đại
diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá trị tương ứng của khoá đó.
Đặc điểm của SortedList
Là 1 Hashtable nhưng các giá trị được sắp xếp theo Key. Việc sắp xếp này được thực
hiện một cách tự động mỗi khi thêm 1 phần tử mới vào SortedList. 
Có thể truy xuất đến các phần tử trong SortedListthông qua Key(như Hashtable)
hoặc thông qua chỉ số phần tử (như ArrayList). 
SortedList chính là sự kết hợp giữa ArrayList với Hashtable .
Do SortedList cũng là 1 Collections nên để sử dụng ta cần thêm thư
viện System.Collections bằng câu lệnh: using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// khởi tạo 1 SortedList r n ỗ g
SortedList MySL = new SortedList();
Cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
// khởi tạo 1 SortedList và ch ỉ đ n ị h Capacity ban đ u ầ là 5
SortedList MySL2 = new SortedList(5);
Cũng có thể khởi tạo 1 SortedList chứa các phần tử được sao chép từ một SortedList khác: /*Kh i ở t o
ạ 1 SortedList có kích thư c ớ b n ằ g v i ớ MySL2. * Sao chép toàn đ ộ ph n ầ t ử trong MySL2 vào MySL3. */
SortedList MySL3 = new SortedList(MySL2);
Vì các phần tử của SortedList được sắp xếp tự động theo Key nên ta cũng có thể chỉ ra
cách sắp xếp do mình tự định nghĩa thông qua constructor có sẵn: /** định nghĩa 1 l p ớ PersonComparer có th c ự thi 1 interface IComparer
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 89 * Sau đó override l i ạ phư n ơ g th c ứ Compare. * Sử dụng lớp trên đ ể truy n ể vào constructor c a ủ SortedList. */
SortedList MySL4 = new SortedList(new PersonComparer());
Ngoài ra cũng có thể khởi tạo 1 SortedList chứa các phần tử được sao chép từ
1 SortedList khác đồng thời sắp xếp lại các phần tử theo 1 cách sắp xếp khác:
/* * Tạo 1 SortedList mới và sao chép các phần tử từ MySL3 đồng thời sắp xếp các phần tử lại
* theo cách sắp xếp được định nghĩa trong lớp PersonComparer. */
SortedList MySL5 = new SortedList(MySL3, new PersonComparer());
Một số thuộc tính và phương thức hỗ trợ sẵn trong SortedList
Vì SortedList là sự kết hợp giữa ArrayList và Hashtable nên nó sẽ mang các thuộc tính,
phương thức giống 2 Collections trên và một vài phương thức mới.
Một số thuộc tính thông dụng trong SortedList: TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong SortedList . Capacity
Trả về 1 số nguyên cho biết số phần tử mà SortedList có thể chứa (sức chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự động tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho SortedList. Keys
Trả về 1 danh sách chứa các Key trong SortedList. Values
Trả về 1 danh sách chứa các Value trong SortedList.
Một số phương thức thông dụng trong SortedList: TÊN PHƯƠNG THỨC Ý NGHĨA Add(object Key, object Value)
Thêm 1 cặp Key - Value vào SortedList. Clear()
Xoá tất cả các phần tử trong SortedList. Clone()
Tạo 1 bản sao từ SortedList hiện tại. ContainsKey(object Key)
Kiểm tra đối tượng Key có tồn tại trong SortedList hay không. ContainsValue(object Value)
Kiểm tra đối tượng Value có tồn tại trong SortedList hay không.
CopyTo(Array array, int Index) Thực hiện sao chép tất cả phần tử trong SortedList sang mảng
một chiều array từ vị trí Index của array.
Lưu ý: array phải là mảng các object hoặc mảng các DictionaryEntry. GetByIndex(int Index)
Trả về giá trị Value tại vị trí Index trong SortedList. GetKey(int Index)
Trả về giá trị Key tại vị trí Index trong SortedList. GetKeyList()
Trả về 1 List các Key trong SortedList. GetValueList()
Trả về 1 List các Value trong SortedList. IndexOfKey(object Key)
Trả về 1 số nguyên là chỉ số phần tử của 1 Key trong SortedList
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 90 Remove(object Key)
Xoá đối tượng có Key xuất hiện đầu tiên trong SortedList. RemoveAt(int Index)
Xoá đối tượng tại vị trí Index trong SortedList. SetByIndex
Gán giá trị Value mới tại vị trí Index trong SortedList. (int Index, object Value)
Về cách sử dụng thì thao tác hoàn toàn giống với Hashtable . Một số lưu ý
Nếu bạn muốn các giá trị Key là các đối tượng thuộc 1 lớp nào đó thì bạn phải định
nghĩa cách so sánh đối tượng đó. Nếu không chương trình sẽ báo lỗi vì nó không biết phải
sắp xếp các Key này như thế nào. Ví dụ đoạn chương trình sau:
SortedList MySL6 = new SortedList();
Khi chạy chương trình trên sẽ nhận được lỗi sau:
MySL6.Add(new Person("HowKteam", 20), 10);
MySL6.Add(new Person("Kteam", 2), 15);
Để khắc phục điều này ta có thể định nghĩa 1 lớp thực thi interface IComparer và định
nghĩa cách sắp xếp trong hàm Comparer: ///
/// Định nghĩa 1 lớp thực thi interface IComparer.
/// override phương thức Comparer và định nghĩa cách sắp xếp trong đó.
/// Chi tiết bạn có thể xem lại bài ArrayList trong C#. ///
class PersonComparer : Icomparer {
public int Compare(object x, object y) { Person a = x as Person; Person b = y as Person; if (a == null || b == null)
{ throw new InvalidOperationException(); } else { if (a.Age > b.Age) { return 1; } else if (a.Age == b.Age) { return 0; } else { return -1; } } } }
Sau đó sử dụng constructor của SortedList để truyền lớp này vào:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 91 // t o ạ 1 SortedList và truy n ề vào cách s p ắ x p ế các Key trong
SortedList SortedList MySL6 = new SortedList(new PersonComparer());
Khi đó chương trình sẽ căn cứ vào hàm Comparer để sắp xếp các Key.
Khi các Key hoặc Value là các đối tượng thuộc 1 lớp nào đó thì thì ta nên override lại
phương thức ToString để việc in ra Key và Value không bị lỗi:
Ví dụ với đoạn chương trình trên khi chưa override phương thức ToString thì kết quả hiển thị là:
public override string ToString() { return Name + " : " + Age; }
Ta thử override phương thức ToString trong lớp Person: Stack
Stack
(hay còn gọi là ngăn xếp) là một cấu trúc dữ liệu hoạt động theo nguyên lý LIFO
(Last In First Out). Người ta hay gọi nó là ngăn xếp bởi vì nó hoạt động giống như ngăn xếp trong thực tế vậy.
Giả sử bạn xếp chồng các chiếc đĩa lên nhau.
Khi đó chiếc đĩa được xếp đầu tiên sẽ nằm dưới cùng và chiếc đĩa được xếp sau cùng sẽ
nằm trên đầu. Nếu bạn muốn lấy đĩa ra sử dụng chắc chắn bạn sẽ lấy chiếc đĩa nằm trên
cùng ra. Đây chính là nguyên lý Last In First Out (vào cuối ra đầu) của Stack.
Quay lại vấn đề lập trình, hãy tưởng tượng cả chồng đĩa đó chính là 1 danh sách (Stack),
các chiếc đĩa là các phần tử của danh sách. Thế là ta đã có cấu trúc dữ liệu Stack trong lập trình rồi.
Trong C#, Stack là một Collections đại diện cho một danh sách hoạt động theo nguyên lý LIFO.
Vì C# đã hỗ trợ sẵn cấu trúc dữ liệu Stack rồi nên chỉ tìm hiểu cách sử dụng thôi.
Đặc điểm của Stack
Là một danh sách lưu trữ các đối tượng nhưng không thể truy cập các phần tử thông qua
chỉ số phần tử được. 
Hành động thêm phần tử vào Stack được gọi là Push (đẩy vào). 
Hành động lấy phần tử ra khỏi Stack được gọi là Pop (đẩy ra). Và luôn luôn lấy ra phần
tử được thêm vào cuối cùng.
Do stack cũng là 1 collection nên khi sử dụng cần thêm thư viện System.Collection.
using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new: // kh i ở t o ạ 1 Stack r n ỗ g
Stack MyStack = new Stack();
Cũng có thể chỉ định sức chứa(Capacity) ngay lúc khởi tạo bằng cách thông qua
construction được hỗ trợ sẵn:
// khởi tạo 1 Stack và chỉ định sức chứa ban đầu là 5
Stack MyStack = new Stack(5);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 92
Bạn cũng có thể khởi tạo 1 Stack chứa các phần tử được sao chép từ một danh sách
// khởi tạo 1 mảng bất kỳ
ArrayList MyArray = new ArrayList(); MyArray.Add(5); MyArray.Add(9); MyArray.Add(10);
// Khởi tạo 1 Stack và sao chép giá trị của các phần tử từ MyArray vào Stack.
Stack MyStack3 = new Stack(MyArray); khác:
Một số thuộc tính và phương thức hỗ trợ sẵn trong Stack
Một số thuộc tính thông dụng trong Stack: TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện trong Stack.
Một số phương thức thông dụng trong Stack: TÊN PHƯƠNG THỨC Ý NGHĨA Clear()
Xoá tất cả các phần tử trong Stack. Clone()
Tạo 1 bản sao từ Stack hiện tại. Contains (object Value)
Kiểm tra đối tượng Value có tồn tại trong Stack hay không. CopyTo
Sao chép tất cả phần tử trong Stack sang mảng một chiều array (Array array, int Index)
từ vị trí Index của Array Peek()
Trả về giá trị của đối tượng tại vị trí trên
cùng
trong Stack (phần tử thêm vào cuối cùng)
nhưng không xoá phần tử khỏi Stack. Pop()
Trả về giá trị của đối tượng tại vị trí trên cùng trong Stack
(phần tử thêm vào cuối cùng) đồng thời xóa phần tử khỏi Stack Push(object Value)
Thêm một phần tử có giá trị Value vào vị trí trên cùng trong Stack. ToArray()
Tạo ra 1 mảng các object chứa tất cả các phần tử trong Stack và trả về mảng đó.
Một số ví dụ về sử dụng Stack
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 93 // Tạo 1 Stack rỗng Stack MyStack4 = new Stack();
// Thực hiện thêm vài phần tử vào Stack thông qua hàm Push. MyStack4.Push("Education"); MyStack4.Push("Free"); MyStack4.Push("HowKteam");
// Thử sử dụng các phương thức của Stack.
Console.WriteLine(" So phan tu hien tai cua Stack la: {0}", MyStack4.Count);
// Lưu ý ở đây ta chỉ muốn xem giá trị mà không muốn nó khỏi Stack thì ta sẽ dùng Peek.
Console.WriteLine(" Phan tu dau cua Stack la: {0}", MyStack4.Peek());
// Thử kiểm tra lại số phần tử để chắc chắn rằng hàm Peek không xoá phần tử ra khỏi Stack.
Console.WriteLine(" So phan tu cua Stack sau khi goi ham Peek: {0}", MyStack4.Count);
// Thực hiện xoá các phần tử ra khỏi Stack.
Console.WriteLine(" Popping..."); int Length = MyStack4.Count;
for (int i = 0; i < Length; i++) { Queue
Queue
(hay còn gọi là hàng đợi) là một cấu trúc dữ liệu hoạt động theo nguyên
FIFO (First In First Out). Người ta hay gọi nó là hàng đợi bởi vì nó hoạt động giống
như việc xếp hàng đợi chờ gì đó trong thực tế vậy.
Giả sử ta đến xếp hàng để mua vé như hình dưới:
 Khi đó người xếp hàng đầu tiên sẽ là người mua được vé đầu tiên. Đây chính là nguyên
First In First Out (vào trước ra trước) của Queue.
 Hãy tưởng tượng những người đứng xếp hàng đó chính là 1 danh sách (Queue), mỗi
người là 1 phần tử của danh sách. Thế là ta đã có cấu trúc dữ liệu Queue trong lập trình rồi đó.
Trong C#, Queue là một Collections đại diện cho một danh sách hoạt động theo nguyên
FIFO đã trình bày ở trên.
Vì C# đã hỗ trợ sẵn cấu trúc dữ liệu Queue rồi nên chỉ tìm hiểu cách sử dụng nó thôi.
Đặc điểm của Queue
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 94
 Là một danh sách lưu trữ các đối tượng nhưng không thể truy cập các phần tử thông qua
chỉ số phần tử được.
 Hành động thêm phần tử vào Queue được gọi là Enqueue (xếp hàng).
 Hành động lấy phần tử ra khỏi Queue được gọi là Dequeue (ra khỏi hàng). Và luôn luôn
lấy ra phần tử được thêm vào đầu tiên.
Queue rất giống Stack chỉ khác ở nguyên lý hoạt động thôi nên Stack có gì Queue sẽ có cái tương tự như vậy.
Do Queue cũng là 1 collection nên khi sử dụng cần thêm thư viện System.Collection.
using System.Collections;
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new: // kh i ở t o ạ 1 Queue r n ỗ g
Queue MyQueue = new Queue();
Cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
// khởi tạo 1 Queue và chỉ định sức chứa ban đầu là 5
Queue MyQueue2 = new Queue(5);
Cũng có thể khởi tạo 1 Queue chứa các phần tử được sao chép từ một danh sách khác:
// khởi tạo 1 mảng bất kỳ
ArrayList MyArray = new ArrayList(); MyArray.Add(5); MyArray.Add(9); MyArray.Add(10);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 95
// Khởi tạo 1 Queue và sao chép giá trị của các phần tử từ MyArray vào Queue.
Queue MyQueue3 = new Queue(MyArray);
Một số phương thức và thuộc tính thoog dụng của Queue
Một số thuộc tính thông dụng trong Queue:
TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong Queue .
Một số phương thức thông dụng trong Queue: TÊN PHƯƠNG THỨC Ý NGHĨA Clear()
Xoá tất cả các phần tử trong Queue . Clone()
Tạo 1 bản sao từ Queue hiện tại.
Contains (object Value) Kiểm tra đối tượng Value có tồn tại trong Queue hay không. CopyTo
Thực hiện sao chép tất cả phần tử trong Queue sang mảng một chiều
(Array array, int Index) array từ vị trí Index của array. Enqueue ()
Trả về giá trị của đối tượng tại vị trí đầu trong Queue (phần tử thêm vào đầu tiên)
nhưng không xoá phần tử khỏi Queue. Dequeue()
Trả về giá trị của đối tượng tại vị trí đầu trong Queue(phần tử thêm vào đầu tiên)
đồng thời xoá phần tử khỏi Queue. Push(object Value)
Thêm một phần tử có giá trị Value vào đầu Queue. ToArray()
Tạo ra 1 mảng các object chứa tất cả các phần tử trong Queue và trả về mảng đó.
Một ví dụ đơn giản về sử dụng Queue
Cách sử dụng Queue hoàn toàn tương tự cách sử dụng Stack. Một ví dụ đơn giản về sử dụng Queue: // Tạo 1 Queue rỗng Queue MyQueue4 = new Queue();
// Thực hiện thêm vài phần tử vào Queue thông qua hàm Enqueue. MyQueue4.Enqueue("HowKteam"); MyQueue4.Enqueue("Free"); MyQueue4.Enqueue("Education");
// Thử sử dụng các phương thức của Queue.
Console.WriteLine(" So phan tu hien tai cua Queue la: {0}", MyQueue4.Count);
// Lưu ý ở đây ta chỉ muốn xem giá trị mà không muốn nó khỏi Queue thì ta sẽ dùng Peek.
Console.WriteLine(" Phan tu dau cua Queue la: {0}", MyQueue4.Peek());
// Thử kiểm tra lại số phần tử để chắc chắn rằng hàm Peek không xoá phần tử ra khỏi Queue.
Console.WriteLine(" So phan tu cua Queue sau khi goi ham Peek: {0}", MyQueue4.Count);
// Thực hiện xoá các phần tử ra khỏi Queue thông qua hàm Dequeue.
Console.WriteLine(" Popping..."); int Length = MyQueue4.Count;
for (int i = 0; i < Length; i++) {
Console.Write(" " + MyQueue4.Dequeue()); } Console.WriteLine();
// Kiểm tra lại số phần tử của Queue sau khi Pop
Console.WriteLine(" So phan tu cua Queue sau khi Pop la: {0}",
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 96 Kết quả: BitArray
BitArray
là một Collections giúp quản lý, lưu trữ một danh sách các bit (0 hoặc 1),
được biểu diễn như kiểu Boolean (kiểu luận lý). Trong đó true biểu thị cho
bit 1 và false biểu thị cho bit 0.
Nó được sử dụng khi ta cần lưu trữ danh sách các bit mà chưa biết trước số lượng. Ta có
thể truy cập đến các phần tử trong BitArray thông qua chỉ số như ArrayList.
Sẽ có thắc mắc sao không dùng mảng các đối tượng kiểu bool mà lại dùng BitArray?
Thì câu trả lời là BitArray giúp tiết kiệm bộ nhớ hơn rất nhiều: 
Mặc dù kiểu bool chỉ lưu 2 giá trị true hoặc false nhưng lại tốn đến 1 bytes cho mỗi biến kiểu bool. 
Trong khi đó mỗi phần tử trong BitArray tốn đúng 1 bit để lưu trữ.
Trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new.
Lưu ý: là ta không thể khởi tạo 1 BitArray rỗng!
Đầu tiên có thể khởi tạo BitArray và cho biết số phần tử ban đầu của BitArray:
/* Khởi tạo 1 BitArray có 10 phần tử.
* Mỗi phần tử có giá trị mặc định 0 (false).*/
BitArray MyBA = new BitArray(10);
Nếu bạn không muốn giá trị mặc định là false thì bạn có thể chỉ định giá trị mặc

định thông qua constructor:
/* Khởi tạo 1 BitArray có 10 phần tử.
* Mỗi phần tử có giá trị mặc định 1 (true). */
BitArray MyBA2 = new BitArray(10, true);
Có thể khởi tạo một BitArray từ một mảng bool có sẵn:
/* Khởi tạo 1 BitArray từ một mảng bool có sẵn. */
bool[] MyBools = new bool[5] { true, false, true, true, false };
BitArray MyBA3 = new BitArray(MyBools); // 1 0 1 1 0
Hoặc khởi tạo một BitArray từ một mảng byte có sẵn:
/** Khởi tạo 1 BitArray từ một mảng byte có sẵn. */
byte[] MyBytes = new byte[5] { 1, 2, 3, 4, 5 };
BitArray MyBA4 = new BitArray(MyBytes);
Đối với trường hợp này ta có thể cách lưu trữ của nó như sau:
 Đầu tiên ta đã biết 1 byte = 8 bits.
 Khi đó trình biên dịch sẽ chuyển các số kiểu byte sang dạng 8 bits và lưu lần lượt
vào BitArray. Như vậy trường hợp trên sẽ có 5 số kiểu byte tương đương với 40 bits sẽ được lưu vào BitArray.
 Thử in giá trị các phần tử BitArray trên để kiểm chứng:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 97
Để dễ nhìn thì cứ in được 8 bits thì mình
sẽ xuống dòng. Lưu ý đây chỉ là cách hiển
thị lên màn hình thôi chứ bên trong vẫn là 1 mảng các bit duy nhất:
Gợi ý: Có thể thử đổi các bit trên xem có
giống 5 số kiểu byte ban đầu không!
Tương tự, cũng có thể khởi tạo một BitArray từ một mảng các số nguyên int có sẵn:
/* Khởi tạo 1 BitArray từ một mảng int có sẵn.*/
int[] MyInts = new int[5] { 1, 2, 3, 4, 5 };
BitArray MyBA5 = new BitArray(MyInts);
Trường hợp này tương tự như trên nhưng
lúc này kiểu int chiếm 4 bytes nên ta sẽ
đổi 4 bytes = 32 bits. Và Trình biên dịch sẽ
chuyển mỗi số nguyên int sang 32 bits và lưu vào ArrayList:
Một số thuộc tính và phương thức hỗ trợ sẵn trong BitArray
Một số thuộc tính thông dụng trong BitArray: TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong BitArray. Length
Trả về 1 số nguyên là số phần tử hiện có trong BitArray.
Đồng thời có thể thay đổi kích thước của BitArray bằng cách gán giá trị mới cho thuộc tính này.
Một số phương thức thông dụng trong BitArray: TÊN PHƯƠNG THỨC Ý NGHĨA And(BitArrayValue)
Thực hiện phép toán AND bit giữa dãy bit hiện tại với dãy bit Value
và trả về 1 BitArray là kết quả của phép toán trên. Clone()
Tạo 1 bản sao từ BitArray hiện tại. CopyTo
Thực hiện sao chép tất cả phần tử trong BitArray sang (Array array, int Index)
mảng một chiều array từ vị trí Index của array. Get(int Index)
Trả về giá trị của bit tại vị trí Index trong BitArray. Not()
Trả về 1 BitArray là kết quả của phép toán NOT trên dãy bit hiện tại. Or(BitArray Value)
Trả về 1 BitArray là kết quả của phép toán OR giữa dãy bit hiện tại với dãy bit Value.
Set(int Index, bool Value) Gán giá trị cho bit tại vị trí Index với giá trị mới là Value. SetAll(bool Value)
Gán giá trị cho toàn bộ các bit trong BitArray với giá trị mới là Value. Xor(BitArray Value)
Trả về 1 BitArray là kết quả của phép toán XOR giữa dãy bit hiện tại với dãy bit Value. Lưu ý:
Các phép toán AND, OR, NOT, XOR phải được thực hiện trên 2 BitArray cùng
độ dài
nếu không sẽ báo lỗi. 
Các phép toán AND, OR, NOT, XOR sẽ làm thay đổi cả BitArray gọi nó. Ví dụ:
BitArray A = new BitArray(5);
BitArray B = new BitArray(5, true); A.And(B);

T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 98
Thì kết quả của phép AND sẽ được cập nhật giá trị vào BitArray A.
Một ví dụ đơn giản về sử dụng BitArray
Một chương trình đơn giản về BitArray:
// Khởi tạo 1 BitArray từ mảng bool có sẵn
bool[] MyBool2 = new bool[5] { true, false, true, true, false };
BitArray MyBA6 = new BitArray(MyBool2);
// Khởi tạo 1 BitArray có 2 phần tử và có giá trị mặc định là 1 (true)
bool[] MyBool3 = new bool[] { false, true, true, false, false };
BitArray MyBA7 = new BitArray(MyBool3);
Console.Write(" Gia tri cua MyBA6: "); PrintBits(MyBA6, 5);
Console.Write(" Gia tri cua MyBA7: "); PrintBits(MyBA7, 5);
Console.WriteLine(" Thuc hien cac phep toan AND, OR, NOT, XOR tren MyBA6 va MyBA7: ");
// thực hiện sao chép giá trị của MyBA6 ra để không làm thay đổi nó
BitArray AndBit = MyBA6.Clone() as BitArray; AndBit.And(MyBA7);
Console.Write(" Ket qua cua phep toan AND: "); PrintBits(AndBit, 5);
BitArray OrBit = MyBA6.Clone() as BitArray; OrBit.Or(MyBA7);
Console.Write(" Ket qua cua phep toan OR: "); PrintBits(OrBit, 5);
BitArray XorBit = MyBA6.Clone() as BitArray; XorBit.Xor(MyBA7);
Console.Write(" Ket qua cua phep toan XOR: "); PrintBits(XorBit, 5);
BitArray NotBit = MyBA6.Clone() as BitArray; NotBit.Not();
Console.Write(" Ket qua cua phep toan NOT tren MyBA6: "); PrintBits(NotBit, 5); Generic trong C#
Template được dùng để tạo các lớp, các hàm mà không cần quan tâm đến đối số kiểu dữ
liệu là gì. Template được đưa ra nhằm mục đích tăng tính tái sử dụng lại mã nguồn.
Generic cho phép định nghĩa 1 hàm, một lớp mà không cần chỉ ra đối số kiểu dữ liệu là
gì. Tùy vào kiểu dữ liệu mà người dùng truyền vào thì nó sẽ hoạt động theo kiểu dữ liệu đó.
Ví dụ: hàm hoán đổi giá trị 2 số nguyên:
Public static void Swap(ref int a, ref int b) { int temp = a; a = b; b = temp; }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 99
Mọi thứ đều hoạt động tốt cho tới khi hoán đổi 2 số thực. khi đó phải viết lại 1 hàm
Swap mới với kiểu dưc liệu của tham số truyền vào là kiểu số thực.
Mặc dù thao tác giống nhau nhưng phải viết hàm 2 lần. Chính vì vậy, Generic ra đời để
giúp giảm thiểu việc code và tăng tính năng sử dụng.
Nếu sử dụng Generic ta viết như sau:
public static void Swap(ref T a, ref T b) { T temp = a; a = b; b = temp; }
Chỉ cần đặt 1 chữ cái nào đó thay cho kiểu dữ liệu và khi gọi hàm ta chỉ ra kiểu dữ liệu
đang thao tác là gì. Ví dụ: int a = 5, b = 7; double c = 1.2, d = 5.6; Swap(ref a, ref b); Swap(ref c, ref d);
Khi gọi cú pháp bên dưới thì hàm Swap sẽ chạy và thay ký tự T thành kiểu dữ liệu int tương ứng.
Swap<int>(ref a, ref b)
Phía trên là Generic cho phương thức. Tiếp theo là Generic cho lớp cũng tương tự. public class MyGeneric { private T[] items; public T[] Items { get { return items; } } public MyGeneric(int Size) { items = new T[Size]; }
public T GetByIndex(int Index) {
// Nếu index vượt ra khỏi chỉ số phần tử của mảng thì ném ra ngoại lệ
if (Index < 0 || Index >= items.Length) {
throw new IndexOutOfRangeException(); } else { return items[Index]; } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 100
public void SetItemValue(int Index, T Value) {
if (Index < 0 || Index >= items.Length) {
throw new IndexOutOfRangeException(); } else { items[Index] = Value; } } }
Thay vì viết một lớp cho kiểu int, long, double gì đó thì giờ thay nó bằng T còn lại thao tác như bình thường.
Đến khi sử dụng thì ta truyền kiểu dữ liệu thích hợp vào. Ví dụ: // Kh i ở t o ạ 1 m n ả g s ố nguyên ki u ể int có 5 ph n ầ tử
MyGeneric<int> MyG = new MyGeneric<int>(5); MyG.SetItemValue(0, 10);
Khi bạn khai báo cú pháp bên dưới thì trình biên dịch sẽ hiểu T trong lớp MyGeneric là
kiểu int và thay thế toàn bộ chữ cái T trong lớp thành int sau đó thực thi. MyGeneric<int>
Đặc điểm của Generic
Giúp định nghĩa một thao tác dữ liệu với kiểu dữ liệu chung nhất nhìn hạn chế viết code và tái sử dụng.
Ứng dụng phổ biến nhất của Generic là tạo ra các Generic Collections. 
Các Collections phổ biến thì giá trị lưu trữ bên trong đều là object. 
Điều này gây rất nhiều khó khăn nếu như muốn quản lý 1 danh sách có cùng kiểu.
Vì object có thể chứa được mọi kiểu dữ liệu nên ta khó kiểm soát rằng việc thêm
phần tử có phải cùng kiểu dữ liệu ta mong muốn hay không. 
Từ đó Generic Collections ra đời để giúp ta vừa có thể sử dụng được các Collections
vừa có thể hạn chế lỗi xảy ra trong quá trình thực thi.
Ngoài ra, Generic còn giúp hạn chế truy cập nếu như không truyền đúng kiểu dữ liệu.
Một số loại Generic Collections thông dụng
Các Generic Collections đều được xây dựng bắt nguồn từ 1 Collections nào đó có sẵn.
Vì thế với mỗi Collections đã học sẽ có một Generic tương ứng.
Một số Generic Collections được sử dụng phổ biến: LỚP MÔ TẢ List
Là một Collections giúp lưu trữ các phần tử liên tiếp (giống mảng)
nhưng có khả năng tự mở rộng kích thước.
Generic Collections này là sự thay thế cho A rrayList đã học. Dictionary
Lớp lưu trữ dữ liệu dưới dạng cặp Key – Value.
Khi đó ta sẽ truy xuất các phần tử trong danh sách này thông qua Key
(thay vì thông qua chỉ số phần tử như mảng bình thường).
Generic Collections này là sự thay thế cho H ashtable đã học.
SortedDictionary Là sự kêt hợp của List và Dictionary. Tức là dữ liệu sẽ lưu dưới dạng Key
– Value. Ta có thể truy xuất các phần tử trong danh sách thông
qua Key hoặc thông qua chỉ số phần tử.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 101
Đặc biệt là các phần tử trong danh sách này luôn được sắp xếp theo giá trị của Key.
Generic Collections này là sự thay thế cho S ortedList đã học. Stack
Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc LIFO (Last In First Out).
Generic Collections này là sự thay thế cho S tack đã học. Queue
Lớp cho phép lưu trữ và thao tác dữ liệu theo cấu trúc FIFO (First In First Out).
Generic Collections là sự thay thế cho Q ueue đã học. List
List là 1 Generic Collections đưa ra như một sự thay thế ArrayList vì thế về khái niệm
cũng như sử dụng nó hoàn toàn giống với ArrayList.
List trong C# là một Generic Collections giúp lưu trữ và quản lý một danh sách các đối
tượng theo kiểu mảng (truy cập các phần tử bên trong thông qua chỉ số index).
Để sử dụng các Collection trong .NET ta cần thêm thư viện System.Collection.Generic bằng câu lệnh.
using System.Collections.Generic;
Vì List là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new: // kh i ở t o ạ 1 List các s ố nguyên r n ỗ g
List<int> MyList = new List<int>();
Có thể chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông qua constructor được hỗ trợ sẵn:
// khởi tạo 1 List các số nguyên và chỉ định Capacity ban đầu là 5
List<int> MyList2 = new List<int>(5);
Ngoài ra bạn cũng có thể khởi tạo 1 List chứa các phần tử được sao chép từ một Generic
Collections khác (lưu ý là có cùng kiểu dữ liệu truyền vào): /* * Kh i ở t o ạ 1 List s ố nguyên có kích thư c ớ b n ằ g v i ớ MyList2. * Sao chép toàn đ ộ ph n ầ t
ử trong MyList2 vào MyList3. */
List<int> MyList3 = new List<int>(MyList2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong List
Một số thuộc tính thông dụng trong List: TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong List. Capacity
Trả về 1 số nguyên cho biết số phần tử mà List có thể chứa (sức chứa).
Nếu số phần tử được thêm vào chạm sức chứa này thì hệ thống sẽ tự động tăng lên.
Ngoài ra ta có thể gán 1 sức chứa bất kỳ cho List.
Một số phương thức thông dụng trong List: TÊN PHƯƠNG THỨC Ý NGHĨA Add(object Value)
Thêm đối tượng Value vào cuối List. AddRange
Thêm danh sách phần tử ListObject vào cuối List.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 102 (ICollection ListObject) BinarySearch
Tìm kiếm đối tượng Value trong List theo thuật toán tìm kiếm nhị (object Value) phân.
Nếu tìm thấy sẽ trả về vị trí của phần tử ngược lại trả về giá trị âm.
Lưu ý: List phải được sắp xếp trước khi sử dụng hàm. Clear()
Xoá tất cả các phần tử trong List. Contains(T Value)
Kiểm tra đối tượng Value có tồn tại trong List hay không. CopyTo
Thực hiện sao chép tất cả phần tử trong List sang mảng một chiều (T[] array, int Index)
array từ vị trí Index của array.
Lưu ý: array phải là mảng kiểu T tương ứng. IndexOf(T Value)
Trả về vị trí đầu tiên xuất hiện đối tượng Value trong List.
Nếu không tìm thấy sẽ trả về -1. Insert
Chèn đối tượng Value vào vị trí Index trong List. (int Index, T Value) InsertRange
Chèn danh sách phần tử ListObject vào vị trí Index trong List. (int Index, IEnumerable ListObject) LastIndexOf(T Value)
Trả về vị trí xuất hiện cuối cùng của đối tượng Value trong List.
Nếu không tìm thấy sẽ trả về -1. Remove(T Value)
Xoá đối tượng Value xuất hiện đầu tiên trong List. Reverse()
Đảo ngược tất cả phần tử trong List. Sort()
Sắp xếp các phần tử trong List theo thứ tự tăng dần. ToArray()
Trả về 1 mảng kiểu T chứa các phần tử được sao chép từ List.
Sử dụng List tương tự như sử dụng ArrayList. Một ví dụ đơn giản về sử dụng List:
List MyList4 = new List();//Tạo List kiểu string và thêm 2 phần tử vào List.
MyList4.Add("Free"); MyList4.Add("Education");
Console.WriteLine(" List ban dau: ");// In giá trị các phần tử trong List
Console.WriteLine("So luong phan tu trong List la:{0}", MyList4.Count);
foreach (string item in MyList4) { Console.Write(" " + item); } Console.WriteLine();
MyList4.Insert(0, "HowKteam");// Chèn 1 phần tử vào đầu List
// In lại giá trị các phần tử trong List để xem đã chèn được hay chưa
Console.WriteLine(" List sau khi insert: ");
Console.WriteLine("So luong phan tu trong List la:{0}", MyList4.Count);
foreach (string item in MyList4) { Console.Write(" " + item); } Console.WriteLine();
// Kiểm tra 1 phần tử có tồn tại trong List hay không
bool isExists = MyList4.Contains("Kteam"); if (isExists == false) {
Console.WriteLine(" Khong tim thay chuoi Kteam trong List"); }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 103 Dictionary
Tương tự như List, Dictionary chính là sự thay thế cho Collections Hashtable. Cho nên
về khái niệm hay sử dụng thì Dictionary đều sẽ giống Hashtable.
Dictionary trong C# là một Collections lưu trữ dữ liệu dưới dạng cặp Key -
Value. Key đại diện cho 1 khoá giống như chỉ số phần tử của mảng và Value chính là giá
trị tương ứng của khoá đó. Ta sẽ dử dụng Key để truy cập đến Value tương ứng.
Do Dictionary là 1 Generic Collections nên để sử dụng ta cần thêm thư
viện System.Collections.Generic bằng câu lệnh:
using System.Collections.Generic;
Vì Dictionary là một lớp nên trước khi sử dụng ta cần khởi tạo vùng nhớ bằng toán tử new:
// khởi tạo 1 Dictionary rỗng với Key và Value đều có kiểu dữ liệu là chuỗi.
Dictionary<string, string> MyHash = new Dictionary<string, string>();
Bạn cũng có chỉ định sức chứa (Capacity) ngay lúc khởi tạo bằng cách thông
qua constructor được hỗ trợ sẵn:
/* khởi tạo 1 Dictionary với Key và Value có kiểu chuỗi
* đồng thời chỉ định Capacity ban đầu là 5 */
Dictionary MyDic2 = new Dictionary(5);
Ngoài ra bạn cũng có thể khởi tạo 1 Dictionary chứa các phần tử được sao chép từ một Dictionary khác: /*
* Khởi tạo 1 Dictionary có kích thước bằng với MyDic2.
* Sao chép toàn độ phần tử trong MyDic2 vào MyDic3. */
Dictionary<string, string> MyDic3 = new Dictionary<string, string>(MyDic2);
Một số thuộc tính và phương thức hỗ trợ sẵn trong Dictionary
Một số thuộc tính thông dụng trong Dictionary: TÊN THUỘC TÍNH Ý NGHĨA Count
Trả về 1 số nguyên là số phần tử hiện có trong Dictionary. Keys
Trả về 1 danh sách chứa các Key trong Dictionary. Values
Trả về 1 danh sách chứa các Value trong Dictionary.
Một số phương thức thông dụng trong Dictionary: TÊN PHƯƠNG THỨC Ý NGHĨA
Add(TKey Key, TValue Value) Thêm 1 cặp Key - Value vào Dictionary. Clear()
Xoá tất cả các phần tử trong Dictionary. ContainsKey(TKey Key)
Kiểm tra đối tượng Key có tồn tại trong Dictionary hay không.
ContainsValue(TValue Value) Kiểm tra đối tượng Value có tồn tại trong Dictionary hay không. Remove(TKey Key)
Xoá đối tượng có Key xuất hiện đầu tiên trong Dictionary. TryGetValue
Kiểm tra Key có tồn tại hay không. (TKey Key, TValue Value)
Nếu có sẽ trả về true đồng thời trả về giá trị Value tương ứng qua biến Value.
Ngược lại trả về false.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 104
Một số lưu ý về Dictionary:
Mỗi một phần tử trong Dictionary (bao gồm 1 cặp Key - Value) được C# định nghĩa là 1 đối tượng có kiểu: KeyValuePair
Trong đó, có 2 thuộc tính chính:
Key: trả về giá trị Key của phần tử hiện tại.
Value: trả về giá trị Value của phần tử hiện tại.
Điều này tương tự như DictionaryEntry trong Hashtable. Vì thế cách sử dụng cũng
tương tự. Ví dụ mình thử dùng foreach duyệt 1 Dictionary và in ra giá trị Key – Value của mỗi phần tử:
// Tạo 1 Dictionary đơn giản và thêm vào 3 phần tử.
Dictionary MyDic4 = new Dictionary();
MyDic4.Add("FE", "Free Education"); MyDic4.Add("K", "Kteam"); MyDic4.Add("HK", "HowKteam");
/*Duyệt qua các phần tử trong Dictionary.
* Vì mỗi phần tử là 1 KeyValuePair nên ta chỉ định kiểu dữ liệu cho item là KeyValuePair
* Thử in ra màn hình cặp Key - Value của mỗi phần tử được duyệt.*/
foreach (KeyValuePair item in MyDic4) {
Console.WriteLine(item.Key + "\t" + item.Value); }
Việc truy xuất các phần tử trong Dictionary giống như truy xuất các phần tử mảng nhưng thông qua Key. Ví dụ:
Console.WriteLine(MyDic4["FE"]); Trong đó:
MyDic4 là tên của Dictionary.
"FE" là tên Key cần truy xuất. Lưu ý là phải cùng kiểu dữ liệu TKey đã được chỉ
định lúc khởi tạo Dictionary.
MyHash["FE"] sẽ lấy ra giá trị Value tương ứng với Key trên.
Khi thao tác với Hashtable nếu như truy xuất đến phần tử có Key không tồn tại sẽ
không báo lỗi nhưng với Dictionary thì không phải vậy. Nếu ta truy xuất đến Key không
tồn tại sẽ nhận được lỗi sau:
Khác biệt giữa Dictionary và HashTable trong C# HASHTABLE DICTIONARY
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 105
Threadsafe - Hỗ trợ multi threading không Không hỗ trợ. đụng độ tài nguyên
Cặp Key - Value lưu kiểu object
Phải xác định cụ thể kiểu dữ liệu của cặp Key - value
Truy xuất phần tử không tồn tại trong
Truy xuất phần tử không tồn tại trong Dictionary sẽ
Hashtable sẽ không báo lỗi suy ra return null báo lỗi
Hiệu quả cho dữ liệu lớn
Không hiệu quả cho dữ liệu lớn
Các phần tử được sắp xếp lại mỗi khi thêm
Các phần tử nằm theo thứ tự được thêm vào.
hoặc xóa các phần tử trong Hashtable. Tìm kiếm nhanh hơn. Tìm kiếm chậm hơn. Tuple
Tuple
là một kiểu dữ liệu có cấu trúc, giúp lưu trữ các dữ liệu phức tạp mà không cần
phải tạo ra một struct hay class mới.
C# cung cấp cho chúng ta: 8 lớp generic public class Tuple public class Tuple public class Tuple public class Tuple public class Tuple public class Tuple public class Tuple public class Tuple
Mỗi lớp Tuple<> đã được định nghĩa sẵn các Property có tên Item1, Item2, Item3,…
tương ứng với các kiểu dữ liệu T1, T2, T3,… được truyền vào. Hình ảnh sau là một ví dụ:
Đây là hình ảnh lớp Tuple<T1, T2>, khi ta truyền 2 kiểu dữ liệu vào T1, T2 thì trong
lớp này sẽ có 2 Property Item1, Item2 có kiểu dữ liệu tương ứng.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 106
 Một static class Tuple. Trong lớp sẽ chứa các hàm Create<> nhằm khởi tạo một Tuple:
Mỗi hàm Create sẽ tương ứng tạo 1 đối tượng kiểu generic Tuple.
Sử dụng Tuple như thế nào khi đã có struct và class?
Trường hợp đầu tiên là khi viết một phương thức và muốn trả về 1 lúc nhiều giá trị.
Lúc này Tuple sẽ dễ dàng giải quyết mà không cần phải tạo thêm struct hay class. 
Trong nhiều trường hợp khác muốn tạo nhanh 1 đối tượng với 1 vài thuộc tính và chỉ
sử dụng 1 lần thôi thì việc dùng struct hoặc class là rất lãng phí, làm chương trình trở
nên dài dòng hơn. Khi đó Tuple được sử dụng như một phương án thay thế tốt hơn vì
có sẵn rồi giờ lấy ra dùng thôi không cần khai báo gì nữa. 
Ngoài ra, Tuple đã override sẵn: o
Phương thức Equals (phương thức dùng để so sánh 2 đối tượng). o
Phương thức ToString (Phương thức chuyển giá trị đối tượng sang chuỗi). o
Phương thức GetHashCode (Phương thức trả về mã băm của một đối tượng,
dùng để hỗ trợ so sánh 2 đối tượng).
Từ đó chúng ta chỉ việc sử dụng mà không cần phải viết lại những phương thức này. Cách sử dụng Tuple
Đầu tiên là khởi tạo một đối tượng Tuplechúng ta có 2 cách:
Cách 1: Thông qua phương thức Create trong lớp Tuple:
// Khởi tạo Tuple thông qua phương thức Create
var MyTuple = Tuple.Create<int, string>(1, "HowKteam");
Ví dụ trên có nghĩa là tạo ra 1 đối tượng (Tuple) có 2 thuộc tính bên trong:
 Thuộc tính thứ nhất (Item1) có kiểu dữ liệu là int và khởi tạo giá trị là 1.
 Thuộc tính thứ hai (Item2) có kiểu dữ liệu là string và khởi tạo giá trị là “HowKteam”.
Cách 2: Thông qua Constructor của các lớp Generic: // Kh i ở t o
ạ Tuple thông qua constructor c a ủ các l p ớ generic
var MyTuple2 = new Tuple<int, string>(2, "Kteam");
Ví dụ này cũng được hiểu tương tự như trên.
 Khi bạn khởi tạo Tuple có bao nhiêu kiểu dữ liệu thì sẽ có bấy nhiêu thuộc
tính Item tương ứng. Hình dưới đây là minh hoạ cho Tuple<T1, T2>:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 107
Ví dụ: in ra giá trị các thuộc tính đã khởi tạo ở trên:
// Lấy giá trị bên trong Tuple
Console.WriteLine(" ID: {0}, Name: {1}",MyTuple.Item1, MyTuple.Item2);
Kết quả: khi chạy chương trình là:
Lúc này đã hiểu Tuple cũng chỉ là một lớp bình thường đã được định nghĩa sẵn. Vì vậy
để giải quyết bài toán trả về nhiều giá trị cùng một lúc, đơn giản chỉ cần khai báo kiểu trả về là 1 Tuple. Ví dụ về tuple
Xét ví dụ sau:
Viết chương trình trả về 3 giá trị là ngày, tháng, năm hiện tại của hệ thống. 
Đầu tiên mình sẽ viết phương thức trả về 3 giá trị ngày, tháng, năm: ///
/// Phương thức trả về 1 Tuple có 3 thuộc tính (cả 3 đều có kiểu dữ liệu là int) /// ///
static Tuple GetCurrentDayMonthYear() {
DateTime now = DateTime.Now; // lấy ngày giờ hiện tại của hệ thống.
/* Sử dụng Constructor của Tuple<> để trả về hoặc có thể sử dụng phương thức Create đã trình bày ở trên. */
return new Tuple(now.Day, now.Month, now.Year);
} Trong chương trình chính ta gọi và sử dụng như sau:
Có thể thử không gọi Item1, Item2, Item3 mà sử dụng phương thức ToString() để xem kết quả thế nào nhé! L
/* ưu ý: Các thuộc tính Item1, Item2, Item3,… là các thuộc tính chỉ đọc nghĩa là chỉ có thể gọi * M ra ì để nh lấy dùng gi var á t đểr ị c C hứ # tự không t nhận di hể ện gá kiể n gi u dữ á l t iệrị c u. ho chúng được.
* Bạn có thể khai báo tường minh kiểu dữ liệu là Tuple I C */ ollection
ICollection
là một interface trong bộ các interface được định nghĩa sẵn của .NET Frame var w nor o k w .= GetCurrentDayMonthYear(); T C rư on ớsco khi le. và Wr o nội iteL dung c ine(" hí Danh y: c húng t {0}, aM c o ùng t nth: ì m { hi 1} ể , u t Y ạ e ia s r a:o .N {2 E }"T, l ạni đị ow nh nghĩ .Item1 a , s ẵn nhiề n uo iwnt .Ierfa temc2e đế , n n như ow.I vậ tem y3.);
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 108
 Đưa ra những chuẩn mực chung, mỗi interface thể hiện cho một hoặc một vài tính chất nào đó.
 Khi bạn muốn cấu trúc dữ liệu của mình có tính chất nào đó (ví dụ như bạn muốn cấu
trúc dữ liệu mình là 1 dạng collection, hay cấu trúc dữ liệu của mình có khả năng duyệt
bằng từ khoá foreach,…) thì bạn chỉ cần thực thi đúng interface đã được cung cấp sẵn là được.
 Ngoài ra, việc định nghĩa sẵn các interface như là một cách để lập trình viên có
thể tuỳ chỉnh một số chức năng bên trong .NET.
 Phân tích 1 chút. Ban đầu hàm Sort trong lớp ArrayList chỉ thực hiện sắp xếp
tăng dần (theo số hoặc theo bảng chữ cái) và để ý kỹ bạn sẽ thấy điều này:
Hàm Sort có thể nhận vào 1 đối tượng có thực thi interface IComparer. Đây chính là
chìa khoá để chúng ta có thể tuỳ chỉnh code.
Để thực hiện sắp xếp cho dù là thuật toán nào thì người ta cũng cần so sánh 2 đối tượng
xem đối tượng nào bé hơn thì đứng trước. Và việc so sánh này được viết bên trong 1 hàm tên là Compare.
Trong hàm Sort mặc định thì hàm Compare chỉ so sánh được số hoặc chữ. Vậy nếu như
ta có thể tuỳ chỉnh hàm Compare này và đưa ra cách so sánh riêng của mình thì danh sách
sẽ được sắp xếp theo ý của mình. Và cách tuỳ chỉnh hàm Compare chính là tạo một đối
tượng có thực thi interface IComaparer.
Mô phỏng lại nội dung bên trong hàm Sort để hình dung được tại sao truyền vào 1 đối
tượng thực thi interface IComparer thì có thể thay đổi được cách sắp xếp:
public void Sort(IComparer comparer) {
/* Mình sử dụng thuật toán sắp xếp đơn giản nhất nhé.
* Còn thực sự hàm Sort của .NET sử dụng thuật toán khác nha. */ int count = array.Count;
for (int i = 0; i < count - 1; i++) {
for (int j = i + 1; j < count; j++) {
/* Nếu phần tử i bé hơn phần tử j thì thực hiện hoán đổi vị trí 2 phần tử này */
if (comparer.Compare(array[i], array[j]) > 0) { object tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } } }
Để ý dòng if thôi là đủ. Rõ ràng họ không quan tâm hàm Compare viết gì trong đó họ
chỉ cần có hàm đó để họ gọi là được. Và để đảm bảo đối tượng của mình có
hàm Compare thì cái này do interface IComparer ràng buộc.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 109
ICollection là 1 interface có ý nghĩa “xác định kích thước, enumerator (bộ liệt kê) và
những phương thức đồng bộ cho những tập hợp không phải kiểu generic” hay nói
ngắn gọn đây là 1 interface thể hiện tính chất của 1 collection.
Interface ICollection yêu cầu chúng ta thực thi những Property, phương thức sau: 
Count: trả về số lượng phần tử của tập hợp. 
IsSynchronized SyncRoot: 2 property để làm cho thao tác đa luồng với tập hợp an toàn hơn. 
CopyTo(Array array, int index): phương thức thực hiện copy tập hợp ra 1 mảng, bắt
đầu từ vị trí index trong tập hợp. 
GetEnumerator(): Trả về 1 đối tượng kiểu IEnumerator.
Thực thi interface ICollection
Chúng ta sẽ cùng tổ chức 1 Collection đơn giản giống A
rrayList bằng cách thực thi các
interface cần thiết. Ví dụ này sẽ được thực hiện xuyên suốt cho các bài sau về interface.
public class MyArrayList : ICollection {
private object[] lstObj; // mảng giá trị
private int count; // số lượng phần tử
private const int MAXCOUNT = 100; // số lượng phần tử tối đa public MyArrayList() { count = -1;
lstObj = new object[MAXCOUNT]; } public MyArrayList(int count) { this.count = count; lstObj = new object[count]; }
public MyArrayList(Array array) { array.CopyTo(lstObj, 0); count = array.Length; }
public void CopyTo(Array array, int index) {
// thực hiện copy các phần tử trong lstObj từ vị trí index đến cuối sang mảng array. lstObj.CopyTo(array, index); } public int Count { get { return count; }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 110
Tạo một lớp MyArrayList và thực thi interface ICollection. Trong lớp MyArrayList có 1
mảng các object và 1 biến lưu trữ số lượng phần tử của mảng. Sau đó thực hiện viết lại
những property hoặc phương thức mà chúng ta biết bao gồm property Count, phương
thức CopyTo() và một số constructor cơ bản. Vì đây chưa phải là 1 collection hoàn thiện
nên không thể chạy chương trình xem thử được. Delegate
Delegate
là một biến kiểu tham chiếu (references) chứa tam chiếu tới một phương thức.
Tham chiếu của Delegate có thể thay đổi runtime (khi chương trình thực thi).
Delegate thường được dùng để triển khai các phương thức hoặc sự kiện call-back.
Cứ hiểu Delegate là một biến bình thường, biến này chứa hàm cần gọi. Sau này lôi ra
sài như hàm bình thường. Giá trị của biến Delegate lúc này là tham chiếu đến hàm. Có thể
thay đổi runtime khi chương trình đang chạy.
Delegate được dẫn xuất từ lớp System.Delegate trong C# Khai báo Delegate trong C#
Khai báo Delegate trong C# sẽ tương tự như khai báo một biến. Nhưng cần thêm từ
khóa Delegate để xác định đây là một Delegate. Đồng thời vì Delegate là để tham chiếu
đến một hàm, nên cũng cần khai báo kèm kiểu dữ liệu trả về của và tham số đầu
vào
của Delegate tương ứng với hàm tham chiếu. Công thức: delegate ể tr ả v > ề (); Ví dụ:
delegate
int MyDelegate(string s);
Lưu ý: Chữ delegate viết thường
Lúc này chúng ta đã tạo một Delegate có tên là MyDelegate. MyDelegate có kiểu trả về
là int, một tham số đầu vào là string.
MyDelegate lúc này có thể dùng làm kiểu dữ liệu cho mọi Delegate tới hàm tương ứng
kiểu trả về và tham số đầu vào.
Khởi tạo và sử dụng Delegate trong C#
Khi kiểu Delegate được khai báo, đối tượng Delegate phải được tạo với từ khóa new và
được tham chiếu đến một phương thức cụ thể. Phương thức này phải cùng kiểu trả về
và tham số đầu vào
với Delegate đã tạo.
Khi tạo một Delegate, tham số được truyền với biểu thức new được viết tương tự như
một lời gọi phương thức, nhưng không có tham số tới phương thức đó. Tức là chỉ
truyền tên hàm
vào thôi. Delegate sẽ tự nhận định hàm được đưa vào có cùng kiểu dữ liệu
trả ra và cùng tham số đầu vào hay không. Ví dụ: class Program {
delegate int MyDelegate(string s);
static void Main(string[] args) {
Console.OutputEncoding = Encoding.Unicode;
MyDelegate convertToInt = new MyDelegate(ConvertStringToInt);
string numberSTR = "35";
int valueConverted = convertToInt(numberSTR); Console.WriteLine("Giá tr
ị đã convert thành int: "+ valueConverted); Console.ReadLine(); }
static int ConvertStringToInt(string stringValue) { int valueInt = 0;
Int32.TryParse(stringValue, out valueInt);
Console.WriteLine("Đã ép ki u ể d ữ li u ệ thành công"); return valueInt; }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 111
Để hiểu rõ hơn về đoạn code trên: 
Ở đây tạo một hàm ConvertStringToInt làm nhiệm vụ là chuyển kiểu dữ liệu của
một số từ string sang int. 
Sử dụng Delegate bằng cách tạo một biến convertToInt có kiểu dữ liệu
là MyDelegate. convertToInt này mình new MyDelegate với tham số đầu vào là tên
hàm ConvertStringToInt (lưu ý chỉ tên hàm thôi). 
Có biến numberSTR kiểu string khởi tạo giá trị là 35. 
Tạo một biến valueConverted kiểu int khởi tạo nó bằng kết quả
gọi Delegate convertToInt với tham số truyền vào Delegate là biến numberSTR. 
Kết quả xuất ra màn hình Console là số 35.
Nhận thấy Delegate convertToInt sử dụng tương tự như một hàm bình thường.
Do MyDelegate đã khởi tạo đồng nhất kiểu dữ liệu trả về và tham số đầu vào với
hàm ConvertStringToInt nên convertToInt mới thỏa mãn điều kiện khởi tạo và sử dụng của hàm ConvertStringToInt này.
Vì sao cần Delegate? Khi bạn cần dùng một hàm như một biến ví dụ như tham số
truyền vào của một hàm, hàm call-back, event…
Multicast(đa hướng) một Delegate trong C#
Khi bạn cần thực hiện một chuỗi hàm với cùng kiểu trả về và cùng tham số đầu
vào mà không muốn gọi nhiều hàm tuần tự (chỉ gọi 1 hàm 1 lần duy nhất). Lúc này bạn
sẽ cần dùng đến Multicast Delegate.
Bản chất có thể làm một chuỗi Delegate cùng kiểu Delegate bằng cách dùng toán tử +.
Lúc này khi gọi Delegate sẽ thực hiện tuần từ các Delegate được cộng vào với nhau.
Có thể loại bỏ Delegate trong multicast bằng toán tử -. Ví dụ: class Program {
delegate int MyDelegate(string s);
static void Main(string[] args) {
Console.OutputEncoding = Encoding.Unicode;
MyDelegate convertToInt = new MyDelegate(ConvertStringToInt);
MyDelegate showString = new MyDelegate(ShowString);
MyDelegate multicast = convertToInt + showString; string numberSTR = "35";
int valueConverted = convertToInt(numberSTR); Console.WriteLine("Giá tr
ị đã convert thành int: " + valueConverted); Console.WriteLine("K t ế qu ả khi g i ọ multicast Delegate"); multicast(numberSTR); Console.ReadLine(); }
static int ConvertStringToInt(string stringValue) { int valueInt = 0;
Int32.TryParse(stringValue, out valueInt);
Console.WriteLine("Đã ép ki u ể d ữ li u ệ thành công"); return valueInt; }
static int ShowString(string stringValue) {
Console.WriteLine(stringValue); return 0; } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 112
Dùng lại ví dụ của phần trước.
Tạo thêm hàm ShowString với mục dích là xuất ra màn hình Console chuỗi truyền vào.
Mình tạo thêm 2 Delegate là showString tham chiếu tới hàm ShowString và multicast là
kết quả cộng của 2 Delegate convertToInt và showString .
Gọi Delegate multicast để thực hiện 1 lần 2 delegate tuần tự và convertToInt và showString. Console.WriteLine("K t ế qu ả khi g i ọ multicast Delegate"); multicast(numberSTR);
Khi cần loại bỏ Delegate trong multicast bạn chỉ việc trừ Delegate ra
multicast = multicast - showString;
Dùng Delegate cho call-back function
Như đã nói ở trên, Delegate cũng là một biến. Vậy nên, có thể truyền Delegate vào hàm
làm parameter như biến bình thường. Lúc này Delegate này sẽ được gọi là call-back
function
. Mục đích của việc này là hàm nhận call-back function là param có thể
gọi Delegate được đưa vào khi nào cần như ví dụ sau:
delegate int MyDelegate(string s);
static void Main(string[] args) {
Console.OutputEncoding = Encoding.Unicode;
MyDelegate showString = new MyDelegate(ShowString); NhapVaShowTen(showString); Console.ReadLine(); }
static void NhapVaShowTen(MyDelegate showTen) { Console.WriteLine("M i ờ nh p ậ tên c a ủ b n ạ :");
string ten = Console.ReadLine(); showTen(ten); }
static int ShowString(string stringValue) {
Console.WriteLine(stringValue); return 0; }
Như đã thấy, việc đã sử dụng Delegate làm call-back function thành công.
Ý nghĩa: mỗi khi người dùng nhập vào tên của mình thì sẽ gọi delegate Showstring để
hiền thị tên người dùng vừa nhập vào ra màn hình console. Vậy lúc này
hàm ShowString này hoàn toàn có thể được định nghĩa do người dùng mà không cần can
thiệp vào code của hàm NhapVaShowTen. Event Event là D
elegate với mục đích để cho lớp khác hoặc đối tượng cha của đối tượng
hiện tại ủy thác(định nghĩa) hàm vào trong đó.
Mục đích chính của chuyện này là để thông báo lên cho đối tượng cha biết mà xử lý. Khai báo Event trong C#
Khai báo Event trong C# sẽ tương tự như khai báo một biến. Nhưng biến này sẽ nhận
kiểu dữ liệu là Delegate đã được tạo trước đó. Cần có từ khóa event để chương trình biết đây là một biến event. Công thức: event ể delegate> ;
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 113 Ví dụ:
event
UpdateNameHandler NameChanged;
Ví dụ code đầy đủ: namespace Event_Voi_Delegate {
public delegate void UpdateNameHandler(string name); class Program {
static void Main(string[] args) { } } public class HocSinh {
public event UpdateNameHandler NameChanged; private string _Name; public string Name { get => _Name; set { _Name = value; } } } }
Lưu ý: Chữ event viết thường
Mục đích là mình mong muốn mỗi khi Name của class HocSinh thay đổi mình sẽ biết
và có thể code xử lý tương ứng.
Ta tạo một class HocSinh có filed là _Name kiểu dữ liệu là string. Được đóng gói thành
property Name.(Bạn để ý rằng mình cố ý tạo property và filed cùng tên nhưng khác nhau là
filed có dấu _ phía trước tên. Như vậy vừa dễ quản lý vừa tiện cho việc code)
Lúc này chúng ta đã tạo một Delegate có tên là UpdateNameHandler cùng cấp
với class Program và class HocSinh . UpdateNameHandler có kiểu trả về là void, một tham
số đầu vào là string. Ta cần tạo Delegate ở phạm vi này là vì muốn có thể được dùng cả
trong class HocSinh và class Program.
Ta tạo event NameChanged thuộc class HocSinh lúc này có kiểu dữ liệu là
Delegate UpdateNameHandler . Lưu ý cách đặt tên event thể hiện được tính chất là Name
Changed(đã thay đổi). Lưu ý: event phải public.
Khởi tạo và sử dụng Delegate trong C#
Event phải được ủy thác từ đối tượng cha của đối tượng chứa event . Bằng
cách += hàm Delegate tương ứng vào event của đối tượng(Tương tự có thể loại bỏ bằng cách -=). Ví dụ:
HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged;
Hàm Hs_NameChanged lúc này được tự tạo ra bằng cách gõ cú pháp sau và nhấn phím tab một lần: hs.NameChanged +=
Hàm Hs_NameChanged lúc này được tự tạo ra bên dưới hàm Main với kiểu trả về và
tham số đầu vào tương ứng với Delegate UpdateNameHandler.
Hoặc bạn cũng có thể dùng anonymous method trong tình huống này:
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 114 hs.NameChanged += (name) => { Console.WriteLine("Tên m i ớ : " + name); };
Trong phương thức set của property Name. Mình sẽ gọi event NameChanged. Nhưng
nếu như NameChanged chưa từng được ủy thác thì khi gọi sẽ bị null exception. Nên mình
sẽ kiểm tra NameChanged có null hay không? Nếu không null thì sẽ gọi event như hàm và
truyền param tương ứng vào là Name. Code hoàn chỉnh: namespace Event_Voi_Delegate {
public delegate void UpdateNameHandler(string name); class Program {
static void Main(string[] args) {
Console.OutputEncoding = Encoding.Unicode; HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged; hs.Name = "Kteam"; Console.WriteLine("Tên t ừ class: " + hs.Name); hs.Name = "HowKteam.com"; Console.WriteLine("Tên t ừ class: " + hs.Name); Console.ReadLine(); }
private static void Hs_NameChanged(string name) { Console.WriteLine("Tên m i ớ : " + name); } } public class HocSinh {
public event UpdateNameHandler NameChanged; private string _Name; public string Name { get => _Name; set { _Name = value; if(NameChanged != null) { NameChanged(Name); } } } } }
Mỗi khi set lại giá trị cho hs.Name thì hàm Hs_NameChanged đã được gọi ngay sau đó.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 115 vent chuẩn .Net
Event chuẩn .Net
là event với Delegate nhưng thỏa mãn các điều kiện:
 Delegate có kiểu trả về là void
 Delegate có hai tham số, tham số thứ nhất có kiểu dữ liệu là object, tham số thứ hai có
kiểu EventArgs. object chính là đối tượng phát sinh sự kiện, EventArgs chính
là class giữ thông tin mà đối tượng gửi kèm trong quá trình phát sinh sự kiện.
 Lúc này thay vì chúng ta dùng Delegate do chúng ta tự tạo thì .Net có sẵn Delegate tên
là EventHandler theo chuẩn ở trên.
Vậy các event có sẵn trong .Net như Console.CancelKeyPress hay nhiều event khác nữa
Các event này thường được đánh ký hiệu là tia sét:
Cách dùng Event chuẩn .Net trong C#
Với mong muốn tạo ra một class HocSinh để quản lý Tên của học sinh. Vì muốn biết
khi nào tên của học sinh thay đổi sẽ ghi lại thành log sau này đối chiếu lịch sử thay đổi này.
Chúng ta sẽ tạo một Event NameChanged với Delegate là EventHandler có sẵn của .Net và
ủy thác việc ghi log lại.
Mình cũng đóng gói event mình sẽ tạo. Nhưng với event thay vì get set thì sẽ
là add và remove. Đồng thời tạo hàm OnNameChanged để thông báo event đã có sự thay đổi.
Lưu ý: sử dụng _NameChanged chứ không phải nameChanged. Vì
filed_NameChanged chính là event thực sự. Còn event NameChanged chính là event nhận
là ủy thác add và remove mà thôi.
Chúng ta truyền mặc định this vào tham số đầu tiên để thể hiện class HocSinh gọi event
này, tạo một đối tượng EventArgs vào tham số thứ hai để thỏa mãn đầy đủ tham số cho
Delegate EventHandler. Sau này, bạn có thể dùng các tham số này ở phần sau.
Mỗi khi có sự thay đổi tên của học sinh thì event NameChanged được thông báo lên.
Vậy là chúng ta có thể biết để ghi ra màn hình cho biết tên học sinh đã thay đổi.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 116 class Program {
static void Main(string[] args) {
Console.OutputEncoding = Encoding.Unicode; HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged; hs.Name = "Tên l n ầ 1"; hs.Name = "Tên l n ầ 2"; hs.Name = "Tên cu i ố "; Console.ReadLine(); }
private static void Hs_NameChanged(object sender, EventArgs e) {
Console.WriteLine("Tên có thay đ i ổ ."); } } public class HocSinh { private string _Name; public string Name { get => _Name; set { _Name = value; OnNameChanged(); } }
private event EventHandler _NameChanged;
public event EventHandler NameChanged { add { _NameChanged += value; } remove { _NameChanged -= value; } } void OnNameChanged() { if(_NameChanged != null) {
_NameChanged(this, new EventArgs()); } } }
Dùng Event chuẩn .Net với tham số truyền vào
Chúng ta đã biết khi nào tên của học sinh thay đổi và thông báo. Nhưng không thể biết
học sinh đó đã đổi tên thành gì. Vậy để có thể biết được giá trị sau khi cập nhật là
gì? Chúng ta sẽ dùng tới tham số thứ hai đó là EventArgs.
Vì tham số thứ hai chỉ cần có kiểu dữ liệu là EventArgs nên chúng ta sẽ tạo một class
mới là NameChangedEventArgs kết thừa lại EventArgs.
Class này sẽ có Constructor với tham số đầu vào là tên mới của học sinh. Và có thuộc tính là Name public ra.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 117
Ngay tại vị trí khai báo event NameChanged và _NameChanged. Chúng ta sẽ
thêm generic NameChangedEventArgs vào sau EventHandler để thông báo rằng mình sẽ
dùng kiểu dữ liệu NameChangedEventArgs thay cho EventArgs.
Khi thông báo event được thực hiện trong hàm OnNameChanged. Chúng ta sẽ
thay new EventArgs thành new NameChangedEventArgs đồng thời truyền tên mới của học
sinh vào. Hàm OnNameChanged cũng phải thêm tham số đầu vào kiểu dữ liệu là string để
có thể truyền tên mới của học sinh vào. Đồng thời cũng phải truyền value vào khi gọi
hàm OnNameChanged tại hàm set của Name.
Phía ủy thác event cũng phải thay đổi EventArgs thành NameChangedEventArgs có thể
lấy tên này ra dùng thông qua tham số thứ hai đang có tên là e. chúng ta sẽ thông báo ra
nên mới của học sinh là gì bằng cách cộng thêm chuỗi e.Name. class Program {
static void Main(string[] args)
{ Console.OutputEncoding = Encoding.Unicode; HocSinh hs = new HocSinh();
hs.NameChanged += Hs_NameChanged; hs.Name = "Tên l n ầ 1"; hs.Name = "Tên l n ầ 2"; hs.Name = "Tên cu i ố "; Console.ReadLine(); }
private static void Hs_NameChanged(object sender, NameChangedEventArgs e) {
Console.WriteLine("Tên có thay đ i ổ : " + e.Name); } } public class HocSinh { private string _Name; public string Name { get => _Name; set
{ _Name = value; OnNameChanged(value); } }
private event EventHandler _NameChanged;
public event EventHandler NameChanged { add { _NameChanged += value; } remove { _NameChanged -= value; } }
void OnNameChanged(string name) { if(_NameChanged != null)
{ _NameChanged(this, new NameChangedEventArgs(name)); } } }
public class NameChangedEventArgs : EventArgs {
public string Name { get; set; }
public NameChangedEventArgs(string name) { Name = name; } }
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 118
Multi threading là gì? Vì sao cần Multi threading
Multi threading
có thể hiểu là một kỹ thuật để cùng một lúc xử lý nhiều tác vụ. Bản
chất chương trình C# được tạo ra sẽ có hai luồng chạy chính. Luồng thứ nhất
là MainThread(luồng chính của chương trình mặc định là hàm Main) và UIThread(luồng cập nhật giao diện).
Các code xử lý mà chúng ta code chính là nằm trên luồng MainThread. Và chương
trình sẽ thực hiện tuần tự từng dòng code từ trên đi xuống. Nên nếu chúng ta có 3 hàm
xử lý cần chạy cùng lúc là không thể. Vì chương trình phải đợi hàm trước đó xử lý
xong
mới đến hàm kế tiếp.
Vậy để giải quyết việc cùng một lúc, có thể xử lý nhiều tác vụ, multi threading ra đời.
Trong C# chúng ta có thể dùng Thread để thực hiện đa luồng.(Cứ tưởng tượng rằng
chúng ta sẽ làm cho chương trình không chỉ còn 2 luồng chạy cơ bản song song mà có
thêm n luồng do chúng ta tạo ra và chạy song song nhau)
Cách dùng Thread trong C#
Class Thread nằm trong thư viện System.Threading của .Net. Để có thể sử
dụng Thread thì chúng ta sẽ using System.Threading.
Chúng ta tạo một hàm DemoThread với mục đích là giả lập lại một hàm xử lý mất thời
gian 5 giây. Ở đây mình dùng Thread.Sleep để mô phỏng lại việc sẽ tiêu tốn một giây cho
mỗi lần lặp xử lý với 5 lần lặp. Thread.Sleep có nhiệm vụ làm cho luồng hiện tại ngủ theo
thời gian được cài đặt. class Program {
static void Main(string[] args) { DemoThread(); DemoThread(); DemoThread(); Console.ReadLine(); } static void DemoThread() {
// Thực hiện vòng lặp 5 lần. Mỗi lần tốn 1 giây
for (int i = 0; i < 5; i++) {
// Làm gì đó tốn 1s. Dùng Thread.Sleep để luồng hiện tại ngủ theo thời gian được cài đặt.
// Mục đích để giả lập độ trễ của code xử lý
Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(i); } } }
Kết quả có thể thấy màn hình Console sẽ in ra giá trị của I sau mỗi 1 giây. Sau 5 giây
thì hoàn thành. Vậy nếu chúng ta gọi 3 hàm DemoThread này liên tục sẽ tiêu tốn 15s để hoàn thành chương trình.
Bây giờ chúng ta sẽ cho mỗi hàm demo vào một luồng riêng biệt để thực hiện. Lúc này
tốc độ sẽ tăng lên đáng kể vì 3 luồng chạy song song nhau. Chúng ta dùng cách tạo và
gọi Thread và sử dụng tối ưu nhất để bạn dễ tiếp cận. Bạn có thể tìm hiểu thêm về ThreadStart nếu muốn.
Vì muốn biết giá trị của i được in ra từ hàm nào nên mình truyền thêm tham số cho
hàm DemoThread và in kèm giá trị đó.
Thread chỉ bắt đầu chạy khi bạn gọi hàm Start.
Sau khi thực hiện thì 5s sau chương trình đã hoàn thành.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 119 class Program Hi ệ n t ạ
{i bạn thấy kết quả in ra vẫn theo một trật tự vì mình dùng Thread.Sleep bên trong hàm D e m oT hr e s a t d. B ati â c y gi vo ờ i m d ì M nh s ain ẽ ( t s ha tr y i đổi ng[ hà ] m ar nà gs y
) theo hướng không sleep nữa và cho tăng s ố l ầ n l ặ p l { ên.
/* Tạo một Thread t với anonymous function và gọi hàm DemoThread bên trong
* Thread chỉ bắt đầu chạy khi gọi hàm Start
* Bạn có thể thực hiện một hàm hay nhiều dòng code ở bên trong anonymous function này */
Thread t = new Thread(()=> { DemoThread("Thread 1"); }); t.Start();
Thread t2 = new Thread(() => { DemoThread("Thread 2"); }); t2.Start();
Thread t3 = new Thread(() => { DemoThread("Thread 3"); }); t3.Start();
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 120 class Program {
static void Main(string[] args) {
/* Tạo một Thread t với anonymous function và gọi hàm DemoThread bên trong
* Thread chỉ bắt đầu chạy khi gọi hàm Start
* Bạn có thể thực hiện một hàm hay nhiều dòng code ở bên trong anonymous function này */
Thread t = new Thread(()=> { DemoThread("Thread 1"); }); t.Start();
Thread t2 = new Thread(() => {
Bạn nhận thấy rằng thứ tự thực hiện của 3 luồng này lộn xộn và giá trị i in ra cũng vậy.
Lý do là vì 3 luồng này đang chạy song song độc lập nhau.
Cách dùng Multi Threading trong C#
Vậy là bạn đã có thể tắc tốc độ xử lý chương trình của mình bằng Thread. Nhưng mình
còn có thể tăng tốc độ xử lý lên nhiều nữa bằng kỹ thuật Multi Threading.
Vậy bây giờ mình muốn thực hiện gọi hàm DemoThread với 5 lần thì mình bắt buộc
phải dùng vòng lặp thay vì tạo tay 5 biến Thread để Start.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 121
Ý tưởng đơn giản là tạo một vòng lặp 5 lần. Mỗi lần tạo ra một Thread mới và đưa biến i vào hàm DemoThread. class Program {
static void Main(string[] args) {
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() => { DemoThread("Thread " + i); }); t.Start(); } Console.ReadLine(); }
static void DemoThread(string threadIndex) {
Bạn có thể nận thấy. Thiếu đâu Thread 1 và Thread 3.
Lý do là vì Thread không Start ngay khi bạn gọi hàm Start. Mà hệ điều hành sẽ tự điều
phối sao cho hợp lý để tối ưu tài nguyên. Trong code bạn đưa vào là biến i. Có
thể DemoThread được Start ở lần lặp sau đó, suy ra biến i lúc này có giá trị là 2. Nên
thành ra Thread 2 được thực hiện tới 2 lần.
Để giải quyết tình trạng Missed Thread này. Bạn có thể dùng foreach hoặc tạo một biến class Program {
static void Main(string[] args) {
for (int i = 0; i < 5; i++) { int tempI = i;
Thread t = new Thread(() => {
DemoThread("Thread " + tempI); }); t.Start(); } Console.ReadLine(); }
static void DemoThread(string threadIndex) {
for (int i = 0; i < 5; i++) {
Console.WriteLine(threadIndex + " - " + i); } } }
tạm để lưu giá trị i bên ngoài Thread rồi dùng biến đó thay cho i.
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 122
T R Ư Ờ N G Đ Ạ I H Ọ C K Ỹ T H U Ậ T C Ô N G N G H Ệ C Ầ N T H Ơ | 123
Lưu ý khi dùng Thread trong C#
Nếu Thread của bạn xử lý nhiều hoặc lặp vô tận. Bạn nên set thuộc
tính IsBackground cho Thread là true để khi chương trình của bạn tắt, Thread của bạn cũng
sẽ được giải phóng. Còn nếu IsBackground = false. Thì chương trình của bạn phải
đợi Thread này chạy xong thì mới có thể kết thúc. t.IsBackground = true;
Nếu bạn code MultiThreading ở các ngôn ngữ khác như W inform hay W PF thì nên đưa
code xử lý liên quan đến giao diện vào trong Invoke để tránh đụng độ tài nguyên khi sử
dụng.(cấu trúc Invoke có thể khác một chút tùy vào ngôn ngữ bạn dùng)  Invoke trong Winform:
// Invoke cần một đối tượng giao diện để Invoke. Ở đây mình dùng this là form hiện tại.
// Bạn có thể dùng control bất kỳ thay thế
this.Invoke(new Action(()=> { // code của bạn }));
// Invoke cần một đối tượng giao diện để Invoke.  Invoke trong WPF:
// Ở đây mình dùng this là Window hiện tại.
// Bạn có thể dùng control bất kỳ thay thế
// WPF cần Dispathcer để Invoke
this.Dispatcher.Invoke(new Action(()=> { // code của bạn }));