原文出处: Peter
Norvig
   译文出处: jnjc(@jnjcc)

本文有两个目的: 一是讲述实现计算机语言解释器的通用方法,另外一点,着重展示如何使用Python来实现Lisp方言Scheme的一个子集。我将我的解释器称之为Lispy (lis.py)。几年前,我介绍过如何使用Java编写一个Scheme解释器,同时我还使用Common
Lisp
语言编写过一个版本。这一次,我的目的是尽可能简单明了地演示一下Alan Kay所说的“软件的麦克斯韦方程组”
(Maxwell’s Equations of Software)[1]

Lispy支持的Scheme子集的语法和语义

大多数计算机语言都有许多语法规约 (例如关键字、中缀操作符、括号、操作符优先级、点标记、分号等等),但是,作为Lisp语言家族中的一员,Scheme所有的语法都是基于包含在括号中的、采用前缀表示的列表的。这种表示看起来似乎有些陌生,但是它具有简单一致的优点。 (一些人戏称”Lisp”是”Lots
of Irritating Silly Parentheses“——“大量恼人、愚蠢的括号“——的缩写;我认为它是”Lisp Is Syntactically Pure“——“Lisp语法纯粹”的缩写。)
考虑下面这个例子:

 
1
2
3
4
5
6
7
/*

Java
*/
if(x.val()
>
0)
{
    z
=

f(a
*x.val()
+

b);
}
/*

Scheme
*/
(if

(> (val x)
0)
    (set!
z (f (
+

(
*

a (val x)) b))))

注意上面的惊叹号在Scheme中并不是一个特殊字符;它只是”set!“这个名字的一部分。(在Scheme中)只有括号是特殊字符。类似于(set! x
y)
这样以特殊关键字开头的列表在Scheme中被称为一个特殊形式 (special form);Scheme的优美之处就在于我们只需要六种特殊形式,以及另外的三种语法构造——变量、常量和过程调用。

形式 (Form) 语法 语义和示例
变量引用 var 一个符号,被解释为一个变量名;其值就是这个变量的值。

示例: x
常量字面值 number 数字的求值结果为其本身

示例: 12 或者-3.45e+6
引用 (quote exp) 返回exp的字面值;不对它进行求值。

示例:(quote (a b c)) ⇒ (a b c)
条件测试 (if test conseq alt) 对test进行求值;如果结果为真,那么对conseq进行求值并返回结果;否则对alt求值并返回结果。

示例:(if (< 10 20) (+ 1 1) (+ 3 3)) ⇒ 2
赋值 (set! varexp) 对exp进行求值并将结果赋给var,var必须已经进行过定义 (使用define进行定义或者作为一个封闭过程的参数)。

示例:(set! x2 (* x x))
定义 (define varexp) 在最内层环境 (environment) 中定义一个新的变量并将对exp表达式求值所得的结果赋给该变量。

示例:(define r 3) 或者 (define
square (lambda (x) (* x x)))
过程 (lambda(var…)exp) 创建一个过程,其参数名字为var…,过程体为相应的表达式。

示例:(lambda (r) (* 3.141592653 (* r r)))
(表达式) 序列 (beginexp…) 按从左到右的顺序对表达式进行求值,并返回最终的结果。

示例:(begin (set! x 1) (set! x (+ x 1)) (* x 2)) ⇒ 4
过程调用 (proc exp…) 如果proc是除了if, set!, define, lambda, begin,或者quote之外的其它符号的话,那么它会被视作一个过程。它的求值规则如下:所有的表达式exp都将被求值,然后这些求值结果作为过程的实际参数来调用该相应的过程。

示例:(square 12) ⇒ 144

在该表中,var必须是一个符号——一个类似于x或者square这样的标识符——number必须是一个整型或者浮点型数字,其余用斜体标识的单词可以是任何表达式。exp…表示exp的0次或者多次重复。

