Bỏ qua điều hướng, tới nội dung chính
Học C
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 callbackDừng sớm đượcBáo lỗi đượcDùng khi
voidKhôngKhôngChỉ có tác dụng phụ, chắc chắn duyệt hết
int, 0 là tiếp tụcCóCó, mã khác 0Trường hợp thường gặp nhất
bool, đúng là tiếp tụcCóKhôngĐọc dễ hơn, nhưng mất thông tin lỗi
enum ba giá trịCóCó, rõ ràngAPI công khai, khi cần phân biệt kỹ

#Bảy quy tắc thiết kế API callback

Quy tắcVì sao
1. Luôn có tham số void *ctxKhô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ớmTìm kiếm và xử lý lỗi đều cần dừng giữa chừng
4. typedef kiểu callbackNế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àoNgườ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ử

  1. Viết dem_neu nhận điều kiện và ngữ cảnh, rồi dùng nó cho ba điều kiện khác nhau.
  2. Chạy chương trình so sánh ss_tru và ss_dung với hai giá trị gần biên của int.
  3. Sắp xếp một mảng const char * bằng qsort, thử cả hàm so sánh quên một tầng con trỏ.
  4. Đưa một giá trị NaN vào mảng double rồi sắp xếp, xem kết quả ra sao.
  5. 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.
  6. 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.