c++ 回调函数_1.1回调机制

在软件设计中,分层设计(Layered designs)是软件开发时常用的策略,例如管理信息系统常用的逻辑三层结构——表现层、业务逻辑层(business logic layer)和数据访问层(data access layer)。分层架构中,除(不依赖其他模块的)公共模块如信息系统中数据模块/model模块,可被各层调用外,一个层的模块只依赖于同一层或下层的模块。

单向依赖原则:依赖应该是单向的,避免循环或相互依赖。

为了讨论方便,可以简单地将代码分为两层。应用程序称为上层模块,而被应用程序依赖的库,称为下层模块/基础设施。比如说,Java程序开发可以粗略地分为两层:应用程序和JDK(和第三方包),其中应用程序属于上层模块,而JDK属于下层模块。分层架构遵循一条军规:上层模块依赖于下层模块。下层模块不可能依赖于上层模块。这是显而易见的。因为在设计下层模块/库时,不可能知道应用程序中使用什么类名、函数名等等,也就是说,下层模块对上层模块一无所知。(对于分层,本书点到为止。软件组织的探讨,更加抽象)。之所以提及和强调分层,因为回调机制内含分层的想法,即使在应用程序中,可以使用回调机制但是没有分层,事实上,隐含地分了层——程序员要做到单向依赖。

那么,什么是回调机制呢?

★一门编程语言,使得下层模块/库可以调用上层模块定义的代码的机制,即为回调(call back,一个动词词组)机制。而上层模块所定义的、被调用的代码,则被称为回调函数 (简称回调、callback,一个名词)。

既然下层模块不可能依赖于上层模块、下层模块对上层模块一无所知,那么下层模块为什么以及怎样调用上层模块的函数呢?这就是回调机制魅力所在。

下层模块调用上层模块的函数,有两种使用场景:

  • 框架中。此时下层模块中的框架,需要上层的应用程序提供具体的策略。本节介绍这方面的内容,通过一个例子的多种语言的实现,说明各种编程语言的核心技术。
  • 通知机制中。此时,下层模块调用上层模块的函数,目的是将下层知道的数据,通过实参传递给应用程序处理。[1.2好莱坞法则]介绍这方面的内容。

1.1.1什么是框架

按照wiki的说法:『在计算机编程中,软件框架是指一种抽象,该软件提供通用功能并通过用户编写的代码来有选择的变化,从而产生特定应用的软件。软件框架提供了一种标准的方式来构建并部署应用。软件框架是一种通用的、可复用的软件环境,它提供(作为一个更大的软件平台的一部分的)特定的功能,以促进软件应用程序、产品和解决方案的开发。软件框架可能会包含支撑程序、编译器、代码库、工具集以及 API,它把所有这些部件汇集在一起,以支持项目或系统的开发。』

这种解释非常复杂。作为OOD学习的出发点之一,这里需要给框架一个十分简化的定义

下层(通常说底层)模块,常常被称为库/ Library ( 简写 Lib ),库中包括工具箱与框架。因此,框架是与工具箱相比较而存在的概念。

工具箱是高级的子程序库。最简单的工具箱,常常被称为库函数,如java.lang .Math,它封装如函数abs、sin、max、random等,形成一个数学工具集;再如C语言的函数库;通常意义的工具箱,相对框架而言,它表现为一个完整的解决方案。如javaI/O流、Java集合类库、Spring依赖注入容器等。

框架/framework是在下层模块中定义的一个骨架式方案,处理特定应用中面临的共同的细节或流程。

★框架是骨架式方案,需要上层模块(为该骨架式方案)提供代码支持。

在事件驱动程序/GUI编程、Java远程方法调用(Remote Method Invocation, RMI),applet等框架中,框架封装了控制逻辑,能够以标准的方式来构建并部署应用。但框架仅仅是骨架,应用程序员在产生特定应用的软件、按照自己面临的需求而使用框架时,要给出自己的功能实现代码——按照框架填入自己的东西。形象地说,应用程序员使用框架时要填空式编程。例如GUI编程,程序员按照需求使用各种窗口、菜单、按钮和各种控件可以得到自己需要的界面布局,而界面布局的编程工作,其实与使用Math工具没有差别。真正的差别在于,框架的设计者不可能预知应用程序员在点击一个按钮时,应用程序应该如何响应,而应用程序员则要给出界面的响应代码/功能实现代码,此时,应用程序员必须按照框架的约束来填入自己的东西。

所以,框架这种骨架式方案,显然地,它需要上层模块提供支持代码,因此需要回调机制。

1.1.2回调机制的实现

现在举一个例子,使读者对框架、回调机制等概念有一些感性认识。然后再介绍该例子背后蕴涵的道理。

【实验1:回调机制的实现】假定下层模块/框架中需要设计一个op函数,对两个参数值进行“某种”操作后返回一个值。

框架设计者不可能预判应用程序员可能进行何种操作,可能是相加,比较、求幂等等。那么如何调用应用程序员的代码,或者说下层要如何调用(call)上层的代码呢?各种编程语言采用了不同的方式来实现回调机制。在实现回调机制(或者说定义“某种”操作,参考《第2章行为参数化》)时,各门编程语言提供的实现,均涉及该语言的精华概念,而这些精华概念解决同一个问题。