更多关于Scheme的内容,可以参考一些优秀的书籍 (如Friedman和FelleseinDybvig,QueinnecHarvey和Wright或者Sussman和Abelson)、视频
(Abelson和Sussman)、教程 (DoraiPLT或者Neller)、或者参考手册

语言解释器的职责

一个语言解释器包括两部分:

1、解析 (Parsing):解析部分接受一个使用字符序列表示的输入程序,根据语言的语法规则对输入程序进行验证,然后将程序翻译成一种中间表示。在一个简单的解释器中,中间表示是一种树结构,紧密地反映了源程序中语句或表达式的嵌套结构。在一种称为编译器的语言翻译器中,内部表示是一系列可以直接由计算机
(作者的原意是想说运行时系统——译者注) 执行的指令。正如Steve Yegge所说,“如果你不明白编译器的工作方式,那么你不会明白计算机的工作方式。”Yegge介绍了编译器可以解决的8种问题
(或者解释器,又或者采用Yegge的典型的反讽式的解决方案)。 Lispy的解析器由parse函数实现。

2、执行:程序的内部表示 (由解释器) 根据语言的语义规则进行进一步处理,进而执行源程序的实际运算。(Lispy的)执行部分由eval函数实现
(注意,该函数覆盖了Python内建的同名函数)。

下面的图片描述了解释器的解释流程,(图片后的) 交互会话展示了parseeval函数对一个小程序的操作方式:

 
1
2
3
4
5
6
7
>>
program
=

"(begin (define r 3) (* 3.141592653 (* r r)))"
 
>>>
parse(program)
['begin',
[
'define',
'r',
3],
[
'*',
3.141592653,
[
'*',
'r',
'r']]]
 
>>>
eval(parse(program))
28.274333877

这里,我们采用了一种尽可能简单的内部表示,其中Scheme的列表、数字和符号分别使用Python的列表、数字和字符串来表示。

执行: eval

下面是eval函数的定义。对于上面表中列出的九种情况,每一种都有一至三行代码,eval函数的定义只需要这九种情况:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def

eval
(x,
env
=global_env):
    "Evaluate
an expression in an environment."
    if

isa(x, Symbol):            
#
variable reference
        return

env.find(x)[x]
    elif

not

isa(x,
list):        
#
constant literal
        return

x               
    elif

x[
0]
==

'quote'
:         
#
(quote exp)
        (_,
exp)
=

x
        return

exp
    elif

x[
0]
==

'if'
:            
#
(if test conseq alt)
        (_,
test, conseq, alt)
=

x
        return

eval
((conseq
if

eval
(test,
env)
else

alt), env)
    elif

x[
0]
==

'set!'
:          
#
(set! var exp)
        (_,
var, exp)
=

x
        env.find(var)[var]
=

eval
(exp,
env)
    elif

x[
0]
==

'define'
:        
#
(define var exp)
        (_,
var, exp)
=

x
        env[var]
=

eval
(exp,
env)
    elif

x[
0]
==

'lambda'
:        
#
(lambda (var*) exp)
        (_,
vars,
exp)
=

x
        return

lambda

*
args:
eval(exp,
Env(
vars,
args, env))
    elif

x[
0]
==

'begin'
:         
#
(begin exp*)
        for

exp
in

x[
1:]:
            val
=

eval
(exp,
env)
        return

val
    else:                         
#
(proc exp*)
        exps
=

[
eval(exp,
env)
for

exp
in

x]
        proc
=

exps.pop(
0)
        return

proc(
*exps)
 
isa
=

isinstance
 
Symbol
=

str

eval函数的定义就是这么多…当然,除了environments。Environments (环境) 只是从符号到符号所代表的值的映射而已。一个新的符号/值绑定由一个define语句或者一个过程定义
(lambda表达式) 添加。

让我们通过一个例子来观察定义然后调用一个Scheme过程的时候所发生的事情 (lis.py>提示符表示我们正在与Lisp解释器进行交互,而不是Python):

 
1
2
3
lis.py>
(define area (
lambda

(r) (
*

3.141592653

(
*

r r))))
lis.py>
(area
3)
28.274333877

当我们对(lambda (r) (* 3.141592653 (* r r)))进行求值时,我们在eval函数中执行elif
x[0] == 'lambda'
分支,将(_, vars, exp)三个变量分别赋值为列表x的对应元素
(如果x的长度不是3,就抛出一个错误)。然后,我们创建一个新的过程,当该过程被调用的时候,将会对表达式['*',
3.141592653 ['*', 'r', 'r']]
进行求值,该求值过程的环境 (environment) 是通过将过程的形式参数 (该例中只有一个参数,r) 绑定为过程调用时所提供的实际参数,外加当前环境中所有不在参数列表
(例如,变量*) 的变量组成的。新创建的过程被赋值给global_env中的area变量。

那么,当我们对(area 3)求值的时候发生了什么呢?因为area并不是任何表示特殊形式的符号之一,它必定是一个过程调用
(eval函数的最后一个else:分支),因此整个表达式列表都将会被求值,每次求值其中的一个。对area进行求值将会获得我们刚刚创建的过程;对3进行求值所得的结果就是3。然后我们
(根据eval函数的最后一行) 使用参数列表[3]来调用这个新创建的过程。也就是说,对exp(也就是['*',
3.141592653 ['*', 'r', 'r']]
)进行求值,并且求值所在的环境中r的值是3,并且外部环境是全局环境,因此*是乘法过程。

现在,我们可以解释一下Env类的细节了:

 
1
2
3
4
5
6
7
8
class

Env(
dict):
    "An
environment: a dict of {'var':val} pairs, with an outer Env."
    def

__init__(
self,
parms
=(),
args
=(),
outer
=None):
        self.update(zip(parms,args))
        self.outer
=

outer
    def

find(
self,
var):
        "Find
the innermost Env where var appears."
        return

self

if

var
in

self

else

self
.outer.find(var)

注意Envdict的一个子类,也就是说,通常的字典操作也适用于Env类。除此之外,该类还有两个方法,构造函数__init__find函数,后者用来为一个变量查找正确的环境。理解这个类的关键
(以及我们需要一个类,而不是仅仅使用dict的根本原因) 在于外部环境 (outer
environment) 这个概念。考虑下面这个程序:

1
2
3
4
5
6
(define
make
-account
  (lambda

(balance)
    (lambda

(amt)
      (begin
(
set!
balance (
+

balance amt)) balance))))
(define
a1 (make
-account
100.00))
(a1
-20.00)

每个矩形框都代表了一个环境,并且矩形框的颜色与环境中最新定义的变量的颜色相对应。在程序的最后两行我们定义了a1并且调用了(a1
-20.00)
;这表示创建一个开户金额为100美元的银行账户,然后是取款20美元。在对(a1 -20.00)求值的过程中,我们将会对黄色高亮表达式进行求值,该表达式中具有三个变量。amt可以在最内层
(绿色) 环境中直接找到。但是balance在该环境中没有定义:我们需要查看绿色环境的外层环境,也就是蓝色环境。最后,+代表的变量在这两个环境中都没有定义;我们需要进一步查看外层环境,也就是全局
(红色) 环境。先查找内层环境,然后依次查找外部的环境,我们把这一过程称之为词法定界 (lexical scoping)。Procedure.find负责根据词法定界规则查找正确的环境。

剩下的就是要定义全局环境。该环境需要包含+过程以及所有其它Scheme的内置过程。我们并不打算实现所有的内置过程,但是,通过导入Python的math模块,我们可以获得一部分这些过程,然后我们可以显式地添加20种常用的过程:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def

add_globals(env):
    "Add
some Scheme standard procedures to an environment."
    import

math, operator as op
    env.update(vars(math))
#
sin, sqrt, ...
    env.update(
     {'+':op.add,
'-':op.sub,
'*':op.mul,
'/':op.div,
'not':op.not_,
      '>':op.gt,
'<':op.lt,
'>=':op.ge,
'<=':op.le,
'=':op.eq,
      'equal?':op.eq,
'eq?':op.is_,
'length':len,
'cons':lambda

x,y:[x]
+y,
      'car':lambda

x:x[
0],'cdr':lambda

x:x[
1:],
'append':op.add, 
      'list':lambda

*
x:list(x),
'list?':
lambda

x:isa(x,
list),
      'null?':lambda

x:x
==[],
'symbol?':lambda

x: isa(x, Symbol)})
    return

env
 
global_env
=

add_globals(Env())

PS1: 对麦克斯韦方程组的一种评价是“一般地,宇宙间任何的电磁现象,皆可由此方程组解释”。Alan Kay所要表达的,大致就是Lisp语言使用自身定义自身 (Lisp was “defined in terms of Lisp”) 这种自底向上的设计对软件设计而言具有普遍的参考价值。——译者注

解析 (Parsing): readparse

接下来是parse函数。解析通常分成两个部分:词法分析和语法分析。前者将输入字符串分解成一系列的词法单元 (token);后者将词法单元组织成一种中间表示。Lispy支持的词法单元包括括号、符号
(如set!或者x)
以及数字 (如2)。它的工作形式如下:

 
1
2
3
4
5
6
7
>>>
program
=

"(set! x*2 (* x 2))"
 
>>>
tokenize(program)
['(',
'set!',
'x*2',
'(',
'*',
'x',
'2',
')',
')']
 
>>>
parse(program)
['set!',
'x*2',
[
'*',
'x',
2]]

有许多工具可以进行词法分析 (例如Mike Lesk和Eric Schmidt的lex)。但是我们将会使用一个非常简单的工具:Python的str.split。我们只是在
(源程序中) 括号的两边添加空格,然后调用str.split来获得一个词法单元的列表。

接下来是语法分析。我们已经看到,Lisp的语法很简单。但是,一些Lisp解释器允许接受表示列表的任何字符串作为一个程序,从而使得语法分析的工作更加简单。换句话说,字符串(set!
1 2)
可以被接受为是一个语法上有效的程序,只有当执行的时候解释器才会抱怨set!的第一个参数应该是一个符号,而不是数字。在Java或者Python中,与之等价的语句1
= 2
将会在编译时被认定是错误。另一方面,Java和Python并不需要在编译时检测出表达式x/0是一个错误,因此,如你所见,一个错误应该何时被识别并没有严格的规定。Lispy使用read函数来实现parse函数,前者用以读取任何的表达式
(数字、符号或者嵌套列表)。

tokenize函数获取一系列词法单元,read通过在这些词法单元上调用read_from函数来进行工作。给定一个词法单元的列表,我们首先查看第一个词法单元;如果它是一个’)’,那么这是一个语法错误。如果它是一个’(‘,那么我们开始构建一个表达式列表,直到我们读取一个匹配的’)’。所有其它的
(词法单元) 必须是符号或者数字,它们自身构成了一个完整的列表。剩下的需要注意的就是要了解’2‘代表一个整数,2.0代表一个浮点数,而x代表一个符号。我们将区分这些情况的工作交给Python去完成:对于每一个不是括号也不是引用
(quote) 的词法单元,我们首先尝试将它解释为一个int,然后尝试float,最后尝试将它解释为一个符号。根据这些规则,我们得到了如下程序:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def

read(s):
    "Read
a Scheme expression from a string."
    return

read_from(tokenize(s))
 
parse
=

read
 
def

tokenize(s):
    "Convert
a string into a list of tokens."
    return

s.replace(
'(','
( '
).replace(')','
) '
).split()
 
def

read_from(tokens):
    "Read
an expression from a sequence of tokens."
    if

len
(tokens)
==

0
:
        raise

SyntaxError(
'unexpected
EOF while reading'
)
    token
=

tokens.pop(
0)
    if

'('

=
=

token:
        L
=

[]
        while

tokens[
0]
!
=

')'
:
            L.append(read_from(tokens))
        tokens.pop(0)
#
pop off ')'
        return

L
    elif

')'

=
=

token:
        raise

SyntaxError(
'unexpected
)'
)
    else:
        return

atom(token)
 
def

atom(token):
    "Numbers
become numbers; every other token is a symbol."
    try:
return

int
(token)
    except

ValueError:
        try:
return

float
(token)
        except

ValueError:
            return

Symbol(token)

最后,我们将要添加一个函数to_string,用来将一个表达式重新转换成Lisp可读的字符串;以及一个函数repl,该函数表示read-eval-print-loop
(读取-求值-打印循环),用以构成一个交互式的Lisp解释器:

 
1
2
3
4
5
6
7
8
9
def

to_string(exp):
    "Convert
a Python object back into a Lisp-readable string."
    return

'('
+'
'
.join(map(to_string,
exp))
+')'

if

isa(exp,
list)
else

str
(exp)
 
def

repl(prompt
='lis.py>
'
):
    "A
prompt-read-eval-print loop."
    while

True
:
        val
=

eval
(parse(raw_input(prompt)))
        if

val
is

not

None
:
print

to_string(val)

下面是函数工作的一个例子:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>>
repl()
lis.py>
(define area (
lambda

(r) (
*

3.141592653

(
*

r r))))
lis.py>
(area
3)
28.274333877
lis.py>
(define fact (
lambda

(n) (
if

(<
=

n
1)
1

(
*

n (fact (
-

n
1))))))
lis.py>
(fact
10)
3628800
lis.py>
(fact
100)
9332621544394415268169923885626670049071596826438162146859296389521759999322991
5608941463976156518286253697920827223758251185210916864000000000000000000000000
lis.py>
(area (fact
10))
4.1369087198e+13
lis.py>
(define first car)
lis.py>
(define rest cdr)
lis.py>
(define count (
lambda

(item L) (
if

L (
+

(equal? item (first L)) (count item (rest L)))
0)))
lis.py>
(count
0

(
list

0

1

2

3

0

0
))
3
lis.py>
(count (quote the) (quote (the more the merrier the bigger the better)))
4

Lispy有多小、多快、多完备、多优秀?

我们使用如下几个标准来评价Lispy:

*小巧:Lispy非常小巧:不包括注释和空白行,其源代码只有90行,并且体积小于4K。(比第一个版本的体积要小,第一个版本有96行——根据Eric
Cooper的建议,我删除了Procedure的类定义,转而使用Python的lambda。)
我用Java编写的Scheme解释器Jscheme最小的版本,其源代码也有1664行、57K。Jscheme最初被称为SILK (Scheme in Fifty Kilobytes——50KB的Scheme解释器),但是只有计算字节码而不是源代码的时候,我才能保证
(其体积) 小于该最小值。Lispy做的要好得多;我认为它满足了Alan Kay在1972年的断言:他声称我们可以使用“一页代码”来定义“世界上最强大的语言”。

 
1
2
bash$
grep
"^\s*[^#\s]"

lis.py | wc
      90    

398
   
3423

*高效:Lispy计算(fact
100)
只需要0.004秒。对我来说,这已经足够快了 (虽然相比起其它的计算方式来说要慢很多)。

*完备:相比起Scheme标准来说,Lispy不是非常完备。主要的缺陷有:

(1) 语法:缺少注释、引用 (quote) / 反引用 (quasiquote) 标记 (即'`——译者注)、#字面值
(例如#\a——译者注)、衍生表达式类型 (例如从if衍生而来的cond,或者从lambda衍生而来的let),以及点列表
(dotted list)。

(2) 语义:缺少call/cc以及尾递归。

(3) 数据类型:缺少字符串、字符、布尔值、端口 (ports)、向量、精确/非精确数字。事实上,相比起Scheme的pairs和列表,Python的列表更加类似于Scheme的向量。

(4) 过程:缺少100多个基本过程:与缺失数据类型相关的所有过程,以及一些其它的过程 (如set-car!set-cdr!,因为使用Python的列表,我们无法完整实现set-cdr!)。

(5) 错误恢复:Lispy没有尝试检测错误、合理地报告错误以及从错误中恢复。Lispy希望程序员是完美的。

*优秀:这一点需要读者自己确定。我觉得,相对于我解释Lisp解释器这一目标而言,它已经足够优秀。

真实的故事

了解解释器的工作方式会很有帮助,有一个故事可以支持这一观点。1984年的时候,我在撰写我的博士论文。当时还没有LaTeX和Microsoft Word——我们使用的是troff。遗憾的是,troff中没有针对符号标签的前向引用机制:我想要能够撰写“正如我们将要在@theoremx页面看到的”,随后在合适的位置撰写”@(set theoremx \n%)” (troff寄存器\n%保存了页号)。我的同伴,研究生Tony
DeRose也有着同样的需求,我们一起实现了一个简单的Lisp程序,使用这个程序作为一个预处理器来解决我们的问题。然而,事实证明,当时我们用的Lisp善于读取Lisp表达式,但在采用一次一个字符的方式读取非Lisp表达式时效率过低,以至于我们的这个程序很难使用。

在这个问题上,Tony和我持有不同的观点。他认为 (实现) 表达式的解释器是困难的部分;他需要Lisp为他解决这一问题,但是他知道如何编写一个短小的C过程来处理非Lisp字符,并知道如何将其链接进Lisp程序。我不知道如何进行这种链接,但是我认为为这种简单的语言编写一个解释器 (其所具有的只是设置变量、获取变量值和字符串连接) 很容易,于是我使用C语言编写了一个解释器。因此,戏剧性的是,Tony编写了一个Lisp程序,因为他是一个C程序员;我编写了一个C程序,因为我是一个Lisp程序员。

最终,我们都完成了我们的论文。

整个解释器

重新总结一下,下面就是Lispy的所有代码 (也可以从lis.py下载):

 
1
2
3
4
5
#
-*- coding: UTF-8 -*-
#
源代码略。以下纯属娱乐。。。
#
你想看完整源代码?真的想看?
#
真的想看你就说嘛,又不是我编写的代码,你想看我总不能不让你看,对吧?
#
真的想看的话就参考上述下载地址喽。。。LOL

如何使用Python编写一个Lisp解释器的更多相关文章

  1. 用Python编写一个简单的Http Server

    用Python编写一个简单的Http Server Python内置了支持HTTP协议的模块,我们可以用来开发单机版功能较少的Web服务器.Python支持该功能的实现模块是BaseFTTPServe ...

  2. 使用 python 编写一个授权登录验证的模块

    使用 python 编写一个授权登录验证的模块 我们编写的思路: 1.登录的逻辑:如果用户名和密码正确,就返回 token . 2.生成 token 的逻辑,根据用户名,随机数,当前时间 + 2 小时 ...

  3. 如何用Python编写一个聊天室

    一.课程介绍 1.简介 本次项目课是实现简单聊天室程序的服务器端和客户端. 2.知识点 服务器端涉及到asyncore.asynchat和socket这几个模块,客户端用到了telnetlib.wx. ...

  4. 为Python编写一个简单的C语言扩展模块

    最近在看pytorh方面的东西,不得不承认现在这个东西比较火,有些小好奇,下载了代码发现其中计算部分基本都是C++写的,这真是要我对这个所谓Python语音编写的框架或者说是库感觉到一丢丢的小失落,细 ...

  5. Python 编写一个有道翻译的 workflow 教程

    最近使用有道翻译的 workflow 总是翻译不了,可能是 appKey 失效了或者超过调用上限,所以打算自己实现一个. 创建 workflow 打开 Alfred3 的 Preferences,选择 ...

  6. 用python编写一个合格的ftp程序,思路是怎样的?

      经验1.一般在比较正规的类中的构造函数.都会有一个verify_args函数,用于验证传入参数.尤其是对于系统传参.2.并且系统传参,其实后面大概都是一个函数名 例如:python server. ...

  7. Python编写一个Python脚本

    我想要一个可以为我的所有重要文件创建备份的程序.(下面测试环境为python2.7) 1.backup_ver1.py #!/usr/bin/python import os import time ...

  8. day-1 用python编写一个简易的FTP服务器

    从某宝上购买了一份<Python神经网络深度学习>课程,按照视频教程,用python语言,写了一个简易的FTP服务端和客户端程序,以前也用C++写过聊天程序,编程思路差不多,但是pytho ...

  9. 用python编写一个计算器

    # 1 - 2 * ((60-30 +(-40.0/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2)))# 通过Pyt ...

  10. 用python编写一个搜索引擎

    完整代码如下: #!/usr/bin/env python #-*- coding: utf-8 -*- import sys import os import datetime from PyQt5 ...

随机推荐

  1. bacnet mstp设备数据 转IEC61850项目案例

    目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 使用YABE软件读取BACNET MSTP设备信息 2 4 配置网关采集BACNET MSTP数据 4 5 用IEC61850协议转发数据 ...

  2. 6.2 XXE和XML利用

    pikaqu靶场xml数据传输测试-有回显,玩法,协议,引入 1.构造payload 写文件 <?xml version="1.0" encoding="UTF-8 ...

  3. QT原理与源码分析之对象级QT事件过滤器

    本文简略介绍了QT原理与源码分析中的QT对象级事件过滤器的自定义过滤函数.对象级事件过滤器的安装过程,以及安装函数的源码实现. 自定义QT事件过滤器的过滤函数的实现: 事件过滤器被调用时执行的函数代码 ...

  4. Nacos 配置加密

    Nacos 配置加密 nacos配置加密官网 官网介绍太简单,而且GitHub 网络受限,随缘访问.Gitee 发现有镜像仓库,同步的最新版本 Gitee nacos 镜像仓库 但是官网中提到的加密插 ...

  5. MySQL笔记--数据库定时备份与恢复

    利用crontab定时.利用mysqldump备份 编写sh启动脚本时记得赋予执行权限(x) 如果没有mysqldump命令执行,基于centos7 yum -y install mysql-clie ...

  6. 【赵渝强老师】Flink的Watermark机制(基于Flink 1.11.0实现)

    在使用eventTime的时候如何处理乱序数据?我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的.虽然大部分情况下,流到operator的数据都是按照事件 ...

  7. 【赵渝强老师】MongoDB的inMemory存储引擎

    一.MongoDB的存储引擎概述 存储引擎(Storage Engine)是MongoDB的核心组件,负责管理数据如何存储在硬盘(Disk)和内存(Memory)上.从MongoDB 3.2 版本开始 ...

  8. std::vector 和 std::map 都支持以下比较运算符

    在 C++ 标准库中,std::vector 和 std::map 都支持以下比较运算符: ==(相等运算符) !=(不等运算符) <(小于运算符) <=(小于等于运算符) >(大于 ...

  9. 前端面试题axaios携带 cookies

    配置 axios.default.widthCredentials = true;

  10. kotlin更多语言结构——>类型安全的构建器

    通过使用命名得当的函数作为构建器,结合带有接收者的函数字面值,可以在 Kotlin 中创建类型安全.静态类型 的构建器 类型安全的构建器可以创建基于 Kotlin 的适用于采用半声明方式构建复杂层次数 ...