Trong bài này chúng ta sẽ tìm hiểu:
- Khai báo, tạo và sử dụng các biến mảng (array)
- Khai báo, tạo và sử dụng các lớp collection
- Mảng như là tham số của phương thức
Mảng (array)
Mảng là một danh sách chứa các phần tử cùng kiểu như các số nguyên, chuỗi,…Mỗi phần tử trong mảng được xác định tại một vị trí được gán chỉ số (index). Phần tử ở vị trí đầu tiên sẽ có chỉ số là 0, phần tử cuối cùng trong mảng có n phần tử có chỉ số là n – 1. Khai báo, truy cập (gán giá trị, hiển thị ) các phần tử mảng dùng dấu ngoặc vuông.
Cú pháp
kiểu_dữ_liệu[] tên_mảng = new kiểu_dữ_liệu[kích cỡ hay số phần tử trong mảng];
Ví dụ khai báo mảng arrInt chứa hai số nguyên:
int[] arrint = new int [2];
Gán giá trị cho các phần tử trong mảng ta dùng cú pháp
tên_mảng [chỉ số của phần tử] = giá trị;
Ví dụ gán các giá trị số nguyên cho hai phần tử trong mảng arrInt như sau:
arrInt [0] = 7; // Gán giá trị 7 cho phần tử thứ nhất trong mảng arrInt [1] = 19; // Gán 19 cho phần tử thứ hai
Có thể gán các phần tử khi khai báo mảng, ví dụ:
int[] arrInt = new int [2]{7,9};
hay đơn giản hơn
int[] arrInt = {7,9};
Để truy cập phần tử trong mảng ta dùng cú pháp
tên_mảng [chỉ số phần tử];
Khi khai báo mảng chúng ta thường gán cho mảng một kích cỡ nào đó mặc định, nhưng trong thực tế có thể giảm hoặc tăng kích thước ban đầu.
Trong C# không có từ khoá để làm mới lại kích cỡ mảng nhưng chúng ta có thể dùng phương thức tĩnh Resize của lớp Array. Ví dụ với mảng arrInt như sau:
int[] arrInt = new int[2]; arrInt [0] = 7; //Gán giá trị 7 cho phần tử thứ nhất trong mảng arrInt [1] = 19; //Gán 19 cho phần tử thứ hai arrInt [2] = 5; // Gán 5 cho phần tử thứ ba
Nếu chúng ta truy cập đến phần tử thứ 3 như lệnh sau:
MessageBox.Show(arrInt[2].ToString());
Khi thực thi chương trình sẽ phát sinh lỗi sau:
Chúng ta có thể bắt ngoại lệ IndexOutOfRangeException bằng lệnh try catch:
try { int[] arrInt = new int[2]; arrInt[0] = 7; arrInt[1] = 19; arrInt[2] = 5; MessageBox.Show(arrInt[2].ToString()); } catch (IndexOutOfRangeException) { MessageBox.Show("Mảng chỉ chứa tối đa hai phần tử!"); }
C# cung cấp phương thức tĩnh Resize của lớp Array để làm mới lại kích cỡ mảng. Ví dụ trên có thể thay đổi lại như sau:
int[] arrInt = new int[2]; arrInt [0] = 7; //Gán giá trị 7 cho phần tử thứ nhất trong mảng arrInt [1] = 19; //Gán 19 cho phần tử thứ hai Array.Resize(ref arrInt,3); arrInt [2] = 5; // Gán 5 cho phần tử thứ ba MessageBox.Show(arrInt[2].ToString());// 5
Tạo một mảng kiểu ngầm định
Khi khai báo một mảng kết hợp với khởi tạo giá trị cho các phần tử chúng ta có thể không cần khai báo kiểu bằng cách dùng từ khoá var như ví dụ sau:
var names = new[]{"John", "Diana", "James", "Francesca"};
Trong ví dụ trên, trình biên dịch C# sẽ ngầm hiểu mảng names chứa các phần tử kiểu string.
Khi khai báo mảng theo kiểu trên chúng ta cần lưu ý các phần tử phải cùng kiểu, khai báo sau sẽ bị lỗi
var names = new[]{"John", "Diana", 3, 5};
Lỗi có thể trông như sau:
Khai báo mảng kiểu ngầm định có ích trong trường hợp khai báo mảng chứa các phần tử kiểu nặc danh (anonymous type). Ví dụ sau khai báo một mảng chứa các đối tượng nặc danh, mỗi đối tượng chứa hai trường Name và Age như sau:
var names = new[] { new { Name = "John", Age = 44 }, new { Name = "Diana", Age = 45 }, new { Name = "James", Age = 17 }, new { Name = "Francesca", Age = 15 } };
Duyệt mảng
Chúng ta có thể duyệt qua các phần tử của một mảng nhờ các lệnh lặp. Ví dụ dùng lệnh for như sau:
int[] arrInt = {2,4,8,9}; for (int i = 0; i < arrInt.Length; i++) { int val = arrInt[i]; MessageBox.Show(val.ToString()); }
Cũng có thể dùng lệnh foreach:
int[] arrInt = {2,4,8,9}; foreach(int val in arrInt) MessageBox.Show(val.ToString());
Lệnh lặp foreach rất hữu ích khi duyệt qua một mảng chứa các phần tử là các đối tượng, ví dụ:
var names = new[] { new { Name = "John", Age = 44 }, new { Name = "Diana", Age = 45 }, new { Name = "James", Age = 17 }, new { Name = "Francesca", Age = 15 } }; foreach (var familyMember in names) { MessageBox.Show(familyMember.Name + " " + familyMember.Age.ToString()); }
Sao chép các mảng
Các mảng là các kiểu tham chiếu và một mảng là một thể hiện (intance) của lớp System.Array. Một biến mảng chứa một tham chiếu đến một thể hiện mảng. Điều này có nghĩa rằng khi chúng ta sao chép một biến mảng, sẽ có hai tham chiếu đến cùng một thể hiện mảng. Ví dụ:
int[] num = {5, 7, 8}; int[] num_copy = num;// num và num_copy tham chiếu đến cùng một thể //hiện mảng MessageBox.Show(num[0].ToString());// 5 MessageBox.Show(num_copy[0].ToString());// 5
Nếu thay đổi num[0] thì num_copy[0] cũng thay đổi như ví dụ:
int[] num = {5, 7, 8}; // num và num_copy tham chiếu đến cùng một thể hiện mảng int[] num_copy = num; MessageBox.Show(num[0].ToString());// 5 MessageBox.Show(num_copy[0].ToString());// 5 num[0] = 2; MessageBox.Show(num[0].ToString());// 2 MessageBox.Show(num_copy[0].ToString());// 2
Nếu chúng ta muốn sao chép thể hiện mảng mà một biến mảng tham chiếu đến, chúng ta phải thực hiện theo hai bước:
– Tạo một thể hiện mảng mới (dùng từ khoá new) cùng kiểu và chiều dài với mảng đang sao chép
– Sao chép các phần tử từ mảng gốc sang mảng mới
Ví dụ về sao chép một thể hiện mảng:
int[] arrInt = { 9, 3, 7, 2 }; // mảng gốc int[] copy = new int[arrInt.Length]; // tạo một thể hiện mảng mới for (int i = 0; i < copy.Length; i++) { copy[i] = arrInt[i]; // sao chép từng phần tử từ mảng gốc sang //mảng mới }
Sao chép mảng là hoạt động rất phổ biến nên lớp System.Array cung cấp nhiều phương thức hữu ích phục vụ cho việc sao chép mảng như phương thức CopyTo, phương thức tĩnh Copy hay phương thức Clone.
Phương thức CopyTo sao chép nội dung từ một mảng sang một mảng khác theo chỉ số cho trước. Ví dụ sao chép nội dung từ mảng arrInt sang mảng copy bắt đầu từ chỉ số 0:
int[] arrInt = { 9, 3, 7, 2 }; int[] copy = new int[arrInt.Length]; arrInt.CopyTo(copy, 0);
Phương thức tĩnh Copy được dùng gần giống phương thức CopyTo, ví dụ:
int[] arrInt = { 9, 3, 7, 2 }; int[] copy = new int[arrInt.Length]; Array.Copy(arrInt, copy, copy.Length);
Có thể dùng phương thức Clone, chỉ phải lưu ý rằng phương thức Clone trả về một object nên cần phải ép kiểu như ví dụ:
int[] pins = { 9, 3, 7, 2 }; int[] copy = (int[])pins.Clone();
Mảng nhiều chiều
Cho đến thời điểm này chúng ta mới chỉ dừng lại ở mảng một chiều nhưng chúng ta có thể tạo ra mảng hơn một chiều. Có thể hình dung mảng một chiều chỉ là một dãy các phần tử, mảng hai chiều là một bảng các phần tử gồm các hàng và cột, mảng 3 chiều là một hình hộp, v.v.
Nếu khai báo mảng hai chiều, chúng ta có thể dùng cú pháp:
kiểu dữ liệu [,] biến mảng = new kiểu dữ liệu[số hàng, số cột];
Ví dụ tạo mảng matrix chứa các số nguyên gồm 2 hàng 3 cột (kích cỡ 2×3):
int[,] matrix = new int[2,3];
Truy cập các phần tử mảng hai chiều thông qua các “ô” (cell) – cặp giá trị (x,y) (x: chỉ số hàng, y:chỉ số cột). Ví dụ gán phẩn tử tại ô (1, 2) – hàng 1 cột 2:
matrix[1,2] = 9;
Khai báo mảng nhiều chiều với cú pháp sau:
kiểu dữ liệu [..,,,..] biến mảng = new kiểu dữ liệu[chiều 1, chiều 2,..,chiều n];
ví dụ tạo mảng cube 3 chiều 5x5x5 như sau:
int[,,] cube = new int[5,5,5];
truy cập đến phần tử mảng cube thông qua bộ 3 chỉ số, ví dụ:
cube[1,2,1] = 9;
Mảng của các mảng (jagged arrays)
C# cung cấp mảng của các mảng, nghĩa là mỗi hàng của mảng có chứa một mảng và kích cỡ các mảng khác nhau trong các hàng khác nhau.
Ví dụ sau khai báo một mảng một chiều chứa hai phần tử và mỗi phần tử chứa một mảng các số nguyên như sau:
int[][] jaggedArray; jaggedArray = new int[2][]; jaggedArray[0] = new int[5]; jaggedArray[1] = new int[3];
Chúng ta cũng có thể khai báo và khởi tạo một mảng của mảng như sau:
int[][] myJaggedArray = { new int[] {5, 7, 2}, new int[] {10, 20, 40}, new int[] {3, 25} };
Cách mảng myJaggedArray được cấp phát bộ nhớ có thể trực quan như hình sau:
Xem các ví dụ về mảng một chiều, mảng hai chiều và mảng của mảng.
Các lớp collection
Mảng (array) là rất hữu ích nhưng cũng có nhiều hạn chế – một trong số đó là sử dụng các chỉ số (index) để truy cập các phần tử. Tuy nhiên, mảng chỉ là một cách để lưu trữ các phần tử cùng kiểu. Microsoft .NET Framework cung cấp một vài lớp để tập hợp các phần tử cùng nhau theo những cách riêng biệt. Đó là các lớp collection được chứa trong namespace System.Collections và các namespace con.
Bên cạnh vấn đề chỉ số, có một khác biệt cơ bản khác giữa một mảng và một collection. Một mảng có thể chứa các phần tử kiểu giá trị (ví dụ int). Một collection chỉ chứa và trả về các phần tử như kiểu đối tượng (object).
Để hiểu rõ hơn, chúng ta sẽ có một so sánh giữa một mảng chứa các phần tử kiểu int và một mảng chứa các phần tử kiểu object. Mảng array chứa các phần tử kiểu int như sau:
int[] array = {9,7,3,2};
biến array chứa một tham chiếu (trên stack) đến một thể hiện trên heap như hình ảnh minh hoạ sau:
Nếu mảng array chứa các phần tử kiểu object như sau:
object[] array = {9,7,3,2};
các phần tử của array có kiểu object nên khi thêm một giá trị nguyên đến mảng này thì quá trình boxing sẽ xảy và ngược lại khi xoá một phần tử khỏi mảng thì quá trình unboxing sẽ xảy ra. Có thể hình dung một cách trực quan như sau:
Một số lớp collection cơ bản
Lớp ArrayList
Lớp ArrayList cung cấp nhiều đặc trưng hữu ích khắc phục các nhược điểm của mảng thông thường:
– Có thể xoá một phần tử từ một ArrayList bằng phương thức Remove. ArrayList sẽ tự động sắp xếp lại các phần tử.
– Có thể thêm một phần tử đến cuối ArrayList bằng phương thức Add. ArrayList sẽ tự động thay đổi kích cỡ nếu cần thiết.
– Có thể thêm phần tử đến vị trí bất kỳ trong ArrayList bằng phương thức Insert. ArrayList sẽ tự động thay đổi kích cỡ nếu cần thiết.
– Có thể truy cập đến các phần tử trong ArrayList bằng cách dùng dấu ngoặc vuông và chỉ số giống như mảng thông thường.
Ví dụ về sử dụng lớp ArrayList
– Khai báo một biến ArrayList (numbers) và tạo một thể hiện ArrayList (lưu ý phải khai báo namespace System.Collections)
using System.Collections; ..... ArrayList numbers = new ArrayList();
– Thêm các phần tử vào ArrayList bằng phương thức Add:
foreach(int number in new int[3]{5,8,9}){ numbers.Add(number); }
– Hiển thị các phần tử từ ArrayList:
// nếu dùng foreach chúng ta không cần ép kiểu int đến object foreach(int number in numbers){ Console.WriteLine(number); } //nếu dùng for chúng ta phải ép kiểu for (int i = 0; i < numbers.Count; i++) { int number = (int)numbers[i]; Console.WriteLine(number); }
Kết quả:
5
8
9
– Thêm số 6 vào giữa 5 và 8 bằng phương thức Insert:
// tham số thứ nhất là vị trí cần chèn, trong trường hợp này là chỉ số //1 // tham số thứ hai là giá trị cần chèn numbers.Insert(1, 6);
Kết quả:
5
6
8
9
– Xoá số 8 từ ArrayList dùng Remove và RemoveAt:
//dùng Remove bằng cách nhập phần tử cần xoá numbers.Remove(8); //dùng RemoveAt bằng cách nhập chỉ số của phần tử 8, tức là 2 numbers.RemoveAt(2);
Lớp Queue
Lớp Queue thực hiện cơ chế FIFO (First-In, First-Out) – Vào trước ra trước. Một phần tử sẽ được chèn vào cuối Queue và được lấy ra (xoá) từ đầu kia của Queue. Hai phương thức cơ bản của lớp Queue là Enqueue (thêm phần tử đến Queue) và Dequeue (xoá phần tử từ Queue).
Ví dụ với lớp Queue
– Khai báo một biến Queue (numbers) và tạo một thể hiện Queue (lưu ý phải khai báo namespace System.Collections)
using System.Collections; ..... Queue numbers = new Queue();
– Thêm các phần tử vào Queue bằng phương thức Enqueue:
foreach(int number in new int[3]{5,8,9}){ numbers.Enqueue(number); }
– Hiển thị các phần tử từ Queue:
foreach(int number in numbers){ Console.WriteLine(number); }
Kết quả:
5
8
9
– Xoá các phần tử từ Queue(lưu ý phải ép kiểu)
while (numbers.Count > 0){ int number = (int)numbers.Dequeue(); Console.WriteLine(number + " đã được xoá"); }
Kết quả
5 đã được xoá
8 đã được xoá
9 đã được xoá
Lớp Stack
Lớp Stack thực hiện cơ chế LIFO (Last-In, First Out)- Vào sau ra trước. Một phần tử được chèn vào đỉnh (top) của Stack và cũng được lấy ra từ đỉnh, do đó, phần tử được chèn vào trước sẽ được lấy ra sau. Hai phương thức cơ bản trong lớp Stack là Push(thêm phần tử vào Stack) và Pop(lấy phần tử ra từ Stack).
Ví dụ dùng Stack
– Khai báo một biến Stack (numbers) và tạo một thể hiện Stack (lưu ý phải khai báo namespace System.Collections)
using System.Collections; ..... Stack numbers = new Stack();
– Thêm các phần tử vào Stack bằng phương thức Push:
foreach(int number in new int[3]{5,8,9}){ numbers.Push(number); }
– Hiển thị các phần tử từ Stack:
foreach(int number in numbers){ Console.WriteLine(number); }
Kết quả (để ý thứ tự):
9
8
5
– Xoá các phần tử từ Stack(lưu ý phải ép kiểu)
while (numbers.Count > 0){ int number = (int)numbers.Pop(); Console.WriteLine(number + " đã được xoá"); }
Kết quả (để ý thứ tự)
9 đã được xoá
8 đã được xoá
5 đã được xoá
Lớp Hashtable
Khi truy cập đến các phần tử của mảng hay ArrayList, chúng ta thường dùng chỉ số, là một số nguyên, cùng với cặp ngoặc vuông (ví dụ arrInt[2] truy cập đến phần tử thứ 3 của mảng arrInt).Tuy nhiên, trong nhiều trường hợp, chỉ số không phải là một số nguyên mà là một giá trị kiểu khác như string, double, hay Time. Trong một số ngôn ngữ khác, như PHP, trường hợp này gọi là associate array. Trong C#, lớp Hashtable cung cấp chức năng này bằng cách duy trì hai mảng đối tượng, một để chứa các thành phần được gọi là khoá (keys) và một được dùng để chứa các thành phần được gọi là giá trị (values). Khi chúng ta thêm một cặp khoá/giá trị (key/value) đến Hashtable, Hashtable sẽ tự động theo dõi những khoá và giá trị tương ứng và cho phép chúng ta nhận giá trị tương ứng với khoá một cách nhanh chóng và dễ dàng.
Một số chú ý khi dùng Hashtable:
– Hashtable không chứa hai khoá trùng nhau.
– Chỉ dùng Hashtable khi có dung lượng bộ nhớ lớn vì kích cỡ của Hashtable sẽ tăng rất nhanh khi chúng ta thêm phần tử đến bảng băm.
– Kết quả nhận được khi duyệt qua Hashtable là một đối tượng DictionaryEntry.
Ví dụ dùng Hashtable
– Khai báo một biến Hashtable (ages) và tạo một thể hiện Hashtable (lưu ý phải khai báo namespace System.Collections)
using System.Collections; ..... Hashtable ages = new Hashtable();
– Thêm các phần tử vào Hashtable:
ages["Minh"] = 44; // key là Minh, value là 44 ages["Dung"] = 45; ages["Ha"] = 17; ages["Thuy"] = 15;
– Hiển thị các phần tử từ Hashtable thông qua đối tượng DictionaryEntry:
foreach (DictionaryEntry element in ages) { string name = (string)element.Key; int age = (int)element.Value; Console.WriteLine("Name: {0}, Age: {1}", name, age); }
Kết quả (để ý thứ tự):
Name: Ha Age: 17
Name: Thuy Age: 15
Name: Minh Age: 44
Name: Dung Age: 45
Bên cạnh lớp Hashtable, C# cung cấp lớp SortedList có chức năng tương tự lớp Hashtable nhưng các cặp khoá/giá trị sẽ hiển thị theo thứ tự tăng dần của giá trị khoá. Ví dụ trên dùng SortedList như sau:
SortedList ages = new SortedList (); ages["Minh"] = 44; // key là Minh, value là 44 ages["Dung"] = 45; ages["Ha"] = 17; ages["Thuy"] = 15; foreach (DictionaryEntry element in ages) { string name = (string)element.Key; int age = (int)element.Value; Console.WriteLine("Name: {0}, Age: {1}", name, age); }
Kết quả (để ý thứ tự):
Name: Dung Age: 45
Name: Ha Age: 17
Name: Minh Age: 44
Name: Thuy Age: 15
Qua các lớp collection cơ bản trên, chúng ta có thể so sánh mảng và collection như sau:
Array | Collection |
Khai báo kiểu của các phần tử | Không cần khai báo kiểu của các phần tử vì collection chứa các đối tượng |
Mảng có kích thước cố định, không thể tăng giảm | Collection có thể thay đổi kích thước tự động |
Mảng có thể có hơn một chiều | Collection chỉ có một chiều nhưng một collection có thể chứa một collection khác. |
Mảng (array) như là tham số của phương thức
Mảng có thể được sử dụng như là tham số của phương thức. Xét ví dụ phương thức Sum tính tổng của 3 số nguyên có thể được viết như sau:
public int Sum(int a, int b, int c) { return a + b + c; }
Sử dụng phương thức Sum:
int result = Sum(3,7,5); Console.WriteLine(result);
Tuy nhiên, nếu chúng ta muốn tính tổng của 100 số nguyên thì sẽ ra sao? Một giải pháp giúp xử lý vấn đề này là sử dụng mảng như là tham số của phương thức bằng cách dùng tham số params theo cú pháp:
Kiểu dữ liệu Tên phương thức(params Kiểu dữ liệu [] tên mảng) { ... }
Phương thức Sum có thể được viết lại như sau:
public int Sum(params int[] numbers) { int s = 0; foreach(int num in numbers) s = s + num; return s; }
Sử dụng phương thức Sum:
int[] numbers = new int[3]{3,5,7}; int result = Sum(numbers); Console.WriteLine(result);
Mảng cũng có thể được sử dụng khi nếu chúng ta chưa biết trước kiểu của các phần tử bằng cách sử dụng kiểu object theo cú pháp:
Kiểu dữ liệu Tên phương thức(params object[] tên mảng) { ... }
Ví dụ phương thức Display được khai báo như sau:
public void Display(params object[] paramList) { foreach (object num in paramList) MessageBox.Show(num.ToString()); }
Sử dụng Display
object[] list = new object[2]{"Minh",34}; Display(list);