1.C语言的回调机制

C语言中经典回调机制的例子,是其标准库qsort函数,其函数原型为

void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void * ) );

它对数组按照“某种”比较规则进行排序。

对于本实验的op函数,简化起见,假定对两个int参数进行某种操作,这时op函数需要第三个参数来刻画进行“何种”具体的操作。

★C语言采用函数指针来实现回调机制。

应用程序/上层模块包括函数plus和main。

//例程 1-1   C语言之函数指针

以上层模块的main为出发点。main调用了下层的函数op,并在调用时将一个函数plus的指针作为实参传递给how_op形参;而op执行过程中将“回过头来调用”上层定义的函数plus。某种程度上,在C语言中,回调机制/Call back这个术语非常形象和直观

引人注目的op函数中的第三个参数,即指针int (*How_op)( int, int),它指定了函数op能够使用的函数的种类,表示为(int, int)→int。可以将各种函数名赋值给一个函数指针,但这些函数必须具有规定的形参列表和返回值类型,即函数指针的种类。

2. Scheme语言的回调机制

函数式编程语言如Scheme中,将函数作为一个数据类型。

★以函数作为参数或返回值的函数,被称为高阶函数(Higher-Order function)

Scheme语言定义函数op,和C语言相似,也需要第三个参数来刻画进行“何种”具体的操作,而op就是一个以函数how-op为形参的高阶函数。

;;;例程 1-2   Scheme语言之高阶函数

在Scheme语言中,由于高阶函数无处不在,高阶函数是该语言的基本特点,它通常不需要回调机制、回调函数等术语。Scheme程序员在应用函数op时,如(op 1 3 plus),不屑说:“上层模块call函数op,而op call back plus”。

3.Java语言的回调机制

具有动态绑定能力的面向对象的Java语言,不同于C和Scheme,不需要第三个参数来刻画进行“何种”具体的操作,因为抽象函数op本身就意味着对两个值进行“某种”操作。抽象函数op由二元操作/BinaryOP封装,在此请停下脚步,体会一下面向对象语言的强大之处:定义函数op时,不需要第三个参数来指定进行“何种”具体的操作;BinaryOP的实现类如AddOp,指定进行何种具体的操作——加法。

package 

Java可以通过BinaryOP的子类型提供支持代码,而更多的提供支持代码的方式,将在[1.1.4匿名类和λ表达式]中介绍。

从实现回调机制的角度看,各语言使用的技术、术语如下

  • C,函数指针
  • Scheme,高阶函数
  • Java,动态绑定/多态

1.1.3回调函数

上层模块所提供的支持代码,是否需要一个术语——例如回调函数(callback)来描叙呢?

Scheme语言中,因为高阶函数的存在,连回调机制这个术语都不必要,更不需要为(应用程序调用框架时,能够)被作为实际参数的函数,设定一个特别的名词;

C语言中,回调机制/Call back很形象。但是,只要符合函数指针指定的函数种类的所有上层函数,都是“潜在的”支持代码。实践中很难将“被作为参数的函数”和“能够作为参数的函数”加以区分。有人给出如下定义:回调函数指可以被作为参数传递给其他代码的可执行代码块,或者一个可执行代码的引用。这种C语言视角的回调(函数)定义,很容易被误用于行为参数化的讨论中,是一个尴尬的存在。

Java中,回调函数与一般函数有着明显的区别。回调函数是上层模块为下层提供的支持代码,是为框架准备的。

★ (Java中,通常) 上层模块中编写的、上层模块不会自己去调用的函数,即回调函数。

典型的回调函数例子,Java多线程中run()、GUI中各种事件处理函数如actionPerformed()。例程1-3中,下层模块/框架BinaryOP中定义的抽象方法op,称为回调接口;上层模块,BinaryOP的实现类如AddOp给出的@Override op即回调函数。

综上所述,就引出了一个有意思的问题:人们定义一个(如回调函数的)概念时,能否通用于各种编程语言中?

1.1.4匿名类和λ表达式

Java语言中使用类层次/动态绑定/多态来提供回调函数/支持代码。

当应用程序Test需要使用BinaryOP时,可以编写BinaryOP的、有单独的.java文件的实现类如AddOp,以提供回调函数;如果实现类不具有复用性,或者说不具有独立存在的价值,可以编写BinaryOP的匿名类。

匿名类在定义其类体的同时,创建了自己的对象,并向上造型为父类型。匿名类没有名字,其类型定义和其对象的创建同时发生。

例程 

匿名类最大的问题就是“代码高度问题”,它和单独实现类一样,具有完整的类体结构,包含很多公式化的代码,而程序员通常在意的仅仅是op的方法体,因此匿名类显得占用太多的行数。为了避免笨重而啰嗦的代码,减少代码的行数,Java 8提供了λ(lambda)表达式。忽略Java编译器对λ表达式与匿名类的不同处理,可以认为:λ表达式是(函数接口的)匿名类的语法糖。技术上,lambda表达式并没有提供新能力,现在程序员能够做的事情,在Java 8之前完全能够做。

