Wednesday, March 30, 2016

Bài 13: Phân nhánh và vòng lặp trong Go

Trong bài này chúng ta sẽ tìm hiểu một số cấu trúc lệnh hay được sử dụng trong lập trình mà hầu như ngôn ngữ nào cũng cung cấp. Đó là phân nhánh và vòng lặp.


Phân nhánh


Trong lập trình có những lúc chúng ta cần xử lý các tính huống phụ thuộc vào giá trị biến nên cần chia ra nhiều nhánh xử lý tùy thuộc giá trị biến đó. Go cung cấp if, switch và select để phân nhánh khi lập trình. If và switch sẽ được giới thiệu ở đây, còn select sẽ được đề cập ở bài về kênh (channel).

If

Câu lệnh if có cú pháp đầy đủ như sau:
if <mô tả 1 (tùy chọn)>; <biểu thức luận  1> { 
<Khối lệnh 1> 
} else if <mô tả 2 (tùy chọn)>; <biểu thức luận  2> { 
<Khối lệnh 2> 
} else if <mô tả n-1 (tùy chọn)>; <biểu thức luận  n-1> { 
<Khối lệnh n-1> 
} else { 
<Khối lệnh n> 
} 
- Những mô tả tùy chọn là có hoặc không có cũng được. Mỗi mô tả là một khai báo đơn giản như là khai báo biến, biểu thức gán hay tính toán, v.v... Nếu là khai báo biến thì biến này chỉ có phạm vi trong các nhánh của if. Nếu mô tả tùy chọn tồn tại, dấu chấm phẩy ; là bắt buộc.
- Biểu thức luận lý phải cho kết quả là kiểu boolean, đúng hoặc sai. Go không tự hiểu hay tự chuyển đổi giá trị của các kiểu khác về kiểu luận lý nên ở đây chúng ta buộc phải dùng phép so sánh hoặc là biến kiểu luận lý.
- Các dấu ngoặc nhọn là bắt buộc và theo đúng vị trí như mô tả bên trên. Ví dụ dấu mở ngoặc nhọn phải nằm sau biểu thức luận lý, không được xuống hàng.
- Các nhánh else if có thể không có hoặc có nhiều nhánh else if trong khối lệnh if. Nhánh else có thể không có hoặc nếu có thì chỉ có một mà thôi.
- Trong các khối lệnh có thể bao gồm một hoặc nhiều khối phân nhánh if <khối lệnh> esle if <khối lệnh> else <khối lệnh> khác lồng nhau.

Ví dụ sau in ra màn hình kết quả 1 số nhập từ bàn phím là số âm hay dương và nếu là dương thì có một hay nhiều chữ số:
1    if num := input(); num < 0 { 
2        fmt.Println(num, "  số âm.") 
3    } else if num < 10 { 
4        fmt.Println(num, " là số dương có một chữ số.") 
5    } else { 
6       fmt.Println(num, " là số dương có nhiều chữ số.") 
7    } 
- Dòng đầu khai báo biến num kiểu int là kết quả trả về của hàm input(), là hàm trả về số nhận từ bàn phím. Sau đó so sánh giá trị num với số 0. Nếu đúng là num < 0 thì in ra màn hình num là số âm như mô tả ở dòng 2 và kết thúc lệnh if mà không cần kiểm tra các nhánh còn lại.
- Trong trường hợp biểu thức luận lý ở dòng 1 sai, dòng 3 sẽ được thực thi. Dòng 3 tiếp tục so sánh num với 10. Nếu đúng là num < 10, dòng 4 được thực thi và kết quả in ra màn hình. Lúc này khối lệnh if cũng sẽ kết thúc.
- Trong trường hợp num >= 10, dòng 5 sẽ được tiếp cận và vì nó là else nên không cần so sánh gì cả, dòng 6 sẽ được thực thi. Kết quả in ra màn hình là num có nhiều chữ số.

Switch

