自己动手写编译器:算术表达式的语法分析实例和代码实现

在编译原理中,语法解析可能是最抽象和难以理解的部分,初学者很容易在这里卡壳。学习抽象知识的最好方法就是在初期先看大量具体实例,获得足够深厚的感性认识后,我们再对感性认知进行推理和抽象从而获得更高级的理性认知,这在哲学上称为经验主义。在学校学习知识最大的问题在于一开始就给初学者灌输抽象概念,这是一种违背人认知规律的方式,因此很容易让学生坠入云里雾里的迷糊状态,最终感觉问题太“难”从而放弃对它的学习。

我作为过来人直到在编译原理中语法解析部分的抽象性。如果你直接去读相关教程中例如大学课本或龙书中对语法解析的描述,你大概率会被其“秒杀”,因为他们描述的东西太抽象太不知所云了,因此根据我过来经验,在语法解析部分,我们最好通过具体的实例来理解。

这次我们做一个计算器,用户输入一系列算术表达式,表达式间使用分号隔开,然后我们的程序给出每个表达式的计算结果,例如输入如下:

1+2;
3+4*5;
(1+2)*(5-3)/2;

程序读入上面内容后输出如下结果:

3;
23;
3;

要完成既定功能,首要任务就是读入表达式后我们需要识别表达式想要执行的操作。前面我们在词法解析中看到,字符串在读入后词法解析会给每个字符串分配一个标签,于是表达式转变成了标签的排列,语法解析的任务就是判断标签排列是否符合规定,然后根据标签排列的情况执行特定动作。

对于表达式 1+2,经过词法解析后它变成 NUM PLUS NUM,现在问题在于这个标签序列是否合理呢,要解决这个问题我们就需要判断其是否符合算术表达式的语法。因此我们需要先设定表达式语法。语法的设定可以根据自顶向下和自底向上两种方式,我们先看第一种。自顶向下是指,我们先用一个抽象的概念描述一个整体,然后将这个概念分解成次一级的抽象概念的组合,然后次一级抽象概念继续向下分解,通过不断的分解,概念变得越来越具体,直到最后概念不再具有抽象性,它变成了具体的对象。

基于以上原则,我们首先用一个概念 stmt 来描述一系列算术表达式的集合。显然 stmt 可以分解成一个或多个用分号隔开的算术表达式 expr,于是 stmt 分解如下:
stmt -> expr SEMI | expr SEMI stmt
我们注意到右边 expr 是比 stmt 次一级的抽象概念,同时在右边还有一个具体对象那就是 SEMI 也就是分号的标签,同时右边还包括了 stmt 自己,有就是说一个抽象概念有可能分解成一个次一级抽象概念和自身的组合,这是语法解析的一个特点。

我们注意到 stmt 可以分解成 expr SEMI, 于是 stmt 本身也就能分解成多个 expr SEMI 的组合,只要我们把右边 expr SEMI stmt 中的 stmt 持续分解成 expr SEMI stmt 即可。例如我们要解析三个算术表达式:
1+2; 3+4; 5+6;
显然上面字符串的组合规律满足 stmt -> expr SEMI stmt, 于是第一个 expr SEMI 对应 1+2;,然后在将右边的 stmt 分解成 expr SEMI,于是有 stmt-> expr SEMI expr SEMI stmt,这样我们就对应了 1+2; 3+4; 最后我们将右边的 stmt 用 stmt->expr SEMI 分解,于是就有 stmt -> expr SEMI expr SEMI expr SEMI,由此三个表达式就能对应到语法规则 stmt-> expr SEMI | expr SEMI stmt

下面我们需要解析 expr 这个概念。我们可以感觉到expr 可以通过运算符+, -, * , / 分成作用两部分,这两部分都可以是 expr,于是 expr 可以解析成:
expr -> expr PLUS expr | expr MINUS expr | expr MUL expr | expr DIV expr | LEFT_PARA expr RIGHT_PARA,
但是上面的分解就会陷入死循环,因为 expr 只能分解成自身的组合,这是一种循环定义。因此在右边分解时,必须有一个不同与左边的概念,显然单独一个数字也能对应到算术表达式,因此我们还有 expr -> NUM,注意 NUM 已经是一个不能继续分解的符号,于是 expr 的分解规则为:

expr -> expr PLUS expr | expr MINUS expr | expr MUL expr | expr DIV expr | LEFT_PARA expr RIGHT_PARA
expr -> NUM

