Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 5.413 phút đọc

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

loi-lom.c
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

Guard clause
Kiểm tra điều kiện xấu ngay đầu hàm và thoát ra luôn. Nhờ vậy phần còn lại của hàm chỉ chứa luồng chính, không bị thụt lề.
phang.c
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.

doc-cau-hinh.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ử

  1. 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.
  2. Chuyển hàm xu_ly lồng năm tầng ở mục hai thành dạng phẳng, không nhìn đáp án.
  3. 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.
  4. 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.
  5. 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.