Câu lệnh switch là một biến thể của câu lệnh if, dùng trong trường hợp có nhiều phân nhánh. Cấu trúc lệnh switch như sau:
switch <mô tả (tùy chọn)>; <biểu thức (tùy chọn)> { 
case <biểu thức 1>: 
    <Khối lệnh 1> 
... 
case <biểu thức n>: 
    <Khối lệnh n> 
default: 
    <Khối lệnh mặc định> 
} 
- Mô tả tùy chọn không nhất thiết tồn tại nhưng nếu có, dấu chấm phẩy là bắt buộc. Cũng như mô tả tùy chọn ở câu lệnh if, mô tả tùy chọn ở đây cũng chỉ là một khai báo đơn giản. Nếu biến được khai báo ở đây, nó có phạm vi sử dụng trong toàn khối switch bao gồm các case và default.
- Biểu thức tùy chọn thường là một giá trị của một kiểu dữ liệu so sánh được bởi vì khi thực thi, Go sẽ so sánh nó với các biểu thức ở từng case, khớp với case nào khối lệnh case đó sẽ thực thi. Biểu thức tùy chọn nếu không có, Go sẽ mặc định là có giá trị đúng (true). Lúc này các biểu thức ở các case cũng buộc phải là các biểu thức luận lý.
- Khối lệnh có thể rỗng hoặc chứa nhiều câu lệnh khai báo, mô tả. Nếu chỉ gồm 1 lệnh, nó có thể nằm cùng dòng với case. Go không cho phép thực hiện xuyên khối lệnh giữa các case như các ngôn ngữ khác nên việc dùng break để kết thúc khối lệnh là không cần thiết. Nếu vẫn muốn tự động nhảy xuống khối case bên dưới sau khi kết thúc khối lệnh, chúng ta có thể dùng khai báo với từ khóa fallthrough. Nhưng nếu fallthrough nằm ở khối lệnh cuối cùng thì Go cũng sẽ báo lỗi khi không còn case hay default nào nữa để thực hiện.
- Default không bắt buộc phải có và khi có nó có thể nằm ở bất kỳ đâu chứ không nhất thiết nằm cuối. Khi các biểu thức trong các case đều sai, khối lệnh ở default sẽ được thực thi.

Chúng ta cùng xem lại ví dụ ở lệnh if được thể hiện dưới switch sẽ như thế nào nhé:
1    num := input() 
2    switch { 
3    case num < 0 : fmt.Println(num, "  số âm.") 
4    case num < 10: fmt.Println(num, " là số dương có một chữ số.") 
5    default: fmt.Println(num, " là số dương có nhiều chữ số.") 
6    } 
- Dòng 1 tạo biến num và gán giá trị qua hàm input().
- Switch không có biểu thức nên mặc định sẽ có giá trị đúng (true).
- Nếu num là -10 thì ngay ở case đầu tiên (dòng 3), kết quả là true nên sẽ in ra màn hình "-10 là số âm". Ngược lại nếu num = 12 thì cả 2 case ở dòng 3 và 4 đều cho kết quả sai (false) nên câu lệnh ở dòng 5 ứng với nhánh default sẽ được thực thi.

Ví dụ khác về việc kiểm tra ngày hiện tại là ngày làm việc hay cuối tuần:
switch time.Now().Weekday() { 
case time.Saturday, time.Sunday: 
    fmt.Println("Hôm nay  cuối tuần!") 
default: 
    fmt.Println("Hôm nay  ngày làm việc.") 
} 
- Hàm time.Now().Weekday() thuộc package time sẽ cho chúng ta biết hôm nay là thứ mấy. Giá trị trả về kiểu time.Weekday, định nghĩa lại từ int bắt đầu với Sunday = 0. Để sử dụng hàm này chúng ta phải khai báo package sử dụng là time.
- Ở dòng 2 kiểm tra xem liệu giá trị ở switch có bằng với time.Saturday hay time.Sunday không. Nếu đúng với một trong hai thì sẽ in ra "Hôm nay là cuối tuần!". Chúng ta thấy là có thể gộp nhiều biểu thức kiểm tra vào trong một case nếu khối lệnh của chúng là như nhau.
- Các trường hợp khác thì lệnh ở dòng 4 sẽ được thực thi và chuỗi "Hôm nay là ngày làm việc." sẽ được in ra.

