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ống | Có default | Không có default |
|---|---|---|
| Thiếu một nhánh enum | Khô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ạy | Rơi vào default | Rơi qua cả switch, chạy tiếp sau nó |
| Xử lý được mọi trường hợp | Có, nhưng im lặng | Có, 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ừ đâu | Cần kiểm tra biên | Vì sao |
|---|---|---|
| Hằng viết thẳng trong mã | Không | Trình biên dịch bảo đảm nó hợp lệ |
| Biến nội bộ chỉ gán bằng hằng enum | Không | Mọi đường gán đều nằm trong tầm kiểm soát |
| Đọc từ file hoặc từ mạng | Có, luôn luôn | Dữ liệu bên ngoài có thể là bất cứ số nào |
| Người dùng nhập vào | Có | Cùng lý do |
| Tham số của hàm công khai trong thư viện | Có | Bạ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í | switch | Bảng tra |
|---|---|---|
| Trình biên dịch bắt thiếu nhánh | Có, nếu không viết default | Không |
| Thời gian tìm | Thường là hằng số, nhờ bảng nhảy | Hằng số |
| Thêm một trường hợp | Sửa hàm điều phối | Thê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ạy | Không | Có, gán lại phần tử của bảng |
| Trình biên dịch nhúng được | Có | Khô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ử
- Viết
switchtrê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. - 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.
- Nâng cảnh báo thành lỗi bằng
-Werror=switchvà xác nhận mã không dịch được. - Viết
switch ((int)t)và xác nhận cảnh báo biến mất, giải thích vì sao. - 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.
- 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. - 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
defaulttrongswitchtrên enum. Đặt xử lý mặc định ở câu lệnh sauswitch. - Nhờ vậy cờ
-Wswitchtrong-Wallchỉ 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=switchtrong 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
switchkhông default để bù lại.