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

enum với switch

Sau bài này bạn sẽ làm được

  • Dùng switch trên enum để trình biên dịch kiểm tra đủ nhánh
  • Biết vì sao nhánh mặc định làm mất lợi ích đó
  • Xử lý giá trị nằm ngoài tập enum một cách an toàn
  • Dùng enum làm chỉ số cho bảng tra

Bài này chỉ có một ý tưởng, nhưng đó là một trong những mẹo giá trị nhất của C: không viết nhánh mặc định trong switch trên enum. Nghe ngược đời, nhưng nó biến trình biên dịch thành người rà soát mã cho bạn.

#Mẹo quan trọng nhất của chương

Có nhánh mặc định
typedef enum { TT_CHO, TT_CHAY, TT_DUNG } TrangThai;

const char *tt_ten(TrangThai t)
{
    switch (t) {
        case TT_CHO:  return "Cho";
        case TT_CHAY: return "Chay";
        case TT_DUNG: return "Dung";
        default:      return "Khong xac dinh";
    }
}

/* Thêm TT_TAM_DUNG vào enum: hàm này VẪN biên dịch sạch,
   và im lặng trả về "Khong xac dinh" cho trạng thái mới. */
Không có nhánh mặc định
const char *tt_ten(TrangThai t)
{
    switch (t) {
        case TT_CHO:  return "Cho";
        case TT_CHAY: return "Chay";
        case TT_DUNG: return "Dung";
    }

    return "Khong xac dinh";      /* đặt SAU switch, không phải trong default */
}