Có một loại switch đặc biệt khi mà biểu thức tùy chọn của nó là một loại kiểu dữ liệu. Lúc này các biểu thức của case cũng là các loại kiểu dữ liệu. Trường hợp này sử dụng khi chúng ta có một biến thuộc kiểu interface (sẽ tìm hiểu sau) và muốn biết kiểu dữ liệu thực sự bên dưới của nó là gì nhưng có nhiều khả năng khác nhau thì thay vì dùng if ta có thể dùng switch. Sẽ nói rõ hơn trong phần tìm hiểu về interface.

Lặp trong Go


Trong lập trình có những lúc chúng ta cần lặp lại thực hiện một nhóm lệnh cho đến khi thõa một điều kiện nào đó mới thôi thì chúng ta cần đến một cấu trúc đặc biệt thay vì lặp lại đoạn mã nhiều lần. Các ngôn ngữ lập trình đều cung cấp cấu trúc này và gọi là vòng lặp. Tùy theo ngôn ngữ mà chúng ta có 1, 2 hay nhiều loại vòng lặp khác nhau như for, while hay do ... while hay repeat ... until. For thường dùng khi cần duyệt một nhóm đối tượng có số lượng nhất định. While thường dùng khi cần lặp lại ở một nhóm đối tượng chưa xác định rõ số lượng nhưng cùng chung đặc điểm được nêu trong biểu thức của while. Do while tương tự như while nhưng đảm bảo khối lệnh được thực hiện ít nhất 1 lần. Repeat until y như do while nhưng khác ở biểu thức điều kiện khi ở until là biểu thức đúng sẽ giúp thoát khỏi vòng lặp.

Go chỉ cung cấp vòng lặp for nhưng có nhiều cú pháp khác nhau để đáp ứng yêu cầu đa dạng của các loại vòng lặp. Các dạng vòng lặp for phổ biến như sau:
for { // Như vòng lặp do while 
    <Khối lệnh> 
} 
for <Biểu thức luận lý> { // Như vòng lặp while 
    <Khối lệnh> 
} 
for <Mô tả trước>; <Biểu thức luận lý>; <Mô tả sau> { 
    <Khối lệnh> 
} 
for <Chỉ số>, <Giá trị> := range <Biến kiểu dữ liệu có phần tử> { 
    <Khối lệnh> 
}
- Dấu ngoặc nhọn là bắt buộc và khối lệnh nên nằm dòng bên dưới dấu mở ngoặc.
- Ở vòng lặp for đầu tiên, trong khối lệnh phải có lệnh kiểm tra điều kiện để thoát ra khỏi vòng lặp nếu không muốn ứng dụng bị treo. Lệnh kiểm tra điều kiện để thoát khỏi vòng for này y như câu lệnh while trong vòng lặp do while ở các ngôn ngữ khác. Nhưng khác với câu lệnh while ở do while ở chỗ, sau khi kiểm tra đúng điều kiện thoát, chúng ta cần lệnh gì đó để thoát khỏi vòng lặp for. Tương tự các ngôn ngữ khác, Go cung cấp từ khóa break dùng để thoát ra khỏi vòng lặp for. Bất kỳ chỗ nào trong khối lệnh for, gặp break thì Go sẽ thoát ra khỏi for ngay lập tức.
- Ở vòng lặp thứ 2, khối lệnh sẽ được lặp lại cho đến khi biểu thức luận lý có giá trị sai. Trường hợp này giống như vòng lặp while ở các ngôn ngữ khác.
- Ở vòng lặp thứ 3, các mô tả đều là tùy chọn, có hoặc không đều được. Khi cả hai mô tả không có, lúc này nó là vòng for thứ 2. Mô tả ở đây là khai báo biến, gán giá trị đơn giản. Nếu là khai báo biến thì biến này có ý nghĩa trong cả vòng lặp for (ở các mô tả, biểu thức điều kiện và khối lệnh). Vòng lặp chấm dứt khi biểu thức luận lý cho giá trị sai (false).
- Ở vòng lặp thứ 4, các phần tử của biến sẽ được duyệt qua tuần tự và trả về chỉ số phần tử và giá trị phần tử. Vòng lặp kết thúc khi duyệt xong toàn bộ phần tử. Nếu không cần sử dụng chỉ số hoặc giá trị, chúng ta có thể dùng dấu gạch dưới _ để thay thế. Biến chỉ số hoặc giá trị này có ý nghĩa trong phạm vi vòng lặp for. Trong trường hợp không cần cả 2, chúng ta có thể khai báo đơn giản là for range <Biến kiểu dữ liệu có phần tử>.
- Khi đang ở trong khối lệnh, nếu muốn thực hiện vòng lặp mới mà không muốn phải chờ hết khối lệnh, ta dùng từ khóa continue. Bất kỳ ở đâu trong khối lệnh vòng for gặp continue, Go sẽ kiểm tra lại điều kiện lặp, nếu thõa sẽ thực hiện từ đầu khối lệnh.

