diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c735cbc..0000000 --- a/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -# top-most EditorConfig file -root = true - -# all files -[*] -indent_style = space -indent_size = 4 \ No newline at end of file diff --git a/APUE.md b/APUE.md deleted file mode 100644 index a9da1e1..0000000 --- a/APUE.md +++ /dev/null @@ -1,5 +0,0 @@ -## UNIX环境高级编程 - -书籍《UNIX环境高级编程》(APUE)的笔记,组织结构完全和书上一致,仅记录重点、关键点和思维骨架,帮助串联回忆,不记录过多细节,细节需要看书来补充。 - -考虑到内容较多,已移动到了 [tch0/APUE_Notes](https://github.com/tch0/APUE_Notes) 仓库。 \ No newline at end of file diff --git a/BNF&RecursiveDescent.md b/BNF&RecursiveDescent.md deleted file mode 100644 index 0e4fc42..0000000 --- a/BNF&RecursiveDescent.md +++ /dev/null @@ -1,604 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [BNF与递归下降](#bnf%E4%B8%8E%E9%80%92%E5%BD%92%E4%B8%8B%E9%99%8D) - - [BNF](#bnf) - - [递归下降](#%E9%80%92%E5%BD%92%E4%B8%8B%E9%99%8D) - - [左递归](#%E5%B7%A6%E9%80%92%E5%BD%92) - - [消除左递归](#%E6%B6%88%E9%99%A4%E5%B7%A6%E9%80%92%E5%BD%92) - - [消除直接左递归](#%E6%B6%88%E9%99%A4%E7%9B%B4%E6%8E%A5%E5%B7%A6%E9%80%92%E5%BD%92) - - [消除间接左递归](#%E6%B6%88%E9%99%A4%E9%97%B4%E6%8E%A5%E5%B7%A6%E9%80%92%E5%BD%92) - - [陷阱](#%E9%99%B7%E9%98%B1) - - [四则运算器实例](#%E5%9B%9B%E5%88%99%E8%BF%90%E7%AE%97%E5%99%A8%E5%AE%9E%E4%BE%8B) - - [EBNF](#ebnf) - - [符号与约定](#%E7%AC%A6%E5%8F%B7%E4%B8%8E%E7%BA%A6%E5%AE%9A) - - [示例](#%E7%A4%BA%E4%BE%8B) - - [表示重复](#%E8%A1%A8%E7%A4%BA%E9%87%8D%E5%A4%8D) - - [ABNF](#abnf) - - [规则](#%E8%A7%84%E5%88%99) - - [最终值](#%E6%9C%80%E7%BB%88%E5%80%BC) - - [操作符](#%E6%93%8D%E4%BD%9C%E7%AC%A6) - - [核心规则](#%E6%A0%B8%E5%BF%83%E8%A7%84%E5%88%99) - - [TODO](#todo) - - [参考](#%E5%8F%82%E8%80%83) - - - -# BNF与递归下降 - -## BNF - -巴科斯范式(Backus Normal Form,缩写为 BNF),又称为巴科斯-诺尔范式(Backus-Naur Form),是一种用于表示[上下文无关文法](https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E6%97%A0%E5%85%B3%E6%96%87%E6%B3%95)的语言,上下文无关文法描述了一类形式语言。 - -广泛地使用于程序设计语言、指令集、通信协议的语法表示中。大多数程序设计语言或者形式语义方面的教科书都采用巴科斯范式。在各种文献中还存在巴科斯范式的一些变体,如[扩展巴科斯范式 EBNF](https://zh.wikipedia.org/wiki/%E6%89%A9%E5%B1%95%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F) 或[扩充巴科斯范式 ABNF](https://zh.wikipedia.org/wiki/%E6%89%A9%E5%85%85%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F)。 - - -BNF文法是一种用递归的思想来表述计算机语言符号集的定义规则。 - -法则: -- `::=`表示定义。 -- `""`双引号中内容表示字符。 -- `<>`尖括号中表示必选内容。 -- `|`两边是可选内容(多个选其一)。 - -其中用`<>`括起来的(出现在`::=`左边的)叫做**非终结符**,它们能用`::=`右边的式子替代,没有出现在`::=`就叫做**终结符**。一般终结符对应词法分析器输出的标记。 - -四则运算的BNF实例: -```BNF - ::= + - | - - | - - ::= * - | / - | - - ::= ( ) - | Number -``` - -当然这里的Number是已经被词法分析处理好的token。其中`+-*/()`都是符号,并没有严格加`""`,明白意思就行。 - -上述BNF文法中已经暗含了运算的优先级`()` > `*/` > `+-` - -## 递归下降 - -当有了BNF就可以直接将其解释为[递归下降](https://zh.wikipedia.org/wiki/%E9%80%92%E5%BD%92%E4%B8%8B%E9%99%8D%E8%A7%A3%E6%9E%90%E5%99%A8)的代码,每一个非终结符用一个函数来解析,通过相互的递归调用完成最终解析,自顶向下直到每一个非终结符。 - -实现上来说比较简单,是实现语法分析的常用算法。 - -## 左递归 - -若一个非终结符号(non-terminal)r 有任何直接的文法规则或者透过多个文法规则,推导出的句型(sentential form)其中最左边的符号 又会出现r,则我们说这个非终结符号r是左递归的。 - -上面的例子中``部分存在[左递归](https://zh.wikipedia.org/wiki/%E5%B7%A6%E9%81%9E%E6%AD%B8),左递归的BNF文法无法直接使用递归下降来实现。因为最左端的递归式的解析过程又是以解析自己为开始的,从而导致无限递归。 - -也就是左递归每一步的调用都没有实质进展,而右递归就不一样,右递归中要解析完了前面的式子才会递归调用自己,每一步调用都会有实质进展,经过有限次调用一定能够解析完成。 - -**直接左递归**就是有直接的文法能够推导出在最左边出现的句型。 -```BNF - ::= | -``` -**间接左递归**是透过多个文法规则推导出在最左边出现自己的句型。 -```BNF - ::= | - ::= | -``` -沿着` -> -> ` 这条路径推导就会出现左递归。 - - -## 消除左递归 - -左递归的[维基百科](https://zh.wikipedia.org/wiki/%E5%B7%A6%E9%81%9E%E6%AD%B8)介绍了消除左递归的方法。这里直接搬运过来: - -### 消除直接左递归 - -一个一般化的移除左递归的算法描述如下。 - -对于每一个如下规则(用BNF描述): -```BNF - ::= B1 | B2 | ... | Bn | C1 | ... | Cn -``` -其中`B1~Bn`是任意终结符与非终结符的序列,并且不为空字符串``。`C1~Cn`是任意不与A开头的终结符与非终结符的序列。 - -**消除左递归后**可将`A`改为如下规则: -```BNF - ::= C1 | C2 | ... | Cn - ::= | B1 | B2 | ... | Bn -``` -其中新创建出来的非终结符``被称为尾巴(tail)或者剩余(rest)。 - -例:考虑如下规则 -```BNF - ::= + | | -``` -可以改成: -```BNF - ::= | - ::= | + -``` -可以进一步简写``为: -```BNF - ::= | + -``` - -理解: -- `A`左递归到头一定是以`C1~Cn`作为开始。 -- `C1~Cn`后面可以为空,也可以是`B1~Bn`和``构成的序列。 -- 将左递归转化为了右递归。 - -### 消除间接左递归 - -如果文法内不存在空字符串的生成(不存在` ::= ...| | ...`这样的规则),而且不是循环的文法(不存在` -> ... -> ... -> `这样形式的规则),就可以用以下算法简化间接左递归: - -- 从`i = 1`到`n` - - 从`j = 1`到`i-1` - - 设``的生成规则为 ` ::= B1 | ... | Bn` - - 将所有规则` ::= C`换成` ::= B1C | ... | BnC`以移除``规则中的左递归 - -其实就是从上往下,将间接左递归依次替换为直接左递归。 - -例: -```BNF - ::= C | D - ::= E | F -``` -第一步: -```BNF - ::= E C | F C | D - ::= E | F -``` -第二步: -```BNF - ::= F C | D - ::= | E C - ::= E | F -``` - -### 陷阱 - -上面的转换使用右递归的文法来避免掉左递归的出现;但是这样会改变规则的结合律。左递归会创造出向左的结合律;但是右递归则会创造出向右的结合律。 - -比如说一个这样的文法(也就是一个整数乘法、加法、括号混合的表达式): -``` - ::= + | - ::= * | - ::= ( ) | Int -``` -去掉左递归后: -``` - ::= | - ::= | + - ::= | - ::= | * - ::= ( ) | Int -``` - -处理一个`a+a+a`的式子为例,使用左递归文法会得到这样的分析树(parse tree): -``` - expr - / | \ - expr + term - / | \ \ - expr + term factor - | | | - term factor Int - | | - factor Int - | - Int -``` -这样的规则代表`(a+a)+a`,也就是左结合。 - -而采用右递归文法会得到这样的分析树: -``` - expr - / \ - term expr - | / | \ - factor + term expr_tail - | | | \ \ - Int factor + term expr_tail - | | | - Int factor - | - Int -``` -这样的规则代表`a+(a+a)`,也就是右结合。 - -对加法这样的左结合与右结合结果相同的运算来说当然不会有影响,但是对于减法或者其他严格要求了左结合的运算来说结果就完全不对了。 - -几种解决这个问题的方法: -- 将规则重新改为左递归,使用能解析左递归的算法来编写程序,比如[LALR语法分析器](https://zh.wikipedia.org/wiki/LALR%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E5%99%A8)。 -- 使用更多的非终端符号来改写规则,以强迫文法合乎正确的结合。例子? -- 如果使用YACC 或者Bison,他们有所谓算符声明(operator declarations), %left, %right and %nonassoc,这一些算符可以告诉语法分析器产生程式(parser generator)应该遵从哪一种结合。 - -其实应该是可以通过递归下降时的调用顺序来控制的,见下面的四则运算实例中减号运算符的实现。 - -## 四则运算器实例 - -消除左递归后的四则运算的BNF: -```BNF - ::= - ::= + - | - - | - - ::= - ::= * - | / - | - - ::= ( ) - | Number -``` - -在此之前为了先实现一个简单的词法分析单独解析整数,其他的token用字符表示。 - -```C++ -enum {Number = 128}; -int token = 0; -int token_val = 0; -char* src = NULL; - -void next() -{ - while (*src == ' ' || *src == '\t') - { - src++; - } - token = *src++; - if (token >= '0' && token <= '9') - { - token_val = token - '0'; - token = Number; - while (*src >= '0' && *src <= '9') - { - token_val = token_val * 10 + *src - '0'; - src++; - } - } - return; -} - -void match(int tk) -{ - if (token != tk) - { - printf("expected token: %d(%c), got: %d(%c)\n", tk, tk, token, token); - exit(-1); - } - next(); -} -``` -然后按照BNF实现递归下降即可: -```C++ -int expr(); -int factor() -{ - int value = 0; - if (token == '(') - { - match('('); - value = expr(); - match(')'); - } - else - { - value = token_val; - match(Number); - } - return value; -} - -int term_tail(int lvalue) -{ - if (token == '*') - { - match('*'); - int value = lvalue * factor(); - return term_tail(value); - } - else if (token == '/') - { - match('/'); - int value = lvalue / factor(); - return term_tail(value); - } - else - { - return lvalue; - } -} - -int term() -{ - int value = factor(); - return term_tail(value); -} - -int expr_tail(int lvalue) -{ - if (token == '+') - { - match('+'); - int value = lvalue + term(); - return expr_tail(value); - } - else if (token == '-') - { - match('-'); - int value = lvalue - term(); - return expr_tail(value); - } - else - { - return lvalue; - } -} - -int expr() -{ - int value = term(); - return expr_tail(value); -} -``` -主函数与测试: -```C++ -void test(char* str, int result) -{ - static count = 0; - src = str; - next(); - int actual = expr(); - printf("test %d %s, input: %s, expected result: %d, actual result : %d\n", - count++, (actual == result) ? "passed" : "failed", str, result, actual); -} -int main() -{ - // test - test("1+1", 2); - test("1+2*3-4+5", 8); - test("1-1-1", -1); - test("(100+1)", 101); - test("(10-5)/2*(8/4) + 6", 10); - test("(2*(10+((10-5)/2*(8/4) + 6)))", 40); -} -``` - -这里实现中是不存在右结合问题的。 - -就四则运算来说,用运算符优先级加栈的方式实现会更加简单。 - -## EBNF - -扩展巴科斯-瑙尔范式(EBNF, Extended Backus–Naur Form)是表达作为描述计算机编程语言和形式语言的正规方式的上下文无关文法的元语法(metalanguage)符号表示法。它是基本巴科斯范式(BNF)元语法符号表示法的一种扩展。 - -它最初由尼克劳斯·维尔特开发,最常用的 [EBNF](https://zh.wikipedia.org/wiki/%E6%89%A9%E5%B1%95%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F) 变体由标准 ISO-14977 所定义。 - -### 符号与约定 - -符号表: -|用途|符号表示|解释| -|:-:|:-:|:-:| -|定义|`=`|| -|串接|`,`|连接多个序列| -|终止|`;`|一个定义的结束| -|分隔|`\|`|表选择| -|可选|`[ ... ]`|出现0或1次| -|重复|`{ ... }`|出现0或任意次| -|分组|`( ... )`|表示优先级,防止歧义,`a | (b, c | d)`| -|双引号|`" ... "`|字符串,和`''`一样,要表示`"`字符就使用`'"'`| -|单引号|`' ... '`|字符串,和`""`一样,要表示`'`字符就使用`"'"`| -|注释|`(* ... *)`|| -|特殊序列|`? ... ?`|特殊序列不由EBNF来解释| -|除外|`-`|除外某些情况,用来表示一个新集合| -|重复|`*`|放在一个单元前,`n * a`,将a重复n次| - -1. 约定: - - 扩展 BNF 每个元标识符都被写为用连字号连接起来的一个或多个字; - - 结束于“-symbol” 的元标识符是扩展 BNF 的终结符的名字。 -2. EBNF 的每个操作符的正常字符和它所蕴涵的优先级(顶部为最高优先级)为: - ```EBNF - * repetition-symbol - - except-symbol - , concatenate-symbol - | definition-separator-symbol - = defining-symbol - ; terminator-symbol - ``` - - 其他可能还不容易混淆,但需要特别注意串联优先级是高于选择的。如果要先选择后串联则需要使用分组,如`(a | b) , c`。 - - 不用空格做串联,所以一个非终结符中可以包含空格。 - -3. 下列括号对超越操作符的优先级: - ```EBNF - ' first-quote-symbol first-quote-symbol ' (* 终结符 *) - " second-quote-symbol second-quote-symbol " (* 终结符 *) - (* start-comment-symbol end-comment-symbol *) (* 注释 *) - ( start-group-symbol end-group-symbol ) (* 分组 *) - [ start-option-symbol end-option-symbol ] (* 可选 *) - { start-repeat-symbol end-repeat-symbol } (* 重复 *) - ? special-sequence-symbol special-sequence-symbol ? (* 特殊序列 *) - ``` - -EBNF中可以有多个序列串接起来,`|`只在其所在的序列起作用。EBNF描述能力比BNF更强: -- 终结符被严格的包围在引号 (`"..."` 或 `'...'`) 中。给非终结符的尖括号 (`"<...>"`)可以省略。 -- 通常使用终止字符分号结束一个规则。 -- 提供了定义重复次数,排除法选择(比如除了引号的所有字符)和注释等的增强机制。 -- EBNF 在能定义的语言的意义上不比 BNF 更强大。在原理上用 EBNF 定义的任何文法都可以用 BNF 表达。 - -顺便一提,在markdown中BNF和EBNF都可以得到高亮支持。 - -EBNF可能还有一些扩展,此处不详述,遇到再说。 - -### 示例 - -四则运算示例: -```EBNF -expr = ([expr "+"] | [expr "-"]), term; -term = ([term "*"] | [term "/"]), factor; -factor = "(", expr, ")" | number; -``` - -消除左递归(在EBNF中,可以不使用空串,因为空串可以用`[]`可选的含义来代替),另外进一步描述number: -```EBNF -expr = term expr_tail; -expr_tail = [("+" | "-"), term]; -term = factor term_tail; -term_tail = [("*" | "-"), factor]; -factor = "(", expr, ")" | number; -number = "0" | ["-"], digit - "0", {digit}; -digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -``` - -维基上的一个只允许赋值的简单编程语言例子: -```EBNF - (* a simple program in EBNF − Wikipedia *) - program = 'PROGRAM' , white space , identifier , white space , - 'BEGIN' , white space , - { assignment , ";" , white space } , - 'END.' ; - identifier = alphabetic character , [ { alphabetic character | digit } ] ; - number = [ "-" ] , digit , [ { digit } ] ; - string = '"' , { all characters − '"' } , '"' ; - assignment = identifier , ":=" , ( number | identifier | string ) ; - alphabetic character = "A" | "B" | "C" | "D" | "E" | "F" | "G" - | "H" | "I" | "J" | "K" | "L" | "M" | "N" - | "O" | "P" | "Q" | "R" | "S" | "T" | "U" - | "V" | "W" | "X" | "Y" | "Z" ; - digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; - white space = ? white space characters ? ; - all characters = ? all visible characters ? ; -``` -一个语法上正确的程序: -``` -PROGRAM DEMO1 -BEGIN - A0:=3; - B:=45; - H:=-100023; - C:=A; - D123:=B34A; - BABOON:=GIRAFFE; - TEXT:="Hello world!"; -END. -``` - -对于 `?...?`特殊序列的解释超出了 EBNF 标准的范围,这些内容应该由写EBNF的人进行解释。 - -### 表示重复 - -```EBNF -aa = "A"; -bb = 3 * aa, "B"; (*aa重复3次*) -cc = 3 * [aa], "C"; (*aa重复0~3次*) -dd = {aa}, "D"; (*aa重复0次或任意次*) -ee = aa, {aa}, "E"; (*aa重复至少1次*) -ff = 3 * aa, 3 * [aa], "F"; (*aa重复3~6次*) -gg = {3 * aa}, "D"; (*aa重复0次或3的整数倍数次*) -``` - -`3 * aa`就和`aa aa aa`等价,是一种简化写法,所以`3 * [aa]`表示`[aa], [aa], [aa]`而不是将`aa`是否存在确定下来之后再重复三次。 - -## ABNF - -在计算机科学中,扩充巴科斯-瑙尔范式(ABNF)是一种基于巴科斯-瑙尔范式(BNF)的元语言,但它有自己的语法和派生规则。ABNF的原动原则是描述一种作为双向通信协议的语言的形式系统。 - -它是由[第68号互联网标准](http://www.rfc-editor.org/std/std68.txt)("STD 68",大小写样式按照原文)定义的,也就是RFC 5234,经常用于互联网工程任务组(IETF)通信协议的定义语言。 - -更多具体内容请查看[扩充巴科斯范式](https://zh.wikipedia.org/wiki/%E6%89%A9%E5%85%85%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F),下面的内容就是从这里搬过来的。 - -ABNF更好地用于描述通信协议中的形式系统,更清晰地定义了字符编码(其二进制值)、定量重复等规则。 - -### 规则 - -一个ABNF规范是一些推导规则的集合,书写为: - -```ABNF -规则 = 定义;注释CR LF -``` -其中: -- “规则”是不区分大小写的非最终符号,也就是**非终结符** -- “定义”由定义该规则的一系列符号组成 -- “注释”用于记录 -- “CR LF”(回车、换行)是位于行尾的字符,用来结束 -- 规则名字是不区分大小写的: ``, ``, ``和``都指的是同一个规则。规则名字由一个字母以及后续的多个字母、数字和连字符(减号)组成。 -- 用尖括号(“<”,“>”)包围规则名并不是必需的(如同它们在BNF里那样),但是它们可以用来界定规则名,以方便识别出规则名。 - -### 最终值 - -也就是**终结符**,由一个或多个数值字符指定。 - -- **数值字符**可按下面的方式指定:先是一个百分号`%`,紧跟着基数(`b` = 二进制, `d` = 十进制, `x` = 十六进制),再其后是这个数值或数值串(用`.`来指示串联),比如b`CR LF`就可以用`%d13.10`或者 `%x0D.0A`表示。 -- **字面文本**是通过包含在在双引号(`"`)中字符串来指定的。这些字符串是不区分大小写的,使用的字符集是ASCII。所以`"abc"`会匹配所有符合`("a" | "A"), ("b" | "B"), ("c" | "C")`(EBNF)的字符串。所以如果要严格匹配大小写,应该使用`%`与其编码严格表示。 - -### 操作符 - -|用途|符号表示|含义| -|:-:|:-:|:-| -|空白字符|空白字符本身|空白字符被用来分隔定义中的各个元素:要使空格被识别为分割符则必须明确的包含它(`%`或者`""`)| -|串联|`规则1 规则2`|用空白字串联两个或多个规则| -|选择|`/`|`规则1 / 规则2`,两者选其一。| -|增量选择|`=/`|`规则1 =/ 规则2`,增加规则1的补充选择| -|值范围|`%c##-##`|`%b`, `%d`, `%x`,`.`指示串联,`-`表示数值范围选择。`"0" / "1"`也可以表示为`%x30-31`| -|序列组合|`()`|`(规则1 规则2)`元素可以放置在圆括号中来将规则组合起来,该组合视为单个元素。| -|不定量重复|`m*n`|`m*n规则`可选的`m`给出要包含的元素的最小数目,默认为0;可选的`n`给出要包含的元素的最大数目,默认为无穷大| -|定量重复|`n规则`|定量重复n次| -|可选序列|`[规则]`|可选,也就是0或1次,等价于`0*1(规则)`| -|注释|`;`|`;comment`| - - -操作符优先级: -- 规则名、最终值 -- 注释; -- 值范围%c##-## -- 重复* -- 组合 ()、可选[] -- 串联 -- 选择 / - -ABNF使用空白符而不是逗号来串联,同BNF/EBNF一样串联优先级高于选择。 - -### 核心规则 - -核心规则定义与标准中,可以直接用。 - -规则|形式定义|意义| -|:-|:-|:-| -|ALPHA|`%x41-5A / %x61-7A`|大写和小写ASCII字母(A-Z, a-z)| -|DIGIT|`%x30-39`|数字(0-9)| -|HEXDIG|`DIGIT / "A" / "B" / "C" / "D" / "E" / "F"`|十六进制数字(0-9, A-F, a-f)| -|DQUOTE|`%x22`|双引号| -|SP|`%x20`|空格| -|HTAB|`%x09`|横向制表符| -|WSP|`SP / HTAB`|空格或横向制表符| -|LWSP|`*(WSP / CRLF WSP)`|直线空白(晚于换行)| -|VCHAR|`%x21-7E`|可见(打印)字符| -|CHAR|`%x01-7F`|任何7-位US-ASCII字符,不包括NUL(%x00)| -|OCTET|`%x00-FF`|8位数据| -|CTL|`%x00-1F / %x7F`|控制字符| -|CR|`%x0D`|回车| -|LF|`%x0A`|换行| -|CRLF|`CR LF`|互联网标准换行| -|BIT|`"0" / "1"`|二进制数字| - -总结:其实EBNF和ABNF规则都大同小异,侧重点不同,ABNF更侧重于网络传输,所以将字符编码作为最基本的非终结符以确保二进制位的正确性。 - -## TODO - -这里只涉及BNF/EBNF/ABNF文法的规则以及递归下降实现。本科时没有上过编译原理这门课,有时间一定要补一下,包括但不限于: - -- 上下文无关文法(CFG) -- PEG([解析表达文法](https://zh.wikipedia.org/wiki/%E8%A7%A3%E6%9E%90%E8%A1%A8%E8%BE%BE%E6%96%87%E6%B3%95)),不同于CFG是另一中形式语言,关键区别就是PEG中的选择是有序的 -- DFA/NFA -- LL分析器与LR分析器 -- lex/yacc & flex/bison -- [Comparison of parser generators](https://en.wikipedia.org/wiki/Comparison_of_parser_generators) -- 写一个正则表达式的parser -- 沿着词法分析、语法分析、语义分析、代码优化、代码生成的标准路径写一个编译器 - -## 参考 -- [手把手教你构建 C 语言编译器(4)- 递归下降](https://lotabout.me/2016/write-a-C-interpreter-4/) -- 维基百科。 - diff --git a/BatchScript.md b/BatchScript.md deleted file mode 100644 index fa33d23..0000000 --- a/BatchScript.md +++ /dev/null @@ -1,1101 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Windows批处理脚本](#windows%E6%89%B9%E5%A4%84%E7%90%86%E8%84%9A%E6%9C%AC) - - [1. Dos命令](#1-dos%E5%91%BD%E4%BB%A4) - - [2. Cmd命令处理程序](#2-cmd%E5%91%BD%E4%BB%A4%E5%A4%84%E7%90%86%E7%A8%8B%E5%BA%8F) - - [3. Batch批处理脚本](#3-batch%E6%89%B9%E5%A4%84%E7%90%86%E8%84%9A%E6%9C%AC) - - [3.1 echo](#31-echo) - - [3.2 @](#32-) - - [3.3 注释](#33-%E6%B3%A8%E9%87%8A) - - [3.4 pause](#34-pause) - - [3.5 命令行参数](#35-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0) - - [3.6 goto](#36-goto) - - [3.7 call](#37--call) - - [3.8 if ELSE](#38-if-else) - - [3.9 for](#39-for) - - [3.10 start](#310-start) - - [3.11 set](#311-set) - - [3.12 重定向 > & >>](#312-%E9%87%8D%E5%AE%9A%E5%90%91---) - - [3.13 管道 |](#313-%E7%AE%A1%E9%81%93-) - - [3.14 & && ||](#314---) - - [3.15 setlocal endlocal](#315-setlocal-endlocal) - - [4. 环境变量详解](#4-%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AF%A6%E8%A7%A3) - - [5. 常用Cmd命令](#5-%E5%B8%B8%E7%94%A8cmd%E5%91%BD%E4%BB%A4) - - [6. 脚本编写技巧](#6-%E8%84%9A%E6%9C%AC%E7%BC%96%E5%86%99%E6%8A%80%E5%B7%A7) - - [7. 批处理脚本实例](#7-%E6%89%B9%E5%A4%84%E7%90%86%E8%84%9A%E6%9C%AC%E5%AE%9E%E4%BE%8B) - - [7.1 文件路径相关](#71-%E6%96%87%E4%BB%B6%E8%B7%AF%E5%BE%84%E7%9B%B8%E5%85%B3) - - [7.2 用户交互](#72-%E7%94%A8%E6%88%B7%E4%BA%A4%E4%BA%92) - - [7.3 区分命令行执行和双击执行](#73-%E5%8C%BA%E5%88%86%E5%91%BD%E4%BB%A4%E8%A1%8C%E6%89%A7%E8%A1%8C%E5%92%8C%E5%8F%8C%E5%87%BB%E6%89%A7%E8%A1%8C) - - [8. 我的评价](#8-%E6%88%91%E7%9A%84%E8%AF%84%E4%BB%B7) - - [9. Windows管理深入](#9-windows%E7%AE%A1%E7%90%86%E6%B7%B1%E5%85%A5) - - [9.1 注册表](#91-%E6%B3%A8%E5%86%8C%E8%A1%A8) - - [9.2 组策略](#92-%E7%BB%84%E7%AD%96%E7%95%A5) - - - -# Windows批处理脚本 - -教程与文章: -- [Windows批处理(cmd/bat)常用命令教程](https://www.cnblogs.com/xpwi/p/9626959.html) -- [Windows 批处理脚本指南](https://www.jianshu.com/p/3da1657f4c2b) -- 网上的教程真不如帮助文档来得详细,**BAT的命令帮助**都可以通过执行命令加参数`/?`得到。 - -虽然感觉好像Windows批处理脚本可能已经过时了,而且据了解,windows命令行解释器好像存在一些陈年老BUG。但是就了解历史,并且当前也可能会用到的目的还是可以了解一下。 - -并且借此机会加深了解Windows系统,作为软件从业人员,对每天都在用的Windows操作系统了解好像实在是有点太少了。 - -## 1. Dos命令 - -DOS(Disk Operating System),就是Windows的前身,DOS中支持的命令就称之为DOS命令,在现在的Windows命令行提示符中依然可以使用。在命令行下执行命令其实就跟Win+R直接运行是一样都是在`Path`环境变量下搜索这个`.exe`可执行文件去执行。现在可能还会用的DOS命令: -- 目录命令: - - `dir` 列出当前目录文件与子目录。 - - `cd` 切换当前目录。 - - `mkdir`/`md` 创建文件夹。 - - `rd` 删除空文件夹。 - - `path` 设备可执行文件搜索路径,就是`path`环境变量。 - - `tree` 以树形式打印一个目录或者驱动器下所有目录和文件。 - - `tasklist` 打印所有进程,功能类似任务管理器。 -- 文件命令: - - `copy` 拷贝文件。 - - `xcopy` 拷贝目录以及文件。 - - `type` 显示ASCII文件内容。 - - `ren` 重命名。 - - `move` 移动文件。 - - `fc` 比较两个文本文件差异。 - - `attrib` 修改指定文件属性。 - - `del` 删除文件。 -- 其他命令: - - `ping` 测试网络。 - - `cls` 清屏。 - - `data` 日期。 - - `time`时间。 - - `ver`系统版本。 - - `shutdown` 执行关机重启等一系列操作。 -- 常用的软件: - - `notepad` 记事本 - - `taskmgr` 任务管理器 - - `regedit` 注册表编辑器 - - `gpedit.msc` 组策略编辑器 - - `calc` 计算器 - - etc - -都非常简单感觉也都没有太大必要去了解。 - - -## 2. Cmd命令处理程序 - -执行方式: -- `Win + R`,输入`Cmd`回车。 -- 执行Windows命令行提示符。 -- 在`Cmd`命令行中执行`cmd`以打开一个新的命令处理程序子进程。 -- 直接执行`C:\Windows\system32\cmd.exe`,总之无论何种方式最终都是执行的该`exe`文件。 - -关于`Cmd`命令,`cmd /?`的帮助如下: - -启动 Windows 命令处理程序的一个新实例 -```bat -CMD [/A | /U] [/Q] [/D] [/E:ON | /E:OFF] [/F:ON | /F:OFF] [/V:ON | /V:OFF] [[/S] [/C | /K] string] -``` -|选项|行为| -|:-|:-| -|`/C` |执行字符串指定的命令然后终止| -|`/K` |执行字符串指定的命令但保留| -|`/S` |修改 `/C` 或 `/K` 之后的字符串处理(见下)| -|`/Q` |关闭回显| -|`/D` |禁止从注册表执行 `AutoRun` 命令(见下)| -|`/A` |使向管道或文件的内部命令输出成为 `ANSI`| -|`/U` |使向管道或文件的内部命令输出成为 `Unicode`| -|`/T:fg` |设置前台/背景颜色(详细信息见 `COLOR /?`)| -|`/E:ON` |启用命令扩展(见下)| -|`/E:OFF` |禁用命令扩展(见下)| -|`/F:ON` |启用文件和目录名补全字符(见下)| -|`/F:OFF` |禁用文件和目录名补全字符(见下)| -|`/V:ON` |使用 `!` 作为分隔符启用延迟的环境变量扩展。例如,`/V:ON` 会允许 `!var!` 在执行时扩展变量 `var`。`var` 语法会在输入时扩展变量,这与在一个 `FOR`循环内不同。| -|`/V:OFF` |禁用延迟的环境扩展。| - -注意,如果字符串加有引号,可以接受用命令分隔符 `&&` 分隔多个命令。另外,由于兼容性原因,`/X` 与 `/E:ON` 相同,`/Y` 与 `/E:OFF` 相同,且 `/R` 与 `/C` 相同。任何其他开关都将被忽略。 - -如果指定了 `/C` 或 `/K`,则会将该开关之后的命令行的剩余部分作为一个命令行处理,其中,会使用下列逻辑处理引号`"`字符: - -1. 如果符合下列所有条件,则会保留命令行上的引号字符: - - 不带 `/S` 开关 - - 正好两个引号字符 - - 在两个引号字符之间无任何特殊字符,特殊字符指下列字符: `&<>()@^|` - - 在两个引号字符之间至少有一个空格字符 - - 在两个引号字符之间的字符串是某个可执行文件的名称。 - -2. 否则,老办法是看第一个字符是否是引号字符,如果是,则去掉首字符并删除命令行上最后一个引号,保留最后一个引号之后的所有文本。 - -如果 `/D` 未在命令行上被指定,当 `CMD.EXE` 开始时,它会寻找以下 `REG_SZ/REG_EXPAND_SZ` 注册表变量。如果其中一个或两个都存在,这两个变量会先被执行:`HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\AutoRun` 和/或 `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\AutoRun`。 - -命令扩展是**按默认值启用**的。你也可以使用 `/E:OFF` ,为某一特定调用而停用扩展。你可以在机器上和/或用户登录会话上启用或停用 `CMD.EXE` 所有调用的扩展,这要通过设置使用 `REGEDIT.EXE` 的注册表中的一个或两个 `REG_DWORD` 值: `HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\EnableExtensions` 和/或 `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\EnableExtensions`。在我的机器上,前者存在,默认值为1,后者不存在,可以自行添加。 - -值为 `0x1` 或 `0x0`。用户特定设置比机器设置有优先权。命令行开关比注册表设置有优先权。 - -在批处理文件中,`SETLOCAL ENABLEEXTENSIONS` 或 `DISABLEEXTENSIONS` 参数比 `/E:ON` 或 `/E:OFF` 开关有优先权。请参阅 `SETLOCAL /?` 获取详细信息。 - -命令扩展包括对下列命令所做的更改和/或添加: - -``` -DEL or ERASE -COLOR -CD or CHDIR -MD or MKDIR -PROMPT -PUSHD -POPD -SET -SETLOCAL -ENDLOCAL -IF -FOR -CALL -SHIFT -GOTO -START (同时包括对外部命令调用所做的更改) -ASSOC -FTYPE -``` - -有关特定详细信息,请键入 `commandname /?` 查看。 - -延迟环境变量扩展不按默认值启用。你可以用 `/V:ON` 或 `/V:OFF` 开关,为 `CMD.EXE` 的某个调用而启用或停用延迟环境变量扩展。你可以在机器上和/或用户登录会话上启用或停用 `CMD.EXE` 所有调用的延迟扩展,这要通过设置使用 `REGEDIT.EXE` 的注册表中的一个或两个 `REG_DWORD` 值: `HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\DelayedExpansion` 和/或 `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\DelayedExpansion`。 - -值为 `0x1` 或 `0x0`。用户特定设置比机器设置有优先权。命令行开关比注册表设置有优先权。 - -在批处理文件中,`SETLOCAL ENABLEDELAYEDEXPANSION` 或 `DISABLEDELAYEDEXPANSION`参数比 `/V:ON` 或 `/V:OFF` 开关有优先权。请参阅 `SETLOCAL /?`获取详细信息。 - -如果延迟环境变量扩展被启用,惊叹号字符 `!` 可在执行时间被用来代替一个环境变量的数值。 - -你可以用 `/F:ON` 或 `/F:OFF` 开关为 `CMD.EXE` 的某个调用而启用或禁用文件名完成。你可以在计算上和/或用户登录会话上启用或禁用 `CMD.EXE` 所有调用的完成,这可以通过使用 `REGEDIT.EXE` 设置注册表中的下列 `REG_DWORD` 的全部或其中之一: - -- `HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\CompletionChar` -`HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\PathCompletionChar` -- `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\CompletionChar` -`HKEY_CURRENT_USER\Software\Microsoft\Command Processor\PathCompletionChar` - -由一个控制字符的十六进制值作为一个特定参数(例如,`0x4`是`Ctrl-D`,`0x6` 是 `Ctrl-F`)。用户特定设置优先于机器设置。命令行开关优先于注册表设置。 - -如果完成是用 `/F:ON` 开关启用的,两个要使用的控制符是:目录名完成用 `Ctrl-D`,文件名完成用 `Ctrl-F`。要停用注册表中的某个字符,请用空格(`0x20`)的数值,因为此字符不是控制字符。 - -如果键入两个控制字符中的一个,完成会被调用。完成功能将路径字符串带到光标的左边,如果没有通配符,将通配符附加到左边,并建立相符的路径列表。然后,显示第一个相符的路径。如果没有相符的路径,则发嘟嘟声,不影响显示。之后,重复按同一个控制字符会循环显示相符路径的列表。将 `Shift`键跟控制字符同时按下,会倒着显示列表。如果对该行进行了任何编辑,并再次按下控制字符,保存的相符路径的列表会被丢弃,新的会被生成。如果在文件和目录名完成之间切换,会发生同样现象。两个控制字符之间的唯一区别是文件完成字符符合文件和目录名,而目录完成字符只符合目录名。如果文件完成被用于内置式目录命令(`CD`、`MD` 或 `RD`),就会使用目录完成。用引号将相符路径括起来,完成代码可以正确处理含有空格或其他特殊字符的文件名。同时,如果备份,然后从行内调用文件完成,完成被调用时位于光标右方的文字会被调用。 - -需要引号的特殊字符是: -``` - -()[]{}^=;!'+,`~(&() -``` - -上述的完成就是Completion,就是补全的意思,执行命令是可以对文件和目录名进行补全,默认使用注册表值(`0x9`)都是`Tab`,一般也不需要改,同时按住`Shift`可以反方向搜索。如果是通过`cmd /F:ON`执行的命令的话,则对目录补全用`Ctrl-D`,文件补全用`Ctrl-F`。只会补全目录和文件不会补全命令。 - -## 3. Batch批处理脚本 - -批处理脚本的后缀是`.bat`或者`.cmd`,没有固定格式,文件内容大区分大小写,每一行可视作一条命令,从第一行执行到最后一行,双击即可执行,类似于Unix中的Shell脚本,主要是调用各种DOS命令来完成操作。顾名思义用来做批处理减少重复操作提高效率的脚本。 - -批处理中的命令都可以在`cmd`中直接执行,写成脚本就方便多了。 - -`bat`脚本的执行其实就是在命令行中执行是一样的,也会弹出Cmd的黑色对话框,其实就是调用了`C:\Windows\system32\cmd.exe`去执行。 - -批处理文件的命名推荐不使用空格,空格出现在文件名中可能会令人头疼,使用驼峰命名法或者`.` `-` `_`等字符作为分隔符是更好的选择,如果使用了空格,执行时请将文件名用`""`包起来。另外请避免命名与系统内置命令或常用软件重名,这会让人感到混乱。 - -就已知信息来看,没有调试批处理脚本的方法,那估计就只有`echo`打印中间信息来调了。 - -### 3.1 echo - -显示消息,或者启用或关闭命令回显。 - -```bat -ECHO [ON | OFF] -ECHO [message] -``` - -若要显示当前回显设置,请键入不带参数的 `ECHO`。 - -`echo.`可以输出一个空行。 - -```bat -echo hello,world! -echo on -echo off -echo -echo. -``` - -### 3.2 @ - -`@`放在一条命令前,表示关闭该条命令本身的回显。通常放在`echo off`前关闭`echo off`回显。 -```bat -@echo off -``` - -### 3.3 注释 - -在批处理文件或 `CONFIG.SYS` 里加上注解或说明。 - -```bat -REM [comment] -``` -`rem`即是remark的意思,后面内容视为注释,但是`echo on`时可以回显。 - -`::`后面也是注释,任何情况下都不回显。 - -行内注释使用`%comment%`,将内容放在`%%`中间,比较少用。 - -### 3.4 pause - -`pause`让批处理程序暂停一下,命令行中显示`请按任意键继续. . .`然后按下任意键后就会继续往后执行。以前在VC中写控制台程序为了让程序执行完停住经常会写的`System("pause")`就是让操作系统调用`pause`命令,当然现在不需要这样写了,注意只有Windows支持。 - -### 3.5 命令行参数 - -就像任何编程语言中你都可以给你编写的程序指定命令行参数(最终他们会作为字符串数组传递给`main`函数)一样,批处理脚本也可以添加命令行参数。但是批处理脚本没有入口,直接使用`%1` `%2` 就可以引用第1个和第2个参数。如果没有该参数,那么`%num`就是一个空的字符串。`%0`代表批处理文件本身。 -```bat -::test.bat -@echo off -echo this is test.bat! -echo %1+%2 -``` - -为了能够给批处理指定参数,你需要在`Cmd`中执行该`bat`。仅能通过`%num`引用最多9个参数,`%10`会被识别为第一个参数后附加了一个`0`。但是可以使用`shift`命令可以是参数从某个位置开始后边的参数向左移动一个位置,该位置参数被丢弃。这样就可以取到后面的参数了。 - -### 3.6 goto - -将 `cmd.exe` 定向到批处理程序中带标签的行。 - -```bat -GOTO label -``` -`label` : 指定批处理程序中用作标签的文字字符串。 - -标签必须单独一行,并且以冒号`:`打头。 - -如果命令扩展被启用,`GOTO` 会如下改变: - -`GOTO` 命令现在接受目标标签 `:EOF`,这个标签将控制转移到当前批脚本文件的结尾。不定义就退出批脚本文件,这是一个容易的办法。有关能使该功能有用的 `CALL` 命令的扩展描述,请键入`CALL /?`。 - - -### 3.7 call - -从批处理程序调用另一个批处理程序。 - -```bat -CALL [drive:][path]filename [batch-parameters] -``` - -`batch-parameters` :指定批处理程序所需的命令行参数。 - -如果命令扩展被启用,`CALL` 会如下改变: - -`CALL` 命令现在将标签当作 `CALL` 的目标接受。语法是: -```bat -CALL :label arguments -``` -一个新的批文件上下文由指定的参数所创建,控制在标签被指定后传递到语句。你必须通过达到批脚本文件末两次来 "exit" 两次。第一次读到文件末时,控制会回到 `CALL` 语句的紧后面。第二次会退出批脚本。键入 `GOTO /?`,参看 `GOTO :EOF` 扩展的描述,此描述允许你从一个批脚本返回。 - -也就是说`call`一个标签时会创建新的批处理上下文,并且执行到文件末才会返回这个`call`语句,相当于把这个标签起的内容当做一个新的`bat`文件。如果在一个`bat`文件中调用了`goto :EOF`,那么相当于跳转到批处理文件末尾也就是结束该批处理文件。 - -另外,批脚本文本参数参照(`%0`、`%1`、等等)已如下改变: - - -批脚本里的 `%*` 指出所有的参数(如 `%1` `%2` `%3` `%4` `%5` ...) -批参数(`%n`)的替代已被增强。你可以使用以下语法: - -|参数|含义| -|:-|:-| -|`%~1` | 删除引号(`"`),扩展 `%1`| -|`%~f1` | 将 `%1` 扩展到一个完全合格的路径名| -|`%~d1` | 仅将 `%1` 扩展到一个驱动器号| -|`%~p1` | 仅将 `%1` 扩展到一个路径| -|`%~n1` | 仅将 `%1` 扩展到一个文件名| -|`%~x1` | 仅将 `%1` 扩展到一个文件扩展名| -|`%~s1` | 扩展的路径只含有短名| -|`%~a1` | 将 `%1` 扩展到文件属性| -|`%~t1` | 将 `%1` 扩展到文件的日期/时间| -|`%~z1` | 将 `%1` 扩展到文件的大小| -|`%~$PATH:1`| 查找列在 `PATH` 环境变量的目录,并将 `%1` 扩展到找到的第一个完全合格的名称。如果环境变量名未被定义,或者没有找到文件,此修改符会扩展到空字符串| - -可以组合修改符来取得多重结果: - -|参数|含义| -|:-|:-| -|`%~dp1` | 只将 `%1` 扩展到驱动器号和路径| -|`%~nx1` | 只将 `%1` 扩展到文件名和扩展名| -|`%~dp$PATH:1` | 在列在 `PATH` 环境变量中的目录里查找 `%1`,并扩展到找到的第一个文件的驱动器号和路径。| -|`%~ftza1` | 将 `%1` 扩展到类似 `DIR` 的输出行。| - -在上面的例子中,`%1` 和 `PATH` 可以被其他有效数值替换。`%~` 语法被一个有效参数号码终止。`%~` 修定符不能跟 `%*`使用。 - -例子,同目录下存在`hello.txt`文件: -```bat -::test.bat -@echo off -echo this is test.bat! -call hello.bat hello.txt -pause -``` -```bat -::hello.bat -@echo off -echo this is hello.bat! -echo %~f1 -echo %~d1 -echo %~p1 -echo %~n1 -echo %~x1 -echo %~s1 -echo %~a1 -echo %~t1 -echo %~z1 -echo %~$PATH:1 -echo %~dp1 -echo %~nx1 -echo %~ftza1 -pause -goto :EOF -echo whatever -``` -执行结果: -``` -this is test.bat! -this is hello.bat! -C:\Users\CapT\Desktop\BatTest\hello.txt -C: -\Users\CapT\Desktop\BatTest\ -hello -.txt -C:\Users\CapT\Desktop\BatTest\hello.txt ---a-------- -2021/04/28 09:00 -15 -ECHO 处于关闭状态。 -C:\Users\CapT\Desktop\BatTest\ -hello.txt ---a-------- 2021/04/28 09:00 15 C:\Users\CapT\Desktop\BatTest\hello.txt -请按任意键继续. . . -请按任意键继续. . . -``` - -利用扩展的批处理参数可以得到很多东西,其中`%~a1` `%~t1` `%~z1` 是获取文件的属性,需要文件存在,不存在的话结果为空,其他除搜索`%~$PATH:1`外都是简单的仅使用字符串作为文件名的操作,不要求文件存在。 - -利用`goto :EOF`和`call :label`就可以做到类似于函数调用的效果,定义多个标签作为函数,再定义一个入口作为整个批处理的入口就有编程语言那个味道了,只能说历史痕迹实在是太重了。 - -```bat -::functest.bat -goto :Main - -:func1 - ::do something - goto :EOF - -:func2 - ::do something - goto :EOF - -:Main - call func1 args1 - call func2 args2 -``` - -### 3.8 if ELSE - -执行批处理程序中的条件处理。 - -```bat -IF [NOT] ERRORLEVEL number command -IF [NOT] string1==string2 command -IF [NOT] EXIST filename command -``` - -- `NOT`指定只有条件为 `false` 的情况下,Windows 才应该执行该命令。 - -- `ERRORLEVEL number` 如果最后运行的程序返回一个等于或大于指定数字的退出代码,指定条件为 `true`。 - -- `string1==string2` 如果指定的文字字符串匹配,指定条件为 `true`。 -- `EXIST filename` 如果指定的文件名存在,指定条件为 `true`。 -- `command` 如果符合条件,指定要执行的命令。如果指定的条件为 `FALSE`,命令后可跟 `ELSE` 命令,该命令将在 `ELSE` 关键字之后执行该命令。 - -`ELSE` 子句必须出现在同一行上的 `IF` 之后。例如: - -```bat -IF EXIST filename. ( - del filename. -) ELSE ( - echo filename. missing. -) -``` - -由于 `del` 命令需要用新的一行终止,因此以下子句不会有效: -```bat -IF EXIST filename. del filename. ELSE echo filename. missing -``` -由于 `ELSE` 命令必须与 `IF` 命令的尾端在同一行上,以下子句也 -不会有效: -```bat -IF EXIST filename. del filename. -ELSE echo filename. missing -``` - -如果都放在同一行上,以下子句有效: -```bat -IF EXIST filename. (del filename.) ELSE echo filename. missing -``` -如果命令扩展被启用,`IF` 会如下改变: -```bat -IF [/I] string1 compare-op string2 command -IF CMDEXTVERSION number command -IF DEFINED variable command -``` -其中, `compare-op` 可以是: - -|比较操作|含义| -|:-|:-| -|`EQU`|等于| -|`NEQ`|不等于| -|`LSS`|小于| -|`LEQ`|小于或等于| -|`GTR`|大于| -|`GEQ`|大于或等于| - -- 而 `/I` 开关(如果指定)说明要进行的字符串比较不分大小写。`/I` 开关可以用于 `IF` 的 `string1==string2` 的形式上。这些比较都是通用的;原因是,如果 `string1` 和 `string2` 都是由数字组成的,字符串会被转换成数字,进行数字比较。 -- `CMDEXTVERSION` 条件的作用跟 `ERRORLEVEL` 的一样,除了它是在跟与命令扩展有关联的内部版本号比较。第一个版本是 `1`。每次对命令扩展有相当大的增强时,版本号会增加一个。命令扩展被停用时,`CMDEXTVERSION` 条件不是真的。 -- 如果已定义环境变量,`DEFINED` 条件的作用跟 `EXIST` 的一样,除了它取得一个环境变量,返回的结果是 `true`。 - -如果没有名为 `ERRORLEVEL` 的环境变量,`%ERRORLEVEL%`会扩充为 `ERROLEVEL` 当前数值的字符串表达式;否则,你会得到其(该环境变量的)数值。运行程序后,以下语句说明 `ERRORLEVEL` 的用法: - -```bat -goto answer%ERRORLEVEL% -:answer0 -echo Program had return code 0 -:answer1 -echo Program had return code 1 -``` - -你也可以使用以上的数字比较: - -```bat -IF %ERRORLEVEL% LEQ 1 goto okay -``` - -如果没有名为 `CMDCMDLINE` 的环境变量,`%CMDCMDLINE%`将在 `CMD.EXE` 进行任何处理前扩充为传递给 `CMD.EXE` 的原始命令行;否则,你会得到其(该环境变量的)数值。 - -如果没有名为 `CMDEXTVERSION` 的环境变量,`%CMDEXTVERSION%` 会扩充为 `CMDEXTVERSION` 当前数值的字串符表达式;否则,你会得到其数值。 - - -你可以使用 `%variable%` 去引用`variable`环境变量,用户和系统环境变量有同名时优先用户环境变量。一般来说无论是注册表中配置还是环境变量都是按照层级,越靠近系统顶层优先级越低,用户配置会覆盖系统配置。 - - -例子: -```bat -::test.bat -@echo off -echo %temp% -echo %errorlevel% -echo %cmdextversion% -echo %cmdcmdline% -pause -``` -结果: -``` -C:\Users\CapT\AppData\Local\Temp -0 -2 -C:\Windows\system32\cmd.exe /c ""C:\Users\CapT\Desktop\BatTest\test.bat" " -请按任意键继续. . . -``` -第一个 `%temp%` 是环境变量,通过 `%cmdcmdline%` 就可以知道系统是通过`C:\Windows\system32\cmd.exe`使用`/c`选项将脚本路径字符串作为参数执行批处理脚本的,即执行就退出。 - -### 3.9 for - -对一组文件中的每一个文件执行某个特定命令。 -```bat -FOR %variable IN (set) DO command [command-parameters] -``` -- `%variable` 指定一个单一字母可替换的参数。 -- `(set)` 指定一个或一组文件。可以使用通配符。 -- `command` 指定对每个文件执行的命令。 -- `command-parameters` 为特定命令指定参数或命令行开关。 - -在批处理程序中使用 `FOR` 命令时,指定变量请使用 `%%variable`而不要用 `%variable`。变量名称是区分大小写的,所以 `%i` 不同于 `%I`. - -如果启用命令扩展,则会支持下列 `FOR` 命令的其他格式: -```bat -FOR /D %variable IN (set) DO command [command-parameters] -``` -如果集中包含通配符,则指定与目录名匹配,而不与文件名匹配。 -```bat -FOR /R [[drive:]path] %variable IN (set) DO command [command-parameters] -``` -检查以 `[drive:]path` 为根的目录树,指向每个目录中的 `FOR` 语句。如果在 `/R` 后没有指定目录规范,则使用当前目录。如果集仅为一个单点(`.`)字符,则枚举该目录树。 -```bat -FOR /L %variable IN (start,step,end) DO command [command-parameters] -``` -该集表示以增量形式从开始到结束的一个数字序列。因此,`(1,1,5)`将产生序列`1 2 3 4 5,(5,-1,1)`将产生序列`(5 4 3 2 1)` -```bat -FOR /F ["options"] %variable IN (file-set) DO command [command-parameters] -FOR /F ["options"] %variable IN ("string") DO command [command-parameters] -FOR /F ["options"] %variable IN ('command') DO command [command-parameters] -``` -或者,如果有 `usebackq` 选项: -```bat -FOR /F ["options"] %variable IN (file-set) DO command [command-parameters] -FOR /F ["options"] %variable IN ("string") DO command [command-parameters] -FOR /F ["options"] %variable IN ('command') DO command [command-parameters] -``` -`fileset` 为一个或多个文件名。继续到 `fileset` 中的下一个文件之前,每份文件都被打开、读取并经过处理。处理包括读取文件,将其分成一行行的文字,然后将每行解析成零或更多的符号。然后用已找到的符号字符串变量值调用 `For` 循环。以默认方式,`/F` 通过每个文件的每一行中分开的第一个空白符号。跳过空白行。你可通过指定可选 `"options"` 参数替代默认解析操作。这个带引号的字符串包括一个或多个指定不同解析选项的关键字。这些关键字为: - -- `eol=c` - 指一个行注释字符的结尾(就一个) -- `skip=n` - 指在文件开始时忽略的行数。 -- `delims=xxx` - 指分隔符集。这个替换了空格和制表符的默认分隔符集。 -- `tokens=x,y,m-n` - 指每行的哪一个符号被传递到每个迭代的 for 本身。这会导致额外变量名称的分配。m-n格式为一个范围。通过 nth 符号指定 mth。如果符号字符串中的最后一个字符星号,那么额外的变量将在最后一个符号解析之后分配并接受行的保留文本。 -- `usebackq` - 指定新语法已在下类情况中使用:在作为命令执行一个后引号的字符串并且一个单引号字符为文字字符串命令并允许在 file-set中使用双引号扩起文件名称。 - - 某些范例可能有助: -```bat -FOR /F "eol=; tokens=2,3* delims=, " %i in (myfile.txt) do @echo %i %j %k -``` -- 会分析 `myfile.txt` 中的每一行,忽略以分号打头的那些行,将每行中的第二个和第三个符号传递给 `for` 函数体,用逗号和/或空格分隔符号。请注意,此 `for` 函数体的语句引用 `%i` 来获得第二个符号,引用 `%j` 来获得第三个符号,引用 `%k` 来获得第三个符号后的所有剩余符号。对于带有空格的文件名,你需要用双引号将文件名括起来。为了用这种方式来使用双引号,还需要使用 `usebackq` 选项,否则,双引号会被理解成是用作定义某个要分析的字符串的。 -- `%i` 在 `for` 语句中显式声明,`%j` 和 `%k` 是通过- `tokens=` 选项隐式声明的。可以通过 `tokens=` 一行- 指定最多 26 个符号,只要不试图声明一个高于字母 "z" 或- "Z" 的变量。请记住,`FOR` 变量是单一字母、分大小写和全局的变量;- 而且,不能同时使用超过 52 个。 -- 还可以在相邻字符串上使用 `FOR /F` 分析逻辑,方法是,用单引号将括号之间的 `file-set` 括起来。这样,该字符串会被当作一个文件中的一个单一输入行进行解析。 -- 最后,可以用 `FOR /F` 命令来分析命令的输出。方法是,将括号之间的 `file-set` 变成一个反括字符串。该字符串会被当作命令行,传递到一个子 CMD.EXE,其输出会被捕获到内存中,并被当作文件分析。如以下例子所示: -```bat -FOR /F "usebackq delims==" %i IN (`set`) DO @echo %i -``` -会枚举当前环境中的环境变量名称。 - -另外,`FOR` 变量参照的替换已被增强。你现在可以使用下列 -选项语法: - -|选项|含义| -|:-|:-| -|`%~I ` |删除任何引号("),扩展 `%I`| -|`%~fI` |将 `%I` 扩展到一个完全合格的路径名| -|`%~dI` |仅将 `%I` 扩展到一个驱动器号| -|`%~pI` |仅将 `%I` 扩展到一个路径| -|`%~nI` |仅将 `%I` 扩展到一个文件名| -|`%~xI` |仅将 `%I` 扩展到一个文件扩展名| -|`%~sI` |扩展的路径只含有短名| -|`%~aI` |将 `%I` 扩展到文件的文件属性| -|`%~tI` |将 `%I` 扩展到文件的日期/时间| -|`%~zI` |将 `%I` 扩展到文件的大小| -|`%~$PATH:I` |查找列在路径环境变量的目录,并将 `%I` 扩展到找到的第一个完全合格的名称。如果环境变量名未被定义,或者没有找到文件,此组合键会扩展到空字符串| - -可以组合修饰符来得到多重结果: -|选项|含义| -|:-|:-| -|`%~dpI` |仅将 `%I` 扩展到一个驱动器号和路径| -|`%~nxI` |仅将 `%I` 扩展到一个文件名和扩展名| -|`%~fsI` |仅将 `%I` 扩展到一个带有短名的完整路径名| -|`%~dp$PATH:I` |搜索列在路径环境变量的目录,并将 `%I` 扩展到找到的第一个驱动器号和路径。| -|`%~ftzaI` |将 `%I` 扩展到类似输出线路的 `DIR`| - -在以上例子中,`%I` 和 `PATH` 可用其他有效数值代替。`%~` 语法用一个有效的 `FOR` 变量名终止。选取类似 `%I` 的大写变量名比较易读,而且避免与不分大小写的组合键混淆。 - -### 3.10 start - -启动一个单独的窗口以运行指定的程序或命令。 - -```bat -START ["title"] [/D path] [/I] [/MIN] [/MAX] [/SEPARATE | /SHARED] - [/LOW | /NORMAL | /HIGH | /REALTIME | /ABOVENORMAL | /BELOWNORMAL] - [/NODE ] [/AFFINITY ] [/WAIT] [/B] - [command/program] [parameters] -``` - -|选项|含义| -|:-|:-| -|`"title"`|在窗口标题栏中显示的标题。| -|`path` |启动目录。| -|`B` |启动应用程序,但不创建新窗口。应用程序已忽略 `^C` 处理。除非应用程序启用 `^C` 处理,否则 `^Break` 是唯一可以中断该应用程序的方式。| -|`I` |新的环境将是传递给 `cmd.exe` 的原始环境,而不是当前环境。| -|`MIN` |以最小化方式启动窗口。| -|`MAX` |以最大化方式启动窗口。| -|`SEPARATE` |在单独的内存空间中启动 16 位 Windows 程序。| -|`SHARED` |在共享内存空间中启动 16 位 Windows 程序。| -|`LOW` |在 `IDLE` 优先级类中启动应用程序。| -|`NORMAL` |在 `NORMAL` 优先级类中启动应用程序。| -|`HIGH` |在 `HIGH` 优先级类中启动应用程序。| -|`REALTIME` |在 `REALTIME` 优先级类中启动应用程序。| -|`ABOVENORMAL`|在 `ABOVENORMAL` 优先级类中启动应用程序。| -|`BELOWNORMAL`|在 `BELOWNORMAL` 优先级类中启动应用程序。| -|`NODE` |将首选非一致性内存结构(NUMA)节点指定为十进制整数。| -|`AFFINITY` |将处理器关联掩码指定为十六进制数字。进程被限制在这些处理器上运行。将 `/AFFINITY` 和 `/NODE` 结合使用时,会对关联掩码进行不同的解释。指定关联掩码,以便将零位作为起始位置(就如将 NUMA节点的处理器掩码向右移位一样)。进程被限制在指定关联掩码和 NUMA 节点之间的那些通用处理器上运行。如果没有通用处理器,则进程被限制在指定的 NUMA 节点上运行。 -|`WAIT` |启动应用程序并等待它终止。| -|`command/program`|如果它是内部 `cmd` 命令或批文件,则该命令处理器是使用 `cmd.exe` 的 `/K` 开关运行的。这表示运行该命令之后,该窗口将仍然存在。如果它不是内部 `cmd` 命令或批文件,则它就是一个程序,并将作为一个窗口化应用程序或控制台应用程序运行。| -|`parameters` | 这些是传递给 `command/program` 的参数。| - -注意: 在 64 位平台上不支持 `SEPARATE` 和 `SHARED` 选项。 - ->Windows为进程定义了6个优先级类别:见Windows API中的[GetPriorityClass](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass)。从高到低分别为`REALTIME_PRIORITY_CLASS` `HIGH_PRIORITY_CLASS` `ABOVE_NORMAL_PRIORITY_CLASS` `NORMAL_PRIORITY_CLASS` `BELOW_NORMAL_PRIORITY_CLASS` `IDLE_PRIORITY_CLASS`。分别对应上述的6个关于优先级类的选项。 - - -通过指定 `/NODE`,可按照利用 NUMA 系统中的内存区域的方式创建进程。例如,可以创建两个完全通过共享内存互相通信的进程以共享相同的首选 NUMA 节点,从而最大限度地减少内存延迟。只要有可能,它们就会分配来自相同 NUMA 节点的内存,并且会在指定节点之外的处理器上自由运行。 - -```bat -start /NODE 1 application1.exe -start /NODE 1 application2.exe -``` - -这两个进程可被进一步限制在相同 NUMA 节点内的指定处理器上运行。在以下示例中,application1 在节点的两个低位处理器上运行,而 application2 在该节点的其后两个处理器上运行。该示例假定指定节点至少具有四个逻辑处理器。请注意,节点号可更改为该计算机的任何有效节点号,而无需更改关联掩码。 - -```bat -start /NODE 1 /AFFINITY 0x3 application1.exe -start /NODE 1 /AFFINITY 0xc application2.exe -``` - ->NUMA 全称 Non-Uniform Memory Access,译为“非一致性内存访问”。这种构架下,不同的内存器件和CPU核心从属不同的 Node,每个 Node 都有自己的集成内存控制器(IMC,Integrated Memory Controller)。 -在 Node 内部,架构类似SMP(Symmetrical Multi-Processing,对称多处理),使用 IMC Bus (IMC, Integrated Memory Controller, 集成内存控制器)进行不同核心间的通信;不同的 Node 间通过QPI(Quick Path Interconnect,快速通道互联,是一种由英特尔开发并使用的点对点处理器互联架构,用来实现CPU之间的互联)进行通信。 - ->对称多处理(英语:Symmetric multiprocessing,缩写为 SMP),也译为均衡多处理、对称性多重处理、对称多处理机,是一种多处理器的电脑硬件架构,在对称多处理架构下,每个处理器的地位都是平等的,对资源的使用权限相同。现代多数的多处理器系统,都采用对称多处理架构,也被称为对称多处理系统(Symmetric multiprocessing system)。在这个系统中,拥有超过一个以上的处理器,这些处理器都连接到同一个共享的主存上,并由单一操作系统来控制。在多核心处理器的例子中,对称多处理架构,将每一个核心都当成是独立的处理器。 -在对称多处理系统上,在操作系统的支持下,无论行程是处于用户空间,或是核心空间,都可以分配到任何一个处理器上运行。因此,行程可以在不同的处理器间移动,达到负载平衡,使系统的效率提升。 - - -如果命令扩展被启用,通过命令行或 `START` 命令的外部命令调用会如下改变: - -- 将文件名作为命令键入,非可执行文件可以通过文件关联调用。(例如,`WORD.DOC` 会调用跟 `.DOC` 文件扩展名关联的应用程序)。关于如何从命令脚本内部创建这些关联,请参阅 `ASSOC` 和 `FTYPE` 命令。 -- 执行的应用程序是 32 位 GUI 应用程序时,`CMD.EXE` 不等应用程序终止就返回命令提示符。如果在命令脚本内执行,该新行为则不会发生。 -- 如果执行的命令行的第一个符号是不带扩展名或路径修饰符的字符串 `CMD`,`CMD` 会被 `COMSPEC` 变量的数值所替换。这防止从当前目录提取 `CMD.EXE`。 -- 如果执行的命令行的第一个符号没有扩展名,`CMD.EXE` 会使用 `PATHEXT` 环境变量的数值来决定要以什么顺序寻找哪些扩展名。`PATHEXT` 变量的默认值是`.COM;.EXE;.BAT;.CM`请注意,该语法跟 `PATH` 变量的一样,分号隔开不同的元素。 -- 查找可执行文件时,如果没有相配的扩展名,看一看该名称是否与目录名相配。如果确实如此,`START` 会在那个路径上调用Explorer(文件资源管理器)。如果从命令行执行,则等同于对那个路径作 `CD /D`。 - - ->COMSPEC环境变量指向命令行解释器的exe完整路径,windows中一般来说是`C:\Windows\system32\cmd.exe`,源自DOS操作系统。你可以在系统环境变量中找到它。 ->PATHEXT环境变量,规定在 `Path` 变量中所指定的可执行文件的扩展名有哪些。我的机器上的值是:`.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW`。 -其中不同后缀对应的文件格式为: - -|后缀|文件格式| -|:-|:-| -|`.COM`|DOS的可执行命令文件| -|`.EXE`|Windows可执行文件| -|`.BAT/.CMD`|Windows批处理文件| -|`.VBS/.VBE`|Visual Basic Script / 加密的VBS脚本文件| -|`.JS/.JSE`|JavaScript脚本文件/加密的JavaScript脚本文件| -|`.WSF/.WSH` |Windows脚本文件(Windows Script)。 -|`.MSC`|微软管理控制台MMC(Microsoft Management Console)用来添加/删除的嵌入式管理单元文件。典型如`gpedit.msc` `services.msc`。| -|`.PY/.PYW`|python脚本文件,.py/.pyw的差别是运行程序分别为python.exe/pythonw.exe,可能是安装python是由安装程序添加,Windows系统本身应该不会将.py作为可执行文件。| - -### 3.11 set - -显示、设置或删除 `cmd.exe` 环境变量。 -```bat -SET [variable=[string]] -``` -- `variable` 指定环境变量名。 -- `string` 指定要指派给变量的一系列字符串。 - -要显示当前环境变量,键入不带参数的 `SET`。 - -如果命令扩展被启用,`SET` 会如下改变: - -可仅用一个变量激活 `SET` 命令,等号或值不显示所有前缀匹配 `SET` 命令已使用的名称的所有变量的值。例如: -```bat -SET P -``` -会显示所有以字母 P 打头的变量 - -如果在当前环境中找不到该变量名称,`SET` 命令将把 `ERRORLEVEL`设置成 1。 - -`SET` 命令不允许变量名含有等号。 - -在 `SET` 命令中添加了两个新命令行开关: -```bat -SET /A expression -SET /P variable=[promptString] -``` -`/A` 命令行开关指定等号右边的字符串为被评估的数字表达式。该表达式评估器很简单并以递减的优先权顺序支持下列操作: - -|运算符|含义| -|:-|:-| -|`()`|分组| -|`! ~ -`|一元运算符| -|`* / %`|算数运算符| -|`+ -`|算数运算符| -|`<< >>`|逻辑移位| -|`&`|按位“与”| -|`^`|按位“异或”| -|`\|`|按位“或”| -|`= *= /= %= += -= &= ^= \|= <<= >>=`|赋值| -|`,`|表达式分隔符| - -如果你使用任何逻辑或取余操作符, 你需要将表达式字符串用引号扩起来。在表达式中的任何非数字字符串键作为环境变量名称,这些环境变量名称的值已在使用前转换成数字。如果指定了一个环境变量名称,但未在当前环境中定义,那么值将被定为零。这使你可以使用环境变量值做计算而不用键入那些 `%` 符号来得到它们的值。如果 `SET /A` 在命令脚本外的命令行执行的,那么它显示该表达式的最后值。该分配的操作符在分配的操作符左边需要一个环境变量名称。除十六进制有 `0x` 前缀,八进制有 `0` 前缀的,数字值为十进位数字。因此,`0x12` 与 `18` 和 `022`相同。请注意八进制公式可能很容易搞混: `08` 和 `09` 是无效的数字,因为 `8` 和 `9` 不是有效的八进制位数。(`&`) - -`/P` 命令行开关允许将变量数值设成用户输入的一行输入。读取输入行之前,显示指定的 `promptString`。`promptString` 可以是空的。 - -环境变量替换已如下增强: - -```bat -%PATH:str1=str2% -``` -会扩展 `PATH` 环境变量,用 "str2" 代替扩展结果中的每个 "str1"。要有效地从扩展结果中删除所有的 "str1","str2" 可以是空的。"str1" 可以以星号打头;在这种情况下,"str1" 会从扩展结果的开始到 str1 剩余部分第一次出现的地方,都一直保持相配。 - -也可以为扩展名指定子字符串。 -```bat -%PATH:~10,5% -``` -会扩展 `PATH` 环境变量,然后只使用在扩展结果中从第 11 个(偏移量 10)字符开始的五个字符。如果没有指定长度,则采用默认值,即变量数值的余数。如果两个数字(偏移量和长度)都是负数,使用的数字则是环境变量数值长度加上指定的偏移量或长度。 -```bat -%PATH:~-10% -``` -会提取 `PATH` 变量的最后十个字符。 -```bat -%PATH:~0,-2% -``` -会提取 `PATH` 变量的所有字符,除了最后两个。 - -终于添加了延迟环境变量扩充的支持。该支持总是按默认值被停用,但也可以通过 `CMD.EXE` 的 `/V` 命令行开关而被启用/停用。请参阅 `CMD /?`。 - -考虑到读取一行文本时所遇到的目前扩充的限制时,延迟环境变量扩充是很有用的,而不是执行的时候。以下例子说明直接变量扩充的问题: -```bat -set VAR=before -if "%VAR%" == "before" ( - set VAR=after - if "%VAR%" == "after" @echo If you see this, it worked -) -``` -不会显示消息,因为在读到第一个 `IF` 语句时,两个 `IF` 语句中的 `%VAR%` 会被代替;原因是: 它包含 `IF` 的文体,`IF` 是一个复合语句。所以,复合语句中的 `IF` 实际上是在比较 "before" 和"after",这两者永远不会相等。同样,以下这个例子也不会达到预期效果: - -```bat -set LIST= -for %i in (*) do set LIST=%LIST% %i -echo %LIST% -``` -原因是,它不会在目前的目录中建立一个文件列表,而只是将 `LIST` 变量设成找到的最后一个文件。这也是因为 `%LIST%` 在 `FOR` 语句被读取时,**只被扩充了一次**;而且,那时的 `LIST` 变量是空的。因此,我们真正执行的 FOR 循环是: - -```bat -for %i in (*) do set LIST= %i -``` -这个循环继续将 `LIST` 设成找到的最后一个文件。 - -延迟环境变量扩充允许你使用一个不同的字符(惊叹号)在执行时间扩充环境变量。如果延迟的变量扩充被启用,可以将上面例子写成以下所示,以达到预期效果: - -```bat -set VAR=before -if "%VAR%" == "before" ( - set VAR=after - if "!VAR!" == "after" @echo If you see this, it worked -) -``` -```bat -set LIST= -for %i in (*) do set LIST=!LIST! %i -echo %LIST% -``` -如果命令扩展被启用,有几个动态环境变量可以被扩展,但不会出现在 SET 显示的变量列表中。每次变量数值被扩展时,这些变量数值都会被动态计算。如果用户用这些名称中任何一个明确定义变量,那个定义会替代下面描述的动态定义: - -- `%CD% `- 扩展到当前目录字符串。 -- `%DATE% `- 用跟 DATE 命令同样的格式扩展到当前日期。 -- `%TIME% `- 用跟 TIME 命令同样的格式扩展到当前时间。 -- `%RANDOM%` - 扩展到 0 和 32767 之间的任意十进制数字。 -- `%ERRORLEVEL%` - 扩展到当前 ERRORLEVEL 数值。 -- `%CMDEXTVERSION%` - 扩展到当前命令处理器扩展版本号。 -- `%CMDCMDLINE%`- 扩展到调用命令处理器的原始命令行。 -- `%HIGHESTNUMANODENUMBER%` - 扩展到此计算机上的最高 NUMA 节点号。 - -用`set`定义的环境变量是临时的,通常用`set`来定义变量,定义的变量在在当前`cmd`进程中可见。任何局部的变量在调用`endlocal`、`exit`或者执行到达文件的末尾都会恢复。下列示例中第一个`%size%`识别,第二个为空。 -```bat -@echo off -setlocal -set size=10 -echo %size% -endlocal -echo %size% -``` -当然也可以在脚本执行环境中去覆盖系统环境变量,比如`path`。 - -### 3.12 重定向 > & >> - -输出重定向命令: -- `>` 清除文件中原有的内容后再写入。 -- `>>` 追加内容到文件末尾,而不会清除原有的内容。 -- `2>` 清除文件内容,并将命令执行的错误输出重定向到文件。 -- `2>>` 追加命令执行的错误输出到文件末尾。 - -主要将本来显示在屏幕上的内容输出到指定文件中,指定文件如果不存在,则自动生成该文件。注意`>`和`>>`只有在命令执行成功时才会将结果重定向,如果要重定向执行失败的提示需要使用`2>`和`2>>`。和linux的重定向基本上是一致的。要输出`>`本身需要用`^>`转义,`echo ^>`。 - -输入重定向: -- `<` 从文件获取内容而不是屏幕上。 - ->三个标准文件(或者叫标准流):使用数字0、1、2分别表示标准输入(stdin)、标准输出(stdout)、标准错误(stderr),C语言中定义在头文件``中。 - -默认的`>`等同与`1>`,`>>`等同于`1>>`。 - -### 3.13 管道 | - -指将一个命令的执行结果输出给另一个命令作为输入。语法是`cmd1 | cmd2`。 -```bat -dir | find "test" -``` -结果与下列执行结果相同: -```bat -dir > test.txt -find "test" < test.txt -``` - -### 3.14 & && || - -- `&` 顺序执行多条命令,而不管命令是否执行成功。 -- `&&` 顺序执行多条命令,当碰到执行出错的命令后将不执行后面的命令。 -- `||` 顺序执行多条命令,当碰到执行正确的命令后将不执行后面的命令。 - -```bat -find "hello" hello.txt & echo execute find -find "hello" hello.txt && echo success -find "hello" hello.txt || echo failed -``` - -### 3.15 setlocal endlocal - -开始批处理文件中环境改动的本地化操作。在执行 `SETLOCAL` 之后所做的环境改动只限于批处理文件。要还原原先的设置,必须执行 `ENDLOCAL`。达到批处理文件结尾时,对于该批处理文件的每个尚未执行的 `SETLOCAL` 命令,都会有一个隐含的 `ENDLOCAL` 被执行。 - -```bat -SETLOCAL -``` -如果启用命令扩展,则 `SETLOCAL` 更改如下: - -`SETLOCAL` 批命令现在可以接受可选参数: -- `ENABLEEXTENSIONS / DISABLEEXTENSIONS` 启用或禁用命令处理器扩展。这些 参数比 `CMD /E:ON` 或 `/E:OFF` 开关有优先权。请参阅 `CMD /?` 获取详细信息。 -- `ENABLEDELAYEDEXPANSION / DISABLEDELAYEDEXPANSION` 启用或禁用延缓环境变量 扩展。这些参数比 `CMD /V:ON` 或 `/V:OFF` 开关有优先权。请参阅 `CMD /?` 获取详细信息。 - -无论在 `SETLOCAL` 命令之前的设置是什么,这些修改会一直生效,直到出现相应的 `ENDLOCAL` 命令。 - -在给定参数的情况下,`SETLOCAL` 命令将设置 `ERRORLEVEL` 值。如果给定两个有效参数中的一个,另一个未给定,则该值为零。 - -通过以下方法,你可以在批脚本中使用此项来确定扩展是否可用: -```bat -VERIFY OTHER 2>nul -SETLOCAL ENABLEEXTENSIONS -IF ERRORLEVEL 1 echo Unable to enable extensions -``` -此方法之所以有效,是因为在 `CMD.EXE` 的旧版本上,`SETLOCAL` 不设置 `ERRORLEVEL` 值。如果参数不正确,`VERIFY` 命令会将 `ERRORLEVEL` 值初始化为非零值。经过新版本上也不设置?那为什么要强调旧版本呢? - -## 4. 环境变量详解 - -主要的Windows系统/用户环境变量: - -- `ComSpec` : 命令行解释器可执行文件位置。 -- `NUMBER_OF_PROCESSORS` :用户电脑中处理器数量,Intel超线程的处理器那应该是逻辑处理器数量,并非物理核心数量。 -- `OS` :用户操作系统,我的Win10是`Windows_NT`。大概操作系统的另一个名称,win10对应版本是Windows NT 10.0。NT指New Technology。 -- `Path` :规定操作系统在指定的文件路径中查看可执行文件。通常安装一个软件之后需要它能够在命令行执行那么就可执行文件所在目录加到`Path`环境变量中,这应该是程序员最熟悉的环境变量。 -- `PathExt` :规定在 Path 变量中所指定的可执行文件的扩展名有哪些。 -- `PROCESSOR_ARCHITECTURE` :用户处理器架构。64位Intel Core处理器结果是AMD64。 -- `PROCESSOR_IDENTIFIER` :用户处理器。我的是Intel64 Family 6 Model 94 Stepping 3, GenuineIntel。 -- `PROCESSOR_LEVEL` :表明用户处理器的等级。我的是6。 -- `PROCESSOR_REVISION` :表明用户处理器的版本。我的是5e03。 -- `TEMP/TMP`:系统/用户临时目录。一般软件使用过程中产生的临时文件都存储用户临时目录,用户临时目录一般是:`C:\Users\username\AppData\Local\Temp`,软件编写过程中也经常会获取`%temp%`系统变量用来生成临时文件。系统临时目录是:`C:\Windows\TEMP`。 -- `windir` :规定操作系统的系统目录的路径。一般来说是:`C:\Windows`。 - -还有一些在环境变量的列表里面找不到,在`set`列表中能够看到,估计是写在注册表里面的: -- `SystemDrive` :系统驱动器,一般都是C盘。 -- `systemroot` :`C:\Windows`。 -- `programfiles` :`C:\Program Files`,一般来说第三方软件会默认安装在这个目录或者另一个带(X86)的目录。 -- `appdata` :用户数据目录,第三方软件的配置信息一般存放在这个目录中,一般是`C:\Users\username\AppData\Roaming`。 -- `userprofile` :当前用户的用户目录,默认是`C:\Users\username`,可以通过修改注册表修改。 -- `ALLUSERSPROFILE` : 所有用户共享的软件配置,`C:\ProgramData`。 -- 还有一些与用户名、机器名、其他目录等。 - -一般第三方软件安装时可能会修改`path`环境变量,添加自己的可执行文件目录,一些还可能会添加新的环境变量。如果没有从命令行执行的需求的话,一般没有必要修改。 - -Win10更新一次,UI就会变一次,当前20H2版本的环境变量设置方法为:电脑,右键属性,高级系统设置,环境变量。 -- 系统环境变量注册表位置:`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\Environment\`。 -- 用户环境变量注册表位置:`HKEY_CURRENT_USER\Environment`。 - -一些国产第三方软件会默认安装在`%appdata%`来防止UAC(User Account Control)检查,基本属于耍流氓的行为。安装软件最好安装在`%programfiles%`中,这样软件暗地里做了什么事情会通过UAC告诉你,当然如果你的UAC是从不通知,那当我没有说。当然也看情况,为单用户安装一般也会装在`%appdata%`中,我的建议都是为所有用户安装。 - -## 5. 常用Cmd命令 - -有关某个命令的详细信息,请键入 `HELP` 命令名 -- `ASSOC ` 显示或修改文件扩展名关联。 -- `ATTRIB ` 显示或更改文件属性。 -- `BREAK ` 设置或清除扩展式 CTRL+C 检查。 -- `BCDEDIT ` 设置启动数据库中的属性以控制启动加载。 -- `CACLS ` 显示或修改文件的访问控制列表(ACL)。 -- `CALL ` 从另一个批处理程序调用这一个。 -- `CD ` 显示当前目录的名称或将其更改。 -- `CHCP ` 显示或设置活动代码页数。 -- `CHDIR ` 显示当前目录的名称或将其更改。 -- `CHKDSK ` 检查磁盘并显示状态报告。 -- `CHKNTFS ` 显示或修改启动时间磁盘检查。 -- `CLS ` 清除屏幕。 -- `CMD ` 打开另一个 Windows 命令解释程序窗口。 -- `COLOR ` 设置默认控制台前景和背景颜色。 -- `COMP ` 比较两个或两套文件的内容。 -- `COMPACT ` 显示或更改 NTFS 分区上文件的压缩。 -- `CONVERT ` 将 FAT 卷转换成 NTFS。你不能转换当前驱动器。 -- `COPY ` 将至少一个文件复制到另一个位置。 -- `DATE ` 显示或设置日期。 -- `DEL ` 删除至少一个文件。 -- `DIR ` 显示一个目录中的文件和子目录。 -- `DISKPART ` 显示或配置磁盘分区属性。 -- `DOSKEY ` 编辑命令行、撤回 Windows 命令并创建宏。 -- `DRIVERQUERY` 显示当前设备驱动程序状态和属性。 -- `ECHO ` 显示消息,或将命令回显打开或关闭。 -- `ENDLOCAL ` 结束批文件中环境更改的本地化。 -- `ERASE ` 删除一个或多个文件。 -- `EXIT ` 退出 CMD.EXE 程序(命令解释程序)。 -- `FC ` 比较两个文件或两个文件集并显示它们之间的不同。 -- `FIND ` 在一个或多个文件中搜索一个文本字符串。 -- `FINDSTR ` 在多个文件中搜索字符串。 -- `FOR ` 为一组文件中的每个文件运行一个指定的命令。 -- `FORMAT ` 格式化磁盘,以便用于 Windows。 -- `FSUTIL ` 显示或配置文件系统属性。 -- `FTYPE ` 显示或修改在文件扩展名关联中使用的文件类型。 -- `GOTO ` 将 Windows 命令解释程序定向到批处理程序中某个带标签的行。 -- `GPRESULT ` 显示计算机或用户的组策略信息。 -- `GRAFTABL ` 使 Windows 在图形模式下显示扩展字符集。 -- `HELP ` 提供 Windows 命令的帮助信息。 -- `ICACLS ` 显示、修改、备份或还原文件和目录的 ACL。 -- `IF ` 在批处理程序中执行有条件的处理操作。 -- `LABEL ` 创建、更改或删除磁盘的卷标。 -- `MD ` 创建一个目录。 -- `MKDIR ` 创建一个目录。 -- `MKLINK ` 创建符号链接和硬链接 -- `MODE ` 配置系统设备。 -- `MORE ` 逐屏显示输出。 -- `MOVE ` 将一个或多个文件从一个目录移动到另一个目录。 -- `OPENFILES ` 显示远程用户为了文件共享而打开的文件。 -- `PATH ` 为可执行文件显示或设置搜索路径。 -- `PAUSE ` 暂停批处理文件的处理并显示消息。 -- `POPD ` 还原通过 PUSHD 保存的当前目录的上一个值。 -- `PRINT ` 打印一个文本文件。 -- `PROMPT ` 更改 Windows 命令提示。 -- `PUSHD ` 保存当前目录,然后对其进行更改。 -- `RD ` 删除目录。 -- `RECOVER ` 从损坏的或有缺陷的磁盘中恢复可读信息。 -- `REM ` 记录批处理文件或 CONFIG.SYS 中的注释(批注)。 -- `REN ` 重命名文件。 -- `RENAME ` 重命名文件。 -- `REPLACE ` 替换文件。 -- `RMDIR ` 删除目录。 -- `ROBOCOPY ` 复制文件和目录树的高级实用工具 -- `SET ` 显示、设置或删除 Windows 环境变量。 -- `SETLOCAL ` 开始本地化批处理文件中的环境更改。 -- `SC ` 显示或配置服务(后台进程)。 -- `SCHTASKS ` 安排在一台计算机上运行命令和程序。 -- `SHIFT ` 调整批处理文件中可替换参数的位置。 -- `SHUTDOWN ` 允许通过本地或远程方式正确关闭计算机。 -- `SORT ` 对输入排序。 -- `START ` 启动单独的窗口以运行指定的程序或命令。 -- `SUBST ` 将路径与驱动器号关联。 -- `SYSTEMINFO ` 显示计算机的特定属性和配置。 -- `TASKLIST ` 显示包括服务在内的所有当前运行的任务。 -- `TASKKILL ` 中止或停止正在运行的进程或应用程序。 -- `TIME ` 显示或设置系统时间。 -- `TITLE ` 设置 CMD.EXE 会话的窗口标题。 -- `TREE ` 以图形方式显示驱动程序或路径的目录结构。 -- `TYPE ` 显示文本文件的内容。 -- `VER ` 显示 Windows 的版本。 -- `VERIFY ` 告诉 Windows 是否进行验证,以确保文件正确写入磁盘。 -- `VOL ` 显示磁盘卷标和序列号。 -- `XCOPY ` 复制文件和目录树。 -- `WMIC ` 在交互式命令 shell 中显示 WMI 信息。 - -大部分目录文件磁盘操作、也有一些系统设置进程之类的操作,这些操作应该都能找到相关的UI操作,用到时去查就行,没必要折磨自己。知道常用的就行,某些磁盘操作命令可能是危险的,请注意数据安全,确保知道自己在做什么。 - -批处理也大多用在需要批量处理文件目录操作的场景中,具体怎么写还得通过实践总结。 - -## 6. 脚本编写技巧 - -脚本编写实践技巧: -- 带空格的文件路径参数传进来时需要用`""`括起来,`%~1`可以去掉引号`""`。 -- 通过`%~dp0`得到当前执行的脚本所在的目录,`%~nx0`得到脚本完全名称。 -- 默认是启用扩展的,也可以用`setlocal ENABLEEXTENSIONS/DISABLEEXTENSIONS`来覆盖默认设置。 -- `echo` `if` `set`将保留`%errorlevel%`现有值,通常通过正确编码用`NEQ`来判断`%errorlevel%`而不是类似于`if errorlevel 1`这种语法。而具体返回什么编码通常通过实验得到,有的时候会返回`9009`,这并不是完美的解决方案,但能解决问题。 -- 程序执行失败就退出可以用`a.exe || exit /B 1`退出脚本不退出cmd,或者用`a.exe || goto :EOF`。 -- 好的编写习惯:坚持使用0作为脚本成功执行返回值,正数作为失败返回。可以在脚本中定义错误码增加可读性,最好从1开始向上递增。 -- 退出脚本时返回一个值:`exit /B retCode`,如果需要组合多个错误状态信息,也可以按位表示错误码,按位与到一起来返回。 -- 输出重定向时可以在末尾附加`1>&2`将`stdout`当做`stderr`来处理,`2>&1`将`stderr`当做`stdout`处理。 -- `NUL`是一个虚拟的设备(文件),将`stdout`重定向到`NUL`,则会丢弃标准输出。可以用`2>NUL`屏蔽错误输出。 -- 使用`CON`将命令行自己的输入重定向到一个文件,输入完成后只需要Ctrl+C发送一个EOF即可结束编辑:`TYPE CON > output.txt`。 -- 新建一个空文本:`TYPE NUL > w.txt`。 -- `if`中可以在在判断的表达式两边加上双引号,这样可以避免一些bug,比如变量不存在,导致语法错误。 -- 通过`:label`标签配合`call :label`可以实现函数调用,函数结束时应该使用`got :EOF`或者`eixt /B [errorCode]`。 -- 善用基于成功失败的条件执行:`||` `&&`。 - - -## 7. 批处理脚本实例 - -### 7.1 文件路径相关 - -得到输入文件的完整路径:`test.bat ..\hello.c` -```bat -::test.bat -@echo off -echo %~f1 -``` -遍历输入目录所有文件: -```bat -::traverse.bat -@echo off -if exist %1 ( - for /R %1 %%v in (*) do echo %%v -) -exit /B 0 -``` -遍历删除所有当前目录中所有子目录下的`overlays`目录: -```bat -@echo off -set parent=%~dp0 -for /D %%I in (%parent%*) do rd /s /q %%I\overlays -exit /B 0 -``` - -### 7.2 用户交互 - -让用户选择是否继续执行: -```bat -@ECHO OFF -SET /P confirm="Continue [y/n]>" -ECHO %confirm% | FINDSTR /I "n" > NUL && exit /B 1 -echo continue to execute! -::more command -``` - -### 7.3 区分命令行执行和双击执行 - -根据当前`%comspec%`是否与`%cmdcmdline%`相同来判断是否命令行交互中执行脚本,如果是双击执行则在末尾`pause`方便查看。 -```bat -@echo off -setlocal -set interactive=0 -echo %comspec% | findstr /L %cmdcmdline% >NUL 2>&1 -if %errorlevel% == 0 set interactive=1 - -echo do work - -if "%interactive%"=="0" pause -endlocal -exit /B 0 -``` -通过搜索`%cmdcmdline%`中是否包含`%comspec%`的字符串来完成,如果是找到了`cmd.exe`文件,双击打开的命令行那么还是会理解为非交互执行,有BUG,但也只能做到这种程度了。 - -## 8. 我的评价 - -我的主观看法: -- 语法晦涩、简陋、不清晰,没有类型系统,写起来吃力。 -- 源自DOS,有点过时了,很多语法很原始。 -- 通过调用命令来组合功能,命令都很过时了,费劲去查帮助理解命令逻辑真的不明智。 -- 如果需要实现的脚本逻辑有一点点复杂,那么选择`python`这种第三方脚本语言可能会降低很多心智负担,也会使脚本严谨很多。 -- 总体来看,了解了个寂寞,用途仅限于读懂现有的可能遇到的脚本吧! - -## 9. Windows管理深入 - -### 9.1 注册表 - -关于注册表,就不赘述了,见[Windows注册表内容](https://zhuanlan.zhihu.com/p/72194354)。 - -注册表是windows操作系统、硬件设备以及客户应用程序得以正常运行和保存设置的核心“数据库”,是一个非常巨大的树状分层结构的数据库系统。执行`regedit`可以打开注册表编辑器,方便查看和修改注册表项。 - -编写Windows客户端程序通常需要将一部分配置存储到注册表中,一般是在`HKEY_LOCAL_MACHINE\SOFTWARE`和`HKEY_CURRENT_USER\SOFTWARE`下存放软件的系统配置和用户配置。除此之外可能还会需要注册COM类等等操作会在其他位置增加注册表项。一般来说在安装时添加软件相关注册表项,卸载删除软件相关的注册表项,也有些软件卸载时不会删除,下次安装时就可以恢复配置。某些软件卸载时注册表未删干净导致卸载后再次安装安装不上的情况并不罕见,需要视相应的第三方软件来讨论。 - -Windows API中提供了[增删改查注册表项的接口](https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry),可以将注册表项导出为`.reg`文件,或者将要添加的注册表项写在`.reg`文件中,双击直接导入。 - - -### 9.2 组策略 - -Windows操作系统的组策略是配置计算机中某一些用户组策略的程序,是系统管理员操作控制计算机程序、访问网络资源、操作行为、各种软件设置的最主要工具。 - -电脑的管理员可以通过组策略进行诸如禁止运行指定程序、锁定注册表编辑器、阻止访问命令提示符、禁止修改系统还原配置、修改用户组密码等等操作。 - -组策略设置就是在修改注册表中的配置。组策略使用了更完善的管理组织方法,可以对各种对象中的设置进行管理和配置,远比手工修改注册表方便、灵活,功能也更加强大。 - -Win+R,`gpedit.msc`打开组策略编辑器,具体的策略管理暂时也用不到,以后实践中总结。 \ No newline at end of file diff --git a/Boost.md b/Boost.md deleted file mode 100644 index 4761bad..0000000 --- a/Boost.md +++ /dev/null @@ -1,114 +0,0 @@ -# 使用Boost - -首先,入门指南:[Boost Getting Started Guide](https://www.boost.org/doc/libs/1_79_0/more/getting_started/index.html) - -## 入门 - -### Boost发行版 - -下载压缩包,解压。得到目录`boost_1_79_0/`,文件结构: -``` -boost_1_79_0\ .................The “boost root directory” - index.htm .........A copy of www.boost.org starts here - boost\ .........................All Boost Header files - lib\ .....................precompiled library binaries - libs\ ............Tests, .cpps, docs, etc., by library - index.html ........Library documentation starts here - algorithm\ - any\ - array\ - …more libraries… - status\ .........................Boost-wide test suite - tools\ ...........Utilities, e.g. Boost.Build, quickbook, bcp - more\ ..........................Policy documents, etc. - doc\ ...............A subset of all Boost library docs -``` - -重点: -- 文档中通常会用`$BOOST_ROOT`来表示这个boost根目录。 -- 为了使用boost,boost的根目录应该在包含路径中。boost的所有头文件都在其下的`boost/`目录中,为`.hpp`后缀的文件。 -- 然后使用时要包含boost头文件时就像这样: -```C++ -#include -#include "boost/whatever.hpp" -``` -- 根目录中的`doc/`仅包含了所有文档的一个子集。 - -### Header-only库 - -大部分的Boost库都是仅有头文件,其中包含模板和内联函数定义,不需要链接任何单独编译的二进制。 - -仅有的必须单独编译的Boost库列表: -- Boost.Chrono -- Boost.Context -- Boost.Filesystem -- Boost.GraphParallel -- Boost.IOStreams -- Boost.Locale -- Boost.Log (see build documentation) -- Boost.MPI -- Boost.ProgramOptions -- Boost.Python (see the Boost.Python build documentation before building and installing it) -- Boost.Regex -- Boost.Serialization -- Boost.Thread -- Boost.Timer -- Boost.Wave - -一些库有可选的单独编译的二进制: -- Boost.Graph also has a binary component that is only needed if you intend to parse GraphViz files. -- Boost.Math has binary components for the TR1 and C99 cmath functions. -- Boost.Random has a binary component which is only needed if you're using random_device. -- Boost.Test can be used in “header-only” or “separately compiled” mode, although separate compilation is recommended for serious use. -- Boost.Exception provides non-intrusive implementation of exception_ptr for 32-bit _MSC_VER==1310 and _MSC_VER==1400 which requires a separately-compiled binary. This is enabled by #define BOOST_ENABLE_NON_INTRUSIVE_EXCEPTION_PTR. -- Boost.System is header-only since Boost 1.69. A stub library is still built for compatibility, but linking to it is no longer necessary. - -### 使用Boost构建一个简单的程序 - -简单起见,首先来使用以下Header-only的Boost库:以下代码从标准输入读取一系列整数,将每个数乘以三之后,写到标准输出。 -```C++ -#include -#include -#include -#include - -int main() -{ - using namespace boost::lambda; - typedef std::istream_iterator in; - - std::for_each( - in(std::cin), in(), std::cout << (_1 * 3) << " " ); -} -``` -- 编译,运行(记住添加Boost根目录到包含路径,我的是`C:\LibsCpp\boost_1_79_0`): -``` -g++ -IC:\LibsCpp\boost_1_79_0 hello.cpp -o hello -``` -- 写一个简单的`Makefile`: -```Makefile -CXX = g++ -CXXFLAGS += -IC:\\LibsCpp\\boost_1_79_0 - -% : %.cpp - $(CXX) $(CXXFLAGS) $^ -o $@ -``` -- 即可一步编译: -```shell -make hello -``` -- 在VsCode中工作时,配置`c_cpp_properties.json`,将该目录添加到包含目录以提供智能提示支持。 -- IDE中比如VS就不多赘述了,添加了包含目录即可。 - -- 在Boost头文件中见到警告在很多编译器中是很正常的事情。确保不是你的代码报警告即可。 - -### 使用Boost的二进制库 - -- 需要使用Boost的b2构建工具构建从源代码需要的二进制,细节可以去看开头的链接中的文档。 -- 或者直接下载[Windows中MSVC工具链下的Boost预编译二进制](https://sourceforge.net/projects/boost/files/boost-binaries/)。 - -最后: -- [B2 User Manual](https://www.boost.org/doc/libs/1_79_0/tools/build/doc/html/index.html#_introduction) -- [Index of all Boost library documentation](https://www.boost.org/doc/libs/1_79_0/libs/libraries.htm) - -细节学习TODO。 \ No newline at end of file diff --git a/C++20Programming.md b/C++20Programming.md deleted file mode 100644 index a2c07ee..0000000 --- a/C++20Programming.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++20高级编程 - -《[C++20高级编程](https://book.douban.com/subject/35948110/)》学习与代码记录,见[tch0/Cpp20Programming](https://github.com/tch0/Cpp20Programming)。 \ No newline at end of file diff --git a/C++CodingStandards.md b/C++CodingStandards.md deleted file mode 100644 index 6639929..0000000 --- a/C++CodingStandards.md +++ /dev/null @@ -1,895 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [C++编程规范:101条规则、准则与最佳实践](#c%E7%BC%96%E7%A8%8B%E8%A7%84%E8%8C%83101%E6%9D%A1%E8%A7%84%E5%88%99%E5%87%86%E5%88%99%E4%B8%8E%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5) - - [组织与策略问题](#%E7%BB%84%E7%BB%87%E4%B8%8E%E7%AD%96%E7%95%A5%E9%97%AE%E9%A2%98) - - [0. 不要拘泥与小节/了解哪些东西该标准化](#0-%E4%B8%8D%E8%A6%81%E6%8B%98%E6%B3%A5%E4%B8%8E%E5%B0%8F%E8%8A%82%E4%BA%86%E8%A7%A3%E5%93%AA%E4%BA%9B%E4%B8%9C%E8%A5%BF%E8%AF%A5%E6%A0%87%E5%87%86%E5%8C%96) - - [1. 在高警告级别干净利落地编程](#1-%E5%9C%A8%E9%AB%98%E8%AD%A6%E5%91%8A%E7%BA%A7%E5%88%AB%E5%B9%B2%E5%87%80%E5%88%A9%E8%90%BD%E5%9C%B0%E7%BC%96%E7%A8%8B) - - [2. 使用自动构建系统](#2-%E4%BD%BF%E7%94%A8%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BA%E7%B3%BB%E7%BB%9F) - - [3. 使用版本控制系统](#3-%E4%BD%BF%E7%94%A8%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E7%B3%BB%E7%BB%9F) - - [4. 做代码审查](#4-%E5%81%9A%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5) - - [设计风格](#%E8%AE%BE%E8%AE%A1%E9%A3%8E%E6%A0%BC) - - [5. 一个实体应该只有一个紧凑的职责](#5-%E4%B8%80%E4%B8%AA%E5%AE%9E%E4%BD%93%E5%BA%94%E8%AF%A5%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E7%B4%A7%E5%87%91%E7%9A%84%E8%81%8C%E8%B4%A3) - - [6. 正确、简单和清晰第一](#6-%E6%AD%A3%E7%A1%AE%E7%AE%80%E5%8D%95%E5%92%8C%E6%B8%85%E6%99%B0%E7%AC%AC%E4%B8%80) - - [7. 编程中应知道何时与如何考虑可伸缩性](#7-%E7%BC%96%E7%A8%8B%E4%B8%AD%E5%BA%94%E7%9F%A5%E9%81%93%E4%BD%95%E6%97%B6%E4%B8%8E%E5%A6%82%E4%BD%95%E8%80%83%E8%99%91%E5%8F%AF%E4%BC%B8%E7%BC%A9%E6%80%A7) - - [8. 不要进行不成熟的优化](#8-%E4%B8%8D%E8%A6%81%E8%BF%9B%E8%A1%8C%E4%B8%8D%E6%88%90%E7%86%9F%E7%9A%84%E4%BC%98%E5%8C%96) - - [9. 不要进行不成熟的劣化](#9-%E4%B8%8D%E8%A6%81%E8%BF%9B%E8%A1%8C%E4%B8%8D%E6%88%90%E7%86%9F%E7%9A%84%E5%8A%A3%E5%8C%96) - - [10. 尽量减少全局和共享数据](#10-%E5%B0%BD%E9%87%8F%E5%87%8F%E5%B0%91%E5%85%A8%E5%B1%80%E5%92%8C%E5%85%B1%E4%BA%AB%E6%95%B0%E6%8D%AE) - - [11. 隐藏信息](#11-%E9%9A%90%E8%97%8F%E4%BF%A1%E6%81%AF) - - [12. 懂得如何和何时进行并发编程](#12-%E6%87%82%E5%BE%97%E5%A6%82%E4%BD%95%E5%92%8C%E4%BD%95%E6%97%B6%E8%BF%9B%E8%A1%8C%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B) - - [13. 确保资源为对象所拥有,使用显式的RAII和智能指针](#13-%E7%A1%AE%E4%BF%9D%E8%B5%84%E6%BA%90%E4%B8%BA%E5%AF%B9%E8%B1%A1%E6%89%80%E6%8B%A5%E6%9C%89%E4%BD%BF%E7%94%A8%E6%98%BE%E5%BC%8F%E7%9A%84raii%E5%92%8C%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88) - - [编程风格](#%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC) - - [14. 宁可编译时链接时错误,也不要运行时错误](#14-%E5%AE%81%E5%8F%AF%E7%BC%96%E8%AF%91%E6%97%B6%E9%93%BE%E6%8E%A5%E6%97%B6%E9%94%99%E8%AF%AF%E4%B9%9F%E4%B8%8D%E8%A6%81%E8%BF%90%E8%A1%8C%E6%97%B6%E9%94%99%E8%AF%AF) - - [15. 积极使用const](#15-%E7%A7%AF%E6%9E%81%E4%BD%BF%E7%94%A8const) - - [16. 避免使用宏](#16-%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E5%AE%8F) - - [17. 避免使用魔法数字](#17-%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E9%AD%94%E6%B3%95%E6%95%B0%E5%AD%97) - - [18. 尽可能局部地声明变量](#18-%E5%B0%BD%E5%8F%AF%E8%83%BD%E5%B1%80%E9%83%A8%E5%9C%B0%E5%A3%B0%E6%98%8E%E5%8F%98%E9%87%8F) - - [19. 总是初始化变量](#19-%E6%80%BB%E6%98%AF%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%98%E9%87%8F) - - [20. 避免函数过长,避免嵌套过深](#20-%E9%81%BF%E5%85%8D%E5%87%BD%E6%95%B0%E8%BF%87%E9%95%BF%E9%81%BF%E5%85%8D%E5%B5%8C%E5%A5%97%E8%BF%87%E6%B7%B1) - - [21. 避免跨编译单元的初始化依赖](#21-%E9%81%BF%E5%85%8D%E8%B7%A8%E7%BC%96%E8%AF%91%E5%8D%95%E5%85%83%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BE%9D%E8%B5%96) - - [22. 尽量减少定义性依赖,避免循环依赖](#22-%E5%B0%BD%E9%87%8F%E5%87%8F%E5%B0%91%E5%AE%9A%E4%B9%89%E6%80%A7%E4%BE%9D%E8%B5%96%E9%81%BF%E5%85%8D%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96) - - [23. 头文件应该自给自足](#23-%E5%A4%B4%E6%96%87%E4%BB%B6%E5%BA%94%E8%AF%A5%E8%87%AA%E7%BB%99%E8%87%AA%E8%B6%B3) - - [24. 总是编写内部包含守卫,不要编写外部包含守卫](#24-%E6%80%BB%E6%98%AF%E7%BC%96%E5%86%99%E5%86%85%E9%83%A8%E5%8C%85%E5%90%AB%E5%AE%88%E5%8D%AB%E4%B8%8D%E8%A6%81%E7%BC%96%E5%86%99%E5%A4%96%E9%83%A8%E5%8C%85%E5%90%AB%E5%AE%88%E5%8D%AB) - - [函数与操作符](#%E5%87%BD%E6%95%B0%E4%B8%8E%E6%93%8D%E4%BD%9C%E7%AC%A6) - - [25. 正确选择通过值、引用还是(智能)指针传递参数](#25-%E6%AD%A3%E7%A1%AE%E9%80%89%E6%8B%A9%E9%80%9A%E8%BF%87%E5%80%BC%E5%BC%95%E7%94%A8%E8%BF%98%E6%98%AF%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88%E4%BC%A0%E9%80%92%E5%8F%82%E6%95%B0) - - [26. 保持重载操作符的自然语义](#26-%E4%BF%9D%E6%8C%81%E9%87%8D%E8%BD%BD%E6%93%8D%E4%BD%9C%E7%AC%A6%E7%9A%84%E8%87%AA%E7%84%B6%E8%AF%AD%E4%B9%89) - - [27. 优先使用算术操作符和赋值操作符的标准形式](#27-%E4%BC%98%E5%85%88%E4%BD%BF%E7%94%A8%E7%AE%97%E6%9C%AF%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%92%8C%E8%B5%8B%E5%80%BC%E6%93%8D%E4%BD%9C%E7%AC%A6%E7%9A%84%E6%A0%87%E5%87%86%E5%BD%A2%E5%BC%8F) - - [28. 优先使用前缀版本的++和--](#28-%E4%BC%98%E5%85%88%E4%BD%BF%E7%94%A8%E5%89%8D%E7%BC%80%E7%89%88%E6%9C%AC%E7%9A%84%E5%92%8C--) - - [29. 考虑重载以避免隐式转换](#29-%E8%80%83%E8%99%91%E9%87%8D%E8%BD%BD%E4%BB%A5%E9%81%BF%E5%85%8D%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2) - - [30. 避免重载&&、||和,](#30-%E9%81%BF%E5%85%8D%E9%87%8D%E8%BD%BD%E5%92%8C) - - [31. 不要编写依赖于函数参数求值顺序的代码](#31-%E4%B8%8D%E8%A6%81%E7%BC%96%E5%86%99%E4%BE%9D%E8%B5%96%E4%BA%8E%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0%E6%B1%82%E5%80%BC%E9%A1%BA%E5%BA%8F%E7%9A%84%E4%BB%A3%E7%A0%81) - - [类的设计与继承](#%E7%B1%BB%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BB%A7%E6%89%BF) - - [32. 弄清楚要编写的是哪一种类](#32-%E5%BC%84%E6%B8%85%E6%A5%9A%E8%A6%81%E7%BC%96%E5%86%99%E7%9A%84%E6%98%AF%E5%93%AA%E4%B8%80%E7%A7%8D%E7%B1%BB) - - [33. 用小类代替巨类](#33-%E7%94%A8%E5%B0%8F%E7%B1%BB%E4%BB%A3%E6%9B%BF%E5%B7%A8%E7%B1%BB) - - [34. 用组合代替继承](#34-%E7%94%A8%E7%BB%84%E5%90%88%E4%BB%A3%E6%9B%BF%E7%BB%A7%E6%89%BF) - - [35. 避免从并非要设计成基类的类中继承](#35-%E9%81%BF%E5%85%8D%E4%BB%8E%E5%B9%B6%E9%9D%9E%E8%A6%81%E8%AE%BE%E8%AE%A1%E6%88%90%E5%9F%BA%E7%B1%BB%E7%9A%84%E7%B1%BB%E4%B8%AD%E7%BB%A7%E6%89%BF) - - [36. 优先提供抽象接口](#36-%E4%BC%98%E5%85%88%E6%8F%90%E4%BE%9B%E6%8A%BD%E8%B1%A1%E6%8E%A5%E5%8F%A3) - - [37. 公有继承即可替换性:继承,不是为了重用,而是为了被重用](#37-%E5%85%AC%E6%9C%89%E7%BB%A7%E6%89%BF%E5%8D%B3%E5%8F%AF%E6%9B%BF%E6%8D%A2%E6%80%A7%E7%BB%A7%E6%89%BF%E4%B8%8D%E6%98%AF%E4%B8%BA%E4%BA%86%E9%87%8D%E7%94%A8%E8%80%8C%E6%98%AF%E4%B8%BA%E4%BA%86%E8%A2%AB%E9%87%8D%E7%94%A8) - - [38. 实现安全的覆盖](#38-%E5%AE%9E%E7%8E%B0%E5%AE%89%E5%85%A8%E7%9A%84%E8%A6%86%E7%9B%96) - - [39. 考虑将虚函数声明为非公有的,将公有函数声明为非虚函数](#39-%E8%80%83%E8%99%91%E5%B0%86%E8%99%9A%E5%87%BD%E6%95%B0%E5%A3%B0%E6%98%8E%E4%B8%BA%E9%9D%9E%E5%85%AC%E6%9C%89%E7%9A%84%E5%B0%86%E5%85%AC%E6%9C%89%E5%87%BD%E6%95%B0%E5%A3%B0%E6%98%8E%E4%B8%BA%E9%9D%9E%E8%99%9A%E5%87%BD%E6%95%B0) - - [40. 避免提供隐式转换](#40-%E9%81%BF%E5%85%8D%E6%8F%90%E4%BE%9B%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2) - - [41. 将数据成员设置为成员、无行为的聚集(C语言形式struct)](#41-%E5%B0%86%E6%95%B0%E6%8D%AE%E6%88%90%E5%91%98%E8%AE%BE%E7%BD%AE%E4%B8%BA%E6%88%90%E5%91%98%E6%97%A0%E8%A1%8C%E4%B8%BA%E7%9A%84%E8%81%9A%E9%9B%86c%E8%AF%AD%E8%A8%80%E5%BD%A2%E5%BC%8Fstruct) - - [42. 不要公开内部数据](#42-%E4%B8%8D%E8%A6%81%E5%85%AC%E5%BC%80%E5%86%85%E9%83%A8%E6%95%B0%E6%8D%AE) - - [43. 明智使用pImpl](#43-%E6%98%8E%E6%99%BA%E4%BD%BF%E7%94%A8pimpl) - - [44. 优先编写非成员非友元函数](#44-%E4%BC%98%E5%85%88%E7%BC%96%E5%86%99%E9%9D%9E%E6%88%90%E5%91%98%E9%9D%9E%E5%8F%8B%E5%85%83%E5%87%BD%E6%95%B0) - - [45. 总是一起提供new和delete](#45-%E6%80%BB%E6%98%AF%E4%B8%80%E8%B5%B7%E6%8F%90%E4%BE%9Bnew%E5%92%8Cdelete) - - [46. 如果为类提供专门的new,那么应该提供所有标准形式(普通、placement、nothrow版本)](#46-%E5%A6%82%E6%9E%9C%E4%B8%BA%E7%B1%BB%E6%8F%90%E4%BE%9B%E4%B8%93%E9%97%A8%E7%9A%84new%E9%82%A3%E4%B9%88%E5%BA%94%E8%AF%A5%E6%8F%90%E4%BE%9B%E6%89%80%E6%9C%89%E6%A0%87%E5%87%86%E5%BD%A2%E5%BC%8F%E6%99%AE%E9%80%9Aplacementnothrow%E7%89%88%E6%9C%AC) - - [构造、析构和复制](#%E6%9E%84%E9%80%A0%E6%9E%90%E6%9E%84%E5%92%8C%E5%A4%8D%E5%88%B6) - - [47. 以同样顺序定义和初始化变量](#47-%E4%BB%A5%E5%90%8C%E6%A0%B7%E9%A1%BA%E5%BA%8F%E5%AE%9A%E4%B9%89%E5%92%8C%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%98%E9%87%8F) - - [48. 在构造函数中使用初始化代替赋值](#48-%E5%9C%A8%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BB%A3%E6%9B%BF%E8%B5%8B%E5%80%BC) - - [49. 避免在构造函数和析构函数中调用虚函数](#49-%E9%81%BF%E5%85%8D%E5%9C%A8%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%92%8C%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%E4%B8%AD%E8%B0%83%E7%94%A8%E8%99%9A%E5%87%BD%E6%95%B0) - - [50. 将基类析构函数设置为公有虚函数或者保护非虚函数](#50-%E5%B0%86%E5%9F%BA%E7%B1%BB%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%E8%AE%BE%E7%BD%AE%E4%B8%BA%E5%85%AC%E6%9C%89%E8%99%9A%E5%87%BD%E6%95%B0%E6%88%96%E8%80%85%E4%BF%9D%E6%8A%A4%E9%9D%9E%E8%99%9A%E5%87%BD%E6%95%B0) - - [51. 析构、释放和交换绝对不能失败](#51-%E6%9E%90%E6%9E%84%E9%87%8A%E6%94%BE%E5%92%8C%E4%BA%A4%E6%8D%A2%E7%BB%9D%E5%AF%B9%E4%B8%8D%E8%83%BD%E5%A4%B1%E8%B4%A5) - - [52. 一致地进行复制和销毁](#52-%E4%B8%80%E8%87%B4%E5%9C%B0%E8%BF%9B%E8%A1%8C%E5%A4%8D%E5%88%B6%E5%92%8C%E9%94%80%E6%AF%81) - - [53. 显式启用或者禁止复制](#53-%E6%98%BE%E5%BC%8F%E5%90%AF%E7%94%A8%E6%88%96%E8%80%85%E7%A6%81%E6%AD%A2%E5%A4%8D%E5%88%B6) - - [54. 避免切片,在基类中考虑使用克隆代替复制](#54-%E9%81%BF%E5%85%8D%E5%88%87%E7%89%87%E5%9C%A8%E5%9F%BA%E7%B1%BB%E4%B8%AD%E8%80%83%E8%99%91%E4%BD%BF%E7%94%A8%E5%85%8B%E9%9A%86%E4%BB%A3%E6%9B%BF%E5%A4%8D%E5%88%B6) - - [55. 使用赋值的标准形式](#55-%E4%BD%BF%E7%94%A8%E8%B5%8B%E5%80%BC%E7%9A%84%E6%A0%87%E5%87%86%E5%BD%A2%E5%BC%8F) - - [56. 只要可行,正确提供不会失败的swap](#56-%E5%8F%AA%E8%A6%81%E5%8F%AF%E8%A1%8C%E6%AD%A3%E7%A1%AE%E6%8F%90%E4%BE%9B%E4%B8%8D%E4%BC%9A%E5%A4%B1%E8%B4%A5%E7%9A%84swap) - - [命名空间与模块](#%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E4%B8%8E%E6%A8%A1%E5%9D%97) - - [57. 将类型和非成员函数接口置于同一命名空间中](#57-%E5%B0%86%E7%B1%BB%E5%9E%8B%E5%92%8C%E9%9D%9E%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E6%8E%A5%E5%8F%A3%E7%BD%AE%E4%BA%8E%E5%90%8C%E4%B8%80%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E4%B8%AD) - - [58. 应该将类型和函数分别置于不同名字空间中,除非有意想让他们一起工作](#58-%E5%BA%94%E8%AF%A5%E5%B0%86%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%87%BD%E6%95%B0%E5%88%86%E5%88%AB%E7%BD%AE%E4%BA%8E%E4%B8%8D%E5%90%8C%E5%90%8D%E5%AD%97%E7%A9%BA%E9%97%B4%E4%B8%AD%E9%99%A4%E9%9D%9E%E6%9C%89%E6%84%8F%E6%83%B3%E8%AE%A9%E4%BB%96%E4%BB%AC%E4%B8%80%E8%B5%B7%E5%B7%A5%E4%BD%9C) - - [59. 不要在头文件#include之前using命名空间](#59-%E4%B8%8D%E8%A6%81%E5%9C%A8%E5%A4%B4%E6%96%87%E4%BB%B6include%E4%B9%8B%E5%89%8Dusing%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4) - - [60. 避免在不同的模块中分配和释放内存](#60-%E9%81%BF%E5%85%8D%E5%9C%A8%E4%B8%8D%E5%90%8C%E7%9A%84%E6%A8%A1%E5%9D%97%E4%B8%AD%E5%88%86%E9%85%8D%E5%92%8C%E9%87%8A%E6%94%BE%E5%86%85%E5%AD%98) - - [61. 不要在头文件中定义具有链接的实体](#61-%E4%B8%8D%E8%A6%81%E5%9C%A8%E5%A4%B4%E6%96%87%E4%BB%B6%E4%B8%AD%E5%AE%9A%E4%B9%89%E5%85%B7%E6%9C%89%E9%93%BE%E6%8E%A5%E7%9A%84%E5%AE%9E%E4%BD%93) - - [62. 不要允许异常跨越模块边界传播](#62-%E4%B8%8D%E8%A6%81%E5%85%81%E8%AE%B8%E5%BC%82%E5%B8%B8%E8%B7%A8%E8%B6%8A%E6%A8%A1%E5%9D%97%E8%BE%B9%E7%95%8C%E4%BC%A0%E6%92%AD) - - [63. 在模块的接口中使用具有良好可移植性的类型](#63-%E5%9C%A8%E6%A8%A1%E5%9D%97%E7%9A%84%E6%8E%A5%E5%8F%A3%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%85%B7%E6%9C%89%E8%89%AF%E5%A5%BD%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%80%A7%E7%9A%84%E7%B1%BB%E5%9E%8B) - - [模板与泛型](#%E6%A8%A1%E6%9D%BF%E4%B8%8E%E6%B3%9B%E5%9E%8B) - - [64. 理智地结合静态多态与动态多态](#64-%E7%90%86%E6%99%BA%E5%9C%B0%E7%BB%93%E5%90%88%E9%9D%99%E6%80%81%E5%A4%9A%E6%80%81%E4%B8%8E%E5%8A%A8%E6%80%81%E5%A4%9A%E6%80%81) - - [65. 有意地进行显式自定义](#65-%E6%9C%89%E6%84%8F%E5%9C%B0%E8%BF%9B%E8%A1%8C%E6%98%BE%E5%BC%8F%E8%87%AA%E5%AE%9A%E4%B9%89) - - [66. 不要特化函数模板](#66-%E4%B8%8D%E8%A6%81%E7%89%B9%E5%8C%96%E5%87%BD%E6%95%B0%E6%A8%A1%E6%9D%BF) - - [67. 不要无意编写不通用的代码](#67-%E4%B8%8D%E8%A6%81%E6%97%A0%E6%84%8F%E7%BC%96%E5%86%99%E4%B8%8D%E9%80%9A%E7%94%A8%E7%9A%84%E4%BB%A3%E7%A0%81) - - [错误处理与异常](#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E5%BC%82%E5%B8%B8) - - [68. 广泛使用断言记录内部假设和不变式](#68-%E5%B9%BF%E6%B3%9B%E4%BD%BF%E7%94%A8%E6%96%AD%E8%A8%80%E8%AE%B0%E5%BD%95%E5%86%85%E9%83%A8%E5%81%87%E8%AE%BE%E5%92%8C%E4%B8%8D%E5%8F%98%E5%BC%8F) - - [69. 建立合理的错误处理策略,并严格遵守](#69-%E5%BB%BA%E7%AB%8B%E5%90%88%E7%90%86%E7%9A%84%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E7%AD%96%E7%95%A5%E5%B9%B6%E4%B8%A5%E6%A0%BC%E9%81%B5%E5%AE%88) - - [70. 区分错误与非错误](#70-%E5%8C%BA%E5%88%86%E9%94%99%E8%AF%AF%E4%B8%8E%E9%9D%9E%E9%94%99%E8%AF%AF) - - [71. 设计和编写错误安全代码](#71-%E8%AE%BE%E8%AE%A1%E5%92%8C%E7%BC%96%E5%86%99%E9%94%99%E8%AF%AF%E5%AE%89%E5%85%A8%E4%BB%A3%E7%A0%81) - - [72. 优先使用异常报告错误](#72-%E4%BC%98%E5%85%88%E4%BD%BF%E7%94%A8%E5%BC%82%E5%B8%B8%E6%8A%A5%E5%91%8A%E9%94%99%E8%AF%AF) - - [73. 通过值抛出,通过引用捕获](#73-%E9%80%9A%E8%BF%87%E5%80%BC%E6%8A%9B%E5%87%BA%E9%80%9A%E8%BF%87%E5%BC%95%E7%94%A8%E6%8D%95%E8%8E%B7) - - [74. 正确地报告、处理和转换错误](#74-%E6%AD%A3%E7%A1%AE%E5%9C%B0%E6%8A%A5%E5%91%8A%E5%A4%84%E7%90%86%E5%92%8C%E8%BD%AC%E6%8D%A2%E9%94%99%E8%AF%AF) - - [75. 避免使用异常规范](#75-%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E5%BC%82%E5%B8%B8%E8%A7%84%E8%8C%83) - - [STL:容器](#stl%E5%AE%B9%E5%99%A8) - - [76. 默认使用vector,否则选择其他合适的容器](#76-%E9%BB%98%E8%AE%A4%E4%BD%BF%E7%94%A8vector%E5%90%A6%E5%88%99%E9%80%89%E6%8B%A9%E5%85%B6%E4%BB%96%E5%90%88%E9%80%82%E7%9A%84%E5%AE%B9%E5%99%A8) - - [77. 用vecotr和string代替数组](#77-%E7%94%A8vecotr%E5%92%8Cstring%E4%BB%A3%E6%9B%BF%E6%95%B0%E7%BB%84) - - [78. 使用vector和string::c_str与非C++API交互](#78-%E4%BD%BF%E7%94%A8vector%E5%92%8Cstringc_str%E4%B8%8E%E9%9D%9Ecapi%E4%BA%A4%E4%BA%92) - - [79. 在容器中只存储值和只能指针](#79-%E5%9C%A8%E5%AE%B9%E5%99%A8%E4%B8%AD%E5%8F%AA%E5%AD%98%E5%82%A8%E5%80%BC%E5%92%8C%E5%8F%AA%E8%83%BD%E6%8C%87%E9%92%88) - - [80. 用push_back代替其他扩充序列的方式](#80-%E7%94%A8push_back%E4%BB%A3%E6%9B%BF%E5%85%B6%E4%BB%96%E6%89%A9%E5%85%85%E5%BA%8F%E5%88%97%E7%9A%84%E6%96%B9%E5%BC%8F) - - [81. 多用范围操作,少用单元素操作](#81-%E5%A4%9A%E7%94%A8%E8%8C%83%E5%9B%B4%E6%93%8D%E4%BD%9C%E5%B0%91%E7%94%A8%E5%8D%95%E5%85%83%E7%B4%A0%E6%93%8D%E4%BD%9C) - - [82. 使用公用惯用法真正压缩容量,真正删除元素](#82-%E4%BD%BF%E7%94%A8%E5%85%AC%E7%94%A8%E6%83%AF%E7%94%A8%E6%B3%95%E7%9C%9F%E6%AD%A3%E5%8E%8B%E7%BC%A9%E5%AE%B9%E9%87%8F%E7%9C%9F%E6%AD%A3%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0) - - [STL:算法](#stl%E7%AE%97%E6%B3%95) - - [83. 使用带检查的STL实现](#83-%E4%BD%BF%E7%94%A8%E5%B8%A6%E6%A3%80%E6%9F%A5%E7%9A%84stl%E5%AE%9E%E7%8E%B0) - - [84. 用算法调用代替手工编写的循环](#84-%E7%94%A8%E7%AE%97%E6%B3%95%E8%B0%83%E7%94%A8%E4%BB%A3%E6%9B%BF%E6%89%8B%E5%B7%A5%E7%BC%96%E5%86%99%E7%9A%84%E5%BE%AA%E7%8E%AF) - - [85. 使用正确的STL查找算法](#85-%E4%BD%BF%E7%94%A8%E6%AD%A3%E7%A1%AE%E7%9A%84stl%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95) - - [86. 使用正确的STL排序算法](#86-%E4%BD%BF%E7%94%A8%E6%AD%A3%E7%A1%AE%E7%9A%84stl%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95) - - [87, 使谓词成为纯函数](#87-%E4%BD%BF%E8%B0%93%E8%AF%8D%E6%88%90%E4%B8%BA%E7%BA%AF%E5%87%BD%E6%95%B0) - - [88. 算法和比较器的参数应多用函数对象少用函数](#88-%E7%AE%97%E6%B3%95%E5%92%8C%E6%AF%94%E8%BE%83%E5%99%A8%E7%9A%84%E5%8F%82%E6%95%B0%E5%BA%94%E5%A4%9A%E7%94%A8%E5%87%BD%E6%95%B0%E5%AF%B9%E8%B1%A1%E5%B0%91%E7%94%A8%E5%87%BD%E6%95%B0) - - [89. 正确编写函数对象](#89-%E6%AD%A3%E7%A1%AE%E7%BC%96%E5%86%99%E5%87%BD%E6%95%B0%E5%AF%B9%E8%B1%A1) - - [类型安全](#%E7%B1%BB%E5%9E%8B%E5%AE%89%E5%85%A8) - - [90. 避免使用类型分支,多使用多态](#90-%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8%E7%B1%BB%E5%9E%8B%E5%88%86%E6%94%AF%E5%A4%9A%E4%BD%BF%E7%94%A8%E5%A4%9A%E6%80%81) - - [91. 依赖类型而不是其表示形式](#91-%E4%BE%9D%E8%B5%96%E7%B1%BB%E5%9E%8B%E8%80%8C%E4%B8%8D%E6%98%AF%E5%85%B6%E8%A1%A8%E7%A4%BA%E5%BD%A2%E5%BC%8F) - - [92. 避免使用reinterpret_cast](#92-%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8reinterpret_cast) - - [93. 避免对指针使用static_cast](#93-%E9%81%BF%E5%85%8D%E5%AF%B9%E6%8C%87%E9%92%88%E4%BD%BF%E7%94%A8static_cast) - - [94. 避免强制转换const](#94-%E9%81%BF%E5%85%8D%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2const) - - [95. 不要使用C风格强制类型转换](#95-%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8c%E9%A3%8E%E6%A0%BC%E5%BC%BA%E5%88%B6%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2) - - [96. 不要对非POD类型进行memcpy或者memcmp操作](#96-%E4%B8%8D%E8%A6%81%E5%AF%B9%E9%9D%9Epod%E7%B1%BB%E5%9E%8B%E8%BF%9B%E8%A1%8Cmemcpy%E6%88%96%E8%80%85memcmp%E6%93%8D%E4%BD%9C) - - [97. 不要使用union重新解释表示方式](#97-%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8union%E9%87%8D%E6%96%B0%E8%A7%A3%E9%87%8A%E8%A1%A8%E7%A4%BA%E6%96%B9%E5%BC%8F) - - [98. 不要使用C风格变长参数(...)](#98-%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8c%E9%A3%8E%E6%A0%BC%E5%8F%98%E9%95%BF%E5%8F%82%E6%95%B0) - - [99. 不要使用失效对象,不要使用不安全函数](#99-%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E5%A4%B1%E6%95%88%E5%AF%B9%E8%B1%A1%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%AE%89%E5%85%A8%E5%87%BD%E6%95%B0) - - [100. 不要多态地处理数组](#100-%E4%B8%8D%E8%A6%81%E5%A4%9A%E6%80%81%E5%9C%B0%E5%A4%84%E7%90%86%E6%95%B0%E7%BB%84) - - - -# C++编程规范:101条规则、准则与最佳实践 - -书籍:《[C++编程规范:101条规则、准则与最佳实践](https://book.douban.com/subject/26899830/)》。 - -首先: -- 任何准则都不应该代替自己的思考,遵从好的准则,但一定要有自己的思考,不要盲从。 -- 另外重在理解为什么,不需要死记硬背。 -- 某些规范仅在某些情况下适用,注意使用场景。 - -## 组织与策略问题 - -### 0. 不要拘泥与小节/了解哪些东西该标准化 - -- 摘要:只规定需要规定的东西,不要强制施加个人喜好和过时的东西。 -- 比如强制大括号位置、空格制表符、强制匈牙利命名、强制函数单出口等。 - -### 1. 在高警告级别干净利落地编程 - -- 摘要:使用编译器的最高警告级别,要求构建干净利落,没有警告。理解所有警告,通过修改代码而不是降低警告级别来排除警告。 -- 但实践时警告等级太高可能会导致报出很多不必要甚至虚假的警告,这时候就需要有一定消除手段。可以通过对不可修改的头文件进行包装消除这种警告,处理警告时需要确保已经完全理解了其含义。 -- 看到这里我立刻去提高了我的Makefile中的g++警告等级,现在它是:`-Wall -Wextra -Wfatal-errors -pedantic-errors -Wshadow`,基本够用了,如果需要将警告变为错误那么还需要`-Werror`。 - -### 2. 使用自动构建系统 - -- 摘要:使用自动构建系统,完全自动化操作,无需用户干预便可构建整个项目。 -- 现代C++中,大中型项目应该使用MsBuild、CMake、Xmake等现代化构建工具,小型项目中Makefile依旧可用。 -- 现代C++构建系统中的基本功能:增量构建、完全构建、构建范围选择、选择目标架构、调试模式发布模式选择、直接构建生成安装包等。 -- 自动构建系统应该在项目启动时就引入,大型项目还会需要专门的构建管理员。 - -### 3. 使用版本控制系统 - -- 摘要:使用版本控制系统(VCS,Version Control System),不要让文件长时间脱离版本控制,不要将工作长时间停留在本地以免丢失,应当保证每一次提交都能够成功构建。 -- 每个程序员都应该会使用git。 - -### 4. 做代码审查 - -- 摘要:代码提交最好都经过审查,相互审查代码,指出问题,互相学习。 -- 代码审查(Code Review)应该作为软件开发周期中的常规环节。 - -## 设计风格 - -### 5. 一个实体应该只有一个紧凑的职责 - -- 摘要:只给一个实体(变量、类、函数、命名空间、模块和库)赋予一个定义良好的职责。随着实体变大,职责范围会扩大但不应该发散。 -- 一个实体如果具有多个目的,那么除了会增加理解难度、实现复杂度、各部分的错误之外,还会导致很多其他问题。 -- 应该实现目的单一的函数、小而且单一的类以及边界清晰的紧凑模块,使用这些简单的功能单一的实体来实现复杂的行为。 -- 应该使用较小的底层抽象构建更高层次的抽象,避免将多个底层抽象集合成较大的低层抽象聚合体。 - -### 6. 正确、简单和清晰第一 - -- 摘要:软件简单为美,质量优于速度,简单优于复杂,清晰优于机巧,安全优于不安全。 -- 代码可读性至关重要,不止有一个人阅读你的代码。即使是自己也不一定能完全看懂自己一个月前写得代码。 -- 代码可读性和代码优化很多时候是矛盾的。 - -### 7. 编程中应知道何时与如何考虑可伸缩性 - -- 摘要:小心算法复杂度的爆炸增长,不要进行不成熟的提前优化,但是任何时候都应该密切关注算法的复杂度。 -- 通过保证复杂度来保证对未来可能面对的更大数据量下的性能。 -- 即使可预见的未来不会有特别大的数据量,也应该避免不能很好应付数据量增加的算法(除非这种算法确实过于清晰、简单、可读性强)。 -- 一些具体做法: - - 使用灵活的动态分配的数组,而不是固定大小数组。 - - 了解算法的复杂度。 - - 优先使用快的算法,有对数复杂度就不用线性复杂度,比如能二分查找就绝对不按顺序遍历查找。 - - 即使要优化,也应该尝试优化复杂度,而不是浪费精力在节省一个多余加法这种无关紧要不关乎大局的细节上。 - -### 8. 不要进行不成熟的优化 - -- 摘要:优化的第一原则是不要优化,第二原则还是不要优化,第三原则是经过再三测试(profiling)之后再优化。 -- 在编写开初就需要考虑复杂度,不要过早就进行复杂的优化,都说过早优化是万恶之源。当性能出现问题时,或者功能完成后才进行优化。 -- 优化也应当在严格profiling之后再优化,应当只在必要的情况下优化运行最多的瓶颈代码。 -- 而不是浪费时间搞一些完全不重要的、主观臆想的、增加代码复杂程度、降低可读性、打乱架构的特例性优化。 - -### 9. 不要进行不成熟的劣化 - -- 摘要:在代码复杂性和可读性相同的情况下,选择高效的设计模式和编程习惯总是更好的。因为并没有以可读性和代码复杂性为代码,所以这是在避免不成熟的劣化,而不是不成熟的优化。 -- 例子:将临时变量从循环中提出来,使用前缀`-- ++`,引用传递参数等。 - -### 10. 尽量减少全局和共享数据 - -- 摘要:避免共享数据,尤其是全局数据。共享数据会增加耦合,降低可维护性,通常还会降低性能。 -- 因为使用共享数据的代码片段不仅取决于数据变化的过程,还取决于以后会使用该数据的未知代码区域的机能。 -- 不同编译单元中全局对象的初始化顺序还是不确定的,应尽量避免使用。 -- 全局的数据还会降低多线程和多处理器环境下的并行性。 -- 即使要用也应该使用工厂来注册与维护。 - -### 11. 隐藏信息 - -- 摘要:不要公开提供抽象实体的内部信息。 -- 信息隐藏限制了变化的影响范围,强化的不变的东西(接口),降低耦合。 - -### 12. 懂得如何和何时进行并发编程 - -- 摘要:如果应用程序使用了多个线程或者进程,应当知道如何尽量减少共享对象,以及如何安全地访问必须共享的对象。 -- 具体做法: - - 了解平台的多线程接口(当然C++11标准已经提供了跨平台的多线程),了解同步原语:原子操作、内存栅栏、互斥体等。 - - 最好将平台原语包装起来自己设计抽象,益于跨平台移植。 - - 确保正在使用的类型是多线程安全的。 - -### 13. 确保资源为对象所拥有,使用显式的RAII和智能指针 - -- 摘要:RAII是惯用的正确处理资源的手段。分配原始资源时应当立即将它传递给其管理对象,永远不要在一条语句中分配一个以上对象。 -- 实现RAII类时,小心拷贝构造与赋值操作。 -- 最好使用智能指针来管理内存。 -- 不要在一条语句中分配一个以上对象,因为C++标准对求值顺序的规定很弱,可能申请了一个资源,但是还没有被管理就去申请另一资源,此时抛异常导致第一个资源没有释放(比如在函数调用中)。 -- 不要滥用智能指针,如果原始指针够用,那么也没有必要用智能指针。 - -## 编程风格 - -讨论更具体的编程问题。 - -### 14. 宁可编译时链接时错误,也不要运行时错误 - -- 摘要:能在编译期做的事情,就不要推迟到运行时。能够在编译期检查不变式就应该在编译期做。 -- 例子: - - 编译期条件就在编译期检查。 - - 考虑在合适的场景使用编译期多态代替运行时多态。 - - 使用枚举。 - - 如果经常使用`dynamic_cast`,说明基类提供功能太少了,可以重新设计接口。 - -### 15. 积极使用const - -- 摘要:应尽量使用常量,会带来编译期类型检查。 -- 当需要合法在`const`函数中修改变量时,声明为变量为`mutable`(应该用在这种修改不影响对象可观察状态的情况下,比如缓存数据:不影响正确性,只提供更快的性能,数据本身并没有改变)。 -- `const`具有传染性。 -- 不要强制转换`const`,这通常意味着设计哪里出现了问题。 -- 应避免将值传递的参数设置为`const`,避免在参数中使用顶层`const`:在函数声明层面它会被忽略,但是语义约束还在。 - -### 16. 避免使用宏 - -- 摘要:避免使用宏。 -- C++提供的`const enum inline template namespace`已经取代了宏的大部分功能,并且提供的更安全的语法。 -- 宏目前唯二无法替代的地方在于代码片段复用和跨平台,即使要用也应该谨慎使用。包含守卫都已经可以使用`#pragma once`代替了。 -- 不要搞那些让人迷惑的宏元编程,大多数人都看不懂。 -- 宏的问题在于不“卫生”,它仅仅是一种文本替换,忽略了作用域,忽略了类型系统,忽略了其他所有语言特性,天生是割裂的显得格格不入。 -- 即使要用也应该尽快`#undef`取消其定义。 - -### 17. 避免使用魔法数字 - -- 摘要:避免显式使用魔法数字,即使它有意义,也应该使用符号名称来替换它。 -- 字符串字面量应该使用符号常量来代替,并集中存放方便查找修改和国际化。 - -### 18. 尽可能局部地声明变量 - -- 摘要:变量将引入状态,状态的存在时间越短越好,最好只作用于用到它的作用域内。 -- 避免污染上下文。 -- 常量不引入状态,不适用于本条。 -- C++中鼓励即用即声明,有了足够的数据初始化时才声明是一个好选择。 -- 将循环内的局部变量提出循环属于特例,可以自行判别该如何选择。 - -### 19. 总是初始化变量 - -- 摘要:总是再定义变量定义时初始化,避免使用未初始化的变量导致的错误。 -- 一般而言安全性总是优于不必要的性能考虑。 - -### 20. 避免函数过长,避免嵌套过深 - -- 摘要:应避免过长的函数与过深的嵌套层次出现。 -- 过长的函数与逻辑可能会使其难以维护、错误频出。 -- 过深的嵌套层次要求我们在读代码时就维护脑子里面的栈,不利于可读性。 -- 例外:如果一个长函数无法拆分,那么最好不要强行拆分。 - -### 21. 避免跨编译单元的初始化依赖 - -- 摘要:不同编译单元的命名空间作用域的对象不应该在初始化上相互依赖,因为他们的初始化顺序是未定义的。 -- 应避免使用全局或者命名空间作用域的对象,如果一定要用,可以用单例模式代替。单例模式也应该在第一次获取时初始化(一般通过static局部变量来做)。 - -### 22. 尽量减少定义性依赖,避免循环依赖 - -- 摘要:如果使用完整声明能够实现,就不要包含完整定义。 -- 模块之间不要相互依赖,双向的依赖代表这他们应该是一个模块。 - -### 23. 头文件应该自给自足 - -- 摘要:每一个头文件应该要能够独立通过编译,它应该包含它所依赖的所有头文件。 -- 但是不要包含不需要的头文件。 - -### 24. 总是编写内部包含守卫,不要编写外部包含守卫 - -- 摘要:在所有头文件中使用包含守卫,而不要在其外部使用该头文件的包含守卫。 -- 包含守卫的宏应该定义为唯一名称。 -- 外部包含守卫已经过时了,不要再使用了。 -- 现代C++中最好使用`#pragma once`。 - -## 函数与操作符 - -### 25. 正确选择通过值、引用还是(智能)指针传递参数 - -- 摘要:正确传递参数,分清输入参数、输出参数、输入输出参数。 -- 不要使用C语言风格的变长参数。 - -### 26. 保持重载操作符的自然语义 - -- 摘要:只有在有充分理由时才重载操作符,而且应当保持其自然语义。 -- 如果无法做到这一点,那么大概率不应该使用运算符重载。 -- 如果一定要使用运算符重载设计一门DSL,那么最好谨慎设计、让他们保持自洽并且不与现有运算符冲突。 - -### 27. 优先使用算术操作符和赋值操作符的标准形式 - -- 摘要:如果要定义以算术运算符,那么最好定义其复合赋值运算符,并且最好是通过复合赋值来实现算术运算符。 -- 算术运算版本应该返回临时变量。 -- 最好将运算符定义为非成员版本以提供转换。 -- 某些情况下用算术运算符版本来实现复合赋值可能更好。 - -### 28. 优先使用前缀版本的++和-- - -- 摘要:重载`++`和`--`应当重载前缀后缀两个版本,并且行为模仿内置运算符。并且在不使用原值时最好使用前缀版本。 -- 前缀版本性能更好一些,不过这点性能真的重要吗,并且编译器会优化。当然这也是避免不成熟的劣化。 - -### 29. 考虑重载以避免隐式转换 - -- 摘要:隐式类型转换提供了语法便利,但如果创建临时对象的工作并不必要并且使用原类型更适合优化,那么可以重载提供精确匹配的版本。 - -### 30. 避免重载&&、||和, - -- 摘要:内置的`&& || ,`具有求值顺序规定,而重载的版本则没有,无法实现和内置版本完全相同的语义,应当避免重载重载这几个运算符。 -- 没有了确定求值顺序,就无法保证`&& ||`的短路求值了。那么`p && p->something`这种代码可能就会出现错误,这种代码是不健壮的。 -- 表达式模板是例外,因为表达式模板的模板就是用来捕获操作符。 - -### 31. 不要编写依赖于函数参数求值顺序的代码 - -- 摘要:函数参数的求值顺序是不确定的,不要依赖于此编程。 -- 可以通过使用命名对象控制求值顺序。 - -## 类的设计与继承 - -### 32. 弄清楚要编写的是哪一种类 - -- 摘要:不同种类的类适用于不通过用途,因此遵循不同规则,弄清楚要编写的是哪一种。 -- 值类: - - 拥有公有析构、拷贝构造、带有值语义的赋值。 - - 没有虚函数,包括析构。 - - 用作具体类,而不是基类。 - - 总是在栈中实例化,或者作为另一个类直接包含的成员实例化。 -- 基类: - - 存在一个公有且虚拟、或者保护而且非虚拟的析构,和一个非公有的拷贝构造与赋值运算符。 - - 通过虚函数建立接口。 - - 总是动态在堆中实例化为具体派生类对象,并通过指针来管理。 -- 特征类: - - 只包含嵌套类型声明与静态数据或者函数,没有可改变状态或者虚函数。 - - 通常不实例化。 -- 异常类: - - 有一个公有析构函数和不会失败的构造函数(特别是拷贝构造)。 - - 有虚函数,经常实现克隆和访问。 - - 从`std::exception`比较好。 -- 还有比如RAII类等。 - -### 33. 用小类代替巨类 - -- 摘要:小类更容易编写,更容易保证正确、测试和使用。小类更可能适用于各种场合,应该使用小类体现简单概念,而不是大杂烩式的类。 -- 小类更易编写与重用,大类更加难以编写和使用。 -- 巨类更难以保证正确性。 -- 人的需求总在变,尝试提供完整解决方案几乎总会失败。 - -### 34. 用组合代替继承 - -- 摘要:继承的耦合非常紧密,仅次于友元。如果能够使用组合代替继承,那么最好使用组合代替继承,除非继承有明显的设计好处。 -- 有了继承之后,人们经常拿着锤子看什么都是钉子,继承很容易被滥用。 -- 除非要用到继承的东西(重写虚函数、派生类替换基类),否则不要使用继承。 - -### 35. 避免从并非要设计成基类的类中继承 - -- 摘要:本意就不是作为基类来设计的类不应作为基类被继承,这是一种严重的设计错误。如果要添加行为,应该添加非成员函数而不是成员函数,要添加数据应该使用组合而不是继承。 - -### 36. 优先提供抽象接口 - -- 摘要:抽象接口有助于集中精力保证抽象的正确性,不至于受到实现或者状态管理细节的干扰。优先采用实现了抽象接口的设计层级结构。 -- 抽象接口是完全由纯虚函数组成的抽象类,没有状态(数据成员),通常也没有成员函数实现。 -- 遵循依赖倒置原则(DIP, Dependency Inversion Principle): - - 高层模块不应该依赖低层模块,两者都应该依赖抽象。 - - 抽象不应该依赖细节,细节应该依赖抽象。 -- DIP具有三个优点:更强健壮性、更大灵活性、更好模块性。 - -### 37. 公有继承即可替换性:继承,不是为了重用,而是为了被重用 - -- 摘要:公有继承能够使基类指针或者引用指向派生类对象。不要通过公有继承重用基类代码(指通过派类对象或者指针引用),而是为了被已经多态使用的基对象的已有代码重用的。 -- 继承的使用应该遵循里氏替换原则,即派生类指针引用能够完美替换基类指针引用,满足Is-A关系。 -- 所以派生类必须正确实现基类接口该有的功能,这种语义约定必须被遵守,不然就是误用。 -- 并且Is-A的关系并非简单的“是一个”,而更类似于“行为像一个”(或者说“可以用作一个”),比如正方形是一个矩形,但是正方形从矩形继承却是怎么看都不合适的。 -- 公有继承的目的并非重用,而是为了实现**可替换性**。 -- 当然现实中实践时可能某些情况下完全不会使用其可替换性,而仅仅是为了重用,这种情况按照书中描述应该**使用组合或者非公有继承**来实现。 -- 通过添加新派生类添加功能时,不需要修改现有使用基类指针引用的代码,这满足开闭原则(对扩展开放,对修改关闭)。 -- 特例:策略类和混入类(MixIn)通过公有继承添加行为,这不是误用。 - -### 38. 实现安全的覆盖 - -- 摘要:重写一个虚函数时,要保持其可替换性。更具体一点,要保持基类虚函数的前后置条件,不要改变虚函数默认参数。应该显式声明为`vritual override`。 -- 可替换性在于多个方面: - - 重写的函数可以要求更少提供更多,但不可以要求更多承诺更少。 - - 如果基类虚函数承诺不会失败,那么派生类重写后不应抛出异常。 - - 重写虚函数永远不应该修改其默认参数,它们不是函数签名一部分,修改默认参数可能导致奇怪的错误,通过基类指针引用多态调用使用的总是基类的默认参数。 -- 应该显式声明重写的虚函数为`virtual`和`override`,借由编译器检查保证正确性。 -- 谨防在派生类中隐藏基类虚函数,这可能会发生在虚函数本身有重载的情况下,只重写了一个那么另一个由于作用域嵌套关系会先找到派生类函数。解决方法是使用`using`引入基类函数。 - -### 39. 考虑将虚函数声明为非公有的,将公有函数声明为非虚函数 - -- 摘要:在基类中进行修改代价高昂:可以将公有函数函数设置为非虚的,并且将虚函数设置为私有,如果派生类需要调用基类版本则设置为保护。 -- 这就是NVI(非虚接口,Non Virtual Interface)模式。 -- 公有虚接口其实提供了两个职责:指定接口,指定实现细节。这两件事职责和动机不同,有些时候会冲突。 -- 使用非虚接口后,公有非虚接口只提供接口,虚函数不再提供接口。可以有更高的灵活性,并且能够健壮地适应变化。 -- 特例: - - 对析构函数不适用。 - - NVI不支持调用者的协变返回类型(即返回基类虚函数指针引用的底层类型的派生类的指针引用)。 - -### 40. 避免提供隐式转换 - -- 摘要:隐式类型转换通常利大于弊,为自定义类型提供隐式类型转换之前,需要三思。应该依赖显式类型转换(explicit转换构造与转换运算符)。 -- 通常来说,只有非常直观的非常合理的隐式类型转换才应该被使用。单参数的构造如果不确定那么最好都加上`explicit`。 -- 转换运算符可以通过提供命名转换函数替代。 - -### 41. 将数据成员设置为成员、无行为的聚集(C语言形式struct) - -- 摘要:将数据设置为私有的。只有表示数据的聚合类才会将数据成员设置为公有。 -- 私有数据成员封装实现,所有修改都通过接口来实现,是可预测的。公有数据成员则是混乱和无法预测的。 -- 考虑使用`pImpl`惯用法来隐藏类的私有成员(通常只针对提供给外部的SDK这样以做到二进制兼容与隐藏实现细节)。 -- 在没有更好方法的情况下,使用`getter/setter`都是可以接受的。提供最小的抽象以及健壮的版本管理。 -- getter/setter很好用,但是主要有getter/setter组成的类可能是一种设计不良的表现。这种时候应该要仔细思考一下,是否应该定义为一个聚合类。不提供抽象,仅保存数据。 - -### 42. 不要公开内部数据 - -- 摘要:避免返回类所管理的内部数据的句柄,这样类的客户就不会不受控制在对象不知情的情况下修改对象的状态。 -- 客户应该通过你提供的接口来进行和内部数据有关的一切操作,你应该将操作封装好以供用户调用。 -- 用户都不应该知道你内部有这样一个东西,也就是接口不应该依赖于实现。 -- 在完全知情的情况下为了方便可以提供,不过不应该直接提供,而应该通过中间层/后门的方式提供给自己用,而不能直接提供给用户。 - -### 43. 明智使用pImpl - -- 摘要:C++将私有成员指定为不可访问,但没有指定为不可见。如果要将数据成员变得真正不可见,可以使用pImpl手法。 -- 这样做即使修改了数据成员也能能够保持二进制兼容。 -- 只应该用在确实要隐藏数据成员时:通常用在要提供给用户的SDK中,内部使用则一般不必要。 - -### 44. 优先编写非成员非友元函数 - -- 摘要:尽可能将函数指定为非成员非友元函数。 -- 如果一个函数没有用到内部数据,只用到公有成员那么就可以这么做,也应该这么做。 - -### 45. 总是一起提供new和delete - -- 摘要:每个类重载的`operator new`都必须要有对应的`operator delete`。 -- 因为构造时如果分配了内存但是构造抛出异常,那么会调用对应`operator delete`来释放,如果没有则不会调用从而导致内存泄漏。 - -### 46. 如果为类提供专门的new,那么应该提供所有标准形式(普通、placement、nothrow版本) - -- 摘要:如果定义了`operator new`,那么就应该定义普通版本、nothrow版本和placement版本。 -- 因为为一个类定义`operator new`就会隐藏全局的所有`operator new`,所以为了能够使用这三种变体,应该提供三个版本的`operator new`。 -- 当然并非说一定要定义3个,这个条款只是为了提醒不要因为疏忽而隐藏他们,定义时要考虑清楚要定义哪些。 -- 数组版本`operator new[]`同理。 - -## 构造、析构和复制 - -### 47. 以同样顺序定义和初始化变量 - -- 摘要:成员初始化顺序要与类定义中声明顺序始终保持一致。最好做法是构造函数初始化列表中的初始化顺序与声明顺序一致。 -- 以避免初始化的变量之间有依赖造成的问题。 -- g++中开启`-Wreorder`可以在构造函数初始化列表中顺序与数据成员声明顺序不一致时提供警告。 -- 一般来说还是尽量不要让一个成员的初始化依赖另一个成员最好。 - -### 48. 在构造函数中使用初始化代替赋值 - -- 摘要:如题。 -- 因为没有在构造函数初始化列表中的数据成员会执行默认初始化,再赋值会导致性能下降。 -- 例外:应该在构造函数体内进行非托管资源获取。 - -### 49. 避免在构造函数和析构函数中调用虚函数 - -- 摘要:如题。 -- 因为构造函数是先基类后派生类,析构函数是先派生类后基类。 -- 所以在构造函数和析构函数中调用虚函数并不会调用到派生类重写的虚函数,只会调用到自己的或者继承而来的。 -- 如果希望在构造函数中调用虚函数,可以有几种解决方案: - - 可以使用后构造函数(post-constructor),也就是构造函数执行完后的类似于`init`这样的初始化函数。这时需要在文档中注明需要这样做,由用户来调用。 - - 可以在第一次调用成员函数时进行初始化,存一个布尔标志做一个判断即可。 - - 使用工厂函数,在其中初始化。 - -### 50. 将基类析构函数设置为公有虚函数或者保护非虚函数 - -- 摘要:如题。 -- 如果允许通过基类指针引用析构对象,那么析构一定要可见(公用)并且必须是虚函数。 -- 如果不允许通过基类指针析构对象,那么则没有必要定义为虚函数,并且需要设置为保护以避免外部调用。 -- 总是为基类编写析构函数,因为隐式生成的是公有且非虚的。 - -### 51. 析构、释放和交换绝对不能失败 - -- 摘要:决不允许析构函数、资源释放函数、交换函数报告错误。 -- 如果无法安全的析构、释放资源、交换,那么无法安全的撤销与回滚(这常见与RAII对象的析构中),那么也就无法实现不会失败的提交。 -- 在捕获到异常时,会对已经构造的对象调用析构,配合RAII就保证了资源在异常发生时也能够正确释放。如果析构不保证不会失败,那么就可能发生抛异常时的析构处理再抛异常,此时程序会直接终止(`std::terminate`)。 - -### 52. 一致地进行复制和销毁 - -- 摘要:如果定义了拷贝构造、拷贝赋值、析构中的任何一个,那么可能也需要定义另外两个。 -- 定义以就意味着要做默认行为之外的事情,而这三个函数是不对称相关的。 - -### 53. 显式启用或者禁止复制 - -- 摘要:在以下三种行为中进行选择——使用编译器生成的拷贝构造和拷贝赋值、编写自己的版本、如果不允许赋值那么显式禁用前两者。 -- 对于值语义的类,编译器生成的往往符合要求。但对于需要自己管理资源的类,则常常不符合。 -- 根据需要声明为`=default =delete`比直接用默认行为要更好,仔细思考类的行为,如果不需要则应该禁用,如果可使用默认生成的版本,则最好显式声明为`=default`。 - -### 54. 避免切片,在基类中考虑使用克隆代替复制 - -- 摘要:多态复制的话,考虑禁用拷贝构造与拷贝赋值,而改用克隆函数复制对象。 -- 因为拷贝构造和拷贝赋值都是值语义的,派生类对象对基类对象赋值会导致切片。 -- 标准做法是在基类声明`clone`虚函数,每个派生类根据自己的类型重写。 - -### 55. 使用赋值的标准形式 - -- 摘要:实现赋值运算符时,应该使用标准形式——具有特定签名的非虚形式。 -- 即: -```C++ -T& operator=(const T&); -T& operator=(T); -``` -- 通常定义为第一个形式,如果通过交换实现,那么则选用第二个(在引入移动语义后第二个还能统一移动语义和拷贝语义)。 -- 不要将赋值运算符定义为虚函数,如果需要这么做,那么定义一个这种功能的其他函数(命名为比如`assign`)。 -- 需要确保自赋值是安全的。基于交换的版本是天然自赋值安全的。 - -### 56. 只要可行,正确提供不会失败的swap - -- 摘要:考虑提供一个安全的不会失败的`swap`以实现高效的交换。 -- 并同时对`std::swap`提供特化,调用成员版的`swap`即可。 -- 对于许多标准库算法,提供`swap`会提升效率。不提供则会使用标准库版本的通过拷贝构造和拷贝赋值(移动构造、移动赋值)来实现的版本。 -- 对于值语义的类来说,提供交换是有用的,对于基类来说往往就没什么用了(一般通过指针使用)。 - -## 命名空间与模块 - -### 57. 将类型和非成员函数接口置于同一命名空间中 - -- 摘要:如果将非成员函数(特别是操作符与辅助函数)设计为类的接口的一部分,那么必须在类相同的命名空间中定义他们,以便正确调用。 -- 公有成员函数和非成员函数都是类公有接口的一部分。 -- 定义在相同命名空间则允许用户使用时不显式声明出函数的命名空间,此时使用ADL查找到函数名称。 -- 通常用于操作符,这样就不必为每一个操作符显式`using`或者通过命名空间以函数形式调用,函数的话还是显式写出名字空间比较好。 - -### 58. 应该将类型和函数分别置于不同名字空间中,除非有意想让他们一起工作 - -- 摘要:如果想让类型和函数分别独立工作,而不是作为类的公有接口。那么应该将他们置于不同的命名空间,防止ADL发生作用。 -- 这一建议主要为了规避ADL带来的可能的问题。 -- ADL的规则比较复杂,特别是在涉及到模板时。 -- 详细了解ADL的规则并以此作为编程基础是没有必要和晦涩的,最好是显式声明名称空间,仅对运算符使用ADL查找。 - -### 59. 不要在头文件#include之前using命名空间 - -- 摘要:不要在包含头文件之前using命名空间或者使用using指令。 -- 这可能导致头文件中的符号的含义发生改变,产生诡异的错误。 -- 另外:在头文件中也不应该using命名空间或者使用using指令(局部作用域是可以的),相反应该显式限定符号的命名空间。 -- 应该在实现文件的`#include`之后using命名空间或者使用using指令。 -- 在源文件中`using`命名空间或者使用`using`指令是很自然的,出现名称冲突时通过限定命名空间即可解决。 -- 当然不能让`using`命名空间和`using`指令的使用限制其他人的代码。也就是说不能在任何可能跟有其他人代码的地方是用它们(其实通常也只有头文件中是这样)。 -- 在命名空间中使用`using`命名空间或者使用`using`指令是同样危险的。 - -### 60. 避免在不同的模块中分配和释放内存 - -- 摘要:在一个模块中分配内存而在另一个模块中释放,会让这两个模块间产生轻微的远距离依赖,使程序变得脆弱。 -- 如果要这么做,必须要使用相同的编译器版本、同样的编译选项和相同的标准库实现来编译他们。 -- 实践中,释放内存时,用来分配内存的模块最好仍在内存中。 -- 为了确保删除由合适的函数进行,一个很好的方式是使用智能指针。 -- 当然一切的前提都是用来释放内存的模块(也就是释放内存的函数位于的那个模块)仍在内存中,也就是说动态链接才会有这个问题,静态链接则不需要烦恼。 - -### 61. 不要在头文件中定义具有链接的实体 - -- 摘要:具有链接属性的实体,包括命名空间级的变量和函数,都需要分配内存。不应该在头文件中这样定义,将具有链接的实体放入实现文件。 -- 特别地,不要在头文件中定义静态全局变量或者函数。 -- 例外:内联函数、内联变量、函数模板、类模板的静态数据成员定义可以放在头文件中,编译器和链接器负责对他们去重。 -- 所以如果要编写仅头文件的库的话,要做的就是将所有非模板的定义声明为内联即可。 - -### 62. 不要允许异常跨越模块边界传播 - -- 摘要:C++异常处理没有普遍使用的二进制标准,不要在两段代码之间传播异常,除非他们是使用相同的编译器相同的编译选项构建的。更具体地来说:不要允许异常跨越模块或者子系统边界传播。 -- C++标准并没有固定异常传播的实现方式,甚至没有大多数系统遵守的事实标准,就MinGW64来说就有3中异常实现方式:dwarf、sjlj、seh。 -- 结合60条,最好是对整个系统使用相同的编译器相同的编译选项。 -- 实践中,以下位置应该要有用于兜底的捕获所有异常的`catch(...)`语句,并将其记录与日志系统中: - - `main`函数附近,捕获任何其他地方没有捕获到的异常,防止系统终止。 - - 从无法控制的代码中执行回调附近,不要让异常传播到回调函数之外。因为回调函数的代码很有可能使用不同的异常处理机制,甚至不是使用C++编写的。 - - 在线程边界附近,不要跨线程传播异常。 - - 在模块接口边界附近,子系统开放公用接口供外部使用。那么异常应该局限与外部,并使用传播的平凡但可靠的错误代码向外界传播错误。当然如果子系统要求外部代码和子系统使用同样的编译器,那么其实是可以跨边界传播的。 - - 析构函数中不应该抛出异常,如果其中调用了可能抛异常的函数,那么应该在析构中捕获所有可能异常,防止异常向外泄漏。 -- 在这里提到的位置之外使用`catch(...)`经常是不良设计的征兆。 -- 理想情况下,错误可以在模块内部到处顺畅地传播,在模块边界转换(为异常外的错误处理机制),在按策略设置的边界上进行处理。 -- 比较好的实践是定义一些中枢性的函数,在异常和子系统返回的错误代码之间进行转换,统一并简化错误处理机制的转换。 - -### 63. 在模块的接口中使用具有良好可移植性的类型 - -- 摘要:在模块的边缘,必须格外小心。不要让客户不能正确理解的类型出现在外部接口中,应该使用客户代码能够理解的最高层抽象。 -- 很遗憾的是,C++没有指定标准的二进制接口,广泛发布的库可能只能依赖于内置类型和外部世界接口(操作系统提供的接口)。即使在相同的环境中使用不同编译选项编译相同的类型,仍然会生成二进制不兼容的版本。 -- 如果能够控制用户用来构建的编译器版本和选项,那么可以使用任何类型,如果不能,那么就只能使用平台提供类型和C++内置类型。如果不是使用完全相同的标准库映象,那么标准库类型都不能在接口中使用。 -- 提供低层次和高层次的抽象是存在冲突的:低层次接口客户使用返回更广,但是也面临着不安全更容易出现错误的问题,高层次接口更安全,但限制更大,必须要编译器编译选项能够控制。应该视具体情况选择。 -- 即使选择在模块外部接口中提供低层次抽象(可移植的类型),也应该始终在内部使用更高层的抽象。 - -## 模板与泛型 - -### 64. 理智地结合静态多态与动态多态 - -- 摘要:静态多态与动态多态是相辅相成的,理解他们的优缺点,善用他们的长处,结合两者以获得两方面的优势。 -- 同一段代码能够用于不同类型,就叫做多态。 -- 通过公有继承实现的动态多态擅长以下方面: - - 基于超集/子集关系的统一操作。 - - 静态类型检查。 - - 动态绑定和分别编译。 - - 二进制接口兼容。 -- 基于模板的静态多态擅长: - - 基于语法和语义接口的统一操作。 - - 静态类型检查。 - - 静态绑定(不能或者防止分别编译)。 - - 效率。 -- 结合两种多态后: - - 用静态多态辅助动态多态:使用静态多态性实现动态多态的接口,典型应用是CRTP。 - - 用动态多态辅助静态多态:提供泛型、易用的静态绑定接口,但内部又是动态分配的。代表是可识别的类型安全的联合。 - - 任何其他结合。 - -### 65. 有意地进行显式自定义 - -- 摘要:在编写模板时,应该有意地、正确地提供自定义点,并清晰地记入文档。 -- 在模板中提供自定义点主要有三种方式: - - 要求模板参数提供特定成员(特定名字、语义的函数、嵌套类型、数据成员等)。 - - 要求模板参数具有给定名字、签名、语义的非成员函数接口(通过ADL找到)。 - - 第三种选择是模板使用一个类型特征,对这个类型特征提供特化。例子`std::iterator_traits`。 -- 如果自定义点对于内置类型也必须可以自定义,那么应该使用选择2或者3。 -- 为了避免无意提供自定义点,应该做到: - - 将模板内部使用的辅助函数放入自己的内嵌命名空间,或者显式限定他们禁用ADL,比如对于模板参数`T`,`(bar)(t)`将不会进行参数依赖查找(ADL)。 - - 避免使用依赖(非独立)名称,使用`this-> Base::`限定基类名称。 - -### 66. 不要特化函数模板 - -- 摘要:在扩展他人的函数模板时(包括`std::swap`)应该避免编写特化,而是提供重载。将其放在重载所有类型的命名空间中,通过ADL来查找。 -- 原因: - - 函数模板不能偏特化,只能全特化,用途有限。 - - 函数模板特化不参与重载决议。 -- 这也引出了可能有重载的函数模板的正确用法,引入必要的名称之后,使用非限定名称。比如引入`std::swap`后,使用`swap`进行交换。 - -### 67. 不要无意编写不通用的代码 - -- 摘要:依赖抽象而非细节,使用最抽象、最通用的方法实现一个功能。 -- 例子: - - 使用`!=`而不是`<`对迭代器进行比较,前者范围更广。 - - 使用迭代器代替索引访问。 - - 使用`empty()`替代`size() == 0`。 - - 使用层次结构中最高层次的类提供需要的功能。 - - 编写常量正确的代码,只读的情况下就使用`const`。 - -## 错误处理与异常 - -### 68. 广泛使用断言记录内部假设和不变式 - -- 摘要:广泛使用断言`assert`或者类似等价物记录模块内部的各种假设,这些假设时必须成立的,否则就说明存在错误。当然,要确保断言无任何副作用。 -- 断言一般只会在调试模式下生效(在`NDEBUG`宏没有定义时),在发型版中是不存在的,不会对性能造成任何影响。 -- 但千万不要在断言中使用具有副作用的表达式,这会导致调试版和发行版行为不同,是绝对的错误。 -- 在`assert`中使用字符串表示输出信息。 -- 标准的`assert`宏比较简单粗暴,可能需要实现自己的断言。并在发型版本中保留大多数断言(不要处于性能原因禁止检查,除非确实需要)。 -- 自定义的断言可以提供不同级别,在发行版中也可以保留一部分高级别的断言。 -- 不要使用断言报告运行时错误:比如内存分配失败、窗口创建失败、线程启动失败等,这些并不是绝对不应该发生的错误,应该交给异常来做。 -- 总而言之,断言应该用在绝对不应该发生的错误上。发生了,那就是程序员的过错。 - -### 69. 建立合理的错误处理策略,并严格遵守 - -- 摘要:应该在设计早期开发实际、一致、合理的错误处理策略,并严格遵守。 -- 策略应当包含以下内容: - - 鉴别:哪些情况属于错误。 - - 严重程度:每个错误的严重性或紧急性。 - - 检查:哪些代码负责错误检查。 - - 传递:用什么机制在模块中报告和传递错误。 - - 处理:哪些代码负责处理错误。 - - 报告:怎样将错误记入日志或者通知用户。 -- 只在模块边界改变错误处理机制。 - -### 70. 区分错误与非错误 - -- 摘要:违反约定就是错误。 -- 违反函数的前置条件、后置条件、不变式是错误。任何其他情况都不是错误。 - -### 71. 设计和编写错误安全代码 - -- 摘要:如果可以应该提供强保证,至少提供基本保证。 -- 基本保证:出现错误时保证程序会处于有效状态。 -- 强保证:最终状态要么是最初状态、要么是目标状态。 -- 不失败保证:保证操作永远不会失败。 - -### 72. 优先使用异常报告错误 - -- 摘要:应该使用异常而不是错误码来报告错误。但不能使用异常时,可以使用错误码来报告错误已经不是错误的情况。当不可能从错误恢复时,可以使用其他方法,比如终止程序(正常终止或者不正常终止)。 -- 异常的好处: - - 异常不能不加修改地忽略。而错误码可以。 - - 异常是自动传播的。 - - 有了异常处理,就不必在控制流主线中加入错误处理和恢复了。异常处理使错误处理变得清晰。 - - 从构造函数和运算符报告错误,异常要优于其他方案。 -- 异常处理存在一些潜在缺点,它要求程序员熟悉一些反复遇到的惯用法: - - 比如析构函数和释放函数决不能失败。 - - 出现异常是必须保证中间代码是正确的。 -- 异常如果没有被抛出,那么所带来的性能开销是可以忽略的。 -- 当异常被抛出时,会有一定性能损耗,但异常处理绝不会是程序执行的热点代码。频繁的异常抛出与捕获通常意味着程序存在严重问题,将不应该视为错误的情况视为错误抛了出来,此时程序可能会存在严重的性能问题。 -- 例外,在极其罕见的情况下,使用异常可能不是很好: - - 异常的优点不适用:比如调用代码几乎总是必须马上处理错误,这意味着调用方能够知道被会发生的所有错误,且都能正确处理,那么是没有必要使用异常的,因为不需要向上传播,丧失了优点还会带来性能损耗。 - - 抛出异常与使用错误码性能存在显著差距:前面提过,这通常意味着异常被频繁抛出和捕获,也意味着设计存在问题。 -- **不要关闭异常处理**,除了极其性能敏感的模块或程序,基本上没有关闭异常的必要。g++选项`-fno-exceptions`。 -- 严格意义来说,异常不是零开销抽象。不抛异常时程序性能一般不会有太大损耗,但是通常来说会有额外的内存空间消耗。(不准确地凭借经验来说)启用异常的代码相比禁用异常的代码二进制大概会增加30%左右 - -### 73. 通过值抛出,通过引用捕获 - -- 摘要:异常的最佳使用方式是通过值(而不是指针)抛出,通过引用(通常是const)捕获。重新抛出时优先选用`throw;`,避免使用`throw e;`。 -- 值抛出的异常对象会由编译器负责管理其生命周期,不需要程序员操心。 -- 而通过指针抛出则需要程序员对内存的分配和释放负责,如果认为确实有必要这样做,那么可以抛出异常的智能指针。 -- 捕获异常时最好通过引用不会,通过值捕获会有切片问题,这会使异常对象丢失多态性。 -- 重新抛出时应该使用`throw;`而不是`throw e;`,第一种形式是抛出源对象,第二种是重新以值抛出,会丢失多态性。 - -### 74. 正确地报告、处理和转换错误 - -- 摘要:在检查并确认是错误时报告错误。在能够正确处理错误的最近一层处理或者转换错误。 -- 函数检查出一个它自己无法解决的错误而且会使函数无法继续执行时,应该报告错误(比如`throw`)。 -- 以下情况需要转换错误: - - 要添加高层语义。 - - 要改变错误处理机制,比如在模块边界将异常转换为错误码。 -- 如果没有对错误做有用处理的上下文,代码就不应该接收错误。如果函数不准备处理错误,那么它应该允许或者使错误向上传播到能够处理它的调用代码。 -- 例外: - - 接收异常并添加额外信息再重新抛出是有用的,虽然并没有处理错误。 - -### 75. 避免使用异常规范 - -- 摘要:如题。 -- 目前C++标准已经废弃异常规范。 - -## STL:容器 - -### 76. 默认使用vector,否则选择其他合适的容器 - -- 摘要:如果有理由选择某个容器,那么就选择它。如果没有什么特别理由,那么直接选择vector即可。 -- vector具有很多优点: - - 容器中最低空间开销。 - - 所有容器中对存放元素存取最快。 - - 与生俱来的数据局部性。 - - C兼容内存布局。 - - 迭代器最灵活:随机访问。 - - 性能最高迭代器:指针或性能相当的类。 -- 如果有了理由选择其他容器,那么也没有必要考虑vector了,否则无脑vector即可。 - -### 77. 用vecotr和string代替数组 - -- 摘要:如题。 -- 原因:几乎同等性能的前提下,提供了更高层次的抽象,自动管理内存,丰富接口,有助于优化。 - -### 78. 使用vector和string::c_str与非C++API交互 - -- 摘要:vector和string的元素内存保证连续,与非C++API交互时应该使用他们(如C)。 -- 对于vector,可以使用`&*v.begin() &v[0] &v.front() vec.data()`来获取首元素地址。 -- 对于string可以使用`s.c_str() s.data()`获取首字符指针。 -- C++17起可以直接统一为`std::data(obj)`。 - -### 79. 在容器中只存储值和只能指针 - -- 摘要:如题。 - -### 80. 用push_back代替其他扩充序列的方式 - -- 摘要:尽量使用push_back。 -- 如果不需要顺序,那么就应该使用`push_back`,如果很关心顺序,那么大概不应该选择vector。 -- 可以通过`back_inserter`配合标准算法使用`push_back`。 -- 例外:如果插入范围,那么应该使用`insert`。 - -### 81. 多用范围操作,少用单元素操作 - -- 摘要:调用范围操作通常比循环调用单元素操作更加高效和易读。 - -### 82. 使用公用惯用法真正压缩容量,真正删除元素 - -- 摘要:压缩容量,可以使用swap惯用法。真正删除元素,可以使用erase-remove惯用法。 -- 压缩容量:现已有`shrink_to_fit`。 - - 去掉多余容量:`container(c).swap(c)`。 - - 去掉全部内容和容量:`container(c).swap(c)`。 -- 删除元素:`c.erase(std::remove(c.begin(), c.end(), value), c.end())`。 - -## STL:算法 - -### 83. 使用带检查的STL实现 - -- 摘要:即是只在发行前的测试版本中使用,仍然使用带检查的STL实现。 -- 至少保证测试时要使用带检查的STL。 - -### 84. 用算法调用代替手工编写的循环 - -- 摘要:如题。 -- 因为算法库的算法就是精心编写的循环,使用标准库算法更简洁与不易出错。 - -### 85. 使用正确的STL查找算法 - -- 摘要:如题。 -- 查找无序范围:`find/findif count/countif`。 -- 查找有序范围:`lower_bound upper_bound equal_range binary_search`。 -- 如果查找关联容器,那么应该使用同名的成员函数,而不是标准库算法。 - -### 86. 使用正确的STL排序算法 - -- 摘要:如题。 -- 按以下顺序选择排序算法:`partition stable_partition nth_element partial_sort/partial_sort_copy sort stable_sort`。 -- 如果前面的能够达到需求,那么就不需要用更强的版本。 -- 如果不是非用不可,应该不用任何排序算法。比如关联容器和优先队列。 - -### 87, 使谓词成为纯函数 - -- 摘要:如题。 -- 应该总是将函数对象的`operator()`声明为`const`,不在传入算法的函数对象中引入状态。这样才能确保算法的正确性。 - -### 88. 算法和比较器的参数应多用函数对象少用函数 - -- 摘要:如题。 -- 适配性更好,而且比较反直觉的是,产生的代码一般更快。 - -### 89. 正确编写函数对象 - -- 摘要:尽量将函数对象设计成复制成本很低的值类型。 - -## 类型安全 - -### 90. 避免使用类型分支,多使用多态 - -- 摘要:避免使用类型分支,多使用多态。使用模板和虚函数,让类型自己而不是调用他们的代码来决定他们的行为。 -- 通过类型分支定制行为既不牢固、容易出错,又不安全,这是使用C++编写C风格代码。 -- 理想情况下,添加新功能只需要添加代码而不需要修改。即满足开闭原则。 -- 通过基于模板实现的编译期多态或者基于虚函数的动态多态来实现不同类型不同逻辑。 - -### 91. 依赖类型而不是其表示形式 - -- 摘要:不要对对象的内存布局做任何假设。 -- 代码中不应该有任何依赖于对象特定内存布局的逻辑,那通常是不可移植的,甚至不跨编译器版本。 - -### 92. 避免使用reinterpret_cast - -- 摘要:不要尝试使用`reinterpret_cast`,这违反了维护类型安全性的原则。 -- `reinterpret_cast`伴随着程序员对对象表示方式的最强假设,这通常意味着坏的设计。 -- 某些非常特殊的场景可以使用,但通常要配合`std::launder`使用。 - -### 93. 避免对指针使用static_cast - -- 摘要:不要对动态对象的指针使用`static_cast`,安全替代方法很多,包括使用`dynamic_cast`、重构、重新设计。 - -### 94. 避免强制转换const - -- 摘要:强制转换const有时会导致未定义行为,即使合法,也是不良的编程风格。 - - -### 95. 不要使用C风格强制类型转换 - -- 摘要:如题。 -- 对于对象构造这种转换,还有另一个层含义是调用转换构造或者类型转换运算符,其实是可以用的。如`Object(obj) (Object)(obj)`。并且最好用前者,对象构造的形式。 -- 但如果目标类型是复合类型,那么最好还是用C++风格类型转换明确自己要做的是哪一种转换。如`(Object*)p`。 - -### 96. 不要对非POD类型进行memcpy或者memcmp操作 - -- 摘要:如题,包括所有直接的内存操作,包括`memset memmove`等。 -- C++20起,POD(简旧数据类型)已经废弃,新的具名要求是平凡复制类型、平凡类型、标准布局类型。 - -### 97. 不要使用union重新解释表示方式 - -- 摘要:这是比`reinterpret_cast`还要糟糕的C风格用法。不要在C++中这么用。 -- 它做的假设比`reinterpret_cast`还要多。 - -### 98. 不要使用C风格变长参数(...) - -- 摘要:C风格变长参数是来自C语言的危险遗产,应该避免使用。 -- 缺点: - - 缺乏类型安全性。 - - 主调和被调紧耦合。 - - 类类型对象行为未知。 - - 参数数量未知。 - -### 99. 不要使用失效对象,不要使用不安全函数 - -- 摘要:如题。 -- 不要使用失效对象: - - 已销毁对象。 - - 语义失效对象:悬垂指针、引用,失效迭代器。 - - 从来都有效的对象:包括使用`reinterpret_cast`伪造指针获得的对象,或者越界访问获得的对象。 -- 不要使用不安全的C遗产:包括不检查长度的缓冲区写入、拷贝、读取等操作,比如`strcpy strncpy sprintf`等。 - -### 100. 不要多态地处理数组 - -- 摘要:多态地处理数组是绝对的类型错误。 -- 数组有两个用途:作为对象别名和数组迭代器。 -- 将指针作为数组迭代器使用时,绝对不能多态使用,比如将`Derived*`转为`Base*`之后迭代。 -- 在接口中使用引用就表明引用的一个对象,而绝不可能是一个数组。 \ No newline at end of file diff --git a/C++ConcurrencyInAction.md b/C++ConcurrencyInAction.md deleted file mode 100644 index 4cb2407..0000000 --- a/C++ConcurrencyInAction.md +++ /dev/null @@ -1,3 +0,0 @@ -# 《C++并发编程实战》第二版笔记 - -C++并发编程必读数据[《C++并发编程实战》第二版](https://book.douban.com/subject/35653912/),见[tch0/CppConcurrencyInAction](https://github.com/tch0/CppConcurrencyInAction)。 \ No newline at end of file diff --git a/C++DesignPattern.md b/C++DesignPattern.md deleted file mode 100644 index 6c705f4..0000000 --- a/C++DesignPattern.md +++ /dev/null @@ -1,83 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [设计模式——可复用面向对象软件的基础](#%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%8F%AF%E5%A4%8D%E7%94%A8%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%9F%BA%E7%A1%80) - - [创建型模式](#%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F) - - [结构型模式](#%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F) - - [行为型模式](#%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F) - - - -# 设计模式——可复用面向对象软件的基础 - -书籍:《[设计模式——可复用面向对象软件的基础](https://book.douban.com/subject/34262305/)》。 - -## 创建型模式 - -- **抽象工厂**(Abstract Factory):通过抽象工厂创建对象,每个工厂可以创建一类多种具有关联的对象(这些对象具体类型都实现自一组接口类),不同具体工厂创建不同类别对象。 -- **生成器**(Builder):通过生成器对象来创建对象,以自行掌控对象各个部分的创建过程。 -- **工厂方法**(Factory Method):将创建对象的类型确定延迟到子类中。 -- **原型**(Prototype):通过克隆从未知具体类型(从统一接口派生)的已有对象上多态地创建相同类型对象。 -- **单例**(Singleton):整个环境中仅有单个(或有限数量个)对象,从一特定接口获取。 - -## 结构型模式 - -- **适配器**(Adapter):将一个接口转换为另一个接口,以实现已有代码的兼容。 -- **桥接**(Bridge):同一接口可以有不同实现,依赖这个接口编程,在不同环境(比如操作系统)中使用不同实现,以屏蔽环境差异。 -- **组合**(Composite):为拓扑结构中不同具体类型的节点提供统一接口(常见于使用递归组合的树结构中)。 -- **装饰器**(Decorator):和现有对象实现同一接口,动态扩充已有组件功能。 -- **外观**(Facade):将系统中的子系统划分出来,提供统一外观(一组接口),降低系统中各个子系统间的耦合。 -- **共享**(Flyweight):通过一个对象池管理大量共享对象,通过引用共享对象避免创建大量重复对象。 -- **代理**(Proxy):为一个对象提供代理对象以控制对该对象的访问(例子:`vector`的`operator[]`)。 - -比较: -- 适配器、桥接、外观: - - 适配器主要解决两个接口之间不匹配的问题,提供一个中间层实现兼容。使两个已有的不能配合的接口协同工作。在类(适配的那个接口)已经设计好后实施。 - - 桥接则需要考虑多个系统的共性,实现各个系统共有的东西。在设计类(要实现的那个接口)之前实施。 - - 外观模式相比适配器的区别是:外观模式提供一组新的接口,旨在解耦(高内聚低耦合),适配器利用已有接口,旨在让系统各部分能够协同工作。 -- 组合、装饰器、代理: - - 组合旨在递归组合,对拓扑结构中的节点进行统一处理。 - - 装饰器旨在不使用派生类即可给对象添加职责。 - - 组合和装饰器目的不同,具有互补性,通常协同使用。 - - 和装饰器一样,代理旨在为用户提供一致的接口。但与装饰器不同,代理不能动态添加或者分离性质,也不为递归组合而设计。 - - 代理是在直接访问不方便时,对对象提供的一种间接访问方式。装饰器将自身实现为兼容源对象的接口,代理对象则是作为接口的一部分。 - -## 行为型模式 - -- **责任链**(Chain of Responsibility):将请求沿着一个动态的接收者链条(通常实现为链表从前往后或者树结构中从下往上)传播,直到有接收者处理。(典型例子:GUI程序中消息的传播)。 -- **命令**(Command):将请求包装为对象,可用不同的请求对客户进行参数化,可以对请求进行排队、记录日志,以及支持事务、撤销等功能。 -- **解释器**(Interpreter):定义一个DSL,实现一个解释器以实现特定问题的高效、通用化处理。(例子:正则表达式处理字符串)。 -- **迭代器**(Iterator):提供一种不暴露聚合内部表示的顺序访问聚合各个元素的方法。 -- **中介者**(Mediator):通过一个中介对象来封装一系列对象之间的交互,使他们不需要显式相互引用,降低耦合,而且可以独立地改变他们之间的交互方式(通过实现新的中介对象)。(例子:Model-View-Controller模式中的Controller)。 -- **备忘录**(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便能够恢复到原先保存的状态。 - - 维护备忘录具有一定代价,可以配合命令模式通过增量方式改变对象,还能实现撤销与重做。 -- **观察者**(Observer):定义对象间的一种一对多(并且松耦合——通过抽象层耦合)的依赖关系,当一个对象状态改变,通知所有依赖于它的对象进行更新。(GUI程序中常见,比如QT) -- **状态**(State):允许一个对象在内部状态改变时改变它的行为,看起来像是修改了类一样。(可以用于实现状态机) -- **策略**(Strategy):定义一系列算法,封装起来,使他们可以在运行时相互替换。使算法可以独立于客户而变化。 -- **模板方法**(Template Method):定义一个算法的骨架,将一些步骤延迟到子类中,使得在不改变算法结构的情况下重定义某些步骤。 -- **访问者**(Visitor):访问者表示一个作用于某对象结构内部个元素的操作,通过将访问者传入对象以实现访问操作。(可以将策略、解释器等对象作为访问者,访问者可以作用于递归组合起来的对象结构) - -讨论: -- **封装变化**:当程序某方面的特征可以变化时,这些模式把这种变化封装起来定义为一个可变的对象。这可以说是所有设计模式的核心思想之一,因为不再依赖变化部分的具体实现,封装变化天然能够降低耦合。 - - 职责链将请求接收方封装为对象,并链在一起。 - - 命令对象将一个可变的动作或者请求封装为对象。 - - 解释器对象将一种DSL或一种DSL实现封装为对象。 - - 迭代器对象封装访问和遍历聚集对象内各个元素的方法。 - - 中介者对象封装对象间的协议。 - - 备忘录模式将可能变化的对象内部状态封装为对象。 - - 状态对象封装与状态相关的行为。 - - 策略对象封装算法。 - - 访问者对象将不同的访问操作封装为对象。 -- 对象作为参数:一些模式引入总是作为参数的对象。 - - 访问者模式将访问者作为参数。 - - 命令模式和备忘录模式将对象作为参数到处传递。 -- 封装通信行为还是分发每一次通信行为: - - 中介者对象将对象间的通信行为封装在内部。 - - 观察者通过通知显式的发送接收行为来通信。 -- 发送者接收者解耦: - - 当合作的对象间互相引用时,他们变得相互依赖,这对系统的分层和复用产生负面影响。 - - 将请求包装为命令对象可以让发送方接收方松耦合。 - - 观察者模式通过动态注册绑定发送者和接收者,将发送者与具体的接收者解耦,通过接口类耦合与接收方的具体实现解耦。 - - 使用中介者对象作为发送方和接收方的中介可以使发送接收方松耦合。 - - 职责链模式中将接收方串成一条链使得发送方和最终接收方解耦。 diff --git a/C++NewStandard.md b/C++NewStandard.md deleted file mode 100644 index b72f4db..0000000 --- a/C++NewStandard.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++新标准学习 - -见[tch0/CppNewStandard](https://github.com/tch0/CppNewStandard)。 \ No newline at end of file diff --git a/C++ObjectModel.md b/C++ObjectModel.md deleted file mode 100644 index 414fb00..0000000 --- a/C++ObjectModel.md +++ /dev/null @@ -1,3 +0,0 @@ -# 深度探索C++对象模型 - -《深度探索C++对象模型》笔记,见[tch0/CppObjectModel](https://github.com/tch0/CppObjectModel)。 \ No newline at end of file diff --git a/C++Primer.md b/C++Primer.md deleted file mode 100644 index 52f2c8e..0000000 --- a/C++Primer.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++ Primer笔记 - -任何学习C++的人入门必看,不多说。主要记录非常重要的细节和练习代码。内容比较多,见仓库[tch0/CppPrimer](https://github.com/tch0/CppPrimer)。 \ No newline at end of file diff --git a/C++STL.md b/C++STL.md deleted file mode 100644 index eec0fe9..0000000 --- a/C++STL.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++标准库学习 - -参见仓库 [tch0/CppSTL](https://github.com/tch0/CppSTL)。 \ No newline at end of file diff --git a/C++TemplateMetaProgrammingInAction.md b/C++TemplateMetaProgrammingInAction.md deleted file mode 100644 index 21f550f..0000000 --- a/C++TemplateMetaProgrammingInAction.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++模板元编程实战 - -《[C++模板元编程实战:一个深度学习框架的初步实现](https://book.douban.com/subject/30394402/)》的代码记录,见[tch0/CppTemplateMetaProgrammingInAction](https://github.com/tch0/CppTemplateMetaProgrammingInAction)。 \ No newline at end of file diff --git a/C++TemplateProgramming.md b/C++TemplateProgramming.md deleted file mode 100644 index dfea47e..0000000 --- a/C++TemplateProgramming.md +++ /dev/null @@ -1,3 +0,0 @@ -# C++模板编程学习 - -见[tch0/CppTemplateProgramming](https://github.com/tch0/CppTemplateProgramming)。 \ No newline at end of file diff --git a/C++ToDo.md b/C++ToDo.md deleted file mode 100644 index c9b9807..0000000 --- a/C++ToDo.md +++ /dev/null @@ -1,17 +0,0 @@ -# 关于C++还需要学习的东西 - -看情况以后逐步实践中学习的东西。 - -C++库: -- Boost -- Asio -- 各个编译器更多使用细节 -- 协程 -- 了解C++反射库,自己实现 - -工具链相关: -- xmake -- bazel -- conan -- [Itanium C++ ABI](http://itanium-cxx-abi.github.io/cxx-abi/) -- profiling工具:gpreftools \ No newline at end of file diff --git a/CMake.md b/CMake.md deleted file mode 100644 index 25f5793..0000000 --- a/CMake.md +++ /dev/null @@ -1,407 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [CMake](#cmake) - - [1. 基础](#1-%E5%9F%BA%E7%A1%80) - - [1.1 入门](#11-%E5%85%A5%E9%97%A8) - - [1.2 头文件](#12-%E5%A4%B4%E6%96%87%E4%BB%B6) - - [1.3 生成并链接静态库](#13-%E7%94%9F%E6%88%90%E5%B9%B6%E9%93%BE%E6%8E%A5%E9%9D%99%E6%80%81%E5%BA%93) - - [1.4 生成动态链接库](#14-%E7%94%9F%E6%88%90%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%BA%93) - - [1.5 安装](#15-%E5%AE%89%E8%A3%85) - - [1.6 构建类型](#16-%E6%9E%84%E5%BB%BA%E7%B1%BB%E5%9E%8B) - - [1.7 设置编译选项](#17-%E8%AE%BE%E7%BD%AE%E7%BC%96%E8%AF%91%E9%80%89%E9%A1%B9) - - [1.8 查找第三方库](#18-%E6%9F%A5%E6%89%BE%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93) - - [1.9 使用Clang编译](#19-%E4%BD%BF%E7%94%A8clang%E7%BC%96%E8%AF%91) - - [2. 子项目](#2-%E5%AD%90%E9%A1%B9%E7%9B%AE) - - [3. 实践](#3-%E5%AE%9E%E8%B7%B5) - - [3.1 模板](#31-%E6%A8%A1%E6%9D%BF) - - [3.2 常用参数](#32-%E5%B8%B8%E7%94%A8%E5%8F%82%E6%95%B0) - - - -# CMake - -首先[CMake](https://cmake.org/)是什么? -- 一个开源、跨平台的构建、测试、打包工具。CMake使用简单的编译器无关的配置文件来控制代码编译过程、生成Makefile和特定IDE的工程文件。最新版本是v3.20.x,[文档](https://cmake.org/documentation/)。CMake做的事情就是Windows中IDE比如VS,Linux中的Makefile做的事情。 - -为什么要使用CMake? -- CMake广泛用于C/C++语言,但也可以用于其他语言。如果写C++时有跨平台的需求,用CMake构建就可以忽略编译器和平台的差异,同时CMake相比Makefile这种基本专用于类Unix平台的工具更加简单,用CMake可以避免许多头发的掉落,减少心智负担。越来越多的人使用CMake,开源的C++项目基本都会使用CMake。 - -如何学习CMake? -- [CMake Reference Documentation](https://cmake.org/cmake/help/v3.20/) -- [知乎问题 - CMake 如何入门?](https://www.zhihu.com/question/58949190) - - -CMake做一些什么事情? -- C/C++代码需要跨平台编译,那首先代码应该是跨平台的,跨平台是代码应该做的事情,构建工具只负责指定包含路径和链接差异。 -- CMake调用编译器如MSVC、gcc进行编译。 - - -CMake使用文件`CMakeLists.txt`来管理工程的构建。 - - -具体教程:- [ttroy50/cmake-examples](https://github.com/ttroy50/cmake-examples) - - -## 1. 基础 - -### 1.1 入门 - -当CMake在一个目录下工作时,会首先寻找并使用`CMakeLists.txt`文件,如果没有则会报错退出。 - -最小支持的CMake版本: -```cmake -cmake_minimum_required(VERSION 3.19) -``` - -指定项目名称,在有多个项目时就需要用项目名称引用某一个项目: -```cmake -project (project_name) -``` - -创建可执行文件: -`add_executable()`命令指定针对特定的源文件应该生成一个什么名称的可执行文件。 -```cmake -add_executable(executable_name source_file_list) -``` -一般来说将可执行文件名称指定为项目名一致,所以可以这样写: -```cmake -cmake_minimum_required(VERSION 3.19) -project (hello_cmake) -add_executable(${PROJECT_NAME} main.cpp) -``` -其中`project()`函数会创建名为`${PROJECT_NAME}`值为`hello_cmake`。 - -二进制文件输出目录: -- 运行CMake的目录会保存在`CMAKE_BINARY_DIR`成为二进制文件的顶层目录。 -- CMake支持就地生成(In-Place Build)二进制文件(即把对象文件生成到源文件同一个目录,最后生成的二进制文件和源文件拥有同样的目录结构),也可支持统一生成在其他位置。 - -就地生成: -- 将所有临时文件生成在源文件同目录下,执行命令`CMake .`即是就地生成。 - -生成在其他位置: -- 执行 `mkdir bulid` `cd ./build` `make ..` 即是生成在新建的`build`目录下,参数是`CMakeLists.txt`所在目录。一般来说实践中大部分情况都是这样用。 - -如果编译器是MSVC(Windows环境下),则会生成VS的解决方案和项目文件,如果在Linux下默认编译器会是GCC则会生成`Makefile`。然后使用VS或者Make来进一步生成库或者可执行文件。 - -### 1.2 头文件 - - -CMake使用[一系列变量](https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/Useful-Variables)来表示源码中一些有用的目录: - - -|变量|含义| -|:-|:-| -| CMAKE_SOURCE_DIR| The root source directory| -| CMAKE_CURRENT_SOURCE_DIR| The current source directory if using sub-projects and directories.| -| PROJECT_SOURCE_DIR| The source directory of the current cmake project.| -| CMAKE_BINARY_DIR| The root binary / build directory. This is the directory where you ran the cmake command.| -| CMAKE_CURRENT_BINARY_DIR| The build directory you are currently in.| -| PROJECT_BINARY_DIR| The build directory for the current project. | - -主要就是源文件目录和二进制文件目录。 - -对于一个下列结构的包含头文件的项目: -``` -├── CMakeLists.txt -├── include -│ └── Hello.h -└── src - ├── Hello.cpp - └── main.cpp -``` - -可以创建一个`SOURCES`变量来保存所有源文件以用来编译: -```cmake -# Create a sources variable with a link to all cpp files to compile -set(SOURCES - src/Hello.cpp - src/main.cpp -) -``` - -也可以使用`GLOB`命令加通配符查找所有匹配的源文件并指定给`SOURCES`变量: -```cmake -file(GLOB SOURCES "src/*.cpp") -``` - -现代CMake不推荐使用`set`直接设置源文件变量,直接使用`add_xxx`函数会更加方便。 - -当头文件和源文件分不同目录存放时,如果不是在源文件中`#include`使用相对路径从而能够查找到头文件,那么就需要在编译器参数(或者工程文件,最终都体现在编译器参数)中指定包含目录以确保编译器能够找到头文件。 - -在CMake对于头文件则使用`target_include_directories`([相关细节](https://cmake.org/cmake/help/v3.0/command/target_include_directories.html)),包含目录最后会体现到编译参数上: -```cmake -# Set the directories that should be included in the build command for this target -# when running g++ these will be included as -I/directory/path/ -target_include_directories(hello_headers - PRIVATE - ${PROJECT_SOURCE_DIR}/include -) -``` - -`PRIVATE`标识符包含的作用域,这对于库来说很重要。 - -`make`执行时不会显示详细信息,可以使用`make VERBOSE=1`显示详细编译信息以帮助调试。 - -### 1.3 生成并链接静态库 - -这里在同一个项目中使用静态库链接,但通常我们会将静态库配置为一个单独的项目,后面会详述。 - -目录结构: -```cmake -├── CMakeLists.txt -├── include -│ └── static -│ └── Hello.h -└── src - ├── Hello.cpp - └── main.cpp -``` - -`add_library()`函数用来向库中添加源文件,这会用来创建一名为`libhello_library.a`的静态链接库(Linux下,Windows下则是`hello_library.lib`): -```cmake -add_library(hello_library STATIC - src/Hello.cpp -) -``` - -然后声明头文件目录(`static/`这一级目录在源文件中指定): -```cmake -target_include_directories(hello_library - PUBLIC - ${PROJECT_SOURCE_DIR}/include -) -``` - -使用`PUBLIC`会导致其中的包含目录用于以下地方: -- 编译这个库时。 -- 编译链接了这个库的目标时。 - -对于`target_include_directories`来说三种不同作用域含义: - -|关键字|含义| -|:-|:-| -|PRIVATE|目录被添加到目标的包含目录中| -|INTERFACE|目录被添加所有链接这个库的目标的包含目录| -|PUBLIC|综合PRIVATE和PUBLIC,目录被添加到目标包含目录和链接这个库的目标的包含目录| - -对于项目中的公共头文件来说,一个常见的实践是: -- 使用一个子目录来区分不同的作用域,比如上面的`include/static/`目录。 -- 在`target_include_directories`传入包含目录的根目录,即是`include/`。 -- 然后在源文件中使用这个子目录来区分不同库的包含目录,`#include "static/Hello.h"`。 -- 这样使用会有效减少一个项目使用了多个库时可能导致的文件名冲突。 - -链接一个库则使用`target_link_libraries` 函数: -```cmake -add_executable(hello_binary - src/main.cpp -) - -target_link_libraries( hello_binary - PRIVATE - hello_library -) -``` - -这样`hello_binary`可执行文件在链接时就会链接`hello_library`库。 - -### 1.4 生成动态链接库 - -示例文件结构: -``` -├── CMakeLists.txt -├── include -│ └── shared -│ └── Hello.h -└── src - ├── Hello.cpp - └── main.cpp -``` - -添动态链接库: -```cmake -add_library(hello_library SHARED - src/Hello.cpp -) -``` - -这会创建一个`libhello_library.so`或者`hello_library.lib/.dll`的动态链接库。 - -`add_library`也可以用来创建一个目标别名: -```cmake -add_library(hello:library ALIAS hello_library) -``` - -然后当引用这个库链接到其他库或可执行文件时就可以使用这个别名了(只能在只读环境中使用)。更多解释参考[文档](https://cmake.org/cmake/help/v3.0/manual/cmake-buildsystem.7.html#alias-targets)。 - -链接到动态库: -```cmake -# Add an executable with the above sources -add_executable(hello_binary - src/main.cpp -) - -# link the new hello_library target with the hello_binary target -target_link_libraries( hello_binary - PRIVATE - hello::library # alias above -) -``` - -可以看到和静态库区别就只有`add_library`中用`STATIC`还是`SHARED`而已。 - -当然动态库的跨平台需要由代码来实现,Windows中需要`__declspec(dllexport)/__declspec(dllimport)`声明导出导入的类和函数等。Linux中则不需要,一般通过宏来实现,在Windows中有导出符,Linux中定义为空。 -```C++ -#ifdef _MSC_VER -#ifdef XXX_DLL -#define DLL_EXPORT_IMPORT __declspec(dllexport) -#else -#define DLL_EXPORT_IMPORT __declspec(dllimport) -#endif -#else -#define DLL_EXPORT_IMPORT -#endif -``` - -在头文件声明中要导出的类或者函数前添加导出符`DLL_EXPORT_IMPORT`,每个动态库写一份上述宏定义被该模块所有头文件使用,`XXX_DLL`定义为该模块特有的名称,并在该模块预处理器中添加该宏的定义。主要为了Windows和Linux的动态库跨平台编译。 - -### 1.5 安装 - - -### 1.6 构建类型 - -CMake可以使用不同配置来构建一个项目,可以指定不同的优化级别,: -- Release - 编译时添加`-O3 -DNDEBUG`标记。 -- Debug - 添加`-g` 标记。 -- MinSizeRel - `-Os -DNDEBUG`。 -- RelWithDebInfo - `-O2 -g -DNDEBUG`。 - -通过执行时执行给`CMAKE_BUILD_TYPE`来确定: -```shell -cmake .. -DCMAKE_BUILD_TYPE=Release -``` - -默认情况下不添加任何标记,可以在`CMakeLists.txt`中设置默认标记: -```cmake -# Set a default build type if none was specified -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message("Setting build type to 'RelWithDebInfo' as none was specified.") - set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" - "MinSizeRel" "RelWithDebInfo") -endif() -``` - - -### 1.7 设置编译选项 - -- 使用`target_compile_definitions`函数。 -- 设置给`CMAKE_C_FLAGS`或者` CMAKE_CXX_FLAGS`变量。 - - -为每一个C++目标设置: -```cmake -target_compile_definitions(hello_exe - PRIVATE EX3 -) -``` - -则编译`hello_exe`时会添加`-DEX3`选项。如果目标是库,那么选择`PUBLIC`或者`INTERFACE`同样会决定头文件包含关系。 - -设置默认编译链接选项: -- `CMAKE_C_FLAGS` C编译选项。 -- `CMAKE_CXX_FLAGS` C++编译选项。 -- `CMAKE_LINKER_FLAGS` 链接选项。 - -这几个标记是全局的,所有目标都会使用。一般来说推荐使用`target_compile_definitions`。 - -同理可以在`CMakeLists.txt`中设置或者cmake命令参数中设置。 - -### 1.8 查找第三方库 - -`find_package`函数可以用来查找已安装第三方库。 - -### 1.9 使用Clang编译 - -CMake中用来控制编译器链接器的变量: -- `CMAKE_C_COMPILER`,C编译器。 -- `CMAKE_CXX_COMPILER`,C++编译器。 -- `CMAKE_LINKER`,链接器。 - -通过命令行传入或者在`CMakeLists.txt`中使用`set`设置可修改。 -```shell -cmake .. -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -``` - - -## 2. 子项目 - -在子项目的子目录中编写`CMakeLists.txt`,分开写即可。 - -然后在主`CMakeLists.txt`中添加对应的子项目: -```cmake -add_subdirectory(sub_project) -``` - - - -## 3. 实践 - -### 3.1 模板 - -```cmake -cmake_minimum_required(VERSION 3.20) -​ -project(CMK-Hello - VERSION 0.0.1 - DESCRIPTION "A Test Project" - LANGUAGES CXX -) -# set language std -set(CMAKE_CXX_STANDARD 17) -# set language std and Disable fallback to a previous version -set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Disable CXX Syntax EXTENSIONS -set(CMAKE_CXX_EXTENSIONS OFF) -​ -​ -set(STATIC_HEADER "include/static/") -add_library(myLib STATIC src/STD_Suggestion.cpp ${STATIC_HEADER}STD_Suggestion.h) -​ -# for #include "static/Suggestion.h" -target_include_directories(myLib - INTERFACE - include/ -) -​ -set(SOURCES - src/hello.cpp -) -​ -add_executable(HelloExe ${SOURCES}) -​ -target_link_libraries(HelloExe - PRIVATE - myLib -) -``` - -### 3.2 常用参数 - -cmake常用参数: -- `-A x64` 指定平台。 -- `-D` 指定变量的值。 -- `-S` 源目录。 -- `-B` 构建目录。 -- `-G` 选择生成器,如`Visual Studio 15 2017`。 -- `-T` 指定编译器,如果生成器支持的话。 - -一般构建流程: -```shell -mkdir build -cd ./build/ -cmake .. -make -``` \ No newline at end of file diff --git a/CSAPP.md b/CSAPP.md deleted file mode 100644 index 525cd96..0000000 --- a/CSAPP.md +++ /dev/null @@ -1,3 +0,0 @@ -# 深入理解计算机系统 - -《[深入理解计算机系统](https://book.douban.com/subject/26912767/)》第三版笔记,见[tch0/CSAPP](https://github.com/tch0/CSAPP)。 \ No newline at end of file diff --git a/CTrapsAndPitFalls.md b/CTrapsAndPitFalls.md deleted file mode 100644 index 26fa1b1..0000000 --- a/CTrapsAndPitFalls.md +++ /dev/null @@ -1,107 +0,0 @@ -# C陷阱与缺陷 - -书籍《[C陷阱和缺陷](https://book.douban.com/subject/2778632/)》的笔记,书内容并不多,稍有些许经验的C程序员应该都遇到过大部分问题。成书很早于ANSI C标准发布前夕,但是其中内容确实是经久不衰且常见的,基本都遇到过,值得一读。 - -## 词法 - -- `=` 不同于 `==`。 -- `& |` 不同于 `&& ||`。 -- 词法分析中的贪心法:优先解析将多个符号构成的字符序列解析为尽可能长的运算符。 -- 八进制整型常量以`0`开头,不能使用数字`8 9`。 -- 区分字符与字符串,单引号与双引号,不可互相替代。 - - -## 语法 - -- 函数声明可以按照解方程的方式来理解。怎么声明怎么用,理解`(*(void(*)()0))()`,类型声明同理。 -- 函数返回一个函数有两种写法:`void (*(f()))()`或者使用类型别名`typedef void (*g_t)(); g_t f();`,虽然返回值类型是`void (*)()`但并不能直接写作`(void (*)()) f();`,使用后者会更清晰。 -- 运算符优先级:一元后缀、一元前缀、算术运算、移位运算、比较运算、按位运算、逻辑运算、三元条件运算、赋值、逗号。注意结合性,一元前缀、赋值和三元条件运算符是右结合,其他都是左结合。应尽量使用括号明确优先级。 -- 多余的分号的影响,主要在循环中。 -- `switch`语句,是否每个`case`都需要`break`。 -- 函数必须使用括号调用。 -- `else`总是与最近的`if`匹配,避免出现悬垂的`else`,`if-else`块最好使用`{}`。 - -## 语义 - -- 数组和指针是不同的类型,尽管数组很多时候可以退化成指针,`sizeof`运算符结果是不一样的,理解多维数组。`[]`就是指针偏移然后解引用操作的语法糖。 -- 留意字符串末尾的空字符,`strlen`长度并不是字符串内存的长度。 -- 数组作为形参时自动退化为指针,退化之后不再有长度信息,传递时也是。 -- 指针复制是浅拷贝。 -- 空指针不是空字符串,特别用在`printf`中效果不一样。 -- 数组循环的边界通常取半闭半开区间,前闭后开,应尽量使用不对称的边界。 -- 求值顺序,C语言中仅`&& || ?: ,`存在规定的求值顺序,不应该假定任何其他运算符的求值顺序。 -- 区分逻辑运算`&& || !`和按位运算`& | ~`。 -- 需要详细考虑整数边界和整数运算溢出,特别地需要考虑有符号数中负数比正数多一个。 -- `main`应该有一个`int`返回值返回给操作系统,最好不要省略`int`返回值类型声明。 - -## 链接 - -- 分别编译,链接器并不知道C语言的细节,仅处理目标文件中符号的引用。 -- 区分声明和`extern`声明,前者同时定义变量,分配内存空间,后者只是对外部变量的引用。 -- `static`修饰的全局变量和函数是文件作用域。 -- 不要使用隐式函数声明(即不声明直接用,默认为返回`int`的函数)、省略函数返回值类型(默认返回`int`)、声明时不写参数列表,这些在ANSI C中都是合法的。 -- 外部声明`extern`和定义时类型一定要严格一致,否则链接可能发现不了导致奇怪的错误(能读能写但读出的值不一样)。 -- 外部声明可以放在头文件中,由某一个源文件定义。 - -## 库函数 - -- `getchar`函数返回整型,最好使用整型来接,使用字符可能无法和`EOF`比较(现在来说用`char`一般也没问题)。 -- `fopen`打开文件交错进行读写,需要进行`fseek`。 -- `setbuf`为输出流分配缓冲,`fflush`刷新缓冲。设置缓冲后如果输出过程中出错,定位错误原因时可能就会找错地方,因为有的数据还在缓冲区没有被输出,可以在调试版本的程序中强制不允许对输出进行缓冲。 -- 可以使用`errno`检测库函数出现的错误,但应该首先检测函数返回值,如果失败再来查看`errno`确定失败原因。 -- `signal`函数是真正意义上的异步,用于注册捕获的异步事件。一个信号可能在C程序执行期间的任何时刻发生,甚至于某些复杂的库函数如`malloc`中,应避免在处理程序中调用此类函数,处理程序应尽量简单。唯一可移植并安全的处理逻辑是打印出错消息然后使用`longjump`或`exit`立刻退出程序。 - - -## 预处理器 - -- 带参宏中的空格,宏名和`()`之间不能有空格。 -- 宏不是函数,也可以理解为传名调用的函数,可能会对传入的参数多次求值,需要特别注意。 -- 宏只是符号替换,也不是语句,像`assert`宏就需要避免使用`if`语句造成可能的`else`的匹配问题。 -- 宏并不是类型定义,使用`typedef`进行类型定义更好。 - -## 可移植性缺陷 - -- 程序尽可能应对标准变更。 -- 整数的大小在不同机器上可能不一致。最好定义一套别名,就算需要更换类型也只改一次就行。当然以现在的观点来看,已经有了`stdint.h`使用定长类型就好。 -- 字符是有符号的还是无符号的,这是取决于实现的,如果是有符号,那么`0x7f-0xff`之间的字符转为更宽的整数时会进行符号扩展,需要特别注意。可以先转换为`unsigned char`以避免。 -- 移位运算符,注意有符号整数、无符号整数的右移由0还是符号位的副本填充,注意移位允许的取值范围不能超过数的长度。 -- 对于空指针`NULL`,机器上是否支持读写地址0处的值(现代的机器上一般都不支持),使用指针前先做判断是必要的。 -- 除法运算发生的截断,目前的实现来说,余数与被除数符号相同,需要注意这点。 -- 随机数大小有`RAND_MAX`宏指定,随机数发生的逻辑是否可移植,不同机器上随机数范围是否一致。 -- `malloc realloc free`一些老的Unix机器上要求`realloc`之前需要`free`,`free`之后还可以`realloc`,不应该依赖特定实现来编程。 - -## 实践建议 - -- 直截了当表达意图,如果程序看起来可能被人误解,应使用加括号或重写等手段明确意图。 -- 考察最简单的特例:构思程序和测试程序时都是必要的。比如空数据、只有一个元素、整数边界等情况。程序设计时还可以从空数据等边界情况的处理获得启发。 -- 使用不对称边界。 -- 坚持使用语言中更众所周知的部分,避免使用生僻的语言特性。 -- 防御性编程,不要对程序的用户和编译器实现做过多的假设。 - -## 可变参数 - -大部分人可能对使用很多的`printf`函数族的实现原理了解甚少。同族函数还有`fprintf sprintf`。 -- `printf(stuff)`等价于`printf(stdout, stuff)`。 -- 这三个函数返回值都是已传送字符数。`sprintf`会在末尾打印一个空字符,不计入传送的字符数中。 -- 由于运行时才拿到格式化参数以决定类型,由编译器来检查参数类型是很困难的。 -- `printf("%c", c)`和`putchar(c)`等价。 -- 格式化中可以加入`l`长度修饰符、宽度修饰符、精度修饰符、对齐或者符号修饰。 - -两种不同的变长参数:`varargs.h`和`stdarg.h`。前者最早源于1981年,后者是标准ANSI C的支持,就目前来说前标准C语言一般都不支持前者。而是支持C语言标准中的后者,比如LLVM或者GCC,包含了`varargs.h`之后都是直接报错并推荐使用`stdarg.h`,MSVC中还保有支持。总之前者已经不再支持,不需要了解。后者的实现是这样的: -```C -int my_printf(char* format, ...) -{ - va_list args; - va_start(args, format); - double d = va_arg(args, double); - printf("%lf", d); - vprintf(format, args); - va_end(args); -} -``` -- C标准库中提供了接受了`va_list`类型的`printf`函数族。可以直接使用`va_list`参数。 -- 使用`va_list`前必须调用`va_start va_end`初始化以及结束可变参数的访问。 -- 使用`Type var = va_arg(list, Type)`从`list`中获取指定类型`Type`的变量的值到`var`。 -- `va_list`指向栈帧,通过`va_arg`宏中的类型做`sizeof`之后取出指定字节大小解释为特定的类型。所以如果变量类型提取错误,那么结果肯定是错误的。 -- `printf`正是根据格式化字符串中的占位符个数和类型来判断和提取特定数目指定类型的参数,如果占位符和参数不匹配,那么输出结果就是错误的。 -- [可变参数文档](https://zh.cppreference.com/w/c/variadic)。 \ No newline at end of file diff --git a/CategoryTheory.md b/CategoryTheory.md deleted file mode 100644 index 5bb693a..0000000 --- a/CategoryTheory.md +++ /dev/null @@ -1,121 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [范畴论了解](#%E8%8C%83%E7%95%B4%E8%AE%BA%E4%BA%86%E8%A7%A3) - - [背景知识——群](#%E8%83%8C%E6%99%AF%E7%9F%A5%E8%AF%86%E7%BE%A4) - - [群(Group)](#%E7%BE%A4group) - - [群同态(group homomorphism)](#%E7%BE%A4%E5%90%8C%E6%80%81group-homomorphism) - - [群同构(group isomorphism)](#%E7%BE%A4%E5%90%8C%E6%9E%84group-isomorphism) - - [生成新群](#%E7%94%9F%E6%88%90%E6%96%B0%E7%BE%A4) - - - -提示:本文含有少量公式,可安装[MathJax Plugin for Github](https://github.com/orsharir/github-mathjax)浏览器插件提供公式渲染,但这样做并非直接解析源文本,而是在Markdown渲染结果的基础之上做,终究存在一些蛋疼的问题: -- 很多时候公式中的$ * $被Markdown渲染成了斜体标签``,导致显示问题。避免这个问题可以在公式中的$ * $两端加上空格。 -- $\LaTeX$中大括号需要使用`\{`转义,但Markdown渲染为html网页时本身就会对其进行一次转义。所以只有`\\{`或者`\\\{`才能被Mathjax正常解析为左花括号,而`\{`就什么都没有了。 -- 不是原生引入的Mathjax所以没办法,不应该本末倒置为了Mathjax能够渲染而使用不规范不正确的$\LaTeX$语法,既然绕不过最好还是克隆到本地查看吧。 - -# 范畴论了解 - -简单了解一下范畴论,也谈不上入门,不求深入。主要是为了对Haskell中各种类型类有更好的理解。 - -符号一览: - -|符号|含义|$\LaTeX$写法| -|:-:|:-:|:-:| -|$\not$|符号取反|`\not`| -|$\subset \subseteq$|集合关系|`\subset \subseteq`| -|$\land \lor \lnot$|逻辑与、或、非|`\land \lor \lnot`| -|$\ni \in \notin \not\ni$|元素与集合关系|`ni \in \notin \not\ni`| -|$\to \Rightarrow \Leftrightarrow$|函数、逻辑推导|`\to \Rightarrow \Leftrightarrow`| -|$\forall \exists$|全称、存在量词|`\forall \exists`| -|$\mathcal{ABHG}$|手写体字母,表示群|`\mathcal{ABHG}`| -|$\sim \cong$|等价、同构|`\sim \cong`| -|$f\circ g$|函数、群同态复合|`f \circ g`| - - -纯数学家研究的是不同的抽象结构,但如果我们把不同的数学结构,如群、偏序、拓扑空间等,进行进一步的抽象,研究结构之上的结构,这就是**范畴**(category)。若再度抽象,我们就得到了**函子**(functor),再往上就是**自然变换**(natural transformation)。范畴论还可以继续研究抽象的抽象,直至无穷。 - -## 背景知识——群 - -背景知识的背景知识:集合、函数、关系。 - -### 群(Group) - -**定义1**:有着非空集合$G$和它的一个二元运算$*$,若满足: -- **封闭性**: $\forall a,b\in G \exists c \in G(a * b = c)$。 -- **结合律**: $\forall x,y,z \in G((x * y) * z = x * (y * z))$ 。 -- **单位元存在**:$\exists e \in G \forall x \in G(x * e = x = e * x)$ , $e$称之为单位元,也称幺元。 -- **逆元存在**:$\forall x \in G \exists y\in G(x * y = e = y * e)$,称$x$和$y$为互逆元素,简称逆元(inverse),$y$可以记做$x^{-1}$。 - -则称$G$对$x$构成一个**群**。 - -记法: -- 三个要素$G$,$*$,$e$的三元组。 -- 有记做$(G, *)$的,有记做$ (G, *, e)$的。随意?只要能理解就行,在没有歧义的情况下也可以简写做$G$。 - -通常称: -- $G$上的的二元运算 $* $ 为乘法,称 $a*b$ 为 $a$ 和 $b$ 的积,简写做 $ab$。 -- 群$G$元素个数有限,在称为有限群,反之无限群,有限群的元素个数称为有限群的阶。 - -运算: -- $g\in G, H \subseteq G$,定义$g* H = \{gh|h\in H\}$,简写作 $gH$, $H*g = \{hg|h\in H\}$,简写作 $Hg$。 -- $A,B \subseteq G$,定义$A*B = \{ab|a\in A, b\in B\}$,简写做$AB$。 -- $H\subseteq G$,记 $H^{-1} = \{h^{-1}|h\in H\}$。 - -替换定理:若$(G, *)$是群,那么$\forall g \in G(gG = Gg = G)$。 - -子群:若$(G, *, e)$是群,那么$H$是$G$的非空子集且$(H, *, e)$也是群,那么称$H$为$G$的**子群**(subgroup)。 - -子群的判定:$HH=H\land H^{-1}=H \Leftrightarrow H是G的子群$。 - -例: -- 整数对加法运算,以$0$为单位元构成群,记做$(Z,+,0)$。 -- 非零实数对乘法,以$1$为单位元构成群。 - -### 群同态(group homomorphism) - -**定义2**:群$(G, *, e)$到群$(G^{'}, *^{'}, e^{'})$的一个群同态,是定义在集合$G$上(定义域),以$G^{'}$元素为取值(陪域)的函数$f$,使得: -- $\forall x,y \in G(f(x*y) = f(x) *^{'} f(y))$ -- $f(e) = e^{'}$ - -则称$f : G\to G^{'}$称之为底层函数(underlying function),而记$f:(G,*,e)\to (G^{'}, *^{'}, e^{'})$为**同态**。 - -例: -- 任何群到单位群 $1$ 的映射都可以视为一个同态。 -- 从整数群到有理数群存在这样的一个同态:$h:(Z,+,0) \to (Q,+,0)$ 。这个同态是**单射**(injective),但不是**满射**(surjective)。 - -**定理1**: -- 设 $\mathcal{G} = (G, *, e)$,那么存在一个**单位同态**(identity homomorphism)$1_{\mathcal{G}}:\mathcal{G}\to\mathcal{G}$,把$\mathcal{G}$中所有元素投射给自己。 -- 对于两个同态$f:\mathcal{G}\to\mathcal{H},g:\mathcal{H}\to\mathcal{J}$,总可以将其复合构成新同态 $g\circ f:\mathcal{G}\to\mathcal{J}$。 -- 同态的**合成**/**复合**(composition)是**结合**的(associative),即满足结合律。 -- 符号说明:$\mathcal{G}$是手写体的$G$,写法`\mathcal{G}`。 - -### 群同构(group isomorphism) - -**定义3**:如果底层函数是一个**双射**(bijection),那么称群同态为群同构,记做 $\mathcal{G}\cong \mathcal{H}$。若源和目标时同一个群,称之为**自同构**(automorphism)。 - -说明: -- 对于函数$f:A\to B$。 -- 单射(injection,一对一):$\forall a,b\in A(f(a) = f(b)\Rightarrow a = b)$。 -- 满射(surjection,映上):$\forall y\in B \exists x\in A(f(x) = y)$。 -- 双射(bijection,一一对应):即是单射,又是满射则称为双射。即陪域$B$中所有元素都是$A$中唯一的一个元素的像。 - -**定理2**:一个群同态$f:\mathcal{G}\to \mathcal{H}$是一个同构当且仅当它有一个**双边逆元**(two-sieded inverse),即$\exists f^{'}:\mathcal{H}\to \mathcal{G}(f^{'}\circ f = 1_{\mathcal{G}},f\circ f^{'}=1_{\mathcal{H}})$。 - -**定理3**:群之间的同构关系是群之间的**等价关系**(equivalence relation)。 - -说明: -- 等价关系:设$R$是非空集合$A$上的二元关系,若$R$自反、对称、传递,则称$R$是$A$上的等价关系。 -- 自反性:$\forall a\in A((a, a)\in R)$。 -- 对称性:$(a,b)\in R\land a\neq b \Rightarrow (b, a)\in R$。 -- 传递性:$(a, b)\in R\land (b, c)\in R \Rightarrow (a, c)\in R$。 -- 若$(a, b)\in R$,则称$a$等价于$b$,记做$a\sim b$。 -- 定理3也就是说:群的同构具有自反、对称、传递性。 - -### 生成新群 - -子群:从群 $\mathcal{G} = (G , * , e)$ 中取出部分元素 $G^{'}$,使其对于群运算 $ * $ 闭合,且 $G^{'}$ 的**逆元**(inverse)也是 $G$ 的逆元。那么称$\mathcal{G}^{'}=(G^{'}, * , e)$是$\mathcal{G} = (G , * , e)$的一个**子群**(subgroup)。 - -两个群的积:对于两个群 $(G, * ,e)$和$(G^{'}, * ^{'},e^{'})$,设$H$为配对元素 $\langle x,y\rangle$ (就是二元组),定义 $d=\langle e, e^{'}\rangle$,又有 $\forall x\in G,y\in G^{'}(\langle x,x^{'}\rangle\star\langle y,y^{'}\rangle = \langle x * y,x^{'} * y^{'}\rangle)$,那么称群 $\mathcal{H} = (H,\star,d)$ 为群 $(G, * ,e)$ 和群 $(G^{'}, * ^{'},e^{'})$ 的**积**(product)。 \ No newline at end of file diff --git a/CompilerOptimizations.md b/CompilerOptimizations.md deleted file mode 100644 index 3a2a638..0000000 --- a/CompilerOptimizations.md +++ /dev/null @@ -1,1185 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [主流编译优化方法一览](#%E4%B8%BB%E6%B5%81%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95%E4%B8%80%E8%A7%88) - - [地址优化(Address Optimization)](#%E5%9C%B0%E5%9D%80%E4%BC%98%E5%8C%96address-optimization) - - [通过地址的别名优化(Alias Optimization by address)](#%E9%80%9A%E8%BF%87%E5%9C%B0%E5%9D%80%E7%9A%84%E5%88%AB%E5%90%8D%E4%BC%98%E5%8C%96alias-optimization-by-address) - - [通过const限定的别名优化(Alias Optimization (const qualified))](#%E9%80%9A%E8%BF%87const%E9%99%90%E5%AE%9A%E7%9A%84%E5%88%AB%E5%90%8D%E4%BC%98%E5%8C%96alias-optimization-const-qualified) - - [通过类型的别名优化(Alias Optimization (by type))](#%E9%80%9A%E8%BF%87%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%88%AB%E5%90%8D%E4%BC%98%E5%8C%96alias-optimization-by-type) - - [数组边界优化(Array Bounds Optimization)](#%E6%95%B0%E7%BB%84%E8%BE%B9%E7%95%8C%E4%BC%98%E5%8C%96array-bounds-optimization) - - [位域优化(Bitfield Optimization)](#%E4%BD%8D%E5%9F%9F%E4%BC%98%E5%8C%96bitfield-optimization) - - [分支消除(Branch Elimination)](#%E5%88%86%E6%94%AF%E6%B6%88%E9%99%A4branch-elimination) - - [循环折叠与展开(Loop Collapsing/Unrolling)](#%E5%BE%AA%E7%8E%AF%E6%8A%98%E5%8F%A0%E4%B8%8E%E5%B1%95%E5%BC%80loop-collapsingunrolling) - - [指令组合(Instruction Combining)](#%E6%8C%87%E4%BB%A4%E7%BB%84%E5%90%88instruction-combining) - - [常量折叠(Constant Folding)](#%E5%B8%B8%E9%87%8F%E6%8A%98%E5%8F%A0constant-folding) - - [常量传播(Constant Propagation)](#%E5%B8%B8%E9%87%8F%E4%BC%A0%E6%92%ADconstant-propagation) - - [公共子表达式消除(Common Subexpression Elimination)](#%E5%85%AC%E5%85%B1%E5%AD%90%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B6%88%E9%99%A4common-subexpression-elimination) - - [死代码消除(Dead Code Elimination)](#%E6%AD%BB%E4%BB%A3%E7%A0%81%E6%B6%88%E9%99%A4dead-code-elimination) - - [整数乘除法优化(Integer Mutiply/Divide Optimization)](#%E6%95%B4%E6%95%B0%E4%B9%98%E9%99%A4%E6%B3%95%E4%BC%98%E5%8C%96integer-mutiplydivide-optimization) - - [整数取模优化(Integer Mod Optimization)](#%E6%95%B4%E6%95%B0%E5%8F%96%E6%A8%A1%E4%BC%98%E5%8C%96integer-mod-optimization) - - [表达式简化(Expression Simplification)](#%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%AE%80%E5%8C%96expression-simplification) - - [Forward Store](#forward-store) - - [循环裂变和融合(Loop fission and fusion)](#%E5%BE%AA%E7%8E%AF%E8%A3%82%E5%8F%98%E5%92%8C%E8%9E%8D%E5%90%88loop-fission-and-fusion) - - [垃圾收集优化(Garbage Collection Optimization)](#%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E4%BC%98%E5%8C%96garbage-collection-optimization) - - [分离循环中不变部分(Hoisting)](#%E5%88%86%E7%A6%BB%E5%BE%AA%E7%8E%AF%E4%B8%AD%E4%B8%8D%E5%8F%98%E9%83%A8%E5%88%86hoisting) - - [If优化(If Optimization)](#if%E4%BC%98%E5%8C%96if-optimization) - - [函数内联(Function Inlining)](#%E5%87%BD%E6%95%B0%E5%86%85%E8%81%94function-inlining) - - [循环变量消除(Induction Variable Elimination)](#%E5%BE%AA%E7%8E%AF%E5%8F%98%E9%87%8F%E6%B6%88%E9%99%A4induction-variable-elimination) - - [合并块(Block Merging)](#%E5%90%88%E5%B9%B6%E5%9D%97block-merging) - - [窄化转换优化(Narrowing)](#%E7%AA%84%E5%8C%96%E8%BD%AC%E6%8D%A2%E4%BC%98%E5%8C%96narrowing) - - [New表达式优化(New Expression Optimization)](#new%E8%A1%A8%E8%BE%BE%E5%BC%8F%E4%BC%98%E5%8C%96new-expression-optimization) - - [Printf优化(Printf Optimization)](#printf%E4%BC%98%E5%8C%96printf-optimization) - - [快速优化(Quick Optimization)](#%E5%BF%AB%E9%80%9F%E4%BC%98%E5%8C%96quick-optimization) - - [基于值范围的优化(Value Range Optimization)](#%E5%9F%BA%E4%BA%8E%E5%80%BC%E8%8C%83%E5%9B%B4%E7%9A%84%E4%BC%98%E5%8C%96value-range-optimization) - - [静态变量优化(Static Optimization)](#%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F%E4%BC%98%E5%8C%96static-optimization) - - [尾递归优化(Tail Recursion Optimization)](#%E5%B0%BE%E9%80%92%E5%BD%92%E4%BC%98%E5%8C%96tail-recursion-optimization) - - [Try/Catch块优化(Try/Catch Block Optimization)](#trycatch%E5%9D%97%E4%BC%98%E5%8C%96trycatch-block-optimization) - - [Unswitching](#unswitching) - - [虚函数优化(Virtual Function Optimization)](#%E8%99%9A%E5%87%BD%E6%95%B0%E4%BC%98%E5%8C%96virtual-function-optimization) - - [结语](#%E7%BB%93%E8%AF%AD) - - - -# 主流编译优化方法一览 - -主要参考自:[Compiler Optimizations](https://compileroptimizations.com/index.html),基本都是直接翻译过来的。 - -各种编译优化方法的简要描述,示例和优化后的等效代码转换。普及总览作用,不深入,有的话附对应wiki链接。 - -不深入了解编译原理看这些东西的话,总有种雾里看花,不学走先学跑的感觉。 - -名词解释: -- 架构一般说的是处理器硬件架构,比如x86,arm等。 - -## 地址优化(Address Optimization) - -一些架构中,引用一个保存在常量地址的全局变量一般需要两条指令,而通过指针引用只要一条。一般来说使用指针和偏移比使用变量快。 - -许多程序里的全局变量数量相对来说不多,经常可以放在一个全局变量池中。对这些程序来说,这些全局的变量和尺寸比较小的全局数组可以通过一个指针可一个偏移来访问,以避免代价更高的加载保存指令序列并且可以减少生成代码长度。 - -例:这个三个全局变量就可以被放到连续的内存区域,然后通过一个指针和偏移来引用。 -```C -int a; -int b; -int c; - -void f (void) -{ - a = 3; - b = 5; - c = 7; - return; -} -``` -优化后等效C代码:放到全局内存池。 -```C -int __t1[3]; /* global pool for a, b, c */ -int *__t2 = &__t1[0]; /* pointer to global pool */ - -void f (void) -{ - *__t2 = 3; /* a = 3; */ - *(__t2 + 1) = 5; /* b = 5; */ - *(__t2 + 2) = 7; /* c = 7; */ - return; -} -``` - -说明: -- 这个优化通常要求编译器和链接器合作能够更加有效。特别是,编译时全局变量的数量和偏移还未确定。 -- 一些编译器支持命令行选项控制全局变量的最大数量然后以此来分配全局内存池。 -- 一些ABI定义了寄存器保存全局内存池的指针。 -- 如果ABI没有定义全局内存池指针,那么加载一个指针可能相比直接加载全局变量来说代价更高。 -- 这个优化应该被启发式的控制,比如一个函数中访问全局变量的次数。次数很多的话可以优化,次数很少则没必要。 - -## 通过地址的别名优化(Alias Optimization by address) - -保存了指向不同数组成员的指针不可能冲突,即是不知道具体的偏移。 - -例:p和q是不同数组的成员指针,不可能是同一个。 -```C -int a[], b[]; - -void f (int i, int j) -{ - int *p, *q; - int x, y; - p = &a[i]; - q = &b[j]; - x = *(q + 3); - *p = 5; - y = *(q + 3); - g (x, y); -} -``` -那么y就可以优化掉,因为知道是不同数组,改变`*p`不可能会影响x。 -```C -int a[], b[]; - -void f(int i, int j) -{ - int *p, *q; - int x, y; - p = &a[i]; - q = &b[j]; - x = *(q + 3); - *p = 5; - g (x, x); -} -``` - -## 通过const限定的别名优化(Alias Optimization (const qualified)) - -const修饰的对象不能修改,因此在显式通过赋值修改一个变量的表达式中不可能是一个左值的别名。 - -```C -const int const_array[]; - -void f (int *p, int i) -{ - int x, y; - const int *q = &const_array[i]; - x = *q; - *p = 5; - y = *q; - g (x, y); -} -``` - -其中`p`不可能是`q`的别名,所以可以这样优化: -```C -const int const_array[]; - -void f (int *p, int i) -{ - int x, y; - const int *q = &const_array[i]; - x = *q; - *p = 5; - g (x, x); -} -``` - -说明: -- 仅仅只有`q`是一个`const int *`是不充分的,因为标准ANSI C允许其指向非const对象。这种情况下没有多余信息的情况下就不能实施这个优化。 -- 这个优化并不常见,尽管有一些编译器支持。 - -## 通过类型的别名优化(Alias Optimization (by type)) - -简单而言就是通过语言规定通过判断类型不同假定了两个变量不会是同一个变量的别名,基于这个假定来做一些别名优化。也就是重复操作的优化。 - -例: -```c -void f (short *ps, int *pi) -{ - int i, j; - i = *pi; - *ps = 3; - j = *pi; - g (i, j); -} -``` - -优化后: -```c -void f (short *ps, int *pi) -{ - int i, j; - i = *pi; - *ps = 3; - g (i, i); -} -``` - -## 数组边界优化(Array Bounds Optimization) - -也可以叫边界检查消除,在强制检查数组下标的运行时系统中将一部分可能的检查分配到编译期可以获得可观的性能提升,比如Java。 - -例:下列代码中,`a[0],a[1]...`等的下标检查可以在编译时进行。 -```java -{ - int a[], notused; - a = new int[100]; - notused = a[0]; notused = a[1]; ... -} -``` -再经过一些其他优化: -```java -{ - // Once Array Bounds Optimization is performed, all other - // statements can be optimized away. -} -``` - -说明: -- 数组边界优化可以在常量传播的过程中轻易实现,更高级的数组边界优化需要范围分析。 -- 数组边界优化是任何Java优化器中最重要的优化之一,某些时候编译器甚至不得不重排控制结构来确保数组边界检查。 - -Wiki: https://en.wikipedia.org/wiki/Bounds-checking_elimination - -## 位域优化(Bitfield Optimization) - -存储和读取具体的比特位代价通常比较高,大部分架构都不支持内存比特操作,而是需要一系列加载、移位、掩码、存储指令才能做到。通过各种各样位域优化可以提升运行时性能:将位域保存在寄存器中、位域级别的常量传播,组合相邻的位域的存储为一次存储等。 - - -例:两次位域赋值可以被组合为一次,减少相应的加载、移位、掩码、存储操作。 -```c -struct -{ - int bit1 : 1; - int bit2 : 1; -} bits; - -bits.bit1 = 1; -bits.bit2 = 1; -``` - -优化后:将两个二进制位的赋值组合到一次整数赋值。 -```c -struct -{ - int t : 2; /* compiler-generated alias for bit1 and bit2 */ -} bits; - -bits.t = 3; -``` - -## 分支消除(Branch Elimination) - -消除仅仅到另一个分支的分支,消除中间过程,直接到目标分支。 - -例: -```C - goto L1; - /* other code */ -L1: - goto L2; -``` -优化后: -```C - goto L2; - /* other code */ -L1: - goto L2; -``` - -## 循环折叠与展开(Loop Collapsing/Unrolling) - -折叠:某些嵌套循环可以被折叠到单循环以减少循环的性能,提高运行时性能。 - -展开:将多次循环合并到一次,减少循环次数。 - - -循环折叠例: -```C -int a[100][300]; - -for (i = 0; i < 300; i++) - for (j = 0; j < 100; j++) - a[j][i] = 0; -``` -优化后: -```c -int a[100][300]; -int *p = &a[0][0]; - -for (i = 0; i < 30000; i++) - *p++ = 0; -``` - -循环展开: -```c -for (i = 0; i < 100; i++) - g (); -``` -优化后: -```c -for (i = 0; i < 100; i += 2) -{ - g (); - g (); -} -``` - -说明: -- 循环折叠可以提升进行其他优化比如循环展开的机会。 -- 在一般C编译器可能并不常见,但在一些科学计算,GPU编程之类可能比较常见。 - -## 指令组合(Instruction Combining) - -在源码级别,将两条可能的语句组合成一条等效语句。在IL(中间语言)级别,将两条指令组合为一条。 - -例: -```C -int i; - -void f (void) -{ - i++; - i++; -} -``` - -优化后: -```c -int i; - -void f (void) -{ - i += 2; -} -``` - -说明: -- 许多指令都是指令组合的候选,比如`+-*<<>>&^|`。 -- 指令组合可以在块内部做,也可以横跨块作用域,前者较多,后者较少。 -- 循环展开可以为指令组合提供额外的机会。 - -## 常量折叠(Constant Folding) - -编译期计算常量表达式。 - -例: -```c -int f (void) -{ - return 3 + 5; -} -``` - -```c -int f (void) -{ - return 8; -} -``` - -说明: -- 相对来说容易做。 -- 一般程序员不太会直接写一个常量表达式出来,但比较常见于宏展开和常量传播后。 -- 所有的C编译器都可以折叠宏展开后的常量表达式,大部分C编译器也可以折叠经过其他优化后的常量表达式。 -- 浮点数可能需要留到运行时计算,如果舍入模式在编译期没有确定的话。 - -Wiki: https://en.wikipedia.org/wiki/Constant_folding - -## 常量传播(Constant Propagation) - -替代表达式中已知常数,被初始化为常数的变量在它真正被修改之前使用的地方都替换为常量。 - -例: -```c -x = 3; -y = x + 4; -``` - -优化后: -```c -x = 3; -y = 7; -``` - -说明: -- 可以在基本块也可以在更复杂的控制流中做常量传播。 -- 一些编译器为整型做,但不为浮点数做。 -- 和常量折叠交替使用,直到不能优化。 -- 很少有编译器在位域赋值时也进行常量传播。 - -Wiki: https://en.wikipedia.org/wiki/Constant_folding - - -## 公共子表达式消除(Common Subexpression Elimination) - -CSE,编译器会视情况将多个相同的表达式替换成一个变量,这个变量存储着计算该表达式后所得到的值。 - -例: -```c -i = x + y + 1; -j = x + y; -``` - -优化后: -```c -t1 = x + y; -i = t1 + 1; -j = t1; -``` - -- 各大编译器都有实现。 - -Wiki: https://en.wikipedia.org/wiki/Common_subexpression_elimination - -## 死代码消除(Dead Code Elimination) - -消除执行不到、无意义、对结果无影响的代码,DCE。包括: -- unreachable code -- Dead Variables - -例: -```c -int global; -void f () -{ - int i; - i = 1; /* dead store */ - global = 1; /* dead store */ - global = 2; - return; - global = 3; /* unreachable */ -} -``` - -优化后: -```c -int global; -void f () -{ - global = 2; - return; -} -``` -Wiki: https://en.wikipedia.org/wiki/Dead_code_elimination - - -## 整数乘除法优化(Integer Mutiply/Divide Optimization) - -对2的幂的整数乘除法可以优化为移位。 - -例: -```c -int f (unsigned int i) -{ - return i / 2; -} - -int f2 (int i) -{ - return i * 4; -} -``` - -优化后: -```c -int f (unsigned int i) -{ - return i >> 1; -} - -int f2 (int i) -{ - return i << 2; -} -``` - -说明: -- 需要考虑是否结果是否一定正确。 -- 除法:对有符号、无符号,正数、负数被除数,四种排列组合都可以优化,但需要考虑正确性。 -- 乘法一般各种情况都不会有问题。 - -## 整数取模优化(Integer Mod Optimization) - -整数对2的幂取模,可以用移位和条件替代。 - -例: -```c -int f (int x) -{ - return x % 8; -} -``` - -优化后: -```c -int f (int x) -{ - int temp = x & 7; - return (x < 0) ? ((temp == 0) ? 0 : (temp | ~7)) : temp; -} -``` - -## 表达式简化(Expression Simplification) - -一些表达式可以被等价的更简单的表达式替代。 - -例: -```c -void f (int i) -{ - a[0] = i + 0; - a[1] = i * 0; - a[2] = i - i; - a[3] = 1 + i + 1; -} -``` - -优化后: -```c -void f (int i) -{ - a[0] = i; - a[1] = 0; - a[2] = 0; - a[3] = 2 + i; -} -``` - -说明: -- 这种优化其实应该程序员来做。 -- 不过可能会常见宏展开后的表达式、数组下标计算等,还是有必要的。 - -## Forward Store - -存储循环中的全局变量访问到寄存器,减少加载保存次数。 - -例: -```c -int sum; - -void f (void) -{ - int i; - sum = 0; - for (i = 0; i < 100; i++) - sum += a[i]; -} -``` - -优化后:这里的意思是将sum的中间结果保存到寄存器中。 -```c -int sum; - -void f (void) -{ - int i; - register int t; - t = 0; - for (i = 0; i < 100; i++) - t += a[i]; - sum = t; -} -``` - -## 循环裂变和融合(Loop fission and fusion) - -循环裂变是指将一个循环,其中有多个互不相干的步骤分解成多个循环,在多核处理器中并行执行,以此来加快执行速度。 - -反过来来,循环融合则是将多个循环条件一致互不相关的循环融合为一个,减少条件和迭代部分的消耗,减少总体指令数量。 - -融合的例子: -```c -for (i = 0; i < 300; i++) - a[i] = a[i] + 3; - -for (i = 0; i < 300; i++) - b[i] = b[i] + 4; -``` -融合后: -```c -for (i = 0; i < 300; i++) -{ - a[i] = a[i] + 3; - b[i] = b[i] + 4; -} - -``` -裂变就是反过来。 - -说明: -- 需要循环条件和迭代情况完全一致,而且要循环体没有依赖关系。 -- 循环融合不一定就能提升速度,一般来说单一循环比如上述例子,融合前有更好的空间局部性。导致最终可能会有更好的缓存命中率。 -- 这两个过程都可以是编译优化的方法,条件也很苛刻,一般可能用不上,都可能负优化。 -- 但可能会用于那种大量计算,数据没有依赖,并行度可以很高的程序可能会非常需要这种优化。对那种问题而言,编程语言层面的设计可能就会考虑这种问题。 - -Wiki: https://en.wikipedia.org/wiki/Loop_fission_and_fusion - -## 垃圾收集优化(Garbage Collection Optimization) - -不必要的垃圾收集器的调用调用成本不应该比空函数高太多。高效率的内存分配也是编译优化的一部分? - -例: -```java -System.gc() ; // First call to the garbage collector -System.gc() ; -System.gc() ; -System.gc() ; -System.gc() ; // Fifth call to the garbage collector -``` - -优化后: -```java -System.gc() ; // First call to the garbage collector -emptyFunction() ; -emptyFunction() ; -emptyFunction() ; -emptyFunction() ; -``` - -说明: -- 垃圾收集会自动回收堆内存,通过一个守护线程实现垃圾收集。可以通过显式调用`System.gc()`回收垃圾。 -- 一般程序员不会手动回收内存,垃圾收集器会自动在合适的时间点回收内存。这种调用可能会使其他优化的结果。 - -## 分离循环中不变部分(Hoisting) - -将循环中进行的重复计算中不依赖于循环的部分剥离到循环外做。 - -例: -```c -void f (int x, int y) -{ - int i; - for (i = 0; i < 100; i++) - { - a[i] = x + y; - } -} -``` -优化后:x+y的结果与循环变量、中间结果没有任何关系,可以剥离出来计算。 -```c -void f (int x, int y) -{ - int i; - int t; - t = x + y; - for (i = 0; i < 100; i++) - { - a[i] = t; - } -} -``` - -## If优化(If Optimization) - -if条件块中if的条件一定是true,else块中if中的条件一定是false,如果其中还有同样的条件判断,那么则可以优化。前提是中间没有可能修改了条件的结果的语句。 - -例: -```c -void f (int *p) -{ - if (p) - { - g(1); - if (p) g(2); - g(3); - } - return; -} - -void f2 (int *p) -{ - if (p) g(1); - if (p) g(2); - return; -} -``` - -优化后: -```c -void f (int *p) -{ - if (p) - { - g(1); - g(2); - g(3); - } - return; -} - -void f2 (int *p) -{ - if (p) - { - g(1); - g(2); - } - return; -} -``` - -## 函数内联(Function Inlining) - -消除比较简单的函数调用开销。 - -例: -```c -int add (int x, int y) -{ - return x + y; -} - -int sub (int x, int y) -{ - return add (x, -y); -} -``` - -优化后: -```c -int sub (int x, int y) -{ - return x + -y; -} -``` -进一步优化: -```c -int sub (int x, int y) -{ - return x - y; -} -``` - -- 内联的函数一般比较简单,否则函数调用很多时可能会增加代码体积。 -- C/C++中有inline关键字指导内联。 - -看一下C的内联优化: -```c -// inline.c -int add (int x, int y) -{ - return x + y; -} - -int sub (int x, int y) -{ - return add (x, -y); -} -``` -```shell -gcc -S inline.c -``` -```x86asm - .file "inline.c" - .text - .globl _add - .def _add; .scl 2; .type 32; .endef -_add: -LFB0: - .cfi_startproc - pushl %ebp - .cfi_def_cfa_offset 8 - .cfi_offset 5, -8 - movl %esp, %ebp - .cfi_def_cfa_register 5 - movl 8(%ebp), %edx - movl 12(%ebp), %eax - addl %edx, %eax - popl %ebp - .cfi_restore 5 - .cfi_def_cfa 4, 4 - ret - .cfi_endproc -LFE0: - .globl _sub - .def _sub; .scl 2; .type 32; .endef -_sub: -LFB1: - .cfi_startproc - pushl %ebp - .cfi_def_cfa_offset 8 - .cfi_offset 5, -8 - movl %esp, %ebp - .cfi_def_cfa_register 5 - subl $8, %esp - movl 12(%ebp), %eax - negl %eax - movl %eax, 4(%esp) - movl 8(%ebp), %eax - movl %eax, (%esp) - call _add - leave - .cfi_restore 5 - .cfi_def_cfa 4, 4 - ret - .cfi_endproc -LFE1: - .ident "GCC: (MinGW.org GCC Build-2) 9.2.0" -``` -add函数加了inline之后: -```x86asm - .file "inline.c" - .text - .globl _sub - .def _sub; .scl 2; .type 32; .endef -_sub: -LFB1: - .cfi_startproc - pushl %ebp - .cfi_def_cfa_offset 8 - .cfi_offset 5, -8 - movl %esp, %ebp - .cfi_def_cfa_register 5 - subl $24, %esp - movl 12(%ebp), %eax - negl %eax - movl %eax, 4(%esp) - movl 8(%ebp), %eax - movl %eax, (%esp) - call _add - leave - .cfi_restore 5 - .cfi_def_cfa 4, 4 - ret - .cfi_endproc -LFE1: - .ident "GCC: (MinGW.org GCC Build-2) 9.2.0" - .def _add; .scl 2; .type 32; .endef -``` - -## 循环变量消除(Induction Variable Elimination) - -对于有多个循环变量(并不一定要在条件中,指每次迭代都会累加、累减做同样的变化的变量)的循环,合适的话可以消除到仅剩一个循环变量。 - -例: -```c -int a[SIZE]; -int b[SIZE]; - -void f (void) -{ - int i1, i2, i3; - for (i1 = 0, i2 = 0, i3 = 0; i1 < SIZE; i1++) - a[i2++] = b[i3++]; - return; -} -``` - -优化后: -```c -int a[SIZE]; -int b[SIZE]; - -void f (void) -{ - int i1; - for (i1 = 0; i1 < SIZE; i1++) - a[i1] = b[i1]; - return; -} -``` - -说明: -- 用于减少循环中累加累减的次数。 - -Wiki: https://en.wikipedia.org/wiki/Induction_variable - -## 合并块(Block Merging) - -合并多个小块为更大的块。 - -例: -```c -int a; -int b; - -void f (int x, int y) -{ - goto L1; /* basic block 1 */ - -L2: /* basic block 2 */ - b = x + y; - goto L3; - -L1: /* basic block 3 */ - a = x + y; - goto L2; - -L3: /* basic block 4 */ - return; -} -``` -重新组织后为一个大的基本块之后: -```c -int a; -int b; - -void f (int x, int y) -{ - a = x + y; /* basic block 1 */ - b = x + y; - return; -} -``` - -再做一个CSE: -```c -int a; -int b; - -void f (int x, int y) -{ - register int __t; /* compiler generated temp */ - __t = x + y; /* assign CSE to temp */ - a = __t; - b = __t; - return; -} -``` - -说明: -- 这个例子中的代码并不具有代表性,一般不会再源码级别这么写。 -- 但是这种代码可能常见于经过其他转换之后的编译器中间语言表示。 - - -## 窄化转换优化(Narrowing) - -一些范围比较小的整数在某些表达式中可以优化。 - -例:下面的表示式结果都是0,即是不知道值。 -```c -unsigned short int s; - -(s >> 20) /* all bits of precision have been shifted out, thus 0 */ -(s > 0x10000) /* 16 bit value can't be greater than 17 bit, thus 0 */ -(s == -1) /* can't be negative, thus 0 */ -``` - -说明: -- 这种类型的表达式可能说明程序中出现了错误。编译器甚至可能发出警告。 -- 程序员可能不会这样写,但是可能出现在宏展开或者某些优化后。 - -## New表达式优化(New Expression Optimization) - -对于有垃圾收集的语言,通过new分配内存,垃圾收集器会自动回收,比如java。如果一个分配了的内存从未使用,那么可以直接不分配。以减少内存分配和默认初始化的消耗。 - - -例: -```java -{ - int a[]; - a = new int[100]; -} -``` - -优化后: -```java -{ - // a not used and hence not allocated/initialized -} -``` - -说明: -- 这是非常重要的优化。某些程序可能不需要默认初始化。 -- 检测这种情况可以避免大数组的默认初始化造成的性能损耗。 - -## Printf优化(Printf Optimization) - -C语言中,编译时解析printf的格式字符串,并用合适的调用替代,可以优化运行时性能。 - -例: -```c -#include - -void f (char *s) -{ - printf ("%s", s); -} -``` -优化后: -```c -#include - -void f (char *s) -{ - fputs (s, stdout); -} -``` - -## 快速优化(Quick Optimization) - -Java字节码可以在运行时修改以获得更好的性能。动态的替换某些特定的JVM指令,比如连续地使用某个变量,中间没有改。每次使用都需要去解析这个变量的入口。那么第一次解析后,后续都不需要了。 - -例: -```java -{ - for(i = 0; i < 10; i++) - arr[i] = obj.i + volatile_var; -} -``` - -优化后:优化后的字节码的逻辑类似于这个样子。 -```java -{ - t = obj.i; - for(i = 0; i < 10; i++) - arr[i] = t + volatile_var; -} -``` - -说明: -- 运行时的优化。 -- 不必要对常量池中变量的引用可以不用去真正访问到变量。 -- 一些编程语言提供特性让程序员控制是否执行这种优化,比如C++的`volatile`关键字修饰的变量,要求执行到时一定要去访问这个变量的内存。主要用于看起来没有变但是可能在运行时通过其他方式同时修改的变量,比如在其他的线程中可能进行修改的变量。用于抑制某些编译器优化。 - -## 基于值范围的优化(Value Range Optimization) - -一些值可能是变化的,但是范围是确定的,基于这个范围可能可以进行某些优化。 - -例: -```c -for (i = 1; i < 100; i++) -{ - if (i) - g (); -} -``` - -优化后: -```c -for (i = 1; i < 100; i++) -{ - g (); -} -``` - -说明: -- 没有在各种编译器产品中得到普遍支持。 - - -## 静态变量优化(Static Optimization) - -如果循环中使用静态变量,可能的话可以将其保存到寄存器优化。 - -例:每次都要去获取变量i代价太大了。 -```c -int a[SIZE]; - -void f (void) -{ - static int i; - - for (i = 0; i < SIZE; i++) - a[i] = i; -} -``` -优化后: -```c -int a[SIZE]; - -void f (void) -{ - register int i; - for (i = 0; i < SIZE; i++) - a[i] = i; -} -``` - -循环中频繁使用的貌似都可以这种思路,无论全局、静态还是局部变量,用寄存器缓存之后访问会快很多。前提是有多余的寄存器来存。 - - -## 尾递归优化(Tail Recursion Optimization) - -尾递归可以被循环替代,减少了递归调用中函数调用的消耗。高级编程语言中都会提,尾递归更应该在源码级别由程序员来优化。 - -例: -```c -int f (int i) -{ - if (i > 0) - { - g (i); - return f (i - 1); - } - else - return 0; -} -``` - -优化后:优化后中间语言、汇编语言等价代码。 -```c -int f (int i) -{ - entry: - if (i > 0) - { - g (i); - i--; - goto entry; - } - else - return 0; -} -``` - -## Try/Catch块优化(Try/Catch Block Optimization) - -不可能抛出异常的try/catch块不进行try/catch。 - -例: -```c++ -try { - a = 1; -} catch (Exception e) - //... -} -``` -优化后: -```c++ -a = 1; -``` - -## Unswitching - -与循环变量无关的判断移到循环外做。源码层面就可以做。 - -例: -```c -for (i = 0; i < N; i++) - if (x) - a[i] = 0; - else - b[i] = 0; -``` - -优化后: -```c -if (x) - for (i = 0; i < N; i++) - a[i] = 0; - else - for (i = 0; i < N; i++) - b[i] = 0; -``` - -## 虚函数优化(Virtual Function Optimization) - -面向对象的编程语言中,虚函数的调用因为晚绑定都会比正常非虚函数调用成本更高。如果编译时能确定对象类型,能确定最终调哪个类的函数,那么就可以进行优化。 - -```java -class TestClass { - public void VirtualGetOne() { return ; } - public final void GetOne() { return ; } -} -class myclass { - public void myfunction() { - TestClass c1 = new TestClass() ; - c1.VirtualGetOne() ; - } -} -``` - -优化后: -```java -class TestClass { - public void VirtualGetOne() { return ; } - public final void GetOne() { return ; } -} -class myclass { - public void myfunction() { - TestClass c1 = new TestClass() ; - c1.GetOne() ; - } -} -``` - - -## 结语 - -作为了解之用,并不细节,也并没有实现方式,有个印象就行,如果以后写编译器或者阅读编译器实现时思考一下可不可以实现、有没有这些东西就行。 - -很多优化还是很简单的,比如: -- 常量折叠传播。 -- 公共子表达式消除。 -- 死代码剔除。 -- 2的幂次整数乘除法优化。 - -某些优化感觉局限性很大,只能用于很少的场合。 \ No newline at end of file diff --git a/CppToolChain.md b/CppToolChain.md deleted file mode 100644 index bbc84ff..0000000 --- a/CppToolChain.md +++ /dev/null @@ -1,284 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [C++工具链使用总结](#c%E5%B7%A5%E5%85%B7%E9%93%BE%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93) - - [GCC](#gcc) - - [Linux安装最新版本的GCC](#linux%E5%AE%89%E8%A3%85%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC%E7%9A%84gcc) - - [Windows中安装最新版本的GCC](#windows%E4%B8%AD%E5%AE%89%E8%A3%85%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC%E7%9A%84gcc) - - [Windows中安装MinGW-w64](#windows%E4%B8%AD%E5%AE%89%E8%A3%85mingw-w64) - - [Windows安装MSYS2](#windows%E5%AE%89%E8%A3%85msys2) - - [Windows安装Cygwin](#windows%E5%AE%89%E8%A3%85cygwin) - - [Clang && LLVM](#clang--llvm) - - [MSVC](#msvc) - - - -# C++工具链使用总结 - -## GCC - -### Linux安装最新版本的GCC - -通常Linux的包管理工具并不提供最新版本的GCC编译器,比如Ubuntu20.04现在这个时间(2022.7.25)的包管理中只提供GCC9.4.0。在这个版本中某些C++20的特性并没有得到支持。所以就需要自己手动安装最新本的GCC。 - -当前时间最新的GCC发布版是2022-05-06发布的12.1.0,以此为例: -- 首先访问[https://ftp.gnu.org/gnu/gcc/](https://ftp.gnu.org/gnu/gcc/)(或者[镜像](https://gcc.gnu.org/mirrors.html))下载最新的gcc版本安装包。 -- 解压`tar -zxvf gcc-12.1.0.tar.gz`。 -- 进入目录中,执行`./contrib/download_prerequisites`下载依赖,依赖的路径在[https://gcc.gnu.org/pub/gcc/infrastructure/](https://gcc.gnu.org/pub/gcc/infrastructure/)。 - - 会下载一系列依赖库:GMP、MPFR、MPC、ISL等。 -- 然后: -```sh -cd gcc-12.1.0 -mkdir build -cd build -../configure --enable-checking=release --enable-languages=c,c++ --disable-multilib -make && make install -``` -- 安装过程会使用旧版本GCC(需要支持C++11)编译新版本GCC,所以如果没有GCC需要先使用包管理工具安装。 -- 安装成功之后需要添加库路径(这里是`/usr/local/lib/../lib64`)到搜索路径: - - 直接添加库路径到`/etc/ld.so.conf`。 - - `~/.bashrc`添加环境变量`export LD_LIBRARY_PATH="/usr/local/lib/../lib64"`。 - - 执行程序前临时设置环境变量:`export LD_LIBRARY_PATH="/usr/local/lib/../lib64"`。 -- 修改原有的gcc符号链接指向新版本的GCC。 -```sh -sudo ln -s /usr/local/bin/gcc /usr/bin/gcc -f -``` -- 原有的GCC9.4.0依然可以通过`gcc-9`来访问。 -- 配合WSL其实和在Windows中也没有什么区别了。 - -### Windows中安装最新版本的GCC - -Windows中使用GCC工具链有多种选择: -- MinGW -- MinGW-w64 -- Cygwin -- MSYS -- MSYS2 -- 其他 - -首先来一点阅读资料: -- [[科普][FAQ]MinGW vs MinGW-W64及其它](https://github.com/FrankHB/pl-docs/blob/master/zh-CN/mingw-vs-mingw-v64.md) -- [Cygwin 和MinGW 的区别与联系是怎样的?](https://www.zhihu.com/question/22137175/answer/90908473) - -简而言之: -- MinGW是GCC在Windows上的移植,但现在仅支持32位,且已不再更新,基本处于废弃状态。 -- 所以有了MinGW-w64,提供了最新的支持,支持了64位,所以现在都应该使用MinGW-w64而非MinGW。其编译出来的程序算是Windows原生的。 -- Cygwin作为工具链来说是在Windows中模拟了POSIX API以支持在Windows中运行Linux程序,所以可用性会更广一些。使用了POSIX API的Linux程序使用Cygwin重新编译后即可在Windows中运行。运行时需要依赖`cygwin1.dll`,相当于多了一个间接层。并且Cygwin并不只是工具链,而是相当于运行于Windows下的类UNIX子环境,提供了非常多UNIX环境下的工具。 -- 由于Cygwin太重了,而单纯的工具链不够用,所以有了MSYS,相当于Cygwin的简化版,提供了类UNIX环境,以MinGW为工具链,在工具链之上还提供了基本的Linux工具,没有多少扩展能力。是辅助在Windows中以MinGW作为工具链进行开发的配套软件包。 -- 由于MinGW更新缓慢且放弃支持64位,而使用MinGW的MSYS也是如此,所以有了比较新的MSYS2,fork了较新版本的Cygwin(做了修改),提供了包管理工具,工具链则使用MinGW-w64,而MSYS2提供的其他Linux工具则依然是源自于Cygwin的。 - -总结: -- 现在已经不再使用MinGW和MSYS了。 -- MinGW-w64是单纯的工具链,而MSYS2做为一个类UNIX环境是配合这个工具链来用的。 -- Cygwin是一个大而全的Windows下的类UNIX子系统。 -- 使用MinGW-w64和Cygwin编译运行在UNIX上的程序都需要重新编译,而不能直接运行UNIX环境的可执行文件。 -- 想一想git for Windows是不是能够运行少量UNIX命令,其实就是因为其中包含了一套Cygwin.dll,MSYS2亦是这样,不过他们和Cygwin都互不兼容。 - -如何选择: -- 如果只需要Windows上的GCC工具链编写标准C/C++程序那就选择MinGW-w64。 -- 如果还想要执行一些Linux常用工具命令,想要一个类UNIX环境,那就选择MSYS2,其使用MinGW-w64工具链且相关工具来自Cygwin。 -- 使用Cygwin都能实现上述说的事情,但是Cygwin会更重,还有间接层导致的可能的性能损失。 -- 如果要编写跨平台但是使用了POSIX API的程序,那么Cygwin就成了仅有的选择。 - -到底该如何选择: -- 小孩子才做选择,为什么不全部装起来,要什么用什么呢? -- 注意不要同时将这些环境配在环境变量中。 - -### Windows中安装MinGW-w64 - -比较通常的选择是[MinGW-w64](https://www.mingw-w64.org/downloads/): -- 到[https://github.com/niXman/mingw-builds-binaries/releases](https://github.com/niXman/mingw-builds-binaries/releases)上下载最新版安装包即可。 -- 线程模型有两种选择Win32或者POSIX: - - 见这里:[What's the difference between thread_posixs and thread_win32 in gcc port of Windows?](https://stackoverflow.com/questions/13212342/whats-the-difference-between-thread-posixs-and-thread-win32-in-gcc-port-of-wind0) - - 简单说就是Win32线程模型与POSIX(pthread)有很大不同,而标准C++以`std::thread`为代表的线程模型与`pthread`很接近,所以在Linux上都是使用`pthread`来实现标准C++线程(这也就是为什么使用了线程就需要手动链接pthread)。 - - 如果在Windows中选择POSIX线程模型,就需要使用pthread在Win32上的一致libwinpthread,因此发布时需要带上`libwinpthread-1.dll`之类的库。 - - 至于Win32线程模型,现在的GCC依然还没有基于Win32线程模型实现的标准C++线程。就是说选择了Win32线程模型,那么其中的标准C++就是残废的,无法用。不过可以在其中使用Win32原生的多线程API。 - - 如果要使用标准C++线程,为了可移植性,应该选择POSIX线程模型。如果要用Win32原生API写多线程程序,那我为什么不选择MSVC呢? -- mcf线程模型: - - 在Win32和POSIX线程模型之外,还有一个独立的[GCC with the MCF thread model](https://gcc-mcf.lhmouse.com/),也实现了标准C++线程。不过二进制兼容性会比POSIX差一些。 -- 异常模型有三种选择:dwarf、sjlj和seh - - dwarf仅支持32位程序。 - - sjlj支持32位以及64位。 - - seh则仅支持64位程序。 - - 现在的系统一般都是64位,所以一般都选择sjlj或者seh,如果有编译32位程序的需求,那么需要sjlj。如果没有一般都是选择seh,两者ABI不兼容(也就是说同编译器同平台但不同异常模型的二进制是不兼容的)。 - - 当你的程序的各个模块使用不同的异常模型时,如果跨模块抛了异常(从一个模块的函数中抛出,被另一个模块的某个函数接住),那么程序很可能会Boom掉。 - - seh性能会比sjlj高一些。 -- 最终这两个都是可以选的: - - x86_64-12.1.0-release-posix-seh-rt_v10-rev3 - - x86_64-12.1.0-release-posix-sjlj-rt_v10-rev3 -- 删除旧版本的path环境变量中的路径,添加新版本的`bin/`目录到path即可。 -- 惯例复制一份`mingw32-make.exe`重命名为`make.exe`。 -- 至此就可以用GCC在Windows上编译C++20程序了。 -- 部署程序时除了编译出来的程序和必要的数据还得带上什么东西: - - Windows环境中默认不包含MinGW这些运行时环境,所以如果使用动态链接那么还需要带上依赖的动态库: - - 异常:`libgcc_s_sjlj.dll`这样的dll。 - - 用到了线程:`libwinpthread-1.dll`之类的dll。 - - 如果使用mcf线程部署:如`mcfgthread-9.dll`。 - - `libstdc++`。 - - 具体还是得看是什么环境,可能还需要其他一些额外的东西,可以将程序复制到没有环境的机器中测试。 - - 更简单可靠的方法还是使用 [Dependency Walker](http://www.dependencywalker.com/) 等工具查看依赖。 - -### Windows安装MSYS2 - -MinGW-w64作为工具链对于编写C++程序已经足够,但是如果要使用make,其中要使用比如`rm`这种工具那就非常糟心。那么MSYS2就是最好的选择了: -- 首先去[官网](https://www.msys2.org/)或者[清华镜像](https://mirrors.tuna.tsinghua.edu.cn/msys2/distrib/x86_64/)(或者其他镜像whatever)下去下载最新的64位MSYS2安装包。 -- [为pacman包管理工具配置清华源](https://mirrors.tuna.tsinghua.edu.cn/help/msys2/)。 -- 执行根目录下的`MSYS2.exe`进入shell。 -- 执行`pacman -Sy`刷新软件包数据。 -- 然后: -```sh -pacman -Ss xx #查询软件xx的信息 -pacman -S xx #安装软件xx -``` -- 安装make: -```sh -pacman -S make -``` -- 安装gcc/g++(最新版本是11.3,通常不应该以这种方式安装,这是Cygwin的打开姿势): -```sh -pacman -S gcc -pacman -S g++ -``` -- 一键安装所有MinGW-w64工具链(看起来更应该这样做,这里的gcc就是MinGW-w64的最新版本12.1.0,这两个工具链是存在本质差别的,直接安装的gcc编译生成的文件依赖于Cygwin的转换层,而MinGW-w64是windows原生的): -```sh -pacman -S mingw-w64-x86_64-toolchain -``` -- 这里装了`make`就不需要搞将`mingw32-make.exe`重命名为`make.exe`这种把戏了。 -- 进入MinGW-w64 Shell(或者直接找到MSYS2根目录的mingw64.exe启动): -```sh -/mingw64.exe -``` -- 在`MinGW-w64 Shell`中才能够使用MinGW-w64的工具链(应该是类似于重新启动了一个bash,并添加了一些环境变量)。 -- MSYS2支持的原生工具链区别: - - mingw64:GCC编译,链接到msvcrt(即msvcrt + listdc++ + GCC)。 - - ucrt64:GCC编译,链接到ucrt(即ucrt + libstdc++ + GCC)。 - - clang64:Clang编译,链接到ucrt(即ucrt + libc++ + Clang)。 - - 当然其实还有clang32,clangarm64,mingw32都比较好理解。 - - 需要安装其他工具链的话同理(其他的当前来说没有什么必要)。 - ```sh - pacman -S mingw-w64-ucrt-x86_64-toolchain - pacman -S mingw-w64-clang-x86_64-toolchain - ``` - - [MSYS2的相关文档](https://www.msys2.org/docs/environments/) - - 不同环境的区别在于环境变量、默认编译器链接器、架构、使用的系统变量、链接到的库等。 - - 这些环境仅仅是`MSYSTEM`这个系统变量的取值不同,比如`mingw64 ucrt64 clang64`等。 -- 通常来说一般不要直接将通过MSYS2安装的MinGW-w64工具链添加到系统变量。通过MSYS2提供的Shell来使用即可。 -- 现在可以通过开始菜单的各个快捷方式愉快地启动MinGW-w64/Clang Shell环境了: - - MSYS2 MinGW x64 - - MSYS2 MinGW UCRT x64 - - MSYS2 MinGW Clang x64 - - MSYS2 MinGW x86 - -各种环境区别: -- 编译器:GCC vs LLVM/Clang - - C++标准库区别libstdc++ vs libc++ - - 链接器:LD vs LLD,LLD更快,LD特性更丰富 - - Clang有对TLS(Thread local Storage)的本地支持 - - Clang提供ASAN(内存分析)工具 - - Clang支持ARM64/AArch64 -- 运行时:MSVCRT vs UCRT,微软的两个不同的标准库 - - MSVCRT(Microsoft Visual C++ runtime)在所有Windows版本中可用,不兼容C99且缺失了某些特性。 - - UCRT(Universal C Runtime)是VS默认使用的新版本C运行时。和MSVC有更好的兼容性,在Win10上是默认运行时,在更早的Windows版本使用需要自行安装。 - -配置MSYS2到Windows Terminal: -- 见[MSYS2 - Documentaion - Terminals](https://www.msys2.org/docs/terminals/) -- 快速配置: -```json -// MSYS2 MSYS -{ - "guid": "{17da3cac-b318-431e-8a3e-7fcdefe6d114}", - "name": "MSYS / MSYS2", - "commandline": "C:/CppToolChain/MSYS2/msys2_shell.cmd -defterm -here -no-start -msys", - "startingDirectory": "C:/CppToolChain/MSYS2/home/%USERNAME%", - "icon": "C:/CppToolChain/MSYS2/msys2.ico", - "fontFace": "Lucida Console", - "fontSize": 12, - "hidden": false, - "colorScheme" : "One Half Dark" -}, -// MSYS2 MinGW x64 -{ - "guid": "{17da3cac-b318-431e-8a3e-7fcdefe6d115}", - "name": "MINGW64 / MSYS2", - "commandline": "C:/CppToolChain/MSYS2/msys2_shell.cmd -defterm -here -no-start -mingw64", - "startingDirectory": "C:/CppToolChain/MSYS2/home/%USERNAME%", - "icon": "C:/CppToolChain/MSYS2/mingw64.ico", - "fontFace": "Lucida Console", - "fontSize": 12, - "hidden": false, - "colorScheme" : "One Half Dark" -}, -// MSYS2 MinGW UCRT x64 -{ - "guid": "{17da3cac-b318-431e-8a3e-7fcdefe6d116}", - "name": "UCRT64 / MSYS2", - "commandline": "C:/CppToolChain/MSYS2/msys2_shell.cmd -defterm -here -no-start -ucrt64", - "startingDirectory": "C:/CppToolChain/MSYS2/home/%USERNAME%", - "icon": "C:/CppToolChain/MSYS2/ucrt64.ico", - "fontFace": "Lucida Console", - "fontSize": 12, - "hidden": false, - "colorScheme" : "One Half Dark" -}, -// MSYS2 MinGW Clang x64 -{ - "guid": "{17da3cac-b318-431e-8a3e-7fcdefe6d117}", - "name": "Clang64 / MSYS2", - "commandline": "C:/CppToolChain/MSYS2/msys2_shell.cmd -defterm -here -no-start -clang64", - "startingDirectory": "C:/CppToolChain/MSYS2/home/%USERNAME%", - "icon": "C:/CppToolChain/MSYS2/clang64.ico", - "fontFace": "Lucida Console", - "fontSize": 12, - "hidden": false, - "colorScheme" : "One Half Dark" -} -``` -- 集成到终端的命令: -```sh -C:/CppToolChain/MSYS2/msys2_shell.cmd -defterm -here -no-start -xxx -``` - -集成MSYS2到VsCode终端: -- `"terminal.integrated.profiles.windows"`中添加: -```json -"MinGW64 MSYS2" : { - "path": "C:\\CppToolChain\\MSYS2\\msys2_shell.cmd", - "args": ["-defterm", "-here", "-no-start", "-mingw64"], - "overrideName":true -}, -"UCRT64 MSYS2" : { - "path": "C:\\CppToolChain\\MSYS2\\msys2_shell.cmd", - "args": ["-defterm", "-here", "-no-start", "-ucrt64"], - "overrideName":true -}, -"Clang64 MSYS2" : { - "path": "C:\\CppToolChain\\MSYS2\\msys2_shell.cmd", - "args": ["-defterm", "-here", "-no-start", "-clang64"], - "overrideName":true -}, -``` -- 同样道理不再赘述。 - -MSYS2中必备的工具: -- Windows中的path在MSYS2中是不可用的,所以MSYS2中安装的工具性能可能会比原生的程序低一点。 -```sh -pacman -S git -pacman -S vim -``` - -MSYS2在VsCode中不添加环境变量的情况下调试,尚待研究。 - -### Windows安装Cygwin - -如果想把UNIX下的使用了POSIX API的程序移植到Windows下来却又不想重写一遍,或者想混合POSIX API以及Win32 API编程(什么逆天的想法),那么Cygwin就是你所需要的。 - -Todo yet! - -## Clang && LLVM - -Todo yet! - -## MSVC - -通常来说我们使用VS,每个人都会的东西就不需要赘述了。 \ No newline at end of file diff --git a/Docs.md b/Docs.md deleted file mode 100644 index 4083590..0000000 --- a/Docs.md +++ /dev/null @@ -1,8 +0,0 @@ -# 各种文档资料在线链接 - -## 处理器架构 - -- 英特尔开发者手册:[Intel® 64 and IA-32 Architectures Software Developer’s Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) - -## C++ ABI -- [Itanium C++ ABI](http://itanium-cxx-abi.github.io/cxx-abi/) \ No newline at end of file diff --git a/EffectiveC++.md b/EffectiveC++.md deleted file mode 100644 index d74f15d..0000000 --- a/EffectiveC++.md +++ /dev/null @@ -1,1161 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Effective C++ 记录与速览](#effective-c-%E8%AE%B0%E5%BD%95%E4%B8%8E%E9%80%9F%E8%A7%88) - - [第一章:习惯C++](#%E7%AC%AC%E4%B8%80%E7%AB%A0%E4%B9%A0%E6%83%AFc) - - [条款1:视C++为一个语言的联邦](#%E6%9D%A1%E6%AC%BE1%E8%A7%86c%E4%B8%BA%E4%B8%80%E4%B8%AA%E8%AF%AD%E8%A8%80%E7%9A%84%E8%81%94%E9%82%A6) - - [条款2:以const,enum,inline替换#define](#%E6%9D%A1%E6%AC%BE2%E4%BB%A5constenuminline%E6%9B%BF%E6%8D%A2define) - - [条款3:尽可能使用const](#%E6%9D%A1%E6%AC%BE3%E5%B0%BD%E5%8F%AF%E8%83%BD%E4%BD%BF%E7%94%A8const) - - [条款4:确定对象使用前已被初始化](#%E6%9D%A1%E6%AC%BE4%E7%A1%AE%E5%AE%9A%E5%AF%B9%E8%B1%A1%E4%BD%BF%E7%94%A8%E5%89%8D%E5%B7%B2%E8%A2%AB%E5%88%9D%E5%A7%8B%E5%8C%96) - - [第二章:构造/析构/赋值运算](#%E7%AC%AC%E4%BA%8C%E7%AB%A0%E6%9E%84%E9%80%A0%E6%9E%90%E6%9E%84%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97) - - [条款5:了解C++默认生成了哪些函数](#%E6%9D%A1%E6%AC%BE5%E4%BA%86%E8%A7%A3c%E9%BB%98%E8%AE%A4%E7%94%9F%E6%88%90%E4%BA%86%E5%93%AA%E4%BA%9B%E5%87%BD%E6%95%B0) - - [条款6:若不想使用编译器自动生成的函数,就应该明确拒绝](#%E6%9D%A1%E6%AC%BE6%E8%8B%A5%E4%B8%8D%E6%83%B3%E4%BD%BF%E7%94%A8%E7%BC%96%E8%AF%91%E5%99%A8%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E7%9A%84%E5%87%BD%E6%95%B0%E5%B0%B1%E5%BA%94%E8%AF%A5%E6%98%8E%E7%A1%AE%E6%8B%92%E7%BB%9D) - - [条款7:为多态基类声明虚析构函数](#%E6%9D%A1%E6%AC%BE7%E4%B8%BA%E5%A4%9A%E6%80%81%E5%9F%BA%E7%B1%BB%E5%A3%B0%E6%98%8E%E8%99%9A%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0) - - [条款8:别让异常逃离析构函数](#%E6%9D%A1%E6%AC%BE8%E5%88%AB%E8%AE%A9%E5%BC%82%E5%B8%B8%E9%80%83%E7%A6%BB%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0) - - [条款9:永远不要在构造和析构函数中调用虚函数](#%E6%9D%A1%E6%AC%BE9%E6%B0%B8%E8%BF%9C%E4%B8%8D%E8%A6%81%E5%9C%A8%E6%9E%84%E9%80%A0%E5%92%8C%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%E4%B8%AD%E8%B0%83%E7%94%A8%E8%99%9A%E5%87%BD%E6%95%B0) - - [条款10:令operator=返回一个*this的引用](#%E6%9D%A1%E6%AC%BE10%E4%BB%A4operator%E8%BF%94%E5%9B%9E%E4%B8%80%E4%B8%AAthis%E7%9A%84%E5%BC%95%E7%94%A8) - - [条款11:在operator=中处理自我赋值](#%E6%9D%A1%E6%AC%BE11%E5%9C%A8operator%E4%B8%AD%E5%A4%84%E7%90%86%E8%87%AA%E6%88%91%E8%B5%8B%E5%80%BC) - - [条款12:赋值对象勿要忘记其每一个成分](#%E6%9D%A1%E6%AC%BE12%E8%B5%8B%E5%80%BC%E5%AF%B9%E8%B1%A1%E5%8B%BF%E8%A6%81%E5%BF%98%E8%AE%B0%E5%85%B6%E6%AF%8F%E4%B8%80%E4%B8%AA%E6%88%90%E5%88%86) - - [第三章:资源管理](#%E7%AC%AC%E4%B8%89%E7%AB%A0%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86) - - [条款13:以对象管理资源](#%E6%9D%A1%E6%AC%BE13%E4%BB%A5%E5%AF%B9%E8%B1%A1%E7%AE%A1%E7%90%86%E8%B5%84%E6%BA%90) - - [条款14:资源管理类中小心复制行为](#%E6%9D%A1%E6%AC%BE14%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E7%B1%BB%E4%B8%AD%E5%B0%8F%E5%BF%83%E5%A4%8D%E5%88%B6%E8%A1%8C%E4%B8%BA) - - [条款15:在资源管理类中提供对原始资源的访问](#%E6%9D%A1%E6%AC%BE15%E5%9C%A8%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E7%B1%BB%E4%B8%AD%E6%8F%90%E4%BE%9B%E5%AF%B9%E5%8E%9F%E5%A7%8B%E8%B5%84%E6%BA%90%E7%9A%84%E8%AE%BF%E9%97%AE) - - [条款16:成对使用new和delete时采取相同形式](#%E6%9D%A1%E6%AC%BE16%E6%88%90%E5%AF%B9%E4%BD%BF%E7%94%A8new%E5%92%8Cdelete%E6%97%B6%E9%87%87%E5%8F%96%E7%9B%B8%E5%90%8C%E5%BD%A2%E5%BC%8F) - - [条款17:以独立语句将new得到的指针置入智能指针](#%E6%9D%A1%E6%AC%BE17%E4%BB%A5%E7%8B%AC%E7%AB%8B%E8%AF%AD%E5%8F%A5%E5%B0%86new%E5%BE%97%E5%88%B0%E7%9A%84%E6%8C%87%E9%92%88%E7%BD%AE%E5%85%A5%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88) - - [第四章:设计与声明](#%E7%AC%AC%E5%9B%9B%E7%AB%A0%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%A3%B0%E6%98%8E) - - [条款18:让接口容易被正确使用,不易被误用](#%E6%9D%A1%E6%AC%BE18%E8%AE%A9%E6%8E%A5%E5%8F%A3%E5%AE%B9%E6%98%93%E8%A2%AB%E6%AD%A3%E7%A1%AE%E4%BD%BF%E7%94%A8%E4%B8%8D%E6%98%93%E8%A2%AB%E8%AF%AF%E7%94%A8) - - [条款19:像设计类型一样设计类](#%E6%9D%A1%E6%AC%BE19%E5%83%8F%E8%AE%BE%E8%AE%A1%E7%B1%BB%E5%9E%8B%E4%B8%80%E6%A0%B7%E8%AE%BE%E8%AE%A1%E7%B1%BB) - - [条款20:以const引用参数替代值传递](#%E6%9D%A1%E6%AC%BE20%E4%BB%A5const%E5%BC%95%E7%94%A8%E5%8F%82%E6%95%B0%E6%9B%BF%E4%BB%A3%E5%80%BC%E4%BC%A0%E9%80%92) - - [条款21:必须返回对象时,不要试图返回引用](#%E6%9D%A1%E6%AC%BE21%E5%BF%85%E9%A1%BB%E8%BF%94%E5%9B%9E%E5%AF%B9%E8%B1%A1%E6%97%B6%E4%B8%8D%E8%A6%81%E8%AF%95%E5%9B%BE%E8%BF%94%E5%9B%9E%E5%BC%95%E7%94%A8) - - [条款21:将成员变量声明为private](#%E6%9D%A1%E6%AC%BE21%E5%B0%86%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F%E5%A3%B0%E6%98%8E%E4%B8%BAprivate) - - [条款23:宁以非成员、非友元替换成员函数](#%E6%9D%A1%E6%AC%BE23%E5%AE%81%E4%BB%A5%E9%9D%9E%E6%88%90%E5%91%98%E9%9D%9E%E5%8F%8B%E5%85%83%E6%9B%BF%E6%8D%A2%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0) - - [条款24:若所有函数皆需类型转换,请为此采用非成员函数](#%E6%9D%A1%E6%AC%BE24%E8%8B%A5%E6%89%80%E6%9C%89%E5%87%BD%E6%95%B0%E7%9A%86%E9%9C%80%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E8%AF%B7%E4%B8%BA%E6%AD%A4%E9%87%87%E7%94%A8%E9%9D%9E%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0) - - [条款25:考虑写出一个不抛出异常的swap函数](#%E6%9D%A1%E6%AC%BE25%E8%80%83%E8%99%91%E5%86%99%E5%87%BA%E4%B8%80%E4%B8%AA%E4%B8%8D%E6%8A%9B%E5%87%BA%E5%BC%82%E5%B8%B8%E7%9A%84swap%E5%87%BD%E6%95%B0) - - [第五章:实现](#%E7%AC%AC%E4%BA%94%E7%AB%A0%E5%AE%9E%E7%8E%B0) - - [条款26:尽可能延后变量定义的出现时间](#%E6%9D%A1%E6%AC%BE26%E5%B0%BD%E5%8F%AF%E8%83%BD%E5%BB%B6%E5%90%8E%E5%8F%98%E9%87%8F%E5%AE%9A%E4%B9%89%E7%9A%84%E5%87%BA%E7%8E%B0%E6%97%B6%E9%97%B4) - - [条款27:尽量少做转型](#%E6%9D%A1%E6%AC%BE27%E5%B0%BD%E9%87%8F%E5%B0%91%E5%81%9A%E8%BD%AC%E5%9E%8B) - - [条款28:避免返回指向对象内部成分的句柄(handle)](#%E6%9D%A1%E6%AC%BE28%E9%81%BF%E5%85%8D%E8%BF%94%E5%9B%9E%E6%8C%87%E5%90%91%E5%AF%B9%E8%B1%A1%E5%86%85%E9%83%A8%E6%88%90%E5%88%86%E7%9A%84%E5%8F%A5%E6%9F%84handle) - - [条款29:为异常安全而努力是值得的](#%E6%9D%A1%E6%AC%BE29%E4%B8%BA%E5%BC%82%E5%B8%B8%E5%AE%89%E5%85%A8%E8%80%8C%E5%8A%AA%E5%8A%9B%E6%98%AF%E5%80%BC%E5%BE%97%E7%9A%84) - - [条款30:透彻了解inline的里里外外](#%E6%9D%A1%E6%AC%BE30%E9%80%8F%E5%BD%BB%E4%BA%86%E8%A7%A3inline%E7%9A%84%E9%87%8C%E9%87%8C%E5%A4%96%E5%A4%96) - - [条款31:将文件间的编译依存关系降至最低](#%E6%9D%A1%E6%AC%BE31%E5%B0%86%E6%96%87%E4%BB%B6%E9%97%B4%E7%9A%84%E7%BC%96%E8%AF%91%E4%BE%9D%E5%AD%98%E5%85%B3%E7%B3%BB%E9%99%8D%E8%87%B3%E6%9C%80%E4%BD%8E) - - [第六章:继承与面向对象设计](#%E7%AC%AC%E5%85%AD%E7%AB%A0%E7%BB%A7%E6%89%BF%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1) - - [条款32:确保你的公有继承建模出is-a关系](#%E6%9D%A1%E6%AC%BE32%E7%A1%AE%E4%BF%9D%E4%BD%A0%E7%9A%84%E5%85%AC%E6%9C%89%E7%BB%A7%E6%89%BF%E5%BB%BA%E6%A8%A1%E5%87%BAis-a%E5%85%B3%E7%B3%BB) - - [条款33:避免遮掩继承而来的名称](#%E6%9D%A1%E6%AC%BE33%E9%81%BF%E5%85%8D%E9%81%AE%E6%8E%A9%E7%BB%A7%E6%89%BF%E8%80%8C%E6%9D%A5%E7%9A%84%E5%90%8D%E7%A7%B0) - - [条款34;区分接口继承与实现继承](#%E6%9D%A1%E6%AC%BE34%E5%8C%BA%E5%88%86%E6%8E%A5%E5%8F%A3%E7%BB%A7%E6%89%BF%E4%B8%8E%E5%AE%9E%E7%8E%B0%E7%BB%A7%E6%89%BF) - - [条款35:考虑虚函数以外的其他选择](#%E6%9D%A1%E6%AC%BE35%E8%80%83%E8%99%91%E8%99%9A%E5%87%BD%E6%95%B0%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E9%80%89%E6%8B%A9) - - [条款36:绝不重新定义继承而来的非虚函数](#%E6%9D%A1%E6%AC%BE36%E7%BB%9D%E4%B8%8D%E9%87%8D%E6%96%B0%E5%AE%9A%E4%B9%89%E7%BB%A7%E6%89%BF%E8%80%8C%E6%9D%A5%E7%9A%84%E9%9D%9E%E8%99%9A%E5%87%BD%E6%95%B0) - - [条款37:绝不重新定义继承而来的默认参数值](#%E6%9D%A1%E6%AC%BE37%E7%BB%9D%E4%B8%8D%E9%87%8D%E6%96%B0%E5%AE%9A%E4%B9%89%E7%BB%A7%E6%89%BF%E8%80%8C%E6%9D%A5%E7%9A%84%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0%E5%80%BC) - - [条款38:通过复合建模出has-a或者is-implemented-in-terms-of关系](#%E6%9D%A1%E6%AC%BE38%E9%80%9A%E8%BF%87%E5%A4%8D%E5%90%88%E5%BB%BA%E6%A8%A1%E5%87%BAhas-a%E6%88%96%E8%80%85is-implemented-in-terms-of%E5%85%B3%E7%B3%BB) - - [条款39:明智而审慎地使用私有继承](#%E6%9D%A1%E6%AC%BE39%E6%98%8E%E6%99%BA%E8%80%8C%E5%AE%A1%E6%85%8E%E5%9C%B0%E4%BD%BF%E7%94%A8%E7%A7%81%E6%9C%89%E7%BB%A7%E6%89%BF) - - [条款40:明智而审慎地使用多重继承](#%E6%9D%A1%E6%AC%BE40%E6%98%8E%E6%99%BA%E8%80%8C%E5%AE%A1%E6%85%8E%E5%9C%B0%E4%BD%BF%E7%94%A8%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF) - - [第七章:模板与泛型编程](#%E7%AC%AC%E4%B8%83%E7%AB%A0%E6%A8%A1%E6%9D%BF%E4%B8%8E%E6%B3%9B%E5%9E%8B%E7%BC%96%E7%A8%8B) - - [条款41:了解隐式接口与编译期多态](#%E6%9D%A1%E6%AC%BE41%E4%BA%86%E8%A7%A3%E9%9A%90%E5%BC%8F%E6%8E%A5%E5%8F%A3%E4%B8%8E%E7%BC%96%E8%AF%91%E6%9C%9F%E5%A4%9A%E6%80%81) - - [条款42:了解typename的双重意义](#%E6%9D%A1%E6%AC%BE42%E4%BA%86%E8%A7%A3typename%E7%9A%84%E5%8F%8C%E9%87%8D%E6%84%8F%E4%B9%89) - - [条款43:学习处理模板化基类内的名称](#%E6%9D%A1%E6%AC%BE43%E5%AD%A6%E4%B9%A0%E5%A4%84%E7%90%86%E6%A8%A1%E6%9D%BF%E5%8C%96%E5%9F%BA%E7%B1%BB%E5%86%85%E7%9A%84%E5%90%8D%E7%A7%B0) - - [条款44:将参数无关的代码抽离模板](#%E6%9D%A1%E6%AC%BE44%E5%B0%86%E5%8F%82%E6%95%B0%E6%97%A0%E5%85%B3%E7%9A%84%E4%BB%A3%E7%A0%81%E6%8A%BD%E7%A6%BB%E6%A8%A1%E6%9D%BF) - - [条款45:运用成员函数模板接受所有兼容类型](#%E6%9D%A1%E6%AC%BE45%E8%BF%90%E7%94%A8%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E6%A8%A1%E6%9D%BF%E6%8E%A5%E5%8F%97%E6%89%80%E6%9C%89%E5%85%BC%E5%AE%B9%E7%B1%BB%E5%9E%8B) - - [条款46:需要类型转换时请为模板定义非成员函数](#%E6%9D%A1%E6%AC%BE46%E9%9C%80%E8%A6%81%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E6%97%B6%E8%AF%B7%E4%B8%BA%E6%A8%A1%E6%9D%BF%E5%AE%9A%E4%B9%89%E9%9D%9E%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0) - - [条款47:请使用traits类表现类型信息](#%E6%9D%A1%E6%AC%BE47%E8%AF%B7%E4%BD%BF%E7%94%A8traits%E7%B1%BB%E8%A1%A8%E7%8E%B0%E7%B1%BB%E5%9E%8B%E4%BF%A1%E6%81%AF) - - [条款48:认识模板元编程(TMP)](#%E6%9D%A1%E6%AC%BE48%E8%AE%A4%E8%AF%86%E6%A8%A1%E6%9D%BF%E5%85%83%E7%BC%96%E7%A8%8Btmp) - - [第八章:定制new和delete](#%E7%AC%AC%E5%85%AB%E7%AB%A0%E5%AE%9A%E5%88%B6new%E5%92%8Cdelete) - - [条款49:了解new-handler的行为](#%E6%9D%A1%E6%AC%BE49%E4%BA%86%E8%A7%A3new-handler%E7%9A%84%E8%A1%8C%E4%B8%BA) - - [条款50:了解new和delete的合理替换时机](#%E6%9D%A1%E6%AC%BE50%E4%BA%86%E8%A7%A3new%E5%92%8Cdelete%E7%9A%84%E5%90%88%E7%90%86%E6%9B%BF%E6%8D%A2%E6%97%B6%E6%9C%BA) - - [条款51:编写new和delete时需要固守常规](#%E6%9D%A1%E6%AC%BE51%E7%BC%96%E5%86%99new%E5%92%8Cdelete%E6%97%B6%E9%9C%80%E8%A6%81%E5%9B%BA%E5%AE%88%E5%B8%B8%E8%A7%84) - - [条款52:写了placement new也要写placement delete](#%E6%9D%A1%E6%AC%BE52%E5%86%99%E4%BA%86placement-new%E4%B9%9F%E8%A6%81%E5%86%99placement-delete) - - [第九章:杂项讨论](#%E7%AC%AC%E4%B9%9D%E7%AB%A0%E6%9D%82%E9%A1%B9%E8%AE%A8%E8%AE%BA) - - [条款53:不要轻易忽视编译器的警告](#%E6%9D%A1%E6%AC%BE53%E4%B8%8D%E8%A6%81%E8%BD%BB%E6%98%93%E5%BF%BD%E8%A7%86%E7%BC%96%E8%AF%91%E5%99%A8%E7%9A%84%E8%AD%A6%E5%91%8A) - - [条款54:让自己熟悉TR1在内的标准程序库](#%E6%9D%A1%E6%AC%BE54%E8%AE%A9%E8%87%AA%E5%B7%B1%E7%86%9F%E6%82%89tr1%E5%9C%A8%E5%86%85%E7%9A%84%E6%A0%87%E5%87%86%E7%A8%8B%E5%BA%8F%E5%BA%93) - - [条款55:让自己熟悉Boost](#%E6%9D%A1%E6%AC%BE55%E8%AE%A9%E8%87%AA%E5%B7%B1%E7%86%9F%E6%82%89boost) - - - -# Effective C++ 记录与速览 - -- C++的一些最佳实践,也就是怎么避开坑写出好的代码。 -- 本书按照章节和条款组织内容。这里简单总结,细节建议直接去看书。 -- 第三版成书2005年,所以C++11及之后的新特性可能不会被讲到,但在传统C++和现代C++中这本书里的东西基本都是通用的。 -- 很大一部分在C++Primer中都已经提到过了,只是这里单独提出来讲了。 - -## 第一章:习惯C++ - -### 条款1:视C++为一个语言的联邦 - -C++的多个编程范式: -- 继承自C的过程式编程 -- C with classes的面向对象编程 -- 基于模板的泛型编程,深奥的模板元编程 -- STL,标准模板库 - -当从一个范式切换到另一个时,编程策略可能需要改变。每个子语言会有自己的规约和属于自己的最佳实践,用的哪一个部分,就使用什么原则。必须对这一点有强烈意识。 - -### 条款2:以const,enum,inline替换#define - -- 以全局的常量定义替换宏定义的常量,以提供类型检查等一系列好处。 -- 用enum值替换宏定义得到编译期常量,以避免非必要内存消耗,在模板中广泛使用(aka enum hack)。 -- 用(模板)内联inline函数替换带参宏,获得更加健壮的程序。 - -宏定义和条件编译依然会扮演重要角色,但不再所有事情都要交给他们来做。 - -### 条款3:尽可能使用const - -const可以修饰基本任何变量: -- 在所有可能的地方使用const,常量,返回值(避免对右值的修改),参数(避免对参数的修改),成员函数(支持const对象调用)。 -- 充分理解指针引用的顶层底层const。 -- 将所有不会修改对象的成员函数定义为const,以提供const对象或者const引用参数来访问。 -- 在const对象中有想要修改的内容可以使用mutable。 -- const和non-const重载成员函数逻辑相似,可以用non-const调用const辅以`const_cast`来避免重复(注意绝不应该反过来做)。 - -### 条款4:确定对象使用前已被初始化 - -不同部分的C++: -- 源自于C部分可能会沿用C的行为,对未初始化的内置类型变量不做任何事情,保持其为内存中的随机值。如局部变量,数组,类的内建成员等。 -- 使用C++部分则会对内置类型做值初始化,如容器中的元素。 -- 自定义类型的默认初始化由默认构造函数完成。 - -原则: -- 在任何时候保证对内置类型做初始化能避免很多错误,C++并不保证初始化他们。 -- 使用构造函数成员初始化列表而非在构造函数体中对成员进行赋值以提高效率。 -- 类中数据成员有着确定的初始化顺序,基类、成员按照声明顺序依次初始化,避免构造成员的初始化有数据依赖。 -- 注意不同编译单元中的非局部静态对象的初始化顺序没有任何保证,避免这些对象的初始化之间存在依赖关系。 - - 最佳实践:使用单例模式返回局部静态变量的引用以替代全局的静态对象。规避初始化次序问题同时还能即用即初始化。 - -## 第二章:构造/析构/赋值运算 - -### 条款5:了解C++默认生成了哪些函数 - -- C++在一些情况下会默认生成默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符。 -- 在已经有构造函数声明的情况下,不会生成默认构造。 -- 在生成的函数的默认行为不合理、无法做到时则会将其生成为删除的函数。 - - 比如类中含有const成员、引用成员时拷贝赋值运算符会生成为删除。 - - 某个成员或者基类析构函数不可用时析构函数会生成为删除。 - - 某个成员或者基类没有默认构造时、有const或者引用成员时默认构造生成为删除。 - -### 条款6:若不想使用编译器自动生成的函数,就应该明确拒绝 - -不想要编译器默认生成的行为时,应该使用`=delete`明确声明为删除,或者自己定义。 -- 在不支持`=delete`特性时比较老式的表达删除一个函数的意思的伎俩是: - - 将不想要生成的函数声明为`private`,并且不做实现。 - - 或者继承一个拷贝构造、拷贝赋值被定义为private且未实现的基类`Uncopyable`。 -- 当然现代C++中使用`=delete`就行了,不用这么绕弯子。 - -### 条款7:为多态基类声明虚析构函数 - -这已经是任何一个菜鸟都知道的共识了! - -要点: -- 如果一个类要作为基类,那么可以肯定它需要一个虚析构函数。 -- 如果一个类不会作为基类,那么不要将析构函数定义为虚函数。会增加一个虚指针的内存消耗。 -- 如果你想要一个抽象类,但又找不到合适的函数来定义为虚函数。那么可以将析构函数定义为纯虚函数,但是同时也需要为其提供定义(合法可行且必须这样做,即使是抽象类甚至纯虚类,派生后这个子对象也是需要析构的)。 -- 不要继承没有定义虚析构函数的类作为基类,使用多态特性时会出现内存泄漏。使用final阻止继承。 - -### 条款8:别让异常逃离析构函数 - -因为异常处理的过程中会调用析构函数销毁对象,如果在这期间再发生异常,那么将会导致UB。 -- 所以永远不要在析构函数中抛出异常。 -- 析构函数中的可能发生的异常都需要在函数体中就得到恰当的处理。 -- 如果析构会发生无法恢复的异常,那么直接结束程序告知用户是一个好主意。 -- 当然无法处理时另一个选择也可以选择吞掉异常,这取决于程序维持稳定运行重要还是暴露出所有的一个错误更重要。当然即使吞掉异常也应该将其记录在日志上之类的手段将其记录下来。 -- 另一个策略是重新设计接口,将这些操作从析构函数中分离出来,将调用失败的处理交给使用类的客户去做。如果客户需要对这些异常做出反应,那么将其放在一个普通函数里面来做就是必须的。 - -### 条款9:永远不要在构造和析构函数中调用虚函数 - -因为在此时调用虚函数并不会达到我们想要的行为。 -- 构造函数中,基类构造先于派生类构造,在构造基类时,this指代的对象是基类对象,虚函数当然也不会被解析为派生类的虚函数,因为这时候派生类成员都还没有被初始化。 -- 析构函数中,派生类析构先于基类析构,在基类析构时,this同样指代基类对象,虚函数当然也会被解析为基类虚函数,此时派生类的成员已经被销毁了。 -- 因为在执行基类的构造和析构时,派生类中的成员都是不可以用的,要么还没有初始化,要么已经销毁,所以这时不能将虚函数下降至派生类。 -- 即是在构造函数和析构函数中,虚函数是不具备多态的,此时this也是指向该基类对象,而非派生类对象。此时做RTTI,`typeid(*this)`得到的将会是基类的`type_info`对象,`dynamic_cast`为派生类对象将会失败(而且这时派生类都不是完整类型,这样做必然是编不过的)。 -- 除了不直接在构造和析构中调用虚函数,也应当注意不要间接调用虚函数。【当然除非这就是你期望的行为】。 - -### 条款10:令operator=返回一个*this的引用 - -这也是常识了,为了和内置运算符的语义对标,为自定义类型重载赋值,前置++/--,取成员,解引用这种返回左值的运算符都应该返回一个左值引用。以实现和内置类型类似的语义。 - -当然这并不是强制的,只是这是最佳实践,为了减少心智负担而已。 - -### 条款11:在operator=中处理自我赋值 - -我们应该假定使用代码的人可能会做出任何事情,包括自己给自己赋值,虽然这并没有什么意义。 -- 某些时候赋值运算符中的逻辑本身就可以正确处理自赋值的情况,但某些情况并不能,每次写赋值运算符都应该考虑这种情况。 -- 比如类中申请了内存,不要急于在还未复制右侧对象内存之前,就将自己的内存释放掉。 -- 如果不好通过调整赋值中的操作顺序来处理自我赋值,那么也可以显式做一个判断。 -- 通常的做法时,复制了右侧对象的内容分配好了新内存之后,再来释放左侧对象内存,兼具自我赋值安全和异常安全。 -- 使用**copy and swap**技术来处理自我赋值也是一种常见手段: - - 可以使用const引用传参,在拷贝复制内部做拷贝之后和左侧对象做swap。 - - 也可以使用另一种更加巧妙的手段:使用值传递(在此时复制),内部做交换之后返回`*this`。这种方式还会将移动赋值和拷贝赋值统一到一个赋值运算符中。 - - 适用于在实现了对象交换的类型(特化了`std::swap`)中这样做。 - -这也算是基本常识了。 - -### 条款12:赋值对象勿要忘记其每一个成分 - -- 拷贝构造和拷贝赋值运算符中一定不要忘记复制每一个成员。(所以,为类添加新成员时千万不要忘记在构造函数中添加初始化,在拷贝控制成员中添加其复制逻辑,如果有必要析构函数中也需要处理)。 -- 除了自己的成员之外,如果是对一个派生类实现拷贝构造和拷贝赋值,那么一定不要忘记去对自己的基类部分执行拷贝。 - - 特别地,应该在拷贝赋值运算符中调用`Base::operator=(rhs)`。 -- 通常来说,拷贝构造和拷贝赋值运算符不应该互相调用,因为前者是构造一个新对象,后者是对一个已有对象赋值覆盖其原有状态。如果两者有很多重复逻辑,应该定义一个新的私有成员函数给两者使用(通常命名为`init`)。 - -## 第三章:资源管理 - -### 条款13:以对象管理资源 - -- 将资源放进对象,依靠析构的自动调用来释放对象,无论是抛出异常还是中途控制流退出都能够释放资源。 -- 可以使用智能指针来管理资源。 - - 获取资源后立即放到管理对象(智能指针)中。即是**RAII(Resource Acquisition Is Initialization,资源获取即初始化)**。 - - 管理对象的析构函数确保资源被释放。 - -注意:这个条目中提到的`auto_ptr`现在已经有`shared_ptr unique_ptr weak_ptr`替代了,并且智能指针都支持自定义deleter了,所以现在也可以管理`new []`分配的内存了。 - - -### 条款14:资源管理类中小心复制行为 - -当我们用RAII将资源释放委托给资源管理对象的析构函数后。需要小心资源管理对象的复制行为: -- RAII:资源在构造期间获得,析构时释放。 -- 几种资源管理对象复制行为的处理: - - 如果复制行为不合理,那么需要禁止复制(但一般允许移动),也就是使用类似于`unique_ptr`的行为。 - - 如果复制行为是合理的,效果是多个管理对象指向同一资源,那么可以使用引用计数管理。也就是`shared_ptr`的行为,可以通过自定义deleter来实现非内存类资源的释放。 - - 复制底层资源,这也是一种复制时可以考虑的行为。对资源做深拷贝,和原先的资源便没有了关系,独立了起来。 - - 转移底部资源拥有权,也可以配合`unique_ptr`使用。某些时候可能需要确保只有一个RAII对象指向底层资源,那么可以这样做。 -- 不同的资源可能需要考虑不同的复制行为,但都可以用RAII来管理。如果单纯的智能指针功能不够,可能需要自定义RAII类来管理资源。 - -### 条款15:在资源管理类中提供对原始资源的访问 - -资源管理类将原始资源封装了一层,那么在需要这些原始资源的地方就需要提供对原始资源的访问: -- 智能指针的`get`接口提供原始指针,`. ->`运算符提供了对资源对象的成员访问。 -- 在自定义资源管理类中可以提供智能指针`get`这样的显式转换接口,或者编写类型转换运算符以提供隐式转换。显式转换更安全,隐式转换更方便,各有优劣,怎样提供视具体情况选择。 -- RAII类并不是为了封装,而是为了确保资源能够在析构时得到释放。 -- 良好设计的类会隐藏客户不需要的部分,提供用户需要的所有东西。 - -### 条款16:成对使用new和delete时采取相同形式 - -`new/delete`以及`new[]/delete[]`是配套的,不能混用,这也是常识了。 -- 特别是用智能指针管理`new[]`分配的内存时需要自定义删除器为`[](T* p) -> void{ delete[] p; }`。 -- 用RAII管理时,在构造和析构中一定要配套使用。并且多个构造中必须要使用相同的`new`运算符。 - -### 条款17:以独立语句将new得到的指针置入智能指针 - -简单来说就是,防止资源创建和资源被转换为资源管理对象之间发生干扰(比如抛出异常),导致内存泄漏。 -- 典型如使用智能指针传参数时,先将智能指针构造出来再传入接受智能指针的函数。而不是在函数调用中直接构造临时的智能指针。 -- 【因为编译器对于跨越语句的操作没有重新排列的自由度】。 - -## 第四章:设计与声明 - -### 条款18:让接口容易被正确使用,不易被误用 - -应该在所有接口中努力达成“容易被正确使用,不易被误用”的原则: -- 促进正确使用的方法有:设计更符合直觉的接口,设计与内置类型行为兼容的接口。 -- 防止误用的方法有:建立新类型(通过类型系统防止错误数据),限制类型上的操作,束缚对象的值,消除客户的资源管理责任(典型如使用智能指针)。 -- `std::shared_ptr`自定义删除器,可以防止DLL问题(跨DLL释放内存,一般会报运行时错误),自动解除互斥锁等。 -- 总之:在最理想的情况下,如果用户错误地使用了接口,那么应该出现编译错误,这基本不可能达不到,但应该为此而努力。 - -### 条款19:像设计类型一样设计类 - -也就是说设计类从来不只是定义一个class那么简单,要像语言设计者设计内置类型那样谨慎。一些设计要点: -- 类的对象如何被创建,影响到构造析构函数即内存分配和释放函数(`operator new/delete/new[]/delete[]`)。 -- 对象的初始化和赋值该有什么样的区别。 -- 对象被值传递时,拷贝构造函数怎么实现。 -- 对象的合法状态,也就是那些值对对象来说是有效的,这将决定了成员函数中的错误检查、抛出异常等。 -- 配合继承体系。如果新类是否继承自既有的类,那么需要遵守既有的基类的影响。如果你的类开放给用户作为基类,那么将影响函数的声明,尤其是析构函数需要声明为虚函数。 -- 新的类型需要怎么样类型转换,其他类型转换到该类型,该类型转为其他类型。 -- 是否需要为新类型重载运算符,那些运算符是合理的。 -- 不该对外部暴露的函数应该声明为private。 -- 谁会来使用这个新的类,也就是这个类的用户是谁。这将决定对外暴露的接口,对派生类暴露的接口,非成员函数接口。 -- 这个类是一个独立的类,还是一整族类,如果是一族类,那么也许应该定义为类模板。 -- 你确实需要一个新的类吗?如果只是在现有类基础上派生,并加上很少的功能,那么为什么不直接修改现有类,或者添加几个函数解决。 - -一个好的设计需要对上述所以问题作出自己的回答,确保在所有方面做到最好。 - -### 条款20:以const引用参数替代值传递 - -- 永远记住值传递会对类型调用拷贝构造函数进行拷贝。 -- 对自定义类型来说,在非必要以值传递的方式传递时,都应该以引用参数传递,不会修改源对象则使用const,会则不使用const。 -- 在多态场景中,采用引用传递也可以避免对象被切割为基类对象。 -- 对于内置类型则一般采用值传递。 -- 一些特殊的自定义类型比如智能指针、STL中的迭代器和函数对象,一般来说采用值传递更为合适。 - -### 条款21:必须返回对象时,不要试图返回引用 - -简单来说就是不要试图在不应该返回引用的地方返回一个引用。 -- 比如返回表示计算结果的右值的重载运算符(`operator +-*/%`等)。 -- 不要试图返回局部变量的引用。 -- 不要去焦虑这一点拷贝构造导致的性能损失,现代C++中有移动语义以及复制消除(构造函数消除、RVO、NRVO等)的优化手段。 - -### 条款21:将成员变量声明为private - -封装就是要将成员声明变量为private,只对外部暴露出接口: -- 成员变量隐藏的背后,可以为所有可能的实现提供弹性。可以在用户无感知的情况下修改背后的实现。 -- 隐藏成员才能确保类的约束条件通过接口函数得到恰当地维护,访问权限得到更精细的控制,而非任由用户直接读取修改成员变量。 -- 封装同样保留了日后变更的权利,不封装几乎意味着不可改变。没有人会需要一个不可改变的程序。 -- 封装性(隐藏的程度)与其被修改时可能造成的代码破坏量成反比: - - public成员的修改可能破坏大量用户代码。 - - protectd成员修改可能破坏大量继承了该类的代码。 - - 唯有private成员具有最高的封装性,得以在用户无感知的情况下替换修改。(友元破坏了封装,所以友元除外)。 - - 所以从封装角度看,只有两种访问权限:private和其他。 -- 最后:记住将所有实现而非接口相关的内容封装起来,声明为private。这可以给与用户访问数据的一致性、可细微划分的访问控制、约束条件得到保证,并提供给类的作者充分的实现弹性。 - -### 条款23:宁以非成员、非友元替换成员函数 - -如果一个非成员函数与友元函数或者成员函数提供同样的功能,那么应该选择非成员非友元函数。 -- 因为成员函数和友元函数会增加能够访问类中私有成分的函数数量。(能够访问类中私有成分越多,封装性越弱)。 -- 所以实现为一个非成员非友元函数会具有更强的封装性。 -- 一个好的选择是将其放在与类同一个命名空间中作为一个非成员非友元的全局函数。 -- 这也能降低编译依赖性,也就是代码耦合度更低。 - -### 条款24:若所有函数皆需类型转换,请为此采用非成员函数 - -很容易理解,如果定义为成员函数,那么第一个隐式参数this是不能够由其他类型参数隐式转换而来的,必须显式构造之后才能使用其调用成员函数。 -- 最常见的是重载的运算符,比如算术运算`+-*/%`这种。定义为非成员函数以允许所有参数都能够隐式转换会更加合理一些。 -- 这通常用在可以由其他类型隐式转换为该类型时。 - -### 条款25:考虑写出一个不抛出异常的swap函数 - -`std::swap`函数模板的平凡实现就是我们想的那样,利用一个临时变量,交换两个变量的值。 -- 当要实现交换函数时,通常我们是使用我们的自定义了类型对`std::swap`做一个(全)特化(特化到命名空间`std`中)。 -- 一般来说的实践是定义一个`swap`成员函数来做交换,然后`std::swap`特化直接调用即可。 -- 但如果我们定义的是类模板而非普通类时,就不能够对`std::swap`做偏特化了(因为C++只支持对类做偏特化,不支持对函数做偏特化),这时的做法时给`std::swap`添加一个新的重载版本(然而直接加在`std`是不合法的)。 -- 但是有一点比较特殊的是命名空间`std`,我们可以对其中的模板做特化,但是不能往其中添加新的模板。所以通常的做法是,将类和非成员的`swap`模板放到同一个命名空间。因为对类做函数调用时同时会去类所在的命名空间查找名字,所以这样做就足够了。调用时应该使用`swap`而不是`std::swap`。【如果没有使用命名空间,那么就是定义全局命名空间,这也是可行的,怎么用取决于你】。 -- 对于类模板来说,在新的或者全局命名空间定义一个新的`swap`模板是最佳选择。 -- 而对于普通类来说,也可以定义一个普通函数重载版本的`swap`。通常情况下的建议是特化`std::swap`和普通函数定义同时做,他们都调用类内部的`swap`。 -- 无论什么情况,使用时的最佳实践是: - - 在作用域内`using std::swap;`,就像这样: - ```C++ - template - void doSomething(T& a, T& b) - { - using std::swap; - ... - swap(a, b); - ... - } - ``` - - 这样做时,匹配顺序会是:T类型同命名空间中的`swap`、自己添加的特化版本的`std::swap`、非特化版本的`std::swap`库实现。 - - 而不应该直接使用`std::swap`(这样会忽略同命名空间中的`swap`实现,如果是类模板,就意味着直接去实例化`std::swap`)。 -- 值得注意的是:`swap`的使用就是为了让成员在拷贝和移动时确保异常安全,所以应该确保**成员版本的swap**(最终被调用的那个实现)不抛出任何异常。 - -另外: -- 更一般地,重载决议时,会将所有参数所在命名空间中的同名函数加入可行函数集(在没有说明所调用函数的命名空间的情况下)。 - -## 第五章:实现 - -本章关注实现中的各种细节问题: -- 变量定义时机。 -- 不要滥用转型。 -- 避免返回内部handle。 -- 为异常安全而努力,避免异常导致的资源泄漏。 -- 不要滥用内联。 -- 降低代码的耦合度。 - -### 条款26:尽可能延后变量定义的出现时间 - -- 通常来说,最好就是变量即用即定义,也就是直到要用到变量的前一刻再定义。 -- 并且定义的时候用一个有意义能用到的值来初始化,好过默认构造之后再赋值。 -- 在循环中使用一个多轮循环中没有关联的临时变量时,在循环前定义还是每一轮循环定义则取决于一组构造+析构与一个赋值操作谁的成本更高。 - - 通常来说两者成本是差不多的,非效率敏感部分代码是可以在循环中定义的,会更加清晰,且不会延长该临时变量的生命周期。 - -### 条款27:尽量少做转型 - -显式类型转换同时破坏了类型系统,非必要不应该使用,优秀的代码很少使用显式类型转换: -- C中转型格式: - - `(T)expression` 继承自的C的最原始的转型风格。 - - `T(expression)` 构造转型风格。 - - 两者等价,前者在C++中不应该使用,后者常用在要构造一个临时变量的地方,这种情况亦可理解为临时的纯右值对象的构造,通常是合理的。 - - C风格转换会尝试去使用C++的`static_cast const_cast`,如果不合理则会使用`reinterpret_cast`。 -- C++中显式类型转换: - - `const_cast(expression)` - - `dynamic_cast(exression)` - - `reinterpret_cast(exression)` - - `static_cast(exression)` -- 通常来说不应该用C风格转换,当然如果语义是调用转换构造构造一个临时对象,那是可以使用的。 -- 通常也不应该使用C++显式类型转换,可能存在些许例外: - - `const_cast`应该只用于类似于条款3所述的等少量场景。 - - `dynamic_cast`具有不小的运行时消耗。试着使用无需`dynamic_cast`的设计: - - 不使用基类指针,而使用派生类指针保存派生类对象。 - - 添加虚函数,在基类添加空实现,从而使用多态来处理,避免转换。 - - `reinterpret_cast`通常意味着坏的设计,通常不会需要重新解释内存,除非一些非常非常特殊的场景,尝试使用其他方法替代。 - - `static_cast`也应该尽量被避免,非要用也应该封装在函数中,而不是让用户来做。典型如`std::move`。 -- 就算要使用显式类型转换,也应该使用C++风格而不是C风格。(当然单参数构造一个临时对象的风格依然很常用,因为可以理解为一个临时对象的构造,这是使用`static_cast`还会更费解)。 - -### 条款28:避免返回指向对象内部成分的句柄(handle) - -这里的句柄包括指针、引用、迭代器或者传统的句柄。 -- 典型如`std::shared_ptr::get`返回的原始指针,通常我们只应该将其用于只能接受原始指针而不能接受智能指针的函数调用场景,调用结束后即释放,避免出现空悬的句柄。 -- 其他类型同理,返回对象内部句柄代表着封装性的降低,内部封装的成员的访问级别其实被提高了。 -- 对一个const对象返回其内部句柄,并且可通过句柄修改内部状态的话,在逻辑上就是错误的(语法却是合法的)。 - - 要让const成员函数的行为像一个const,此时应该在返回的句柄上加上const修改时,让其变为只读。 -- 不得不用的时候避免返回的句柄空悬也是非常重要的。使用得到的句柄时避免源对象已经被析构: - - 如果将返回的句柄作为返回值,可能应该值返回,而不是返回指针、引用或者迭代器。 - - 能用外层对象完成的事情就避免使用内部句柄来做。 -- 比较特殊的情况下可能不得不这么做,比如`operator[]`、迭代器等。但都要时刻注意使用时决不能在对象析构之后还在用返回的句柄。 - -### 条款29:为异常安全而努力是值得的 - -编写异常安全的代码,给与了程序更高的健壮性,也给了用户在异常抛出时更好的操作空间: -- 当异常抛出时,有异常安全性的函数需要满足: - - 不泄露任何资源。 - - 不允许数据损坏。 -- 防止资源泄漏可以使用资源管理类利用RAII特性解决,将发生异常时的资源释放动作委托给RAII类的析构。见第三章。 -- 不允许数据损坏就要求我们小心安排申请新资源和释放旧资源的顺序: - - 通常做法是统一先申请新资源/获取并计算新状态,成功之后再释放旧资源/设置新状态。避免旧的资源已经释放,但新的资源却申请失败的情况。 - - 特别是有多个资源和状态时,要避免一部分状态已更新,一部分还没有更新时抛出异常的问题。 -- 异常安全提供以下三个程度的保证: - - **基本承诺**:异常抛出时,程序内内部事物依然处于有效状态,但状态是否改变并不确定。 - - **强烈保证**:异常抛出时,程序状态不发生任何改变,和调用前一致。 - - **不抛出(nothrow)保证**:承诺绝不抛出异常,通常我们会为这种函数加上`noexcept/throw()`修饰。 -- 如果函数不提供以上三种保证之一,那么它就不具备异常安全性。 -- 编写异常安全的代码时,我们需要抉择提供哪一种保证: - - 不抛出保证很诱人,那么很简单的函数很容易提供不抛出保证,但是如果我们调用了任何可能抛出异常的函数,那就不可能实现了。 - - 通常情况下都是在基本承诺和不抛出保证中做选择。 - - 实现强烈保证的一个一般化的设计策略就是我们前面提到过的copy and swap技术。先创建打算修改的对象的副本(用智能指针保存以避免资源泄漏),在副本上做状态修改,修改完之后在与目标对象做交换(swap操作通常都需要承诺不抛出异常)。典型实现示例: - ```C++ - // member of Foo example: - // Mutex mutex; - // shared_ptr pImpl; - void Foo::someExceptionSafeFunc(const Bar& bar) - { - using std::swap; - Lock m(&mutex); // RAII manage mutex - // copy old states - // RAII make sure the copy will surely be released - shared_ptr pNew(new FooImpl(*pImpl)); - // set new states - pNew->xxxMember.reset(new Bar(bar)); - ++pNew->xxxMember; - // swap - swap(pImpl, pNew); - } - ``` - - copy and swap技术提供了一个“全有或者全无”的一个好方法。 - - 但是因为一个函数能提供的异常安全保证取决于函数实现中调用的所有函数中最弱的那个保证,如果函数使用了copy and swap提供强烈保证,但是额外调用了一些只提供基本保证的函数,那么就只能有基本保证。要提供强烈保证就必须在基本保证的函数调用两侧去记录原始状态,在发生异常时做状态恢复,会有一定的性能代价。 - - 上面所说都是函数只操作局部状态的情况,如果函数还会操作全局状态,那么提供强烈保证就更为困难了。 -- 总结: - - 强烈保证不一定容易实现,很多时候强烈保证是不切实际的(可能的巨量效率损失与繁杂的实现成本),这时尽可能提供基本保证也许是更好的选择。 - - 因为异常安全是传递的,所以一个程序要么是全局异常安全的,要么是不安全的。不存在说局部异常安全的。 - - 但并不应该滑坡谬误,已经存在异常不安全的代码不是继续编写异常不安全的代码和不再为异常安全做任何努力的理由。任何时候都应该努力编写出异常安全的代码。 - - 异常安全应该作为接口的一部分,被写进文档中。 - -作为曾今很少为异常安全考虑的人来说,这一节具有非凡的指导意义。 - -### 条款30:透彻了解inline的里里外外 - -函数内联是典型的空间换时间策略,减少函数调用的开销,但会增大程序的体积。取决于你对哪一个资源的敏感度更高。 -- 函数体足够小是使用内联的一个有效理由,当函数体小到比函数调用的开销更小时,内联就只有好处没有坏处了(减小程序体积同时提高效率)。 -- 内联只是一个向编译器提供的建议,不是强制命令。 -- inline的细节: - - 在类中定义函数是隐式内联的。 - - C++的内联是在编译时做的。 - - 显式的内联建议通常将函数定义在头文件中,为了能够将代码嵌入到调用的位置,编译器需要知道函数体是什么。因为最终不会生成函数,所以可以有多份同样的定义而不会造成符号重定义。 - - 模板通常也会将定义写在头文件中,但满足同样规则,并不会直接隐式内联,要内联同样需要显式使用inline。并且记住他们定义在头文件里面并不是他们应该内联的理由。 - - 编译器并不会执行过于复杂的内联。 - - 虚函数的调用也不会执行内联(除非非常简单的编译期就能确定调用哪一个的,属于编译优化的一种)。 - - 多数编译器如果无法内联一个inline函数,可能会给出警告。 - - 某些时候编译器虽然内联了某个函数,但却依然生成了函数代码(比如需要取其地址时)。同理编译也通常不为通过函数指针调用的函数内联。 - - 构造和析构函数虽然函数体里面没什么东西,但是通常他们会做很多事情,基类和成员的构造、析构,异常处理等。通常也不是内联的好候选。 -- 内联也可能有一系列其他缺点: - - 因为没有生成函数代码导致定义发生改变时,必须重新编译。内联增加了模块间的耦合度。 - - 很多编译器在调试环境下禁止内联。 - -总结: -- 只对必要的代码做内联。 -- 80-20经验法则:平均而言一个程序往往将80%的时间花在20%的代码上。在profiling的时候再来做优化可能才是一个好选择,毕竟过早优化是万恶之源。 - -### 条款31:将文件间的编译依存关系降至最低 - -首先分别考虑类和函数的声明和定义对其使用到的自定义数据类型的声明和定义的依赖程度。 -- 最根本原则就是,编译器需要能够有足够的的信息来生成代码。 - -对于函数来说: -- 函数声明中:参数、返回值类型中可以出现任何只声明了但未定义的类型,以及其引用或指针。 -- 函数定义中: - - 参数中、返回值类型中、函数体中可以出现只声明未定义类型的引用和指针。但不能通过引用和指针去引用类型的成员,某些情况下就算只使用了引用或者指针也需要完整类型,比如做了`static_cast dynamic_cast`这种类型转换时需要转换构造函数/类型转换运算符、类的派生关系可见,换言之也就需要定义可见。 - - 参数、返回值、函数体中使用了自定义类型的变量,或者通过任何方式引用了自定义类型的任何成员,都需要自定义类型的定义可见而不能只有声明(即是完整类型)。 - -对于类来说: -- 成员函数声明和定义对用到的类型与普通函数的要求一致。 -- 数据成员是指针或者引用的话,只需要声明可见即可,如果是该自定义类型的数据成员的话,则要求是完整类型(以便确定占用内存空间)。 - -为了避免编译依赖我们应该做什么: -- 一般的构想是,能依赖于声明就不要依赖于定义。 -- 鉴于在头文件中我们只放声明(内联函数、模板除外,当然还以后类的定义,但成员函数仅仅放声明),所以只有在类成员中包含自定义类型的变量(而非指针引用)时,类定义才会依赖于成员类型的定义。 - - 此时只要这个成员类型的定义有修改就会影响所有使用了这个类以及其他用了这个成员类型的定义的函数、类的代码。牵一发而动全身,降低编译效率。 - - 解决方法可以是将成员类型换为其指针或者引用,但会带来内存资源管理的复杂度。 - - 当然包含标准库类型一般不会成为编译瓶颈,标准库类型不会更改,并且一般都有预编译头。 -- 考虑到上面一点,有两种手段可以实现使用该类的代码不依赖于其成员: -- 一种叫做Handle classes(句柄类): - - 典型实现如下: - ```C++ - class FooImpl; - class Foo - { - public: - Foo(Args ... args) : pImpl(make_shared(args...)) {} - private: - shared_ptr pImpl; - }; - ``` - - 将一个类型的实现全部委托给其实现类,接口类中仅仅只做一个转调。这种实现方式也叫做pImpl idiom(pImpl惯例)。 - - 所有涉及到这个类内部实现的东西都在其实现类中。所以修改成员、添加成员等不会影响接口的操作,不会引起使用该类的代码的重新编译。 -- 另一中实现叫做Interface classes(接口类): - - 典型实现: - ```C++ - class FooInterface - { - public: - FooInterface(Args ... args) = 0; - virtual ~FooInterface() = 0; - virtual xxx otherVirtualMethods() = 0; - xxx someNonVirtualFunc() { ... } - - // factory - static shared_ptr create(Args ... args) - { - return shared_ptr(new Foo(args...)); - } - }; - class Foo : public FooInterface - { - public: - Foo(Args ... args) {} - ~Foo() {} - xxx otherVirtualMethods() {} - }; - ``` - - 即将接口和实现分离,定义一个抽象类作为基类,接口全部定义为虚函数,在派生类中实现。非虚的接口则可以实现。 - - 将构造委托给静态工厂来做。 - - 实现内部的改变也不会影响到使用接口类的代码,只要接口不发生改变,用户代码都不需要重新编译使用该类的代码。 -- 但这两种方法都存在一定缺点: - - 前者进行了一层转调。 - - 后者所有接口调用都是虚调用,有一层间接层次,并且增加了一个虚指针的内存消耗。 - - 都有一定性能损耗与内存空间损耗。 -- 这两种方法也都可以隐藏内部实现细节,在实际生产中广泛使用,且更多用在对外部的API/SDK等强烈需要隐藏内部实现(比如类有哪些数据成员,有哪些私有函数)的地方。 -- 实际使用时还是应该权衡编译速度、运行时性能、代码解耦需求程度、代码规模等因素综合考虑是否使用。 -- 某些程序库还会提供单独的仅有声明的头文件以提供给自定义的头文件使用(如标准库``中声明了IO相关类型),而包含定义的头文件则提供给实现源文件来使用。 -- 以上做法是否涉及模板都可以使用。 - -## 第六章:继承与面向对象设计 - -### 条款32:确保你的公有继承建模出is-a关系 - -公有继承意味着is-a关系,根据面向对象的里氏替换原则,任何能用基类对象的地方应该要都能使用派生类对象。 -- 具体到C++语言中,因为引用和指针才具有多态,为了避免基类子对象被拷贝切割,应该说任何能使用基类指针或者引用的地方都可以使用派生类对象。 -- is-a的关系即是“是一个”的关系,也即是说每一个派生类对象一定是一个基类对象。 -- is-a关系的特点就是基类能做的事情,派生类也一定可以做。 -- 应该仔细思考要建模的事物是否满足这种关系。某些现实中表现为is-a关系的事物,用面向对象来描述时不一定就能完美地建模为公有继承。 - -### 条款33:避免遮掩继承而来的名称 - -讨论继承中的名称覆盖问题,其实和继承没有关系,而是和作用域有关。 -- 规则就是派生类作用域被嵌套在了基类作用域中,所以派生类的名称会隐藏基类的名称。 -- 名称可以是变量名、函数名、嵌套类名、枚举、类型别名。 -- 永远记住**名称查找先于类型检查**,从内层作用域向外找,找到的一定是最近的那一个,并且只会找到最近的那一个。 -- 在派生类成员函数中,一个名称的作用域查找顺序是:成员函数、派生类、基类、全局作用域。 -- 名称隐藏几乎总是简单清晰的,推荐的实践原则: - - 不要在派生类中重新定义基类的非虚函数。 - - 不要在派生类中定义与基类数据成员同名的数据成员。 - - 这两个原则已经基本足够了。 -- 但面临到虚函数有多个形式的重载时就有点特殊了: - - 一般来说如果有多个重载的话,通常要么都是虚函数,要么都是非虚函数。一部分虚而一部分非虚会让人非常迷惑。 - - 多个重载形式的虚函数在派生类中该怎么办呢? - - 如果我们覆写了全部,自然无任何问题。 - - 但如果只想覆写一部分,那么在派生类中就只能使用覆写的那一部分,因为名称查找只能查找到那一部分。【注意这其实违反了公有继承的is-a关系,这使得派生类无法使用未覆写的那一部分了。】 - - 一个可行做法是,覆写所有,不需要覆写的那一部分直接调用基类实现,做一个**转发**(forwarding)。但这也许并不是最佳做法。当然如果你确实仅需要其中一部分可见,使用转发无疑会更加灵活。 - - 最佳做法是使用**using声明**(`using Base::func;`),将派生类作用域声明该基类名字,使基类该名字可见,从而基类实现与派生类覆写的部分构成重载。 -- 总结: - - 名字查找先于类型检查。 - - 针对需要做到的事情选择合适的做法,确保你知道你做的事情意味着什么。 -- 当继承结合模板时,事情又有些不一样,见条款43。 - -### 条款34;区分接口继承与实现继承 - -公有继承中,派生类总是继承基类的所有接口,但继承接口还是继承实现亦有区别: -- 纯虚函数(pure virtual)只指定接口继承,派生类负责实现。 -- 非纯虚的虚函数(impure virtual)指定接口继承以及默认实现继承。 - - 当提供默认实现,但是又想要派生类显式指明想要继承实现时才给与实现继承可以这样做: - - 声明为纯虚,但提供默认实现,派生类中必须覆写,想要继承默认实现就去显式调用基类实现。 - - 声明为纯虚,默认实现提供在另一个非虚函数中(这样可以更精细地控制访问权限),派生类需要继承默认实现时在派生类覆写中显式去调用该默认实现。是否真需要这样做也有点争议。 -- 非虚函数(non-virtual)指定接口继承以及强制性的实现继承,也就是派生类基类应该使用同一实现,派生类绝不应该去重写(这会将基类实现隐藏,并且得不到多态的动态绑定特性)。 - -最后,final和override关键字可以阻止覆写、显式指明覆写,前者可以用来指定基类应该继承接口以及实现、后者用来表明覆写更加健壮与清晰。 - -### 条款35:考虑虚函数以外的其他选择 - -考虑使用非虚接口(Non-Virtual Interface)来实现模板方法(Temlate Method)模式: -- 简称NVI手法。 -- 即将接口定义为非虚函数,实现在基类中,在其中调用一个虚函数完成实际工作。 -- 这个虚函数可以是private、protected、public,取决于默认实现是否要对派生类可见(某些场景需要派生类首先调用基类实现就不能private必须protected)、是否对用户可见(这是实现,通常可能并不会选择暴露给用户)。 -- 好处是在基类非虚接口实现中可以在调用虚函数前后做一些更多的事情,比如获取/释放一个互斥锁、记录日志、验证约束条件、验证函数先决条件等。可以更加灵活,让客户直接调用虚函数则不是很好做这些事情(在每个实现中都这样做一遍显然不是一个好选择)。 - -借由函数指针实现策略(Strategy)模式: -- 如果一个接口的功能与具体类无关,而且可以在构造时由**外部定制**,甚至在**运行时变更**。那么可以使用策略模式,用函数指针来保存这个功能。 -- 有一点要求就是这个定制的函数需要仅由类的公有接口就能够实现功能。如果需要弱化类的封装,将这一族功能定义为友元,看起来就不是那么方便了。 -- 优点是:可以定制、可以运行时变更,缺点就是:需要弱化类的封装。可根据设计情况抉择。 - -使用`std::function`完成策略模式: -- 在C++11之后,基于函数指针的做法就显得有点死板了,有了`std::function`之后我们可以保存函数指针、函数对象、成员函数指针等多种可调用对象,弹性大大提高。 - -古典的策略模式: -- 古典的策略模式可能需要对这个功能本身再定义一个继承体系,将虚函数定义其最顶层基类(称其为功能类好了)中。然后我们要使用这一族功能的基类中包含一个该功能类的对象或者指针。 -- 在Java这种语言中可能就需要这样来实现。 -- 有了`std::function`之后还使用这种实现方式可能会显得有那么一点呆。但如果这个功能如此复杂以至于完全有必要搞一个复杂的继承体系来充分地复用,那么这种传统实现也不失为一个好选择。 - -最后,面向对象的设计是灵活多变的,而并非死板固定的,需要视实际情况抉择、视问题规模抉择、视运行性能抉择、视设计风格统一度来抉择。 - -### 条款36:绝不重新定义继承而来的非虚函数 - -这恐怕是面向对象设计的常识了。从实践层面考虑,要覆写,那么就定义为虚函数。定义为非虚,就一定不要去重新定义,因为得不到多态动态绑定的支持,会让代码陷入不确定性的漩涡。 - -从理论层面去考虑,非虚函数建立起了对该类型来说 **不变性(invariant)凌驾于特异性(sepcialization)** 的接口。公有继承is-a的关系也就是说每一个派生类对象都应该是一个基类对象。该非虚接口表示的属性是基类的一部分,也是这一族类型的一部分,它是is-a关系的组成部分。如果派生类中重新定义了基类非虚接口,那么就违背了is-a关系,也就不应该使用公有继承。 - -### 条款37:绝不重新定义继承而来的默认参数值 - -一言以蔽之:默认参数值是静态绑定,而虚函数是动态绑定,这是冲突的。重新定义继承而来的默认参数同样会让代码陷入不确定的漩涡。 - -通常来说,如果基类给了虚函数默认参数,那么选择可以是: -- 派生类不要再给默认参数,由基类指针引用调用该函数时可以使用基类的默认参数。由派生类调用则需要指定参数。 -- 派生类定义相同的默认参数,这会带来代码重复与依赖,如果基类默认参数改变,派生类所有默认参数必须相应改变。通常来说不要这样做。 -- 但如果就是要派生类也可以使用该默认参数呢?使用条款35提到的NVI(Non-Virtual Interface),将虚函数变为非虚接口并定义默认参数,实际工作在私有虚接口中做即可。 - -### 条款38:通过复合建模出has-a或者is-implemented-in-terms-of关系 - -复合(composition,也成组合、聚合(aggregation))是指一种类型对象内含其他类型对象的关系。 -- 它有两个含义:has-a有一个,或者is-implemented-in-terms-of(根据某物实现出)。 -- 当自定义类型对应于现实世界中的事物时,称这样的对象处于应用域(application domain)。复合发生在应用域内的对象之间时,表现出has-a的关系。 -- 当自定义类型对应于实现细节上的人工制品时(比如缓冲区、互斥锁、查找树),称之处于实现域(implementation domain)。复合发生在实现域时,表现出is-implemented-in-terms-of的关系。 - -### 条款39:明智而审慎地使用私有继承 - -我们称私有继承为继承实现(对应地公有继承是继承接口): -- 在派生类中基类子对象是private的,所以不可以在外部访问其接口,派生类指针引用也不会隐式转换为基类。 -- 私有继承通常意味着is-implemented-in-terms-of关系(某些时候我们也说是has-a关系)。而复合的含义也是如此。所以私有继承能做到的事情复合同样可以做到。 -- 某些时候可能只有复合能做到某些事情:复合可以阻止派生类重写虚接口。(当然现代C++中其实已经有final语法可以阻止重写虚接口了)。 -- 指导原则是:尽可能使用复合,在必要时才使用私有继承。 - -使用私有继承的理由:即大名鼎鼎的**空基类优化**: -- 对于没有任何非静态成员变量、没有任何虚函数、没有任何虚基类、的空类对象不会有任何存储空间,但是C++规定任何**独立非附属**对象都必须有非零大小(至少为1)。 -- 所以如果对这种类型使用组合的话,大小至少为1,再加上字节对齐,可能会造成一些不必要的内存消耗。 -- 而如果将这个类型作为基类的话,就可以实现大小为0,即是所谓的空基类优化(Empty Base Optimization)。 -- 通常在对内存十分在意的情况下才会使用这个技巧,库中(如STL)可能会比较常见,使用私有和保护继承很多时候是因为这个原因,但相比而言公有继承还是会占有压倒性的比例(通过超过99%)。 - -总结: -- 一般来说我们应该直接选择复合而非私有继承,在考虑每一毫的性能与内存占用时可以考虑因为空基类优化而使用私有继承。 -- 另一个使用私有继承的原因是:is-imlemented-in-terms-of关系,但派生类需要访问基类的保护成员、或者需要重新定义基类虚函数(继承+组合亦可做到)的情况。 -- 如果你考虑了多方因素之后依然有理由选择私有继承,那么选择私有继承也是可以接受的。程序设计是多样化的。 -- 现实世界中私有继承和保护继承相对来说比较罕见。 - -私有继承与保护继承: -- 这两者都是继承实现而非继承接口,区别仅仅是私有继承实现仅对该派生类可见,而保护继承中实现还对下层的派生类可见。 -- 保护继承亦可使用复合,并将访问权限设置为protected来取代。 - -### 条款40:明智而审慎地使用多重继承 - -讨论起多重继承这个话题,有忠实的拥护者,也有坚决的反对者。 -- 多重继承(multiple inheritance)就是继承了多个基类的意思。 -- 多重继承的首要问题就是可能会引入名称冲突,从多个基类继承了相同名称的成员、函数该怎么办。如果是函数的话,构成重载,如果无法决议,则需要显式指定作用域。如果是成员变量,则需要显式指定作用域。 -- 这些基类可能又有自己的继承体系,最复杂的情况会导致菱形继承: -``` - A - / \ - B C - \ / - D -``` -- 菱形继承的问题是,最终D中到底存储几份A的数据。C++支持存储一份与两份。 -- 存储两份是默认行为,存储一份则需要虚继承(令BC虚继承A)。 -- 虚继承是需要代价的:编译器做了若干幕后工作,最后只使用公共基类数据的虚继承最终产生的对象往往比非虚继承要大,访问虚基类成员变量时,往往比访问非虚基类慢。所以默认行为选择了非虚继承。 -- 虚继承的初始化规则也不同于普通多重继承:虚基类的初始化责任由继承体系中的最底层负责。 -- 忠告: - - 非必要不使用虚继承。 - - 非要使用虚基类,避免在其中放置数据,逃避初始化责任(这种行为就类型与Java或者C#中的纯粹的接口类了)。 -- 多重继承也是有合理用途的,比如公有继承与私有/保护继承混用(公有继承一个接口类,私有继承某个帮助实现的实现类),同时表达is-a和has-a/is-implemented-in-terms-of关系。 -- 多重继承无可替代(我们总是应该先考虑单一继承)或者有显著优势时,是值得使用的,相比私有、保护继承,多重继承可能使用频率还会更高一些。 -- 总之,明智而审慎地使用多重继承。 - -## 第七章:模板与泛型编程 - -### 条款41:了解隐式接口与编译期多态 - -在一个普通函数中,要使用多态,可以将参数定义为基类接口(指针引用),传入派生类对象来实现运行时多态,这种接口也叫做显式接口。 - -而在函数模板中,可以将使用模板类型参数作为函数参数类型,在函数中调用该模板参数类型的成员函数,只要拥有这些函数的类型(函数类的表达式有效)都可以做为模板类型实参用以实例化函数模板,这种接口被称作隐式接口(implicit interface)。对于重载的函数模板,在编译期确定的多态行为称之为编译期多态(compile-time polymorphism)。 -- 隐式接口仅仅由一组有效表达式组成。只要支持这一组表达式,就可以作为类型参数实例化模板。(很像动态类型的鸭子类型,不过发生在编译期)。 -- 编译期多态则是通过模板实例化与函数重载解析发生于编译期。 - -### 条款42:了解typename的双重意义 - -- 首先,typename用在模板类型参数中时,和class语义完全相同。我更倾向于使用typename。 -- 在模板中指代一个嵌套从属类型时,必须使用typename作为前缀,但不能使用在基类列表、以及构造函数成员初始化列表中作为基类修饰符。 - -模板内部`typename`用以显式表明这是一个类型: -- 指代类型必须加`typename`的原因是嵌套从属名称(nested dependent names,比如`T::iterator`这样是嵌套在模板类型参数T作用域的名称,非嵌套的则是普通从属名称dependent names比如`T&`,不依赖于模板参数的则是非从属名称non-dependent names比如int)可能导致解析困难。 -- 为了区分普通嵌套从属名称与嵌套从属类型名称,编译器在遇到一个嵌套从属名称(nested dependent type names)时,直接假定其不是一个类型名称。如果其是一个类型名称,需要在前面添加`typename`关键字。 -- `typename`只被用来验明嵌套从属名称。单纯的模板参数`T&`这种则不应该使用。 -- 应该用在参数、返回值类型等所有用到嵌套从属名称的地方。 -- 但不能用于基类列表、以及构造函数成员初始化列表中表示基类。(估计是这种情况能够确定一定是一个类型?)。这样的不一致有点令人烦恼。 -```C++ -template -class Derived : public Base::Nested // do not allow typename -{ -public: - explicit Derived(int x) : Base::Nested(x) // do not allow typename - { - typename Base::Nested temp; // must need typename - ... - } -} -``` - -### 条款43:学习处理模板化基类内的名称 - -类模板比普通类更为泛化,但是也就会造成更多的不确定,比如上面的`typename`需要显式指明一个嵌套从属名称。还有一点也很类似,就是在派生类模板中使用基类名称时需要显式指明其使用的是基类的东西(比如在不知道模板参数的情况下,解析到派生类模板的成员函数中调用了一个函数,不知道基类针对某一个模板参数是否进行了特化,这个特化中是否包含这个被调用的基类函数,所以需要在派生类中显式声明),有三个方法可以避免这个问题: -- 在基类函数调用前加上`this->`。 -- 使用`using`声明使基类名称可见(推荐做法)。 -- 显式使用作用域运算符指定使用基类函数。 - - 这样会导致不支持多态,如果是派生类非虚函数中调用基类虚函数的话不推荐这样做。 - - 当然如果是在重写的虚函数中调用基类实现,那么这就是标准做法。 -- 例: -```C++ -template -class Foo -{ -public: - void bar() {} -}; - -template -class DerivedFoo : public Foo -{ -public: - using Foo::bar; // solution 1 - void derivedBar() - { - bar(); // invalid without using declaration, there are no arguments to 'bar' that depend on a template parameter, so a declaration of 'bar' must be available - this->bar(); // solution 2 - Foo::bar(); // solution 3 - } -}; -``` - -### 条款44:将参数无关的代码抽离模板 - -定义了一个函数模板或者类模板时,对于不同的模板参数会生成不同的代码。模板参数是不同的,最终生成的代码也是不同的,但我们应该最大限度地提取出其中本质上来说是二进制相同的部分以减少最终的二进制代码冗余。 - -就像定义类时,如果多个类拥有相同的某些操作,我们不会重复实现他们,而会将他们提取到一个公共类中,使用继承或者组合来复用。 - -为了最大化地减少最终生成代码臃肿,我们应该使用共性与变性分析(commonality and variability analysis): -- 在模板中代码都是共用的,但是最终会生成二进制相同的代码,主要是只有非类型模板参数变化的那一部分。 -- 可以将涉及到非类型模板参数的代码提出来将非类型模板参数作为函数参数实现为模板参数无关的函数,将该部分代码提取到不含该非类型模板参数的公共基类模板中来做。在派生类中传入非类型模板参数去调用(继承实现,私有或者保护继承)。 -- 优点是能够减小生成的二进制体积,一族类模板使用同一函数来实现功能。 -- 缺点是生成的代码可能没有直接使用运行时的非类型模板参数作为常量表达式的版本高效(编译期常量版本能得到更好的优化,基于常量传播、常量折叠等手段),并且可能需要额外增加对象大小(可能需要在基类中存储必要信息以实现该函数)。 -- 例子: -```C++ -template -class SquareMatrixBase -{ -protected: - SquareMatrixBase(size_t n, T* pMem) : size(n), pData(pMem) {} - void setDataPtr(T* ptr) { pData = ptr; } - void invert(); // common function for all SquareMatrix, different N shares one invert() -private: - size_t size; - T* pData; -}; - -template -class SquareMatrix : private SquareMatrixBase -{ -public: - SquareMatrix() : SqaureMatrixBase(N, data) {} -private: - T data[N*N]; -}; -``` -- 因类型参数而造成的代码膨胀,也有可能可以消除,前提是他们拥有完全相同的模板实例化后的二进制代码。比如在容器中保存指针:用`void*`类型(无类型指针)可以保存所有类型指针,而不是使用强类型指针然后为所有指针类型生成同样的二进制代码。 -- 总结:无论怎样设计都需要权衡(tradeoff),精密的做法会让事情变得复杂,时空占用与代码复杂度代码清晰程度存在取舍,时间和空间也存在取舍。视具体情况抉择。 - -### 条款45:运用成员函数模板接受所有兼容类型 - -当我们实现智能指针这种类型时,要使其行为就像内置指针一样,就需要支持派生类指针向基类指针的转换。但是我们不可能为所有可能用到的具体类型定义转换构造函数,这时就需要在为类模板编写泛化的**成员函数模板**: -- 在实现过程中需要允许满足预期的合法行为,将非预期的非法行为筛选掉(让其在编译期报错)。通常来说这可以由实现中的有效表达式来约束。 -- 就智能指针这个例子:我们需要泛化的接受裸指针的构造函数、拷贝构造、拷贝赋值、移动赋值以及`get`接口等。 -- 泛化的构造、赋值运算符不会阻止编译器生成默认构造、默认赋值,如果要阻止编译器生成默认构造、默认赋值需要自行定义默认构造、默认赋值。 - -### 条款46:需要类型转换时请为模板定义非成员函数 - -条款24中说明了,要实现在所有实参上都能够进行隐式转换,应该将其定义为非成员函数。 - -但在模板中有点不一样,**因为在模板实参推导中,不将隐式类型转换考虑在内**。 -- 例子: -```C++ -template -class Rational -{ - friend const Rational operator*(const Rational& lhs, const Rational& rhs) - { - return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); - } - // equal to: - // friend const Rational operator*(const Rational& lhs, const Rational& rhs); -public: - Rational(const T& numerator = 0, const T& denominator = 1) : nume(numerator), denom(denominator) { } - const T numerator() const { return nume; } - const T denominator() const { return denom; } -private: - T nume; - T denom; -}; -``` -- 如果将`operator*`定义在类外部会导致`Rational(1, 2) * 2`这样的代码无法编译通过。因为在模板实参推导中,不考虑隐式类型转换。这就是C++的模板部分与OO部分的众多区别之一。 -- 那么要怎么做才能编译成功呢? -- 可行的方法是将这个非成员定义为友元,因为需要在类内有了声明,编译器便知道可以去匹配这个函数了。和友元的常见用法有点区别。 - - 上面的友元声明中,在类模板内部可以不写模板参数,如果使用类模板名称默认就是使用类模板同样的模板参数的意思,如果要定义成员模板函数或者泛化的友元模板才必须加(同时需要在前面加上`template`)。当然就类模板的同一个模板参数来说加不加都是可以的(前提是在作用域内,如果在类外实现,那么在进入作用域之前是必须加的,和类的作用域限定差不多)。 -- 但只有声明而没有定义会导致链接时找不到定义(即使在外部给了定义)。这时外部的定义依然没有实例化。解决方法可以是将模板函数的定义放在友元声明中,令友元声明成为一个定义。 -- 这时候声明为友元,并且在外部定义,且进行显式实例化也会链接时找不到函数,具体原理未知?进行了显式实例化定义依然未实例化? -- 看来目前来说只有定义为类模板内部的友元函数并在类内实现这一个途径处理。 -- 如果逻辑很长的话,可以转调一个外部函数。友元仅做一个转调以内联处理。 - -### 条款47:请使用traits类表现类型信息 - -看一个例子,编写标准库`std::advance(iter, diff)`功能,对迭代器移动给定的距离: -- 很容易想到,对不同类型的迭代器,实现可能不同,输入要求也不同。 -- C++为不同的迭代器类型定义了多个空的struct结构来标识: -```C++ -struct input_iterator_tag {}; -struct output_iterator_tag {}; -struct forward_iterator_tag : input_iterator_tag {}; -struct bidirectional_iterator_tag : forward_iterator_tag {}; -struct random_access_iterator_tag : bidirectional_iterator_tag {}; -``` -- 每个迭代器类中都会有一个名为`iterator_category`的类型别名,这个别名指代的类型就是上面的结构类型 -- 我们可能会想到在实现中去做这样一个`if`判断,用`typeid`去检测输入迭代器类型是否是对应类型。但是这样就不能兼容内置的指针类型了,因为内置类型中没有这样一个类型别名。还有`if`判断会带来运行时消耗,当然其实还会有编译的时候有不支持的操作导致编不过的问题。 -- 标准的做法是在定义一个`traits`类型,即标准库中的`std::iterator_traits`,其中定义了`iterator_category`类型别名,对于标准库中迭代器而言指代其内部的`iterator_category`类型,对于内置指针偏特化一个版本,将其定义为`random_access_iterator_tag`(即内置指针实质上等价于随机访问迭代器)。 -- 接下来在实现时使用traits类,根据模板参数中的迭代器类型获取到其tag结构类型,为不同类别的迭代器做一个重载,将实际工作转发到一个添加了tag参数的重载函数中做,即可实现编译期的分支选择。 -- 标准库`std::advance`实现模拟: -```C++ -// simulation of std::iterator_traits -template -struct my_iterator_traits -{ - using iterator_category = typename IterT::iterator_category; -}; -template -struct my_iterator_traits -{ - using iterator_category = std::random_access_iterator_tag; -}; - -template -void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) -{ - iter += d; -} - -template -void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) -{ - if (d >= 0) - { - while (d--) - ++iter; - } - else - { - while (d++) - --iter; - } -} - -template -void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) -{ - if (d < 0) - { - throw std::out_of_range("Negative distance"); - } -} - -// implementation of advance -template -void advance(IterT& iter, DistT d) -{ - doAdvance(iter, d, typename my_iterator_traits::iterator_category()); -} -``` - -如何设计并实现一个traits类: -- 确认若干希望将来可以取得的类型相关信息。(例如对于迭代器可以取得其分类) -- 为该信息选择一个名称。(这个例子中是`iterator_category`,更典型的是`value_type`) -- 提供一个模板和一组特化版本,内含希望支持的类型相关信息。 - -如何使用traits类: -- 建立一组重载函数或者函数模板,彼此差异只在traits参数。不同traits参数可以根据其提供的信息来编写不同具体实现。 -- 建立一个控制函数或者函数模板,使用traits类在编译期获得类型相关信息,用其来调用上面的重载函数或者重载函数模板。以实现根据类型在编译期选择特定实现的目的。 - -总结: -- traits类的作用:在编译期获得类型相关信息。通过模板和模板特化实现。 -- 通过整合重载技术,traits类可以在编译期对类型进行`if-else`测试。 -- 标准库中的traits类定义在``中,很多常用的traits类都在其中,比如:`remove_reference add_const add_pointer`等,都是通过类似手法来做的。 -- traits类是模板编程中的重要一环,可以通过这一条款认识其意义。 - -### 条款48:认识模板元编程(TMP) - -模板元编程(Template Metaprogramming,TMP)是编写基于模板的执行于编译期的C++程序,也就是通过编译这个过程来执行。一旦TMP程序结束执行,其执行的输出结果就是从模板实例化出的若干C++源码,一如往常会被编译。 -- TMP是图灵完全(Turing complete)的,也就是任何计算都能够在编译期做到。 -- 就像前面使用函数模板特化与重载和traits类来实现if-else一样。TMP中的各种程序结构和正常的C++中可能存在一定的区别。 -- TMP是嵌入在C++中的一门子语言,准确地说,一门**函数式语言**(functional language)。 -- 在TMP也可以进行循环,是通过递归模板实例化(recursive template instantiation)来做到的。 -- 起手式,编译期计算阶乘: -```C++ -template -struct Factorial -{ - enum { value = n * Factorial::value }; -}; - -template<> -struct Factorial<0> -{ - enum { value = 1 }; -}; -``` -- 可以看到`enum`常量在编译期的妙用,枚举值不占用对象空间,当写出`Factorial<10>::value`这种表达式时,它已经在编译期就算好了。(这叫enum hack,在条款2中介绍过。) -- 模板递归同普通递归一样,需要特别注意递归终止条件,TMP没有调试器,而模板特别是TMP相关的报错众所周知也是非常晦涩,所以编写起来更多地需要靠经验。 -- 更多内容这里也没有,需要另外的资料来学习(如《C++ Templates》)。 - -## 第八章:定制new和delete - -### 条款49:了解new-handler的行为 - -标准库``中定义了`new_handler`类型,是一个函数类型,签名是`void()`,含义是`operator new`无法分配够内存时调用的函数。 -- 可以通过`new_handler set_new_handler(new_handler) noexcept`这个函数设置,返回旧的`new_handler`。 -- 一个设计良好的`new_handler`必须做的事情(拥有很大的弹性,可以自行选择怎么处理): - - 让更多内存可被使用(比如释放某些不必要的内存)。 - - 安装另一个`new_handler`。(这个做法的变种之一是让`new_handler`修改自己的行为,为了达成这种目的,做法之一是修改静态或者全局数据。) - - 卸载`new_handler`,也就是`set_new_handler(nullptr)`,这样在内存不足时会执行默认行为抛出`bad_alloc`异常。 - - 抛出`bad_alloc`(或派生自其的)异常。 - - 不返回,通常调用`abort`或者`exit`结束程序运行。 -- C++支持类定制自己的`operator new`,但不支持其定制自己的`new_handler`。但我们可以自己实现这一点: - - 为类定义一个静态成员函数`set_new_handler`,类似于全局的,作用是为类的`operator new`设置专门的`new_handler`。 - - 当然上述的`set_new_handler`操作的数据应该是一个类的`new_handler`类型的静态数据成员。 - - 在`operator new`中做以下事情: - - 将该类的静态`new_handler`成员调用全局`set_new_handler`设置给全局,并保存全局的`new_handler`。 - - 调用全局`operator new`来分配内存。 - - 将全局的`new_handler`恢复回来。 - - 这个步骤可以通过自定义一个资源管理类来做,以保证抛出异常时能够正确恢复。 - - 典型实现: - ```C++ - class Foo - { - public: - static new_handler set_new_handler(new_handler nh) noexcept - { - new_handler oldHandler = currentNewHandler; - currentNewHandler = nh; - return oldHandler - } - void* operator new(size_t size) - { - currentNewHandler = set_new_handler(currentNewHandler); - void* pMem = ::operator new(size); - set_new_handler(currentNewHandler); - return pMem; - } - private: - static new_handler currentNewHandler; - }; - - new_handler Foo::currentNewHandler = nullptr; - ``` -- 为了避免调用全局`operator new`过程中抛出`bad_alloc`异常导致`new_handler`不能恢复的情况,更好的方式是使用RAII: - - 典型实现: - ```C++ - // RAII class that manage new_handler - class NewHandlerHolder - { - public: - explicit NewHandlerHolder(new_handler nh) : handler(nh) {} - ~NewHandlerHolder() { set_new_handler(handler); } - NewHandlerHolder(const NewHandlerHolder&) = delete; // prevent copying - NewHandlerHolder& operator=(const NewHandlerHolder&) = delete; - private: - new_handler handler; - }; - - class Bar - { - public: - static new_handler set_new_handler(new_handler nh) noexcept - { - new_handler oldHandler = currentNewHandler; - currentNewHandler = nh; - return oldHandler; - } - static void* operator new(size_t size) - { - NewHandlerHolder holder(set_new_handler(currentNewHandler)); - return ::operator new(size); - } - private: - static new_handler currentNewHandler; - }; - - new_handler Bar::currentNewHandler = nullptr; - ``` - - 任何类都可以这样做。在每个类做一次依然会带来代码的重复。 -- 最终级的做法是将这些功能定制为一个公共基类模板,只要派生就可以得到这个功能: -- 典型实现: -```C++ -// generic RAII -template -class NewHandlerSupport -{ -public: - static new_handler set_new_handler(new_handler nh) noexcept - { - new_handler oldHandler = currentNewHandler; - currentNewHandler = nh; - return oldHandler; - } - static void* operator new(size_t size) - { - NewHandlerHolder holder(set_new_handler(currentNewHandler)); - return ::operator new(size); - } -private: - static new_handler currentNewHandler; -}; - -template -new_handler NewHandlerSupport::currentNewHandler = nullptr; - -class Buz : public NewHandlerSupport -{ - -}; -``` -- 为了不同的类型拥有不同的静态`currentNewHandler`成员,需要将派生类加到基类的模板类型参数中。即使这个类型参数在基类中并没有被使用。这在模板编程中算是一个很常用的技术手段(初看起来确实奇怪)。 -- 这种手段主要用来表示:我要针对我自己继承某个模板,这个基类与继承该模板的其他派生类的基类是全然不同的类型。 -- 另外存在`nothrow`版本的`operator new`,主要用来兼容比较老的代码,行为是分配失败不抛出异常,而是返回空指针。但是众所周知`new`运算符包含两个阶段,分配内存和构造,这并不保证在构造中就不抛出异常,所以`nothrow`版本其实没有多少使用场景。 - -### 条款50:了解new和delete的合理替换时机 - -常见理由: -- 用来检测运用上的错误:检测是否有内存没有释放、多次delete、或者发生了overrun或者underrun(写入到分配区块之后或之前)。在替换的`operator new/delete`中管理这些事情。 -- 为了强化效能:现实实现中的`operator new/delete`采用中庸之道,既要适合小内存分配,也要满足大内存分配。所以不可能根据程序的内存分配状况表现出最佳的性能,而是对所有情况都表现出适度好的性能。如果你对你的程序的动态内存分配状况有深刻了解,可以定制`operator new/delete`替换标准库版本,以获得更佳的性能和内存占用。这属于比较高级的用法了。 -- 为了收集使用上的统计数据:在深度定制动态内存分配之前,必须先收集软件上的动态内存是怎么使用的的信息。区块大小分布如何?寿命分布如何?分配释放次序倾向于FIFO还是LIFO?最大动态内存分配量是多少?等等各种信息。这些信息就可以通过定制`operator new/delete`来实现。 -- 为了检测运行时错误。 -- 为了收集动态内存使用的统计信息。 -- 为了增加分配和释放的速度。 -- 为了降低内存管理器带来的额外空间开销。 -- 为了弥补分配器中的非最佳对齐。 -- 为了将相关对象组织得更加集中。降低换页频率,提高缓存命中。 -- 为了获得非传统的行为,比如将释放掉的内存置为0以提高数据安全性。 -- 总而言之,自定义`operator new/delete`属于比较高级的内容,写一个好用的分配器是不简单的,通常的程序可能不会这样做,大型程序中几乎都需要这样做。 - -### 条款51:编写new和delete时需要固守常规 - -无论什么目的,无论怎样实现,实现new和delete时有一些必须遵守的原则: - -首先是`operator new`: -- `operator new`应该实现的正确行为: - - 如果有能力提供该内存,就返回一个指针指向那块内存。 - - 如果没有能力,就抛出`std::bad_alloc`异常。 - - 还有条款49中提到的:如果`new_handler`为空,才抛出异常。如果不为空则在每次失败后调用new-handler函数。 - - C++规定,即使用户要求0字节,也返回一个合法指针。实现时可以简单处理为在分配0字节时分配1个字节。 -- 典型实现示例: -```C++ -void* operator new(size_t size) -{ - using namespace std; - if (size == 0) - { - size == 1 - } - while (true) - { - if (/*allocation is successful*/) - { - return /*pointer to memory*/; - } - new_handler globalNewHandler = get_new_handler(); - if (globalNewHandler) - { - (*globalNewHandler)(); - } - else - { - throw std::bad_alloc(); - } - } -} -``` -- 需要注意的是这个无限循环,如果设置了`new_handler`但是其中既没有抛出异常、也没有设置其他`new_handler`、也没有直接结束程序、也没有通过释放一部分内存来让下一次分配成功,那么就会一直死循环,所以`new_handler`必须做到条款49所述的事情。 - -为自定义类型定制的`operator new`: -- 关于`operator new`还需要注意的一点是,其可以被派生类继承,也就是说在基类中重载了`operator new`,动态分配派生类对象时也会使用基类的`operator new`。 -- 但通常来说基类的`operator new`可能是针对基类大小优化的,派生类大小改变了。因为可能为派生类分配内存,所以不能假定一定是为基类分配内存: -- 这时候的典型实现是在基类`operator new`中做一个判断,如果要分配的内存大小等于基类大小,照常做,不等则调用全局的`operator new`: -```C++ -static void* operator new(size_t size) -{ - if (size != sizeof(Base)) // include size == 0 - { - return ::operator new(size); - } - else - { - // process of base class allocation - } -} -``` -- 但是对于`operator new[]`这就行不通,同理我们也不能通过`size / sizeof(Base)`这种方式获取要分配的动态数组大小。只能所有大小同等处理。 - -关于`operator delete`: -- 需要记住的唯一一件事就是C++保证删除空指针永远是安全的。 -- 典型实现: -```C++ -void operator delete(void* pMem) noexcept -{ - if (pMem == nullptr) - return; - // process of delete -} -``` -- 成员版本与成员版本的`operator new`同理: -```C++ -static void operator delete(void* pMem, size_t size) noexcept -{ - if (pMem == nullptr) - return; - if (size != sizeof(Base)) - { - ::operator delete(pMem); - return; - } - // process of base class deallocation -} -``` - -### 条款52:写了placement new也要写placement delete - -每个人看到这里可能都会奇怪为什么会有placement delete这种东西,因为placement delete(狭义版本的)确实是不可用的,我们使用显式的析构调用来替代placement delete的地位。但读完之后你会发现placement new的定义被扩充了(所有加了多余参数的版本都可以叫做placement new,加了对应参数的`operator delete`也就是其对应的placement delete),所以对应的placement delete可以是有用的,并且只被用在非常有限的场景。 - -所以标题中并非我们通常意义上的狭义的placement new(因为狭义的placement new不分配内存,何来内存泄漏一说。),而是加了其他参数的广义版本的placement new的意思(也就是说同样要分配内存)。分清这一点就能理解了,细节不赘述,赘述了也大概率很久都用不到,直接看总结: - -总结: -- 当编写一个palcement operator new时,也需要写出对应的placement operator delete。如果没有这样做,则会在分配内存成功,但构造函数抛异常时,发生内存泄漏(编译器会使用placement new对应的placement delete来释放,如果没有就会直接不管从而造成内存泄漏)。 -- 当声明了placement new和placement delete时,请不要无意识地遮盖他们的正常版本。通常是说在类中定义的成员版本的情况: - - 方法1:为类定义所有需要的`operator new`和`operator delete`,其余的非关注的可以直接调用全局的实现。 - - 方法2:定义一个基类,实现所有重载的`operator new`和`operator delete`(直接调用全局版本),在派生类中使用`using`声明使基类名称`operator new`和`operator delete`可见,然后定义自己需要定制的版本。 - -## 第九章:杂项讨论 - -### 条款53:不要轻易忽视编译器的警告 - -- 严肃对待编译器提供的警告信息。 -- 不要过度依赖编译器的报警能力,不同编译器对待同一件事情的态度可能不同。 - -### 条款54:让自己熟悉TR1在内的标准程序库 - -已经成为历史,现已进入标准库,略。 - -### 条款55:让自己熟悉Boost - -如标题,去看[https://Boost.org](https://Boost.org)。 \ No newline at end of file diff --git a/Encoding.md b/Encoding.md deleted file mode 100644 index 96c1217..0000000 --- a/Encoding.md +++ /dev/null @@ -1,686 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [0. 总览](#0-%E6%80%BB%E8%A7%88) -- [1. 编码与字符集定义](#1-%E7%BC%96%E7%A0%81%E4%B8%8E%E5%AD%97%E7%AC%A6%E9%9B%86%E5%AE%9A%E4%B9%89) - - [1.1 字符集(Charset)](#11-%E5%AD%97%E7%AC%A6%E9%9B%86charset) - - [1.2 字符编码(Character Encoding)](#12-%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81character-encoding) -- [2. 大端与小端](#2-%E5%A4%A7%E7%AB%AF%E4%B8%8E%E5%B0%8F%E7%AB%AF) -- [3. 常见的字符集与编码](#3-%E5%B8%B8%E8%A7%81%E7%9A%84%E5%AD%97%E7%AC%A6%E9%9B%86%E4%B8%8E%E7%BC%96%E7%A0%81) - - [3.1 ASCII编码](#31-ascii%E7%BC%96%E7%A0%81) - - [3.2 Latin1/ISO-8859-1](#32-latin1iso-8859-1) - - [3.3 GB2312](#33-gb2312) - - [3.4 BIG5](#34-big5) - - [3.5 GBK](#35-gbk) - - [3.6 GB 18030](#36-gb-18030) -- [4. UniCode字符集与相关编码](#4-unicode%E5%AD%97%E7%AC%A6%E9%9B%86%E4%B8%8E%E7%9B%B8%E5%85%B3%E7%BC%96%E7%A0%81) - - [4.1 UniCode字符集](#41-unicode%E5%AD%97%E7%AC%A6%E9%9B%86) - - [4.2 UniCode字符平面映射](#42-unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84) - - [4.3 UTF-8](#43-utf-8) - - [4.4 UTF-16](#44-utf-16) - - [4.5 UTF-32](#45-utf-32) - - [4.6 BOM](#46-bom) - - [4.7 UCS-2与UCS-4](#47-ucs-2%E4%B8%8Eucs-4) - - [4.8 UTF实现对比](#48-utf%E5%AE%9E%E7%8E%B0%E5%AF%B9%E6%AF%94) -- [5. 编程语言中的字符](#5-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6) - - [5.1 C/C++](#51-cc) - - [5.2 java](#52-java) -- [6. 操作系统对字符的支持与处理](#6-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AF%B9%E5%AD%97%E7%AC%A6%E7%9A%84%E6%94%AF%E6%8C%81%E4%B8%8E%E5%A4%84%E7%90%86) - - [6.1 Windows代码页](#61-windows%E4%BB%A3%E7%A0%81%E9%A1%B5) - - [6.2 Linux](#62-linux) - - - -## 0. 总览 - -主流字符编码的一切,如果遇到新东西,也会不断补充。包括常见字符集、常见字符编码、常见语言中的字符编码,不同操作系统下的字符编码、ASCII,Latin1,MBCS,UniCode,utf-8,GB2312,GBK,大端与小端,码点,代理对,多文种平面,BOM等内容。 - -相关阅读: -- [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/) -- https://www.cnblogs.com/leesf456/p/5317574.html -- https://zhuanlan.zhihu.com/p/38333902 -- https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html -- https://zhuanlan.zhihu.com/p/51202412 -- https://zh.wikipedia.org/wiki/Unicode - - -## 1. 编码与字符集定义 - -计算机中储存的信息都是用二进制数表示的;而我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果。通俗的说,按照何种规则将字符存储在计算机中,如'a'用什么表示,称为"编码";反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密。在解码过程中,如果使用了错误的解码规则,则导致'a'解析成'b'或者乱码。 - - -### 1.1 字符集(Charset) - -是一个系统所支持的所有抽象字符的集合,字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。 - -常见字符集:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。 - -### 1.2 字符编码(Character Encoding) - -是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。 - -## 2. 大端与小端 - -对于单字节编码比如ASCII和Latin1来说,不存在大端和小端的说法。但对于多字节编码就存在这个概念。 - -**大端(Big Endian)**:高字节存储在低地址,低字节存储在高地址,更符合我们的日常使用习惯。 -**小端(Little Endian)**:高字节存储在高地址,低字节存储在低地址。 - -## 3. 常见的字符集与编码 - -### 3.1 ASCII编码 - -[ASCII](https://zh.wikipedia.org/wiki/ASCII) ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0,从**0x00-0x7F**。 - -学习C语言的时候都会讲的东西,[ISO页面](https://www.iso.org/standard/4777.html)。 - -控制字符: - -|二进制 |十进制 |十六进制 |缩写 |Unicode表示法 |脱出字符表示法 |名称/意义| -|:-:|:-:|:-:|:-:|:-:|:-:|:-| -|0000 0000 |0 |00 |NUL |␀ |^@ |空字符(Null)| -|0000 0001 |1 |01 |SOH |␁ |^A |标题开始| -|0000 0010 |2 |02 |STX |␂ |^B |本文开始| -|0000 0011 |3 |03 |ETX |␃ |^C |本文结束| -|0000 0100 |4 |04 |EOT |␄ |^D |传输结束| -|0000 0101 |5 |05 |ENQ |␅ |^E |请求| -|0000 0110 |6 |06 |ACK |␆ |^F |确认回应| -|0000 0111 |7 |07 |BEL |␇ |^G |响铃| -|0000 1000 |8 |08 |BS |␈ |^H |退格| -|0000 1001 |9 |09 |HT |␉ |^I |水平定位符号| -|0000 1010 |10 |0A |LF |␊ |^J |换行键| -|0000 1011 |11 |0B |VT |␋ |^K |垂直定位符号| -|0000 1100 |12 |0C |FF |␌ |^L |换页键| -|0000 1101 |13 |0D |CR |␍ |^M |CR (字符)| -|0000 1110 |14 |0E |SO |␎ |^N |取消变换(Shift out)| -|0000 1111 |15 |0F |SI |␏ |^O |启用变换(Shift in)| -|0001 0000 |16 |10 |DLE |␐ |^P |跳出数据通讯| -|0001 0001 |17 |11 |DC1 |␑ |^Q |设备控制一(XON 激活软件速度控制)| -|0001 0010 |18 |12 |DC2 |␒ |^R |设备控制二| -|0001 0011 |19 |13 |DC3 |␓ |^S |设备控制三(XOFF 停用软件速度控制)| -|0001 0100 |20 |14 |DC4 |␔ |^T |设备控制四| -|0001 0101 |21 |15 |NAK |␕ |^U |确认失败回应| -|0001 0110 |22 |16 |SYN |␖ |^V |同步用暂停| -|0001 0111 |23 |17 |ETB |␗ |^W |区块传输结束| -|0001 1000 |24 |18 |CAN |␘ |^X |取消| -|0001 1001 |25 |19 |EM |␙ |^Y |连线介质中断| -|0001 1010 |26 |1A |SUB |␚ |^Z |替换| -|0001 1011 |27 |1B |ESC |␛ |^[ |退出键| -|0001 1100 |28 |1C |FS |␜ |^\ |文件分割符| -|0001 1101 |29 |1D |GS |␝ |^] |组群分隔符| -|0001 1110 |30 |1E |RS |␞ |^^ |记录分隔符| -|0001 1111 |31 |1F |US |␟ |^_ |单元分隔符| -|0111 1111 |127 |7F |DEL |␡ |^? |Delete字符| - -可显示字符: - -|二进制 |十进制 |十六进制 |图形| -|:-:|:-:|:-:|:-:| -|0010 0000 |32 |20 |(space)| -|0010 0001 |33 |21 |!| -|0010 0010 |34 |22 |"| -|0010 0011 |35 |23 |#| -|0010 0100 |36 |24 |$| -|0010 0101 |37 |25 |%| -|0010 0110 |38 |26 |&| -|0010 0111 |39 |27 |'| -|0010 1000 |40 |28 |(| -|0010 1001 |41 |29 |)| -|0010 1010 |42 |2A |*| -|0010 1011 |43 |2B |+| -|0010 1100 |44 |2C |,| -|0010 1101 |45 |2D |-| -|0010 1110 |46 |2E |.| -|0010 1111 |47 |2F |/| -|0011 0000 |48 |30 |0| -|0011 0001 |49 |31 |1| -|0011 0010 |50 |32 |2| -|0011 0011 |51 |33 |3| -|0011 0100 |52 |34 |4| -|0011 0101 |53 |35 |5| -|0011 0110 |54 |36 |6| -|0011 0111 |55 |37 |7| -|0011 1000 |56 |38 |8| -|0011 1001 |57 |39 |9| -|0011 1010 |58 |3A |:| -|0011 1011 |59 |3B |;| -|0011 1100 |60 |3C |<| -|0011 1101 |61 |3D |=| -|0011 1110 |62 |3E |>| -|0011 1111 |63 |3F |?| -|0100 0000 |64 |40 |@| -|0100 0001 |65 |41 |A| -|0100 0010 |66 |42 |B| -|0100 0011 |67 |43 |C| -|0100 0100 |68 |44 |D| -|0100 0101 |69 |45 |E| -|0100 0110 |70 |46 |F| -|0100 0111 |71 |47 |G| -|0100 1000 |72 |48 |H| -|0100 1001 |73 |49 |I| -|0100 1010 |74 |4A |J| -|0100 1011 |75 |4B |K| -|0100 1100 |76 |4C |L| -|0100 1101 |77 |4D |M| -|0100 1110 |78 |4E |N| -|0100 1111 |79 |4F |O| -|0101 0000 |80 |50 |P| -|0101 0001 |81 |51 |Q| -|0101 0010 |82 |52 |R| -|0101 0011 |83 |53 |S| -|0101 0100 |84 |54 |T| -|0101 0101 |85 |55 |U| -|0101 0110 |86 |56 |V| -|0101 0111 |87 |57 |W| -|0101 1000 |88 |58 |X| -|0101 1001 |89 |59 |Y| -|0101 1010 |90 |5A |Z| -|0101 1011 |91 |5B |[| -|0101 1100 |92 |5C |\| -|0101 1101 |93 |5D |]| -|0101 1110 |94 |5E |^| -|0101 1111 |95 |5F |_| -|0110 0000 |96 |60 |`| -|0110 0001 |97 |61 |a| -|0110 0010 |98 |62 |b| -|0110 0011 |99 |63 |c| -|0110 0100 |100| 64| d| -|0110 0101 |101| 65| e| -|0110 0110 |102| 66| f| -|0110 0111 |103| 67| g| -|0110 1000 |104| 68| h| -|0110 1001 |105| 69| i| -|0110 1010 |106| 6A| j| -|0110 1011 |107| 6B| k| -|0110 1100 |108| 6C| l| -|0110 1101 |109| 6D| m| -|0110 1110 |110| 6E| n| -|0110 1111 |111| 6F| o| -|0111 0000 |112| 70| p| -|0111 0001 |113| 71| q| -|0111 0010 |114| 72| r| -|0111 0011 |115| 73| s| -|0111 0100 |116| 74| t| -|0111 0101 |117| 75| u| -|0111 0110 |118| 76| v| -|0111 0111 |119| 77| w| -|0111 1000 |120| 78| x| -|0111 1001 |121| 79| y| -|0111 1010 |122| 7A| z| -|0111 1011 |123| 7B| {| -|0111 1100 |124| 7C| || -|0111 1101 |125| 7D| }| -|0111 1110 |126| 7E| ~| - -**ASCII中的控制字符:** - -ASCII 编码中第 0~31 个字符(开头的 32 个字符)以及第 127 个字符(最后一个字符)都是不可见的(无法显示),但是它们都具有一些特殊功能,所以称为控制字符( Control Character)或者功能码(Function Code)。 - -这 33 个控制字符大都与通信、数据存储以及老式设备有关,有些在现代电脑中的含义已经改变了。 - -下面列出了部分控制字符的具体功能: - -- **NUL (0)** - -NULL,空字符。空字符起初本意可以看作为 NOP(中文意为空操作,就是啥都不做的意思),此位置可以忽略一个字符。 - -之所以有这个空字符,主要是用于计算机早期的记录信息的纸带,此处留个 NUL 字符,意思是先占这个位置,以待后用,比如你哪天想起来了,在这个位置在放一个别的啥字符之类的。 - -后来呢,NUL 被用于C语言中,表示字符串的结束,当一个字符串中间出现 NUL 时,就意味着这个是一个字符串的结尾了。这样就方便按照自己需求去定义字符串,多长都行,当然只要你内存放得下,然后最后加一个\0,即空字符,意思是当前字符串到此结束。 - -- **SOH (1)** - -Start Of Heading,标题开始。如果信息沟通交流主要以命令和消息的形式的话,SOH 就可以用于标记每个消息的开始。 - -1963年,最开始 ASCII 标准中,把此字符定义为 Start of Message,后来又改为现在的 Start Of Heading。 - -现在,这个 SOH 常见于主从(master-slave)模式的 RS232 的通信中,一个主设备,以 SOH 开头,和从设备进行通信。这样方便从设备在数据传输出现错误的时候,在下一次通信之前,去实现重新同步(resynchronize)。如果没有一个清晰的类似于 SOH 这样的标记,去标记每个命令的起始或开头的话,那么重新同步,就很难实现了。 - -- **STX (2) 和 ETX (3)** - -STX 表示 Start Of Text,意思是“文本开始”;ETX 表示 End Of Text,意思是“文本结束”。 - -通过某种通讯协议去传输的一个数据(包),称为一帧的话,常会包含一个帧头,包含了寻址信息,即你是要发给谁,要发送到目的地是哪里,其后跟着真正要发送的数据内容。 - -而 STX,就用于标记这个数据内容的开始。接下来是要传输的数据,最后是 ETX,表明数据的结束。 - -而中间具体传输的数据内容,ASCII 并没有去定义,它和你所用的传输协议有关。 -|帧头|数据或文本内容| - - - - - - - - - - - - - - - -
-帧头 -数据或文本内容
-SOH(表明帧头开始) -......(帧头信息,比如包含了目的地址,表明你发送给谁等等) -STX(表明数据开始) -......(真正要传输的数据) -ETX(表明数据结束
- -- **BEL (7)** - -BELl,响铃。在 ASCII 编码中,BEL 是个比较有意思的东西。BEL 用一个可以听得见的声音来吸引人们的注意,既可以用于计算机,也可以用于周边设备(比如打印机)。 - -注意,BEL 不是声卡或者喇叭发出的声音,而是蜂鸣器发出的声音,主要用于报警,比如硬件出现故障时就会听到这个声音,有的计算机操作系统正常启动也会听到这个声音。蜂鸣器没有直接安装到主板上,而是需要连接到主板上的一种外设,现代很多计算机都不安装蜂鸣器了,即使输出 BEL 也听不到声音,这个时候 BEL 就没有任何作用了。 - -- **BS (8)** - -BackSpace,退格键。退格键的功能,随着时间变化,意义也变得不同了。 - -退格键起初的意思是,在打印机和电传打字机上,往回移动一格光标,以起到强调该字符的作用。比如你想要打印一个 a,然后加上退格键后,就成了 aBS^。在机械类打字机上,此方法能够起到实际的强调字符的作用,但是对于后来的 CTR 下时期来说,就无法起到对应效果了。 - -而现代所用的退格键,不仅仅表示光标往回移动了一格,同时也删除了移动后该位置的字符。 - -- **HT (9)** - -Horizontal Tab,水平制表符,相当于 Table/Tab 键。 - -水平制表符的作用是用于布局,它控制输出设备前进到下一个表格去处理。而制表符 Table/Tab 的宽度也是灵活不固定的,只不过在多数设备上制表符 Tab 都预定义为 4 个空格的宽度。 - -水平制表符 HT 不仅能减少数据输入者的工作量,对于格式化好的文字来说,还能够减少存储空间,因为一个Tab键,就代替了 4 个空格。 - -- **LF (10)** - -Line Feed,直译为“给打印机等喂一行”,也就是“换行”的意思。LF 是 ASCII 编码中常被误用的字符之一。 - -LF 的最原始的含义是,移动打印机的头到下一行。而另外一个 ASCII 字符,CR(Carriage Return)才是将打印机的头移到最左边,即一行的开始(行首)。很多串口协议和 MS-DOS 及 Windows 操作系统,也都是这么实现的。 - -而C语言和 Unix 操作系统将 LF 的含义重新定义为“新行”,即 LF 和 CR 的组合效果,也就是回车且换行的意思。 - -从程序的角度出发,C语言和 Unix 对 LF 的定义显得更加自然,而 MS-DOS 的实现更接近于 LF 的本意。 - -现在人们常将 LF 用做“新行(newline)”的功能,大多数文本编辑软件也都可以处理单个 LF 或者 CR/LF 的组合了。 - -- **VT (11)** - -Vertical Tab,垂直制表符。它类似于水平制表符 Tab,目的是为了减少布局中的工作,同时也减少了格式化字符时所需要存储字符的空间。VT 控制符用于跳到下一个标记行。 - -说实话,还真没看到有些地方需要用 VT,因为一般在换行的时候都是用 LF 代替 VT 了。 - -- **FF (12)** - -Form Feed,换页。设计换页键,是用来控制打印机行为的。当打印机收到此键码的时候,打印机移动到下一页。 - -不同的设备的终端对此控制符所表现的行为各不同,有些会清除屏幕,有些只是显示^L字符,有些只是新换一行而已。例如,Unix/Linux 下的 Bash Shell 和 Tcsh 就把 FF 看做是一个清空屏幕的命令。 - - -- **CR (13)** - -Carriage return,回车,表示机器的滑动部分(或者底座)返回。 - -CR 回车的原意是让打印头回到左边界,并没有移动到下一行的意思。随着时间的流逝,后来人们把 CR 的意思弄成了 Enter 键,用于示意输入完毕。 - -在数据以屏幕显示的情况下,人们按下 Enter 的同时,也希望把光标移动到下一行,因此C语言和 Unix 重新定义了 CR 的含义,将其表示为移动到下一行。当输入 CR 时,系统也常常隐式地将其转换为LF。 -SO (14) 和 SI (15) -SO,Shift Out,不用切换;SI,Shift In,启用切换。 - -早在 1960s 年代,设计 ASCII 编码的美国人就已经想到了,ASCII 编码不仅仅能用于英文,也要能用于外文字符集,这很重要,定义 Shift In 和 Shift Out 正是考虑到了这点。 - -最开始,其意为在西里尔语和拉丁语之间切换。西里尔语 ASCII(也即 KOI-7 编码)将 Shift 作为一个普通字符,而拉丁语 ASCII(也就是我们通常所说的 ASCII)用 Shift 去改变打印机的字体,它们完全是两种含义。 - -在拉丁语 ASCII 中,SO 用于产生双倍宽度的字符(类似于全角),而用 SI 打印压缩的字体(类似于半角)。 - -- **DLE (16)** - -Data Link Escape,数据链路转义。 - -有时候我们需要在通信过程中发送一些控制字符,但是总有一些情况下,这些控制字符被看成了普通的数据流,而没有起到对应的控制效果,ASCII 编码引入 DLE 来解决这类问题。 - -如果数据流中检测到了 DLE,数据接收端会对数据流中接下来的字符另作处理。但是具体如何处理,ASCII 规范中并没有定义,只是弄了个 DLE 去打断正常的数据流,告诉接下来的数据要特殊对待。 - -- **DC1 (17)** - -Device Control 1,或者 XON – Transmission on。 - -这个 ASCII 控制符尽管原先定义为 DC1, 但是现在常表示为 XON,用于串行通信中的软件流控制。其主要作用为,在通信被控制符 XOFF 中断之后,重新开始信息传输。 - -用过串行终端的人应该还记得,当有时候数据出错了,按 Ctrl+Q(等价于XON)有时候可以起到重新传输的效果。这是因为,此 Ctrl+Q 键盘序列实际上就是产生 XON 控制符,它可以将那些由于终端或者主机方面,由于偶尔出现的错误的 XOFF 控制符而中断的通信解锁,使其正常通信。 - -- **DC3 (19)** - -Device Control 3,或者 XOFF(Transmission off,传输中断)。 - -- **EM (25)** - -End of Medium,已到介质末端,介质存储已满。 - -EM 用于,当数据存储到达串行存储介质末尾的时候,就像磁带或磁头滚动到介质末尾一样。其用于表述数据的逻辑终点,即不必非要是物理上的达到数据载体的末尾。 - -- **FS(28)** - -File Separator,文件分隔符。FS 是个很有意思的控制字符,它可以让我们看到 1960s 年代的计算机是如何组织的。 - -我们现在习惯于随机访问一些存储介质,比如 RAM、磁盘等,但是在设计 ASCII 编码的那个年代,大部分数据还是顺序的、串行的,而不是随机访问的。此处所说的串行,不仅仅指的是串行通信,还指的是顺序存储介质,比如穿孔卡片、纸带、磁带等。 - -在串行通信的时代,设计这么一个用于表示文件分隔的控制字符,用于分割两个单独的文件,是一件很明智的事情。 - -- **GS(29)** - -Group Separator,分组符。 - -ASCII 定义控制字符的原因之一就是考虑到了数据存储。 - -大部分情况下,数据库的建立都和表有关,表包含了多条记录。同一个表中的所有记录属于同一类型,不同的表中的记录属于不同的类型。 - -而分组符 GS 就是用来分隔串行数据存储系统中的不同的组。值得注意的是,当时还没有使用 Excel 表格,ASCII 时代的人把它叫做组。 - -- **RS(30)** - -Record Separator,记录分隔符,用于分隔一个组或表中的多条记录。 - -- **US(31)** - -Unit Separator,单元分隔符。 - -在 ASCII 定义中,数据库中所存储的最小的数据项叫做单元(Unit)。而现在我们称其字段(Field)。单元分隔符 US 用于分割串行数据存储环境下的不同单元。 - -现在的数据库实现都要求大部分类型都拥有固定的长度,尽管有时候可能用不到,但是对于每一个字段,却都要分配足够大的空间,用于存放最大可能的数据。 - -这种做法的弊端就是占用了大量的存储空间,而 US 控制符允许字段具有可变的长度。在 1960s 年代,数据存储空间很有限,用 US 将不同单元分隔开,能节省很多空间。 - -- **DEL (127)** - -Delete,删除。 - -有人也许会问,为何 ASCII 编码中其它控制字符的值都很小(即 0~31),而 DEL 的值却很大呢(为 127)? - -这是由于这个特殊的字符是为纸带而定义的。在那个年代,绝大多数的纸带都是用7个孔洞去编码数据的。而 127 这个值所对应的二进制值为111 1111(所有 7 个比特位都是1),将 DEL 用在现存的纸带上时,所有的洞就都被穿孔了,就把已经存在的数据都擦除掉了,就起到了删除的作用。 - -### 3.2 Latin1/ISO-8859-1 - -ASCII只使用7位二进制编码,只包含了英文字母,如果用一个字节进行编码那还剩下128个位置没有用完。所以后来ISO组织又制定了[ISO-8859-1](https://www.iso.org/standard/28245.html)编码,向下兼容ASCII,也被称为**Latin1、Latin-1、扩展ASCII码**。将128-255的码点也用上了。编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。 - -相关链接: -- [ISO-8859-1 的官网页面](https://www.iso.org/standard/28245.html),不过浏览标准的话需要购买。 -- [斯坦福大学的一个说明Latin-1编码的页面](https://cs.stanford.edu/people/miles/iso8859.html)。 -- [维基百科](https://zh.wikipedia.org/wiki/ISO/IEC_8859-1)。 - -0xA0 - 0xFF之间的字符一览: -``` - ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ -``` -Latin1可见字符如下: -``` -ISO/IEC 8859-1 - x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF -0x -1x -2x SP ! " # $ % & ' ( ) * + , - . / -3x 0 1 2 3 4 5 6 7 8 9 : ; < = > ? -4x @ A B C D E F G H I J K L M N O -5x P Q R S T U V W X Y Z [ \ ] ^ _ -6x ` a b c d e f g h i j k l m n o -7x p q r s t u v w x y z { | } ~ -8x -9x -Ax NBSP ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ SHY ® ¯ -Bx ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ -Cx À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï -Dx Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß -Ex à á â ã ä å æ ç è é ê ë ì í î ï -Fx ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ -``` - -Latin1除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中。 - ->ISO-8859-1对应于ISO/IEC 10646即Unicode的前256个码位。 -此字符集支持部分于欧洲使用的语言,包括阿尔巴尼亚语、巴斯克语、布列塔尼语、加泰罗尼亚语、丹麦语、荷兰语、法罗语、弗里西语、加利西亚语、德语、格陵兰语、冰岛语、爱尔兰盖尔语、意大利语、拉丁语、卢森堡语、挪威语、葡萄牙语、里托罗曼斯语、苏格兰盖尔语、西班牙语及瑞典语。 -英语虽然没有重音字母,但仍会标明为ISO/IEC 8859-1编码。除此之外,欧洲以外的部分语言,如南非荷兰语、斯瓦希里语、印尼语及马来语、菲律宾他加洛语等也可使用ISO/IEC 8859-1编码。 -法语及芬兰语本来也使用ISO/IEC 8859-1来表示。但因它没有法语使用的 œ、Œ、Ÿ 三个字母及芬兰语使用的 Š、š、Ž、ž ,故于1998年被[ISO/IEC 8859-15](https://zh.wikipedia.org/wiki/ISO/IEC_8859-15)所取代。(ISO 8859-15同时加入了欧元符号)。 - -其中编码**0xA9**比较常见,表示版权符号©。0x20是空格、0xA0是不换行空格、0xAD是选择性连接号。 - -因为Latin1编码范围使用了单字节内的所有空间,在支持Latin1编码的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作Latin1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。 - -### 3.3 GB2312 - -《信息交换用汉字编码字符集》是由中国国家标准总局1980年发布,1981年5月1日开始实施的一套国家标准,标准号是GB 2312-1980,[百度百科](https://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E4%BA%A4%E6%8D%A2%E7%94%A8%E6%B1%89%E5%AD%97%E7%BC%96%E7%A0%81%E5%AD%97%E7%AC%A6%E9%9B%86/8074272?fromtitle=GB2312&fromid=483170&fr=aladdin),[维基百科](https://zh.wikipedia.org/wiki/GB_2312)。 - -该文档可以在[国家标准全文公开系统](http://openstd.samr.gov.cn/bzgk/gb/index)中找到,注意现在是在推荐性国家标准,而不是强制性国家标准中。显示的标准号也是[GB/T 2312-1980](http://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=5664A728BD9D523DE3B99BC37AC7A2CC)。(自2017年3月23日起,该标准转化为推荐性标准,不再强制执行。) - -GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时,GB 2312收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。所以一共7445个图形字符。GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。 - -对于人名、古汉语等方面出现的罕用字,GB 2312不能处理,这导致了后来**GBK**及**GB 18030**汉字字符集的出现。 - -GB 2312中对所收汉字进行了分区处理,每区含有94个汉字/符号。这种表示方式也称为区位码。 -- 01-09区为特殊符号。 -- 16-55区为一级汉字,按拼音排序。 -- 56-87区为二级汉字,按部首/笔画排序。 -- 10-15区及88-94区则未有编码。 - -在GB 2312中,每个汉字字符使用2个字节来表示。第一个字节称为高位字节(也称区字节),第二个字节为低位字节(也称位字节)。 - -其中**高位字节使用0xA1-0xF7**(01-87区号加上0xA1),**低位字节使用0xA1-0xFE**(01-94位号加上0xA0)。如果知道了一个汉字的区号和位号就能知道其GB 2312编码。[要查看所有GB2312编码可以访问这里](https://www.qqxiuzi.cn/zh/hanzi-gb2312-bianma.php)。 - -示例:**奈芙莲** 在GB 2312的编码为 **C4CE DCBD C1AB**,每个汉字两个字节。 - -GB2312是**向下兼容ASCII**的(或者应该叫可以共存?),因为GB2312的所有编码都是两个字节并且每个字节的第一位都是1(因为从0xA1开始的),但肯定就不可能兼容Latin1了。所以一般ANSI编码的文档中汉字和英文同时存在时英文只占一个字节,中文则会占2个字节(在GB2312编码中的话)。 - -值得注意的是01-09区定义了特殊符号,其中在03区是包括了完整了英文大小写字符,不过这些字符是全角的,和ASCII的字符显示并不一样。 - -下列两行字母前者为全角也就是GB2312中的字母,后者为ASCII的字母(也就是半角符号)。全角的英文字母可能在某些场合有特殊用途吧。一般的中文输入法比如搜狗输入法提供了切换全角半角的功能,全角输入模式下输入的英文字母就是前者,半角下就是后者。其实最常见的还是全角和半角空格问题,刚开始学编程时很容易输入全角空格导致编不过,全角的字母和符号倒是非常好区分。 -``` -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z -``` -上述全角英文大写字母的GB2312编码为从0xA3C1-0xA3DA,而半角大写英文字母就是其ASCII编码为0x41-0x5A。 - -### 3.4 BIG5 - -也成[大五码](https://zh.wikipedia.org/wiki/%E5%A4%A7%E4%BA%94%E7%A2%BC),台湾地区繁体中文标准字符集,采用双字节编码,共收录13053个中文字,1984年实施。 - -### 3.5 GBK - -[GBK](https://zh.wikipedia.org/wiki/GBK)编码:1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,对汉字采用双字节编码。GBK字符集共收录21003个汉字,包含国家标准[GB13000-1](http://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=48A2A707F0A3CEC495BD6277EDDA0F9F)(已废止的国标)中的全部中日韩汉字,和BIG5编码中的所有汉字。 - -GBK向下完全兼容GB2312-80编码。支持GB2312-80编码不支持的部分中文姓,中文繁体,日文假名,还包括希腊字母以及俄语字母等字母。 - -GBK共收录21886个汉字和图形符号,其中汉字(包括部首和构件)21003个,图形符号883个。 - -GBK和GB 2312仍是目前广泛使用的中文编码。 - -### 3.6 GB 18030 - -也就是[GB 18030-2005](http://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=C344D8D120B341A8DD328954A9B27A99) (国家标准全文公开系统不过下载到的PDF不能直接打开,需要Adobe Reader并且安装FileOpen插件,联网才能解密,这...),[维基百科](https://zh.wikipedia.org/wiki/GB_18030)。 - -- 向下完美兼容GB 2312,基本兼容GBK。 -- GB 18030在其标准中以码表形式定义了除去代理对外的全部Unicode码位的定义,因此算得上是一种Unicode的变换格式(UTF)。 -- GB 18030包含三种长度的编码:单字节的ASCII、双字节的GBK(略带扩展)、以及用于填补所有Unicode码位的四字节UTF区块。 - -具体信息略过不谈。 - -## 4. UniCode字符集与相关编码 - -世界上存在多种字符编码方式,一串文本或者一个文本文件,如果保存和打开者、发送和接收方使用不同的编码,那么可能就会按照错误的方式解读从而出现乱码。而且面对一个需要混合多种语言的文本需求时单一的编码就会显得比较无力。 - -### 4.1 UniCode字符集 - -因此,在上世纪80年代末,Xerox、Apple 等公司开始研究,是否能制定一套多语言的统一编码系统。后来,多个机构成立了 Unicode 联盟,在 1991 年释出 Unicode 1.0,收录了 24 种语言共 7161 个字符。在四分之一个世纪后的 2016年,[Unicode](https://zh.wikipedia.org/wiki/Unicode) 已释出 9.0 版本,收录 135 种语言共 128237 个字符。 - -这些字符被收录为统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数**码点**(**code point**),码点的范围是 **0 至 0x10FFFF**,码点又通常记作 U+XXXX,当中 XXXX 为 16 进位数字。例如 奈 → `U+5938`、芙 → `U+8299`、莲 → `U+83B2`。很明显,UCS 中的字符无法像 ASCII 般以一个字节存储。 - -因此,Unicode 还制定了各种储存码点的方式,这些方式称为 Unicode 转换格式(**Uniform Transformation Format, UTF**)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会**把一个码点储存为一至多个编码单元(code unit)**。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。 - -注意,为了区分UniCode和其他编码比如ASCII,并不能直接将码点按照二进制存储,而要对其进行编码,也就是确定每个码点的存储方式(二进制格式)。从UniCode码点到具体的编码会经过一次转换。 - -### 4.2 UniCode字符平面映射 - -参见[UniCode字符平面映射](https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84),目前的UniCode字符被编排为17组,每组成为一个**平面**(Plane),每个平面65536个码点。目前只使用了少量平面。 - -|平面 |始末字符值 |中文名称 |英文名称| -|:-|:-|:-|:-| -|0号平面 |U+0000 - U+FFFF |基本多文种平面 |Basic Multilingual Plane,简称BMP| -|1号平面 |U+10000 - U+1FFFF |多文种补充平面 |Supplementary Multilingual Plane,简称SMP| -|2号平面 |U+20000 - U+2FFFF |表意文字补充平面 |Supplementary Ideographic Plane,简称SIP| -|3号平面 |U+30000 - U+3FFFF |表意文字第三平面 |Tertiary Ideographic Plane,简称TIP| -|4号平面至 13号平面 |U+40000 - U+DFFFF |(尚未使用)| || -|14号平面 |U+E0000 - U+EFFFF |特别用途补充平面 |Supplementary Special-purpose Plane,简称SSP| -|15号平面 |U+F0000 - U+FFFFF |保留作为私人使用区(A区)[1] |Private Use Area-A,简称PUA-A| -|16号平面 |U+100000 - U+10FFFF |保留作为私人使用区(B区)[1] |Private Use Area-B,简称PUA-B| - -3号平面到14号平面目前还没有使用,TIP(Plane 3) 准备用来映射甲骨文、金文、小篆等表意文字。 - -第4到第13平面尚无使用计划。 - -第15、16辅助平面都是私人使用区,可以用来存储自定义的字符。 - -### 4.3 UTF-8 - -UTF-8是互联网上使用最广的一种Unicode实现方式,有如下特点。 -- 它采用字节为编码单元,不会有字节序(endianness)的问题。 -- 每个 ASCII 字符只需一个字节去储存。 -- 如果程序原来是以字节方式储存字符,理论上不需要特别改动就能处理 UTF-8 的数据。 - -UTF-8的编码规则如下表: - -|码点范围|码点位数|字节1|字节2|字节3|字节4| -|:-|:-|:-|:-|:-|:-| -|U+0000 ~ U+007F |7 |0xxxxxxx | | | -|U+0080 ~ U+07FF |11 |110xxxxx |10xxxxxx | | -|U+0800 ~ U+FFFF |16 |1110xxxx |10xxxxxx |10xxxxxx | -|U+10000 ~ U+10FFFF |21 |11110xxx |10xxxxxx |10xxxxxx |10xxxxxx| - -UTF-8是向下兼容ASCII编码的,U+0000 ~ U+007F与ASCII编码完全一致。 - -比如编码汉字莲,码点为`U+83B2`,在`U+0800~U+FFFF`范围内所有最终结果应该为三个字节,码点转换为二进制`10000011 10110010`,将16个二进制位一次填充得到UTF-8编码为`11101000 10001110 10110010`,转化为16进制即是`E88EB2`。 - -在以英语为中心的互联网世界中广泛采用,因为英文只需要一个字节编码。而Plane 0中的常用汉字则需要3个字节。 - -### 4.4 UTF-16 - -UTF-16采用2个或者4个字节来编码(或者说由1个或2个编码单元来编码,每个编码单元16位),编码关系如下表: - -|平面|码点|编码| -|:-|:-|:-| -|Plane 0|U+0000 ~ U+FFFF|`xxxxxxxx xxxxxxxx`| -|Plane 1 ~ Plane 16|U+10000 ~ U+10FFFF|`110110yy yyyyyyyy 110111xx xxxxxxxx`| - -其中Plane 0中的字符直接将码点转化为16位二进制,编码为两个字节。而扩展平面上的字符则使用 **代理对(Surrogate Pair)** 编码。 - -代理对由一个 **高位代理(High Surrogate)** 和一个 **低位代理(Low Surrogate)** 来表示。 - -`110110yy yyyyyyyy`(`0xd800 - 0xdbff`)是高位代理,`110111xx xxxxxxxx`(`0xdc00 - 0xdfff`)是低位代理。为了能够区分2字节的UTF-16编码和代理对,Unicode将U+D800 ~ U+DFFF段预留了出来,不会有正常的Unicode码点处于这个范围。 - -代理对的编码方法: -- 将码点位于U+10000 ~ U+10FFFF内的码点值减去0x10000,会得到一个0x00000 ~ 0xFFFFF之间的20位二进制数。 -- 取左边10位`yy yyyyyyyy`,加上`0xD800`(或者直接其二进制位前加上`110110`),得到16位的高位代理`110110yy yyyyyyyy`。 -- 取右边10位`xx xxxxxxxx`,加上`0xDC00`(或者直接其二进制位前加上`110111`),得到16位的高位代理`110111xx xxxxxxxx`。 -- 组合之后得到32位的代理对。 - -解析时反之即可,解析时如果代理不成对,计算机通常不显示该代理字符。 - -可以看出对于ASCII编码也会使用2个字节,也会存在一定空间浪费。 - -### 4.5 UTF-32 - -UTF-32非常简单,使用一个32位的编码单元表示,就是将码点转化为32位二进制数存储,位数不够的话左边补0。 - -但这样编码会非常浪费空间,U+0000~U+00FF只有一个字节的有效内容,利用率仅四分之一,基本多文种平面(BMP, Plane 0)内的字符只会占用2个字节,利用率仅一半。而平时使用最多的英文字符都只有一个字节,最常用的汉字都在Plane 0中。所以UTF-32并不利于网络传输,存储效率也不高。实际很少使用。 - -### 4.6 BOM - -[BOM](https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F)即字节顺序标记(Byte Order Mark)。放在文件开始标记大端还是小端。 - -相应的UTF-16和UTF-32都有两种编码方式也就是BE大端和LE小端。 - -|UTF Encoding|BOM| -|:-|:-| -|UTF-8|`EF BB BF`| -|UTF-16 LE|`FF FE`| -|UTF-16 BE|`FE FF`| -|UTF-32 LE|`FF FE 00 00`| -|UTF-32 BE|`00 00 FE FF`| - -其实BOM就是`U+FEFF`这个码点在对应UTF实现下的编码,小端时低字节在前所以二进制位是`FF FE`,大端时则是`FE FF`,UTF-32则需要在高两个字节补0。 - -而对应的另一个码点`U+FFFE`则被Unicode定义为非字符,不应该出现在文本中,如果用错误的字节序读出了这个字符,那么就代表字节序反了,也就意味着`FF FE`只能被解释为小端序中的`U+FEFF`。 - -在UTF-8中,虽然在 Unicode 标准上允许字节顺序标记的存在,但实际上并不一定需要。UTF-8编码过的字节顺序标记则被用来标示它是UTF-8的文件。它只用来标示一个UTF-8的文件,而不用来说明字节顺序。 - -Unicode标准允许在UTF-8中使用BOM,但并不要求或推荐使用它。字节顺序在UTF-8中没有任何意义,所以它在UTF-8中的唯一用途是在开始时发出信号,表明文本流是用UTF-8编码的,或者表明它是从包含可选BOM的文本流转换到UTF-8的。该标准也不建议在有BOM的情况下将其删除,以便在不同的编码之间往返不会丢失信息,并使依赖BOM的代码继续工作。 - -通常我们称呼UTF-8时,一般是指没有BOM的UTF-8编码,如果有BOM的话一般称为UTF-8 BOM或者UTF-8 With BOM,中文叫做带BOM的UTF-8。 - -### 4.7 UCS-2与UCS-4 - -UCS-2用两个字节来表示Plane 0中的字符,在0000到FFFF的码位范围内,它和UTF-16基本一致,为什么说基本一致,因为在UTF-16中从U+D800到U+DFFF的码点不对应于任何字符,而在使用UCS-2的时代,U+D800到U+DFFF内的值被占用,所以不能表示扩展平面的码点。BOM机制同UTF-16。 - -UCS-4同UTF-32一致。大端和小端的BOM定义完全一致。 - -需要注意区分UCS-2和UTF-16。 - -### 4.8 UTF实现对比 - -UTF-8表示ASCII只需要一个字节,Plane 0的汉字(汉字都在U+0800 ~ U+FFFF内)会被编码到3个字节。而UTF-16则将Plane 0内的无论是ASCII还是汉字都编码到2个字节。所以如果英文字母更多UTF-8会更节约存储空间,如果汉字更多则是UTF-16更节约。 - -而UTF-32则效率较低,实际中也比较少使用。 - -UTF-8不需要考虑字节序,大部分网页都采用UTF-8编码传输。 - -## 5. 编程语言中的字符 - - -### 5.1 C/C++ - -**语言特性** - -C++中用宽字符wchar_t提供对Unicode和多字节编码的支持,char * 字符串有专门的封装类 std::string 来处理,标准输入输出流是 std::cin 和 std::cout 。对于 wchar_t * 字符串,其封装类是 std::wstring,标准输入输出流是 wcin 和 wcout。 - -C++标准规定了宽字符,但并没有规定宽字符占用几个字节。Windows 系统里的宽字符是两个字节,就是 UTF-16;而 Unix/Linux 系统里为了更全面的国际码支持,其宽字符是四个字节,即 UTF-32 编码。这为程序的跨平台带来一定的混乱,除了 Windows 程序开发常用 wchar_t* 字符串表示 UTF-16 ,其他情况下 wchar_t* 都用得比较少。 - -MFC 一般用自家的 TCHAR 和 CString 类支持国际化,当没有定义 _UNICODE 宏时,TCHAR = char,当定义了 _UNICODE宏 时,TCHAR = wchar_t,CString 内部也是类似的。Qt 则用 QChar 和 QString 类(内部恒定为 UTF-16),一般的图形开发库都用自家的字符串类库。 - -在新标准 C++11 中,对国际码的支持做了明确的规定: -- char * 对应 UTF-8 编码字符串(字面值表示为 u8"多种文字"),封装类为 std::string; -- 新增 char16_t * 对应 UTF-16 编码字符串(字面值表示为 u"多种文字"),封装类为 std::u16string ; -- 新增 char32_t * 对应 UTF-32 编码字符串(字面值表示为 U"多种文字"),封装类为 std::u32string 。 - -**源文件编码** - -在 Windows 系统里最常用的文本字符编码格式是 ANSI (简体是 GBK,繁体是 Big5)和 Unicode (UTF-16LE)格式,Windows 命令行默认的输入输出格式是 ANSI 的。在 Linux 系统里统统都是 UTF-8。 - -Windows命令窗口Cmd都有对应的输入输出的字符编码,就是代码页,用`chcp`命令查看和修改。Windows默认代码页936,也就是GBK编码。 - -windows下: -- Windows下GCC/G++不会对源文件中的字符串做转码。源文件是什么编码,那你得到的字符串中存储的就是对应的什么编码的字符串。如果源文件用GBK编码,并且在936的命令窗口运行,那么就是正常的,源文件如果用UTF-8,但命令窗口是GBK,那么就会乱码。 -- 而用MSVC的话如果是char*的字符串字面值,则会将不同编码的源文件转码为操作系统的ANSI多字节编码,wchar_t * 字符串一律转成 Unicode(UTF-16LE)。 -- 如果是Qt的话,官方推荐使用UTF-8。 - -Linux下: -- 请一律使用UTF-8编码源文件。 - - -### 5.2 java - -java字符串使用Unicode字符集,char使用Latin1或者UTF-16 BE。如果整个字符串都能用单字节表示,也就是Unicode码点在0 ~ 255,那么就用Latin1,否则就用UTF-16 BE。所以如果不在基本多文种平面的字符会使用代理对也就是两个字符来表示。把这样的字符字面值赋给单个字符是编不过的。 - -## 6. 操作系统对字符的支持与处理 - -### 6.1 Windows代码页 - -Windows用代码也来管理字符编码,通常用 ANSI 用来表示本机的编码。这样不同地区的不同编码都能被叫做ANSI。 -常见代码页: -Latin1 1252 -GBK 936 -GB 18030 54936 -Big5 950 -UTF-8 65001 - -### 6.2 Linux - -用UTF-8就完事了。 - diff --git a/Git.md b/Git.md deleted file mode 100644 index 972cfa7..0000000 --- a/Git.md +++ /dev/null @@ -1,734 +0,0 @@ - - -**目录** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Git学习笔记](#git%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0) - - [0. Git学习资料](#0-git%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99) - - [1. Git初见:安装与配置(Get Started)](#1-git%E5%88%9D%E8%A7%81%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AEget-started) - - [1.1 Git核心特点](#11-git%E6%A0%B8%E5%BF%83%E7%89%B9%E7%82%B9) - - [1.2 安装Git](#12-%E5%AE%89%E8%A3%85git) - - [1.3 安装后配置Git](#13-%E5%AE%89%E8%A3%85%E5%90%8E%E9%85%8D%E7%BD%AEgit) - - [1.4 获取帮助](#14-%E8%8E%B7%E5%8F%96%E5%B8%AE%E5%8A%A9) - - [2. Git基本功:基础功能](#2-git%E5%9F%BA%E6%9C%AC%E5%8A%9F%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD) - - [2.1 创建Git仓库](#21-%E5%88%9B%E5%BB%BAgit%E4%BB%93%E5%BA%93) - - [2.2 忽略列表](#22-%E5%BF%BD%E7%95%A5%E5%88%97%E8%A1%A8) - - [2.3 记录更新到git仓库](#23-%E8%AE%B0%E5%BD%95%E6%9B%B4%E6%96%B0%E5%88%B0git%E4%BB%93%E5%BA%93) - - [2.4 查看提交历史](#24-%E6%9F%A5%E7%9C%8B%E6%8F%90%E4%BA%A4%E5%8E%86%E5%8F%B2) - - [2.5 撤销操作](#25-%E6%92%A4%E9%94%80%E6%93%8D%E4%BD%9C) - - [2.6 远程仓库](#26-%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [2.7 标签](#27-%E6%A0%87%E7%AD%BE) - - [2.8 Git技巧](#28-git%E6%8A%80%E5%B7%A7) - - [3. Git必杀技:分支](#3-git%E5%BF%85%E6%9D%80%E6%8A%80%E5%88%86%E6%94%AF) - - [3.1 什么是分支](#31-%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E6%94%AF) - - [3.2 管理分支](#32-%E7%AE%A1%E7%90%86%E5%88%86%E6%94%AF) - - [3.3 如何利用分支管理开发的工作流程](#33-%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%E5%88%86%E6%94%AF%E7%AE%A1%E7%90%86%E5%BC%80%E5%8F%91%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B) - - [3.4 远程分支](#34-%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF) - - [3.5 分支的衍合](#35-%E5%88%86%E6%94%AF%E7%9A%84%E8%A1%8D%E5%90%88) - - [4. 服务器上的Git](#4-%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E7%9A%84git) - - [4.1 协议](#41-%E5%8D%8F%E8%AE%AE) - - [4.2 通过ssh协议架设Git服务器](#42-%E9%80%9A%E8%BF%87ssh%E5%8D%8F%E8%AE%AE%E6%9E%B6%E8%AE%BEgit%E6%9C%8D%E5%8A%A1%E5%99%A8) - - [4.3 生成SSH公钥](#43-%E7%94%9F%E6%88%90ssh%E5%85%AC%E9%92%A5) - - [4.4 Git服务器架设流程](#44-git%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9E%B6%E8%AE%BE%E6%B5%81%E7%A8%8B) - - [4.5 公共访问](#45-%E5%85%AC%E5%85%B1%E8%AE%BF%E9%97%AE) - - [4.6 GitWeb](#46-gitweb) - - [4.7 Gitosis](#47-gitosis) - - [4.8 Git托管服务](#48-git%E6%89%98%E7%AE%A1%E6%9C%8D%E5%8A%A1) - - [5. 分布式Git](#5-%E5%88%86%E5%B8%83%E5%BC%8Fgit) - - [5.1 分布式工作流程](#51-%E5%88%86%E5%B8%83%E5%BC%8F%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B) - - [5.2 为项目做贡献](#52-%E4%B8%BA%E9%A1%B9%E7%9B%AE%E5%81%9A%E8%B4%A1%E7%8C%AE) - - [5.3 管理自己的项目](#53-%E7%AE%A1%E7%90%86%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE) - - [TODO](#todo) - - - -# Git学习笔记 - -## 0. Git学习资料 - -- Git官网书籍资料:[ProGit](https://git-scm.com/book/en/v2) -- [ProGit中文翻译](http://iissnan.com/progit/)(本文参考) -- [菜鸟教程](https://www.runoob.com/git/git-tutorial.html)(极简使用教程) -- [廖雪峰Git教程](https://www.liaoxuefeng.com/wiki/896043488029600) - -## 1. Git初见:安装与配置(Get Started) - -### 1.1 Git核心特点 - -- 分布式版本管理软件,本地保存版本库所有信息。 -- 直接记录快照,而非文件差异。如果一个文件在该版本没有变化,则只对上一次快照做一个链接。 -- 近乎所有操作都是本地完成:从版本库中读取数据,做差异运算,提交。 -- 数据完整性:保存到Git版本库前,都会进行所有内容校验和计算(checksum),并将结果作为唯一标识和索引,数据传输或者磁盘损坏导致数据不完整都会立刻被察觉。使用SHA-1哈希值:40个16进制字符,作为保存到Git数据库的索引。 -- 多数操作仅对数据库添加数据,一旦提交快照就不用再担心数据丢失。 -- 文件的三种状态:已提交(commited),已修改(Modified),已暂存(Staged)。 -- 三个工作区域:工作目录,暂存区,本地仓库。 -- Git版本库保存位置:`.git`目录。 -- 基本Git工作流程: - 1. 工作目录修改文件。 - 2. 对修改文件进行快照,保存到暂存区。 - 3. 提交更新到Git版本库。 - -### 1.2 安装Git - -- 下载:[https://git-scm.com/](https://git-scm.com/) -- Windows安装:为所有用户安装,记得勾选添加到环境变量。 -- Linux安装: - - `yum install git-core` - - `apt install git` - - 编译安装:见[这里](http://iissnan.com/progit/html/zh/ch1_4.html)。 - -### 1.3 安装后配置Git - -安装后需要配置Git工作环境,更新后沿用配置,也可以随时使用同样命令更改配置。 - -- 命令:`git config` -- 不同级别配置、存放位置: - -|配置级别|Windows配置文件|Linux配置文件|Git选项|作用范围| -|:-:|:-:|:-:|:-:|:-:| -|系统|`安装目录/etc/gitconfig`|`/etc/gitconfig`|`--system`|所有用户| -|全局|`$HOME`,即用户根目录,一般来说`C:\Users\your-user-name\.gitconfig`|`~/.gitconfig`|`--global`|当前用户| -|本地|`.git/config`|`.git/config`|`--local`|本地仓库| - -- 每个级别的配置会覆盖上一级的同名变量,所以对一个项目,项目配置 > 用户配置 > 系统配置。 -- 用户信息: - - `git config --global user.name "your-name"` - - `git config --global user.email "your-email"` -- 不同级别的配置选项: -``` ---global use global config file ---system use system config file ---local use repository config file ---worktree use per-worktree config file -``` -- 查看配置信息,`--list`,直接编辑配置文件`--edit`。 - -### 1.4 获取帮助 -- `git help ` -- `git --help` -- `man git-` -- Windows中可以使用前两者,会打开本地网页帮助文档。 -- Linux中三者同样效果,打开对应的man手册。 - - -## 2. Git基本功:基础功能 - -最基本最常用的Git命令。 - -### 2.1 创建Git仓库 - -- 将已有文件添加到Git版本控制:在你的工作目录中初始化新仓库,拷贝文件到git目录,直接在需要进行版本管理的工作目录进行初始化,提交为初始版本。 -```shell -git init -cp [your-file] ./ -git add * -git commit -m "initial projet version" -``` -- 从现有仓库克隆,完整克隆仓库的所有提交历史。 -```shell -git clone [url] [local-repo-name] -``` - -### 2.2 忽略列表 - - -忽略某些文件:无需进行版本管理的文件,将其列入`.gitignore`中,将不会再进行跟踪,`git status`时将不再显示其未跟踪状态。一般情况下我们可以选择将项目生成的二进制文件,临时文件,自己本地特有但不需要进行版本管理的文件加入忽略列表。 -``` -*.[oa] -*~ -``` -`.gitignore`格式规范: -- 所有空行或以注释符号`#`号开头的行被忽略。 -- 可以使用标准的glob模式匹配。 -- 匹配模式后加`/`说明要忽略目录。 -- 可以在要忽略的文件或者目录模式前加`!`取反。 - -**glob模式**是指Shell使用的简化的正则表达式。规则: -- `*`号匹配零个或者多个任意字符。不包括表示目录的`/`或者`\`(Windows中)。`**`则可以匹配任意字符,包括目录。 -- `[abc]`匹配任意一个方括号中的字符,`[0-9]`匹配0到9的任意一个字符。 -- `?`匹配一个任意字符。 - -例子: -```shell -# 忽略所有.a文件,除了lib.a -*.a -!lib.a -# 忽略TODO文件和bulid文件夹 -/TODO -/bulid/ -# 忽略doc/目录下的所有txt文件和忽略doc/以及其子目录下所有txt文件 -doc/*.txt -doc/**/*.txt -``` - - -### 2.3 记录更新到git仓库 - -Git仓库中的文件状态: -1. 已跟踪(tracked):已纳入版本管理。 -2. 未跟踪(untracked):未纳入版本管理。 - -![Git的文件状态变化周期](Images/git_file_lifecycle.png) - -Git的版本管理操作: -- 检查当前Git仓库文件状态:`Git status`。 -- 添加文件到暂存区:`git add`。这个命令用于跟踪未跟踪文件,暂存已修改文件。 -- 比较工作区相对暂存区差异:`git diff`。即是修改后还没有暂存起来的内容。 -- 比较已经暂存的内容相较上次提交(版本库内最新内容)之间的差异:`git diff --cached`或者`git diff --staged`。 -- 提交更新:`git commit`,Git会进入默认编辑器提示你输入提交信息。编辑器一般是vim,有由`git config core.editor`指定。编辑时注释中会包含此次提交改动的文件信息。加入`-v`选项则文件的具体差异都会显示在提示中,帮助你更好的编写提交信息,提交时注释会被丢弃。加入`-m`选项则直接在其后添加提交信息参数,用`""`包含,则不会进入编辑。得到的提示信息如下: - ``` - [master 60dcae0] update ignore list & hello.c - 2 files changed, 9 insertions(+) - ``` - - 提示中告诉你提交到了那个分支,本次提交的SHA-1校验和是多少(只是开头的一部分),有多少文件被修改,多少行被添加修改删除。 -- 提交时跳过使用暂存区域:`git commit -a -m "commit message"`,跳过`git add`步骤。 -- 从Git中移除文件:`git rm`,从跟踪清单(确切地说从暂存区域)移除,同时从本地删除,这样提交时就会从将文件从版本库删除了。仅删除本地文件的话不会从暂存区域移除。 -- 如果只想从版本库移除,而不想从本地删除,则使用`git rm --cached [file]`,保留本地文件,移除暂存区文件(不再跟踪),下次提交时即可生效。使用`git rm`也可以使用文件模式匹配规则。并且git在Shell之上也有自己的扩展模式匹配规则(不展开说)。 -- 移动文件:`git mv [file_from] [file_to]`,仓库中的元数据并不会体现出这是一次改名操作。不过Git会推断出发生了什么。其实运行`git mv readme.md readme.txt`就相当于(必须先`rm`再`add`): - ```shell - mv readme.md readme.txt - git rm readme.md - git add readme.txt - ``` - 分开操作git也会意识到这是一次改名操作,怎么做到的?思考。 - -### 2.4 查看提交历史 - -查看提交记录命令:`git log`。每次提交记录中都会显示:SHA-1校验和,作者和邮箱,提交时间,提交说明。 -- 添加选项`-p`显示提交文件差异。 -- `-2`只显示最近两次更新。 -- `--stat`仅显示简要的增改行数统计 -- 另外`--pretty`提供了不同的参数:`oneline`,`short`,`full`,`fuller`可以显示不同风格的记参数还可以是`format`用于自己定制要显示的记录格式,这样的输出方便后期编程提取(哇,细节太多。常用格式占位和说明: - ``` - %H 提交对象(commit)的完整哈希字串 - %h 提交对象的简短哈希字串 - %T 树对象(tree)的完整哈希字串 - %t 树对象的简短哈希字串 - %P 父对象(parent)的完整哈希字串 - %p 父对象的简短哈希字串 - %an 作者(author)的名字 - %ae 作者的电子邮件地址 - %ad 作者修订日期(可以用 -date= 选项定制格式) - %ar 作者修订日期,按多久以前的方式显示 - %cn 提交者(committer)的名字 - %ce 提交者的电子邮件地址 - %cd 提交日期 - %cr 提交日期,按多久以前的方式显示 - %s 提交说明 - ``` - 用法:`git log --pretty=format:"%h - %an , %ar : %s"`。还要细心注意作者和提交者的区不得不说细节真的到位,也是真的多。 -- `--graph`选项:可以显示ASCII图形表示的分支合并历史。 - -常用选项和含义: -``` -选项 说明 --p 按补丁格式显示每个更新之间的差异。 ---word-diff 按 word diff 格式显示差异。 ---stat 显示每次更新的文件修改统计信息。 ---shortstat 只显示 --stat 中最后的行数修改添加移除统计。 ---name-only 仅在提交信息后显示已修改的文件清单。 ---name-status 显示新增、修改、删除的文件清单。 ---abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。 ---relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。 ---graph 显示 ASCII 图形表示的分支合并历史。 ---pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。 ---oneline --pretty=oneline --abbrev-commit 的简化用法。 -``` - -- `-`限制显示提交次数。 -- `--since`,`--until`给定时间限制。 -- 还可以搜索:`--author`搜索指定作者,`--grep`按关键字搜索。 -相关选项: -``` -选项 说明 --(n) 仅显示最近的 n 条提交 ---since, --after 仅显示指定时间之后的提交。 ---until, --before 仅显示指定时间之前的提交。 ---author 仅显示指定作者相关的提交。 ---committer 仅显示指定提交者相关的提交。 -``` - -不得不说,使用命令行去显示,查看和搜索记录就不是那么的方便,显得有点繁琐且选项众多,增大了记忆负担。使用图形化的界面查看提交历史可能看起来会更清晰简单一些。使用`gitk`或者`git-gui`也可以清晰的使用图形界面查看提交信息。这两个好像还不是一个东西,值得研究一下,后续再展开。 - -### 2.5 撤销操作 - -- 修正最后一次提交:`git commit --amend` -- 取消暂存的文件:`git restore --staged `。执行`git status`是有已暂存的文件时提示中便会显示这个命令。或者`git reset head `。我的提示前者,可能老版本会提示后者,都能用。 -- 放弃本地文件修改:`git restore `,或者`git checkout -- `。所以同一个功能为什么有不同命令,版本更新追加的吗?如果放弃本地所有文件修改`git checkout .`。 -- 所有已经提交的数据都会被记录,都可以被恢复。可能失去的数据,只可能是没有提交的数据。当然前提是你不能把版本库删了,而且其他人也没有克隆。 - -### 2.6 远程仓库 - -- 要和别人协助开发,版本库当然不能只位于本地。远程仓库指托管到网络上的仓库。可能只读,可能可以写。管理远程仓库,以便推送拉取数据,分享各自最新工作进展。 -- 查看当前远程仓库:`git remote`。克隆一个项目后,默认远程仓库名是`origin`。如果本地通过`git init`创建,则没有远程仓库。 - - 远程仓库不一定非得位于远程,同样可以从本地仓克隆,被克隆的仓库就是`origin`。 - - 一个本地仓库可以有多个远程仓库。`-v`可以全部列出。 -- 添加远程仓库:`git remote add [remote-name] [url]`。此时就可以用这个`[remote-name]`指代远程仓库了。 -- 从远程仓库抓取数据:`git fetch [remote-name]`。将远程仓库所有数据抓取到本地仓库,完成后可在本地访问仓库所有分支。 -- 如果设置了某个分支用于跟踪远程仓库的某个分支(如何设置?注意观察提示信息。),那么使用`git pull`将会自动抓取远程分支合并到本地仓库中当前分支。完成命令`git pull [remote-name] [branch-name]`。 -- 默认情况下`git clone`就是自动创建本地`master`分支用于追踪远程`master`分支。克隆会自动使用`master`分支和远程仓库`origin`名称。(关于分支,暂时不清楚,后续详细了解。) -- 推送数据到远程仓库:`git push [remote-name] [branch-anme]`。 -- 查看远程仓库详细信息:`git remote show [remote-name]`。 -- 远程仓库重命名:`git remote rename [old-remote] [new-remote]`。 -- 移除远程仓库:`git remote rm/remove [remote-name]`。 - -### 2.7 标签 - -打标签:对某一个时间节点上的版本打上标签,比如发布版本时(如V1.0等)。 - -- 用法:`git tag [-a | -s | -u ] [-f] [-m | -F ] [-e] [ | ]`。 -- 列出所有标签:`git tag`。或者`git tag -l`。 -- 两种标签:轻量级(LightWeight)和含附注的(Annotated)。轻量级标签就是一个指向特定提交对象的引用。含附注标签则是存储在版本库中的独立对象,有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明。 -- 创建一个含附注的标签:`git tag -a [tag-name] -m "message"`。轻量标签的话直接使用:`git tag [tag-name]`。给特定版本指定标签则最后一个参数指定为该版本(用校验和代表)。 -- 显示标签信息:`git show [tag-name]`。轻量级标签则只会显示打上标签的那个版本的信息,含附注的标签则会显示附注信息。 -- 列出所有标签:`git tag -l`。 -- 删除标签:`git tag -d [tag-name]`。 -- 推送到远端仓库时默认不会推送标签,使用`git push [remote-name] [branch-name] [tag-name]`推送单个标签,使用`--tags`推送所有新增标签。拉取时会将标签拉下来。 -- 问题:我本地删除了标签的话,要怎么将标签删除推送到远端仓库呢?试了`-d`删除,直接`push --tags`没有用。 - -### 2.8 Git技巧 -- 自动补全,安装后应该就有了,Windows上安装Git Bash和Linux安装Git应该是直接就可以使用的。如果需要自定义:[看这里](http://iissnan.com/progit/html/zh/ch2_7.html)。 -- Git命令别名: - ``` - git config --global alias.co checkout - git config --global alias.br branch - git config --global alias.ci commit - git config --global alias.st status - git config --global alias.unstage 'reset HEAD --' - git config --global alias.last 'log -1 HEAD' - git config --global alias.visual '!gitk' // 外部命令需要加一个! - ``` - - -## 3. Git必杀技:分支 - -使用分支可以从主线开发流程中独立出一个开发流程来,然后在不影响主线的同时并行开发。在其他版本控制系统(VCS)中,通常需要创建一个源代码工作目录的完整副本。 - -但将Git的分支模型称为必杀技也是因为这点将Git和其他VCS家族区分了出来,Git的分支难以置信的轻量。创建可以快速完成,在不同分支间切换也同样可以快速完成。 - -Git鼓励在工作流程中频繁使用分支与合并,理解分支并熟练运用,将真正改变你开发的方式。(如是说,我很期待) - -### 3.1 什么是分支 - -在了解什么是分支前,先必须回顾Git如何保存数据:Git不保存差异,而是保存一系列文件快照。Git中提交时,会保存一个提交对象,该对象包含一个指向暂存区域快照的指针,并且包含本次提交作者等附属信息,0个或者多个改提交对象的父对象指针:首次提交无祖先,普通提交一个祖先,两个或多个分支合并产生的提交则有多个祖先。 - -暂存操作会对每一个文件计算SHA-1校验和,然后把当前版本文件快照保存到Git仓库(使用Blob类型对象存储这些快照。是一个文件一个Blob对象吗?),将校验和加入暂存区域。 - -然后使用`git commit`新建一个提交对象前,Git会先计算每一个子目录的校验和,然后将这些目录存储为Git仓库中的树(tree)对象。然后创建的提交对象,除了包含相关提交信息之外,还包含指向这个树对象(这个树对象可能不是根目录吗?如果只更改了一个子目录中的内容。)的指针。有了树对象,就可以重现快照的内容了。 - -![单个提交对象在仓库中的数据结构](Images/git_commit_object.png) - -做一些修改后再次提交,本地提交对象会包含一个指向上次提交的指针。 - -![多个提交对象链接关系](Images/git_commit_object_link.png) - -现在来看什么是**分支**(branch),分支本质上就是指向某个提交对象的可变指针。 - -![分支](Images/git_commit_object_branch.png) - -那么创建分支其实就是创建一个新的指向当前提交对象的指针。也就是说创建一个分支只需要写入一个40字节的SHA-1校验和的成本。这就和其他新建分支需要备份所有项目文件到特定目录的VCS区别开来。 -- 创建新分支命令:`git branch [branch-name] [commit]`。 - -![当前版本上创建新分支](Images/git_commit_object_branches.png) - -那么Git如何知道当前在那个分支上工作呢?答案是**HEAD指针**,HEAD指针是指向当前正在工作的本地分支的指针。`git branch`只会创建新分支,但并不会自动切换到新分支, -- 切换到分支:`git checkout [branch-name]`。 - -![创建并切换到testing分支之后进行了一次提交](Images/git_commit_obejct_HEAD_pointer.png) - -### 3.2 管理分支 - -分支操作: -- 创建新分支:`git branch [new-branch-name] [commit]`。 -- 切换当前工作目录要跟踪的分支:`git checkout [branch-name]`。 -- 创建新分支同时切换:`git checkout -b [new-branch]`。 -- 合并分支到当前分支:`git merge [branch-name]`。 - -合并时如果当前HEAD指向的版本是要合并的版本的祖先的话,那么就代表要合并的分支是直接在当前版本上做的修改,中间未进行过其他修改,直接将当前分支的指针移动到要合并的提交对象即可,不会有冲突。但如果当前分支有提交,并且有冲突(比如两条线中都修改了同一个文件的同一个位置),逻辑上来说冲突只能由人来解决。Git做了合并,但没有提交,会停下来等待你来解决冲突,此时文件状态是**unmerged/未合并**。可以使用`git mergetool [file]`来编辑文件处理冲突,默认合并工具是`vimdiff`(暂时不会用)。 - -管理分支: -- `git branch` - - 不加选项:列出所有分支与当前分支。 - - `-d`:删除分支。删除还未合并的分支时会提示错误。`-D`则会忽略这个直接删除。 - - `--merged` 和 `--no-merged`筛选已经与**当前分支**合并的分支与未合并的分支。 - -### 3.3 如何利用分支管理开发的工作流程 - -使用Git可以管理各种工作流程: - -- 长期分支:在master分支只保留最稳定的代码,而在develop平行分支(也可以是多个分不同层次稳定性的分支)中管理后续的开发。等到经过充分测试确保代码稳定之后再逐步合并到master分支上来。 - -- 特性分支:在任何规模的项目中都可以使用,指一个短期的用来实现某个单一特性或与其相关工作的分支。比如修复一个紧急问题,添加一个新功能等。等到开发全部完成经过测试之后再将所有提交合并到主干/master上来。 - -当然目前所述都是本地分支,完全不涉及和服务器的交互。 - -### 3.4 远程分支 - -远程分支(remote branch)就是对远程仓库的分支的索引。他们是一些无法移动的本地分支,在Git进行网络交互时更新。用`remote-repo-name/branch-name`这种形式表示远程分支。 - -一个本地仓库可以有多个远程仓库,一个远程从仓库也可以有多个分支,所有当然可以有多个远程分支。当执行`git fetch`时远程分支就会被获取到本地。 - -- 如果在本地创建了分支,并且需要与其他人分享,那么就要推送分支到远程仓库。使用命令:`git push origin [local-branch-name]:[remote-branch-name]`。 -- 克隆仓库是会自动创建本地`master`分支用来跟踪`origin/master`,所以`git push`和`git pull`一开始就可以正常工作。 -- 跟踪远程分支: - - `git checkout --track origin/[remote-branch]` - - `git checkout -b [local-branch] origin/[remote-branch]`,本地分支名称不同于要跟踪的远程分支。 -- 删掉远程分支:`git push origin :[remote-branch]`,仅仅是把本地分支推送到远程时将本地分支的位置留空。 - - -### 3.5 分支的衍合 - -把一个分支的修改整合到另一个分支有两种方法,`merge`合并和`rebase`衍合(好像也称作**变基**)。 - -前面提到过,如果要合并的两个分支沿共同祖先产生了分叉,那么合并时就会产生一个新的提交对象。如果没有分叉,那么就只是单纯的分支指针移动。 - -![合并分叉的分支](Images/git_rebase_merge.png) - -还有一种选择就是`rebase`,效果就是把C3(要合并的分支`experiment`的所有提交对象)的变化在C4(当前`master`分支的最后一个提交对象)中打一个补丁(patch),生成一个新的合并提交对象C3'(每一个分支的提交对象打一次补丁在`master`生成一个对应提交对象),然后**改写**`expriment`的提交历史,使之成为`master`的直接下游。 - -![rebase](Images/git_rebase_rebase.png) - -- 命令:`git rebase master`。执行效果是将当前分支衍合到`master`分支的上去。此时再执行合并直接移动`master`指针即可完成。 - - -衍合操作得到了一个整洁的提交历史,看起来所有修改都是在一条线上顺序进行的,但同样将分支中的提交历史提到了`master`中。`rebase`之后`master`和其他分支的提交历史是完全一致的,分支也就没有了存在的必要。从这个角度来看,这个操作可以在要删除分支的前提下保留提交历史。 - -一种应用场景:本地分支开发后`rebase`到`origin/master`再提交,维护者就不需要做任何整合工作。 - -一看衍合这种操作就能理解这肯定是具有风险的,因为分支的历史被修改了。 - -- 衍合的风险:**一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。** - -在进行衍合的时候,实际上抛弃了一些现存的提交对象而创造了一些类似但不同的新的提交对象。如果你把原来分支中的提交对象发布出去,并且其他人更新下载后在其基础上开展工作,而稍后你又用 `git rebase` 抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。所以`rebase`原则上只应该在本地分支上做,做完之后即删掉本地分支。 - -如果把衍合当成一种在**推送之前清理提交历史的手段**,而且仅仅衍合那些**尚未公开的提交对象**,就没问题。如果衍合那些已经公开的提交对象,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。 - -总结: -- `rebase`操作把当前分支衍合到目标分支上。也就是说仅修改被衍合分支的提交对象。 -- `rebase`仅修改被衍合的分支指针和该分支的提交对象,不修改衍合到的分支的任何对象。 -- 衍合之后没有分叉,即可直接合并。 -- 衍合再合并和合并得到的结果是相同的,仅历史提交的拓扑关系不同。 -- 衍合再合并之后两个分支是完全相同的,即可把被衍合的分支删掉。 -- 衍合会修改提交对象,重算SHA-1校验和,和衍合前分支上的历史提交不是同一个提交对象。 -- 如果有冲突,衍合是在衍合时处理冲突,并且衍合后衍合到的分支同样会被移动,没有冲突则不会移动(是这样吗?本地测试是这样的),合并则是在合并时处理冲突。 -- 使用下来VSCode的Git支持做得是真的方便。图形化操作减少脑力负担,就算硬要敲命令内部也集成了终端,一点也不割裂。 - - -## 4. 服务器上的Git - -多人开发时为了方便,就需要在服务器上创建一个远程的仓库共享给所有人用以拉取和推送自己的工作内容。通常称这个共享的仓库为Git服务器。当然技术上是可以从个人仓库里拉取和推送修改内容,但是这样很容易和本地工作混淆,当然也可以在本地创建仓库作为远程仓库,然后克隆一次之后就不会了。 - -这一节主要内容是如何架设Git服务器。当然不想自己架设维护服务器的话,选择Github或者Gitee这种代码托管也是完全可以达到同样目的的。 - -远程仓库通常是一个裸仓库(bare repository),即没有工作目录的仓库,因为我们不需要在这个仓库上直接修改提交。简单来说,就是只包含`.git`子目录的内容。 - -### 4.1 协议 - -Git可以使用的四种传输协议:本地传输、SSH协议、Git协议、HTTP协议。 - -**本地协议**: 远程仓库就在硬盘的另一个目录,需要团队每一个成员都拥有对共享文件系统拥有访问权。 -使用方法: -```bash -# 克隆一个仓库下来 -git clone /git/project.git # 直接复制 -git clone file:///git/project.git # 网络传输 -# 对现有仓库添加远程仓库 -git remote add origin /git/project.git -``` -优点:方便简单,也方便同时获取其他人工作内容。 -缺点:难以控制不同位置的访问权限,通过NFS(网络文件系统)不一定是最快的。 - -**SSH协议**:唯一一个同时支持读写操作的网络协议(HTTP和Git都是只读的),同时也需要验证授权。 -使用: -```bash -git clone ssh://user@server/project.git -git clone user@server:project.git -``` -优点:如果要对网络仓库有写权限,ssh协议基本是必须的。架设简单,管理方便。访问安全,所有数据传输都是经过授权和加密的。高效,传输时会压缩数据。 -缺点:不能实现匿名访问,所以ssh不利于开源。如果是公司团队开发,ssh已经足够,如果需要开源,那么除了为自己架设ssh协议外,还需要提供匿名访问。 - -**Git协议**:这是一个包含在Git软件包里的特殊协议,它会监听一个提供类似于ssh服务的特定端口(9418),而且无需任何授权。如果你的仓库打算支持Git协议,需要先创建 `git-daemon-export-ok` 文件 — 它是协议进程提供仓库服务的必要条件 — 但除此之外该服务没有任何安全措施。要么所有人都可以克隆Git仓库,要么谁也不能。要开发推送也是可以的,不过没有授权,所以能访问的人都有同样的权限。 - -优点:现存最快的传输协议,它使用与 SSH 协议相同的数据传输机制,但省去了加密和授权的开销。 -缺点:缺少授权,所以不能作为访问Git仓库的唯一方法。一般的做法是,同时提供ssh接口,让开发者拥有推送权限。需要单独的守护进程与端口开放(大型企业级防火墙通常会封锁这个少见的端口)。 - -**HTTP/HTTPS协议**:架设便捷,基本上,只需要把 Git 的裸仓库文件放在 HTTP 的根目录下,配置一个特定的 post-update 挂钩(hook)就可以搞定。每个能访问 Git 仓库所在服务器上 web 服务的人都可以进行克隆操作。 - -搭建: -```bash -cd /var/www/htdocs/ -$ git clone --bare /path/to/git_project gitproject.git -$ cd gitproject.git -$ mv hooks/post-update.sample hooks/post-update -$ chmod a+x hooks/post-update -``` -然后克隆仓库: -```bash -git clone http://example.com/gitproject.git -``` -经过HTTP进行推送也是可以实现的,不过不常见太复杂略过。 - -优点:易于架设,HTTP占用服务器资源少,一般只用静态HTTP服务提供所有数据。HTTP如此常见,以至于企业级防火墙都允许通信。 -缺点:客户端效率低,HTTP协议传输体积和网络开销大。 - - -### 4.2 通过ssh协议架设Git服务器 - -架设之前肯定要导出现有仓库为裸仓库(使用`-bare`选项,不包含工作目录),目录名通常以`.git`结尾。 -```bash -git clone --bare my_project my_project.git -``` -其实clone操作相当于`git init` + `git fetch`。当然如果从头开始还没有本地仓库的话,直接`git init --bare`即可。整体的效果的话大致相当于`.git`目录的拷贝: -```bash -cp -Rf my_project/.git my_project.git -``` - -**将裸仓库移动到服务器上**: -假设已经有了一个git.example.com的服务器,当然没有解析域名只有IP(局域网或者公网IP)也是可以的,架设服务器的教程网络上汗牛充栋,不赘述;并可以通过ssh访问: -```bash -scp -r my_project.git user@git.example.com:/opt/git # scp是linux下跨机器复制文件的常用命令 -``` -简单来说传输到服务器器即可,不一定需要如此传输。现在有了ssh访问权限之后,即可以克隆远程仓库到本地: -```bash -git clone user@git.example.com:/opt/git/my_project.git -``` -如果ssh用户对目录有写权限,那么他现在就可以推送本地的修改了。如果仅有几个人在一个不公开的项目上工作,那么一个ssh服务器与裸仓库便足够了。 - -**权限控制**:如果想一部分人只读,一部分人可写,那么使用服务器操作系统的本地文件访问控制机制即可(关于这点,还不是很清楚,Linux也没有那么深入,需要深入了解ssh协议和命令,TODO。) - -建立访问权方法:其一,每个人建立一个账户,设定密码。其二,主机上建立一个git账户,每个写权限的人发送一个SSH公钥,然后将其加入git账户的`~/.ssh/authorized_keys`文件。这并不会影响提交的数据:访问主机用的身份(git账户)不会影响提交对象的提交者信息(`git config user.name/user.email`)。另外,使用其他的SSH授权机制也可以达到相同效果。 - - -### 4.3 生成SSH公钥 - -大多数 Git 服务器都会选择使用 SSH 公钥来进行授权。系统中的每个用户都必须提供一个公钥用于授权,没有的话就要生成一个。生成公钥的过程在所有操作系统上都差不多。 首先先确认一下是否已经有一个公钥了。SSH 公钥默认储存在账户的主目录下的`~/.ssh`目录下的`id_rsa.pub`或者`id_dsa.pub`文件。有`.pub`后缀的是公钥,另一个同名文件是密钥/私钥。 - -如果还没有或者甚至还没有`.ssh`目录,那么可以使用`ssh-keygen`命令生成,生成过程中会提示输入密码,如果输入了,则使用这个ssh key推送到远程仓库是需要输入密码,也就是生成过程中指定的那个。否则的话不需要,用户名则是不需要输入。 - -注册了Github之后,将这个SSH key配置到Github上(Setting -> SSH and GPG keys)之后就可以本地推送到github了。 - -### 4.4 Git服务器架设流程 - -假设你已经有了一台服务器,还没有的话那就去[腾讯云](https://cloud.tencent.com/)或者[阿里云](https://cn.aliyun.com/)百元一年左右的价格就可以购买一台入门级的VPS,这时候会得到一个公网IP。一般选择Linux操作系统,如Ubuntu/CentOS等,和本地虚拟机、实体机上的装的Linux系统、WSL等操作起来并没有区别。使用[XShell](https://www.netsarang.com/en/xshell/),[Putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/)等远程连接工具即可连接使用。当然在做下面所有事情之前你需要确保已经安装`git`与`openssh-server`。 - -1. 首先创建名为`git`的用户,也就是ssh路径里`@`符号前的那个用户名。创建`.ssh`目录。 - ```bash - sudo adduser git - su git - cd ~ - mkdir .ssh - ``` -2. 把要授权的开发者的公钥添加到`git`用户的`~/.ssh/authorized_keys`文件。公钥看起来是这样的(就是本地用`ssh-keygen`生成的`id_rsa.pub`文件内容): - ``` - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L - ojG6rs6hPB09j9R/T17/x4lhJA0F3FR1rP6kYBRsWj2aThGw6HXLm9/5zytK6Ztg3RPKK+4k - Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez - Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv - O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq - dAv8JggJICUvax2T9va5 gsg-keypair - ``` - 逐个添加到文件尾部即可。 -3. 创建裸仓库之后本地克隆下来或者添加远程仓库就可以推送修改了。远程: - ```bash - cd /opt/git # 当然可以在任何有权限可以读写的目录创建 - mkdir project.git - cd project.git - git --bare init - ``` - 本地:(用`gitserver`替代主机,实际使用时替换为你的服务器IP) - ```bash - git clone git@gitserver:/opt/git/project.git - cd myproject - touch Readme.md - git commit -am 'first commit : add Readme.md' - git push origin master - ``` - 再登录到服务器上`git log`即可看到提交记录了。 -4. 至此就搭建完毕了,非常快捷与简单。然后再做一些权限配置,使用`git`自带的`git-shell`工具限制`git`用户的活动范围。只要把它设置为`git`用户的登录shell,那么该用户就无法使用普通的bash或者其他shell程序。编辑`/etc/passwd`文件: - ```bash - sudo vim /etc/passwd - ``` - 找到文件末尾类似`git:x:1000:1000::/home/git:/bin/sh`这样的行,将其中的`bin/sh`改为`usr/bin/git-shell`。 - 现在`git`用户只能通过SSH连接来推送和获取Git仓库,而不能直接使用主机Shell。如果`ssh git@gitserver`直接进行普通SSH登录则会报错。这样的话就只能通过其他用户远程连接,连接后也不能切换到`git`用户来创建仓库。 - -### 4.5 公共访问 - -配置好了ssh协议访问,如果是开源项目,可能还需要你匿名的读取访问。或许对小型的配置来说最简单的方法是运行一个静态Web服务(任何Web服务都可以达到同样的效果:Apache, Nginx, etc.)。将根目录设定为Git仓库所在位置,使用前面提到的`post-update`钩子。 - -1. 首先开启钩子,就是重命名`post-update`的样本文件。 - ```bash - cd project.git/hooks - mv post-update.sample post-update - chmod a+x ./post-update - ``` - 该文件核心内容如下: - ``` - exec git-update-server-info - ``` - 意思是当通过 SSH 向服务器推送时,Git 将运行这个 `git-update-server-info `命令来更新匿名 HTTP 访问获取数据时所需要的文件。 - -2. 把文档根目录设为 Git 项目所在的根目录。然后假定DNS服务已经配置好后,会把`.gitserver`请求发送到这台主机。另外需要将仓库所在目录的Unix用户组设置为`www-data`,这样web服务才可以读取仓库。 - ```bash - chgrp -R www-data /opt/git - ``` - 接下来就可以在本地匿名使用HTTP协议访问仓库了: - ```bash - git clone http://git.gitserver/project.git # http://gitserver/git/project.git 这样可以吗? - ``` - 当然这需要配置服务器设置。这里省略了,根据具体使用的Web服务器配置方法不同。 - -另一个提供非授权访问的简单方法是开启一个 Git 守护进程,也就是使用Git协议来匿名访问。 - -### 4.6 GitWeb - -现在已经可以读写或者匿名只读访问了,如果能有一个简单的Web界面访问的话就更好了。 - -Git自带了一个命令可以做到,使用类似`lighttpd`或者`webrick`这样轻量级的服务器启动一个临时进程,,可以到项目目录中键入`git instaweb`来启动。 - -如果要用 `lighttpd` 以外的程序来启动 `git instaweb`,可以通过 `--httpd` 选项指定: -```bash -git instaweb -httpd=webrick # 这会在1234端口开一个HTTPD服务 -git instaweb -httpd=webrick --stop # 关闭服务时添加--stop选项即可 -``` - -- 这里暂时还没有跑起来:还缺一些前置知识,待后续不补充。 -- TODO:了解Web服务器相关内容。什么是CGI?Web服务器的配置和使用,Apache和Nginx? - -### 4.7 Gitosis - -把用户公钥保存在`authorized_keys`中,对团队人数不多时,是比较有效的,当团队达到一定规模,管理起来可能就会非常痛苦。并且这种情况所有用户都会拥有对Git仓库的读写权限,无法更为有效精细的管理。 - -这时候就需要使用Gitosis了,有趣的是权限的修改并非通过网页设置,而是通过管理一个特殊的Git仓库来实现。 - -安装Gitosis: -```bash -# 依赖 -apt install python-setuptools -# 从 Gitosis 项目主页克隆并安装 -git clone https://github.com/tv42/gitosis.git -cd gitosis -sudo python setup.py install - -# 待补全,暂时没有需求,属于具体应用,需要修改现有的authorized_keys -``` -TODO:后续的Gitolite和Git守护进程(使用Git协议提供匿名访问)暂不涉及。 - - -### 4.8 Git托管服务 - -[托管服务列表](https://git.wiki.kernel.org/index.php/GitHosting) - -当然最广闻人知的必然是世界上最大的程序员交友网站[Github](https://github.com)。 - -当然使用就不必多说了,添加SSH Key,创建仓库,克隆或者添加远程仓库,提交。和Git服务器并无二致,也不需要操心去配置Git服务器。 - - -## 5. 分布式Git - -前面已经有了基本的本地工作流程。但是作为项目贡献者时,怎样做才能方便维护者采纳更新,作为项目维护者,又怎么样有效管理大量贡献者的提交呢? - -### 5.1 分布式工作流程 - -Git与传统的集中式版本控制系统(CVCS)不同,因为是分布式的,所以开发者可以互相分享自己的代码,流程多样灵活。 -集中常见的工作流程: - -**集中式工作流**:单点协作模型,一个存放代码仓库的中心服务器,可以接受所有开发者提交代码。所有开发者都是普通节点,作为仓库的消费者,平时工作就是和中心仓库同步数据。 - -![集中式工作流](Images/git_workflow_centralized.png) - -那么当两个人同时修改了同一个地方,就有可能产生冲突,第二个提交的人就需要在推送之前先拉取数据解决冲突。团队不大的话完全可以采用这种工作流程,使用也非常广泛。 - -**集成管理员工作流**:由于Git允许使用多个远程仓库,开发者可以建立自己的公共仓库,将数据共享给其他人。这种情况通常有一个代表官方发布的项目仓库(blessed repository),开发者由此仓库克隆自己的公共仓库(developer public),推送自己的提交。维护者有自己本地的克隆仓库,将所有公共仓库作为远程仓库添加进来,测试无误后合并到主干,再推送到官方仓库。 - -![集成管理员工作流](Images/git_workflow_Integrated_administrator.png) - -GitHub网站上多采用这种工作流,要贡献代码时先fork项目到自己的列表,成为自己的公共仓库,然后更新提交之后,然后提起Pull Request,等待维护者接受你的贡献。这种工作方式下你可以按照你自己的节奏工作,不必等待维护者处理你的更新,维护者也可以按照自己的节奏,任何时候都可以来处理别人贡献的代码。 - -**司令官与副官工作流**:一般超大型项目才会采用,比如Linux内核。各个集成管理员分别负责集中项目中的特定部分,称为副官(lieutenant)。这些集成管理员上方还有负责统筹的总集成管理员,成为司令官(dictator),维护仓库以提供所有协作者拉取最新集成的项目代码。 - -![司令官与副官工作流](Images/git_workflow_dictator_and_lieutenaut.png) - -这种分而治之的项目管理职责也清晰,不易出错,项目极为庞杂或者需要多级别管理时才会体现出优势。 - -除了这些工作流,实际工作中为了满足各式各样的需求,会需要各种不同的工作方式。 - - -### 5.2 为项目做贡献 - -那作为项目贡献者,又有那些常见的工作模式呢? - -Git如此灵活,人们的协作方式也可以各种各样,没有固定不变的范式可循。具体到个人,项目所选的工作流程,参与者规模,提交权限等都会影响具体操作的细节。 - -比如说影响较大的团队规模,如果几百个人工作的仓库,每天可能有几十上百个提交,那么你的提交可能会由于其他人的更新而变得过时甚至无法运行偏离原有逻辑。那么如何确保代码最新,且每次提交后逻辑正确无误呢?几个人的小团队就不需要为这种问题烦恼。 - -**提交指南**:代码提交需要规范以方便管理,项目管理时应该需要制定提交规范,不一而足。列举一些常见的点: -- 不要提交多余的空白字符。 -- 每次提交限定于完成一次逻辑功能。可以的话,适当分解为多次小提交,以便每次提交都易于理解。这样做便于其他人复阅代码,取消某个特定问题的修复等。 -- 提交说明撰写,要清晰简洁易理解易跟踪搜索,项目规范不一而足。 - -可以看一看Git本身的项目提交:`git log --no-merges`。 -是时候把Git克隆下来了:`git clone git://git.kernel.org/pub/scm/git/git.git`,仓库共140MB左右,没有我想象中那么大。TODO:后续有时间回滚并读一下源码。 - -**私有小型团队**:仅在一台服务器上提交的话其实与使用SVN等CVCS工作流并无太大差别。但还是有一些区别,比如如果你当前版本不是最新,那么需要先`git fetch origin`之后合并到本地(`git merge origin/master`)之后再提交,而SVN提交后会自动在服务器进行合并。有冲突的的话需要先解决再提交。最后的本地的仓库历史类似于: - -![仓库](Images/git_work_with_git_rebase.png) - -当然也可以`rebase`,那就成一条直线了。 - -最简单的协作方式之一:在自己的特性分支中工作一段时间,完成后合并到自己的`master`分支,或者直接在`master`中修改提交,拉取并合并`origin/master`的更新,再推向远程服务。最后的提交一般都是分叉的而不是线性的(不同于SVN上在同一个分支上提交时完全线性的提交历史)。 - -**私有团队间协作**:几个小组分别负责若干特性开发和集成时,每个特性签出一个分支,项目组成员在分支上工作,并把工作推送到服务器,然后项目组管理员负责在项目完成后集成到`master`分支并提交到远程。看起来是很清晰的,只是如果同时在多个项目间工作时,因为都在一个本地,千万要记住自己当前工作的分支,不要提交错了。 - -推送本地分支提交到追踪的远程分支:`git push origin [local-branch]:[remote-branch]`。 - - -**公开项目**:如果不是项目的直接维护者,只是想贡献一些代码,没有直接推送到主仓库的权限,那么就需要把工作成果提交给项目维护人。当然在Github中使用 fork + PR 的方式解决这个问题。另外也可以通过电子邮件发送补丁完成提交。 - -具体流程: -- 克隆项目仓库。 -- fork这个仓库,然后将fork之后的仓库同时添加为本地仓库的远端仓库。 -- 创建特性分支,修改提交,推送到自己fork的仓库`git push myfork featureA`。这样如果项目维护者未采纳贡献,也不需要回退自己的`master`,因为你的提交还在远程分支上。 -- 通知项目管理员,让他来抓取你的代码,也就是 Pull Request。 -- 如果管理员采纳或者选择了你的提交,那么就会被合并到项目主干上。 -- 这是如果要开始第二项特性开发,就先保持自己`master`与`origin/master`同步,签出新分支继续开始工作。 - -这样的做法方便又灵活,采纳和丢弃都轻而易举。多个特性的开发修改互不干扰,不用担心特性代码交叉混杂。 - -如果开发期间管理员接受了其他人的提交,导致合并无法正确干净地完成,那么你需要将特性分支衍合到最新的`origin/master`,解决冲突后,重新提交发起PR。或者在PR前都选择先衍合到最新的`origin/master`? - -也可以将`git request-pull origin/master myfork`的结果发送给管理员以通知采纳。 - -**公开的大型项目**:许多大型项目都会立有一套自己的接受补丁流程,你应该注意下其中细节。但多数项目都允许通过开发者邮件列表接受补丁。 - -工作流程类似于:为每个补丁创建独立的特性分支,而不同之处在于如何提交这些补丁。不需要创建自己可写的公共仓库,也不用将自己的更新推送到自己的服务器,你只需将每次提交的差异内容以电子邮件的方式依次发送到邮件列表中即可。 - -- 使用`git format-patch`生成mbox格式的文件作为附件发送给管理员。 -- 甚至可以通过配置直接使用`git send-email`来发送补丁。这些内容具体现在并无太大作用,具体贡献时按照项目约定的流程来即可。 - - -### 5.3 管理自己的项目 - -不了解也没有做过,不展开说,总结要点: -- 使用`git apply`或者`git am`采纳别人的补丁。 -- 合并其他人提交时,将其他人仓库添加为远程仓库,拉取提交为特性分支,决定代码取舍,集成到主干,删除特性分支。 -- 大型项目还需要`master`稳定分支确保发布代码的稳定与`develop`开发分支来接受其他人代码。 -- 签名、生成版本号、发布、制作简报等内容以后有机会再详细了解不详述。 - - -## TODO - -待长期使用总结,有用到之后再补充。 - -- Git服务器相关内容补充:GitWeb,HTTP访问,Git协议等 -- 更多Git托管服务内容补充 -- Git工具 -- 自定义Git -- Git与其他VCS的交互 -- 底层原理 -- GUI:git-gui,gitk,etc -- 主流IDE的Git集成 -- 操作细节 - - diff - - merge - - etc -- 使用体验,待长期使用后总结 -- etc diff --git a/Haskell.md b/Haskell.md deleted file mode 100644 index 897f6b8..0000000 --- a/Haskell.md +++ /dev/null @@ -1,4845 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Haskell语言入门](#haskell%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8) - - [关于Haskell](#%E5%85%B3%E4%BA%8Ehaskell) - - [Haskell与函数式编程](#haskell%E4%B8%8E%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B) - - [环境搭建](#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA) - - [使用GHCup安装](#%E4%BD%BF%E7%94%A8ghcup%E5%AE%89%E8%A3%85) - - [安装stack](#%E5%AE%89%E8%A3%85stack) - - [使用stack安装GHC](#%E4%BD%BF%E7%94%A8stack%E5%AE%89%E8%A3%85ghc) - - [关于Cabal和stack](#%E5%85%B3%E4%BA%8Ecabal%E5%92%8Cstack) - - [GHC基本使用](#ghc%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8) - - [Stack使用指南](#stack%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97) - - [关于Stack](#%E5%85%B3%E4%BA%8Estack) - - [开始使用](#%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8) - - [基本命令](#%E5%9F%BA%E6%9C%AC%E5%91%BD%E4%BB%A4) - - [项目配置](#%E9%A1%B9%E7%9B%AE%E9%85%8D%E7%BD%AE) - - [运行现有的项目](#%E8%BF%90%E8%A1%8C%E7%8E%B0%E6%9C%89%E7%9A%84%E9%A1%B9%E7%9B%AE) - - [编译选项](#%E7%BC%96%E8%AF%91%E9%80%89%E9%A1%B9) - - [路径](#%E8%B7%AF%E5%BE%84) - - [exec](#exec) - - [ghci](#ghci) - - [脚本](#%E8%84%9A%E6%9C%AC) - - [编辑器集成](#%E7%BC%96%E8%BE%91%E5%99%A8%E9%9B%86%E6%88%90) - - [VsCode环境配置](#vscode%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [感受一下Haskell](#%E6%84%9F%E5%8F%97%E4%B8%80%E4%B8%8Bhaskell) - - [基本要素](#%E5%9F%BA%E6%9C%AC%E8%A6%81%E7%B4%A0) - - [基本内容](#%E5%9F%BA%E6%9C%AC%E5%86%85%E5%AE%B9) - - [运算符](#%E8%BF%90%E7%AE%97%E7%AC%A6) - - [基本类型类](#%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E7%B1%BB) - - [函数](#%E5%87%BD%E6%95%B0) - - [定义函数](#%E5%AE%9A%E4%B9%89%E5%87%BD%E6%95%B0) - - [使用List](#%E4%BD%BF%E7%94%A8list) - - [使用Range](#%E4%BD%BF%E7%94%A8range) - - [List Comprehension](#list-comprehension) - - [元组](#%E5%85%83%E7%BB%84) - - [Type & Typeclass](#type--typeclass) - - [类型](#%E7%B1%BB%E5%9E%8B) - - [类型变量](#%E7%B1%BB%E5%9E%8B%E5%8F%98%E9%87%8F) - - [Typeclass](#typeclass) - - [函数相关语法](#%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3%E8%AF%AD%E6%B3%95) - - [模式匹配(Pattern matching)](#%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8Dpattern-matching) - - [守卫](#%E5%AE%88%E5%8D%AB) - - [where关键字](#where%E5%85%B3%E9%94%AE%E5%AD%97) - - [let关键字](#let%E5%85%B3%E9%94%AE%E5%AD%97) - - [case表达式](#case%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [递归](#%E9%80%92%E5%BD%92) - - [高阶函数](#%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0) - - [柯里化](#%E6%9F%AF%E9%87%8C%E5%8C%96) - - [函数作为参数](#%E5%87%BD%E6%95%B0%E4%BD%9C%E4%B8%BA%E5%8F%82%E6%95%B0) - - [常用高阶函数](#%E5%B8%B8%E7%94%A8%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0) - - [lambda](#lambda) - - [fold & scan](#fold--scan) - - [$函数调用符](#%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E7%AC%A6) - - [函数复合(Function Composition)](#%E5%87%BD%E6%95%B0%E5%A4%8D%E5%90%88function-composition) - - [模块](#%E6%A8%A1%E5%9D%97) - - [引入模块](#%E5%BC%95%E5%85%A5%E6%A8%A1%E5%9D%97) - - [常用库](#%E5%B8%B8%E7%94%A8%E5%BA%93) - - [编写自己的模块](#%E7%BC%96%E5%86%99%E8%87%AA%E5%B7%B1%E7%9A%84%E6%A8%A1%E5%9D%97) - - [定义类型和类型类](#%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%9E%8B%E5%92%8C%E7%B1%BB%E5%9E%8B%E7%B1%BB) - - [定义新类型](#%E5%AE%9A%E4%B9%89%E6%96%B0%E7%B1%BB%E5%9E%8B) - - [Record Syntax](#record-syntax) - - [类型参数](#%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0) - - [派生标准类型类](#%E6%B4%BE%E7%94%9F%E6%A0%87%E5%87%86%E7%B1%BB%E5%9E%8B%E7%B1%BB) - - [类型别名](#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D) - - [递归定义数据结构](#%E9%80%92%E5%BD%92%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) - - [自定义类型类](#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%9E%8B%E7%B1%BB) - - [Functor/函子](#functor%E5%87%BD%E5%AD%90) - - [Kind](#kind) - - [输入与输出](#%E8%BE%93%E5%85%A5%E4%B8%8E%E8%BE%93%E5%87%BA) - - [IO动作](#io%E5%8A%A8%E4%BD%9C) - - [输入与输出函数](#%E8%BE%93%E5%85%A5%E4%B8%8E%E8%BE%93%E5%87%BA%E5%87%BD%E6%95%B0) - - [文件与字符流](#%E6%96%87%E4%BB%B6%E4%B8%8E%E5%AD%97%E7%AC%A6%E6%B5%81) - - [命令行参数](#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0) - - [随机数](#%E9%9A%8F%E6%9C%BA%E6%95%B0) - - [ByteString](#bytestring) - - [异常(Exceptions)](#%E5%BC%82%E5%B8%B8exceptions) - - [问题解决实例](#%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E5%AE%9E%E4%BE%8B) - - [逆波兰表达式](#%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [路径规划问题](#%E8%B7%AF%E5%BE%84%E8%A7%84%E5%88%92%E9%97%AE%E9%A2%98) - - [函子、应用函子与幺半群](#%E5%87%BD%E5%AD%90%E5%BA%94%E7%94%A8%E5%87%BD%E5%AD%90%E4%B8%8E%E5%B9%BA%E5%8D%8A%E7%BE%A4) - - [函子](#%E5%87%BD%E5%AD%90) - - [应用函子](#%E5%BA%94%E7%94%A8%E5%87%BD%E5%AD%90) - - [newtype](#newtype) - - [Monoid](#monoid) - - [Monad](#monad) - - [Monad类型类](#monad%E7%B1%BB%E5%9E%8B%E7%B1%BB) - - [Monad应用](#monad%E5%BA%94%E7%94%A8) - - [do表示法](#do%E8%A1%A8%E7%A4%BA%E6%B3%95) - - [Monad实例](#monad%E5%AE%9E%E4%BE%8B) - - [Monad Law](#monad-law) - - [More Monad](#more-monad) - - [Writer](#writer) - - [Reader Monad](#reader-monad) - - [State Monad](#state-monad) - - [常用的操作Monad的函数](#%E5%B8%B8%E7%94%A8%E7%9A%84%E6%93%8D%E4%BD%9Cmonad%E7%9A%84%E5%87%BD%E6%95%B0) - - [Zippers](#zippers) - - [定义一个树](#%E5%AE%9A%E4%B9%89%E4%B8%80%E4%B8%AA%E6%A0%91) - - [Zipper](#zipper) - - [Zipper of List](#zipper-of-list) - - [总结](#%E6%80%BB%E7%BB%93) - - - -# Haskell语言入门 - -提示:本文含有少量公式,可安装[MathJax Plugin for Github](https://github.com/orsharir/github-mathjax)浏览器插件提供公式渲染,或者Clone到本地查看。 - -## 关于Haskell - -关于Haskell: ->[Haskell](https://zh.wikipedia.org/wiki/Haskell)(发音为/ˈhæskəl/)是一种标准化的,通用的纯函数式编程语言,有惰性求值和强静态类型。它的命名源自美国逻辑学家哈斯凯尔·加里,他在数理逻辑方面上的工作使得函数式编程语言有了广泛的基础。在Haskell中,“函数是第一类对象”。作为一门函数编程语言,主要控制结构是函数。Haskell语言是1990年在编程语言Miranda的基础上标准化的,并且以λ演算为基础发展而来。这也是为什么Haskell语言以希腊字母“λ”(Lambda)作为自己的标志。Haskell具有“证明即程序、命题为类型”的特征。 - -首先需要明确的是: -- Haskell是一门纯函数式编程语言,学习曲线非常陡峭,收获同样会很丰富。 -- 不要在没有任何编程基础的情况下学习Haskell,至少先学习一些过程式和支持函数式编程的语言,有一定数据结构和算法基础。 -- Haskell非常注重理论,范畴论是支持函数式编程的理论基础之一,作为一门数学分支理论,学习Haskell将会深入范畴论的内容,也就是说不可避免地需要了解很多数学概念和定理,将会时刻与抽象作伴。 -- 不要期待几天几个月就学懂并深入Haskell,这将会是一条艰涩的道路。 -- 不要期待通过一本书或者一门课程就学到Haskell的全部,从不同的教程和书籍不同的视角思考是必要的。 -- 相教传统的命令式编程而言,需要换一种方式来思考,否则永远学不好Haskell。 -- 入门至少要了解一定的范畴论概念,理解函子、应用函子、单子等概念,理解清楚“**单子是自函子范畴上的幺半群**”这句话。 -- 学习不害怕没有基础,没有老师,怕的是没有热情。 - -学习一门新的语言,收集资料是必不可少的: -- [怎么学习Haskell-github.com/bitemyapp/learnhaskell](https://github.com/bitemyapp/learnhaskell/blob/master/guide-zh_CN.md),入门现代Haskell的最好材料。 -- 书籍:现有的书籍屈指可数。 - - [Learn You a Haskell for Great Good](http://learnyouahaskell.com/),中译[Haskell趣学指南](https://book.douban.com/subject/25803388/),入门书,据评价前面很轻松,但后面会难度陡增让人不知所云,而且不够现代,覆盖的东西不足以写出实用的代码。[在线阅读](http://learnyouahaskell.com/chapters)。 - - [HaskellBook](https://haskellbook.com/),第一个链接learnhaskell仓库作者写的书籍,英文版书籍很贵,但很靠谱,尚无中译版本出版,所以说暂无法阅读到。 - - [Real World Haskell](https://book.douban.com/subject/3134515/),中译在译中,尚未出版。成书有点早,有些库可能已经用不了了,但绝对还可以读,初学有难度。[中译的仓库](https://github.com/huangz1990/real-world-haskell-cn),[在线阅读](https://rwh.readthedocs.io/en/latest/)。 - - [Haskell并行与并发编程](https://book.douban.com/subject/26256849/),如其名,进阶书籍,内容全面,翻译有些瑕疵但都有勘误。 - - 更多。 -- 一些课程: - - [诺丁汉大学——函数式编程范式入门课程](https://www.bilibili.com/video/BV1ti4y1P7TF?p=1) - - 待补充。 - -资料选择: -- [Learn You a Haskell for Great Good](https://www.bookstack.cn/read/learnyouahaskell-zh-cn/README.md) 目前在看,本文理论部分的最主要参考,台湾人翻译的某些名词会有一些差异,比如实现称为实作、类型称为型别、参数称为引数、随机数称为乱数等,需要留意,不是很影响阅读。 -- [Real World Haskell](http://cnhaskell.com/index.html) -- [Haskell 2010 Report](https://www.haskell.org/definition/haskell2010.pdf) 没有什么比标准更准确,进阶的话必须要看,还没有到这一步。 - -语言相关链接: -- [haskell主页](https://www.haskell.org/),[Wiki](https://wiki.haskell.org/Haskell) -- [GHC文档](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/index.html) -- [stack文档](https://docs.haskellstack.org/en/stable/README/) -- [Cabal文档](https://cabal.readthedocs.io/en/3.6/) -- [Stackage首页](https://www.stackage.org/) -- [Hackage首页](https://hackage.haskell.org/) -- [Hoogle API Search](https://hoogle.haskell.org/) - - -课外阅读: -- [一个很全面的Haskell笔记](https://blog.tonycrane.cc/p/b3ca5c18.html) -- [School of Haskell](https://www.schoolofhaskell.com/),一个学习Haskell的网站。 - -## Haskell与函数式编程 - -函数式(functional languages)与命令式(imperative languages): -- 不同于命令式编程语言,程序是描述要怎么做,要做什么,函数式编程需要通过函数描述出问题**是什么**,比如「阶乘就是只从1到某个数的乘积」,而命令式编程语言则会用程序描述出阶乘的计算过程:定义结果的初值为1,然后从1一直累乘到某个数的计算过程。 -- 在函数式编程语言中,变量一旦指定就不可以更改了,在命令式编程语言中,变量表示状态,如果状态不可变,那么能做的事情将非常有限。而函数式编程语言中,变量的含义更接近数学中的变量,`x=5`表示`x`就是`5`,而不是`x`处于`5`这个状态。 -- 所以在纯粹的函数式编程语言中,函数唯一能做的事情就是利用参数计算结果,不会产生副作用(side effect),副作用的含义是改变非函数内部的状态,这在命令式编程中是非常常见的。在函数式编程语言中,若以同样的参数调用一个函数两次,结果必定相同,也就是说函数都是**可重入**的。在命令式编程语言中,则需要函数实现时进行非常严格的限定才能做到。没有副作用的函数实现对于并发非常有用,因为没有副作用,并行执行的正确性就能够得到保证。 - -Haskell的特点: -- Haskell是惰性的,如非特殊说明,函数真正需要结果以前不会被求值,加上引用透明,可以把程序看做数据的一系列变形。也就是说惰性语言中的计算只是一组初始数据和变换公式。 -- Haskell是静态强类型的,拥有一套强大的类型系统,支持自动类型推导(type inference),比如`a = 5+4`编译器能自动推导出`a`是整数。 -- Haskell 采纳了很多高端编程语言的概念,因而它的代码优雅且简练。与同层次的命令式语言相比,Haskell 的代码往往会更短,更短就意味着更容易理解,bug 也就更少。 - -Haskell语言发展: -- 始于1987年。 -- 1997年底,该系列形成了Haskell 98,旨在定义一个稳定、最小化、可移植的语言版本以及相应的标准库,以用于教学和作为将来扩展的基础。委员会明确欢迎创建各种增加或集成实验性特性的Haskell 98的扩展和变种。 -- 1999年2月,Haskell 98语言标准公布,名为《The Haskell 98 Report》。2003年1月,《Haskell 98 Language and Libraries: The Revised Report》公布。接着,Glasgow Haskell Compiler (GHC)实现了当时的事实标准,Haskell快速发展。 -- Haskell 2010加入了外部函数接口(Foreign Function Interface,FFI)允许绑定到其它编程语言,修正了一些语法问题(在正式语法中的改动)并废除了称为“n加k模式”(换言之,不再允许形如fact (n+1) = (n+1) * fact n的定义)。引入了语言级编译选项语法扩展(Language-Pragma-Syntax-Extension),使得在Haskell源代码中可以明确要求一些扩展功能。 - -## 环境搭建 - -使用目前最流行的haskell编译器GHC(The Glasgow Haskell Compiler),是当前最先进的开源的Haskell编译器和交互式执行环境。 -- 支持整个Haskell 2010标准和一大堆扩展。 -- 并行和并发的良好支持,包括STM。 -- 跨平台,Windows、Mac、Linux、大部分Unix等平台。 -- GHC编译器可以直接编译为本地代码或者使用LLVM作为后端,可以生成C代码作为中间目标以支持扩展到新的平台。在交互式环境中将Haskell编译到字节码,并支持字节码和编译程序的混合执行。 -- 支持Profiling(性能分析),时间、内存、多种堆分析。 -- GHC包含多个库,更多库还可以到Hackage([The Haskell Package Repository](https://hackage.haskell.org/))上寻找。 - -安装: -- 当前时刻最新版本2021年10月29日发布的是9.2.1,[GHC首页](https://www.haskell.org/ghc/)。 -- 一般不直接安装GHC,而是通过cabal或者Stack安装GHC,这样可以管理项目并且管理第三方库从Hackage或者Stackage的安装。只安装GHC的话不是很方便。 - -### 使用GHCup安装 - -- 使用[GHCup](https://www.haskell.org/ghcup/install/),GHCup是一个帮助安装Haskell GHC工具链的工具。支持安装的工具链: - - GHC - - cabal-install - - haskell-language-server - - stack -- GHCup提供了详细的安装文档: -- Linux下运行: -```shell -curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh -``` -- Windows下PowerShell中运行: -```powershell -Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -ArgumentList $true -``` -- 因为网络原因,Linux上和Windows上都没有安装成功。 -- 好像可以换源解决,参考[这里](https://zh.z.haskell.world/guide01),尝试结果是换源之后仅GHCup是从镜像下载,stack和GHC还是从Github上下载,啊这。 -- GHCup只是一个安装工具,不是一定需要,转为直接安装stack。 - -### 安装stack - -特性与文档: -- [一个视频教程](https://www.youtube.com/watch?v=sRonIB8ZStw)。 -- [stack](https://github.com/commercialhaskell/stack)是一个跨平台的Haskell项目管理、环境管理工具。 -- [stack文档](https://docs.haskellstack.org/en/stable/README/)。 -- 特性: - - 安装GHC到一个独立位置。 - - 为项目安装需要的包。 - - 构建、测试项目。 - - 为项目做Benchmark。 - -Windows环境: -- Windows中:下载[Windows 64-bit Installer.](https://get.haskellstack.org/stable/windows-x86_64-installer.exe)。默认会添加用户path环境变量,并设置了用户环境变量`STACK_ROOT=C:\sr`表示使用stack安装程序的位置。由于Windows下默认由260字节的路径长度限制,且stack管理的文件通常具有较深的目录层次,所以这里的目录名很短,也可以设置为其他盘或者其他路径。 -- 升级stack: -```shell -stack upgrade -``` - -换源: -- [更换stack的源为清华源](https://mirrors.tuna.tsinghua.edu.cn/help/stackage/),注意其中给出的配置文件目录为`%APPDATA%\stack\config.yaml`,但由于我们修改了`%STACK_ROOT%`,需要修改的配置文件其实在`%STACK_ROOT%\config.yaml`。 -- 清华的源存在一定问题,有一些东西没有镜像过来,配置也有问题,安装GHC时会有问题。提了[Issue](https://github.com/tuna/issues/issues/1379),不过一直没解决。 -- 建议[更换为中科大的源](https://mirrors.ustc.edu.cn/help/stackage.html),则没有这个问题。 -- 注意配置文件位置 - - windows中在`%STACK_ROOT%\config.yaml`。 - - Linux中在`~/.stack/config.yaml`。 - -Linux环境: -- 途径一:官网安装方法,网络原因失败。 -```shell -curl -sSL https://get.haskellstack.org/ | sh -wget -qO- https://get.haskellstack.org/ | sh -``` -- 途径二:包管理器,版本有点低。不过可以安装,可以安装之后再换源然后更新,注意不同版本的`config.yaml`文件配置方式存在差异,参见镜像的配置指导。尝试过,但低版本的Stack更新还是从Github,这。 -```shell -sudo apt install haskell-stack -``` -- 途径三:由于网络原因是在难搞,可以使用代理,或者直接从下载压缩包解压,从中科大源找到最新版本stack的压缩包。 -```shell -wget https://mirrors.ustc.edu.cn/stackage/stack/stack-2.7.3-linux-x86_64-static.tar.gz -``` -- 解压后将单个二进制拷贝到`/usr/bin/stack`,创建`~/.stack/`就OK了,`stack`安装之后就只有这两个东西,卸载的话直接把二进制和`.stack/`目录删掉就足够了。 -```shell -tar -zxvf stack-2.7.3-linux-x86_64-static.tar.gz -cd stack-2.7.3-linux-x86_64-static/ -sudo cp stack /usr/bin/stack -``` - -### 使用stack安装GHC - -- 到[Stackage](https://www.stackage.org/)找到最新的LTS版本,当前是18.17,然后安装GHC,[文档](https://docs.haskellstack.org/en/stable/GUIDE/#resolvers-and-changing-your-compiler-version)。 -- 安装指定LTS版本的GHC,直接`--resolver lts`则安装最新的LTS版本。 -```shell -stack --resolver lts-18.17 setup -``` -- 也可以安装最新的Nigthly版本,好像是每天发布,一般来说没有必要安装最新版本,安装最新的长期支持版是最好的。 -```shell -stack --resolver nigthly setup -``` -- 清华源存在问题,解决途径可以参考[这里](https://krantz-xrf.github.io/2020/09/25/windows-install-stack-ghc.html),使用[中科大的源](https://mirrors.ustc.edu.cn/help/stackage.html)则没有这个问题。 -- GHC的安装路径在`stack path`中的`ghc-paths`中。 -- GHC版本: -```shell -stack exec -- ghc --version -``` -- 进入交互式执行环境: -```shell -stack exec -- ghci -``` -- 离开交互式环境:`:q`。 -- 如果希望直接`ghci ghc`命令启动编译器或者交互环境,可以将其路径添加到path变量。不过这样是不能使用stack安装的包的,所以仅在非常有限的范围内测试时可以这样做,更建议使用通过stack启动。 -- 到这里GHC就安装成功了,和我们手动从GHC的网站上下载解压配置其实是一样的,不过是用stack来管理了。 -- stack可以管理多个版本的GHC,为了避免冲突,可以使用命令行来选择版本:`stack --compiler ghc-8.8.4 exec ghci`。 - -### 关于Cabal和stack - -- cabal是另一个包管理和项目工具,和stack有区别有联系,cabal的包管理库是Hackage,stack是Stackage,Stackage官网介绍Stackage是Hackage的子集的分发。 -- 都可以管理包,都可以管理项目,使用Stack还可以实现同一个项目兼容两个工具。但stack好像是为了解决cabal的某些痛点,具体还未研究那么深。暂时未安装cabal,仅使用Stack。 - -### GHC基本使用 - -编译器使用: -- 新建文件`hello.hs` -```haskell -main = print "hello,world" -``` -- 编译执行: -```shell -ghc hello.hs -./hello -``` -- 交互式执行环境 -```shell -ghci -``` -- GHCI中加载加载`hello.hs`文件。 -``` -Prelude> :l hello.hs -[1 of 1] Compiling Main ( hello.hs, interpreted ) -Ok, one module loaded. -*Main> main -"hello,world" -``` -- 学习中的正常工作流程可以是创建修改`hs`文件,ghci中重新加载,执行特定函数。 -- GHCI常用命令: - - `:l :load` 加载 - - `:r :reload` 重载 - - `:t :type` 类型,针对函数 - - `:i :info` 信息,针对函数、类型、类型类等。 - - `:k :kind` 得知一个类型的Kind。 - -- 更改GHCI的提示符: -```shell -:set prompt "ghci> " -``` -- 仅执行haskell脚本不编译: -```shell -runhaskell hello.hs -``` -- 列出所有已安装的包,通过stack执行则可以同时列出通过stack安装的所有包: -```shell -ghc-pkg list -stack exec ghc-pkg list -``` - -VsCode环境配置: -- 安装Haskell扩展。 -- 安装Haskell插件后将会自动下载对应版本的Haskell Language Server,也可以在插件设置中语言服务器的路径(只有一个全局设置,无法为项目设置)。启用插件将会自动开始Haskell Language Server子进程,将会吃掉将近1个G内存,提供补全、求值、类型推断等服务。 -- 仅仅学习语法的话,如果每次测试都将结果打印出来,会非常冗余,利用VsCode插件和语言服务器提供的功能,可以在注释中进行测试: -```haskell -{- ->>> 1 + 1 -2 --} -``` -- `{--}`注释中在`>>>`后输入需要测试的表达式,语言服务器会自动求值并将结果填写在注释中,既能保留下测试结果,又不影响主体逻辑,和在`ghci`中运行是一个道理,修改了代码刷新一下便会立即得出测试结果。这样就不需要再写很多冗余的简单测试打印代码了,也不必在每个`.hs`中都定义`main`,如果是单文件编译,和C一样不定义`main`是链接不过的。 -- `>>>`测试中不支持标准输入输出,[更多详细信息查看文档](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md)。 - -## Stack使用指南 - -前期可以使用单文件GHC命令行编译加上GHCI交互环境已经够用了,后面必然需要了解如何组织项目,使用stack管理项目和环境。这节内容基本都翻译自Stack文档。 - -### 关于Stack - -文档: -- [Stack快速入门](https://docs.haskellstack.org/en/stable/README/#quick-start-guide)。 -- [Stack User Guide](https://docs.haskellstack.org/en/stable/GUIDE/)。 - -Stack功能: -- 管理GHC工具链(Windows中还包括MSYS)。 -- 构建和注册库。 -- 可以说stack管理所有做Haskell开发需要的东西。 - -Stack的设计: -- 设计哲学是可重用的构建,也就是说今天和明天运行`stack build`应该得到同样的结果。某些情况下可能会有变化,比如修改了操作系统配置,但整体上来说stack基本上是遵循这条设计哲学的。 -- 为了简单地实现这一点,stack使用精心选择和组织的包集合称之为**snapshots**(快照)。 -- 读完`stack --help`中的帮助就足够开始和运行了。 -- 根目录中的`stack.yaml`主要保存项目的环境,称之为**resolver**,根据其中的版本信息来选择要使用哪个版本编译器和库。 -- stack是独立的,不会影响和干扰本地独立安装的GHC或者cabal或者其他安装工具安装的包和编译器。 -- 更推荐在GNU/Linux上使用stack(特别是64位Ubuntu),stack除了极少数子命令是平台特定的外命令基本是跨平台的。代码如果跨平台的话,那项目就可以轻松跨平台。 - -### 开始使用 - -基本命令: -```shell -stack new my-project -cd my-project -stack setup -stack build -stack exec my-project-exe -``` -- `stack new`新建项目。 -- `stack setup`下载编译器(如果有必要)到一个独立的位置(默认在`~/.stack`),不会干扰系统中已有的安装,`stack path`查看路径。 -- `stack build`构建项目。 -- `stack exec my-project-exe`执行构建完成的项目,`-exe`加在项目名称后,这是默认的最终生成的可执行文件名称。 -- 使用`stack install `安装一个包。 -- `stack new`创建的项目目录结构: -```haskell -. -├── app -│ └── Main.hs -├── ChangeLog.md -├── LICENSE -├── my-project.cabal -├── package.yaml -├── README.md -├── Setup.hs -├── src -│ └── Lib.hs -├── stack.yaml -└── test - └── Spec.hs - - 3 directories, 10 files -``` -- `stack build`会生成`.stack-work/`目录,依赖和生成文件都会被放在这里,默认会添加到`.gitignore`。 -- 管理库:编辑`src/`目录。 -- `app/`目录中应该仅包含只与可执行文件相关的内容是最完美的。 -- 添加依赖:编辑`package.yaml`的`dependencies`域。 -- 再次运行`stack build`,`stack`会自动更新`my-project.cabal`,如果想的话,也可以手动编辑`.cabal`然后`stack`为你自动更新`package.yaml`。这两个文件是`stack`和`cabal`的项目文件,`stack`同时提供支持。 -- 如果遇到依赖的包不在当前LTS版本中时,可以尝试在`stack.yaml`中的`extra-deps`域中添加新版本。 - -### 基本命令 - -新建项目: -- `stack new PACKAGE_NAME [TEMPLATE_NAME]`,不指定模板,则使用默认的模板,更多模板相关信息执行`stack templates`查看,模板也可以是本地文件、远程URL。 -- 如果最终会发布的话包的名称就是这个`PACKAGE_NAME`,由字母数字和连字符组成。 - -构建项目: -- `stack build`会查找本地没有的依赖,然后自动下载,也可以手动`stack setup`做这一步。然后开始构建。 -- GHC被下载到全局的stack路径中,Windows中是`%LOCALAPPDATA%\Programs\stack\`,而Linux中是`~/.stack/programs/`。 -- `stack ghc`或者`stack exec ghc`执行GHC编译器,还有`runhaskell runghc ghci`等命令。 -- 观察`stack build`的输出,会发现同时构建了库和可执行文件,默认模板中创建了模块`Lib Main`,前者是库,后者是可执行文件包含入口`main`,并将其安装到了`./.stack-work/`中。 -- 现在在目录内执行`stack exec PACKAGE_NAME-exe`就会执行程序了,stack知道去哪里找这个文件。 -- 在目录内执行`stack exec ghci`会直接加载已经编译的所有模块。 -- 构建命令是整个stack的核心和灵魂,用以构建、测试、获取依赖等,还可以定制,有许多高级的功能。 -- 使用同样的选项运行`stack build`两次,那么第二次应该什么事情也不做,构建运行过程应该是可重复的。 -- 后续会更加详细地介绍。 - -测试项目: -- 仔细看会发现有一个`test/`目录,其中用来编写测试用例。 -- 执行`stack test`会先编译其中的程序,然后再执行。对于`build test`子命令,已经构建过的组件不会被再次编译,除非经过了修改。 - -`stack setup`: -- `stack setup [GHC_VERSION]`可以安装特定版本的GHC,其他选项可以查看帮助。 -- `stack exec -- which ghc`可以查看GHC安装路径(Linux),或者`stack path`。 - -清理项目: -- `stack clean`清理工作目录,清理编译器输出文件,一般是`.stack-work/dist/`。 -- `stack clean `为特定的包做清理。 -- `stack purge`清理得更彻底,会直接将整个`.stack-work/`删掉,包括exrea-deps,git依赖和包括日志在内的编译器输出。但不会删除已经安装的包的快照,编译器或者使用`stack install`安装的包。让项目回到未进行`stack build`的状态,是`stack clean --full`的别名。 - -不同的数据库: -- 在项目内执行`stack exec -- ghc-pkg list`,会看到不同层级的包。 -- 三个不同位置: - - GHC安装位置:`.stack/programs/...`(Linux)或者`%LOCALAPPDATA%\programs\stack\...`(Windows)。编译器自带。 - - stack安装新包的位置`.stack/snapshots/`(Linux)或者`%STACK_ROOT%\snapshots\`(Windows)。通过`stack install`安装。 - - 本地项目中生成的。 -- 不同的项目使用同一个包是可以复用但又不会互相干扰。 - -命令别名: -- 一些命令是由别名定义的: -```shell - build Build the package(s) in this directory/configuration - install Shortcut for 'build --copy-bins' - test Shortcut for 'build --test' - bench Shortcut for 'build --bench' - haddock Shortcut for 'build --haddock' -``` -- 具体含义可查看帮助,[Haddock](https://github.com/haskell/haddock)是从Haskell源码生成Haskell文档的标准工具。 -- `install / --copy-bins`仅做一件事情(并非下载),就是将生成的可执行文件拷贝到本地`bin`路径。可以通过`stack path --local-bin`获取。所有文档中会建议将这个路径添加到`path`环境变量。这个特性很方便,一些包在安装时就会做这样的事情,添加之后就可以使用`executable-name`执行,而不再需要在项目内执行`stack exec executable-name`。 - -灵活地构建: -- 通过命令参数可以实现非常灵活地构建。 -- 指定包名:`stack build package-name`,包不仅可以是本地的,还可以在`extra-deps`中,snapshot中,或者仅仅是在网络的上游。如果在网络上并且没有在本地或者snapshot和extra-deps中,那么会自动添加到`extra-deps`中(试验中好像并不会)。 -- 最灵活的地方来自`stack build helloworld:test:helloworld-test`指定组件构造,含义是构建`helloworld`包中`helloworld-test`的测试组件。可以简写为`stack build helloworld:helloworld-test`设置`stack build :helloworld-test`。 -- 也可以指定目录构建,只触发该目录和其子目录的构建。仅构建当前所在目录可以用`stack build .`。 -- 不指定参数和同时指定所有包名为参数是同一含义。 -- 这里说组件其实就是指其中的模块,比如默认模板中就会生成`Lib Main`模块。 -- `stack ide targets`可以看到所有目标。 - -构建测试和Benchmark: -- `stack build`会构建所有库(如果有)、可执行文件但是会忽略Test Suite和Benchmark。 -- 如果要构建测试用例和性能测试可以使用`--test` `--bench`参数,加进来之后就会一起构建了`stack build --test helloworld`。 -- 直接指定测试组件则不会同时构建可执行文件`stack build :helloworld-test`。(文档中这样说,但是在我本地却同时构建了,尽管并不依赖),构建测试套件之后会执行,benchmark同理,可以使用`--no-run-tests --no-run-benchmarks`来让他们不要运行。 -- stack不会为非本地项目构建测试套件和性能测试。 - -### 项目配置 - -仔细看目录结构: -- `app/Main.hs src/Lib.hs test/Spec.hs`是源文件,分别是可执行文件、库、测试逻辑的代码,是项目的功能代码。 -- `LICENSE README.md ChangeLog.md`是契合开源项目的信息,不参与构建。 -- `my-project.cabal`是另一个构建工具Cabal的配置文件,在`stack build`过程中会自动更新,不应该修改。 -- 核心项目配置文件是`Setup.hs stack.yaml package.yaml`。 - - `Setup.hs`是Cabal构建系统的一个组件,从技术上来说stack并不需要这个文件,但包含这个文件依然是一个好的实践。 - - `stack.yaml`中信息并不多,但注释很多。目前主要看两个域:`packages`告诉stack构建本地项目中的哪些包,仅有一个包的话,一个`.`就足够。但stack是支持在同一个项目中包含多个包的。另一个域`resolver`,stack按这个域确定构建项目使用的GHC版本和包的依赖,也就是Stackage的版本,比如lts-18.17就对应ghc-8.10.7,`setup`是根据这个信息去下载GHC的,在[Stackage官网](https://www.stackage.org/)上能够看到。 - - `package.yaml`则是关于包的信息,是由内建在stack中的[hpack tool](https://github.com/sol/hpack)提供的。默认行为是从`package.yaml`生成`.cabal`,而不去更改`.cabal`。 -- stack是基于Cabal的,Cabal中,每个包使用一个独立的`.cabal`文件描述,其中包含多个组件:库、可执行文件、测试套件、benchmark,还有其他信息,比如库依赖、默认编译选项等。 -- 最重要的是需要知道如何修改`package.yaml`中的必要配置。可以在[Hpack 文档](https://github.com/sol/hpack#quick-reference)中找到所有可用选项。 - -添加依赖: -- `package.yaml`的`dependencies`域: -```yaml -dependencies: -- base >= 4.7 && < 5 -- text -- random -# add more dependencies here -``` -- 使用了新的包时需要在此处添加依赖,可以指定版本,再次运行`stack build`将会安装。 -- 列出所有依赖:`stack ls dependencies`。 - -**extrs-deps**: -- 如果添加一个依赖: -```haskell -module Lib - ( someFunc - ) where - -import Acme.Missiles - -someFunc :: IO () -someFunc = launchMissiles -``` -```yaml -dependencies: -- base >= 4.7 && < 5 -- acme-missiles -``` -- 执行`stack build`却报错了。原因就在于[LTS resolver](https://github.com/commercialhaskell/lts-haskell#readme),`stack new`创建项目时是选择了当前的LTS版本的,每个LTS都有自己的精心维护的包的集合。如果依赖中的包不在这个集合中那么即使添加到了`dependencies`中也会报错。 -- 当前版本已经不包含这个包了,所以自然不行,为了解决这个问题,需要使用到`stack.yaml`的`extra-deps`域,用来定义不在当前LTS resolver但是在[Hackage](https://hackage.haskell.org/package/acme-missiles)中的包。添加之后再次`stack build`就会成功。 -```yaml -extra-deps: -- acme-missiles-0.3 # not in the LTS -``` -- Stackage是Stable sets of Haskell Packages from Hackage,所以这样的需求可能会常遇到。 - -关于LTS resolver: -- 在[Stack官网](https://www.stackage.org/lts)可以找到最新的LTS,`resolver`的值是`resolver: lts-18.17`,在`setup`时会用到。其使用的GHC版本,然后其中可用的包的集合(快照,Snapshots),可以通过Hoogle搜索这个快照。 -- 点开某一个包,可以看到其中可用的模块,根据这些信息可以确定要将那个包加入到`package.yaml`中。 -- 会注意到有[LTS](https://github.com/commercialhaskell/lts-haskell#readme)(Long Term Support)和Nighthly的区分,一般使用长期支持版,stack也会默认使用LTS。 - -修改编译器版本: -- 当前使用`lts-18.17`如果我想换成Nighthly版本,那么只需要将`stack.yaml`修改一下:可以指定为LTS版本、Nightly或者GHC版本都可以。 -```yaml -resolver: nightly-2020-02-08 -``` -- 运行`stack build`将下载对应的GHC和依赖的库,选这个版本也是因为[最新的Nightly版本](https://github.com/commercialhaskell/stackage-snapshots)TUNA和中科大没有镜像用不了,仅选一个有镜像的版本做测试而已,实践中还是使用LTS最好。 -- 在命令中使用`--resolver`选项时,可以用`nightly`参数指代最新Nightly版本,`lts`指代最新LTS版本,`lts-2`指代`lts-2.x.x`的最新版本。不可用于`stack.yaml`。 -```shell -stack --resolver lts-2 build -``` -- Nightly的版本是按照日期命名的,`nightly-YYYY-MM-DD`,LTS和GHC则是向上走的版本号`lts-X.Y` `ghc-X.Y.Z`。 - -本地和远程的依赖: -- stack可以管理多个包,如果你将多个包`unpack`到本地,那么`packages`域将会有多个包。 -- 需要区分依赖的是本地的包还是上游的包(构建时下载到本地snapshots)。 - -### 运行现有的项目 - -来构建一个开源项目,这里选择[yackage](https://www.stackage.org/package/yackage),为了获取到发布到Stackage的代码,可以使用`stack unpack`: -``` -stack unpack yackage-0.8.1 [--to yourDir] -``` -- 也可以直接`git clone`,可以看到其中没有`stack.yaml`,可以手动创建,也可以使用`stack init`: -- `stack init`会生成`stack.yaml`,并尝试使用一个最匹配的LTS或者Nightly版本: -- 也可以指定resolver版本: -```shell -stack init --resolver -``` -- 由于各种原因,本地构建失败了,略过。 - -### 编译选项 - -两种方式更改一个包安装的方式:Cabal标志和GHC选项。 -- 前者为每一个项目设置,意味着编译`yackage`时`-`关掉`upload`选项,说实话没太搞懂什么意思。 -```shell -stack build --flag yackage:-upload -``` -- GHC选项和Cabal的标志类似,但有一些改变,[文档](https://docs.haskellstack.org/en/stable/yaml_configuration/#ghc-options)。GHC看来也还是要学习一下,编译选项,基本使用之类的,毕竟stack也是调用的GHC去编译。 -- 这一节都尚不是很清楚,需要实践后补充细节。 - -### 路径 - -一般来说不需要知道stack存了一些什么文件在什么地方,`stack path`可以很多好的展示这些路径: -```shell -tch@KillingBoat:~$ stack path -snapshot-doc-root: /home/tch/.stack/snapshots/x86_64-linux-tinfo6/92a82299ffe7e01dd411553be385541e3e9cf3a60cd0bd12003f0fa41dfe1b7a/8.10.7/doc -local-doc-root: /home/tch/.stack/global-project/.stack-work/install/x86_64-linux-tinfo6/92a82299ffe7e01dd411553be385541e3e9cf3a60cd0bd12003f0fa41dfe1b7a/8.10.7/doc -local-hoogle-root: /home/tch/.stack/global-project/.stack-work/hoogle/x86_64-linux-tinfo6/92a82299ffe7e01dd411553be385541e3e9cf3a60cd0bd12003f0fa41dfe1b7a/8.10.7 -stack-root: /home/tch/.stack -project-root: /home/tch/.stack/global-project -config-location: /home/tch/.stack/global-project/stack.yaml -bin-path: /home/tch/.stack/snapshots/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/bin:/home/tch/.stack/compiler-tools/x86_64-linux-tinfo6/ghc-8.10.7/bin:/home/tch/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin:/home/tch/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -programs: /home/tch/.stack/programs/x86_64-linux -compiler-exe: /home/tch/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-8.10.7 -compiler-bin: /home/tch/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin -compiler-tools-bin: /home/tch/.stack/compiler-tools/x86_64-linux-tinfo6/ghc-8.10.7/bin -local-bin: /home/tch/.local/bin -extra-include-dirs: -extra-library-dirs: -snapshot-pkg-db: /home/tch/.stack/snapshots/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/pkgdb -local-pkg-db: /home/tch/.stack/global-project/.stack-work/install/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/pkgdb -global-pkg-db: /home/tch/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/lib/ghc-8.10.7/package.conf.d -ghc-package-path: /home/tch/.stack/global-project/.stack-work/install/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/pkgdb:/home/tch/.stack/snapshots/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/pkgdb:/home/tch/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/lib/ghc-8.10.7/package.conf.d -snapshot-install-root: /home/tch/.stack/snapshots/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7 -local-install-root: /home/tch/.stack/global-project/.stack-work/install/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7 -dist-dir: .stack-work/dist/x86_64-linux-tinfo6/Cabal-3.2.1.0 -local-hpc-root: /home/tch/.stack/global-project/.stack-work/install/x86_64-linux-tinfo6/82db7b2595a76656db6d1508a5dd57aced5d628188b355b35247c7fd357c1396/8.10.7/hpc -local-bin-path: /home/tch/.local/bin -ghc-paths: /home/tch/.stack/programs/x86_64-linux -``` - -- 看到名称基本都能知道是干什么的,`ghc-paths`就是编译器安装路径,基本都是在`.stack`中,`local-bin`就是前面提到的`install`的文件复制的目标目录。Windows同理。 - -移除stack: -- 说到路径要移除stack的话,需要移除的目录或文件有: - - stack可执行文件本身。 - - `stack path --stack-root`,根目录,Linux中的`~/.stack/`,windows中的`C:\sr\`。 - - `stack path --programs`,Windows中的`%LOCALAPPDATA%\Programs\stack`。 - - 任何项目内的`.stack-work`。 - -### exec - -`stack exec`命令在执行命令时会稍微修改一些环境,查找目录也会额外从stack的二进制路径中查找。然后设置一些额外的环境变量(比如添加一些路径到PATH,设置`GHC_PACKAGE_PATH`环境变量,这个会告诉GHC用哪个包数据库)。 - -添加到path中的骑士就是`stack path --stack-root`中的一些子目录`bin`和`stack path --programs`中的一些可执行文件目录。 -linux中执行可以看到环境: -```shell -stack exec env -``` -区分传递给`stack`还是传递给`exec`执行的程序的选项很重要,可以用`--`分隔。要执行命令前加`--`,后面都是该命令的参数,前面则是给`stack`的。 - -运行`stack exec bash`,那么则可以在这个子Shell中执行那些添加到PATH中的命令。 - -### ghci - -GHCI是GHC的REPL环境,进入GHCI: -```shell -stack exec ghci -``` -如果要让本地模块都可以访问,可以在项目内`stack ghci`,会将项目内的包可以被访问。然后使用`:m module`加载模块使用。 - -对于单文件编译的场合,提供了`stack exec ghc/runghc`命令或者单纯使用`stack ghc/runghc`都可以。 - -### 脚本 - -stack还可以作为脚本解释器,用来创建可重用的Haskell脚本,而不像Bash或者Python那样。具体就不展开了。Windows不能直接`./script.hs`执行脚本,可以用`stack script.hs`这样执行。 -```haskell -#!/usr/bin/env stack -main = print "hello,world!" -``` -这样写脚本需要一些必要的注释说明Resolver(固定resolver之后就能保证可重用)等信息,具体信息不展开。上面的脚本会报警告,但能执行。 - -更多信息可在查阅文档。 - -## 编辑器集成 - -主要讨论VsCode如何搭配Stack和[Haskell Language Server](https://github.com/haskell/haskell-language-server)的问题。 - -Hakell Language Server是[Ghcide](https://github.com/haskell/ghcide)和[Haskell IDE Engine(HIE)](https://github.com/haskell/haskell-ide-engine)的继任者,由双方的团队合作共同开发。后面两者现已归档,最新的程序应该使用Haskell Language Server。 - -### VsCode环境配置 - -插件: -- 使用Haskell官方插件[vscode-haskell](https://github.com/haskell/vscode-haskell),由Haskell Language Server提供支持。 - -特性(来自Haskell Language Server): -- 来自GHC的警告和错误诊断。 -- 鼠标悬停类型信息和文档信息,包括本地代码中的注释。 -- 本地代码跳转。 -- 文档符号。 -- 文档高亮。 -- 代码补全。 -- 代码格式化。 -- `>>>`注释中的代码求值,`prop>`注释中的代码测试。 -- 继承了retrie,一个强大的代码修改工具。左上角灯泡。 -- 提示导入函数列表,就是会提示只导入使用的符号。 -- 集成了hlint,分析代码并提供快速修复的选项,下划线。 -- 模块名修改建议。 -- 调用层次查询,右键菜单Show Call Hierarchy。 - -插件配置: -- 无需过于关心,修改文档和代码打开行为为本地VsCode而不是Hackage网页更好一些。 -- 可能需要手动设置Haskell Language Server的路径,而不是让其自动下载。 - -依赖: -- 对于单独的`.hs .lhs`文件来说,GHC必须在PATH中。 -- 基于Cabal的项目,GHC和cabal-install都要在PATH中。 -- 基于stack的项目,stack必须在PATH中。 - -对stack项目的支持: -- `haskell-language-server`需要编译项目之后才能提供诊断,也就是说它必须知道怎么做。 -- 一个叫做[hie-bios](https://github.com/haskell/hie-bios)的项目就是用来处理这个事情的。 -- `hie-bios`使用一个项目根目录中的`hie.yaml`来管理这些,显式描述了怎么设置环境来编译项目中的不同组件。为此你需要知道项目中有什么组件(模块)并将它们的路径显式指出来。 -- 可以使用[implicit-hie](https://github.com/Avi-D-coder/implicit-hie)项目来从stack或者cabal配置生成这个`hie.yaml`文件。 -- 如果stack项目中有多个组件,`hie.yaml`会像是这个样子: -```yaml -cradle: - stack: - - path: "./test/functional/" - component: "haskell-language-server:func-test" - - path: "./exe/Main.hs" - component: "haskell-language-server:exe:haskell-language-server" - - path: "./exe/Wrapper.hs" - component: "haskell-language-server:exe:haskell-language-server-wrapper" - - path: "./src" - component: "haskell-language-server:lib" - - path: "./ghcide/src" - component: "ghcide:lib:ghcide" - - path: "./ghcide/exe" - component: "ghcide:exe:ghcide" -``` -- 最终的配置会是下面的一个子集: -```yaml -cradle: - cabal: - component: "optional component name" - stack: - component: "optional component name" - bios: - program: "program to run" - dependency-program: "optional program to run" - direct: - arguments: ["list","of","ghc","arguments"] - default: - none: - -dependencies: - - someDep -``` -- 上面是Haksell Language Server文档提供的。 - -看一下`hie-bios`文档: -- `hie-bios`需要知道传递给GHC的参数,和包的依赖,因为需要先构建依赖。 -- 它的设计指导原则是由构建工具负责描述环境,确定要构建哪一个包。 -- `hie-bios`既不依赖Cabal也不读取任何编译生成文件。而是仅仅依赖于标准GHC的标志,如果一个构建工具支持`repl`命令,运行`repl`会使用正确的标记调用`ghci`,`hie-bios`需要一个方式来得到这些标记。然后才能正确设置给GHC API session。进一步说任何设置API session的错误都是构建工具的锅,他们需要提供正确的标记以便编辑器对项目提供支持。 - -`hie-bios`对stack的支持: -- 显式声明想使用stack,那么`hie.yaml`就要像这样: -```yaml -cradle: - stack: -``` -- 如果整个项目能够被`stack repl`载入,那么这样就足够了。这种配置在最简单的仅有一个库、一个可执行文件、一个测试套件时正常工作。 -- 一些项目拥有多个`stack-*.yaml`指定了多个版本的GHC编译器。这种情况可以使用`stackYaml`指定要使用哪一个,路径以`hie.yaml`为基准。 -```yaml -cradle: - stack: - stackYaml: "./stack-8.8.3.yaml" -``` -- 如果项目更加复杂,就需要指定想要加载哪一个组件,一个组件简单来说就是stack中的一个库、一个可执行文件、或者一个测试套件、或者benchmark。可以使用命令查看所有组件或者叫目标,至于目标的语法可以参见[stack文档-Traget Syntax](https://docs.haskellstack.org/en/stable/build_command/#target-syntax)。 -```shell -stack ide targets -``` -- 然后指定组件对应的路径(目录或者文件,当多个组件共用一个目录中文件时指定为文件很有用): -```yaml -cradle: - stack: - - path: "./src" - component: "hie-bios:lib" - - path: "./exe" - component: "hie-bios:exe:hie-bios" - - path: "./tests/BiosTests.hs" - component: "hie-bios:test:hie-bios" - - path: "./tests/ParserTests.hs" - component: "hie-bios:test:parser-tests" -``` -- 如果插件对stack项目未工作,那么可以尝试`stack repl`和`stack repl `。如果失败了,那么应该就是项目无法构建,当解决之后便能成功加载。 - -最后便是使用[`implicit-hie`](https://github.com/Avi-D-coder/implicit-hie)自动生成`hie.yaml`: -```shell -cd packageDir -stack install implicit-hie -gen-hie > hie.yaml -``` -- 需要在项目根目录运行,根据项目文件识别是使用stack还是cabal,也可以用`--stack --cabal`显式指定。 -- 普通的项目是完全能用的,如果使用了更高级的特性,可能还是需要自己来再修改一下`hie.yaml`。 - -使用感受: -- 感觉使用体验也不是那么完美,速度不算快,先用用看吧。 - -## 感受一下Haskell - -ghci中进行表达式求值: -``` -Prelude> 1 + 2 -3 -Prelude> 5 * -3 - -:2:1: error: - Precedence parsing error - cannot mix ‘*’ [infixl 7] and prefix `-' [infixl 6] in the same infix expression -Prelude> 5 * (-3) --15 -Prelude> 1 / 3 -0.3333333333333333 -Prelude> True -True -Prelude> True && False -False -Prelude> True && True -True -Prelude> False || True -True -Prelude> 5 == 5 -True -Prelude> 5 /= 4 -True -Prelude> 5 /= 5 -False -Prelude> "hello" == "hello" -True -``` -- `5 * -3`会有错误,使用负数时最好加上括号。 -- 运算符也是函数,`== + * /`等需要两个操作数,运算符放中间,就叫中缀函数,一个操作数运算符放前面叫前缀函数。函数调用就是`funcName arg1 arg2`,函数名加上空格分隔的参数列表。 -- 函数`succ`返回一个数的后继: -``` -Prelude> succ 1 -2 -Prelude> succ (succ 2 + 3) -7 -``` -- 函数调用拥有最高优先级。 -``` -Prelude> succ 9 + max 5 4 + 1 -16 -Prelude> succ 8 * 10 -90 -Prelude> succ (8 * 10) -81 -``` - -## 基本要素 - -事后补充,要熟悉了解有概念的东西。 - -### 基本内容 - -- 单行注释:`--` -- 多行注释:`{- -}` -- 在文件头对GHC声明一些编译参数:`{-# #-}` - - - -### 运算符 - -基本类似于C语言,但使用`not`表示逻辑非。 -- [Haskell运算符全解](https://zhuanlan.zhihu.com/p/263797220) -- 结合性分为左结合、右结合和不结合,优先级越大越高。 - -优先级|左结合|不结合|右结合 -:-:|:-|:-|:- -9|`!!`||`.` -8|||`^` `^^` `**` -7|`*` `/` `` `div` `` `` `mod` `` `` `rem` `` `` `quot` ``|| -6|`+` `-`|| -5|||`:` `++` -4||`=` `/=` `<` `<=` `>` `>=` `` `elem` `` `` `notElem` ``| -3|||`&&` -2|||`\|\|` -1|`>` `>>=`|| -0|||`$` `$!` `seq` - -### 基本类型类 - -基本的Typeclass: -- `Eq` 可判断相等性的类型 -- `Ord` 可比较大小的类型 -- `Show` 可表示为字符串的类型 -- `Read` 可从字符串转换出值的类型 -- `Enum` 连续的,也就是可枚举的类型。每个值都有后继 (successer) 和前置 (predecesor),分别可以通过 `succ` 函数和 `pred` 函数得到。 -- `Bounded` 有上限和下限。例如:`maxBound :: Char` 或者 `maxBound :: Bool` -- `Num` 数字 -- `Integral` 整数,包括`Int Integer` -- `Floating` 浮点数,包括`Float Double` - -## 函数 - -### 定义函数 - -定义一个函数:和调用类似,参数列表加上`=`号后跟函数体。 -```haskell -doubleMe x = x + x -``` -调用: -```haskell -doubleMe 10 -``` -函数中当然可以调用函数: -```haskell -doubleUs x y = doubleMe x + y + y -``` -Haskell中函数不需要先声明或者定义才能使用,可以先定义`doubleUs`再定义`doubleMe`。 - -Haskell中每个函数或者表达式都要返回一个结果,比如`if`必须要有一个`else`语句。Haskell中的`if`语句其实是一个表达式。 -```haskell -doubleSmallNumber' x = (if x > 100 then x else x * 2) + 1 -``` -其中`'`也是函数名的合法字符,常常使用单引号来区分一个稍经修改但差别不大的函数。 - -没有参数的函数称之为定义或者名字,定义后不可修改。 -```haskell -hello = "hello,world!" -``` - -### 使用List - -Haskell中,List是最常用的数据结构,十分强大,可以解决许多问题。List是单类型的数据结构,不能将不同类型数据放到同一个List中。 - -ghci中可以使用`let a = 1`来定义常量,与脚本中`a = 1`相同。 - -List列表语法: -- 对于字符串来说,`"hello"`仅仅只是`['h','e','l','l','o']`的语法糖,也就是说字符串就是List。 -- 合并两个List,`l1 ++ l2`。实现中会遍历左边的List,对于长字符串或者列表不是很友好。 -- 使用`:`可以往列表前插入元素,`elem1 : list`。如果要使用`++`连接单个元素到List可以用`[elem1] ++ list`。 -- 实际上`[1, 2, 3]`就是`1:2:3:[]`的语法糖,也就是空列表依次在前面插入元素。 -- 按照索引取List元素:`[1, 2, 3] !! 2`,索引从0开始。越界将报错。 -- List同样可以存放List,不过List的元素类型是它的类型的一部分,需要类型匹配:`[1]:[[2]]`。 -- List内部元素可比较时,可以使用`> >= ==`等运算符比较大小,将会按元素依次比较。 -- List常用函数: - - `head`返回首部,即首元素,结果是元素,列表为空将触发异常。 - - `tail`返回尾部,去掉首个元素后的部分,结果是列表,列表为空将触发异常。 - - `last`返回最后一个元素。 - - `init`返回除去最后一个元素的部分。 - - 上面几个函数用于空列表,将在运行时触发异常,编译时不会检查到。 - - `length`得到列表长度。 - - `null`判断列表是否为空,返回`True False`。相比`list == []`来判断会更好。 - - `reverse`反转列表。 - - `take n list`取前n个元素构成的列表。 - - `drop n list`去掉前n个元素,得到剩余元素构成的列表。 - - `maximum minimum`得到最大和最小的元素。 - - `sum product`返回列表所有元素的和与乘积。 - - `elem`判断一个元素是否包含与一个list,`elem 10 [1, 2, 10]`,通常以中缀形式调用``10 `elem` [1, 2, 10]``(需要加反引号)。 - -### 使用Range - -范围: -- `[1..20]`即表示`[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]`。 -- 给出指定第二个元素将使用第一个和第二个元素的间隔作为步长,默认是1`[1,3..20]`表示`[1,3,5,7,9,11,13,15,17,19]`。 -- 步长可以为负,到达不了上限则生成列表为空:`[1,0..10]`为`[]`。 -- 步长仅能指明一次,`[1,2,4..20]`是非法的。 -- 不指定上限则无限长度,步长为0同样也是无限长度。无限长度列表将会在使用到时才求值,因为是懒惰求值脚本中定义后不立即求职,比如取某个元素时才会求值。 -- **注意**:如果对无限长度列表求值,在ghci中输入无限长度的列表将会一直打印,在`{--}`注释中`>>>`后的表达式不要写无限长度列表,因为会对列表求值,求值工作由后台的Haskell Language Server来做(比如在VsCode中,点击了提示中的`Evaluate...`),此时后台的语言服务器将陷入无限循环,吃掉大量内存和CPU。需要杀掉子进程才能结束。 -- `cycle list`可以生成循环list元素的无限列表。 -- `repeat value`生成一个仅包含该值的无限列表。 -- `replicate n value`重复一个值n次。 -### List Comprehension - -也就是列表生成式: -- `[expression | ranges and constrait]`,`|`左侧是表达式,右侧是变量范围和约束条件。 -- `[x * 2 | x <- [1..10]]`得到`[2,4,6,8,10,12,14,16,18,20]`。 -- 表达式中可以是`if-else`语句,因为其也是一个表达式。 -- 作用就是`x`遍历`<-`后列表中所有元素,类似于做了循环,变量可以有多个`[[x, y] | x <- [1..], y <- [10..], x + y == 20]`,可以加条件,就像一个数学中的集合生成式一样。 -- 如果不关心从列表中取出的值,可以用`_`,`length' xs = sum [1 | _ <- xs]`。 -- 移除所有非大写字符:``removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]``。 -- 列表生成式可以拆成多行,比较长的话拆成多行比较合适。 - -### 元组 - -一般来说,在Haskell中,列表的元素类型必须相同,而元组Tuple不同,元组可以放入不同类型元素。 -- 定义:`(elem1, elem2, ...)`。 -- 一般来说基本所有语言中,元组的元素数量和每个元素的类型同样是元组类型的一部分。很多语言中元组列表元素都可变,有的语言中元组不可变列表元素可变,当然Haskell是纯函数式语言,所以都不可变。 -- 元组的重点在于类型:`(1, 2)`和`(1, 2, 3)`和`(1, "hello")`自然是不同类型。元组的定位应该更类似于C语言中的结构,但是是匿名的,而列表则类似于数组,不可一概而论。 -- `fst snd`用于二元组(pair,序对)上取首和尾元素。不能用于其他长度的元组。 -- `zip`拉链,将两个列表对应元素合并起来得到二元组列表。较长的列表中的无对应元素被丢弃。 - -函数式编程语言的一般思路:先取一个初始集合,对其进行变形、执行过滤条件(map and reduce)得到最终结果。 - -## Type & Typeclass - -一般将其称之为**类型和类型类**。 - -### 类型 - -- Haskell是强类型和静态类型的。编译期每个表达式的类型都会被确定下来。类似于Scala,Haskell提供类型推导的能力。 -- 在VsCode中编写函数时,Haskell语言服务器会为我们自动推导类型,并作出提示。 -- 在ghci交互环境中:使用`:t`命令可以检测出其后跟的表达式的类型。 -``` -Prelude> :t [] -[] :: [a] -Prelude> :t [1] -[1] :: Num a => [a] -Prelude> :t ('a', "hello", 10) -('a', "hello", 10) :: Num c => (Char, [Char], c) -``` -- 输出结果为`expression :: type`,`::`读做“类型为”,凡是明确的类型,首字母必须大写。函数名则必须小写字母开头。 -- 常见类型:`Int Integer Float Double Bool Char`,`Integer`是高精度大整数,`Int`在32位系统中通常是32整数,在64位系统中是64位整数,`Float Double`分别是IEEE 754单精度和双精度浮点数。 -- 元组类型和元素个数和类型相关,比如`(Char, Int)`,数组类型则与元素类型相关,类似于`[Char]`,字符串是`String`等价于`[Char]`。 -- 函数的类型使用`->`将参数列表的类型和返回值类型串联起来,比如参数是两个`Int`,返回值类型是`Double`,则函数类型是`Int -> Int -> Double`。最后一个类型表示返回值类型。 - -### 类型变量 - -函数`fst`类型: -``` -Prelude> :t fst -fst :: (a, b) -> a -``` -- 注意到,这里的`a b`表示类型,可以用于任意类型,称之为类型变量。作用类似于其他编程语言中的泛型(Generic),但在Haskell中更为强大。有了类型变量可以轻易写出**类型无关的函数**,使用类型变量的函数称之为**多态函数**。 -- 上述的`a b`只是代表一个类型,并不一定要是不同类型,当用于`(Int, Char)`的参数时,`a`是`Int`,`b`是`Char`。 - -### Typeclass - -- Typeclass称之为类型类,定义类型的行为,如果某一个类型属于某一个类型类,那么它必然实现了该类型类描述的方法。就类似于其他语言中纯虚类或者接口类的作用。 -``` -Prelude> :t (==) -(==) :: Eq a => a -> a -> Bool -Prelude> :t elem -elem :: (Foldable t, Eq a) => a -> t a -> Bool -``` -- 其中的`Eq`就是一种类型类。 -- 这里有一个符号`=>`,其左边的东西叫**类型约束**(Type constraints ),一个类型声明可以看做两段,`=>`右边的部分是类型,左边的部分约束了类型变量必须属于的类型类。 -- 参数和返回值的类型如果属于某一个或几个类型类(也就是只对类型进行约束,不限定具体类型),那么必须放在`=>`前并用类型变量指代,如果是具体类型,必须放在`=>`后。 -- 多个类型约束放在括号中,可以用多个类型类约束同一个类型变量,表示一个类型必须同时属于多个类型类。 -- 常见类型类: - - `Eq`是可判断相等性的类型类,提供`== /=`函数,除函数以外所有类型都实现了这个类型类。 - - `Ord`是可比较大小的类型类,提供`< > <= >=`之类用于比较大小的函数。 - - `compare`函数用于两个同类`Ord`的比较,类型是`Ord a => a -> a -> Ordering`,结果是以下三个值之一:`LT EQ GT`,并具有大小关系`LT < EQ < GT`。 - ``` - Prelude> 5 `compare` 3 - GT - ``` - - `Show`是成员可用字符串表示的类型类。常用函数是`show`,将类型转换为`[Char]/String`。 - ``` - >>> :t show - show :: Show a => a -> String - >>> show True - "True" - >>> show [1, 2] - "[1,2]" - >>> show "hello" - "\"hello\"" - ``` - - `Read`是`Show`相反的类型类,`read`将一个字符串转换为`Read`的实例类型。作用可能就类似于在代码中这样写差不多。 - ``` - >>> :t read - read :: Read a => String -> a - >>> read "1.2" + 3.2 - 4.4 - >>> read "1" - Prelude.read: no parse - ``` - - 单纯的`read`返回一个`Read a`,无法区分具体类型,可以在调用时后面加上`::`类型注释,以明确类型。 - ``` - >>> read "1.0" :: Double - 1.0 - >>> read "(1.0, \"hello\", 10)" :: (Double, [Char], Int) - (1.0,"hello",10) - ``` - - `Enum`的类型类的实例都是可枚举类型,属于`Enum`类型类的类型可以用于`Range`中。每个值都有后继(successer)和前置(predecesor),可分别通过`suc`和`pred`得到。包含类型有:`() Bool Char Ordering Int Integer Float Double`。 - ``` - >>> succ LT - EQ - >>> [LT .. GT ] - [LT,EQ,GT] - >>> [1 .. 10] - [1,2,3,4,5,6,7,8,9,10] - >>> [False ..] - [False,True] - >>> succ 1.3 - 2.3 - ``` - - `Bounded`类型类都有一个上限和下限。`minBound maxBound`的返回类型是`Bounded a => a`,无参数,得到一个`Bounded`类型的下限和上限。 - ``` - >>> :t minBound - minBound :: Bounded a => a - >>> minBound :: Int - -9223372036854775808 - >>> maxBound :: Int - 9223372036854775807 - >>> minBound :: Ordering - LT - >>> minBound :: Char - '\NUL' - >>> maxBound :: Char - '\1114111' - ``` - - `Num`表示数字。包括所有实数和整数: - ``` - >>> :t (*) - (*) :: Num a => a -> a -> a - >>> :t (+) - (+) :: Num a => a -> a -> a - >>> (5 :: Int) * 6.0 - No instance for (Fractional Int) arising from the literal ‘6.0’ - ``` - - `Integral`是表示整数的类型类,包含`Int Integer`。 - - `Floating`表浮点数,包含`Float Double`。 - - `fromIntegral`函数处理数字时很有用,类型是`(Integral a, Num b) => a -> b`从整数提取出一个更通用的`Num`。比如当`length [1, 2] * 5`的`*`类型是`Int -> Int`没有问题,但`length [1, 2] * 5.0`则会类型不匹配。 - ``` - >>> :t fromIntegral - fromIntegral :: (Integral a, Num b) => a -> b - >>> length [1, 2, 3] * 5.0 - No instance for (Fractional Int) arising from the literal ‘5.0’ - >>> fromIntegral (length [1, 2, 3]) * 5.0 - 15.0 - ``` -- 可见Haskell对类型匹配的处理是很严格的,C++模板也可以做到类似的事情,不过对于内置类型来说,因为有整型提升和隐式类型转换的存在,运算符的类型检查其实并没有严格到这种地步。 -- 其实只是一种形式,类型类提供的功能,在其他语言中也有提供,java的interface,Python中的`__eq__ __str__`等特殊方法,C++的继承,都异曲同工。 - -多态函数: -- 多态函数在调用时会隐式地给传入类型参数给类型变量(可以类比C++模板函数调用时给的可以省略的模板类型参数),可以是具体类型那么类型参数就被确定,也可以是类型类那么就添加到约束中。如果类型变量本身就有约束,可以传入约束中类型类本身、子类型类、实现了这个类型类的类型实例。这是由Haskell的类型推导做到的,不用显示传入。 -```haskell ->>> :t fromIntegral -fromIntegral :: (Integral a, Num b) => a -> b ->>> :t sqrt -sqrt :: Floating a => a -> a ->>> :t sqrt . fromIntegral -sqrt . fromIntegral :: (Floating c, Integral a) => a -> c -``` -- 记住**类型类是约束而不是一种具体的类型**。 - -## 函数相关语法 - -### 模式匹配(Pattern matching) - -模式匹配本质上就是提供一种用来简化复杂的判断和比较的语法糖。 - -模式匹配通过检查数据的特定结构来检查是否匹配,并从模式中提取数据。 -- 对于列表可以使用`:`运算符进行匹配,比如判断一个列表是否是递增的函数: -```haskell -increasing :: (Ord a) => [a] -> Bool -increasing xs = - if xs == [] - then True - else if tail xs == [] - then True - else if head xs <= head (tail xs) - then increasing (tail xs) - else False -``` -- 如果使用模式匹配: -```haskell -increasing'' :: (Ord a) => [a] -> Bool -increasing'' [] = True -increasing'' [x] = True -increasing'' (x:y:ys) = x <= y && increasing''(y:ys) --- simplify -increasing''' :: Ord a => [a] -> Bool -increasing''' (x:y:ys) = x <= y && increasing''' (y:ys) -increasing''' _ = True -``` -- 模式匹配是按照定义顺序来的,匹配到就停止。【不像prolog会同时尝试匹配所有模式,不同模式的条件需要互斥】。 -- 调用的参数没有匹配到的话会抛出运行时异常,所以匹配应该完备,必须能够匹配所有情况的输入,需要编写一个模式用以匹配剩余的情况,比如使用`_`放到最后。 -- 匹配时若不需要接受匹配结果,则可以使用`_`。 -- 列表不能使用`++`匹配,`x:xs`常用于匹配不固定长度列表,常见于递归函数中,`[x, y]`直接列出元素用于匹配固定长度列表。 -- 列表的`as`模式可以用于获取整个列表,比如`xs@(x:y:ys)`,则`xs`得到整个`x:y:ys`,而不必再将整个`x:y:ys`写一遍。 -- 元组的模式匹配仅可以匹配具体的项。如: -```haskell -first :: (a, b, c) -> a -first (x, _, _) = x -second :: (a, b, c) -> b -second (_, y, _) = y -third :: (a, b, c) -> c -third (_, _, z) = z -``` - -### 守卫 - -模式用来检查一个值是否可以从中取值,而守卫(Guard)则用来检查一个值的某项属性是否为真,是另一种`if`的语法糖。在多分支条件下守卫的可读性更高,并且和模式匹配可以很好的契合。 -- 对于`increasing`函数的例子,可以使用守卫: -```haskell -increasing' :: (Ord a) => [a] -> Bool -increasing' xs - | null xs = True - | null (tail xs) = True - | head xs <= head (tail xs) = increasing' (tail xs) - | otherwise = False -``` -- 可以将其看做`if - else if - else if ... else`结构,最后的`otherwise`是最后的`else`也就是万能匹配,可有可无,如果没有`otherwise`且不满足前面所有条件,则会转入下一个模式进行匹配,所以守卫和模式是完全契合起来的。 - -### where关键字 - -在守卫中,可能出现多个条件使用了同一个变量,计算了同一个中间值的情况,如果在每个条件中计算一次就会有重复,而重复是天生就应该被优化的。这时就可以使用`where`,用于计算重复的部分。 -```haskell -bmiTell :: (RealFloat a) => a -> a -> String -bmiTell weight height - | bmi <= skinny = "You're underweight, you emo, you!" - | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!" - | bmi <= fat = "You're fat! Lose some weight, fatty!" - | otherwise = "You're a whale, congratulations!" - where bmi = weight / height ^ 2 - skinny = 18.5 - normal = 25.0 - fat = 30.0 -``` -- `where`中可以定义多个名字和函数,每个名字对守卫都是可见的,并且仅对本函数可见,不会污染全局和其他函数的名称空间。其中的名字都是一列垂直排开,这是语法规范。 -- `where`中也可以使用模式匹配。 -```haskell -where bmi = weight / height ^ 2 - (skinny, normal, fat) = (18.5, 25.0, 30.0) -``` -- `where`中可以定义辅助函数,其中又可以使用`where`,其中又可以定义辅助函数,可以多层嵌套。 - -### let关键字 - -`where` 绑定是在函数底部定义名字,对包括所有守卫在内的整个函数可见。`let`绑定则是个表达式,允许在任何位置定义局部变量,对不同的守卫不可见。正如Haskell中所有赋值结构一样,`let`绑定也可以使用模式匹配。 -- 格式为`let [binding] in [expressions]`,在`binding`中绑定的名字仅在`expressions`中可见。 -- `let`将绑定放在前面,`where`放在后面。区别在于`where`是一个语法结构,而`let`是一个表达式。所以和`if`表达式一样,可以放在任何表达式可以放的地方。`let`结构中`expressions`的值就是整个`let`表达式的值。 -- 比如用于定义局部函数: -```haskell ->>> [let square x = x * x in (square 1, square 2)] -[(1,4)] -``` -- 如果要在一行中绑定多个名字,如果要将多个名字排成一行可以用`;`隔开。 -```haskell ->>> let a = 100; b = 20 in a + b -120 -``` -- 但`let`绑定中也是可以使用模式匹配的,所以用元组匹配显然更好: -```haskell ->>> let (a, b) = (100, 20) in a + b -120 -``` -- 可以将`let`绑定放在列表生成式中单纯用于定义名字,而没有`in`,此时其中定义的名字对其后的条件和`|`前的表达式可见。如果是用`let`表达式作为条件则可以有`in`,那么就是一个普通的`let`表达式,其中名字不会对列表生成式`|`前的表达式和后续条件可见。 -```haskell -calcBmis :: (RealFloat a) => [(a, a)] -> [a] -calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0] -``` - -### case表达式 - -语法: -```haskell -case expression of pattern1 -> result1 - pattern2 -> result2 - pattern3 -> result3 - ... -``` -- 函数的模式匹配就是`case`表达式的语法糖而已, -- 比如`incresing`的模式匹配就等价于: -```haskell -increasing'''' :: Ord a => [a] -> Bool -increasing'''' xs = case xs of (x:y:ys) -> x <= y && increasing'''' (y:ys) - _ -> True -``` -- 函数参数模式匹配只能用于函数定义时,而`case`表达式可以用于任何地方。 - -究根结底,模式匹配、守卫、case表达式都是条件判断的语法糖,为了更方便地进行分支而产生的语法,在支持函数式编程的语言中,这些都是必不可少的糖,使用多层的`if-else`嵌套会显得很冗余。 - -## 递归 - -先来一个传统艺能: -- 斐波那契数列(指数复杂度,避免测试过大的参数): -```haskell -{- fibonacci sequence ->>> fib 10 -55 ->>> fib 10 -55 --} -fib :: (Integral a) => a -> Integer -fib 0 = 0 -fib 1 = 1 -fib n = fib(n-1) + fib(n-2) -``` -- 尾递归版本: -```haskell -{- ->>> fib' 10 -55 ->>> fib' 100 -354224848179261915075 ->>> fib' 300 -222232244629420445529739893461909967206666939096499764990979600 ->>> fib' (50 :: Integer) -12586269025 ->>> fib' (100 :: Int) -3736710778780434371 --} - -fibonacci :: Integral a => a -> Integer -> Integer -> Integer -fibonacci 0 a b = b -fibonacci n a b = fibonacci (n - 1) (a + b) a -fib' :: Integral a => a -> Integer -fib' n = fibonacci n 1 0 -``` - -因为Haskell是纯函数式编程语言,没有循环,所以要实现循环就只能通过递归,所以递归的重要性又上了一层,不再是仅用于递归性质的数据结构或者递归的表达式求解。最普通的列表集合的操作都需要通过递归来实现,这是函数式编程的特点,从命令式编程语言转换到函数式需要重点熟悉锻炼这一点。 -- 求最大值: -```haskell -maximum' :: (Ord a) => [a] -> a -maximum' [] = error "maximum a empty list" -maximum' [x] = x -maximum' (x:xs) = max x (maximum' xs) -``` -- `replciate`函数: -```haskell -replicate' :: (Ord t, Num t) => t -> a -> [a] -replicate' n x - | n <= 0 = [] - | otherwise = x:replicate' (n-1) x -``` -- 取列表前n个元素: -```haskell -take' :: (Ord a1, Num a1) => a1 -> [a2] -> [a2] -take' n _ - | n <= 0 = [] -take' _ [] = [] -take' n (x:xs) = x : take' (n-1) xs -``` -- 判断元素是否在列表中: -```haskell -elem' :: Eq t => t -> [t] -> Bool -elem' a [] = False -elem' a (x:xs) - | x == a = True - | otherwise = a `elem'` xs -``` -- 列表相关操作都可以通过递归实现,借助模式匹配实现起来都非常简单。 -- 快速排序:就一个字,简单得有点离谱。 -```haskell -{- quick sort ->>> quicksort [3, 1, 2, 4, 5, 7, 9, 100, -10] -[-10,1,2,3,4,5,7,9,100] ->>> quicksort "the quick brown fox jumps over the lazy dog's back" -" 'aabbccdeeefghhijkklmnoooopqrrssttuuvwxyz" --} -quicksort :: Ord a => [a] -> [a] -quicksort [] = [] -quicksort (x:xs) = - let smallerSorted = quicksort [a | a <- xs, a <= x] - biggerSorted = quicksort [a | a <- xs, a > x] - in smallerSorted ++ (x : biggerSorted) -``` - -递归的书写模式: -- 处理边界条件,如空列表、没有子节点的节点、0、负值等情况,单独定义为一个或多个模式。多个参数的话则需要考虑多种情况,并注意他们的顺序。 -- 定义一般处理逻辑,从一系列元素中取出一个,处理完后,将剩余的元素交给这个函数继续处理。 - -在命令式语言中,为了避免递归带来的栈消耗,能循环肯定是不递归的,递归常用在递归数据结构和特定问题处理中。但在纯函数式编程语言中,递归被用来替代循环。 - -## 高阶函数 - -高阶函数:函数可以作为参数、返回值、赋给另一个变量。 - -### 柯里化 - -在Haskell中,所有的多参数函数都支持柯里化,所以也可以说本质上Haskell的所有函数都只有一个参数,多参数的调用就是多个一参数函数的调用。 -- 比如`max`函数: -```haskell -{- curried functions ->>> :t max -max :: Ord a => a -> a -> a ->>> :t max 4 -max 4 :: (Ord a, Num a) => a -> a ->>> :t max4 -max4 :: (Ord a, Num a) => a -> a ->>> max4 5.0 -5.0 ->>> (max 4) 5 -5 --} -max4 :: (Ord a, Num a) => a -> a -max4 = max 4 -max4' :: (Ord a, Num a) => a -> a -max4' x = max 4 x -``` -- `max 4`将得到一个函数,`max4 max4'`从含义是等价的,并且Haskell的hlint会提示后者可以简写为前者。 -- 所以在实际上类型`a -> a -> a -> a`和`a -> (a -> (a -> a))`是等价的,从语法上来说这源于运算符`->`是右结合的。 -- 当然如果要固定第二个参数,那么还是需要`max4'' x = max x 4`这样的定义方法。 -- 中缀函数也可以柯里化,并且可以可以固定第一个或者第二个参数: -```haskell -{- infix functions ->>> :t divBy10 -divBy10 :: Double -> Double ->>> divBy10 100 -10.0 ->>> divX 100 -0.1 --} -divBy10 :: Double -> Double -divBy10 = (/10) -divX :: Double -> Double -divX = (10/) -``` -- 前缀函数也可以按照这个逻辑转为中缀之后固定第一个或者第二个参数。 -- 对于某些一元和二元运算符使用同一个符号的情况,比如`-`用作减号和负号,`(-4)`则表示值-4,而不是接受一个参数将参数减4的函数。属于例外,为了避免冲突的选择,要使用减号含义则可以使用`subtract`,负号含义和`negate`等价。 - -### 函数作为参数 - -类型声明中将函数的类型加上括号即可。 -- 实现标准库`zipWith`函数,传入接受两个参数得到结果的函数和两个列表,得到对两个列表对应值应用函数后结果的列表,不得不说类型推导确实强大。 -```haskell -{- function as arguments ->>> zipWith (+) [1, 2, 3] [4, 5, 6, 7] -[5,7,9] ->>> zipWith' (+) [1, 2, 3] [4, 5, 6, 7] -[5,7,9] --} -zipWith' :: (t1 -> t2 -> a) -> [t1] -> [t2] -> [a] -zipWith' _ [] _ = [] -zipWith' _ _ [] = [] -zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys -``` - -而至于函数作为返回值,其实默认就是柯里化的函数本身就已经将函数作为返回值了,也可以显式定义: -```haskell -f :: Num a => a -> a -> a -> a -f x = let tmp1 y = (let tmp2 z = x * y * z in tmp2) in tmp1 -``` -- 这样和`f' x y z = x * y * z`并没有任何区别,还会显得很呆。 - -### 常用高阶函数 - -map(映射)/reduce(规约)是最常用的高阶函数。前者将一个列表映射到另一个列表,后者将一个列表规约为一个值。 -- `map :: (a -> b) -> [a] -> [b]` 映射一个列表到另一个列表。 -- `filter :: (a -> Bool) -> [a] -> [a]` 筛选符合条件的元素到结果列表。 -- `map filter`完全其实可以用列表推导式来代替,或者说本身就是等价的,用什么并不重要,凭个人喜好就好。 -- `takeWhile :: (a -> Bool) -> [a] -> [a]` 按顺序取元素直到条件不满足。 -- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` 将两个列表的对应元素应用函数后得到新列表。 -- `flip :: (a -> b -> c) -> b -> a -> c` 接受一个二元函数,将两个参数翻转并返回新的二元函数。 - - -### lambda - -有高阶函数那肯定要有匿名函数了。Haskell用`\`来表示匿名函数,定义方法:`\args -> retval`,用的时候一般用括号将整个匿名函数括起来。 -```haskell ->>> zipWith (\x y -> x + y) [1, 2] [10, 100, 1] -[11,102] ->>> map (\x -> x ** x) [1, 2, 3, 4] -[1.0,4.0,27.0,256.0] -``` -同普通函数一样可以使用模式匹配,但是无法为匿名函数设置多个模式,所以在匿名函数中要慎用模式匹配。 - -使用匿名函数来实现`x * y * z`的柯里化会更容易理解一些,当然熟悉了默认柯里化之后,用`\x y z -> x * y * z`更简单直观。 -```haskell -f'' :: Num a => a -> a -> a -> a -f'' = \x -> \y -> \z -> x * y * z -``` -实现`flip`函数:使用匿名函数实现会更容易让人理解一些,表明其返回一个函数,但两者其实是等价的。 -```haskell -flip' :: (t1 -> t2 -> t3) -> t2 -> t1 -> t3 -flip' f = \x y -> f y x -flip'' :: (t1 -> t2 -> t3) -> t2 -> t1 -> t3 -flip'' f x y = f y x -``` - -### fold & scan - -折叠: -- 折叠,就是将一个列表规约为一个值: -```haskell ->>> :t foldl -foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b ->>> :t foldr -foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b -``` -- `foldl foldr`接受一个函数,一个初值和一个可折叠对象,对初值和起始元素调用函数,然后一次对结果和下一个值调用直接结束,得到结果。`foldl`从左到右,`foldr`从右到左。并且注意传入函数的参数对应关系是不同的,`foldl`第一个参数为初值或者中间结果,第二个参数是可折叠对象元素,而`foldr`是反过来的。当中间结果和可折叠对象元素类型不同时需要特别注意。 -```haskell ->>> foldl (-) 0 [1, 2, 3] --6 ->>> 0 - 1 - 2 - 3 --6 ->>> foldr (-) 0 [1, 2, 3] -2 ->>> 1 - (2 - (3 - 0)) -2 -``` -- `foldl1 foldr1`和`foldl foldr`类似,不过他们使用首或尾元素作为初值而不需要再传入初值。 -- 对空列表进行折叠会抛出运行时异常。 -- 有个小区别是`foldl`能用于无限列表(但这不是会无限循环吗?),`foldr`不能。经过实测都会进入无限循环?这一条是存在疑问的,应该避免将`fold`用于无限列表。 - -扫描: -- `scanl scanr`与`foldl foldr`类似,不同的是结果,`scanl scanr`的结果是一个列表,包括初始值和所有中间结果与最终结果。`scanl`是从左往右添加结果,`scanr`则是从右向左。 -- `scanl1 scanr1`用首尾元素作为初始值,同理。 -```haskell ->>> :t scanl -scanl :: (b -> a -> b) -> b -> [a] -> [b] ->>> :t scanr -scanr :: (a -> b -> b) -> b -> [a] -> [b] ->>> scanl (-) 0 [1, 2, 3, 4] -[0,-1,-3,-6,-10] ->>> scanr (-) 0 [1, 2, 3, 4] -[-2,3,-1,4,0] ->>> :t scanl1 -scanl1 :: (a -> a -> a) -> [a] -> [a] ->>> scanl1 (-) [1, 2, 3, 4] -[1,-1,-4,-8] ->>> scanr1 (-) [1, 2, 3, 4] -[-2,3,-1,4] -``` - -折叠和扫描在一定程度上可以用来替代递归在列表上的使用。 - -### $函数调用符 - -`$`被称作函数调用符: -- 定义:接受一个函数和一个参数并返回使用这个函数调用参数的结果,不改变具体逻辑,但使用`$`后,函数调用的优先级被改变了。 -```haskell -($) :: (a -> b) -> a -> b -f $ x = f x -``` -- 用空格调用的函数调用是左结合的`f x y z`与`((f x) y) z`等价。 -- `$`是中缀函数,右结合,最低优先级,其他表达式都会得到优先计算,然后才从右向左执行使用`$`调用的函数。 -- `f $ 1 + 1`表示`f (1 + 1)`,作用是降低了函数调用的优先级,减少括号的使用。 -- 因为是右结合,并且`$`只接受一个函数参数,所以`f x y z`不能写作`f $ x $ y $ z`(应该在当`x y z`是表达式或者函数调用时才用`$`,不然是没有必要的),因为右结合含义就变成了`f $ (x $ (y $ z))`,这明显不是想要的语义。多个参数可以使用括号指定结合性`((f $ x) $ y) $ z`但这样本质上并没有减少括号,所以对于多参数的函数,还不如`f (x) (y) (z)`,当然最后一个参数永远是可以用的`f (x) (y) $ z`。 -- `$ x`可以将数据变成函数,接受一个函数,返回值是将这个数据`x`用于传入的函数后得到的结果。 -```haskell -{- ($) operator ->>> :t ($) -($) :: (a -> b) -> a -> b ->>> fn $ 1 + 2 -9 ->>> :t ($ 1) -($ 1) :: Num a => (a -> b) -> b --} -fn :: Num a => a -> a -fn x = x * x -``` -- 虽然有点魔法的味道,又改优先级又改结合性,但在一定程度上使用可以减少括号,让程序更清晰易读。 -- 注意适度,不要滥用,请在充分理解之后再使用。 - -### 函数复合(Function Composition) - -或者叫做函数组合。在数学中,复合函数的定义是$(f \circ g)(x) = f(g(x))$,即将函数$g(x)$的值作为$f(x)$的自变量,既然函数式编程中的函数的含义是数学中的函数而不是一般命令式编程中表是一个计算过程的函数。那么理所应当要支持复合函数(或者叫做函数组合)了,$f(g(x))$的含义就是先调用$g(x)$再对结果调用$f(x)$。 -- Haskell中使用`.`运算符定义复合函数。 -- 定义:非常直白,中缀,接受两个单参数的函数,先调用后者,再调用前者。 -```haskell -(.) :: (b -> c) -> (a -> b) -> a -> c -f . g = \x -> f (g x) -``` -- 很显然定义函数复合时内层函数`g`的返回值类型必须要和外层函数`f`的参数类型一致。 -- `.`优先级低于函数调用,需要将复合函数括起来,配合`$`可以不用括号`f . g $ (expression)`。 -```haskell -{- function composition ->>> map (f' . g') [1..20] -[8,24,48,80,120,168,224,288,360,440,528,624,728,840,960,1088,1224,1368,1520,1680] ->>> map h [1..20] -[8,24,48,80,120,168,224,288,360,440,528,624,728,840,960,1088,1224,1368,1520,1680] ->>> map (\x -> f' (g' x)) [1..20] -[8,24,48,80,120,168,224,288,360,440,528,624,728,840,960,1088,1224,1368,1520,1680] --} - --- f(x) = (2*x + 1) ^ 2 - 1 -g' :: Num a => a -> a -g' x = 2 * x + 1 -f' :: Num a => a -> a -f' x = x ^ 2 - 1 - -h :: Num a => a -> a -h x = (2 * x + 1) ^ 2 - 1 -``` -- 实际使用可以用`f . g`这样用,或者直接写成匿名函数`\x -> f (g x)`也很简单和清晰,毕竟是等价的。 -- 这里的函数都只包含一个参数,如果是多个参数函数,可以使用不全调用,传入部分参数只剩下最后一个参数,便可以用于复合。 -- 比如`sum (replicate 5 (max 6.7 8.9))`可以写作`(sum . replicate 5 . max 6.7) 8.9`(看起来很怪,这样组合意义不明,仅做演示)。 -- 使用函数复合配合`$`可以进一步去掉括号,刚开始可能会有点晦涩,但充分理解之后用起来非常舒服。 -- 其中一个使用理由就是定义Point free style(Pointless style)的函数,比如: -```haskell -{- point free style function ->>> map func [100..120.0] -[-1,-1,0,1,2,1,0,-1,0,1,2,1,0,-1,0,1,2,1,0,-1,-1] ->>> map func' [100..120.0] -[-1,-1,0,1,2,1,0,-1,0,1,2,1,0,-1,0,1,2,1,0,-1,-1] --} -func :: (RealFrac a, Integral b, Floating a) => a -> b -func x = ceiling (negate (tan (cos (max 50 x)))) -func' :: Double -> Integer -func' = ceiling . negate . tan . cos . max 50 -``` -- Point free style中函数定义去掉了参数,而是已知的多个简单函数组合形成一个复杂函数。 -- 思考如何写出Point free style的函数时,思考的会是函数的组合方式,而不是数据的传递方式。 -- 当然如果函数太复杂,硬要写成Point free可能会适得其反,这时候更好的方法可能是用`let`语句给中间结果绑定名字,或者再将问题分割成更小的问题再组合到一起。 -- 编码风格是个人选择,Haskell提供了灵活的语法,相信实践时代码的迭代优化过程会很有趣,但如果是项目开发为了风格统一估计也要付出更多代价。 -- 趣学指南中给了一个简单例子:实用、好看又清晰,`$`的使用使得所有函数经过复合之后最后再进行调用。 -```haskell -oddSquareSum :: Integer -oddSquareSum = sum . takeWhile (<10000) . filter odd . map (^2) $ [1..] -``` -- 阅读经过复合的函数时应该从里到外去理解。 - -## 模块 - -Haskell中模块就是一组相关函数、类型、类型类的组合,Haskell进程本质就是从主模块中引用其他模块调用其中的函数执行操作。如果一个模块足够独立,里面的函数就可以被不同进程共用。 - -Haskell标准库就是一组模块,每个模块都有一组功能相近或相关的函数和类型。比如处理List的、处理并发的、处理复数的等。目前谈及的所有数据结构、类型、类型类都是`Prelude`模块一部分,默认自动引入。 - -### 引入模块 - -引入模块的语句必须在函数定义前,一般都是至于文件顶部。引入后引入模块中所有函数都进入全局命名空间: -```haskell -import module1 module2 module3 -``` -如果只需要用到某一个模块的两个函数,则可以只引入这两个函数: -```haskell -import module1 (func1, func2) -``` -如果想引入某个模块的全部函数和类型但除了其中某个函数可以使用`hiding`,不能和引入部分符号同时使用: -```haskell -import module1 hiding (func) -``` -`Prelude`模块虽然已经自动引入,但仍可以手动只引入其中部分符号或者屏蔽其中部分符号: -```haskell -import Prelude hiding (max) -``` - -某些模块中具有同名的函数,为了避免名称冲突,可以使用`import qualified`,这样使用时必须使用名称`modulename.func`。 -```haskell -import qualified modulename -``` -给引入的模块定义别名,模块名必须大写字母开头,同样可以隐藏或部分引入: -```haskell -import qualified Data.Map as M hiding (map) -``` -在ghci中引入模块: -``` -:m module1 module2 module3 ... -``` - -深入学习可查阅[Haskell标准库](https://downloads.haskell.org/~ghc/latest/docs/html/libraries/)。 - -检索标准库或者第三方库中的Haskell函数、类型可以上[Hoogle](https://hoogle.haskell.org/),允许通过函数名模块名甚至近似的函数类型签名并允许指定搜索范围或者库作者来检索Stackage上的Haskell函数库。并且能够直接跳转到库文档或者页面,十分方便。 - -### 常用库 - -调用模块中函数类型时使用`module.func`,同样是`.`号,中间不能有空格,区分于函数复合。 - -`Data.List`: -- List常用操作,`map fileter`便是出自这个库,太过常用所以`Prelude`中将其导出了。 -- `intersperse :: a -> [a] -> [a]` 将一个元素穿插到一个列表的每两个元素间。 -- `intercalate :: [a] -> [[a]] -> [a]` 将一个列表插入到一个列表中的所有列表间。 -- `transpose :: [[a]] -> [[a]]` 翻转一个二维列表的行和列。如果用来存储矩阵,那就表示转置。 -- `foldl' :: Foldable t => (b -> a -> b) -> b -> t a -> b` `foldl`的严格(restrict)版本,`foldl`是惰性(lazy)的,不会立即求值,而是做一个”在必要时会取得所需的结果”的承诺。每过一遍累加器,这一行为就重复一次。在列表很大时,这堆承诺可能会塞满堆栈造成栈溢出,此时应改用严格版本,严格版本会直接计算出中间值并继续执行下去。 -- `foldl1' :: (a -> a -> a) -> [a] -> a` `foldl1`的严格版本。 -- `concat :: Foldable t => t [a] -> [a]` 连接一组列表。 -- `concatMap :: Foldable t => (a -> [b]) -> t a -> [b]` 与先`map`再`concat`等价。 -- `and :: Foldable t => t Bool -> Bool` 对一组元素求与。 -- `or :: Foldable t => t Bool -> Bool` 对一组元素求或。 -- `any :: Foldable t => (a -> Bool) -> t a -> Bool` 判断是否存在满足条件的元素。 -- `all :: Foldable t => (a -> Bool) -> t a -> Bool` 判断是否所有元素都满足条件。 -- `iterate :: (a -> a) -> a -> [a]` 用一个值调用一个函数,并用结果继续调用函数,产生一个无限的列表。 -- `splitAt :: Int -> [a] -> ([a], [a])` 将列表从特定位置断开,返回前后两个列表的元组。 -- `takeWhile :: (a -> Bool) -> [a] -> [a]` 取元素直到条件不满足。 -- `dropWhile :: (a -> Bool) -> [a] -> [a]` 舍弃满足条件的元素直到首个不满足的元素,得到剩余元素的列表,`takeWhile`的补集。 -- `span :: (a -> Bool) -> [a] -> ([a], [a])` 得到`takeWhile dropWhile`的列表构成的元组。 -- `break :: (a -> Bool) -> [a] -> ([a], [a])` 和`span`条件相反,直到首次满足时断开,等价于`break p`等价于`span (not . p)`。 -- `sort :: Ord a => [a] -> [a]` 排序,升序排列。 -- `group :: Eq a => [a] -> [[a]]` 归类相邻相等的元素。 -- `inits :: [a] -> [[a]] tails :: [a] -> [[a]]` 类似于`init tail`,只是会递归调用直到空,得到子列表构成的列表。 -- `isPrefixOf :: Eq a => [a] -> [a] -> Bool` 检查是否是前缀,前者是子列表,常以中缀形式使用。 -- `isSuffixOf :: Eq a => [a] -> [a] -> Bool` 检查是否是后缀。 -- `elem :: (Foldable t, Eq a) => a -> t a -> Bool` -- `notElem :: (Foldable t, Eq a) => a -> t a -> Bool` -- `partition :: (a -> Bool) -> [a] -> ([a], [a])` 按条件分为满足和不满足的元素构成列表的元组。搜索整个列表,区别于`span break`。 -- `find :: Foldable t => (a -> Bool) -> t a -> Maybe a` 查找首个满足的元素,结果是一个`Maybe`,其值是`Just something`或者`Nothing`(单个元素或者空值)。 -- `elemIndex :: Eq a => a -> [a] -> Maybe Int` 查找元素并返回下标,`Just index`或者`Nothing`。 -- `elemIndices :: Eq a => a -> [a] -> [Int]` 查找元素返回所有下标。 -- `findIndex :: (a -> Bool) -> [a] -> Maybe Int` 按条件查找首个满足元素下标。 -- `findIndices :: (a -> Bool) -> [a] -> [Int]` 按条件查找所有满足元素,得到所有下标。 -- `zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]` `zip`的更多列表版本,同理`zip4 zip5 ... zip7`。 -- `zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]` 同理`zipWith4 zipWith5 ...`。 -- `lines :: String -> [String]` 按行(依据字符`\n`)切分字符串。 -- `unlines :: [String] -> String` `lines`反函数,拼接多个字符串,每个字符串末尾补`\n`。 -- `nub :: Eq a => [a] -> [a]` 元素去重,nub意思是一小块一部分,用在这里感觉有点老掉牙不确切。 -- `delete :: Eq a => a -> [a] -> [a]` 删除第一次出现的元素。 -- `(\\) :: Eq a => [a] -> [a] -> [a]` `\`运算符,计算差集,从前者中减去后者,使用`\`需要转义,所以代码中都是`\\`。 -- `union :: Eq a => [a] -> [a] -> [a]` 取并集。 -- `intersect :: Eq a => [a] -> [a] -> [a]` 取交集。 -- `insert :: Ord a => a -> [a] -> [a]` 插入元素到可排序列表的首个大于等于它的元素前,如果原先是升序排列的,那么插入后仍是。 -- 对于更为常用的`length take drop splitAt !! replciate`之类函数的参数类型都是`Int`,按道理来说提供`Integral Num`会更好,但是处于历史原因修改会引起兼容性问题。所以提供了`genericLength genericTake genericDrop genericSplitAt genericIndex genericReplicate`函数提供更通用的类型。 -```haskell -length :: Foldable t => t a -> Int -take :: Int -> [a] -> [a] -drop :: Int -> [a] -> [a] -splitAt :: Int -> [a] -> ([a], [a]) -(!!) :: [a] -> Int -> a -replicate :: Int -> a -> [a] -genericLength :: Num i => [a] -> i -- for scene like average -genericTake :: Integral i => i -> [a] -> [a] -genericDrop :: Integral i => i -> [a] -> [a] -genericSplitAt :: Integral i => i -> [a] -> ([a], [a]) -genericIndex :: Integral i => [a] -> i -> a -genericReplicate :: Integral i => i -> a -> [a] -``` -- `sort insert maximum minimum`都有各自更通用的版本,可以传入比较函数。 -```haskell -sort :: Ord a => [a] -> [a] -insert :: Ord a => a -> [a] -> [a] -minimum :: (Foldable t, Ord a) => t a -> a -maximum :: (Foldable t, Ord a) => t a -> a -sortBy :: (a -> a -> Ordering) -> [a] -> [a] -insertBy :: (a -> a -> Ordering) -> a -> [a] -> [a] -minimumBy :: Foldable t => (a -> a -> Ordering) -> t a -> a -maximumBy :: Foldable t => (a -> a -> Ordering) -> t a -> a -``` -- `nub delete union intersect group`也有通用版本,就是后面加上`By`,他们可以传入一个函数用以替代`(==)`来测试相等。比如`group`等价于`groupBy (==)`。 -```haskell -nub :: Eq a => [a] -> [a] -delete :: Eq a => a -> [a] -> [a] -union :: Eq a => [a] -> [a] -> [a] -intersect :: Eq a => [a] -> [a] -> [a] -group :: Eq a => [a] -> [[a]] -nubBy :: (a -> a -> Bool) -> [a] -> [a] -deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] -unionBy :: (a -> a -> Bool) -> [a] -> [a] -> [a] -intersectBy :: (a -> a -> Bool) -> [a] -> [a] -> [a] -groupBy :: (a -> a -> Bool) -> [a] -> [[a]] -``` -- `Data.Function`模块提供了`on`函数,可以方便地定义这种比较函数: -```haskell -on :: (b -> b -> c) -> (a -> b) -> a -> a -> c -f `on` g = \x y -> f (g x) (g y) -``` -- `on`就相当于对两个自变量的函数做一个复合:$(f\circ g)(x, y) = f(g(x), g(y))$。比如``compare `on` length``用于按照列表长度比较,``(==) `on` (>0)``用于按照是否同为正数判等,非常地灵活。 -```haskell ->>> groupBy ((==) `on` (>0)) [-1, -2, 0, 1, 22, 10, -100] -[[-1,-2,0],[1,22,10],[-100]] ->>> sortBy (compare `on` length) $ reverse [[1..x] | x <- [0..5]] -[[],[1],[1,2],[1,2,3],[1,2,3,4],[1,2,3,4,5]] -``` -- 通常与`By`结尾函数打交道,如果判断相等性,常用``(==) `on` something``,若判断大小,常用``compare `on` something``。 - -`Data.Char`: -- 一组用于处理字符的函数,字符串本质是函数,所以在处理字符串的`filter map`时会比较常用到。 -- 类型都是`Char -> Bool`。 -- `isControl` 判断一个字符是否是控制字符。 -- `isSpace` 判断一个字符是否是空格字符,包括空格,tab,换行符等. -- `isLower` 判断一个字符是否为小写. -- `isUper` 判断一个字符是否为大写。 -- `isAlpha` 判断一个字符是否为字母. -- `isAlphaNum` 判断一个字符是否为字母或数字. -- `isPrint` 判断一个字符是否是可打印的. -- `isDigit` 判断一个字符是否为数字. -- `isOctDigit` 判断一个字符是否为八进制数字. -- `isHexDigit` 判断一个字符是否为十六进制数字. -- `isLetter` 判断一个字符是否为字母. -- `isMark` 判断是否为 unicode 注音字符,你如果是法国人就会经常用到的. -- `isNumber` 判断一个字符是否为数字. -- `isPunctuation` 判断一个字符是否为标点符号. -- `isSymbol` 判断一个字符是否为货币符号. -- `isSeperater` 判断一个字符是否为 unicode 空格或分隔符. -- `isAscii` 判断一个字符是否在 unicode 字母表的前 128 位。 -- `isLatin1` 判断一个字符是否在 unicode 字母表的前 256 位. -- `isAsciiUpper` 判断一个字符是否为大写的 ascii 字符. -- `isAsciiLower` 判断一个字符是否为小写的 ascii 字符. -- `GeneralCategory` 是一个类型类,同时是一个枚举用来表示字符的分类,共有31个分类。 -```haskell ->>> map generalCategory "\r\nab\t A?[%#@!" -[Control,Control,LowercaseLetter,LowercaseLetter,Control,Space,UppercaseLetter,OtherPunctuation,OpenPunctuation,OtherPunctuation,OtherPunctuation,OtherPunctuation,OtherPunctuation] ->>> [(minBound :: GeneralCategory) .. (maxBound :: GeneralCategory)] -[UppercaseLetter,LowercaseLetter,TitlecaseLetter,ModifierLetter,OtherLetter,NonSpacingMark,SpacingCombiningMark,EnclosingMark,DecimalNumber,LetterNumber,OtherNumber,ConnectorPunctuation,DashPunctuation,OpenPunctuation,ClosePunctuation,InitialQuote,FinalQuote,OtherPunctuation,MathSymbol,CurrencySymbol,ModifierSymbol,OtherSymbol,Space,LineSeparator,ParagraphSeparator,Control,Format,Surrogate,PrivateUse,NotAssigned] -``` -- `toUpper` 将一个字符转为大写字母,若该字符不是小写字母,就按原值返回。 -- `toLower` 将一个字符转为小写字母,若该字符不是大写字母,就按原值返回。 -- `toTitle` 将一个字符转为 title-case,对大多数字元而言,title-case 就是大写。 -- `digitToInt` 将一个字符转为 Int 值,而这一字符必须得在 '1'..'9','a'..'f'或'A'..'F' 的范围之内。 -- `ord :: Char -> Int` 字符转Unicode码点。 -- `chr :: Int -> Char` Unicode码点转字符。 -- 一个原始的加密算法,仅仅偏移字符串: -```haskell -encode :: Int -> String -> String -encode shift = map (chr . (+shift) . ord) - -decode :: Int -> String -> String -decode shift = encode (-shift) -``` - -`Data.Map`: -- 关联列表或者叫字典,元素是键值对,没有特定顺序。如果要实现类似功能,可以使用键值二元组的List。 -- 其中有部分和`Data.List`中重名的函数,注意`import qualified Data.Map as M`。 -- `Data.Map.fromList :: Ord k => [(k, a)] -> Map k a` 从列表创建字典,对关键字去重。 -```haskell ->>> :t M.fromList -M.fromList :: Ord k => [(k, a)] -> Map k a ->>> M.fromList [(1,'a'), (1, 'z'), (2, 'B')] -fromList [(1,'z'),(2,'B')] -``` -- 对于普通列表,只需要元素能够判等,而对于字典,需要可排序,实现使用平衡树。处理键值对时,如果键属于`Ord`类型类,就应该尽量用`Data.Map`。 -- `Data.Map.empty :: Map k a` 创建空字典。 -- `Data.Map.insert :: Ord k => k -> a -> Map k a -> Map k a` 插入元素。 -- 利用`empty insert`创建自己的`fromList`,(类型推导不是万能的,需要自己明确类型)。 -```haskell -fromList' :: Ord k => [(k, a)] -> M.Map k a -fromList' = foldr (\(k, v) acc -> M.insert k v acc) M.empty - -fromList'' :: Ord k => [(k, a)] -> M.Map k a -fromList'' = foldl (\acc (k, v) -> M.insert k v acc) M.empty -``` -- `Data.Map.null :: Map k a -> Bool` 判空。 -- `Data.Map.size :: Map k a -> Int` 大小。 -- `Data.Map.singleton :: k -> a -> Map k a` 构建单元素字典。 -- `Data.Map.lookup :: Ord k => k -> Map k a -> Maybe a` 查找键对应值。 -- `Data.Map.member :: Ord k => k -> Map k a -> Bool` 判断键是否存在。 -- `Data.Map.map :: (a -> b) -> Map k a -> Map k b` 字典版本`map`。 -- `Data.Map.filter :: (a -> Bool) -> Map k a -> Map k a` 字典版本的`filter`。 -- `Data.Map.toList :: Map k a -> [(k, a)]` 字典到列表。 -- `Data.Map.keys :: Map k a -> [k]` 键列表,等价于`map fst . Data.Map.toList`。 -- `Data.Map.elems :: Map k a -> [a]` 值列表,等价于`map snd . Data.Map.toList`。 -- `Data.Map.fromListWith :: Ord k => (a -> a -> a) -> [(k, a)] -> Map k a` 和`fromList`很像,但不会直接忽略重复键,而是交给一个函数处理重复键的值。可以组合多个值、选最大值、累加到一起等,由传入函数决定。 -```haskell ->>> M.fromListWith (\v1 v2 -> v1 ++ ", " ++ v2) [(1, "hello"), (1, "world")] -fromList [(1,"world, hello")] ->>> M.fromListWith (+) [(1, 2), (1, 3)] -fromList [(1,5)] -``` -- `Data.Map.insertWith :: Ord k => (a -> a -> a) -> k -> a -> Map k a -> Map k a` 插入元素,重复键交给传入函数处理。 -- 更多函数查看[文档](https://downloads.haskell.org/~ghc/latest/docs/html/libraries/containers-0.6.5.1/Data-Map.html)。 - -`Data.Set`: -- `import qualified Data.Set as Set`。 -- 常用函数: -```haskell -Data.Set.fromList :: Ord a => [a] -> Set a -Data.Set.difference :: Ord a => Set a -> Set a -> Set a -Data.Set.null :: Set a -> Bool -Data.Set.size :: Set a -> Int -Data.Set.member :: Ord a => a -> Set a -> Bool -Data.Set.empty :: Set a -Data.Set.singleton :: a -> Set a -Data.Set.insert :: Ord a => a -> Set a -> Set a -Data.Set.delete :: Ord a => a -> Set a -> Set a -Data.Set.map :: Ord b => (a -> b) -> Set a -> Set b -Data.Set.filter :: (a -> Bool) -> Set a -> Set a -``` -- 其中`difference`是求差集: -```haskell ->>> Set.difference (Set.fromList [1, 2, 3]) $ Set.fromList [2, 100] -fromList [1,3] -``` -- 集合的一个常见用途时,列表转集合再转列表去重,要求元素是`Ord`,比`nub`更快,但不保留列表中元素的顺序。 -```haskell -setNub :: Ord a => [a] -> [a] -setNub = Set.toList . Set.fromList -``` - -### 编写自己的模块 - -单个文件: -```haskell -module Geometry -( sphereVolume -, sphereArea -, cubeVolume -, cubeArea -, cuboidVolume -, cuboidArea -) where - - -sphereVolume :: Floating a => a -> a -sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3) - -sphereArea :: Floating a => a -> a -sphereArea radius = 4 * pi * (radius ^ 2) - -cubeVolume :: Floating a => a -> a -cubeVolume side = cuboidVolume side side side - -cubeArea :: Floating a => a -> a -cubeArea side = cuboidArea side side side - -cuboidVolume :: Floating a => a -> a -> a -> a -cuboidVolume a b c = a * b * c - -cuboidArea :: Floating a => a -> a -> a -> a -cuboidArea a b c = (rectangleArea a b + rectangleArea b c + rectangleArea a c) * 2 - -rectangleArea :: Floating a => a -> a -> a -rectangleArea a b = a * b -``` -- 要导出的函数放到`()`中,没有导出的是模块内部函数。 -- 便可以在同级目录下的`.hs`中进行导入,使用由导入的模块导出的函数。 - -多个文件: -- 新建目录`Geometry`,并在其中添加文件`Sphere.hs`: -```haskell -module Geometry.Sphere -( volume -, area -) where - -volume :: Floating a => a -> a -volume radius = (4.0 / 3.0) * pi * (radius ^ 3) - -area :: Floating a => a -> a -area radius = 4 * pi * (radius ^ 2) -``` -- 在`Geometry`同级目录下的`.hs`文件中可以引入。 - -更多细节待挖掘。 - - -## 定义类型和类型类 - -### 定义新类型 - -使用`data`关键字,标准库中`Bool`类型的定义: -```haskell -data Bool = False | True -``` -语法: -```haskell -data NameOfType = ValueConstructor1 TypesOfParams1 | ... deriving (Typeclass1, Typeclass2, ...) -``` -- `=`右侧称之为**值构造器**(Value Constructor),其中明确了类型所有可能的值,`|`读作或,类型和值构造器中的m名称都必须首字母大写。 -- 值构造器也是函数,可以有参数,按照参数调用某个值构造器就会返回一个类型实例,和普通函数的区别就是首字母是否大写。 -- 类型名称可以和某个值构造器相同,在一个类型只有一个值构造器时很常见。 -- 使用`deriving`从其他类型类派生。 -- 例子: -```haskell -data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) -``` -- 则调用`Circle f1 f2 f3`或者`Rectangle f1 f2 f3 f4`会得到一个新的`Shape`,`Circle Rectangle`不是类型,只是一个函数,他们的返回类型都是`Shape`。对于`Bool`,`True False`没有参数,所以不需要传入参数`True False`就是`Bool`的不同取值。 -- 需要一个类型能够在控制台输出为字符串,则需要派生自`Show`类型类。 -- 导出类型和构造器:在要导出的类型后加`()`,其中加入要导出的值构造器,使用`(..)`可以导出全部值构造器。 -```haskell -module Shapes -( Shape(Circle, Rectangle) -) where -``` -- 值构造器也只是函数,如果不导出,只是拒绝外部使用这些值构造器而已,仍然可以提供其他函数用于构造类型,比如`Data.Map.fromList`这种,返回一个`Data.Map`。 -- 类型的值构造器可以用于模式匹配,还可以嵌套匹配。 - -### Record Syntax - -仅仅使用上面的值构造器的话,每个类型的成员都没有名字,如果要获取就必须通过模式匹配定义类似这样的函数: -```haskell -{- ->>> kim = Person "kim" "Possible" 20 160 "Call me later!" ->>> firstName kim ->>> lastName kim ->>> age kim ->>> height kim -"kim" -"Possible" -20 -160.0 --} -data Person = Person String String Int Float String deriving (Show) -firstName :: Person -> String -firstName (Person firstname _ _ _ _) = firstname -lastName :: Person -> String -lastName (Person _ lastname _ _ _ ) = lastname -age :: Person -> Int -age (Person _ _ age _ _ ) = age -height :: Person -> Float -height (Person _ _ _ height _) = height -phoneNumber :: Person -> String -phoneNumber (Person _ _ _ _ number) = number -``` -有用,但非常无趣,所以有了Record 语法: -```haskell -{- Record Syntax ->>> kim = Person' "kim" "Possible" 20 160 "Call me later!" ->>> firstName' kim ->>> lastName' kim ->>> age' kim ->>> height' kim -"kim" -"Possible" -20 -160.0 ->>> :t firstName' -firstName' :: Person' -> String ->>> kim -Person' {firstName' = "kim", lastName' = "Possible", age' = 20, height' = 160.0, phoneNumber' = "Call me later!"} --} - -data Person' = Person' { - firstName' :: String, - lastName' :: String, - age' :: Int, - height' :: Float, - phoneNumber' :: String -} deriving(Show) -``` -- 加了`{}`,写出了项名字,跟上类型标记,用`,`分隔。通过Record语法就会自动生成这些函数,不能再定义同名函数。 -- Record语法调用`show`得到字符串是不同的,信息会更详细。 -- 如果是定义简单类型,可能不需要Recrod语法,如果要定义复杂类型,一个类型有多个项且不易区分,则应该使用Record语法,在其他语言中一般对象的项都要给名称。 - -### 类型参数 - -在类型后加上类型参数可以实现泛型的功能,比如`Map k a`,键和值的类型是类型的一部分。是对应于C++模板、java泛型之类的语法。比如`Maybe`: -```haskell -data Maybe a = Nothing | Just a -``` -- 有了类型参数`a`后,`Maybe`就不再是类型,`Maybe a`整体才是一个类型,`Maybe`则称为**类型构造器**:传入类型参数就可以得到类型,`Nothing`和`Just`是它的值构造器。 -- 前面接触到的列表类型,`[]`其实就是列表的类型构造器,只是提供了语法糖。 -- `Nothing :: Maybe a`是类型是多态的,`[] :: [a]`空列表也是多态的,可以被用于任何类型参数的`Maybe`或列表运算上。 -- 类型参数一般用在不关心一个项具体的值的地方,比如`Map k a`只需要`k`属于`Ord`类型类就行,不关心键和值的具体类型和具体值,如果不是像容器这样的通用数据结构,一般不会使用类型参数。同模板和泛型一样,要能够有多个类型能够提取出公共的逻辑才比较适合使用类形参数,如果定义的方法都是针对某一种数据类型的,那么无法定义类型参数,定义类型参数也没有意义。 -- 函数定义时类型参数可以加约束,但Haskell中有一个比较严格的**约定**,在`data`声明的类型参数中不要添加类型约束。 - - 注意是编程约定而不是语法规定(要启用这个语法需要启用在文件前添加`{-# LANGUAGE DatatypeContexts #-}`,并且目前已经废弃但未移除,新代码中不应该再使用,并提供了[ExistentialQuantification](https://wiki.haskell.org/Existential_type)作为替代),如果在类型中添加了类型参数,所有使用到该类型的地方都必须添加约束。为了避免函数声明中出现过多无所谓的类型约束,约定为不使用。那么类型又需要约束该怎么办呢?答案就是只在需要关心该约束的函数中添加约束(最典型的就是值构造器或者类似作用的函数),比如`Data.Map.fromList :: Ord k => [(k, a)] -> Map k a`,构造时添加了约束,那么得到的`Map`就一定是满足约束的。像`Data.Map.toList :: Map k a -> [(k, a)]`这种方法就完全不需要关心约束。 - - 又比如构造可以不加约束,但某些方法只有在某种约束下才能工作,那么就只需要那一部分方法添加约束,调用这些方法时编译器自然会检查类型参数是否满足了约束,不满足则会直接报错。 - - 甚至可以为不同约束的类型参数编写多组不同约束不同名称的方法,使用时根据构造时传入的类型参数选择使用哪一组,而通用的不关心约束的方法又可以用于所有类型,非常灵活,编译器的类型检查可以保证了通过了编译就不会发生类型不匹配之类的错误。 -- **注意**:区分类型构造器和值构造器,类型声明中,左边是类型构造器,右边是值构造器,前者得到类型,后者得到该类型实例。 - -### 派生标准类型类 - -当从`Eq Ord Enum Bounded Show Read`这几个常见类型类派生时,只需要加上`deriving`关键字,Haskell就会自动为这些类型加上这些行为。 -- 声明了`deriving(Eq)`时,就可以使用`== /=`来判断实例是否相等。判断依据是先判断其值构造器是否一致,再用`==`检查其中的所有数据是否一致(数据的类型必须都是`Eq`的实例)。 -- 属于`Ord`的类型比较时会先判断值构造器是否一致(按照顺序后面比前面的大),再判断他们的参数,并且参数类型需要都是`Ord`的实例。函数不是`Ord`的实例,所以`Just (*2) < Just (*3)`这种比较会报错。 -```haskell -{- Ord typeclass ->>> Nothing < Just (-100) -True ->>> Just 2 < Just 3 -True ->>> Just 100 `compare` Just 50 -GT --} -``` -- `Read Show`同样,只要成员都实现了`Read Show`就可以直接使用,`read`时需要添加类型注释注明想要得到的类型,否则Haskell不知道该如何转换。如果将`read`结果直接参与计算,那么也可以不注明类型。 -```haskell -{- derived type behaviors ->>> kim = Person {name = "Kim", age = 18} ->>> mygirl = Person "Kim" 18 ->>> kim == mygirl -True ->>> show kim -"Person {name = \"Kim\", age = 18}" ->>> mygirl -Person {name = "Kim", age = 18} ->>> read "Person {name = \"Kim\", age = 18}" :: Person -Person {name = "Kim", age = 18} ->>> kim > read "Person {name = \"catholly\", age = 15}" -False --} -data Person = Person { - name :: String, - age :: Int -} deriving(Eq, Ord, Read, Show) -``` -- 如果所有值构造器都没有参数,每个值构造器都有前置和后继,可以让其成为`Enum`的成员,并且可以使用Range。如果每个东西都有可能的最大值和最小值,可以成为`Bouned`类型类的成员。 -```haskell -{- Enum ->>> Monday < Sunday -True ->>> [minBound .. maxBound] :: [Day] -[Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday] ->>> map succ [Monday .. Saturday] -[Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday] ->>> map pred [Tuesday .. Sunday] -[Monday,Tuesday,Wednesday,Thursday,Friday,Saturday] ->>> succ Sunday -succ{Day}: tried to take `succ' of last tag in enumeration ->>> pred Monday -pred{Day}: tried to take `pred' of first tag in enumeration ->>> [Tuesday .. Sunday] == map succ [Monday .. Saturday] -True --} -data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving(Eq, Ord, Show, Read, Enum, Bounded) -``` -- 注意不能对最后一个枚举项求后继,不能对第一个枚举项求前置。 - -### 类型别名 - -类型别名不创建新类型,仅提供一个类型别名,使用`type`关键字,可以用在所有地方,类型声明、类型注释、类型别名声明中。 -```haskell -type String = [Char] -``` -使用类型别名可以让类型声明更易读,类型别名也可以有类型参数: -```haskell -type AssocList k v = [(k, v)] -``` -此时别名`AssocList`是一个类型构造器,加上两个类型参数之后才是类型。 - -类型构造器也可以不全调用,得到新的类型构造器,但是下面的代码后者在本地并没有通过编译(`? The type synonym ‘AssocList’ should have 2 arguments, but has been given 1`),疑问尚存!类型构造器和值构造器或普通函数不是一个概念,只是有类似之处,不要混为一谈。 -```haskell -type IntMap v = AssocList Int v -type IntMap' = AssocList Int -``` - -一个很有用的类型是[`Either`](https://hackage.haskell.org/package/base-4.14.3.0/docs/Data-Either.html),定义大概就像: -```haskell -data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) -``` -功能和`Maybe`类似,不过`Maybe`只提供一种值的封装,另一个选项是表示不存在含义的`Nothing`。但`Either`可以表示将可能是两种类型的值封装起来。可以提供左右两种值`Left Right`的模式匹配。常用于需要关心失败原因的场合,用`Maybe`只有`Nothing`无法传递失败原因等信息,就可以使用`Either`,约定`Left`表示错误,`Right`表示成功即可。 - -### 递归定义数据结构 - -就像递归定义函数一样,在类的值构造器中递归调用自己,就可以递归地定义数据类型: -- 比如类似于内置的列表自定义一个列表数据类型: -```Haskell -{- simulate a list, define data recursively ->>> Empty -Empty ->>> 5 `Cons` Empty -Cons 5 Empty ->>> 3 `Cons` (4 `Cons` (5 `Cons` Empty)) -Cons 3 (Cons 4 (Cons 5 Empty)) --} -data MyList a = Empty | Cons a (MyList a) deriving(Eq, Ord, Show, Read) -``` -- `Empty`对应于`[]`,`:`对应于`Cons`,`1:2:[]`对应于`Cons 1 (Cons 2 Empty)`,而`[1, 2]`仅仅是Haskell对`1:2:[]`提供的语法糖。这也解释了为什么`:`可以用于列表的模式匹配,而且只能从左边开始匹配,因为模式匹配就是用值构造器来做的,递归定义所以只能从左边的最外层开始匹配。 -- Haskell还提供自定义运算符(也即是中缀函数)的方法: -```Haskell -{- use self define operator ->>> let a = 1 :-: 2 :-: 3 :-: Empty' ->>> a -1 :-: (2 :-: (3 :-: Empty')) ->>> :t (1 :-: Empty') -(1 :-: Empty') :: Num a => MyList' a ->>> let b = 10 :-: 100 :-: Empty' ->>> a .++ b -1 :-: (2 :-: (3 :-: (10 :-: (100 :-: Empty')))) --} -infixr 5 :-: -data MyList' a = Empty' | a :-: (MyList' a) deriving(Eq, Ord, Show, Read) -infixr 5 .++ -(.++) :: MyList' a -> MyList' a -> MyList' a -Empty' .++ xs = xs -(x :-: xs) .++ ys = x :-: (xs .++ ys) -``` -- 使用`infixr 5 :-:`定义了中缀运算符`:-:`,优先级是5,右结合(所以`1 :-: 2 :-: Empty`从右往左计算,可以不用加括号)。 -- 这是新的语法结构。左结合是`infixl`,右结合是`infixr`,也可以没有结合性`infix`。 -- 定义`.++`类似于列表的`++`,用到了模式匹配和递归定义。可以看到其实并没有任何魔法,都是有迹可循的。 -- 更多自定义运算符的优先级结合性、前缀的函数使用`` ` ` ``转为中缀运算符的优先级和结合性问题仍需探究。 - -例子,二叉搜索树: -```haskell -{- example: binary search tree ->>> treeInsert 3 EmptyTree -Node 3 EmptyTree EmptyTree ->>> let t = listToTree [0, 3, 5, 6, 7, 1, 2, 4, 4, 4] ->>> t -Node 4 (Node 2 (Node 1 (Node 0 EmptyTree EmptyTree) EmptyTree) (Node 3 EmptyTree EmptyTree)) (Node 7 (Node 6 (Node 5 EmptyTree EmptyTree) EmptyTree) EmptyTree) ->>> treeElem 5 t ->>> treeElem 10 t -True -False --} - -data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving(Show, Read, Eq) - -singleton :: a -> Tree a -singleton x = Node x EmptyTree EmptyTree -treeInsert :: Ord a => a -> Tree a -> Tree a -treeInsert x EmptyTree = singleton x -treeInsert x (Node a left right) - | x == a = Node x left right -- de-duplicate, another option is to insert to right - | x < a = Node a (treeInsert x left) right - | x > a = Node a left (treeInsert x right) - | otherwise = singleton x -- can be removed - -listToTree :: (Ord a) => [a] -> Tree a -listToTree = foldr treeInsert EmptyTree - -treeElem :: Ord a => a -> Tree a -> Bool -treeElem x EmptyTree = False -treeElem x (Node a left right) - | x == a = True - | x < a = treeElem x left - | x > a = treeElem x right - | otherwise = False -- can be removed -``` -- 守卫的条件其实已经完备了,`otherwise`可以去掉,只不过会警告所以加上了。 -- 递归的思想在任何语言里都是一样的。 - -### 自定义类型类 - -自定义类型类: -- 回顾一下类型类: - - 类型类以函数的形式定义了一些行为,一个类型如果被定义为该类型类的实例,便可以使用这些函数。 - - 类型类与命令式编程中的类没有任何关系,更加类似于接口类、纯虚类、抽象类等概念,不能直接使用类型类来声明一个实例,而需要从其派生出具体的类型实例。 -- 看一看`Eq`的定义: -```Haskell -class Eq a where - (==) :: a -> a -> Bool - (/=) :: a -> a -> Bool - x == y = not (x /= y) - x /= y = not (x == y) -``` -- `a`是一个类型变量,代表我们定义的任何`Eq`实例类型。并且声明了类型类提供的函数,并不一定需要有函数的定义,不过必须写出函数的类型声明。 -- `Eq`提供的函数是`== /=`,并且是以相互递归的形式定义的。查看`==`的类型,会发现是`(==) :: Eq a => a -> a -> Bool`,`a`所属类型类`Eq`被添加到了约束中。 -- 如果定义一个类型: -```haskell -data TrafficLight = Red | Yellow | Green -``` -- 此时调用`Red == Red`会报错`No instance for (Eq TrafficLight) arising from a use of ‘==’`。 -- 除了`deriving(Eq)`显式从`Eq`派生,还可以通过`instance`使其成为`Eq`的实例,此时就要自行提供`==`的实现。如果不提供`==`实现,那么会报警告,并调用`Eq`的默认实现相互递归直到栈溢出,一切都是合乎逻辑的。 -```haskell -{- define our own typeclasses ->>> Red == Red -True ->>> Red /= Green -True --} - -data TrafficLight = Red | Yellow | Green -instance Eq TrafficLight where - Red == Red = True - Green == Green = True - Yellow == Yellow = True - _ == _ = False -``` -- 此时再调用`== /=`便可以成功,并且由于`Eq`中递归定义了`== /=`,只需要在具体的类型实例中定义其中一者覆盖类型类中定义,便可以使用两者。 - -总结: -- 使用`class`关键字定义类型类,其中声明类型类提供的函数,可以提供缺省定义也可以不提供,其中的类型参数表示类型类的实例。 -- 使用`instance`关键字定义某个类型类的实例,此时将类型参数替换为具体的实例,提供需要的函数定义用来覆盖类型类中的定义。 -- `deriving`关键字对于标准类型类会提供默认的实现,比如`Eq Show`等,如果需要改变这种默认行为,则需要针对该类型类定义类型实例。 -```haskell -{- ->>> show Red -"Red light" --} -instance Show TrafficLight where - show Red = "Red light" - show Yellow = "Yellow light" - show Green = "Green light" -``` -- 也可以把类型类定义为其他类型类的子类,比如`Num`同时也是`Eq`,定义类型类时加上类型约束即可。 -```haskell -class (Eq a) => Num a where - ... -``` -- `instance`声明中,实际的类型类实例必须是具体的类型,如果是有类型参数的`data`,必须需要加上其类型参数(具体的类型或者通用的参数名),而不能仅仅只使用其类型构造器(类型构造器并不表示一个或一类类型)。 -```haskell -instance Eq (Maybe m) where - Just x == Just y = x == y - Nothing == Nothing = True - _ == _ = False -``` -- 大部分情况下,在`class`定义中的类型约束都是宣告一个类型类成为另一个类型类的子类。而在`instance`定义中的类型约束则表达对于类型的限制。比如,要求`Maybe`的内容物(`Just a`中的`a`)也是属于`Eq`。 -- 查看一个类型类有哪些实例,可以在ghci中使用命令`:i/:info YourTypeClass`。 - -实例: -- 定义`YesNo`类型类,提供非`Bool`类型的`True False`判断: -```Haskell -{- YesNo typeclass ->>> yesnoIf [] "Yes" "No" -"No" ->>> yesnoIf [2, 3, 4] "Not Empty" "Empty" -"Not Empty" ->>> yesnoIf True True False -True ->>> yesno Nothing -False ->>> :t Nothing -Nothing :: Maybe a ->>> yesno (Just 1) -True --} - -class YesNo a where - yesno :: a -> Bool - -instance YesNo Int where - yesno 0 = False - yesno _ = True -instance YesNo Bool where - yesno a = a -instance YesNo [a] where - yesno [] = False - yesno _ = True -instance YesNo (Maybe m) where - yesno Nothing = False - yesno _ = True - -yesnoIf :: YesNo a => a -> p -> p -> p -yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult -``` -- 再从`YesNo`派生新类型类`EmptyOrNot`,仅针对列表或者`Maybe`类型,演示一下`class isntance`声明中的类型约束、使用类型类默认实现等情况。 -```haskell -{- Empty or not type class ->>> empty (Just []) -True ->>> empty (Just [1, 2, 3]) -False ->>> empty (Just (Just (Just [1, 2, 3]))) -False ->>> empty [] -True --} - -class YesNo a => EmptyOrNot a where - empty :: a -> Bool - empty = not . yesno - -instance EmptyOrNot [a] -- use implementation of YesNo, equals to (not . yesno) -instance (EmptyOrNot m) => EmptyOrNot (Maybe m) where - empty Nothing = False - empty (Just m) = empty m -``` -- 值得注意的细节是,每个实例类型都需要对多个类型类依次声明,而不能一次性在一次`instance`声明中同时实现`empty`和`yesno`方法,尽管类型类是具有派生关系的。 - -### Functor/函子 - -`Functor`是一个类型类,也称作**函子**,定义: -```haskell -class Functor f where - fmap :: (a -> b) -> f a -> f b -``` -- `Functor`比较特殊的地方在于类型参数`f`,前面遇到的类型参数一般都是一个类型,而这里的`f`并不是具体类型,而是**接受一个类型参数的类型构造器**。 -- `fmap`函数接受一个函数,这个函数从一个类型映射到另一个类型,还接受一个装有原始类型的`Functor` `f a`,返回映射后另一个类型的`Functor` `f b`。或者不全调用,传入一个类型`a`到`b`的映射(函数),得到对应`Functor`函子`f a`到`f b`的映射(函数)。 -- 对于列表类型来说,`map`其实就是列表类型的`fmap`。 -```haskell -map :: (a -> b) -> [a] -> [b] -``` -- 如何将列表定义成函子,注意类型参数传入的类型构造器`[]`而不是具体类型`[a]`。 -```haskell -instance Functor [] where - fmap = map -``` -- `Maybe`作为函子的定义: -```haskell -instance Functor Maybe where - fmap f (Just x) = Just (f x) - fmap f Nothing = Nothing -``` -- 对于`Nothing`,还是`Nothing`,对于`Just x`,则是取出元素映射之后再放回`Just`。就像列表一样,只是列表可以保存多个元素,`Just`只保存一个。 -- 简单来说,将函子看做容器(盒子),`fmap`就是将盒子中数据取出来做运算之后得到结果再装进同样的盒子,结果类型并不需要和源类型一致。容器中存储的数据类型需要是单一的数据类型(实现函子的类型的类型构造器只有一个类型参数)。`fmap`需要在具体类型中进行实现。 -- 当然如果是多个数据类型那么可以固定之后只剩一个,比如对于`Map k v`可以将`Map k`变为函子。 -- 对于`data Either a b = Left a | Right b`一般用`Left`表示错误,`Right`表数据,那么可以将`a`固定不变(错误信息没有改变类型和`fmap`的必要),将`Either a`变为函子。 -- 函子的定义应该遵守一些规则,这样他们的一些性质才能够得到保证。比如使用`(\a -> a)`函数来调用`fmap`那么应该期望得到与参数相同的结果。 - -### Kind - -类型构造器: -- 类型构造器可以接受类型作为类型参数,来构造出一个具体的类型,这样的行为会让我们想到函数,接受参数并返回并一个值。 -- ghci中使用`:k :kind`命令可以查看一个类型的Kind。 -```haskell -import Data.Map -{- Kind ->>> :k Int -Int :: * ->>> :k [] -[] :: * -> * ->>> :k Maybe -Maybe :: * -> * ->>> :kind Maybe -Maybe :: * -> * ->>> :k Maybe Int -Maybe Int :: * ->>> :k Map -Map :: * -> * -> * ->>> :k Map Int -Map Int :: * -> * ->>> :k Map Int String -Map Int String :: * ->>> :k Num -Num :: * -> Constraint ->>> :k Either -Either :: * -> * -> * --} -``` -- 比如`Maybe`的Kind是`* -> *`表示接受一个类型参数并返回一个具体类型。 -- 而`-> Contraint`则表示这是一个约束或者类型类。 -- 对一个类型使用`:k`就类似于对一个值使用`:t`。 -- 类型构造器也是柯里化的,可以部分应用参数,得到新构造器,比如`Map Int`。 -- 类型本身也是有类型系统的,比如一个类型构造器的类型参数也可以被限定为是接受一个类型参数的类型构造器(就像函数接受函数作为参数那样): -```haskell -{- ->>> :k Frank -Frank :: * -> (* -> *) -> * --} -data Frank a b = Frank {frankField :: b a} deriving (Show) -``` -- 函数与类型构造器虽然有相似,但是它们是两个完全不同的东西,不要混淆。 -- 一般来说写实用的Haskell程序时不会需要用到Kind,也不太需要去推敲,但需要知道有这些概念。 -- 最后来一个比较绕的问题:类型类可以有类型参数吗?经过试验大概是不可以。 - -## 输入与输出 - -函数的副作用: -- Haskell是纯函数式语言,命令式语言中给电脑一串指令,在函数式编程中都是以定义东西的方式进行的。Haskell中的函数不能改变状态,比如改变变量内容,当一个函数会改变状态,称之为有副作用。没有副作用的函数在任何时候任何情况下以相同参数进行两次调用,结果都必定是相同的。 -- 无副作用的函数即是优点也是限制,也很好理解,但是如果要进行输入输出,就必须要改变输入输出设备的状态。所以也需要存在有副作用的函数。 -- Haskell在设计上对有副作用的函数做了区分,将程序分为纯粹和非纯粹两部分,输入输出由非纯粹的部分来处理,纯粹的部分依然具有函数式编程的优点,比如惰性求值、容错、模块性。 - -### IO动作 - -前面讨论的内容都是无副作用的,都是应该如何编写函数,计算结果,没有讨论过如何输出结果与组织程序,从输出开始: - -```haskell -main = putStrLn "hello, world" -``` -保存为`helloworld.hs`,编译`ghc helloworld.hs`,得到`helloworld.hi helloworld.i helloworld(.exe)`。可执行文件达到10MB,这得链了多少东西进去。 - -看一下`putStrLn`的类型声明: -``` -Prelude> :t putStrLn -putStrLn :: String -> IO () -Prelude> :t putStrLn "hello" -putStrLn "hello" :: IO () -``` -`putStrLn`接受一个字符串并返回一个IO动作,这个IO动作的类型参数是`()`(即空的元组,或者是unit类型)。 - -所谓IO动作: -- 一个IO动作(I/O Action)是一个会造成副作用的动作,常常是读取输入或者输出到屏幕,同时会返回一些值。在标准输出打印字符串没有具体的值返回,用一个`()`代表。 -- 一个IO动作会在我们把它绑定到 `main` 这个名字并且执行程序的时候触发。 -- 整个程序限制到只能有一个IO动作看起来是很大的限制,所有有了`do`表示法将所有IO动作绑成一个。 - -`do`表示法: -```haskell -main :: IO () -main = do - putStrLn "hello, input your name:" - name <- getLine - putStrLn ("Hey ! " ++ name ++ " Yo ! what's up !") -``` -- `do`后接了一串指令,就像命令式程序一样,每一步都是一个IO动作,将所有IO动作绑到一起变成了一个大的IO动作,类型同样是`IO something`由最后一个IO动作决定。 -- `main`的类型永远是`main :: IO somehting`,按照惯例,我们通常不会将`main`的类型在程序中写出来。 -- 在`do`块中使用`let`表达式可以没有`in`部分,含义就是变量或者函数的绑定。 - -使用IO动作: -- 输入:`getLine :: IO String`。 -- IO就像一个盒子,打开盒子拿到其中的字符串的方法就是`<-`。 -- `getLine`是不纯粹的有副作用的,执行两次不能保证拿到同样的结果。 -- 一段程序如果依赖着IO数据,那么这段程序也会被视为IO代码。这并不代表不能在纯粹的代码中使用IO动作返回的数据,只需要将其绑定到一个名字便可以短暂使用它。 -- 同理如果要处理一些非纯粹的数据,应该到非纯粹的环境中做,最好把IO的部分缩减到最小的比例。 -- 像`putStrLn :: String -> IO ()`也可以取出其中的值,`foo <- putStrLn "hello"`只不过取出来的也是`()`,没必要。 -- 总之,要取出一个IO动作的值,就需要在另一个IO动作中将他用`<-`绑定给一个名字。换句话说,IO动作中才能执行`<-`,还不能是作为最后一个表达式。 -- IO动作只有绑定给`main`或是在另一个用`do`串起来的IO动作时才会执行。可以用`do`串接IO动作之后再用`do`来串接这些串接起来的IO动作。最外层的IO动作绑定到`main`时才会触发执行。就类似于程序入口`main`函数。 -- GHCI中也可以执行IO动作。 - -`return`: -```haskell -main :: IO () -main = do - line <- getLine - if null line - then return () - else do - putStrLn $ reverseWords line - main - -reverseWords :: String -> String -reverseWords = unwords . map reverse . words -``` -- 这个程序按行接受输入,将输入中的每个单词反转之后合并为新行输出,直到空行则停止。 -- `do`块可以使用`return :: Monad m => a -> m a`,`IO`同时也是一个`Monad`。`return`这里与命令式语言中的单纯函数返回的逻辑完全不同,Haskell中`return`的含义是利用一个纯粹的值制造出一个IO对象,因为`main`的返回值类型是`IO ()`。 -- 一般的命令式语言中,`return`都代表中断函数执行,从此处返回。而Haskell中`return`并不会导致函数返回,`return`同样只是做一个函数调用通过一个值得到一个IO对象而已。整个`do`就是一个表达式,它的值是其中最后一个`IO`动作的值,不存在说在一个表达式中返回这样的操作。 -```haskell -main = do - return () - return "HAHAHA" - line <- getLine - return "BLAH BLAH BLAH" - return 4 - putStrLn line -``` -- 像这样的逻辑,`return`将值装到IO对象中,其实就相当于什么都没做,最后返回还是`putStrLn line`的结果。甚至可以用`a <- return "hell"`这样来在从IO对象中取出数据。 -- 需要`return`的原因:需要一个什么都不做的IO动作,或者不希望`do`块这个IO动作的结果值是其中最后一个IO动作的值时,就用`return`装在IO中后放到`do`块的最后面。 -- 无论如何`return`要起作用都应该放在最后一个表达值中或者直接作为最后一个表达式。 - -### 输入与输出函数 - -常用输出函数: -- `putStr :: String -> IO ()` 不换行输出。 -- `putStrLn :: String -> IO ()` 换行输出。 -- `putChar :: Char -> IO ()` 输出字符。 -- `print :: Show a => a -> IO ()` 输出`Show`实例,基本上就是`putStrLn . show`。 - -输入函数: -- `getChar :: IO Char` 读取字符。 -- `getLine :: IO String` 读取行。 -- `getContents :: IO String` 读取内容直到EOF(End of file)。 - -固定结构和模式: -- `when :: Applicative f => Bool -> f () -> f ()`在模块`Control.Monad`中,其作用就是将`if condition then (do some I/O action) else return ()`这样的模式封装为`when condition (do some I/O action)`,如果你写出了前面的结构,hlint会提示可以改写为后者: -```haskell -import Control.Monad -main :: IO () -main = do - c <- getChar - when (c /= ' ') $ do - putChar c - main -``` -- `sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)`可以接受一串IO动作,回传一个会依次执行他们的IO动作,运算的结果是包在一个IO动作中的一连串IO动作结果,用在`IO`中比较典型是使用是`t`是列表,`m`是`IO`。常见用法类似于`sequence (map print [1, 2, 3])`这样。例:接受三行输入并输出: -```haskell -testSequence :: IO () -testSequence = do - rs <- sequence [getLine, getLine, getLine] - print rs -``` -- 对于一个列表`map`传一个返回IO动作的函数,然后再`sequence`这个动作太常用了,以至于函数库中有`mapM mapM_`来简化了这个操作,前者保留结果,后者丢弃结果。当我们对结果不关心时,后者会用得多一些。 -```haskell -mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b) -mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m () - -testMapM :: IO [()] -testMapM = do - mapM print [1, 2, 3] - -testMapM_ :: IO () -testMapM_ = do - mapM_ print [1, 2, 3] -``` -- `Control.Monad`中的`forM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)`函数和`mapM`作用一致,只是参数顺序不一样。 -```haskell -forM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b) -forM_ :: (Foldable t, Monad m) => t a -> (a -> m b) -> m () - -testForM :: IO [()] -testForM = do - colors <- forM [1,2,3,4] (\a -> do - putStrLn $ "Which color do you associate with the number " ++ show a ++ "?" - getLine) - putStrLn "The colors that you associate with 1, 2, 3 and 4 are: " - mapM putStrLn colors -``` -- `Control.Monad.forever :: Applicative f => f a -> f b`接受一个IO动作并返回永远做这件事(直到EOF)的IO动作: -```haskell -testForever :: IO () -testForever = forever $ do - l <- getLine - putStrLn $ map Data.Char.toUpper l -``` - -总结: -- 输入输出函数仍是函数,要将其看做进行输入输出操作并返回IO action的函数,而不是输出内容到屏幕。 -- `do`仅仅是语法糖,封装多个IO动作为一个。 -- `return`仅做包装,与命令式程序中的函数返回区分开来。 - -### 文件与字符流 - -**惰性I/O**: -- 输入函数`getContents`同样是懒惰I/O(Lazy I/O)的,直到需要用到内容时才去读取,而不是像命令式一样立马读取输入。 -```haskell -import Control.Monad -import Data.Char -testContents :: IO () -testContents = do - contents <- getContents - putStr $ map toUpper contents - -main :: IO () -main = testContents -``` -- 如果我们使用文件和管道去操作它,新建`test.txt`: -``` -Nephren Ruq Insania -Catholly Nota Seniorious -Lilia Asplay -``` -- 编译执行:`cat test.txt | ./IOFunction` -```shell -$ cat test.txt | ./IOFunctions -NEPHREN RUQ INSANIA -CATHOLLY NOTA SENIORIOUS -LILIA ASPLAY -``` -- 而如果从标准输入按行输入,那么会发现内容会按行(行缓冲)逐渐输入到`contents`,直到输入EOF结束,这就是懒惰I/O,而不是表示`contents`是一个在内存中存储了具体字符串的中间变量。 -```shell -$ ./IOFunctions -asdf -ASDF -asdf -ASDF -Catholly Nota Seniorious -CATHOLLY NOTA SENIORIOUS -``` -- 由于懒惰IO的存在,没有输入在真正被用到之前被读入。 - -**`interact`**: -- 从输入取字符串,执行一些转换后输出这种模式太常见了,于是有一个函数专门做这个事情:`interact :: (String -> String) -> IO ()`,传入一个转换函数,对所有输入构成的字符串执行转换,直到EOF。 -- 比如上面的函数,再加上一个只输出长度小于10的行: -```haskell -main :: IO () -main = interact $ map toUpper . unlines . filter ((<10) . length) . lines -``` -- 使用场景主要是用管道读取整个文件做一些处理输出,或者按行处理输入直接输出的场景。 -- 在Windows上测试时发现这样对标准输入工作但对管道不工作(而`getContents`是工作的),按道理管道不应该和标准输入有区别,尚不知道具体原因! - - -文件操作: -- 读写文件与标准输入输出并没有什么不同,标准输入输出就是读取名为`stdin stdout`的特殊文件IO。 -- 打开文件: -```haskell -import System.IO - -main :: IO () -main = do - handle <- openFile "test.txt" ReadMode - contents <- hGetContents handle - putStr contents - hClose handle -``` -- 相关函数和类型,打开后使用句柄操作,读取结束后关闭,读写模式有读、写、追加写、读写。和其他语言大同小异。 -```haskell -openFile :: FilePath -> IOMode -> IO Handle -hGetContents :: Handle -> IO String -hClose :: Handle -> IO () -data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode -``` -- 可将`stdin stdout`作为句柄文件文件IO的函数上,`hGetContents stdin`与`getContents`含义相同。 -- 文件打开后需要关闭,可以使用`withFile`函数来管理文件的关闭,离开后自动关闭,而不用显式调用`hClose`。接受文件、读写模式和一个句柄到要执行的IO操作的函数(通常都会传入一个lambda),`withFile`打开文件后将句柄传给函数执行其中的操作,并在执行结束后关闭。 -```haskell -withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r - -testWithFile :: IO () -testWithFile = do - withFile "test.txt" ReadMode (\handle -> do - contents <- hGetContents handle - putStr contents) -``` -- 实现一个`withFile`: -```haskell -withFile' :: FilePath -> IOMode -> (Handle -> IO b) -> IO b -withFile' path mode f = do - handle <- openFile path mode - result <- f handle - hClose handle - return result -``` -- 常见输入输出函数的文件版本: -```haskell -hGetLine :: Handle -> IO String -hGetChar :: Handle -> IO Char -hPutChar :: Handle -> Char -> IO () -hPutStr :: Handle -> String -> IO () -hPutStrLn :: Handle -> String -> IO () -hPrint :: Show a => Handle -> a -> IO () -``` -- 除了这些常见IO操作,读取文件并处理字符串内容的操作实在太常见了,于是有三个函数用来简化工作,用于读文件内容、写内容到文件、添加内容到文件。 -```haskell -readFile :: FilePath -> IO String -writeFile :: FilePath -> String -> IO () -appendFile :: FilePath -> String -> IO () -``` -- `getContents hGetContents`都是懒惰IO,不会一次将文件读到内存中,其实就是一个有缓冲的流。文本文件默认是行缓冲,也就是一次读进来的内容是一行,二进制文件默认是块缓冲(Block-Buffering),一个块一个块(Chunk)的读取。可以用`hSetBuffering`控制缓冲的行为。`hSetBuffering hFlush`会返回一个会设置缓冲和刷新缓冲的IO动作。 -```haskell -hSetBuffering :: Handle -> BufferMode -> IO () -hGetBuffering :: Handle -> IO BufferMode -hFlush :: Handle -> IO () -data BufferMode = NoBuffering | LineBuffering | BlockBuffering (Maybe Int) -``` -- 其他函数: -```haskell -System.Directory.renameFile :: FilePath -> FilePath -> IO () -System.Directory.removeFile :: FilePath -> IO () -System.Directory.getCurrentDirectory :: IO FilePath -System.IO.openTempFile :: FilePath -> String -> IO (FilePath, Handle) -- open a temporary file -``` -- 使用`openTempFile`打开临时文件时,可以传入文件名`"temp"`这样会生成一个`temp`加上随机字符串的文件名,可以防止覆写已有的文件。 - -### 命令行参数 - -编写运行在终端中的函数时,命令行参数是必不可少的,利用Haskell的标准库可以有效地处理命令行参数。 - -`System.Environment`中提供了获取程序名称和命令行参数的函数: -```haskell -getArgs :: IO [String] -getProgName :: IO String -``` -- 比如编译得到的目标文件是`test`,那么执行`test hello world`那么`getArgs`得到`IO ["hello", "world"]`而`getProgName`得到`IO "test"`。 -- 一个例子,处理文本文件,通过命令行参数输入,可以查看增加和删除待办事项,错误处理待完善: -```haskell -{-# OPTIONS_GHC -Wno-incomplete-patterns #-} -import System.Environment -import System.Directory -import System.IO -import Data.List - -{- command arguments ->>> :t getArgs ->>> :t getProgName -getArgs :: IO [String] -getProgName :: IO String --} - --- a to do list processing example -dispatch :: [([Char], [String] -> IO ())] -dispatch = [("add", add) - , ("view", view) - , ("remove", remove) - ] - --- look up command and execute -main :: IO () -main = do - (command:args) <- getArgs - let (Just action) = lookup command dispatch - action args - --- prog add file item, add item to end of to-do list -add :: [String] -> IO () -add [fileName, todoItem] = do - appendFile fileName (todoItem ++ "\n") - view [fileName] - --- prog view file, view to-do list file -view :: [String] -> IO () -view [fileName] = do - contents <- readFile fileName - let todoTasks = lines contents - numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks - putStr $ unlines numberedTasks - --- prog remove file number, to remove line of to do list -remove :: [String] -> IO () -remove [fileName, numberOfString] = do - handle <- openFile fileName ReadMode - (tempName, tempHandle) <- openTempFile "." "temp" - contents <- hGetContents handle - let number = read numberOfString - todoTasks = lines contents - newTodoItems = delete (todoTasks !! number) todoTasks - hPutStr tempHandle $ unlines newTodoItems - hClose handle - hClose tempHandle - removeFile fileName - renameFile tempName fileName - view [fileName] -``` - -### 随机数 - -首先要明确的是要产生(伪)随机数,那么每次调用就应该拿到不一样的数字,但是Haskell是纯函数式语言,纯粹的引用透明(referential transparency)的函数是没有副作用的,特定输入就会得到特定输出。所以随机数发生的部分一定是有副作用的。 - -其他编程语言是怎么产生随机数的呢?可能会拿到电脑的一些信息,比如时间、鼠标信息、甚至CPU中的微小扰动等,根据这些信息算出一个看起来随机的值,或者更简单的类似于线性同余这种具有特定周期的伪随机数。在Haskell中,我们需要的随机函数应该是接受具有随机性的值,根据信息经过计算后得到一个值,也就是函数本身没有副作用,只是传入参数发生了变化。 - -`System.Random`模块(不在基础库`base`中,需要安装`random`包)中提供了这样的函数:`System.Random.random :: (Random a, RandomGen g) => g -> (a, g)`。 -- 其中`Random`是可以用来装随机数的类型类,`RandomGen`是随机数发生器的类型类。 -- `random`会接受一个随机数发生源,返回生成的随机数和新的随机数发生器(random generator)。 - -制作随机数发生器: -- `mkStdGen :: Int -> StdGen`可以用来制作一个整数随机数发生器。 -- 注意`Random`是一个类型类,在`System.Random`中实现了多个类型实例,所以使用时需要通过类型指定具体用哪一个。 -```haskell -Prelude System.Random> :i Random -type Random :: * -> Constraint -class Random a where - randomR :: RandomGen g => (a, a) -> g -> (a, g) - default randomR :: (RandomGen g, UniformRange a) => - (a, a) -> g -> (a, g) - random :: RandomGen g => g -> (a, g) - default random :: (RandomGen g, Uniform a) => g -> (a, g) - randomRs :: RandomGen g => (a, a) -> g -> [a] - randoms :: RandomGen g => g -> [a] - -- Defined in ‘System.Random’ -instance Random Word -- Defined in ‘System.Random’ -instance Random Integer -- Defined in ‘System.Random’ -instance Random Int -- Defined in ‘System.Random’ -instance Random Float -- Defined in ‘System.Random’ -instance Random Double -- Defined in ‘System.Random’ -instance Random Char -- Defined in ‘System.Random’ -instance Random Bool -- Defined in ‘System.Random’ -``` -- 如果一直调用`random $ mkStdGen 100`的话会得到同样的结果,因为没有副作用,要生成多个随机数的话,需要使用返回的随机数发生器。但又因为没有循环,所以要这样生成很多随机数就很麻烦了。 -- 多个随机数可以使用`randoms :: (Random a, RandomGen g) => g -> [a]`,返回一个无限列表。 -```haskell -Prelude System.Random> take 10 $ randoms (mkStdGen 100) :: [Double] -[0.5003737531410817,0.3750119639966999,0.12733827138953357,3.882299251466059e-2,0.21477954261574972,0.6105785015461408,3.65223575759297e-2,0.9636215830016226,0.9939144570578136,0.9113023469143849] -Prelude System.Random> take 10 $ randoms (mkStdGen 100) :: [Bool] -[True,False,False,False,False,False,True,False,False,False] -Prelude System.Random> take 10 $ randoms (mkStdGen 100) :: [Int] -[9216477508314497915,-6917749724426303066,-2348976503111297336,-716157807093485800,-3961983254901128710,7183558718778820252,-673718583171682711,671063348175752782,112258453204082922,1636182906409015240] -Prelude System.Random> take 10 $ randoms (mkStdGen 100) :: [Char] -"\537310\28348\950093\909872\685754\243589\711431\321177\1019517\261448" -``` -- `randoms`的实现就像是这样的: -```haskell -randoms' :: (RandomGen g, Random a) => g -> [a] -randoms' gen = let (value, newGen) = random gen in value:randoms' newGen -``` -- 做一下验证就知道其实就是这样实现的。 -- 实现一个返回有限个随机数和一个随机数生成器的函数: -```haskell -finiteRandoms :: (RandomGen g, Random a) => Int -> g -> ([a], g) -finiteRandoms n gen - | n <= 0 = ([], gen) - | otherwise = - let (value, newGen) = random gen - (restOfList, finalGen) = finiteRandoms (n-1) newGen - in (value:restOfList, finalGen) -``` -- 除了`random randoms`还可以使用`randomR randomRs`得到范围内随机数。 -```haskell -Prelude System.Random> :t randomR -randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g) -Prelude System.Random> :t randomRs -randomRs :: (Random a, RandomGen g) => (a, a) -> g -> [a] -Prelude System.Random> randomR (1, 10000) (mkStdGen 100) -(892,StdGen {unStdGen = SMGen 712633246999323047 2532601429470541125}) -Prelude System.Random> randomR (1.0, 10.0) (mkStdGen 100) -(5.503363778269735,StdGen {unStdGen = SMGen 712633246999323047 2532601429470541125}) -Prelude System.Random> randomR (False, True) (mkStdGen 100) -(True,StdGen {unStdGen = SMGen 712633246999323047 2532601429470541125}) -Prelude System.Random> take 10 $ randomRs (1.0, 100.0) (mkStdGen 100) -[50.537001560967084,38.12618443567329,13.606488867563824,4.843476258951399,22.26317471895922,61.447271653067936,4.61571340001704,96.39853671716064,99.39753124872354,91.2189323445241] -Prelude System.Random> take 100 $ randomRs ('a', 'z') (mkStdGen 0) -"apyzsowwxjpdgslfiaqdhpawqyhuewqdnciakestkcsdttutjrnyjnqvfnmiiyneyggtfggvkoujcptcdeesvswyjxrcssudsgzw" -``` - -`mkStdGen`每次只要传入的数相同,得到的随机数序列一定是相同的,传入的数就像随机数种子。实际生产环境这样是不能接受的,要么我们需要一个随机的随机数种子,要么需要更加随机化的随机数发生器。 - -所以`System.Random`提供了`getStdGen :: Control.Monad.IO.Class.MonadIO m => m StdGen`用来获取一个`IO StdGen`。 -- 程序执行时会有一个全局的随机数生成器,`getStdGen`就是拿到这个random generator并绑定到某个名字上。 -```haskell -main :: IO () -main = do - gen <- getStdGen - print (take 30 $ randomRs (1.0, 100.0) gen :: [Double]) -``` -- 某些程序在GHCI下也许不需要显式执行也能有一个类型,但在`.hs`中则需要指定类型,比如`random (mkStdGen 100)`,在GHCI中不指定类型会得到整数,需要注意,在`.hs`中则需要像这样声明类型`(let (value, _) = random (mkStdGen 100) in value :: Int)`。 -- `::`类型声明的优先级和结合性值得研究。 -- 两次通过`getStdGen`拿到的`StdGen`其实是一样的。要生成不一样的序列,可从一个长的随机序列中截取。 -- 需要每次都得到不一样的可以使用`newStdGen :: Control.Monad.IO.Class.MonadIO m => m StdGen`: -```haskell -Prelude System.Random> getStdGen -StdGen {unStdGen = SMGen 2577900980329305605 14820693616592480073} -Prelude System.Random> getStdGen -StdGen {unStdGen = SMGen 2577900980329305605 14820693616592480073} -Prelude System.Random> getStdGen -StdGen {unStdGen = SMGen 2577900980329305605 14820693616592480073} -Prelude System.Random> newStdGen -StdGen {unStdGen = SMGen 10681034358804626100 9442248464978456119} -Prelude System.Random> newStdGen -StdGen {unStdGen = SMGen 16153741442900193658 9209421901824985913} -Prelude System.Random> getStdGen -StdGen {unStdGen = SMGen 6520443225570571049 14820693616592480073} -Prelude System.Random> getStdGen -StdGen {unStdGen = SMGen 6520443225570571049 14820693616592480073} -``` -- 调用`newStdGen`后,全局的随机数发生器会被重新指定,`getStdGen`会和原先不一样,但再次调用又会一样。 - -### ByteString - -在Haskell中因为列表非常重要,因为惰性求值,所以可以用`map filter`等函数来代替其他语言中的循环。由于求值只发生在需要的时候,所以甚至可以实现无限列表的无限列表这种东西。也正是因为惰性求值,所以可以用列表来表示流(Stream),比如`getContents`。 - -但因为`[1,2,3,4]`只是`1:2:3:4:[]`的语法糖(syntactic sugar),而`[Char]`中Char是Unicode字符,没有一个固定大小,所以用`[Char]`其实并不是一个高效的做法。比如在读大文件时可能就会造成负担。所以Haskell提供了`ByteString`。 -- 模块:`Data.ByteString`,类型`ByteString`,这个类型是严格的,也就是没有惰性求值。严格的`ByteString`不可以无限长,如果求第一个字节,就必须求出整个`ByteString`。它没有Thunk(也即是说Haskell中常说的术语**保证**),缺点是可能快速消耗内存,因为进行了一次性读取。 -- 对应的惰性求值的`ByteString`在`Data.ByteString.Lazy`中。惰性的`ByteString`具有惰性,但不会像`List`那么极端,它的数据被存在一个个64K的chunk(块)中,每次求值会按照chunk作为单位来求值,求到包含需要的字节的chunk即可。有点像装了一堆大小为 64K 的严格`ByteString`的列表。 -- 一个`ByteString`的基本组成是8比特的字节。 -- 大多数情况我们使用惰性的`ByteString`。 -```haskell -import qualified Data.ByteString.Lazy as B -import qualified Data.ByteString as S -``` -- 要构建`ByteString`需要使用:`pack :: [Word8] -> ByteString`,参数中的`Word8`类型在`GHC.Word`中,可以直接用`[1, 2, 100, 256, 300]`这样的列表来初始化(因为字面值是多态的,可以用于整型浮点等多种数据类型),超过一个字节的值会被截断并报警告。 -- 对`ByteString`调用`show`得到的结果和字符串差不多。 -- `unpack :: ByteString -> [Word8]`做相反的事情。 -- `fromChunks`将一个列表的严格的`ByteString`转换为一个懒惰的,`toChunks`做相反的事情。 -```haskell -B.fromChunks :: [ByteString] -> ByteString -B.toChunks :: ByteString -> [ByteString] -{- ->>> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]] -"()*+,-./0" ->>> B.toChunks (B.pack [40..48]) -["()*+,-./0"] --} -``` -- `ByteString`对应于列表的`:`运算符的是`B.cons :: Word8 -> ByteString -> ByteString` `B.cons' :: Word8 -> ByteString -> ByteString`前者是懒惰的,后者是严格的。但用起来没有看到差别: -```haskell -Prelude S B> B.cons 80 $ B.pack [81..90] -"PQRSTUVWXYZ" -Prelude S B> B.cons' 80 $ B.pack [81..90] -"PQRSTUVWXYZ" ->>> foldr B.cons B.empty [50..60] ->>> foldr B.cons' B.empty [50..60] -"23456789:;<" -"23456789:;<" -``` -- `ByteString`也有列表操作类似的函数:`head tail init last null length map reverse foldl concat take takeWhile`等。 -- 也有`System.IO`中一样的函数,只是`String`换成了`ByteString`。比如:`B.readFile :: FilePath -> IO ByteString`。 - -`ByteString`可以为数据读取提供更好的性能,一般正常用`String`,在性能不好是替换为`ByteString`。 - -### 异常(Exceptions) - -Haskell中常用`Maybe Either`这种包装类型来处理失败的情况。 - -除此之外,Haskell是支持异常的,除了IO这种依赖于环境的行为,算术运算比如除0等操作也可能引发异常。 -```haskell -Prelude S B> 4 / 0 -Infinity -Prelude S B> 4 `div` 0 -*** Exception: divide by zero -Prelude S B> head [] -*** Exception: Prelude.head: empty list -``` -- 无副作用的纯粹代码(Pure Code)或者IO操作都可能抛出异常,但是异常只能在IO环境中才能被catch。因为纯函数默认懒惰求值,我们不知道什么时候会求值,也就不知道应该在什么地方加捕获代码。而do块中的IO是顺序执行的,可以捕获。 -- 这种设计就要求我们尽量不要在纯函数中使用异常(尽管某些函数还是会抛出),而是使用`Maybe Either`,然后仅在IO操作中使用异常。 - -捕获异常: -- 使用`catch :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a` -```haskell -import System.Environment -import System.IO -import System.IO.Error -import Control.Monad.Catch.Pure (MonadCatch(catch)) - -{- ->>> :t catch -catch :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a --} - -toTry :: IO () -toTry = do - (fileName:_) <- getArgs - contents <- readFile fileName - putStrLn ("This file has " ++ (show . length . lines) contents ++ " lines !") - -handler :: IOError -> IO () -handler e - | isDoesNotExistError e = putStrLn ("File not exist : " ++ show e) - | isEOFError e = putStrLn "EOF Error!" - -- other errors - | otherwise = ioError e -- ioError is like raise/throw in other languages - -main :: IO () -main = toTry `catch` handler -``` -- 接受一个要捕获的IO动作,和处理异常的函数。 -- 使用不存在的文件调用时就会抛出异常并被捕获到。 -```shell -$ stack exec runhaskell Exception.hs hello -File not exist : hello: openFile: does not exist (No such file or directory) -``` -- 有多个检测IOError类型的函数: -```haskell -isDoesNotExistError :: IOError -> Bool -isAlreadyExistsError :: IOError -> Bool -isFullError :: IOError -> Bool -isEOFError :: IOError -> Bool -isIllegalOperation :: IOError -> Bool -isPermissionError :: IOError -> Bool -isUserError :: IOError -> Bool -``` - -抛出异常: -- 也可以捕获到异常后不处理继续抛出:`ioError :: IOError -> IO a`函数。接受一个`IOError`并返回一个会抛出该异常的`IO`操作。 -- 可以使用`userError :: String -> IOError`得到一个对`isUserError`为`True`的`IOError`。 -- 上面的逻辑在处理`IOError`是还可以用`ioeGetFileName :: IOError -> Maybe FilePath`从`IOError`中抽出路径。 -- 不想捕获所有异常的话,可以只捕获某一个IO动作的异常并处理,`catch`函数本身也是返回`IO a`。 - -这里并未提到如何在纯代码中抛出异常,正如前面所说的,不要这么做。异常也大多是在IO动作中处理,异常本身也都是IO的异常,就算是IO操作,如果能用`IO (Either a b)`相比使用异常可能也会更舒服一些。 - -总体来说,Haskell具备基本的异常处理,但用起来并不是那么舒服,也不太推荐使用异常,无可避免的时候还是要用的。 - -## 问题解决实例 - -看两个经典的问题用Haskell要如何解决。 - -### 逆波兰表达式 - -经典的计算逆波兰表达式(RPN,Reverse Polish Notation,或者叫后缀表达式Postfix notation)的例子。 -- 计算方式,比如`10 1 2 + *`,从左往右,遇到数值就压栈,遇到符号从栈中取两个数计算后压栈,直到无运算符或者值,后缀表达式合法时最终栈中仅剩一个数,取出即最终结果。后缀表达式蕴含了计算顺序,不需要括号来规定。 -```haskell -calculatePostfix :: (Num a, Read a) => String -> a -calculatePostfix = head . foldl foldingFunction [] . words - where foldingFunction (x:y:ys) "*" = (x * y):ys - foldingFunction (x:y:ys) "+" = (x + y):ys - foldingFunction (x:y:ys) "-" = (x - y):ys - -- other functions - foldingFunction xs numberString = read numberString:xs -``` -- 调用,可以用于整数浮点等类型,还可以容易地扩展至其他运算符,比如`/ ^ log sum`等。 -```haskell ->>> calculatePostfix "1.1 2.4 + 3.1 *" :: Double -10.85 ->>> calculatePostfix "1 2 + 3 *" :: Int -9 -``` -- 其中函数声明用了函数组合,不用组合的话可能会更清晰一些: -```haskell -calculatePostfix expression = head (foldl foldingFunction [] (words expression)) - where ... -``` -- 基本没有错误处理,数值少了可能直接宕掉,如果要处理错误可以声明成这样并添加错误处理的逻辑。 -```haskell -calculatePostfix :: (Num a, Read a) => String -> Maybe a -``` - -### 路径规划问题 - -直接看代码,问题描述、建模过程、求解思路都在这里了,问题很简单,建模的过程比较精彩: -```haskell -{- a route planing problem from LEARN YOU A HASKELL FOR GREAT GOOD -example: - 50 5 40 10 -A------------------------------- -C |30 |20 |25 |0 destination -B------------------------------- - 10 90 2 8 - -best route: BCACBB -best length: 75 - -input: many groups of length of ABC -this example: 50 10 30 5 90 20 40 2 25 10 8 0 --} - --- a section of road, Section a b c -data Section = Section {getA :: Int, getB :: Int, getC :: Int} deriving(Show) -type RoadSystem = [Section] --- example : [Section 50 10 30, Section 5 90 20, Section 40 2 25, Section 10 8 0] - --- type of path -data Label = A | B | C deriving(Show) -type Path = [(Label, Int)] - --- One step of road, input the optimal path to current node of A & B and this section of road, --- get next optimal path to next node of A & B. -roadStep :: (Path, Path) -> Section -> (Path, Path) -roadStep (pathA, pathB) (Section a b c) = - let priceA = sum $ map snd pathA - priceB = sum $ map snd pathB - forwardPriceToA = priceA + a - crossPriceToA = priceB + b + c - forwardPriceToB = priceB + b - crossPriceToB = priceA + a + c - newPathToA = if forwardPriceToA <= crossPriceToA - then (A,a):pathA -- path is a reverse list of actual path, from right to left - else (C,c):(B,b):pathB - newPathToB = if forwardPriceToB <= crossPriceToB - then (B,b):pathB - else (C,c):(A,a):pathA - in (newPathToA, newPathToB) - --- expected return of example: [(B,10),(C,30),(A,5),(C,20),(B,2),(B,8)] -optimalPath :: RoadSystem -> Path -optimalPath roadSystem = - let (bestPathA, bestPathB) = foldl roadStep ([], []) roadSystem -- if stackoverflow, try strict version foldl' - in if sum (map snd bestPathA) > sum (map snd bestPathB) - then reverse bestPathA - else reverse bestPathB - -{- test of example ->>> optimalPath [Section 50 10 30, Section 5 90 20, Section 40 2 25, Section 10 8 0] -[(B,10),(C,30),(A,5),(C,20),(B,2),(B,8)] --} - --- group input data into sections -groupsOf :: Int -> [a] -> [[a]] -groupsOf _ [] = [] -groupsOf n _ - | n <= 0 = undefined -groupsOf n xs = take n xs : groupsOf n (drop n xs) - --- read input from stdin -main :: IO () -main = do - contents <- getContents - let threes = groupsOf 3 (map read $ lines contents) - roadSystem = map (\[a, b, c] -> Section a b c) threes - path = optimalPath roadSystem - pathString = concatMap (show . fst) path -- concat $ map (show . fst) path - pathLength = sum $ map snd path - putStrLn $ "Best path is : " ++ show pathString - putStrLn $ "Best length is : " ++ show pathLength -``` -- 测试文件`road.txt`: -``` -50 -10 -30 -5 -90 -20 -40 -2 -25 -10 -8 -0 -``` -- 运行(windows中可以用`type road.txt`): -``` -$ cat road.txt | stack exec runhaskell RoutePlaning.hs -Best path is : "BCACBB" -Best length is : 75 -``` -- 从这个问题可以看出,用Haskell对问题建模要一步一步来,一个函数做一个确定的事情,函数最好短小而确切,一个个函数的作用就是对数据的映射筛选和处理。对数据和问题建模,并将问题拆分成一个个小问题之后依次解决,最后在`main`函数中处理IO。 - -## 函子、应用函子与幺半群 - -接下来就是讨论函子(Functors),应用函子(Applicative Functors)和幺半群(Monoids)。 - -前面提到了函子`Functor`,就是一群可以被映射的对象,可以理解为一个盒子,`fmap`就是对盒子中的对象做操作,当前已经遇到了很多函子的实例,比如`[] IO Maybe Either`等。 - -这里还会学到更多比较强一些的版本。 - -注意:这里用盒子来比喻函子,后续的应用函子和单子也会这样比喻,多数情况下这样比喻是恰当的,但不要过度引申,某些函子可能不适用这个比喻。一个比较正确的形容是函子是一个计算语境(computational context),这个语境可能带有值,可能会失败(`Maybe Either`),可能有多个值(`List`)等。 - -### 函子 - -回顾一下`Functor`,是一个类型类,接受一个类型构造器作为类型参数,定义了`fmap`函数,将函子中的值按照传入函数映射之后再包装在一个函子中。 -```haskell -type Functor :: (* -> *) -> Constraint -class Functor f where - fmap :: (a -> b) -> f a -> f b - (<$) :: a -> f b -> f a - {-# MINIMAL fmap #-} - -- Defined in ‘GHC.Base’ -instance Functor (Either a) -- Defined in ‘Data.Either’ -instance Functor [] -- Defined in ‘GHC.Base’ -instance Functor Maybe -- Defined in ‘GHC.Base’ -instance Functor IO -- Defined in ‘GHC.Base’ -instance Functor ((->) r) -- Defined in ‘GHC.Base’ -instance Functor ((,,,) a b c) -- Defined in ‘GHC.Base’ -instance Functor ((,,) a b) -- Defined in ‘GHC.Base’ -instance Functor ((,) a) -- Defined in ‘GHC.Base’ -``` - -`IO`: --看一下`IO`是怎么实现`Functor`实例的: -```haskell -instance Functor IO where - fmap f action = do - result <- action - return (f result) -``` -- 试一试`fmap`: -```haskell -main :: IO () -main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine - putStrLn line -``` -- 如果想要对Functor中的数据做变换,可以先将变换函数定义出来,或者使用lambda或者使用函数组合。 - -`(->) r`: -- 注意到`(->)`的Kind是`(->) :: * -> * -> *`,也就是说其实`(->)`是一个类型构造器,而非运算符(函数类型)。 -```haskell -type (->) :: * -> * -> * -data (->) a b -``` -- `(->)`接受两个类型参数并得到一个函数类型,而函子接受一个类型参数,所以`(->) r`被实现为`Functor`的实例,而`->`本身并不是函子。如果类型构造器可以像函数一样写成中缀形式的话,那么也可以写成`(r ->)`,实际上并不可以。看一下实现: -```haskell -instance Functor ((->) r) where - fmap f g = (\x -> f (g x)) -``` -- 在`fmap :: Functor f => (a -> b) -> f a -> f b`中带入`(->) r`就能得到在这里实例中`fmap`的类型:`fmap :: (a -> b) -> (r -> a) -> (r -> b)`。 -- 表示将`r -> a`的函数映射到`r -> b`的函数。看一下实现其实就是做了一个函数组合(有趣!),简写: -```haskell -instance Functor ((->) r) where - fmap = (.) -``` -- 很明显`fmap`可以当做函数组合来用: -```haskell ->>> :t fmap (*3) (+100) -fmap (*3) (+100) :: Num b => b -> b ->>> :t (*3) . (+100) -(*3) . (+100) :: Num c => c -> c ->>> fmap (*3) (+100) $ 1 -303 ->>> (*3) . (+100) $ 1 -303 ->>> (*3) `fmap` (+100) $ 1 -303 -``` -- `fmap`用于函数时其实就可以用来替代`.`,这很有趣,但并不实用。 -- 这里用再用盒子来比喻就不是那么恰当了,因为实现函子的实例类型是函数,函数里面装了值这种说法可能并不直观和恰当。 - -`(,) a`: -- `(,)`是一个二元组的类型构造器,它的`Kind`是`* -> * -> *`。同时也是值构造器,等价于`\x y -> (x, y)`。 -```haskell ->>> :k (,) -(,) :: * -> * -> * ->>> :t (,) -(,) :: a -> b -> (a, b) ->>> :k (,) Int -(,) Int :: * -> * ->>> :k (,) Int String -(,) Int String :: * ->>> (,) 1 2 -(1,2) - -``` -- 固定了一个类型参数后,`(,) a`的Kind是`* -> *`,被实现为函子: -```haskell -instance Functor ((,) a) where - fmap f (x, y) = (x, f y) -``` -- 多元组的类型构造器`(,,) (,,,) ...`同理: -```haskell -instance Functor ((,,) a b) where - fmap f (a, b, c) = (a, b, f c) - -instance Functor ((,,,) a b c) where - fmap f (a, b, c, d) = (a, b, c, f d) -``` -- `fmap`只应用于最后一个元素。 - -**函子和`fmap`的理解**: -- 将`fmap`柯里化地只传一个参数调用的话,可以解释为接受一个函数,并返回一个接受一个函子然后返回一个函子的函数。 -```haskell ->>> :t fmap (*3) -fmap (*3) :: (Functor f, Num b) => f b -> f b ->>> :t fmap (replicate 3) -fmap (replicate 3) :: Functor f => f a -> f [a] -``` -- 也就是说有两种解释`fmap`的说法: - - `fmap`接受一个函数和一个函子,将函子看做容器,把函数对容器上的每一个元素做应用,得到应用后的新容器。 - - 函子是一种计算上下文,`fmap :: (a -> b) -> (f a -> f b)`接受一个普通函数,并将这个函数Lift(提升)成可以在应用在新的计算上下文`f`中的函数。也就是**对函数做映射**。 - - 对于第二种理解,其中有一种情况就是函子本身就是一个函数,接收的第一个函数类型的参数被提升为可以应用在函数上的函数,应用之后得到的结果就是两个函数的组合,里层是输入的函子,外层是这个普通函数,也即是提升这件事就是做一个组合。理解清楚了也不用那么绕,结论就是这时候`fmap`就是函数的组合。 - - 因为柯里化,两种解释等价并且都正确。 - -实现函子的规定(functor law): -- 第一条:`fmap id = id`,毕竟`id = (\x -> x)`。即是一个函子应该有`fmap id a = id a = a`。 -- 第二条:`fmap (f . g) = fmap f . fmap g`,函子的`fmap`支持结合律,例: - - `famp (f . g) (Just x) = Just ((f . g) x) = Just (f (g x))` - - `fmap f . fmap g (Just x) = fmap f (fmap g (Just x)) = fmap f (Just (g x)) = Just (f (g x))` -- 看一个不满足规定,实现了`Functor`类型类的类型实例,但是不是函子的例子: -```haskell --- example, an instance of typeclass Functor, but it's not a functor -data CMaybe a = CNothing | CJust Int a deriving(Show) -instance Functor CMaybe where - fmap f CNothing = CNothing - fmap f (CJust counter x) = CJust (counter + 1) (f x) -{- ->>> fmap id (CJust 1 2) -CJust 2 2 ->>> fmap ((+1) . (*3)) (CJust 1 2) -CJust 2 7 ->>> fmap (+1) . fmap (*3) $ CJust 1 2 -CJust 3 7 --} -``` -- 第一条第二条都未满足,所以其实`CMaybe`不是函子,这里需要从概念上区分函子和代码中`Functor`的类型实例。 -- 如果我们使用一个`Functor`类型,那么会期待函子的规定(funtor laws)被遵守。如果这些规定被遵守,那么我们就知道它做`fmap`时不会做多余的事情,只是用一个函数来映射而已,基于此看到代码就能推导出它的行为,写出来的代码足够抽象也容易扩展。 -- 所有标准函数库中函子实例都遵守这两点,在自己实现`Functor`实例时也应该花时间推导一下是否满足。 -- 满足第一个规定的话一定满足第二个规定,只需要检查函子是否满足第一条规定即可。 - -### 应用函子 - -应用函子(Applicative Functors)是函子的升级版,包含在`Control.Applicative`模块中,由`Applicative`类型类定义。 - -函子中的函数: -- 我们知道Haskell函数时默认柯里化的,也就是说函数`f :: a -> b -> c`的调用`f x y`就是`(f x) y`,而单独的`f x`也是合法的调用,那么比如用`fmap (*) (Just 3)`会得到什么呢?很明显`Just (* 3)`,一个装在`Just`中的函数,类型为`Num a => Maybe (a -> a)`。 -```haskell ->>> :t fmap (*) (Just 3) -fmap (*) (Just 3) :: Num a => Maybe (a -> a) ->>> :t Just (* 3) -Just (* 3) :: Num a => Maybe (a -> a) -``` -- 也可以有更多参数: -```haskell ->>> :t fmap (\x y z w -> x + y + z + w) (Just 1) -fmap (\x y z w -> x + y + z + w) (Just 1) :: Num a => Maybe (a -> a -> a -> a) -``` -- 这些放在函子`Maybe`中的函数要怎么调用呢?当然可以使用接受函数的函数来做`fmap`,并且还可以部分参数调用,得到依然装在`Maybe`中的参数更少的函数。 -```haskell ->>> fmap (\f -> f 10) (Just (* 3)) -Just 30 ->>> :t fmap (\f -> f 10) (fmap (\x y z w -> x + y + z + w) (Just 1)) -fmap (\f -> f 10) (fmap (\x y z w -> x + y + z + w) (Just 1)) :: Num t => Maybe (t -> t -> t) -``` -- 但如果要用这些装在函子中的函数来应用到装在另一个函子中的数据呢?看起来是没有办法的,只能用模式匹配将数据或者函数抽出来后,再`fmap`,或者都抽出来之后直接调用。 - -应用函子就是做这个事情的,看一下`Control.Applicative`中的`Applicative`类型类: -```haskell -type Applicative :: (* -> *) -> Constraint -class Functor f => Applicative f where - pure :: a -> f a - (<*>) :: f (a -> b) -> f a -> f b - liftA2 :: (a -> b -> c) -> f a -> f b -> f c - (*>) :: f a -> f b -> f b - (<*) :: f a -> f b -> f a - {-# MINIMAL pure, ((<*>) | liftA2) #-} -``` -- 可以看到`Applicative`是`Functor`子类型类,也就是说只要是应用函子那么就一定是函子。 -- 注意`(<*>) :: Applicative f => f (a -> b) -> f a -> f b`函数,和`fmap :: Functor f => (a -> b) -> f a -> f b`很像,不过将函数也放到了函子`f`中。 -- 看一下`Maybe`的`Applicative`类型类实例实现: -```haskell -instance Applicative Maybe where - pure = Just - Nothing <*> _ = Nothing - (Just f) <*> something = fmap f something -``` -- `Nothing`中没有函数,所以将其应用于任何参数都是`Nothing`,而`Just f`应用于`something`,就是将函数`f`抽出来之后做了`fmap`。 -- 例子: -```haskell ->>> Just (*3) <*> Just 10 -Just 30 ->>> pure (*3) <*> Just 10 -Just 30 ->>> Just (*3) <*> Nothing -Nothing ->>> Nothing <*> Just 10 -Nothing ->>> Just reverse <*> Just "hello" -Just "olleh" -``` -- `pure :: Applicative f => a -> f a`函数包装一个参数到应用函子`f`中。经过类型推导后在这个上下文中`pure`和`Just`效果一样。 -- 上面函数仅接受一个参数,看一下其他操作: -```haskell ->>> Just (*) <*> Just 3 <*> Just 10 -Just 30 ->>> pure (+) <*> Just 3 <*> Nothing -Nothing ->>> Nothing <*> Just 3 <*> Just 10 -Nothing ->>> Just (\x y z -> x + y + z) <*> Just 1 <*> Just 2 <*> Just 3 -Just 6 ->>> :t Just (\x y z -> x + y + z) <*> Just 1 -Just (\x y z -> x + y + z) <*> Just 1 :: Num a => Maybe (a -> a -> a) ->>> liftA2 (\x y z -> x + y + z) (Just 1) (Just 2) <*> Just 3 -Just 6 -``` -- 由于柯里化,所以这里第一个例子中对应于`(<*>) :: f (a -> b) -> f a -> f b`中的类型参数`b`其实是`(Int -> Int)`,很好理解。还可以有更多参数,每个`<*>`调用都会接受一个装在`Applicative`中的参数。 -- `Applicative`还定义了`liftA2`函数用来接受两个参数的函数,但感觉完全可以被支持柯里化的`<*>`替代。 -- 显然`<*>`是左结合的,效果上来说就是在做部分参数调用,每次一个参数。 - -**`pure`** : -- `pure`是将一个普通值放到一个默认的上下文(函子,注意这里说上下文就是指计算上下文,就是指一个函子,确切说应用函子)中,是一个**最小的包含这个值的上下文(函子)**。 -- 列表的`Applicative`类型类实例的实现: -```haskell -instance Applicative [] where - pure x = [x] - fs <*> xs = [f x | f <- fs, x <- xs] -``` -- 对于列表而言,最小的上下文(函子)就是`[]`,但`[]`不包含值,不能当做`pure`。看`pure`类型声明`pure :: Applicative f => a -> f a`,对于列表,就是接受一个值,返回仅包含该值的列表。同理,`Maybe`的最小上下文是`Nothing`,但没有值,要能够包含这个值,所以`pure`的实现是`Just`。 -- `pure`也是多态的,指定类型后会根据类型推导使用不同应用函子的实现。不过不指定类型的话就没有应用函子,这个逻辑是怎么来的呢?这个`pure`是调用的哪个`data`的`pure`实现呢? -```haskell ->>> pure "hello" :: [String] -["hello"] ->>> pure "hello" :: Maybe String -Just "hello" ->>> pure "hello" -"hello" -``` -- 另外注意列表的`<*>`实现,由于列表保存多个数据,所以`<*>`结果是列表中多个函数排列运用于参数中多个值的结果的列表,相当于做了二层循环。如果参数更多,那么循环层数还会更多。前面的列表相当于外层循环,后面相当于内层。 -```haskell ->>> [(+), (-), (*)] <*> [1..3] <*> [1..3] -[2,3,4,3,4,5,4,5,6,0,-1,-2,1,0,-1,2,1,0,1,2,3,2,4,6,3,6,9] ->>> [(\x y z -> x + y + z)] <*> [1..3] <*> [1..3] <*> [1..3] -[3,4,5,4,5,6,5,6,7,4,5,6,5,6,7,6,7,8,5,6,7,6,7,8,7,8,9] -``` -- 对于列表而言,使用`<*>`是一种可以替代列表生成式的方式(本来也就是用列表生成式实现的): -```haskell --- just like list comprehension ->>> [x * y | x <- [1..5], y <- [6..10]] -[6,7,8,9,10,12,14,16,18,20,18,21,24,27,30,24,28,32,36,40,30,35,40,45,50] ->>> (*) <$> [1..5] <*> [6..10] -[6,7,8,9,10,12,14,16,18,20,18,21,24,27,30,24,28,32,36,40,30,35,40,45,50] ->>> filter (>25) $ (*) <$> [1..5] <*> [6..10] -[27,30,28,32,36,40,30,35,40,45,50] -``` - -**`<$>`**: -- 考虑`pure f <*> x`其实就等于`fmap f x`(这是Applicative laws的其中一条)。 -- 如果我们要将函数`f`放到默认的上下文(函子)中并调用其他放在应用函子中的值,可以这样写:`pure f <*> x <*> y <*> ...`,但一般不会这样写而是写成`fmap f x <*> y <*> ...`。 -- 看一下`<$>`运算符: -```haskell -Prelude> :i <$> -(<$>) :: Functor f => (a -> b) -> f a -> f b - -- Defined in ‘Data.Functor’ -infixl 4 <$> -``` -- 定义,其实就是中缀版的`fmap`。 -```haskell -f <$> x = fmap f x -``` -- 所以上面的`fmap f x <*> y <*> ...`就等价于`f <$> x <*> y <*> ...`,含义是将普通的函数运用于应用函子`x y ...`上。 -- 所以对于普通函数`f`,想要应用于应用函子中的值,可以写成`f <$> x <*> y <*> z`,如果是应用于普通值则写成`f x y z`。这种调用风格叫做**Applicative style**。 -- 回顾一下能这样做的底层逻辑是`pure f <*> x = fmap f x = f <$> x`。 -- 区分`<$> <*>`:如果函数在应用函子中,就用`<*>`,普通函数就用`<$>`。 -- 只需要加一些`<$> <*>`就能将运用于普通值的函数改写为运用在应用函子上的函数。 -```haskell ->>> (++) <$> Just "hello" <*> Just "world" -Just "helloworld" ->>> (++) "hello" "world" -"helloworld" -``` - -上面介绍了`Maybe`和`[]`的例子。看一看其他的`Applicative`实例: -```haskell -instance Monoid m => Applicative (Const m) - -- Defined in ‘Data.Functor.Const’ -instance Applicative ZipList -- Defined in ‘Control.Applicative’ -instance Monad m => Applicative (WrappedMonad m) - -- Defined in ‘Control.Applicative’ -instance Arrow a => Applicative (WrappedArrow a b) - -- Defined in ‘Control.Applicative’ -instance Applicative (Either e) -- Defined in ‘Data.Either’ -instance Applicative [] -- Defined in ‘GHC.Base’ -instance Applicative Maybe -- Defined in ‘GHC.Base’ -instance Applicative IO -- Defined in ‘GHC.Base’ -instance Applicative ((->) r) -- Defined in ‘GHC.Base’ -instance (Monoid a, Monoid b, Monoid c) => - Applicative ((,,,) a b c) - -- Defined in ‘GHC.Base’ -instance (Monoid a, Monoid b) => Applicative ((,,) a b) - -- Defined in ‘GHC.Base’ -instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’ -``` -- 抛开没见过的东西,常见的还有`Either a` `IO` `((->) r)` 各种元组。可以看到现在了解到的类型只要是函子,那都实现为了应用函子的实例。 - -`IO`: -- 实现: -```haskell -instance Applicative IO where - pure = return - a <*> b = do - f <- a - x <- b - return (f x) -``` -- `IO`中同样放函数,所以`<*>`实现就是取出函数和参数,应用后再通过`return`放到`IO`中。而`pure`实现则就是`return`,做一个不做任何事情的IO动作,可以产生某些值作为结果。 -- 考虑下面的例子: -```haskell -concatLine :: IO String -concatLine = do - a <- getLine - b <- getLine - return $ a ++ b - -concatLine' :: IO String -concatLine' = (++) <$> getLine <*> getLine -``` -- 对于`IO`来说,我们说`do`块中的IO动作是类似于顺序执行的。使用应用函子,替换成`<$> <*>`之后是存在一个执行顺序的概念的,就类似于`sequence`。 -- 如果是在做绑定IO动作(取其中的值)的事情,并且绑定之后还调用了一些函数,可以考虑使用Applicative Style。 - -`(->) r`: -- 前面提到了`fmap`用于函数作为函子的情况,不适用于盒子的比喻,`fmap`就是在做函数组合。 -- 同样地,`(->) r`也是应用函子。 -- 将`(->) r`带入到`(<*>) :: Applicative f => f (a -> b) -> f a -> f b`类型签名中替代`f`得到,注意`(->) r`转为中缀是`(r ->)`: -```haskell -<*> :: (r -> a -> b) -> (r -> a) -> (r -> b) -``` -- 看一下实现: -```haskell -instance Applicative ((->) r) where - pure x = (\_ -> x) - f <*> g = \x -> f x (g x) -``` -- 将一个值放在函数的上下文中,那么最小上下文(应用函子)就是返回这个值本身的函数,所以`pure`接受一个参数`x`返回接受一个任何参数都返回`x`的函数。 -```haskell ->>> :t pure 3 -pure 3 :: (Applicative f, Num a) => f a ->>> :t (pure 3) "hello" -(pure 3) "hello" :: Num t => t ->>> (pure 3) "hello" -3 ->>> pure 3 "hello" -3 ->>> pure 3 -3 -``` -- 函数调用左结合,括号可以省略,所以给`pure`调用再传递一个参数,通过类型推断就会调用`(->) r`的`pure`实现。 -- 观察`<*>`函数签名和实现,`f`的类型是`r -> a -> b`,`g`类型是`r -> a`,`<*>`实现中接受`r`类型参数,返回`f`函数输出,同时由`g x`作为`f`第二个参数。至于为什么这样实现还不得而知。 -- 看一个例子: -```haskell -{- ->>> f <*> g $ "10" -110.0 ->>> (\x -> f x (g x)) "10" -110.0 ->>> f "10" (g "10") -110.0 --} --- <*> :: (r -> a -> b) -> (r -> a) -> (r -> b) --- example : r String, a Int, b Double -f :: String -> Int -> Double -f s x = read s + fromIntegral x - -g :: String -> Int -g s = read s * 10 - -{- ->>> :t (+) <*> (*100) -(+) <*> (*100) :: Num a => a -> a ->>> (+) <*> (*100) $ 10 -1010 --} -``` -- 和`<$>`一起用,对于函数来说`<$>`也就是`fmap`就是函数组合: -```haskell ->>> :t (+) <$> (+3) <*> (*100) -(+) <$> (+3) <*> (*100) :: Num b => b -> b ->>> (+) <$> (+3) <*> (*100) $ 5 -508 ->>> (\x -> (x+3) + (x*100)) 5 -508 ->>> ((+) . (+3)) <*> (*100) $ 5 -508 - ->>> (\x y -> [x, y]) <$> (+1) <*> (*10) $ (10 :: Int) -[11,100] ->>> (\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5 -[8.0,10.0,2.5] -``` -- 所以说对于函数`k <$> f <*> g`的含义得到一个函数,这个函数有一个参数,它会将参数分别传给`f g`,并将结果再传给`k`。 -- 上面的代码是能够理解的,但并不算那么好理解,平时使用时我们通常不会将函数当做应用函子来用,但它确实是。 -- 技巧:对于函数类型的应用函子,用`r ->`代入类型变量`f`即可得到最终类型,用最终类型来理解,不要用盒子来类比。 - -`ZipList`: -- 考虑`[(+3), (*2)] <*> [1, 2]`这种调用,显然会调用列表的`<*>`,得到`[4, 5, 2, 4]`。 -- 那么如果想得到的结果是`[(+3) 1, (*2) 2]`,也就是列表对应元素调用,有没有办法呢?可能也会有这种需求,所以有了类型`ZipList`。 -- `ZipList`只有一个值构造器`newtype ZipList a = ZipList {getZipList :: [a]}`,包含一个列表类型的字段。并且定义为了应用函子: -```haskell -instance Applicative ZipList where - pure x = ZipList (repeat x) - ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs) -``` -- 即是对列表做包装,并将`<*>`的行为定义了第一个列表函数对第二个列表对应元素的调用。 -```haskell ->>> ZipList [(+3), (*2)] <*> ZipList [1, 2] -ZipList {getZipList = [4,4]} ->>> getZipList (ZipList [(+3), (*2)] <*> ZipList [1, 2]) -[4,4] ->>> getZipList $ (+) <$> ZipList [1, 2] <*> ZipList [2, 3] -[3,5] ->>> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat" -[('d','c','r'),('o','a','a'),('g','t','t')] -``` -- 使用时将列表用`ZipList`包装后,要取出结果则使用`getZipList`。 -- 对于列表,如果要将多个列表Zip起来,需要使用`zipWith3 zipWith4 ...`等函数,但使用Applicative Style的`ZipList`则不需要,只要将任意数量的`ZipList`用`<*>`连接起来即可,因为函数是柯里化的,单纯的数据则不能这样。 - -`liftA2`函数; -- 定义: -```haskell -liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c -liftA2 f a b = f <$> a <*> b -``` -- 应用函子比起函子要强的一点除了能应用在应用函子中的函数,还在于可以将函数或者函子中的函数应用于多个函子。通过`liftA2`函数或者Applicative Style。 -- 例子:如何将`Just 2`附加到`Just [3, 4]`上使其变成`Just [2, 3, 4]`? -```haskell ->>> (:) <$> Just 2 <*> Just [3, 4] -Just [2,3,4] -``` -- 实现接受一个装有多个应用函子的列表到一个列表的应用函子: -```haskell -sequenceA' :: Applicative f => [f a] -> f [a] -sequenceA' [] = pure [] -sequenceA' (x:xs) = (:) <$> x <*> sequenceA' xs - -sequenceA'' :: Applicative f => [f a] -> f [a] -sequenceA'' = foldr (\x xs -> (:) <$> x <*> xs) (pure []) - -sequenceA''' :: Applicative f => [f a] -> f [a] -sequenceA''' = foldr (liftA2 (:)) (pure []) -``` -- 几乎任何递归走遍整个列表然后累加的函数都可以使用`foldr/foldl`实现,和`Data.Traversable`中的`sequenceA`含义是相同的,用`liftA2`还可以进一步简化: -```haskell -sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a) -{- ->>> sequenceA [Just 1, Just 2, Just 3] -Just [1,2,3] ->>> sequenceA' [Just 1, Just 2, Just 3] -Just [1,2,3] ->>> sequenceA'' [Just 1, Just 2, Just 3] -Just [1,2,3] ->>> sequenceA [[1, 2], [3, 4]] -[[1,3],[1,4],[2,3],[2,4]] ->>> sequenceA [[1, 2], [3, 4], []] -[] --} -``` -- `liftA2`在这种场合很实用,理解为将运算符`(:)`提升(Lift)为能应用于应用函子上的函数,函数命名是非常准确的,的确是有存在意义的。 -- 当`sequenceA`接受装有函数的列表时,会回传一个返回列表的函数,此时其实就是应用于`(->) r`应用函子上,直接用`r ->`替换为类型变量`f`来得到最终类型。当有一系列函数需要应用在相同的参数上时使用`sequenceA`会非常方便,比使用`map`用接受函数的函数做映射更加方便。 -```haskell ->>> :t sequenceA [(>2), (>3)] -sequenceA [(>2), (>3)] :: (Ord a, Num a) => a -> [Bool] ->>> sequenceA [(>2), (>3)] 3 -[True,False] ->>> map (\f -> f 3) [(>2), (>3)] -[True,False] -``` -- 当使用在`IO`对象上时,`sequenceA`和`sequence`是等价的。接受一串IO动作,返回一个会执行列表中所有IO动作并将结果放在一个列表中的IO动作。 -```haskell -sequence :: (Traversable t, Monad m) => t (m a) -> m (t a) -sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a) -``` - -Applicative Functor Laws: -- 同函子一样,应用函子也有一定要遵守的定律,前面提到的`pure f <*> x = fmap f x`是其中最重要的一个: -```haskell -pure id <*> v = v -pure (.) <*> u <*> v <*> w = u <*> (v <*> w) -pure f <*> pure x = pure (f x) -u <*> pure y = pure ($ y) <*> u -``` - -总结: -- 应用函子可以用来结合不同种类的运算。 -- `<*> <$> pure`。 -- 注意列表上的non-deterministic的行为。 - -### newtype - -`data`用于定义新类型,`type`用于定义类型别名,还有一种定义新类型的方式就是`newtype`。 - -比如定义`ZipList`时可以这样定义: -```haskell -data ZipList a = ZipList { getZipList :: [a] } -``` -- 这里的目的是将`[a]`包在`ZipList`中,还可以使用`newtype`,实际的库中也是这样定义的: -```haskell -newtype ZipList a = ZipList { getZipList :: [a] } -``` - -那么`newtype`相较`data`有何异同呢? -- 使用`newtype`会告诉Haskell你只是想将一个类型包起来,有了这一点作为基础,Haskell可以将包装和解包的成本优化掉。`data`则不能。 -- `newtype`定义一个新类型时,只能定义一个值构造器,而且这个值构造器只能有一个字段。使用`data`则不限制值构造器数量和值构造器的字段数量。 -- 直观理解就是如果你要想用`newtype`包装一个类型,那么只能是一个类型的一个数据。 -- `newtype`也能使用`deriving`关键字,可以直接派生`Eq Ord Enum Bounded Show Read`。如果想对新的类型类做派生,那么包装的那个类型必须也派生了那个类型类。这很合理。 -- 配合Record Syntax,值构造器就是将内部包装的类型转为新类型的函数,字段名称就是将新类型转为内部包装类型的函数,轻易就可以取到其中的值。 -- 只能包装一个类型的一个数据不代表只能有一个类型参数,内部包装的数据本身可以有多个类型参数,例: -```haskell -newtype Pair a b = Pair {getTuple :: (a, b)} deriving(Show, Read, Eq) -``` -- 可以对`newtype`定义的类型做模式匹配,其实就和`data`一样,本质就是嵌套模式匹配: -```haskell -showPair :: (Show a, Show b) => Pair a b -> String -showPair (Pair (a, b)) = show (Pair (a, b)) -``` -- 元组在做`fmap`时只会对最后一个元素做,可以包装一层并将行为更改为对第一个元素: -```haskell -newtype Pair' b a = Pair' {getPair' :: (a, b)} deriving(Eq, Read, Show) -instance Functor (Pair' b) where - fmap f (Pair' (x, y)) = Pair' (f x, y) -{- ->>> getPair' $ fmap reverse (Pair' ("hello", 3)) -("olleh",3) --} -``` -- 看做一种有着限制的`data`定义就行。 - -`newtype`的懒惰特性: -- `undefined :: a`函数在求值时会触发异常。Haskell默认是懒惰求值,也就是真正需要值的时候才会去求(比如说要输出的时候)。 -```haskell ->>> [0, 1, 2, 3, undefined, 5, undefined] !! 5 -5 ->>> sum [0, 1, 2, 3, undefined, 5, undefined] -Prelude.undefined -``` -- 可以看到列表中元素的求值时懒惰的,下标为`5`的元素取出时,下标为`4`的元素并没有被求值。 -- `newtype`还有一个重要特点就是其对字段求值具有懒惰特性,而`data`则没有: -```haskell -data NewBool = NewBool {getNewBool :: Bool} -helloBool :: NewBool -> [Char] -helloBool (NewBool _) = "hello" -{- ->>> helloBool undefined -Prelude.undefined --} - -newtype NewBool' = NewBool' {getNewBool' :: Bool} -helloBool' :: NewBool' -> [Char] -helloBool' (NewBool' _) = "hello" -{- ->>> helloBool' undefined -"hello" --} -``` -- 能做到这一点的原因和前面能将包装和解包优化掉的原因一样,`newtype`只能有一个值构造器和一个字段,在模式匹配时不需要计算数据的值就能知道形式一定是匹配的。 -- `newtype`定义一种新的数据类型,但除了从盒子中取东西之外,更像是将一个类型转换为另一个类型。 - -`type data newtype`对比: -- `type`定义类型别名,并不定义新类型,只是给一个现有类型起一个新名字。定义别名的作用更多是增加可读性,使l类型签名更清晰。 -```haskell -{- type & data & newtype ->>> :t PhoneBook -PhoneBook :: PersonName -> PhoneNumber -> PhoneBook --} -type PersonName = String -type PhoneNumber = String -data PhoneBook = PhoneBook PersonName PhoneNumber deriving(Show, Read, Eq) -``` -- `data`就是最普通最常见的类型定义,定义一个全新的类型。 -- 当新类型只有一个值构造器和一个字段时,就可以使用`newtype`,可以获得`newtype`的优化,同时和`data`定义的类型含义差不多,只有懒惰求值的特点会有区别。注意和`data`一样,并不会自动派生内部包装的类型的基类,需要手动添加`deriving`或者实现`instance`。 - -### Monoid - -Monoid这个单词的意思是**幺半群**,**半群**则是Semigroup。至于定义是什么,暂时未知,后续再解释。 - -前面所说的类型类定义多种类型拥有的共同属性,比如`Eq`,可以判断相等的类型都应该实现为`Eq`的实例。 - -让我们将这种抽象放到函数而不仅限于`data newtype`定义的数据类型,这里考虑函数`*`和`+`的共同特性: -- 都接受两个参数,参数和返回值类型相同。 -- 存在某些值当应用于二元函数时不会改变其他值,1对于`*`,0对于`+`。 -- 都满足结合律(associativity),`5*(3*4) = 5*3*4`。 - -将这些性质抽象具体地写出来,就可以得到一个`Monoid`: -```haskell -type Monoid :: * -> Constraint -class Semigroup a => Monoid a where - mempty :: a - mappend :: a -> a -> a - mconcat :: [a] -> a - mconcat = foldr mappend mempty - {-# MINIMAL mempty #-} - -- Defined in ‘GHC.Base’ -``` -- 是一个类型类,从`Semigroup`派生。 -- 其中`mempty`就是那个相对于二元函数作为Identity的值,是一个多态的常数,`mappend`则是这个二元函数,`mconcat`对一个列表的所有元素做`mappend`(满足结合律)。 -- 实现一个`Monoid`实例时,一般实现`mempty mappend`就行,`mconcat`定义都没有问题,不过在某些情况下比如可以提供更高效的实现,依然可以实现`mconcat`。 - -`Monoid`类型类的定律(Monoid Law): -```haskell -mempty `mappend` x = x -x `mappend` mempty = x -(x `mappend` y) `mappend` z = x `mappend` (y `mappend` z) -``` -- 即是与单位元(Identity,暂且这么翻译)的运算结果还是自己,和满足结合律。 -- Haskell不会检查这些定律是否被遵守,将类型实现为`Monoid`时需要自己小心地检查他们。 -- 值得注意``a `mappend` b``和``b `mappend` a``并不需要相等。交换律并不要求被满足,`+ *`满足这一点这是他们自己的性质。 - -内置的实现了`Monoid`的类型: -- 列表:对于列表而言,`mconcat`就是`concat`,这个二元运算就是`++`操作。 -```haskell -instance Monoid [a] where - mempty = [] - mappend = (++) -{- ->>> mempty :: [a] -[] ->>> [1, 2] `mappend` [3, 4] -[1,2,3,4] ->>> mconcat [[1, 2], [3, 4], [5, 6, 7]] -[1,2,3,4,5,6,7] --} -``` -- `Product Sum`,对于整数浮点数`+ *`都满足Monoid Law,那么如何选择呢?答案是不做选择,`Data.Monoid`导出了两个类型`Product a/Sum a`,都是用`newtype`定义的,实现了常见的`Num Show Read Eq Ord `等类型类,可以用其来**包装**`Num`实例类型的数据。`Product a/Sum a`的`Monoid`实现中分别定义了`mappend`为`*/+`。使用时选择要用哪一个来包装,而更一般的`Num`则没有从`Monoid`派生。 -```haskell -newtype Product a = Product {getProduct :: a} - -- Defined in ‘Data.Semigroup.Internal’ - ... -instance Num a => Monoid (Product a) where - mempty = Product 1 - Product x `mappend` Product y = Product (x * y) - -newtype Sum a = Sum {getSum :: a} - -- Defined in ‘Data.Semigroup.Internal’ - ... -instance Num a => Monoid (Sum a) where - mempty = Sum 0 - Sum x `mappend` Sum y = Sum (x + y) -{- ->>> getSum . mconcat . map Sum $ [1, 2, 3] -6 ->>> getProduct . mconcat . map Product $ [1, 10, 100] -1000 --} -``` -- 对于`Bool`类型,`&& ||`运算符都满足`Monoid`的规则,所以定义了两个包装类型`Any All`,实现就类似于下面这样,用法类似,包装就行: -```haskell -newtype Any = Any {getAny :: Bool} -instance Monoid Any where - mempty = Any False - Any x `mappend` Any y = Any (x || y) - -newtype All = All {getAll :: Bool} -instance Monoid All where - mempty = All True - All x `mappend` All y = All (x && y) -{- ->>> getAll . mconcat . map All $ [True, True, False] -False ->>> getAny . mconcat . map Any $ [False, False, True] -True --} -``` -- `Ordering`类型是比较的结果,也是`Monoid`的实例。实现非常符合直觉,这个实现的含义就是:对于列表来说,在进行比较时,左边的元素优先级更高,如果左边小于/大于,那么最终结果就是小于/大于,左边元素相等那么继续比较后续元素。Monoid Law都是满足的。这个`Monoid`会用于什么场合呢?可以用于有多个比较因素时,用`mappend`或者`mconcat`连接起来构成最终的关系。 -```haskell -data Ordering = LT | EQ | GT -instance Monoid Ordering where - mempty = EQ - LT `mappend` _ = LT - EQ `mappend` y = y - GT `mappend` _ = GT - --- usage -lengthCompare :: String -> String -> Ordering -lengthCompare x y = (length x `compare` length y) `mappend` (x `compare` y) --- just write as an example, not necessary -lengthCompare' :: String -> String -> Ordering -lengthCompare' x y = mconcat $ [compare `on` length, compare, compare `on` map toUpper] <*> [x] <*> [y] -{- ->>> lengthCompare "hello" "world" -LT ->>> lengthCompare "hello1" "world" -GT ->>> lengthCompare' "hello" "world" -LT ->>> mconcat (zipWith compare "abcd" "abce") -LT --} -``` -- `Maybe a`类型包装一个数据,如果这个数据的类型`a`是`Monoid`,那么也可以将`Maybe a`实现为`Monoid`: -```haskell -instance Monoid a => Monoid (Maybe a) where - mempty = Nothing - Nothing `mappend` m = m - m `mappend` Nothing = m - Just m1 `mappend` Just m2 = Just (m1 `mappend` m2) -{- ->>> Nothing `mappend` Just "hello" -Just "hello" ->>> Just "hello" `mappend` Nothing -Just "hello" ->>> Just "hello" `mappend` Just "world" -Just "helloworld" --} -``` -- `First a / Last a`,`Maybe a`除了包装其中的`Monoid`这种实现方式还可以有其他实现。如果其中的数据类型不是`Monoid`,我们可以选择将`mappend`实现为丢弃其中某个数据,留下前者或者后者,留下前者则是`First a`,后者则是`Last a`,当然如果其中有`Nothing`那会优先留下非`Nothing`的值。`Maybe a`已经有了实现,他们都是`Maybe a`的包装。 -```haskell -newtype First a = First { getFirst :: Maybe a } - deriving (Eq, Ord, Read, Show) -instance Monoid (First a) where - mempty = First Nothing - First (Just x) `mappend` _ = First (Just x) - First Nothing `mappend` x = x -{- First a & Last a ->>> mempty :: First a -First {getFirst = Nothing} ->>> First Nothing `mappend` First (Just 1) -First {getFirst = Just 1} ->>> First (Just 1) `mappend` First Nothing -First {getFirst = Just 1} ->>> First (Just 1) `mappend` First (Just 2) -First {getFirst = Just 1} - ->>> mempty :: Last a -Last {getLast = Nothing} ->>> Last Nothing `mappend` Last (Just 1) -Last {getLast = Just 1} --} -``` -- `foldl foldr`可以用来折叠`Foldable`,`[a]`也是一种`Foldable`,前面多用于折叠`[a]`,其实还可以用于其他实现了`Foldable`的数据类型。注意其中的`fold foldMap foldMap'`方法,他们会使用`Monoid`的`mappend`方法进行折叠,可以是数据本来就是`Monoid`,也可以是使用传入函数将数据转换为`Monoid`。 -```haskell -type Foldable :: (* -> *) -> Constraint -class Foldable t where - fold :: Monoid m => t m -> m - foldMap :: Monoid m => (a -> m) -> t a -> m - foldMap' :: Monoid m => (a -> m) -> t a -> m - foldr :: (a -> b -> b) -> b -> t a -> b - foldr' :: (a -> b -> b) -> b -> t a -> b - foldl :: (b -> a -> b) -> b -> t a -> b - foldl' :: (b -> a -> b) -> b -> t a -> b - foldr1 :: (a -> a -> a) -> t a -> a - foldl1 :: (a -> a -> a) -> t a -> a - toList :: t a -> [a] - null :: t a -> Bool - length :: t a -> Int - elem :: Eq a => a -> t a -> Bool - maximum :: Ord a => t a -> a - minimum :: Ord a => t a -> a - sum :: Num a => t a -> a - product :: Num a => t a -> a - {-# MINIMAL foldMap | foldr #-} - -- Defined in ‘Data.Foldable’ -instance Foldable (Const m) -- Defined in ‘Data.Functor.Const’ -instance Foldable [] -- Defined in ‘Data.Foldable’ -instance Foldable Sum -- Defined in ‘Data.Foldable’ -instance Foldable Product -- Defined in ‘Data.Foldable’ -instance Foldable Maybe -- Defined in ‘Data.Foldable’ -instance Foldable Last -- Defined in ‘Data.Foldable’ -instance Foldable First -- Defined in ‘Data.Foldable’ -instance Foldable (Either a) -- Defined in ‘Data.Foldable’ -instance Foldable Dual -- Defined in ‘Data.Foldable’ -instance Foldable f => Foldable (Ap f) - -- Defined in ‘Data.Foldable’ -instance Foldable f => Foldable (Alt f) - -- Defined in ‘Data.Foldable’ -instance Foldable ((,) a) -- Defined in ‘Data.Foldable’ -instance Foldable ZipList -- Defined in ‘Control.Applicative’ -``` -- 二叉树的`Foldable`实现例子: -```haskell -data Tree a = EmptyTree | TreeNode a (Tree a) (Tree a) deriving(Show, Read, Eq) -instance Foldable Tree where - foldMap f EmptyTree = mempty - foldMap f (TreeNode x l r) = foldMap f l `mappend` f x `mappend` foldMap f r - -testTree :: Tree Integer -testTree = TreeNode 5 - (TreeNode 3 - (TreeNode 1 EmptyTree EmptyTree) - (TreeNode 6 EmptyTree EmptyTree) - ) - (TreeNode 9 - (TreeNode 8 EmptyTree EmptyTree) - (TreeNode 10 EmptyTree EmptyTree) - ) -{- ->>> foldl (+) 0 testTree -42 ->>> foldr (*) 1 testTree -64800 ->>> foldMap (\x -> [x]) testTree -[1,3,6,5,8,9,10] ->>> getAny $ foldMap (\x -> Any $ x > 10) testTree -False ->>> getAll $ foldMap (\x -> All $ x > 5) testTree -False --} -``` - -## Monad - -`Functor`函子代表可以被映射(使用`fmap`)的值,将概念提升到`Applicative`应用函子,代表一种具有上下文的类型,可以用函数操作同时保有其上下文(这里的上下文可以通过`Applicative`将值包了起来,值的这一层包裹就叫做上下文这种说法来理解)。 - -注意说法上的细微差别,`Functor`一般称其能被map over,提升到`Applicative`时才说其具有上下文,一个Applicative value可以被看做一个附加了上下文的值,`pure`包装就是用来给其附加上下文的。然后使用`<$> <*>`就可以用普通函数或者具有上下文的函数操作具有上下文的值,同时保有上下文到结果中。 - -然后对于特定的`Applicative`,上下文含义不同,`Maybe a`代表可能失败的计算,`[a]`代表同时有多种结果的计算(non-deterministic),而`IO a`代表有副作用的计算。 - -现在一个新的问题是如果有一个具有上下文的值`m a`,和一个接受普通值返回具有上下文的值的函数`a -> m b`,如何将函数`a -> m b`应用于值`m a`上得到具有上下文的值`m b`。为此定义新的类型类`Monad`: -```haskell -type Monad :: (* -> *) -> Constraint -class Applicative m => Monad m where - (>>=) :: m a -> (a -> m b) -> m b - (>>) :: m a -> m b -> m b - return :: a -> m a - {-# MINIMAL (>>=) #-} - -- Defined in ‘GHC.Base’ -instance Monad (Either e) -- Defined in ‘Data.Either’ -instance Monad [] -- Defined in ‘GHC.Base’ -instance Monad Maybe -- Defined in ‘GHC.Base’ -instance Monad IO -- Defined in ‘GHC.Base’ -instance Monad ((->) r) -- Defined in ‘GHC.Base’ -instance (Monoid a, Monoid b, Monoid c) => Monad ((,,,) a b c) - -- Defined in ‘GHC.Base’ -instance (Monoid a, Monoid b) => Monad ((,,) a b) - -- Defined in ‘GHC.Base’ -instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’ -``` -其中的`(>>=) :: Monad m => m a -> (a -> m b) -> m b`即是我们要的函数,这个函数称呼为bind。 - -`Monad`中文则翻译为**单子**,也就是开头所说的单子是自函子范畴上的幺半群的那个单子。 - -### Monad类型类 - -`Monad`是`Applicative`的子类型类,所以一个`Monad`单子也是一个应用函子,同样也是一个函子。对于具体的单子实例类型而言,实现`Monad`实例时必须要考虑其上下文的含义。 - -早期的`Monad`类型类定义看起来像这样: -```haskell -class Monad m where - return :: a -> m a - (>>=) :: m a -> (a -> m b) -> m b - (>>) :: m a -> m b -> m b - x >> y = x >>= \_ -> y - fail :: String -> m a - fail msg = error msg -``` -- 这里并没有将`Applicative`加到`m`的类型约束中,实际上是有的,这里没有写出来。在Haskell早期,人们没有想到应用函子适合被放进语言中,那时暂时还没有约束。但确实每个`Monad`都是应用函子,即便没有这么声明。 -- `return`函数就像`Applicative`的`pure`,做一样的事情,类型是`return :: Monad m => a -> m a`,接受普通值并将其放在最小的上下文中。在介绍`IO`时已经遇到过,再次提醒`return`并不表示函数返回,不改变任何函数的执行流程(况且Haskell中函数并非执行流程而是数据的变换),只是将值做包装。 -- 现在`fail :: MonadFail m => String -> m a`函数不在`Monad`中,而是`Monad`的子类型类`MonadFail`中。被用在处理Haskell错误语法的情况,当前不用在意。 -- `>>`已经有了默认实现,一般情况我们不会需要去考虑覆写它。 - -`Maybe`的实现: -- `Maybe a`同样也是单子。 -- 实现: -```haskell -instance Monad Maybe where - return x = Just x - Nothing >>= f = Nothing - Just x >>= f = f x - fail _ = Nothing -``` -- 例子: -```haskell ->>> return 1 :: Maybe Int -Just 1 ->>> Nothing >>= (\x -> Just x) -Nothing ->>> Just 10 >>= (\x -> Just $ x * x) -Just 100 -``` - -### Monad应用 - -一个使用`Maybe`表示可能失败的上下文的例子,来自Haskell趣学指南: -- 大意是一个人在走钢丝,拿着一根长竿,这根竿两端会不时随机飞来一些鸟停在这里或者随机飞走,当两边鸟的数量差达到3时,这个人就会掉下去。 -- 模拟这个过程,得到最终结果,如果掉下去了那么就表示已经失败了,不会有鸟在飞过来,用`Maybe`来表示很合理。 -```haskell -type Birds = Int -type Pole = (Birds, Birds) - -landLeft :: Birds -> Pole -> Maybe Pole -landLeft n (left, right) - | abs (left + n - right) < 4 = Just (left + n, right) - | otherwise = Nothing - -landRight :: Birds -> Pole -> Maybe Pole -landRight n (left, right) - | abs (right + n - left) < 4 = Just (left, right + n) - | otherwise = Nothing -``` -- 最后整个模拟过程的调用链条中,上一步输出是`Maybe Pole`,而下一步的要求输入是`Pole`,使用`Maybe`的`Monad`特性,就可以使用`>>=`连接起来。 -```haskell ->>> :t landLeft 2 -landLeft 2 :: Pole -> Maybe Pole ->>> Nothing >>= landLeft 1 -Nothing - ->>> landLeft 1 (0, 0) >>= landLeft 3 >>= landRight 2 >>= landRight 1 -Nothing ->>> landLeft 1 (0, 0) >>= landRight 3 >>= landLeft 2 >>= landRight 1 -Just (3,4) ->>> return (0, 0) >>= landLeft 1 >>= landRight 3 >>= landLeft 2 >>= landRight 1 -Just (3,4) -``` -- 将初值用`return`函数附加了上下文之后也可以加到调用链条中。 - -使用`>>=`为我们省去了检查上一步结果的繁琐步骤,`Maybe`的实现含义就是遇到`Nothing`就返回`Nothing`,一直都是有效值就持续地用`Just`进行传递。 - -再看一下`>>`运算符: -```haskell -x >> y = x >>= \_ -> y -``` -- 和`>>=`很类似,但是它会调用`>>=`传入第一个参数,第二个参数的函数不考虑参数并直接返回`>>`的第二个参数值。 -- 也就是说`>>`做和`>>=`类似的事情,但不传递参数,而是保留最后一个有效的结果。 -- 对于`Maybe`来说,包含可能失败的上下文语义,具体含义就是如果中途某一步失败,那么结果就是`Nothing`,如果每一步都成功,那么最终结果就是最后一步的结果。中间的结果不会被传递。 -```haskell ->>> :t (>>) -(>>) :: Monad m => m a -> m b -> m b ->>> Nothing >> Just 1 -Nothing ->>> Just 1 >> Just 2 >> Just 3 -Just 3 ->>> Just 1 >> Just 2 >> Nothing -Nothing -``` - -### do表示法 - -介绍IO时说过了`do`表示法,这是一个语法糖,但并未揭示它的细节。其实`do`串联多个`IO`对象的本质就是使用了`>>=`。 - -在前面所述的调用链中,如果要插入一些其他的值(这些值也具有上下文,所以也需要使用`>>=`传递),比如某个函数需要两个参数,一个是`>>=`前面的参数,一个是另一个具有上下文的值。那么可以使用lambda: -```haskell ->>> Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y))) -Just "3!" ->>> Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y))) -Nothing -``` -将第一个例子写作多行: -```haskell -foo :: Maybe [Char] -foo = Just 3 >>= (\x -> - Just "!" >>= (\y -> - Just (show x ++ y))) -``` -为了简化这种写法,摆脱烦人的lambda,于是有了`do`表示法,将`foo`写成等价的`do`表示法: -```haskell -foo' :: Maybe [Char] -foo' = do - x <- Just 3 - y <- Just "!" - Just (show x ++ y) -{- ->>> foo -Just "3!" ->>> foo' -Just "3!" --} -``` -这也说明了为什么`do`表达式结果是最后一个式子的值,它串联的前面的所有结果,是最终的结果。 - -用`do`来表示上一个走钢丝例子中的`return (0, 0) >>= landLeft 1 >>= landRight 3 >>= landLeft 2 >>= landRight 1`: -```haskell -bar :: Maybe Pole -bar = do - start <- return (0, 0) -- let start = (0, 0) - second <- landLeft 1 start - third <- landRight 3 second - fourth <- landLeft 2 third - landRight 1 fourth -{- ->>> bar -Just (3,4) --} -``` - -总结: -- 很显然,如果其中某一步骤没有使用`<-`,其实就是使用`>>`而不是`>>=`。这很好理解。 -- **`do`表示法就是`Monad`的`>>= >>`运算符和lamdba的语法糖**。 -- 在`<-`左端可以使用模式匹配,因为本质上是lambda的参数,参数当然是可以使用模式匹配的。 -- 使用`<-`的语句不能作为最后一个语句,因为本质上它只定义了lambda的参数,还没有定义函数体。 -- 需要注意在每一步过程中结果类型都是可以发生改变的,并不需要和`>>=`的参数保持一致,只需要每一步输出类型和下一步输入类型一致,并且最终结果和返回值类型一致即可。 - -使用的选择: -- 具体是使用`>>= >>`还是用`do`其实主要看习惯问题。等价的怎么选都行。 -- 一般来说如果每一步都只使用(甚至不使用)上一步的结果,并且还要是作为最后一个参数(不然会需要使用lambda),没有额外的值的参与,那么使用`>>= >>`就可以很清晰。(走钢丝的例子) -- 如果有额外的值的参与,或者某一步的值并不仅仅直接用于下一步而是用在后面几步之后,或者使用的值不是最后一个参数,改成`do`会更好一些。(各种复杂的IO动作) - -### Monad实例 - -列表`[a]`: -- 就像应用函子中讨论的,列表提供了一种**不确定性(Non-determinism)含义的上下文**。 -- 实现: -```haskell -instance Monad [] where - return x = [x] - xs >>= f = concat (map f xs) - fail _ = [] -``` -- 目前来说`fail`实现在`MonadFail`中,不用管,`return`同`pure`接受一个值将其放到最小的上下文中。 -- `>>=`提供了类似于`<*>`的non-deterministic(不确定)的计算结果。`>>=`将`xs`中所有参数应用于函数`f`之后将得到的所有列表连接起来。 -- `<*>`的不确定性是由`<*>`连接的多个列表类型参数中值的排列(多层循环)导致的,而`>>=`则是输入列表中的一个值,通过`a -> [a]`的函数变成了多个值导致的(每一层中一个值都会扩展为多个值)。 -- 例子: -```haskell ->>> [1, 2, 3] >>= (\x -> [x, -x]) >>= (\x -> [(x, x * 10)]) -[(1,10),(-1,-10),(2,20),(-2,-20),(3,30),(-3,-30)] ->>> [(x, x * 10) | n <- [1, 2, 3], x <- [n, -n]] -[(1,10),(-1,-10),(2,20),(-2,-20),(3,30),(-3,-30)] ->>> [] >>= (\x -> ["hello", "world"]) -[] ->>> [1, 2, 3] >>= (\x -> []) -[] -``` -- 如果输入或者某一步输出是空列表,那么调用链的最终结果都会是`[]`,这就像`Maybe`的`Nothing`,执行任何`map f []`对于任何函数`f`结果都会是空列表。 -- 用`do`表示法改写第一个例子: -```haskell -listOfTuples :: [Integer ] -> [(Integer, Integer)] -listOfTuples l = do - n <- l - m <- [n, -n] - return (m, m * 10) -{- ->>> listOfTuples [1, 2, 3] -[(1,10),(-1,-10),(2,20),(-2,-20),(3,30),(-3,-30)] --} -``` -- 上一个例子中下一步直接使用了上一步返回值,经过几步之后再用也同样可以: -```haskell -listOfTuples' :: [(Int,Char)] -listOfTuples' = do - n <- [1,2] - ch <- ['a','b'] - return (n,ch) -{- ->>> listOfTuples' -[(1,'a'),(1,'b'),(2,'a'),(2,'b')] ->>> [(n, ch) | n <- [1, 2], ch <- ['a', 'b']] -[(1,'a'),(1,'b'),(2,'a'),(2,'b')] --} - -listOfTuples'' :: [(Int, Char)] -listOfTuples'' = [1, 2] >>= (\n -> - ['a', 'b'] >>= (\ch -> - [(n, ch)])) -{- ->>> listOfTuples'' -[(1,'a'),(1,'b'),(2,'a'),(2,'b')] --} - -``` -- 实际使用时,其实就表示一种Non-deterministic的上下文,用`do`表示法亦可。 -- 我们会发现都可以等级为对应的List Comprehension,其实**List Comprehension就是`>>=`函数在列表这个`Monad`上的语法糖**。列表生成式中不需要最后`return`而是将最终列表中元素放在了`|`前。 -- 无论用`do`表示法还是List Comprehension最终都会转换成`>>=`来计算。 -- 列表生成式中可以允许条件来对结果进行筛选,这一点要怎么在`>>=`串联的函数调用中要怎么做到呢? - - 可以考虑使用`Control.Monad`中的`guard`函数和`MonadPlus`函数: - ```haskell - type MonadPlus :: (* -> *) -> Constraint - class (Alternative m, Monad m) => MonadPlus m where - mzero :: m a - mplus :: m a -> m a -> m a - -- Defined in ‘GHC.Base’ - instance MonadPlus [] -- Defined in ‘GHC.Base’ - instance MonadPlus Maybe -- Defined in ‘GHC.Base’ - instance MonadPlus IO -- Defined in ‘GHC.Base’ - - instance MonadPlus [] where - mzero = [] -- same as mempty in Monoid - mplus = (++) -- same as mappend in Monoid - - -- guard :: Alternative f => Bool -> f () defined in Control.Monad - guard' :: MonadPlus m => Bool -> m () - guard' True = return () -- for [] return [()] - guard' False = mzero -- for [] return [] - ``` - - `[]`同样是`MonadPlus`的实例,`MonadPlus`函数的`mzero mplus`对于列表来说就是`Monoid`的`mempty mappend`的同义词,实现也一样。用在列表上时`guard`函数的返回类型是`[()]`。 - - `guard`使用: - ```haskell - >>> guard (5 > 2) :: [()] - [()] - >>> guard (2 > 3) :: [()] - [] - >>> [()] >> return "cool" :: [String] - ["cool"] - >>> [] >> return "cool" :: [String] - [] - >>> [(), ()] >> return "cool" :: [String] - ["cool","cool"] - >>> [1..50] >>= (\x -> guard ('7' `elem` show x) >> return x) - [7,17,27,37,47] - >>> [x | x <- [1..50], '7' `elem` show x] - [7,17,27,37,47] - >>> do x <- [1..50]; guard ('7' `elem` show x); return x - [7,17,27,37,47] - ``` - - `guard`实现,对于列表来说,输入`True`返回`[()]`,输入`False`则返回空列表`[]`,如果是非空列表,经过`>>`之后输入的空元组会被忽略,此时是一个成功状态,`return x`得到`[x]`。而输入为`[]`时是一个失败状态(实现上都是`map`然后`concat`)结果会为空`[]`。即实现了筛选功能。 - - 当然也可以使用`do`表示法。 - - 当然就这个例子而言,一个简单的`if-then-else`当然也可以做到: - ```haskell - >>> [1..50] >>= (\x -> if ('7' `elem` show x) then [x] else []) - [7,17,27,37,47] - ``` -- 另外,对于列表来说,`>>`运算符,将左边列表的所有元素,替换为结果列表中的一个或多个元素(即替换为列表后再`concat`)。就是说左边列表的元素类型和值不重要,一般来说就像`guard`一样可以使用空元组`()`来占位。 -```haskell ->>> [1..5] >> return 1 -[1,1,1,1,1] ->>> [1..5] >> return [()] -[[()],[()],[()],[()],[()]] ->>> [1..5] >> [0, 1, 2] -[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2] -``` - -一个例子: -- 查找国际象棋的骑士(就像中国象棋中的马,一个方向走1格,一个方向走两格)走3步可能到达的所有位置。 -- 利用列表上下文的不确定性来做。 -```haskell -import Data.List -import Control.Monad --- example: find all possible position for knight to move in chess --- valid chess posotin : row from 1 to 8, from column 1 to 8 -type KnightPos = (Int, Int) -moveKnight :: KnightPos -> [KnightPos] -moveKnight (r, c) = do - (a, b) <- [(a, b) | a <- [-1, 1], b <- [-2, 2]] -- offset - (or, oc) <- [(a, b), (b, a)] -- all possible offset - (rr, rc) <- [(r + or, c + oc)] -- result - guard (rr `elem` [1..8] && rc `elem` [1..8]) -- filter - return (rr, rc) - -moveKnight3 :: KnightPos -> [KnightPos] -moveKnight3 start = nub $ moveKnight start >>= moveKnight >>= moveKnight - -canReachIn3 :: KnightPos -> KnightPos -> Bool -canReachIn3 start end = end `elem` moveKnight3 start -{- ->>> moveKnight (6, 2) -[(4,1),(5,4),(8,1),(4,3),(7,4),(8,3)] ->>> moveKnight (8, 1) -[(7,3),(6,2)] ->>> moveKnight3 (6, 2) -[(2,1),(1,2),(2,5),(5,2),(4,1),(1,4),(4,5),(5,4),(3,4),(4,3),(3,2),(7,2),(6,1),(6,5),(7,4),(8,1),(8,3),(2,3),(6,3),(3,8),(2,7),(5,8),(6,7),(8,5),(1,6),(4,7),(5,6),(7,8),(8,7),(3,6),(7,6)] ->>> (6,2) `canReachIn3` (6,1) -True ->>> (6,2) `canReachIn3` (7,3) -False --} -``` - -### Monad Law - -正如函子和应用函子等各种类型类,单子也有自己的定律需要遵守: -```haskell -return x >>= f = f x -- Left Identity -m >>= return = m -- Right Identity -(m >>= f) >>= g = m >>= (\x -> f x >>= g) -- Associativity -``` -- 前两者描述的是`return`的行为,`return`将普通值转换为具有上下文的值,这两条非常重要。前者表示将一个值放到最小`Monad`上下文中再通过`>>=`传递给`f`不应该与直接调用`f`有任何差别。后者表示一个单子通过`>>=`应用于`return`应该就是自己。 -- 最后的结合律则是说明当我们用`>>=`将一串monadic function串联起来,他们的先后顺序不应该有影响。 - -可以定义一个运算符来将两个Monadic functin复合起来: -```haskell -(<=<) :: Monad m => (a -> m b) -> (t -> m a) -> t -> m b -f <=< g = \x -> g x >>= f -``` -- `g`是里层,`f`是外层,`<=<`由里层指向外层。 -- `Control.Monad`中定义同样含义的运算符`>=>`,只不过参数是反过来的,注意区分: -```haskell ->>> :t (Control.Monad.>=>) -(Control.Monad.>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c -``` -- 使用`<=<`运算符来描述Moand Law: -```haskell -f <=< return = f -return <=< f = f -m >>= f >>= g = m >>= (f <=< g) -``` - -其实就很像普通函数的: -```haskell -f . id = f -id . f = f -(f . g) . h = f . (g . h) -``` - -## More Monad - -已经详细介绍了`Maybe []`,而`IO`这个`Monad`其实前面已经说过了,不需要再赘述。我们需要了解更多的`Monad`以培养对`Monad`的直觉,直觉非常重要。 - -下面介绍的`Monad`都在包[`mtl`](https://github.com/haskell/mtl)中,这个包是GHC内置的(用`[stack exec] ghc-pkg list`查看已安装的包),我的本地版本是`mtl-2.2.2`,模块名都是`Control.Monad.xxx`。这个包叫做The Monad Transformer Library,是一系列`Monad`类型类的集合。 - -### Writer - -对比`Maybe`是可能失败的上下文,`[]`是加入不确定性语义的上下文,`Writer`则是加进了一个附加值的上下文,就像日志一样,`Writer`可以在计算的同时搜索log记录,汇集成一个最终的log附加到结果上。 - -模拟`Writer`: -- 考虑附加其上的信息不仅可以是字符串、列表,任何`Monoid`都可以。 -- 将附有上下文的数据用`Monoid m => (a, m)`来表示,那么`>>=`的实现就类似于: -```haskell --- implement a Writer-like >>= function -applylog :: Monoid m => (a, m) -> (a -> (b, m)) -> (b, m) -applylog (x, log) f = let (y, newLog) = f x in (y, log `mappend` newLog) -``` -- 使用: -```haskell -type Food = String -type Price = Sum Int -addDrink :: Food -> (Food,Price) -addDrink "beans" = ("milk", Sum 25) -addDrink "jerky" = ("whiskey", Sum 99) -addDrink _ = ("beer", Sum 30) -{- ->>> ("jerky", Sum 25) `applyLog` addDrink -("whiskey",Sum {getSum = 124}) ->>> ("beef", Sum 5) `applyLog` addDrink -("beer",Sum {getSum = 35}) --} -``` - -`Writer`类型: -- `Control.Monad.Writer`模块,`Writer w`是`WriterT w Identity`的别名。 -```haskell -type Writer :: * -> * -> * -type Writer w = WriterT w Identity :: * -> * -type WriterT :: * -> (* -> *) -> * -> * -newtype WriterT w m a = WriterT {runWriterT :: m (a, w)} -instance [safe] (Monoid w, Monad m) => Monad (WriterT w m) -``` -- 等价定义就像是这样,基本就和`applyLog`一个意思: -```haskell -instance (Monoid w) => Monad (Writer w) where - return x = Writer (x, mempty) - (Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v') -``` -- `return`附加的信息是空值`mempty`,最小的上下文就是没有附加的信息。 -- 类型`Writer w a`中,`w`是附加的信息的类型,是一个`Monoid`,`a`是其中的数据的类型。 -- 文档:[Control.Monad.Writer.Lazy](https://www.stackage.org/haddock/lts-18.18/mtl-2.2.2/Control-Monad-Writer-Lazy.html) -- 方法: -```haskell -writer :: MonadWriter w m => (a, w) -> m a -runWriter :: Writer w a -> (a, w) -execWriter :: Writer w a -> w -mapWriter :: ((a, w) -> (b, w')) -> Writer w a -> Writer w' b -``` -- `writer`生成一个函数。 -- `runWriter`得到`(result, output)`形式输出。 -- `execWriter`就等价于`execWriter m = snd (runWriter m)`,只取出其中累加的信息。 -- `mapWriter`则使用函数将`(result, output)`两者都进行计算。 -- `MonadWriter`是`WriterT`实现的类型类,具体信息查看文档。 -```haskell ->>> writer (1, Sum 0) :: Writer (Sum Int) Int -WriterT (Identity (1,Sum {getSum = 0})) ->>> runWriter (return 0 :: Writer String Int) -(0,"") ->>> execWriter (writer (10, "hello") :: Writer String Int) -"hello" ->>> runWriter . mapWriter (\(a, Sum b) -> (show a, show b)) $ (writer (1, Sum 0)) -("1","0") -``` - -使用: -- 可以通过`do`表示法来用,如果就是想在某个时间点放入一个Monoid值,那么可以使用`tell :: MonadWriter w m => w -> m ()`(可以看到返回一个包装空元组的Monad,可以通过`>>`或者`do`来用): -```haskell -logNumber :: Int -> Writer [String] Int -logNumber x = writer (x, ["Got a number: " ++ show x]) - -multWithLog :: Writer [String] Int -multWithLog = do - a <- logNumber 3 - b <- logNumber 5 - tell ["hello"] - c <- logNumber 2 - return (a * b * c) -{- ->>> runWriter multWithLog -(30,["Got a number: 3","Got a number: 5","hello","Got a number: 2"]) --} -``` -- 例子,计算最大公约数的同时记录计算过程: -```haskell -gcd' :: Int -> Int -> Writer [String] Int -gcd' a b - | b == 0 = do - tell ["Finished with : " ++ show a] - return a - | otherwise = do - tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)] - gcd' b (a `mod` b) -{- -ghci> mapM_ putStrLn $ snd $ runWriter $ gcd' 98 51 -98 mod 51 = 47 -51 mod 47 = 4 -47 mod 4 = 3 -4 mod 3 = 1 -3 mod 1 = 0 -Finished with : 1 --} -``` - -使用Difference List: -- 注意其中`[]`的`++`运算符的效率,列表是从右往左递归定义的,所以`a ++ (b ++ (c ++ d))`会很高效,而`((a ++ b) ++ c) ++ d`则相对效率不够好。上面的`gcd'`没有这种问题,但这点是需要注意的,如果在递归中先计算`gcd'`再`tell`则会有效率问题。 -- 为了能够总是在列表的`++`操作上得到最好的效率,可以定义一个新的类型差异列表,将列表包装一下,实现无论怎么附加列表都是`(a ++ b) ++ c) ++ d`的效果。 -- 类型定义: -```haskell -newtype DiffList a = DiffList {getDiffList :: [a] -> [a]} - -toDiffList :: [a] -> DiffList a -toDiffList xs = DiffList (xs++) - -fromDiffList :: DiffList a -> [a] -fromDiffList (DiffList f) = f [] -``` -- 将列表`xs`转换为`xs++`函数包装在`DiffList`中,通过传入`[]`就可以得到内部的原始列表,通过定义新函数来实现转换而不是使用模式匹配。 -- 将其定义为`Monoid`: -```haskell --- declare DiffList as a Monoid -instance Semigroup (DiffList a) where - DiffList f <> DiffList g = DiffList (f . g) - -instance Monoid (DiffList a) where - mempty = DiffList ([]++) - DiffList f `mappend` DiffList g = DiffList (f . g) -``` -- `Monoid`派生自`Semigourp`,实现`Monoid`同时需要实现`Semigourp`,可以看到`DiffList`的`mempty`是附加一个空列表的函数,而`mappend`是函数组合。每次`mappend`,新的列表都会通过函数函数组合调用的方式附加到列表最前面,不会有从前往后附加这种情况出现。 -- 效率测试: -```haskell --- test performance of DiffList -finalCountDown :: Int -> Writer (DiffList String) () -finalCountDown 0 = do - tell (toDiffList ["0"]) -finalCountDown x = do - finalCountDown (x-1) - tell (toDiffList [show x]) - -finalCountDown2 :: Int -> Writer [String] () -finalCountDown2 0 = do - tell ["0"] -finalCountDown2 x = do - finalCountDown2 (x-1) - tell [show x] - -test1 :: IO () -test1 = mapM_ putStrLn . fromDiffList . snd . runWriter $ finalCountDown 50000 -test2 :: IO () -test2 = mapM_ putStrLn . snd . runWriter $ finalCountDown2 50000 - -main :: IO () -main = test1 --- main = test2 -``` -- 这里从一个数计数直到`0`,`Writer`值保存为`()`不关心,附加的信息使用`[]`或者`DiffList`的`mappend`来做。最终得到的`Writer`是`0-x`字符串的列表。 -- 执行`test1 test2`,参数`50000`时能够感受到非常明显的性能差距。其实直观理解上来说就是$O(N)$和$O(N^2)$时间复杂度的差别。 -- 做到这一点依赖于Haskell的懒惰求值的特性,函数的调用只是数据的变换过程,真正需要数据时才会计算。因为`finalCountDown`是将数组累加变成了函数的组合,没有实际地进行计算,计算过程中`DiffList`中的信息是类似于这样的:`["50000"]++["49999"]++ ... ++["xxxx"]++`,是一个函数。将数据的叠加变成了函数的组合,从而改变了最终运算符的结合顺序 -- **设计要点:将数据以函数形式存储并包装起来,将`Monoid`的`mappend`则实现为函数的组合,改变运算符的结合顺序**。 -- 当然底层涉及到函数式编程数据结构的设计,这和命令式编程的数据结构设计是存在差别的,这需要进一步了解,在入门Hakell之后如果有需求的话。 - -### Reader Monad - -函数`(->) r`除了是函子和应用函子,同样也是一个`Moand`,实现: -```haskell -instance Monad ((->) r) where - return x = \_ -> x - h >>= f = \w -> f (h w) w -``` -- 其中`return`的定义同`pure`,最小的函数上下文就是接受一个参数,直接返回`x`。 -- 看`>>=`类型签名: -```haskell -(>>=) :: Monad m => m a -> (a -> m b) -> m b -``` -- 将`(->) r`也就是`r ->`带入到`m`,得到`(->) r`实例的`>>=`函数签名: -```haskell -(>>=) :: (r -> a) -> (a -> r -> b) -> r -> b -``` -- 定义中`h :: r -> a`,`f :: a -> r -> b`。定义很像`Applicative`的`<*>`。 -- 最终得到一个函数,接受`r`类型参数,传给函数`h`后得到`a`类型结果,作为`f`第一个参数,`r`类型参数同时作为`f`第二个参数,得到最终`b`类型返回值。 - -例子: -```haskell -addStuff :: Int -> Int -addStuff = do - a <- (*2) - b <- (+10) - return (a + b) - -addStuff' :: Int -> Int -addStuff' x = let - a = (*2) x - b = (+10) x - in a + b -{- ->>> addStuff 3 -19 ->>> addStuff' 3 -19 --} -``` -- `addStuff`中所有的函数都固定从一个地方取值,所以function monad又被称作**reader monad**。 - -总结: -- 函数作为单子的含义是将所有的函数粘在一起做成一个大的函数,把这个大的函数的参数喂给全部组成的函数。 -- 通常使用`do`来实现,`>>=`会保证一切能够正常工作。 - -说实话不是非常理解。 - -### State Monad - -Haskell是纯函数式语言,除去有副作用的部分比如IO,程序是由一堆无法改变全局状态或变量的纯函数组成。能做的事情只有处理并返回结果,这个性质使得我们很容易思考程序在干什么,不需要关心变量在某一时间点的值是什么。 - -然而某些领域的问题根本上就是随着时间改变的状态,要写出这样的程序,纯函数的特性就变成了阻碍。因此引入了State Monad,让程序能够处理状态性的问题,并让其他部分依然保持纯粹。 - -考虑随机数:生成随机数需要一个有副作用的随机数生成器,并返回新的随机数生成器,但随机数生成的过程是纯粹的。所以需要将会发生改变的状态传入,并将新的状态作为返回值返回。而在其他命令式语言中的话,一般会将妆台作为全局的状态,在生成随机数的同时改变状态,而不是将状态返回。 -```haskell -let (value, _) = random (mkStdGen 100) in value :: Int -``` - -一般来说,这种函数的签名都会类似这样: -```haskell -s -> (a, s) -``` -- `s`是状态类型,`a`是计算结果类型。 -- 为了保持纯粹性,状态必须被作为参数和返回值。这样写非常不方便,可以将这些事情扔给State Monad来做。 - -例子: -- 考虑建立一个栈的模型,支持压栈和出栈操作,压栈时传入新值和栈,得到新的栈,出栈时传入栈得到新栈。 -- 其中的栈就可以看做状态,通过返回值的方式返回新的状态: -```haskell -type Stack a = [a] - -pop :: Stack a -> (a, Stack a) -pop [] = undefined -pop (x:xs) = (x, xs) - -push :: a -> Stack a -> ((), Stack a) -push a xs = ((), a:xs) - -stackMainOp :: Num a => Stack a -> (a, Stack a) -stackMainOp stack = let - ((), newStack1) = push 3 stack - (a, newStack2) = pop newStack1 - in pop newStack2 -{- ->>> stackMainOp [1, 2, 3] -(1,[2,3]) --} -``` -- 为了避免将状态操作写得这么具体,我们可以将状态封装在State Monad中,之后便可以像这样的方式调用: -```haskell -stackMainOp' = do - push 3 - a <- pop - pop -``` - -State Monad: -- 位于`Control.Monad.State`模块,具体来说`Control.Monad.State.Lazy`。 -```haskell -type State :: * -> * -> * -type State s = StateT s Identity :: * -> * -type StateT :: * -> (* -> *) -> * -> * -newtype StateT s m a = StateT {runStateT :: s -> m (a, s)} - -state :: MonadState s m => (s -> (a, s)) -> m a -runState :: State s a -> s -> (a, s) -(>>=) :: State s a -> (a -> State s b) -> State s b -``` -- 更多信息查看[Wiki](https://wiki.haskell.org/State_Monad)。 -- 类型`State s a`代表改变状态的操作,`s`是状态类型,`a`是产生的结果类型,`State s`被实现为`Monad`。 -- `Monad`实例的实现类似于: -```haskell -instance Monad (State s) where - return x = State $ \s -> (x,s) - (State h) >>= f = State $ \s -> let (a, newState) = h s - (State g) = f a - in g newState -``` -- 注意`State`作为类型构造器,接受2个类型参数,`s a`分别是状态和结果类型,而作为值构造器接受一个`s -> (a, s)`的函数类型参数,即是改变状态的操作。书上是这样写的,但注意现在的版本实现有了变化,就像`Writer`一样,应该使用`state`函数来构造`State`对象,而不是`State`直接作为值构造器。 -- `return`要做的事是接受一个值,返回做出一个改变状态的操作,所以此时值构造器接受参数就是`\s -> (x, s)`函数,`x`当成结果,状态仍然是`s`。这即是最小上下文。 -- `State`中封装的改变状态的操作,接受改变状态操作`h`,`f`则是接受状态操作返回`State Monad`的函数。`>>=`实现则是,先做操作`h`,再将结果做操作`f`。说实话不是很好理解。 -- 运行一个`State`则使用,`runState myState initial_state`。 -- 改写栈做为状态的例子: -```haskell -pop' :: State (Stack a) a -pop' = state $ \(x:xs) -> (x, xs) - -push' :: a -> State (Stack a) () -push' a = state $ \xs -> ((), a:xs) - -stackMainOp' :: State (Stack Int) Int -stackMainOp' = do - push' 3 - pop' - pop' - -{- ->>> runState stackMainOp' [1, 2, 3] -(1,[2,3]) ->>> runState (push' 10 >> push' 100 >> pop') [1, 2, 3] -(100,[10,1,2,3]) --} -``` -- 有一点点费解,因为`Monad`中函数,`>>=`其实是在对`Monad (State s)`中的函数做解开包装之后,将其应用在另一个接受函数返回使用`Monad (State s)`封装函数的函数上。简而言之就是对函数做映射,可以理解为将两个改变状态的行为合在了一起。 -- 单纯的`>>`则可以直接将多个`State s a`合并起来,前提是后面的状态不会使用前面状态的返回值。等价在`do`中则就是不使用`<-`。当然有依赖的话用`do`表示法是最好的: -```haskell -stackComplexOp :: State (Stack Int) () -stackComplexOp = do - a <- pop' - if a >= 10 then do - push' 100 - else do - push' 1000 -{- ->>> runState stackComplexOp [1, 2, 10] -((),[1000,2,10]) ->>> runState stackComplexOp [10, 1, 2] -((),[100,1,2]) --} -``` - -总结: -- 封装函数的行为还是有点太让人费解了,暂时尚无法熟练运用,需要后续有机会结合实践加深理解。注意和函数作为`Monad`相区分,这完全不是一个东西。和`DiffList`这样以函数形式封装数据也不一样(和单纯封装数据差别不大)。 -- 写代码时对比`IO`就行,用`do`表示法的话非常符合直觉,用起来并不难。 - - -### 常用的操作Monad的函数 - -操作包装在单子中的值(Monadic value)的函数,称之为Monadic Function,有一些是常见函数的变形,有一些是第一次遇到。 - -`liftM`: -```haskell -liftM :: Monad m => (a1 -> r) -> m a1 -> m r -fmap :: Functor f => (a -> b) -> f a -> f b -``` -- 其实就是`fmap`,不过是针对`Monad`单独定义的,即使每一个`Monad`都是`functor`,但我们不需要依赖这一点。就像`pure`和`return`其实是同一件事,不过一个在`Applicative`中,一个在`Monad`中。 -- 例: -```haskell ->>> liftM (*2) (Just 10) -Just 20 ->>> liftM (*2) [1, 2, 3] -[2,4,6] ->>> :t liftM not -liftM not :: Monad m => m Bool -> m Bool ->>> runWriter $ liftM not $ writer (True, "whatever") -(False,"whatever") -``` -- 除了`fmap`含义,另一种含义就是将直接用于值的函数提升为能够用于`Monad`的函数。 -- 也就是说`fmap <$> liftM`其实是一个意思。 -- 看一下实现: -```haskell -liftM :: (Monad m) => (a -> b) -> m a -> m b -liftM f m = m >>= (\x -> return (f x)) -``` -- 用等价的`do`表示法: -```haskell -liftM :: (Monad m) => (a -> b) -> m a -> m b -liftM f m = do - x <- m - return (f x) -``` -- 实现只用到了`Monad`而并没有用到`Functor`的性质。可以看出`Monad`比`Functor`性质要强。 -- 回顾`<*>`的类型签名: -```haskell -(<*>) :: Applicative f => f (a -> b) -> f a -> f b -``` -- 其实`<*>`也能够用`Monad`保证的性质实现出来: -```haskell -apply :: Monad m => m (a -> b) -> m a -> m b -apply mf m = do - f <- mf - x <- m - return (f x) -{- ->>> Just (*3) `apply` Just 5 -Just 15 --} -``` -- 对于`liftA2`等函数,也可以类似实现: -```haskell -liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c -liftA2 f x y = f <$> x <*> y -``` -- 对于`Monad`有类似的函数,`liftM2 liftM3 liftM4 ...`等。 - -`join`: -- 如果有包了多层`Monad`的值,那么可以使用`join`函数来解开包装。 -```haskell -join :: Monad m => m (m a) -> m a -``` -- 例子: -```haskell ->>> join (Just (Just 1)) -Just 1 ->>> join . join $ (Just (Just (Just 1))) -Just 1 ->>> join $ Just Nothing -Nothing ->>> join [[1, 2, 3], [4, 5, 6]] -[1,2,3,4,5,6] ->>> runWriter $ join (writer (writer (1, "aaa"), "bbb")) -(1,"bbbaaa") ->>> join (Right (Right 1)) -Right 1 ->>> join (Right (Left "error")) -Left "error" -``` -- 对于列表其实就是`concat`,对于`Monoid`会调用`mappend`。 -- `m >>= f`永远等价于`join (fmap f m)`。 - -`filterM`: -```haskell -filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a] -``` -- 对比`filter`,只是将函数编程了返回Monadic Value,然后相应的返回值也变了。 -```haskell ->>> filterM (\a -> Just (a > 0)) $ [-10..0] ++ [0..10] -Just [1,2,3,4,5,6,7,8,9,10] ->>> filterM (\a -> [True, False]) [1, 2, 3] -[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]] -``` -- 使用返回绝对不仅仅只是筛选这么简单,比如配合列表的不确定性得到一个列表的幂集。配合`Writer`可以在筛选同时写信息进去等,结合上下文的含义会让功能变得很强大。 - -`foldM`: -- `foldl`对`Monad`的版本是`foldM`: -```haskell -foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b -foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b -``` -- 例子: -```haskell ->>> foldM (\a b -> Just (max a b)) 10 [1, 2, 3] -Just 10 -``` - -## Zippers - -### 定义一个树 - -尝试定义一个二叉树类型,因为数据不可变,要修改只能使用模式匹配: -```haskell -data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show) - -freeTree :: Tree Char -freeTree = - Node 'P' - (Node 'O' - (Node 'L' - (Node 'N' Empty Empty) - (Node 'T' Empty Empty) - ) - (Node 'Y' - (Node 'S' Empty Empty) - (Node 'A' Empty Empty) - ) - ) - (Node 'L' - (Node 'W' - (Node 'C' Empty Empty) - (Node 'R' Empty Empty) - ) - (Node 'A' - (Node 'A' Empty Empty) - (Node 'C' Empty Empty) - ) - ) - -data Direction = L | R deriving (Eq, Show) -type Directions = [Direction] - -changeToP :: Directions -> Tree Char -> Tree Char -changeToP (L:ds) (Node x l r) = Node x (changeToP ds l) r -changeToP (R:ds) (Node x l r) = Node x l (changeToP ds r) -changeToP [] (Node _ l r) = Node 'P' l r -changeToP _ Empty = Empty - -{- ->>> changeToP [R, L] freeTree -Node 'P' (Node 'O' (Node 'L' (Node 'N' Empty Empty) (Node 'T' Empty Empty)) (Node 'Y' (Node 'S' Empty Empty) (Node 'A' Empty Empty))) (Node 'L' (Node 'P' (Node 'C' Empty Empty) (Node 'R' Empty Empty)) (Node 'A' (Node 'A' Empty Empty) (Node 'C' Empty Empty))) --} -``` -- 可以将要修改的节点的路径作为一个数组传入,方便在树上游走。 - -### Zipper - -但是这样非常不方便,我们希望在游走的同时保留能够重建一颗树所需要的所有信息以满足修改某个节点删除某个子树等需求。举个例子,游走到左节点,就可以将树的根节点值和右子树保存起来,单独定义一个类型`TreePath`来保留这两个信息,和左子树的二元组就构成了这棵树的完整信息。 -```haskell --- save a path of walking through a tree, LeftPath/RightPath rootValue subTreeOfTheOtherSide -data TreePath a = LeftPath a (Tree a) | RightPath a (Tree a) deriving(Show) -type TreePaths a = [TreePath a] - -goLeft :: (Tree a, TreePaths a) -> (Tree a, TreePaths a) -goLeft (Node x l r, tzs) = (l, LeftPath x r:tzs) -goLeft (Empty, tzs) = error "go to left of empty tree" - -goRight :: (Tree a, TreePaths a) -> (Tree a, TreePaths a) -goRight (Node x l r, tzs) = (r, RightPath x l:tzs) -goRight (Empty, tzs) = error "go to right of empty tree" - -goUp :: (Tree a, TreePaths a) -> (Tree a, TreePaths a) -goUp (t, LeftPath x r:tzs) = (Node x t r, tzs) -goUp (t, RightPath x l:tzs) = (Node x l t, tzs) -goUp (t, []) = error "go to up of root node" - -infixl 5 -: -(-:) :: t1 -> (t1 -> t2) -> t2 -x -: f = f x - -{- ->>> fst $ goLeft (goRight (freeTree, [])) -Node 'W' (Node 'C' Empty Empty) (Node 'R' Empty Empty) ->>> fst $ (freeTree, []) -: goLeft -: goRight -: goLeft -Node 'S' Empty Empty --} - -``` - -这样的二元组就称之为`Zipper`,就像拉链一样,将其定义为类型别名,作为函数的输入和输出就可以方便的修改一棵树: -```haskell --- type synonym -type TreeZipper a = (Tree a, TreePaths a) - --- modify value of a node -modify :: (a -> a) -> TreeZipper a -> TreeZipper a -modify f (Node x l r, tps) = (Node (f x) l r, tps) -modify f (Empty, tps) = (Empty, tps) - --- replace a subtree -attach :: Tree a -> TreeZipper a -> TreeZipper a -attach t (_, tps) = (t, tps) - --- go to root of a tree -goRoot :: TreeZipper a -> TreeZipper a -goRoot (t, []) = (t, []) -goRoot (t, tps) = goRoot $ goUp (t, tps) - -{- ->>> let (newTree, zipper) = (freeTree, []) -: goRight -: goLeft -: modify (\_ -> 'P') ->>> newTree -Node 'P' (Node 'C' Empty Empty) (Node 'R' Empty Empty) ->>> fst $ (newTree, zipper) -: goUp -: attach (Node '&' Empty Empty) -: goRoot -Node 'P' (Node 'O' (Node 'L' (Node 'N' Empty Empty) (Node 'T' Empty Empty)) (Node 'Y' (Node 'S' Empty Empty) (Node 'A' Empty Empty))) (Node '&' Empty Empty) --} -``` - -虽然数据不可变,但通过`Zipper`,其实基本上所有的事情都可以做了。 - -### Zipper of List - -`Zipper`几乎可以套用在任何数据结构,其实思想很简单,就是将一个数据结构拆开,边界位于关心的位置,要增加删除或者修改都可以方便地做,然后也可以方便的合并起来得到最终的结果。操作前后的对象类型就是`Zipper`。 - -比如列表: -```haskell --- zipper of list, (rightSideOfList, reversedLeftSideOfList) -type ListZipper a = ([a], [a]) - --- index from low to high -goForward :: ListZipper a -> ListZipper a -goForward (x:xs, ys) = (xs, x:ys) -goForward ([], ys) = error "go forward of empty list" - --- index from high to low -goBack :: ListZipper a -> ListZipper a -goBack (xs, x:ys) = (x:xs, ys) -goBack (xs, []) = error "go back of full list" -{- ->>> goForward . goForward . goForward $ ([1, 2, 3, 4], []) -([4],[3,2,1]) ->>> goBack ([4],[3,2,1]) -([3,4],[2,1]) --} -``` - -更多应用: -- 利用`Zipper`和树类型可以实现文件系统。 -- 对于可能失败的情况可以将数据用`Maybe`上下文包装,并将`-:`运算符换成`>>=`。 - - -## 总结 - -总结: -- 到此Haskell趣学指南就结束了,也算基本入门了Haskell了?但其实一开始的那句话单子是自函子范畴上的幺半群其实并没有理解,并不涉及到范畴论的内容。对于函子、应用函子、单子的理解仅限于实践层面,对应的范畴论还没有学习。 -- 另外这本书确实一点都不Real World,其中的内容仅算是介绍,真实世界的Haskell编程其实基本可以说没有任何了解。 - -下一步方向: -- 刷完[Haskell 99 Problem](https://wiki.haskell.org/H-99:_Ninety-Nine_Haskell_Problems),这只是习题水平,还是远远不足以指导实践。 -- 看[Typeclassopedia](https://wiki.haskell.org/Typeclassopedia),搞清楚所有常用内建类型类,建立起直觉。 -- 看[Real World Haskell](http://cnhaskell.com/index.html)。 -- 学习范畴论。 -- 暂时可以放一放,有空有兴趣了来做。 \ No newline at end of file diff --git a/Images/Java_collections.jpg b/Images/Java_collections.jpg deleted file mode 100644 index cc92a9f..0000000 Binary files a/Images/Java_collections.jpg and /dev/null differ diff --git a/Images/SICP_book_Cover.jpg b/Images/SICP_book_Cover.jpg deleted file mode 100644 index f24c74b..0000000 Binary files a/Images/SICP_book_Cover.jpg and /dev/null differ diff --git a/Images/Scala_immutable_collections_tree.jpg b/Images/Scala_immutable_collections_tree.jpg deleted file mode 100644 index 66f84ca..0000000 Binary files a/Images/Scala_immutable_collections_tree.jpg and /dev/null differ diff --git a/Images/Scala_implicit_datatype_cast.jpg b/Images/Scala_implicit_datatype_cast.jpg deleted file mode 100644 index 243be5c..0000000 Binary files a/Images/Scala_implicit_datatype_cast.jpg and /dev/null differ diff --git a/Images/Scala_mutable_collections_tree.jpg b/Images/Scala_mutable_collections_tree.jpg deleted file mode 100644 index 43067a4..0000000 Binary files a/Images/Scala_mutable_collections_tree.jpg and /dev/null differ diff --git a/Images/Scala_sbt_dsl_setting_expression.png b/Images/Scala_sbt_dsl_setting_expression.png deleted file mode 100644 index e5b8352..0000000 Binary files a/Images/Scala_sbt_dsl_setting_expression.png and /dev/null differ diff --git a/Images/git_commit_obejct_HEAD_pointer.png b/Images/git_commit_obejct_HEAD_pointer.png deleted file mode 100644 index 1df0edb..0000000 Binary files a/Images/git_commit_obejct_HEAD_pointer.png and /dev/null differ diff --git a/Images/git_commit_object.png b/Images/git_commit_object.png deleted file mode 100644 index 6e2df92..0000000 Binary files a/Images/git_commit_object.png and /dev/null differ diff --git a/Images/git_commit_object_branch.png b/Images/git_commit_object_branch.png deleted file mode 100644 index 624531f..0000000 Binary files a/Images/git_commit_object_branch.png and /dev/null differ diff --git a/Images/git_commit_object_branches.png b/Images/git_commit_object_branches.png deleted file mode 100644 index 4e20660..0000000 Binary files a/Images/git_commit_object_branches.png and /dev/null differ diff --git a/Images/git_commit_object_link.png b/Images/git_commit_object_link.png deleted file mode 100644 index 9ba06ab..0000000 Binary files a/Images/git_commit_object_link.png and /dev/null differ diff --git a/Images/git_file_lifecycle.png b/Images/git_file_lifecycle.png deleted file mode 100644 index 922b02c..0000000 Binary files a/Images/git_file_lifecycle.png and /dev/null differ diff --git a/Images/git_rebase_merge.png b/Images/git_rebase_merge.png deleted file mode 100644 index 2208826..0000000 Binary files a/Images/git_rebase_merge.png and /dev/null differ diff --git a/Images/git_rebase_rebase.png b/Images/git_rebase_rebase.png deleted file mode 100644 index d0b5b24..0000000 Binary files a/Images/git_rebase_rebase.png and /dev/null differ diff --git a/Images/git_work_with_git_rebase.png b/Images/git_work_with_git_rebase.png deleted file mode 100644 index 814a1dd..0000000 Binary files a/Images/git_work_with_git_rebase.png and /dev/null differ diff --git a/Images/git_workflow_Integrated_administrator.png b/Images/git_workflow_Integrated_administrator.png deleted file mode 100644 index 573c526..0000000 Binary files a/Images/git_workflow_Integrated_administrator.png and /dev/null differ diff --git a/Images/git_workflow_centralized.png b/Images/git_workflow_centralized.png deleted file mode 100644 index 22c1af1..0000000 Binary files a/Images/git_workflow_centralized.png and /dev/null differ diff --git a/Images/git_workflow_dictator_and_lieutenaut.png b/Images/git_workflow_dictator_and_lieutenaut.png deleted file mode 100644 index 85f2d5a..0000000 Binary files a/Images/git_workflow_dictator_and_lieutenaut.png and /dev/null differ diff --git a/Images/nginx_first_tomcat10.0.2_page.png b/Images/nginx_first_tomcat10.0.2_page.png deleted file mode 100644 index 07d9b1d..0000000 Binary files a/Images/nginx_first_tomcat10.0.2_page.png and /dev/null differ diff --git a/Images/nginx_logo.png b/Images/nginx_logo.png deleted file mode 100644 index 93461fc..0000000 Binary files a/Images/nginx_logo.png and /dev/null differ diff --git a/InputWords.txt b/InputWords.txt new file mode 100644 index 0000000..cc443a0 --- /dev/null +++ b/InputWords.txt @@ -0,0 +1,10657 @@ +aahed +aalii +aargh +aarti +abaca +abaci +abacs +abaft +abaka +abamp +aband +abash +abask +abaya +abbas +abbed +abbes +abcee +abeam +abear +abele +abers +abets +abies +abler +ables +ablet +ablow +abmho +abohm +aboil +aboma +aboon +abord +abore +abram +abray +abrim +abrin +abris +absey +absit +abuna +abune +abuts +abuzz +abyes +abysm +acais +acari +accas +accoy +acerb +acers +aceta +achar +ached +aches +achoo +acids +acidy +acing +acini +ackee +acker +acmes +acmic +acned +acnes +acock +acold +acred +acres +acros +acted +actin +acton +acyls +adaws +adays +adbot +addax +added +adder +addio +addle +adeem +adhan +adieu +adios +adits +adman +admen +admix +adobo +adown +adoze +adrad +adred +adsum +aduki +adunc +adust +advew +adyta +adzed +adzes +aecia +aedes +aegis +aeons +aerie +aeros +aesir +afald +afara +afars +afear +aflaj +afore +afrit +afros +agama +agami +agars +agast +agave +agaze +agene +agers +agger +aggie +aggri +aggro +aggry +aghas +agila +agios +agism +agist +agita +aglee +aglet +agley +agloo +aglus +agmas +agoge +agone +agons +agood +agria +agrin +agros +agued +agues +aguna +aguti +aheap +ahent +ahigh +ahind +ahing +ahint +ahold +ahull +ahuru +aidas +aided +aides +aidoi +aidos +aiery +aigas +aight +ailed +aimed +aimer +ainee +ainga +aioli +aired +airer +airns +airth +airts +aitch +aitus +aiver +aiyee +aizle +ajies +ajiva +ajuga +ajwan +akees +akela +akene +aking +akita +akkas +alaap +alack +alamo +aland +alane +alang +alans +alant +alapa +alaps +alary +alate +alays +albas +albee +alcid +alcos +aldea +alder +aldol +aleck +alecs +alefs +aleft +aleph +alews +aleye +alfas +algal +algas +algid +algin +algor +algum +alias +alifs +aline +alist +aliya +alkie +alkos +alkyd +alkyl +allee +allel +allis +allod +allyl +almah +almas +almeh +almes +almud +almug +alods +aloed +aloes +aloha +aloin +aloos +alowe +altho +altos +alula +alums +alure +alvar +alway +amahs +amain +amate +amaut +amban +ambit +ambos +ambry +ameba +ameer +amene +amens +ament +amias +amice +amici +amide +amido +amids +amies +amiga +amigo +amine +amino +amins +amirs +amlas +amman +ammon +ammos +amnia +amnic +amnio +amoks +amole +amort +amour +amove +amowt +amped +ampul +amrit +amuck +amyls +anana +anata +ancho +ancle +ancon +andro +anear +anele +anent +angas +anglo +anigh +anile +anils +anima +animi +anion +anise +anker +ankhs +ankus +anlas +annal +annas +annat +anoas +anole +anomy +ansae +antae +antar +antas +anted +antes +antis +antra +antre +antsy +anura +anyon +apace +apage +apaid +apayd +apays +apeak +apeek +apers +apert +apery +apgar +aphis +apian +apiol +apish +apism +apode +apods +apoop +aport +appal +appay +appel +appro +appui +appuy +apres +apses +apsis +apsos +apted +apter +aquae +aquas +araba +araks +arame +arars +arbas +arced +archi +arcos +arcus +ardeb +ardri +aread +areae +areal +arear +areas +areca +aredd +arede +arefy +areic +arene +arepa +arere +arete +arets +arett +argal +argan +argil +argle +argol +argon +argot +argus +arhat +arias +ariel +ariki +arils +ariot +arish +arked +arled +arles +armed +armer +armet +armil +arnas +arnut +aroba +aroha +aroid +arpas +arpen +arrah +arras +arret +arris +arroz +arsed +arses +arsey +arsis +artal +artel +artic +artis +aruhe +arums +arval +arvee +arvos +aryls +asana +ascon +ascus +asdic +ashed +ashes +ashet +asked +asker +askoi +askos +aspen +asper +aspic +aspie +aspis +aspro +assai +assam +asses +assez +assot +aster +astir +astun +asura +asway +aswim +asyla +ataps +ataxy +atigi +atilt +atimy +atlas +atman +atmas +atmos +atocs +atoke +atoks +atoms +atomy +atony +atopy +atria +atrip +attap +attar +atuas +audad +auger +aught +aulas +aulic +auloi +aulos +aumil +aunes +aunts +aurae +aural +aurar +auras +aurei +aures +auric +auris +aurum +autos +auxin +avale +avant +avast +avels +avens +avers +avgas +avine +avion +avise +aviso +avize +avows +avyze +awarn +awato +awave +aways +awdls +aweel +aweto +awing +awmry +awned +awner +awols +awork +axels +axile +axils +axing +axite +axled +axles +axman +axmen +axoid +axone +axons +ayahs +ayaya +ayelp +aygre +ayins +ayont +ayres +ayrie +azans +azide +azido +azine +azlon +azoic +azole +azons +azote +azoth +azuki +azurn +azury +azygy +azyme +azyms +baaed +baals +babas +babel +babes +babka +baboo +babul +babus +bacca +bacco +baccy +bacha +bachs +backs +baddy +baels +baffs +baffy +bafts +baghs +bagie +bahts +bahus +bahut +bails +bairn +baisa +baith +baits +baiza +baize +bajan +bajra +bajri +bajus +baked +baken +bakes +bakra +balas +balds +baldy +baled +bales +balks +balky +balls +bally +balms +baloo +balsa +balti +balun +balus +bambi +banak +banco +bancs +banda +bandh +bands +bandy +baned +banes +bangs +bania +banks +banns +bants +bantu +banty +banya +bapus +barbe +barbs +barby +barca +barde +bardo +bards +bardy +bared +barer +bares +barfi +barfs +baric +barks +barky +barms +barmy +barns +barny +barps +barra +barre +barro +barry +barye +basan +based +basen +baser +bases +basho +basij +basks +bason +basse +bassi +basso +bassy +basta +basti +basto +basts +bated +bates +baths +batik +batta +batts +battu +bauds +bauks +baulk +baurs +bavin +bawds +bawks +bawls +bawns +bawrs +bawty +bayed +bayer +bayes +bayle +bayts +bazar +bazoo +beads +beaks +beaky +beals +beams +beamy +beano +beans +beany +beare +bears +beath +beats +beaty +beaus +beaut +beaux +bebop +becap +becke +becks +bedad +bedel +bedes +bedew +bedim +bedye +beedi +beefs +beeps +beers +beery +beets +befog +begad +begar +begem +begot +begum +beige +beigy +beins +bekah +belah +belar +belay +belee +belga +bells +belon +belts +bemad +bemas +bemix +bemud +bends +bendy +benes +benet +benga +benis +benne +benni +benny +bento +bents +benty +bepat +beray +beres +bergs +berko +berks +berme +berms +berob +beryl +besat +besaw +besee +beses +besit +besom +besot +besti +bests +betas +beted +betes +beths +betid +beton +betta +betty +bever +bevor +bevue +bevvy +bewet +bewig +bezes +bezil +bezzy +bhais +bhaji +bhang +bhats +bhels +bhoot +bhuna +bhuts +biach +biali +bialy +bibbs +bibes +biccy +bices +bided +bider +bides +bidet +bidis +bidon +bield +biers +biffo +biffs +biffy +bifid +bigae +biggs +biggy +bigha +bight +bigly +bigos +bijou +biked +biker +bikes +bikie +bilbo +bilby +biled +biles +bilgy +bilks +bills +bimah +bimas +bimbo +binal +bindi +binds +biner +bines +bings +bingy +binit +binks +bints +biogs +biont +biota +biped +bipod +birds +birks +birle +birls +biros +birrs +birse +birsy +bises +bisks +bisom +bitch +biter +bites +bitos +bitou +bitsy +bitte +bitts +bivia +bivvy +bizes +bizzo +bizzy +blabs +blads +blady +blaer +blaes +blaff +blags +blahs +blain +blams +blart +blase +blash +blate +blats +blatt +blaud +blawn +blaws +blays +blear +blebs +blech +blees +blent +blert +blest +blets +bleys +blimy +bling +blini +blins +bliny +blips +blist +blite +blits +blive +blobs +blocs +blogs +blook +bloop +blore +blots +blows +blowy +blubs +blude +bluds +bludy +blued +blues +bluet +bluey +bluid +blume +blunk +blurs +blype +boabs +boaks +boars +boart +boats +bobac +bobak +bobas +bobol +bobos +bocca +bocce +bocci +boche +bocks +boded +bodes +bodge +bodhi +bodle +boeps +boets +boeuf +boffo +boffs +bogan +bogey +boggy +bogie +bogle +bogue +bogus +bohea +bohos +boils +boing +boink +boite +boked +bokeh +bokes +bokos +bolar +bolas +bolds +boles +bolix +bolls +bolos +bolts +bolus +bomas +bombe +bombo +bombs +bonce +bonds +boned +boner +bones +bongs +bonie +bonks +bonne +bonny +bonza +bonze +booai +booay +boobs +boody +booed +boofy +boogy +boohs +books +booky +bools +booms +boomy +boong +boons +boord +boors +boose +boots +boppy +borak +boral +boras +borde +bords +bored +boree +borel +borer +bores +borgo +boric +borks +borms +borna +boron +borts +borty +bortz +bosie +bosks +bosky +boson +bosun +botas +botel +botes +bothy +botte +botts +botty +bouge +bouks +boult +bouns +bourd +bourg +bourn +bouse +bousy +bouts +bovid +bowat +bowed +bower +bowes +bowet +bowie +bowls +bowne +bowrs +bowse +boxed +boxen +boxes +boxla +boxty +boyar +boyau +boyed +boyfs +boygs +boyla +boyos +boysy +bozos +braai +brach +brack +bract +brads +braes +brags +brail +braks +braky +brame +brane +brank +brans +brant +brast +brats +brava +bravi +braws +braxy +brays +braza +braze +bream +brede +breds +breem +breer +brees +breid +breis +breme +brens +brent +brere +brers +breve +brews +breys +brier +bries +brigs +briki +briks +brill +brims +brins +brios +brise +briss +brith +brits +britt +brize +broch +brock +brods +brogh +brogs +brome +bromo +bronc +brond +brool +broos +brose +brosy +brows +brugh +bruin +bruit +brule +brume +brung +brusk +brust +bruts +buats +buaze +bubal +bubas +bubba +bubbe +bubby +bubus +buchu +bucko +bucks +bucku +budas +budis +budos +buffa +buffe +buffi +buffo +buffs +buffy +bufos +bufty +buhls +buhrs +buiks +buist +bukes +bulbs +bulgy +bulks +bulla +bulls +bulse +bumbo +bumfs +bumph +bumps +bumpy +bunas +bunce +bunco +bunde +bundh +bunds +bundt +bundu +bundy +bungs +bungy +bunia +bunje +bunjy +bunko +bunks +bunns +bunts +bunty +bunya +buoys +buppy +buran +buras +burbs +burds +buret +burfi +burgh +burgs +burin +burka +burke +burks +burls +burns +buroo +burps +burqa +burro +burrs +burry +bursa +burse +busby +buses +busks +busky +bussu +busti +busts +busty +buteo +butes +butle +butoh +butts +butty +butut +butyl +buzzy +bwana +bwazi +byded +bydes +byked +bykes +byres +byrls +byssi +bytes +byway +caaed +cabas +caber +cabob +caboc +cabre +cacas +cacks +cacky +cadee +cades +cadge +cadgy +cadie +cadis +cadre +caeca +caese +cafes +caffs +caged +cager +cages +cagot +cahow +caids +cains +caird +cajon +cajun +caked +cakes +cakey +calfs +calid +calif +calix +calks +calla +calls +calms +calmy +calos +calpa +calps +calve +calyx +caman +camas +cames +camis +camos +campi +campo +camps +campy +camus +caned +caneh +caner +canes +cangs +canid +canna +canns +canso +canst +canto +cants +canty +capas +caped +capes +capex +caphs +capiz +caple +capon +capos +capot +capri +capul +carap +carbo +carbs +carby +cardi +cards +cardy +cared +carer +cares +caret +carex +carks +carle +carls +carns +carny +carob +carom +caron +carpi +carps +carrs +carse +carta +carte +carts +carvy +casas +casco +cased +cases +casks +casky +casts +casus +cates +cauda +cauks +cauld +cauls +caums +caups +cauri +causa +cavas +caved +cavel +caver +caves +cavie +cawed +cawks +caxon +ceaze +cebid +cecal +cecum +ceded +ceder +cedes +cedis +ceiba +ceili +ceils +celeb +cella +celli +cells +celom +celts +cense +cento +cents +centu +ceorl +cepes +cerci +cered +ceres +cerge +ceria +ceric +cerne +ceroc +ceros +certs +certy +cesse +cesta +cesti +cetes +cetyl +cezve +chace +chack +chaco +chado +chads +chaft +chais +chals +chams +chana +chang +chank +chape +chaps +chapt +chara +chare +chark +charr +chars +chary +chats +chave +chavs +chawk +chaws +chaya +chays +cheep +chefs +cheka +chela +chelp +chemo +chems +chere +chert +cheth +chevy +chews +chewy +chiao +chias +chibs +chica +chich +chico +chics +chiel +chiks +chile +chimb +chimo +chimp +chine +ching +chink +chino +chins +chips +chirk +chirl +chirm +chiro +chirr +chirt +chiru +chits +chive +chivs +chivy +chizz +choco +chocs +chode +chogs +choil +choko +choky +chola +choli +cholo +chomp +chons +choof +chook +choom +choon +chops +chota +chott +chout +choux +chowk +chows +chubs +chufa +chuff +chugs +chums +churl +churr +chuse +chuts +chyle +chyme +chynd +cibol +cided +cides +ciels +ciggy +cilia +cills +cimar +cimex +cinct +cines +cinqs +cions +cippi +circs +cires +cirls +cirri +cisco +cissy +cists +cital +cited +citer +cites +cives +civet +civie +civvy +clach +clade +clads +claes +clags +clame +clams +clans +claps +clapt +claro +clart +clary +clast +clats +claut +clave +clavi +claws +clays +cleck +cleek +cleep +clefs +clegs +cleik +clems +clepe +clept +cleve +clews +clied +clies +clift +clime +cline +clint +clipe +clips +clipt +clits +cloam +clods +cloff +clogs +cloke +clomb +clomp +clonk +clons +cloop +cloot +clops +clote +clots +clour +clous +clows +cloye +cloys +cloze +clubs +clues +cluey +clunk +clype +cnida +coact +coady +coala +coals +coaly +coapt +coarb +coate +coati +coats +cobbs +cobby +cobia +coble +cobza +cocas +cocci +cocco +cocks +cocky +cocos +codas +codec +coded +coden +coder +codes +codex +codon +coeds +coffs +cogie +cogon +cogue +cohab +cohen +cohoe +cohog +cohos +coifs +coign +coils +coins +coirs +coits +coked +cokes +colas +colby +colds +coled +coles +coley +colic +colin +colls +colly +colog +colts +colza +comae +comal +comas +combe +combi +combo +combs +comby +comer +comes +comix +commo +comms +commy +compo +comps +compt +comte +comus +coned +cones +coney +confs +conga +conge +congo +conia +conin +conks +conky +conne +conns +conte +conto +conus +convo +cooch +cooed +cooee +cooer +cooey +coofs +cooks +cooky +cools +cooly +coomb +cooms +coomy +coons +coops +coopt +coost +coots +cooze +copal +copay +coped +copen +coper +copes +coppy +copra +copsy +coqui +coram +corbe +corby +cords +cored +cores +corey +corgi +coria +corks +corky +corms +corni +corno +corns +cornu +corps +corse +corso +cosec +cosed +coses +coset +cosey +cosie +costa +coste +costs +cotan +coted +cotes +coths +cotta +cotts +coude +coups +courb +courd +coure +cours +couta +couth +coved +coves +covin +cowal +cowan +cowed +cowks +cowls +cowps +cowry +coxae +coxal +coxed +coxes +coxib +coyau +coyed +coyer +coypu +cozed +cozen +cozes +cozey +cozie +craal +crabs +crags +craic +craig +crake +crame +crams +crans +crape +craps +crapy +crare +craws +crays +creds +creel +crees +crems +crena +creps +crepy +crewe +crews +crias +cribs +cries +crims +crine +crios +cripe +crips +crise +crith +crits +croci +crocs +croft +crogs +cromb +crome +cronk +crons +crool +croon +crops +crore +crost +crout +crows +croze +cruck +crudo +cruds +crudy +crues +cruet +cruft +crunk +cruor +crura +cruse +crusy +cruve +crwth +cryer +ctene +cubby +cubeb +cubed +cuber +cubes +cubit +cuddy +cuffo +cuffs +cuifs +cuing +cuish +cuits +cukes +culch +culet +culex +culls +cully +culms +culpa +culti +cults +culty +cumec +cundy +cunei +cunit +cunts +cupel +cupid +cuppa +cuppy +curat +curbs +curch +curds +curdy +cured +curer +cures +curet +curfs +curia +curie +curli +curls +curns +curny +currs +cursi +curst +cusec +cushy +cusks +cusps +cuspy +cusso +cusum +cutch +cuter +cutes +cutey +cutin +cutis +cutto +cutty +cutup +cuvee +cuzes +cwtch +cyano +cyans +cycad +cycas +cyclo +cyder +cylix +cymae +cymar +cymas +cymes +cymol +cysts +cytes +cyton +czars +daals +dabba +daces +dacha +dacks +dadah +dadas +dados +daffs +daffy +dagga +daggy +dagos +dahls +daiko +daine +daint +daker +daled +dales +dalis +dalle +dalts +daman +damar +dames +damme +damns +damps +dampy +dancy +dangs +danio +danks +danny +dants +daraf +darbs +darcy +dared +darer +dares +darga +dargs +daric +daris +darks +darky +darns +darre +darts +darzi +dashi +dashy +datal +dated +dater +dates +datos +datto +daube +daubs +dauby +dauds +dault +daurs +dauts +daven +davit +dawah +dawds +dawed +dawen +dawks +dawns +dawts +dayan +daych +daynt +dazed +dazer +dazes +deads +deair +deals +deans +deare +dearn +dears +deary +deash +deave +deaws +deawy +debag +debby +debel +debes +debts +debud +debur +debus +debye +decad +decaf +decan +decko +decks +decos +dedal +deeds +deedy +deely +deems +deens +deeps +deere +deers +deets +deeve +deevs +defat +deffo +defis +defog +degas +degum +degus +deice +deids +deify +deils +deism +deist +deked +dekes +dekko +deled +deles +delfs +delft +delis +dells +delly +delos +delph +delts +deman +demes +demic +demit +demob +demoi +demos +dempt +denar +denay +dench +denes +denet +denis +dents +deoxy +derat +deray +dered +deres +derig +derma +derms +derns +derny +deros +derro +derry +derth +dervs +desex +deshi +desis +desks +desse +devas +devel +devis +devon +devos +devot +dewan +dewar +dewax +dewed +dexes +dexie +dhaba +dhaks +dhals +dhikr +dhobi +dhole +dholl +dhols +dhoti +dhows +dhuti +diact +dials +diane +diazo +dibbs +diced +dicer +dices +dicht +dicks +dicky +dicot +dicta +dicts +dicty +diddy +didie +didos +didst +diebs +diels +diene +diets +diffs +dight +dikas +diked +diker +dikes +dikey +dildo +dilli +dills +dimbo +dimer +dimes +dimps +dinar +dined +dines +dinge +dings +dinic +dinks +dinky +dinna +dinos +dints +diols +diota +dippy +dipso +diram +direr +dirke +dirks +dirls +dirts +disas +disci +discs +dishy +disks +disme +dital +ditas +dited +dites +ditsy +ditts +ditzy +divan +divas +dived +dives +divis +divna +divos +divot +divvy +diwan +dixie +dixit +diyas +dizen +djinn +djins +doabs +doats +dobby +dobes +dobie +dobla +dobra +dobro +docht +docks +docos +docus +doddy +dodos +doeks +doers +doest +doeth +doffs +dogan +doges +dogey +doggo +doggy +dogie +dohyo +doilt +doily +doits +dojos +dolce +dolci +doled +doles +dolia +dolls +dolma +dolor +dolos +dolts +domal +domed +domes +domic +donah +donas +donee +doner +donga +dongs +donko +donna +donne +donny +donsy +doobs +dooce +doody +dooks +doole +dools +dooly +dooms +doomy +doona +doorn +doors +doozy +dopas +doped +doper +dopes +dorad +dorba +dorbs +doree +dores +doric +doris +dorks +dorky +dorms +dormy +dorps +dorrs +dorsa +dorse +dorts +dorty +dosai +dosas +dosed +doseh +doser +doses +dosha +dotal +doted +doter +dotes +dotty +douar +douce +doucs +douks +doula +douma +doums +doups +doura +douse +douts +doved +doven +dover +doves +dovie +dowar +dowds +dowed +dower +dowie +dowle +dowls +dowly +downa +downs +dowps +dowse +dowts +doxed +doxes +doxie +doyen +doyly +dozed +dozer +dozes +drabs +drack +draco +draff +drags +drail +drams +drant +draps +drats +drave +draws +drays +drear +dreck +dreed +dreer +drees +dregs +dreks +drent +drere +drest +dreys +dribs +drice +dries +drily +drips +dript +droid +droil +droke +drole +drome +drony +droob +droog +drook +drops +dropt +drouk +drows +drubs +drugs +drums +drupe +druse +drusy +druxy +dryad +dryas +dsobo +dsomo +duads +duals +duans +duars +dubbo +ducal +ducat +duces +ducks +ducky +ducts +duddy +duded +dudes +duels +duets +duett +duffs +dufus +duing +duits +dukas +duked +dukes +dukka +dulce +dules +dulia +dulls +dulse +dumas +dumbo +dumbs +dumka +dumky +dumps +dunam +dunch +dunes +dungs +dungy +dunks +dunno +dunny +dunsh +dunts +duomi +duomo +duped +duper +dupes +duple +duply +duppy +dural +duras +dured +dures +durgy +durns +duroc +duros +duroy +durra +durrs +durry +durst +durum +durzi +dusks +dusts +duxes +dwaal +dwale +dwalm +dwams +dwang +dwaum +dweeb +dwile +dwine +dyads +dyers +dyked +dykes +dykey +dykon +dynel +dynes +dzhos +eagre +ealed +eales +eaned +eards +eared +earls +earns +earnt +earst +eased +easer +eases +easle +easts +eathe +eaved +eaves +ebbed +ebbet +ebons +ebook +ecads +eched +eches +echos +ecrus +edema +edged +edger +edges +edile +edits +educe +educt +eejit +eensy +eeven +eevns +effed +egads +egers +egest +eggar +egged +egger +egmas +ehing +eider +eidos +eigne +eiked +eikon +eilds +eisel +ejido +ekkas +elain +eland +elans +elchi +eldin +elemi +elfed +eliad +elint +elmen +eloge +elogy +eloin +elops +elpee +elsin +elute +elvan +elven +elver +elves +emacs +embar +embay +embog +embow +embox +embus +emeer +emend +emerg +emery +emeus +emics +emirs +emits +emmas +emmer +emmet +emmew +emmys +emoji +emong +emote +emove +empts +emule +emure +emyde +emyds +enarm +enate +ended +ender +endew +endue +enews +enfix +eniac +enlit +enmew +ennog +enoki +enols +enorm +enows +enrol +ensew +ensky +entia +enure +enurn +envoi +enzym +eorls +eosin +epact +epees +ephah +ephas +ephod +ephor +epics +epode +epopt +epris +eques +equid +erbia +erevs +ergon +ergos +ergot +erhus +erica +erick +erics +ering +erned +ernes +erose +erred +erses +eruct +erugo +eruvs +erven +ervil +escar +escot +esile +eskar +esker +esnes +esses +estoc +estop +estro +etage +etape +etats +etens +ethal +ethne +ethyl +etics +etnas +ettin +ettle +etuis +etwee +etyma +eughs +euked +eupad +euros +eusol +evens +evert +evets +evhoe +evils +evite +evohe +ewers +ewest +ewhow +ewked +exams +exeat +execs +exeem +exeme +exfil +exies +exine +exing +exits +exode +exome +exons +expat +expos +exude +exuls +exurb +eyass +eyers +eyots +eyras +eyres +eyrie +eyrir +ezine +fabby +faced +facer +faces +facia +facta +facts +faddy +faded +fader +fades +fadge +fados +faena +faery +faffs +faffy +faggy +fagin +fagot +faiks +fails +faine +fains +fairs +faked +faker +fakes +fakey +fakie +fakir +falaj +falls +famed +fames +fanal +fands +fanes +fanga +fango +fangs +fanks +fanon +fanos +fanum +faqir +farad +farci +farcy +fards +fared +farer +fares +farle +farls +farms +faros +farro +farse +farts +fasci +fasti +fasts +fated +fates +fatly +fatso +fatwa +faugh +fauld +fauns +faurd +fauts +fauve +favas +favel +faver +faves +favus +fawns +fawny +faxed +faxes +fayed +fayer +fayne +fayre +fazed +fazes +feals +feare +fears +feart +fease +feats +feaze +feces +fecht +fecit +fecks +fedex +feebs +feeds +feels +feens +feers +feese +feeze +fehme +feint +feist +felch +felid +fells +felly +felts +felty +femal +femes +femmy +fends +fendy +fenis +fenks +fenny +fents +feods +feoff +ferer +feres +feria +ferly +fermi +ferms +ferns +ferny +fesse +festa +fests +festy +fetas +feted +fetes +fetor +fetta +fetts +fetwa +feuar +feuds +feued +feyed +feyer +feyly +fezes +fezzy +fiars +fiats +fibro +fices +fiche +fichu +ficin +ficos +fides +fidge +fidos +fiefs +fient +fiere +fiers +fiest +fifed +fifer +fifes +fifis +figgy +figos +fiked +fikes +filar +filch +filed +files +filii +filks +fille +fillo +fills +filmi +films +filos +filum +finca +finds +fined +fines +finis +finks +finny +finos +fiord +fiqhs +fique +fired +firer +fires +firie +firks +firms +firns +firry +firth +fiscs +fisks +fists +fisty +fitch +fitly +fitna +fitte +fitts +fiver +fives +fixed +fixes +fixit +fjeld +flabs +flaff +flags +flaks +flamm +flams +flamy +flane +flans +flaps +flary +flats +flava +flawn +flaws +flawy +flaxy +flays +fleam +fleas +fleek +fleer +flees +flegs +fleme +fleur +flews +flexi +flexo +fleys +flics +flied +flies +flimp +flims +flips +flirs +flisk +flite +flits +flitt +flobs +flocs +floes +flogs +flong +flops +flors +flory +flosh +flota +flote +flows +flubs +flued +flues +fluey +fluky +flump +fluor +flurr +fluty +fluyt +flyby +flype +flyte +foals +foams +foehn +fogey +fogie +fogle +fogou +fohns +foids +foils +foins +folds +foley +folia +folic +folie +folks +folky +fomes +fonda +fonds +fondu +fones +fonly +fonts +foods +foody +fools +foots +footy +foram +forbs +forby +fordo +fords +forel +fores +forex +forks +forky +forme +forms +forts +forza +forze +fossa +fosse +fouat +fouds +fouer +fouet +foule +fouls +fount +fours +fouth +fovea +fowls +fowth +foxed +foxes +foxie +foyle +foyne +frabs +frack +fract +frags +fraim +franc +frape +fraps +frass +frate +frati +frats +fraus +frays +frees +freet +freit +fremd +frena +freon +frere +frets +fribs +frier +fries +frigs +frise +frist +frith +frits +fritt +frize +frizz +froes +frogs +frons +frore +frorn +frory +frosh +frows +frowy +frugs +frump +frush +frust +fryer +fubar +fubby +fubsy +fucks +fucus +fuddy +fudgy +fuels +fuero +fuffs +fuffy +fugal +fuggy +fugie +fugio +fugle +fugly +fugus +fujis +fulls +fumed +fumer +fumes +fumet +fundi +funds +fundy +fungo +fungs +funks +fural +furan +furca +furls +furol +furrs +furth +furze +furzy +fused +fusee +fusel +fuses +fusil +fusks +fusts +fusty +futon +fuzed +fuzee +fuzes +fuzil +fyces +fyked +fykes +fyles +fyrds +fytte +gabba +gabby +gable +gaddi +gades +gadge +gadid +gadis +gadje +gadjo +gadso +gaffs +gaged +gager +gages +gaids +gains +gairs +gaita +gaits +gaitt +gajos +galah +galas +galax +galea +galed +gales +galls +gally +galop +galut +galvo +gamas +gamay +gamba +gambe +gambo +gambs +gamed +games +gamey +gamic +gamin +gamme +gammy +gamps +ganch +gandy +ganef +ganev +gangs +ganja +ganof +gants +gaols +gaped +gaper +gapes +gapos +gappy +garbe +garbo +garbs +garda +gares +garis +garms +garni +garre +garth +garum +gases +gasps +gaspy +gasts +gatch +gated +gater +gates +gaths +gator +gauch +gaucy +gauds +gauje +gault +gaums +gaumy +gaups +gaurs +gauss +gauzy +gavot +gawcy +gawds +gawks +gawps +gawsy +gayal +gazal +gazar +gazed +gazes +gazon +gazoo +geals +geans +geare +gears +geats +gebur +gecks +geeks +geeps +geest +geist +geits +gelds +gelee +gelid +gelly +gelts +gemel +gemma +gemmy +gemot +genal +genas +genes +genet +genic +genii +genip +genny +genoa +genom +genro +gents +genty +genua +genus +geode +geoid +gerah +gerbe +geres +gerle +germs +germy +gerne +gesse +gesso +geste +gests +getas +getup +geums +geyan +geyer +ghast +ghats +ghaut +ghazi +ghees +ghest +ghyll +gibed +gibel +giber +gibes +gibli +gibus +gifts +gigas +gighe +gigot +gigue +gilas +gilds +gilet +gills +gilly +gilpy +gilts +gimel +gimme +gimps +gimpy +ginch +ginge +gings +ginks +ginny +ginzo +gipon +gippo +gippy +girds +girls +girns +giron +giros +girrs +girsh +girts +gismo +gisms +gists +gitch +gites +giust +gived +gives +gizmo +glace +glads +glady +glaik +glair +glams +glans +glary +glaum +glaur +glazy +gleba +glebe +gleby +glede +gleds +gleed +gleek +glees +gleet +gleis +glens +glent +gleys +glial +glias +glibs +gliff +glift +glike +glime +glims +glisk +glits +glitz +gloam +globi +globs +globy +glode +glogg +gloms +gloop +glops +glost +glout +glows +gloze +glued +gluer +glues +gluey +glugs +glume +glums +gluon +glute +gluts +gnarl +gnarr +gnars +gnats +gnawn +gnaws +gnows +goads +goafs +goals +goary +goats +goaty +goban +gobar +gobbi +gobbo +gobby +gobis +gobos +godet +godso +goels +goers +goest +goeth +goety +gofer +goffs +gogga +gogos +goier +gojis +golds +goldy +goles +golfs +golpe +golps +gombo +gomer +gompa +gonch +gonef +gongs +gonia +gonif +gonks +gonna +gonof +gonys +gonzo +gooby +goods +goofs +googs +gooks +gooky +goold +gools +gooly +goons +goony +goops +goopy +goors +goory +goosy +gopak +gopik +goral +goras +gored +gores +goris +gorms +gormy +gorps +gorse +gorsy +gosht +gosse +gotch +goths +gothy +gotta +gouch +gouks +goura +gouts +gouty +gowan +gowds +gowfs +gowks +gowls +gowns +goxes +goyim +goyle +graal +grabs +grads +graff +graip +grama +grame +gramp +grams +grana +grans +grapy +gravs +grays +grebe +grebo +grece +greek +grees +grege +grego +grein +grens +grese +greve +grews +greys +grice +gride +grids +griff +grift +grigs +grike +grins +griot +grips +gript +gripy +grise +grist +grisy +grith +grits +grize +groat +grody +grogs +groks +groma +grone +groof +grosz +grots +grouf +grovy +grows +grrls +grrrl +grubs +grued +grues +grufe +grume +grump +grund +gryce +gryde +gryke +grype +grypt +guaco +guana +guano +guans +guars +gucks +gucky +gudes +guffs +gugas +guids +guimp +guiro +gulag +gular +gulas +gules +gulet +gulfs +gulfy +gulls +gulph +gulps +gulpy +gumma +gummi +gumps +gundy +gunge +gungy +gunks +gunky +gunny +guqin +gurdy +gurge +gurls +gurly +gurns +gurry +gursh +gurus +gushy +gusla +gusle +gusli +gussy +gusts +gutsy +gutta +gutty +guyed +guyle +guyot +guyse +gwine +gyals +gyans +gybed +gybes +gyeld +gymps +gynae +gynie +gynny +gynos +gyoza +gypos +gyppo +gyppy +gyral +gyred +gyres +gyron +gyros +gyrus +gytes +gyved +gyves +haafs +haars +hable +habus +hacek +hacks +hadal +haded +hades +hadji +hadst +haems +haets +haffs +hafiz +hafts +haggs +hahas +haick +haika +haiks +haiku +hails +haily +hains +haint +hairs +haith +hajes +hajis +hajji +hakam +hakas +hakea +hakes +hakim +hakus +halal +haled +haler +hales +halfa +halfs +halid +hallo +halls +halma +halms +halon +halos +halse +halts +halva +halwa +hamal +hamba +hamed +hames +hammy +hamza +hanap +hance +hanch +hands +hangi +hangs +hanks +hanky +hansa +hanse +hants +haole +haoma +hapax +haply +happi +hapus +haram +hards +hared +hares +harim +harks +harls +harms +harns +haros +harps +harts +hashy +hasks +hasps +hasta +hated +hates +hatha +hauds +haufs +haugh +hauld +haulm +hauls +hault +hauns +hause +haver +haves +hawed +hawks +hawms +hawse +hayed +hayer +hayey +hayle +hazan +hazed +hazer +hazes +heads +heald +heals +heame +heaps +heapy +heare +hears +heast +heats +heben +hebes +hecht +hecks +heder +hedgy +heeds +heedy +heels +heeze +hefte +hefts +heids +heigh +heils +heirs +hejab +hejra +heled +heles +helio +hells +helms +helos +helot +helps +helve +hemal +hemes +hemic +hemin +hemps +hempy +hench +hends +henge +henna +henny +henry +hents +hepar +herbs +herby +herds +heres +herls +herma +herms +herns +heros +herry +herse +hertz +herye +hesps +hests +hetes +heths +heuch +heugh +hevea +hewed +hewer +hewgh +hexad +hexed +hexer +hexes +hexyl +heyed +hiant +hicks +hided +hider +hides +hiems +highs +hight +hijab +hijra +hiked +hiker +hikes +hikoi +hilar +hilch +hillo +hills +hilts +hilum +hilus +himbo +hinau +hinds +hings +hinky +hinny +hints +hiois +hiply +hired +hiree +hirer +hires +hissy +hists +hithe +hived +hiver +hives +hizen +hoaed +hoagy +hoars +hoary +hoast +hobos +hocks +hocus +hodad +hodja +hoers +hogan +hogen +hoggs +hoghs +hohed +hoick +hoied +hoiks +hoing +hoise +hokas +hoked +hokes +hokey +hokis +hokku +hokum +holds +holed +holes +holey +holks +holla +hollo +holme +holms +holon +holos +holts +homas +homed +homes +homey +homie +homme +homos +honan +honda +honds +honed +honer +hones +hongi +hongs +honks +honky +hooch +hoods +hoody +hooey +hoofs +hooka +hooks +hooky +hooly +hoons +hoops +hoord +hoors +hoosh +hoots +hooty +hoove +hopak +hoped +hoper +hopes +hoppy +horah +horal +horas +horis +horks +horme +horns +horst +horsy +hosed +hosel +hosen +hoser +hoses +hosey +hosta +hosts +hotch +hoten +hotty +houff +houfs +hough +houri +hours +houts +hovea +hoved +hoven +hoves +howbe +howes +howff +howfs +howks +howls +howre +howso +hoxed +hoxes +hoyas +hoyed +hoyle +hubby +hucks +hudna +hudud +huers +huffs +huffy +huger +huggy +huhus +huias +hulas +hules +hulks +hulky +hullo +hulls +hully +humas +humfs +humic +humps +humpy +hunks +hunts +hurds +hurls +hurly +hurra +hurst +hurts +hushy +husks +husos +hutia +huzza +huzzy +hwyls +hydra +hyens +hygge +hying +hykes +hylas +hyleg +hyles +hylic +hymns +hynde +hyoid +hyped +hypes +hypha +hyphy +hypos +hyrax +hyson +hythe +iambi +iambs +ibrik +icers +iched +iches +ichor +icier +icker +ickle +icons +ictal +ictic +ictus +idant +ideas +idees +ident +idled +idles +idola +idols +idyls +iftar +igapo +igged +iglus +ihram +ikans +ikats +ikons +ileac +ileal +ileum +ileus +iliad +ilial +ilium +iller +illth +imago +imams +imari +imaum +imbar +imbed +imide +imido +imids +imine +imino +immew +immit +immix +imped +impis +impot +impro +imshi +imshy +inapt +inarm +inbye +incel +incle +incog +incus +incut +indew +india +indie +indol +indow +indri +indue +inerm +infix +infos +infra +ingan +ingle +inion +inked +inker +inkle +inned +innit +inorb +inrun +inset +inspo +intel +intil +intis +intra +inula +inure +inurn +inust +invar +inwit +iodic +iodid +iodin +iotas +ippon +irade +irids +iring +irked +iroko +irone +irons +isbas +ishes +isled +isles +isnae +issei +istle +items +ither +ivied +ivies +ixias +ixnay +ixora +ixtle +izard +izars +izzat +jaaps +jabot +jacal +jacks +jacky +jaded +jades +jafas +jaffa +jagas +jager +jaggs +jaggy +jagir +jagra +jails +jaker +jakes +jakey +jalap +jalop +jambe +jambo +jambs +jambu +james +jammy +jamon +janes +janns +janny +janty +japan +japed +japer +japes +jarks +jarls +jarps +jarta +jarul +jasey +jaspe +jasps +jatos +jauks +jaups +javas +javel +jawan +jawed +jaxie +jeans +jeats +jebel +jedis +jeels +jeely +jeeps +jeers +jeeze +jefes +jeffs +jehad +jehus +jelab +jello +jells +jembe +jemmy +jenny +jeons +jerid +jerks +jerry +jesse +jests +jesus +jetes +jeton +jeune +jewed +jewie +jhala +jiaos +jibba +jibbs +jibed +jiber +jibes +jiffs +jiggy +jigot +jihad +jills +jilts +jimmy +jimpy +jingo +jinks +jinne +jinni +jinns +jirds +jirga +jirre +jisms +jived +jiver +jives +jivey +jnana +jobed +jobes +jocko +jocks +jocky +jocos +jodel +joeys +johns +joins +joked +jokes +jokey +jokol +joled +joles +jolls +jolts +jolty +jomon +jomos +jones +jongs +jonty +jooks +joram +jorum +jotas +jotty +jotun +joual +jougs +jouks +joule +jours +jowar +jowed +jowls +jowly +joyed +jubas +jubes +jucos +judas +judgy +judos +jugal +jugum +jujus +juked +jukes +jukus +julep +jumar +jumby +jumps +junco +junks +junky +jupes +jupon +jural +jurat +jurel +jures +justs +jutes +jutty +juves +juvie +kaama +kabab +kabar +kabob +kacha +kacks +kadai +kades +kadis +kafir +kagos +kagus +kahal +kaiak +kaids +kaies +kaifs +kaika +kaiks +kails +kaims +kaing +kains +kakas +kakis +kalam +kales +kalif +kalis +kalpa +kamas +kames +kamik +kamis +kamme +kanae +kanas +kandy +kaneh +kanes +kanga +kangs +kanji +kants +kanzu +kaons +kapas +kaphs +kapok +kapow +kapus +kaput +karas +karat +karks +karns +karoo +karos +karri +karst +karsy +karts +karzy +kasha +kasme +katal +katas +katis +katti +kaugh +kauri +kauru +kaury +kaval +kavas +kawas +kawau +kawed +kayle +kayos +kazis +kazoo +kbars +kebar +kebob +kecks +kedge +kedgy +keech +keefs +keeks +keels +keema +keeno +keens +keeps +keets +keeve +kefir +kehua +keirs +kelep +kelim +kells +kelly +kelps +kelpy +kelts +kelty +kembo +kembs +kemps +kempt +kempy +kenaf +kench +kendo +kenos +kente +kents +kepis +kerbs +kerel +kerfs +kerky +kerma +kerne +kerns +keros +kerry +kerve +kesar +kests +ketas +ketch +ketes +ketol +kevel +kevil +kexes +keyed +keyer +khadi +khafs +khans +khaph +khats +khaya +khazi +kheda +kheth +khets +khoja +khors +khoum +khuds +kiaat +kiack +kiang +kibbe +kibbi +kibei +kibes +kibla +kicks +kicky +kiddo +kiddy +kidel +kidge +kiefs +kiers +kieve +kievs +kight +kikes +kikoi +kiley +kilim +kills +kilns +kilos +kilps +kilts +kilty +kimbo +kinas +kinda +kinds +kindy +kines +kings +kinin +kinks +kinos +kiore +kipes +kippa +kipps +kirby +kirks +kirns +kirri +kisan +kissy +kists +kited +kiter +kites +kithe +kiths +kitul +kivas +kiwis +klang +klaps +klett +klick +klieg +kliks +klong +kloof +kluge +klutz +knags +knaps +knarl +knars +knaur +knawe +knees +knell +knish +knits +knive +knobs +knops +knosp +knots +knout +knowe +knows +knubs +knurl +knurr +knurs +knuts +koans +koaps +koban +kobos +koels +koffs +kofta +kogal +kohas +kohen +kohls +koine +kojis +kokam +kokas +koker +kokra +kokum +kolas +kolos +kombu +konbu +kondo +konks +kooks +kooky +koori +kopek +kophs +kopje +koppa +korai +koras +korat +kores +korma +koros +korun +korus +koses +kotch +kotos +kotow +koura +kraal +krabs +kraft +krais +krait +krang +krans +kranz +kraut +krays +kreep +kreng +krewe +krona +krone +kroon +krubi +krunk +ksars +kubie +kudos +kudus +kudzu +kufis +kugel +kuias +kukri +kukus +kulak +kulan +kulas +kulfi +kumis +kumys +kuris +kurre +kurta +kurus +kusso +kutas +kutch +kutis +kutus +kuzus +kvass +kvell +kwela +kyack +kyaks +kyang +kyars +kyats +kybos +kydst +kyles +kylie +kylin +kylix +kyloe +kynde +kynds +kypes +kyrie +kytes +kythe +laari +labda +labia +labis +labra +laced +lacer +laces +lacet +lacey +lacks +laddy +laded +lader +lades +laers +laevo +lagan +lahal +lahar +laich +laics +laids +laigh +laika +laiks +laird +lairs +lairy +laith +laity +laked +laker +lakes +lakhs +lakin +laksa +laldy +lalls +lamas +lambs +lamby +lamed +lamer +lames +lamia +lammy +lamps +lanai +lanas +lanch +lande +lands +lanes +lanks +lants +lapin +lapis +lapje +larch +lards +lardy +laree +lares +largo +laris +larks +larky +larns +larnt +larum +lased +laser +lases +lassi +lassu +lassy +lasts +latah +lated +laten +latex +lathi +laths +lathy +latke +latus +lauan +lauch +lauds +laufs +laund +laura +laval +lavas +laved +laver +laves +lavra +lavvy +lawed +lawer +lawin +lawks +lawns +lawny +laxed +laxer +laxes +laxly +layed +layin +layup +lazar +lazed +lazes +lazos +lazzi +lazzo +leads +leady +leafs +leaks +leams +leans +leany +leaps +leare +lears +leary +leats +leavy +leaze +leben +leccy +ledes +ledgy +ledum +leear +leeks +leeps +leers +leese +leets +leeze +lefte +lefts +leger +leges +legge +leggo +legit +lehrs +lehua +leirs +leish +leman +lemed +lemel +lemes +lemma +lemme +lends +lenes +lengs +lenis +lenos +lense +lenti +lento +leone +lepid +lepra +lepta +lered +leres +lerps +lesbo +leses +lests +letch +lethe +letup +leuch +leuco +leuds +leugh +levas +levee +leves +levin +levis +lewis +lexes +lexis +lezes +lezza +lezzy +liana +liane +liang +liard +liars +liart +liber +libra +libri +lichi +licht +licit +licks +lidar +lidos +liefs +liens +liers +lieus +lieve +lifer +lifes +lifts +ligan +liger +ligge +ligne +liked +liker +likes +likin +lills +lilos +lilts +liman +limas +limax +limba +limbi +limbs +limby +limed +limen +limes +limey +limma +limns +limos +limpa +limps +linac +linch +linds +lindy +lined +lines +liney +linga +lings +lingy +linin +links +linky +linns +linny +linos +lints +linty +linum +linux +lions +lipas +lipes +lipin +lipos +lippy +liras +lirks +lirot +lisks +lisle +lisps +lists +litai +litas +lited +liter +lites +litho +liths +litre +lived +liven +lives +livor +livre +llano +loach +loads +loafs +loams +loans +loast +loave +lobar +lobed +lobes +lobos +lobus +loche +lochs +locie +locis +locks +locos +locum +loden +lodes +loess +lofts +logan +loges +loggy +logia +logie +logoi +logon +logos +lohan +loids +loins +loipe +loirs +lokes +lolls +lolly +lolog +lomas +lomed +lomes +loner +longa +longe +longs +looby +looed +looey +loofa +loofs +looie +looks +looky +looms +loons +loony +loops +loord +loots +loped +loper +lopes +loppy +loral +loran +lords +lordy +lorel +lores +loric +loris +losed +losel +losen +loses +lossy +lotah +lotas +lotes +lotic +lotos +lotsa +lotta +lotte +lotto +lotus +loued +lough +louie +louis +louma +lound +louns +loupe +loups +loure +lours +loury +louts +lovat +loved +loves +lovey +lovie +lowan +lowed +lowes +lownd +lowne +lowns +lowps +lowry +lowse +lowts +loxed +loxes +lozen +luach +luaus +lubed +lubes +lubra +luces +lucks +lucre +ludes +ludic +ludos +luffa +luffs +luged +luger +luges +lulls +lulus +lumas +lumbi +lumme +lummy +lumps +lunas +lunes +lunet +lungi +lungs +lunks +lunts +lupin +lured +lurer +lures +lurex +lurgi +lurgy +lurks +lurry +lurve +luser +lushy +lusks +lusts +lusus +lutea +luted +luter +lutes +luvvy +luxed +luxer +luxes +lweis +lyams +lyard +lyart +lyase +lycea +lycee +lycra +lymes +lynes +lyres +lysed +lyses +lysin +lysis +lysol +lyssa +lyted +lytes +lythe +lytic +lytta +maaed +maare +maars +mabes +macas +maced +macer +maces +mache +machi +machs +macks +macle +macon +madge +madid +madre +maerl +mafic +mages +maggs +magot +magus +mahoe +mahua +mahwa +maids +maiko +maiks +maile +maill +mails +maims +mains +maire +mairs +maise +maist +makar +makes +makis +makos +malam +malar +malas +malax +males +malic +malik +malis +malls +malms +malmy +malts +malty +malus +malva +malwa +mamas +mamba +mamee +mamey +mamie +manas +manat +mandi +maneb +maned +maneh +manes +manet +mangs +manis +manky +manna +manos +manse +manta +manto +manty +manul +manus +mapau +maqui +marae +marah +maras +marcs +mardy +mares +marge +margs +maria +marid +marka +marks +marle +marls +marly +marms +maron +maror +marra +marri +marse +marts +marvy +masas +mased +maser +mases +mashy +masks +massa +massy +masts +masty +masus +matai +mated +mater +mates +maths +matin +matlo +matte +matts +matza +matzo +mauby +mauds +mauls +maund +mauri +mausy +mauts +mauzy +maven +mavie +mavin +mavis +mawed +mawks +mawky +mawns +mawrs +maxed +maxes +maxis +mayan +mayas +mayed +mayos +mayst +mazed +mazer +mazes +mazey +mazut +mbira +meads +meals +meane +means +meany +meare +mease +meath +meats +mebos +mechs +mecks +medii +medle +meeds +meers +meets +meffs +meins +meint +meiny +meith +mekka +melas +melba +melds +melic +melik +mells +melts +melty +memes +memos +menad +mends +mened +menes +menge +mengs +mensa +mense +mensh +menta +mento +menus +meous +meows +merch +mercs +merde +mered +merel +merer +meres +meril +meris +merks +merle +merls +merse +mesal +mesas +mesel +meses +meshy +mesic +mesne +meson +messy +mesto +meted +metes +metho +meths +metic +metif +metis +metol +metre +meuse +meved +meves +mewed +mewls +meynt +mezes +mezze +mezzo +mhorr +miaou +miaow +miasm +miaul +micas +miche +micht +micks +micky +micos +micra +middy +midgy +midis +miens +mieve +miffs +miffy +mifty +miggs +mihas +mihis +miked +mikes +mikra +mikva +milch +milds +miler +miles +milfs +milia +milko +milks +mille +mills +milor +milos +milpa +milts +milty +miltz +mimed +mimeo +mimer +mimes +mimsy +minae +minar +minas +mincy +minds +mined +mines +minge +mings +mingy +minis +minke +minks +minny +minos +mints +mired +mires +mirex +mirid +mirin +mirks +mirky +mirly +miros +mirvs +mirza +misch +misdo +mises +misgo +misos +missa +mists +misty +mitch +miter +mites +mitis +mitre +mitts +mixed +mixen +mixer +mixes +mixte +mixup +mizen +mizzy +mneme +moans +moats +mobby +mobes +mobey +mobie +moble +mochi +mochs +mochy +mocks +moder +modes +modge +modii +modus +moers +mofos +moggy +mohel +mohos +mohrs +mohua +mohur +moile +moils +moira +moire +moits +mojos +mokes +mokis +mokos +molal +molas +molds +moled +moles +molla +molls +molly +molto +molts +molys +momes +momma +mommy +momus +monad +monal +monas +monde +mondo +moner +mongo +mongs +monic +monie +monks +monos +monte +monty +moobs +mooch +moods +mooed +mooks +moola +mooli +mools +mooly +moong +moons +moony +moops +moors +moory +moots +moove +moped +moper +mopes +mopey +moppy +mopsy +mopus +morae +moras +morat +moray +morel +mores +moria +morne +morns +morra +morro +morse +morts +mosed +moses +mosey +mosks +mosso +moste +mosts +moted +moten +motes +motet +motey +moths +mothy +motis +motte +motts +motty +motus +motza +mouch +moues +mould +mouls +moups +moust +mousy +moved +moves +mowas +mowed +mowra +moxas +moxie +moyas +moyle +moyls +mozed +mozes +mozos +mpret +mucho +mucic +mucid +mucin +mucks +mucor +mucro +mudge +mudir +mudra +muffs +mufti +mugga +muggs +muggy +muhly +muids +muils +muirs +muist +mujik +mulct +muled +mules +muley +mulga +mulie +mulla +mulls +mulse +mulsh +mumms +mumps +mumsy +mumus +munga +munge +mungo +mungs +munis +munts +muntu +muons +muras +mured +mures +murex +murid +murks +murls +murly +murra +murre +murri +murrs +murry +murti +murva +musar +musca +mused +muser +muses +muset +musha +musit +musks +musos +musse +mussy +musth +musts +mutch +muted +muter +mutes +mutha +mutis +muton +mutts +muxed +muxes +muzak +muzzy +mvule +myall +mylar +mynah +mynas +myoid +myoma +myope +myops +myopy +mysid +mythi +myths +mythy +myxos +mzees +naams +naans +nabes +nabis +nabks +nabla +nabob +nache +nacho +nacre +nadas +naeve +naevi +naffs +nagas +naggy +nagor +nahal +naiad +naifs +naiks +nails +naira +nairu +naked +naker +nakfa +nalas +naled +nalla +named +namer +names +namma +namus +nanas +nance +nancy +nandu +nanna +nanos +nanua +napas +naped +napes +napoo +nappa +nappe +nappy +naras +narco +narcs +nards +nares +naric +naris +narks +narky +narre +nashi +natch +nates +natis +natty +nauch +naunt +navar +naves +navew +navvy +nawab +nazes +nazir +nazis +nduja +neafe +neals +neaps +nears +neath +neats +nebek +nebel +necks +neddy +needs +neeld +neele +neemb +neems +neeps +neese +neeze +negro +negus +neifs +neist +neive +nelis +nelly +nemas +nemns +nempt +nenes +neons +neper +nepit +neral +nerds +nerka +nerks +nerol +nerts +nertz +nervy +nests +netes +netop +netts +netty +neuks +neume +neums +nevel +neves +nevus +newbs +newed +newel +newie +newsy +newts +nexts +nexus +ngaio +ngana +ngati +ngoma +ngwee +nicad +nicht +nicks +nicol +nidal +nided +nides +nidor +nidus +niefs +nieve +nifes +niffs +niffy +nifty +niger +nighs +nihil +nikab +nikah +nikau +nills +nimbi +nimbs +nimps +niner +nines +ninon +nipas +nippy +niqab +nirls +nirly +nisei +nisse +nisus +niter +nites +nitid +niton +nitre +nitro +nitry +nitty +nival +nixed +nixer +nixes +nixie +nizam +nkosi +noahs +nobby +nocks +nodal +noddy +nodes +nodus +noels +noggs +nohow +noils +noily +noint +noirs +noles +nolls +nolos +nomas +nomen +nomes +nomic +nomoi +nomos +nonas +nonce +nones +nonet +nongs +nonis +nonny +nonyl +noobs +nooit +nooks +nooky +noons +noops +nopal +noria +noris +norks +norma +norms +nosed +noser +noses +notal +noted +noter +notes +notum +nould +noule +nouls +nouns +nouny +noups +novae +novas +novum +noway +nowed +nowls +nowts +nowty +noxal +noxes +noyau +noyed +noyes +nubby +nubia +nucha +nuddy +nuder +nudes +nudie +nudzh +nuffs +nugae +nuked +nukes +nulla +nulls +numbs +numen +nummy +nunny +nurds +nurdy +nurls +nurrs +nutso +nutsy +nyaff +nyala +nying +nyssa +oaked +oaker +oakum +oared +oases +oasis +oasts +oaten +oater +oaths +oaves +obang +obeah +obeli +obeys +obias +obied +obiit +obits +objet +oboes +obole +oboli +obols +occam +ocher +oches +ochre +ochry +ocker +ocrea +octad +octan +octas +octyl +oculi +odahs +odals +odeon +odeum +odism +odist +odium +odors +odour +odyle +odyls +ofays +offed +offie +oflag +ofter +ogams +ogeed +ogees +oggin +ogham +ogive +ogled +ogler +ogles +ogmic +ogres +ohias +ohing +ohmic +ohone +oidia +oiled +oiler +oinks +oints +ojime +okapi +okays +okehs +okras +oktas +oldie +oleic +olein +olent +oleos +oleum +olios +ollas +ollav +oller +ollie +ology +olpae +olpes +omasa +omber +ombus +omens +omers +omits +omlah +omovs +omrah +oncer +onces +oncet +oncus +onely +oners +onery +onium +onkus +onlay +onned +ontic +oobit +oohed +oomph +oonts +ooped +oorie +ooses +ootid +oozed +oozes +opahs +opals +opens +opepe +oping +oppos +opsin +opted +opter +orach +oracy +orals +orang +orant +orate +orbed +orcas +orcin +ordos +oread +orfes +orgia +orgic +orgue +oribi +oriel +orixa +orles +orlon +orlop +ormer +ornis +orpin +orris +ortho +orval +orzos +oscar +oshac +osier +osmic +osmol +ossia +ostia +otaku +otary +ottar +ottos +oubit +oucht +ouens +ouija +oulks +oumas +oundy +oupas +ouped +ouphe +ouphs +ourie +ousel +ousts +outby +outed +outre +outro +outta +ouzel +ouzos +ovals +ovels +ovens +overs +ovist +ovoli +ovolo +ovule +owche +owies +owled +owler +owlet +owned +owres +owrie +owsen +oxbow +oxers +oxeye +oxids +oxies +oxime +oxims +oxlip +oxter +oyers +ozeki +ozzie +paals +paans +pacas +paced +pacer +paces +pacey +pacha +packs +pacos +pacta +pacts +padis +padle +padma +padre +padri +paean +paedo +paeon +paged +pager +pages +pagle +pagod +pagri +paiks +pails +pains +paire +pairs +paisa +paise +pakka +palas +palay +palea +paled +pales +palet +palis +palki +palla +palls +pally +palms +palmy +palpi +palps +palsa +pampa +panax +pance +panda +pands +pandy +paned +panes +panga +pangs +panim +panko +panne +panni +panto +pants +panty +paoli +paolo +papas +papaw +papes +pappi +pappy +parae +paras +parch +pardi +pards +pardy +pared +paren +pareo +pares +pareu +parev +parge +pargo +paris +parki +parks +parky +parle +parly +parma +parol +parps +parra +parrs +parti +parts +parve +parvo +paseo +pases +pasha +pashm +paska +paspy +passe +pasts +pated +paten +pater +pates +paths +patin +patka +patly +patte +patus +pauas +pauls +pavan +paved +paven +paver +paves +pavid +pavin +pavis +pawas +pawaw +pawed +pawer +pawks +pawky +pawls +pawns +paxes +payed +payor +paysd +peage +peags +peaks +peaky +peals +peans +peare +pears +peart +pease +peats +peaty +peavy +peaze +pebas +pechs +pecke +pecks +pecky +pedes +pedis +pedro +peece +peeks +peels +peens +peeoy +peepe +peeps +peers +peery +peeve +peggy +peghs +peins +peise +peize +pekan +pekes +pekin +pekoe +pelas +pelau +peles +pelfs +pells +pelma +pelon +pelta +pelts +pends +pendu +pened +penes +pengo +penie +penis +penks +penna +penni +pents +peons +peony +pepla +pepos +peppy +pepsi +perai +perce +percs +perdu +perdy +perea +peres +peris +perks +perms +perns +perog +perps +perry +perse +perst +perts +perve +pervo +pervs +pervy +pesos +pests +pesty +petar +peter +petit +petre +petri +petti +petto +pewee +pewit +peyse +phage +phang +phare +pharm +pheer +phene +pheon +phese +phial +phish +phizz +phlox +phoca +phono +phons +phots +phpht +phuts +phyla +phyle +piani +pians +pibal +pical +picas +piccy +picks +picot +picra +picul +piend +piers +piert +pieta +piets +piezo +pight +pigmy +piing +pikas +pikau +piked +piker +pikes +pikey +pikis +pikul +pilae +pilaf +pilao +pilar +pilau +pilaw +pilch +pilea +piled +pilei +piler +piles +pilis +pills +pilow +pilum +pilus +pimas +pimps +pinas +pined +pines +pingo +pings +pinko +pinks +pinna +pinny +pinon +pinot +pinta +pints +pinup +pions +piony +pious +pioye +pioys +pipal +pipas +piped +pipes +pipet +pipis +pipit +pippy +pipul +pirai +pirls +pirns +pirog +pisco +pises +pisky +pisos +pissy +piste +pitas +piths +piton +pitot +pitta +piums +pixes +pized +pizes +plaas +plack +plage +plans +plaps +plash +plasm +plast +plats +platt +platy +playa +plays +pleas +plebe +plebs +plena +pleon +plesh +plews +plica +plies +plims +pling +plink +ploat +plods +plong +plonk +plook +plops +plots +plotz +plouk +plows +ploye +ploys +plues +pluff +plugs +plums +plumy +pluot +pluto +plyer +poach +poaka +poake +poboy +pocks +pocky +podal +poddy +podex +podge +podgy +podia +poems +poeps +poets +pogey +pogge +pogos +pohed +poilu +poind +pokal +poked +pokes +pokey +pokie +poled +poler +poles +poley +polio +polis +polje +polks +polls +polly +polos +polts +polys +pombe +pomes +pommy +pomos +pomps +ponce +poncy +ponds +pones +poney +ponga +pongo +pongs +pongy +ponks +ponts +ponty +ponzu +poods +pooed +poofs +poofy +poohs +pooja +pooka +pooks +pools +poons +poops +poopy +poori +poort +poots +poove +poovy +popes +poppa +popsy +porae +poral +pored +porer +pores +porge +porgy +porin +porks +porky +porno +porns +porny +porta +ports +porty +posed +poses +posey +posho +posts +potae +potch +poted +potes +potin +potoo +potsy +potto +potts +potty +pouff +poufs +pouke +pouks +poule +poulp +poult +poupe +poupt +pours +pouts +powan +powin +pownd +powns +powny +powre +poxed +poxes +poynt +poyou +poyse +pozzy +praam +prads +prahu +prams +prana +prang +praos +prase +prate +prats +pratt +praty +praus +prays +predy +preed +prees +preif +prems +premy +prent +preon +preop +preps +presa +prese +prest +preve +prexy +preys +prial +pricy +prief +prier +pries +prigs +prill +prima +primi +primp +prims +primy +prink +prion +prise +priss +proas +probs +prods +proem +profs +progs +proin +proke +prole +proll +promo +proms +pronk +props +prore +proso +pross +prost +prosy +proto +proul +prows +proyn +prunt +pruta +pryer +pryse +pseud +pshaw +psion +psoae +psoai +psoas +psora +psych +psyop +pubco +pubes +pubis +pucan +pucer +puces +pucka +pucks +puddy +pudge +pudic +pudor +pudsy +pudus +puers +puffa +puffs +puggy +pugil +puhas +pujah +pujas +pukas +puked +puker +pukes +pukey +pukka +pukus +pulao +pulas +puled +puler +pules +pulik +pulis +pulka +pulks +pulli +pulls +pully +pulmo +pulps +pulus +pumas +pumie +pumps +punas +punce +punga +pungs +punji +punka +punks +punky +punny +punto +punts +punty +pupae +pupas +pupus +purda +pured +pures +purin +puris +purls +purpy +purrs +pursy +purty +puses +pusle +pussy +putid +puton +putti +putto +putts +puzel +pwned +pyats +pyets +pygal +pyins +pylon +pyned +pynes +pyoid +pyots +pyral +pyran +pyres +pyrex +pyric +pyros +pyxed +pyxes +pyxie +pyxis +pzazz +qadis +qaids +qajaq +qanat +qapik +qibla +qophs +qorma +quads +quaff +quags +quair +quais +quaky +quale +quant +quare +quass +quate +quats +quayd +quays +qubit +quean +queme +quena +quern +queyn +queys +quich +quids +quiff +quims +quina +quine +quino +quins +quint +quipo +quips +quipu +quire +quirt +quist +quits +quoad +quods +quoif +quoin +quoit +quoll +quonk +quops +qursh +quyte +rabat +rabic +rabis +raced +races +rache +racks +racon +radge +radix +radon +raffs +rafts +ragas +ragde +raged +ragee +rager +rages +ragga +raggs +raggy +ragis +ragus +rahed +rahui +raias +raids +raiks +raile +rails +raine +rains +raird +raita +raits +rajas +rajes +raked +rakee +raker +rakes +rakia +rakis +rakus +rales +ramal +ramee +ramet +ramie +ramin +ramis +rammy +ramps +ramus +ranas +rance +rands +ranee +ranga +rangi +rangs +rangy +ranid +ranis +ranke +ranks +rants +raped +raper +rapes +raphe +rappe +rared +raree +rares +rarks +rased +raser +rases +rasps +rasse +rasta +ratal +ratan +ratas +ratch +rated +ratel +rater +rates +ratha +rathe +raths +ratoo +ratos +ratus +rauns +raupo +raved +ravel +raver +raves +ravey +ravin +rawer +rawin +rawly +rawns +raxed +raxes +rayah +rayas +rayed +rayle +rayne +razed +razee +razer +razes +razoo +readd +reads +reais +reaks +realo +reals +reame +reams +reamy +reans +reaps +rears +reast +reata +reate +reave +rebbe +rebec +rebid +rebit +rebop +rebuy +recal +recce +recco +reccy +recit +recks +recon +recta +recti +recto +redan +redds +reddy +reded +redes +redia +redid +redip +redly +redon +redos +redox +redry +redub +redux +redye +reech +reede +reeds +reefs +reefy +reeks +reeky +reels +reens +reest +reeve +refed +refel +reffo +refis +refix +refly +refry +regar +reges +reggo +regie +regma +regna +regos +regur +rehem +reifs +reify +reiki +reiks +reink +reins +reird +reist +reive +rejig +rejon +reked +rekes +rekey +relet +relie +relit +rello +reman +remap +remen +remet +remex +remix +renay +rends +reney +renga +renig +renin +renne +renos +rente +rents +reoil +reorg +repeg +repin +repla +repos +repot +repps +repro +reran +rerig +resat +resaw +resay +resee +reses +resew +resid +resit +resod +resow +resto +rests +resty +resus +retag +retax +retem +retia +retie +retox +revet +revie +rewan +rewax +rewed +rewet +rewin +rewon +rewth +rexes +rezes +rheas +rheme +rheum +rhies +rhime +rhine +rhody +rhomb +rhone +rhumb +rhyne +rhyta +riads +rials +riant +riata +ribas +ribby +ribes +riced +ricer +rices +ricey +richt +ricin +ricks +rides +ridgy +ridic +riels +riems +rieve +rifer +riffs +rifte +rifts +rifty +riggs +rigol +riled +riles +riley +rille +rills +rimae +rimed +rimer +rimes +rimus +rinds +rindy +rines +rings +rinks +rioja +riots +riped +ripes +ripps +rises +rishi +risks +risps +risus +rites +ritts +ritzy +rivas +rived +rivel +riven +rives +riyal +rizas +roads +roams +roans +roars +roary +roate +robed +robes +roble +rocks +roded +rodes +roguy +rohes +roids +roils +roily +roins +roist +rojak +rojis +roked +roker +rokes +rolag +roles +rolfs +rolls +romal +roman +romeo +romps +ronde +rondo +roneo +rones +ronin +ronne +ronte +ronts +roods +roofs +roofy +rooks +rooky +rooms +roons +roops +roopy +roosa +roose +roots +rooty +roped +roper +ropes +ropey +roque +roral +rores +roric +rorid +rorie +rorts +rorty +rosed +roses +roset +roshi +rosin +rosit +rosti +rosts +rotal +rotan +rotas +rotch +roted +rotes +rotis +rotls +roton +rotos +rotte +rouen +roues +roule +rouls +roums +roups +roupy +roust +routh +routs +roved +roven +roves +rowan +rowed +rowel +rowen +rowie +rowme +rownd +rowth +rowts +royne +royst +rozet +rozit +ruana +rubai +rubby +rubel +rubes +rubin +ruble +rubli +rubus +ruche +rucks +rudas +rudds +rudes +rudie +rudis +rueda +ruers +ruffe +ruffs +rugae +rugal +ruggy +ruing +ruins +rukhs +ruled +rules +rumal +rumbo +rumen +rumes +rumly +rummy +rumpo +rumps +rumpy +runch +runds +runed +runes +rungs +runic +runny +runts +runty +rupia +rurps +rurus +rusas +ruses +rushy +rusks +rusma +russe +rusts +ruths +rutin +rutty +ryals +rybat +ryked +rykes +rymme +rynds +ryots +ryper +saags +sabal +sabed +saber +sabes +sabha +sabin +sabir +sable +sabot +sabra +sabre +sacks +sacra +saddo +sades +sadhe +sadhu +sadis +sados +sadza +safed +safes +sagas +sager +sages +saggy +sagos +sagum +saheb +sahib +saice +saick +saics +saids +saiga +sails +saims +saine +sains +sairs +saist +saith +sajou +sakai +saker +sakes +sakia +sakis +sakti +salal +salat +salep +sales +salet +salic +salix +salle +salmi +salol +salop +salpa +salps +salse +salto +salts +salue +salut +saman +samas +samba +sambo +samek +samel +samen +sames +samey +samfu +sammy +sampi +samps +sands +saned +sanes +sanga +sangh +sango +sangs +sanko +sansa +santo +sants +saola +sapan +sapid +sapor +saran +sards +sared +saree +sarge +sargo +sarin +saris +sarks +sarky +sarod +saros +sarus +saser +sasin +sasse +satai +satay +sated +satem +sates +satis +sauba +sauch +saugh +sauls +sault +saunt +saury +sauts +saved +saver +saves +savey +savin +sawah +sawed +sawer +saxes +sayed +sayer +sayid +sayne +sayon +sayst +sazes +scabs +scads +scaff +scags +scail +scala +scall +scams +scand +scans +scapa +scape +scapi +scarp +scars +scart +scath +scats +scatt +scaud +scaup +scaur +scaws +sceat +scena +scend +schav +schmo +schul +schwa +sclim +scody +scogs +scoog +scoot +scopa +scops +scots +scoug +scoup +scowp +scows +scrab +scrae +scrag +scran +scrat +scraw +scray +scrim +scrip +scrob +scrod +scrog +scrow +scudi +scudo +scuds +scuff +scuft +scugs +sculk +scull +sculp +sculs +scums +scups +scurf +scurs +scuse +scuta +scute +scuts +scuzz +scyes +sdayn +sdein +seals +seame +seams +seamy +seans +seare +sears +sease +seats +seaze +sebum +secco +sechs +sects +seder +sedes +sedge +sedgy +sedum +seeds +seeks +seeld +seels +seely +seems +seeps +seepy +seers +sefer +segar +segni +segno +segol +segos +sehri +seifs +seils +seine +seirs +seise +seism +seity +seiza +sekos +sekts +selah +seles +selfs +sella +selle +sells +selva +semee +semes +semie +semis +senas +sends +senes +sengi +senna +senor +sensa +sensi +sente +senti +sents +senvy +senza +sepad +sepal +sepic +sepoy +septa +septs +serac +serai +seral +sered +serer +seres +serfs +serge +seric +serin +serks +seron +serow +serra +serre +serrs +serry +servo +sesey +sessa +setae +setal +seton +setts +sewan +sewar +sewed +sewel +sewen +sewin +sexed +sexer +sexes +sexto +sexts +seyen +shads +shags +shahs +shako +shakt +shalm +shaly +shama +shams +shand +shans +shaps +sharn +shash +shaul +shawm +shawn +shaws +shaya +shays +shchi +sheaf +sheal +sheas +sheds +sheel +shend +shent +sheol +sherd +shere +shero +shets +sheva +shewn +shews +shiai +shiel +shier +shies +shill +shily +shims +shins +ships +shirr +shirs +shish +shiso +shist +shite +shits +shiur +shiva +shive +shivs +shlep +shlub +shmek +shmoe +shoat +shoed +shoer +shoes +shogi +shogs +shoji +shojo +shola +shool +shoon +shoos +shope +shops +shorl +shote +shots +shott +showd +shows +shoyu +shred +shris +shrow +shtik +shtum +shtup +shule +shuln +shuls +shuns +shura +shute +shuts +shwas +shyer +sials +sibbs +sibyl +sices +sicht +sicko +sicks +sicky +sidas +sided +sider +sides +sidha +sidhe +sidle +sield +siens +sient +sieth +sieur +sifts +sighs +sigil +sigla +signa +signs +sijos +sikas +siker +sikes +silds +siled +silen +siler +siles +silex +silks +sills +silos +silts +silty +silva +simar +simas +simba +simis +simps +simul +sinds +sined +sines +sings +sinhs +sinks +sinky +sinus +siped +sipes +sippy +sired +siree +sires +sirih +siris +siroc +sirra +sirup +sisal +sises +sista +sists +sitar +sited +sites +sithe +sitka +situp +situs +siver +sixer +sixes +sixmo +sixte +sizar +sized +sizel +sizer +sizes +skags +skail +skald +skank +skart +skats +skatt +skaws +skean +skear +skeds +skeed +skeef +skeen +skeer +skees +skeet +skegg +skegs +skein +skelf +skell +skelm +skelp +skene +skens +skeos +skeps +skers +skets +skews +skids +skied +skies +skiey +skimo +skims +skink +skins +skint +skios +skips +skirl +skirr +skite +skits +skive +skivy +sklim +skoal +skody +skoff +skogs +skols +skool +skort +skosh +skran +skrik +skuas +skugs +skyed +skyer +skyey +skyfs +skyre +skyrs +skyte +slabs +slade +slaes +slags +slaid +slake +slams +slane +slank +slaps +slart +slats +slaty +slaws +slays +slebs +sleds +sleer +slews +sleys +slier +slily +slims +slipe +slips +slipt +slish +slits +slive +sloan +slobs +sloes +slogs +sloid +slojd +slomo +sloom +sloot +slops +slopy +slorm +slots +slove +slows +sloyd +slubb +slubs +slued +slues +sluff +slugs +sluit +slums +slurb +slurs +sluse +sluts +slyer +slype +smaak +smaik +smalm +smalt +smarm +smaze +smeek +smees +smeik +smeke +smerk +smews +smirr +smirs +smits +smogs +smoko +smolt +smoor +smoot +smore +smorg +smout +smowt +smugs +smurs +smush +smuts +snabs +snafu +snags +snaps +snarf +snark +snars +snary +snash +snath +snaws +snead +sneap +snebs +sneck +sneds +sneed +snees +snell +snibs +snick +snies +snift +snigs +snips +snipy +snirt +snits +snobs +snods +snoek +snoep +snogs +snoke +snood +snook +snool +snoot +snots +snowk +snows +snubs +snugs +snush +snyes +soaks +soaps +soare +soars +soave +sobas +socas +soces +socko +socks +socle +sodas +soddy +sodic +sodom +sofar +sofas +softa +softs +softy +soger +sohur +soils +soily +sojas +sojus +sokah +soken +sokes +sokol +solah +solan +solas +solde +soldi +soldo +solds +soled +solei +soler +soles +solon +solos +solum +solus +soman +somas +sonce +sonde +sones +songs +sonly +sonne +sonny +sonse +sonsy +sooey +sooks +sooky +soole +sools +sooms +soops +soote +soots +sophs +sophy +sopor +soppy +sopra +soral +soras +sorbo +sorbs +sorda +sordo +sords +sored +soree +sorel +sorer +sores +sorex +sorgo +sorns +sorra +sorta +sorts +sorus +soths +sotol +souce +souct +sough +souks +souls +soums +soups +soupy +sours +souse +souts +sowar +sowce +sowed +sowff +sowfs +sowle +sowls +sowms +sownd +sowne +sowps +sowse +sowth +soyas +soyle +soyuz +sozin +spacy +spado +spaed +spaer +spaes +spags +spahi +spail +spain +spait +spake +spald +spale +spall +spalt +spams +spane +spang +spans +spard +spars +spart +spate +spats +spaul +spawl +spaws +spayd +spays +spaza +spazz +speal +spean +speat +specs +spect +speel +speer +speil +speir +speks +speld +spelk +speos +spets +speug +spews +spewy +spial +spica +spick +spics +spide +spier +spies +spiff +spifs +spiks +spile +spims +spina +spink +spins +spirt +spiry +spits +spitz +spivs +splay +splog +spode +spods +spoom +spoor +spoot +spork +sposh +spots +sprad +sprag +sprat +spred +sprew +sprit +sprod +sprog +sprue +sprug +spuds +spued +spuer +spues +spugs +spule +spume +spumy +spurs +sputa +spyal +spyre +squab +squaw +squeg +squid +squit +squiz +stabs +stade +stags +stagy +staig +stane +stang +staph +staps +starn +starr +stars +stats +staun +staws +stays +stean +stear +stedd +stede +steds +steek +steem +steen +steil +stela +stele +stell +steme +stems +stend +steno +stens +stent +steps +stept +stere +stets +stews +stewy +steys +stich +stied +sties +stilb +stile +stime +stims +stimy +stipa +stipe +stire +stirk +stirp +stirs +stive +stivy +stoae +stoai +stoas +stoat +stobs +stoep +stogy +stoit +stoln +stoma +stond +stong +stonk +stonn +stook +stoor +stope +stops +stopt +stoss +stots +stott +stoun +stoup +stour +stown +stowp +stows +strad +strae +strag +strak +strep +strew +stria +strig +strim +strop +strow +stroy +strum +stubs +stude +studs +stull +stulm +stumm +stums +stuns +stupa +stupe +sture +sturt +styed +styes +styli +stylo +styme +stymy +styre +styte +subah +subas +subby +suber +subha +succi +sucks +sucky +sucre +sudds +sudor +sudsy +suede +suent +suers +suete +suets +suety +sugan +sughs +sugos +suhur +suids +suint +suits +sujee +sukhs +sukuk +sulci +sulfa +sulfo +sulks +sulph +sulus +sumis +summa +sumos +sumph +sumps +sunis +sunks +sunna +sunns +sunup +supes +supra +surah +sural +suras +surat +surds +sured +sures +surfs +surfy +surgy +surra +sused +suses +susus +sutor +sutra +sutta +swabs +swack +swads +swage +swags +swail +swain +swale +swaly +swamy +swang +swank +swans +swaps +swapt +sward +sware +swarf +swart +swats +swayl +sways +sweal +swede +sweed +sweel +sweer +swees +sweir +swelt +swerf +sweys +swies +swigs +swile +swims +swink +swipe +swire +swiss +swith +swits +swive +swizz +swobs +swole +swoln +swops +swopt +swots +swoun +sybbe +sybil +syboe +sybow +sycee +syces +sycon +syens +syker +sykes +sylis +sylph +sylva +symar +synch +syncs +synds +syned +synes +synth +syped +sypes +syphs +syrah +syren +sysop +sythe +syver +taals +taata +taber +tabes +tabid +tabis +tabla +tabor +tabun +tabus +tacan +taces +tacet +tache +tacho +tachs +tacks +tacos +tacts +taels +tafia +taggy +tagma +tahas +tahrs +taiga +taigs +taiko +tails +tains +taira +taish +taits +tajes +takas +takes +takhi +takin +takis +takky +talak +talaq +talar +talas +talcs +talcy +talea +taler +tales +talks +talky +talls +talma +talpa +taluk +talus +tamal +tamed +tames +tamin +tamis +tammy +tamps +tanas +tanga +tangi +tangs +tanhs +tanka +tanks +tanky +tanna +tansy +tanti +tanto +tanty +tapas +taped +tapen +tapes +tapet +tapis +tappa +tapus +taras +tardo +tared +tares +targa +targe +tarns +taroc +tarok +taros +tarps +tarre +tarry +tarsi +tarts +tarty +tasar +tased +taser +tases +tasks +tassa +tasse +tasso +tatar +tater +tates +taths +tatie +tatou +tatts +tatus +taube +tauld +tauon +taupe +tauts +tavah +tavas +taver +tawai +tawas +tawed +tawer +tawie +tawse +tawts +taxed +taxer +taxes +taxis +taxol +taxon +taxor +taxus +tayra +tazza +tazze +teade +teads +teaed +teaks +teals +teams +tears +teats +teaze +techs +techy +tecta +teels +teems +teend +teene +teens +teeny +teers +teffs +teggs +tegua +tegus +tehrs +teiid +teils +teind +teins +telae +telco +teles +telex +telia +telic +tells +telly +teloi +telos +temed +temes +tempi +temps +tempt +temse +tench +tends +tendu +tenes +tenge +tenia +tenne +tenno +tenny +tenon +tents +tenty +tenue +tepal +tepas +tepoy +terai +teras +terce +terek +teres +terfe +terfs +terga +terms +terne +terns +terry +terts +tesla +testa +teste +tests +tetes +teths +tetra +tetri +teuch +teugh +tewed +tewel +tewit +texas +texes +texts +thack +thagi +thaim +thale +thali +thana +thane +thang +thans +thanx +tharm +thars +thaws +thawy +thebe +theca +theed +theek +thees +thegn +theic +thein +thelf +thema +thens +theow +therm +thesp +thete +thews +thewy +thigs +thilk +thill +thine +thins +thiol +thirl +thoft +thole +tholi +thoro +thorp +thous +thowl +thrae +thraw +thrid +thrip +throe +thuds +thugs +thuja +thunk +thurl +thuya +thymi +thymy +tians +tiars +tical +ticca +ticed +tices +tichy +ticks +ticky +tiddy +tided +tides +tiers +tiffs +tifos +tifts +tiges +tigon +tikas +tikes +tikis +tikka +tilak +tiled +tiler +tiles +tills +tilly +tilth +tilts +timbo +timed +times +timon +timps +tinas +tinct +tinds +tinea +tined +tines +tinge +tings +tinks +tinny +tints +tinty +tipis +tippy +tired +tires +tirls +tiros +tirrs +titch +titer +titis +titre +titty +titup +tiyin +tiyns +tizes +tizzy +toads +toady +toaze +tocks +tocky +tocos +todde +toeas +toffs +toffy +tofts +tofus +togae +togas +toged +toges +togue +tohos +toile +toils +toing +toise +toits +tokay +toked +toker +tokes +tokos +tolan +tolar +tolas +toled +toles +tolls +tolly +tolts +tolus +tolyl +toman +tombs +tomes +tomia +tommy +tomos +tondi +tondo +toned +toner +tones +toney +tongs +tonka +tonks +tonne +tonus +tools +tooms +toons +toots +toped +topee +topek +toper +topes +tophe +tophi +tophs +topis +topoi +topos +toppy +toque +torah +toran +toras +torcs +tores +toric +torii +toros +torot +torrs +torse +torsi +torsk +torta +torte +torts +tosas +tosed +toses +toshy +tossy +toted +toter +totes +totty +touks +touns +tours +touse +tousy +touts +touze +touzy +towed +towie +towns +towny +towse +towsy +towts +towze +towzy +toyed +toyer +toyon +toyos +tozed +tozes +tozie +trabs +trads +tragi +traik +trams +trank +tranq +trans +trant +trape +traps +trapt +trass +trats +tratt +trave +trayf +trays +treck +treed +treen +trees +trefa +treif +treks +trema +trems +tress +trest +trets +trews +treyf +treys +triac +tride +trier +tries +triff +trigo +trigs +trike +trild +trill +trims +trine +trins +triol +trior +trios +trips +tripy +trist +troad +troak +troat +trock +trode +trods +trogs +trois +troke +tromp +trona +tronc +trone +tronk +trons +trooz +troth +trots +trows +troys +trued +trues +trugo +trugs +trull +tryer +tryke +tryma +tryps +tsade +tsadi +tsars +tsked +tsuba +tsubo +tuans +tuart +tuath +tubae +tubar +tubas +tubby +tubed +tubes +tucks +tufas +tuffe +tuffs +tufts +tufty +tugra +tuile +tuina +tuism +tuktu +tules +tulpa +tulsi +tumid +tummy +tumps +tumpy +tunas +tunds +tuned +tuner +tunes +tungs +tunny +tupek +tupik +tuple +tuque +turds +turfs +turfy +turks +turme +turms +turns +turnt +turps +turrs +tushy +tusks +tusky +tutee +tutti +tutty +tutus +tuxes +tuyer +twaes +twain +twals +twank +twats +tways +tweel +tween +tweep +tweer +twerk +twerp +twier +twigs +twill +twilt +twink +twins +twiny +twire +twirp +twite +twits +twoer +twyer +tyees +tyers +tyiyn +tykes +tyler +tymps +tynde +tyned +tynes +typal +typed +types +typey +typic +typos +typps +typto +tyran +tyred +tyres +tyros +tythe +tzars +udals +udons +ugali +ugged +uhlan +uhuru +ukase +ulama +ulans +ulema +ulmin +ulnad +ulnae +ulnar +ulnas +ulpan +ulvas +ulyie +ulzie +umami +umbel +umber +umble +umbos +umbre +umiac +umiak +umiaq +ummah +ummas +ummed +umped +umphs +umpie +umpty +umrah +umras +unais +unapt +unarm +unary +unaus +unbag +unban +unbar +unbed +unbid +unbox +uncap +unces +uncia +uncos +uncoy +uncus +undam +undee +undos +undug +uneth +unfix +ungag +unget +ungod +ungot +ungum +unhat +unhip +unica +units +unjam +unked +unket +unkid +unlaw +unlay +unled +unlet +unlid +unman +unmew +unmix +unpay +unpeg +unpen +unpin +unred +unrid +unrig +unrip +unsaw +unsay +unsee +unsew +unsex +unsod +untax +untin +unwet +unwit +unwon +upbow +upbye +updos +updry +upend +upjet +uplay +upled +uplit +upped +upran +uprun +upsee +upsey +uptak +upter +uptie +uraei +urali +uraos +urare +urari +urase +urate +urbex +urbia +urdee +ureal +ureas +uredo +ureic +urena +urent +urged +urger +urges +urial +urite +urman +urnal +urned +urped +ursae +ursid +urson +urubu +urvas +users +usnea +usque +usure +usury +uteri +uveal +uveas +uvula +vacua +vaded +vades +vagal +vagus +vails +vaire +vairs +vairy +vakas +vakil +vales +valis +valse +vamps +vampy +vanda +vaned +vanes +vangs +vants +vaped +vaper +vapes +varan +varas +vardy +varec +vares +varia +varix +varna +varus +varve +vasal +vases +vasts +vasty +vatic +vatus +vauch +vaute +vauts +vawte +vaxes +veale +veals +vealy +veena +veeps +veers +veery +vegas +veges +vegie +vegos +vehme +veils +veily +veins +veiny +velar +velds +veldt +veles +vells +velum +venae +venal +vends +vendu +veney +venge +venin +vents +venus +verbs +verra +verry +verst +verts +vertu +vespa +vesta +vests +vetch +vexed +vexer +vexes +vexil +vezir +vials +viand +vibes +vibex +vibey +viced +vices +vichy +viers +views +viewy +vifda +viffs +vigas +vigia +vilde +viler +villi +vills +vimen +vinal +vinas +vinca +vined +viner +vines +vinew +vinic +vinos +vints +viold +viols +vired +vireo +vires +virga +virge +virid +virls +virtu +visas +vised +vises +visie +visne +vison +visto +vitae +vitas +vitex +vitro +vitta +vivas +vivat +vivda +viver +vives +vizir +vizor +vleis +vlies +vlogs +voars +vocab +voces +voddy +vodou +vodun +voema +vogie +voids +voile +voips +volae +volar +voled +voles +volet +volks +volta +volte +volti +volts +volva +volve +vomer +voted +votes +vouge +voulu +vowed +vower +voxel +vozhd +vraic +vrils +vroom +vrous +vrouw +vrows +vuggs +vuggy +vughs +vughy +vulgo +vulns +vulva +vutty +waacs +wacke +wacko +wacks +wadds +waddy +waded +wader +wades +wadge +wadis +wadts +waffs +wafts +waged +wages +wagga +wagyu +wahoo +waide +waifs +waift +wails +wains +wairs +waite +waits +wakas +waked +waken +waker +wakes +wakfs +waldo +walds +waled +waler +wales +walie +walis +walks +walla +walls +wally +walty +wamed +wames +wamus +wands +waned +wanes +waney +wangs +wanks +wanky +wanle +wanly +wanna +wants +wanty +wanze +waqfs +warbs +warby +wards +wared +wares +warez +warks +warms +warns +warps +warre +warst +warts +wases +washy +wasms +wasps +waspy +wasts +watap +watts +wauff +waugh +wauks +waulk +wauls +waurs +waved +waves +wavey +wawas +wawes +wawls +waxed +waxer +waxes +wayed +wazir +wazoo +weald +weals +weamb +weans +wears +webby +weber +wecht +wedel +wedgy +weeds +weeke +weeks +weels +weems +weens +weeny +weeps +weepy +weest +weete +weets +wefte +wefts +weids +weils +weirs +weise +weize +wekas +welds +welke +welks +welkt +wells +welly +welts +wembs +wends +wenge +wenny +wents +weros +wersh +wests +wetas +wetly +wexed +wexes +whamo +whams +whang +whaps +whare +whata +whats +whaup +whaur +wheal +whear +wheen +wheep +wheft +whelk +whelm +whens +whets +whews +wheys +whids +whift +whigs +whilk +whims +whins +whios +whips +whipt +whirr +whirs +whish +whiss +whist +whits +whity +whizz +whomp +whoof +whoot +whops +whore +whorl +whort +whoso +whows +whump +whups +whyda +wicca +wicks +wicky +widdy +wides +wiels +wifed +wifes +wifey +wifie +wifty +wigan +wigga +wiggy +wikis +wilco +wilds +wiled +wiles +wilga +wilis +wilja +wills +wilts +wimps +winds +wined +wines +winey +winge +wings +wingy +winks +winna +winns +winos +winze +wiped +wiper +wipes +wired +wirer +wires +wirra +wised +wises +wisha +wisht +wisps +wists +witan +wited +wites +withe +withs +withy +wived +wiver +wives +wizen +wizes +woads +woald +wocks +wodge +woful +wojus +woker +wokka +wolds +wolfs +wolly +wolve +wombs +womby +womyn +wonga +wongi +wonks +wonky +wonts +woods +wooed +woofs +woofy +woold +wools +woons +woops +woopy +woose +woosh +wootz +words +works +worms +wormy +worts +wowed +wowee +woxen +wrang +wraps +wrapt +wrast +wrate +wrawl +wrens +wrick +wried +wrier +wries +writs +wroke +wroot +wroth +wryer +wuddy +wudus +wulls +wurst +wuses +wushu +wussy +wuxia +wyled +wyles +wynds +wynns +wyted +wytes +xebec +xenia +xenic +xenon +xeric +xerox +xerus +xoana +xrays +xylan +xylem +xylic +xylol +xylyl +xysti +xysts +yaars +yabas +yabba +yabby +yacca +yacka +yacks +yaffs +yager +yages +yagis +yahoo +yaird +yakka +yakow +yales +yamen +yampy +yamun +yangs +yanks +yapok +yapon +yapps +yappy +yarak +yarco +yards +yarer +yarfa +yarks +yarns +yarrs +yarta +yarto +yates +yauds +yauld +yaups +yawed +yawey +yawls +yawns +yawny +yawps +ybore +yclad +ycled +ycond +ydrad +ydred +yeads +yeahs +yealm +yeans +yeard +years +yecch +yechs +yechy +yedes +yeeds +yeesh +yeggs +yelks +yells +yelms +yelps +yelts +yenta +yente +yerba +yerds +yerks +yeses +yesks +yests +yesty +yetis +yetts +yeuks +yeuky +yeven +yeves +yewen +yexed +yexes +yfere +yiked +yikes +yills +yince +yipes +yippy +yirds +yirks +yirrs +yirth +yites +yitie +ylems +ylike +ylkes +ymolt +ympes +yobbo +yobby +yocks +yodel +yodhs +yodle +yogas +yogee +yoghs +yogic +yogin +yogis +yoick +yojan +yoked +yokel +yoker +yokes +yokul +yolks +yolky +yomim +yomps +yonic +yonis +yonks +yoofs +yoops +yores +yorks +yorps +youks +yourn +yours +yourt +youse +yowed +yowes +yowie +yowls +yowza +yrapt +yrent +yrivd +yrneh +ysame +ytost +yuans +yucas +yucca +yucch +yucko +yucks +yucky +yufts +yugas +yuked +yukes +yukky +yukos +yulan +yules +yummo +yummy +yumps +yupon +yuppy +yurta +yurts +yuzus +zabra +zacks +zaida +zaidy +zaire +zakat +zaman +zambo +zamia +zanja +zante +zanza +zanze +zappy +zarfs +zaris +zatis +zaxes +zayin +zazen +zeals +zebec +zebub +zebus +zedas +zeins +zendo +zerda +zerks +zeros +zests +zetas +zexes +zezes +zhomo +zibet +ziffs +zigan +zilas +zilch +zilla +zills +zimbi +zimbs +zinco +zincs +zincy +zineb +zines +zings +zingy +zinke +zinky +zippo +zippy +ziram +zitis +zizel +zizit +zlote +zloty +zoaea +zobos +zobus +zocco +zoeae +zoeal +zoeas +zoism +zoist +zombi +zonae +zonda +zoned +zoner +zones +zonks +zooea +zooey +zooid +zooks +zooms +zoons +zooty +zoppa +zoppo +zoril +zoris +zorro +zouks +zowee +zowie +zulus +zupan +zupas +zuppa +zurfs +zuzim +zygal +zygon +zymes +zymic \ No newline at end of file diff --git a/IoLanguage.md b/IoLanguage.md deleted file mode 100644 index 8e162a8..0000000 --- a/IoLanguage.md +++ /dev/null @@ -1,573 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Io](#io) - - [环境配置](#%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [介绍](#%E4%BB%8B%E7%BB%8D) - - [基本要素](#%E5%9F%BA%E6%9C%AC%E8%A6%81%E7%B4%A0) - - [基于原型](#%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B) - - [运行环境](#%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83) - - [面向对象](#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - - [消息](#%E6%B6%88%E6%81%AF) - - [控制流](#%E6%8E%A7%E5%88%B6%E6%B5%81) - - [DSL](#dsl) - - [forward](#forward) - - [并发](#%E5%B9%B6%E5%8F%91) - - [总结](#%E6%80%BB%E7%BB%93) - - - -# Io - -书籍[七周七语言:理解多种编程范型](https://book.douban.com/subject/10555435/)的第二门编程语言。 - -2002年,Steve Dekorte发明了Io语言,看起来这语言非常年轻,并且非常小众,小众到浏览器搜索除了官网和百科只有极少其他的文章。 - -Io语言机器精简与小巧,一刻钟学会语法,半小时学会基本原理。只是需要多花些心思在Io的库上,因为这门语言的复杂性和丰富性,统统来自于库的设计。 - -如今的大多数Io社区,都致力于把Io作为带有微型虚拟机和丰富并发特性的可嵌入语言来推广。Io的核心优势是拥有大量可定制语法和函数,以及强有力的并发模型。它的简单语法和原型编程模型都值得我们重点关注。 - -资料阅读: -- [官网的英文入门Guide](https://iolanguage.org/guide/guide.html) -- [找到一个入门Guide的中文翻译,并且作者配套还写了教程](https://github.com/sluke/learn-io-language-the-hard-way/blob/master/io-guide-cn.md),练习和翻译的教程看完就可以入门io语言了,7周7语言这本书中介绍得还是有点太简单了。 -- [源码仓库](https://github.com/IoLanguage/io),最近有一定活跃。 - -提前抓一下重点: -- 基于原型的面向对象设计。 -- 万物皆对象,甚至上下文、方法、命名空间、代码块都是可以动态赋值并进行传递的对象。 -- 代码由表达式构成,所有表达式都蕴含了一个动态的信息发送,槽和消息也是关注重点。 -- 库、基本数据结构。 -- 并发模型:actors和coroutine。 -- 嵌入和扩展到其他语言中,作为可选内容。 -- 理解清楚这几点才算是入门了Io。 -- 这里只阐述核心思想,不详细介绍语法细节。 - -观前提示: -- 这是一门纯粹面向对象的语言,任何东西都是对象。 -- 这是一门很能套娃的语言。 -- 这是很自由的语言。 - -## 环境配置 - -搜索Io语言或者Io Language找到官网,下载Windows二进制和适合的Linux包。包装管理没有这个包,需要手动安装。 - -Windows: -- 执行exe将文件提取到要安装的目录。 -- 添加`bin\`目录到path环境变量。 - -Linux: -- Ubuntu为例,下载deb包。 -```shell -wget https://iobin.suspended-chord.info/linux/iobin-linux-x64-deb-current.zip -unzip iobin-linux-x64-deb-current.zip -sudo dpkg -i ./IoLanguage-2013.11.04-Linux-x64.deb -``` -按照README中说的需要先安装依赖(有点老了,有的已经废弃过时了),我这里估计都有不需要也能安装成功: -```shell -sudo apt-get install build-essential cmake libreadline-dev libssl-dev ncurses-dev libffi-dev zlib1g-dev libpcre3-dev libpng-dev libtiff4-dev libjpeg62-dev python-dev libpng-dev libtiff4-dev libjpeg62-dev libmysqlclient-dev libmemcached-dev libtokyocabinet-dev libsqlite3-dev libdbi0-dev libpq-dev libgmp3-dev libogg-dev libvorbis-dev libtaglib-cil-dev libtag1-dev libtheora-dev libsamplerate0-dev libloudmouth1-dev libsndfile1-dev libflac-dev libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev libxmu-dev libxi-dev libxml2-dev libyajl-dev uuid-dev liblzo2-dev zlib1g-dev libevent-de -``` - -包的更新时间是2013年,如果要最新版本估计还得去找源码编译,这里就免了。 - -VSCode搭建执行环境: -- 无论Windows,WSL,Linux都可以用。 -- 安装插件,还是有3款插件的,能高亮,好像都没有补全,只能凑合着用了。比如名为io的插件就行。 -- 运行的话使用Code-Runner插件为`.io`后缀配置执行命令,`".io": "io $fileName"`就可以执行了。或者直接在命令行`io file.io`执行。 -- 调试暂时忽略。 - -进入交互式执行环境: -```shell -io -``` - -执行文件: -```shell -io hello.io -``` - -HelloWorld: -```io -"hello,world!" println -``` - -作为脚本执行: -```io -#!/usr/bin/env io -``` - -在文件或者交互环境中执行源文件,自己执行自己会死循环: -```io -doFile("file.io") -``` - -执行字符串: -```io -doString("xxx") -``` - -## 介绍 - -官网的介绍: -``` -Io is a programming language focused on expressiveness through simplicity. - -inspirations -Self, Lua, Smalltalk, NewtonScript, Act1, LISP - -pure -minimal syntax -all values are objects -prototype-based object model -everything is a message, even assignment -no keywords, no globals - -dynamic -all messages are dynamic -code is a runtime modifiable tree of messages -arguments passed by expression, receiver controls eval -differential inheritance -become, transparent proxies, weak links - -concurrent -coroutine based actors -transparent futures -automatic lock detection - -implementation -small -open source -embeddable -multi-state -incremental collector -``` - -需要关注的点:动态类型、纯粹面向对象、基于原型、消息机制、并发模型。 - -语言文法: -```BNF -messages -expression ::= { message | sctpad } -message ::= [wcpad] symbol [scpad] [arguments] -arguments ::= Open [argument [ { Comma argument } ]] Close -argument ::= [wcpad] expression [wcpad] - -symbols -symbol ::= Identifier | number | Operator | quote -Identifier ::= { letter | digit | "_" } -Operator ::= { ":" | "." | "'" | "~" | "!" | "@" | "$" | -"%" | "^" | "&" | "*" | "-" | "+" | "/" | "=" | "{" | "}" | -"[" | "]" | "|" | "\" | "<" | ">" | "?" } - -quotes -quote ::= MonoQuote | TriQuote -MonoQuote ::= """ [ "\"" | not(""")] """ -TriQuote ::= """"" [ not(""""")] """"" - -spans -Terminator ::= { [separator] ";" | "\n" | "\r" [separator] } -separator ::= { " " | "\f" | "\t" | "\v" } -whitespace ::= { " " | "\f" | "\r" | "\t" | "\v" | "\n" } -sctpad ::= { separator | Comment | Terminator } -scpad ::= { separator | Comment } -wcpad ::= { whitespace | Comment } - -comments -Comment ::= slashStarComment | slashSlashComment | poundComment -slashStarComment ::= "/*" [not("*/")] "*/" -slashSlashComment ::= "//" [not("\n")] "\n" -poundComment ::= "#" [not("\n")] "\n" - -numbers -number ::= HexNumber | Decimal -HexNumber ::= "0" anyCase("x") { [ digit | hexLetter ] } -hexLetter ::= "a" | "b" | "c" | "d" | "e" | "f" -Decimal ::= digits | "." digits | -digits "." digits ["e" [-] digits] - -characters -Comma ::= "," -Open ::= "(" | "[" | "{" -Close ::= ")" | "]" | "}" -letter ::= "a" ... "z" | "A" ... "Z" -digit ::= "0" ... "9" -digits ::= { digit } -The uppercase words above designate elements the lexer treats as tokens. -``` - -## 基本要素 - -注释: -```io -# comment -// single line comment -/* -multi line comment -*/ -``` -运算符表与优先级: -``` - 0 ? @ @@ - 1 ** - 2 % * / - 3 + - - 4 << >> - 5 < <= > >= - 6 != == - 7 & - 8 ^ - 9 | - 10 && and - 11 or || - 12 .. - 13 %= &= *= += -= /= <<= >>= ^= |= - 14 return -``` -赋值运算符: -``` - ::= newSlot Creates slot, creates setter, assigns value - := setSlot Creates slot, assigns value - = updateSlot Assigns value to slot if it exists, otherwise raises exception -``` -和C语言中没有太大区别,多了`**`支持浮点数的求幂运算,`%`支持浮点数,`/`统一执行浮点除法,字符串字面值使用`""`运算符,浮点数采用IEEE754 64位浮点数,浮点数字面值同C不支持任何类型后缀,`..`为字符串连接运算符,`&& ||`同样短路求值,运算符基本上和C语言语义、优先级都一致。C中没有的`? @ @@`后续详述。 - -赋值运算符自成一组。 - -变量:Io是动态语言,不需要先声明后使用,但也不能直接赋值,如果没有定义过第一次需要使用`::=`或者`:=`进行创建,前者还会创建一个setter。已经定义的话`:= =`等价,`::=`创建一个setter。Io将变量称作**槽(slot)**。 - -赋值运算符编译后:可以直接等价替换。 -``` -source compiles to -a ::= 1 newSlot("a", 1) -a := 1 setSlot("a", 1) -a = 1 updateSlot("a", 1) -``` - -字符串:转义和C语言差不多,具体细节暂时不多研究,`\t \r \n \\ \"`等都是支持的,ASCII值转义好像不支持,使用一对`"""`包起来表示原始字符串不进行转义。拼接两个字符串用`..`,字面值也需要,字面值在Io中一样是对象。 - -除去这些东西外,Io语言并没有关键字,所有的内容都定义在库中,包括控制结构、循环一个原因的基本要素在内的所有东西。 - -`true false nil`:分别表示逻辑真、逻辑假、对象为空,同样也都是对象而不是关键字,这三个对象是单例的对象,也就是调用克隆还是返回自己而不是克隆一个新的对象,后面会说怎么实现。和Ruby一样,0用在条件中时是true,而不是false。 - -## 基于原型 - -Io的面向对象系统是基于原型的,也就是说Io中并没有类这个概念。维基百科中的定义: ->基于原型编程(英语:prototype-based programming)或称为原型程序设计、原型编程,是面向对象编程的一种风格和方式。在原型编程中,行为重用(在基于类的语言通常称为继承)是通过复制已经存在的原型对象的过程实现的。这个模型一般被认为是无类的、面向原型、或者是基于实例的编程。 - -基于原型的语言最初的例子是Self,后续有很多语言采取这种方式:JavaScript、Cecil、NewtonScript、Io、REBOL等。其中使用最广泛的是JavaScript。 - -Io中的原型: -- 最初的原型:`Object`,是一个根对象。 -- 创建新的对象的方式:克隆`clone`,从另一个对象(原型对象或普通对象)。 -- 获取到一个对象的原型:`proto`。 -- 约定:大写字母开头的是新的原型对象,而小写字母开头的不是,只是一个普通的对象。这是一个惯例,但同时也作为了一个语法。 -- Io中叫对象的成员(也就是面向对象语言中说的方法或者字段,方法和字段还是有区分的)称为**槽(slot)**。 -- 对象就是**槽的容器**。 -- 原型和普通的对象的区别仅仅在于:原型多一个自己的`type`槽(可以理解为字符串类型的一个字段),用来表示类型。而普通对象则不会有这个`type`槽,所以调用时会调用它的原型的`type`槽,得到和它的原型调用`type`相同的结果。除此之外,无其他区别。它只是帮助程序员更好地组织的代码的工具,也可以理解为原型对象时一个实例的同时也是一个类型,普通对象则仅仅是实例。 -- 前面所说的`::=`会多生成一个setter,这个setter就是一个槽(方法),比如如果`age`字段就会生成一个`setAge`方法。 -- 重复一遍,在这里**没有类的概念**,可以往任何对象上添加任何槽,修改原型上、原型的原型上或者自己已经定义的槽。获取槽时对象上找不到槽会到它的原型上去找(就像子类和基类一样)。 -- 也就是说槽在某种程度是有数据依赖的,如果你不在克隆的对象上修改继承自原型的槽,那么修改了原型的槽后在克隆对象上获取该槽会获取到原型的数据。 -- 所以应该从原型对象克隆(作为类型使用),并且不修改已知的原型对象,而不应该从普通对象克隆,这样数据会依赖,除非你的目的就是要依赖,否则不太建议这样做。如果在克隆对象中赋值覆盖掉了那些槽,那么和从原型克隆最终行为上来说是一样的。 -- 使用`print println`的打印一个对象时只会打印在这个对象自己覆盖了原型中或者新添加的槽,`slotNames`槽获取到的列表同理。 -- 可以预见地对象最终会组织成一棵树数的形式,根节点是`Object`,更多时候应该是一个沿着叶子节点向上遍历的过程。 -- 注意一个对象的原型(指克隆的源对象)不一定要是原型对象(指大写字母开头,`type`得到自己名称的对象),也可以是一个普通对象。有点小抽象,这是什么阅读理解啊! -```io -Io> Person := Object clone # new proto object -==> Person_0x2c6c700: - type = "Person" - -Io> Person type -==> Person -Io> p := Person clone # just object -==> Person_0x2bb6a20: - -Io> p type -==> Person -Io> p type type # type return a string(Sequecne) -==> Sequence -Io> p proto type -==> Person -Io> p proto proto type -==> Object -Io> Object type -==> Object -Io> p proto == Person -==> true -Io> Person proto == Object -==> true -``` - -往对象上动态添加槽: -```io -Person := Object clone -Person name ::= nil # also create setter -Person age := nil -Person address := nil -Person println - -mary := Person clone -mary hash := 0 # new slot -mary setName("mary") # use setter -mary age = 13 -mary println - -Student := Person clone do( - class := nil - grade := nil -) - -lily := Student clone do( - name = "lily" - age ::= 13 # create setter - setAge(14) # use setter - class = "6-1" - grade = 4.0 - hobby := "chess" # new slot - getDescription := method( # new method slot - name .. " " .. age .. " " .. address .. " " .. class # return this string - ) -) - -lily println -lily getDescription println -``` -从这个例子就可以看出每个对象都可以不一样,太自由了,原型对象和普通对象用起来没有任何区别。 - -## 运行环境 - -当前的运行环境:交互环境或文件的执行环境是一个命名空间`namespace`,名为`Lobby`也是一个对象,`type`是`Object`,看来这是一个特例(大写字母开头但是自己并不是原型?)。 - -常见类型: -```io -Io> 1 type -==> Number -Io> 1.0 type -==> Number -Io> "hello" type -==> Sequence -Io> true type -==> true -Io> nil type -==> nil -Io> Lobby type -==> Object -Io> Lobby -==> Object_0x7e1cf0: - Lobby = Object_0x7e1cf0 - Protos = Object_0x7e1ba0 - _ = Object_0x7e1cf0 - exit = method(...) - forward = method(...) - set_ = method(...) - -Io> Protos == Lobby proto -==> true -Io> Protos proto == Object -==> false -Io> Protos proto proto == Object -==> true -Io> Protos -==> Object_0x7e1ba0: - Addons = Object_0x7e1a20 - Core = Object_0x7e1960 - -Io> Protos proto == Protos Core -==> true -Io> Protos Core proto == Object -==> true -``` - -- `Lobby`是当前命名空间,同时它也是一个对象,在当前命名空间定义的对象都是它的槽。在命名空间内打印`println`就是直接打印`Lobby`对象。 -- 所以可以通过`Lobby`引用已经定义在Lobby内的槽,当然`Lobby`是可以被省略的,也就是我们通常的引用方式。 -- `Lobby`内已经预先定义了一些符号: - - `Lobby`就是自己。 - - `_`值也是自己,而且包含它的setter。 - - `exit`是退出方法。交互环境下执行会退出交互环境,文件执行环境好像什么都不会发生。 - - `Protos`类型是`Object`,其中包含两个槽,具体是什么作用暂时不清楚。这个对象也是`Lobby`的原型(和`Lobby`一样,大写开头,但是不做为原型)。上面还有一层原型`Protos Core`,然后原型的原型就是`Object`。 - ``` - Lobby ---> Lobby Protos ---> Lobby Protos Core ---> Object - ``` - - `forward`方法不知道是做什么的,调用不了。 -- 可以看到中间有一些对象都是大写开头的,都不是新的原型,而是`Object`。 -- 再次注意一个对象的原型并不一定要是原型对象,也可以是普通对象。从谁克隆,谁就是它的原型。 -- 你甚至可以改变`Lobby`的值,但是这个命名空间对象还是存在的,只是`Lobby`不再指向它了而已。新创建的槽也还是在这个命名空间内。 -- 你可以改变几乎任意对象的任意一个槽,自由度很高,但太过放纵未必是件好事。 - -## 面向对象 - -添加槽前面已经说过了,用`:= ::=`。 - -其中方法使用`Object method`方法(`method`本身是方法吗?)创建(套娃了属于是),得到一个匿名函数,因为在`Lobby`中,而`Lobby`又是一个`Object`,所以`Object`可以省略。 -```io -method((2+2) println) -``` -确实无时无刻任何东西都是对象的感觉了,剩下唯一的疑问就是方法是什么,方法放在槽里面这毋庸置疑,但方法的类型是什么呢?(不能通过type获得,因为会执行方法,从而得到返回值的类型) - -方法定义的形式: -```io -method(, , ..., ) -``` - -例: -```io -fibonacci := method(n, do( - return if (n <= 0, 0, if (n == 1, 1, fibonacci(n-1) + fibonacci(n-2))) -)) - -fibonacci2 := method(n, do( - if(n <= 0) then(return 0 - ) elseif(n == 1) then(return 1) - return fibonacci2(n-1) + fibonacci2(n-2) -)) - -fibonacci3 := method(n, do( - if (n <= 0) then (return 0) elseif (n == 1) then(return 1) - a := 0 - b := 1 - n repeat ( - tmp := a - a = b - b = tmp + a - ) - return a -)) -``` - -单例的`true false nil`:通过修改`clone`字段,将其改为自己就可以实现。 -```io -Person := Object clone -Person clone := Person -``` -原始的`clone`槽定义在`Object`上:应该是调用一个本地的C函数来克隆并返回新对象,`Object`中很多方法都是这样的。既可以理解为方法(克隆并返回一个新对象)也可以理解为字段(每次获取都得到一个克隆后的新对象),都统一称为槽,调用方式本就没有区别。 -```io -Io> Object getSlot("clone") -==> Object_clone() -Io> Object getSlot("clone") type -==> CFunction -``` - -## 消息 - -对象有槽,槽中存放了对象(包括方法对象)。Io中所有与对象的交互都是**消息**,通过向对象传递消息会返回槽的值或者调用槽中的方法,传递的消息就是槽的名称。 - -前面一直没有提过,其实就是面向对象中的获取字段和调用方法,这里换了个说法并统一起来了而已。但在机制上来说这是Io的核心机制之一。 - -一个消息由三个部分组成: -- 发送者(`sender`) -- 目标(`target`) -- 参数(`arguments`) - -特点: -- 消息由发送者发送至目标,然后由目标执行消息(获取字段或执行方法)。 -- 可以用`call`方法访问任何消息的元消息,称之为消息反射。 - - `call sender` 发送者 - - `call target` 发送目标 - - `call message arguments` 发送消息的参数列表 - - `call message argAt(n)` 消息参数 - - `call message name` 消息名称 -- 大部分语言将参数作为栈上的值传递,都会先计算每个参数的值,然后再传递给函数。Io不是这样,**Io传递的是消息本身和上下文,由接收者对消息求值**。 - - -任何程序结构都通过消息实现,包括创建对象、控制流、函数调用、定义新类型。 - -通过元消息实现一个`unless`方法,仅仅是`if(, , )`结构的反义: -```io -unless := method( - (call sender doMessage(call message argAt(0)) ifFalse( - call sender doMessage(call message argAt(1))) ifTrue( - call sender doMessage(call message argAt(2)))) -) - -unless(1 == 2, write("hello"), write("world")) -``` -这里是由发送目标`sender`调用`doMessage`来执行消息的参数,也就是由调用方来求值。注意`ifFalse`和`ifTrue`前的空格,写在一行是必须的。 - -## 控制流 - -```io -# if -if(, , ) -if(condition) then (operations) elseif(condition) then (operations) else (operations) - -# for -for(counter, initial_val, finalize_val, optional increment, body, extra argument); message with sender -list() foreach(count, value, ) # list method - -# while -while(condition, body); message with sender -``` - -## DSL - -通过添加运算符将特定的文件格式解析为Io的新增语法可以方便地实现DSL。 - -比如要将这个文件解析为电话本: -```io -// book.txt -// parsing target: -{ - "peter" : "2134213412", - "yes" : "1234" -} -``` -实现:确实方便,但也是真的晦涩。 -```io -/* -parsing target: book.txt -{ - "peter" : "2134213412", - "yes" : "1234" -} -*/ - -OperatorTable addAssignOperator(":", "atPutNumber") # make "key" : value to be atPutNumber("key", value) -curlyBrackets := method( # override curlyBrackets - r := Map clone - call message arguments foreach(arg, - r doMessage(arg) - ) - r # return map -) - -Map atPutNumber := method( - self atPut( - call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""), // key - call evalArgAt(1) // value - ) -) - -s := File with("book.txt") openForReading contents -phoneNumbers := doString(s) -# or just phoneNumbers := doFile("book.txt") -phoneNumbers keys println -phoneNumbers values println -``` -直接就将这个文件变成了语法的一部分,用Io的语法来解析,没有一定熟悉程度谁能写出这种代码啊! - -## forward - -重写`forward`方法使用消息反射可以实现Ruby中的`method_missing`类似的功能。 - -## 并发 - -协程是并发的基础。它提供了进程的自动挂起和恢复执行的机制。你可以把协程想象为带有多个入口和出口的函数。每次yield都会自动挂起当前进程,并把控制权转到另一进程当中。 - -通过在消息前加上@或@@,你可以异步触发消息,前者将返回future,后者会返回nil,并在其自身线程中触发消息。 - -Io就到这儿把,不详述了。 - - -## 总结 - -- 同样是动态类型、鸭子类型的。 -- 基于原型很有趣,万物皆对象并且可以动态增改对象的槽很自由。 -- 语法有点让人摸不着头脑,只能说勉强能用。具体体现在返回值,各种操作的优先级,括号应该怎么加,什么时候加。 -- 消息这个点有点无法评价,搞不懂为什么要这样设计。 -- 健壮的并发模型,暂未理解。 -- 语法糖太太太少了,做事情很费劲。 -- 任何结构都是消息(其实也就是函数调用),很多代码依赖返回值,而又是动态类型,搞得读代码和查错很头疼,需要精心设计程序才能work,心智负担感觉也太严重了。 -- 也许去学一下JavaScript是一个不错的选择。 -- 仅看书是完全不够的,7周7语言中Io篇幅不长,我并不认为它讲得很清楚。 -- 以后有空再来详细学习一下。 - - diff --git a/Java.md b/Java.md deleted file mode 100644 index 8f59220..0000000 --- a/Java.md +++ /dev/null @@ -1,5178 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [入门一下Java](#%E5%85%A5%E9%97%A8%E4%B8%80%E4%B8%8Bjava) - - [0. 准备](#0-%E5%87%86%E5%A4%87) - - [0.1 关于Java](#01-%E5%85%B3%E4%BA%8Ejava) - - [0.2 开发环境](#02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83) - - [0.3 基本Eclipse使用](#03-%E5%9F%BA%E6%9C%ACeclipse%E4%BD%BF%E7%94%A8) - - [0.4 基本IntelliJ IDEA使用](#04-%E5%9F%BA%E6%9C%ACintellij-idea%E4%BD%BF%E7%94%A8) - - [1. Java语言基础](#1-java%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80) - - [1.1 hello,world!](#11-helloworld) - - [1.2 基本约定](#12-%E5%9F%BA%E6%9C%AC%E7%BA%A6%E5%AE%9A) - - [1.3 基本数据类型](#13-%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) - - [1.4 基本运算](#14-%E5%9F%BA%E6%9C%AC%E8%BF%90%E7%AE%97) - - [1.5 字符和字符串](#15-%E5%AD%97%E7%AC%A6%E5%92%8C%E5%AD%97%E7%AC%A6%E4%B8%B2) - - [1.6 数组](#16-%E6%95%B0%E7%BB%84) - - [1.7 输入输出](#17-%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA) - - [1.8 流程控制——条件](#18-%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E6%9D%A1%E4%BB%B6) - - [1.9 流程控制——循环](#19-%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E5%BE%AA%E7%8E%AF) - - [1.10 命令行参数](#110-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0) - - [2. java面向对象](#2-java%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - - [2.1 类和对象](#21-%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1) - - [2.2 方法](#22-%E6%96%B9%E6%B3%95) - - [2.3 继承](#23-%E7%BB%A7%E6%89%BF) - - [2.4 多态](#24-%E5%A4%9A%E6%80%81) - - [2.5 抽象类](#25-%E6%8A%BD%E8%B1%A1%E7%B1%BB) - - [2.6 接口](#26-%E6%8E%A5%E5%8F%A3) - - [2.7 静态字段与方法](#27-%E9%9D%99%E6%80%81%E5%AD%97%E6%AE%B5%E4%B8%8E%E6%96%B9%E6%B3%95) - - [2.8 包](#28-%E5%8C%85) - - [2.9 作用域](#29-%E4%BD%9C%E7%94%A8%E5%9F%9F) - - [2.10 嵌套类](#210-%E5%B5%8C%E5%A5%97%E7%B1%BB) - - [2.11 classpath](#211-classpath) - - [2.12 jar包](#212-jar%E5%8C%85) - - [2.13 模块](#213-%E6%A8%A1%E5%9D%97) - - [3. Java核心类](#3-java%E6%A0%B8%E5%BF%83%E7%B1%BB) - - [3.1 字符串与编码](#31-%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%8E%E7%BC%96%E7%A0%81) - - [3.2 String操作类](#32-string%E6%93%8D%E4%BD%9C%E7%B1%BB) - - [3.3 包装类型](#33-%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B) - - [3.4 JavaBean](#34-javabean) - - [3.5 枚举类](#35-%E6%9E%9A%E4%B8%BE%E7%B1%BB) - - [3.6 记录类](#36-%E8%AE%B0%E5%BD%95%E7%B1%BB) - - [3.7 BigInteger](#37-biginteger) - - [3.8 BigDecimal](#38-bigdecimal) - - [3.9 常用工具类](#39-%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7%E7%B1%BB) - - [3.10 BigInteger实现分析](#310-biginteger%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90) - - [4. 异常处理与日志](#4-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E4%B8%8E%E6%97%A5%E5%BF%97) - - [4.1 Java异常](#41-java%E5%BC%82%E5%B8%B8) - - [4.2 捕获异常](#42-%E6%8D%95%E8%8E%B7%E5%BC%82%E5%B8%B8) - - [4.3 抛出异常](#43-%E6%8A%9B%E5%87%BA%E5%BC%82%E5%B8%B8) - - [4.4 自定义异常](#44-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%82%E5%B8%B8) - - [4.5 NullPointerException](#45-nullpointerexception) - - [4.6 断言](#46-%E6%96%AD%E8%A8%80) - - [4.7 使用JDK Logging](#47-%E4%BD%BF%E7%94%A8jdk-logging) - - [4.8 Commons Logging](#48-commons-logging) - - [4.9 Log4j](#49-log4j) - - [4.10 SLF4J & Logback](#410-slf4j--logback) - - [5. 反射](#5-%E5%8F%8D%E5%B0%84) - - [5.1 Class类](#51-class%E7%B1%BB) - - [5.2 访问字段](#52-%E8%AE%BF%E9%97%AE%E5%AD%97%E6%AE%B5) - - [5.3 访问方法](#53-%E8%AE%BF%E9%97%AE%E6%96%B9%E6%B3%95) - - [5.4 调用构造方法](#54-%E8%B0%83%E7%94%A8%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95) - - [5.5 获取继承关系](#55-%E8%8E%B7%E5%8F%96%E7%BB%A7%E6%89%BF%E5%85%B3%E7%B3%BB) - - [5.6 动态代理](#56-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86) - - [6. 注解](#6-%E6%B3%A8%E8%A7%A3) - - [6.1 使用注解](#61-%E4%BD%BF%E7%94%A8%E6%B3%A8%E8%A7%A3) - - [6.2 定义注解](#62-%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3) - - [6.3 处理注解](#63-%E5%A4%84%E7%90%86%E6%B3%A8%E8%A7%A3) - - [6.4 TODO](#64-todo) - - [7. 泛型](#7-%E6%B3%9B%E5%9E%8B) - - [7.1 什么是泛型](#71-%E4%BB%80%E4%B9%88%E6%98%AF%E6%B3%9B%E5%9E%8B) - - [7.2 使用泛型](#72-%E4%BD%BF%E7%94%A8%E6%B3%9B%E5%9E%8B) - - [7.3 编写泛型](#73-%E7%BC%96%E5%86%99%E6%B3%9B%E5%9E%8B) - - [7.4 泛型实现方法](#74-%E6%B3%9B%E5%9E%8B%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%B3%95) - - [7.5 extends通配符](#75-extends%E9%80%9A%E9%85%8D%E7%AC%A6) - - [7.6 super通配符](#76-super%E9%80%9A%E9%85%8D%E7%AC%A6) - - [7.7 泛型和反射](#77-%E6%B3%9B%E5%9E%8B%E5%92%8C%E5%8F%8D%E5%B0%84) - - [8. 集合](#8-%E9%9B%86%E5%90%88) - - [8.1 Java集合](#81-java%E9%9B%86%E5%90%88) - - [8.2 List](#82-list) - - [8.3 Map & HashMap](#83-map--hashmap) - - [8.4 EnumMap](#84-enummap) - - [8.5 TreeMap](#85-treemap) - - [8.6 Properties](#86-properties) - - [8.7 Set](#87-set) - - [8.8 Queue](#88-queue) - - [8.9 PriorityQueue](#89-priorityqueue) - - [8.10 Deque](#810-deque) - - [8.11 Stack](#811-stack) - - [8.12 Iterator](#812-iterator) - - [8.13 Collections](#813-collections) - - [9. IO](#9-io) - - [9.1 File](#91-file) - - [9.2 InputStream](#92-inputstream) - - [9.3 OutputStream](#93-outputstream) - - [9.4 Filter](#94-filter) - - [9.5 Zip](#95-zip) - - [9.6 读取classpath的资源](#96-%E8%AF%BB%E5%8F%96classpath%E7%9A%84%E8%B5%84%E6%BA%90) - - [9.7 序列化](#97-%E5%BA%8F%E5%88%97%E5%8C%96) - - [9.8 Reader](#98-reader) - - [9.9 Writer](#99-writer) - - [9.10 PrintStream & PrintWriter](#910-printstream--printwriter) - - [9.11 工具](#911-%E5%B7%A5%E5%85%B7) - - [10. 日期和时间](#10-%E6%97%A5%E6%9C%9F%E5%92%8C%E6%97%B6%E9%97%B4) - - [10.1 基本概念](#101-%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5) - - [10.2 时间戳](#102-%E6%97%B6%E9%97%B4%E6%88%B3) - - [10.3 Date & Calendar](#103-date--calendar) - - [10.4 LocalDateTime](#104-localdatetime) - - [10.5 ZonedDateTime](#105-zoneddatetime) - - [10.6 DateTimeFormatter](#106-datetimeformatter) - - [10.7 Instant](#107-instant) - - [10.8 最佳实践](#108-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5) - - [TODO](#todo) - - - - -# 入门一下Java - -Java教程:[廖雪峰Java教程](https://www.liaoxuefeng.com/wiki/1252599548343744) - -Eclipse教程:[Eclipse 教程](https://www.runoob.com/eclipse/eclipse-tutorial.html) - -JavaSE15 API文档:[Java® Platform, Standard Edition & Java Development Kit Version 15 API Specification](https://docs.oracle.com/en/java/javase/15/docs/api/index.html) - -写在前面:仅仅是关键性知识点的笔记,用来串联、查阅和回顾,并不系统也并不细节。 - -## 0. 准备 - -### 0.1 关于Java -- 1995年发布1.0版本,Java之父[James Gosling](https://en.wikipedia.org/wiki/James_Gosling),SUN公司财产,2009年被Orcale收购。 -- Java源文件编译为字节码后运行在虚拟机(JVM)上,字节码与指令集或者操作系统无关,通过在多平台实现虚拟机来实现跨平台。 -- JVM解释执行字节码,也就是所谓的解释器, 并不会最终生成不依赖于JVM的目标平台的可执行文件。那么从理解上来说Java的跨平台就可以通过Java源码或者Java字节码来实现。 -- Java字节码是二进制文件,可以理解为一层不依赖于硬件平台的指令集。 -- Java即要编译也要解释执行,所以称其为混合型语言。那么可预见的多了一层之后Java的性能应该要优于解释型语言如Python,劣于编译型语言如C/C++。 -- Java字节码向下兼容,低版本字节码可运行在高版本的JVM上。那么Java语言语法是否向下兼容呢?标准库是否向下兼容呢? -- Java是面向对象的编程语言,类似于C#,不同于C++多范式。当然这无关紧要。 -- 名词术语: - - Java SE: 标准版,标准的JVM和标准库 - - Java EE: 企业版,在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等 - - Java ME: 嵌入式版,针对嵌入式设备的“瘦身版” - - JRE:Java Runtime Environment,包含虚拟机(JVM)和运行时库,只是运行编译好的Java程序的话只安装JRE即可。如果要编译则需要完整的JDK。 - - JDK: Java Development Kit,除了JRE还包括编译器调试器等基础设施。前面说的三个版本(SE/EE/ME)就是所谓的JDK。如[Java SE Development Kit 8](https://www.oracle.com/cn/java/technologies/javase/javase-jdk8-downloads.html)。安装了JDK则不需要再安装JRE。 - - JSR:Java Specification Request,Java规范,给Java平台添加工功能时先创建JSP规范,定义好接口,以保证兼容性。发布时还需要同时发布参考实现(RI,Reference Implementation,实现基础功能不保证性能给其他人实现做参考)和兼容性测试套件(TCK,Technology Compatibility Kit,用于兼容性测试)。 - - [JCP](https://jcp.org/en/home/index):Java Community Process,负责审核JSR的组织。 -- Java版本:1.0 ~ 1.9(1.5 ~ 1.9也称5.0 ~ 9.0),之后10 ~ 15,最新版本Java15。 -- 文件后缀:`.java` - -### 0.2 开发环境 - -- 下载[JavaSE](https://www.oracle.com/java/technologies/javase-downloads.html),安装。 -- 配置环境变量: - - Windows:`JAVA_HOME`指向安装目录,把`%JAVA_HOME%\bin`添加到path。 -- `java -version`查看版本。 -- `bin/`下的执行文件: - - `java`: JVM运行java程序。 - - `javac`: 编译器,编译java源文件(`.java`)到字节码文件(`.class`)。 - - `jar`: 打包,把一组`.class`打包为`.jar`。 - - `javadoc`: 从java源码提取注释生成文档。 - - `jdb`: java调试器。 -- 常用IDE: - - [Eclipse](https://www.eclipse.org/): Java开发,基于插件结构。使用广泛,免费。 - - [IntelliJ Idea](https://www.jetbrains.com/idea/): `JetBrains`全家桶系列。商用,有免费和收费版。 -- 使用Eclipse: - - 安装:下载[Eclipse IDE for Java Developers](https://www.eclipse.org/downloads/packages/),无需安装,解压即可使用。 - - 汉化:下载中文汉化包,[地址](https://www.eclipse.org/babel/downloads.php),找到IDE对应版本汉化包,找到简体中文的包全部下载下来共20个左右,批量解压提取到当前位置,复制得到的两个文件夹`features/`和`plugins/`到安装目录。 - - 具体配置等略过不谈,网上一大把。无非就是编码、补全、文字样式、注释和代码风格等。 - -### 0.3 基本Eclipse使用 - -**项目管理:** -Eclipse中每个Eclipse进程管理一个工作空间(WorkSpace),其中可以创建多个项目,每个项目可以添加多个包。工作空间根目录下会生成`.metadata`目录保存工作空间相关配置,每个项目目录下也会生成项目文件。 - -**Eclipse基本调试操作:** -- `Ctrl+Shift+B` 添加移除断点 -- `Ctrl+F11` 开始运行 -- `F11` 开始调试 -- `F5` 单步跳入,能跳入(语句有函数调用)就跳入,不能就下一条语句 -- `Ctrl+F5` 单步执行选择,能跳入就跳入,不能就不执行 -- `F6` 单步跳过,下一条语句 -- `F7` 单步跳出,跳出这个函数执行(只执行完这个函数调用,回到调用语句) -- `F8` 继续,执行到下一个断点 -- `Ctrl+F2` 终止调试 -- `Ctrl+R` 运行至行 -- `Ctrl+Alt+B` 跳过所有断点,也就是无效化所有断点 -- `Drop to Frame` 拖放至帧(这什么破翻译,真就直译?),跳转到当前执行函数的开头开始执行,不会改变一个全局数据原有的值,只是切换了栈帧。 -- 跳过、跳出、运行至行等能够运行多行代码的操作执行过程中遇到断点都会断住。 -- 首选项,调试,单步执行过滤,过滤不需要关注的类。 -- 配合调用堆栈(位于调试窗口)、本地变量监视、条件断点、表达式求值,常用的调试操作也就这些了。 - -**其他提高效率的快捷键:** -- `Ctrl+O` 右键快速大纲,用于搜索当前文件中的字段或者方法以快速跳转 -- `Ctrl+F3` 显示当前符号的快速大纲 -- `Ctrl+T` 右键快速类型层次结构,显示派生层次结构 -- `F3` 转到一个符号的声明 -- `Ctrl+F` 查找符号 -- `Ctrl+Shift+G` 搜索当前工作空间所有该符号引用位置 -- `Alt + ->/<-` 代码浏览位置跳转 -- `Ctrl+H` 搜索,有精细的搜索选项,类型、方法、包等。 -- `Ctrl+Shift+R` 文件搜索 -- `Alt+Shift+R` 重命名符号 -- `Ctrl+Shift+F` 格式化当前文件的编码风格 -- IDE内运行终端:`Ctrl+Alt+Shift+T` - -说实话有些快捷键实在是有点太啰嗦,一点都起不到快捷的作用,说的就是`Alt+Sift+Q`加上一个键的那一堆,也懒得自己改,不要增加太多心智负担,记住常用的就好。如`Ctrl+O`, `F3`, `Ctrl+T`,加上常用的查找和调试快捷键就行。快捷键的使用必须要能够方便到在两秒钟内定位到想要的某个类、方法、文件、某个符号的所有引用、某个类的派生结构层次才算是舒服。 - -**Eclipse导入第三方库:** -- 右键项目,属性,Java构建路径,库,类路径(就是`classpath`),添加外部JAR,选择JAR添加之后即可导入到该项目中。然后在包资源管理器中`src`、JRE系统同级的引用的库中就有这个库了。并且这个库的路径是绝对路径。 -- 也可以在项目中新建文件夹,然后将`jar`文件复制到这个目录中,右键选中该jar,构建路径,添加到构建路径后同样会被加入到引用的库中,此时`jar`被加入到了项目中,如果删除文件夹会移除依赖,库的路径是以相对路径保存的。或者类路径右边选择"添加JAR"就是添加项目内的`Jar`路径。 - -**启用预览特性:** -- 右键项目,属性,Java编译器,取消JDK一致性,并勾选`Enable preview features for Java 15`。并且可以设置警告等级。 - -**Eclipse多个包或者多个入口管理:** -- 多个包同时存在都有入口时,在某个包含合法入口`main`的文件`Ctrl+F11`运行即执行当前入口。如果当前文件没有入口,那么执行上一个配置。 -- 在学习或者测试时可以给每个需要测试的类写一个`public static void main`,然后在当前文件编写并执行测试。 -- Ctrl+F11运行时会自动创建运行配置,也可以项目右键,运行方式,运行 配置,Java应用程序,新建配置进行新建。或者右键项目或者包,属性,运行/调试设置。 -- 上一个对话框中`Show Command Line`即可看到当前配置执行时的命令行。例: - ```shell - C:\eclipse\plugins\org.eclipse.justj.openjdk.hotspot.jre.full.win32.x86_64_15.0.1.v20201027-0507\jre\bin\javaw.exe - -Dfile.encoding=UTF-8 - -classpath "C:\Users\tch\Desktop\LearnJava\JavaStarted\bin" - -XX:+ShowCodeDetailsInExceptionMessages Test.Main - ``` -- 要更改命令行的公共参数可以在窗口,首选项,Java,已安装的JRE找到对应JRE,编辑修改默认VM参数。 -- 可以写多个入口的确很舒服,甚至可以互相调用。 - -**使用自己安装的JDK而不是Eclipse自带的:** -- 同样在窗口,首选项,Java,已安装的JRE,添加,标准VM,找到安装的JDK目录,添加自己安装的JRE环境而不是用Eclipse默认的。设置为默认后创建新项目时选择使用默认JRE作为编译环境即可。 -- 修改已有项目的JRE环境:右键项目,属性,Java构建路径,库,模块路径找到JRE,编辑,修改为工作空间缺省JRE即可跟随首选项里面的默认JRE变化。 -- Eclipse自带了JRE,最终执行结果都是一样的,一般来说也不需要改。 - -**使用Eclipse导出jar包:** -- 包资源管理器中选择包右键导出->Java->JAR文件,选择要导出的一个或多个包,填写入口类,即可出。 -- 不设置其他选项的话,导出的清单文件中也就只有版本和入口类的信息。 -- 当然还可以导出其他文件,清单文件也可以有很多其他配置内容,尚不清楚,有需求再了解。 - -### 0.4 基本IntelliJ IDEA使用 - -[IDEA](https://www.jetbrains.com/idea/download/#section=windows)分为旗舰版和社区版,免费的社区版相比旗舰版阉割了不少功能,比如Profiling Tools,Spring/Java EE等框架,HTTP客户端,前端编程语言支持,数据库相关,Kubernetes等。旗舰版有30天实用。刚开始可以用社区版,但要开发Web和企业应用还得旗舰版,个人授权旗舰版\$149/Year,Jet Brains全家桶捆绑包\$249/Year。 - -**安装中文插件:** -插件商店搜索Chinese,安装**Chinese (Simplified) Language Pack / (中文语言包)**。 - -**项目管理:** -新建项目,选择Java模块,会自动检测添加到path的JDK,也可以添加JDK选项选择其他版本JDK。项目文件存放在`.idea`目录中。 - -在IDEA中没有工作空间的概念,只有Project和Module的概念,分别对应于Eclipse的WorkSpace和Project。所以一个IDEA窗口只能管理一个项目,如果要打开多个项目,需要打开多个窗口。一个项目可以有多个模块。 - -第一次新建模块时会需要创建新项目,模块不需要一定放在项目目录下(类似于VS的解决方案和项目),但最好将模块放在项目目录下,并且是子目录中,而不是项目根目录。然后打开项目之后再次新建模块就会默认在项目目录下的模块同名子目录了。模块文件`.iml`。 - -然后模块下可以新建Package,Package中新建class/interface/enum/annotation/record,总体感受上和Eclipse差不多。只是Eclipse由项目管理自己的生成文件,而IDEA由项目管理所有模块的生成文件,生成文件保存在`Project_Dir/out/production/moduleName/package/XXX.class`。 - -文件菜单,项目结构,打开项目结构窗口,可以修改项目、模块、库、JDK等一系列设置。比如项目默认JDK、语言级别、输出目录;模块名称、源文件夹、模块JDK版本;添加新的JDK等。 - -可以用不同视图:项目、包、项目文件等视图观察当前项目。 - -**引入第三方库:** - -**运行与调试:** -`Shift+F10/F9` 运行/调试当前配置 -`Alt+Shift+F10/F9` 选择配置并运行/调试 -`Alt+Shift+F5` 附加到进程 - -**常用快捷键:** - -**导出jar包:** - - -## 1. Java语言基础 - -### 1.1 hello,world! -```java -public class Hello -{ - public static void main(String[] args) - { - System.out.println("hello,world"); - } -} -``` -从hello,world!可以看出来的细节: -- 语句以`;`结尾。 -- 类似C#以某个类的静态main函数为入口,`main`的参数不同于C/C++,没有命令参数个数,因为数组可以确定大小。 - -### 1.2 基本约定 - -- 类名必须以英文字母开头,后接字母,数字和下划线的组合。 -- 习惯以大写字母开头。 -- 注释`/** */`, `/* */`, `//`同C++,多行注释`/** */`需要写在类和方法的定义处,可以用于自动创建文档: - ```java - /** - * comments for javadoc - */ - public class HelloWorld { - } - ``` - -### 1.3 基本数据类型 - -- 整型:只提供带符号类型 - - `byte`:1个字节 - - `short`:2个字节 - - `int`:4个字节 - - `long`:8个字节 -- 浮点:IEEE-754标准浮点数,同C - - `float`:4个字节 - - `double`:8个字节 -- 字符:UniCode字符 - - `char`:2个字节 -- 布尔:取值`true`和`false`。 - - `boolean`:大小取决于JVM实现,底层不提供。 - -字面值后缀: -- `long` : L/l -- `float` : F/f -- `double` : D/d - -从`byte/short/char–>int–>long–>float–>double` 可以自动进行隐式转换,反过来如果是窄化转换则必须使用强制转换。 - -强制类型转换: -- `(targetType)variable`, 同C风格,而不是C++风格,表达式需要加括号。 -- 可以进行窄化转换: - - 浮点转整型,会将小数部分丢掉,如果超过表示范围,则返回整型最大值。 - - 整型窄化转换直接截断,因为没有无符号类型,少了很多转换问题。 - -变量作用域: -- 和其他语言一样。 -- 局部变量和函数参数作用于块作用域`{}`。 -- 类成员作用于类作用域。 -- 不存在全局变量,也就不存在全局作用域。 -- 里层作用域符号覆盖外层作用域同名符号定义。 - -### 1.4 基本运算 - -- 算术运算:`+` `-` `*` `/` `%` `+=` `-=` `*=` `/=` `%=` `++` `--` 同样有前置和后置的区别,同C。 -- 关系运算:`>` `>=` `<` `<=` `==` `!=` -- 逻辑运算:`&&` `||` `!`(`&&` `||` 短路求值) -- 按位运算:`&` `|` `~` `^` -- 移位运算:`>>` `<<` 算术移位,`>>>`不考虑符号位逻辑右移,有无符号右移但没有左移,支持`<<=` `>>=`。 -- 三元运算:`? :`。 - -类型自动提升: -- 整型运算:计算结果为较大类型的整型。 -- 整型与浮点运算:结果提升至浮点型。 -- 注意在复杂运算中,就算最终结果是浮点,但其中两个整数的运算不会出现自动先提升为浮点。同所有语言一样,适当位置`*1.0`先提升为浮点保证结果正确。 - -溢出: -- 溢出同样不会报错,需注意。 -- 整型除0会报错。抛出`java.lang.ArithmeticException` -- 浮点除0不会报错,会返回一些特殊值: - - `NaN`表示`Not a Number`, 如`0.0/0.`0,`(3/0.0) / -(3/0.0)` - - `Infinity`表示无穷大,如`3/0.0`。 - - `-Infinity`表示负无穷大,如`-3/0.0`。 - - 实际上很少出现。 - -### 1.5 字符和字符串 - -字符`char`: -- 常量使用`''` -- 保存一个UniCode字符 -- 可以用`\uXXXX`来表示 -- 转义`\"` `\'` `\\` `\n` `\r` `\t` `\uXXXX` - -字符串: -- `String` -- 字面值:`""` -- 可以用`+`连接字符串和其他任意数据类型,连接前会先将其他数据类型转换为字符串 -- 多行字符串:前面的共同空格会被去掉,取最短的。 - ```java - """ - 就这就这? - 不会吧?不会吧? - """ - ``` -- 字符串是引用类型,可以指向一个空值`null`。类似于表示它是空指针,不指向任何值。空字符串和`null`当然是有区别的。引用类型指向一个值,基本类型持有一个值,非常好理解。 - -### 1.6 数组 - -- 创建后大小即固定,也就是指向的那个数组大小就固定,但是可以将其指向新的数组。 -- 数组下标越界将引发运行时错误,抛出`java.lang.ArrayIndexOutOfBoundsException`异常。 -- 初始化时直接指定元素值,则编译器会自动推导大小:`int[] A = new int[] {1,2};`,初始化时必须指定大小或者提供初始值,两者选一者,不能同时存在,指定大小时初始化为默认值(整型0,浮点0.0,`boolean`则是`fals`, 引用`null`)。可以进一步简写为`int[] A = {1,2}`。 -- 定义: `type[]` -- 长度: `length` -- 取成员: `[]` -- for each: `for(type val : arr)` -- 转换为`String`: `Arrays.toString()`,输出`[elem1, elem2, ..., elemLast]`,需要`import java.util.Arrays;` -- 排序: `Arrays.sort()` -- 多维数组: `type[][]`,即是数组的数组,不要求每个元素统一大小。 -- 多维数组转字符串: `Arrays.deepToString()` - -### 1.7 输入输出 - -- 输出:`System.out.print` -- 换行输出:`System.out.println` -- 格式化输出:`System.out.printf`,占位符`%d`十进制 `%x`十六进制 `%f`浮点数 `%e`科学计数法 `%s`字符串,`%%`转义表示`%`本身。详细信息见文档[java.util.Formatter](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/Formatter.html)。 -- 输入: -```java -import java.util.Scanner; -public class Main { - public static void main(String[] args) { - Scanner scanner = new Scanner(System.in); - String sInputStr = scanner.nextLine(); // 下一行 - int iNextInt = scanner.nextInt(); // 下一个输入解析为整数 - } -} -``` -很好理解,但也感觉很啰嗦,可能的确需要啰嗦才能好理解吧。 - -### 1.8 流程控制——条件 - -和C/C++如出一辙:同样,只有一条语句时`{}`可以省略。 -```java -if (condition) { -} -else if (other-condition) { -} -else { -} -``` - -对引用变量`==`判等时,含义是判断是否指向同一个对象。判断内容相等需要使用`equals`方法。在java中,通常称函数为**方法(mtthod)**,因为函数必定是某个类的成员,不管是不是静态。 - -众所周知,浮点数不要直接判等,通常判断差值是否在精度范围内。 - -`switch`语句: 同C,不赘述,可以使用整型、枚举。不同的是java中`String`也可以作为`switch`的判断条件,这就很方便了。因为C/C++其实并没有语法层面的字符串抽象类型。 -```java - switch (option) { - case 1: - System.out.println("Selected 1"); - break; - case 2: - System.out.println("Selected 2"); - break; - case 3: - System.out.println("Selected 3"); - break; - } -``` -语法糖:`case -> statement;`执行结束退出,不需要`break`。Java12开始。 -并且还可以直接返回一个值: -```java -double d = switch (i) { -case 1 -> 1.0; -case 2 -> { - System.out.println("i is 2"); - yield 10; // switch statement return to 10 -} -default -> 100; -}; - -switch (i) { - case 1 -> System.out.println("i is 1"); - default -> System.out.println("i is not 1"); -} -``` -如果有多种情况,中间还有语句执行,还可以用`{}`包起来,执行多个语句后用`yield`返回,就像定义一个函数那样。 - -算是一个语法糖,完全可以找到等价的写法替代。 - - -### 1.9 流程控制——循环 - -```java -while(condition) { -} - -do { -} -while(conditon); - -for (init; condition; update conuter) { -} - -for (type val : valArr) { // iterate iterable type, like array([])/String/etc. -} -``` - -同样可以用`break`/`continue`。 - - -### 1.10 命令行参数 - -```java -public class Main { - public static void main(String[] args) { - System.out.println(Arrays.toString(args)); - } -} -``` - -命令行执行`java Main.java hello world`,则传递给`main`的参数`args`为`["hello", "world"]`。 - - -## 2. java面向对象 - -不同于C++,Java中称数据成员为**字段**(**field**),称成员函数为**方法**(**method**)。 - - -### 2.1 类和对象 - -- 关键字: `class` -- 指代对象自身: `this`引用变量 -- 引用类型: 创建类对象实例都是使用引用变量来使用,可以理解为指向堆区域对象的指针,但是毕竟java没有指针(但的确有一个与空指针语义相同的`null`),也就没有解引用和直接在栈上实例化的操作。C++通过多重指针、指针引用等可以精确控制一个对象的生存周期,持有关系等任何行为。 -- 单从语义上理解java的引用和C++非常像,不同的是java中可以更改引用指向的对象,还可以指向`null`,就像是C++指针和引用的融合一样。 -- 构造方法(Constructor): 无返回值,当然不定义的话编译器会生成一个函数体为空的无参默认构造方法,定义之后便不再生成默认构造。光凭想象就可以想到不用管理内存之后相比C++会少掉多少考虑拷贝构造、移动构造、赋值运算符这类的事情,瞬间好像就轻松起来了。 -- 默认构造时各种类型默认值: 不同于C++如果不给初始化,对于没有构造函数的内置类型(整型浮点指针),分配内存之后那块区域里面存什么值,成员就会是什么值。java中理所当然会执行默认初始化,引用初始化为`null`,数值类型用默认值`0`,`boolean`则是`false`。 -- 初始化:java可以直接在类内成员定义时给初始值。执行顺序理所当然是先初始化为字段默认值或定义时给定的值,然后再执行构造方法。 -```java -class Person { - private String name; // 默认初始化为null - private int age = 10; // 初始化为10 - private double weight = 50; // 最终初始化为构造函数中给的值 - public Person(double weight) { - this.weight = weight; - } -} -``` -- java中没有类似C++中构造函数初始化列表这种东西。所以初始化就两个途径:给初始值、构造函数中赋值。 -- 调用其他构造:`this(args);`,必须放在当前构造的第一条语句。 -- 调用基类构造:`super(args);` -- 除了构造函数之外,从各种意义上我们都需要有一个析构函数,因为不需要管理内存,好像析构存在的意义就没有那么大了。但用不用另说,必须有是确定的。java中扮演这个角色的就是`void finalize()`方法。 - - 但其实如果去看`Object.finalize`的注释的话,上面会说,这个方法从Java9就已经废弃了,原因是这个机制在本质上存在问题。如果在`finalizer`中出现错误,可能会导致资源泄漏、线程/进程挂起、死锁、造成性能问题。而且如果没有必要也无法取消,析构时机和顺序也无法保证。这我这直接好家伙!那你这保证了个啥? - - 作为替代:那些持有了并非是普通的堆内存资源但是需要释放的,都应该提供方法来显式释放资源,如果合适的话应该实现`AutoCloseable`接口。`java.lang.ref.Cleaner`和`java.lang.ref.PhantomReference`提供了更灵活和高效的对象用完后释放资源的方法。具体怎么用什么场景用还需要进一步探究。 -- 访问权限: - - `public` - - `private` - - `protected` - - 默认,不写访问修饰符 - - 用在类、方法或者字段上。 -- 思考与探索:有没有类似于C++中`=default`,`=delete`那种显式使用或者禁用默认构造/`=`运算符的语法呢? - -### 2.2 方法 - -- 可变参数: - - 定义: `void mthod(type ... args)` - - 调用: `method(arg1, arg2, arg3)` - - 当然这要求所有参数同类型,和C中通过`va_list`加上一个显式或隐式的参数个数来实现的方式有区别 - - 最终`args`被解释为一个数组,同类型可变参数完全可以使用数组来传递,但是需要调用时显式构造数组,并且可以传递`null`为参数,可变参数算是一个还不错的语法糖。 -- 方法重载(Overload): - - 同C++一致的是,方法重载只与参数列表(类型和顺序)有关,和返回值,访问修饰符无关系。C++中函数重载之后其实就是成为了不同的函数,经过名称修饰之后符号是不同的。那么java有没有类似于名称修饰一类的东西呢?是如何保证调用时正确跳转到对应的函数入口地址的呢?这可能需要后续了解了字节码之后才能知道。 - - 重载函数返回值类型和访问修饰符不必相同,这是理所当然的。 -- 问题来了:可变参数类型参数算作数组还是多个参数呢?和重载混合在一起如何工作?如何判断调用哪一个? - - 答案是如果一个调用即匹配有可变参数的版本也可以匹配无可变参数的版本。那么优先调用无可变参数版本。如果同时匹配了两个有可变参数的版本,那么会存在歧义编译错误。也就是只有无可变参数优先于有可变参数,都有可变参数的话优先级相同。这样的重载如果不尝试去调用则并不会报冲突。 -```java -public class Person { - private String name; - public void setName(String ... args) { - StringJoiner joiner = new StringJoiner(" "); - for (String s : args) { - joiner.add(s); - } - name = joiner.toString(); - } - - public void setName(String firstName, String ... otherName) { - StringJoiner joiner = new StringJoiner(" "); - joiner.add(firstName); - for (String s : otherName) { - joiner.add(s); - } - name = joiner.toString(); - } - - public void setName(String firstName, String lastName) { - name = firstName + " " + lastName; - } - - @Override - public String toString() { - return "Person: " + name; - } - - public static void main(String[] args) { - Person p = new Person(); - p.setName("Mary", "Jane"); // call setName(String firstName, String lastName) - p.setName("Mary", "Jane", "King"); // java.lang.Error - p.setName("Mary"); // java.lang.Error - System.out.println(p); - } -} -``` -- 默认参数: - - 根据已知信息,并不存在这种东西,请通过方法重载来实现。 - ```java - public void method(type1 v1, type2 v2) { - } - public void method(type1 v1) { - method(v1, default_v2); - } - ``` - - 也是,不然和可变参数混在一起编译器估计必然要凌乱,也减少心智负担,挺好。 - - -### 2.3 继承 -- 提前总结所有关键字: `class` `extends` `Object` `implements` `super` `protected` `sealed` `permits` `final` `intanceOf` `@Override` -- 语法:`class Student extends Person` -- 根类:`Object`,没有父类的类都会自动继承`Object`。 -- 继承方式仅有一种,不像C++一样还有私有保护公有继承,虽然我从来未在工作中碰到过私有和保护继承就是了,无用的东西都可以剃掉,好耶! -- java不像C++一样可以多继承,也就是一个类只能有一个基类。一般来说的确是如此呢!这样的话类的继承关系就会是一棵真正的树,不会有多个基类。 -- 其实也可以变相的多继承:`class C implements A,B`。只是`A`和`B`必须是接口(`interface`)。 -- 调用基类构造:`super(args);`,也即是用`super`指代这个对象中的基类部分。引用父类字段或者方法时使用。 -- 当然子类不会继承父类所有构造函数,不写的话默认构造是编译器生成,此时调基类的默认构造,即等价于在构造函数第一行添加`super();`,如果基类没有默认构造,那么是编不过的。 -- 访问限定:和C++的公有继承如出一辙,子类无法访问父类`private`,可以访问`protected`和`public`。 -- 不要在派生类中定义和基类字段同名的字段。当然并非语法上禁止,只是工程实践中不要这么做比较好,子类作用于会覆盖基类作用域,然后局部方法作用域会覆盖类作用域,你总可以使用`this.val`、`super.val`来区分它们。在C++允许多继承所以使用`baseclass::val/baseclass::func`。 -- 如果在一个很长的继承链条里面都定义了同名的字段(当然这是很脑残的行为),因为只有`super`和`this`可以指代父类对象和当前对象引用,而没有可以代表父类的父类对象的引用。如果没有定义同名字段,`this.val`是从父类继承而来,那么就等同于`super.val`。当然在实践中一般还是创建`getter`和`setter`调方法而不是直接操作父类字段比较好。 -- 阻止继承:`sealed`配合`permits`关键字只允许指定的类继承该类,`public sealed class Shape permits Rect, Circle, Triangle`,这个操作有点意思。 -- 使用`final`修饰类表明该类不能再被继承。同C++。`final calss A extends B`。 -- 类型转换:向上转换必然成功,向下转换如果类型匹配则可以成功,不匹配则会失败抛异常,很好理解。 -- 判断是否是指定类型:`instanceof`,返回`boolean`,完全匹配的类型或者基类都会返回`true`。RTTI有了,好耶!语法层面支持真棒!C++某些时候还要自己去实现RTTI就很烦了(小声说其实C++也有原生的半吊子的RTTI就是了)。 - - 用法:`obj instanceof Type`,返回`boolean`。 - - Java14开始:`obj instanceof Type s`,返回`boolean`。且可直接使用转换后的变量`s`。默认配置本地Eclipse/Java SE15上显示`preview feature and disabled by default`?见[0.3 基本Eclipse使用](#03-%E5%9F%BA%E6%9C%ACeclipse%E4%BD%BF%E7%94%A8)。 -- 继承与组合:is与has的关系要区分清楚当然不用多说,has关系不应该用继承。 -- 覆写(Override): - - 在C++中,只有虚函数可以重写,和重载一样,判断是否是覆写的标准依然是函数参数列表(方法签名)。如果重写了一个并非虚函数的函数,那么也就不能称之为重写,非虚函数不会在虚表中,也就不会有多态,调用指针/引用/变量类型在编译期就确定了到底调用哪一个。 - - 在Java中,所有函数皆是虚函数,其实也就是没有虚函数的概念,所有普通函数皆可以被重写。除非显式使用`final`阻止重写,不然都可以重写,使用了`final`再在子类中重写会编不过。如果是方法签名相同但返回值不同的重写,当然会报错。 - - `@Override`放在方法前,可以让编译器帮助检查是否进行了正确的覆写。如果是重写,但是方法名写错了,就会报错,如果不加,那么就会有基类方法和子类接口写错的方法同时可用。懂了,防呆设计!怪不得都说Java可靠性高,新手不容易犯错。当然,这是非必须的。 - - 看起来并没有那种能够在子类重写父类同名方法但又能使其不具有多态性的行为。这不是废话吗? - -### 2.4 多态 - -- java中不能在栈中实例化对象,所以都取决于运行时类型就是了,运行时是什么类型,就调那个类型的方法,可能是重写的基类方法,也可能是继承自基类的方法,总之是运行时类型所决定,当然要能够编过,那必然要编译时类型能够调用该方法才行。也就是说要使用基类重写的方法,需要是用这个基类方法重写自的那个类或者派生链条中间的类的引用。总感觉我在说一堆废话。 -- 根类`Object`重要方法: - - `toString`,将对象输出为`String`。 - - `equals`,两对象判等。貌似并没有重写`operator==`这种选项(因为根本就不支持运算符重载呀),看来C++的确自由度要更高那么一点点,当然心智负担也要高那么亿点点。对引用变量来说,`==`就是判断是否指向同一个对象,也就是保存的地址是否相等。 - - `hashCode`,计算对象哈希值,java赛高! -- 子类调用基类方法:`super.method`。 -- 字段加`final`之后表示在第一次初始化(字段赋初值和在构造函数中初始化两者选其一)之后便不能更改。有点像C++`const`成员,只能在构造函数初始化列表中赋值,或者C++11之后的类内初始值。 - -### 2.5 抽象类 - -- 声明:`abstract`同时用于类和方法。放在类返回值前和`class`前。方法也就称之为**抽象方法**。 -- 抽象类无法实例化,和C++抽象类同样的,还是类,也可以有成员,也可以有构造、被派生。当然也不能多继承,抽象类的修为还不够,需要舍弃血肉,灵魂飞升变为接口后才可以被多继承。 -- 面向抽象编程:尽量引用顶层抽象类或接口,不关心具体子类型。本质: - - 上层代码只定义规范。 - - 不需要子类就可以实现业务逻辑。 - - 具体的业务逻辑由不同的子类实现,调用者并不关心。 -- 定义了抽象方法的类必须定义为抽象类。不实现抽象方法的话,子类依然是抽象类。 -- 抽象类可以没有抽象方法,但同样无法直接实例化。但可以通过匿名类(见后文)实例化: -```java -abstract class Animal { - String name; - public static void main(String[] args) { - Animal p = new Animal() {}; - } -} -``` - -### 2.6 接口 - -- 关键字:`interface`。 -- 对标C++的纯虚类,当然C++允许多继承,所以纯虚类和普通抽象类并无太大区别,可以构造、可以有成员。但java只有接口类允许多继承,那么当然就不能由只有一个的基类引用`super`来完成构造,那么必然就不能有正常属于普通类对象的字段和构造。 -- 其实写了构造也能编过,会被识别为一个普通方法提示缺少返回值,而不是构造。写了字段也没有问题,貌似被当做了静态字段来处理。别说细节还挺多。经过求证接口的字段会自动`public final static`。 -- 实现接口时,需要使用`implements`关键字。 -- 同时派生一个类,并实现接口:`class A extends B implements C,D`。`super`将指代`B`,没有`B`的话就是默认的`Object`。 -- 接口是可以允许类似菱形继承这种继承方式的,因为没有字段就不需要考虑多份数据怎么存存几份怎么取这种问题了。 -- `default`方法:接口中也可以有实现了的方法,此时就需要加`default`关键字,当然没有字段可以给它访问。目的是实现没必要在所有子类中重写的接口,派生类中可以不进行重写。所以其实接口和类的区别就只有是否有字段和构造这一点是吗? -- 接口中所有方法默认`public abstract`,不需要显示写出。 -- 接口可以继承另一个接口,同样使用`extends`。 -- 最后接口这个词,正常来说应该是表示纯虚类,上层的抽象出来的和实现类相对的没有数据的抽象类。但工作里面感觉好多人都用接口来表示函数,比如在这个类里面加一个接口这种说法。最后Java里面有人说函数吗?都是说方法吗?字段会说成成员吗?说实话从C++切换过来什么都还好还是有点不适应名词变了。 - -### 2.7 静态字段与方法 - -- 属于整个类,并不属于某个实例,所有实例都可以使用。 -- 通过`className.staticFieldOrMethod`来访问,当然也可以通过`aInstance.staticFieldOrMethod`访问,等价于前者,但最好使用类名来访问,更加清晰。这点与C++是相同的。 -- 静态方法无法使用`this`变量,只能访问静态字段。 -- `interface`是可以有静态字段的,并且只能是`final`的。所以编译器会自动为interface的字段加上`public final static`。 -- 常用于工具类辅助类等。 - -### 2.8 包 - -- Java中使用Package来解决名称冲突,因为不同的人写的代码是完全可能出现名称重复的。C++中则是使用`namespace`,但是C++的`namespace`仅仅是加了一层作用域而已,仅用于解决名称冲突问题。而java的包功能则更多。 -- 调用方式:`Package.class`。 -- 声明:`package packageName;`,必须写在程序有效代码第一行。 -- 虚拟机运行时,JVM只看完整的类名,只要包名不同,类就不同。包可以是多层结构,用`.`隔开,类似于`java.util`。 -- 包不存在任何父子关系,所以`java.util`和`java.util.zip`是不同的包。C++的namespce也可以嵌套,而且是有关系的。 -- 不使用包名,那么就是使用默认包,类似于使用全局作用域。 -- Java中包还要求和目录层次完全统一,不然就是语法错误。生成的`.class`也会是同样的结构。由衷感叹java真省心啊! -- 在一个类中引用了其他类时,可以使用完整包名,也可以使用`import`将包中的类导入进来。类似于C++中的`using namespace XXX;`。 -- `import`用法: - - 导入一个包所有`class`:`import package.*;` - - 导入一个类:`import package.XXXclass;` - - 导入一个类中的所有静态字段和方法:`import static pacakge.class.*;`,使用较少。这样引入甚至可以引入同一个包的其他类中的静态方法,和引入其他包一样,包名不能省略。 -- Java编译器最终编译出的`.class`只使用完整类名,编译器遇到一个类名时如果是完整类名,直接根据完整类名查找这个 `class`。如果是简单类名按照以下查找顺序进行查找: - - 当前包中查找。 - - 导入的包中查找。 - - `java.lang`包查找。 - - 还无法确定类名就报错。 -- 编写一个类时,编译器默认做的事情: - - 默认自动导入当前包所有类。 - - 默认自动导入`java.lang.*`。但像`java.lang.reflect`这种其实和`java.lang`不是一个包,也没有父子关系,还是需要手动导入的。 -- 不同包中两个类有相同的类名,都导入就会名称冲突,因为包没有嵌套这个说法所以最多只能导入其中一个,另一个需要写完整类名。 -- 要移动一个文件所在的包时,IDE都会自动完成文件操作,还提供一键更新引用这种操作,可以说很方便了。 -- 相比C++的继承自C的原始的头文件包含方式、头文件宏定义保护防止重复包含、接口实现分离、交叉引用、前向声明,java可以算的上很方便了。 -- 命令行编译多个包多个源文件:在`src`目录下编译到`bin`目录下。 -```shell -javac -d ../bin ming/Person.java hong/Person.java mr/jun/Arrays.java -``` -- 最佳实践: - - 推荐包名命名方法:使用倒置的域名。如: - - `org.apache` - - `org.apache.commons.log` - - 注意类命名不要和`java.lang`包重名。不要使用类名:`String` `Runtime` `System` ... - - 也不要和JDK常用类重名:`java.util.ArrayList` `java.math.BigInteger` ... - -### 2.9 作用域 - -总览: -|修饰符\能否访问\访问位置|本类|同一个包的类|继承类|其他包的类| -|:-:|:-:|:-:|:-:|:-:| -|`private`| Yes|No|No|No| -|无(默认)| Yes|Yes|No|No| -|`protected`| Yes|Yes|Yes|No| -|`public`| Yes|Yes|Yes|Yes| - -- 访问修饰符限定了访问作用域 -- `public` - - `public`的类和接口可以被其他任何类访问。 - - `public`的方法和字段可以被其他类访问,前提是能访问类。 -- `private` - - `private`字段和方法无法被其他类访问,仅类内可以访问。 - - Java支持嵌套类,嵌套的类也在类内,也可以访问该类的私有字段和方法。 -- 包作用域/默认作用域 - - 包作用域的类和方法、字段可以被同一个包内的类访问。 - - 一个包不可访问另一个包内默认作用域的类、字段或方法。 -- `protected` - - `protected`字段和方法可以派生类访问。 - - `protected`字段和方法包内同样可见。 - - 需要注意子类和父类不在同一个包时: - - 在实例方法中可以通过`this`或者`super`来访问父类的`protected`实例方法。 - - 但在子类静态和实例方法中都不能通过父类引用(引用一个新`new`出来的子类或者父类对象)来访问父类的`protected`实例方法。如果是使用子类引用则可以访问。【因为编译时应该是按照引用类型的访问修饰符来确定访问权限的】算是一个没什么卵用的小细节。 - - 静态`protected`方法则在子类中均可访问。 -- `private`和`protected`不能用来修饰类,但可以用来修饰嵌套类。一个最外层的非嵌套类只能用`public`/`final`/`abstract`修饰。或者不用`public`包内使用。 - -- 在子类中重写实例方法时可以扩展访问限定符: - - 也就是可以从`protected`扩展到`public`。 - - 同一个包内的话也可以将默认访问扩展到`protected`或者`public`。 - - 不能在子类重写时减低访问范围。目的很明显,确保我能用父类引用访问的方法,用子类引用都能够访问。 - -- 最佳实践: - - 不确定是否需要`public`,就不声明为`public`。 - - 把方法定义为`package`权限有助于测试,因为测试类和被测试类只要位于同一个`package`,测试代码就可以访问被测试类的`package`权限方法。可以用于包内不用来公开的内部实现类。 -- 一个java类只能有一个`public`类,有`public`类时文件名必须和类名相同。没有时则不要求。 - -### 2.10 嵌套类 - -- 内部类(Inner Class) - ```java - class Outer { - class Inner { - } - } - ``` - - Inner类实例不能单独存在,必须依附于一个Outer的实例。 - ```java - public class Main { - public static void main(String[] args) { - Outer outer = new Outer("Nested"); // 实例化一个Outer - Outer.Inner inner = outer.new Inner(); // 实例化一个Inner - inner.hello(); - } - } - - class Outer { - private String name; - Outer(String name) { - this.name = name; - } - class Inner { - void hello() { - System.out.println("Hello, " + Outer.this.name); - } - } - } - - ``` - - 要实例化一个Inner,我们必须首先创建一个Outer的实例,然后调用``Outer``实例的`new`来创建Inner 实例。因为Inner Class除了有一个`this`指向它自己,还隐含地持有一个Outer Class实例,可以用 `Outer.this`访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。 - - `Outer`类被编译为`Outer.class`,而`Inner`类被编译为`Outer$Inner.class` - -- 匿名类(Anonymous Class) - - 不需要在Outer Class中明确地定义这个Class,而是在方法内部,通过匿名类(Anonymous Class)来定义。 - ```java - public class Main { - public static void main(String[] args) { - Outer outer = new Outer("Nested"); - outer.asyncHello(); - } - } - - class Outer { - private String name; - - Outer(String name) { - this.name = name; - } - - void asyncHello() { - Runnable r = new Runnable() { - @Override - public void run() { - System.out.println("Hello, " + Outer.this.name); - } - }; - new Thread(r).start(); - } - } - ``` - - `Runnable`是一个接口,`asyncHello`方法内`new`的时候定义了一个没有类名的匿名类重写了`run`方法,重写`run`接口之后实例化并给了`r`。 - - `Outer`类被编译为`Outer.class`,而匿名类被编译为`Outer$1.class`,如果有多个匿名类,那么被编译为`Outer$2.class` etc - - 除了接口外,匿名类也完全可以继承自普通类。 - - 匿名类相对来说还比较常用。 -- 静态嵌套类(Static Nested Class) - - 和Inner Class类似,但是使用`static`修饰,称为静态内部类。 - - 用static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用`Outer.this`,但它可以访问`Outer`的`private`静态字段和静态方法。 - - 就是一个独立的类,只是有Outer Class的private访问权限。 - - 果然我觉得这才比较正常,像内部类,一个类依赖于一个对象感觉有一点点奇怪,暂不清楚应用场景。 - ```java - public class Outter { - public static void main(String[] args) { - Outter.Inner inner = new Outter.Inner(); - inner.hello(); - } - static class Inner { - public void hello() { - System.out.println("hello, static nested class"); - } - } - } - ``` - -### 2.11 classpath - -- classpath是什么? -- JVM用到的一个环境变量,它用来指示JVM如何搜索class。 -- 因为Java是编译型语言,源码文件是`.java`,而编译后的`.class`文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个`abc.xyz.Hello`的类,应该去哪搜索对应的`Hello.class`文件。 -- 设定方法 - - 系统环境变量中设置`classpath`环境变量,不推荐,会污染整个系统环境。 - - 启动JVM时设置`classpath`变量,推荐。启动时添加`-classpath`或者`-cp`选项,添加`;`分割的路径作为参数(Windows中,linux中用`:`分割)。 -- IDE中运行时,自动传入的`-cp`参数就是工程`bin`目录和引入的`jar`包。 -- JVM不依赖classpath加载核心库,不需要将核心库的路径传入classpath。 -- 更好的做法是,不要设置`classpath`!默认的当前目录`.`对于绝大多数情况都够用了。 - - -### 2.12 jar包 - -- 如果有多个`.class`文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。 -- `jar`包就是用来干这个事的,它可以把package组织的目录层级,以及各个目录下的所有文件(包括`.class`文件和其他文件)都打成一个`jar`文件。 -- `jar`包实际上是一个zip格式的压缩包文件,jar包相当于目录。执行一个jar包里的class,就可以把jar包放到classpath中。 -```shell -java -cp ./hello.jar abc.xyz.hello -``` -- 因为`jar`包就是zip文件,所以直接将`bin`目录中的目录和文件压缩成`zip`文件,更改后缀为`.jar`就算制作成功了一个`jar`包。值得注意的是,`bin`目录不应该被包含到压缩包的路径中。 -- `jar`包还可以包含一个特殊的`/META-INF/MANIFEST.MF`文件,`MANIFEST.MF`是纯文本,可以指定`Main-Class`和其它信息。JVM会自动读取这个`MANIFEST.MF`文件,如果存在`Main-Class`,我们就不必在命令行指定启动的类名,而是用更方便的命令:`java -jar hello.jar`。 -- 举例来说,如果写了两个包一个`Main`一个`Hello`,编译后`bin`目录下生成了两个目录`Main/`和`Hello/`,选中这两个目录,zip格式压缩到文件`Main.jar`,文件名无关紧要。执行时:`java -cp ./Main.jar Main.Main`。jar文件位置随意,路径给对就行,包中的类名随意,只要你定义了`public static main`即可执行。没有这个类或者依赖了其他类但是打包时没有加进去则解释执行时JVM会抛出`java.lang.ClassNotFoundException`。 -- 一个包中可以有多个类有`public static main`方法,甚至可以互相调用。执行时通过参数指定想执行哪一个就执行哪一个。 - -- 清单文件 - - 如果没有`jar`包中`/META-INF/MANIFEST.MF`,那么是不能通过`java -jar`来执行的。 - - 清单文件中定义了许多内容,但不必全部关心。 - - 手动创建清单文件:注意最后要有一个空行。 - ``` - Manifest-Version: 1.0 - Main-Class: package.mainClass - - ``` - 只要给出入口类`Main-Class`就可以通过`java -jar file.jar`来执行了。 -- 使用Eclipse导出`jar`包: - - 包资源管理器中选择包右键导出->Java->JAR文件,选择要导出的一个或多个包,填写入口类,即可导出。 - - 不设置其他选项的话,导出的清单文件中也就只有版本和入口类的信息。 - - 当然还可以导出其他文件,清单文件也可以有很多其他配置内容,尚不清楚,有需求再了解。 -- 命令行创建jar包命令:更多选项查看`jar -h`帮助,首先`cd`到`.class`文件根目录。 - ```shell - jar -c --file target.jar --main-class YourMainClass .\package\*.class - ``` -- 到这里只能说,Java的确很方便。无论是项目配置,编译,执行,依赖配置,打包发布都如此简单方便。怪不得是时下最流行的编程语言。 -- **你永远可以通过增加一个中间层来解决一些问题**。永远可以通过减少一个中间层来提升一些性能。 -- 现在这个时代,硬件性能已经普遍强大到绝大部分情况下我们并不需要去抠一个程序是到底是多占了几个字节的内存还是多执行了几条指令。愉快地开始java之旅吧! -- 最后,JVM是世界上最好的虚拟机! - - -### 2.13 模块 - -- `.class`是JVM看到的最小执行文件,`jar`包就是`.class`的容器。但写一个大型程序时是可能需要依赖其他第三方的jar包的。最后执行时就需要将所有jar放在一起来执行,少了或者写漏了某个jar就可能会抛出`ClassNotFoundException`。 - ```shell - java -cp 1.jar;2.jar;...;last.jar package.mainClass - ``` -- 引入了模块解决**依赖**的问题。如果`a.jar`依赖`b.jar`,那我们应该给`a.jar`加点东西说明这个信息。让程序编译运行时自动定位到`b.jar`,这种自带依赖关系的`class`容器就是模块。始于Java 9。 -- 创建模块:与创建Java项目一致,在`src/`目录一级下创建`module-info.java`文件,即是**模块描述文件**。文件内容类似与下面这样:使用`module`和`requires`说明模块和依赖。 -```java -module hello.world { - requires java.base; // 可不写,任何模块都会自动引入java.base - requires java.xml; -} -``` -- `module-info.java`经过编译后会在`bin`下生成`module-info.class`。 -- 下一步把`bin`目录所有`class`文件打包成`jar`。使用`jar`命令。 -- 模块还可以导入导出。使用`jmod`命令从`jar`生成模块。 -- 模块要能够访问另一个模块的类,除了访问限定符支持,还需要在目标模块导出外部能访问的类: -```java -module Hello.World { - exports hello.world; - ... -} -``` -- 在包之外,模块又进一步隔离了代码的访问权限。 -- 更详细的理解和说明:TODO。 - -## 3. Java核心类 - -### 3.1 字符串与编码 - -- `String`是一个引用类型,本身也是一个class,Java编译器对`String`有特殊处理,可以直接用字符串字面值`"string-literal"`来表示,每一个字符串字面值底层都被实现为一个`String`实例。 -- 实际上字符串内部是通过字符数组来表示,这点很多编程语言应该都是一样的。 -- Java字符串的重要特性就是不可变,内部保存字符串的字段是`private final`的字符数组,赋值后即**不可变**。`String`类中没有实现任何修改这个数组的方法。使用Eclipse的话F3到定义里面可以看到其实是一个字节数组`private final byte[] value;`,并不是`char[]`。 -- 用字符串字面值创建就相当于使用字符数组创建。java的语法层面支持使我们可以简写。 -```java -String s = "yes"; -String s2 = new String(new char[] {'y', 'e', 's'}); -``` -- 对字符串判等应该用`equals`方法,如果使用`==`则是判断两个字符串引用变量是否引用同一个对象。 -- 忽略大小写判等:`equalsIgnoreCase` -- 搜索提取子串的接口:`idnexOf` `lastIndexOf` `startsWith` `endsWith` `substring` -- 下标从0开始,遗憾的是不支持像数组一样使用`[]`来引用字符串中的字符。可能是因为不允许改变的原因。还是说因为内建的字符类型不支持引用,就算获取了也无法设置,没有理由提供语法层面支持。获取某一个字符使用`public char charAt(int index)`。 -- 修改字符串的所有操作都不改变原字符串内容,而是返回新字符串。 -- 去除收尾空字符:`trim()`,返回去除后的新字符串。包括`\t` `\t` `\n` `\0`。 -- `stricp()`也是去除首位空字符,在`trim()`基础上还会去除像`\u3000`中文空格这样的字符。只移除首或尾:`stricpLeading` `stripTrailing` -- 判空:`isEmpty` -- 判断是否是空白:`isBlank` -- 替换:`replace` -- 分割:`split` -- 拼接:`join` -- 格式化:`formatted` `format` -- 将任意类型转换为`String`:`valueOf` -- 转换为`char[]`:`toCharArray` -- 字符编码: - - java的`String`和`char`在内存中总是用UniCode表示。 - - 可以调用`String`的方法手动将字符串转换为其他编码,结果为`byte[]` - ```java - String s = "你好,世界!"; - printBytes(s.getBytes()); // 系统默认编码,最好不要这么写 - printBytes(s.getBytes(StandardCharsets.UTF_8)); - printBytes(s.getBytes(StandardCharsets.UTF_16)); - printBytes(s.getBytes(StandardCharsets.UTF_16BE)); - printBytes(s.getBytes(StandardCharsets.UTF_16LE)); - ``` -- 进入到`String`的声明里面可以看到内部是怎么存储一个字符串的,早期可能会直接使用`char`数组,但那样的话对于只有ASCII字符构成的字符串内存空间明显不够友好,现在都是用字节数组并且内部有区分编码,但可以发现都是`final`修饰的,也就是赋值之后即不可变。而我们在外部不需要关心`String`内是怎么存储的。 -```java -public final class String - implements java.io.Serializable, Comparable, CharSequence, - Constable, ConstantDesc { - private final byte[] value; - private final byte coder; // 0 - LATIN1(即ISO-8859-1,单字节,向下兼容ASCII), 1 - UTF16 - // ... -} -``` - -### 3.2 String操作类 - -StringBuilder:频繁编辑字符串,位于`java.lang` -- Java字符串赋值之后即不可变,对字符串的编辑的操作都返回一个新的字符串,指向新的内存,如果需要对字符串频繁编辑,那么频繁构造也就会频繁分配内存,也会影响GC效率。所以Java标准库提供了`StringBuilder`,它是一个可变的对象,编辑操作改变自身,而不是新构造对象。 -- 看看它都有些什么方法:`compare` `append` `delete` `replace` `insert` `indexOf` `lastIndexOf` `resverse` `toString` 都是一些字符串应该有的操作,都有不同参数的重载,操作完之后使用`toString`得到字符串。 -```java -StringBuilder sb = new StringBuilder(2048); -sb.append(100).append(',').append(" your princess!"); -sb.insert(3, "%"); -sb.replace(0, 3, "99"); -sb.delete(sb.length()-1, sb.length()); -System.out.println(sb.toString()); -``` -- 总感觉参数的含义怪怪的,不是很好用的感觉。 -- 因为编辑操作修改自己,并返回`this`,所以可以连起来调用。 - -StringJoiner:用来高效拼接字符串,位于`java.util` -- 能用的方法不多: -```java -public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) -public StringJoiner setEmptyValue(CharSequence emptyValue) // 为空时的默认值 -public String toString() // 转字符串 -public StringJoiner add(CharSequence newElement) // 添加 -public int length() // 长度 -public StringJoiner merge(StringJoiner other) // 合并 -``` -- 这也能单独实现为一个类是我没有想到的。 - -### 3.3 包装类型 - -- 简单来说就是把基本类型如`boolean` `byte` `short` `int` `long` `float` `double` `char`等类型变为**引用**的手段。 -- 为`int`定义包装类:类似于这样包装一层之后就可以将其当做对象来用。 -```java -class Integer { - private final int value; - public Integer(int value) { - this.value = value; - } - public int intValue() { - return value; - } -} -``` -- java核心库`java.lang`为每种基本类型都定义了包装类型,分别为 `Boolean` `Byte` `Short` `Integer` `Long` `Float` `Double` `Character` -- 可以`new`包装对象传数值对象来创建,但会提示从java 9开始就弃用了,会有Warning。正常用应该用类似于`Integer.valueOf(int n)`这种静态方法来创建。 -- 提供了很多操作:与字符串的互相转换,解析,比较,进制转换,该类型的常量边界值,编码等。其中大部分是静态方法,感觉的确还是有点用的。 -- 将`Integer.valueOf()`这种方法成为静态工厂方法,创建新对象时应该优先选择静态工厂方法,而不是`new`运算符。看一下实现可以知道某些情况下会返回缓存的实例而不是`new`的新实例。 -```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -``` -- 包装也成为装箱(Boxing),自动装箱和自动拆箱都是在编译器完成的,装箱拆箱会影响执行效率,且拆箱是可能会`NullPointerException`。 -- 包装类型比较应该使用`equals`,整数和浮点数的包装类型都继承自`Number`。 - -### 3.4 JavaBean - -Java中,很多时候为了封装,都会这样去写一个类: -- 若干`private`字段。 -- 通过`public`方法去读写实例字段。 - -如果读写方法符合以下的命名规范,那么这种类被称为`JavaBean`,其中的字段是`xyz`。 -```java -// 字段 -private Type xyz; -// 读方法getter -public Type getXyz() -// 写方法setter -public void setXyz(Type value) -``` -通常将读方法(`getter`)和写方法(`setter`)称之为属性(`property`)。 -- 只写`getter`的属性称为只读(read-only)属性,比较常见。 -- 只有`setter`的属性称为只写(write-only)属性,不常见。 -- 属性是一种通用叫法、实践约定,不是语法规定,本质上其实就是一个方法。 - -作用: -- 用来传输数据,把一组数据组合成一个`JavaBean`来传输。 -- 方便被IDE分析,直接生成读写属性的接口。就不需要自己来慢慢写了。Eclipse中,右键->源码->生成getter和setter,选择要生成属性的字段和读写接口,访问修饰符等就可以直接生成了。 - -使用`java.beans`提供的`Introspector`可以枚举出一个`JavaBean`的所有属性。 -```java -public static void main(String[] args) throws IntrospectionException { - BeanInfo info = Introspector.getBeanInfo(Person.class); - for (PropertyDescriptor pd : info.getPropertyDescriptors()) { - System.out.println(pd.getName()); - System.out.println("\t" + pd.getReadMethod()); - System.out.println("\t" + pd.getWriteMethod()); - } -} -``` -因为可能抛异常所以最后声明中必须加上`throws`语句,不然会报错,异常暂时没有了解,后续详解。其中的`class`字段是继承自`Object`而来。得到的结果中对于`class`字段有一个只读属性:`public final native java.lang.Class java.lang.Object.getClass()`。 - -`Introspector`称之为**内省**,使用方法一般是: ->内省(IntroSpector)是Java 语言针对 Bean 类属性、事件的一种缺省处理方法。一般的做法是通过类 `Introspector` 的 `getBeanInfo` 方法来获取某个对象的 `BeanInfo` 信息,然后通过 `BeanInfo` 来获取属性的描述器(`PropertyDescriptor`),通过这个属性描述器就可以获取某个属性对应的 getter/setter 方法,然后我们就可以通过反射机制来调用这些方法,这就是内省机制。 - -当然现在还未接触**反射**,后续详解。 - - -### 3.5 枚举类 - -要用常量时,java暂时没有`const`(预留关键字,但还没有使用),定义局部常量的话使用`final`即可。定义全局常量的话定义为`static final`就行,使用时`ClassName.staticFinalVar`这样来用就行。某些时候要做判断时,无法通过编译器来检查值的合理性,当然这时候枚举就必须要有了。 - -枚举类定义: -```java -enum WorkingState { - coding, testing, takeABreak, slacking, drinkingCoffee, watchingVideos, playingGame, snapping -} -``` -- 比较枚举值是编译器会进行类型检查,确保了枚举值的有效性。不同类型的枚举不能互相比较或者赋值。 -- `enum`是引用类型,比较值时应该使用`equals`方法,但是由于`enum`类型的所有常量在JVM中只有一个实例,所以使用`==`比较也不会有问题。 -```java -if (state == WorkingState.coding) { - state = WorkingState.takeABreak; -} -``` - -`enum`和`class`有什么区别呢,答案是本质上没有区别,只是具有几个特点: -- 定义的`enum`总是从`java.lang.Enum`派生,且无法比继承。 -- 只能定义`enum`的实例,而无法通过`new`运算符创建实例。 -- 定义的每个实例都是引用的该枚举类型的唯一实例。 -- 可以用于`switch`语句。 - -编译后得到的`class`类似于这样: -```java -public final class WorkingState extends Enum { // 继承自Enum,标记为final class - // 每个实例均为全局唯一: - public static final Color coding = new WorkingState(); - public static final Color testing = new WorkingState(); - public static final Color takeABreak = new WorkingState(); - // ... - // private构造方法,确保外部无法调用new操作符: - private WorkingState() {} -} -``` - -每个枚举的值都是`class`的一个实例,所以可以使用一些从`Enum`继承而来的方法: -```java -WorkingState state = WorkingState.coding; -String s = state.name(); // 获取枚举名称:coding -int order = state.ordinal(); // 获取枚举常量定义的顺序(从0开始):0 -``` - -可以为枚举定义自己的构造,字段和方法。例如自己定义枚举的值:因为`enum`就是`class`,所以定义`enum`常量时可以调用自己的构造。要加字段建议声明为`final`因为你也没办法`new`一个`enum`去改它。 -```java -enum Weekday { - MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0); - public final int dayValue; - private Weekday(int dayValue) { - this.dayValue = dayValue; - } -} -``` -默认情况下`toString`方法会返回和`name`一样的值(也就是是这个枚举常量的名称),但是是可以重写实现自己的`toString`的,目的一般在于使输出更具可读性。所以如果要判断枚举常量的名字,应该始终使用`name`。 - - -### 3.6 记录类 - -不变类: -- 定义时使用`final`,无法派生子类。 -- 每个字段使用`final`,保证创建实例后无法修改任何字段。 -- 为了保证不变类比较,还需要重写`equals`和`hashCode`方法。这样才能在集合类中使用。 - -这样写起来很繁琐,所以语法糖又来了。java14开始,引入了`Record`类,使用关键字`record`: -```java -public record Point(int x, int y) {} -``` -上述定义改写为`class`后类似于: -```java -public final class Point extends Record { - private final int x; - private final int y; - - public Point(int x, int y) { - this.x = x; - this.y = y; - } - - public int x() { - return this.x; - } - - public int y() { - return this.y; - } - - public String toString() { - return String.format("Point[x=%s, y=%s]", x, y); - } - - public boolean equals(Object o) { - ... - } - public int hashCode() { - ... - } -} -``` -啊这,java的繁琐真是不无道理呢! - -和`Enum`类似,不能人为的从`Record`派生,只需要使用`record`关键字由编译器来处理就行。 -- 简单来说,`record`就是为了定义纯数据载体类。 -- 可以添加自己的静态方法。 -- 可以给一个Compact Constructor添加逻辑。 -- 一种典型静态方法就是`of`方法,实现静态工厂,用来创建新对象。`Point p = Point.of(1, 2)`这样写还是挺方便的。 -```java -public record Point(int x, int y) { - public Point { // 没有参数列表,赋值的逻辑由编译器负责补全 - if (x < 0 || y < 0) { - throw new IllegalArgumentException(); - } - } - public static Point of() { - return new Point(0, 0); - } - public static Point of(int x, int y) { - return new Point(x, y); - } -} -``` -java15版本中,这好像还是preview feature: -- 打开编译开关:`--source 15 --enable-preview`才能使用,不然会报错。 -- Eclipse中右键项目,属性,Java编译器,勾选`Enable preview features for Java15`才能使用。并且会有警告`You are using a preview language feature that may or may not be supported in a future release`。 - - -### 3.7 BigInteger - -Java提供的原生类型中表示范围最大的整型是64为整型`long`,和其他任何语言中一样,内置类型计算一般来说都可以由CPU指令直接提供支持,基本上一条指令就可以搞定。但是表示范围超过了64位的范围,那么就必须自己造轮子实现高精度整数了,用空间和时间来换表示范围和精度。 - -java提供了高精度大整数实现:`java.math.BigInteger`。其中用`int[]`数组来模拟大整数,做算术运算时需要通过调用其方法来实现,编译器没有提供运算符支持。这时候就不得不提一句C++的运算符重载了。 - -定义: -- `public class BigInteger extends Number implements Comparable` -- 继承自`Number` -- 不可变对象。 - -方法: -- 构造:任意进制`public BigInteger(String val, int radix)`,十进制`public BigInteger(String val)`,字节数组`public BigInteger(byte[] val, int off, int len)`,静态工厂`public static BigInteger valueOf(long val)`,等。 -- 比较操作 -```java -public int compareTo(BigInteger val) // return -1, 0, 1 - less, equal, greater -public boolean equals(Object x) -public BigInteger min(BigInteger val) -public BigInteger max(BigInteger val) -``` -- 算术运算:尚未列完全,该有的都有了,加减乘除、相反数、取余求模、开方乘方、绝对值、位运算。 -```java - public BigInteger add(BigInteger val) - public BigInteger subtract(BigInteger val) - public BigInteger multiply(BigInteger val) - public BigInteger divide(BigInteger val) - public BigInteger[] divideAndRemainder(BigInteger val) - public BigInteger remainder(BigInteger val) - public BigInteger pow(int exponent) - public BigInteger sqrt() - public BigInteger gcd(BigInteger val) - public BigInteger abs() - public BigInteger negate() - public int signum() - public BigInteger mod(BigInteger m) - public BigInteger shiftLeft(int n) - public BigInteger shiftRight(int n) - public BigInteger and(BigInteger val) - public BigInteger or(BigInteger val) - public BigInteger xor(BigInteger val) - public BigInteger not() -``` -- 转换为算术类型: -```java -// 超过表示范围将丢失高位信息 -public int intValue() -public long longValue() -public float floatValue() -public double doubleValue() -// 超过表示范围抛异常 -public long longValueExact() -public int intValueExact() -public short shortValueExact() -public byte byteValueExact() -``` -- 其他转换 -```java -public byte[] toByteArray() -public String toString(int radix) -public String toString() -``` - -### 3.8 BigDecimal - -对应于高精度整数,必然也应该有高精度浮点数。`java.math.BigDecimal`可以表示一个任意大小且精度完全准确的浮点数。 - -定义: -- `public class BigDecimal extends Number implements Comparable` -- 继承自`Number` -- 不可变对象。 - -方法: -- 构造和算术运算都和`BigInteger`差不多就不列除了。区别是都有包含指定保留小数位数与舍入规则的参数的重载版本。 -- `public int scale()` 用来表示小数位数。如果小数点后只有0那么会返回负值。 -- `public BigDecimal setScale(int newScale)`设置小数位数,精度会丢失时按指定方法舍入或截断。 -- `public BigDecimal stripTrailingZeros()`去除末尾0。 - -比较: -- `equals`会比较scale,如果值相等scale不等,则也会不等。 -- 忽略scale比较:`public int compareTo(BigInteger val)`,返回值-1,0,1。一般来说比较应该用`compareTo`而不是`equals`。 - -实现:通过一个表示每一位值的大整数,和一个表示小数位数的`scale`实现。 -```java -public class BigDecimal extends Number implements Comparable { - private final BigInteger intVal; - private final int scale; -} -``` - -### 3.9 常用工具类 - -**数学计算**: - -- `java.lang.Math`类提供了大量静态方法来执行常见的数学运算,比如指数、对数、开方、三角函数、取整舍入、绝对值、最大值,还定义了常见的常数(`e` `PI`)。 - -- 很多时候它的实现是直接调用`java.lang.StrictMath`,他们的关系是: -在`Math`类中,为了达到最快的性能,所有的方法都使用计算机浮点单元中的例程。如果得到一个完全可预测的结果比运行速度更重要的话,就应该使用`StrictMath`类。它使用“Freely Distributable Math Library”实现算法,以确保在所有平台上得到相同的结果。有关这些算法的源代码请参阅 [fdlibm](http://www.netlib.org/fdlibm/index.html),都是常用数学函数的C实现。 -- 简单来说就是因为存在浮点计算误差,不同平台(如x86和ARM)计算结果不能不一样(指误差不同),`StrictMath`保证所有平台计算结果一致,而`Math`会针对平台优化计算速度。大部分情况使用`Math`就足够了。 -- `java.lang.*`是默认导入的,但因为是静态方法,所以还是要在调用时加上`Math.`。如果不想要在调用时加上`Math.`,那么如前所述导入`Math`类的所有公有静态字段和方法即可:`import static java.lang.Math.*;` - -**伪随机数**: - -- `java.util.Random`包可用于生成伪随机数:所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。 -- 要生成一个随机数,可以使用`nextInt()`、`nextLong()`、`nextFloat()`、`nextDouble()`。 -- 都是非静态方法,要生成随机数需要构造一个`Random`实例。 -```java -Random r = new Random(); -System.out.println(r.nextInt()); -System.out.println(r.nextDouble()); -System.out.println(r.nextLong()); -``` -- 可以使用一个随机数种子作为参数构造`Random`对象,不给种子的话就会采用系统时间戳作为种子。也可以通过`public synchronized void setSeed(long seed)`设置种子。 -- 可以生成的随机数类型包括:`int` `long` `float` `double` -- `Math.random`其实也是调用了内部的`Random`实现的。 - -**真随机数**: - -有**伪随机数**,就有**真随机数**。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,`SecureRandom`就是用来创建安全的随机数的 -```java -SecureRandom sr = new SecureRandom(); -System.out.println(sr.nextInt(100)); -``` -- `SecureRandom`无法指定种子,使用RNG(random number generator)算法。JDK的`SecureRandom`实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成。 -```java -public class Main { - public static void main(String[] args) { - SecureRandom sr = null; - try { - sr = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { - sr = new SecureRandom(); - } - byte[] randomBuffer = new byte[64]; - sr.nextBytes(randomBuffer); - System.out.println(Arrays.toString(randomBuffer)); - } -} -``` ->`SecureRandom`的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用`SecureRandom`来产生安全的随机数。 -- 需要使用安全随机数的时候,必须使用`SecureRandom`,绝不能使用`Random`! -- 这里要提一句java的异常处理感觉很舒服,处理成本不高,如果调用了一个可能抛出异常的函数,那么就一定需要处理这个异常,可以向上抛出,也可以就地处理。并且IDE会给提示,不处理是不能编过的。而对于C++,抛异常要考虑的事情就多了,资源和内存的释放,接到了异常之后能否恢复正确的上下文继续执行?太多需要考虑的事情,而对于内存用光、下标越界这种事也不会去考虑,让其崩溃然后修BUG也许是更好的选择。就我个人在实践中C++的异常处理用的不算多,更多的实践是返回一个错误码。 - -### 3.10 BigInteger实现分析 - -插播一小节闲话,分析一下`BigInteger`的实现: - - - -`BigInteger`源码分析: -- 包声明和导入:主要是异常、IO、数组、随机数、数学、以一些不知道的东西。 -```java -package java.math; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamField; -import java.util.Arrays; -import java.util.Objects; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import jdk.internal.math.DoubleConsts; -import jdk.internal.math.FloatConsts; -import jdk.internal.HotSpotIntrinsicCandidate; -import jdk.internal.vm.annotation.Stable; -import jdk.internal.vm.annotation.ForceInline; -``` - -- 声明: -```java -public class BigInteger extends Number implements Comparable -``` -- 基类`Number`:抽象类,可以和java原始类型`byte` `short` `int` `long` `float` `double` 互相转换的类型的基类。派生中应该实现这些转换方法。 -```java -package java.lang; -public abstract class Number implements java.io.Serializable { - public Number() {super();} - public abstract int intValue(); - public abstract long longValue(); - public abstract float floatValue(); - public abstract double doubleValue(); - public byte byteValue() { - return (byte)intValue(); - } - public short shortValue() { - return (short)intValue(); - } - @java.io.Serial - private static final long serialVersionUID = -8742448824652078965L; -} -``` -- 字段、常量 -```java -final int signum; // 表示符号,取值-1,0,1,表示负数,0,正数 -final int[] mag; // 表示值,大端序存储,0元素表最大量级 -private int bitCountPlusOne; // 比特位之和+1 -private int bitLengthPlusOne; // 比特位长度+1 -private int lowestSetBitPlusTwo; -private int firstNonzeroIntNumPlusTwo; -``` - -尚未开始分析好吧,TODO。 - -## 4. 异常处理与日志 - -### 4.1 Java异常 - -为什么要有异常? -- 错误不可避免:未获取到资源,用户错误操作,程序有BUG,随机错误等。 -- 需要处理错误,使用错误码返回值来标识出现的错误处理太麻烦。 -- Java语言层面上提供异常处理机制,用异常来表示错误。 - -**异常**是一种类,本身带有类型信息。可以在任何地方抛出,只需要在上层捕获,和方法调用分离,不需要返回一个错误码来标识错误。 - -使用`try-catch`块来捕获与处理异常: -```java -try { - // dosomething - // ok -} catch (FileNotFoundException e) { - // file not found: -} catch (SecurityException e) { - // no read permission: -} catch (IOException e) { - // io error: -} catch (Exception e) { - // other error: -} -``` - -java的异常是类,派生关系如图如下: -``` - ┌───────────┐ - │ Object │ - └───────────┘ - ▲ - │ - ┌───────────┐ - │ Throwable │ - └───────────┘ - ▲ - ┌─────────┴─────────┐ - │ │ - ┌───────────┐ ┌───────────┐ - │ Error │ │ Exception │ - └───────────┘ └───────────┘ - ▲ ▲ - ┌───────┘ ┌────┴──────────┐ - │ │ │ -┌─────────────────┐ ┌─────────────────┐ ┌───────────┐ -│OutOfMemoryError │... │RuntimeException │ │IOException│... -└─────────────────┘ └─────────────────┘ └───────────┘ - ▲ - ┌───────────┴──────────────┐ - │ │ - ┌─────────────────────┐ ┌─────────────────────────┐ - │NullPointerException │ │IllegalArgumentException │... - └─────────────────────┘ └─────────────────────────┘ -``` - -`Throwable`有两个子类:`Error`和`Exception`,`Error`表示较为严重的错误,程序一般无法处理,比如: -- `OutOfMemoryError` 内存用尽 -- `NoClassDefFoundError` 类定义未找到 -- `StackOverflowError` 栈溢出 - -而`Exception`是运行时的错误,可以被捕捉并处理。某些异常是程序处理的一部分,比如: -- `NumberFormatException` 数值类型的格式错误 -- `FileNotFoundException` 未找到文件 -- `SocketException` 读取网络失败 - -某些异常是错误的程序逻辑导致的,应该修复程序。比如: -- `NullPointerException` 对某个`null`的对象调用方法或字段 -- `IndexOutOfBoundsException` 数组索引越界 - -Java规定: -- 必须捕获的异常,包括`Exception`及其子类,但不包括`RuntimException`及其子类。这种类型的异常称为Checked Exception。必须捕捉也就是说编译器会强制调用方对异常进行处理,不然会直接编译报错。 -- 不需要捕获的异常,包括`Error`及其子类,`RuntimeException`及其子类。也成Unchecked Excetion。 - -当然编译器并不强制要求程序捕获`RuntimeException`,但是否捕获应该视程序逻辑而定,具体情况具体分析。 - -- 捕捉异常:使用`try-catch`块。 -- 抛出异常:`throws`语句,比如`throws new XXException(args);` 异常也是一个对象,也需要`new`。 - -如果是Checked Exception,但是没有用`try-catch`捕捉,那么必须在方法定义时,使用`throw XXXException`表明该函数可能抛出某种异常,由上层调用者来处理。 -```java -static byte[] stringToGBK(String s) throws UnsupportedEncodingException{ - return s.getBytes("GBK"); -} -``` - -此时就表明`stringToGBK`可能抛出Checked Exception,调用者就必须处理:`try-catch`或向上抛出,当然也可以使用`try-catch`捕获到异常后再向上抛出。 - -从编译器层面保证了Checked Exception必定能够得到处理,当然有助于保证程序的健壮性,但同时也使程序变得啰嗦起来。 - -当然只捕获不处理也是行得通的,但那感觉就像把异常抛弃了一样的脱裤子放屁的作弊行为,并不值得推荐,至少应该将其记录下来,让人知道程序运行出现了什么问题,而不是发现了问题但包着拒不报告增加调试和纠错难度。程序并不只是为了运行而编写,程序首先是给人看让人理解的,其次才是给机器执行的。 - -打印异常栈:`Throwable.printStackTrace()`方法。 - -### 4.2 捕获异常 - -使用`try-catch`语句来捕获异常,可以使用多个`catch`,每个`catch`分别捕获一个`Exception`的子类。捕获异常时从上到下匹配,匹配到后便不在继续,类似于`if - else if - else`逻辑。最后只能由一个`catch`语句能够被执行。 - -所以说`catch`的顺序很重要,如果同时捕获的多个异常类具有派生关系,那么为了确保子类异常能够被捕捉到,就必须将其放在父类异常前面。 - -如果无论是否有一场发生,都希望能够执行一些语句,做一些清理工作,那么应该放到`finally`中。 -```java -try { - // do try things -} catch (XXException e) { - // do XXexcetion things -} catch (Exception e) { - // do exception things -} finally { - // do finally things -} -``` - -关于`finally`: -- 可写可不写,可选。 -- `finally`总是最后执行。 -- 如果有异常被捕获,那么执行对应的`catch`,然后执行`finally`,如果没有异常被捕获,那么`try`语句执行完后直接执行`finally`。 -- 比较反直觉的一点是,如果在`catch`里面`return`了,同样会执行`fianlly`。并且如果`return`返回了一个表达式,那么会先计算这个表达式的值,然后再去执行`finally`中处理,然后返回原先计算得到的值,返回值是确定的,不会因为`finally`中有可能造成表达式值修改的处理而改变了返回的值。搞清楚这点即可。 -- 某些情况也可以没有`catch`,直接使用`try ... finally`保证能够有一些抛出异常时同样能够得到执行的语句,并将异常继续往上抛。 - -甚至还可以合并多个类型的异常捕获: -```java -try { - // do try things -} catch (XXXException | YYYException e) { - // do XXexcetion or YYYException things -} catch (Exception e) { - // do exception things -} finally { - // do finally things -} -``` -如果他们做的事情很类似的话,可以合并,否则感觉也没有多大必要。那么问题来了,合并处理的时候,异常对象`e`的类型是运行时才确定的吗? - - -### 4.3 抛出异常 - -异常的传播: -- 如果抛出的异常在调用层没有被捕获,那么异常会一致沿着调用者向上抛。直到被某一层的`try ... catch`捕获到。 -- `try ... catch`可以嵌套,可以在`try`语句块里面再去`try ... catch`,如果没有里层`catch`没有捕获到,会被抛到外层来由外层的`catch`尝试捕获。 -- 可以通过基类的`Throwable.printStackTrace()`方法来打印出该异常传播的调用栈。从最底层抛出的那一层直到调用的最顶层。对于调试错误很有帮助,给出了源代码行号,可以直接定位。 -```java -java.io.UnsupportedEncodingException: unknown - at java.base/java.lang.StringCoding.encode(StringCoding.java:440) - at java.base/java.lang.String.getBytes(String.java:960) - at Main.Main.stringToEncode(Main.java:41) - at Main.Main.test(Main.java:22) - at Main.Main.main(Main.java:13) -``` -- 如果在某一层捕获了异常,但重新`new`了一个新的异常像上抛出,那么这个新的异常打印调用堆栈时就会丢失原始异常的信息。为了能够追踪原始的异常栈,可以把捕获到的异常作为参数,构造新的异常。 -- 作为参数传入的用来构造异常的原始异常会被保存在`Throwable.cause`字段中,通过`Throwable.getCause()`方法获取到。 -- 捕获到异常后,一定要**保留住原始异常**,以便定位最终的抛出位置。打印时你能够显示出原始异常信息。 -```java -java.lang.IllegalArgumentException: java.lang.NumberFormatException: For input string: "abc" - at Main.Main.test(Main.java:29) - at Main.Main.main(Main.java:13) -Caused by: java.lang.NumberFormatException: For input string: "abc" - at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException. java:68) - at java.base/java.lang.Integer.parseInt(Integer.java:652) - at java.base/java.lang.Integer.parseInt(Integer.java:770) - at Main.Main.test(Main.java:24) - ... 1 more -``` -- 在`catch`中抛出异常,同`return`一样不会影响`finally`的执行,JVM会执行`finally`再抛出异常。那如果在`finally`抛出异常呢?那么既然是先执行`finally`自然是先抛出`finally`中的异常,`catch`中要跑出的异常就被屏蔽了。 -- 像上述描述那样如果有多个类型的异常需要抛出,但是由于只能抛出一个,怎么办呢?如果我们需要捕获所有的异常的话,方法是先用`origin`变量保存原始异常,然后调用`Throwable.addSuppressed()`,把原始异常添加进来,最后在`finally`抛出 -```java -static int test() throws Exception { - Exception origin = null; - try { - System.out.println(Integer.parseInt("abc")); - } catch (Exception e) { - origin = e; - throw e; - } finally { - Exception e = new IllegalArgumentException(); - if (origin != null) { - e.addSuppressed(origin); - } - throw e; - } -} -``` -最终打印出的异常信息会是这个样子的:两个异常的信息都会得到保留。感觉也同样可以将`origin`作为参数用来构造新异常,但表示的含义应该有点区别:导致的关系与并列的关系? -```java -java.lang.IllegalArgumentException - at Main.Main.test(Main.java:29) - at Main.Main.main(Main.java:13) - Suppressed: java.lang.NumberFormatException: For input string: "abc" - at java.base/java.lang.NumberFormatException.forInputString (NumberFormatException.java:68) - at java.base/java.lang.Integer.parseInt(Integer.java:652) - at java.base/java.lang.Integer.parseInt(Integer.java:770) - at Main.Main.test(Main.java:24) - ... 1 more -``` -通过`Throwable.getSuppressed()`可以获取到所有的Suppressed Exception,结果是一个`Throwable []`。绝大多数情况下,`finally`中不需要抛出异常,通常也不需要关心Suppressed Exception,但需要知道可以这么用。 - -### 4.4 自定义异常 - -Java标准库常用异常: -``` -Exception -│ -├─ RuntimeException -│ │ -│ ├─ NullPointerException -│ │ -│ ├─ IndexOutOfBoundsException -│ │ -│ ├─ SecurityException -│ │ -│ └─ IllegalArgumentException -│ │ -│ └─ NumberFormatException -│ -├─ IOException -│ │ -│ ├─ UnsupportedCharsetException -│ │ -│ ├─ FileNotFoundException -│ │ -│ └─ SocketException -│ -├─ ParseException -│ -├─ GeneralSecurityException -│ -├─ SQLException -│ -└─ TimeoutException -``` -当我们要抛异常时,尽量使用标准库异常,然而在一个大型项目中,必然需要定义自己的异常。这是,保持一个合理的异常继承体系非常重要。 - -常见做法是定义一个根异常,然后所有异常类从其派生,实现的话可以参照标准库`RuntimeException`: -```java -package java.lang; -public class RuntimeException extends Exception { - public RuntimeException() { - super(); - } - public RuntimeException(String message) { - super(message); - } - public RuntimeException(String message, Throwable cause) { - super(message, cause); - } - public RuntimeException(Throwable cause) { - super(cause); - } - protected RuntimeException(String message, Throwable cause, - boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} -``` -应该要提供多种构造方法,好像也就构造了,其他具体问题具体看。 - -### 4.5 NullPointerException - -派生于`RuntimeException`,异常类的定义都出人意料的简单。 - -java语法层面没有指针的概念,指针当然源自于C,指针让使用者看见了地址,提供给使用去操作地址的手段,引用类型只是隐藏了内存地址让其对使用者不可见,但思想是完全相同的,并且使用起来更简单。java中还是能看见指针的影子的,比如打印一个引用变量的值就能够看到类似于地址的东西。 - -如果使用了一个值为`null`的引用变量去调用它的非静态字段和方法,就会抛出`NullPointerException`。当然使用空引用操作静态字段或方法就等价于直接用类去使用,和引用的值没有半毛钱关系,是可以的,只是一般来说也不会去这么用。 - -如果遇到了`NullPointerException`,那么正确的处理应该是找到抛出的位置,添加判空处理,修正逻辑错误,而不是将其捕捉后隐藏错误或向上抛。至少在C++中,使用一个指针前先判空是常识性的东西,通过空指针去操作对象会直接导致崩溃。相信任何语言都是一样的,一个可用的程序它必须是健壮的,不应该有空指针异常这样的低级错误。 - -另一些避免该类问题的手段:当可以返回一个空字符串/空数组/其他空值或者返回一个`null`时,构造一个空值返回可以避免一部分这类问题。但不管怎么说,使用一个可能为空的东西前都应该是要判空的,难道不是吗? - -给JVM添加一个`-XX:+ShowCodeDetailsInExceptionMessages`参数启用`NullPointerException`的详细信息输出,说明是谁空掉了。在IDE里面一般是默认开启的。 - -Eclispe给JRE指定参数:`window > preferences > java > Installed JREs > your JRE > edit > default VM arguments`。 - -### 4.6 断言 - -语法:`assert condition : "assertion message";`,其中的断言消息是一个字符串,可选。 - -断言失败,会抛出`AssertionError`,程序结束退出,因此断言不能用于可恢复的程序错误,只应该用在开发和测试阶段。我的理解是断言只应该用在那种基本不会失败,一旦失败程序再执行下去就没有什么意义不如直接退出的地方,以帮助快速定位。 - -那么如何区分测试阶段和正式上线,不让断言影响到用户体验呢?C/C++的方法一般是通过宏定义在DEBUG版本中断言是正常语义,在Release版本中断言不做任何事情。所以断言内部不应该有会修改变量的操作,去掉断言不应该会改变程序逻辑。绝对不应该写出这样的语句:`assert x++ > 0;`,当然这在任何语言中应该都是常识。 - -JVM默认关闭断言,遇到`assert`语句直接就忽略了,要开启断言,需要给JVM传递`-enableassertions`(简写`-ea`)参数。还可以有选择的启用断言,比如参数`-ea:Main.Main`就是对`Main.Main`这个类启用断言,或者某一个函数也可以。 - -实际开发中,很少使用断言,更好的做法是编写**单元测试**。以我有限的C++开发经验来说,在一个复杂的大型程序中,断言除了在调试版本中让你的程序崩溃之外没有任何作用。 - - -### 4.7 使用JDK Logging - -某些时候我们需要完整运行程序,而不是在调试环境下调试程序,但又想知道程序的详细运行状态。此时就需要知道一些中间过程执行情况,中间变量是否正确,这是可以怎么办呢? - -最简单的方式就是通过`System.out.println`打印我们需要输出的变量,但这样比较初级,也不好管理。如果想要更加详细的信息,那么可以实现一个日志系统,在程序中穿插日志的记录,日志记录不会影响也不应该影响程序的正常执行逻辑,只是记录程序的执行状态。并且可以设定输出样式、设置日志分级、重定向到文件等等功能。 - -Java当然考虑到了这些东西,所以提供了内置的日志包`java.util.logging`,不需要我们来自己造轮子。 - -`java.util.logging.Logger`类: -```java -import java.util.logging.Logger; - -LLogger logger = Logger.getGlobal(); -logger.severe("a fatal error occurred..."); -logger.warning("just a warining..."); -logger.info("started..."); -logger.config("config ..."); -logger.fine("just fine..."); -logger.finer("won't crash..."); -logger.finest("work normally..."); -``` -打印信息中包含了时间,调用类和方法,输出信息: -```java -3月 24, 2021 10:49:03 下午 Main.Main main -严重: a fatal error occurred... -3月 24, 2021 10:49:03 下午 Main.Main main -警告: just a warining... -3月 24, 2021 10:49:03 下午 Main.Main main -信息: started... -``` - -JDK的`Logging`定义了7个日志级别,从严重到普通: -- `SEVERE` -- `WARNING` -- `INFO` -- `CONFIG` -- `FINE` -- `FINER` -- `FINEST` - -默认级别是`INFO`,`INFO`及以下的信息不会被打印出来。使用日志级别的好处在于可以调整级别就可以筛选和屏蔽调很多调试相关的日志输出。 - -局限: -- JDK的`Logging`系统在JVM启动时读取配置文件完成初始化,一旦开始运行`main`方法,就无法修改配置。 -- 配置不太方便,需要在JVM启动时传递参数`-Djava.util.logging.config.file=`以重定向到文件。 - -好处: -- 可以存档以追踪问题,将一次程序的运行状况记录下来分析。 -- 可以按级别分类,方别打开或关闭某些级别。 -- 可以根据配置文件调整日志,无需修改代码。 - -TODO:使用参数`-Djava.util.logging.config.file=`重定向到文件无论是IDE还是命令行都可以执行,但都没有成功输出到文件,更多细节待以后有需要来补充。 - -### 4.8 Commons Logging - -Commons Logging是一个由Apache创建的第三方日志库,可以挂接不同的日志系统,通过配置文件指定挂接的日志系统。默认情况下自动搜索并使用Log4j,如果没有找到就再使用JDK Logging。 - -使用方法: -- 通过`LogFactory`获取`Log`类实例,然后使用`Log`实例来打印日志。 - -```java -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -public class Main { - public static void main(String[] args) { - Log log = LogFactory.getLog(Main.class); - log.info("start..."); - log.warn("end."); - } -} -``` -当然因为是第三方库,JDK里面是没有的,所以需要下载([下载地址](https://commons.apache.org/proper/commons-logging/download_logging.cgi),最新版本1.2)后添加到`classpath`。 - -命令行编译执行: -- 将`commons-logging-1.2.jar`文件放在`Main.java`同一个目录。 -- 执行`javac -cp commons-logging-1.2.jar Main.java`编译得到`Main.class`。 -- 然后`java -cp .;commons-logging-1.2.jar Main`执行java程序。如果执行`.class`时指定了classpath,那么应该将当前目录也作为classpath,不然会找不到主类`Main`。注意classpth多个路径时Windows中使用`;`分割,Linux和MacOS使用`:`分割,Windows中如果在Powershell中执行,多个路径还要加上双引号包起来。 -- 也可以跳过编译,直接执行java文件,`java -cp commons-logging-1.2.jar Main.java`。 - -Eclipse中导入第三方库见 [0.3 基本Eclipse使用](#03-%E5%9F%BA%E6%9C%ACeclipse%E4%BD%BF%E7%94%A8)。 - -Common Logging定义了6个日志级别,默认级别是`INFO`: -- `FATAL` -- `ERROR` -- `WARN` -- `INFO` -- `DEBUG` -- `TRACE` - -`Log`的使用: -- 如果在静态方法中使用`Log`,通常直接定义了一个静态成员给所有方法共用,使用`LogFactory.getLog(Main.class)`获取`Log`实例,只能在该类中使用。 -- 如果是在实例方法中使用,通常定义一个实例变量,使用`LogFactory.getLog(getClass())`获取实例,这样做的好处时,由于多态的特性子类的`getClass()`返回的是子类的类型,所以子类也可以直接使用该`Log`实例。最好定义为`protected`。 -- `Log`接口(interface)对每种级别的日志都声明了两个重载的方法,接口名称和上面列出的级别一致: -```java -void info(Object message) -void info(Object message, Throwable t) -``` -第二个重载可以传入异常,用在`catch`语句中很方便,结果除了输出`message`之外,还会调用异常的`printStackTrace`输出异常栈。 -```java -public static void main(String[] args) { - try { - throw new RuntimeException(); - } catch (Exception e) { - log.error("exception occureed", e); - } -} -``` - -### 4.9 Log4j - -上面的Commons Logging可以作为“日志接口”来使用,而真正的“日志实现”可以使用Log4j。前面提到Commons Logging默认查找`classpath`下的Log4j来作为日志实现,没有的话则会使用JDK Logging。 - -Log4j是一种非常流行的日志框架,当前最新版本2.14,[下载地址](https://logging.apache.org/log4j/2.x/download.html),同样是Apache的。 - -Log4j是一个组件化的日志系统,架构如下: -``` -log.info("User signed in."); - │ - │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - ├──>│ Appender │───>│ Filter │───>│ Layout │───>│ Console │ - │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ - │ - │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - ├──>│ Appender │───>│ Filter │───>│ Layout │───>│ File │ - │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ - │ - │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - └──>│ Appender │───>│ Filter │───>│ Layout │───>│ Socket │ - └──────────┘ └──────────┘ └──────────┘ └──────────┘ -``` -使用Log4j输出日志时,自动通过不同的Appender把日志输出到不同的目的地。 -- console: 屏幕/控制台 -- file: 文件 -- socket: 通过网络输出到远端计算机 -- jdbc: 输出到数据库 - -输出日志时可以通过Filter过滤哪些日志要输出,哪些不输出。例如仅输出ERROR级别的日志,最后通过Layout来格式化日志信息。例如自动添加日期、时间、方法名称等。 - -实际使用时,并不需要关心Log4j的API,而是通过配置文件来配置它。使用时,将一个`Log4j2.xml`文件放到`classpath`下就可以让Log4j读取配置文件并按照我们想要的输出方式输出日志。例子: -```xml - - - - - %d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n - - log/err.log - log/err.%i.log.gz - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` -虽然配置Log4j比较繁琐,但一旦配置完成,使用起来就非常方便。对上面的配置文件,凡是INFO级别的日志,会自动输出到屏幕,而ERROR级别的日志,不但会输出到屏幕,还会同时输出到文件。并且,一旦日志文件达到指定大小(1MB),Log4j就会自动切割新的日志文件,并最多保留10份。更多配置参见[官方文档](https://logging.apache.org/log4j/2.x/manual/configuration.html)。 - -使用:下载后将下面三个`jar`和配置文件`Log4j2.xml`添加到`classpath`: -- `log4j-api-2.x.jar` -- `log4j-core-2.x.jar` -- `log4j-jcl-2.x.jar` - -将这三个包添加到classpath之后还要保证`Log4j.xml`在classpath根目录(也就是要放到Eclipse工程的`bin/`目录下,或者命令行执行时放到生成`.class`的根目录),这样就可以按照配置文件内容输出到日志文件。 - -总结: -- 通过Commons Logging输出日志,不用修改代码只需进行配置就可以使用Log4j。 -- 使用Log4j只需要将jar和配置文件Log4j2.xml添加到classpth。 -- 更换Log4j,只需要移除jar和Log4j2.xml。 -- 扩展Log4j时才需要使用Log4j的接口,例如自己开发将日志加密写入数据库的功能。 -- 当然其实也可以跳过Commons Logging这一层直接使用Log4j输出日志。 - -### 4.10 SLF4J & Logback - -上面的Commons Logging和Log4j分别扮演日志API和日志实现的角色,搭配使用。同样的库还有SLF4J(API)和Logback(实现)。他们都是开源的第三方库,因为对Commons Logging的接口和Log4j的性能不满意,所以就分别有了[SLF4J](https://www.slf4j.org/download.html)和[Logback](https://logback.qos.ch/download.html)。 - -那么SLF4J相较Commons Logging有什么优势呢? -- 支持`logger.info("{},{}", str1, str2)`这样的字符串格式化。 -- 还有呢? - -事实上SLF4J的日志接口与Commons Logging几乎一波一样,对比: -|Commons Logging|SLF4j| -|:-|:-| -|`org.apache.commons.logging.Log`|`org.slf4j.Logger`| -|`org.apache.commons.logging.LogFactory`|`org.slf4j.LoggerFactory`| - -就是`Log`变成了`Logger`,`LogFactory`变成了`LoggerFactory`。 - -配置SLF4J和Logback,需要下列三个jar包,目前使用的是`SLF4J 1.7.9`和`logback 1.2.3`: -- `slf4j-api-1.7.x.jar` -- `logback-classic-1.2.x.jar` -- `logback-core-1.2.x.jar` - -添加到`classpath`,添加配置文件`logback.xml`到`classpath`根目录: -```xml - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - utf-8 - - log/output.log - - log/output.log.%i - - - 1MB - - - - - - - -``` - -然后使用时替换为对应的类型和接口接口: -```java -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class Main { - public static void main(String[] args) { - Logger logger = LoggerFactory.getLogger(Main.class); - logger.info("start..."); - logger.warn("end."); - } -} -``` - -总结: -- SLF4J和Logback可以取代Commons Logging和Log4j。 -- 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。 - -知道大概用法即可,有需求时再去研究具体配置和细节用法。 - - -## 5. 反射 - -### 5.1 Class类 - -除了`int`等基本类型外,Java的其他类型都是`class`,包括`interface`。如`Runable`、`Object`、`String`、`Exception`等。 - -类这种东西是由Java在执行过程中动态加载的,那么Java是如何实现RTTI的呢?每加载一个类,JVM会为其创建一个`Class`类实例,并将这个类和这个`Class`实例关联起来。这个`Class`类的定义大概是这样的: -```java -public final class Class { - private Class() {} -} -``` - -在C++里面,如果我们要自己实现RTTI,其实也是这样来做的,具体做法大概是: -- 当有新的类被添加到继承体系结构时,新的`Class`对象被创建,并被加到派生关系树的对应位置(叶子节点),通过这个对象我们可以访问到他的所有直接与间接基类(向上遍历)。 -- 每个类中使用一个静态数据成员保存该类对应的`Class`对象(指针),定义静态成员函数以获取到该指针。 -- 每个类中再定义非静态成员函数(`getClass()`)转调上面的静态成员函数,这个非静态成员函数需要是从根类继承来的**虚函数**,在每个类中重写以返回自己的`Class`对象,这是最关键的地方,通过该接口获取到对象真实类型的`Class`对象即可实现运行时类型识别。 -- 通常通过一个机制实现上面3个过程,一般来说就是写一个宏而已。典型的实现可以参考MFC(一个经典/过时的Windows界面库)。 -- 一般来说还会允许用户自己从指定的基类派生实现的类也有这个特性,那么就需要对外公开上一步说的那个机制让用户来用,还需要提供能够使类型动态加载和卸载的机制,也就是动态地从类型树上删除或者添加节点的机制,一般作为静态函数实现在类中,在模块加载和卸载时做即可。 - -TODO:了解反射之后,可以尝试在C++上实现反射。 - -能够预想到,java从语言层面就实现了这个机制,那么就来看一下对应的功能是如何实现的: -- `Class`类构造是`private`的,也就是说只有JVM能够来创建`Class`实例。 -- JVM持有的每一个`Class`对象都对应一个数据类型(`class`或`interface`)。 -- `Class`实例包含了该类的所有信息(`String`为例): - - 名称,`java.lang.String` - - 包,`java.lang` - - 基类,`java.lang.Object` - - 接口,`java.io.Serializable`,`Comparable`,etc. - - 字段,`value[]`,etc. - - 方法,`String()`,`length()`,etc. - -这种通过该`Class`实例获取到类的信息的方法称为**反射(Reflection)**。 - -那么要如何获取到一个类的`Class`对象呢: -- 通过类的静态变量`class`获取:`Class strClass = String.class`。因为`class`还是定义类的关键字,所以这里是Java编译器做了特殊处理,不是简单地在定义在`Object`类中的一个静态字段。在`Object`定义中也是找不到`class`字段的。 -- 通过实例的`getClass()`方法获取,这个方法是定义在万物的基类`Object`中的,因为内置类型不从`Object`派生,所以没有这个方法可以调用。 - ```java - String s = "hello"; - Class strClass = s.getClass(); - ``` -- 如果知道一个`Class`实例的完整类名,可以通过静态方法`Class.forName(String className) throws ClassNotFoundException`来获取: - ```java - try { - Class strCls = Class.forName("java.lang.String"); - } catch (Exception e) { - System.out.println("no such a class: java.lang.String"); - } - ``` - -注意获取类的`Class`实例判等与`isntanceOf`的区别,如果要求是同一个类型,后者可以是基类。 - -`Class`定义: -```java -public final class Class implements java.io.Serializable, - GenericDeclaration, - Type, - AnnotatedElement, - TypeDescriptor.OfField>, - Constable -``` - - -通过`Class`对象获取类的基本信息:获取类名、去除包名后的简化类名、类型名、包、包名、基类、接口、字段、方法、判断是否是各种特殊的类型、通过类名得到对应`Class`对象等,这里并未列全。 -```java -public String getName() -public String getSimpleName() -public String getTypeName() -public String getCanonicalName() -public Package getPackage() // If this class represents an array type, a primitive type or void, this method returns null -public String getPackageName() -public native Class getSuperclass() -public Class[] getInterfaces() -public boolean isAnonymousClass() -public boolean isLocalClass() -public boolean isMemberClass() -public Class[] getClasses() -public Field[] getFields() throws SecurityException -public Method[] getMethods() throws SecurityException -public Constructor[] getConstructors() throws SecurityException -public Field getField(String name) -public Method getMethod(String name, Class... parameterTypes) throws NoSuchMethodException, SecurityException -public Constructor getConstructor(Class... parameterTypes) throws NoSuchMethodException, SecurityException -public boolean isEnum() -public boolean isRecord() -public native boolean isArray(); -public native boolean isInterface(); -public native boolean isPrimitive(); -public boolean isAnnotation() -public boolean isSynthetic() -public static Class forName(String className) throws ClassNotFoundException -public T newInstance() throws InstantiationException, IllegalAccessException -``` - -JVM为每一种基本类型进行了特殊处理创建了`Class`对象,可以直接通过`int.class`这种方式访问。但是对内置类型的变量,因为没有从`Object`派生,所以是不能通过实例的`getClass`方法来获取`Class`对象的。 - -获取到一个类型的`Class`对象之后,就可以用其来创建该类型对象:但是因为没有参数,所有只能调用无参构造函数。 -```java -try { - Class cls = Student.class; - Student s = (Student)cls.newInstance(); - System.out.println(s); -} catch (Exception e) { - System.out.println(e); -} -``` -如果传入的这个`Class`对象对应的类没有无参构造,那么会抛出`java.lang.InstantiationException`,如果无参构造无法访问或者使用的是`Class`类对应的`Class`对象,那么会抛出`java.lang.IllegalAccessException`。其中做了特殊处理,况且`Class`的无参构造是`private`的。`Class`对象只能由JVM在加载了新的类时来创建。 - -JVM并不会在一次性把所有用到的类加载到内存中(即是不会一次性创建所有`Class`对象),需要程序执行过程中用到了一个新的类,才会把这个类加载到内存(也就是创建它的`Class`对象)。这是JVM**动态加载**`class`的特性。 - -因为动态加载特性,就可以用下面的函数来判断一个类是否存在,如果传入的类名在`classpath`中存在那么就会返回`true`,前面提到的Commons Logging判断Log4j是否存在就可以用这样的方法。 -```java -static boolean isClassPresent(String name) { - try { - Class.forName(name); - return true; - } catch (Exception e) { - return false; - } -} -``` - -我比较好奇的一点是`Class`对象是如何创建的,用什么样的数据结构来存储的,JVM会如何管理。当然这个可能需要了解JVM的实现,TODO。 - -### 5.2 访问字段 - -对任意的`Object`,有了它的`Class`对象,就可以获取这个它的一切信息。 -- `public Field getField(String name)` 根据字段名获取某个public的字段(包括父类和接口,找不到的话先按照声明顺序找接口,再找基类) -- `public Field getDeclaredField(String name)` 根据字段名获取当前类的某个字段,包括所有访问权限的字段(不包括父类和接口) -- `public Field[] getFields()` 获取所有public字段,包括所有基类和实现的接口,如果是内置类型或者数组对应的`Class`实例,那么返回空数组 -- `public Field[] getDeclaredFields()` 获取当前类定义的所有访问权限的字段,不包括基类和接口 - -`Field`类型: -- 定义:`public final class Field extends AccessibleObject implements Member` -- `public String getName()` 获取字段名称 -- `public Class getType()` 获取字段类型的`Class`对象 -- `public native int getModifiers();` 获取字段修饰符,不同的bit表示不同含义 - ```java - public static final int PUBLIC = 0x00000001; - public static final int PRIVATE = 0x00000002; - public static final int PROTECTED = 0x00000004; - public static final int STATIC = 0x00000008; - public static final int FINAL = 0x00000010; - public static final int SYNCHRONIZED = 0x00000020; - public static final int VOLATILE = 0x00000040; - public static final int TRANSIENT = 0x00000080; - public static final int NATIVE = 0x00000100; - public static final int INTERFACE = 0x00000200; - public static final int ABSTRACT = 0x00000400; - public static final int STRICT = 0x00000800; - ``` - -获取字段的值:`public Object get(Object obj)`,参数为需要获取字段的对象,返回值被装箱到`Object`对象中,如果是内置类型,会自动包装为对应的包装类型。 - -如果在没有访问该字段权限的地方用了`Field.get`那么可能会抛出`IllegalAccessException`,如果非要访问,可以在前面加上`public void setAccessible(boolean flag)`调用传入`true`确保能够访问。`setAccessible`是在从基类`AccessibleObject`中继承而来的,`Field` `Method` `Constructor`都直接或间接从其派生。 -- 反射是一个非常规用法,使用反射,代码会很繁琐,使用反射会破坏对象的封装。 -- 反射更多提供给工具或底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。 -- `setAccessible(true)`可能会失败,如果JVM运行期存在`SecurityManager`,那么它会根据规则进行检查,有可能阻止`setAccessible(true)`。 - -设置字段值:`public void set(Object obj, Object value)` - -静态实例的话`get/set`的`obj`参数会被忽略,自动为`null`,建议写为`null`,就像调用类的静态方法是最好用类名而不是用实例一样,只为让代码更清晰。 - -值得注意的是,反射相关类型位于`java.lang.reflect`包内,与`java.lang`不是一个包,不会自动导入,需要手动`import`。 - -### 5.3 访问方法 - -类似于访问字段,访问一个类的方法在`Class`类中有如下方法: -- `public Method getMethod(String name, Class... parameterTypes)` public,包括基类 -- `public Method getDeclaredMethod(String name, Class... parameterTypes)` 所有权限,不包括基类和接口 -- `public Method[] getMethods()` public,包括基类 -- `public Method[] getDeclaredMethods()` 所有权限,不包括基类和接口 - -末尾的可变参数需要按顺序传入方法参数列表的`Class`对象,为空就是无参版本。 - -`Method`类型: -- 定义:`public final class Method extends Executable` -- `public String getName()` -- `public int getModifiers()` -- `public int getParameterCount()` -- `public TypeVariable[] getTypeParameters()` -- `public Class getReturnType()` -- `public Class[] getExceptionTypes()` - -见名知意,其中后面三个都还有一个`Generic`的方法,`getGenericParameterTypes` `getGenericReturnType()` `getGenericExceptionTypes()`返回值类型为`Type`,也就是`Class`实现的其中一个接口。这三个方法表示得到参数列表、返回值、异常的正式类型。 - -使用反射调用方法: -- `public Object invoke(Object obj, Object... args)` 第一个是对象实例,后面的是参数列表。 -- 对`Method`实例调用`invoke`方法,就等同与直接使用该对象调用该方法。 -- 调用静态方法的话第一个参数传入`null`即可,会被忽略。 -- 同理,调用非`public`字段需要`setAccessible(true)`。 -- 当然也遵守多态原则。 - - -### 5.4 调用构造方法 - -我们通常用`new`操作符创建新的实例,有了反射也可以通过`Class`对象的方法来创建: -```java -Student s = new Student(); -Student s2 = Student.class.newInstance(); // 调用public无参构造 -``` - -后者只能调用公有的无参构造,为了能够调用到所有构造,Java的反射API提供了`Constructor`对象,包含一个构造方法的所以信息,可以用来创建一个实例,和`Method`很类似,不同之处仅在于它是构造方法,并且总是返回实例。`Class`中用于获取`Constructor`的方法: -```java -public Constructor getConstructor(Class... parameterTypes) throws NoSuchMethodException, SecurityException -public Constructor[] getConstructors() throws SecurityException -public Constructor getDeclaredConstructor(Class... parameterTypes) throws NoSuchMethodException, SecurityException -public Constructor[] getDeclaredConstructors() throws SecurityException -``` -同理前两者获取public的构造,后两者获取所有访问权限的构造,不同于方法和字段的是前两个方法不会获取到基类的构造,因为并不能调用基类的构造方法来构造子类的对象。如果是非静态的Inner class,那么第一个参数还需要额外传入内部类关联的实例,不展开详述。 - -`Constructor`类型: -- 定义:`public final class Constructor extends Executable` -- 方法:设置访问权限、获取定义的类、名称、修饰符、参数列表类型、异常类型、调用构造方法等。 -```java -public void setAccessible(boolean flag) -public Class getDeclaringClass() -public String getName() -public int getModifiers() -public TypeVariable>[] getTypeParameters() -public Class[] getExceptionTypes() -public T newInstance(Object ... initargs) throws - InstantiationException, IllegalAccessException, - IllegalArgumentException, InvocationTargetException -``` - - -### 5.5 获取继承关系 - -获取一个`Class`对象的三种方法: -- `className.class` -- `classInstance.getClass()` -- `Class.forName(classNameString)` - -对同一个类而言,这三种方法获取的都是同一个实例,JVM对每个类只会创建一个`Class`实例。 - -获取父类的`Class`,前面已经有提到,就是`Class`的`getSuperClass`方法,获取接口则使用`getInterfaces`接口: -```java -public native Class getSuperclass(); -public Class[] getInterfaces() -``` - -判断继承关系: -- 如果是一个实例,那么使用`instanceof` - ```java - Double n = Double.valueOf(10.0); - boolean isDouble = n instanceof Double; - ``` -- 如果是`Class`对象,那么使用`isAssignableFrom`,含义是`cls`表示的类的对象是否可以被赋给`this`表示的类的变量,即是传入的`Class`是否是当前`Class`的子类。 - ```java - // declaration - public native boolean isAssignableFrom(Class cls); - // calling - boolean isNumber = Number.class.isAssignableFrom(Integer.class); // true - ``` -- 判断一个对象是否是一个类或者其子类的对象:`isInstance`,等价于使用`instanceof` - ```java - // declaration - public native boolean isInstance(Object obj); - // calling - Integer n = Integer.valueOf(10); - boolean isNumber = Number.class.isInstance(n); // true - ``` - -### 5.6 动态代理 - -Java的`class`和`interface`的区别就是接口可以多继承,接口没有构造,接口不能有类成员,接口不能实例化。当然抽象类也不可以实例化,这样看感觉其实也就多继承和区别而已,因为需要用接口来多继承所以才不能有构造和实例字段。 - -那么能不能不编写实现类,在运行时创建出一个`inteface`实例呢?Java标准库提供了动态代理(Dynamic Proxy)来实现这个事情。 - -所谓的动态是和静态对应的,典型的静态创建即定义类来实现接口,然后实例化类对象并用接口来调用: -```java -interface Hello { - public void morning(); -} -class HelloWorld implements Hello { - public void morning() { - System.out.println("hello, world"); - } -} -public class Test { - public static void Main() { - Hello h = new HelloWorld(); - h.morning(); - } -} -``` - -那么动态创建怎么做呢? -```java -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -interface Hello { - public void morning(); -} - -public class Main { - public static void main(String[] args) throws Exception { - InvocationHandler handler = new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - System.out.println(method); - if (method.getName().equals("morning")) - { - System.out.println("hello,world"); - } - return null; - } - }; - Hello h = (Hello)Proxy.newProxyInstance( - Hello.class.getClassLoader(), - new Class[] {Hello.class}, - handler); - h.morning(); - } -} -``` - -方法如下: -- 定义一个`InvocationHandler`实例,负责实现接口的方法调用,这里的语法是使用匿名类实现了`InvocationHandler`接口的`invoke`方法,并且在其中动态实现了`morning`方法。 -- 通过`Proxy.newProxyInstance()`创建`interface`实例,需要三个参数: - - 使用的`ClassLoader`,通常就是接口的`ClassLoader`,通过接口`Class`实例的`getClassLoader`方法获取。 - - 需要实现的接口数组,至少传入一个接口。 - - 处理调用方法的`InvocationHandler`实例。 -- 将返回的`Object`实例转换为接口。 - -其实实现的方式就是JVM为我们自动编写了一个类(不需要源码直接生成字节码),并不存在可以直接实例化接口的黑魔法。而且因为最终创建的对象是通过接口来用,也就没有办法添加和使用实例字段。暂不清楚使用场景。 - -动态代理是通过`Proxy`创建代理对象,然后将方法代理给`InvocationHandler`完成的。 - -更多理解TODO。 - -## 6. 注解 - -### 6.1 使用注解 - -注解(Annotation)是放在Java源码的类、字段、方法、参数前的一种特殊“注释”。注释会被编译器直接忽略,但注解可以被编译器打包进`class`文件中,因此,注解是一种用作标注的“元数据”。比如重写基类方法时开头添加的`@Override`就是注解。 - -注解的作用: - -从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具(比如一些库自己定义的注解)、你的代码(你自己定义的注解需要编写逻辑来使用它)决定。Java的注解可以分为三类: - -第一类是由编译器使用的注解,如: -- `@Override`:让编译器检查方法是否正确实现了覆写。 -- `@SuppressWarnings`:告诉编译器忽略此处代码产生的警告。 - -类注解不会被编译到`.class`文件中,编译后就被扔掉了。 - -第二类是由工具处理的`.class`文件时使用的注解,比如某些工具加载class时候会对class做动态修改,实现一些特殊的功能。这类注解会被编译到`.class`文件中,但加载结束后不会存在于内存中。这类注解只会被一些底层库使用,一般我们不必处理。例如? - -第三类是程序运行期能够读取的注解,加载后一直存在JVM中,这也是**最常用**的注解。比如一个配置了`@PostConstruct`注解的方法会在调用构造方法后自动被调用,这是我们自己的Java代码读取该注解需要实现的功能,JVM并不认识该注解也不会帮你实现你脑子里的东西。 - -定义一个注解时,还可以定义**配置参数**,配置参数可以包括: -- 所有基本类型 -- `String` -- 枚举 -- 基本类型、`String`、`Class`以及枚举的数组 - -因为配置参数必须是常量,上述的限制保证了注解在定义时就已经确定了每个参数的值,就是说编译期就已经确定了。 - -注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。大部分注解会有一个名为`value`的配置参数,对此参数赋值,可以只写常量,相当于省略了`value`参数。如果只写注解,相当于全部使用默认值。 -```java -public class Hello { - @Check(min=0, max=100, value=55) // 定义了三个参数 - public int n; - - @Check(value=99) // 只定义了一个value参数 - public int p; - - @Check(99) // 等价于@Check(value=99) - public int x; - - @Check // 全部使用默认值 - public int y; -} -``` - -### 6.2 定义注解 - -注解当然不是从虚空中生长出来的,Java语言使用`@interface`语法来定义注解。格式如下: -```java -public @interface Report { - int type() default 0; - String level() default "info"; - String value() default ""; -} -``` -`@interface`定义注解名,注解的参数类似无参数方法,使用`类型 参数名() default 默认值;`这样的语法来定义,可以用`default`设定一个默认值(强烈推荐)。最常用的参数应当命名为`value`(这样使用时就可以不写参数名)。 - -**元注解**:一些可以被用来修饰其他注解的注解(搁着套娃呢?),这些注解就被称为元注解(meta annotation)。元注解也是注解,当然也可以被元注解修饰。 - -Java标准库定义了一些元注解,通常不需要自己去编写元注解,就是说其实也是可以的喽。 - -**@Target**:最常用的元注解,使用`@Target`用来定义注解可以被用于源码的哪些位置: -- 类或接口:`ElementType.TYPE` -- 字段:`ElementType.FIELD` -- 方法:`ElementType.METHOD` -- 构造方法:`ElementType.CONSTRUCTOR` -- 方法参数:`ElementType.PARAMETER` -- 其他:`LOCAL_VARIABLE` `ANNOTATION_TYPE` `PACKAGE` `TYPE_PARAMETER` `TYPE_USE` `MODULE` - -例如定义`@Report`注解可以用于方法上,则需要加上元注解`@Target(ElementType.METHOD)`。 -```java -@Target(ElementType.METHOD) -public @interface Report { - int type() default 0; - String level() default "info"; - String value() default ""; -} -``` - -`ElementType`是一个位于`java.lang.annotation`的枚举类型,和注解`@Target`一样使用时都需要导入。`@Target`的`value`参数是一个`ElementType[]`,如果可以用在多个位置,则需要使用数组`@Target({ElementType.METHOD, ElementType.TYPE})`,只用在一个位置则可以只使用一个枚举值。 - -**@Retention**:定义注解的生命周期: -- 仅编译期:`RetentionPolicy.SOURCE` -- 仅`class`文件:`RetentionPolicy.CLASS` -- 运行期:`RetentionPolicy.RUNTIME` - -如果`@Retention`不存在,则该注解默认为`CLASS`,通常我们自定义的注解都是`RUNTIME`,所以务必要加上`@Retention(RetentionPolicy.RUNTIME)`这个元注解。 - -**@Repeatable**:使用这个元注解可以定义注解是否可以重复,应用不是很广泛。经过`@Repeatable`修饰之后,就可以在某个类型声明处添加多个注解。 -```java -@Repeatable(Reports.class) -@Target(ElementType.TYPE) -@interface Report { - int type() default 0; - String level() default "info"; - String value() default ""; -} - -@Target(ElementType.TYPE) -@interface Reports { - Report[] value(); -} - -@Report(type=1, level="debug") -@Report(type=2, level="warning") -class Hello { -} -``` - -**@Inherited**:定义子类是否可以继承父类的注解,`@Inherited`仅针对`@Target(ElementType.TYPE)`的注解有效,并且仅针对`class`继承,对`interface`继承无效。 - -以上这些元注解都是定义在包`java.lang.annotation`中的,前面说到的标准库中的注解如`@Override`/`@SuppressWarnings`是定义在`java.lang`中自动导入的,并不是编译器或者JVM凭空生成的。 - -总结: -- 注解使用`@interface`定义。 -- 可定义多个参数和默认值,核心参数使用`value`名称。 -- 必须使用`@Target`来指定注解可以应用的范围。 -- 自定义注解时应当设置`@Retention(RetentionPolicy.RUNTIME)`以便运行期读取改注解。 - - -到这里其实我已经有一堆疑问了: -- 为什么用`@interface`来定义,感觉这么暧昧,注解可以理解为一种特殊的类型/接口定义吗? -- 注解到底是怎么起作用的?我自己定义的注解能够做到些什么?应该怎么编写逻辑来实现注解的功能?标准库的如`@Override`这种注解是怎么实现效果的?是写在代码中的逻辑?还是java编译器的特殊处理? -- java编译器如何处理`RetentionPolicy.SOURCE`的注解?`.class`文件如何保存`RetentionPolicy.CLASS`的注解?JVM如何看待和处理`RetentionPolicy.RUNTIME`的注解? - -### 6.3 处理注解 - -根据`@Retention`的配置: -- `RetentionPolicy.SOURCE`类型的注解主要由编译器使用,因此我们一般只使用,不编写。 -- `RetentionPolicy.CLASS`的注解主要由底层工具使用,涉及到class的加载,一般我们很少用到。 -- `RetentionPolicy.RUNTIME`的注解会被经常用到,经常需要编写。 - -这里只讨论如何读取`RetentionPolicy.RUNTIME`类型的注解。注解定义后也是一种`class`,所有注解都继承自`java.lang.annotation.Annotation`。读取注解需要使用反射API。 - -java提供的使用反射API读取注解的方法包括: -判断某个注解是否存在于`Class` `Field` `Method` `Constructor`: -- `Class.isAnnotationPresent(Class)` -- `Field.isAnnotationPresent(Class)` -- `Method.isAnnotationPresent(Class)` -- `Constructor.isAnnotationPresent(Class)` - -`Clss`是实现了这个方法的,后三者是直接或间接从基类`AccessibleObject`继承而来的该方法。最终都是实现的最顶层接口`AnnotatedElement`的方法。 - -`AnnotatedElement`接口定义,主要是判断注解是否存在和获取注解: -```java -default boolean isAnnotationPresent(Class annotationClass) - T getAnnotation(Class annotationClass); -Annotation[] getAnnotations(); -default T[] getAnnotationsByType(Class annotationClass) -default T getDeclaredAnnotation(Class annotationClass) -default T[] getDeclaredAnnotationsByType(Class annotationClass) -Annotation[] getDeclaredAnnotations(); -``` - -这里的`Annotation`接口的就是所有注解的基类。定义: -```java -public interface Annotation { - boolean equals(Object obj); - int hashCode(); - String toString(); - Class annotationType(); -} -``` - -使用反射API读取注解: -- 对于`Class` `Field` `Method` `Constructor`的话就是使用`AnnotatedElement.getAnnotation()`等相关接口了。`Class` `Field` `Method` `Constructor`都是有定义的。如果不存在对应的注解,会返回`null`。可以通过返回值是否为`null`来判断是否有传入的注解。 -- 而要获取到方法参数的注解就相对麻烦了,因为可能有多个参数,每个参数也可能有多个注解,所以结果使用一个二维数组来表示。使用`public abstract Annotation[][] getParameterAnnotations();`方法,最顶层定义在`Executable`中(`Method`和`Constructor`的抽象基类),在`Field`和`Constructor`中做了实现。 - - -使用注解:我们要在运行期来使用注解,那必然是使用`RetentionPolicy.RUNTIME`类型的注解,那注解要怎么用呢?这完全由程序自己决定,也就是说我们必须编写代码来使用注解,使用方法就是通过反射去读取。JVM并不会对我们的注解添加任何额外的逻辑,应该说通过反射的统一处理仅仅是将其作为了一个类动态加载进来然后将一个编译期就已经确定内容的注解实例关联到对应的类、方法、字段、参数等上而已。 - -我的理解:不使用注解是完全OK的,但注解提供了一种方法让我们能够定义自己的关于类、构造、方法、实例等的“规则”,以提供给自己活着其他人使用。这个规则的解释完全由自己的程序进行实现。 - -例子,定义一个`@Range`注解,希望用它来定义一个`String`字段的规则:字段长度必须满足`@Range`参数的定义: -- 定义注解 -```java -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Range { - int min() default 0; - int max() default 255; -} -``` -- 使用注解 -```java -public class Person { - @Range(min=1, max=20) - public String name; - - @Range(max=10) - public String city; -} -``` -- 编写注解的规则 -```java -void check(Person person) throws IllegalArgumentException, ReflectiveOperationException { - // 遍历所有Field: - for (Field field : person.getClass().getFields()) { - // 获取Field定义的@Range: - Range range = field.getAnnotation(Range.class); - // 如果@Range存在: - if (range != null) { - // 获取Field的值: - Object value = field.get(person); - // 如果值是String: - if (value instanceof String) { - String s = (String) value; - // 判断值是否满足@Range的min/max: - if (s.length() < range.min() || s.length() > range.max()) { - throw new IllegalArgumentException("Invalid field: " + field.getName()); - } - } - } - } -} -``` -这样通过`@Range`配合`check()`方法,就可以完成`Person`实例的检查。虽然就这个例子而言谁都能够想到替代的写法,但好像的确能够更方便一些的样子,比如说如果新增加了一个字段并且同样需要进行长度校验的话那么只需要在新字段上添加注解而不需要去修改`check`的逻辑。更多使用场景待挖掘,任何东西都需要结合具体的使用场景才能够有深刻的理解。 - -### 6.4 TODO - -梳理反射和注解相关的类、接口、方法,了解实现,了解`java.lang.relfect`包。 - -## 7. 泛型 - -### 7.1 什么是泛型 - -可以将类型作为参数,从而创建一种可以同时用于多种类型的通用的逻辑的方法。比如创建一种可以应用于所有类型的变长数组类型:`ArrayList`,其中`T`是数据元素的类型。`T`需要编译期可知,当然一个类型不可能编译期不知道,这里的`T`是指的`int` `double` `Runnable`这种内置类型或已经在代码中定义的类型,而不是反射中和一个类型相关联的`Class`对象。 - -向上转型,标准库的`ArrayList`实现了`List`接口,所以可以向上转型。但是需要注意这里的`T`是整个类型定义的一部分,`ArrayList`不能转换为`ArrayList`或者`List`。`T`必须严格一致才能向上转换。类型参数`T`不一致的,比如`ArrayList`和`List`是没有继承关系的两个类型。 - -和C++模板差不太多,就是编写模板代码来适应任意类型,类型参数确定之后才成为一个类型,参数类型不同的模板类是不同的类型。 - -那么C++的模板特化偏特化、模板递归、可变模板参数、模板元编程这种烧脑袋的东西有没有对应的呢? - -TODO:深入了解Java泛型的实现方式,和C++模板有何异同。 - -### 7.2 使用泛型 - -以`java.util.ArrayList`为例,如果不定义泛型类型,泛型类型��际上就是`Object`,相当于默认类型参数是`Object`,这个机制应该是语言层面实现的,因为java并没有默认参数或者默认类型参数这种东西。 -```java -List list = new ArrayList(); -list.add("hello"); -System.out.println((String)list.get(0)); -``` - -当定义泛型类型为`String`之后,`List`泛型接口变为强类型`List`。 -```java -List list = new ArrayList(); -list.add("hello"); -System.out.println(list.get(0)); -``` - -编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。如用`ArrayList`指定泛型类型为`String`后,接口`List`就自动成为`List`。 - -除了类,接口也可以使用泛型,正如`List`。一如: -```java -public interface Comparable { - public int compareTo(T o); -} -``` -对于`Arrays.sort`接口对数组元素排序就会使用`Comparable.compareTo`来比较,元素类型需要实现`Comparable`接口后才能调用。如果未实现则会抛出`java.lang.ClassCastException`提示元素类型不能转化为`java.lang.Comparable`。但这样其实只能对元素按照一种类型来排序,如果需要更高的可定制性,可以使用`Arrays`的`public static void sort(T[] a, Comparator c)`接口传入一个`Comparator`已进行更加灵活的比较。当然也可以在重写`compareTo`比较多种比较方式,加上标记或者`boolean`实例成员来控制即可。 - -### 7.3 编写泛型 - -编写泛型类比普通类复杂,泛型类一般用于集合类中。 - -比如编写一个类表示键值对:先用一个特定类型来实现。 -```java -class kvPair { - private String key; - private String value; - public String getKey() { - return key; - } - public void setKey(String key) { - this.key = key; - } - public String getValue() { - return value; - } - public void setValue(String value) { - this.value = value; - } -} -``` -然后将`String`替换为`T`,并在类名后面加上类型参数``的声明。 -```java -class kvPair { - private T key; - private T value; - public T getKey() { - return key; - } - public void setKey(T key) { - this.key = key; - } - public T getValue() { - return value; - } - public void setValue(T value) { - this.value = value; - } -} -``` -那么如果我需要键和值可以是不同的类型呢,添加多个泛型类型参数即可。 -```java -class kvPair { - private K key; - private V value; - public K getKey() { - return key; - } - public void setKey(K key) { - this.key = key; - } - public V getValue() { - return value; - } - public void setValue(V value) { - this.value = value; - } -} -``` - -实例方法是类的一部分,泛型参数类型对其是可见的。但是对于静态方法来说,类的泛型类型参数对其是不可见的,如果要定义静态泛型方法,需要为静态泛型方法专门指定静态类型参数。这里的静态方法的`K` `V`和`kvPair`中的`K` `V`是没有任何关系的,完全可以替换为其他名称。如果不在`static`后指定静态类型参数的话会报错:不能对非静态类型`K`/`V`进行静态引用。 -```java -class kvPair { - private K key; - private V value; - public kvPair(K k, V v) { - key = k; - value = v; - } - public K getKey() { - return key; - } - public void setKey(K key) { - this.key = key; - } - public V getValue() { - return value; - } - public void setValue(V value) { - this.value = value; - } - - public static kvPair create(K key, V value) { - return new kvPair(key, value); - } -} -``` -使用时:如果就是实例化这个类的对象,那么类型声明时需要写上类型参数,不然类型还是会默认为`Object`,而且貌似类型参数不能使用内置类型,因为默认是`Object`无法持有内置类型的缘故吗?那么就使用相应的包装类型吧,应该就是提供了来满足类似这种场景的。 -```java -kvPair pair = new kvPair("xiaoming", 100.0); -``` - - -### 7.4 泛型实现方法 - -不同于C++的模板实现,Java中的泛型实现方法是**擦拭法**(**Type Erasure**)。 - -所谓擦拭法是指: -- JVM对泛型一无所知,所有工作都是编译器做的。编写了一个泛型类`kvPair`,虚拟机执行的代码就是`kvPair`。 -- 然后编译器根据使用的具体的泛型类型参数实现了安全的强制类型转换。因为泛型类型参数都是编译期确定的,不能转换就会报错停止编译。 - -所以,Java的泛型是编译器在编译器实现的,编译器内部永远把所有类型`T`当做`Object`处理(即是说**擦拭**成了`Object`),但是需要转换类型时,编译器会自动根据`T`的类型为我们安全的实行强制转换。 - -所以必然就有局限: -- ``不能是基本类型,因为`Object`无法持有基本类型。 -- 无法取得类型参数的`class`,因为`class`是运行期的。编译期对编译器来说所有泛型类型参数都是`Object`。 -- 无法区分带泛型的类型,因为都是同一个`Class`对象。 -- 不能实例化类型参数`T`的变量,实例化`Object`明显不是我们想要的,所以java编译器阻止了在泛型类中对参数类型变量的实例化。 - -所以`kvPair`和`kvPair`和`kvPair`实际上是一个类型,他们的`Class`对象是同一个。那直观感受来看和C++的模板是有区别的,C++的模板会为每一种模板类型参数生成一个类或者函数的机器码,模板参数实际上最终的类的一部分,所以以上局限都是没有的。而Java中的泛型看来类型参数只是提供了用来告诉编译器需要如何做类型转换的手段,而并不是类型的一部分。 - -**不恰当的覆写**: -```java -public class Pair { - public boolean equals(T t) { - return this == t; - } -} -``` -像这样的代码其实会被擦拭成`equals(Object t)`,这个方法是继承自`Object`的,编译器会阻止一个实际上会变成覆写的方法定义。错误提示为:类型 `Pair` 的方法 `equals(T)`与类型 `Object` 的 `equals(Object)`具有相同的擦除,但是未覆盖它。 - - -需要换个方法名,避免与`Object.equals(Object)`冲突。 -```java -public class Pair { - public boolean same(T t) { - return this == t; - } -} -``` - -**泛型类的继承**: - -一个类可以继承自一个泛型类。比如 -```java -class StringDoublePair extends kvPair { - public StringDoublePair() { - super("", 0.0); - } -} -``` -继承之后`StringDoublePair`的基类的类型参数是确定的,就是``,但是我们无法通过`kvPair.class`对象获取到这个类型参数。但在继承了泛型类型的情况下,子类是可以获取到父类的泛型类型的。获取方式: -```java -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -public class Main { - public static void main(String[] args){ - Class cls = StringDoublePair.class; - Type t = cls.getGenericSuperclass(); - if (t instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType)t; - Type[] types = pt.getActualTypeArguments(); - for (Type typeArgs : types) { - Class typeClass = (Class)typeArgs; - if (typeClass != null) { - System.out.println(typeClass); - } - } - } - } -} -``` -这里获取到的是`Type`,因为java引入了泛型,所以单纯的`Class`用来标识类型就不够了,Java的类型系统结构如下: -``` -Type -|____Class -|____ParameterizedType -|____GenericArrayType -|____WildcardType -``` - -### 7.5 extends通配符 - -还是上面的`kvPair`,如果定义一个适用于`kvPair`的方法: -```java -class PairHelper { - public static int add(kvPair pair) { - return pair.getKey().intValue() + pair.getValue().intValue(); - } -} -``` -那么下面的语句时能够编译通过的: -```java -int sum = PairHelper.add(new kvPair(1, 1)); -``` -但是其实参数的类型是`(Integer, Integer)`,如果类型参数就是`Integer`呢: -```java -int sum = PairHelper.add(new kvPair(1, 1)); -``` -编译错误:类型 `PairHelper` 中的方法`add(kvPair)`对于参数`(kvPair)`不适用。 - -原因很简单:类型`kvPair`不是`kvPair`的子类。但是很明显`kvPair`参数类型运用于`add`函数是完全满足内部代码的类型规范的,即基类或者接口来使用子类对象。 - -那现在有没有方法能够使得方法`add`能够接受`kvPair`其中`Type1` `Type2`是`Number`子类呢?办法当然是有的,就是定义`add`时使用`? extends Number`替代类型参数`Number`。 -```java -class PairHelper { - public static int add(kvPair pair) { - return pair.getKey().intValue() + pair.getValue().intValue(); - } -} -``` - -这种使用``的泛型定义称为**上界通配符**(Upper Bounds Wildcards)。即把泛型类型参数`T`的上界限定为`Number`,就是只要是`Number`和其子类都可以。 - -此时编译器能够确定`kvPair`的`getKey` `getValue`接口返回值一定`Number`或其子类,但无法确定具体类型。 - -使用了通配符之后能不能用子类实例设置给基类成员呢? -```java -class PairHelper { - public static int add(kvPair pair) { - pair.setKey(Integer.valueOf(pair.getKey().intValue() + 100)); - pair.setKey(Integer.valueOf(pair.getValue().intValue() + 100)); - return pair.getKey().intValue() + pair.getValue().intValue(); - } -} -``` -这时会编译错误提示:类型 `kvPair` 中的方法 `setKey(capture#1-of ? extends Number)`对于参数`(Integer)`不适用 - -编译错误的原因还是在于擦拭法,这就是``通配符的一个重要限制: -- 方法参数签名`setKey(? extends Number)`无法传递任何`Number`或者子类型给`setKey(? extends Number)`,唯一的例外是可以传入`null`。 -- 对于方法类型参数``的泛型参数来说,方法内部不能调用它的传入`Number`引用的方法,总结来说**只读不能写**。为什么呢?这里我有一万个问号?后续再来理解,TODO。 - -定义泛型类时也可以用``这样的方式使用通配符来限定`T`的类型。那么实例化时该类型参数就只能使用`Number`或者其子类。这里当然也可以是`interface`,只不过语法规定为使用`extends`。 - -### 7.6 super通配符 - -除了`extends`通配符外还有`super`通配符,对于类型参数中有``的实例来说。 -- 对应地,可以使用`Integer`和它的基类来进行匹配。 -- 使用该类型作为方法参数时,该参数**只能写不能读**,不能调用它的返回``类型的接口,但可以调用它的使用``参数的接口。就上面例子来说就是能调`setKey`不能调用`getKey`。 - -**PECS原则**:一般使用`extends`和`super`是遵循**Producer Extends Consumer Super**。即生产者使用`extends`,而消费者使用`super`。 - -除了`extends`和`super`通配符,`Java`的泛型还允许使用**无限定通配符**(Unbounded Wildcard Type),即只定义一个`?`。 - -特点: -- 因为``通配符既没有`extends`也没有`super`: - - 不允许调用`set(T)`方法并传入引用(`null`除外)。 - - 不允许调用`T get()`方法并获取`T`引用(只能获取`Object`引用)。 - - 即既不能读也不能写,只能做一些`null`判断。 -- 大多数情况可以引入泛型参数``消除``的使用,就是说可以替换。 -- 无限定通配符``很少使用。 - -``通配符有一个独特的特点,就是`pair`是所有`pair`的超类,可以安全地向上类型转换。 - -到这里我只能说并没有搞懂为什么要有`extends`和`super`通配符这两个东西,也没有完全理解,TODO。 - -### 7.7 泛型和反射 - -java的部分反射API也是泛型,比如`Class`类就是泛型: -```java -public final class Class implements java.io.Serializable, - GenericDeclaration, - Type, - AnnotatedElement, - TypeDescriptor.OfField>, - Constable { - public static Class forName(String className) {...} - public native boolean isAssignableFrom(Class cls); - public TypeVariable>[] getTypeParameters() {...} - public native Class getSuperclass(); - public Class[] getInterfaces() {...} - public Type[] getGenericInterfaces() {...} - public Class getComponentType() {...} // 获取元素类型,仅对数组类型有效,否则返回null - public Class getDeclaringClass() throws SecurityException {...} - public Constructor[] getConstructors() throws SecurityException {...} - // other method about Constructor ... - public A getAnnotation(Class annotationClass) {...} - public boolean isAnnotationPresent(Class annotationClass) {...} - // etc ... -} -``` - -构造方法`Constructor`也是泛型。 - -泛型与数组: -- 我们可以声明带泛型的数组,但不能直接`new`带泛型的数组,需要经过强制类型转换。 -```java -Pair[] ps = null; // ok -Pair[] ps = new Pair[2]; // compile error! -@SuppressWarnings("unchecked") -Pair[] ps = (Pair[]) new Pair[2]; // ok -``` -- 要安全使用泛型数组,不要将上述`new Pair[2]`的结果保存之后使用。因为`new Pair[2]`的结果不是泛型数组,编译器不会检查。 -- 带泛型的数组编译器也会做类型擦除: -```java -Pair[] ps = (Pair[]) new Pair[2]; -System.out.println(ps.getClass() == Pair[].class); // true -``` -- 不能直接创建泛型数组`T[]`,因为擦拭后代码变`为Object[]` -```java -// compile error: -public class Abc { - T[] createArray() { - return new T[5]; - } -} -``` -- 必须借助`java.lang.reflect.Array`来创建 -```java -T[] createArray(Class cls) { - return (T[]) Array.newInstance(cls, 5); -} -``` -- 还可以利用可变参数创建泛型数组: -```java -public class ArrayHelper { - @SafeVarargs - static T[] asArray(T... objs) { - return objs; - } -} -``` -- 谨慎使用泛型可变参数,如果仔细观察,可以发现编译器对所有可变泛型参数都会发出警告,除非确认完全没有问题,才可以用`@SafeVarargs`消除警告。跟详细解释参考[*Effective Java*](https://www.oreilly.com/library/view/effective-java/9780134686097/),看起来java的“上层建筑”确实有点太多太繁杂了。 - -说实话泛型有点云里雾里的感觉,后续看书补充理解,TODO。 - -## 8. 集合 - -集合大部分时候应该会是使用最多的类型,因为任何东西都需要存储、管理,元素多了的时候就需要使用集合来存储。其中各式各样的数据结构服务于各种不同的使用场景:存储、查找、遍历、增删修改元素等操作的不同侧重。 - -先上Java集合类框架: -![java集合类框架](Images/Java_collections.jpg) - -### 8.1 Java集合 - -数组的限制: -- 数组初始化后大小不可变。 -- 数组只能按索引顺序存取,即随机存取。 - -其他存储需求: -- 可变大小 -- 保证无重复元素 -- 快速查找 - -**java.util.Collection**:java标准库提供的集合类,定义在`java.util`包中,除`Map`所有其他集合类的根接口。`java.util`包主要提供了三种类型集合: -- `List` 一种有序列表的集合。 -- `Set` 一种保证没有重复元素的集合。 -- `Map` 一种通过键值(key-value)查找的映射表集合。 - -Java集合设计特点: -- 接口和实现类分离,有序表接口`List`,实现`ArrayList` `LinkedList`。 -- 支持泛型。 -- 同一方式访问:迭代器(Iterator)。好处:无需知道集合内部元素的存储方式。 - -Java集合历史久远,不应再使用的遗留类: -- `Hashtable` 一种线程安全的`Map`实现 -- `Vector` 一种线程安全的`List`实现 -- `Stack` 基于`Vector`实现的LIFO的栈 - -不应使用的遗留接口: -- `Enumeration`:已被`Iterator`取代。 - -`Collection`接口: -```java -public interface Collection extends Iterable { - int size(); - boolean isEmpty(); - boolean contains(Object o); - Iterator iterator(); - Object[] toArray(); - T[] toArray(T[] a); - default T[] toArray(IntFunction generator) { - return toArray(generator.apply(0)); - } - boolean add(E e); - boolean remove(Object o); - boolean containsAll(Collection c); - boolean addAll(Collection c); - boolean removeAll(Collection c); - default boolean removeIf(Predicate filter) { // 移除所有满足给定条件的元素 - Objects.requireNonNull(filter); - boolean removed = false; - final Iterator each = iterator(); - while (each.hasNext()) { - if (filter.test(each.next())) { - each.remove(); - removed = true; - } - } - return removed; - } - boolean retainAll(Collection c); - void clear(); - boolean equals(Object o); - int hashCode(); - @Override - default Spliterator spliterator() { - return Spliterators.spliterator(this, 0); - } - default Stream stream() { - return StreamSupport.stream(spliterator(), false); - } - default Stream parallelStream() { - return StreamSupport.stream(spliterator(), true); - } -} -``` - - -`Iterable`接口: -```java -public interface Iterable { - Iterator iterator(); // 迭代器 - default void forEach(Consumer action) { // 对所有元素执行传入的操作 - Objects.requireNonNull(action); - for (T t : this) { - action.accept(t); - } - } - default Spliterator spliterator() { - return Spliterators.spliteratorUnknownSize(iterator(), 0); - } -} -``` - -### 8.2 List - -有序列表,可变数组实现`ArrayList`,和链表实现`LinkedList`,对比: - -||ArrayList|LinkedList| -|:-:|:-:|:-:| -|获取|快|从头查找,慢| -|添加到末尾|快|快| -|指定位置添加/删除|需要移动元素|不需要移动元素| -|内存占用|小|较大| - -通常情况下,我们总是优先使用`ArrayList`。 - -`List`接口: -```java -public interface List extends Collection { - // Query Operations - int size(); - boolean isEmpty(); - boolean contains(Object o); - Iterator iterator(); // 迭代器 - Object[] toArray(); // 返回列表元素构成的一个重新分配的数组 - T[] toArray(T[] a); // 返回特定类型,如果可以的话返回内置数组(如ArrayList),否则新分配 - // Modification Operations - boolean add(E e); // 添加元素到末尾 - boolean remove(Object o); // 移除特定元素 - // Bulk Modification Operations - boolean containsAll(Collection c); // 包含集合中所有元素 - boolean addAll(Collection c); // 添加集合所有元素到列表末尾,参数是自己的话未定义行为 - boolean addAll(int index, Collection c); // 添加集合元素到index开始位置,从index开始所有元素向后移 - boolean removeAll(Collection c); // 移除当前列表中所有在集合中的元素 - boolean retainAll(Collection c); // 移除所有不在集合中的元素,保留集合中的元素 - default void replaceAll(UnaryOperator operator) {...} // 替换所有元素对给该元素应用传入的operator之后的结果 - @SuppressWarnings({"unchecked", "rawtypes"}) - default void sort(Comparator c) {...} // 使用传入的比较运算对数组元素进行s稳定排序(不会重排相等的元素) - void clear(); // 清空所有元素 - // Comparison and hashing - boolean equals(Object o); // 和另一个List判等,同一位置元素相同、相同大小则等 - int hashCode(); // 哈希值,根据列表所有元素哈希值求得 - // Positional Access Operations - E get(int index); // 获取元素 - E set(int index, E element); // 设置元素 - void add(int index, E element); // 添加/插入元素到下标index处,index开始所有元素右移 - E remove(int index); // 移除index处元素,后面元素左移 - // Search Operations - int indexOf(Object o); // 第一个出现元素下标,没有则返回-1 - int lastIndexOf(Object o); // 最后一个出现的该元素下标,没有则返回-1 - // List Iterators - ListIterator listIterator(); // 列表迭代器 - ListIterator listIterator(int index); // 从index开始的列表迭代器 - // View - List subList(int fromIndex, int toIndex); // 返回一个子列表,包括fromIndex,不包括toIndex,相等则返回空 - @Override - default Spliterator spliterator() {...} - @SuppressWarnings("unchecked") - static List of() {...} // 得到空列表 - // other of method ... // 得到由传入的多个元素构成的列表,不接受null值 - static List copyOf(Collection coll) {...} // 得到传入集合元素构成的不可修改的列表 -} -``` - -特点: -- 允许添加重复元素。 -- 允许添加`null`元素。 -- 不支持`[]`取元素,仅有原生数组支持`[]`。不能运算符重载有点微妙。 - -遍历:`String`列表为例 -- 经典`for`循环,对`ArrayList`来说是随机存取,但链表访问需要遍历,但要遍历修改还是得老老实实用。 -```java -for (int i = 0; i < list.size(); i ++) { - String elem = list.get(i); - // ... -} -``` -- 范围`for`循环,通过迭代器实现,但不能修改元素 -```java -for (String elem : list) { - // ... -} -``` -- 迭代器遍历,始终推荐使用迭代器 -```java -Iterator iter = list.iterator(); -while (iter.hasNext()) { - String elem = iter.next(); - // ... -} -``` - -常用方法解析: -- `toArray()`只能返回`Object[]`少用。 -- `toArray(T[])`更为常用,填充传入的数组并返回,如果传入的数组元素不够,那么会重新分配并返回,如果超过了,剩余的会填`null`。一般都根据列表大小传:`Integer[] array = list.toArray(new Integer[list.size()]);`。 -- 所有会比较两个元素的操作都是调用`equals`方法判等。要正确使用查找相关的方法,就必须正确重写`equals`方法。 - - -如何正确覆写`equals`方法: -- 自反性(Reflexive):非`null`的`x`来说,`x.equals(x)`一定返回`true` -- 对称性(Symmetric):非`null`的`x`和`y`,`x.equals(y)`结果一定和`y.equals(x)`相同 -- 传递性(Transitive):非`null`的`x`、`y`、`z`,如果`x.equals(y)==true`,`y.euqals(z) == true`,那么`y.equals(z)`一定为`true` -- 一致性(Consistent):非`null`的`x`和`y`,只要`x`和`y`状态不变,则`x.euqals(y)`总是一致地返回`true`或者`false`,就是它不能薛定谔,需要具有确定性。 -- 对`null`的比较:`x.equals(null)`一定返回`false` - - -### 8.3 Map & HashMap - -`Map`即键值(key-value)映射表,高效通过`key`查找`value`。 - -`Map`接口: -```java -public interface Map { - // Query Operations - int size(); - boolean isEmpty(); - boolean containsKey(Object key); - boolean containsValue(Object value); - V get(Object key); - - // Modification Operations - V put(K key, V value); - V remove(Object key); - // Bulk Operations - void putAll(Map m); - void clear(); - // Views - Set keySet(); - Collection values(); - Set> entrySet(); - interface Entry { - K getKey(); - V getValue(); - V setValue(V value); - boolean equals(Object o); - int hashCode(); - public static , V> Comparator> comparingByKey() { - return (Comparator> & Serializable) - (c1, c2) -> c1.getKey().compareTo(c2.getKey()); - } - public static > Comparator> comparingByValue() { - return (Comparator> & Serializable) - (c1, c2) -> c1.getValue().compareTo(c2.getValue()); - } - public static Comparator> comparingByKey(Comparator cmp) { - Objects.requireNonNull(cmp); - return (Comparator> & Serializable) - (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey()); - } - public static Comparator> comparingByValue(Comparator cmp) { - Objects.requireNonNull(cmp); - return (Comparator> & Serializable) - (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue()); - } - } - - // Comparison and hashing - boolean equals(Object o); - int hashCode(); - - // Defaultable methods - default V getOrDefault(Object key, V defaultValue) { - V v; - return (((v = get(key)) != null) || containsKey(key)) - ? v - : defaultValue; - } - default void forEach(BiConsumer action) { - Objects.requireNonNull(action); - for (Map.Entry entry : entrySet()) { - K k; - V v; - try { - k = entry.getKey(); - v = entry.getValue(); - } catch (IllegalStateException ise) { - // this usually means the entry is no longer in the map. - throw new ConcurrentModificationException(ise); - } - action.accept(k, v); - } - } - default void replaceAll(BiFunction function) { - Objects.requireNonNull(function); - for (Map.Entry entry : entrySet()) { - K k; - V v; - try { - k = entry.getKey(); - v = entry.getValue(); - } catch (IllegalStateException ise) { - // this usually means the entry is no longer in the map. - throw new ConcurrentModificationException(ise); - } - - // ise thrown from function is not a cme. - v = function.apply(k, v); - - try { - entry.setValue(v); - } catch (IllegalStateException ise) { - // this usually means the entry is no longer in the map. - throw new ConcurrentModificationException(ise); - } - } - } - default V putIfAbsent(K key, V value) { - V v = get(key); - if (v == null) { - v = put(key, value); - } - - return v; - } - default boolean remove(Object key, Object value) { - Object curValue = get(key); - if (!Objects.equals(curValue, value) || - (curValue == null && !containsKey(key))) { - return false; - } - remove(key); - return true; - } - default boolean replace(K key, V oldValue, V newValue) { - Object curValue = get(key); - if (!Objects.equals(curValue, oldValue) || - (curValue == null && !containsKey(key))) { - return false; - } - put(key, newValue); - return true; - } - default V replace(K key, V value) { - V curValue; - if (((curValue = get(key)) != null) || containsKey(key)) { - curValue = put(key, value); - } - return curValue; - } - default V computeIfAbsent(K key, - Function mappingFunction) { - Objects.requireNonNull(mappingFunction); - V v; - if ((v = get(key)) == null) { - V newValue; - if ((newValue = mappingFunction.apply(key)) != null) { - put(key, newValue); - return newValue; - } - } - - return v; - } - default V computeIfPresent(K key, - BiFunction remappingFunction) { - Objects.requireNonNull(remappingFunction); - V oldValue; - if ((oldValue = get(key)) != null) { - V newValue = remappingFunction.apply(key, oldValue); - if (newValue != null) { - put(key, newValue); - return newValue; - } else { - remove(key); - return null; - } - } else { - return null; - } - } - default V compute(K key, - BiFunction remappingFunction) { - Objects.requireNonNull(remappingFunction); - V oldValue = get(key); - - V newValue = remappingFunction.apply(key, oldValue); - if (newValue == null) { - // delete mapping - if (oldValue != null || containsKey(key)) { - // something to remove - remove(key); - return null; - } else { - // nothing to do. Leave things as they were. - return null; - } - } else { - // add or replace old mapping - put(key, newValue); - return newValue; - } - } - default V merge(K key, V value, - BiFunction remappingFunction) { - Objects.requireNonNull(remappingFunction); - Objects.requireNonNull(value); - V oldValue = get(key); - V newValue = (oldValue == null) ? value : - remappingFunction.apply(oldValue, value); - if (newValue == null) { - remove(key); - } else { - put(key, newValue); - } - return newValue; - } - @SuppressWarnings("unchecked") - static Map of() { - return (Map) ImmutableCollections.EMPTY_MAP; - } - static Map of(K k1, V v1) { - return new ImmutableCollections.Map1<>(k1, v1); - } - static Map of(K k1, V v1, K k2, V v2) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, - K k6, V v6) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, - k6, v6); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, - K k6, V v6, K k7, V v7) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, - k6, v6, k7, v7); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, - K k6, V v6, K k7, V v7, K k8, V v8) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, - k6, v6, k7, v7, k8, v8); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, - K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, - k6, v6, k7, v7, k8, v8, k9, v9); - } - static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, - K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) { - return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, - k6, v6, k7, v7, k8, v8, k9, v9, k10, v10); - } - @SafeVarargs - @SuppressWarnings("varargs") - static Map ofEntries(Entry... entries) { - if (entries.length == 0) { // implicit null check of entries array - @SuppressWarnings("unchecked") - var map = (Map) ImmutableCollections.EMPTY_MAP; - return map; - } else if (entries.length == 1) { - // implicit null check of the array slot - return new ImmutableCollections.Map1<>(entries[0].getKey(), - entries[0].getValue()); - } else { - Object[] kva = new Object[entries.length << 1]; - int a = 0; - for (Entry entry : entries) { - // implicit null checks of each array slot - kva[a++] = entry.getKey(); - kva[a++] = entry.getValue(); - } - return new ImmutableCollections.MapN<>(kva); - } - } - static Entry entry(K k, V v) { - // KeyValueHolder checks for nulls - return new KeyValueHolder<>(k, v); - } - @SuppressWarnings({"rawtypes","unchecked"}) - static Map copyOf(Map map) { - if (map instanceof ImmutableCollections.AbstractImmutableMap) { - return (Map)map; - } else { - return (Map)Map.ofEntries(map.entrySet().toArray(new Entry[0])); - } - } -} -``` - -最常用的实现类是`HashMap`,采用**数组+链表+红黑树**实现,也就是分离链表法(**拉链法**)实现的**哈希表**。简而言之就是用一个大数组存元素,每个不同元素都有一个特定的哈希值通过某种计算之后得到一个数组下标,然后这个元素就存在这个下标的位置,如果哈希冲突了(两个不同元素哈希值相同或者通过哈希值计算得到的下标相同)就用链表将下标相同的键值对链起来,如果链表长度超过8,则将链表转换为红黑树以提高查找效率。哈希表分配了数组之后数组大小是确定的,因为需要利用这个大小和哈希值来计算索引,在往哈希表中添加元素的过程中,必然会导致哈希冲突越来越频繁,当达到某一个阈值时基于效率考虑需要对哈希表进行扩容,重新分配更大的数组,并且根据哈希值重算所有元素的下标(再哈希,rehash)。扩容时机选择、哈希函数的编写、哈希冲突的解决方案都会影响哈希表的性能。 - -常用方法:`get` `put` `remove` `containsKey` `keySet` `entrySet` - -遍历: -- `for each`循环遍历`keySet`返回的集合 -- `for each`遍历`entrySet()`集合,同时遍历`key`和`value` - -特点: -- 哈希表特性:不保证按插入顺序存储,也无法对元素排序,最佳`O(1)`的访问、插入、删除、按key查找时间复杂度。 -- 使用`key`的`hashCode()`作为哈希值,`key`的值作为发生哈希冲突时辅助判断的方法。 - -正确使用`Map`必须保证: -- 判断`key`相等依然是通过`equals`方法,所以要正确覆写`key`类型的`equals`方法。 -- 作为`key`还需要正确覆写`int hashCode()`方法以获取哈希值。要求: - - 如果对象相等(`equals()`返回`true`),那么哈希值必须相等。必须满足以保证正确性。 - - 两个对象不相等,尽量保证两个对象的`hashCode()`不相等。尽量保证以减少哈希冲突,提高查找效率。 -- 编写`equals(`)和`hashCode()`遵循的原则是: `equals()`用到的用于比较的每一个字段,都必须在`hashCode()`中用于计算,`equals()`中没有使用到的字段,绝不可放在`hashCode()`中计算。 -- `对于value`对象则没有任何要求。 - -`equals`和`hashCode`编写实例: -```java -class Person { - private String name; - private int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } - public boolean equals(Person other) { - return name.equals(other.name) && age == other.age; - } - public int hashCode() { - return name.hashCode() * 31 + age; - } -} -``` -`hashCode`需要用到每一个参与`equals`比较的字段,一种常见方法是:迭代逐次将每一轮的哈希值乘以一个素数并加上下一个字段的哈希值,直到所有字段都参与计算。 - -上述`Person.hashCode`其实还有一点问题,如果`name`为`null`那么就直接`NullPointerException`了,所以经常借助`Objects.hash()`来计算哈希值。它的实现是差不多一样的逻辑: -```java -public final class Objects { - ublic static int hash(Object... values) { - return Arrays.hashCode(values); - } -} - -public class Arrays { - public static int hashCode(Object a[]) { - if (a == null) - return 0; - - int result = 1; - - for (Object element : a) - result = 31 * result + (element == null ? 0 : element.hashCode()); - - return result; - } -} -``` - - -最一般的通过哈希值计算下标的方式就是直接取低位或者做取余操作。取低位操作计算量小,且实现时默认尺寸是16,每次扩容都是扩容为原先的2倍,`HashMap`就是采用取低位的方式。 -```java -int index = key.hashCode() & 0xf; // 数组大小默认是16,直接取低4位 -int index = key.hashCode() % arraySize; // 计算量相对取低位来说就大了一点 -``` -``HashMap``内部其实使用提供的哈希值又做了一次计算然后才用来计算下标。在低16位加入了高16位的扰动(将高16位异或到了低16位),因为直接取低位的方式会导致元素不多数组不大时高位用不到,加入高位扰动后可以进一步将低哈希冲突的概率。 -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -TODO:详细分析`HashMap`实现。 - - -### 8.4 EnumMap - -如果`Map`的`key`是`enum`的话,还可以使用`java.util.EnumMap`,可以在内部以一个非常紧凑的数组存储`value`,并不需要计算`hashCode()`,不但效率最高,而且没有额外的空间浪费。当然其实如果是其他语言可能直接用`enum`转整数作为下标,`new`一个数组直接存其实就行了。 - -定义: -```java -public class EnumMap, V> extends AbstractMap implements java.io.Serializable, Cloneable -``` - -使用时直接使用`Map`接口来使用即可。和使用`HashMap`没有任何区别。 - -### 8.5 TreeMap - -`HashMap`以哈希表来实现就决定了它的元素是无序的,当我们需要`Map`的元素有序时可以使用`SortedMap`接口以及实现类`TreeMap`。 - -对应于C++的容器来说`TreeMap`就对应于`std::map`,而`HashMap`则对应于`std::unordered_map`。 - -派生关系: -``` -Map -|__HashMap -|__SortedMap - |__TreeMap -``` - -`SortedMap`保证遍历时以`key`的顺序进行排序,具体的排序规则则由传入的`Comparable`接口决定。对于没有实现`Comparable`的接口的`key`,则需要在构造`TreeMap`时传入一个自定义的排序算法。相关方法: -```java -public class TreeMap - extends AbstractMap - implements NavigableMap, Cloneable, java.io.Serializable -{ - private final Comparator comparator; - public TreeMap() { - comparator = null; - } - public TreeMap(Comparator comparator) { - this.comparator = comparator; - } - @SuppressWarnings("unchecked") - final int compare(Object k1, Object k2) { - return comparator==null ? ((Comparable)k1).compareTo((K)k2) - : comparator.compare((K)k1, (K)k2); - } -} -``` -当调用`key`的比较方法`compare`时,首先使用构造时传入的`Comparator`对象,如果构造时没有传则为`null`,则使用`key`实现的`Comparable`接口的`compareTo`方法比较。 - -如果使用`Comparator`对象的,需要针对这个类编写新的类实现`Comparator`接口,`Comparator`需要能够读取需要用于比较的`key`的成员,就必须实现相关的`getter`方法,直接在`key`中实现`Comparable`接口相对来说更为简单一些。如果多个`TreeMap`需要多种方式比较可以通过实现多个`Comparator`接口来做到。 - -实现`Comparable.compareTo`或者`Comparator.compare`接口时,都是如果小于则返回负数,通常是`-1`,相等返回`0`,大于返回正数,通常是`1`。实现时应该按照规范,相等时必须返回`0`,不然查找时就查找不到了。注意使用`TreeMap`就不强制要求实现`equals`和`hashCode`方法了。 - -`TreeMap`是按照升序排列的,也就是遍历得到的结果一定满足前者与后者比较结果为负。需要按照不同规则排序的话,可以实现不同的比较接口。 - -看一下`SortedMap`接口提供了哪些特有的方法,并且其实在`SortedMap`和`TreeMap`之间还有一层`NavigableMap`。主要是子`Map`和各种查找操作。 -```java -public interface SortedMap extends Map { - Comparator comparator(); // 比较操作 - SortedMap subMap(K fromKey, K toKey); // from inclusive, to exclusive - SortedMap headMap(K toKey); - SortedMap tailMap(K fromKey); - K firstKey(); - K lastKey(); - Set keySet(); // key升序集合 - Collection values(); // 值的集合 - Set> entrySet(); // key升序排列的键值对集合 -} - -public interface NavigableMap extends SortedMap { - Map.Entry lowerEntry(K key); // 得到最大的key小于给定的key的键值对,或者没有这样的key则返回null - K lowerKey(K key); // 同上一个不过返回的是key - Map.Entry floorEntry(K key); // 得到最大的key小于或等于给定key的键值对,没有返回null - K floorKey(K key); // // 得到最大的小于或等于给定key的key - Map.Entry ceilingEntry(K key); // 最小的大于等于给定key的键值对 - K ceilingKey(K key); // 最小的大于等于给定key的key - Map.Entry higherEntry(K key); // 最小的大于给定key的键值对 - K higherKey(K key); // 最小的大于给定key的key - Map.Entry firstEntry(); // 最小key,也就是第一个key,空Map则返回null - Map.Entry lastEntry(); // 最大key,最后一个key,空Map返回null - Map.Entry pollFirstEntry(); // 移除最小key对应元素,返回该键值对,空Map返回null - Map.Entry pollLastEntry(); // 移除最大key对应元素 - NavigableMap descendingMap(); // 返回逆序Map,修改元素会反应到原始Map - NavigableSet navigableKeySet(); // key的有序集合,升序排列 - NavigableSet descendingKeySet(); // key的降序集合, - NavigableMap subMap(K fromKey, boolean fromInclusive, - K toKey, boolean toInclusive); // 子Map,见名知意 - NavigableMap headMap(K toKey, boolean inclusive); // 小于或等于给定key元素构成的Map - NavigableMap tailMap(K fromKey, boolean inclusive); // 大于或等于给定key元素构成的Map - SortedMap subMap(K fromKey, K toKey); // subMap(fromKey, true, toKey, false) - SortedMap headMap(K toKey); // headMap(toKey, false) - SortedMap tailMap(K fromKey); // tailMap(fromKey, true) -} -``` - -### 8.6 Properties - -编写应用程序时,通常要读写配置文件,配置文件通常来说`Key-Value`是`String-String`类型的,因此可以用`Map`来表示。如: -``` -account=32767 -username=tikot -``` -java集合库提供了一个`Properties`来表示一组配置,由于历史遗留原因,`Properties`是从`Hashtable`派生的,但只需要用到`Properties`本身的方法。 - -Java默认配置文件以`.properties`为扩展名,以`#`为注释,如: -``` -# account.properties -account=32767 -username=tikot -``` - -**读写修改配置文件**: -```java -public class Main { - public static void main(String[] args) throws Exception { - String file = "account.properties"; - Properties props = new Properties(); - props.load(new java.io.FileInputStream(file)); - System.out.println(props.getProperty("account")); - System.out.println(props.getProperty("username")); - props.setProperty("locatoin", "mars"); - props.store(new FileOutputStream(file), "this is comment"); - } -} -``` - -相关方法: -- `load`从流读取配置 -- `store`将配置写入流 -- `getProperty()` `setProperty()` 获取和修改配置 - - -总结: -- Java集合库提供的`Properties`用于读写配置文件`.properties`。`.properties`文件可以使用`UTF-8`编码 -- 可以从文件系统、`classpath`或其他任何地方读取`.properties`文件。 -- 读写时调用`getProperty()`、`setProperty()`方法,不要使用从基类重写的方法`put` `set`。 -- `FileInputStream`表示字节流,使用表示字符流的`FileReader`重载版本指定字节编码可以更好的处理文件编码。 - -### 8.7 Set - -如果只需要存储不重复的`key`,不需要存储`value`,则可以使用`Set`。定义: -```java -public interface Set extends Collection { - // Query Operations - int size(); - boolean isEmpty(); - boolean contains(Object o); - Iterator iterator(); - Object[] toArray(); // 转数组,元素按照迭代器顺序 - T[] toArray(T[] a); - // Modification Operations - boolean add(E e); - boolean remove(Object o); - // Bulk Operations - boolean containsAll(Collection c); - boolean addAll(Collection c); - boolean retainAll(Collection c); - boolean removeAll(Collection c); - void clear(); - // Comparison and hashing - boolean equals(Object o); - int hashCode(); - @Override - default Spliterator spliterator() { - return Spliterators.spliterator(this, Spliterator.DISTINCT); - } - @SuppressWarnings("unchecked") - static Set of() { - return (Set) ImmutableCollections.EMPTY_SET; - } - // ... 多个参数和可变参数的of - // 由Collection创建Set - @SuppressWarnings("unchecked") - static Set copyOf(Collection coll) { - if (coll instanceof ImmutableCollections.AbstractImmutableSet) { - return (Set)coll; - } else { - return (Set)Set.of(new HashSet<>(coll).toArray()); - } - } -} -``` - -除了从`Collection`继承而来的方法外,主要操作为: -- `add` `remove` 添加移除 -- `contains` `isEmpty`是否包含特定元素、判空 - -`Set`就相当于只存储`key`,不存储`value`的`Map`,经常使用`Set`来**去重**。 - -实现: -- `HashSet`无序,类似于`HashMap`是哈希表实现,所以需要`key`正确实现`equals`和`hashCode`方法。 -- `TreeSet`有序,实现了`SortedSet`接口,同`TreeMap`红黑树实现,需要构造时传入`key`的`Comparator`或者`key`正确实现`Comparable`接口。 - -### 8.8 Queue - -队列,即先进先出的有序表,定义: -```java -public interface Queue extends Collection { - boolean add(E e); // 入队 - boolean offer(E e); // 入队 - E remove(); // 出队 - E poll(); // 出队 - E element(); // 队首元素 - E peek(); // 队首元素 -} -``` -相同操作的不同方法的区别仅在于队满或队空时的行为是抛异常还是仅返回一个`null`或`false`,`add` `remove` `element`会抛异常,`offer`队满(达到了容量限制的大小)时返回`false`,`poll`和`peek`队空时返回`null`。 - -注意应该避免把`null`添加到队列,不然`peek` `poll`返回`null`就无法判断是队空了还是返回了一个`null`元素。 - -可以注意到`LinkedList`实现了`Queue`接口,并且中间还有一层`Deque`。 - -### 8.9 PriorityQueue - -即优先队列,`PriorityQueue`实现了`Queue`接口,对`PriorityQueue`调用`remove`或者`poll`,出队时总是优先级最高的元素。 - -我们需要提供比较接口:实现`Comparable`接口或者传入`Comparator`对象,以便能够通过比较确定优先级。 - -`PriorityQueue`是实现类,使用时直接使用`new`,直接使用实现的`Queue`接口的方法即可。 - -优先队列通常用堆实现,入队出队提供`O(logn)`的平均时间复杂度,`TreeMap`可以提供优先队列能够做到的所有操作,只是应该会占用更多空间,特定场景下还是可以使用`PriorityQueue`。 - - -### 8.10 Deque - -双端队列,也就是同时提供了堆和栈操作的队列,即同时提供队头队尾插入移除的操作。 -|操作\接口|`Queue`|`Deque`| -|:-|:-:|:-:| -|添加元素到队尾|`add(E e)` / `offer(E e)`|`addLast(E e)` / `offerLast(E e)`| -|取队首元素并删除|`E remove()` / `E poll()`|`E removeFirst()` / `E pollFirst()`| -|取队首元素但不删除|`E element()` / `E peek()`|`E getFirst()` / `E peekFirst()`| -|添加元素到队首|无|`addFirst(E e)` / `offerFirst(E e)`| -|取队尾元素并删除|无|`E removeLast()` / `E pollLast()`| -|取队尾元素但不删除|无|`E getLast()` / `E peekLast()`| - -定义:`public interface Deque extends Queue` - -`Deque`是从`Qeuee`派生的,所以其实也可以用`Queue`的`offer`/`poll`方法,其实就等同与`offerLast`/`pollFirst()`,但如果使用`Deque`接口,还是最好调用它自己的方法,这样更能明确表明自己在做什么事情。 - -实现类: -- `ArrayDeque`,数组实现。 -- `LinkedList`,链表。 - -虽然像`LinkedList`这种实现了`List` `Queue` `Deque`等多个接口,但一般来说我们在使用时总是通过特定的接口来使用它,而不是直接持有一个`LinkedList`,因为持有接口说明代码的抽象层次更高,而接口本身定义的方法代表了特定的用途。 - -**面向抽象编程**:尽量持有接口,而不是具体的实现类。 - -### 8.11 Stack - -栈,即后进先出表,定义: -```java -public class Stack extends Vector { - public Stack() { - } - public E push(E item) { // 压栈 - addElement(item); - - return item; - } - public synchronized E pop() { // 出栈 - E obj; - int len = size(); - - obj = peek(); - removeElementAt(len - 1); - - return obj; - } - public synchronized E peek() { // 取栈顶元素 - int len = size(); - - if (len == 0) - throw new EmptyStackException(); - return elementAt(len - 1); - } - public boolean empty() { // 判空 - return size() == 0; - } - public synchronized int search(Object o) { // 到栈顶距离,栈顶返回1 - int i = lastIndexOf(o); - - if (i >= 0) { - return size() - i; - } - return -1; - } - @java.io.Serial - private static final long serialVersionUID = 1224463164541339165L; -} -``` - -可以看到`Stack`已经是实现类了,而不是接口,派生自`Vector`。 - -`Vector`和`ArrayList`一样,都是`List`的动态数组实现,不同于`ArrayList`的是,它支持多线程的同步,同一时刻只能有一个线程能够写`Vector`,也就是`Vector`是线程安全的,但实现同步需要花费更多代价,所以性能不如`ArrayList`。 - -为什么`Stack`是实现类,而不是接口呢?前面其实也说过,`Vector`和`Stack`都是**历史遗留**的不再推荐使用的类型。因为已经有了名为`Stack`的类,基于兼容性考虑,没有再定义接口。日常使用的话双端队列`Deque`拥有所有栈该有的功能。 - -### 8.12 Iterator - -Java的集合类可以使用`for each`循环: -```java -List list = new ArrayList("hello", "world"); -for (String s : list) { - System.out.println(s); -} -``` -但是实际上,java编译器并不知道如何遍历`List`,能够编译通过的原因是编译器把范围`for`循环改成了普通的`for`循环: -```java -for (Interator it = list.iterator(); it.hasNext(); ) { - System.out.println(it.next()); -} -``` - -使用迭代器的好处在于,调用方总是可以通过统一的方式遍历所有集合,不必关心他们的内部存储结构。如果关心存储结构,那么遍历`ArrayList`就要使用下标,遍历链表就要使用表节点(因为使用下标会有效率问题)。但是使用迭代器可以将这两种方式统一起来,统一形式并且达到最好的效率,只是需要由实现类来实现迭代器的高效遍历访问。 - -先看一下`Interator`的定义: -```java -public interface Iterator { - boolean hasNext(); // 是否存在下一个元素 - E next(); // 得到下一个元素,如果没有则抛出NoSuchElementException - // 移除上一个next返回的元素,每次next只能调用一次remove,不支持该操作抛 - // UnsupportedOperationException,调用前没有调用next抛IllegalStateException - default void remove() { - throw new UnsupportedOperationException("remove"); - } - // 对每一个指定元素执行给定操作,直到结尾,迭代过程中修改元素会导致未定义行为,除非实现类定义了对应的并发策略 - default void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - while (hasNext()) - action.accept(next()); - } -} -``` -非常简单,只需要实现`hasNext`和`next`方法,就可以支持遍历了,如果要支持删除元素,还要实现`remove`,最后的`forEachRemaining`是给`for each`循环来用的,如果不需要支持并发通常不需要再在实现类重写。 - -实现一个简单的可变数组类作为例子: -```java -public class MyArray implements Iterable{ - private Object[] arr = null; - private int size = 0; - public MyArray(int capacity) { - if (capacity > 0) { - arr = new Object[capacity]; - } - else { - arr = new Object[8]; - } - size = 0; - } - public MyArray() { - arr = new Object[8]; - size = 0; - } - public boolean isEmpty() { - return size == 0; - } - public T get(int index) { - checkIndex(index); - return (T)arr[index]; - } - public void set(int index, T obj) { - checkIndex(index); - arr[index] = obj; - } - public void add(T obj) { - if (size == arr.length) { - grow(); - } - arr[size++] = obj; - } - public T removeAt(int index) { - checkIndex(index); - T elem = (T)arr[index]; - for (int i = index+1; i < size; i ++) { - arr[i-1] = arr[i]; - } - arr[size-1] = null; - size--; - return elem; - } - private void grow() { - int oldCapacity = arr.length; - Object[] newArr = new Object[oldCapacity*2]; - for (int i = 0; i < arr.length; i ++) { - newArr[i] = arr[i]; - } - arr = newArr; - } - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IllegalArgumentException("Illegal index: " + index); - } - } - // TODO : other method about equals, hashCode, subArray, searching, sorting, etc. - - @Override - public Iterator iterator() { - return new MyArrayIterator(); - } - - private class MyArrayIterator implements Iterator { - private int index = 0; - private boolean bNext = false; - public MyArrayIterator() { - } - @Override - public boolean hasNext() { - return index < MyArray.this.size; - } - @Override - public T next() { - bNext = true; - return (T)MyArray.this.get(index++); - } - @Override - public void remove() { - if (bNext == false) { - throw new IllegalStateException("there is no last next() called."); - } - MyArray.this.removeAt(--index); - bNext = false; - } - } -} -``` -这只是最小功能简化,正常实现比如`ArrayList`要达到可用需要考虑比较多的东西。此时就可以用`for each`循环或者迭代器去迭代这个类了: -```java -MyArray arr = new MyArray(10); -arr.add("hello"); -arr.add("world"); -arr.add("nice"); -arr.removeAt(1); -for (Iterator it = arr.iterator(); it.hasNext();) { - System.out.println("elem: " + it.next()); - it.remove(); -} -``` - -总结: -- `Iterator`是一种抽象的数据访问模型,好处有: -- 对任何集合都采用一种访问模型。 -- 调用者对集合内部结构一无所知。 -- 集合类返回的`Iterator`对象知道该如何迭代。 - - -### 8.13 Collections - -`Collections`是JDK提供的工具类,同样位于`java.util`包中,注意末尾有s,区别于`Collection`接口。它提供了一系列静态方法,能更方便地操作各种集合。 - -创建空集合: -```java -public static final List emptyList() -public static final Map emptyMap() -public static final Set emptySet() -``` -- 注意返回的空集合是不可变集合,无法向其中添加或者删除元素。 -- 也可以使用各个集合接口提供的`of(T...)`方法来创建空集合,比如`List.of()`和`Collections.emptyList()`就是等价的。 - -创建单元素集合: -```java -public static List singletonList(T o) -public static Map singletonMap(K key, V value) -public static Set singleton(T o) // 单元素Set -``` -单元素集合也是不可变集合,不可添加元素,空集合和单元素集合都是有`Collections`的静态嵌套类实现。 - -排序: -```java -public static > void sort(List list) -public static void sort(List list, Comparator c) -``` -排序会改变元素,所以参数需要是可变的`List`。 - -洗牌: -```java -public static void shuffle(List list) -public static void shuffle(List list, Random rnd) -``` -传入有序的`List`,随机打乱`List`内部元素顺序。 - -不可变集合: - -`Collections`提供了方法将可变集合封装为不可变集合。都是有内部的静态嵌套类实现,实际上是通过创建代理对象,拦截掉所有修改方法实现。 -```java -public static List unmodifiableList(List list) -public static Map unmodifiableMap(Map m) -public static Set unmodifiableSet(Set s) -``` -然而改变原始的可变集合是可以进行修改的,并且会影响到封装后的不可变集合。 - -线程安全集合: -```java -public static List synchronizedList(List list) -public static Map synchronizedMap(Map m) -public static Set synchronizedSet(Set s) -``` -上述方法将线程不安全的集合变为线程安全的集合,Java5开始,引入了更高效的并发集合类,上述的同步方法已经没什么用了。 - -`Collections`还有很多其他方法。 - - -## 9. IO - -输入输出: -- 输入,即是从外部读入数据到内存。例如从磁盘、网络、用户输入等。读到内存之后无非就是用字节数组`byte[]`或者字符数组`char[]`表示。 -- 输出,将数据从内存输出到外部。例如输出到磁盘、网络、屏幕等。输出也就是将`byte[]`或者`char[]`写到文件或者其他位置。 -- `InputStream` / `OutputStream`是以字节为最小传输单位的输入输出流,也称字节流。 -- `Reader` / `Writer`是以字符`char`为最小传输单位的输入输出流,也成字符流。本质上`Reader`和`Writer`就是能够自动编码解码的字节流。`Reader`读取是将字节流解码转换为字符流,`Writer`将数据写入前会先将字符流编码为字节流。 -- 输入输出流是单向流动的。 - -同步IO与异步IO: -- 读写IO是代码必须等待数据返回后才继续执行后续代码,优点是代码简单,缺点是CPU效率低(因为CPU速度远高于IO速度)。 -- 异步IO是指,读写IO时仅发送请求,然后立刻执行后续代码,优点是CPU执行效率高,缺点是代码编写复杂。 - -Java标准库提供了`java.io`同步IO以及`java.nio`异步IO。上述流相关的类都是同步IO的抽象类。这里只讨论同步IO。 - -### 9.1 File - -Java用`java.io.File`来操作文件和目录,构建一个`File`对象需要传入路径。路径可以是绝对或者相对路径,或者绝对路径中使用`..`表示的相对路径。其中路径分隔符,windows中是`\\`,Linux中是`/`。 - -三种路径: -- `getPath` 传入路径 -- `getAbsolutePath` 绝对路径,传入相对路径中的`.`或者`..`不会被展开,而是直接拼接到当前目录上。 -- `getCanonicalPath` 规范路径,展开传入相对路径中的`.`或者`..`,得到文件的绝对路径。 - -因为Windows和Linux路径分隔符不同,在`File`中用静态字段`File.separator`字符串表示。 - -`File`既可以表示文件,也可以表示目录,构建`File`时,即使传入路径不存在,也不会出错,调用`File`对象某些方法时才会真正进行磁盘操作。 - -`File`属性: -```java -public boolean isFile() -public boolean isDirectory() -public boolean isAbsolute() -public boolean isHidden() -``` - -文件读写权限和大小: -```java -public boolean canRead() -public boolean canWrite() -public boolean canExecute() -public boolean exists() -public long length() // 文件大小,如果是目录,返回值不确定 -``` -对目录而言,是否能够执行代表能够列出它包含的文件和子目录。 - -创建删除文件:创建文件是先检查文件,如果不存在则创建一个空文件,检查和创建对文件系统来说是原子操作。 -```java -public boolean createNewFile() throws IOException -public boolean delete() -``` - -临时文件:使用`createTempFile`创建临时对象,如果调用了`deleteOnExit`则会在JVM退出时自动删除。 -```java -public static File createTempFile(String prefix, String suffix) throws IOException -public static File createTempFile(String prefix, String suffix, File directory) -public void deleteOnExit() -``` - -遍历文件和目录: -```java -public String[] list() -public String[] list(FilenameFilter filter) -public File[] listFiles() -public File[] listFiles(FilenameFilter filter) -public File[] listFiles(FileFilter filter) -public static File[] listRoots() // 列出文件系统所有根目录 -``` - -目录操作: -```java -public boolean mkdir() // 只能创建最后一级目录 -public boolean mkdirs() // 如果中间目录不存在也会创建 -public boolean delete() // 目录为空才能成功 -``` - -java标准库还提供了`Path`对象,位于`java.nio.file`包,和`File`对象类似,但操作更为简单。如果需要对目录进行复杂的拼接遍历等操作,使用`Path`对象更为方便。 - -有了`File`类就可以写一个简单的文件操作命令了,简单实现`ls` `mkdir` `touch` `tree` `rm` `cp` `mv`命令,不支持任何选项,只支持字面上的功能,列出当前目录所有文件、创建目录、创建新文件、树形结构列出所有文件、移除文件或目录、复制、移动文件。再来一个简单的命令循环就可以模拟一个简陋至极的`shell`了。 -```java -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Scanner; -import java.util.StringJoiner; - -public class FileOp { - private File curDir = null; - - public FileOp() { - curDir = new File(System.getProperty("user.dir")); - } - - public FileOp(File inputFile) throws IOException { - if (inputFile == null || !inputFile.exists()) { - curDir = new File(System.getProperty("user.dir")); - } else if (inputFile.isDirectory()) { - curDir = new File(inputFile.getParent()); - } else { - curDir = new File(inputFile.getCanonicalPath()); - } - } - - public void run() throws IOException { - if (curDir == null) { - return; - } - - Scanner sc = new Scanner(System.in); - boolean bContinue = true; - do { - System.out.print(curDir.getCanonicalPath() + " > "); - String cmd = sc.nextLine(); - cmd.trim(); - if (cmd.isEmpty()) { - continue; - } - String[] args = cmd.split("[\\s]+"); // 正则表达式,匹配一个或多个空白符 - for (int i = 0; i < args.length; i++) { - args[i].trim(); - } - if (args.length == 0) { - continue; - } - switch (args[0]) { - case "ls": - if (args.length == 1) { - ls(curDir.getPath()); - } else if (args.length == 2) { - ls(args[1]); - } else { - System.out.println("invalid args of ls : " + cmd); - } - break; - case "cd": - if (args.length == 2) { - cd(args[1]); - } else if (args.length >= 3) { - System.out.println("invalid args of cd : " + cmd); - } - break; - case "mkdir": - if (args.length == 2) { - mkdir(args[1]); - } else { - System.out.println("invalid args of mkdir : " + cmd); - } - break; - case "touch": - if (args.length == 2) { - touch(args[1]); - } else { - System.out.println("invalid args of touch : " + cmd); - } - break; - case "tree": - if (args.length == 1) { - tree(curDir.getCanonicalPath()); - } else if (args.length == 2) { - tree(args[1]); - } else { - System.out.println("invalid args of tree : " + cmd); - } - break; - case "rm": - if (args.length == 2) { - rm(args[1]); - } else { - System.out.println("invalid args of rm : " + cmd); - } - break; - case "cp": - if (args.length == 3) { - cp(args[1], args[2]); - } else { - System.out.println("invalid args of cp : " + cmd); - } - break; - case "mv": - if (args.length == 3) { - mv(args[1], args[2]); - } else { - System.out.println("invalid args of mv : " + cmd); - } - break; - case "exit": - bContinue = false; - break; - default: - System.out.println("invalid args : " + cmd); - break; - } - } while (bContinue); - sc.close(); - } - - public void ls(String lsFile) { - lsFile = realToAbs(lsFile); - File f = new File(lsFile); - if (!f.exists()) { - System.out.println("non-exist file or directories : " + lsFile); - } else if (f.isFile()) { - System.out.println(f.getName()); - } else { - File[] files = f.listFiles(); - StringJoiner sj = new StringJoiner(" "); - for (int i = 0; i < files.length; i++) { - if (files[i].isFile()) { - sj.add(files[i].getName()); - } else { - sj.add(files[i].getName() + "/"); - } - } - if (files.length > 0) { - System.out.println(sj); - } - } - } - - public void cd(String cdDir) { - cdDir = realToAbs(cdDir); - File f = new File(cdDir); - if (f.exists() && f.isDirectory()) { - curDir = f; - } else { - System.out.println("invalid directory path : " + cdDir); - } - } - - public void mkdir(String mkDir) { - mkDir = realToAbs(mkDir); - File f = new File(mkDir); - if (f.isDirectory()) { - System.out.println("directory already exists : " + mkDir); - } else if (f.isFile()) { - System.out.println("a same name file already exists : " + mkDir); - } else if (!f.mkdir()) { - System.out.println("failed to mkdir : " + mkDir); - } - } - - public void touch(String newFile) { - newFile = realToAbs(newFile); - File f = new File(newFile); - if (f.isDirectory()) { - System.out.println("a same neme directory already exists : " + newFile); - } else if (f.isFile()) { - System.out.println("file alredy exists : " + newFile); - } else { - try { - if (!f.createNewFile()) { - System.out.println("failed to create new file : " + newFile); - } - } catch (IOException e) { - System.out.println("failed to create new file : " + newFile); - } - } - } - - public void tree(String inputFile) { - inputFile = realToAbs(inputFile); - File f = new File(inputFile); - if (!f.exists()) { - System.out.println("file or directory does not exist : " + inputFile); - } else if (f.isFile()) { - ls(inputFile); - } else if (f.isDirectory()) { - System.out.println(inputFile); - printFileOrDirWithTreeFormat(f, 0); - } - } - - private void printFileOrDirWithTreeFormat(File f, int indent) { - for (int i = 0; i < indent; i++) { - System.out.print(" "); - } - if (f.isDirectory()) { - System.out.println(f.getName() + "/"); - File[] files = f.listFiles(); - for (File tmpFile : files) { - printFileOrDirWithTreeFormat(tmpFile, indent + 1); - } - } else if (f.isFile()) { - System.out.println(f.getName()); - } - } - - public void rm(String inputFile) { - inputFile = realToAbs(inputFile); - File f = new File(inputFile); - if (f.exists()) { - if (!f.delete()) { - System.out.println("fialed to delte file or directory : " + inputFile); - } - } else { - System.out.println("file or directory does not exist : " + inputFile); - } - } - - public void cp(String fromFile, String toFile) { - File f = new File(realToAbs(fromFile)); - File fto = new File(realToAbs(toFile)); - if (fto.exists()) { - System.out.println("destination file or directory already exists : " + toFile); - } else if (f.exists()) { - try { - Files.copy(f.toPath(), fto.toPath()); - } catch (IOException e) { - System.out.printf("fialed to copy %s to %s\n", fromFile, toFile); - } - } else { - System.out.println("source file does not exist : " + fromFile); - } - } - - public void mv(String fromFile, String toFile) { - File f = new File(realToAbs(fromFile)); - File fto = new File(realToAbs(toFile)); - if (fto.exists()) { - System.out.println("destination file or directory already exists : " + toFile); - } else if (f.exists()) { - if (!f.renameTo(fto)) { - System.out.printf("failed to move %s to %s\n", fromFile, toFile); - } - } else { - System.out.println("source file does not exist : " + fromFile); - } - } - - // common logic - private String realToAbs(String path) { - path.replace('/', File.separatorChar); - path.replace('\\', File.separatorChar); - Path p = Path.of(path); - if (!p.isAbsolute()) { - path = curDir.getPath() + File.separator + p; - } - return path; - } -} -``` -虽然极端简陋,但也具备了最基本的文件操作可用性了,类UNIX系统中每个命令都支持多个选项,功能丰富太多了。TODO:有空时阅读Linux系统的简单命令实现源码。 - -### 9.2 InputStream - -输入流: -```java -public abstract class InputStream implements Closeable { - private static final int MAX_SKIP_BUFFER_SIZE = 2048; - private static final int DEFAULT_BUFFER_SIZE = 8192; - public InputStream() {} - public static InputStream nullInputStream() { ... } // 返回一个打开的不读取任何自己的匿名派生类对象 - public abstract int read() throws IOException; // 从输入流中读取下一个字节,返回0~255,到达了流的末尾则返回-1 - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } // 读取固定长度字节到数组中,返回实际读取到的字节数,到达了流的末尾返回-1 - public int read(byte b[], int off, int len) throws IOException { - Objects.checkFromIndexSize(off, len, b.length); - if (len == 0) { - return 0; - } - - int c = read(); - if (c == -1) { - return -1; - } - b[off] = (byte)c; - - int i = 1; - try { - for (; i < len ; i++) { - c = read(); - if (c == -1) { - break; - } - b[off + i] = (byte)c; - } - } catch (IOException ee) { - } - return i; - } // 读取固定长度字节到数组,返回实际读取到的长度,鼓励在派生类中重写为更高效的实现 - private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8; - public byte[] readAllBytes() throws IOException { - return readNBytes(Integer.MAX_VALUE); - } // 读取输入流所有字节的方便实现,不鼓励用来读取量非常大的数据(会阻塞线程+大量内存占用) - public byte[] readNBytes(int len) throws IOException { ... } // 读取固定长度字节 - public int readNBytes(byte[] b, int off, int len) throws IOException { } // 读取固定长度字节 - public long skip(long n) throws IOException { ... } // 跳过n个字节,返回实际跳过的字节数 - public void skipNBytes(long n) throws IOException { ... } // 跳过n字节,不够n字节抛异常 - public int available() throws IOException { - return 0; - } // 子类应该重写,返回可用的字节数,返回估计流中还剩余的字节数,可能并不准确,不要用返回结果去分配内存来存储所有数据 - public void close() throws IOException {} // 关闭流释放资源,子类应该重写 - public synchronized void mark(int readlimit) {} // 标记一个位置,reset时将流重新定位到这个位置 - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - public boolean markSupported() { - return false; - } - public long transferTo(OutputStream out) throws IOException { ... } // 输入流数据转移到输出流 -} -``` - -`InputStream`是一个抽象类,最重要的就是`read`相关的读取方法,读取完之后需要`close`(来自`Closeable extends AutoCloseable`), - -计算机中,文件、网络端口等资源由操作系统管理,应用程序运行中,可能会出现IO错误,比如文件没有读写权限,不存在等情况,底层错误由虚拟机封装为`IOException`抛出,所以所有IO操作都必须正确处理`IOException`。并且需要关闭流以释放系统资源。 - -用`try-finally`保证无论是否发生IO错误流都能够关闭是一种常见写法: -```java -InputStream is = null; -try { - is = new FileInputStream("readme.txt"); - while (true) { - int n = is.read(); - if (n != -1) { - System.out.println(n); - } - else { - break; - } - } -} finally { - if (is != null) { - is.close(); - } -} -``` - -这样会有一点繁琐,更好的写法是使用Java7引入的`try(resource)`语法,只需要写`try`让编译器自动为我们关闭资源。 -```java -try (InputStream is = new FileInputStream("readme.txt")) { - while (true) { - int n = is.read(); - if (n != -1) { - System.out.println(n); - } - else { - break; - } - } -} // 编译器自动在此处添加finally并调用close -``` - -实际上编译器只看`try(resource = ...)`中的对象是否实现了`java.lang.AutoCloseable`,如果实现了就自动加上`finally`并`close`。 - - -**缓冲**:读取流时一次读一个字节并不高效,一次性读取多个字节到缓冲区往往比一次一个字节高效很多,`InputStream`提供了多个`read`和相关接口来读取多个字节到字节数组。 - -**阻塞**:同步IO在读取时会阻塞,也就是说`read`语句会读取到数据之后才返回执行下一条语句,读取IO的操作相比普通的计算操作速度会慢很多。 - -**实现**:`InputStream`是一个抽象类,具体的实现在实现类中,`FileInputStream`获取文件输入流就是一个典型。此外`ByteArrayInputStream`可以在内存中模拟一个输入流,实际上就是把数组变成流,实际应用不多,可以用在测试时构造一个输入流。 - -```java -byte[] b = new byte[] {1, 100, 101}; -try (InputStream is = new ByteArrayInputStream(b)) { - while (true) { - int n = is.read(); - if (n != -1) { - System.out.println(n); - } - else { - break; - } - } -} -``` - -### 9.3 OutputStream - -类似于`InputStream`,输出流也是抽象类,最基本方法是`write`。 -```java -public abstract class OutputStream implements Closeable, Flushable { - public OutputStream() {} - public static OutputStream nullOutputStream() { ... } // 得到一个丢弃所有字节的打开的输出流 - public abstract void write(int b) throws IOException; // 写一个字节到输出流,只写低8字节,高24字节忽略 - public void write(byte b[]) throws IOException { // 写多个字节 - write(b, 0, b.length); - } - public void write(byte b[], int off, int len) throws IOException { - Objects.checkFromIndexSize(off, len, b.length); - // len == 0 condition implicitly handled by loop bounds - for (int i = 0 ; i < len ; i++) { - write(b[off + i]); - } - } - public void flush() throws IOException { - } // 如果实现类中缓冲了已写的字节,那么这个接口的调用会将缓冲的字节实际交给操作系统去写 - public void close() throws IOException { - } -} -``` - -和`InputStream`一样,需要关闭和处理IO错误,`write`时同样会阻塞。 -```java -try (OutputStream os = new FileOutputStream("readme.txt")) { - os.write("how are you!".getBytes("utf-8")); -} -``` - -其实`InputStream`和`OutputStream`都有缓冲区,只是`InputStream`的缓冲区不会被感知到,打开输入流时,操作系统会一次性读取若干字节到缓冲区,`read`读完之后会再次读取并填满缓冲区。而`OutputStream`的缓冲区会被感知到,因为缓冲区不满时操作系统并不会真正去执行IO操作,所以提供了`flush`给我们去手动刷新缓冲区,当然缓冲区满了或者关闭输出流时都会自动调用`flush`。如果是文件输出流可能影响不大,但如果是网络输出流那可能就需要视场景调用`flush`了。 - -实现类: -- `FileOutputStream`文件输出流。 -- `ByteArrayOutputStream`字节数组输出流可以在内存中模拟一个`OutputStream`。 - -复制文件: -```java -public static void copyFile(String src, String dest) throws FileNotFoundException,IOException { - try(InputStream is = new FileInputStream(src); OutputStream os = new FileOutputStream(dest)) { - is.transferTo(os); - } -} -``` -将流内容读取为字符串: -```java -public static String readAsString(InputStream is) throws IOException { - StringBuilder sb = new StringBuilder(); - int n = 0; - while ((n = is.read()) != -1) { - sb.append((char)n); - } - return sb.toString(); -} -``` - -### 9.4 Filter - -某些时候可能需要给输入输出流添加其他的功能,可以选择从`InputStream`或者`OutputStrem`派生一个类来实现,比如添加缓冲、加密解密、计算签名功能。但如果需要同时支持其中的多项功能呢?那又需要再实现派生类,因为不允许多继承那不知道要实现多少类了。为了解决依赖继承会导致子类数量爆炸的问题,JDK将`InputStream`分为两类。 -- 一类是直接提供数据的流:`FileInputStream` `ByteArrayInputStream` `ServletInputStream` etc -- 一类是提供额外附加功能的流:`BufferedInputStream` `DigestInputStream` `CipherInputStream` etc - -当我们希望给一个流提供其他功能,比如提供缓冲来提高读取效率,这时候就用`BufferedInputStream`来包装这个类。 -```java -InputStream file = new FileInputStream("test.gz"); -InputStream buffered = new BufferedInputStream(file); -``` - -可以多次包装,无论包装多少次,得到的流都是`InputStream`,直接用`InputStream`来引用它就可以正常读取。 - -``` -InputStream -|__FileInputStream -|__ByteArrayInputStream -|__ServletInputStream -|__FilterInputStream // 包装类基类,只做包装,不干任何其他事情 - |__BufferedInputStream - |__DataInputStream - |__CheckedInputStream -``` -输出流类似。这种通过一个基础组件再叠加各种附加功能组件的模式称之为**装饰器模式**(Decorator)。让我们可以通过少量类来实现各种功能的组合。 - -叠加多个`FilterInputStream`时,只需要持有最外层的`InputStream`,最外层的`InputStream`关闭时,内层的`InputStream`的`close`方法也会被调用。其实就是在`FilterInputStream`中保存了传入的`InputStream`,然后进行转调。 - -实现一个自己的`FilterInputStream`以统计读取的总字节数: -```java -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class CountInputStream extends FilterInputStream { - private int count = 0; - protected CountInputStream(InputStream in) { - super(in); - } - @Override - public int read() throws IOException { - int n = in.read(); - if (n != -1) - count ++; - return n; - } - @Override - public int read(byte b[], int off, int len) throws IOException { - int n = in.read(b, off, len); - if (n != -1) { - count += n; - } - return n; - } - public int getReadCount() { - return count; - } -} -``` - -使用: -```java -try (InputStream is = new FileInputStream("readme.txt"); CountInputStream cis = new CountInputStream(is)) { - System.out.println(readAsString(cis)); - System.out.println(cis.getReadCount()); -} -``` - -### 9.5 Zip - -`ZipInputStream`是一种`FilterInputStream`,提供了直接读取zip包的功能。层次结构: -``` -InputStream -|__FilterInputStream - |__InflaterInputStream - |__ZipInputStream - |__JarInputStream -``` - -`jar`包本身就是zip压缩文件,`JarInputStream`相对`ZipInputStream`增加的功能主要是用来读取jar中的`MANIFEST.MF`。其中`InflaterInputStream`和`ZipInputStream`定义在`java.util.zip`包,`JarInputStream`定义在`java.util.jar`包。 - -`java.util.zip`包中定义了zip/gzip相关的压缩文件条目、输入输出流、校验和等相关类。 - -示例,当然有了读取到了数据再利用文件输出流就可以实现解压缩了: -```java -public static void readZip(String zipfile) throws FileNotFoundException, IOException { - try (ZipInputStream zip = new ZipInputStream(new FileInputStream(zipfile))) { - ZipEntry entry = null; - while ((entry = zip.getNextEntry()) != null) { - System.out.println(entry.getName()); - if (!entry.isDirectory()) { - StringBuilder sb = new StringBuilder(); - int n; - while ((n = zip.read()) != -1) { - sb.append((char)n); - } - System.out.println(sb.toString()); - } - } - } -} -``` - -`ZipInputStream`通常用循环`getNextEntry`来遍历压缩包内所有文件,其中`ZipEntry`用来表示其中的一个压缩条目,用`isDirectory`判断是目录还是文件,判断依据是`getName`获取到的名称是否是以`/`结尾的。除了这两个常用方法,还有获取校验和,创建读取修改时间等相关方法。 -```java -public boolean isDirectory() { - return name.endsWith("/"); -} -``` - -写入Zip包则需要调用`ZipOutputStream`,派生关系类似于`ZipInputStreram`,通常是包装一个`FileOutputStream`,没写入一个文件,先调用`putNextEntry()`,然后用`write`写入`byte[]`数据,写入完毕后调用`closeEntry`结束文件打包。 - -添加压缩文件,需要注意`ZipEntry`的名称: -```java -public static void createZip(String zipfile, File[] inputFiles) throws FileNotFoundException, IOException { - try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipfile))) { - for (File file : inputFiles) { - if (file != null && file.exists()) { - writeFileToZip(zip, file, ""); - } - } - } -} - -public static void writeFileToZip(ZipOutputStream zip, File file, String prefix) throws IOException { - if (file.exists()) { - if (file.isDirectory()) { - String entryName = prefix + file.getName() + "/"; - zip.putNextEntry(new ZipEntry(entryName)); - File[] files = file.listFiles(); - if (files != null) { - for (File tmpFile : files) { - writeFileToZip(zip, tmpFile, entryName); - } - } - } - else { - zip.putNextEntry(new ZipEntry(prefix + file.getName())); - try (InputStream is = new FileInputStream(file)) { - zip.write(is.readAllBytes()); - } - } - } -} -``` - -利用ZIP流和文件流,实现压缩和解压缩ZIP文件就是这么简单。 - -TODO:了解zip以及常见的压缩算法. - -### 9.6 读取classpath的资源 - -如果我们读取一个配置文件、资源文件,需要指定它的路径,如果文件不在项目里面,那么需要根据当前路径指定相对路径或者直接用绝对路径。如果这个文件在`classpath`里面,最终随着`jar`文件一起打包,那么读取时就只需要指定它在`classpath`中的路径即可。 - -在`classpath`中的资源文件,路径总是以`/`开头,获取到当前`Class`对象,然后调用`getResourceAsStream`就可以直接从`classpath`读取任意资源。 - -在Eclipse中,最终资源应该放在`bin`目录下,`/hello.txt`资源代表的就是`bin/hello.txt`资源,最终打包后就是`jar`包根目录中的`hello.txt`。TODO:理清楚Eclipse工程、classpath等配置的详细含义。 - -例: -```java -public static void readClassPathResFile(String path) throws IOException { - try (InputStream is = ResourceHelper.class.getResourceAsStream(path)) { - if (is != null) { - System.out.println(readStreamAsString(is)); - } - } -} -public static String readStreamAsString(InputStream is) throws IOException { - int n; - StringBuilder sb = new StringBuilder(); - while ((n = is.read()) != -1) { - sb.append((char)n); - } - return sb.toString(); -} -``` -调用: -```java -ResourceHelper.readClassPathResFile("/hello.txt"); -``` - -如果没有该资源,`getResourceAsStream`返回的`InputStream`是空的,需要检查。 - -### 9.7 序列化 - -序列化(serialization, serialize)就是把Java对象变成二进制内容的过程,本质上就是一个`byte[]`。有序列化就有反序列化(deserialization, deserialize),也就是把二进制内容`byte[]`变回Java对象。 - -为什么要序列化:序列化后方便存储到磁盘、通过网络传输,通过反序列化再还原回一个Java对象。 - -一个对象要能序列化,必须要实现一个特殊的接口:`java.io.Serializable`接口。这个接口没有任何方法,只起到一个标记的作用,这样的接口被称为“标记接口”。 - -要把一个对象序列化为`byte[]`,需要使用`ObjectOutputStream`,负责将一个Java对象写入字节流。这个接口既可以写入内置类型`int` `double`等,可以以utf-8编码写入String,也可以写入实现了`Serializable`接口的对象。 - -同理反序列化则使用`ObejctInputStream`从`byte[]`读取一个`Java`对象。调用`readObject`读取到一个`Obejct`之后做强制类型转换为特定类型。 - -相关重要方法: -```java -public class ObjectOutputStream - extends OutputStream implements ObjectOutput, ObjectStreamConstants -{ - public final void writeObject(Object obj) throws IOException {} - public void writeBoolean(boolean val) throws IOException {} - public void writeByte(int val) throws IOException {} - public void writeShort(int val) throws IOException {} - public void writeChar(int val) throws IOException {} - public void writeInt(int val) throws IOException {} - public void writeLong(long val) throws IOException {} - public void writeFloat(float val) throws IOException {} - public void writeDouble(double val) throws IOException {} - public void writeBytes(String str) throws IOException {} - public void writeChars(String str) throws IOException {} - public void writeUTF(String str) throws IOException {} -} - -public class ObjectInputStream - extends InputStream implements ObjectInput, ObjectStreamConstants -{ - public final Object readObject() {} - public byte readByte() throws IOException {} - public int readUnsignedByte() throws IOException {} - public char readChar() throws IOException {} - public short readShort() throws IOException {} - public int readUnsignedShort() throws IOException {} - public int readInt() throws IOException {} - public long readLong() throws IOException {} - public float readFloat() throws IOException {} - public double readDouble() throws IOException {} - @Deprecated - public String readLine() throws IOException {} - public String readUTF() throws IOException {} -} -``` - -`readObejct`反序列化是可能抛出异常有: -- `ClassNotFoundException` 未找到对应类。 -- `InvalidClassException` 类型不匹配。`extends ObjectStreamException extends IOException` - -对于`InvalidClassException`常见情况是类的定义可能发生了细微改变,比如一个字段类型由`int`改为`long`导致不兼容。为了避免这种不兼容,Java的序列化允许对象定义一个特殊的`serialVersionUID`静态字段。用于标识类的序列化版本,通常可由IDE自动生成。如果修改了实例字段,则需要修改这个ID。这样就可以自动阻止不匹配的class版本。 - -在Eclipse中,实现了`Serializable`接口后,鼠标放在类名上面,就可以看到生成序列化ID的选项,一种是添加自动生成的使用类名、接口名、成员方法即属性来生成的一个64位的哈希字段,一种是缺省的比如`1L`。 -```java -private static final long serialVersionUID = -5351371831194389028L; -``` - -反序列化并不会调用构造函数,而是直接用数据填充这个对象的字段,正式因为这一点,Java的反序列化机制可以不经过构造方法就可以直接创建对象,所以可能存在安全隐患。 - -另外静态字段不会序列化,使用`transient`修饰的实例字段不会参与序列化,一般用于那种可以通过已有字段重新计算得到的字段或者业务需要不应该做序列化的字段,标准库里面经常可以看到。 -```java -private transient long fastTime; -``` - -所以Java本身提供的基于对象的序列化和反序列化既存在安全性问题,又存在兼容性问题。更好的序列化方法是通过Json这样的通用数据结构来实现。如果需要与其他语言交换数据,也必须用通用的序列化方法比如Json。 - -### 9.8 Reader - -`Reader`是`java.io`的另一个输入流接口,代表字符输入流。读取单位是`char`,和`InputStream`接口的方法可以说一模一样,只是基本数据类型由`byte`换成了`char`。 -```java -public int read() throws IOException -public int read(char cbuf[]) throws IOException -``` -上面两个接口前者返回`0~65535`,或者到达流末尾返回`-1`,后者返回读取字符数。 - -示例: -```java -try (Reader reader = new FileReader("hello.txt", StandardCharsets.UTF_8)) { - int n; - StringBuilder sb = new StringBuilder(); - while ((n = reader.read()) != -1) { - sb.append((char)n); - } - System.out.println(sb); -} -``` -此时`hello.txt`中的中文就可以被识别了。纯文本文件的读取是与编码相关的,`FileReader`构造时最好指定编码,如果不指定会使用默认编码。 - -和`InputStream`一样,`Reader`也是资源,也需要关闭,所以最好使用`try (resource)`。 - -`java.io`也提供了同`ByteArrayInputStream`类似的`CharArrayReader`,使用一个`char[]`在内存中模拟一个`Reader`: -```java -Reader rd = new CharArrayReader("hello".toCharArray()); -``` - -`StringReader`可以直接把`String`作为数据源,和`CharArrayReader`几乎一模一样。 -```java -Reader rd = new StringReader("hello"); -``` - -实际上,处理特殊的`CharArrayReader`和`StringReader`,普通的`Reader`都是基于`InputStream`构造的。因为`Reader`也需要读取字节,然后再解码成字符流。比如`FileReader`: -```java -public class FileReader extends InputStreamReader { - public FileReader(String fileName) throws FileNotFoundException { - super(new FileInputStream(fileName)); - } - public FileReader(File file) throws FileNotFoundException { - super(new FileInputStream(file)); - } - public FileReader(FileDescriptor fd) { - super(new FileInputStream(fd)); - } - public FileReader(String fileName, Charset charset) throws IOException { - super(new FileInputStream(fileName), charset); - } -} -``` -可以看到`FileReader`基本没有做任何事情,解码和读取都是在基类做的。 - -作为`Reader`和`InputStream`之间的桥梁的就是`InputStreamReader`类,`InputStreamReader`接受一个`InputStream`和一个编码,负责解码工作: -```java -public class InputStreamReader extends Reader { - // constructors - private final StreamDecoder sd; - public String getEncoding() { - return sd.getEncoding(); - } - public int read() throws IOException { - return sd.read(); - } - public int read(char cbuf[], int offset, int length) throws IOException { - return sd.read(cbuf, offset, length); - } - public boolean ready() throws IOException { - return sd.ready(); - } - public void close() throws IOException { - sd.close(); - } -} -``` -具体的解码工作由底层的一个`StreamDecoder`完成,不用太关注细节。使用时只需要传入`InputStream`和编码即可。 - -下面的写法逻辑上是等价的: -```java -Reader reader1 = new InputStreamReader(new FileInputStream("hello.txt"), "GBK"); -Reader reader2 = new FileReader("hello.txt", Charset.forName("GBK")); -``` -注意标准字符集`StandardCharsets`中是没有`GBK`编码的常量的,所以需要使用字符串表示或者使用`Charset.forName`来创建。 - -### 9.9 Writer - -`Writer`同理就是`OutputStream`加上一个解码的功能。主要方法: -```java -public void write(int c) throws IOException -public void write(char cbuf[]) throws IOException -public void write(String str) throws IOException -``` - -主要派生类: -- `FileWriter` -- `CharArrayWriter` -- `StringWriter` - -同理`OutputStream`和`Writer`之间的桥梁是`OutputStreamWriter`负责输出到流并进行字符串编码。使用方法同`Reader`: -```java -try (Writer wt = new FileWriter("hello.txt", Charset.forName("GBK"))) { - wt.write("你好呀"); -} -``` - -TODO:分析了解底层`StreamDecoder`和`StreamEncoder`字符串编解码的实现。 - -### 9.10 PrintStream & PrintWriter - -PrintStream是一种`FilterStream`,在`OutputStream`的基础上提供了各种写入数据的方法: -```java -public void print(char c) { - write(String.valueOf(c)); -} -public void print(int i) { - write(String.valueOf(i)); -} -public void print(long l) { - write(String.valueOf(l)); -} -public void print(float f) { - write(String.valueOf(f)); -} -public void print(double d) { - write(String.valueOf(d)); -} -public void print(char s[]) { - write(s); -} -public void print(String s) { - write(String.valueOf(s)); -} -public void print(Object obj) { - write(String.valueOf(obj)); // (obj == null) ? "null" : obj.toString(); -} -public void println() { - newLine(); -} -// other println ... -public PrintStream printf(String format, Object ... args) { - return format(format, args); -} -``` -`PrintStream`与`OutputStream`相比提供了一组`print/println`方法用来打印各种数据类型,而且不会抛出`IOException`,编写代码时,不必处理异常。 - -我们常用的`System.out`就是`PrintStream`类型。`PrintStream`是一种`FilterOutputStream`,所以最终输出的是`byte`数据。 - -`PrintWriter`则是扩展了`Writer`接口,包装一个`Writer`并提供类似的`print/println/printf`等打印函数,最终输出`char`数据。 - -另外`Reader`和`Writer`也是可以由`FilterReader`和`FilterWriter`来装饰的,不赘述。 - -`System`类提供标准输入输出、获取外部环境变量等能力: -```java -public final class System { - public static final InputStream in = null; - public static final PrintStream out = null; - public static final PrintStream err = null; - private static volatile SecurityManager security; // read by VM - private static volatile Console cons; -} -``` -其中: -- `in` 标准输入 -- `out` 标准输出 -- `err` 标准错误输出 - -类似于C语言中的`stdin` `stdout` `stderr`,非常好理解。 - -### 9.11 工具 - -使用`java.nio.file.Files`和`java.nio.file.Paths`这两个工具类,可以方便的处理文件操作和路径操作。 - -`Files`提供了诸如删除、拷贝、移动、判断是否存在、读取文件数据等操作。读取数据的话如果文件过大,建议还是使用文件流进行操作。 - -`Paths`提供的方法不多,主要的路径操作在`Path`类本身。 - -## 10. 日期和时间 - -### 10.1 基本概念 - -日期: -- `2021-5-2` -- `2012-12-21` - -时间: -- `20:30:28` -- `2021-5-2 21:26:48` - -时间可以带日期可以不带日期,只有带日期才能准确地表示一个**时刻**。 - -**时区与本地时间** - -世界上的不同时区,同一时刻,时间表示是不同的。中国国内都用北京时间,也就是东八区时间。 - -光靠时间和日期无法唯一确定一个时刻,还需要一个时区。世界上有24个时区,相邻时区时间相差1个小时。以伦敦格林尼治本初子午线为0时区的中心,往东从东1区到东12区,往西西1区到西12区,东西12时区合并做一个时区,所以共24时区,东西12时区的中心就是国际日变更线。时区是1个时区15度,但是时区并不严格按照经度划分,在海洋上基本上是按照经度划分,在陆地上按照国界线或者其他界线进行了划分,比如中国在都是统一用北京的东八区时间,全部划分到了东八区。 - -地球是自西往东转的,东边比西边更早看到太阳。同一时刻,西区比东区时间要早(也就是时间更小,更慢),比如北京东八区例如正午12点时英国伦敦0时区慢8小时就应该是凌晨4点,美国纽约西5区比伦敦再慢5小时,就应该是日期早一天的23点(简捷算法:12-(8-(-5)) = -1, 即前一天的23:00)。 - -如果穿越了国际日界线,从东12区往西12区的话(横跨太平洋中国到美国的方向),东区快就应该将时间倒退一天,从西12区到东12区(横跨太平洋美国到中国的方向),西区慢就应该将时间前进一天。 - -表示本地时间就需要加上时区,时区有几种表示方式。 -- 一种是`GMT`或者`UTC`加上时区偏移,例如`GMT+08:00`/`UTC+08:00`表示北京东八区,美国纽约西五区就是`GMT-05:00`/`UTC-05:00`。 - -其中GMT是格林尼治标准时间(Greenwich Mean Time),GMT时间定义一天就是24小时,一小时60分钟,一分钟60秒,所以一天就是86400秒。但是地球并不是自始至终保持恒定不变的速度自转,而是在缓慢减速的,这样定义会导致一秒钟变得越来越长,所以格林尼治时间不再作为标准时间。而是使用UTC(Coodinated Universal Time, 协调世界时间),UTC使用原子时,一秒定义为,铯-133原子基态的两个超精细能级间在零磁场下辐射跃迁9,192,631,770周所持续的时间。【仿佛回到了大学上半导体物理的时光,时间一去不复返,非常惭愧的是大学学的物理也全都还给老师了】所以使用UTC的一秒就被固定了,就会导致每一年用秒来计算的时间会越来越长,就需要在某些年份进行闰秒(负闰秒,最后一分钟为59秒,正闰秒则是最后一分钟为61秒),规定当时间时和原子时相差超过正负0.9秒时就需要进行闰秒。目前,全球已经进行了27次闰秒,均为正闰秒,最近一次是北京时间2017年1月1日7时59分59秒,时钟显示为07:59:60。 - -用GMT和UTC来表示时区时可以认为是等价的,UTC每过几年会闰秒,开发程序时可以忽略,计算机的时钟联网时会自动与时间服务器同步。不过貌似有闰秒后电子设备不工作的传闻,不知真假,就是没有考虑到这种情况的原因。 - -- 另一中时区是使用洲/城市来表示,例如`Asia/Shanghai`,表示上海所在时区,需要注意城市名称不是任意的,而是由国际标准组织规定的城市。 - -**夏令时** - -夏令时(Daylight Saving Time, DST)的意思是在天亮早的夏季将时间调快一小时,本意是使人早睡早起,减少照明,节约用电。大概的操作夏季开始时讲时间调快一小时,夏季结束时调慢一小时,不同地区夏令时开始和结束时间节点还不一样。 - -比如前面提到的例子,东八区的12点,如果在今天(5月2日)夏令时期间,由于伦敦和纽约都实行夏令时,伦敦就是凌晨5点,纽约就是夜晚0点。 - -全世界不同地区可能实行起止时间不同的夏令时,这使得计算就变得繁琐且非常容易出错。理解还是很好理解的,但你如果要我去算那还是饶了我吧。记住一点:**计算夏令时应该使用标准库提供的类,而不是自己计算。** - -**本地化** - -在计算机中,通常使用`Locale`表示一个国家或地区的日期、时间、数字、货币等格式。`Locale`由`语言_国家`的字母缩写构成。例如`zh_CN`表示中国,`en_US`表示美国,语言用小写,国家用大写。对于日期不同地区表示可能不同,如: -- `zh_CN: 2020-05-02` -- `en_US: 05/02/2020` - -计算机用`Locale`在日期、时间、货币和字符串之间进行转换。 - -### 10.2 时间戳 - -我们知道不同的时区表示同一个时刻时间表示也不同,但同一个时刻只需要一个整数就可以精确表示,我们称之为**Epoch Time**,定义为1970年1月1日零点(格林尼治时间/GMT+00:00)到现在所经历的秒数。 - -根据这个整数给定时区就可以算出当前时区当前时刻的时间表示。Epoch Time也成为了时间戳(Time Stamp),不同编程语言中会有不同的存储方式: -- 以秒为单位的整数,缺点是只能精确到秒。 -- 以毫秒为单位的整数,最后三位表示毫秒。 -- 秒为单位的浮点数,小数点后表示零点几秒。 - -他们之间的转换很简单,Java中,时间戳通常是用64位整数long表示的毫秒数。使用`System.currentTimeMillis()`获取: -```java -long t = System.currentTimeMillis(); -``` - -有了这个时刻之后我们自己都可以来算现在的时间了,只要用上小学学到的闰年和大小月,考虑已经进行了的闰秒(时间戳没有累加闰秒,所以不需要考虑),转到东八区,就可以得到一个当前机器的毫秒级精确的北京时间。 -```java -public class BeiJingTime { - private int year; - private int month; - private int day; - private int hour; - private int min; - private int sec; - private int milliSec; - - public BeiJingTime(int y, int m, int d, int h, int mi, int s, int ms) { - year = y; - month = m; - day = d; - hour = h; - min = mi; - sec = s; - milliSec = ms; - } - - @Override - public String toString() { - return year + "-" + month + "-" + day + " " + hour + ":" + min + ":" + sec + "." + milliSec + " UTC+08:00"; - } - - public static BeiJingTime of(long timeMillis) { - long t = timeMillis; - long milliSec = t % 1000; - t = t / 1000; // do not need to accumulate leap seconds - long day = t / 86400; - long reminder = t % 86400; - long hour = reminder / 3600 + 8; // to UTC+08:00 - long min = (reminder % 3600) / 60; - long sec = reminder % 60; - - int from = 1970; - int count = 0; - while (day > 0) { - int curYearDay = 365; - if (isLeapYear(from + count)) { - curYearDay = 366; - } - if (day - curYearDay < 0) { - break; - } - day -= curYearDay; - count++; - } - int curYear = from + count; - int curMonth = 1; - while (day > 0) { - int curMonthDay = getMonthDay(curYear, curMonth); - if (day - curMonthDay < 0) { - break; - } - day -= curMonthDay; - curMonth++; - } - day += 1; - return new BeiJingTime(curYear, curMonth, (int)day, (int)hour, (int)min, (int)sec, (int)milliSec); - } - - public static Boolean isLeapYear(int year) { - return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; - } - - public static int getMonthDay(int year, int month) { - if (month <= 0 || month > 12) { - throw new IllegalArgumentException("Unexpected month: " + month); - } - int curMonthDay = 30; - switch (month) { - case 1: case 3: case 5: case 7: case 8: case 10: case 12: - curMonthDay = 31; - break; - case 2: - curMonthDay = 28; - if (isLeapYear(year)) { - curMonthDay = 29; - } - break; - } - return curMonthDay; - } - - public static void main(String[] args) throws InterruptedException { - while (true) { - Thread.sleep(200); - System.out.println(BeiJingTime.of(System.currentTimeMillis())); - } - } -} -``` -`Thread.sleep(200)`就是当前线程睡眠200毫秒,多线程相关内容后续详解。 - -这里算的就是UTC时间,UTC的话应该要考虑闰秒,但是Epoch Time是不计算闰秒的,所以我们可以直接忽略闰秒的存在,得到的就是当前机器时间戳算出来的标准时间。通过比较和我本地电脑的时间是完全一致的,但不同设备本身保存的这个时间戳由于同步关系可能会有几秒的差距,比如我的手机比电脑就慢2秒左右。但同一个设备不同软件、不同编程语言只要取同一个时间戳计算无误的话那时间肯定应该是完全一致的。 - -按照平年365天来算,不考虑几年才进行一次的闰秒,一年就是365*86400=31536000秒。一般来说这个系统时间戳使用整数来表示,如果是用32位整数表示的话比如Unix时间戳,因为整数带符号,最大能表示2147483648,也就是68年左右,也就是2038年之后这个时间戳就会溢出变为负数。有人预测2038年之后诸如Linux、IOS等的类Unix系统就会发生时间倒退、不能启动等现象,但现在毕竟还没有到。以后应该会解决这个问题,在现在64位设备已经全面普及的当下,将这个时间戳从底层改为`int64_t`之类的64位整数就可以继续用到天荒地老了。Java本身采用64位long表示,无需担心。 - -考虑夏令时的各个其他国家地区的时间也很简单,只是比较繁琐没有太大的编写意义,用标准库就好。 - -### 10.3 Date & Calendar - -现在看一下Java标准库中的标准API。Java提供了两套API: -- 旧的一套在`java.util`包中,包括`Date` `Calendar` `TimeZone`。历史遗留原因,旧的API存在很多问题。 -- 新的一套是Java8引入的`java.time`包中,`LocalDateTime` `ZoneDateTime` `ZoneId`等。 - -新的代码当然应该使用新的API,但遇到历史遗留项目中用的旧的API的话也可以在新旧对象之间做转换。 - -`java.util.Date`是用于表示时间和日期的类,注意与`java.sql.Date`区分,后者用在数据库中。 - -`Date`中保存了`long`类型的毫米时间戳,时间表示都是通过其计算而来。 - -```java -public class Date - implements java.io.Serializable, Cloneable, Comparable -{ - private transient long fastTime; - public Date(long date) {} - // other constructors... - public static long parse(String s) {} // 从字符串解析时间 - public int getYear() {} - public void setYear(int year) {} - // month, day, hour, etc... - public long getTime() {} // 获取到时间戳 - public void setTime(long time) {} // 设置时间戳 - public boolean before(Date when) {} // 测试当前日期是否早于指定日期 - public boolean after(Date when) {} // 测试当前日期是否晚于指定日期 - public String toString() {} // dow mon dd hh:mm:ss zzz yyyy格式 - public String toLocaleString() {} - public String toGMTString() {} - public int getTimezoneOffset() {} - public static Date from(Instant instant) {} - public Instant toInstant() {} -} -``` -测试: -```java -Date date = new Date(); -System.out.println(date); // Mon May 03 16:14:39 CST 2021 -System.out.println(date.toLocaleString()); // 2021年5月3日 下午4:14:39 -System.out.println(date.toGMTString()); // 3 May 2021 08:14:39 GMT -System.out.println(Date.parse(date.toGMTString())); // 1620029679000 没有毫秒因为GMTString中没有毫秒的信息 -System.out.println(date.getTime()); // 时间戳 1620029679673 -System.out.println(date.getYear()+1900); // 需要加上1900才是真实年份 -System.out.println(date.getMonth()+1); // 结果是0~11需要+1 -System.out.println(date.getDate()); // 1~31 -System.out.println(date.getDay()); // 1 for Monday -System.out.println(date.toInstant()); // 2021-05-03T08:20:22.197Z -``` -其中`getYear`的结果需要+1900,`getMonth`结果需要+1,相关set方法同理。 - -想要针对用户的偏好精确地控制日期和时间的格式,可以使用`SimpleDateFormat`类,用预定义的字符串表示格式化。 -- yyyy:年 -- MM:月 -- dd: 日 -- HH: 小时 -- mm: 分钟 -- ss: 秒 - -```java -SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); -System.out.println(sdf.format(date)); -``` - -上面`Date`从字符串解析时间只能针对特定的格式,比如`toString`或者`toGMTString`的结果,而针对本地化格式化的时间则不能解析。但`SimpleDateFormat`就可以解析自己定义格式的时间。 -```java -public Date parse(String text, ParsePosition pos) -``` - -`SimpleDateFormat`预定义了很多格式,一般来说字母越长,输出越长,与`M`表示月份为例: -- `M` 5 -- `MM` 05 -- `MMM` 5月 -- `MMMM` 五月 - -|Letter|Date or Time Component|Presentation|Examples| -|:-|:-|:-|:-| -|G|Era designator|Text|AD| -|y|Year|Year|1996; 96| -|Y|Week year|Year|2009; 09| -|M|Month in year (context sensitive)|Month|July; Jul; 07| -|L|Month in year (standalone form)|Month|July; Jul; 07| -|w|Week in year|Number|27| -|W|Week in month|Number|2| -|D|Day in year|Number|189| -|d|Day in month|Number|10| -|F|Day of week in month|Number|2| -|E|Day name in week|Text|Tuesday; Tue| -|u|Day number of week (1 = Monday, ..., 7 = Sunday)|Number|1| -|a|Am/pm marker|Text|PM| -|H|Hour in day (0-23)|Number|0| -|k|Hour in day (1-24)|Number|24| -|K|Hour in am/pm (0-11)|Number|0| -|h|Hour in am/pm (1-12)|Number|12| -|m|Minute in hour|Number|30| -|s|Second in minute|Number|55| -|S|Millisecond|Number|978| -|z|Time zone|General time zone|Pacific Standard Time; PST; GMT-08:00| -|Z|Time zone|RFC 822 time zone|-0800| -|X|Time zone|ISO 8601 time zone|-08; -0800; -08:00| - -更多信息详见[SimpleDateFormat的JDK文档](https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/text/SimpleDateFormat.html)。 - -`Calendar`可以用于设置年月日时分秒,和`Date`相比,多了一个可以做简单的日期和时间运算的功能。 - -相关接口: -- 获取`Calendar`只有一种方式,使用`Calendar.getInstance()`。获取到就是当前时间。如果我们想给它设置成特定的一个日期和时间,就必须先使用`clear`清除所有字段。 -- 获取信息使用`get(int field)`,返回的年份不需要转换,返回的月份仍需要加1,星期需要特别注意,1~7分别表示周日、周一到周六。 -- 利用`Calendar.getTime()`可以将`Calendar`转换为`Date`对象。 - - -### 10.4 LocalDateTime - -### 10.5 ZonedDateTime - -### 10.6 DateTimeFormatter - -### 10.7 Instant - -### 10.8 最佳实践 - - - -## TODO - -- 包与模块详解 -- 日期与时间 -- 单元测试 -- 正则 -- 加密与安全 -- 多线程 -- Maven -- 网络编程 -- XML&JSON -- JDBC -- 函数式 -- 设计模式 -- Web开发 -- Spring -- Spring Boot - - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 63322d1..0000000 --- a/LICENSE +++ /dev/null @@ -1,427 +0,0 @@ -Attribution-ShareAlike 4.0 International - -======================================================================= - -Creative Commons Corporation ("Creative Commons") is not a law firm and -does not provide legal services or legal advice. Distribution of -Creative Commons public licenses does not create a lawyer-client or -other relationship. Creative Commons makes its licenses and related -information available on an "as-is" basis. Creative Commons gives no -warranties regarding its licenses, any material licensed under their -terms and conditions, or any related information. Creative Commons -disclaims all liability for damages resulting from their use to the -fullest extent possible. - -Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and -conditions that creators and other rights holders may use to share -original works of authorship and other material subject to copyright -and certain other rights specified in the public license below. The -following considerations are for informational purposes only, are not -exhaustive, and do not form part of our licenses. - - Considerations for licensors: Our public licenses are - intended for use by those authorized to give the public - permission to use material in ways otherwise restricted by - copyright and certain other rights. Our licenses are - irrevocable. Licensors should read and understand the terms - and conditions of the license they choose before applying it. - Licensors should also secure all rights necessary before - applying our licenses so that the public can reuse the - material as expected. Licensors should clearly mark any - material not subject to the license. This includes other CC- - licensed material, or material used under an exception or - limitation to copyright. More considerations for licensors: - wiki.creativecommons.org/Considerations_for_licensors - - Considerations for the public: By using one of our public - licenses, a licensor grants the public permission to use the - licensed material under specified terms and conditions. If - the licensor's permission is not necessary for any reason--for - example, because of any applicable exception or limitation to - copyright--then that use is not regulated by the license. Our - licenses grant only permissions under copyright and certain - other rights that a licensor has authority to grant. Use of - the licensed material may still be restricted for other - reasons, including because others have copyright or other - rights in the material. A licensor may make special requests, - such as asking that all changes be marked or described. - Although not required by our licenses, you are encouraged to - respect those requests where reasonable. More considerations - for the public: - wiki.creativecommons.org/Considerations_for_licensees - -======================================================================= - -Creative Commons Attribution-ShareAlike 4.0 International Public -License - -By exercising the Licensed Rights (defined below), You accept and agree -to be bound by the terms and conditions of this Creative Commons -Attribution-ShareAlike 4.0 International Public License ("Public -License"). To the extent this Public License may be interpreted as a -contract, You are granted the Licensed Rights in consideration of Your -acceptance of these terms and conditions, and the Licensor grants You -such rights in consideration of benefits the Licensor receives from -making the Licensed Material available under these terms and -conditions. - - -Section 1 -- Definitions. - - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. BY-SA Compatible License means a license listed at - creativecommons.org/compatiblelicenses, approved by Creative - Commons as essentially the equivalent of this Public License. - - d. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - e. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - f. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - g. License Elements means the license attributes listed in the name - of a Creative Commons Public License. The License Elements of this - Public License are Attribution and ShareAlike. - - h. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - i. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - j. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - k. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - l. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - m. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - - -Section 2 -- Scope. - - a. License grant. - - 1. Subject to the terms and conditions of this Public License, - the Licensor hereby grants You a worldwide, royalty-free, - non-sublicensable, non-exclusive, irrevocable license to - exercise the Licensed Rights in the Licensed Material to: - - a. reproduce and Share the Licensed Material, in whole or - in part; and - - b. produce, reproduce, and Share Adapted Material. - - 2. Exceptions and Limitations. For the avoidance of doubt, where - Exceptions and Limitations apply to Your use, this Public - License does not apply, and You do not need to comply with - its terms and conditions. - - 3. Term. The term of this Public License is specified in Section - 6(a). - - 4. Media and formats; technical modifications allowed. The - Licensor authorizes You to exercise the Licensed Rights in - all media and formats whether now known or hereafter created, - and to make technical modifications necessary to do so. The - Licensor waives and/or agrees not to assert any right or - authority to forbid You from making technical modifications - necessary to exercise the Licensed Rights, including - technical modifications necessary to circumvent Effective - Technological Measures. For purposes of this Public License, - simply making modifications authorized by this Section 2(a) - (4) never produces Adapted Material. - - 5. Downstream recipients. - - a. Offer from the Licensor -- Licensed Material. Every - recipient of the Licensed Material automatically - receives an offer from the Licensor to exercise the - Licensed Rights under the terms and conditions of this - Public License. - - b. Additional offer from the Licensor -- Adapted Material. - Every recipient of Adapted Material from You - automatically receives an offer from the Licensor to - exercise the Licensed Rights in the Adapted Material - under the conditions of the Adapter's License You apply. - - c. No downstream restrictions. You may not offer or impose - any additional or different terms or conditions on, or - apply any Effective Technological Measures to, the - Licensed Material if doing so restricts exercise of the - Licensed Rights by any recipient of the Licensed - Material. - - 6. No endorsement. Nothing in this Public License constitutes or - may be construed as permission to assert or imply that You - are, or that Your use of the Licensed Material is, connected - with, or sponsored, endorsed, or granted official status by, - the Licensor or others designated to receive attribution as - provided in Section 3(a)(1)(A)(i). - - b. Other rights. - - 1. Moral rights, such as the right of integrity, are not - licensed under this Public License, nor are publicity, - privacy, and/or other similar personality rights; however, to - the extent possible, the Licensor waives and/or agrees not to - assert any such rights held by the Licensor to the limited - extent necessary to allow You to exercise the Licensed - Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this - Public License. - - 3. To the extent possible, the Licensor waives any right to - collect royalties from You for the exercise of the Licensed - Rights, whether directly or through a collecting society - under any voluntary or waivable statutory or compulsory - licensing scheme. In all other cases the Licensor expressly - reserves any right to collect such royalties. - - -Section 3 -- License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the -following conditions. - - a. Attribution. - - 1. If You Share the Licensed Material (including in modified - form), You must: - - a. retain the following if it is supplied by the Licensor - with the Licensed Material: - - i. identification of the creator(s) of the Licensed - Material and any others designated to receive - attribution, in any reasonable manner requested by - the Licensor (including by pseudonym if - designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of - warranties; - - v. a URI or hyperlink to the Licensed Material to the - extent reasonably practicable; - - b. indicate if You modified the Licensed Material and - retain an indication of any previous modifications; and - - c. indicate the Licensed Material is licensed under this - Public License, and include the text of, or the URI or - hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any - reasonable manner based on the medium, means, and context in - which You Share the Licensed Material. For example, it may be - reasonable to satisfy the conditions by providing a URI or - hyperlink to a resource that includes the required - information. - - 3. If requested by the Licensor, You must remove any of the - information required by Section 3(a)(1)(A) to the extent - reasonably practicable. - - b. ShareAlike. - - In addition to the conditions in Section 3(a), if You Share - Adapted Material You produce, the following conditions also apply. - - 1. The Adapter's License You apply must be a Creative Commons - license with the same License Elements, this version or - later, or a BY-SA Compatible License. - - 2. You must include the text of, or the URI or hyperlink to, the - Adapter's License You apply. You may satisfy this condition - in any reasonable manner based on the medium, means, and - context in which You Share Adapted Material. - - 3. You may not offer or impose any additional or different terms - or conditions on, or apply any Effective Technological - Measures to, Adapted Material that restrict exercise of the - rights granted under the Adapter's License You apply. - - -Section 4 -- Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that -apply to Your use of the Licensed Material: - - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database; - - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material, - - including for purposes of Section 3(b); and - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not -replace Your obligations under this Public License where the Licensed -Rights include other Copyright and Similar Rights. - - -Section 5 -- Disclaimer of Warranties and Limitation of Liability. - - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - - -Section 6 -- Term and Termination. - - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. - - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided - it is cured within 30 days of Your discovery of the - violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any - right the Licensor may have to seek remedies for Your violations - of this Public License. - - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. - - -Section 7 -- Other Terms and Conditions. - - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. - - -Section 8 -- Interpretation. - - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. - - -======================================================================= - -Creative Commons is not a party to its public licenses. -Notwithstanding, Creative Commons may elect to apply one of its public -licenses to material it publishes and in those instances will be -considered the “Licensor.” The text of the Creative Commons public -licenses is dedicated to the public domain under the CC0 Public Domain -Dedication. Except for the limited purpose of indicating that material -is shared under a Creative Commons public license or as otherwise -permitted by the Creative Commons policies published at -creativecommons.org/policies, Creative Commons does not authorize the -use of the trademark "Creative Commons" or any other trademark or logo -of Creative Commons without its prior written consent including, -without limitation, in connection with any unauthorized modifications -to any of its public licenses or any other arrangements, -understandings, or agreements concerning use of licensed material. For -the avoidance of doubt, this paragraph does not form part of the public -licenses. - -Creative Commons may be contacted at creativecommons.org. diff --git a/LaTeX.md b/LaTeX.md deleted file mode 100644 index ca77404..0000000 --- a/LaTeX.md +++ /dev/null @@ -1,73 +0,0 @@ -# 如何方便地编辑公式 - - - -## Github支持公式 - -首先Github的Markdown文档:[GitHub Flavored Markdown Spec](https://github.github.com/gfm/)。 - - -### 图片插入 - -- 简单粗暴。 -- 繁琐,需要图床,Github放图片的话大一点就很慢。 - -### 装插件 - -Chrome浏览器插件:[mathjax-plugin-for-github](https://github.com/orsharir/github-mathjax)。 - -- 装了插件才可见。 -- 不需要图床。 -- 保留公式全部信息,编辑修改方便。 - -### 外链 - -**其一**:Codecogs -``` -![](http://latex.codecogs.com/svg.latex?公式代码) -``` -实例:![](http://latex.codecogs.com/svg.latex?E=c^2) - -**其二**:Github自己的API -``` -https://render.githubusercontent.com/render/math?math= -``` -实例:![](https://render.githubusercontent.com/render/math?math=E=c^2) - -注意事项: -- 不要有多余的空格,回车。 -- 需要用`\\`表示`\`,因为要转义。 - -优缺点: -- 相对截图省力,外链图片,Github直接可预览。 -- 过长,繁琐,需要转义,效果不算那么完美。 -- 被绑定到了某个平台,不再是纯粹的![](https://render.githubusercontent.com/render/math?math=LaTeX)了。 - -找个python脚本[latex2pic.py](https://github.com/blmoistawinde/ml_equations_latex/blob/master/latex2pic.py)专门来做,自动化起来: -```python -import re -from urllib.parse import quote - -if __name__ == "__main__": - text = open("index.md",encoding="utf-8").read() - - parts = text.split("$$") - - for i, part in enumerate(parts): - if i & 1: - parts[i] = f'![math](https://render.githubusercontent.com/render/math?math={quote(part.strip())})' - - text_out = "\n\n".join(parts) - - lines = text_out.split('\n') - for lid, line in enumerate(lines): - parts = re.split(r"\$(.*?)\$", line) - for i, part in enumerate(parts): - if i & 1: - parts[i] = f'![math](https://render.githubusercontent.com/render/math?math={quote(part.strip())})' - lines[lid] = ' '.join(parts) - text_out = "\n".join(lines) - - with open("readme.md", "w", encoding='utf-8') as f: - f.write(text_out) -``` diff --git a/LearnOpenGL.md b/LearnOpenGL.md deleted file mode 100644 index 084884c..0000000 --- a/LearnOpenGL.md +++ /dev/null @@ -1,3 +0,0 @@ -# OpenGL学习 - -书籍《计算机图形学编程——使用OpenGL和C++》,见仓库[tch0/LeanOpenGL](https://github.com/tch0/LearnOpenGL)。 \ No newline at end of file diff --git a/Lua.md b/Lua.md deleted file mode 100644 index d9fc34d..0000000 --- a/Lua.md +++ /dev/null @@ -1,199 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Lua语言学习](#lua%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0) - - [准备](#%E5%87%86%E5%A4%87) - - [关于Lua](#%E5%85%B3%E4%BA%8Elua) - - [编译Lua](#%E7%BC%96%E8%AF%91lua) - - [IDE集成](#ide%E9%9B%86%E6%88%90) - - [语法](#%E8%AF%AD%E6%B3%95) - - - -# Lua语言学习 - -## 准备 -### 关于Lua -- Lua是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 -- Lua读音是LOO-ah(/ˈluːə/),在葡萄牙语中是月亮的意思(巴西是前葡萄牙殖民地)。正确写法是大写字母L开头的Lua,不是lua也不是LUA。 -- Lua是一个轻量的可以轻松嵌入其他语言项目中的脚本语言,可以用在任何类型项目中,广泛用在游戏中作为脚本。 - - 主要原因是Lua是脚本语言且足够高效,更新Lua脚本的逻辑不会造成程序的ABI不兼容,在国内主要用在手机游戏中以资源形式更新Lua脚本以实现频繁的热更新,从而绕过漫长的应用商店审查、和漫长的下载安装过程,以提升用户体验。 - - 国外的话则是由2001年魔兽世界的使用带火起来的,现在来说已经有了很多替代,比如quickjs。 - - 单纯的Lua并不具备太大生产力,就是一个很多人都在用的胶水语言。 -- Lua使用ANSI C(C89)实现,使用源码分发,代码量不大,可以轻松将解释器嵌入C/C++(以及任何兼容C ABI的语言)项目中。 -- lua使用MIT协议开源,可以用在任何商业项目中。 -- 官方有一本书:[Programming in Lua](https://www.lua.org/pil/),当前出到第四版。 -- 特点: - - 轻量、高效、可嵌入、可移植。 - - 特别是高效、快:有benchmark说明Lua是最快的脚本语言,没有之一。 - - 如果需要更快的速度,可以使用[LuaJIT](https://luajit.org/),一个独立的Lua编译器,支持JIT编译(运行时将热点代码编译为native二进制以加速,广泛运用于Java等虚拟机语言)。 - - 使用C89编写,代码只有三万行,支持几乎所有平台(除了Windows/Unix,还支持Android、IOS、Windows Phone甚至嵌入式平台)。 - - 可以无缝嵌入C/C++等多种语言程序中(Java、C#、Fortran甚至其他脚本语言Perl、Ruby),可以轻松使用其他语言编写的库来扩充Lua的能力。 - - 支持面向过程、面向对象、函数式、数据驱动编程。 - - 动态类型,编译为字节码后运行在一个基于寄存器的虚拟机上。 - - 使用增量式垃圾收集的自动内存管理。 - - 适用于配置、脚本、快速原型设计。 -- 官网:https://www.lua.org/ -- github:https://github.com/lua/lua -- 文档:https://www.lua.org/docs.html -- Getting started:https://www.lua.org/start.html -- 手册:https://www.lua.org/manual/5.4/manual.html -- 教程:http://lua-users.org/wiki/TutorialDirectory - -### 编译Lua - -- 官网指南:https://www.lua.org/manual/5.4/readme.html -- 下载源码,当前版本5.4.4。 - -Linux下编译lua: -- 下载编译: -```sh -curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz -tar zxf lua-5.4.4.tar.gz -cd lua-5.4.4 -make all test -``` -- Linux中安装: -```sh -sudo make install -``` -- Linux本地安装: -```sh -make install INSTALL_TOP=xxx -``` - -Windows中: -- 使用mingw-w64编译:(不过此时生成文件比较散乱,需要手动收集) -```sh -make all -``` -- 可以直接运行下面脚本: -```bat -@echo off -:: ======================== -:: build.bat -:: -:: build lua to dist folder -:: tested with lua-5.3.5 -:: based on: -:: https://medium.com/@CassiusBenard/lua-basics-windows-7-installation-and-running-lua-files-from-the-command-line-e8196e988d71 -:: ======================== -setlocal -:: you may change the following variable’s value -:: to suit the downloaded version -set work_dir=%~dp0 -:: Removes trailing backslash -:: to enhance readability in the following steps -set work_dir=%work_dir:~0,-1% -set lua_install_dir=%work_dir%\dist -set compiler_bin_dir=%work_dir%\tdm-gcc\bin -set lua_build_dir=%work_dir% -set path=%compiler_bin_dir%;%path% -cd /D %lua_build_dir% -make PLAT=mingw -echo. -echo **** COMPILATION TERMINATED **** -echo. -echo **** BUILDING BINARY DISTRIBUTION **** -echo. -:: create a clean “binary” installation -mkdir %lua_install_dir% -mkdir %lua_install_dir%\doc -mkdir %lua_install_dir%\bin -mkdir %lua_install_dir%\include -mkdir %lua_install_dir%\lib -copy %lua_build_dir%\doc\*.* %lua_install_dir%\doc\*.* -copy %lua_build_dir%\src\*.exe %lua_install_dir%\bin\*.* -copy %lua_build_dir%\src\*.dll %lua_install_dir%\bin\*.* -copy %lua_build_dir%\src\luaconf.h %lua_install_dir%\include\*.* -copy %lua_build_dir%\src\lua.h %lua_install_dir%\include\*.* -copy %lua_build_dir%\src\lualib.h %lua_install_dir%\include\*.* -copy %lua_build_dir%\src\lauxlib.h %lua_install_dir%\include\*.* -copy %lua_build_dir%\src\lua.hpp %lua_install_dir%\include\*.* -copy %lua_build_dir%\src\liblua.a %lua_install_dir%\lib\liblua.a -echo. -echo **** BINARY DISTRIBUTION BUILT **** -echo. -%lua_install_dir%\bin\lua.exe -e "print [[Hello!]];print[[Simple Lua test successful!!!]]" -echo. - -:: configure environment variable -:: https://stackoverflow.com/a/21606502/4394850 -:: http://lua-users.org/wiki/LuaRocksConfig -:: SETX - Set an environment variable permanently. -:: /m Set the variable in the system environment HKLM. - -pause -``` -- 然后将`dist`中的所有子目录复制到比如:`C:\lua-5.4.4`,然后将其中`bin`目录添加到path即可。 - -测试: -- 检查版本: -```sh -lua -v -``` -- 两个可执行文件: - - `lua`是解释器,解释执行。 - - `luac`是编译器,编译为字节码。 - -### IDE集成 - -VsCode: -- 原生的lua支持几乎只有高亮,很简陋,刚开始的话配合CodeRunner也基本能用。 -- 可以选择官方插件[lua](https://github.com/LuaLS/lua-language-server),提供代码感知、高亮等功能,配合lua Debug实现调试。 -- 也可以选择腾讯的插件[LuaHelper](https://github.com/Tencent/LuaHelper),名称还是叫lua,选Tencent开发的那一个就好,提供智能感知、高亮、调试等一系列必备功能。 -- 这里选择LuaHelper。 - -## 语法 - -Reference Manual is all you need:https://www.lua.org/manual/5.4/manual.html - -记录见:https://github.com/tch0/LearnLua - -EBNF描述: -```EBNF -chunk ::= block -block ::= {stat} [retstat] -stat ::= ‘;’ | - varlist ‘=’ explist | - functioncall | - label | - break | - goto Name | - do block end | - while exp do block end | - repeat block until exp | - if exp then block {elseif exp then block} [else block] end | - for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end | - for namelist in explist do block end | - function funcname funcbody | - local function Name funcbody | - local attnamelist [‘=’ explist] -attnamelist ::= Name attrib {‘,’ Name attrib} -attrib ::= [‘<’ Name ‘>’] -retstat ::= return [explist] [‘;’] -label ::= ‘::’ Name ‘::’ -funcname ::= Name {‘.’ Name} [‘:’ Name] -varlist ::= var {‘,’ var} -var ::= Name | prefixexp ‘[’ exp ‘]’ | prefixexp ‘.’ Name -namelist ::= Name {‘,’ Name} -explist ::= exp {‘,’ exp} -exp ::= nil | false | true | Numeral | LiteralString | ‘...’ |functiondef | - prefixexp | tableconstructor | exp binop exp | unop exp -prefixexp ::= var | functioncall | ‘(’ exp ‘)’ -functioncall ::= prefixexp args | prefixexp ‘:’ Name args -args ::= ‘(’ [explist] ‘)’ | tableconstructor | LiteralString -functiondef ::= function funcbody -funcbody ::= ‘(’ [parlist] ‘)’ block end -parlist ::= namelist [‘,’ ‘...’] | ‘...’ -tableconstructor ::= ‘{’ [fieldlist] ‘}’ -fieldlist ::= field {fieldsep field} [fieldsep] -field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp -fieldsep ::= ‘,’ | ‘;’ -binop ::= ‘+’ | ‘-’ | ‘*’ | ‘/’ | ‘//’ | ‘^’ | ‘%’ | - ‘&’ | ‘~’ | ‘|’ | ‘>>’ | ‘<<’ | ‘..’ | - ‘<’ | ‘<=’ | ‘>’ | ‘>=’ | ‘==’ | ‘~=’ | - and | or -unop ::= ‘-’ | not | ‘#’ | ‘~’ -``` \ No newline at end of file diff --git a/Makefile.md b/Makefile.md deleted file mode 100644 index 2642a34..0000000 --- a/Makefile.md +++ /dev/null @@ -1,1543 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Makefile](#makefile) - - [0. Make介绍](#0-make%E4%BB%8B%E7%BB%8D) - - [1. Makefile介绍](#1-makefile%E4%BB%8B%E7%BB%8D) - - [1.1 Makefile的规则](#11-makefile%E7%9A%84%E8%A7%84%E5%88%99) - - [1.2 Make如何工作](#12-make%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C) - - [1.3 使用变量](#13-%E4%BD%BF%E7%94%A8%E5%8F%98%E9%87%8F) - - [1.4 Make自动推导](#14-make%E8%87%AA%E5%8A%A8%E6%8E%A8%E5%AF%BC) - - [1.5 新风格的Makefile](#15-%E6%96%B0%E9%A3%8E%E6%A0%BC%E7%9A%84makefile) - - [1.5 伪目标](#15-%E4%BC%AA%E7%9B%AE%E6%A0%87) - - [1.6 Makefile里面有什么](#16-makefile%E9%87%8C%E9%9D%A2%E6%9C%89%E4%BB%80%E4%B9%88) - - [1.7 Makefile文件名](#17-makefile%E6%96%87%E4%BB%B6%E5%90%8D) - - [1.8 引用其他Makefile](#18-%E5%BC%95%E7%94%A8%E5%85%B6%E4%BB%96makefile) - - [1.9 环境变量MAKEFILES](#19-%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8Fmakefiles) - - [1.10 Make的工作步骤](#110-make%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%AD%A5%E9%AA%A4) - - [2. 规则](#2-%E8%A7%84%E5%88%99) - - [2.1 规则语法](#21-%E8%A7%84%E5%88%99%E8%AF%AD%E6%B3%95) - - [2.2 规则中使用通配符](#22-%E8%A7%84%E5%88%99%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%80%9A%E9%85%8D%E7%AC%A6) - - [2.3 文件搜寻](#23-%E6%96%87%E4%BB%B6%E6%90%9C%E5%AF%BB) - - [2.4 伪目标](#24-%E4%BC%AA%E7%9B%AE%E6%A0%87) - - [2.5 多目标](#25-%E5%A4%9A%E7%9B%AE%E6%A0%87) - - [2.6 静态模式](#26-%E9%9D%99%E6%80%81%E6%A8%A1%E5%BC%8F) - - [2.7 自动生成依赖](#27-%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E4%BE%9D%E8%B5%96) - - [3. 命令](#3-%E5%91%BD%E4%BB%A4) - - [3.1 显示命令](#31-%E6%98%BE%E7%A4%BA%E5%91%BD%E4%BB%A4) - - [3.2 命令执行](#32-%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C) - - [3.3 命令出错](#33-%E5%91%BD%E4%BB%A4%E5%87%BA%E9%94%99) - - [3.4 嵌套执行make](#34-%E5%B5%8C%E5%A5%97%E6%89%A7%E8%A1%8Cmake) - - [3.5 命令包](#35-%E5%91%BD%E4%BB%A4%E5%8C%85) - - [4. 变量](#4-%E5%8F%98%E9%87%8F) - - [4.1 使用变量](#41-%E4%BD%BF%E7%94%A8%E5%8F%98%E9%87%8F) - - [4.2 变量中的变量](#42-%E5%8F%98%E9%87%8F%E4%B8%AD%E7%9A%84%E5%8F%98%E9%87%8F) - - [4.3 高级用法](#43-%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95) - - [4.4 追加变量值](#44-%E8%BF%BD%E5%8A%A0%E5%8F%98%E9%87%8F%E5%80%BC) - - [4.5 override指示符](#45-override%E6%8C%87%E7%A4%BA%E7%AC%A6) - - [4.6 多行变量](#46-%E5%A4%9A%E8%A1%8C%E5%8F%98%E9%87%8F) - - [4.7 环境变量](#47-%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F) - - [4.8 目标变量](#48-%E7%9B%AE%E6%A0%87%E5%8F%98%E9%87%8F) - - [4.9 模式变量](#49-%E6%A8%A1%E5%BC%8F%E5%8F%98%E9%87%8F) - - [5. 使用条件判断](#5-%E4%BD%BF%E7%94%A8%E6%9D%A1%E4%BB%B6%E5%88%A4%E6%96%AD) - - [6. 函数](#6-%E5%87%BD%E6%95%B0) - - [6.1 函数调用语法](#61-%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E8%AF%AD%E6%B3%95) - - [6.2 字符串处理函数](#62-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%A4%84%E7%90%86%E5%87%BD%E6%95%B0) - - [6.3 文件名操作函数](#63-%E6%96%87%E4%BB%B6%E5%90%8D%E6%93%8D%E4%BD%9C%E5%87%BD%E6%95%B0) - - [6.4 foreach函数](#64-foreach%E5%87%BD%E6%95%B0) - - [6.5 if函数](#65-if%E5%87%BD%E6%95%B0) - - [6.6 call函数](#66-call%E5%87%BD%E6%95%B0) - - [6.7 origin函数](#67-origin%E5%87%BD%E6%95%B0) - - [6.8 shell函数](#68-shell%E5%87%BD%E6%95%B0) - - [6.9 error & warning](#69-error--warning) - - [7. make的运行](#7-make%E7%9A%84%E8%BF%90%E8%A1%8C) - - [7.1 退出码](#71-%E9%80%80%E5%87%BA%E7%A0%81) - - [7.2 指定Makefile](#72-%E6%8C%87%E5%AE%9Amakefile) - - [7.3 指定目标](#73-%E6%8C%87%E5%AE%9A%E7%9B%AE%E6%A0%87) - - [7.4 检查规则](#74-%E6%A3%80%E6%9F%A5%E8%A7%84%E5%88%99) - - [8. 隐含规则](#8-%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99) - - [8.1 使用隐含规则](#81-%E4%BD%BF%E7%94%A8%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99) - - [8.2 隐含规则一览](#82-%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99%E4%B8%80%E8%A7%88) - - [8.3 隐含规则中的变量](#83-%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99%E4%B8%AD%E7%9A%84%E5%8F%98%E9%87%8F) - - [8.4 隐含规则链](#84-%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99%E9%93%BE) - - [8.5 定义模式规则](#85-%E5%AE%9A%E4%B9%89%E6%A8%A1%E5%BC%8F%E8%A7%84%E5%88%99) - - [8.6 老式风格的后缀规则](#86-%E8%80%81%E5%BC%8F%E9%A3%8E%E6%A0%BC%E7%9A%84%E5%90%8E%E7%BC%80%E8%A7%84%E5%88%99) - - [8.6 隐含规则搜索算法](#86-%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99%E6%90%9C%E7%B4%A2%E7%AE%97%E6%B3%95) - - [9. 使用make更新函数库文件](#9-%E4%BD%BF%E7%94%A8make%E6%9B%B4%E6%96%B0%E5%87%BD%E6%95%B0%E5%BA%93%E6%96%87%E4%BB%B6) - - [9.1 函数库文件的成员](#91-%E5%87%BD%E6%95%B0%E5%BA%93%E6%96%87%E4%BB%B6%E7%9A%84%E6%88%90%E5%91%98) - - [9.2 函数库成员的隐含规则](#92-%E5%87%BD%E6%95%B0%E5%BA%93%E6%88%90%E5%91%98%E7%9A%84%E9%9A%90%E5%90%AB%E8%A7%84%E5%88%99) - - [10. 总结](#10-%E6%80%BB%E7%BB%93) - - [11. 结语](#11-%E7%BB%93%E8%AF%AD) - - - -# Makefile - -- [GUN Make 首页](http://www.gnu.org/software/make/) -- [GUN Make 文档](https://www.gnu.org/software/make/manual/html_node/index.html) -- [跟我一起写Makefile](https://seisman.github.io/how-to-write-makefile/index.html) - -测试环境:WSL Ubuntu 20.04 LTS -make版本:GNU Make 4.2.1 - -## 0. Make介绍 - -在Windows上写程序时,因为有了IDE来帮助我们编译构建项目,通常来说都不会使用Make,但人人都说作为Pro的程序员,Makefile必须要懂。如果是在Unix系统下编译构建程序,Make是必不可少的。 - -Make是一个解释Makefile文件来实现自动化编译的工具,不同厂商的Make各不相同,也有不同的语法。这里的Make特指应用最广泛的[GNU Make](http://www.gnu.org/software/make/),这里也同[跟我一起写Makefile](https://seisman.github.io/how-to-write-makefile/index.html)一致,使用C/C++源码,GCC/G++编译器来学习。 - -在C/C++中,一般来说无论是C还是C++,都是先把源文件**编译**为中间代码文件(Unix中`.o`,Windows中`.obj`),即对象文件。然后再把对象文件**链接**为可执行文件。编译时需要语法正确,函数变量声明的正确,通常编译文件很多,需要给对象文件打包以方便链接(Windows下的`.lib`库文件,Unix下的`.a`Archive file)。链接时会寻找检查函数的实现,变量的定义等。 - -## 1. Makefile介绍 - -make执行时需要一个Makefile文件,以告诉make如何进行编译和链接程序。执行`make`就是执行`Makefile`中指定的命令序列,就像Shell脚本那样。使用`Makefile`的好处在于: -- 告诉make如何去编译和链接。 -- 处理文件之间的依赖关系,编译完成后如果只修改了部分文件,那么只需要重新编译链接有必要的文件,不需要全部编译,对大型项目来说这是必须的。 - -这些事情在成熟的IDE中都会由IDE来做。 - -### 1.1 Makefile的规则 - -```Makefile -target ... : prerequisites ... - command - ... - ... -``` - -- `target` 可以是一个目标文件(Object file),也可以是可执行文件,也可以是一个标签(伪目标)。 -- `prerequisites` 生成该`target`所依赖的文件或者`target`。 -- `command` 该`target`所执行的命令,任意的Shell命令序列。 -- `command` 缩进只能够使用`Tab`而不能使用空格。 - -以上`Makefile`确定了文件的依赖关系与生成规则,如果`prerequisites`中的任意一个文件发生了更改,那么都会重新执行`command`定义的命令以重新生成`target`。 - -示例:编译文件`hello.c` -```Makefile -hello : hello.o - gcc -o hello hello.o - -hello.o : hello.c - gcc -c hello.c - -clean: - rm -rf hello.o hello -``` - -- 其中`clean`就是一个标签,没有依赖,它代表一个动作,而不是具体的文件。执行`make clean`是就会执行`clean`下的命令也就是清除生成的文件,执行`make hello.o`也会只执行`hello.o` 目标下的命令。 -- 使用标签可以定义一些编译链接之外的动作,比如程序的打包,备份等。 -- 可以使用`\`进行换行,逻辑上与写在一行一致。 -- `make`并不会管命令如何工作,它只负责按照`Makefile`的规则去依次执行目标下的命令。 - -### 1.2 Make如何工作 - -输入`make`之后的执行逻辑: - -1. `make` 会在当前目录下按照优先级茶找名称为`GNUmakefile`/`makefile`/`Makefile`的文件。 -2. 在`Makefile`中找到第一个目标。 -3. 如果第一个目标的文件不存在,或者是目标后依赖的文件(GCC的话通常就是`.o`文件)的文件修改时间比目标文件新,那么会执行第一个目标后的命令来生成目标。 -4. 如果第一个目标依赖的文件不存在,那么会找到以这个文件为目标的命令来按照同样逻辑执行。就像一个有条件的函数调用的压栈出栈过程那样,直到最终依赖的文件被找到生成之后,再返回回来执行更上层的生成命令。 -5. 如果执行`make`是带了标签,那就是同样原理执行那个标签,而不是第一个标签。 - -在寻找与生成过程中,如果出现错误: -- 被依赖的文件找不到时该目标无法生成,那么`make`就会直接退出并报错。 -- 如果是定义的命令执行错误,比如编译失败,那么`make`不会理会,只管文件的依赖性。即如果执行了后面的命令但目标文件还是不在,那么就直接退出。 - -### 1.3 使用变量 - -如果多加了一个文件`comamnd.c`,并且hello.c通过`extern`调用了其中定义的函数或者变量。那么`Makefile`就需要改为: - -```Makefile -hello : hello.o command.o - gcc -o hello hello.o command.o - -hello.o : hello.c - gcc -c hello.c - -command.o : command.c - gcc -c command.c - -clean: - rm -rf hello.o hello \ - command.o -``` - -其中`.o`相关字符串被重复了多次。可以使用变量来减少重复: -```Makefile -objects = hello.o command.o - -hello : $(objects) - gcc -o hello $(objects) - -hello.o : hello.c - gcc -c hello.c - -command.o : command.c - gcc -c command.c - -clean: - rm hello $(objects) -``` - -- 定义时使用`varName = xxx`。 -- 通过`$(varName)`引用。 -- 变量的含义就是简单的字符串替换。 - -后续详细解释。 - -### 1.4 Make自动推导 - -GNU的make很强大,可以自动推导文件以及文件依赖关系的命令,上面的例子可以简化为下面这样: -```Makefile -objects = hello.o command.o - -hello : $(objects) - cc -o hello $(objects) - -hello.o : hello.c -command.o : command.c - -clean: - rm hello $(objects) -``` -执行`make`时的命令序列是: -```shell -cc -c -o hello.o hello.c -cc -c -o command.o command.c -cc -o hello hello.o command.o -``` - -Linux中`cc`就是一个指向`gcc`的符号链接。 - -### 1.5 新风格的Makefile - -一个`.o`可能会依赖于多个文件,多个`.o`也可能依赖同一个源文件,最新风格的`Makefile`可以让我们将多个`.o`写在一起: -```Makefile -target1.o target2.o : source1.h -target2.o : source2.h -``` -这样和: -``` -target1.o : source1.h source2.h -target2.o : source2.h -``` -等价,区别是目标文件和源文件写哪一个的问题。在我看来,虽然都可以描述依赖关系,但后者可能更清晰一些。 - -### 1.5 伪目标 - -使用`.PHONY target`可以将一个目标声明为伪目标,伪目标应该定义一个操作,而不生成一个文件。如果不声明为伪目标的话,会默认为文件目标,那么如果在目录下定义了同名的文件,像`clean`这种伪目标通常又没有依赖,只是执行操作,而不生成文件,就会被认为已经是最新,而不会执行目标后的命令。所以如果是伪目标,就应该使用`.PHONY`声明。 - -```Makfile -.PHONY clean -clean: - rm hello $(objects) -``` - -### 1.6 Makefile里面有什么 - -Makefile主要有5个东西:显示规则、隐晦规则、变量定义、文件指示、注释。 -- 显示规则:说明如何生成一个或多个目标文件。由`Makefile`作者明显指出生成文件、依赖文件、生成命令。 -- 隐晦规则:自动推导,可以简写生成的命令。 -- 变量定义:变量一般都是字符串,就像C语言中的宏。 -- 文件指示:包括三个部分,一个Makefile引用另一个Makefile;根据某些情况指定Makefile中的有效部分,就像C中的#if等预编译指令一样;还有就是定义多行命令。 -- 注释:Makefile只有行注释,和Unix的Shell脚本一样,使用`#`号注释。要使用`#`号时则需要使用`\#`转义。 - -还需要注意所有`Makefile`中的命令必须以`Tab`开始,这应该也基本可以说是唯一的要求缩进一定使用`Tab`而不能使用空格的场景。请注意编辑器设置或者`.editorconfig`配置。 - -### 1.7 Makefile文件名 - -前面提到,GNU Make是按照`GNUmakefile` `makefile` `Makefile`的优先级顺序查找识别的。一般来说最好使用`Makefile`这个文件名,大写开头更为醒目,也用得最多。 - -当然也可能指定别的文件名作为`Makefile`,只需要执行时添加`-f`/`--file`/`--makefile`选项,指定文件作为参数即可同样执行。 - - -### 1.8 引用其他Makefile - -```Makefile -include -``` -- 使用`include` 关键字,同C语言的`#include`很像,被包含的文件会原模原样的被放在包含的位置。 -- `include`前可以有空格,但不能是`Tab`,可以同时包含多个文件,文件也可以使用变量来定义之后再进行包含,可以使用通配符。 -- 一般以`.mk`作为文件后缀。 - -例: -```Makefile -commandfile = command.make - -include *.mk hello.make $(commandfile) - -.PHONY : clean -clean: - rm hello $(objects) -``` - -make命令开始时就会做引用文件内容的替换,找不到文件则会报错。引用文件查找顺序: -- 绝对或相对路径的话就去该路径查找 -- 不是绝对或相对路径的话在当前目录查找 -- 当前目录找不到,则会在下面的目录查找: - - 在传入`make`命令的`-I DIRECTORY, --include-dir=DIRECTORY`选项的目录中查找。 - - 目录`/include`存在也会去查找(一般是`usr/local/include`或者`/usr/include`)。 - -如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。 -```Makefile --include -``` - -### 1.9 环境变量MAKEFILES - -如果当前环境中定义了环境变量,那么make执行时就会把这个变量中的值做一个类似`include`的动作。这个变量的值就是其他`Makefile`,用空格分隔。同`include`不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。 - -一般来说不建议使用,因为会造成所有的`Makefile`执行都收到影响,也许有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。 - -### 1.10 Make的工作步骤 - -GNU Make的执行步骤: - -1. 读入Makefile内容。 -2. 读入被`include`的其它Makefile。 -3. 初始化文件中的变量。 -4. 推导隐晦规则,并分析所有规则。 -5. 为所有的目标文件创建依赖关系链。 -6. 根据依赖关系和时间戳,决定哪些目标要重新生成。 -7. 执行生成命令。 - -1-5步为第一个阶段,6-7为第二个阶段。 - - -## 2. 规则 - -### 2.1 规则语法 - -```Makefile -targets : prerequisites - command - ... -``` -或者是这样: -```Makefile -targets : prerequisites ; command - command - ... -``` - -- `targets`是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。 -- `command`是命令行,如果不与`targets : prerequisites`在一行,那么必须以`Tab`开头,如果与`targets : prerequisites`在一行,可以用`;`隔开。 -- 命令太长时可以用`\`分开。 -- 规则告诉make两件事:文件依赖关系和如何生成目标。 -- 一般来说会以Unix标准Shell执行命令,也就是`/bin/sh`。 - -### 2.2 规则中使用通配符 - -make支持三个通配符:`*` `?` `~` - -- `~`在文件名中特殊用法,与Shell中相同,表示当前用户家目录`$HOME`,如`~usrname/test`表示用户`usrname`的家目录下的`test`目录。Windows中视环境变量`$HOME`确定,比如在Git Bash中就是当前用户的用户目录`C:\Users\username`。 -- `*`匹配一系列文件,比如`*.c`匹配所有`.c`后缀的源文件。如果文件名含有`*`号,那么需要使用`\*`转义,这与在Shell中一致,Windows中则不支持含`*`好的文件名。 -- `$?`是一个自动变量,后续详解。 - -如果这样使用: -```Makefile -objects = *.o -``` -那么`objects`的值就是`*.o`,最后直接用`*.o`去替换使用`$(objects)`的地方。如果需要将通配符展开,可以使用: -```Makefile -obejcts := $(wildcard *.o) -``` - -### 2.3 文件搜寻 - -在一些大的工程中,会有大量的源文件,通常会将这些源文件分类存放在不同目录中。所以当`make`寻找文件依赖关系时,可以在文件前面加上路径。但最好的方法是告诉`make`路径,让`make`自己去找。 - -Makefile中的特书变量`VPATH`就是完成这个功能,如果没有定义这个变量,则只会在当前目录中寻找,依赖文件和目标文件。如果定义了,那么会在当前目录找不到的情况下,到所指定的目录中去找。 - -```Makefile -VPATH = src:../headers -``` - -多个目录使用`:`分隔,当前目录永远是最高优先级最先去寻找的地方。 - -另一个设置文件搜索路径的方法,使用全小写的`vpath`**关键字**。作用类似于`VPATH`变量,但更为灵活,使用方法: -```Makefile -vpath -# 为符合模式的文件指定搜索目录 - -vpath -# 清除符合模式的文件的搜索目录 - -vpath -# 清除所有已被设置好了的文件搜索目录。 -``` - -其中的``指定文件的模式,需要使用`%`字符。`%`的意思是匹配零或者若干字符,引用原始的`%`字符需要用`\%`转义。如`%.h`表示所有`.h`结尾的文件。``指定要搜索的文件集,而``指定``的文件集的搜索的目录。 - -可以连续使用`vpath`语句指定不同的搜索策略,但是如果连续的`vpath`中出现了相同的``或者被重复了的``,那么`make`按照`vpath`语句先后顺序执行搜索。 - -示例: -```Makefile -vpath %.h inc -vpath % thirdparty -vpath %.c src -``` -其中`%`匹配所有文件,所以所有`.h`头文件都会先去`inc`中再去`thridparty`中找,`.c`源文件会先去`thridparty`中再去`src`中找。 - -### 2.4 伪目标 - -`clean`是经常需要在`Makefile`中定义的一个伪目标,用来清理生成的文件,以备完整的重编译。为了避免和文件重名所以需要使用`.PHONY`。 - -伪目标一般没有依赖文件,但是我们也可以给伪目标指定依赖文件,伪目标同样可以作为默认目标放在第一个。常见的就是需要生成多个可执行文件: -```Makefile -executable = hello world - -.PHONY : all -all : $(executable) - -hello : hello.c - cc -o hello hello.c command.c -world : world.c - cc -o world world.c -``` - -前面也说了,`.PHONY`不写但是没有生成`all`文件,也不会有问题,但目录下存在同名的`all`文件时就会就会被认为是已经存在了文件,显示地用`.PHONY`声明伪目标是一个好习惯。 - -伪目标同样可以成为依赖,如: -```Makefile -.PHONY : clean cleanobj cleanexe -clean : cleanobj cleanexe -cleanobj : - rm *.o -cleanexe: - rm $(executable) -``` - -### 2.5 多目标 - -Makefile规则中的目标可以有多个,有可能多个目标依赖于同一个文件,并且生成命令类似,就能够合并起来。多个目标的生成规则的执行命令不是同一个,可能会有问题,好在可以使用自动化变量`$@`来表示目标规则中所有目标的集合。 - -例: -```Makefile -bigoutput littleoutput : text.g - generate text.g -$(subst output,,$@) > $@ -``` -等价于: -```Makefile -bigoutput : text.g - generate text.g -big > bigoutput -littleoutput : text.g - generate text.g -little > littleoutput -``` -其中的`$(subst output,,$@)`表示执行函数`subst`,后面的为参数。关于函数后续详述。 - - -### 2.6 静态模式 - -静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。语法: -```Makefile - : : - - ... -``` -- `targets` 定义目标文件集合,可以有通配符。 -- `target-pattern` 指明目标文件集合模式。 -- `prereq-patterns` 目标的依赖模式,对`target-pattern`形成的模式再进行一次依赖目标的定义。 - -例如目标文件是多个`.o`集合,目标文件集合模式是`%.o`,依赖模式是`%.c`,那么就是对目标文件集合模式进行二次定义,也就是依赖文件集合是取`%.o`中所有文件去掉`.o`换为`.c`之后构成的集合。同理`%`字符本身由`\%`进行转义。 - -例子,还是最开始的`hello.c` `command.c`: -```Makefile -objects = hello.o command.o -executable = hello -CC = cc - -$(executable) : $(objects) - $(CC) -o $(executable) $(objects) - -$(objects) : %.o : %.c - $(CC) -c $< -o $@ - -.PHONY : clean -clean : - rm $(objects) $(executable) -``` - -对于由`.c`生成`.o`一对一生成,且生成文件名都相同的情况,使用一个静态模式即可生成所有的对象文件,无论是几十还是几百个。 - -`$<`和`$@`是自动化变量,`$<`表示第一个依赖文件,`$@`表示目标集中的目标文件。 - -还可以配合函数对目标集做筛选过滤操作。 - -### 2.7 自动生成依赖 - -`Makefile`中依赖关系可能需要包含一系列头文件。虽然编译时没有必要将头文件放到源文件中,因为预编译时会处理文件包含,但是为了使头文件的修改反馈到`make`的重新编译上,头文件也需要加到依赖列表中(上面的例子并没有加,所以头文件改变时执行make不能重新编译)。这样添加删除了工程文件,新增头文件包含等都需要维护`Makefile`,这会是一个可维护性非常差非常繁琐的事情。为了避免这样的事情,大多数C/C++编译器都提供了一个`-M`选项,即自动寻找源文件中包含的头文件,并生成一个依赖关系。例如: -```shell -cc -M hello.c -``` -就可以得到结果: -```Makefile -hello.o: hello.c command.h -``` -注意要在`gcc/g++`编译器下得到上述结果,应该使用`-MM`选项,`-M`选项会连标准库中的头文件也一并列出。 - -如何将这个功能与`Makefile`结合起来呢?`Makefile`不应该依赖于源文件,我们应该在`Makefile`中自己根据`gcc -MM`选项做到这件事情。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个 `name.c` 的文件都生成一个 `name.d` 的`Makefile`文件, `.d` 文件中就存放对应 `.c` 文件的依赖关系。 - -然后`%.d`依赖`%.c`,写出这个生成规则就是: -```Makefile -%.d : %.c - @set -e; rm -rf $@;\ - $(CC) -M $< > $@.$$$$;\ - sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ - rm -f $@.$$$$ -``` - -- `@set -e` 表示后续命令只要有执行失败就退出。 -- `rm -rf $@` 表示生成每个目标时先删除原来的`.d`文件。 -- 然后使用`cc -M`生成依赖,重定向到`name.d.XXXX`中,`$$$$`意为一个编号,类似`name.d.1234`。 -- 调用`sed`命令对`name.d.XXXX`做了一个字符串操作,结果保存在`name.d`中。 -- 最后删除临时文件。 - -`sed`命令执行后将生成的依赖文件: -```Makefile -hello.o : hello.c command.h -``` -添加了`.d`文件的目标: -```Makefile -hello.o hello.d : hello.c command.h -``` -就可以同步更新`.d`文件了。接下来将生成的规则`include`到主`Makefile`中: -```Makefile -soruces = hello.c command.c -include $(soruces:.c=.d) -``` - -`$(soruces:.c=.d)`表示将`$(sources)`中的所有`.c`字符串替换为`.d`。如果`include`在开头,因为`include`是按照默认顺序的,那么第一个就会变成默认目标。 - -最终例子,3个源文件`hello.c` `command.h` `command.c`,`hello.c`包含了`command.h`: -```Makefile -sources = $(wildcard *.c) -objects = $(sources:.c=.o) -executable = hello -CC = cc - -$(executable) : $(objects) - $(CC) -o $(executable) $(objects) - -include $(sources:.c=.d) - -%.d : %.c - @set -e; rm -rf $@;\ - $(CC) -MM $< > $@.$$$$;\ - sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ - rm -f $@.$$$$ - -.PHONY : clean -clean : - rm $(objects) $(executable) *.d -``` - -可以看到`include`同样会造成依赖,然后就会执行`.d`文件的生成规则。上述例子的Makefile大概就可以解决同目录下大量C源文件生成一个可执行文件的编译问题了。 - -## 3. 命令 - -生成命令的书写规则与Shell命令行行为一致,`make`会按照顺序一条一条执行。每条命令必须以`Tab`开头,除非命令紧跟在命令规则的`;`后。命令行之间的空格或者空行会被忽略,如果这个空格或者空行以`Tab`开头就会被认为是一个空命令。 - -### 3.1 显示命令 - -通常,make会把要执行的命令显示在屏幕上,当用`@`放在命令前时,则不会显示出来。 -比如生成依赖文件时 -```Makefile -%.d : %.c - @echo 正在生成依赖文件 $@ - @set -e; rm -rf $@;\ - $(CC) -MM $< > $@ -``` -将会在生成每个依赖文件是显示,如果不加`@`,那么执行的命令本身就会被显示出来。生成正在编译的信息时很常用。 - -如果执行`make`时制定了`-n, --just-print, --dry-run, --recon`选项,那么就只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。 - -而`make`选项 `-s` 或 `--silent` 或 `--quiet` 则是全面禁止命令的显示。 - -### 3.2 命令执行 - -当依赖文件新于目标时,目标就需要被更新,`make`会一条条执行后面的命令。如果希望前一条命令的结果作用于第二条命令时,也就是第二条在第一条基础上运行,需要用`;`分隔这两条命令。 - -如第一条是`cd`切换了目录: -```Makefile -.PHONY : test -test : - cd ~ - pwd -``` -此时`pwd`得到的目录还是`Makefile`所在目录。 - -修改后: -```Makefile -.PHONY : test -test : - @cd ~;pwd -``` -则能够得到当前用户家目录。并且用`;`分隔之后加`@`就只需要在第一个命令前写一次,就可以作用于所有命令。 - -`make`一般使用环境变量`$SHELL`所定义的系统Shell来执行命令,我的Linux环境中默认情况是`/bin/bash`。MS-DOS中则比较特殊,如果`$SHELL`找不到,则会在当前目录和环境变量中去找,并且会添加`.exe` `.com` `.bat`等可执行文件后缀去找。 - -### 3.3 命令出错 - -命令执行完后`make`会检查命令返回码,0代表成功,非零代表出错,如果成功则会执行下一条,失败则会终止当前规则执行,这有可能终止所有规则执行。 - -有些时候可能命令失败对规则执行并不影响,比如`mkdir`已经存在目录时失败。就可以在命令前加一个`-`号标记命令无论出不出错都算执行成功,都继续往下执行。 - -```Makefile -clean: - -rm -f *.o -``` - -另外还有全局方法:给`make`加上 `-i` 或是 `--ignore-errors` 参数,那么,Makefile中所有命令都会忽略错误。如果一个规则是以 `.IGNORE` 作为目标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你的不同喜欢设置。 - -```Makefile -.PHONY .IGNORE : clean -clean : - -rm $(objects) $(executable) *.d - rm hello.d - rm world.d -``` - -还有一个选项 `-k` 或是 `--keep-going` ,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。 - -### 3.4 嵌套执行make - -在大的工程里面,不同的模块或者不同功能的源码会放在不同目录中,可以在每个目录中编写该目录的Makefile,这有利于使Makefile变得简洁,所有东西都写在一起的话,会很难维护。例如有一个子目录`subdir`,那么执行这个目录下的Makefile就可以这样写: -```Makefile -.PHONY : world -world : - @cd ./world && $(MAKE) -``` -等价于: -```Makefile -.PHONY : world -world : - @cd $(MAKE) -C ./world -``` - -变量`$(MAKE)`表示当前`Makefile`执行时的`make`命令行,也可以在当前`Makefile`重新定义,也许需要一些参数,使用变量更易维护。 - -最顶层的Makefile称作总控Makefile,总控Makefile的变量可以传递到下级的Makefile中,但不会覆盖下层定义的变量,除非指定了`-e, --environment-overrides`参数。 - -要传递变量或者不想传递变量到下层可以这样写: -```Makefile -export ; -unexport ; - -# 例 -export variable = value - -variable = value -export variable - -export variable := value - -variable := value -export variable -``` - -注意两个变量,`$SHELL`和`$MAKEFLAGS`,这两个无论是否`export`,总是传递到下层。`MAKEFALGS`中包含了`make`的参数信息,执行时给了参数或者在上层定义了这个变量,那么就会传递到下层,这是一个系统级的系统变量。如果你定义了`MAKEFLAGS`,请确保是下层都会使用的,不然可能会有意想不到的问题。 - -如果使用了`-w`或`--print-directory`选项,那么进入子目录时会打印,退出是也会打印,这个选项默认开启,如果使用了`-s`或`--no-print-directory`选项,那么则会关闭。 -``` -make[1]: Entering directory '/home/tch/LearnMake/started/world' -gcc -c -o world.o world.c -gcc -o world world.o -make[1]: Leaving directory '/home/tch/LearnMake/started/world' -``` - -系统变量`$(MAKELEVEL)`表示嵌套的调用层数,顶层`Makefile`其值为0,每向下调用一层就加1。 - -### 3.5 命令包 - -语法: -```Makefile -define generateDependency -@set -e; rm -rf $@;\ -$(CC) -MM $< > $@ -endef - -%.d : %.c - $(generateDependency) -``` - -使用`define`和`endef`将多条命令包起来即可,调用时使用`$(youCmdPakName)`。 - - -## 4. 变量 - -在Makefile中定义变量,就像在C中定义宏一样,在执行时会自动替换为展开为表示的字符串,与宏不同的是,你可以在Makefile中修改变量的值。变量可以使用在目标、依赖、命令或者其他部分中。 - -变量命名:可以包含字符、数字、下划线,可以是数字开头,不能有其他字符,大小写敏感,推荐使用大小写搭配的驼峰法来命名。 - - -### 4.1 使用变量 - -声明时要给予初值,使用时前面需要加上`$`,最好用小括号`()`或者大括号`{}`将变量名包起来,使用`$`字符本身则需要使用`$$`转义。 - -变量可以用于目标、依赖、命令中等,会像宏一样精确展开。 -```Makefile -objects = program.o foo.o utils.o -program : $(objects) - cc -o program $(objects) - -$(objects) : defs.h -``` - -给变量加上括号`()` `{}`是为了安全地使用它。 - -### 4.2 变量中的变量 - -可以用变量的值来初始化其他变量,变量可以使用后面的变量来定义: -```Makefile -var1 = $(var2) -var2 = hello - -.PHONY : test -test : - echo $(var1) -``` - -这个功能有好的地方,就是我们可以把真实的变量值推到后面来定义。不好的地方就是可能造成递归定义: -```Makefile -A = $(B) -B = $(A) -``` -make会检测出这种递归定义并报错。 - -为了避免递归定义,可以使用另一个种方式:`:=`操作符。 -```Makefile -x := test -y := $(x) bar -x := lter -``` -这里的结果`x`是`test bar`。 - -使用`:=`操作符定义的变量只能使用前面已经定义好的变量,如果前面没有定义,那么对应的值就是空的。 - -注意定义变量时从第一个有效字符开始后面的所有空格也会算在变量中。如果是定义目录之类的变量,后续要进行拼接的话需要额外注意: -```Makefile -pwd = $(shell pwd) # four spaces ahead -subdir = $(pwd)/subdir -``` -则得到的`$(subdir)`中会包含四个空格。 - -还有一个操作符:`?=` 表示如果前面有定义过该变量就什么也不做,没有定义过则定义它。 -```Makefile -var ?= val -``` - -注意使用没有定义的变量不会报错,只是它的的值是空的,定义了变量没有显示赋值它也是空的,可以使用一个空变量来**定义一个空格**: -```Makefile -foo = -space = $(foo) # a space ahead -``` - -### 4.3 高级用法 - -变量值的替换,前面也有用到: -```Makefile -sources = a.c b.c -objects = $(sources:.c=.o) -``` - -变量值的替换也可以使用静态模式: -```Makefile -sources = a.c b.c -objects = $(sources:%.c=%.o) -``` - -另一种是把变量值再当成一个变量: -```Makefile -z = hello -x = y -y = z -a := $($($(x))) -``` -这种方式中可以使用多个变量来组合成另一个变量的名字,也可以把这种组合放到`=`左边,因为本质其实就是字符串替换。 - - -### 4.4 追加变量值 - -使用`+=`运算符给变量追加值。 -```Makefile -variable := value -variable += more -``` -等价于: -```Makefile -variable := value -variable := $(variable) more -``` - -如果变量之前没有定义过,那么`+=`自动变成`=`,如果定义过,那么`+=`的操作符就继承于上一次赋值的操作符:`=`或者`:=`。中间会加一个空格,不能用这种方式拼接路径。 - -例: -```Makefile -sources = $(wildcard *.c) -testSrc = $(wildcard test/*.c) -sources += testSrc -``` -则`sources`的值是当前目录和`test`目录下所有`.c`源文件。 - - -### 4.5 override指示符 - -如果有变量是使用`make`命令行参数设置的,那么Makefile中对其的赋值会被忽略,如果要在Makefile中对其赋值,需要使用`override`关键字。 - -```Makefile -override = -override := -override += -``` - -多行变量定义`define`前也可以加`override`: -```Makefile -override define var -val -endef -``` - -### 4.6 多行变量 - -可以使用`define`和`endef`来定义多行变量: -```Makefile -define two-lines -echo foo -echo $(bar) -endef -``` - -### 4.7 环境变量 - -make运行时的系统环境变量会在make开始运行时被载入到Makefile中,如果Makefile中已经定义了这个变量,或者这个变量由make命令行带入,那么系统变量的值会被覆盖。 - -如果make执行时指定了`-e, --environment-overrides`选项,那么系统变量会覆盖Makefile中定义变量。 - -当make嵌套调用时,上层Makefile中定义的变量会以系统变量的方式传递到下层的Makefile中。当然,默认情况下只有通过命令行设置的变量会出传递,定义在文件中的变量需要传递则需要使用`export`声明。 - -并不推荐把太多变量定义在环境中,执行环境变了或者执行不同Makefile都可能出问题。 - -### 4.8 目标变量 - -前面定义的变量都类似于全局变量,在整个文件中都可以访问。当然也可以定义针对特定目标的变量,称之为"Target-specific Variable",可以和全局变量同名,因为在规则中会覆盖全局变量定义,其值只在定义的规则及其下的连带规则中有效。 - -语法,当然后者是针对需要覆盖的make命令行带入的变量,或是系统环境变量。 -```Makefile - : var = XXX - : override var = XXX -``` - -例: -```Makefile -prog : CFLAGS = -g -prog : prog.o foo.o bar.o - $(CC) $(CFLAGS) prog.o foo.o bar.o - -prog.o : prog.c - $(CC) $(CFLAGS) prog.c - -foo.o : foo.c - $(CC) $(CFLAGS) foo.c - -bar.o : bar.c - $(CC) $(CFLAGS) bar.c -``` -上述例子中,在所有由prog目标引发的规则中`$(CFLAGS)`都是`-g`。 - - -### 4.9 模式变量 - -GNU make中还支持模式变量(Pattern-specific Variable),也就是针对特定模式指定变量,和文件搜寻`vpath`有点类似。 - -```Makefile -%.o : CFLAGS = -O -``` -含义是针对所有`.o`文件为目标的规则,`$(CFLAGS)`值为`-O`。 - -语法: -```Makefile - : var = XXX - : override var = XXX -``` - -## 5. 使用条件判断 - -语法: -```Makefile - - -endif -``` -或者 -```Makefile - - -else - -endif -``` - -其中表条件的指令可以是: -- `ifeq` 判等 -- `ifneq` 判不等 -- `ifdef` 条件定义判断变量是否定义 -- `ifndef` 条件定义判断变量是否未定义 - -`ifeq`和`ifneq`语法: -```Makefile -ifneq (, ) -ifeq '' '' -``` -用引号包起来的话可以用单引号`'`可以用双引号`"`,无要求。 - -`ifdef`和`ifndef`语法: -```Makefile -ifdef -``` -注意`ifdef`含义其实是判断**是否非空**,因为使用一个没有定义的变量不会报错,只是变量值是空的。那么一个定义为空的变量和没有定义就是一样的。 - -```Makefile -foo = -ifdef foo - foodef = yes -else - foodef = no -endif -``` -得到的值是`no`。如果变量有值,就算是一个空格,条件也会为真。 - -值得注意的是make是在**读取Makefile时就计算条件表达式的值**,并根据条件的值来选择语句,所以最好不要把自动化变量如`$<` `$^` `$@`等放在条件表达式中,自动化变量要运行时才能确定。 - -## 6. 函数 - -make支持的函数不多,但足够使用。函数调用后,返回值可以作为变量来用。函数调用都不修改参数,将结果作为返回值返回。 - -### 6.1 函数调用语法 - -```Makefile -$( ) -${ } -``` -函数名与参数之间用空格分隔,多个参数之间用逗号`,`分隔。 - -**注意**:如果在参数与逗号之间添加了空格,空格也会被算到参数中。一般来说不要在参数列表中加空格。 - -示例: -```Makefile -comma := , -empty := -space := $(empty) $(empty) -foo = a b c -bar = $(subst $(space),$(comma),$(foo)) -``` -其中`subst`接受三个参数,将最后一个参数中出现的第一个参数值全部替换为第二个,结果是`a,b,c`。 - - -### 6.2 字符串处理函数 - -术语及共识: -- 字符串:即一个变量表示的字符串或者就是能够用来初始化一个变量的字符串。 -- 单词:字符串内部被空格、Tab、回车、换行隔开来的一个个单词。 -- 模式:可以有通配符`%`表任意字符串,也可以是单词用来精确匹配。 -- 多个模式间可以用空格分隔。 -- 字符串的值是大小写敏感的。 - -#### subst - -```Makefile -$(subst ,,) -``` -将 `text` 中所有 `from` 子串替换为 `to`,返回替换后的字符串。 - -#### patsubst - -```Makefile -$(patsubst ,,) -``` - -模式字符串替换函数,查找 `` 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 `` ,如果匹配的话,则以 `` 替换。返回替换后字符串。 - -这里的``可以包含通配符`%`,表示任意长度字串。如果``也包含`%`,那么它就是``中的那个`%`代表字符串。`%`字符使用`\%`转义。 - -前面提到的`$(var:=)`其实就是`$(patsubst ,,$(var))`,而`$(var: =)`就相当于`$(patsubst %,%,$(var))`。 - -例,这三个例子是等价的: -```Makefile -foo = a.c b.c c.c -bar = $(patsubst %.o,%.c,$(foo)) -bar = $(foo:.c=.o) -bar = $(foo:%.o=%.o) -``` - -#### strip - -```Makefile -$(strip ) -``` - -去掉``两端的空字符,返回结果。 - -#### findstring - -```Makefile -$(findstring ,) -``` -在``中查找``,找到则返回``,否则返回空字符串。 - -#### filter - -```Makefile -$(filter ,) -``` -以 `` 模式过滤 `` 字符串中的单词,保留符合模式 `` 的单词。可以有多个模式,用空格分隔。 - -例: -```Makefile -sources := foo.c bar.c baz.s ugh.h -filterRes := $(filter %.c %.s,$(sources)) # foo.c bar.c baz.s -``` - -#### filter-out - -```Makefile -$(filter-out ,) -``` -`filter`结果的补集,去除符合模式 `` 的单词,可以有多个模式,用空格分隔。模式不一定非得有通配符,可以是具体的单词的集合。 - -#### sort - -```Makefile -$(sort ) -``` -将``中的单词升序排列,会去掉重复单词,字符串大小写敏感,比较依据当然是ASCII码值。 - -#### word - -```Makefile -$(word ,) -``` -取字符串 `` 中第 `` 个单词。从`1`开始,超过了最大单词数返回空字符串。 - -#### wordlist - -```Makefile -$(wordlist ,,) -``` - -从字符串 `` 中取从 `` 开始到 `` (闭区间)的单词串, `` 和 `` 是一个数字。单词之间的空字符会被保留,比如多个空格,结果字符串前后空字符不会保留。 - -#### words - -```Makefile -$(words ) -``` -统计 `` 中字符串中的单词个数。 - -例,取字符中最后一个单词: -```Makefile -$(word $(words ),) -``` - -#### firstword - -```Makefile -$(firstword ) -``` - -取字符串 `` 中的第一个单词。等价于`$(word 1,)`。 - -#### 例子 - -例:make使用`VPATH`变量指定依赖文件搜索路径,路径用`:`分割,可以利用这个变量来指定编译器对头文件的搜索路径: -```Makefile -override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH))) -``` - - -### 6.3 文件名操作函数 - -下面的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。 - -#### dir - -```Makefile -$(dir ) -``` - -从文件名序列 `` 中取出目录部分。目录部分是指最后一个斜杠 `/` 之前的部分。如果没有反斜杠,那么返回 `./` - -例:`$(dir src/src.c test/ bar.c)`结果是`src/ test/ ./`。 - -#### notdir - -```Makefile - -``` -从文件名序列 `` 中取出非目录部分。非目录部分是指最後一个反斜杠 `/` 之后的部分,没有非目录部分则为空。 - -例:`echo $(notdir src/src.c test/ bar.c)`返回`src.c bar.c`。 - -#### suffix - -```Makefile -$(suffix ) -``` -从文件名序列 `` 中取出各个文件名的后缀,没有后缀或者为目录则为空。 - -#### basename - -```Makefile -$(basename ) -``` -从文件名序列 `` 中取出各个文件名的前缀部分,没有前缀返回目录,没有目录返回空。 - -#### addsuffix - -```Makefile -$(addsuffix ,) -``` - -把后缀 `` 加到 `` 中的每个单词后面。 - -#### addprefix - -```Makefile -$(addprefix ,) -``` - -把前缀 `` 加到 `` 中的每个单词后面。 - -#### join - -```Makefile -$(join ,) -``` - -把 `` 中的单词对应地加到 `` 的单词后面。`` 更长的话多出来的保持不变,``更长的话,扩展 ``,`` 中多出来的单词被复制到对应位置。返回结果``。 - -### 6.4 foreach函数 - -毫无疑问`foreach`是用来循环的。Makefile中的`foreach`几乎就是仿照Unix标准Shell中的`for`语句。语法: -```Makefile -$(foreach ,,) -``` - -将``中的单词取出来放到``中,然后执行`` 包含的表达式,每一次``会得到一个字符串,最终结果就是每次循环得到的结果用空格分隔之后的整个字符串。 - -也就是循环遍历列表``,循环变量是``,然后对每个变量执行操作``,由每轮循环结果组成最终结果。``中一般会使用定义的变量``。 - -`foreach`中定义的变量只是一个临时变量,像C++的循环一样,作用域只在`foreach`内部。 - -例:给多个文件名排列组合添加多个后缀。 -```Makefile -foo = .a .b .c -bar = test src inc -res = $(foreach name,$(bar),$(foreach ext,$(foo),$(name)$(ext))) -``` - -### 6.5 if函数 - -`if` 语句很像`ifeq`,只是语法有不同: -```Makefile -$(if ,) -``` -或者 -```Makefile -$(if ,,) -``` - -只是将条件语句用一个函数的形式表达。其中的条件``如果返回非空字符串,那么相当于返回真,如果是空串则是假。返回值为`condition`对应的语句的执行结果,如果``为空,又没有``那么返回空。 - -当然``和``只会有一个被计算。 - -### 6.6 call函数 - -call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以call函数来向这个表达式传递参数。 - -当make执行这个函数时, `` 参数中的变量,如 `$(1)` 、 `$(2)` 等,会被参数 `` 、 `` 、 `` 依次取代。而 `` 的返回值就是 `call` 函数的返回值。 - -例:反转参数1和2。 -```Makefile -reverse = $(2) $(1) -foo = $(call reverse,a,b) -``` - -函数其实也是一个变量,参数中`$(0)`表示了函数名称。需要注意`call`调用中第二个及以后的参数中的空格会被保留,就像所有函数调用那样。最好的方式是`,`之间不要添加空格。 - -### 6.7 origin函数 - -```Makefile -$(origin ) -``` -注意, `` 是变量的名字,不应该是变量引用。所以你最好不要在 `` 中使用 `$` 字符。 - -`origin`函数不操作变量的值,只是返回这个变量的来源,一个字符串。可能结果如下: - -|结果|含义| -|:-|:-| -|`undefined`|从来没有定义过| -|`default`|默认的定义,比如`CC`这个变量| -|`environment`|环境变量,并且当Makefile被执行时, `-e` 参数没有被打开| -|`file`|定义在Makefile中| -|`command line`|被命令行定义的| -|`override`|被`override`指示符重新定义的| -|`automatic`|命令运行中的自动化变量| - -函数参数`$(1)`或者`foreach`中的临时变量都是自动化变量。 - - -### 6.8 shell函数 - -参数就是操作系统Shell的命令,把执行操作系统命令后的输出作为函数返回。 - -```Makefile -curDir = $(shell pwd) -``` - -`shell`函数会新生成一个Shell来执行命令,所以需要注意性能,如果定义了复杂规则并大量使用了`shell`函数,那么可能会有性能问题。 - -### 6.9 error & warning - -make提供了`error`函数来控制make的运行,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。 - -```Makefile -$(error ) -``` -`error`函数产生一个致命错误,参数是信息。不会一开始就产生,所以可以定义定义在一个变量中,后续的脚本中来使用这个变量。 - -```Makefile -$(warning ) -``` -而`warning`函数不会退出,只是输出警告信息,而make继续执行。 - -## 7. make的运行 - -一般来说直接键入`make`就可以执行默认目标,但有些时候可能只需要编译部分文件,Makefile定义了多套编译规则需要选择等。这里介绍如何使用make命令。 - -### 7.1 退出码 - -- 0 表示成功执行 -- 1 运行时出现错误 -- 2 使用了`-q`选项,并且make使得一些目标不需要更新,那么返回2 - -### 7.2 指定Makefile - -`-f FILE, --file=FILE, --makefile=FILE`参数,多次指定的话会连在一起传递给make执行。 - -### 7.3 指定目标 - -在执行make时指定终极目标,如果不指定则会是第一个目标。有一个make的环境变量叫 `MAKECMDGOALS` ,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。 - -GNU的开源软件发布时,Makefile中都包含了如下目标,包含了编译、安装、打包等功能,可以参照来写我们自己的Makefile的目标以显得更专业: - -- `all`:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。 -- `clean`:这个伪目标功能是删除所有被make创建的文件。 -- `install`:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。 -- `print`:这个伪目标的功能是列出改变过的源文件。 -- `tar`:这个伪目标功能是把源程序打包备份。也就是一个tar文件。 -- `dist`:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。 -- `TAGS`:这个伪目标功能是更新所有的目标,以备完整地重编译使用。 -- `check`和`test`:这两个伪目标一般用来测试makefile的流程。 - -也不必刻板遵循,只是一种软件工程实践,作为了解。 - -### 7.4 检查规则 - -有时候,并不想规则执行起来,只想检查一下命令,或者执行序列,可以使用如下参数: - -- `-n`, `--just-print`, `--dry-run`, `--recon` 不执行参数,只打印命令,不管命令是否更新,把规则和连带规则下的命令打印出来,但不执行。用于调试Makefile。 -- `-t`, `--touch`,没有目标的话touch一个空文件出来,有目标的话只更新时间戳而不重新按照规则生成。也就是假装编译了目标,把目标更新到最新状态,但其实并没有真正地编译目标。 -- `-q`, `--question` 如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。 -- `-W , --what-if=, --assume-new=, --new-file=` 这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。 - -常用的选项还有很多:`-e` `-f` `-f` `-i` `-I` `-k` `-r` `-s` `-w`可查看手册和帮助了解更多,具体使用时再详细了解。 - -## 8. 隐含规则 - -“隐含规则”(隐含规则)也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。 - -例如由`.c`生成`.o`。 - -“隐含规则”会使用一些我们系统变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。如系统变量 CFLAGS 可以控制编译时的编译器参数。 - -使用“模式规则”会更加得智能和清楚,但“后缀规则”可以用来保证我们Makefile的兼容性。有时候“隐含规则”也会给我们造成不小的麻烦,所以需要搞清楚。 - - - -### 8.1 使用隐含规则 - -如果要使用隐含规则生成你需要的目标,你所需要做的就是不要写出这个目标的规则。make会试图去自动推导产生这个目标的规则和命令,如果 make可以自动推导生成这个目标的规则和命令,那么这个行为就是隐含规则的自动推导。 - -make会在自己的“隐含规则”库中寻找可以用的规则,如果找到,那么就会使用。如果找不到,那么就会报错。 - -make和我们约定好了用C编译器 `cc` 生成 `.o` 文件的规则,这就是隐含规则。 -```Makefile -foo.o : foo.c - cc –c foo.c $(CFLAGS) -``` -当然,如果我们为 .o 文件书写了自己的规则,那么make就不会自动推导并调用隐含规则,它会按照我们写好的规则忠实地执行。 - -还有,在make的“隐含规则库”中,每一条隐含规则都在库中有其顺序,越靠前的则是越被经常使用的,所以,这会导致我们有些时候即使我们显示地指定了目标依赖,make也不会管。 - -如果确实不希望任何隐含规则推导,那么,就不要只写出“依赖规则”,而要把生成命令一并写出来。 - -### 8.2 隐含规则一览 - -使用`-r, --no-builtin-rules`选项来取消所有预设值的隐含规则。当然,即使指定了`-r`还是会有一些隐含规则生效,因为许多隐含规则都是使用了“后缀规则”来定义的。 - -常用隐含规则: -1. 编译C程序的隐含规则。 -`.o` 的目标的依赖目标会自动推导为 `.c` ,并且其生成命令是 `$(CC) –c $(CPPFLAGS) $(CFLAGS)`。 -2. 编译C++程序的隐含规则。 -`.o` 的目标的依赖目标会自动推导为 `.cc` 或是 `.C` (那`.cpp`呢?),并且其生成命令是 `$(CXX) –c $(CPPFLAGS) $(CFLAGS)` 。(建议使用 `.cc` 作为C++源文件的后缀,而不是 `.C` ) -3. 编译Pascal程序的隐含规则。 -`.o` 的目标的依赖目标会自动推导为 `.p` ,并且其生成命令是 `$(PC) –c $(PFLAGS)` -4. 编译Fortran/Ratfor程序的隐含规则。 -`.o` 的目标的依赖目标会自动推导为 `.r` 或 `.F` 或 `.f` ,并且其生成命令是: - - `.f` `$(FC) –c $(FFLAGS)` - - `.F` `$(FC) –c $(FFLAGS) $(CPPFLAGS)` - - `.f` `$(FC) –c $(FFLAGS) $(RFLAGS)` -5. 预处理Fortran/Ratfor程序的隐含规则。 -`.f` 的目标的依赖目标会自动推导为 `.r` 或 `.F` 。这个规则只是转换 Ratfor 或有预处理的Fortran程序到一个标准的Fortran程序。其使用的命令是: - - `.F` `$(FC) –F $(CPPFLAGS) $(FFLAGS)` - - `.r` `$(FC) –F $(FFLAGS) $(RFLAGS)` -6. 编译Modula-2程序的隐含规则。 -`.sym` 的目标的依赖目标会自动推导为 `.def` ,并且其生成命令是: `$(M2C) $(M2FLAGS) $(DEFFLAGS)` 。 `.o` 的目标的依赖目标会自动推导为 `.mod` ,并且其生成命令是: `$(M2C) $(M2FLAGS) $(MODFLAGS)` 。 -7. 汇编和汇编预处理的隐含规则。 -`.o` 的目标的依赖目标会自动推导为 `.s` ,默认使用编译器 `as` ,并且其生成命令是: $ `(AS) $(ASFLAGS)` 。 `.s` 的目标的依赖目标会自动推导为 `.S` ,默认使用C预编译器 `cpp` ,并且其生成命令是: `$(AS) $(ASFLAGS)` 。 -8. 链接Object文件的隐含规则。 -`` 目标依赖于 `.o` ,通过运行C的编译器来运行链接程序生成(一般是 `ld` ),其生成命令是: `$(CC) $(LDFLAGS) .o $(LOADLIBES) $(LDLIBS)` 。这个规则对于只有一个源文件的工程有效,同时也对多个Object文件(由不同的源文件生成)的也有效。 -9. Yacc C程序时的隐含规则。 -`.c` 的依赖文件被自动推导为 `n.y` (Yacc生成的文件),其生成命令是: `$(YACC) $(YFALGS)` 。(“Yacc”是一个语法分析器,关于其细节请查看相关资料) -10. Lex C程序时的隐含规则。 -`.c` 的依赖文件被自动推导为 `n.l` (Lex生成的文件),其生成命令是: `$(LEX) $(LFALGS)` 。(关于“Lex”的细节请查看相关资料) -11. Lex Ratfor程序时的隐含规则。 -`.r` 的依赖文件被自动推导为 `n.l` (Lex生成的文件),其生成命令是: `$(LEX) $(LFALGS)` 。 -12. 从C程序、Yacc文件或Lex文件创建Lint库的隐含规则。 -`.ln` (lint生成的文件)的依赖文件被自动推导为 `n.c` ,其生成命令是: `$(LINT) $(LINTFALGS) $(CPPFLAGS) -i` 。对于 `.y` 和 `.l` 也是同样的规则。 - -Pascal/Fortran/Rational Fortran/Modula-2语言,Yacc和Lex,Lint相关的程序目前没用过可以不用管,知道基本的C/C++编译、汇编、链接规则即可。重点关注1,2,7,8条。 - -### 8.3 隐含规则中的变量 - -隐含规则中都使用了预先定义的变量,可以在Makefile中改变这些值,或者在make命令行传入这些值。无论如何,只要设置了就对隐含规则起作用。可以使用make的`-R, --no-builtin-variables`选项来取消文件中定义变量对隐含规则的作用。 - -隐含规则使用的变量有两种,命令相关和规则相关。 - -#### 命令相关变量 - -- `AR` : 函数库打包程序。默认命令是 `ar` -- `AS` : 汇编语言编译程序。默认命令是 `as` -- `CC` : C语言编译程序。默认命令是 `cc` -- `CXX` : C++语言编译程序。默认命令是 `g++` -- `CO` : 从 RCS文件中扩展文件程序。默认命令是 `co` -- `CPP` : C程序的预处理器(输出是标准输出设备)。默认命令是 `$(CC) –E` -- `FC` : Fortran 和 Ratfor 的编译器和预处理程序。默认命令是 `f77` -- `GET` : 从SCCS文件中扩展文件的程序。默认命令是 `get` -- `LEX` : Lex方法分析器程序(针对于C或Ratfor)。默认命令是 `lex` -- `PC` : Pascal语言编译程序。默认命令是 `pc` -- `YACC` : Yacc文法分析器(针对于C程序)。默认命令是 `yacc` -- `YACCR` : Yacc文法分析器(针对于Ratfor程序)。默认命令是 `yacc –r` -- `MAKEINFO` : 转换Texinfo源文件(.texi)到Info文件程序。默认命令是 `makeinfo` -- `TEX` : 从TeX源文件创建TeX DVI文件的程序。默认命令是 `tex` -- `TEXI2DVI` : 从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是 `texi2dvi` -- `WEAVE` : 转换Web到TeX的程序。默认命令是 `weave` -- `CWEAVE` : 转换C Web 到 TeX的程序。默认命令是 `cweave` -- `TANGLE` : 转换Web到Pascal语言的程序。默认命令是 `tangle` -- `CTANGLE` : 转换C Web 到 C。默认命令是 `ctangle` -- `RM` : 删除文件命令。默认命令是 `rm –f` - -#### 参数相关变量 - -下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。 - -- `ARFLAGS` : 函数库打包程序AR命令的参数。默认值是 `rv` -- `ASFLAGS` : 汇编语言编译器参数。(当明显地调用 `.s` 或 `.S` 文件时) -- `CFLAGS` : C语言编译器参数。 -- `CXXFLAGS` : C++语言编译器参数。 -- `COFLAGS` : RCS命令参数。 -- `CPPFLAGS` : C预处理器参数。( C 和 Fortran 编译器也会用到)。 -- `FFLAGS` : Fortran语言编译器参数。 -- `GFLAGS` : SCCS “get”程序参数。 -- `LDFLAGS` : 链接器参数。(如: `ld` ) -- `LFLAGS` : Lex文法分析器参数。 -- `PFLAGS` : Pascal语言编译器参数。 -- `RFLAGS` : Ratfor 程序的Fortran 编译器参数。 -- `YFLAGS` : Yacc文法分析器参数。 - -### 8.4 隐含规则链 - -有的时候一个目标可能被一系列规则作用,比如`.o`可能先由Yacc的`.y`生成`.c`,再被C编译器生成。这一系列隐含规则称为隐含规则链。 - -上面例子中的`.c`叫做中间目标,对于中间目标,make会努力自动推导,但和一般目标有两个不同: -- 除非中间目标不存在,才会引发中间规则。 -- 只要最终目标成功生成,那么在产生过程中,所产生的中间目标会被`rm -f`删除。 - -通常,一个Makefile指定为目标或者依赖目标的文件不能作为中介,但是可以使用`.INTERMEDIATE : target`强制声明一个目标是中介,然后将其放在依赖中就可以执行隐式规则。 - -可以阻止make删除中间目标,只需要使用`.SECONDARY : sec`来声明,或者使用模式声明`.PRECIOUS : %.o`。 - -隐含规则链中会禁止一个目标出现两次,为了防止无限递归。 - -### 8.5 定义模式规则 - -可以使用模式规则来定义一个自己的隐含规则。模式规则和一般的区别就是,目标的定义需要有`%`字符,依赖中同样可以有`%`,其值就是目标中`%`代表的值。 - -值得注意的是:`%`的展开在运行时,不像变量和函数的展开在载入Makefile时。 - -#### 模式规则 - -目标中`%`表任意长的字符串,用来匹配文件名,如`%.c`表示`.c`后缀的文件(文件名至少三个字符长),`s.%.c`表示`s.`开头`.c`结尾的文件(至少5个字符)。 - -例,将`.c`编译为`.o`的规则(其实前面介绍过) -```Makefile -%.o : %.c - %(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ -``` -#### 自动化变量 - -自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。 - -- `$@`: 规则中的目标文件集合。如果有多个目标,那么, `$@` 就是匹配于目标中模式定义的集合。 -- `$%`: 仅当目标是函数库文件中,表示规则中的目标成员名。如果目标不是函数库文件(Unix下是 `.a` ,Windows下是 `.lib` ),那么,其值为空。比如如果目标时`foo.a(foo.o)`则`$%`为`foo.o`,`$@`是`foo.a`。 -- `$<`: 依赖中的第一个名字。如果依赖目标是以模式(即 `%` )定义的,那么 `$<` 将是符合模式的一系列的文件集。 -- `$?`: 所有比目标新的依赖目标的集合。以空格分隔。 -- `$^`: 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。 -- `$+`: 这个变量很像 `$^` ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。 -- `$*`: 这个变量表示目标模式中 `%` 及其之前的部分。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用 `$*` ,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么 `$*` 就是空值。 - -当你希望只对更新过的依赖文件进行操作时, `$?` 在显式规则中很有用。 - -上述变量中:四个变量(`$@` 、 `$<` 、 `$%` 、` $*`)在扩展时只会有一个文件,而另三个的值是一个文件列表。 - -上述自动变量只需要搭配上 `D` 或 `F` 字样就可以实现取目录或者文件,同样功能也可以用`dir`和`notdir`实现。 - -这些变量都是使用在显示规则或者静态模式规则的命令中。 - -#### 模式的匹配 - -一般来说,一个目标的模式有一个有前缀或是后缀的 `%` ,或是没有前后缀,直接就是一个 `%`。们把 `%` 所匹配的内容叫做“茎”。 - -当一个模式匹配包含有斜杠(实际也不经常包含)的文件时,那么在进行模式匹配时,目录部分会首先被移开,然后进行匹配,成功后,再把目录加回去。在进行“茎”的传递时,我们需要知道这个步骤。 - -比如:规则`e%t : c%r`中,一个模式`e%t`匹配了`src/eat`那么,茎就是`src/a`,如果把茎传递给`c%r`,那么就匹配`src/car`。 - -#### 重载内建的隐含规则 - -通过模式规则,可以重新构建与内建隐含规则不同的骨子额: -```Makefile -%.o : %.c - $(CC) -c $(CPPFLAGS) $(CFLAGS) -D$(date) -``` - -也可以取消内建的隐含规则,只要不在后面写命令就行 -```Makefile -%.o : %.s -``` - -### 8.6 老式风格的后缀规则 - -老版本的用法,新版本都应该使用模式规则: -```Makefile -# 后缀规则 -.c.o: - $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $< -# 等价于 -%.o : %.c - $(CC) -c $(CPPFLAGS) $(CFLAGS) -D$(date) -``` -后缀规则不允许任何的依赖文件,如果有依赖文件的话,那就不是后缀规则,那些后缀统统被认为是文件名。 - -而要让make知道一些特定的后缀,我们可以使用伪目标 `.SUFFIXES` 来定义或是删除。 - -make的参数 `-r` 或 `-no-builtin-rules` 也会使用得默认的后缀列表为空。而变量 `SUFFIXE` 被用来定义默认的后缀列表,你可以用 `.SUFFIXES` 来改变后缀列表,但请不要改变变量 `SUFFIXE` 的值。 - -新编写的Makefile不要使用后缀规则,知道是何含义即可。所有的后缀规则在Makefile被载入内存时,会被转换成模式规则。 - -### 8.6 隐含规则搜索算法 - -1. 把T的目录部分分离出来。叫D,而剩余部分叫N。(如:如果T是 src/foo.o ,那么,D就是 src/ ,N就是 foo.o ) -2. 创建所有匹配于T或是N的模式规则列表。 -3. 如果在模式规则列表中有匹配所有文件的模式,如 % ,那么从列表中移除其它的模式。 -4. 移除列表中没有命令的规则。 -5. 对于第一个在列表中的模式规则: - 1. 推导其“茎”S,S应该是T或是N匹配于模式中 % 非空的部分。 - 2. 计算依赖文件。把依赖文件中的 % 都替换成“茎”S。如果目标模式中没有包含斜框字符,而把D加在第一个依赖文件的开头。 - 3. 测试是否所有的依赖文件都存在或是理当存在。(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件,那么这个文件就叫“理当存在”) - 4. 如果所有的依赖文件存在或是理当存在,或是就没有依赖文件。那么这条规则将被采用,退出该算法。 -6. 如果经过第5步,没有模式规则被找到,那么就做更进一步的搜索。对于存在于列表中的第一个模式规则: - 1. 如果规则是终止规则,那就忽略它,继续下一条模式规则。 - 2. 计算依赖文件。(同第5步) - 3. 测试所有的依赖文件是否存在或是理当存在。 - 4. 对于不存在的依赖文件,递归调用这个算法查找他是否可以被隐含规则找到。 - 5. 如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。那么这条规则被采用,退出该算法。 - 6. 如果没有隐含规则可以使用,查看 .DEFAULT 规则,如果有,采用,把 .DEFAULT 的命令给T使用。 - -一旦规则被找到,就会执行其相当的命令,而此时,我们的自动化变量的值才会生成。 - -## 9. 使用make更新函数库文件 - -函数库文件也就是对Object文件(程序编译的中间文件)的打包文件。在Unix下,一般是由命令 `ar` 来完成打包工作。 - -### 9.1 函数库文件的成员 - -一个函数库由多个文件组成,可以由如下格式指定: -```Makefile -archive(member) -``` - -这个不是一个命令,而一个目标和依赖的定义。一般来说,这种用法基本上就是为了 `ar` 命令来服务的。 -```Makefile -foolib(hack.o) : hack.o - ar cr foolib hack.o -``` - -如果有多个成员,可以用空格隔开。 -```Makefile -foolib(hack.o kludge.o) -``` -等价于: -```Makefile -foolib(hack.o) foolib(kludge.o) -``` -还可以使用Shell的文件通配符: -```Makefile -foolib(*.o) -``` - -### 9.2 函数库成员的隐含规则 - -当make搜索一个目标的隐含规则时,一个特殊的特性是,如果这个目标是 `a(m)` 形式的,其会把目标变成 `(m)`。比如使用 `make foo.a(bar.o)` 的形式调用Makefile时,隐含规则会去找 `bar.o` 的规则,如果没有定义 `bar.o` 的规则,那么内建隐含规则生效,make会去找 `bar.c` 文件来生成 `bar.o`,如果找到了,那么大致的执行命令如下: -```Makefile -cc -c bar.c -o bar.o -ar r foo.a bar.o -rm -f bar.o -``` - -`$%`是专属库文件的自动化变量。 - -在进行函数库打包文件生成时,请小心使用make的并行机制( `-j` 参数)。如果多个 `ar` 命令在同一时间运行在同一个函数库打包文件上,就很有可以损坏这个函数库文件。目前而言,尽量不要使用 `-j` 参数。 - -## 10. 总结 - -重点: -- 规则:目标、依赖和命令。 -- 善用`%`静态模式、`gcc -MM`自动生成依赖。 -- 变量:`=` `:=` `+=` `?=` `override` 多行、目标与模式变量。变量都是字符串。 -- make相关环境变量,嵌套执行时的变量传递。 -- 条件:`ifeq` `ifneq` `ifdef` `ifndef`。 -- 函数: - - 字符串处理:替换、模式替换、查找、筛选、排序、字符串取单词。 - - 文件名操作:目录、文件名、前缀、后缀、连接。 - - `foreach`循环、`if`条件、`origin`变量来源、`shell`。 - - `error` & `warning`。 -- make的调试相关参数。 -- 隐含规则:各种自动化变量:`$@` `$<` `$^` `$?` `$%` `$+` `$*`。 - -## 11. 结语 - -花了两天多时间,看了一遍,本文主要来源于[跟我一起写Makefile](https://seisman.github.io/how-to-write-makefile/index.html),可以理解为简略的摘抄和实践总结,最终来源应该还是[GNU Make文档](https://www.gnu.org/software/make/manual/html_node/index.html)。理解还很浅,还需要后续多读多写结合实践加深理解。 - -术语 `foobar` , `foo` , `bar` , `baz` 和 `qux` 经常在计算机编程或计算机相关的文档中被用作 占位符 的名字。当变量、函数、或命令本身不太重要的时候, `foobar` , `foo` , `bar` ,`baz` 和 `qux` 就被用来充当这些实体的名字,这样做的目的仅仅是阐述一个概念,说明一个想法。这些术语本身相对于使用的场景来说没有任何意义。就像我们写一个没有具体含义的示例时经常使用hello,world一样。 diff --git a/MathFunc.md b/MathFunc.md deleted file mode 100644 index 8e4b311..0000000 --- a/MathFunc.md +++ /dev/null @@ -1,111 +0,0 @@ -# 如果没有math.h我们可以干什么? - -参考:[没有math.h我们能干啥?-知乎文章-王希](https://zhuanlan.zhihu.com/p/20085048) - -## 0. math.h里面有什么? - -打开GCC编译的`include`目录找到`math.h`打开可以看到,常用的数学函数如下: -```C++ -_CRTIMP double __cdecl sin (double); // 正弦 -_CRTIMP double __cdecl cos (double); // 余弦 -_CRTIMP double __cdecl tan (double); // 正切 -_CRTIMP double __cdecl sinh (double); // 双曲正弦 -_CRTIMP double __cdecl cosh (double); // 双曲余弦 -_CRTIMP double __cdecl tanh (double); // 双曲正切 -_CRTIMP double __cdecl asin (double); // 反正弦 -_CRTIMP double __cdecl acos (double); // 反余弦 -_CRTIMP double __cdecl atan (double); // 反正切 -_CRTIMP double __cdecl atan2 (double, double); // 反正切 -_CRTIMP double __cdecl exp (double); // e为底指数 -_CRTIMP double __cdecl log (double); // e为底对数 -_CRTIMP double __cdecl log10 (double); // 10为底对数 -_CRTIMP double __cdecl pow (double, double); // 实数的实数次方 -_CRTIMP double __cdecl sqrt (double); // 开方 -_CRTIMP double __cdecl ceil (double); // 向负无穷方向取整 -_CRTIMP double __cdecl floor (double); // 向正无穷方向取整 -_CRTIMP double __cdecl fabs (double); // 绝对值 -_CRTIMP double __cdecl ldexp (double, int); // 计算前者乘以e的后面这个数次方的乘积 -_CRTIMP double __cdecl frexp (double, int*); // 将实数分解为x*2^exp次方形式 -_CRTIMP double __cdecl modf (double, double*); // 得到一个浮点数的整数和小数部分 -_CRTIMP double __cdecl fmod (double, double); // 实数对实数求余 -``` - -- 比较有用的函数 - - 三角函数:sin, cos, tan - - 反三角函数:asin, acos, atan, atan2 - - 双曲函数:sinh, cosh, tanh - - 绝对值:abs, fabs - - 求余:mod, fmod, modf - - 开方: sqrt - - 幂函数:pow, powf - - 指数函数:exp - - 对数函数:log, log10 - - 取整函数:ceil, floor - -## 1. 基本常数 -```C++ -#define PI 3.14159265358979323846 -#define e 2.7182818284590452354 -#define ln_2 0.69314718055994530942 -#define ln_10 2.30258509299404568402 -``` - -这些宏预处理展开之后会作为双精度浮点型也就是`double`字面值,一般来说长度是64位,其中1位符号位,11位指数位,52位尾数位。换算成十进制的话尾数部分有效位数能够达到15~16位左右。为保证达到IEEE754双精度浮点数能表示的精度极限,常量的定义最好至少定义到小数点后16位。如果需要更高的精度请使用`long double`字面值。 - -## 2. 取绝对值 - -不必多说,有手就行。 -```C++ -double fabs(double x) -{ - return x > 0 ? x : -x; -} -``` -或者使用宏: -```C++ -#define fabs(x) ((x) > 0 ? (x) : (-x)) -``` -当然定义为宏的话,传入表达式可能会造成重复计算,所以还是最好定义为函数。 - -## 3. 实数的整数次方和实数的实数次方 - -实数的整数次方,快速幂就行。 -```C++ -double pow(double a, int n) -{ - if (n < 0) - return 1/pow(a,-n); - double res = 1.0; - while (n) - { - if (n&1) - res *= a; - a *= a; - n >>= 1; - } - return res; -} -``` -然后根据实数的整数次方求实数的实数此方,根据 $a^x = \exp (x \ln a)$,那么只有有了`exp`以e为底的指数函数和`ln`以e为底的对数函数,那么就可以实现了。 -```C++ -double exp(double x); // e^x -double ln(double x); // ln(x) -double powf(double a, double x) -{ - return exp(x*ln(a)); -} -``` -这里需要e为底指数函数,和e为底对数函数,先声明,后续来实现。 - - -## 4. e为底指数 - - - -## TODO - -- 标准库函数实现分析 -- BenchMark - - - diff --git a/MySTL.md b/MySTL.md deleted file mode 100644 index d3ffac1..0000000 --- a/MySTL.md +++ /dev/null @@ -1,3 +0,0 @@ -## 个人STL实现 - -见 [tch0/MySTL](https://github.com/tch0/MySTL)。 \ No newline at end of file diff --git a/Nginx.md b/Nginx.md deleted file mode 100644 index cd6e97b..0000000 --- a/Nginx.md +++ /dev/null @@ -1,296 +0,0 @@ - - -**目录** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Nginx](#nginx) - - [0. Nginx简介](#0-nginx%E7%AE%80%E4%BB%8B) - - [0.1 什么是Nginx](#01-%E4%BB%80%E4%B9%88%E6%98%AFnginx) - - [0.2 Nginx基本特性](#02-nginx%E5%9F%BA%E6%9C%AC%E7%89%B9%E6%80%A7) - - [0.3 Nginx相关资料](#03-nginx%E7%9B%B8%E5%85%B3%E8%B5%84%E6%96%99) - - [1. Mginx概念](#1-mginx%E6%A6%82%E5%BF%B5) - - [1.1 反向代理](#11-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86) - - [1.2 负载均衡](#12-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1) - - [1.3 动静分离](#13-%E5%8A%A8%E9%9D%99%E5%88%86%E7%A6%BB) - - [2. Nginx使用](#2-nginx%E4%BD%BF%E7%94%A8) - - [2.1 安装Nginx](#21-%E5%AE%89%E8%A3%85nginx) - - [2.2 常用命令](#22-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4) - - [2.3 配置文件](#23-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) - - [3. Nginx配置实例](#3-nginx%E9%85%8D%E7%BD%AE%E5%AE%9E%E4%BE%8B) - - [3.1 反向代理实例](#31-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E5%AE%9E%E4%BE%8B) - - [3.2 负载均衡实例](#32-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E5%AE%9E%E4%BE%8B) - - [3.3 动静分离实例](#33-%E5%8A%A8%E9%9D%99%E5%88%86%E7%A6%BB%E5%AE%9E%E4%BE%8B) - - [3.4 Nginx配置高可用集群](#34-nginx%E9%85%8D%E7%BD%AE%E9%AB%98%E5%8F%AF%E7%94%A8%E9%9B%86%E7%BE%A4) - - [4. Nginx原理](#4-nginx%E5%8E%9F%E7%90%86) - - [5. 参考资料](#5-%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - [TODO](#todo) - - - -# Nginx - -![Nginx](Images/nginx_logo.png) - -## 0. Nginx简介 - -### 0.1 什么是Nginx - -发音:engine X - -简介: -- Nginx是一款轻量级的Web服务器、反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。 -- 其特点是占有内存少,并发能力强,高性能。 -- 专为性能而开发,性能是其最重要的考量,实现上非常注重效率。 - -### 0.2 Nginx基本特性 - -- 处理静态文件,索引文件以及自动索引。 -- 反向代理加速(无缓存),简单的负载均衡和容错。 -- FastCGI,简单的负载均衡和容错。 -- 模块化的结构。过滤器包括gzipping, byte ranges, chunked responses, 以及 SSI-filter 。在SSI过- 滤器中,到同一个 proxy 或者 FastCGI 的多个子请求并发处理。 -- SSL 和 TLS SNI 支持。 - - -### 0.3 Nginx相关资料 - -- [官网](https://www.nginx.com/) -- [源码Github镜像](https://github.com/nginx/nginx) -- [Nginx中文文档](https://www.nginx.cn/doc/) -- [菜鸟教程Nginx安装配置](https://www.runoob.com/linux/nginx-install-setup.html) - - -## 1. Mginx概念 - -### 1.1 反向代理 - -Nginx 不仅可以做反向代理,实现负载均衡。还能用作正向代理来进行上网等功能。 -**正向代理**:如果把局域网外的 Internet 想象成一个巨大的资源库,则局域网中的客户端要访问 Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理。**在客户端中都需要配置代理服务器。** - -**反向代理**:反向代理,其实客户端对代理是无感知的,因为**客户端不需要任何配置就可以访问**,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器 IP 地址。 - -现在有了一个大概的概念,但完全不清楚细节。 - -### 1.2 负载均衡 - -客户端发送多个请求到服务器,服务器处理请求,有一些可能要与数据库进行交互,服务器处理完毕后,再将结果返回给客户端。 - -这种架构模式对于早期的系统相对单一,并发请求相对较少的情况下是比较适合的,成本也低。但是随着信息数量的不断增长,访问量和数据量的飞速增长,以及系统业务的复杂度增加,这种架构会造成服务器相应客户端的请求日益缓慢,并发量特别大的时候,还容易造成服务器直接崩溃。 - -由于单台服务器配置提升是有上限的,而且成本高,所以就需要横向增加服务器的数量。这时候集群的概念产生了,单个服务器解决不了,增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为由**反向代理服务器**(NginX就是充当的这个角色)将请求分发到多个服务器上,将负载分发到不同的服务器,这就是**负载均衡**。 - -### 1.3 动静分离 - -为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度。降低原来单个服务器的压力。 - -Tomcat服务器资源: -- 静态资源:html/css/js -- 动态资源:jsp/servelet - -将其分开部署到不同的Tomcat服务器就叫做动静分离。 - -## 2. Nginx使用 - -### 2.1 安装Nginx - -- [官网下载地址](http://nginx.org/) -- Ubuntu: `apt install nginx` -- CentOS: `yum install nginx` - -我的环境:Ubuntu18.04 Server,什么环境不重要,也可以在本地的WSL或者虚拟机上安装 -- 查看软件安装路径:`whereis nginx` -- 查看运行文件路径:`which nginx`, 路径是`usr/sbin/nginx` -- 配置文件路径:`/etc/nginx/` -- 启动: `nginx` -- 防火墙:CentOS中是`Firewalld`,Ubuntu中默认是`ufw` -- `ufw staus`查看防火墙状态,如果端口未开放,可以添加使其开放。或者直接把防火墙关了也行,防火墙相关内容不在这里详述。 - -### 2.2 常用命令 - -- 启动:`nginx` -- 版本:`nginx -v` -- 关闭:`nginx -s stop` -- 安全退出: `nginx -s quit` -- 重新加载配置文件:`nginx -s reload` -- 检查配置文件语法:`nginx -t` -- 执行`nginx`启动后就可以在主机地址上看到Nginx默认页面了。 -- `ps -aux | grep nginx`查看nginx进程 - -### 2.3 配置文件 - -- 主配置文件:`/etc/nginx/nginx.conf` -- 一个配置文件内容: -```conf -user root; -worker_processes auto; -pid /run/nginx.pid; -include /etc/nginx/modules-enabled/*.conf; - -events { - worker_connections 768; - # multi_accept on; -} - -http { - server { - keepalive_requests 120; - listen 80; - server_name 127.0.0.1; - location / { - root /var/www/blog; - index index.html; - } - } - - ## - # Basic Settings - ## - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - # server_tokens off; - - # server_names_hash_bucket_size 64; - # server_name_in_redirect off; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - ## - # SSL Settings - ## - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE - ssl_prefer_server_ciphers on; - - ## - # Logging Settings - ## - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - ## - # Gzip Settings - ## - - gzip on; - - # gzip_vary on; - # gzip_proxied any; - # gzip_comp_level 6; - # gzip_buffers 16 8k; - # gzip_http_version 1.1; - # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - ## - # Virtual Host Configs - ## - - include /etc/nginx/conf.d/*.conf; - include /etc/nginx/sites-enabled/*; - - -} - - -#mail { -# # See sample authentication script at: -# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript -# -# # auth_http localhost/auth.php; -# # pop3_capabilities "TOP" "USER"; -# # imap_capabilities "IMAP4rev1" "UIDPLUS"; -# -# server { -# listen 80; -# protocol pop3; -# proxy on; -# } -# -# server { -# listen localhost:143; -# protocol imap; -# proxy on; -# } -#} -``` - -其中分为三个块: -- 全局块:影响服务器整体运行的配置指令。 - ```conf - user www-data; # 用户 - worker_processes auto; # 允许生成的进程数 - pid /run/nginx.pid; # 进程pid存放路径 - include /etc/nginx/modules-enabled/*.conf; - ``` -- `events`块: Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 worker process 下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 worker process 可以同时支持的最大连接数等。 - ```conf - worker_connections 768; # 支持最大连接数 - ``` -- `http`块:Nginx服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里。`http`块也可以包括 `http`全局块、`server`块。 - - `http`全局块配置的指令包括文件引入、`MIME-TYPE` 定义、日志自定义、连接超时时间、单链接请求数上限等。 - - `server`块:这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。 - - 每个`http`块可以包括多个`server`块,而每个`server`块就相当于一个虚拟主机。 - - 而每个`server`块也分为全局`server`块,以及可以同时包含多个`location`块。 - - 全局`server`块:最常见的配置是本虚拟机主机的监听配置和本虚拟主机的名称或IP配置。 - - `location`块:一个`server`块可以配置多个`location`块。这块的主要作用是基于 Nginx 服务器接收到的请求字符串(例如 `server_name/uri-string`),对虚拟主机名称(也可以是 IP 别名)之外的字符串(例如 前面的 `/uri-string`)进行匹配,对特定的请求进行处理。地址定向、数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行。 - -## 3. Nginx配置实例 - -### 3.1 反向代理实例 - -**实现效果**:浏览器地址栏输入`www.123.com`,跳转到linux系统tomcat主页面。 - -**装备工作**: -1. Linux中安装tomcat,使用默认端口8080。 - - [官网下载](https://tomcat.apache.org/),有好几个版本,不同的版本好像对应不同的Servlet/JSP/WebSocket版本。姑且先装一个10.0.x吧,当然也不一定越新越好,这里测试用啥版本都行。本地下载后传输到Linux服务器比较方便,使用`rz`(服务器received)命令上传,`sz`(服务器send)下载文件到本地,使用`apt install lrzsz`安装。如果在WSL里面的话,是可以直接在`/mnt`找到挂载的硬盘的,直接`cp`过去即可。VMware虚拟机的话安装了VMTools,直接拖进去就行。 - - 得到`apache-tomcat-10.0.2.tar.gz`之后,放到`/opt`,解压`tar -xvf [file]`。 - - 进入解压后的`bin/`目录,执行`./startup.sh`启动tomcat。 - - 未安装JDk的话需要安装JDK才能成功执行。(TODO: Java相关的东西不了解,先挖坑,以后填了记得更新这里。)这里装`apt install openjdk11-jre-headless`,只要tomcat支持这个版本就行,都是向后支持,安装最新的JDK则一定是支持的(这里服务器包管理上最新只有这个,应该不算最新的版本)。执行`java -version`查看版本。 - - 然后回到tomcat的`bin`目录执行`./startup.sh`启动tomcat,就可以在`yourhost:8080`上看到tomcat页面了,端口是必不可少的。 - ![tomcat page](Images/nginx_first_tomcat10.0.2_page.png) -2. 检查8080端口是否开放,防火墙相关,如果禁用了需要开放8080端口,不赘述,但要有这个意识。 -3. 修改Host文件(管理员权限打开`C:\Windows\System32\drivers\etc\hosts`)添加`your_host_ip www.123.com`项。无论是局域网IP还是公网IP都是可行的。 - -**Nginx配置反向代理**: -- 编辑配置文件`/etc/nginx/nginx.conf`。 -- `http`域中的`server`域添加或修改: -```conf -server { # 将发送给Nginx服务器192.168.35.1:80的请求转发到127.0.0.1:8080 - listen 80; - server_name 192.168.35.1; # 主机IP - location \ { - # root html; - proxy_pass http://127.0.0.1:8080; # 转发到8080端口 - index index.html index.htm; - } -} -``` -- `nginx`启动Nginx,浏览器输入`www.123.com`即可访问。 -- 网页的数据一般放在:`/var/www/`。 - -**访问过程分析**: -- 三个部分:客户端(浏览器),代理服务器(Nginx),Web服务器(tomcat)。 -- 套接字:Nginx(`you_host_ip:80`),tomcat(`127.0.0.1:8080`)。 -- 浏览器不能直接访问到tomcat,而是通过Nginx反向代理来访问到tomcat。Nginx负责转发请求到tomcat。 -- 需要修改本地host文件,将`www.123.com`指向你的主机IP,也就是说本地host文件优先于DNS解析。实际建立网站的话有了服务器之后还需要购买域名,解析到对应的IP即可。 - -### 3.2 负载均衡实例 - -### 3.3 动静分离实例 - -### 3.4 Nginx配置高可用集群 - -## 4. Nginx原理 - -## 5. 参考资料 - -- [Bilibili-尚硅谷Nginx教程](https://www.bilibili.com/video/BV1zJ411w7SV?from=search&seid=6880208276596727856)(本文主要参考) -- [Nginx中文文档](https://www.nginx.cn/doc/) - -## TODO - -- 相关内容了解学习:Java,tomcat,Web服务器 -- Nginx深入,如阅读源码 diff --git a/OperatorPrecedenceParser.md b/OperatorPrecedenceParser.md deleted file mode 100644 index 30efdb0..0000000 --- a/OperatorPrecedenceParser.md +++ /dev/null @@ -1,801 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [运算符优先级分析](#%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7%E5%88%86%E6%9E%90) - - [前缀、中缀、后缀表达式](#%E5%89%8D%E7%BC%80%E4%B8%AD%E7%BC%80%E5%90%8E%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [分析树](#%E5%88%86%E6%9E%90%E6%A0%91) - - [优先级与结合性](#%E4%BC%98%E5%85%88%E7%BA%A7%E4%B8%8E%E7%BB%93%E5%90%88%E6%80%A7) - - [中缀转后缀](#%E4%B8%AD%E7%BC%80%E8%BD%AC%E5%90%8E%E7%BC%80) - - [计算后缀表达式](#%E8%AE%A1%E7%AE%97%E5%90%8E%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [四则运算实例](#%E5%9B%9B%E5%88%99%E8%BF%90%E7%AE%97%E5%AE%9E%E4%BE%8B) - - [直接计算中缀表达式](#%E7%9B%B4%E6%8E%A5%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [递归计算中缀表达式](#%E9%80%92%E5%BD%92%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [参考](#%E5%8F%82%E8%80%83) - - - -# 运算符优先级分析 - -运算符优先级分析是一种自底向上的**语法分析**方法。而递归下降是一种自顶向下的分析方法。在一般的语法分析过程中,两者都能够独立处理。但是对于表达式的处理,由于运算符较多且具有多层优先级,递归下降会显得复杂且不够有效率。对于其他程序结构:变量函数声明、固定结构声明等运算符优先级会显得不够直观。所以一般的语法分析过程会将两者结合使用,运算符优先级分析用来处理表达式,递归下降用来处理其他部分。 - -[另一篇笔记](BNF&RecursiveDescent.md)详细介绍了BNF以及相关扩展语言以及递归下降。 - -## 前缀、中缀、后缀表达式 - -[中缀表示法](https://zh.wikipedia.org/wiki/%E4%B8%AD%E7%BC%80%E8%A1%A8%E7%A4%BA%E6%B3%95)(或中缀记法)是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间(例:3 + 4)。 - -[前缀表示法](https://zh.wikipedia.org/wiki/%E6%B3%A2%E5%85%B0%E8%A1%A8%E7%A4%BA%E6%B3%95)也叫波兰表示法,是一种逻辑、算术和代数表示方法,其特点是操作符置于操作数的前面。如果操作符的元数(arity)是固定的,则语法上不需要括号仍然能被无歧义地解析。 - -[后缀表示法](https://zh.wikipedia.org/wiki/%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E7%A4%BA%E6%B3%95),类似于前缀表达式,只是所有操作符置于操作数的后面。也成为逆波兰表达式。 - -中缀表达式中需要使用括号来确定运算顺序,比如`(1+2)*3`,加法优先级低于乘法,如果要先计算优先级更低的操作就必须使用括号。 - -而在运算符操作数确定的情况下(这里主要探讨二元运算符),前缀和后缀表达式就不存在这个问题,因此前缀和后缀表达式不需要括号来规定优先级。`(1+2)*3`改为前缀表达式为 `* + 1 2 3`。一个运算符后紧跟两个操作数,如果操作数是运算符,则是子表达式就需要先计算子表达式,已经暗含了计算顺序。同理后缀表达式为 `1 2 + 3 *`。从前往后计算的话前缀表达式是自顶向下的,而后缀表达式是自底向上的。 - - - -## 分析树 - -将一个表达式中运算符作为根节点,操作数或者子表达式作为子节点,递归的将一个表达式表示为树的话。就能得到其分析树。比如`(1+2)*3`: -``` - * - / \ - + 3 - / \ - 1 2 -``` -先序遍历这棵树就能得到前缀表达式,后序遍历就能得到后缀表达式,中序遍历则是中缀表达式。可以看做等价的结构,只有中缀表达式需要加括号表明计算顺序,但因为中缀表达式是最适合人类阅读的结构,所以依然是程序语言中最常见的结构。 - -当然计算中缀表达式就显得有点复杂,常见的做法是将其转换为后缀或者前缀表达式后再计算。 - -## 优先级与结合性 - -**优先级**指定运算的优先运算顺序,优先级越高应该越早计算,括号超越优先级或者说最高优先级。**结合性**是指从左往右运算还是从右往左运算。典型的比如[C语言运算符优先级](https://zh.cppreference.com/w/c/language/operator_precedence)。 - -某些运算符左结合右结合结果是相同的,比如加法、乘法,某些是不同的比如减法、除法。相同优先级的运算符原则上应该具有相同的结合性。 - -在C语言中,运算符的优先级和结合性派生自文法,是自然而然的。 -- 前缀的的一元运算符都是左结合,因为也只能左结合。`*&p`。 -- 后缀的一元运算符都是右结合。 -- 二元运算符基本都是左结合。 -- 二元的右结合运算符只有赋值和一些复合赋值的运算符。所有可以有这种语句`a = b = 10;`。 - -## 中缀转后缀 - -以中缀转后缀表达式为例。以一个存放运算符的栈就可以实现,算法: -- 遍历中缀表达式,遇到左括号时压栈。 -- 遇到操作数时直接输出。 -- 遇到运算符时,从栈顶开始依次将优先级高于或等于当前运算符的运算符出栈输出,直到栈空、遇到左括号(括号是用来划定运算顺序的运算符,不看做一般意义的运算符,但具有最高的优先级)、遇到更低优先级的运算符。才将当前运算符压栈。 -- 遇到右括号时,依次出栈运算符知道遇到左括号,然后将左括号出栈。 -- 知道表达式遍历完,依次出栈输出栈中所有运算符。 - -注意:出栈运算符是是直到遇到等于当前运算符优先级还是小于当前运算符优先级应该取决于当前运算符结合性,左结合就小于,右结合就等于。越先输出就越先计算。 - -例:`(1+2)*3`,左括号压栈,1输出,+压栈,输出2,右括号时+出栈,*压栈,输出3,输出*,结果`1 2 + 3 *`。 - -## 计算后缀表达式 - -计算后缀表达式同样使用一个保存中间结果的栈就可以完成,算法: -- 遍历后缀表达式,遇到操作数入栈。 -- 遇到操作符就从出栈需要个数的操作数(这就需要没有歧义,也就是一个操作符数量固定),计算完成后将计算结果压栈。直到结束。 -- 从栈顶取出运算结果。 - -例;`1 2 + 3 *`,1和2压栈,遇到+,出栈2和1,得到2压栈,3压栈,遇到*,出栈3和2,计算得到6。 - -## 四则运算实例 - -以四则运算为例实现表达式解析:中缀转后缀,再通过后缀表达式计算。 - -先实现词法分析: -```C++ -class Calculator -{ -public: - Calculator() : str(NULL), token(0), token_val(0) {} - - int getResult(); - int getPriority(int token); // 获取运算符优先级,用于比较 - void next(); - bool calculate(const char* s); // 直接计算,跳过中间转逆波兰的过程 - bool toPostfix(const char * s, string& postfix); // 将中缀表达式转换为后缀表达式(逆波兰表达式) - bool calculatePostfix(const char* s); // 计算逆波兰表达式 - -private: - enum { Number = 128 }; - const char* str; - int token; - int token_val; - int result; -}; - -void Calculator::next() -{ - token = 0; - while (str && (token = *str)) - { - str++; - if (token >= '0' && token <= '9') - { - token_val = token - '0'; - token = Number; - while (*str >= '0' && *str <= '9') - { - token_val = token_val * 10 + *str - '0'; - str++; - } - return; - } - else if (token != ' ' && token != '\t') - { - return; - } - } -} -``` - -中缀转后缀: -```C++ -bool Calculator::toPostfix(const char *s, string& postfix) -{ - if (s == NULL || *s == 0) - { - cout << "invalid input"; - return false; - } - str = s; - stack opStack; - next(); - while (token) - { - if (token == Number) - { - postfix += to_string(token_val); - postfix += " "; - } - else if (token == '(' || token == '+' || token == '-' || token == '*' || token == '/') - { - while (!opStack.empty() && opStack.top() != '(' && getPriority(opStack.top()) >= getPriority(token)) - { - // 栈中运算符优先级大于等于当前运算符,则先输出栈中的运算符,例外就是左括号时不输出(留着)来匹配),也就是直到遇到一个优先级更低的运算符 - postfix += opStack.top(); - postfix += " "; - opStack.pop(); - } - opStack.push(token); - } - else if (token == ')') // 找到匹配的左括号,输出()中间的内容 - { - while (!opStack.empty() && opStack.top() != '(') - { - postfix += opStack.top(); - postfix += " "; - opStack.pop(); - } - if (opStack.empty()) - { - cout << "miss left (" << endl; - return false; - } - else - { - opStack.pop(); - } - } - else if (token != 0) // token == 0表示结束 - { - cout << "invalid token " << token << endl; - return false; - } - next(); - } - while (!opStack.empty()) - { - if (opStack.top() == '(') - { - cout << "miss right )" << endl; - return false; - } - postfix += opStack.top(); - postfix += " "; - opStack.pop(); - } - return true; -} -``` -需要以一种方式确定运算符优先级: -```C++ -int Calculator::getPriority(int token) -{ - if (token == '(') - { - return 3; - } - else if (token == '*' || token == '/') - { - return 2; - } - else if (token == '+' || token == '-') - { - return 1; - } - return -1; -} -``` -计算后缀表达式; -```C++ -bool Calculator::calculatePostfix(const char * s) -{ - if (s == NULL || *s == 0) - { - cout << "invalid input reverse polish" << endl; - return false; - } - str = s; - stack numStack; - next(); - while (token) - { - if (token == Number) - { - numStack.push(token_val); - } - else if (token == '+' || token == '-' || token == '*' || token == '/') - { - if (numStack.size() < 2) - { - cout << "miss numbers" << endl; - return false; - } - int num2 = numStack.top(); - numStack.pop(); - int num1 = numStack.top(); - numStack.pop(); - if (token == '+') - { - result = num1 + num2; - } - else if (token == '-') - { - result = num1 - num2; - } - else if (token == '*') - { - result = num1 * num2; - } - else if (token == '/') - { - result = num1 / num2; - } - numStack.push(result); - } - else if (token != 0) // token == 0表示结束 - { - cout << "invalid token " << token << endl; - return false; - } - next(); - } - if (numStack.size() >= 2) - { - cout << "miss operators" << endl; - return false; - } - else - { - return true; - } -} -``` - -需要注意,在表达式合法的情况下,计算简单而优雅,表达式非法时如果要提供详细的错误信息1则需要仔细处理各种情况。 - -在上面的算法中,转后缀的过程中,只能识别括号没有匹配的错误,其他非法的表达式能够合法转为后缀,只能在后缀表达式计算时才能识别出来,比如缺少或者多了操作数的情况。而这时已经没有了原始的中缀表达式,已经不能确定具体错误的位置了,可能会进行错误的中间计算直到最后才能确定发生了错误。 - -## 直接计算中缀表达式 - -结合中缀转后缀计算后缀的过程,使用两个栈,一个保存运算符,一个保存操作数,就可以在直接计算中缀表达式。 - -将转后缀过程中遇到运算符输出改为遇到运算符就出栈两个操作数,运算并压栈即可。最后在依次出栈运算符然后取出操作数计算,计算最终结果。 - -那么四则运算就可改成: -```C++ -bool Calculator::calculate(const char* s) -{ - if (s == NULL || *s == 0) - { - cout << "invalid input" << endl; - return false; - } - str = s; - stack numStack; - stack opStack; - int last_token = 0; // 增加last_token辅助判断表达式是否合法,比如不能出现两个相连的number和两个相连的非括号运算符或者(*与/+)这种情况都是非法的。 - - next(); - while (token) - { - if (token == Number) - { - numStack.push(token_val); - } - else if (token == '+' || token == '-' || token == '*' || token == '/') - { - if (numStack.empty()) // 需要两个操作数,但前面没有操作数 - { - cout << "miss numbers" << endl; - return false; - } - if (opStack.empty() || opStack.top() == '(') - { - opStack.push(token); - } - else - { - while (!opStack.empty() && opStack.top() != '(' && getPriority(opStack.top()) >= getPriority(token)) - { - if (numStack.size() >= 2) // 需要两个操作数 - { - int num2 = numStack.top(); - numStack.pop(); - int num1 = numStack.top(); - numStack.pop(); - int lastOp = opStack.top(); - if (lastOp == '+') - { - result = num1 + num2; - } - else if (lastOp == '-') - { - result = num1 - num2; - } - else if (lastOp == '*') - { - result = num1 * num2; - } - else if (lastOp == '/') - { - result = num1 / num2; - } - else - { - cout << "invalid operator " << (char)lastOp << endl; - return false; - } - numStack.push(result); - opStack.pop(); - } - else - { - cout << "miss numbers" << endl; - return false; - } - } - opStack.push(token); - } - } - else if (token == '(') - { - opStack.push(token); - } - else if (token == ')') // 找到匹配的左括号,计算括号中表达式的值 - { - while (!opStack.empty() && opStack.top() != '(') - { - int lastOp = opStack.top(); - opStack.pop(); - if (numStack.size() >= 2) - { - int num2 = numStack.top(); - numStack.pop(); - int num1 = numStack.top(); - numStack.pop(); - if (lastOp == '+') - { - result = num1 + num2; - } - else if (lastOp == '-') - { - result = num1 - num2; - } - else if (lastOp == '*') - { - result = num1 * num2; - } - else if (lastOp == '/') - { - result = num1 / num2; - } - else - { - cout << "invalid operator " << (char)lastOp << endl; // 不会走到这 - return false; - } - numStack.push(result); - } - else - { - cout << "miss numbers" << endl; - return false; - } - } - if (opStack.empty()) - { - cout << "miss left (" << endl; - return false; - } - opStack.pop(); - } - else if (token != 0) - { - cout << "invalid token " << token << endl; - return false; - } - last_token = token; - next(); - } - // 计算最终结果 - while (!opStack.empty()) - { - int lastOp = opStack.top(); - if (lastOp == '(') - { - cout << "miss right )" << endl; - return false; - } - opStack.pop(); - if (numStack.size() >= 2) - { - int num2 = numStack.top(); - numStack.pop(); - int num1 = numStack.top(); - numStack.pop(); - if (lastOp == '+') - { - result = num1 + num2; - } - else if (lastOp == '-') - { - result = num1 - num2; - } - else if (lastOp == '*') - { - result = num1 * num2; - } - else if (lastOp == '/') - { - result = num1 / num2; - } - else - { - cout << "invalid operator " << (char)lastOp << endl; - return false; - } - numStack.push(result); - } - else - { - cout << "miss numbers" << endl; - return false; - } - } - // 最终结果存放在栈顶,且应该仅有一个元素 - if (numStack.size() != 1) - { - cout << "invalid expression" << endl; - return false; - } - return numStack.top(); - return true; -} -``` -上述代码报错位置能够得到更进一步确定,存在一些可以简化的冗余。 - -## 递归计算中缀表达式 - -上述的过程可以改成递归的,可以将保存运算符的栈隐含的函数递归调用栈中,操作数和中间结果的保存则可以用递归调用中的局部变量来加以保存。 - -```C++ -#include -#include -using namespace std; - -enum Token -{ - // 一元运算符不涉及, 从Lor到Mul/Div/Mod优先级依次升高 - Number = 128, - Lor, // || - Land, // && - Or, // | - Xor, // ^ - And, // & - Eq, Ne, // == != - Lt, Le, Gt, Ge, // < <= > >= - Shl, Shr, // << >> - Add, Sub, // + - - Mul, Div, Mod, // * * / %优先级相同 - Top -}; - -class Expression -{ -public: - Expression(); - void next(); - bool match(int tk); - void expression(int level); - bool calculate(const char* s); - int getResult() { return result; } - int getHasResult() { return hasResult; } - void test(const char * s, bool valid, int expected); -private: - const char* str; - int token; - int token_val; - int result; - bool hasResult; -}; - -Expression::Expression() : str(NULL), token(0), token_val(0), result(0), hasResult(false) -{ -} - -// 词法分析 -void Expression::next() -{ - token = 0; - while (str && (token = *str)) - { - str++; - while (token == ' ' || token == '\t') - { - token = *str++; - } - - if (token >= '0' && token <= '9') - { - token_val = token - '0'; - token = Number; - while (*str >= '0' && *str <= '9') - { - token_val = token_val * 10 + *str - '0'; - str++; - } - } - else if (token == '*') token = Mul; - else if (token == '/') token = Div; - else if (token == '%') token = Mod; - else if (token == '+') token = Add; - else if (token == '-') token = Sub; - else if (token == '<') - { - if (*str == '<') { token = Shl; str++; } - else if (*str == '=') { token = Le; str++; } - else token = Lt; - } - else if (token == '>') - { - if (*str == '>') { token = Shr; str++; } - else if (*str == '=') { token = Ge; str++; } - else token = Gt; - } - else if (token == '!') - { - if (*str == '=') { token = Ne; str++; } - } - else if (token == '=') - { - if (*str == '=') { token = Eq; str++; } - } - else if (token == '&') - { - if (*str == '&') { token = Land; str++; } - else token = And; - } - else if (token == '|') - { - if (*str == '|') { token = Lor; str++; } - else token = Or; - } - else if (token == '^') token = Xor; - return; - } -} - -bool Expression::match(int tk) -{ - if (token == tk) - { - next(); - return true; - } - else - { - cout << "expected token: "; - if (token >= 128) - { - cout << token << endl; - } - else - { - cout << (char)token << endl; - } - return false; - } -} - -// 1 + 2*5/3 - (2 - 3) --> 5 -void Expression::expression(int level) -{ - if (hasResult) - { - cout << "lack of operator" << endl; // 常理来说不会到这里 - return; - } - if (token == Number) - { - match(Number); - result = token_val; - hasResult = true; - } - // () - else if (token == '(') - { - match('('); - expression(Lor); - - if (!hasResult) - { - cout << "miss expression in ()" << endl; - return; - } - - if (!match(')')) - { - hasResult = false; - return; - } - } - // 其他一元运算符支持... - - // 二元运算符 - while (token >= level) - { - int tmpResult = 0; - if (hasResult) - { - tmpResult = result; - hasResult = false; - } - else - { - cout << "binary operator " << token << " miss left operand" << endl; - return; - } - - if (token == Add) - { - match(Add); - expression(Mul); // 应该用更高优先级中最低的那一个 - if (!hasResult) - { - cout << "miss right operands for +" << endl; - return; - } - result = tmpResult + result; - } - else if (token == Sub) - { - match(Sub); - expression(Mul); - if (!hasResult) - { - cout << "miss right operands for -" << endl; - return; - } - result = tmpResult - result; - } - else if (token == Mul) - { - match(Mul); - expression(Top); - if (!hasResult) - { - cout << "miss right operands for *" << endl; - return; - } - result = tmpResult * result; - } - else if (token == Div) - { - match(Div); - expression(Top); - if (!hasResult) - { - cout << "miss right operands for /" << endl; - return; - } - result = tmpResult / result; - } - else if (token == Mod) - { - match(Mod); - expression(Top); - if (!hasResult) - { - cout << "miss right operands for %" << endl; - return; - } - result = tmpResult % result; - } - else - { - cout << "invalid token : " << token << endl; - hasResult = false; - return; - } - } -} - -bool Expression::calculate(const char* s) -{ - if (s == NULL || *s == 0) - { - cout << "null input" << endl; - return false; - } - str = s; - hasResult = false; - // 当前运算符优先级设置为最低的Lor,只是为了在此基础上任何运算都会开始,这个最终的Lor运算是不做的 - next(); - expression(Lor); - if (hasResult && token != 0) // 一个表达式解析成功,但是没有到达末尾 - { - cout << "expression has extra string" << endl; - hasResult = false; - } - return hasResult; -} - -void Expression::test(const char * s, bool valid, int expected = 0) -{ - static int count = 1; - calculate(s); - bool passed = (valid == hasResult && !valid) || (valid == hasResult && valid && expected == result); - cout << "test " << count++ << (passed ? " passed, " : " failed, ") - << "expected: " << (valid ? "valid " : "invalid ") << expected << ", " - << "actual: " << (hasResult ? "valid " : "invalid ") << result << endl; -} - - -int main() -{ - Expression expr; - expr.test("1 + 2*5/3 - (2 - 3)", true, 5); - expr.test("100 + 200 + (300 * 10 - 2000 - 500 *4/(200/100)) *10 - 100 * 2", true, 100); - expr.test("(1)", true, 1); - expr.test("(1+5%3%3*4-1)/5+10", true, 11); - expr.test("100+20-30*2/4+100%30/10*(100+10%10)", true, 205); - - expr.test("1*", false, 0); - expr.test("+1", false, 0); - expr.test("()", false, 0); - expr.test("100 100", false, 0); - expr.test("", false, 0); - return 0; -} -``` - -递归实现中是找到一个运算符后向右递归计算比自己优先级高的,而不是向左查找。 - -**注意结合性的影响**,如果右结合就需要向右计算等于或高于当前优先级的,如果左结合只需要计算高于当前优先级的。由于计算时是计算了大于等于当前运算符优先级的所有运算符,所以这个点体现在传入右边表达式的当前运算符优先级上,左结合的应该传递更高一个等级的运算符,右结合的话就传递当前即可。开始计算时从最低开始。 - -因为要区分不同运算符,必须使用不同枚举值,用来代指更高优先级的运算符应该选用高一级的同类优先级运算符中枚举值最小的那一个。 - -这样实现,报错信息可以很充分,也可以很容易的扩展。 - -## 参考 - -- [Operator-precedence parser - Wikipedia](https://en.wikipedia.org/wiki/Operator-precedence_parser) -- [C语言的运算符优先级](https://zh.cppreference.com/w/c/language/operator_precedence) -- [手把手教你构建 C 语言编译器(8)- 表达式](https://lotabout.me/2016/write-a-C-interpreter-8/) \ No newline at end of file diff --git a/Prolog.md b/Prolog.md deleted file mode 100644 index 84243cf..0000000 --- a/Prolog.md +++ /dev/null @@ -1,992 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Prolog语言](#prolog%E8%AF%AD%E8%A8%80) - - [环境搭建](#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA) - - [基本要素](#%E5%9F%BA%E6%9C%AC%E8%A6%81%E7%B4%A0) - - [填空](#%E5%A1%AB%E7%A9%BA) - - [Unification](#unification) - - [递归](#%E9%80%92%E5%BD%92) - - [列表和元组](#%E5%88%97%E8%A1%A8%E5%92%8C%E5%85%83%E7%BB%84) - - [列表和数学运算](#%E5%88%97%E8%A1%A8%E5%92%8C%E6%95%B0%E5%AD%A6%E8%BF%90%E7%AE%97) - - [经典问题](#%E7%BB%8F%E5%85%B8%E9%97%AE%E9%A2%98) - - [看一些常见问题](#%E7%9C%8B%E4%B8%80%E4%BA%9B%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) - - [斐波那契数列](#%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97) - - [阶乘](#%E9%98%B6%E4%B9%98) - - [翻转列表](#%E7%BF%BB%E8%BD%AC%E5%88%97%E8%A1%A8) - - [查找列表最大最小值](#%E6%9F%A5%E6%89%BE%E5%88%97%E8%A1%A8%E6%9C%80%E5%A4%A7%E6%9C%80%E5%B0%8F%E5%80%BC) - - [列表排序](#%E5%88%97%E8%A1%A8%E6%8E%92%E5%BA%8F) - - [数独问题](#%E6%95%B0%E7%8B%AC%E9%97%AE%E9%A2%98) - - [八皇后问题](#%E5%85%AB%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98) - - [总结](#%E6%80%BB%E7%BB%93) - - - -# Prolog语言 - -Prolog(Programming in Logic的缩写)是一种逻辑编程语言。它创建在逻辑学的理论基础之上, 最初被运用于自然语言等研究领域。现在它已广泛的应用在人工智能的研究中,它可以用来建造专家系统、自然语言理解、智能知识库等。 - -Prolog语言的理论基础创建于爱丁堡大学的罗伯特·科瓦尔斯基对霍恩子句(Horn Clause)的程序性解释,最早由艾克斯-马赛大学的Alain Colmerauer与Phillipe Roussel等人于60年代末研究开发。1972年被公认为是Prolog语言正式诞生的年份,自1972年以后,分支出多种Prolog的方言。最主要的两种方言为爱丁堡和艾克斯-马赛。最早的Prolog解释器由Roussel建造,而第一个Prolog编译器则是David Warren编写的。 - -Prolog是声明式编程语言(decalarative language),向Prolog提供一些事实(fact)和推论(inference),并让它为你推断一个问题。 - -基本构建单元: -- 事实:关于真实世界的基本断言。($A = True$) -- 规则:关于真实世界的一些事实的推论。($A \to B$) -- 查询:查询一个真实世界的问题。($B = True?$) - -分别对应于并同有头(无体、有体)和无头的霍恩子句,[霍恩子句](https://zh.wikipedia.org/wiki/%E9%9C%8D%E6%81%A9%E5%AD%90%E5%8F%A5)是Prolog的基础。 - -可以使用这门基于逻辑的语言表达和提出问题。需要改变思考方式,因为Prolog并不像通常用的命令式编程语言。 - -事实和规则被放入一个知识库(knowledge base),Prolog编译器的功能就是将这个知识库编译成适于高效查询的形式。Prolog更多地是作为一门领域特定语言(DSL),而不是通用的编程语言,应该趁早放弃用它去做一些通用的功能。可以用看待SQL的眼光来看待它。 - -声明式语言的魅力在于,只要把问题准确地描述出来(这就是所有要做的事情,而不像其他编程语言描述的是解决问题的方案和过程),那么就会得到答案,当然前提是问题可以被描述并且有答案。 - -阅读: -- [一个Prolog教程](https://zhzluke96.github.io/prolog-tut-cn/) -- [阮一峰的网络日志 - Prolog 语言入门教程](http://ruanyifeng.com/blog/2019/01/prolog.html) -- [GNU Prolog手册](http://gprolog.org/manual/gprolog.html) -- [一个Prolog教程整合](https://zhuanlan.zhihu.com/p/136762642) -- 书籍:The Craft of Prolog - -## 环境搭建 - -Prolog有多种方言实现,这里使用GNU Prolog,并且介绍的部分都是不同方言的交集。 - -搜索GNU Prolog找到官网,下载二进制: -- 安装并添加环境变量,Windows下有自己的控制台`gprolog`,`gplc`是命令行编译器。 -- Linux:`sudo apt install gprolog`,`gprolog/prolog`进入控制台,`gplc`编译。 -- Linux中也可以选择安装SWI prolog:`sudo apt install swi-prolog`,命令`swipl`,差不太多。 - -VS Code选个合适的插件,能高亮就行,插件并不多,不要求太多。 - -文件后缀:`.pl .plo .prolog` - -编译: -```shell -gplc file.pl -``` - -交互环境下编译: -``` -| ?- ['filename.pl']. -| ?- [filename]. -``` -编译完就可以向它提问题了,对这个语言的运作方式就是你问我答,你给prolog编写了事实和推论,然后查询一个问题,它会回答你`yes no`。 - -传统艺能: -```prolog -hello :- write('Hello World!'), nl. -``` -编译,执行: -```shell -gplc hello.pl -./hello -``` -这时候就会进入交互执行环境下,再输入hello才会打印出来: -``` -GNU Prolog 1.4.5 (64 bits) -Compiled Feb 23 2020, 20:14:50 with gcc -By Daniel Diaz -Copyright (C) 1999-2020 Daniel Diaz -| ?- hello. -Hello World! - -yes -``` - -## 基本要素 - -- 原子(Atom):一个类似于Ruby中Symbol的固定值,小写字母开头的符号就是原子,值不能改变。 -- 变量:一个词以大写字母或者下划线开始则时变量,值可以改变。 -- 范围注释:`/* */` -- 行注释:`% ...` -- `.`作为语句结尾。 - -例子: -```prolog -% facts -likes(a, b). -likes(c, b). -likes(d, e). - -% rules / inferences -friend(X, Y) :- \+(X = Y), likes(X, Z), likes(Y, Z). -``` -前面的是事实,后面的是推论的规则,含义就是喜欢同一个东西的人是朋友。交互环境中编译: -```prolog -| ?- ['hello.pl']. -``` -然后就可以进行查询了: -``` -| ?- likes(a, b). - -yes -| ?- likes(c, b). - -yes -| ?- likes(c, d). - -no -| ?- likes(a, c). - -no -| ?- friend(a, b). - -no -| ?- friend(a, c). - -yes -| ?- friend(a, a). - -no -``` -既可以查询已经定义的事实,也可以让Prolog帮你根据事实推论一个事件是否成立。`\+`表示取反,所以这里排除了自己和自己做朋友的情况。 - -推论中: -- `:-`左边的称为子目标(subgoal)。 -- `:-`右边的就是这个子目标的规则,`,`分隔,所有规则为真这个子目标才为真。 -- `\+`表示取反。 - -## 填空 - -Prolog的能力远不止让他帮你做选择题。可以使用逻辑引擎为一个查询寻找所有可能的匹配,就是是让它做填空题。为了做到这一点,需要在查询中指定变量。 - -例子: -```prolog -% food.pl -% facts -things_type(banana, fruit). -things_type(apple, fruit). -things_type(tomato, fruit). -things_type(beans, vegetable). -things_type(tomato, vegetable). -things_type(knife, kitchenware). -things_type(pot, kitchenware). -food_category(fruit). -food_category(vegetable). - -% inferences -is_exact_food(X) :- food_category(Y), things_type(X, Y). -is_food(X) :- food_category(X). -is_food(X) :- is_exact_food(X). -``` - -编译,使用一个变量`What`来查询: -``` -| ?- is_food(What). - -What = fruit ? a - -What = vegetable - -What = banana - -What = apple - -What = tomato - -What = beans - -What = tomato - -no -| ?- is_exact_food(What). - -What = banana ? a - -What = apple - -What = tomato - -What = beans - -What = tomato - -(1 ms) no -| ?- is_food(knife). - -no -| ?- things_type(tomato, What). - -What = fruit ? a - -What = vegetable - -yes -| ?- things_type(What, fruit). - -What = banana ? a - -What = apple - -What = tomato - -no -``` -感觉上来说做的事情就是一个遍历搜索。 - -做填空题会停一下:`Action (; for next solution, a for all solutions, RET to stop) ? `。 - -因为某些时候可能我们只需要其中一个解,某些时候需要所有解,有多种情况时Prolog提供了选项供选择。 - -为方便起见,如果在剩余部分中Prolog检测不到其他可选项,你将看到一个yes。如果Prolog在未经更多计算的情况下不能立刻断定是否还有更多选项,那么它将提示你查询下一个并返回no。 - -简单来说,Prolog就是一个帮助你进行推论的工具,这可能比自己写通用的程序简单,但同时也可能使你放弃寻找更优秀的算法而依赖于Prolog的能力,从而沦为单纯描述问题的工具人。 - -只能说编码一条规则和推论是简单的,难点可能是对问题的抽象、建模和准确描述。 - -## Unification - -书中将其翻译为合一,我只能说看到这词鬼能想到是什么玩意? - -合一的意思是找出那些使规则两侧匹配的值,合一在规则的两侧都能工作。 - -书中举了个简单的例子,但好像也没有说清楚什么是Unification。应该就是说`=`两侧的值要匹配。 - -## 递归 - -和命令式语言描述过程的递归不同,Prolog中递归就是声明的递归: - -例子:建模JOJO中乔斯达家族的人物关系。 -```prolog -% father(a, b) means a is father of b -father(georgeJoestar, jonathanJoestar). -father(jonathanJoestar, georgeJoestar_the_second). -father(georgeJoestar_the_second, josefJoestar). -father(josefJoestar, higashikataJosuke). -father(josefJoestar, holyJoestar). -father(kujoJotaro, kujoJolyne). -mother(erinaJoestar, georgeJoestar_the_second). -mother(lisaLisa, josefJoestar). -mother(susieQ, holyJoestar). -mother(holyJoestar, kujoJotaro). - -% recursive inferences -ancestor(X, Y) :- father(X, Y). -ancestor(X, Y) :- mother(X, Y). -ancestor(X, Y) :- father(X, Z), ancestor(Z, Y). -ancestor(X, Y) :- mother(X, Z), ancestor(Z, Y). -``` - -很好理解的递归定义。 - -测试: -``` -| ?- ancestor(lisaLisa, Who). - -Who = josefJoestar ? a - -Who = higashikataJosuke - -Who = holyJoestar - -Who = kujoJotaro - -Who = kujoJolyne - -(1 ms) no -| ?- ancestor(Who, kujoJotaro). - -Who = holyJoestar ? a - -Who = georgeJoestar - -Who = jonathanJoestar - -Who = georgeJoestar_the_second - -Who = josefJoestar - -Who = erinaJoestar - -Who = lisaLisa - -Who = susieQ - -no -``` -感觉结果就是深度优先搜索的结果。这个问题用树来建模一样很简单。 - -注意避免死递归,比如这种: -```prolog -couple(X, Y) :- couple(Y, X). -``` - -Prolog会优化尾递归,所以如果可以的话,将递归的子目标放到规则的末尾可以进行优化。 - -## 列表和元组 - -- 列表是变长容器:`[1, 2, 3]`指定列表。 -- 元组和定长容器:`(1, 2, 3)`指定元组。 -- 当`=`两侧都是元组或者列表时,要匹配就需要长度和每个元素都匹配。 - -``` -| ?- (1, 2, 3) = (1, 2, 3). - -yes -| ?- [1, 2, 3] = (1, 2, 3). - -no -| ?- (A, B) = (1, 2, 3). - -A = 1 -B = (2,3) - -yes -| ?- (A, B, C) = (1, 2, 3). - -A = 1 -B = 2 -C = 3 - -yes -| ?- (A, B, C) = (1, 2). - -no -| ?- (A, 2, C) = (1, B, 3). - -A = 1 -B = 2 -C = 3 - -yes -| ?- () = (). -uncaught exception: error(syntax_error('user_input:2 (char:55) expression expected'),read_term/3) -| ?- [A, B] = [1, 2, 3]. - -no -| ?- [A, B, C] = [1, 2, 3]. - -A = 1 -B = 2 -C = 3 - -yes -| ?- [2, 2, 3] = [A, A, C]. - -A = 2 -C = 3 - -yes -| ?- [1, 2, 3] = [A, A, C]. - -no -| ?- [] = []. - -yes - -``` - -列表和元组在某些匹配规则上是有区别的! -- 可以用`[Head|Tail]`匹配不为空的列表,其中`Head`匹配第一个元素,`Tail`匹配剩余的元素构成的列表,可以为空。元组则不行。 -``` -| ?- [1] = [Head | Tail]. - -Head = 1 -Tail = [] - -yes -| ?- [1, 2, 3, 4] = [Head | Tail]. - -Head = 1 -Tail = [2,3,4] - -yes -| ?- [] = [Head | Tail]. - -no -| ?- [a, b, c] = [A | [B | C]]. - -A = a -B = b -C = [c] - -yes -``` -- 在元组中也有类似语法。不过不需要`|`,大小不匹配时就是尝试用更短的元组最末尾的元素去匹配另一个元组的剩余末尾元素构成的元组。下面例子可以看出只有最后一个元素有这个规则。列表则不行。 -``` -| ?- (A, B, C) = (a, b, c, d). - -A = a -B = b -C = (c,d) - -yes -| ?- (A, B, (C, D)) = (a, b, c, d). - -A = a -B = b -C = c -D = d - -yes -| ?- (A, B, C, D) = ((a, b), c, d). - -no -| ?- (A, B, C) = ((a, b), c, d). - -A = (a,b) -B = c -C = d - -yes -| ?- (1, (2, 3), 4, 4) = (A, B, C). - -A = 1 -B = (2,3) -C = (4,4) - -yes -| ?- ((1, 2), 3, 4) = (1, 2, 3, 4). - -no -``` - - -- `_`是一个通配符,可以和任何对象匹配。含义就是我不关心这个位置上是什么。 -``` -| ?- [a, b, c, d, e] = [_, _ | [Head | _]]. - -Head = c - -yes -``` - -- 以上就是Prolog的核心数据结构列表和元组和匹配(Unification,合一)的工作方式。 - -## 列表和数学运算 - -例子:计数、求和、求平均值。 -```prolog -count(0, []). -count(Count, [Head|Tail]) :- count(TailCount, Tail), Count is TailCount + 1. - -sum(0, []). -sum(Sum, [Head|Tail]) :- sum(Total, Tail), Sum is Head + Total. - -average(Average, List) :- count(Count, List), sum(Sum, List), Average is Sum / Count. -``` -这样求平均值有个问题是,0的时候会除0报错。可以和计数、求和同样方法改为: -```prolog -average(0, []). -average(Average, [Head|Tail]) :- count(Count, [Head, Tail]), sum(Sum, [Head, Tail]), Average is Sum / Count. -``` - -都是利用递归实现的,求值时用`is`而不是`=`,如果用`=`,那么比如`[1, 2]`得到的长度会是`0+1+1`而并未求出值。 - - -`append`规则:`append(List1, List2, List3)`如果List2**附加**到List1上与List3匹配,那么这个规则为真。 -``` -| ?- append([], [], []). - -yes -| ?- append([1, 2], [3], [1, 2, 3]). - -yes -| ?- append([1, 2], [2, 10], What). - -What = [1,2,2,10] - -yes -``` - -可以正向用其附加一个列表到另一个上,也可以已知附加的结果和其中一个得到另一个,也可以只给出附加结果,得到所有可能列表的组合。 -``` -| ?- append(List1, List2, [1, 2, 3, 4]). - -List1 = [] -List2 = [1,2,3,4] ? a - -List1 = [1] -List2 = [2,3,4] - -List1 = [1,2] -List2 = [3,4] - -List1 = [1,2,3] -List2 = [4] - -List1 = [1,2,3,4] -List2 = [] - -yes -| ?- append(List1, [4], [1, 2, 3, 4]). - -List1 = [1,2,3] - -yes -| ?- append([1, 2], What, [1, 2, 3, 4]). - -What = [3,4] - -yes -``` -一个规则给了我们四种能力:判断列表附加是否正确、列表求差、列表附加、求一个列表可能的所有附加组合。**正向和反向都可以使用**,足可见其强大。 - -来尝试写一个类似的规则,叫做`concatenate`: -```prolog -% implement a inference concatenate(List1, List2, List3) just like append -concatenate([], List, List). -concatenate([Head | Tail1], List, [Head | Tail2]) :- concatenate(Tail1, List, Tail2). -``` - -简洁得令人难以置信,非常地优雅!在prolog中用递归还真有推公式的感觉,需要练习才能熟练。 - -测试: -``` -| ?- concatenate([], [], []). - -yes -| ?- concatenate([], [1], [1]). - -yes -| ?- concatenate([1, 2], What, [1, 2, 3, 4, 5]). - -What = [3,4,5] - -yes -| ?- concatenat([1, 2], [4, 5], What). - -What = [1,2,4,5] - -yes -| ?- concatenate(List1, List2, [1, 2, 3, 4, 5]). - -List1 = [] -List2 = [1,2,3,4,5] ? a - -List1 = [1] -List2 = [2,3,4,5] - -List1 = [1,2] -List2 = [3,4,5] - -List1 = [1,2,3] -List2 = [4,5] - -List1 = [1,2,3,4] -List2 = [5] - -List1 = [1,2,3,4,5] -List2 = [] - -no -``` - -能用递归解决的问题在Prolog中看来都很方便。 - -比如要求三个列表的组合都可以简单灵活地实现:可以看到规则中的值数量不同可以看做不同的规则,因为参数数量是精确匹配的。 -```prolog -% base on concatenate(List1, List2, List3) above -% first implementation, good -concatenate([], List1, List2, List3) :- concatenate(List1, List2, List3). -concatenate([Head | Tail1], List1, List2, [Head | Tail2]) :- concatenate(Tail1, List1, List2, Tail2). - -% second implementation, bad and wrong, dead recursion -concatenate2(List1, List2, List3, List4) :- concatenate(List1, List2, ListTemp), concatenate(ListTemp, List3, List4). -``` - -后者看起来并不是一个好的实现,分析一下递归树,后者的时间和空间复杂度可能达到了$O(2^N)$,在求四个值的列表的三个子数组的排列时栈溢出了。而前者有更好的表现,应该注意递归**尽量使用尾递归**,就算不能使用尾递归也要尽量保证只有一个递归式,不然复杂度很可能达到指数级,是程序可用性大大降低。而后者就像斐波那契数列的递归实现一样做了大量无用计算。 -```prolog -| ?- concatenate2(List1, List2, List3, [1, 2, 3, 4]). - -List1 = [] -List2 = [] -List3 = [1,2,3,4] ? a - -List1 = [] -List2 = [1] -List3 = [2,3,4] - -List1 = [] -List2 = [1,2] -List3 = [3,4] - -List1 = [] -List2 = [1,2,3] -List3 = [4] - -List1 = [] -List2 = [1,2,3,4] -List3 = [] - -List1 = [1] -List2 = [] -List3 = [2,3,4] - -List1 = [1] -List2 = [2] -List3 = [3,4] - -List1 = [1] -List2 = [2,3] -List3 = [4] - -List1 = [1] -List2 = [2,3,4] -List3 = [] - -List1 = [1,2] -List2 = [] -List3 = [3,4] - -List1 = [1,2] -List2 = [3] -List3 = [4] - -List1 = [1,2] -List2 = [3,4] -List3 = [] - -List1 = [1,2,3] -List2 = [] -List3 = [4] - -List1 = [1,2,3] -List2 = [4] -List3 = [] - -List1 = [1,2,3,4] -List2 = [] - -Fatal Error: global stack overflow (size: 32768 Kb, reached: 32765 Kb, environment variable used: GLOBALSZ) -List3 = [] -``` - -而前者尾递归再加复杂度不高(具体是prolog做的,用的DFS还是BFS呢?我也不知道!)数据很大时都不会挂。经过进一步分析后者应该是死递归了,看来递归真的不是随便就能写得健壮的! - -感觉好像递归写得不好动不动栈溢出给你看啊!一定要注意,**终止条件时一定要仅仅只能够匹配终止的规则**,如果也还能匹配通用规则,那么终止条件虽然也能被搜索到,但通用规则成功匹配之后可能就会死递归。 - -比如规则`increaseList(Min, Max, [Min, Min + 1, ..., Max])`: -```prolog -increasingList(Min, Min, [Min]). -increasingList(Min, Max, [Min | List]) :- Min1 is Min + 1, increasingList(Min1, Max, List). -``` -如果这样实现,终止条件时同时匹配就死递归了。比如`increaseList(1, 1, What)`匹配了前者,但同时也匹配了后者,匹配后者就向上累加死递归了,应该更改成: -```prolog -% create List with increasing numbers [Min, Min + 1, ..., Max] -increasingList(Min, Min, [Min]). -increasingList(Min, Max, [Min | List]) :- Min1 is Min + 1, Min < Max, increasingList(Min1, Max, List). -``` -测试: -```prolog -What = [0,1] ? a - -(1 ms) no -| ?- increasingList(0, 100, What). - -What = [0,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,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100] ? a - -(1 ms) no -``` - -回去测试`concatenate`: -```prolog -% test of concatenate -concatenateList(List1, List2, List3, Min, Max) :- increasingList(Min, Max, List4), concatenate(List1, List2, List3, List4). -``` -执行并输出所有结果: -```prolog -| ?- concatenateList(List1, List2, List3, 1, 100). - -... - -List1 = [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,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100] -List2 = [] -List3 = [] - -(387 ms) no -``` -可以看出程序写得高效Prolog还是非常强大的。这个结果组合的数量应该是$101*101 = 5050$。根据以往控制台输出的经验,大批量IO的效率并不高,大部分时间可能是花在了IO上。不知道有没有方法只计算不输出? - -如何避免死递归:递归因为没有用条件判断走一个分支就返回确保不能走另一个分支,所以**所有的情况应该互斥**,添加新的递归规则时把已有的递归进入条件叠加起来取反加到规则右侧就行。 - -## 经典问题 - -鸡兔同笼问题:经典解方程问题,可太适合Prolog了。TODO。 - -地图着色问题:典型应用,TODO。 - - -## 看一些常见问题 - -鉴于Prolog的推理属性,来实现一些常见数(搜)学(索)题巩固一下: - -### 斐波那契数列 - -最直白的递归版本; -```prolog -fibonacci(0, 0). -fibonacci(1, 1). -fibonacci(N, Res) :- N > 1, fibonacci(N-1, A), fibonacci(N-2, B), Res is A + B. -``` -但你会发现这样除了0和1都不会工作,因为`N-1 N-2`没有放在谓词`is`中所有不会进行计算。比如计算`fibonacci(2, What)`是,找`fibonacci(2-1, A) fibonacci(2-2, B)`找不到,因为`2-1 2-2`都没有计算出来。啊这! - -修改之后: -```prolog -fibonacci(0, 0). -fibonacci(1, 1). -fibonacci(N, Res) :- N > 1, N1 is N-1, N2 is N-2, fibonacci(N1, A), fibonacci(N2, B), Res is A + B. -``` -我们都知道这样递归是很低效的,改进: -```prolog -fib2(0, 0). -fib2(1, 1). -fib2(N, Res) :- N > 1, fibonacci2(N, [Res | _]). -fibonacci2(0, [0]). -fibonacci2(1, [1, 0]). -fibonacci2(N, [Res | Tail]) :- N > 1, N1 is N-1, fibonacci2(N1, Tail), Tail = [A | [B | _]], Res is A + B. -``` -`fibonacci2`得到整个数列,除`N>1`外更改任何一个规则的顺序都不能得到结果。在过程中,我尝试过的错误解决方案: -```prolog -fibonacci2(0, [0]). -fibonacci2(1, [1, 0]). -fibonacci2(N, [Res | Tail]) :- N > 1, N1 is N-1, Tail = [A | [B | _]], Res is A + B, fibonacci2(N1, Tail). -``` -这样做是无法得到结果的,错误:`uncaught exception: error(instantiation_error,(is)/2)`。分析了原因应该是,以`fibonacci2(2)`为例,此时`Res Tail A B`未知,`Res`依赖`A B`,`A B`依赖`Tail`,`Tail`依赖已知的`N1`,环环相扣。就是说只要有了`Tail`就能推导出结果。但是此时`Tail`的推导放到了最后,但它是被依赖的源头,应该放到一开始,并且是N1需要已知。也没人讲,毕竟没有系统学习,看来还是要踩过坑才能明白。 - -递归编写总结:**编写递归时根据依赖关系排列规则,不要用让要推导的变量依赖未知的变量**。 - -这里编写的规则都只能正推,不能已知数列反推是第几项。要如何才能反推呢?只能说太麻烦了,普通编程语言中查个表就完事了。 - - -### 阶乘 - -```prolog -% factorial -factorial(0, 1). -factorial(N, Res) :- N > 0, N1 is N-1, factorial(N1, Tmp), Res is N * Tmp. -``` - -### 翻转列表 - -```prolog -% reverse a list -reverseList([], []). -reverseList([Head | Tail], ReversedList) :- reverseList(Tail, ReversedTail), append(ReversedTail, [Head], ReversedList). -``` - -### 查找列表最大最小值 - -```prolog -% util : get min value of two values -min(Val1, Val2, Min) :- Val1 > Val2, Min is Val2. -min(Val1, Val2, Min) :- \+(Val1 > Val2), Min is Val1. - -max(Val1, Val2, Max) :- Val1 < Val2, Max is Val2. -max(Val1, Val2, Max) :- \+(Val1 < Val2), Max is Val1. - -% find min of list -minElement([Head | []], Min) :- Min is Head. -minElement([Head | Tail], Min) :- \+Tail = [], minElement(Tail, MinOfTail), min(Head, MinOfTail, Min). - -% find max of list -maxElement([Head | []], Max) :- Max is Head. -maxElement([Head | Tail], Max) :- \+Tail = [], maxElement(Tail, MaxOfTail), max(Head, MaxOfTail, Max). -``` - -好像有内置的`min`和`max`。 - -### 列表排序 - -```prolog -q:- L=[33,18,2,77,18,66,9,25], (sortcsj(L,P), write(P), nl). - -sortcsj(L,S) :- permutation(L,S), ordered(S). /* L為原list, S為排序好的list, 此為permutation關係(built-in) */ - -ordered([]). /* 表empty list視為排序好的list */ -ordered([_|[]]). /* 只有一元素之list視為排序好的list */ -ordered([A|[B|T]]) :- A =< B, ordered([B|T]). /* 此规则約束所謂的排序好是指前項元素小於或等於後一項元素 */ - -:- initialization(q). -``` - -维基百科上给出的,排序还是那些套路,归并、快排、选择之流,不写了就。 - - -## 数独问题 - -首先决定查询的形式:`sudoku(Puzzle, Solution)`,用一个列表表示难题,下划线表示未知数字,如果存在解决方法则提供,不存在当然得到no。 - -比如4x4数独为例: -```prolog -sudoku([_, _, 2, 3, - _, _, _, _, - _, _, _, _, - 3, 4, _, _], - Solution). -``` - -我们要做的事情就是准确描述规则,而不必考虑求解的算法。 -```prolog -valid([]). -valid([Head | Tail]) :- % Head will be list - fd_all_different(Head), % built-in predicate indicate all elements in a list are different from each other. - valid(Tail). - - -sudoku(Puzzle, Solution) :- - % rule1: 16 units, range of numbers: 1-4 - Solution = Puzzle, - Puzzle = [S11, S12, S13, S14, - S21, S22, S23, S24, - S31, S32, S33, S34, - S41, S42, S43, S44], - fd_domain(Puzzle, 1, 4), % built-in predicate fd_domain(List, LowerBound, UpperBound) - - % rule2: all rows, columns and squares are valid - Row1 = [S11, S12, S13, S14], - Row2 = [S21, S22, S23, S24], - Row3 = [S31, S32, S33, S34], - Row4 = [S41, S42, S43, S44], - - Col1 = [S11, S21, S31, S41], - Col2 = [S12, S22, S32, S42], - Col3 = [S13, S23, S33, S43], - Col4 = [S14, S24, S34, S44], - - Square1 = [S11, S12, S21, S22], - Square2 = [S13, S14, S23, S24], - Square3 = [S31, S32, S41, S42], - Square4 = [S33, S34, S43, S44], - - valid([Row1, Row2, Row3, Row4, - Col1, Col2, Col3, Col4, - Square1, Square2, Square3, Square4]). -``` - -Prolog比较擅长解决这种易于表达但难于解决的约束问题。 - -## 八皇后问题 - -8x8的格子里面有八个皇后(国际象棋),编号1~8,皇后之间不能位于同一行,同一列,同一条对角线(两个方向),求出所有八个皇后的放置方式(横纵坐标1到8表示)。源于国际象棋的问题:在棋盘上方八个皇后,使其中任何一个都无法吃掉其他。 - -```prolog -% eight queens problem -% queens' number from 1 to 8 -% coordinate is (x, y) -/* -board - 1 2 3 4 5 6 7 8 rows -1 * -2 -3 -4 -5 -6 -7 -8 -columns -*/ - -% every queen is valid -valid_queen((Row, Col)) :- - Range = [1, 2, 3, 4, 5, 6, 7, 8], - member(Row, Range), member(Col, Range). - -% every queens in the board is valid -valid_board([]). -valid_board([Head | Tail]) :- valid_queen(Head), valid_board(Tail). - -% get the list of the row of every queen -rows([], []). -rows([(Row, _) | QueensTail], [Row | RowsTail]) :- rows(QueensTail, RowsTail). -% get the list of the column of every queen -cols([], []). -cols([(_, Col) | QueensTail], [Col | ColsTail]) :- cols(QueensTail, ColsTail). -% get diagonals value from left-top to right-bottom, represent with x - y -diags1([], []). -diags1([(Row, Col) | QueensTail], [Diagonal | DiagonalsTail]) :- - Diagonal is Col - Row, - diags1(QueensTail, DiagonalsTail). -% get diagonals value from left-bottom to right-top, represent with x+y -diags2([], []). -diags2([(Row, Col) | QueensTail], [Diagonal | DiagonalsTail]) :- - Diagonal is Col + Row, - diags2(QueensTail, DiagonalsTail). - -% eight queens -eight_queens(Board) :- - length(Board, 8), - valid_board(Board), - - rows(Board, Rows), - cols(Board, Cols), - diags1(Board, Diags1), - diags2(Board, Diags2), - - % rules - fd_all_different(Rows), - fd_all_different(Cols), - fd_all_different(Diags1), - fd_all_different(Diags2). - -% query: -% eight_queens([(1, A), (2, B), (3, C), (4, D), (5, E), (6, F), (7, G), (8, H)]). -``` - -八皇后问题如果将皇后视作等同的话,最终有92种解法。本地测试上面的程序求解大概在25秒左右。 - -8皇后因为一定是每个都在不同行,所以可以固定行,简化大量计算: -```prolog -% eight queens problem -% queens' number from 1 to 8 -% coordinate is (x, y) - -% every queen is valid -valid_queen((_, Col)) :- Range = [1, 2, 3, 4, 5, 6, 7, 8], member(Col, Range). - -% every queens in the board is valid -valid_board([]). -valid_board([Head | Tail]) :- valid_queen(Head), valid_board(Tail). - -% get the list of the column of every queen -cols([], []). -cols([(_, Col) | QueensTail], [Col | ColsTail]) :- cols(QueensTail, ColsTail). -% get diagonals value from left-top to right-bottom, represent with x + y -diags1([], []). -diags1([(Row, Col) | QueensTail], [Diagonal | DiagonalsTail]) :- - Diagonal is Col + Row, - diags1(QueensTail, DiagonalsTail). -% get diagonals value from left-bottom to right-top, represent with x+y -diags2([], []). -diags2([(Row, Col) | QueensTail], [Diagonal | DiagonalsTail]) :- - Diagonal is Col + Row, - diags2(QueensTail, DiagonalsTail). - -% eight queens -eight_queens(Board) :- - Board = [(1, _), (2, _), (3, _), (4, _), (5, _), (6, _), (7, _), (8, _)], - valid_board(Board), - - cols(Board, Cols), - diags1(Board, Diags1), - diags2(Board, Diags2), - - % rules - fd_all_different(Cols), - fd_all_different(Diags1), - fd_all_different(Diags2). - -% query: -% eight_queens([(1, A), (2, B), (3, C), (4, D), (5, E), (6, F), (7, G), (8, H)]). -``` -优化到了20秒以内。只能说搜索的能力确实强大。 - -## 总结 - -- 语法细节都还不甚了解,暂时不准备深入。 -- DSL,适用场景有限。 -- 声明式语言,通过深度优先的决策树求解问题,只需要描述问题,不需要描述解决方案。 -- 递归很核心很有用,写好需要一些技巧。 -- 对于某些约束问题很方便,作为通用语言已经几乎没有意义。 - -可以深入的点: -- 更多语法、库、内建的规则、方法。 -- 阅读第三方的经典实践,阅读应用于现实的代码。 - -TODO: -- 对文中的笑话建模,建模为一个简单的边带权重的有向图,找最短路径,设定有多条最短路径时的解决方式。 diff --git a/Python.md b/Python.md deleted file mode 100644 index 8ca4b1c..0000000 --- a/Python.md +++ /dev/null @@ -1,2842 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Python语言入门](#python%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8) - - [环境](#%E7%8E%AF%E5%A2%83) - - [变量与字符串](#%E5%8F%98%E9%87%8F%E4%B8%8E%E5%AD%97%E7%AC%A6%E4%B8%B2) - - [常用数据结构](#%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) - - [控制流](#%E6%8E%A7%E5%88%B6%E6%B5%81) - - [函数](#%E5%87%BD%E6%95%B0) - - [集合高级特性](#%E9%9B%86%E5%90%88%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7) - - [函数式编程](#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B) - - [模块](#%E6%A8%A1%E5%9D%97) - - [面向对象](#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - - [错误、调试与测试](#%E9%94%99%E8%AF%AF%E8%B0%83%E8%AF%95%E4%B8%8E%E6%B5%8B%E8%AF%95) - - [IO](#io) - - [并发编程](#%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B) - - [正则表达式](#%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [常用内建模块](#%E5%B8%B8%E7%94%A8%E5%86%85%E5%BB%BA%E6%A8%A1%E5%9D%97) - - [常用第三方模块](#%E5%B8%B8%E7%94%A8%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A8%A1%E5%9D%97) - - [virtualenv & pipenv](#virtualenv--pipenv) - - [图形界面](#%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2) - - [网路编程](#%E7%BD%91%E8%B7%AF%E7%BC%96%E7%A8%8B) - - [电子邮件](#%E7%94%B5%E5%AD%90%E9%82%AE%E4%BB%B6) - - [数据库](#%E6%95%B0%E6%8D%AE%E5%BA%93) - - [Web开发](#web%E5%BC%80%E5%8F%91) - - [异步IO](#%E5%BC%82%E6%AD%A5io) - - [总结](#%E6%80%BB%E7%BB%93) - - - -# Python语言入门 - -Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/)是一种广泛使用的解释型、高级和通用的编程语言。Python支持多种编程范型,包括函数式、指令式、结构化、面向对象和反射式编程。它拥有动态类型系统和垃圾回收功能,能够自动管理内存使用,并且其本身拥有一个巨大而广泛的标准库。 - -Python由吉多·范罗苏姆(Guido van Rossum,荷兰人)创造,第一版发布于1991年,它是ABC语言的后继者,也可以视之为一种使用传统中缀表达式的LISP方言。 - -语言特点: -- 基础库完善:网络、文件、GUI、数据库、文本等。 -- 第三方库众多。 -- 解释型语言,运行效率不高。 -- 动态类型、强类型。 - -多种解释器: -- CPython,官方版本,C语言编写。 -- IPython,基于CPython的一个交互式解释器。 -- PyPy:采用JIT技术对Python进行动态编译,目标是执行速度,[PyPy和Python有一定不同](https://doc.pypy.org/en/latest/cpython_differences.html)。 -- Jython:运行在JVM上的Python解释器,可以直接将Python代码编译为Java字节码执行。 -- IronPython:和Jython类似,编译到.NET字节码。 -- Python的解释器很多,但使用最广泛的还是CPython。如果要和Java或.Net平台交互,最好的办法不是用Jython或IronPython,而是通过网络调用来交互,确保各程序之间的独立性。 - -阅读: -- [Python3中文文档](https://docs.python.org/zh-cn/3/) -- [Python 语言参考手册](https://docs.python.org/zh-cn/3/reference/index.html) Python句法和核心语义,有一定基础可直接阅读。 -- [Python 教程](https://docs.python.org/zh-cn/3/tutorial/index.html) Python官方非正式教程,无基础可先从这开始阅读。 -- [Python 标准库](https://docs.python.org/zh-cn/3/library/index.html) Python标准库的文档,用来查阅。 -- [廖雪峰Python教程](https://www.liaoxuefeng.com/wiki/1016959663602400)(本文主要参考,用于入门) -- [Python 3 教程 | 菜鸟教程](https://www.runoob.com/python3/python3-tutorial.html) -- [Python最佳实践指南](https://pythonguidecn.readthedocs.io/zh/latest/index.html),一份第三方的最佳实践指南,强烈建议阅读。 - -## 环境 - -Python3和Python2不兼容,Python2已经停止维护,原则上不应再使用。时下(2021.10.1)最新版本3.9.7。 - -- 官方解释器CPython,下载安装配置环境变量。 -- VSCode安装Python插件。 -- 推荐IDE:PyCharm。 - -```python -print("hello,world!") -``` - -各种Python相关文件后缀: -- `.py` python源文件。 -- `.pyw` 默认的`.py`是控制台应用,而`.pyw`是用于编写GUI应用的,运行时`stdout stderr`输出无效,`stdin`只会读取到`EOF`。用`pythonw.exe`运行。 -- `.pyc` 类似于Java字节码文件,编译后的Python字节码脚本文件,供解释器使用,不想提供源码时可以提供。某些情况`__pycache__`就会生成和Python源文件同名并加上后缀`.cpython-3X.pyc`的文件,其实就是编译后字节码。如果源文件未发生改变,那么就不会再次编译,而是直接执行。 -- `.pyo` 优化编译后的`.pyc`文件,截止至Python3.5,现已不再使用。 -- `.pyd` 一般是其他语言编写的编译后Python扩展模块,提供给python用来调用。其实就是编译后的动态链接库。 -- `.pyi` 存根文件。 -- `.pyz` Python脚本存档,包含标准Python脚本头之后的二进制形式的压缩Python脚本(ZIP)的脚本。 -- `.pyx` Cython源文件,Python的C扩展,可以调用本地C/C++代码,提供接近C的性能。 -- `.pxd` Cython脚本,相当于C/C ++标头。 - - -编译运行: -- `python -m compileall ` 编译结果保存在`__pycache__/`下。编译后的`.pyc`可以通过`python xxx.pyc`运行。`compileall`其实就是python提供的一个模块。 -- 一般情况下是直接运行:`python xxx.py`。 - -## 变量与字符串 - -注释:行注释`#` - -代码块: -- 每一行一条语句。一行写多条语句可以用`;`分隔。 -- 语句以`:`结尾时,缩进表示代码块。 -- 约定俗成是用4空格缩进。 -- 不要在Python源文件中混用空格和Tab。 - -标识符: -- 字母数字下划线组合,不能用数字开头。 - -数据类型: -- 整数:没有大小限制,天然支持高精度。 -- 浮点数:没有大小限制,超出一定范围会直接表示为`inf`无穷大。其实就是IEEE 754 64位浮点数,最大范围在十进制下约为10的308数量级。 -- 字符串 -- 布尔值 -- 在Python中,任何数据都是对象, - -字面量: -- 整数:十进制、十六进制`0x`,整数浮点数中允许使用下划线分隔,下划线会被忽略。 -- 浮点:C写法。 -- 字符串:单引号或者双引号括起来的文本,使用其中一者时另一者可以不用转义。一般是如果字符串中包含其中一者就用另一者,如果都包含那使用转义字符。 -- 字符:和字符串一样,长度为1那就是字符,单引号或者双引号表示。具体有无字符这个类型还不好说,可能接受字符的函数只是通过字符串长度做了判断而已。 -- 转义字符:`\r \n \t \\ ...`。 -- 原始字符串:前缀`r""`内部字符串不会转义。其中的同样的引号仍需转义。 -- 多行字符串:`"""hello"""`,也可以是原始字符串。 -- 布尔值:`True False` -- 空值:`None` -- 习惯上使用全大写来定义常量,但是没有机制保证不变。 - -动态类型语言: -- 类型绑定发生在运行时。 -- 定义时不要求显式写出类型。 -- 可以将一个已经存在的变量赋为其他类型,变量即变为新类型。 - -字符串编码: -- ASCII,Unicode,UTF-8,GBK,不赘述。 -- 一般对Unicode的处理方式,文件使用utf-8编码,读取后Python字符串中按照Unicode码点形式存放,读取和保存是做解码和编码的工作。 -- Python支持Unicode,Python 3中字符串是以Unicode编码(即保存为Unicode码点)的。 -- `ord()`函数获取字符对应的Unicode码点,`chr()`将Unicode码点转化为字符。 -- 可以使用`\u4e2d`这种十六进制Unicode转义字符。 -- 字符串的`encode()`方法可以将Unicode字符串编码为指定编码的字节。对不能编码的字符,比如中文编码为`ascii`的话会运行错误。 -- 在编码得到的字节序列中,无法显示为ASCII的字符将显示为`\x##`的形式。 -- 如果读取了字节流,保存为字节序列,需要解码就需要调用`decode()`方法。如果字节中有无效的字节,可以添加命名参数`errors='ignore'`忽略错误的字节。 -- 字符串长度:`len(string)` -- 字符类型:`str` -- 字节序列类型:`bytes` -- 为了避免乱码问题,应当始终坚持使用UTF-8编码对`str`和`bytes`进行转换。 -- 字节序列`bytes`的字面量表示:`b"absd\x##"`,只能使用ASCII字符和`\x##`这种形式表示`7f-ff`之间的字节。 - -文件编码: -- 保存源文件时,最好保存为`utf-8`编码。 -- 当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行: -```python -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -``` -- 第一行是为了能在类Unix系统中当做脚本执行,windows中不能当脚本执行,不需要的话可以直接去掉。 -- 第二行告诉Python解释器,按照UTF-8编码读取源码,声明了UTF-8是确定了编译器的读取方式,并不意味这文件就是utf-8编码了。为了正确读取,需要保存为UTF-8 无BOM格式。 -- 支持中文标识符。 - -字符串格式化: -- 第一种和C类似`%`占位符实现:`"name = %s, age = %2d" % (name, age)`,格式化字符串后再用`%`加上参数列表。 -- 格式化占位符:`%d %f %s %x`等。 -- `%s`永远起作用,会把任何数据类型转换为字符串。 -- 第二种格式化方式是使用字符串的`format()`方法:`"name = {0}, age = {1:2.3f}".format(name, age)`。使用`{}`包围的从0开始的参数索引作为占位符。 -- 第三种格式化方式`f-string`,字符串前添加`f`,其中可以直接插入变量,用`{var:formatstr}`的形式。也即是字符串插值,算是最方便的方式。 -- 二三种格式中使用`{{ }}`对原始的`{ }`字符进行转义。 - -## 常用数据结构 - -列表`list`: -- 定义:`[elem1, elem2, ...]` -- 索引访问:`[index]`从0开始 -- 访问最后一个元素`[-1]` -- 追加`append` 插入`insert` 删除指定位置元素`pop(i)` -- 长度:`len(list)` -- 元素类型可以不同、列表中存列表可以当多维数组使用。 - -元组`tuple`: -- 定义:`(elem1, elem2, elem3)` -- 长度固定。 -- 同样可以使用`[index]`索引访问。 -- 定义元组时,元组的元素必须被确定下来,不能修改元素的值。 -- `()`定义空的元组。 -- 只有一个元素是和单纯的一个值有歧义,所以`(1)`表示值1,而不是一个元素的元组,末尾加`,`可以消除歧义,`(1, )`。 -- tuple确定下来后,tuple中的元素值便不能修改,也就是tuple元素指向的对象不能变,但是可以修改对象本身,比如对象是一个列表,那就可以修改列表的元素值。 - -字典: -- 类型:`dict` -- 定义:`d = {key1: value1, key2: value2, ...}`,元素无序。 -- 取元素: - - `d[key]`,不存在报错。 - - `d.get(key)`,不存返回None。 - - `d.get(key, -1)`,不存在时返回传入的默认值。 -- 判断key是否存在:`key in d` -- 插入:`d[key] = value` -- 删除某一个key:`d.pop(key)` -- 哈希表实现,O(1)插入删除时间复杂度,内存占用大。 -- 需要注意`dict`的key需要是不可变对象。这样才能才能保证每次通过哈希函数计算出的哈希值不变。 -- 元组是不可变对象,但是元组内的元素不一定是,元组需要内部元素都是不可变时才能作为key。 -- 最常见的key是字符串。 - -集合: -- 无序、哈希表实现、自动去重, -- 定义:`s = {elem1, elem2, ...}` -- 或者:`s = set([elem1, elem2, ...])` 传入一个列表得到列表所有元素构成的集合。 -- 直接对元素做哈希,元素需要不可变。 -- 添加:`add(key)` -- 删除:`remove(key)` 元素需要存在。 -- 判断是否在集合内:`key in s`。 -- 集合间操作:交集`s1 & s2` 并集`s1 & s2`。 - - -不可变对象: -- 字符串`str`是不可变对象,列表`list`是可变对象。 -- 对于可变对象:对其进行操作可以改变源对象内部的内容。 -- 不可变对象改变内容的操作都是返回一个新的对象,源对象保持不变。 -- 不可变对象优点:不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。同时多线程环境下同时读取不用加锁。 -- 编写程序时,如果可以设计为不变对象,尽量设计为不变对象。 - -## 控制流 - -条件: -```python -if condition: - if_statements -elif condition: - elif_statements -else: - else_statements -``` -- 并不要求条件一定是逻辑值`True False`,只要是非零数值、非空字符串、非空list等,都判断为`True`,否则`False`。 -- 检查是否是int类型`int()`,如果不是则会 - -循环: -```python -for x in collections: - body - -while condition: - body -``` -- 循环中可以用`break continue`。 - -范围: -- python提供了一个方便的`range(a, b)`函数,用来方便地遍历,传入`list(range(a, b))`可以得到a到b-1的列表。默认步长是1。 - - `range(a)` 0到a-1。 - - `range(a, b)` a到b-1。 - - `range(a, b, step)` a到b-1,指定步长。 -```python -for x in range(1, 5): # x from 1 to 4 - body -``` - - -## 函数 - -内置函数: -- [Python内置了很多有用的函数](https://docs.python.org/zh-cn/3/library/functions.html),前面已经用过一些了,包括字符和码点转换、输入输出、列表长度、构造范围列表等集合、常用数学操作等。都了解一遍是必要的。 -- 内置的帮助函数可以打印出内置函数的帮助:`help(abs)`,可以早交互式执行环境下尽情查看。 -- 数据类型转换:`int float`。 - -定义函数: -```python -def func(args): - body -``` -- 返回:`return retval`,函数结束时没有`return`语句会自动返回`None`,也可以显式地`return`和`return None`。 -- 空语句:`pass`语句什么都不做,一般用来作为占位符(比如条件、循环、函数体等)。比如还未实现函数,因为必须要有一个函数体。 -- 会检查函数调用的参数个数是否匹配。 -- 返回多个值: - - `return nx, ny`,结果会作为一个元组,所以和`return (nx, ny)`是等价的。这个过程叫打包(pack)。 - - 如果用一个变量来接收返回值,那么会获取到整个元组。用和元组大小匹配的变量来接收,那么会一一赋值(解包),其他情况,变量少了会 - - 用多个变量接受时其实对返回值就是做迭代,依次赋值。只要返回值是可迭代的比如列表,并且变量个数等于或多于元素元素个数,那么就能成功解包。 -- 如果有必要可以对参数做类型检查,使用内置的`isinstance(instance, typeOrTypeTuple)`。 - -参数: -- 默认参数:从后往前添加。 -```python -def power(x, n = 2): - res = 1 - while n > 0: - res *= x - n -= 1 - return res -``` -- 注意:python函数在定义时默认参数就被计算出来了,如果默认参数也是一个变量,多次调用时使用默认参数,如果在函数内改变了默认参数,那么后面的调用时参数就被改变了。要避免这一点,请将默认参数设置为不变量。即**默认参数必须指向不变对象!**,比如`str None`等。 - - -可变参数: -- 定义参数时使用`*args`,`args`在函数内将作为对应传入参数构成的元组。可以是空,可以是任意个数。 -- 调用时可以传0个或任意个参数。 -- 可以传入`list`或者`tuple`,只要在参数前加一个`*`,就等价于是将列表或者元组所有元素按顺序传入(所以传入非可变参数函数也是可行的,只要数量匹配)。这种写法非常常见。 - -关键字参数: -- 定义参数时使用`**args`,`args`在函数内作为一个字典,key是参数名称,值是参数值。 -- 可以传入0或任意个必须带参数名的参数。 -- 非常灵活,可以用在除了必要选项还支持用户自定选项的场景下。 -- 调用时同理,可以使用`**dict`方式调用,key必须是字符串,表示参数名。 - -命名关键字参数: -- 如果要限制关键字参数的名字,就可以用命名关键字参数。 -- 命名关键字参数需要一个特殊分隔符`*`,`*`后面的参数被视为命名关键字参数。 -```python -def person(name, age, *, city, job): # city and job are named keyword parameters - print(name, age, city, job) -``` -- 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。 -- 如果中间有可变参数了,那后面的参数自动成为命名关键字参数。 -- 命名关键字参数可以有默认值,并且调用时因为已经有参数命了,所以顺序可以随意。 - -参数组合: -- 在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。 -- 参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。 -- 对于任意形式组合参数列表,总是可以通过`fun(*args, **kw)`的方式调用,只要数量和名称是匹配的。 -- 必选参数也可以通过命名参数方式调用,然后后续的所有参数都必须以命名方式传入,对顺序没有要求。总体上**只要函数能够获取到所有参数并且不会重复即可**,没有传入但有默认值的参数就用默认值。 -- 使用太多组合会降低可读性,适度就行。 - -递归: -```python -def fib(n): - return fibonacci(n, 0, 1) - -def fibonacci(n, a, b): - if (n == 0): - return a - else: - return fibonacci(n-1, b, a+b) -``` -- python中函数不要求定义一定在调用前。 -- 尾递归优化可以避免递归层次太深导致的栈溢出问题。但好像cpython对上面的尾递归直接做优化,需要自己手动实现。 -- 在没有循环的函数语言中,循环只能通过尾递归实现。 -- python中限制了最大递归深度,`import sys`,通过`sys.getrecusionlimit()`可以获取最大递归深度,通过`sys.setrecursionlimit()`可以设置。 - -## 集合高级特性 - -切片(slicing): -- 取列表或元组部分元素:`L[beginIndex : endIndexExclusive : step]`,从开头开始取那起始索引可以省略,取到结尾时结束索引也可以省略,步长默认是1可省略,`:step`可选。结束索引超出范围按照最大计算。 -- 从末尾开始计数取元素,用负的索引即可:`[-2:-1]`,最后一个元素索引是`-1`而不是0需要记住。也就是负的索引范围是`-length to -1`。取到末尾那么结束索引应该省略而不是用0。 -- 起始结束都省略:`L[:]`即表示复制整个列表。 -- 正负索引可以混用,都是表示一个位置而已。 -- `step`为负可以倒过来切,此时同样包括起始但不包括结束,方向反过来了而已。形成的子序列和其在原序列中的相对顺序是反过来的,很好理解。 -- 字符串也可以切片,结果同样是字符串。 -- 得到的结果是新对象。列表得到列表,元组得到元组。 -- `range`也可以随机访问,也可以切片。 - -迭代: -- 用for循环来遍历一个集合,称之为迭代,`for x in collection`。 -- 除了对于`list tuple range`这种有下标的集合,还有`set dict`这种无下标的集合。只要可迭代,无论有无下标,都可以用`for`进行迭代。包括自定义的数据类型。 -- 对字典进行迭代: - - `for key in d` 默认迭代key。 - - `for key, value in d.items()` 字典元素其实就是二元组,也可以用一个变量获取。 - - `for value in d.values()` 迭代值。 -- 对可迭代的集合同时迭代下标: - - `enumerate()`可以将集合元素变成索引元素对。 - - `for index, val in enumerate(collection)` -- `from collections.abc import Iterable`,可迭代的对象都是`Iterable`类型。 - -列表生成式: -- 使用列表生成式(List Comprehensions)可以方便地生成列表。 -- `[expression for ... for ... if condition]` -- 可以有多层循环,可以添加条件进行筛选,条件满足才会执行前面的表达式得到元素。 -- 例:`[x*x for x in range(10) if x % 2 == 0]` -- `[]`中的式子本身是一个生成器。 -- `for`前面也可以添加`if-else`,函数是表达式,必须有`else`,而`for`后面的`if`是筛选条件,不能带`else`。 - -生成器: -- 一边执行循环一边生成元素的机制,就叫生成器(Generator),可以避免一开始就将所有元素生成。 -- 将上面列表生成式的式子用`()`括起来就是一个生成器。 -- 使用`next()`可以获取生成式的下一个元素,没有更多元素抛出`StopIteration`错误。 -- 生成器也是可迭代对象,用for循环迭代生成器时,整个迭代过程是生成器和循环代码交叉执行的,需要元素就执行生成器取出元素,取到后执行下一次循环,如此往复。生成结束后循环就结束了。 -- 创建了生成器后,其实一般不会通过`next`取元素来用,基本上都是通过`for`循环来迭代,不需要担心`StopIteration`错误。 -- 定义函数时在其中使用`yield`返回生成的元素使其成为一个生成器。函数返回值保存在`StopIteration.value`中,要获取返回值,需要在迭代完成后调用`next`捕获异常进行获取。 -- 杨辉三角例子: -```python -# pascal triangles -def triangles(max): - n, L = 0, [1] - while n < max: - yield L.copy() - L.append(0) - L = [L[i] + L[i-1] for i in range(len(L))] - n += 1 - return "done" - -g = triangles(10) -res = [elem for elem in g] -print(res) -``` - -迭代器: -- 可以被`next()`函数调用并不断返回下一个值的对象称为迭代器:`Iterator`。 -- `typing.py`中: -```python -Iterable = _alias(collections.abc.Iterable, 1) -Iterator = _alias(collections.abc.Iterator, 1) -``` -- 生成器都是`Iterator`对象,但`list`、`dict`、`str`虽然是`Iterable`,却不是`Iterator`。将其变为`Iterator`可以调用`iter()`函数。 -- Python的`for`循环本质就是不断调用`next`函数实现的。 -```python -for x in [1, 2, 3, 4, 5]: - pass -# equals to -it = iter([1, 2, 3, 4, 5]) -while True: - try: - x = next(it) - except StopIteration: - break -``` -- `Iterator`继承自`Iterable`,后续再详述。 - -## 函数式编程 - -纯粹的函数式编程语言中没有变量,任何一个函数,只要输入确定,输出就是确定的。这种纯函数是没有副作用的。而允许使用变量的编程语言中,函数内部状态不确定,同样输入可能得到不同输出,称之为有副作用。 - -函数编程特点: -- 函数可以用来赋值。 -- 可以作为参数返回值 -- 支持高阶函数、闭包、柯里化。 - -高阶函数:使用函数作为参数返回值的函数。 -```python -def add(x, y, f): - return f(x) + f(y) -``` - -映射(map)和规约(reduce): -- `map(func, iterable)`传入一个函数和`Iterable`,将函数一次作用于序列每个元素,得到一个`Iterator`。`Iterator`是惰性序列,要求出具体结果需要遍历,或者传入`list()`得到序列。 -- `from functools import reduce` `reduce(func, iterable, ...)`传入一个函数,一个`Iterable`,这个函数必须接受2个参数,reduce将func应用于序列第1和第2个元素,并将结果继续运用于下一个元素,直到序列结束。不是内建函数,可以传入多个`Iterable`,这是函数接受与`Iterable`个数相同的参数,运用之后得到结果,最短的`Iterable`迭代完之后将结束。 -```python -# map -def add10(x): - return x + 10 -print(list(map(add10, [1, 2, 3]))) - -def add(x, y): - return x + y -print(list(map(add, range(100)[::-1], [-x for x in range(100, 250)]))) - -# reduce -def sum(x, y): - return x + y -print(reduce(sum, range(101))) - -# example -DIGITS = dict((chr(ord('0') + val), val) for val in range(10)) # '0': 0, '1': 1, ... -def str2int(s): - def fn(x, y): - return x * 10 + y - def char2num(s): - return DIGITS[s] - return reduce(fn, map(char2num, s)) -``` -- `filter(func, iterable)`用于过滤,其实也是广义上的映射。根据函数作用于元素是`True`保留,`False`丢弃。 -- 例子:埃拉托色尼筛法求素数。 -```python -# example: use Sieve of Eratosthenes to find all prime nunbers -# https://zh.wikipedia.org/wiki/%E5%9F%83%E6%8B%89%E6%89%98%E6%96%AF%E7%89%B9%E5%B0%BC%E7%AD%9B%E6%B3%95 -def _odd_iter(): - n = 1 - while True: - n = n + 2 - yield n - -def _not_division(n): - return lambda x: x % n > 0 - -def primes(): - yield 2 - it = _odd_iter() # generate odd numbers - while True: - n = next(it) - yield n - it = filter(_not_division(n), it) # construct new Iterator - -gp = primes() -print([next(gp) for _ in range(100)]) -``` -- 内建的`sorted(iterable, *, key=None, reverse=False)`方法。接受`key`函数实现自定义排序,`key`函数将作用于元素上,根据其结果进行排序。比如字符串忽略大小写传入`str.lower`,返回排序后的新列表。 - -闭包: -- 函数作为返回值时,调用函数将获得返回的函数,此时传入的参数变量等状态被保存,也就是喜闻乐见的闭包了。 -- 返回闭包时不要引用任何循环变量,或者后续会发生变化的量。 -- 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。 -```python -# every call will return a incresing value -def createCounter(): - n = 0 - def counter(): - nonlocal n # need define as nonlocal, if call outter local variable - n = n + 1 - return n - return counter - -c = createCounter() -print([c() for _ in range(10)]) -d = createCounter() -print([d() for _ in range(10)]) -``` - -匿名函数: -- 传入函数时,有些比较简单的情况,传入匿名函数更加方便。 -- Python中对匿名函数提供了有限的支持。 -- 语法:`lambda args: expression`,匿名函数的限制是只能有个表达式,不用写`return`,返回值就是该表达式值。 -- 匿名函数也是函数对象,也可以赋值。 -- 支持确实有限,相比之下Scala就灵活了很多。 -```python -f = lambda x : (lambda y : (lambda z : x + y + z)) -print(f) -print(f(1)) -print(f(1)(2)) -print(f(1)(2)(3)) # 6 -``` - -偏函数: -- `functools`模块中提供了偏函数(partial function)支持。 -- `functools.partial`就是用来帮助创建偏函数的。 -- python中偏函数是指,把一个参数的某些参数固定住,并返回一个新的函数。调用这个函数会更加简单。 -```python -from functools import partial - -# partial function -int2 = partial(int, base = 2) -print(int2("1000")) # 8 - -# equals to -kw = {"base": 2} -print(int("1000", **kw)) - -def f(a, b, c, d): - print(f"{a}, {b}, {c}, {d}") - -f1 = partial(f, 10, 20) -f1(30, 40) # 10, 20, 30, 40 -``` -- 偏函数如果定义时传了命名参数,在生成的偏函数调用中还可以通过命名参数的方式覆盖这个偏函数定义时传入的参数。 -- 定义时不命名的话会将参数加到`*args`的最左边。不能使用命名参数再覆盖。 -- 命名关键字参数只能以命名方式传入,位置参数可以通过位置传入就是从前往后,也可以命名传入,那么后面的都需要命名传入。和普通函数调用规则差不多。 - - -装饰器(Decorator): -- 在运行期增加函数功能的一种方式,装饰器模式在语言层面的实践。 -- 本质上,decorator就是一个返回函数的高阶函数。调用要修饰的函数,并添加自己的功能。 -- 例子:添加日志打印功能。 -```python -# decorator -def log(func): - def warpper(*args, **kv): - print(f"call {func.__name__}") - return func(*args, **kv) - return warpper - -@log -def now(): - print('2021-10-2') - -def now2(): - print('2021-10-2') - -now() -log(now2)() # equals to now() -print(now.__name__) # wrapper -``` -- 将`@log`放在函数定义处,相当于执行`log = log(now)`。 -- 如果`log`需要加参数,那么就需要多加一层,最外层接受装饰器参数,里层接受函数,最里层添加逻辑执行转调。此时经过装饰后的函数 `__name__`等属性会变成最里层函数的属性,需要添加`@functools.wraps(func)`来将原始函数的属性复制到`warpper`函数函数中。 -```python -# decorator with arguments -def log2(text): - def decorator(func): - @functools.wraps(func) - def warpper(*args, **kv): - print(f"{text} : {func.__name__}") - return func(*args, **kv) - return warpper - return decorator - -@log2("execute") # equals to now3 = log("execute")(now3) -def now3(): - print('2021-10-2') - -now3() -print(now3.__name__) # now3, if without @functools.wraps(func), will be wrapper -``` - -## 模块 - -模块: -- 在Python中,一个`.py`就是一个模块。 -- 可以避免函数变量名冲突,编写模块时不必考虑名字会与其他模块冲突,但要注意尽量不要和内置函数重名。 -- 为了避免模块名冲突,Python又引入了按目录组织模块的方法,称为包(Package)。 -- 引入包以后,只有顶层的名字不与别人冲突,那所有模块就不会与别人冲突。 -``` -mycompany -├─ __init__.py -├─ abc.py -└─ xyz.py -``` -- 上述例子中`mycompany`中的模块名就分别是`mycompany.abc` `mycompany.utils`。 -- 每个包目录下都会有一个`__init__.py`文件,是必须存在的,否则Python不会将其视为包。`__init__.py`可以是空文件,也可以有Python代码,因为`__init__.py`本身就是一个模块,而它的模块名就是`mycompany`。【Python3.3后版本模块已经可以不要这个文件了。】 -- 自定义模块时只有命名不要和Python自带模块冲突。例如系统引入了`sys`模块,就不要再命名`sys.py`,否则将无法导入系统自带的`sys`模块。 -- 模块名要遵循Python变量命名规范,不要使用中文、特殊字符。 -- 模块名(文件名)不要和系统模块冲突,最好先查看系统是否有这个模块,交互环境下`import abc`成功则说明存在。 - -写一个模块的标椎手法: -- 脚本和编码注释。 -- 模块代码的第一个字符串都被视为模块的文档注释。 -- 使用`__author__`变量表明作者。 -- 后面是真正的代码部分。一般导入模块写在最前面。 -```python -if __name__=='__main__': - test() -``` -- 然后是通过命名行运行模块文件时,`__name__`会被置为`__main__`,其中逻辑就会执行,而如果在其他文件中导入(此时`__name__`是模块名)判断就会失败,就不会执行。最常见是将模块内测试代码写在此处。 - -作用域: -- 在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过`_`前缀来实现的。 -- 正常的函数和变量名是公开的,可以被直接引用。 -- 类似`__xxx__`这种变量是特殊变量,可以直接引用,但是有特殊用途,比如`__author__ __name__`,模块文档注释可以通过特殊变量`__doc__`引用。自己定义变量一般不要定义为这种形式。 -- 类似于`_xxx __xxx`这种命名的变量是非公开(private)的,不应该直接引用(其实也是可以引用的,只是编程习惯约定而已)。 -- 外部不需要使用的函数全部定义为`private`,只有需要引用的才定义为`public`(通过命名的方式,非常简单粗暴)。 -- 引入模块的操作只作用于当前模块,也就是当前文件,其他模块引入了该模块并不会引入该模块引入的模块。 - -包管理工具pip: -- [PyPI](https://pypi.org/)(The Python Package Index)是Python的包管理工具,可以搜索安装第三方库,命令是`pip`。 -- 比如:`pip install numpy`。 -- 版本:`pip --version` -- 更新`pip`:` python -m pip install --upgrade pip`。 -- 换源安装: -```shell -pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple -``` -- 国内镜像设为默认源: -```shell -# 清华源 -pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple - -# 或: -# 阿里源 -pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ -# 腾讯源 -pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple -# 豆瓣源 -pip config set global.index-url http://pypi.douban.com/simple/s -``` -- 更新包: -```shell -pip install --upgrade -``` -- 安装包: -```shell -pip install packagename # 最新版本 -pip install packagename==1.0.4 # 指定版本 -``` -- 卸载包: -```shell -pip uninstall -``` -- windows下一般Python安装到了`Program Files`目录中的话,安装第三方库时会没权限写入,转而安装到`Users\user\AppData\Roaming\Python\Python39\site-packages`下。 -- `import sys`,`sys.path`下存放着包的搜索目录,包含安装目录和用户目录,都能搜索到。不需要太过关心。 -- 要添加自己的搜索目录: - - 第一种`sys.path.append("your_path")`。这种方法是运行时修改,运行后失效。 - - 第二种方法是设置环境变量`PYTHONPATH`,该环境变量中路径会自动添加到模块搜索路径中。只需要添加自己的搜索路径,Python标准库和第三方库的路径不受影响。 - -使用Anaconda: -- Anaconda自带了很多python第三方科学计算库。可以方便地直接使用。 -- 安装时如果选择添加环境变量到path,就会将系统path中的Python指向自己的Python,在命令行下就能使用这些库了。 - -## 面向对象 - -面向对象: -- Python中,所有数据类型都可以视作对象。同样也支持自定义类。 -- 类的数据成员在Python中称为属性(Property),成员函数称为方法(Method)。 -- 定义类: -```python -class Person(object): - def __init__(self, name, age) -> None: - super().__init__() - self.name = name - self.age = age - def print(self): - print(f"name: {self.name}, age: {self.age}") - def hello(): - print("hello") -``` -- 如果没有合适的基类,就使用`object`。 -- 担任构造方法角色的是`__init__`方法。 -- Python中定义实例方法第一个参数一定是`self`,通过实例调用时解释器会默认传入实例自身,但定义时需要显式声明。 -- 定义类方法则不用传入`self`,这是时可以通过类名调用。以第一个参数是否是`self`区分实例和类方法。 -- 在实例方法中调用其他实例方法需要通过`self.method()`,调用类方法需要通过类名`ClassName.method()`。 -- 和静态类型语言不通,Python允许实例变量绑定任何数据,通过`instance.newproperty = val`就可以绑定新属性到实例上。 -- 构造新实例:`instance = ClassName(*args)`。 -- 类方法和实例方法是有区分的,也就是有没有传`self`,调用时也能明确的确定。但是属性就不一样了,类的属性(也就相当于静态字段或静态数据成员)是可以通过实例访问的,所以千万不要定义同名的实例属性和类属性。在类中可以通过`self`或者类名调用区分,但外部调用时就区分不了了(用实例可以调,但不能通过实例改,一改其实就相当于定义了同名实例属性了)。 - - 即**不要定义同名类属性/方法和实例属性/方法**。 - - 一般来说经验也是不要通过实例调用类属性。 - -访问修饰: -- 不通过任何修饰符,还是通过变量名称,属性或者方法前添加`__`就变成了私有的,只能内部访问。 -- 当然同样,特殊方法和属性,前后都有`__`的方法外部还是可以访问。 -- 封装依然可以做,可以添加getter和setter,可以在其中视情况做一些参数检查和容错。 -- 也会有单下划线开头的,外部可以访问,但一般约定为私有的。 -- 其实并不是不可访问,只是双下划线开头的做了修饰,`__attr`被修饰成了`_ClassName__attr`,其实依旧可以访问。脱裤子放屁有一套,简单粗暴,能用就行。毕竟代码是死的人是活的。这算是CPython解释器特性,所以不建议这么干,不同解释器规则可能不同。 -- 如果在外部设置私有变量是可以成功的,但和想设置的那一个不是一个变量了,毕竟内部的已经做了名称修饰。比如`p.__name = "lisa"`其实是新加了一个变量。 - - -继承与多态: -- `class ClassName(baseClass):` -- 子类会继承父类的全部实例属性和方法。 -- 子类中不能访问父类的私有属性和方法,因为经过了名称修饰之后子类中再去访问同名属性经过修饰后和基类是不一样的,会相当于在子类中新增了同名属性,而不会访问到基类的那一个。 -- 子类父类存在相同方法时,子类的覆盖父类的。 -- 所有类型最终都有共同基类`object`,不写基类默认就是`object`。 -```python -print(isinstance(1, object)) # True -print(isinstance(None, object)) # True -print(isinstance("hello", object)) # True -print(isinstance(True, object)) # True -``` -- 类方法(也就是其他语言中所说的静态方法)不会继承,只能通过自己的类名访问。 - - -鸭子类型: -- 动态语言是鸭子类型的,就决定了实现多态不必像静态类型一样必须继承,只要实现同样的方法,就可以视为实现了多态。 - -对象信息: -- 类型:`type(obj)`,得到的`type`类型对象。 -- 可以理解`int str bytes`等类型都是这个`type`类型实例。所以内置类型可以直接这样判断类型`type(1) == int` -- 函数则可以使用`types`中定义的常量: -```python -def f(): - pass -# all True -print(type(1) == int) -print(type(type) == type) -print(type(object) == type) -print(type(int) == type) -print(type(f) == types.FunctionType) -print(type(abs) == types.BuiltinFunctionType) -print(type(x for x in range(100)) == types.GeneratorType) -print(type([].append) == types.BuiltinMethodType) -print(type(lambda x : x) == types.LambdaType) -``` -- `type`是确定对象的严格类型,`isinstance`则是可以匹配对象类型或者其基类类型。 -- `isinstance`第二个参数可以是类型,也可以是类型元组,用于匹配多个类型,只有有一个匹配,就返回`True`。 -- 一般来说为了支持多态总是优先使用`isinstance`。 -- 获取一个对象的所有属性和方法:`dir(obj)`,得到一个列表。 -- 前后双下划线的特殊方法都是有用途的,比如`__len__`方法,就用于内建的`len`函数,`len`函数实际上就是调用`__len__`方法。只要实现了`__len__`方法,就可以用于`len`函数。 -- 除了列出属性和方法,配合`getattr()`、`setattr()`以及`hasattr()`,可以直接操作一个对象的状态。 -```python -# dir, getattr(), setattr(), hasattr() -class Person(): - def __init__(self, name) -> None: - self.name = name - def sayHi(self): - print(f"hi, {self.name}") - def sayHello(): - print("hello") - -p = Person("Adam") -print(dir(p)) -print(hasattr(p, "name")) -setattr(p, "age", 10) -print(p.age) -print(getattr(p, "age")) -print(getattr(p, "nonexist", "default value")) - -f = getattr(Person, "sayHello") -f() -print(f) # -f = getattr(p, "sayHi") -f() -print(f) # > -``` -- 也可以获取方法,获取到后就是一个函数,实例方法就通过对象获取,相当于第一个参数已经传递。而类方法,就通过类名(其实也是一个对象)来获取。 -- 通过内置函数,可以对Python对象进行剖析,拿到对象信息,一般只有不知道对象信息时才这样做。 - - -动态绑定属性: -- 动态类型具有静态类型不具有的灵活性,例如动态给对象或者类添加属性和方法。 -```python -class Person: - def __init__(self, *args, **kwargs) -> None: - self.age = kwargs.pop("age") - -def setAge(self, age): - self.age = age -def getAge(self): - return self.age - -# bind to a single Person object -p = Person(age = 20) -p.setAge = MethodType(setAge, p) -p.getAge = MethodType(getAge, p) -p.setAge(10) -print(p.age) # 10 -print(p.getAge()) # 10 - -# bind to Person class (instance of type class) -Person.setAge = MethodType(setAge, Person) -Person.getAge = MethodType(getAge, Person) -Person.setAge(18) -print(Person.getAge()) # 18 -print(Person.age) # 18 - -# bind instance method to class, just assignment -Person.setAge = setAge -p = Person(age = 10) -p.setAge(100) -print(p.age) # 100 -print(p.getAge()) # 18, Person has no instance method call getAge(), so will call Person.getAge() -> 18 -Person.getAge = getAge -print(p.getAge()) # 100 -``` -- 注意就算定义了`self`,`MethodType`第二个参数是要添加方法的实例,所以通过这种方式绑定方法到类其实是成为类方法而不是成为实例方法。【Python中类也是对象(`type`类的实例)!!!】 -- 要绑定类的实例方法到类上,直接赋值就可以搞定! -- 总结: - - 通过`MethodType`绑定,**第二个参数作为`self`被传入方法**,方法必须有`self`参数。 - - 通过赋值绑定的不会传入一个默认的`self`。 - - 通过赋值直接绑定到类上就和直接定义在类里面没有区别,绑定到类上通过实例调用则会将实例作为`self`传入。 - - 通过赋值绑定到对象上也可以,不能有`self`参数。 - - `MethodType`的结果给人感觉就像是定义了一个偏函数(类型本身打印出来并不是),然后指定了`self`参数。 -- 类方法和实例方法并不像其他语言区分那么严格(或许就不该这么区分):通过实例调用会隐式传`self`,同时通过类也可以调用,只要把实例放在`self`位置,效果是完全一样的。 -```python -class Student(object): - __slots__ = ("name", "age", "getName") - def getAge(self): - return self.age - -s = Student() -s.age = 10 -f = s.getAge -print(f) # > -print(Student.getAge) # -print(f()) # 10 -print(Student.getAge(s)) # 10 -``` -- 需要分清楚绑定方法和函数:`MethodType`返回的结果就是一个绑定方法,通过对象调用的实例方法也是一个绑定方法。 - -使用`__slots__`: -- 动态添加属性很方便也可能被滥用,Python中允许限制实例的属性,通过定义一个特殊的`__slots__`变量,限制能添加的属性。 -```python -class Student: - __slots__ = ("name", "age") - -s = Student() -s.age = 10 -s.name = "Adam" -# s.grade = 4.0 # AttributeError -``` -- 此时再添加其他属性,就会失败。方法也可以看做函数类型的属性,所以添加同样会失败。 -- `__slots__`定义的属性仅对当前类实例的属性起限制作用,对继承子类实例和类本身属性不起作用。如果继承的子类中使用`__slots__`,那么能用的属性就是自身加上父类的。 -- 如果父类不限制,仅子类限制,那么子类实例也是可以绑定新属性的。【这确实有点让人迷惑!】 -- 也就是说要限制属性必须要继承链条上所有类都有`__slots__`才行。 - - -`@property`: -- 直接暴露属性简单但是如果要做参数有效性检查就麻烦了,设置为私有并添加对应的getter和setter也可以不过调用起来就有点繁琐了。 -- 通过Python内置的`@property`装饰器可以把一个getter方法变成属性调用,然后本身又会创建另一个装饰器`@attr.setter`添加到`setter`上就可以直接通过属性形式转调方法实现读写。 -```python -class Person: - __slots__ = ("_age") - @property - def age(self): - return self._age - @age.setter - def age(self, value): - if not isinstance(value, int): - raise ValueError("age must be a integer!") - if value < 0: - raise ValueError("age must non-negative!") - self._age = value - -p = Person() -p.age = 10 -print(p.age) -p.age = "18" # ValueError: age must be a integer! -``` -- 只定义getter不定义setter就是只读属性。 -- 属性的方法不要和实例变量重名,实例变量最好使用`_`开头的私有访问。 - -多继承/Mixin: -- `class ClassName(BaseClass1, BaseClass2, ...)` -- 基类可以有多个,如果多个类有同一个方法,那么继承顺序按照顺位调用第一个。 -- 多继承也叫混入,不同语言有不同语言的叫法。 -- `__mro__`特殊变量是基类的元组,一个实例的方法解析期间基于此来查找基类。 -- 已知`__mro__`,在使用[`super`](https://docs.python.org/zh-cn/3/library/functions.html?highlight=super#super)时可以在类定义中调用基类方法的情形中从`__mro__`元组中的特定位置开始查找。 -```python -# -*- coding: utf-8 -*- - -class A(object): - def foo(self): - print('A foo') - def bar(self): - print('A bar') - -class B(object): - def foo(self): - print('B foo') - def bar(self): - print('B bar') - -class C1(A, B): - pass - -class C2(A, B): - def bar(self): - print('C2-bar') - -class D(C1, C2): - pass - -if __name__ == '__main__': - print(D.__mro__) # (, , , , , ) - d = D() - d.foo() # A foo - d.bar() # C2-bar -``` -- 菱形继承不会有多份数据,最终都是通过`__mro__`中的查找顺序来确定的。 - -定制类: -- 通过定义特殊变量和方法可以定制特定的功能。 -- `__xxx__` 特殊变量的用途: -- `__slots__` -- `__len__()` 用于`len`函数。 -- `__str__()` 返回字符串,调用`print`打印对象时会打印这个字符串。 -- `__repr__()` 为调试服务,python交互环境中输入变量打印出的那个字符串,通常`__repr__`和`__str__`是一样的,可以直接`__repr__ = __str__`。 -- `__iter__()` 返回迭代器(`Iterator`),要在`for in`循环中使用必须重写这个方法,`for`循环拿到迭代器后会调用器`__next__()`获取下一个元素,直到`StopIteration`。`collections.abc.Iterable`是提供了这个方法的抽象基类。`iter(obj)`内置函数调用这个方法。 -- `__next__()` 返回下一个元素,`Iterator`类型是提供了`__iter__()`和`__next__()`的抽象基类。`next(obj)`调用这个函数。 -- `__getitem__()` 用于通过下标访问`[]`,可能传入整数下标,可能传入切片对象(`slice`),比如对于`dict`,可能传入的是一个作为key的对象。视支持情况实现。 -- `__setitem__()` 通过下标设置元素`[]`。 -- `__delitem__()` 删除元素。 -- `__getattr__()` 动态返回属性,只有类中没有的才会尝试通过这个方法获取,类似于`method_missing()`的功能。 -- `__call__()` 一个对象实例可以有自己的属性和方法,定义`__call__()`之后就可以直接对实例进行调用,可以类比为C++中的函数对象,可以有参数。这样其实函数和对象的边界就很模糊了。`callable(obj)`会检查对象是否可调用。比如其实自定义类也是一个`type`的对象,创建时调用`className()`其实就是调用了`type`的`__call__()`然后可能是转调了自定义类的`__init__`。 -- 每种内置类型都会定义很多的特殊属性和方法。通过实现同样的方法就可以模拟这些行为,甚至不需要去继承抽象类,因为Python是鸭子类型的,依赖方法而不依赖接口。 -- 很多内置函数都是依赖于特殊属性和方法的,将所有[特殊属性、方法](https://docs.python.org/zh-cn/3/reference/datamodel.html#special-method-names)和[内置函数](https://docs.python.org/zh-cn/3/library/functions.html)都理解一遍是有必要的。 - -枚举类: -```python -from enum import Enum -WeekDay = Enum('WeekDay', ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')) -``` -- 更精确的控制可以派生`Enum`:`@unique`装饰器检查没有重复值。 -```python -@unique -class WeekD(Enum): - Sun = 0 - Mon = 1 - Tue = 2 - Wed = 3 - Thu = 4 - Fri = 5 - Sat = 6 -print(WeekD) # -print(WeekD.Sun) -print(WeekD["Tue"]) -print(WeekD(1)) -print(WeekD.Fri.value) - -for name, member in WeekD.__members__.items(): - print(name, "->", member) -``` -- 访问: - - `EnumClassName.member` - - `EnumClassName["membername"]` - - `EnumClass(valueofenum)` 从常量构建枚举值。 - - `EnumClass.__members__` 获取枚举名到枚举常量的字典。 - - `EnumClass.member.value` 枚举常量的值。 - -`type`: -- 动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。 -- 当Python解释器载入一个模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个其中类的class对象。 -- `type`函数查看一个类型或者变量的类型,也就是用类名表示的那个`type`类型实例。传入类名得到的结果就是`type`,自定义的类其实就是`type`类型的实例。 -- `type`函数还可以用于创建一个新的类型。依次传入: - - 类名。 - - 继承父类的元组。 - - 方法名称与函数绑定的字典。 -```python -def fn(self, name = "world"): - print(f"hello, {name}!") - -Hello = type("Hello", (object, ), dict(hello = fn)) - -h = Hello() -h.hello() -print(type(h)) # -print(Hello) # -``` -- 通过`type`创建类直接通过`calss`关键字创建时完全一致的,也非常简单。而在静态语言中创建类必须通过各种方式动态编译才能做到。 - -元类metaclass: -- 除了`type`还可以通过元类来管理类的创建行为。 -- 可以将类理解为元类的实例,要通过元类来创建类,就需要先定义元类。可以通过元类来创建或者修改类。 -- 元类是Python面向对象中最难理解、最难使用的魔法代码,正常情况下不会碰到。 -- 元类是类的模板,所以需要从`type`派生。 -```python -# create class dynamically using metaclass - -# first define metaclass, derived from type -class ListMetaClass(type): - def __new__(cls, name, bases, attrs): - attrs['add'] = lambda self, value: self.append(value) - return type.__new__(cls, name, bases, attrs) - -# create class using metaclass -class MyList(list, metaclass = ListMetaClass): - pass - -print(MyList) # -print(type(MyList)) # - -# create instance using calss -l = MyList() -l.add(10) -l.add(100) -print(l) # [10, 100] -``` -- 定义类时传入`metaclass`关键字参数即可使用元类。 -- 此时创建出的`MyList`类(对象)的类型不再是`type`,而是自定义的从`type`派生的元类,在其中重写`__new__`并添加方法,调用`__type__`的`__new__`并且添加了自定义的`add`方法。 -- `__new__`接收参数: - - 当前准备创建的类对象。 - - 类名字。 - - 类父类集合。 - - 类方法集合。 -- 最终`MyList`是`list`子类,同时类型是`ListMetaClass`(`type`子类)。 -- 那么可以理解为`type`就是一个元类,并且应该作为自定义元类的基类。有一点点抽象,可能需要深入了解一个类的具体创建过程才理解。 -- `__new__ __init__`区别是前者是创建过程,是一个静态方法,返回这个实例,后者是已经创建好了后的初始化过程,实例方法,构造时使用前者返回的实例调用`__init__`,返回`None`。两者加起来是实例化过程。 -- 元类可以隐式继承到子类中。 - -元类使用例子: -- 定义一个简单的ORM(Object Relational Mapping,对象关系映射)框架,将数据库的一张表映射为一个类,每一行映射为一个实例。通过定义调用方法就可以实现`update insert delete`等操作。 -- [代码在Python分支](../../tree/Python/OOP/ORM.py) - - -总结: -- Python面向对象实现相当自由与简单,但动态类型的确是这样的。 -- 写惯了静态类型切换过来感觉确实诸多魔法。 -- 万物皆对象,甚至类也是对象,函数也是对象,对象也可以被调用。 -- 特殊属性非常有用非常魔法,自由度可能不及运算符重载,稍微繁琐了一点,但调用规则都是语法内置的,不像运算符重载读起来那么摸不着头脑。 -- 还需要进一步深入。 - - -## 错误、调试与测试 - -各种各样错误: -- 输入非法。添加检测。 -- 逻辑问题,修改逻辑。 -- 运行时错误,比如磁盘写满,网络连接中断等。这时候就需要异常处理。 - -异常处理: -- 可以使用错误代码返回值,需要大量代码判读是否出错。没执行一个函数都要检查返回值。 -- 异常处理:`try...except...finally...`,将可能出错的逻辑放在`try`中运行,出错则会跳转到`except`,然后执行`finally`(finally中语句无论是否发生错误都会执行,可以没有`finally`)。 -- 可以有多个`except`依次捕获不同类型的错误,直到捕获到。未捕获到则会继续向上抛,直到被Python解释器捕获,打印出调用栈,结束程序运行。 -- 所有错误的基类`BaseException`。 -- [Python内置的异常的派生结构层次](https://docs.python.org/zh-cn/3/library/exceptions.html#exception-hierarchy)。 - -记录错误: -- 使用`logging`模块,捕获错误记录下后继续执行。 -- `import logging` -- 捕获错误时:`logging.exception(e)` - -抛出错误: -- 错误不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多错误,自己编写的函数也可以抛出错误。 -- 可以自定义错误类,选择好继承关系,使用`raise AnExceptionInstance`抛出。 -- 在捕获一个错误后可以记录下错误,如果无法处理,再向外抛出。直接`raise`可以原样抛出,也可以抛出另一种类型的错误(需要有道理可言比如说把多种错误合并成一种公共的错误,不能说转换成一种不相干的错误,那样会干扰错误诊断)。 -- 出错都是希望得到处理的,分析错误信息并定位错误发生的代码位置并修正错误才是最关键的。 -- 如果程序给别人用,应当说明什么情况下会抛哪些错误,以帮助使用者编写错误处理逻辑。 - -调试: -- `print`大法永远可行,但是调完后得删,其实就是简略版日志。 -- 断言:`assert assertionCondition, errinfoString`,断言失败会抛出`AssertionError`。启动时加上`-O`选项关闭断言。 -- 日志:`import logging`。 - - 不同级别:`NOTSET DEBUG INFO WARNING/WARN ERROR FATAL/CRITICAL` - - 设置输出级别:`logging.basicConfig(level = logging.INFO)` - - 对应输出方法:`debug info warning error/exception critical/fatal` - - 指定`level = INFO`时`debug`就不起作用了。 - - 默认日志级别为`WARNING`。 - - 通过简单的配置,一条语句可以输出到不同地方,比如控制台和文件。 - - 日志是比较方便的工具。 -- `pdb`单步调试:`python -m pdb xxx.py` - - `l`查看代码。 - - `n`单步执行。 - - `p variable`查看变量。 - - `a` 查看当前函数所有变量。 - - `q` 退出程序。 - - `c` 继续执行。 -- `pdb`设置断点: - - `import pdb` - - `pdb.set_trace()` 设置断点。 -- 命令行调试还是太麻烦了,使用VsCode或者PyCharm就行。 -- 使用IDE调试虽然方便,但到最后`logging`才是终极武器。对于大型系统单步调试关注局部,大量日志分析更关注整体,都必不可少。 - -单元测试: -- 单元测试是对一个模块、一个类或者一个函数进行正确性检验的测试工作。 -- 思路,比如对于函数`abs()`: - - 输入正数期待与输入相同。 - - 输入负数期待与输入相反。 - - 输入0期待返回0。 - - 输入非数值类型,比如`[] () {} None`期待抛出`TypeError`。 - - 以上测试放到一个模块中就是一个完整的针对`abs`函数的单元测试。 -- 如果测试未通过,需要确定是单元测试编写得有问题还是函数有bug,有则修复,使之能够测试通过。 -- 单元测试的意义:在我们修改了`abs()`代码之后,再跑一边测试用例,通过则说明修改对现有`abs()`函数原有的行为未造成影响。不通过的话,要么修改代码与原来需求一致,要么修改测试函数功能发生变化(使用的地方同样需要注意)。 -- 这种测试驱动开发的好处就是确保程序模块行为符合设计的测试用例,在将来修改重构时,可以极大程度保证该模块行为仍然是正确的。 -- python自带的单元测试模块:`unittest` -- 测试类从`unittest.TestCase`继承,其中以`test`开头的方法就是测试方法,不以`test`开头的方法不会被认为是测试方式,测试时不会被执行。 -- `unittest.TestCase`提供了很多内置的条件判断: - - `assertEqual` 最常用 - - `assertTrue` - - `assertRaises` 处理错误输入,会抛出异常的情况。 -- 最后执行测试的逻辑中直接执行`unittest.main()`(放到`__name__ == "__main__"`中)即可执行这些测试,不需要一个一个添加到代码中。 -- 运行测试:`python xxx.py`或者`python -m unittest xxx`测试模块。 -- 可以在单元测试中编写两个特殊的`setUp()`和`tearDown()`方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。设想你的测试需要启动一个数据库,这时,就可以在`setUp()`方法中连接数据库,在`tearDown()`方法中关闭数据库。 -- 单元测试通过并不代表没有bug了,但是没有通过一定就有bug,无论是测试代码还是具体逻辑。 - -文档测试: -- Python内置的文档测试`doctest`可以直接提取注释中的代码并执行测试。 -- `doctest`严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有在测试异常时,可以用`...`表示其中一大段烦人的输出。 -- 执行测试:`import doctest` `doctest.testmod()` 一般同样写在`__name__ == "__main__"`条件中,被其他模块导入时不会被执行,只有单独执行改文件才会执行。执行失败会有提示,执行成功不会有任何提示。 - - -## IO - -IO分为同步IO和异步IO,因为磁盘读写网络操作等都比CPU处理慢,所以发起一个IO操作CPU可以选择等待处理结束再继续执行,还是说直接继续执行,以其他方式处理IO(IO处理结束后回调或者CPU去轮询IO状态)。异步IO复杂高效,同步IO简单但是低效,这里仅先探讨同步IO。 - -文件读写: -- 打开文件:`f = open("Test.txt", "r")` -- 函数原型:`open(file, mode='r', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)` -- 读写模式:可以读或者写,写时可以选择覆盖还是添加到末尾,读写可以选择文本格式还是二进制格式,写时可以选择文件不存在和存在时的默认操作(创建还是报错)。 -- 具体读写选项:`rwxa` `bt` `+` 可以排列组合,根据需要添加,默认是`rt`文本格式读打开。 - -|字符|含意| -|:-:|:-| -|`'r'`|读取(默认) -|`'w'`|写入,并先截断文件 -|`'x'`|排它性创建,如果文件已存在则失败 -|`'a'`|打开文件用于写入,如果文件存在则在末尾追加 -|`'b'`|二进制模式 -|`'t'`|文本模式(默认) -|`'+'`|打开用于更新(读取与写入) - -- Python区分二进制和文本IO,二进制格式打开的内容返回`bytes`对象,不进行任何解码。文本格式打开内容返回`str`,使用指定的`encoding`(如果指定了的话)或者平台默认字节编码解码。 -- 平台无关,不依赖操作系统底层的文本文件概念,所有处理由python自身完成。 -- 更多参数细节查看[文档](https://docs.python.org/zh-cn/3/library/functions.html#open)。 -- 返回一个文件对象,类型取决于所用模式,文本二进制、是否使用缓冲都会有影响,一般文本模式读或写打开是返回的是一个`io.TextIOBase`子类(特别是`io.TextIOWrapper`)。 -- 调用`read`读取全部内容,得到的文件对象可以迭代,文本模式下迭代单位是行。 -- 使用结束后需要`close`关闭。 -- 文件读写时都有可能错误,可以使用`try ... finally`确保文件一定被关闭。 -```python -try: - f = open("Test.txt", "r") - print(f.read()) -finally: - if f: - f.close() -``` -- 为了简化,Python引入了`with`语句来自动调用`close`。 -```python -with open("Test.txt", "r") as f: - print(f.read()) -``` -- 文件很大时`read`直接读取可能并不好,可以使用`read(size)`读取指定字节内容,`readline()`每次读取一行,`readlines()`读取所有行放到列表中。 -```python -for line in f.readlines(): # iterate list of lines - print(line) -for line in f: # iterate line in file - print(line) -``` -- `open`返回的具有`read()`方法的对象成为file-like object,除了文件还可以是字节流、网络流、自定义流等,可以自定义,不要求从特定类派生,因为是鸭子类型的,只需要实现`read()`方法就行。 -- 写文件使用`write`。写时会缓冲,不会立即写到磁盘,文件关闭时才被写到磁盘。 -- python中最好使用`with`语句操作文件IO。 - -`StringIO`/`BytesIO`: -- 很多时候数据读写不一定就是文件,也可以在内存中读写,内存中读写`str`和`bytes`分别使用`io.StringIO io.BytesIO`。 -- 用法和文件流一样,创建之后就可以使用,另外可以使用`getvalue()`获取内容。 -- 除了读写,其实IO对象有一个指针指向当前的位置,使用`tell`获取,并且可以使用`seek(offset, whence)`移动(偏移可负可正表向前后移动,后一个参数表相对的位置,默认为0文件开头,1当前位置,2文件末尾),读写、读取行等操作后指针就会移动到写的内容末尾或者下一行。 -- 搞清楚当前位置,同时只用来读或写,一般不要同时读或写,会很迷惑容易出错。 - -操作系统接口: -- 使用`os`模块直接调用操作系统提供了接口。 -- `import os` -- `os.name`表明当前系统,类Unix系统是`posix`,windows系统是`nt`。 -- `os.uname()`获取系统详细信息。windows上不提供,某些函数是与操作系统相关的。 -- 环境变量: -```python -print(os.environ) # environment variables -print(os.environ.get("path")) # get specific environment variable -``` - -操作文件与目录: -- 接口一部分在`os`模块,`os.path`下。 -- `os.mkdir() os.rmdir()`新建和删除目录。 -- `os.path.abspath('.')`获取绝对路径。 -- 合并路径时,使用`os.path.join()`而不是使用字符串的`join`,这个接口会处理不同操作系统中的目录分隔符。 -```python -print(os.path.join('..', "test", "hello")) # ../test/hello in Unix-like, ..\test\hello in windows -``` -- 同理拆分路径时使用`os.path.split()`,拆成目录和文件名的元组。 -- 拆分文件扩展名`os.path.splitext()`。 -- 路径拆分不要求文件存在,仅处理路径。 -- 更多函数`os.rename() os.remove()` -- `os`中不存在复制文件的接口,在`shutil`模块中提供了`copyfile`用于复制,这个模块可以看做是`os`模块的补充。 -- 还有更多接口,可以查看标准库文档。 - -序列化: -- 序列化就是将对象写进文件,反过来的过程称为反序列化。Python中称之为Pickling和Unpickling。 -- Python的对象序列化可以使用`pickle`模块,使用`pickle.dumps(obj)`将对象转换为字节序列`bytes`,可以直接保存到文件`pickle.dump(obj, fd)`。 -- 反序列化则使用`obj = pickle.load(fd)`从文件加载,`obj = pickle.loads(s)`从`bytes`加载。 -- 使用`pickle`模块问题和其他语言特有的序列化问题一样,只能用于Python语言,不同版本可能不兼容。泛用性有限。 -- 更一般的序列化方法还是使用JSON或者XML这种结构化描述。JSON表示的对象就是标准的Javascript语言的对象。 -- JSON和python内置类型对应关系。 - -|JSON类型|Python类型| -|:-:|:-| -`{}`|`dict` -`[]`|`list` -`"string"`|`str` -`1234.56`|`int`或`float` -`true/false`|`True/False` -`null`|`None` -- Python内置的`json`模块提供了非常完善的JSON格式转换。 -- `json.dumps(obj) -> str` `json.dump(obj, fd)` -- `json.loads(json_str) -> obj` `json.load(fd) -> obj` -- 实际使用中还需要能序列化一般对象,为此对象需要是能够序列化为JSON的对象才行,为此需要实现一个将对象转换为字典的方法,作为关键字参数`default`传入`dumps dump`。 -```python -class Person: - def __init__(self, name, age): - self.name = name - self.age = age - def __str__(self) -> str: - return f"Person -> name : {self.name}, age : {self.age}" - -def person2dict(p): - return { - "name": p.name, - "age": p.age - } - -def dict2person(d): - return Person(d['name'], d['age']) - -json_str = json.dumps(Person("Kim", 18), default=person2dict) -print(json_str) -print(json.dumps(Person("Jim", 17), default=lambda x : x.__dict__)) -print(json.loads(json_str, object_hook=dict2person)) -``` -- 同理反序列化时也可以定制一个将字典转换为对象的钩子方法,作为`object_hook`关键字参数传入。 -- 通常的实例都有一个`__dict__`属性,就是一个字典,用来存储实例变量,所以序列化时可以传入`default=lambda x : x.__dict__`。也有少数例外,比如定义了`__slots__`的class。 -- 当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。 -- 比如`ensure_ascii`参数可以确保写到Json字符串中的字符是否允许非ASCII字符,默认是`True`则会将非ASCII的Unicode字符用`\uxxxx`转义。 - - -## 并发编程 - -多任务: -- 即操作系统同时运行多个任务,表现上就是并发执行的。 -- 操作系统的任务就是进程,进程内部可以有子任务,就是线程。 -- 多任务的实现方式: - - 多进程。 - - 多线程。 - - 多进程+多线程。 -- Python既支持多进程,又支持多线程。 -- 线程是最小的执行单元,进程至少由一个线程组成。进程和线程的调度由操作系统决定,程序不能决定什么时候执行,执行多长时间。 -- 多线程和多进程的程序涉及到同步、数据共享等问题,编写起来更复杂。 - -类Unix系统的多进程: -- `fork()`系统调用一次,返回两次,操作系统自动把当前进程(父进程)复制了一份(子进程),然后在父进程和子进程内分别返回。 -- 子进程永远返回0,父进程返回子进程的ID。一个父进程可以`fork`出多个子进程,所以父进程要记下子进程的ID,子进程只需要调用`getppid()`就可以拿到父进程ID。 -- Python的`os`模块封装了常见的系统调用,包括`fork`。仅在*nix上有这个接口,windows上没有。 -```python -# -*- coding: utf-8 -*- -import os -print(f"porcess {os.getpid()} start ...") - -# only works on *nix(Linux/Unix/MacOS) -pid = os.fork() -if pid == 0: - print(f"This is child process {os.getpid()}, and parent is {os.getppid()}.") -else: - print(f"This is parent process {os.getpid()}, and just created a child process {pid}") -``` - -跨平台的多线程: -- 如果是打算编写多进程的服务程序,运行在Linux平台显然是最佳的选择。 -- Python是跨平台的,所以也封装了跨平台的多线程模块`multiprocessing`。 -- 其中提供了`Process`类代表一个进程。 -```python -from multiprocessing import Process -import os - -# subprocess will execute -def run_proc(name): - print(f"Run child process {name} ({os.getpid()})...") - -if __name__ == "__main__": - print(f"Parent process {os.getpid()}.") - p = Process(target = run_proc, args = ('test', )) - print("Child process will start.") - p.start() - p.join() - print("Child process end.") -``` -- 创建子进程时,传入一个执行函数和函数参数构造新的`Process`对象,使用`start`方法启动,`join()`方法等待子进程结束后再继续往下执行,通常用于进程间同步。 - -进程池: -- 如果要创建大量子进程,可以使用进程池`multiprocessing.Pool`批量创建子进程。 -```python -from multiprocessing import Pool, Process -import os, time, random - -def long_time_task(name): - print(f"Run task {name} ({os.getpid()})...") - start = time.time() - time.sleep(random.random() * 3) - end = time.time() - print(f"Task {name} runs {end-start:0.2f} seconds.") - -if __name__ == "__main__": - print(f"Parent process {os.getpid()}.") - p = Pool(4) - for i in range(5): - p.apply_async(long_time_task, args = (i, )) - print("Waiting for all subprocess done...") - p.close() - p.join() - print("All subprocess done.") -``` -- 通过`Pool`对象创建进程池,调用`apply_async`添加子进程,调用`join()`会等待进程池内所有子进程执行完毕,之前必须调用`close()`,调用`close()`之后便不能再添加新的子进程了。 -- 某一次运行结果:子进程0,1,2,3是立即执行的,而子进程4要等待前面某个子进程执行完后才执行,这是因为`Pool(4)`指定了同时执行的子进程数量是4,因此最多同时执行4个子进程,这是刻意的设计。如果改成`Pool(5)`就能同时执行5个进程了。如果不指定的话,默认大小是CPU的核心数量(逻辑核心而非物理核心数量,比如Intel的四核八线CPU,逻辑核心数量就是8)。 -``` -Parent process 7400. -Waiting for all subprocess done... -Run task 0 (17464)... -Run task 1 (10520)... -Run task 2 (1504)... -Run task 3 (5836)... -Task 2 runs 0.20 seconds. -Run task 4 (1504)... -Task 3 runs 0.89 seconds. -Task 1 runs 1.23 seconds. -Task 0 runs 1.31 seconds. -Task 4 runs 1.58 seconds. -All subprocess done. -``` - -和外部进程通信: -- 很多时候子进程可能是外部进程,这时候如果要通信,可以使用`subprocess`模块调用外部命令。 -- 通过`Popen`调用,通过`communicate`通信,传入输入信息,得到标准输出和标准错误输出。 -```python -import subprocess - -p = subprocess.Popen(['python', '-c', 'print("hello,world!")'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -out, err = p.communicate() -print(out.decode("utf-8"), err.decode("utf-8")) -print("exit code: ", p.returncode) -``` - -进程间的通信: -- `Process`需要通信的话,python的`multiprocessing`模块封装了底层机制,提供了`Queue Pipes`多种方式用来交换数据。 -- 以`Queue`为例,父进程中创建两个子进程。一个往队列中写数据,另一个读取数据。 -```python -from multiprocessing import Process, Queue -import os, time, random - -# write to queue -def write(q): - print(f"Process to write : {os.getpid()}") - for value in ['A', 'B', 'C']: - print(f"Put {value} in queue...") - q.put(value) - time.sleep(random.random()) - -# read from queue -def read(q : Queue): - print(f"Process to read : {os.getpid()}") - while True: - value = q.get(True) # block = True - print(f"Get value {value} from queue.") - -if __name__ == "__main__": - # create queue and pass to subprocess - q = Queue() - pw = Process(target=write, args=(q,)) - pr = Process(target=read, args=(q,)) - # start subprocess to write - pw.start() - # start subprocess to read - pr.start() - # wait pw end - pw.join() - # pr is infinite loop, can not end by itself, must be terminated. - pr.terminate() -``` -- 在Unix/Linux下,`multiprocessing`模块封装了`fork()`调用,使我们不需要关注`fork()`的细节。由于Windows没有`fork`调用,因此,`multiprocessing`需要“模拟”出`fork`的效果,父进程所有Python对象都必须通过`pickle`序列化再传到子进程去,所以,如果`multiprocessing`在Windows下调用失败了,要先考虑是不是`pickle`失败了。 -- `Pipe`就是管道,每端都有`send()`和`recv()`方法,也就是发送和接收,如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。 - -**多线程**: -- 进程是由若干线程组成的,一个进程至少有一个线程。线程是操作系统直接支持的执行单元,高级语言都有内置多线程支持。Python的线程是真正的Posix Thread,而不是模拟出来的线程。 -- Python的多线程模块:`_thread`和`threading`,`_thread`是低级模块,`threading`是高级模块,对`_thread`进行了封装。绝大多数情况话都是直接使用`threading`模块。 -- 启动一个线程:创建一个`Thread`实例,传入函数,调用`start()`开始执行。 -```python -import time, threading - -def loop(): - print(f"thread {threading.current_thread().name} is running...") - for i in range(5): - print(f"thread {threading.current_thread().name} >>> {i}") - time.sleep(1) - print(f"thread {threading.current_thread().name} end.") - -if __name__ == "__main__": - print(f"thread {threading.current_thread().name} is running...") - t = threading.Thread(target=loop, name="LoopThread") - t.start() - t.join() - print(f"thread {threading.current_thread().name} end.") -``` -- 主线程名称默认是`MainThread`。 -- 多进程中,同一个变量有多份拷贝,互不影响,交互一般通过文件或者管道。而在多线程中,每个线程是一个调用栈,变量是共享的。而线程之间执行顺序不是确定的,一个线程修改变量之后会影响另一个线程的执行。所以必须对这种行为加以限制。 -- 调用`threading.active_count()`获取当前活跃的线程数量(主线程或者子线程)。 -- `Thread.setDaemon(True)`设置当前线程为主线程的守护线程,守护线程不必被全部执行完毕,当主线程执行完毕时,它的守护线程就会自动停止结束,直接退出。 - -锁: -- 为了保证线程安全,必须给多个线程使用或者修改了共享变量的代码加锁。当一个线程执行时,另一个线程不得同时进入,必须等待另一个线程执行结束之后才能进入。 -- 通过`threading.Lock()`创建互斥锁。 -- 使用`lock.acquire()`获取锁,`lock.release()`释放锁。获取锁时如果已经被其他线程获取,那么就会挂起等待其他线程执行完释放锁之后再才能获取。如果加锁的代码可能可能会抛异常,可以使用`try ... finally`确保一定能够释放锁。 -- 多个线程同时获取锁时,只要一个能获取成功。加了锁的部分在线程内可是视为原子的操作,一定能够顺序地完整执行完。 -- 获取锁之后一定要释放,否则其他需要获取该锁的线程就会苦苦等待,成为死线程。 -- 锁的的存在实际上阻止了多线程并发执行,包含锁的代码也只能同时在一个线程内执行。 -- 可以存在多个锁,当不同线程拥有不同锁,并尝试去获取对方的锁时,就会造成死锁。 -- Python中的多线程有一个全局的GIL锁(Global Interpreter Lock,官方解释器CPython的历史遗留问题),任何Python代码执行前必须先获得GIL锁。然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。所以其实Python中多线程也只能交替执行,而无法在多核CPU的多个核心上同时执行。PyPy和Jpython中是没有GIL的。 -- Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。 -- 如果一定要在Python多线程中利用多核,也可以通过C扩展实现。 -- 锁本身属于共享变量,不属于任何一个线程,这里的说的锁主要是`threading.Lock()`互斥锁,还有其他锁类型。 - -`ThreadLocal`: -- 多线程环境中,线程使用局部变量更好(这样每一个线程都会有一份),而不是使用共享的全局变量,全局变量使用必须加锁。 -- 要在一个线程内局部变量,麻烦的是,如果另一个函数需要使用该变量就必须作为参数传进去。这样多调用几层后参数会越来越多。 -- 还有一个方法就是在一个全局的字典中根据线程ID作为key,保存属于不同线程的同类型对象,线程中根据ID去获取。这样理论上虽然可行,但是太麻烦了,并且还需要注意字典的并发访问。 -- 为了应对这种问题,就出现了`ThreadLocal`对象。 -```python -import threading - -class Student(): - def __init__(self, name) -> None: - self.name = name - def __str__(self) -> str: - return f"Student {self.name}" - -# create global ThreadLocal object -local_shool = threading.local() - -def process_student(): - std = local_shool.student - print(f"hello, {std} in thread {threading.current_thread().name}") - -def process_thread(name): - local_shool.student = Student(name) # bind thread local object to atrribute of global threading.local object - process_student() - -if __name__ == "__main__": - t1 = threading.Thread(target=process_thread, args=("Alice",), name="Thread-A") - t2 = threading.Thread(target=process_thread, args=("Bob",), name="Thread-B") - t1.start() - t2.start() - t1.join() - t2.join() -``` -- 创建全局的`thread.local()`对象之后,将线程局部的变量绑定到其上,就可以在线程内部访问该线程对应的对象了。每个线程都有有该对象,但是每个线程都不一样。 -- `ThreadLocal`最常见的用法是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等。 - -进程与线程: -- 为了实现多任务,我们通常为使用Master-Worker模式,一个主进程/线程负责任务分发,多个工作进程/线程负责任务执行。 -- 多进程模式优点是稳定性高,一个子进程崩溃,不会影响主进程和其他子进程。(当然主进程崩溃其他进程也无法幸免,但是主进程只负责任务分发一般崩溃概率比较低)。 -- 多进程的缺点是创建进程的代价较大,类Unix下,`fork`还行,但Windows下,进程创建的开销很大。操作系统能运行的进程数量也是有限的。 -- 多线程模式的话,线程创建的开销会比进程小一些。多线程下任何一个子线程崩溃都可能导致整个程序崩溃,因为所有线程共享内存。 -- 无论是线程还是进程,切换都是有开销的,线程和进程多到一定程度,光是切换可能就会消耗大量CPU资源,得不偿失。 -- 考虑采用多任务处理的类型,可以将任务分为IO密集型还是计算密集型任务。计算密集型大量消耗CPU资源,对程序性能有要求,Python这种脚本语言运行效率低下,不适合用来编写计算密集型程序。IO密集型任务涉及网络文件等IO,CPU消耗少,大部分时间用于等待IO操作,对于IO密集型任务,并行的任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。对于IO密集型语言,使用Python这样的脚本语言开发比较合适,开发效率高,运行效率也不差。 -- 一个程序中如果涉及到IO密集操作,如果采用单进程/线程模型可能会导致IO时其他任务无法并行执行,需要花费大量时间等待IO的情况。为此才需要多进程和多线程模型来支持并行。 -- 现代操作系统对IO操作做了巨大改进,最大的特点就是支持异步IO,如果充分利用操作系统的异步IO,可以使用单进程/线程来执行多任务。这种全新的模型成为**事件驱动模型**。Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。 -- 在Python语言中,单线程的异步模型成为协程(很多编程语言中都有对协程的支持),有了协程的支持,就可以基于事件驱动编写高效的多任务程序。后续探讨协程(实践中真正用得多的东西)。 - -协程:TODO。 -- 对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。协程,则只使用一个线程,分解一个线程成为多个“微线程”,在一个线程中规定某个代码块的执行顺序。 -- 适用场景:当程序中存在大量不需要CPU的操作时(IO) -常用第三方模块`gevent`和`greenlet`,前者是后者的封装,常用前者。 - -分布式进程: -- 创建多任务程序时,应该首选多进程,多进程更稳定,而且多进程可以部署到不同的机器上,而多线程最多只能部署到同一机器的多个CPU上。何况CPython的多线程不能并行。 -- Python的`multiprocessing`不仅支持多线程,其中的子模块`managers`还支持把多进程分布到多台机器上,一个服务进程作为调度者,依靠网络通信将任务分布到其他多个进程。封装很好,不必了解网络通信的细节。 -- 例子:通过`Queue`通信,多进程,发送任务的进程和处理任务的进程分布到不同机器上。通过`managers`模块把`Queue`通过网络暴露出去,就可以让其他机器的进程访问`Queue`了。 -- 主进程: -```python -# -*- coding: utf-8 -*- -# TaskMaster.py - -# distributed multi process, task manager -import random, time, queue -from multiprocessing.managers import BaseManager - -# queue that send tasks -task_queue = queue.Queue() -# queue that receive tasks -result_queue = queue.Queue() - -class QueueManager(BaseManager): - pass - -def get_task_q(): - return task_queue -def get_result_q(): - return result_queue - -if __name__ == '__main__': - # register two queues to network - QueueManager.register('get_task_queue', callable=get_task_q) - QueueManager.register('get_result_queue', callable=get_result_q) - # bind to port 5000, authentication code abc - manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc') - - # start the manager - manager.start() - # get Queue object through network - task = manager.get_task_queue() - result = manager.get_result_queue() - - # put some tasks to task queue - for i in range(10): - n = random.randint(0, 10000) - print(f"Put task {n}") - task.put(n) - - # read result from result queue - print("Try get results...") - for i in range(10): - try: - r = result.get(timeout=10) - print(f"Result : {r}") - except queue.Empty: - print("The queue is empty...") - - # shudown manager - manager.shutdown() - print("Master exit.") -``` -- 工作进程; -```python -# -*- coding: utf-8 -*- -# TaskWorker.py - -# distributed multi process, task wroker -import time, sys, queue -from multiprocessing.managers import BaseManager - -class QueueManager(BaseManager): - pass - -if __name__ == '__main__': - QueueManager.register('get_task_queue') - QueueManager.register('get_result_queue') - server_addr = "127.0.0.1" - print(f"Connect to server {server_addr}...") - m = QueueManager(address=(server_addr, 5000), authkey=b'abc') - # connect to server - m.connect() - # get Queue from network - task = m.get_task_queue() - result = m.get_result_queue() - # get task from task queue, calculate and put result to result queue - for i in range(10): - try: - n = task.get(timeout=1) - print(f"Run task {n} * {n}...") - r = f"{n} * {n} = {n * n}" - time.sleep(1) - result.put(r) - except queue.Empty: - print("task queue is empty.") - - # end wrok process - print("Worker exit.") - -``` -- 加了多余的循环保证5次任务能执行完,执行时在两个终端中分别依次执行`python TaskMaster.py`和`python TaskWorker.py`。将会通过本机的5000端口进行网路交互,完成任务分发、执行和结果获取。修改地址为局域网IP后还可以在虚拟机/WSL和本机中执行。 -- 没有对网络连接是否成功做检查,需要在主线程连接还存在时执行工作线程。 -- `QueueManager`给任务和结果队列都注册了网络调用接口,在工作进程中调用就能够获取到了。 -- `authkey`是校验码,保证两台机器通信不受干扰。如果不一致,网络连接会失败。 -- 总结: - - Python的分布式进程接口简单、封装良好,适合把繁重任务分布到多台机器。 - - 注意`Queue`的作用是用来传递和接收结果,每个任务的描述应该尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。 - -阅读: -- [搞定python多线程和多进程](https://www.cnblogs.com/whatisfantasy/p/6440585.html) - -## 正则表达式 - -正则表达式: -- 正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。 -- 判断一个字符串是否是合法的正则的方法,创建一个正则表达式,用该正则表达式去匹配用户输入。 - -规则: -- 正则表达式也是用字符串来表示的,表示规则: -- 直接给出字符则精确匹配字符。 -- `\d`匹配数字。 -- `\w`匹配数字或者字母。 -- `\b`匹配单词的开始或者结束。 -- `\s`匹配空白符,Tab空格等。 -- 上面几个的大写版本表反义:`\D \W \B \S`。 -- `.`匹配除换行符以外的任意字符。 -- 变长字符:跟在一个规则后。 - - `*`匹配任意字符,包括0个。 - - `+`匹配至少一个字符。 - - `?`匹配0个或1个字符。 - - `{m}`匹配m个字符。 - - `{n,}`匹配n个或更多个字符。 - - `{n,m}`匹配n到m个(包括m)字符。 -- 特殊字符`.*+?,;-\_{}()|^$`需要使用`\`转义。 -- 表示范围:`[]`,其中可以使用`-`前后跟数字或字母,表示某个字符到某个字符。 - - 如`[a-zA-Z0-9_]`匹配数字字母和下划线。`[a-z0-9A-Z_]`等价于`\w` - - 匹配一个范围中的某字符时前面加`^`表反义:`[^a-z]`匹配所有非小写字母的字符。 -- 匹配两者中一者`|`,`A|B`。 -- `^`表行开头,`^\d`表示以数字开头。 -- `$`表行结束,`\d$`表示以数字结束。匹配一整行可以用`^`开头,`$`结尾。 - -Python中的正则表示式: -- Python字符串中本身存在转义,而正则字符串某些字符也存在转义,为了避免混淆,实践中建议全部使用`r`前缀原始字符串表示正则字符串。如此便只需要考虑`'"`的转义了。 -- `import re` -- `re.match(pattern, string)`匹配成功则返回一个`Match`对象,失败返回`None`。 -- `re.split()`可以切分字符串, -- 除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用`()`表示的就是要提取的分组(Group)。用`re.Match.group(index)`方法。`group(0)`永远是原始字符串,`group(1)`、`group(2)`、...表示第1、2、...个子串。 -- 正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。为了采用非贪婪匹配,可以在表数量的字符后缀后再加一个`?`。 - -编译正则表达式: -- 在Python中使用正则表达式时,内部会先编译正则表达式,如果正则表达式本身不合法会报错,然后再用编译后的正则表达式去匹配字符串。 -- 如果要匹配一个正则表达式多次,可以编译后再去匹配,提升执行效率。 -- 使用`re.compile(pattern)`编译一个正则表达式,会得到一个`re.Pattern`对象,使用这个对象去匹配字符串不需要再传入正则表达式。 - -更多正则表达式内容: -- 捕获、零宽断言、负向零宽断言、注释、平衡组、递归匹配等有机会接触到再详细了解。 - -## 常用内建模块 - -`datetime` -- `from datetime import datetime`,前者是模块,后者是类表示日期和时间。 -- 当前时区时间`datetime.now()`,标准时区时间`datetime.utcnow()`。 -- 构造指定日期时间:`datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]])` -- 时间戳:`datetimeInstance.timestamp()`,从UTC+00:00时区的1970年1月1日00:00:00时刻(Epoch Time)到现在的时刻的秒数成为时间戳,单位是秒,浮点数,精确到微妙(小数点后6位)。时间戳和时区、闰年闰秒等无关。 -- 时间戳转`datetime`:`datetime.fromtimestamp(stamp)`,结果是本地时区。 -- 时间戳转UTC标准时区(UTC+00:00)时间:`datetime.utcfromtimestamp(stamp)`。 -- 字符串转`datetime`:`datetime.strptime("2021-1-1 10:00:01", "%Y-%m-%d %H:%M:%S")`,时间字符串和格式化字符串格式要吻合,详见[文档](https://docs.python.org/zh-cn/3/library/datetime.html#strftime-strptime-behavior)。得到的时间没有时区信息。 -- `datetime`转字符串:`datetime.now().strftime('%a, %b %d %H:%M')`。格式化字符串同上见文档。 -- 表时间间隔的类:`timedelta`,支持加减、正负号绝对值、和`timedelta`比较,与整数做乘除法。构造:`timdelta(days, seconds, microseconds)`。两个`datetime`相减会得到一个`timedelta`。 -- 时区信息:`timezone`类。 - - 创建UTC+8:00时区:`timezone(timedelta(hours=8))` - - `datetime`有`tzinfo`属性,默认是`None`时表示当前时区。 - - 强制替换时区:`datetimeinstance.replace(tzinfo=timezone(timedelta(hours=7)))`,时间日期不会变,只是强制改变时区。 -- 时区转换: - - `astimezone(self, tz=None)`方法切换到某一时区,默认当前时区。 - - 如果以前没有时区信息`None`,那么就是从当前时区转到特定时区。 - - 关键在于要知道拿到一个`datetime`时它的时区,一般比如`datetime.now()`是不包含时区信息的,可以通过`tzinfo`属性设置或者先调用一次加上时区信息。 - -`collections` -- 命名元组:`namedtuple`函数,用来创建一个命名了的元组,元素个数固定,并可以指定属性名称,除了下标还可以通过属性名称来访问元素。`Point = namedtuple('Point', ['x', 'y'])`。 -- 双向列表:`deque`,使用`list`插入删除效率不高,`deque`可以用作队列和栈,提供高效头尾插入和删除元素。方法`append pop appendleft popleft`。 -- `defaultdict`:使用`dict`时,如果key不存在,会抛出`KeyError`,如果希望key不存在时返回一个默认值,可以用`defaultdict`。 -- `OrderedDict`:有序字典,元素会按照key插入顺序排列。 -- `ChainMap`:将多个`dict`有序地组合起来,构成一个逻辑上的字典,查找时会按照顺序依次查找每一个字典,和多个字典取并集有区别。可以实现多个层次优先级查找,比如应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap实现参数的优先级查找。 -- `Counter`:计数器,`dict`子类,value类型是整数,用来计数。 -- Python命名感觉有点混乱,有时大驼峰有时全小写。 - -`base64` -- Base64编码是一种用64个字符来编码任意二进制数据的方法。 -- 众所周知文本编辑器无法处理二进制文件,因为编码不一致,用ASCII的话可能存在非ASCII的字符,其他编码字符编码也不一定符合。为了能用文本字符串来表示二进制数据,就可以将二进制数据用Base64进行编码。 -- 原理:准备64个字符的数组,将二进制数据按照每6位编码为一个8位的字符(这6位的值作为数组下标的数组元素),每3个字节就会编码为4个字节的字符串,体积膨胀33%。如果二进制数据长度不是3个倍数,用`\x00`字节在二进制数据末尾补足,再在编码末尾添加`=`或`==`表示补了多少字节。 -- 这个字符数组是:`['A', 'B', ..., 'Z', 'a', 'b', ..., 'z', '0', ..., '9', '+', '/']` 字母数字加上加减号共64个字符。 -- 编码解码都非常简单,编码就查表替换就行,解码则反推索引,恢复二进制数据即可。 -- Python内置`base64`模块提供Base64编解码功能。 -- 编码:`base64.b64encode(b'helloworld')` -- 解码:`base64.b64decode(b'aGVsbG93b3JsZA==')` -- 编解码是可以提供一个长度为2的字节序列关键字参数`altchars`用来替换`+/`以获得合法的url或者文件系统路径字符串。 -- 内置的用`-`和`_`来替换`+/`的编解码方法:`urlsafe_b64encode urlsafe_b64decode`。 -- 还可以自定义64个字符的排列顺序,这样就可以自定义Base64编码,不过通常情况下完全没有必要。 -- Base64可以用以编解码,不能用于加密,即使使用自定义编码表(太过原始,不安全)。 -- Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。 -- 但`=`用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把`=`去掉。因为Base64是把3字节变4字节,所以解码前只需要在Base64字符串后加上`=`使长度变成4个整数倍即可。 - -`struct` -- `struct`模块提供`bytes`和其他二进制数据类型比如整数浮点数之类的转换。 -- `import struct`。 -- `strcut.pack(format, *args)`将多个数据打包成一个二进制序列,格式字符见[文档](https://docs.python.org/zh-cn/3/library/struct.html#format-characters),比如`>I`就表示用大端序编码一个4字节无符号整数。 -- Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,可以使用`struct`。 -- 解析二进制序列:`struct.unpack(format, bytes)`,得到元组。 -- 使用场景:一些特定的二进制文件格式(比如图片BMP、JPG等)都会有特定结构,此时读入二进制流,然后使用`struct`解析或者打包文件头就可以很方便。 - -`hashlib` -- `hashlib`提供了常见的摘要算法,比如MD5,SHA1等。 -- 摘要算法的目的主要是通过摘要函数对任意长度的数据计算出固定长度的摘要`digest`,可以用以确认原始数据是否被人篡改过。 -```python -import hashlib -md5 = hashlib.md5() -md5.update('how to use md5 in '.encode('utf-8')) -md5.update('python hashlib?'.encode('utf-8')) -print(md5.hexdigest()) -``` -- 其他比如`sha1 sha256 sha512`用法类似,对较长数据做哈希时可以分多次传入`update`,和一次传入计算结果一致。 -- MD5生成128位的字节,通常用长度32的16进制字符串表示。SHA1生成160位的字节,通常用长度40的字符串表示。比SHA1更安全的算法是SHA256和SHA512,分别生成256和512位,摘要长度越长越安全,计算起来越慢。 -- 通常在很多网站下载文件时都会给一个SHA256检验码,可以拿到文件后计算文件的SHA256是否吻合以确保文件在网络上传输过程中没有被篡改。 -- 应用:在保存用户密码时保存经过摘要之后的哈希,而不是原始的密码字符串,用户输入密码后计算出哈希然后对比哈希,这样即使运维人员能访问数据库、数据库被黑客攻击窃取,也无法知道用户输入的密码明文,从而防止撞库等攻击手段。当然这一定程度上也是取决于摘要算法本身是否能够被破解的,这就是另一个问题了。摘要并不一定就是唯一的,做哈希那很显然对不同数据做哈希得到的哈希值可能是相同的,只是在实践中这种情况发生概率是非常非常小的。 -- 采用MD5等哈希存储密码是否一定安全呢?也不一定,因为用户极有可能使用很简单的密码比如123456,qwerty等这种常用密码,从哈希值反推密文是非常费劲的,但黑客可以维护一个常见密码到哈希值的数据库(彩虹表),如果用户密码很简单在库中,那么就可以通过哈希反推出密码。所以作为用户来说,为了防止这种攻击,一般不要使用太简单的密码。 -- 为了保护较为简单的密码被反推,也可以对原始密文字符串加上一个复杂字符串之后再做哈希,俗称“加盐”。经过加盐之后,只要Salt不被黑客知道,就无法推出原始密文。甚至计算哈希时将密码加上用户名和盐一起计算。 -- 哈希算法无法用于加密,因为信息是有损的且无法反推明文,只能用于防篡改。它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。 - - -`hmac` -- 上述计算哈希时如果Salt是我们随机生成的,那么计算MD5通常采用`md5(message+salt)`,如果把盐看做口令,计算消息的哈希时需要提供这个口令,验证时也必须要提供正确的口令。 -- 这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。不同于我们自己计算MD5加盐,Hmac算法对所有哈希算法都适用,无论MD5还是SHA1,采用Hmac算法代替自己编写加盐代码,可以使程序更加标准化也更安全。 -- Python的`hmac`模块可以做这件事情。 -```python -import hmac -message = b"hello,world!" -key = b"sercet" -h = hmac.new(key, message, digestmod="MD5") -print(h.hexdigest()) - -h = hmac.new(key, message, digestmod="SHA1") # SHA256, SHA512, ... -print(h.hexdigest()) -``` -- 需要注意的是,如果黑客知道了用户的盐,那么还是可以通过已知密码列表和这个盐算出一个库,最后和哈希值对比,如果有那么就破解成功了。实践中一般会给每个用户生成一个随机的盐,保存在服务端,这样黑客就无法通过每次计算一个盐得到一个库来尝试撞出所有用户的密码,而是要对每一个盐都对密码表中所有密码生成一个库,极大地增大了黑客的计算成本,使拿到数据库的黑客批量计算出密码这件事情变得几乎不可行了。但黑客还是可以针对一个特定的用户去尝试,这时候还是需要用户设置更加复杂的密码,以及不在不同网站使用同样的密码才可以很好地避免。一般人其实也不具备被黑客攻击的价值,但还是要有最基本的安全意识。 -- 摘要算法的输入是字节序列。 - -`itertools` -- 其中提供了一些用于迭代的有用的函数。 -- 比如Map/Reduce相关操作的补充。 -- 几个无限迭代器: - - `count(start, step = 1)`从n开始的无限迭代器。 - - `cycle(iterator)`循环迭代一个迭代器,结束后又从头开始。 - - `repeat(elem, times)`迭代一个元素指定次数,不传次数则是无数次。 - - 迭代时才回去访问元素,不会事先创建无限的元素。 -- 串联多个迭代器,形成一个更大的迭代器:`chain(*iters)`。 -- 把迭代器中相邻的重复元素挑出来放在一起:`groupby(iterable, grouprule)`,挑选规则为传入的函数,只要作用于两个元素返回的值相等就被放到一组。 -- 还有很多有用的函数。 - -`contextlib` -- Python中读写文件这种资源处理需要特别注意,需要确保关闭,一个方法是使用`try...finally`,不过很繁琐,更常见的是使用`with`语句。 -- 除了`open()`函数打开文件对象,其实对于任何对象,只要正确实现了上下文管理,都能用于`with`语句。 -- 实现上下文管理是通过`__enter__`和`__exit__`这两个方法实现的。 -```python -class Query(object): - def __init__(self, name) -> None: - self.name = name - def __enter__(self): - print('Begin') - return self - def __exit__(self, exc_type, exc_value, traceback): - if exc_type: - print('Error') - else: - print('End') - def query(self): - print(f"Query info about {self.name}") - -with Query('Bob') as q: - q.query() -``` -- 因为是鸭子类型的,所以不需要继承什么类,只需要实现这两个方法,就可以在`with`语句中使用。 -- 使用`__enter__ __exit__`依然比较繁琐,Python标准库`contextlib`提供了更简单的写法。上面的代码可以改写为: -```python -from contextlib import contextmanager - -class Query2(object): - def __init__(self, name): - self.name = name - def query(self): - print(f"Query info about {self.name}") - -@contextmanager -def create_query(name): - print("Begin") - q = Query2(name) - yield q - print('End') - -with create_query('Bob') as q: - q.query() -``` -- 使用`@contextmanager`装饰器,定义函数,执行要执行的操作,将要放在`with`语句中的对象`yield`出去,将进入和离开释放资源的逻辑写在其中即可。 -- 执行顺序:`yield`前的语句,`with`块中语句,然后是`yield`后的语句。 -- 很多时候,希望在某段代码执行前后自动执行特定代码,也可以用`@contextmanager`实现。 -- 比如输出xml时输出内容后自动输出元素的结束标记。 -```python -@contextmanager -def tag(name): - print(f"<{name}>") - yield - print(f"") - -with tag("h1"): - print("hello") -``` -- 如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用`closing()`来把该对象变为上下文对象。 -- 效果类似于:结束后自动调用`close`方法。 -```python -@contextmanager -def closing(thing): - try: - yield thing - finally: - thing.close() -``` -- 这个库中还有许多用于上下文管理的装饰器,见[文档](https://docs.python.org/zh-cn/3/library/contextlib.html)。 - -`urllib` -- 提供了一系列操作URL的功能,`urllib`模块的`request`模块可以方便的抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应。 -```python -from urllib import request - -with request.urlopen('https://baidu.com') as f: - data = f.read() - print('Status:', f.status, f.reason) - for k, v in f.getheaders(): - print('%s: %s' % (k, v)) - print('Data:', data.decode('utf-8')) -``` -- 如果要模拟浏览器发送GET请求,就需要使用`Request`对象,添加HTTP头,就可以把请求伪装成浏览器: -```python -req = request.Request('http://www.douban.com/') -req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25') -with request.urlopen(req) as f: - print('Status:', f.status, f.reason) - for k, v in f.getheaders(): - print('%s: %s' % (k, v)) - print('Data:', f.read().decode('utf-8')) -``` -- 如果要发送POST请求,就需要把参数`data`以bytes形式传入 -- 还有更复杂的控制,比如通过一个代理去访问网站,可以利用`ProxyHandler`来处理。 -- `urlopen`得到的对象是一个file-like对象,可以像文件一样通过`read`读取。 - -`XML` -- XML比JSON更复杂,不过依然还有许多地方在使用。 -- XML有两种操作方法:DOM(Document Object Model)和SAX(Simple API for XML)。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。 -- 正常情况下优先考虑SAX,因为DOM太占用内存。 -- Python使用SAX,通常我们关心的事件是`start_element`,`end_element`和`char_data`,准备好这3个函数,然后就可以解析xml了。 -```python -from xml.parsers.expat import ParserCreate - -class DefaultSaxHandler(object): - def start_element(self, name, attrs): - print('sax:start_element: %s, attrs: %s' % (name, str(attrs))) - - def end_element(self, name): - print('sax:end_element: %s' % name) - - def char_data(self, text): - print('sax:char_data: %s' % text) - -xml = r''' -
    -
  1. Python
  2. -
  3. Ruby
  4. -
-''' - -handler = DefaultSaxHandler() -parser = ParserCreate() -parser.StartElementHandler = handler.start_element -parser.EndElementHandler = handler.end_element -parser.CharacterDataHandler = handler.char_data -parser.Parse(xml) -``` -- 除了解析字符串之外,生成字符串可以直接使用简单的字符串拼接完成。不太建议生成大段的XML,最好使用JSON。 - - -`HTMLParser` -- 如果要实现一个浏览器,假设html页面已经已经爬取到了,下一步就是解析HTML。 -- HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。 -- Python提供了`HTMLParser`来解析HTML,只需要简单几行代码。 -```python -from html.parser import HTMLParser -from html.entities import name2codepoint - -class MyHTMLParser(HTMLParser): - - def handle_starttag(self, tag, attrs): - print('<%s>' % tag) - - def handle_endtag(self, tag): - print('' % tag) - - def handle_startendtag(self, tag, attrs): - print('<%s/>' % tag) - - def handle_data(self, data): - print(data) - - def handle_comment(self, data): - print('') - - def handle_entityref(self, name): - print('&%s;' % name) - - def handle_charref(self, name): - print('&#%s;' % name) - -parser = MyHTMLParser() -parser.feed(''' - - - -

Some html HTML tutorial...
END

-''') -``` -- 基本逻辑是从`HTMLParser`派生,实现对应方法即可解析。 - -## 常用第三方模块 - -除了内建模块,Python还有数量众多的第三方模块。所有的第三方模块都会在[PyPI - the Python Package Index](https://pypi.python.org/)上注册,只要找到对应的模块名字,即可用pip安装。 - -`Pillow` -- PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。但PIL仅支持到Python2.7,后续由一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性。 -- 安装:`pip install pillow`。 -- 下面的代码可以完成缩放和模糊的操作: -```python -from PIL import Image, ImageFilter - -# open an image -im = Image.open('nephren.png') -# get image size -w, h = im.size -print('Original image size: %sx%s' % (w, h)) -# resize to 50% -im.thumbnail((w//2, h//2)) -print('Resize image to: %sx%s' % (w//2, h//2)) -# save scaled image -im.save('thumbnail.png', 'png') - -# blur an iamge -im = Image.open('nephren.png') -im2 = im.filter(ImageFilter.BLUR) -im2.save('blur.png', 'png') -``` -- `PIL`的`ImageDraw`提供了一系列绘图方法让我们可以直接绘图。 -- 更多用法详见[文档](https://pillow.readthedocs.io/en/stable/) - -`requests` -- Python内置的`urllib`可以用于访问网络资源,但是用起来很麻烦。而且缺少高级一点的功能。 -- 更好的方案是使用`requests`第三方库,处理URL资源特别方便。 -- 安装`pip install requests` -- 发送GET请求: -```python -import requests -r = requests.get('https://baidu.com') -print(r.status_code) -print(r.text) -``` -- 对于带参数的URL,传入一个`dict`作为`params`参数。 -- `requests`会自动检测编码,使用`encoding`属性查看。 -- 无论响应时文本还是二进制内容,都可以使用`content`属性获取`bytes`对象。 -- 对于特定类型响应,比如JSON可以直接通过`json()`方法获取到json对象。 -- 需要传入HTTP头时,可以通过`headers`参数传入。 -- 发送POST请求,只需要把`get()`方法保存`post()`,传入`data`参数作为请求数据即可。 -- requests默认使用`application/x-www-form-urlencoded`对POST数据编码。如果要传递JSON数据,可以直接传入json参数。 -- 上传文件需要更复杂的编码格式,但是requests把它简化成files参数。读取文件时,注意务必使用'rb'即二进制模式读取,这样获取的bytes长度才是文件的长度。 -- 同样还可以`put() delete()`方法请求资源。 -- 除了获取响应内容,获取响应的其他信息也很轻松,比如响应头`r.headers['Content-Type']`。 -- requests对Cookie做了特殊处理,使得我们不必解析Cookie就可以轻松获取指定的Cookie。`r.cookies['ts']`。 -- 要在请求中传入Cookie,只需准备一个dict传入`cookies`参数。 -- 要指定超时,传入以秒为单位的`timeout`参数。 -- 更多内容详见[文档](https://docs.python-requests.org/zh_CN/latest/)。 - -`chardet` -- 作用:检测编码。 -- 安装:`pip install chardet`。 -- 拿到一个`bytes`检测它的编码:`chardet.detect(b'Hello, world!')`。 -- 支持编码见[文档](https://chardet.readthedocs.io/en/latest/supported-encodings.html)。 - -`psutil` -- 在Linux下,有许多系统命令可以让我们时刻监控系统运行的状态,如ps,top,free等等。要获取这些系统信息,Python可以通过subprocess模块调用并获取结果。但这样做显得很麻烦,尤其是要写很多解析代码。 -- 在Python中获取系统信息的另一个好办法是使用psutil这个第三方模块。顾名思义,psutil = process and system utilities,它不仅可以通过一两行代码实现系统监控,还可以跨平台使用,支持Linux/UNIX/OSX/Windows等,是系统管理员和运维小伙伴不可或缺的必备模块。 -- 使用`psutil`可以获取CPU信息、内存信息、磁盘信息、进程信息、用户信息、Windows服务等诸多有用的系统信息。 -- [文档](https://psutil.readthedocs.io/en/latest/)。 -- 示例: -```python -import psutil - -# CPU info -print(psutil.cpu_count()) # logical cpu count -print(psutil.cpu_count(logical=False)) # physical cpy count -print(psutil.cpu_times()) - -# print the usage of every cpu core, 5 times in one second -for x in range(5): - print(psutil.cpu_percent(interval=0.2, percpu=True)) - -# memory and swap memory info -print(psutil.virtual_memory()) -print(psutil.swap_memory()) - -# internet -print(psutil.net_io_counters()) -print(psutil.net_if_addrs()) # port info -print(psutil.net_if_stats()) # port status -print(psutil.net_connections()) - -# process -print(psutil.pids()) -p = psutil.Process(psutil.pids()[-1]) -print(p.exe()) # executable of process -print(p.cwd()) # working directory of process -print(p.cmdline()) # cmd line of process -print(p.ppid()) # parent process id -print(p.parent()) # parent process -print(p.children()) # children processes -print(p.status()) # status -print(p.username()) -print(p.create_time()) -# print(p.terminal()) # Unix only -print(p.cpu_times()) -print(p.memory_info()) -print(p.connections()) # internet connections -print(p.num_threads()) -print(p.threads()) -print(p.environ()) # environment variables of process - -# like ps command -print(psutil.test()) -``` - -## virtualenv & pipenv - -virtualenv可以用来在一台机器上创建多个隔离的Python运行环境,比如一个应用需要某个包的一个特定版本,而另一个应用需要另一个版本,而这两个包可能又依赖另一个包的不同版本,将这两个版本放到同一个环境中势必会造成冲突,那么此时就可以使用virtualenv。 -- [官方文档](https://virtualenv.pypa.io/en/latest/)。 -- 安装:`pip install virtualenv` -- 使用:`python -m virtualenv [options] [args]` -- 创建一个新环境:在一个目录中`python -m virtualenv venv`,`venv`就是这个新环境的名称,并且会在目录中创建一个`venv/`目录,其中存放了Python可执行文件以及`pip`库的一份拷贝。省略名字将会把文件直接放在当前目录。 -- 使用虚拟环境前,需要先激活: - - Unix中:`source venv/bin/activate` - - Windows中执行:`.\venv\Scripts\activate.bat` - - 激活成功后命令行提示符前会出现`(venv)`,即表示进入虚拟环境。 - - 直接执行`deactivate`可以停用虚拟环境(可以不用显式指明脚本路径),在虚拟环境中暂时完成了工作后离开时就可以停用它,这是会回到系统默认的Python解释器和安装的库。 -- 删除一个虚拟环境,只需要删除其目录。 -- 记得将虚拟环境的目录添加到版本控制的忽略文件中。 -- 在虚拟环境中安装第三方库将会保留在这个环境中,不会和系统默认环境发生冲突。 -- 运行原理:在执行了`activate`后,会修改相关环境变量,让Python和pip指向当前虚拟环境。 - -另一种管理虚拟环境的工具Pipenv: -- 结合了`pip`和`virtualenv`,侧重点是包环境管理。 -- 安装:`pip install pipenv` -- Pipenv 管理每个项目的依赖关系。要安装软件包时,更改到项目目录,为项目安装一个包:`pipenv install package`。不加某一个具体的包的话就是安装`Pipfile`中所有包。 -- 卸载:`pipenv uninstall package` -- 使用`pipenv`后会生成一个`Pipfile`,其中有最新安装的包文件的信息,如名称、版本等,用来在重新安装项目依赖或与他人共享项目时,你可以用 `Pipfile` 来跟踪项目依赖,这个文件就是`pipenv`用来替代`pip`的`requirements.txt`的文件。还会有一个`Pipfile.lock`包含你的系统信息,所有已安装包的依赖包及其版本信息,以及所有安装包及其依赖包的 Hash 校验信息。 -- 使用时可以通过`pipenv run python main.py`可以确保你的安装包可以用于你的脚本,就是说只会使用`Pipfile`中的依赖,如果没有在目录中用`pipenv install`安装的包将无法使用。 -- 还可以使用`pipenv shell`来生成一个新的shell,就像进入虚拟环境那样,就不用执行前都加一个`pipenv run`了。 -- 使用`pipenv run pip list`将会得到使用`pipenv run`执行时可用的包列表。 -- 其实`pipenv`也类似于`virtualenv`,只不过虚拟环境的文件不在当前目录下,而是在家目录下的`./virtualenvs`下的目录中。`pipenv --venv`可以查看其虚拟环境所在目录。 -- 更多命令: - - `pipenv update packge`更新第三方包。 - - `pipenv --where` 查看项目根目录。 - - `pipenv check` 检查第三方包的完整性。 - - `pipenv graph` 查看依赖树。 -- `pipenv`换源: - - 新建系统变量`PIPENV_PYPI_MIRROR`为`https://pypi.tuna.tsinghua.edu.cn/simple`(或其他源)。对所有`pipenv`环境生效。 - - 修改`Pipfile`中的`url`可以更改这个项目安装时的源。 - - -安装与生成依赖: -- 如果你的程序和开发环境高度相关,就需要生成依赖文件`requirements.txt`。 -- 使用`pip freeze`可以得到当前环境所有的包,直接执行会得到当前安装的所有包,如果`virtualenv`或者`pipenv run`下执行,那么只会得到虚拟环境中可用的包。 -- 使用`pip freeze > requirements.txt`即可生成依赖文件。 -- 重新创建这样的环境:`pip install -r requirements.txt`。帮助确保安装、部署和开发者之间的一致性。 -- 如果没有使用虚拟环境,所有包都使用系统的Python包,那么`pip freeze`就会得到所有包,当发布项目时仅需要项目的依赖,可以使用包`pipreqs`来查找当前项目的依赖并自动生成`requirements.txt`。 - - `pip install pipreqs` - - `pipreqs ./` - -另外还有`pyenv`可以用来管理多个版本的Python,这点Pipenv也可以做到,此处不详述`pyenv`。 - -扩展阅读: -- [Pipenv & 虚拟环境](https://pythonguidecn.readthedocs.io/zh/latest/dev/virtualenvs.html),更多关于项目依赖于虚拟环境的说明。 - -## 图形界面 - -Python支持多种图形界面的第三方库:Tk、wxWidgets、Qt、GTK。 - -Tkinter: -- Python自带的库是支持Tk的Tkinter,使用Tkinter,无需安装任何包,就可以直接使用。 -- 第一个Tkinter的GUI程序: -```python -from tkinter import * - -class Application(Frame): - def __init__(self, master = None): - Frame.__init__(self, master) - self.pack() - self.createWidgets() - def createWidgets(self): - self.helloLabel = Label(self, text='Hello, world!') - self.helloLabel.pack() - self.quitButton = Button(self, text='Quit', command=self.quit) - self.quitButton.pack() - -app = Application() -app.master.title('hello,world') -app.mainloop() -``` -- 和其他语言的GUI程序差不多,派生Frame,其中创建各种Widget,实例化后启动消息循环。 -- GUI程序的主线程负责监听来自操作系统的消息,并依次处理每一条消息。如果消息处理非常耗时,就需要在新线程中处理。 -- ython内置的Tkinter可以满足基本的GUI程序的要求,如果是非常复杂的GUI程序,建议用操作系统原生支持的语言和库来编写。 - -海龟绘图`turtle`库: -- 简单来说就是指挥一个海龟前进转向以此来绘图的API,移植到Python上之后就是这个库,作用有限,可以用来体验GUI的乐趣。内置不需要安装。 -- [文档](https://docs.python.org/3.3/library/turtle.html)。 - -## 网路编程 - -TCP/IP协议就不多介绍了,IPv4就是一个32位整数,一般用4个0-255的十进制用`.`分隔来表示。IPv6是128位整数,用8个4位十六进制整数`:`分隔表示。 -- TCP是可靠传输,会进行三次握手,四次挥手,UDP是不可靠传输。其他应用层的协议建立在TCP协议之上,比如浏览器的HTTP协议、邮件协议SMTP。 -- TCP协议使用一个一个的数据包传输数据,一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。 -- 端口的作用是在机器上区分应用,Ip则用来区分机器,一个IP:端口的组合被称为一个套接字,用来唯一标识一个连接。 - -TCP编程: -- Socket是网络编程的一个抽象概念,用一个Socket表示打开了一个网络链接,打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。 -- 大多数连接都是可靠的TCP连接,创建TCP连接时,发起连接的叫**客户端**,被动响应连接的叫**服务器**。 - -客户端: -- 创建一个基于TCP的连接: -```python -import socket - -# create a socket: AF_INET -> ipv4, SOCK_STREAM -> TCP -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect(('www.baidu.com', 80)) -``` -- 作为服务器,提供什么服务,使用什么端口号必须固定下来,80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。 -- 建立连接之后,可以发送请求: -```python -s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n') -``` -- TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。 -- 接下里就可以接收数据了: -```python -# receive data -buffer = [] -while True: - # 1 KB every time - d = s.recv(1024) - if d: - buffer.append(d) - else: - break -data = b''.join(buffer) -``` -- 数据接收完毕之后,调用`close`方法关闭Socket,一次完整的网络通信就结束了。 -```python -s.close() -``` -- 接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件,在浏览器中打开这个`html`文件就可以看到百度的首页了。 -```python -header, html = data.split(b'\r\n\r\n') -print(header.decode('utf-8')) -with open('baidu.html', 'wb') as f: - f.write(html) -``` - -服务器端: -- 服务器编程比客户端要复杂一点。 -- 服务器进程需要绑定一个端口来监听其他客户端的连接,如果某个客户端连接过来,服务器就与该客户端建立Socket连接,随后的通信就依靠这个Socket连接。 -- 服务器可能会有大量客户端连接,由服务器地址、服务器端口、客户端地址、客户端端口唯一确定一个Socket。 -- 每个连接创建一个新线程进行处理。 -```python -if __name__ == "__main__": - # create a socket: Ipv4, TCP - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # bind a port - s.bind(('127.0.0.1', 9999)) - - # listen a port, argument is max connection count - s.listen(5) - print("waiting for connection...") - # accept connection from client - while True: - # accept a new conection - sock, addr = s.accept() - # create a new thread to handle TCP connection - t = threading.Thread(target=tcplink, args=(sock, addr)) - t.start() -``` -- 处理逻辑:首先发送欢迎消息,然后接受客户端消息,如果是`exit`字符串就关闭连接,否则就发送消息到客户端。 -```python -def tcplink(sock, addr): - print('Accept new connection from %s:%s...' % addr) - sock.send(b"Welcome!") - while True: - data = sock.recv(1024) - time.sleep(1) - if not data or data.decode('utf-8') == 'exit': - break - sock.send((f"hello {data.decode('utf-8')}").encode('utf-8')) - sock.close() - print("Connection from %s:%s closed." % addr) -``` -- 在客户端,同样处理: -```python -# create a socket -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect(('127.0.0.1', 9999)) - -# send requests -print(s.recv(1024).decode('utf-8')) -for data in [b"Alice", b"Bob", b"Mary"]: - s.send(data) - print(s.recv(1024).decode('utf-8')) -s.send(b'exit') -s.close() -``` -- 执行结果:在服务器端先执行,会等待客户端来连接,执行客户端代码后连接成功服务端新建线程处理,客户端收到欢迎消息,客户端依次发送并接受消息,服务端依次接受并发送消息,直到收到`exit`关闭连接。服务端处理线程结束,主线程依然处于等待连接状态。 - -UDP编程: -- UDP是不可靠传输,不需要建立连接,只需要直到对方的IP地址和端口号,就可以直接发送数据包。但是能不能到达是不知道的。 -- 虽然传输不可靠,但优点是相比TCP更快。 -- 服务端:不需要监听,发送之前也不需要连接,这里比较简单,也不用多线程处理。 -```python -import socket - -# create a socket: IPv4, UDP -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -# bind to port -s.bind(('127.0.0.1', 9999)) -print("Bind UDP on 9999...") - -# do not need listen, just receive -while True: - data, addr = s.recvfrom(1024) - print("Received from %s:%s" % addr) - s.sendto(b'hello, %s' % data, addr) -``` -- 客户端:不需要连接,直接给服务器发送数据。 -```python -import socket - -# IPv4, UDP -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -for data in [b"Alice", b"Bob", b"Mary"]: - s.sendto(data, ('127.0.0.1', 9999)) - print(s.recv(1024).decode('utf-8')) -s.close() -``` -- 服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。 - -## 电子邮件 - -电子邮件的传递流程: -- 写好一封邮件之后,从邮件软件(称之为MUA:Mail User Agent,邮件用户代理)发送。 -- 从MUA发出后,不是直接送达对方电脑,而是先送到MTA:Mail Transfer Agent,邮件传输代理,也就是自己用的什么邮件服务商提供的邮件就到哪儿。比如`163.com`那就先投递到网易的MTA。 -- 然后从自己的MTA对方的MTA(中间可能还会经过其他MTA),然后而对方的MTA会把邮件投递到最终目的地:MDA,Mail Delivery Agent,邮件投递代理。 -- 因为对方不一定在线,所以某个时刻在MUA上登录邮箱之后需要从MDA上把邮件取到自己的电脑上。 -- 所以大概的流程是:`发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人` -- 要编写程序来发送接收邮件,本质上就是: - - 编写MUA把邮件发到MTA。 - - 编写MUA从MDA上收邮件。 - -邮件协议: -- 发邮件时,MUA和MTA使用的协议是**SMTP**:Simple Mail Transfer Protocol,MTA到另一个MTA也是SMTP。 -- 收邮件时,MUA和MDA使用的协议有两种。第一种POP:Post Office Protocol,目前版本是3,俗称**POP3**。第二种**IMAP**:Internet Message Access Protocol,目前版本是4,不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱等。 -- 目前大多数邮件服务商都需要手动打开SMTP发信和POP收信功能,否则只允许网页登录使用。 - -SMTP发送邮件: -- Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件、带附件的邮件。 -- 两个模块`smtplib`和`email`,前者构造邮件,后者发送邮件。 -```python -from email.mime.text import MIMEText -from email.header import Header -from email.utils import parseaddr, formataddr - -def _format_addr(s): - name, addr = parseaddr(s) - return formataddr((Header(name, 'utf-8').encode(), addr)) - -# input email and passwd -from_addr = input('From: ') -password = input('password: ') -to_addr = input('To: ') - -# input SMTP server address -smtp_server = input('SMTP server: ') - -# plain text email -msg = MIMEText("hello, send by Python...", 'Plain', 'utf-8') -msg['From'] = _format_addr('暗之恶魔 <%s>' % from_addr) # 发件人 -msg['To'] = _format_addr('光之使者 <%s>' % to_addr) # 收件人 -msg['Subject'] = Header("接受地狱的审判吧!", 'utf-8').encode() # 主题 - -# send to MTA -import smtplib -server = smtplib.SMTP(smtp_server, 25) -server.set_debuglevel(1) # print interactive info with the server -server.login(from_addr, password) -server.sendmail(from_addr, [to_addr], msg.as_string()) -server.quit() -``` -- 发件人和收件人格式时`name `,不能直接发中文,需要使用`Header`进行编码。 -- 密码并不一定就是邮箱密码,比如QQ邮箱就是其生成的一个用于第三方登录的授权码。 -- 上述代码输入信息时可以使用文件输入重定向,不必每一次都重新输入。 -- 要有发件人、收件人、主题才是一封完整的邮件,没有也可以发。 -- 如果要发送附件,可以构造一个`MIMEMultipart`,在其中添加一个`MIMEText`作为正文,在继续加上表示附加的`MIMEBase`对象即可。 -- 除了发送纯文本,也可以发送html邮件,邮件内容就是一个网页,如果要在其中嵌入图片,由于大部分邮件服务商会自动屏蔽带有外链的图片,因为不知道是否指向恶意网站。可以在HTML中通过引用`src="cid:x"`(x为图片编号)就可以把附加作为图片插入了。 -- 更多信息查看文档获取。 - -POP3收取邮件: -- 分两步:用`poplib`把邮件原始文本下载到本地,第二步,用`email`解析原始文本,还原为邮件对象。 -```python -# input email -email = input('Email: ') -password = input('Password: ') -pop3_server = input('POP3 server: ') - -# connect to POP3 server -server = poplib.POP3(pop3_server) -server.set_debuglevel(1) -print(server.getwelcome().decode('utf-8')) - -# authentication -server.user(email) -server.pass_(password) - -# email number and space -print('Message: %s, Size: %s', server.stat()) - -# get numbers of all mails -resp, mails, octets = server.list() -print(mails) - -# get newest mail -index = len(mails) -resp, lines, octets = server.retr(index) - -# get raw content of mail -msg_content = b'\r\n'.join(lines).decode('utf-8') -# parse mail content -msg = Parser().parsestr(msg_content) - -server.quit() -``` -- 后续的解析逻辑就省略了,可查看Python分支或者[廖雪峰的教程](https://www.liaoxuefeng.com/wiki/1016959663602400/1017800447489504)获取。 - -## 数据库 - -程序在运行时,数据存在于内存中,但当程序结束后,数据无论以何种形式最终都会保存到磁盘上,如何定义存储格式就成为了问题,可以是标准化的格式,也可以是自定义格式。当再次运行程序需要读入文件时,就需要将数据全部读入内存,如果数据远超内存大小,就根本无法全部读入内存。 -- 此背景下,为了便于数据的保存、读取和方便的查询,就出现了数据库(Database)这种专门用于集中存储和查询的软件。 -- 数据库诞生历史很久远,早于1950年就诞生了,经历了网状数据库,层次数据库,我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。 -- 关系模型有一套复杂的数学理论。 -- 关系数据库中,基于表的一对多关系是基础。一个表中的一行记录就某一项而言可能对应于另一张表的多行记录。 -- 关系数据库有访问和处理的领域特定语言SQL。无论什么编程语言,涉及到操作数据库,基本都是通过SQL来完成,[廖雪峰教程](https://www.liaoxuefeng.com/wiki/1177760294764384)。 -- 目前使用广泛的商用闭源付费关系数据库:Oracle,微软的SQL Server,IBM的DB2等。 -- 开源数据库相对来说使用更为广泛:使用广泛的MySQL,学术气息挺重的PostgreSQL,适合桌面和移动应用的嵌入式数据库sqlite。 -- MySQL使用最多,一般作为首选,[MySQL Community Server免费下载](https://dev.mysql.com/downloads/mysql/)。更多MySQL与SQL语言的东西可以看[SQL.md](SQL.md)。 - - -使用SQLite: -- SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。 -- Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。 -- [sqlite3库文档](https://docs.python.org/zh-cn/3/library/sqlite3.html)。 -- 访问[SQLite主页](https://www.sqlite.org/index.html)查询支持的SQL方言语法与可用数据类型。 -- 首先要明确的概念: - - 要操作关系数据库,首先需要连接到数据库,一个数据库连接称为`Connection`。 - - 连接到数据库后,需要打开游标,称之为`Cursor`,通过`Cursor`执行SQL语句,然后,获得执行结果。 - - Python定义了一套操作数据库的API接口,任何数据库要连接到Python,只需要提供符合Python标准的数据库驱动即可。 - - 由于SQLite的驱动内置在Python标准库中,所以我们可以直接来操作SQLite数据库。 -```python -import sqlite3 - -# connect to sqlite3 database, the database is file test.db, if not exist, will create a new file -with sqlite3.connect('test.db') as conn: - # creat a cursor - cursor = conn.cursor() - # execute SQL - cursor.execute('drop table if exists user') - cursor.execute('create table user (id varchar(20) primary key, name varchar(20), score int)') - cursor.execute(r'insert into user (id, name, score) values ("1", "Michael", 90)') - cursor.execute(r'insert into user (id, name, score) values ("2", "Kim", 80)') - cursor.execute(r'insert into user (id, name, score) values ("3", "Bob", 65)') - - print(cursor.rowcount) - - # querys - cursor.execute('select * from user where id=? or id=?', ('1','2')) - - values = cursor.fetchall() - print(values) - - # close - cursor.close() # not necessary, __del__ will close automatically - conn.commit() - - def get_score_in(con, min, max): - cursor = conn.cursor() - cursor.execute(r'select name from user where score >= ? and score <= ?', (min, max)) - values = cursor.fetchall() - cursor.close() - return [v[0] for v in values] - - assert get_score_in(conn, 85, 100) == ['Michael'] - assert get_score_in(conn, 70, 100) == ['Michael', 'Kim'] - assert get_score_in(conn, 60, 100) == ['Michael', 'Kim', 'Bob'] -``` -- 使用Python的DB-API时,只要搞清楚`Connection`和`Cursor`对象,打开后一定记得关闭,就可以放心地使用。 -- 使用`Cursor`对象执行`insert update delete`语句,执行结果由`rowcount`返回影响的行数。 -- 使用`Cursor`执行`select`时,使用`fetchall`拿到结果集。结果集是一个列表,元素是元组,对应于每一行记录。 -- SQL语句带有参数时使用`?`作为占位符,第二个参数元组元素对应传入,有几个占位符就需要几个参数。而不应该使用Python自带的字符串参数,这样会有SQL注入的风险。 -- 需要确保打开的`Connection`对象能够正确关闭。可以使用`try...except..finally`或者`with`。 - -使用MySQL: -- 确保本地安装的MySQL配置支持utf-8。 -- `show variables like '%char%';`,其中有很多项,如果登录`mysql`的终端修改了字符页为65001,那么按道理来说应该是全都是utf-8。具体编码问题这里不细究,确保支持中文就行。 -```shell -mysql> show variables like 'char%'; -+--------------------------+---------------------------------------------------------+ -| Variable_name | Value | -+--------------------------+---------------------------------------------------------+ -| character_set_client | utf8mb4 | -| character_set_connection | utf8mb4 | -| character_set_database | utf8mb4 | -| character_set_filesystem | binary | -| character_set_results | utf8mb4 | -| character_set_server | utf8mb4 | -| character_set_system | utf8mb3 | -| character_sets_dir | C:\Program Files\MySQL\MySQL Server 8.0\share\charsets\ | -+--------------------------+---------------------------------------------------------+ -``` -- MySQL的utf-8并不是完整的utf-8,最多只支持3个字节编码,不支持4个字节编码,最新的utfmb4则是完整的utf-8。 -- 安装[MySQL官方驱动](https://www.mysql.com/products/connector/): -```shell -pip install mysql-connector -``` -- 使用:同样通过Python的DB API使用,使用`mysql.connector.connect()`获取连接之后即可使用。 -```python -import mysql.connector - -conn = mysql.connector.connect(user = 'root', password = 'password', database = 'test') -``` -- 连接时可能出现`mysql.connector.errors.NotSupportedError: Authentication plugin 'caching_sha2_password' is not supported`错误,可以[参考这里](https://www.runoob.com/note/45833): - - 原因就是MySQL8.0中验证插件和密码加密的方式发生了变化,由之前版本的`mysql_native_password`变更为了`caching_sha2_password`。 - - 解决方案1是安装`mysql-connector-python`插件。 - - 2是修改MySQL配置`my.ini`中验证方式改回以前的,并且在`connect`时添加参数`auth_plugin='mysql_native_password'`。 - - 能解决即可,这里选择1。 -- 可以使用`fetchall fetchmany`等接口获取`cursor`执行结果,也可以直接对`cursor`进行迭代。 - -- 也可以使用[pymysql](https://github.com/PyMySQL/PyMySQL)模块,[文档](https://pymysql.readthedocs.io/en/latest/index.html),同样使用DB API: -```python -import pymysql -conn = pymysql.connect(user = 'root', password = 'password', database = 'test') -``` - -使用[SQLAlchemy](https://www.sqlalchemy.org/): -- 安装:`pip install SQLAlchemy` -- 前面的使用Python DB API操作结果都是返回一个`list`,每一条记录是一个`tuple`作为元素。使用元组很难看出表的结构,如果将一个记录作为一个对象表示出来,会更加直观一些。也就是传说中的ORM技术(Object-Relational Mapping,对象关系映射)。这个转换由ORM框架来做。 -- Python中最有名的ORM框架就是`SQLAlchemy`。 -- 基本使用: -```python -from sqlalchemy import Column, String, create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.sql.ddl import DDLBase - -# create a base class -Base = declarative_base() - -# define ORM class -class User(Base): - # table name - __tablename__ = 'user' - # table structrue - di = Column(String(20), primary_key=True) - name = Column(String(20)) - -# initialize database connection -# databse+connector://user:password@host:port/database -engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/test') -# create DBSession object -DDSession = sessionmaker(bind=engine) -``` -- 上面代码完成SQLAlchemy初始化和具体表的定义,有多个表就从`Base`派生其他类。 -- `create_engine`初始化数据库连接。用以字符串表示连接信息: -```python -'数据库类型+数据库驱动名称://用户名:口令@机器地址:端口号/数据库名' -``` -- 有了ORM,我们向数据库表中添加一行记录,可以视为添加一个User对象。 -- 添加记录的话,通过像会话中添加对象即可: -```python -# create DbSession object -session = DBSession() -# create new User object -new_user = User(id = '5', name = 'Bob') -# add to session -session.add(new_user) -# commit to database -session.commit() -session.close() -``` -- 查询使用`session.query()`: -```python -session = DBSession() -user = session.query(User).filter(User.id == 5).one() -print('type: ', type(user)) -print('name: ', user.name) -session.close() -``` -- 如果还没有表的话,可以先创建表,会创建所有从`Base`派生的类对应的表: -```python -Base.metadata.create_all(engine) -``` -- 可以配合另一个库[SQLAlchemy-utils](https://github.com/kvesteri/sqlalchemy-utils)使用,为SQLAlchemy提供了一些自定义数据类型和库,[文档](https://sqlalchemy-utils.readthedocs.io/en/latest/index.html)。 -- 更多操作还需要看文档研究。 -- ORM框架的作用就是把数据库表的一行记录与一个对象互相做自动转换。 -- 正确使用ORM的前提是了解关系数据库的原理。 - -TODO: -- 每个库的使用都不能说简单,都使用DB API的操作还还说,但SqlAlchemy要使用时需要下功夫的。这里只是最基本操作,要熟练在项目中使用都需要阅读文档踩坑。具体有实践需求时再来做这些事情。 - -## Web开发 - -Web应用: -- 软件运行在桌面客户端上,而数据库这种服务型的软件运行在服务器端,这种Client/Server模式称为CS架构。 -- 互联网兴起后,Web应用程序因为要快速迭代修改和升级,如果使用桌面客户端就需要逐个频繁升级,因此流行起了将客户端运行在浏览器上的Browser/Server模式,称BS架构。 -- Web应用的几个阶段: - - 静态Web页面,静态HTML页面,修改页面内容就需要编辑HTML源文件。早期的互联网Web页面就是静态的。 - - CGI:静态Web页面无法与用户交互,如果用户填了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了Common Gateway Interface,简称CGI,用C/C++编写。 - - ASP/JSP/PHP:Web应用由于修改频繁,用C/C++这种更偏底层的语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。 - - MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了Model-View-Controller的模式,来简化Web开发。ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。 -- Python有很多Web框架,有很多成熟的模板技术,选择Python开发应用,开发效率高,运行速度快。 - -HTML: -- 超文本标记语言。 -- HTML定义了页面的内容,CSS来控制页面元素的样式,而JavaScript负责页面的交互逻辑。 -- 对于优秀的Web开发人员来说,精通HTML、CSS和JavaScript是必须的。 -- 学习网站:https://www.w3school.com.cn/ -- 当我们用Python或者其他语言开发Web应用时,我们就是要在服务器端动态创建出HTML,这样,浏览器就会向不同的用户显示出不同的Web页面。 -- 示例: -```html - - - Hello - - - - -

Hello, world!

- - -``` - -WSGI接口: -- Web应用的本质: - - 浏览器发送一个HTTP请求; - - 服务器收到请求,生成一个HTML文档; - - 服务器把HTML文档作为HTTP响应的Body发送给浏览器; - - 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。 -- 最简单的静态Web应用就是将HTML文件保存好,用现成的HTTP服务器软件,接受用户请求,从文件中取出HTML返回。比如Apache、Nginx、Lighttpd等常见的静态服务器。 -- 而要动态生成HTML,就需要自己实现生成HTML的步骤。接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活。正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。 -- 这个接口就是WSGI:Web Server Gateway Interface。用来接收并响应HTTP请求。 -- 定义一个最简单的WSGI接口: -```python -def application(environ, start_response): - start_response('200 OK', [('Content-Type', 'text/html')]) # header, response code and header content - return [b'

Hello, web!

'] # body -``` -- 我们只需要关系从参数`environ`字典中拿到HTTP请求信息,然后构造HTML,通过`start_response`发送Header,然后返回Body。底层解析HTTP请求或者构造HTTP响应头的操作不需要自己来做。 -- Python内置了一个WSGI服务器,这个模块叫`wsgiref`,它是用纯Python编写的WSGI服务器的参考实现。 -```python -from wsgiref.simple_server import make_server - -# create a http server -httpd = make_server('', 8000, application) -print("Serving HTTP on port 8000...") -httpd.serve_forever() -``` -- 启动后,打开浏览器输入`http://localhost:8000/`即可访问。命令行可以看到`wsgiref`打印的日志信息。 -- 使用WSGI服务器,无论是多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过`environ`获得,HTTP响应的输出都可以通过`start_response()`加上函数返回值作为Body。 -- 对于复杂的应用程序来说,光靠WSGI函数还是太底层了,需要在WSGI之上再抽象出Web框架。进一步简化Web开发。 -- Python的WSGI接口可以看这里:[PEP 333 - Python Web Server Gateway Interface v1.0 中文版](https://github.com/mainframer/PEP333-zh-CN)。 - -Web框架: -- 使用[Flask](https://flask.palletsprojects.com/en/2.0.x/):`pip install flask`。 -```python -from flask import Flask -from flask import request - -app = Flask(__name__) - -@app.route('/', methods=['GET', 'POST']) -def home(): - return '

Home

' - -@app.route('/signin', methods=['GET']) -def signin_form(): - return '''
-

-

-

-
''' - -@app.route('/signin', methods=['POST']) -def signin(): - # 需要从request对象读取表单内容: - if request.form['username']=='admin' and request.form['password']=='password': - return '

Hello, admin!

' - return '

Bad username or password.

' - -if __name__ == '__main__': - app.run() -``` -- 处理三个请求: - - `GET /` - - `GET /signin`,登录页,显示登录表单。 - - `POST /signin`,处理登录表单,显示登录结果。 - - 同一个URL/signin分别有GET和POST两种请求,映射到两个处理函数中 -- Flask通过装饰器在内部自动地把URL函数关联起来。 -- 运行之后可以在`http://127.0.0.1:5000/`访问。 -- 实际应用的话,需要配合上数据库,拿到用户名和口令之后应该去数据库中查询对比来判定用户登录状态。 -- 除了Flask还有其他Web框架: - - [Django](https://www.djangoproject.com/):全能型Web框架。 - - [Web.py](https://webpy.org/):小巧Web框架。 - - [Bottle](http://bottlepy.org/docs/dev/):类似于Flask。 - - [Tornado](http://www.tornadoweb.org/):Facebook的开源异步框架。 -- 有了Web框架,我们在编写Web应用时,注意力就从WSGI处理函数转移到URL+对应的处理函数,这样,编写Web App就更加简单了。 -- 在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。Web框架都提供了自己的API来实现这些功能。Flask通过request.form['name']来获取表单的内容。 - - -使用模板: -- 有了Web框架就不需要在WSGI函数中编写整个网站的逻辑,但依然需要提供页面HTML,但对于一个复杂的页面来说将所有HTML以字符串方式写在源码中是不现实也不合理的。 -- 所以有了模板技术,准备一个HTML文档,其中潜入了一些变量和指令,根据传入的指令和数据,经过程序逻辑替换后得到最终的HTML,发送给用户。 -- 这就是MVC:Model-View-Controller,即模型-视图-控制器。 -- 在这里,模型就是要传递给HTML的数据,视图就是HTML模板最终输出用户看到的HTML,控制器则是Python代码中将模型数据传递给HTML的逻辑。 -- 模板中大多是留下由变量表示的空位,由框架将数据通过关键字参数或者字典传递给模板得到最终的HTML。 -- 比如Flask: -```python -from flask import Flask, request, render_template - -app = Flask(__name__) - -@app.route('/', methods=['GET', 'POST']) -def home(): - return render_template('home.html') - -@app.route('/singin', methods=['GET']) -def signin_form(): - return render_template('form.html') - -@app.route('/signin', methods=['POST']) -def signin(): - username = request.form['username'] - password = request.form['password'] - if username == 'admin' and password == 'password': - return render_template('signin-ok.html', username=username) - return render_template('form.html', message='Bad username or password', username=username) - -if __name__ == '__main__': - app.run() -``` -- 和上面的例子一样,不过换成了使用模板,Flask默认的模板是Jinja2,安装Flask时会安装。 -- 模板文件需要放在`templates`目录下。 -- `Jinja2`模板中使用`{{ name }}`表示要替换的变量,很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用`{% ... %}`表示指令。 -- 除了Jinja2,常见的模板还有: - - [Mako](https://www.makotemplates.org/):用`<% ... %>`和`${xxx}`的一个模板; - - [Cheetah](http://www.cheetahtemplate.org/):也是用`<% ... %>`和`${xxx}`的一个模板; - - [Django](https://www.djangoproject.com/):Django是一站式框架,内置一个用`{% ... %}`和`{{ xxx }}`的模板。 -- 目前前后端分离,已经没有人用模板了。 - -## 异步IO - -异步IO: -- 要解决CPU的高速和IO的低速不匹配的问题,避免CPU暂停等待IO完成,就需要使用多线程或者多进程将IO任务分配到其他线程或者进程去做,也就是异步IO。 -- 当代码要执行IO操作时,只发出IO指令,并不等待IO结果,然后执行其他代码。一段时间后当IO执行完成,再通知CPU进行处理。 -- 普通同步IO代码: -```python -# things before - -f = open('test.txt', 'r') -text = f.read() # thread wait here -f.close() -print(text) - -# other things -``` -- 同步IO模型代码无法实现异步IO模型。 -- 异步IO模型需要一个消息循环,消息循环中,主线程不断重复读取消息-处理消息这个循环,就像所有GUI程序做的那样。 -```python -loop = get_event_loop() -while True: - event = loop.get_event() - process_event(event) -``` -- 消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。 -- 在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。 -- 消息模型中的的异步IO:遇到IO操作时,代码只发出IO请求,不等待IO结果,直接结束本轮消息处理,进入下一轮消息处理。IO操作完成后,收到IO完成的消息,处理该消息时获得IO操作的结果。 -- 同步IO模型中,处理IO过程中主线程只能挂起,异步IO模型中,主线程继续执行,由IO线程处理IO,一个主线程可以同时处理多个IO请求,并且没有切换线程的操作。对于IO密集型应用程序,使用异步IO将大大提升系统的多任务处理能力。 - -**协程**: -- 又名微线程,Coroutine。 -- 提出很早,但直到最近几年才在某些语言中广泛应用。 -- 子程序又称函数,基本在所有语言中都是通过栈实现的层级调用,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。 -- 协程看上去也是子程序,但执行过程中,子程序内部可中断,然后转去执行别的子程序,适当的时候在回来执行。中断执行其他子程序不是通过函数调用去执行,而是类似于CPU的中断,就像线程切换一样,但多个协程其实是一个线程在执行。 -- 协程的优势: - - 相对线程而言极高的执行效率,子程序切换而不是线程切换,没有线程切换的开销,由程序自身控制而不是操作系统调度。和多线程比,线程数量越多,协程性能优势就越明显,体现在子程序的切换上。 - - 第二优势就是不需要多线程的锁机制,因为只有一个线程,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 -- 协程是一个线程执行,如果要利用多核CPU,最简单的方法是通过多进程+协程实现。 -- Python中对协程的支持是通过`generator`实现的。生成器同时也是一个迭代器,可以用`for`来迭代,也可以用`next()`获取下一个`yield`出来的值。 -- Python的`yield`不但可以返回一个值,还可以用来接受调用者发出的参数,调用生成器的`send(arg)`函数发送消息。 -- 使用Python的协程实现生产者消费者模型: -```python -def consumer(): - r = '' - while True: - n = yield r - if not n: - return - print('[CONSUMER] Consuming %s...' % n) - r = '200 OK' - -def produce(c): - c.send(None) - n = 0 - while n < 5: - n = n + 1 - print('[PRODUCER] Producing %s...' % n) - r = c.send(n) - print('[PRODUCER] Consumer return: %s' % r) - c.close() - -c = consumer() -produce(c) -``` -- `c.send(None)`启动生成器。最后通过`close()`关闭生成器。 -- 整个流程无锁,有一个线程执行,`produce`和`consumer`协作完成任务,所以称为“协程”,而非线程的抢占式多任务。 -- “子程序就是协程的一种特例”。 -- 对生成器调用`next()`时,`yield`语句将得到`None`。 - - -**asyncio**: -- `asyncio`是Python3.4引入的标准库支持了异步IO。 -- 例: -```python -import asyncio -import threading - -@asyncio.coroutine -def hello(n): - print(f'hello,world! from {threading.currentThread()}, n = {n}') - r = yield from asyncio.sleep(1) - print(f'hello,again! from {threading.currentThread()}, n = {n}') - -loop = asyncio.get_event_loop() -# execute coroutine -tasks = [hello(1), hello(2)] -loop.run_until_complete(asyncio.wait(tasks)) -loop.close() -``` -- 执行结果: -``` -hello,world! from <_MainThread(MainThread, started 7600)>, n = 2 -hello,world! from <_MainThread(MainThread, started 7600)>, n = 1 -hello,again! from <_MainThread(MainThread, started 7600)>, n = 2 -hello,again! from <_MainThread(MainThread, started 7600)>, n = 1 -``` -- `@asyncio.coroutine`把一个生成器标记为`coroutine`类型,然后将这个协程放到执行协程的事件循环中就行。就实现了异步IO。 -- `asyncio.sleep()`也是一个`coroutine`,线程不会等待`asyncio.sleep()`,而是直接中断并执行下一个消息循环,当`asyncio.sleep()`返回时,从`yield from`拿到返回值,然后接着执行下一语句。 -- 两个`coroutine`在同一线程中执行。 -- 例子:异步连接获取三个网站的响应,打印响应头: -```python -import asyncio - -@asyncio.coroutine -def wget(host): - print('wget %s...' % host) - connect = asyncio.open_connection(host, 80) # connect is a coroutine - reader, writer = yield from connect - header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host - writer.write(header.encode('utf-8')) - yield from writer.drain() - while True: - line = yield from reader.readline() - if line == b'\r\n': - break - print('%s header > %s' % (host, line.decode('utf-8').rstrip())) - # Ignore the body, close the socket - writer.close() - -loop = asyncio.get_event_loop() -tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']] -loop.run_until_complete(asyncio.wait(tasks)) -loop.close() -``` -- 执行结果中可以看到,在去连接前一个网站的过程中,协程让出了时间片,事件循环中断并执行了其他协程。 -- 多个协程可以封装成一组Task并发执行。 -- 阅读: - - [Async IO in Python: A Complete Walkthrough](https://realpython.com/async-io-python/) - -**async/await**: -- `asyncio`提供的语法是:`@asyncio.coroutine`包装一个生成器为协程,然后在内部可以使用`yield from`调用另一个协程实现异步。当然也可以包装一个普通函数为协程。 -- Python3.5引入了新语法`async`和`await`,用以替代`@asyncio.coroutine`和`yield from`。旧语法在Python3.8版本废弃,并计划于Python3.10中移除。 -- 不要与普通的生成器混淆,`async`用来定义协程,`await`用来调用协程(不能用来调用一个普通的生成器)。 - -**aiohttp**: -- `asyncio`可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。 -- 如果把`asyncio`用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程加上协程实现多用户的高并发支持。 -- `asyncio`实现了TCP、UDP、SSL等协议,`aiohttp`则是基于`asyncio`实现的HTTP框架。 -- 例子: -```python -''' -async web application. -''' - -import asyncio - -from aiohttp import web - -async def index(request): - await asyncio.sleep(0.5) - return web.Response(body=b'

Index

', content_type='text/html') - -async def hello(request): - await asyncio.sleep(0.5) - text = '

hello, %s!

' % request.match_info['name'] - return web.Response(body=text.encode('utf-8'), content_type='text/html') - -async def init(loop): - app = web.Application(loop=loop) - app.router.add_route('GET', '/', index) - app.router.add_route('GET', '/hello/{name}', hello) - srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000) - print('Server started at http://127.0.0.1:8000...') - return srv - -loop = asyncio.get_event_loop() -loop.run_until_complete(init(loop)) -loop.run_forever() -``` - -## 总结 - -- 动态类型代码确实太难以读代码了,虽然语法简单,但是类型会令人纠结。 -- 某些时候缺失了类型信息,补全都难以进行,也许类型标注会是一个好的手段。 -- 函数式编程支持有限,不过任何手段都只是手段而不是目的,如果达成目的有唯一的最佳方式,那使用最佳实践就好了,还降低了选择成本。 -- 各种库功能完善,细节略多,基本有了一个大概印象,具体使用则基本都是走马观花,还需要具体用到时才好深入。 -- 简单入门了网络编程、HTTP编程、异步编程、SQL编程,但具体实用还远远不够,还需要更多的理论与实践。比如HTTP、协程、WSGI等。 -- 更多高级的用法,更多细节,更多具体库的使用,待实践后深入。 \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 07a4d51..0000000 --- a/README.md +++ /dev/null @@ -1,77 +0,0 @@ -## 内容目录 - -C/C++: -- [Makefile.md](Makefile.md):Makefile笔记。 -- [CMake.md](CMake.md):CMake笔记。 -- [TCC.md](TCC.md):一个轻量极简自举的C编译器TinyCC的使用文档与源码解析。 -- [CTrapsAndPitFalls.md](CTrapsAndPitFalls.md):《C陷阱与缺陷》粗略笔记。 -- [C++Primer.md](C++Primer.md):C++入门书籍《C++ Primer》笔记与代码记录。 -- [EffectiveC++.md](EffectiveC++.md):C++必读书籍《Effective C++》笔记与记录。 -- [C++STL.md](C++STL.md):《C++标准库》笔记与代码。 -- [C++ObjectModel.md](C++ObjectModel.md):《深度探索C++对象模型》笔记与代码。 -- [MySTL.md](MySTL.md):基于《STL源码剖析》的个人STL实现。 -- [Boost.md](Boost.md):C++Boost库学习,TODO。 -- [CppToolChain.md](CppToolChain.md):C++工具链使用总结。 -- [C++NewStandard.md](C++NewStandard.md):C++新标准学习(C++11/14/17/20/23)。 -- [C++TemplateProgramming.md](C++TemplateProgramming.md):C++模板编程学习。 -- [C++20Programming.md](C++20Programming.md):《C++20高级编程》代码记录。 -- [C++TemplateMetaProgrammingInAction.md](C++TemplateMetaProgrammingInAction.md):《C++模板元编程实战:一个深度学习框架的初步实现》代码记录。 -- [C++CodingStandards.md](C++CodingStandards.md):《C++编程规范:101条规则、准则与最佳实践》笔记。 -- [C++ConcurrencyInAction.md](C++ConcurrencyInAction.md):《C++并发编程实战》笔记。 -- [C++DesignPattern.md](C++DesignPattern.md):《设计模式——可复用面向对象软件的基础》笔记。 -- [C++ToDo.md](C++ToDo.md):C++待学习的更多东西。 - -通用编程语言: -- [Java.md](Java.md):Java入门。 -- [Python.md](Python.md):Python语言入门,基本语法。 -- [Haskell.md](Haskell.md):纯函数式编程语言Haskell入门。 -- [Scala.md](Scala.md):Scala语言入门,基本语法。 -- [SICP.md](SICP.md):SICP学习笔记,兼Scheme语法入门。 -- [IoLanguage.md](IoLanguage.md):编程语言IO入门,非常粗浅无甚参考价值。 -- [Ruby.md](Ruby.md):Ruby语言入门,很粗浅无甚参考价值。 -- [Lua.md](Lua.md):脚本语言Lua入门。 - -领域特定语言: -- [BatchScript.md](BatchScript.md):Windows批处理脚本了解。 -- [SQL.md](SQL.md):基本SQL知识。 -- [Prolog.md](Prolog.md):声明式编程语言Prolog入门。 - -操作系统: -- [APUE.md](APUE.md):《Unix环境高级编程》(APUE)阅读笔记。 -- [CSAPP.md](CSAPP.md):《深入理解计算机系统》(CSAPP)阅读笔记。 - -工具: -- [LaTex.md](LaTeX.md):LaTex学习笔记,暂无实质内容。 -- [Git.md](Git.md):Git学习笔记。 -- [Nginx.md](Nginx.md):Nginx Web服务器了解与配置入门。 - -编译原理: -- [BNF&RecursiveDescent.md](BNF&RecursiveDescent.md):BNF文法与递归下降算法。 -- [OperatorPrecedenceParser.md](OperatorPrecedenceParser.md):运算符优先级分析法解析表达式。 -- [CompilerOptimizations.md](CompilerOptimizations.md):粗浅的编译器优化方法了解。 - -图形学: -- [LearnOpenGL.md](LearnOpenGL.md):OpenGL学习。 - -数学: -- [MathFunc.md](MathFunc.md):``常用数学函数分析与实现,TODO。 -- [CategoryTheory.md](CategoryTheory.md):范畴论粗浅了解,为了理解Haskell,TODO中。 - -杂项: -- [Encoding.md](Encoding.md):关于字符编码。 -- [Repos.md](Repos.md):感兴趣的开源项目列表,用来索引和查询。 - -文档链接记录: -- [Docs.md](Docs.md):各种计算机相关的文档与资料链接记录。 - -娱乐: -- [Wordle分支](../../tree/Wordle/):填字游戏[Wordle](https://www.nytimes.com/games/wordle/index.html)的作弊程序。 - -## 关于这个仓库 - -记录个人计算机相关笔记,方便随时查阅,基本都是一些入门和常识性的东西。 - -## 镜像仓库 - -- Github: [tch0/notes](https://github.com/tch0/notes) -- Gitee: [tch0/notes](https://gitee.com/tch0/notes) \ No newline at end of file diff --git a/Repos.md b/Repos.md deleted file mode 100644 index df234b4..0000000 --- a/Repos.md +++ /dev/null @@ -1,291 +0,0 @@ -# 可读计算机图书目录 - -其他地方找来的,做个参考。 -## C - -- 《C程序设计语言》(经典,不适合小白入门) -- 《C语言程序设计:现代方法》 -- 《C primer plus》(入门推荐) -- 《C陷阱和缺陷》 -- 《C专家编程》 -- 《C和指针》(领悟指针精髓) -- 《C语言接口与实现》 -- 《C11标准文档》(非书,可查阅) - -## C++ - -- 《C++ primer》(适合有一定基础) -- 《C++标准库》 -- 《Effective Modern C++ 》 -- 《more effective C++》 -- 《深度探索C++对象模型》 -- 《STL源码剖析》 -- 《effective STL》 -- 《C++ template》 -- 《Exceptional C++》 -- 《C++编程思想》 -- 《C++语言的设计和演化》 - -## Java - -- 《effective java》 -- 《Java核心技术卷》(有两卷) -- 《Java语言程序设计》(有两卷) -- 《深入理解Java虚拟机》 -- 《Java编程思想》(进阶) -- 《Java并发编程实战》 - -## Python - -- 《ython编程 : 从入门到实践》 -- 《A Byte of Python》(快速上手) -- 《Python编程快速上手》(适合完全零基础) -- 《流畅的Python》(非小白入门所选) -- 《Python Cookbook》 - -## GO - -- 《go程序设计语言》 - -## Haskell - -- 《Haskell趣学指南》 -- 《[Real World Haskell](http://cnhaskell.com/index.html)》 - -## 计算机基础 - -- 《编码:隐匿在计算机软硬件背后的语言》 -- 《深入理解计算机系统》(强烈推荐) -- 《计算机程序的构造和解释》 -- 《计算机组成与设计 : 硬件/软件接口》 - -## 算法 - -- 《算法导论》(大钻头,不易读) -- 《编程珠玑》 -- 《算法》(相对易读) -- 《数据结构与算法分析-C语言描述》 - -## 计算机网络 - -- 《计算机网络》 -- 《计算机网络-自顶向下方法》 -- 《TCP/IP详解-卷1》 -- 《网络是怎样连接的》 - -## 编译链接 - -- 《编译原理》(龙书) -- 《程序员的自我修养-装载,链接和库》 -- 《Linkers and Loaders》 - -## 操作系统/Linux - -- 《现代操作系统》(中文版感觉有点晦涩) -- 《操作系统精髓与设计原理》 -- 《操作系统概念》 -- 《Linux内核设计与实现》(整体介绍,不如后面两本深入) -- 《深入理解Linux内核》 -- 《深入Linux内核架构》(大砖头,讲解了Linux中关键部分) -- 《鸟哥的linux私房菜基础篇》(基础篇可入门Linux) -- 《Linux命令行与shell脚本编程大全》 -- 《Linux Tools Quick Tutorial》 (教程) - -## 数据库 - -- 《Mysql必知必会》(超薄小册子) -- 《高性能Mysql》 -- 《redis设计与实现》(学习里面的设计思路,数据结构与算法) -- 《数据库系统实现》 - -## 系统/网络编程 - -- 《Unix环境高级编程》(APUE)(经典) -- 《Unix网络编程》(UNP)(两卷,经典) -- 《Linux/UNIX系统编程手册》 - -## 设计/软件工程 - -- 《代码大全》 -- 《代码整洁之道》 -- 《程序员修炼之道》 -- 《Unix编程艺术》 -- 《重构》 -- 《敏捷软件开发:原则、模式与实践》 -- 《设计模式_可复用面向对象软件的基础》 - -## 工具 - -- 《pro git》在线文档 - -## 工作面试 - -- 《编程之美——微软技术面试心得》 -- 《剑指offer 名企面试官精讲典型编程题》 - -## 其他 - -- 《浪潮之巅》 -- 《黑客与画家》 - -# 开源项目目录 - -选入标准:觉得有趣,拿来玩,看一看学习,很牛,学习方向,时下流行。不限于github,但大部分都在github上应该都有。 - -推荐一个网站:[HelloGithub][hellogithub] - -[hellogithub]: https://www.hellogithub.com/ - -## 组织 - -- [The Algorithms](https://github.com/TheAlgorithms)——各种语言实现的各种数据结构和算法集合,包括C/C++/Ruby/Python/Java/Scala等多种语言。 -- [Zio](https://github.com/zio)——包含多个Scala库,用函数式编程的设计,解决Scala编程中异步、并发的各种问题。 - -## C - -- [fengyoulin/ef](https://github.com/fengyoulin/ef)——一个C语言的轻量协程(池)实现,以及基于IO多路复用的协程调度。 -- [TheAlgorithms/C](https://github.com/TheAlgorithms/C)——C语言实现的各种数学、机器学习、计算机科学、数学算法,教学目的。 -- [cstack/db_tutorial](https://github.com/cstack/db_tutorial)——一个从头开始C语言实现SQL数据库的教程。 - -## Cpp - -- [vczh/vczh_toys][cpp-1]——轮子哥的黑魔法玩具与实验,学习学习。 -- [vczh/tinymoe][cpp-2]——轮子哥的动态类型编程语言,学习编译原理。 -- [wuye9036/cppTemplateTutorial][cpp-3]——C++模板编程入门进阶教程,可惜作者没有写完。 -- [Ubpa/USRefl][cpp-4]——一个C++静态反射库。 -- [facebook/folly][cpp-5]——facebook开源的基于C++14的内部组件库,对标准库的补充,尤其是大规模下的性能。 -- [nothings/single_file_libs][cpp-6]——单文件C/C++库目录,收集了许多C/C++的至多两个文件的轻量级库,包含数据结构、数学、Parsing、图形、音视频、调试等多个领域,值得一看。 - -[cpp-1]: https://github.com/vczh/vczh_toys -[cpp-2]: https://github.com/vczh/tinymoe -[cpp-3]: https://github.com/wuye9036/cppTemplateTutorial -[cpp-4]: https://github.com/Ubpa/USRefl -[cpp-5]: https://github.com/facebook/folly -[cpp-6]: https://github.com/nothings/single_file_libs - -## Python - -- [python-guide](https://github.com/realpython/python-guide)——Python最佳实践指南。 - - -## GUI - -- [duilib/duilib][proj-1]——一个C++轻量级UI库。 -- [vczh-libraries/GacUI][proj-2]——轮子哥的C++界面库,支持GPU加速。 -- [notepad-plus-plus/notepad-plus-plus][proj-3]——notepad++编辑器。 -- [Embarcadero/Dev-Cpp][proj-4]——上古IDE,DevC++。 - - -[proj-1]: https://github.com/duilib/duilib -[proj-2]: https://github.com/vczh-libraries/GacUI -[proj-3]: https://github.com/notepad-plus-plus/notepad-plus-plus -[proj-4]: https://github.com/Embarcadero/Dev-Cpp - - -## 工具库 - -- [open-source-parsers/jsoncpp][util-1]——JsonCpp,易用的json解析库。 - -[util-1]: https://github.com/open-source-parsers/jsoncpp - -## 图形学 - -- [ssloy/tinyrenderer][graphics-1]——OpenGL的渲染器课程,使用OpenGL。 -- [TensShinet/toy_renderer][graphics-2]——一个大佬学习上面课程的记录。 -- [yangzhenzhuozz/Renderer][graphics-3]——基于EasyX实现一个3D渲染器,有文档提供入门教程。 -- [matrixcascade/PainterEngine][graphics-4]——一个高度可移植完整开源跨平台的C游戏引擎。 -- [miloyip/light2d][graphics-5]——2D图形学光照入门。 - - -[graphics-1]: https://github.com/ssloy/tinyrenderer -[graphics-2]: https://github.com/TensShinet/toy_renderer -[graphics-3]: https://github.com/yangzhenzhuozz/Renderer -[graphics-4]: https://github.com/matrixcascade/PainterEngine -[graphics-5]: https://github.com/miloyip/light2d - - -## Windows/Microsoft - -- [PowerShell/PowerShell][windows-1]——Windows PowerShell。 -- [microsoft/TypeScript][windows-2]——微软亲生TS语言。 -- [microsoft/Windows-classic-samples][windows-3]——WindowsAPI编程实例。 -- [microsoft/calculator][windows-4]——win10上的计算器。 - -[windows-1]: https://github.com/PowerShell/PowerShell -[windows-2]: https://github.com/microsoft/TypeScript -[windows-3]: https://github.com/microsoft/Windows-classic-samples -[windows-4]: https://github.com/microsoft/calculator - - -## VSCode插件 - -- [hediet/vscode-drawio][vscode-1]——在VSCode里面画图。 -- [shd101wyy/markdown-preview-enhanced][vscode-2]——MarkDown插件。 - -[vscode-1]: https://github.com/hediet/vscode-drawio -[vscode-2]: https://github.com/shd101wyy/markdown-preview-enhanced - - -## 面试、知识体系、技能树、教程 - -- [qianguyihao/Web][skilltree-1]——前端入门进阶学习笔记。 -- [LearnOpenGL-CN/LearnOpenGL-CN][skilltree-2]——LeanOpenGL教程中文翻译。 -- [linw7/Skill-Tree][skilltree-3]——后端开发面试知识体系。 -- [zhengjianglong915/note-of-interview][skilltree-4]——互联网面试笔记。 -- [CyC2018/CS-Notes][skilltree-5]——技术面试必备基础知识,计算机体系。 -- [sdmg15/Best-websites-a-programmer-should-visit][skilltree-6]——程序员必备的网站合集。 - -[skilltree-1]: https://github.com/qianguyihao/Web -[skilltree-2]: https://github.com/LearnOpenGL-CN/LearnOpenGL-CN -[skilltree-3]: https://github.com/linw7/Skill-Tree -[skilltree-4]: https://github.com/zhengjianglong915/note-of-interview -[skilltree-5]: https://github.com/CyC2018/CS-Notes -[skilltree-6]: https://github.com/sdmg15/Best-websites-a-programmer-should-visit - - -## 前端 - -- [markdown-it/markdown-it][frontend-1]——一个MarkDown解析器。 - -[frontend-1]: https://github.com/markdown-it/markdown-it - -## AI - -- [lllyasviel/style2paints][ai-1]——二次元线稿自动上色的引擎。 - - -[ai-1]: https://github.com/lllyasviel/style2paints - - -## 开发工具 - -- [OpenCppCoverage/OpenCppCoverage][tools-1]——C++覆盖率测试工具。 -- [skywind3000/awesome-cheatsheets][tools-2]——编程语言、开发工具速查表。 - -[tools-1]: https://github.com/OpenCppCoverage/OpenCppCoverage -[tools-2]: https://github.com/skywind3000/awesome-cheatsheets - - -## 编译原理 - -- [miloyip/json-tutorial][compiler-1]——从零开始的JSON库教程。 -- [fool2fish/dragon-book-exercise-answers][compiler-2]——龙书第二版课后习题答案。 -- [rswier/c4][c4]——4个函数实现的基于虚拟机的极简C语言编译器,实现了C语言的一个子集。我自己也重构了一下,并扩展了部分功能,[tch0/JustAToyCCompiler][jatcc],但仍不能算是能用的地步,玩具级别。 -- [drh/lcc][lcc]——一个教学用的、完整的C99编译器。 -- [Tiny CC][tcc]——Fabrice Bellard大神和伙伴们写的小型C编译器。 - -[compiler-1]: https://github.com/miloyip/json-tutorial -[compiler-2]: https://github.com/fool2fish/dragon-book-exercise-answers -[c4]: https://github.com/rswier/c4 -[jatcc]: https://github.com/tch0/JustAToyCCompiler -[lcc]: https://github.com/drh/lcc -[tcc]: https://bellard.org/tcc/tcc-doc.html - - -## 娱乐项目 - -- [komeiji-satori/Dress][fun-1]——女装。 -- [995icu/996ICU][fun-2]——996.ICU。 - -[fun-1]: https://github.com/komeiji-satori/Dress -[fun-2]: https://github.com/995icu/996ICU diff --git a/Ruby.md b/Ruby.md deleted file mode 100644 index d610232..0000000 --- a/Ruby.md +++ /dev/null @@ -1,965 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [7周7语言](#7%E5%91%A87%E8%AF%AD%E8%A8%80) -- [Ruby](#ruby) - - [环境配置](#%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [语法快速入门](#%E8%AF%AD%E6%B3%95%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8) - - [编程模型](#%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B) - - [条件与循环](#%E6%9D%A1%E4%BB%B6%E4%B8%8E%E5%BE%AA%E7%8E%AF) - - [鸭子类型](#%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B) - - [阶段总结](#%E9%98%B6%E6%AE%B5%E6%80%BB%E7%BB%93) - - [函数](#%E5%87%BD%E6%95%B0) - - [数组](#%E6%95%B0%E7%BB%84) - - [哈希表](#%E5%93%88%E5%B8%8C%E8%A1%A8) - - [代码块和yield](#%E4%BB%A3%E7%A0%81%E5%9D%97%E5%92%8Cyield) - - [定义类](#%E5%AE%9A%E4%B9%89%E7%B1%BB) - - [Mixin](#mixin) - - [模块、枚举和集合](#%E6%A8%A1%E5%9D%97%E6%9E%9A%E4%B8%BE%E5%92%8C%E9%9B%86%E5%90%88) - - [阶段总结](#%E9%98%B6%E6%AE%B5%E6%80%BB%E7%BB%93-1) - - [元编程](#%E5%85%83%E7%BC%96%E7%A8%8B) - - [开放类](#%E5%BC%80%E6%94%BE%E7%B1%BB) - - [method_missing](#method_missing) - - [模块](#%E6%A8%A1%E5%9D%97) - - [Ruby总结](#ruby%E6%80%BB%E7%BB%93) - - - -# 7周7语言 - -书籍[七周七语言:理解多种编程范型](https://book.douban.com/subject/10555435/)的读书笔记。用来串联与总结,要想详细还得读完全书。 - -因为书本身比较短仅245页,所以缩短为7天7语言。重点在于理解和体会编程范式,体会一门语言的精髓之处,而不是掌握一个语言的全部,要想7周掌握7种编程语言那肯定是不现实的。 - -探索一门语言时应该关注什么: -- 类型模型:强类型(Java)还是弱类型(C),静态类型(Java)还是动态类型(Python),本书中全部都是强类型。类型模型将会影响你对问题的处理方式,还会控制语言的运行方式。 -- 编程范式:基于逻辑、面向对象、面向过程、函数式还是多种范式的综合。基于逻辑的编程语言(Prolog)、两门完全支持面向对象思想的语言(Ruby和Scala)、四门带有函数式特性的语言(Scala、Erlang、Clojure和Haskell)及一门原型语言(Io)。里有Scala这样的多范型(multiparadigm)语言,也有Clojure这种多方法(multimethod)语言,后者甚至允许你实现自定义范型。学习新的编程范式将会是一个重点。 -- 怎么和语言交互:解释器、虚拟机、命令行交互、单文件编程、项目组织与管理。 -- 语言的判断结构与核心数据结构:很多语言使用和if、while这种常规结构不同的判断结构,集合是每一门编程语言的核心数据结构。无论用哪一类语言,都必须全面、透彻地理解集合。 -- 语言的高级核心特性:并发编程的高级特性、独一无二的高级结构、性能强劲的虚拟机、针对特定问题的编程模型等。 - -7门语言: -- Ruby:面向对象,重点是**Ruby元编程**(metaprogramming)。 -- Io:原型语言,最简语法,拥有兼具简单性和语法一致性的**并发结构**,还有着独一无二、韵味无穷的**消息分发机制**。小众语言,与商业无缘。 -- Prolog:**基于逻辑**的经典编程语言。 -- Scala:运行在Java虚拟机上,为Java系统引入了强大的**函数式编程**思想,同时保留了面向对象特性。 -- Erlang:历史悠久,**函数式**,在**并发**、分布式编程、容错等诸多方面都有优异表现。 -- Clojure:运行在Java虚拟机上,**Lisp方言**,书中唯一在版本数据库中使用同一种策略管理并发的语 -言。拥有全书最灵活的**编程模型**。 -- Haskell:书中唯一的**纯函数式语言**,拥有令人艳羡的**类型模型**。 - -仅用几个关键字概括一门语言是无法做到的,还需了解之后细细品味。 - -关注重点: -- 除了Ruby和Prolog之外,另外5门语言都有强大的并发模型,并发应该是一个关注重点。 -- 4门语言都有函数式编程,这是另一个关注重点。 -- 新的不熟悉的编程范式也应该重点关注。 - -涉及的范围: -- 环境配置,供读者以及作者以后参考。(书中未给出) -- 限于命令行交互式编程或者单文件编程,不会深入包管理以及项目组织与管理。 -- 只涉及关心的核心语法,不是编程参考书,不会全面解读一门语言,某些不关心的核心语法甚至都不会解读。 - -实践要求: -- 每门语言的实践目标应该是完成一个超越基本语法的任务。比如Ruby当然需要写一些元编程的代码。 -- 独立思考并完成课后习题很重要。 - -# Ruby - -[Ruby](https://zh.wikipedia.org/wiki/Ruby)由松本行弘大约在1993年左右发明。语言角度来看,Ruby是**解释型、面向对象、动态类型**的原因。运行在解释器上而非由编译器编译执行,类型绑定发生在运行时而非编译期,面向对象的特性:封装继承多态当然也都支持。执行速度谈不上高效,但是Ruby的编程效率却很高。Ruby变得流行的原因很大程度上是因为Ruby编写的Web框架 [Ruby on Rails](https://rubyonrails.org/) 非常受欢迎。 - - -## 环境配置 - -浏览器搜索Ruby,找到官网,截止目前最新稳定版本是3.0.2,上一个稳定版是2.7.4,入门阶段装哪个版本无所谓。 -- Windows上直接运行安装包,一键安装,记得勾选添加环境变量。 -- Linux上使用包管理器,Ubuntu为例,`sudo apt install ruby`即可。 - -版本: -``` -ruby -v -``` - -[Ruby文档](https://www.ruby-lang.org/zh_cn/documentation/)中包含了多个快速入门教程、API参考文档、手册、开发环境介绍。挑选一些进行阅读是很有必要的,开始前在其中选一个15、20或者30分钟的交互式入门教程熟悉一下语法是比较好的。 - -进入命令行交互环境: -```shell -irb -``` -经典HelloWorld: -```ruby -puts 'hello,world!' -``` - -Linux上和Windows上是同理的,任选一个作为实验环境就行。 - -配置文件执行与调试环境: -- 这里选择VSCode安装Ruby插件以及Ruby solargraph(需要首先运行`gem install solargraph`安装)插件,以支持代码补全、智能提示、鼠标悬停文档。 -- 文件后缀:`.rb` `.rbw`一般使用前者。 -- 执行:`ruby hello.rb` 或点击运行按钮 - -作为脚本执行: -```ruby -#!/usr/bin/env ruby -``` - -## 语法快速入门 - -注释: -```ruby -# single line comment, like python -``` - -函数: -```ruby -def hello(name = "world") # 默认参数 - puts "hello #{name}" # ""字符串中使用#{var}插入变量 -end - -hello("world") -hello # 无参数可以不加括号 -``` - -类: -```ruby -class Person - attr_accessor :name#,:age - def initialize(name = "nobody", age = 10) # constructor - @name = name - @age = age - end - def hello() - puts "Hello, #{@name}" # @name instance variable - end - def bye - puts "Bye, #{@name}" - end -end - -if __FILE__ == $0 # __FILE__ is current script, $0 is first start argument - mary = Person.new("mary") - if mary.respond_to?("hello") # true - mary.hello - end - if mary.respond_to?("hi") # false - mary.hi - end - if mary.respond_to?("age") # false - puts(mary.age) - else - puts("can not access age") - end - if mary.respond_to?("name") # true - puts(mary.name) - end - - puts(Person.instance_methods(false)) # print all functions of Person - - mary.name = nil # nil is like null - mary.bye -end -``` -类的内部使用`@`访问实例字段,使用`attr_accessor`控制外部是否可以访问字段。提供类反射方法`instance_methods`获取类的所有方法,实例方法`respond_to?`询问是否可以从外部访问实例的字段和方法。 - -最后得到的结果:一个数组`[:hello, :bye, :name, :name=]`,其中`name`用来获取,`name=`用来赋值。所以其实字段还是使用对应生成的方法(也就是其他编程语言中比如C#/Java中所说的属性)来获取和设置的。 - -入口的技巧:如果当前是启动文件则执行。 - -解释型语言,动态类型,没有执行到的部分有运行时错误(比如找不到方法),也不会报错。 - -循环: -```ruby -names = ["Nagato Yuki", "Suzumiya Haruhi"] # array -if names.respond_to?("each") # true for array - names.each do |name| - puts("Long tim no see, #{name}") - end -end -``` - -就等价于C++中: -```c++ -vector names = {"Nagato Yuki", "Suzumiya Haruhi"}; -for (auto name : names) -{ - cout << "Long tim no see, " << name << ".\n"; -} -``` - -在`each`内部实际上会自动调用`yield "Nagato Yuki"`和`yield "Suzumiya Haruhi"`。 - -## 编程模型 - -面向对象,Ruby中一切皆是对象,就连单独的数也是。 -```shell -irb(main):005:0> x = 4 -=> 4 -irb(main):006:0> x > 5 -=> false -irb(main):007:0> false -=> false -irb(main):008:0> false.class -=> FalseClass -irb(main):009:0> 4.class -=> Integer -irb(main):010:0> 1.0.class -=> Float -``` - -## 条件与循环 - -`true` `false`是对象,并且是**一等对象**。每个语句表达式都会有返回值,如果没有,就返回`nil`。交互环境下会打印出来。 - -条件: -```ruby -x = 4 -unless x > 4 - puts "x <= 4" -end -puts "x <= 4" if not x > 4 -puts "x <= 4" if !(x > 4) - -if x >= 4 - puts "x >= 4" -end -``` - -两种形式: -```ruby -(if/unless condition statements end) -(statements if/unless condition) # single line format -``` - -`unless`就是`if`反义,也可以用`not` `!`表示反义,到底清不清晰好不好理解就见仁见智了。毕竟我觉得自由度变高了是需要更多心智负担的。 - -条件判断时除了`nil`和`false`其他都是`true`,也就是说**0也是true**。 - -循环: -```ruby -x = 1 -sum = 0 -while x <= 100 - sum += x - x = x + 1 -end -puts(sum) # 5050 - -x = 0 -sum = 0 -sum = sum + (x = x + 1) until x == 100 -puts(sum) # 5050 - -x = 0 -sum = 0 -sum = sum + (x = x + 1) while x < 100 -puts(sum) # 5050 -``` -`while` `until`,`until`就是`do while`。同样两种写法等价,单行形式只能有一条语句。 - -逻辑运算符:`&& and` `|| or` `! not`,`&& ||`短路求值。`& |`也是逻辑运算符,只不过是非短路版本,会将所有表达式求值。一般使用前者。 - -## 鸭子类型 - ->什么是鸭子类型?鸭子类型表述为当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。(在只关心鸭子的这几个特性的情况下)。鸭子类型是编程语言中动态类型语言中的一种设计风格,一个对象的特征不是由父类决定,而是通过对象的方法决定的。 - -Ruby是**强类型**的,也就是**类型安全**的,当某些操作错误使用了类型时,将得到一个错误。 - -```ruby -irb(main):006:0> 4 + "hello" -(irb):6:in `+': String can't be coerced into Integer (TypeError) -``` - -- 这是运行时而非编译期检查的。(某些语法上的错误是能够在运行前(编译期?)进行检查的!)。因此从最严格的角度来说,Ruby不是强类型语言,不过一般情况下,Ruby表现得像一门强类型语言。 -- 也就是说Ruby是**动态类型**,尝试执行代码时才进行类型检查。(在完整的多趟编译/解释过程中,类型检查应该是在语义分析过程中,而不是语法分析)。 -- 这种类型系统也有自己的优势,就是鸭子类型的应用。例: -```ruby -a = ["100", 10.0] -sum = 0 -a.each do |num| - num = num.to_i # duck type, every type of num which has a to_i method can run this code - sum = sum + num -end - -puts(sum) -``` -字符串和浮点数都可以转化为整数,这是因为Ruby中一切皆是对象,都能调用`to_i`。其中还动态改变了num的语言,这也是动态类型的特点。面向对象中的重要思想就是:面对接口而不是实现编程,利用鸭子类型,实现这一原则仅需极少的工作就可以轻松完成。使用鸭子类型,可以在不使用继承的情况下实现多态。 - -## 阶段总结 - -内容总结: -- Ruby是解释型语言,一切皆是对象,且易于获取对象的任何信息,如对象的各方法及所属类。 -- 动态类型语言,它是鸭子类型的,且行为通常和强类型语言毫无二致。 -- 感受上来说和Python非常地像。 -- 用end显式表示块的结束。 - -## 函数 - -- 不像Java或者C#那样,定义一个函数必须构建一个类,像python一样可以直接构建函数。 -- 函数也是对象,也可以作为参数传递。 -- 每个函数都会返回结果,如果没有显示指定。函数就将返回退出函数前最后处理的表达式的值。 - - -```ruby -def fib(n) - if n <= 0 - return 0 - elsif n == 1 - return 1 - else - return fib(n-1) + fib(n-2) - end -end - -puts(fib(10)) -``` - -函数调用时的括号可以省略,甚至函数定义时的括号也可以省略(不推荐)。 - -## 数组 - -```ruby -irb(main):001:0> a = ["Catholly", "Nephren", "Lilia"] -=> ["Catholly", "Nephren", "Lilia"] -irb(main):002:0> a[0] -=> "Catholly" -irb(main):003:0> a[-1] -=> "Lilia" -irb(main):004:0> a[1..2] -=> ["Nephren", "Lilia"] -irb(main):005:0> (0..1).class -=> Range -irb(main):006:0> a[0] = "Rhantolk" -=> "Rhantolk" -irb(main):007:0> a[3] -=> nil -irb(main):008:0> a.size -=> 3 -irb(main):009:0> a.class -=> Array -irb(main):010:0> a[4] = 0 -=> 0 -irb(main):011:0> a -=> ["Rhantolk", "Nephren", "Lilia", nil, 0] -``` - -使用`[]`定义,类型是`Array`,可以通过下标获取,并且提供了灵活的`Range`来获取范围。数组中元素不要求类型相同,多维数组就是数组的数组。数组有及其丰富的API,可以将其当做队列、链表、栈、集合来使用。 - -## 哈希表 - -```ruby -irb(main):001:0> dict = {1 => 2, "hello" => "world", :a => 10, :f => 15} -=> {1=>2, "hello"=>"world", :a=>10, :f=>15} -``` - -使用`{}` 配合`=>`定义,同样使用`[]`获取元素。哈希表的键和值都可以是任意类型。还可以在其中引入**符号**(Symbol):前面带有冒号的标识符。尽管两个同值字符串在物理上不同,但相同的符号却是同一物理对象。 - -```ruby -irb(main):007:0> "string".object_id -=> 280 -irb(main):008:0> "string".object_id -=> 300 -irb(main):009:0> :a.object_id -=> 768028 -irb(main):010:0> :a.object_id -=> 768028 -irb(main):011:0> :a.class -=> Symbol -``` - -使用散列表可以有一些很有用的应用,比如Ruby虽然不支持命名参数,但可以用散列表来模拟它。 - -```ruby -def fun(options = {}) - if (options[:hello] == :world) - puts("hello,world!") - elsif - puts("world is crazy") - end -end - -fun :hello => "yes" # omit () and {} -fun({:hello => :world}) -``` - -## 代码块和yield - -代码块是**没有名字的函数**。它可以作为参数传递给函数或方法。代码块有两种形式: -- `{}`或者`do / end`。 -- 惯例是单行使用`{}`,多行使用`do / end`。仅仅是惯例而已,其实用于代码块时是等价的。 - -```ruby -3.times {puts "hello,world!"} - -10.times do |i| - print i, ' ' -end -``` -这里将代码块作为了参数传递给了实例方法`Integer.times`。实现是从0遍历到该整数,每次使用`yield`返回一个整数。如果没有参数给定,那么返回一个Enumerator。 -```ruby -irb(main):013:0> 2.times -=> # -irb(main):015:0> 10.times do |i| print i, ' ' end -0 1 2 3 4 5 6 7 8 9 => 10 -``` - -```ruby -class Integer - def my_times - i = self - while i > 0 - i = i - 1 - yield i - end - end -end - -10.my_times do |i| - if !i.nil? - print i, ' ' - elsif - puts "nil" - end -end -``` -其中`my_times`的定义**打开现有的类,向其中添加一个方法**,这就有点黑魔法的味道了!这里是和`times`返回的值是反过来的。 - -它用`yield`调用代码块,每次`yield`都会调用外面的代码块一次,如果`yield`不返回值,那么外面的代码块就会获取到`nil`,外部的代码块通过`|*args|`的形式来获取`yield *args`的值。 - -代码块还可直接用作**一等参数**,不需要包装在任何结构中,需要使用`&`: -```ruby -def call_block(&block) - block.call - puts(block.class) # Proc -end - -def pass_block(&block) - call_block(&block) -end -pass_block {puts "hello"} -``` - -我们说过万物皆对象,代码块其实就是一个`Proc`对象。 - -Ruby中代码块不仅可以用于循环,还可以用于延迟执行。 -```ruby -execute_at_noon {puts 'hello'} -``` - -- 可以看到其实`yield`和回调其实是一回事,`yield`就是在`yield`的位置执行外部传入的函数块,而回调则是显式地传入了函数。 -- 使用`yield`的话参数是空的,但如果调用时有参数又相当于传入了参数,应该考虑调用时没有传函数块和传了函数块的行为有什么区别。比如`times`实现。个人感觉主要用于函数实现中有循环的情况会比较适用。 -- 使用回调的话,可以做到和`yield`同样的事情。`yield`算是一个很甜的语法糖,还有许多细节比如函数参数个数需要细细考究。 - -用回调做到`my_times`的事情: -```ruby -class Integer - def my_times2(&block) - i = self - while i > 0 - i = i - 1 - block.call i, i*i - end - end -end - -10.my_times2 do |i, j| - print i, ' ', j, ' ' -end -``` - -- 经过试验可以确定`yield`和回调其实就是等价的,`yield`也是可以返回多个值的。如果调用的函数块获取的参数多于yield返回值数量或者回调`call`调用实参数量,那么没有对应到的部分会获取到`nil`。如果参数多了,多了的部分会被省略,也就是说不检查参数数量是否匹配,可见其灵活性。 -- 有一点区别就是当没有函数块传入时,yield语法提示是`no block given (yield) (LocalJumpError)`,回调的提示是`undefined method 'call' for nil:NilClass (NoMethodError)`。回调的方式可以通过判断`block.nil?`来判断是否为空,但yield语法还不知道怎么判断传入函数块为空。 - -**疑问**:使用yield语法时参数列表是空的是怎么判断有没有函数块参数传入的? - -**华点发现**:一个空格引起的问题,开`-w`选项会报warning,这本身是一种歧义。函数调用可以不加括号真的很拉风,不过需要注意优先级,一个好习惯是运算符两边总是应该加空格。可以看一下这份[ruby-style-guide](https://github.com/JuanitoFatas/ruby-style-guide/blob/master/README-zhCN.md)。 -```ruby -arr = [] -arr.push 0 -a = arr.last + 1 # 1 -b = arr.last +1 # [0], equals to arr.last(+1), -``` - -## 定义类 - -前面其实已经用了许多内置的类了,`Integer Float String Range Proc Symbol`等。 - -```ruby -irb(main):001:0> 4.class -=> Integer -irb(main):002:0> 4.class.superclass -=> Numeric -irb(main):003:0> 4.class.superclass.superclass -=> Object -irb(main):004:0> 4.class.superclass.superclass.superclass -=> BasicObject -irb(main):005:0> 4.class.superclass.superclass.superclass.superclass -=> nil -irb(main):006:0> 4.0.class -=> Float -irb(main):007:0> 4.0.class.superclass -=> Numeric -irb(main):010:0> 4.class.class -=> Class -irb(main):011:0> 4.class.class.superclass -=> Module -irb(main):012:0> 4.class.class.superclass.superclass -=> Object -irb(main):013:0> 4.class.class.superclass.superclass.superclass -=> BasicObject -``` - -自定义的类将派生自`Object`。所有的类的共同祖先都是`Object`然后是`BasicObject`。一个类同时也是一个模块,`Class`派生自`Module`,有点抽象! - -上述的不为`nil`的表达式都表示一个`Class`对象,用来标识一个类。 - -实现一个简易的树,支持外部定义访问方式: -```ruby -class Tree - attr_accessor :children, :node_name # from Module - def initialize(name, children=[]) - @children = children - @node_name = name - end - - def visit_all(&block) - visit &block - children.each {|c| c.visit_all &block} - end - - def visit(&block) - block.call self - end -end - -t = Tree.new("Catholly", [Tree.new("Nephren", [Tree.new("Tiat")]), Tree.new("Lilia")]) - -puts("visiting a node") -t.visit {|node| puts node.node_name} - -puts("visiting entire tree") -t.visit_all {|node| puts node.node_name} -``` -如果用`yield`的话会不太好理解,并且很繁杂,因为多层函数调用中的`yield`时不会自动传递出来,需要手动接收了之后再`yield`出来。不然执行时就会报`no block for yield`。 -```ruby -class Tree - attr_accessor :children, :node_name # from Module - def initialize(name, children=[]) - @children = children - @node_name = name - end - - def visit_all - visit {|c| yield c} - children.each {|c| c.visit_all {|c| yield c}} - end - - def visit - yield self - end -end -``` - -`each`在没有函数块时会返回一个`Enumerator`,所以要手动`yield`每一个子节点。 - -类定义的惯例: -- 大写字母开头,驼峰命名。也就是大驼峰。 -- 实例变量和方法以小写字母开头,采用下划线命名法。常量采用全大写形式。 -- 用于测试逻辑的方法和函数一般要加上`?`。 - -规则: -- 实例变量在类中访问前面必须加`@`。类变量前面加`@@`。 -- `attr`关键字用于定义实例变量和访问变量的同名方法。`attr_accessor`定义实例变量、 -访问方法和设置方法。前者也可以定义设置方法,只是需要多传入一个`true`,`attr :children, true` -- `initialize`是构造函数。 - - -## Mixin - -面向对象的语言利用继承,可以将行为传播到相似对象上。想要继承并不相似的多种行为可以通过多继承实现(C++支持,java不支持)。 - -ruby中通过Mixin可以动态地给类添加方法: -```ruby -module ToFile - def filename - "objct_#{self.object_id}.txt" - end - - def to_f - File.open(filename, 'w') {|f| f.write(to_s)} - end -end - -class Person - include ToFile - attr_accessor :name, :age - def initialize(name, age) - @name = name - @age = age - end - - def to_s - "#{name} #{age}" - end -end - -p = Person.new("Nehpren", 13) -p.to_f -``` - -首先定义了一个模块,通过`include`包含进来到`Person`类中,Person就拥有了`filename` `to_f`两个方法。 - -稍微扩展一下,就可以实现对象的文件保存与从文件读取: -```ruby -module FileName - def filename - "objct_#{self.object_id}.txt" - end -end - -module ToFile - include FileName - def to_f - File.open(filename, 'w') {|f| f.write(to_s)} - end -end - -module FromFile - include FileName - def from_f - File.open(filename, 'r') {|f| from_s(f.read)} - end -end - -class Person - include ToFile - include FromFile - attr_accessor :name, :age - def initialize(name, age) - @name = name - @age = age - end - - def to_s - "#{@name} #{@age}" - end - - def from_s(s) - a = s.split(' ') - @name = a[0] - @age = a[1].to_i - end -end - -p = Person.new("Nephren", 13) -p.to_f -p.from_f -puts p -``` -这里读写的文件名依赖对象的`objct_id`,所以前后必须是同一个对象,可以进一步改写。 - -利用这种嵌入Mixin的方式,可以先编写类主体,然后嵌入其他功能。可以编写自己的通用嵌入方法,合理添加到已定义的类中。 - -ruby是动态类型、鸭子类型的,所以继承就被弱化了,通过嵌入就可以实现完全一样的功能。 - -## 模块、枚举和集合 - -Ruby中两个重要的Mixin:枚举(enumerable)和比较(comparable)。让类可枚举,就需要实现`each`方法,让类可比较,就需要实现`<=>`操作符。常见类比如字符串、整数等已经实现了这两个方法。 - -```ruby -irb(main):003:0> "begin" < "end" -=> true -irb(main):004:0> "begin" <=> "end" -=> -1 -irb(main):005:0> a = [4, 5, 1] -=> [4, 5, 1] -irb(main):006:0> a.sort -=> [1, 4, 5] -irb(main):007:0> a -=> [4, 5, 1] -irb(main):008:0> a.any? {|i| i > 0} -=> true -irb(main):009:0> a.select {|i| i % 2 == 0} -=> [4] -irb(main):010:0> a.max -=> 5 -irb(main):011:0> a.member?(1) -=> true -``` - -常用类比如`String Array`已经实现了许多有用的方法。选择、排序、查找、包含、枚举、最大最小值等。 - -数组成员实现`<=>`之后才可以排序,很好理解。 - -## 阶段总结 - -- 代码块就是匿名函数。 -- `yield`就是一个接受代码块的语法糖。 -- 内建的各种结合类型提供了方便的操作。 -- ruby是单继承,但Mixin嵌入,打开类定义,加入新方法可以使面向对象变得非常灵活。因为是鸭子类型,多继承能做的ruby都能做。 - -## 元编程 - -改变一门语言的本来面目和行为方式,你才算真正掌握了赋予编程无穷乐趣的魔法。这就是Ruby的**元编程**(metaprogramming)。 - -元编程,说白了就是**写能写程序的程序**。 - -## 开放类 - -你可以在任何时候改变任何类的定义,常见于给类添加行为。 - -比如: -```ruby -class NilClass - def blank? - true - end -end -class String - def blank? - self.size == 0 - end -end -``` -添加`blank?`方法就可以将`nil`和空字符串调用同一个方法来判空了。 - -有了随时重定义任何类或对象的自由,我们就能写出极为通俗易懂的代码。甚至可以随意修改内建的类的定义,需要谨慎处理,这甚至可能会让Ruby瘫痪。 - -## method_missing - -Ruby找不到某个方法时,会调用一个特殊的调试方法显示诊断信息。该特性不仅让Ruby更易于调试,有时还能实现一些不易想到的有趣行为。 - -通过`method_missing`可以获取到未找到的方法的名称和参数。 -```ruby -class Roman - def self.method_missing name, *args # name is call method name, *args is arguments - roman = name.to_s - roman.gsub!("IV", "IIII") - roman.gsub!("IX", "VIIII") - roman.gsub!("XL", "XXXX") - roman.gsub!("XC", "LXXXX") - (roman.count('I') + - roman.count('V') * 5 + - roman.count('X') * 10 + - roman.count('L') * 50 + - roman.count('C') * 100) - end -end - -puts Roman.X -puts Roman.XC -puts Roman.XII -puts Roman.X -``` - -这样写调用任何Roman的方法都不会报错了,这样调试起来也会更难,需要谨慎使用。 - -## 模块 - -Ruby最流行的元编程方式还要属模块。仅在模块中写上寥寥数行代码,就可以实现def或attr_accessor关键字的功能。你还可以通过一些令人惊叹的方式扩展类定义,其中一种技术是设计自己的DSL(domain-specific language,领域特定语言),再用DSL定义自己的类。 - -比如写一个解析CSV文件的类: -```ruby -class ActAsCsv - def read - file = File.new(self.class.to_s.downcase + '.txt') - @headers = file.gets.chomp.split(', ') - - file.each do |row| - @result << row.chomp.split(', ') - end - end - - def headers - @headers - end - def csv_contents - @result - end - def initialize - @result = [] - read - end -end - -class RubyCsv < ActAsCsv -end - -m = RubyCsv.new -puts m.headers.inspect -puts m.csv_contents.inspect -``` - -当然这里并没有解析完CSV文件的所有情况,比如一个值有空格需要用双引号包起来,其中有双引号还要用`""`转义。这里不处理这些细节。 - -上面的代码就是一个普通的实现,下面使用**宏(macro)**来实现同样的行为。 -```ruby -class ActAsCsv - def self.act_as_csv - define_method "read" do - file = File.new(self.class.to_s.downcase + '.txt') - @headers = file.gets.chomp.split(', ') - file.each do |row| - @result << row.chomp.split(', ') - end - end - define_method "headers" do - @headers - end - - define_method "csv_contents" do - @result - end - - define_method "initialize" do - @result = [] - read - end - end -end - -class RubyCsv < ActAsCsv - act_as_csv -end -``` - -元编程发生在`act_as_csv`宏中,可以在子类实现中选择是否需要添加该功能。但其实这样看起来和通过继承实现也没有什么两样。 - -接下来在模块中,同样的行为如何实现: -```ruby -module ActAsCsv - def self.included(base) - base.extend ClassMethods - end -end - -module ClassMethods - def act_as_csv - include InstanceMethods - end -end - -module InstanceMethods - attr_accessor :headers, :csv_contents - def read - @csv_contents = [] - file = File.new(self.class.to_s.downcase + '.txt') - @headers = file.gets.chomp.split(', ') - file.each do |row| - @csv_contents << row.chomp.split(', ') - end - end - def initialize - read - end -end - -class RubyCsv # do not use inheritance - include ActAsCsv - act_as_csv -end -``` - -其实看起来就是把继承放在了模块里面,然后不需要继承只需要包含模块,调用模块中函数就能够做到添加函数。 - -只要一个模块被另一个模块包含,就会调用该模块的`included`方法。类也是模块,在`ActAsCsv`模块的`included`方法中扩展了名为`base`的目标类(即调用时的`RubyCsv`类)。通过模块`ClassMethods`为`RubyCsv`类添加了`act_as_csv`方法。在`act_as_csv`类方法中又打开了`RubyCsv`类,添加了模块`InstanceMethods`中定义的实例方法。 - -这样就有了一个会写程序的程序,说实话还是不是很能get到这个点的威力在什么地方,可能还需要一个更加有力的例子来说明问题。 - -所有这些元编程技术的有趣之处在于,程序可以根据它应用时的状态而改变。 - -元编程功能: -- 利用Ruby定义自己的语法。 -- 动态地改变类。 - - -练习,上述例子基础上可以根据每列的名称作为函数名作用于每行上: -```ruby -module ActAsCsv - def self.included(base) - base.extend ClassMethods - end -end - -module ClassMethods - def act_as_csv - include InstanceMethods - end -end - -module InstanceMethods - attr_accessor :headers, :csv_contents - def read - @csv_contents = [] - file = File.new(self.class.to_s.downcase + '.txt') - @headers = file.gets.chomp.split(', ') - file.each do |row| - @csv_contents << row.chomp.split(', ') - end - end - def initialize - read - end - def each - csv_contents.each {|row| yield CsvRow.new(@headers, row)} - end -end - -class CsvRow - attr_accessor :row - def initialize(headers, row) - @headers = headers - @row = row - end - def method_missing(name, *args, &block) - index = @headers.index(name.to_s) - @row[index] if index # != nil # only nil and false is false to if, 0 is true - end -end - -class RubyCsv # do not use inheritance - include ActAsCsv - act_as_csv -end - -csv = RubyCsv.new -csv.each {|row| puts row.one} # print all elements in column "one" -``` - -## Ruby总结 - -- 动态类型、强类型、解释执行。 -- 提供了多种条件循环写法供选择。 -- 鸭子类型,更方便地实现多态。 -- 万物皆对象,看一下什么才是真正的面向对象啊! -- 完备的内建类型和常用数据结构支持。 -- 函数块非常有趣,写法也非常有创造性。 -- `yield`语法糖。 -- 开放类结合Mixin太强大了。 -- 元编程才算是精髓。 -- 很多语法都没有介绍,比如for循环,Ruby中并不推荐使用for循环(指仅有的foreach循环),因为for循环能做到的事用一个`each`配合函数块就能做到,只是写法不一样,而且for循环好像还有副作用。 - -可继续探究: -- 补完语法细节。 -- 内置类型、库API。 -- 正则表达式。 -- 多线程、并发。 -- 网络编程、数据库。 -- Ruby On Rails。 - -Ruby优劣势: -- 脚本语言,编码效率高,Web开发Ruby On Rails有史以来最成功的Web开发框架之一。 -- 性能表现一般,但据说最新版本[Ruby3x3](https://blog.heroku.com/ruby-3-by-3)有极大性能改进。 -- 过于自由,相比python这种你只有一种最佳实践的设计哲学,Ruby是你可以有多种完成一件事的方式,说不上谁好谁坏,对于geek来说这可以是优势,对于团队开发也能是劣势,见仁见智。 - -感受: -- 给我最大的惊喜主要有两点: - - 函数块和万物皆对象的纯粹面向对象思想。 - - 开放类以及鸭子类型带来的灵活的多态。 -- 元编程还不熟悉,感觉就是各种动态修改类的方式,需要更多实践才能更好体会。 -- 用了一天多的时间,看来还是有点低估了。 - -深入资料阅读: -- [ruby-doc](https://ruby-doc.org/) 更多的语法细节与文档。 -- [ruby-style-guide](https://github.com/JuanitoFatas/ruby-style-guide/blob/master/README-zhCN.md) 一份编程风格指南。 -- [Ruby教程 | 菜鸟教程](https://www.runoob.com/ruby/ruby-tutorial.html) 一份中文入门教程。 diff --git a/SICP.md b/SICP.md deleted file mode 100644 index 47af7a6..0000000 --- a/SICP.md +++ /dev/null @@ -1,3 +0,0 @@ -## SICP笔记 - -书中代码、练习解答与笔记见 [tch0/SICP](https://github.com/tch0/SICP)。 \ No newline at end of file diff --git a/SQL.md b/SQL.md deleted file mode 100644 index 1cdfddd..0000000 --- a/SQL.md +++ /dev/null @@ -1,825 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [SQL入门](#sql%E5%85%A5%E9%97%A8) - - [关系数据库](#%E5%85%B3%E7%B3%BB%E6%95%B0%E6%8D%AE%E5%BA%93) - - [环境配置](#%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [关系模型](#%E5%85%B3%E7%B3%BB%E6%A8%A1%E5%9E%8B) - - [查询数据](#%E6%9F%A5%E8%AF%A2%E6%95%B0%E6%8D%AE) - - [修改数据](#%E4%BF%AE%E6%94%B9%E6%95%B0%E6%8D%AE) - - [MySQL](#mysql) - - [MySQL实用SQL语句](#mysql%E5%AE%9E%E7%94%A8sql%E8%AF%AD%E5%8F%A5) - - [事务](#%E4%BA%8B%E5%8A%A1) - - [TODO](#todo) - - - -# SQL入门 - -现代程序员离不开关系数据库,要使用关系数据库就必须掌握SQL。SQL(Structured Query Language)称之为结构化查询语言,是一门典型的DSL。 - -现在很流行NoSQL,也就是非SQL的数据库,但事实上SQL从始至终都没有被取代过,学习NoSQL之前学习关系数据库也是必须的。 - -阅读: -- [廖雪峰SQL教程](https://www.liaoxuefeng.com/wiki/1177760294764384) -- [SQL教程 | 菜鸟教程](https://www.runoob.com/sql/sql-tutorial.html) - -## 关系数据库 - -为什么要使用数据库? -- 随着应用程序的功能越来越复杂,数据量越来越大,如果管理数据就成为了大问题。 -- 读取文件并解析数据需要大量重复代码。 -- 从大量数据中快速查询指定数据需要大量复杂的逻辑。 -- 每个应用程序都有各自读写数据的代码,效率低容易出错。 -- 每个程序访问数据的接口各不相同,数据难以复用。 -- 所以数据库出现了,作为专门管理数据的程序,应用程序通过数据库提供的接口来读写数据,而不需要自己管理数据,至于数据如何存储如何读取,则不需要应用程序自己来考虑。 - -数据模型: -- 数据库按照组织、存储和管理数据。典型的有三种模型:层次模型、网状模型、关系模型。 -- 层次模型的数据结构就是一棵树。 -- 网状模型则是一个图,每个数据节点和许多其他节点连接起来。 -- 关系模型把数据看做一个二维表格,任何数据都可以通过行号列号唯一确定。 - -数据库关系模型具有一套复杂的数学理论,以关系为基础,不在这里展开。 - -举例: -- 所有学生信息可以用一张表表示。 - -ID|姓名|班级ID|性别|年龄 -:-:|:-:|:-:|:-:|:-: -1|小明|201|M|9 -2|小红|202|F|8 -3|小军|202|M|8 -4|小白|201|F|9 - -- 其中班级ID对应着另一个班级表。 - -ID|名称|班主任 -:-:|:-:|:-: -201|二年级一班|王老师 -202|二年级二班|李老师 - -- 这样,给定一个班级名称,就可通过第二张表查找到班级ID,然后去第一张表中找到这个班中所有学生,二维表之间通过ID映射建立了一对多的关系。 - -数据类型: -- 对于一个关系表,除了定义每一列名称,还需要定义每一列的数据类型,关系数据库支持的标准数据类型如下: - -名称|类型|说明 -:-:|:-:|:-: -INT|整型|4字节整数类型,范围约+/-21亿 -BIGINT|长整型|8字节整数类型,范围约+/-922亿亿 -REAL|浮点型|4字节浮点数,范围约+/-10^38 -DOUBLE|浮点型|8字节浮点数,范围约+/-10^308 -DECIMAL(M,N)|高精度小数|由用户指定精度的小数,例如,DECIMAL(20,10)表示一共20位,其中小数10位,通常用于财务计算 -CHAR(N)|定长字符串|存储指定长度的字符串,例如,CHAR(100)总是存储100个字符的字符串 -VARCHAR(N)|变长字符串|存储可变长度的字符串,例如,VARCHAR(100)可以存储0~100个字符的字符串 -BOOLEAN|布尔类型|存储True或者False -DATE|日期类型|存储日期,例如,2018-06-22 -TIME|时间类型|存储时间,例如,12:20:59 -DATETIME|日期和时间类型|存储日期+时间,例如,2018-06-22 12:20:59 - -- 上表为常用数据类型,许多数据类型还有别名,还有一些不常用数据类型,例如,TINYINT(范围在0~255)。各数据库厂商还会支持特定的数据类型,例如JSON。选择数据类型的时候,要根据业务规则选择合适的类型。通常来说,BIGINT能满足整数存储的需求,VARCHAR(N)能满足字符串存储的需求,这两种类型是使用最广泛的。 - -主流关系数据库: -- 商用数据库,例如:[Oracle](https://www.oracle.com),[SQL Server](https://www.microsoft.com/sql-server/),[DB2](https://www.ibm.com/db2/)等。 -- 开源数据库,例如:[MySQL](https://www.mysql.com/),[PostgreSQL](https://www.postgresql.org/)等。 -- 桌面数据库,以微软[Access](https://products.office.com/access)为代表,适合桌面应用程序使用。 -- 嵌入式数据库,以[Sqlite](https://sqlite.org/)为代表,适合手机应用和桌面程序。 - - -SQL: -- Structured Query Language,称之为结构化查询语言 -- SQL语句既可以查询数据库中的数据,也可以添加、更新和删除数据库中的数据,还可以对数据库进行管理和维护操作。 -- 不同数据库都支持SQL。 -- 虽然SQL已经被ANSI组织定义为标准,不幸地是,各个不同的数据库对标准的SQL支持不太一致。 -- 就是说如果只使用标准SQL,理论上所有数据库都支持,如果是使用某个特定数据库的扩展SQL,那么换一个数据库就不能执行了。 -- 但现实情况是,只使用标准SQL的核心功能,所有数据库都可以执行,不常用的功能,不同数据库支持程度不一样。各个数据库支持的各自扩展的功能,通常我们把它们称之为“方言”。 -- SQL定义了集中操作数据库的能力: - - DDL:Data Definition Language,允许用户定义数据,就是创建表、删除表、修改表,通常由数据库管理员执行。 - - DML:Data Manipulation Language,为用户提供添加、删除、更新数据的能力,应用程序对数据库的日常操作。 - - DQL:Data Query Language,允许用户查询数据,通常是最频繁的数据库日常操作。 - - DCL:Data Control Language,对用户访问数据的权限的控制,基本表和视图的授权和回收。 - - TCL:Transaction Control Language,事务控制,事务提交和回滚。 - - 其中DDL、DML、DQL最为常用。 -- 语法特点: - - SQL语言关键字不区分大小写。 - - 针对不同的数据库,对于表名和列名,有的数据库区分大小写,有的数据库不区分大小写。 - - 同一个数据库,有的在Linux上区分大小写,有的在Windows上不区分大小写。 - - 此处SQL关键字总是大写,以示突出,表名和列名均使用小写。 - -## 环境配置 - -安装MySQL: -- MySQL是瑞典的MySQLAB公司开发,后被Sun公司收购,Sun后又被Oracle收购,MySQL就变成了Oracle旗下产品。 -- MySQL本身实际上只是一个SQL接口,它的内部还包含了多种数据引擎,常用包括: - - InnoDB:由Innobase Oy公司开发的一款支持事务的数据库引擎,2006年被Oracle收购; - - MyISAM:MySQL早期集成的默认数据库引擎,不支持事务 - - 接口和数据库引擎的关系就像浏览器和浏览器引擎,对用户而言,切换引擎不影响用户界面。 - - 使用MySQL时,不同的表还可以使用不同的数据库引擎,不知道该如何选择时,选择InnoDB引擎就行。 -- MySQL还有其他的开源分支版,比如MariaDB、Aurora、PolarDB。 -- 官方版分了好几个版本:社区,标准,企业,集群。社区版免费,其他版本功能和价格依次递增,主要增加监控、集群等管理功能,基本的SQL功能是一致的。 -- 安装: - - Windows:[官网下载](https://dev.mysql.com/downloads/mysql/),选择版本下载安装,会自动添加环境变量。 - - Linux:`sudo apt install mysql-server`。 - - 安装之后设置密码:`sudo mysqladmin -u root password "newpwd"` -- MySQL安装后会自动后台运行,Linux中启动MySQL:`sudo service mysql restart`。 -- 登录MySQL:`mysql -u root -p`输入密码,即可进入MySQL的shell,Linux中可能需要`sudo`。 - -启动和停止MySQL服务: -- Linux中: -```shell -service mysql status -service mysql start -service mysql stop -service mysql restart -``` -- windows中:服务名可以在windows服务`services.msc`中找到,本机上是`MySQL80`。需要管理员权限执行终端。 -```shell -net stop mysql80 -net restart mysql80 -``` - -Linux中卸载MySQL: -```shell -sudo apt purge mysql-* -sudo rm -rf /etc/mysql/ /var/lib/mysql -sudo apt autoremove -sudo apt autoclean -``` - - -## 关系模型 - -关于表: -- 表的一行称为记录(Record),记录是一个逻辑意义上的数据。 -- 表的一列称为字段(Column),同一个表的每一行记录都拥有相同的若干字段。 -- 字段定义了数据类型,以及是否允许为`NULL`,`NULL`表示字段数据不存在。一个整型字段为`NULL`表示不存在而不是为0,其他同理。 -- 通常情况下应该避免允许`NULL`,不允许`NULL`可以简化查询条件,加快查询速度,应用程序读取数据后也不需要判断是否为`NULL`。 -- 关系数据库的表和表之间需要建立“一对多”,“多对一”和“一对一”的关系,这样才能够按照应用程序的逻辑来组织和存储数据。 -- 关系数据库中,关系是通过**主键**和**外键**来维护的。 - -主键: -- 一行是一条记录,一条记录由若干定义好的字段组成,同一个表的所有记录都拥有相同的字段定义。 -- 对于一张表而言,有一个很重要的约束,就是任意两条记录不能重复,不是指两条记录不能完全相同,而是要能够通过某个字段唯一区分出不同记录。这个字段成为**主键**。 -- 插入两条主键相同的记录是非法的。 -- 对主键的要求:记录一旦被插入到表中,主键最好不要再修改,因为主键是用来唯一定位用的,修改主键会造成一系列影响。 -- 如何选取主键将对业务开发产生重要的影响,像用户名、身份证号等唯一的编号似乎能够唯一定位记录,但也同样存在可能修改扩展等对业务产生的风险。 -- 所以,选取主键,非常重要的一个基本原则是:**不使用任何业务相关的字段作为主键**。比如身份证号、手机号、邮箱这种看起来可以唯一的字段,都不可以作为主键。 -- 一般将主键选为一个业务无关的字段,一般命名为`id`,常见的id可以有: - - 自增整数,数据库插入数据时自动为每一条记录分配一个自增整数。 - - 全局唯一的GUID:使用全局唯一的一个GUID(一个128的标识符)作为主键,类似于`8f55d96b-8acc-4636-8cb8-76bf8abc2f57`。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了GUID算法,可以自己预算出主键。 -- 大部分应用来说,自增类型主键就能满足需求,这里的主键使用`BIGINT NOT NULL AUTO_INCREMENT`,如果用`INT`因为是32位整数,最高到21亿还是有风险的(大部分情况下`INT`可能也够用了)。用`BIGINT`的话实际中不可能有那么多条记录。 -- 关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。 -- 对于联合主键,允许一列有重复,只要不是所有主键列都重复即可。实际上并不常用。 -- 没有必要的情况下,应该尽量不使用联合主键,因为提高了表的复杂度。 - -外键: -- 主键可以表示和记录的一对一关系。 -- 外键用来表示一种一对多的关系,比如一个学生有一个唯一的班级,就需要用一个键来表示,就可以用班级表的主键(班级的id)来表示。一个班中可以有多名学生,一个班级id唯一标识了一个班级,在学生表中就有班级id到记录或者说学生id的一对多关系。 -- 外键并不通过列名实现,而是通过定义外键约束`FOREIGN KEY`实现: -```sql -ALTER TABLE students -ADD CONSTRAINT fk_class_id -FOREIGN KEY (class_id) -REFERENCES classes (id); -``` -- `fk_class_id`是外键约束名称,可以随意,`FOREIGN KEY (class_id)`指定了外键为`calss_id`,`REFERENCES classes (id)`指定了这个外键将关联到`classes`表的`id`列。 -- 定义外键约束可以保证无法插入无效的数据,比如`class_id`为空或者不存在于表`classes`中的记录。 -- 外键约束会降低数据库性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,`class_id`仅仅是一个普通的列,只是它起到了外键的作用而已。 -- 删除一个外键约束(并不删除外键这一列,只是这一类不再是外键): -```sql -ALTER TABLE students -DROP FOREIGN KEY fk_class_id; -``` - -多对多: -- 主键可以一对一,外键一对多。有些时候,还需要定义多对多关系,多对多关系可以通过关联两个一对多关系实现。 -- 比如一个班级可能有多个老师,一个老师也可能教多个班级,已经有一个老师表`teachers`,一个班级表`classes`,可以新增一个`teacher_class`表,将所有老师教某个班级记录插入(两个外键分别是老师的`id`和班级的`id`),就形成了多对多关系。 -- `teachers` - -id|name -:-:|:-: -1|张老师 -2|王老师 -3|李老师 -4|赵老师 - -- `classes` - -id|name -:-:|:-: -1|一班 -2|二班 - -- `teacher_class` - -id|teacher_id|class_id -:-:|:-:|:-: -1|1|1 -2|1|2 -3|2|1 -4|2|2 -5|3|1 -6|4|2 - -一对一: -- 除了主键和记录的这种亲而易见的一对一,可能还需要一个表的记录对应到另一个表的唯一一个记录。 -- 比如每个学生都可以有自己的唯一联系方式,添加一个`contacts`保存学生id为外键,就可以实现一对一。准确地说是电话对应学生的一对一,而不是学生对应电话的一对一,因为学生有可能没有电话。如果是双向的一对一,那么两张表就可以合并为一张表了。业务允许也可以合并为一张表,然后没有电话的直接为`NULL`就行,但这样对查询性能可能不是很友好。 -- 一些应用会将一个大表拆分成两个一对一的表,目的可能只是为了将经常读取和不经常读取的字段分开,以获得更高性能。 -- 关系数据库通过外键可以实现一对多、多对多和一对一的关系。外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证。 - -索引: -- 在关系数据库中,如果有上万甚至上亿条记录,在查找记录的时候,想要获得非常快的速度,就需要使用索引。 -- 索引是关系数据库中对某一列或多列的值进行预排序的数据结构(就是用一部分列作为key做了散列)。通过使用散列可以大大加快查询速度。 -- 比如对`name`和`score`两列做索引: -```sql -ALTER TABLE students -ADD INDEX idx_name_score (name, score); -``` -- `ADD INDEX idx_name_score (name, score)`创建名为`idx_name_score`,使用列`name score`的索引。 -- 索引效率取决于索引项的值是否散列,值越不相同,索引效率越高,如果不同记录这一列存在大量相同的值,那么对该列创建索引意义不大。 -- 可以对一张表建立多个索引,索引的优点是提高了查询效率,缺点就是在插入、更新和删除记录时必须要同时修改索引。索引越多,插入更新和删除的速度就越慢。 -- 对于主键,关系数据库会自动对其创建主键索引,主键一定唯一,所以使用效率最高。 -- 设计数据表时,某些看上去唯一的列,比如身份证号、电话、邮箱等,因为具有业务含义,不用来做主键,但又因为具有唯一性约束,就可以给这种键添加一个唯一索引。查找时效率就会很高。 -- 通过`UNIQUE`关键字可以添加唯一索引,这时这一列同时具有了唯一性约束。 -```sql -ALTER TABLE students -ADD UNIQUE INDEX uni_email (email); -``` -- 如果只添加唯一性约束,而不创建索引,可以使用`ADD CONSTRAINT xxx UNIQUE`: -```sql -ALTER TABLE students -ADD CONSTRAINT uni_email UNIQUE (email); -``` -- 无论是否创建索引,对于用户和程序来说,使用关系数据库都不会有任何区别(透明)。索引仅仅是影响查询和修改效率,因此索引可以在使用数据库过程中逐步优化,比如随着用户增多感受到性能压力之后来建立索引。 - -## 查询数据 - -准备: -- 建立一个表,后续操作会以这个为基础。 -```sql --- init-test-data.sql --- 如果test数据库不存在,就创建test数据库: -CREATE DATABASE IF NOT EXISTS test; - --- 切换到test数据库 -USE test; - --- 删除classes表和students表(如果存在): -DROP TABLE IF EXISTS classes; -DROP TABLE IF EXISTS students; - --- 创建classes表: -CREATE TABLE classes ( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(100) NOT NULL, - PRIMARY KEY (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- 创建students表: -CREATE TABLE students ( - id BIGINT NOT NULL AUTO_INCREMENT, - class_id BIGINT NOT NULL, - name VARCHAR(100) NOT NULL, - gender VARCHAR(1) NOT NULL, - score INT NOT NULL, - PRIMARY KEY (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- 插入classes记录: -INSERT INTO classes(id, name) VALUES (1, '一班'); -INSERT INTO classes(id, name) VALUES (2, '二班'); -INSERT INTO classes(id, name) VALUES (3, '三班'); -INSERT INTO classes(id, name) VALUES (4, '四班'); - --- 插入students记录: -INSERT INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'M', 90); -INSERT INTO students (id, class_id, name, gender, score) VALUES (2, 1, '小红', 'F', 95); -INSERT INTO students (id, class_id, name, gender, score) VALUES (3, 1, '小军', 'M', 88); -INSERT INTO students (id, class_id, name, gender, score) VALUES (4, 1, '小米', 'F', 73); -INSERT INTO students (id, class_id, name, gender, score) VALUES (5, 2, '小白', 'F', 81); -INSERT INTO students (id, class_id, name, gender, score) VALUES (6, 2, '小兵', 'M', 55); -INSERT INTO students (id, class_id, name, gender, score) VALUES (7, 2, '小林', 'M', 85); -INSERT INTO students (id, class_id, name, gender, score) VALUES (8, 3, '小新', 'F', 91); -INSERT INTO students (id, class_id, name, gender, score) VALUES (9, 3, '小王', 'M', 89); -INSERT INTO students (id, class_id, name, gender, score) VALUES (10, 3, '小丽', 'F', 85); - --- OK: -SELECT 'ok' as 'result:'; -``` -- 在`mysql`的shell内执行`\. init-test-data.sql`会导入这个sql文件执行其中语句。 -- 新建`test`数据库并在其中创建出如下的表: -```shell -mysql> show tables; -+----------------+ -| Tables_in_test | -+----------------+ -| classes | -| students | -+----------------+ -2 rows in set (0.00 sec) - -mysql> select * from students; -+----+----------+------+--------+-------+ -| id | class_id | name | gender | score | -+----+----------+------+--------+-------+ -| 1 | 1 | 小明 | M | 90 | -| 2 | 1 | 小红 | F | 95 | -| 3 | 1 | 小军 | M | 88 | -| 4 | 1 | 小米 | F | 73 | -| 5 | 2 | 小白 | F | 81 | -| 6 | 2 | 小兵 | M | 55 | -| 7 | 2 | 小林 | M | 85 | -| 8 | 3 | 小新 | F | 91 | -| 9 | 3 | 小王 | M | 89 | -| 10 | 3 | 小丽 | F | 85 | -+----+----------+------+--------+-------+ -10 rows in set (0.00 sec) - -mysql> select * from classes; -+----+------+ -| id | name | -+----+------+ -| 1 | 一班 | -| 2 | 二班 | -| 3 | 三班 | -| 4 | 四班 | -+----+------+ -4 rows in set (0.00 sec) -``` -- 显示所有数据库:`show databses;` -- 切换到`test`数据库:`use test;` -- 显示数据库中的表:`show tables;` -- Windows中再Cmd中执行显示会乱码,在PowerShell中不会。数据都是同样的,显示问题,切换控制台字符页为65001未能解决。 - - -基本查询: -- 查询一张表中所有记录: -```sql -SELECT * FROM -``` -- `select`可以不跟`from`,比如`select 100 + 200;`则会得到`100+200`的结果。 - -条件查询: -- 根据某一个条件来查询: -```sql -SELECT * FROM WHERE ; -``` -- 条件可以是单一的条件比如:`score >= 80`,也可以是复合的条件:使用`AND OR NOT`。 -```sql - AND - OR -NOT -``` -- 要组合三个或以上条件,需要用括号`()`表明逻辑运算符的优先级。 -- 如果不加括号,则按照优先级进行运算:`NOT > AND > OR`。 -- 常用条件运算符: - -条件|表达式举例1|表达式举例2|说明 -:-|:-|:-|:- -使用=判断相等|score = 80|name = 'abc'|字符串需要用单引号括起来 -使用>判断大于|score > 80|name > 'abc'|字符串比较根据ASCII码,中文字符比较根据数据库设置 -使用>=判断大于或相等|score >= 80|name >= 'abc'| -使用<判断小于|score < 80|name <= 'abc'| -使用<=判断小于或相等|score <= 80|name <= 'abc'| -使用<>判断不相等|score <> 80|name <> 'abc'| -使用LIKE判断相似|`name LIKE 'ab%'`|`name LIKE '%bc%'`|%表示任意字符,例如`'ab%'`将匹配`'ab'`,`'abc'`,`'abcd'`,`%`表示0个到多个,`_`表1个。 - -投影查询: -- 如果只希望结果返回某些列数据,可以使用: -```sql -SELECT , , ..., FROM WHERE ; -``` -- 称之为投影查询,返回的结果集只会包含指定的列,结果集中列顺序可以和原表中不一致。还可以给每一列起一个别名,结果集显示的就是别名而不是原表中的列名。 -```sql -SELECT [AS] , [AS] <, ..., [AS] FROM WHERE ; -``` -- 列名和别名中间可以可以加`AS`可以不加。 - - -排序: -- 使用`SELECT`查询时,查询结果通常是按照主键排序的,如果要按照其他条件排序,可以使用`ORDER BY`子句。 -```sql -SELECT * from ORDER BY [DESC], [DESC], ...; -``` -- 默认是`ASC`升序排列可以省略,如果在要排序的列名后面加了`DESC`则会逆序也就是降序排列。如果有多个排序的列,那么先按照第一个排序,对于第一个列值相同的记录再按照第二列的值排序,以此类推。 -- 如果有`WHERE`子句,那么`ORDER BY`子句放到`WHERE`后面。 - -分页: -- 当结果集数据量很大时,比如几万条,放在一个页面显示就太大,就需要分页,分页其实就是从中截取出一部分记录。 -- 分页可以通过`LIMIT OFFSET `子句实现,放在`ORDER BY`后面,最多显示`record_num`数量的记录。 -```sql -SELECT * from ORDER BY ... LIMIT OFFSET ; -``` -- 比如得到按成绩排序的开始三条记录:`SELECT * FROM students ORDER BY score DESC LIMIT 3 OFFSET 0;` -- `OFFSET`是可选的,不加`OFFSET`相当于`OFFSET 0`。 -- MySQL中,`LIMIT OFFSET `可以写作`LIMIT , `。 -- 使用`LIMIT OFFSET `分页时,随着`N`越来越大,查询效率会越来越低。 - -聚合查询: -- SQL提供了专门的聚合函数,比如统计总数、计算平均数等,使用聚合函数进行查询,就是聚合查询,可以快速获得结果。 -- 查询一共有多少条记录:聚合结果虽然是一个整数,但是结果依然是一个二维表,只有一行一列,列别名是`COUNT(*)`。 -```SQL -SELECT COUNT(*) FROM students; -``` -- 使用聚合查询时应该给一个别名,便于处理结果:`SELECT COUNT(*) AS num FROM students`。 -- 其中`COUNT(*)`和`COUNT(id)`是一样的结果。 -- 聚合查询同样可以使用`WHERE`条件。 -- 其他聚合函数: - -函数|说明 -:-:|:-: -`SUM`|计算某一列的合计值,该列必须为数值类型 -`AVG`|计算某一列的平均值,该列必须为数值类型 -`MAX`|计算某一列的最大值 -`MIN`|计算某一列的最小值 - -- `MAX MIN`并不限于数值类型,会返回排序最后和最前的值。 -- 统计男生的平均成绩可以使用:`SELECT AVG(score) average FROM students WHERE gender = 'M'`。 -- 如果`WHERE`没有匹配到任何行,`COUNT()`返回0而`SUM() AVG() MAX() MIN()`返回`NULL`。 -- 对于聚合查询,还提供了**分组聚合**的功能。 -```sql -SELECT gender, COUNT(*) num FROM students GROUP BY gender; -``` -- `GROUP BY`子句指定了分组,执行`SELECT`前会先将表按照`gender`分组,再分别计算,所以会得到两行结果。 -``` -mysql> SELECT gender, COUNT(*) num FROM students GROUP BY gender; -+--------+-----+ -| gender | num | -+--------+-----+ -| M | 5 | -| F | 5 | -+--------+-----+ -2 rows in set (0.00 sec) -``` -- 上述一条结果代表了表中的多条记录,`sql_mode`变量中有没有`ONLY_FULL_GROUP_BY`规则将决定查询结果中有不是分组的列时的效果,有的话将报错,没有的话将会显示多条记录中第一条的数据。 -``` -mysql> set @@sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY'; -Query OK, 0 rows affected, 1 warning (0.00 sec) - -mysql> select @@sql_mode; -+---------------------------------------------------------------+ -| @@sql_mode | -+---------------------------------------------------------------+ -| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION | -+---------------------------------------------------------------+ -1 row in set (0.00 sec) - -mysql> SELECT name, gender, COUNT(*) num FROM students GROUP BY gender; -ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.students.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by -``` -- 也可以使用多个列进行分组,均不同才会看做一组。 -```sql -SELECT class_id, gender, COUNT(*) num FROM students GROUP BY class_id, gender; -``` -- 结果: -``` -mysql> SELECT class_id, gender, COUNT(*) num FROM students GROUP BY class_id, gender; -+----------+--------+-----+ -| class_id | gender | num | -+----------+--------+-----+ -| 1 | M | 2 | -| 1 | F | 2 | -| 2 | F | 1 | -| 2 | M | 2 | -| 3 | F | 2 | -| 3 | M | 1 | -+----------+--------+-----+ -6 rows in set (0.00 sec) -``` -- 例子: -``` -mysql> SELECT class_id, AVG(score) avg_score FROM students GROUP BY class_id; -+----------+-----------+ -| class_id | avg_score | -+----------+-----------+ -| 1 | 86.5000 | -| 2 | 73.6667 | -| 3 | 88.3333 | -+----------+-----------+ -3 rows in set (0.00 sec) - -mysql> SELECT class_id, gender, COUNT(*) num, AVG(score) avg_score FROM students GROUP BY class_id, gender; -+----------+--------+-----+-----------+ -| class_id | gender | num | avg_score | -+----------+--------+-----+-----------+ -| 1 | M | 2 | 89.0000 | -| 1 | F | 2 | 84.0000 | -| 2 | F | 1 | 81.0000 | -| 2 | M | 2 | 70.0000 | -| 3 | F | 2 | 88.0000 | -| 3 | M | 1 | 89.0000 | -+----------+--------+-----+-----------+ -6 rows in set (0.00 sec) -``` - -多表查询: -- 除了从一张表查数据,还可以从多张表查数据: -```sql -SELECT * from classes, students; -``` -- 查询结果也是一张二维表,是两张表的乘积,具体来说就是将两张表的记录做了一个笛卡尔积,两张表的记录的列叠加到一起,行直接笛卡尔积全排列。 -- 多表查询也称笛卡尔积查询。比如上述结果中会有两列`id`,分别来自两张表。要解决这个问题,可以使用投影查询设置列的别名。 -```sql -SELECT - students.id sid, - students.name, - students.gender, - students.score, - classes.id cid, - classes.name cname -FROM students, classes; -``` -- 多表查询时可以使用`table_name.column_name`这样的方式引用列和设置列的别名。这种方式也还是有点麻烦,SQL还允许给表设置别名。 -```sql -SELECT - s.id sid, - s.name, - s.gender, - s.score, - c.id cid, - c.name cname -FROM students s, classes c; -``` -- 上述结果中某些记录是不合理没有意义的,因为`class_id`已经代表了班级,添加`WHERE c.id = s.class_id`条件可以得到合理的查询结果。 -- 多表查询会做笛卡尔积,结果集数量可能很大,要小心使用,一般情况下不使用,因为会有效率问题。 - -连接查询: -- 另一种类型的多表查询,连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地“连接”在主表结果集上。 -- 连接查询使用`INNER JOIN`子句: -```sql -SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score -FROM students s -INNER JOIN classes c -ON s.class_id = c.id; -``` -- `INNER JOIN`写法: - - 确定主表`FROM table1` - - 确定要连接的表`INNER JOIN table2` - - 确定连接条件,`ON `,这里使用`ON s.class_id = c.id`表示`students`表的`class_id`列与`classes`表的`id`列相同的行需要连接。 - - 加上可选的`WHERE ORDER BY`等子句。 -- `INNER JOIN`是内连接,同样还有`OUTER JOIN`外连接。 -- 外连接分三种:`RIGHT OUTER JOIN` `LEFT OUTER JOIN` `FULL OUTER JOIN`。 - - 内连接`INNER JOIN`只返回同时存在于两张表的行数据 - - 右连接`RIGHT OUTER JOIN`返回右表中都存在的行。如果某一行仅在右表存在,那么结果集就会以`NULL`填充剩下的字段。 - - 左连接`LEFT OUTER JOIN`则返回左表都存在的行。如果某一行仅在左表存在,那么结果集中同样以`NULL`填充右表中的对应列。 - - 全连接`FULL OUTER JOIN`则会将两张表中所有记录都选择出来,自动把对方不存在的列填充`NULL`。 - - 用集合来表示的话,左表记录集合为A,右表为B,`INNER`就是AB交集,`LEFT`是AB交集与A并集,`RIHGT`是AB交集与B并集,`FULL`是AB并集。 -- `JOIN`查询依然可以使用`WHERE`条件`ORDRE BY`排序。 -- `INNER JOIN`是最常用的`JOIN`查询,`INNER JOIN`也可以直接写作`JOIN`。 -- MySQL中不能用`FULL OUTER JOIN`全连接,可以通过对左右连接的结果`UNION`取并集实现。 -- `RIGHT OUTER JOIN` `LEFT OUTER JOIN` `FULL OUTER JOIN`中的`OUTER`可以省略,写作`RIGHT JOIN` `LEFT JION` `FULL JION`。 - -## 修改数据 - -数据库的基本操作就是CRUD即Create、Retrieve、Update、Delete。 - -除了查询,增删改对应的SQL是: -- `INSERT` 插入新纪录。 -- `UPDATE` 更新已有记录。 -- `DELETE` 删除已有记录。 - -插入: -- 基本语法: -```sql -INSERT INTO (field1, field2, ..., fieldn) VALUES (value1, value2, ... valuen) [, (value21, ...)] ; -``` -- 如果一个字段有默认值,比如一个自增的主键,那么`INSERT`语句中可以不出现。 -- 字段顺序不必和数据库中字段顺序一致,值顺序必须和字段顺序一致。 -- 可以一次性插入多条记录,数据用`,`分隔,每条记录的值放在`()`中。 - -更新: -- 语法: -```sql -UPDATE SET field1=value1, field2=value2, ... WHERE ; -``` -- 可以根据条件同时更新多条记录。 -- 设置值时可以使用原值比如`SET score = score + 10`。 -- 如果`WHERE`没有匹配到任何记录,`UPDATE`不会报错,也不会更新任何记录。 -- 使用MySQL这类数据库,`UPDATE`语句会返回更新的行数以及`WHERE`条件匹配的行数。 -```shell -mysql> update classes set name = '六班' where id = 6; -Query OK, 1 row affected (0.00 sec) -Rows matched: 1 Changed: 1 Warnings: 0 -``` - -删除: -- 语法: -```sql -DELETE FROM WHERE ; -``` -- 一次可以删除一条或者多条记录。 -- 如果`WHERE`条件没有匹配到任何记录,`DELETE`语句不会报错,也不会有任何记录被删除。 -- 注意如果不带`WHERE`条件,那么所有记录都匹配,整张表中所有记录都会删除。所以删除之前最好先用`SELECT`语句测试`WHERE`条件筛选的结果集是否符合期望。 -- 使用MySQL这类关系数据库时,`DELETE`语句也会返回删除的行数以及`WHERE`条件匹配的行数。 -- 如果一个记录有外键: - - 创建外键时定义了`ON DELETE CASCADE`,删除记录时关联数据自动删除。 - - 如果没有定义`ON DELETE CASCADE`,有关联数据时删除会报错。 - -基本的增删改查大概就这些了。 - -## MySQL - -安装完MySQL后,除了真正的MySQL服务器(MySQL Server),还有一个MySQL Client,是一个命令行客户端。通过客户端可以登录MySQL,输入`mysql -u root -p`,输入密码后,就连上了MySQL Server,进入MySQL的提示符。 -- MySQL Client执行的程序是`mysql`,MySQL Server执行的程序是`mysqld`。 -- MySQL Client是通过TCP连接连接到MySQL Server的。默认端口是3306,如果是连接到本机MySQL Server那么地址就是`127.0.0.1:3306`。 -- 在MySQL Client中输入的SQL语句被发送到MySQL Server中执行。 -- 也可以只装MySQL Client,然后连接到远程的MySQL Server,假设远程MySQL Server的地址是`10.0.1.99`,那么可以用`-h`选项指定IP或者域名:`mysql -h 10.0.1.99 -u root -p`。 -- 在Windows或者Linux中,命令行执行的`mysql`就是客户端,MySQL服务器以服务的方式运行。 -- 可以有多个MySQL Client连接到同一个MySQL Server。 - -可视化终端: -- 除了使用命令行客户端连接MySQL Server,也可以使用可视化图形界面终端[MySQL Workbench](https://dev.mysql.com/downloads/workbench/)。 -- MySQL Workbench可以用可视化的方式查询、创建和修改数据库表,但是,归根到底,MySQL Workbench是一个图形客户端,它对MySQL的操作仍然是发送SQL语句并执行。 -- 虽然可以图形界面管理数据库,但是很多时候通过SSH远程连接,只能使用SQL命令,了解SQL是必要的。 - -数据库常用命令: -- 列出所有数据库:`SHOW DATABASES;` -- 其中,`information_schema`、`mysql`、`performance_schema`和`sys`是系统库,不要去改动它们。其他的是用户创建的数据库。 -- 创建数据库:`CREATE DATABASE test;` -- 删除数据库:`DROP DATABASE test;` -- 删除一个数据库将导致该数据库的所有表全部被删除。 -- 对一个数据库进行操作前,需要先将其切换为当前数据库:`USE test;` -- 列出当前数据库所有表:`SHOW TABLES;` -- 查看表的结构:`DESC
;` - - 可以获得的信息:字段名称、数据类型、是否允许NULL、键的约束(主键外键唯一等)、默认值,额外约束(比如自增)。 -- 查看创建表的SQL语句:`SHOW CREATE TABLE
;` -- 创建表:`CREATE TABLE
;` -- 删除表:`DROP TABLE
;` -- 修改表:`ALTER TABLE
...` - - 新增一列:`... ADD COLUMN birth VARCHAR(10) NOT NULL;` - - 修改某一列:`... CHANGE COLUMN birth birthday VARCHAR(20) NOT NULL;` - - 删除列:`... DROP COLUMN birthday;` - - 添加约束:`... ADD CONSTRAINT ...;` - - 删除约束:`... DROP CONSTRAINT ;` - - 添加外键约束:`... ADD CONSTRAINT FOREIGN KEY () REFERENCES ();` - - 删除外键约束:`... DROP FOREIGN KEY ` -- 退出MySQL客户端:`exit`。 -- 更多常用命令TODO,待实践后补充。 - -## MySQL实用SQL语句 - -在编写SQL时,灵活运用一些技巧,可以大大简化程序逻辑。一些常用的SQL语句,其他数据库不一定有: - -插入或替换: -```SQL -REPLACE INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99); -``` -- 若`id = 1`的记录不存在,则直接插入,如果存在,则先删除在插入新纪录。 - -插入或更新: -```SQL -INSERT INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99) -ON DUPLICATE KEY UPDATE name='小明', gender='F', score=99; -``` -- 使用`INSERT INTO ... ON DUPLICATE KEY UPDATE ...` -- 若`id = 1`的记录不存在,则直接插入,否则,更新当前记录,更新的字段由`UPDATE`指定。 - -插入或忽略: -```SQL -INSERT IGNORE INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99); -``` -- 使用`INSERT IGNORE INTO ...`语句。 -- 若`id = 1`记录不存在,则插入,否则直接忽略什么事也不做。 - -对表做快照: -- 即复制一个表的数据到一个新表,可以结合`CREATE TABLE`和`SELECT`: -```sql -CREATE TABLE students_of_class1 SELECT * FROM students WHERE class_id=1; -``` -- 新创建的表结构和SELECT使用的表结构完全一致。 - -将查询结果集写入表: -- 可以创建表后结合`INSERT INTO`和`SELECT`直接写入: -```sql -CREATE TABLE statistics ( - id BIGINT NOT NULL AUTO_INCREMENT, - class_id BIGINT NOT NULL, - average DOUBLE NOT NULL, - PRIMARY KEY (id) -); -INSERT INTO statistics (class_id, average) SELECT class_id, AVG(score) FROM students GROUP BY class_id; -``` -- 确保`INSERT`语句的列和`SELECT`语句的列能一一对应,就可以在statistics表中直接保存查询的结果。 - -强制使用索引: -- 在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。 -- 但很多时候数据库的查询优化器并不一定总是能使用最优索引,如果知道如何选择索引,就可以使用`FORCE INDEX`强制查询使用的索引: -```sql -SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC; -``` -- 指定索引需要索引存在,不要将索引名称指定为与某一列名称相同,会导致冲突找索引名称时会找不到。推荐使用`idx_`这种名称。 - - -## 事务 - -事务: -- 在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。也就是整个操作必须是原子的,如果一系列操作中途某条语句执行失败,已经执行的语句就必须全部撤销。 -- 这种将多条语句作为一个整体操作的功能,成为**数据库事务**。 -- 数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。 -- 数据库事务具有ACID这4个特性: - - A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行。 - - C:Consistent,一致性,事务完成后,所有数据状态是一致的。 - - I:Isolation,隔离性,如果有多个事务并行执行,那么事务所做的修改必须与其他事务隔离。 - - D:Duration,持久性,事务完成后,对数据库数据的修改被持久化存储。 -- 对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为**隐式事务**。 -- 手动将多条SQL作为一个事务执行:使用`BEGIN`开启事务,`COMMIT`提交一个事务,这种称之为**显式事务**。 -```sql -BEGIN; -UPDATE accounts SET balance = balance - 100 WHERE id = 1; -UPDATE accounts SET balance = balance + 100 WHERE id = 2; -COMMIT; -``` -- 多条语句要作为事务执行,就必须使用显式事务。 -- `COMMIT`是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果`COMMIT`语句执行失败了,整个事务也会失败。 -- 某些场景,如果我们希望事务执行主动失败,可以使用`ROLLBACK`回滚事务。 -```sql -BEGIN; -UPDATE accounts SET balance = balance - 100 WHERE id = 1; -UPDATE accounts SET balance = balance + 100 WHERE id = 2; -ROLLBACK; -``` -- 数据库事务是数据库系统保证的,只需要按照业务逻辑使用即可。 - -隔离级别: -- 对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能就会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。 -- SQL标准定义了4中隔离级别,其可能产生的: - -Isolation Level|脏读(Dirty Read)|不可重复读(Non Repeatable Read)|幻读(Phantom Read) -:-:|:-:|:-:|:-: -Read Uncommitted|Yes|Yes|Yes -Read Committed|-|Yes|Yes -Repeatable Read|-|-|Yes -Serializable|-|-|- - -实际使用事务的用法类似(Java示例):如果事务中的SQL执行失败,立即执行`ROLLBACK`。 -```java -try { - executeUpdate("..."); - executeUpdate("..."); - commit(); -} catch (SQLException e) { - rollback(); -} -``` - -**Read Uncommitted**: -- Read Uncommitted是隔离级别最低的一种事务级别。在这种隔离级别下,**一个事务会读到另一个事务更新后但未提交的数据**,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是**脏读(Dirty Read)**。 - -**Read Committed**: -- 在Read Committed隔离级别下,一个事务可能会遇到**不可重复读(Non Repeatable Read)**的问题。 -- 不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务已经开始执行并结束提交恰好修改了这个数据,那么,在第一个事务中,**两次读取的数据就可能不一致(就不可重复读)**。 - -**Repeatable Read**: -- 在Repeatable Read隔离级别下,一个事务可能会遇到 **幻读(Phantom Read)** 的问题。 -- 幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。 -- 幻读就是没有读到的记录,以为不存在,但其实是可以更新成功的,并且,更新成功后,再次读取,就出现了。原因是其他事务在中途插入了这条数据,但插入后的时刻去读取结果是不存在,因为是**可重复读,但允许更新,经过更新后,就能够读取**到了。 - -**Serializable**: -- Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。 -- 虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。 - -默认事务隔离级别: -- 如果没有指定隔离级别,数据库就会使用默认的隔离级别。在MySQL中,如果使用InnoDB,默认的隔离级别是Repeatable Read。 - -除了靠数据库系统本身提供的隔离级别,还需要应用程序的逻辑处理脏读、不可重复读、幻读的情况。或者某些场景下可能就不需要处理,脏读、不可重复读、幻读本身可能就是符合业务逻辑的。 - -MySQL获取修改事务隔离级别: - -- 查询全局和会话的事务隔离级别,MySQL8.0.3版本以前是`tx_isolation`: -```sql -SELECT @@global.transaction_isolation; -SELECT @@session.transaction_isolation; -``` -- 修改: -```sql -SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} -``` -- 影响范围: - - SESSION:表示修改的事务隔离级别将应用于当前 session(当前 cmd 窗口)内的所有事务。 - - GLOBAL:表示修改的事务隔离级别将应用于所有 session(全局)中的所有事务,且当前已经存在的 session 不受影响。 - - 如果省略 SESSION 和 GLOBAL,表示修改的事务隔离级别将应用于当前 session 内的下一个还未开始的事务。 - - 任何用户都能改变会话的事务隔离级别,但是只有拥有 SUPER 权限的用户才能改变全局的事务隔离级别。 -- 也可以用`set transaction_isolation`设置当前session的事务隔离级别: -```sql -SET transaction_isolation='READ-COMMITTED'; -``` -- 也可以通过配置文件修改隔离级别: -```sql -transaction-isolation = REPEATABLE-READ -transaction-isolation = READ-COMMITTED -transaction-isolation = READ-UNCOMMITTED -transaction-isolation = SERIALIZABLE -``` - -## TODO - -- 更多SQL语法。 -- 更严谨的SQL语法描述。 -- SQL标准与数据库实现的区分。 -- 具体数据库实现相关内容,其他数据库系统内容。 -- 更多数据库系统使用与配置内容。 -- 编程语言与数据库的交互。 -- SQL编码风格。 -- 待有实践需求后补充。 \ No newline at end of file diff --git a/Scala.md b/Scala.md deleted file mode 100644 index c310486..0000000 --- a/Scala.md +++ /dev/null @@ -1,3134 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Scala语言入门](#scala%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8) - - [环境配置](#%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [IDEA环境配置](#idea%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) - - [变量与数据类型](#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) - - [运算符](#%E8%BF%90%E7%AE%97%E7%AC%A6) - - [控制流](#%E6%8E%A7%E5%88%B6%E6%B5%81) - - [函数式编程](#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B) - - [包管理](#%E5%8C%85%E7%AE%A1%E7%90%86) - - [面向对象](#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - - [集合](#%E9%9B%86%E5%90%88) - - [模式匹配](#%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D) - - [异常处理](#%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86) - - [隐式转换](#%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2) - - [泛型](#%E6%B3%9B%E5%9E%8B) - - [Style Guide](#style-guide) - - [sbt](#sbt) - - [通过案例入门sbt](#%E9%80%9A%E8%BF%87%E6%A1%88%E4%BE%8B%E5%85%A5%E9%97%A8sbt) - - [sbt使用](#sbt%E4%BD%BF%E7%94%A8) - - [build.sbt](#buildsbt) - - [多项目构建](#%E5%A4%9A%E9%A1%B9%E7%9B%AE%E6%9E%84%E5%BB%BA) - - [任务图](#%E4%BB%BB%E5%8A%A1%E5%9B%BE) - - [更多内容](#%E6%9B%B4%E5%A4%9A%E5%86%85%E5%AE%B9) - - [并发编程](#%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B) - - [总结](#%E6%80%BB%E7%BB%93) - - - -# Scala语言入门 - -Scala(发音为/ˈskɑːlə, ˈskeɪlə/)是一门多范式的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。洛桑联邦理工学院的Martin Odersky于2001年基于Funnel的工作开始设计Scala。Java平台的Scala于2003年底/2004年初发布。 - -特点: -- 同样运行在JVM上,可以与现存程序同时运行。 -- 可直接使用Java类库。 -- 同Java一样静态类型。 -- 语法和Java类似,比Java更加简洁(简洁而并不是简单),表达性更强。 -- 同时支持面向对象、函数式编程。 -- 比Java更面向对象。 - -关注点: -- 类型推断、不变量、函数式编程、高级程序构造。 -- 并发:actor模型。 -- 和现有Java代码交互、相比Java异同和优缺。 - -和Java关系: -``` - javac java -.java --------> .class ----------> run on JVM -.scala -------> .class ----------> run on JVM - scalac scala -``` - -测试代码:[Scala分支](../../tree/Scala/) - -阅读: -- [尚硅谷大数据技术之Scala入门到精通教程](https://www.bilibili.com/video/BV1Xh411S7bP)(本文参考) -- [Scala官网语法速查](https://docs.scala-lang.org/zh-cn/cheatsheets/index.html) -- [Scala官方文档 Tour Of Scala](https://docs.scala-lang.org/zh-cn/tour/tour-of-scala.html) - -## 环境配置 - -Scala需要依赖Java,访问[这里](https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html)查看特定Scala版本依赖的Java版本。这里选择,最新的JDK17配合Scala2.13.6。 - -Windows中下载安装配置环境变量: -- 类似于java配置`SCALA_HOME`为安装目录。 -- 添加`%SCALA_HOME%\bin`到path环境变量。 - -Linux中类似,可以使用包管理器,但如果依赖版本不严格一致的话,需要官网下载对应版本安装即可。 - -也有交互式执行环境: -``` -scala -``` - -交互式执行环境中的传统艺能: -```scala -println("hello,world!") -``` - -暂时不管项目配置,还是单文件编译执行为主,项目开发肯定要以包的形式组织可以使用IntelliJ IDEA开发,使用maven或者sbt进行项目配置。 - -使用VSCode编辑器,安装插件Scala Syntax (official)和Scala (Mentals)。 - -新建文件`HelloScala.scala`。 -```scala -object HelloScala { // HelloScala is a object, not a class, will create a - def main(args : Array[String]) : Unit = { - println("hello,world!"); - } -} -``` - -可以使用插件CodeRunner直接快捷键运行。也可以在命令行编译为字节码后再运行: -``` -scalac HelloScala.scala -scala helloScala -``` -或者直接运行scala源文件: -``` -scala HelloScala.scala -``` -和java如出一辙。 - -如果编译的话会生成2个`.class`字节码文件,`HelloScala.class`和`HelloScala$.class`。都是字节码但是不能通过`java`直接运行。但对于HelloWorld这个例子来说,java源代码编译而成的字节码是可以通过`scala`命令运行的。 - -原因是没有引入Scala的库,添加`classpath`就可以通过java执行scala编译成的字节码了: -``` -java -cp %SCALA_HOME%/lib/scala-library.jar; HelloScala -``` -使用[Java Decompiler](http://java-decompiler.github.io/)反编译字节码到java源文件可以看到引入Scala库的逻辑。并且: -- scala源文件中的`HelloScala`对象编译后成为了一个类,但对象本身编译后就是生成的另一个类`HelloScala$`类的单例对象`HelloScala$.MODULE$`,称之为伴生对象。 -- `HelloScala$`有一个`main`实例方法,`HelloScala`类的静态方法通过这个单例对象转调这个实例方法。完成打印。 -- Scala比Java更面向对象。 - -## IDEA环境配置 - -使用IntelliJ IDEA: -- 创建Maven项目,JDK版本17。 -- 安装插件:Scala。一般默认都已经装了。 -- Maven项目默认用Java写,在`main/`目录下新建目录`scala/`,然后将目录标记为Source Root。 -- 这样甚至可以在同一个项目中混用Scala和Java源文件,并互相调用。 -- 需要能够添加scala源文件,右键项目,添加框架支持,配置Scala SDK,选择,然后就可以右键添加Scala源文件了。 -- 添加包,添加Scala类,选择对象,编辑源码。 -```scala -package VeryStarted - -object HelloWorld { - def main(args: Array[String]): Unit= { - println("Hello,world!") - } -} -``` -- Ctrl + Shift + F10运行。 -- 可以看到执行的命令是`java`在`classpath`中引入了Scala的`jar`包形式的库。 -- 调用java的类库: -```scala -package VeryStarted - -object HelloWorld { - def main(args: Array[String]): Unit= { - println("Hello,world!") - System.out.println("Hello,world! from java") - } -} -``` - -语法含义: -```scala -object SingletonObject { body } -def MethodName(ArgName: ArgType): RetType = { body } -``` - -`object`关键字创建的伴生对象,可以理解为替代Java的`static`关键字的方式,将静态方法用单例对象的实例方法做了替代,做到了更纯粹的面向对象。 - -仅仅测试理解语法既可以单文件编写VSCode编译执行也可以用IDEA管理项目,影响不大。 - -再用一个等价的类定义来认识和区别一下Scala和Java: - -java: -```java -public class Student { - private String name; - private Integer age; - private static String school = "XDU"; - - public Student(String name, Integer age) { - this.name = name; - this.age = age; - } - - public void printInfo() { - System.out.println(this.name + " " + this.age + " " + Student.school); - } - - // psvm - public static void main(String[] args) { - Student tch = new Student("tch", 20); - tch.printInfo(); - } -} -``` -scala: -```scala -package VeryStarted - -class Student(name: String, age: Int) { - def printInfo(): Unit = { - println(name + " " + age + " " + Student.school) - } -} - -// 引入伴生对象,名称一致,同一个文件 -object Student { - val school: String = "XDU" - - def main(args: Array[String]): Unit = { - val tch = new Student("tch", 20) - tch.printInfo() - } -} -``` - -Scala库源码与API文档: -- 官网上下载下载Scala源码,解压到Scala安装目录或任意位置。 -- IntelliJ IDEA进入反编译的源码后选择右上角附加源码,选择源码的`src/`目录。 -- 安装包里面已经有了文档,没有的话可以单独下载。 -- Scala依赖Java,某些类型就是Java的包装,库中有一部分java源码。 - -## 变量与数据类型 - -注释: -- 和java一样 -- `//` 单行 -- `/* */` 多行 -- `/** */` 文档,方法或者类前面,便于`scaladoc`生成文档。 - -变量和常量: -```scala -var name [:VariableType] = value // variable -val name [:ConstantType] = value // constant -``` -因为Scala的函数式编程要素,所以一个指导意见就是能用常量就不要用变量。 -- 声明变量时,类型可以省略,编译器会自动推导。 -- 静态类型,类型经过给定或推导确定后就不能修改。 -- 变量和常量声明时,必须有初始值。 -- 变量可变,常量不可变。 -- 引用类型常量,不能改变常量指向的对象,可以改变对象的字段。 -- 不以`;`作为语句结尾,scala编译器自动识别语句结尾。 - -标识符命名规范: -- 字母下划线开头,后跟字母数字下划线,和C/C++/Java一样。 -- 操作符开头,且只包含(+-*/#!等),也是有效的标识符。这样用会用什么奇怪的好处吗?答案是灵活到天顶星的运算符重载。 -- 用反引号包括的任意字符串,即使是同39个Scala关键字同名也可以。有点奇怪的用法,尚不知道为什么。 - -```scala -var _abc:String = "hello" -val -+/%# = 10 -val `if` = 10 -println(_abc) -println(-+/%#) -println(`if`) -``` - -关键字: -- `package import class obejct trait extends with type for` -- `private protected abstract sealed final implicit lazy override` -- `try catch finlly throw` -- `if else match case do while for return yield` -- `def var val` -- `this super` -- `new` -- `true false null` -- 其中Java没有的关键字:`object trait with implicit match yield def val var` - -字符串: -- 类型:`String` -- `+`号连接 -- `*`字符串乘法,复制一个字符串多次 -- `printf`格式化输出 -- 字符串插值:`s"xxx${varname}"`前缀`s`模板字符串,前缀`f`格式化模板字符串,通过`$`获取变量值,`%`后跟格式化字符串。 -- 原始字符串:`raw"rawstringcontents${var}"`,不会考虑后跟的格式化字符串。 -- 多行字符串:`""" """`。 -- 输出:`print printf println ...` -```scala -val name: String = "Pyrrha" + " " + "Nikos" -val age = 17 -println((name + " ") * 3) -printf("%s : dead in %d\n", name, age) -print(s"$name : dead in ${age}") -val power = 98.9072 -println(f" : power ${power}%.2f.") - -var sql = s""" - |Select * - |from - | Student - |Where - | name = ${name} - |and - | age >= ${age} -""".stripMargin // strip | and whitespaces before | -println(sql) -``` - -输入: -- `StdIn.readLine()` -- `StdIn.readShort() StdIn.readDouble` -- `import scala.io.StdIn` -```scala -println("input name:") -val name: String = StdIn.readLine() -println("input age:") -val age:Int = StdIn.readInt() -println(name + " : " + age) -``` - -读写文件: -```scala -import scala.io.Source -import java.io.PrintWriter -import java.io.File -object FileIO { - def main(args: Array[String]): Unit ={ - // read from file - Source.fromFile("FileIO.txt").foreach(print) - - // write to file - // call java API to write - val writer = new PrintWriter(new File("WFile.txt")) - writer.write("Nephren!") - writer.close() - } -} -``` - -数据类型: -- java基本类型`char byte short int long float double boolean`。 -- java基本类型对应包装类型:`Charater Byte Short Integer Long Float Double Boolean`。 -- java中不是纯粹的面向对象。 -- Scala吸取了这一点,所有数据都是对象,都是`Any`的子类。 -- `Any`有两个子类:`AnyVal`值类型 `AnyRef`引用类型。 -- 数值类型都是`AnyVal`子类,和Java数值包装类型都一样,只有整数在scala中是`Int`、字符是`Char`有点区别。 -- `StringOps`是java中`String`类增强,`AnyVal`子类。 -- `Unit`对应java中的`void`,`AnyVal`子类。用于方法返回值的位置,表示方法无返回值,`Unit`是一个类型,只有一个单例的对象,转成字符串打印出来为`()`。 -- `Void`不是数据类型,只是一个关键字。 -- `Null`是一个类型,只有一个单例对象`null`就是空引用,所有引用类型`AnyRef`的子类,这个类型主要用途是与其他JVM语言互操作,几乎不在Scala代码中使用。 -- `Nothing`所有类型的子类型,也称为底部类型。它常见的用途是发出终止信号,例如抛出异常、程序退出或无限循环。 - - -整数类型:都是有符号整数,标准补码表示。 -- `Byte` 1字节 -- `Short` 2字节 -- `Int` 4字节 -- `Long` 8字节 -- 整数赋初值超出表示范围报错。 -- 自动类型推断,整数字面值默认类型`Int`,长整型字面值必须加`L`后缀表示。 -- 直接向下转换会失败,需要使用强制类型转换,`(a + 10).toByte`。 - -浮点类型: -- `Float` IEEE 754 32位浮点数 -- `Double` IEEE 754 64位浮点数 -- 字面值默认`Double` - -字符类型: -- 同java的`Character`,2字节,UTF-16编码的字符。 -- 字符常量:`''` -- 类型`Char` -- 转义:`\t \n \r \\ \" \'`etc - -布尔类型:`true false` - -空类型: -- `Unit` 无值,只有一个实例,用于函数返回值。 -- `Null` 只有一个实例`null`,空引用。 -- `Nothing` 确定没有正常的返回值,可以用Nothing来指定返回值类型。好像意思是抛异常时返回Nothing,不是特别懂。 -```scala -object NullType { - def main(arg : Array[String]) : Unit = { - // Unit - def f1(): Unit = { - println("just nothing!") - } - val a = f1() - println(a) // () - - // null only used for AnyRef - // val n:Int = null // invalid - } -} -``` - -数据类型转换: -- 自动类型提升:多种数据类型混合运算,自动提升到精度最大的数据类型。 -- 高精度赋值到低精度,直接报错。 -- 除了图中的隐式类型转换,都需要强制类型转换。 -- `Byte Short Char`计算时会直接提升为`Int`。 -- `Boolean`不能参与整数浮点运算,不能隐式转换为整数。 -![Scala中的数据类型转换](Images/Scala_implicit_datatype_cast.jpg) - -强制类型转换: -- `toByte toInt toChar toXXXX` -- `'a'.toInt` `2.7.toInt` -- 数值与String的转换:`"" + n` `"100".toInt` `"12.3".toFloat` `12.3".toDouble.toInt` -- 整数强转是二进制截取,整数高精度转低精度可能会溢出,比如`128.toByte`。 - -Scala标准库: -- `Int` `Double`这些数据类型对应于Java中的原始数据类型,在底层的运行时不是一个对象,但Scala提供了从这些类型到`scala.runtime.RichInt/RichDouble/...`的(低优先级)隐式类型转换(在`Perdef`中定义),从而提供了非原始类型具有的对象操作。 -- 基本类型都是默认导入的,不需要显式导入,位于包`scala`中。还有`scala.Predef`对象也是自动导入。 -- 其他需要导入的包: - - `scala.collection`集合。 - - `scala.collection.immutable`不可变数据结构,比如数组、列表、范围、哈希表、哈希集合。 - - `scala.collection.mutable`可变数据结构,数组缓冲、字符串构建器、哈希表、哈希集合。 - - `scala.collection.concurrent`可变并发数据结构,比如字典树。 -- `scala.concurrent`原始的并发编程。 -- `scala.io`输入输出。 -- `scala.math`基本数学操作。 -- `scala.sys`操作系统交互。 -- `scala.util.matching`正则。 -- 标准库中的其他部分被放在独立的分开的库中。可能需要单独安装,包括: -- `scala.reflect`反射API。 -- `scala.xml`xml解析、操作、序列化。 -- `scala.collection.parallel`并行集合。 -- `scala.util.parsing` parser的组合子,什么东西? -- `scala.swing`java的GUI框架Swing的封装。 -- 定义了一些别名给常用的类,比如`List`是`scala.collection.immutable.List`的别名,也可以理解为默认导入? -- 其他别名可能是底层平台JVM提供的,比如`String`是`java.lang.String`的别名。 - -## 运算符 - -运算符: -- 和Java基本相同。 -- 算术运算:`+ - * / %` ,`+`可以用于一元正号,二元加号,还可以用作字符串加法,取模也可用于浮点数。没有自增和自减语法`++ --`。 -- 关系运算:`== != < > <= >=` -- 逻辑运算:`&& || !`, `&& ||`所有语言都支持短路求值,scala也不例外。 -- 赋值运算:`= += -= *= /= %=` -- 按位运算:`& | ^ ~` -- 移位运算:`<< >> >>>`,其中`<< >>`是有符号左移和右移,`>>>`无符号右移。 -- scala中所有运算符本质都是对象的方法调用,拥有比C++更灵活的运算符重载。 - -自定义运算符: -- Scala中运算符即是方法,任何具有单个参数的方法都可以用作**中缀运算符**,写作中缀表达式的写法。`10.+(1)`即是`10 + 1`。 -- 定义时将合法的运算符(只有特殊符号构成的标识符)作为函数名称即可定义。 - -运算符优先级: -- 当一个表达式使用多个运算符时,将**根据运算符的第一个字符来评估优先级**。内置的运算符和自定义运算符都是函数,遵守同样的规则。 -```scala -(characters not shown below) -* / % -+ - -: -= ! -< > -& -^ -| -(all letters, $, _) -``` -- 比如下面两个表示等价: -```scala -a + b ^? c ?^ d less a ==> b | c -((a + b) ^? (c ?^ d)) less ((a ==> b) | c) -``` - -上面都是粗浅的理解,以下引用自[Scala2.13标准 - 06表达式 - 12前缀中缀和后缀操作](https://www.scala-lang.org/files/archive/spec/2.13/06-expressions.html#prefix-infix-and-postfix-operations)一节: - -词法: -```bnf -PostfixExpr ::= InfixExpr [id [nl]] -InfixExpr ::= PrefixExpr - | InfixExpr id [nl] InfixExpr -PrefixExpr ::= ['-' | '+' | '!' | '~'] SimpleExpr -``` -- 可以看到前缀运算符只有`- + ! ~`含义是正负号、逻辑非、按位取反。 -- 表达式是通过运算符和操作数构建的。 - -前缀运算: -- 仅有`+ - ! ~`,等价于后缀的方法调用`.+() .-() .!() .~()` -- 前缀运算符的操作数是**原子**的,比如`-sin(x)`被解析为`-(sin(x))`。这不同于一般的函数,如果定义一个相同含义的`negate`函数,那么`negate sin(x)`会被解析为`sin`是操作符,而`negate`和`(x)`是它的操作数。 - -后缀运算: -- 后缀的运算符可以是任意的标识符,所有的`e op`被解释为`e.op`。原来还可以这样! - -中缀运算: -- 一个中缀运算符可以是任意的标识符,中缀运算符 -- 中缀运算符的优先级定义如下:递增顺序,优先级由运算符首字符确定,同一行拥有同等优先级。 -```scala -(all letters, as defined in [chapter 1], including `_` and `$`) -| -^ -& -= ! -< > -: -+ - -* / % -(other operator characters, as defined in [chapter 1], including Unicode categories `Sm` and `So`) -``` -- 运算符的结合性由运算符的尾字符确定,以`:`结尾的字符为右结合,其他的都是左结合。后面的集合操作运算符中有例子。 -- 优先级和结合性决定了一个表达式的语义,也即是它的组合方式。具体规则: - - 多个中缀运算符同时出现,优先级更高的更显绑定到操作数。 - - 如果有多个连续的同一优先级的中缀运算符(同一优先级结合性必定相同),那么按照结合性绑定,左结合从左往右,右结合从右往左。 - - 后缀运算符总是比中缀运算符优先级更低。比如`e1 op1 e2 op2`总是解释为`(e1 op1 e2) op2`。 -- 中缀的左结合的运算符可能包含多个参数,`e op (e1, e2, ..., en)`总是被解释为`e.op(e1, e2, ..., en)`。 -- 对于中缀表达式`e1 op e2`,如果`op`左结合那么被解释为`e1.op(e2)`,如果右结合并且是它的参数是传名参数那么被解释为`e2.op(e1)`,如果参数是值传递,那么解释为`{val x = e2; e2.op(x)}`,其中x是一个新名称。后面的集合操作有例子。 -- 前面所说的前缀运算、后缀运算、中缀运算对应的运算符分别是一元前置运算符、一元后置运算符、二元后置运算符,不存在其他类型,比如三元的条件运算符。 - -赋值运算符: -- 赋值运算符是指以`=`结尾的运算符。除了同样以`=`开始,和`>= <= !=`之外。 -- 赋值运算符在当其他的解释都非法时会有特殊对待。 -- 比如说`l += r`,如果没有显式定义的`+=`运算符,那么会被解释为`l = l + r`。 -- 这种重新解释发生在满足以下两个条件的情况下: - - `l`没有`+=`运算符。 - - `l = l + r`是类型正确的,通常情况下这意味着`l`是一个左值,并且`l + r`是合法的(定义了`+`或者可隐式转换为定义了`+`的类型)。 - -试验与总结: -- 内置前缀运算符有`+ - ! ~`对少量内置类型提供支持,自定义一元前缀请使用`unary_`加上要定义的运算符名称,并且不要加参数列表`()`。而且这东西真的可以加参数列表,但我并没有找到用运算符形式调用的方式。 -- 自定义一元后置运算符(即定义为空参数列表)需要引入`scala.language.postfixOps`才能使用后缀运算符形式调用。当然用函数调用形式调用总是没有语法问题的。同样去掉参数列表后就可以后置使用。 -- 二元后置运算符,是最常用的自定义运算符。优先级上面有,结合性以是否`:`结尾确定。需要特别注意的是右结合和比如C++中重载赋值运算符是有区别的,需要了解。 -- Scala中内置的赋值运算符返回空`()`,所以其实是不能连续赋值的,赋值类运算符算是被特殊对待了。并且因为推崇函数式编程风格,能定义为`val`则定义为`val`也就不能连续赋值。因为变量都是引用变量,定义`=`变成了没有道理也不可能的一件事情,所以实践中也只能一条语句只做一次赋值。 -- Scala不能定义`=`运算符,但可以定义复合赋值,并且做了处理,某些情况下只要类设计得好定义了运算类运算符就不需要多去定义复合赋值了。所以说如果真要定义复合赋值返回值也应该返回`()`吗?也许是的。 -- 多参数的方法其实也可以写成中缀形式,`e op (e1, e2, ..., en)`。 -- 对于类方法来说,**运算符是函数,函数也是运算符**。 - -例子:可窥见其灵活程度。 -```scala -import scala.language.postfixOps -object UserDefindOps { - def main(args: Array[String]): Unit = { - val v = Vector2(10, 5) - val v2 = Vector2(5, 10) - - println(v) - // prefix unary - println("==========================================") - println(+v) - println(-v) - println(v.unary_-("hello")) - - // binary - println("==========================================") - println(v * 3) - println(v * v2) - println(v + v2) - println(v - v2) - - // postfix unary, just for test, no meaning - println("==========================================") - println(v-) - println((v-)-) - - // multiple - println("==========================================") - println(v hello ("test", "yes")) - println(v + (10, 10)) - println(v - (10, 10)) - - // assignment operator - println("==========================================") - var v3 = Vector2(10, 5) - println(v3 *= 3) // () - println(v3) // Vector2(30.0, 15.0) - v3 /= 3 - println(v3) - v3 += v2 - println(v3) - v3 += (10, 10) - println(v3) - } -} - -class Vector2(val x: Double, val y: Double) { - override def toString(): String = s"Vector2($x, $y)" - // prefix unary - def unary_- = this.- // call postfix - - def unary_+ = Vector2(x, y) - def unary_-(str: String) = s"unary - with a string parameter: $str" // can not call this through operator format - // binary - def +(v: Vector2) = Vector2(x + v.x, y + v.y) - def -(v: Vector2) = Vector2(x - v.x, y - v.y) - def *(v: Vector2) = x * v.x + y * v.y // Inner product - def *(d: Double) = Vector2(d * x, d * y) // multiply - def /(d: Double) = Vector2(x / d, y / d) - // postfix unary - def - = Vector2(-x, -y) - // multiple - def hello(a: String, b: String) = s"$a, $b, ${toString()}" - def +(_x: Double, _y: Double): Vector2 = this + Vector2(_x, _y) - def -(_x: Double, _y: Double): Vector2 = Vector2(x - _x, y - _y) -} - -object Vector2 { - def apply(x: Double, y: Double) = new Vector2(x, y) -} -``` - -实践指南: -- 一元前缀和一元后缀运算符定义时不加参数列表,运算符形式使用。 -- 在复杂表达式中使用一元前缀和后缀运算符使用时最好加括号表明优先级,不然在复杂表达式中编译器可能难以区分这是一元的还是二元的。至少定义了一元前置和后置`-`的类中无法像`a - -`和`- - a`这样来用。 -- 二元运算符定义只给一个参数,运算符形式使用。 -- 参数多于1个时不要通过运算符形式使用,但如果很清晰的话其实也无妨。 -- 函数也是运算符,非特殊符号运算符形式使用也可以很有用,表达能力很强,比如`1 to 10`。 -- 不要滥用,用到都搞不清谁是运算符谁是操作数就不好了。 -- 经验尚浅,还需多实践。 - -## 控制流 - -`if-else`: -```scala -if (condition) { - xxx -} else if (condition) { - xxx -} else { - xxx -} -``` -- scala中特殊一点,`if-else`语句也有返回值,也就是说也可以作为表达式,定义为执行的最后一个语句的返回值。 -- 可以强制要求返回`Unit`类型,此时忽略最后一个表达式的值,得到`()`。 -- 多种返回类型的话,赋值的目标变量类型需要指定为具体公共父类,也可以自动推断。 -- scala中没有三元条件运算符,可以用`if (a) b else c` 替代`a ? b : c`。 -- 嵌套条件同理。 - -`for`循环,也叫`for`推导式: -- 范围遍历:`for(i <- 1 to 10) {}`,其中`1 to 10`是`Int`一个方法调用,返回一个`Range`。 -- 范围`1 to 10` `1 until 10`是包含右边界和不包含右边界的范围,也可以直接用`Range`类。 -- 范围步长`1 to 10 by 2`。 -- 范围也是一个集合,也可以遍历普通集合:`for(i <- collection) {}` -- 循环守卫:即循环保护式,或者叫条件判断式,循环守卫为`true`则进入循环体内部,为`fasle`则跳过,类似于`continue`。 - - 写法: - ```scala - for(i <- collection if condition) { - } - ``` - - 等价于: - ```scala - if (i <- collection) { - if (condition) { - } - } - ``` -- 嵌套循环同理。嵌套循环可以将条件合并到一个`for`中: - - 标准写法: - ```scala - for (i <- 1 to 4) { - for (j <- 1 to 5) { - println("i = " + i + ", j = " + j) - } - } - ``` - - 等价写法: - ```scala - for (i <- 1 to 4; j <- 1 to 5) { - println("i = " + i + ", j = " + j) - } - ``` - - 典型例子,乘法表: - ```scala - for (i <- 1 to 9; j <- 1 to i) { - print(s"$j * $i = ${i * j} \t") - if (j == i) println() - } - ``` -- 循环中的引入变量,但不是循环变量: - ```scala - for (i <- 1 to 10; j = 10 - i) { - println("i = " + i + ", j = " + j) - } - ``` -- 循环条件也可以用`{}` - - 上面的引入变量循环等价于: - ```scala - for { - i <- 1 to 10 - j = 10 - i - } { - println("i = " + i + ", j = " + j) - } - ``` -- 循环同样有返回值,返回值都是空,也就是`Unit`实例`()`。 -- 循环中同样可以用`yield`返回,外面可以接住用来操作,循环暂停,执行完后再继续循环。就像Ruby/Python。 - ```scala - val v = for (i <- 1 to 10) yield i * i // default implementation is Vector, Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100) - ``` - -`while`和`do while`: -- 为了兼容java,不推荐使用,结果类型是`Unit`。 -- 不可避免需要声明变量在循环外部,等同于循环内部对外部变量造成了影响,所以不推荐使用。 -```scala -while (condition) { -} -do { -} while (condition) -``` - -循环中断: -- Scala内置控制结构去掉了`break continue`关键字,为了更好适应函数式编程,推荐使用函数式风格解决。 -- 使用`breakable`结构来实现`break continue`功能。 -- 循环守卫可以一定程度上替代`continue`。 -- 可以用抛出异常捕获的方式退出循环,替代`break`。 - ```scala - try { - for (i <- 0 to 10) { - if (i == 3) - throw new RuntimeException - println(i) - } - } catch { - case e: Exception => // do nothing - } - ``` -- 可以使用Scala中的`Breaks`类中的`break`方法(只是封装了异常捕获),实现异常抛出和捕获。 - ```scala - import scala.util.control.Breaks - Breaks.breakable( - for (i <- 0 to 10) { - if (i == 3) - Breaks.break() - println(i) - } - ) - ``` - -## 函数式编程 - -不同范式对比: -- 面向过程:按照步骤解决问题。 -- 面向对象:分解对象、行为、属性,通过对象关系以及行为调用解决问题。耦合低,复用性高,可维护性强。 -- 函数式编程:面向对象和面向过程都是命令式编程,但是函数式编程不关心具体运行过程,而是关心数据之间的映射。纯粹的函数式编程语言中没有变量,所有量都是常量,计算过程就是不停的表达式求值的过程,每一段程序都有返回值。不关心底层实现,对人来说更好理解,相对地编译器处理就比较复杂。 -- 函数式编程优点:编程效率高,函数式编程的不可变性,对于函数特定输入输出是特定的,与环境上下文等无关。函数式编程无副作用,利于并行处理,所以Scala特别利于应用于大数据处理,比如Spark,Kafka框架。 - -函数定义: -```scala -def func(arg1: TypeOfArg1, arg2: ...): RetType = { - ... -} -``` -- 函数式编程语言中,函数是一等公民(可以像对象一样赋值、作为参数返回值),可以在任何代码块中定义函数。 -- 一般将定义在类或对象中(最外层)的函数称为方法,而定义在方法中(内层)的称为函数。广义上都是函数。 -- 返回值用`return`返回,不写的话会使用最后一行代码作为返回值。 -- 无返回值`Unit`时可以用`return`可以用`return ()`可以不返回。 -- 其他时候只需要返回值是返回值类型的子类对象就行。 - -术语说明: -- java中不提函数的说法,而是说类或者实例方法,不涉及一般化的函数。 -- 函数式编程中的函数二字来源于数学上的函数,也就是映射,集合和集合之间的关系,强调数据之间的映射关系。 -- 而编程语言中的函数,也包括scala中的函数定义都是指的一个完成特定功能的子程序(subroutine),并不等同于数学意义上的函数。 - -函数参数: -- 可变参数,类似于Java,使用数组包装。 - - `def f4(str:String*): Unit = {}`。 - - 如果除了可变参数还有其他参数,需要将可变参数放在末尾。 - - 可变参数当做数组来使用。 -- 参数默认值: - - `def f5(name: String = "alice"): Unit = {}` - - 和C++一样,默认参数可以不传,默认参数必须全部放在末尾。 -- 带名称传参: - - 调用时带名称。 - ```scala - def f6(name: String, age: Int = 20, loc: String = "BeiJing"): Unit = { - println(s"name ${name}, age ${age}, location ${loc}") - } - f6("Bob") - f6("Alice", loc = "Xi'An") - f6("Michael", 30) - ``` - - 不给名称的就是按顺序赋值。 - - 调用时带名参数必须位于实参列表末尾。 - - 和默认参数一起使用会很方便,比如有多个默认参数,但只想覆盖其中一个。 - -函数至简原则: -- 能省则省。 -- 最后一行代码会作为返回值,可以省略`return`。 -- 函数体只有一行代码的话,可以省略花括号。 -- 如果返回值类型能够自动推断那么可以省略。 -- 如果函数体中用`return`做返回,那么返回值类型必须指定。 -- 如果声明返回`Unit`,那么函数体中使用`return`返回的值也不起作用。 -- 如果期望是无返回值类型,那么可以省略`=`。这时候没有返回值,函数也可以叫做过程。【2.13.0已废弃,能编过不过会提示。】 -- 无参函数如果声明时没有加`()`,调用时可以省略`()`。【如果声明时有`()`调用也可以省略,不过2.13.3废弃了。】 -- 不关心函数名称时,函数名称和`def`也可以省略,去掉返回值类型,将`=`修改为`=>`定义为匿名函数。 - ```scala - val fun = (name: String) => { println("name") } - ``` - -匿名函数: -- 没有名称的函数,可以被赋值给一个量。也叫lambda表达式 -- `val fun = (name: String) => { println("name") }` -- 匿名函数定义时不能有函数的返回值类型。 -- 简化原则: - - 参数的类型可以省略,如果可以根据高阶函数形参自动推导。 - - 类型省略之后如果只有一个参数,那么可以省略参数列表的`()`,`name => println(name)`。 - - 匿名函数函数体只要一行,那么`{}`可以省略。 - - 如果参数只出现一次,则参数可以省略,后面出现的参数用`_`代替,`println(_)`也是一个lambda,表示`name => {println(name)}`。 - - 如果可以推断出当前传入的`println`是一个函数体,而不是函数调用语句,那么可以省略下划线。也就是省略了转调,直接将函数名称作为参数传递。 - ```scala - def f(func: String => Unit): Unit = { - func("alice") - } - f((name: String) => { println(name) }) - f((name) => println(name)) - f(println(_)) - f(println) - ``` -- 例子:省得太极端就没有可读性了。 -```scala -def dualOp(func: (Int, Int) => Int): Int = { - func(1, 2) -} -println(dualOp((a: Int, b: Int) => a + b)) -println(dualOp((a: Int, b: Int) => a - b)) -println(dualOp((a, b) => a - b)) -println(dualOp(_ + _)) // a + b -println(dualOp(-_ + _)) // -a + b -``` - -高阶函数: -- 三种形式:函数作为值传递、函数作为参数、函数作为返回值。 -- 作为值传递:经过赋值之后在底层变成一个lambda对象。 -```scala -// define function -def foo(n: Int): Int = { - println("call foo") - n + 1 -} -// function assign to value, also a object -val func = foo _ // represent the function foo, not function call -val func1: Int => Int = foo // specify the type of func1 -println(func) // Main$$$Lambda$674/0x000000080103c588@770beef5 -println(func == func1) // false, not a same object -``` -- 函数作为参数,上面展示过了。可以传匿名函数、函数名称、lambda对象。 -```scala -// function as arguments -def dualEval(op: (Int, Int) => Int, a: Int, b: Int) = { - op(a, b) -} -def add(a: Int, b: Int): Int = a + b -println(dualEval(add, 10, 100)) -val mul:(Int, Int) => Int = _ * _ -println(dualEval(mul, 10, 100)) -println(dualEval((a, b) => a + b, 1000, 24)) -``` -- 函数作为返回值: -```scala -// function as return value -def outerFunc(): Int => Unit = { - def inner(a: Int): Unit = { - println(s"call inner with argument ${a}") - } - inner // return a function -} -println(outerFunc()(10)) // inner return () -``` -- 现在就可以套娃了,比如定义一个返回一个返回函数的函数的函数。 - -高阶函数举例: -- 使用特定操作处理数组元素,得到新数组。也就是集合处理的map(映射)操作。 -```scala -// deal with an array, get a new array -// map operation of array -def arrayOp(arr: Array[Int], op: Int => Int): Array[Int] = { - for (elem <- arr) yield op(elem) // the whole for expression get a new array -} -val arr = Array(1, 2, 3, 4) -def addOne(elem: Int): Int = elem + 1 -println(arrayOp(arr, addOne _).mkString(", ")) // pass addOne also work -println(arrayOp(arr, elem => elem * 2).mkString(", ")) -println(arrayOp(arr, _ * 3).mkString(", ")) -``` -- 套娃: -```scala -def func(a: Int): String => (Char => Boolean) = { - def f1(s: String): Char => Boolean = { - def f2(c: Char): Boolean = { - if (a == 0 && s == "" && c == '0') false else true - } - f2 - } - f1 -} -println(func(0)("")('0')) // false -println(func(1)("hello")('c')) // true -``` -- 上面的例子经过极致简写:只能说类型推导也太强大了。**内层函数可以使用外层函数的参数**。 -```scala -// simplify to anonymous function -def func1(a: Int): String => (Char => Boolean) = { - s => c => !(a == 0 && s == "" && c == '0') -} -println(func1(0)("")('0')) // false -println(func1(1)("hello")('c')) // true -``` -- 柯里化之后: -```scala -// Currying -def func2(a: Int)(s: String)(c: Char): Boolean = !(a == 0 && s == "" && c == '0') -println(func2(0)("")('0')) // false -println(func2(1)("hello")('c')) // true -``` - -**函数柯里化**和**闭包**:**重点**。 - -闭包:如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和他所处的环境,称为闭包。 - -- [闭包](https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6))的定义: -> 在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持[头等函数](https://zh.wikipedia.org/wiki/%E5%A4%B4%E7%AD%89%E5%87%BD%E6%95%B0)的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。 -- 因为外层调用结束返回内层函数后,经过堆栈调整(比如在C中主调或者被调清理),外层函数的参数已经被释放了,所以内层是获取不到外层的函数参数的。为了能够将环境(函数中用到的并非该函数参数的变量和他们的值)保存下来(需要考虑释放问题,可以通过GC可以通过对象生命周期控制,GC是一个常见选择),这时会将执行的环境打一个包保存到堆里面。 - -函数[柯里化](https://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C%E5%8C%96)(Currying):将一个参数列表的多个参数,变成多个参数列表的过程。也就是将普通多参数函数变成高阶函数的过程。 -- 定义: ->在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。柯里化是一种处理函数中附有多个参数的方法,并在只允许单一参数的框架中使用这些函数。 -- scala中的柯里化函数定义: -```scala -// Currying -def add(a: Int)(b: Int): Int = a + b -println(add(4)(3)) -val addFour = add(4) _ -// val addFour: Int => int = add(4) -println(addFour(3)) -``` - -递归: -- 太常见了,不用过多介绍。 -- 方法调用自身。 -- 递归要有结束逻辑。 -- 调用自身时,传递参数要有规律。 -- scala中递归定义函数必须声明返回值类型,因为无法通过推导获得。 -- 纯函数式语言比如Haskell,连循环都没有,很多操作都需要通过递归来做,性能比较依赖尾递归优化。 -- scala中的尾递归优化例子: -```scala -def factorial(n: Int) : Int = { - if (n < 0) - return -1 - if(n == 0) - return 1 - factorial(n-1) * n -} -// tail recusion implementation of factorial -def tailFact(n: Int): Int = { - if (n < 0) - return -1 - @annotation.tailrec - def loop(n: Int, curRes: Int): Int = { - if (n == 0) - return curRes - loop(n - 1, curRes * n) - } - loop(n, 1) -} -``` - -控制抽象: -- 值调用:按值传递参数,计算值后再传递。多数语言中一般函数调用都是这个方式,C++还存在引用传递。 -- 名调用:按名称传递参数,直接用实参替换函数中使用形参的地方。能想到的只有C语言中的带参宏函数,其实并不是函数调用,预处理时直接替换。 -- 例子: -```scala -// pass by value -def f0(a: Int): Unit = { - println("a: " + a) - println("a: " + a) -} -f0(10) - -// pass by name, argument can be a code block that return to Int -def f1(a: => Int): Unit = { - println("a: " + a) - println("a: " + a) -} -def f2(): Int = { - println("call f2()") - 10 -} -f1(10) -f1(f2()) // pass by name, just replace a with f2(), then will call f2() twice -f1({ - println("code block") // print twice - 30 -}) -``` -- 应用:使用传名参数实现一个函数相当于while的功能。 -```scala -// built-in while -var n = 10 -while (n >= 1) { - print(s"$n ") - n -= 1 -} -println() - -// application: self-defined while, implement a function just like while keyword -def myWhile(condition: => Boolean): (=> Unit) => Unit = { - def doLoop(op: => Unit): Unit = { - if (condition) { - op - myWhile(condition)(op) - } - } - doLoop _ -} -n = 10 -myWhile (n >= 1) { - print(s"$n ") - n -= 1 -} -println() - -// simplfy -def myWhile2(condition: => Boolean): (=> Unit) => Unit = { - op => { - if (condition) { - op - myWhile2(condition)(op) - } - } -} -n = 10 -myWhile (n >= 1) { - print(s"$n ") - n -= 1 -} -println() - -// use currying -def myWhile3(condition: => Boolean)(op: => Unit): Unit = { - if (condition) { - op - myWhile3(condition)(op) - } -} -n = 10 -myWhile3 (n >= 1) { - print(s"$n ") - n -= 1 -} -println() -``` - -惰性加载: -- 当函数返回值被声明为`lazy`时,函数的执行将会被推迟,知道我们首次对此取值,该函数才会被执行。这种函数成为惰性函数。 -```scala -def main(args: Array[String]): Unit = { - // just like pass by name - lazy val result: Int = sum(13, 47) - println("before lazy load") - println(s"result = ${result}") // first call sum(13, 47) - println(s"result = ${result}") // result has been evaluated -} -def sum(a: Int, b: Int): Int = { - println("call sum") - a + b -} -``` -- 有点像传名参数,但懒加载只是推迟求值到第一次使用时,而不是单纯替换。 - -## 包管理 - -关于Scala面向对象: -- Scala的面向对象思想源自Java,很多概念是一致的。 -- 语法和java不同,补充了更多功能。 - -包: -- `package name` -- 作用: - - 区分相同名字类,避免名称冲突。 - - 类很多时,分模块管理。 - - 访问权限控制。 -- 命名:包名称只能是常规的标识符(字母数字下划线,数字不能开头)。同样`.`作为不同层级分割符,整体作为包名。 -- 命名规范:一般情况下按照如下规则命名`com.company.projectname.modulename`,视项目规定而定,只是一个名称而已。 -- scala中的两种包管理方式: - - 第一种,java风格,每个源文件声明一个包,写在源文件最上方。但源文件位置不需要和包名目录层级一致,只代表逻辑层级关系,不像java一样源文件也必须按照包名目录层级关系放置。当然惯例是和java一样按照包名目录层级来放置。 - - 第二种,用`{}`嵌套风格定义包: - ```scala - package com { - // code in com package - object Outer { - var name = "Outer" - } - package inner { - // code in com.inner package - package scala { - // code in com.innner.scala package - object Inner { - def main(args: Array[String]):Unit = { - println(Outer.name) - Outer.name = "Inner" - println(Outer.name) - } - } - } - } - } - ``` - - 嵌套风格好处: - - 一个源文件可以声明多个并列的最顶层的包。 - - 子包中的类可以访问父包中的内容,无需导入。但外层是不能直接访问内层的,需要导入。 - - 如果单文件VsCode测试嵌套包,而不是用IDE的话,那定义了包就不能直接执行了,需要`scalac`先编译,并指定入口类运行。编译后的字节码文件和java一样会自动按照包层级关系排列。 - ```shell - scalac PackageManagement.scala - scala com.inner.scala.Inner - ``` - - - -包对象: -- 为scala包定义一个同名的单例包对象,定义在包对象中的成员,作为其对应包下的所有类和对象的共享变量,可以被直接访问,无需导入。 -- 关键字`package object`,需要和包在同一层级下。比如为`com.inner`包定义包对象的话,必须在`com`包中,定义形式`package obejct inner { ... }`。 - -包的导入: -```scala -import users._ // 导入包 users 中的所有成员 -import users.User // 导入类 User -import users.{User, UserPreferences} // 仅导入选择的成员 -import users.{UserPreferences => UPrefs} // 导入类并且设置别名 -import users.{User => _, _} // 导入出User类以外的所有users包中的内容 -``` -- 可以在任意位置导入(作用于代码块),可以设置别名,可以选择性导入想要导入的内容,可以屏蔽某个类。 -- 所有scala源文件默认导入: -```scala -import java.lang._ -import scala._ -import scala.Predef._ -``` - -## 面向对象 - -类定义: -- 回顾java中,如果是`public`向外公开的,那么必须和文件名一致,也只能有一个。不写访问修饰符则可以定义多个,包访问权限。 -- scala中没有`public`关键字,默认就是公有,不能加`public`,一个文件可以写多个类,不要求和文件名一致。 -```scala -[descriptor] class classname { - // body: fields & methods - [descriptor] var/val name: Type = _ - [descriptor] method(args: ArgsType): RetType = { - // method body - } -} -``` -- 访问修饰符可以是:`private` `protected` `private [pacakgeName]`,默认就是公有,不需要加。 -- 成员如果需要Java Bean规范的getter和setter的话可以加`@scala.beans.BeanProperty`相当于自动创建,不需要显式写出。 -- 成员给初值`_`会赋默认值,scala中定义变量必须赋值,可以这样做。值类型的值0,引用则是`null`。定义常量的话不能用`_`,因为只能初始化一次,编译器会提示。 - -封装: -- Java的封装:私有化,提供getter和setter。 -- scala中考虑到Java太冗余了,脱裤子放屁一样。scala中的公有属性,底层实际为`private`,并通过get方法`obj.field()`和set方法`obj.field_=(value)`对其进行操作。所以scala不推荐设置为`private`。如果需要和其他框架互操作,必须提供Java Bean规范的getter和setter的话可以加`@scala.beans.BeanProperty`注解。 - -访问权限: -- Java中`private protected public`和默认包访问权限。 -- scala中属性和方法默认公有,并且不提供`public`关键字。 -- `private`私有,类内部和伴生对象内可用。 -- `protected`保护权限,scala中比java中严格,只有同类、子类可访问,同包无法访问。【因为java中说实话有点奇怪】 -- `private [pacakgeName]`增加包访问权限,在包内可以访问。 - -构造器: -- 包括主构造器和辅助构造器。 -```scala -class ClassName [descriptor] [([descriptor][val/var] arg1: Arg1Type, [descriptor][val/var] arg2: ...)] { // main constructor, only one, like record in java - // assist constructor - def this(argsList1) { - this(args) // call main constructor - } - def this(argsList2) { // overload constrcutor - this(argsList1) // can call main constructor or other constructor that call main constructor directly or indirectly - } -} -``` -- 例子: -```scala -object Constructor { - def main(args: Array[String]): Unit = { - val p: Person = new Person() - p.Person() // call main constructor - - val p1 = new Person("alice") - val p2 = new Person("bob", 25) - p1.Person() - } -} -class Person { - var name: String = _ - var age: Int = _ - println("call main construtor") - - def this(name: String) { - this() - println("call assist constructor 1") - this.name = name - println(s"Person: $name $age") - } - - def this(name: String, age: Int) { - this(name) - this.age = age - println("call assist constructor 2") - println(s"Person: $name $age") - } - - // just a common method, not constructor - def Person(): Unit = { - println("call Person.Person() method") - } -} -``` -- 特点: - - 主构造器写在类定义上,一定是构造时最先被调用的构造器,方法体就是类定义,可以在类中方法定义的同级编写逻辑,都是主构造器一部分,按顺序执行。 - - 辅助构造器用`this`定义。 - - 辅助构造器必须直接或者间接调用主构造器,调用其他构造必须位于第一行。 - - 主构造器和辅助构造器是重载的方法,所以参数列表不能一致。 - - 可以定义和类名同名方法,就是一个普通方法。 -- 主构造器中形参三种形式:不使用任何修饰,`var`修饰,`val`修饰。 - - 不使用任何修饰那就是一个形参,但此时在类内都可以访问到这个变量。逻辑上不是一个成员(报错信息这么写),但是可以访问,WTF??? - - 使用`var val`修饰那就是定义为类成员,分别是变量和常量,不需要也不能在类内再定义一个同名字段。调用时传入参数就直接给到该成员,不需要再显式赋值。 - - 主构造器中的`var val`成员也可以添加访问修饰符。 - - 不加参数列表相当于为空,`()`可以省略。 - - 主构造器的访问修饰符添加到参数列表`()`前。 -- 实践指南: - - 推荐使用scala风格的主构造器`var val`修饰参数的编写方法,而不要被Java毒害! - - 如果需要多种重载的构造器那么就添加新的的辅助构造器。 -```scala -class Person(private var name: String) { - var age: Int = _ - println("call main construtor") - - def this(name: String, age: Int) = { - this(name) - this.age = age - println("call assist constructor 2") - println(s"Person: $name $age") - } - - // just a common method, not constructor - def Person(): Unit = { - println("call Person.Person() method") - } -} -``` - - -继承: -- `class ChildClassName[(argList1)] extends BaseClassName[(args)] { body }` -- 子类继承父类属性和方法。 -- 可以调用父类构造器,但感觉好像很局限,子类中只可能调用到主构造或者辅助构造中的其中一个构造器。那如果父类有多种构造方式,子类想继承也没有办法?只能是其中一种。 -- 不考虑太多负担,按照scala惯用写法来写起来还是挺轻松的。 - -多态: -- java中属性静态绑定,根据变量的引用类型确定,方法是动态绑定。 -- 但scala中**属性和方法都是动态绑定**。就属性而言,其实也不应该在子类和父类中定义同名字段。 -- 同java一样,所有实例方法都是虚方法,都可以被子类覆写。 -- `override`关键字覆写。 -- scala中**属性(字段)也可以被重写**,加`override`关键字。 - -抽象类: -- `abstract calss ClassName` -- 抽象属性:`val/var name: Type`,不给定初始值。 -- 抽象方法:`def methodName(): RetType`,只声明不实现。 -- 子类如果没有覆写全部父类未定义的属性和方法,那么就必须定义为抽象类。老生常谈了。 -- 重写非抽象方法属性必须加`override`,重写抽象方法则可以不加`override`。 -- 子类调用父类中方法使用`super`关键字。 -- 子类重写父类抽象属性,父类抽象属性可以用`var`修饰,`val var`都可以。因为父类没有实现嘛,需要到子类中来实现。 -- 如果是**重写非抽象属性**,则父类非抽象属性只支持`val`,不支持`var`。因为`var`修饰为可变量,子类继承后可以直接使用修改,没有必要重写。`val`不可变才有必要重写。 -- 实践建议是重写就加`override`,都是很自然的东西,理解就好,不必纠结于每一个细节。 - -匿名子类; -- 和java如出一辙。重写所有抽象字段和方法。 -```scala -val/var p: baseClass = new baseClass { - override ... -} -``` - -伴生对象(Companion Object): -- 取代`static`语义。 -- 编译后其实会生成两个类,伴生对象是伴生类(类名为对应类后加`$`符号)的单例对象。 -- `obejct`,名称和类一致,必须放同一个文件,前面已经说过了。 -- 常见用法:构造器私有化,用伴生对象中的工厂方法。和静态工厂方法使用起来也没有什么区别。 -- 伴生对象实现`apply`方法后调用时可以省略`.apply`,直接使用`className(args)`。库中很多这种用法创建实例,是一个语法糖。 -- 测试伴生对象时就在该对象内定义`main`函数编译时会出现的奇怪的访问权限问题。可能对包含入口的伴生对象做了特殊处理,具体细节尚不清楚。最好将`main`定义在单独的伴生对象内。 - - -Trait(特征/特质): -- 替代java接口的概念。但比接口更为灵活,一种实现多继承的手段。 -- 多个类具有相同的特征时,就可以将这个特征提取出来,用继承的方式来复用。 -- 用关键字`trait`声明。 -```scala -trait traitName { - ... -} -``` -- 引入/混入(mixin)特征: - - 有父类`class extends baseClass with trait1 with trait2 ... {}` - - 没有父类`class extends trait1 with trait2 ... {}` -- 其中可以定义抽象和非抽象的属性和方法。 -- 匿名子类也可以引入特征。 -- 特征和基类或者多个特征中重名的属性或方法需要在子类中覆写以解决冲突,最后因为动态绑定,所有使用的地方都是子类的字段或方法。属性的话需要类型一致,不然提示不兼容。方法的话参数列表不一致会视为重载而不是冲突。 -- 如果基类和特征中的属性或方法一个是抽象的,一个非抽象,且兼容,那么可以不覆写。很直观,就是不能冲突不能二义就行。 -- 多个特征和基类定义了同名方法的,就需要在子类重写解决冲突。其中可以调用父类和特征的方法,此时`super.methodName`指代按照顺序最后一个拥有该方法定义的特征或基类。也可以用`super[baseClassOrTraitName].methodName`直接指代某个基类的方法,注意需要是直接基类,间接基类则不行。 -- 也就是说基类和特征基本是同等地位。 -- 例子: -```scala -class Person { - val name: String = "Person" - var age: Int = 18 - - def sayHi(): Unit = { - println(s"hello from : $name") - } -} - -trait Young { - // abstract and non-abstract attribute - var age: Int - val name: String = "young" - - // method - def play(): Unit = { - println(s"young people $name is playing") - } - def dating(): Unit -} - -trait Knowledge { - var amount: Int = 0 - def increase(): Unit = { - amount += 1 - } -} - -trait Talent { - def increase(): Unit = { - println("increase talent") - } -} - -class Student extends Person with Young with Knowledge with Talent{ - override val name: String = "alice" - - def dating(): Unit = { - println(s"Sutdent $name $age is dating") - } - - def study(): Unit = println(s"Student $name is studying") - - override def sayHi(): Unit = { - super.sayHi() - println(s"hello from : student $name") - } - - override def increase(): Unit = { - super.increase() // call Talent.increase(), just the last - println(s"studnet $name knowledge increase: $amount") - } -} - -object Trait { - def main(args: Array[String]): Unit = { - val s = new Student() - s.sayHi() - s.increase() - - s.study() - s.increase() - - s.play() - s.increase() - - s.dating() - s.increase() - } -} -``` -- 特征的继承:`trait childTrait extends baseTrait` -- 特征的**菱形继承**解决方式:转换为线性的继承链条,在前面的成为基类,后面的成为子类。 -- 例子: -```scala -trait Ball { - def describe(): String = "ball" -} - -trait ColorBall extends Ball { - var color: String = "red" - override def describe(): String = color + "_" + super.describe() -} - -trait CategoryBall extends Ball { - var category: String = "foot" - override def describe(): String = category + "_" + super.describe() -} - -// equals to MyFootBall -> ColorBall -> CategoryBall -> Ball -class MyFootBall extends CategoryBall with ColorBall { - override def describe(): String = super.describe() -} - -object TraitInheritance { - def main(args: Array[String]): Unit = { - val b = new MyFootBall() - println(b.describe()) // red_foot_ball - } -} -``` -- 其实特征的多继承和C++的多继承已经很像了,只是名称冲突的解决方式不一样,菱形继承的解决方式也不一样,而且不能访问间接基类。 -- scala**单继承多实现**,实现体现在特征上。基类主要用于一个对象比较核心比较本质的部分上。 -- **继承特征与类的区别**:特征构造时不能给参数。其他都是同样的,都可以实现多态。 - -自身类型(self type): -- 可实现**依赖注入**的功能。 -- 一个类或者特征指定了自身类型的话,它的对象和子类对象就会拥有这个自身类型中的所有属性和方法。 -- 是将一个类或者特征插入到另一个类或者特征中,属性和方法都就像直接复制插入过来一样,能直接使用。但不是继承,不能用多态。 -- 语法,在类或特征中:`_: SelfType =>`,其中`_`的位置是别名定义,也可以是其他,`_`指代`this`。插入后就可以用`this.xxx`来访问自身类型中的属性和方法了。 -- 注入进来的目的是让你能够使用,可见,提前使用应该拥有的属性和方法。最终只要自身类型和注入目标类型同时被继承就能够得到定义了。 -- 例子: -```scala -class User(val name: String, val password: String) -// user database access object -trait UserDao { - // dependency injection from external - _: User => // self type - // simulate insert data to databse - def insert(): Unit = { - println(s"insert into db: $name $password") - } -} -// register user -class RegisterUser(name: String, password: String) extends User(name, password) with UserDao - -object SelfType { - def main(args: Array[String]): Unit = { - val u = new RegisterUser("catholly", "nephren") - u.insert() - } -} -``` - -运行时类型识别RTTI: -- 判断类型:`obj.isInstanceOf[T]`,确切匹配的类型或者父类都返回true。 -- 转换类型:`obj.asInstance[T]`,转换为目标类型。 -- 获取类名:`classOf[T]`,得到类对应的`Class`对象`Class[T]`,转字符串结果是`class package.xxx.className`。 -- 获取对象的类:`obj.getClass` - -枚举类: -- 继承`Enumeration`。 -- 用`Value`类型定义枚举值。 -```scala -object WorkDay extends Enumeration { - val MONDAY = Value(1, "Monday") - val TUESDAY = Value(2, "Tuesday") - val THURSDAy = Value(3, "Thrusday") -} - -object EnumClass { - def main(args: Array[String]): Unit = { - println(WorkDay.MONDAY) - println(WorkDay.TUESDAY) - } -} -``` - -应用类: -- 继承`App`,包装了`main`方法,就不需要显式定义`main`方法了,可以直接执行。 -```scala -object TestApp extends App { - println("hello,world!") -} -``` - -定义类型别名:`type SelfDefineType = TargetType`。 - -密封类: `sealed`,子类只能定义在同一个文件内。 - -## 集合 - -Java集合: -- 三大类型:列表`List`、集合`Set`、映射`Map`,有多种不同实现。 - -Scala集合三大类型: -- 序列`Seq`,集合`Set`,映射`Map`,所有集合都扩展自`Iterable`。 -- 对于几乎所有集合类,都同时提供**可变和不可变**版本。 - - 不可变集合:`scala.collection.immutable` - - 可变集合:`scala.collection.mutable` - - 两个包中可能有同名的类型,需要注意区分是用的可变还是不可变版本,避免冲突和混淆。 -- 对于不可变集合,指该集合长度数量不可修改,每次修改(比如增删元素)都会返回一个新的对象,而不会修改源对象。 -- 可变集合可以对源对象任意修改,一般也提供不可变集合相同的返回新对象的方法,但也可以用其他方法修改源对象。 -- **建议**:操作集合时,不可变用操作符,可变用方法。操作符也不一定就会返回新对象,但大多是这样的,还是要具体看。 -- scala中集合类的定义比java要清晰不少。 - -不可变集合: -- `scala.collection.immutable`包中不可变集合关系一览: -![Scala_mutable_collections_tree](Images/Scala_immutable_collections_tree.jpg) -- 不可变集合没有太多好说的,集合和映射的哈希表和二叉树实现是肯定都有的,序列中分为随机访问序列(数组实现)和线性序列(链表实现),基本数据结构都有了。 -- `Range`是范围,常用来遍历,有语法糖支持`1 to 10 by 2` `10 until 1 by -1`其实就是隐式转换加上方法调用。 -- scala中的`String`就是`java.lang.String`,和集合无直接关系,所以是虚箭头,是通过`Perdef`中的低优先级隐式转换来做到的。经过隐式转换为一个包装类型后就可以当做集合了。 -- `Array`和`String`类似,在图中漏掉了。 -- 此类包装为了兼容java在scala中非常常见,scala中很多类型就是对java类型的包装或者仅仅是别名。 -- scala中可能会推荐更多地使用不可变集合。能用不可变就用不可变。 - -可变集合: -- `scala.collection.mutable`包中可变集合关系一览: -![Scala_mutable_collections_tree](Images/Scala_mutable_collections_tree.jpg) -- 序列中多了`Buffer`,整体结构差不多。 - -不可变和可变: -- 不可变指的是对象大小不可变,但是可以修改元素的值(不能修改那创建了也没有用对吧),需要注意这一点。而如果用了`val`不变量存储,那么指向对象的地址也不可变。 -- 不可变集合在原集合上个插入删除数据是做不到的,只能返回新的集合。 - -泛型: -- 集合类型大多都是支持泛型,使用泛型的语法是`[Type]`,不同于java的``。 - -不可变数组: -- 访问元素使用`()`运算符,通过`apply/update`方法实现,源码中的实现只是抛出错误作为**存根方法**(stab method),具体逻辑由编译器填充。 -```scala -// 1. new -val arr = new Array[Int](5) - -// 2. factory method in companion obejct -val arr1 = Array[Int](5) -val arr2 = Array(0, 1, 3, 4) - -// 3. traverse, range for -for (i <- 0 until arr.length) arr(i) = i -for (i <- arr.indices) print(s"${arr(i)} ") -println() - -// 4. tarverse, foreach -for (elem <- arr) print(s"$elem ") // elem is a val -println() - -// 5. tarverse, use iterator -val iter = arr.iterator -while (iter.hasNext) - print(s"${iter.next()} ") -println() - -// 6. traverse, use foreach method, pass a function -arr.foreach((elem: Int) => print(s"$elem ")) -println() - -println(arr2.mkString(", ")) // to string directly - -// 7. add element, return a new array, : should toward to object -val newArr = arr :+ 10 // arr.:+(10) add to end -println(newArr.mkString(", ")) -val newArr2 = 20 +: 10 +: arr :+ 30 // arr.+:(10).+:(20).:+(30) -println(newArr2.mkString(", ")) -``` -- 可以看到自定义运算符可以非常灵活,规定如果运算符首尾有`:`那么`:`一定要指向对象。 -- 下标越界当然会抛出异常,使用前应该检查。 -- 通过`Predef`中的隐式转换为一个混入了集合相关特征的包装类型从而得以使用scala的集合相关特征,`Array`类型中并没有相关混入。 - -可变数组: -- 类型`ArrayBuffer`。 -```scala -// 1. create -val arr: ArrayBuffer[Int] = new ArrayBuffer[Int]() -val arr1: ArrayBuffer[Int] = ArrayBuffer(10, 20, 30) -println(arr.mkString(", ")) -println(arr1) // call toString ArrayBuffer(10, 20, 30) - -// 2. visit -arr1(2) = 10 -// 3. add element to tail -var newArr = arr :+ 15 :+ 20 // do not change arr -println(newArr) -newArr = arr += 15 // modify arr itself, add to tail return itself, do notrecommand assign to other var -println(arr) -println(newArr == arr) // true -// 4. add to head -77 +=: arr // WTF? -println(arr) -// 5. insert to middle -arr.insert(1, 10) -println(arr) -// 6. remove element -arr.remove(0, 1) // startIndex, count -println(arr) -arr -= 15 // remove specific element -println(arr) -// 7. convert to Array -val newImmuArr: Array[Int] = arr.toArray -println(newImmuArr.mkString(", ")) -// 8. Array to ArryBuffer -val buffer: scala.collection.mutable.Buffer[Int] = newImmuArr.toBuffer -println(buffer) -``` -- 推荐:不可变集合用运算符,可变集合直接调用对应方法。运算符容易迷惑。 -- 更多方法查看文档和源码用到去找就行。 -- 可变数组和不可变数组可以调用方法互相转换。 - -二维数组: -- 就是数组的数组。 -- 使用`Array.ofDim[Type](firstDim, secondDim, ...)`方法。 -```scala -// create 2d array -val arr: Array[Array[Int]] = Array.ofDim[Int](2, 3) -arr(0)(1) = 10 -arr(1)(0) = 100 - -// traverse -arr.foreach(v => println(v.mkString(","))) -``` - -不可变列表: -- `List`,抽象类,不能直接`new`,使用伴生对象`apply`传入元素创建。 -- `List`本身也有`apply`能随机访问(做了优化),但是不能`update`更改。 -- `foreach`方法遍历。 -- 支持`+: :+`首尾添加元素。 -- `Nil`空列表,`::`添加元素到表头。 -- 常用`Nil.::(elem)`创建列表,换一种写法就是`10 :: 20 :: 30 :: Nil`得到结果`List(10, 20, 30)`,糖是真滴多! -- 合并两个列表:`list1 ::: list2` 或者`list1 ++ list2`。 - -可变列表: -- 可变列表`ListBuffer`,和`ArrayBuffer`很像。 -- `final`的,可以直接`new`,也可以伴生对象`apply`传入元素创建(总体来说scala中更推荐这种方式)。 -- 方法:`append prepend insert remove` -- 添加元素到头或尾:`+=: +=` -- 合并:`++`得到新的列表,`++=`合并到源上。 -- 删除元素也可以用`-=`运算符。 -- 具体操作很多,使用时阅读文档即可。 - -不可变集合: -- 数据无序,不可重复。 -- 可变和不可变都叫`Set`,需要做区分。默认`Set`定义为`immutable.Set`别名。 -- 创建时重复数据会被去除,可用来去重。 -- 添加元素:`set + elem` -- 合并:`set1 ++ set2` -- 移除元素:`set - elem` -- 不改变源集合。 - -可变集合: -- 操作基于源集合做更改。 -- 为了与不可变集合区分,`import scala.collection.mutable`并用`mutable.Set`。 -- 不可变集合有的都有。 -- 添加元素到源上:`set += elem` `add` -- 删除元素:`set -= elem` `remove` -- 合并:`set1 ++= set2` -- 都很简单很好理解,多看文档和源码就行。 - -不可变映射: -- `Map`默认就是`immutable.Map`别名。 -- 两个泛型类型。 -- 基本元素是一个二元组。 -```scala -// create Map -val map: Map[String, Int] = Map("a" -> 13, "b" -> 20) -println(map) -// traverse -map.foreach((kv: (String, Int)) => println(kv)) -map.foreach(kv => println(s"${kv._1} : ${kv._2}")) -// get keys and values -for (key <- map.keys) { - println(s"${key} : ${map.get(key)}") -} -// get value of given key -println(map.get("a").get) -println(map.getOrElse("c", -1)) // avoid excption -println(map("a")) // if no such key will throw exception -// merge -val map2 = map ++ Map("e" -> 1024) -println(map2) -``` - -可变映射: -- `mutable.Map` -- 不可变的都支持。 -```scala -// create mutable Map -val map: mutable.Map[String, Int] = mutable.Map("a" -> 10, "b" -> 20) -// add element -map.put("c", 30) -map += (("d", 40)) // two () represent tuple to avoid ambiguity -println(map) -// remove element -map.remove("a") -map -= "b" // just need key -println(map) -// modify element -map.put("c", 100) // call update, add/modify -println(map) -// merge Map -map ++= Map("a" -> 10, "b" -> 20, "c" -> 30) // add and will override -println(map) -``` - -元组: -- `(elem1, elem2, ...)` 类型可以不同。 -- 最多只能22个元素,从`Tuple1`定义到了`Tuple22`。 -- 使用`_1 _2 _3 ...`访问。 -- 也可以使用`productElement(index)`访问,下标从0开始。 -- `->`创建二元组。 -- 遍历:`for(elem <- tuple.productIterator)` -- 可以嵌套,元组的元素也可以是元组。 - -集合通用属性和方法: -- 线性序列才有长度`length`、所有集合类型都有大小`size`。 -- 遍历`for (elem <- collection)`、迭代器`for (elem <- collection.iterator)`。 -- 生成字符串`toString` `mkString`,像`Array`这种是隐式转换为scala集合的,`toString`是继承自`java.lang.Object`的,需要自行处理。 -- 是否包含元素`contains`。 - -衍生集合的方式: -- 获取集合的头元素`head`(元素)和剩下的尾`tail`(集合)。 -- 集合最后一个元素`last`(元素)和除去最后一个元素的初始数据`init`(集合)。 -- 反转`reverse`。 -- 取前后n个元素`take(n) takeRight(n)` -- 去掉前后n个元素`drop(n) dropRight(n)` -- 交集`intersect` -- 并集`union`,线性序列的话已废弃用`concat`连接。 -- 差集`diff`,得到属于自己、不属于传入参数的部分。 -- 拉链`zip`,得到两个集合对应位置元素组合起来构成二元组的集合,大小不匹配会丢掉其中一个集合不匹配的多余部分。 -- 滑窗`sliding(n, step = 1)`,框住特定个数元素,方便移动和操作。得到迭代器,可以用来遍历,每个迭代的元素都是一个n个元素集合。步长大于1的话最后一个窗口元素数量可能个数会少一些。 - -集合的简单计算操作: -- 求和`sum` 求乘积`product` 最小值`min` 最大值`max` -- `maxBy(func)`支持传入一个函数获取元素并返回比较依据的值,比如元组默认就只会判断第一个元素,要根据第二个元素判断就返回第二个元素就行`xxx.maxBy(_._2)`。 -- 排序`sorted`,默认从小到大排序。从大到小排序`sorted(Ordering[Int].reverse)`。 -- 按元素排序`sortBy(func)`,指定要用来做排序的字段。也可以再传一个隐式参数逆序`sortBy(func)(Ordering[Int].reverse)` -- 自定义比较器`sortWith(cmp)`,比如按元素升序排列`sortWith((a, b) => a < b)`或者`sortWith(_ < _)`,按元组元素第二个元素升序`sortWith(_._2 > _._2)`。 -- 例子: -```scala -object Calculations { - def main(args: Array[String]): Unit = { - // calculations of collections - val list = List(1, 4, 5, 10) - - // sum - var sum = 0 - for (elem <- list) sum += elem - println(sum) - - println(list.sum) - println(list.product) - println(list.min) - println(list.max) - - val list2 = List(('a', 1), ('b', 2), ('d', -3)) - println(list2.maxBy((tuple: (Char, Int)) => tuple._2)) - println(list2.minBy(_._2)) - - // sort, default is ascending - val sortedList = list.sorted - println(sortedList) - // descending - println(list.sorted(Ordering[Int].reverse)) - - // sortBy - println(list2.sortBy(_._2)) - - // sortWith - println(list.sortWith((a, b) => a < b)) - println(list2.sortWith(_._2 > _._2)) - } -} -``` -- 简单操作还是太少了,不足以应对复杂的需求。 - -集合高级计算函数: -- 大数据的处理核心就是映射(map)和规约(reduce)。 -- 映射操作(广义上的map): - - 过滤:自定义过滤条件,`filter(Elem => Boolean)` - - 转化/映射(狭义上的map):自定义映射函数,`map(Elem => NewElem)` - - 扁平化(flatten):将集合中集合元素拆开,去掉里层集合,放到外层中来。`flatten` - - 扁平化+映射:先映射,再扁平化,`flatMap(Elem => NewElem)` - - 分组(group):指定分组规则,`groupBy(Elem => Key)`得到一个Map,key根据传入的函数运用于集合元素得到,value是对应元素的序列。 -- 规约操作(广义的reduce): - - 简化/规约(狭义的reduce):对所有数据做一个处理,规约得到一个结果(比如连加连乘操作)。`reduce((CurRes, NextElem) => NextRes)`,传入函数有两个参数,第一个参数是第一个元素(第一次运算)和上一轮结果(后面的计算),第二个是当前元素,得到本轮结果,最后一轮的结果就是最终结果。`reduce`调用`reduceLeft`从左往右,也可以`reduceRight`从右往左(实际上是递归调用,和一般意义上的从右往左有区别,看下面例子)。 - - 折叠(fold):`fold(InitialVal)((CurRes, Elem) => NextRes)`相对于`reduce`来说其实就是`fold`自己给初值,从第一个开始计算,`reduce`用第一个做初值,从第二个元素开始算。`fold`调用`foldLeft`,从右往左则用`foldRight`(翻转之后再`foldLeft`)。具体逻辑还得还源码。从右往左都有点绕和难以理解,如果要使用需要特别注意。 -- 以上: -```scala -object HighLevelCalculations { - def main(args: Array[String]): Unit = { - val list = List(1, 10, 100, 3, 5, 111) - - // 1. map functions - // filter - val evenList = list.filter(_ % 2 == 0) - println(evenList) - - // map - println(list.map(_ * 2)) - println(list.map(x => x * x)) - - // flatten - val nestedList: List[List[Int]] = List(List(1, 2, 3), List(3, 4, 5), List(10, 100)) - val flatList = nestedList(0) ::: nestedList(1) ::: nestedList(2) - println(flatList) - - val flatList2 = nestedList.flatten - println(flatList2) // equals to flatList - - // map and flatten - // example: change a string list into a word list - val strings: List[String] = List("hello world", "hello scala", "yes no") - val splitList: List[Array[String]] = strings.map(_.split(" ")) // divide string to words - val flattenList = splitList.flatten - println(flattenList) - - // merge two steps above into one - // first map then flatten - val flatMapList = strings.flatMap(_.split(" ")) - println(flatMapList) - - // divide elements into groups - val groupMap = list.groupBy(_ % 2) // keys: 0 & 1 - val groupMap2 = list.groupBy(data => if (data % 2 == 0) "even" else "odd") // keys : "even" & "odd" - println(groupMap) - println(groupMap2) - - val worldList = List("China", "America", "Alice", "Curry", "Bob", "Japan") - println(worldList.groupBy(_.charAt(0))) - - // 2. reduce functions - // narrowly reduce - println(List(1, 2, 3, 4).reduce(_ + _)) // 1+2+3+4 = 10 - println(List(1, 2, 3, 4).reduceLeft(_ - _)) // 1-2-3-4 = -8 - println(List(1, 2, 3, 4).reduceRight(_ - _)) // 1-(2-(3-4)) = -2, a little confusing - - // fold - println(List(1, 2, 3, 4).fold(0)(_ + _)) // 0+1+2+3+4 = 10 - println(List(1, 2, 3, 4).fold(10)(_ + _)) // 10+1+2+3+4 = 20 - println(List(1, 2, 3, 4).foldRight(10)(_ - _)) // 1-(2-(3-(4-10))) = 8, a little confusing - } -} -``` - -集合应用案例: -- Map的默认合并操作是用后面的同key元素覆盖前面的,如果要定制为累加他们的值可以用`fold`。 -```scala -// merging two Map will override the value of the same key -// custom the merging process instead of just override -val map1 = Map("a" -> 1, "b" -> 3, "c" -> 4) -val map2 = mutable.Map("a" -> 6, "b" -> 2, "c" -> 5, "d" -> 10) -val map3 = map1.foldLeft(map2)( - (mergedMap, kv) => { - mergedMap(kv._1) = mergedMap.getOrElse(kv._1, 0) + kv._2 - mergedMap - } -) -println(map3) // HashMap(a -> 7, b -> 5, c -> 9, d -> 10) -``` -- 经典案例:单词计数:分词,计数,取排名前三结果。 -```scala -// count words in string list, and get 3 highest frequency words -def wordCount(): Unit = { - val stringList: List[String] = List( - "hello", - "hello world", - "hello scala", - "hello spark from scala", - "hello flink from scala" - ) - - // 1. split - val wordList: List[String] = stringList.flatMap(_.split(" ")) - println(wordList) - - // 2. group same words - val groupMap: Map[String, List[String]] = wordList.groupBy(word => word) - println(groupMap) - - // 3. get length of the every word, to (word, length) - val countMap: Map[String, Int] = groupMap.map(kv => (kv._1, kv._2.length)) - - // 4. convert map to list, sort and take first 3 - val countList: List[(String, Int)] = countMap.toList - .sortWith(_._2 > _._2) - .take(3) - - println(countList) // result -} -``` -- 单词计数案例扩展,每个字符串都可能出现多次并且已经统计好出现次数,解决方式,先按次数合并之后再按照上述例子处理。 -```scala -// strings has their frequency -def wordCountAdvanced(): Unit = { - val tupleList: List[(String, Int)] = List( - ("hello", 1), - ("hello world", 2), - ("hello scala", 3), - ("hello spark from scala", 1), - ("hello flink from scala", 2) - ) - - val newStringList: List[String] = tupleList.map( - kv => (kv._1.trim + " ") * kv._2 - ) - - // just like wordCount - val wordCountList: List[(String, Int)] = newStringList - .flatMap(_.split(" ")) - .groupBy(word => word) - .map(kv => (kv._1, kv._2.length)) - .toList - .sortWith(_._2 > _._2) - .take(3) - - println(wordCountList) // result -} -``` -- 当然这并不高效,更好的方式是利用上已经统计的频率信息。 -```scala -def wordCountAdvanced2(): Unit = { - val tupleList: List[(String, Int)] = List( - ("hello", 1), - ("hello world", 2), - ("hello scala", 3), - ("hello spark from scala", 1), - ("hello flink from scala", 2) - ) - - // first split based on the input frequency - val preCountList: List[(String, Int)] = tupleList.flatMap( - tuple => { - val strings: Array[String] = tuple._1.split(" ") - strings.map(word => (word, tuple._2)) // Array[(String, Int)] - } - ) - - // group as words - val groupedMap: Map[String, List[(String, Int)]] = preCountList.groupBy(_._1) - println(groupedMap) - - // count frequency of all words - val countMap: Map[String, Int] = groupedMap.map( - kv => (kv._1, kv._2.map(_._2).sum) - ) - println(countMap) - - // to list, sort and take first 3 words - val countList = countMap.toList.sortWith(_._2 > _._2).take(3) - println(countList) -} -``` - -队列: -- 可变队列`mutable.Queue` -- 入队`enqueue(Elem*)` 出队`Elem = dequeue()` -- 不可变队列`immutable.Queue`,使用伴生对象创建,出队入队返回新队列。 - -并行集合(Parllel Collection): -- 使用并行集合执行时会调用多个线程加速执行。 -- 使用集合类前加一个`.par`方法。 -- 具体细节待补。 -- 依赖`scala.collection.parallel.immutable/mutable`,2.13版本后不再在标准库中提供,需要单独下载,暂未找到编好的jar的下载地址,从源码构造需要sbt,TODO。 - -## 模式匹配 - -`match-case`中的模式匹配: -- 用于替代传统C/C++/Java的`switch-case`结构,但补充了更多功能,拥有更强的能力。 -- 语法:(Java中现在也支持`=>`的写法了) -```scala -value match { - case caseVal1 => returnVal1 - case caseVal2 => returnVal2 - ... - case _ => defaultVal -} -``` -- 每一个case条件成立才返回,否则继续往下走。 -- `case`匹配中可以添加模式守卫,用条件判断来代替精确匹配。 -```scala -def abs(num: Int): Int= { - num match { - case i if i >= 0 => i - case i if i < 0 => -i - } -} -``` -- 模式匹配支持类型:所有类型字面量,包括字符串、字符、数字、布尔值、甚至数组列表等。 -- 你甚至可以传入`Any`类型变量,匹配不同类型常量。 -- 需要注意默认情况处理,`case _`也需要返回值,如果没有但是又没有匹配到,就抛出运行时错误。默认情况`case _`不强制要求通配符(只是在不需要变量的值建议这么做),也可以用`case abc`一个变量来接住,可以什么都不做,可以使用它的值。 -- 通过指定匹配变量的类型(用特定类型变量接住),可以匹配类型而不匹配值,也可以混用。 -- 需要注意类型匹配时由于泛型擦除,可能并不能严格匹配泛型的类型参数,编译器也会报警告。但`Array`是基本数据类型,对应于java的原生数组类型,能够匹配泛型类型参数。 -```scala -// match type -def describeType(x: Any) = x match { - case i: Int => "Int " + i - case s: String => "String " + s - case list: List[String] => "List " + list - case array: Array[Int] => "Array[Int] " + array - case a => "Something else " + a -} -println(describeType(20)) // match -println(describeType("hello")) // match -println(describeType(List("hi", "hello"))) // match -println(describeType(List(20, 30))) // match -println(describeType(Array(10, 20))) // match -println(describeType(Array("hello", "yes"))) // not match -println(describeType((10, 20))) // not match -``` -- 对于数组可以定义多种匹配形式,可以定义模糊的元素类型匹配、元素数量匹配或者精确的某个数组元素值匹配,非常强大。 -```scala -for (arr <- List( - Array(0), - Array(1, 0), - Array(1, 1, 0), - Array(10, 2, 7, 5), - Array("hello", 20, 50) -)) { - val result = arr match { - case Array(0) => "0" - case Array(1, 0) => "Array(1, 0)" - case Array(x: Int, y: Int) => s"Array($x, $y)" // Array of two elements - case Array(0, _*) => s"an array begin with 0" - case Array(x, 1, z) => s"an array with three elements, no.2 is 1" - case Array(x:String, _*) => s"array that first element is a string" - case _ => "somthing else" - } - println(result) -``` -- `List`匹配和`Array`差不多,也很灵活。还可用用集合类灵活的运算符来匹配。 - - 比如使用`::`运算符匹配`first :: second :: rest`,将一个列表拆成三份,第一个第二个元素和剩余元素构成的列表。 -- 注意模式匹配不仅可以通过返回值当做表达式来用,也可以仅执行语句类似于传统`switch-case`语句不关心返回值,也可以既执行语句同时也返回。 -- 元组匹配: - - 可以匹配n元组、匹配元素类型、匹配元素值。如果只关心某个元素,其他就可以用通配符或变量。 - - 元组大小固定,所以不能用`_*`。 - - -变量声明匹配: -- 变量声明也可以是一个模式匹配的过程。 -- 元组常用于批量赋值。 -- `val (x, y) = (10, "hello")` -- `val List(first, second, _*) = List(1, 3, 4, 5)` -- `val List(first :: second :: rest) = List(1, 2, 3, 4)` - -`for`推导式中也可以进行模式匹配: -- 元组中取元素时,必须用`_1 _2 ...`,可以用元组赋值将元素赋给变量,更清晰一些。 -- `for ((first, second) <- tupleList)` -- `for ((first, _) <- tupleList)` -- 指定特定元素的值,可以实现类似于循环守卫的功能,相当于加一层筛选。比如`for ((10, second) <- tupleList)` -- 其他匹配也同样可以用,可以关注数量、值、类型等,相当于做了筛选。 -- 元组列表匹配、赋值匹配、`for`循环中匹配非常灵活,灵活运用可以提高代码可读性。 - -匹配对象: -- 对象内容匹配。 -- 直接`match-case`中匹配对应引用变量的话语法是有问题的。编译报错信息提示:不是样例类也没有一个合法的`unapply/unapplySeq`成员实现。 -- 要匹配对象,需要实现伴生对象`unapply`方法,用来对对象属性进行拆解以做匹配。 - -样例类: -- 第二种实现对象匹配的方式是样例类。 -- `case class className`定义样例类,会直接将打包`apply`和拆包`unapply`的方法直接定义好。 -- 样例类定义中主构造参数列表中的`val`甚至都可以省略,如果是`var`的话则不能省略,最好加上的感觉,奇奇怪怪的各种边角简化。 - -对象匹配和样例类例子: -```scala -object MatchObject { - def main(args: Array[String]): Unit = { - val person = new Person("Alice", 18) - - val result: String = person match { - case Person("Alice", 18) => "Person: Alice, 18" - case _ => "something else" - } - println(result) - - val s = Student("Alice", 18) - val result2: String = s match { - case Student("Alice", 18) => "Student: Alice, 18" - case _ => "something else" - } - println(result2) - } -} - - -class Person(val name: String, val age: Int) -object Person { - def apply(name: String, age: Int) = new Person(name, age) - def unapply(person: Person): Option[(String, Int)] = { - if (person == null) { // avoid null reference - None - } else { - Some((person.name, person.age)) - } - } -} - -case class Student(name: String, age: Int) // name and age are vals -``` - -偏函数: -- 偏函数是函数的一种,通过偏函数我们可以方便地对参数做更精确的检查,例如偏函数输入类型是`List[Int]`,需要第一个元素是0的集合,也可以通过模式匹配实现的。 -- 定义: -```scala -val partialFuncName: PartialFunction[List[Int], Option[Int]] = { - case x :: y :: _ => Some(y) -} -``` -- 通过一个变量定义方式定义,`PartialFunction`的泛型类型中,前者是参数类型,后者是返回值类型。函数体中用一个`case`语句来进行模式匹配。上面例子返回输入的`List`集合中的第二个元素。 -- 一般一个偏函数只能处理输入的一部分场景,实际中往往需要定义多个偏函数用以组合使用。 -- 例子: -```scala -object PartialFunctionTest { - def main(args: Array[String]): Unit = { - val list: List[(String, Int)] = List(("a", 12), ("b", 10), ("c", 100), ("a", 5)) - - // keep first constant and double second value of the tuple - // 1. use map - val newList = list.map(tuple => (tuple._1, tuple._2 * 2)) - println(newList) - - // 2. pattern matching - val newList1 = list.map( - tuple => { - tuple match { - case (x, y) => (x, y * 2) - } - } - ) - println(newList1) - - // simplify to partial function - val newList2 = list.map { - case (x, y) => (x, y * 2) // this is a partial function - } - println(newList2) - - // application of partial function - // get absolute value, deal with: negative, 0, positive - val positiveAbs: PartialFunction[Int, Int] = { - case x if x > 0 => x - } - val negativeAbs: PartialFunction[Int, Int] = { - case x if x < 0 => -x - } - val zeroAbs: PartialFunction[Int, Int] = { - case 0 => 0 - } - - // combine a function with three partial functions - def abs(x: Int): Int = (positiveAbs orElse negativeAbs orElse zeroAbs) (x) - println(abs(-13)) - println(abs(30)) - println(abs(0)) - } -} -``` -## 异常处理 - -scala异常处理整体上的语法和底层处理细节和java非常类似。 - -Java异常处理: -- 用`try`语句包围要捕获异常的块,多个不同`catch`块用于捕获不同的异常,`finally`块中是捕获异常与否都会执行的逻辑。 -```java -try { - int a = 0; - int b = 0; - int c = a / b; -} catch (ArithmeticException e) { - e.printStackTrace(); -} catch (Exception e) { - e.printStackTrace(); -} finally { - System.out.println("finally"); -} -``` - -scala异常处理: -- `try`包围要捕获异常的内容,`catch`仅仅是关键字,将捕获异常的所有逻辑包围在`catch`块中。`finally`块和java一样都会执行,一般用于对象的清理工作。 -- scala中没有编译期异常,所有异常都是运行时处理。 -- scala中也是用`throw`关键字抛出异常,所有异常都是`Throwable`的子类,`throw`表达式是有类型的,就是`Nothing`。`Nothing`主要用在一个函数总是不能正常工作,总是抛出异常的时候用作返回值类型。 -- java中用了`throws`关键字声明此方法可能引发的异常信息,在scala中对应地使用`@throws[ExceptionList]`注解来声明,用法差不多。 -```scala -object Exceptionstest { - def main(args: Array[String]): Unit = { - // test of exceptions - try { - val n = 10 / 0 - } catch { - case e: ArithmeticException => { - println(s"ArithmeticException raised.") - } - case e: Exception => { - println("Normal Exceptions raised.") - } - } finally { - println("finally") - } - } -} -``` - -## 隐式转换 - -前面说了很多了,编译器做隐式转换的时机: -- 编译器第一次编译失败时,会在当前环境中查找能让代码编译通过的方法,将类型隐式转换,尝试二次编译。 - -隐式函数: -- 函数定义前加上`implicit`声明为隐式转换函数。 -- 当编译错误时,编译器会尝试在当前作用域范围查找能调用对应功能的转换规则,这个过程由编译器完成,称之为隐式转换或者自动转换。 -```scala -// convert Int to MyRichInt -implicit def convert(arg: Int): MyRichInt = { - new MyRickInt(arg) -} -``` -- 在当前作用域定义时需要在使用前定义才能找到。 -```scala -object ImplicitConversion { - def main(args: Array[String]): Unit = { - implicit def convert(num: Int): MyRichInt = new MyRichInt(num) - - println(12.myMax(15)) // will call convert implicitly - } -} - -class MyRichInt(val self: Int) { - // self define compare method - def myMax(n: Int): Int = if (n < self) self else n - def myMin(n: Int): Int = if (n > self) self else n -} -``` - -隐式参数: -- 普通方法或者函数中的参数可以通过`implicit`关键字声明为隐式参数,调用方法时,如果传入了,那么以传入参数为准。如果没有传入,编译器会在当前作用域寻找复合条件的隐式值。例子:集合排序方法的排序规则就是隐式参数。 -- 隐式值: - - 同一个作用域,相同类型隐式值只能有一个。 - - 编译器按照隐式参数的类型去寻找对应隐式值,与隐式值名称无关。 - - 隐式参数优先于默认参数。(也就是说隐式参数和默认参数可以同时存在,加上默认参数之后其实就相当于两个不同优先级的默认参数) -- 隐式参数有一个很淦的点: - - 如果参数列表中只有一个隐式参数,无论这个隐式参数是否提供默认参数,那么如果要用这个隐式参数就应该**将调用隐式参数的参数列表连同括号一起省略掉**。如果调用时又想加括号可以在函数定义的隐式参数列表前加一个空参数列表`()`,那么`()`将使用隐式参数,`()()`将使用默认参数(如果有,没有肯定编不过),`()(arg)`使用传入参数。 - - 也就是说一个隐式参数时通过是否加括号可以区分隐式参数、默认参数、传入参数三种情况。 - - 那么如果多参数情况下:隐式参数、默认参数、普通参数排列组合在一个参数列表中混用会怎么样呢?没有试验过,不要这么用,思考这些东西搞什么哦! - - 具体要不要加这个柯里化的空参数列表,那看习惯就行。不加可能更好一点,加了可能有点让人费解。 -- 可以进一步简写隐式参数,在参数列表中直接去掉,在函数中直接使用`implicity[Type]`(`Predef`中定义的)。但这时就不能传参数了,有什么用啊?相当于一个在自己作用域范围内起作用的全局量? -```scala -object ImplicitArgments { - def main(args: Array[String]): Unit = { - implicit val str: String = "Alice from implicit argument" - - def sayHello()(implicit name: String = "Alice from default argument"): Unit = { - println(s"hello $name") - } - - sayHello() // implicit - sayHello()() // default - sayHello()("Alice from normal argument") // normal - - def sayHi(implicit name: String = "Alice from default argument"): Unit = { - println(s"hi $name") - } - - sayHi // implicit - sayHi() // default - sayHi("Alice from normal argument") // normal - - def sayBye() = { - println(s"bye ${implicitly[String]}") - } - - sayBye() - } -} -``` - - -隐式类: -- scala2.10之后提供了隐式类,使用`implicit`声明为隐式类。将类的构造方法声明为隐式转换函数。 -- 也就是说如果编译通不过,就可能将数据直接传给构造转换为对应的类。 -- 隐式函数的一个扩展。 -- 说明: - - 所带构造参数有且只能有一个。 - - 隐式类必须被定义在类或者伴生对象或者包对象中,隐式类不能是顶层的。 -- 同一个作用域定义隐式转换函数和隐式类会冲突,定义一个就行。 - -隐式解析机制的作用域: -- 首先在**当前代码作用域下**查找隐式实体(隐式方法、隐式类、隐式对象)。 -- 如果第一条规查找隐式对象失败,会继续在**隐式参数的类型的作用域**中查找。 -- 类型的作用域是指该类型相关联的全部伴生对象以及该类型所在包的包对象。 - -作用: -- 隐式函数和隐式类可以用于扩充类的功能,常用语比如内建类`Int Double String`这种。 -- 隐式参数相当于就是一种更高优先级的默认参数。用于多个函数需要同一个默认参数时,就不用每个函数定义时都写一次默认值了。为了简洁无所不用其极啊真是。 - -## 泛型 - -泛型: -- `[TypeList]`,定义和使用都是。 -- 常用于集合类型中用于支持不同元素类型。 -- 和java一样通过类型擦除/擦拭法来实现。 -- 定义时可以用`+-`表示协变和逆变,不加则是不变。 -```scala -class MyList[+T] {} // 协变 -class MyList[-T] {} // 逆变 -class MyList[T] {} // 不变 -``` - -协变和逆变: -- 比如`Son`和`Father`是父子关系,`Son`是子类。 - - 协变(Covariance):`MyList[Son]`是`MyList[Father]`的子类,协同变化。 - - 逆变(Contravariance):`MyList[Son]`是`MyList[Father]`的父类,逆向变化。 - - 不变(Invariant):`MyList[Father] MyList[Son]`没有父子关系。 -- 还需要深入了解。 - -泛型上下限: -- 泛型上限:`class MyList[T <: Type]`,可以传入`Type`自身或者子类。 -- 泛型下限:`class MyList[T >: Type]`,可以传入`Type`自身或者父类。 -- 对传入的泛型进行限定。 - -上下文限定: -- `def f[A : B](a: A) = println(a)`等同于`def f[A](a: A)(implicit arg: B[A])` -- 是将泛型和隐式转换结合的产物,使用上下文限定(前者)后,方法内无法使用隐式参数名调用隐式参数,需要通过`implicitly[Ordering[A]]`获取隐式变量。 -- 了解即可,可能基本不会用到。 - -## Style Guide - -[官方的Style Guide](https://docs.scala-lang.org/style/index.html)中的一些建议: -- 缩进鼓励为2个,当然我上面都是用的4个。Scala中很多时候嵌套层次会很深,也鼓励这样做,模式匹配、匿名函数、循环、条件等各种嵌套,层次深了之后4空格可能会比较折磨。 -- 一个表达式一行放不下要换行时,语义上不会产生歧义就行,比如一个运算符放在末尾将其必需的操作数换到下一行。 -- 多参数函数调用需要换行书写时,将第一个参数放到第二行并缩进2个空格书写,而不是第一个参数放到第一行,然后缩进到对齐(典型java风格)。 -```scala -// right! -val myLongFieldNameWithNoRealPoint = - foo( - someVeryLongFieldName, - andAnotherVeryLongFieldName, - "this is a string", - 3.1415) - -// wrong! -val myLongFieldNameWithNoRealPoint = foo(someVeryLongFieldName, - andAnotherVeryLongFieldName, - "this is a string", - 3.1415) -``` -- 仅介绍第一页的内容,也没有空去看完,以后真写得多了再去看。 - -## sbt - -上面已经简单介绍了IDEA使用Maven项目编写Scala的配置,但学习scala,官方的构建工具sbt还是必须要了解一下的。 - -关于SBT: -- SBT是Scala的构建工具,全称Simple Build Tool,类似于 Maven 或 Gradle。 -- [GETTING STARTED WITH SCALA AND SBT ON THE COMMAND LINE](https://docs.scala-lang.org/getting-started/sbt-track/getting-started-with-scala-and-sbt-on-the-command-line.html) -- [sbt Reference Manual](https://www.scala-sbt.org/1.x/docs/index.html) 要使用sbt,阅读完第一章Getting Started with sbt是必要的。下面的内容皆是第一章翻译。 - -特性: -- 简单项目零配置。 -- 用Scala源码管理项目构建。 -- 精确的重编译,节省时间。 -- 使用Coursier的库管理器。 -- 支持Scala和Java的混合项目。 -- 等等等,具体就不列了,总之一个大型项目构建系统该有的东西。 - -安装: -- sbt依赖Java,确保已经安装了JDK1.8或以上版本。 -- 下载压缩包或者安装包,这里的版本是1.5.5。 -- 解压或者安装。 -- 配置环境变量`SBT_HOME`,并添加`%SBT_HOME%\bin`到path环境变量,安装包的话会自动配置。 - -### 通过案例入门sbt - -创建一个项目hello作为例子: - -- windows上没有的命令按照含义操作即可。 -```shell -$ mkdir foo-build -$ cd foo-build -$ touch build.sbt -``` - -开始sbt shell: -```shell -$ sbt -[info] Updated file /tmp/foo-build/project/build.properties: set sbt.version to 1.1.4 -[info] Loading project definition from /tmp/foo-build/project -[info] Loading settings from build.sbt ... -[info] Set current project to foo-build (in build file:/tmp/foo-build/) -[info] sbt server started at local:///Users/eed3si9n/.sbt/1.0/server/abc4fb6c89985a00fd95/sock -sbt:foo-build> -``` -- 第一次初始化时间会很长。 -- 退出shell: -```shell -sbt:foo-build> exit -``` -- 编译: -```shell -sbt:foo-build> compile -``` -- sbt shell中Tab可以补全。 - -对修改重新编译: -- 在`compile`命令(或其他命令同理)前加一个`~`前缀,会进入等待状态,当项目发生修改是会自动重新编译。当然退出这个状态后就不会了。 -```shell -sbt:foo-build> ~compile -[success] Total time: 0 s, completed May 6, 2018 3:52:08 PM -1. Waiting for source changes... (press enter to interrupt) -``` - -创建源文件: -- 执行`~compile`并保持,创建目录`src/main/scala/example`新建源文件保存就能看到编译过程了。 -```scala -// src/main/scala/example/Hello.scala -package example - -object Hello extends App { - println("Hello") -} -``` - -sbt shell常用操作: -- `help`帮助。 -- `help run`具体条目的帮助。 -- `run`运行程序。 -- 上下箭头切换已执行命令。 -- `scalaVersion` scala版本。 - -配置修改: -- 切换当前项目的scala版本:`set ThisBuild / scalaVersion := "2.13.6"`。 -- `session save`保存配置到`build.sbt`,此时其中就会多出`ThisBuild / scalaVersion := "2.13.6"`。 -- 编辑`build.sbt`: -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -lazy val hello = (project in file(".")) - .settings( - name := "Hello" - ) -``` -- 重新加载配置`reload`。 - -测试: -- 添加ScalaTest到依赖 -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -lazy val hello = (project in file(".")) - .settings( - name := "Hello", - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.7" % Test, - ) -``` -- 执行测试:`test`。 -- 后续继续运行追加的测试:`~testQuick` - -编写测试:`src/test/scala/HelloSpec.scala` -```scala -// src/test/scala/HelloSpec.scala -import org.scalatest.funsuite._ - -class HelloSpec extends AnyFunSuite { - test("Hello should start with H") { - assert("hello".startsWith("H")) - } -} -``` -- 测试结果当然是失败 -```shell -sbt:Hello> test -[info] HelloSpec: -[info] - Hello should start with H *** FAILED *** -[info] "hello" did not start with "H" (HelloSpec.scala:5) -[info] Run completed in 214 milliseconds. -[info] Total number of tests run: 1 -[info] Suites: completed 1, aborted 0 -[info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 -[info] *** 1 TEST FAILED *** -[error] Failed tests: -[error] HelloSpec -[error] (Test / test) sbt.TestsFailedException: Tests unsuccessful -[error] Total time: 0 s, completed 2021年9月27日 下午11:58:01 -``` -- 改一下源码再测试就能通过了: -```scala -// src/test/scala/HelloSpec.scala -import org.scalatest.funsuite._ - -class HelloSpec extends AnyFunSuite { - test("Hello should start with H") { - assert("Hello".startsWith("H")) - } -} -``` - -添加库依赖: -```scala -// build.sbt -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -lazy val hello = (project in file(".")) - .settings( - name := "Hello", - libraryDependencies += "com.typesafe.play" %% "play-json" % "2.9.2", - libraryDependencies += "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0", - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.7" % Test, - ) -``` - - -使用REPL(Read-Eval-Print Loop): -```shell -sbt:Hello> console -``` -- 在scala的REPL环境中粘贴:`:paste`。 -- 退出:`:q` - -修改`build.sbt`创建一个子项目: -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -lazy val hello = (project in file(".")) - .settings( - name := "Hello", - libraryDependencies += "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0", - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.7" % Test, - ) - -lazy val helloCore = (project in file("core")) - .settings( - name := "Hello Core", - ) -``` -- `reload`时会自动创建目录`core/`。 -- 列出所有子项目:`projects` -- 编译子项目: -```shell -helloCore/compile -``` -- 子项目添加依赖: -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -val scalaTest = "org.scalatest" %% "scalatest" % "3.2.7" - -lazy val hello = (project in file(".")) - .settings( - name := "Hello", - libraryDependencies += "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0", - libraryDependencies += scalaTest % Test, - ) - -lazy val helloCore = (project in file("core")) - .settings( - name := "Hello Core", - libraryDependencies += scalaTest % Test, - ) -``` - -广播命令、添加依赖: -- 设置`.aggregate(...)`,这样发送到`hello`的命令都会被广播到`helloCore` -- 使用`.dependsOn(...)`可以设置依赖,下面的设置使`hello`依赖于`helloCore` -- 将Gigahorse的依赖移到`helloCore`。 -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -val scalaTest = "org.scalatest" %% "scalatest" % "3.2.7" -val gigahorse = "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0" - -lazy val hello = (project in file(".")) - .aggregate(helloCore) - .dependsOn(helloCore) - .settings( - name := "Hello", - libraryDependencies += scalaTest % Test, - ) - -lazy val helloCore = (project in file("core")) - .settings( - name := "Hello Core", - libraryDependencies += scalaTest % Test, - libraryDependencies += gigahorse, - ) -``` - -使用Play JSON解析JSON: -- 添加依赖。 -```scala -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -val scalaTest = "org.scalatest" %% "scalatest" % "3.2.7" -val gigahorse = "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0" -val playJson = "com.typesafe.play" %% "play-json" % "2.9.2" - -lazy val hello = (project in file(".")) - .aggregate(helloCore) - .dependsOn(helloCore) - .settings( - name := "Hello", - libraryDependencies += scalaTest % Test, - ) - -lazy val helloCore = (project in file("core")) - .settings( - name := "Hello Core", - libraryDependencies ++= Seq(gigahorse, playJson), - libraryDependencies += scalaTest % Test, - ) -``` -- 重载,添加文件:`core/src/main/scala/example/core/Weather.scala` -```scala -// core/src/main/scala/example/core/Weather.scala -package example.core - -import gigahorse._, support.okhttp.Gigahorse -import scala.concurrent._, duration._ -import play.api.libs.json._ - -object Weather { - lazy val http = Gigahorse.http(Gigahorse.config) - - def weather: Future[String] = { - val baseUrl = "https://www.metaweather.com/api/location" - val locUrl = baseUrl + "/search/" - val weatherUrl = baseUrl + "/%s/" - val rLoc = Gigahorse.url(locUrl).get. - addQueryString("query" -> "New York") - import ExecutionContext.Implicits.global - for { - loc <- http.run(rLoc, parse) - woeid = (loc \ 0 \ "woeid").get - rWeather = Gigahorse.url(weatherUrl format woeid).get - weather <- http.run(rWeather, parse) - } yield (weather \\ "weather_state_name")(0).as[String].toLowerCase - } - - private def parse = Gigahorse.asString andThen Json.parse -} -``` -- 修改`src/main/scala/example/Hello.scala`: -```scala -package example - -import scala.concurrent._, duration._ -import core.Weather - -object Hello extends App { - val w = Await.result(Weather.weather, 10.seconds) - println(s"Hello! The weather in New York is $w.") - Weather.http.close() -} -``` -- 运行:`run` -```shell -sbt:Hello> run -[info] running example.Hello -Hello! The weather in New York is light cloud. -[success] Total time: 5 s, completed 2021年9月28日 上午10:29:57 -``` - -添加sbt-native-packager插件: -- 创建`project/plugins.sbt` -```scala -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4") -``` -- 修改`build.sbt`对`Hello`项目添加`.enablePlugins(JavaAppPackaging)` -- 重载,本地没有执行成功,所以下面的`dist`命令也就不能用。 - -重载并创建.zip分发包:`dist` -```shell -sbt:Hello> dist -[info] Wrote /tmp/foo-build/target/scala-2.12/hello_2.12-0.1.0-SNAPSHOT.pom -[info] Wrote /tmp/foo-build/core/target/scala-2.12/hello-core_2.12-0.1.0-SNAPSHOT.pom -[info] Your package is ready in /tmp/foo-build/target/universal/hello-0.1.0-SNAPSHOT.zip -``` - -应用容器化: -- `Docker/publishLocal` -- 运行容器化后的应用:`docker run hello:0.1.0-SNAPSHOT` - -设置应用版本: -```scala -// build.sbt -ThisBuild / version := "0.1.0" -ThisBuild / scalaVersion := "2.13.6" -ThisBuild / organization := "com.example" - -val scalaTest = "org.scalatest" %% "scalatest" % "3.2.7" -val gigahorse = "com.eed3si9n" %% "gigahorse-okhttp" % "0.5.0" -val playJson = "com.typesafe.play" %% "play-json" % "2.9.2" - -lazy val hello = (project in file(".")) - .aggregate(helloCore) - .dependsOn(helloCore) - .enablePlugins(JavaAppPackaging) - .settings( - name := "Hello", - libraryDependencies += scalaTest % Test, - ) - -lazy val helloCore = (project in file("core")) - .settings( - name := "Hello Core", - libraryDependencies ++= Seq(gigahorse, playJson), - libraryDependencies += scalaTest % Test, - ) -``` - -临时切换Scala版本: -- `++2.12.14` - -在Bash中直接运行sbt的命令: -```shell -sbt clean "testOnly HelloSpec" -``` -- 这样程序运行起来会慢一些。 -- 连续的开发的话,推荐使用sbt shell或者连续测试比如`~testQuick`。 - -`new`命令: -```shell -$ sbt new scala/scala-seed.g8 -.... -A minimal Scala project. - -name [My Something Project]: hello - -Template applied in ./hello -``` -- 会创建一个简单的项目,要求输入项目名时输入`hello`,会在`hello/`下创建一个新项目。 - - -### sbt使用 - -项目的目录结构: -- base directory是包含项目的目录,这里称为项目根目录。 -- sbt使用和Maven一样的源码结构,源文件路径都是基于项目根目录的相对路径。 -``` -src/ - main/ - resources/ - - scala/ -
- scala-2.12/ -
- java/ -
- test/ - resources - - scala/ - - scala-2.12/ - - java/ - -``` -- 其他`src/`中的目录会被忽略,所有隐藏目录都会被忽略。 -- 源码可以被放在根目录的`hello/app.scala`,对小项目是可行的。然而一般来说,人们倾向于将项目放在`src/main/`下面来保证事情能够有条理地进行。如果你自行管理定制了项目的构建的话,自定义源码的位置也是可行的。 -- sbt的构建定义文件:`build.sbt` -- 除此之外,`project`目录中的`.scala`文件可以定义项目帮助文件和一次性的插件。 -``` -build.sbt -project/ - Dependencies.scala -``` -- 生成文件:`.class`,生成的`jar`,其他文件和文档等会被默认输出到`target`目录。 -- 一般生成文件应该要排除在版本控制之外,在`.gitignore`中添加: -``` -target/ -``` - - -运行: -- 运行sbt shell:`sbt`无参数运行,进入sbt的提示符,有tab补全和执行历史。 -- 编译:`compile` -- 运行:`run` -- 不进入sbt shell直接运行sbt命令:用`""`包起来表示是一个命令,相对来说会慢一些。 -``` -sbt clean compile "testOnly TestA TestB" -``` -- 会一次执行`clean compile testOnly`,`TestA TestB`是传给`testOnly`的参数。 -- 保存文件时自动重编译运行测试:`~testQuick`。 -- 命令加上`~`后会进入循环模式,保存文件都会自动运行。回车退出。 -- 常用命令: - -Command|Description -:-:|:- -clean|Deletes all generated files (in the target directory). -compile|Compiles the main sources (in src/main/scala and src/main/java directories). -test|Compiles and runs all tests. -console|Starts the Scala interpreter with a classpath including the compiled sources and all dependencies. To return to sbt, type :quit, Ctrl+D (Unix), or Ctrl+Z (Windows). -run argument*|Runs the main class for the project in the same virtual machine as sbt. -package|Creates a jar file containing the files in src/main/resources and the classes compiled from src/main/scala and src/main/java. -help command|Displays detailed help for the specified command. If no command is provided, displays brief descriptions of all commands. -reload|Reloads the build definition (build.sbt, project/*.scala, project/*.sbt files). Needed if you change the build definition. - -### build.sbt - -`build.sbt`构建定义: -- 指定sbt版本,这样就使用不同版本的sbt构建同一个项目了。如果指定的版本不可用,那么会自动下载。 -```scala -sbt.version=1.5.5 -``` -- 构建定义(build definition): -- 包含了一系列项目([Scala中的Project类型](https://www.scala-sbt.org/1.x/api/sbt/Project.html)),项目这个名词有一定的模糊性,所以其中的一个个项目一般将之称为子项目。 -- 在`build.sbt`中可以定义一个在当前目录中的子项目: -```scala -lazy val root = (project in file(".")) - .settings( - name := "Hello", - scalaVersion := "2.12.7" - ) -``` -- 项目的名称在`.setting`方法中用一个键值对定义,key是`name`,值是一个字符串表示项目名称。 -- `build.sbt`定义所有的子项目,包含一些的键值对称为`setting`表达式,使用一门build.sbt DSL(本质上其实就是Scala)来定义项目。 -```scala -ThisBuild / organization := "com.example" -ThisBuild / scalaVersion := "2.12.14" -ThisBuild / version := "0.1.0-SNAPSHOT" - -lazy val root = (project in file(".")) - .settings( - name := "hello" - ) -``` -- 看一看这门DSL的定义: -![setting expression](Images/Scala_sbt_dsl_setting_expression.png) -- 每一个都叫做一个setting expression,其中的一些又被叫做task expression。 -- 一个setting expression包含三部分: - 1. 左边是key。 - 2. 操作符,这个例子中是`:=` - 3. 右边是setting body。 -- 一个key的类型是`sbt.SettingKey[T] sbt.TaskKey[T] sbt.InputKey[T]`其中一者的实例,T是期望的值类型。 -- 比如`name`就绑定到了`SettingKey[String]`类型,给个其他类型比如整数的话就会编译错误。 -- 在`build.sbt`中可以穿插`val` `lazy val` `def`,但是不能有顶层`object class`定义。 - -expression的key: -- 三种类型: - - `SettingKey[T]` 值仅在加载子项目时计算一次,然后保持。 - - `TaskKey[T]` 值被称为一个任务(task),每次都会重新计算(何时?),存在潜在的副作用。 - - `InputKey[T]` 值是有命令行输入作为参数的任务,细节见[Input Tasks](https://www.scala-sbt.org/1.x/docs/Input-Tasks.html)。 -- 内建的keys就是`sbt.Keys`单例伴生对象的域。`build.sbt`隐式导入`import sbt.Keys._`,`sbt.Keys.name`就是`name`,所以其实就是对这些字段做赋值。 -- 自定义key:使用各自的方法`settingKey taskKey inputKey`,每个方法需要一个value的类型和描述。key的名称就是被赋值到的引用变量名称。 -- 定义一个自定义key,名称是`hello`,类型是`TaskKey`,对应值类型是`Unit`: -```scala -lazy val hello = taskKey[Unit]("An example task") -``` -- 所有这种定义都在设置前被求值,无论定义在文件什么位置。 -- 一般来说,使用`lazy val`而不是`val`来避免初始化顺序导致的问题。 -- Task和Setting区别: - - Task是任务,比如`compile` `package`都是`sbt.Keys`中的域,同时也是sbt shell中可执行的命令。应该返回`Unit`或者返回和这个任务相关的值,比如`package`是`TaskKey[File]`值是其创建的jar文件。 - - 每一次开始一个任务,比如sbt shell中执行`compile`,sbt都会重新跑(仅)一次这个任务相关的所有任务。 - - 而Setting仅仅只是一个朴素的设置项。 - -定义任务和设置: -- 使用`:=`可以将一个设置或者一项计算任务赋值。设置只会在记载项目时计算一次,任务则会在每次执行这个任务被执行时重新执行。 -- 新建一个任务: -```scala -lazy val hello = taskKey[Unit]("An example task") - -lazy val root = (project in file(".")) - .settings( - hello := { println("Hello!") } - ) -``` -- 任务也在`.settings`中被赋值。 -- 每次在sbt shell中执行`hello`都会执行其中`println`语句。 -- 定义设置的话已经说过。 -- 从类型系统的视角来看,对任务赋值得到一个`Setting[Task[T]]`,对设置赋值得到`Setting[T]`。`T Task[T]`的区别有一层隐含的含义:一个设置不能依赖于一个任务。因为设置仅记载是求值一次不会每次都重新运行。 - -sbt shell中的key: -- sbt shell中可以输入任何任务名称都会运行该任务,因为这个任务名称是key。运行该任务但并不会显示运行结果值(也就是返回值,类型就是`taskKey[T]`中的`T`),要显示结果值,应该使用`show `而不是单纯的``。 -- 如果输入设置的key的话,会显示设置的值。 -- 要知道一个key的更多信息,可以使用`inspect `。某些信息现在看起来可能不知道含义,但最顶上有类型和简要描述。 - -在`build.sbt`中导入信息: -- 比如: -```scala -import sbt._ -import Keys._ -``` -- 中间不能有空行。 -- 如果有自动插件([`sbt.AutoPlugin`](https://www.scala-sbt.org/1.x/api/sbt/AutoPlugin.html),可以从其派生实现自己的插件),那么在其中的`autoImport`单例对象下的名称会被自动导入。 - -Bare .sbt build definition: -- 也就是裸的构建定义,设置可以被直接写到`.sbt`而不是项目的`.setting(...)`调用下,称之为bare style。 -```scala -ThisBuild / version := "1.0" -ThisBuild / scalaVersion := "2.12.14" -``` -- 这种语法推荐用来写在`ThisBuild`作用域下的设置和添加插件。后续会有作用域和插件的说明。 - -添加库依赖: -- 为了能够依赖第三方库,有两种方式: -- 一是将jar文件直接放在`lib/`(未管理的依赖)目录下,第二种是添加管理的依赖,通过在`.setting(..)`调用中对`libraryDependencies`key做`+=`操作做到。 -```scala -val derby = "org.apache.derby" %% "derby" % "10.4.1.3" - -ThisBuild / organization := "com.example" -ThisBuild / scalaVersion := "2.12.14" -ThisBuild / version := "0.1.0-SNAPSHOT" - -lazy val root = (project in file(".")) - .settings( - name := "Hello", - libraryDependencies += derby - ) -``` -- 包含包的组织、包名、和包的版本。可以定义变量来复用。`%`运算符被用来从字符串构建一个模块ID。 - -### 多项目构建 - -多个项目定义: -- 在一次构建中编译多个相关联的子项目是很有用的,特别是其间存在依赖,想更改他们所有的时候。 -- 每个子项目都有自己的目录,构建时都会生成自己的jar文件, -- 项目使用`lazy val`定义一个[sbt.Project](https://www.scala-sbt.org/1.x/api/sbt/Project.html)来实现。 -```scala -lazy val util = (project in file("util")) - -lazy val core = (project in file("core")) -``` -- 这个`val`的不变量名称被用做子项目的ID(也即是项目名称),在sbt shell中也用来指代一个子项目。 -- 后面的`in file()`调用指定他们的base directory是可选的,目录名就是他们的名称。 -```scala -lazy val util = project - -lazy val core = project -``` - -公共设置; -- 为了分离出跨子项目的设置,可以将其定义在`ThisBuild`范围下。`ThisBuild`表现的像一个普通的子项目名称一样使用,其下用来定义默认值。 -- 如果定义了多个子项目,并且子项目下没有定义比如`scalaVersion`这个key,就会查找`ThisBuild / scalaVersion`。 -- 这样定义的限制是右边的值只能是纯粹的值或者在`Global`/`ThisBuild`下的设置。 -- 子项目范围下没有默认值。 -```scala -ThisBuild / organization := "com.example" -ThisBuild / version := "0.1.0-SNAPSHOT" -ThisBuild / scalaVersion := "2.12.14" - -lazy val core = (project in file("core")) - .settings( - // other settings - ) - -lazy val util = (project in file("util")) - .settings( - // other settings - ) -``` -- 重载后,现在`versoin`和其他的设置在所有子项目中都会生效。 - -另一种定义公共设置的方式: -- 将默认设置放在`commonSettings`下,然后添加到所有子项目中`.setting()`调用中。 -```scala -lazy val commonSettings = Seq( - target := { baseDirectory.value / "target2" } -) - -lazy val core = (project in file("core")) - .settings( - commonSettings, - // other settings - ) - -lazy val util = (project in file("util")) - .settings( - commonSettings, - // other settings - ) -``` - -项目间依赖: -- 项目间可以完全独立,也通常可能会有某种方式的依赖。 -- 有两种方式的依赖:aggregate and classpath,用`.aggregate .dependsOn`定义。 -- 聚合用来广播命令,类路径依赖则是指项目之间存在依赖关系。 - - -聚合(Aggregation): -```scala -lazy val root = (project in file(".")) - .aggregate(util, core) - -lazy val util = (project in file("util")) - -lazy val core = (project in file("core")) -``` -- 这种方式在聚合项目上运行任务时会同样在它聚合的所有子项目上运行。 -- 比如上面的子项目定义,编译`root`是会同时编译`util core`。 -- 通过在做聚合的项目中定义设置可以修改这种默认行为: -```scala -lazy val root = (project in file(".")) - .aggregate(util, core) - .settings( - update / aggregate := false - ) - -[...] -``` -- 上面设置表示在root项目上执行`update`是就不会在被聚合的子项目上执行。`update / aggregate`是update作用域下的key。 - -类路径依赖: -- 一个项目可能依赖另一个项目的代码,通过`.dependsOn(proj1, proj2, ...)`方法调用来定义。依赖之后就会被添加到classpth,从而能够导入。 -```scala -lazy val core = project.dependsOn(util) -``` -- 现在在`core`中就可以调用`util`中的代码了。这同样也确定了代码编译顺序必然是先`util`后`core`。 -- `core dependsOn(util)`意味着`core`的编译配置依赖`util`,也可以显式通过`dependsOn(util % "compile->compile")`这种方式来指定,`compile->compile`中的`->`意味着依赖。因此如果是`"test->compile"`就以为着`core`的`test`配置依赖`util`的`compile`配置。可以忽略后面的`->config`部分意味着就是`->comile`。 -- 一个比较有用的定义是`test->test`意味着测试`core`是先测试`util`。 -- 可以用分好分隔:`dependsOn(util % "test->test;compile->compile")`。 - -项目间依赖: -- 在大项目中,会有许多文件,持续监视文件修改并重新编译将会消耗大量磁盘和IO资源。 -- sbt使用`trackInternalDependencies` 和 `exportToInternal`设置用来控制执行`compile`任务时是否触发独立子项目的编译。两个key都接受三个输入: - - `TrackLevel.NoTracking` - - `TrackLevel.TrackIfMissing` - - `TrackLevel.TrackAlways` 默认是这一项。 - - 含义显而易见。 -- `trackInternalDependencies`设置如果是`TrackLevel.TrackIfMissing`,那么sbt将不会尝试自动编译项目间依赖,除非输出目录中的`*.class`文件缺失了(或者`exportJars`时jar文件缺失了)。 -- 当被设置为`TrackLevel.NoTracking`,项目将依赖将被跳过。但是classpth仍然会被添加,依赖图也会显示他们还是依赖的。这么做的目的是为了减小检查文件修改的IO负担。 -- 设置方法: -```scala -ThisBuild / trackInternalDependencies := TrackLevel.TrackIfMissing -ThisBuild / exportJars := true - -lazy val root = (project in file(".")) - .aggregate(....) -``` -- `exportToInternal`设置允许依赖于当前项目的子项目跳过内部追踪。用在一个子项目上,用在当前不关心的依赖于其他项目的子项目,当其他项目发生修改重新编译,它也不会重新编译。 -```scala -lazy val dontTrackMe = (project in file("dontTrackMe")) - .settings( - exportToInternal := TrackLevel.NoTracking - ) -``` -- `trackInternalDependencies`和`exportToInternal`,比如当前修改项目时A,A依赖B,C依赖A,那么前者是针对B也就是当前项目依赖的那些子项目,后者针对C也就是依赖当前项目的子项目。这是我的理解,应该是这个样子! -- 如果没理解错的话,项目的依赖关系(指classpth,会调用的那种依赖)应该是一棵树,而不会有环图。 - - -默认的根项目: -- 如果没有为最顶层目录`.`定义项目,那么sbt会创建一个默认的然后在构建时聚合所有子项目。 - -与项目的交互: -- `projects`列出所有子项目。 -- `proejct`列出当前项目,`project `切换项目。运行一个任务比如`compile`时是针对当前项目。 -- 也可以通过指定项目名称来在某个项目上运行任务:`subProjectID / `。 - -公共代码: -- `.sbt`文件之间的定义是不共享的。 -- 为了能够在不同`.sbt`之间共享代码,需要在根目录的`project/`下面(子目录中的是没有用的,会被忽略,只有根目录中的才会有效)定义一个或多个scala文件。后续会详述。 - -子项目中的`.sbt`文件: -- 所有的`.sbt`文件都会被合并到一个整体的构建(build)中来,但是只在他们自己的范围内起作用。定义不会被共享。 -- 比如顶层`hello`目录中初始化`sbt`,`.`定义根项目`hello`,`hello/foo/ hello/bar`分别定义项目foo和bar并且有自己的`build.sbt`并把项目定义在了其中,其中定义了自己的不同版本。 -- 那么执行`show version`的结果就是这样的: -```shell -> show version -[info] hello-foo/*:version -[info] 0.7 -[info] hello-bar/*:version -[info] 0.9 -[info] hello/*:version -[info] 0.5 -``` -- 所有的`build.sbt`都是整个构建的一部分,但都有自己的作用范围。 -- 可以分开定义,也可以合起来定义`build.sbt`,项目很多时都定义在根目录中可能就太复杂了,定义在子目录找起来好像又挺麻烦。 -- 风格选择: - - 子项目的设置在子项目的`.sbt`中定义,根`build.sbt`中只定义最小的项目声明,形如:`lazy val foo = (project in file("foo"))`不修改任何设置。 - - 推荐是将所有项目定义全都放在根目录中的`build.sbt`,保持项目定义在一个文件中。 - - 都可以,这完全取决于你。 - - -### 任务图 - -任务图: -- 除了将设置视作一个个键值对,更好的比喻其实是以有向无环图(DAG)。边的方向表示**在之前发生**,称之为任务图(Task Graph) 。 -- Setting/Task expression就是前面的在`.setting(...)`中定义了设置或者任务的表达式。 - -任务间依赖: -- 使用一个特殊的`.value`方法调用来解释任务之间的依赖。 -- 直到非常熟悉`hello.sbt`之前,都推荐将`.value`调用放在task定义块中最上方。 -- 除了使用一个不变量赋值的方式,也可以使用内联的`.value`调用,更加间接,也不用去想变量名。 -- `.value`调用会在进入task body之前被求值,这是需要非常注意的。 -- 测试: -```scala -lazy val hi = taskKey[Unit]("An example task for dependency") - -lazy val hello = (project in file(".")) - .aggregate(helloCore) - .dependsOn(helloCore) - // .enablePlugins(JavaAppPackaging) - .settings( - name := "Hello", - libraryDependencies += scalaTest % Test, - hi := { - val ur = update.value // streams task happens-before hi - if (false) { - val x = clean.value // clean task happens-before hi - } - } - ) -``` -- 此时任务`hi`就会依赖于任务`update`和`clean`,并且这两个任务是在进入`hi`任务体前执行的,且不确定两者先后顺序,可先可后可并行。 -- 在任务体中调用`.value`仅用来表明任务之间的依赖关系。 -- 先编译项目,执行`hi`后会发现`target/scala-2.13/clsses/`被清除,就是因为执行了`clean`任务。 -- 查看任务间依赖:在`Dependencies:`后可以看到。 -```shell -inspect hi -``` -- 执行`inspect tree compile`会看到`compile`命令的依赖树,什么含义暂时不知。 -- sbt中的构建任务依赖是自动的而不是显式定义的,如果通过`.value`定义了,那么会造成任务间依赖。 - -任务依赖设置: -- 在任务体定义中调用设置的`.value`就行。 -- 但设置是不能依赖任务的,因为设置只在重载时执行一次,而任务一直都可以执行。 - -设置依赖设置: -- 可以将设置看做仅在记加载是执行一次的任务,所以设置也可以依赖设置。 -- 同样其中调用`.value`就可以依赖,并且执行时求值: -- 一个实际的例子:当Scala版本是2.11时讲将`Compile / scalaSource`定义到一个不同的目录。 -```scala -Compile / scalaSource := { - val old = (Compile / scalaSource).value - scalaBinaryVersion.value match { - case "2.11" => baseDirectory.value / "src-2.11" / "main" / "scala" - case _ => old - } -} -``` - -再看`build.sbt`DSL: -- 构建了一个设置和任务的有向无环图。 -- 设置表达式编码了设置、任务以及他们之间的依赖。 -- 这种结构在Make/Ant/Rake等构建工具中很常见。 -- 基于流的编程,减少了重复过程,好处: -- 一个任务仅仅只会执行一次,即使它被多个任务所依赖比如`Compile / compile`。 -- 基于任务图,任务引擎会安排不相关的任务并行执行。 -- 关注点分离和灵活性,任务图让用户可以将任务以不同的方式连接到一起,sbt和各种插件可以提供各种各样的特性比如库依赖管理等。 - -总结: -- 核心就是任务图,任务之间的关系是一个有向无环图。 -- `hello.sbt`是一个设计来面向依赖编程(dependency-oriented programming)的DSL,或者叫基于流(flow)的编程。就像Makefile。语法就像Scala,但并不等同于Scala,语义上是有区别的。 -- 基于流的编程的核心是:减少重复过程、并行处理和定制化。 - -### 更多内容 - -更多内容就不翻译了,官网上第一章也有中文翻译。需要时再去阅读,现阶段感觉都用不到,真写项目了再看不迟。 - -## 并发编程 - -TODO - -## 总结 - -总结: -- 看起来是一门静态类型语言,提供了很其强大的类型推导,可以一定程度上实现隐式静态类型,但写起来如果高度依赖类型推导的话会和动态类型一样简洁,仅需提供少量必须的类型,只是有点牺牲可读性。 -- 函数式编程很有趣。 -- 语法糖太太太多了,虽然看起来更简洁了,但是读起来不一定更简单,学起来心智负担也更大。 -- 运算符非常灵活,目前遇到过的运算符最灵活的语言。 -- 并发编程还没有学,TODO。 -- Scala语法确实有点太强大了,当然软件工程的东西都是tradeoff,写起来爽用起来复杂学起来难。 - -Scala是我目前学过的最舒服的语言,很多特点简直太棒了,如果此生只能选一门语言的话,那我可能真会选这门刚学了几天的语言。吸引我的点: -- 函数式编程,和集合的映射推导结合起来很有用。 -- 类型推导,像动态语言用起来的感觉,但也有编译期类型检查,再加上隐式类型转换,真我全都要。 -- 各种能简则简的语法糖,初看可能很诧异,习惯之后只能说去**的java,简洁而不简单。 -- 运算符重载,容易被滥用,但用得好会使代码进一步简化,当然各式各样的运算符会进一步增加读代码的难度。 -- 更加纯粹的面向对象,万物皆是对象。 \ No newline at end of file diff --git a/TCC.md b/TCC.md deleted file mode 100644 index b63f56d..0000000 --- a/TCC.md +++ /dev/null @@ -1,1477 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [TCC学习](#tcc%E5%AD%A6%E4%B9%A0) - - [0. 特点](#0-%E7%89%B9%E7%82%B9) - - [1. 安装与使用](#1-%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8) - - [2. C语言特性支持](#2-c%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7%E6%94%AF%E6%8C%81) - - [2.1 ISO C99扩展](#21-iso-c99%E6%89%A9%E5%B1%95) - - [2.2 GNU C扩展](#22-gnu-c%E6%89%A9%E5%B1%95) - - [2.3 TinyCC 自己的扩展](#23-tinycc-%E8%87%AA%E5%B7%B1%E7%9A%84%E6%89%A9%E5%B1%95) - - [3. TinyCC的汇编器](#3-tinycc%E7%9A%84%E6%B1%87%E7%BC%96%E5%99%A8) - - [3.1 语法](#31-%E8%AF%AD%E6%B3%95) - - [3.2 表达式](#32-%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [3.3 标号](#33-%E6%A0%87%E5%8F%B7) - - [3.4 指令](#34-%E6%8C%87%E4%BB%A4) - - [3.5 x86汇编器](#35-x86%E6%B1%87%E7%BC%96%E5%99%A8) - - [4. 链接器](#4-%E9%93%BE%E6%8E%A5%E5%99%A8) - - [4.1 ELF文件生成](#41-elf%E6%96%87%E4%BB%B6%E7%94%9F%E6%88%90) - - [4.2 加载ELF文件](#42-%E5%8A%A0%E8%BD%BDelf%E6%96%87%E4%BB%B6) - - [4.3 PE-i386文件生成](#43-pe-i386%E6%96%87%E4%BB%B6%E7%94%9F%E6%88%90) - - [4.4 GNU 链接脚本文件](#44-gnu-%E9%93%BE%E6%8E%A5%E8%84%9A%E6%9C%AC%E6%96%87%E4%BB%B6) - - [4.5 TCC内存和边界检查](#45-tcc%E5%86%85%E5%AD%98%E5%92%8C%E8%BE%B9%E7%95%8C%E6%A3%80%E6%9F%A5) - - [5. libtcc库](#5-libtcc%E5%BA%93) - - [6. 开发者指南](#6-%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97) - - [6.1 文件读取](#61-%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96) - - [6.2 词法分析](#62-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90) - - [6.3 语法分析](#63-%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90) - - [6.4 类型系统](#64-%E7%B1%BB%E5%9E%8B%E7%B3%BB%E7%BB%9F) - - [6.5 符号表](#65-%E7%AC%A6%E5%8F%B7%E8%A1%A8) - - [6.6 Sections](#66-sections) - - [6.7 代码生成](#67-%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90) - - [6.7.1 The Vlaue Stack](#671-the-vlaue-stack) - - [6.7.2 操作表达式求值栈](#672-%E6%93%8D%E4%BD%9C%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC%E6%A0%88) - - [6.7.3 依赖CPU的代码生成](#673-%E4%BE%9D%E8%B5%96cpu%E7%9A%84%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90) - - [6.8 编译优化](#68-%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96) - - [7. 编译源码](#7-%E7%BC%96%E8%AF%91%E6%BA%90%E7%A0%81) - - [8. 使用libtcc](#8-%E4%BD%BF%E7%94%A8libtcc) - - [8.1 使用libtcc来编译一个源码](#81-%E4%BD%BF%E7%94%A8libtcc%E6%9D%A5%E7%BC%96%E8%AF%91%E4%B8%80%E4%B8%AA%E6%BA%90%E7%A0%81) - - [8.2 libtcc使用示例](#82-libtcc%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B) - - [8.3 调试测试程序](#83-%E8%B0%83%E8%AF%95%E6%B5%8B%E8%AF%95%E7%A8%8B%E5%BA%8F) - - [8.4 编译过程](#84-%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B) - - - -# TCC学习 - -Tiny CC,简称TCC,是Fabrice Bellard大神和伙伴们写的小型C编译器。是一个小型、快速的编译器。 - -- 首页:https://bellard.org/tcc/ -- 文档:https://bellard.org/tcc/tcc-doc.html -- Git仓库:https://repo.or.cz/w/tinycc.git -- 最新发布版本:2017年12月17日发布的0.9.27版本,https://repo.or.cz/tinycc.git/shortlog/refs/tags/release_0_9_27 -- Fabrice Bellard已经不再维护TCC了,但是依然有不少爱好者还在维护,尽管后续没有发布正式版本。 -- 下载地址:http://download.savannah.gnu.org/releases/tinycc/ - -这里来了解一下TCC,学习一下源码,主要基于0.9.27版本。 - -为什么要学习了解一下TCC,是因为最近了解了c4,重构了一下并加了一点功能之后对编译器有了一点了解。但 -c4就算扩展了一点语法,因为运行在虚拟机上、不支持预处理、不支持完整的标准C语法、不支持多文件编译、不能编译库,注定了就只能是一个玩具。有点意犹未尽,相比而言TCC支持标准C、预处理编译链接过程完善,基本可以用来编译小型的C项目了,完善了很多,项目大小也正合适。 - -下面在编译源码前的部分几乎都是翻译自官方文档,作为学习源码前的了解。 - -## 0. 特点 - -- 小而快,生成代码质量一般。 -- 包含完整的汇编器和链接器,可以直接生成可执行文件。 -- 支持ANSI C标准,并且几乎支持所有ISO C99标准,支持许多GNUC扩展包括内联汇编。 -- 可以使用TCC来制作C脚本,在C源文件第一行添加`#!/usr/local/bin/tcc -run`,像使用脚本语言一样使用C。 -- 安全,TCC会进行内存边界检查,边界检查的代码可以和生成代码自由地混合在一起。 -- 使用libtcc,你可以将TCC作为编译器后端来使用,为你生成代码。 -- TCC支持生成Linux和Windows上的i386目标文件。也有arm和其他架构的版本,当然这里只关注x86版本。 - -## 1. 安装与使用 - -Linux上可以通过包管理器直接安装,最新版本还是0.9.27: -```shell -sudo apt install tcc -``` - -帮助: -``` -Tiny C Compiler 0.9.27 - Copyright (C) 2001-2006 Fabrice Bellard -Usage: tcc [options...] [-o outfile] [-c] infile(s)... - tcc [options...] -run infile [arguments...] -General options: - -c compile only - generate an object file - -o outfile set output filename - -run run compiled source - -fflag set or reset (with 'no-' prefix) 'flag' (see tcc -hh) - -Wwarning set or reset (with 'no-' prefix) 'warning' (see tcc -hh) - -w disable all warnings - -v -vv show version, show search paths or loaded files - -h -hh show this, show more help - -bench show compilation statistics - - use stdin pipe as infile - @listfile read arguments from listfile -Preprocessor options: - -Idir add include path 'dir' - -Dsym[=val] define 'sym' with value 'val' - -Usym undefine 'sym' - -E preprocess only -Linker options: - -Ldir add library path 'dir' - -llib link with dynamic or static library 'lib' - -r generate (relocatable) object file - -shared generate a shared library/dll - -rdynamic export all global symbols to dynamic linker - -soname set name for shared library to be used at runtime - -Wl,-opt[=val] set linker option (see tcc -hh) -Debugger options: - -g generate runtime debug info - -b compile with built-in memory and bounds checker (implies -g) - -bt N show N callers in stack traces -Misc. options: - -x[c|a|n] specify type of the next infile - -nostdinc do not use standard system include paths - -nostdlib do not link with standard crt and libraries - -Bdir set tcc's private include/library dir - -MD generate dependency file for make - -MF file specify dependency file name - -m32/64 defer to i386/x86_64 cross compiler -Tools: - create library : tcc -ar [rcsv] lib.a files -``` - - -TCC的选项和GCC很像,主要的区别就是TCC可以直接加`-run`选项给参数直接执行C程序。 - -不生成目标文件直接执行: -```shell -tcc -run a.c -tcc -run a.c arg1 -tcc a.c -run b.c arg1 # 编译a.c和b.c,链接,并传递arg1给main执行 -tcc -o myprog a.c b.c # 编译链接a.c和b.c为可执行文件myprog -tcc -o myprog a.o b.o # 链接目标文件a.o和b.o为可执行文件myprog -tcc -c a.c # 编译生成目标文件a.o -tcc -c asmfile.S # 预处理并汇编得到目标文件asmfile.o -tcc -c asmfile.s # 汇编得到目标文件asmfile.o -tcc -r -o ab.o a.c b.c # 编译并链接a.c和b.c到目标文件ab.o -``` - -从脚本调用TCC:加上执行权限之后就可以从命令行直接执行了。 -```C -#!/usr/bin/tcc -run -#include -int main() -{ - printf("Hello World\n"); - return 0; -} -``` - -从标准输入调用TCC:一般应该不会这么干。 -```shell -echo 'main(){puts("hello");}' | tcc -run - -``` - -就选项来说,相比GCC,少了`-S`编译为汇编源文件这个选项,因为是直接生成机器码。预处理`-E`,编译到目标文件`-c`,不加选项直接编译为可执行文件都是一样的。 - -系统变量: -``` -CPATH -C_INCLUDE_PATH -``` -冒号分隔的头文件目录,`-I`指定的目录会先搜索。 - -``` -LIBRARY_PATH -``` -冒号分隔的库文件目录,`-l`选项指定的库的搜索目录,`-L`指定的目录会先搜索。 - - -## 2. C语言特性支持 - -- 实现了所有ANSI C标准,包括结构体位于声明和浮点数,完全支持`long double double float `。 - -### 2.1 ISO C99扩展 - -实现的特性有: -- VLA。 -- 64位`long long`类型完全支持。 -- `_Bool`类型支持。 -- `__func__`宏名支持,当前函数名称。 -- 可变参数宏`__VA_ARGS__`可以用在带参宏里面: - ```C - #define dprintf(level, __VA_ARGS__) printf(__VA_ARGS__) - ``` -- 声明可以出现在块的任何位置。 -- 可以使用指派符初始化结构、联合、数组中的元素: - ```C - struct { int x, y; } st[10] = { [0].x = 1, [0].y = 2 }; - int tab[10] = { 1, 2, [5] = 5, [9] = 9}; - ``` -- 支持复合初始化:结构或字符串也支持。 - ```C - int *p = (int []){ 1, 2, 3 }; - ``` -- 十六进制浮点数常量。 - ```C - double d = 0x1234p10; - ``` -- `inline` 关键字被忽略。 -- `restrict` 关键字被忽略。 - - -### 2.2 GNU C扩展 - -实现的GNU C特性有: -- 数组指派符初始化可以没有赋值号。 - ```C - int a[10] = { [0] 1, [5] 2, 3, 4 }; - ``` -- 结构指派符初始化可以用像标号一样的语法: - ```C - struct { int x, y; } st = { x: 1, y: 1} - ``` -- `\e` 被视作ASCII字符27。 -- `case`标签支持范围: - ```C - switch(a) { - case 1 … 9: - printf("range 1 to 9\n"); - break; - default: - printf("unexpected\n"); - break; - } - ``` -- `__attribute__` 关键字指定变量或者函数属性,支持下列属性: - - `aligned(n)` 结构体域对齐的字节大小。 - - `packed` 强制结构体域的对齐大小为1。 - - `section(name)` 将函数代码生成在名称为name的节,而不是默认。 - - `unused` 指明变量或函数未使用。 - - `cdecl` 使用标准C的调用约定。 - - `stdcall` 类Pascal调用约定。 - - `regparm(n)` 使用x86的快速调用约定,n在1到3,函数的前3个参数分别放入`%eax` `%edx` `%ecx`。 - - `dllexport` 在dll或者可执行文件中导出一个函数,仅在Win32中使用。 - - 例: - ```C - int a __attribute__ ((aligned(8), section(".mysection"))); - int my_add(int a, int b) __attribute__ ((section(".mycodesection"))) - { - return a + b; - } - ``` -- GNU 风格的可变参数。 - ```C - #define dprintf(fmt, args...) printf(fmt, ## args) - - dprintf("no arg\n"); - dprintf("one arg %d\n", 1); - ``` -- `__FUNCTION__` 宏被解释为`__func__`。和GNUC不完全是同一个语义。 -- `__alignof__` 可以像`sizeof`一样使用,用来获取一个类型或者表达式的对齐字节大小。 -- `typeof(x)` 得到x的类型,x是表达式或类型。 -- 可计算的goto:`&&label` 返回一个void*类型指向标号`label`的指针,`goto *expr`跳转到`expr`的结果表示的指针保存的地址。 -- 内联汇编: - ```C - static inline void * my_memcpy(void * to, const void * from, size_t n) - { - int d0, d1, d2; - __asm__ __volatile__( - "rep ; movsl\n\t" - "testb $2,%b4\n\t" - "je 1f\n\t" - "movsw\n" - "1:\ttestb $1,%b4\n\t" - "je 2f\n\t" - "movsb\n" - "2:" - : "=&c" (d0), "=&D" (d1), "=&S" (d2) - :"0" (n/4), "q" (n),"1" ((long) to),"2" ((long) from) - : "memory"); - return (to); - } - ``` -- 支持`__builtin_types_compatible_p()`和`__builtin_constant_p()`。 -- 为了Win32的兼容性支持`#pragma pack`。 - -### 2.3 TinyCC 自己的扩展 - -- `__TINYC__` 预定义宏表明在使用TCC。 -- 行首的`#!`被忽略以支持脚本。 -- 可以使用二进制整数字面值,`0b101`表示5。 -- `__BOUNDS_CHECKING_ON` 宏被定义,如果开了边界检查的话。 - - -## 3. TinyCC的汇编器 - -0.9.16版本开始TCC实现了自己的汇编器,汇编器用来处理GNU汇编器风格的`.s` `.S`源文件,汇编器也被用来处理`asm`关键字指定的GNU内联汇编。 - -- TCC的C编译器不依赖汇编器,可以停用以减小TCC的体积。 -- TCC的编译过程中没有生成汇编这个步骤,而是直接生成二进制机器码。 - -### 3.1 语法 - -TCC的汇编器支持大部分的GAS([GNU Assembler](https://en.wikipedia.org/wiki/GNU_Assembler))语法(也就是被熟知的AT&T风格的汇编)。 -- 支持C/C++注释。 -- 标识符同C,不能使用`. $`。 -- 仅支持32位整数。 - -### 3.2 表达式 - -- 支持十进制、八进制、十六进制整数。 -- 一元运算符:`+ - ~`。 -- 二元运算符优先级降序排列:同C有区别。 - - `* / %` - - `& | ^` - - `+ -` -- 一个值要么是立即数,要么是一个标号加上一个偏移。所有的操作符都支持立即数除了`+`和`-`,`+`和`-`可以用来将一个偏移加到标号上。`-`支持标号相减,仅当他们在同一个section。 - -### 3.3 标号 - -- 标号都被视为局部的,除非是未定义的。 -- 数值标号可以用作GAS风格的标号,可以在同一个源中定义多次。使用后缀b(backward)或者f(forward)作为前缀来引用。 - ```x86asm - 1: - jmp 1b /* jump to '1' label before */ - jmp 1f /* jump to '1' label after */ - 1: - ``` - -### 3.4 指令 - -所有指令均已`.`开头,支持下列指令: - -```x86asm -.align n[,value] -.skip n[,value] -.space n[,value] -.byte value1[,...] -.word value1[,...] -.short value1[,...] -.int value1[,...] -.long value1[,...] -.quad immediate_value1[,...] -.globl symbol -.global symbol -.section section -.text -.data -.bss -.fill repeat[,size[,value]] -.org n -.previous -.string string[,...] -.asciz string[,...] -.ascii string[,...] -``` - -### 3.5 x86汇编器 - -支持所有x86操作码,仅支持AT&T风格汇编(源操作数在前,目标操作数在后)。如果前缀没有给定大小,TCC会尝试从操作数的大小猜测。 - -当前支持MMX操作码,但是不支持SSE。 - -## 4. 链接器 - -### 4.1 ELF文件生成 - -TCC可以在不依赖外部链接器的情况下生成可重定位的ELF文件(目标文件),可执行的ELF文件和动态ELF库。 - -可以输出动态ELF库,但是C编译器不生成位置无关代码(PIC, position independent code),这意味着TCC生成的动态库不能在进程间共用。 - -链接器会删掉库中未被引用的目标代码,对目标文件和库的单趟操作做这个事情。所以目标文件和库的顺序很重要(和GNU ld的约束一样)。不支持组选项`--start-group` `--end-group`。 - -### 4.2 加载ELF文件 - -TCC可以加载ELF对象文件,包括静态库`.a`和动态库`.so`。 - -### 4.3 PE-i386文件生成 - -TCC支持生成Win32的可执行文件格式,可以生成exe(控制台和GUI)和DLL文件。 - -Windows详细使用说明可以参见tcc-win32.txt。 - -### 4.4 GNU 链接脚本文件 - -因为在许多Linux系统上,一些动态链接库(比如`/usr/lib/libc.so`)世界上是GNN链接脚本文件(这很糟糕!),所以TCC同样支持一个GNU ld链接脚本的子集。 - -- 支持`GROUP`和`FILE`命令,忽略`OUTPUT_FORMAT`和`TARGET`命令。 - -`/usr/lib/libc.so`的例子: -``` -/* GNU ld script - Use the shared library, but some functions are only in - the static library, so try that secondarily. */ -GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a ) -``` - -### 4.5 TCC内存和边界检查 - -使用`-b`选项开启。 - -开启与未开启,指针长度不会发生变化,未进行边界检查和进行了的完全二进制兼容。未检查的代码中的指针会被假定为有效的。即是非常模糊的C代码的指针转换都会正确工作。更多内容看这里:http://www.doc.ic.ac.uk/~phjk/BoundsChecking.html - -内存边界检查会检查出来错误举例: - -标准C字符串函数的非法范围: -```C -{ - char tab[10]; - memset(tab, 0, 11); -} -``` - -全局或者局部数组中的下标越界: -```C -{ - int tab[10]; - for(i=0;i<11;i++) { - sum += tab[i]; - } -} -``` -malloc分配的内存越界访问: -```C -{ - int *tab; - tab = malloc(20 * sizeof(int)); - for(i=0;i<21;i++) { - sum += tab4[i]; - } - free(tab); -} -``` -访问已释放内存: -```C -{ - int *tab; - tab = malloc(20 * sizeof(int)); - free(tab); - for(i=0;i<20;i++) { - sum += tab4[i]; - } -} -``` -多次释放内存: -```C -{ - int *tab; - tab = malloc(20 * sizeof(int)); - free(tab); - free(tab); -} -``` - -## 5. libtcc库 - -你可以使用libtcc作为动态代码生成的后端来使用。 - -`libtcc.h`中的声明了API,`libtcc_test.c`有基本的使用示例。 - -比如传递一个想编译的C源码的字符串到libtcc,然后可以获得文件中的全局符号和函数定义。 - - -## 6. 开发者指南 - -TCC源码指南,如果想要读懂甚至修改TCC可以看一看。 - -### 6.1 文件读取 - -`BufferedFile` 结构包含了读取一个文件所需要的上下文,包括当前行号。 -- `tcc_open()`打开文件。 -- `tcc_close()`关闭文件。 -- `inp()`返回下一个字符。 - -### 6.2 词法分析 - -- `next()`读取当前文件的下一个token。 -- `next_nomacro()`读取未进行宏展开的下一个token。 -- `tok`是当前的token,`TOK_xxx`定义了token。 -- 标识符和关键字都是关键字。 -- `tokc`包含了当前token的额外信息,比如常数或者字符串token的值。 - -### 6.3 语法分析 - -语法分析是硬编码的(没有必要使用yacc),仅执行单趟。除非对以下情况: -- 为给定大小的初始化了的数组,第一趟会计算数组元素个数。 -- 对于实参按照逆序求值的目标架构中,第一趟会将参数顺序翻转过来。 - -### 6.4 类型系统 - -类型存储在一个`int`变量中,在tcc开发早期就这样做了选择,现在来看,可能并不是最好的选择。 - -相关常量: -```C -#define VT_INT 0 /* integer type */ -#define VT_BYTE 1 /* signed byte type */ -#define VT_SHORT 2 /* short type */ -#define VT_VOID 3 /* void type */ -#define VT_PTR 4 /* pointer */ -#define VT_ENUM 5 /* enum definition */ -#define VT_FUNC 6 /* function type */ -#define VT_STRUCT 7 /* struct/union definition */ -#define VT_FLOAT 8 /* IEEE float */ -#define VT_DOUBLE 9 /* IEEE double */ -#define VT_LDOUBLE 10 /* IEEE long double */ -#define VT_BOOL 11 /* ISOC99 boolean type */ -#define VT_LLONG 12 /* 64 bit integer */ -#define VT_LONG 13 /* long integer (NEVER USED as type, only - during parsing) */ -#define VT_BTYPE 0x000f /* mask for basic type */ -#define VT_UNSIGNED 0x0010 /* unsigned type */ -#define VT_ARRAY 0x0020 /* array type (also has VT_PTR) */ -#define VT_VLA 0x20000 /* VLA type (also has VT_PTR and VT_ARRAY) */ -#define VT_BITFIELD 0x0040 /* bitfield modifier */ -#define VT_CONSTANT 0x0800 /* const modifier */ -#define VT_VOLATILE 0x1000 /* volatile modifier */ -#define VT_DEFSIGN 0x2000 /* signed type */ - -#define VT_STRUCT_SHIFT 18 /* structure/enum name shift (14 bits left) */ -``` - -- 当需要引用其他类型时(指针、函数、结构),`32-VT_STRUCT_SHIFT`位(14位)的高位被用来存储引用的类型。 -- `VT_UNSIGNED`可以设置给`char` `short` `int` `long long` -- 数组被视为指针`VT_PTR`并且同时设置了`VT_ARRAY`标记。变长数组VLA被视为特殊数组,设置`VT_VLA`标记。 -- `VT_BITFIELD`可以被设置给`char` `short` `int` `long long`。如果被设置了,那么位于的位置存储在`VT_STRUCT_SHIFT`到`VT_STRUCT_SHIFT + 5`的位中,位域大小被存储在`VT_STRUCT_SHIFT + 6`到`VT_STRUCT_SHIFT + 11`位。 -- `VT_LONG`除了在解析时,其他时候都不用。 -- 解析时,一个对象的Storage(这怎么翻译啊?)同样被存储在这个表示类型的整数中。 -```C -#define VT_EXTERN 0x00000080 /* extern definition */ -#define VT_STATIC 0x00000100 /* static variable */ -#define VT_TYPEDEF 0x00000200 /* typedef definition */ -#define VT_INLINE 0x00000400 /* inline definition */ -#define VT_IMPORT 0x00004000 /* win32: extern data imported from dll */ -#define VT_EXPORT 0x00008000 /* win32: data exported from dll */ -#define VT_WEAK 0x00010000 /* win32: data exported from dll */ -``` - -### 6.5 符号表 - -所有的符号都被存储在哈希符号栈中,符号栈中的元素是`Sym`结构。 - -- Sym.v保存符号名称。 -- Sym.t表示符号的类型。 -- Sym.r是这个变量存储的寄存器。 -- Sym.c通常是符号对相应的常量,比如一个常规符号的地址,数组长度。VLA用使用这个域存储栈上的一个保存了VLA大小的位置。 - -四个主要的符号栈: -- `define_stack` 保存宏。 -- `global_stack` 全局变量函数和类型。 -- `local_stack` 局部变量函数和类型。 -- `global_label_stack` 局部的goto的标号。 -- `label_stack` GCC块中的局部标号,见`__label__`关键字。 - -函数: -- `sym_push()` 添加一个新的符号到当前局部符号栈,如果没有局部的符号栈活跃,那么添加到全局。 -- `sym_pop(st, b)` 从符号栈`st`中依次出栈,知道`b`位于栈顶。如果`b`为空,那么`st`直接置空。 -- `sym_find(v)` 得到和标识符`v`关联的符号。先自栈顶向下搜索局部符号栈,然后是全局符号栈。 - -### 6.6 Sections - -这个应该怎么翻译?节?部分?段?就叫section吧。段应该是比较符合含义的,但段其实一般说的segment。 - -生成的代码和数据被写到一个个的section中,`Section`结构包含了一个section需要的所有信息。`new_section()`函数创建一个新的section。每个节都假定拥有ELF文件语义(什么意思?) - -预定义了下面的sections: - -- `text_section` 保存生成代码,`ind`保存代码段中当前位置。 -- `data_section` 保存初始化了的数据。 -- `bss_section` 保存未初始化的数据。 -- `bounds_section` `lbounds_section` 开启了内存边界检查才用。 -- `stab_section` `stabstr_section` 调试模式下用来存储调试信息。 -- `symtab_section` `strtab_section` 保存导出符号,当前只用于调试。 - -### 6.7 代码生成 - -TCC的代码生成在一趟过程中直接生成二进制代码,放到现在来说并不是通常的做法(比如GCC会生成汇编源文件,TCC没有这一步),这样实现执行速度很快,而且令人意外地会有一点复杂。 - -TCC的代码生成器是基于寄存器的,仅仅有一些表达式级别的优化,除了保存在一个栈(原文Value Stack)中的值之外没有中间表示。【这个栈应该是语法分析时用来辅助解析表达式的栈,而不是指运行时栈】 - -在x86的代码生成中,使用了3个临时寄存器,当需要更多寄存器时,其中一个寄存器上的值会被转移到栈上的临时变量中。 - -#### 6.7.1 The Vlaue Stack - -姑且将其称作**表达式求值栈**,解析完一个表达式后,它的值被压到栈(`vstack`)顶,栈顶用`vtop`表示。每一个条目用一个结构`SValue`表示。 - -- `SValue.t` 表示类型。 -- `SValue.r` 表示这个值在生成代码中怎样存储。通常使用一个CPU寄存器索引,和一些值和额外的标记组成。 - ```C - #define VT_CONST 0x00f0 - #define VT_LLOCAL 0x00f1 - #define VT_LOCAL 0x00f2 - #define VT_CMP 0x00f3 - #define VT_JMP 0x00f4 - #define VT_JMPI 0x00f5 - #define VT_LVAL 0x0100 - #define VT_SYM 0x0200 - #define VT_MUSTCAST 0x0400 - #define VT_MUSTBOUND 0x0800 - #define VT_BOUNDED 0x8000 - #define VT_LVAL_BYTE 0x1000 - #define VT_LVAL_SHORT 0x2000 - #define VT_LVAL_UNSIGNED 0x4000 - #define VT_LVAL_TYPE (VT_LVAL_BYTE | VT_LVAL_SHORT | VT_LVAL_UNSIGNED) - ``` -- `VT_CONST` 值是常量,存储在`SVlue.c`中,是一个联合体,具体哪个域看类型。 -- `VT_LOCAL` 局部变量,在运行时栈中的指针偏移存储在`SValue.c.i`。 -- `VT_CMP` 表示值存储在CPU的标志寄存器中(CPU flags),值为0或者1,使用了哪个CPU标志寄存器保存在`SValue.c.i`中。如果生成了会改变CPU标志的指令,那么这个值必须被移到一个通用寄存器中。 -- `VT_JMP` `VT_JMPI` 表示这个值是一个有条件跳转的结果。对于`VT_JMP`类型,如果执行了跳转,值为1,未执行为0,`VT_JMPI`则反过过来。 - - 这些值被用来编译`||`和`&&`运算符。 - - 如果中间生成了任何代码,那么这些值必须被放到一个通用寄存器。否则,如果执行了跳转生成的代码就不会被执行。意义不明! -- `VT_LVAL` 表明这个值是一个左值的标记,表明这个值存储的实际上是想要的值的指针。理解`VT_LVAL`的使用对理解TCC的工作方式非常重要。**重要!** -- `VT_LVAL_BYTE` `VT_LVAL_SHORT` `VT_LVAL_UNSIGNED` 如果一个左值是整型,这些标记表明了它的真实类型,仅有类型在类型转换的优化时是不够的。 -- `VT_LLOCAL` 栈上已保存的左值,`VT_LLOCAL`必须和`VT_LVAL`一同被设置。当一个寄存器中的`VT_LVAL`被存到栈里面时`VT_LLOCAL`就会被设置,或者某种特定于架构的调用约定中。 -- `VT_MUSTCAST` 表明如果这个值被使用的话应该进行类型转换(lazy casting)。 -- `VT_SYM` 表明符号`SValue.sym`需要被添加到常量中。 -- `VT_MUSTBOUND` `VT_BOUNDED`内存边界检查时使用。 - -#### 6.7.2 操作表达式求值栈 - -- `vsetc()` `vset()` 将一个新值压到表达式求值栈中,如果上一个栈顶`vtop`存储在一个非常不安全的位置,比如CPU标志寄存器,那么就会生成将上一个`vtop`存到安全位置的代码。 -- `vpop()` 将`vtop`出栈。某些情况也会生成清理代码,比如使用了x86上的浮点寄存器栈(stacked floating point registers)。 -- `gv(rc)` 生成对栈顶`vtop`求值到寄存器中的代码,`rc`是选择的寄存器,`gv()`是代码生成器中最重要的函数。 -- `gv2()` 功能同`gv()`,但是对栈顶的两个栈帧求值。 - - -#### 6.7.3 依赖CPU的代码生成 - -以x86的代码生成`i386-gen.c`文件为例: -- `load()` 生成需要的代码将一个栈中的值加载到寄存器。 -- `store()` 生成将寄存器中的值保存到一个栈中的左值的代码。 -- `gfunc_start() gfunc_param() gfunc_call()` 生成函数调用代码。 -- `gfunc_prolog() gfunc_epilog()` 生成函数的[prolog/epilog](https://docs.microsoft.com/zh-cn/cpp/build/prolog-and-epilog?view=msvc-160)。 -- `gen_opi(op)` 为表达式求值栈的保证是整数的栈顶两个栈帧,生成操作`op`对应运算的代码。结果应该存到表达式求值栈里面。 -- `gen_opf(op)` 和上一个类似,只不过为浮点数,保证栈顶两个栈帧是浮点数。 -- `gen_cvt_itof()` 整数到浮点数转换。 -- `gen_cvt_ftoi()` 浮点数到整数转换。 -- `gen_cvt_ftof()` 不同长度的浮点数到浮点数转换。 -- `gen_bounded_ptr_add() gen_bounded_ptr_deref()` 仅用在内存边界检查中。 - -### 6.8 编译优化 - -- [常量折叠/常量传播](https://en.wikipedia.org/wiki/Constant_folding)(Constant folding/constant propagation):所有的运算都会进行常量折叠,常量表达式编译期计算、未修改的变量会在编译期被替代为常数,以简化计算过程。 - - 常量折叠是指将一个常量表达式在编译期计算出来。 - - 常量传播是指将一个被初始化为常数的变量在它真正被修改之前使用的地方都替换为常量。 - - 交替进行这两个过程,直到没有必要再优化。 - - 例: - ```C - int a = 30; - int b = 9 - a / 5; - int c; - c = b * 4; - if (c > 10) { - c = c - 10; - } - return c * (60 / a); - ``` - - 例子中最终结果是确定的,借由不段地常量折叠和传播。最终可以优化为: - ```C - return 4; - ``` -- 除法和乘法在合适的时候会被优化为移位。 -- 比较操作的结果通过维护处理器标志寄存器的缓存来优化。 -- `&& || !`通过维护一个特殊的跳转目标来优化。 -- 目前没有其他的JUMP优化,因为那需要以更抽象的方式保存代码,比如中间代码、AST这种。 -- 找到一个简要描述编译优化的网站:https://compileroptimizations.com/index.html - - -至此TCC文档就结束了,TCC的使用,结构,一些具体的实现点都了解了,TCC有什么东西,没有什么基本了解了。下面会逐步编译、阅读、理解源码。 - -## 7. 编译源码 - -将仓库检出到本地: -```shell -git clone https://repo.or.cz/tinycc.git ./TCC -``` - -文件结构: -``` -. -├── COPYING -├── Changelog -├── CodingStyle -├── Makefile -├── README -├── RELICENSING -├── TODO -├── USES -├── VERSION -├── arm-asm.c -├── arm-gen.c -├── arm-link.c -├── arm-tok.h -├── arm64-asm.c -├── arm64-gen.c -├── arm64-link.c -├── c67-gen.c -├── c67-link.c -├── coff.h -├── configure* -├── conftest.c -├── elf.h -├── examples/ -├── i386-asm.c -├── i386-asm.h -├── i386-gen.c -├── i386-link.c -├── i386-tok.h -├── il-gen.c -├── il-opcodes.h -├── include/ -├── lib/ -├── libtcc.c -├── libtcc.h -├── riscv64-asm.c -├── riscv64-gen.c -├── riscv64-link.c -├── riscv64-tok.h -├── stab.def -├── stab.h -├── tcc-doc.texi -├── tcc.c -├── tcc.h -├── tccasm.c -├── tcccoff.c -├── tccelf.c -├── tccgen.c -├── tcclib.h -├── tccmacho.c -├── tccpe.c -├── tccpp.c -├── tccrun.c -├── tcctok.h -├── tcctools.c -├── tests/ -├── texi2pod.pl* -├── win32/ -├── x86_64-asm.h -├── x86_64-gen.c -└── x86_64-link.c -``` - -编译: -```shell -./configure -make [CONFIG_debug=yes] -``` - -运行`./configure`之后生成了`config.mak`文件,包含当前的配置信息,系统、架构、编译器、版本等信息,被`Makefile`包含: -```make -# Automatically generated by configure - do not modify -prefix=/usr/local -bindir=$(DESTDIR)/usr/local/bin -tccdir=$(DESTDIR)/usr/local/lib/tcc -libdir=$(DESTDIR)/usr/local/lib -includedir=$(DESTDIR)/usr/local/include -mandir=$(DESTDIR)/usr/local/share/man -infodir=$(DESTDIR)/usr/local/share/info -docdir=$(DESTDIR)/usr/local/share/doc -CC=gcc -CC_NAME=gcc -GCC_MAJOR=9 -GCC_MINOR=3 -AR=ar -CFLAGS=-Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -LDFLAGS= -LIBSUF=.a -EXESUF= -DLLSUF=.so -NATIVE_DEFINES+=-DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -ARCH=x86_64 -TARGETOS=Linux -VERSION = 0.9.27 -TOPSRC=$(TOP) -``` - -看一下`make`后执行的命令,如果需要调试就定义变量`CONFIG_debug=yes`,然后编译链接时会加上`-g`选项: -```shell -gcc -o tcc.o -c tcc.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -DTCC_GITHASH="\"mob:c7a57bf\"" -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o libtcc.o -c libtcc.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -DC2STR conftest.c -o c2str.exe && ./c2str.exe include/tccdefs.h tccdefs_.h -gcc -o tccpp.o -c tccpp.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o tccgen.o -c tccgen.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o tccelf.o -c tccelf.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o tccasm.o -c tccasm.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o tccrun.o -c tccrun.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o x86_64-gen.o -c x86_64-gen.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o x86_64-link.o -c x86_64-link.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -gcc -o i386-asm.o -c i386-asm.c -DCONFIG_TRIPLET="\"x86_64-linux-gnu\"" -DTCC_TARGET_X86_64 -DONE_SOURCE=0 -Wall -O2 -Wdeclaration-after-statement -fno-strict-aliasing -Wno-pointer-sign -Wno-sign-compare -Wno-unused-result -Wno-format-truncation -I. -ar rcs libtcc.a libtcc.o tccpp.o tccgen.o tccelf.o tccasm.o tccrun.o x86_64-gen.o x86_64-link.o i386-asm.o -gcc -o tcc tcc.o libtcc.a -lm -lpthread -ldl -s -make[1]: Entering directory '/home/tch/Compiler/tcc/lib' -../tcc -c libtcc1.c -o libtcc1.o -B.. -I.. -../tcc -c alloca.S -o alloca.o -B.. -I.. -../tcc -c alloca-bt.S -o alloca-bt.o -B.. -I.. -../tcc -c tcov.c -o tcov.o -B.. -I.. -../tcc -c stdatomic.c -o stdatomic.o -B.. -I.. -../tcc -c va_list.c -o va_list.o -B.. -I.. -../tcc -c dsohandle.c -o dsohandle.o -B.. -I.. -../tcc -ar rcs ../libtcc1.a libtcc1.o alloca.o alloca-bt.o tcov.o stdatomic.o va_list.o dsohandle.o -../tcc -c bt-exe.c -o ../bt-exe.o -B.. -I.. -../tcc -c bt-log.c -o ../bt-log.o -B.. -I.. -../tcc -c bcheck.c -o ../bcheck.o -B.. -I.. -g -make[1]: Leaving directory '/home/tch/Compiler/tcc/lib' -perl ./texi2pod.pl tcc-doc.texi tcc-doc.pod -pod2man --section=1 --center="Tiny C Compiler" --release="0.9.27" tcc-doc.pod >tcc.1 && rm -f tcc-doc.pod -``` - -编译了哪些文件: -``` -tcc.c -libtcc.c -conftest.c -tccpp.c -tccgen.c -tccelf.c -tccasm.c -tccrun.c -x86_64-gen.c -x86_64-link.c -i386-asm.c -``` -链接: -```shell -ar rcs libtcc.a libtcc.o tccpp.o tccgen.o tccelf.o tccasm.o tccrun.o x86_64-gen.o x86_64-link.o i386-asm.o -gcc -o tcc tcc.o libtcc.a -lm -lpthread -ldl -s -``` -后面的所有C文件打包为静态库`libtcc.a`,链接`tcc.o`后生成`tcc`。 - -然后嵌套执行了`./lib/`目录中的Makefile。 - -仅看编译了的这部分代码的话,代码量在2W行左右,加上其他架构ARM、RISCV64等全部接近4W行。 - -## 8. 使用libtcc - -先不关注`tcc`的对与命令行选项的处理。从编译相关的接口入手,或者说先写一个程序调用libtcc中的接口进行编译,来搞清编译主要流程之后再看其他的处理。 - -### 8.1 使用libtcc来编译一个源码 - -先看一下`libtcc.h`中定义的接口: -```c++ -#ifndef LIBTCC_H -#define LIBTCC_H - -#ifndef LIBTCCAPI -# define LIBTCCAPI -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/*compilation context*/ -struct TCCState; - -typedef struct TCCState TCCState; - -typedef void (*TCCErrorFunc)(void *opaque, const char *msg); - -/* create a new TCC compilation context */ -LIBTCCAPI TCCState *tcc_new(void); - -/* free a TCC compilation context */ -LIBTCCAPI void tcc_delete(TCCState *s); - -/* set CONFIG_TCCDIR at runtime */ -LIBTCCAPI void tcc_set_lib_path(TCCState *s, const char *path); - -/* set error/warning display callback */ -LIBTCCAPI void tcc_set_error_func(TCCState *s, void *error_opaque, TCCErrorFunc error_func); - -/* return error/warning callback */ -LIBTCCAPI TCCErrorFunc tcc_get_error_func(TCCState *s); - -/* return error/warning callback opaque pointer */ -LIBTCCAPI void *tcc_get_error_opaque(TCCState *s); - -/* set options as from command line (multiple supported) */ -LIBTCCAPI void tcc_set_options(TCCState *s, const char *str); - -/*****************************/ -/* preprocessor */ - -/* add include path */ -LIBTCCAPI int tcc_add_include_path(TCCState *s, const char *pathname); - -/* add in system include path */ -LIBTCCAPI int tcc_add_sysinclude_path(TCCState *s, const char *pathname); - -/* define preprocessor symbol 'sym'. value can be NULL, sym can be "sym=val" */ -LIBTCCAPI void tcc_define_symbol(TCCState *s, const char *sym, const char *value); - -/* undefine preprocess symbol 'sym' */ -LIBTCCAPI void tcc_undefine_symbol(TCCState *s, const char *sym); - -/*****************************/ -/* compiling */ - -/* add a file (C file, dll, object, library, ld script). Return -1 if error. */ -LIBTCCAPI int tcc_add_file(TCCState *s, const char *filename); - -/* compile a string containing a C source. Return -1 if error. */ -LIBTCCAPI int tcc_compile_string(TCCState *s, const char *buf); - -/*****************************/ -/* linking commands */ - -/* set output type. MUST BE CALLED before any compilation */ -LIBTCCAPI int tcc_set_output_type(TCCState *s, int output_type); -#define TCC_OUTPUT_MEMORY 1 /* output will be run in memory (default) */ -#define TCC_OUTPUT_EXE 2 /* executable file */ -#define TCC_OUTPUT_DLL 3 /* dynamic library */ -#define TCC_OUTPUT_OBJ 4 /* object file */ -#define TCC_OUTPUT_PREPROCESS 5 /* only preprocess (used internally) */ - -/* equivalent to -Lpath option */ -LIBTCCAPI int tcc_add_library_path(TCCState *s, const char *pathname); - -/* the library name is the same as the argument of the '-l' option */ -LIBTCCAPI int tcc_add_library(TCCState *s, const char *libraryname); - -/* add a symbol to the compiled program */ -LIBTCCAPI int tcc_add_symbol(TCCState *s, const char *name, const void *val); - -/* output an executable, library or object file. DO NOT call - tcc_relocate() before. */ -LIBTCCAPI int tcc_output_file(TCCState *s, const char *filename); - -/* link and run main() function and return its value. DO NOT call - tcc_relocate() before. */ -LIBTCCAPI int tcc_run(TCCState *s, int argc, char **argv); - -/* do all relocations (needed before using tcc_get_symbol()) */ -LIBTCCAPI int tcc_relocate(TCCState *s1, void *ptr); -/* possible values for 'ptr': - - TCC_RELOCATE_AUTO : Allocate and manage memory internally - - NULL : return required memory size for the step below - - memory address : copy code to memory passed by the caller - returns -1 if error. */ -#define TCC_RELOCATE_AUTO (void*)1 - -/* return symbol value or NULL if not found */ -LIBTCCAPI void *tcc_get_symbol(TCCState *s, const char *name); - -/* return symbol value or NULL if not found */ -LIBTCCAPI void tcc_list_symbols(TCCState *s, void *ctx, - void (*symbol_cb)(void *ctx, const char *name, const void *val)); - -#ifdef __cplusplus -} -#endif - -#endif - -``` -包含了以下接口: -- 编译环境设置。 - - `TCCState`结构就是编译的上下文,贯穿始终。 -- 预处理 - - 添加头文件路径。 - - 添加系统头文件路径。 - - 处理宏定义符号。 -- 编译 - - 添加以文件:c文件、dll文件、对象文件、库文件、链接脚本。 - - 编译一个包含C源代码的字符串。 -- 链接 - - 设置输出文件类型:输出在内存中、可执行文件、对象文件、动态/静态链接库、预处理文件。 - - 添加库目录。 - - 添加一个符号。 - - 输出文件。 -- 运行 - - 从`main`函数入口运行编译好的文件。 - - 重定位。 - -定义都在 - -### 8.2 libtcc使用示例 - -`./tests/libtcc_test.c`展示了libtcc的使用示例: -```c++ -/* - * Simple Test program for libtcc - * - * libtcc can be useful to use tcc as a "backend" for a code generator. - */ -#include -#include -#include -#include - -#include "libtcc.h" - -void handle_error(void *opaque, const char *msg) -{ - fprintf(opaque, "%s\n", msg); -} - -/* this function is called by the generated code */ -int add(int a, int b) -{ - return a + b; -} - -/* this strinc is referenced by the generated code */ -const char hello[] = "Hello World!"; - -char my_program[] = -"#include \n" /* include the "Simple libc header for TCC" */ -"extern int add(int a, int b);\n" -"#ifdef _WIN32\n" /* dynamically linked data needs 'dllimport' */ -" __attribute__((dllimport))\n" -"#endif\n" -"extern const char hello[];\n" -"int fib(int n)\n" -"{\n" -" if (n <= 2)\n" -" return 1;\n" -" else\n" -" return fib(n-1) + fib(n-2);\n" -"}\n" -"\n" -"int foo(int n)\n" -"{\n" -" printf(\"%s\\n\", hello);\n" -" printf(\"fib(%d) = %d\\n\", n, fib(n));\n" -" printf(\"add(%d, %d) = %d\\n\", n, 2 * n, add(n, 2 * n));\n" -" return 0;\n" -"}\n"; - -int main(int argc, char **argv) -{ - TCCState *s; - int i; - int (*func)(int); - - s = tcc_new(); - if (!s) { - fprintf(stderr, "Could not create tcc state\n"); - exit(1); - } - - assert(tcc_get_error_func(s) == NULL); - assert(tcc_get_error_opaque(s) == NULL); - - tcc_set_error_func(s, stderr, handle_error); - - assert(tcc_get_error_func(s) == handle_error); - assert(tcc_get_error_opaque(s) == stderr); - - /* if tcclib.h and libtcc1.a are not installed, where can we find them */ - for (i = 1; i < argc; ++i) { - char *a = argv[i]; - if (a[0] == '-') { - if (a[1] == 'B') - tcc_set_lib_path(s, a+2); - else if (a[1] == 'I') - tcc_add_include_path(s, a+2); - else if (a[1] == 'L') - tcc_add_library_path(s, a+2); - } - } - - /* MUST BE CALLED before any compilation */ - tcc_set_output_type(s, TCC_OUTPUT_MEMORY); - - if (tcc_compile_string(s, my_program) == -1) - return 1; - - /* as a test, we add symbols that the compiled program can use. - You may also open a dll with tcc_add_dll() and use symbols from that */ - tcc_add_symbol(s, "add", add); - tcc_add_symbol(s, "hello", hello); - - /* relocate the code */ - if (tcc_relocate(s, TCC_RELOCATE_AUTO) < 0) - return 1; - - /* get entry symbol */ - func = tcc_get_symbol(s, "foo"); - if (!func) - return 1; - - /* run the code */ - func(32); - - /* delete the state */ - tcc_delete(s); - - return 0; -} -``` - -步骤: -- tcc_new新建一个编译环境,最后需要调用tcc_delete释放。 -- 设置错误输出设备和错误处理函数。 -- 通过选项设置了包含目录和库目录。 -- 设置输出类型。 -- 编译字符串。 -- 甚至中间添加符号到编译的程序中,可以获取到编译后的程序的符号。 -- 重定位。 -- 执行获取到的函数。 - - -编译这个测试程序: -```shell -make libtcc_test -``` -执行了命令: -``` -gcc -o libtcc_test libtcc_test.c ../libtcc.a -fno-strict-aliasing -I.. -I.. -lm -lpthread -ldl -``` -执行测试: -```shell -./libtcc_test -I.. -I../include -B.. -``` -执行结果 -``` -Hello World! -fib(32) = 2178309 -add(32, 64) = 96 -``` - -其中实时了编译了一个字符串,加载到了内存,取到了其中的一个函数的函数指针并进行了调用。 - -编译链接加载的过程都有了,可以先从这个流程入手。 - -### 8.3 调试测试程序 - -编译调试版本的libtcc_test,进行调试。 -``` -gcc -o libtcc_test libtcc_test.c ../libtcc.a -fno-strict-aliasing -I.. -I.. -lm -lpthread -ldl -g -``` - -编译环境: -```c++ -struct TCCState { - unsigned char verbose; /* if true, display some information during compilation */ - unsigned char nostdinc; /* if true, no standard headers are added */ - unsigned char nostdlib; /* if true, no standard libraries are added */ - unsigned char nocommon; /* if true, do not use common symbols for .bss data */ - unsigned char static_link; /* if true, static linking is performed */ - unsigned char rdynamic; /* if true, all symbols are exported */ - unsigned char symbolic; /* if true, resolve symbols in the current module first */ - unsigned char filetype; /* file type for compilation (NONE,C,ASM) */ - unsigned char optimize; /* only to #define __OPTIMIZE__ */ - unsigned char option_pthread; /* -pthread option */ - unsigned char enable_new_dtags; /* -Wl,--enable-new-dtags */ - unsigned int cversion; /* supported C ISO version, 199901 (the default), 201112, ... */ - - /* C language options */ - unsigned char char_is_unsigned; - unsigned char leading_underscore; - unsigned char ms_extensions; /* allow nested named struct w/o identifier behave like unnamed */ - unsigned char dollars_in_identifiers; /* allows '$' char in identifiers */ - unsigned char ms_bitfields; /* if true, emulate MS algorithm for aligning bitfields */ - - /* warning switches */ - unsigned char warn_none; - unsigned char warn_all; - unsigned char warn_error; - unsigned char warn_write_strings; - unsigned char warn_unsupported; - unsigned char warn_implicit_function_declaration; - unsigned char warn_discarded_qualifiers; - #define WARN_ON 1 /* warning is on (-Woption) */ - unsigned char warn_num; /* temp var for tcc_warning_c() */ - - unsigned char option_r; /* option -r */ - unsigned char do_bench; /* option -bench */ - unsigned char just_deps; /* option -M */ - unsigned char gen_deps; /* option -MD */ - unsigned char include_sys_deps; /* option -MD */ - - /* compile with debug symbol (and use them if error during execution) */ - unsigned char do_debug; - unsigned char do_backtrace; -#ifdef CONFIG_TCC_BCHECK - /* compile with built-in memory and bounds checker */ - unsigned char do_bounds_check; -#endif - unsigned char test_coverage; /* generate test coverage code */ - - /* use GNU C extensions */ - unsigned char gnu_ext; - /* use TinyCC extensions */ - unsigned char tcc_ext; - - unsigned char dflag; /* -dX value */ - unsigned char Pflag; /* -P switch (LINE_MACRO_OUTPUT_FORMAT) */ - -#ifdef TCC_TARGET_X86_64 - unsigned char nosse; /* For -mno-sse support. */ -#endif -#ifdef TCC_TARGET_ARM - unsigned char float_abi; /* float ABI of the generated code*/ -#endif - - unsigned char has_text_addr; - addr_t text_addr; /* address of text section */ - unsigned section_align; /* section alignment */ -#ifdef TCC_TARGET_I386 - int seg_size; /* 32. Can be 16 with i386 assembler (.code16) */ -#endif - - char *tcc_lib_path; /* CONFIG_TCCDIR or -B option */ - char *soname; /* as specified on the command line (-soname) */ - char *rpath; /* as specified on the command line (-Wl,-rpath=) */ - - char *init_symbol; /* symbols to call at load-time (not used currently) */ - char *fini_symbol; /* symbols to call at unload-time (not used currently) */ - - /* output type, see TCC_OUTPUT_XXX */ - int output_type; - /* output format, see TCC_OUTPUT_FORMAT_xxx */ - int output_format; - /* nth test to run with -dt -run */ - int run_test; - - /* array of all loaded dlls (including those referenced by loaded dlls) */ - DLLReference **loaded_dlls; - int nb_loaded_dlls; - - /* include paths */ - char **include_paths; - int nb_include_paths; - - char **sysinclude_paths; - int nb_sysinclude_paths; - - /* library paths */ - char **library_paths; - int nb_library_paths; - - /* crt?.o object path */ - char **crt_paths; - int nb_crt_paths; - - /* -D / -U options */ - CString cmdline_defs; - /* -include options */ - CString cmdline_incl; - - /* error handling */ - void *error_opaque; - void (*error_func)(void *opaque, const char *msg); - int error_set_jmp_enabled; - jmp_buf error_jmp_buf; - int nb_errors; - - /* output file for preprocessing (-E) */ - FILE *ppfp; - - /* for -MD/-MF: collected dependencies for this compilation */ - char **target_deps; - int nb_target_deps; - - /* compilation */ - BufferedFile *include_stack[INCLUDE_STACK_SIZE]; - BufferedFile **include_stack_ptr; - - int ifdef_stack[IFDEF_STACK_SIZE]; - int *ifdef_stack_ptr; - - /* included files enclosed with #ifndef MACRO */ - int cached_includes_hash[CACHED_INCLUDES_HASH_SIZE]; - CachedInclude **cached_includes; - int nb_cached_includes; - - /* #pragma pack stack */ - int pack_stack[PACK_STACK_SIZE]; - int *pack_stack_ptr; - char **pragma_libs; - int nb_pragma_libs; - - /* inline functions are stored as token lists and compiled last - only if referenced */ - struct InlineFunc **inline_fns; - int nb_inline_fns; - - /* sections */ - Section **sections; - int nb_sections; /* number of sections, including first dummy section */ - - Section **priv_sections; - int nb_priv_sections; /* number of private sections */ - - /* got & plt handling */ - Section *got; - Section *plt; - - /* predefined sections */ - Section *text_section, *data_section, *rodata_section, *bss_section; - Section *common_section; - Section *cur_text_section; /* current section where function code is generated */ -#ifdef CONFIG_TCC_BCHECK - /* bound check related sections */ - Section *bounds_section; /* contains global data bound description */ - Section *lbounds_section; /* contains local data bound description */ -#endif - /* test coverage */ - Section *tcov_section; - /* symbol sections */ - Section *symtab_section; - /* debug sections */ - Section *stab_section; - /* Is there a new undefined sym since last new_undef_sym() */ - int new_undef_sym; - - /* temporary dynamic symbol sections (for dll loading) */ - Section *dynsymtab_section; - /* exported dynamic symbol section */ - Section *dynsym; - /* copy of the global symtab_section variable */ - Section *symtab; - /* extra attributes (eg. GOT/PLT value) for symtab symbols */ - struct sym_attr *sym_attrs; - int nb_sym_attrs; - /* ptr to next reloc entry reused */ - ElfW_Rel *qrel; - #define qrel s1->qrel - -#ifdef TCC_TARGET_RISCV64 - struct pcrel_hi { addr_t addr, val; } last_hi; - #define last_hi s1->last_hi -#endif - -#ifdef TCC_TARGET_PE - /* PE info */ - int pe_subsystem; - unsigned pe_characteristics; - unsigned pe_file_align; - unsigned pe_stack_size; - addr_t pe_imagebase; -# ifdef TCC_TARGET_X86_64 - Section *uw_pdata; - int uw_sym; - unsigned uw_offs; -# endif -#endif - -#ifndef ELF_OBJ_ONLY - int nb_sym_versions; - struct sym_version *sym_versions; - int nb_sym_to_version; - int *sym_to_version; - int dt_verneednum; - Section *versym_section; - Section *verneed_section; -#endif - -#ifdef TCC_IS_NATIVE - const char *runtime_main; - void **runtime_mem; - int nb_runtime_mem; -#endif - -#ifdef CONFIG_TCC_BACKTRACE - int rt_num_callers; -#endif - - /* benchmark info */ - int total_idents; - int total_lines; - int total_bytes; - int total_output[4]; - - /* option -dnum (for general development purposes) */ - int g_debug; - - /* used by tcc_load_ldscript */ - int fd, cc; - - /* for warnings/errors for object files */ - const char *current_filename; - - /* used by main and tcc_parse_args only */ - struct filespec **files; /* files seen on command line */ - int nb_files; /* number thereof */ - int nb_libraries; /* number of libs thereof */ - char *outfile; /* output filename */ - char *deps_outfile; /* option -MF */ - int argc; - char **argv; -}; -``` - -编译的选项配置、C语言选项、内存边界检查、编译后生成的各个段全都在这里了,贯穿整个编译链接加载到执行过程的始终。 - -Section定义:看不懂,暂时不管。 -```c++ -/* section definition */ -typedef struct Section { - unsigned long data_offset; /* current data offset */ - unsigned char *data; /* section data */ - unsigned long data_allocated; /* used for realloc() handling */ - TCCState *s1; - int sh_name; /* elf section name (only used during output) */ - int sh_num; /* elf section number */ - int sh_type; /* elf section type */ - int sh_flags; /* elf section flags */ - int sh_info; /* elf section info */ - int sh_addralign; /* elf section alignment */ - int sh_entsize; /* elf entry size */ - unsigned long sh_size; /* section size (only used during output) */ - addr_t sh_addr; /* address at which the section is relocated */ - unsigned long sh_offset; /* file offset */ - int nb_hashed_syms; /* used to resize the hash table */ - struct Section *link; /* link to another section */ - struct Section *reloc; /* corresponding section for relocation, if any */ - struct Section *hash; /* hash table for symbols */ - struct Section *prev; /* previous section on section stack */ - char name[1]; /* section name */ -} Section; -``` -熟悉的`.text` `.data` `.bss`等段都会在`tcc_new`中调用`tccelf_new`创建。 - - -### 8.4 编译过程 - -入口:`tcc_compile_string`直接调用`tcc_compile` - -传入编译环境和源码字符串。 - -```c++ -/* compile the file opened in 'file'. Return non zero if errors. */ -static int tcc_compile(TCCState *s1, int filetype, const char *str, int fd) -{ - /* Here we enter the code section where we use the global variables for - parsing and code generation (tccpp.c, tccgen.c, -gen.c). - Other threads need to wait until we're done. - - Alternatively we could use thread local storage for those global - variables, which may or may not have advantages */ - - tcc_enter_state(s1); - s1->error_set_jmp_enabled = 1; - - if (setjmp(s1->error_jmp_buf) == 0) { - s1->nb_errors = 0; - - if (fd == -1) { - int len = strlen(str); - tcc_open_bf(s1, "", len); - memcpy(file->buffer, str, len); - } else { - tcc_open_bf(s1, str, 0); - file->fd = fd; - } - - tccelf_begin_file(s1); - preprocess_start(s1, filetype); - tccgen_init(s1); - if (s1->output_type == TCC_OUTPUT_PREPROCESS) { - tcc_preprocess(s1); - } else if (filetype & (AFF_TYPE_ASM | AFF_TYPE_ASMPP)) { - tcc_assemble(s1, !!(filetype & AFF_TYPE_ASMPP)); - } else { - tccgen_compile(s1); - } - } - tccgen_finish(s1); - preprocess_end(s1); - - s1->error_set_jmp_enabled = 0; - tcc_exit_state(s1); - - tccelf_end_file(s1); - return s1->nb_errors != 0 ? -1 : 0; -} -``` - -主要步骤: -- `tcc_enter_state` -- `tccelf_begin_file` -- `preprocess_start` -- `tccgen_compile` -- `tccgen_finish` -- `preprocess_end` -- `tcc_exit_state` -- `tccelf_end_file` - -感觉代码风格太C了,读起来真的那叫一个晦涩,太难懂了,TODO。 - - diff --git a/TargetWords.txt b/TargetWords.txt new file mode 100644 index 0000000..cc169d8 --- /dev/null +++ b/TargetWords.txt @@ -0,0 +1,2315 @@ +cigar +rebut +sissy +humph +awake +blush +focal +evade +naval +serve +heath +dwarf +model +karma +stink +grade +quiet +bench +abate +feign +major +death +fresh +crust +stool +colon +abase +marry +react +batty +pride +floss +helix +croak +staff +paper +unfed +whelp +trawl +outdo +adobe +crazy +sower +repay +digit +crate +cluck +spike +mimic +pound +maxim +linen +unmet +flesh +booby +forth +first +stand +belly +ivory +seedy +print +yearn +drain +bribe +stout +panel +crass +flume +offal +agree +error +swirl +argue +bleed +delta +flick +totem +wooer +front +shrub +parry +biome +lapel +start +greet +goner +golem +lusty +loopy +round +audit +lying +gamma +labor +islet +civic +forge +corny +moult +basic +salad +agate +spicy +spray +essay +fjord +spend +kebab +guild +aback +motor +alone +hatch +hyper +thumb +dowry +ought +belch +dutch +pilot +tweed +comet +jaunt +enema +steed +abyss +growl +fling +dozen +boozy +erode +world +gouge +click +briar +great +altar +pulpy +blurt +coast +duchy +groin +fixer +group +rogue +badly +smart +pithy +gaudy +chill +heron +vodka +finer +surer +radio +rouge +perch +retch +wrote +clock +tilde +store +prove +bring +solve +cheat +grime +exult +usher +epoch +triad +break +rhino +viral +conic +masse +sonic +vital +trace +using +peach +champ +baton +brake +pluck +craze +gripe +weary +picky +acute +ferry +aside +tapir +troll +unify +rebus +boost +truss +siege +tiger +banal +slump +crank +gorge +query +drink +favor +abbey +tangy +panic +solar +shire +proxy +point +robot +prick +wince +crimp +knoll +sugar +whack +mount +perky +could +wrung +light +those +moist +shard +pleat +aloft +skill +elder +frame +humor +pause +ulcer +ultra +robin +cynic +agora +aroma +caulk +shake +pupal +dodge +swill +tacit +other +thorn +trove +bloke +vivid +spill +chant +choke +rupee +nasty +mourn +ahead +brine +cloth +hoard +sweet +month +lapse +watch +today +focus +smelt +tease +cater +movie +lynch +saute +allow +renew +their +slosh +purge +chest +depot +epoxy +nymph +found +shall +harry +stove +lowly +snout +trope +fewer +shawl +natal +fibre +comma +foray +scare +stair +black +squad +royal +chunk +mince +slave +shame +cheek +ample +flair +foyer +cargo +oxide +plant +olive +inert +askew +heist +shown +zesty +hasty +trash +fella +larva +forgo +story +hairy +train +homer +badge +midst +canny +fetus +butch +farce +slung +tipsy +metal +yield +delve +being +scour +glass +gamer +scrap +money +hinge +album +vouch +asset +tiara +crept +bayou +atoll +manor +creak +showy +phase +froth +depth +gloom +flood +trait +girth +piety +payer +goose +float +donor +atone +primo +apron +blown +cacao +loser +input +gloat +awful +brink +smite +beady +rusty +retro +droll +gawky +hutch +pinto +gaily +egret +lilac +sever +field +fluff +hydro +flack +agape +wench +voice +stead +stalk +berth +madam +night +bland +liver +wedge +augur +roomy +wacky +flock +angry +bobby +trite +aphid +tryst +midge +power +elope +cinch +motto +stomp +upset +bluff +cramp +quart +coyly +youth +rhyme +buggy +alien +smear +unfit +patty +cling +glean +label +hunky +khaki +poker +gruel +twice +twang +shrug +treat +unlit +waste +merit +woven +octal +needy +clown +widow +irony +ruder +gauze +chief +onset +prize +fungi +charm +gully +inter +whoop +taunt +leery +class +theme +lofty +tibia +booze +alpha +thyme +eclat +doubt +parer +chute +stick +trice +alike +sooth +recap +saint +liege +glory +grate +admit +brisk +soggy +usurp +scald +scorn +leave +twine +sting +bough +marsh +sloth +dandy +vigor +howdy +enjoy +valid +ionic +equal +unset +floor +catch +spade +stein +exist +quirk +denim +grove +spiel +mummy +fault +foggy +flout +carry +sneak +libel +waltz +aptly +piney +inept +aloud +photo +dream +stale +vomit +ombre +fanny +unite +snarl +baker +there +glyph +pooch +hippy +spell +folly +louse +gulch +vault +godly +threw +fleet +grave +inane +shock +crave +spite +valve +skimp +claim +rainy +musty +pique +daddy +quasi +arise +aging +valet +opium +avert +stuck +recut +mulch +genre +plume +rifle +count +incur +total +wrest +mocha +deter +study +lover +safer +rivet +funny +smoke +mound +undue +sedan +pagan +swine +guile +gusty +equip +tough +canoe +chaos +covet +human +udder +lunch +blast +stray +manga +melee +lefty +quick +paste +given +octet +risen +groan +leaky +grind +carve +loose +sadly +spilt +apple +slack +honey +final +sheen +eerie +minty +slick +derby +wharf +spelt +coach +erupt +singe +price +spawn +fairy +jiffy +filmy +stack +chose +sleep +ardor +nanny +niece +woozy +handy +grace +ditto +stank +cream +usual +diode +valor +angle +ninja +muddy +chase +reply +prone +spoil +heart +shade +diner +arson +onion +sleet +dowel +couch +palsy +bowel +smile +evoke +creek +lance +eagle +idiot +siren +built +embed +award +dross +annul +goody +frown +patio +laden +humid +elite +lymph +edify +might +reset +visit +gusto +purse +vapor +crock +write +sunny +loath +chaff +slide +queer +venom +stamp +sorry +still +acorn +aping +pushy +tamer +hater +mania +awoke +brawn +swift +exile +birch +lucky +freer +risky +ghost +plier +lunar +winch +snare +nurse +house +borax +nicer +lurch +exalt +about +savvy +toxin +tunic +pried +inlay +chump +lanky +cress +eater +elude +cycle +kitty +boule +moron +tenet +place +lobby +plush +vigil +index +blink +clung +qualm +croup +clink +juicy +stage +decay +nerve +flier +shaft +crook +clean +china +ridge +vowel +gnome +snuck +icing +spiny +rigor +snail +flown +rabid +prose +thank +poppy +budge +fiber +moldy +dowdy +kneel +track +caddy +quell +dumpy +paler +swore +rebar +scuba +splat +flyer +horny +mason +doing +ozone +amply +molar +ovary +beset +queue +cliff +magic +truce +sport +fritz +edict +twirl +verse +llama +eaten +range +whisk +hovel +rehab +macaw +sigma +spout +verve +sushi +dying +fetid +brain +buddy +thump +scion +candy +chord +basin +march +crowd +arbor +gayly +musky +stain +dally +bless +bravo +stung +title +ruler +kiosk +blond +ennui +layer +fluid +tatty +score +cutie +zebra +barge +matey +bluer +aider +shook +river +privy +betel +frisk +bongo +begun +azure +weave +genie +sound +glove +braid +scope +wryly +rover +assay +ocean +bloom +irate +later +woken +silky +wreck +dwelt +slate +smack +solid +amaze +hazel +wrist +jolly +globe +flint +rouse +civil +vista +relax +cover +alive +beech +jetty +bliss +vocal +often +dolly +eight +joker +since +event +ensue +shunt +diver +poser +worst +sweep +alley +creed +anime +leafy +bosom +dunce +stare +pudgy +waive +choir +stood +spoke +outgo +delay +bilge +ideal +clasp +seize +hotly +laugh +sieve +block +meant +grape +noose +hardy +shied +drawl +daisy +putty +strut +burnt +tulip +crick +idyll +vixen +furor +geeky +cough +naive +shoal +stork +bathe +aunty +check +prime +brass +outer +furry +razor +elect +evict +imply +demur +quota +haven +cavil +swear +crump +dough +gavel +wagon +salon +nudge +harem +pitch +sworn +pupil +excel +stony +cabin +unzip +queen +trout +polyp +earth +storm +until +taper +enter +child +adopt +minor +fatty +husky +brave +filet +slime +glint +tread +steal +regal +guest +every +murky +share +spore +hoist +buxom +inner +otter +dimly +level +sumac +donut +stilt +arena +sheet +scrub +fancy +slimy +pearl +silly +porch +dingo +sepia +amble +shady +bread +friar +reign +dairy +quill +cross +brood +tuber +shear +posit +blank +villa +shank +piggy +freak +which +among +fecal +shell +would +algae +large +rabbi +agony +amuse +bushy +copse +swoon +knife +pouch +ascot +plane +crown +urban +snide +relay +abide +viola +rajah +straw +dilly +crash +amass +third +trick +tutor +woody +blurb +grief +disco +where +sassy +beach +sauna +comic +clued +creep +caste +graze +snuff +frock +gonad +drunk +prong +lurid +steel +halve +buyer +vinyl +utile +smell +adage +worry +tasty +local +trade +finch +ashen +modal +gaunt +clove +enact +adorn +roast +speck +sheik +missy +grunt +snoop +party +touch +mafia +emcee +array +south +vapid +jelly +skulk +angst +tubal +lower +crest +sweat +cyber +adore +tardy +swami +notch +groom +roach +hitch +young +align +ready +frond +strap +puree +realm +venue +swarm +offer +seven +dryer +diary +dryly +drank +acrid +heady +theta +junto +pixie +quoth +bonus +shalt +penne +amend +datum +build +piano +shelf +lodge +suing +rearm +coral +ramen +worth +psalm +infer +overt +mayor +ovoid +glide +usage +poise +randy +chuck +prank +fishy +tooth +ether +drove +idler +swath +stint +while +begat +apply +slang +tarot +radar +credo +aware +canon +shift +timer +bylaw +serum +three +steak +iliac +shirk +blunt +puppy +penal +joist +bunny +shape +beget +wheel +adept +stunt +stole +topaz +chore +fluke +afoot +bloat +bully +dense +caper +sneer +boxer +jumbo +lunge +space +avail +short +slurp +loyal +flirt +pizza +conch +tempo +droop +plate +bible +plunk +afoul +savoy +steep +agile +stake +dwell +knave +beard +arose +motif +smash +broil +glare +shove +baggy +mammy +swamp +along +rugby +wager +quack +squat +snaky +debit +mange +skate +ninth +joust +tramp +spurn +medal +micro +rebel +flank +learn +nadir +maple +comfy +remit +gruff +ester +least +mogul +fetch +cause +oaken +aglow +meaty +gaffe +shyly +racer +prowl +thief +stern +poesy +rocky +tweet +waist +spire +grope +havoc +patsy +truly +forty +deity +uncle +swish +giver +preen +bevel +lemur +draft +slope +annoy +lingo +bleak +ditty +curly +cedar +dirge +grown +horde +drool +shuck +crypt +cumin +stock +gravy +locus +wider +breed +quite +chafe +cache +blimp +deign +fiend +logic +cheap +elide +rigid +false +renal +pence +rowdy +shoot +blaze +envoy +posse +brief +never +abort +mouse +mucky +sulky +fiery +media +trunk +yeast +clear +skunk +scalp +bitty +cider +koala +duvet +segue +creme +super +grill +after +owner +ember +reach +nobly +empty +speed +gipsy +recur +smock +dread +merge +burst +kappa +amity +shaky +hover +carol +snort +synod +faint +haunt +flour +chair +detox +shrew +tense +plied +quark +burly +novel +waxen +stoic +jerky +blitz +beefy +lyric +hussy +towel +quilt +below +bingo +wispy +brash +scone +toast +easel +saucy +value +spice +honor +route +sharp +bawdy +radii +skull +phony +issue +lager +swell +urine +gassy +trial +flora +upper +latch +wight +brick +retry +holly +decal +grass +shack +dogma +mover +defer +sober +optic +crier +vying +nomad +flute +hippo +shark +drier +obese +bugle +tawny +chalk +feast +ruddy +pedal +scarf +cruel +bleat +tidal +slush +semen +windy +dusty +sally +igloo +nerdy +jewel +shone +whale +hymen +abuse +fugue +elbow +crumb +pansy +welsh +syrup +terse +suave +gamut +swung +drake +freed +afire +shirt +grout +oddly +tithe +plaid +dummy +broom +blind +torch +enemy +again +tying +pesky +alter +gazer +noble +ethos +bride +extol +decor +hobby +beast +idiom +utter +these +sixth +alarm +erase +elegy +spunk +piper +scaly +scold +hefty +chick +sooty +canal +whiny +slash +quake +joint +swept +prude +heavy +wield +femme +lasso +maize +shale +screw +spree +smoky +whiff +scent +glade +spent +prism +stoke +riper +orbit +cocoa +guilt +humus +shush +table +smirk +wrong +noisy +alert +shiny +elate +resin +whole +hunch +pixel +polar +hotel +sword +cleat +mango +rumba +puffy +filly +billy +leash +clout +dance +ovate +facet +chili +paint +liner +curio +salty +audio +snake +fable +cloak +navel +spurt +pesto +balmy +flash +unwed +early +churn +weedy +stump +lease +witty +wimpy +spoof +saner +blend +salsa +thick +warty +manic +blare +squib +spoon +probe +crepe +knack +force +debut +order +haste +teeth +agent +widen +icily +slice +ingot +clash +juror +blood +abode +throw +unity +pivot +slept +troop +spare +sewer +parse +morph +cacti +tacky +spool +demon +moody +annex +begin +fuzzy +patch +water +lumpy +admin +omega +limit +tabby +macho +aisle +skiff +basis +plank +verge +botch +crawl +lousy +slain +cubic +raise +wrack +guide +foist +cameo +under +actor +revue +fraud +harpy +scoop +climb +refer +olden +clerk +debar +tally +ethic +cairn +tulle +ghoul +hilly +crude +apart +scale +older +plain +sperm +briny +abbot +rerun +quest +crisp +bound +befit +drawn +suite +itchy +cheer +bagel +guess +broad +axiom +chard +caput +leant +harsh +curse +proud +swing +opine +taste +lupus +gumbo +miner +green +chasm +lipid +topic +armor +brush +crane +mural +abled +habit +bossy +maker +dusky +dizzy +lithe +brook +jazzy +fifty +sense +giant +surly +legal +fatal +flunk +began +prune +small +slant +scoff +torus +ninny +covey +viper +taken +moral +vogue +owing +token +entry +booth +voter +chide +elfin +ebony +neigh +minim +melon +kneed +decoy +voila +ankle +arrow +mushy +tribe +cease +eager +birth +graph +odder +terra +weird +tried +clack +color +rough +weigh +uncut +ladle +strip +craft +minus +dicey +titan +lucid +vicar +dress +ditch +gypsy +pasta +taffy +flame +swoop +aloof +sight +broke +teary +chart +sixty +wordy +sheer +leper +nosey +bulge +savor +clamp +funky +foamy +toxic +brand +plumb +dingy +butte +drill +tripe +bicep +tenor +krill +worse +drama +hyena +think +ratio +cobra +basil +scrum +bused +phone +court +camel +proof +heard +angel +petal +pouty +throb +maybe +fetal +sprig +spine +shout +cadet +macro +dodgy +satyr +rarer +binge +trend +nutty +leapt +amiss +split +myrrh +width +sonar +tower +baron +fever +waver +spark +belie +sloop +expel +smote +baler +above +north +wafer +scant +frill +awash +snack +scowl +frail +drift +limbo +fence +motel +ounce +wreak +revel +talon +prior +knelt +cello +flake +debug +anode +crime +salve +scout +imbue +pinky +stave +vague +chock +fight +video +stone +teach +cleft +frost +prawn +booty +twist +apnea +stiff +plaza +ledge +tweak +board +grant +medic +bacon +cable +brawl +slunk +raspy +forum +drone +women +mucus +boast +toddy +coven +tumor +truer +wrath +stall +steam +axial +purer +daily +trail +niche +mealy +juice +nylon +plump +merry +flail +papal +wheat +berry +cower +erect +brute +leggy +snipe +sinew +skier +penny +jumpy +rally +umbra +scary +modem +gross +avian +greed +satin +tonic +parka +sniff +livid +stark +trump +giddy +reuse +taboo +avoid +quote +devil +liken +gloss +gayer +beret +noise +gland +dealt +sling +rumor +opera +thigh +tonga +flare +wound +white +bulky +etude +horse +circa +paddy +inbox +fizzy +grain +exert +surge +gleam +belle +salvo +crush +fruit +sappy +taker +tract +ovine +spiky +frank +reedy +filth +spasm +heave +mambo +right +clank +trust +lumen +borne +spook +sauce +amber +lathe +carat +corer +dirty +slyly +affix +alloy +taint +sheep +kinky +wooly +mauve +flung +yacht +fried +quail +brunt +grimy +curvy +cagey +rinse +deuce +state +grasp +milky +bison +graft +sandy +baste +flask +hedge +girly +swash +boney +coupe +endow +abhor +welch +blade +tight +geese +miser +mirth +cloud +cabal +leech +close +tenth +pecan +droit +grail +clone +guise +ralph +tango +biddy +smith +mower +payee +serif +drape +fifth +spank +glaze +allot +truck +kayak +virus +testy +tepee +fully +zonal +metro +curry +grand +banjo +axion +bezel +occur +chain +nasal +gooey +filer +brace +allay +pubic +raven +plead +gnash +flaky +munch +dully +eking +thing +slink +hurry +theft +shorn +pygmy +ranch +wring +lemon +shore +mamma +froze +newer +style +moose +antic +drown +vegan +chess +guppy +union +lever +lorry +image +cabby +druid +exact +truth +dopey +spear +cried +chime +crony +stunk +timid +batch +gauge +rotor +crack +curve +latte +witch +bunch +repel +anvil +soapy +meter +broth +madly +dried +scene +known +magma +roost +woman +thong +punch +pasty +downy +knead +whirl +rapid +clang +anger +drive +goofy +email +music +stuff +bleep +rider +mecca +folio +setup +verso +quash +fauna +gummy +happy +newly +fussy +relic +guava +ratty +fudge +femur +chirp +forte +alibi +whine +petty +golly +plait +fleck +felon +gourd +brown +thrum +ficus +stash +decry +wiser +junta +visor +daunt +scree +impel +await +press +whose +turbo +stoop +speak +mangy +eying +inlet +crone +pulse +mossy +staid +hence +pinch +teddy +sully +snore +ripen +snowy +attic +going +leach +mouth +hound +clump +tonal +bigot +peril +piece +blame +haute +spied +undid +intro +basal +shine +gecko +rodeo +guard +steer +loamy +scamp +scram +manly +hello +vaunt +organ +feral +knock +extra +condo +adapt +willy +polka +rayon +skirt +faith +torso +match +mercy +tepid +sleek +riser +twixt +peace +flush +catty +login +eject +roger +rival +untie +refit +aorta +adult +judge +rower +artsy +rural +shave \ No newline at end of file diff --git a/wordle.py b/wordle.py new file mode 100644 index 0000000..d2bfa2f --- /dev/null +++ b/wordle.py @@ -0,0 +1,113 @@ +# every step: input a five char word and a five char result, output list of rest words, choose one fot next step. +# in result: y/x/n represent correct/wrong location/incorrect +# input "quit" to quit the game when input word + +allWords = [] +restWords = [] +currentWord = "" +currentResult = "" + +def wordInList(target, list): + for word in list: + if word == target: + return True + return False + +# input "quit" to quit the process +def inputAWord(): + global currentWord + currentWord = input("Input a new word: ") + if currentWord == "quit": + exit() + if not wordInList(currentWord, restWords): + print("Input word is not in the list of rest words, please re-input:") + inputAWord() + +def validResult(result): + for ch in result: + if ch != 'y' and ch != 'x' and ch != 'n': + return False + return len(result) == 5 + +def inputCurrentResult(): + global currentWord, currentResult + currentResult = input(f"Input result of {currentWord}: ") + if not validResult(currentResult): + print("Input result is invalid, please re-input:") + inputCurrentResult() + +def readAllWords(): + global allWords + with open("TargetWords.txt", "rt") as f: + allWords = f.readlines() + for i in range(0, len(allWords)-1): + allWords[i] = allWords[i].strip() + +def printWords(words): + print(f"Current rest words ({len(words)}):") + index = 1 + for word in words: + ending = "" + if index % 10 != 0: + ending = "," + else : + ending = "\n" + print(f"{index:5d} : {word}", end = ending) + index += 1 + print() + +# all filters +def charInString(targetCh, string): + for ch in string: + if ch == targetCh: + return True + return False + +# n +def filterNotHaveChar(ch): + global restWords + newRestWords = [] + for word in restWords: + if not charInString(ch, word): + newRestWords.append(word) + restWords = newRestWords + +# y +def filterIsChar(ch, index): + global restWords + newRestWords = [] + for word in restWords: + if word[index] == ch: + newRestWords.append(word) + restWords = newRestWords + +# x +def filterHasCharButNotThisLocation(ch, index): + global restWords + newRestWords = [] + for word in restWords: + if charInString(ch, word) and word[index] != ch: + newRestWords.append(word) + restWords = newRestWords + + +def filterAccordingCurrentResult(): + global currentWord, currentResult + index = 0 + for ch in currentResult: + if ch == 'n': + filterNotHaveChar(currentWord[index]) + elif ch == 'y': + filterIsChar(currentWord[index], index) + elif ch == 'x': + filterHasCharButNotThisLocation(currentWord[index], index) + index += 1 + +if __name__ == "__main__": + readAllWords() + restWords = allWords + while True: + printWords(restWords) + inputAWord() + inputCurrentResult() + filterAccordingCurrentResult() \ No newline at end of file