但上面的语法存在一个问题叫歧义性,也就是对同一个算术表达式1+2*3,上面语法能给出两种解析方式,第一种是先运用 expr -> expr PLUS expr,这样它能解析掉 1+2,然后再运用 expr -> expr MUL expr, 于是所得结果相当与(1+2)*3,如果我们用树形结构来表达我们的解析过程,那就是:
请添加图片描述
第二种是先运用 expr -> expr MUL expr, 这样就能先对应 2 * 3,然后再运用 expr -> expr PLUS expr, 于是就对应 1 + (2 * 3),这种解析方式对应的树形结构为:
请添加图片描述
上面的树形结构也叫语法解析树,可以看到如果某个操作越早被执行,那么它对应的节点就会越接近树的底部,对于第一颗树加法先于乘法被执行,因此加法对应节点就低于乘法节点,同理在第二个解析树中,由于乘法先于加法被执行,因此它对应的节点就低于加号节点。

语法的歧义性主要是因为解析式的右边出现了至少两个以上相同的非终结符。例如 expr -> expr PLUS expr,右边就出现了两次非终结符 expr,因此我们需要改掉这个特点。修改方法是使用一个次一级的符号来替换掉右边的一个 expr,例如:

expr -> expr PLUS term
expr -> expr MINUS term
expr -> expr MUL term
expr -> expr DIV term 
expr -> LEFT_PARA expr RIGHT
expr -> term
term -> NUM

于是表达式 1 + 2 * 3 就只会有 1 中解析方式,首先我们只能用 expr -> expr MUL term 来对应,于是右边 expr 就对应1+2, 于是右边 expr 再使用 expr -> expr PLUS term 来对应,因此语法解析树就是:

请添加图片描述
上面语法虽然解决了歧义性,但是又产生两个问题,第一个是它无法解析 1+(2*3)这样的表达式,处理起来也简单,只要去掉 expr -> LEFT_PARA expr RIGHT_PARA,然后增加 term->LEFT_PARA expr RIGHT_PARA 即可。

第二个问题是,语法没有处理运算符的优先级,在计算中乘法和除法优先于加法和减法,但在我们前面语法中是哪个运算符更加靠近左边,它的优先级就会更高。处理这个问题的方法是再加一个抽象符号factor,然后把乘法和除法往下挪动,于是语法变成:

expr -> expr PLUS term
expr -> expr MINUS term
expr -> term
term -> term MUL factor
term -> term DIV factor
term -> factor
factor -> NUM
factor -> LEFT_PARA expr RIGHT 

于是根据上面语法,表达式 1 + 2 * 3 对应的解析树就是:

请添加图片描述
通过上面语法的修改后可以看到,在一个算术表达式中,乘法和除法优先于加法和减法,不管乘法除法操作符位于表达式哪个位置,同时括号的优先级最高,因为括号对应的表达式在所有表达式的位置最低,所以设计语法时,你要想对应操作的优先级越高,那么把出现该操作符对应的表达式方得就要越低。

在上面解析中需要注意的是,我们会根据算术表达式中的符号来确定语法表达式,如果算术表达式中包含+,-两个符号,那么我们可以用 expr->expr + term 或者 expr -> expr - term 来推导。我们把+或-前面部分再递归的使用 expr 对应表达式来推导,后面部分使用 term 来推导。问题是如果算术表达式中包含多个+或者-的时候,我们依靠哪个+或者-来将表达式分割成两部分呢。例如 1 + 2 - 3 * 4,我们是将1 分配给 expr,然后将2-3*4分配给 term,还是将 1+2-3 分配给 expr,3*4 分配给term 呢,答案是后者,因为2-3*4 分配给 term 时,它无法解析,因为 term 右边的分解中不包含+或-符号,所以推导的原则就是从左到右扫码算术表达式,找到最后一个+或者-号,再将其分成两部分,同理在进入 term 的推导时也是如此,从左到右扫描表达式,找到最后一个*或者/号时再将表达式分割成两部分。