Java8引入的λ表达式对特定的(函数接口)匿名类进行精简,简化方式如图1-1所示。

c0e93adae3ab9b938b684e30cb17e930.png
图 1 1 λ表达式的格式

精简过程包含:

①删除new BinaryOP()。这就暗示着λ表达式对使用场景有一些要求,编译器必须能够从使用场景/环境/上下文中能够推断λ表达式的父类型——BinaryOP;

②删除op函数的方法头,包括@Override、修饰符、返回值类型和函数名,因为BinaryOP的op的这些东西是已知的。找到匿名类的父类型——某个函数接口(Functional interfaces)后,之所以能够省略函数名,是因为函数接口具有一个必须的特点:它是SAM(单一抽象方法/Single Abstract Method)形式的接口,它只有唯一的抽象方法,而该抽象方法的名字就是λ表达式所省略的函数名。

为什么要保留形参列表?因为方法体的代码中用到了a和b,需要交代其含义;

③形参列表中变量的数据类型也可以删除,因为op参数的数据类型,是已知的。通常λ表达式不需要写出形参列表中参数的类型,某些时候,显式地写出参数的数据类型可能使程序更具有可读性,程序员可以从可读性上决定是否省略形参的类型;不论省略与否,编译器能够进行类型推断。如果形参列表中只有一个参数时,可以省略形参列表的括号。但是没有形参时,需要一个表示形参列表的一个空括号。

④对匿名类的函数体也可以简化。如果函数体为单一的语句而且返回一个结果时,函数体简化为return后的表达式。

因此,一个λ表达式只能够用于能确定其父类型——称为该λ表达式的目标类型(Target typing)的场合,例如赋值语句、作为实参和强制类型转换中,而且目标类型必须是函数接口(可以用@FunctionalInterface来强调这一点)。

例程 

通过λ表达式对匿名类进行精简的过程,可知λ表达式由三部分组成:(参数列表),英文减号和大于号构成的箭头->,函数体。

λ表达式是什么?从语法的角度看:

★lambda表达式是函数接口的一个具体匿名类(的对象的引用)。

★lambda表达式是(函数接口的)匿名类的语法糖。

λ表达式看起来如同一个函数,lambda这个词源于邱奇的拉姆达运算/lambda calculus。但是,Java 8并没有将函数设计为一种数据类型而是依靠原有的类型系统,因此Java的lambda表达式不过是对函数式编程语言如Scheme中的lambda表达式的模拟,两者有本质上的不同。在语法上,Java的λ表达式毕竟不是一个函数。

另外需要注意的是,Java的λ表达式并不是没有名字(匿名类没有程序员定义的类名——编译器会给出一个名字,不要想当然地将Java的λ表达式称为匿名函数)而是省略了名字

函数式编程语言如Scheme中,依据丘奇的λ演算,它的lambda表达式定义了匿名(没有名字)函数,当然也可以为匿名函数命名。

((lambda (x) (* x x)) 2 ) ;;;→ 4
( define (square x) (* x x) )
(square 2) ;;; → 4 函数应用

Java的λ表达式省略函数名,正是因为λ表达式肯定使用某个特定的函数名,而被省略的名字就是其目标类型——一个函数接口的唯一抽象方法。这一省略了名字的函数(Java的λ表达式)依靠或者说带来了相关概念:函数接口、目标类型。

BinaryOP clazz = (m, n) -> m + 2 * n; //这里省略的函数名是op

IntBinaryOperator op= x->2*x; //JDK中的接口,省略的函数名是什么呢?大多数情况下程序员不关心。

虽然程序员可以简单地认为λ表达式是(函数接口的)匿名类,但是λ表达式和匿名类有两点不同:①编译器并没有将λ表达式简单地转换成匿名类。编译器会为每个匿名类生成一个新的.class文件,而编译器则通过invokedynamic指令,把λ表达式的字节码的生成延迟到运行时。②λ表达式与匿名类中使用this时,含义不同,请参阅[2.1.2条件的组合]。


1.我看见网上一些对回调机制的介绍,比如如下的说法:将函数调用分为同步调用(直接调用)和异步调用,再将回调称为双向调用。

回调的思想是:
类A的a()方法调用类B的b()方法
类B的b()方法执行完毕主动调用类A的callback()方法

这种回调说法,我认为没有意思,它也不能够与“回调函数指可以被作为参数传递给其他代码的可执行代码块,或者一个可执行代码的引用”这样的说法统一。因此,回调机制并非讨论函数间的调用问题,也绝对不应该是两个类型如类A与类B中间的函数调用关系,而应该是上层与下层中间的函数调用关系——地基/框架/下层模块为什么以及如何调用应用程序的代码。

2. 关于Java的λ表达式,[Java8实战]写得不太好,特别是错误地将Java的λ表达式称为匿名函数。该书没有将Java的λ表达式是什么(函数接口的一个具体匿名类(的对象的引用))与Java的λ表达式需要看成什么(一个函数)分开,所以写得混乱。其实,我认为对Java的λ表达式,按照我写的这些加以介绍就足够了,很多东西读者可以自己脑补或推导。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值