/* Thêm TT_TAM_DUNG vào enum: trình biên dịch CẢNH BÁO ngay. */
terminal
# Sau khi thêm TT_TAM_DUNG vào enum
gcc -std=c17 -Wall -Wextra bai.c -o t
bai.c:8:5: warning: enumeration value 'TT_TAM_DUNG' not handled in switch [-Wswitch]
    8 |     switch (t) {
      |     ^~~~~~
terminal
# Nâng cảnh báo này thành lỗi trong quy trình kiểm thử
gcc -std=c17 -Wall -Wextra -Werror=switch bai.c -o t
bai.c:8:5: error: enumeration value 'TT_TAM_DUNG' not handled in switch [-Werror=switch]

#Vì sao nó hoạt động

Cờ -Wswitch
Cảnh báo khi một switch có biểu thức điều khiển kiểu enum mà thiếu nhánh case cho một hoặc nhiều hằng của enum đó, và không có nhánh mặc định. Cờ này nằm sẵn trong -Wall.
Tình huốngCó defaultKhông có default
Thiếu một nhánh enumKhông cảnh báo gìCảnh báo, nêu đúng tên hằng bị thiếu
Giá trị lạ lúc chạyRơi vào defaultRơi qua cả switch, chạy tiếp sau nó
Xử lý được mọi trường hợpCó, nhưng im lặngCó, nếu bạn đặt xử lý sau switch

Mẫu đầy đủ với return

mau.c
const char *tt_ten(TrangThai t)
{
    switch (t) {
        case TT_CHO:      return "Cho";
        case TT_CHAY:     return "Chay";
        case TT_TAM_DUNG: return "Tam dung";
        case TT_DUNG:     return "Dung";
    }

    /* Tới được đây nghĩa là t không phải hằng TrangThai nào cả. */
    return "Khong xac dinh";
}

Mẫu với break và biến kết quả

mau-2.c
int tt_thoi_gian_cho(TrangThai t)
{
    int giay = -1;      /* giá trị mặc định, đặt trước switch */

    switch (t) {
        case TT_CHO:      giay = 0;   break;
        case TT_CHAY:     giay = 30;  break;
        case TT_TAM_DUNG: giay = 5;   break;
        case TT_DUNG:     giay = 0;   break;
    }

    if (giay < 0)
        fprintf(stderr, "Trang thai la: %d\n", (int)t);

    return giay;
}

#Xử lý giá trị nằm ngoài tập enum

Bài 16.1 đã nói: C không giới hạn giá trị của biến enum. Nên mọi hàm nhận enum từ bên ngoài phải phòng thủ.

phong-thu.c
typedef enum {
    TT_CHO,
    TT_CHAY,
    TT_DUNG,
    TT_SO_LUONG,      /* hằng đếm, xem Bài 16.1 */
} TrangThai;

/* Kiểm tra biên: dùng cho dữ liệu đến từ file, mạng hoặc người dùng */
static int tt_hop_le(TrangThai t)
{
    return t >= 0 && t < TT_SO_LUONG;
}

int xu_ly(TrangThai t)
{
    if (!tt_hop_le(t)) {
        fprintf(stderr, "Trang thai khong hop le: %d\n", (int)t);
        return -1;
    }

    switch (t) {
        case TT_CHO:      return lam_cho();
        case TT_CHAY:     return lam_chay();
        case TT_DUNG:     return lam_dung();
        case TT_SO_LUONG: break;      /* không phải trạng thái thật */
    }

    return -1;      /* không bao giờ tới được, nhưng phải có return */
}

Khi nào cần kiểm tra biên

Enum đến từ đâuCần kiểm tra biênVì sao
Hằng viết thẳng trong mãKhôngTrình biên dịch bảo đảm nó hợp lệ
Biến nội bộ chỉ gán bằng hằng enumKhôngMọi đường gán đều nằm trong tầm kiểm soát
Đọc từ file hoặc từ mạngCó, luôn luônDữ liệu bên ngoài có thể là bất cứ số nào
Người dùng nhập vàoCùng lý do
Tham số của hàm công khai trong thư việnBạn không kiểm soát được người gọi
doc-file.c
/* Đọc trạng thái từ file: BẮT BUỘC kiểm tra */
int doc_trang_thai(FILE *f, TrangThai *ra)
{
    int n;

    if (fscanf(f, "%d", &n) != 1)         return -1;
    if (n < 0 || n >= TT_SO_LUONG)        return -1;

    *ra = (TrangThai)n;

    return 0;
}

#Enum làm chỉ số bảng tra

bang-tra.c
typedef enum {
    LENH_THEM,
    LENH_XOA,
    LENH_SUA,
    LENH_TIM,
    LENH_SO_LUONG,
} Lenh;

/* Bảng tên, dùng khởi tạo có chỉ định để không lệch thứ tự */
static const char *TEN_LENH[LENH_SO_LUONG] = {
    [LENH_THEM] = "them",
    [LENH_XOA]  = "xoa",
    [LENH_SUA]  = "sua",
    [LENH_TIM]  = "tim",
};

/* Bảng hàm xử lý, xem Bài 13.5 */
typedef int (*XuLy)(const char *);

static const XuLy BANG[LENH_SO_LUONG] = {
    [LENH_THEM] = xu_ly_them,
    [LENH_XOA]  = xu_ly_xoa,
    [LENH_SUA]  = xu_ly_sua,
    [LENH_TIM]  = xu_ly_tim,
};

/* Phân tích tên lệnh thành enum */
int phan_tich_lenh(const char *s, Lenh *ra)
{
    for (int i = 0; i < LENH_SO_LUONG; ++i)
        if (TEN_LENH[i] != NULL && strcmp(s, TEN_LENH[i]) == 0) {
            *ra = (Lenh)i;
            return 0;
        }

    return -1;
}

/* Điều phối */
int chay(const char *ten_lenh, const char *tham_so)
{
    Lenh l;

    if (phan_tich_lenh(ten_lenh, &l) != 0) {
        fprintf(stderr, "Lenh khong biet: %s\n", ten_lenh);
        return -1;
    }

    if (BANG[l] == NULL) {
        fprintf(stderr, "Lenh chua duoc cai dat\n");
        return -1;
    }

    return BANG[l](tham_so);
}
terminal
./cong-cu them abc
them: abc
./cong-cu xoa2 abc
Lenh khong biet: xoa2

#Bảng tra hay switch

Tiêu chíswitchBảng tra
Trình biên dịch bắt thiếu nhánhCó, nếu không viết defaultKhông
Thời gian tìmThường là hằng số, nhờ bảng nhảyHằng số
Thêm một trường hợpSửa hàm điều phốiThêm một dòng dữ liệu
Xử lý phức tạp, nhiều dòngĐọc thoải mái ngay tại chỗPhải nhảy tới hàm khác
Đổi hành vi lúc chạyKhôngCó, gán lại phần tử của bảng
Trình biên dịch nhúng đượcKhông, vì gọi gián tiếp
ca-hai.c
/* Mẹo: một hàm switch duy nhất để giữ kiểm tra lúc biên dịch,
   dù phần điều phối thật dùng bảng tra. */
static XuLy lay_xu_ly(Lenh l)
{
    switch (l) {
        case LENH_THEM: return xu_ly_them;
        case LENH_XOA:  return xu_ly_xoa;
        case LENH_SUA:  return xu_ly_sua;
        case LENH_TIM:  return xu_ly_tim;
        case LENH_SO_LUONG: break;
    }

    return NULL;
}

/* Thêm LENH_SAP_XEP vào enum thì hàm này cảnh báo ngay,
   nên bạn không thể quên cài đặt nó. */

Bài toán mẫu: máy tính bỏ túi

may-tinh.c
typedef enum {
    PHEP_CONG,
    PHEP_TRU,
    PHEP_NHAN,
    PHEP_CHIA,
    PHEP_SO_LUONG,
} Phep;

typedef enum {
    KQ_OK,
    KQ_CHIA_KHONG,
    KQ_PHEP_LA,
} MaKetQua;

MaKetQua tinh(Phep p, double a, double b, double *ra)
{
    if (ra == NULL) return KQ_PHEP_LA;

    switch (p) {
        case PHEP_CONG: *ra = a + b; return KQ_OK;
        case PHEP_TRU:  *ra = a - b; return KQ_OK;
        case PHEP_NHAN: *ra = a * b; return KQ_OK;

        case PHEP_CHIA:
            if (b == 0.0) return KQ_CHIA_KHONG;

            *ra = a / b;

            return KQ_OK;

        case PHEP_SO_LUONG:
            break;
    }

    return KQ_PHEP_LA;
}

const char *kq_ten(MaKetQua k)
{
    switch (k) {
        case KQ_OK:         return "OK";
        case KQ_CHIA_KHONG: return "Chia cho khong";
        case KQ_PHEP_LA:    return "Phep toan la";
    }

    return "Loi khong xac dinh";
}
terminal
# Thêm PHEP_LUY_THUA vào enum Phep rồi biên dịch lại
gcc -std=c17 -Wall -Wextra may-tinh.c -o t
may-tinh.c:20:5: warning: enumeration value 'PHEP_LUY_THUA' not handled in switch [-Wswitch]

Tự làm thử

  1. Viết switch trên enum có nhánh mặc định, thêm một hằng vào enum và xác nhận không có cảnh báo nào.
  2. Bỏ nhánh mặc định đi rồi làm lại, chép lại thông báo cảnh báo.
  3. Nâng cảnh báo thành lỗi bằng -Werror=switch và xác nhận mã không dịch được.
  4. Viết switch ((int)t) và xác nhận cảnh báo biến mất, giải thích vì sao.
  5. Cài hàm đọc enum từ file có kiểm tra biên, thử với một giá trị nằm ngoài tập.
  6. Cài bảng tra lệnh với hằng đếm và kiểm tra NULL, thêm một lệnh mới mà quên điền bảng, quan sát chuyện gì xảy ra.
  7. Cài chương trình máy tính bỏ túi trong bài, thêm phép lũy thừa và để trình biên dịch chỉ cho bạn mọi chỗ cần sửa.

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

  • Không viết nhánh default trong switch trên enum. Đặt xử lý mặc định ở câu lệnh sau switch.
  • Nhờ vậy cờ -Wswitch trong -Wall chỉ cho bạn mọi chỗ thiếu nhánh khi bạn thêm một hằng enum mới.
  • Bật -Werror=switch trong quy trình kiểm thử để mã không dịch được cho tới khi xử lý hết.
  • Enum không giới hạn giá trị, nên dữ liệu đến từ file, mạng hoặc người dùng phải được kiểm tra biên trước khi ép kiểu.
  • Bảng tra linh hoạt hơn nhưng mất kiểm tra lúc biên dịch. Giữ một hàm switch không default để bù lại.