另外在推导时我们看到无论是 expr 还是 term,它右边解析时都不包含左括号和右括号,所以 (1+2)这样的表达式我们应该怎么推导呢,由于 expr 不包含括号,因此它在从左向右扫码表达式时,我们不处理位于括号中的标签,同理在推导 term 时,由于它右边包含的表达式也不包含括号,因此它也需要忽略所有括号里面的标签。所以我们在推导 (1+2)时,首先使用 expr 表达式,我们发现它不满足 expr->expr + term, expr -> expr - term, 因此我们使用 expr -> term。然后在已经 term 推导时,发现(1+2)不满足 term->term * factor, term -> term / factor,于是推导进入 term->factor,由于 factor 右边支持括号,因此推导进入 factor -> ( expr ),这里我们去掉左右括号后,剩下的部分也就是 1+2就能放入到 expr 进行解析。

下面我们使用代码实现上面的语法解析流程。前面我们做 dragon_compiler 的时候体验过语法解析,我们本节在当时完成的 lexer 基础上去实现我们现在的算术表达式语法解析。拿来当时的 dragon compiler 代码,去掉里面的 parser 部分,增加 expression_parser 文件夹,cd 到expression-parer 目录下,使用 go mod init expression_parser做初始化,然后添加一个名为 expression_parser.go 的文件,其内容如下:

package expression_parserimport ("fmt""io""lexer""math""strconv"
)type Symbol struct {token lexer.Tokenvalue int
}type ExpressionParser struct {parserLexer lexer.Lexer//用于存储一个算术表达式的所有标签symbols []Symbol
}func NewExpressionParser(parserLexer lexer.Lexer) *ExpressionParser {return &ExpressionParser{parserLexer: parserLexer,symbols:     make([]Symbol, 0),}
}func (e *ExpressionParser) makeSymbol(token lexer.Token) {val, err := strconv.Atoi(e.parserLexer.Lexeme)if err != nil {val = math.MaxInt}symbol := Symbol{token: token,value: val,}e.symbols = append(e.symbols, symbol)
}func (e *ExpressionParser) getExprTokens() error {sawSemi := falsefor true {//读取算术表达式对应的标签,结束标志是遇到分号 stoken, err := e.parserLexer.Scan()if err != nil && token.Tag != lexer.EOF {errStr := fmt.Sprintf("error: %v\n", err)panic(errStr)}if err == io.EOF {return err}e.makeSymbol(token)if token.Tag == lexer.SEMI {sawSemi = truebreak}}if sawSemi != true {//算术表达式没有 1️⃣ 分号结尾errStr := fmt.Sprintf("err: expression missing semi")panic(errStr)}return nil
}func (e *ExpressionParser) Parse() {//第一个表达式左边是 stmt 所以从调用函数 stmt 开始e.stmt()
}func (e *ExpressionParser) ioEnd() bool {e.symbols = make([]Symbol, 0)token, err := e.parserLexer.Scan()if err != nil && err != io.EOF {strErr := fmt.Sprintf("err: %v\n", err)panic(strErr)}if err == io.EOF {return true}e.makeSymbol(token)return false
}func (e *ExpressionParser) stmt() {//stmt -> expr SEMI | expr SEMI stmte.getExprTokens()val := e.expr(e.symbols[:len(e.symbols)-1])if e.symbols[len(e.symbols)-1].token.Tag != lexer.SEMI {panic("parsing error, expression not end with semi")}fmt.Printf("%d;", val)if e.ioEnd() {//所有标签读取完毕,这里采用 stmt -> expr SEMIreturn}//这里采用 stmt -> expr SEMI stmte.stmt()
}func (e *ExpressionParser) expr(symbols []Symbol) int {if len(symbols) == 0 || symbols == nil {panic("error token begin for expr parsing")}/*读取 PLUS 或 MINUS 标签,读取到,那么标签前面部分继续用 expr 分析后面部分用 term 分析*/sawOperator := falseoperatorPos := 0inPara := falsefor i := 0; i < len(symbols); i++ {/*在将 expr 通过+,-分割成两部分时,如果遇到左括号,那么在括号内部的+,-不作为分割的依据*/if symbols[i].token.Tag == lexer.LEFT_BRACKET {inPara = true}if symbols[i].token.Tag == lexer.RIGHT_BRACKET {if !inPara {panic("expr parsing err, missing left ")}inPara = false}if inPara {continue}if symbols[i].token.Tag == lexer.PLUS || symbols[i].token.Tag == lexer.MINUS {//必须找到表达式中最后一个加号或减号sawOperator = trueoperatorPos = i}}if sawOperator {//expr -> expr PLUS term | expr MINUS termleft := e.expr(symbols[0:operatorPos])right := e.term(symbols[operatorPos+1:])res := 0if symbols[operatorPos].token.Tag == lexer.PLUS {res = left + right} else {res = left - right}return res} else {//expr -> termreturn e.term(symbols)}panic("expr parsing error: should not go here")
}func (e *ExpressionParser) term(symbols []Symbol) int {if len(symbols) == 0 || symbols == nil {panic("error token begin for term parsing")}/*遍历标签,如果找到 MUL 或者 DIV,那么使用term -> term MUL factor | term DIV factor如果找不到使用term -> factor*/sawOperator := falseoperatorPos := 0inPara := falsefor i := 0; i < len(symbols); i++ {/*在将 expr 通过+,-分割成两部分时,如果遇到左括号,那么在括号内部的+,-不作为分割的依据*/if symbols[i].token.Tag == lexer.LEFT_BRACKET {inPara = true}if symbols[i].token.Tag == lexer.RIGHT_BRACKET {if !inPara {panic("expr parsing err, missing left ")}inPara = false}if inPara {continue}if symbols[i].token.Tag == lexer.MUL || symbols[i].token.Tag == lexer.DIV {//必须是表达式中最后一个乘号或除号sawOperator = trueoperatorPos = i}}if sawOperator {//term -> term MUL factor | term DIV factorleft := e.term(symbols[0:operatorPos])right := e.factor(symbols[operatorPos+1:])if symbols[operatorPos].token.Tag == lexer.MUL {return left * right} else {return left / right}} else {return e.factor(symbols)}panic("term parsing err, should not go here")
}func (e *ExpressionParser) factor(symbols []Symbol) int {if len(symbols) == 0 || symbols == nil {panic("error token begin for factor parsing")}sawLeftPara := falseif symbols[0].token.Tag == lexer.LEFT_BRACKET {sawLeftPara = truesymbols = symbols[1:]}sawRightPara := falseif symbols[len(symbols)-1].token.Tag == lexer.RIGHT_BRACKET {sawRightPara = truesymbols = symbols[:len(symbols)-1]}if sawLeftPara && !sawRightPara {panic("parsing factor err: missing right para")}if !sawLeftPara && sawRightPara {panic("parsing factor err: missing left para")}if sawLeftPara && sawRightPara {return e.expr(symbols)}//factor -> NUMif len(symbols) == 0 || len(symbols) > 1 {panic("factor->num but we have zero or more than 1 tokens")}if symbols[0].value == math.MaxInt {panic("parsing factor->num error: not a number")}return symbols[0].value
}

上面代码需要做几方面的说明,第一是stmt 函数启动语法解析流程,它调用getExprTokens函数一次性读入一个算术表达式的所有标签,算术表达式的结尾用分号结束,因此它一直读取到遇到分号对应的 token 为止,然后它创建 Symbol 对象,改对象包含两个字段,分别是当前字符串对应的标签,如果它对应数字,那么 value 字段对应该字符串转换为数字后的数值。

根据表达式 stmt-> expr SEMI | expr SEMI stmt,函数 stmt 调用 expr 进行下一步解析。这里需要注意的有两点,第一是 expr 推导中没有括号,所以它在遍历表达式的标签时,一旦进入了括号,它就必须忽略括号内所有标签。第二点需要注意的是,根据 expr->expr PLUS term | expr MINUS term,我们需要找到标签 PLUS或者 MINUS,将标签分成两部分,前半部分继续交给 expr 解析,后半部分交给 term 解析,问题在于我们必须找到最后一个 PLUS 或者 MINUS,因为 term 解析式不能包含加号或减号,例如 1+2-34,如果我们在第一个加号处将其分解成两部分:1 和 2-34,后者传入 term 时解析就会出错,因此我们需要找到最后一个减号才能分割,也就是需要将其分解成1+2和 3*4才行,这也是 expr 进去后第一个 for 循环的工作。

如果传给 expr 的标签中找不到加号或者减号,那么他会调用 term 对传入的符号进行解析。term 的实现跟 expr 一样,首先它不支持括号,因此一旦进入到括号后,它需要忽略所有在括号内的标签。同理由于其表达式为 term->term MUL factor| term DIV factor| factor,因此它需要找到乘号和除号来将表达式分成两部分,跟 expr 一样,他必须找最后一个乘号或除号才能将标签分解成两部分,将前部分继续调用 term 分析,后部分交给 factor。

根据 factor->LEFT_PARA expr RIGHT PARA | NUM,factor 被调用时首先判断起始标签是不是左括号,如果是那么需要判断最后标签是不是有右括号,如果不是那么意味着括号缺失,如果有,那么将括号内部的所有标签交给 expr 进行解析,要是没有括号标签,那么它只能将当前标签进行数字解析,因此它需要判断当前标签只有一个,并且能解析成数字。

完成上面代码后,在 main.go 中添加代码如下:

package mainimport ("expression_parser""fmt""lexer"
)func main() {source := "1+(2*3)-4;(1+2)*(6/2);1+2*3-4/2;"my_lexer := lexer.NewLexer(source)parser := expression_parser.NewExpressionParser(my_lexer)parser.Parse()fmt.Println("\nparsing end here")
}

上面代码运行后所得结果如下:

my@MACdeAir dragon-compiler % go run main.go
3;9;5;
parsing end here

根据输出结果可以确认我们的代码实现是正确的。根据前面我们实现的 dragon-compiler,大家可以知道我们当前语法存在一些问题,首先就是左递归,那时我们说过左递归的语法难以用代码实现,这导致我们必须一次性读完一个算术表达式的所有标签后才能解析。第二是语法清晰度不够,这导致我们在 expr 和 term 中必须要找到最后一个加号减号或者乘号除号才能将标签分解成两部分进行解析,后续我们会给出这些问题的处理方法。

本节代码下载路径为:
https://github.com/wycl16514/compiler-implementation-of-expression-parser.git

更详细的调试演示请在 b 站搜索:coding 迪斯尼。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2660416.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

VMware之FTP的简介以及搭建使用计算机端口的介绍

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《产品经理如何画泳道图&流程图》 ⛺️ 越努力 &#xff0c;越幸运 目录 一、FTP介绍 1、什么是FTP&#xff1a; 2、FTP适用于以下情况和应用场景&#xff1a; 3、winServer2012搭…

最长连续子序列 - 华为OD统一考试

OD统一考试(B卷) 分值: 100分 题解: Java / Python / C++ 题目描述 有N个正整数组成的一个序列。给定整数sum,求长度最长的连续子序列,使他们的和等于sum,返回此子序列的长度, 如果没有满足要求的序列,返回-1。 输入描述 第一行输入是:N个正整数组成的一个序列。…

VBA启动问题:vbe6ext.olb不能被加载

1、拷贝文件&#xff1a; 从&#xff1a; C:\Program Files\Microsoft Office\root\vfs\ProgramFilesCommonX86\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB 到&#xff1a; C:\Program Files\Microsoft Office\root\vfs\ProgramFilesCommonX86\Microsoft Shared\VBA\VBA7.1\VBE…

虹科方案丨L2进阶L3,数据采集如何助力自动驾驶

来源&#xff1a;康谋自动驾驶 虹科方案丨L2进阶L3&#xff0c;数据采集如何助力自动驾驶 原文链接&#xff1a;https://mp.weixin.qq.com/s/qhWy11x_-b5VmBt86r4OdQ 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 12月14日&#xff0c;宝马集团宣布&#xff0c;搭载…

【2024最新版】我用python代码带你看最绚烂的烟花,浪漫永不过时!

2024年就快要到了&#xff0c;提前用python代码给自己做一个烟花秀庆祝一下。本次介绍的python实例是实现一个简易的烟花秀。 一、步骤分析 总的来说&#xff0c;要实现烟花秀的效果&#xff0c;需要以下几个步骤&#xff1a; 1.1、创建一个类&#xff0c;包含烟花各项粒子的…

解决 Nginx 反向代理中的 DNS 解析问题:从挑战到突破20231228

引言 在使用 Nginx 作为反向代理服务器时&#xff0c;我们可能会遇到各种配置和网络问题。最近&#xff0c;我遇到了一个有趣的挑战&#xff1a;Nginx 在反向代理配置中无法解析特定的域名&#xff0c;导致 502 错误。这个问题的解决过程不仅揭示了 Nginx 的一个不太为人知的功…

力扣刷题记录(20)LeetCode:198、213、337

198. 打家劫舍 我们从第一个开始分析&#xff1a; dp[i]:i表示索引&#xff0c;dp表示当前索引可以拿到的最高金额 索引为0时&#xff0c;可以拿到的最高金额为1&#xff1b; 索引为1时&#xff0c;可以拿到的最高金额就是在索引[0,1]之间取&#xff0c;为2 索引为2时&…

[华为诺亚实验室+中科大提出TinySAM | 比SAM小10倍,精度的超车!]

文章目录 概要整体架构流程Related Work技术细节小结 概要 最近&#xff0c;Segment Anything Model (SAM) 已经展示出了强大的分割能力&#xff0c;在计算机视觉领域引起了广泛关注。基于预训练的 SAM 的大量研究工作已经开发了各种应用&#xff0c;并在下游视觉任务上取得了令…

leaflet学习笔记-自定义Icon(四)

前言 leaflet的marker可以使用icon&#xff0c;所以这篇文章我们自定义一个icon&#xff0c;并在marker中使用&#xff0c;满足我的恶趣味 实例化Icon 首先准备一个你喜欢的图片&#xff0c;并将它添加到你的项目中&#xff0c;这里我找了一张本人的卡通图片 icon实例化代码&…

121. 买卖股票的最佳时机(Java)

给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。…

二分查找及其复杂的计算

&#xff08;一&#xff09;二分查找及其实现 二分查找&#xff0c;也称为折半查找&#xff0c;是一种高效的搜索算法&#xff0c;用于在有序数组&#xff08;或有序列表&#xff09;中查找特定元素的位置。 二分查找的基本思想是将待查找的区间不断地二分&#xff0c;然后确…

静态HTTP:为什么它如此重要

静态HTTP是互联网上使用最广泛的数据传输协议之一&#xff0c;它在Web应用程序中扮演着至关重要的角色。本文将探讨静态HTTP的重要性及其在Web开发中的应用。 一、静态HTTP的定义和优势 静态HTTP是指服务器上预先生成好的静态文件&#xff0c;这些文件包含HTML、CSS、JavaScr…

erp是什么意思啊?erp系统中有哪些模块?

对于在企业管理领域深耕的人来说&#xff0c;erp这个名字一定不陌生&#xff0c;甚至可以说是他们日常工作中必不可少的重要伙伴。那么&#xff0c;erp究竟是什么意思&#xff1f;erp系统中包含着哪些模块&#xff1f;又是如何协助企业日常运作的呢&#xff1f;今天就让我们一起…

Boot Camp分区备份还原工具boot Camp迁移助手---Winclone pro 10

Winclone Pro 10是一款专为Mac用户设计的先进Windows系统备份和迁移工具。它提供了强大而易于使用的功能&#xff0c;让用户能够轻松地创建、克隆和还原Windows系统&#xff0c;并在Mac上运行。以下是Winclone Pro 10的主要功能和特点&#xff1a; 完整的系统备份和还原&#…

【最新报道】初窥Windows AI 工作室

自我介绍 做一个简单介绍&#xff0c;酒研年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

Vue - Class和Style绑定详解

1. 模板部分 <template><div><!-- Class 绑定示例 --><div :class"{ active: isActive, text-danger: hasError }">Hello, Vue!</div><!-- Class 绑定数组示例 --><div :class"[activeClass, errorClass]">Cla…

大数据Doris(四十三):创建物化视图

文章目录 创建物化视图 一、首先你需要有一个Base表

Android---Kotlin 学习013

互操作性和可空性 Java 世界里所有对象都可能是 null&#xff0c;而 kotlin 里面不能随便给一个变量赋空值的。所有&#xff0c;kotlin 取调用 java 的代码就很容易出现返回一个 null&#xff0c;而 Kotlin 的接收对象不能为空&#xff0c;你不能想当然地认为 java 的返回值就…

(13)Linux 进程的优先级、进程的切换以及环境变量等

前言&#xff1a;我们先讲解进程的优先级。然后讲解进程的切换&#xff0c;最后我们讲解环境变量&#xff0c;并且做一个 "让自己的可执行程序不带路径也能执行"的实践&#xff0c;讲解环境变量的到如何删除&#xff0c;最后再讲几个常见的环境变量。 一、进程优先级…

【Linux基础】8. 网络工具

文章目录 【 1. 查询网络服务和端口 】【 2. 网络路由 】【 3. 镜像下载 】【 4. ftp sftp lftp ssh】【 5. 网络复制 】 【 1. 查询网络服务和端口 】 全称 netstat&#xff08;network statistics&#xff09;网络统计。作用 netstat 命令用于显示各种网络相关信息&#xff…