Sau đây chúng ta cùng tìm hiểu cách sử dụng 4 vòng for ở trên qua ví dụ tính giai thừa của 1 số nhập vào từ bàn phím như sau:
1    func fact1(n uint) uint { 
2       var factn uint = 1 
3       var i uint = 1 
4 
5       for { 
6           factn *= i 
7           i++ 
8           if i > n { 
9                break 
10          } 
11      } 
12 
13      return factn 
14   }  
- Hàm fact1 nhận tham số n kiểu nguyên dương (thực ra là không âm) và sẽ trả về kết quả là n! (n giai thừa).
- Dòng 2 khai báo biến nguyên dương factn chứa giá trị giai thừa của n cần trả về có giá trị khởi tạo là 1.
- Dòng 3 khai báo biến i cũng nguyên dương có giá trị khởi tạo là 1.
- Dòng 5-11 là vòng lặp for theo phong cách do while:
 + Dòng 6 lấy giá trị hiện tại của factn nhân với i rồi gán lại cho factn.
 + Dòng 7 tăng giá trị i lên 1.
 + Dòng 8 là câu điều kiện if kiểm tra xem i có lớn hơn n không. Nếu có thì sẽ thoát khỏi vòng lặp bằng break.
 + Như vậy vòng lặp này có tác dụng tăng i từ 1 đến n và nhân giá trị i mỗi lần lặp với factn. Nói cách khác, khi ra khỏi vòng lặp, biến factn = 1*2*3*...*(n-1)*n. Đây chính là định nghĩa giai thừa của n.
 + Dòng 7 là bước rất quan trọng vì nó thay đổi i qua mỗi vòng lặp và là chìa khóa để thoát khỏi vòng lặp khi i đạt đến n. Nếu quên thực hiện dòng 7, ta sẽ có vòng lặp vô tận do câu if ở dòng 8 luôn sai và câu lệnh break không bao giờ được thực thi.
