Trong bài trước chúng ta đã tìm hiểu về con trỏ. Hôm nay chúng ta tìm hiểu về 2 kiểu dữ liệu sử dụng con trỏ để mô tả thông tin của nó là slice và map.
Slice
Slice (lát) cũng là kiểu dữ liệu mô tả dãy các đối tượng cùng kiểu dữ liệu như mảng nhưng không cố định chiều dài như mảng. Slice được khai báo là []<kiểu dữ liệu> nên nó như là mảng không xác định chiều dài. Thực ra mỗi khi khai báo 1 biến kiểu slice, Go sẽ tạo 1 mảng để chứa dữ liệu cho nó.
Một biến kiểu slice gồm 3 thành phần:
- Con trỏ tham chiếu đến mảng chứa các phần tử của slice.
- Chiều dài (số phần tử).
- Sức chứa (số phần tử tối đa, là chiều dài mảng chứa các phần tử).
Các hàm và toán tử xử lý một slice a:
- Hàm len(a) trả về số phần tử (chiều dài) của slice a.
- Hàm cap(a) trả về sức chứa của slice a.
- Toán tử a[i] truy xuất phần tử i của biến kiểu slice a với i chạy từ 0 đến len(a) - 1.
- Toán tử a[i:j] để tạo một slice mới từ slice a với 0 <= i <= j <= cap(a). slice mới này có j-i phần tử, phần tử đầu tiên là a[i] và cuối cùng là a[j-1]. Cả a và slice mới này đều trỏ đến cùng mảng lưu trữ được Go tạo ra để lưu giữ phần tử cho slice a, chỉ khác nhau vị trí bắt đầu. Nếu thiếu i trong toán tử trên, Go sẽ ngầm hiểu là bắt đầu từ 0. Ngược lại thiếu j, Go sẽ lấy len (a). Với a[:] ta có slice mới y chang như slice a.
- Hàm a = append(a, <phần tử mới>) giúp ta thêm phần tử cho slice a, do Go cung cấp sẵn. append có thể áp dụng thêm 1 hoặc nhiều phần tử (1 slice khác). Khi thêm, Go sẽ xác định số phần tử hiện tại của slice a và số phần tử sắp thêm liệu có vượt quá sức chứa hay không. Nếu có, Go sẽ tạo 1 mảng mới đủ để chứa và chép các phần tử cũ và mới vào đó. Nếu không, Go đơn giản chép thêm phần tử mới vào và tăng chiều dài lên tương ứng.
- Hàm copy(a, <slice nguồn>) sẽ giúp chép các phần tử của slice nguồn vào a. Hàm copy không làm thay đổi số phần tử của slice a. Nếu slice a dài hơn slice nguồn thì chép toàn bộ slice nguồn. Ngược lại, chép đến phần tử cuối cùng của slice a. Hàm copy trả về số phần tử chép được.
- 5 dòng đầu tiên là 5 kiểu khai báo khác nhau 1 biến kiểu slice:1 var a1 []int 2 var a2 []int = []int{1, 2, 3} 3 a3 := make([]int, 3) 4 a4 := []int{1:1, 3:2} 5 a5 := make([]int, 3, 5) 6 fmt.Println(a1) // [] 7 fmt.Println(a2) // [1 2 3] 8 fmt.Println(a3) // [0 0 0] 9 fmt.Println(a4) // [0 1 0 2] 10 fmt.Println(a5) // [0 0 0] 11 a1 = append(a1, 1) 12 a3 = append(a3, a2...) 13 fmt.Println(a1) // [1] 14 fmt.Println(a3) // [0 0 0 1 2 3] 15 fmt.Println(copy(a4, a5), a4) // 3 [0 0 0 2]
+ a1 khai báo được Go khởi tạo giá trị mặc định.
+ a2 khởi tạo có gán giá trị.
+ a3 khai báo nhanh qua hàm make có khai chiều dài nhưng không khai khả năng chứa.
+ a4 khai báo nhanh có gán giá trị cho các phần tử không liên tiếp.
+ a5 tương tự a3 nhưng có khai báo thêm sức chứa.
- 5 dòng tiếp theo in giá trị các biến kiểu slice lên màn hình:
+ a1 là slice rỗng, con trỏ đến mảng dữ liệu bên dưới là nil, chiều dài và sức chứa đều là 0.
+ a2 có chiều dài 3 nhờ khởi tạo 3 phần tử khi khai báo.
+ a3 có chiều dài 3 như tham số thứ 2 hàm make đã khai báo. Do không khai báo sức chứa nên Go gán mặc định bằng chiều dài là 3. Go gán giá trị mặc định là 0 cho các phần tử.
+ a4 có chiều dài là 4 do khi khai báo có khởi tạo phần tử 3. Các phần tử không gán giá trị khi khởi tạo, Go sẽ gán mặc định là 0.
+ a5 chỉ khác a3 ở chỗ có khai báo sức chứa là 5.
- Dòng 11 thực hiện tăng thêm 1 phần tử có giá trị là 1 cho biến a1. Lúc này a1 có chiều dài là 1 với 1 phần tử như kết quả thấy ở dòng 13.
- Dòng 12 thực hiện tăng nhiều phần tử cho a3 bằng cách bổ sung các phần tử a2. Cách viết a2... là bắt buộc thể hiện lấy các phần tử a2[0] đến a2[len(a2)-1]. Kết quả như dòng 14. Do a3 trước đó sức chứa chỉ có 3 nên chắc chắn Go sẽ tạo mảng khác rồi chép các phần tử a3 vào, sau đó chép tiếp các phần tử a2 theo thứ tự vào rồi gán các thông tin cho biến a3: con trỏ đến đầu mảng mới tạo, chiều dài sức chứa cùng là 6 (tổng chiều dài của a3 và a2). Mảng dữ liệu cũ mà a3 trỏ đến Go sẽ đưa vào diện dọn rác nếu không có slice nào khác trỏ đến.
- Dòng 15 là một dòng lệnh phức tạp: ban đầu lệnh copy sẽ được thực hiện, sau đó lệnh Println được thực hiện in ra màn hình kết quả trả về từ hàm copy và giá trị biến a4. Hàm copy thực hiện trước nên giá trị biến a4 đã đổi khi thực hiện lệnh in. Kết quả là chép thành công 3 phần tử từ a5 qua a4 và giá trị 3 phần tử đầu tiên của a4 đã bị thay bởi 3 phần tử a5.
Một số cách xử lý trên slice:
- Dòng đầu tiên khai báo slice a có 5 phần tử và gán giá trị từ 1 đến 5.1 var a []int = []int{1, 2, 3, 4, 5} 2 a1 := a[1:] 3 fmt.Println(a1) // [2 3 4 5] 4 a2 := a[:len(a)-1] 5 fmt.Println(a2) // [1 2 3 4] 6 a3 := a[:2] 7 a3 = append(a3, a[3:]...) 8 fmt.Println(a3) // [1 2 4 5] 9 fmt.Println(a1) // [2 4 5 5] 10 fmt.Println(a2) // [1 2 4 5] 11 fmt.Println(a) // [1 2 4 5 5]
- Dòng 2 tạo slice a1 dựa trên slice a, bỏ qua phần tử 0 của a. Kết quả in slice a1 như dòng 3.
- Dòng 4 tạo slice a2 dựa trên slice a, loại phần tử cuối của a. Kết quả in slice a2 như dòng 5.
- Dòng 6 và dòng 7 tạo slice a3 dựa trên slice a, loại phần tử 2 của a. Kết quả in slice a3 như dòng 8.
- Sau đó tại dòng 9 và 10, thử in lại a1 và a2 thì thật bất ngờ giá trị các phần tử đã đổi. Điều gì đã xảy ra? Nguyên do là cả 3 slice a1, a2, a3 đều trỏ đến mảng lưu trữ bên dưới của slice a nên khi có sự thay đổi giá trị của phần tử liên quan thì tất cả các slice đều bị đổi. Xem kết quả in ở dòng 11 chúng ta thấy có gì đó kỳ lạ ở slice a: mất phần tử giá trị 3 và xuất hiện thêm một phần tử giá trị 5 nữa. Kết quả này là do câu lệnh ở dòng 7. Sau câu lệnh dòng 6, slice a3 trỏ đến phần tử đầu tiên mảng dữ liệu bên dưới slice a, có 2 phần tử. Dòng lệnh 7 yêu cầu thêm 2 phần tử a[3](giá trị 4) và a[4] (giá trị 5) vào lát a3. Như vậy, sau lệnh append ở dòng 7, a3 có thêm 2 phần tử, tức là a3[2] = 4 và a3[3] = 5. Nhưng a3[2] cũng chính là a[2] mà cũng là a1[1], a2[2] nên lúc này giá trị 3 ở phần tử a[2] đã bị thay thành 4. Tương tự như vậy thì a[3] = 5 trong khi a[4] không đổi.
Map
Bảng băm (hash) là khái niệm tồn tại ở nhiều ngôn ngữ lập trình bởi sự linh hoạt và tiện dụng của nó. Đó là một tập hợp các cặp khóa/giá trị không có trật tự, trong đó các khóa phải có giá trị khác nhau. Giá trị sẽ được truy xuất thông qua khóa một cách nhanh chóng bất kể bảng băm có nhiều hay ít phần tử. Một số ngôn ngữ đặt tên khác cho bảng băm như bản đồ (map) hay từ điển (dictionary).
Kiểu dữ liệu map (bản đồ) trong Go là 1 dạng bảng băm với khai báo map[<kiểu dữ liệu khóa>]<kiểu dữ liệu giá trị>.
Tất cả các khóa phải cùng kiểu dữ liệu và phải là kiểu dữ liệu so sánh được do khi cần truy xuất 1 phần tử thông qua khóa thì Go so sánh để tìm ra phần tử tương ứng. Mặc dù kiểu dấu chấm động so sánh được nhưng hầu như không xài vì gặp phải khóa vô tỉ thì không so sánh được. Trong trường hợp vẫn muốn dùng kiểu không so sánh được làm khóa thì ta dùng hàm fmt.Sprintf() để chuyển giá trị kiểu này thành chuỗi trước, sau đó dùng chuỗi đó làm khóa. Tất cả các giá trị phải cùng kiểu dữ liệu và không có ràng buộc kiểu với giá trị.
1 var student map[int]string 2 fmt.Println(student) // map[] 3 student[1] = "Mai" // Go báo lỗi do chưa cấp phát cho student 4 student = make(map[int]string) 5 student[1] = "Mai" 6 student[2] = "Lan" 7 student[3] = "Cúc" 8 student[4] = "Trúc" 9 age := map[string]int{ 10 "Mai": 20, 11 "Lan": 18, 12 "Cúc": 21, 13 "Trúc": 16, 14 } 15 fmt.Println(student) // map[4:Trúc 1:Mai 2:Lan 3:Cúc] 16 fmt.Println(age) // map[Lan:18 Cúc:21 Trúc:16 Mai:20] 17 i := 1 18 fmt.Println("SV 1: ", student[i], " - ", age[student[i]]) // SV 1: Mai - 20 19 i++ 20 delete(age, student[i]) 21 fmt.Println("SV 2: ", student[i], " - ", age[student[i]]) // SV 2: Lan - 0 22 age["Cúc"]++ 23 fmt.Println("SV 3: ", student[3], " - ", age[student[3]]) // SV 3: Cúc - 22 24 age[student[4]] = 23 25 fmt.Println("SV 4: ", student[4], " - ", age[student[4]]) // SV 4: Trúc - 23 26 age["trúc"] = 22 27 fmt.Println("SV 4: ", student[4], " - ", age[student[4]]) // SV 4: Trúc - 23 28 fmt.Println(age) // map[Mai:20 trúc:22 Cúc:22 Trúc:23]
- Dòng 1 khai báo một map student. Với khai báo kiểu này, chúng ta có 1 map rỗng, dữ liệu lưu trữ chưa được cấp nên việc gán giá trị như dòng 3, Go sẽ báo lỗi.
- Để khai báo và cấp phát vùng nhớ cho map, Go cung cấp hàm make như dòng 4. Sau đó việc gán giá trị sẽ được Go chấp nhận như các dòng 5-8.
- Dòng 9-14 là kiểu khai báo map age kèm với cấp giá trị luôn.
- Dòng 15 và 16 in kết quả map student và age ra màn hình. Để ý sẽ thấy kết quả không có theo thứ tự khi tạo mà rất ngẫu nhiên.
- Dòng 17 khai báo biến nguyên i và gán i = 1. Dòng 18 in thông tin của sinh viên thứ nhất ra màn hình. Ta thấy việc truy xuất tên sinh viên thông qua mã số khá đơn giản qua toán tử [] và cách dùng 2 toán tử [] lồng nhau để lấy tuổi của sinh viên có mã là 1.
- Dòng 19 tăng biến i lên 2. Câu lệnh dòng 20 dùng để xóa phần tử của map age có khóa là giá trị của phần tử có khóa là i của map student. Do i = 2 nên student[2]="Lan". Như vậy phần tử age["Lan"] không còn nữa.
- Dòng 21 in thông tin sinh viên thứ 2 ra màn hình, sinh viên này có tên là Lan nhưng ta đã xóa phần tử có khóa là "Lan" ở bản đồ age rồi. Liệu Go có báo lỗi không khi ta truy xuất 1 phần tử không tồn tại? Go không báo lỗi, nó chỉ trả về giá trị zero mặc định tùy theo kiểu dữ liệu của giá trị.
- Go hỗ trợ phép toán cho các phần tử tùy theo kiểu dữ liệu của giá trị nên ở dòng 22 phép toán cộng thêm 1 rút gọn hoàn toàn hợp lệ. Dòng 23 cho kết quả thông tin sinh viên thứ 3 với tuổi đã được tăng lên 1.
- Tương tự, dòng 24 gán giá trị mới cho tuổi sinh viên thứ 4 và dòng 25 cho kết quả với tuổi đã được đổi.
- Dòng 26 gán giá trị của 1 phần tử map age có khóa là "trúc". Do "trúc" khác với "Trúc" nên Go sẽ tạo phần tử mới cho map age nên khi in lại thông tin sinh viên thứ 4 như dòng 27, kết quả không đổi.
- Dòng 28 in lại bản đồ age: vẫn có 4 phần tử nhưng "trúc" đã thay cho "Lan" và giá trị vài chỗ đổi.
Trong bài tới, chúng ta sẽ tìm hiểu về hàm.
Tóm tắt:- Slice là kiểu dữ liệu gồm nhiều phần tử cùng kiểu dữ liệu như mảng nhưng không cố định chiều dài:
+ Gồm 3 thành phần: mảng chứa dữ liệu các phần tử, số phần tử (chiều dài slice) và số phần tử tối đa (sức chứa slice).
+ Các hàm và toán tử xử lý:
* make([]<kiểu dữ liệu>, <chiều dài>, <sức chứa>): tạo slice a với chiều dài và mảng.
* len(a): chiều dài slice a.
* cap(a): sức chứa slice a.
* a[i]: truy xuất phần tử i của a.
* a[i:j]: slice con của a gồm các phần tử từ i đến j-1. a[:] chính là a.
* append(): thêm phần tử vào cuối slice.
* copy(): chép phần tử từ slice này sang slice khác.
- Map là dạng bảng băm với khóa là kiểu so sánh được và giá trị cùng kiểu:
+ Khai báo: var <tên biến> map[<kiểu dữ liệu khóa>]<kiểu dữ liệu giá trị>. Sau khai báo, biến cần khởi tạo bằng hàm make để có thể tạo phần tử.
+ a = make(map[int]string): tạo vùng nhớ lưu trữ cho biến kiểu map a.
+ a[1] truy xuất chuỗi ứng với phần tử có khóa là 1.
+ delete(a[1]) dùng để xóa phần tử 1 của a.
Hi anh, kiểu map không có trật tự vậy khi thêm phần tử mới nó append vào theo tiêu chí nào a nhỉ?, em code đoạn trên như anh nhưng "truc" ở age lại thêm vào đầu tiên khi in ra. Cái này ko ảnh hưởng tới logic nhưng em thắc mắc thôi a :)
ReplyDeleteỞ trên anh cũng có nói là nó không theo trật tự. Anh cũng không rõ cách tổ chức bên dưới của map ở Go nhưng theo anh nhớ bảng băm nó sẽ dùng mảng để lưu và mỗi khóa nó sẽ băm sao để có vị trí trong mảng mà lưu giá trị nên không thể theo thứ tự như khóa mình đưa vào được.
ReplyDeleteVâng anh
DeleteThis comment has been removed by the author.
DeleteHi a
ReplyDeleteCho e hỏi 1 câu: Sức chứa của Slice có tác dụng gì không, khi mà nó tự mở rộng nếu cần thiết.
Ở trên chỗ append có nói như sau: "Khi thêm, Go sẽ xác định số phần tử hiện tại của slice a và số phần tử sắp thêm liệu có vượt quá sức chứa hay không. Nếu có, Go sẽ tạo 1 mảng mới đủ để chứa và chép các phần tử cũ và mới vào đó. Nếu không, Go đơn giản chép thêm phần tử mới vào và tăng chiều dài lên tương ứng." Do đó sức chứa nhỏ thì sẽ tốn chi phí tạo mới và copy dữ liệu mảng bên dưới khi append vượt quá sức chứa. Do đó cần chọn sức chứa phù hợp với lượng append.
Delete