如何寫一個回調(diào)?


在本文中,我們將介紹在c、java這三種編程語言中如何在把函數(shù)作為一個參數(shù)傳入另一個函數(shù)中,以及分析這些語言函數(shù)傳參內(nèi)部的原理。

  • c語言 (函數(shù)指針)
  • Java (lambda表達式)

一、c語言與函數(shù)指針

  1. 指針函數(shù)和函數(shù)指針
    在介紹函數(shù)指針的使用之前我們先要分清楚和它長得特別想的一個概念——指針函數(shù)
    來人啊,上定義!

函數(shù)指針

指針函數(shù)是指帶指針的函數(shù),即本質(zhì)是一個函數(shù),函數(shù)返回類型是某一類型的指針。

比如說:

int * add(int a,int b){
     int c = a+b;
     return &c;
}

這就是一個指針函數(shù),它的接受兩個int值的參數(shù),返回一個int類型的指針。

指針函數(shù)

函數(shù)指針是指向函數(shù)的指針變量,即本質(zhì)是一個指針變量

函數(shù)指針的定義形式如下:

函數(shù)返回值類型 (* 指針變量名) (函數(shù)參數(shù)列表);

我們可以這樣去使用一個函數(shù)指針——

int add(int a,int b){
    return a+b;
}

int main(){
    int (*fun)(int,int);
    fun = &add;
    int c = (*fun)(1,2);
    printf("%d\n",c);
    return 0;
}

上面代碼main函數(shù)中的第一行就定義了一個函數(shù)指針int (*fun)(int,int),這個函數(shù)的參數(shù)列表中有兩個int類型的參數(shù),返回值類型是,并將add函數(shù)的首地址賦給了這個指針fun,在后面我們就像調(diào)用了這個函數(shù),結(jié)果是顯而易見的,打印出來的c值為3。
這個過程是不是有些熟悉?想想我們平常所用的變量指針,我們會寫如下的代碼:

int main(){
    int a = 0;
    int * p;
    p = &a;
    printf("%p\n",p);
}

