if lồng nhau
Sau bài này bạn sẽ làm được
- Viết if lồng nhau khi bài toán thật sự cần
- Chuyển mã lồng sâu thành guard clause
- Giải thích vì sao mã phẳng dễ bảo trì hơn mã lồng sâu
Đặt một if bên trong một if khác là chuyện bình thường, nhưng khi lồng tới ba bốn tầng thì mã trở nên rất khó đọc và khó sửa. Bài này giới thiệu guard clause, kỹ thuật làm phẳng mã được dùng ở gần như mọi dự án C nghiêm túc.
#Khi nào cần lồng
if lồng nhau là đúng đắn khi các điều kiện phụ thuộc lẫn nhau, tức là điều kiện trong chỉ có nghĩa khi điều kiện ngoài đã đúng.
if (tuoi >= 18) {
if (co_bang_lai) {
if (con_diem) {
printf("Được lái xe\n");
} else {
printf("Hết điểm bằng lái\n");
}
} else {
printf("Chưa có bằng lái\n");
}
} else {
printf("Chưa đủ tuổi\n");
}Ở đây việc kiểm tra bằng lái chỉ có nghĩa khi người đó đã đủ tuổi, và kiểm tra điểm chỉ có nghĩa khi đã có bằng. Cấu trúc lồng phản ánh đúng quan hệ đó.
#Vấn đề của lồng sâu
int xu_ly(int *p, int n)
{
if (p != NULL) {
if (n > 0) {
if (n < 1000) {
if (kiem_tra_du_lieu(p, n)) {
/* Logic chính nằm ở đây,
thụt lề tận năm tầng. */
tinh_toan(p, n);
return 0;
} else {
return -4;
}
} else {
return -3;
}
} else {
return -2;
}
} else {
return -1;
}
}Đoạn mã trên có bốn vấn đề rõ rệt:
- Logic chính bị đẩy vào sâu nhất, trong khi nó mới là thứ quan trọng nhất cần đọc.
- Điều kiện và cách xử lý lỗi ở xa nhau. Muốn biết
return -1ứng với điều kiện nào, bạn phải đếm ngoặc nhọn. - Thêm một điều kiện mới phải thụt lề lại toàn bộ.
- Dòng bị đẩy sang phải quá xa, phải cuộn ngang để đọc.
#Guard clause
int xu_ly(int *p, int n)
{
if (p == NULL) {
return -1;
}
if (n <= 0) {
return -2;
}
if (n >= 1000) {
return -3;
}
if (!kiem_tra_du_lieu(p, n)) {
return -4;
}
/* Từ đây trở xuống, mọi điều kiện đều đã đảm bảo.
Logic chính nằm ở mức thụt lề thấp nhất. */
tinh_toan(p, n);
return 0;
}Hai đoạn mã làm đúng một việc, nhưng đoạn thứ hai dễ đọc hơn hẳn. Bốn lợi ích:
- Điều kiện và cách xử lý nằm cạnh nhau. Nhìn một dòng là biết ngay lỗi nào ứng với mã nào.
- Logic chính ở mức thụt lề thấp nhất, dễ tìm và dễ đọc.
- Thêm điều kiện mới chỉ cần thêm một khối ba dòng, không phải sửa gì khác.
- Mỗi điều kiện đọc như một câu khẳng định: nếu con trỏ rỗng thì thoát.
#Áp dụng vào xử lý lỗi
Guard clause đặc biệt hợp với các hàm phải kiểm tra nhiều điều kiện trước khi làm việc chính. Đây là khuôn mẫu bạn sẽ dùng suốt khóa học.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int doc_cau_hinh(const char *duong_dan, CauHinh *out)
{
if (duong_dan == NULL || out == NULL) {
return -1; /* tham số không hợp lệ */
}
if (duong_dan[0] == '\0') {
return -1;
}
FILE *f = fopen(duong_dan, "r");
if (f == NULL) {
return -2; /* không mở được file */
}
char dong[256];
if (!fgets(dong, sizeof dong, f)) {
fclose(f);
return -3; /* file rỗng */
}
/* Tới đây mọi thứ đã hợp lệ, làm việc chính */
phan_tich(dong, out);
fclose(f);
return 0;
}Quy tắc thực dụng về độ sâu
Tự làm thử
- Viết hàm kiểm tra điều kiện lái xe theo ví dụ đầu bài, dùng cấu trúc lồng, rồi viết lại bằng guard clause. So sánh hai bản.
- Chuyển hàm
xu_lylồng năm tầng ở mục hai thành dạng phẳng, không nhìn đáp án. - Viết hàm nhận vào một chuỗi và trả về mã lỗi khác nhau cho các trường hợp: con trỏ rỗng, chuỗi rỗng, chuỗi quá dài, chuỗi chứa ký tự không hợp lệ. Dùng guard clause.
- Viết hàm mở file, đọc nội dung vào bộ nhớ, xử lý, rồi dọn dẹp. Dùng thành ngữ nhãn dọn dẹp chung.
- Tìm trong bài tập cũ của bạn một hàm lồng từ ba tầng trở lên và tái cấu trúc lại.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Lồng if là đúng khi các điều kiện phụ thuộc nhau, sai khi chúng độc lập.
- Điều kiện độc lập thì gộp bằng
&&thay vì lồng. - Guard clause kiểm tra điều kiện xấu và thoát sớm, giúp logic chính không bị thụt lề.
- Lợi ích: điều kiện và cách xử lý nằm cạnh nhau, dễ thêm điều kiện mới, dễ đọc.
- Khi có tài nguyên cần dọn dẹp, dùng nhãn dọn dẹp chung ở cuối hàm.
- Lồng quá ba tầng là dấu hiệu nên tách hàm hoặc chuyển sang guard clause.