分类 技术志 下的文章

Observer模式的C实现

设计模式 (Design Pattern,以下简称DP)的定义有很多种。我个人的理解:DP是人们在软件开发过程中所总结出来的一些典型问题的经验解决方法模板。使用它们可以使我们的代码更易被复用,更易扩展,更好地适应变化以及更便于后期维护。

人们都说设计模式是独立于语言的,但这里的"语言"更多的是指面向对象语言,比如Java、C++、C#、Python和Ruby等。使用面向对象语言(OO)在实现设计模式时更为自然而然。GoF的经典书籍《Design Patterns》 的副标题就是"Elements of Reusable Object-Oriented Software",显然DP主要针对面向对象的软件开发,书中的内容也主要是用C++表述的。

相比于OO语言,关于C语言 等面向过程的语言与设计模式结合使用的资料和例子都甚少,它们就像是走在两条互相平行的马路上的路人,老死不相往来。难道我们真地找不到C与设计模式的交集吗,非也!设计模式强调高内聚,低耦合 ,一切面向接口!这种思想其实也有助于C程序员写出更加模块化、更加灵活以及更为优美的代码来。只是用C语言实现后所展现出来的形式与支持继承、多态的OO语言相比可能不是那么自然。

我相信在现实使用C语言的开发过程中,有些C程序员已经在不知不觉中使用了模式的思想,但DP对于多数C程序员还是略显生分的,虽然DP已经诞生10多年了,也许这与C语言诞生在设计模式之前不无关系^_^。

如何在日常开发中融入模式的思想呢?GoF在《Design Patterns》一书的第一章就告诉了我们,大致是一切从问题出发:弄清楚你遇到的问题,浏览模式,找到适合解决你的问题的模式。

言归正传。我们的系统常常有这样的业务情景:某种数据对象发生变化,与其相关的其他数据对象也需要一并做出改变。为了便于理解,这里举一个大家都比较易懂的例子:Tony是一个中国移动全球通(GoTone)用户,他订购了中国移动提供的139邮箱和手机报业务。中国移动有一个系统用于管理全球通用户信息、各种移动业务以及全球通用户的各种业务订购关系。Tony这个人记性不大好,每天丢三落四,同时也经常忘记及时缴纳手机话费,导致系统中Tony这个用户的状态经常在"正常"和"停机"间变来变去。为了防止Tony在停机的状态下依旧可以使用邮箱和手机报业务,移动公司提出了一个新需求:当用户处于"停机"状态时,用户应该无法使用139邮箱和手机报业务。如果你是负责开发这个需求的程序员,你如何在系统中满足这个需求呢?

将客户提出的需求翻译为程序员的行话就是当Tony的用户信息由"正常"变为"停机"时,系统需要同时修改139邮箱订购关系数据和手机报订购关系数据,将两类数据记录中Tony的订购关系由"开通"变为"暂停";当Tony的用户信息由"停机"变为"正常"时,系统则需要将两类数据记录中Tony的订购关系由"暂停"改为"开通"。总之,当全球通用户信息发生变化时,139邮箱订购关系与手机报订购关系数据就需要随着进行变更。

从例子中已有的描述可以看出,现有的系统内部至少有三套数据集以及相应的数据操作接口,分别是全球通用户数据、139邮箱订购关系数据和手机报订购关系数据,这也是最基本的数据封装与抽象。现在我们就在此基础上来满足新的需求。

很多人首先想到的是修改全球通用户数据操作接口,实现两类订购关系的随动变更。
void update_gotone_customer_state(const char *number, int state) {
    /* 根据number查找出对应的用户信息,并更新其state */
    … …

    /* 新增如下操作 */
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
}

我们可以看出这种方法是在全球通用户数据操作接口中直接调用两种订购关系的数据操作接口来修改数据状态,这种方法显然是耦合最高的方法,它在本无耦合的数据对象之间建立了耦合。update_mailbox_order和update_newspaper_order的变化将直接导致update_gotone_customer_state接口的变化。我们也无法独立地对update_gotone_customer_state接口进行单元测试了,除非对新增的两个依赖接口进行mock — 显然这种mock是被动的,也是不合理的。

为了去除这一方法引入的耦合,我们可以引入一个新函数来作为用户状态变更时的处理函数,如下:
void gotone_customer_state_switch(const char *number, int state) {
    update_gotone_customer_state(number, state);
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
}