與之前的代碼進行比較,其區(qū)別在于指針指向的函數(shù)而不是變量,而我們調(diào)用這個函數(shù)指針所指向的函數(shù),得到了返回值并打印。

  1. c語言的函數(shù)回調(diào)
    首先先說一下什么是回調(diào),回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。emmm其實還蠻好理解的嘛,就是把函數(shù)作為參數(shù)傳進去嘛,這么機智的你一定想到了可以在函數(shù)的參數(shù)列表中接收一個函數(shù)指針類型的參數(shù)~沒錯就是醬紫?° (?′?`?):

    int add(int a,int b){
        return a+b;
    }
    
    void dosome(int (*callback)(int a,int b))
    {
        printf("%d\n",(*callback)(1,2));
    }
    
    int main(){
        dosome(&add);
    }
    

    我們把add函數(shù)作為參數(shù)傳給了dosome,在dosome函數(shù)中調(diào)用了這個callback函數(shù)(也就是我們傳入的add函數(shù)),并把返回值打印了出來,輸出結(jié)果顯而易見也是3。是不是寫一個回調(diào)特別的簡單呢~當然我們可以定義不同參數(shù)列表和返回值的函數(shù)指針來滿足我們不同的需求。

  2. 函數(shù)指針內(nèi)部原理
    在講內(nèi)部原理之前,先需要預(yù)備一些匯編的知識。計算機并不能識別我們平時寫的c語言程序,真正物理機器可以識別的,就是一串串二進制01流。我們用c語言寫的程序,到最后都會被編譯成二進制指令。在函數(shù)被執(zhí)行時,由eip指向下一條要被執(zhí)行的指令,CPU 會取出這一條指令并且執(zhí)行。就比如之前的add函數(shù),編譯成對應(yīng)的匯編語言就是:

    //c語言函數(shù)
    int add(int a,int b){
        return a+b;
    }
    
    //對應(yīng)匯編
    add:
    pushl %ebp 
    movl %esp,%ebp
    subl $16,%esp
    movl $3, -4(%ebp)
    movl -4(%ebp),%eax
    leave
    ret
    

    注:匯編語言其實就是機器指令的助記符,為方便展示,此處使用匯編語言

    而這些代表程序代碼的01流被存放在了內(nèi)存區(qū)域中的Text段。之前我們提到過,函數(shù)指針是一個指針,因此,它和別的指針并沒有什么太大的差異,只不過其它指針一般指向的是Heap、Stack和Data區(qū)中的變量,而函數(shù)指針如果指向函數(shù)的話,指針指向的是存放在Text段中,這個函數(shù)編譯得到的二進制串的首地址。

    在我們調(diào)用這個函數(shù)時,就把這個地址賦值給了eip寄存器,然后,CPU讀取到地址中存放的指令,開始執(zhí)行這一段函數(shù)。而給函數(shù)指針賦不同的地址值,其指向的函數(shù)(二進制代碼)也就不同,我們可以通過一個函數(shù)指針賦不同的值來調(diào)用不同的函數(shù)。

  3. 來點騷操作?
    之前我們說函數(shù)指針如果指向函數(shù)的話,指針指向的是存放在Text段中,這個函數(shù)編譯得到的二進制串的首地址。但是我們一定要指向一個函數(shù)嗎?答案是否定的,我們可以自己把一個函數(shù)的二進制代字符流存在一個char數(shù)組中,并讓函數(shù)指向這個數(shù)組。

    //add函數(shù)對應(yīng)的二進制代碼
    const unsigned char code[]="\x55\x89\xe5\x8b\x45\x0c\x8b\x55\x08\x01\xd0\x5d\xc3";
    
    int main(){
      int (*fun)(int,int);
      fun = (void*)code;
      r = fun(1,2);
      printf("%d\n",r);
    }
    

    在本實例中,定義了一個全局數(shù)組 code,code數(shù)組里保存的若干字符就是機器指令,這些制定與我們之前定義的add函數(shù)作用一樣,在main函數(shù)中通過int (*fun)(int,int)定義了一個指針函數(shù) fun ,接著通過 fun = (void*)code 將該指針函數(shù)指向一個內(nèi)存地址,也就是code數(shù)組的首地址,最后通過 r = fun(1,2); 就可以調(diào)用這個指針函數(shù)了。當這段c程序執(zhí)行到 r = fun(1,2) 這條指令時,就會將CS:IP 段寄存器指向了code首地址,從而code數(shù)組所在的一塊連續(xù)內(nèi)存區(qū)域被當作代碼來執(zhí)行。不出意外這段代碼的運行結(jié)果,依舊是3。
    值得一提的是,這也是JVM中函數(shù)執(zhí)行的奧秘所在,JVM會將由解釋器或者即時編譯的生成的二進制文件存放在內(nèi)存的某一個區(qū)域,在需要執(zhí)行的時候,將函數(shù)指針指向想要執(zhí)行的二進制代碼的首地址,并執(zhí)行這個函數(shù)。

二、Java與 lambda表達式

  1. java 回調(diào)
    在jdk1.8之前還沒有l(wèi)ambda表達式的時候,我們一般會定義一個接口,然后接收這個接口類的對象,調(diào)用這個對象的方法:
    //OnClickListener.java
    interface OnClickListener{
        void onClick();
    }
    
    //OnClickListenerImpl.java
    class OnClickListenerImpl implements OnClickListener{    
        @Override
        void onClick(){
               System.out.println("clicked");
        }
    }
    
    //Test.java
    class Test{
        void static onClicked(OnClickedListener listener){
            listener.onClick();
        }
        
        public static void main(String []args){
            onClicked(new OnClickListenerImpl());
        }
     }
    
    當然我們可以用匿名內(nèi)部類來簡化這種寫法:
    //OnClickListener.java
    interface OnClickListener{
        void onClick();
    }
     
    //Test.java
    class Test{
        void static onClicked(OnClickedListener listener){
            listener.onClick();
        }
        
        public static void main(String []args){
            onClicked(new OnClickListener(){
                  @Override
                  void onClick(){
                       System.out.println("clicked");
                  }            
            });
        }
     }
    

2.為什么要需要傳一個接口的實現(xiàn)?
從上面的代碼我們可以看出,如果不用lambda表達式的話,其實Java寫回調(diào)似乎是要比c語言的函數(shù)指針要麻煩一些的。我們需要實現(xiàn)一個接口,并把這個接口的實例傳進去。

  • java是一門純粹的面向?qū)ο笳Z言,不允許函數(shù)脫離類而存在。
  • 在jdk1.7以前,java又沒有動態(tài)語言支持,不可以隨便一個類中的同參數(shù)列表、同返回值的函數(shù)就可以被函數(shù)調(diào)用。
  • 在Java中不允許多繼承
    所以,最好的解決辦法就是使用接口作為函數(shù)和參數(shù)之間的橋梁。
  1. 函數(shù)句柄
    那在Java中有沒有類似函數(shù)指針的概念呢?筆者個人認為,在jdk1.7中出現(xiàn)的java.lang.invoke包中的MethodHandle就是類似的概念。
    MethodHandle如何使用呢?請參考 http://www.itdecent.cn/p/a9cecf8ba5d9
    這篇文章

  2. lambda表達式:
    Lambda表達式是java8的重要更新,可以替代傳統(tǒng)的匿名內(nèi)部類去實現(xiàn)參數(shù)的回調(diào),比如之前的代碼可以簡化如下

    //OnClickListener.java
    interface OnClickListener{
        void onClick();
    }
     
    //Test.java
    class Test{
        void static onClicked(OnClickedListener listener){
            listener.onClick();
        }  
    
        public static void main(String []args){
            onClicked(()->System.out.println("clicked"));
        }
     }
    

    lambda表達式的寫法為

    (參數(shù)列表)->{
         //程序代碼
    }
    

    除此之外,lambda表達式還有著一定的限制,就是lambda表達式所表示的接口中只能有一個非default方法。

  3. lambda表達式的內(nèi)部原理
    看完如此簡潔的lambda表達式,我們應(yīng)當去一探其內(nèi)部的實現(xiàn)原理。
    其實我們也可以猜測一下,我們
    在java語言層面我們自然是看不出來什么東西了啦,不如去探究一下lambda表達式編譯后的字節(jié)碼,于是編寫demo如下:

    interface Add{
         void add(int a,int b); 
    }
    class Test {
        public static void main(String []args){
              Add add = (int a,int b) -> a+b;
              
        }
    }
    

Test類的編譯后的字節(jié)碼如下:

// class version 52.0 (52)
// access flags 0x21
public class com/example/Test {

  // compiled from: Test.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/Test; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 9 L0
    INVOKEDYNAMIC add()Lcom/example/Add; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (II)I, 
      // handle kind 0x6 : INVOKESTATIC
      com/example/Test.lambda$main$0(II)I, 
      (II)I
    ]
    ASTORE 1
   L1
    LINENUMBER 10 L1
    ALOAD 1
    ICONST_1
    ICONST_2
    INVOKEINTERFACE com/example/Add.add (II)I
    ISTORE 2
   L2
    LINENUMBER 11 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE add Lcom/example/Add; L1 L3 1
    LOCALVARIABLE c I L2 L3 2
    MAXSTACK = 3
    MAXLOCALS = 3

  // access flags 0x100A
  private static synthetic lambda$main$0(II)I
   L0
    LINENUMBER 9 L0
    ILOAD 0
    ILOAD 1
    IADD
    IRETURN
   L1
    LOCALVARIABLE a I L0 L1 0
    LOCALVARIABLE b I L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2
}
 

其中<init>是編譯器幫我們生成的無參構(gòu)造函數(shù)。
而lambdamain0是編譯器根據(jù)我們寫的lambda表達式生成的一個靜態(tài)函數(shù)

reference

  1. 函數(shù)指針與指針函數(shù)
  2. 函數(shù)指針定義
  3. 《揭秘Java虛擬機》 ——封亞飛
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容