- Dòng 13 trả kết quả factn về đó là giá trị tính n!. Trong trường hợp n = 0, vòng lặp thực hiện 1 lần và cho kết quả là factn = 1 phù hợp với định nghĩa 0! = 1.
1   func fact2(n uint) uint { 
2      var i, factn uint = 1, 1 
3 
4      for i <= n { 
5           factn *= i 
6           i++ 
7      } 
8 
9      return factn 
10  } 
- Hàm fact2 tính n! tương tự như fact1 nhưng đổi cấu trúc vòng lặp for bằng cách đưa biểu thức luận lý vào khai báo vòng for.
- Dòng 2 khai báo 2 biến nguyên dương i và factn đều có giá trị khởi tạo là 1.
- Dòng 4 là vòng lặp for dạng while. Vòng lặp kết thúc khi i > n.
- Dòng 5 gán tích factn*i cho factn.
- Dòng 6 tăng i lên một. Dòng này cũng rất quan trọng, thiếu nó vòng lặp sẽ lặp vô tận.
- Dòng 9 trả kết quả hàm là giá trị n!. Trường hợp n = 0, vòng lặp sẽ không thực hiện lần nào do điều kiện kiểm tra ở dòng 4 cho kết quả sai. Lúc này factn có giá trị là 1, phù hợp 0! = 1.
1   func fact3(n uint) uint { 
2      var factn uint = 1 
3        
4      for  i := uint(1); i <= n; i++ { 
5          factn *= i 
6      } 
7 
8      return factn 
9  } 
- Hàm fact3 cũng như các hàm fact1 và fact2 dùng để tính giai thừa của n nhưng sử dụng vòng for chuẩn.
- Dòng 2 khai báo biến factn = 1.
- Dòng 4 khai báo for chuẩn , biến i được tạo kiểu uint và gán giá trị 1. (Do Go mặc định kiểu int khi khai báo giá trị nguyên nên cần gán lại uint cho i). Sau đó Go sẽ kiểm tra điều kiện ở biểu thức luận lý, nếu đúng, tức i <= n, khối lệnh sẽ được thực hiện. Mỗi lần thực hiện xong khối lệnh từ dòng 5, Go sẽ thực hiện mô tả sau (tăng i lên 1) rồi tiến hành thực hiện biểu thức luận lý (kiểm tra i <= n). Nếu biểu thức đúng, khối lệnh lại tiếp tục thực hiện.
- Dòng 5 thực hiện gán tích factn* i cho factn
- Dòng 8 trả kết quả factn là tích của các i chạy từ 1 đến n, tức là ta sẽ có giá trị n giai thừa khi nhận kết quả trả về của hàm fact3. Trong trường hợp n = 0, vòng for sẽ không thực hiện và factn có giá trị 1.
1   func fact4(n uint) uint { 
2       var factn uint = 1 
3       a := make([]int, n) 
4 
5       for i, _ := range a { 
6            ai *= uint(i + 1) 
7       } 
8 
9        return factn 
10  } 
- Hàm fact4 tính giai thừa cho tham số n dựa trên slice a bằng cách tính tích các chỉ số phần tử của slice a cộng thêm 1 (do chỉ số phần tử của slice bắt đầu từ 0).
- Dòng 2 khai báo biến lưu giữ kết quả qua các vòng lặp factn. Ra khỏi vòng lặp nó sẽ chứa giá trị n!
- Dòng 3 khai báo slice a có n phần tử kiểu dữ liệu int (kiểu gì cũng được vì chúng ta chỉ quan tâm đến chỉ số phần tử)
- Dòng 5-7 là vòng lặp for range:
 + Do chỉ quan tâm đến chỉ số nên biến nhận giá trị được thay bằng dấu gạch dưới _. Vòng lặp này sẽ duyệt tuần tự các phần tử của slice a từ đầu cho đến cuối.
 + Dòng 6 tính tích của factn và i+1 rồi gán lại cho factn. Ở đây chúng ta phải sử dụng i +1 là bởi chỉ số slice a sẽ được duyệt từ 0 đến n - 1 nhưng chúng ta cần giá trị từ 1 đến n nên cần cộng thêm 1 vào. Cách viết uint(i+1) là cách ép kiểu cho giá trị này thành uint bởi vì factn có kiểu uint  trong khi biến i có kiểu là int do mặc định của for range trả về như vậy.
- Dòng 9 trả kết quả là n! về khi kết thúc hàm fact4. Trong trường hợp n = 0, vòng for range sẽ không duyệt và ta có factn = 1 nhờ khởi tạo ban đầu.
- Đây chỉ là ví dụ dùng for range để tính n! chứ thực tế không ai dùng cách này để tính giai thừa cả vì hơi rối do mượn tính chất duyệt tuần tự chỉ số của slice để tính giai thừa. Bài tới, chúng ta sẽ quay lại cách tính giai thừa bằng cách sử dụng đệ quy.

Trong bài tới chúng ta sẽ tìm hiểu về một số đặc điểm của hàm trong Go.


Tóm tắt:
- Câu lệnh if theo cú pháp if <biểu thức luận lý> {} else if <biểu thức luận lý> {} else {}
- Câu lệnh switch:
 + Không cần break để kết thúc khối lệnh mỗi case.
 + Muốn thực hiện xuyên case phải xài fallthrough
- Vòng lặp for: có 4 dạng tương thích while, do while, for và for range.

No comments:

Post a Comment