Bài 29.226 phút đọc
Hàm gọi lại
Sau bài này bạn sẽ làm được
- Viết hàm nhận callback để tách phần thay đổi khỏi phần cố định
- Viết hàm so sánh cho qsort đúng, không dùng phép trừ
- Nêu vì sao mọi API nhận callback phải có tham số ngữ cảnh
- Xử lý lỗi và điều kiện dừng trong callback
Callback là cách bạn viết một hàm mà chưa biết nó sẽ làm gì. Người gọi quyết định phần đó. Đây là một trong vài kỹ thuật thay đổi hẳn cách bạn thiết kế mã, và nó chỉ cần một con trỏ hàm.
#Tách chính sách khỏi cơ chế
Hàm gọi lại
Hàm mà bạn truyền cho một hàm khác, để hàm đó gọi ngược lại vào lúc thích hợp. Hàm nhận giữ cơ chế, còn callback giữ chính sách.
Một hàm cho mỗi điều kiện
/* Ba ham gan giong het nhau */
size_t dem_chan(const int *a, size_t n) {
size_t k = 0;
for (size_t i = 0; i < n; ++i) if (a[i] % 2 == 0) ++k;
return k;
}
size_t dem_duong(const int *a, size_t n) {
size_t k = 0;
for (size_t i = 0; i < n; ++i) if (a[i] > 0) ++k;
return k;
}
size_t dem_lon_hon_100(const int *a, size_t n) {
size_t k = 0;
for (size_t i = 0; i < n; ++i) if (a[i] > 100) ++k;
return k;
}Một hàm nhận điều kiện
typedef int (*ThoaDieuKien)(int gia_tri, void *ngu_canh);
size_t dem_neu(const int *a, size_t n, ThoaDieuKien dk, void *ngu_canh) {
size_t k = 0;
for (size_t i = 0; i < n; ++i)
if (dk(a[i], ngu_canh)) ++k;
return k;
}
/* Chinh sach nam o day, moi cai ba dong */
static int la_chan (int x, void *c) { (void)c; return x % 2 == 0; }
static int la_duong(int x, void *c) { (void)c; return x > 0; }
static int lon_hon (int x, void *c) { return x > *(const int *)c; }
int nguong = 100;
dem_neu(a, n, la_chan, NULL);
dem_neu(a, n, la_duong, NULL);
dem_neu(a, n, lon_hon, &nguong);#Hàm so sánh cho qsort
void qsort(void *co_so, size_t n, size_t co,
int (*ss)(const void *, const void *));
/* ss phai tra ve:
so am neu a dung TRUOC b
0 neu hai cai tuong duong
so duong neu a dung SAU b */ss.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
typedef struct { int ma; } BG;
/* SAI: phep tru tran */
static int ss_tru(const void *a, const void *b) {
return ((const BG *)a)->ma - ((const BG *)b)->ma;
}
/* DUNG: so sanh roi tra ba gia tri */
static int ss_dung(const void *a, const void *b) {
int x = ((const BG *)a)->ma;
int y = ((const BG *)b)->ma;
return (x > y) - (x < y);
}
int main(void) {
BG p = { -2000000000 }, q = { 2000000000 };
printf("so sanh -2000000000 voi 2000000000:\n");
printf(" cach tru : %d (ky vong: so AM)\n", ss_tru(&p, &q));
printf(" cach dung: %d\n", ss_dung(&p, &q));
BG ds[] = {{2000000000},{-2000000000},{0},{-1},{1}};
BG ds2[] = {{2000000000},{-2000000000},{0},{-1},{1}};
qsort(ds, 5, sizeof ds[0], ss_tru);
qsort(ds2, 5, sizeof ds2[0], ss_dung);
printf("\nsap bang cach tru :");
for (int i = 0; i < 5; ++i) printf(" %d", ds[i].ma);
printf("\nsap bang cach dung:");
for (int i = 0; i < 5; ++i) printf(" %d", ds2[i].ma);
printf("\n");
return 0;
}terminal
gcc -std=c11 -Wall -Wextra -o ss ss.c && ./ss
so sanh -2000000000 voi 2000000000: cach tru : 294967296 (ky vong: so AM) cach dung: -1 sap bang cach tru : 2000000000 -2000000000 -1 0 1 sap bang cach dung: -2000000000 -1 0 1 2000000000
Hàm so sánh cho bốn loại dữ liệu
#include <string.h>
/* So nguyen */
int ss_int(const void *a, const void *b) {
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y);
}
/* So thuc: chu y NaN */
int ss_double(const void *a, const void *b) {
double x = *(const double *)a, y = *(const double *)b;
if (x < y) return -1;
if (x > y) return 1;
return 0; /* ca truong hop NaN roi vao day */
}
/* MANG con tro chuoi: doi so la con tro TOI con tro */
int ss_chuoi(const void *a, const void *b) {
const char *const *x = a;
const char *const *y = b;
return strcmp(*x, *y);
}
/* Struct, nhieu tieu chi */
int ss_sv(const void *a, const void *b) {
const SinhVien *x = a, *y = b;
if (x->diem != y->diem)
return (x->diem < y->diem) - (x->diem > y->diem); /* GIAM dan */
return strcmp(x->mssv, y->mssv); /* pha the hoa */
}#Tham số ngữ cảnh
Ba cách giải quyết
/* Cach 1: qsort_r, mo rong cua GNU va cua BSD, hai ban KHAC nhau */
/* GNU: ngu canh la tham so CUOI cua ham so sanh */
static int ss_gnu(const void *a, const void *b, void *ngu_canh) {
const double *khoa = ngu_canh;
double x = khoa[*(const int *)a], y = khoa[*(const int *)b];
return (x > y) - (x < y);
}
qsort_r(idx, 4, sizeof idx[0], ss_gnu, khoa); /* GNU: ctx cuoi cung */
/* BSD va macOS: ngu canh la tham so DAU, va thu tu doi so khac han:
qsort_r(base, n, size, ctx, cmp)
Nen ma dung qsort_r khong kha chuyen giua Linux va macOS. */
/* Cach 2: goi VAO mang du lieu, khong dung chi so */
typedef struct { double khoa; int chi_so; } Cap;
static int ss_cap(const void *a, const void *b) {
double x = ((const Cap *)a)->khoa, y = ((const Cap *)b)->khoa;
return (x > y) - (x < y);
}
/* Ton them bo nho, nhung dung qsort chuan va khong co bien toan cuc. */
/* Cach 3: tu viet ham sap xep co tham so ngu canh */
void sap_xep(void *co_so, size_t n, size_t co,
int (*ss)(const void *, const void *, void *), void *ngu_canh);#Dừng sớm và báo lỗi
/* Callback tra ve void: khong dung som duoc */
typedef void (*HamTham)(int gia_tri, void *ctx);
/* Callback tra ve int: 0 la tiep tuc, khac 0 la dung */
typedef int (*HamTham2)(int gia_tri, void *ctx);
int duyet(const int *a, size_t n, HamTham2 f, void *ctx) {
for (size_t i = 0; i < n; ++i) {
int r = f(a[i], ctx);
if (r != 0) return r; /* tra ve MA cua callback */
}
return 0;
}
/* Nguoi dung: tim phan tu dau tien lon hon nguong */
typedef struct { int nguong, tim_thay; } Ctx;
static int kiem(int x, void *c) {
Ctx *ctx = c;
if (x > ctx->nguong) { ctx->tim_thay = x; return 1; }
return 0;
}
Ctx ctx = { .nguong = 100, .tim_thay = -1 };
if (duyet(a, n, kiem, &ctx))
printf("tim thay %d\n", ctx.tim_thay);| Kiểu trả về của callback | Dừng sớm được | Báo lỗi được | Dùng khi |
|---|---|---|---|
| void | Không | Không | Chỉ có tác dụng phụ, chắc chắn duyệt hết |
| int, 0 là tiếp tục | Có | Có, mã khác 0 | Trường hợp thường gặp nhất |
| bool, đúng là tiếp tục | Có | Không | Đọc dễ hơn, nhưng mất thông tin lỗi |
| enum ba giá trị | Có | Có, rõ ràng | API công khai, khi cần phân biệt kỹ |
#Bảy quy tắc thiết kế API callback
| Quy tắc | Vì sao |
|---|---|
| 1. Luôn có tham số void *ctx | Không có thì người dùng buộc phải dùng biến toàn cục |
| 2. Đặt ctx ngay sau con trỏ hàm | Đọc ra là thấy chúng đi với nhau |
| 3. Callback trả về mã để dừng sớm | Tìm kiếm và xử lý lỗi đều cần dừng giữa chừng |
| 4. typedef kiểu callback | Nếu không thì mọi khai báo đều khó đọc, xem Bài 29.1 |
| 5. Ghi rõ callback được phép làm gì | Sửa cấu trúc đang duyệt là lớp lỗi rất hay gặp |
| 6. Ghi rõ callback được gọi bao nhiêu lần và theo thứ tự nào | Người dùng cần biết để viết callback đúng |
| 7. Cho phép callback là NULL nếu hợp lý | Người chỉ cần một nửa chức năng không phải viết hàm rỗng |
Một API viết đúng
/* Duyet moi phan tu cua danh sach.
f duoc goi dung mot lan cho moi phan tu, theo thu tu tu dau
toi cuoi. Neu f tra ve khac 0 thi duyet dung ngay va ham nay
tra ve chinh gia tri do.
f KHONG duoc them, xoa, hay sap xep lai danh sach.
f co the doc va sua GIA TRI cua phan tu hien tai.
ctx duoc truyen nguyen ven cho moi lan goi f. No co the la NULL.
Tra ve 0 neu duyet het, hoac gia tri khac 0 dau tien ma f tra ve. */
int ds_duyet(DanhSach *ds, HamTham f, void *ctx);Tự làm thử
- Viết
dem_neunhận điều kiện và ngữ cảnh, rồi dùng nó cho ba điều kiện khác nhau. - Chạy chương trình so sánh
ss_truvàss_dungvới hai giá trị gần biên củaint. - Sắp xếp một mảng
const char *bằngqsort, thử cả hàm so sánh quên một tầng con trỏ. - Đưa một giá trị NaN vào mảng
doublerồi sắp xếp, xem kết quả ra sao. - Sắp xếp mảng chỉ số theo một mảng khóa nằm ngoài, bằng cách ghép cặp thay vì dùng biến toàn cục.
- Viết hàm duyệt có dừng sớm, dùng nó để tìm phần tử đầu tiên thỏa một điều kiện.
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
- Callback tách chính sách khỏi cơ chế: hàm nhận giữ vòng lặp, người gọi quyết định làm gì với từng phần tử.
- Hàm so sánh phải viết
(x > y) - (x < y), vì phép trừ tràn và cho ra kết quả sai dấu. - Với mảng con trỏ, đối số của hàm so sánh là con trỏ tới con trỏ, nên phải giải tham chiếu một lần trước.
- Mọi API nhận callback phải có tham số
void *ctx. Thiếu nó là ép người dùng dùng biến toàn cục. - Callback nên trả về mã để dừng sớm, và tài liệu phải ghi rõ nó được phép làm gì với cấu trúc đang duyệt.