1. コールバック関数とは?

コールバック関数は、名前を聞くだけで普通の関数よりもハイレベルに感じられますが、一体何なのでしょうか? 恐れながら私が読んだ本が少なく、どの本にもコールバック関数の定義は載っていませんでした。百度で検索してみると、諸説あり、その多くが次のようなシナリオで説明しています:A君がB君の店に買い物に行き、ちょうどに庫切れだったので、A君はB君に電話番号を残し、入荷したらA君に連絡してもらう、というものです。これはどちらかというと、コールバックではなく非同期操作を連想させます。また、印象に残った英文が2つあります:1) If you call me, I will call you back; 2) Don't call me, I will call you. 一見もっともらしいですが、よく考えると、普通の関数でもこの2つは実現できるのではないでしょうか? したがって、これらの説明はどれも適切ではないと思います。なぜなら、これらの説明はコールバック関数の特徴を表現しておらず、普通の関数との違いが見えてこないからです。ただし、百度百科の解説はまあまあ良いと思います(百度検索についてはよく批判されますが...):コールバック関数とは、関数ポインタを介して呼び出される関数のことです。関数のポインタ(アドレス)をパラメータとして別の関数に渡し、そのポインタが指す関数を呼び出すために使われたとき、それをコールバック関数と呼びます。

まず私の見解を述べます。「コールバック関数」を文字通り分解してみると、中国語では次の2つの意味に解釈できます:1) コールバックされる関数;2) 後から呼び出し動作を実行する関数。では、この「後から呼び出す」とは一体何なのでしょうか?

まずウィキペディアによるコールバック(Callback)の解説を見てみましょう。:In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback. つまり、実行可能なコードをパラメータのように他のコードに渡し、そのコードが特定の時点で呼び出されて実行されることをコールバックと呼びます。コードが即座に実行される場合は同期コールバックと呼ばれ、後で遅れた時点で実行される場合は非同期コールバックと呼ばれます。同期と非同期についてはここでは議論しませんので、関連資料を参照してください。

次に、Stack Overflowのある達人の簡潔で明瞭な説明を見てみましょう:A "callback" is any function that is called by another function which takes the first function as a parameter. つまり、関数F1が関数F2を呼び出すとき、関数F1はパラメータを介して関数F2に別の関数F3のポインタを渡します。関数F2の実行中に、関数F2が関数F3を呼び出します。この動作をコールバック(Callback)と呼び、先にポインタとして渡され、後でコールバックされる関数F3がコールバック関数です。これでコールバック関数の定義がおわかりいただけたでしょうか?

2. なぜコールバック関数を使うのか?

多くの方はこう思うかもしれません。「普通の関数呼び出しのように、コールバックの場所に関数名を直接書けばいいのではないか? それでもいいのではないか? なぜわざわざコールバック関数を使う必要があるのか?」 この考えは素晴らしいです。なぜなら、ネット上にあるコールバック関数を解説する多くの例は、実際には普通の関数呼び出しで完全に実現できるからです。この質問に答えるには、まずコールバック関数の利点と役割を理解する必要があります。それは「疎結合(デカップリング)」です。そう、これほど簡単な答えです。この特徴があるからこそ、普通の関数はコールバック関数の代わりにはなれません。したがって、私の目には、これこそがコールバック関数の最大の特徴です。ウィキペディアに掲載されている、とてもよく描かれていると思う画像を見てみましょう。

以下に、不完全なC言語コードで上の図の意味を示します:

実例

#include<stdio.h>
#include<softwareLib.h> // Library Functionが存在するSoftware libraryライブラリのヘッダファイルをインクルード

int Callback() // Callback Function
{
    // TODO
    return 0;
}
int main() // Main program
{
    // TODO
    Library(Callback);
    // TODO
    return 0;
}

一見すると、コールバックは関数間の呼び出しに過ぎず、普通の関数呼び出しと何ら変わりがないように見えます。しかし、よく見ると、両者の間には重要な違いがあることがわかります:コールバックでは、メインプログラムがコールバック関数をパラメータのようにライブラリ関数に渡します。これにより、ライブラリ関数に渡すパラメータを変えるだけで、異なる機能を実現できます。これはとても柔軟だと思いませんか? しかも、ライブラリ関数の実装を一切修正する必要がありません。これが疎結合です。さらに詳しく見ると、メイン関数とコールバック関数は同じ階層にあり、ライブラリ関数は別の階層にあります。考えてみてください。もしライブラリ関数が私たちから見えず、ライブラリ関数の実装を変更できないとすれば、つまりライブラリ関数の実装を変更して普通の関数を呼び出させるようなことはできないわけですから、異なるコールバック関数を渡すしかありません。これは日常業務でよくある状況です。では、main()、Library()、Callback()関数を先ほどのF1、F2、F3関数に当てはめてみると、もっと理解が深まるのではないでしょうか?

コールバック関数の特徴を理解すれば、どのような場面で使うべきかも大体わかるのではないでしょうか? その通り、多くの場面でコールバック関数を使って普通の関数呼び出しを置き換えることができます。しかし、私の考えでは、結合度を下げる必要がある場合には、よりコールバック関数を使うべきです。

3. コールバック関数の使い方

コールバック関数が何かを知り、その特徴を理解したところで、どう使えばいいのでしょうか? 次に、実行可能な簡単な同期コールバック関数のコードを見てみましょう。

実例

#include<stdio.h>

int Callback_1() // Callback Function 1
{
    printf("Hello, this is Callback_1 ");
    return 0;
}

int Callback_2() // Callback Function 2
{
    printf("Hello, this is Callback_2 ");
    return 0;
}

int Callback_3() // Callback Function 3
{
    printf("Hello, this is Callback_3 ");
    return 0;
}

int Handle(int (*Callback)())
{
    printf("Entering Handle Function. ");
    Callback();
    printf("Leaving Handle Function. ");
}

int main()
{
    printf("Entering Main Function. ");
    Handle(Callback_1);
    Handle(Callback_2);
    Handle(Callback_3);
    printf("Leaving Main Function. ");
    return 0;
}

実行結果:

Entering Main Function.
Entering Handle Function.
Hello, this is Callback_1
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_2
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_3
Leaving Handle Function.
Leaving Main Function.

ご覧のとおり、Handle()関数のパラメータはポインタです。main()関数でHandle()関数を呼び出すとき、関数Callback_1()/Callback_2()/Callback_3()の関数名を渡しています。このときの関数名は対応する関数へのポインタです。つまり、コールバック関数は関数ポインタの一種の使い方に過ぎないのです。では、この文をもう一度読んでみてください:A "callback" is any function that is called by another function which takes the first function as a parameter。より理解が深まるのではないでしょうか?

4. 引数付きコールバック関数の使い方

目の鋭い方はお気づきかもしれませんが、前の例ではコールバック関数に引数がありませんでした。では、引数付きの関数をコールバックできるでしょうか? 答えは「はい」です。では、どう呼び出すのでしょうか? 上の例を少し修正するだけでOKです:

実例

#include<stdio.h>

int Callback_1(int x) // Callback Function 1
{
    printf("Hello, this is Callback_1: x = %d ", x);
    return 0;
}

int Callback_2(int x) // Callback Function 2
{
    printf("Hello, this is Callback_2: x = %d ", x);
    return 0;
}

int Callback_3(int x) // Callback Function 3
{
    printf("Hello, this is Callback_3: x = %d ", x);
    return 0;
}

int Handle(int y, int (*Callback)(int))
{
    printf("Entering Handle Function. ");
    Callback(y);
    printf("Leaving Handle Function. ");
}

int main()
{
    int a = 2;
    int b = 4;
    int c = 6;
    printf("Entering Main Function. ");
    Handle(a, Callback_1);
    Handle(b, Callback_2);
    Handle(c, Callback_3);
    printf("Leaving Main Function. ");
    return 0;
}

実行結果:

Entering Main Function.
Entering Handle Function.
Hello, this is Callback_1: x = 2
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_2: x = 4
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_3: x = 6
Leaving Handle Function.
Leaving Main Function.

ご覧のとおり、int Handle(int (*Callback)()) を int Handle(int (*Callback)(int)) に変更するだけでは不十分で、別のパラメータを追加してコールバック関数の引数の値を保持する必要があります。ここでは int Handle(int y, int (*Callback)(int)) のパラメータ y がそれにあたります。同様に、複数の引数を持つコールバック関数も使用できます。

原文アドレス:https://www.cnblogs.com/jiangzhaowei/p/9129105.html