这种方法依次调用三个数据集各自的操作接口更新用户状态。显然这种方法去除了数据集操作接口之间的耦合,但也存在着另外一个更严重的问题,那就是这种方法难以适应移动业务日新月异的变化。

假定此时用户Tony又订购了一个移动业务 -手机电视,那么如何让手机电视订购关系感知到用户状态的变化呢?你可能会这样来改。
void gotone_customer_state_switch(const char *number, int state) {
    update_gotone_customer_state(number, state);
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
    update_mobiletv_order(number, state);
}

你知道这样的修改意味着什么吗?意味着程序要重新编译,重新发布以及重新部署上线。一个用户新订购了一个业务就带来如此大的变动,这是不能忍受的,不是吗!

关于这类问题,DP给出了一个解决模式,即Observer模式,中文叫作观察者模式。观察者模式中有两个主要元素:主题(Subject)和观察者(Observer),主题的变更引发诸多相应观察者的随动更新。在这个例子中,全球通用户数据显然是一个Subject,而139邮箱、手机报以及手机电视等业务订购数据则是Observer的角色。

全球通用户数据是一个Subject,但是却是一个具体的Subject,如果让其与139邮箱等具体业务Observer直接关联,势必会像最初的解决方法那样在不同数据对象间引入耦合。DP始终在告诉我们要面向接口,要依赖抽象,所以我们需要建立抽象的Subject和Observer接口来。在C语言中没有interface关键字,也没有abstract class(抽象类),C语言只有struct和函数指针。将struct和函数指针结合,我们就有了C语言接口的概念:

/* isubject.h */
struct iobserver_t;

struct isubject_t {
    void (*attach)(struct isubject_t *subject, struct iobserver_t *observer);
    void (*detach)(struct isubject_t *subject, struct iobserver_t *observer);
    void (*notify)(struct isubject_t *subject, void *arg);
};

/* iobserver.h */
struct iobserver_t {
    void (*update)(void *arg);
};

isubject_t声明了三个函数指针字段,attach用于增加监视这个Subject的Observer的;detach用于卸载监视这个Subject的某个Obsever;而notify则是在Subject发生变化时用于通知各个Observer进行更新的。iobserver_t相对比较简单,只有一个update函数指针字段,该字段指向的函数在Subject发生变化时被调用,完成Observer自身的更新。

isubject_t只是一个抽象的接口,我们还需要isubject_t的一个具体实现,包括attach, detach以及notify等函数的具体实现算法。下面是isubject_t的一个具体实现isubject_imp_t的相关数据类型与操作接口:

/* isubject_imp.h */
struct isubject_t* isubject_imp_new();
void isubject_imp_free(struct isubject_t **subject);

/* isubject_imp.c */
typedef struct _iobserver_t _iobserver_t;

/* 这里用apache apr库中的apr_ring作为存储observers的数据结构 */
typedef APR_RING_HEAD(_iobserver_head_t, _iobserver_t) _iobserver_head_t;

struct isubject_imp_t {
    struct isubject_t subject; /* 这里务必将subject放在第一个字段的位置 */
    _iobserver_head_t observers;
};

struct isubject_t* isubject_imp_new() {
    struct isubject_imp_t *p = NULL;

    p = (struct isubject_imp_t*)malloc(sizeof(*p));
    if (!p) return NULL;

    memset(p, 0, sizeof(*p));
    APR_RING_INIT(&(p->observers), _iobserver_t, link);
    p->subject.attach = isubject_imp_attach;
    p->subject.detach = isubject_imp_detach;
    p->subject.notify = isubject_imp_notify;

    return (struct isubject_t*)p;
}

static void isubject_imp_attach(struct isubject_t *subject, struct iobserver_t *observer) {
    struct isubject_imp_t *p = (struct isubject_imp_t *)subject;

    //将observer插入ring,这里代码省略
}

static void isubject_imp_detach(struct isubject_t *subject, struct iobserver_t *observer) {
    struct isubject_imp_t *p = (struct isubject_imp_t *)subject;
   
    //将observer从ring中移出,这里代码省略
}
   
static void isubject_imp_notify(struct isubject_t *subject, void *arg) {
    struct isubject_imp_t *imp = (struct isubject_imp_t *)subject;

    //遍历ring,调用每个observer的update接口,并传入参数arg,这里代码省略
}

注意以上操作都是面向isubject_t这个接口类型的,而不是isubject_imp_t这个具体类型的,isubject_imp_t对于外部是不可见的。下面我们尝试用Observer模式解决一下上面例子中的问题。

首先建立tony这个用户的subject,并初始化其当前的业务Observer:
struct isubject_t *tony_subj = isubject_imp_new();
struct iobserver_t mailbox_observer = {
        .update = update_mailbox_order
};
struct iobserver_t newspaper_observer = {
        .update = update_newspaper_order
};

tony_subj->attach(tony_subj, &mailbox_observer);
tony_subj->attach(tony_subj, &newspaper_observer);

恰巧Tony上个月又忘记缴纳话费了,月初Tony的手机被停机了。中国移动的系统进行了Tony这个账户的状态变更操作:
tony_subj->notify(tony_subj, tony_account_info); /* tony_account_info表示Tony的基本账户信息 */

这行代码的执行结果是update_mailbox_order和upudate_newspaper_order被调用,139信箱以及手机报订购关系中有关Tony的订购信息状态变为"暂停"。也许上面的代码还没有让你看到Observer模式的好处,Ok,让我们进行一些变化!

移动为了推广3G业务,允许用户免费试用3个月的手机电视业务。有便宜不能不占,Tony遂订购了手机电视业务。前面的两种方法对于此种情况都无能为例,但采用了Observer模式之后,对于手机电视的订购,系统只需要在相应的处理函数中执行:
struct iobserver_t mobiletv_observer = {
        .update = update_mobiletv_order
};
tony_subj->attach(tony_subj, &mobiletv_observer);

这样一来我们无需修改Tony被停机时的处理代码,手机电视订购关系也可以感知到Tony的账户变更。

同样,Tony使用了移动的手机上网套餐,打开手机浏览器,天下信息便可一览无余。于是Tony决定退订有些鸡肋的手机报业务,对于这一退订事件,系统只需要在相应的处理函数中执行:
tony_subj->detach(tony_subj, &newspaper_observer);

可以看到Tony停机的处理无需因手机报退订而做出改变,同时Tony停机也不会导致手机报订购关系数据的变更,这正是我们期望的。

以上是Observer模式的一种C语言实现,同时也解决了我们遇到的实际问题。关于Observer模式的详细描述还是参见GoF的《Design Patterns》一书吧。用过程式语言实现DP的确不那么自然,但这就是C语言的方式。

C程序员驯服Common Lisp – 函数

Common Lisp函数式编程语言,其基本组成单元自然是函数。对Common Lisp函数的理解也是学习Common Lisp语言的关键。另外与C语言以内存单元修改为主要编程方法不同,Common Lisp的主要编程方法是将函数应用于参数。这里我们分别用两种范式风格实现同一个函数,该函数用于取得第n个fibonacci数(n从0开始):

;; 命令式风格
(defun imperative-fibonacci (n)
    (let ((first 0)
          (second 1)
          (sum 0))
        (dotimes (i n)
            (progn
                (setf sum (+ first second))
                (setf first second)
                (setf second sum)))
        first))

;; 函数式风格
(defun functional-fibonacci (n)
    (cond 
        ((= 0 n) 0)
        ((= 1 n) 1)
        (t (+ (functional-fibonacci (- n 1)) (functional-fibonacci (- n 2))))))

对比一下我们可以看出函数式风格代码更加简洁,可读性更强,更易于理解。虽然使用Common Lisp也可以写出命令式风格的代码,不过我们强烈建议你使用函数式风格,这才是Common Lisp的首选范式 – 即用自然而然的函数嵌套调用或递归调用,而不是堆砌一些修改变量值的语句。C语言中也有函数,但与Common Lisp语言中函数的区别就在于其内部实现均为对变量的修改操作,就像上面例子中imperative-fibonacci函数定义的那样。

一、定义新函数
Common Lisp使用defun宏来定义一个新函数,其语法形式如下:
(defun function-name (param*)
    "Optional documentation string."
    expr1
    expr2
    expr3
    … )

其实我在之前的几篇文章以及上面的例子中已经多次用到了defun宏,与C语言的函数原型相比,defun定义的函数缺少了一些类型信息,包括返回值类型信息和参数列表中参数的类型信息。

defun定义的函数在全局作用域可见,即使这个定义是嵌套在另外一个函数定义中的(标准C是不允许函数嵌套定义的):
[1]> (defun foo (x)
        (print x)
        (defun bar (y)
            (print (1+ y))))
FOO
[2]> (foo 1)
1
BAR
[3]> (bar 2)
3

注意:嵌套在其他函数定义中函数定义,如bar,其生命周期起始于外围函数执行后,例如例子中foo函数未执行前,bar是未定义的。

对于C程序员来说,也许Common Lisp函数定义与C函数定义最大的不同在函数参数列表上。C语言的函数只支持两种参数列表形式:定长参数列表和变长参数列表,比如:
int main(int argc, char* argv[]); /* 定长参数列表 */
int printf( const char* format, …);  /* 变长参数列表 */

而Common Lisp函数对参数列表的支持更加灵活,参数的类型更加丰富,功能也更为强大。下面我们逐一来看。

Common Lisp函数参数列表默认都是定长的,参数也是必选的(required),也就是说参数列表中有几个形式参数,你在调用该函数时就需要传入等量的实际参数,不能多,也不能少,例如:
[1]> (defun foo (x y) (print (+ x y)))
FOO
[2]> (foo 1)
*** – EVAL/APPLY: too few arguments given to FOO
[3]> (foo 1 2 3)
*** – EVAL/APPLY: too many arguments given to FOO
[4]> (foo 1 2)
3

除了参数列表的必选参数外,Common Lisp还支持可选参数(Optional Parameter)。参数列表中的可选参数由&optional指定,例如:
(defun foo (a b &optional c d)
    (print a)
    (print b)
    (print c)
    (print d))

其中位于&optional后面的c,d为可选参数;如果未显式指定默认值,则其值为NIL。
[1]> (foo 1 2)
1
2
NIL
NIL

我们可以为可选参数指定默认值,例如:
(defun foo (a b &optional (c 10) (d 11))
    … …)
[2]> (foo 1 2)
1
2
10
11

可以看出对于指定了默认值的可选参数,如果调用时没有为可选参数传入实际参数,则可选参数将绑定默认值。可选参数的默认值不仅仅可以是常量值,还可以是全局变量或该可选参数左侧的某个必选参数,例如:
(defvar *x* 13)
(defun foo (&optional a b c (d *x*))
    … …)

(defun foo (&optional a b c (d a))
    … …)
 
如果有必选参数,那可选参数必须放在必选参数的后面,但我们可以将一个函数的所有参数都定义为可选参数,如:
(defun foo (&optional a b (c 10) (d 11))
    … …)

在带有可选参数的函数体内我们如何知道该函数被调用时外面是否给可选参数传入实际参数了呢?Common Lisp提供了一个指示器,你可以将该指示器放在可选参数默认值的后面,就像这样:
(defun foo (&optional a b (c 10) (d 11 d-supplied-p))
    (print a)
    (print b)
    (print c)
    (print d)
    (print d-supplied-p))

如果函数在执行时可选参数绑定了实际参数而不是默认值,则该指示器的值将为T,否则为NIL。
[1]> (foo 1 2 3 4)
1
2
3
4
T
[2]> (foo 1 2)
1
2
10
11
NIL

Common Lisp语言引入可选参数至少有两个用途,一是为了适应某些场合的需求:一些场合的确不需要显式为所有参数传递实际参数;二是我们通过可选参数可以为某些参数显式地设置默认值。

在Common Lisp中,与C语言变长参数列表对应的是函数列表中的rest参数。rest参数通过在参数前面的&rest关键字修饰。通过rest参数,我们可以定义出类似format这样接受不定个数参数的函数,例如:
[1]> (defun foo (x y &optional z &rest others)
        (print x)
        (print y)
        (print z)
        (mapcar #'print others))
[2]> (foo 1 2 3 4 "hello lisp")
1
2
3
4
"hello lisp"
(4 "hello lisp")

在函数定义内部rest参数是以一个list的形式存在的,例如上面例子中,传入函数体内的参数others的值实际上是(4 "hello lisp")。

我们知道C语言虽然支持变长参数列表,但其参数列表中至少需要有一个必选参数,例如:int printf( const char* format, …)中的format参数是无法省略的;但是Common Lisp就不同,Common Lisp支持完全的变长参数列表,例如:(defun my-add (&rest addends) …)

Common Lisp还提供一种C语言中没有的参数类型 – keyword参数。keyword参数允许你只为参数列表中的某个特定参数传入实际参数,这种能力是rest和optional参数所不具备的。我们可以通过&key来指示keyword参数,如:
[1]> (defun foo (&key x y z)
        (print x)
        (print y)
        (print z))

作为keyword参数,如果在函数调用时没有为其显式赋值,那么该参数的值将为NIL。我们可以通过如下方式为特定的keyword参数赋值:
[2]> (foo :y 2)
NIL
2
NIL

Keyword参数赋值是不用考虑赋值先后的顺序的,例如:
[3]> (foo :z 11 :x 13)
13
NIL
11

对于带有keyword参数的函数,调用该函数时要么不为任何keyword参数传参,要么至少为其中某一个keyword参数传参,不能只为必选参数传参,如:
[1]> (defun foo (&key x y z)
        (print x)
        (print y)
        (print z))
[2]> (foo 1)
*** – FOO: keyword arguments in (1) should occur pairwise

与Optional参数类似,keyword参数也可以指定默认值,也可以通过指示器来标定keyword参数到底是否绑定了外面传入的实际参数,其中默认值既可以是常量也可以是其左侧其他keyword参数组成的表达式,例如:
[1]> (defun foo (&key (x 17) (y 15 y-supplied-p) z)
        (print x)
        (print y)
        (print y-supplied-p)
        (print z))

[2]> (foo :z 23)
17
15
NIL
23

在之前有关keyword参数的例子中我们使用的形式参数多以x,y这样的简单名字命名,这些名字虽便于函数定义内部使用,但是对于这个函数的调用者来说,这些名字却没有什么实际含义。keyword参数支持通过别名方式解决这个问题:
[1]> (defun foo (&key ((:name a)) ((:age b)) ((:gender c) "Unknown"))
        (print a)
        (print b)
        (print c))
[2]> (foo :name "tony" :age 29)
"tony"
29
"Unknown"

Common Lisp中函数的返回值默认为函数体中最后执行的那个表达式的求值结果,上面举的例子也都是这种情况。在C语言中我们通过return语句可以从函数体内的任何位置主动退出该函数,在Common Lisp中我们同样可以用return-from达到这一目的,例如:
[1]> (defun foo (x y)
        (if (< x 0)
            (return-from foo "IIlegal X Value"))
        (if (< y 0)
            (return-from foo))
        (+ x y))
FOO
[2]> (foo 1 2)
3
[3]> (foo -1 2)
"IIlegal X Value"
[4]> (foo 1 -2)
NIL

对于函数而言,return-from的语法形式为:(return-from func-name optional-value),若不指定返回值,那么默认return-from的返回值为NIL。

二、匿名函数
与C语言不同,Common Lisp支持定义匿名函数,例如:
[1]> (funcall #'(lambda (x) (print (1+ x))) 2)
3

上面例子中这行语句既包含了函数定义,也包含了函数调用。与之前使用defun定义有名函数不同的是,这次我们定义出来的函数没有指定函数名,这种使用lambda关键字定义的函数被称作为匿名函数。

从例子中也可以看出,匿名函数的定义也很简单,其一般形式为:
(lambda (args*) body-form*)

我们也称这种表达式为lambda表达式。lambda表达式定义的匿名函数与有名函数一样,也支持使用optional,rest和keyword参数。

三、高阶函数
函数式编程语言与命令式语言除了在风格方面的不同之外,最大的不同点之一在于函数式语言中函数已经成为了一等公民(first-class citizen),与整型、字符串等原生类型具有同等的地位。更具体地说,函数成为一等公民意味着我们可以像对待整型数、字符串那样将函数当作数据对待:将函数赋值给变量、将函数作为参数传递给其他函数以及将函数作为返回值返回给函数调用者等等。作为C程序员你也许会说这似乎与C语言中的函数指针很类似啊,但别忘了C语言真正原生支持的是类型是指针,而不是函数。

有了一等公民地位的函数,我们就得到了高阶函数。高阶函数就是那些接受其他函数为函数或将其他函数作为返回值的函数。例如Common Lisp提供的标准函数sort就接受一个比较函数作为参数:
[1]> (defun integer-over-than (x y) (> x y))
INTEGER-OVER-THAN
[2]> (sort '(5 2 98) (function integer-over-than))
(98 5 2)
[3]> (sort '("hello" "world") (function string>=))
("world" "hello")

标准库中的sort函数接受一个自定义的比较函数作为参数,并在内部将传入的函数应用于参数list。例子中我们没有直接将integer-over-than传给sort,而是使用了(function integer-over-than)。function是Common Lisp提供的一个特殊操作符,将其应用于函数名可以得到该函数名对应的函数对象。比如通过(function foo),我们可以得到名字为foo的内部函数对象。如果没有foo这个函数定义,解释器会提示"undefined function FOO"。可以看出真正被当作一等公民对待的不是foo这个符号,而是foo这个符号名字背后所对应的那个函数对象,也就是函数在Common Lisp中的内部表示形式。我们在将函数绑定到某个变量或将函数传递给某个函数作为实际参数时,我们都需要使用这个内部函数对象,而不是foo这个符号,例如:
[1]> (sort '(5 2 98) integer-over-than)
*** – EVAL: variable MY-OVER-THAN has no value
[2]> (setf *sort-func* integer-over-than)
*** – EVAL: variable MY-OVER-THAN has no value
[3]> (setf *func* (function my-over-than))
# FUNCTION MY-OVER-THAN (X Y) (DECLARE (SYSTEM::IN-DEFUN MY-OVER-THAN)) (BLOCK MY-OVER-THAN (> X Y))>

Common Lisp提供了一个语法糖用于简化function的使用,即我们可以用#'代替function操作符,比如:#'foo就等价于(function foo)。

那么在接受函数作为参数的函数定义内部我们如何使用函数对象呢?Common Lisp提供了两个函数funcall和apply用来执行函数对象对应的函数。我们先以funcall为例,funcall的语法形式如下:
(funcall function-obj args*)

例如:
[1]> (defun foo (x y) (print (+ x y)))
FOO
[2]> (defun my-add (x y f) (funcall f x y))
MY-ADD
[3]> (my-add 1 2 #'foo)
3

apply与funcall的不同之处在于其接受的参数格式有不同,apply的语法形式如下:
(apply function-obj args* other-args-list)

直观地比较:(apply #'+ '(1 2 3))就等价于(funcall #'+ 1 2 3),不同的是使用apply需要将各个参数打包到一个list中,或至少保证最后一个参数为list。下面几种调用方式与(apply #'+ '(1 2 3))都是等价的:
(apply #'+ 1 '(2 3))
(apply #'+ 1 2 '(3))

lambda表达式用于定义一个匿名函数,我们同样可以通过#'来获得这个匿名函数对应的函数对象,例如:
#'(lambda (x y) (print (+ x y)))

匿名函数对象可以直接作为实际参数传递给函数,我们也可以通过funcall来直接执行匿名函数,例如:
[1]> (my-add 1 2 #'(lambda (x y) (print (+ x y))))
3
[2]> (funcall #'(lambda (x y) (print (+ x y))) 1 2)
3

以上是推荐的标准用法,下面方法(去掉了lambda前面的#')虽然也可以达到相同效果,但不推荐使用:
[1]> (my-add 1 2 (lambda (x y) (print (+ x y))))
3
[2]> (funcall (lambda (x y) (print (+ x y))) 1 2)
3
[3]> ((lambda (x y) (print (+ x y))) 1 2)
3

在C语言中我们通过函数指针和回调手法也可以模拟一些高阶函数的行为,这里就不赘述了。

四、闭包(Closure)
市面上有很多编程语言都支持闭包,比如JavaScript,Python,Perl,Ruby等。这里所说的闭包不是离散数学里的那个闭包,而是编程语言引入的一种机制,目前对于编程语言中的闭包尚未有一个精确的定义,但一般认为闭包是引用了外部作用域(但不是全局作用域)的变量的函数,这个被引用的变量与这个函数一同存在,即使是脱离了定义它们的上下文环境。

Common Lisp支持闭包,关于Common Lisp闭包的一个最典型例子是这样的:
[1]> (setf *fn* (let ((i 0))
            #'(lambda () (setf i (+ i 1)))))
#FUNCTION :LAMBDA NIL (SETF I (+ I 1))>
[2]> (funcall *fn*)
1
[3]> (funcall *fn*)
2
[4]> (funcall *fn*)
3

按照之前我们对变量的理解,let引入的i只是一个局部变量,在离开定义的环境后,该变量生命周期将终结。理论上我们三次调用*fn*所对应的你们函数得到的结果应该是相同的才对。但就是由于在let构造的局部作用域内的那个匿名函数引用了外部的变量i,导致变量i可以脱离其原生作用域的束缚,让其生命周期等同于了其内部的那个匿名函数,这个内部的匿名函数就被称为闭包,而那个被引用的外部变量被成为自由变量(free variable)。当我们连续调用函数*fn*时,i就像一个全局变量一样,每次值都加一。

引用了自由变量的闭包似乎是终结了自由变量的局部绑定关系,将自由变量从局部作用域环境中取出,并重新放入一个与闭包同生命周期的新作用域。自由变量会常驻内存中,这也是闭包的常用场景之一。闭包的另外一个用途可能就是出于保护自由变量的考虑,让自由变量只有通过闭包函数才能访问到。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats