[{"content":"📕 转载自 emacs lisp 简明教程 - 水木社区 emacs 版\n这是 叶文彬(水木 id: happierbee) 写的一份 emacs lisp 的教程,深入浅出,非常适合初学者。文档的 tex 代码及 pdf 文档可在* 此处下载* 。\nemacs 的高手不能不会 elisp。但是对于很多人来说 elisp 学习是一个痛苦的历程,至少我是有这样一段经历。因此,我写了这一系列文章,希望能为后来者提供一点捷径。\n一个 hello world 例子 自从 k\u0026amp;r 以来,hello world 程序历来都是程序语言教程的第一个例子。我也用一个 hello world 的例子来演示 emacs 里执行 elisp 的环境。下面就是这个语句:\n(message \u0026#34;hello world\u0026#34;) 前面我没有说这个一个程序,这是因为,elisp 不好作为可执行方式来运行(当然也不是不可能),所有的 elisp 都是运行在 emacs 这个环境下。\n首先切换到 *scratch* 缓冲区里,如果当前模式不是 lisp-interaction-mode,用 m-x lisp-interaction-mode 先转换到 lisp-interaction-mode。然后输入前面这一行语句。在行尾右括号后,按 c-j 键。如果 minibuffer 里显示 hello world,光标前一行也显示 \u0026quot;hello world\u0026quot;,那说明你的操作没有问题。我们就可以开始 elisp 学习之旅了。\n注:elisp 里的一个完整表达式,除了简单数据类型(如数字,向量),都是用括号括起来,称为一个 s-表达式。让 elisp 解释器执行一个 s-表达式除了前一种方法之外,还可以用 c-x c-e。它们的区别是,c-x c-e 是一个全局按键绑定,几乎可以在所有地方都能用。它会将运行返回值显示在 minibuffer 里。这里需要强调一个概念是返回值和作用是不同的。比如前面 message 函数它的作用是在 minibuffer 里显示一个字符串,但是它的返回值是 \u0026quot;hello world\u0026quot; 字符串。\n基础知识 这一节介绍一下 elisp 编程中一些最基本的概念,比如如何定义函数,程序的控制结构,变量的使用和作用域等等。\n函数和变量 elisp 中定义一个函数是用这样的形式:\n(defun function-name (arguments-list)\r\u0026#34;document string\u0026#34;\rbody) 比如:\n(defun hello-world (name) \u0026#34;say hello to user whose name is name.\u0026#34; (message \u0026#34;hello, %s\u0026#34; name)) 其中函数的文档字符串是可以省略的。但是建议为你的函数(除了最简单,不作为接口的)都加上文档字符串。这样将来别人使用你的扩展或者别人阅读你的代码或者自己进行维护都提供很大的方便。\n在 emacs 里,当光标处于一个函数名上时,可以用 c-h f 查看这个函数的文档。比如前面这个函数,在 *help* 缓冲区里的文档是:\nhello-world is a lisp function.\r(hello-world name)\rsay hello to user whose name is name. 如果你的函数是在文件中定义的。这个文档里还会给出一个链接能跳到定义的地方。\n要运行一个函数,最一般的方式是:\n(function-name arguments-list) 比如前面这个函数:\n(hello-world \u0026#34;emacser\u0026#34;) ; =\u0026gt; \u0026#34;hello, emacser\u0026#34; 每个函数都有一个返回值。这个返回值一般是函数定义里的最后一个表达式的值。\nelisp 里的变量使用无需象 c 语言那样需要声明,你可以用 setq 直接对一个变量赋值。\n(setq foo \u0026#34;i\u0026#39;m foo\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; (message foo) ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; 和函数一样,你可以用 c-h v 查看一个变量的文档。比如当光标在 foo 上时用 c-h v 时,文档是这样的:\nfoo\u0026#39;s value is \u0026#34;i\u0026#39;m foo\u0026#34;\rdocumentation:\rnot documented as a variable. 有一个特殊表达式(special form)defvar,它可以声明一个变量,一般的形式是:\n(defvar variable-name value\r\u0026#34;document string\u0026#34;) 它与 setq 所不同的是,如果变量在声明之前,这个变量已经有一个值的话, 用 defvar 声明的变量值不会改变成声明的那个值 。另一个区别是 defvar 可以为变量提供文档字符串,当变量是在文件中定义的话,c-h v 后能给出变量定义的位置。比如:\n(defvar foo \u0026#34;did i have a value?\u0026#34; \u0026#34;a demo variable\u0026#34;) ; =\u0026gt; foo foo ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; (defvar bar \u0026#34;i\u0026#39;m bar\u0026#34; \u0026#34;a demo variable named \\\u0026#34;bar\\\u0026#34;\u0026#34;) ; =\u0026gt; bar bar ; =\u0026gt; \u0026#34;i\u0026#39;m bar\u0026#34; 用 c-h v 查看 foo 的文档,可以看到它已经变成:\nfoo\u0026#39;s value is \u0026#34;i\u0026#39;m foo\u0026#34;\rdocumentation:\ra demo variable 由于 elisp 中函数是全局的,变量也很容易成为全局变量(因为全局变量和局部变量的赋值都是使用 setq 函数),名字不互相冲突是很关键的。所以除了为你的函数和变量选择一个合适的前缀之外,用 c-h f 和 c-h v 查看一下函数名和变量名有没有已经被使用过是很关键的。\n局部作用域的变量 如果没有局部作用域的变量,都使用全局变量,函数会相当难写。elisp 里可以用 let 和 let* 进行局部变量的绑定。let 使用的形式是:\n(let (bindings)\rbody) bingdings 可以是 (var value) 这样对 var 赋初始值的形式,或者用 var 声明一个初始值为 nil 的变量。比如:\n(defun circle-area (radix) (let ((pi 3.1415926) area) (setq area (* pi radix radix)) (message \u0026#34;直径为 %.2f 的圆面积是 %.2f\u0026#34; radix area))) (circle-area 3) c-h v 查看 area 和 pi 应该没有这两个变量。\nlet* 和 let 的使用形式完全相同,唯一的区别是在 let* 声明中就能使用前面声明的变量,比如:\n(defun circle-area (radix) (let* ((pi 3.1415926) (area (* pi radix radix))) (message \u0026#34;直径为 %.2f 的圆面积是 %.2f\u0026#34; radix area))) lambda 表达式 可能你久闻 lambda 表达式的大名了。其实依我的理解,lambda 表达式相当于其它语言中的匿名函数。比如 perl 里的匿名函数。它的形式和 defun 是完全一样的:\n(lambda (arguments-list)\r\u0026#34;documentation string\u0026#34;\rbody) 调用 lambda 方法如下:\n(funcall (lambda (name) (message \u0026#34;hello, %s!\u0026#34; name)) \u0026#34;emacser\u0026#34;) 你也可以把 lambda 表达式赋值给一个变量,然后用 funcall 调用:\n(setq foo (lambda (name) (message \u0026#34;hello, %s!\u0026#34; name))) (funcall foo \u0026#34;emacser\u0026#34;) ; =\u0026gt; \u0026#34;hello, emacser!\u0026#34; lambda 表达式最常用的是作为参数传递给其它函数,比如 mapc。\n控制结构 顺序执行 一般来说程序都是按表达式顺序依次执行的。这在 defun 等特殊环境中是自动进行的。但是一般情况下都不是这样的。比如你无法用 eval-last-sexp 同时执行两个表达式,在 if 表达式中的条件为真时执行的部分也只能运行一个表达式。这时就需要用 progn 这个特殊表达式。它的使用形式如下:\n(progn a b c ...) 它的作用就是让表达式 a, b, c 顺序执行。比如:\n(progn (setq foo 3) (message \u0026#34;square of %d is %d\u0026#34; foo (* foo foo))) 条件判断 elisp 有两个最基本的条件判断表达式 if 和 cond。使用形式分别如下:\n(if condition\rthen\relse)\r(cond (case1 do-when-case1)\r(case2 do-when-case2)\r...\r(t do-when-none-meet)) 使用的例子如下:\n(defun my-max (a b) (if (\u0026gt; a b) a b)) (my-max 3 4) ; =\u0026gt; 4 (defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; =\u0026gt; 55 还有两个宏 when 和 unless,从它们的名字也就能知道它们是作什么用的。使用这两个宏的好处是使代码可读性提高,when 能省去 if 里的 progn 结构,unless 省去条件为真子句需要的的 nil 表达式。\n循环 循环使用的是 while 表达式。它的形式是:\n(while condition\rbody) 比如:\n(defun factorial (n) (let ((res 1)) (while (\u0026gt; n 1) (setq res (* res n) n (- n 1))) res)) (factorial 10) ; =\u0026gt; 3628800 逻辑运算 条件的逻辑运算和其它语言都是很类似的,使用 and、or、not。and 和 or 也同样具有短路性质。很多人喜欢在表达式短时,用 and 代替 when,or 代替 unless。当然这时一般不关心它们的返回值,而是在于表达式其它子句的副作用。比如 or 经常用于设置函数的缺省值,而 and 常用于参数检查:\n(defun hello-world (\u0026amp;optional name) (or name (setq name \u0026#34;emacser\u0026#34;)) (message \u0026#34;hello, %s\u0026#34; name)) ; =\u0026gt; hello-world (hello-world) ; =\u0026gt; \u0026#34;hello, emacser\u0026#34; (hello-world \u0026#34;ye\u0026#34;) ; =\u0026gt; \u0026#34;hello, ye\u0026#34; (defun square-number-p (n) (and (\u0026gt;= n 0) (= (/ n (sqrt n)) (sqrt n)))) (square-number-p -1) ; =\u0026gt; nil (square-number-p 25) ; =\u0026gt; t 函数列表 (defun name arglist [docstring] body...) (defvar symbol \u0026amp;optional initvalue docstring) (setq sym val sym val ...) (let varlist body...) (let* varlist body...) (lambda args [docstring] [interactive] body) (progn body ...) (if cond then else...) (cond clauses...) (when cond body ...) (unless cond body ...) (when cond body ...) (or conditions ...) (and conditions ...) (not object) 基本数据类型之一 \u0026ndash; 数字 elisp 里的对象都是有类型的,而且每一个对象它们知道自己是什么类型。 你得到一个变量名之后可以用一系列检测方法来测试这个变量是什么类型(好像没有什么方法来让它说出自己是什么类型的)。内建的 emacs 数据类型称为 primitive types,包括整数、浮点数、cons、符号 (symbol)、字符串、向量 (vector)、散列表 (hash-table)、subr(内建函数,比如 cons, if, and 之类)、byte-code function,和其它特殊类型,例如缓冲区(buffer)。\n在开始前有必要先了解一下读入语法和输出形式。所谓读入语法是让 elisp 解释器明白输入字符所代表的对象,你不可能让 elisp 读入 .#@!? 这样奇怪的东西还能好好工作吧(perl 好像经常要受这样的折磨:))。简单的来说,一种数据类型有(也可能没有,比如散列表)对应的规则来让解释器产生这种数据类型,比如 123 产生整数 123, (a . b) 产生一个 cons。所谓输出形式是解释器用产生一个字符串来表示一个数据对象。比如整数 123 的输出形式就是 123,cons cell (a . b) 的输出形式是 (a . b)。与读入语法不同的是,数据对象都有输出形式。比如散列表的输出可能是这样的:\n#\u0026lt;hash-table \u0026#39;eql nil 0/65 0xa7344c8\u0026gt; 通常一个对象的数据对象的输出形式和它的读入形式都是相同的。现在就先从简单的数据类型──数字开始吧。\nemacs 的数字分为整数和浮点数(和 c 比没有双精度数 double)。1, 1.,+1, -1, 536870913, 0, -0 这些都是整数。整数的范围是和机器是有关的,一般来最小范围是在 -268435456 to 268435455(29 位,-2**28 ~ 2**28-1)。可以从 most-positive-fixnum 和 most-negative-fixnum 两个变量得到整数的范围。\n你可以用多种进制来输入一个整数。比如:\n#b101100 =\u0026gt; 44 ; 二进制 #o54 =\u0026gt; 44 ; 八进制 #x2c =\u0026gt; 44 ; 十六进制 最神奇的是你可以用 2 到 36 之间任意一个数作为基数,比如:\n#24r1k =\u0026gt; 44 ; 二十四进制 之所以最大是 36,是因为只有 0-9 和 a-z 36 个字符来表示数字。但是我想基本上不会有人会用到 emacs 的这个特性。\n1500.0, 15e2, 15.0e2, 1.5e3, 和 .15e4 都可以用来表示一个浮点数 1500.。遵循 ieee 标准,elisp 也有一个特殊类型的值称为 nan (not-a-number)。你可以用 (/ 0.0 0.0) 产生这个数。\n测试函数 整数类型测试函数是 integerp,浮点数类型测试函数是 floatp。数字类型测试用 numberp。你可以分别运行这几个例子来试验一下:\n(integerp 1.) ; =\u0026gt; t (integerp 1.0) ; =\u0026gt; nil (floatp 1.) ; =\u0026gt; nil (floatp -0.0e+nan) ; =\u0026gt; t (numberp 1) ; =\u0026gt; t 还提供一些特殊测试,比如测试是否是零的 zerop ,还有非负整数测试的 wholenump 。\n注:elisp 测试函数一般都是用 p 来结尾,p 是 predicate 的第一个字母。如果函数名是一个单词,通常只是在这个单词后加一个 p,如果是多个单词,一般是加 -p。\n数的比较 常用的比较操作符号是我们在其它言中都很熟悉的,比如 \u0026lt;, \u0026gt;, \u0026gt;=, \u0026lt;=,不一样的是,由于赋值是使用 set 函数,所以 = 不再是一个赋值运算符了,而是测试数字相等符号。和其它语言类似, 对于浮点数的相等测试都是不可靠的 。比如:\n(setq foo (- (+ 1.0 1.0e-3) 1.0)) ; =\u0026gt; 0.0009999999999998899 (setq bar 1.0e-3) ; =\u0026gt; 0.001 (= foo bar) ; =\u0026gt; nil 所以一定要确定两个浮点数是否相同,是要在一定误差内进行比较。这里给出一个函数:\n(defvar fuzz-factor 1.0e-6) (defun approx-equal (x y) (or (and (= x 0) (= y 0)) (\u0026lt; (/ (abs (- x y)) (max (abs x) (abs y))) fuzz-factor))) (approx-equal foo bar) ; =\u0026gt; t 还有一个测试数字是否相等的函数 eql ,这是函数不仅测试数字的值是否相等,还测试数字类型是否一致,比如:\n(= 1.0 1) ; =\u0026gt; t (eql 1.0 1) ; =\u0026gt; nil elisp 没有 +=, -=, /=, *= 这样的命令式语言里常见符号,如果你想实现类似功能的语句,只能用赋值函数 setq 来实现了。 /= 符号被用来作为不等于的测试了。\n数的转换 整数向浮点数转换是通过 float 函数进行的。而浮点数转换成整数有这样几个函数:\ntruncate 转换成靠近 0 的整数 floor 转换成最接近的不比本身大的整数 ceiling 转换成最接近的不比本身小的整数 round 四舍五入后的整数,换句话说和它的差绝对值最小的整数 很晕是吧。自己用 1.2, 1.7, -1.2, -1.7 对这四个函数操作一遍就知道区别了(可以直接看 info。按键顺序是 c-h i m elisp ret m numeric conversions ret。以后简写成 info elisp - numeric conversions)。\n这里提一个问题,浮点数的范围是无穷大的,而整数是有范围的,如果用前面的函数转换 1e20 成一个整数会出现什么情况呢?试试就知道了。\n数的运算 四则运算没有什么好说的,就是 + - * /。值得注意的是,和 c 语言类似,如果参数都是整数,作除法时要记住 (/ 5 6) 是会等于 0 的。如果参数中有浮点数,整数会自动转换成浮点数进行运算,所以 (/ 5 6.0) 的值才会是 5/6。\n没有 ++ 和 -- 操作了,类似的两个函数是 1+ 和 1- 。可以用 setq 赋值来代替 ++ 和 --:\n(setq foo 10) ; =\u0026gt; 10 (setq foo (1+ foo)) ; =\u0026gt; 11 (setq foo (1- foo)) ; =\u0026gt; 10 注:可能有人看过有 incf 和 decf 两个实现 ++ 和 -- 操作。这两个宏是可以用的。这两个宏是 common lisp 里的,emacs 有模拟的 common lisp 的库 cl。但是 rms 认为最好不要使用这个库。但是你可以在你的 elisp 包中使用这两个宏,只要在文件头写上:\n(eval-when-compile (require \u0026#39;cl)) 由于 incf 和 decf 是两个宏,所以这样写不会在运行里导入 cl 库。有点离题是,总之一句话,教主说不好的东西,我们最好不要用它。其它无所谓,只可惜了两个我最常用的函数 remove-if 和 remove-if-not 。不过如果你也用 emms 的话,可以在 emms-compat 里找到这两个函数的替代品。\nabs 取数的绝对值。\n有两个取整的函数,一个是符号 % ,一个是函数 mod 。这两个函数有什么差别呢?一是 % 的第一个参数必须是整数,而 mod 的第一个参数可以是整数也可以是浮点数。二是即使对相同的参数,两个函数也不一定有相同的返回值:\n(+ (% dividend divisor)\r(* (/ dividend divisor) divisor)) 和 dividend 是相同的。而:\n(+ (mod dividend divisor)\r(* (floor dividend divisor) divisor)) 和 dividend 是相同的。\n三角运算有函数: sin, cos, tan, asin, acos, atan。开方函数是 sqrt。\nexp 是以 e 为底的指数运算,expt 可以指定底数的指数运算。log 默认底数是 e,但是也可以指定底数。log10 就是 (log x 10)。logb 是以 2 为底数运算,但是返回的是一个整数。这个函数是用来计算数的位。\nrandom 可以产生随机数。可以用 (random t) 来产生一个新种子。虽然 emacs 每次启动后调用 random 总是产生相同的随机数,但是运行过程中,你不知道调用了多少次,所以使用时还是不需要再调用一次 (random t) 来产生新的种子。\n位运算这样高级的操作我就不说了,自己看 info elisp - bitwise operations on integers 吧。\n函数列表 ;; 测试函数 (integerp object) (floatp object) (numberp object) (zerop number) (wholenump object) ;; 比较函数 (\u0026gt; num1 num2) (\u0026lt; num1 num2) (\u0026gt;= num1 num2) (\u0026lt;= num1 num2) (= num1 num2) (eql obj1 obj2) (/= num1 num2) ;; 转换函数 (float arg) (truncate arg \u0026amp;optional divisor) (floor arg \u0026amp;optional divisor) (ceiling arg \u0026amp;optional divisor) (round arg \u0026amp;optional divisor) ;; 运算 (+ \u0026amp;rest numbers-or-markers) (- \u0026amp;optional number-or-marker \u0026amp;rest more-numbers-or-markers) (* \u0026amp;rest numbers-or-markers) (/ dividend divisor \u0026amp;rest divisors) (1+ number) (1- number) (abs arg) (% x y) (mod x y) (sin arg) (cos arg) (tan arg) (asin arg) (acos arg) (atan y \u0026amp;optional x) (sqrt arg) (exp arg) (expt arg1 arg2) (log arg \u0026amp;optional base) (log10 arg) (logb arg) ;; 随机数 (random \u0026amp;optional n) 变量列表 most-positive-fixnum\rmost-negative-fixnum 基本数据类型之二 \u0026ndash; 字符和字符串 在 emacs 里字符串是有序的字符数组。和 c 语言的字符串数组不同,emacs 的字符串可以容纳任何字符,包括 \\0:\n(setq foo \u0026#34;abc\\000abc\u0026#34;) ; =\u0026gt; \u0026#34;abc^@abc\u0026#34; 关于字符串有很多高级的属性,例如字符串的表示有单字节和多字节类型,字符串可以有文本属性(text property)等等。但是对于刚接触字符串,还是先学一些基本操作吧。\n首先 构成字符串的字符其实就是一个整数 。一个字符 \u0026lsquo;a\u0026rsquo; 就是一个整数 65。但是目前字符串中的字符被限制在 0-524287 之间。字符的读入语法是在字符前加上一个问号,比如 ?a 代表字符 \u0026lsquo;a\u0026rsquo;。\n?a ; =\u0026gt; 65 ?a ; =\u0026gt; 97 对于标点来说,也可以用同样的语法,但是最好在前面加上转义字符 \\ ,因为有些标点会有岐义,比如 ?\\(。 \\ 必须用 ?\\ 表示。控制字符,退格、制表符,换行符,垂直制表符,换页符,空格,回车,删除和 escape 表示为 ?\\a, ?\\b, ?\\t, ?\\n, ?\\v, ?\\f, ?\\s, ?\\r, ?\\d, 和 ?\\e。对于没有特殊意义的字符,加上转义字符 \\ 是没有副作用的,比如 ?\\+ 和 ?+ 是完全一样的。所以标点还是都用转义字符来表示吧。\n?\\a =\u0026gt; 7 ; control-g, `c-g\u0026#39; ?\\b =\u0026gt; 8 ; backspace, \u0026lt;bs\u0026gt;, `c-h\u0026#39; ?\\t =\u0026gt; 9 ; tab, \u0026lt;tab\u0026gt;, `c-i\u0026#39; ?\\n =\u0026gt; 10 ; newline, `c-j\u0026#39; ?\\v =\u0026gt; 11 ; vertical tab, `c-k\u0026#39; ?\\f =\u0026gt; 12 ; formfeed character, `c-l\u0026#39; ?\\r =\u0026gt; 13 ; carriage return, \u0026lt;ret\u0026gt;, `c-m\u0026#39; ?\\e =\u0026gt; 27 ; escape character, \u0026lt;esc\u0026gt;, `c-[\u0026#39; ?\\s =\u0026gt; 32 ; space character, \u0026lt;spc\u0026gt; ?\\\\ =\u0026gt; 92 ; backslash character, `\\\u0026#39; ?\\d =\u0026gt; 127 ; delete character, \u0026lt;del\u0026gt; 控制字符可以有多种表示方式,比如 c-i,这些都是对的:\n?\\^i ?\\^i ?\\c-i ?\\c-i 它们都对应数字 9。\nmeta 字符是用 修饰键(通常就是 alt 键)输入的字符。之所以称为修饰键,是因为这样输入的字符就是在其修饰字符的第 27 位由 0 变成 1 而成,也就是如下操作:\n(logior (lsh 1 27) ?a) ; =\u0026gt; 134217793 ?\\m-a ; =\u0026gt; 134217793 你可以用 \\m- 代表 meta 键,加上修饰的字符就是新生成的字符。比如:?\\m-a, ?\\m-\\c-b. 后面这个也可以写成 ?\\c-\\m-b。\n如果你还记得前面说过字符串里的字符不能超过 524287 的话,这就可以看出字符串是不能放下一个 meta 字符的。所以按键序列在这时只能用 vector 来储存。\n其它的修饰键也是类似的。emacs 用 2**25 位来表示 shift 键,2**24 对应 hyper,2**23 对应 super,2**22 对应 alt。\n测试函数 字符串测试使用 stringp ,没有 charp,因为字符就是整数。 string-or-null-p 当对象是一个字符或 nil 时返回 t。 char-or-string-p 测试是否是字符串或者字符类型。比较头疼的是 emacs 没有测试字符串是否为空的函数。这是我用的这个测试函数,使用前要测试字符串是否为 nil:\n(defun string-emptyp (str) (not (string\u0026lt; \u0026#34;\u0026#34; str))) 构造函数 产生一个字符串可以用 make-string 。这样生成的字符串包含的字符都是一样的。要生成不同的字符串可以用 string 函数。\n(make-string 5 ?x) ; =\u0026gt; \u0026#34;xxxxx\u0026#34; (string ?a ?b ?c) ; =\u0026gt; \u0026#34;abc\u0026#34; 在已有的字符串生成新的字符串的方法有 substring, concat 。 substring 的后两个参数是起点和终点的位置。如果终点越界或者终点比起点小都会产生一个错误。这个在使用 substring 时要特别小心。\n(substring \u0026#34;0123456789\u0026#34; 3) ; =\u0026gt; \u0026#34;3456789\u0026#34; (substring \u0026#34;0123456789\u0026#34; 3 5) ; =\u0026gt; \u0026#34;34\u0026#34; (substring \u0026#34;0123456789\u0026#34; -3 -1) ; =\u0026gt; \u0026#34;78\u0026#34; concat 函数相对简单,就是把几个字符串连接起来。\n字符串比较 char-equal 可以比较两个字符是否相等。与整数比较不同,这个函数还考虑了大小写。如果 case-fold-search 变量是 t 时,这个函数的字符比较是忽略大小写的。编程时要小心,因为通常 case-fold-search 都是 t,这样如果要考虑字符的大小写时就不能用 char-equal 函数了。\n字符串比较使用 string= ,string-equal 是一个别名。\nstring\u0026lt; 是按字典序比较两个字符串, string-less 是它的别名。空字符串小于所有字符串,除了空字符串。前面 string-emptyp 就是用这个特性。当然直接用 length 检测字符串长度应该也可以,还可以省去检测字符串是否为空。没有 string\u0026gt; 函数。\n转换函数 字符转换成字符串可以用 char-to-string 函数,字符串转换成字符可以用 string-to-char ,当然只是返回字符串的第一个字符。\n数字和字符串之间的转换可以用 number-to-string 和 string-to-number 。其中 string-to-number 可以设置字符串的进制,可以从 2 到 16。 number-to-string 只能转换成 10 进制的数字。如果要输出八进制或者十六进制,可以用 format 函数:\n(string-to-number \u0026#34;256\u0026#34;) ; =\u0026gt; 256 (number-to-string 256) ; =\u0026gt; \u0026#34;256\u0026#34; (format \u0026#34;%#o\u0026#34; 256) ; =\u0026gt; \u0026#34;0400\u0026#34; (format \u0026#34;%#x\u0026#34; 256) ; =\u0026gt; \u0026#34;0x100\u0026#34; 如果要输出成二进制,好像没有现成的函数了。calculator 库倒是可以,这是我写的函数:\n(defun number-to-bin-string (number) (require \u0026#39;calculator) (let ((calculator-output-radix \u0026#39;bin) (calculator-radix-grouping-mode nil)) (calculator-number-to-string number))) (number-to-bin-string 256) ; =\u0026gt; \u0026#34;100000000\u0026#34; 其它数据类型现在还没有学到,不过可以先了解一下吧。 concat 可以把一个字符构成的列表或者向量转换成字符串, vconcat 可以把一个字符串转换成一个向量, append 可以把一个字符串转换成一个列表。\n(concat \u0026#39;(?a ?b ?c ?d ?e)) ; =\u0026gt; \u0026#34;abcde\u0026#34; (concat [?a ?b ?c ?d ?e]) ; =\u0026gt; \u0026#34;abcde\u0026#34; (vconcat \u0026#34;abdef\u0026#34;) ; =\u0026gt; [97 98 100 101 102] (append \u0026#34;abcdef\u0026#34; nil) ; =\u0026gt; (97 98 99 100 101 102) 大小写转换使用的是 downcase 和 upcase 两个函数。这两个函数的参数既可以字符串,也可以是字符。capitalize 可以使字符串中单词的第一个字符大写,其它字符小写。 upcase-initials 只使第一个单词的第一个字符大写,其它字符小写。 这两个函数的参数如果是一个字符,那么只让这个字符大写。比如:\n(downcase \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (downcase ?x) ; =\u0026gt; 120 (upcase \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (upcase ?x) ; =\u0026gt; 88 (capitalize \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (upcase-initials \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; = 💡 这里 upcase-initials 的作用应该是使单词的第一个字符大写,其它字符大小写保持不变。\n格式化字符串 format 类似于 c 语言里的 printf 可以实现对象的字符串化。数字的格式化和 printf 的参数差不多,值得一提的是 \u0026quot;%s\u0026quot; 这个格式化形式,它可以把对象的输出形式转换成字符串,这在调试时是很有用的。\n查找和替换 字符串查找的核心函数是 string-match 。这个函数可以 从指定的位置对字符串进行正则表达式匹配 ,如果匹配成功,则返回匹配的起点,如:\n(string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34;) ; =\u0026gt; 3 (string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34; 10) ; =\u0026gt; 13 注意 string-match 的参数是一个 regexp。 emacs 好象没有内建的查找子串的函数。如果你想把 string-match 作为一个查找子串的函数,可以先用 regexp-quote 函数先处理一下子串。比如:\n(string-match \u0026#34;2*\u0026#34; \u0026#34;232*3=696\u0026#34;) ; =\u0026gt; 0 (string-match (regexp-quote \u0026#34;2*\u0026#34;) \u0026#34;232*3=696\u0026#34;) ; =\u0026gt; 2 事实上, string-match 不只是查找字符串,它更重要的功能是捕捉匹配的字符串。如果你对正则表达式不了解,可能需要先找一本书,先了解一下什么是正则表达式。 string-match 在查找的同时,还会记录下每个要捕捉的字符串的位置。这个位置可以在匹配后用 match-data、 match-beginning 和 match-end 等函数来获得。先看一下例子:\n(progn (string-match \u0026#34;3\\\\(4\\\\)\u0026#34; \u0026#34;01234567890123456789\u0026#34;) (match-data)) ; =\u0026gt; (3 5 4 5) 最后返回这个数字是什么意思呢?正则表达式捕捉的字符串按括号的顺序对应一个序号,整个模式对应序号 0,第一个括号对应序号 1,第二个括号对应序号 2,以此类推。所以 \u0026ldquo;3(4)\u0026rdquo; 这个正则表达式中有序号 0 和 1,最后 match-data 返回的一系列数字对应的分别是要捕捉字符串的起点和终点位置,也就是说子串 \u0026ldquo;34\u0026rdquo; 起点从位置 3 开始,到位置 5 结束,而捕捉的字符串 \u0026ldquo;4\u0026rdquo; 的起点是从 4 开始,到 5 结束。这些位置可以用 match-beginning 和 match-end 函数用对应的序号得到。要注意的是,起点位置是捕捉字符串的第一个字符的位置,而终点位置不是捕捉的字符串最后一个字符的位置,而是下一个字符的位置。这个性质对于循环是很方便的。比如要查找上面这个字符串中所有 34 出现的位置:\n(let ((start 0)) (while (string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34; start) (princ (format \u0026#34;find at %d\\n\u0026#34; (match-beginning 0))) (setq start (match-end 0)))) 查找会了,就要学习替换了。替换使用的函数是 replace-match 。这个函数既可以用于字符串的替换,也可以用于缓冲区的文本替换。对于字符串的替换, replace-match 只是按给定的序号把字符串中的那一部分用提供的字符串替换了而已:\n(let ((str \u0026#34;01234567890123456789\u0026#34;)) (string-match \u0026#34;34\u0026#34; str) (princ (replace-match \u0026#34;x\u0026#34; nil nil str 0)) (princ \u0026#34;\\n\u0026#34;) (princ str)) 可以看出 replace-match 返回的字符串是替换后的新字符串,原字符串被没有改变。\n如果你想挑战一下,想想怎样把上面这个字符串中所有的 34 都替换掉?如果想就使用同一个字符串来存储,可能对于固定的字符串,这个还容易一些,如果不是的话,就要花一些脑筋了,因为替换之后,新的字符串下一个搜索起点的位置就不能用 (match-end 0) 给出来的位置了,而是要扣除替换的字符串和被替换的字符串长度的差值。\nemacs 对字符串的替换有一个函数 replace-regexp-in-string 。这个函数的实现方法是把每次匹配部分之前的子串收集起来,最后再把所有字符串连接起来。\n单字符的替换有 subst-char-in-string 函数。但是 emacs 没有类似 perl 函数或者程序 tr 那样进行字符替换的函数。只能自己建表进行循环操作了。\n函数列表 ;; 测试函数 (stringp object) (string-or-null-p object) (char-or-string-p object) ;; 构建函数 (make-string length init) (string \u0026amp;rest characters) (substring string from \u0026amp;optional to) (concat \u0026amp;rest sequences) ;; 比较函数 (char-equal c1 c2) (string= s1 s2) (string-equal s1 s2) (string\u0026lt; s1 s2) ;; 转换函数 (char-to-string char) (string-to-char string) (number-to-string number) (string-to-number string \u0026amp;optional base) (downcase obj) (upcase obj) (capitalize obj) (upcase-initials obj) (format string \u0026amp;rest objects) ;; 查找与替换 (string-match regexp string \u0026amp;optional start) (replace-match newtext \u0026amp;optional fixedcase literal string subexp) (replace-regexp-in-string regexp rep string \u0026amp;optional fixedcase literal subexp start) (subst-char-in-string fromchar tochar string \u0026amp;optional inplace) 基本数据类型之三 \u0026ndash; cons cell 和列表 如果从概念上来说,cons cell 其实非常简单的,就是两个有顺序的元素。第一个叫 car,第二个就 cdr。car 和 cdr 名字来自于 lisp。它最初在 ibm 704 机器上的实现。在这种机器有一种取址模式,使人可以访问一个存储地址中的“地址(address)”部分和“减量(decrement)”部分。car 指令用于取出地址部分,表示 (contents of address part of register),cdr 指令用于取出地址的减量部分 (contents of the decrement part of register)。cons cell 也就是 construction of cells。car 函数用于取得 cons cell 的 car 部分,cdr 取得 cons cell 的 cdr 部分。cons cell 如此简单,但是它却能衍生出许多高级的数据结构,比如链表,树,关联表等等。\ncons cell 的读入语法是用 . 分开两个部分,比如:\n\u0026#39;(1 . 2) ; =\u0026gt; (1 . 2) \u0026#39;(?a . 1) ; =\u0026gt; (97 . 1) \u0026#39;(1 . \u0026#34;a\u0026#34;) ; =\u0026gt; (1 . \u0026#34;a\u0026#34;) \u0026#39;(1 . nil) ; =\u0026gt; (1) \u0026#39;(nil . nil) ; =\u0026gt; (nil) 注意到前面的表达式中都有一个 ' 号,这是什么意思呢?其实理解了 eval-last-sexp 的作用就能明白了。 eval-last-sexp 其实包含了两个步骤, 一是读入前一个 s-表达式,二是对读入的 s-表达式求值 。这样如果读入的 s-表达式是一个 cons cell 的话,求值时会把这个 cons cell 的第一个元素作为一个函数来调用。而事实上,前面这些例子的第一个元素都不是一个函数,这样就会产生一个错误 invalid-function。之所以前面没有遇到这个问题,那是因为前面数字和字符串是一类特殊的 s-表达式,它们求值后和求值前是不变,称为 自求值表达式 (self-evaluating form)。 ' 号其实是一个特殊的函数 quote ,它的作用是 将它的参数返回而不作求值 。 '(1 . 2) 等价于 (quote (1 . 2))。为了证明 cons cell 的读入语法确实就是它的输出形式,可以看下面这个语句:\n(read \u0026#34;(1 . 2)\u0026#34;) ; =\u0026gt; (1 . 2) 列表包括了 cons cell。但是列表中有一个特殊的元素 - 空表 nil 。\nnil ; =\u0026gt; nil \u0026#39;() ; =\u0026gt; nil 空表不是一个 cons cell,因为它没有 car 和 cdr 两个部分,事实上空表里没有任何内容。但是为了编程的方便,可以认为 nil 的 car 和 cdr 都是 nil:\n(car nil) ; =\u0026gt; nil (cdr nil) ; =\u0026gt; nil 按列表最后一个 cons cell 的 cdr 部分的类型分,可以把列表分为三类。如果它是 nil 的话,这个列表也称为“真列表”(true list)。如果既不是 nil 也不是一个 cons cell,则这个列表称为“点列表”(dotted list)。还有一种可能,它指向列表中之前的一个 cons cell,则称为环形列表 (circular list)。这里分别给出一个例子:\n\u0026#39;(1 2 3) ; =\u0026gt; (1 2 3) \u0026#39;(1 2 . 3) ; =\u0026gt; (1 2 . 3) \u0026#39;(1 . #1=(2 3 . #1#)) ; =\u0026gt; (1 2 3 . #1) 从这个例子可以看出前两种列表的读入语法和输出形式都是相同的,而环形列表的读入语法是很古怪的,输出形式不能作为环形列表的读入形式。\n如果把真列表最后一个 cons cell 的 nil 省略不写,也就是 (1 . nil) 简写成 (1) ,把 ( obj1 . ( obj2 . list)) 简写成 (obj1 obj2 . list) ,那么列表最后可以写成一个用括号括起的元素列表:\n\u0026#39;(1 . (2 . (3 . nil))) ; =\u0026gt; (1 2 3) 尽管这样写是清爽多了,但是,我觉得看一个列表时还是在脑子里反映的前面的形式,这样在和复杂的 cons cell 打交道时就不会搞不清楚这个 cons cell 的 cdr 是一个列表呢,还是一个元素或者是嵌套的列表。\n测试函数 测试一个对象是否是 cons cell 用 consp ,是否是列表用 listp 。\n(consp \u0026#39;(1 . 2)) ; =\u0026gt; t (consp \u0026#39;(1 . (2 . nil))) ; =\u0026gt; t (consp nil) ; =\u0026gt; nil (listp \u0026#39;(1 . 2)) ; =\u0026gt; t (listp \u0026#39;(1 . (2 . nil))) ; =\u0026gt; t (listp nil) ; =\u0026gt; t 没有内建的方法测试一个列表是不是一个真列表。通常如果一个函数需要一个真列表作为参数,都是在运行时发出错误,而不是进行参数检查,因为检查一个列表是真列表的代价比较高。\n测试一个对象是否是 nil 用 null 函数。只有当对象是空表时,null 才返回空值。\n构造函数 生成一个 cons cell 可以用 cons 函数。比如:\n(cons 1 2) ; =\u0026gt; (1 . 2) (cons 1 \u0026#39;()) ; =\u0026gt; (1) 也是在列表前面增加元素的方法。比如:\n(setq foo \u0026#39;(a b)) ; =\u0026gt; (a b) (cons \u0026#39;x foo) ; =\u0026gt; (x a b) 值得注意的是前面这个例子的 foo 值并没有改变。事实上有一个宏 push 可以加入元素的同时改变列表的值:\n(push \u0026#39;x foo) ; =\u0026gt; (x a b) foo ; =\u0026gt; (x a b) 生成一个列表的函数是 list 。比如:\n(list 1 2 3) ; =\u0026gt; (1 2 3) 可能这时你有一个疑惑,前面产生一个列表,我常用 quote (也就是 ' 符号)这个函数,它和这个 cons 和 list 函数有什么区别呢?其实区别是很明显的,quote 是把参数直接返回不进行求值,而 list 和 cons 是对参数求值后再生成一个列表或者 cons cell。看下面这个例子:\n\u0026#39;((+ 1 2) 3) ; =\u0026gt; ((+ 1 2) 3) (list (+ 1 2) 3) ; =\u0026gt; (3 3) 前一个生成的列表的 car 部分是 (+ 1 2) 这个列表,而后一个是先对 (+ 1 2) 求值得到 3 后再生成列表。\n思考题\n如果你觉得你有点明白的话,我提一个问题考考你:怎样用 list 函数构造一个 (a b c) 这样的列表呢?\n前面提到在列表前端增加元素的方法是用 cons ,在列表后端增加元素的函数是用 append 。比如:\n(append \u0026#39;(a b) \u0026#39;(c)) ; =\u0026gt; (a b c) append 的功能可以认为它是把第一个参数最后一个列表的 nil 换成第二个参数,比如前面这个例子,第一个参数写成 cons cell 表示方式是 (a . (b . nil)) ,把这个 nil 替换成 (c) 就成了 (a . (b . (c))) 。对于多个参数的情况也是一样的,依次把下一个参数替换新列表最后一个 nil 就是最后的结果了。\n(append \u0026#39;(a b) \u0026#39;(c) \u0026#39;(d)) ; =\u0026gt; (a b c d) 一般来说 append 的参数都要是列表,但是最后一个参数可以不是一个列表,这也不违背前面说的,因为 cons cell 的 cdr 部分本来就可以是任何对象:\n(append \u0026#39;(a b) \u0026#39;c) ; =\u0026gt; (a b . c) 这样得到的结果就不再是一个真列表了,如果再进行 append 操作就会产生一个错误。\n如果你写过 c 的链表类型,可能就知道如果链表只保留一个指针,那么链表只能在一端增加元素。elisp 的列表类型也是类似的,用 cons 在列表前增加元素比用 append 要快得多。\nappend 的参数不限于列表,还可以是字符串或者向量。前面字符串里已经提到可以把一个字符串转换成一个字符列表,同样可能把向量转换成一个列表:\n(append [a b] \u0026#34;cd\u0026#34; nil) ; =\u0026gt; (a b 99 100) 注意前面最后一个参数 nil 是必要的,不然你可以想象得到的结果是什么 \u0026ndash; (a b . \u0026quot;cd\u0026quot;)。\n把列表当数组用 要得到列表或者 cons cell 里元素,唯一的方法是用 car 和 cdr 函数。很容易明白,car 就是取得 cons cell 的 car 部分,cdr 函数就是取得 cons cell 的 cdr 部分。通过这两个函数,我们就能访问 cons cell 和列表中的任何元素。\n思考题\n你如果知道 elisp 的函数如果定义,并知道 if 的使用方法,不妨自己写一个函数来取得一个列表的第 n 个 cdr。\n通过使用 elisp 提供的函数,我们事实上是可以把列表当数组来用。依惯例,我们用 car 来访问列表的第一个元素,cadr 来访问第二个元素,再往后就没有这样的函数了,可以用 nth 函数来访问:\n(nth 3 \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; 3 获得列表一个区间的函数有 nthcdr、last 和 butlast。nthcdr 和 last 比较类似,它们都是返回列表后端的列表。nthcdr 函数返回第 n 个元素后的列表:\n(nthcdr 2 \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; (2 3 4 5) last 函数返回倒数 n 个长度的列表:\n(last \u0026#39;(0 1 2 3 4 5) 2) ; =\u0026gt; (4 5) butlast 和前两个函数不同,返回的除了倒数 n 个元素的列表。\n(butlast \u0026#39;(0 1 2 3 4 5) 2) ; =\u0026gt; (0 1 2 3) 思考题\n如何得到某个区间(比如从 3 到 5 之间)的列表(提示列表长度可以用 length 函数得到):\n(my-subseq \u0026#39;(0 1 2 3 4 5) 2 5) ; =\u0026gt; (2 3 4) 使用前面这几个函数访问列表是没有问题了。但是你也可以想象,链表这种数据结构是不适合随机访问的,代价比较高,如果你的代码中频繁使用这样的函数或者对一个很长的列表使用这样的函数,就应该考虑是不是应该用数组来实现。\n直到现在为止,我们用到的函数都不会修改一个已有的变量。这是函数式编程的一个特点。只用这些函数编写的代码是很容易调试的,因为你不用去考虑一个变量在执行一个代码后就改变了,不用考虑变量的引用情况等等。下面就要结束这样轻松的学习了。\n首先学习怎样修改一个 cons cell 的内容。首先 setcar 和 setcdr 可以修改一个 cons cell 的 car 部分和 cdr 部分。比如:\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (setcar foo \u0026#39;x) ; =\u0026gt; x foo ; =\u0026gt; (x b c) (setcdr foo \u0026#39;(y z)) ; =\u0026gt; (y z) foo ; =\u0026gt; (x y z) 思考题\n好像很简单是吧。我出一个比较 bt 的一个问题,下面代码运行后 foo 是什么东西呢?\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (setcdr foo foo) 现在来考虑一下,怎样像数组那样直接修改列表。使用 setcar 和 nthcdr 的组合就可以实现了:\n(setq foo \u0026#39;(1 2 3)) ; =\u0026gt; (1 2 3) (setcar foo \u0026#39;a) ; =\u0026gt; a (setcar (cdr foo) \u0026#39;b) ; =\u0026gt; b (setcar (nthcdr 2 foo) \u0026#39;c) ; =\u0026gt; c foo ; =\u0026gt; (a b c) 把列表当堆栈用 前面已经提到过可以用 push 向列表头端增加元素,在结合 pop 函数,列表就可以做为一个堆栈了。\n(setq foo nil) ; =\u0026gt; nil (push \u0026#39;a foo) ; =\u0026gt; (a) (push \u0026#39;b foo) ; =\u0026gt; (b a) (pop foo) ; =\u0026gt; b foo ; =\u0026gt; (a) 重排列表 如果一直用 push 往列表里添加元素有一个问题是这样得到的列表和加入的顺序是相反的。通常我们需要得到一个反向的列表。reverse 函数可以做到这一点:\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (reverse foo) ; =\u0026gt; (c b a) 需要注意的是使用 reverse 后 foo 值并没有改变。不要怪我太啰唆,如果你看到一个函数 nreverse,而且确实它能返回逆序的列表,不明所以就到处乱用,迟早会写出一个错误的函数。这个 nreverse 和前面的 reverse 差别就在于它是一个有破坏性的函数,也就是说它会修改它的参数。\n(nreverse foo) ; =\u0026gt; (c b a) foo ; =\u0026gt; (a) 为什么现在 foo 指向的是列表的末端呢?如果你实现过链表就知道,逆序操作是可以在原链表上进行的,这样原来头部指针会变成链表的尾端。列表也是(应该是,我也没有看过实现)这个原理。使用 nreverse 的唯一的好处是速度快,省资源。所以如果你只是想得到逆序后的列表就放心用 nreverse,否则还是用 reverse 的好。\nelisp 还有一些是具有破坏性的函数。最常用的就是 sort 函数:\n(setq foo \u0026#39;(3 2 4 1 5)) ; =\u0026gt; (3 2 4 1 5) (sort foo \u0026#39;\u0026lt;) ; =\u0026gt; (1 2 3 4 5) foo ; =\u0026gt; (3 4 5) 这一点请一定要记住,我就曾经在 sort 函数上犯了好几次错误。那如果我既要保留原列表,又要进行 sort 操作怎么办呢?可以用 copy-sequence 函数。这个函数只对列表进行复制,返回的列表的元素还是原列表里的元素,不会拷贝列表的元素。\nnconc 和 append 功能相似,但是它会修改除最后一个参数以外的所有的参数,nbutlast 和 butlast 功能相似,也会修改参数。这些函数都是在效率优先时才使用。总而言之,以 n 开头的函数都要慎用。\n把列表当集合用 列表可以作为无序的集合。合并集合用 append 函数。去除重复的 equal 元素用 delete-dups。查找一个元素是否在列表中,如果测试函数是用 eq,就用 memq,如果测试用 equal,可以用 member。删除列表中的指定的元素,测试函数为 eq 对应 delq 函数,equal 对应 delete。还有两个函数 remq 和 remove 也是删除指定元素。它们的差别是 delq 和 delete 可能会修改参数,而 remq 和 remove 总是返回删除后列表的拷贝。注意前面这是说的是可能会修改参数的值,也就是说可能不会,所以保险起见,用 delq 和 delete 函数要么只用返回值,要么用 setq 设置参数的值为返回值。\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (remq \u0026#39;b foo) ; =\u0026gt; (a c) foo ; =\u0026gt; (a b c) (delq \u0026#39;b foo) ; =\u0026gt; (a c) foo ; =\u0026gt; (a c) (delq \u0026#39;a foo) ; =\u0026gt; (c) foo ; =\u0026gt; (a c) 把列表当关联表 用在 elisp 编程中,列表最常用的形式应该是作为一个关联表了。所谓关联表,就是可以用一个字符串(通常叫关键字,key)来查找对应值的数据结构。由列表实现的关联表有一个专门的名字叫 association list。尽管 elisp 里也有 hash table,但是 hash table 相比于 association list 至少这样几个缺点:\nhash table 里的关键字(key)是无序的,而 association list 的关键字 可以按想要的顺序排列 hash table 没有列表那样丰富的函数,只有一个 maphash 函数可以遍历列 表。而 association list 就是一个列表,所有列表函数都能适用 hash table 没有读入语法和输入形式,这对于调试和使用都带来很多不便 所以 elisp 的 hash table 不是一个首要的数据结构,只要不对效率要求很高,通常直接用 association list。数组可以作为关联表,但是数组不适合作为与人交互使用数据结构(毕竟一个有意义的名字比纯数字的下标更适合人脑)。所以关联表的地位在 elisp 中就非比寻常了,emacs 为关联表专门用 c 程序实现了查找的相关函数以提高程序的效率。在 association list 中关键字是放在元素的 car 部分,与它对应的数据放在这个元素的 cdr 部分。根据比较方法的不同,有 assq 和 assoc 两个函数,它们分别对应查找使用 eq 和 equal 两种方法。例如:\n(assoc \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (\u0026#34;a\u0026#34; 97) (assq \u0026#39;a \u0026#39;((a . 97) (b . 98))) ; =\u0026gt; (a . 97) 通常我们只需要查找对应的数据,所以一般来说都要用 cdr 来得到对应的数据:\n(cdr (assoc \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98)))) ; =\u0026gt; (97) (cdr (assq \u0026#39;a \u0026#39;((a . 97) (b . 98)))) ; =\u0026gt; 97 assoc-default 可以一步完成这样的操作:\n(assoc-default \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (97) 如果查找用的键值(key)对应的数据也可以作为一个键值的话,还可以用 rassoc 和 rassq 来根据数据查找键值:\n(rassoc \u0026#39;(97) \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (\u0026#34;a\u0026#34; 97) (rassq \u0026#39;97 \u0026#39;((a . 97) (b . 98))) ; =\u0026gt; (a . 97) 如果要修改关键字对应的值,最省事的作法就是用 cons 把新的键值对加到列表的头端。但是这会让列表越来越长,浪费空间。如果要替换已经存在的值,一个想法就是用 setcdr 来更改键值对应的数据。但是在更改之前要先确定这个键值在对应的列表里,否则会产生一个错误。另一个想法是用 assoc 查找到对应的元素,再用 delq 删除这个数据,然后用 cons 加到列表里:\n(setq foo \u0026#39;((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98))) ; =\u0026gt; ((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98)) ;; update value by setcdr (if (setq bar (assoc \u0026#34;a\u0026#34; foo)) (setcdr bar \u0026#34;this is a\u0026#34;) (setq foo (cons \u0026#39;(\u0026#34;a\u0026#34; . \u0026#34;this is a\u0026#34;) foo))) ; =\u0026gt; \u0026#34;this is a\u0026#34; foo ; =\u0026gt; ((\u0026#34;a\u0026#34; . \u0026#34;this is a\u0026#34;) (\u0026#34;b\u0026#34; . 98)) ;; update value by delq and cons (setq foo (cons \u0026#39;(\u0026#34;a\u0026#34; . 97) (delq (assoc \u0026#34;a\u0026#34; foo) foo))) ; =\u0026gt; ((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98)) 如果不对顺序有要求的话,推荐用后一种方法吧。这样代码简洁,而且让最近更新的元素放到列表前端,查找更快。\n把列表当树用 列表的第一个元素如果作为结点的数据,其它元素看作是子节点,就是一个树了。由于树的操作都涉及递归,现在还没有说到函数,我就不介绍了。(其实是我不太熟,就不班门弄斧了)。\n遍历列表 遍历列表最常用的函数就是 mapc 和 mapcar 了。它们的第一个参数都是一个函数,这个函数只接受一个参数,每次处理一个列表里的元素。这两个函数唯一的差别是前者返回的还是输入的列表,而 mapcar 返回的函数返回值构成的列表:\n(mapc \u0026#39;1+ \u0026#39;(1 2 3)) ; =\u0026gt; (1 2 3) (mapcar \u0026#39;1+ \u0026#39;(1 2 3)) ; =\u0026gt; (2 3 4) 另一个比较常用的遍历列表的方法是用 dolist。它的形式是:\n(dolist (var list [result]) body...) 其中 var 是一个临时变量,在 body 里可以用来得到列表中元素的值。使用 dolist 的好处是不用写 lambda 函数。一般情况下它的返回值是 nil,但是你也可以指定一个值作为返回值(我觉得这个特性没有什么用,只省了一步而已):\n(dolist (foo \u0026#39;(1 2 3)) (incf foo)) ; =\u0026gt; nil (setq bar nil) (dolist (foo \u0026#39;(1 2 3) bar) (push (incf foo) bar)) ; =\u0026gt; (4 3 2) 其它常用函数 如果看过一些函数式语言教程的话,一定对 fold(或叫 accumulate、reduce)和 filter 这些函数记忆深刻。不过 elisp 里好像没有提供这样的函数。remove-if 和 remove-if-not 可以作 filter 函数,但是它们是 cl 里的,自己用用没有关系,不能强迫别人也跟着用,所以不能写到 elisp 里。如果不用这两个函数,也不用别人的函数的话,自己实现不妨用这样的方法:\n(defun my-remove-if (predicate list) (delq nil (mapcar (lambda (n) (and (not (funcall predicate n)) n)) list))) (defun evenp (n) (= (% n 2) 0)) (my-remove-if \u0026#39;evenp \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; (1 3 5) fold 的操作只能用变量加循环或 mapc 操作来代替了:\n(defun my-fold-left (op initial list) (dolist (var list initial) (setq initial (funcall op initial var)))) (my-fold-left \u0026#39;+ 0 \u0026#39;(1 2 3 4)) ; =\u0026gt; 10 这里只是举个例子,事实上你不必写这样的函数,直接用函数里的遍历操作更好一些。\n产生数列常用的方法是用 number-sequence(这里不禁用说一次,不要再用 loop 产生 tab-stop-list 了,你们 too old 了)。不过这个函数好像 在 emacs21 时好像还没有。\n解析文本时一个很常用的操作是把字符串按分隔符分解,可以用 split-string 函数:\n(split-string \u0026#34;key = val\u0026#34; \u0026#34;\\\\s-*=\\\\s-*\u0026#34;) ; =\u0026gt; (\u0026#34;key\u0026#34; \u0026#34;val\u0026#34;) 与 split-string 对应是把几个字符串用一个分隔符连接起来,这可以用 mapconcat 完成。比如:\n(mapconcat \u0026#39;identity \u0026#39;(\u0026#34;a\u0026#34; \u0026#34;b\u0026#34; \u0026#34;c\u0026#34;) \u0026#34;\\t\u0026#34;) ; =\u0026gt; \u0026#34;a b c\u0026#34; identity 是一个特殊的函数,它会直接返回参数。mapconcat 第一个参数是一个函数,可以很灵活的使用。\n函数列表 ;; 列表测试 (consp object) (listp object) (null object) ;; 列表构造 (cons car cdr) (list \u0026amp;rest objects) (append \u0026amp;rest sequences) ;; 访问列表元素 (car list) (cdr list) (cadr x) (caar x) (cddr x) (cdar x) (nth n list) (nthcdr n list) (last list \u0026amp;optional n) (butlast list \u0026amp;optional n) ;; 修改 cons cell (setcar cell newcar) (setcdr cell newcdr) ;; 列表操作 (push newelt listname) (pop listname) (reverse list) (nreverse list) (sort list predicate) (copy-sequence arg) (nconc \u0026amp;rest lists) (nbutlast list \u0026amp;optional n) ;; 集合函数 (delete-dups list) (memq elt list) (member elt list) (delq elt list) (delete elt seq) (remq elt list) (remove elt seq) ;; 关联列表 (assoc key list) (assq key list) (assoc-default key alist \u0026amp;optional test default) (rassoc key list) (rassq key list) ;; 遍历函数 (mapc function sequence) (mapcar function sequence) (dolist (var list [result]) body...) ;; 其它 (number-sequence from \u0026amp;optional to inc) (split-string string \u0026amp;optional separators omit-nulls) (mapconcat function sequence separator) (identity arg) 问题解答 用 list 生成 (a b c) 答案是 (list 'a 'b 'c)。很简单的一个问题。从这个例子可以看出为什么要想出 用 \u0026rsquo; 来输入列表。这就是程序员“懒”的美德呀!\nnthcdr 的一个实现 (defun my-nthcdr (n list) (if (or (null list) (= n 0)) (car list) (my-nthcdr (1- n) (cdr list)))) 这样的实现看上去很简洁,但是一个最大的问题的 elisp 的递归是有限的,所以如果想这个函数没有问题,还是用循环还实现比较好。\nmy-subseq 函数的定义 (defun my-subseq (list from \u0026amp;optional to) (if (null to) (nthcdr from list) (butlast (nthcdr from list) (- (length list) to)))) (setcdr foo foo) 是什么怪东西? 可能你已经想到了,这就是传说中的环呀。这在 info elisp - circular objects 里有介绍。elisp 里用到这样的环状列表并不多见,但是也不是没有,org 和 session 那个 bug 就是由于一个环状列表造成的。\n基本数据类型之四 \u0026ndash; 数组和序列 序列是列表和数组的统称,也就是说列表和数组都是序列。它们的共性是内部的元素都是有序的。elisp 里的数组包括字符串、向量、char-table 和布尔向量。它们的关系可以用下面图表示:\n_____________________________________________\r| |\r| sequence |\r| ______ ________________________________ |\r| | | | | |\r| | list | | array | |\r| | | | ________ ________ | |\r| |______| | | | | | | |\r| | | vector | | string | | |\r| | |________| |________| | |\r| | ____________ _____________ | |\r| | | | | | | |\r| | | char-table | | bool-vector | | |\r| | |____________| |_____________| | |\r| |________________________________| |\r|_____________________________________________| 组有这样一些特性:\n数组内的元素都对应一个下标,第一个元素下标为 0,接下来是 1。数组内 的元素可以在常数时间内访问。 数组在创建之后就无法改变它的长度。 数组是自求值的。 数组里的元素都可以用 aref 来访问,用 aset 来设置。 向量可以看成是一种通用的数组,它的元素可以是任意的对象。而字符串是一种特殊的数组,它的元素只能是字符。如果元素是字符时,使用字符串相比向量更好,因为字符串需要的空间更少(只需要向量的 1/4),输出更直观,能用文本属性(text property),能使用 emacs 的 io 操作。但是有时必须使用向量,比如存储按键序列。\n由于 char-table 和 bool-vector 使用较少,而且较难理解,这里就不介绍了。\n测试函数 sequencep 用来测试一个对象是否是一个序列。arrayp 测试对象是否是数组。vectorp、char-table-p 和 bool-vector-p 分别测试对象是否是向量、char-table、bool-vector。\n序列的通用函数 一直没有提到一个重要的函数 length,它可以得到序列的长度。但是这个函数只对真列表有效。对于一个点列表和环形列表这个函数就不适用了。点列表会出参数类型不对的错误,而环形列表就更危险,会陷入死循环。如果不确定参数类型,不妨用 safe-length。比如:\n(safe-length \u0026#39;(a . b)) ; =\u0026gt; 1 (safe-length \u0026#39;#1=(1 2 . #1#)) ; =\u0026gt; 3 思考题\n写一个函数来检测列表是否是一个环形列表。由于现在还没有介绍 let 绑定和循环,不过如果会函数定义,还是可以用递归来实现的。\n取得序列里第 n 个元素可以用 elt 函数。但是我建议,对于已知类型的序列,还是用对应的函数比较好。也就是说,如果是列表就用 nth,如果是数组就用 aref。这样一方面是省去 elt 内部的判断,另一方面读代码时能很清楚知道序列的类型。\ncopy-sequence 在前面已经提到了。不过同样 copy-sequence 不能用于点列表和环形列表。对于点列表可以用 copy-tree 函数。环形列表就没有办法复制了。 好在这样的数据结构很少用到。\n数组操作 创建字符串已经说过了。创建向量可以用 vector 函数:\n(vector \u0026#39;foo 23 [bar baz] \u0026#34;rats\u0026#34;) 当然也可以直接用向量的读入语法创建向量,但是由于数组是自求值的,所以这样得到的向量和原来是一样的,也就是说参数不进行求值,看下面的例子就明白了:\nfoo ; =\u0026gt; (a b) [foo] ; =\u0026gt; [foo] (vector foo) ; =\u0026gt; [(a b)] 用 make-vector 可以生成元素相同的向量。\n(make-vector 9 \u0026#39;z) ; =\u0026gt; [z z z z z z z z z] fillarray 可以把整个数组用某个元素填充。\n(fillarray (make-vector 3 \u0026#39;z) 5) ; =\u0026gt; [5 5 5] aref 和 aset 可以用于访问和修改数组的元素。如果使用下标超出数组长度的话,会产生一个错误。所以要先确定数组的长度才能用这两个函数。\nvconcat 可以把多个序列用 vconcat 连接成一个向量。但是这个序列必须是真列表。这也是把列表转换成向量的方法。\n(vconcat [a b c] \u0026#34;aa\u0026#34; \u0026#39;(foo (6 7))) ; =\u0026gt; [a b c 97 97 foo (6 7)] 把向量转换成列表可以用 append 函数,这在前一节中已经提到。\n思考题\n如果知道 elisp 的 let 绑定和循环的使用方法,不妨试试实现一个 elisp 的 tr 函数,它接受三个参数,一是要操作的字符串,另外两个分别是要替换的字符集,和对应的替换后的字符集(当它是空集时,删除字符串中所有对应的字符)。\n函数列表 ;; 测试函数 (sequencep object) (arrayp object) (vectorp object) (char-table-p object) (bool-vector-p object) ;; 序列函数 (length sequence) (safe-length list) (elt sequence n) (copy-sequence arg) (copy-tree tree \u0026amp;optional vecp) ;; 数组函数 (vector \u0026amp;rest objects) (make-vector length init) (aref array idx) (aset array idx newelt) (vconcat \u0026amp;rest sequences) (append \u0026amp;rest sequences) 问题解答 测试列表是否是环形列表 这个算法是从 safe-length 定义中得到的。你可以直接看它的源码。下面是我写的函数。\n(defun circular-list-p (list) (and (consp list) (circular-list-p-1 (cdr list) list 0))) (defun circular-list-p-1 (tail halftail len) (if (eq tail halftail) t (if (consp tail) (circular-list-p-1 (cdr tail) (if (= (% len 2) 0) (cdr halftail) halftail) (1+ len)) nil))) 转换字符的 tr 函数 (defun my-tr (str from to) (if (= (length to) 0) ; 空字符串 (progn (setq from (append from nil)) (concat (delq nil (mapcar (lambda (c) (if (member c from) nil c)) (append str nil))))) (let (table newstr pair) ;; 构建转换表 (dotimes (i (length from)) (push (cons (aref from i) (aref to i)) table)) (dotimes (i (length str)) (push (if (setq pair (assoc (aref str i) table)) (cdr pair) (aref str i)) newstr)) (concat (nreverse newstr) nil)))) 这里用到的 dotimes 函数相当于一个 c 里的 for 循环。如果改写成 while 循环,相当于:\n(let (var) (while (\u0026lt; var count) body (setq var (1+ var))) result) 从这个例子也可以看出,由于列表具有丰富的函数和可变长度,使列表比数组使用更方便,而且效率往往更高。\n基本数据类型之五 \u0026ndash; 符号 符号是有名字的对象。可能这么说有点抽象。作个不恰当的比方,符号可以看作是 c 语言里的指针。通过符号你可以得到和这个符号相关联的信息,比如值,函数,属性列表等等。\n首先必须知道的是符号的命名规则。符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点“-+=*/”。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \\ 表示为符号,比如:\n(symbolp \u0026#39;+1) ; =\u0026gt; nil (symbolp \u0026#39;\\+1) ; =\u0026gt; t (symbol-name \u0026#39;\\+1) ; =\u0026gt; \u0026#34;+1\u0026#34; 其它字符 _~!@$%^\u0026amp;:\u0026lt;\u0026gt;{}? 用的比较少。但是也可以直接作为符号的名字。任何其它字符都可以用 \\ 转义后用在符号名字里。但是和字符串里字符表示不同,\\ 转义后只是表示其后的字符,比如 \\t 代表的字符 t,而不是制表符。如果要在符号名里使用制表符,必须在 \\ 后加上制表符本身。\n符号名是区分大小写的。这里有一些符号名的例子:\nfoo ; 名为 `foo\u0026#39; 的符号 foo ; 名为 `foo\u0026#39; 的符号,和 `foo\u0026#39; 不同 char-to-string ; 名为 `char-to-string\u0026#39; 的符号 1+ ; 名为 `1+\u0026#39; 的符号 (不是整数 `+1\u0026#39;) \\+1 ; 名为 `+1\u0026#39; 的符号 (可读性很差的名字) \\(*\\ 1\\ 2\\) ; 名为 `(* 1 2)\u0026#39; 的符号 (更差劲的名字). +-*/_~!@$%^\u0026amp;=:\u0026lt;\u0026gt;{} ; 名为 `+-*/_~!@$%^\u0026amp;=:\u0026lt;\u0026gt;{}\u0026#39; 的符号。 ; 这些字符无须转义 创建符号 一个名字如何与数据对应上呢?这就要了解一下符号是如何创建的了。符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。从这个名字可以看出这个表是用数组类型,事实上是一个向量。当 emacs 创建一个符号时,首先会对这个名字求 hash 值以得到一个在 obarray 这个向量中查找值所用的下标。hash 是查找字符串的很有效的方法。这里强调的是 obarray 不是一个特殊的数据结构,就是一个一般的向量。全局变量 obarray 里 emacs 所有变量、函数和其它符号所使用的 obarray(注意不同语境中 obarray 的含义不同。前一个 obarray 是变量名,后一个 obarray 是数据类型名)。也可以自己建立向量,把这个向量作为 obarray 来使用。这是一种代替散列的一种方法。它比直接使用散列有这样一些好处:\n符号不仅可以有一个值,还可以用属性列表,后者又可以相当于一个关联列表。这样有很高的扩展性,而且可以表达更高级的数据结构。 emacs 里有一些函数可以接受 obarray 作为参数,比如补全相关的函数。 当 lisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的 obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray 里。为了不污染 obarray,我下面的例子中尽量在 foo 这个 obarray 里进行。一般来说,去了 foo 参数,则会在 obarray 里进行。其结果应该是相同的:\n(setq foo (make-vector 10 0)) ; =\u0026gt; [0 0 0 0 0 0 0 0 0 0] (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; nil (intern \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc lisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #::\n(intern-soft \u0026#34;abc\u0026#34;) ; =\u0026gt; nil \u0026#39;abc ; =\u0026gt; abc (intern-soft \u0026#34;abc\u0026#34;) ; =\u0026gt; abc (intern-soft \u0026#34;abcd\u0026#34;) ; =\u0026gt; nil \u0026#39;#:abcd ; =\u0026gt; abcd (intern-soft \u0026#34;abcd\u0026#34;) ; =\u0026gt; nil 如果想除去 obarray 里的符号,可以用 unintern 函数。unintern 可以用符号名或符号作参数在指定的 obarray 里去除符号,成功去除则返回 t,如果没有查找到对应的符号则返回 nil:\n(intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc (unintern \u0026#34;abc\u0026#34; foo) ; =\u0026gt; t (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; nil 和 hash-table 一样,obarray 也提供一个 mapatoms 函数来遍历整个 obarray。比如要计算 obarray 里所有的符号数量:\n(setq count 0) ; =\u0026gt; 0 (defun count-syms (s) (setq count (1+ count))) ; =\u0026gt; count-syms (mapatoms \u0026#39;count-syms) ; =\u0026gt; nil count ; =\u0026gt; 28371 (length obarray) ; =\u0026gt; 1511 思考题\n由前面的例子可以看出 elisp 中的向量长度都是有限的,而 obarray 里的符号有成千上万个。那这些符号是怎样放到 obarray 里的呢?\n符号的组成 每个符号可以对应四个组成部分,一是符号的名字,可以用 symbol-name 访问。二是符号的值。符号的值可以通过 set 函数来设置,用 symbol-value 来访问。\n(set (intern \u0026#34;abc\u0026#34; foo) \u0026#34;i\u0026#39;m abc\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m abc\u0026#34; (symbol-value (intern \u0026#34;abc\u0026#34; foo)) ; =\u0026gt; \u0026#34;i\u0026#39;m abc\u0026#34; 可能大家最常见到 setq 函数,而 set 函数确很少见到。setq 可以看成是一个宏,它可以让你用 (setq sym val) 代替 (set (quote sym) val)。事实上这也是它名字的来源 (q 代表 quoted)。但是 setq 只能设置 obarray 里的变量,前面这个例子中就只能用 set 函数。\n思考题\n参考 assoc-default 的代码,写一个函数从一个关联列表中除去一个关键字对应的元素。这个函数可以直接修改关联列表符号的值。要求可以传递一个参数作为测试关键字是否相同的函数。比如:\n(setq foo \u0026#39;((?a . a) (?a . c) (?b . d))) (remove-from-list \u0026#39;foo ?b \u0026#39;char-equal) ; =\u0026gt; ((97 . a) (65 . c)) foo ; =\u0026gt; ((97 . a) (65 . c)) 如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil。对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误。\n符号的第三个组成部分是函数。它可以用 symbol-function 来访问,用 fset 来设置\n(fset (intern \u0026#34;abc\u0026#34; foo) (symbol-function \u0026#39;car)) ; =\u0026gt; #\u0026lt;subr car\u0026gt; (funcall (intern \u0026#34;abc\u0026#34; foo) \u0026#39;(a . b)) ; =\u0026gt; a 类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。\n符号的第四个组成部分是属性列表 (property list)。通常属性列表用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:\n(put (intern \u0026#34;abc\u0026#34; foo) \u0026#39;doc \u0026#34;this is abc\u0026#34;) ; =\u0026gt; \u0026#34;this is abc\u0026#34; (get (intern \u0026#34;abc\u0026#34; foo) \u0026#39;doc) ; =\u0026gt; \u0026#34;this is abc\u0026#34; (symbol-plist (intern \u0026#34;abc\u0026#34; foo)) ; =\u0026gt; (doc \u0026#34;this is abc\u0026#34;) 关联列表和属性列表很相似。符号的属性列表在内部表示上是用 (prop1 value1 prop2 value2 \u0026hellip;) 的形式,和关联列表也是很相似的。属性列表在查找和这个符号相关的信息时,要比直接用关联列表要简单快捷的多。所以变量的文档等信息都是放在符号的属性列表里。但是关联表在头端加入元素是很快的,而且它可以删除表里的元素。而属性列表则不能删除一个属性。\n如果已经把属性列表取出,那么还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表\n(plist-get \u0026#39;(foo 4) \u0026#39;foo) ; =\u0026gt; 4 (plist-get \u0026#39;(foo 4 bad) \u0026#39;bar) ; =\u0026gt; nil (setq my-plist \u0026#39;(bar t foo 4)) ; =\u0026gt; (bar t foo 4) (setq my-plist (plist-put my-plist \u0026#39;foo 69)) ; =\u0026gt; (bar t foo 69) (setq my-plist (plist-put my-plist \u0026#39;quux \u0026#39;(a))) ; =\u0026gt; (bar t foo 69 quux (a)) 思考题\n你能不能用已经学过的函数来实现 plist-get 和 plist-put?\n函数列表 (symbolp object) (intern-soft name \u0026amp;optional obarray) (intern string \u0026amp;optional obarray) (unintern name \u0026amp;optional obarray) (mapatoms function \u0026amp;optional obarray) (symbol-name symbol) (symbol-value symbol) (boundp symbol) (set symbol newval) (setq sym val sym val ...) (symbol-function symbol) (fset symbol definition) (fboundp symbol) (symbol-plist symbol) (get symbol propname) (put symbol propname value) 问题解答 obarray 里符号数为什么大于向量长度 其实这和散列的的实现是一样的。obarray 里的每一个元素通常称为 bucket。 一个 bucket 是可以容纳多个相同 hash 值的字符串和它们的数据。我们可以用 这样的方法来模拟一下:\n(defun hash-string (str) (let ((hash 0) c) (dotimes (i (length str)) (setq c (aref str i)) (if (\u0026gt; c #o140) (setq c (- c 40))) (setq hash (+ (setq hash (lsh hash 3)) (lsh hash -28) c))) hash)) (let ((len 10) str hash) (setq foo (make-vector len 0)) (dotimes (i (1+ len)) (setq str (char-to-string (+ ?a i)) hash (% (hash-string str) len)) (message \u0026#34;i put %s in slot %d\u0026#34; str hash) (if (eq (aref foo hash) 0) (intern str foo) (message \u0026#34;i found %s is already taking the slot: %s\u0026#34; (aref foo hash) foo) (intern str foo) (message \u0026#34;now i\u0026#39;am in the slot too: %s\u0026#34; foo)))) 在我这里的输出是\ni put a in slot 7\ri put b in slot 8\ri put c in slot 9\ri put d in slot 0\ri put e in slot 1\ri put f in slot 2\ri put g in slot 3\ri put h in slot 4\ri put i in slot 5\ri put j in slot 6\ri put k in slot 7\ri found a is already taking the slot: [d e f g h i j a b c]\rnow i\u0026#39;am in the slot too: [d e f g h i j k b c] 当然,这个 hash-string 和实际 obarray 里用的 hash-string 只是算法上是 相同的,但是由于数据类型和 c 不是完全相同,所以对于长一点的字符串结果 可能不一样,我只好用单个字符来演示一下。\n根据关键字删除关联列表中的元素 (defun remove-from-list (list-var key \u0026amp;optional test) (let ((prev (symbol-value list-var)) tail found value elt) (or test (setq test \u0026#39;equal)) (if (funcall test (caar prev) key) (set list-var (cdr prev)) (setq tail (cdr prev)) (while (and tail (not found)) (setq elt (car tail)) (if (funcall test (car elt) key) (progn (setq found t) (setcdr prev (cdr tail))) (setq tail (cdr tail) prev (cdr prev))))) (symbol-value list-var))) 注意这个函数的参数 list-var 是一个符号,所以这个函数不能直接传递一个列表。这和 add-to-list 的参数是一样的。\nplist-get 和 plist-put 的实现 (defun my-plist-get (plist prop) (cadr (memq plist prop))) (defun my-plist-put (plist prop val) (let ((tail (memq prop plist))) (if tail (setcar (cdr tail) val) (setcdr (last plist) (list prop val)))) plist) my-plist-put 函数没有 plist-put 那样 robust,如果属性列表是 \u0026lsquo;(bar t foo) 这样的话,这个函数就会出错。而且加入一个属性的时间复杂度比 plist 更高(memq 和 last 都是 o(n)),不过可以用循环来达到相同的时间复杂度。\n求值规则 至此,elisp 中最常见的数据类型已经介绍完了。我们可以真正开始学习怎样写一个 elisp 程序。如果想深入了解一下 lisp 是如何工作的,不妨先花些时间看看 lisp 的求值过程。当然忽略这一部分也是可以的,因为我觉得这个求值规则是那么自然,以至于你会认为它就是应该这样的。\n求值是 lisp 解释器的核心,理解了求值过程也就学会了 lisp 编程的一半。正因为这样,我有点担心自己说得不清楚或者理解错误,会误导了你。所以如果真想深入了解的话,还是自己看 info elisp - evaluation 这一章吧。\n一个要求值的 lisp 对象被称为表达式(form)。所有的表达式可以分为三种:符号、列表和其它类型(废话)。下面一一说明各种表达式的求值规则。\n第一种表达式是最简单的,自求值表达式。前面说过数字、字符串、向量都是自求值表达式。还有两个特殊的符号 t 和 nil 也可以看成是自求值表达式。\n第二种表达式是符号。符号的求值结果就是符号的值。如果它没有值,就会出现 void-variable 的错误。\n第三种表达式是列表表达式。而列表表达式又可以根据第一个元素分为函数调用、宏调用和特殊表达式(special form)三种。列表的第一个表达式如果是一个符号,解释器会查找这个表达式的函数值。如果函数值是另一个符号,则会继续查找这个符号的函数值。这称为“symbol function indirection”。最后直到某个符号的函数值是一个 lisp 函数(lambda 表达式)、byte-code 函数、原子函数(primitive function)、宏、特殊表达式或 autoload 对象。如果不是这些类型,比如某个符号的函数值是前面出现的某个符号导致无限循环,或者某个符号函数值为空,都会导致一个错误 invalid-function。\n这个函数显示 indirection function\n(symbol-function \u0026#39;car) ; =\u0026gt; #\u0026lt;subr car\u0026gt; (fset \u0026#39;first \u0026#39;car) ; =\u0026gt; car (fset \u0026#39;erste \u0026#39;first) ; =\u0026gt; first (erste \u0026#39;(1 2 3)) ; =\u0026gt; 1 对于第一个元素是 lisp 函数对象、byte-code 对象和原子函数时,这个列表也称为函数调用(funtion call)。对这样的列表求值时,先对列表中其它元素先求值,求值的结果作为函数调用的真正参数。然后使用 apply 函数用这些参数调用函数。如果函数是用 lisp 写的,可以理解为把参数和变量绑定到函数后,对函数体顺序求值,返回最后一个 form 的值。\n如果第一个元素是一个宏对象,列表里的其它元素不会立即求值,而是根据宏定义进行扩展。如果扩展后还是一个宏调用,则会继续扩展下去,直到扩展的结果不再是一个宏调用为止。例如\n(defmacro cadr (x) (list \u0026#39;car (list \u0026#39;cdr x))) 这样 (cadr (assq 'handler list)) 扩展后成为 (car (cdr (assq 'handler list)))。\n第一个元素如果是一个特殊表达式时,它的参数可能并不会全求值。这些特殊表达式通常是用于控制结构或者变量绑定。每个特殊表达式都有对应的求值规则。这在下面会提到。\n最后用这个伪代码来说明一下 elisp 中的求值规则:\n(defun (eval exp) (cond ((numberp exp) exp) ((stringp exp) exp) ((arrayp exp) exp) ((symbolp exp) (symbol-value exp)) ((special-form-p (car exp)) (eval-special-form exp)) ((fboundp (car exp)) (apply (car exp) (cdr exp))) (t (error \u0026#34;unknown expression type -- eval %s\u0026#34; exp)))) 变量 在此之前,我们已经见过 elisp 中的两种变量,全局变量和 let 绑定的局部变量。它们相当于其它语言中的全局变量和局部变量。\n关于 let 绑定的变量,有两点需要补充的。当同一个变量名既是全局变量也是局部变量,或者用 let 多层绑定,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如\n(progn (setq foo \u0026#34;i\u0026#39;m global variable!\u0026#34;) (let ((foo 5)) (message \u0026#34;foo value is: %s\u0026#34; foo) (let (foo) (setq foo \u0026#34;i\u0026#39;m local variable!\u0026#34;) (message foo)) (message \u0026#34;foo value is still: %s\u0026#34; foo)) (message foo)) 另外需要注意一点的是局部变量的绑定不能超过一定的层数,也就是说,你不能把 foo 用 let 绑定 10000 层。当然普通的函数是不可能写成这样的,但是递归函数就不一定了。限制层数的变量在 max-specpdl-size 中定义。如果你写的递归函数有这个需要的话,可以先设置这个变量的值。\nemacs 有一种特殊的局部变量 ── buffer-local 变量。\nbuffer-local 变量 emacs 能有如此丰富的模式,各个缓冲区之间能不相互冲突,很大程度上要归功于 buffer-local 变量。\n声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是相当于在所有变量中都产生一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。\n为了方便演示,下面的代码我假定你是在 *scratch* 缓冲区里运行。我使用另一个一般都会有的缓冲区 *messages* 作为测试。先介绍两个用到的函数( with-current-buffer 其实是一个宏)。\nwith-current-buffer 的使用形式是\n(with-current-buffer buffer body) 其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。\nget-buffer 可以用缓冲区的名字得到对应的缓冲区对象。如果没有这样名字的缓冲区会返回 nil。\n下面是使用 buffer-local 变量的例子:\n(setq foo \u0026#34;i\u0026#39;m global variable!\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; (make-local-variable \u0026#39;foo) ; =\u0026gt; foo foo ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; (setq foo \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34; foo ; =\u0026gt; \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34; (with-current-buffer \u0026#34;*messages*\u0026#34; foo) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; 从这个例子中可以看出,当一个符号作为全局变量时有一个值的话,用 make-local-variable 声明为 buffer-local 变量时,这个变量的值还是全局变量的值。这时候全局的值也称为缺省值。你可以用 default -value 来访问这个符号的全局变量的值\n(default-value \u0026#39;foo) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; 如果一个变量是 buffer-local,那么在这个缓冲区内使用用 setq 就只能用改变当前缓冲区里这个变量的值。setq-default 可以修改符号作为全局变量的值。通常在 .emacs 里经常使用 setq-default,这样可以防止修改的是导入 .emacs 文件对应的缓冲区里的 buffer-local 变量,而不是设置全局的值。\n测试一个变量是不是 buffer-local 可以用 local-variable-p\n(local-variable-p \u0026#39;foo) ; =\u0026gt; t (local-variable-p \u0026#39;foo (get-buffer \u0026#34;*messages*\u0026#34;)) ; =\u0026gt; nil 如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量可以用 buffer-local-value\n(with-current-buffer \u0026#34;*messages*\u0026#34; (buffer-local-value \u0026#39;foo (get-buffer \u0026#34;*scratch*\u0026#34;))) ; =\u0026gt; \u0026#34;i\u0026#39;m buffer local variable!\u0026#34; 变量的作用域 我们现在已经学习这样几种变量:\n全局变量 buffer-local 变量 let 绑定局部变量 如果还要考虑函数的参数列表声明的变量,也就是 4 种类型的变量。那这种变量的作用范围 (scope) 和生存期(extent)分别是怎样的呢?\n作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。\n比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子:\n(defun binder (x) ; `x\u0026#39; is bound in `binder\u0026#39;. (foo 5)) ; `foo\u0026#39; is some other function. (defun user () ; `x\u0026#39; is used \u0026#34;free\u0026#34; in `user\u0026#39;. (list x)) (defun foo (ignore) (user)) (binder 10) ; =\u0026gt; (10) 对于词法作用域的语言,在 user 函数里无论如何是不能访问 binder 函数中绑定的 x。但是在 elisp 中可以。\n生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭 emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,emacs lisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。这和 c 和 pascal 里的 local 和 automatic 变量是一样的。与此相对的是 indefinite extent,变量即使离开绑定它的表达式还能有效。比如:\n(defun make-add (n) (function (lambda (m) (+ n m)))) ; return a function. (fset \u0026#39;add2 (make-add 2)) ; define function `add2\u0026#39; ; with `(make-add 2)\u0026#39;. (add2 4) ; try to add 2 to 4. 其它 lisp 方言中有闭包,但是 emacs lisp 中没有。\n说完这些概念,可能你还是一点雾水。我给一个判断变量是否有效的方法吧:\n看看包含这个变量的 form 中是否有 let 绑定这个局部变量。如果这个 form 不是在定义一个函数,则跳到第 3 步。 如果是在定义函数,则不仅要看这个函数的参数中是否有这个变量,而且还要看所有直接或间接调用这个函数的函数中是否有用 let 绑定或者参数列表里有这个变量名。这就没有办法确定了,所以你永远无法判断一个函数中出现的没有用 let 绑定,也不在参数列表中的变量是否是没有定义过的。但是一般来说这不是一个好习惯。 看这个变量是否是一个全局变量或者是 buffer-local 变量。 对于在一个函数中绑定一个变量,而在另一个函数中还在使用,manual 里认为这两个种情况下是比较好的:\n这个变量只有相关的几个函数中使用,在一个文件中放在一起。这个变量起程序里通信的作用。而且需要写好注释告诉其它程序员怎样使用它。 如果这个变量是定义明确、有很好文档作用的,可能让所有函数使用它,但是不要设置它。比如 case-fold-search。(我怎么觉得这里是用全局变量呢。) 思考题\n先在 *scratch* 缓冲区里运行了 (kill-local-variable 'foo) 后,运行几次下面的表达式,你能预测它们结果吗?\n(progn (setq foo \u0026#34;i\u0026#39;m local variable!\u0026#34;) (let ((foo \u0026#34;i\u0026#39;m local variable!\u0026#34;)) (set (make-local-variable \u0026#39;foo) \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34;) (setq foo \u0026#34;this is a variable!\u0026#34;) (message foo)) (message foo)) 其它函数 一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 xemacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。\n使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。\nfoo ; =\u0026gt; \u0026#34;i\u0026#39;m local variable!\u0026#34; (boundp \u0026#39;foo) ; =\u0026gt; t (default-boundp \u0026#39;foo) ; =\u0026gt; t (makunbound \u0026#39;foo) ; =\u0026gt; foo foo ; this will signal an error (default-boundp \u0026#39;foo) ; =\u0026gt; t (kill-local-variable \u0026#39;foo) ; =\u0026gt; foo 变量名习惯 对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:\nhook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。 function 值为一个函数 functions 值为一个函数列表 flag 值为 nil 或 non-nil predicate 值是一个作判断的函数,返回 nil 或 non-nil program 或 -command 一个程序或 shell 命令名 form 一个表达式 forms 一个表达式列表。 map 一个按键映射(keymap) 函数列表 (make-local-variable variable) (make-variable-buffer-local variable) (with-current-buffer buffer \u0026amp;rest body) (get-buffer name) (default-value symbol) (local-variable-p variable \u0026amp;optional buffer) (buffer-local-value variable buffer) (boundp symbol) (default-boundp symbol) (makunbound symbol) (kill-local-variable variable) (kill-all-local-variables) 变量列表 max-specpdl-size 问题解答 同一个表达式运行再次结果不同? 运行第一次时,foo 缺省值为 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;,而 buffer-local 值为 \u0026ldquo;this is a variable!\u0026quot;。第一个和第二个 message 都会显示 \u0026ldquo;this is a variable!\u0026quot;。运行第二次时,foo 缺省值和 buffer-local 值都成了 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;,而第一次 message 显示 \u0026ldquo;this is a variable!\u0026quot;,第二次 显示 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;。这是由于 make-local-variable 在这个符号是 否已经是 buffer-local 变量时有不同表现造成的。如果已经是一个 buffer-local 变量,则它什么也不做,而如果不是,则会生成一个 buffer-local 变量,这时在这个表达式内的所有 foo 也被重新绑定了。希望你 写的函数能想到一点。\n函数和命令 在 elisp 里类似函数的对象很多,比如:\n函数。这里的函数特指用 lisp 写的函数。 原子函数(primitive)。用 c 写的函数,比如 car、append。 lambda 表达式 特殊表达式 宏 (macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。 命令。命令能用 command-execute 调用。函数也可以是命令。 以上这些用 functionp 来测试都会返回 t。\n我们已经学过如何定义一个函数。但是这些函数的参数个数都是确定。但是你可以看到 emacs 里有很多函数是接受可选参数,比如 random 函数。还有一些函数可以接受不确定的参数,比如加减乘除。这样的函数在 elisp 中是如何定义的呢?\n参数列表的语法 这是参数列表的方法形式:\n(required-vars...\r[\u0026amp;optional optional-vars...]\r[\u0026amp;rest rest-var]) 它的意思是说,你必须把必须提供的参数写在前面,可选的参数写在后面,最后用一个符号表示剩余的所有参数。比如\n(defun foo (var1 var2 \u0026amp;optional opt1 opt2 \u0026amp;rest rest) (list var1 var2 opt1 opt2 rest)) (foo 1 2) ; =\u0026gt; (1 2 nil nil nil) (foo 1 2 3) ; =\u0026gt; (1 2 3 nil nil) (foo 1 2 3 4 5 6) ; =\u0026gt; (1 2 3 4 (5 6)) 从这个例子可以看出,当可选参数没有提供时,在函数体里,对应的参数值都是 nil。同样调用函数时没有提供剩余参数时,其值也为 nil,但是一旦提供了剩余参数,则所有参数是以列表的形式放在对应变量里。\n思考题\n写一个函数测试两个浮点数是否相等,设置一个可选参数,如果提供这个参数,则用这个参数作为测试误差,否则用 1.0e-6 作为误差。\n关于文档字符串 最好为你的函数都提供一个文档字符串。关于文档字符串有一些规范,最好遵守这些约定。\n字符串的第一行最好是独立的。因为 apropos 命令只能显示第一行的文档。所以最好用一行(一两个完整的句子)总结这个函数的目的。\n文档的缩进最好要根据最后的显示的效果来调用。因为引号之类字符会多占用一个字符,所以在源文件里缩进最好看,不一定显示的最好。\n如果你想要让你的函数参数显示的与函数定义的不同(比如提示用户如何调用这个函数),可以在文档最后一行,加上一行:\n\\(fn arglist) 注意这一行前面要有一个空行,这一行后不能再有空行。比如\n(defun foo (var1 var2 \u0026amp;optional opt1 opt2 \u0026amp;rest rest) \u0026#34;you should call the function like: \\(fn v1 v2)\u0026#34; (list var1 var2 opt1 opt2 rest)) 还有一些有特殊标记功能的符号,比如 ``\u0026rsquo;引起的符号名可以生成一个链接,这样可以在help 中更方便的查看相关变量或函数的文档。\\{major-mode-map}` 可以显示扩展成这个模式按键的说明,例如:\n(defun foo () \u0026#34;a simple document string to show how to use `\u0026#39; and \\\\=\\\\{}. you can press this button `help\u0026#39; to see the document of function \\\u0026#34;help\\\u0026#34;. this is keybind of text-mode(substitute from \\\\=\\\\{text-mode-map}): \\\\{text-mode-map} see also `substitute-command-keys\u0026#39; and `documentation\u0026#39;\u0026#34; ) 调用函数 通常函数的调用都是用 eval 进行的,但是有时需要在运行时才决定使用什么函数,这时就需要用 funcall 和 apply 两个函数了。这两个函数都是把其余的参数作为函数的参数进行调用。那这两个函数有什么参数呢?唯一的区别就在于 funcall 是直接把参数传递给函数,而 apply 的最后一个参数是一个列表,传入函数的参数把列表进行一次平铺后再传给函数,看下面这个例子就明白了\n(funcall \u0026#39;list \u0026#39;x \u0026#39;(y) \u0026#39;(z)) ; =\u0026gt; (x (y) (z)) (apply \u0026#39;list \u0026#39;x \u0026#39;(y ) \u0026#39;(z)) ; =\u0026gt; (x (y) z) 思考题\n如果一个 list 作为一个树的结构,任何是 cons cell 的元素都是一个内部节点(不允许有 dotted list 出现),任何不是 cons cell 的元素都是树的叶子。请写一个函数,调用的一个类似 mapcar 的函数,调用一个函数遍历树的叶子,并收集所有的结果,返回一个结构相同的树,比如:\n(tree-mapcar \u0026#39;1+ \u0026#39;(1 (2 (3 4)) (5))) ; =\u0026gt; (2 (3 (4 5)) (6)) 宏 前面在已经简单介绍过宏。宏的调用和函数是很类似的,它的求值和函数差不多,但是有一个重要的区别是,宏的参数是出现在最后扩展后的表达式中,而函数参数是求值后才传递给这个函数:\n(defmacro foo (arg) (list \u0026#39;message \u0026#34;%d %d\u0026#34; arg arg)) (defun bar (arg) (message \u0026#34;%d %d\u0026#34; arg arg)) (let ((i 1)) (bar (incf i))) ; =\u0026gt; \u0026#34;2 2\u0026#34; (let ((i 1)) (foo (incf i))) ; =\u0026gt; \u0026#34;2 3\u0026#34; 也许你对前面这个例子 foo 里为什么要用 list 函数很不解。其实宏可以这样看,如果把宏定义作一个表达式来运行,最后把参数用调用时的参数替换,这样就得到了宏调用最后用于求值的表达式。这个过程称为扩展。可以用 macroexpand 函数进行模拟\n(macroexpand \u0026#39;(foo (incf i))) ; =\u0026gt; (message \u0026#34;%d %d\u0026#34; (incf i) (incf i)) 上面用 macroexpand 得到的结果就是用于求值的表达式。\n使用 macroexpand 可以使宏的编写变得容易一些。但是如果不能进行 debug 是很不方便的。在宏定义里可以引入 declare 表达式,它可以增加一些信息。目前只支持两类声明:debug 和 indent。debug 可选择的类型很多,具体参考 info elisp - edebug 一章,一般情况下用 t 就足够了。indent 的类型比较简单,它可以使用这样几种类型:\nnil 也就是一般的方式缩进 defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进 整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1 符号 这个是最坏情况,你要写一个函数自己处理缩进。 看 when 的定义就能知道 declare 如何使用了\n(defmacro when (cond \u0026amp;rest body) (declare (indent 1) (debug t)) (list \u0026#39;if cond (cons \u0026#39;progn body))) 实际上,declare 声明只是设置这个符号的属性列表\n(symbol-plist \u0026#39;when) ; =\u0026gt; (lisp-indent-function 1 edebug-form-spec t) 思考题\n一个比较常用的结构是当 buffer 是可读情况下,绑定 inhibit-read-only 值为 t 来强制插入字符串。请写一个这样的宏,处理好缩进和调用。\n从前面宏 when 的定义可以看出直接使用 list,cons,append 构造宏是很麻烦的。为了使记号简洁,lisp 中有一个特殊的宏 \u0026ldquo;`\u0026quot;,称为 backquote。在这个宏里,所有的表达式都是引起(quote)的,如果要让一个表达式不引起(也就是列表中使用的是表达式的值),需要在前面加 “,”,如果要让一个列表作为整个列表的一部分(slice),可以用 \u0026ldquo;,@\u0026quot;。\n`(a list of ,(+ 2 3) elements) ; =\u0026gt; (a list of 5 elements) (setq some-list \u0026#39;(2 3)) ; =\u0026gt; (2 3) `(1 ,some-list 4 ,@some-list) ; =\u0026gt; (1 (2 3) 4 2 3) 有了这些标记,前面 when 这个宏可以写成\n(defmacro when (cond \u0026amp;rest body) `(if ,cond (progn ,@body))) 值得注意的是这个 backquote 本身就是一个宏,从这里可以看出宏除了减少重复代码这个作用之外的另一个用途:定义新的控制结构,甚至增加新的语法特性。\n命令 emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。lisp 编写的命令都含有一个 interactive 表达式。这个表达式指明了这个命令的参数。比如下面这个命令\n(defun hello-world (name) (interactive \u0026#34;swhat you name? \u0026#34;) (message \u0026#34;hello, %s\u0026#34; name)) 现在你可以用 m-x 来调用这个命令。让我们来看看 interactive 的参数是什么意思。这个字符串的第一个字符(也称为代码字符)代表参数的类型,比如这里 s 代表参数的类型是一个字符串,而其后的字符串是用来提示的字符串。如果这个命令有多个参数,可以在这个提示字符串后使用换行符分开,比如:\n(defun hello-world (name time) (interactive \u0026#34;swhat you name? \\nnwhat the time? \u0026#34;) (message \u0026#34;good %s, %s\u0026#34; (cond ((\u0026lt; time 13) \u0026#34;morning\u0026#34;) ((\u0026lt; time 19) \u0026#34;afternoon\u0026#34;) (t \u0026#34;evening\u0026#34;)) name)) interactive 可以使用的代码字符很多,虽然有一定的规则,比如字符串用 s,数字用 n,文件用 f,区域用 r,但是还是很容易忘记,用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。\n首先学习和代码字符等价的几个函数。s 对应的函数是 read-string。比如\n(read-string \u0026#34;what your name? \u0026#34; user-full-name) n 对应的函数是 read-number,文件对应 read-file-name。很容易记对吧。其实大部分代码字符都是有这样对应的函数或替换的方法(见下表)。\n代码字符 代替的表达式 a (completing-read prompt obarray \u0026lsquo;fboundp t) b (read-buffer prompt nil t) b (read-buffer prompt) c (read-char prompt) c (read-command prompt) d (point) d (read-directory-name prompt) e (read-event) f (read-file-name prompt nil nil t) f (read-file-name prompt) g 暂时不知道和 f 的差别 k (read-key-sequence prompt) k (read-key-sequence prompt nil t) m (mark) n (read-number prompt) n (if current-prefix-arg (prefix-numeric-value current-prefix-arg) (read-number prompt)) p (prefix-numeric-value current-prefix-arg) p current-prefix-arg r (region-beginning) (region-end) s (read-string prompt) s (completing-read prompt obarray nil t) v (read-variable prompt) x (read-from-minibuffer prompt nil nil t) x (eval (read-from-minibuffer prompt nil nil t)) z (read-coding-system prompt) z (and current-prefix-arg (read-coding-system prompt)) 知道这些表达式如何用于 interactive 表达式里呢?简而言之,如果 interactive 的参数是一个表达式,则这个表达式求值后的列表元素对应于这个命令的参数。请看这个例子:\n(defun read-hiden-file (file arg) (interactive (list (read-file-name \u0026#34;choose a hiden file: \u0026#34; \u0026#34;~/\u0026#34; nil nil nil (lambda (name) (string-match \u0026#34;^\\\\.\u0026#34; (file-name-nondirectory name)))) current-prefix-arg)) (message \u0026#34;%s, %s\u0026#34; file arg)) 第一个参数是读入一个以 \u0026ldquo;.\u0026rdquo; 开头的文件名,第二个参数为当前的前缀参数(prefix argument),它可以用 c-u 或 c-u 加数字提供。list 把这两个参数构成一个列表。这就是命令一般的自定义设定参数的方法。\n需要注意的是 current-prefix-arg 这个变量。这个变量当一个命令被调用,它就被赋与一个值,你可以用 c-u 就能改变它的值。在命令运行过程中,它的值始终都存在。即使你的命令不用参数,你也可以访问它\n(defun foo () (interactive) (message \u0026#34;%s\u0026#34; current-prefix-arg)) 用 c-u foo 调用它,你可以发现它的值是 (4)。那为什么大多数命令还单独为它设置一个参数呢?这是因为命令不仅是用户可以调用,很可能其它函数也可以调用,单独设置一个参数可以方便的用参数传递的方法调用这个命令。事实上所有的命令都可以不带参数,而使用前面介绍的方法在命令定义的部分读入需要的参数,但是为了提高命令的可重用性和代码的可读性,还是把参数分离到 interactive 表达式里好。\n从现在开始可能会遇到很多函数,它们的用法有的简单,有的却复杂的要用大段篇幅来解释。我可能就会根据需要来解释一两个函数,就不一一介绍了。自己看 info elisp,用 i 来查找对应的函数。\n思考题\n写一个命令用来切换 major-mode。要求用户输入一个 major-mode 的名字,就切换到这个 major-mode,而且要提供一种补全的办法,去除所有不是 major-mode 的符号,这样用户需要输入少量词就能找到对应的 major-mode。\n函数列表 (functionp object) (apply function \u0026amp;rest arguments) (funcall function \u0026amp;rest arguments) (defmacro name arglist [docstring] [decl] body...) (macroexpand form \u0026amp;optional environment) (declare \u0026amp;rest specs) (` arg) (interactive args) (read-string prompt \u0026amp;optional initial-input history default-value inherit-input-method) (read-file-name prompt \u0026amp;optional dir default-filename mustmatch initial predicate) (completing-read prompt collection \u0026amp;optional predicate require-match initial-input hist def inherit-input-method) (read-buffer prompt \u0026amp;optional def require-match) (read-char \u0026amp;optional prompt inherit-input-method seconds) (read-command prompt \u0026amp;optional default-value) (read-directory-name prompt \u0026amp;optional dir default-dirname mustmatch initial) (read-event \u0026amp;optional prompt inherit-input-method seconds) (read-key-sequence prompt \u0026amp;optional continue-echo dont-downcase-last can-return-switch-frame command-loop) (read-number prompt \u0026amp;optional default) (prefix-numeric-value raw) (read-from-minibuffer prompt \u0026amp;optional initial-contents keymap read hist default-value inherit-input-method) (read-coding-system prompt \u0026amp;optional default-coding-system) 变量列表 current-prefix-arg 问题解答 可选误差的浮点数比较 (defun approx-equal (x y \u0026amp;optional err) (if err (setq err (abs err)) (setq err 1.0e-6)) (or (and (= x 0) (= y 0)) (\u0026lt; (/ (abs (- x y)) (max (abs x) (abs y))) err))) 这个应该是很简单的一个问题。\n遍历树的函数 (defun tree-mapcar (func tree) (if (consp tree) (mapcar (lambda (child) (tree-mapcar func child)) tree) (funcall func tree))) 这个函数可能对于树算法比较熟悉的人一点都不难,就当练手吧。\n宏 with-inhibit-read-only-t (defmacro with-inhibit-read-only-t (\u0026amp;rest body) (declare (indent 0) (debug t)) (cons \u0026#39;let (cons \u0026#39;((inhibit-read-only t)) body))) 如果用 backquote 来改写一个就会发现这个宏会很容易写,而且更容易读了。\n切换 major-mode 的命令 (defvar switch-major-mode-history nil) (defun switch-major-mode (mode) (interactive (list (intern (completing-read \u0026#34;switch to mode: \u0026#34; obarray (lambda (s) (and (fboundp s) (string-match \u0026#34;-mode$\u0026#34; (symbol-name s)))) t nil \u0026#39;switch-major-mode-history)))) (setq switch-major-mode-history (cons (symbol-name major-mode) switch-major-mode-history)) (funcall mode)) 这是我常用的一个命令之一。这个实现也是一个使用 minibuffer 历史的例子。\n正则表达式 如果你不懂正则表达式,而你还想进一步学习编程的话,那你应该停下手边的事情,先学学正则表达式。即使你不想学习编程,也不喜欢编程,学习一点正则表达式,也可能让你的文本编辑效率提高很多。\n在这里,我不想详细介绍正则表达式,因为我觉得这类的文档已经很多了,比我写得好的文章多的是。如果你找不到一个好的入门教程,我建议你不妨看看 perlretut。我想说的是和 emacs 有关的正则表达式的内容,比如,和 perl 正则表达式的差异、语法表格(syntax table)和字符分类(category)等。\n与 perl 正则表达式比较 perl 是文本处理的首选语言。它内置强大而简洁的正则表达式,许多程序也都兼容 perl 的正则表达式。说实话,就简洁而言,我对 emacs 的正则表达式是非常反感的,那么多的反斜线经常让我抓狂。首先,emacs 里的反斜线构成的特殊结构(backslash construct)出现是相当频繁的。在 perl 正则表达式里,()[]{}| 都是特殊字符,而 emacs 它们不是这样。所以它们匹配字符时是不用反斜线,而作为特殊结构时就要用反斜线。而事实上()|作为字符来匹配的情形远远小于作为捕捉字符串和作或运算的情形概率小。而 emacs 的正则表达式又没有 perl 那种简洁的记号,完全用字符串来表示,这样使得一个正则表达式常常一眼看去全是 \\\\。\n到底要用多少个\\? 经常会记不住在 emacs 的正则表达式中应该用几个 \\。有一个比较好的方法,首先想想没有引号引起时的正则表达式是怎样。比如对于特殊字符 $ 要用 \\$,对于反斜线结构是 \\(, \\{,\\| 等等。知道这个怎样写之后,再把所有 \\ 替换成 \\\\,这就是最后写到双引号里形式。所以要匹配一个 \\,应该用 \\\\,而写在引号里就应该用 \\\\\\\\ 来匹配。\nemacs 里匹配的对象不仅包括字符串,还有 buffer,所以有一些对字符串和 buffer 有区分的结构。比如 $ 对于字符串是匹配字符串的末尾,而在 buffer 里是行尾。而 \\' 匹配的是字符串和 buffe 的末尾。对应 ^ 和 ``` 也是这样。\nemacs 对字符有很多种分类方法,在正则表达式里也可以使用。比如按语法类型分类,可以用 \u0026ldquo;\\s\u0026rdquo; 结构匹配一类语法分类的字符,最常用的比如匹配空格的 \\s- 和匹配词的 \\sw(等价于 \\w)。这在 perl 里是没有的。另外 emacs 里字符还对应一个或多个分类(category),比如所有汉字在分类 c 里。这样可以用 \\cc 来匹配一个汉字。这在 perl 里也有类似的分类。除此之外,还有一些预定义的字符分类,可以用 [:class:] 的形式,比如 [:digit:] 匹配 0-9 之间的数,[:ascii:] 匹配所有 ascii 字符等等。在 perl 里只定义几类最常用的字符集,比如 \\d, \\s, \\w,但是我觉得非常实用。比 emacs 这样长的标记好用的多。\n另外在用 [] 表示一个字符集时,emacs 里不能用 \\ 进行转义,事实上 \\ 在这里不是一个特殊字符。所以 emacs 里的处理方法是,把特殊字符提前或放在后面,比如如果要在字符集里包括 ] 的话,要把 ] 放在第一位。而如果要包括 -,只能放在最后一位,如果要包括 ^ 不能放在第一位。如果只想匹配一个 ^,就只能用 \\^ 的形式。比较拗口,希望下面这个例子能帮你理解\n(let ((str \u0026#34;abc]-^]123\u0026#34;)) (string-match \u0026#34;[]^0-9-]+\u0026#34; str) (match-string 0 str)) ; =\u0026gt; \u0026#34;]-^]123\u0026#34; 最后提示一下,emacs 提供一个很好的正则表达式调试工具:m-x re-builder。它能显示 buffer 匹配正则表达式的字符串,并用不同颜色显示捕捉的字符串。\n语法表格和分类表格简介 语法表格指的是 emacs 为每个字符都指定了语法功能,这为解析函数,复杂的移动命令等等提供了各种语法结构的起点和终点。语法表使用的数据结构是一种称为字符表(char-table)的数组,它能以字符作为下标(还记得 emacs 里的字符就是整数吗)来得到对应的值。语法表里一个字符对应一个语法分类的列表。每一个分类都有一个助记字符(mnemonic character)。一共有哪几类分类呢?\n名称 助记符 说明 空白(whitespace) - 或 \u0026rsquo; ' 词(word) w 符号(symbol) _ 这是除 word 之外其它用于变量和命令名的字符。 标点(punctuation) . open 和 close ( 和 ) 一般是括号 ()[]{} 字符串引号(string quote) \u0026quot; 转义符(escape-syntax) \\ 用于转义序列,比如 c 和 lisp 字符串中的 \\。 字符引号(character quote) / paired delimiter $ 只有 tex 模式中使用 expression prefix ' 注释开始和注释结束 \u0026lt; 和 \u0026gt; inherit standard syntax @ generic comment delimiter ! 语法表格可以继承,所以基本上所有语法表格都是从 standard-syntax-table 继承而来,作少量修改,加上每个模式特有的语法构成就行了。一般来说记住几类重要的分类就行了,比如,空白包括空格,制表符,换行符,换页符。词包括所有的大小写字母,数字。符号一般按使用的模式而定,比如 c 中包含 _,而 lisp 中是 $\u0026amp;*+-_\u0026lt;\u0026gt;。可以用 m-x describe-syntax 来查看所有字符的语法分类。\n字符分类(category)是另一种分类方法,每个分类都有一个名字,对应一个从 ``到 ~ 的 ascii 字符。可以用 m-x describe-categories 查看所有字符的分类。每一种分类都有说明,我就不详细解释了。\n几个常用的函数 如果你要匹配的字符串中含有很多特殊字符,而你又想用正则表达式进行匹配,可以使用 regexp-quote 函数,它可以让字符串中的特殊字符自动转义。\n一般多个可选词的匹配可以用或运算连接起来,但是这有两个不好的地方,一是要写很长的正则表达式,还含有很多反斜线,不好看,容易出错,也不好修改,二是效率很低。这时可以使用 regexp-opt 还产生一个更好的正则表达式\n(regexp-opt \u0026#39;(\u0026#34;foobar\u0026#34; \u0026#34;foobaz\u0026#34; \u0026#34;foo\u0026#34;)) ; =\u0026gt; \u0026#34;foo\\\\(?:ba[rz]\\\\)?\u0026#34; 函数列表 (regexp-quote string) (regexp-opt strings \u0026amp;optional paren) 命令列表 describe-syntax\rdescribe-categories 操作对象之一 \u0026ndash; 缓冲区 缓冲区(buffer)是用来保存要编辑文本的对象。通常缓冲区都是和文件相关联的,但是也有很多缓冲区没有对应的文件。emacs 可以同时打开多个文件,也就是说能同时有很多个缓冲区存在,但是在任何时候都只有一个缓冲区称为当前缓冲区(current buffer)。即使在 lisp 编程中也是如此。许多编辑和移动的命令只能针对当前缓冲区。\n缓冲区的名字 emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。下面的函数列表中如果参数是 buffer-or-name 则是能同时接受缓冲区对象和缓冲区名的函数,否则只能接受一种参数。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。\n要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 m-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 \u0026lt;序号\u0026gt; 的方式使名字变得不同。你也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名。\n当前缓冲区 当前缓冲区可以用 current-buffer 函数得到。当前缓冲区不一定是显示在屏幕上的那个缓冲区,你可以用 set-buffer 来指定当前缓冲区。但是需要注意的是,当命令返回到命令循环时,光标所在的缓冲区 会自动成为当前缓冲区。这也是单独在 *scratch* 中执行 set-buffer 后并不能改变当前缓冲区,而必须使用 progn 语句同时执行多个语句才能改变当前缓冲区的原因\n(set-buffer \u0026#34;*messages*\u0026#34;) ; =\u0026gt; #\u0026lt;buffer *messages*\u0026gt; (message (buffer-name)) ; =\u0026gt; \u0026#34;*scratch*\u0026#34; (progn (set-buffer \u0026#34;*messages*\u0026#34;) (message (buffer-name))) ; \u0026#34;*messages*\u0026#34; 但是你不能依赖命令循环来把当前缓冲区设置成使用 set-buffer 之前的。因为这个命令很可以会被另一个程序员来调用。你也不能直接用 set-buffer 设置成原来的缓冲区,比如\n(let (buffer-read-only (obuf (current-buffer))) (set-buffer ...) ... (set-buffer obuf)) 因为 set-buffer 不能处理错误或退出情况。正确的作法是使用 save-current-buffer、with-current-buffer 和 save-excursion 等方法。save-current-buffer 能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区。如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区。lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句。比如 with-current-buffer 使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区\n(with-current-buffer buffer-or-name body) 相当于\n(save-current-buffer (set-buffer buffer-or-name) body) save-excursion 与 save-current-buffer 不同之处在于,它不仅保存当前缓冲区,还保存了当前的位置和 mark。在 *scratch* 缓冲区中运行下面两个语句就能看出它们的差别了\n(save-current-buffer (set-buffer \u0026#34;*scratch*\u0026#34;) (goto-char (point-min)) (set-buffer \u0026#34;*messages*\u0026#34;)) (save-excursion (set-buffer \u0026#34;*scratch*\u0026#34;) (goto-char (point-min)) (set-buffer \u0026#34;*messages*\u0026#34;)) 创建和关闭缓冲区 产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 \u0026lt;n\u0026gt;(n 是一个整数,从 2 开始) 的名字创建新的缓冲区。\n关闭一个缓冲区可以用 kill-buffer。当关闭缓冲区时,如果要用户确认是否要关闭缓冲区,可以加到 kill-buffer-query-functions 里。如果要做一些善后处理,可以用 kill-buffer-hook。\n通常一个接受缓冲区作为参数的函数都需要参数所指定的缓冲区是存在的。如果要确认一个缓冲区是否依然还存在可以使用 buffer-live-p。\n要对所有缓冲区进行某个操作,可以用 buffer-list 获得所有缓冲区的列表。\n如果你只是想使用一个临时的缓冲区,而不想先建一个缓冲区,使用结束后又需要关闭这个缓冲区,可以用 with-temp-buffer 这个宏。从这个宏的名字可以看出,它所做的事情是先新建一个临时缓冲区,并把这个缓冲区作为当前缓冲区,使用结束后,关闭这个缓冲区,并恢复之前的缓冲区为当前缓冲区。\n在缓冲区内移动 在学会移动函数之前,先要理解两个概念:位置(position)和标记(mark)。位置是指某个字符在缓冲区内的下标,它从 1 开始。更准确的说位置是在两个字符之间,所以有在位置之前的字符和在位置之后的字符之说。但是通常我们说在某个位置的字符都是指在这个位置之后的字符。\n标记和位置的区别在于位置会随文本插入和删除而改变位置。一个标记包含了缓冲区和位置两个信息。在插入和删除缓冲区里的文本时,所有的标记都会检查一遍,并重新设置位置。这对于含有大量标记的缓冲区处理是很花时间的,所以当你确认某个标记不用的话应该释放这个标记。\n创建一个标记使用函数 make-marker。这样产生的标记不会指向任何地方。你需要用 set-marker 命令来设置标记的位置和缓冲区\n(setq foo (make-marker)) ; =\u0026gt; #\u0026lt;marker in no buffer\u0026gt; (set-marker foo (point)) ; =\u0026gt; #\u0026lt;marker at 3594 in *scratch*\u0026gt; 也可以用 point-marker 直接得到 point 处的标记。或者用 copy-marker 复制一个标记或者直接用位置生成一个标记\n(point-marker) ; =\u0026gt; #\u0026lt;marker at 3516 in *scratch*\u0026gt; (copy-marker 20) ; =\u0026gt; #\u0026lt;marker at 20 in *scratch*\u0026gt; (copy-marker foo) ; =\u0026gt; #\u0026lt;marker at 3502 in *scratch*\u0026gt; 如果要得一个标记的内容,可以用 marker-position,marker-buffer\n(marker-position foo) ; =\u0026gt; 3502 (marker-buffer foo) ; =\u0026gt; #\u0026lt;buffer *scratch*\u0026gt; 位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。\n和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 point。point 是一个特殊的缓冲区位置,许多命令在这个位置进行文本插入。每个缓冲区都有一个 point 值,它总是比函数 point-min 大,比另一个函数 point-max 返回值小。注意,point-min 的返回值不一定是 1,point-max 的返回值也不定是比缓冲区大小函数 buffer-size 的返回值大 1 的数,因为 emacs 可以把一个缓冲区缩小(narrow)到一个区域,这时 point-min 和 point-max 返回值就是这个区域的起点和终点位置。所以要得到 point 的范围,只能用这两个函数,而不能用 1 和 buffer-size 函数。\n和 point 类似,有一个特殊的标记称为 \u0026ldquo;the mark\u0026rdquo;。它指定了一个区域的文本用于某些命令,比如 kill-region,indent-region。可以用 mark 函数返回当前 mark 的值。如果使用 transient-mark-mode,而且 mark-even-if-inactive 值是 nil 的话,在 mark 没有激活时(也就是 mark-active 的值为 nil),调用 mark 函数会产生一个错误。如果传递一个参数 force 才能返回当前缓冲区 mark 的位置。mark-marker 能返回当前缓冲区的 mark,这不是 mark 的拷贝,所以设置它的值会改变当前 mark 的值。set-mark 可以设置 mark 的值,并激活 mark。每个缓冲区还维护一个 mark-ring,这个列表里保存了 mark 的前一个值。当一个命令修改了 mark 的值时,通常要把旧的值放到 mark-ring 里。可以用 push-mark 和 pop-mark 加入或删除 mark-ring 里的元素。当缓冲区里 mark 存在且指向某个位置时,可以用 region-beginning 和 region-end 得到 point 和 mark 中较小的和较大的值。当然如果使用 transient-mark-mode 时,需要激活 mark,否则会产生一个错误。\n思考题\n写一个命令,对于使用 transient-mark-mode 时,当选中一个区域时显示区域 的起点和终点,否则显示 point-min 和 point-max 的位置。如果不使用 transient-mark-mode,则显示 point 和 mark 的位置。\n按单个字符位置来移动的函数主要使用 goto-char 和 forward-char、backward-char。前者是按缓冲区的绝对位置移动,而后者是按 point 的偏移位置移动比如\n(goto-char (point-min)) ; 跳到缓冲区开始位置 (forward-char 10) ; 向前移动 10 个字符 (forward-char -10) ; 向后移动 10 个字符 可能有一些写 elisp 的人没有读文档或者贪图省事,就在写的 elisp 里直接用 beginning-of-buffer 和 end-of-buffer 来跳到缓冲区的开头和末尾,这其实是不对的。因为这两个命令还做了其它事情,比如设置标记等等。同样,还有一些函数都是不推荐在 elisp 中使用的,如果你准备写一个要发布 elisp,还是要注意一下。\n按词移动使用 forward-word 和 backward-word。至于什么是词,这就要看语法表格的定义了。\n按行移动使用 forward-line。没有 backward-line。forward-line 每次移动都是移动到行首的。所以,如果要移动到当前行的行首,使用 (forward-line 0)。如果不想移动就得到行首和行尾的位置,可以用 line-beginning-position 和 line-end-position。得到当前行的行号可以用 line-number-at-pos。需要注意的是这个行号是从当前状态下的行号,如果使用 narrow-to-region 或者用 widen 之后都有可能改变行号。\n由于 point 只能在 point-min 和 point-max 之间,所以 point 位置测试有时是很重要的,特别是在循环条件测试里。常用的测试函数是 bobp(beginning of buffer predicate)和 eobp(end of buffer predicate)。对于行位置测试使用 bolp(beginning of line predicate)和 eolp(end of line predicate)。\n缓冲区的内容 要得到整个缓冲区的文本,可以用 buffer-string 函数。如果只要一个区间的文本,使用 buffer-substring 函数。point 附近的字符可以用 char-after 和 char-before 得到。point 处的词可以用 current-word 得到,其它类型的文本,比如符号,数字,s 表达式等等,可以用 thing-at-point 函数得到。\n思考题\n参考 thing-at-point 写一个命令标记光标处的 s 表达式。这个命令和 mark-sexp 不同的是,它能从整个 s 表达式的开始标记。\n修改缓冲区的内容 要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。\n插入文本最常用的命令是 insert。它可以插入一个或者多个字符串到当前缓冲区的 point 后。也可以用 insert-char 插入单个字符。插入另一个缓冲区的一个区域使用 insert-buffer-substring。\n删除一个或多个字符使用 delete-char 或 delete-backward-char。删除一个区间使用 delete-region。如果既要删除一个区间又要得到这部分的内容使用 delete-and-extract-region,它返回包含被删除部分的字符串。\n最常用的查找函数是 re-search-forward 和 re-search-backward。这两个函数参数如下\n(re-search-forward regexp \u0026amp;optional bound noerror count) (re-search-backward regexp \u0026amp;optional bound noerror count) 其中 bound 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward),noerror 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil。count 是指定查找匹配的次数。\n替换一般都是在查找之后进行,也是使用 replace-match 函数。和字符串的替换不同的是不需要指定替换的对象了。\n思考题\n从 openoffice 字处理程序里拷贝到 emacs 里的表格通常都是每一个单元格就是一行的。写一个命令,让用户输入表格的列数,把选中区域转换成用制表符分隔的表格。\n函数列表 (buffer-name \u0026amp;optional buffer) (rename-buffer newname \u0026amp;optional unique) (generate-new-buffer-name name \u0026amp;optional ignore) (current-buffer) (set-buffer buffer-or-name)) (save-current-buffer \u0026amp;rest body) (with-current-buffer buffer-or-name \u0026amp;rest body) (save-excursion \u0026amp;rest body) (get-buffer-create name) (generate-new-buffer name) (kill-buffer buffer-or-name) (buffer-live-p object) (buffer-list \u0026amp;optional frame) (with-temp-buffer \u0026amp;rest body) (make-marker) (set-marker marker position \u0026amp;optional buffer) (point-marker) (copy-marker marker \u0026amp;optional type) (marker-position marker) (marker-buffer marker) (point) (point-min) (point-max) (buffer-size \u0026amp;optional buffer) (mark \u0026amp;optional force) (mark-marker) (set-mark pos) (push-mark \u0026amp;optional location nomsg activate) (pop-mark) (region-beginning) (region-end) (goto-char position) (forward-char \u0026amp;optional n) (backward-char \u0026amp;optional n) (beginning-of-buffer \u0026amp;optional arg) (end-of-buffer \u0026amp;optional arg) (forward-word \u0026amp;optional arg) (backward-word \u0026amp;optional arg) (forward-line \u0026amp;optional n) (line-beginning-position \u0026amp;optional n) (line-end-position \u0026amp;optional n) (line-number-at-pos \u0026amp;optional pos) (narrow-to-region start end) (widen) (bobp) (eobp) (bolp) (eolp) (buffer-string) (buffer-substring start end) (char-after \u0026amp;optional pos) (char-before \u0026amp;optional pos) (current-word \u0026amp;optional strict really-word) (thing-at-point thing) (insert \u0026amp;rest args) (insert-char character count \u0026amp;optional inherit) (insert-buffer-substring buffer \u0026amp;optional start end) (delete-char n \u0026amp;optional killflag) (delete-backward-char n \u0026amp;optional killflag) (delete-region start end) (delete-and-extract-region start end) (re-search-forward regexp \u0026amp;optional bound noerror count) (re-search-backward regexp \u0026amp;optional bound noerror count) 问题解答 可选择区域也可不选择区域的命令 (defun show-region (beg end) (interactive (if (or (null transient-mark-mode) mark-active) (list (region-beginning) (region-end)) (list (point-min) (point-max)))) (message \u0026#34;region start from %d to %d\u0026#34; beg end)) 这是通常那种如果选择区域则对这个区域应用命令,否则对整个缓冲区应用命令的方法。我喜欢用 transient-mark-mode,因为它让这种作用于区域的命令更灵活。当然也有人反对,无所谓了,emacs 本身就是很个性化的东西。\n标记整个 s 表达式 (defun mark-whole-sexp () (interactive) (let ((bound (bounds-of-thing-at-point \u0026#39;sexp))) (if bound (progn (goto-char (car bound)) (set-mark (point)) (goto-char (cdr bound))) (message \u0026#34;no sexp found at point!\u0026#34;)))) 学习过程中应该可以看看其它一些函数是怎样实现的,从这些源代码中常常能学到很多有用的技巧和方法。比如要标记整个 s 表达式,联想到 thing-at-point 能得到整个 s 表达式,那自然能得到整个 s 表达式的起点和终点了。所以看看 thing-at-point 的实现,一个很简单的函数,一眼就能发现其中最关键的函数是 bounds-of-thing-at-point,它能返回某个语法实体(syntactic entity)的起点和终点。这样这个命令就很容易就能写出来了。从这个命令中还应该注意到的是对于错误应该很好的处理,让用户明白发生什么错了。比如这里,如果当前光标下没有 s 表达式时,bound 变量为 nil,如果不进行判断,会出现错误:\nwrong type argument: integer-or-marker-p, nil 加上这个判断,用户就明白发生什么事了。\noowriter 表格转换 实现这个目的有多种方法:\n一行一行移动,删除回车,替换成制表符 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (setq col (1- col)) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (dotimes (i col) (forward-line 1) (backward-delete-char 1) (insert-char ?\\t 1)) (forward-line 1))))) 用 subst-char-in-region 函数直接替换 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (subst-char-in-region (point) (progn (forward-line col) (1- (point))) ?\\n ?\\t))))) 用 re-search-forward 和 replace-match 查找替 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (let (start bound) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (setq start (point)) (forward-line col) (setq bound (copy-marker (1- (point)))) (goto-char start) (while (re-search-forward \u0026#34;\\n\u0026#34; bound t) (replace-match \u0026#34;\\t\u0026#34;)) (goto-char (1+ bound))))))) 之所以要给出这三种方法,是想借此说明 elisp 编程其实要实现一个目的通常有 很多种方法,选择一种适合的方法。比如这个问题较好的方法是使用第二种方法, 前提是你要知道有 subst-char-in-region 这个函数,这就要求你对 emacs 提供 的内置的函数比较熟悉了,没有别的办法,只有自己多读 elisp manual,如果你 真想学习 elisp 的话,读 manual 还是值得的,我每读一遍都会有一些新的发 现。如果你不知道这个函数,只知道常用的函数,那么相比较而言,第一种方法 是比较容易想到,也比较容易实现的。但是事实上第三种方法才是最重要的方法, 因为这个方法是适用范围最广的。试想一下你如果不是替换两个字符,而是字符 串的话,前面两种方法都没有办法使用了,而这个方法只要稍微修改就能用了。\n另外,需要特别说明的是这个命令中 bound 使用的是一个标记而不是一个位置, 如果替换的字符串和删除的字符串是相同长度的,当前用什么都可以,否则就要 注意了,因为在替换之后,边界就有可能改变。这也是写查找替换的函数中很容 易出现的一个错误。解决的办法,一是像我这样用一个标记来记录边界位置。另 一种就是用 narrow-to-region 的方法,先把缓冲区缩小到查找替换的区域,结 束后用 widen 展开。当然为了省事,可以直接用 save-restriction。\n操作对象之二 \u0026ndash; 窗口 首先还是要定义一下什么是窗口(window)。窗口是屏幕上用于显示一个缓冲区 的部分。和它要区分开来的一个概念是 frame。frame 是 emacs 能够使用屏幕的 部分。可以用窗口的观点来看 frame 和窗口,一个 frame 里可以容纳多个(至 少一个)窗口,而 emacs 可以有多个 frame。(可能需要和通常所说的窗口的概 念要区分开来,一般来说,我们所说的其它程序的窗口更类似于 emacs 的一个 frame,所以也有人认为这里 window 译为窗格更好一些。但是窗格这个词是一个 生造出来的词,我还是用窗口比较顺一些,大家自己注意就行了。)在任何时候, 都有一个被选中的 frame,而在这个 frame 里有一个被选中的窗口,称为选择的 窗口(selected window)。\n分割窗口 刚启动时,emacs 都是只有一个 frame 一个窗口。多个窗口都是用分割窗口的函 数生成的。分割窗口的内建函数是 split-window。这个函数的参数如下:\n(split-window \u0026amp;optional window size horizontal) 这个函数的功能是把当前或者指定窗口进行分割,默认分割方式是水平分割,可 以将参数中的 horizontal 设置为 non-nil 的值,变成垂直分割。如果不指定 大小,则分割后两个窗口的大小是一样的。分割后的两个窗口里的缓冲区是同 一个缓冲区。使用这个函数后,光标仍然在原窗口,而返回的新窗口对象:\n(selected-window) ; =\u0026gt; #\u0026lt;window 136 on *scratch*\u0026gt; (split-window) ; =\u0026gt; #\u0026lt;window 138 on *scratch*\u0026gt; 需要注意的是,窗口的分割也需要用树的结构来看分割后的窗口,比如这样一个过程:\n+---------------+ +---------------+\r| | | | |\r| win1 | | win1 | win2 |\r| | --\u0026gt; | | |\r| | | | |\r+---------------+ +---------------+\r|\rv\r+---------------+ +---------------+\r| win1 | | | | |\r| | win2 | | win1 | win2 |\r|--------| | \u0026lt;-- |-------| |\r| 3 | 4 | | | win3 | |\r| | | | | | |\r+---------------+ +---------------+ 可以看成是这样一种结构:\n(win1) -\u0026gt; (win1 win2) -\u0026gt; ((win1 win3) win2) -\u0026gt; ((win1 (win3 win4)) win2) 事实上可以用 window-tree 函数得到当前窗口的结构,如果忽略 minibuffer 对应的窗口,得到的应该类似这样的一个结果:\n(nil (0 0 170 42) (t (0 0 85 42) #\u0026lt;win 3\u0026gt; (nil (0 21 85 42) #\u0026lt;win 8\u0026gt; #\u0026lt;win 10\u0026gt;)) #\u0026lt;win 6\u0026gt;) window-tree 返回值的第一个元素代表子窗口的分割方式,nil 表示水平分割, t 表示垂直分割。第二个元素代表整个结构的大小,这四个数字可以看作是左上 和右下两个顶点的坐标。其余元素是子窗口。每个子窗口也是同样的结构。所以 把前面这个列表还原成窗口排列应该是这样:\n(0,0) +-------------------+\r| | |\r| win 3 | win6 |\r| | |\r(0,21) |---------| |\r| | | |\r| 8 | 10 | |\r| | | |\r+-------------------+ (170, 42)\r(85, 42) 由上面的图可以注意到由 window-tree 返回的结果一些窗口的大小不能确定, 比较上面的 win 8 和 win 10 只能知道它们合并起来的大小,不能确定它们分 别的宽度是多少。\n删除窗口 如果要让一个窗口不显示在屏幕上,要使用 delete-window 函数。如果没有指定 参数,删除的窗口是当前选中的窗口,如果指定了参数,删除的是这个参数对应 的窗口。删除的窗口多出来的空间会自动加到它的邻接的窗口中。如果要删除除 了当前窗口之外的窗口,可以用 delete-other-windows 函数。\n当一个窗口不可见之后,这个窗口对象也就消失了\n(setq foo (selected-window)) ; =\u0026gt; #\u0026lt;window 90 on *scratch*\u0026gt; (delete-window) (windowp foo) ; =\u0026gt; t (window-live-p foo) ; =\u0026gt; nil 窗口配置 窗口配置 (window configuration) 包含了 frame 中所有窗口的位置信息:窗口 大小,显示的缓冲区,缓冲区中光标的位置和 mark,还有 fringe,滚动条等等。 用 current-window-configuration 得到当前窗口配置,用 set-window-configuration 来还原\n(setq foo (current-window-configuration)) ;; do sth to make some changes on windows (set-window-configuration foo) 选择窗口 可以用 selected-window 得到当前光标所在的窗口\n(selected-window) ; =\u0026gt; #\u0026lt;window 104 on *scratch*\u0026gt; 可以用 select-window 函数使某个窗口变成选中的窗口\n(progn (setq foo (selected-window)) (message \u0026#34;original window: %s\u0026#34; foo) (other-window 1) (message \u0026#34;current window: %s\u0026#34; (selected-window)) (select-window foo) (message \u0026#34;back to original window: %s\u0026#34; foo)) 两个特殊的宏可以保存窗口位置执行语句:save-selected-window 和 with-selected-window。它们的作用是在执行语句结束后选择的窗口仍留在执行 语句之前的窗口。with-selected-window 和 save-selected-window 几乎相同, 只不过 save-selected-window 选择了其它窗口。这两个宏不会保存窗口的位置 信息,如果执行语句结束后,保存的窗口已经消失,则会选择最后一个选择的窗 口。\n;; 让另一个窗口滚动到缓冲区开始 (save-selected-window (select-window (next-window)) (goto-char (point-min))) 当前 frame 里所有的窗口可以用 window-list 函数得到。可以用 next-window 来得到在 window-list 里排在某个 window 之后的窗口。对应的用 previous-window 得到排在某个 window 之前的窗口。\n(selected-window) ; =\u0026gt; #\u0026lt;window 245 on *scratch*\u0026gt; (window-list) ;; =\u0026gt; (#\u0026lt;window 245 on *scratch*\u0026gt; #\u0026lt;window 253 on *scratch*\u0026gt; #\u0026lt;window 251 on *info*\u0026gt;) (next-window) ; =\u0026gt; #\u0026lt;window 253 on *scratch*\u0026gt; (next-window (next-window)) ; =\u0026gt; #\u0026lt;window 251 on *info*\u0026gt; (next-window (next-window (next-window))) ; =\u0026gt; #\u0026lt;window 245 on *scratch*\u0026gt; walk-windows 可以遍历窗口,相当于 (mapc proc (window-list))。 get-window-with-predicate 用于查找符合某个条件的窗口。\n窗口大小信息 窗口是一个长方形区域,所以窗口的大小信息包括它的高度和宽度。用来度量窗 口大小的单位都是以字符数来表示,所以窗口高度为 45 指的是这个窗口可以容 纳 45 行字符,宽度为 140 是指窗口一行可以显示 140 个字符。\nmode line 和 header line 都包含在窗口的高度里,所以有 window-height 和 window-body-height 两个函数,后者返回把 mode-line 和 header line 排除后 的高度。\n(window-height) ; =\u0026gt; 45 (window-body-height) ; =\u0026gt; 44 滚动条和 fringe 不包括在窗口的亮度里,window-width 返回窗口的宽度\n(window-width) ; =\u0026gt; 72 也可以用 window-edges 返回各个顶点的坐标信息\n(window-edges) ; =\u0026gt; (0 0 73 45) window-edges 返回的位置信息包含了滚动条、fringe、mode line、header line 在内,window-inside-edges 返回的就是窗口的文本区域的位置\n(window-inside-edges) ; =\u0026gt; (1 0 73 44) 如果需要的话也可以得到用像素表示的窗口位置信息\n(window-pixel-edges) ; =\u0026gt; (0 0 511 675) (window-inside-pixel-edges) ; =\u0026gt; (7 0 511 660) 思考题\ncurrent-window-configuration 可以将当前窗口的位置信 息保存到一个变量中以便将来恢复窗口。但是这个对象没有读入形式,所以不 能保存到文件中。请写一个函数可以把当前窗口的位置信息生成一个列表,然 后用一个函数就能从这个列表恢复窗口。提示:这个列表结构用窗口的分割顺 序表示。比如用这样一个列表表示对应的窗口:\n;; +---------------+\r;; | | | |\r;; |-------| |\r;; | | |\r;; +---------------+\r;; =\u0026gt;\r(horizontal 73\r(vertical 22\r(horizontal 36 win win)\rwin)\rwin) 窗口对应的缓冲区 窗口对应的缓冲区可以用 window-buffer 函数得到:\n(window-buffer) ; =\u0026gt; #\u0026lt;buffer *scratch*\u0026gt; (window-buffer (next-window)) ; =\u0026gt; #\u0026lt;buffer *info*\u0026gt; 缓冲区对应的窗口也可以用 get-buffer-window 得到。如果有多个窗口显示同一 个缓冲区,那这个函数只能返回其中的一个,由 window-list 决定。如果要得到 所有的窗口,可以用 get-buffer-window-list\n(get-buffer-window (get-buffer \u0026#34;*scratch*\u0026#34;)) ;; =\u0026gt; #\u0026lt;window 268 on *scratch*\u0026gt; (get-buffer-window-list (get-buffer \u0026#34;*scratch*\u0026#34;)) ;; =\u0026gt; (#\u0026lt;window 268 on *scratch*\u0026gt; #\u0026lt;window 270 on *scratch*\u0026gt;) 让某个窗口显示某个缓冲区可以用 set-window-buffer 函数。 让选中窗口显示某个缓冲区也可以用 switch-to-buffer,但是一般不要在 elisp 编程中用这个命令,如果需要让某个缓冲区成为当前缓冲区使用 set-buffer 函数,如果要让当前窗口显示某个缓冲区,使用 set-window-buffer 函数。\n让一个缓冲区可见可以用 display-buffer。默认的行为是当缓冲区已经显示在某 个窗口中时,如果不是当前选中窗口,则返回那个窗口,如果是当前选中窗口, 且如果传递的 not-this-window 参数为 non-nil 时,会新建一个窗口,显示缓 冲区。如果没有任何窗口显示这个缓冲区,则新建一个窗口显示缓冲区,并返回 这个窗口。display-buffer 是一个比较高级的命令,用户可以通过一些变量来改 变这个命令的行为。比如控制显示的 pop-up-windows, display-buffer-reuse-frames,pop-up-frames,控制新建窗口高度的 split-height-threshold,even-window-heights,控制显示的 frame 的 special-display-buffer-names,special-display-regexps, special-display-function,控制是否应该显示在当前选中窗口 same-window-buffer-names,same-window-regexps 等等。如果这些还不能满 足你的要求(事实上我觉得已经太复杂了),你还可以自己写一个函数,将 display-buffer-function 设置成这个函数。\n思考题\n前一个思考题只能还原窗口,不能还原缓冲区。请修改一下使它能保存缓冲区信息,还原时让对应的窗口显示对应的缓冲区。\n改变窗口显示区域 每个窗口会保存一个显示缓冲区的起点位置,这个位置对应于窗口左上角光标在 缓冲区里的位置。可以用 window-start 函数得到某个窗口的起点位置。可以通 过 set-window-start 来改变显示起点位置。可以通过 pos-visible-in-window-p 来检测缓冲区中某个位置是否是可见的。 但是直接通过 set-window-start 来控制显示比较容易出现错误,因为 set-window-start 并不会改变 point 所在的位置,在窗口调用 redisplay 函 数之后 point 会跳到相应的位置。如果你确实有这个需要,我建议还是用: (with-selected-window window (goto-char pos)) 来代替。\n函数列表 (windowp object) (split-window \u0026amp;optional window size horflag) (selected-window) (window-tree \u0026amp;optional frame) (delete-window \u0026amp;optional window) (delete-other-windows \u0026amp;optional window) (current-window-configuration \u0026amp;optional frame) (set-window-configuration configuration) (other-window arg \u0026amp;optional all-frames) (save-selected-window \u0026amp;rest body) (with-selected-window window \u0026amp;rest body) (window-list \u0026amp;optional frame minibuf window) (next-window \u0026amp;optional window minibuf all-frames) (previous-window \u0026amp;optional window minibuf all-frames) (walk-windows proc \u0026amp;optional minibuf all-frames) (get-window-with-predicate predicate \u0026amp;optional minibuf all-frames default) (window-height \u0026amp;optional window) (window-body-height \u0026amp;optional window) (window-width \u0026amp;optional window) (window-edges \u0026amp;optional window) (window-inside-edges \u0026amp;optional window) (window-pixel-edges \u0026amp;optional window) (window-inside-pixel-edges \u0026amp;optional window) (window-buffer \u0026amp;optional window) (get-buffer-window buffer-or-name \u0026amp;optional frame) (get-buffer-window-list buffer-or-name \u0026amp;optional minibuf frame) (set-window-buffer window buffer-or-name \u0026amp;optional keep-margins) (switch-to-buffer buffer-or-name \u0026amp;optional norecord) (display-buffer buffer-or-name \u0026amp;optional not-this-window frame) (window-start \u0026amp;optional window) (set-window-start window pos \u0026amp;optional noforce) 问题解答 保存窗口位置信息 这是我的答案。欢迎提出改进意见\n(defun my-window-tree-to-list (tree) (if (windowp tree) \u0026#39;win (let ((dir (car tree)) (children (cddr tree))) (list (if dir \u0026#39;vertical \u0026#39;horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (\u0026gt; (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-window-width (win) (if (windowp win) (window-width win) (let ((edge (cadr win))) (- (nth 2 edge) (car edge))))) (defun my-window-height (win) (if (windowp win) (window-height win) (let ((edge (cadr win))) (- (nth 3 edge) (cadr edge))))) (defun my-list-to-window-tree (conf) (when (listp conf) (let (newwin) (setq newwin (split-window nil (cadr conf) (eq (car conf) \u0026#39;horizontal))) (my-list-to-window-tree (nth 2 conf)) (select-window newwin) (my-list-to-window-tree (nth 3 conf))))) (defun my-restore-window-configuration (winconf) (delete-other-windows) (my-list-to-window-tree winconf)) (defun my-current-window-configuration () (my-window-tree-to-list (car (window-tree)))) ;; test code here (setq foo (my-current-window-configuration)) ;; do sth to change windows (my-restore-window-configuration foo) 改进的保存窗口信息的函数 由于缓冲区对象也是没有读入形式的,所以返回的列表里只能用缓冲区名来代表 缓冲区,只要没有修改过缓冲区的名字,就能正确的还原缓冲区。如果对于访问 文件的缓冲区,使用文件名可能是更好的想法。保存信息只要对 my-window-tree-to-list 函数做很小的修改就能用了。而恢复窗口则要做较大 改动。my-list-to-window-tree 加了一个函数参数,这样这个函数的可定制性 更高一些。\n(defun my-window-tree-to-list (tree) (if (windowp tree) (buffer-name (window-buffer tree)) (let ((dir (car tree)) (children (cddr tree))) (list (if dir \u0026#39;vertical \u0026#39;horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (\u0026gt; (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-list-to-window-tree (conf set-winbuf) (let ((newwin (split-window nil (cadr conf) (eq (car conf) \u0026#39;horizontal)))) (if (eq (car conf) \u0026#39;horizontal) (progn (funcall set-winbuf (selected-window) (nth 2 conf)) (select-window newwin) (if (listp (nth 3 conf)) (my-list-to-window-tree (nth 3 conf)) (funcall set-winbuf newwin (nth 3 conf)))) (if (listp (nth 2 conf)) (my-list-to-window-tree (nth 2 conf)) (funcall set-winbuf (selected-window) (nth 2 conf))) (select-window newwin) (funcall set-winbuf newwin (nth 3 conf))))) (defun my-restore-window-configuration (winconf) (let ((buf (current-buffer))) (delete-other-windows) (my-list-to-window-tree winconf (lambda (win name) (set-window-buffer win (or (get-buffer name) buf)))))) 操作对象之三 \u0026ndash; 文件 作为一个编辑器,自然文件是最重要的操作对象之一。这一节要介绍有关文件的一系列命令,比如查找文件,读写文件,文件信息、读取目录、文件名操作等。\n打开文件的过程 当你打开一个文件时,实际上 emacs 做了很多事情:\n把文件名展开成为完整的文件名 判断文件是否存在 判断文件是否可读或者文件大小是否太大 查看文件是否已经打开,是否被锁定 向缓冲区插入文件内容 设置缓冲区的模式 这还只是简单的一个步骤,实际情况比这要复杂的多,许多异常需要考虑。而且 为了所有函数的可扩展性,许多变量、handler 和 hook 加入到文件操作的函数 中,使得每一个环节都可以让用户或者 elisp 开发者可以定制,甚至完全接管 所有的文件操作。\n这里需要区分两个概念:文件和缓冲区。它们是两个不同的对象,文件是在计算 机上可持久保存的信息,而缓冲区是 emacs 中包含文件内容信息的对象,在 emacs 退出后就会消失,只有当保存缓冲区之后缓冲区里的内容才写到文件中去。\n文件读写 打开一个文件的命令是 find-file。这命令使一个缓冲区访问某个文件,并让这 个缓冲区成为当前缓冲区。在打开文件过程中会调用 find-file-hook。 find-file-noselect 是所有访问文件的核心函数。与 find-file 不同,它只返 回访问文件的缓冲区。这两个函数都有一个特点,如果 emacs 里已经有一个缓冲 区访问这个文件的话,emacs 不会创建另一个缓冲区来访问文件,而只是简单返 回或者转到这个缓冲区。怎样检查有没有缓冲区是否访问某个文件呢?所有和文 件关联的缓冲区里都有一个 buffer-local 变量 buffer-file-name。但是不要直 接设置这个变量来改变缓冲区关联的文件。而是使用 set-visited-file-name 来 修改。同样不要直接从 buffer-list 里搜索 buffer-file-name 来查找和某个文 件关联的缓冲区。应该使用 get-file-buffer 或者 find-buffer-visiting。\n(find-file \u0026#34;~/temp/test.txt\u0026#34;) (with-current-buffer (find-file-noselect \u0026#34;~/temp/test.txt\u0026#34;) buffer-file-name) ; =\u0026gt; \u0026#34;/home/ywb/temp/test.txt\u0026#34; (find-buffer-visiting \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; #\u0026lt;buffer test.txt\u0026gt; (get-file-buffer \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; #\u0026lt;buffer test.txt\u0026gt; 保存一个文件的过程相对简单一些。首先创建备份文件,处理文件的位模式,将 缓冲区写入文件。保存文件的命令是 save-buffer。相当于其它编辑器里另存为 的命令是 write-file。在这个过程中会调用一些函数或者 hook。 write-file-functions 和 write-contents-functions 几乎功能完全相同。它们 都是在写入文件之前运行的函数,如果这些函数中有一个返回了 non-nil 的值, 则会认为文件已经写入了,后面的函数都不会运行,而且也不会使用再调用其它 写入文件的函数。这两个变量有一个重要的区别是 write-contents-functions 在 改变主模式之后会被修改,因为它没有 permanent-local 属性,而 write-file-functions 则会仍然保留。before-save-hook 和 write-file-functions 功能也比较类似,但是这个变量里的函数会逐个执行,不 论返回什么值也不会影响后面文件的写入。after-save-hook 是在文件已经写入 之后才调用的 hook,它是 save-buffer 最后一个动作。\n但是实际上在 elisp 编程过程中经常遇到的一个问题是读取一个文件中的内容, 读取完之后并不希望这个缓冲区还留下来,如果直接用 kill-buffer 可能会把 用户打开的文件关闭。而且 find-file-noselect 做的事情实在超出我们的需要 的。这时你可能需要的是更底层的文件读写函数,它们是 insert-file-contents 和 write-region,调用形式分别是\n(insert-file-contents filename \u0026amp;optional visit beg end replace) (write-region start end filename \u0026amp;optional append visit lockname mustbenew) insert-file-contents 可以插入文件中指定部分到当前缓冲区中。如果指定 visit 则会标记缓冲区的修改状态并关联缓冲区到文件,一般是不用的。 replace 是指是否要删除缓冲区里其它内容,这比先删除缓冲区其它内容后插入文 件内容要快一些,但是一般也用不上。insert-file-contents 会处理文件的编 码,如果不需要解码文件的话,可以用 insert-file-contents-literally。\nwrite-region 可以把缓冲区中的一部分写入到指定文件中。如果指定 append 则是添加到文件末尾。和 insert-file-contents 相似,visit 参数也会把缓冲 区和文件关联,lockname 则是文件锁定的名字,mustbenew 确保文件存在时会 要求用户确认操作。\n思考题\n写一个函数提取出某个 c 头文件中的函数声明中的函数名和声明位置。\n文件信息 文件是否存在可以使用 file-exists-p 来判断。对于目录和一般文件都可以用 这个函数进行判断,但是符号链接只有当目标文件存在时才返回 t。\n如何判断文件是否可读或者可写呢?file-readable-p、file-writable-p, file-executable-p 分用来测试用户对文件的权限。文件的位模式还可以用 file-modes 函数得到。\n(file-exists-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-readable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-writable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-executable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; nil (format \u0026#34;%o\u0026#34; (file-modes \u0026#34;~/temp/test.txt\u0026#34;)) ; =\u0026gt; \u0026#34;644\u0026#34; 文件类型判断可以使用 file-regular-p、file-directory-p、file-symlink-p, 分别判断一个文件名是否是一个普通文件(不是目录,命名管道、终端或者其它 io 设备)、文件名是否一个存在的目录、文件名是否是一个符号链接。其中 file-symlink-p 当文件名是一个符号链接时会返回目标文件名。文件的真实名 字也就是除去相对链接和符号链接后得到的文件名可以用 file-truename 得到。 事实上每个和文件关联的 buffer 里也有一个缓冲区局部变量 buffer-file-truename 来记录这个文件名。\n$ ls -l t.txt lrwxrwxrwx 1 ywb ywb 8 2007-07-15 15:51 t.txt -\u0026gt; test.txt (file-regular-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; t (file-directory-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; nil (file-symlink-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; \u0026#34;test.txt\u0026#34; (file-truename \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; \u0026#34;/home/ywb/temp/test.txt\u0026#34; 文件更详细的信息可以用 file-attributes 函数得到。这个函数类似系统的 stat 命令,返回文件几乎所有的信息,包括文件类型,用户和组用户,访问日 期、修改日期、status change 日期、文件大小、文件位模式、inode number、 system number。这是我写的方便使用的帮助函数:\n(defun file-stat-type (file \u0026amp;optional id-format) (car (file-attributes file id-format))) (defun file-stat-name-number (file \u0026amp;optional id-format) (cadr (file-attributes file id-format))) (defun file-stat-uid (file \u0026amp;optional id-format) (nth 2 (file-attributes file id-format))) (defun file-stat-gid (file \u0026amp;optional id-format) (nth 3 (file-attributes file id-format))) (defun file-stat-atime (file \u0026amp;optional id-format) (nth 4 (file-attributes file id-format))) (defun file-stat-mtime (file \u0026amp;optional id-format) (nth 5 (file-attributes file id-format))) (defun file-stat-ctime (file \u0026amp;optional id-format) (nth 6 (file-attributes file id-format))) (defun file-stat-size (file \u0026amp;optional id-format) (nth 7 (file-attributes file id-format))) (defun file-stat-modes (file \u0026amp;optional id-format) (nth 8 (file-attributes file id-format))) (defun file-stat-guid-changep (file \u0026amp;optional id-format) (nth 9 (file-attributes file id-format))) (defun file-stat-inode-number (file \u0026amp;optional id-format) (nth 10 (file-attributes file id-format))) (defun file-stat-system-number (file \u0026amp;optional id-format) (nth 11 (file-attributes file id-format))) (defun file-attr-type (attr) (car attr)) (defun file-attr-name-number (attr) (cadr attr)) (defun file-attr-uid (attr) (nth 2 attr)) (defun file-attr-gid (attr) (nth 3 attr)) (defun file-attr-atime (attr) (nth 4 attr)) (defun file-attr-mtime (attr) (nth 5 attr)) (defun file-attr-ctime (attr) (nth 6 attr)) (defun file-attr-size (attr) (nth 7 attr)) (defun file-attr-modes (attr) (nth 8 attr)) (defun file-attr-guid-changep (attr) (nth 9 attr)) (defun file-attr-inode-number (attr) (nth 10 attr)) (defun file-attr-system-number (attr) (nth 11 attr)) 前一组函数是直接由文件名访问文件信息,而后一组函数是由 file-attributes 的返回值来得到文件信息。\n修改文件信息 重命名和复制文件可以用 rename-file 和 copy-file。删除文件使用 delete-file。创建目录使用 make-directory 函数。不能用 delete-file 删除 目录,只能用 delete-directory 删除目录。当目录不为空时会产生一个错误。\n设置文件修改时间使用 set-file-times。设置文件位模式可以用 set-file-modes 函数。set-file-modes 函数的参数必须是一个整数。你可以用位 函数 logand、logior 和 logxor 函数来进行位操作。\n思考题\n写一个函数模拟 chmod 命令的行为。\n文件名操作 虽然 mswin 的文件名使用的路径分隔符不同,但是这里介绍的函数都能用于 mswin 形式的文件名,只是返回的文件名都是 unix 形式了。路径一般由目录和 文件名,而文件名一般由主文件名 (basename)、文件名后缀和版本号构成。 emacs 有一系列函数来得到路径中的不同部分\n(file-name-directory \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/\u0026#34; (file-name-nondirectory \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;test.txt\u0026#34; (file-name-sans-extension \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test\u0026#34; (file-name-extension \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;txt\u0026#34; (file-name-sans-versions \u0026#34;~/temp/test.txt~\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test.txt\u0026#34; (file-name-sans-versions \u0026#34;~/temp/test.txt.~1~\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test.txt\u0026#34; 路径如果是从根目录开始的称为是绝对路径。测试一个路径是否是绝对路径使用 file-name-absolute-p。如果在 unix 或 gnu/linux 系统,以 ~ 开头的路径也是绝对路径。在 mswin 上,以 \u0026ldquo;/\u0026rdquo; 、 \u0026ldquo;\u0026quot;、\u0026ldquo;x:\u0026rdquo; 开头的路径都是绝对路径。如果不是绝对路径,可以使用 expand-file-name 来得到绝对路径。把一个绝对路径转换成相对某个路径的相 对路径的可以用 file-relative-name 函数。\n(file-name-absolute-p \u0026#34;~rms/foo\u0026#34;) ; =\u0026gt; t (file-name-absolute-p \u0026#34;/user/rms/foo\u0026#34;) ; =\u0026gt; t (expand-file-name \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;/home/ywb/foo\u0026#34; (expand-file-name \u0026#34;foo\u0026#34; \u0026#34;/usr/spool/\u0026#34;) ; =\u0026gt; \u0026#34;/usr/spool/foo\u0026#34; (file-relative-name \u0026#34;/foo/bar\u0026#34; \u0026#34;/foo/\u0026#34;) ; =\u0026gt; \u0026#34;bar\u0026#34; (file-relative-name \u0026#34;/foo/bar\u0026#34; \u0026#34;/hack/\u0026#34;) ; =\u0026gt; \u0026#34;../foo/bar\u0026#34; 对于目录,如果要将其作为目录,也就是确保它是以路径分隔符结束,可以用 file-name-as-directory。不要用 (concat dir \u0026ldquo;/\u0026rdquo;) 来转换,这会有移植问题。 和它相对应的函数是 directory-file-name\n(file-name-as-directory \u0026#34;~rms/lewis\u0026#34;) ; =\u0026gt; \u0026#34;~rms/lewis/\u0026#34; (directory-file-name \u0026#34;~lewis/\u0026#34;) ; =\u0026gt; \u0026#34;~lewis\u0026#34; 如果要得到所在系统使用的文件名,可以用 convert-standard-filename。比如 在 mswin 系统上,可以用这个函数返回用 \u0026ldquo;\u0026quot; 分隔的文件名\n(convert-standard-filename \u0026#34;c:/windows\u0026#34;) ;=\u0026gt; \u0026#34;c:\\\\windows\u0026#34; 临时文件 如果需要产生一个临时文件,可以使用 make-temp-file。这个函数按给定前缀产 生一个不和现有文件冲突的文件,并返回它的文件名。如果给定的名字是一个相 对文件名,则产生的文件名会用 temporary-file-directory 进行扩展。也可以 用这个函数产生一个临时文件夹。如果只想产生一个不存在的文件名,可以用 make-temp-name 函数\n(make-temp-file \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;/tmp/foo5611dxf\u0026#34; (make-temp-name \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;foo5611q7l\u0026#34; 读取目录内容 可以用 directory-files 来得到某个目录中的全部或者符合某个正则表达式的 文件名。\n(directory-files \u0026#34;~/temp/dir/\u0026#34;) ;; =\u0026gt; ;; (\u0026#34;#foo.el#\u0026#34; \u0026#34;.\u0026#34; \u0026#34;.#foo.el\u0026#34; \u0026#34;..\u0026#34; \u0026#34;foo.el\u0026#34; \u0026#34;t.pl\u0026#34; \u0026#34;t2.pl\u0026#34;) (directory-files \u0026#34;~/temp/dir/\u0026#34; t) ;; =\u0026gt; ;; (\u0026#34;/home/ywb/temp/dir/#foo.el#\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/.\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/.#foo.el\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/..\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/foo.el\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/t.pl\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/t2.pl\u0026#34;) (directory-files \u0026#34;~/temp/dir/\u0026#34; nil \u0026#34;\\\\.pl$\u0026#34;) ; =\u0026gt; (\u0026#34;t.pl\u0026#34; \u0026#34;t2.pl\u0026#34;) directory-files-and-attributes 和 directory-files 相似,但是返回的列表 中包含了 file-attributes 得到的信息。file-name-all-versions 用于得到某 个文件在目录中的所有版本,file-expand-wildcards 可以用通配符来得到目录 中的文件列表。\n思考题\n写一个函数返回当前目录包括子目录中所有文件名。\n神奇的 handle 如果不把文件局限在存储在本地机器上的信息,如果有一套基本的文件操作,比 如判断文件是否存在、打开文件、保存文件、得到目录内容之类,那远程的文件 和本地文件的差别也仅在于文件名表示方法不同而已。在 emacs 里,底层的文件 操作函数都可以托管给 elisp 中的函数,这样只要用 elisp 实现了某种类型文 件的基本操作,就能像编辑本地文件一样编辑其它类型文件了。\n决定何种类型的文件名使用什么方式来操作是在 file-name-handler-alist 变 量定义的。它是由形如 (regexp . handler) 的列表。如果文件名匹配这个 regexp 则使用 handler 来进行相应的文件操作。这里所说的文件操作,具体的 来说有这些函数:\n`access-file\u0026#39;, `add-name-to-file\u0026#39;, `byte-compiler-base-file-name\u0026#39;,\r`copy-file\u0026#39;, `delete-directory\u0026#39;, `delete-file\u0026#39;,\r`diff-latest-backup-file\u0026#39;, `directory-file-name\u0026#39;, `directory-files\u0026#39;,\r`directory-files-and-attributes\u0026#39;, `dired-call-process\u0026#39;,\r`dired-compress-file\u0026#39;, `dired-uncache\u0026#39;,\r`expand-file-name\u0026#39;, `file-accessible-directory-p\u0026#39;, `file-attributes\u0026#39;,\r`file-directory-p\u0026#39;, `file-executable-p\u0026#39;, `file-exists-p\u0026#39;,\r`file-local-copy\u0026#39;, `file-remote-p\u0026#39;, `file-modes\u0026#39;,\r`file-name-all-completions\u0026#39;, `file-name-as-directory\u0026#39;,\r`file-name-completion\u0026#39;, `file-name-directory\u0026#39;, `file-name-nondirectory\u0026#39;,\r`file-name-sans-versions\u0026#39;, `file-newer-than-file-p\u0026#39;,\r`file-ownership-preserved-p\u0026#39;, `file-readable-p\u0026#39;, `file-regular-p\u0026#39;,\r`file-symlink-p\u0026#39;, `file-truename\u0026#39;, `file-writable-p\u0026#39;,\r`find-backup-file-name\u0026#39;, `find-file-noselect\u0026#39;,\r`get-file-buffer\u0026#39;, `insert-directory\u0026#39;, `insert-file-contents\u0026#39;,\r`load\u0026#39;, `make-auto-save-file-name\u0026#39;, `make-directory\u0026#39;,\r`make-directory-internal\u0026#39;, `make-symbolic-link\u0026#39;,\r`rename-file\u0026#39;, `set-file-modes\u0026#39;, `set-file-times\u0026#39;,\r`set-visited-file-modtime\u0026#39;, `shell-command\u0026#39;, `substitute-in-file-name\u0026#39;,\r`unhandled-file-name-directory\u0026#39;, `vc-registered\u0026#39;,\r`verify-visited-file-modtime\u0026#39;,\r`write-region\u0026#39;. 在 handle 里,可以只接管部分的文件操作,其它仍交给 emacs 原来的函数来完 成。举一个简单的例子。比如最新版本的 emacs 把 *scratch* 的 auto-save-mode 打开了。如果你不想这个缓 冲区的自动保存的文件名散布得到处都是,可以想办法让这个缓冲区的自动保存 文件放到指定的目录中。刚好 make-auto-save-file-name 是在上面这个列表里 的,但是不幸的是在函数定义里 make-auto-save-file-name 里不对不关联文件 的缓冲区使用 handler(我觉得是一个 bug 呀),继续往下看,发现生成保存文 件名是使用了 expand-file-name 函数,所以解决办法就产生了:\n(defun my-scratch-auto-save-file-name (operation \u0026amp;rest args) (if (and (eq operation \u0026#39;expand-file-name) (string= (car args) \u0026#34;#*scratch*#\u0026#34;)) (expand-file-name (concat \u0026#34;~/.emacs.d/backup/\u0026#34; (car args))) (let ((inhibit-file-name-handlers (cons \u0026#39;my-scratch-auto-save-file-name (and (eq inhibit-file-name-operation operation) inhibit-file-name-handlers))) (inhibit-file-name-operation operation)) (apply operation args)))) 函数列表 (find-file filename \u0026amp;optional wildcards) (find-file-noselect filename \u0026amp;optional nowarn rawfile wildcards) (set-visited-file-name filename \u0026amp;optional no-query along-with-file) (get-file-buffer filename) (find-buffer-visiting filename \u0026amp;optional predicate) (save-buffer \u0026amp;optional args) (insert-file-contents filename \u0026amp;optional visit beg end replace) (insert-file-contents-literally filename \u0026amp;optional visit beg end replace) (write-region start end filename \u0026amp;optional append visit lockname mustbenew) (file-exists-p filename) (file-readable-p filename) (file-writable-p filename) (file-executable-p filename) (file-modes filename) (file-regular-p filename) (file-directory-p filename) (file-symlink-p filename) (file-truename filename) (file-attributes filename \u0026amp;optional id-format) (rename-file file newname \u0026amp;optional ok-if-already-exists) (copy-file file newname \u0026amp;optional ok-if-already-exists keep-time preserve-uid-gid) (delete-file filename) (make-directory dir \u0026amp;optional parents) (delete-directory directory) (set-file-modes filename mode) (file-name-directory filename) (file-name-nondirectory filename) (file-name-sans-extension filename) (file-name-sans-versions name \u0026amp;optional keep-backup-version) (file-name-absolute-p filename) (expand-file-name name \u0026amp;optional default-directory) (file-relative-name filename \u0026amp;optional directory) (file-name-as-directory file) (directory-file-name directory) (convert-standard-filename filename) (make-temp-file prefix \u0026amp;optional dir-flag suffix) (make-temp-name prefix) (directory-files directory \u0026amp;optional full match nosort) (dired-files-attributes dir) 问题解答 提取头文件中函数名 这是我写的一个版本,主要是函数声明的正则表达式不好写,函数是很简单的。 从这个例子也可以看出它错误的把那个 typedef void 当成函数声明了。如果你 知道更好的正则表达式,请告诉我一下。\n(defvar header-regexp-list \u0026#39;((\u0026#34;^\\\\(?:\\\\(?:g_const_return\\\\|extern\\\\|const\\\\)\\\\s-+\\\\)?[a-za-z][_a-za-z0-9]*\\ \\\\(?:\\\\s-*[*]*[ \\t\\n]+\\\\|\\\\s-+[*]*\\\\)\\\\([a-za-z][_a-za-z0-9]*\\\\)\\\\s-*(\u0026#34; . 1) (\u0026#34;^\\\\s-*#\\\\s-*define\\\\s-+\\\\([a-za-z][_a-za-z0-9]*\\\\)\u0026#34; . 1))) (defun parse-c-header (file) \u0026#34;extract function name and declaration position using `header-regexp-list\u0026#39;.\u0026#34; (interactive \u0026#34;fheader file: \\np\u0026#34;) (let (info) (with-temp-buffer (insert-file-contents file) (dolist (re header-regexp-list) (goto-char (point-min)) (while (re-search-forward (car re) nil t) (push (cons (match-string (cdr re)) (line-beginning-position)) info)))) info)) (parse-c-header \u0026#34;/usr/include/glib-2.0/gmodule.h\u0026#34;) ;; =\u0026gt; ;; ((\u0026#34;g_module_name\u0026#34; . 1788) ;; (\u0026#34;g_module_open\u0026#34; . 1747) ;; (\u0026#34;g_module_export\u0026#34; . 1396) ;; (\u0026#34;g_module_export\u0026#34; . 1317) ;; (\u0026#34;g_module_import\u0026#34; . 1261) ;; (\u0026#34;g_module_build_path\u0026#34; . 3462) ;; (\u0026#34;g_module_name\u0026#34; . 2764) ;; (\u0026#34;g_module_symbol\u0026#34; . 2570) ;; (\u0026#34;g_module_error\u0026#34; . 2445) ;; (\u0026#34;g_module_make_resident\u0026#34; . 2329) ;; (\u0026#34;g_module_close\u0026#34; . 2190) ;; (\u0026#34;g_module_open\u0026#34; . 2021) ;; (\u0026#34;g_module_supported\u0026#34; . 1894) ;; (\u0026#34;void\u0026#34; . 1673)) 模拟 chmod 的函数 这是一个改变单个文件模式的 chmod 版本。递归版本的就自己作一个练习吧。最 好不要直接调用这个函数,因为每次调用都要解析一次 mode 参数,想一个只解 析一次的方法吧。\n(defun chmod (mode file) \u0026#34;a elisp function to simulate command chmod. note that the command chmod can accept mode match `[ugoa]*([-+=]([rwxxst]*|[ugo]))+\u0026#39;, but this version only can process mode match `[ugoa]*[-+=]([rwx]*|[ugo])\u0026#39;. \u0026#34; (cond ((integerp mode) (if (\u0026gt; mode #o777) (error \u0026#34;unknown mode option: %d\u0026#34; mode))) ((string-match \u0026#34;^[0-7]\\\\{3\\\\}$\u0026#34; mode) (setq mode (string-to-number mode 8))) ((string-match \u0026#34;^\\\\([ugoa]*\\\\)\\\\([-+=]\\\\)\\\\([rwx]*\\\\|[ugo]\\\\)$\u0026#34; mode) (let ((users (append (match-string 1 mode) nil)) (mask-func (string-to-char (match-string 2 mode))) (bits (append (match-string 3 mode) nil)) (oldmode (file-modes file)) (user-list \u0026#39;((?a . #o777) (?u . #o700) (?g . #o070) (?o . #o007))) mask) (when bits (setq bits (* (cond ((= (car bits) ?u) (lsh (logand oldmode #o700) -6)) ((= (car bits) ?g) (lsh (logand oldmode #o070) -3)) ((= (car bits) ?o) (logand oldmode #o007)) (t (+ (if (member ?r bits) 4 0) (if (member ?w bits) 2 0) (if (member ?x bits) 1 0)))) #o111)) (if users (setq mask (apply \u0026#39;logior (delq nil (mapcar (lambda (u) (assoc-default u user-list)) users)))) (setq mask #o777)) (setq mode (cond ((= mask-func ?\\=) (logior (logand bits mask) (logand oldmode (logxor mask #o777)))) ((= mask-func ?\\+) (logior oldmode (logand bits mask))) (t (logand oldmode (logxor (logand bits mask) #o777)))))))) (t (error \u0026#34;unknow mode option: %s\u0026#34; mode))) (set-file-modes file mode)) 列出目录中所有文件 为了让这个函数更类似 directory-files 函数,我把参数设置为和它一样的:\n(defun my-directory-all-files (dir \u0026amp;optional full match nosort) (apply \u0026#39;append (delq nil (mapcar (lambda (file) (if (and (not (string-match \u0026#34;^[.]+$\u0026#34; (file-name-nondirectory file))) (file-directory-p (expand-file-name file dir))) (if full (my-directory-all-files file full match nosort) (mapcar (lambda (f) (concat (file-name-as-directory file) f)) (my-directory-all-files (expand-file-name file dir) full match nosort))) (if (string-match match file) (list file)))) (directory-files dir full nil nosort))))) 操作对象之四 \u0026ndash; 文本 文本的插入删除,查找替换操作已经在缓冲区一节中讲过了。这一节主要介绍文 本属性。\n如果使用过其它图形界面的文本组件进行编程,它们对于文本的高亮一般都是采 用给对应文本贴上相应标签的方法。emacs 的处理方法也是类似的,但是相比之 下,要强大的多。在 emacs 里,在不同位置上的每个字符都可以有一个属性列表。 这个属性列表和符号的属性列表很相似,都是由一个名字和值构成的对组成。名 字和值都可以是一个 lisp 对象,但是通常名字都是一个符号,这样可以用这个 符号来查找相应的属性值。复制文本通常都会复制相应的字符的文本属性,但是 也可以用相应的函数只复制文本字符串,比如 substring-no-properties、 insert-buffer-substring-no-properties、buffer-substring-no-properties。\n产生一个带属性的字符串可以用 propertize 函数\n(propertize \u0026#34;abc\u0026#34; \u0026#39;face \u0026#39;bold) ; =\u0026gt; #(\u0026#34;abc\u0026#34; 0 3 (face bold)) 如果你在一个 text-mode 的缓冲区内用 m-x eval-expression 用 insert 函数 插入前面这个字符串,就会发现插入的文本已经是粗体字了。之所以不能在 *scratch* 产生这种效果,是因为通常我们是开启了 font-lock-mode,在 font-lock-mode 里,文本的 face 属性是实时计算出来的。 在插入文本之后,它的 face 属性已经很快地被改变了。你可以在关闭 font-lock-mode 后再测试一次应该是可以看到 *scratch* 里也是可以用这种方法插入带 face 属性的文本的。\n虽然文本属性的名字可以是任意的,但是一些名字是有特殊含义的。\n属性名 含义 category 值必须是一个符号,这个符号的属性将作为这个字符的属性 face 控制文本的字体和颜色 font-lock-face 和 face 相似,可以作为 font-lock-mode 中静态文本的 face mouse-face 当鼠标停在文本上时的文本 face fontified 记录是否使用 font lock 标记了 face display 改变文本的显示方式,比如高、低、长短、宽窄,或者用图片代替 help-echo 鼠标停在文本上时显示的文字 keymap 光标或者鼠标在文本上时使用的按键映射 local-map 和 keymap 类似,通常只使用 keymap syntax-table 字符的语法表 read-only 不能修改文本,通过 stickness 来选择可插入的位置 invisible 不显示在屏幕上 intangible 把文本作为一个整体,光标不能进入 field 一个特殊标记,有相应的函数可以操作带这个标记的文本 cursor (不知道具体用途) pointer 修改鼠标停在文本上时的图像 line-spacing 新的一行的距离 line-height 本行的高度 modification-hooks 修改这个字符时调用的函数 insert-in-front-hooks 与 modification-hooks 相似,在字符前插入调用的函数 insert-behind-hooks 与 modification-hooks 相似,在字符后插入调用的函数 point-entered 当光标进入时调用的函数 point-left 当光标离开时调用的函数 composition 将多个字符显示为一个字形 正是由于 emacs 的文本有如此丰富的属性,使得 emacs 里的文字才变得多彩, 变得人性化。\n查看文本属性 由于字符串和缓冲区都可以有文本属性,所以下面的函数通常不提供特定参数就是检 查当前缓冲区的文本属性,如果提供文本对象,则是操作对应的文本属性。\n查看文本对象在某处的文本属性可以用 get-text-property 函数。\n(setq foo (concat \u0026#34;abc\u0026#34; (propertize \u0026#34;cde\u0026#34; \u0026#39;face \u0026#39;bold))) ; =\u0026gt; #(\u0026#34;abccde\u0026#34; 3 6 (face bold)) (get-text-property 3 \u0026#39;face foo) ; =\u0026gt; bold (save-excursion (goto-char (point-min)) (insert foo)) (get-text-property 4 \u0026#39;face) ; =\u0026gt; bold get-char-property 和 get-text-property 相似,但是它是先查找 overlay 的 文本属性。overlay 是缓冲区文字在屏幕上的显示方式,它属于某个缓冲区,具 有起点和终点,也具有文本属性,可以修改缓冲区对应区域上文本的显示方式。\nget-text-property 是查找某个属性的值,用 text-properties-at 可以得到某 个位置上文本的所有属性。\n修改文本属性 put-text-property 可以给文本对象添加一个属性。比如\n(let ((str \u0026#34;abc\u0026#34;)) (put-text-property 0 3 \u0026#39;face \u0026#39;bold str) str) ; =\u0026gt; #(\u0026#34;abc\u0026#34; 0 3 (face bold)) 和 put-text-property 类似,add-text-properties 可以给文本对象添加一系 列的属性。和 add-text-properties 不同,可以用 set-text-properties 直接 设置文本属性列表。你可以用 =(set-text-properties start end nil)= 来除去 某个区间上的文本属性。也可以用 remove-text-properties 和 remove-list-of-text-properties 来除去某个区域的指定文本属性。这两个函 数的属性列表参数只有名字起作用,值是被忽略的。\n(setq foo (propertize \u0026#34;abcdef\u0026#34; \u0026#39;face \u0026#39;bold \u0026#39;pointer \u0026#39;hand)) ;; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 0 6 (pointer hand face bold)) (set-text-properties 0 2 nil foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 6 (pointer hand face bold)) (remove-text-properties 2 4 \u0026#39;(face nil) foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (pointer hand) 4 6 (pointer hand face bold)) (remove-list-of-text-properties 4 6 \u0026#39;(face nil pointer nil) foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (pointer hand)) 查找文本属性 文本属性通常都是连成一个区域的,所以查找文本属性的函数是查找属性变化的 位置。这些函数一般都不作移动,只是返回查找到的位置。使用这些函数时最好 使用 limit 参数,这样可以提高效率,因为有时一个属性直到缓冲区末尾也没 有变化,在这些文本中可能就是多余的。\nnext-property-change 查找从当前位置起任意一个文本属性发生改变的位置。 next-single-property-change 查找指定的一个文本属性改变的位置。 next-char-property-change 把 overlay 的文本属性考虑在内查找属性发生改 变的位置。next-single-property-change 类似的查找指定的一个考虑 overlay 后文本属性改变的位置。这四个函数都对应有 previous- 开头的函数,用于查 找当前位置之前文本属性改变的位置\n(setq foo (concat \u0026#34;ab\u0026#34; (propertize \u0026#34;cd\u0026#34; \u0026#39;face \u0026#39;bold) (propertize \u0026#34;ef\u0026#34; \u0026#39;pointer \u0026#39;hand))) ;; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (face bold) 4 6 (pointer hand)) (next-property-change 1 foo) ; =\u0026gt; 2 (next-single-property-change 1 \u0026#39;pointer foo) ; =\u0026gt; 4 (previous-property-change 6 foo) ; =\u0026gt; 4 (previous-single-property-change 6 \u0026#39;face foo) ; =\u0026gt; 4 text-property-any 查找区域内第一个指定属性值为给定值的字符位置。 text-property-not-all 和它相反,查找区域内第一个指定属性值不是给定值的 字符位置。\n(text-property-any 0 6 \u0026#39;face \u0026#39;bold foo) ; =\u0026gt; 2 (text-property-any 0 6 \u0026#39;face \u0026#39;underline foo) ; =\u0026gt; nil (text-property-not-all 2 6 \u0026#39;face \u0026#39;bold foo) ; =\u0026gt; 4 (text-property-not-all 2 6 \u0026#39;face \u0026#39;underline foo) ; =\u0026gt; 2 思考题\n写一个命令,可在 text-mode 里用指定模式给选中的文本添加高亮。\n函数列表 (propertize string \u0026amp;rest properties) (get-text-property position prop \u0026amp;optional object) (get-char-property position prop \u0026amp;optional object) (text-properties-at position \u0026amp;optional object) (put-text-property start end property value \u0026amp;optional object) (add-text-properties start end properties \u0026amp;optional object) (set-text-properties start end properties \u0026amp;optional object) (remove-text-properties start end properties \u0026amp;optional object) (remove-list-of-text-properties start end list-of-properties \u0026amp;optional object) (next-property-change position \u0026amp;optional object limit) (next-single-property-change position prop \u0026amp;optional object limit) (next-char-property-change position \u0026amp;optional limit) (next-single-char-property-change position prop \u0026amp;optional object limit) (previous-property-change position \u0026amp;optional object limit) (previous-single-property-change position prop \u0026amp;optional object limit) (previous-char-property-change position \u0026amp;optional limit) (previous-single-char-property-change position prop \u0026amp;optional object limit) (text-property-any start end property value \u0026amp;optional object) (text-property-not-all start end property value \u0026amp;optional object) 问题解答 手工高亮代码 (defun my-fontify-region (beg end mode) (interactive (list (region-beginning) (region-end) (intern (completing-read \u0026#34;which mode to use: \u0026#34; obarray (lambda (s) (and (fboundp s) (string-match \u0026#34;-mode$\u0026#34; (symbol-name s)))) t)))) (let ((buf (current-buffer)) (font-lock-verbose nil) (start 1) face face-list) (set-text-properties beg end \u0026#39;(face nil)) (with-temp-buffer (goto-char (point-min)) (insert-buffer-substring buf beg end) (funcall mode) (font-lock-fontify-buffer) (or (get-text-property start \u0026#39;face) (setq start (next-single-property-change start \u0026#39;face))) (while (and start (\u0026lt; start (point-max))) (setq end (or (next-single-property-change start \u0026#39;face) (point-max)) face (get-text-property start \u0026#39;face)) (and face end (setq face-list (cons (list (1- start) (1- end) face) face-list))) (setq start end))) (when face-list (dolist (f (nreverse face-list)) (put-text-property (+ beg (car f)) (+ beg (cadr f)) \u0026#39;face (nth 2 f)))))) 但是直接从那个临时缓冲区里把整个代码拷贝出来也可以了,但是可能某些情况 下,不好修改当前缓冲区,或者不想把那个模式里其它文本属性拷贝出来,这个 函数还是有用的。当然最主要的用途是演示使用查找和添加文本属性的方法。事 实上这个函数也是我用来高亮 muse 模式里 src 标签内源代码所用的方法。但是 不幸的是 muse 模式里这个函数并不能产生很好的效果,不知道为什么。\n后记 到现在为止,我计划写的 elisp 入门内容已经写完了。如果你都看完看懂这些内 容,我想写一些简单的 elisp 应用应该是没有什么问题了。还有一些比较重要的 内容没有涉及到,我在这列一下,如果你对此有兴趣,可以自己看 elisp manual 里相关章节:\n按键映射 (keymap) 和菜单 minibuffer 和补全 进程 调试 主模式 (major mode) 和从属模式 (minor mode) 定制声明 修正函数 (advising function) 非 ascii 字符 其实看一遍 elisp manual 也是很好的选择。我在写这些文字时就是一边参考 elisp manual 一边写的。写的时候我一直有种不安的感觉,这近 3m 的文字被 我压缩到这么一点点是不是太过份了。在 elisp manual 里一些很重要的说明经 常被我一两句话就带过了,有时根本就没有提到,这会不会让刚学 elisp 的人 误入歧途呢?每每想到这一点,我就想就此停住。但是半途而废总是不好的。所 以我还是决定写完应该写的就好了。其它的再说吧。\n如果你是一个新手,我很想知道你看完这个入门教程的感受。当然如果实在没有 兴趣看,也可以告诉我究竟哪里写的不好。我希望在这份文档上花的时间和精力 没有白费。\n","date":"2023-04-18","permalink":"https://loveminimal.github.io/posts/elisp/","summary":"\u003cp\u003e📕 转载自 \u003ca href=\"http://smacs.github.io/elisp/\"\u003eEmacs Lisp 简明教程 - 水木社区 Emacs 版\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是 叶文彬(水木 ID: happierbee) 写的一份 Emacs Lisp 的教程,深入浅出,非常适合初学者。文档的 TeX 代码及 PDF 文档可在* \u003ca href=\"http://www.newsmth.net/nForum/article/Emacs/58338?s=58338\"\u003e此处下载\u003c/a\u003e* 。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eemacs 的高手不能不会 elisp。但是对于很多人来说 elisp 学习是一个痛苦的历程,至少我是有这样一段经历。因此,我写了这一系列文章,希望能为后来者提供一点捷径。\u003c/p\u003e","title":"emacs lisp"},{"content":"为什么会制作这款 typora 的主题呢?\n📙 virgo , 仓库地址 loveminimal/typora-theme-virgo: a typora theme for relative hugo.\n更新日志 - 2023-04-17 20:48 新增 virgo dark 暗色主题样式 快速开始 现在,它已经上传到了 typora 官方的主题仓库 - virgo ,但是我首次提交 pull request 的时候写错了 homepage 和 download 的链接…… 😅\n我已经提交了新 pull request ,后续官方合并后就没有问题了。幸运的是,它并影响你使用这个主题,毕竟你正在浏览器这个页面。\n官方合并的速度还挺快的,目前 virgo 中的链接已经是正确的了。\n在 偏好设置 / 外观 中点击 打开主题文件夹 ,如下:\n该文件夹下存放着 typora 的主题文件,在文件夹下,执行如下命令:\ngit clone https://github.com/loveminimal/typora-theme-virgo.git 然后,将文件夹 typora-theme-virgo 中的 virgo 文件夹和 virgo.css 、 virgo-dark.css 拷贝一份到当前目录下,重启 typora ,即可选择主题 virgo 、 virgo dark 。\n有点不明白,作者不什么不直接遍历 themes 文件下的所有第一级文件夹,然后指定其中的默认加载样式文件,如此,也方便使用 git 管理啊,省了手动拷贝的这一步。 😶\n为什么需要 之前是在 vscode 中编辑博客文件的,使用体验也很好,基本上没有什么不足之处。那为什么要使用 typora 呢?\n一是, typora 的预览效果十分接近于网页渲染,因为当前的主题就是从站点的主题适配而来的,除了部分借助于 javascript 实现的动态交互,其渲染结果有着 90%+ 的相似性。‘所见即所得’,很不错的体验。\n二是,vscode 中对表格的处理没有 typora 优雅,尤其是中英文混输的时候,光是对齐就让人‘崩溃’,尽管有一些所谓有对齐表格的办法,也是不那么让人满意。\n这是 vscode 中对于 table 的编辑状态,很乱很乱,如下:\n\u0026gt; 这是用 vs 编辑器中的 table\n我们来看看,当前主题中对于 table 的编辑和渲染,如下:\n\u0026gt; typora 下当前主题 virgo 的渲染和编辑\n对比很‘惨烈’! 😈\n最后, typora 对于图片的处理也很优秀,不仅实现了直接从剪切板复制粘贴,最近的更新中还优化了对图片相关操作(改、删)的状态同步。\n其他 typora 内置了几个主题,各有特色,在 themes gallery — typora 中也有不少第三方的精美主题可以选择。总之呢,基本上可以满足各类用户的喜好。\n当前主题目前已经提交 pr 到该主题仓库,具体生效日期不定,其实就算合并成功,你还是得按照 快速开始 中描述的步骤那样使其生效。\n结语 最后,使用了一段时间了,对 typora 做一些简评吧。总体来说,纯文本编辑的话,还是不错的。就个人而言,希望在后续更新中可以改进或开放以下两个方面。\n开放设置项,允许用户快速打开文件的时候,可以选择在当前窗口打开,而不是新开一个窗口。\n可以增强快捷键设置,当前的开放支持太弱了,希望可以增加更多的按键接口,尤其的光标的上下左右移动的映射。编辑的时候,习惯使用 alt + h/j/k/l 来移动了,使用 箭头 的话就不得不移动一下右手……\n","date":"2023-04-04","permalink":"https://loveminimal.github.io/posts/about-typora-theme-virgo/","summary":"\u003cp\u003e为什么会制作这款 Typora 的主题呢?\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://theme.typora.io/theme/Virgo/\"\u003e📙 Virgo\u003c/a\u003e , 仓库地址 \u003ca href=\"https://github.com/loveminimal/typora-theme-virgo\"\u003eloveminimal/typora-theme-virgo: A typora theme for relative hugo.\u003c/a\u003e\u003c/p\u003e","title":"一款 typora 主题"},{"content":"本文不涉及具体的细节,浅谈一下 web 前端的 1 2 3 ……\n前端是什么 在当下的产品开发流程中,大致分为以下几个过程:\n1. 产品设计(需求分析)\r2. 技术选型\r3. 前端负责界面渲染、交互 \u0026amp; 后端负责数据处理\r4. 测试(功能)\r5. 运维部署 这里,我们只关注第 3 点,即具体的开发过程。\n要理解‘什么是前端’,最好回顾一下 web 的发展史。这是世界上第一个网站 http://info.cern.ch/ ,于 1991.8.6 上网,它解释了万维网是什么,如何使用网页浏览器和如何建立一个网页服务器等等。\n你会发现,这个页面就是一份简单的 .html 文件,让我们来看一下它的实现过程。\n- 伯纳斯·李(互联网之父)写了一个 html 文档,\r- 然后把这个文档放在连接着网络的一台电脑上(服务器),\r- 用户电脑通过客户端(通常是浏览器)连网访问它。 看,最初的时候,只有 html ,它就是一个文本文件,没有交互,用户只能浏览。这个时候,也没有什么前后端的概念。开发人员需要做的,就是编写 html 文档,并把它放到可以被访问的服务器上。\n用户或开发人员,感觉单纯的 html 文件在浏览器上显示出来不好看,于是有了 css 来装饰样式(1995),我们进入了 html + css 的时代。\n目前为止,用户还是只能看啊,用户说:“我要交互!我要交互!我要交互!”\n伯纳斯说:“给你表单!给你表单!给你表单!”\n现在用户可以通过表单,输入一些信息(比如登录),点击按钮,来和服务器进行一定的交互了。同样,服务器端也有同样的程序来处理用户提交的表单信息。此时,一切都还是简单的!\n直到有一天,用户又不满意了,大声说:“我要更多的交互!更多!更多!”\n网景公司(火狐浏览器前身)委托 brendan eich 开发了 javascript 语言,用于满足用户的需求。 brendan 只花了 10 天时间,太快了,所以 javascript 有许多的语言缺陷,但又阴差阳错地成了浏览器最钟爱的(内置了的)脚本语言,简直了。有些事,就是那么不讲道理。\n我们现在进入了 html + css + js 的时代!\n你还没说什么是前端呢?\n别着急,就要说了。最初的时候,开发任务不算很重,一个开发人员就可以完成 .html、 .css、 .js 文件及服务器上处理用户请求的程序的编写。\n这时,还是没有区分前端、后端,因为都是一个人写的!\n后来,网页内容越来越丰富,用户的交互越来越多,服务器端处理的请求越来越复杂。于是,程序员 b 对程序员 f 说:“我来处理用户提交的信息,生成展示所需的数据,你负责编写文档(html、css、js 等)文件,把数据插入到 html 中,返回给用户的浏览器来展示。”\n于是乎,程序员 b 只负责处理用户请求,程序员 f 负责页面渲染,提高了效率。慢慢地,也就有了专职于后端(back end)和专职于前端(front end)的方向。\n现在,我们明白了,所谓‘前端’,主要就是负责展示部分,最终的交付便是用于用户端浏览器可以识别的页面文件。\n近些年来,前端发展可谓是如火如荼,页面的交互也越来越复杂,各种框架层出不穷,让人眼花缭乱。但它的本质,从来就没有改变过。最终, 前端的核心,就是生成用户浏览器可以渲染的页面文件。\n重点来了,一是浏览器,二是页面文件。\n浏览器 说起浏览器,大家肯定都不陌生,如 chrome、 edge、 firefox、 ie ……\n浏览器只认识 html !\n如果,深入了解,就会发现浏览器实在是一个复杂的软件,好在我们只需要应用它。更多关于浏览器的渲染细节,可以参考 浏览器 这篇文章。\n对于一位前端开发人员来说,现在是幸福的,因为有了浏览器开发工具(一般现代浏览器通过 f12 打开),你可查看页面的请求过程和渲染细节,很方便地开发和调试项目页面。\n页面文件 再厉害的框架,最终都要编译打包成 html + css + js , 因为浏览器只认这个组合!\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;页面文件\u0026lt;/title\u0026gt; …… \u0026lt;link href=\u0026#34;/path/to/style.css\u0026#34; /\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;关于页面文件\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;description\u0026#34;\u0026gt; \u0026lt;p\u0026gt;再厉害的框架,最终都要编译打包成 `html + css + js` 组合。\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;……\u0026lt;/p\u0026gt; …… \u0026lt;/div\u0026gt; …… \u0026lt;script src=\u0026#34;/path/to/main.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 一个 .html 文件,通过 \u0026lt;link\u0026gt; 标签引入 .css 文件,通过 \u0026lt;script\u0026gt; 标签引入 .js 文件,这就是页面文件的基本结构。\nhtml html ,全称‘超文本标记语言’(hyperext markup language) ,是一种用于创建网页的标准标记语言。\n为什么需要‘标记’呢?\n你直接对浏览器讲中文,它是听不懂的!你必须跟它讲 html 语言,它才能懂你要干什么。\nhtml 语言很简单,你只需要用相应的标签来包裹你想要浏览器渲染的内容,就可以了。比如,段落标签 \u0026lt;p\u0026gt; ,分割线标签 \u0026lt;hr\u0026gt;,标题标签 \u0026lt;title\u0026gt; ,引入样式文件的标签 \u0026lt;link\u0026gt; ,引入脚本文件的标签 \u0026lt;script\u0026gt; 等等。\n在实际学习的过程中,有一些标签很常用,有一些标签几乎不用,边学边用即可!\ncss css,全称‘层叠样式表’(cascading style sheets),是一种用来为结构化文档(如 html)添加样式(字体、间距和颜色等)的计算机语言。\ncss 中最重要的一个概念就是‘选择器’了,即选中你要添加样式的页面元素! ……\njavascript 前端领域, javascript 可谓是绝对的主角(后续简称 js),它是目前主流浏览器(所有)指定的唯一脚本语言。\n最初,它只是为了满足用户日益俱增的交互需求,现在也是,只不过更加的强大和复杂。但本质上,仍然是服务于 html 的,这一点从未改变。\njs 的核心语法很简单,如变量、函数及日常应用过程中常用到的数组、对象等数据结构。\n缺陷很多,但不影响 js 登大雅之堂!可以这样说,精通 js 的过程,就是一场学习如何‘踩坑’的旅程,开个玩笑,其实现在好多了。\n框架 js 是通过什么来指挥浏览器的呢?\n浏览器提供了一系列的接口 api ,与 js 最紧密的便是 dom 了。最初的时候,开发人员也是这样做的,通过 html 编写好页面内容,通过 css 来添加样式,通过 js 操作浏览器提供的 api 接口来操作 dom 。\ndom (document object model) 文档对象模型,是浏览器解析 .html 文件后生成的一个树形结构,用于渲染页面。\n简单的页面,这样做很容易。\n问题就在于,现在的页面越来越不简单了……\n于是框架就上场了!看,有‘需求’,就有‘市场’!\njquery 与其说 jquery 是一个框架,不如说,它是一个函数库。本质上来说,它是对原生 js 的一种封装,用来更方便地操作 dom 。对,你仍然需要操作 dom !\n现在用的不多了,在使用中了解即可。\nvue \u0026amp; react 这两个前端框架,是目前最热门、最流行的前端框架了。除了,实现原理的某些部分有些不同外,其余应用都是大同小异。学习了其中一种,再学习另一种,也会很快。\n这里,我们以 vue 为主,某些方面对比 react 来说。不得不说,vue 对于初学者是更加友好的,性能也很不错!国内来说,流行程度更胜一筹。\n首先,需要明白,vue 本身并没有为 html + css + js 引入什么新的东西,你所编写的 vue 相关的模板文件 .vue、 样式文件 .scss、 逻辑文件 .js ,最终经过编译打包后,还是 html + css + js 这一套组合!\n也就是说,你最终,放在服务器上,供用户访问的页面文件,始终都是 html !\n学习 vue ,究竟学什么? 模板语法!!!\n是的,和你学习 html ,没什么太大的区别,正如你学习 html 是为了让浏览器知道你的说什么,学习 vue 的模板语法也是为了让 vue 相关的编译器、打包器知道你要它们干什么!仅此而已!\n你按着它规定的语法来编写 .vue、 .scss、 .js ,对应的编译器、打包器就知道如何把你编写的内容转成 html + css + js 供浏览器渲染绘制页面。\n我们不止一次提到了编译器和打包器,它们是什么呢?有什么作用?\n编译器\n拿 vue 来说,你编写的是组件是 app.vue ,浏览器是不认识的,经过编译器编译就成了 *.js ,就可以引入到 *.html 中供浏览器解析了。可以认为编译器就是一个‘加工厂’,进去的是 .vue ,出来的是 .js 。\n看,编译器并不是什么神奇的东东,它本质上来说,就是个‘翻译机’。\n编译器不需要应用开发者参与,它是由框架实现的,开发者只需要按着框架给出的‘写法’,编译器就能读懂,并正确转译。\n打包器\n打包器呢?为什么需要打包器?因为随着前端内容越来越复杂,把所有的逻辑写在一个 .js 文件中对于程序员来说是不友好的。比较典型的方式,就是通过模块化编程来避开这一点。也就是说,我们会把交互逻辑分散到不同的 .js 文件中,方便编写和复用。如此,在一个项目中,我们使用的资源分分散在项目的不同层级之中,有 .js ,有 .css 等等。最终使用的时候,打包器会根据它们之间的依赖关系,将这些资源再合并到一起,引入到 .html 中,供浏览器渲染。\n是的,打包器就像一个‘拼接手’,按照资源(模块)的依赖关系,把零碎的‘资源’合成一个整体。\n当前,比较流行的打包器仍然是 webpack ,你需要学习并了解它,同样的,边学习,边使用,在使用中学习。\n另外,你可能也需要了解一下 rollup ,它也是一种打包工具,最新的 vue3 推荐的脚手架 vite (项目初始化工具)就是集成了 rollup 。\n同样的,所有的打包工具,实现方式可能各有不同,但其最终目的和思想,都是大同小异的。\n其他 模块和组件 在 模块化编程 中,我们具体说明过模块相关的概念,可以参考一下。\n什么是模块?什么是组件?\n很多时候,这两个概念的划分不是很清楚,甚至是通用的。比如,一个 .vue 文件,可以称为是一个组件,一个完全的 vue 应用其实就是许多组件组成的‘组件树’。\n模块,多是缩写的 .js 文件,每个模块实现了不同的功能,可以通过相应的方式引入别的模块,及暴露自身供外部模块使用的功能。它是一个封装的概念,比如模块 a 引入了模块 b(当然也可不引入),并包含 1、 2、 3 三个功能,模块 a 可以选择只暴露 1、 3 两个功能给外部使用。\n在编写 vue 组件的过程中,我们常会引用到不同的功能模块,比如,我们引入一个时间模块,用来格式化当前的时间,引入一个排序模块,用来给某个数组进行排序等等。\n预编译器 什么是预编译器?顾名思义,它也是一种编译器,一般用于资源打包前,比如将 .scss 样式文件编译成 .css 文件。 dart 就是目前使用最多的一种 scss 的预编译器,只需要在打包工具中,配置好,打包过程中,就会自动完成这个转译的过程。\n扩展一下,就会发现,程序这个东东经常做这种转来转去的东西。一方面为了程序员编写方便,维护方便,另一方面为了编译器、计算机能理解执行。\n一度连 .html 文件都有相应的编译器,如 jade ,现在不怎么使用了。\n后端是什么 在前后端分离之后,后端方面主要就是接受用户在前端发出的请求,分析处理,该请求数据库的请求数据库,该拒绝响应的拒绝响应,把相应的处理结果返回给前端,前端使用这些数据进行页面渲染或其它处理。\n有一点需要注意的是,无论是后端的服务包,还是前端的项目包,都是需要部署在服务器端的,用户只是通过浏览器访问它们。\n当然,后端开发也经历了一系列的变革,有了不少好用的框架工具,比如 java 领域集大成的 spring 家族框架。\n关于 node node 让 js 也可以在服务器端大展拳脚,它与用在浏览器中的 js 有什么区别呢?其实,语言本身没有什么很大的区别,毕竟基本上都遵守 ecmascript 标准。它们主要是宿主(运行环境)不同,nodejs 的宿主是 v8 ,前端 js 的宿主则是浏览器,仅些而已。\n内容看着不少,其实,刚开始的时候,只需要学习一下 npm 包管理方面的知识就可以了。多数时候,只需要使用它安装项目所需要的第三方工具包。\n学习路线 - html、 css、 js 的基本使用\r- npm 的相关概念\r- vuejs 的模板语法及结构\r- webpack 的了解 结语 以上内容,只是一个大概的、过程性的描述,并没有牵扯到具体的开发细节。但是,理解需要学习什么,为什么学习,也是一件重要的事情。我们对于不熟悉的事物,多数时候有些过于‘敬畏’,这是正常的,一旦揭开了它的面纱,就会发现,一切不过如此。\n","date":"2023-03-28","permalink":"https://loveminimal.github.io/posts/web-front-end/","summary":"\u003cp\u003e本文不涉及具体的细节,浅谈一下 Web 前端的 1 2 3 ……\u003c/p\u003e","title":"前端浅谈"},{"content":"在 《一场“疲惫”的主题制作之旅》 中,已经有了不少博客相关的碎碎念。这里,主要用来浅谈一下当前站点博客系统的 =搭建、编辑及部署 相关的系列流程。\n当前站点,使用 hugo 静态博客生成系统 驱动,部署在个人服务器上。事实上,你可以把生成的站点项目部署在任何可以被访问的地方(比如 github page),它本质上是一个包含了若干 .html 文件及相关静态资源的文件夹。\n更新日志 - 2023-04-05 21:31 添加新的日常编辑方式 - 使用 typora \u0026gt; 更新日志仅是为了记录每篇文章的更新过程\n准备篇 ……\n搭建篇 ……\n编辑篇 这里,我们以当前文章的创建及编辑过程为例。\n创建文章 = 怎么说呢?这个章节写的过于太细节了……好像…… 😅\n我们可以使用以下命令来创建文章:\nhugo new posts/how-do-i-blog/index.md\t# 推荐 # 或 hugo new posts/how-do-i-blog.md\t# 不推荐 这里我们使用第一条命令,该命令会自动生成如下目录层级下 .md 文件。\ncontent\r├── posts\r│ ├── how-do-i-blog\r│ │ ├── imgs\r│ │ │ └── 1aa09c580e674b09e82c722a3689d280012f2ae6e1700e924deeef558347d91a.png\r│ │ └── index.md 为什么不直接使用 how-do-i-blog.md ,而使用 how-do-i-blog/index.md ?\n正如上述目录层级中所反映的,如此方便我们把当前文件所需要的资源(如图片 imgs )都放在当前文章的层级下,方便管理。\n这样做还有额外的好处,我们将在后面 插入图片 的部分进一步说明。\n为什么我们直接手动创建 .md 文件呢?\n事实上,你完全可以手动创建它,但手动创建出的 index.md 文件是空白的,而通过上述命令创建出的 index.md 文件则会包含类似以下内容文件头:\n---\rtitle: \u0026#34;how do i blog\u0026#34;\rdate: 2023-03-06t09:40:02+08:00\rdraft: false\rcategories: [_misc]\rtags: []\rcard: false\rweight: 0\r--- 它们包含了一些 hugo 生成文章时要使用到的信息,我们可以把 title 修改为自己喜欢的文章标题,并为其设置分类和标签等。以下,是我们修改之后的文章头信息:\n---\rtitle: \u0026#34;我是怎么写博客的\u0026#34;\rdate: 2023-03-06t09:40:02+08:00\rdraft: false\rcategories: [关于]\rtags: []\rcard: false\rweight: 0\r--- 我们这里把文章名称改为了 我是怎么写博客的 ,并把它分类到 关于 类别中。\n这些头信息是怎么生成的?在哪里配置它们?\n它位于站点根目录下的 /archetypes/default.md 中,该文件默不存在。一般会启用你所使用主题下的 themes/virgo/archetypes/default.md 文件。\n.\r├── archetypes\r│ ├── default.md 以下是 loveminimal/hugo-theme-virgo 主题中的 default.md 内容:\n---\rtitle: \u0026#34;{{ replace .name \u0026#34;-\u0026#34; \u0026#34; \u0026#34; | title }}\u0026#34;\rdate: {{ .date }}\rdraft: false\rcategories: [_misc]\rtags: []\rcard: false\rweight: 0\r--- 你可以修改它,或者在站点根目录下,创建新的 default.md 文件,并修改为自己喜欢的初始配置。\n插入图片 在文章中插入图片是一个相对高频的操作。在第三方的博客平台中,一般来说直接复制图片并粘贴到要插入的位置就可以了,很方便。而编辑 .md 文件,插入图片就稍微麻烦一些。\n我们通过 ![图片名称](地址链接) 在文章中插入图片,默认情况下,你需要经过:\n1. 搜索图片\r2. 另存图片到本地\r3. 编辑 `![图片名称](地址链接)` 引用\r…… 很繁琐!\n而且还不能控制图片的‘显示’尺寸,需要插入图片数量过多的时候,简直就是一种折磨了。\n有没有一种更好的方式来插入图片呢?\n很幸运,有!\n我平时是使用 vscode 来管理站点内容和编辑 .md 文件的,其中有一款插件很好地解决了这个问题。\n它提供了丰富的自定义设置选项,这里主要用到以下几种:\nmarkdown-image › base: file name format ,设置为 ${hash} ,当然有其它各种格式可选组合; markdown-image › base: image width ,设置为 400 ,默认宽度设为 400px ; markdown-image › local: path ,设置为 ./imgs ,生成的图片放在当前文章同级目录下的 imgs 文件夹中。 这也是上文中我们推荐使用 hugo new posts/how-do-i-blog/index.md 命令来生成文章的原因之一。\n使用该插件,你只需要复制所需要图片(本地或网络图片),并通过其提供的粘贴方式(右键选择)插入到位置即可。如此,你的 .md 文件中,就会插如下内容:\n\u0026lt;img alt=\u0026#34;picture 3\u0026#34; src=\u0026#34;imgs/30737f6467ed6269eed8911b8a915f47b9fed706b8f892efd3271d9b6a76181c.png\u0026#34; width=\u0026#34;400\u0026#34; /\u0026gt; 它会被渲染成下面这张图片,是不是很方便!\n它的原理是什么?\n它会读取你剪切板中刚刚复制的图片数据,在你粘贴的时候,重新生成一份拷贝,并在 .md 文件中,插入对应的图片格式,并引用。真的很方便!🎉\n语法增强 我们在 hugo-theme-virgo 主题中,对 markdown 的语法提供了一些增强功能 - 下划线、文字高亮、标注、折叠板 ,你要以在 《关于 virgo 需要知道的一些事》:标记语法增强 文章中了解它。\n当然,如果你使用其它主题,这些增强的样式是无法生效的,幸运的是,它在其他主题中依然得到渲染 - 使用斜体显示,你并不会丢失你想表达的内容。\n使用 typora 编辑 = 现在的编辑操作就是使用 typora 完成的。 😄\n之前的 vscode 使用的不是很爽吗?为什么切换为 typora 了呢?\nvscode 确实很爽,到目前为止,我也经常使用它。切换到 typora 的原因也很简单,家里的电脑性能不行,新的主机配置还在纠结中……\n更多原因可以查看 一款 typora 主题 中的描述。\nps: vscode 真的是一款非常优秀的编辑器,插件丰富且优质,可扩展性强,可轻可重。\n使用 typora 的感觉怎么样?\n很好!如 一款 typora 主题 中所描述的那样,配合自己制作的主题(由站点样式适配),基本上实现了所见即所得的编辑。另外,typora 本身集成了许多 markdown 相关的快捷键,很直观也很好用,尤其是当你专注于编写内容的时候。\n它还有‘专注模式’和‘打字机模式’,很舒心。\n其内置的图片插入功能也不错,基本上和 插入图片 章节中的实现是相同的,原理没有探究。怎么说呢,typora 上的相对来说,更加符合日常的编辑逻辑,还贴心地增加了可以缩放图片的选项,最最重要的还是‘所见即所得’,你可以实时看到你的图片。\n部署篇 当你想部署你的站点内容到托管平台时,你可能会经过以下步骤:\n1. 执行 `hugo` 命令,生成站点内容,默认放在站点根目录的 `public` 文件夹中;\r2. 复制内容包,上传到托管平台;\r…… 如果,只操作一次的话,不是很复杂,但如果,你的内容更新比较频繁,那就有些烦扰了。内容包的部署方式有很多,各有优缺。\n脚本部署 我们这里,使用脚本部署,一次性配置之后,每次部署只需要执行一条简单的命令即可。分享出来,供大家参考使用。\n我们的站点目录如下所示:\n.\r├── config.toml\r├── content\r│ ├── about\r│ ├── archive.md\r│ ├── _index.md\r│ ├── nav\r│ ├── posts\r│ └── search.md\r├── package.json\r├── readme.md\r├── resources\r│ └── _gen\r├── scripts\r│ ├── deploy.sh\r│ └── gitee.sh 其中, scripts/deploy.sh 便是我们定义的部署脚本,其内容如下:\n#!/bin/sh # ------------------- # deploy posts to `loveminimal.github.io` # ------------------- if [ -d \u0026#34;public\u0026#34; ] then # 如果你是部署到 github ,并绑定了域名,那你可能需要启用该行,以 # 保证其正确的指向 # rm -rf \u0026#34;public/cname\u0026#34; \u0026amp;\u0026amp; cp -r \u0026#34;cname\u0026#34; \u0026#34;public/\u0026#34; # 拷贝内容包 public 到一个临时文件夹 .temp ,并 # 用 git 初始化管理该它 cp -r \u0026#34;public\u0026#34; \u0026#34;../.temp\u0026#34; cd \u0026#34;../.temp\u0026#34; pwd git init git add . git commit -m \u0026#34;posts update.\u0026#34; # 添加远程库,引得我们使用的是个人服务器的仓库地址,如果 # 你是托管在 github 上,那么连接的对应的远程库即可 - \u0026lt;your_username\u0026gt;.github.io # 如果你是在 github ‘政治正确’后创建的库,其默认分支为 main, 那你 # 需要 master:main 而不是 master git remote add origin jack@ovirgo.com:/home/jack/.repo/site.git git push -f origin master # git remote add origin https://github.com/loveminimal/loveminimal.github.io.git # git push -f origin master:main # 清除临时文件夹 cd .. rm -rf \u0026#34;.temp\u0026#34; # 返回站点目录 cd \u0026#34;site\u0026#34; fi 如上所示,只需要在站点根目录下,运行 source scripts/deploy.sh 就可以静待站点部署完成了。\n初看,上述脚本初看可能有些混乱,尤其是对初学者来说。但其实,如果你愿意,理论上你需要:\n更改远程仓库地址; 更改站点目录,即可。 也许你会遇到各种各样的问题,但你也会从中收获到不少的乐趣,不是嘛~~\n命令简化 使用 source scripts/deploy.sh 还是有点太复杂了?有如下限制:\n需要进行到站点根目录才可以运行它; 部署后,站点内的内容包并没得到清理。 哈,你只需要多加几条命令即可,如下:\ncd ~/appdata/roaming/site \u0026amp;\u0026amp; rm -rf public \u0026amp;\u0026amp; hugo \u0026amp;\u0026amp; source scripts/deploy.sh \u0026amp;\u0026amp; rm -rf public 如此,无论当前你在那一种路径,都会:\n自动进入站点根目录(此处是 site); 删除站点下旧的 public (如果有的话); 根据当前内容生成新的 public 内容包; 执行部署脚本,发布到对应托管平台; 清理掉生成的 public 内容包。 emm\u0026hellip; 还是长啊,每次都要键入这么长,太麻烦了,怎么办?\n那么你就需要了解一些关于 bash alias 方面的知识了,如下所示,在当前用户家目录下,创建 .bash_aliases 文件(若无),并添加如下内容:\nalias ssd=\u0026#34;cd ~/appdata/roaming/site \u0026amp;\u0026amp; rm -rf public \u0026amp;\u0026amp; hugo \u0026amp;\u0026amp; source scripts/deploy.sh \u0026amp;\u0026amp; rm -rf public\u0026#34; 保存后,在用户家目录下,运行 source .bashrc 命令使 .bash_aliases 中的别名生效。\nok,现在,当你想部署站点的时候,你只需要运行键入 ssd ,回车即可完成部署。\n‘用户家目录’是什么?\n在 windows 下,有两个家目录:\n用户家目录,如 c:\\users\\jack ,一些软件的默认配置会放在该目录下; 用户漫游家目录,如 c:\\users\\jack\\appdata\\roaming ,另一些软件的配置又会放在这个目录下。 😅 微软的东西真的有点混乱哈。\n如果,你使用的是 gnu/linux 系统,那么家目录就只有一个喽,如 /home/jack 。\n","date":"2023-03-06","permalink":"https://loveminimal.github.io/posts/how-do-i-blog/","summary":"\u003cp\u003e在 \u003ca href=\"../a-theme-making-journey\"\u003e《一场“疲惫”的主题制作之旅》\u003c/a\u003e 中,已经有了不少博客相关的碎碎念。这里,主要用来浅谈一下当前站点博客系统的 \u003cem\u003e=搭建、编辑及部署\u003c/em\u003e 相关的系列流程。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/how-do-i-blog/imgs/1aa09c580e674b09e82c722a3689d280012f2ae6e1700e924deeef558347d91a.png\" width=\"300\" /\u003e \r\n\u003cp\u003e当前站点,使用 \u003ca href=\"https://gohugo.io/\"\u003eHugo 静态博客生成系统\u003c/a\u003e 驱动,部署在个人服务器上。事实上,你可以把生成的站点项目部署在任何可以被访问的地方(比如 \u003ccode\u003eGithub Page\u003c/code\u003e),它本质上是一个包含了若干 \u003ccode\u003e.html\u003c/code\u003e 文件及相关静态资源的文件夹。\u003c/p\u003e","title":"我是怎么写博客的"},{"content":"emoji(えもじ 绘文字),就是表情符号,来自日语词汇“絵文字”(假名为“えもじ”,读音即 emoji)。最早是由栗穰崇于 1999 年创作,并在日本网络及手机用户中流行。自苹果公司发布的 ios5 输入法中加入 emoji 后,这种表情符号开始席卷全球,现已被大多数计算机系统所兼容的 unicode 编码采纳,得以普遍运用。\n💡 loveminimal/emojing: emojing - github\n现代浏览器对 emoji 的支持越来越广泛,并且 emoji 也很有趣!\n先前一直使用 emoji 表情符号大全 ,本来布局很紧凑,某天打开就变大了…… 这就很🤕,自己撰一个,以便使用。\n这里,我们实现一个简单的复制 emoji 的页面 🎉 ➭ emojing 。\n用法 它本身就是一个工具页,你可以很方便地使用它 emojing 。\n配置 config.js :\nexport default { en: false } 默认为中文界面,如果你想设置为英文,设置 en: true 即可。\n预览 记录 我们用到了插件 clipboard.js 和 toastify ,并参考完善了 emoji 表情大全_武恩赐的博客 的 emoji 表情集合。\n","date":"2022-11-15","permalink":"https://loveminimal.github.io/posts/emojing/","summary":"\u003cp\u003eEmoji(えもじ 绘文字),就是表情符号,来自日语词汇“絵文字”(假名为“えもじ”,读音即 emoji)。最早是由栗穰崇于 1999 年创作,并在日本网络及手机用户中流行。自苹果公司发布的 ios5 输入法中加入 emoji 后,这种表情符号开始席卷全球,现已被大多数计算机系统所兼容的 Unicode 编码采纳,得以普遍运用。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/loveminimal/emojing\"\u003e💡 loveminimal/emojing: Emojing - GitHub\u003c/a\u003e\u003c/p\u003e","title":"emojing"},{"content":" 🔔 本文摘录自 如何维护更新日志 \u0026ndash; 更新日志绝对不应该是 git 日志的堆砌物\n简介 version 1.0.0\n# changelog\rall notable changes to this project will be documented in this file.\rthe format is based on [keep a changelog](https://keepachangelog.com/en/1.0.0/),\rand this project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).\r## [unreleased]\r## [1.0.0] - 2017-06-20\r### added\r- new visual identity by [@tylerfortune8](https://github.com/tylerfortune8).\r- version navigation.\r- links to latest released version in previous versions.\r- \u0026#34;why keep a changelog?\u0026#34; section.\r- \u0026#34;who needs a changelog?\u0026#34; section.\r- \u0026#34;how do i make a changelog?\u0026#34; section.\r- \u0026#34;frequently asked questions\u0026#34; section.\r- new \u0026#34;guiding principles\u0026#34; sub-section to \u0026#34;how do i make a changelog?\u0026#34;.\r- simplified and traditional chinese translations from [@tianshuo](https://github.com/tianshuo).\r- german translation from [@mpbzh](https://github.com/mpbzh) \u0026amp; [@art4](https://github.com/art4).\r- italian translation from [@azkidenz](https://github.com/azkidenz).\r- swedish translation from [@magol](https://github.com/magol).\r- turkish translation from [@karalamalar](https://github.com/karalamalar).\r- french translation from [@zapashcanon](https://github.com/zapashcanon).\r- brazilian portugese translation from [@webysther](https://github.com/webysther).\r- polish translation from [@amielucha](https://github.com/amielucha) \u0026amp; [@m-aciek](https://github.com/m-aciek).\r- russian translation from [@aishek](https://github.com/aishek).\r- czech translation from [@h4vry](https://github.com/h4vry).\r- slovak translation from [@jkostolansky](https://github.com/jkostolansky).\r- korean translation from [@pierceh89](https://github.com/pierceh89).\r- croatian translation from [@porx](https://github.com/porx).\r- persian translation from [@hameds](https://github.com/hameds).\r- ukrainian translation from [@osadchyi-s](https://github.com/osadchyi-s).\r### changed\r- start using \u0026#34;changelog\u0026#34; over \u0026#34;change log\u0026#34; since it\u0026#39;s the common usage.\r- start versioning based on the current english version at 0.3.0 to help\rtranslation authors keep things up-to-date.\r- rewrite \u0026#34;what makes unicorns cry?\u0026#34; section.\r- rewrite \u0026#34;ignoring deprecations\u0026#34; sub-section to clarify the ideal\rscenario.\r- improve \u0026#34;commit log diffs\u0026#34; sub-section to further argument against\rthem.\r- merge \u0026#34;why can’t people just use a git log diff?\u0026#34; with \u0026#34;commit log\rdiffs\u0026#34;\r- fix typos in simplified chinese and traditional chinese translations.\r- fix typos in brazilian portuguese translation.\r- fix typos in turkish translation.\r- fix typos in czech translation.\r- fix typos in swedish translation.\r- improve phrasing in french translation.\r- fix phrasing and spelling in german translation.\r### removed\r- section about \u0026#34;changelog\u0026#34; vs \u0026#34;changelog\u0026#34;.\r## [0.3.0] - 2015-12-03\r### added\r- ru translation from [@aishek](https://github.com/aishek).\r- pt-br translation from [@tallesl](https://github.com/tallesl).\r- es-es translation from [@zeliosariex](https://github.com/zeliosariex).\r## [0.2.0] - 2015-10-06\r### changed\r- remove exclusionary mentions of \u0026#34;open source\u0026#34; since this project can\rbenefit both \u0026#34;open\u0026#34; and \u0026#34;closed\u0026#34; source projects equally.\r## [0.1.0] - 2015-10-06\r### added\r- answer \u0026#34;should you ever rewrite a change log?\u0026#34;.\r### changed\r- improve argument against commit logs.\r- start following [semver](https://semver.org) properly.\r## [0.0.8] - 2015-02-17\r### changed\r- update year to match in every readme example.\r- reluctantly stop making fun of brits only, since most of the world\rwrites dates in a strange way.\r### fixed\r- fix typos in recent readme changes.\r- update outdated unreleased diff link.\r## [0.0.7] - 2015-02-16\r### added\r- link, and make it obvious that date format is iso 8601.\r### changed\r- clarified the section on \u0026#34;is there a standard change log format?\u0026#34;.\r### fixed\r- fix markdown links to tag comparison url with footnote-style links.\r## [0.0.6] - 2014-12-12\r### added\r- readme section on \u0026#34;yanked\u0026#34; releases.\r## [0.0.5] - 2014-08-09\r### added\r- markdown links to version tags on release headings.\r- unreleased section to gather unreleased changes and encourage note\rkeeping prior to releases.\r## [0.0.4] - 2014-08-09\r### added\r- better explanation of the difference between the file (\u0026#34;changelog\u0026#34;)\rand its function \u0026#34;the change log\u0026#34;.\r### changed\r- refer to a \u0026#34;change log\u0026#34; instead of a \u0026#34;changelog\u0026#34; throughout the site\rto differentiate between the file and the purpose of the file — the\rlogging of changes.\r### removed\r- remove empty sections from changelog, they occupy too much space and\rcreate too much noise in the file. people will have to assume that the\rmissing sections were intentionally left out because they contained no\rnotable changes.\r## [0.0.3] - 2014-08-09\r### added\r- \u0026#34;why should i care?\u0026#34; section mentioning the changelog podcast.\r## [0.0.2] - 2014-07-10\r### added\r- explanation of the recommended reverse chronological release ordering.\r## [0.0.1] - 2014-05-31\r### added\r- this changelog file to hopefully serve as an evolving example of a\rstandardized open source project changelog.\r- cname file to enable github pages custom domain\r- readme now contains answers to common questions about changelogs\r- good examples and basic guidelines, including proper date formatting.\r- counter-examples: \u0026#34;what makes unicorns cry?\u0026#34;\r[unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...head\r[1.0.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...v1.0.0\r[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0\r[0.2.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.1.0...v0.2.0\r[0.1.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.8...v0.1.0\r[0.0.8]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.7...v0.0.8\r[0.0.7]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.6...v0.0.7\r[0.0.6]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.5...v0.0.6\r[0.0.5]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.4...v0.0.5\r[0.0.4]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.3...v0.0.4\r[0.0.3]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.2...v0.0.3\r[0.0.2]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.1...v0.0.2\r[0.0.1]: https://github.com/olivierlacan/keep-a-changelog/releases/tag/v0.0.1 更新日志是什么?\n更新日志(change log)是一个由人工编辑,以时间为倒序的列表, 以记录一个项目中所有版本的显著变动。\n为何要提供更新日志?\n为了让用户和开发人员更简单明确的知晓项目在不同版本之间有哪些显著变动。\n哪些人需要更新日志?\n人人需要更新日志。无论是消费者还是开发者,软件的最终用户都关心软件所包含什么。 当软件有所变动时,大家希望知道改动是为何、以及如何进行的。\n怎样制作高质量的更新日志? 指导原则:\n记住日志是写给人的,而非机器 每个版本都应该有独立的入口 同类改动应该分组放置 版本与章节应该相互对应 新版本在前,旧版本在后 应包括每个版本的发布日期 注明是否遵守 语义化版本格式 变动类型 备注 added 新添加的功能 changed 对现有功能的变更 deprecated 已经不建议使用,准备很快移除的功能 removed 已经移除的功能 fixed 对 bug 的修复 security 对安全的改进 \u0026gt; 表:变动类型\n如何减少维护更新日志的精力? 在文档最上方提供 unreleased 区块以记录即将发布的更新内容。\n这样有两大意义:\n大家可以知道在未来版本中可能会有哪些变更 在发布新版本时,可以直接将 unreleased 区块中的内容移动至新发布版本的描述区块就可以了 有很糟糕的更新日志吗? 当然有,下面就是一些糟糕的方式。\n使用 git 日志\n使用 git 日志作为更新日志是个非常糟糕的方式:git 日志充满各种无意义的信息, 如合并提交、语焉不详的提交标题、文档更新等。\n提交的目的是记录源码的演化。 一些项目会清理提交记录,一些则不会。\n更新日志的目的则是记录重要的变更以供最终受众阅读,而记录范围通常涵盖多次提交。\n无视即将弃用功能\n当从一个版本升级至另一个时,人们应清楚(尽管痛苦)的知道哪些部分将出现问题。 应该允许先升级至一个列出哪些功能将会被弃用的版本,待去掉那些不再支持的部分后, 再升级至把那些弃用功能真正移除的版本。\n即使其他什么都不做,也要在更新日志中列出 derecations,removals 以及其他重大变动。\n易混淆的日期格式\n在美国,人们将月份写在日期的开头 (06-02-2012 对应 2012 年 6 月 2 日), 与此同时世界上其他地方的很多人将至写作 2 june 2012,并拥有不同发音。 2012-06-02 从大到小的排列符合逻辑,并不与其他日期格式相混淆,而且还 符合 iso 标准。因此,推荐在更新日志中采用使用此种日期格式。\n还有更多内容。请通过 发布问题 或发布 pull 请求帮助我收集更多异常模式。\nfaq 是否有一个标准化的更新日志格式?\n并没有。虽然 gnu 提供了更新日志样式指引,以及那个仅有两段长的 gnu news 文件“指南”, 但两者均远远不够。\n此项目意在提供一个 更好的更新日志惯例 所有点子都来自于在开源社区中对优秀实例的观察与记录。\n对于所有建设性批评、讨论及建议,我们都非常 欢迎。\n更新日志文件应被如何命名?\n可以叫做 changelog.md。 一些项目也使用 history、news 或 releases。\n当然,你可以认为更新日志的名字并不是什么要紧事,但是为什么要为难那些仅仅是想看到都有哪些重大变更的最终用户呢?\n对于 github 发布呢?\n这是个非常好的倡议。releases 可通过手动添加发布日志或将带 有注释的 git 标签信息抓取后转换的方式,将简单的 git 标签(如一个叫 v1.0.0 的标签) 转换为信息丰富的发布日志。\ngithub 发布会创建一个非便携、仅可在 github 环境下显示的更新日志。尽管会花费更 多时间,但将之处理成更新日志格式是完全可能的。\n现行版本的 github 发布不像哪些典型的大写文件 (readme, contributing, etc.),仍可以认为是不利于最终用户探索的。 另一个小问题则是界面并不提供不同版本间 commit 日志的链接。\n更新日志可以被自动识别吗?\n非常困难,因为有各种不同的文件格式和命名。\nvandamme 是一个 ruby 程序,由 gemnasium 团队制作,可以解析多种 (但绝对不是全部)开源库的更新日志。\n那些后来撤下的版本怎么办?\n因为各种安全/重大 bug 原因被撤下的版本被标记 \u0026lsquo;yanked\u0026rsquo;。 这些版本一般不出现在更新日志里,但建议他们出现。 显示方式应该是: ## 0.0.5 - 2014-12-13 [yanked] 。\n[yanked] 的标签应该非常醒目。人们应该非常容易就可以注意到他。 并且被方括号所包围也使其更易被程序识别。\n是否可以重写更新日志?\n当然可以。总会有多种多样的原因需要我们去改进更新日志。 对于那些有着未维护更新日志的开源项目,我会定期打开 pull 请求以加入缺失的发布信息。\n另外,很有可能你发现自己忘记记录一个重大功能更新。这种情况下显然你应该去重写更新日志。\n如何贡献?\n本文档并非真理。而是我深思熟虑后的建议,以及我收集的信息与典例。\n我希望我们的社区可以对此达成一致。我相信讨论的过程与最终结果一样重要。\n所以欢迎 贡献.\n访谈 我在 更新日志播客 上讲述了为何维护者与贡献者应关心更新日志, 以及支持我进行此项目的诸多因素。\n","date":"2022-11-02","permalink":"https://loveminimal.github.io/posts/keep-changelog/","summary":"\u003cimg alt=\"picture 14\" src=\"/posts/keep-changelog/imgs/de78a20f0efae9c4f2c0432200dfe721f1007dc1c9af179b9c33d842f20d1e7b.png\" width=\"300\" /\u003e \r\n\u003cp\u003e🔔 本文摘录自 \u003ca href=\"https://keepachangelog.com/zh-CN/1.0.0/\"\u003e如何维护更新日志 \u0026ndash; 更新日志绝对不应该是 git 日志的堆砌物\u003c/a\u003e\u003c/p\u003e","title":"如何维护更新日志"},{"content":" i.e.semantic versioning\n🔔 本文摘录自 语义化版本 2.0.0 | semantic versioning - semver.org\n= emm\u0026hellip; 直接借用文章中的一段话 \u0026ndash; 为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。*\n摘要 版本格式: 主版本号.次版本号.修订号 ,版本号递增规则如下:\n主版本号:当你做了不兼容的 api 修改, 次版本号:当你做了向下兼容的功能性新增, 修订号:当你做了向下兼容的问题修正。 先行版本号及版本编译信息可以加到“主版本号。次版本号。修订号”的后面,作为延伸。\n简介 在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。\n= 在维护陈旧的历史项目时尤为让人烦扰……*\n在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。\n作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。 为了让这套理论运作,你必须先有定义好的公共 api。 这可能包括文档或代码的强制要求。无论如何,这套 api 的清楚明了是十分重要的。一旦你定义了公共 api,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式: x.y.z(主版本号。次版本号。修订号) 修复问题但不影响 api 时,递增修订号;api 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。\n我称这套系统为 _“语义化的版本控制” ,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。\n语义化版本控制规范(semver) 以下关键词 must、must not、required、shall、shall not、should、should not、 recommended、may、optional 依照 rfc 2119 的叙述解读。\n使用语义化版本控制的软件必须(must)定义公共 api。 该 api 可以在代码中被定义或出现于严谨的文档内。无论何种形式都应该力求精确且完整。 标准的版本号必须(must)采用 x.y.z 的格式,其中 x、y 和 z 为非负的整数,且禁止(must not)在数字前方补零。x 是主版本号、y 是次版本号、而 z 为修订号。每个元素必须(must)以数值来递增。例如: 1.9.1 -\u0026gt; 1.10.0 -\u0026gt; 1.11.0。 标记版本号的软件发行后,禁止(must not)改变该版本软件的内容。任何修改都必须(must)以新版本发行。 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 api 不应该被视为稳定版。 1.0.0 的版本号用于界定公共 api 的形成。 这一版本之后所有的版本号更新都基于公共 api 及其修改内容。 修订号 z(x.y.z | x \u0026gt; 0)必须(must)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。 次版本号 y(x.y.z | x \u0026gt; 0)必须(must)在有向下兼容的新功能出现时递增。在任何公共 api 的功能被标记为弃用时也必须(must)递增。也可以(may)在内部程序有大量新功能或改进被加入时递增,其中可以(may)包括修订级别的改变。每当次版本号递增时,修订号必须(must)归零。 主版本号 x(x.y.z | x \u0026gt; 0)必须(must)在有任何不兼容的修改被加入公共 api 时递增。其中可以(may)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(must)归零。 先行版本号可以(may)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(must)由 ascii 字母数字和连接号 [0-9a-za-z-] 组成,且禁止(must not)留白。数字型的标识符禁止(must not)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、 1.0.0-alpha.1、 1.0.0-0.3.7、 1.0.0-x.7.z.92。 版本编译信息可以(may)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(must)由 ascii 字母数字和连接号 [0-9a-za-z-] 组成,且禁止(must not)留白。当判断版本的优先层级时,版本编译信息可(should)被忽略。因此当两个版本只有在版本编译信息有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、 1.0.0+20130313144700、 1.0.0-beta+exp.sha.5114f85。 版本的优先层级指的是不同版本在排序时如何比较。 判断优先层级时,必须(must)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。 由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较。例如:1.0.0 \u0026lt; 2.0.0 \u0026lt; 2.1.0 \u0026lt; 2.1.1。 当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。 有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(must)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定: 只有数字的标识符以数值高低比较。 有字母或连接号时则逐字以 ascii 的排序来比较。 数字的标识符比非数字的标识符优先层级低。 若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。例如:1.0.0-alpha \u0026lt; 1.0.0-alpha.1 \u0026lt; 1.0.0-alpha.beta \u0026lt; 1.0.0-beta \u0026lt; 1.0.0-beta.2 \u0026lt; 1.0.0-beta.11 \u0026lt; 1.0.0-rc.1 \u0026lt; 1.0.0。 合法语义化版本的巴科斯范式语法 巴科斯范式 以美国人巴科斯 (backus) 和丹麦人诺尔 (naur) 的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯-诺尔形式 (backus-naur form)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。\rbnf 表示语法规则的方式为:非终结符用尖括号括起。每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以 “::=” 分开。具有相同左部的规则可以共用一个左部,各右部之间以直竖 “|” 隔开。 = 规范!规范!还是 tmd 规范!*\n\u0026lt;valid semver\u0026gt; ::= \u0026lt;version core\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r\u0026lt;version core\u0026gt; ::= \u0026lt;major\u0026gt; \u0026#34;.\u0026#34; \u0026lt;minor\u0026gt; \u0026#34;.\u0026#34; \u0026lt;patch\u0026gt;\r…… \u0026lt;valid semver\u0026gt; ::= \u0026lt;version core\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r\u0026lt;version core\u0026gt; ::= \u0026lt;major\u0026gt; \u0026#34;.\u0026#34; \u0026lt;minor\u0026gt; \u0026#34;.\u0026#34; \u0026lt;patch\u0026gt;\r\u0026lt;major\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;minor\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;patch\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;pre-release\u0026gt; ::= \u0026lt;dot-separated pre-release identifiers\u0026gt;\r\u0026lt;dot-separated pre-release identifiers\u0026gt; ::= \u0026lt;pre-release identifier\u0026gt;\r| \u0026lt;pre-release identifier\u0026gt; \u0026#34;.\u0026#34; \u0026lt;dot-separated pre-release identifiers\u0026gt;\r\u0026lt;build\u0026gt; ::= \u0026lt;dot-separated build identifiers\u0026gt;\r\u0026lt;dot-separated build identifiers\u0026gt; ::= \u0026lt;build identifier\u0026gt;\r| \u0026lt;build identifier\u0026gt; \u0026#34;.\u0026#34; \u0026lt;dot-separated build identifiers\u0026gt;\r\u0026lt;pre-release identifier\u0026gt; ::= \u0026lt;alphanumeric identifier\u0026gt;\r| \u0026lt;numeric identifier\u0026gt;\r\u0026lt;build identifier\u0026gt; ::= \u0026lt;alphanumeric identifier\u0026gt;\r| \u0026lt;digits\u0026gt;\r\u0026lt;alphanumeric identifier\u0026gt; ::= \u0026lt;non-digit\u0026gt;\r| \u0026lt;non-digit\u0026gt; \u0026lt;identifier characters\u0026gt;\r| \u0026lt;identifier characters\u0026gt; \u0026lt;non-digit\u0026gt;\r| \u0026lt;identifier characters\u0026gt; \u0026lt;non-digit\u0026gt; \u0026lt;identifier characters\u0026gt;\r\u0026lt;numeric identifier\u0026gt; ::= \u0026#34;0\u0026#34;\r| \u0026lt;positive digit\u0026gt;\r| \u0026lt;positive digit\u0026gt; \u0026lt;digits\u0026gt;\r\u0026lt;identifier characters\u0026gt; ::= \u0026lt;identifier character\u0026gt;\r| \u0026lt;identifier character\u0026gt; \u0026lt;identifier characters\u0026gt;\r\u0026lt;identifier character\u0026gt; ::= \u0026lt;digit\u0026gt;\r| \u0026lt;non-digit\u0026gt;\r\u0026lt;non-digit\u0026gt; ::= \u0026lt;letter\u0026gt;\r| \u0026#34;-\u0026#34;\r\u0026lt;digits\u0026gt; ::= \u0026lt;digit\u0026gt;\r| \u0026lt;digit\u0026gt; \u0026lt;digits\u0026gt;\r\u0026lt;digit\u0026gt; ::= \u0026#34;0\u0026#34;\r| \u0026lt;positive digit\u0026gt;\r\u0026lt;positive digit\u0026gt; ::= \u0026#34;1\u0026#34; | \u0026#34;2\u0026#34; | \u0026#34;3\u0026#34; | \u0026#34;4\u0026#34; | \u0026#34;5\u0026#34; | \u0026#34;6\u0026#34; | \u0026#34;7\u0026#34; | \u0026#34;8\u0026#34; | \u0026#34;9\u0026#34;\r\u0026lt;letter\u0026gt; ::= \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34; | \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;g\u0026#34; | \u0026#34;h\u0026#34; | \u0026#34;i\u0026#34; | \u0026#34;j\u0026#34;\r| \u0026#34;k\u0026#34; | \u0026#34;l\u0026#34; | \u0026#34;m\u0026#34; | \u0026#34;n\u0026#34; | \u0026#34;o\u0026#34; | \u0026#34;p\u0026#34; | \u0026#34;q\u0026#34; | \u0026#34;r\u0026#34; | \u0026#34;s\u0026#34; | \u0026#34;t\u0026#34;\r| \u0026#34;u\u0026#34; | \u0026#34;v\u0026#34; | \u0026#34;w\u0026#34; | \u0026#34;x\u0026#34; | \u0026#34;y\u0026#34; | \u0026#34;z\u0026#34; | \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34;\r| \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;g\u0026#34; | \u0026#34;h\u0026#34; | \u0026#34;i\u0026#34; | \u0026#34;j\u0026#34; | \u0026#34;k\u0026#34; | \u0026#34;l\u0026#34; | \u0026#34;m\u0026#34; | \u0026#34;n\u0026#34;\r| \u0026#34;o\u0026#34; | \u0026#34;p\u0026#34; | \u0026#34;q\u0026#34; | \u0026#34;r\u0026#34; | \u0026#34;s\u0026#34; | \u0026#34;t\u0026#34; | \u0026#34;u\u0026#34; | \u0026#34;v\u0026#34; | \u0026#34;w\u0026#34; | \u0026#34;x\u0026#34;\r| \u0026#34;y\u0026#34; | \u0026#34;z\u0026#34; = 版本驱动开发 edd ?有点意思……*\n为什么要使用语义化的版本控制? 这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。 问题在于只是“近似”还不够。 如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。\n举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函数库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能,你可以放心地指定依赖于梯子的版本号大于等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。\n作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。\n如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函数库正在使用它并遵循这些规则就可以了。请在你的 readme 文件中保留此页链接,让别人也知道这些规则并从中受益。\nfaq 在 0.y.z 初始开发阶段,我该如何进行版本控制?\n最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。\n如何判断发布 1.0.0 版本的时机?\n当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 api 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。\n这不会阻碍快速开发和迭代吗?\n主版本号为零的时候就是为了做快速开发。如果你每天都在改变 api,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。\n对于公共 api,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?\n这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。\n为整个公共 api 写文档太费事了!\n为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。保持项目高效的一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 api 有良好规范的坚持,可以让每个人及每件事都运行顺畅。\n万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?\n一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。\n= git 也是这种思想,每一次修改都是一次新增!*\n如果我更新了自己的依赖但没有改变公共 api 该怎么办?\n由于没有影响到公共 api,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。\n如果我变更了公共 api 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)\n自行做最佳的判断。如果你有庞大的使用者群在依照公共 api 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。\n我该如何处理即将弃用的功能?\n弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 api 时,你应该做两件事:(1)更新你的文档让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 api。\n语义化版本对于版本的字符串长度是否有限制呢?\n没有,请自行做适当的判断。举例来说,长到 255 个字符的版本已过度夸张。再者,特定的系统对于字符串长度可能会有他们自己的限制。\n“v1.2.3” 是一个语义化版本号吗?\n“v1.2.3” 并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。比如: git tag v1.2.3 -m \u0026quot;release version 1.2.3\u0026quot; 中,“v1.2.3” 表示标签名称,而 “1.2.3” 是语义化版本号。\n是否有推荐的正则表达式用以检查语义化版本号的正确性?\n有两个推荐的正则表达式。第一个用于支持按组名称提取的语言(pcre[perl 兼容正则表达式,比如 perl、php 和 r]、python 和 go)。\n参见:https://regex101.com/r/ly7o1x/3/\n^(?p\u0026lt;major\u0026gt;0|[1-9]\\d*)\\.(?p\u0026lt;minor\u0026gt;0|[1-9]\\d*)\\.(?p\u0026lt;patch\u0026gt;0|[1-9]\\d*)(?:-(?p\u0026lt;prerelease\u0026gt;(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*))*))?(?:\\+(?p\u0026lt;buildmetadata\u0026gt;[0-9a-za-z-]+(?:\\.[0-9a-za-z-]+)*))?$ 第二个用于支持按编号提取的语言(与第一个对应的提取项按顺序分别为:major、minor、patch、prerelease、buildmetadata)。主要包括 ecma script(javascript)、pcre(perl 兼容正则表达式,比如 perl、php 和 r)、python 和 go。\n参见:https://regex101.com/r/vkijkf/1/\n^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*))*))?(?:\\+([0-9a-za-z-]+(?:\\.[0-9a-za-z-]+)*))?$ 关于 语义化版本控制的规范是由 gravatars 创办者兼 github 共同创办者 tom preston-werner 所建立。\n如果您有任何建议,请到 github 上提出您的问题。\n许可证 知识共享 署名 3.0 (cc by 3.0)\n","date":"2022-11-02","permalink":"https://loveminimal.github.io/posts/semantic-versioning/","summary":"\u003cimg alt=\"picture 10\" src=\"/posts/semantic-versioning/imgs/f9ff34a8e93cc860f20282e5587cafd221ef73f0d25b926452bec4b7d1dfcb96.png\" width=\"300\" /\u003e \r\n\u003cp\u003ei.e.Semantic Versioning\u003c/p\u003e\n\u003cp\u003e🔔 本文摘录自 \u003ca href=\"https://semver.org/lang/zh-CN/\"\u003e语义化版本 2.0.0 | Semantic Versioning - semver.org\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= Emm\u0026hellip; 直接借用文章中的一段话 \u0026ndash; 为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"语义化版本"},{"content":"在 《gtd 管理系统》 和 《极简主义生活》 中,我们已经聊过类似的内容,然而“个人管理”终究是个没有止境的话题……\n\u0026gt; 这里不妨抛弃‘结构’,想到哪儿,说到哪儿……\n为什么需要‘个人管理’ 一万年太久,只争朝夕。\n时间看似有很多,却是不经用的很,而且,生活往往如一团乱麻,理不清,扯不顺。稍不留意,哪天顾镜自盼,也许就青丝霜雪了。\n你可以懒,但不能懒不自知,更不应知懒而不欲改之,毕竟,是你的人生不是?且大概率只有一次,不可回溯,就算可以轮回,你又有什么资格透支下辈子的人生呢?自欺欺人,可不是一件值得让人‘骄傲’的事。聪明如你,自不会如此愚蠢的,对吧?\n虽说‘难得糊涂’,却不应‘如此糊涂’,更不应‘一直糊涂’,保持清醒,了解自己。\n‘理清生活’的首要之举便在于探索收集生活中的方方面面,对其有过去及现在有一个基本的了解和掌握,当然,这不是一蹴而就的事情,也完全没有必要毕其功于一役,稍加思考就会发现,它本就贯穿你的一生,因其本就是生活。\n世事纷繁,人生苦短,‘善假物’者多少会轻松些,过程本身也会多些乐趣,毕竟我们不是‘苦行僧’,不用证那‘宏愿道’。\n切记,成功无捷径,必须得有的话,只会是 \u0026ndash; 戒骄戒躁,实事求是 !\n如何管理 如何管理呢?日程表?事务巨细,尽皆预排?这可是一个大工程,基本上是很难坚持下来的,它本身对身心就是一个不小的消耗。相信,关注个人管理的,多少都有过类似的心中历程。费了大气力写的计划,不多日,便只能束之以高阁。叹兮,悲兮,奈若何?\n尽收所有‘悬而未决’之事!何为‘悬而未决’?乱你心者,搅你意者,困你心者,扰你念者!一句话就是,所有,你感觉要解决的或将要解决的,一网打尽,统统先记在‘小本本’上。\n然后,从上而下,逐次分析,是否需要采取行动,不需要行动的该怎么办,需要行动的又当如何做。分了门类,进一步组织管理它们,大而化小,小而具象,行之检之,周而往复。(具体步骤,详见 gtd)\n这里的‘行、检、周往’是很重要的,精进很始于此。生活,本就不应是简单地重复,只要保持向上之心,并身体力行,许多‘看似的后退’,其实质都是‘曲折的前进’。\n=全面收集,认真分析,细致管理,定期回顾,踏实行动,实是缺一不可!\n少即是多 在不断地管理过程中,自然而然地就会发现,我们的生活‘太满了’,以至于显得‘拥挤’,变得‘混乱’。多而无序,杂乱无章,许多‘方便’反而成了‘负担’,以致让人积重难行,疲惫不堪。\n生活,应该“减负”了!\n极简主义生活方式得到了不少人的倾心,我个人也很喜欢这种理念,‘少即是多’,确保有限的选择对处女座简直是一种救赎。当然,我感觉,要追求‘适度的少’,而不是‘绝对的少’,太‘少’了,真的不够用了,反而失去了‘极简主义’本身服务于生活的初心。不能为了‘简’而简,一切最终要回归到生活本身,以人为本。\n适度的‘少’!少于何处?\n其实很简单,从生活的日常开始。如果你经常熬夜,那就要注意了,你肯定有很多需要‘少’的地方,比如,少刷或不刷短视频,少看或不看头条‘震惊榜’,少…… 如此,熬夜‘少’了,睡眠自然‘多’了,精力也就‘多’了,做事情更有效率,收获当然可预期地增‘多’……\n舍得舍得,少即是多哦 ~\n提到极简主义,就很难不涉及到“断、舍、离”,就我理解来看,没必要太纠结这三字分是何意,实则是同出而异名,谓之一物。它的内涵是什么呢?是放下包袱,是停止内耗,是平衡取舍,以达到与自己身心的和解。\n以此,愈精简,愈益于收集、分析和管理,良好的感觉又反馈自身,继而实现更深层次的精简。循环往复,平衡便在其中。\n具体的‘术’ 我们已经确定了基本的宗旨和理念 \u0026ndash; 尽收‘悬而未决’,以至‘少即是多’,如何采取具体的行动呢,有没有‘万能药’拿来一试?\n有,也没有!\n所谓‘有’,是指其理念本身就是一种绝佳的指导;所谓‘没有’,是指从来存在什么‘一剑破万法’,凡事皆需要“具体问题,具体分析”,不可有思想上的怠惰。任何行之有效的具体方法,皆可拿来学习、借鉴和模仿,目的在于体会、理解其真义,但切忌一味地‘照猫画虎’,不求甚解。\n正所谓,“一切具象,皆为虚妄”, 本就没有“可道之常道,可名之常名”。\n如此,‘术’何以求?‘术’乃自生!\n在求‘道’的过程中,真正合适本身的‘术’自然就诞生了。当然,这并不是提倡什么不假外求,固步自封。正相反,‘道’存于心,求之在内、外,老祖宗自古就讲求一个“天人合一”,便是此理。\n未结之语 以上各种,难免‘言不及意’,哈,你不能要求一个初窥门径的人能说的清楚,道的明白,不过是抛砖引玉之举,探讨二三事罢了。\n","date":"2022-10-17","permalink":"https://loveminimal.github.io/posts/personal-management/","summary":"\u003cp\u003e在 \u003ca href=\"../get-things-done/\"\u003e《GTD 管理系统》\u003c/a\u003e 和 \u003ca href=\"../minimalist-lifestyle/\"\u003e《极简主义生活》\u003c/a\u003e 中,我们已经聊过类似的内容,然而“个人管理”终究是个没有止境的话题……\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 这里不妨抛弃‘结构’,想到哪儿,说到哪儿……\u003c/code\u003e\u003c/p\u003e","title":"再谈“个人管理”"},{"content":"近来,准备组装一台 pc 主机,做个记录 ~\n🖥 2022 年电脑配置推荐完整榜单(通用篇)\n🖥 cpu篇:2023年3月台式电脑cpu一文搞定\n说真的,采购对于‘处女座’来说真心不是一件简单的事情,很容易就会陷入到‘内耗’的境地……\n采购单 amd 系 配件 型号 价格 备注 主板 华硕 x570 1599 m-atx 中型主板,供电和散热能力是衡量主板好坏的核心标准 cpu amd5800x3d 2399 要关注 cpu 的接口规格,以便于选取合适的主板 显卡 华硕 n 卡 3060ti 3999 独显 硬盘 三星 1t 980pro 899 m.2 nvme 内存条 金士顿 3200 频 699 ddr4 16*2 电源 航嘉 850w 679 散热 华硕 360 水冷 1299 机箱 爱国者 k 269 atx \u0026gt; m-atx \u0026gt; itx 显示器 三星 27 英寸 2k 1579 已购 总价 12742 intel 系 配件 型号 价格 备注 主板 华硕 x570 1599 m-atx 中型主板,供电和散热能力是衡量主板好坏的核心标准 cpu i5-13600k ¥2599 2599 i7-13700k ¥2999 显卡 华硕 n 卡 3060ti 3999 独显 硬盘 三星 1t 980pro 899 m.2 nvme 内存条 金士顿 3200 频 699 ddr4 16*2 电源 航嘉 850w 679 散热 华硕 360 水冷 1299 机箱 爱国者 k 269 atx \u0026gt; m-atx \u0026gt; itx 显示器 三星 27 英寸 2k 1579 已购 总价 12742 备选配件 配件 型号 备注 支架 显卡支架用于防止显卡卡座垂直变形 散热器 用于给 cpu 散热,绝大多数 cpu 有配套风冷,水冷另外买 机箱扇 用于机箱散热,部分机箱有带,很多带有灯效 当前主机 当前使用 2016 年购入的小米笔记本,外接 2k 显示屏,播放 1080p 分辨率视频都会卡顿……\n配件 型号 备注 cpu i5-6500 显卡 gt 940mx “烂大街”的轻薄本显卡 内存 8g 不够用哦 硬盘 m.2 256gb 可扩展,预留了一个 ssd 卡位 \u0026gt; 2016(首代)小米笔记本 air 13.3 英寸 银色\n配件 cpu cpu 型号解读:教你 cpu 型号后缀怎么看?cpu 型号后面的字母和数字区别是什么?\ncpu ,是一台主机的‘心脏’ ❤️!\n","date":"2022-10-14","permalink":"https://loveminimal.github.io/posts/hi-pc/","summary":"\u003cp\u003e近来,准备组装一台 PC 主机,做个记录 ~\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/83636026\"\u003e🖥 2022 年电脑配置推荐完整榜单(通用篇)\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/425492516\"\u003e🖥 CPU篇:2023年3月台式电脑CPU一文搞定\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e说真的,采购对于‘处女座’来说真心不是一件简单的事情,很容易就会陷入到‘内耗’的境地……\u003c/p\u003e","title":"嗨,pc"},{"content":"感谢小伙伴 whatacold 的创意 a bookmarklet for copying a link as an org-mode link - whatacold\u0026rsquo;s space - whatacold.io ,可以方便地复制当前页面的地址并格式化 .md 链接格式,当然,也可以格式化成 .org 链接格式。\n* 配置链接类型 在源码中的 copy.js 中,2 ~ 10 行,你可以看到如下代码片段。\nlet md = `[${(document.title || document.location.hostname)}](${document.location.href})` let org = `[[${document.location.href}][${(document.title || document.location.hostname)}]]` // ----------------------------------------------------------- // 🛠️ 链接类型设置 // org - org mode link, eg. [[https://ovirgo.com/ship/][ship]] // md - markdown link, eg. [ship](https://ovirgo.com/ship/) let type = md // ----------------------------------------------------------- 这里,我们提供了 .md 和 .org 两种文件的链接格式,并通过 type 来设置它。此处,默认 type 为 md ,如果,你经常编辑的是 .org 文件,只需要把 type 设为 org 即可。\n为什么我们这里不提供一个直接在插件设置中的配置选项?\n首先,通常情况下,频繁地切换编辑 .org 和 .md 文件的场景很少,它们有各自的粘性用户;其次,尽可能的减少操作步骤,不想赋予插件图标过多的功能交互;最后,因为是本地导入,你完全可以拷贝一份文件夹,分别配置,导入成两个插件。\n快速使用 浏览器插件方式 loveminimal/copy-link: a browser extension to copy current url as a markdown link etc.\n下载 copy link 📥 到本地,解压缩后,打开浏览器的‘扩展程序’,打开开发者模式,加载已解压的扩展程序,选择你解压后的文件夹,即可添加插件到浏览器。\n点击图标,或使用 ctrl + b 即可使用该功能。\n标签方式 在浏览器的‘书签管理器’中添加新书签,内容如下图所示:\n以下为压缩并添加前缀之后的代码,复制粘贴至上图剪头处即可。\njavascript:function copy(){const e=document.queryselector(\u0026#34;#btn\u0026#34;),t=document.queryselector(\u0026#34;#ipt\u0026#34;),o=document.queryselector(\u0026#34;#cont\u0026#34;);t.select(),document.execcommand(\u0026#34;copy\u0026#34;)?(document.execcommand(\u0026#34;copy\u0026#34;),console.log(\u0026#34;copy success\u0026#34;),e.innertext=\u0026#34;🎉 success\u0026#34;,e.style.background=\u0026#34;#67c23a\u0026#34;,e.style.color=\u0026#34;#fff\u0026#34;):(console.log(\u0026#34;copy failed\u0026#34;),e.innertext=\u0026#34;❌ faild\u0026#34;,e.style.background=\u0026#34;#f56c6c\u0026#34;,e.style.color=\u0026#34;#fff\u0026#34;),settimeout(()=\u0026gt;{document.body.removechild(o)},1500)}!function(){let e=\u0026#34;[\u0026#34;+(document.title||document.location.hostname)+\u0026#34;](\u0026#34;+document.location.href+\u0026#34;)\u0026#34;,t=document.createelement(\u0026#34;div\u0026#34;);t.id=\u0026#34;btn\u0026#34;,t.innertext=\u0026#34;🥳 copy\u0026#34;,t.style=\u0026#34;background: #e6a23c; box-shadow: 1px 1px 3px #333; width: 120px; height: 40px; text-align: center; line-height: 40px; border-radius: 4px; color: #333; cursor: pointer; font-weight: 700; font-family: segoe script, courier new;font-size: 16px;\u0026#34;,t.addeventlistener(\u0026#34;click\u0026#34;,copy);let o=document.createelement(\u0026#34;input\u0026#34;);o.value=e,o.id=\u0026#34;ipt\u0026#34;,o.style=\u0026#34;opacity: 0;\u0026#34;,o.select();let c=document.createelement(\u0026#34;div\u0026#34;);c.id=\u0026#34;cont\u0026#34;,c.style=\u0026#34;position: fixed; right: 32px; top: 16px; width: 100px;z-index: 10000;\u0026#34;,c.appendchild(t),c.appendchild(o),document.body.appendchild(c)}(); 完成后,点击标签后,会在当前网页左上角弹出复制按钮;\n点击复制后,按钮会更新状态及样式,并在 1.5s 自动消失。\n当然,也可能失败,但希望你永远都不会看到它。\n源码解析 请访问 copy-link/copy.js at master · loveminimal/copy-link/copy.js 。\n📌 结语\njust for fun 🎉\n","date":"2022-09-30","permalink":"https://loveminimal.github.io/posts/a-simple-bookmark-copying/","summary":"\u003cp\u003e感谢小伙伴 Whatacold 的创意 \u003ca href=\"https://whatacold.io/blog/2022-08-08-org-link-bookmarklet/\"\u003eA Bookmarklet for Copying a Link as an Org-mode Link - whatacold\u0026rsquo;s space - whatacold.io\u003c/a\u003e ,可以方便地复制当前页面的地址并格式化 \u003ccode\u003e.md\u003c/code\u003e 链接格式,当然,也可以格式化成 \u003ccode\u003e.org\u003c/code\u003e 链接格式。\u003c/p\u003e","title":"a simple bookmark copying"},{"content":"在 canvas 中,我们介绍了画布的基本概念和使用方式,现在,我们来用它实现一个基本的手写板 - 你可以在 tablet 查看源码 。\n\u0026gt; 原生 js 实现一下喽 ~\n\u0026lt;!-- 画布 --\u0026gt; \u0026lt;canvas id=\u0026#34;cvs\u0026#34; width=\u0026#34;600\u0026#34; height=\u0026#34;300\u0026#34; ontouchstart=\u0026#34;touchstart(event)\u0026#34; ontouchmove=\u0026#34;touchmove(event)\u0026#34; ontouchend=\u0026#34;touchend(event)\u0026#34; onmousedown=\u0026#34;mousedown(event)\u0026#34; onmousemove=\u0026#34;mousemove(event)\u0026#34; onmouseup=\u0026#34;mouseup(event)\u0026#34; \u0026gt; \u0026lt;/canvas\u0026gt; 一般来说,手写板什么的多在移动设备(触摸)上使用,只涉及 touch*** 相关事件,当然,如上所示,pc 端使用 mouse*** 事件模拟即可。\n下面,让我们看一下具体实现吧(以 mouse*** 事件为例)。\n基本思路 手写板应用的核心,就是使用 canvas 实时绘制路径(path),我们先来简单回顾一下这方面的知识,如下:\nbeginpath()\r- 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径\rmoveto(x, y)\r- 把画笔移动到指定的坐标 (x, y),相当于设置路径的起始点坐标\rlineto(x, y)\r- 添加一个新点,然后创建从该点到画面中最后指定点的线条\rclosepath()\r- 闭合路径之后,图形绘制命令又重新指向到上下文中\rstroke()\r- 通过线条来绘制图形轮廓 通过以上绘制路径的方法,我们使用 mousestart 结合 beginpath() 和 moveto(x, y) 方法,开始绘制路径,并在 mousemove 事件触发的过程中结合 lineto(x, y) 和 stroke() 实时绘制路径。\n核心解析 1. 先准备一下吧\nlet cvs = document.queryselector(\u0026#39;#cvs\u0026#39;);\t// 获取画布 let ctx = cvs.getcontext(\u0026#39;2d\u0026#39;); // 上下文 2. 看看相关的事件\nfunction mousedown(e) { drawstart(e.pagex - cvs.offsetleft, e.pagey - cvs.offsettop); } // function drawstart(x, y) { // document.body.classlist.add(\u0026#39;body-fix\u0026#39;);\t// 书写时禁止页面滚动 // ctx.beginpath(); // ctx.moveto(x, y); // } mousedown 和 mousemove 事件中,我们可以方便获取鼠标指针相对于其第一个父级元素(带有 position 属性)的相对位置 (x, y),如果没有,就是相对于 body 了(本例中即是如此)。\nfunction mousemove(e) { if (e.buttons === 1) {\t// 鼠标左键按下时 drawmove(e.pagex - cvs.offsetleft, e.pagey - cvs.offsettop); } } // function drawmove(x, y) { // ctx.lineto(x, y); // ctx.stroke(); // } 实时坐标的获取同上,此外需要注意的是,这里我们限制了 =仅当鼠标左键按下时 才会绘制路径,否则,绘制出来的路径只会是个鬼画符。当然,如果你是在触摸设备中使用 touch*** 事件,则不存在这个问题。\n不同浏览器对于鼠标事件的监听指示可能有所不同,chrome 中,当 e.buttons 为 1 时,表示左键是按下状态,如果你想做兼容,请查看相关文档。\nfunction mouseup(e) { drawend(); } // function drawend() { // // ctx.closepath() // document.body.classlist.remove(\u0026#39;body-fix\u0026#39;);\t// 书写完成恢复页面滚动 // } 你可能已经注意到了,在 mousedown 和 mouseup 中,我们针对 body 元素做了一些类别修改 - 添加/删除 body-fix ,它有什么作用呢?\n.body-fix { overflow: hidden; } 很简单,就是为了防止在书写签名时页面滚动,导致你写不成字 ~ 当然,别忘记在 mouseup 时,移除该类,否则,你就滚动不了页面喽。\n辅助功能 1. 生成签名\n// 生成签名 function expcvs() { let src = cvs.todataurl(\u0026#39;image/png\u0026#39;, 1); /* * canvas.todataurl(type, encoderoptions) * 返回: * - 该方法返回一串 uri 字符串(canvas 中图像数据的 base64 编码) * * 参数: * - type:图像格式,默认为\u0026#34;image/png\u0026#34; * - encoderoptions:数值为 0 ~ 1,表示图片质量,仅在 type 为 \u0026#34;image/jpeg\u0026#34; 或 \u0026#34;image/webp\u0026#34; 时有效 * * 其他: * png 默认生成图片无背景,jpeg 默认生成图片为黑色背景 * 如果需要白色背景,可以在绘制前先绘制背景: * ctx.fillstyle = \u0026#39;#fff\u0026#39;; * ctx.fillrect(0, 0, canvas.width, canvas.height); */ console.log(src); img.src = src; } canvas 可以方便地生成图片格式(base64)文件 - 通过 todataurl 方法,如此,我们就可以方便的传递数据或将其作为图片标签的 src 属性使用。\n这里注意,做为签名来说,我们通常需要生成背景透明的图像,所以默认即为 png 格式的。\n2. 清除签名\n// 清除签名 function clrcvs() { ctx.clearrect(0, 0, 600, 300); img.src = \u0026#39;\u0026#39;; } 很简单,直接使用 clearrect 清空一个画布就可以了。\n3. 选择颜色\n// 选择签名颜色 function selectcolor(e) { console.log(e.target.value); ctx.strokestyle = e.target.value; } 可选功能,用来设置画笔颜色,当然,还有其他设置项,你完全可以按需添加。\n结语 emm\u0026hellip; 基本原理,就是讲的这些,具体项目中实现可能会稍有改变,但难不到你的,对吧 🥳\n参考链接 canvas 生成一张图片在后,图片背景颜色默认是黑色 怎么改成其他颜色呢? - 知乎 - www.zhihu.com 解决 canvas 转 base64/jpeg 时透明区域变成黑色背景的方法_html5_网页制作_脚本之家 - www.jb51.net szimek/signature_pad: html5 canvas based smooth signature drawing - github.com h5 前端实现移动端手写 canvas 签名(支持横竖屏,自定义图片旋转角度)_canvas 手写签名横屏 h5 canvas 签名板 - 简书 - www.jianshu.com ","date":"2022-09-28","permalink":"https://loveminimal.github.io/posts/tablet/","summary":"\u003cp\u003e在 \u003ca href=\"../canvas/\"\u003eCanvas\u003c/a\u003e 中,我们介绍了画布的基本概念和使用方式,现在,我们来用它实现一个基本的手写板 - 你可以在 \u003ca href=\"https://github.com/loveminimal/tablet\"\u003eTablet\u003c/a\u003e 查看源码 。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 原生 JS 实现一下喽 ~\u003c/code\u003e\u003c/p\u003e","title":"手写板"},{"content":"\u0026gt; 近来要开发一个手写板程序,顺便来系统回顾一下 canvas 这个东东……\n🔔 本篇摘录自 《学习 html5 canvas 这一篇文章就够了》 ,写的真不错。\n\u0026lt;canvas\u0026gt; 是 html5 新增的一个可以使用脚本(通常为 javascript)在其中绘制图像的 html 元素。\n\u0026gt; 上图就是使用 canvas 绘制出来的\ncanvas 是由 html 代码配合高度和宽度属性而定义出的可绘制区域。javascript 代码可以访问该区域,类似于其他通用的二维 api,通过一套完整的绘图函数来动态生成图形。\n基本使用 1. \u0026lt;canvas\u0026gt; 元素\n\u0026lt;canvas id=\u0026#34;tutorial\u0026#34; width=\u0026#34;300\u0026#34; height=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;canvas\u0026gt; 标签只有两个可选的属性 width 和 height ,如果省略,则默认 width 为 300 ,height 为 150 ,单位是 px 。也可以使用 css 属性来设置宽高,但是如宽高属性和初始比例不一致,会出现扭曲(强烈不推荐 ❌)。\n2. 渲染上下文 context\n\u0026lt;canvas\u0026gt; 会创建一个固定大小的画布,会公开一个或多个渲染上下文(画笔),使用渲染上下文来绘制和处理要展示的内容。\n我们重点研究 2d 渲染上下文。如何获取它呢?\nvar canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if(!canvas.getcontext) return;\t// 检测支持性 var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;);\t// 获得 2d 上下文对象 绘制形状 1. 栅格(grid)和坐标空间\n如下图所示,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角,坐标为 (0,0) 。所有元素的位置都相对于原点来定位。所以图中蓝色方形左上角的坐标为距离左边(x 轴)x 像素,距离上边(y 轴)y 像素,坐标为 (x,y)。\n后面我们会涉及到坐标原点的平移、网格的旋转以及缩放等。\n2. 绘制矩形\n\u0026lt;canvas\u0026gt; 只支持一种原生的图形绘制:矩形。\n\u0026gt; 所有其他图形都至少需要生成一种路径 (path)。\ncanvas 提供了有三种方法绘制矩形:\n// 绘制一个填充的矩形\r1. fillrect(x, y, width, height)\r// 绘制一个矩形的边框\r2. strokerect(x, y, width, height)\r// 清除指定的矩形区域,然后这块区域会变的完全透明\r3. clearrect(x, y, widh, height) 这 3 个方法具有相同的参数。其中, x, y 指矩形左上角的坐标, width, height 指绘制的矩形的宽高。\n上 🌰 ,如下:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if(!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillrect(10, 10, 100, 50);\t//绘制矩形,填充的默认颜色为黑色 ctx.strokerect(10, 70, 100, 50);\t//绘制矩形边框 ctx.clearrect(15, 15, 50, 25); } draw(); 绘制路径 图形的基本元素是路径。\n路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。\n一个路径,甚至一个子路径,都是闭合的。\n使用路径绘制图形需要一些额外的步骤:\n创建路径起始点; 调用绘制方法去绘制出路径; 把路径封闭; 一旦路径生成,通过描边或填充路径区域来渲染图形。 下面是需要用到的方法:\nbeginpath()\r- 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径\rmoveto(x, y)\r- 把画笔移动到指定的坐标 (x, y),相当于设置路径的起始点坐标\rlineto(x, y)\r- 添加一个新点,然后创建从该点到画面中最后指定点的线条\rclosepath()\r- 闭合路径之后,图形绘制命令又重新指向到上下文中\rstroke()\r- 通过线条来绘制图形轮廓\rfill()\r- 通过填充路径的内容区域生成实心的图形 1. 绘制线段\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath();\t// 新建一条 path ctx.moveto(50, 50);\t// 把画笔移动到指定的坐标 ctx.lineto(200, 50);\t// 绘制一条从当前位置到指定坐标 (200, 50) 的直线 // 闭合路径 // 会拉一条从当前点到 path 起始点的直线, // 如果当前点与起始点重合,则什么都不做 ctx.closepath(); ctx.stroke(); //绘制路径 } draw(); 2. 绘制三角形边框\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); ctx.lineto(200, 50); ctx.lineto(200, 200); ctx.closepath();\t//虽然我们只绘制了两条线段,但是 closepath 会自动闭合,仍然是一个 3 角形 ctx.stroke();\t//描边,stroke 不会自动 closepath() } draw(); 3. 填充三角形\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); ctx.lineto(200, 50); ctx.lineto(200, 200); ctx.fill();\t//填充闭合区域,如果 path 没有闭合,则 fill() 会自动闭合路径。 } draw(); 4. 绘制圆弧\n有两个方法可以绘制圆弧:\n1. arc(x, y, r, startangle, endangle, anticlockwise)\r- 以 (x, y) 为圆心,以 r 为半径,从 startangle 弧度开始到 endangle 弧度结束,\r- anticlosewise 是布尔值,true 表示逆时针,false 表示顺时针(默认是顺时针)\r* 这里的度数都是弧度,0 弧度是指 x 轴正向\r* radias = (math.pi/180)*degrees // 角度转换成弧度\r2. arcto(x1, y1, x2, y2, radius)\r- 根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点\r* 这个方法可以这样理解,绘制的弧形是由两条切线所决定\r* - 第 1 条切线:起始点和控制点 1 决定的直线\r* - 第 2 条切线:控制点 1 和控制点 2 决定的直线\r* - 其实绘制的圆弧就是与这两条直线相切的圆弧 来看几个示例 🍩\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.arc(50, 50, 40, 0, math.pi / 2, false); ctx.stroke(); ctx.beginpath(); ctx.arc(150, 50, 40, 0, -math.pi / 2, true); ctx.closepath(); ctx.stroke(); ctx.beginpath(); ctx.arc(50, 150, 40, -math.pi / 2, math.pi / 2, false); ctx.fill(); ctx.beginpath(); ctx.arc(150, 150, 40, 0, math.pi, false); ctx.fill(); } draw(); 再来看一下关于 arcto 的方法示例如下:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); //参数 1、2:控制点 1 坐标 参数 3、4:控制点 2 坐标 参数 5:圆弧半径 ctx.arcto(200, 50, 200, 200, 100); ctx.lineto(200, 200) ctx.stroke(); ctx.beginpath(); ctx.rect(50, 50, 10, 10); ctx.rect(200, 50, 10, 10) ctx.rect(200, 200, 10, 10) ctx.fill() } draw(); 5. 绘制贝塞尔曲线\n贝塞尔曲线 (bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。\n一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。\n贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具如 photoshop 等。在 flash4 中还没有完整的曲线工具,而在 flash5 里面已经提供出贝塞尔曲线工具。\n贝塞尔曲线于 1962,由法国工程师皮埃尔·贝塞尔(pierre bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由 paul de casteljau 于 1959 年运用 de casteljau 演算法开发,以稳定数值的方法求出贝兹曲线。\n_一次贝塞尔曲线其实是一条直线\n_二次贝塞尔曲线\n_三次贝塞尔曲线\n好的,我们已初步了解了 =贝塞尔曲线 是什么东东,那么如何绘制它呢?\n// 二次贝塞尔曲线\rquadraticcurveto(cp1x, cp1y, x, y)\r- 参数 1 和 2:控制点坐标\r- 参数 3 和 4:结束点坐标\r// 三次贝塞尔曲线\rbeziercurveto(cp1x, cp1y, cp2x, cp2y, x, y)\r- 参数 1 和 2:控制点 1 的坐标\r- 参数 3 和 4:控制点 2 的坐标\r- 参数 5 和 6:结束点的坐标 像下面这样:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(10, 200); // 起始点 var cp1x = 40, cp1y = 100; // 控制点 var x = 200, y = 200; // 结束点 //绘制二次贝塞尔曲线 ctx.quadraticcurveto(cp1x, cp1y, x, y); ctx.stroke(); ctx.beginpath(); ctx.rect(10, 200, 10, 10); ctx.rect(cp1x, cp1y, 10, 10); ctx.rect(x, y, 10, 10); ctx.fill(); ctx.beginpath(); ctx.moveto(40, 200); // 起始点 var cp1x = 20, cp1y = 100; // 控制点 1 var cp2x = 100, cp2y = 120; // 控制点 2 var x = 200, y = 200; // 结束点 //绘制三次贝塞尔曲线 ctx.beziercurveto(cp1x, cp1y, cp2x, cp2y, x, y); ctx.stroke(); ctx.beginpath(); ctx.rect(40, 200, 10, 10); ctx.rect(cp1x, cp1y, 10, 10); ctx.rect(cp2x, cp2y, 10, 10); ctx.rect(x, y, 10, 10); ctx.fill(); } draw(); 添加样式和颜色 在前面的绘制矩形章节中,只用到了默认的线条和颜色。\n如果想要给图形上色,有两个重要的属性可以做到:\n1. fillstyle = color // 设置图形的填充颜色\r2. strokestyle = color // 设置图形轮廓的颜色\r备注:\r- color 可以是表示 css 颜色值的字符串、渐变对象或者图案对象\r- 默认情况下,线条和填充颜色都是黑色\r- 一旦您设置了 strokestyle 或者 fillstyle 的值,那么这个新值就会成为新绘制的图形的默认值,\r- 如果你要给每个图形上不同的颜色,你需要重新设置 fillstyle 或 strokestyle 的值 1. fillstyle\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); for (var i = 0; i \u0026lt; 6; i++){ for (var j = 0; j \u0026lt; 6; j++){ ctx.fillstyle = \u0026#39;rgb(\u0026#39; + math.floor(255 - 42.5 * i) + \u0026#39;,\u0026#39; + math.floor(255 - 42.5 * j) + \u0026#39;,0)\u0026#39;; ctx.fillrect(j * 50, i * 50, 50, 50); } } } draw(); 2. strokestyle\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); for (var i = 0; i \u0026lt; 6; i++){ for (var j = 0; j \u0026lt; 6; j++){ ctx.strokestyle = `rgb( ${randomint(0, 255)}, ${randomint(0, 255)} )`; ctx.strokerect(j * 50, i * 50, 40, 40); } } } draw(); /** * 返回随机的 [from, to] 之间的整数(包括 from,也包括 to) */ function randomint(from, to){ return parseint(math.random() * (to - from + 1) + from); } 3. transparency (透明度)\nglobalalpha = transparencyvalue\r- 这个属性影响到 canvas 里所有图形的透明度\r- 有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0\r- globalalpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效,\r- 不过,我认为使用 rgba() 设置透明度更加好一些 4. line style\n_1. 线宽\nlinewidth = value\r关于 value :\r- 只能是正值,默认是 1.0\r- 起始点和终点的连线为中心,上下各占线宽的一半 ctx.beginpath(); ctx.moveto(10, 10); ctx.lineto(100, 10); ctx.linewidth = 10; ctx.stroke(); ctx.beginpath(); ctx.moveto(110, 10); ctx.lineto(160, 10) ctx.linewidth = 20; ctx.stroke() _2. 线条末端样式\nlinecap = type\r关于 type :\r- butt:线段末端以方形结束\r- round:线段末端以圆形结束\r- square:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域 var linecaps = [\u0026#34;butt\u0026#34;, \u0026#34;round\u0026#34;, \u0026#34;square\u0026#34;]; for (var i = 0; i \u0026lt; 3; i++){ ctx.beginpath(); ctx.moveto(20 + 30 * i, 30); ctx.lineto(20 + 30 * i, 100); ctx.linewidth = 20; ctx.linecap = linecaps[i]; ctx.stroke(); } ctx.beginpath(); ctx.moveto(0, 30); ctx.lineto(300, 30); ctx.moveto(0, 100); ctx.lineto(300, 100) ctx.strokestyle = \u0026#34;red\u0026#34;; ctx.linewidth = 1; ctx.stroke(); _3. 同一个 path 内,设定线条与线条间接合处的样式\nlinejoin = type\r关于 type :\r- round :通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度\r- bevel :在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角\r- miter(默认) :通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域 function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); var linejoin = [\u0026#39;round\u0026#39;, \u0026#39;bevel\u0026#39;, \u0026#39;miter\u0026#39;]; ctx.linewidth = 20; for (var i = 0; i \u0026lt; linejoin.length; i++){ ctx.linejoin = linejoin[i]; ctx.beginpath(); ctx.moveto(50, 50 + i * 50); ctx.lineto(100, 100 + i * 50); ctx.lineto(150, 50 + i * 50); ctx.lineto(200, 100 + i * 50); ctx.lineto(250, 50 + i * 50); ctx.stroke(); } } draw(); 4. 虚线\n用 setlinedash 方法和 linedashoffset 属性来制定虚线样式,如下:\nsetlinedash 方法接受一个数组,来指定线段与间隙的交替; linedashoffset 属性设置起始偏移量。 function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.setlinedash([20, 5]); // [实线长度,间隙长度] ctx.linedashoffset = -0; ctx.strokerect(50, 50, 210, 210); } draw(); // getlinedash(): 返回一个包含当前虚线样式,长度为非负偶数的数组 绘制文本 canvas 提供了两种方法来渲染文本:\nfilltext(text, x, y [, maxwidth])\r- 在指定的 (x,y) 位置填充指定的文本,绘制的最大宽度是可选的\rstroketext(text, x, y [, maxwidth])\r- 在指定的 (x,y) 位置绘制文本边框,绘制的最大宽度是可选的 var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.font = \u0026#34;100px sans-serif\u0026#34; ctx.filltext(\u0026#34;天若有情\u0026#34;, 10, 100); ctx.stroketext(\u0026#34;天若有情\u0026#34;, 10, 200) } draw(); 我们都可以给文本添加哪些样式呢?\n1. 绘制文本的样式\rfont = value\r- 这个字符串使用和 css font 属性相同的语法\r- 默认的字体是 10px sans-serif\r2. 文本对齐选项\rtextalign = value\r- 可选的值包括:start, end, left, right or center\r- 默认值是 start。\r3. 基线对齐选项\rtextbaseline = value\r- 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom\r- 默认值是 alphabetic\r4. 文本方向\rdirection = value\r- 可能的值包括:ltr, rtl, inherit\r- 默认值是 inherit 绘制图片 我们也可以在 canvas 上直接绘制图片。\n1. 由零开始创建图片\nvar img = new image(); // 创建 img 元素 img.src = \u0026#39;myimage.png\u0026#39;; // 设置图片源地址 img.onload = function(){ // 参数 1:要绘制的 img // 参数 2、3:绘制的 img 在 canvas 中的坐标 ctx.drawimage(img, 0, 0) } // 关于 onload // - 考虑到图片是从网络加载,如果 drawimage 的时候图片还没有完全加载完成,则什么都不做,个别浏览器会抛异常, // - 所以我们应该保证在 img 绘制完成之后再 drawimage 2. 绘制 img 标签元素中的图片\nimg 可以 new 也可以来源于我们页面的 \u0026lt;img\u0026gt; 标签。\n\u0026lt;img src=\u0026#34;./美女。jpg\u0026#34; alt=\u0026#34;\u0026#34; width=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;canvas id=\u0026#34;tutorial\u0026#34; width=\u0026#34;600\u0026#34; height=\u0026#34;400\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); var img = document.queryselector(\u0026#34;img\u0026#34;); ctx.drawimage(img, 0, 0); } document.queryselector(\u0026#34;img\u0026#34;).onclick = function (){ draw(); } \u0026lt;/script\u0026gt; \u0026gt; 上图就是页面中的 \u0026lt;img\u0026gt; 标签\n3. 缩放图片\ndrawimage() 也可以再添加两个参数:\ndrawimage(image, x, y, width, height)\r- width 和 height,这两个参数用来控制 当像 canvas 画入时应该缩放的大小 4. 切片\ndrawimage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight)\r- 第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用\r其他 8 个参数:\r- 前 4 个是定义图像源的切片位置和大小\r- 后 4 个则是定义切片的目标显示位置和大小 状态的保存和恢复 canvas 的状态就是当前画面应用的所有样式和变形的一个快照,状态的保存和恢复是绘制复杂图形时必不可少的操作。save() 和 restore() 方法是用来保存和恢复 canvas 状态的,都没有参数。\n1. save()\ncanvas 状态存储在栈中,每当 save() 方法被调用后,当前的状态就被推送到栈中保存。\n\u0026gt; canvas 都有哪些状态呢?\n一个绘画状态包括:\n当前应用的变形(即移动,旋转和缩放); strokestyle, fillstyle, globalalpha, linewidth, linecap, linejoin, miterlimit, shadowoffsetx, shadowoffsety, shadowblur, shadowcolor, globalcompositeoperation 的值; 当前的裁切路径(clipping path)。 可以调用任意多次 save 方法(类似数组的 push())。\n2. restore()\n每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复(类似数组的 pop())。\n上个例子吧 🍩\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillrect(0, 0, 150, 150); // 使用默认设置绘制一个矩形 ctx.save(); // 保存默认状态 ctx.fillstyle = \u0026#39;red\u0026#39; // 在原有配置基础上对颜色做改变 ctx.fillrect(15, 15, 120, 120); // 使用新的设置绘制一个矩形 ctx.save(); // 保存当前状态 ctx.fillstyle = \u0026#39;#fff\u0026#39; // 再次改变颜色配置 ctx.fillrect(30, 30, 90, 90); // 使用新的配置绘制一个矩形 ctx.restore(); // 重新加载之前的颜色状态 ctx.fillrect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形 ctx.restore(); // 加载默认颜色配置 ctx.fillrect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形 } draw(); 变形 1. translate\ntranslate(x, y)\r- 用来移动 canvas 的原点到指定的位置\r- 接受两个参数,x 是左右偏移量,y 是上下偏移量 \u0026gt; 注意:translate 移动的是 canvas 的坐标原点(坐标变换)。\n在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore() 方法比手动恢复原先的状态要简单得多。又如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.save(); //保存坐原点平移之前的状态 ctx.translate(100, 100); ctx.strokerect(0, 0, 100, 100); ctx.restore(); //恢复到最初状态 ctx.translate(220, 220); ctx.fillrect(0, 0, 100, 100) } draw(); 2. rotate\nrotate(angle)\r- 旋转坐标轴, 旋转的中心是坐标原点\r- 这个方法只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值 var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillstyle = \u0026#34;red\u0026#34;; ctx.save(); ctx.translate(100, 100); ctx.rotate(math.pi / 180 * 45); ctx.fillstyle = \u0026#34;blue\u0026#34;; ctx.fillrect(0, 0, 100, 100); ctx.restore(); ctx.save(); ctx.translate(0, 0); ctx.fillrect(0, 0, 50, 50) ctx.restore(); } draw(); 3. scale\n我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。\nscale(x, y)\r- scale 方法接受两个参数。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值\r- 值比 1.0 小表示缩 小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有 默认情况下,canvas 的 1 单位就是 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。\n4. transform(变形矩阵)\ntransform(a, b, c, d, e, f)\r- a (m11): horizontal scaling.\r- b (m12): horizontal skewing.\r- c (m21): vertical skewing.\r- d (m22): vertical scaling.\r- e (dx): horizontal moving.\r- f (dy): vertical moving. var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.transform(1, 1, 0, 1, 0, 0); ctx.fillrect(0, 0, 100, 100); } draw(); 合成 在前面的所有例子中、,我们总是将一个图形画在另一个之上,对于其他更多的情况,仅仅这样是远远不够的。比如,对合成的图形来说,绘制顺序会有限制。不过,我们可以利用 globalcompositeoperation 属性来改变这种状况。\nglobalcompositeoperation = type\r其中 type 值有 13 个,如下:\r- source-over (默认值), source-in, source-out, source-atop\r- destination-over, destination-in, destination-out, destination-atop\r- lighter, darken, lighten\r- xor, copy 下面我们分别来看一下这些值的表示。\n1. source-over\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillstyle = \u0026#34;blue\u0026#34;; ctx.fillrect(0, 0, 200, 200); ctx.globalcompositeoperation = \u0026#34;source-over\u0026#34;; //全局合成操作 ctx.fillstyle = \u0026#34;red\u0026#34;; ctx.fillrect(100, 100, 200, 200); } draw(); 注:下面的展示中,蓝色是原有的,红色是新的。\n这是默认设置,新图像会覆盖在原有图像。\n2. source-in\n仅仅会出现新图像与原来图像重叠的部分,其他区域都变成透明的。(包括其他的老图像区域也会透明)\n3. source-out\n仅仅显示新图像与老图像没有重叠的部分,其余部分全部透明。(老图像也不显示)\n4. source-atop\n新图像仅仅显示与老图像重叠区域。(老图像仍然可以显示)\n5. destination-over\n新图像会在老图像的下面。\n6. destination-in\n仅仅新老图像重叠部分的老图像被显示,其他区域全部透明。\n7. destination-out\n仅仅老图像与新图像没有重叠的部分。 (注意显示的是老图像的部分区域)\n8. destination-atop\n老图像仅仅仅仅显示重叠部分,新图像会显示在老图像的下面。\n9. lighter\n新老图像都显示,但是重叠区域的颜色做加处理。\n10. darken\n保留重叠部分最黑的像素。(每个颜色位进行比较,得到最小的)\nblue: #0000ff\rred: #ff0000\r// 所以重叠部分的颜色:#000000 11. lighten\n保证重叠部分最量的像素。(每个颜色位进行比较,得到最大的)\nblue: #0000ff\rred: #ff0000\r// 所以重叠部分的颜色:#ff00ff 12. xor\n重叠部分会变成透明。\n13. copy\n只有新图像会被保留,其余的全部被清除(边透明)。\n裁剪路径 clip()\r- 把已经创建的路径转换成裁剪路径 裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。\n注意:clip() 只能遮罩在这个方法 =调用之后 绘制的图像,如果是 clip() 方法调用之前绘制的图像,则无法实现遮罩。\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.arc(20,20, 100, 0, math.pi * 2); ctx.clip(); ctx.fillstyle = \u0026#34;pink\u0026#34;; ctx.fillrect(20, 20, 100,100); } draw(); 动画 动画的基本步骤:\n清空 canvas 再绘制每一帧动画之前,需要清空所有。清空所有最简单的做法就是 clearrect() 方法; 保存 canvas 状态 如果在绘制的过程中会更改 canvas 的状态(颜色、移动了坐标原点等), 又在绘制每一帧时都是原始状态的话,则最好保存下 canvas 的状态; 绘制动画图形这一步才是真正的绘制动画帧; 恢复 canvas 状态如果你前面保存了 canvas 状态,则应该在绘制完成一帧之后恢复 canvas 状态。 控制动画\n我们可用通过 canvas 的方法或者自定义的方法把图像绘制到 canvas 上。正常情况,我们能看到绘制的结果是在脚本执行结束之后。例如,我们不可能在一个 for 循环内部完成动画。\n也就是,为了执行动画,我们需要一些可以定时执行重绘的方法。\n一般用到下面三个方法:\nsetinterval() settimeout() requestanimationframe() let sun; let earth; let moon; let ctx; init(); function init() { sun = new image(); earth = new image(); moon = new image(); sun.src = \u0026#34;sun.png\u0026#34;; earth.src = \u0026#34;earth.png\u0026#34;; moon.src = \u0026#34;moon.png\u0026#34;; let canvas = document.queryselector(\u0026#34;#solar\u0026#34;); ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); sun.onload = function (){ draw() } } function draw() { ctx.clearrect(0, 0, 300, 300); // 清空所有内容 // 绘制太阳 ctx.drawimage(sun, 0, 0, 300, 300); ctx.save(); ctx.translate(150, 150); //绘制 earth 轨道 ctx.beginpath(); ctx.strokestyle = \u0026#34;rgba(255,255,0,0.5)\u0026#34;; ctx.arc(0, 0, 100, 0, 2 * math.pi) ctx.stroke() let time = new date(); //绘制地球 ctx.rotate( 2 * math.pi / 60 * time.getseconds() + 2 * math.pi / 60000 * time.getmilliseconds() ); ctx.translate(100, 0); ctx.drawimage(earth, -12, -12); //绘制月球轨道 ctx.beginpath(); ctx.strokestyle = \u0026#34;rgba(255,255,255,.3)\u0026#34;; ctx.arc(0, 0, 40, 0, 2 * math.pi); ctx.stroke(); //绘制月球 ctx.rotate( 2 * math.pi / 6 * time.getseconds() + 2 * math.pi / 6000 * time.getmilliseconds() ); ctx.translate(40, 0); ctx.drawimage(moon, -3.5, -3.5); ctx.restore(); requestanimationframe(draw); } emm\u0026hellip; 不错,再来一个吧 🍪\n// \u0026lt;canvas id=\u0026#34;solar\u0026#34; width=\u0026#34;300\u0026#34; height=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; init(); function init(){ let canvas = document.queryselector(\u0026#34;#solar\u0026#34;); let ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); draw(ctx); } function draw(ctx){ requestanimationframe(function step(){ drawdial(ctx); //绘制表盘 drawallhands(ctx); //绘制时分秒针 requestanimationframe(step); }); } /*绘制时分秒针*/ function drawallhands(ctx){ let time = new date(); let s = time.getseconds(); let m = time.getminutes(); let h = time.gethours(); let pi = math.pi; let secondangle = pi / 180 * 6 * s; //计算出来 s 针的弧度 let minuteangle = pi / 180 * 6 * m + secondangle / 60; //计算出来分针的弧度 let hourangle = pi / 180 * 30 * h + minuteangle / 12; //计算出来时针的弧度 drawhand(hourangle, 60, 6, \u0026#34;red\u0026#34;, ctx); //绘制时针 drawhand(minuteangle, 106, 4, \u0026#34;green\u0026#34;, ctx); //绘制分针 drawhand(secondangle, 129, 2, \u0026#34;blue\u0026#34;, ctx); //绘制秒针 } /*绘制时针、或分针、或秒针 * 参数 1:要绘制的针的角度 * 参数 2:要绘制的针的长度 * 参数 3:要绘制的针的宽度 * 参数 4:要绘制的针的颜色 * 参数 4:ctx * */ function drawhand(angle, len, width, color, ctx){ ctx.save(); ctx.translate(150, 150); //把坐标轴的远点平移到原来的中心 ctx.rotate(-math.pi / 2 + angle); //旋转坐标轴。 x 轴就是针的角度 ctx.beginpath(); ctx.moveto(-4, 0); ctx.lineto(len, 0); // 沿着 x 轴绘制针 ctx.linewidth = width; ctx.strokestyle = color; ctx.linecap = \u0026#34;round\u0026#34;; ctx.stroke(); ctx.closepath(); ctx.restore(); } /*绘制表盘*/ function drawdial(ctx){ let pi = math.pi; ctx.clearrect(0, 0, 300, 300); //清除所有内容 ctx.save(); ctx.translate(150, 150); //一定坐标原点到原来的中心 ctx.beginpath(); ctx.arc(0, 0, 148, 0, 2 * pi); //绘制圆周 ctx.stroke(); ctx.closepath(); for (let i = 0; i \u0026lt; 60; i++){ //绘制刻度。 ctx.save(); ctx.rotate(-pi / 2 + i * pi / 30); //旋转坐标轴。坐标轴 x 的正方形从 向上开始算起 ctx.beginpath(); ctx.moveto(110, 0); ctx.lineto(140, 0); ctx.linewidth = i % 5 ? 2 : 4; ctx.strokestyle = i % 5 ? \u0026#34;blue\u0026#34; : \u0026#34;red\u0026#34;; ctx.stroke(); ctx.closepath(); ctx.restore(); } ctx.restore(); } 结语 canvas 画布的功能还是挺强大的,go go go!\n","date":"2022-09-23","permalink":"https://loveminimal.github.io/posts/canvas/","summary":"\u003cp\u003e\u003ccode\u003e\u0026gt; 近来要开发一个手写板程序,顺便来系统回顾一下 Canvas 这个东东……\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e🔔 本篇摘录自 \u003ca href=\"https://www.runoob.com/w3cnote/html5-canvas-intro.html\"\u003e《学习 HTML5 Canvas 这一篇文章就够了》\u003c/a\u003e ,写的真不错。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026lt;canvas\u0026gt;\u003c/code\u003e 是 HTML5 新增的一个可以使用脚本(通常为 JavaScript)在其中绘制图像的 HTML 元素。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/canvas/imgs/1232422ad2c81ef221b8d3cf3938b9cc48fda66fdd7418d5c400728d82a2c162.png\" width=\"500\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; 上图就是使用 canvas 绘制出来的\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eCanvas 是由 HTML 代码配合高度和宽度属性而定义出的可绘制区域。JavaScript 代码可以访问该区域,类似于其他通用的二维 API,通过一套完整的绘图函数来动态生成图形。\u003c/p\u003e","title":"canvas"},{"content":"📔 官方文档\n具体安装及引入细节,请直接参考官方文档。\nreact 是一个用于构建用户界面的 javascript 库,你可以用它给简单的 html 页面增加一点交互,也可以开始一个完全由 react 驱动的复杂应用。\n\u0026gt; 对的,它只是一个 ui 库而已 !!!\n简单的就不说了,直接来看一下 react 团队推荐的创建 spa (单页面,single page app)的工具链 - create react app 。\n要创建项目,请执行:\nnpx create-react-app my-app\rcd my-app\rnpm start create react app 不会处理后端逻辑或操纵数据库;它只是创建一个前端构建流水线(build pipeline),所以你可以使用它来配合任何你想使用的后端。它在内部使用 babel 和 webpack,但你无需了解它们的任何细节。当然,关于它,你肯定想了解更多,请参考 用户指南 。\n如果你倾向于从头开始打造你自己的 javascript 工具链,可以 查看这个指南,它重新创建了一些 create react app 的功能。\n核心概念 老规矩,上 \u0026quot;hello world\u0026quot; 😂\n// \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; const element = \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;; reactdom.render( element, document.getelementbyid(\u0026#39;root\u0026#39;) ); 它将在页面上展示一个 “hello, world!” 的标题。不要着急,马上你就明白它的工作原理了!\njsx 简介 再观察一下上面的例子,这是什么?\nconst element = \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;; \u0026gt; 怎么把 dom 元素直接赋给了一个变量 ❓\n这个有趣的标签语法既不是字符串也不是 html。它被称为 jsx,是一个 javascript 的语法扩展。\njsx 可以生成 react “元素”,它其实一个表达式,在编译(通过 babel)之后,会被转为普通 javascript 函数(react.createelement())调用,并且对其取值后得到 javascript 对象。\njsx 的语法格式十分简单!上 🌰\nconst name = \u0026#39;josh perez\u0026#39;; const element = ( \u0026lt;h1\u0026gt; hello, {name} \u0026lt;/h1\u0026gt; ); const element = \u0026lt;img src={user.avatarurl}\u0026gt;\u0026lt;/img\u0026gt;; 在 jsx 语法中,你可以在大括号内放置任何有效的 javascript 表达式。\n只需要注意:\n尽量将内容包裹在括号中,以避免多行书写时遇到自动插入分号陷阱; 在属性中嵌入 javascript 表达式时,不要在大括号外面加上引号; 使用 camelcase(小驼峰命名)来定义属性的名称,而不使用 html 属性名称的命名约定。 ok,这就是 jsx ,再来一个例子看看它的具体转译过程!\n// 我们用 jsx 是这样写的 const element = ( \u0026lt;h1 classname=\u0026#34;greeting\u0026#34;\u0026gt; hello, world! \u0026lt;/h1\u0026gt; ); // 被 babel 转译为 react.crateelement() 调用 const element = react.createelement( \u0026#39;h1\u0026#39;, {classname: \u0026#39;greeting\u0026#39;}, \u0026#39;hello, world!\u0026#39; ); // react.createelement() 会预先进行一些检查,实际上创建了如下对象, // 这些对象被称为 “react 元素”, // react 通过读取这些对象,然后使用它们来构建 dom 以及保持随时更新 // 注意:这是简化过的结构 const element = { type: \u0026#39;h1\u0026#39;, props: { classname: \u0026#39;greeting\u0026#39;, children: \u0026#39;hello, world!\u0026#39; } }; 是的,jsx 就是这么简单 ❗\n元素渲染 在上一节中,我们已经多次提到了 =react “元素” ,它究竟是什么呢?\n元素描述了你在屏幕上想看到的内容。如 element 就是一个 react 元素:\nconst element = \u0026lt;h1\u0026gt;hello, world\u0026lt;/h1\u0026gt;; 与浏览器的 dom 元素不同,react 元素是创建开销极小的普通对象(详见上节)。react dom 会负责更新 dom 来与 react 元素保持一致。\n\u0026gt; 那 react dom 到底是如何渲染 react 元素为 dom 的呢 ❓\n只需要把它们传入 reactdom.render() 就可以了(该元素会被自动渲染到根 dom 节点中)!\n需要注意的是, react 元素是不可变对象! 一旦被创建,你就无法更改它的子元素或者属性。\n如何更新 ui 呢?\n根据我们已有的知识,更新 ui 唯一的方式是创建一个全新的元素,并将其传入 reactdom.render()。react dom 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 dom 达到预期的状态。\nfunction tick() { const element = ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;it is {new date().tolocaletimestring()}.\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); reactdom.render(element, document.getelementbyid(\u0026#39;root\u0026#39;)); } // 每秒都创建一个新元素,并传入 reactdom.render() setinterval(tick, 1000); 当然,在实践中,我们并不会那么蠢,大多数 react 应用只会调用一次 reactdom.render(),后续我们将学习如何封装一个有状态的组件。\n组件 \u0026amp; props 组件允许你将 ui 拆分为独立可复用的代码片段,并对每个片段进行独立构思。\n组件,从概念上类似于 javascript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 react 元素。\n在 react 中,有两种组件形式:函数组件和类组件,如下:\n// 函数组件 function welcome(props) { return \u0026lt;h1\u0026gt;hello, {props.name}\u0026lt;/h1\u0026gt;; } // 类组件 class welcome extends react.component { render() { return \u0026lt;h1\u0026gt;hello, {this.props.name}\u0026lt;/h1\u0026gt;; } } 上述两个组件在 react 里是等效的。 它们返回的都是 react 元素哦!\n= 在实际应用中,函数式组件明显更受欢迎,也更符合直觉,再加上现在有了 hook,所以你懂得 ……*\n例如,这段代码会在页面上渲染 “hello, sara”:\nfunction welcome(props) { return \u0026lt;h1\u0026gt;hello, {props.name}\u0026lt;/h1\u0026gt;; } const element = \u0026lt;welcome name=\u0026#34;sara\u0026#34; /\u0026gt;; reactdom.render( element, document.getelementbyid(\u0026#39;root\u0026#39;) ); 让我们来回顾一下这个例子中发生了什么:\n我们调用 reactdom.render() 函数,并传入 \u0026lt;welcome name=\u0026quot;sara\u0026quot; /\u0026gt; 作为参数; react 调用 welcome 组件,并将 {name: 'sara'} 作为 props 传入; welcome 组件将 \u0026lt;h1\u0026gt;hello, sara\u0026lt;/h1\u0026gt; 元素作为返回值; react dom 将 dom 高效地更新为 \u0026lt;h1\u0026gt;hello, sara\u0026lt;/h1\u0026gt;。 注意: 组件名称必须以大写字母开头 !!!(react 会将以小写字母开头的组件视为原生 dom 标签)\n组件可以在其输出中引用其他组件(=组件组合)。有时候,将组件拆分为更小的组件也是很不错的选择(=组件提取)。\n所有 react 组件都必须像纯函数一样保护它们的 props 不被更改。\n= 其实,props 很简单,就把它理解为一个只读的函数入参就行了!函数,你足够了解的,对吧?*\nprops 是不可变的,但应用程序的 ui 是动态的,并会伴随着时间的推移而变化,emm\u0026hellip; 😟\n放心!在下一章节中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state 允许 react 组件随用户操作、网络响应或者其他变化而动态更改输出内容。\nstate \u0026amp; 生命周期 在元素渲染章节中,我们只了解了一种更新 ui 界面的方法,通过调用 reactdom.render() 来修改我们想要渲染的元素。\n我们也说了,那种方法有点蠢 🤣! 在本章节中,我们将学习如何封装真正可复用的组件。\nstate 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。\n下面,让我们看一个完整的 clock 组件(请留意注释内容):\nclass clock extends react.component { // 构造函数 - 用来初始化的 constructor(props) { // 将 props 传递到父类的构造函数中 ❓ // class 组件应该始终使用 props 参数来调用父类的构造函数 super(props); // 在构造函数中为 this.state 赋初值 this.state = { data: new date() }; // 将生命周期方法添加到 class 中 // ^ 组件挂载 componentdidmount() { // 尽管 this.props 和 this.state 是 react 本身设置的,且都拥有特殊的含义, // 但是其实你可以向 class 中随意添加不参与数据流(比如计时器 id)的额外字段 this.timerid = setinterval(() =\u0026gt; this.tick(), 1000 ); } // ^ 组件卸载 componentwillunmount() { clearinterval(this.timerid); } tick() { // 使用 this.setstate() 来时刻更新组件 state this.setstate({ date: new date() }); } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;it is {this.state.date.tolocaletimestring()}.\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); } } } reactdom.render( \u0026lt;clock /\u0026gt;, document.getelementbyid(\u0026#39;root\u0026#39;) ); 让我们来快速概括一下发生了什么和这些方法的调用顺序:\n当 \u0026lt;clock /\u0026gt; 被传给 reactdom.render() 的时候,react 会调用 clock 组件的构造函数。因为 clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state 。我们会在之后更新 state ; 之后 react 会调用组件的 render() 方法。这就是 react 确定该在页面上展示什么的方式。然后 react 更新 dom 来匹配 clock 渲染的输出; 当 clock 的输出被插入到 dom 中后,react 就会调用 componentdidmount() 生命周期方法。在这个方法中,clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法; 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,clock 组件会通过调用 setstate() 来计划进行一次 ui 更新。得益于 setstate() 的调用,react 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。react 也会相应的更新 dom; 一旦 clock 组件从 dom 中被移除,react 就会调用 componentwillunmount() 生命周期方法,这样计时器就停止了。 是的!state 就是一个组件的核心!!! 下面我们来看一下,如何正确的使用它!\n// 🅰️ 不要直接修改 state,应该使用 setstate() // 构造函数是唯一可以给 this.state 赋值的地方: this.state.comment = \u0026#39;hello\u0026#39;;\t// ❌ this.setstate({comment: \u0026#39;hello\u0026#39;});\t// ✔️ // 🅱️ state 的更新可能是异步的,不要依赖他们的值来更新下一个状态 this.setstate({ counter: this.state.counter + this.props.increment, });\t// 可能 ❌ // 要解决这个问题,可以让 setstate() 接收一个函数而不是一个对象, // 这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数 this.setstate((state, props) =\u0026gt; ({ counter: state.counter + props.increment }));\t// ✔️ 数据是向下流动的!\n不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。\n组件可以选择把它的 state 作为 props 向下传递到它的子组件中。\n事件处理 react 元素的事件处理和 dom 元素的很相似,但是有一点语法上的不同:\nreact 事件的命名采用小驼峰式(camelcase),而不是纯小写; 使用 jsx 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串; 在 react 中另一个不同点是你不能通过返回 false 的方式阻止默认行为,你必须显式的使用 preventdefault 。 function actionlink() { function handleclick(e) { // 在这里,e 是一个合成事件,react 根据 w3c 规范来定义这些合成事件 e.preventdefault();\t// 显式的使用 ✔️ console.log(\u0026#39;the link was clicked.\u0026#39;); } return ( \u0026lt;a href=\u0026#34;#\u0026#34; onclick={handleclick}\u0026gt;\t// 注意,大括号外不要加引号 click me \u0026lt;/a\u0026gt; ); } 另外,当你使用 es6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。如下:\nclass toggle extends react.component { constructor(props) { super(props); this.state = {istoggleon: true}; // 为了在回调中使用 `this`,这个绑定是必不可少的,否则 // 当你调用 onclick={this.handleclick} 这个事件函数回调的时候, // `this` 的值为 `undefined` ,会报错 this.handleclick = this.handleclick.bind(this); // bind 太麻烦 ?试试下面这个等效写法 - class fields 语法 // create react app 默认启用此语法 // 此语法确保 `handleclick` 内的 `this` 已被绑定 // 注意:这是 *实验性* 语法 // handleclick = () =\u0026gt; { // console.log(\u0026#39;this is:\u0026#39;, this); // } } handleclick() { this.setstate(state =\u0026gt; ({ istoggleon: !state.istoggleon })); } render() { return ( \u0026lt;button onclick={this.handleclick}\u0026gt; {this.state.istoggleon ? \u0026#39;on\u0026#39; : \u0026#39;off\u0026#39;} \u0026lt;/button\u0026gt; ); } } reactdom.render( \u0026lt;toggle /\u0026gt;, document.getelementbyid(\u0026#39;root\u0026#39;) ); 你必须谨慎对待 jsx 回调函数中的 this,在 javascript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleclick 并把它传入了 onclick,当你调用这个函数的时候 this 的值为 undefined。\n这并不是 react 特有的行为,这其实与 javascript 函数工作原理有关。\n= emm\u0026hellip; this 可以说是 javascript 永远的痛了,好在应用起来并不算太难!*\n在事件处理中,除了 this 的绑定之外,还有一个需要注意的地方 - 向事件处理程序传递参数。\n在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 id,以下两种方式都可以向事件处理函数传递参数:\n\u0026lt;button onclick={(e) =\u0026gt; this.deleterow(id, e)}\u0026gt;delete row\u0026lt;/button\u0026gt; \u0026lt;button onclick={this.deleterow.bind(this, id)}\u0026gt;delete row\u0026lt;/button\u0026gt; 上述两种方式是等价的,分别通过箭头函数和 function.prototype.bind 来实现。\n在这两种情况下,react 的事件对象 e 会被作为第二个参数传递。 如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。\n条件渲染 这里,就不多讲了,只要记住 jsx 最终会被转成一个 javascript 对象,条件渲染也就是 if 或者条件运算符那点事了。\n在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。在组件的 render 方法中返回 null 并不会影响组件的生命周期。\n想要了解更多,直接阅读 条件渲染 。\n列表 \u0026amp; key 同上,略!\n唯一需要注意的是 key ,它是什么?\nkey 帮助 react 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。\n元素的 key 只有放在就近的数组上下文中才有意义。\n= 所谓列表,就是利用一些迭代数据,组装出可用子元素集合,然后把它们放在应该放的父元素中就可以了。*\n详见 列表 \u0026amp; key 。\n表单 主要是弄清楚 “受控组件” 和 “非受控组件” 的概念,就可以喽。详见 表单 。\n状态提升 = 抽象和共享,永远不变的真理!\n通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。\n在 react 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。\n更多详见 状态提升。\n组合 vs 继承 详见 组合 vs 继承 – react - react.docschina.org。\nreact 哲学 我们认为,react 是用 javascript 构建快速响应的大型 web 应用程序的首选方式。\n= emm\u0026hellip; vue:我才是,angular:你们都是弟弟!\nok,上心法 ❤️。\n第一步:将设计好的 ui 划分为组件层级\n第二步:用 react 创建一个静态版本\n第三步:确定 ui state 的 =最小(且完整)表示\n通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:\n该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。 该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。 你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。 第四步:确定 state 放置的位置\n第五步:添加反向数据流\n= 基础的核心概念并不多(毕竟就是一个 ui 库嘛),但其思想非常好,官方文档也相当 ok ,可以不定期地多看几遍。\nhook = 不着急,先过几遍再说这个,很简单的!*\n相关技术栈 当然,你可以选择从零开始,但更好的选择是使用官方提供的脚架 - create react app 。\n样式 react 对样式如何定义并没有明确态度;如果存在疑惑,比较好的方式是和平时一样,在一个单独的 *.css 文件定义你的样式,并且通过 classname 指定它们。\nreact 并没有原生提供 css 封装方案!!!\nreact 本身的设计原则决定了其不会提供原生的 css 封装方案,或者说 css 封装并不是 react 框架本身的关注点。因此 ,react 社区从很早的时候就开始寻找相关替代办法。\n- css 模块化(css modules)\r这种做法非常类似 angular 与 vue 对样式的封装方案,其核心是以 css 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。对于大多数 react 项目来说,这种方案已经足够用了。\r- 基于共识的人工维护的方法论,如 bem\r这种方法的缺点是会为团队带来很大的挑战,对于全局和局部规划选择器的命名,团队对于这种方法需要有共识,即使熟练使用的情况下,在使用中依然有着较高的思维负担和维护成本。\r- shadow dom\r借助 direflow.io 等工具,我们可以将 react 组件输出为 web component,借助 shadow dom 实现组件的 css 样式封装。这是一种解决办法,不过基本很少有项目选择这样做。\r- css-in-js _1. scss\n好吧,相信你的项目是由 create react app (cra) 生成的,如果你想使用 scss ,只需要安装 dart-sass 库即可,像下面这样:\nnpm i sass --save-dev 感谢 node-sass 退出历史舞台,但感谢作者的贡献 😅!\nok,安装之后,就可以把 *.scss 文件作为一个模块引入了,如:\nimport example from \u0026#39;./example.scss\u0026#39;; _2. css in js\n注意此功能并不是 react 的一部分,而是由第三方库提供。\n“css-in-js” 是指一种模式,其中 css 由 javascript 生成而不是在外部文件中定义。在 此处 阅读 css-in-js 库之间的对比。\n= css in js 的本质就是写行内样式 style ❓❗*\nconst style = { \u0026#39;color\u0026#39;: \u0026#39;red\u0026#39;, \u0026#39;fontsize\u0026#39;: \u0026#39;46px\u0026#39; }; const clickhandler = () =\u0026gt; alert(\u0026#39;hi\u0026#39;); reactdom.render( \u0026lt;h1 style={style} onclick={clickhandler}\u0026gt; hello, world! \u0026lt;/h1\u0026gt;, document.getelementbyid(\u0026#39;example\u0026#39;) ); 当然,大项目,这样直接写是非常不明智的,好在有懒人包 🥳!\n目前比较流行的两个解决方案是 styled-components 和 emotion 。\n相关参考:\ncss-in-js:一个充满争议的技术方案 - 知乎 - zhuanlan.zhihu.com _3. css modules 🏆(首推)\n这种做法非常类似 angular 与 vue 对样式的封装方案,其核心是以 css 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。对于大多数 react 项目来说,这种方案已经足够用了。\n由于一般的脚手架都默认集成了 css modules,比如 react 官方的脚手架:create-react-app,已经将 css modules 集成进来了,我们可以直接使用。\n如何使用呢?\n详见 在 react 中使用 css modules。\n路由 = hmmm\u0026hellip; 页面路由,大大的有用!\nreact-router-dom 使用指南(最新 v6.0.1) - 知乎 - zhuanlan.zhihu.com introduction · react router 中文手册 - uprogrammer.cn ","date":"2022-09-19","permalink":"https://loveminimal.github.io/posts/react/","summary":"\u003cp\u003e\u003ca href=\"%5Bhttps://%5D(https://react.docschina.org/docs/getting-started.html)\"\u003e📔 官方文档\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e具体安装及引入细节,请直接参考官方文档。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eReact 是一个用于构建用户界面的 JavaScript 库,你可以用它给简单的 HTML 页面增加一点交互,也可以开始一个完全由 React 驱动的复杂应用。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 对的,它只是一个 UI 库而已 !!!\u003c/code\u003e\u003c/p\u003e","title":"react"},{"content":"该主题已上传到官方主题面,跳转了解 hugo-theme-virgo ,当然也可以直接访问 github 仓库 。\n更新日志 - 2023-03-23 14:58 增加 utterances 评论插件\r- 2023-03-22 16:08 更新主页快捷联系选项,增加知乎和简书\r- 2023-03-21 17:58 更新导航页面文件结构 - `nav.md` 或 `nav/index.md`\r- 2023-03-19 00:05 优化导航页面快速跳转 快速开始 首先,下载该主题。\ncd your_site_dir\t# 本地站点目录 git clone https://github.com/loveminimal/hugo-theme-virgo.git themes/virgo # or cd your_site_dir git submodule add https://github.com/loveminimal/hugo-theme-virgo.git themes/virgo 💡 使用哪种方式呢?如果你有这个疑问,那么就选择第一种。\n然后,更新你站点的 config.toml 内容,如下(后续可按需修改):\nbaseurl = \u0026#39;https://ovirgo.com/\u0026#39; languagecode = \u0026#39;zh-cn\u0026#39; title = \u0026#39;virgo\u0026#39; # 启用主题 theme = \u0026#39;virgo\u0026#39; hascjklanguage = true summarylength = 80 paginate = 11 enablegitinfo = true [params] author = \u0026#39;jack\u0026#39; slogan = \u0026#39;life should be interesting.\u0026#39; # \u0026#39;life is just a joker.\u0026#39; # 页面语言,默认中文 en = false # 英文首页标题,默认 \u0026#39;virgo\u0026#39; hometitleen = \u0026#39;virgo\u0026#39; # 中文首页标题,默认 ‘一晌贪欢’ hometitlezh = \u0026#39;一晌贪欢\u0026#39; # 激活暗色模式, # 由于静态页面的限制,我们使用浏览器本地存储来记忆该状态, # 如果设置为 `true` 后,默认不是暗色模式,清除浏览器缓存后刷新页面即可 dark = false # 文章列表页单列显示 issinglecolumnofpostlist = true # 是否显示相邻页链接 isshowprevnextlink = true # 激活页面加载时的过渡动画 hasactiveanimate = true # 激活 cool 模式,酷爽但是消耗资源也更多, # 如果想更换页面背景,只需要将图片命名为 `default.jpg` 后,置于 `/static/imgs/bg` 文件夹中即可, # 浏览器有缓存,更换后强制页面刷新(快捷键为 ctrl+shift+r)一下即可 hasactivecool = true # 展开/折叠代码块,默认不折叠, # 设置为 `true` ,则默认折叠所有代码块, # 提示,在移动设备中,系统设置为永久折叠代码块 # (该项设置不重要,完全是个人喜好) hasfoldallcodeblocks = false # 如下导航链接,你应该创建对应的 `.md` 文件,以生成对应的页面 # ----------------------------------- # nav - nav.md or nav/index.md # search - search.md or search/index.md # archive - archive.md or archive/index.md # wiki - posts/wiki.md or wiki/index.md # about - about.md or about/index.md # ----------------------------------- # 菜单选项定制,使用 `00、01、23` 等进行选项顺序调整 # 🐶🎉👀💡👓🐌 [params.menu] [params.menu.00] active = true path = \u0026#39;/nav\u0026#39; en = \u0026#39;nav\u0026#39; zh = \u0026#39;导航\u0026#39; icon = \u0026#39;🎯\u0026#39; [params.menu.11] active = true path = \u0026#39;/search\u0026#39; en = \u0026#39;search\u0026#39; zh = \u0026#39;搜索\u0026#39; icon = \u0026#39;🔎\u0026#39; [params.menu.22] active = true path = \u0026#39;/posts\u0026#39; en = \u0026#39;posts\u0026#39; zh = \u0026#39;文章\u0026#39; icon = \u0026#39;📜\u0026#39; [params.menu.33] active = true path = \u0026#39;/archive\u0026#39; en = \u0026#39;archive\u0026#39; zh = \u0026#39;归档\u0026#39; icon = \u0026#39;🎉\u0026#39; [params.menu.44] active = true path = \u0026#39;/posts/wiki\u0026#39; en = \u0026#39;wiki\u0026#39; zh = \u0026#39;百科\u0026#39; icon = \u0026#39;🚀\u0026#39; [params.menu.55] active = true path = \u0026#39;/about\u0026#39; en = \u0026#39;about\u0026#39; zh = \u0026#39;关于\u0026#39; icon = \u0026#39;🐌\u0026#39; # 首页图片/文字 [params.img] # 如果你不想显示图片,想显示一段话,只需要 # 设置 `noimgbutwords` 为 true 即可 notimgbutwords = false # 内置了 `girl.jpg, wukong.jpg, and tux.jpg, cat.svg ……`,当然你可以 # 把自己喜欢图片放在 `static/imgs/` 目录中,并在 `src` 引用它, # 你还可以通过 `width` 调整引入图片的显示大小, # 如果,将 `width` 设置为 \u0026#39;\u0026#39; 或 0 , # 将默认使用图片自身分辨率尺寸 src = \u0026#39;cat.svg\u0026#39; width = 0 # words = \u0026#34;stay hungry, stay foolish. \u0026lt;br\u0026gt;your time is limited, so don\u0026#39;t waste it living someone else\u0026#39;s life. \u0026lt;br\u0026gt;have the courage to follow your heart and intuition. they somehow already know what you truly want to become. everything else is secondary. \u0026lt;br\u0026gt;-- steve jobs.\u0026#34; # words = \u0026#34;多少事,从来急;\u0026lt;br\u0026gt;天地转,光阴迫。\u0026lt;br\u0026gt;一万年太久,只争朝夕。\u0026lt;br\u0026gt;-- 教员\u0026#34; words = \u0026#34;“照顾好自己的身体和情绪,\u0026lt;br\u0026gt;这场人生,\u0026lt;br\u0026gt;你就赢了一大半,\u0026lt;br\u0026gt;其余的其余,\u0026lt;br\u0026gt;人生自有安排。”\u0026#34; [params.contact] icp = \u0026#39;豫 icp 备 2022002918 号-1\u0026#39; # 备案号,如果你不想显示,设置为 \u0026#39;\u0026#39; 空即可 icplink = \u0026#39;//beian.miit.gov.cn\u0026#39; # 备案链接 weibo = \u0026#39;6867589681\u0026#39; # e.g. https://weibo.com/u/6867589681 wechat = \u0026#39;imgs/bg/wechat-public.jpg\u0026#39; # 微信二维码地址 # zhihu = \u0026#39;loveminimal\u0026#39; # e.g. https://www.zhihu.com/people/loveminimal # jianshu = \u0026#39;eebcc2974936\u0026#39; # e.g. https://www.jianshu.com/u/eebcc2974936 email = \u0026#39;loveminimal@outlook.com\u0026#39; github = \u0026#39;loveminimal\u0026#39; # e.g. https://github.com/loveminimal bilibili = \u0026#39;11608450\u0026#39; # e.g. https://space.bilibili.com/11608450 # twitter = \u0026#39;loveminimal163\u0026#39; # e.g. https://twitter.com/loveminimal163 # facebook = \u0026#39;loveminimal\u0026#39; # e.g. https://facebook.com/loveminimal # instagram = \u0026#39;loveminimal163\u0026#39; # e.g. https://www.instagram.com/loveminimal163 # youtube = \u0026#39;uckwibwe3rztdambs0gjngka\u0026#39; # e.g. https://www.youtube.com/channel/uckwibwe3rztdambs0gjngka # telegram = \u0026#39;loveminimal\u0026#39; # e.g. https://web.telegram.org/k/#@loveminimal color = \u0026#39;#696969\u0026#39; # 图标颜色,默认为浅灰色 slogan = \u0026#39;find me via : )\u0026#39; # 联系标语,不想显示,可以置空 # 在开发环境下(http://localhost:1313/),不再启用评论插件, # 如果想在开发环境下启用它,修改服务端口即可,如下 # hugo server -p=1314 [params.utterances] active = true # 是否启用评论插件 repo = \u0026#34;loveminimal/comment\u0026#34; # 输入你的仓库名称 issueterm = \u0026#34;pathname\u0026#34; theme = \u0026#34;github-light\u0026#34; crossorigin = \u0026#34;anonymous\u0026#34; # 以下为 markdown 解析擎的一些设置, # 建议保持不变 [markup] [markup.asciidocext] preservetoc = true [markup.highlight] # 代码块显示风格、行号显示 style = \u0026#34;github\u0026#34; linenos = false [markup.tableofcontents] endlevel = 3 ordered = false startlevel = 2 [markup.goldmark] [markup.goldmark.renderer] unsafe = true 现在,你就可以运行 hugo server -d 开始你的折腾之旅了。\n导航页 如果,你想使用导航页并正确显示,那么就应该严格按照下面这种格式搭建你的 nav.md 文件结构:\n--- title: \u0026#34;nav\u0026#34; draft: false --- \u0026lt;div class=\u0026#34;nav\u0026#34;\u0026gt; ## 🌞 *分类 one* - [mineitem one](/) - [mineitem two](/archive) - [mineitem three](https://nav-item-three.com) ## 🔨 *分类 two* - [toolitem three](https://nav-item-three.com) ## 📑 *分类 three* - [docitem one](/) - [docitem two](/archive) - [docitem three](https://nav-item-three.com) \u0026lt;/div\u0026gt; ## 🔖 *标签 bookmarks* \u0026lt;div class=\u0026#34;bookmark\u0026#34;\u0026gt; - bookmark item one https://bookmark-item-one.com - bookmark item two https://bookmark-item-two.com - bookmark item three https://bookmark-item-three.com \u0026lt;/div\u0026gt; 为什么要做格式方面的限制呢?\n众所周知,markdown 对 table 的支持很一般,鉴于导航页的内容主要是外链和书签,使用列表管理是最方便的。另外,我们会使用 js 进行内容项的统计,所以就需要使用者遵守格式,不然可能页面显示可能会不正常。\n标记语法增强 \u0026gt; 使用 js 对 markdown 做出的一些增强性修改\n不止一次吐槽过 markdown 虽然是纯文本性质的,但是其某些标记语法真的是让人不敢恭维,直观性和表现力都是一般。不过,从另一个方面来说,本来就是轻量级的标记语言,不可能承载太多。\n本来想直接修改 markdown 引擎来实现,研究了一下,还要颇费一番工夫。鉴于仅满足于个人使用,用一些曲线方式使用 js 来实现反而更加简单些。\n此处就记录一下针对 hugo-theme-virgo 做的一些魔改。\n行内格式 markdown 中的行内格式有以下几种:\n语法 效果 转译 html 标签 **加粗** 加粗 \u0026lt;strong\u0026gt;加粗\u0026lt;/strong\u0026gt; *斜体* 斜体 \u0026lt;em\u0026gt;斜体\u0026lt;/em\u0026gt; ~~删除线~~ 删除线 \u0026lt;del\u0026gt;删除线\u0026lt;/del\u0026gt; ` 行内代码 行内代码 \u0026lt;code\u0026gt;行内代码\u0026lt;/code\u0026gt; \u0026mdash; 下划线 \u0026lt;u\u0026gt;下划线\u0026lt;/u\u0026gt; 是的,markdown 中没有下划线的标记语法。\n本来想用行内代码的标记格式做魔改,鉴于博文中出现行内的代码的概率较高,遍历起来相对更耗性能(虽然并没有多少),故决定选择 *斜体* 语法标记,其使用频率不多,且其对应的 org mode 中可以直接显为粗体显示。\n新增语法 效果 *_下划线* _下划线 *=高亮* =高亮 | *-高亮* | -高亮 | ❌\n| *=吐槽系* | =吐槽系 | ❌\n如此,我们便增加了 _下划线 和 =高亮 两种语法标识了。另外,在文章中,尤其是一些摘录和转载的文章中,我们需要做一些随笔,之前我们是使用 \u0026lt;div class=\u0026quot;oh-essay\u0026quot;\u0026gt;...\u0026lt;/div\u0026gt; 这种标签插入,如上表,我们也对其做了语法标识。\n如上,所示,更改了一些语法标记,因为有的 markdown 引擎中使用 ==高亮== 来高亮文本,我们这里就用 *=高亮* 来表示,以做到在观感上统一。\n另外,我们不再使用 *=吐槽系* 来表示个人在摘录或编辑中的个人想法展示,主要是由于在不支持当前语法标记的主题中,它只能以斜体展示,不容易和正文内容作区分。我们使用 \u0026gt;= 吐槽系 或 \u0026gt; = 吐槽系 来表示,如此在不支持的情况下,可以解析为引用样式,便于区分。\n之前我们做了一些 snippet 进行 html 标签的插入,以实现以上效果,但是这就限定在了某些编辑器中,些许背离了纯文本输入的理念,以上小小的增强,使得我们可以任何文本编辑器中进行方便的文本输入。\n\u0026gt;= 好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅\r\u0026gt; =好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅\r\u0026gt; = 好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅 上述语法标记是等价的,会被解析为如下样式:\n= 好吧,尽管它们只能在 hugo-theme-virgo 中才有效果 😅\n代码块折叠 在 markdown 中,包裹代码块很方便。但有时候在博文中,我们可能引入较多的代码片段,这会导致正文内容的间断,所以,允许其进行折叠,可以在 config.tom 中,使用 hasfoldallcodeblocks: true 进行初始化。\n既然已经可以折叠了,这里我们不妨用它再做一个更通用的折叠板(默认折叠),原理也很简单,利用 lang 判别。\n如果其为 _lang 这种格式,则表示轻量级代码折叠,效果如下:\n这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。 可以看到,上面的内容并没有换行,如果它的代码内容,自然是没有问题的,但对于一个通用的折叠板来说,我们希望其可以自动换行,很简单,使用 __lang 即可,如下:\n会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽! wiki 图片链接语法渲染 最近使用 obsidian ,其使用的链接及图片格式为 wiki 语法,如下:\n名称 描述 链接 [[link]] 链接(带描述) [[link | desc]] 图片 ![[path/to/img]] 图片(带尺寸) ![[path/to/img | 200]] hugo 默认的 markdown 引擎是不支持渲染这种语法的,我们这里做了一下增强,现在你可以畅快地使用 obsidian 来编辑你的博客了。\n= 图片和链接,好像 wiki 的这种语法写起来更加简洁。其实,还是使用 \u0026lt;img\u0026gt; 标签的通用性更好些,不过许多软件的即时渲染又不支持,就很伤。\n*为什么开始使用 obsidian ?\n它的双向链接功能不错,使用了几天,整个编辑体验是要比 typora 好的,后者其实也不弱,但扩展性不好,开发者方面迭代也好慢。比如,其默认 快速打开 文件是在新的窗口(想要在当前窗口打开),而且无法设置,邮件也发了,issue 也提了,久久没有回应,这就让人很无奈。相对来说,vscode 和 obsidian 这种具有第三方插件扩展的,不满意的话可以自己修改,就很不错,emacs 和 vim 更是灵活扩展方面的翘楚。\n站点编辑 你可以在 《我是怎么写博客的》 了解到本站内容的日常编写流程。\n","date":"2022-09-13","permalink":"https://loveminimal.github.io/posts/about-virgo/","summary":"\u003cp\u003e该主题已上传到官方主题面,跳转了解 \u003ca href=\"https://themes.gohugo.io/themes/hugo-theme-virgo/\"\u003ehugo-theme-virgo\u003c/a\u003e ,当然也可以直接访问 \u003ca href=\"https://github.com/loveminimal/hugo-theme-virgo\"\u003eGithub 仓库\u003c/a\u003e 。\u003c/p\u003e","title":"♫ 关于 virgo 需要知道的一些事"},{"content":" 好吧,内容不少,为了后续插入图片之后 ,页面太大,我们这里拆分到两个页面中。\n26. 图形用户界面 i.e. graphical user interfaces\n(。・∀・)ノ゙嗨 我是 carrie anne 欢迎收看计算机科学速成课。\n我们上集最后 ,谈了苹果在 1984 年发布的 macintosh ,这是普通人可以买到的 第一台带图形用户界面的计算机,还带一个鼠标。那时的计算机全是命令行, 图形界面是个革命性进展,不必记住或猜正确的命令,图形界面直接显示了,你可以做什么,只要在屏幕上找选项就行了。这是一个\u0026quot;选择并点击\u0026quot;的界面,突然间计算机更直观了。不只是爱好者或科学家能用计算机 ,任何人都可以用计算机解决问题。\n人们认为是 macintosh 把图形用户界面(gui)变成主流,但实际上图形界面是数十年研究的成果。前几集,我们讨论了早期的交互式图形程序,比如 sketchpad 和太空战争 都是 1962 年制作的,但都是一次性项目,不是整合良好的体验,现代图形界面的先驱 可以说是 道格拉斯·恩格尔巴特。\n让我们进入思想泡泡!\n二战期间 恩格尔巴特 驻扎在菲律宾做雷达操作员,他读了 万尼瓦尔·布什 的 memex 文章,这些文章启发了他。当他海军服役结束时,他回到学校 1955 年在 ucb 取得博士学位,他沉溺于新兴的计算机领域,他在 1962 年一份开创性报告中 汇集了各种想法,报告名为:\u0026ldquo;增强人类智力\u0026rdquo;。恩格尔巴特认为,人类面临的问题 比解决问题的能力增长得更快,因此,找到增强智力的方法 似乎是必要且值得一做的目标。他构想计算机不仅做自动化工作,也可以成为未来知识型员工 应对复杂问题的工具。\n伊凡·苏泽兰 的\u0026quot;几何画板\u0026quot; 进一步启发了 恩格尔巴特,他决定动手把愿景变为现实 开始招募团队来做 on-line system ,他意识到如果只有键盘 ,对他想搭建的程序来说是不够的。用他的话说:\u0026ldquo;我们设想人们用计算机辅助工作站来增强工作,用户需要和屏幕上的信息互动,用某种设备在屏幕上移动 [光标]\u0026quot;。\n1964 年,和同事比尔·英格利希的共同努力下,他创造了第一个计算机鼠标,尾部有一根线,看起来很像老鼠 因此\u0026quot;鼠标\u0026quot;这个名字沿用了下来。\n谢了思想泡泡!\n1968 年 恩格尔巴特 在\u0026quot;秋季计算机联合会议\u0026quot;展示了他的系统 ,这次演示 被视为如今所有演示的祖先,演示有 90 分钟 展现了现代计算机的许多功能:包括 位图图像、视频会议、文字处理和实时协作编辑文件,还有现代图形界面的原型 - 比如鼠标和多窗口, 不过窗口不能重叠,远远先于那个时代。\n就像其它\u0026quot;跨时代\u0026quot;的产品一样,它最终失败了,至少商业上是这样,但它对当时的计算机研究者影响巨大,恩格尔巴特 因此获得 1997 年图灵奖。\n政府资金在 1970 年代初开始减少,我们在两集前说过(第 24 集:冷战和消费主义)。那时,恩格尔巴特团队里的许多人,包括比尔·英格利希去了施乐公司新成立的\u0026quot;帕洛阿尔托研究中心\u0026rdquo;,更为人熟知的名字是 xerox parc 。他们在这里开发了第一台带真正 gui 的计算机:施乐奥托 , 于 1973 年完成。为了让计算机易于使用,需要的不只是花哨的图形,还要借助一些人们已经熟悉的概念,让人们不用培训 就能很快明白如何使用。施乐的答案是将 2d 屏幕当作\u0026quot;桌面\u0026quot;,就像桌面上放很多文件一样,用户可以打开多个程序 每个程序都在一个框里,叫\u0026quot;窗口\u0026quot;,就像桌上的文件一样。窗口可以重叠,挡住后面的东西,还有桌面配件,比如计算器和时钟。用户可以把配件在屏幕上四处移动,它不是现实桌面的完美复制,而是用桌面这种隐喻,因此叫\u0026quot;桌面隐喻\u0026quot;。有很多方法来设计界面, 但 alto 团队用窗口,图标,菜单和指针来做 - 因此叫 wimp 界面。如今大部分图形界面都用这个,它还提供了一套基本部件,可复用的基本元素, 比如按钮,打勾框,滑动条和标签页,这些也来自现实世界,让人们有熟悉感,gui 程序就是这些小组件组成的。\n让我们试着写一个简单例子。\n首先,我们必须告诉操作系统 为程序创建一个窗口\r我们通过 gui api 实现 需要指定窗口的名字和大小\r假设是 500×500 像素\r现在再加一些小组件,一个文本框和一个按钮\r创建它们需要一些参数\r首先要指定出现在哪个窗口 因为程序可以有多个窗口\r还要指定默认文字窗口中的 x,y 位置 以及宽度和高度\r好,现在我们有个 看起来像 gui 程序的东西\r但它还没有功能\r如果点 roll 按钮,什么也不会发生\r在之前的例子中,代码是从上到下执行的\r但 gui 是 \u0026#34;事件驱动编程\u0026#34;\r代码可以在任意时间执行 以响应事件\r这里是用户触发事件 比如点击按钮,选一个菜单项,或滚动窗口\r或一只猫踩过键盘\r就会一次触发好多事件!\r假设当用户点 roll 按钮\r我们产生 1 到 20 之间的随机数\r然后在文本框中,显示这个数字\r我们可以写一个函数来做\r我们还可以让它变有趣些,假设随机数是 20 就把背景颜色变成血红色!\r最后,把代码与\u0026#34;事件\u0026#34;相连 每次点按钮时 都触发代码\r那么,要设置事件触发时 由哪个函数来处理\r我们可以在初始化函数中,加一行代码来实现\r我们要处理的,是\u0026#34;点击\u0026#34;事件 然后函数会处理这个事件\r现在完成了 可以点按钮点上一整天 每次都会执行 rolld20 函数,这就是程序背后的原理。在编辑器里点 粗体 ,或菜单里选 关机 ,一个处理该事件的函数会触发,希望不会随机到 20,啊!!!\n好,现在回到施乐奥托!\n大约制作了 2000 台奥托有的在施乐公司内部用,有的送给大学实验室,从来没有商业出售过,然而,parc 团队不断完善硬件和软件,最终于 1981 年发布了 施乐之星系统,施乐之星扩展了\u0026quot;桌面隐喻\u0026quot;。现在文件看起来就像一张纸 ,还可以存在文件夹里,这些都可以放桌面上,或数字文件柜里,这样来隐喻底层的文件系统。\n从用户角度来看,是一层新抽象!\n施乐卖的是印刷机 但在文本和图形制作工具领域也有领先,例如,他们首先使用了 \u0026ldquo;剪切\u0026quot;\u0026ldquo;复制\u0026quot;\u0026ldquo;粘贴\u0026quot;这样的术语,这个比喻来自编辑打字机文件,真的是剪刀\u0026quot;剪切\u0026rdquo; 然后胶水\u0026quot;粘贴\u0026rdquo; 到另一个文件,然后再复印一次,新文件就是一层了,看不出编辑的痕迹……\n感谢计算机的出现!\n文字处理软件出现后 这种手工做法就消失了。apple ii 和 commodore pet 上有文字处理软件,但施乐在这点上走的更远。无论你在计算机上做什么, 文件打印出来应该长得一样,他们叫这个\u0026quot;所见即所得\u0026rdquo;。不幸的是,就像恩格尔巴特的 on-line system ,施乐之星也领先于那个时代,销售量不高,因为在办公室里配一个,相当如今 20 万美元 。ibm 同年推出了 ibm pc ,之后便宜的\u0026quot;ibm 兼容\u0026quot;计算机席卷市场,但 parc 研究人员花了十几年做的这些 没有被浪费。\n1979 年 12 月,施乐之星出货前一年半,有个人去施乐公司参观 你可能听说过这个人:史蒂夫·乔布斯。这次参观有很多传闻,许多人认为乔布斯和苹果偷走了施乐的创意,但那不是事实。事实上是施乐公司主动找苹果,希望合作,最终施乐还买了苹果的一百万美元股份,在苹果备受瞩目的 首次公开募股 (ipo) 前买的,但一个额外条款是: \u0026ldquo;公布一切施乐研究中心正在进行的酷工作\u0026rdquo; 。史蒂夫知道他们很厉害,但他完全没预想到这些,其中有个演示是,一个清晰的位图显示器上,运行着施乐公司的图形界面 ,操作全靠鼠标直观进行。史蒂夫后来说:\u0026ldquo;就像拨开了眼前的一层迷纱,我可以看到计算机产业的未来\u0026rdquo;。史蒂夫和随行的工程师回到苹果公司,开始开发新功能,比如菜单栏和垃圾桶,垃圾桶存删除文件,满了甚至会膨胀 - 再次使用了隐喻。苹果第一款有图形界面和鼠标的产品,是 1983 年发行的 apple lisa ,一台超级先进的机器,标了\u0026quot;超级先进\u0026quot;的价格 - 差不多是如今的 25000 美元。虽然比施乐之星便宜不少,但在市场上同样失败。幸运的是,苹果还有另一个项目: macintosh,于 1984 年发布,价格大约是如今的 6000 美元 - lisa 的四分之一。它成功了,开售 100 天就卖了 7 万台,但在最初的狂潮后,销售额开始波动。苹果公司卖的 apple ii 比 mac 多,一个大问题是:没人给这台新机器做软件,之后情况变得更糟,竞争对手赶上来了。不久,其它价格只有 mac 几分之一的个人计算机 有了原始但可用的图形界面,消费者认可它们, pc 软件开发者也认可。随着苹果的财务状况日益严峻 以及和苹果新 ceo 约翰·斯卡利 的关系日益紧张,史蒂夫乔布斯被赶出了苹果公司。几个月后,微软发布了 windows 1.0 ,它也许不如 mac os 漂亮,但让微软在市场中站稳脚跟 奠定了统治地位。十年内,95%的个人计算机上都有微软的 windows。最初,mac os 的爱好者还可以说 mac 有卓越的图形界面和易用性,windows 早期版本都是基于 dos 而 dos 设计时 ,没想过运行图形界面,但 windows 3.1 之后,微软开始开发新的,面向消费者的 gui 操作系统,叫 windows 95,这是一个意义非凡的版本 ,不仅提供精美的界面,还有 mac os 没有的高级功能,比如\u0026quot;多任务\u0026quot;和\u0026quot;受保护内存\u0026quot;。windows 95 引入了许多 如今依然见得到的 gui 元素,比如开始菜单,任务栏和 windows 文件管理器。\n不过微软也失败过,为了让桌面更简单友好, 微软开发了一个产品叫 microsoft bob ,将比喻用到极致。现在屏幕上有了一个虚拟房间,程序是物品,可以放在桌子和书架上,甚至还有噼啪作响的壁炉 和提供帮助的虚拟狗狗,你看到那边的门没?,是的,那些门通往不同房间 房间里有不同程序,你可能猜到了,它没有获得成功。这是一个好例子,说明如今的用户界面是自然选择后的结果。无论你用的是 windows,mac,linux 或其他 gui,几乎都是施乐奥托 wimp 的变化版。一路上,人们试了各种做法并失败了。一切都必须发明,测试,改进,适应或抛弃,如今,图形界面无处不在 使用体验一般只是可以接受,而不是非常好,你肯定体验过差劲的设计,比如下载了很烂的 app,用过别人糟糕的手机,或者看到过很差的网站,因此计算机科学家和界面设计师 会继续努力工作,做出更好更强大的界面,朝着恩格尔巴特\u0026quot;增强人类智能\u0026quot;的愿景努力。\n我们下周见。\n27. 3d 图形 i.e. 3d graphics\n嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n在过去五集,我们从基于电传打字机的命令行界面讲到图形怎么显示到屏幕上,再到上集的 图形用户界面(gui),以及图形界面的美味。\n之前的例子都是 2d, 但我们生活的世界是 3d 的,我也是个三维 girl~\n所以今天,我们讲 3d 图形的基础知识,以及如何渲染 3d 图形到 2d 屏幕上。24 集中说过,可以写一个函数,从 a 到 b 画一条线,通过控制 a 和 b 的 (x,y) 坐标,可以控制一条线。在 3d 图像中,点的坐标不再是两点,而是三点,x,y,z ,或读\u0026quot;zee\u0026quot;,但我之后会读成\u0026quot;zed\u0026quot; 。当然,2d 的电脑屏幕上、不可能有 xyz 立体坐标轴,所以有图形算法 负责把 3d 坐标\u0026quot;拍平\u0026quot;显示到 2d 屏幕上,这叫 \u0026ldquo;3d 投影\u0026rdquo; 。所有的点都从 3d 转成 2d 后,就可以用画 2d 线段的函数 来连接这些点,这叫 \u0026ldquo;线框渲染\u0026rdquo; 。想象用筷子做一个立方体,然后用手电筒照它,墙上的影子就是投射,是平的。如果旋转立方体,投影看起来会像 3d 物体,尽管是投影面是平的,电脑也是这样 3d 转 2d ,只不过用大量数学,而不是筷子。\n3d 投影有好几种,你现在看到的,叫 正交投影 。立方体的各个边,在投影中互相平行,在真实 3d 世界中,平行线段会在远处收敛于一点,就像远处的马路汇聚到一点,这叫 透视投射 。过程是类似的,只是数学稍有不同。有时你想要透视投影,有时不想,具体取决于开发人员。\n如果想画立方体这种简单图形,直线就够了,但更复杂的图形,三角形更好,在 3d 图形学中 我们叫三角形\u0026quot;多边形\u0026quot;(polygons),看看这个多边形组成的 漂亮茶壶。一堆多边形的集合叫 网格 。网格越密,表面越光滑,细节越多,但意味着更多计算量。游戏设计者要平衡角色的真实度 和多边形数量,如果数量太多 帧率会下降到肉眼可感知,用户会觉得卡,因此有算法用来简化网格。\n之所以三角形更常用 而不是用正方形,或其它更复杂的图形,是因为三角形的简单性。空间中三点定义一个平面,如果给 3 个 3d 点,我能画出一个平面,而且只有这一个答案,4 个或多于 4 个点就不一定了,而 2 个点不够定义平面,只能定义线段,所以 3 是最完美的数字,三角形万岁。\n线框渲染 虽然很酷,但 3d 图像需要填充,填充图形的经典算法叫 扫描线渲染 (scanline rendering) ,于 1967 年诞生在犹他州大学。为了例子简单,我们只看一个多边形。我们要思考这个多边形如何转成一块填满像素的区域,我们先铺一层像素网格,扫描线算法 先读多边形的 3 个点,找最大和最小的 y 值,只在这两点间工作,然后算法从上往下,一次处理一行,计算每一行和多边形相交的 2 个点。因为是三角形,如果相交一条边,必然相交另一条,扫描线算法 会填满 2 个相交点之间的像素。\n来看个具体例子。第一行 相交于这里和这里,算法把两点间填满颜色,然后下一行,再下一行,所以叫 扫描线渲染,扫到底部就完成了。填充的速度叫 fillrate(填充速率)。当然 这样的三角形比较丑,边缘满是锯齿,当像素较小时 就不那么明显,但尽管如此,你肯定在游戏里见过这种效果,特别是低配电脑。一种减轻锯齿的方法叫 抗锯齿 (antialiasing),与其每个像素都涂成一样的颜色,可以判断多边形切过像素的程度,来调整颜色,如果像素在多边形内部,就直接涂颜色,如果多边形划过像素,颜色就浅一些,这种边缘羽化的效果看着更舒服些。抗锯齿 被广泛使用,比如字体和图标,如果你把脸贴近屏幕,近点, 再近点,你能看到浏览器里字体是抗锯齿的,超平滑。\n在 3d 场景中,多边形到处都是,但只有一部分能看见,因为其它的被挡住了,这叫 遮挡 。最直接的处理办法是用排序算法,从远到近排列,然后从远到近渲染,这叫 画家算法 。因为画家也是先画背景,然后再画更近的东西。\n看这个例子,有 3 个重叠的多边形。为了简单,我们画成不同颜色,同时,假设 3 个多边形都和屏幕平行,但在实际应用中,比如游戏里,多边形可能是倾斜的,3 个多边形 a,b,c,距离 20,12,14 。画家算法的第一件事,是从远到近排序,现在有序了,我们可以用 扫描线算法 填充多边形,一次填一个。我们从最远的 a 开始,然后重复这个过程,填充第二远的 c ,然后是 b 。现在完成了,可以看到顺序是对的,近的多边形在前面!\n还有一种方法叫 深度缓冲 ,它和之前的算法做的事情一样,但方法不同。我们回到之前的例子,回到排序前的状态。因为这个算法不用排序,所以速度更快。简而言之,z-buffering 算法会记录场景中每个像素和摄像机的距离,在内存里存一个数字矩阵。首先,每个像素的距离被初始化为\u0026quot;无限大\u0026quot;,然后 z-buffering 从列表里第一个多边形开始处理,也就是 a ,它和扫描线算法逻辑相同,但不是给像素填充颜色,而是把多边形的距离 和 z-buffer 里的距离进行对比,它总是记录更低的值 ,a 距离 20,20 小于\u0026quot;无限大\u0026quot;,所以缓冲区记录 20 ,算完 a 之后算下一个,以此类推 。因为没对多边形排序,所以后处理的多边形并不总会覆盖前面的,对于多边形 c ,缓冲区里只有一部分值会被多边形 c 的距离值覆盖。z 缓冲区完成后,会和\u0026quot;扫描线\u0026quot;算法的改进高级版配合使用,不仅可以勘测到线的交叉点,还可以知道某像素是否在最终场景中可见。如果不可见,扫描线算法会跳过那个部分,当两个多边形距离相同时,会出现一个有趣问题,比如多边形 a 和 b 距离都是 20, 哪个画上面?多边形会在内存中移来移去,访问顺序会不断变化。另外,计算浮点数有舍入误差,所以哪一个画在上面,往往是不可预测的,导致出现 z-fighting 效果 如果你玩过 3d 游戏,肯定见过。\n说起 故障,3d 游戏中有个优化叫 背面剔除 。你想想,三角形有两面,正面和背面,游戏角色的头部或地面,只能看到朝外的一面,所以为了节省处理时间,会忽略多边形背面,减了一半多边形面数。这很好,但有个 bug 是 如果进入模型内部往外看,头部和地面会消失。\n继续,我们讲灯光,也叫 明暗处理 ,因为 3d 场景中,物体表面应该有明暗变化。我们回到之前的茶壶网格,用\u0026quot;扫描线\u0026quot;算法渲染所有多边形后,茶壶看起来像这样,没什么 3d 感。我们来加点灯光,提高真实感。为了举例,我们从茶壶上挑 3 个不同位置的多边形,和之前的例子不同,这次要考虑这些多边形面对的方向,它们不平行于屏幕,而是面对不同方向,他们面对的方向叫 \u0026ldquo;表面法线\u0026rdquo; 。我们可以用一个垂直于表面的小箭头来显示这个方向,现在加个光源,每个多边形被照亮的程度不同,有的更亮,因为面对的角度导致更多光线反射到观察者。举个例子,底部的多边形向下倾斜,远离光源,所以更暗一些。类似的,最右的多边形更背对光源,所以只有部分照亮。最后是左上角的多边形,因为它面对的角度 意味着会把光线反射到我们这里,所以会显得更亮。如果对每个多边形执行同样的步骤,看上去会更真实!这叫 平面着色 ,是最基本的照明算法。不幸的是,这使多边形的边界非常明显,看起来不光滑,因此开发了更多算法,比如 高洛德着色 和 冯氏着色,不只用一种颜色给整个多边形上色,而是以巧妙的方式改变颜色得到更好的效果。\n我们还要说下 \u0026ldquo;纹理\u0026rdquo; ,纹理在图形学中指外观,而不是手感,就像照明算法一样,纹理也有多种算法,来做各种花哨效果。最简单的是 纹理映射 ,为了理解纹理映射,回到单个多边形,用\u0026quot;扫描线算法\u0026quot;填充时,可以看看内存内的纹理图像,决定像素用什么颜色。为了做到这点,需要把多边形坐标和纹理坐标对应起来,我们来看看\u0026quot;扫描线算法\u0026quot;要填充的第一个像素,纹理算法会查询纹理,从相应区域取平均颜色,并填充多边形,重复这个过程,就可以获得纹理。\n如果结合这集提到的所有技巧 会得到一个精美的小茶壶。这个茶壶可以放进更大的场景里,场景由上百万个多边形组成。渲染这样的场景需要大量计算,但重要的是,再大的场景,过程都是一样的,一遍又一遍,处理所有多边形,扫描线填充,抗锯齿,光照,纹理化,然而,有几种方法可以加速渲染:\n首先,我们可以为这种特定运算做专门的硬件来加快速度,让运算快如闪电 其次,我们可以把 3d 场景分解成多个小部分,然后并行渲染,而不是按顺序渲染。 cpu 不是为此设计的,因此图形运算不快,所以,计算机工程师为图形做了专门的处理器,叫 gpu \u0026ldquo;图形处理单元\u0026rdquo; 。gpu 在显卡上,周围有专用的 ram ,所有网格和纹理都在里面,让 gpu 的多个核心可以高速访问。现代显卡,如 geforce gtx 1080 ti 有 3584 个处理核心,提供大规模并行处理,每秒处理上亿个多边形!\n好了,本集对 3d 图形的介绍到此结束。下周我们聊全新的主题。\n我到时会 ping 你~\n28. 计算机网络 i.e. computer networks\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n互联网太棒啦,键盘敲几下就能在 youtube 直播\u0026ndash;哈喽!在维基百科上阅读文章,在亚马逊买东西,和朋友视频发一条天气推特。毫无疑问,用户在全球网络中发送和接收信息的能力,永远改变了这个世界。150 年前 发一封信件从伦敦到加州 要花 2~3 周,而且还是特快邮件,如今,电子邮件只要几分之一秒。\u0026ldquo;时延\u0026quot;改善了上百万倍 (时延指传播一条信息所需的时间),振兴了全球经济,帮助现代世界在遍布全球的光纤中快速发展。\n你可能觉得计算机和网络密切相关,但事实上,1970 年以前 大多数计算机是独立运行的,然而 ,因为大型计算机开始随处可见,廉价机器开始出现在书桌上,分享数据和资源渐渐变得有用起来,首个计算机网络出现了。\n今天起,我们花 3 集视频讲网络是如何发展成现在的样子,以及支撑它们的基础原理和技术。\n第一个计算机网络出现在 1950~1960 年代,通常在公司或研究室内部使用,为了方便信息交换,比把纸卡或磁带送到另一栋楼里更快速可靠,这叫 \u0026ldquo;球鞋网络\u0026rdquo; 。\n第二个好处是能共享物理资源。举个例子,与其每台电脑配一台打印机,大家可以共享一台联网的打印机。早期网络也会共享存储空间,因为每台电脑都配存储器太贵了。\n计算机近距离构成的小型网络叫局域网,简称 lan(local area networks)。局域网能小到是同一个房间里的两台机器,或大到校园里的上千台机器。尽管开发和部署了很多不同 lan 技术,其中最著名和成功的是 \u0026ldquo;以太网\u0026rdquo;, 开发于 1970 年代 ,在施乐的\u0026quot;帕洛阿尔托研究中心\u0026quot;诞生,今日仍被广泛使用。\n以太网的最简单形式是:一条以太网电线连接数台计算机,当一台计算机要传数据给另一台计算机时,它以电信号形式,将数据传入电缆,当然 因为电缆是共享的,连在同一个网络里的其他计算机也看得到数据,但不知道数据是给它们的,还是给其他计算机的。为了解决这个问题,以太网需要每台计算机有唯一的媒体访问控制地址,简称 mac 地址。这个唯一的地址放在头部,作为数据的前缀发送到网络中,所以,计算机只需要监听以太网电缆,只有看到自己的 mac 地址,才处理数据。这运作得很好,现在制造的每台计算机都自带唯一的 mac 地址,用于以太网和无线网络。\n多台电脑共享一个传输媒介,这种方法叫 \u0026ldquo;载波侦听多路访问\u0026rdquo; ,简称\u0026quot;csma\u0026rdquo; 。载体 (carrier) 指运输数据的共享媒介,以太网的\u0026quot;载体\u0026quot;是铜线,wifi 的\u0026quot;载体\u0026quot;是传播无线电波的空气。很多计算机同时侦听载体,所以叫\u0026quot;侦听\u0026quot;和\u0026quot;多路访问\u0026quot;,而载体传输数据的速度 叫 \u0026ldquo;带宽\u0026rdquo; 。不幸的是,使用共享载体有个很大的弊端 - 当网络流量较小时 计算机可以等待载体清空,然后传送数据,但随着网络流量上升,两台计算机想同时写入数据的概率也会上升,这叫冲突,数据全都乱套了、就像两个人同时在电话里讲话,幸运的是,计算机能够通过监听电线中的信号检测这些冲突,最明显的解决办法是停止传输,等待网络空闲,然后再试一遍。问题是 其他计算机也打算这样做,其他等着的计算机可能在任何停顿间隙闯入,导致越来越多冲突。很快,每个人都一个接一个地讲话,而且有一堆事要说,就像在家庭聚餐中和男朋友分手一样,馊主意!\n以太网有个超简单有效的解决方法,当计算机检测到冲突,就会在重传之前等待一小段时间,因为要举例,假设是 1 秒好了,当然 如果所有计算机用同样的等待时间 是不行的,它们会在一秒后再次冲突,所以加入一个随机时间 一台计算机可能等 1.3 秒,另一台计算机等待 1.5 秒,要是运气好 等 1.3 秒的计算机会醒来,发现载体是空闲的 然后开始传输,当 1.5 秒的计算机醒来后,会发现载体被占用,会等待其他计算机完成,这有用,但不能完全解决问题,所以要用另一个小技巧。 正如我刚才说的,如果一台计算机在传输数据期间检测到冲突,会等一秒+随机时间,然而 ,如果再次发生冲突 表明有网络拥塞,这次不等 1 秒,而是等 2 秒,如果再次发生冲突 等 4 秒 然后 8 秒 16 秒等等,直到成功传输。因为计算机的退避,冲突次数降低了,数据再次开始流动起来,网络变得顺畅,家庭晚餐有救啦! 这种指数级增长等待时间的方法叫: 指数退避。以太网和 wifi 都用这种方法,很多其他传输协议也用。但即便有了\u0026quot;指数退避\u0026quot;这种技巧,想用一根网线链接整个大学的计算机还是不可能的,为了减少冲突+提升效率,我们需要减少同一载体中设备的数量。载体和其中的设备总称 \u0026ldquo;冲突域\u0026rdquo; 。\n让我们回到之前以太网的例子,一根电缆连 6 台计算机,也叫一个冲突域。为了减少冲突,我们可以用交换机把它拆成两个冲突域,交换机位于两个更小的网络之间,必要时才在两个网络间传数据。交换机会记录一个列表,写着哪个 mac 地址在哪边网络。如果 a 想传数据给 c ,交换机不会把数据转发给另一边的网络,没必要。如果 e 想同一时间传数据给 f,网络仍然是空的,两个传输可以同时发生,但如果 f 想发数据给 a 数据会通过交换机,两个网络都会被短暂占用。\n大的计算机网络也是这样构建的,包括最大的网络 - 互联网,也是多个连在一起的稍小一点网络,使不同网络间可以传递信息。这些大型网络有趣之处是,从一个地点到另一个地点通常有多条路线,这就带出了另一个话题 路由 。\n连接两台相隔遥远的计算机或网路,最简单的办法 是分配一条专用的通信线路,早期电话系统就是这样运作的。假设\u0026quot;印第安纳波利斯\u0026quot;和\u0026quot;米苏拉\u0026quot;之间,有五条电话线,如果在 1910 年代,john 想打电话给 hank,john 要告诉操作员他想打到什么地方,然后工作人员手动将 john 的电话连到 通往米苏拉的未使用线路。通话期间,这条线就被占用了,如果五条线都被占用了 john 要等待某条线空出来,这叫 \u0026ldquo;电路交换\u0026rdquo; ,因为是把电路连接到正确目的地。能用倒是能用 ,但不灵活而且价格昂贵 ,因为总有闲置的线路。好处是 如果有一条专属于自己的线路 你可以最大限度地随意使用,无需共享。因此军队,银行和其他一些机构,依然会购买专用线路来连接数据中心。\n传输数据的另一个方法是 \u0026ldquo;报文交换\u0026rdquo; ,\u0026ldquo;报文交换\u0026rdquo; 就像邮政系统一样,不像之前 a 和 b 有一条专有线路,消息会经过好几个站点。 如果 john 写一封信给 hank,信件可能从\u0026quot;印第安纳波利斯\u0026quot;到\u0026quot;芝加哥\u0026quot;,然后\u0026quot;明尼阿波利斯\u0026quot; ,然后\u0026quot;比林斯\u0026quot; 最后到\u0026quot;米苏拉\u0026quot;。每个站点都知道下一站发哪里 ,因为站点有表格,记录到各个目的地,信件该怎么传。报文交换的好处是 可以用不同路由 ,使通信更可靠更能容错。\n回到邮件的例子,如果\u0026quot;明尼阿波利斯\u0026quot;有暴风雪中断了通信 \u0026ldquo;芝加哥\u0026quot;可以传给\u0026quot;奥马哈\u0026rdquo;,在这个例子里,城市就像路由器一样,消息沿着路由跳转的次数 叫 \u0026ldquo;跳数\u0026rdquo; (hop count)。记录跳数很有用,因为可以分辨出路由问题。举例,假设芝加哥认为 去米苏拉的最快路线是 奥马哈,但奥马哈认为 去米苏拉的最快路线是 芝加哥,这就糟糕了,因为 2 个城市看到目的地是米苏拉,结果报文会在 2 个城市之间 不停传来传去,不仅浪费带宽 ,而且这个路由错误需要修复! 这种错误会被检测到,因为跳数记录在消息中 ,而且传输时会更新跳数。如果看到某条消息的跳数很高 ,就知道路由肯定哪里错了,这叫 \u0026ldquo;跳数限制\u0026rdquo; 。\n报文交换的缺点之一是有时候报文比较大,会堵塞网络 ,因为要把整个报文从一站传到下一站后 才能继续传递其他报文。传输一个大文件时,整条路都阻塞了,即便你只有一个 1kb 的电子邮件要传输 ,也只能等大文件传完,或是选另一条效率稍低的路线,这就糟了。\n解决方法是 将大报文分成很多小块,叫 \u0026ldquo;数据包\u0026rdquo; ,就像报文交换 ,每个数据包都有目标地址 ,因此路由器知道发到哪里。报文具体格式由\u0026quot;互联网协议\u0026quot;定义,简称 ip 。这个标准创建于 1970 年代,每台联网的计算机都需要一个 ip 地址。你可能见过,以点分隔的 4 组数字,例如 172.217.7.238 是 google 其中一个服务器的 ip 地址。数百万台计算机在网络上不断交换数据 ,瓶颈的出现和消失是毫秒级的,路由器会平衡与其他路由器之间的负载, 以确保传输可以快速可靠,这叫 \u0026ldquo;阻塞控制\u0026rdquo; 。\n有时,同一个报文的多个数据包 会经过不同线路,到达顺序可能会不一样,这对一些软件是个问题。幸运的是,在 ip 之上还有其他协议,比如 tcp/ip, 可以解决乱序问题。我们下周会讲。\n将数据拆分成多个小数据包,然后通过灵活的路由传递,非常高效且可容错,如今互联网就是这么运行的,这叫 \u0026ldquo;分组交换\u0026rdquo; 。有个好处是 它是去中心化的,没有中心权威机构,没有单点失败问题。事实上 ,因为冷战期间有核攻击的威胁,所以创造了分组交换。如今,全球的路由器协同工作,找出最高效的线路,用各种标准协议运输数据,比如 \u0026ldquo;因特网控制消息协议\u0026rdquo;(icmp) 和 \u0026ldquo;边界网关协议\u0026rdquo;(bgp)。世界上第一个分组交换网络以及现代互联网的祖先是 arpanet(advanced research projects agency),名字来源于赞助这个项目的机构,美国高级研究计划局。\n这是 1974 年整个 arpanet 的样子,每个小圆表示一个地点, 比如大学或实验室,那里运行着一个路由器,并且有一台或多台计算机,能看到 \u0026ldquo;pdp-1\u0026rdquo; 和\u0026quot;ibm 360 系统\u0026quot;,甚至还有一个伦敦的 atlas 是通过卫星连到网络里的。显然 ,互联网在这几十年间发展迅速,如今不再只有几十台计算机联网 据估计 有接近 100 亿台联网设备,而且互联网会继续快速发展,特别是如今各种智能设备层出不穷 ,比如联网冰箱,恒温器,以及其他智能家电,它们组成了\u0026quot;物联网\u0026quot;。\n第一部分到此结束 我们对计算机网络进行了概览。\n网络是一堆管子组成的吗?额 算是吧。下周我们会讨论一些高级传输协议,然后讲万维网。\n到时见啦。\n29. 互联网 i.e. the internet\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n上集讲到,你的计算机和一个巨大的分布式网络连在一起,这个网络叫互联网。\n你现在就在网上看视频呀。互联网由无数互联设备组成,而且日益增多。计算机为了获取这个视频 ,首先要连到局域网,也叫 lan ,你家 wifi 路由器连着的所有设备,组成了局域网。局域网再连到广域网,广域网也叫 wan (wide area network),wan 的路由器一般属于你的\u0026quot;互联网服务提供商\u0026quot;,简称 isp(internet service provider),比如 comcast,at\u0026amp;t 和 verizon 这样的公司。\n广域网里,先连到一个区域性路由器,这路由器可能覆盖一个街区。然后连到一个更大的 wan,可能覆盖整个城市。可能再跳几次,但最终会到达互联网主干。互联网主干由一群超大型、带宽超高路由器组成,为了从 youtube 获得这个视频,数据包(packet)要先到互联网主干,沿着主干到达有对应视频文件的 youtube 服务器,数据包从你的计算机跳到 youtube 服务器,可能要跳个 10 次,先跳 4 次到互联网主干,2 次穿过主干,主干出来可能再跳 4 次,然后到 youtube 服务器。如果你在用 windows, mac os 或 linux 系统,可以用 traceroute 来看跳了几次,更多详情看视频描述(youtube 原视频下)。\n我们在\u0026quot;印第安纳波利斯\u0026quot;的 chad\u0026amp;stacy emigholz 工作室,访问加州的 dftba 服务器,经历了 11 次中转。从 192.168.0.1 出发,这是我的电脑在 局域网(lan)里的 ip 地址,然后到工作室的 wifi 路由器,然后穿过一个个地区路由器,到达主干。然后从主干出来,又跳了几次,到达\u0026quot;dftba.com”的服务器,ip 地址是 104.24.109.186 。\n但数据包到底是怎么过去的 ?如果传输时数据包被弄丢了,会发生什么?如果在浏览器里输 \u0026ldquo;dftba.com\u0026rdquo;,浏览器怎么知道服务器的地址多少?\n我们今天会讨论这些话题。\n上集说过,互联网是一个巨型分布式网络 ,会把数据拆成一个个数据包来传输。如果要发的数据很大,比如邮件附件, 数据会被拆成多个小数据包。举例,你现在看的这个视频 ,就是一个个到达你电脑的数据包,而不是一整个大文件发过来。数据包(packet)想在互联网上传输 ,要符合\u0026quot;互联网协议\u0026quot;的标准,简称 ip 。就像邮寄手写信一样,邮寄是有标准的每封信需要一个地址,而且地址必须是独特的,并且大小和重量是有限制的,违反这些规定,信件就无法送达。\nip 数据包也是如此,因为 ip 是一个非常底层的协议,数据包的头部(或者说前面)只有目标地址,头部存 \u0026ldquo;关于数据的数据\u0026rdquo; 也叫 元数据 (metadata),这意味着当数据包到达对方电脑 ,对方不知道把包交给哪个程序,是交给 skype 还是使命召唤?因此需要在 ip 之上,开发更高级的协议。\n这些协议里 最简单最常见的叫\u0026quot;用户数据报协议\u0026quot;,简称 udp 。udp 也有头部,这个头部位于数据前面,头部里包含有用的信息。信息之一是端口号,每个想访问网络的程序 ,都要向操作系统申请一个端口号,比如 skype 会申请端口 3478 。当一个数据包到达时 ,接收方的操作系统会读 udp 头部,读里面的端口号,如果看到端口号是 3478,就把数据包交给 skype。\n总结:ip 负责把数据包送到正确的计算机, udp 负责把数据包送到正确的程序。\nudp 头部里还有\u0026quot;校验和\u0026quot;,用于检查数据是否正确,正如\u0026quot;校验和\u0026quot;这个名字所暗示的, 检查方式是把数据求和来对比。\n以下是个简单例子。假设 udp 数据包里 原始数据是 89 111 33 32 58 41 ,在发送数据包前 ,电脑会把所有数据加在一起,算出\u0026quot;校验和\u0026quot; - 89+111+33+\u0026hellip; 以此类推,得到 364,这就是\u0026quot;校验和\u0026quot;。 udp 中,\u0026ldquo;校验和\u0026rdquo; 以 16 位形式存储 (就是 16 个 0 或 1),如果算出来的和,超过了 16 位能表示的最大值, 高位数会被扔掉,保留低位。当接收方电脑收到这个数据包,它会重复这个步骤 把所有数据加在一起,89+111+33\u0026hellip; 以此类推,如果结果和头部中的校验和一致 ,代表一切正常。如果不一致,数据肯定坏掉了。也许传输时碰到了功率波动,或电缆出故障了。\n不幸的是,udp 不提供数据修复或数据重发的机制,接收方知道数据损坏后,一般只是扔掉。而且,udp 无法得知数据包是否到达。发送方发了之后,无法知道数据包是否到达目的地,这些特性听起来很糟糕,但是有些程序不在意这些问题,因为 udp 又简单又快。\n拿 skype 举例 ,它用 udp 来做视频通话,能处理坏数据或缺失数据,所以网速慢的时候 skype 卡卡的 因为只有一部分数据包到了你的电脑。但对于其他一些数据,这个方法不适用。 比如发邮件,邮件不能只有开头和结尾 ,没有中间,邮件要完整到达收件方!\n如果\u0026quot;所有数据必须到达\u0026quot; ,就用\u0026quot;传输控制协议\u0026quot;,简称 tcp(transmission control protocol)。tcp 和 udp 一样,头部也在存数据前面,因此,人们叫这个组合 tcp/ip 。就像 udp ,tcp 头部也有\u0026quot;端口号\u0026quot;和\u0026quot;校验和\u0026quot;,但 tcp 有更高级的功能,我们这里只介绍重要的几个。\n1、 tcp 数据包有序号\n15 号之后是 16 号,16 号之后是 17 号,以此类推 发上百万个数据包也是有可能的。序号使接收方可以把数据包排成正确顺序,即使到达时间不同。哪怕到达顺序是乱的,tcp 协议也能把顺序排对。\n2、 tcp 要求接收方的电脑收到数据包 并且\u0026quot;校验和\u0026quot;检查无误后(数据没有损坏)给发送方发一个确认码,代表收到了\n\u0026ldquo;确认码\u0026rdquo; 简称 ack . 得知上一个数据包成功抵达后,发送方会发下一个数据包。假设这次发出去之后,没收到确认码 ,那么肯定哪里错了。如果过了一定时间还没收到确认码, 发送方会再发一次。注意 ,数据包可能的确到了,只是确认码延误了很久,或传输中丢失了,但这不碍事 ,因为收件方有序列号,如果收到重复的数据包就删掉。\n还有,tcp 不是只能一个包一个包发,可以同时发多个数据包,收多个确认码 ,这大大增加了效率,不用浪费时间等确认码。有趣的是,确认码的成功率和来回时间 可以推测网络的拥堵程度,tcp 用这个信息,调整同时发包数量,解决拥堵问题。\n简单说,tcp 可以处理乱序和丢失数据包,丢了就重发,还可以根据拥挤情况自动调整传输率。相当厉害!\n你可能会奇怪,既然 tcp 那么厉害,还有人用 udp 吗?tcp 最大的缺点是 ,那些\u0026quot;确认码\u0026quot;数据包把数量翻了一倍,但并没有传输更多信息,有时候这种代价是不值得的 ,特别是对时间要求很高的程序,比如在线射击游戏。如果你玩游戏很卡,你也会觉得这样不值!\n当计算机访问一个网站时 需要两个东西:1.ip 地址, 2. 端口号 。\n例如 172.217.7.238 的 80 端口 ,这是谷歌的 ip 地址和端口号。事实上,你可以输到浏览器里,然后你会进入谷歌首页。有了这两个东西就能访问正确的网站, 但记一长串数字很讨厌,google.com 比一长串数字好记,所以互联网有个特殊服务 ,负责把域名和 ip 地址一一对应,就像专为互联网的电话簿 它叫 \u0026ldquo;域名系统\u0026rdquo; ,简称 dns 。\n它的运作原理你可能猜到了,在浏览器里输 youtube.com ,浏览器会去问 dns 服务器,它的 ip 地址是多少。一般 dns 服务器 是互联网供应商提供的,dns 会查表,如果域名存在,就返回对应 ip 地址。如果你乱敲键盘加个。com, 然后按回车,你很可能会看到 dns 错误,因为那个网站不存在,所以 dns 无法返回给你一个地址。如果你输的是有效地址,比如 youtube.com ,dns 按理会返回一个地址,然后浏览器会给这个 ip 地址 发 tcp 请求。如今有三千万个注册域名,所以为了更好管理,dns 不是存成一个超长超长的列表,而是存成树状结构。顶级域名(简称 tld)在最顶部,比如 .com 和 .gov ,下一层是二级域名,比如 .com 下面有 google.com 和 dftba.com ,再下一层叫子域名,比如 images.google.com, store.dftba.com ,这个树超!级!大!\n我前面说的\u0026quot;三千万个域名\u0026quot;只是二级域名 ,不是所有子域名,因此,这些数据散布在很多 dns 服务器上,不同服务器负责树的不同部分。\n好了 我知道你肯定在等这个梗:我们到了一层新抽象!\n过去两集里 我们讲了线路里的电信号,以及无线网络里的无线信号,这些叫\u0026quot;物理层\u0026quot;,而\u0026quot;数据链路层\u0026quot; 负责操控 \u0026ldquo;物理层\u0026rdquo;,数据链路层有:媒体访问控制地址(mac),碰撞检测,指数退避,以及其他一些底层协议。再上一层是\u0026quot;网络层\u0026quot;,负责各种报文交换和路由。而今天,我们讲了\u0026quot;传输层\u0026quot;里一大部分, 比如 udp 和 tcp 这些协议,负责在计算机之间进行点到点的传输,而且还会检测和修复错误。我们还讲了一点点\u0026quot;会话层\u0026quot;,\u0026ldquo;会话层\u0026rdquo; 会使用 tcp 和 udp 来创建连接,传递信息,然后关掉连接,这一整套叫\u0026quot;会话\u0026quot;。查询 dns 或看网页时,就会发生这一套流程。这是 开放式系统互联通信参考模型 (osi,open system interconnection) 的底下 5 层,这个概念性框架 把网络通信划分成多层,每一层处理各自的问题。如果不分层 直接从上到下捏在一起实现网络通信,是完全不可能的!\n抽象使得科学家和工程师能分工同时改进多个层 不被整体复杂度难倒。而且惊人的是!我们还没讲完呢!\nosi 模型还有两层,\u0026ldquo;表示层\u0026quot;和\u0026quot;应用程序层\u0026rdquo;,其中有浏览器,skype,html 解码,在线看电影等。\n我们下周说,到时见。\n30. 万维网 i.e. the world wide web\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n前两集我们深入讨论了电线 信号 交换机 数据包 路由器以及协议,它们共同组成了互联网。今天我们向上再抽象一层,来讨论万维网。\n万维网 (world wide web) 和互联网 (internet) 不是一回事,尽管人们经常混用这两个词。万维网在互联网之上运行,互联网之上还有 skype, minecraft 和 instagram 。互联网是传递数据的管道,各种程序都会用,其中传输最多数据的程序是万维网,分布在全球数百万个服务器上,可以用\u0026quot;浏览器\u0026quot;来访问万维网,这集我们会深入讲解万维网。\n万维网的最基本单位,是单个页面。页面有内容,也有去往其他页面的链接 ,这些链接叫\u0026quot;超链接\u0026quot;。你们都见过:可以点击的文字或图片,把你送往另一个页面,这些超链接形成巨大的互联网络,这就是\u0026quot;万维网\u0026quot;名字的由来。现在说起来觉得很简单,但在超链接做出来之前,计算机上每次想看另一个信息时,你需要在文件系统中找到它 ,或是把地址输入搜索框,有了超链接,你可以在相关主题间轻松切换。超链接的价值早在 1945 年 就被 vannevar bush 意识到了,在第 24 集中我们说过,他发过一篇文章 ,描述一个假想的机器 memex ,bush 的形容是\u0026quot;关联式索引 - 选一个物品会引起另一个物品被立即选中\u0026quot;。他解释道:\u0026ldquo;将两样东西联系在一起的过程十分重要,在任何时候,当其中一件东西进入视线,只需点一下按钮,立马就能回忆起另一件\u0026rdquo; 。1945 年的时候计算机连显示屏都没有,所以这个想法非常超前!因为文字超链接是如此强大,它得到了一个同样厉害的名字:\u0026ldquo;超文本\u0026rdquo;!如今超文本最常指向的,是另一个网页,然后网页由浏览器渲染,我们待会会讲。\n为了使网页能相互连接,每个网页需要一个唯一的地址,这个地址叫 \u0026ldquo;统一资源定位器\u0026rdquo;,简称 url(uniform resource locator)。一个网页 url 的例子是 \u0026ldquo;thecrashcourse.com/courses\u0026rdquo; ,就像上集讨论的,当你访问一个网站时,计算机首先会做\u0026quot;dns 查找\u0026quot;,\u0026ldquo;dns 查找\u0026quot;的输入是一个域名 比如 thecrashcourse.com ,会输出对应的 ip 地址,现在有了 ip 地址 ,你的浏览器会打开一个 tcp 连接到这个 ip 地址,这个地址运行着\u0026quot;网页服务器\u0026rdquo;,网页服务器的标准端口是 80 端口,这时,你的计算机连到了 thecrashcourse.com 的服务器。下一步是向服务器请求\u0026quot;courses\u0026quot;这个页面,这里会用\u0026quot;超文本传输协议\u0026quot;(http)。\nhttp 的第一个标准,http 0.9,创建于 1991 年,只有一个指令,\u0026ldquo;get\u0026rdquo; 指令。幸运的是,对当时来说也够用,因为我们想要的是\u0026quot;courses\u0026quot;页面,我们向服务器发送指令:\u0026ldquo;get /courses\u0026rdquo;,该指令以\u0026quot;ascii 编码\u0026quot;发送到服务器,服务器会返回该地址对应的网页 ,然后浏览器会渲染到屏幕上。如果用户点了另一个链接,计算机会重新发一个 get 请求。你浏览网站时,这个步骤会不断重复。\n在之后的版本,http 添加了状态码,状态码放在请求前面。举例,状态码 200 代表 \u0026ldquo;网页找到了,给你\u0026rdquo;。状态码 400~499 代表客户端出错,比如网页不存在,就是可怕的 404 错误。\n\u0026ldquo;超文本\u0026quot;的存储和发送都是以普通文本形式。举个例子,编码可能是 ascii 或 utf-16 , 我们在第 4 集和第 20 集讨论过,因为如果只有纯文本 ,无法表明什么是链接,什么不是链接,所以有必要开发一种标记方法,因此开发了 超文本标记语言(html)。\nhtml 第一版的版本号是 0.8,创建于 1990 年,有 18 种 html 指令,仅此而已!\n我们来做一个网页吧!\n\u0026gt; 首先,给网页一个大标题\r\u0026gt; 我们输 h1 代表一级标题,然后用\u0026lt;\u0026gt;括起来\r\u0026gt; 这就是一个 html 标签\r\u0026gt; \u0026gt; 然后输入想要的标题\r\u0026gt; 我们不想一整页都是标题 所以加 \u0026lt;/h1\u0026gt; 作为结束标签\r\u0026gt; \u0026gt; 现在来加点内容\r\u0026gt; 读者可能不知道\u0026#34;克林贡\u0026#34;是什么,所以我们给这个词\r\u0026gt; 加一个超链接到\u0026#34;克林贡语言研究院\u0026#34;\r\u0026gt; 我们用 \u0026lt;a\u0026gt; 标签来做,它有一个 href 属性\r\u0026gt; 说明链接指向哪里,当点击链接时就会进入那个网页\r\u0026gt; 最后用 \u0026lt;/a\u0026gt; 关闭标签\r\u0026gt; \u0026gt; 接下来用 \u0026lt;h2\u0026gt; 标签做二级标题\r\u0026gt; \u0026gt; html 也有做列表的标签\r\u0026gt; 我们先写\u0026lt;ol\u0026gt; 代表 有序列表(ordered list)\r\u0026gt; 然后想加几个列表项目 , 就加几个 用 \u0026lt;li\u0026gt; 包起来就行\r\u0026gt; 读者可能不知道 bat\u0026#39;leth 是什么,那么也加上超链接\r\u0026gt; 最后,为了保持良好格式,用\u0026lt;/ol\u0026gt;代表列表结束\r\u0026gt; \u0026gt; 这就完成了 - 一个很简单的网页! 如果把这些文字存入记事本或文本编辑器,然后文件取名 \u0026ldquo;test.html\u0026rdquo;,就可以拖入浏览器打开。\n当然,如今的网页更复杂一些,最新版的 html,html5,有 100 多种标签,图片标签,表格标签,表单标签,按钮标签,等等。还有其他相关技术就不说了比如 层叠样式表 (css) 和 javascript,这俩可以加进网页,做一些更厉害的事。\n让我们回到浏览器,网页浏览器可以和网页服务器沟通,浏览器不仅获取网页和媒体,获取后还负责显示。\n第一个浏览器和服务器,是 tim berners-lee 在 1990 年写的,一共花了 2 个月。那时候他在瑞士的\u0026quot;欧洲核子研究所\u0026quot;工作,为了做出来,他同时建立了几个最基本的网络标准 - url, html 和 http。两个月能做这些很不错啊!不过公平点说,他研究超文本系统已经有十几年了,和同事在 cern 内部使用一阵子后,在 1991 年发布了出去,万维网就此诞生。\n重要的是,万维网有开放标准,大家都可以开发新服务器和新浏览器,因此\u0026quot;伊利诺伊大学香槟分校\u0026quot;的一个小组在 1993 年做了 mosaic 浏览器,第一个可以在文字旁边显示图片的浏览器,之前浏览器要单开一个新窗口显示图片,还引进了书签等新功能,界面友好,使它很受欢迎。尽管看上去硬邦邦的,但和如今的浏览器长的差不多。\n1990 年代末有许多浏览器面世,netscape navigator, internet explorer opera, omniweb, mozilla ,也有很多服务器面世,比如 apache 和 微软互联网信息服务 (iis)。每天都有新网站冒出来,如今的网络巨头,比如亚马逊和 ebay,创始于 1990 年代中期,那是个黄金时代!\n随着万维网日益繁荣,人们越来越需要搜索。如果你知道网站地址, 比如 ebay.com,直接输入浏览器就行,如果不知道地址呢?比如想找可爱猫咪的图片,现在就要!去哪里找呢?\n起初人们会维护一个目录,链接到其他网站,其中最有名的叫\u0026quot;jerry 和 david 的万维网指南\u0026rdquo;,1994 年改名为 yahoo 。随着网络越来越大,人工编辑的目录变得不便利,所以开发了搜索引擎。\n让我们进入思想泡泡!\n长的最像现代搜索引擎的最早搜素引擎,叫 jumpstation ,由 jonathon fletcher 于 1993 年在斯特林大学创建,它有 3 个部分:\n第一个是爬虫,一个跟着链接到处跑的软件,每当看到新链接,就加进自己的列表里;第二个部分是不断扩张的索引,记录访问过的网页上,出现过哪些词;最后一个部分,是查询索引的搜索算法,举个例子,如果我在 jumpstation 输入\u0026quot;猫\u0026quot;,每个有\u0026quot;猫\u0026quot;这个词的网页都会出现。\n早期搜索引擎的排名方式 非常简单,取决于 搜索词在页面上的出现次数。刚开始还行,直到有人开始钻空子,比如在网页上写几百个\u0026quot;猫\u0026quot;,把人们吸引过来。谷歌成名的一个很大原因是, 创造了一个聪明的算法来规避这个问题。与其信任网页上的内容 ,搜索引擎会看其他网站 有没有链接到这个网站。如果只是写满\u0026quot;猫\u0026quot;的垃圾网站,没有网站会指向它,如果有关于猫的有用内容,有网站会指向它,所以这些\u0026quot;反向链接\u0026quot;的数量,特别是有信誉的网站,代表了网站质量。\ngoogle 一开始时是 1996 年斯坦福大学 一个叫 backrub 的研究项目,两年后分离出来,演变成如今的谷歌。\n谢谢思想泡泡!\n最后 我想讲一个词,你最近可能经常听到 - 网络中立性 。现在你对数据包,路由和万维网,有了个大体概念,足够你理解这个争论的核心点,至少从技术角度。简单说\u0026quot;网络中立性\u0026quot;是应该平等对待所有数据包,不论这个数据包是我的邮件,或者是你在看视频,速度和优先级应该是一样的,但很多公司会乐意让它们的数据优先到达。拿 comcast 举例,它们不但是大型互联网服务提供商而且拥有多家电视频道,比如 nbc 和 the weather channel,可以在线看。我不是特意找 comcast 麻烦 ,但要是没有网络中立性,comcast 可以让自己的内容优先到达 ,节流其他线上视频。节流 (throttled) 意思是故意给更少带宽和更低优先级。再次重申,这只是举例,不是说 comcast 很坏。支持网络中立性的人说 没有中立性后,服务商可以推出提速的\u0026quot;高级套餐\u0026quot;,给剥削性商业模式埋下种子。互联网服务供应商成为信息的\u0026quot;守门人\u0026quot;,它们有着强烈的动机去碾压对手,另外,netflix 和 google 这样的大公司可以花钱买特权,而小公司,比如刚成立的创业公司,会处于劣势,阻止了创新。另一方面,从技术原因看,也许你会希望不同数据传输速度不同,你希望 skype 的优先级更高,邮件晚几秒没关系。而反对\u0026quot;网络中立性\u0026quot;的人认为,市场竞争会阻碍不良行为,如果供应商把客户喜欢的网站降速 ,客户会离开供应商。\n这场争辩还会持续很久,就像我们在 crash course 其他系列中说过,你应该自己主动了解更多信息,因为\u0026quot;网络中立性\u0026quot;的影响十分复杂而且广泛。\n我们下周再见\n31. 计算机安全 i.e. cybersecurity\n(。・∀・)ノ゙嗨,我是 carrie anne ,欢迎收看计算机科学速成课!\n过去 3 集 我们讲了计算机如何互连,让我们能瞬时跨全球沟通,但不是每个使用网络的人都会规规矩矩,不损害他人利益。就像现实世界中我们用锁和栅栏保证物理安全,有警察减少犯罪,我们需要网络安全减少虚拟世界中的犯罪 🚨。\n计算机没有道德观念。只要给计算机写清具体问题 , 它们很乐意地闪电般算出答案。破坏医院计算机系统的代码 和 保持病人心跳的代码 ,对计算机来说没有区别,就像\u0026quot;原力\u0026quot;一样 ,计算机可以被拉到\u0026quot;光明面\u0026quot;或\u0026quot;黑暗面\u0026quot;。网络安全就像 绝地武士团 ,给网络世界带来和平与正义。\n计算机安全的范围,和计算能力的发展速度一样快。我们可以把计算机安全,看成是保护系统和数据的:保密性,完整性和可用性 。我们逐个细说:\n\u0026ldquo;保密性\u0026quot;是只有有权限的人 ,才能读取计算机系统和数据。黑客泄露别人的信用卡信息,就是攻击保密性。\n\u0026ldquo;完整性\u0026quot;是只有有权限的人 ,才能使用和修改系统和数据。黑客知道你的邮箱密码,假冒你发邮件,就是攻击\u0026quot;完整性\u0026rdquo;。\n\u0026ldquo;可用性\u0026quot;是有权限的人 ,应该随时可以访问系统和数据。拒绝服务攻击 (ddos) 就是黑客发大量的假请求到服务器,让网站很慢或者挂掉,这就是攻击\u0026quot;可用性\u0026rdquo;。\n为了实现这三个目标,安全专家会从 抽象层面想象\u0026quot;敌人\u0026quot;可能是谁,这叫\u0026quot;威胁模型分析\u0026rdquo;,模型会对攻击者有个大致描述:能力如何,目标可能是什么,可能用什么手段 。攻击手段又叫\u0026quot;攻击矢量\u0026quot; ,\u0026ldquo;威胁模型分析\u0026quot;让你能为特定情境做准备,不被可能的攻击手段数量所淹没 ,因为手段实在有太多种了。假设你想确保笔记本计算机的\u0026quot;物理安全\u0026rdquo; ,你的威胁模型是\u0026quot;好管闲事的室友\u0026quot;。为了保证保密性,完整性和可用性, 你可以藏在脏兮兮的洗衣篮里。但如果威胁模型是调皮的兄弟姐妹,知道你喜欢藏哪里,那么你需要更多保护:比如锁在保险箱里。换句话说,要怎么保护,具体看对抗谁。当然,威胁模型通常比\u0026quot;好管闲事的室友\u0026quot;更正式一些,通常威胁模型分析里 会以能力水平区分,比如\u0026quot;某人可以物理接触到笔记本计算机,而且时间无限\u0026quot;。在给定的威胁模型下,安全架构师要提供解决方案,保持系统安全。只要某些假设不被推翻,比如没人会告诉攻击者密码,保护计算机系统,网络和数据的方法有很多。\n很多安全问题可以总结成 2 个问题: 你是谁?你能访问什么?\n权限应该给合适的人,拒绝错误的人,比如银行员工可以打开取款机来补充现金。但我不应该有权限打开,因为我会把钱拿走 全拿走!陶瓷猫收藏品可不会从天上掉下来哟!所以,为了区分谁是谁,我们用 \u0026ldquo;身份认证\u0026rdquo;(authentication) - 让计算机得知使用者是谁。\n身份认证有三种,各有利弊:你知道什么、你有什么、你是什么。\n\u0026ldquo;你知道什么\u0026rdquo; 是基于某个秘密,只有用户和计算机知道,比如 用户名和密码,这是如今使用最广泛的,因为最容易实现,但如果黑客通过猜测或其他方式,知道你的密码,就惨了。有些密码很容易猜中,比如 12356 或 qwerty 。但有些密码对计算机很容易,比如 pin 码:2580 ,看起来很难猜中 - 起码对人类来说是这样,但 4 位数字,只有一万种可能。一台计算机可以尝试 0000,然后 0001,然后 0002,然后到 9999,不到一秒内试完,这叫\u0026quot;暴力攻击\u0026quot;,因为只是试遍一切可能,这种算法没什么聪明的地方。如果你错误尝试 3 次,有些系统会阻止你继续尝试,或让你等一会儿,这个策略普遍而且合理。对于一般的攻击者确实很难,但假设黑客控制了数以万计的计算机,形成一个僵尸网络,用这么多计算机尝试密码 2580 ,同时尝试很多银行账户,即使每个账户只试一次,也很可能,碰到某个账户刚好用这个 pin,事实上,看视频的某人可能刚好用这个 pin 。增加密码长度有帮助,但即使 8 位数字的 pin 码也很容易破解,这就是为什么现在很多网站 要求大写+小写字母,还有特殊符号等,大大增加可能的密码。8 位数字的 pin 只有一亿种组合,对计算机轻而易举,但包含各种字符的 8 位长度密码,有超过 600 万亿种组合。当然,这些密码会难以记住,所以更好的方法是 选一些更好记的东西,比如三个单词连在一起:\u0026ldquo;格林兄弟好厉害\u0026quot;或\u0026quot;披萨尝起来好好吃\u0026rdquo;。英文大约有 10 万个单词,所以三个单词连一起大概有 1 亿亿种可能,想猜中的话,祝你好运!另外使用不在字典内的单词被猜中的可能性更低,但我们没时间细说这个。computerphile 频道有个视频讲怎么选择好密码 - 链接请看 youtube 描述。\n\u0026ldquo;你有什么\u0026quot;这种验证方式,是基于用户有特定物体,比如钥匙和锁。如果你有钥匙,就能开门,这避免了被人\u0026quot;猜中\u0026quot;的问题,而且通常需要人在现场,所以远程攻击就更难了。另一个国家的人,得先来佛罗里达州,才能到你家前门。但如果攻击者离你比较近,那么也不安全,钥匙可以被复制,手机可能被偷,锁可以撬开。\n最后,\u0026ldquo;你是什么\u0026quot;这种验证,是基于你把特征展示给计算机进行验证,生物识别验证器,比如指纹识别器和虹膜扫描仪就是典型例子,这些非常安全,但最好的识别技术仍然很贵,而且,来自传感器的数据每次会不同。\n\u0026ldquo;你知道什么\u0026quot;和\u0026quot;你有什么\u0026rdquo;。这两种验证是\u0026quot;确定性\u0026quot;的 - 要么正确,要么错误。如果你知道密码,或有钥匙,那么 100%能获得访问权限,如果没有,就绝对进不去,但\u0026quot;生物识别\u0026quot;是概率性的,系统有可能认不出你,可能你戴了帽子,或者光线不好。更糟的是,系统可能把别人错认成你,比如你的邪恶双胞胎。当然,在现实世界中几率很低,但不是零。\n生物认证的另一个问题是无法重设。你只有这么多手指,如果攻击者拿到你的指纹数据怎么办,你一辈子都麻烦了。最近还有研究人员表示,拍个照都有可能伪造虹膜,所以也不靠谱。\n所有认证方法都有优缺点,它们都可以被攻破,所以,对于重要账户,安全专家建议用两种或两种以上的认证方式,这叫\u0026quot;双因素\u0026quot;或\u0026quot;多因素\u0026quot;认证。攻击者可能猜出你密码,或偷走你的手机:但两个都做到,会比较难。\n\u0026ldquo;身份验证\u0026quot;后,就来到了\u0026quot;访问控制\u0026rdquo;。一旦系统知道你是谁,它需要知道你能访问什么,因此应该有个规范,说明谁能访问什么,修改什么,使用什么。这可以通过\u0026quot;权限\u0026quot;或\u0026quot;访问控制列表\u0026rdquo;(acl)来实现,其中描述了用户对每个文件,文件夹和程序的访问权限。\n\u0026#34;读\u0026#34;权限允许用户查看文件内容,\r\u0026#34;写\u0026#34;权限允许用户修改内容,\r\u0026#34;执行\u0026#34;权限允许用户运行文件,比如程序 有些组织需要不同层级的权限,比如间谍机构,\u0026ldquo;访问控制列表\u0026quot;的正确配置非常重要,以确保保密性,完整性和可用性。假设我们有三个访问级别:公开,机密,绝密。\n第一个普遍的好做法是,用户不能\u0026quot;读上\u0026rdquo;, 不能读等级更高的信息,如果用户能读\u0026quot;机密\u0026quot;文件那么不应该有权限读\u0026quot;绝密\u0026quot;文件,但能访问\u0026quot;机密\u0026quot;和\u0026quot;公开\u0026quot;文件\n第二个法则是用户不能\u0026quot;写下\u0026rdquo;,如果用户等级是\u0026quot;绝密\u0026quot;,那么能写入或修改\u0026quot;绝密\u0026quot;文件,但不能修改\u0026quot;机密\u0026quot;或\u0026quot;公共\u0026quot;文件。听起来好像很奇怪 ,有最高等级也不能改等级更低的文件,但这样确保了\u0026quot;绝密\u0026quot; 不会意外泄露到\u0026quot;机密\u0026quot;文件或\u0026quot;公共\u0026quot;文件里。\n这个\u0026quot;不能向上读,不能向下写\u0026quot;的方法叫 bell-lapadula 模型,它是为美国国防部\u0026quot;多层安全政策\u0026quot;制定的,还有许多其他的访问控制模型 - 比如\u0026quot;中国墙\u0026quot;模型和\u0026quot;比伯\u0026quot;模型。哪个模型最好,取决于具体情况。\n身份验证\u0026quot;和\u0026quot;访问控制\u0026quot;帮助计算机知道\u0026quot;你是谁\u0026quot;,以及\u0026quot;你可以访问什么\u0026quot;,但做这些事情的软硬件必须是可信的,这个依赖很重要。如果攻击者给计算机装了恶意软件 - 控制了计算机的操作系统。我们怎么确定安全程序没有给攻击者留后门?短回答是 - 无法确定!我们仍然无法保证程序或计算机系统的安全,因为安全软件在理论上可能是\u0026quot;安全的\u0026quot;,实现时可能会不小心留下漏洞,但我们有办法减少漏洞出现的可能性,比如一找到就马上修复,以及当程序被攻破时尽可能减少损害。大部分漏洞都是具体实现的时候出错了,为了减少执行错误,减少执行。\n系统级安全的圣杯之一是\u0026quot;安全内核\u0026quot;,或\u0026quot;可信计算基础\u0026quot;:一组尽可能少的操作系统软件。安全性都是接近可验证的,构建安全内核的挑战在于 决定内核应该有什么。记住,代码越少越好!在最小化代码数量之后,要是能\u0026quot;保证\u0026quot;代码是安全的,会非常棒。\n正式验证代码的安全性 是一个活跃的研究领域,我们现在最好的手段,叫 \u0026ldquo;独立安全检查和质量验证\u0026rdquo; 。让一群安全行业内的软件开发者来审计代码,这就是为什么安全型代码几乎都是开源的,写原始代码的人通常很难找到错误,但外部开发人员有新鲜的眼光和不同领域的专业知识,可以发现问题。 另外还有一些安全大会,安全专家可以相互认识,分享想法。 一年一次在拉斯维加斯举办的 def con 是全球最大的安全大会。\n最后,即便尽可能减少代码 ,并进行了安全审计。聪明的攻击者还是会找到方法入侵,因为如此,优秀的开发人员应该计划当程序被攻破后,如何限制损害,控制损害的最大程度,并且不让它危害到计算机上其他东西,这叫 \u0026ldquo;隔离\u0026rdquo; 。要实现隔离,我们可以\u0026quot;沙盒\u0026quot;程序,这好比把生气的小孩放在沙箱里,他们只能摧毁自己的沙堡,不会影响到其他孩子。操作系统会把程序放到沙盒里,方法是给每个程序独有的内存块,其他程序不能动。一台计算机可以运行多个虚拟机,虚拟机模拟计算机,每个虚拟机都在自己的沙箱里。如果一个程序出错,最糟糕的情况是它自己崩溃或者搞坏它处于的虚拟机。计算机上其他虚拟机是隔离的,不受影响。\n好,一些重要安全概念的概览 我们到此就介绍完了。我都还没讲网络安全,比如防火墙。下集我们会讨论 黑客侵入系统的一些方法,然后我们学加密。在此之前,别忘了加强你的密码,打开两步验证,永远不要点可疑邮件。\n我们下周见。\n32. 黑客 \u0026amp; 攻击 i.e. hackers \u0026amp; cyber attacks\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讲了计算机安全的基础知识,包括各种原则和技术,但尽管尽了最大努力,新闻上还是各种 个人,公司,政府被黑客攻击的故事。那些黑客凭技术知识闯入计算机系统,不是所有黑客都是坏人,有些黑客会寻找并修复软件漏洞 ,让系统更安全,他们经常被公司和政府雇来做安全评估,这些黑客叫\u0026quot;白帽子\u0026quot;,他们是好人。另一方面,也有\u0026quot;黑帽\u0026quot;黑客,他们窃取,利用和销售计算机漏洞和数据。黑客的动机有很多种,有些是好玩和好奇,而网络罪犯一般是为了钱。还有的叫\u0026quot;黑客行动主义者\u0026quot;,通过黑客手段影响社会或达到政治目的,这只是冰山一角。\n一般对黑客的刻板印象是某个不受欢迎的小孩在黑暗的房间里,到处都是吃完的比萨盒,这个印象是错的,形容约翰·格林的宿舍还更贴切些。\n今天,我们不会教你如何成为黑客,而是讨论一些入侵原理,给你一个大概概念。\n黑客入侵最常见的方式,不是通过技术,而是欺骗别人,这叫\u0026quot;社会工程学\u0026quot;,欺骗别人让人泄密信息,或让别人配置电脑系统,变得易于攻击。\n最常见的攻击是网络钓鱼,你可能见过,银行发邮件叫你点邮件里的链接,登陆账号,然后你会进入一个像官网的网站,但实际上是个假网站,当你输入用户名和密码时,信息会发给黑客,然后黑客就可以假扮你登陆网站,坏消息!即使成功率只有 1/1000,发一百万封钓鱼邮件,也有一千个帐户中招。\n另一种方法叫 假托 (pretexting),攻击者给某个公司打电话,假装是 it 部门的人,攻击者的第一通电话一般会叫人转接,这样另一个人接的时候,电话看起来像内部的,然后让别人把电脑配置得容易入侵,或让他们泄露机密信息,比如密码或网络配置。\n\u0026gt; 不好意思,等一下。\r\u0026gt; 嘿,我是 it 部门的苏珊\r\u0026gt; 我们遇到一些网络问题,你能帮我检查一个配置吗?\r\u0026gt; 然后就开始了…… 只要预先做一点研究,攻击者可以装得很像真的,比如关键员工的名字。也许要 10 通电话才能找到一个受害者,但只要一个人上当就够了。\n邮件里带\u0026quot;木马\u0026quot;也是常见手段,木马会伪装成无害的东西,比如照片或发票,但实际上是恶意软件。恶意软件有很多种,有的会偷数据,比如银行凭证,有的会加密文件,交赎金才解密,也就是\u0026quot;勒索软件\u0026quot;。如果攻击者无法用木马或电话欺骗,攻击者只能被迫用其他手段,方法之一是暴力尝试,我们上集讨论过,尝试所有可能的密码,直到进入系统。大多数现代系统会加长等待时间,来抵御这种攻击,每次失败就加长等待时间,甚至失败超过一定次数后,完全锁住。\n最近出现一种攻破方法叫 \u0026ldquo;nand 镜像\u0026rdquo;,如果能物理接触到电脑,可以往内存上接几根线,复制整个内存,复制之后,暴力尝试密码,直到设备让你等待,这时只要把复制的内容覆盖掉内存,本质上重置了内存,就不用等待,可以继续尝试密码了,这项方法在 iphone 5c 上管用,更新的设备有机制阻止这种攻击。\n如果你无法物理接触到设备,就必须远程攻击,比如通过互联网。 远程攻击一般需要攻击者利用系统漏洞,来获得某些能力或访问权限,这叫\u0026quot;漏洞利用\u0026quot;(exploit)。一种常见的漏洞利用叫\u0026quot;缓冲区溢出\u0026quot;,\u0026ldquo;缓冲区\u0026quot;是一种概称,指预留的一块内存空间。\n我们在第 23 集,讨论过存像素数据的视频缓冲区。举个简单例子,假设我们在系统登陆界面,要输入用户名和密码。在幕后,系统用缓冲区存输入的值,假设缓冲区大小是 10 ,两个文本缓冲区看起来会像这样:\n当然,操作系统记录的远不止用户名和密码,所以缓冲区前后 肯定有其他数据。当用户输入用户名和密码时,这些值会复制到缓冲区,然后验证是否正确。缓冲区溢出\u0026quot;正如名字所暗示的:它会溢出缓冲区。在这个例子中,超过十个字符的密码会覆盖掉相邻的数据,有时只会让程序或系统崩溃,因为重要值被垃圾数据覆盖了。系统崩溃是坏事,但也许恶作剧黑客就只是想系统崩溃,当个讨厌鬼。但攻击者可以更巧妙地利用这个漏洞 (bug),注入有意义的新值到程序的内存中,比如把\u0026quot;is_admin\u0026quot;的值改成 true。有了任意修改内存的能力,黑客可以绕过\u0026quot;登录\u0026quot;之类的东西,甚至使用那个程序劫持整个系统。有很多方法阻止缓冲区溢出,最简单的方法是,复制之前先检查长度,这叫 \u0026ldquo;边界检查\u0026rdquo;。 许多现代编程语言自带了边界检查,程序也会随机存放变量在内存中的位置, 比如我们之前假设的\u0026quot;is_admin\u0026rdquo;,这样黑客就不知道应该覆盖内存的哪里,导致更容易让程序崩溃,而不是获得访问权限。程序也可以在缓冲区后,留一些不用的空间,然后跟踪里面的值,看是否发生变化,如果发生了变化,说明有攻击者在乱来,这些不用的内存空间叫\u0026quot;金丝雀\u0026quot;,因为以前矿工会带金丝雀下矿,金丝雀会警告危险。\n另一种经典手段叫\u0026quot;代码注入\u0026quot;,最常用于攻击用数据库的网站,几乎所有大网站都用数据库。我们这个系列中不会讲解数据库,所以以下是个简单例子。\n我们会用\u0026quot;结构化查询语言\u0026quot;,也叫 sql,一种流行的数据库 api。假设网页上有登录提示,当用户点击\u0026quot;登录\u0026quot;时,值会发到服务器,服务器会运行代码,检查用户名是否存在,如果存在,看密码是否匹配。 为了做检查,服务器会执行一段叫 \u0026ldquo;sql 查询\u0026rdquo; 的代码,看起来像这样。\n首先,语句要指定从数据库里查什么数据。在这个例子中,我们想查的是密码 (password) (select password),还要指定从哪张表查数据 (from users)。在这个例子里,我们假设所有用户数据都存在 \u0026ldquo;users\u0026rdquo; 表里,最后,服务器不想每次取出一个巨大密码列表,包含所有用户密码,所以用 username = \u0026lsquo;用户名\u0026rsquo;代表只要这个用户,用户输的值会复制到\u0026quot;sql 查询\u0026quot;,所以实际发到 sql 数据库的命令,是这样的 - where username=\u0026lsquo;philbin\u0026rsquo; 。还要注意,sql 命令以分号结尾,那怎么破解这个? 做法是把\u0026quot;sql 命令\u0026quot;输入到用户名里! 比如我们可以发这个奇怪的用户名:\n当服务器把值复制到 sql 查询中,会变成这样:\n正如之前提的,分号用于分隔命令,所以第一条被执行的命令是:\n如果有个用户叫\u0026quot;whateer\u0026quot;,数据库将返回密码。当然,我们不知道密码是什么,所以会出错,服务器会拒绝我们。如果没有一个用户叫\u0026quot;whatever\u0026quot;,数据库会返回 空密码或直接错误,服务器也会拒绝我们。总之 ,我们不在乎,我们感兴趣的是下一个 sql 命令: \u0026ldquo;drop table users\u0026rdquo; - 我们注入的命令。这条命令的意思是删掉 users 这张表,全删干净!这会造成很多麻烦,不管是银行或什么其他地方,注意,我们甚至不需要侵入系统,我们没有猜到正确的用户名和密码,即使没有正式访问权限,还是可以利用 bug 来制造混乱,这是代码注入的一个简单例子。如今几乎所有服务器都会防御这种手段,如果指令更复杂一些,也许可以添加新记录到数据库 - 比如一个新管理员帐户 - 甚至可以让数据库泄露数据,使得黑客窃取信用卡号码,社会安全号码,以及各种其他信息,但我们不会教你具体怎么做 。\n就像缓冲区溢出攻击一样,应该总是假设外部数据是危险的,应该好好检查。\n很多用户名和密码表单,不让你输入特殊字符,比如分号或者括号,作为第一道防御。好的服务器也会清理输入,比如修改或删除特殊字符,然后才放到数据库查询语句里。管用的漏洞利用 (exploits) 一般会在网上贩卖或分享,如果漏洞很流行,或造成的危害很大,价格会越高,或者名气越大,有时甚至政府也会买漏洞利用,让他们侵入系统做间谍工作。当软件制造者不知道软件有新漏洞被发现了,那么这个漏洞叫 \u0026ldquo;零日漏洞\u0026rdquo;。黑帽黑客经常赶时间,抢在白帽程序员做出补丁之前,尽可能利用漏洞,所以保持系统更新非常重要,很多更新都是安全性补丁。如果有足够多的电脑有漏洞,让恶意程序可以在电脑间互相传播,那么叫\u0026quot;蠕虫\u0026quot;。如果黑客拿下大量电脑,这些电脑可以组成\u0026quot;僵尸网络\u0026quot;,可以用于很多目的,比如发大量垃圾邮件,用别人电脑的计算能力和电费挖 bitcoin,或发起\u0026quot;拒绝服务攻击\u0026quot;简称 ddos,攻击服务器。ddos 就是僵尸网络里的所有电脑发一大堆垃圾信息,堵塞服务器,要么迫使别人交钱消灾,或纯粹为了作恶。\n尽管白帽黑客非常努力工作,漏洞利用的文档都在网上,编写软件有很多\u0026quot;最佳实践\u0026quot;,网络攻击每天都在发生,每年损害全球经济差不多 5000 亿,并且随着我们越来越依赖计算机系统,这个数字只会增加。 这使得政府非常担心,因为基础设施越来越电脑化,比如电力厂,电网,交通灯,水处理厂,炼油厂,空管,还有很多其他关键系统。\n很多专家预测下一次大战会主要是网络战争。国家不是被物理攻击打败,而是因为网络战争导致经济和基础设施崩溃。也许不会发射一颗子弹,但是人员伤亡的可能性依然很高,甚至可能高于传统战争,所以大家都应该知道一些方法保证网络安全。\n全球社区因为互联网而互相连接,我们应该确保自己的电脑安全,抵御其他想做坏事的人,也许不要再忽略更新提示?\n我们下周见。\n33. 加密 i.e. cryptography\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n在过去两集,我们聊了很多计算机安全话题,但事实是 ,世上不存在 100%安全的系统,总会有漏洞存在,而且安全专家知道这一点,所以系统架构师会部署\u0026quot;多层防御\u0026quot;,用多层不同的安全机制来阻碍攻击者。有点像城堡的设计一样,首先要避开弓箭手,穿过护城河,翻过城墙,避开热油,打败守卫,才能达到王座。不过我们这里要说的是,计算机安全中最常见的防御形式 - 密码学!\n密码学 (cryptography) 一词 来自 crypto 和 graphy,大致翻译成\u0026quot;秘密写作\u0026quot;。为了加密信息,要用加密算法 (cipher) 把明文转为密文,除非你知道如何解密,不然密文看起来只是一堆乱码。把明文转成密文叫\u0026quot;加密\u0026quot;,把密文恢复回明文叫\u0026quot;解密\u0026quot;(decryption),加密算法早在计算机出现前就有了。\n朱利叶斯·凯撒 用如今我们叫\u0026quot;凯撒加密\u0026quot;的方法 来加密私人信件,他会把信件中的字母 向前移动三个位置,所以 a 会变成 d,brutus 变成 euxwxv。为了解密,接收者要知道 1. 用了什么算法 2. 要偏移的字母位数。\n有一大类算法叫\u0026quot;替换加密\u0026quot;,凯撒密码是其中一种,算法把每个字母替换成其他字母。但有个巨大的缺点是,字母的出现频率是一样的,举个例子,e 在英语中是最常见的字母,如果把 e 加密成 x ,那么密文中 x 的出现频率会很高,熟练的密码破译师可以从统计数据中发现规律,进而破译密码。 1587 年,正因为一个\u0026quot;替换加密\u0026quot;的密文被破译,导致杀伊丽莎白女王的阴谋暴露,使得玛丽女王被处决。\n另一类加密算法叫 \u0026ldquo;移位加密\u0026rdquo;。我们来看一个简单例子叫 \u0026ldquo;列移位加密\u0026rdquo;,我们把明文填入网格,网格大小我们这里选择 5x5,为了加密信息,我们换个顺序来读,比如从左边开始,从下往上,一次一列。加密后字母的排列不同了,解密的关键是,知道读取方向和网格大小是 5x5 。就像之前,如果接收者知道密文和加密方法 才能解密得到原始消息。\n到了 1900 年代,人们用密码学做了加密机器,其中最有名的是德国的英格玛(enigma)纳粹在战时用英格玛加密通讯信息。正如第 15 集中说过,enigma 是一台像打字机的机器,有键盘和灯板,两者都有完整的字母表,而且它有一系列\u0026quot;转子\u0026quot;(rotros) ,是加密的关键。首先,我们只看一个转子,它一面有 26 个接触点,代表 26 个字母,然后线会连到另一面,替换字母,如果输入\u0026rsquo;h\u0026rsquo;,\u0026lsquo;k\u0026rsquo;会从另一边出来,如果输入\u0026rsquo;k\u0026rsquo;,\u0026lsquo;f\u0026rsquo;会从另一边出来,以此类推,这个字母替换过程你应该听起来很熟悉:它是\u0026quot;替换加密\u0026quot;! 但英格玛 (enigma) 更复杂一些,因为它有 3 个或更多转子,一个转子的输出作为下一个转子的输入。 转子还有 26 个起始位置,还可以按不同顺序放入转子,提供更多字母替换映射。转子之后是一个叫\u0026quot;反射器\u0026quot;的特殊电路,它每个引脚会连到另一个引脚,并把信号发回给转子。最后,机器前方有一个插板,可以把输入键盘的字母预先进行替换,又加了一层复杂度。\n让我们用这里的简化版电路,加密一些字母。如果我们按下\u0026quot;h\u0026quot;键,电流会先通过插板,然后通过转子,到达反射器,然后回来转子,回来插板,并照亮键盘灯板的字母\u0026quot;l\u0026quot;,h 就加密成了 l 。注意,电路是双向的,所以如果我们按下 l,h 会亮起来。换句话说,加密和解密的步骤是一样的,你只需要确保 发送机和接收机的初始配置一样就行。如果你有仔细观察,会注意到字母加密后一定会变成另一个字母,之后这成为最大的弱点。最后,为了让英格玛不只是简单的\u0026quot;替换加密\u0026quot;,每输入一个字母,转子会转一格,有点像汽车里程表。如果你输入 a-a-a,可能会变成 b-d-k,映射会随着每次按键而改变。英格玛当然是一块难啃的骨头,但正如我们第 15 集中说的,艾伦·图灵和同事破解了英格玛加密,并把大部分破解流程做成了自动化。\n但随着计算机出现,加密从硬件转往软件,早期加密算法中,应用最广泛的是 ibm 和 nsa 于 1977 年开发的\u0026quot;数据加密标准\u0026quot;(data encryption standard)。des 最初用的是 56 bit 长度的二进制密钥,意味着有 2 的 56 次方,或大约 72 千万亿个不同密钥。在 1977 年时,也许 nsa 有这能力,但没有其他人有足够计算能力 来暴力破解所有可能密钥。但到 1999 年,一台 25 万美元的计算机能在两天内 把 des 的所有可能密钥都试一遍,让 des 算法不再安全。因此 2001 年出了:高级加密标准(aes,advanced encryption standard)。\naes 用更长的密钥 - 128 位/192 位/256 位 - 让暴力破解更加困难。128 位的密钥,哪怕用现在地球上的所有计算机也要上万亿年才能试遍所有组合。你最好赶紧开始!aes 将数据切成一块一块,每块 16 个字节,然后用密钥进行一系列替换加密和移位加密,再加上一些其他操作,进一步加密信息,每一块数据,会重复这个过程 10 次或以上。你可能想知道:为什么只重复 10 次?为什么用 128 位密钥,而不是 10000 位?这其实是基于性能的权衡。如果要花几小时加密和发邮件,或几分钟载入网站,没人愿意用。aes 在性能和安全性间取得平衡,如今 aes 被广泛使用,比如 iphone 上加密文件,用 wpa2 协议在 wifi 中访问 https 网站。\n到目前为止 ,我们讨论过的加密技术依赖于发送者和接收者都知道密钥,发件人用密钥加密,收件人用相同的密钥解密。以前,密钥可以口头约定,或依靠物品,比如德国人给英格玛配了密码本,上面有每天的配置,但互联网时代没法这样做,你能想象 要打开密码本才能访问 youtube 吗?我们需要某种方法 在公开的互联网上传递密钥给对方,这看起来好像不安全,如果密钥被黑客拦截了,黑客不就能解密通信了吗?解决方案是 \u0026ldquo;密钥交换\u0026rdquo;!\n密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法,我们可以用\u0026quot;单向函数\u0026quot;来做。单项函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难。为了让你明白单项函数,我们拿颜色作比喻,将颜色混合在一起很容易,但想知道混了什么颜色很难,要试很多种可能才知道,用这个比喻,那么我们的密钥是一种独特的颜色。首先,有一个公开的颜色,所有人都可以看到,然后,约翰和我各自选一个秘密颜色,只有自己知道。为了交换密钥,我把我的 秘密颜色 和 公开颜色 混在一起,然后发给约翰,可以写信发,用信鸽发,什么方式都行。约翰也这样做,把他的秘密颜色和公开颜色混在一起,然后发我。我收到约翰的颜色之后,把我的秘密颜色加进去, 现在 3 种颜色混合在一起。john 也一样做。瞧!我们有了一样的颜色,我们可以把这个颜色当密钥,尽管我们从来没有给对方发过这颜色。外部窥探者可以知道部分信息,但无法知道最终颜色,当然,计算机要传输数据时,混合颜料和发颜料不太合适,但幸运的是,数学单向函数是完美的,我们可以用 \u0026ldquo;迪菲-赫尔曼密钥交换\u0026rdquo;。在 diffie-hellman 中,单向函数是模幂运算,意思是先做幂运算,拿一个数字当底数,拿一个数字当指数,比如 a\nb\n然后除以第三个数字,最后拿到我们想要的余数。\n举个例子,假设我们想算 3 的 5 次方,模 31 ,我们先算 3 的 5 次方,得到 243,然后除 31,取余数,得到 26 。重点是如果只给余数和基数,很难得知指数是多少。如果我告诉你,3 的某次方 模 31,余数是 7 。你要试很多次,才能知道次方是多少。如果把数字变长一些,比如几百位长,想找到秘密指数是多少,几乎是不可能的。\n现在我们来讨论 diffie-hellman 是怎么,用模幂运算 算出双方共享的密钥。首先,我们有公开的值 - 基数和模数,就像公开的油漆颜色,所有人都看的到,甚至坏人!为了安全向 john 发信息,我选一个秘密指数:x ,然后算 b^x mod m 的结果,然后把这个大数字发给 john 。john 也一样做,选一个秘密指数 y,然后把 b^y mod m 的结果发我,为了算出 双方共用的密钥,我把 john 给我的数,用我的秘密指数 x,进行模幂运算 (看上图),数学上相等于 b 的 xy 次方 模 m 。john 也一样做,拿我给他的数 进行模幂运算,最终得到一样的数。双方有一样的密钥,即使我们从来没给对方发过各自的秘密指数。我们可以用这个大数字当密钥,用 aes 之类的加密技术来加密通信,\u0026ldquo;diffie-hellman 密钥交换\u0026quot;是建立共享密钥的一种方法。\n双方用一样的密钥加密和解密消息,这叫\u0026quot;对称加密\u0026rdquo;, 因为密钥一样,凯撒加密,英格玛,aes 都是\u0026quot;对称加密\u0026quot;,还有\u0026quot;非对称加密\u0026quot;,有两个不同的密钥,一个是公开的,另一个是私有的。人们用公钥加密消息 ,只有有私钥的人能解密。换句话说,知道公钥只能加密但不能解密 - 它是\u0026quot;不对称\u0026quot;的!\n想象一个可以锁上的盒子,为了收到安全的信息,我们可以给别人箱子和锁,别人把信息放箱子,然后锁起来,把盒子寄回给我,只有我的钥匙能打开。上锁后,如果发件人或其他人想打开盒子 除了暴力尝试没有其他办法。和盒子例子一样,公钥加密后只能私钥来解密。反过来也是可以的:私钥加密后 ,用公钥解密。这种做法用于签名,服务器可以用私钥加密,任何人都可以用服务器的公钥解密,就像一个不可伪造的签名,因为只有私钥的持有人 能加密,这能证明数据来自正确的服务器或个人,而不是某个假冒者。\n目前最流行的\u0026quot;非对称加密\u0026quot;技术是 rsa ,名字来自发明者: rivest, shamir, adleman 。\n现在你学会了现代密码学的所有\u0026quot;关键\u0026quot;部分: 对称加密,密钥交换,公钥密码学。\n当你访问一个安全的网站,比如银行官网,绿色锁图标代表 用了公钥密码学,验证服务器的密钥,然后建立临时密钥,然后用对称加密保证通信安全。不管你是网上购物,发邮件给朋友,还是看猫咪视频,不管你是网上购物,发邮件给朋友,还是看猫咪视频,密码学都在保护你的隐私和安全。\n谢啦密码学!\n34. 机器学习 \u0026amp; 人工智能 i.e. machine learning \u0026amp; artificial intelligence\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n我们之前说过 计算机很擅长存放,整理,获取和处理大量数据,很适合有上百万商品的电商网站,或是存几十亿条健康记录,方便医生看,但如果想根据数据做决定呢?这是机器学习的本质,机器学习算法让计算机可以从数据中学习,然后自行做出预测和决定,能自我学习的程序很有用 。比如判断是不是垃圾邮件?这人有心律失常吗?youtube 的下一个视频该推荐哪个?虽然有用,但我们不会说它有人类一般的智能,虽然 ai 和 ml 这两词经常混着用。\n大多数计算机科学家会说 ,机器学习是为了实现人工智能这个更宏大目标的技术之一,人工智能简称 ai(artificial intelligence)。机器学习和人工智能算法一般都很复杂,所以我们不讲具体细节,重点讲概念。\n我们从简单例子开始:判断飞蛾是\u0026quot;月蛾\u0026quot;还是\u0026quot;帝蛾\u0026quot;,这叫\u0026quot;分类\u0026quot;,做分类的算法叫 \u0026ldquo;分类器\u0026rdquo;。虽然我们可以用 照片和声音 来训练算法,很多算法会减少复杂性,把数据简化成 \u0026ldquo;特征\u0026rdquo;,\u0026ldquo;特征\u0026quot;是用来帮助\u0026quot;分类\u0026quot;的值。对于之前的飞蛾分类例子我们用两个特征:\u0026ldquo;翼展\u0026quot;和\u0026quot;重量\u0026rdquo;,为了训练\u0026quot;分类器\u0026quot;做出好的预测,我们需要\u0026quot;训练数据\u0026rdquo;,为了得到数据,我们派昆虫学家到森林里 收集\u0026quot;月蛾\u0026quot;和\u0026quot;帝蛾\u0026quot;的数据。专家可以认出不同飞蛾,所以专家不只记录特征值,还会把种类也写上,这叫 \u0026ldquo;标记数据\u0026rdquo;。因为只有两个特征,很容易用散点图把数据视觉化。红色标了 100 个帝蛾蓝色标了 100 个月蛾,可以看到大致分成了两组,但中间有一定重叠,所以想完全区分两个组比较困难,所以机器学习算法登场 - 找出最佳区分。我用肉眼大致估算下,然后判断 翼展小于 45 毫米的 很可能是帝蛾,可以再加一个条件,重量必须小于 75 ,才算是帝蛾。这些线叫 \u0026ldquo;决策边界\u0026rdquo;,如果仔细看数据,86 只帝蛾在正确的区域,但剩下 14 只在错误的区域。另一方面,82 只月蛾在正确的区域,18 个在错误的区域。这里有个表 记录正确数和错误数,这表叫\u0026quot;混淆矩阵\u0026quot;。\u0026ldquo;黑客帝国三部曲\u0026quot;的后两部也许该用这个标题。注意我们没法画出 100% 正确分类的线,降低翼展的决策边界,会把更多\u0026quot;帝蛾\u0026quot;误分类成\u0026quot;月蛾\u0026rdquo;。如果提高,会把更多月蛾分错类。\n机器学习算法的目的,是最大化正确分类 + 最小化错误分类。在训练数据中,有 168 个正确,32 个错误,平均准确率 84% 。用这些决策边界,如果我们进入森林,碰到一只不认识的飞蛾,我们可以测量它的特征,并绘制到决策空间上,这叫 \u0026ldquo;未标签数据\u0026rdquo;。决策边界可以猜测飞蛾种类,这里我们预测是\u0026quot;月蛾\u0026quot;,这个把决策空间 切成几个盒子的简单方法,可以用\u0026quot;决策树\u0026quot;来表示。画成图像,会像左侧 用 if 语句写代码,会像右侧。生成决策树的 机器学习算法需要选择用什么特征来分类,每个特征用什么值。\u0026ldquo;决策树\u0026quot;只是机器学习的一个简单例子,如今有数百种算法,而且新算法不断出现,一些算法甚至用多个\u0026quot;决策树\u0026quot;来预测,计算机科学家叫这个\u0026quot;森林\u0026rdquo;,因为有多颗树嘛。也有不用树的方法,比如\u0026quot;支持向量机\u0026quot;,本质上是用任意线段来切分\u0026quot;决策空间\u0026quot;,不一定是直线,可以是多项式或其他数学函数。就像之前,机器学习算法负责找出最好的线,最准的决策边界。之前的例子只有两个特征,人类也可以轻松做到,如果加第 3 个特征,比如\u0026quot;触角长度\u0026quot;,那么 2d 线段,会变成 3d 平面,在三个维度上做决策边界,这些平面不必是直的,而且 真正有用的分类器 会有很多飞蛾种类。你可能会同意 现在变得太复杂了,但这也只是个简单例子 - 只有 3 个特征和 5 个品种,我们依然可以用 3d 散点图 画出来。不幸的是,一次性看 4 个或 20 个特征,没有好的方法,更别说成百上千的特征了,但这正是机器学习要面临的问题。你能想象靠手工 在一个上千维度的决策空间里给超平面 (hyperplane) 找出一个方程吗?大概不行。但聪明的机器学习算法可以做到, google,facebook,微软和亚马逊的计算机里整天都在跑这些算法。\n\u0026ldquo;决策树\u0026quot;和\u0026quot;支持向量机\u0026quot;这样的技术 发源自统计学,统计学早在计算机出现前,就在用数据做决定。\n有一大类机器学习算法用了统计学,但也有不用统计学的算法,其中最值得注意的是 人工神经网络,灵感来自大脑里的神经元,想学习神经元知识的人,可以看这 3 集。神经元是细胞,用电信号和化学信号 来处理和传输消息,它从其他细胞 得到一个或多个输入,然后处理信号并发出信号,形成巨大的互联网络,能处理复杂的信息。就像你的大脑 在看这个视频,人造神经元很类似,可以接收多个输入,然后整合并发出一个信号,它不用电信号或化学信号,而是吃数字进去,吐数字出来。它们被放成一层层,形成神经元网络,因此得名神经网络。\n回到飞蛾例子,看如何用神经网络分类。\n\u0026gt; 我们的第一层 - 输入层 -\r\u0026gt; 提供需要被分类的单个飞蛾数据\r\u0026gt; 同样,这次也用重量和翼展\r\u0026gt; \u0026gt; 另一边是输出层,有两个神经元:\r\u0026gt; 一个是帝蛾,一个是月蛾\r\u0026gt; 2 个神经元里最兴奋的 就是分类结果\r\u0026gt; \u0026gt; 中间有一个隐藏层\r\u0026gt; 负责把输入变成输出,负责干分类这个重活\r\u0026gt; 为了看看它是如何分类的\r\u0026gt; 我们放大\u0026#34;隐藏层\u0026#34;里的一个神经元\r\u0026gt; 神经元做的第一件事 是把每个输入乘以一个权重\r\u0026gt; \u0026gt; 假设 2.8 是第一个输入,0.1 是第二个输入。\r\u0026gt; 然后它会相加输入\r\u0026gt; 总共是 9.74\r\u0026gt; \u0026gt; 然后对这个结果,用一个偏差值处理\r\u0026gt; 意思是 加或减一个固定值\r\u0026gt; 比如-6,得到 3.74\r\u0026gt; \u0026gt; 做神经网络时,这些偏差和权重,一开始会设置成随机值\r\u0026gt; 然后算法会调整这些值 来训练神经网络\r\u0026gt; 使用\u0026#34;标记数据\u0026#34;来训练和测试\r\u0026gt; 逐渐提高准确性\r\u0026gt; - 很像人类学习的过程\r\u0026gt; \u0026gt; 最后,神经元有激活函数,它也叫传递函数,\r\u0026gt; 会应用于输出,对结果执行最后一次数学修改\r\u0026gt; 例如,把值限制在-1 和+1 之间\r\u0026gt; 或把负数改成 0\r\u0026gt; 我们用线性传递函数,它不会改变值\r\u0026gt; 所以 3.74 还是 3.74\r\u0026gt; \u0026gt; 所以这里的例子\r\u0026gt; 输入 0.55 和 82,输出 3.74\r\u0026gt; 这只是一个神经元,\r\u0026gt; 但加权,求和,偏置,激活函数\r\u0026gt; 会应用于一层里的每个神经元\r\u0026gt; 并向前传播,一次一层\r\u0026gt; \u0026gt; 数字最高的就是结果:\r\u0026gt; 月蛾 重要的是,隐藏层不是只能有一层,可以有很多层,\u0026ldquo;深度学习\u0026quot;因此得名。训练更复杂的网络, 需要更多的计算量和数据。尽管神经网络 50 多年前就发明了,深层神经网络直到最近才成为可能,感谢强大的处理器和超快的 gpu,感谢游戏玩家对帧率的苛刻要求!\n几年前,google 和 facebook 展示了深度神经网络 在照片中识别人脸的准确率,和人一样高 - 人类可是很擅长这个的! 这是个巨大的里程碑! 现在有深层神经网络开车,翻译,诊断医疗状况等等,这些算法非常复杂,但还不够\u0026quot;聪明\u0026rdquo;,它们只能做一件事,分类飞蛾,找人脸,翻译,这种 ai 叫\u0026quot;弱 ai\u0026quot;或\u0026quot;窄 ai\u0026rdquo;,只能做特定任务。但这不意味着它没用,能自动做出诊断的医疗设备,和自动驾驶的汽车真是太棒了!但我们是否需要这些计算机来创作音乐,在空闲时间找美味食谱呢?也许不要! 如果有的话 还挺酷的,真正通用的,像人一样聪明的 ai,叫 \u0026ldquo;强 ai\u0026rdquo;,目前没人能做出来 接近人类智能的 ai,有人认为不可能做出来,但许多人说 数字化知识的爆炸性增长 - 比如维基百科,网页和 youtube 视频 - 是\u0026quot;强 ai\u0026quot;的完美引燃物。你一天最多只能看 24 小时的 youtube ,计算机可以看上百万小时,比如,ibm 的沃森吸收了 2 亿个网页的内容,包括维基百科的全文。虽然不是\u0026quot;强 ai\u0026quot;, 但沃森也很聪明 。在 2011 年的知识竞答中碾压了人类,ai 不仅可以吸收大量信息 也可以不断学习进步,而且一般比人类快得多。2016 年 google 推出 alphago - 一个会玩围棋的窄 ai ,它和自己的克隆版下无数次围棋 ,从而打败最好的人类围棋选手。\n学习什么管用,什么不管用 自己发现成功的策略,这叫 \u0026ldquo;强化学习\u0026rdquo; ,是一种很强大的方法,和人类的学习方式非常类似。人类不是天生就会走路,是上千小时的试错学会的,计算机现在才刚学会反复试错来学习,对于很多狭窄的问题,强化学习已被广泛使用,有趣的是,如果这类技术可以更广泛地应用,创造出类似人类的\u0026quot;强 ai\u0026quot; 能像人类小孩一样学习,但学习速度超快。 如果这发生了,对人类可能有相当大的影响 - 我们以后会讨论。\n感谢收看。我们下周见。\n35. 计算机视觉 i.e. computer vision\n(。・∀・)ノ゙嗨 我是 carrie anne,欢迎收看计算机科学速成课\n今天, 我们来思考视觉的重要性,大部分人靠视觉来做饭,越过障碍,读路牌,看视频,以及无数其它任务。视觉是信息最多的感官 ,比如周围的世界是怎样的,如何和世界交互,因此半个世纪来计算机科学家一直在想办法让计算机有视觉,因此诞生了\u0026quot;计算机视觉\u0026quot;这个领域,目标是让计算机理解图像和视频。用过相机或手机的都知道 ,可以拍出有惊人保真度和细节的照片 - 比人类强得多,但正如计算机视觉教授 李飞飞 最近说的,\u0026ldquo;听到\u0026quot;不等于\u0026quot;听懂\u0026rdquo;,\u0026ldquo;看到\u0026quot;不等于\u0026quot;看懂\u0026rdquo;。\n复习一下,图像是像素网格,每个像素的颜色通过三种基色定义:红,绿,蓝,通过组合三种颜色的强度 ,可以得到任何颜色, 也叫 rgb 值。\n最简单的计算机视觉算法,最合适拿来入门的,是跟踪一个颜色物体,比如一个粉色的球。首先,我们记下球的颜色,保存最中心像素的 rgb 值,然后给程序喂入图像,让它找最接近这个颜色的像素。算法可以从左上角开始,逐个检查像素,计算和目标颜色的差异,检查了每个像素后,最贴近的像素,很可能就是球。不只是这张图片, 我们可以在视频的每一帧图片跑这个算法,跟踪球的位置。当然,因为光线,阴影和其它影响,球的颜色会有变化,不会和存的 rgb 值完全一样,但会很接近。如果情况更极端一些 比如比赛是在晚上,追踪效果可能会很差。如果球衣的颜色和球一样,算法就完全晕了,因此很少用这类颜色跟踪算法,除非环境可以严格控制。\n颜色跟踪算法是一个个像素搜索, 因为颜色是在一个像素里,但这种方法 不适合占多个像素的特征,比如物体的边缘,是多个像素组成的。为了识别这些特征,算法要一块块像素来处理,每一块都叫\u0026quot;块\u0026quot;。举个例子,找垂直边缘的算法,假设用来帮无人机躲避障碍。为了简单,我们把图片转成灰度 ,不过大部分算法可以处理颜色,放大其中一个杆子,看看边缘是怎样的,可以很容易地看到 杆子的左边缘从哪里开始,因为有垂直的颜色变化。我们可以弄个规则说,某像素是垂直边缘的可能性 取决于左右两边像素的颜色差异程度,左右像素的区别越大,这个像素越可能是边缘,如果色差很小,就不是边缘,这个操作的数学符号 看起来像这样,这叫\u0026quot;核\u0026quot;或\u0026quot;过滤器\u0026quot;,里面的数字用来做像素乘法,总和 存到中心像素里。\n我们来看个实际例子。我已经把所有像素转成了灰度值,现在把\u0026quot;核\u0026quot;的中心,对准感兴趣的像素,这指定了每个像素要乘的值,然后把所有数字加起来。在这里,最后结果是 147,成为新像素值,把 核 应用于像素块,这种操作叫\u0026quot;卷积\u0026quot;。现在我们把\u0026quot;核\u0026quot;应用到另一个像素,结果是 1,色差很小,不是边缘。如果把\u0026quot;核\u0026quot;用于照片中每个像素,结果会像这样,垂直边缘的像素值很高,注意,水平边缘(比如背景里的平台),几乎看不见。如果要突出那些特征,要用不同的\u0026quot;核\u0026quot;,用对水平边缘敏感的\u0026quot;核\u0026quot;,这两个边缘增强的核叫 \u0026ldquo;prewitt 算子\u0026rdquo;,以发明者命名,这只是众多\u0026quot;核\u0026quot;的两个例子。\n\u0026ldquo;核\u0026quot;能做很多种图像转换,比如这个\u0026quot;核\u0026quot;能锐化图像,这个\u0026quot;核\u0026quot;能模糊图像。\u0026ldquo;核\u0026quot;也可以像饼干模具一样,匹配特定形状,之前做边缘检测的\u0026quot;核\u0026rdquo;,会检查左右或上下的差异,但我们也可以做出 擅长找线段的\u0026quot;核\u0026rdquo;,或者包了一圈对比色的区域,这类\u0026quot;核\u0026quot;可以描述简单的形状,比如鼻梁往往比鼻子两侧更亮,所以线段敏感的\u0026quot;核\u0026quot;对这里的值更高,眼睛也很独特 - 一个黑色圆圈被外层更亮的一层像素包着,有其它\u0026quot;核\u0026quot;对这种模式敏感。\n当计算机扫描图像时,最常见的是用一个窗口来扫,可以找出人脸的特征组合,虽然每个\u0026quot;核\u0026quot;单独找出脸的能力很弱 ,但组合在一起会相当准确,不是脸但又有一堆脸的特征在正确的位置,这种情况不太可能。这是一个早期很有影响力的算法的基础,叫 维奥拉·琼斯 人脸检测算法,如今的热门算法是 \u0026ldquo;卷积神经网络\u0026rdquo;。\n我们上集谈了神经网络,如果需要可以去看看,总之,神经网络的最基本单位,是神经元,它有多个输入,然后会把每个输入 乘一个权重值,然后求总和。听起来好像挺耳熟,因为它很像\u0026quot;卷积\u0026quot;。实际上,如果我们给神经元输入二维像素,完全就像\u0026quot;卷积\u0026quot;,输入权重等于\u0026quot;核\u0026quot;的值,但和预定义\u0026quot;核\u0026quot;不同,神经网络可以学习对自己有用的\u0026quot;核\u0026quot;,来识别图像中的特征。\u0026ldquo;卷积神经网络\u0026quot;用一堆神经元处理图像数据,每个都会输出一个新图像,本质上是被不同的\u0026quot;核\u0026quot;处理了,输出会被后面一层神经元处理,卷积卷积再卷积,第一层可能会发现\u0026quot;边缘\u0026quot;这样的特征,单次卷积可以识别出这样的东西,之前说过,下一层可以在这些基础上识别,比如由\u0026quot;边缘\u0026quot;组成的角落,然后下一层可以在\u0026quot;角落\u0026quot;上继续卷积,下一些可能有识别简单物体的神经元,比如嘴和眉毛,然后不断重复,逐渐增加复杂度,直到某一层把所有特征放到一起: 眼睛,耳朵,嘴巴,鼻子,然后说:\u0026ldquo;啊哈,这是脸!\u0026rdquo;\n\u0026ldquo;卷积神经网络\u0026quot;不是非要很多很多层,但一般会有很多层,来识别复杂物体和场景,所以算是\u0026quot;深度学习\u0026rdquo; 。\u0026ldquo;维奥拉·琼斯\u0026quot;和\u0026quot;卷积神经网络\u0026quot;不只是认人脸,还可以识别手写文字,在 ct 扫描中发现肿瘤,监测马路是否拥堵,但我们这里接着用人脸举例。\n不管用什么算法,识别出脸之后,可以用更专用的计算机视觉算法 来定位面部标志,比如鼻尖和嘴角,有了标志点,判断眼睛有没有张开就很容易了,只是点之间的距离罢了,也可以跟踪眉毛的位置,眉毛相对眼睛的位置 可以代表惊喜或喜悦,根据嘴巴的标志点,检测出微笑也很简单,这些信息可以用\u0026quot;情感识别算法\u0026quot;来识别。让电脑知道你是开心,忧伤,沮丧,困惑等等,然后计算机可以做出合适的行为。比如当你不明白时 给你提示,你心情不好时,就不弹更新提示了,这只是计算机通过视觉感知周围的一个例子。不只是物理环境 - 比如是不是在上班,或是在火车上,还有社交环境 - 比如是朋友的生日派对,还是正式商务会议。你在不同环境会有不同行为,计算机也应如此,如果它们够聪明的话。..\n面部标记点 也可以捕捉脸的形状,比如两只眼睛之间的距离,以及前额有多高,做生物识别,让有摄像头的计算机能认出你。不管是手机解锁, 还是政府用摄像头跟踪人,人脸识别有无限应用场景。另外,跟踪手臂和全身的标记点,最近也有一些突破,让计算机理解用户的身体语言,比如用户给联网微波炉的手势。\n正如系列中常说的,抽象是构建复杂系统的关键。\n计算机视觉也是一样。硬件层面,有工程师在造更好的摄像头 ,让计算机有越来越好的视力,我自己的视力却不能这样。用来自摄像头的数据 ,可以用视觉算法找出脸和手,然后可以用其他算法接着处理,解释图片中的东西,比如用户的表情和手势。有了这些,人们可以做出新的交互体验,比如智能电视和智能辅导系统 会根据用户的手势和表情来回应。\n这里的每一层都是活跃的研究领域,每年都有突破,这只是冰山一角。如今, 计算机视觉无处不在 - 商店里扫条形码, 等红灯的自动驾驶汽车,或是 snapchat 里添加胡子的滤镜。令人兴奋的是,一切才刚刚开始。最近的技术发展,比如超快的 gpu,会开启越来越多可能性。视觉能力达到人类水平的计算机 会彻底改变交互方式,当然,如果计算机能听懂我们然后回话,就更好了。\n我们下周讨论,到时见。\n36. 自然语言处理 i.e. natural language processing\n(。・∀・)ノ゙嗨 我是 carrie anne,欢迎收看计算机科学速成课\n上集我们讨论了计算机视觉 - 让电脑能看到并理解,今天我们讨论怎么让计算机理解语言。你可能会说:计算机已经有这个能力了,在第 9 和第 12 集,我们聊了机器语言和更高层次的编程语言。虽然从定义来说 ,它们也算语言,但词汇量一般很少,而且非常结构化,代码只能在拼写和语法完全正确时,编译和运行。\n当然,这和人类语言完全不同 - 人类语言叫\u0026quot;自然语言\u0026rdquo;。自然语言有大量词汇,有些词有多种含义,不同口音,以及各种有趣的文字游戏。人们在写作和说话时也会犯错,比如单词拼在一起发音,关键细节没说 ,导致意思模糊两可,以及发错音。但大部分情况下,另一方能理解,人类有强大的语言能力,因此,让计算机拥有语音对话的能力,这个想法从构思计算机时就有了。\n\u0026ldquo;自然语言处理\u0026quot;因此诞生,简称 nlp(natural language processing),结合了计算机科学和语言学的 一个跨学科领域。单词组成句子的方式有无限种,我们没法给计算机一个字典,包含所有可能句子,让计算机理解人类在嘟囔什么,所以 nlp 早期的一个基本问题是 怎么把句子切成一块块,这样更容易处理。\n上学时,老师教你 英语单词有九种基本类型:名词,代词,冠词,动词,形容词,副词,介词,连词和感叹词,这叫\u0026quot;词性\u0026rdquo;。还有各种子类,比如单数名词 vs 复数名词 ,副词最高级 vs 副词比较级,但我们不会深入那些。\n了解单词类型有用,但不幸的是,很多词有多重含义 比如 rose 和 leaves ,可以用作名词或动词。仅靠字典,不能解决这种模糊问题,所以电脑也要知道语法,因此开发了 \u0026ldquo;短语结构规则\u0026rdquo; 来代表语法规则。例如,英语中有一条规则,句子可以由一个名词短语和一个动词短语组成,名词短语可以是冠词,如 the ,然后一个名词,或一个形容词后面跟一个名词。你可以给一门语言制定出一堆规则,用这些规则,可以做出\u0026quot;分析树\u0026rdquo;,它给每个单词标了可能是什么词性,也标明了句子的结构。数据块更小 ,更容易处理,每次语音搜索,都有这样的流程,比如 \u0026ldquo;最近的披萨在哪里\u0026rdquo;,计算机能明白这是\u0026quot;哪里\u0026quot;(where)的问题,知道你想要名词\u0026quot;披萨\u0026quot;(pizza),而且你关心的维度是\u0026quot;最近的\u0026quot;(nearest)。\u0026ldquo;最大的长颈鹿是什么?\u0026ldquo;或\u0026quot;thriller 是谁唱的?\u0026rdquo; 也是这样处理。\n把语言像乐高一样拆分,方便计算机处理,计算机可以回答问题 ,以及处理命令,比如\u0026quot;设 2:20 的闹钟\u0026rdquo;,或\u0026quot;用 spotify 播放 t-swizzle\u0026quot;,但你可能体验过,如果句子复杂一点,计算机就没法理解了。\n嘿 siri ...... 俺觉得蒙古人走得太远了\r在这个最温柔的夏日的日子里,你觉得怎么样?\rsiri:我没明白 还有,\u0026ldquo;短语结构规则\u0026quot;和其他把语言结构化的方法,可以用来生成句子。数据存在语义信息网络时,这种方法特别有效,实体互相连在一起,提供构造句子的所有成分。siri:thriller 于 1983 年发行,由迈克尔杰克逊演唱,google 版的叫\u0026quot;知识图谱\u0026rdquo;。\n在 2016 年底,包含大概七百亿个事实,以及不同实体间的关系,处理, 分析, 生成文字 ,是聊天机器人的最基本部件 - 聊天机器人就是能和你聊天的程序。早期聊天机器人大多用的是规则,专家把用户可能会说的话,和机器人应该回复什么,写成上百个规则,显然,这很难维护,而且对话不能太复杂。\n一个著名早期例子叫 eliza ,1960 年代中期 诞生于麻省理工学院,一个治疗师聊天机器人。它用基本句法规则 来理解用户打的文字,然后向用户提问,有时候会感觉像和人类沟通一样,但有时会犯简单甚至很搞笑的错误。\n聊天机器人和对话系统,在过去五十年发展了很多,如今可以和真人很像!如今大多用机器学习,用上 gb 的真人聊天数据 来训练机器人,现在聊天机器人已经用于客服回答,客服有很多对话可以参考,人们也让聊天机器人互相聊天。在 facebook 的一个实验里,聊天机器人甚至发展出自己的语言,很多新闻把这个实验 报导的很吓人,但实际上只是计算机 在制定简单协议来帮助沟通,这些语言不是邪恶的,而是为了效率。\n但如果听到一个句子 - 计算机怎么从声音中提取词汇?这个领域叫\u0026quot;语音识别\u0026quot;。这个领域已经重点研究了几十年,贝尔实验室在 1952 年推出了第一个语音识别系统,绰号 audrey,自动数字识别器。如果你说得够慢,它可以识别全部十位数字,这个项目没有实际应用,因为手输快得多。十年后,1962 年的世界博览会上,ibm 展示了一个鞋盒大小的机器,能识别 16 个单词,为了推进\u0026quot;语音识别\u0026quot;领域的研究,darpa 在 1971 年启动了一项雄心勃勃的五年筹资计划,之后诞生了卡内基梅隆大学的 harpy。harpy 是第一个可以识别 1000 个单词以上的系统,但那时的电脑,语音转文字,经常比实时说话要慢十倍或以上。幸运的是,1980,1990 年代 计算机性能的大幅提升,实时语音识别变得可行,同时也出现了处理自然语言的新算法,不再是手工定规则,而是用机器学习,从语言数据库中学习。\n如今准确度最高的语音识别系统 用深度神经网络,我们在第 34 集讲过。为了理解原理,我们来看一些对话声音,先看元音,比如 a 和 e ,这是两个声音的波形,我们在第 21 集(文件格式)说过。\n这个信号来自 麦克风内部隔膜震动的频率,在这个视图中,横轴是时间,竖轴是隔膜移动的幅度,或者说振幅,虽然可以看到 2 个波形有区别,但不能看出,\u0026ldquo;啊!这个声音肯定是 e\u0026rdquo;。为了更容易识别,我们换个方式看:\n谱图\n这里横轴还是时间,但竖轴不是振幅,而是不同频率的振幅,颜色越亮,那个频率的声音越大,这种波形到频率的转换 是用一种很酷的算法做的 - 快速傅立叶变换(fft)。如果你盯过立体声系统的 eq 可视化器,它们差不多是一回事,谱图是随着时间变化的,你可能注意到,信号有种螺纹图案,那是我声道的回声。为了发出不同声音,我要把声带,嘴巴和舌头变成不同形状,放大或减少不同的共振,可以看到有些区域更亮,有些更暗。如果从底向上看,标出高峰 - 叫\u0026quot;共振峰\u0026quot; - 可以看到有很大不同,所有元音都是如此,这让计算机可以识别元音,然后识别出整个词。\n让我们看一个更复杂的例子,当我说\u0026quot;她很开心\u0026quot;的时候,可以看到 e 声,和 a 声,以及其它不同声音,比如 she 中的 shh 声,was 中的 wah 和 sss,等等,这些构成单词的声音片段叫\u0026quot;音素\u0026quot;。语音识别软件 知道这些音素,英语有大概 44 种音素,所以本质上变成了音素识别,还要把不同的词分开,弄清句子的开始和结束点,最后把语音转成文字,使这集视频开头里讨论的那些技术成为可能。因为口音和发音错误等原因,人们说单词的方式略有不同,所以结合语言模型后,语音转文字的准确度会大大提高。里面有单词顺序的统计信息,比如:\u0026ldquo;她\u0026quot;后面很可能跟一个形容词,比如\u0026quot;很开心\u0026rdquo;,\u0026ldquo;她\u0026quot;后面很少是名词,如果不确定是 happy 还是 harpy,会选 happy,因为语言模型认为可能性更高。\n最后,我们来谈谈 \u0026ldquo;语音合成\u0026rdquo;,让计算机输出语音,它很像语音识别,不过反过来。把一段文字,分解成多个声音,然后播放这些声音。早期语音合成技术,可以清楚听到音素是拼在一起的,比如这个 1937 年贝尔实验室的手动操作机器,不带感情的说 \u0026ldquo;她看见了我\u0026rdquo;。\n到了 1980 年代,技术改进了很多,但音素混合依然不够好,产生明显的机器人声。如今,电脑合成的声音,比如 siri, cortana, alexa 好了很多,但还不够像人,但我们非常非常接近了,这个问题很快会被解决。\n现在语音界面到处都是,手机里,汽车里,家里,也许不久之后耳机也会有。这创造一个正反馈循环,人们用语音交互的频率会提高,这又给了谷歌,亚马逊,微软等公司更多数据来训练语音系统,提高准确性。准确度高了,人们更愿意用语音交互,越用越好,越好越用。很多人预测,语音交互会越来越常见,就像如今的屏幕,键盘,触控板等设备,这对机器人发展是个好消息。\n机器人就不用走来走去时带个键盘和人类沟通。\n下周我们讲机器人。到时见。\n37. 机器人 i.e. robots\n嗨,我是 carrie anne,欢迎收看计算机速成课。\n今天 我们要讨论机器人,你脑中冒出来的第一个印象估计是 类人机器人,经常在电视剧和电影里看到。有时候它们是朋友和同事,但更常见的是阴险无情,身经百战。我们经常把机器人看成未来科技,但事实是:机器人时代已经来临了 - 它们是同事,帮我们把困难的工作,做得更快更好。机器人的定义有很多种,但总的来说,机器人由计算机控制,可以自动执行一系列动作的机器,外观并不重要,可以是给汽车喷漆的机械臂,无人机,或辅助外科医生的蛇状机器人,以及人形机器人。\n有时我们叫虚拟人物\u0026quot;机器人\u0026rdquo;,但叫 bot 甚至 agent 会更合适,因为\u0026quot;机器人\u0026quot;的潜在含义是存在于现实世界中的机器。robot (机器人) 一词 ,首次出现在 1920 年的一部捷克戏剧,代表人造的类人角色。robot 源于斯拉夫语词汇 robota 代表强迫劳动,代表农民在十九世纪 欧洲封建社会的强迫劳动,戏剧没讲太多技术细节。但即使一个世纪后,这种描述依然很普遍:机器人都是大规模生产,高效不知疲倦,看起来像人的东西,但毫无情感,不会保护自己,没有创造力。\n更广义的自动运行机器,早在 1920 年代前就有了。很多古代发明家 发明了能自动运行的机械装置,比如计时和定时敲钟。有很多装置 ,有动物和人类的形象 ,能跳舞,唱歌,打鼓等。这些不用电,而且肯定没有电子部件的机器,叫\u0026quot;自动机\u0026quot;。\n举个例子 1739 年法国人 jacques de vaucans 做了个自动机,法语叫 canard digerateur,翻译过来是 \u0026ldquo;吃饭鸭\u0026rdquo;,一个像鸭子的机器,能吃东西然后排便。伏尔泰在 1739 年写,\u0026ldquo;如果没有吃饭鸭的声音,还有什么能提醒你法国的荣光呢?\u0026rdquo;\n一个名声很臭的例子是\u0026quot;土耳其行棋傀儡\u0026quot;,一个能下国际象棋的人形机器人,在 1770 年建造完成后,就在欧洲各地展览。好棋艺惊叹观众,像某种机械人工智能,不幸的是,这是个骗局 - 机器里有人控制。\n第一台计算机控制的机器,出现在 1940 年代晚期,这些计算机数控的机器,简称 cnc (computer numerical control)机器,可以执行一连串 程序指定的操作,精细的控制 ,让我们能生产之前很难做的物品,比如从一整块铝 加工出复杂的螺旋桨 - 这用普通机械工具很难做到,并且误差容忍度很小,无法手工加工。cnc 机器大大推进了制造业,不仅提高了制造能力和精确度 ,还降低了生产成本 - 我们之后会深入讨论这个(第 40 集)。\n第一个商业贩卖的 可编程工业机器人 叫 unimate,于 1960 年卖给通用汽车公司,它可以把压铸机做出来的热金属成品提起来,然后堆起来,机器人行业由此开始。很快,机器人开始堆叠货盘,焊接,给汽车喷漆等等。对于简单运动 - 比如机器爪子 在轨道上来回移动,可以指示它移动到特定位置,它会一直朝那个方向移动,直到到达 然后停下来,这种行为 可以用简单控制回路做。\n首先,判断机器人的位置,我们到了吗?\r没有\r那么继续前进\r再次判断位置\r我们到了吗?\r没有,所以继续前进\r我们到了吗?\r是的!\r现在可以停下来了,别问了! 因为我们在不断缩小 当前位置和目标位置的距离,这个控制回路 更准确的叫 \u0026ldquo;负反馈回路\u0026rdquo;。负反馈回路 有三个重要部分:\n首先是一个传感器,可以测量现实中的东西,比如水压,马达位置,气温,或任何你想控制的东西,根据传感器,计算和目标值相差多大,得到一个\u0026quot;错误\u0026quot;,然后\u0026quot;控制器\u0026quot;会处理这个\u0026quot;错误\u0026quot;,决定怎么减小错误,然后用泵,电机,加热元件,或其他物理组件来做出动作。在严格控制的环境中,这种简单控制回路也够用了,但在很多现实应用中,情况复杂得多,假设爪子很重,哪怕控制回路叫停了,惯性让爪子超过了预期位置,然后控制回路又开始运行,叫爪子移动回去。一个糟糕的控制回路 可能会让爪子不断来回移动,甚至永远循环。更糟糕的是,现实世界中,机器人会受到各种外力影响,比如摩擦力,风,等等。为了处理这些外力,我们需要更复杂的控制逻辑。\n一个使用广泛的机制,有控制回路和反馈机制,叫 \u0026ldquo;比例-积分-微分控制器\u0026rdquo;。这个有点绕口,所以一般简称 \u0026ldquo;pid 控制器\u0026rdquo;,它以前是机械设备,现在全是纯软件了。想象有一个机器人,端咖啡给客人,设计目标是 每秒两米的速度在顾客间穿行,这个速度是理想速度,安全又合适。当然,环境是会变化的,有时候有风,有时候有上坡下坡,以及其他影响机器人速度的因素,所以,给马达的动力要加大或减少,以保持目标速度。用机器人的速度传感器,我们可以把当前速度和目标速度画张图,pid 控制器根据这些数据,算出 3 个值,首先是 \u0026ldquo;比例值\u0026rdquo; ,就是\u0026quot;实际值\u0026quot;和\u0026quot;理想值\u0026quot;差多少,\u0026ldquo;实际值\u0026quot;可能有一定滞后,或者是实时的。之前的简单控制回路,用的就是这个值,\u0026ldquo;实际值\u0026quot;和\u0026quot;理想值\u0026quot;的差距越大,就越用力。换句话说,它是\u0026quot;比例控制\u0026quot;的。接下来,算 \u0026ldquo;积分值\u0026rdquo; ,就是一段时间内 误差的总和,比如最近几秒,帮助弥补误差,比如上坡时可能就会产生误差。如果这个值很大,说明比例控制不够,要继续用力前进。最后有 \u0026ldquo;导数值\u0026rdquo; ,是期望值与实际值之间的变化率,有助于解决 未来可能出现的错误, 有时也叫\u0026quot;预期控制\u0026rdquo;,比如前进的太快,要稍微放松一点,避免冲过头。\n这三个值会一起使用,它们有不同权重,然后用来控制系统。pid 控制器到处都是,比如汽车里的巡航控制,无人机调整螺旋桨速度,以保持水平,以及一些更奇怪的机器人,比如这个用球来平衡和移动的机器人。更高级的机器人一般需要多个控制回路同时运行,来保持机器人平衡,调整肢体位置,等等。\n之前说过,控制回路负责把机器人的属性(比如当前位置)变成期望值,你可能好奇这些值 是哪里来的,这是更高层软件的责任。软件负责做出计划 并让机器人执行动作,比如制定一条路线来绕过障碍物,或者把任务分成一步步,比如把拿起一个球,分解成一个个简单连续动作。用这些技术,机器人已经取得不少令人印象深刻的成就 - 它们潜到了海洋最深处,在火星上跑了十几年。\n但有趣的是,许多对人类来说很简单的任务,对机器人很困难: 比如两条腿走路,开门,拿东西时不要捏碎了,或是穿 t 恤,或是摸狗,这些你可能想都不用想,但有超级计算机能力的机器人却做不到。机器人研究领域在全力解决这些问题,我们前几集聊过的 人工智能,最有可能解决这些问题。例如,谷歌在进行一项实验,让一堆机器人手臂把各种东西,从一个盒子拿到另一个盒子,不断试错学习。经过数千小时的练习,机器人把错误率降低了一半。不像人类,机器人可以 24 小时全天运行,而且多个手臂同时练习,所以机器人擅长抓东西只是时间问题,但现在,小婴儿都比机器人更会抓东西。\n近年最大的突破之一,是无人驾驶汽车。如果你仔细想想,汽车没几个输入 - 只是加速减速,左转右转,难的问题是 判断车道,理解路标,预测车流,车流中穿行,留心行人和骑自行车的,以及各种障碍。车上布满了传感器,无人驾驶汽车非常依赖计算机视觉算法,我们在第 35 集讨论过。\n现在也开始出现类人机器人 - 外貌和行为像人类的机器人,不过现在两个目标都没接近(外貌和行为),因为看起来一般怪怪的,行为也怪怪的。但至少有《西部世界》可以看看。无论如何,对机器人研究者来说,把各种技术结合起来,比如人工智能,计算机视觉和自然语言处理,来让机器人越来越像人,是个诱人的目标。至于人类为什么如此着迷 做出和我们一样的机器人,你得去看《哲学速成课》。在未来好一段时间里,和人类一样的机器人 依然只能存在科幻小说里。军队也对机器人很有兴趣 - 因为机器人可以替换, 而且力量,耐力,注意力,准确性可以远超人类。拆弹机器人和无人侦察机如今很常见,但完全自主决定,全副武装的机器人也在慢慢出现,比如韩国的三星 sgr-a1 哨兵炮,有智力并且可以杀人的机器人,叫 \u0026ldquo;致命自主武器\u0026rdquo;,这种武器是复杂又棘手的问题。毫无疑问,它们可以把士兵从战场带离 挽救生命,甚至阻止战争的发生。值得注意的是 人们对炸药和核弹也说过一样的话。另一方面,我们可能会不小心创造出 无情又高效的杀人机器,没有人类般的判断力和同情心,战争的硝烟会变得更加黑暗和复杂。机器人会接受命令并高效执行,但有时人类的命令是错的。\n这场辩论会持续很长时间,而且随着机器人技术的进步,两边的辩论会越来越激烈,这也是个老话题了。\n科幻作家 艾萨克·阿西莫夫 早预见了这种危险,他在 1942 年短篇小说 runaround 中写了\u0026quot;机器人三定律\u0026rdquo;,之后又加了\u0026quot;定律 0\u0026quot;。简单说 这些定律指导机器人的行为准则 或者说道德指南,让机器人不要伤害,特别是不要伤害人类,这些规则实践起来相当不足,并且有很多模糊的地方,但阿西莫夫三定律 激发了大量科幻小说讨论和学术讨论, 如今有专门讨论机器人伦理的会议。重要的是,阿西莫夫写这些虚构规则,是为了反对 \u0026ldquo;机器人都很邪恶\u0026rdquo; 这种常见描述。他童年读的小说里,这样的场景很常见,机器人脱离控制,然后伤害甚至毁灭创造者。阿西莫夫认为 机器人有用,可靠,甚至可以让人喜爱。\n我想让你思考这种两面性。我们讨论过的许多技术,有好的一面也有坏的一面,我们要认真思考计算机的潜力和危害,来改善这个世界,而机器人最能提醒我们这一点了。\n我 们 下 周 见。\n38. 计算机心理学 i.e. psychology of computing\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n在这个系列中,我们聊的话题几乎全是计算机-比如电路和算法,毕竟这是计算机速成课,但归根结底,计算机只是给人用的工具,而人类有点。.. 乱。\n人类不是被工程师设计的,没有具体性能规格。我们一会儿是理性的,一会儿是不理性的。你有没有对导航生过气?或是漫无目的的刷维基百科?求浏览器加载快点?给扫地机器人取名?这些是人类行为!\n为了做出使用愉快的计算机,我们需要了解计算机和人类的优缺点。优秀的系统设计师在创造软件时,会运用社会心理学,认知心理学,行为心理学,感知心理学的原理。你肯定见过难用的物理界面/计算机界面 阻碍你做事,甚至糟糕到放弃使用,那个界面的\u0026quot;易用度\u0026quot;很差。\u0026ldquo;易用度\u0026quot;指的是人造物体,比如软件 达到目的的效率有多高。为了帮助人类工作,我们需要了解人类 - 怎么看,思考,反应和互动。\n举个例子,心理学家已经对 人类的视觉系统做了全面的研究,我们知道人类擅长给颜色强度排序,这里有三个颜色,你能从浅色到深色排序吗?你可以轻易做到,所以颜色强度很适合显示连续值。另一方面,人类很不擅长排序颜色,这是另一个例子,把橙色放到蓝色前面还是后面?绿色放哪里?你可能想通过光的波长排序 ,就像彩虹一样,但这样太累了。大部分人会很慢而且容易出错,由于视觉系统天生是这样,所以用不同颜色显示连续性数据,是个糟糕的选择,你得经常看表格来对比数据。 然而,如果数据没有顺序,用不同颜色就很合适,比如分类数据,也许这些看起来很明显 ,但你会惊讶有多少设计把这些基本事情搞错。\n除了视觉 ,理解人类的认知系统能帮我们设计更好的界面,比如,如果信息分块了 ,会更容易读,更容易记。分块是指把信息分成更小,更有意义的块,人类的短期记忆能记住 5 到 9 个东西,保守一点,分组一般是 5 个或更少,所以电话号码一般分块,比如 317-555-3897 。10 个连续数可能会忘,分成 3 块更好记。从计算机的角度来看,分块更费时费空间,效率更低,但这对人类更有效率 - 碰到这种抉择时,我们总是以人类优先。现在我们还是老大,暂时啦。\n界面设计用了分块, 比如下拉菜单 和带按钮的菜单栏,对电脑来说,全部挤在一起更有效率,分块浪费内存 浪费屏幕,但这样设计更容易扫视,记住和访问。界面设计中另一个重点概念是\u0026quot;直观功能\u0026rdquo;,don norman 让这个词在计算机界流行起来,根据他的说法,\u0026ldquo;直观功能 为如何操作物体提供线索,平板用来推,旋钮用来转,插槽用来插东西,[\u0026hellip;] 直观功能做的好,用户只需要看一眼就知道怎么做: 不需要图片,标签或指南来说明\u0026rdquo;。\n如果你拉过门把手打不开,然后意识到要推开才对,那么你发现了一个坏掉的\u0026quot;直观功能\u0026quot;。平板是更好的设计,因为只能推开,门是简单的东西,如果你要贴指示让人们明白怎么用,那么也许你应该重新设计。\n\u0026ldquo;直观功能\u0026quot;广泛用于图形界面,我们在第 26 集讨论过,这是图形界面比命令行更容易用的原因之一。你不用猜测屏幕上什么东西是可点的,可点的会看起来像按钮,他们弹出来,只是等着你压他们!\n我最喜欢的\u0026quot;直观功能\u0026quot;之一,是向用户表明元素是可拖动的,\u0026ldquo;滚花\u0026rdquo; - 一种视觉纹理,告诉用户哪里可以拖动,这个点子来自现实世界中的工具,和\u0026quot;直观功能\u0026quot;相关的一个心理学概念是 \u0026ldquo;认出与回想\u0026rdquo;。如果你考过试,肯定感受过这个,这就是为什么选择题比填空题容易。一般来说,用感觉来触发记忆会容易得多,比如文字,图片或声音,所以我们用图标代表功能 - 比如\u0026quot;垃圾桶\u0026quot;图标 代表里面放着被删除的文件。我们不用去回想图标的功能是什么,只要能认出来就行了,比命令行好得多,命令行得依靠记忆来输命令,到底是输入\u0026quot;删除\u0026quot;\u0026ldquo;移除\u0026quot;\u0026ldquo;垃圾\u0026quot;还是\u0026quot;射出\u0026rdquo;?可能是任何命令! 顺带一说,在 linux 里删除文件的命令是 \u0026ldquo;rm\u0026rdquo; 。\n回到正题,让所有菜单选项好找好记,有时候意味着用的时候会慢一些,这与另一个心理学概念冲突:\u0026ldquo;专业知识\u0026rdquo;。当你用界面熟悉之后,速度会更快一些,建立如何高效完成事情的\u0026quot;心理模型\u0026rdquo;,所以 好的界面应该提供多种方法来实现目标。一个好例子是复制粘贴,可以在\u0026quot;编辑\u0026quot;的下拉菜单中找到,也可以用快捷键,一种适合新手,一种适合专家,两者都不耽误,鱼和熊掌兼得!\n除了让人类做事更高效,我们也希望电脑能有一点情商,能根据用户的状态做出合适地反应,能根据用户的状态做出合适地反应,让使用电脑更加愉快。rosalind picard 在 1995 年关于\u0026quot;情感计算\u0026quot;的论文中,阐述了这一愿景,这篇论文开创了心理学,社会科学和计算机科学的跨学科结合,促进了让计算机理解人类情感的研究,这很重要,因为情绪会影响日常活动,比如学习,沟通和决策。情感系统会用传感器,录声音,录像(你的脸)以及生物指标,比如出汗和心率,得到的数据和计算模型结合使用。模型会写明人类如何表达情感,怎么是快乐 ,怎么是沮丧,以及社交状态,比如友谊和信任。模型会估算用户的情绪,以及怎样以最好的回应用户,以达到目标,比如让用户冷静下来,建立信任,或帮忙完成作业。\nfacebook 在 2012 年进行了一项\u0026quot;影响用户\u0026quot;的研究,数据科学家在一个星期内,修改了很多用户 时间线上显示的内容,有些人会看到更多积极向上的内容,有些人会看到更多负面消极的内容。研究人员分析了那一周内人们的发帖,发现看到积极向上内容的用户,发的帖子往往更正面。另一方面,看到负面内容的用户,发的内容也更负面。显然,facebook 和其他网站向你展示的内容,绝对会对你有影响。作为信息的守门人,这是巨大的机会 ,同时也是责任,研究结果相当有争议性。而且它还产生了一个有趣的问题: 计算机程序如何回应人类?\n如果用户的情绪比较负面,也许电脑不应该以一种烦人的 \u0026ldquo;你要振作起来呀\u0026rdquo; 的态度回答问题。或者,也许电脑应该试着积极正面的回应用户,即使这有点尴尬。什么行为是\u0026quot;正确的\u0026rdquo;,是个开放性的研究问题。\n既然说到 facebook,这是一个\u0026quot;以计算机为媒介沟通\u0026quot;的好例子,简称 \u0026ldquo;cmc\u0026rdquo;(computer-mediated communication),也是一个很大的研究领域。这包括同步通信 - 所有参与者同时在线进行视频通话,以及异步通信 - 比如推特,邮件,短信,人们可以随时随地回复信息。研究人员还研究用户怎么用表情包,怎么轮换发言,以及用不同沟通渠道时,用词有什么区别。\n一个有趣的发现是,比起面对面沟通,人们更愿意在网上透露自己的信息。所以如果想知道用户 真正花了多少小时看\u0026quot;大英烘培大赛\u0026quot;(电视节目),比起做个带脸的虚拟助理 做 聊天机器人 是个更好的选择。心理学研究也表明,如果想说服,讲课,或引起注意 眼神注视非常重要。在谈话时看着别人叫 相互凝视,这被证明可以促进参与感 帮助实现谈话目标,不管是学习,交朋友,还是谈生意。在录像讲座中,老师很少直视相机, 一般是看在场学生,对他们没问题,但这会让在线看视频的人没什么参与感,为此,研究人员开发了计算机视觉和图形软件 来纠正头部和眼睛,视频时会觉得对方在直视摄像头,看着他们,这叫\u0026quot;增强凝视\u0026quot;。类似技术也用于视频会议,纠正摄像头位置,因为摄像头几乎总在屏幕上方,因为你一般会盯着屏幕上的另一方 ,而不是盯着摄像头,所以视频里看起来像在向下看。没有相互凝视 - 这会导致各种不幸的副作用,比如权力不平衡,幸运的是,可以用软件修正,看起来像在凝视着对方的眼睛。人类也喜欢\u0026quot;拟人化\u0026quot;的物体,对计算机也不例外,特别是会动的计算机,比如上集说的机器人。\n在过去一个世纪,除了工业用途机器人,有越来越多机器人用于医疗,教育和娱乐 。它们经常和人类互动 - 人机交互,简称 hri(human-robot interaction) - 是一个研究人类和机器人交互的领域,比如人类如何感受 机器人的不同形式和不同行为,或是机器人如何明白人类暗示来社交,而不是尴尬的互动。\n正如上集说的,我们有追求。把机器人的外表和行为,做得尽可能像人一样。\n工程师在 1940 1950 年代刚开始做机器人时,看起来完全不像人,是完完全全的工业机器。随着时间的推移,工程师越来越擅长做类人机器人,它们有头,而且用两条腿走路,但它们做不到伪装成人类去餐馆点餐。随着机器人可以做得越来越像人类,用人造眼球代替摄像头,用人工肌肉盖住金属骨架,事情会开始变得有些奇怪,引起一种怪异不安的感觉,这个\u0026quot;几乎像人类\u0026quot;和\u0026quot;真的人类\u0026quot;之间的小曲线,叫 \u0026ldquo;恐怖谷\u0026rdquo;。\n对于机器人是否应该有人类一样的行为,也存在争议。很多证据表明,即使机器人的行为不像人类,人类也会用社交习俗对待它们,而当机器人违反习俗时 - 比如插队或踩了脚不道歉 人们会很生气!毫无疑问,心理学+计算机科学是强大的组合,可以影响日常生活的巨大潜力,这也带来了很多开放式问题,比如你可能会对计算机撒谎,但计算机应不应该对你撒谎?如果撒谎能让你更高效更快乐呢?或社交媒体公司 是否应该精心挑选展示给你的内容,让你在网站上多待一会儿,买更多东西?\n顺带一说,他们的确有这样做!!!\n这类道德问题不容易回答,但心理学至少可以帮助我们理解不同选择 带来的影响和意义。但从积极的方面来说,了解设计背后的心理学,能增加易用性,让更多人可以明白和使用电脑,如今计算机比以往更加直观,线上会议和虚拟教室的体验越来越好。随着机器人技术不断提高,互动也会越来越舒适。另外,感谢心理学,让我们能分享对\u0026quot;滚花\u0026quot;的热爱。\n我们下周见。\n39. 教育科技 i.e. educational technology\n(。・∀・)ノ゙嗨,我是 carrie anne ,欢迎收看计算机科学速成课!\\n\n计算机带来的最大改变之一 , 是信息的创造和传播能力。目前有 13 亿个网站在互联网上,仅维基百科就有 500 万篇英文文章,涵盖从\u0026quot;1518 年的舞蹈瘟疫\u0026quot;,到\u0026quot;正确的纸卷方向\u0026quot;。每天,google 提供 40 亿次搜索来访问这些信息,youtube 上每分钟有 350 万个视频被观看,每分钟用户上传 400 小时的新视频,很多观看量都是 gangnam style 和 despacito,但剩下的 大部分是教育型内容,就像你现在看的这个。\n如今只要手机上点几下 就能访问到这些宝藏,任何时间,任何地点,但能获取到信息和学习不是一回事。\n先说清楚,我们 crash course 喜欢互动式课堂学习,课上提问,以及上手实践,它们是很棒的学习途径,但我们也相信教育型技术在课内课外带来的帮助。今天我们要在这个教育型视频里 聊教育型科技,具体讲解计算机怎么帮助我们学习。从纸和笔 到用机器学习的智能系统,科技几千年来一直在辅助教育,甚至早期人类 在洞穴里画狩猎场景也是为了后代。远距离教育一直推动着教育科技的发展,例如公元 50 年左右,圣保罗就发书信 给亚洲设立的新教堂提供宗教课程,从那以后,有几大技术浪潮,自称要改变教育,从广播和电视,到 dvd 和光碟。事实上,在 1913 年 托马斯·爱迪生 预测说,\u0026ldquo;书籍很快会过时,用影片来教授所有知识是可能的,学校体系将在未来十年彻底改变\u0026rdquo;。当然,他的预测没有成真,但发布教育视频变得越来越流行。在讨论教育技术可以帮你做什么之前,有研究表明 - 有些简单事情 ,可以显著提高学习效率:\n把速度调整到适合你,youtube 的速度设置在右下角,让你能理解视频 有足够的时间思考; 暂停!在困难的部分暂停,问自己一些问题,看能不能回答,或想想视频接下来可能讲什么 然后继续播放,看猜对没有; 做视频中的提供的练习。 即使不是程序员,你也可以在纸上写伪代码,或试试学编程,这些主动学习的技巧已被证明 ,可以把学习效率提升 10 倍或以上。如果想学学习技巧,有整个系列专门讲这个。\n把高质量教育内容做成视频传播 ,在过去一个世纪吸引了很多人,这个老想法的新化身,以\u0026quot;大型开放式在线课程\u0026quot;(mooc,massive open online courses)的形式出现,纽约时报宣称 2012 年是 mooc 年!很多早期视频 直接录制著名教授上课,有段时间,有些人以为大学要终结了。不管你是担心还是开心,这暂时还没成为现实,现在热度也淡去了,这可能是因为加大规模时, 同时教百万名学生,但老师数量很少,甚至完全没有老师 - 会遇到很多问题。幸运的是,这引起了计算机科学家,或具体一点 \u0026ldquo;教育科技家\u0026quot;的兴趣,他们在想办法解决这些问题,比如,为了有效学习,学生要及时获得反馈。但如果有几百万学生,只有一名老师,怎么提供好的反馈?一个老师怎么给一百万份作业打成绩?为了解决问题,很多时候需要把科技和人类都用上,一种有用 但有些争议的做法是学生互相之间提供反馈。不幸的是,学生一般做不好,他们既不是专家也不是老师,但我们可以用技术来帮助他们,比如通过算法,从数百万个选择里 匹配出最完美的学习伙伴,另外,有些部分可以机器打分,剩下的让人类打分,例如,给 sat 写作部分打分的电脑算法,已被证实和人工打分一样准确,还有些算法提供个性化学习体验,类似于 netflix 的电影推荐 或 google 的个性化搜索结果,为了个性化推荐,软件需要了解用户知道什么,不知道什么,在正确的时间提供正确的资料。\n让用户练习没理解的难的部分,而不是给出用户已经学会的内容,这种系统一般用 ai 实现,泛称叫法是\u0026quot;智能辅导系统\u0026rdquo;。我们现在讲一个假想的辅导系统,假设学生在这个假想的辅导系统中,研究一个代数问题,正确的下一步是两边-7,我们可以用 \u0026ldquo;判断规则\u0026rdquo; 来表示这一步,用 if-then 语句来描述,伪代码是:\n*如果* 变量和常数在同一边\r*那么* 两侧都减去这个常数\r\u0026#34;判断规则\u0026#34; 酷的地方是也可以用来代表学生的常犯错误\r这些\u0026#34;判断规则\u0026#34;叫\u0026#34;错误规则\u0026#34;\r例如,学生可能不去减常数,而是去减系数\r这不行! 学生做完一个步骤后可能触发多个\u0026quot;判断规则\u0026quot;,系统不能完全弄清 是什么原因让学生选了那个答案,所以\u0026quot;判断规则\u0026quot;会和算法结合使用,判断可能原因,让学生得到有用反馈。\n\u0026ldquo;判断规则\u0026rdquo;+选择算法,组合在一起成为 \u0026ldquo;域模型\u0026rdquo;,它给知识,解决步骤和一门学科 ,比如代数,用一种\u0026quot;正式写法\u0026quot;来表示。域模型可以用来 帮助学习者解决特定问题,但它无法带着学习者 以正确顺序搞定整个学科该上的所有课程,因为域模型不记录进度,因此智能辅导系统 负责创建和维护学生模型 - 记录学生已经掌握的判断规则,以及还需练习的生疏部分,这正是个性化辅导系统需要的。\n听起来好像不难,但只靠学生对一些问题的回答,来弄清学生知道什么,不知道什么,是很大的挑战。\u0026ldquo;贝叶斯知识追踪\u0026rdquo; 常用来解决这个问题,这个算法把学生的知识 当成一组隐藏变量,这些变量的值,对外部是不可见的,比如我们的软件。\n这在现实中也是一样的,老师无法知道 学生是否完全掌握了某个知识点,老师会出考题,测试学生能否答对,同样,\u0026ldquo;贝叶斯知识追踪\u0026rdquo; 会看学生答题的正确度,更新学生掌握程度的估算值,它会记录四个概率:首先是 \u0026ldquo;学生已经学会的概率\u0026rdquo;,比如从代数方程的两边减去常数,假设学生正确将两边-7,做对了,我们可以假设她知道怎么做,但也有可能她是瞎蒙的,没有真的学会怎么解决问题,这叫 \u0026ldquo;瞎猜的概率\u0026rdquo;。类似的,如果学生答错了,你可能会假设她不会做,但她可能知道答案,只是不小心犯了个错,这叫 \u0026ldquo;失误的概率\u0026rdquo;。最后一个概率,是学生一开始不会做,但是在解决问题的过程中,学会了怎么做,这叫 \u0026ldquo;做题过程中学会的概率\u0026rdquo;。有一组方程,会用这四个概率,更新学生模型,对学生应该学会的每项技能进行持续评估。\n第一个等式问:学生已经知道某技能的概率是多少?等式里有 \u0026ldquo;之前已经学会的概率\u0026quot;和\u0026quot;做题过程中学会的概率\u0026rdquo;,就像老师一样,\u0026ldquo;之前已经学会的概率\u0026rdquo;,取决于学生回答问题正确与否,回答正确和错误分别有 2 个公式,算出结果之后,我们把结果放到第一个方程,更新\u0026quot;之前已经学会的概率\u0026quot;,然后存到学生模型里。\n虽然存在其他方法,但\u0026quot;智能辅导系统\u0026quot;通常用 贝叶斯知识追踪,让学生练习技能,直到掌握。为了高效做到这点,软件要选择合适的问题呈现给学生,让学生学,这叫:自适应式程序,个性化算法的形式之一,但我们的例子只是一个学生的数据。\n现在有 app 或网站,让教师和研究人员 收集上百万学习者的数据,从数据中可以发现常见错误一般哪里难倒学生,除了学生的回答,还可以看回答前暂停了多久,哪个部分加速视频,以及学生如何在论坛和其他人互动,这个领域叫 \u0026ldquo;教育数据挖掘\u0026rdquo;,它能用上学生所有的\u0026quot;捂脸\u0026quot;和\u0026quot;啊哈\u0026quot;时刻,帮助改善未来的个性化学习。\n谈到未来,教育技术人员经常从科幻小说中获得灵感,具体来说,neal stephenson 的\u0026quot;钻石时代\u0026quot;这本书激励了很多研究人员,里面说一个年轻女孩从书中学习,书中有一些虚拟助手会和她互动,教她知识,这些助手和她一起成长,直到她学会了什么,以及感觉如何,给她正确的反馈和支持,帮助她学习。如今 有非科幻小说研究者,比如 贾斯汀卡塞尔,在制作虚拟教学助手,助手可以\u0026quot;像人类一样沟通, 有人类一样的行为,在陪伴过程中和学习者建立信任,相处融洽,甚至和人类学生成为朋友\u0026quot;。\n2040 年的\u0026quot;速成课\u0026quot; 可能会有一个 john green ai,活在你的 iphone 30 上,教育科技和设备如今在逐渐扩展到笔记本和台式电脑之外,比如巨大桌面设备,让学生可以团队合作,以及小型移动设备,让学生路上也能学习。\n\u0026ldquo;虚拟现实\u0026quot;和\u0026quot;增强现实\u0026quot;也让人们兴奋不已,它们可以为学习者提供全新的体验 - 深潜海洋,探索太空,漫游人体,或是和现实中难以遇见的生物互动。如果猜想遥远的未来,教育可能会完全消失,直接在大脑层面进行,把新技能直接下载到大脑,这看起来可能很遥远,但科学家们已经在摸索 - 比如,仅仅通过检测大脑信号,得知某人是否知道什么。\n这带来了一个有趣的问题:如果我们可以把东西下载到大脑里,我们能不能上传大脑里的东西?\n下周的最后一集,我们会讨论计算的未来。\n到时见。\n40. 奇点,天网,计算机的未来 i.e. the singularity, skynet, and the future of computing\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n我们到了 最后一集!\n如果你看了整个系列,希望你对计算机影响的深度和广度 有全新的认知和欣赏。难以相信 我们从简单的晶体管和逻辑门开始,一直到计算机视觉,机器学习,机器人以及更多。\n我们站在巨人的肩膀上 - charles 、babbage 、ada 、lovelac 、herman 、hollerith 、alan turing 、j. presper eckert、 grace hopper 、 ivan sutherland douglas 、engelbart 、vannevar 、bush (memex) 、berners-lee (万维网) 、bill gates (微软)、steve wozniak (苹果),和许多其他先驱。\n我最大的希望是 这些视频能激励你 去了解这些东西如何影响你的人生,甚至开始学编程,或找一份计算机职业,这很棒!这是未来的技能!\n我在第一集说过,计算机科学不是魔法但它有点像魔法,学习使用电脑和编程,是 21 世纪的巫术,只不过用的不是咒语 ,而是代码。懂得运用的人,能创造出伟大的东西,不仅改善自己的生活,还有当地社区乃至整体人类。计算机会随处可见 - 不仅是放在桌上 ,带在包里,而是在所有可想象的东西里 - 厨房用具里,墙里,食物里,编织进衣服里,在你的血液里,这是\u0026quot;普适计算\u0026quot;的愿景。\n从某种角度来讲, 它已经来临了而换一个角度 , 还要几十年。有些人把这种未来看成 反乌托邦,到处都有监视器,有无数东西想吸引我们的注意力,但 1990 年代提出这个想法的 马克·维泽尔,看到了非常不同的潜力:\u0026quot;[五十] 年来,大多数界面和计算机设计,都是朝\u0026quot;戏剧性\u0026quot;方向前进,想把计算机做得超好,让人一刻也不想离开。另一条少有人走的路 是\u0026quot;无形\u0026quot;的,把计算机整合到所有东西里 ,用的时候很自然完全注意不到。最厉害的科技是看不见的科技,它们融入到日常生活的每一部分 ,直到无法区分\u0026rdquo;。\n如今我们还没达到这样 - 人们在电脑前连续坐好几小时,吃晚餐被手机推送通知打扰,但它可以描述计算的未来本系列最后一个主题,人们思考计算机的未来时,经常会直接想到人工智能。毫无疑问,接下来几十年人工智能会有巨大进步,但不是所有东西都要做成 ai ,或需要 ai,车有自动驾驶 ai,但门锁依然会很简单。人工智能可能只是增强现有设备,比如汽车,ai 带来了一个全新的产品种类,刚出现电力时也是这样,灯泡取代了蜡烛。但电气化也导致上百种新的电动小工具诞生。当然 我们如今仍然有蜡烛。最可能的情况是 ai 变成 计算机科学家手中的另一门新工具,但真正让人深思和担忧的是,人工智能是否会超越人类智能?\n这个问题很难 有多方面原因,比如 \u0026ldquo;智能的准确定义是什么?\u0026rdquo; 一方面,有会开车的计算机,几秒就能识别歌的 app 。翻译几十种语言,还称霸了一些游戏,比如象棋,知识竞答和围棋,听起来很聪明!但另一方面,计算机连一些简单事情都做不了,比如走楼梯,叠衣服,在鸡尾酒派对和人聊天,喂饱自己。人工智能成长到和人类一样通用,还有很长的路,因为\u0026quot;智能\u0026quot;是难以量化的指标,人们更喜欢用处理能力来区分,但这种衡量智能的方法比较\u0026quot;以计算为中心\u0026quot;,但如果把视频中出现过的电脑和处理器 画张图,可以看到 如今的计算能力粗略等同于一只老鼠。公平点说,老鼠也不会叠衣服,但如果真的会叠 ,就太可爱了。\n人类的计算能力在这儿,多 10 的 5 次方,也就是比如今电脑强 10 万倍。听起来差距很大,但按如今的发展速度,也许十几年就可以赶上了。虽然现在处理器的速度不再按摩尔定律增长了,我们在第 17 集讨论过,假设趋势继续保持下去,在本世纪结束前,计算机的处理能力/智能 会比全人类加起来还多。然后人的参与会越来越少,人工超级智能会开始改造自己,智能科技的失控性发展叫 \u0026ldquo;奇点\u0026rdquo;。\n第 10 集 约翰·冯·诺伊曼 最早用这个词,他说:\u0026ldquo;越来越快的技术发展速度和人类生活方式的改变,看起来会接近人类历史中某些重要的奇点,这个势头不会永远继续下去\u0026rdquo;。冯诺依曼在 1950 年代说的这话,那时计算机比现在慢得多,六十年后的今天,奇点仍然在遥远的地平线上。一些专家认为 发展趋势会更平缓一些,更像是 s 型,而不是指数型,而随着复杂度增加,进步会越来越难。微软联合创始人 保罗·艾伦 叫这个\u0026quot;复杂度刹车\u0026quot;,但当作思维练习,我们假设 超智能计算机会出现。这对人类意味着什么,是个讨论激烈的话题,有些人迫不及待,有些人则努力阻止它,最直接的影响可能是\u0026quot;技术性失业\u0026quot;。很多工作被计算机,比如 ai 和机器人,给代替掉了,它们的效率更高,成本更低。虽然计算机出现没多久,但\u0026quot;技术性失业\u0026quot;不是新事,还记得第 10 集里 雅卡尔的织布机 吗?它让 1800 年代的纺织工人失业,导致了骚乱,当时美国和欧洲 大部分人都是农民,如今农民占人口比例 \u0026lt;5%,因为有合成肥料和拖拉机等等技术。时间更近一些的例子是\u0026quot;电话接线员\u0026quot;,在 1960 年被自动接线板代替了,还有 1980 年代的\u0026quot;机器喷漆臂\u0026quot;替代了人工喷漆,这样的例子还有很多。一方面,因为自动化失去了工作,另一方面,我们有大量产品,衣服,食物,自行车,玩具等,因为可以廉价生产,但专家认为人工智能,机器人 以及更广义的计算,比之前更有破坏性。\n工作可以用两个维度概括,首先,手工型工作,比如组装玩具,或思维型工作 - 比如选股票,还有重复性工作,一遍遍做相同的事,或非重复性,需要创造性的解决问题。我们知道 重复性手工工作,可以让机器自动化,现在有些已经替代了,剩下的在逐渐替代。让人担心的是\u0026quot;非重复性手工型工作\u0026quot;,比如厨师,服务员,保安。思维型工作也一样,比如客服,收银员,银行柜员和办公室助理。剩下一个暂时比较安全的象限,非重复性思维型工作,包括教师和艺术家,小说家和律师,医生和科学家。这类工作占美国劳动力大概 40% ,意味着剩下 60%工作容易受自动化影响。有人认为这种规模的技术失业是前所未有的,会导致灾难性的后果,大部分人会失业,其他人则认为很好,让人们从无聊工作解脱,去做更好的工作,同时享受更高生活水平,有更多食物和物品,都是计算机和机器人生产的。\n没人知道未来到底会怎样,但如果历史有指导意义,长远看 一切会归于平静。毕竟,现在没人嚷嚷着让 90%的人 回归耕田和纺织,政界在讨论的棘手问题是怎么处理数百万人突然失业 造成的短期经济混乱。\n除了工作,计算机很可能会改变我们的身体,举个例子,未来学家 ray kurzweil 认为,\u0026ldquo;奇点会让我们超越 肉体和大脑的局限性,我们能掌控自己的命运,可以想活多久活多久 。 我们能完全理解并扩展大脑思维,超人类主义者认为会出现\u0026quot;改造人\u0026rdquo;,人类和科技融合在一起,增强智力和身体。如今已经有脑电接口了,而 google glass 和 微软 hololens 这样的穿戴式计算机 也在模糊这条界线,也有人预见到\u0026quot;数字永生\u0026quot;。jaron lanier 的说法是,\u0026ldquo;人类的肉体死去,意识上传到计算机\u0026rdquo;,从生物体变成数字体 可能是下一次进化跨越。\n一层新的抽象!\n其他人则预测,人类大体会保持原样,但超智能电脑会照顾我们,帮我们管农场,治病,指挥机器人收垃圾,建房子以及很多其他事情,让我们在这个可爱蓝点上(地球)好好享受。另一些人对 ai 持怀疑态度 - 为什么超级人工智能 会费时间照顾我们?人类不也没照顾蚂蚁吗?也许会像许多科幻电影一样,和计算机开战。我们无法知道未来到底会怎样,但现在已经有相关讨论了,这非常好。所以等这些技术出现后,我们可以更好地计划,不论你把计算机视为未来的朋友或敌人,更有可能的是,它们的存在时间会超过人类。许多未来学家和科幻作家猜测,机器人会去太空殖民,无视时间,辐射 以及一些其他让人类难以长时间太空旅行的因素。亿万年后太阳燃尽 ,地球成为星尘 ,也许我们的机器人孩子会继续努力探索宇宙每一个角落,以纪念它们的父母,同时让宇宙变得更好,大胆探索无人深空。\n与此同时,计算机还有很长的路要走,计算机科学家们在努力推进过去 40 集谈到的话题。在接下来的十几年,vr 和 ar,无人驾驶车,无人机,可穿戴计算机,和服务型机器人 会变得主流。互联网会继续诞生新服务,在线看新媒体,用新方式连接人们,会出现新的编程语言和范例,帮助创造令人惊叹的新软件,而新硬件能让复杂运算快如闪电 ,比如神经网络和 3d 图形。个人电脑也会创新,不像过去 40 年着重宣传 \u0026ldquo;桌面\u0026rdquo; 电脑,而是变成无处不在的虚拟助手。\n这个系列,我们还有很多话题没谈,比如加密货币,无线通讯,3d 打印,生物信息学和量子计算,我们正处于计算机的黄金时代,有很多事情在发生,全部总结是不可能的,但最重要的是 你可以学习计算机 成为这个惊人转型的一部分,把世界变得更好!\n感谢收看!\n结语 真的好长,也很有趣。知其先后,以预未来。\n","date":"2022-08-29","permalink":"https://loveminimal.github.io/posts/cscc/16/","summary":"\u003cblockquote\u003e\n\u003cp\u003e好吧,内容不少,为了后续插入图片之后 ,页面太大,我们这里拆分到两个页面中。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc16 - 其他 2"},{"content":" 后续章节为概念性章节,统一汇总在该章节内。\n15. 阿兰·图灵 i.e. alan turing\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n前几集我们聊了基础,比如函数,算法和数据结构,今天,我们来看一位对计算机理论 贡献巨大的人,计算机科学之父,长得不怎么像本尼的阿兰·图灵。\n\u0026gt; 阿兰·图灵\n阿兰·马蒂森·图灵 于 1921 年出生在伦敦, 从小就表现出惊人数学和科学能力。他对计算机科学的建树始于 1935 年,当时他是剑桥国王学院的硕士生,他开始解决德国数学家大卫·希尔伯特提出的问题 - 叫 entscheidungsproblem (德语),即\u0026quot;可判定性问题\u0026quot;: 是否存在一种算法,输入正式逻辑语句,输出准确的\u0026quot;是\u0026quot;或\u0026quot;否\u0026quot;答案?\n如果这样的算法存在,可以回答比如 \u0026ldquo;是否有一个数大于所有数\u0026rdquo;?不,没有。我们知道答案,但有很多其他数学问题,我们想知道答案,所以如果这种算法存在,我们想知道。\n美国数学家阿隆佐·丘奇于 1935 年 首先提出解决方法,开发了一个叫\u0026quot;lambda 算子\u0026quot;的数学表达系统,证明了这样的算法不存在。虽然\u0026quot;lambda 算子\u0026quot;能表示任何计算,但它使用的数学技巧难以理解和使用。同时在大西洋另一边,阿兰·图灵 想出了自己的办法来解决\u0026quot;可判定性问题\u0026quot;,提出了一种假想的计算机,现在叫\u0026quot;图灵机\u0026quot;。\n图灵机提供了简单又强大的数学计算模型,虽然用的数学不一样,但图灵机的计算能力和 lambda 算子一样,同时因为图灵机更简单,所以在新兴的计算机领域更受欢迎。因为它如此简单,我现在就给你解释。\n图灵机是一台理论计算设备,还有一个状态变量,保存当前状态,还有一组规则,描述机器做什么,规则是根据当前状态+读写头看到的符号,决定机器做什么,结果可能是在纸带写入一个符号,或改变状态,或把读写头移动一格,或执行这些动作的组合。\n为了更好理解,讲个简单例子:让图灵机读一个以零结尾的字符串,并计算 1 的出现次数是不是偶数。如果是,在纸带上写一个 1 ;如果不是,在纸带上写一个 0 。\n首先要定义\u0026quot;图灵机\u0026quot;的规则,如果当前状态是\u0026quot;偶数\u0026quot;, 当前符号是 1 ,那么把状态更新为\u0026quot;奇数\u0026quot;,把读写头向右移动;如果当前状态为偶数,当前符号是 0 ,意味着到了字符串结尾 。那么在纸带上写一个 1,并且把状态改成停机 (halt),状态改为\u0026quot;停机\u0026quot; 是因为图灵机已完成计算。但我们还需要 2 条规则,来处理状态为奇数的情况,一条处理奇数+纸带是 0 的情况, 一条处理奇数+纸带是 1 的情况,最后,要决定机器的初始状态,这里定成\u0026quot;偶数\u0026quot;。\n初始状态:偶\r偶 + 1 - 状态更新为奇数,读写头向右移动一格\n偶 + 0 - 状态更新为停机,在纸带上写一个 1\n奇 + 1 - 状态更新为偶数,读写头向右移动一格\n奇 + 0 - 在纸带上写一个 0\n💡 真的是需要天赋的……\n定义好了起始状态+规则,就像写好了程序,现在可以输入了。\n假设把 110 放在纸带上,有两个 1,是偶数,注意,规则只让读写头向右移动,其他部分无关紧要,为了简单所以留空。\n\u0026ldquo;图灵机\u0026quot;准备好了,开始吧。\n机器起始状态为\u0026quot;偶数\u0026rdquo;,看到的第一个数是 1,符合最上面那条规则,所以执行对应的步骤 - 把状态更新到\u0026quot;奇数\u0026quot;, 读写头向右移动一格;然后又看到 1, 但机器状态是\u0026quot;奇数\u0026quot;,所以执行第三条规则,使机器状态变回\u0026quot;偶数\u0026quot;,读写头向右移动一格;现在看到 0,并且机器状态是偶数,所以执行第二条规则,在纸带上写 1,表示\u0026quot;真\u0026quot; 的确有偶数个 1,然后机器停机。\n这就是图灵机的原理,很简单对吧?你可能想知道有什么大不了的。\n图灵证明了这个简单假想机器。如果有足够时间和内存,可以执行任何计算。它是一台通用计算机!刚才的程序就是个简单例子,只要有足够的规则,状态和纸带,可以创造任何东西 - 浏览器,魔兽世界 任何东西!当然 这样做效率很低,但理论上可行,所以图灵机是很强大的计算模型。\n事实上,就可计算和不可计算而言,没有计算机比图灵机更强大。和图灵机一样强大的,叫 \u0026ldquo;图灵完备\u0026rdquo;。每个现代计算系统,比如笔记本电脑,智能手机,甚至微波炉和恒温器内部的小电脑,都是\u0026quot;图灵完备\u0026quot;的。\n为了回答可判定性问题,他把图灵机用于一个有趣计算问题:\u0026ldquo;停机问题\u0026rdquo;。简单说就是,\u0026ldquo;给定图灵机描述和输入纸带,是否有算法可以确定机器会永远算下去还是到某一点会停机?\u0026rdquo; 我们知道输入 110,图灵机会停机,因为刚做过这个例子,它最后停机了,但如果是更复杂的问题呢?有没有办法在不执行的情况,弄清会不会停机?一些程序可能要运行好几年,所以在运行前知道 会不会出结果很有用,否则就要一直等啊等,忧虑到底会不会出结果,当几十年后变老了,再按强制结束。好悲伤!\n图灵通过一个巧妙逻辑矛盾证明了停机问题是无法解决的。\n我们来看看他的推理,想象有一个假想图灵机, 输入:问题的描述 + 纸带的数据,输出 yes 代表会\u0026quot;停机\u0026quot;,输出 no 代表不会。我要给这台机器一个有趣的名字叫 h,来自\u0026quot;停机\u0026quot;的第一个字母,不用担心它具体怎么工作,假设这样的机器存在就好,毕竟重点是推论。图灵推理说: 如果有个程序, h 无法判断是否会\u0026quot;停机\u0026quot;,意味着\u0026quot;停机问题\u0026quot;无法解决。为了找到这样的程序,图灵用 h 设计了另一个图灵机。如果 h 说程序会\u0026quot;停机\u0026quot;,那么新机器会永远运行(即不会停机)。如果 h 的结果为 no,代表不会停机,那么让新机器输出 no,然后\u0026quot;停机\u0026quot;。实质上是一台和 h 输出相反的机器,如果程序不停机,就停机,如果程序停机,就永远运行下去。我们还需要在机器前面加一个分离器,让机器只接收一个输入, 这个输入既是程序,也是输入,我们把这台新机器叫异魔😈。目前为止,这个机器不难理解,但接下来马上会变复杂,会有点难懂。如果把异魔的描述,作为本身的输入会怎样?意味着在问 h ,当异魔的输入是自己时会怎样,但如果 h 说异魔会停机,那么异魔会进入无限循环,因此不会停机,如果 h 说异魔不会停机,那么异魔会输出 no 然后停机,所以 h 不能正确判定停机问题,因为没有答案。这是一个悖论!意味着\u0026quot;停机问题\u0026quot;不能用图灵机解决!\n还记得刚刚说: 图灵证明了图灵机可以实现任何计算。\u0026ldquo;停机问题\u0026quot;证明了,不是所有问题都能用计算解决。哇,好难理解!我都可能要再看一遍。\n长话短说,丘奇和图灵证明了计算机的能力有极限。无论有多少时间或内存,有些问题是计算机无法解决的。丘奇和图灵证明了计算是有极限的,起步了可计算性理论,现在叫\u0026quot;丘奇-图灵论题\u0026rdquo;。\n当时是 1936 年,图灵只有 24 岁,他的职业生涯才刚刚开始。从 1936 年到 1938 年 在丘奇指导下,他在普林斯顿拿到博士学位,毕业后回到剑桥。1939 年后不久,英国卷入第二次世界大战,图灵的才能很快被投入战争。事实上,在战争开始前一年,他已经在英国政府的密码破译学校兼职 - 位于\u0026quot;布莱切利园\u0026quot;的一个密码破译组织。他的工作内容之一是破解德国的通信加密,特别是\u0026quot;英格玛机\u0026quot;加密的信息,简单说,英格玛机会加密明文,如果输入字母 h-e-l-l-o ,机器输出 x-w-d-b-j ,这个过程叫\u0026quot;加密\u0026quot;,文字不是随便打乱的。加密由\u0026quot;英格玛机\u0026quot;顶部的齿轮组合决定,每个齿轮有 26 个可能位置,机器前面还有插板,可以将两个字母互换,总共有上十亿种可能,如果你有\u0026quot;英格玛机\u0026quot;,并且知道正确的齿轮和插头设置,输入 x-w-d-b-j,机器会输出 hello ,解密了这条消息。\n当然,德军不会把机器设置发到微博上,盟军必须自己破译密码,有数十亿种组合,根本没法手工尝试所有组合。幸运的是,英格玛机和操作员不是完美的,一个大缺陷是:字母加密后绝不会是自己,h 加密后绝对不是 h 。图灵接着之前波兰破译专家的成果继续工作,设计了一个机电计算机,叫 bombe,利用了这个缺陷,它对加密消息尝试多种组合。如果发现字母解密后和原先一样,我们知道英格玛机决不会这么做,这个组合会被跳过,接着试另一个组合。bombe 大幅减少了搜索量,让破译人员把精力花在更有可能的组合,比如在解码文本中找常见的德语单词。德国人时不时会怀疑有人在破解,然后升级英格玛机,比如加一个齿轮,创造更多可能组合,他们甚至还做了全新的加密机。整个战争期间,图灵和同事在布莱切利园努力破解加密,解密得到的德国情报,为盟军赢得了很多优势,些史学家认为他们把战争减短了好几年。战后,图灵回到学术界,为许多早期计算机工作做出贡献,比如曼彻斯特 1 号,一个早期有影响力的存储程序计算机。但他最有名的战后贡献是\u0026quot;人工智能\u0026quot;,这个领域很新,直到 1956 年才有名字,这个话题很大,以后再谈(第 34 集)。\n1950 年,图灵设想了未来的计算机,拥有和人类一样的智力,或至少难以区分。图灵提出,如果计算机能欺骗人类相信它是人类,才算是智能,这成了智能测试的基础,如今叫\u0026quot;图灵测试\u0026quot;。想像你在和两个人沟通,不用嘴或面对面,而是来回发消息,可以问任何问题,然后会收到回答,但其中一个是计算机,如果你分不出哪个是人类,哪个是计算机,那么计算机就通过了图灵测试。这个测试的现代版叫 \u0026ldquo;公开全自动图灵测试,用于区分计算机和人类\u0026rdquo;,简称\u0026quot;验证码\u0026quot;。防止机器人发垃圾信息等,我承认 有时我都认不出那些扭曲的东西是什么字,这难道意味着我是计算机?🤔\n通常这个系列我们不会深入历史人物的个人生活,但图灵与悲剧密不可分,所以他的故事值得一提。图灵那个时代,同性恋是违法的,英国和大部分国家都是。1952 年调查他家的入室盗窃案时,向当局暴露了他的性取向,被起诉 \u0026ldquo;行为严重不检点\u0026rdquo;,图灵被定罪,有 2 个选择:1. 入狱; 2. 接受激素来压制性欲。他选了后者,部分原因是为了继续学术工作,但药物改变了他的情绪和性格,虽然确切情况永远无法得知。图灵于 1954 年服毒自尽,年仅 41 岁。\n图灵真的是惨……看看现在西方,lgbt 都上天了快……\r由于图灵对计算机科学贡献巨大,许多东西以他命名,其中最出名的是\u0026quot;图灵奖\u0026quot; - 计算机领域的最高奖项。相当于物理,化学等其它领域的诺贝尔奖。虽然英年早逝,但图灵激励了第一代计算机科学家,而且为如今便利的数字时代 做出了重要基石性工作。\n我们下周见。\n16. 软件工程 i.e. software engineering\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n之前花了很多时间讲排序,也写了些 10 行左右的排序代码,对 1 个程序员来说很容易写。而且代码很短,不必用专门工具 - 记事本就够了。真的!但排序算法很少会是独立程序 ,更可能是大项目的一小部分,举个例子,微软的 office 大约有 4000 万代码,4000 万!太多了,一个人不可能做到,为了写大型程序,程序员用各种工具和方法,所有这些形成了\u0026quot;软件工程\u0026quot;学科 - 这个词由工程师 margaret hamilton 创造,她帮助 nasa 在阿波罗计划中避免了严重问题。\nshe once explained it this way: 她曾说过:\u0026ldquo;有点像牙根管治疗:你总是拖到最后才做,但有些事可以预先做好。有点像预防性体检,只不过是预防软件出错。\u0026rdquo;\n第 12 集提过,把大项目分解成小函数,可以让多人同时工作,不用关心整个项目,关心自己的函数就好了。如果你的任务是写排序算法,你只需要确保高效和正确就可以了,然而把代码打包成函数 依然不够。如果只是这样,微软 office 会有几十万个函数,虽然比 4000 万行代码要好一些,但还是太多了。解决办法是:把函数打包成层级,把相关代码都放在一起,打包成对象(objects)。\n例如,汽车软件中 可能有几个和定速巡航有关的函数,比如 设定速度,逐渐加速减速,停止定速巡航,因为这些函数都相关,可以包装成一个\u0026quot;定速巡航对象\u0026quot;,但不止如此,我们还可以做更多。\u0026ldquo;定速巡航\u0026quot;只是引擎软件的一部分,可能还有 \u0026ldquo;火花塞点火\u0026rdquo; \u0026ldquo;燃油泵\u0026rdquo; 和 \u0026ldquo;散热器\u0026rdquo;,我们可以做一个\u0026quot;引擎对象\u0026rdquo; 来包括所有\u0026quot;子\u0026quot;对象,除了子对象,\u0026ldquo;引擎对象\u0026quot;可能有自己的函数,比如 开关引擎,它也会有自己的变量,比如汽车行驶了多少英里。\n总的来说,对象可以包其它对象,函数和变量。\n当然,\u0026ldquo;引擎对象\u0026quot;只是\u0026quot;汽车对象\u0026quot;的一部分,还有传动装置,车轮,门,窗等。作为程序员,如果想设\u0026quot;定速巡航\u0026rdquo;,要一层层向下,从最外面的对象往里找,最后找到想执行的函数:\n\u0026#34;car, then engine, then cruise control, then set cruise speed to 55\u0026#34;.\rcar. engine. cruisecontrol. setcruisespeed(55) 编程语言经常用类似这样的语法,把函数打包成对象的思想叫 \u0026ldquo;面向对象编程\u0026rdquo; 。这种思想和之前类似,通过封装组件,隐藏复杂度。之前把晶体管打包成了逻辑门,现在软件也这样做。\n又提升了一层抽象!\n把大型软件(如汽车软件) 拆成一个个更小单元,适合团队合作,一个团队负责定速巡航系统,团队里的一位程序员负责其中一些函数。类似建摩天大楼,有电工装电线、管道工配管、焊接工焊接、油漆工涂油漆,还有成百上千人做其他事情,在不同岗位同时工作,各尽其能,直到整栋楼完成。\n回到定速巡航的例子,定速巡航 要用到引擎的其它函数,来保持车速。定速巡航 团队不负责这些代码,另一个团队负责,因为是其他团队的代码,定速巡航 团队需要文档 帮助理解代码都做什么,以及定义好的 \u0026ldquo;程序编程接口\u0026rdquo; -简称 api。api 帮助不同程序员合作,不用知道具体细节,只要知道怎么使用就行了。例如\u0026quot;点火控制\u0026quot;对象中,可能有\u0026quot;设置发动机转数\u0026quot;函数、\u0026ldquo;检查火花塞电压\u0026quot;函数、\u0026ldquo;点燃单个火花塞\u0026quot;函数。\u0026ldquo;设置发动机转速\u0026quot;非常有用,\u0026ldquo;定速巡航\u0026quot;团队要用到这个函数,但他们对点火系统不怎么了解,让他们调用\u0026quot;点燃单个火花塞\u0026quot;函数,不是好主意,引擎可能会炸!可能啦 !👻 api 控制哪些函数和数据让外部访问,哪些仅供内部。\u0026ldquo;面向对象\u0026quot;的编程语言可以指定函数是 public 或 private,来设置权限。如果函数标记成 private ,意味着 只有同一个对象内的其他函数能调用它。在这个例子里,只有内部函数比如 setrpm 才能调用 firesparkplug 函数 ,而 setrpm 函数是 public,所以其它对象可以调用它,比如 定速巡航。\n\u0026ldquo;面向对象编程\u0026quot;的核心是 隐藏复杂度,选择性的公布功能。因为做大型项目很有效,所以广受欢迎。计算机上几乎所有软件,游戏机里几乎所有游戏,都是 \u0026ldquo;面向对象\u0026rdquo; 编程语言写的,比如 c++, c#, objective-c 等。其他流行 oo 语言,你可能听过 python 和 java 。\n有一点很重要:代码在编译前就只是文字而已,前面提过,你可以用记事本或任何文字处理器,有人确实这样做。😳 但一般来说,现代软件开发者会用专门的工具来写代码,工具里集成了很多有用功能 帮助写代码,整理,编译和测代码,因为集成了所有东西,因此叫 集成开发环境,简称 ide 。所有 ide 都有写代码的界面,还带一些有用功能,比如代码高亮,来提高可读性。许多 ide 提供实时检查,比如拼写。大型项目有很多源代码文件,ide 帮助开发者整理和看代码,很多 ide 还可以直接编译和运行代码。\n如果程序崩了,因为你还没写完呢,ide 可以定位到出错代码,还会提供信息 帮你解决问题,这叫 调试(debug)。调试很重要,大多数程序员会花 70%~80% 时间调试,而不是在写代码。好工具能极大帮助程序员防止和解决错误,很多开发者只用一款 ide,但承认吧,vim 才是最棒的编辑器,如果你知道怎么退出的话。\n除了写代码和调试,程序员工作的另一个重要部分是给代码写文档,文档一般放在一个叫 readme 的文件里,告诉其他程序员,看代码前先看这个文件。文档也可以直接写成\u0026quot;注释\u0026rdquo;,放在源代码里,注释是标记过的一段文字,编译代码时注释会被忽略,注释存在的唯一作用 - 就是帮助开发者理解代码。好文档能帮助开发者 ,几个月后理解自己的代码,对其他人也很重要。我想花一秒再强调一下注释很重要!最糟糕的就是拿到一堆代码,没有任何注释和文档,结果得逐行读代码,理解到底干嘛的。我是认真的,别做那种人!文档还可以提高复用性,与其让程序员一遍遍写同样的东西,可以直接用别人写好的来解决问题,读文档看怎么用就行,不用读代码,\u0026ldquo;读文档啊\u0026rdquo;!\n除了 ide,还有一个重要软件帮助团队协作,源代码管理,也叫\u0026quot;版本控制\u0026rdquo;!\n苹果或微软这样的大型软件公司,会把代码放到一个中心服务器上,叫\u0026quot;代码仓库\u0026rdquo;。程序员想改一段代码时,可以 check out ,有点像从图书馆借书 。一般这种操作,可以直接在 ide 内完成,然后开发者在自己的电脑上编辑代码,加新功能,测试。如果代码没问题了,所有测试通过了,可以把代码放回去,这叫 提交 (commit) 。当代码被 check out,而且可能被改过了,其他开发者不会动这段代码,防止代码冲突和重复劳动,这样多名程序员可以同时写代码,建立庞大的系统。重要的是,你不希望提交的代码里有问题,因为其他人可能用到了这些代码,导致他们的代码崩溃,造成困惑而且浪费时间。代码的主版本 (master),应该总是编译正常,尽可能少 bug ,但有时 bug 还是会出现 。幸运的是,源代码管理可以跟踪所有变化,如果发现 bug ,全部或部分代码,可以\u0026quot;回滚\u0026quot;到之前的稳定版 。\u0026ldquo;源代码管理\u0026rdquo; 也记录了谁改了什么代码,所以同事可以给你发 讨厌的,我是说\u0026quot;有帮助的\u0026rdquo; 邮件给写代码的人。\n写代码和测代码密不可分,测试一般由个人或小团队完成,测试可以统称 \u0026ldquo;质量保证测试\u0026rdquo;,简称 qa 。严格测试软件的方方面面,模拟各种可能情况,看软件会不会出错,基本上就是找 bug 。解决大大小小的错误需要很多工作,但对确保软件质量至关重要,让软件在各种情况下按预期运行。你可能听过 \u0026ldquo;beta 版\u0026rdquo; 软件,意思是软件接近完成,但不是 100% 完全测试过。公司有时会向公众发布 beta 版,以帮助发现问题,用户就像免费的 qa 团队。你听过比较少的是 beta 版之前的版本:alpha 版本,alpha 版一般很粗糙,错误很多, 经常只在公司内部测试。\n以上只是软件工程师用的工具和技巧的冰山一角。它们帮助软件工程师制作令人喜爱的软件,如 youtube,gta5 和 ppt 等等。如你所料,这些代码要强大的处理能力 才能高速速度运行。\n所以下集讨论,计算机怎么发展到如今这么快。\n到时见。\n17. 集成电路 \u0026amp; 摩尔定律 i.e. integrated circuits \u0026amp; moore’s law\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n过去 6 集我们聊了软件,从早期编程方式到现代软件工程。在大概 50 年里,软件从纸带打孔,变成面向对象编程语言,在集成开发环境中写程序,但如果没有硬件的大幅度进步,软件是不可能做到这些的。\n为了体会硬件性能的爆炸性增长 我们要回到电子计算机的诞生年代。\n大约 1940 年代~1960 年代中期这段时间里,计算机都由独立部件组成,叫\u0026quot;分立元件\u0026rdquo;,然后不同组件再用线连在一起。举例,eniac 有 1 万 7 千多个真空管,7 万个电阻,1 万个电容器,7 千个二极管,5 百万个手工焊点。如果想提升性能,就要加更多部件,这导致更多电线,更复杂,这个问题叫 \u0026ldquo;数字暴政\u0026rdquo; 。\n1950 年代中期,晶体管开始商业化(市场上买得到),开始用在计算机里。晶体管比电子管更小更快更可靠,但晶体管依然是分立元件。\n1959 年,ibm 把 709 计算机从原本的电子管 全部换成晶体管,诞生的新机器 ibm 7090,速度快 6 倍,价格只有一半。晶体管标志着\u0026quot;计算 2.0 时代\u0026quot;的到来。虽然更快更小 但晶体管的出现 还是没有解决\u0026quot;数字暴政\u0026quot;的问题,有几十万个独立元件的计算机不但难设计 而且难生产。\n1960 年代,这个问题的严重性达到顶点 电脑内部常常一大堆电线缠绕在一起。\n看看这个 1965 年 pdp-8 计算机的内部。\n解决办法是引入一层新抽象,封装复杂性。\n突破性进展在 1958 年 当时 jack killby 在德州仪器工作,演示了一个电子部件: \u0026ldquo;电路的所有组件都集成在一起\u0026rdquo;,简单说就是: 与其把多个独立部件用电线连起来,拼装出计算机,我们把多个组件包在一起,变成一个新的独立组件,这就是 集成电路(ic)。几个月后,在 1959 年 robert noyce 的仙童半导体 让集成电路变为现实,kilby 用锗来做集成电路,锗很稀少而且不稳定,仙童半导体公司用硅 硅的蕴藏量丰富,占地壳四分之一,也更稳定可靠,所以 noyce 被公认为现代集成电路之父,开创了电子时代,创造了硅谷(仙童公司所在地),之后有很多半导体企业都出现在硅谷。\n起初,一个 ic 只有几个晶体管,例如这块早期样品,由西屋公司制造。即使只有几个晶体管 也可以把简单电路,第 3 集的逻辑门,能封装成单独组件。ic 就像电脑工程师的乐高积木,可以组合出无数种设计,但最终还是需要连起来, 创造更大更复杂的电路,比如整个计算机,所以工程师们再度创新:印刷电路板,简称 pcb 。pcb 可以大规模生产,无需焊接或用一大堆线。它通过蚀刻金属线的方式,把零件连接到一起,把 pcb 和 ic 结合使用 可以大幅减少独立组件和电线,但做到相同的功能,而且更小,更便宜,更可靠。三赢!\n许多早期 ic 都是把很小的分立元件 封装成一个独立单元,例如这块 1964 年的 ibm 样品,不过,即使组件很小 塞 5 个以上的晶体管还是很困难。为了实现更复杂的设计,需要全新的制作工艺 \u0026ldquo;光刻\u0026quot;登场!简单说就是用光把复杂图案印到材料上,比如半导体,它只有几个基础操作,但可以制作出复杂电路。\n下面用一个简单例子,来做一片这个!\n我们从一片硅开始,叫\u0026quot;晶圆\u0026rdquo; 长得像薄饼干一样。美味!我们在第 2 集讨论过 硅很特别,它是半导体,它有时导电,有时不导电,我们可以控制导电时机,所以硅是做晶体管的绝佳材料。我们可以用晶圆做基础,把复杂金属电路放上面,集成所有东西,非常适合做。. 集成电路!\n下一步是在硅片顶部加一层薄薄的氧化层,作为保护层,然后加一层特殊化学品,叫 \u0026ldquo;光刻胶\u0026rdquo;,光刻胶被光照射后 会变得可溶,可以用一种特殊化学药剂洗掉。单单光刻胶本身,并不是很有用,但和\u0026quot;光掩膜\u0026quot;配合使用会很强大,光掩膜就像胶片一样,只不过不是 吃墨西哥卷饼的可爱仓鼠,而是要转移到晶圆上的图案。把光掩膜盖到晶圆上,用强光照射 挡住光的地方,光刻胶不会变化,光照到的地方,光刻胶会发生化学变化 洗掉它之后,暴露出氧化层,用另一种化学物质 - 通常是一种酸 可以洗掉\u0026quot;氧化层\u0026quot;露出的部分,蚀刻到硅层。注意,氧化层被光刻胶保护住了。为了清理光刻胶,我们用另一种化学药品洗掉它,是的,光刻法用很多化学品,每种都有特定用途,现在硅又露出来了,我们想修改硅露出来的区域,让它导电性更好,所以用一种化学过程来改变它,叫\u0026quot;掺杂\u0026rdquo;。不是开玩笑!我们继续!\u0026ldquo;掺杂\u0026rdquo; 通常用高温气体来做,比如磷 渗透进暴露出的硅,改变电学性质。\n半导体的具体物理和化学性质我们不会深究,如果你感兴趣,描述里有个视频链接 视频制作者是 derek muller 他的频道叫 veritasium 。\n但我们还需要几轮光刻法 来做晶体管,过程基本一样,先盖氧化层,再盖光刻胶,然后用新的光掩膜,这次图案不同 在掺杂区域上方开一个缺口,洗掉光刻胶,然后用另一种气体掺杂 把一部分硅转成另一种形式。为了控制深度,时机很重要,我们不想超过之前的区域,现在,所有需要的组件都有了。\n最后一步,在氧化层上做通道 这样可以用细小金属导线,连接不同晶体管,再次用光刻胶和光掩膜蚀刻出小通道。现在用新的处理方法 叫\u0026quot;金属化\u0026rdquo; 放一层薄薄的金属,比如铝或铜,但我们不想用金属盖住所有东西 我们想蚀刻出具体的电路,所以又是类似的步骤 用光刻胶+光掩膜,然后溶掉暴露的光刻胶,暴露的金属。咻~\n晶体管终于做好了! 它有三根线,连接着硅的三个不同区域,每个区域的掺杂方式不同,这叫双极型晶体管。这个 1962 年的真实专利,永远改变了世界!\n用类似步骤,光刻可以制作其他电子元件 比如电阻和电容,都在一片硅上,而且互相连接的电路也做好了。再见了,分立元件!\n之前的例子 只做了一个晶体管,但现实中 光刻法一次会做上百万个细节。芯片放大是这样的,导线上下交错,连接各个元件,尽管可以把光掩膜投影到一整片晶圆上 但光可以投射成任意大小,就像投影仪可以投满荧幕一样,我们可以把光掩膜 聚焦到极小的区域,制作出非常精细的细节。一片晶圆可以做很多 ic 整块都做完后,可以切割然后包进微型芯片,微型芯片就是在电子设备中那些小长方体,记住,芯片的核心都是一小片 ic 。\n随着光刻技术(photolithography techniques)发展,晶体管变小,密度变高。1960 年代初,ic 很少超过 5 个晶体管,因为塞不下,但 1960 年代中期 市场上开始出现超过 100 个晶体管的 ic 。1965 年,戈登·摩尔看到了趋势:每两年左右,得益于材料和制造技术的发展 同样大小的空间,能塞进两倍数量的晶体管!这叫 摩尔定律 ,然而这个名字不太对 因为它不是定律,只是一种趋势,但它是对的!\n芯片的价格也急剧下降 1962 年平均 50 美元,下降到 1968 年 2 美元左右。如今 几美分就能买到 ic ,晶体管更小密度更高 还有其他好处。晶体管越小,要移动的电荷量就越少 能更快切换状态,耗电更少。电路更紧凑 还意味着信号延迟更低,导致时钟速度更快。\n1968 年,罗伯特·诺伊斯 和 戈登·摩尔 联手成立了一家新公司,结合 intergrated(集成) 和 electronics(电子) 两个词,取名 intel, 如今最大的芯片制造商!\nintel 4004 cpu, 在第 7, 8 集介绍过 是个重要里程碑,发布于 1971 年 是第一个用 ic 做的处理器,也叫微型处理器,因为真的非常小!它有 2300 个晶体管,人们惊叹于它的整合水平 整个 cpu 在一个芯片里,而仅仅 20 年前,用分立元件会占满整个屋子。\n集成电路的出现 尤其是用来做微处理器,开启了计算 3.0 。\n而 intel 4004 只是个开始,cpu 晶体管数量爆发增长。1980 年,3 万晶体管。1990 年,100 万晶体管。2000 年,3000 万个晶体管。2010 年,10 亿个晶体管! 在一个芯片里!我的天啊!\n为了达到这种密度,光刻的分辨率 从大约一万纳米,大概是人类头发直径的 1/10 ,发展到如今的 14 纳米 比血红细胞小 400 倍!\n当然,cpu 不是唯一受益的元件。大多数电子器件都在指数式发展: 内存,显卡,固态硬盘,摄像头感光元件,等等。如今的处理器,比如 iphone 7 的 a10 cpu 有 33 亿个晶体管,面积仅有 1cm x 1cm,比一张邮票还小。\n现代工程师设计电路时,当然不是手工一个个设计晶体管 这不是人力能做到的,1970 年代开始,超大规模集成 (vlsi) 软件 用来自动生成芯片设计,用比如 \u0026ldquo;逻辑综合\u0026rdquo; 这种技术,可以放一整个高级组件,比如内存缓存。软件会自动生成电路,做到尽可能高效,许多人认为这是计算 4.0 的开始。\n坏消息是,专家们几十年来 一直在预言摩尔定律的终结,现在可能终于接近了。进一步做小,会面临 2 个大问题:\n用光掩膜把图案弄到晶圆上 因为光的波长,精度已达极限。所以科学家在研制波长更短的光源,投射更小的形状; 当晶体管非常小,电极之间可能只距离几个原子,电子会跳过间隙,这叫:量子隧穿效应。 如果晶体管漏电,就不是好开关。科学家和工程师在努力找解决方法,实验室中已造出小至 1 纳米的晶体管,能不能商业量产依然未知,未来也许能解决。\n我非常期待!下周见!\n18. 操作系统 i.e. operating systems\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n1940, 1950 年代的电脑,每次只能运行一个程序。程序员在打孔纸卡上写程序,然后拿到一个计算机房间,交给操作员,等计算机空下来了,操作员会把程序放入,然后运行,输出结果,停机。以前计算机慢,这种手动做法可以接受,运行一个程序通常要几小时,几天甚至几周。但上集说过,计算机越来越快,越来越快 - 指数级增长!很快,放程序的时间比程序运行时间还长,我们需要一种方式让计算机自动运作,于是\u0026quot;操作系统\u0026quot;诞生了。\n操作系统,简称 os(operating systems),其实也是程序,但它有操作硬件的特殊权限,可以运行和管理其它程序。操作系统一般是开机第一个启动的程序,其他所有程序都由操作系统启动。\n操作系统开始于 1950 年代,那时计算机开始变得更强大更流行。第一个操作系统加强了程序加载方式,之前只能一次给一个程序,现在可以一次多个。当计算机运行完一个程序,会自动运行下一个程序,这样就不会浪费时间找下一个程序的纸卡,这叫 批处理 。电脑变得更快更便宜,开始在出现在世界各地,特别是大学和政府办公室。很快,人们开始分享软件,但有一个问题 - 在哈佛 1 号和 eniac 那个时代,计算都是一次性的。程序员只需要给那\u0026quot;一台\u0026quot;机器写代码,处理器,读卡器,打印机都是已知的。但随着电脑越来越普遍,计算机配置并不总是相同的,比如计算机可能有相同 cpu,但不同的打印机。这对程序员很痛苦,不仅要担心写程序,还要担心程序怎么和不同型号打印机交互,以及计算机连着的其他设备,这些统称\u0026quot;外部设备\u0026quot; 。和早期的外部设备交互,是非常底层的,程序员要了解设备的硬件细节。加重问题的是,程序员很少能拿到所有型号的设备来测代码,所以一般是阅读手册来写代码,祈祷能正常运行。现在是\u0026quot;即插即用\u0026quot;,以前是\u0026quot;祈祷能用\u0026quot;。\n这很糟糕,所以为了程序员写软件更容易,操作系统充当软件和硬件之间的媒介。\n更具体地说,操作系统提供 api 来抽象硬件,叫 \u0026ldquo;设备驱动程序\u0026rdquo;,程序员可以用标准化机制和输入输出硬件(i/o)交互。比如,程序员只需调用 print(highscore),操作系统会处理输到纸上的具体细节。\n到 1950 年代尾声,电脑已经非常快了。处理器经常闲着,等待慢的机械设备(比如打印机和读卡器)。程序阻塞在 i/o 上,而昂贵的处理器则在度假,就是放松啥也不做。50 年代后期,英国曼彻斯特大学 开始研发世界上第一台超级计算机,atlas ,他们知道机器会超级快,所以需要一种方式来最大限度的利用它,他们的解决方案是一个程序叫 atlas supervisor ,于 1962 年完成。这个操作系统不仅像更早期的批处理系统那样,能自动加载程序,还能在单个 cpu 上同时运行几个程序,它通过调度来做到这一点。假设 atlas 上有一个游戏在运行,并且我们调用一个函数 print(highscore),它让 atlas 打印一个叫 highscore 的变量值,让朋友知道 我是最高分冠军。 print 函数运行需要一点时间,大概上千个时钟周期,但因为打印机比 cpu 慢,与其等着它完成操作,atlas 会把程序休眠,运行另一个程序,最终,打印机会告诉 atlas, 打印已完成,atlas 会把程序标记成可继续运行,之后在某时刻会安排给 cpu 运行,并继续 print 语句之后的下一行代码。这样,atlas 可以在 cpu 上运行一个程序,同时另一个程序在打印数据,同时另一个程序读数据。atlas 的工程师做的还要多,配了 4 台纸带读取器,4 台纸带打孔机,多达 8 个磁带驱动器。\n使多个程序可以同时运行,在单个 cpu 上共享时间,操作系统的这种能力叫 \u0026ldquo;多任务处理\u0026rdquo; 。同时运行多个程序有个问题,每个程序都会占一些内存,当切换到另一个程序时,我们不能丢失数据,解决办法是给每个程序分配专属内存块。\n举个例子,假设计算机一共有 10000 个内存位置,程序 a 分配到内存地址 0 到 999,而程序 b 分配到内存地址 1000 到 1999,以此类推。如果一个程序请求更多内存,操作系统会决定是否同意,如果同意,分配哪些内存块。这种灵活性很好,但带来一个奇怪的后果,程序 a 可能会分配到非连续的内存块,比如内存地址 0 到 999,以及 2000 到 2999。这只是个简单例子,真正的程序可能会分配到内存中数十个地方,你可能想到了,这对程序员来说很难跟踪。也许内存里有一长串销售额,每天下班后要算销售总额,但列表 存在一堆不连续的内存块里…… 🤬\n为了隐藏这种复杂性,操作系统会把内存地址进行 \u0026ldquo;虚拟化\u0026rdquo;,这叫 \u0026ldquo;虚拟内存\u0026rdquo; ,程序可以假定内存总是从地址 0 开始,简单又一致,而实际物理位置被操作系统隐藏和抽象了。\n一层新的抽象!!!\n用程序 b 来举例 它被分配了内存地址 1000 到 1999,对程序 b 而言,它看到的地址是 0 到 999,操作系统会自动处理 虚拟内存和物理内存之间的映射。如果程序 b 要地址 42,实际上是物理地址 1042,这种内存地址的虚拟化对程序 a 甚至更有用。在例子中,a 被分配了两块隔开的内存,程序 a 不知道这点,以 a 的视角,它有 2000 个连续地址。当程序 a 读内存地址 999 时 会刚好映射到物理内存地址 999,但如果程序 a 读下一个地址 1000 ,会映射到物理地址 2000 ,这种机制使程序的内存大小可以灵活增减 叫 \u0026ldquo;动态内存分配\u0026rdquo; 。对程序来说,内存看起来是连续的。它简化了一切,为操作系统同时运行多个程序 提供了极大的灵活性,给程序分配专用的内存范围,另一个好处是 这样隔离起来会更好。如果一个程序出错,开始写乱七八糟的数据,它只能捣乱自己的内存,不会影响到其它程序,这叫 \u0026ldquo;内存保护\u0026rdquo; 。防止恶意软件(如病毒)也很有用,例如,我们不希望其他程序有能力 读或改邮件程序的内存,如果有这种权限 恶意软件可能以你的名义发邮件,甚至窃取个人信息,一点都不好!\natlas 既有\u0026quot;虚拟内存\u0026quot;也有\u0026quot;内存保护\u0026quot;,是第一台支持这些功能的计算机和操作系统!\n到 1970 年代,计算机足够快且便宜,大学会买电脑让学生用。计算机不仅能同时运行多个程序,还能让多用户能同时访问。多个用户用\u0026quot;终端\u0026quot;来访问计算机,\u0026ldquo;终端\u0026quot;只是键盘+屏幕,连到主计算机 终端本身没有处理能力。冰箱大小的计算机可能有 50 个终端,能让 50 个用户使用,这时操作系统不但要处理多个程序,还要处理多个用户。为了确保其中一个人不会占满计算机资源,开发了 分时操作系统 ,意思是 每个用户只能用一小部分处理器,内存等。因为电脑很快 即使拿到 1/50 的资源也足以完成许多任务。\n早期分时操作系统中,最有影响力的是 multics(多任务信息与计算系统),于 1969 年发布。multics 是第一个,从设计时就考虑到安全的操作系统,开发人员不希望恶意用户访问不该访问的数据,比如学生假装成教授,访问期末考试的文件,这导致 multics 的复杂度超过当时的平均水准,操作系统会占大约 1 mb 内存,这在当时很多!可能是内存的一半,只拿来运行操作系统!\nmultics 的研究人员之一 dennis ritchie 曾说过,\u0026ldquo;阻碍 multics 获得商业成功的一个明显问题是 - 从某种方面来说,它被过度设计了,功能太多了\u0026rdquo; 。所以 dennis 和另一个 multics 研究员 ken thompson 联手打造新的操作系统,叫 unix 。他们想把操作系统分成两部分:\n首先是操作系统的核心功能,如内存管理,多任务和输入/输出处理 这叫 \u0026ldquo;内核\u0026rdquo; ; 第二部分是一堆有用的工具,但它们不是内核的一部分(比如程序和运行库)。 紧凑的内核意味着功能没有那么全面。\nmultics 的另一个开发者 tom van vleck 回忆说:\u0026ldquo;我对 dennis 说,我在 multics 写的一半代码都是错误恢复代码\u0026rdquo;。他说:\u0026ldquo;unix 不会有这些东西,如果有错误发生,我们就让内核\u0026quot;恐慌\u0026rdquo;(panic),当调用它时,机器会崩溃,你得在走廊里大喊,\u0026ldquo;嘿,重启电脑\u0026rdquo;!😹\n你可能听过 \u0026ldquo;内核恐慌\u0026rdquo;(kernel panic),这就是这个词的来源。内核如果崩溃,没有办法恢复,所以调用一个叫\u0026quot;恐慌\u0026rdquo;(panic)的函数。起初只是打印\u0026quot;恐慌\u0026quot;一词,然后无限循环,这种简单性意味着 unix 可以在更便宜更多的硬件上运行。\nunix 在 dennis 和 ken 工作的贝尔实验室大受欢迎,越来越多开发人员用 unix 写程序和运行程序,工具数量日益增长。1971 年发布后不久,就有人写了不同编程语言的编译器,甚至文字处理器,使得 unix 迅速成为 1970~80 年代最流行的操作系统之一。\n到 1980 年代早期,计算机的价格 降到普通人买得起,这些叫\u0026quot;个人电脑\u0026quot;或\u0026quot;家庭电脑\u0026quot;。这些电脑比大型主机简单得多,主机一般在大学,公司和政府,因此操作系统也得简单。举例,微软的磁盘操作系统(ms-dos)只有 160 kb 一张磁盘就可以容纳,于 1981 年发布,成为早期家用电脑最受欢迎的操作系统,虽然缺少\u0026quot;多任务\u0026quot;和\u0026quot;保护内存\u0026quot;这样功能,意味着程序经常使系统崩溃。虽然很讨厌但还可以接受,因为用户可以重启,哪怕是微软 1985 年发布的早期 windows 虽然在 90 年代很流行,但却缺乏\u0026quot;内存保护\u0026quot;,当程序行为不当时,就会\u0026quot;蓝屏\u0026quot;,代表程序崩溃的非常严重,把系统也带崩溃了…… 👻👻👻\n幸运的是,新版 windows 有更好的保护,不会经常崩溃。如今的计算机 有现代操作系统,比如 mac os x,windows 10 ,linux,ios 和 android 。虽然大部分设备只有一个人使用 - 你! 操作系统依然有\u0026quot;多任务,\u0026ldquo;虚拟内存\u0026rdquo;, \u0026ldquo;内存保护\u0026rdquo;,因此可以同时运行多个程序: 一边在浏览器看 youtube,一边在 photoshop 修图,用 spotify 放音乐,同步 dropbox 。如果没有操作系统这几十年的发展,这些都不可能,当然,我们也需要地方放程序 。\n下周会讨论。\n19. 内存 \u0026amp; 储存介质 i.e. memory \u0026amp; storage\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n系列中 我们多次谈到内存(memory), 甚至在第 6 集设计了一个简单内存。一般来说,电脑内存是 \u0026ldquo;非永久性\u0026rdquo;,如果 xbox 电源线不小心拔掉了,内存里所有数据都会丢失,所以内存叫\u0026quot;易失性\u0026quot;存储器。\n我们还没谈过的话题 是存储器(storage),存储器(storage)和内存(memory)有点不同 - 任何写入\u0026quot;存储器\u0026quot;的数据,比如你的硬盘 数据会一直存着,直到被覆盖或删除,断电也不会丢失。存储器是\u0026quot;非易失性\u0026quot;的,以前是\u0026quot;易失性\u0026quot;的速度快,\u0026ldquo;非易失性\u0026quot;的速度慢。但随着技术发展,两者的差异越来越小。\n如今我们认为稀松平常的技术,比如这个 u 盘能低成本+可靠+长时间 存储上 gb 的数据。但以前可不是这样的,最早的存储介质是 打孔纸卡 以及纸卡的亲戚 - 打孔纸带。到 1940 年代,纸卡标准是 80 列 x 12 行,一张卡能存 960 位数据 (80x12=960) 。据我们所知的最大纸卡程序是美国军方的\u0026quot;半自动地面防空系统\u0026rdquo;,简称 sage - 一个在 1958 年投入使用的防空系统,主程序存储在 62,500 个纸卡上,大小 5mb 左右,相当如今手机拍张照。\n纸卡用了十几年,因为不用电而且便宜耐用,然而坏处是读取慢,只能写入一次,打的孔无法轻易补上。对于存临时值,纸卡不好用,我们需要更快更大更灵活的存储方式。\nj. presper eckert 在 1944 年建造 eniac 时发明了一种方法,叫\u0026quot;延迟线存储器\u0026quot;(delay line memory),原理如下:拿一个管子装满液体,如水银,管子一端放扬声器,另一端放麦克风,扬声器发出脉冲时会产生压力波,压力波需要时间传播到另一端的麦克风,麦克风将压力波转换回电信号。我们可以用压力波的传播延迟来存储数据!假设有压力波代表 1,没有代表 0 ,扬声器可以输出 1010 0111 。压力波沿管子传播,过了一会儿,撞上麦克风,将信号转换回 1 和 0 ,如果加一个电路,连接麦克风和扬声器,再加一个放大器(amplifier)来弥补信号衰弱,就能做一个存储数据的循环。信号沿电线传播几乎是瞬时的,所以任何时间点只显示 1 bit 数据,但管子中可以存储多个位 (bit) 。忙完 eniac 后,eckert 和同事 john mauchly 着手做一个更大更好的计算机叫 edvac,使用了延迟线存储器,总共有 128 条延迟线,每条能存 352 位(bits),总共能存 45,000 位 (bit) ,对 1949 年来说还不错!这使得 edvac 成为最早的 \u0026ldquo;存储程序计算机\u0026rdquo; 之一,我们在第 10 集讨论过。\n但\u0026quot;延迟线存储器\u0026quot;的一大缺点是 - 每一个时刻只能读一位 (bit) 数据,如果想访问一个特定的 bit,比如第 112 位 (bit) 你得等待它从循环中出现,所以又叫 \u0026ldquo;顺序存储器\u0026quot;或\u0026quot;循环存储器\u0026rdquo;,而我们想要的是 \u0026ldquo;随机存取存储器\u0026rdquo; 可以随时访问任何位置。增加内存密度也是一个挑战,把压力波变得更紧密,意味着更容易混在一起,所以出现了其他类型的 \u0026ldquo;延迟线存储器\u0026rdquo;,如 \u0026ldquo;磁致伸缩延迟存储器\u0026rdquo;,用金属线的振动来代表数据,通过把线卷成线圈,1 英尺× 1 英尺的面积能存储大概 1000 位 (bit),然而,延迟线存储器在 1950 年代中期就基本过时了,因为出现了新技术,性能,可靠性和成本都更好 - \u0026ldquo;磁芯存储器\u0026rdquo; 🧲 ,用了像甜甜圈的小型磁圈,如果给磁芯绕上电线,并施加电流,可以将磁化在一个方向,如果关掉电流,磁芯保持磁化;如果沿相反方向施加电流,磁化的方向(极性)会翻转,这样就可以存 1 和 0!如果只存 1 位不够有用,所以把小甜甜圈排列成网格,有电线负责选行和列,也有电线贯穿每个磁芯,用于读写一位 (bit)。我手上有一块磁芯存储器,每个黄色方格 有 32 行 x32 列的磁芯 每个磁芯存 1 位数据,所以能存 1024 位 (bit) (32x32=1024),总共 9 个黄色方格,所以这块板子最多能存 9216 位 (bit) (1024x9=9216),换算过来大约是 9 千字节 (9216 bit ~= 9 kb)。磁芯内存的第一次大规模运用是 1953 年麻省理工学院的 whirlwind 1 计算机,磁芯排列是 32×32,用了 16 块板子,能存储大约 16000 位 (bit)。更重要的是,不像\u0026quot;延迟线存储器\u0026quot; 磁芯存储器能随时访问任何一位 (bit),这在当时非常了不起!\n\u0026ldquo;磁芯存储器\u0026rdquo; 从 1950 年代中期开始成为主流 流行了 20 多年,而且一般还是手工编织的!刚开始时,存储成本大约 1 美元 1 位 (bit) 到 1970 年代,下降到 1 美分左右,不幸的是,即使每位 1 美分也不够便宜,之前提过,现代手机随便拍张照片都有 5 mb,5mb 约等于 4000 万 bit。你愿意花 40 万美元在\u0026quot;磁芯存储器\u0026quot;上存照片吗?如果你有这么多钱,你知道 crash course 在 patreon 有赞助页吗?对吧?你懂的!🤑\n总之,当时对存储技术进行了大量的研究。到 1951 年,eckert 和 mauchly 创立了自己的公司,设计了一台叫 univac 的新电脑,最早进行商业销售的电脑之一。它推出了一种新存储:磁带,磁带是纤薄柔软的一长条磁性带子 卷在轴上,磁带可以在\u0026quot;磁带驱动器\u0026quot;内前后移动,里面有一个\u0026quot;写头\u0026quot;绕了电线,电流通过产生磁场,导致磁带的一小部分被磁化。电流方向决定了极性,代表 1 和 0 。还有一个\u0026quot;读头\u0026quot;,可以非破坏性地检测极性 。univac 用了半英寸宽,8 条并行的磁带,磁带每英寸可存 128 位数据,每卷有 1200 英尺长,意味着一共可以存 1500 万位左右 - 接近 2 兆字节!(2 mb) 虽然磁带驱动器很贵,但磁带又便宜又小,因此磁带至今仍用于存档。磁带的主要缺点是访问速度 - 磁带是连续的,必须倒带或快进到达特定位置,可能要几百英尺才能得到某个字节 (byte),这很慢!🐢!\n1950,60 年代,有个类似技术是 \u0026ldquo;磁鼓存储器\u0026rdquo;。有金属圆筒,盖满了磁性材料以记录数据,滚筒会持续旋转,周围有数十个读写头,等滚筒转到正确的位置读写头会读或写 1 位 (bit) 数据,为了尽可能缩短延迟,鼓轮每分钟上千转!到 1953 年,磁鼓技术飞速发展,可以买到存 80,000 位的\u0026quot;磁鼓存储器\u0026quot; - 也就是 10 kb 。但到 1970 年代 \u0026ldquo;磁鼓存储器\u0026rdquo; 不再生产,然而,磁鼓导致了硬盘的发展。硬盘和磁鼓很相似\nwhich are very similar, but use a different geometric configuration. 然而,磁鼓导致了硬盘的发展 硬盘和磁鼓很相似,不过硬盘用的是盘,不像磁鼓用圆柱体,因此得名。原理是一样的,磁盘表面有磁性,写入头和读取头,可以处理上面的 1 和 0 。硬盘的好处是薄,可以叠在一起,提供更多表面积来存数据。ibm 对世上第一台磁盘计算机就是这样做的 - ramac 305 ,顺便一说名字不错,它有 50 张 24 英寸直径的磁盘,总共能存 5 mb 左右…… 太棒啦!终于能存一张现代手机的照片了 🖼 !这年是 1956 年!要访问某个特定 bit ,一个读/写磁头会向上或向下移动,找到正确的磁盘,然后磁头会滑进去,就像磁鼓存储器一样,磁盘也会高速旋转,所以读写头要等到正确的部分转过来。ramac 305 访问任意数据,平均只要六分之一秒左右,也叫寻道时间。虽然六分之一秒对存储器来说算不错,但对内存来说还不够快,所以 ramac 305 还有\u0026quot;磁鼓存储器\u0026quot;和\u0026quot;磁芯存储器\u0026quot;。\n这是\u0026quot;内存层次结构\u0026quot;的一个例子,一小部分高速+昂贵的内存 ,一部分稍慢+相对便宜些的内存 ,还有更慢+更便宜的内存,这种混合在成本和速度间取得平衡。\n1970 年代,硬盘大幅度改进并变得普遍,如今的硬盘可以轻易容纳 1tb 的数据,能存 20 万张 5mb 的照片!网上最低 40 美元就可以买到,每 bit 成本 0.0000000005 美分,比磁芯内存 1 美分 1 bit 好多了!另外,现代硬盘的平均寻道时间低于 1/100 秒。我简单地提一下硬盘的亲戚,软盘,除了磁盘是软的,其他基本一样。你可能见过某些程序的保存图标是一个软盘,软盘曾经是真实存在的东西!软盘是为了便携,在 1970~1990 非常流行,如今当杯垫挺不错的。密度更高的软盘,如 zip disks,在 90 年代中期流行起来,但十年内就消失了。\n光学存储器于 1972 年出现,12 英寸的\u0026quot;激光盘\u0026quot;。你可能对后来的产品更熟:光盘(compact disk)(简称 cd),以及 90 年代流行的 dvd,功能和硬盘软盘一样,都是存数据,但用的不是磁性。光盘表面有很多小坑,造成光的不同反射,光学传感器会捕获到,并解码为 1 和 0。如今,存储技术在朝固态前进,没有机械活动部件。比如这个硬盘,以及 u 盘,里面是集成电路,我们在第 15 集讨论过。\n第一个 ram 集成电路出现于 1972 年,成本每比特 1 美分,使\u0026quot;磁芯存储器\u0026quot;迅速过时。如今成本下降了更多,机械硬盘 被 固态硬盘 逐渐替代,简称 ssd(solid state drives)。由于 ssd 没有移动部件,磁头不用等磁盘转,所以 ssd 访问时间低于 1/1000 秒,这很快!🐇 但还是比 ram 慢很多倍,所以现代计算机 仍然用存储层次结构。\n我们从 1940 年代到现在进步巨大,就像在第 14 集讨论过的 晶体管数量和摩尔定律,内存和存储技术也有类似的趋势,从早期每 mb 成本上百万美元,下滑到 2000 年只要几分钱,如今远远低于 1 分钱,完全没有打孔纸卡,你能想象 sega 的纸卡房间风一吹会怎样吗? 62,500 张卡 …… 我想都不敢想 😂\n\u003e 不同的介质,不同的方法,归其本质,都是为了区分两种显著的状态,分别标识 0 和 1 。有了 0 和 1 ,就有了整个数据世界。\r我们下周见。\n20. 文件系统 i.e. files \u0026amp; file systems\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讲了数据存储,磁带和硬盘这样的技术,可以在断电状态长时间存上万亿个位,非常合适存一整块有关系的数据,或者说\u0026quot;文件\u0026quot;。你肯定见过很多种文件,比如文本文件,音乐文件,照片和视频。今天,我们要讨论文件到底是什么 以及计算机怎么管理文件。\n随意排列文件数据完全没问题,但按格式排会更好,这叫 \u0026ldquo;文件格式\u0026rdquo; 。你可以发明自己的文件格式,程序员偶尔会这样做,但最好用现成标准,比如 jpeg 和 mp3 。来看一些简单文件格式,最简单的是文本文件,也叫 txt 文件,里面包含的是。.. 文字 (惊喜吧)。就像所有其它文件, 文本文件只是一长串二进制数 ,原始值看起来会像这样:\n可以转成十进制看,但帮助不大,解码数据的关键是 ascii 编码 - 一种字符编码标准,第 4 集讨论过。第一个值 72 在 ascii 中是大写字母 h ,以此类推解码其他数字 。\n来看一个更复杂的例子:波形 (wave) 文件,也叫 wav 它存音频数据。在正确读取数据前,需要知道一些信息,比如码率 (bit rate),以及是单声道还是立体声。关于数据的数据,叫 \u0026ldquo;元数据\u0026rdquo; (meta data) 。元数据存在文件开头,在实际数据前面,因此也叫 文件头 (header) 。wav 文件的前 44 个字节长这样:\n有的部分总是一样的,比如写着 wave 的部分,其他部分的内容,会根据数据变化。音频数据紧跟在元数据后面,是一长串数字,数字代表每秒捕获多次的声音幅度。如果想学声音的基础知识,可以看物理速成课(crash course physics)第 18 集,举个例子,看一下\u0026quot;你好\u0026quot;的波形,现在捕获到了一些声音,我们放大看一下。\n电脑和手机麦克风,每秒可以对声音进行上千次采样,每次采样可以用一个数字表示,声压越高数字越大,也叫\u0026quot;振幅\u0026quot;,wave 文件里存的就是这些数据!每秒上千次的振幅!播放声音文件时,扬声器会产生相同的波形 - \u0026ldquo;你好!\u0026rdquo;\n现在来谈谈位图 (bitmap),后缀 .bmp,它存图片,计算机上,图片由很多个叫\u0026quot;像素\u0026quot;的方块组成,每个像素由三种颜色组成:红,绿,蓝,叫\u0026quot;加色三原色\u0026quot;,混在一起可以创造其它颜色。就像 wav 文件一样,bmp 文件开头也是元数据有:图片宽度,图片高度,颜色深度,举例,假设元数据说图是 4 像素宽 x 4 像素高,颜色深度 24 位、8 位红色,8 位绿色,8 位蓝色,提醒一下,8 位 (bit) 和 1 字节 (byte) 是一回事。一个字节能表示的最小数是 0,最大 255 。图像数据看起来会类似这样:来看看第一个像素的颜色 - 红色是 255,绿色是 255,蓝色也是 255,这等同于全强度红色,全强度绿色和全强度蓝色,混合在一起变成白色,所以第一个像素是白色!下一个像素的红绿蓝值,或 rgb 值 255,255,0 是黄色! 下一个像素的红绿蓝值,或 rgb 值 255,255,0 是黄色! 下一个像素是 0,0,0 ,黑色 !下一个是黄色 !因为元数据说图片是 4x4 我们知道现在到了第一行结尾,所以换一行,下一个 rgb 值是 255,255,0 ,又是黄色!好,我们读完剩下的像素 - 一个低分辨率的吃豆人。\n刚才显然只是一个简单例子,但这张图片也可以用 bmp 存。我想再次强调,不管是文本文件,wav,bmp,或是我们没时间讨论的其他格式。 文件在底层全是一样的: 一长串二进制 。为了知道文件是什么,文件格式至关重要。\n现在你对文件更了解了 我们接下来讨论计算机怎么存文件。\n虽然硬件可能是磁带,磁鼓,磁盘或集成电路,通过软硬件抽象后,可以看成一排能存数据的桶。在很早期时,计算机只做一件事,比如算火炮射程表。整个储存器就像一整个文件,数据从头存到尾,直到占满,但随着计算能力和存储容量的提高存多个文件变得非常有用,最简单的方法是把文件连续存储。这样能用,但怎么知道文件开头和结尾在哪里?储存器没有文件的概念,只是存储大量位,所以为了存多个文件,需要一个特殊文件,记录其他文件的位置,这个特殊文件有很多名字,这里泛称 \u0026ldquo;目录文件\u0026rdquo; 🗃 。这个文件经常存在最开头,方便找 - 位置 0!目录文件里,存所有其他文件的名字,格式是 文件名 + 一个句点 + 扩展名 ,比如 bmp 或 wav 。\n扩展名帮助得知文件类型,目录文件还存文件的元数据,比如创建时间,最后修改时间,文件所有者是谁、是否能读/写、或读写都行。最重要的是,目录文件有文件起始位置和长度,如果要添加文件,删除文件,更改文件名等,必须更新目录文件。就像书的目录,如果缩短或移动了一个章节,要更新目录,不然页码对不上。\n\u003e 文件名 + 一个句点 + 扩展名 文件起始位置和长度\n创建时间\n最后修改时间\n文件所有者是谁\n是否能读/写、或读写都行\n……\n目录文件,以及对目录文件的管理,是一个非常简单的文件系统例子!文件系统专门负责管理文件!\n刚刚的例子叫\u0026quot;平面文件系统\u0026quot; 因为文件都在同一个层次,平的!当然,把文件前后排在一起有个问题。如果给 todo.txt 加一点数据,会覆盖掉后面 carrie.bmp 的一部分,所以现代文件系统会做两件事:\n把空间划分成一块块,导致有一些 \u0026ldquo;预留空间\u0026rdquo; 可以方便改动,同时也方便管理(用这样的方案,目录文件要记录文件在哪些块里); 拆分文件,存在多个块里。 假设打开 todo.txt 加了些内容、文件太大存不进一块里,我们不想覆盖掉隔壁的块,所以文件系统会分配 一个没使用的块,容纳额外的数据。目录文件会记录不止一个块,而是多个块,只要分配块,文件可以轻松增大缩小,如果你看了第 18 集 操作系统 这听起来很像\u0026quot;虚拟内存\u0026quot;,概念上讲的确很像!假设想删掉 carrie.bmp 只需要在目录文件删掉那条记录,让一块空间变成了可用,注意这里没有擦除数据,只是把记录删了,之后某个时候,那些块会被新数据覆盖,但在此之前,数据还在原处,所以计算机取证团队可以\u0026quot;恢复\u0026quot;数据,虽然别人以为数据已经\u0026quot;删了\u0026quot;, 狡猾!😈 假设往 todo.txt 加了更多数据,所以操作系统分配了一个新块,用了刚刚 carrie.bmp 的块,现在 todo.txt 在 3 个块里,隔开了,顺序也是乱的,这叫 碎片。碎片是增/删/改文件导致的,不可避免,对很多存储技术来说,碎片是坏事。如果 todo.txt 存在磁带上,读取文件要先读块 1, 然后快进到块 5,然后往回转到块 2 ,来回转个半天。现实世界中,大文件可能存在数百个块里,你可不想等五分钟才打开文件,答案是碎片整理!这个词听起来好像很复杂,但实际过程很简单。以前看计算机做碎片整理,真的很有趣!计算机会把数据来回移动,排列成正确的顺序,整理后 todo.txt 在 1 2 3,方便读取。\n目前只说了平面文件系统,文件都在同一个目录里。如果存储空间不多,这可能就够用了,因为只有十几个文件。但上集说过,容量爆炸式增长,文件数量也飞速增长,很快,所有文件都存在同一层变得不切实际。就像现实世界、相关文件放在同一个文件夹会方便很多,然后文件夹套文件夹,这叫 \u0026ldquo;分层文件系统\u0026rdquo; ,你的计算机现在就在用这个。\n实现方法有很多种,我们用之前的例子来讲重点好了,最大的变化是 目录文件不仅要指向文件,还要指向目录。我们需要额外元数据来区分开文件和目录,这个目录文件在最顶层,因此叫根目录,所有其他文件和文件夹,都在根目录下。\n图中可以看到根目录文件有 3 个文件 2 个子文件夹:\u0026ldquo;音乐\u0026quot;和\u0026quot;照片\u0026rdquo;,如果想知道\u0026quot;音乐\u0026quot;文件夹里有什么 必须去那边读取目录文件(格式和根目录文件一样),有很多好歌啊!\n除了能做无限深度的文件夹,这个方法也让我们可以轻松移动文件,如果想把 theme.wav 从根目录移到音乐目录,不用移动任何数据块,只需要改两个目录文件,一个文件里删一条记录,另一个文件里加一条记录,theme.wav 依然在块 5 。\n文件系统的几个重要概念,现在介绍完了。它提供了一层新抽象!\n文件系统使我们不必关心 文件在磁带或磁盘的具体位置,整理和访问文件更加方便。我们像普通用户一样直观操纵数据,比如打开和整理文件,接下来几集也会从用户角度看问题。\n下周见。\n21. 压缩 i.e. compression\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讨论了文件格式,如何编码文字,声音,图片,还举了具体例子 .txt .wav .bmp 。这些格式虽然管用,而且现在还在用,但它们的简单性意味着效率不高。我们希望文件能小一点,这样能存大量文件,传输也会快一些。等邮件附件下载烦死人了,解决方法是 压缩,把数据占用的空间压得更小,用更少的位 (bit) 来表示数据。\n听起来像魔法,但其实是计算机科学!\n我们继续用上集的 吃豆人例子,图像是 4 像素 x4 像素。之前说过,图像一般存成一长串像素值,为了知道一行在哪里结束 图像要有元数据,写明尺寸等属性,但为了简单起见,今天忽略这些细节,如果红绿蓝都是 255 会得到白色,如果混合 255 红色和 255 绿色,会得到黄色,这个图像有 16 个像素 (4x4), 每个像素 3 个字节,总共占 48 个字节(16x3=48),但我们可以压缩到少于 48 个字节。\n一种方法是 减少重复信息,最简单的方法叫 游程编码 (run-length encoding),适合经常出现相同值的文件。比如吃豆人 有 7 个连续黄色像素,与其全存下来:黄色,黄色,黄色。.. 可以插入一个额外字节,代表有 7 个连续黄色像素,然后删掉后面的重复数据。为了让计算机能分辨哪些字节是\u0026quot;长度\u0026quot;,哪些字节是\u0026quot;颜色\u0026quot;,格式要一致,所以我们要给所有像素前面标上长度。有时候数据反而会变多,但就这个例子而言,我们大大减少了字节数,之前是 48 现在是 24 ,小了 50%!省了很多空间!还有,我们没有损失任何数据 我们可以轻易恢复到原来的数据,这叫 \u0026ldquo;无损压缩\u0026rdquo; ,没有丢失任何数据,解压缩后,数据和压缩前完全一样。\n我们来看另一种无损压缩,它用更紧凑的方式表示数据块,有点像 \u0026ldquo;别忘了变厉害\u0026rdquo; 简写成 dftba ,为此,我们需要一个字典,存储\u0026quot;代码\u0026quot;和\u0026quot;数据\u0026quot;间的对应关系。我们看个例子,我们可以把图像看成一块块,而不是一个个像素。为了简单,我们把 2 个像素当成 1 块(占 6 个字节),但你也可以定成其他大小。我们只有四对: 白黄 黑黄 黄黄 白白 ,我们会为这四对 生成紧凑代码 (compact codes) 。有趣的是,这些块的出现频率不同。1950 年代 大卫·霍夫曼 发明了一种高效编码方式叫 \u0026ldquo;霍夫曼树\u0026rdquo;(huffman tree),当时他是麻省理工学院的学生,算法是这样的。\n首先,列出所有块和出现频率,每轮选两个最低的频率,这里 黑黄 和 白白 的频率最低,它们都是 1 ,可以把它们组成一个树,总频率 2 ,现在完成了一轮算法。现在我们重复这样做,这次有 3 个可选,就像上次一样,选频率最低的两个,放在一起,并记录总频率。好,我们快完成了。这次很简单,因为只有 2 个选择,把它们组合成一棵树就完成了!\n现在看起来像这样,它有一个很酷的属性:按频率排列,频率低的在下面。现在有了一棵树,你可能在想 \u0026ldquo;怎么把树变成字典?\u0026rdquo;\n我们可以把每个分支用 0 和 1 标注,就像这样。现在可以生成字典,黄黄 编码成 0 ,白黄 编码成 10 ,黑黄 编码成 110 ,白白 编码成 111 。酷的地方是它们绝对不会冲突,因为树的每条路径是唯一的,意味着代码是\u0026quot;无前缀\u0026quot;的,没有代码是以另一个代码开头的,现在我们来压缩!注意是位 (bit)! 不是字节 (byte)! 14 位 (bit) 还不到 2 个字节 (byte)!但,先别急着开香槟!字典也要保存下来,否则 14 bit 毫无意义,所以我们把字典 加到 14 bit 前面,就像这样。\n现在加上字典,图像是 30 个字节 (bytes) 比 48 字节好很多。\n\u0026ldquo;消除冗余\u0026quot;和\u0026quot;用更紧凑的表示方法\u0026rdquo;,这两种方法通常会组合使用。几乎所有无损压缩格式都用了它们,比如 gif, png, pdf, zip 。\n游程编码 和 字典编码 都是无损压缩!压缩时不会丢失信息,解压后,数据和之前完全一样。无损对很多文件很重要,比如我给你发了个压缩的 word 文档你解压之后发现内容变了,这就很糟糕了。但其他一些文件,丢掉一些数据没什么关系,丢掉那些人类看不出区别的数据。大多数有损压缩技术,都用到了这点。实际细节比较复杂,所以我们讲概念就好。以声音为例,你的听力不是完美的,有些频率我们很擅长,其他一些我们根本听不见,比如超声波,除非你是蝙蝠。举个例子,如果录音乐,超声波数据都可以扔掉 因为人类听不到超声波,另一方面,人类对人声很敏感,所以应该尽可能保持原样。低音介于两者之间,人类听得到,但不怎么敏感,一般是感觉到震动。有损音频压缩利用这一点,用不同精度编码不同频段,听不出什么区别,不会明显影响体验,音乐发烧友估计要吐槽了!日常生活中你会经常碰到这类音频压缩,所以你在电话里的声音和现实中不一样,压缩音频是为了让更多人能同时打电话,如果网速变慢了,压缩算法会删更多数据,进一步降低声音质量,所以 skype 通话有时听起来像机器人,和没压缩的音频格式相比,比如 wav 或 flac ( 这下音乐发烧友满意了),压缩音频文件如 mp3,能小 10 倍甚至更多。省了超多空间!\n所以我的旧 ipod 上有一堆超棒的歌,别批判我!\n这种删掉人类无法感知的数据的方法,叫 \u0026ldquo;感知编码\u0026rdquo; 。它依赖于人类的感知模型,模型来自\u0026quot;心理物理学\u0026quot;领域,这是各种\u0026quot;有损压缩图像格式\u0026quot;的基础,最著名的是 jpeg 。就像听力一样,人的视觉系统也不是完美的。我们善于看到尖锐对比,比如物体的边缘,但我们看不出颜色的细微变化。jpeg 利用了这一点,把图像分解成 8x8 像素块,然后删掉大量高频率空间数据。举个例子,这是导演的狗,面面,超可爱!我们来看其中一个 8x8 像素,几乎每个像素都和相邻像素不同,用无损技术很难压缩,因为太多不同点了,很多小细节,但人眼看不出这些细节,因此可以删掉很多,用这样一个简单的块来代替。这看起来一样,但可能只占 10%的原始数据。我们可以对所有 8x8 块做一样的操作,图片依然可以认出是一只狗,只是更粗糙一些,以上例子比较极端,进行了高度压缩,只有原始大小的八分之一。通常你可以取得平衡,图片看起来差不多,但文件小不少。你看得出两张图的区别吗?估计看不出。\n但我想提一下,视频压缩也造成了影响。毕竟你现在在看视频啊,视频只是一长串连续图片 所以图片的很多方面也适用于视频,但视频可以做一些小技巧。因为帧和帧之间很多像素一样,但视频可以做一些小技巧 因为帧和帧之间很多像素一样,比如我后面的背景!这叫 时间冗余 ,视频里不用每一帧都存这些像素,可以只存变了的部分,当帧和帧之间有小小的差异时,比如后面这个频率发生器,很多视频编码格式,只存变化的部分,这比存所有像素更有效率 - 利用了帧和帧之间的相似性。更高级的视频压缩格式会更进一步,找出帧和帧之间相似的补丁,然后用简单效果实现,比如移动和旋转,变亮和变暗。如果我这样摆手,视频压缩器会识别到相似性,用一个或多个补丁代表我的手,然后帧之间直接移动这些补丁,所以你看到的是我过去的手(不是实时的),有点可怕,但数据量少得多。mpeg-4 是常见标准,可以比原文件小 20 倍到 200 倍,但用补丁的移动和旋转来更新画面。当压缩太严重时会出错,没有足够空间更新补丁内的像素。即使补丁是错的,视频播放器也会照样播放,导致一些怪异又搞笑的结果,你肯定见过这些。\n总的来说,压缩对大部分文件类型都有用\n从这个角度来讲,人类不完美的视觉和听觉 也算有用。学习压缩非常重要 因为可以高效存储图片,音乐,视频。如果没有压缩,在 youtube 看\u0026quot;明星拼车唱歌\u0026quot;几乎不可能,因为你的带宽可能不够(会很卡) 而且供应商不愿意免费传输那么多数据。现在你知道为什么打 skype 电话,有时像在和恶魔通话。\n下周见。\n22. 命令行界面 i.e. keyboards \u0026amp; command line interfaces\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n我们之前讨论过输入输出 ,但都是计算机组件互相输入输出,比如 ram 输出数据,或输指令进 cpu 。我们还没讲来自人类的输入,也没讲怎么从电脑中拿出信息,除了用打孔纸卡。当然,有很多种 \u0026ldquo;输入输出设备\u0026rdquo; , 让我们和计算机交互,它们在人类和机器间提供了界面。如今有整个学科专门研究这个,叫 \u0026ldquo;人机交互\u0026rdquo;。界面对用户体验非常重要,所以是我们接下来几集的重点。\n在系列开头的几集,我们提过,早期机械计算设备用齿轮,旋钮和开关等机械结构来输入输出,这些就是交互界面。甚至早期电子计算机,比如 colossus 和 eniac,也是用一大堆机械面板和线来操作,输入一个程序可能要几星期,还没提运行时间。运行完毕后想拿出数据,一般是打印到纸上。打印机超有用,甚至查尔斯·巴贝奇都给差分机专门设计了一个,那可是 1820 年代!\n然而,到 1950 年代,机械输入完全消失,因为出现了打孔纸卡和磁带,但输出仍然是打印到纸上,还有大量指示灯,在运行中提供实时反馈。那个时代的特点是 ,尽可能迁就机器 ,对人类好不好用是其次。打孔纸带就是个好例子,就是为了方便计算机读取,纸带是连续的,方便机器处理。纸孔可以方便地用机械或光学手段识别,纸孔可以编码程序和数据,当然,人类不是以纸孔方式思考的。所以负担放到了程序员身上,他们要花额外时间和精力转成计算机能理解的格式,一般需要额外人员和设备帮忙。要注意的是,基本上 1950 年前的早期计算机,\u0026ldquo;输入\u0026quot;的概念很原始,是的,的确是人类负责输入程序和数据,但计算机不会交互式回应。程序开始运行后会一直运行,直到结束。因为机器太贵了,不能等人类慢慢敲命令和给数据,要同时放入程序和数据。\n这在 1950 年代晚期开始发生变化。一方面,小型计算机变得足够便宜,让人类来回和计算机交互变得可以接受,交互式就是人和计算机之间来回沟通。而另一方面,大型计算机变得更快,能同时支持多个程序和多个用户,这叫\u0026quot;多任务\u0026quot;和\u0026quot;分时系统\u0026rdquo;。\n但交互式操作时,计算机需要某种方法来获得用户输入,所以借用了当时已经存在的数据录入机制:键盘!当时,打字机已经存在几个世纪了,但现代打字机是克里斯托弗·莱瑟姆·肖尔斯在 1868 年发明的,虽然到 1874 年才完成设计和制造,但之后取得了商业成功。肖尔斯的打字机用了不寻常的布局,qwerty ,名字来自键盘左上角按键,为什么这么设计?有很多猜测。最流行的理论是,这样设计是为了把常见字母放得远一些,避免按键卡住。这个解释虽然省事,但可能是错的,或至少不够全面。事实上,qwerty 把很多常见字母放在了一起,比如 th 和 er 。我们知道 肖尔斯和他的团队设计了很多版才进化到这个布局。总之,肖尔斯的打字机取得了成功 ,所以其它公司很快开始抄他的设计。\n这……\r过去一个世纪有不少新的键盘布局被发明,宣称各种好处,但人们已经熟悉了 qwerty 布局 ,根本不想学新布局,这是经济学家所说的 转换成本。所以现在都快 1 个半世纪了 ,我们还在用 qwerty 键盘布局。我应该提一下,qwerty 不是通用的,有很多变体,比如法国 azerty 布局,以及中欧常见的 qwertz 布局。有趣的是,肖尔斯根本没想到打字会比手写快,手写速度大约是每分钟 20 个,打字机主要为了易读性和标准化,而不是速度,然而随着打字机成为办公室标配 ,对快速打字的渴望越来越大。\n有两个重大进步解放了打字的潜力。\n1880 年左右,辛辛那提速记学院一名叫伊丽莎白·朗利的老师开始推广十指打字,比一个手指打字要移动的距离短得多,所以速度更快。几年后,弗兰克·爱德华·麦克格林 盐湖城的一位联邦法庭书记学会了盲打,打字时不用看键盘。1888 年,麦格高林赢了备受关注的打字速度比赛,之后\u0026quot;十指盲打\u0026quot;开始流行。专业打字员每分钟 100 字以上,比手写快多了!而且清晰又整洁!虽然人类擅长用打字机,但我们没法把打字机塞到计算机面前,让它打字,计算机又没有手指。所以早期计算机用了一种特殊打字机,是专门用来发电报的,叫电传打字机。这些打字机是强化过的,可以用电报线发送和接收文本,按一个字母,信号会通过电报线,发到另一端,另一端的电传打字机会打出来,使得两人可以长距离沟通,基本是个蒸汽朋克版聊天室。因为电传打字机有电子接口,稍作修改就能用于计算机,电传交互界面在 1960~1970 很常见,用起来很简单,输入一个命令,按回车,然后计算机会输回来。用户和计算机来回\u0026quot;对话\u0026quot;,这叫 \u0026ldquo;命令行界面\u0026rdquo; 。它是最主要的人机交互方式,一直到 1980 年代,用电传打字机的命令行交互,类似这样:\n\u0026gt; 用户可以输入各种命令\r\u0026gt; 我们来看几个命令,先看当前目录有什么文件\r\u0026gt; 输入命令 ls,名字来自 list 的缩写\r\u0026gt; 然后计算机会列出 当前目录里的所有文件\r\u0026gt; 如果想看 secretstartrekdiscoverycast.txt 有什么\r\u0026gt; 要用另一个命令 显示文件内容\r\u0026gt; unix 用 cat 命令显示文件内容 cat 是连接 (concatenate) 的缩写\r\u0026gt; 然后指定文件名,指定的方法是写在 cat 命令后面 传给命令的值叫 参数\r\u0026gt; \u0026gt; 如果同一个网络里有其他人\r\u0026gt; 你可以用 finger 命令找朋友 就像是个很原始的\u0026#34;找朋友\u0026#34; app\r…… 电传打字机直到 1970 年代左右都是主流交互方式,尽管屏幕最早出现在 1950 年代,但对日常使用太贵而且分辨率低,然而因为针对普通消费者的电视机开始量产,同时处理器与内存也在发展。到 1970 年代,屏幕代替电传打字机变得可行,但与其为屏幕专门做全新的标准,工程师直接用现有的电传打字机协议,屏幕就像无限长度的纸,除了输入和输出字,没有其它东西。 协议是一样的,所以计算机分不出是纸还是屏幕 ,这些\u0026quot;虚拟电传打字机\u0026quot;或\u0026quot;玻璃电传打字机\u0026quot;叫 终端 。到 1971 年,美国大约有 7 万台电传打字机 以及 7 万个终端,屏幕又好又快又灵活。如果删一个错别字,会立刻消失,所以到 1970 年代末,屏幕成了标配。你也许会想,命令行界面太原始了,做不了什么有意思的事。即便只有文字,程序员也找到了一些方法,让它变得有趣一些。\n早期的著名交互式文字游戏 zork ,出现于 1977 年。早期游戏玩家需要丰富的想象力,想像自己身在虚构世界,比如\u0026quot;四周漆黑一片,附近可能有怪物会吃掉你\u0026quot;。我们用命令行玩玩看,就像之前,我们可以用 ls 命令,看当前目录有什么,然后用 cd 命令,进入游戏文件夹 cd 的意思是 \u0026ldquo;改变文件夹\u0026rdquo;(change directory),再用 ls 看有哪些游戏。超棒!我们有\u0026quot;冒险旅程\u0026quot;!(adventure)。想运行这个程序,只需要输入它的名字。在程序自行停止或我们主动退出前,它会接管命令行。\n你现在看到的,是\u0026quot;巨大洞穴冒险\u0026quot;这款游戏的真实输出,由 will crowther 在 1976 年开发。游戏中,玩家可以输入 1 个词或 2 个词的命令来移动人物,和其他东西交互,捡物品等,然后游戏会像旁白一样,输出你的当前位置,告诉你能做什么动作,以及你的动作造成的结果,有些动作会导致死亡!原始版本只有 66 个地方可供探索,但它被广泛认为是最早的互动式小说。游戏后来从纯文字进化成多人游戏 简称 mud,或多人地牢游戏(multi-user dungeons),是如今 mmorpg 的前辈 (大型多人在线角色扮演游戏, massive, multiplayer online role playing games)。如果你想了解游戏史,我们有游戏速成课 主持人 andre meadows 。\n命令行界面虽然简单,但十分强大。\n编程大部分依然是打字活, 所以用命令行比较自然,因此,即使是现在大多数程序员工作中依然用命令行界面,而且用命令行访问远程计算机 是最常见的方式, 比如服务器在另一个国家。如果你用 windows, macos, linux ,你的计算机有命令行界面,但你可能从来没用过,你可以在 windows 搜索栏中输入 cmd,或在 mac 上搜 terminal ,然后你可以装 zork 玩!\n现在你知道了,早期计算机的发展是如何影响到现在的。\n想想要是手机没有 qwerty 键盘 ,在 instagram 给图片配标题可就麻烦了。但我们还有一个重要话题没讲,美妙的图形界面!这是下周的主题!\n下周见。\n23. 屏幕 \u0026amp; 2d 图形显示 i.e. screens\u0026amp;2d graphics\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n这台 1960 年的 pdp-1 是一个早期图形计算机的好例子,你可以看到左边是柜子大小的电脑,中间是电传打字机,右边是一个圆形的屏幕,注意它们是分开的,因为当时文本任务和图形任务是分开的。事实上,早期的屏幕无法显示清晰的文字,而打印到纸上有更高的对比度和分辨率。早期屏幕的典型用途是跟踪程序的运行情况,比如寄存器的值,如果用打印机一遍又一遍打印出来没有意义,不仅费纸而且慢。另一方面,屏幕更新很快,对临时值简直完美。但屏幕很少用于输出计算结果,结果一般都打印到纸上,或其它更永久的东西上。\n但屏幕超有用!到 1960 年代,人们开始用屏幕做很多酷炫的事情。几十年间出现了很多显示技术,但最早最有影响力的是阴极射线管(crt,cathode ray tubes)。原理是把电子发射到有磷光体涂层的屏幕上,当电子撞击涂层时,会发光几分之一秒,由于电子是带电粒子,路径可以用磁场控制,屏幕内用板子或线圈把电子引导到想要的位置,上下左右都行。既然可以这样控制,有 2 种方法绘制图形 :\n引导电子束描绘出形状,这叫 \u0026ldquo;矢量扫描\u0026rdquo; 。因为发光只持续一小会儿,如果重复得足够快,可以得到清晰的图像; 按固定路径,一行行来,从上向下,从左到右,不断重复,只在特定的点打开电子束,以此绘制图形,这叫 \u0026ldquo;光栅扫描\u0026rdquo; 。用这种方法,可以用很多小线段绘制形状,甚至文字。 最后,因为显示技术的发展,我们终于可以在屏幕上显示清晰的点,叫\u0026quot;像素\u0026quot; 。液晶显示器,简称 lcd(liquid crystal displays),和以前的技术相当不同,但 lcd 也用光栅扫描,每秒更新多次,像素里红绿蓝的颜色。有趣的是,很多早期计算机不用像素 - 不是技术做不到,而是因为像素占太多内存。 200 像素×200 像素的图像,有 40,000 个像素,哪怕每个像素只用一个 bit 表示 代表黑色或白色,连灰度都没有!会占 40,000 bit ,内存比 pdp-1 全部内存的一半还多,所以计算机科学家和工程师,得想一些技巧来渲染图形,等内存发展到足够用。所以 早期计算机不存大量像素值,而是存符号,80x25 个符号最典型,总共 2000 个字符。如果每个字符用 8 位表示,比如用 ascii ,总共才 16000 位,这种大小更合理。为此,计算机需要额外硬件来从内存读取字符,转换成光栅图形,这样才能显示到屏幕上,这个硬件叫 \u0026ldquo;字符生成器\u0026rdquo;,基本算是第一代显卡。它内部有一小块只读存储器,简称 rom ,存着每个字符的图形,叫 \u0026ldquo;点阵图案\u0026rdquo;。如果图形卡看到一个 8 位二进制,发现是字母 k ,那么会把字母 k 的点阵图案光栅扫描显示到屏幕的适当位置。为了显示,\u0026ldquo;字符生成器\u0026rdquo; 会访问内存中一块特殊区域,这块区域专为图形保留,叫 屏幕缓冲区 。程序想显示文字时,修改这块区域里的值就行,这个方案用的内存少得多,但也意味着只能画字符到屏幕上。即使有这样限制 ,人们用 ascii 艺术发挥了很多创意!也有人用字符模仿图形界面,用下划线和加号来画盒子,线,和其他简单形状,但字符集实在太小,做不了什么复杂的事,因此对 ascii 进行了各种扩展,加新字符,比如上图的 ibm cp437 字符集,用于 dos。\n用来读取字符,转换成光栅图形的硬件 - 字符生成器(‘第一代显卡’) ,有一个 rom,存储着每个字符的图形(‘点阵图案’) - 为了显示,访问内存中的一块特殊区域(‘屏幕缓冲区’)。\r屏幕缓冲区,是不是类似于内存和屏幕显示光栅的映射 ❓ 也是一层抽象 ❓\n某些系统上可以用额外的 bit 定义字体颜色和背景颜色,做出这样的 dos 界面 这界面只用了刚刚提到的字符集。字符生成器是一种省内存的技巧,但没办法绘制任意形状。制任意形状很重要,因为电路设计,建筑平面图,地图,好多东西都不是文字!为了绘制任意形状,同时不吃掉所有内存,计算机科学家用 crt 上的\u0026quot;矢量模式\u0026quot;。概念非常简单:所有东西都由线组成,没有文字这回事,只有线条,没有别的。明白了吗?好,我们举个实例吧!\n假设这个视频是一个 笛卡尔平面 200 个单位宽,100 个单位高,原点 (0,0) 在左上角,我们可以画形状,用如下矢量命令,这些命令来自 vectrex,一个早期矢量显示系统。首先,reset ,这个命令会清空屏幕,把电子枪的绘图点移动到坐标 (0,0),并把线的亮度设为 0 , move_to 50 50 把绘图点移动到坐标 (50,50) ,intensity 100 把强度设为 100 ,现在亮度提高了,移动到 (100,50) 然后 (60,75) 然后 (50,50) ,最后把强度设回 0 。酷,我们画了一个三角形!这些命令占 160 bit 比存一个庞大的像素矩阵更好。就像之前的\u0026quot;字符生成器\u0026quot; 把内存里的字符转成图形一样,这些矢量指令也存在内存中,通过矢量图形卡画到屏幕上。数百个命令可以按序存在屏幕缓冲区,画出复杂图形,全是线段组成的!由于这些矢量都在内存中,程序可以更新这些值,让图形随时间变化 - 动画!\n最早的电子游戏之一,spacewar! 是 1962 年在 pdp-1 上用矢量图形制作的。它启发了许多后来的游戏,比如 爆破彗星 (asteroids),甚至第一个商业街机游戏:太空大战 。\n1962 年是一个大里程碑 - sketchpad 诞生,一个交互式图形界面,用途是计算机辅助设计 (cad,computer-aided design),它被广泛认为是第一个完整的图形程序,发明人伊万·萨瑟兰后来因此获得图灵奖。为了与图形界面交互 ,sketchpad 用了当时发明不久的输入设备 - 光笔,就是一个有线连着电脑的触控笔,笔尖用光线传感器,可以检测到显示器刷新,通过判断刷新时间,电脑可以知道笔的位置,有了光笔和各种按钮,用户可以画线和其他简单形状。sketchpad 可以让线条完美平行,长度相同,完美垂直 90 度,甚至动态缩放,这些在纸上很费力,在计算机上非常简单!用户还可以保存设计结果,方便以后再次使用,甚至和其他人分享。你可以有一整个库,里面有电子元件和家具之类的,可以直接拖进来用。从如今的角度来看,好像很普通,但在 1962 年 计算机还是吃纸带的大怪兽,有柜子般大小,sketchpad 和光笔让人大开眼界,它们代表了人机交互方式的关键转折点 - 电脑不再是关在门后负责算数的机器了,可以当助手帮人类做事。\n最早用真正像素的计算机和显示器出现于 1960 年代末,内存中的位 (bit) 对应屏幕上的像素,这叫 位图显示。现在我们可以绘制任意图形了,你可以把图形想成一个巨大像素值矩阵。就像之前,计算机把像素数据存在内存中一个特殊区域 叫 \u0026ldquo;帧缓冲区\u0026rdquo;。早期时,这些数据存在内存里,后来存在高速视频内存里,简称 vram 。vram 在显卡上,这样访问更快,如今就是这样做的。在 8 位灰度屏幕上,我们可用的颜色范围是 0 强度(黑色),到 255 强度(白色)。其实更像绿色或橙色 ,因为许多早期显示器不能显示白色。我们假设这个视频在低分辨率的位图屏幕上,分辨率 60x35 像素。如果我们想把 (10,10) 的像素设为白色 可以用这样的代码,…… ,如果想画一条线,假设从 (30,0) 到 (30,35) 可以用这样一个循环,……,把整列像素变成白色,如果想画更复杂的图形,比如矩形,那么需要四个值:\n1. 起始点 x 坐标 2. 起始点 y 坐标 3. 宽度 4. 高度 目前只试了白色,这次画矩形试下灰色,灰色介于 0 到 255 中间 所以我们用 127 (255/2=127.5),然后用两个循环,一个套另一个,这样外部每跑一次,内部会循环多次 ,可以画一个矩形。计算机绘图时会用指定的颜色 127 ,我们来包装成 \u0026ldquo;画矩形函数\u0026rdquo;,就像这样:\n……\n假设要在屏幕的另一边,画第二个矩形,这次可能是黑色矩形,可以直接调用 \u0026ldquo;画矩形函数\u0026rdquo;, 超棒!\n就像之前说的其他方案,程序可以操纵\u0026quot;帧缓冲区\u0026quot;中的像素数据,实现交互式图形。当然,程序员不会浪费时间从零写绘图函数 而是用预先写好的函数来做,画直线,曲线,图形,文字等\n一层新抽象!\n位图的灵活性,为交互式开启了全新可能,但它的高昂成本持续了十几年,上集提到,1971 年 整个美国也只有大约 7 万个电传打字机和 7 万个终端,令人惊讶的是 只有大约 1000 台电脑有交互式图形屏幕,这可不多!\nsketchpad 和 太空大战 这样的先驱,推动了图形界面发展,帮助普及了计算机显示器 由此,图形界面的曙光初现,帮助普及了计算机显示器 由此,图形界面的曙光初现。\n接下来讲图形界面。\n下周见。\n24. 冷战和消费主义 i.e. the cold war and consumerism\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n之前介绍了计算机历史 从人类文明的曙光开始 (第 1 集),一直到 1940 年代中期电子计算机诞生,过去 23 集里讲的很多东西,比如编程语言和编译器,算法和集成电路,软盘(floppy disks)和操作系统,电报机和屏幕,全都是 1940~1970 年代,大概这 30 年间里出现的。那时苹果和微软还不存在,也没有推特,谷歌或者 uber 。还没到个人电脑时代,而万维网,无人驾驶汽车,虚拟现实等主题,这个系列的后半部分会讲。\n今天,我们不管电路和算法 来聊聊这个影响力巨大的时代!\n我们会把重点放在 冷战,太空竞赛,全球化,消费主义的兴起。1945 年二战结束后不久,两个超级大国的关系越发紧张,美国和苏联开始了冷战,因此政府往科学和工程学 投入大量资金。计算机在战时已经证明了自身的价值, 比如曼哈顿计划 和 破解纳粹通讯加密,所以政府大量投入资源 各种雄心勃勃的项目得以进行,比如之前提过的 eniac, edvac, atlas, whirlwind ,这种高速发展,如果仅靠商业运作是根本无法做到的 - 要依靠销售收回开发成本。\n1950 年代,事情开始发生变化,特别是 univac 1,它是第一台取得商业成功的电脑,不像 eniac 或 atlas univanc 1 不是一台机器,而是一个型号,一共造了 40 多台,大部分 univac 去了政府或大公司,成为美国日益增长的军事工业综合体的一部分,因为政府有钱承担这些尖端科技。一个著名的例子是,一台给 美国原子能委员会 生产的 univac 1 ,被 cbs 用来预测 1952 年美国总统大选的结果,仅用 1%的选票,univac 1 正确预测了结果。艾森豪威尔 获得压倒性胜利,而专家预测 史蒂文森 会赢,这次事件把计算机推到了公众面前。\n计算机和以前的机器不一样,以前的机器 增强人类的物理能力,比如卡车能带更多东西,自动织布机更快,机床更精确 等等。这些东西代表了工业革命。而计算机增强的是人类智力,范内瓦·布什 看到了这种潜力。他在 1945 年发表了一篇文章,描述了一种假想计算设备叫 memex。可以用这个设备 存自己所有的书,其他资料 以及和别人沟通,而且数据是按照格式存储,所以可以快速查询,有很大灵活性,可以辅助我们的记忆。他还预测会出现新的百科全书形式,信息之间相互链接,听起来是不是很熟悉?(维基百科)\nmemex 启发了之后几个重要里程碑,比如上集 伊万·萨瑟兰 的 sketchpad(画板),以及后面很快会讲到 dough engelbart 的 on-line 系统(第 26 集)。\n范内瓦·布什 做过\u0026quot;美国科学研究与开发办公室\u0026quot;的头头,这个部门负责在二战期间 资助和安排科学研究。冷战时, 范内瓦·布什 到处游说,想建立一个职责类似,但是在和平时期运作的部门,因此 国家科学基金会 于 1950 年成立,至今,国家科学基金会 依然负责给科学研究 提供政府资金。美国的科技领先全球,主要原因之一就是这个机构。\n1950 年代,消费者开始买晶体管设备,其中值得注意的是 收音机,它又小又耐用,用电池就够了,而且便携,不像 1940 年代之前的收音机,用的是真空管。收音机非常成功,卖的像\u0026quot;菲比精灵\u0026quot;和 iphone 一样畅销。日本政府也在寻求工业机会,想振兴战后经济,他们很快动手从贝尔实验室 取得晶体管的授权,帮助振兴日本的半导体和电子行业。1955 年,索尼的第一款产品面世 - tr-55 晶体管收音机。他们把重心放在质量和价格,因此日本公司在短短 5 年内,就占有了美国便携式收音机市场的一半。这为日本成为美国的强大工业对手,埋下伏笔。\n1953 年,整个地球大概有 100 台计算机,苏联这时的计算机科技只比西方落后几年。苏联在 1950 年 ,完成了第一个可编程电子计算机,但苏联在太空竞赛远远领先。\n我们进入思想泡泡\n苏联在 1957 年 把第一个卫星送上轨道,史波尼克 1 号。不久,在 1961 年,苏联宇航员 尤里·加加林 第一个进入太空,美国民众对此不满,使得肯尼迪总统 在加加林太空任务一个月后,提出要登陆月球。登月很贵的!nasa 的预算增长了几乎十倍,在 1966 年达到顶峰,占了政府预算的 4.5% ,如今,nasa 的预算只占 0.5% 。nasa 用这笔钱资助各种科学研究,阿波罗计划花的钱最多,雇了 40 万人左右,而且有 2 万多家大学和公司参与。其中一个挑战是 怎样在太空中导航,nasa 需要电脑计算复杂的轨道来引导太空船,因此,他们造了 \u0026ldquo;阿波罗导航计算机\u0026rdquo;,有 3 个重要要求:\n计算机要快,这在意料之中; 计算机要又小又轻。太空船里的空间不多,而且要飞去月球,能轻一点是一点; 要超级可靠。 这对太空船非常重要,因为太空中有很多震动,辐射,极端温度变化。如果东西坏掉了,可没办法去\u0026quot;百思买\u0026quot;买新的。那时的主流科技 真空管和晶体管 无法胜任这些要求,所以 nasa 用全新科技:集成电路。\n我们几集前聊过,阿波罗导航计算机 首先使用了集成电路,nasa 是唯一负担得起集成电路的组织。最初,一个芯片差不多 50 美金,导航计算机需要上千个芯片,但美国也因此成功登月,打败苏联。\n谢了 思想泡泡\n虽然人们经常把集成电路的发展 归功于阿波罗导航计算机,但它们的产量很低,一共只有 17 次阿波罗任务。实际上是军事 大大推进了集成电路发展,特别是洲际导弹和核弹,使集成电路大规模生产。美国建造强大计算机时,也进一步推进了集成电路,一般叫\u0026quot;超级计算机\u0026quot;,因为它们经常比全球最快电脑还快 10 倍以上,但 cdc,cray,ibm 制造的计算机非常昂贵,几乎只有政府负担得起,这些计算机用于政府机构,比如美国国家安全局,以及实验室比如 劳伦斯·利弗莫尔 实验室 、 洛斯·阿拉莫斯 国家实验室。\n最初,美国的半导体行业 靠高利润政府合同起步,因此忽略了消费者市场,因为利润小。因此日本半导体行业在 1950 和 1960 年代 靠低利润率占领了消费者市场,日本人投入大量资金,大量制造以达到规模经济,同时研究技术,提高质量和产量 以及用自动化来降低成本。1970 年代,太空竞赛和冷战逐渐消退 高利润的政府合同变少,美国的半导体和电子设备公司发现更难竞争了。虽然很多计算机组件商品化了,但并没有什么帮助。dram 就是 dram ,能从日立买便宜的,干嘛要从英特尔买贵的? 1970 年代 美国公司开始缩小,合并,或直接倒闭 。1974 年 英特尔不得不裁员三分之一 ,知名的仙童半导体也在 1979 年濒临倒闭 ,被其他公司收购了。为了生存,很多公司把生产外包出去,降低成本。英特尔不再把精力放在 内存集成电路, 而是把精力放在处理器,这个决定最后挽救了公司。美国公司的无力 ,导致 夏普 和 卡西欧 这样的日本公司占领了 1970 年代的主流产品 - 手持计算器。因为集成电路,计算机又小又便宜。,取代了办公室里昂贵的桌面计算器。对大多数人 这是他们第一次不必用纸笔和计算尺来做计算,手持计算机因此大卖,进一步降低了集成电路的成本,使得微处理器被广泛使用,比如之前讨论过的 intel 4004 。intel 在 1971 年 应日本计算器公司 busicom 的要求做了这个芯片,很快,日本电子产品到处都是,从电视到手表到随身听,而廉价的微处理器,也催生了全新的产品,比如街机游戏。1972 年诞生了 pong,1976 年诞生了打砖块。因为成本不断下降,很快,普通人也买得起计算机了,这段期间,第一批家用电脑开始出现,比如 1975 年的 altair 8800,以及第一款家用游戏机,比如 1977 年的 atari 2600 。家用!我再说一遍 家用!如今没什么大不了的,但那时是计算机的全新时代!\n在短短三十年内,计算机从大到人类可以在 cpu 里走来走去(当然,你要有政府许可你这样做),发展到小到小孩都能拿住的手持玩具,而且微处理器还快得多。这种巨大变化是由两种力量推动的:政府和消费者!政府资金,比如冷战期间美国投入的钱,推动了计算机的早期发展,并且让计算机行业活得足够久,使得技术成熟到可以商用。然后是公司,最后是消费者,把计算机变成了主流。冷战虽然结束了,但这种关系今天仍在继续。政府依然在资助科学研究,情报机构依然在超级计算机,人类仍然被发射到太空里,而你依然在买电视,xbox,playstation,笔记本电脑和手机。\n因此,计算机会继续飞速发展。\n我们下周见。\n25. 个人计算机革命 i.e. the personal computer revolution\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n上周说过\u0026quot;个人计算机\u0026quot;的概念 ,在计算机发展的头 30 年难以想象,如果只让一个人用,成本实在太高。但到 70 年代初,各种组件的成本都下降了 可以做出低成本 同时性能足够强大的计算机。不是玩具级计算机,是真正能用的计算机。这个转变中 最有影响力的是 单芯片 cpu 的出现,强大 + 体积小 + 便宜 ,集成电路的进步,也提供了低成本固态存储器,可以用于计算机的 ram 和 rom 。忽然间 把整台计算机做到一张电路板上成为可能,大大地降低了制造成本,而且,那时有便宜可靠的储存介质, 比如磁带和软盘,最后是 低成本的显示器 ,通常是电视机稍作改装而成。如果在 1970 年代 将这四种原料混在一起,就得到了\u0026quot;微型计算机\u0026quot;。因为和那个时代的\u0026quot;普通\u0026quot;计算机相比 ,这些计算机很小,\u0026ldquo;普通\u0026quot;计算机就是公司或大学里的那种。但比大小更重要的是成本,这是有史以来第一次,计算机的价格足够低,\u0026ldquo;一个人专用\u0026quot;的想法变得可行,不用划分时间和别人公用计算机,没有多用户登录,计算机只属于一个人,只有一个用户,个人计算机时代到来!\n计算机成本下降+性能提升,让个人计算机成为可能,但这个时间点很难准确定义,并没有一个具体时间点,因此\u0026quot;第一台个人计算机\u0026quot;这个名号,有很多竞争者,比如 kenback-1 和 mcm/70 。不过第一台取得商业成功的个人计算机 争议较小:altair 8800,首次亮相在 1975 年《popular electronics》封面,售价 $439 美元,需要自己组装。计算通货膨胀后,相当如今的 2000 美元左右,不算小钱,但比起 1975 年的其它计算机,算是非常便宜了!\n各种需要自己组装的组件包卖给了计算机爱好者,因为买的人多,很快相关产品出现了,比如内存,纸带读取器,甚至电传接口,让你可以从纸带上读取更长更复杂的程序,然后用电传终端交互,但程序还是要用 机器码 写,写起来很麻烦,即使计算机爱好者也讨厌写,这没有吓跑年轻的比尔·盖茨和保罗·艾伦!他们当时是 19 岁和 22 岁,他们联系了制造 altair 8800 的 mits 公司,建议说,如果能运行 basic 程序 会对爱好者更有吸引力。basic 是一门更受欢迎更简单的编程语言,为此,他们需要一个程序 把 basic 代码转成可执行机器码,这叫 解释器 (interpreter)。\u0026ldquo;解释器\u0026quot;和\u0026quot;编译器\u0026quot;类似,区别是\u0026quot;解释器\u0026quot;运行时转换, 而\u0026quot;编译器\u0026quot;提前转换。\n让我们进入思想泡泡!\nmits 表示感兴趣,同意与 bill 和 paul 见个面,让他们演示一下。问题是,他们还没写好解释器,所以他们花了几个星期赶工 ,而且还不是在 altair 8800 上写的,最后在飞机上完成了代码。他们在墨西哥 阿尔伯克基(城市) 的 mits 总部做演示时,才知道代码可以成功运行。幸运的是进展顺利 mits 同意在计算机上搭载他们的软件,altair basic 成了微软的第一个产品。\n虽然 1975 年之前就有计算机爱好者,但 altair 8800 大量催生了更多计算机爱好者,爱好者们组成各种小组 分享知识,软件,以及对计算机的热爱,最具传奇色彩的小组是\u0026quot;家酿计算机俱乐部\u0026rdquo;。第一次小组聚会在 1975 年 3 月,看一台第一批运来加州的 altair 8800 。第一次聚会上,24 岁的 steve wozniak 被 altair 8800 大大激励,开始想设计自己的计算机。1976 年 5 月,他向小组展示了原型机,并且把电路图分享给感兴趣的其他会员,他的设计不同寻常 要连到电视显示,并提供文本界面,在低成本计算机上还是第一次见。同是俱乐部成员和大学朋友的 史蒂夫·乔布斯 建议说与其免费分享设计,不如直接出售装好的主板,但用户依然需要自己加键盘,电源和机箱。1976 年 7 月开始发售,价格 $666.66 美元,它叫 apple-i ,苹果计算机公司的第一个产品。\n谢了 思想泡泡\n就像 altair 8800 一样,apple-i 也是作为套件出售,apple-i 吸引了业余爱好者 不介意机器买回来自己组装,但个人消费者和公司对 apple-i 不感兴趣。\n这在 1977 年发生变化 市场上有了三款开箱即用的计算机。\n第一款是 apple-ii ,苹果公司第一个提供全套设备的产品,设计和制造工艺都是专业的,它还提供了简单彩色图形和声音输出,这些功能对低成本机器非常了不起。apple-ii 卖了上百万套,把苹果公司推到了个人计算机行业的前沿。第二款是\u0026quot;trs-80 1 型\u0026rdquo;,由 tandy 公司生产,由 radioshack 销售,所以叫 trs。虽然不如 apple-ii 先进 但因为价格只有一半,所以卖得很火爆。最后一款是 commodore pet 2001 ,有一体化设计,集成了计算机,显示器,键盘和磁带驱动器,目标是吸引普通消费者。\n计算机和家用电器之间的界限开始变得模糊,这 3 台计算机被称为 1977 年的\u0026quot;三位一体\u0026rdquo; 。它们都自带了 basic 解释器,让不那么精通计算机的人也能用 basic 写程序,针对消费者的软件行业 开始腾飞。市场上出现了各种 针对个人计算机的游戏和生产力工具,比如计算器和文字处理器,最火的是 1979 年的 visicalc - 第一个电子表格程序,比纸好无数倍,是微软 excel 和 google sheets 的老祖先。\n但这些计算机带来的最大影响 也许是他们的营销策略,它们针对普通消费者, 而不是企业和爱好者。这是第一次大规模地,计算机出现在家庭,小公司,以及学校中,这引起了全球最大计算机公司 ibm 的注意,其市场份额从 1970 年的 60% 在 1980 年降到了 30%左右,因为 ibm 忽略了增长的\u0026quot;微型计算机\u0026quot;市场,这个市场每年增长约 40% 。随着微型计算机演变成个人计算机 ibm 知道他们需要采取行动,但要做到这一点 公司要从根本上重新思考战略和设计 。1980 年 ibm 最便宜的计算机 \u0026ldquo;5120\u0026quot;的价格大概是一万美元,永远也没法和 apple-ii 这样的计算机竞争,意味着要从头开始。一个由十二名工程师组成的精干团队(后来叫\u0026quot;肮脏十二人\u0026rdquo;),被派往佛罗里达州的 博卡拉顿(boca raton)办公室,让他们独立工作。不受 ibm 内部的政治斗争干扰 他们想怎么设计怎么设计,没用 ibm 的 cpu,选了 intel 的芯片,也没用 ibm 的首选操作系统 cp/m ,而是用了微软的 dos 。依此类推,从屏幕到打印机都这样自由选择 。ibm 第一次不得不与外部公司竞争,来给新计算机做硬件和软件,这和 ibm 的传统做法不同:自己做硬件来节省成本,然后和其它公司合作,经过短短一年,ibm 个人计算机发布了,简称 ibm pc,产品立马取得了成功。长期信任 ibm 品牌的企业买了很多,但最有影响力的是 它使用 \u0026ldquo;开放式架构\u0026rdquo;,有良好的文档和扩展槽,使得第三方可以做硬件/外设 - 包括显卡,声卡,外置硬盘,游戏控制杆 以及无数其它组件,这刺激了创新,激发了竞争,产生了巨大的生态系统,这个开放架构叫 ibm compatible\u0026quot;( ibm 兼容 ),意味着如果买了\u0026quot;ibm 兼容\u0026quot;的计算机,你可以用庞大生态系统中的其它软硬件。开放架构也意味着 竞争对手公司可以遵循这个标准,做出自己的\u0026quot;ibm 兼容\u0026quot;计算机。很快,康柏和戴尔也开始卖 pc ,微软很乐意把 ms-dos 授权给他们,使 dos 迅速成为最受欢迎的 pc 操作系统。仅在前三年 ibm 就卖出了 200 万台 pc ,超过了苹果。有了庞大用户群,软件和硬件开发人员 把精力放在\u0026quot;ibm 兼容\u0026quot;平台,因为潜在用户更多,同时,想买计算机的人 也会看哪种计算机的软硬件选择更多,就像雪球效应一样,而那些生产非\u0026quot;ibm 兼容\u0026quot;计算机的公司 (一般性能更好),都失败了。只有苹果公司在没有\u0026quot;ibm 兼容\u0026quot;的情况下 保持了足够市场份额,苹果公司最终选了相反的方式:\u0026ldquo;封闭架构\u0026rdquo;,即自己设计一切,用户一般无法加新硬件到计算机中,意味着苹果公司要做自己的计算机,自己的操作系统,还有自己的外围设备,如显示器,键盘和打印机。通过控制整个范围,从硬件到软件,苹果能控制用户体验并提高可靠性。不同的商业策略是 \u0026ldquo;mac vs pc 谁更好\u0026rdquo; 这种争论的起源,这些争论如今还存在 不过\u0026quot;mac vs pc\u0026quot;用词不对,因为它们都是个人计算机!但是随便啦!\n为了在低成本个人计算机的竞争冲击下生存下来,苹果需要提高自身水平 提供比 pc 和 dos 更好的用户体验,他们的答案是 macintosh,于 1984 年发布 - 一台突破性 价格适中的一体式计算机 ,用的不是命令行界面,而是图形界面!\n我们下周讨论图形界面。到时见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/15/","summary":"\u003cblockquote\u003e\n\u003cp\u003e后续章节为概念性章节,统一汇总在该章节内。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc15 - 其他 1"},{"content":"i.e. data structures\n上集讲了一些经典算法,比如给数组排序,找图的最短路径,而上集没讲的是 算法处理的数据 存在内存里的格式是什么。\n你肯定不想数据像 john green 的大学宿舍一样乱 ,到处都是食物,衣服和纸,我们希望数据是结构化的,方便读取,因此计算机科学家发明了 \u0026ldquo;数据结构\u0026rdquo;!\n上集已经介绍了一种基本数据结构:数组(array),也叫列表(list)或向量(vector)(在其它编程语言里)。数组的值一个个连续存在内存里,所以不像之前,一个变量里只存一个值(比如 j = 5),我们可以把多个值存在数组变量里。为了拿出数组中某个值,我们要指定一个下标(index),大多数编程语言里,数组下标都从 0 开始,用方括号 [ ] 代表访问数组。如果想相加数组 j 的第一个和第三个元素,把结果存在变量 a,可以写上图这样一行代码。\n数组存在内存里的方式十分易懂。\n为了简单,假设编译器从内存地址 1000 开始存数组,数组有 7 个数字,像上图一样按顺序存。写 j[0],会去内存地址 1000 加 0 个偏移,得到地址 1000,拿值:5 ,如果写 j[5],会去内存地址 1000 加 5 个偏移,得到地址 1005,拿值: 4 。很容易混淆 \u0026ldquo;数组中第 5 个数\u0026rdquo; 和 \u0026ldquo;数组下标为 5 的数\u0026rdquo;,它们不是一回事,记住,下标 5 其实是数组中第 6 个数,因为下标是从 0 开始算的。\n数组的用途广泛。所以几乎所有编程语言都自带了很多函数来处理数组,举例,数组排序函数很常见,只需要传入数组,就会返回排序后的数组,不需要写排序算法。\n数组的亲戚是 字符串 (string),其实就是字母,数字,标点符号等 组成的数组。第 4 集讨论过计算机怎么存储字符,写代码时 用引号括起来就行了 j = \u0026quot;stan rocks\u0026quot; 。虽然长的不像数组,但的确是数组,幕后看起来像这样。\n注意,字符串在内存里以 0 结尾。不是\u0026quot;字符 0\u0026quot;,是\u0026quot;二进制值 0\u0026quot;,这叫字符\u0026quot;null\u0026quot;,表示字符串结尾。这个字符非常重要,如果调用 print 函数,print 在屏幕上输出字符串,会从开始位置,逐个显示到屏幕,但得知道什么时候停下来!否则会把内存里所有东西 都显示出来,0 告诉函数何时停下。\n因为计算机经常处理字符串,所以有很多函数专门处理字符串,比如连接字符串的 strcat,strcat 接收两个字符串,把第二个放到第一个结尾。\n我们可以用数组做一维列表(one dimensional lists),但有时想操作二维数据,比如电子表格,或屏幕上的像素,那么需要 矩阵(matrix)- 可以把矩阵看成 数组的数组!\n一个 3x3 矩阵就是一个长度为 3 的数组 ,数组里每个元素都是一个长度为 3 的数组。可以这样初始化,内存里是这样排列的。为了拿一个值,需要两个下标,比如 j[2][1] ,告诉计算机在找数组 2 里,位置是 1 的元素,得到数字 12 。\n矩阵酷的地方是,不止能做 3x3 的矩阵,任何维度都行,可以做一个 5 维矩阵,然后这样访问 a = j[2][0][18][18][3] 。现在你知道了,怎么读一个 5 维矩阵,快去告诉你的朋友!\n目前我们只存过单个数字/字符,存进数组或矩阵,但有时,把几个有关系的变量存在一起,会很有用。比如银行账户号和余额,多个变量打包在一起叫 结构体 (struct)。现在多个不同类型数据,可以放在一起,甚至可以做一个数组,里面放很多结构体,这些数据在内存里会自动打包在一起。如果写 j[0],能拿到 j[0] 里的结构体,然后拿银行账户和余额。存结构体的数组,和其它数组一样,创建时就有固定大小,不能动态增加大小,还有,数组在内存中 按顺序存储,在中间插入一个值很困难。\n结构体可以创造更复杂的数据结构,消除这些限制,我们来看一个结构体,叫 节点 (node),它存一个变量 - 一个指针(pointer)。\u0026ldquo;指针\u0026rdquo; 是一种特殊变量,指向一个内存地址,因此得名。用 节点 可以做 链表(linked list),链表是一种灵活数据结构,能存很多个 节点 (node),灵活性是通过每个节点 指向 下一个节点实现的。\n假设有三个节点,在内存地址 1000,1002, 1008,隔开的原因 可能是创建时间不同,它们之间有其他数据。可以看到第一个节点,值是 7,指向地址 1008,代表下一个节点,位于内存地址 1008,现在来到下一个节点,值是 112,指向地址 1002,如果跟着它,会看到一个值为 14 的节点,这个节点 指回地址 1000,也就是第一个节点,这叫 循环链表 。\n但链表也可以是非循环的,最后一个指针是 0 - \u0026ldquo;null\u0026rdquo;,代表链表尽头。当程序员用链表时,很少看指针具体指向哪里,而是用链表的抽象模型,就像上图,更容易看懂。\n数组大小需要预先定好,链表大小可以动态增减。可以创建一个新节点,通过改变指针值,把新节点插入链表;链表也很容易重新排序,两端缩减,分割,倒序等。超方便!\n链表也适合上集的排序算法,因为灵活,很多复杂数据结构 都用链表,最出名的是 队列(queue)和 栈。\u0026ldquo;队列\u0026rdquo; 就像邮局排队,谁先来就排前面,虽然你可能只想买邮票,而前面的人要寄 23 个包裹,这叫 先进先出(fifo)。我指队列,不是指那 23 个包裹。想象有个指针叫\u0026quot;邮局队列\u0026quot;,指向链表第一个节点。第一个节点是 hank,服务完 hank 之后 读取 hank 的指针,把\u0026quot;邮局队列\u0026quot;指向下一个人,这样就把 hank \u0026ldquo;出队\u0026rdquo;(dequeue)了。如果我们想把某人\u0026quot;入队\u0026quot;(enqueue) - 意思是加到队列里,要遍历整个链表到结尾,然后把结尾的指针,指向新人(nick)。\n只要稍作修改,就能用链表做 栈,栈是后进先出 (lifo)。可以把\u0026quot;栈\u0026quot;想成一堆松饼。做好一个新松饼,就堆在之前上面,吃的时候,是从最上面开始。美味!栈就不叫\u0026quot;入队\u0026quot;\u0026ldquo;出队\u0026quot;了,叫\u0026quot;入栈\u0026rdquo;(push) \u0026ldquo;出栈\u0026rdquo;(pop)。对,这些是正确术语!\n如果节点改一下,改成 2 个指针,就能做 树(tree)。很多算法用了 \u0026ldquo;树\u0026rdquo; 这种数据结构。同样,程序员很少看指针的具体值,而是把\u0026quot;树\u0026quot;抽象成这样:最高的节点叫\u0026quot;根节点\u0026quot;(root),\u0026ldquo;根节点\u0026quot;下的所有节点都叫\u0026quot;子节点\u0026rdquo;(children)。任何子节点的直属上层节点,叫\u0026quot;母节点\u0026quot;(parent node)。没有任何\u0026quot;子节点\u0026quot;的节点,也就是\u0026quot;树\u0026quot;结束的地方,叫\u0026quot;叶节点\u0026quot;(leaf)\n在这里的例子中,节点最多只可以有 2 个子节点,因此叫 二叉树(binary tree)。但你可以随便改,弄成 3 个,4 个,或更多,甚至节点 可以用链表存所有子节点。\u0026ldquo;树\u0026quot;的一个重要性质是(不管现实中还是数据结构中),\u0026ldquo;根\u0026quot;到\u0026quot;叶\u0026quot;是 单向 的。如果根连到叶,叶连到根,就很奇怪。\n如果数据随意连接,包括循环,可以用\u0026quot;图\u0026quot;表示,还记得上集用路连接城市的\u0026quot;图\u0026quot;吗?这种结构可以用有多个指针的节点表示,因此没有 根、叶、子节点、父节点这些概念,可以随意指向!\n以上概述了计算机科学中,最主要的一些数据结构,这些基本结构之上,程序员做了各种新变体,有不同性质。比如\u0026quot;红黑树\u0026quot;和\u0026quot;堆\u0026rdquo;,我们没时间讲。\n不同数据结构适用于不同场景,选择正确数据结构会让工作更简单,所以花时间考虑用什么数据结构是值得的。幸运的是,大多数编程语言自带了预先做好的数据结构。比如,c++有\u0026quot;标准模板库\u0026rdquo;,java 有\u0026quot;java 类库\u0026quot;,程序员不用浪费时间从零写,时间可以花在更有趣的事情。\n又提升了一层抽象!\n下周见!\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/14/","summary":"\u003cp\u003ei.e. Data Structures\u003c/p\u003e\n\u003cp\u003e上集讲了一些经典算法,比如给数组排序,找图的最短路径,而上集没讲的是 算法处理的数据 存在内存里的格式是什么。\u003c/p\u003e\n\u003cp\u003e你肯定不想数据像 John Green 的大学宿舍一样乱 ,到处都是食物,衣服和纸,我们希望数据是结构化的,方便读取,因此计算机科学家发明了 \u0026ldquo;数据结构\u0026rdquo;!\u003c/p\u003e","title":"cscc14 - 数据结构"},{"content":"i.e. intro to algorithms\n前两集,我们\u0026quot;初尝\u0026quot;了高级编程语言(比如 python 和 java),我们讨论了几种语句 - 赋值语句,if 语句,循环语句,以及把代码打包成 \u0026ldquo;函数\u0026rdquo;,比如算指数。重要的是,之前写的指数函数只是无数解决方案的一种,还有其它方案 - 用不同顺序写不同语句,也能得到一样结果。\n不同的是\u0026quot;算法\u0026quot;,意思是:解决问题的具体步骤,即使结果一致,有些算法会更好。 一般来说,所需步骤越少越好,不过有时我们也会关心其他因素,比如占多少内存。\n\u0026ldquo;算法\u0026rdquo; 一词来自 波斯博识者 阿尔·花拉子密, 1000 多年前的代数之父之一 ,如何想出高效算法 - 是早在计算机出现前就有的问题,诞生了专门研究计算的领域,然后发展成一门现代学科。\n你猜对了!计算机科学!\n记载最多的算法之一是\u0026quot;排序\u0026quot; ,比如给名字、数字排序。排序到处都是,找最便宜的机票、按最新时间排邮件、按姓氏排联系人,这些都要排序。你可能想\u0026quot;排序看起来不怎么难… 能有几种算法呢?\u0026ldquo;答案是超多!\n计算机科学家花了数十年发明各种排序算法。还起了酷酷的名字,\u0026ldquo;冒泡排序\u0026rdquo; (bubble sort)、\u0026ldquo;意面排序\u0026rdquo;(spaghetti sort),我们来试试排序!\n选择排序 试想有一堆机票价格,都飞往印第安纳波利斯 (美国地名),数据具体怎么在内存中表示 下周再说。\n上图的这样一组数据 叫\u0026quot;数组\u0026rdquo;(array)。\n来看看怎么排序,先从一种简单算法开始。\n- 先找到最小数,从最上面的 307 开始,因为现在只看了这一个,所以它是最小数\r- 下一个是 239,比 307 小,所以新的最小数变成 239\r- 下一个是 214 ,新的最小数\r- 250 不是,384, 299, 223, 312 都不是\r- 现在扫完了所有数字,214 是最小的\r- 为了升序排列(从小到大排序),把 214 和最上面的数字,交换位置 好棒!刚排序了一个数字!现在重复同样的过程!\n- 这次不从最上面开始,从第 2 个数开始,先看到 239,我们当作是 \u0026#34;最小数\u0026#34;\r- 扫描剩下的部分,发现 223 最小,所以把它和第 2 位交换\r- 重复这个过程,从第 3 位数字开始,让 239 和 307 互换位置\r- 重复直到最后一个数字 瞧,数字排好了,可以买机票了!\n刚刚这种方法,或者说算法,叫 选择排序 - 非常基础的一种算法。\n以下是\u0026quot;伪代码\u0026quot;(pseudo-code)。\n这个函数可以排序 8 个,80 个或 8 千万个数字,函数写好了就可以重复使用,这里用循环遍历数组,每个数组位置都跑一遍循环,找最小数然后互换位置。可以在代码中看到这一点 (一个 for 循环套另一个 for 循环),这意味着,大致来说,如果要排 n 个东西,要循环 n 次,每次循环中再循环 n 次,共 n*n 。\n算法的 输入大小 和 运行步骤 之间的关系,叫算法的 复杂度 ,表示运行速度的量级。计算机科学家们把算法复杂度叫 - 没开玩笑 - 大 o 表示法 。\n算法复杂度 o(n) 效率不高。前面的例子有 8 个元素(n=8), 8 = 64,如果 8 个变 80 个,运行时间变成 80 = 6400,虽然大小只增长了 10 倍(8 到 80),但运行时间增加了 100 倍!(64 到 6400 )。随着数组增大,对效率的影响会越来越大,这对大公司来说是个问题,比如 谷歌,要对几十亿条信息排序。\n= 数据量上来了,一切都会变得复杂了!*\n作为未来的计算机科学家你可能会问:有没有更高效的排序算法?\n归并排序 回到未排序的数组,试另一个算法 \u0026ldquo;归并排序\u0026rdquo;。\n- 第一件事是检查数组大小是否 \u0026gt; 1\r- 如果是,就把数组分成两半\r- 因为数组大小是 8,所以分成两个数组,大小是 4\r- 但依然大于 1,所以再分成大小是 2 的数组\r- 最后变成 8 个数组,每个大小为 1\r- 现在可以\u0026#34;归并\u0026#34;了,\u0026#34;归并排序\u0026#34;因此得名\r- - 从前两个数组开始,读第一个(也是唯一一个)值\r- 307 和 239\r- 239 更小,所以放前面\r- 剩下的唯一数字是 307 ,所以放第二位\r- 成功合并了两个数组 重复这个过程,按序排列,然后再归并一次。\n- 同样,取前两个数组,比较第一个数\r- 239 和 214 - 214 更小,放前面\r- - 再看两个数组里的第一个数:239 和 250\r- 239 更小,所以放下一位\r- - 看剩下两个数:307 和 250\r- 250 更小,所以放下一位\r- - 最后剩下 307 ,所以放最后\r- - 每次都以 2 个数组开始\r- 然后合并成更大的有序数组 我们把刚隐藏起来的,下面的数组也这样做。\n现在有两个大小是 4 的有序数组,就像之前,比较两个数组的第一个数,取最小数,重复这个过程,直到完成,就排好了!\n但坏消息是:无论排多少次,你还是得付 214 美元到印第安纳波利斯。总之,\u0026ldquo;归并排序\u0026quot;的算法复杂度是 o(n * log n),\nn 是需要 比较+合并 的次数,和数组大小成正比 log n 是合并步骤的次数 例子中把大小是 8 的数组,分成四个数组,然后分成 2 个,最后分成 1 个,分了 3 次。重复切成两半,和数量成对数关系\n相信我!\nlog_2 8=3\n如果数组大小变成 16 - 之前的两倍,也只要多分割 1 次,因为 log_2 16=4 ,即使扩大一千倍,从 8 到 8000,分割次数也不会增大多少 - log_2 8000≈13 ,13 比 3 只是 4 倍多一点,然而排序的元素多得多,因此\u0026quot;归并排序\u0026quot;比\u0026quot;选择排序\u0026quot;更有效率。\n这下我收藏的陶瓷猫 可以更快排序了!\n有好几十种排序算法,但没时间讲。\n图搜索 所以我们来谈一个经典算法问题:图搜索(graph search)。\n\u0026ldquo;图\u0026rdquo; 是用线连起来的一堆 \u0026ldquo;节点\u0026rdquo;,可以想成地图,每个节点是一个城市,线是公路。一个城市到另一个城市,花的时间不同,可以用 成本 (cost) 或 权重 (weight) 来代称,代表要几个星期。假设想找\u0026quot;高庭\u0026quot;到\u0026quot;凛冬城\u0026quot;的最快路线,最简单的方法是尝试每一条路,计算总成本,这是蛮力方法。假设用蛮力方法 来排序数组,尝试每一种组合,看是否排好序,这样的时间复杂度是 o(n!),n 是节点数,n! 是 n 乘 n-1 乘 n-2\u0026hellip; 一直到 1,比 o(n ) 还糟糕。\n我们可以更聪明些!\n图搜索问题的经典算法发明者是理论计算机科学的伟人 edsger dijkstra,所以叫 \u0026ldquo;dijkstra 算法\u0026rdquo;。从\u0026quot;高庭\u0026quot;开始,此时成本为 0,把 0 标在节点里,其他城市标成问号,因为不知道成本多少,dijkstra 算法总是从成本最低的节点开始,目前只知道一个节点 \u0026ldquo;高庭\u0026rdquo;, 所以从这里开始,跑到所有相邻节点,记录成本,完成了一轮算法,但还没到\u0026quot;凛冬城\u0026rdquo;,所以再跑一次 dijkstra 算法,\u0026ldquo;高庭\u0026rdquo; 已经知道了,下一个成本最低的节点,是 \u0026ldquo;君临城\u0026rdquo;。就像之前,记录所有相邻节点的成本,到\u0026quot;三叉戟河\u0026quot;的成本是 5,然而我们想记录的是,从\u0026quot;高庭\u0026quot;到这里的成本,所以\u0026quot;三叉戟河\u0026quot;的总成本是 8+5=13 周,现在走另一条路到\u0026quot;奔流城\u0026quot;,成本高达 25 ,总成本 33,但 \u0026ldquo;奔流城\u0026rdquo; 中最低成本是 10,所以无视新数字,保留之前的成本 10,现在看了\u0026quot;君临城\u0026quot;的每一条路,还没到\u0026quot;凛冬城\u0026quot; 所以继续。下一个成本最低的节点,是\u0026quot;奔流城\u0026quot;,要 10 周,先看 \u0026ldquo;三叉戟河\u0026rdquo; 成本: 10+2=12,比之前的 13 好一点,所以更新 \u0026ldquo;三叉戟河\u0026rdquo; 为 12,\u0026ldquo;奔流城\u0026quot;到\u0026quot;派克城\u0026quot;成本是 3,10+3=13,之前是 14,所以更新 \u0026ldquo;派克城\u0026rdquo; 为 13。\u0026ldquo;奔流城\u0026quot;出发的所有路径都走遍了, 你猜对了,再跑一次 dijkstra 算法。下一个成本最低的节点,是\u0026quot;三叉戟河\u0026rdquo;,从\u0026quot;三叉戟河\u0026quot;出发,唯一没看过的路,通往\u0026quot;凛冬城\u0026rdquo;!成本是 10,加\u0026quot;三叉戟河\u0026quot;的成本 12,总成本 22。再看最后一条路,\u0026ldquo;派克城\u0026quot;到\u0026quot;凛冬城\u0026rdquo;,成本 31。现在知道了最低成本路线,让军队最快到达,还绕过了\u0026quot;君临城\u0026quot;!\ndijkstra 算法的原始版本,构思于 1956 年,算法复杂度是 o(n )。前面说过这个效率不够好,意味着输入不能很大,比如美国的完整路线图,幸运的是,dijkstra 算法几年后得到改进,变成 o(n log n + l)。\n\u0026gt; n 是节点数,l 是多少条线\n虽然看起来更复杂,但实际更快一些。用之前的例子,可以证明更快(6 个节点 9 条线),从 36 减少到 14 左右。\n就像排序,图搜索算法也有很多,有不同优缺点。每次用谷歌地图时,类似 dijkstra 的算法就在服务器上运行,找最佳路线,算法无处不在,现代世界离不开它们。\n这集只触及了算法的冰山一角。\n但成为计算机科学家的核心,是根据情况合理决定,用现有算法,还是自己写新算法。\n希望这集的小例子能让你体会到这点。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/13/","summary":"\u003cp\u003ei.e. Intro to Algorithms\u003c/p\u003e\n\u003cp\u003e前两集,我们\u0026quot;初尝\u0026quot;了高级编程语言(比如 Python 和 Java),我们讨论了几种语句 - 赋值语句,if 语句,循环语句,以及把代码打包成 \u0026ldquo;函数\u0026rdquo;,比如算指数。重要的是,之前写的指数函数只是无数解决方案的一种,还有其它方案 - 用不同顺序写不同语句,也能得到一样结果。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e不同的是\u0026quot;算法\u0026quot;,意思是:解决问题的具体步骤,即使结果一致,有些算法会更好。\u003c/strong\u003e 一般来说,所需步骤越少越好,不过有时我们也会关心其他因素,比如占多少内存。\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;算法\u0026rdquo; 一词来自 波斯博识者 阿尔·花拉子密, 1000 多年前的代数之父之一 ,如何想出高效算法 - 是早在计算机出现前就有的问题,诞生了专门研究计算的领域,然后发展成一门现代学科。\u003c/p\u003e\n\u003cp\u003e你猜对了!计算机科学!\u003c/p\u003e","title":"cscc13 - 算法入门"},{"content":"i.e. programming basics - statements \u0026amp; functions\n上集讲到用机器码写程序,还要处理那么多底层细节,对写大型程序是个巨大障碍。为了脱离底层细节,开发了编程语言,让程序员专心解决问题,不用管硬件细节。\n= 底层各类太多了,编写麻烦,调试麻烦,改动麻烦……*\n语句 今天我们讨论大多数编程语言都有的基本元素,就像口语一样,编程语言有\u0026quot;语句\u0026quot;。语句表达单个完整思想,比如\u0026quot;我想要茶\u0026quot;或者\u0026quot;在下雨\u0026quot;,用不同词汇可以代表不同含义,比如\u0026quot;我想要茶\u0026quot;变成\u0026quot;我想要独角兽\u0026quot;,但没法把\u0026quot;我想要茶\u0026quot;改成\u0026quot;我想要雨\u0026quot;- 语法毫无意义。\n规定句子结构的一系列规则叫 语法 (syntax),英语有语法,所有编程语言也都有语法。 a = 5 是一个编程语言语句,意思是创建一个叫 a 的变量,把数字 5 放里面,这叫\u0026quot;赋值语句\u0026quot;,把一个值赋给一个变量。\n为了表达更复杂的含义,需要更多语句,比如 a = 5 b = 10 c = a + b ,意思是,变量 a 设为 5,变量 b 设为 10 ,把 a 和 b 加起来,把结果 15 放进变量 c 。注意,变量名可以随意取。除了 a b c,也可以叫苹果、梨、水果。计算机不在乎你取什么名,只要不重名就行,当然取名最好还是有点意义,方便别人读懂。\n程序由一个个指令组成 ,有点像菜谱:烧水、加面,等 10 分钟,捞出来就可以吃了。程序也是这样,从第一条语句开始,一句一句运行到结尾。\n刚才我们只是把两个数字加在一起,无聊,我们来做一款游戏吧 !🤖🔫 当然,现在这个学习阶段,来编写一整个游戏还太早了,所以我们只写一小段一小段的代码,来讲解一些基础知识。\n假设我们在写一款老派街机游戏:grace hopper 拍虫子,阻止虫子飞进计算机造成故障,关卡越高,虫子越多。grace 要在虫子损坏继电器之前抓住虫子,好消息是她有几个备用继电器。\n开始编写时,我们需要一些值来保存游戏数据,比如当前关卡数、分数、剩余虫子数、grace 还剩几个备用继电器,所以我们要\u0026quot;初始化\u0026quot;变量 ,\u0026ldquo;初始化\u0026quot;的意思是设置最开始的值。\n关卡=1 分数=0 虫子数=5 备用继电器=4 玩家名=andre 为了做成交互式游戏,程序的执行顺序要更灵活,不只是从上到下执行,因此用 \u0026ldquo;控制流语句\u0026rdquo;。控制流语句有好几种,最常见的是 if 语句,可以想成是 \u0026ldquo;如果 x 为真,那么执行 y\u0026rdquo;,用英语举例就是 \u0026ldquo;如果累了,就去喝茶\u0026rdquo;,如果 \u0026ldquo;累了\u0026rdquo; 为真,就去喝茶,如果 \u0026ldquo;累了\u0026rdquo; 为假,就不喝茶。\nif 语句就像岔路口,走哪条路取决于 \u0026ldquo;表达式\u0026rdquo; 的真假,因此这些表达式又叫 \u0026ldquo;条件语句\u0026rdquo;。在大多数编程语言中,if 语句看起来像这样: if [条件], then [一些代码],结束 if 语句 。比如, if [第一关],then [分数设为 0] ,因为玩家才刚开始游戏,同时把虫子数设为 1,让游戏简单些。\n注意,依赖于 if 条件的代码,要放在 if 和 end if 之间,当然,条件表达式 可以改成别的,比如: \u0026quot;分数 \u0026gt;10\u0026quot; 或者 \u0026quot;虫子数 \u0026lt;1\u0026quot; 。\nif 还可以和 else 结合使用,条件为假会执行 else 里的代码。如果不是第 1 关,else 里的指令就会被执行,grace 要抓的虫子数,是当前关卡数 * 3 ,所以第 2 关有 6 个虫子,第 3 关有 9 个虫子,以此类推。else 中没有改分数,所以 grace 的分数不会变。\n这里列了一些热门编程语言 if-then-else 的具体语法。具体语法略有不同,但主体结构一样。\nif 语句 根据条件执行一次。如果希望根据条件执行多次,需要\u0026quot;条件循环\u0026rdquo;。比如 while 语句,也叫 \u0026ldquo;while 循环\u0026rdquo;,当 while 条件为真,代码会重复执行。不管是哪种编程语言,结构都是这样。假设到达一定分数会冒出一个同事,给 grace 补充继电器,棒极了!把继电器补满到最大数 4 个 , 我们可以用 while 语句来做。\n来过一遍代码。\n假设同事入场时, grace 只剩一个继电器。当执行 while 循环,第一件事是检查条件 - 继电器数量\u0026lt;4? ,继电器数量现在是 1,所以是真,进入循环!\n碰到这一行: 继电器数量 = 继电器数量 + 1 。看起来有点怪,变量的赋值用到了自己。\n我们讲下这个,总是从等号右边开始,\u0026quot;继电器数量+1\u0026quot; 是多少? 当前值是 1,所以 1+1=2 ,结果存到\u0026quot;继电器数量\u0026quot;,覆盖旧的值,所以现在继电器数量是 2 。\n现在到了结尾,跳回开始点。和之前一样,先判断条件,看要不要进入循环 - 继电器数量\u0026lt;4? 。是,继电器数量是 2,所以再次进入循环! 2+1=3 ,3 存入\u0026quot;继电器数量\u0026quot; 。回到开头 。3\u0026lt;4? 是!进入循环。 3+1=4 ,4 存入\u0026quot;继电器数量\u0026quot;,回到开头。 4\u0026lt;4? ,不!现在条件为假,退出循环,执行后面的代码。\nwhile 循环就是这样运作的!\n另一种常见的叫 \u0026ldquo;for 循环\u0026rdquo;,不判断条件,判断次数,会循环特定次数,看起来像上图。现在放些真正的值进去,上图例子会循环 10 次,因为设了变量 i ,从 1 开始,一直到 10 。for 的特点是,每次结束, i 会 +1 ,当 i 等于 10,就知道循环了 10 次,然后退出。我们可以用任何数字,10, 42, 10 亿 。\n假设每关结束后给玩家一些奖励分,奖励分多少取决于继电器剩余数量,随着难度增加,剩下继电器会越来越难。因此奖励分会根据当前关卡数,指数级增长,我们要写一小段代码来算指数。指数是一个数乘自己,乘特定次数。用循环来实现简直完美!\n首先,创建一个叫\u0026quot;奖励分\u0026quot;的新变量,设为 1 (看上图),然后 for 循环,从 1 到 [当前关卡数] ,[奖励分] x [继电器剩余数],结果存入 [奖励分] ,比如继电器数是 2,关卡数是 3 ,for 会循环 3 次,奖励分会乘 继电器数量 x 继电器数量 x 继电器数量 ,也就是 1×2×2×2,奖励分是 8,2 的 3 次方。这个指数代码很实用,其他地方可能会用到。如果每次想用就复制粘贴,会很麻烦,每次都要改变量名。如果代码发现问题,要补漏洞时,要把每一个复制黏贴过的地方都找出来改,而且会让代码更难懂。\n少即是多!\n我们想要某种方法,把代码\u0026quot;打包\u0026quot;,可以直接使用,得出结果,不用管内部复杂度。\n这又提升了一层抽象!\n函数 为了隐藏复杂度,可以把代码打包成 \u0026ldquo;函数\u0026rdquo;,也叫 \u0026ldquo;方法\u0026rdquo; 或 \u0026ldquo;子程序\u0026rdquo;(有些编程语言这么叫)。其他地方想用这个函数,直接写函数名就可以了。\n现在我们把指数代码变成函数。\n第一步,取名。叫什么都行,比如\u0026quot;快乐独角兽\u0026quot;,但因为是算指数,直接叫\u0026quot;指数\u0026quot;合适一些。\n还有,与其用特定变量名,比如 \u0026ldquo;继电器\u0026rdquo; 和 \u0026ldquo;关卡数\u0026rdquo;,用更通用的名字,比如 底数 (base) 和 指数 (exp),base 和 exp 的初始值需要外部传入,剩余代码和之前一样。现在完成了,有函数名和新变量名。\n最后,我们还需要把结果 交给使用这个函数的代码,所以用 return 语句,指明返回什么。\n完整版代码是这样!\n现在可以随意用这个函数,只需要写出名字 然后传入 2 个数字就可以了。如果要算 2 的 44 次方,写 exponent(2,44) ,结果是 18 万亿左右。幕后原理是,2 和 44 存进 base 和 exp ,跑循环,然后返回结果。我们来用这个新函数 算奖励分,首先,奖励分初始化为 0 ,然后用 if 语句,看剩不剩继电器(看上图的 \u0026gt; 0),如果还剩,用指数函数,传入 [继电器数] 和 [关卡数] ,它会算 [继电器数] 的 [关卡数] 次方,存入奖励分。这段算奖励分的代码,之后可能还会用,也打包成一个函数。没错,这个函数 (calcbonus) 会调用另一个函数 (exponent) 。还有!这个 calcbonus 函数,可以用在其他更复杂的函数 。\n我们来写一个函数,每一关结束后都会调用,叫 levelfinished (关卡结束)- 需要传入 [剩余继电器数] 、 [关卡数] 、[当前分] ,这些数据必须传入。里面用 calcbonus 算奖励分,并加进总分,还有,如果 当前分 \u0026gt; 游戏最高分 ,把新高分和玩家名 存起来,现在代码变得蛮\u0026quot;花哨\u0026quot;了。函数调函数调函数。\n我们写这样一行代码时,复杂度都隐藏起来了,不需要知道内部的循环和变量,只知道结果会像魔术一样返回,总分 53 。 但是这不是魔术,是抽象的力量! 如果你理解了这个例子,就明白了函数的强大之处和现代编程的核心。\n比如浏览器这样的复杂程序,用一长串语句来写是不可能的,会有几百万行代码,没人能理解。所以现代软件由上千个函数组成,每个负责不同的事。如今超过 100 行代码的函数很少见,如果多于 100 行,应该有东西可以拆出来做成一个函数。模块化编程不仅可以让单个程序员独立制作 app,也让团队协作可以写更大型的程序。不同程序员写不同函数,只需要确保自己的代码工作正常,把所有人的拼起来,整个程序也应该能正常运作!\n现实中,程序员不会浪费时间写指数函数这种东西,现代编程语言有很多预先写好的函数集合,叫 \u0026ldquo;库\u0026rdquo; ,由专业人员编写,不仅效率高,而且经过了仔细检查。几乎做所有事情都有库,网络、图像、声音。\n我们之后会讲这些主题。\n但在此之前,我们先讲算法。好奇吗?应该!\n下周见\n= 该章节其实没有多少内容,简单介绍了语句和函数,其中的例子用语言描述起来简直‘累’,还是动画来的直观。*\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/12/","summary":"\u003cp\u003ei.e. Programming Basics - Statements \u0026amp; Functions\u003c/p\u003e\n\u003cp\u003e上集讲到用机器码写程序,还要处理那么多底层细节,对写大型程序是个巨大障碍。为了脱离底层细节,开发了编程语言,让程序员专心解决问题,不用管硬件细节。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 底层各类太多了,编写麻烦,调试麻烦,改动麻烦……*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc12 - 编程原理-语句和函数"},{"content":"i.e. the first programming languages\n之前我们把重点放在硬件 - 组成计算机的物理组件,比如电,电路,寄存器,ram,alu,cpu ,但在硬件层面编程非常麻烦。\n所以程序员想要一种更通用的方法编程 - 一种\u0026quot;更软的\u0026quot;媒介。\n没错,我们要讲软件!\n第 8 集我们一步步讲了一个简单程序,第一条指令在内存地址 0:0010 1110 ,之前说过,前 4 位是操作码 ,简称 opcode 。对于这个假设 cpu,0010 代表 load_a 指令 - 把值从内存复制到寄存器 a ,后 4 位是内存地址,1110 是十进制的 14 ,所以这 8 位表达的意思是 \u0026ldquo;读内存地址 14,放入寄存器 a\u0026rdquo;。\n只是用了两种不同语言,可以想成是英语和摩尔斯码的区别 - \u0026ldquo;你好\u0026rdquo; 和 \u0026ldquo;\u0026hellip;. . .-.. .-.. \u0026mdash;\u0026rdquo; 是一个意思:你好。只是编码方式不同,英语和摩尔斯码的复杂度也不同,英文有 26 个字母以及各种发音,摩尔斯码只有\u0026quot;点\u0026quot;和\u0026quot;线\u0026quot;,但它们可以传达相同的信息,计算机语言也类似。\n机器语言 计算机能处理二进制,二进制是处理器的\u0026quot;母语\u0026quot;,事实上,它们只能理解二进制,这叫 \u0026ldquo;机器语言\u0026rdquo; 或 \u0026ldquo;机器码\u0026rdquo;。\n在计算机早期阶段,必须用机器码写程序。具体来讲,会先在纸上用英语写一个\u0026quot;高层次版\u0026quot;。举例:\u0026ldquo;从内存取下一个销售额,然后加到天、周、年的总和,然后算税\u0026rdquo;,等等。.. 这种对程序的高层次描述,叫 \u0026ldquo;伪代码\u0026rdquo; 。在纸上写好后,用\u0026quot;操作码表\u0026quot;把伪代码转成二进制机器码,翻译完成后,程序可以喂入计算机并运行。\n汇编语言 你可能猜到了,很快人们就厌烦了。所以在 1940~1950 年代,程序员开发出一种新语言, 更可读、更高层次,每个操作码分配一个简单名字,叫\u0026quot;助记符\u0026quot;,\u0026ldquo;助记符\u0026quot;后面紧跟数据,形成完整指令。与其用 1 和 0 写代码,程序员可以写\u0026quot;load_a 14\u0026rdquo;,我们在第 8 集用过这个助记符,因为容易理解得多!\n当然,cpu 不知道 load_a 14 是什么,它不能理解文字,只能理解二进制。所以程序员想了一个技巧,写二进制程序来帮忙,它可以读懂文字指令,自动转成二进制指令,这种程序叫 - 汇编器(assembler)。汇编器读取用\u0026quot;汇编语言\u0026quot;写的程序,然后转成\u0026quot;机器码\u0026quot; ,\u0026ldquo;load_a 14\u0026rdquo; 是一个汇编指令的例子。\n= 对的,把翻译工作交给了 ‘汇编器’ - 一个二进制程序。🤣*\n随着时间推移,汇编器有越来越多功能,让编程更容易。\n其中一个功能是自动分析 jump 地址。\n这里有一个第 8 集用过的例子:注意,jump negative 指令跳到地址 5 ,jump 指令跳到地址 2 ,问题是,如果在程序开头多加一些代码,所有地址都会变,更新程序会很痛苦!所以汇编器不用固定跳转地址,而是让你插入可跳转的标签,当程序被传入汇编器,汇编器会自己搞定跳转地址。程序员可以专心编程,不用管底层细节。\n隐藏不必要细节来做更复杂的工作。\n我们又提升了一层抽象!🔆\n然而,即使汇编器有这些厉害功能,比如自动跳转。汇编只是修饰了一下机器码,一般来说,一条汇编指令对应一条机器指令,所以汇编码和底层硬件的连接很紧密。汇编器仍然强迫程序员思考用什么寄存器和内存地址,如果你突然要一个额外的数,可能要改很多代码。\n让我们进入思考泡泡\n葛丽丝·霍普博士 也遇到了这个问题,作为美国海军军官,她是哈佛 1 号计算机的首批程序员之一,这台机器我们在第 2 集提过。这台巨大机电野兽在 1944 年战时建造完成,帮助盟军作战,程序写在打孔纸带上,放进计算机执行。顺便一说,如果程序里有漏洞,真的就 直接用胶带来补\u0026quot;漏洞\u0026quot;。\n\u0026gt; 补‘漏洞’\nmark 1 的指令集非常原始,甚至没有 jump 指令,如果代码要跑不止一次,得把带子的两端连起来做成循环。换句话说,给 mark 1 编程简直是噩梦!\n战后,霍普继续在计算机前沿工作,为了释放电脑的潜力,她设计了一个高级编程语言,叫\u0026quot;算术语言版本 0\u0026quot;,简称\u0026quot;a-0\u0026quot; 。\n汇编与机器指令是一一对应的,但一行高级编程语言 可能会转成几十条二进制指令。为了做到这种复杂转换 hopper 在 1952 年创造了第一个编译器,编译器专门把高级语言 转成低级语言,比如汇编或机器码(cpu 可以直接执行机器码)。\n谢了 思想泡泡\n尽管\u0026quot;使编程更简单\u0026quot;很诱人,但很多人对霍普的点子持怀疑态度。她曾说 \u0026ldquo;我有能用的编译器,但没人愿意用,他们告诉我计算机只能做算术,不能运行程序\u0026rdquo;。\n但这个点子是好的,不久,很多人尝试创造新编程语言 - 如今有上百种语言!\n可惜的是,没有任何 a-0 的代码遗留下来,所以我们用 python 举例(一门现代编程语言)。\n假设我们想相加两个数字,保存结果。记住,如果用汇编代码,我们得从内存取值,和寄存器打交道,以及其他底层细节,但同样的程序可以用 python 这样写:\n不用管寄存器或内存位置 - 编译器会搞定这些细节,不用管底层细节。\n程序员只需要创建 代表内存地址的抽象,叫 \u0026ldquo;变量\u0026rdquo; ,给变量取名字,现在可以把两个数 存在变量里,这里取名 a 和 b, 实际编程时你可以随便取名;然后相加两个数,把结果存在变量 c 。底层操作时,编译器可能把变量 a 存在寄存器 a ,但我不需要知道这些!眼不见心不烦 !😼 这是个重要历史里程碑,但 a-0 和之后的版本没有广泛使用。\n高级程序语言 fortran,名字来自 \u0026ldquo;公式翻译\u0026rdquo;(formula translation),这门语言数年后由 ibm 在 1957 年发布,主宰了早期计算机编程。\njohn backus, the fortran project director, fortran 项目总监 john backus 说过,\u0026ldquo;我做的大部分工作都是因为懒,我不喜欢写程序,所以我写这门语言,让编程更容易\u0026rdquo;\n你懂的,典型的\u0026quot;懒人\u0026quot;,😎 创造自己的编程语言。\n平均来说,fortran 写的程序,比等同的手写汇编代码短 20 倍,然后 fortran 编译器会把代码转成机器码。人们怀疑性能是否比得上手写代码,但因为能让程序员写程序更快,所以成了一个更经济的选择,运行速度慢一点点,编程速度大大加快。\n当时 ibm 在卖计算机,因此最初 fortran 代码只能跑在 ibm 计算机上。1950 年代大多数编程语言和编译器只能运行在一种计算机上,如果升级电脑,可能要重写所有代码!因此工业界,学术界,政府的计算机专家在 1959 年组建了一个联盟 - 数据系统语言委员会,grace hopper 担任顾问,开发一种通用编程语言,可以在不同机器上通用,最后诞生了一门高级,易于使用,\u0026ldquo;普通面向商业语言\u0026rdquo;,简称 cobol(common business-oriented language) 。\n为了兼容不同底层硬件,每个计算架构需要一个 cobol 编译器。最重要的是,这些编译器都可以接收相同 cobol 代码,不管是什么电脑,这叫\u0026quot;一次编写,到处运行\u0026quot; 。如今大多数编程语言都是这样,不必接触 cpu 特有的汇编码和机器码,减小了使用门槛。\n在高级编程语言出现之前,编程只是计算机专家和爱好者才会做的事,而且通常是主职,但现在,科学家,工程师,医生,经济学家,教师等等,都可以把计算机用于工作。\n感谢这些语言!\n计算机科学从深奥学科 变成了大众化工具,同时,编程的抽象也让计算机专家,现在叫\u0026quot;专业程序员\u0026quot;,制作更复杂的程序,如果用汇编写可能要上百万行。\n当然,计算机的历史没有在 1959 年结束,编程语言设计的黄金时代才刚刚开始,和硬件一起飞速发展。\nin the 1960s, we had languages like algol, lisp and basic. 在 1960 年代,有 algol, lisp 和 basic 等语言,70 年代有:pascal,c 和 smalltalk ,80 年代有:c++,objective-c 和 perl ,90 年代有:python,ruby 和 java ,新千年 swift, c#, go 在崛起 。 有些语言你可能听起来耳熟 - 很多现在还存在,你现在用的浏览器很可能是 c++ 或 objective-c 写的。\n我刚才说的编程语言名字只是冰山一角,新的编程语言在不断诞生。新语言想用更聪明的抽象,让某些方面更容易或更强大,或利用新技术和新平台带来的优势,让更多人能快速做出美妙的事情。许多人认为编程的\u0026quot;圣杯\u0026quot;是直接用英文,直接对计算机说话,然后它会理解并执行,这种智能系统目前只存在于科幻小说,\u0026ldquo;2001:太空漫游\u0026rdquo; 的粉丝可能没什么意见。\n现在你理解了编程语言,接下来几集我们会深入了解编程语言和用语言写的软件,是怎么做到那些酷事。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/11/","summary":"\u003cp\u003ei.e. The First Programming Languages\u003c/p\u003e\n\u003cp\u003e之前我们把重点放在硬件 - 组成计算机的物理组件,比如电,电路,寄存器,RAM,ALU,CPU ,但在硬件层面编程非常麻烦。\u003c/p\u003e\n\u003cp\u003e所以程序员想要一种更通用的方法编程 - 一种\u0026quot;更软的\u0026quot;媒介。\u003c/p\u003e\n\u003cp\u003e没错,我们要讲软件!\u003c/p\u003e","title":"cscc11 - 编程语言发展史"},{"content":"i.e. early programming\n前几集我们把重点放在计算机的原理,怎么从内存读写数据,执行操作,比如把两个数字加在一起,还简单讲了下指令的执行,也就是计算机程序。\n但我们还没讲的是:程序如何\u0026quot;进入\u0026quot;计算机。你应该记得在第 7, 8 集,我们一步步讲了例子程序,当时为了简单,我们假设程序已经魔法般在内存里了,但事实是,程序需要加载进内存,这不是魔法,是计算机科学!\n给机器编程这个需求,早在计算机出现之前就有了。\n最著名的例子来自纺织业,如果你只想织一块红色大桌布,可以直接放红线进织布机,但如果想要图案怎么办?比如条纹或者方格。工人要每隔一会儿调整一次织布机,因为非常消耗劳动力,所以图案纺织品很贵。特定位置有没有穿孔,决定了线是高是低,横线是从上/从下穿过,为了让每行图案不同,纸卡连成长条,形成连续指令,听起来很熟?\n\u0026gt; 雅卡尔织布机\n很多人认为雅卡尔织布机是最早的编程。\n事实证明,穿孔纸卡便宜、可靠、也易懂。近一个世纪后,穿孔纸卡用于 1890 年美国人口普查,我们在第一集提过,一张卡存一个人的信息,比如种族、婚姻状况、子女数量、出生国家等等。针对每个问题,人口普查工作者会在对应位置打孔,当卡片插入汇总机,孔会让对应总和值+1 ,可以插入整个国家人口的卡片,在结束后得到各个总值。值得注意的是,早期汇总机不算计算机,因为它们只做一件事 - 汇总数据,操作是固定的,不能编程,穿孔纸卡存的是数据,不是程序。\n\u0026gt; 穿孔纸卡 \u0026amp; 汇总机\n之后 60 年,这些机器被加强,可以做减、乘、除,甚至可以做一些小决定,决定何时执行某指令。为了正确执行不同计算,程序员需要某种控制面板,面板有很多小插孔,程序员可以插电线,让机器的不同部分互相传数据和信号,因此也叫 \u0026ldquo;插线板\u0026rdquo;。\n\u0026gt; 插线板\n不幸的是,这意味着运行不同程序要重新接线,所以到 1920 年代,控制面板变成了可拔插,让编程更方便,可以给机器插入不同程序。比如,一个插线板算销售税,另一个算工资单,但给插线板编程很复杂,图中乱成一团的线负责算盈亏总额,用于 ibm 402 核算机。在 1940 年代这样做很流行,用插线板编程不只在机电计算机流行。世上第一台通用电子计算机,eniac,完成于 1946 年,用了一大堆插线板,程序在纸上设计好之后,给 eniac 连线,最多可能花三个星期。因为早期计算机非常昂贵,停机几个星期只为换程序完全无法接受。人们急需更快、更灵活的新方式来编程。\n幸运的是,到 1940 年代晚期 1950 年代初,内存变得可行,价格下降,容量上升。与其把程序存在插线板,存在内存变得可行,这样程序易于修改、方便 cpu 快速读取,这类机器叫 \u0026ldquo;存储程序计算机\u0026rdquo;。如果内存足够,不仅可以存要运行的程序,还可以存程序需要的数据,包括程序运行时产生的新数据。\n程序和数据都存在一个地方,叫 \u0026ldquo;冯诺依曼结构\u0026rdquo; ,命名自 约翰·冯·诺依曼 - 杰出的数学家和物理学家 ,参与了曼哈顿计划和早期电子计算机项目。他曾说:我在思考比炸弹重要得多的东西 - 计算机 。\n冯诺依曼计算机的标志是,一个处理器(有算术逻辑单元) + 数据寄存器 + 指令寄存器 + 指令地址寄存器 + 内存(负责存数据和指令) 。\n希望这听起来很耳熟,因为第 7 集我们造了一个冯诺依曼计算机。\n第一台冯诺依曼架构的\u0026quot;储存程序计算机\u0026quot;,由曼彻斯特大学于 1948 年建造完成,绰号\u0026quot;宝宝\u0026quot;,甚至你现在看视频的计算机,也在用一样的架构。虽然有内存很棒,但程序和数据依然需要某种方式输入计算机,所以用穿孔纸卡。\n让我们进入思维泡泡\n到 1980 年代,几乎所有的计算机都有穿孔纸卡读取器,可以吸入一张卡片,把卡片内容写进内存。如果放了一叠卡片,读取器会一个个写进内存,一旦程序和数据写入完毕,电脑会开始执行,即便简单程序也有几百条指令,要用一叠纸卡来存。如果不小心摔倒弄撒了,要花上几小时、几天、甚至几周来整理。\n有个小技巧是在卡片侧面画对角线,如果弄散了,整理起来会方便很多。\n用纸卡的最大型程序是美国空军的 sage 防空系统,于 1955 年完成。据称顶峰时期雇佣了世上 20% 程序员,主控制程序用了 62500 张穿孔纸卡,等同于大约 5mb 的数据,以如今的标准,不值一提。穿孔纸卡不仅可以往计算机放数据,还可以取出数据,程序运行到最后,结果可以输到纸卡上,方式嘛,当然是打孔,然后人可以分析结果,或者再次放进计算机,做进一步计算。\n谢了 思维泡泡\n穿孔纸卡的亲戚是纸带。基本是一回事,只不过更连续,不是一张张卡。当然我们还没提硬盘,只读光盘,dvd, u 盘等等,以后我们会讲这些更先进的存储方法。\n\u0026gt; 纸带\n最后,除了插线板和穿孔纸卡,在 1980 年代前,还有一种常见编程方式,面板编程。与其插一堆线到插线板,可以用一大堆开关和按钮,做到一样的效果,面板上有指示灯,代表各种函数的状态和内存中的值。50 和 60 年代的计算机,一般都有这样巨大的控制台,很少有人只用开关来输入一整个程序,但技术上是可行的。早期针对计算机爱好者的家用计算机,大量使用了开关,因为大多数家庭用户负担不起昂贵的外围设备,比如穿孔纸卡读取器。\n\u0026gt; 面板编程\n第一款取得商业成功的家用计算机是 altair 8800 ,有两种版本可以买:1. 预先装好的整机 ;2. 需要组装的组件。计算机爱好者 喜欢买组件版,售价极低,在 1975 年卖 400 美元左右,相当于 2017 年的 2000 美元。为了给 8800 编程,你要拨动面板上的开关,输入二进制操作码,然后按 \u0026ldquo;存储键\u0026rdquo; 把值存入内存,然后会到下一个内存位置,你可以再次拨开关,写下一个指令,重复这样做,把整个程序都写入内存之后,可以推动开关,回到内存地址 0 ,然后按运行按钮,灯会闪烁,这就是 1975 年的家用计算机,哇。\n\u0026gt; altair 8800\n不管是插线板、开关或穿孔纸卡,早期编程都是专家活。不管是全职还是技术控,都要非常了解底层硬件,比如 操作码,寄存器等,才能写程序,所以编程很难,很烦。哪怕工程师和科学家都无法 完全发挥计算机的能力,我们需要一种更简单方式告诉计算机要做什么。\n一种更简单的编程方式!\n这带领我们到下一个话题 - 编程语言,我们下集会讲。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/10/","summary":"\u003cp\u003ei.e. Early Programming\u003c/p\u003e\n\u003cp\u003e前几集我们把重点放在计算机的原理,怎么从内存读写数据,执行操作,比如把两个数字加在一起,还简单讲了下指令的执行,也就是计算机程序。\u003c/p\u003e\n\u003cp\u003e但我们还没讲的是:程序如何\u0026quot;进入\u0026quot;计算机。你应该记得在第 7, 8 集,我们一步步讲了例子程序,当时为了简单,我们假设程序已经魔法般在内存里了,但事实是,程序需要加载进内存,这不是魔法,是计算机科学!\u003c/p\u003e","title":"cscc10 - 早期的编程方式"},{"content":"i.e. advanced cpu designs\n随着本系列进展,我们知道计算机进步巨大,从 1 秒 1 次运算,到现在有千赫甚至兆赫的 cpu ,你现在看视频的设备八成也有 ghz 速度 ,1 秒十亿条指令 ,这是很大的计算量!\n早期计算机的提速方式是减少晶体管的切换时间 ,晶体管组成了逻辑门,alu 以及前几集的其他组件,但这种提速方法最终会碰到瓶颈,所以处理器厂商发明各种新技术来提升性能,不但让简单指令运行更快,也让它能进行更复杂的运算。\n上集我们写了个做除法的程序,给 cpu 执行,方法是做一连串减法,比如 16 除 4 会变成 - 16-4 -4 -4 -4 ,碰到 0 或负数才停下 。但这种方法要多个时钟周期,很低效。所以现代 cpu 直接在硬件层面设计了除法,可以直接给 alu 除法指令,这让 alu 更大也更复杂一些,但也更厉害 。 复杂度 vs 速度的平衡 在计算机发展史上经常出现。\n举例,现代处理器有专门电路来处理图形操作,解码压缩视频,加密文档 等等。如果用标准操作来实现,要很多个时钟周期。你可能听过某些处理器有 mmx, 3dnow, sse ,它们有额外电路做更复杂的操作,用于游戏和加密等场景。指令不断增加,人们一旦习惯了它的便利就很难删掉,所以为了兼容旧指令集,指令数量越来越多。\n英特尔 4004,第一个集成 cpu,有 46 条指令,足够做一台能用的计算机,但现代处理器有上千条指令,有各种巧妙复杂的电路,超高的时钟速度带来另一个问题 - 如何快速传递数据给 cpu 。就像有强大的蒸汽机,但无法快速加煤。ram 成了瓶颈,ram 是 cpu 之外的独立组件,意味着数据要用线来传递,叫\u0026quot;总线\u0026quot;,总线可能只有几厘米,别忘了电信号的传输接近光速,但 cpu 每秒可以处理上亿条指令,很小的延迟也会造成问题。ram 还需要时间找地址取数据,配置,输出数据,一条\u0026quot;从内存读数据\u0026quot;的指令可能要多个时钟周期,cpu 空等数据。\n缓存 解决延迟的方法之一是给 cpu 加一点 ram - 叫 \u0026ldquo;缓存\u0026rdquo; ,因为处理器里空间不大,所以缓存一般只有 kb 或 mb ,而 ram 都是 gb 起步。缓存提高了速度,cpu 从 ram 拿数据时,ram 不用传一个,可以传一批,虽然花的时间久一点,但数据可以存在缓存,这很实用,因为数据常常是一个个按顺序处理。\n\u0026gt; cpu 缓存\n举个例子,算餐厅的当日收入,先取 ram 地址 100 的交易额,ram 与其只给 1 个值,直接给一批值,把地址 100 到 200 都复制到缓存。当处理器要下一个交易额时,地址 101,缓存会说:\u0026ldquo;我已经有了,现在就给你\u0026rdquo;,不用去 ram 取数据,因为缓存离 cpu 近,一个时钟周期就能给数据 - cpu 不用空等!比反复去 ram 拿数据快得多。如果想要的数据已经在缓存,叫 缓存命中 ,如果想要的数据不在缓存,叫 缓存未命中 。缓存也可以当临时空间,存一些中间值,适合长/复杂的运算。继续餐馆的例子,假设 cpu 算完了一天的销售额,想把结果存到地址 150,就像之前,数据不是直接存到 ram ,而是存在缓存,这样不但存起来快一些,如果还要接着算,取值也快一些。\n但这样带来了一个有趣的问题,缓存和 ram 不一致了 😈。这种不一致必须记录下来,之后要同步,因此缓存里每块空间有一个特殊标记,叫 \u0026ldquo;脏位\u0026rdquo; (dirty bit) - 这可能是计算机科学家取的最贴切的名字。同步一般发生在 当缓存满了而 cpu 又要缓存时,在清理缓存腾出空间之前,会先检查 \u0026ldquo;脏位\u0026rdquo;。如果是\u0026quot;脏\u0026quot;的,在加载新内容之前,会把数据写回 ram 。\n指令流水线 另一种提升性能的方法叫 \u0026ldquo;指令流水线\u0026rdquo; 。\n想象下你要洗一整个酒店的床单,但只有 1 个洗衣机,1 个干燥机,选择 1:按顺序来,放洗衣机等 30 分钟洗完,然后拿出湿床单,放进干燥机等 30 分钟烘干,这样 1 小时洗一批;另外一说:如果你有 30 分钟就能烘干的干燥机,请留言告诉我是什么牌子,我的至少要 90 分钟。即使有这样的神奇干燥机,我们可以用\u0026quot;并行处理\u0026quot;进一步提高效率。就像之前,先放一批床单到洗衣机,等 30 分钟洗完,然后把湿床单放进干燥机,但这次,与其干等 30 分钟烘干,可以放另一批进洗衣机,让两台机器同时工作,30 分钟后,一批床单完成,另一批完成一半,另一批准备开始,效率 x2 !🎉\n处理器也可以这样设计。\n第 7 集,我们演示了 cpu 按序处理 - 取指 → 解码 → 执行,不断重复。这种设计,三个时钟周期执行 1 条指令,但因为每个阶段用的是 cpu 的不同部分,意味着可以并行处理(parallelize)!\u0026ldquo;执行\u0026quot;一个指令时,同时\u0026quot;解码\u0026quot;下一个指令,\u0026ldquo;读取\u0026quot;下下个指令,不同任务重叠进行,同时用上 cpu 里所有部分。\n这样的流水线每个时钟周期执行 1 个指令,吞吐量 x 3 。\n和缓存一样,这也会带来一些问题。\n第一个问题是指令之间的依赖关系,举个例子,你在读某个数据,而正在执行的指令会改这个数据,也就是说拿的是旧数据,因此流水线处理器要先弄清数据依赖性,必要时停止流水线,避免出问题。高端 cpu,比如笔记本和手机里那种,会更进一步,动态排序有依赖关系的指令,最小化流水线的停工时间,这叫 \u0026ldquo;乱序执行\u0026rdquo; 。和你猜的一样,这种电路非常复杂,但因为非常高效,几乎所有现代处理器都有流水线。\n第二个问题是 \u0026ldquo;条件跳转\u0026rdquo;,比如上集的 jump negative ,这些指令会改变程序的执行流。简单的流水线处理器,看到 jump 指令会停一会儿,等待条件值确定下来,一旦 jump 的结果出了,处理器就继续流水线。因为空等会造成延迟,所以高端处理器会用一些技巧,可以把 jump 想成是 \u0026ldquo;岔路口\u0026rdquo;,高端 cpu 会猜哪条路的可能性大一些,然后提前把指令放进流水线,这叫 \u0026ldquo;推测执行\u0026rdquo; 。当 jump 的结果出了,如果 cpu 猜对了,流水线已经塞满正确指令,可以马上运行,如果 cpu 猜错了,就要清空流水线,就像走错路掉头。让 gps 不要再!叫!了!为了尽可能减少清空流水线的次数,cpu 厂商开发了复杂的方法,来猜测哪条分支更有可能,叫 \u0026ldquo;分支预测\u0026rdquo; ,现代 cpu 的正确率超过 90% 。\n理想情况下,流水线一个时钟周期完成 1 个指令,然后\u0026quot;超标量处理器\u0026quot;出现了,一个时钟周期完成多个指令。即便有流水线设计,在指令执行阶段,处理器里有些区域还是可能会空闲。比如,执行一个 \u0026ldquo;从内存取值\u0026rdquo; 指令期间, alu 会闲置,所以一次性处理多条指令(取指令+解码) 会更好。如果多条指令要 alu 的不同部分,就多条同时执行。我们可以再进一步,加多几个相同的电路,执行出现频次很高的指令。举例,很多 cpu 有四个,八个甚至更多完全相同的 alu ,可以同时执行多个数学运算。\n好了,目前说过的方法,都是优化 1 个指令流的吞吐量。\n另一个提升性能的方法是同时运行多个指令流,用多核处理器。你应该听过双核或四核处理器,意思是一个 cpu 芯片里,有多个独立处理单元,很像是有多个独立 cpu,但因为它们整合紧密,可以共享一些资源,比如缓存,使得多核可以合作运算,但多核不够时,可以用多个 cpu 。高端计算机,比如现在给你传视频的 youtube 服务器,需要更多马力,让上百人能同时流畅观看,2 个或 4 个 cpu 是最常见的。\n\u0026gt; 多核处理器\n但有时人们有更高的性能要求,所以造了超级计算机!如果要做怪兽级运算,比如模拟宇宙形成,你需要强大的计算能力,给普通台式机加几个 cpu 没什么用,你需要很多处理器! 不…不…还要更多,更多。截止至视频发布,世上最快的计算机在中国无锡的国家超算中心 - 神威·太湖之光有 40960 个 cpu,每个 cpu 有 256 个核心,总共超过 1 千万个核心,每个核心的频率是 1.45ghz ,每秒可以进行 9.3 亿亿次浮点数运算,也叫 每秒浮点运算次数 (flops)。相信我,这个速度很可怕,没人试过跑最高画质的《孤岛危机》但我估计没问题。\n\u0026gt; 神威·太湖之光\n长话短说,这些年处理器不但大大提高了速度,而且也变得更复杂,用各种技巧,榨干每个时钟周期 做尽可能多运算。我们的任务是利用这些运算能力,做又酷又实用的事。\n编程就是为了这个,我们下集说。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/09/","summary":"\u003cp\u003ei.e. Advanced CPU Designs\u003c/p\u003e\n\u003cp\u003e随着本系列进展,我们知道计算机进步巨大,从 1 秒 1 次运算,到现在有千赫甚至兆赫的 CPU ,你现在看视频的设备八成也有 GHz 速度 ,1 秒十亿条指令 ,这是很大的计算量!\u003c/p\u003e\n\u003cp\u003e早期计算机的提速方式是减少晶体管的切换时间 ,晶体管组成了逻辑门,ALU 以及前几集的其他组件,但这种提速方法最终会碰到瓶颈,所以处理器厂商发明各种新技术来提升性能,不但让简单指令运行更快,也让它能进行更复杂的运算。\u003c/p\u003e","title":"cscc09 - 高级 cpu 设计"},{"content":"i.e. instructions \u0026amp; programs\n上集我们把 alu, 控制单元,ram, 时钟 结合在一起,做了个基本,但可用的\u0026quot;中央处理单元\u0026quot;, 简称 cpu ,它是计算机的核心。\n我们已经用电路做了很多组件,这次我们给 cpu 一些指令来运行! cpu 之所以强大,是因为它是可编程的(programmable)- 如果写入不同指令,就会执行不同任务。cpu 是一块硬件,可以被软件控制!\n我们重新看一下上集的简单程序,内存里有这些值,每个地址可以存 8 位数据。因为我们的 cpu 是假设的,这里前 4 位是\u0026quot;操作码\u0026quot;,后 4 位指定一个内存地址,或寄存器,内存地址 0 是 0010 1110 ,前 4 位代表 load_a 指令 - 意思是:把后 4 位指定的内存地址的值,放入寄存器 a ,后 4 位是 1110,十进制的 14 ,我们来把 0010 1110 看成 \u0026ldquo;load_a 14\u0026rdquo; 指令,这样更好理解!也更方便说清楚!可以对内存里剩下的数也这样转换,这里,我们的程序只有 4 个指令,还有数字 3 和 14 。\n现在一步步看:\n\u0026ldquo;load_a 14\u0026rdquo; 是从地址 14 中拿到数字 3,放入寄存器 a ; \u0026ldquo;load_b 15\u0026rdquo; 是从地址 15 中拿到数字 14,放入寄存器 b ; 下一个是 add 指令 - \u0026ldquo;add b a\u0026rdquo; 告诉 alu 把寄存器 b 和寄存器 a 里的数字加起来,(b 和 a 的)顺序很重要,因为结果会存在第二个寄存器 - 也就是寄存器 a ; 最后一条指令是 \u0026ldquo;store_a 13\u0026rdquo; ,把寄存器 a 的值存入内存地址 13 。 好棒!我们把 2 个数加在了一起!毕竟只有 4 个指令,也只能做这个了 。\n加多一些指令吧!\n\u0026gt; 更多的指令\nsub 是减法,和 add 一样也要 2 个寄存器来操作。\n还有 jump(跳转)- 让程序跳转到新位置,如果想改变指令顺序,或跳过一些指令,这个很实用。举例,jump 0 可以跳回开头。 jump 在底层的实现方式 - 是把指令后 4 位代表的内存地址的值覆盖掉 \u0026ldquo;指令地址寄存器\u0026rdquo; 里的值 。 还有一个特别版的 jump 叫 jump_negative,它只在 alu 的 \u0026ldquo;负数标志\u0026rdquo; 为真时,进行 jump ,第 5 集讲过,算术结果为负,\u0026ldquo;负数标志\u0026quot;才是真,结果不是负数时,\u0026ldquo;负数标志\u0026quot;为假。如果是假,jump_negative 就不会执行,程序照常进行。\n我们之前的例子程序,其实应该是这样,才能正确工作。否则跑完 store_a 13 之后, cpu 会不停运行下去,处理后面的 0 ,因为 0 不是操作码,所以电脑会崩掉 !\n我还想指出一点,指令和数据都是存在同一个内存里的,它们在根本层面上毫无区别 - 都是二进制数 。halt 很重要,能区分指令和数据。\n好,现在用 jump 让程序更有趣一些,我们还把内存中 3 和 14 两个数字,改成 1 和 1 ,现在来从 cpu 的视角走一遍程序。\n首先 load_a 14,把 1 存入寄存器 a (因为地址 14 里的值是 1);然后 load_b 15,把 1 存入寄存器 b (因为地址 15 里的值也是 1);然后 add b a 把寄存器 b 和 a 相加 结果放到寄存器 a 里,现在寄存器 a 的值是 2 (当然是以二进制存的);然后 store_a 13 指令,把寄存器 a 的值存入内存地址 13。\n现在遇到 jump 2 指令 ,cpu 会把\u0026quot;指令地址寄存器\u0026quot;的值,现在是 4,改成 2 ,因此下一步不再是 halt ,而是读内存地址 2 里的指令,也就是 add b a 。\n我们跳转了!\n寄存器 a 里是 2,寄存器 b 里是 1 ,1+2=3,寄存器 a 变成 3 ,存入内存 ,又碰到 jump 2,又回到 add b a 。 1+3=4 ,现在寄存器 a 是 4 。\n发现了吗?每次循环都+1 ,不断增多 ,酷! 但没法结束啊 ,永远不会碰到 halt ,总是会碰到 jump ,这叫无限循环 - 这个程序会永远跑下去。.. 下去。.. 下去。.. 下去。.. 为了停下来,我们需要有条件的 jump , 只有特定条件满足了,才执行 jump 。比如 jump negative 就是条件跳转的一个例子,还有其他类型的条件跳转,比如、 jump if equal(如果相等)、jump if greater(如果更大)。\n现在把代码弄花哨一点,再过一遍代码。\n就像之前,程序先把内存值放入寄存器 a 和 b 。寄存器 a 是 11,寄存器 b 是 5 ;sub b a,用 a 减 b,11-5=6 ,6 存入寄存器 a ;jump negative 出场,上一次 alu 运算的结果是 6 ,是正数,所以 \u0026ldquo;负数标志\u0026rdquo; 是假 ,因此处理器不会执行 jump 。\n继续下一条指令 - jump 2 , jump 2 没有条件,直接执行!又回到寄存器 a-b,6-5=1 ,a 变成 1 ;\n下一条指令又是 jump negative ,因为 1 还是正数,因此 jump negative 不会执行 ;来到下一条指令,jump 2 ,又来减一次 ,这次就不一样了 1-5=-4 ,这次 alu 的 \u0026ldquo;负数标志\u0026rdquo; 是真,现在下一条指令, jump negative 5,cpu 的执行跳到内存地址 5 ,跳出了无限循环!\n现在的指令是 add b a,-4+5=1,1 存入寄存器 a ;下一条指令 store_a 13,把 a 的值存入内存地址 13 ,最后碰到 halt 指令,停下来。\n虽然程序只有 7 个指令,但 cpu 执行了 13 个指令,因为在内部循环了 2 次。\n这些代码其实是算余数的,11 除 5 余 1 。\n如果加多几行指令,我们还可以跟踪循环了多少次,11 除 5,循环 2 次,余 1 。当然,我们可以用任意 2 个数,7 和 81,18 和 54,什么都行,这就是软件的强大之处。\n软件还让我们做到硬件做不到的事,alu 可没有除法功能,是程序给了我们这个功能。别的程序也可以用我们的除法程序,来做其他事情。\n这意味着一层新抽象!\n我们这里假设的 cpu 很基础,所有指令都是 8 位,操作码只占了前面 4 位,即便用尽 4 位,也只能代表 16 个指令,而且我们有几条指令,是用后 4 位来指定内存地址。因为 4 位最多只能表示 16 个值,所以我们只能操作 16 个地址,这可不多。我们甚至不能 jump 17 ,因为 4 位二进制无法表示数字 17 ,因此,真正的现代 cpu 用两种策略: 最直接的方法是用更多位来代表指令,比如 32 位或 64 位 - 这叫 \u0026ldquo;指令长度\u0026rdquo; ;第二个策略是 \u0026ldquo;可变指令长度\u0026rdquo; 。\n举个例子,比如某个 cpu 用 8 位长度的操作码,如果看到 halt 指令,halt 不需要额外数据,那么会马上执行。如果看到 jump,它得知道位置值,这个值在 jump 的后面,这叫 \u0026ldquo;立即值\u0026rdquo;。这样设计,指令可以是任意长度,但会让读取阶段复杂一点点。要说明的是,我们拿来举例的 cpu 和指令集都是假设的,是为了展示核心原理。\n所以我们来看个真的 cpu 例子。1971 年,英特尔发布了 4004 处理器。这是第一次把 cpu 做成一个芯片,给后来的英特尔处理器打下了基础。它支持 46 个指令,足够做一台能用的电脑,它用了很多我们说过的指令,比如 jump add sub load ,它也用 8 位的\u0026quot;立即值\u0026quot;来执行 jump, 以表示更多内存地址。处理器从 1971 年到现在发展巨大,现代 cpu, 比如英特尔酷睿 i7, 有上千个指令和指令变种,长度从 1 到 15 个字节。\n举例,光 add 指令就有很多变种!指令越来越多,是因为给 cpu 设计了越来越多功能。\n下集我们会讲。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/08/","summary":"\u003cp\u003ei.e. Instructions \u0026amp; Programs\u003c/p\u003e\n\u003cp\u003e上集我们把 ALU, 控制单元,RAM, 时钟 结合在一起,做了个基本,但可用的\u0026quot;中央处理单元\u0026quot;, 简称 CPU ,它是计算机的核心。\u003c/p\u003e\n\u003cp\u003e我们已经用电路做了很多组件,这次我们给 CPU 一些指令来运行!\nCPU 之所以强大,是因为它是可编程的(programmable)- 如果写入不同指令,就会执行不同任务。CPU 是一块硬件,可以被软件控制!\u003c/p\u003e","title":"cscc08 - 指令和程序"},{"content":"i.e. the central processing unit(cpu)\n今天我们讲 处理器(processors),提示下 - 这集可能是最难的一集,所以一旦你理解了,就会变得超厉害 der~ 😈\n我们已经做了一个算术逻辑单元(alu,arithmetic and logic unit),输入二进制,它会执行计算。我们还做了两种内存:寄存器(registers) - 很小的一块内存,能存一个值;之后我们增大做出了 ram ,ram 是一大块内存,能在不同地址存大量数字。 现在是时候把这些放在一起,组建计算机的 \u0026ldquo;心脏\u0026rdquo; 了,但这个 \u0026ldquo;心脏\u0026rdquo; 不会有任何包袱,比如人类情感。\n= 这一部分的描述真好,对上个章节的迷惑内容做了一个很不错的总结!*\n计算机的心脏是\u0026quot;中央处理单元\u0026quot;,简称 \u0026ldquo;cpu\u0026rdquo;(central processing unit)。\ncpu 负责执行程序,比如 office,safari 浏览器,你最爱的《半条命 2》。程序由一个个操作组成,这些操作叫 \u0026ldquo;指令\u0026rdquo; (instruction),因为它们\u0026quot;指示\u0026quot;计算机要做什么。如果是数学指令,比如加/减(add/subtract),cpu 会让 alu 进行数学运算,也可能是内存指令,cpu 会和内存通信,然后读/写值。\ncpu 里有很多组件,所以我们一边说一边建。我们把重点放在功能,而不是一根根线具体怎么连。当我们用一条线连接两个组件时,这条线只是所有必须线路的一个抽象,这种高层次视角叫 \u0026ldquo;微体系架构\u0026rdquo; (microarchitecture)。\n好,我们首先要一些内存,把上集做的 ram 拿来就行。为了保持简单,假设它只有 16 个位置,每个位置存 8 位;再来四个 8 位寄存器,叫 a,b,c,d,寄存器用来 临时存数据 和 操作数据。\n我们已经知道数据是以二进制值存在内存里,程序也可以存在内存里。我们可以给 cpu 支持的所有指令,分配一个 id。\n在这个假设的例子,我们用前四位存 \u0026ldquo;操作代码\u0026rdquo; (operation code),简称 \u0026ldquo;操作码\u0026rdquo; (opcode);后四位代表数据来自哪里 - 可以是寄存器或内存地址。\n我们还需要两个寄存器,来完成 cpu:\n一个寄存器追踪程序运行到哪里了,我们叫它 \u0026ldquo;指令地址寄存器\u0026rdquo; (instruction address register),顾名思义,存当前指令的内存地址; 另一个寄存器存当前指令,叫 \u0026ldquo;指令寄存器\u0026rdquo; (instruction register)。 当启动计算机时,所有寄存器从 0 开始。\n\u0026gt; 初始状态\n为了举例,我们在 ram 里放了一个程序,我们今天会过一遍。\ncpu 的第一个阶段叫 \u0026ldquo;取指令阶段\u0026rdquo; (fetch phase),负责拿到指令。首先,将 \u0026ldquo;指令地址寄存器\u0026rdquo; 连到 ram,寄存器的值为 0,因此 ram 返回地址 0 的值, 0010 1110 会复制到 \u0026ldquo;指令寄存器\u0026rdquo; 里,现在指令拿到了。\n\u0026gt; 取指令阶段\n要弄清是什么指令,才能执行(execute),而不是杀死(kill)它,这是 \u0026ldquo;解码阶段\u0026rdquo; 。\n\u0026gt; 解码阶段\n前 4 位 0010 是 load a 指令,意思是,把 ram 的值放入寄存器 a ;后 4 位 1110 是 ram 的地址,转成十进制是 14 。接下来,指令由 \u0026ldquo;控制单元\u0026rdquo; 进行解码,就像之前的所有东西,\u0026ldquo;控制单元\u0026rdquo; 也是逻辑门组成的 。比如,为了识别 \u0026ldquo;load a\u0026rdquo; 指令,需要一个电路,检查操作码是不是 0010 ,我们可以用很少的逻辑门来实现。\n\u0026gt; 检查操作码是否为 load a 的电路\n现在知道了是什么指令,就可以开始执行了,开始 \u0026ldquo;执行阶段\u0026rdquo; ,用 \u0026ldquo;检查是否 load_a 指令的电路\u0026rdquo;,可以打开 ram 的 \u0026ldquo;允许读取线\u0026rdquo;, 把地址 14 传过去,ram 拿到值, 0000 0011 ,十进制的 3 。因为是 load_a 指令,我们想把这个值只放到寄存器 a,其他寄存器不受影响,所以需要一根线,把 ram 连到 4 个寄存器,用 \u0026ldquo;检查是否 load_a 指令的电路\u0026rdquo; 启用寄存器 a 的 \u0026ldquo;允许写入线\u0026rdquo;,这就成功了 - 把 ram 地址 14 的值,放到了寄存器 a 。\n\u0026gt; 执行阶段\n既然指令完成了,我们可以关掉所有线路,去拿下一条指令,我们把 \u0026ldquo;指令地址寄存器\u0026rdquo;+1,\u0026ldquo;执行阶段\u0026quot;就此结束。\nload_a 只是 cpu 可以执行的各种指令之一, 不同指令由不同逻辑电路解码 ,这些逻辑电路会配置 cpu 内的组件来执行对应操作。具体分析这些解码电路太繁琐了,既然已经看了 1 个例子,干脆把 \u0026ldquo;控制单元 \u0026ldquo;包成一个整体,简洁一些。\n\u0026gt; 抽象了的‘控制单元’\n没错,一层新抽象。\n控制单元就像管弦乐队的指挥,\u0026ldquo;指挥\u0026rdquo; cpu 的所有组件, \u0026ldquo;取指令→解码→执行\u0026rdquo; 完成后。现在可以再来一次,从 \u0026ldquo;取指令\u0026rdquo; 开始,\u0026ldquo;指令地址寄存器\u0026rdquo; 现在的值是 1,所以 ram 返回地址 1 里的值:0001 1111 ;到 \u0026ldquo;解码\u0026rdquo; 阶段!0001 是 load b 指令,从 ram 里把一个值复制到寄存器 b ,这次内存地址是 1111,十进制的 15;现在到 \u0026ldquo;执行阶段\u0026rdquo;!\u0026ldquo;控制单元\u0026rdquo; 叫 ram 读地址 15,并配置寄存器 b 接收数据,成功,我们把值 0000 1110 ,也就是十进制的 14 存到了寄存器 b ;最后一件事是 \u0026ldquo;指令地址寄存器\u0026rdquo; +1 ,我们又完成了一个循环。 🥳\n下一条指令有点不同,来取它吧。\n1000 0100 1000 是 add 指令,这次后面的 4 位不是 ram 地址, 而是 2 位 2 位,分别代表 2 个寄存器。2 位可以表示 4 个值,所以足够表示 4 个寄存器。第一个地址是 01, 代表寄存器 b ,第二个地址是 00, 代表寄存器 a ,因此,1000 0100,代表把寄存器 b 的值,加到寄存器 a 里 。\n\u0026gt; add b 到 a\n为了执行这个指令,我们要整合第 5 集的 alu ,\u0026ldquo;控制单元\u0026rdquo; 负责选择正确的寄存器作为输入,并配置 alu 执行正确的操作。对于 \u0026ldquo;add\u0026rdquo; 指令, \u0026ldquo;控制单元\u0026rdquo; 会启用寄存器 b,作为 alu 的第一个输入,还启用寄存器 a,作为 alu 的第二个输入。之前说过,alu 可以执行不同操作,所以控制单元必须传递 add 操作码告诉它要做什么,最后,结果应该存到寄存器 a ,但不能直接写入寄存器 a ,这样新值会进入 alu ,不断和自己相加,因此,控制单元用一个自己的寄存器暂时保存结果,关闭 alu,然后把值写入正确的寄存器。这里 3+14=17,二进制是 0001 0001 ,现在存到了寄存器 a ,和之前一样,最后一件事是把指令地址 + 1 ,这个循环就完成了。\n好,来看最后一个指令:0100 1101 ,解码得知是 store a 指令(把寄存器 a 的值放入内存), ram 地址 13 ,接下来,把地址传给 ram ,但这次不是 \u0026ldquo;允许读取\u0026rdquo; ,而是 \u0026ldquo;允许写入\u0026rdquo; 。同时,打开寄存器 a 的 \u0026ldquo;允许读取\u0026rdquo; ,这样就可以把寄存器 a 里的值,传给 ram 。\n\u0026gt; 存储 a 到 ram\n恭喜,我们刚运行了第一个电脑程序!它从内存中加载两个值,相加,然后把结果放回内存。\n刚刚是我一步步来讲的,我们人工切换 cpu 的状态 \u0026ldquo;取指令→解码→执行\u0026rdquo; ,但不是每台电脑里都有一个迷你 carrie anne ,其实是 \u0026ldquo;时钟\u0026rdquo; 来负责管理 cpu 的节奏。时钟以精确的间隔触发电信号,控制单元会用这个信号,推进 cpu 的内部操作,确保一切按步骤进行 - 就像罗马帆船的船头,有一个人负责按节奏的击鼓,让所有划船的人同步。.. 就像节拍器一样。节奏不能太快,因为就算是电也要一定时间来传输。cpu \u0026ldquo;取指令→解码→执行\u0026rdquo; 的速度叫 \u0026ldquo;时钟速度\u0026rdquo; ,单位是赫兹 - 赫兹是用来表示频率的单位,1 赫兹代表一秒 1 个周期。因为我花了大概 6 分钟,给你讲了 4 条指令 - 读取→读取→相加→存储 ,所以我的时钟速度大概是 0.03 赫兹,我承认我算数不快,但哪怕有人算数很快,最多也就是一秒一次,或 1 赫兹。\n\u0026gt; ‘时钟’ 哦\n第一个单芯片 cpu 是 \u0026ldquo;英特尔 4004\u0026rdquo; ,1971 年发布的 4 位 cpu ,它的微架构很像我们之前说的 cpu 。\n\u0026gt; 英特尔 4004 的微架构\n\u0026gt; 英特尔 4004\n虽然是第一个单芯片的处理器,但它的时钟速度达到了 740 千赫兹 - 每秒 74 万次,你可能觉得很快,但和如今的处理器相比不值一提,一兆赫兹是 1 秒 1 百万个时钟周期,你现在看视频的电脑或手机,肯定有几千兆赫兹 - 1 秒 10 亿次时钟周期。你可能听过有人会把计算机超频(overclocking),意思是修改时钟速度,加快 cpu 的速度 - 就像罗马帆船要撞另一艘船时,鼓手会加快敲鼓速度。芯片制造商经常给 cpu 留一点余地,可以接受一点超频,但超频太多会让 cpu 过热或产生乱码,因为信号跟不上时钟。你可能很少听说降频,但降频其实很有用,有时没必要让处理器全速运行,可能用户走开了,或者在跑一个性能要求较低的程序,把 cpu 的速度降下来,可以省很多电。省电对用电池的设备很重要,比如笔记本和手机。为了尽可能省电,很多现代处理器可以按需求加快或减慢时钟速度,这叫 \u0026ldquo;动态调整频率\u0026rdquo; ,加上时钟后,cpu 才是完整的。\n现在可以放到盒子里,变成一个独立组件。\n\u0026gt; 抽象的 cpu\n对!一层新的抽象!\nram,上集说过,是在 cpu 外面的独立组件,cpu 和 ram 之间用 \u0026ldquo;地址线\u0026rdquo;、\u0026ldquo;数据线\u0026rdquo; 和 \u0026ldquo;允许读/写线\u0026rdquo; 进行通信。\n虽然今天我们设计的 cpu 是简化版的,但我们提到的很多机制,依然存在于现代处理器里。\n下一集,我们要加强 cpu,给它扩展更多指令,同时开始讲软件。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/07/","summary":"\u003cp\u003ei.e. The Central Processing Unit(CPU)\u003c/p\u003e\n\u003cp\u003e今天我们讲 处理器(processors),提示下 - 这集可能是最难的一集,所以一旦你理解了,就会变得超厉害 der~ 😈\u003c/p\u003e\n\u003cp\u003e我们已经做了一个算术逻辑单元(ALU,Arithmetic and Logic Unit),输入二进制,它会执行计算。\u003cstrong\u003e我们还做了两种内存:寄存器(Registers) - 很小的一块内存,能存一个值;之后我们增大做出了 RAM ,RAM 是一大块内存,能在不同地址存大量数字。\u003c/strong\u003e 现在是时候把这些放在一起,组建计算机的 \u0026ldquo;心脏\u0026rdquo; 了,但这个 \u0026ldquo;心脏\u0026rdquo; 不会有任何包袱,比如人类情感。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 这一部分的描述真好,对上个章节的迷惑内容做了一个很不错的总结!*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc07 - 中央处理器"},{"content":"i.e. registers and ram\n上集,我们用逻辑门做了个简单 alu,它能执行算术 (arithmetic) 和逻辑 (logic) 运算 ,alu 里的 a 和 l 因此得名。当然,算出来之后如果扔掉就没什么意义了,得找个方法存起来,可能还要进行多个连续操作,这就用到计算机内存了。\n= 算出来,存起来。\n如果你在主机上打过一场长时间的对局,或玩困难模式的 \u0026ldquo;扫雷\u0026rdquo;(minesweeper),然后狗跑过来,被电源线绊倒,把插头拔了出来,你知道失去进度的痛苦 😈 真同情你 :(\n你损失数据的原因是,电脑用的是\u0026quot;随机存取存储器\u0026quot;,简称\u0026quot;ram\u0026quot;(random access memory),它只能在有电的情况下存储东西,比如游戏状态,另一种存储 (memory) 叫持久存储,电源关闭时数据也不会丢失,它用来存其他东西,我们之后会讨论存储 (memory) 的持久性问题。\n今天我们从简单开始 - 做只能存储 1 位的电路,之后再扩大,做出我们的内存模块,下次和 alu 结合起来,做出 cpu !\n只能存储 0 和 1 的电路 我们至今说过的电路都是单向的 - 总是向前流动,比如上集的 8 位 \u0026ldquo;脉动进位加法器\u0026rdquo;。但也可以做回向电路,把输出连回输入。\n我们拿一个 or 门试试,把输出连回输入,看看会发生什么。\n首先,两个输入都设为 0 ,\u0026ldquo;0 or 0\u0026rdquo; 是 0,所以电路输出 0 ;如果将 a 变成 1 ,\u0026ldquo;1 or 0\u0026rdquo; 为 1,所以输出 1 ;一转眼的功夫,输出回到 b ,or 门看到两个输入都是 1 ,\u0026ldquo;1 or 1\u0026rdquo; 仍然为 1,所以输出不变 。 如果将 a 变成 0,or 门依然输出 1 ,现在我们有个电路能记录 \u0026ldquo;1\u0026rdquo; ,然而有个小问题:这是永久的 ! 无论怎么试,都没法从 1 变回 0 。\n我们换成 and 门看看会怎样。\n开始时,a 和 b 都设 1 ,\u0026ldquo;1 and 1\u0026rdquo; 永远输出 1 ;如果之后 a 设为 0,由于是 and 门,输出会变成 0 ,这个电路能记录 0,和之前那个相反。就像之前,无论 a 设什么值,电路始终输出 0 。\n现在有了能存 0 和 1 的电路。\n= 或门存 1 ,与门存 0\n锁存器 为了做出有用的存储 (memory),我们把两个电路结合起来,这叫 \u0026ldquo;and-or 锁存器\u0026rdquo; 。它有两个输入:\u0026ldquo;设置\u0026quot;输入,把输出变成 1;\u0026ldquo;复位\u0026quot;输入,把输出变成 0 。如果\u0026quot;设置\u0026quot;和\u0026quot;复位\u0026quot;都是 0,电路会输出最后放入的内容,也就是说,它存住了 1 位的信息!\n\u0026gt; and-or 锁存器\n存储!这叫\u0026quot;锁存\u0026rdquo;(latch), 因为它\u0026quot;锁定\u0026quot;了一个值。放入数据的动作叫 \u0026ldquo;写入\u0026rdquo;,拿出数据的动作叫 \u0026ldquo;读取\u0026rdquo;。现在我们终于有办法存一个位了!超棒 !\n麻烦的是,用两条线 \u0026ldquo;设置\u0026quot;和\u0026quot;复位\u0026rdquo; 来输入,有点难理解。\n⬇️ ⬇️ ⬇️\n为了更容易用,我们希望只有一条输入线,将它设为 0 或 1 来存储值;还需要一根线来\u0026quot;启用\u0026quot;内存,启用时允许写入,没启用时就 \u0026ldquo;锁定\u0026rdquo; - 这条线叫 \u0026ldquo;允许写入线\u0026rdquo;。加一些额外逻辑门,可以做出这个电路,这叫 \u0026ldquo;门锁\u0026rdquo;,因为门可以打开和关上。\n1. 写什么 (0、1) 2. 是否允许写入\r现在有点复杂了,我们不想关心单独的逻辑门,所以我们提升一层抽象。\n把 \u0026ldquo;门锁\u0026rdquo; 放到盒子里 - 这个盒子能存一个 bit 。\n\u0026gt; 门锁\n我们来测一下新组件!一切从 0 开始,数据输入从 0 换到 1, 从 1 换到 0,什么也不会发生 - 输出依然是 0,因为 \u0026ldquo;允许写入线\u0026rdquo; 是关闭的,所以内容不会变化。所以要给 \u0026ldquo;允许写入线\u0026rdquo; 输入 1, \u0026ldquo;打开\u0026rdquo; 门,现在往 \u0026ldquo;数据线\u0026rdquo; 放 1,1 就能存起来了,注意输出现在是 1 了,成功!现在可以关掉 \u0026ldquo;允许写入线\u0026rdquo; ,输出会保持 1,现在不管给 \u0026ldquo;数据线\u0026rdquo; 什么值,输出都不会变。值存起来了!现在又打开 \u0026ldquo;允许写入线\u0026rdquo; \u0026ldquo;数据线\u0026rdquo; 设为 0,完成,\u0026ldquo;允许写入线\u0026rdquo; 关闭,输出 0,成功了!\n= 不难发现,允许写入线关闭的时候,上面的 and 门输出总为 0 。\n寄存器 当然,只能存 1 bit 没什么大用,肯定玩不了游戏,或做其它事情,但我们没限制只能用一个锁存器。如果我们并排放 8 个锁存器,可以存 8 位信息,比如一个 8 bit 数字,一组这样的锁存器叫 \u0026ldquo;寄存器\u0026rdquo; 。寄存器能存一个数字,这个数字有多少位,叫 \u0026ldquo;位宽\u0026rdquo; 。\n= 寄存器的‘位宽’就是你放了多少个‘锁存器’~~\n\u0026gt; 8 位寄存器 -\u0026gt; d 数据输入 q 数据输出 e 允许写入线\n早期电脑用 8 位寄存器,然后是 16 位,32 位,如今许多计算机都有 64 位宽的寄存器。写入寄存器前,要先启用里面所有锁存器,我们可以用一根线连接所有 \u0026ldquo;允许写入线\u0026rdquo;, 把它设为 1 ,然后用 8 条数据线发数据,然后将 \u0026ldquo;允许写入线\u0026rdquo; 设回 0 ,现在 8 位的值就存起来了。\n如果只有很少的位 (bits),把锁存器并排放置,也勉强够用了。\n64 位寄存器要 64 根数据线,64 根连到输出端。幸运的是,我们只要 1 根线启用所有锁存器,但加起来也有 129 条线了。如果存 256 位要 513 条线!而这需要不少的钱 💵,怎么办?解决方法是矩阵!\n= 哈,二维化!\n\u0026gt; 16*16 门锁矩阵\n在矩阵中,我们不并列排放锁存器,而是做成网格,存 256 位,我们用 16x16 网格的锁存器,有 16 行 16 列,要启用某个锁存器,就打开相应的 行线 和 列线。\n放大看看怎么做的。\n\u0026gt; 锁存器\n我们只想打开交叉处锁存器的 \u0026ldquo;允许写入线\u0026rdquo;,所有其他锁存器,保持关闭,我们可以用 and 门!只有 行线和列线 均为 1 ,and 门才输出 1,所以可以用选择单个锁存器。这种行/列排列法,用一根 \u0026ldquo;允许写入线\u0026rdquo; 连所有锁存器,为了让锁存器变成 \u0026ldquo;允许写入\u0026rdquo;, 行线,列线和 \u0026ldquo;允许写入线\u0026rdquo; 都必须是 1 ,每次只有 1 个锁存器会这样。代表我们可以只用一根 \u0026ldquo;数据线\u0026rdquo; 连所有锁存器来传数据。因为只有一个锁存器会启用,只有那个会存数据,其他锁存器会忽略数据线上的值,因为没有 \u0026ldquo;允许写入\u0026rdquo;。我们可以用类似的技巧,做\u0026quot;允许读取线\u0026quot;来读数据,从一个指定的锁存器,读取数据。所以对于 256 位的存储,只要 35 条线 - 1 条\u0026quot;数据线\u0026rdquo;, 1 条\u0026quot;允许写入线\u0026quot;, 1 条\u0026quot;允许读取线\u0026quot;,还有 16 行 16 列的线用于选择锁存器 (16+16=32, 32+3=35),这省了好多线!🤑\n💡 看,只需要这些线 :\r\u0026gt; 1 条数据线\r\u0026gt; 1 条允许写入线\r\u0026gt; 1 条允许读取线\r\u0026gt; 16 条行线\r\u0026gt; 16 条列线 但我们需要某种方法来唯一指定交叉路口。\n我们可以想成城市,你可能想和别人在第 12 大道和第 8 街的交界碰面 - 这是一个交叉点的地址,我们刚刚存了一位的地址是 \u0026ldquo;12 行 8 列\u0026rdquo;,由于最多 16 行,用 4 位就够了,12 用二进制表示为 1100 ,列地址也可以这样: 8 用二进制表示为 1000 ,刚才说的\u0026quot;12 行 8 列\u0026quot;可以写成 11001000 。\n为了将地址转成行和列,我们需要 \u0026ldquo;多路复用器\u0026rdquo; - 这个名字起码比 alu 酷一点,多路复用器有不同大小,因为有 16 行,我们需要 1 到 16 多路复用器。工作方式是:输入一个 4 位数字,它会把那根线,连到相应的输出线,如果输入 0000,它会选择第一列,如果输入 0001,会选择下一列,依此类推……\n\u0026gt; 会‘寻址’的多路复用器\n一个多路复用器处理行 (row) ,另一个多路复用器处理列 (column)。\n好吧,开始有点复杂了,那么把 256 位内存当成一个整体好了。 又提升了一层抽象! 它输入一个 8 位地址 :4 位代表列,4 位代表行(纵横经纬,用来唯一确定启动的锁存器位置)。我们还需要 \u0026ldquo;允许写入线\u0026rdquo; 和 \u0026ldquo;允许读取线\u0026rdquo;,最后,还需要一条数据线,用于读/写数据 。\n= 💡 提示,这个 ‘8 位地址’ 的含义是 - 定位使用 256 个锁存器中的哪一个。也就是说,虽然我们的 256 位‘寄存器’包含 256 个锁存器,但是同一时间,我们只能使用其中地址相对应的那一个而已。即同一时间,只能存储 1bit 的数。\n= ❓ 这里可能会产生疑惑?既然只能存 1bit 的数,为什么不直接使用 1 个锁存器来存储???\n= 关于‘寄存器’和‘内存’的区别,需要留意一下。‘寄存器’一般是指 cpu 上的高速存储,造价不菲,一般不大(128m 都算很大的了),‘内存’你可以理解为内存条,即 ram ,造价相对便宜,常见单条有 8g 、16g 了。\n可寻址内存 不幸的是,256 位的内存也没法做什么事,所以还要扩大规模,把它们并排放置,就像寄存器一样。一行 8 个,可以存一个 8 位数字,8 位也叫一个字节(byte)。为了存一个 8 位数字,我们同时给 8 个 256 位内存一样的地址,每个地址存 1 位,意味着这里总共能存 256 个字节 (byte)。\n\u0026gt; 看,我们获得了一个 1 byte(8 bit) 大小的可寻址内存了\n= 这里,我们将 8 个 ‘256 位的寄存器’并排连接,就可以存储 8 bit 的数。这个数的每一位,分别存在 ‘8 位地址’对应的 8 个 256 位寄存器的相应的锁存器中。\n= ❕ 也就是说,上图中的 ‘8 个并排寄存器’ ,共有 256 个 8 位地址,每个地址可以存储 1 byte(8 bit) 的数,最多可以存储 256 byte 的数。看,锁存器还是得到了充分利用的,这就解答了上面的疑问。\n= 好的吧,其实这里我是有点震惊的!原来一个 8 位可寻址内存就需要用 8 个 256 内存,每个内存的相同行列处(相同地址)的锁存器启用 - 存储 1 bit 。\n再次,为了简单,我们不管内部,不看作是一堆独立的存储模块和电路,而是看成一个整体的 可寻址内存 。我们有 256 个地址,每个地址能读或写一个 8 位值。\n\u0026gt; 256 byte 大小的内存 - 又抽象了\n= 上图其实就是前面 8 个并排‘256 位寄存器’的抽象。\n我们下集做 cpu 时会用到这个内存。\n现代计算机的内存扩展到上兆字节(mb)和千兆字节(gb)的方式,和我们这里做的一样,不断把内存打包到更大规模。随着内存地址增多,内存地址也必须增长。8 位最多能代表 256 个内存地址(1111 1111 是 255,0~255 一共 256 个数字),只有这么多。要给千兆或十亿字节的内存寻址,需要 32 位的地址。\n= 额,指数级增长,就离谱!指数 🐮 🍺 !\n内存的一个重要特性是:可以随时访问任何位置,因此叫 \u0026ldquo;随机存取存储器\u0026rdquo; ,简称 ram 。\n当你听到有人说 ram 有多大,他的意思是内存有多大。ram 就像人类的短期记忆,记录当前在做什么事。比如吃了午饭没,或有没有交电话费。\n这是一条真的内存,上面焊了 8 个内存模块。\n如果打开其中一个,然后放大,会看到 32 个内存方块。\n\u0026gt; 其中一个芯片\n放大其中一个方块,可以看到有 4 个小块。\n如果再放大,可以看到存一个\u0026quot;位\u0026quot;的矩阵,这个矩阵是 128 位 x 64 位,总共 8192 位。\n= 哈哈,这里很好的解答了阅览前方内容时产生的一个疑惑,是矩阵,而不一定是方阵。*\n每个方格 4 个矩阵,所以一个方格有 32768 个位 (8192 x 4 = 32768),而一共 32 个方格。总而言之,1 个芯片大约存 100 万位。ram 有 8 个芯片,所以总共 800 万位,也就是 1 兆字节(1 mb)。\n1 mb 如今不算大 - 这是 1980 年代的 ram。\n= 现在基本是 8g 、16g 大小的了,那么一个 16g 大小的内存就有 16 * 1024 * 1024 * 8 = 2^27 = 134,217,728 位。*\n= 目前最大为单条 64g,也就是 2^29 位,理论上目前电脑支持的最大运行内存为 128g (2^30)。\n如今你可以买到千兆字节(gb)的 ram,那可是数十亿字节的内存。\n今天,我们用锁存器做了一块 sram(静态随机存取存储器),还有其他类型的 ram,如 dram,闪存和 nvram ,它们在功能上与 sram 相似,但用不同的电路存单个位 - 比如用不同的逻辑门,电容器,电荷捕获或忆阻器。但根本上,这些技术都是矩阵层层嵌套,来存储大量信息。 就像计算机中的很多事情,底层其实都很简单,让人难以理解的,是一层层精妙的抽象。\n像一个越来越小的俄罗斯套娃。\n下周见。\n= 该部分的内存相对来说是有些抽象的,尤其是一些相关的概念可能会混淆 - 如‘寄存器’和‘内存’,它们的原理是一样的,只有‘寄存器’用的材料更好更贵,当然速度也更快。文章的后半部分,我们在纸上模拟出了一块可以存储 256 byte 的内存(8 个并排的 256 位寄存器组成 - 同样,这里用寄存器来描述好像是不太准确)。在真实的示例中,我们也看到了可以存储 1mb 的内存条组成。只能说,学无止境哦!不过基本原理相对来说是简单的,不得不说,现在的许多计算机课程跑偏的挺厉害的,基础原理和应用才是永远的神!*\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/06/","summary":"\u003cp\u003ei.e. Registers and RAM\u003c/p\u003e\n\u003cp\u003e上集,我们用逻辑门做了个简单 ALU,它能执行算术 (Arithmetic) 和逻辑 (Logic) 运算 ,ALU 里的 A 和 L 因此得名。当然,算出来之后如果扔掉就没什么意义了,得找个方法存起来,可能还要进行多个连续操作,这就用到计算机内存了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 算出来,存起来。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果你在主机上打过一场长时间的对局,或玩困难模式的 \u0026ldquo;扫雷\u0026rdquo;(Minesweeper),然后狗跑过来,被电源线绊倒,把插头拔了出来,你知道失去进度的痛苦 😈 真同情你 :(\u003c/p\u003e","title":"cscc06 - 寄存器 \u0026 内存"},{"content":"i.e. how computers calculate-the alu\n上集,我们谈了如何用二进制表示数字,比如二进制 00101010 是十进制的 42,表示和存储数字是计算机的重要功能。但真正的目标是计算(computation),有意义的处理数字,比如把两个数字相加,这些操作由计算机的 \u0026ldquo;算术逻辑单元 (arithmetic and logic unit)\u0026ldquo;处理,但大家会简称:alu。\nalu 是计算机的数学大脑,等你理解了 alu 的设计和功能之后,你就理解了现代计算机的基石。\nalu 就是计算机里负责运算的组件,基本其他所有部件都用到了它,先来看看这个美人。这可能是最著名的 alu,英特尔 74181 。1970 年发布时,它是第一个封装在单个芯片内的完整 alu ,这在当时是惊人的工程壮举。\n\u0026gt; 英特尔 74181\n今天我们用上周学的布尔逻辑门,做一个简单的 alu 电路,功能和 74181 一样。然后接下来几集,用它从头做出一台电脑,所以会有点复杂,但我觉得你们搞的定。\nalu 有 2 个单元,1 个算术单元和 1 个逻辑单元。\n算术单元 我们先讲\u0026quot;算术单元\u0026rdquo;,它负责计算机里的所有数字操作。比如加减法,它还做很多其他事情,比如给某个数字+1,这叫增量运算,我们之后会说。\n今天的重点是一切的根本 - \u0026ldquo;把两个数字相加\u0026rdquo; 。\n我们可以用单个晶体管一个个拼,把这个电路做出来,但很快就会复杂的难以理解,所以与其用晶体管,我们会像第 3 集 - 用更高层的抽象,用逻辑门来做。我们会用到 and,or,not 和 xor 逻辑门,最简单的加法电路, 是拿 2 个 bit 加在一起(bit 是 0 或 1),有 2 个输入:a 和 b, 1 个输出:就是两个数字的和。需要注意的是:a, b, 输出 ,这 3 个都是单个 bit ( 0 或 1 )。\n输入只有四种可能:\n前三个是 0 + 0 = 0 、 1 + 0 = 1 、0 + 1 = 1 。记住二进制里,1 与 true 相同,0 与 false 相同,这组输入和输出,和 xor 门的逻辑完全一样,所以我们可以把 xor 用作 1 位加法器(adder)。\n但第四个输入组合,1+1 是个特例 1 + 1 = 2(显然),但二进制里没有 2,上集说过,二进制 1+1 的结果是 0,1 进到下一位,和是 10 (二进制)。\n\u0026gt; 1 + 1 = 10 呢 ?\nxor 门的输出,只对了一部分, 1+1 输出 0 ,但我们需要一根额外的线代表 \u0026ldquo;进位\u0026rdquo;,只有输入是 1 和 1 时,进位才是 \u0026ldquo;true\u0026rdquo; ( 也就是 1 )。因为算出来的结果用 1 个 bit 存不下,方便的是,我们刚好有个逻辑门能做这个事!没那么复杂 - 就两个逻辑门而已。\n\u0026gt; 1 + 1 = 10 了(carry = 1, sum = 0)\n让我们抽象化。\n把 \u0026ldquo;半加器\u0026rdquo; 封装成一个单独组件 - 两个输入 a 和 b 都是 1 位 ,两个输出 \u0026ldquo;总和\u0026rdquo; 与 \u0026ldquo;进位\u0026rdquo;,这进入了另一层抽象。\n\u0026gt; 抽象出了‘半加器’\n如果想处理超过 1+1 的运算,我们需要\u0026quot;全加器\u0026rdquo;,半加器输出了进位,意味着,我们算下一列的时候,还有之后的每一列,我们得加 3 个位在一起,并不是 2 个。\n全加器复杂了一点点。\n全加器表格,有 3 个输入:a, b, c (都是 1 个 bit),所以最大的可能是 1 + 1 + 1,\u0026ldquo;总和\u0026quot;1 \u0026ldquo;进位\u0026quot;1 ,所以要两条输出线: \u0026ldquo;总和\u0026quot;和\u0026quot;进位\u0026rdquo; 。\n我们可以用 半加器 做 全加器,我们先用半加器将 a 和 b 相加,然后把 c 输入到第二个半加器,最后用一个 or 门检查进位是不是 true,这样就做出了一个全加器!\n\u0026gt; emm... ‘全加器’的诞生\n我们可以再提升一层抽象,把全加器作为独立组件。全加器会把 a,b,c 三个输入加起来,输出 \u0026ldquo;总和\u0026rdquo; 和 \u0026ldquo;进位\u0026rdquo;。\n现在有了新组件,我们可以相加两个 8 位数字。叫两个数字叫 a 和 b 好了,我们从 a 和 b 的第一位开始,叫 a0 和 b0 好了,现在不用处理任何进位,因为是第一次加法,所以我们可以用半加器,来加这 2 个数字,输出叫 sum0 ;现在加 a1 和 b1,因为 a0 和 b0 的结果有可能进位,所以这次要用全加器,除了 a1 和 b1,还要连上进位,输出叫 sum1 ;然后,把这个全加器的进位连到下个全加器的输入,处理 a2 和 b2;以此类推,把 8 个 bit 都搞定…… 注意每个进位是怎么连到下一个全加器的,所以叫 \u0026ldquo;8 位行波进位加法器\u0026rdquo; ,注意最后一个全加器有 \u0026ldquo;进位\u0026rdquo; 的输出。如果第 9 位有进位,代表着 2 个数字的和太大了,超过了 8 位,这叫 \u0026ldquo;溢出\u0026rdquo; (overflow)。一般来说 \u0026ldquo;溢出\u0026rdquo; 的意思是,两个数字的和太大了,超过了用来表示的位数,这会导致错误和不可预期的结果。\n\u0026gt; 8 位行波进位加法器\n著名的例子是,吃豆人用 8 位存当前关卡数,如果你玩到了第 256 关( 8 位 bit 最大表示 255),alu 会溢出,造成一连串错误和乱码(glitches),使得该关卡无法进行,这个 bug 成了厉害吃豆人玩家的代表。如果想避免溢出,我们可以加更多全加器,可以操作 16 或 32 位数字,让溢出更难发生,但代价是更多逻辑门,另外一个缺点是,每次进位都要一点时间,当然时间不久,因为电子移动的很快,但如今的量级是每秒几十亿次运算,所以会造成影响。所以,现代计算机用的加法电路有点不同,叫 \u0026ldquo;超前进位加法器\u0026rdquo;。它更快,做的事情是一样的 - 把二进制数相加。\n\u0026gt; 牛‘爆’的吃豆人\nalu 的算术单元,也能做一些其他数学运算,一般支持这 8 个操作 - 见下图。\n= 哪 8 个 ❓\n就像加法器一样,这些操作也是由逻辑门构成的。有趣的是,你可能注意到没有乘法和除法。因为简单的 alu 没有专门的电路来处理,而是把乘法用多次加法来实现。假设想算 12 x 5 ,这和把 \u0026ldquo;12\u0026rdquo; 加 5 次是一样的,所以要 5 次 alu 操作来实现这个乘法。很多简单处理器(processors)都是这样做的,比如恒温器,电视遥控器和微波炉,慢是慢,但是搞的定。然而笔记本和手机有更好的处理器,有专门做乘法的算术单元,你可能猜到了,乘法电路比加法复杂 - 没什么魔法,只是更多逻辑门,所以便宜的处理器没有。\n= 是的,只是逻辑门而已 ✔️\n好了,我们现在讲 alu 的另一半:逻辑单元。\n逻辑单元 逻辑单元执行逻辑操作,比如之前讨论过的 and,or 和 not 操作,它也能做简单的数值测试,比如一个数字是不是负数。\n例如,这是检查 alu 输出是否为 0 的电路,它用一堆 or 门检查其中一位是否为 1,哪怕只有一个 bit (位) 是 1,我们就知道那个数字肯定不是 0,然后用一个 not 门取反,所以只有输入的数字是 0,输出才为 1 。\n\u0026gt; 检查 alu 输出是否为 0 的电路\n以上就是 alu 的一个高层次概括。\nalu 我们甚至从零做了几个主要组件,比如行波进位加法器(ripple adder),它们只是一大堆逻辑门巧妙的连在一起而已。\n让我们回到视频开始时的 alu,英特尔 74181,和我们刚刚做的 8 位 alu 不同,74181 只能处理 4 位输入。也就是说,你刚做了一个比英特尔 74181 还好的 alu !\n其实 差不多啦。我们虽然没有全部造出来,但你理解了整体概念。\n74181 用了大概 70 个逻辑门,但不能执行乘除。但它向小型化迈出了一大步,让计算机可以更强大更便宜。4 位 alu 已经要很多逻辑门了,但我们的 8 位 alu 会需要数百个逻辑门。工程师不想在用 alu 时去想那些事情,所以想了一个特殊符号来代表它,看起来像一个大 \u0026ldquo;v\u0026rdquo; 。\n又一层抽象!\n我们的 8 位 alu 有两个输入,a 和 b,都是 8 位 (bits)。\n我们还需要告诉 alu 执行什么操作 ,例如加法或减法,所以我们用 4 位的操作代码。我们之后的视频会再细说。简言之,\u0026ldquo;1000\u0026quot;可能代表加法命令,\u0026ldquo;1100\u0026quot;代表减法命令,操作代码告诉 alu 执行什么操作,输出结果是 8 位的。alu 还会输出一堆标志(flag),\u0026ldquo;标志\u0026quot;是 1 位的,代表特定状态。比如相减两个数字,结果为 0 ,我们的零测试电路(前面做的)会将 零标志 设为 true(1),如果想知道两个数字是否相等,这个非常有用。如果想知道: a 是否小于 b,可以用 alu 来算 a 减 b,看 负标志 是否为 true 。如果是 true,我们就知道 a 小于 b 。最后,还有一条线连到加法器的进位,如果有溢出,我们就知道,这叫 溢出标志 。\n高级 alu 有更多标志,但这 3 个标志是 alu 普遍用的。其实,我们之后的视频会用到它们。\n现在你知道了,计算机是怎样在没有齿轮或杠杆的情况下进行运算。\n接下来两集,我们会用 alu 做 cpu ,但在此之前,计算机需要一些 \u0026ldquo;记忆\u0026rdquo; !\n我们下周会讲。\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/05/","summary":"\u003cp\u003ei.e. How Computers Calculate-the ALU\u003c/p\u003e\n\u003cp\u003e上集,我们谈了如何用二进制表示数字,比如二进制 \u003ccode\u003e00101010\u003c/code\u003e 是十进制的 42,表示和存储数字是计算机的重要功能。但\u003cstrong\u003e真正的目标是计算(computation)\u003c/strong\u003e,有意义的处理数字,比如把两个数字相加,这些操作由计算机的 \u0026ldquo;算术逻辑单元 (Arithmetic and Logic Unit)\u0026ldquo;处理,但大家会简称:ALU。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eALU 是计算机的数学大脑,等你理解了 ALU 的设计和功能之后,你就理解了现代计算机的基石。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eALU 就是计算机里负责运算的组件,基本其他所有部件都用到了它,先来看看这个美人。这可能是最著名的 ALU,英特尔 74181 。1970 年发布时,它是第一个封装在单个芯片内的完整 ALU ,这在当时是惊人的工程壮举。\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/05/imgs/94bce3a3e3a075eae098b8bf3b522246e0fd78c936596fb1e2a9084e9514019b.png\" width=\"300\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; 英特尔 74181\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e今天我们用上周学的布尔逻辑门,做一个简单的 ALU 电路,功能和 74181 一样。然后接下来几集,用它从头做出一台电脑,所以会有点复杂,但我觉得你们搞的定。\u003c/p\u003e","title":"cscc05 - 算术逻辑单元"},{"content":"i.e. representing numbers and letters with binary\n今天,我们讲计算机如何存储和表示数字,所以会有一些数学,不过别担心,你们的数学水平绝对够用了。\n上集我们讲了,怎么用晶体管(transistors)做逻辑门(logic gates),逻辑门可以判断布尔语句,布尔代数只有两个值:true 和 false 。\n但如果只有两个值,我们怎么表达更多东西 ❓ 这就需要数学了!\n进制和运算 上集提到,1 个二进制值可以代表 1 个数,我们可以把真和假 ,当做 1 和 0 。如果想表示更多东西,加位数就行了。和我们熟悉的十进制(decimal)一样,十进制只有 10 个数(0 到 9),要表示大于 9 的数,加位数就行了。\n二进制也可以这样玩。\n拿 263 举例,这个数字 \u0026ldquo;实际\u0026rdquo; 代表什么?2 个 100、6 个 10、3 个 1,加在一起,就是 263。注意每列有不同的乘数 - 100、10、1 ,每个乘数都比右边大十倍,因为每列有 10 个可能的数字(0 到 9),如果超过 9,要在下一列进 1 ,因此叫 \u0026ldquo;基于十的表示法\u0026rdquo; 或 十进制。\n二进制也一样,只不过是基于 2 而已。因为二进制只有两个可能的数, 1 和 0 ,意味着每个乘数必须是右侧乘数的两倍,就不是之前的 100、10、1 ,而是 4、2、1 。\n拿二进制数 101 举例,意味着有 1 个 \u0026ldquo;4\u0026rdquo;、 0 个 \u0026ldquo;2 、1 个 \u0026ldquo;1\u0026rdquo;,加在一起,得到十进制的 5 。\n为了表示更大的数字,二进制需要更多位数。拿二进制数 10110111 举例,我们可以用相同的方法转成十进制 - 1 x 128 ,0 x 64 ,1 x 32 ,1 x 16,0 x 8 ,1 x 4 ,1 x 2 ,1 x 1 ,加起来等于 183。\n二进制数的计算也不难。\n以十进制数 183 加 19 举例,首先 3 + 9,得到 12,然后位数记作 2,向前进 1,现在算 8+1+1=10,所以位数记作 0,再向前进 1,最后 1+1=2,位数记作 2,所以和是 202。\n二进制也一样。\n和之前一样,从个位开始,1+1=2,在二进制中也是如此,但二进制中没有 2,所以位数记作 0 ,进 1。就像十进制的例子一样,1+1,再加上进位的 1,等于 3,用二进制表示是 11。所以位数记作 1,再进 1,以此类推,最后得到这个数字,跟十进制 202 是一样的。\n= 二进制中,逢二进一。\n位和字节 二进制中,一个 1 或 0 叫一 \u0026ldquo;位\u0026rdquo;(bit)。\n上个例子我们用了 8 位 , 8 位能表示的最小数是 0, 8 位都是 0,最大数是 255,8 位都是 1,能表示 256 个不同的值,2 的 8 次方。你可能听过 8 位机,8 位图像,8 位音乐,意思是计算机里大部分操作都是 8 位 8 位这样处理的。但 256 个值不算多,意味着 8 位游戏只能用 256 种颜色。\n8 位是如此常见,以至于有专门的名字: 字节(byte) ❗\n1 字节 = 8 位 1 bytes = 8 bits 如果有 10 个字节,意味着有 80 位。你听过 千字节(kb)兆字节(mb)千兆字节(gb)等等。不同前缀代表不同数量级,就像 1 千克 = 1000 克,1 千字节 = 1000 字节,或 8000 位。mega 是百万字节(mb), giga 是十亿字节(gb)。如今你可能有 1 tb 的硬盘,8 万亿个 1 和 0。等等,我们有另一种计算方法 - 二进制里,1 千字节 = 2 的 10 次方 = 1024 字节。1000 也是千字节(kb)的正确单位,1000 和 1024 都对。\n你可能听过 32 位 或 64 位计算机,你现在用的电脑几乎肯定是其中一种,意思是 一块块处理数据,每块是 32 位或 64 位,这可是很多位。32 位能表示的最大数,是 43 亿左右 - 也就是 32 个 1,所以 instagram 照片很清晰,它们有上百万种颜色,因为如今都用 32 位颜色。当然,不是所有数都是正数,比如我上大学时的银行账户 t_t ……\n= cpu 一次性读取的位数。\n数的表示 我们需要有方法表示正数(positive)和负数(negative),大部分计算机用第一位表示正负 :1 是负,0 是正。用剩下 31 位来表示符号外的数值,能表示的数的范围大约是正 20 亿到负 20 亿。虽然是很大的数,但许多情况下还不够用,全球有 70 亿人口,美国国债近 20 万亿美元。所以 64 位数很有用,64 位能表达最大数大约是 9.2 × 10 ^ 18,希望美国国债在一段时间内不会超过这个数!\n重要的是(我们之后的视频会深入讲),计算机必须给内存(memory)中每一个位置,做一个 \u0026ldquo;标记\u0026rdquo;,这个标记叫 \u0026ldquo;地址\u0026rdquo;(addresses)(也叫‘位址’), 目的是为了方便存取(store and retrieve)数据。如今硬盘已经增长到 gb 和 tb,上万亿个字节!内存地址也应该有 64 位。\n除了负数和正数,计算机也要处理非整数(not whole numbers),比如 12.7 和 3.14,或\u0026quot;星历 43989.1\u0026rdquo; - 这叫 浮点数(floating point numbers)。因为小数点可以在数字间浮动,有好几种方法 表示浮点数,最常见的是 ieee 754 标准,你以为只有历史学家取名很烂吗?它用类似科学计数法的方法,来存十进制值,例如,625.9 可以写成 0.6259 × 10 ^ 3 ,这里有两个重要的数:.6259 叫 \u0026ldquo;有效位数\u0026rdquo;(significand) , 3 是指数(exponent)。\n\u0026gt; 计算机中浮点数的表示方法\n在 32 位浮点数中,第 1 位表示数的符号 —— 正或负,接下来 8 位存指数,剩下 23 位存有效位数。\n= 以上为正、负 \u0026amp; 整数、浮点数在计算机中的表示,‘数’的表示任务已达成 🎉\n字符的表示 好了,聊够数了,但你的名字是字母(letters)组成的 !所以我们也要表示文字(text),与其用特殊方式来表示字母,计算机可以用数表示字母 - 最直接的方法是给字母编号:a 是 1,b 是 2,c 是 3,以此类推。\n\u0026gt; 人才啊·培根\n著名英国作家 弗朗西斯·培根(francis bacon),曾用 5 位序列 来编码英文的 26 个字母。在十六世纪传递机密信件,五位(bit)可以存 32 个可能值(2^5) - 这对 26 个字母够了,但不能表示 标点符号,数字和大小写字母。\n= 在内存中,字符也是用数字表示的,本质上是一张数字与字符一一对应的编码表!\nascii,美国信息交换标准代码(the american standard code for information interchange),发明于 1963 年,ascii 是 7 位代码,足够存 128 个不同值。范围扩大之后,可以表示大写字母,小写字母,数字 0 到 9, @ 这样的符号,以及标点符号(punctuation marks)。举例,小写字母 a 用数字 97 表示,大写字母 a 是 65,: (colon)是 58, ) 是 41。\na a : ) 97 65 58 41 ascii 甚至有特殊命令符号,比如换行符(newline),用来告诉计算机换行。在老计算机系统中,如果没换行符,文字会超出屏幕。\n因为 ascii 是个很早的标准,所以它被广泛使用,让不同公司制作的计算机,能互相交换数据,这种通用交换信息的能力叫 \u0026ldquo;互操作性\u0026rdquo;。但有个限制:它是为英语设计的。\n幸运的是,一个字节有 8 位,而不是 7 位,128 到 255 的字符渐渐变得常用,这些字符以前是空的,是给各个国家自己 \u0026ldquo;保留使用的\u0026rdquo; 。在美国,这些额外的数字主要用于编码附加符号,比如数学符号,图形元素和常用的重音字符。另一方面,虽然拉丁字符被普遍使用,在俄罗斯,他们用这些额外的字符表示西里尔字符,而希腊电脑用希腊字母,等等。这些保留下来给每个国家自己安排的空位,对大部分国家都够用,问题是,如果在土耳其电脑上打开拉脱维亚语 写的电子邮件,会显示乱码。\n\u0026gt; 8 位远远不够啊。..\n随着计算机在亚洲兴起,这种做法彻底失效了,中文和日文这样的语言有数千个字符,根本没办法用 8 位来表示所有字符!为了解决这个问题,每个国家都发明了多字节编码方案,但相互不兼容,日本人总是碰到编码问题,以至于专门有词来称呼:\u0026ldquo;mojibake\u0026rdquo; 意思是 乱码。\n所以 unicode 诞生了 - 统一所有编码的标准。设计于 1992 年,解决了不同国家不同标准的问题,unicode 用一个统一编码方案,最常见的 unicode 是 16 位的,有超过一百万个位置,对所有语言的每个字符都够了,100 多种字母表加起来占了 12 万个位置,还有位置放数学符号,甚至 emoji 。\n就像 ascii 用二进制来表示字母一样,其他格式 - 比如 mp3 或 gif,用二进制编码声音/颜色,表示照片,电影,音乐。重要的是,这些标准归根到底是一长串位 。短信,这个 youtube 视频,互联网上的每个网页,甚至操作系统,只不过是一长串 1 和 0 。\n下周,我们会聊计算机怎么操作二进制,初尝\u0026quot;计算\u0026quot;的滋味。\n感谢观看,下周见。\n----------------\n你可以在我的另一篇博文中了解更多关于字符编码的知识 \u0026ndash; 《字符集和字符编码》 。\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/04/","summary":"\u003cp\u003ei.e. Representing Numbers and Letters with Binary\u003c/p\u003e\n\u003cp\u003e今天,我们讲计算机如何存储和表示数字,所以会有一些数学,不过别担心,你们的数学水平绝对够用了。\u003c/p\u003e\n\u003cp\u003e上集我们讲了,怎么用晶体管(transistors)做逻辑门(logic gates),逻辑门可以判断布尔语句,布尔代数只有两个值:\u003ccode\u003eTrue\u003c/code\u003e 和 \u003ccode\u003eFalse\u003c/code\u003e 。\u003c/p\u003e\n\u003cp\u003e但如果只有两个值,我们怎么表达更多东西 ❓ 这就需要数学了!\u003c/p\u003e","title":"cscc04 - 二进制"},{"content":"i.e. boolean-logic-and-logic-gates\n今天我们开始\u0026quot;抽象\u0026quot;(abstraction)的旅程!不用管底层细节,把精力用来构建更复杂的系统。\n上集,我们谈了计算机最早是机电设备(electromechanical),一般用十进制(decimal)计数,比如用齿轮数来代表十进制, 再到晶体管计算机。\n幸运的是,只用 ‘开/关’ 两种状态也可以代表信息,这叫二进制,意思是\u0026quot;用两种状态表示\u0026quot;,就像自行车有两个轮,双足动物有两条腿。你可能觉得两种状态不多,你是对的!但如果只需要表示 true 和 false,两个值就够了。电路闭合,电流流过,代表 \u0026ldquo;真\u0026rdquo;;电路断开,无电流流过,代表\u0026quot;假\u0026quot;。二进制也可以写成 1 和 0 而不是 true 和 false,只是不同的表达方式罢了。\n我们下集(episode)会讲更多细节。\n晶体管的确可以不只是开/关,还可以让不同大小的电流通过。一些早期电子计算机是三进制的,有 3 种状态,甚至五进制,5 种状态。问题是,状态越多,越难区分信号。如果手机快没电了或者附近有电噪音,因为有人在用微波炉(microwave),信号可能会混在一起。.. 而每秒百万次变化的晶体管会让这个问题变得更糟!所以我们把两种信号尽可能分开,只用\u0026quot;开\u0026quot;和\u0026quot;关\u0026quot;两种状态,可以尽可能减少这类问题。\n\u0026gt; on \u0026amp; off\n计算机用二进制的另一个原因是,有一整个数学分支存在,专门处理\u0026quot;真\u0026quot;和\u0026quot;假\u0026quot;。它已经解决了所有法则和运算 - 叫\u0026quot;布尔代数\u0026quot;(boolean algebra)!\n乔治·布尔(george boole)是布尔二字的由来,是一位 19 世纪自学成才的英国数学家,他有兴趣用数学式子扩展亚里士多德基于哲学的逻辑方法,布尔用逻辑方程系统而正式的证明真理 (truth)。他在 1847 年的第一本书\u0026quot;逻辑的数学分析\u0026quot;中介绍过,在\u0026quot;常规\u0026quot;代数里 - 你在高中学的那种 - 变量的值是数字,可以进行加法或乘法之类的操作,但在布尔代数中,变量的值是 true 和 false,能进行逻辑操作。\n布尔逻辑 布尔代数中有三个基本操作:not, and 和 or 。这些操作非常有用,我们一个个来看:\n= 布尔逻辑的本质是什么呢?很简单,一个或若干个输入(真或假),经过若干次逻辑运算,最终得到一个确定的输出(真或假)的过程!\nnot not 操作把布尔值反转,把 true 进行 not 就会变成 false,反之亦然。我们可以根据 not 操作的输入和输出,做出这个表。\ninput output ture false false true \u0026gt; not 真值表\n酷的地方是 - 用晶体管可以轻松实现这个逻辑。\n= 晶体管的用武之地~~\n上集说过,晶体管只是电控制的开关 - 有 3 根线:2 根电极和 1 根控制线。\n控制线通电时,电流就可以从一个电极流到另一个电极,就像水龙头一样 - 打开水龙头,就有水流出来,关掉水龙头,就没水了。可以把控制线当做输入 ( input ),底部的电极当做输出(output),所以 1 个晶体管,有一个输入和一个输出。如果我们打开输入(input on) ,输出也会打开(output on),因为电流可以流过。如果关闭输入(input off),输出也会关闭(output off),因为电流无法通过。或者用布尔术语来说,输入为真,输出为真,输入为假,输出为假,我们也可以把这个做成\u0026quot;真值表\u0026quot;。\n这个电路没什么意思,因为它没做什么事 - 输入和输出是一样的。\n但我们可以稍加修改,实现 not 。\n与其把下面那根线当做 输出,我们可以把 输出 放到上面。如果打开 输入,电流可以流过然后 \u0026ldquo;接地\u0026rdquo;,输出就没有电流,所以输出是 off 。如果用水来举例,就像家里的水都从一个大管子流走了,打开淋浴头一点水也没有。\n如果输入是 on,输出是 off 。当输入是 off,电流没法接地,就流过了输出,所以输出是 on ;当输入是 off,电流没法接地,就流过了输出,所以输出是 on 。\n如果输入是 off,输出是 on 。\n和 not 操作表一样!太棒了!我们做了个有点用的电路!🎉\n我们叫它 \u0026ldquo;not 门\u0026rdquo;,之所以叫 \u0026ldquo;门\u0026rdquo;,是因为它能控制电流的路径 。\nand \u0026ldquo;and\u0026quot;操作有 2 个输入,1 个输出。如果 2 个输入都是 true,输出才是 true 。和上次一样,可以给\u0026quot;and\u0026quot;做个表 。\ninput a input b output true true true true false false false true false false false false \u0026gt; and 真值表\n为了实现 \u0026ldquo;and 门\u0026rdquo;,我们需要 2 个晶体管连在一起,这样有 2 个输入和 1 个输出。如果只打开 a,不打开 b ,电流无法流到 output,所以输出是 false ;如果只打开 b,不打开 a ,也一样,电流无法流到 output ;只有 a 和 b 都打开了,output 才有电流 。\nor 最后一个是 or (前面讲了 not 和 and)。\ninput a input b output true true true true false true false true true false false false \u0026gt; or 真值表\n只要 2 个输入里,其中 1 个是 true,输出就是 true ,只有 2 个输入都是 false,or 的结果才是 false 。\n实现 \u0026ldquo;or 门\u0026rdquo; 除了晶体管还要额外的线,不是串联起来,而是并联。然后左边这条线有电流输入,我们用\u0026quot;小拱门\u0026quot;代表 2 条线没连在一起,只是跨过而已,虽然看起来像连在一起。\n如果 a 和 b 都是 off,电流无法流过,所以输出是 off ;如果只打开 a,电流可以流过,输出是 on ;如果只打开 b 也一样;只要 a or b 是 on, 输出就是 on 。\n好,现在 not 门,and 门,or 门 都搞定了。\n我们可以进行一次抽象!\nnot 门的画法是三角形前面一个圆点,and 门用 d 表示 ,or 门用太空船表示 。\u0026ldquo;d 形状和太空船\u0026quot;不是标准叫法,只是我喜欢这样叫而已。我们可以用这种方法表示它们,构建更大的组件,就不会变得很复杂。\nxor 晶体管和电线依然在那里,我们只是用符号来代表而已。除了前面说的三个、n 另一个有用的布尔操作叫 \u0026ldquo;异或\u0026rdquo;,简称 xor 。\ninput a input b output true true false true false true false true true false false false \u0026gt; xor 真值表\nxor 就像普通 or,但有一个区别:如果 2 个输入都是 true,xor 输出 false 。想要 xor 输出 true ,一个输入必须是 true,另一个必须是 false 。\n就像你出去吃晚饭,你点的饭要么配沙拉,要么配汤 - 你不能两个都要!\n用晶体管实现 xor 门有点烧脑子,但我可以展示一下,怎么用前面提到的 3 种门来做 xor 门。\n\u0026gt; xor 的组成\n= 组合的威力!\n我们有 2 个输入,a 和 b ,还有 1 个输出。我们先放一个 or 门,因为 or 和 xor 的逻辑表很像。只有 1 个问题 - 当 a 和 b 都是 true 时 , or 的输出和想要的 xor 输出不一样。我们想要 false,xor 超有用的,我们下次再说它。因为超有用,工程师给了它一个符号,一个 or 门 + 一个笑脸。\n重要的是,现在可以把 xor 放入\u0026quot;工具箱\u0026quot;了。不用担心 xor 具体用了几个门,这几个门又是怎么用晶体管拼的,或电子是怎么流过半导体的。\n再次向上抽象。\n工程师设计处理器(processors)时,很少在晶体管的层面上思考,而是用更大的组件,比如逻辑门,或者由逻辑门组成的更大组件。我们以后会讲,就算是专业程序员,也不用考虑逻辑是怎样在物理层面实现的。\n我们从电信号开始,到现在第一次表示数据 - 真和假 - 开始有点\u0026quot;计算\u0026quot;的感觉了。\n仅用这集讲的 逻辑门,我们可以判断复杂的语句 比如:[如果是 john green] and [下午 5 点后] or [周末] and [在比萨店附近],那么 \u0026quot;john 想要比萨\u0026quot; = 真 。\n我都说饿了,下周见。\n","date":"2022-08-17","permalink":"https://loveminimal.github.io/posts/cscc/03/","summary":"\u003cp\u003ei.e. boolean-logic-and-logic-gates\u003c/p\u003e\n\u003cp\u003e今天我们开始\u0026quot;抽象\u0026quot;(abstraction)的旅程!不用管底层细节,把精力用来构建更复杂的系统。\u003c/p\u003e\n\u003cp\u003e上集,我们谈了计算机最早是机电设备(electromechanical),一般用十进制(decimal)计数,比如用齿轮数来代表十进制, 再到晶体管计算机。\u003c/p\u003e\n\u003cp\u003e幸运的是,\u003cstrong\u003e只用 \u003ccode\u003e‘开/关’\u003c/code\u003e 两种状态也可以代表信息\u003c/strong\u003e,这叫\u003ccode\u003e二进制\u003c/code\u003e,意思是\u0026quot;用两种状态表示\u0026quot;,就像自行车有两个轮,双足动物有两条腿。你可能觉得两种状态不多,你是对的!但如果只需要表示 \u003ccode\u003etrue\u003c/code\u003e 和 \u003ccode\u003efalse\u003c/code\u003e,两个值就够了。电路闭合,电流流过,代表 \u0026ldquo;真\u0026rdquo;;电路断开,无电流流过,代表\u0026quot;假\u0026quot;。二进制也可以写成 \u003ccode\u003e1\u003c/code\u003e 和 \u003ccode\u003e0\u003c/code\u003e 而不是 \u003ccode\u003etrue\u003c/code\u003e 和 \u003ccode\u003efalse\u003c/code\u003e,只是不同的表达方式罢了。\u003c/p\u003e","title":"cscc03 - 布尔逻辑和逻辑门"},{"content":"i.e. electronic computing\n上集讲到 20 世纪初,当时的早期计算设备都针对特定用途,比如制表机(tabulating machines),大大推进了政府和企业。它们帮助,甚至代替了人工。然而人类社会的规模在以前所未有的速度增长,20 世纪上半叶,世界人口几乎翻倍。一战动员 7 千万人,二战 1 亿多人。全球贸易和运输更加紧密,工程和科学的复杂度也达到新高。我们甚至开始考虑造访其他行星,复杂度的增高导致数据量暴增,人们需要更多自动化,更强的计算能力。\n很快,柜子大小的计算机变成房间大小,维护费用高,而且容易出错,而正是这些机器为未来的创新打下基础。\n继电器 最大的机电计算机之一是哈佛马克一号,ibm 在 1944 完成建造,给二战同盟国建造的。它有 76 万 5 千个组件,300 万个连接点和 500 英里长的导线。为了保持内部机械装置同步,它有一个 50 英尺的传动轴,由一个 5 马力的电机驱动,这台机器最早的用途之一 是给\u0026quot;曼哈顿计划\u0026quot;跑模拟。\n这台机器的大脑是 \u0026ldquo;继电器\u0026rdquo;(relays),继电器是:用电控制的机械开关。继电器里,有根\u0026quot;控制线路\u0026quot;,控制电路是开还是关。\u0026ldquo;控制线路\u0026quot;连着一个线圈,当电流流过线圈,线圈产生电磁场,吸引金属臂,从而闭合电路。你可以把继电器想成水龙头,把控制线路想成水龙头把,打开水龙头,水会流出来,关闭水龙头,水就没有了。继电器是一样的,只不过控制的是电子而不是水。这个控制电路可以连到其他电路,比如马达,马达让计数齿轮 +1,就像上集中 hollerith 的制表机一样。\n= 电磁原理…… 另外,看,最初的开始就是为了 ‘控制连通’ ,区分开关,划分阴阳。\n不幸的是,继电器内的机械臂有质量,因此无法快速开关。\n1940 年代一个好的继电器 1 秒能翻转 50 次,看起来好像很快,但还不够快,不足以解决复杂的大问题。哈佛马克一号,1 秒能做 3 次加法或减法运算,一次乘法要花 6 秒,除法要花 15 秒。更复杂的操作,比如三角函数,可能要一分钟以上。\n除了速度慢,另一个限制是齿轮磨损。\n任何会动的机械都会随时间磨损,有些部件会完全损坏,有些则是变黏,变慢,变得不可靠,并且随着继电器数量增加,故障概率也会增加。哈佛马克一号有大约 3500 个继电器,哪怕假设继电器的使用寿命是 10 年,也意味着平均每天得换一个故障继电器!这个问题很严重,因为有些重要运算要运行好几天,而且还有更多其他问题要考虑。\n这些巨大,黑色,温暖的机器也会吸引昆虫 🦟。\n\u0026gt; ha, bug...\n1947 年 9 月,哈佛马克 2 型的操作员从故障继电器中,拔出一只死虫。grace hopper(这位我们以后还会提到)曾说,\u0026ldquo;从那时起,每当电脑出了问题,我们就说它出了 bug(虫子)\u0026quot;,这就是术语 \u0026ldquo;bug\u0026rdquo; 的来源。\n显然,如果想进一步提高计算能力,我们需要更快更可靠的东西,来替代继电器。幸运的是,替代品已经存在了!\n真空管 在 1904 年,英国物理学家 \u0026ldquo;约翰·安布罗斯·弗莱明\u0026rdquo;,开发了一种新的电子组件,叫 \u0026ldquo;热电子管\u0026rdquo;。把两个电极(electrodes)装在一个气密的玻璃灯泡里,这是世上第一个真空管(vacuum tube)。其中一个电极可以加热,从而发射电子(electrons),这叫 \u0026ldquo;热电子发射\u0026rdquo;。另一个电极会吸引电子,形成\u0026quot;电龙头\u0026quot;的电流,但只有带正电才行。如果带负电荷或中性电荷,电子就没办法被吸引,越过真空区域,因此没有电流。\n电流只能单向流动的电子部件叫 \u0026ldquo;二极管\u0026rdquo;(diode),但我们需要的是,一个能开关电流的东西。\n= ⚡️ 控制联通!控制联通!控制联通!\n幸运的是,不久之后在 1906 年,美国发明家 \u0026ldquo;李·德富雷斯特\u0026rdquo;,他在\u0026quot;弗莱明\u0026quot;设计的两个电极之间,加入了第三个 \u0026ldquo;控制\u0026rdquo; 电极,向\u0026quot;控制\u0026quot;电极施加正电荷,它会允许电子流动,但如果施加负电荷,它会阻止电子流动。因此通过控制线路,可以断开或闭合电路,和继电器的功能一样。\n\u0026gt; 真空管 但重要的是,真空管内没有会动的组件,这意味着更少的磨损。更重要的是,每秒可以开闭数千次。因此这些 \u0026ldquo;三极真空管\u0026rdquo;(triode vacuum tubes)成为了无线电,长途电话以及其他电子设备的基础,持续了接近半个世纪。我应该提到,真空管不是完美的,它们有点脆弱,并且像灯泡一样会烧坏,但比起机械继电器是一次巨大进步。\n起初,真空管非常昂贵,收音机一般只用一个,但计算机可能要上百甚至上千个电气开关。但到了 1940 年代,它的成本和可靠性得到改进,可以用在计算机里,至少有钱人负担得起,比如政府。\n这标志着计算机从机电转向电子。\n💭 我们来进入思想泡泡\n第一个大规模使用真空管的计算机是 \u0026ldquo;巨人 1 号\u0026rdquo;,由工程师 tommy flowers 设计,完工于 1943 年 12 月。巨人 1 号 在英国的\u0026quot;布莱切利园\u0026rdquo;, 用于破解纳粹通信,听起来可能有点熟,因为 2 年前 阿兰·图灵(他经常被称为\u0026quot;计算机科学之父\u0026rdquo;)也在\u0026quot;布莱切利园\u0026quot;做了台机电装置,叫 \u0026ldquo;bombe\u0026rdquo;,这台机器的设计目的是破解纳粹\u0026quot;英格码\u0026quot;通讯加密设备,但 bombe 严格来说不算计算机。我们之后会讨论\u0026quot;阿兰·图灵\u0026quot;的贡献。总之,巨人 1 号有 1600 个真空管,总共造了 10 台巨人计算机,来帮助破解密码。巨人被认为是第一个可编程的电子计算机,编程的方法是把几百根电线插入插板(plugboards),有点像老电话交换机,这是为了让计算机执行正确操作。虽然\u0026quot;可编程\u0026quot;(programmable) ,但还是要配置它。\n\u0026gt; eniac - 世上第一个真正的通用、可编程的电子计算机\n电子数值积分计算机 \u0026ldquo;eniac\u0026quot;几年后在 1946 年,在\u0026quot;宾夕法尼亚大学\u0026quot;完成建造,设计者是 john mauchly 和 j. presper eckert,这是世上第一个真正的通用,可编程,电子计算机。eniac 每秒可执行 5000 次十位数加减法,比前辈快了很多倍,它运作了十年。据估计,它完成的运算,比全人类加起来还多。因为真空管很多,所以故障很常见,eniac 运行半天左右就会出一次故障。\n谢了 思想泡泡\n到 1950 年代,真空管计算机都达到了极限。美国空军的 an/fsq-7 计算机于 1955 年完成,是 \u0026ldquo;sage\u0026rdquo; 防空计算机系统的一部分,之后的视频还会提到。\n为了降低成本和大小,同时提高可靠性和速度,我们需要一种新的电子开关。\n晶体管 1947 年,贝尔实验室科学家 john bardeen,walter brattain,william shockley,发明了 晶体管(transistor),一个全新的计算机时代诞生了!\n晶体管的物理学相当复杂,牵扯到量子力学(quantum mechanics),所以我们只讲基础。晶体管就像之前提过的\u0026quot;继电器\u0026quot;或\u0026quot;真空管\u0026rdquo;,它是一个开关,可以用控制线路来控制开或关。晶体管有两个电极,电极之间有一种材料隔开它们,这种材料有时候导电,有时候不导电,这叫\u0026quot;半导体\u0026quot;(semiconductor)。控制线连到一个 \u0026ldquo;门\u0026rdquo;(gate)电极,通过改变 \u0026ldquo;门\u0026rdquo; 的电荷。我们可以控制半导体材料的导电性,来允许或不允许电流流动,就像之前的水龙头比喻。\n\u0026gt; 第一个晶体管\n贝尔实验室(bell labs)的第一个晶体管就展示了巨大的潜力,每秒可以开关 10,000 次,而且,比起玻璃制成小心易碎的真空管,晶体管是固态的。晶体管可以远远小于继电器或真空管,导致更小更便宜的计算机,比如 1957 年发布的 ibm 608 - 第一个完全用晶体管,而且消费者也可以买到的计算机,它有 3000 个晶体管,每秒执行 4500 次加法,每秒能执行 80 次左右的乘除法。ibm 很快把所有产品都转向了晶体管,把晶体管计算机带入办公室,最终引入家庭,如今,计算机里的晶体管小于 50 纳米,而一张纸的厚度大概是 10 万纳米。晶体管不仅小,还超级快 - 每秒可以切换上百万次,并且能工作几十年。\n很多晶体管和半导体的开发在\u0026quot;圣克拉拉谷\u0026quot;,这个地方在加州,位于\u0026quot;旧金山\u0026quot;和\u0026quot;圣荷西\u0026quot;之间,而生产半导体最常见的材料是 \u0026ldquo;硅\u0026rdquo;,所以这个地区被称为 \u0026ldquo;硅谷\u0026rdquo;。甚至 william shockley 都搬了过去,创立了\u0026quot;肖克利半导体\u0026quot;,里面的员工后来成立了\u0026quot;仙童半导体\u0026quot;,这里面的员工后来创立了英特尔 - 当今世界上最大的计算机芯片制造商。\n好了,我们从\u0026quot;继电器\u0026quot;到\u0026quot;真空管\u0026quot;到\u0026quot;晶体管\u0026quot;,我们可以让电路开闭得非常非常快,但我们是如何用晶体管做计算的?我们没有马达和齿轮啊?\n= 开关的过程,就是阴阳转换的过程,信息变化的过程!\n我们接下来几集会讲,感谢观看,下周见。\n","date":"2022-08-15","permalink":"https://loveminimal.github.io/posts/cscc/02/","summary":"\u003cp\u003ei.e. Electronic Computing\u003c/p\u003e\n\u003cp\u003e上集讲到 20 世纪初,当时的早期计算设备都针对特定用途,比如制表机(tabulating machines),大大推进了政府和企业。它们帮助,甚至代替了人工。然而人类社会的规模在以前所未有的速度增长,20 世纪上半叶,世界人口几乎翻倍。一战动员 7 千万人,二战 1 亿多人。全球贸易和运输更加紧密,工程和科学的复杂度也达到新高。我们甚至开始考虑造访其他行星,复杂度的增高导致数据量暴增,人们需要更多自动化,更强的计算能力。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/cscc/02/imgs/dde01eb023647fbef243f8ce436363ef4ee215027d014314260ee97fa00187fb.png\" width=\"400\" /\u003e \r\n\u003cp\u003e很快,柜子大小的计算机变成房间大小,维护费用高,而且容易出错,而正是这些机器为未来的创新打下基础。\u003c/p\u003e","title":"cscc02 - 电子计算机"},{"content":"i.e. early computing\nhello world!我是 carrie anne,欢迎收看计算机科学速成课(crash course computer science)!\n\u0026gt; carrie anne 小姐姐\n在这个系列中,我们会学习 bits(位),bytes(字节),晶体管(transistors),逻辑门(logic gates),一直到操作系统,虚拟现实和机器人!我们要学很多东西,但预先说明,我们 不会 教你怎么编程,我们会从高层次上纵览一系列计算机话题。\n计算机是当今世界的命脉,如果突然关掉所有的计算机,电网会关闭,车辆会相撞,飞机会坠毁,净水厂会关闭,证券市场会停止运作,装满食物的卡车不知运往何方,员工得不到薪水,甚至很多和计算机无关的东西,例如 dftba 的 t 恤和我现在坐的椅子也都是在计算机管理的工厂中制造的。\n计算机改变了我们生活中几乎所有方面。\n我们也不是第一次遇到推动全球发展的科技了。\n工业革命(industrial revolution)中生产能力的提高,大幅提升了农业,工业,畜牧业的规模。机械化导致更好的收成,更多的食物,商品可以大批量生产。旅行和通讯变得更便宜更快,生活质量变得更好。\n\u0026gt; 蒸气车 计算机和工业革命有一样的影响。\n从自动化农业和医疗设备到全球通信和教育机会,还有虚拟现实和无人驾驶汽车等新领域,现在这个时代很可能会被后人总结成 \u0026ldquo;信息时代\u0026rdquo;。\n你的智能手机中有数十亿个晶体管,看起来好像很复杂,但实际上它是很简单的机器,通过一层层的 抽象(abstraction) 来做出复杂操作。\n在这个系列中,我们会一层层讲解,从最底层的 1 和 0,到逻辑门,cpu,操作系统,整个互联网,以及更多~~\n不用担心,正如在网上买 t 恤的人不用知道网站代码是怎么写的,设计师不用知道数据包(packets)是怎么传输的,设计路由器的工程师不用理解晶体管的逻辑。\n本系列中每个视频会接着上集继续讲,但并不依赖前面的视频。等这个系列结束后,希望你能了解计算机在你的人生以及社会中扮演什么角色,以及这个人类史上最伟大的发明(可以这样说啦)是怎么开始的。\n它对未来还会有更大的影响。\n但深入之前,我们应该从计算的起源讲起,虽然电子计算机才出现不久,但人类对计算的需求早就有了。\n公认最早的计算设备是算盘(abacus),发明于\u0026quot;美索不达米亚\u0026quot;,大约公元前 2500 年。它是手动计算器,用来帮助加减数字,它存储着当前的计算状态,类似于如今的硬盘。人们制造算盘,是因为社会的规模已经超出个人心算的能力。一个村庄可能有上千个人和上万头牛。\n= “公认”?算盘起源,此处存疑,待求证。\n算盘有很多变种,但我们来看一个基础版,每行代表 10 的不同次方。最底下那行,一个珠子代表 10 的 0 次方,也就是 1,再上面一行是 10 的 1 次方(也就是 10),再上面一行是 10 的 2 次方 (以此类推)……\n假设最底部的 3 颗珠子,代表 3 头牛。假设再买 4 头牛,只需要向右移动 4 颗珠子,共 7 个珠子。但如果再买 5 头,珠子就不够用了,所以把所有珠子移回左边。在第二排把 1 颗珠子向右移动,代表 10,然后最底下那行,向右移动 2 颗珠子,代表 12。\n这种方法处理大数字很有效。假设要表示 1251,从下往上:第一行移 1 个,第二行移 5 个,第三行移 2 个,第四行移 1 个。\n\u0026gt; 计量‘牛牛’\n我们不用记在脑子里,算盘会记住。\n在接下来 4000 年,人类发明了各种巧妙的计算设备。比如星盘,让船只可以在海上计算纬度,或计算尺,帮助计算乘法和除法。人们还创造了上百种时钟,算日出,潮汐,天体的位置,或纯粹拿来计时。这些设备让原先很费力的事变得更快,更简单,更精确,降低了门槛,加强了我们的能力。\n记笔记!(敲黑板)这个系列会多次提到这一点。\n计算机先驱 charles babbage 说过:\u0026ldquo;随着知识的增长和新工具的诞生,人工劳力会越来越少\u0026rdquo;。\n然而,这些设备那时都不叫\u0026quot;计算机\u0026quot;。最早使用 \u0026ldquo;计算机\u0026rdquo; 一词的文献来自 1613 年的一本书,作者 richard braithwait。然而指的不是机器,而是一种职业。\n\u0026gt; hi, computers.\nbraithwait 说:\u0026ldquo;我听说过的计算者里最厉害的,能把好几天的工作量大大缩减。\u0026rdquo;\n那时,\u0026ldquo;computer\u0026rdquo; 指负责计算的人。\u0026ldquo;computer\u0026rdquo; 偶尔会用机器帮忙,但大部分时候靠自己。这个职位一直到 1800 年代还存在,之后 \u0026ldquo;computer\u0026rdquo; 逐渐开始代表机器。\n\u0026gt; 步进计算器\n其中\u0026quot;步进计算器\u0026quot;(step reckoner)最有名,由德国博学家戈特弗里德·莱布尼茨建造于 1694 年。\n= 对,就是和牛顿先后发明微积分的那个莱布尼茨……\n莱布尼茨说过 \u0026ldquo;\u0026hellip; 让优秀的人浪费时间算数简直侮辱尊严,农民用机器能算得一样准。\u0026rdquo;\n\u0026ldquo;步进计算器\u0026quot;有点像汽车里的里程表,不断累加里程数。它有一连串可以转动的齿轮(gears),每当一个齿轮转过 9,它会转回 0,同时让旁边的齿轮前进 1 个齿,就像算盘超过 10 一样。做减法时,机器会反向运作。利用一些巧妙的机械结构,步进计算器也能做乘法和除法。\n乘法和除法实际上只是多个加法和减法。举例,17 除以 5,我们只要减 5,减 5,再减 5,直到不能再减 5,就知道了 17=5x3+2 。步进计算器可以自动完成这种操作,它是第一台能做\u0026quot;加减乘除\u0026quot;全部四种运算的机器。它的设计非常成功,以至于沿用了 3 个世纪。\n不幸的是,即使有机械计算器,许多现实问题依然需要很多步,算一个结果可能要几小时甚至几天,而且这些手工制作的机器非常昂贵,大部分人买不起。所以在 20 世纪以前,大部分人会用预先算好的计算表,这些计算表由之前说的 \u0026ldquo;人力计算器\u0026rdquo; 编撰。如果你想知道 867,5309 的平方根,与其花一整天来手摇 \u0026ldquo;步进计算器\u0026rdquo;,你可以花一分钟在表里找答案。\n速度和准确性(speed and accuracy)在战场上尤为重要,因此军队很早就开始用计算解决复杂问题。如何精确瞄准炮弹是一个很难的问题。19 世纪,这些炮弹的射程可以达到 1 公里以上(比半英里多一点),因为风力,温度,大气压力会不断变化,想打中船一样大的物体也非常困难。于是出现了射程表(range tables),炮手可以查环境条件和射击距离,然后这张表会告诉他们,角度要设成多少。这些射程表很管用,二战中被广泛应用。问题是如果改了大炮或炮弹的设计,就要算一张新表,这样很耗时而且会出错。\ncharles babbage 在 1822 年写了一篇论文,向皇家天文学会指出了这个问题,标题叫: \u0026ldquo;机械在天文与计算表中的应用\u0026rdquo;(\u0026ldquo;note on the application of machinery to the computation of astronomical and mathematical tables\u0026rdquo;)。\n💭 让我们进入思想泡泡。\ncharles babbage 提出了一种新型机械装置叫 \u0026ldquo;差分机\u0026rdquo;(the difference engine),一个更复杂的机器,能近似多项式(polynomials)。多项式描述了几个变量之间的关系,比如射程和大气压力,或者 carrie anne 要吃多少披萨才开心。多项式也可以用于近似对数(logarithmic)和三角函数(trigonometric functions),这些函数手算相当麻烦。\n\u0026gt; ‘分析机’进行时...\ncharles babbage 在 1823 年开始建造差分机,并在接下来二十年,试图制造和组装 25,000 个零件,总重接近 15 吨。不幸的是,该项目最终放弃了,但在 1991 年,历史学家根据 charles babbage 的草稿做了一个差分机,而且它还管用!但更重要的是,在差分机的建造期间,charles babbage 构想了一个更复杂的机器 - 分析机。不像差分机,步进计算器 和以前的其他计算设备,分析机是 \u0026ldquo;通用计算机\u0026rdquo;(\u0026ldquo;general purpose computer\u0026rdquo;)。它可以做很多事情,不只是一种特定运算,甚至可以给它数据,然后按顺序执行一系列操作。它有内存,甚至一个很原始的打印机。就像差分机,这台机器太超前了,所以没有建成,然而,这种 \u0026ldquo;自动计算机\u0026rdquo;(\u0026ldquo;automatic computer\u0026rdquo;)的概念。\n计算机可以自动完成一系列操作,是个跨时代的概念,预示着计算机程序的诞生。\n英国数学家 ada lovelace 给分析机写了假想的程序,她说:\u0026ldquo;未来会诞生一门全新的,强大的,专为分析所用的语言。\u0026rdquo; 因此 ada 被认为是世上第一位程序员(programmer)。\n分析机激励了(可以这么讲)第一代计算机科学家,这些计算机科学家把很多 charles babbage 的点子融入到他们的机器。所以 charles babbage 经常被认为是 \u0026ldquo;计算之父\u0026rdquo;。\n谢啦!思想泡泡\n到了 19 世纪末,科学和工程领域中的特定任务会用上计算设备。但公司,政府,家庭中很少见到计算设备。然而,美国政府在 1890 年的人口普查中面临着严重的问题,只有计算机能提供所需的效率。美国宪法要求 10 年进行一次人口普查,目的是分配联邦资金,国会代表,等等。到 1880 年代,美国人口迅速增长,大部分因为移民,人口普查要七年时间来手工编制,等做完都过时了,而且 1890 年的人口普查,预计要 13 年完成,但人口普查可是 10 年一次啊!\n人口普查局找了 herman hollerith,他发明了打孔卡片制表机(tabulating machine),他的机器是 \u0026ldquo;电动机械的\u0026rdquo;,用传统机械来计数,结构类似莱布尼茨的乘法器,但用电动结构连接其他组件。hollerith 的机器用打孔卡(punch cards) - 一种纸卡,上面有网格,用打孔来表示数据。\n\u0026gt; herman hollerith 和 ‘打孔卡片制表机’\n举个例子,有一连串孔代表婚姻状况。如果你结婚了,就在 \u0026ldquo;结婚\u0026rdquo; 的位置打孔。当卡插入 hollerith 的机器时,小金属针会到卡片上。如果有个地方打孔了,针会穿过孔。泡入一小瓶汞,联通电路,电路会驱动电机,然后给 \u0026ldquo;已婚\u0026rdquo; 的齿轮 + 1 。\n= 一个简单程序的威力 - 控制联通!\n\u0026gt; 是不是最早的‘程序’卡?\nhollerith 的机器速度是手动的 10 倍左右,使人口普查在短短两年半内完成,给人口普查办公室省了上百万美元。企业开始意识到计算机的价值,可以提升劳动力以及数据密集型任务来提升利润。比如会计,保险评估和库存管理等行业。为了满足这一需求,hollerith 成立了制表机器公司,这家公司后来在 1924 年与其它机械制造商合并,成为了 \u0026ldquo;国际商业机器公司\u0026rdquo;,简称 ibm(the international business machines corporation)。\n= 利润!!!恐怕没有什么比它更吸引企业的了!\n你可能听过 ibm 😂\n这些电子机械的 \u0026ldquo;商业机器\u0026rdquo; 取得了巨大成功,改变了商业和政府。到了 1900 年代中叶,世界人口的爆炸和全球贸易的兴起。要求更快,更灵活的工具来处理数据,为电子计算机的发展奠定了基础。\n我们下周讨论。\n","date":"2022-08-14","permalink":"https://loveminimal.github.io/posts/cscc/01/","summary":"\u003cp\u003ei.e. Early Computing\u003c/p\u003e\n\u003cp\u003eHello world!我是 Carrie Anne,欢迎收看计算机科学速成课(Crash Course Computer Science)!\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/01/imgs/8a3e8687c14cce28b9f8368ee5e06354c6ce69625c4f8167244ec40f3079f3ea.png\" width=\"400\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; Carrie Anne 小姐姐\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e在这个系列中,我们会学习 Bits(位),Bytes(字节),晶体管(transistors),逻辑门(logic gates),一直到操作系统,虚拟现实和机器人!我们要学很多东西,但预先说明,我们 \u003cstrong\u003e不会\u003c/strong\u003e 教你怎么编程,我们会从高层次上纵览一系列计算机话题。\u003c/p\u003e","title":"cscc01 - 计算机早期历史"},{"content":" “圣人不治已病,治未病;不治已乱,治未乱。”\n-- 《黄帝内经·素问·四气调神大论》\n生老病死,成住坏空,古往皆如是。生命的前几十年,身体是在上升期,经得起小风小浪小消耗。然岁月不饶人,专治各种不服 …… 😅\n我们要爱护自己的身体,守护好自己的健康。养生之道,任重道远,却是千里之行,始于足下。生活中的每一点有益的改变,都会让我们身体受用。现代医学多是西方那一套,不是说不好,心中却总是有一种其“治标不治本”的“偏见”。西医的“头痛医头,脚痛医脚”,着实有其局限,它把人体的大系统割裂式的看待,感觉还是太局部了。之前也没有看过什么医学著作,主要是感觉看了也看不懂,不知为何心血来潮,突然就想了解一二(也就只能学一点皮毛常识)。就想着从我国传统医学著作着手,毕竟几千年的实践检验了,至少不会给自己带坑里去,而且感觉上中医侧重养生,比较符合目前的状态。再不济,全当强化一下古文学习了,当成一种休闲消遣也不错。\n百科一下,传统医学四大经典著作:《黄帝内经》、《难经》、《伤寒杂病论》、《神农本草经》,让我们择其一二先一窥究竟。\n一二经典 \u0026gt; 黄帝内经\n《黄帝内经》分《灵枢》、《素问》两部分,是一本综合性的医书,在黄老道家理论上建立了中医学上的“阴阳五行学说”、“脉象学说”、“藏象学说”、“经络学说”、“病因学说”、“病机学说”、“病症”、“诊法”、“论治”及“养生学”、“运气学”等学说。 其基本素材来源于中国古人对生命现象的长期观察、大量的临床实践以及简单的解剖学知识。《黄帝内经》奠定了人体生理、病理、诊断以及治疗的认识基础,是中国影响极大的一部医学著作,被称为医之始祖。\n\u0026gt; 难经\n《难经》是战国时期成书,作者是扁鹊 ,原名《黄帝八十一难经》,古代中医学著作之一,传说为战国时期秦越人(扁鹊)所作。本书以问答解释疑难的形式编撰而成,共讨论了81个问题,故又称《八十一难》,全书所述以基础理论为主,还分析了一些病证。其中一至二十二难为脉学,二十三至二十九难为经络,三十至四十七难为脏腑,四十八至六十一难为疾病,六十二至六十八为腧穴,六十九至八十一难为针法。\n\u0026gt; 四圣心源\n《四圣心源》是清·黄元御撰写于1753年的医书,又名《医圣心源》。作者将黄帝、岐伯、秦越人、张仲景视为医中四圣。本书阐发《内经》、《难经》、《伤寒论》、《金匮要略》诸书蕴义,卷一天人解;卷二六气解;卷三脉法解;卷四劳伤解;卷五至卷七杂病解;卷八七窍解;卷九疮疡解;卷十妇人解。是一部包括中医基本理论和部分临床医学的综合性著作。\n\u0026gt; 奇经八脉考\n奇经八脉是指十二经脉之外的八条经脉,包括任脉、督脉、冲脉、带脉、阴跷脉、阳跷脉、阴维脉、阳维脉。奇者,异也。因其异于十二正经,故称“奇经”。它们既不直属脏腑,又无表里配合。其生理功能,主要是对十二经脉的气血运行起着溢蓄、调节作用。(看小说的都爱这个,长长见识 🧐)\n其他如《金匮要略》、《温病条辨》、《伤寒杂病论》和《神农本草经》太过专项了,概览了一下,实在是看不懂,暂且不去管它,后续有一定基础后再啃。\n结语 无论做什么事情,思而后行,行而方有所得。看,你其实有很多事情值得去做,总可以变得更好。\n","date":"2022-08-08","permalink":"https://loveminimal.github.io/posts/learn-some-medicines/","summary":"\u003cblockquote\u003e\n\u003cp\u003e“圣人不治已病,治未病;不治已乱,治未乱。”\u003cbr\u003e\n\u003ccode\u003e-- 《黄帝内经·素问·四气调神大论》\u003c/code\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e生老病死,成住坏空,古往皆如是。生命的前几十年,身体是在上升期,经得起小风小浪小消耗。然岁月不饶人,专治各种不服 …… 😅\u003c/p\u003e","title":"你可能需要学一些医学知识"},{"content":"i.e. cs (computer science) crash course\n这个系列来自于 crash course 的一个视频系列课程(共有 40 课程,每个课程 10 分钟左右),多谢热心的 crash course 字幕组配上了优质的字幕 - b 站地址 📺 。\n知乎上已经有小伙伴做了同样的事 - 将视频课程文档化,详见 计算机速成课笔记 。我们在这里,重新制作一次文档,一是为了加深印象、深化理解;二是方便后续做内容扩展。\n好吧,竟然还有神奇的字幕组 - 计算机科学速成课字幕组 repo,本系列内容为以该仓库字幕文件为基础的图文版。\n感谢所有主创、二创人员的付出!🎉\n","date":"2022-08-03","permalink":"https://loveminimal.github.io/posts/cscc/00/","summary":"\u003cp\u003ei.e. cs \u003ccode\u003e(computer science)\u003c/code\u003e crash course\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/00/imgs/e95986763251c49010ca90d1af26b02870c7f0070bfa57452dae28bc028f8588.png\" width=\"\" /\u003e \r\n\u003cp\u003e这个系列来自于 \u003ca href=\"https://thecrashcourse.com/\"\u003eCRASH COURSE\u003c/a\u003e 的一个视频系列课程(共有 40 课程,每个课程 10 分钟左右),多谢热心的 CRASH COURSE 字幕组配上了优质的字幕 - \u003ca href=\"https://www.bilibili.com/video/BV1EW411u7th?p=1\u0026amp;vd_source=a6f6452712ce1cd91d115827d0148715\"\u003e B 站地址 📺\u003c/a\u003e 。\u003c/p\u003e","title":"cscc00 - 计算机科学速成课(引言)"},{"content":" 转载自 https://www.jb51.net/hardware/cpu/610074.html\n我们知道,cpu 性能是主要由 cpu 构架、核心线程数量、主频、缓存等诸多因素共同决定,而“缓存”是很多网友容易忽视的一个地方。那么,cpu 缓存是什么?在电脑 cpu 中,一二三级缓存究竟谁对 cpu 性能影响最重要呢?\ncpu 缓存是什么 cpu 缓存(cache memory)是位于 cpu 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。cpu 高速缓存的出现主要是为了解决 cpu 运算速度与内存读写速度不匹配的矛盾,因为 cpu 运算速度要比内存读写速度快很多,这样会使 cpu 花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内 cpu 即将访问的,当 cpu 调用大量数据时,就可先缓存中调用,从而加快读取速度。\n\u0026gt; cpu 缓存\n缓存大小是 cpu 的重要指标之一,而且缓存的结构和大小对 cpu 速度的影响非常大,cpu 内缓存的运行频率极高,一般是和处理器同频运作,工作效率远远大于系统内存和硬盘。实际工作时,cpu 往往需要重复读取同样的数据块,而缓存容量的增大,可以大幅度提升 cpu 内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能。但是从 cpu 芯片面积和成本的因素来考虑,缓存都很小。\n\u0026gt; cpu 缓存设计示意图\n按照数据读取顺序和与 cpu 结合的紧密程度,cpu 缓存可以分为一级缓存,二级缓存,如今主流 cpu 还有三级缓存,甚至有些 cpu 还有四级缓存。每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。\n为什么 cpu 会有 l1、l2、l3 这样的缓存设计?主要是因为现在的处理器太快了,而从内存中读取数据实在太慢(一个是因为内存本身速度不够,另一个是因为它离 cpu 太远了,总的来说需要让 cpu 等待几十甚至几百个时钟周期),这个时候为了保证 cpu 的速度,就需要延迟更小速度更快的内存提供帮助,而这就是缓存,如下图所示。\n当 cpu 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在 80%左右,也就是说全部数据量的 80%都可以在一级缓存中找到,只剩下 20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个 cpu 缓存架构中最为重要的部分。\ncpu 的一二三级缓存哪个最重要 一般来说,每级缓存的命中率大概都在 80%左右,也就是说全部数据量的 80%都可以在一级缓存中找到,只剩下 20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个 cpu 缓存架构中最为重要的部分。\n但是,现在 cpu 的一级缓存几乎都一样,容量都比较小,多为 64k,因此如今的 cpu 基本很少提一级缓存,主要是大家都一样,虽然最重要,但却不值得一提。\n二级缓存,对 cpu 是很重要的,不过很多朋友会发现,如今很多 intel 的 cpu 也都不怎么提二级缓存,只标注三级缓存。而 amd 的不少新 cpu 也多为标注三级缓存为主,二级缓存只有部分型号会标注,比如 amd 锐龙 5 2600x 提供 3m 三级缓存和 16m 三级缓存,r7 2700x 则也只有 16m 三级缓存。而 intel 酷睿 i3 8100 则只有 6m 三级缓存,高端的 i7 8700k 则只标注 12mb。\n因此,在目前的新款 cpu 中,二级缓存的重要性在减弱,三级缓存则成为重点。\n现代 cpu 的高速缓存体系结构是非常复杂的,其中包括硬件预取和数据转发,以便能提供最佳的高速缓存命中机会,有些 cpu 甚至还加入了 l4 缓存。\n以上就是电脑 cpu 一二三级缓存的知识科普。对于普通电脑用户来说,只要知道 cpu 缓存是决定 cpu 性能的因素之一,普通用户对 cpu 缓存并不太敏感,游戏玩家更为在意的 cpu 核心数、主频等因素,而对于一些 3d 制图、视频渲染用户来说,比较考验 cpu 综合性能,这个时候大缓存会显得更优势。\n","date":"2022-08-02","permalink":"https://loveminimal.github.io/posts/cpu-cache/","summary":"\u003cblockquote\u003e\n\u003cp\u003e转载自 \u003ca href=\"https://www.jb51.net/hardware/cpu/610074.html\"\u003ehttps://www.jb51.net/hardware/cpu/610074.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我们知道,CPU 性能是主要由 CPU 构架、核心线程数量、主频、缓存等诸多因素共同决定,而“缓存”是很多网友容易忽视的一个地方。那么,CPU 缓存是什么?在电脑 CPU 中,一二三级缓存究竟谁对 CPU 性能影响最重要呢?\u003c/p\u003e","title":"cpu 缓存是什么"},{"content":" 转载自 https://www.jb51.net/hardware/cpu/610350.html\n2018 年 4 月 3 日,intel 在北京举办了第八代酷睿移动处理器全球发布会,正式发布了全新的第八代酷睿移动处理器。很多小伙伴其实对 intel 的芯片很感兴趣,但是一但深入了解,很快就会被扑面而来的晶体管,微架构,纳米,cpi,睿频,超频,缓存等概念搞疯………\n为何 cpu 工作如此迅速,为何主频变化多端,纳米级别的工艺到底是外星文明的帮助还是人类智慧的结晶,一片薄薄硅圆晶上到底隐藏着怎样的秘密,就让我们大家一起走一走“芯”吧……\n先说说,芯片到底是个什么东西。\n芯片,就是我们说的 cpu,又名中央处理器,常说芯片是一个机器的大脑,重要性可见一斑,它负责计算机多项工作的完成,快速处理数据,并把工作分类并传输指令给计算机的其它部件,这有点像一个物流中心,要把源源不断又种类繁多的数据“包裹”分析,分类,处理,传送……\n所以,要不我们就把它比作一个物流中心好了,为方便理解,我们就从甄老板要开一个物流厂说起吧……\n从沙子到芯片 甄老板要开一个物流厂,首先第一件事,当然是要选一块地,芯片物流厂的用地要求比较奇怪,它是需要建在沙子上的,因为工厂要顺利运行,需要一种沙子里面含有的特殊元素,si(硅),这也就是我们熟知的 cpu 是由沙子造的。\n但是我们知道沙子上是不能直接建厂的,所以沙子需要特殊处理,这个处理就是通过一系列的提纯及切割,获得芯片基座-圆晶的过程。\n圆晶(wafer)是芯片的基础,是由硅纯度 99.9999% 的硅锭切割成的薄片。在这个基础上,芯片制造商们将展开体现人类制造智慧的顶级工艺。当然,为了好理解,我们还是回到工厂的建造上来吧。\n纳米技术及微架构 好了,现在工厂有了地,但是怎么建造流水线设施,让工人好好干活呢?\n当然得找个牛一点的设计师先设计一下厂房的构造, 越牛的设计师,设计得越精巧,就是我们说的“工艺”,随着时代的进步,设计师们的精细度也从 45nm 级的设计,逐步精细化到 32,22,甚至现在的第八代 14nm,工艺越好,能容纳的工作单位(晶体管)就越多,一个针尖就有高达 5k 晶体管,当然也就意味着可以在工厂干活的人也越来越多,干活的速度也就能越来快啦……\n设计师们还得去设计厂房的具体布局,比如这条流水线应该从左往右,还是从上到下呢?这种布局就叫微架构,当然布局过程非常复杂,介于我们都不是学建筑的,具体的工序也没必要特别了解,通俗点讲可以看作是一个在晶圆上做微雕,雕出众多晶体管的过程……这个东东的技术性和复杂性都已经登峰造极了,下方的部分工艺大家随意感受下就行……\n架构决定了芯片的性能和功能,设计师们为了工人们能更快,更好的干活,并且能处理更多新出现的任务,在架构上简直绞尽脑汁,尽善尽美,所以在工艺进步的同时,也经常在架构上推陈出新,比如第八代 intel 就是“coffee lake-h”设计代替了前一代的“kabylake-h”的设计。\n不过微微吐槽一句,intel 的架构命名还真是好喜欢用地名啊,不是这个湖就是那个桥,可能是想表明他们就是个跟土建沙石有关的公司吧(……大误)。\n晶体管做完之后,后续还有一系列的测试,切割,封装等工序,圆晶就变成了一块块芯片,我们的工厂也就建好啦。\n虽然讲的很简单,但其实背后有很多的尖端工艺,intel 作为金字塔的顶端,也只有 70% 的成功率,所以芯片行业真是汇集了精英资本和精英智慧的一个产业啊,不是高帅富的大佬还真的玩不起,当然这也是为什么,世界上的芯片制造商屈指可数的原因。\n工厂总算建好啦,工人们各就各位,开始干活吧……\n时钟频率与核心 人和厂都准备好了,但是人实在太多了,如果不好好管理的话,也会乱套,所以甄老板找了个大喇叭统一喊“1,2,1,2”,让工人们有节奏地干活,这个“1,2,1,2”就是时钟频率。\n通常意义上我们把它认为是 cpu 的主频,主频越高,说明工人们干活越快,cpu 的速度也就越快,一般我们能看到就是 xxghz 这样的显示,数值越高,说明速度越快。\n当然甄老板为了效率能更高点,可能会同时建 2 个或者 4 个厂房,就我们熟知的双核 cpu,4 核 cpu,这样通过协同,工人们的效率就更高,单位时间能处理的活也就越多。\n而这样根据不同的等级,cpu 也就有了 i3,i5,i7 之分(现在还出现了 i9 级 cpu),等级越高,主频表现越好,工厂越有效率,而在第八代的 intel 芯片中,又有了划时代的产品:首次出现了 6 核的移动平台 cpu。\n这颗芯将主流移动计算提身高了 6 核的时代(需要说明的是服务器及商用领域其实早就已经出现 6 核,但是移动平台是首次),当然,我们在这一张图中除了看到熟悉的 14 纳米制程工艺,核心数之外,还看到了另外的参数,一是功耗,一是线程,功耗我们放后面说,先说说线程。\n线程、睿频、超频 还是举工厂的例子,甄老板在厂房布局(微架构)及工人数量(晶体管)的布置上已经到了极限,还想提高效率怎么办呢?就要说到我们下面的概念了线程和超线程。\n线程其实是个虚拟概念,类似于工厂外面排好等待处理的包裹队列,一般每个核心对应着一个线程,包裹按顺序进入工厂,这样 cpu 就可以有条不稳地工作了\n而超线程相当于一个核心对应了两条队列,两条队列上的包裹按工厂的处理能力依次进入厂房,道路宽了,也让整个进程更有效率。\n然后工厂这边,任务突然多了起来,处理速度也要相应的提高,甄老板想到了一个办法,号召想多干活的工人们去拿加班奖励,一部分工人当然积极响应,多出来的任务很快完成了,这个自主接收激励的动作,就叫睿频。\n睿频:是指当启动一个运行程序后,cpu 会自动加速到合适的频率,而原来的运行速度会提升 10%~20% 以保证程序流畅运行的一种技术。睿频是 intel 的一个重要技术,也是智能 cpu 的基础,这项技术被运用到了 intel 的全系列,包括第八代 cpu。\n下图就是最新的睿频技术,可以提升至 4.8ghz,完成峰值任务 so easy!\n当然,有时候工厂的活特别多,自愿去加班的工人都完成不了了。这时候甄老板不得不放出了“全体人必须加班“的大招,将工厂的速度强制提升到某一个值上,这个动作叫做超频。\n超频:用户强制将处理器的所有内核运行在规格限定频率范围之外,以求更好的速度。这个动作通过给 cpu 加压实现的,虽然可以提升效率,但有时候也会损伤元件,各位少侠,请慎重使用哦。\n缓存与傲腾 经过一系列的处理,工厂的生意越来越好,但是甄老板渐渐发现,这个厂,有!问!题!\n因为他发现,每次包裹传送带都要从很远的“内存仓库“甚至更远的“硬盘仓库”传送,路途遥远,耗费时常不说,还经常丢失包裹。\n痛定思痛,甄老板决定在厂房的周围自建仓库以临时存储待处理的包裹,离厂房最近且最快的叫一级仓库,但因为地价比较贵,所以一级仓库的面积最小,以此类推,又建了二级仓库,三级仓库,三个仓库配合无间,让包裹能不间断地运送到工厂。\n这就是 cpu 一,二,三级缓存的概念(下图中其实只能看到三级,因为一二级太小了)。\n后来 intel 认为,3 级缓存已经跟不上现在的时代,所以在最近的第八代 cpu 中,直接在芯片上建了了内存仓库大小的超级缓存,就是“傲腾”。\n这个可选配的“傲腾”内存,这大大提高了用户的峰值速度,也让电脑在大任务面前,终于可以轻松起飞。\n能耗 刚刚借着 intel 第八代 cpu 普及了很多芯片上的参数,最后说说能耗一般效率越高的工厂,当然能耗就越多,放在 cpu 上,意味着更废电,以及发热更大,所以保证运行的稳定,cpu 主频往往到一定阶段就不再上升了,能耗性能如何平衡?也是考验 cpu 厂商的一个关键。\n不过 intel 在这方面处理得还是相当不错的,可以说是极大的兼顾了速度与稳定在第八代的 intel i9 cpu 中还首次实现了移动智能不锁频的技术,可以说是很艺高人胆大了。\n结语 熟悉 intel 的人对这个发布会还是有所意料,因为毕竟我们都已经知道,intel 遵循 tick-tock 开发模式,两年更新一个工艺,隔年更新微架构,不过这两年 intel 在工艺上已经遥遥领先于对手,所以新 cpu 在工艺上的更新并没有太大,但转而在架构上进行调整,以适应现阶段如 4k、vr、娱乐、游戏、无线技术等更应用化也更新的多任务的处理。\n不管怎么说,一代确实比一代好了,而随着时代的发展,intel 也逐渐由原来单纯的芯片厂商,逐渐开始在自家的处理器上集成显卡、内存(四级缓存),不满足做个快递大厂,开始做店面,还包了飞机火车轮船,直接开始打开了地图打野模式……\n当然乐见的是,技术的革新为新硬件的诞生也提供了坚实的基础。\n未来已来,我们拭目以待!\n","date":"2022-08-02","permalink":"https://loveminimal.github.io/posts/expensive-cpu/","summary":"\u003cblockquote\u003e\n\u003cp\u003e转载自 \u003ca href=\"https://www.jb51.net/hardware/cpu/610350.html\"\u003ehttps://www.jb51.net/hardware/cpu/610350.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2018 年 4 月 3 日,Intel 在北京举办了第八代酷睿移动处理器全球发布会,正式发布了全新的第八代酷睿移动处理器。很多小伙伴其实对 intel 的芯片很感兴趣,但是一但深入了解,很快就会被扑面而来的晶体管,微架构,纳米,CPI,睿频,超频,缓存等概念搞疯………\u003c/p\u003e","title":"用沙子造的 cpu 凭什么卖的那么贵"},{"content":"好吧,试了,但真的没找到 “佚名” 是谁 😂 ,希望原作者能感受到我的原力感谢吧…… 不少网站都有转载该篇文章,以下是图片较清晰的几个页面链接。\nhttps://www.sohu.com/a/255397866_468626 https://www.jb51.net/hardware/cpu/611229.html https://www.jb51.net/hardware/cpu/610350.html 1 寸 = 33.3333 mm 集成电路 ic (integrated circuit )\n引言 cpu 是现代计算机的核心部件,又称为“微处理器”。对于 pc 而言,cpu 的规格与频率常常被用来作为衡量一台电脑性能强弱重要指标。\n许多对电脑知识略知一二的朋友大多会知道 cpu 里面最重要的东西就是晶体管了,提高 cpu 的速度,最重要的一点说白了就是如何在相同的 cpu 面积里面放进去更加多的晶体管,由于 cpu 实在太小,太精密,里面组成了数目相当多的晶体管,所以人手是绝对不可能完成的,只能够通过光刻工艺来进行加工的。\n这就是为什么一块 cpu 里面为什么可以数量如此之多的晶体管。晶体管其实就是一个双位的开关:即开和关。如果您回忆起基本计算的时代,那就是一台计算机需要进行工作的全部。两种选择,开和关,对于机器来说即 0 和 1。\n那么如何制作一个 cpu 呢?\n大家可能只知道制作 ic 芯片的硅来源于沙子,但是为什么沙子做的 cpu 却卖那么贵?下面本文以 intel 电脑 cpu 作为例子,讲述沙子到 cpu 简要的生产工序流程,希望大家对 cpu 制作的过程有一个大体认识,其它 cpu 或者手机 cpu 制造原理也大抵相同。\n一、硅圆片的制作 1. 硅的重要来源:沙子 作为半导体材料,使用得最多的就是硅元素,其在地球表面的元素中储量仅次于氧,含硅量在 27.72%,其主要表现形式就是沙子(主要成分为二氧化硅),沙子里面就含有相当量的硅。因此硅作为 ic 制作的原材料最合适不过,想想看地球上有几个浩瀚无垠的沙漠,来源既便宜又方便。\n2. 硅熔炼、提纯 不过实际在 ic 产业中使用的硅纯度要求必须高达 99.999999999%。目前主要通过将二氧化硅与焦煤在 1600-1800℃中,将二氧化硅还原成纯度为 98% 的冶金级单质硅,紧接着使用氯化氢提纯出 99.99% 的多晶硅。虽然此时的硅纯度已经很高,但是其内部混乱的晶体结构并不适合半导体的制作,还需要经过进一步提纯、形成固定一致形态的单晶硅。\n3. 制备单晶硅锭 单晶的意思是指原子在三维空间中呈现规则有序的排列结构,而单晶硅拥有“金刚石结构”,每个晶胞含有 8 个原子,其晶体结构十分稳定。\n通常单晶硅锭都是采用直拉法制备,在仍是液体状态的硅中加入一个籽晶,提供晶体生长的中心,通过适当的温度控制,就开始慢慢将晶体向上提升并且逐渐增大拉速,上升同时以一定速度绕提升轴旋转,以便将硅锭控制在所需直径内。结束时,只要提升单晶硅炉温度,硅锭就会自动形成一个锥形尾部,制备就完成了,一次性产出的 ic 芯片更多。\n\u0026gt; 籽晶是具有和所需晶体相同晶向的小晶体,是生长单晶的种子,也叫晶种。\n制备好的单晶硅锭直径约在 300mm 左右,重约 100kg。而目前全球范围内都在生产直径 12 寸的硅圆片,硅圆片尺寸越大,效益越高。\n4. 硅锭切片 将制备好的单晶硅锭一头一尾切削掉,并且对其直径修整至目标直径,同时使用金刚石锯把硅锭切割成一片片厚薄均匀的晶圆(1mm)。有时候为了定出硅圆片的晶体学取向,并适应 ic 制作过程中的装卸需要,会在硅锭边缘切割出“取向平面”或“缺口”标记。\n5. 研磨硅圆片 切割后的晶圆其表面依然是不光滑的,需要经过仔细的研磨,减少切割时造成的表面凹凸不平,期间会用到特殊的化学液体清洗晶圆表面,最后进行抛光研磨处理,还可以在进行热处理,在硅圆片表面成为“无缺陷层”。一块块亮晶晶的硅圆片就这样被制作出来,装入特制固定盒中密封包装。\n\u0026gt; 制作完成的硅圆片\n通常半导体 ic 厂商是不会自行生产这种晶圆,通常都是直接从硅圆片厂中直接采购回来进行后续生产。\n二、前工程 \u0026ndash; 制作带有电路的芯片 6. 涂抹光刻胶 买回来的硅圆片经过检查无破损后即可投入生产线上,前期可能还有各种成膜工艺,然后就进入到涂抹光刻胶环节。微影光刻工艺是一种图形影印技术,也是集成电路制造工艺中一项关键工艺。首先将光刻胶(感光性树脂)滴在硅晶圆片上,通过高速旋转均匀涂抹成光刻胶薄膜,并施加以适当的温度固化光刻胶薄膜。\n\u0026gt; 光刻胶是一种对光线、温度、湿度十分敏感的材料,可以在光照后发生化学性质的改变,这是整个工艺的基础。\n7. 紫外线曝光 就单项技术工艺来说,光刻工艺环节是最为复杂的,成本最为高昂的。因为光刻模板、透镜、光源共同决定了“印”在光刻胶上晶体管的尺寸大小。\n将涂好光刻胶的晶圆放入步进重复曝光机的曝光装置中进行掩模图形的“复制”。掩模中有预先设计好的电路图案,紫外线透过掩模经过特制透镜折射后,在光刻胶层上形成掩模中的电路图案。一般来说在晶圆上得到的电路图案是掩模上的图案 1/10、1/5、1/4,因此步进重复曝光机也称为“缩小投影曝光装置”。\n一般来说,决定步进重复曝光机性能有两大要素:一个是光的波长,另一个是透镜的数值孔径。如果想要缩小晶圆上的晶体管尺寸,就需要寻找能合理使用的波长更短的光(euv,极紫外线)和数值孔径更大的透镜(受透镜材质影响,有极限值)。\n\u0026gt; asml 公司 twinscan nxe:3300b\n8. 溶解部分光刻胶 对曝光后的晶圆进行显影处理。以正光刻胶为例,喷射强碱性显影液后,经紫外光照射的光刻胶会发生化学反应,在碱溶液作用下发生化学反应,溶解于显影液中,而未被照射到的光刻胶图形则会完整保留。显影完毕后,要对晶圆表面的进行冲洗,送入烘箱进行热处理,蒸发水分以及固化光刻胶。\n\u0026gt; 将光刻胶分为两类,一类是负性光刻胶,一类是正性光刻胶。在显影后,正性光刻胶未曝光的部分将被保留下来,而负性光刻胶则在显影后保留下来。也就是说,在曝光结束后,用显影液显影的是正性光刻胶的感光区、负性光刻胶的非感光区,将溶解在显影液中,此步骤完成后,光刻胶涂层上的图象便可显示出来。为提高分辨率,几乎每种光刻胶都配备了专用显影液,以保证显影效果。\n\u0026gt; 负性光刻胶一般采用负性显影液。在无界面活性剂的情况下,正性光刻胶采用正性显影液显影。正性和负性显影液都是碱性溶剂,其主要区别是负性显影液中含有表面活性剂,而正性显影液中没有这种成分。\n9. 蚀刻 将晶圆浸入内含蚀刻药剂的特制刻蚀槽内,可以溶解掉暴露出来的晶圆部分,而剩下的光刻胶保护着不需要蚀刻的部分。期间施加超声振动,加速去除晶圆表面附着的杂质,防止刻蚀产物在晶圆表面停留造成刻蚀不均匀。\n10. 清除光刻胶 通过氧等离子体对光刻胶进行灰化处理,去除所有光刻胶。此时就可以完成第一层设计好的电路图案。\n11. 重复第 6-8 步 由于现在的晶体管已经 3d finfet finfet 的原理与工艺 设计,不可能一次性就能制作出所需的图形,需要重复第 6-8 步进行处理,中间还会有各种成膜工艺(绝缘膜、金属膜)参与到其中,以获得最终的 3d 晶体管。\n12. 离子注入 在特定的区域,有意识地导入特定杂质的过程称为“杂质扩散”。通过杂质扩散可以控制导电类型(p 结、n 结)pn 结原理简述之外,还可以用来控制杂质浓度以及分布。\n现在一般采用离子注入法进行杂质扩散,在离子注入机中,将需要掺杂的导电性杂质导入电弧室,通过放电使其离子化,经过电场加速后,将数十到数千 kev 能量的离子束由晶圆表面注入。离子注入完毕后的晶圆还需要经过热处理,一方面利用热扩散原理进一步将杂质“压入”硅中,另一方面恢复晶格完整性,活化杂质电气特性。\n离子注入法具有加工温度低,可均匀、大面积注入杂质,易于控制等优点,因此成为超大规模集成电路中不可缺少的工艺。\n10. 再次清除光刻胶 完成离子注入后,可以清除掉选择性掺杂残留下来的光刻胶掩模。此时,单晶硅内部一小部分硅原子已经被替换成“杂质”元素,从而产生可自由电子或空穴。\n\u0026gt; 左:硅原子结构; 中:掺杂砷,多出自由电子; 右:掺杂硼,形成电子空穴\n真的是复杂……\r11. 绝缘层处理 此时晶体管雏形已经基本完成,利用气相沉积法,在硅晶圆表面全面地沉积一层氧化硅膜,形成绝缘层。同样利用光刻掩模技术在层间绝缘膜上开孔,以便引出导体电极。\n12. 沉淀铜层 利用溅射沉积法,在晶圆整个表面上沉积布线用的铜层,继续使用光刻掩模技术对铜层进行雕刻,形成场效应管的源极、漏极、栅极。最后在整个晶圆表面沉积一层绝缘层以保护晶体管。\nokay ,现在我们有晶体管了……\r13. 构建晶体管之间连接电路 经过漫长的工艺,数以十亿计的晶体管已经制作完成。剩下的就是如何将这些晶体管连接起来的问题了。同样是先形成一层铜层,然后光刻掩模、蚀刻开孔等精细操作,再沉积下一层铜层。\u0026hellip;..,这样的工序反复进行多次,这要视乎芯片的晶体管规模、复制程度而定。最终形成极其复杂的多层连接电路网络。\n由于现在 ic 包含各种精细化的元件以及庞大的互联电路,结构非常复杂,实际电路层数已经高达 30 层,表面各种凹凸不平越来越多,高低差异很大,因此开发出 cmp 化学机械抛光技术。每完成一层电路就进行 cmp 磨平。\n另外为了顺利完成多层 cu 立体化布线,开发出大马士革法新的布线方式,镀上阻挡金属层后,整体溅镀 cu 膜,再利用 cmp 将布线之外的 cu 和阻挡金属层去除干净,形成所需布线。\n\u0026gt; 大马士革法多层布线\n芯片电路到此已经基本完成,其中经历几百道不同工艺加工,而且全部都是基于精细化操作,任何一个地方出错都会导致整片晶圆报废,在 100 多平方毫米的晶圆上制造出数十亿个晶体管,是人类有文明以来的所有智慧的结晶。\n三、后工程 \u0026ndash; 从划片到成品销售 14. 晶圆级测试 前工程与后工程之间,夹着一个 good-chip/wafer 检测工程,简称 g/w 检测。目的在于检测每一块晶圆上制造的一个个芯片是否合格。通常会使用探针与 ic 的电极焊盘接触进行检测,传输预先编订的输入信号,检测 ic 输出端的信号是否正常,以此确认芯片是否合格。\n由于目前 ic 制造广泛采用冗余度设计,即便是“不合格”芯片,也可以采用冗余单元置换成合格品,只需要使用激光切断预先设计好的熔断器即可。当然,芯片有着无法挽回的严重问题,将会被标记上丢弃标签。\n15. 晶圆切片、外观检查 ic 内核在晶圆上制作完成并通过检测后后,就进入了划片阶段。划片使用的划刀是粘附有金刚石颗粒的极薄的圆片刀,其厚度仅为人类头发的 1/3。将晶圆上的每一个 ic 芯片切划下来,形成一个内核 die。\n裂片完成后还会对芯片进行外观检查,一旦有破损和伤痕就会抛弃,前期 g/w 检查时发现的瑕疵品也将一并去除。\n\u0026gt; 未裂片的一个个 cpu 内核\n16. 装片 芯片进行检测完成后只能算是一个半成品,因为不能被消费者直接使用。还需要经过装片作业,将内核装配固定到基片电路上。装片作业全程由于计算机控制的自动固晶机进行精细化操作。\n17. 封装 装片作业仅仅是完成了芯片的固定,还未实现电气的连接,因此还需要与封装基板上的触点结合。现在通常使用倒装片形式,即有触点的正面朝下,并预先用焊料形成凸点,使得凸点与相应的焊盘对准,通过热回流焊或超声压焊进行连接。\n封装也可以说是指安装半导体集成电路芯片用的外壳,它不仅起着安放、固定、密封、保护芯片,还可以增强导热性能的作用。目前像 intel 近些年都采用 lga 封装,在核心与封装基板上的触点连接后,在核心涂抹散热硅脂或者填充钎焊材料,最后封装上金属外壳,增大核心散热面积,保护芯片免受散热器直接挤压。\n至此,一颗完整的 cpu 处理器就诞生了。\n18. 等级测试 cpu 制造完成后,还会进行一次全面的测试。测试出每一颗芯片的稳定频率、功耗、发热,如果发现芯片内部有硬件性缺陷,将会做硬件屏蔽措施,因此划分出不同等级类型 cpu,例如 core i7、i5、i3。\n19. 装箱零售 cpu 完成最终的等级划测试后,就会分箱进行包装,进入 oem、零售等渠道。\n结语 现在进入了科技时代,极度依赖计算机科学与技术,其中的 cpu 又是各种计算机必不可少重要部件。暂且不论架构上的设计,仅仅在 cpu 的制作上就凝聚了全人类的智慧,基本上当今世界上最先进的工艺、生产技术、尖端机械全部都投入到了该产业中。因此半导体产业集知识密集型、资本密集型于一身的高端工业。\n\u0026gt; cpu 制造全过程图解\n一条完整而最先进 cpu 生产线投资起码要数十亿人民币,而且其中占大头的是前工程里面的光刻机、掩膜板、成膜机器、扩散设备,占到总投资的 70%,这些都是世界上最精密的仪器,每一台都价值不菲。作为参照,cpu 工厂建设、辅助设备、超净间建设费用才占到 20%。\n不知道大家看到这里,觉得最低几百块就可以买到一颗汇聚人类智慧结晶的 cpu,还值不值呢?\n","date":"2022-08-01","permalink":"https://loveminimal.github.io/posts/how-to-make-a-cpu/","summary":"\u003cp\u003e好吧,试了,但真的没找到 “佚名” 是谁 😂 ,希望原作者能感受到我的原力感谢吧…… 不少网站都有转载该篇文章,以下是图片较清晰的几个页面链接。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.sohu.com/a/255397866_468626\"\u003ehttps://www.sohu.com/a/255397866_468626\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.jb51.net/hardware/cpu/611229.html\"\u003ehttps://www.jb51.net/hardware/cpu/611229.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.jb51.net/hardware/cpu/610350.html\"\u003ehttps://www.jb51.net/hardware/cpu/610350.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003e1 寸 = 33.3333 mm\u003c/code\u003e\n\u003ccode\u003e集成电路 ic (integrated circuit )\u003c/code\u003e\u003c/p\u003e","title":"cpu 是如何制造出来的"},{"content":"约两个月前,午休后的一个小意外不小心拉伤了右手姆指,当时也没有太注意,想着过几天就好了。俗话说,“伤筋动骨一百天嘛”。一周又一周,情况并没有得到好转。正好周末安排了体检,就顺带就诊一下。\n为什么拖了那么久?一是不在意,二是天太热、人太懒。\n不得不说“二档的医保不如狗”,限制太多 😿。医院的医生呢?现在离开各种检查就跟个“白痴”一样,当然,医生也要吃饭,但水平确实有待提一提。经历过一系列 blabla\u0026hellip; 的检查,医生瞄了半眼片子,真的就半眼(无力吐槽\u0026hellip;),来了句不用吃药,做三周(3 次)的冲击波治疗\u0026hellip; 好的,不是情绪上的对立,不过现在的医患关系总给我一种下图的感觉,反正 1000 大洋就这样交待了。\n做护理治疗的小姐姐还可以,挺温柔,说可能有些疼。过程中痛感也还好,感觉可能是轻微拉伤的原因。好吧,5 分钟 200 大洋 💰 。目前效果还不显著,估计是在恢复期,希望可以完美恢复。\n总之呢,身体已经在敲警钟了。\n想想,不知道有多少人长期处于亚健康的状态,一天又一天,像是一辆“衣衫褴褛”的自行车(除了铃铛不响)歪歪扭扭地向前行驶。当身体给我们发信号的时候,千万不要忽视它,意味着你的健康已欠费了,请充值。“人生不过数十载,无病无灾过一生”,真好!\n言归正传,之前没检查还好,一检查,感觉整个手都不太好了,搞得现在基本上是“一指禅”输入。作为一个程序猿,这真的是被缴了枪了。听我说,一定要爱护好你的手,脱离输入效率神教,“远离 emacs” 😂,好吧,少用点。业余时间,不要长时间玩手机或敲击键盘,可以做一些手工 diy 、弹吉他、吹笛子、下象棋、读纸质书等,总之就是不要让你的手过渡地重复性劳损。\n治疗后续\n第二次是真的有感觉痛了,医生小姐姐说是因为内部组织重建、血液流通了,痛觉更强烈…… 好吧,真的痛,后悔没有接受小姐姐的建议,在那里冷敷一下。好吧,下次一定…… 不过效果是真的感觉到了这次,估计再有一次就可以完全恢复了,这倒是一件让人欣慰的事情了。\n如何养护 如何养护你的手,先停止输入请敲击键盘 🤣 …… 好吧,一指禅太弱了,先这样,后续再补充。\n","date":"2022-08-01","permalink":"https://loveminimal.github.io/posts/protect-your-hands/","summary":"\u003cp\u003e约两个月前,午休后的一个小意外不小心拉伤了右手姆指,当时也没有太注意,想着过几天就好了。俗话说,“伤筋动骨一百天嘛”。一周又一周,情况并没有得到好转。正好周末安排了体检,就顺带就诊一下。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么拖了那么久?一是不在意,二是天太热、人太懒。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"保护好你的手"},{"content":"🔔 摘录自 冴羽写博客之深入系列 ,写的很不错。\n深入系列 从原型到到原型链 先来一张“震山总图”,后续我们会慢慢讲解它是什么得到的。\nprototype 每个函数都有一个 prototype 属性。那么这个函数到底指向的是什么?是这个函数的原型吗?\n先来看个简单的示例:\nfunction person() { } // 虽然写在注释里,但是你要注意: // prototype 是函数才会有的属性 person.prototype.name = \u0026#39;kevin\u0026#39;; var person1 = new person(); var person2 = new person(); console.log(person1.name)\t// kevin console.log(person2.name)\t// kevin 其实,函数的 prototype 属性指向了一个对象,这个对象正是调用构造函数面创建的实例的原型,也就是上述示例中 person1 和 person2 的原型。\n说了这么多, 什么是原型呢? 你可以这样理解:每一个 javascript 对象( null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性。\n“继承” ❓ 继承意味着复制操作,然而 javascript 默认并不会复制对象的属性,相反,它只是在两个对象之间创建一个关联,如此一个对象就可能通过委托访问另一个对象的属性和函数,所以,与其叫继承,委托的说法反而更准确些。\n上图表示构造函数和实例原型(此处我们用 object.prototype 表示实例原型)之间的关系。那么,我们该怎么表示实例与实例原型,即 person 和 person.prototype 之间的关系呢?__proto__ ❗\n__proto__ 每一个 javascript 对象(除了 null)都具有一个属性 \u0026ndash; __proto__ ,这个属性会指向该对象的原型。\n关于 __proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 person.prototype 中,实际上,它是来自于 object.prototype ,与其说是一个属性,不如说是一个 getter/setter ,当使用 obj.__proto__ 可以理解成返回了 object.getprototypeof(obj) 。\n为了证明这一点,我们可以在 firefox 或 chrome 浏览器中输入:\nfunction person() { } var person = new person(); console.log(person.__proto__ === person.prototype);\t// true chrome 的开发工具,真是一个神器 🎉\rok,现在让我们更新一下关系图,如下:\n既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数呢?\n原型:是啊,凭什么只能你指我,不服 👺\rconstructor 指向实例的倒是没有,因为一个构造函数可以生成多个实例。\n物以稀为贵,多了就是“原罪”……\r但是原型指构造函数倒是有的 \u0026ndash; constructor ,每个原型都有一个 constructor 属性指向关联的构造函数。如图:\n综上我们已经得出:\nfunction person() { } var person = new person(); console.log(person.__proto__ == person.prototype) // true console.log(person.prototype.constructor == person) // true // 顺便学习一个 es5 的方法,可以获得对象的原型 console.log(object.getprototypeof(person) === person.prototype) // true 实例与原型 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。\nfunction person() { } person.prototype.name = \u0026#39;kevin\u0026#39;; var person = new person(); person.name = \u0026#39;daisy\u0026#39;; console.log(person.name) // daisy delete person.name; console.log(person.name) // kevin 原型的原型是什么呢 ❓ 最顶层也没查到怎么办?\n在前面,我们已经讲了原型也是一个对象,即然是对象,我们就可以用最原始的方式创建它。如下:\nvar obj = new object(); obj.name = \u0026#39;kevin\u0026#39; console.log(obj.name) // kevin 上述示例中的 obj 就是 person.prototype 。其实原型对象就是通过 object 构造函数生成的。\n原型链 这样,就又产生了一个问题, ojbect.prototype 的原型是什么呢? null ❗ 不妨打印一下:\nconsole.log(object.prototype.__proto__ === null)\t// true 然而 null 到底是个什么东东?\nnull 表示“没有对象”,即该处不应该有值。所以 object.prototype.__proto__ 的值为 null 跟 object.prototype 没有原型,其实表达了一个意思。\n所以,查找属性的时候查到的 object.prototype 就可以停止查找了。\n然后,我们就得到了最初的这张图:\n其实,这些都是规定好了的,以上各项都是产生这种规定的可能的过程。\r词法作用域和动态作用域 什么是作用域?作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。\njavascript 采用词法作用域(lexical scoping),即静态作用域。\n其实,这个概念并没有那么多弯弯绕,它是 javascript 这门语言的运行机制和实现所自然而然会出现的结果。\r所谓“词法作用域”,就是函数的作用域在函数定义的时候就决定了;而与之相对对应的“动态作用域”,函数的作用域则是在函数调用的时候才决定的。\n我们先来看一个示例:\nvar value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar(); // 结果是 ??? 结果是 1 !你答对了吗? 让我们分析一下具体的执行过程。\n执行 bar 函数,其在内部调用了 foo 函数,foo 函数执行输出 value 。输出哪个 value ?(上面说过,所谓“词法作用域”,就是函数的作用域在函数定义的时候就决定了)在 foo 定义的时候,其内部并没有局部变量 value ,就根据书写的位置查找上一层的代码,(在这里也就是 var value = 1)即 value 等于 1 ,所以结果会打印 1 。\n试想,如果 javascript 采用的是动态作用域,会输出什么?\n明白了吗?😏\n好的,我们再来看一下示例,如下:\nvar value = 10; foo(); var value = 20; function foo() { console.log(value); } 思考一下,上面的代码会输出什么?为什么?\n❗️ 10\r为什么会是 10 呢?不应该是 20 吗?\r是这样的!在 javascript 中,函数声明和变量声明都会被提升,且函数声明会被先提升。\r提升之后,上述代码的实际执行顺序就如下面这样了(伪代码):\r---------\rvar value;\rfunction foo;\rvalue = 10;\rfoo = function() {\rconsole.log(value);\r}\rfoo();\rvalue = 20;\r---------\r看,现在很容易就可能看出输出 10 了,对吧。\r💡 注意:提升的只是声明,并不是初始定义哦 有不清楚的地方,继续阅读后续章节。😸\n执行上下文栈 思考一下:javascript 中的程序是怎么执行的呢?顺序执行吗?\n实际上,javascript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,如变量提升、函数提升等。\n那么,这个“段”究竟是如何划分的呢?javascript 引擎遇到一段怎样的代码时才会做“准备工作” ?\n可执行代码 这就要说到 javascript 的可执行代码(executable code)的类型有哪些了?\n其实很简单,就有三种:全局代码、函数代码、eval 代码。\n举个 🌰 ,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”专业一点的说法就是“执行上下文(execution context)” 。\n执行上下文栈 接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢???\njavascript 引擎🎙:“我”创建了执行上下文栈(execution context stack, ecs)来管理执行上下文。\n为了模拟执行上下文,我们不妨定义执行上下文栈为一个数组,如下 :\necstack = []; 试想,当 javascript 开始要执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalcontext 表示它,并且只有当整个应用程序结束的时候,ecstack 才会被清空。也就是说,在程序结束之前,ecstack 最底部永远有个 globalcontext :\necstack = [ globalcontext ]; 可见,环境是很重要的,是程序设计语言所必须实现的。\r我们来看一下示例,现在 javascript 引擎遇到下面的这段代码了:\nfunction fun3() { console.log(\u0026#39;fun3\u0026#39;) } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); 当执行一个函数的时候,就会创建一个执行上下文,并且压入 ecs(执行上下文栈),当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了工作原理,让我们来看看如何处理上面这段代码:\n// 伪代码\r// fun1()\recstack.push(\u0026lt;fun1\u0026gt; functioncontext);\r// fun1 中竟然调用了 fun2 ,还要创建 fun2 的执行上下文\recstack.push(\u0026lt;fun2\u0026gt; functioncontext);\r// f**k, fun2 还调用了 fun3 ...\recstack.push(\u0026lt;fun3\u0026gt; functioncontext);\r// fun3 执行完毕\recstack.pop();\r// fun2 执行完毕\recstack.pop();\r// fun1 执行完毕\recstack.pop();\r// javascript 接着执行后续的代码,但是 ecstack 底层永远有个 globalcontext ,直到程序结束 okay ,趁热打铁,让我们来看一段《javascript 高级程序设计》中的示例:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f(); } checkscope(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 再来看另一段示例:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } checkscope()(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.pop();\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 上述两个示例的输出是一样的,都是 'local scope' (若不明白,回顾一下上个章节),但是他们的执行的过程却是有区别的。请好好体会一下其中的不同。\n是不是意犹未尽呢?为了更详细了解两个函数执行上的区别,我们在后续章节继续探究一下执行上下文到底包含了哪些内容。\n变量对象 在上个章节中讲到,当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n变量对象(variable object,vo); 作用域链(scope chain); this 这里,我们重点讲讲创建变量对象的过程。\n变量对象,是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。\n全局上下文 我们先了解一个概念 \u0026ndash; 全局对象。以下是 w3school 中的介绍:\n全局对象是预定义的对象,作为 javascript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。\n在顶层 javascript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定的变量和函数名都会作为该对象的属性来查询。\n例如,当 javascript 代码引用 parseint() 函数时,它引用的是全局对象的 parseint 属性。全局对象是作用域链的头,还意味着在顶层 javascript 代码中声明的所有变量都将成为全局对象的属性。\n下面让我们来看一些关于全局对象的具体示例吧。\n1、可能通过 this 引用,在客户端 javascript 中,全局对象就是 window 对象。\nconsole.log(this); 2、全局对象是由 object 构造函数实体化的一个对象。\nconsole.log(this instanceof object); 3、预定义了一堆,嗯,一大堆函数和属性。\n// 都能生效\rconsole.log(math.random());\rconsole.log(this.math.random()); 4、作为全局变量的宿主。\nvar a = 1;\rconsole.log(this.a); 5、客户端 javascript 中,全局对象有 window 对象指向自身。\nvar a = 1;\rconsole.log(window.a);\rthis.window.b = 2;\rconsole.log(this.b); 看,全局上下文中的变量对象就是全局对象呐!\n可以认为全局对象就是一级掌控者,凡是游离在全局上下文的变量也好、函数也好,都归它管。🔱\r函数上下文 在函数上下文中,我们用活动对象(activation object,ao)来表示变量对象。\n❓ 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎上实现的,不可在 javascript 环境中访问,只有当进行一个执行上下文中,这个执行上下文的变量对象才会被激活,而只有被激活的变量对象(即活动对象)上的各种属性才能被访问。\n❗ 未进入执行阶段之前,变量对象(vo)中的属性都不能访问 !但是进入执行阶段之后,变量对象(vo)转变为了活动对象(ao),里面的属性都能被访问了,然后开始进行执行阶段的操作。它们其实都是同一个对象,只是处于执行上下文的不同生命周期。\n静则 v ,动则 a 活动对象是在进入函数上下文时被创建的,它通过函数的 arguments 属性初始化。 arguments 属性值是 arguments 对象。\n执行过程 执行上下文的代码会分成两个阶段进行处理 \u0026ndash; 分析和执行:\n进入执行上下文; 代码执行。 进入执行上下文\n当进入执行上下文时,这时候还没有执行代码。\n变量对象会包括:\n函数的所有形参(如果是函数上下文) 由名称和对应值组成的一个变量对象的属性被创建 没有实参,属性值设为 undefined 函数声明 由名称和对应值(函数对象 function-object)组成一个变量对象属性被创建 如果变量对象已经存在相同名称的属性,则完全替换这个属性 变量声明 由名称和对应值(undefined)组成一个变量对象的属性被创建 如果变量对象跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 好吧,还是来看例子 🍩。\nfunction foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1); 在进入执行上下文后,这时候的 ao 如下:\nao = { // 进入上下文,活动对象创建\rarguments: {\r0: 1,\rlength: 1\r},\ra: 1, // 形参函数的 `arguments` 属性初始化,\rb: undefined, // 没有对应实参的属性值设为 `undefined`\rc: reference to function c() {}, // 函数声明\rd: undefined // 变量声明。..\r} 代码执行\n在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。\n依然使用上面的例子,当代码执行完后,这时候的 ao 是:\nao = {\rauguments: {\r0: 1,\rlength: 1\r},\ra: 1,\rb: 3,\rc: reference to function c(){},\rd: reference to functionsexpression \u0026#34;d\u0026#34;\r} 到这里,变量对象的创建过程就介绍完了,来个小结吧:\n全局上下文的变量对象初始化是全局对象; 函数上下文的变量对象初始化只包括 arguments 对象; 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始化的属性值; 在代码执行阶段,会再次修改变量对象的属性值。 作用域链 当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 是的,我们将在这个章节来看一下什么是作用域链。\n在上个章节中,我们知道,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。\n下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。\n函数创建 我们知道,在 javascript 中,函数的作用域在函数定义的时候就决定了。\n这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意, [[scope]] 并不代表完整的作用域链。\n举个例子:\nfunction foo() { function bar() { // ... } } 函数创建时,各自的 [[scope]] 为:\nfoo.[[scope]] = [\rglobalcontext.vo\r];\rbar.[[scope]] = [\rfoocontext.ao,\rglobalcontext.vo\r] 函数激活 当函数激活时,进入函数上下文 ,创建 vo/ao 后,就会将活动对象添加到作用域链的前端 。\n这时候执行正文的作用域链,我们命名为 scope :\nscope = [ao].concat([[scope]]);\r// 引自的 ao 为当前执行函数的变量对象,\r// [[scope]] 为其所有父变量对象的层级链\r// 如:上例中 bar 的完整作用域链为 [ao, foocontext.ao, globalcontext.vo] 至此,作用域链创建完毕。\n🌰 我们来看一个示例,结合之前讲的变量对象和执行上下文栈,来总结一下函数执行上下文中作用域链和变量对象的创建过程:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope() { var scope2 = \u0026#39;local scope\u0026#39;; return scope2; } checkscope(); 执行过程如下:\n1、 checkscope 函数被创建,保存作用域链到内部属性 [[scope]]\ncheckscope.[[scope]] = [\rglobalcontext.vo\r]; 2、执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 3、checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数 [[scope]] 属性创建作用域链\ncheckscopecontext = {\rscope: checkscope.[[scope]],\r} 4、第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope2: undefined\r},\rscope: checkscope.[[scope]]\r} 5、第三步:将活动对象压入 checkscope 作用域链顶端\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope2: undefined\r},\rscope: [ao, [[scope]]]\r} 6、准备工作做完,开始执行函数,随着函数的执行,修改 ao 的属性值\ncheckscopecontext = {\rao = {\rarguments: {\rlength: 0\r},\rscope2: \u0026#39;local scope\u0026#39;\r},\rscope: [ao, [[scope]]]\r} 7、查找到 scope2 的值 ,返回函数执行完毕,函数上下文从执行上下文栈中弹出\necstack = [\rglobalcontext\r]; 从 ecmascript 规范解读 this 好吧,这个 cd 的 this 😂\r当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 是的,我们将在这个章节来看一下什么是 this 。让我们从 ecmascript5 规范开始讲起,附上 ecmascript 5.1 规范地址:英文版、中文版 。okay,让我们开始了解规范吧。\nes(ecmascript)的类型分为语言类型和规范类型。\nes 语言类型是开发者直接使用 es 可以操作的,其实就是我们常说的 undefined, null, boolean, string, number 和 object 。\nes 规范类型相当于 meta-values (元类型),是用来用算法描述 es 语言结构和 es 语言类型的。规范类型包括:reference, list, completion, property descriptor, property identifier, lexical environment 和 environment record 。\n这都是什么东东 ❓\r没懂?没关系,我们只要知道在 es 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。今天我们要讲的重点便是其中的 reference 类型,它与 this 的指向有着密切的关联。\nreference 什么是 reference 呢?\nreference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。它是一个 specification type ,也就是“只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。\nreference 由三个部分组成:\nbase value ,属性所在的对象或者就是 environmentrecord ,其值只可能是 undefined, object, boolean, string, number 或 environmentrecord 中的一种; referenced name ,属性的名称; strict reference ,…… 来看个例子:\nvar foo = 1; // 对应的 reference 是: var fooreference = { base: environmentrecord, name: \u0026#39;foo\u0026#39;, strict: false } 又如:\nvar foo = { bar: function() { return this; } }; foo.bar(); // foo // bar 对应的 reference 是: var barreference = { base: foo, propertyname: \u0026#39;bar\u0026#39;, strict: false }; 而且规范中还提供了获取 reference 组成部分的方法,比如:\ngetbase ,返回 reference 的 base value ; ispropertyreference ,如果 base value 是一个对象,就返回 true ; getvalue ,用于从 reference 类型获取对应值。 我们来简单模拟 getvalue 的使用:\nvar foo = 1; var fooreference = { base: environmentrecord, name: \u0026#39;foo\u0026#39;, strict: false }; getvalue(foorefence); // 1 可以看到, getvalue 返回对象属性真正的值,而不再是一个 reference ,切记切记。\n如何确定 this 的值 我们说了那么多关于 reference 的事情,它跟本文的主题 this 有哪些关联呢?\n看规范 11.2.3 function calls:这里讲了当函数调用的时候,如何确定 this 的取值。让我们简单描述一下:\n计算 memberexpression 的结果赋值给 ref ; 判断 ref 是不是一个 reference 类型: 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref) ; 2.2 如果 ref 是 reference ,并且 base value 的值是 environmentrecord,那么 this 的值为 implicitthisvalue(ref) ; 2.3 如果 ref 不是 reference ,那么 this 的值为 undefined 。 下面,让我们一步一步来具体分析一下。\n1、计算 memberexpression 的结果赋值给 ref\n什么是 memberexpression ?规范 11.2 left-hand-side expressions 说明如下:\nmemberexpression :\nprimaryexpression ,原始表达式 functionexpression ,函数定义表达式 memberexpression[expression] ,属性访问表达式 memberexpression.indentifiername ,属性访问表达式 new memberexpression arguments ,对象创建表达式 来看一个例子:\nfunction foo() { console.log(this) } foo(); // memberexpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // memberexpression 是 foo() var foo = { bar: function() { return this; } } foo.bar(); // memberexpression 是 foo.bar 所以简单理解 memberexpression 其实就是 () 左边的部分。\n2、判断 ref 是不是一个 reference 类型\n关键就在于看规范是如何处理各种 memberexpression ,返回的结果是不是一个 reference 类型。来看一个示例:\nvar value = 1; var foo = { value: 2, bar: function() { return this.value; } } // e.g.1 console.log(foo.bar()); // 2 // e.g.2 console.log((foo.bar)()); // 2 // e.g.3 console.log((foo.bar = foo.bar)()); // 1 // e.g.4 console.log((false || foo.bar)()); // 1 // e.g.5 console.log((foo.bar, foo.bar)()); // 1 foo.bar()\n在 e.g.1 中,memberexpression 计算的结果是 foo.bar ,那么 foo.bar 是不是一个 reference 呢?\n查看规范 11.2.1 property accessors 得知该表达式返回了一个 reference 类型,其值为:\nvar reference = {\rbase: foo,\rname: \u0026#39;bar\u0026#39;,\rstrict: false\r}; 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref)\n该值是 reference 类型,那么 ispropertyreference(ref) 的结果是多少呢? base value 为 foo ,是一个对象,所以 ispropertyreference(ref) 返回 true 。\n此时,我们可以确定 this 的值了:\nthis = getbase(ref) // getbase 返回 reference 的 `base value` ,即 foo 所以,在这个例子中, this 的值就是 foo ,e.g.1 的结果就是 2 !\n好吧,依然云里雾里,看来真的有必要系统去阅读一遍规范……\r(foo.bar)()\n实际上, () 并没有对 memberexpression 进行计算,所以其实跟 e.g.1 的结果是一样的。\n(foo.bar = foo.bar)()\n看 e.g.3 ,有赋值操作符,返回的值不是 reference 类型。\n2.3 如果 ref 不是 reference ,那么 this 的值为 undefined\nthis 为 undefined ,非严格模式下, this 的值为 undefined 的时候,其值会被隐式转换为全局对象。\n(false || foo.bar)()\n看 e.g.4 ,逻辑与算法,返回的值不是 reference 类型, this 为 undefined 。\n(foo.bar, foo.bar)()\n看 e.g.5 ,逗号操作符,返回的值不是 reference 类型, this 为 undefined 。\n注意,以上 是在非严格模式下的结果,严格模式下因为 this 返回 undefined , e.g.3 会报错。\n………………\r最后,我们来看一种最普通的情况:\nfunction foo() { console.log(this); } foo(); memberexpression 是 foo ,解析标识符,查看规范 10.3.1 identifier resolution,会返回一个 reference 类型的值:\nvar fooreference = {\rbase: environmentrecord,\rname: \u0026#39;foo\u0026#39;,\rstrict: false\r} 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref)\n因为 base value 是 environmentrecord ,并不是一个 object 类型, ispropertyreference(ref) 的结果为 false ,继续判断\n2.2 如果 ref 是 reference ,并且 base value 的值是 environmentrecord,那么 this 的值为 implicitthisvalue(ref)\nbase value 正是 environmentrecord ,调用 implicitthisvalue(ref) ,查看规范 10.2.1.1.6,implicitthisvalue 方法的介绍:该函数始终返回 undefined 。\n所以最后 this 的值就是 undefined 。\n🍨 最后 尽管我们可以简单地理解 this 为调用函数的对象,如果是这样的话,如何解释下面这个例子呢?\nvar value = 1; var foo = { value: 2, bar: function () { return this.value; } } console.log((false || foo.bar)()); // 1 此外,又如何确定调用函数的对象是谁呢?\n在写这个章节之初,我(冴羽)就面临着这些问题,最后还是放弃从多个情形下给大家讲解 this 指向的思路,而是追根溯源地从 es 规范讲解 this 的指向。尽管从这个角度写起来和读起来都比较吃力,但是一旦多读几遍,明白原理,绝对会给你一个全新的视角看待 this 。而你也就能明白,尽管 foo() 和 (foo.bar = foo.bar)() 最后结果都指向了 undefined ,但是两者从规范的角度上却有着本质的区别。\n此篇讲解执行上下文的 this ,即便不是很理解此篇的内容,依然不影响大家了解执行上下文这个主题下其他的内容。所以,依然可以安心的看下一篇文章。\n好吧,其实在实际应用中,你大概率不会遇到太多关于 `this` 的刁难,常见的应用场景就那几个。一切的一切都要为了解决实际问题而服务。\r我们必须明白,规范或参考书之类的东西,晦涩繁琐几乎是它们的特定属性,也就是说,时间多就通读一下学习,后续逐点参考即可。\n执行上下文 当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 让我们来看一段《javascript 高级程序设计》中的示例:\n// e.g.1 var scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f(); } checkscope(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 再来看另一段示例:\n// e.g.2 var scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } checkscope()(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.pop();\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 上述两段代码都会打印 'local scope' ,虽然执行结果一样,但执行过程却有区别(其执行上下文栈的变化不一样),我们在这个章节就详细的解析执行上下文栈和执行上下文的具体变化过程。\n以 e.g.1 的示例来说,其执行过程如下 :\n1、执行全局代码,创建全局执行上下文,全局上下文被存款利率执行上下文栈\necstack = [\rglobalcontext\r]; 2、全局上下文初始化\nglobalcontext = {\rvo: [global],\rscope: [globalcontext.vo],\rthis: globalcontext.vo\r} 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性 [[scope]]\ncheckscope.[[scope]] = [\rglobalcontext.vo\r]; 3、执行 checkscope 函数,创建 checkscope 函数执行上下文, checkscope 函数执行上下文被存款利率执行上下文栈\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 4、 checkscope 函数执行上下文初始化\n复制函数 [[scope]] 属性创建作用域链; 用 arguments 创建活动对象; 初始化活动对象,即加入形参、函数声明、变量声明; 将活动对象压入 checkscope 作用域链顶端。 同时 f 函数被创建,保存作用域到 f 函数的内部属性[[scope]]\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope: undefined,\rf: reference to function f(){}\r},\rscope: [ao, globalcontext.vo],\rthis: undefined\r} 5、执行 f 函数,创建 f 函数执行上下文, f 函数执行上下文被压入上下文栈\necstack = [\rfcontext,\rcheckscopecontext,\rglobalcontext\r]; 6、 f 函数执行上下文初始化,以下跟第 4 步相同\n复制函数 [[scope]] 属性创建作用域链; 用 arguments 创建活动对象; 初始化活动对象,即加入形参、函数声明、变量声明; 将活动对象压入 f 作用域链顶端。 fcontext = {\rao: {\rarguments: {\rlength: 0\r}\r},\rscope: [ao, checkscopecontext.ao, globalcontext.vo],\rthis: undefined\r} 7、 f 函数执行,沿着作用域链查找 scope 值,返回 scope 值\n8、 f 函数执行完毕, f 函数上下文从执行上下文栈中弹出\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 9、 checkscope 函数执行完毕, checkscope 执行上下文从执行上下文栈中弹出\necstack = [\rglobalcontext\r]; okay,就这样。\n对于 e.g.2 ,大家自己尝试模拟它的执行过程吧。\n好的吧,要返工了 😂\r深入闭包 什么是闭包?\nthis combination of a function object and a scope (a set of variable bindings) in which the function\u0026rsquo;s variables are resolved is called a closure in the computer science literature.\nes 中,闭包指的是:\n从理论角度:所有的函数。因为它们都在创建的时候就将上下文的数据保存起来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于在访问自由变量,这个时候使用最外层的作用域。 从实践角度:以下函数才算是闭包: 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回), 在代码中引用了自由变量 \u0026gt; 闭包是指那些能够访问自由变量的函数;自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。\n\u0026gt; 闭包 = 函数 + 函数能够访问的自由变量\n我们在实际应用中,基本上只关注实践角度的闭包 🤗\n来看看我们的老朋友:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } var foo = checkscope(); foo(); 让我们再次分析一下这段代码中执行上下文栈和执行上下文的变化情况,其简要的执行过程如下:\n进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈; 全局执行上下文初始化; 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈; checkscope 执行上下文初始化,创建变量对象、作用域链、this 等; checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出; 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈; f 执行上下文初始化,创建变量对象、作用域链、this 等; f 函数执行完毕,f 函数上下文从执行上下文栈中弹出。 你可能会有一个疑问 - 当 f 函数执行的时候, checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢 ❓\n当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:\nfcontext = {\rscope: [ao, checkscopecontext.ao, globalcontext.vo],\r} 对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopecontext.ao 的值,说明当 f 函数引用了 checkscopecontext.ao 中的值的时候,即使 checkscopecontext 被销毁了,但是 javascript 依然会让 checkscopecontext.ao 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 javascript 做到了这一点,从而实现了闭包这个概念。\n所以,让我们再看一遍实践角度上闭包的定义:\n即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回), 在代码中引用了自由变量。 看,没什么太多玄妙的东西,只是 js 底层做了某些实现。同样的代码,在 php 中,就会报错,因为在 php 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。\n接下来,看这道刷题必刷,面试必考的闭包题 💯\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2](); 答案是都是 3,让我们分析一下原因。\n当执行到 data[0] 函数之前,此时全局上下文的 vo 为:\nglobalcontext = {\rvo: {\rdata: [...],\ri: 3\r}\r} 当执行 data[0] 函数的时候,data[0] 函数的作用域链为:\ndata[0]context = {\rscope: [ao, globalcontext.vo]\r} data[0]context 的 ao 并没有 i 值,所以会从 globalcontext.vo 中查找,i 为 3,所以打印的结果就是 3。\ndata[1] 和 data[2] 是一样的道理。\n所以让我们改成闭包看看:😈\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2](); 当执行到 data[0] 函数之前,此时全局上下文的 vo 为:\nglobalcontext = {\rvo: {\rdata: [...],\ri: 3\r}\r} 跟没改之前一模一样 ❓ 不然 ❗️\n当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:\ndata[0]context = {\rscope: [ao, 匿名函数 context.ao, globalcontext.vo]\r} 匿名函数执行上下文的 ao 为:\n匿名函数 context = {\rao: {\rarguments: {\r0: 0,\rlength: 1\r},\ri: 0\r}\r} data[0]context 的 ao 并没有 i 值,所以会沿着作用域链从匿名函数 context.ao 中查找,这时候就会找 i 为 0,找到了就不会往 globalcontext.vo 中查找了,即使 globalcontext.vo 也有 i 的值(值为 3),所以打印的结果就是 0。\ndata[1] 和 data[2] 是一样的道理。\n参数按值传递 ecmascript 中所有函数的参数都是按值传递的。\n\u0026gt; 按值传递,就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。\n什么是引用传递 ❓\n当值是一个复杂的数据结构的时候,就按引用传递,即传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。\n其实,就是一个是变量值,一个是地址值(引用内存地址),都是值啦 👻\rcall、apply、bind 的应用 \u0026gt; 该章节为附加章节,旨在说明其使用场景 ,可以参考 js 中的 call、apply、bind 方法详解 和 javascript 中 call、apply、bind 的区别 。\nbind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。\napply \u0026amp; call 在 javascript 中, call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。\njavascript 的一大特点是,函数存在 =定义时上下文 和 =运行时上下文 以及 =上下文是可以改变的 这样的概念。\nfunction fruits() {} fruits.prototype = { color: \u0026#39;red\u0026#39;, say: function() { console.log(\u0026#39;my color is \u0026#39; + this.color); } } var apple = new fruits; apple.say(); // my color is red 如果我们有一个对象 banana = { color: 'yellow' } ,但我们不想对它重新定义 say 方法,那么我们就可以通过 call 或 apply 来调用 apple 的 say 方法,如下:\nbanana = { color: \u0026#39;yellow\u0026#39; } apple.say.call(banana); // my color is yellow apple.say.apply(banana); // my color is yellow = 这里 apply 的语义就更符合人们的直觉,表示把一个对象的方法应用在另一个对象上;相对来说, call 就比较反直觉(用 called 都比 call 强)!好的吧,js 就是个坑货 😅*\n那么,apply 和 call 有什么区别呢?\n它们的作用完全一样,只是接受参数的方式不太一样。老规矩,上例子 🌰\nvar func = function(arg1, arg2) { // ... }; 就可以通过如下方式来调用:\nfunc.call(this, arg1, arg2); func.apply(this, [arg1, arg2]); 其中, this 就是想指定的上下文对象(可以是任何一个 javascript 对象), call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里面。\n= 其实这个很好记忆,只需要知道 apply 和 array 都是 5 个字母,所以其参数要放在数组中,就可以了。*\n一个相对常遇到的场景 - 类(伪)数组使用数组的方法。\n我们来几个面试中可能遇到的问题:\neg.1 定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:\nfunction log(msg) { console.log(msg); } log(1); // 1 log(1, 2); // 1 2 接下来的要求是给每一个 log 消息添加一个 '(app)' 的前缀,比如:\nlog(\u0026#34;hello world\u0026#34;); // (app)hello world 如何做比较优雅呢?我们知道 arguments 参数是个伪数组,通过 array.prototype.slice.call 转化为标准数组,即可使用数组的方法 unshift ,如下:\nfunction log() { var args = array.protype.slice.call(arguments); args.unshift(\u0026#39;(app)\u0026#39;); console.log.apply(console, args); } = 哎,怎么说呢,原理的理解是非常重要的,但学以致用,了解怎么用原理的具体应用才能真正的落地!*\nbind 在讨论 bind() 方法之前,我们先来看一道题目,如下:\nvar altwrite = document.write; altwrite(\u0026#39;hello\u0026#39;); 结果: uncaught typeerror: illegal invocation 怎么样,你答对了吗?为什么会是这样的结果呢?\n原来 altwrite() 函数改变 this 的指向 global 或 window 对象,导致执行时提示非法调用异常,正确的方案就是使用 bind() 方法,使其重新指向 document ,如下:\naltwrite.bind(document)(\u0026#39;hello\u0026#39;); // 或者 altwrite.call(document, \u0026#39;hello\u0026#39;) = 在 react 中,你会经常碰到 bind \u0026hellip;*\nbind() 都有什么用呢???\n_1. 绑定函数\nbind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。\n常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望 this 指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用 bind() 方法能够很漂亮的解决这个问题。\nthis.num = 9; var mymodule = { num: 81, getnum: function() { console.log(this.num); } }; mymodule.getnum(); // 81 var getnum = mymodule.getnum; getnum(); // 9, 因为在这个例子中,\u0026#34;this\u0026#34;指向全局对象 var boundgetnum = getnum.bind(mymodule); boundgetnum(); // 81 mdn 的解释是: bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind() 方法的第一个参数作为 this ,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的能数来调用原函数。\n= 是不是也是一种代理 proxy 呢 ❓*\n具体是个什么东东呢???\n在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:\nvar foo = { bar : 1, eventbind: function(){ var _this = this; $(\u0026#39;.someclass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* act on the event */ console.log(_this.bar); //1 }); } } 由于 javascript 特有的机制,上下文环境在 eventbind:function(){ } 过渡到 $('.someclass').on('click',function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:\nvar foo = { bar : 1, eventbind: function(){ $(\u0026#39;.someclass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* act on the event */ console.log(this.bar); //1 }.bind(this)); } } 在上述代码里,bind() 创建了一个函数,当这个 click 事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用 bind() 时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:\nvar bar = function(){ console.log(this.x); } var foo = { x:3 } bar(); // undefined var func = bar.bind(foo); func(); // 3 这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。\n_2. 偏函数(partial functions)\nbind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。\nfunction list() { return array.prototype.slice.call(arguments); } var list1 = list(1, 2, 3); // [1, 2, 3] // 预定义参数 37 var leadingthirtysevenlist = list.bind(undefined, 37); var list2 = leadingthirtysevenlist(); // [37] var list3 = leadingthirtysevenlist(1, 2, 3); // [37, 1, 2, 3] = 知道这些就行了,其他的不常用……*\napply call bind 的比较 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:\nvar obj = { x: 81, }; var foo = { getx: function() { return this.x; } } console.log(foo.getx.bind(obj)()); //81 console.log(foo.getx.call(obj)); //81 console.log(foo.getx.apply(obj)); //81 三个输出的都是 81,但是注意看使用 bind() 方法的,他后面多了对括号。\n也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。\n最后,来个小结吧:\napply 、 call 、bind 三者都是用来改变函数的this对象的指向的; apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文; apply 、 call 、bind 三者都可以利用后续参数传参; bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 call 和 apply 的模拟实现 call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。\n举个例子:\nvar foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1 注意两点:\ncall 改变了 this 的指向,指向到 foo bar 函数执行了 如何实现模拟一下 call 呢???思考一下 😎\n……\nbind 的模拟实现 bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 mdn )\n由此我们可以首先得出 bind 函数的两个特点:\n返回一个函数 可以传入参数 如何实现模拟一下 bind 呢???思考一下 😎\n……\nnew 的模拟实现 ……\n类数组对象与 arguments 类数组对象 什么是类数组对象呢?\u0026ndash; 拥有一个 length 属性和若干索引属性的对象。像这样:\nvar array = [\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;sex\u0026#39;]; var arraylike = { 0: \u0026#39;name\u0026#39;, 1: \u0026#39;age\u0026#39;, 2: \u0026#39;sex\u0026#39;, length: 3 } 类数组对象在读写、获取长度、遍历三个方基本是相同的,但类数组对象可以使用数组的一些方法会报错,如:\narraylike.push(\u0026#39;4\u0026#39;); // ❌ arraylike.push is not a function 所以终归还是类数组呐…… 错付了呀 😭\n调用数组方法 如果类数组就是任性的想用数组的方法怎么办呢?可以用 function.call 间接调用,如下:\nvar arraylike = {0: \u0026#39;name\u0026#39;, 1: \u0026#39;age\u0026#39;, 2: \u0026#39;sex\u0026#39;, length: 3 } array.prototype.join.call(arraylike, \u0026#39;\u0026amp;\u0026#39;); // name\u0026amp;age\u0026amp;sex // 类数组转数组 // 1. slice array.prototype.slice.call(arraylike); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 2. splice array.prototype.splice.call(arraylike, 0); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 3. es6 array.from array.from(arraylike); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 4. apply array.prototype.concat.apply([], arraylike) array.prototype.map.call(arraylike, function(item){ return item.touppercase(); }); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] 看, call 改变了当前函数调用者的 this 指向到了 arraylike 。\narguments 对象 arguments 对象就是一个类数组对象!\narguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 arguments 对象。\n翠花,上 🌰\nfunction foo(name, age, sex) { console.log(arguments); } foo(\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;sex\u0026#39;) 打印结果如下:\nlength 属性\n其中,arguments 对象的 length 属性,表示 实参的长度\nfunction foo(b, c, d){ console.log(\u0026#34;实参的长度为:\u0026#34; + arguments.length) } console.log(\u0026#34;形参的长度为:\u0026#34; + foo.length) foo(1) // 形参的长度为:3 // 实参的长度为:1 callee 属性\narguments 对象的 callee 属性,通过它可以调用函数自身。\n讲个闭包经典面试题使用 callee 的解决方法:\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { (data[i] = function () { console.log(arguments.callee.i) }).i = i; } data[0](); // 0 data[1](); // 1 data[2](); // 2 arguments 和对应参数的绑定\n传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享。\n* 如果是在严格模式下,实参和 arguments 是不会共享的。\nfunction foo(name, age, sex, hobbit) { console.log(name, arguments[0]); // name name // 改变形参 name = \u0026#39;new name\u0026#39;; console.log(name, arguments[0]); // new name new name // 改变 arguments arguments[1] = \u0026#39;new age\u0026#39;; console.log(age, arguments[1]); // new age new age // 测试未传入的是否会绑定 console.log(sex); // undefined sex = \u0026#39;new sex\u0026#39;; console.log(sex, arguments[2]); // new sex undefined arguments[3] = \u0026#39;new hobbit\u0026#39;; console.log(hobbit, arguments[3]); // undefined new hobbit } foo(\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;) 传递参数\n将参数从一个函数传递到另一个函数,如下:\n// 使用 apply 将 foo 的参数传递给 bar function foo() { bar.apply(this, arguments); } function bar(a, b, c) { console.log(a, b, c); } foo(1, 2, 3) // 1 2 3 使用 es6 的 ... 运算符,我们可以把 arguments 轻松转成数组。\n结语 好的吧,好像学到了一些东西,又好像什么都没学到 🤪 ,后续还有一些篇章,但对我来说已经没有多大吸引力,有兴趣的小伙伴可以参阅 冴羽写博客 javascript 深入系列 。\n","date":"2022-07-25","permalink":"https://loveminimal.github.io/posts/deep-js/","summary":"\u003cp\u003e🔔 摘录自 \u003ca href=\"https://github.com/mqyqingfeng/Blog\"\u003e冴羽写博客之深入系列\u003c/a\u003e ,写的很不错。\u003c/p\u003e","title":"冴羽写博客之深入系列"},{"content":"健身训练的项目有很多,而有一些运动是大多数人所热爱的。跑步 🏃 就是一项大众所熟悉的,门槛比较低的运动项目,无论男女老少都可以跑起来。\n那么,每天坚持跑步,身材可以瘦下来吗?可以 ❗ 跑步的可以实现的目标很多,想要达到减肥的目的,需要注意以下一些方面。\n跑步速度 跑步可以分为快跑跟慢跑,快跑属于无氧运动,是无法持续坚持的运动,容易力竭并且出现小粗腿,这并不是你跑步的初衷。\n而慢跑属于可持续进行的训练,可以达到瘦腿塑形的效果。因此,跑步的时候你的速度控制为 6-9km/h 的速度即可。\n如果你的体重基数太大,很难坚持下来,建议你可以慢跑结合快走,坚持一段时间后心肺功能会加强,运动能力会提升,这个时候尝试慢跑训练,你会发现持久力有所提升。\n跑步时长 慢跑一小时大概可以消耗 500 大卡 的热量,而一斤脂肪的热量是 3900 大卡,如果你想要减掉 1 斤脂肪,那么要跑步将近 8 小时,如果你想要减掉 5 斤脂肪,那么你需要跑 40 小时左右。\n任重而道远!!!\r如果你每天跑步 1 小时,那么要坚持 40 天左右才能减脂 5 斤。所以,跑步减肥是一项需要坚持才能有所成效的事情。\n拉伸训练 跑步后肌肉会出现充血问题,很多人误以为是肌肉腿,其实,跑步后进行充分的拉伸放松腿部肌群,这样双腿也会越来越细长好看,跑步后肌肉酸疼问题也会减轻。\n所以,跑步后不要马上坐着,一定要针对腿部肌群进行拉伸,这样还能提升双腿的灵活性跟柔软度。\n饮食管理 跑步的时候,如果你能结合饮食管理,那么减肥效果也会翻倍。如果你胡吃海喝没有管理好饮食,那么你每天多摄入的热量可能比你跑步消耗的热量更高,比如:一份薯条的热量有 320 大卡,一杯可乐的热量是 180 大卡,一个炸鸡汉堡的热量达到了 400 大卡以上。\n只有管理好饮食,远离各种高热量、过度加工的食物,多吃一些低热量、天然的食物,才能控制卡路里摄入,提升减肥效率。\n参考链接 https://new.qq.com/omn/20220718/20220718a09w9900.html ","date":"2022-07-22","permalink":"https://loveminimal.github.io/posts/running/","summary":"\u003cp\u003e健身训练的项目有很多,而有一些运动是大多数人所热爱的。跑步 🏃 就是一项大众所熟悉的,门槛比较低的运动项目,无论男女老少都可以跑起来。\u003c/p\u003e\n\u003cp\u003e那么,每天坚持跑步,身材可以瘦下来吗?可以 ❗ 跑步的可以实现的目标很多,想要达到减肥的目的,需要注意以下一些方面。\u003c/p\u003e","title":"跑步"},{"content":"为什么需要防抖和节流?它们是什么?有什么区别?适用场景是什么?\n简介 防抖与节流,主要用来控制事件处理函数的调用频率 ❗\n在进行窗口的缩放(resize)、滚动(scroll),输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器负担,导致用户体验糟糕。\n试想,你的事件处理函数是异步的,每次调用都会进行大量的数据检索……\r如何控制调用频率呢?防抖和节流就是做这个的。\n防抖 什么是防抖(debounce)?当持续触发事件时,在设定时间段🕐内没有再触发事件,事件处理函数才会执行一次;若在设定的时间段🕜内,又一次触发事件,就重新开始延时。\n如下图,持续触发 scroll 事件时,并不执行 handle 函数,当 1000 毫秒内没有触发 scroll 事件时,才会延时触发 scroll 事件。\n……\n节流 什么是节流(throttle)?当持续触发事件时,保证一定时间段内只调用一次事件处理函数。如下图,持续触发 scroll 事件时,并不立即执行 handle 函数,每隔 1000 毫秒才会执行一次 handle 函数。\n……\n区别 不能看出,防抖的控频原理在于持续触发事件时,在设定的时间段内不再触发事件,才会调用一次执行函数;而节流的控频原理在于持续触发事件时,每隔设定的时间段才会调用一次执行函数。\n当然,实际操作中,我们会对二者做一些相应的优化处理,但是从本质上来说,它们就是如此。\n参考链接 https://www.cnblogs.com/momo798/p/9177767.html https://www.cnblogs.com/youma/p/10559331.html https://segmentfault.com/a/1190000012066399 https://segmentfault.com/a/1190000018428170 https://zhuanlan.zhihu.com/p/38313717 https://juejin.cn/post/6844903669389885453 https://redd.one/blog/debounce-vs-throttle https://webdesign.tutsplus.com/tutorials/javascript-debounce-and-throttle--cms-36783 ","date":"2022-07-21","permalink":"https://loveminimal.github.io/posts/debounce-and-throttle/","summary":"\u003cp\u003e为什么需要防抖和节流?它们是什么?有什么区别?适用场景是什么?\u003c/p\u003e","title":"防抖与节流"},{"content":"我理解中的极简主义,核心“断、舍、离”,理念“少即是多”,重点“有序”。\n极简主义是一种生活和思考模式,实现它的方式有很多, gtd 是其中一种,也是目前我感觉比较好的一种。它很有趣,但并不容易坚持,毕竟形成一种全新的生活习惯是一个长期的过程。开始很容易,难在持续。\n真正的“少” 为什么强调“少”?是因为,日常生活中往往充斥了过多不必要的东西,它们毫无意义,却占用了过多的时间,消耗掉过多的精力。所以,我们需要强调“少”,一般情况下,这样做总是没有错的。但是也不要走入另一个极端,我们应该始终明白,之所以强调“少”,是因为“过多了”,而不是因为要“足够少”。切忌为了“少”而少,适度、平衡才是我们真正的追求。\n现实生活中没有遇到过,网上到是见了不少“极简主义”的拥趸者,他们往往大肆鼓吹宣扬过度的“少”,有甚者还以清空所有家具为荣。也许,他们是真正的享受吧,但我并认为这是可持续的。如同,我认为吃面要用筷子而不是勺子,喝汤要用勺子而不是筷子一样。再次强调,真正的“少”,归于平衡、有序,而不是“空”。\n极简主义的目标在于形成一个“最少需要”,你可拥有很多,但我们说的“最少”是一个临界,比“最少”再少一些,就会带来不便,比“最少”再多一点,虽然不会带来不便,但可能产生毫无意义的消耗。“最少需要”的观念是也只能是一个指导,因人而异,要根据自己当前的现状去调整,持续性的调整,以达到一个相对舒适和谐的生活节奏。\n“最少需要”是动态的,其内容在不断地更新,但原则是一致的 \u0026ndash; 但(仅)取所需。精简是一个过程,这过程本身就是一种享受。结果是重要的,但结果也只不过是阶段性的一个时刻,是水到渠成,是自然而然。\n有序 gtd 之所以是一种优秀的极简主义实现方式,在于其“应收尽收,拒绝悬而未决”的品质。它使得你生活中的事务更加系统化、层次化,有主次、分轻重缓急,非常有助于“减少”不必要的消耗。你知道自己当下最适合做的事情是什么,焦虑自然而然地远离你。因为我们焦虑的原因往往不在于要做的事很多,而在于不知道要做什么。\n安全感和掌控来自“有序”,“有序”是系统性的收集、组织和管理的结果。\n未完待续……\n✔️ 《再谈“个人管理”》\n","date":"2022-07-20","permalink":"https://loveminimal.github.io/posts/minimalist-lifestyle/","summary":"\u003cp\u003e我理解中的极简主义,核心“断、舍、离”,理念“少即是多”,重点“有序”。\u003c/p\u003e\n\u003cp\u003e极简主义是一种生活和思考模式,实现它的方式有很多, \u003ca href=\"../get-things-done/\"\u003eGTD\u003c/a\u003e 是其中一种,也是目前我感觉比较好的一种。它很有趣,但并不容易坚持,毕竟形成一种全新的生活习惯是一个长期的过程。开始很容易,难在持续。\u003c/p\u003e","title":"极简主义生活"},{"content":"博客系统迁移好多次了,多少次?详见 一场“疲惫”的主题制作之旅 。域名啊、博客文章的层级结构啊,都难免存在差异。这就导致了一个问题,之前在别的网站发布的一些页面链接就成了“死链”。虽然,可以简单地重写向到 404 ,体验却不好,最好是可以重定向到变化后的页面。\n周末无聊,简单实现了一下,做个记录。\n如何重定向 之前的链接都是 https://ovirgo.com/xxx-yyy-zzz.html 格式的链接,现在要使之重定向到 https://ovirgo.com/posts/xxx-yyy-zzz 到这样的链接地址。\n修改服务器端 nginx 的相关配置,比如 /etc/nginx/conf.d/xxx.conf (当前主机为 ubuntu server ,其他发型版可能有区别)。\nlocation / {\r# ...\rif (!-e $request_filename) {\rrewrite ((\\w*=)*\\w*)\\.html /posts/$1 last;\rbreak;\r}\r# ...\r} $1 是什么?就是前面正则表达式的匹配的第一个捕获分组。\n当请求资源不存在时,重定向到新的路径。对,就是 rewrite 了。\n如果,你还不了解,或是很长时间没有使用正则表达式了,可以阅读一下另一篇博文 \u0026ndash; 正则表达式,附一个很不错的 正则表达式测试网站 。\nnginx rewrite 访问重写 rewrite 是 nginx http 请求处理过程中的一个重要功能,它是以模块的形式存在于代码中的,其功能是对用户请求的 uri 进行 pcre 正则重写,然后返回 30x 重定向跳转或按条件执行相关配置。\nrewrite 模块内置了类似脚本语言的 set、if、break、return 配置指令,通过这些指令,用户可以在 http 请求处理过程中对 uri 进行更灵活的操作控制。rewrite 模块提供的指令可以分为两类:\n标准配置指令(只是对指定的操作进行相应的操作控制); 脚本指令(可以在 http 指令域内以类似脚本编程的形式进行编写)。 标准配置指令 这里我们只介绍与本次修改相关的配置指令。\n名称 rewrite 指令 指令 rewrite 作用域 server, location 指令说明 对用户的 uri 用正则表达式的方式进行重写,并跳转到新的 uri 其语法格式如下:\nrewrite regex replacement [flag];\r# - regex 是 pcre 语法格式的正则表达式\r# - replacement 是重写 uri 的改写规则\r# - flag 是执行该条重写指令后的操作控制符 其中,对于 replacement 来说,当改写规则以 http://、https:// 或 $scheme 开头时,nginx 重写该语句后将停止执行后续任务,并将改写后的 uri 跳转返回客户端。\n对于,flag ,作为操作符有如下 4 种:\nlast 执行完当前重写规则跳转到新的 uri 后继续执行后续操作; break 执行完当前重写规则跳转到新的 uri 后不再执行后续操作,不影响用户浏览器 uri 显示; redirect 返回响应码 302 的临时重定向,返回内容是重定向 uri 的内容,但浏览器网址仍为请求时的 uri ; permanent 返回响应状态码 301 的永久重定向,返回内容是重写向 uri 的内容,浏览器网址变为重定向的 uri 。 脚本指令 这一块目前没有涉及,用的到的时候再说吧。(但取所需 😂)\n……\nfaq 如果,你要实验重写规则,你可能会遇到和我一样的问题,比如说,当你把 flag 为 permanent 时,你可能重定向了某个 uri,当你实验一次之后,发现后续再修改也不起作用了,咦,困扰。\n设置了 permanent ,被永久重定向了,你需要先清理浏览器缓存,再进行后续的操作。\n参考链接 https://www.w3schools.cn/nginx/nginx_command_rewrite.asp ","date":"2022-07-16","permalink":"https://loveminimal.github.io/posts/nginx-rewrite/","summary":"\u003cp\u003e博客系统迁移好多次了,多少次?详见 \u003ca href=\"../a-theme-making-journey\"\u003e一场“疲惫”的主题制作之旅\u003c/a\u003e 。域名啊、博客文章的层级结构啊,都难免存在差异。这就导致了一个问题,之前在别的网站发布的一些页面链接就成了“死链”。虽然,可以简单地重写向到 404 ,体验却不好,最好是可以重定向到变化后的页面。\u003c/p\u003e\n\u003cp\u003e周末无聊,简单实现了一下,做个记录。\u003c/p\u003e","title":"nginx 重定向"},{"content":"👉 推荐一个不错的网站 - c 语言中文网\n关于进制 在计算机内部,数据都是以二进制的形式存储的。\n进制是什么?\n进制(system of numeration),是人为定义的带进位的计数方法。对于任何一种进制(如 x 进制),就表示每一位上的数运算时都是逢 x 进一位。比如,我们常用的十进制,基数为 10,数码由 0-9 组成,计数规律逢十进一。\n人类天然选择了十进制,思考一下这是为什么?\n同理,二进制的基数为 2,数码由 0-1 组成,计数规律逢二进一。八进制的基数为 8,数码由 0~7 组成,计数规律逢八进一。十六进制的基数为 16,数码由 0~9a~f 组成,计数规律逢十六进一。\n进制转换 n 进制转十进制 二进制、八进制和十六进制向十进制转换都非常简单,就是 “按权相加”。所谓“权”,即“位权”。\n假设当前数字是 n 进制,那么:\n对于整数部分,从右往左看,第 i 位的位权等于 ni-1 ; 对于小数部分,恰好相反,要从左往右看,第 j 位的位权为 n-j 然而,将十进制转换为其它进制时比较复杂,整数部分和小数部分的算法不一样。\n十进制转 n 进制 十进制整数转换为 n 进制整数采用 “除 n 取余,逆序排列” 法。如下图:\n十进制数字 36926 转换成八进制的结果为 110076 。\n十进制小数转换成 n 进制小数采用 “乘 n 取整,顺序排列” 法。如下图:\n十进制小数 0.6875 转换成二进制小数的结果为 0.1011 。\n二进制和八进制、十六进制的转换 从上文,我们知道,以十进制为中转,可以完成所有进制的转换。但由于某种必然,将二进制转换为八进制和十六进制时有更加简洁的方法。\n1. 二进制整数和八进制整数之间的转换\n二进制整数转换为八进制整数时,每三位二进制数字转换为一位八进制数字,运算的顺序从低位向高位依次进行,高位不足三位用零补齐。\n二进制整数 1110111100 转换为八进制的结果为 1674 。\n八进制整数转换为二进制整数时,思路是相反的,每一位八进制数字转换为三位二进制数字,运算的顺序也是从低位向高位依次进行。\n八进制整数 2743 转换为二进制的结果为 10111100011。\n2. 二进制整数和十六进制整数之间的转换\n二进制整数转换为十六进制整数时,每四位二进制数字转换为一位十六进制数字,运算的顺序是从低位向高位依次进行,高位不足四位用零补齐。\n二进制整数 10 1101 0101 1100 转换为十六进制的结果为 2d5c。\n十六进制整数转换为二进制整数时,思路是相反的,每一位十六进制数字转换为四位二进制数字,运算的顺序也是从低位向高位依次进行。\n十六进制整数 a5d6 转换为二进制的结果为 1010 0101 1101 0110。\n实际工作中,二进制、八进制、十六进制之间几乎不会涉及小数的转换,八进制和十六进制之间也极少直接转换。\n数据存储 计算机要处理的信息是多种多样的,如数字、文字、符号、图形、视音频等,这些信息在人们的眼里是不同的。但对于计算机来说,它们在内存中都是一样的,皆 以二进制的形式来表示 。\n在运行时,数据和指令都要载入到内存中才可以。内存依托于内存条,后者是一个非常精密的部件,包含了上亿个电子元器件。这些元器件,实际上就是电路,依据电路电压的不同(断电 0v、通电5v)状态,表示为 0 和 1 。\n看,物理世界与数字世界连接了。🧲 以此为基点,我们可以模拟整个宇宙了。\r一个元器件为 1 bit ,表现力太小了,一般情况下,我们将 8 个元器件看做一个单位 (8 bit = 1 byte),这样每个单位就有 28 = 256 种表示。\n答前所问,人类天然选择了十进制,计算机则天然选择了二进制,因为人类有十根手指,而计算机只有两种状态。(可以如此认为 😸)\n计算机以二进制的形式来存储数据,它只认识 0 和 1 两个数字,我们在屏幕上看到的文字,在存储之前都被转换成了二进制,在显示时也要根据二进制找到对应的字符。\n可想而知,特定的文字必然对应着固定的二进制,否则在转换时将发生混乱。那么, 怎样将文字与二进制对应起来呢?\n这就需要有一套规范,计算机公司和软件开发者都必须遵守,这样的一套规范就称为字符集(character set)或者字符编码(character encoding)。\n我们将在 字符集和字符编码 中认识它们。\n参考链接 http://c.biancheng.net/view/1724.html https://www.interviewcake.com/article/python/data-structures-coding-interview?course=fc1\u0026amp;section=algorithmic-thinking ","date":"2022-07-15","permalink":"https://loveminimal.github.io/posts/base-and-conversion/","summary":"\u003cp\u003e👉 推荐一个不错的网站 - \u003ca href=\"http://c.biancheng.net/\"\u003eC 语言中文网\u003c/a\u003e\u003c/p\u003e","title":"进制及转换"},{"content":"字符是什么?字母、汉字、标点符号、控制字符、假名……\n计算机中储存的信息都是二进制数表示的,我们在屏幕上看到的英文、汉字等字符都是二进制转换之后的结果。按照何种规则将字符存储在计算机中,如 a 用什么表示,称为“编码”;反之,将存储在计算机中的二进制数解析显示出来,称为“解码”。\n= 可以这样说,人类可读的即为“解”,计算机可读的即为“编”。\n严格来说,字符集和字符编码不是一个概念,字符集定义了字符和二进制的对应关系,为字符分配了唯一的编号,而字符编码规定了如何将字符的编号存储到计算机中。\n也就是说,字符编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。如下图所示:\n为什么要严格区分字符集与字符编码这两个概念呢?\n在早期,字符集与字符编码是一对一的,但随着时间的发展,出现了一对多的情形,即一种字符集可能有了多种编码实现。如上图所示,unicode 字符集就有 utf-8、utf-16、utf-32 多种编码方式。\n如果你想要了解更多关于字符集及字符编码相关的历史,可以阅读 该文档 。\n常用字符集 \u0026amp; 编码 知道了字符、字符集及字符编码的基本概念,哪到底都有什么字符集及其编码规则呢 ❓\nascii ascii(american standard code for information interchange,美国信息交换标准代码),是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语(ascii)和其他西欧语言 (eascii,基于 ascii 的扩展)。它由 ansi(american national standard insitute,美国国家标准学会)制定的,是一种标准的 单字节字符编码方案 。\n好吧,还是缩写好用 😅……\r单字节?1 个字节 包含 8 位,也就是最多编码 256 个字符喽。事实上,基础的 ascii 只使用了 7 位,共 128 个字符,后续的 eascii 扩展为了表示更多欧洲常用字符,才使用了第 8 位。\n下图为基础版的 ascii :\ngb* 系 256 种表示?在汉字面前微不足道!不够用啊!!!\n其实对于汉字的个数并没有个准确的数字,不完全统计汉字大约有十万个左右。根据 1988 年颁布的《现代汉语常用字表》,在我们日常生活中常用的汉字有 2500 个,次常用的汉字有 1000 个。\n一个字节不够用?那就再加一个,216 = 65536 种表示,勉强基本够用了……\n前后经历了,gb2312 → gbk → gb18030 ,具体细节请自行查阅哦。你可能还见过 big5 ,它是繁体中文常用的汉字字符标准。\ngbk 并非国标,是微软搞出来的在 gb13000(国标过渡版)基础上扩展的(编码方式不同,emm\u0026hellip;),最初实现于 windows95 简体中文版。\nunicode 全世界有上百种语言,各国有各国的标准,会冲突的!!!多语言混合的文本中,就成了“一锅粥”,乱码了……\n有没有一种字符集,收录了世界上所有的字符,统一编码呢 ❓ 有,unicode !\nunicode 编码系统为表达任意语言的任意字符而设计,它使用 4 字节数来表达每个字母、符号,或者表意文字。\n4 个字节,232 = 4,294,967,296 种表示,遇到外星人 📡 也够用了……\nutf-32 就是用 4 个字体,utf-16 用的是 2 个字节,那 utf-8 就是 1 个字节了? 不,utf-8 是变长的(可变长度字符编码)。\n就左边这们大佬(肯·汤普森)搞出来的,他还做了 b 语言,基于 b 语言的 unix ,c 语言,后又用 c 语言重新编写了 unix ,现在又搞了个 golang …… 右边这位好基友(丹尼斯·里奇)也是个神,unix 和 c 语言的共同创始人。\n谢祖师爷……歇歇吧,卷不动了…… 😱\r为什么我们需要 utf-8 呢?如果用 utf-16(最常用的 unicode 标准),如果你写的都全部是英文的话,使用它编码就需要多出一倍的存储空间,在存储和传输上就十分不划算。\n硬盘不贵,带宽贵啊!\r本着节约的精神,可变长编码的 utf-8 诞生了,它把一个 unicode 字符根据不同的字符大小编码成 1~6 个字节,常用的英文字母被编码成 1 个字节(ascii 的超集),汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4~6 个字节。\n实际上,现在计算机系统通用的字符编码工作方式:在计算机内存中,统一使用 unicode 编码(utf-16),当需要保存到硬盘或者需要传输的时候,就转换为 utf-8 编码。\n用记事本编辑的时候,从文件读取的 utf-8 字符被转换为 unicode 字符到内存里,编辑完成后,保存的时候再把 unicode 转换为 utf-8 保存到文件:\n浏览网页的时候,服务器会把动态生成的 unicode 内容转换为 utf-8 再传输到浏览器。\n互联网工程工作小组(ietf)要求所有互联网协议都必须支持 utf-8 编码。\nutf-8 的实现原理 上文,我们知道 utf-8 是可变长度编码,那么在解码时,如何知道当前字符占用几个字节呢?通过解析第一个字节获取信息。\n1 个字节\n如果第一个字节的最高位是 0 ,那么表示当前字符占一个字节,如下:\n这里也可以看出 utf-8 是完全兼容 ascii 码的,因为 ascii 码的最高位也是 0 。\n2 个字节\n如果第一个字节的最高位是 110 ,那么表示这个字符占 2 个字节,第二个字节的最高 2 位是 10 ,如下:\n蓝色部分的数字组合在一起,就是实际的码位值。假如,要表示的字符,其码位值为 413 (对应填制为 00110011101 ),其表示就如下:\n3 个字节\n如果第一个字节的最高位是 1110,那么第 2 和第 3 个字节的最高位是 10 ,如下:\n4 个字节\n原理同上,只是第一个字节的最高位是 11110 ,如下 :\n6 个字节\n不同字节对应的码位范围如下图,左侧 bits 栏表示用于表示码位的 bit 数,如 4 个字节,其中有 21 位用于表示码位,即上图中的蓝色部分 。\n不难看出,utf-8 的产生是循序渐进的, 其拥有很高的灵活性,而且可以进行扩展,能够表示的字符范围很大。\n结语 一切都是在发展的,一切都是在改善的,有时候,只要一点奇思妙想,就会让世界变得更加美好。\n参考链接 https://blog.csdn.net/weixin_44198965/article/details/93125017 https://zhuanlan.zhihu.com/p/260192496 https://www.runoob.com/w3cnote/charset-encoding.html https://www.liaoxuefeng.com/wiki/1016959663602400/1017075323632896 https://blog.csdn.net/whahu1989/article/details/118314154 ","date":"2022-07-14","permalink":"https://loveminimal.github.io/posts/charset-and-character-encoding/","summary":"\u003cp\u003e字符是什么?字母、汉字、标点符号、控制字符、假名……\u003c/p\u003e\n\u003cp\u003e计算机中储存的信息都是二进制数表示的,我们在屏幕上看到的英文、汉字等字符都是二进制转换之后的结果。按照何种规则将字符存储在计算机中,如 \u003ccode\u003ea\u003c/code\u003e 用什么表示,称为“编码”;反之,将存储在计算机中的二进制数解析显示出来,称为“解码”。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 可以这样说,人类可读的即为“解”,计算机可读的即为“编”。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"字符集和字符编码"},{"content":"https://jwt.io/\n相关参考:\njson web token introduction - jwt.io json web token 入门教程 - 阮一峰的网络日志 jwt 介绍 - step by step - 技术译民 - 博客园 ","date":"2022-07-14","permalink":"https://loveminimal.github.io/posts/json-web-tokens/","summary":"\u003cp\u003e\u003ca href=\"https://jwt.io/\"\u003ehttps://jwt.io/\u003c/a\u003e\u003c/p\u003e","title":"json web tokens"},{"content":" 当前博文以 org mode 格式编辑。\ni have a vim, i have a emacs.\neh~ vim-emacs!\ni have a code, i have a vim-emacs.\neh~ code-vim-emacs!\ncve!\n简介 在 键位映射那些事儿 中, blablabla…… 说了好多。之前,我一直在个人电脑上使用 vscode ,各种插件和键位设置都是可以自动同步的。然而,当切换到别人的电脑时,就又不得不面对原版的键位模式。还是那句话,不是不能接受,只是有点困扰,哎,处女座……\n于是,我开发了一款简单易用的 vscode 插件 – cve keymap ,本文做一些插件开发过程中遇到问题的记录。\n\u0026lt;img src=\u0026#34;imgs/1.jpg\u0026#34; width=\u0026#34;\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n基础 磨刀不误砍柴工,先补了一下基础知识和概念。\n一次偶然的机会,看到极客教程中的 vscode 模块 ,里面系统性的介绍了 vscode 的一些实现原理( 部分命令可能有些老旧,但无伤大雅)。大家如果感兴趣,可以点进去阅读一下。\n当然,你也可以直接阅读官方文档。\n安装 开发和编译 vscode 插件,需要做一些准备工作,确保当前环境已经安装了 node.js 和 git ,然后执行以下命令安装 yeoman 和 vscode extension generator 。\nnpm install -g yo generator-code # 使用脚手架创建插件模板, myextension 是你的插件名称 yo code myextension 注意, node.js 的版本不能太低,本机使用的是 16+ ,最低不要低于 14 ,不然有些依赖不兼容。如果,你的已有项目依赖低版本的 node.js (哎,历史项目啦),那么你需要 nvm 。\n\u0026lt;img src=\u0026#34;imgs/tn.jpg\u0026#34; width=\u0026#34;\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n我们选择 new keymap 模板,其他模板请自行了解 😅。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这个过程中,唯一可能遇到的问题,就是我们提到的 node.js 版本的问题。 \u0026lt;/div\u0026gt;\n一系列操作之后,就生成了我们的插件目录。如下:\n. ├── changelog.md ├── .git ├── .gitattributes ├── .gitignore ├── package.json ├── readme.md ├── vsc-extension-quickstart.md ├── .vscode └── .vscodeignore 2 directories, 7 files 使用 vscode 打开该目录,即可进行开发工作了。具体细节,请参考官方文档。\n开发 开发过程中,可以使用 f5 进入到调试预览模式,它会新打开一个 vscode 窗口(微软良心开发工具 🥰)。\n发布 在插件开发完成之后呢,就可以打包成 vsix 格式的插件包分享给其他小伙伴使用了。更多了解 publishing extensions。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 如果,你是第一次制作插件,并准备发布它,恭喜你,坑才刚刚开始,你还有一系列准备工作要完成。 \u0026lt;/div\u0026gt;\n打包、发布在配置完成之后,后续执行都是很简单的。\n首先,我们先准备一个 cli 工具 – vsce ,来管理我们的 vscode 插件。安装:\nnpm install -g vsce 而后执行 vsce package 即可完成打包。but 你也大概率可能会遇到以下问题,来看看吧。\n# 问题 1 error:make sure to edit the readme.md file before you publish your extension. 直接删除目录中的 readme.md ,新建一个写些说明保存提交即可。 不要用原文件进行修改,我这边试了好多次都通不过,一直报上述错误。\n\u0026gt; 注意:同于安全考虑, vsce 不允许插件中包含用户自己添加的 svg 格式图片,且远程图片链接要使用 https urls 。\n1. 获取 pat\n因为 vscode 使用 azure devops 为插件市场提供服务,所以去申请一个账号吧,如果你还没有的话。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 此处,建议使用你平时使用的微软账号。 \u0026lt;/div\u0026gt;\nvsce 可以通过 personal access tokens 发布扩展,所以,如果我们想要更加方便的发布插件,就需要先创建一个 pat(personal access tokens ,私人访问令牌) 。如何创建呢?点击这里查看 。\n注意:给 pat 复制下来记录一下,关了页面之后 ,你就再也找不到了。\n2.创建 publisher\n# 问题 2 error:missing publisher name. learn more:https://code.visualstudio.com... publisher 自然就是发布者了,它是插件市场的一种身份认证,随后你需要在 package.json 中插入 \u0026#34;publisher\u0026#34;: \u0026#34;xxxx\u0026#34; 类似行,更多详情查看 package.json 。\n我们可以通过插件市场的 management page 来创建一个 publisher 。\n注意,这里需要使用申请 pat 时的那个微软账号(要一致)。\n创建完成之后 ,我们可以使用通过下述命令进行验证:\nvsce login \u0026lt;publisher name\u0026gt; 3. 发布\n通过验证后,后续只需要执行如下命令,就可以方便地发布你的插件到插件市场了。\nvsce publish 当然,我们也可以使用 vsce package 打包的 vsix 文件,手动上传到插件市场,但是如果,你后续大频率更新插件的话,就会很困扰了。\n最后 怎么说呢?初看可能很繁琐,但其实按照官方教程一步步进行操作,遇到问题后再搜索查询一下,整个过程还是很容易完成的。just go ! 🌟\n","date":"2022-07-06","permalink":"https://loveminimal.github.io/posts/cve-keymap/","summary":"\u003cp\u003e\n\u003cstrong\u003e当前博文以 \u003ccode class=\"verbatim\"\u003eOrg Mode\u003c/code\u003e 格式编辑。\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eI have a vim, I have a emacs.\u003cbr\u003e\nEh~ vim-emacs!\u003cbr\u003e\nI have a code, I have a vim-emacs.\u003cbr\u003e\nEh~ code-vim-emacs!\u003cbr\u003e\nCVE!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"vscode 插件 cve keymap 的开发记录"},{"content":"vim 的键位不错,无论是移动光标还是进行编辑操作,但是如果是使用中文输入的话,就很尬 \u0026ndash; 中文状态下命令模式不能用,虽然通过一些方式可以使得在切换为非插入模式时自动切换为英文状态,但总是不那么尽如人意。\nemacs 的键位模式,就很适应中英文的混合输入,唯一的缺点就是有点费小手指。在 emacs 中,我一般会启用 evil 插件,如此,可以实现如果是只读文件和做一些删减工作的时候,就直接使用 vim 的键位模式,而在插入模式下,就使用 emacs 的键位模式,很不错,附上一份个人的 evil 插件的配置 init-evil.el 。唯一不足的是,这种方式只有在 emacs 中才是可用的,如果你使用其他编辑器时,就没有这份待遇了。不过,单纯的 emacs 键位也足够好用了。\n在 ide 中,基本上都可以配置键位映射,如 jetbrains 家的系列产品中的 emacs 键位映射都很好用,映射的也比较完整。\n但是在 visual studio 中,就不那么喜人了,vs 恪守着在 windows 系列下的那一套操作模式,不是说不好,而是你好歹让人定制一下。和 vs 比起来,vscode 就良心的多了,丰富的插件仓库,美观的主题,流畅度也很好。因为 vim 存在中英文混输时候的不便,我之前一直使用 emacs 的键位映射,通过下面这两个插件的一种。\n让人遗憾的是,这两个插件,无论哪一个都不能带来完美的体验。在通常的编辑区还可以,但是在诸如搜索、替换等操作时让人很不爽。只能说有实现方式,但实现的比较曲线。\n正如在主题之旅中描述的那样,window 下的 emacs 也或多或少的有一些问题,小问题,但让人很强迫。出于此种原因,我当下只在 linux 中使用它。\n目前的文字编辑工作主力是 vscode ,它有一个非常靠谱的功能,就是你可以自定义键位映射。你完全可以在原版键位的基础上进行个性化的定制,使之变为你想要的样子。\n由于原版按键的兼容性最好,所以这里尽量避免了与原有按键的冲突,充分发挥了 alt 键的使用。它像是一个 emacs 和 vim 的混合体,😄 如下:\n上图对应的 keybindings.json 如下:\n// place your key bindings in this file to override the defaults [ { \u0026#34;key\u0026#34;: \u0026#34;alt+p\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopen\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+n\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.terminal.toggleterminal\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;terminal.active\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+x\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.showcommands\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+x\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopennavigatepreviousinfilepicker\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;infilespicker \u0026amp;\u0026amp; inquickopen\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+i\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.insertsnippet\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+b\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.toggleactivitybarvisibility\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+h\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorleft\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+l\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorright\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorup\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursordown\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+g\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorhome\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_1\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorend\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_1\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.commentline\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editortextfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_2\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;undo\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+z\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.togglezenmode\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+e\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.view.explorer\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;viewcontainer.workbench.view.explorer.enabled\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+g\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.view.scm\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;workbench.scm.active\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+f\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.replaceinfiles\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+s\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.replaceinfiles\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.triggersuggest\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editorhascompletionitemprovider \u0026amp;\u0026amp; textinputfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;togglesuggestiondetails\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.triggersuggest\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editorhascompletionitemprovider \u0026amp;\u0026amp; textinputfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;togglesuggestiondetails\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopenselectnext\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopenselectprevious\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;selectnextsuggestion\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetmultiplesuggestions \u0026amp;\u0026amp; suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;selectprevsuggestion\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetmultiplesuggestions \u0026amp;\u0026amp; suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, ] 体验非常之不错,如果,你也使用 vscode ,快些动手开始定制你自己的键位映射吧!🌟\n","date":"2022-07-05","permalink":"https://loveminimal.github.io/posts/editor-keybindings/","summary":"\u003cp\u003eVim 的键位不错,无论是移动光标还是进行编辑操作,但是如果是使用中文输入的话,就很尬 \u0026ndash; 中文状态下命令模式不能用,虽然通过一些方式可以使得在切换为非插入模式时自动切换为英文状态,但总是不那么尽如人意。\u003c/p\u003e\n\u003cp\u003eEmacs 的键位模式,就很适应中英文的混合输入,唯一的缺点就是有点费小手指。在 Emacs 中,我一般会启用 Evil 插件,如此,可以实现如果是只读文件和做一些删减工作的时候,就直接使用 Vim 的键位模式,而在插入模式下,就使用 Emacs 的键位模式,很不错,附上一份个人的 Evil 插件的配置 \u003ca href=\"https://github.com/loveminimal/emacs.d/blob/master/lisp/init-evil.el\"\u003einit-evil.el\u003c/a\u003e 。唯一不足的是,这种方式只有在 Emacs 中才是可用的,如果你使用其他编辑器时,就没有这份待遇了。不过,单纯的 Emacs 键位也足够好用了。\u003c/p\u003e","title":"键位映射那些事儿"},{"content":"🔔 以下内容主要摘录自 廖雪峰老师的博客,具体示例请跳到原文参考。\n= 好吧,廖老师配图一直可以的。\n注解是什么 注解是什么呢?它和注释有什么区别?\n注解定义后也是一种 class ,所有的注解都继承自 java.lang.annotaion.annotation 。 ok,我们先看看注解是做什么用的,再来了解如何定义和使用它。\n注解是放在 java 源码的类、方法、字段、参数前的一种特殊的“注释”(注意并不是注释),是一种用做标注的“元数据”。如,我们日常使用的 @override 就是一种注解。\n从 jvm 的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。\n其实这里的意思时,注解的作用取决于你是如何定义注解和使用注解的方法的。注解,就像是你在代码的某个位置放的一个钩子,至于如何使用它,则完全由你决定。\rjava 的注解可以分为三类:\n由编译器使用的注解; 由工具处理 .class 文件使用的注解; 在程序运行运行期能够读取的注解。 第一类,编译器使用的注解,如 @override 让编译器检查该方法是否正确地实现了覆写, @suppresswarnings 告诉编译器忽略此处代码产生的警告等。这类注解不会被编译进入 .class 文件,它们在编译后就被编译器扔掉了。(😿)\n第二类,由工具处理 .class 文件使用的注解,比如有些工具会在加载 class 的时候,对 class 做动态修改,以实现一些特殊的功能。这类注解会被编译进入 .class 文件,但在类加载结束后并不会存在于内存中(使命已经完成了,仅作用于 class)。它只被一些底层库使用,一般不必我们自己处理。\n第三类,在程序运行期能够读取的注解,这类注解在加载后一起存在于 jvm(内存中啦) 中(因为要在运行期读取啦 😏),这也是最常用的注解。\nok,了解了注解是什么,有什么用之后 ,让我们来看一下如何定义一个注解吧。\n定义注解 java 语言使用 @interface 语法来定义注解,它的格式如下:\npublic @interface report { int type() default 0;\t// default 后就是默认值 string level() default \u0026#34;info\u0026#34;; string value() default \u0026#34;\u0026#34;; } 注解定义后也是一种 class,所有的注解都继承自 java.lang.annotation.annotation 。\n不难看出,在定义一个注解时,还可以定义配置参数。需要注意的是,配置参数必须是常量,在定义注解时就已经确定了每个参数的值(可以有默认值)。\n*大部分注解会有一个名为 value 的配置参数,对此参数赋值,可以只写常量,相当于省略了 value 参数。\n有一些注解可以修饰其他注解 \u0026ndash; 元注解(meta annotation)。\n这里,我们只了解两个常用的元注解: @target 和 @retention 。\n@target\n最常用的元注解是 @target ,它用来定义 annotation 能够被应用于源码的哪些位置:\n类或接口: elementtype.type ; 字段: elementtype.field ; 方法: elementtype.method ; 构造方法: elementtype.constructor ; 方法参数: elementtype.parameter 。 实际上 @target 定义的 value 是 elementtype[] 数组,只有一个元素时,可以省略数组的写法。\n@retention\n@retention 定义了 annotation 的生命周期:\n仅编译器(译后即丢): retentionpolicy.source ; 仅 class (不入 jvm)文件: retentionpolicy.class ; 运行期(加载进 jvm,供程序读取): retentionpolicy.runtime 。 如果 @retention 不存在,则该 annotation 默认为 class,但其实通常我们自定义的 annotation 都是 runtime ,所以 务必要加上 @retention(retentionpolicy.runtime) 这个元注解。\nok,我们来总结一下定义 annotation 的步骤:\n用 @interface 定义注解; 添加参数、默认值(把最常用的参数定义为 value() ,方便使用时直接写常量); 用元注解配置注解。 一直走在偷懒的路上,永不停歇……\r如这样:\n@target(elementtype.type)\t// 3 @retention(retentionpolicy.runtime) public @interface report {\t// 1 int type() default 0;\t// 2 string level() default \u0026#34;info\u0026#34;; string value() default \u0026#34;\u0026#34;; } 处理注解 在日常生产环境中,我们基本上只需编写和使用 runtime 类型的注解,所以我们只讨论它。前面已经说过,该类型注解是加载进 jvm 供程序读取的,那么如何读取呢?反射 api!\n使用反射 api 读取 annotation:\nclass.getannotation(class) ; field.getannotation(class) ; method.getannotation(class) ; constructor.getannotation(class) ; 如:\n// 获取 person 定义的@report 注解: report report = person.class.getannotation(report.class); int type = report.type(); string level = report.level(); 如果读取时,annotation 不存在,则返回 null 。\n……\n注意,定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解,检查逻辑完全是我们自己编写的,jvm 不会自动给注解添加任何额外的逻辑。\n应用注解 这时我们概览一下注解在 web 开发中的常见应用形式。\n在 servlet 中的应用 在 javaee 平台上,处理 tcp 连接,解析 http 协议这些底层工作统统扔给现成的 web 服务器去做。我们使用 servlet api 编写自己的 servlet 来处理 http 请求,web 服务器实现 servlet api 接口,实现底层功能:\n┌───────────┐\r│my servlet │\r├───────────┤\r│servlet api│\r┌───────┐ http ├───────────┤\r│browser│\u0026lt;──────\u0026gt;│web server │\r└───────┘ └───────────┘ 注解在 servlet 中如何应用呢?它有什么作用呢?\n1. @webservlet\n我们知道,一个 servlet 总是继承自 httpservlet ,然后覆写 doget() 或 dopost() 方法。如何知道客户端的请求地址呢?早期的 servlet 使用 web.xml 文件来配置映射路径,现在我们使用注解 @webservlet 来实现。如下:\n// webservlet 注解表示这是一个 servlet ,并映射到地址 /hello: @webservlet(urlpatterns = \u0026#34;/hello\u0026#34;) public class helloservlet extends httpservlet { protected void doget(httpservletrequest req, httpservletresponse resp) throws servletexception, ioexception { // 设置响应类型: resp.setcontenttype(\u0026#34;text/html\u0026#34;); // 获取输出流: printwriter pw = resp.getwriter(); // 写入响应: pw.write(\u0026#34;\u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;\u0026#34;); // 最后不要忘记 flush 强制输出: pw.flush(); } } 浏览器发出的 http 请求总是由 web server 先接收,然后,根据 servlet 配置的映射,不同的路径转发到不同的 servlet 。\n2. @webfilter\n在一个复杂的 web 应用程序中,通常有很多 url 映射,对应的,也会有多个 servlet 来处理 url 。为了把一些公用逻辑从各个 servlet 中抽离出来,javaee 的 servlet 规范还提供了一种 filter 组件,即过滤器。它的作用是,在 http 请求到达 servlet 之前,可以被一个或多个 filter 预处理,类似打印日志、登录检查等逻辑,完全可以放到 filter 中。\n使用也很简单,来看一段示例:\n// 用 @webfilter 注解标注该 filter 需要过滤的 url ,这里的 /* 表示所有路径 @webfilter(urlpatterns = \u0026#34;/*\u0026#34;) public class encodingfilter implements filter { public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { system.out.println(\u0026#34;encodingfilter:dofilter\u0026#34;); request.setcharacterencoding(\u0026#34;utf-8\u0026#34;); response.setcharacterencoding(\u0026#34;utf-8\u0026#34;); chain.dofilter(request, response); } } 3. @weblistener\n除了 servlet 和 filter 外,javeee 的 servlet 规范还提供了第三种组件 - listener (监听器) 。\n有好几种 listener ,其中最常用的是 servletcontextlistener ,我们来编写一个实现该接口的类,如下:\n@weblistener public class applistener implements servletcontextlistener { // 在此初始化 webapp, 例如打开数据库连接池等: public void contextinitialized(servletcontextevent sce) { system.out.println(\u0026#34;webapp initialized.\u0026#34;); } // 在此清理 webapp, 例如关闭数据库连接池等: public void contextdestroyed(servletcontextevent sce) { system.out.println(\u0026#34;webapp destroyed.\u0026#34;); } } 任何标注为 @weblistener ,且实现了特定接口的类会被 web 服务器自动初始化。\n看,我们有钩子了 🥰\r一个 web 服务器可以运行一个或多个 webapp,对于每个 webapp ,web 服务器都会为其创建一个全局唯一的 servletcontext 实例,我们在上例中编写的两个回调方法实际上对应的就是 servletcontext 实例的创建和销毁。\n在 spring 中的应用 我们知道 spring 的核心就是提供了一个 ioc 窗口,它可以管理所有轻量级的 javabean 组件。起初,spring 也使用类似 xml 这样的配置文件,来描述 bean 的依赖关系,然后让容器来创建并装配 bean 。然而,这种方式虽然直观,写起来却很繁琐。\n1. @component 和 @autowired\n现在,我们可以使用 annotation 来注解,让 spring 自动扫描 bean 并组装它们。如:\n@component class mailservice { ... } @component public class userservice { @autowired mailservice mailservice; // ... } 如上,这个 @component 注解就相当于定义了一个 bean ,它有一个可选的名称,默认是 mailservice 、userservice (小写开头的类名)。\n@autowired 则相当于把指定类型的 bean 注入到指定的字段中。它不但可以写在 set() 方法上,还可以直接写在字段上,甚至可以写在构造方法中。\n2. @configuration 和 @componentscan\n要启动一个 spring 应用,我们需要编写一个类启动容器,如下:\n@configuration @componentscan public class appconfig { public static void main(string[] args) { applicationcontext context = new annotationconfigapplicationcontext(appconfig.class); userservice userservice = context.getbean(userservice.class); user user = userservice.login(\u0026#34;bob@example.com\u0026#34;, \u0026#34;password\u0026#34;); system.out.println(user.getname()); } } 其中, @configuration 表示 appconfig.class 是一个配置类,在创建 applicationcontext 时,使用的实现类是 annotationconfigapplicationcontext ,必须传入一个标注了 @configuration 的类名。\n@componentscan 则告诉容器,自动搜索当前类所在的包以及子包,把所有标注为 @component 的 bean 自动创建出来,并根据 @autowired 进行装配。\n看,ioc 容器其实啥都不知道,你需要用 annotation 告诉它,做什么、怎么做、在哪做。\r使用 @componentscan 很方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置类位于自定义的顶层包,其他 bean 按类别放入子包。\n思考一下,如何创建并配置一个第三方 bean 呢?它并不在当前可搜索的包中!\n3. @bean\n如果一个 bean 不在我们自己的 package 管理之内,例如 zoneid ,如何创建它?我们只需要在 @configuration 配置类中编写一个 java 方法(该方法使用 @bean 注解)创建并返回它。\n@configuration @componentscan public class appconfig { // 创建一个 bean: @bean zoneid createzoneid() { return zoneid.of(\u0026#34;z\u0026#34;); } } 4. @propertysource\n在开发应用程序时,经常需要读取配置文件,最常用的配置方法是以 key=value 的形式写在 *.properties 文件中。 spring 提供了一个简单的 @propertysource 来自动读取配置文件,只需要在配置类上再添加一个注解。\n@configuration @componentscan @propertysource(\u0026#34;app.properties\u0026#34;) // 表示读取 classpath 的 app.properties public class appconfig { @value(\u0026#34;${app.zone:z}\u0026#34;) string zoneid; @bean zoneid createzoneid() { return zoneid.of(zoneid); } } 如上,spring 容器看到 @propertysource(\u0026quot;app.properties\u0026quot;) 注解后,就会自动读取这个配置文件,然后,我们使用 @value 正常注入。\n5. @profile 和 @conditional\n创建某个 bean 时,spring 容器可以根据注解 @profile 来决定是否创建,除此之外,也可以根据 @conditional 来决定。\n其实,还有其他的一些,blablabla…… 因为,spring boot 提供了更好的,所以,我们在实际工作中并不怎么一些“老旧”的注解了。\r6. @aspect 和 @enableaspectjautoproxy\n当 spring 的 ioc 容器看到 @enableaspectjautoproxy 这个注解,就会自动查找带有 @aspect 的 bean,然后根据每个方法的 @before、@around 等注解把 aop 注入到特定的 bean 中。\n7. @enabletransactionmanagement 和 @transactional\nspring 提供了一个 platformtransactionmanager 来表示事务管理器,所有的事务都由它负责管理。使用编程的方式使用 spring 事务仍然比较繁琐,更好的方式是通过声明式事务来实现。\n使用声明式事务非常简单,除了在配置类中追加一个定义的 platformtransactionmanager 外,再添加一个 @enabletransactionmanagement 就可启用声明式事务。\n@configuration @componentscan @enabletransactionmanagement // 启用声明式 @propertysource(\u0026#34;jdbc.properties\u0026#34;) public class appconfig { // ... } 然后,对需要事务支持的方法,加一个 @transactional 注解。也可以直接加在 bean 的 class 处,它表示其所有 public 方法都具有事务支持。\nspring 对一个声明式事务的方法开启事务支持的原理,仍然是 aop 代理,取了通过自动创建 bean 的 proxy 实现。\n在 spring mvc 中的应用 我们知道,spring 提供的是一个 ioc 容器,所有的 bean 都在该容器中被初始化。而 servlet 容器由 javaee 服务器人提供(如 tomcat), servlet 容器对 spring 一无所知,它们之间依靠什么进行联系?又是以何种顺序初始化的呢? 详细答案请参考 如何关联 servlet 和 spring 。\n只需要在配置类上加上 @enablewebmvc 注意,就激活了 spring mvc 。\n// controller 使用@controller 标记而不是@component: @controller public class usercontroller { // 正常使用 @autowired 注入: @autowired userservice userservice; // 处理一个 url 映射: @getmapping(\u0026#34;/\u0026#34;) public modelandview index() { ... } ... } 这里,我们需要注意, controller 使用 @controller 标记,而不是 @component 。(很明显,前者针对 coontroller 做了一些增强)。\n好的,现在我们来回答一下开始的问题 - 如何关联 servlet 和 spring ?\nspring mvc 提供了一个 dispatcherservlet 类,我们只需在 web.xml 中配置它。\n\u0026lt;!doctype web-app public \u0026#34;-//sun microsystems, inc.//dtd web application 2.3//en\u0026#34; \u0026#34;http://java.sun.com/dtd/web-app_2_3.dtd\u0026#34; \u0026gt; \u0026lt;web-app\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.dispatcherservlet\u0026lt;/servlet-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextclass\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;org.springframework.web.context.support.annotationconfigwebapplicationcontext\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextconfiglocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;com.itranswarp.learnjava.appconfig\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;load-on-startup\u0026gt;0\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; servlet 容器会首先初始化 dispatcherservlet ,在 dispatcherservlet 启动时,根据配置类 appconfig 创建一个类型是 webapplicationcontext 的 ioc 容器,完成所有 bean 的初始化,并将该容器绑到 servletcontext 上。\n如此, dispatcherservlet 持有 ioc 容器,自然就可以从 ioc 容器中获取所有的 @controller 的 bean ,在接收到 http 请求后,根据 controller 方法配置的路径转发到指定方法,并根据返回的 modelandview 决定如何渲染页面。\n最后,在配置类 appconfig 中通过 main() 方法启动嵌入式 tomcat 即可。\n1. @controller\n该注解用来标识当前 bean 是一个 controller 。spring mvc 对 controller 没有固定的要求,也不需要实现特定的接口,只需要在 controller 类中,编写对应的方法处理相应的请求路径就可以了。\n2. @getmapping、@postmapping、@requestparam\n@controller public class usercontroller { // ... @postmapping(\u0026#34;/signin\u0026#34;) public modelandview dosignin( @requestparam(\u0026#34;email\u0026#34;) string email, @requestparam(\u0026#34;password\u0026#34;) string password, httpsession session) { // ... } } } 一个方法对应一个 http 请求路径,用 @getmapping 或 @postmapping 表示 get 或 post 请求。\n需要接收的 http 参数以 @requestparam() 标注。\n2. @restcontroller\n直接用 spring 的 controller 配合一大堆注解写 rest 太麻烦了,因此,spring 额外提供了一个 @restcontroller 注解,使用它注解 controller,每个方法自动变成 api 接口方法。\n@restcontroller @requestmapping(\u0026#34;/api\u0026#34;) public class apicontroller { @autowired userservice userservice; @getmapping(\u0026#34;/users\u0026#34;)\t// 实际为 /api/users,下同 public list\u0026lt;user\u0026gt; users() { return userservice.getusers(); } @getmapping(\u0026#34;/users/{id}\u0026#34;) public user user(@pathvariable(\u0026#34;id\u0026#34;) long id) { return userservice.getuserbyid(id); } @postmapping(\u0026#34;/signin\u0026#34;) public map\u0026lt;string, object\u0026gt; signin(@requestbody signinrequest signinrequest) { try { user user = userservice.signin(signinrequest.email, signinrequest.password); return map.of(\u0026#34;user\u0026#34;, user); } catch (exception e) { return map.of(\u0026#34;error\u0026#34;, \u0026#34;signin_failed\u0026#34;, \u0026#34;message\u0026#34;, e.getmessage()); } } public static class signinrequest { public string email; public string password; } } 如此,编写 rest 接口只需要定义 `@restcontroller` ,然后每个方法都是一个 api 接口,输入和输出只要能被 jackson 序列化或反序列化为 json 就没有问题。\n3. @crossorigin\n……\n在 spring boot 中的应用 spring boot 是什么?了解 她 。\n1. @springbootapplication\nspring boot 要求 main() 方法所在的启动类必须放到 package 下,命名不作要求。启动 spring boot 应用程序只需要一行代码加上一个注解 @springbootapplication 即可。\n@springbootapplication public class application { public static void main(string[] args) throws exception { springapplication.run(application.class, args); } } 还要啥自行车,直接飞起了 🚀\r2. @conditionalonxxx\nspring 本身提供了条件装配 @conditional,但是要自己编写比较复杂的 condition 来做判断,比较麻烦。spring boot 则为我们准备好了几个非常有用的条件,如:\n@conditionalonproperty :如果有指定的配置,条件生效; @conditionalonbean :如果有指定的 bean,条件生效; @conditionalonmissingbean :如果没有指定的 bean,条件生效; @conditionalonmissingclass :如果没有指定的 class,条件生效; @conditionalonwebapplication :在 web 环境中条件生效; @conditionalonexpression :根据表达式判断条件是否生效。 ……\n","date":"2022-07-05","permalink":"https://loveminimal.github.io/posts/annotation/","summary":"\u003cp\u003e🔔 以下内容主要摘录自 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255945389098144\"\u003e廖雪峰老师的博客\u003c/a\u003e,具体示例请跳到原文参考。\u003c/p\u003e","title":"注解"},{"content":"🔔 相关内容请参阅 https://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\nspring boot 是什么?它是一个基于 spring 的套件,它帮助我们预组装了一系列组件,以便以尽可能少的代码和配置来开发基于 spring 的 java 应用程序。\nspring boot makes it easy to create stand-alone, production-grade spring based applications that you can \u0026ldquo;just run\u0026rdquo;.\nwe take an opinionated view of the spring platform and third-party libraries so you can get started with minimum fuss. most spring boot applications need minimal spring configuration.\n\u0026ndash; spring boot\n在 spring 中,我们使用 xml 文件或是注解来告诉 spring 如果处理我们的组件。但是你必需给它明确的指令,它才知道如何正确的执行。随着组件数量的增多,配置项变得越来越长,以至于难以维护。所以,有必要让框架为我们做更多的事儿,比如解决依赖的依赖等问题,这就是为什么我们需要 spring boot。\nspring boot 是如何做到这些的呢?下面让我们来慢慢揭开这位“俏姑娘”的面纱 🥰。\n她长什么样? 我们不妨新建一个 springboot-hello 项目,创建标准的 maven 目录结构如下:\nspringboot-hello\r├── pom.xml\r├── src\r│ └── main\r│ ├── java\r│ └── resources\r│ ├── application.yml\t# spring boot 默认的配置文件\r│ ├── logback-spring.xml\t# spring boot 的 logback 配置文件名称\r│ ├── static\t# 静态文件目录\r│ └── templates\t# 模板文件目录\r└── target 我们主要的工作目录是 src/main/java/ ,我们来看一看源码目录结构:\nsrc/main/java\r└── com\r└── itranswarp\r└── learnjava\r├── application.java\t# !启动类\r├── entity\r│ └── user.java\r├── service\r│ └── userservice.java\r└── web\r└── usercontroller.java 注意:spring boot 要求 main() 方法所在的启动类必须放到根 package 下,命名不做要求(这里我们以 application.java 命名)。\n启动类,是一切的起点哦。\r她的魔法之源 启动类 我们来看一下上个章节中的 application.java 启动类,其内容如下:\n@springbootapplication public class application { public static void main(string[] args) throws exception { springapplication.run(application.class, args); } } 像是之前,我们使用的 spring 启动类,它包含了各种各样的注解,如 @configuration、 @componentscan 等。现在,我们却只需要 @springbootapplication,它是如何工作的?\n原来 @springbootapplication 这个注解实际上包含了:\n- @springbootconfiguration\r- @configuration\r- @enableautoconfiguration\r- @autoconfigurationpackage\r- @componentscan 所以,这一个注解相当于启动了自动配置和自动扫描。那么,她是 如何实现自动配置和自动扫描的呢?\n自动扫描和配置 我们再观察 pom.xml ,它的内容如下:\n\u0026lt;project ...\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-parent\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;2.3.0.release\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelversion\u0026gt;4.0.0\u0026lt;/modelversion\u0026gt; \u0026lt;groupid\u0026gt;com.itranswarp.learnjava\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;springboot-hello\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;1.0-snapshot\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;11\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;11\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;java.version\u0026gt;11\u0026lt;/java.version\u0026gt; \u0026lt;pebble.version\u0026gt;3.1.2\u0026lt;/pebble.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-web\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-jdbc\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 集成 pebble view --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;io.pebbletemplates\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;pebble-spring-boot-starter\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;${pebble.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- jdbc 驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.hsqldb\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;hsqldb\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 使用 spring boot 时,强烈推荐从 spring-boot-starter-parent 继承,它会引入 spring boot 的预置配置。\n按大佬说的做,别给自己找 trouble 😂\r紧接着,我们引入了各种 starter 依赖,并且没有指定版本号,因为引入的 \u0026lt;parent\u0026gt; 内已经指定了,只有我们自己引入的某些第三方 jar 包需要指定版本号。\nstarter 是个啥?\nspring boot 将日常企业应用研发中的各种场景都抽取出来,做成一个个的 starter (启动器),starter 中整合了该场景下各种可能用到的依赖,用户只需要在 maven 中引入 starter 依赖,springboot 就能自动扫描到要加载的信息并启动相应的配置。\nstarter 提供了大量的自动配置,让用户摆脱了处理各种依赖和配置项的困扰。所有这些 starter 都遵循着约定俗成的默认配置,并允许用户调整这些配置。\n并不是所有的 starter 都是由 spring boot 官方提供的,也有部分 starter 是第三方技术厂商提供的,如 druid-spring-boot-starter 和 mybatis-spring-boot-starter 等等。\n在启动时, spring boot 自动启动了嵌入式 tomcat ,如数据源、声明式事务、jdbctemplate 等 bean 都是由 spring boot 自动创建 \u0026ndash; 通过 autoconfiguration。\n在 starter 引入后,在启动时会自动扫描所有的 xxxautoconfiguration ,如,当我们引入 spring-boot-starter-jdbc 时,它自动扫描了如下:\ndatasourceautoconfiguration :自动创建一个 datasource ,其中配置项从 application.yml 的 spring.datasource 中读取; datasourcetransactionmanagerautoconfiguration :自动创建了一个基于 jdbc 的事务管理器; jdbctemplateautoconfiguration:自动创建了一个 jdbctemplate 。 因此,我们自动得到了一个 datasource、一个 datasourcetransactionmanager 和一个 jdbctemplate 。\nspring boot 大量使用 xxxautoconfiguration 来使得许多组件被自动化配置并创建。\n看, xxxautoconfiguration 就是 spring boot 的魔力之源,许多事情这些自动配置类都帮我们做了,谢谢它们全家。\n点石成金 i.e. 打包 spring boot 应用\nspring boot 自带了一个简单强大的 spring-boot-maven-plugin 插件用来打包,我们只需要在 pom.xml 中引入它即可。像这样:\n\u0026lt;project ...\u0026gt; ... \u0026lt;build\u0026gt; \u0026lt;!-- 默认的项目名为 `项目名+版本号`,不喜欢可以通过 `finalname` 自定义 --\u0026gt; \u0026lt;!-- \u0026lt;finalname\u0026gt;awesome-app\u0026lt;/finalname\u0026gt; --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactid\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 无需使用配置,spring boot 的这款插件会自动定位应用程序的入口 class ,我们只需要执行 mvn clean package 命令就可以打包了。\n之后 ,在打包后的 target 目录下,可以看到一个 springboot-exec-jar-1.0-snapshot.jar 包,它包含了项目运行所需的所有依赖,可以直接运行:\njava -jar springboot-exec-jar-1.0-snapshot.jar 这样,部署一个 spring boot 应用就非常简单,无需预安装任何服务器,只需要上传 jar 包即可。\n结语 看 spring boot 很简单,让我们的工程实现也变得简单。我们在这里只涉及了基本的初步介绍,想要了解更多,去设计一个项目吧,只有在具体的问题中才能更好地了解“她”。\n","date":"2022-07-04","permalink":"https://loveminimal.github.io/posts/spring-boot/","summary":"\u003cp\u003e🔔 相关内容请参阅 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\"\u003ehttps://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eSpring Boot 是什么?它是一个基于 \u003ca href=\"../spring/\"\u003eSpring\u003c/a\u003e 的套件,它帮助我们预组装了一系列组件,以便以尽可能少的代码和配置来开发基于 Spring 的 Java 应用程序。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSpring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can \u0026ldquo;just run\u0026rdquo;.\u003cbr\u003e\nWe take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.\u003cbr\u003e\n\u0026ndash; \u003ca href=\"https://spring.io/projects/spring-boot\"\u003eSpring Boot\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在 Spring 中,我们使用 \u003ccode\u003exml\u003c/code\u003e 文件或是注解来告诉 Spring 如果处理我们的组件。但是你必需给它明确的指令,它才知道如何正确的执行。随着组件数量的增多,配置项变得越来越长,以至于难以维护。所以,有必要让框架为我们做更多的事儿,比如解决依赖的依赖等问题,这就是为什么我们需要 Spring Boot。\u003c/p\u003e\n\u003cp\u003eSpring Boot 是如何做到这些的呢?下面让我们来慢慢揭开这位“俏姑娘”的面纱 🥰。\u003c/p\u003e","title":"spring boot"},{"content":"vim 是 vi 的升级版,一款功能强大、高度可定制的文本编辑器,它只有一个对手 \u0026ndash; emacs 。那么,它来自哪里?\nvim 123 说起 vi ,就不得不提起这们大佬 \u0026ndash; bill joy。除了 vi ,他还创建了 bsd 和 sun ,ok,大佬就是大佬。\n2003年9月9日,乔伊离开sun公司,sun发言人除了宣布joy辞职的消息外,不愿意发表其他评论。从一些迹象看来,他很关注机器人、纳米、基因工程等可能会改变全人类未来生存发展的技术;更加关注科技带来的道德问题: 如何不让科技成为一个国家、政府、集体、甚至个人做恶的工具?\n基础 vim 好学吗?好学,先来个速成 y 分钟学习 vim 。另外,附上关注的一挺有趣的视频 vim 使用技巧系列 。其实,一张图足矣,重要的是使用和练习。\n下图虽好,但可以先忽略,它是用来做查询用的,还是先了解下一些基础的概念。\r如果,以前没有接触过 vim/vi ,初次使用的时候大概率会很迷惑,这再正常不过了。\n安装 去官网下载对应版本安装即可,乌干达 forever …… ⁉️\n我们这里只针对 cli ,vim 谁用 gui ?那玩意没有灵魂\u0026hellip; 如果,你是在 windows 下(常用工作环境),建议使用 git-bash ,安装了 git 之后自带,内置已经安装好了 vim。\n模式 vim 编辑器基于 模式 这个概念。它有以下 4 种模式:\n命令模式:启动后处于这个模式,用于导航和操作命令; 插入模式:用于在文件中进行修改和编辑(同常规文本编辑器,如 notepad 😺); 可视模式:用于高亮文本并对它们进行操作(就是选择文字内容,进行复制粘贴之类的); ex 模式(底线命令模式):用于跳到底部的 : 提示行上输入命令。 在命令模式下,我们使用 hjkl 进行光标移动,为什么不直接使用 ←↓↑→ 方向键呢?原来,当前 bill joy 在开发 vi 编辑器时所使用的计算机是一个被称为 adm-3a 的终端,该终端附带的 hjkl 键本就和方向键同位一体,根本就没有独立的方向键。\n从历史中寻找答案,而不是主观臆断。\r配置 vim 是高度可定制的,而你只需要一个配置文件 .vimrc ,它位于家目录下面,gnu/linux 上就是 ~ ,windows 上为 /c/users/\u0026lt;用户名\u0026gt;。为了便于管理,我们一般会把 .vimrc 命名为 vimrc 放于文件夹 .vim 中,然后把该文件夹置于家目录。如下:\n.vim\r├── autoload\r│ └── plug.vim\r├── plugged\r│ ├── delimitmate\r│ ├── molokai\r│ ├── vim-airline\r│ ├── vim-markdown\r│ ├── vim-startify\r│ └── vim-surround\r├── readme.md\r└── vimrc 上边这个目录图是在 linux 下,由 tree 命令生成的,确切的说是 tree -l 2 ,其中 2 是指定的展开的目录层级。不是手画的,不是手画的,曾经手画过……\n我自己的 vim 配置是很简单的,因为平时使用 emacs 😅 。vim 有多种不同的插件管理工具,如 vundle、neobundle、vimplug 和 pathogen 等,我使用的是 vimplug ,小巧、稳定而强大。这里,我们推荐一个神奇的插件库 vimawesome ,速度快、视图怡人。\n最后,再放一张简单配置后的图片吧。\n预祝大家使用愉快,最后欢迎加入 vim 和 emacs 之间旷日持久的圣战,虽然没什么用 😏 。\n更多 着实是平时使用 vim 不是很多,就只说一些基本的使用理念和一些基础的配置情况。相对来说,这种理念性的东西,对所有编辑器都是通用的,只是实现方式有差异而已。\r所有类 unix 的系统中的工具,基本上都配备了一个不错的帮助系统,vim 自然也不例外。安装完成之后,可以在终端执行 vimtutor 打开一个内置的入门教程,默认是英文,当然你可以通过执行 vimtutor zh 打开对应的中文教程。\n*记住,你总是可以通过 vim --help 来获取更多的帮助。\n文件 编辑器是用来编辑文件的,自然最先接触的就是如何新建、打开、更新及保存文件。\n1.打开、新建\n不妨假设,我们在文件夹 ~/demo 中,想要打开该文件夹中的文件 a ,只需要执行 vim a 即可。也可以同时打开多个文件,如 vim a b c 同时打开文件 a、b、c 。(若有,则打开文件,若没有,则创建一个新的文件。)\n.\r├── a\r└── b\r0 directories, 2 files 同时打开多文件时,默认只会显示第一个文件的 buffer ,可以使用以下命令,多窗展示,如下:\nvim -o a b c\t# 窗口在一行\rvim -o a b c\t# 窗口在一列 2. 更新、保存\n打开文件后,进入插入模式,就可以编辑文本了,完成之后使用 :\n:w 保存当前文件; :wq 或是 \u0026lt;s-z s-z\u0026gt; 保存并退出。 我们使用 \u0026lt;c、s、m/a-\u0026gt;表示 ctrl、shift 和 meta/alt 键。\n3. 展示、切换\n当我们编辑多文件时,很多时候避免不了在文件之间进行切换,当然也免不了需要打印出当前所有打开文件的列表。该如何做呢?(其实,这些常规操作才是 vim 的魅力所在)\n在 vim 中,我们可以通过 :ls 来展示当前所有打开的文件列表,如下:\n其中 %a 表示当前激活的 buffer ,可以通过 :bn (buffer next)切换到下一个 buffer ,通过 :bp (buffer previous)切换到上一个 buffer 。\n文件过多了,也是繁琐,怎么办?\n可以通过 :e xxx 打开你想要的文件,但如果文件名 xxx 太长了,也很困扰。有一种更好的文件,如上图中所示,所有打开的文件都会被分配一个 id 号,我们可以通过 :buffer id 来进行直接切换到相应 buffer 。\n窗口 多窗口编辑是很常见的,窗口的新建、切换、关闭也是最常用到的操作之一。\n命令 描述 :sp 横向分屏(即窗口横向排列) :vsp 纵向分屏(即窗口纵向排列) :wincmd w 窗口切换 :close 关闭当前窗口 :only 关闭所有其他窗口 :q 退出当前窗口 当然,还有一些快捷键可以用,也很方便。这个请自行了解,因为我平时也不怎么用这些快捷键,也不大记的住。\n快捷键 关于快捷键,我们这里来单开一个章节来说明一下,之前也写过这方面的一些东西,如 vscode 插件 cve keymap 的开发记录 和 键位映射那些事儿 。这里,我们着重说明一下,在 vim 中如何方便的设计自己的键位映射。\n在 vim 中有个神奇的好东西,就是 \u0026lt;leader\u0026gt; ,强烈建议使用它来自定义你的键位。使用 emacs 的过程中,离不开 evil 插件的原因,很大程度上也是为了这个 \u0026lt;leader\u0026gt; 。\nvim 中在各种模式间进行切换的时候,会频繁使用到 \u0026lt;esc\u0026gt; 这个按键,它一般分布在键盘的左上角,很容易找到。也可以使用 \u0026lt;c-[\u0026gt; ,它与 \u0026lt;esc\u0026gt; 是等效的。很多朋友,也会把 ,, 映射成 \u0026lt;esc\u0026gt; ,看你爱好。\n我个人是使用 sharpkeys 软件(windows 系统),直接把键盘上的右 shift 键映射成了 esc ,如此全局通用。\n如果,你使用的是 gnu/linux ,可以方便地使用 xmodmap 进行全局的键位映射,更多可以阅读 如何使用 xmodmap 工具进行映射 。\nok,让我们转回 \u0026lt;leader\u0026gt; ,以下是我的一些键位映射配置:\n我把 \u0026lt;space\u0026gt; 空格键设置成了 \u0026lt;leader\u0026gt; 键,默认是 \\ 。如上图所示,我们用它实现了,上个章节中常用的文件及窗口操作。它有很多好处,方便记忆(毕竟是你自己设置的),还可以组合命令。如,我们使用 \u0026lt;space\u0026gt; jj 就可以先展示打开的文件列表,然后再供你输入想要跳转的 buffer 。\n更多配置,可以阅读我的 vim 配置 。\n结语 我自用的 vim ,配置的是很简单的,使用的功能也远不及 vim 所拥有功能的千分之一。如果感兴趣的话,不妨自己去亲自折腾一下。come on 🏃♂️ !\n","date":"2022-07-04","permalink":"https://loveminimal.github.io/posts/vim/","summary":"\u003cp\u003eVim 是 Vi 的升级版,一款功能强大、高度可定制的文本编辑器,它只有一个对手 \u0026ndash; Emacs 。那么,它来自哪里?\u003c/p\u003e","title":"vim"},{"content":" 哎,这让人费解的强迫症……\n博客没写几篇,博客系统倒是折腾了不少,哎,有点本末倒置了,希望这次是最后一次啦(不可能的),单开一篇碎碎念一下前前后后的那些事儿。\n“史前” 最初使用的“日志”系统就是 qq 空间了 😅(我是没有用过人人网的),那个时候一个与众不同的空间装扮是不少人心心念的。陆陆续续用了好多年,就是个情绪收集器,“为赋新词”强说的愁。随着 qq 的废弃,一时冲动就清空了所有内容,不时想起,难免有些遗憾,我的青春啊(冲动是魔鬼~~)。\n后来新浪博客可谓是红红火火,也不知道现在还有几个人用,反正我是连账号都不记得了。\n第三方博客平台 csdn 用的很少,界面太 low 了,广告一大堆,不过这个平台的 seo 做的真的不错,随便搜点什么,它的词条都排在很前面。感觉不少人直接把它当成备忘录用了,君不见,多少博客内容就只一个链接。\n博客园相对前者来说就清爽了很多,而且在一定程度上支持 diy ,可以添加不同的 css 模式,申请之后还可以加入 js 脚本。里面的内容也不错,至少不会是一两句话就自成一篇。\n简书最初的时候是个小可爱,简约美观,编辑内容的时候提供了两种模式,富文本和 markdown 模式,色调也柔和。只不过它的 markdown 解析引擎有点差劲,表现力不那么好。\n……\n第三方博客平台对博客内容的审核有时让人很困扰,ai 不够智能。像是 wordpress 之类的,生态是丰富,但也是折腾,而且编辑内容的体验让人不敢恭维。\n静态博客 markdown 是一种轻量级的标记语言,语法简单,书写清爽,表现力也还可以,用的范围挺广。许多静态博客生成器都只支持 markdown 的内容解析,如 jekyll 和 hexo。hugo 也支持 markdown,但它同时还支持 org mode 。\norg mode 是 emacs 编辑器中的一个插件,它拥有更加强大的表现力和直观性,配合 emacs 简直是纯文本编辑的无双利器。但是,成就它的同样也会束缚它,只有在 emacs 中才能发挥出它最大的威力。我知道有不少人都是为了 org mode 才开始接触并学习了 emacs 。\njekyll 在国内的网络环境下不是很友好,gem 不加镜像是真的慢(pass),虽然也没少折腾,但最初的时候真的是让人心累。hexo 就很好了,它基于 nodejs ,生态丰富完善,除了速度不如 hugo (但我们真的需要那么快的编译速度吗?),其他方面都让人很满意。使用了 hexo 挺长一段时间,甚至还写了一个主题 hexo-theme-zero。\n鉴于对 emacs 的一往情深,当时的博客内容都是用 org mode 写的,配合自制的一个 emacs 插件,把写好的 org mode 转成 markdown 输出到 hexo 进行解析生成。很繁琐是吧,听着就繁琐。\n后来,在阅读 org mode 文档的时候,发现了 ox-publish 这个功能,突然间感觉新世界的大门打开了。当时的念头是,如果可以借助这个功能直接从 org mode 生成页面,不就不需要繁琐的格式转换了嘛,当然后来我了解到已经有不少人已经对这个功能的增强和封装。在实现自定义封装的过程中,我参考和借鉴了不少别人的封装,具体实现 init-site.el,在这里主要实现了几个常用的功能,如文件保存的时候自动编译当前文件,配合一个 simple-httpd 实现当前文件实时预览,其他如删除当前文件对应的 html 文件,同时删除当前文件及其对应的 html 文件等等。配合第三方的 live-server ,封装并写一了一个简单的 bat 命令脚本,双击快速启动服务器,并开启浏览器本地开发。好吧,当时还做了一个视频 闲聊一种使用 org mode 生成静态博客的方式,尬 🤪 。\n如此,使用了有一段时间,也算是满足了个人对于博客的几点要求,如图片尺寸自定义大小等。它同样也有让人不爽的地方,就是慢,比 hexo 还慢,可怜的 elisp 。人性总是不安分的,有时候只是需要一个理由。在 hugo 经过了一段时间迭代之后 ,我又再次迁移到了它,先通览了一遍官方文档,看得我“飘飘然”。在经过了一周多的折腾之后,总算是有了个阶段性的稳定(主要自己给自己整得很疲惫,呵,处女座),hugo-theme-virgo 终于完工了,当然还制作了一个很随意的视频 virgo。\n这次应该能持续比较长的时间,大部分刚需功能在 hugo template 的帮助下都得以很快实现,最初的念头是好的,想着写一个朴素的主题就好。但你知道,这东西一旦开始,就停不来了,你会不断地想要重构它,增加一些功能。总之,就是事前“很会”,事后“很累”,自己折腾自己。目前终归是告一段落了,本地搜索,文章归档,同时具备朴素和炫酷、亮暗切换两种模式。hugo 原生支持 org mode,这非常好,我对它做了一些小小的增强,很爽。迫使我做出迁移的原因在于 windows 下的 emacs 问题多多,对中文字符的渲染尤其让人不爽。当下,对 vscode 进行了适当程度的“调教”,无论是书写 org mode 还是书写 markdown ,都到了差强人意的地步。\n= 呼,都不知道这些碎碎念有什么用,碎碎念能有什么用呢?\n好了,牢骚也发的差不多了,以后要减少无用的折腾,把时间和精力用在更有益的地方 ,比如深入学习、写博客等等。\n结语 事实上,这个章节是后来( 2023-04-17 16:00 )添加,使用 typora 进行编辑的。什么?又换编辑器了?是,也不是 😂。为什么呢?看了 一款 typora 主题 你就明白了。\n","date":"2022-07-02","permalink":"https://loveminimal.github.io/posts/a-theme-making-journey/","summary":"\u003cblockquote\u003e\n\u003cp\u003e哎,这让人费解的强迫症……\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e博客没写几篇,博客系统倒是折腾了不少,哎,有点本末倒置了,希望这次是最后一次啦(不可能的),单开一篇碎碎念一下前前后后的那些事儿。\u003c/p\u003e","title":"一场“疲惫”的主题制作之旅"},{"content":" 这里是 hugo 解析 org mode 内容的展示,如果你使用的是 markdown ,可以跳转 markdown 查看相关样式。\n*想要了解更多,可以去阅读 oxhugo 的文档 。\n以下 markdown 正文内容,摘自 markdown 测试文本 ,我们引用这部分内容,修改为 org mode 语法格式,并添加 org mode 独有的部分功能。\nthis post is originated from here and is used for testing markdown style. this post contains nearly every markdown usage. make sure all the markdown elements below show up correctly.\n\u0026lt;!– more –\u0026gt;\n—\nheaders ** h2 *** h3 **** h4 ***** h5 ****** h6 一篇文章中,通常一级标题只有一个,用来做为当前文章的标题,正文内容层级一般使用二级及其以下层级标题。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 是的,hugo 中内置的 org mode 解析引擎,默认会把正文中的最高层级置为 h2 ,事实上,这样做是正确的,markdown 没有这样做,反而有点奇怪。 \u0026lt;/div\u0026gt;\nenhance org mode 分析之后才发现 hugo 使用的 org mode 引擎是没有对 #+begin_html ... #+end_html 这种原样输出 html 内容的语法做处理的,它会把其内部元素整体做为一个字符串进行输出。好在这个问题很容易处理,我们对它做了一些增强,如下:\n$(\u0026#39;.html-block p\u0026#39;).each((idx, item) =\u0026gt; { item.innerhtml = `${item.innertext}` }) 思路很简单,我们操作转义后的文本,逆向解析为 html 。如此,我们便可以更加灵活的插入相应的 html 标签了,如我们插入图片。\n#+begin_html \u0026lt;img src=\u0026#34;/imgs/bg/cat.jpg\u0026#34; width=\u0026#34;400\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt; #+end_html \u0026lt;img src=\u0026#34;/imgs/bg/cat.jpg\u0026#34; width=\u0026#34;400\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 原样输出 html 真的很重要,也更加灵活。 \u0026lt;/div\u0026gt;\nemphasis emphasis, aka underlines, with _asterisks_ or _underscores_. emphasis, aka italics, with /asterisks/ or /underscores/. strong emphasis, aka bold, with *asterisks* or *underscores*. combined emphasis with *asterisks and _underscores_*. strikethrough uses two tildes. +scratch this.+ emphasis, aka underlines, with asterisks or underscores.\nemphasis, aka italics, with asterisks or underscores.\nstrong emphasis, aka bold, with asterisks or underscores.\ncombined emphasis with asterisks and underscores.\nstrikethrough uses two tildes. scratch this.\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 很容易看出,org mode 的表现能力及语法直观形象性,都要比 markdown 好不少,后者连下划线都没有 😿 \u0026lt;/div\u0026gt;\nlists 1. first ordered list item 2. another item - unordered sub-list. 1. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number 1. ordered sub-list - unordered list can use asterisks + or minuses - or pluses + paragraph in unordered list common paragraph with some text. and more text. first ordered list item another item unordered sub-list. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number ordered sub-list unordered list can use asterisks or minuses or pluses paragraph in unordered list common paragraph with some text. and more text.\ninline html \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt;\n\u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt; \u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt; \u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt;\n\u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does not work very well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt;\nlinks [[https://www.google.com][]] [[https://www.google.com][i\u0026#39;m an inline-style link]] https://www.google.com\ni\u0026#39;m an inline-style link\nimages [[https://hexo.io/icon/favicon-196x196.png][click to see png]] ;; 设置图片链接 [[https://hexo.io/icon/favicon-196x196.png]] ;; 直接展示图片 [[./imgs/org-mode.png]] ;; 本地图片 hover to see the title text:\ninline-style:\nclick to see png\n引用本地图片\n或者,你可以直接输出 \u0026lt;img\u0026gt; 标签,更方便。\ncode and syntax highlighting inline code has equal sign around it. 你可以使用 =inline-code= 来包裹行内代码,也可以使用 ~ ,它们转义后的效果是相同的。 source code block 在 org mode 中,使用 \u0026lt;s ,按 tab 展开,很容易得到一组供插入代码块的标签,如下:\n#+begin_src js var s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); #+end_src var s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); 或者,\ns = \u0026#34;python syntax highlighting\u0026#34; print s 有时候,我们不指定语言侦测,也可以直接使用 \u0026lt;e 去展开,如下:\n#+begin_example no language indicated, so no syntax highlighting. but let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. #+end_example no language indicated, so no syntax highlighting. but let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. 如果,你在 emacs 中使用的话,可以直接用上述方式展开。如果,你使用 vscode 的话,这里有一个很不错的插件,如下:\ntables | name | phone | sub1 | sub2 | total | |-------------+--------+------+------+-------| | maple | 134... | 89 | 98 | | | wizard | 152... | 78 | 65 | | | hello world | 123... | 76 | 87 | | | hehe | 157... | 87 | 78 | | name phone sub1 sub2 total maple 134… 89 98 wizard 152… 78 65 hello world 123… 76 87 hehe 157… 87 78 blockquotes #+begin_quote blockquotes are very handy in email to emulate reply text. this line is part of the same quote. #+end_quote blockquotes are very handy in email to emulate reply text.\nthis line is part of the same quote.\nhorizontal rule three or more…\n----- 至少 5 个横折线。\noh essay 在编辑博文的时候,经常想插入一些突然闪现出来的内容,或是于行文无关的吐槽等。为了更好地与正文内容做区分,做了一个定制模式,以 html 格式插入。\n#+begin_html \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt; #+end_html \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt;\nbilibili videos \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; youbube ? no no no ! bilibili ? yes ! \u0026lt;/div\u0026gt;\n\u0026lt;iframe src=\u0026#34;//player.bilibili.com/player.html?aid=338348299\u0026amp;bvid=bv1fr4y1u7gf\u0026amp;cid=489898794\u0026amp;page=1\u0026#34; scrolling=\u0026#34;no\u0026#34; border=\u0026#34;0\u0026#34; frameborder=\u0026#34;no\u0026#34; framespacing=\u0026#34;0\u0026#34; allowfullscreen=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;/iframe\u0026gt;\nbilibili 真的很不错,体验上比 youtube 要好,内容类型嘛,则没有后者丰富,这个没办法,生态大环境决定的。\n## youtube videos ```markdown \u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau \u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34; alt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt; pure markdown version: [![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) \u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau \u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34; alt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt; pure markdown version: [![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) ``` ","date":"2022-06-29","permalink":"https://loveminimal.github.io/posts/org-mode/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这里是 Hugo 解析 Org Mode 内容的展示,如果你使用的是 Markdown ,可以跳转 \u003ca href=\"/posts/markdown\"\u003eMarkdown\u003c/a\u003e 查看相关样式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\n*想要了解更多,可以去阅读 \u003ca href=\"https://ox-hugo.scripter.co/\"\u003eoxhugo\u003c/a\u003e 的文档 。\u003c/p\u003e","title":"org mode"},{"content":" 这里是 hugo 解析 markdown 内容的展示,如果你使用的是 org mode ,可以跳转 org mode 查看相关样式。\n以下 markdown 正文内容,摘自 markdown 测试文本 ,并添加、修改了一些章节。\nthis post is originated from here and is used for testing markdown style. this post contains nearly every markdown usage. make sure all the markdown elements below show up correctly.\nheaders # h1 ## h2 ### h3 #### h4 ##### h5 ###### h6 alternatively, for h1 and h2, an underline-ish style: # alt-h1 ## alt-h2 h1 h2 h3 h4 h5 h6 alternatively, for h1 and h2, an underline-ish style:\nalt-h1 alt-h2 emphasis emphasis, aka italics, with _asterisks_ or _underscores_. strong emphasis, aka bold, with **asterisks** or **underscores**. combined emphasis with **asterisks and _underscores_**. strikethrough uses two tildes. ~~scratch this.~~ emphasis, aka italics, with asterisks or underscores.\nstrong emphasis, aka bold, with asterisks or underscores.\ncombined emphasis with asterisks and underscores.\nstrikethrough uses two tildes. scratch this.\nlists 1. first ordered list item 2. another item - unordered sub-list. 1. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number 1. ordered sub-list 1. and another item. you can have properly indented paragraphs within list items. notice the blank line above, and the leading spaces (at least one, but we\u0026#39;ll use three here to also align the raw markdown). to have a line break without a paragraph, you will need to use two trailing spaces. note that this line is separate, but within the same paragraph. (this is contrary to the typical gfm line break behaviour, where trailing spaces are not required.) - unordered list can use asterisks * or minuses - or pluses * paragraph in unordered list for example like this. common paragraph with some text. and more text. first ordered list item another item unordered sub-list. actual numbers don\u0026rsquo;t matter, just that it\u0026rsquo;s a number\nordered sub-list\nand another item.\nyou can have properly indented paragraphs within list items. notice the blank line above, and the leading spaces (at least one, but we\u0026rsquo;ll use three here to also align the raw markdown).\nto have a line break without a paragraph, you will need to use two trailing spaces.\nnote that this line is separate, but within the same paragraph.\n(this is contrary to the typical gfm line break behaviour, where trailing spaces are not required.)\nunordered list can use asterisks or minuses or pluses paragraph in unordered list\nfor example like this.\ncommon paragraph with some text. and more text.\ninline html \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt; to reboot your computer, press ctrl+alt+del.\n\u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt; \u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt; definition list\ris something people use sometimes.\r\u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt;\r\u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt;\rlinks [i\u0026#39;m an inline-style link](https://www.google.com) [i\u0026#39;m an inline-style link with title](https://www.google.com \u0026#34;google\u0026#39;s homepage\u0026#34;) [i\u0026#39;m a reference-style link][arbitrary case-insensitive reference text] [i\u0026#39;m a relative reference to a repository file](../blob/master/license) [you can use numbers for reference-style link definitions][1] or leave it empty and use the [link text itself] some text to show that the reference links can follow later. [arbitrary case-insensitive reference text]: https://hexo.io [1]: https://hexo.io/docs/ [link text itself]: https://hexo.io/api/ i\u0026rsquo;m an inline-style link\ni\u0026rsquo;m an inline-style link with title\ni\u0026rsquo;m a reference-style link\ni\u0026rsquo;m a relative reference to a repository file\nyou can use numbers for reference-style link definitions\nor leave it empty and use the link text itself\nsome text to show that the reference links can follow later.\nimages hover to see the title text: inline-style: ![alt text](https://hexo.io/icon/favicon-196x196.png \u0026#39;logo title text 1\u0026#39;) reference-style: ![alt text][logo] [logo]: https://hexo.io/icon/favicon-196x196.png \u0026#39;logo title text 2\u0026#39; hover to see the title text:\ninline-style:\nreference-style: code and syntax highlighting inline code has back-ticks around it.\nvar s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); s = \u0026#34;python syntax highlighting\u0026#34; print s no language indicated, so no syntax highlighting.\rbut let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. tables | | ascii | html | |------------------|---------------------------------|-------------------------------| | single backticks | `\u0026#39;isn\u0026#39;t this fun?\u0026#39;` | \u0026#39;isn\u0026#39;t this fun?\u0026#39; | | quotes | `\u0026#34;isn\u0026#39;t this fun?\u0026#34;` | \u0026#34;isn\u0026#39;t this fun?\u0026#34; | | dashes | `-- is en-dash, --- is em-dash` | -- is en-dash, --- is em-dash | ascii html single backticks 'isn't this fun?' \u0026lsquo;isn\u0026rsquo;t this fun?\u0026rsquo; quotes \u0026quot;isn't this fun?\u0026quot; \u0026ldquo;isn\u0026rsquo;t this fun?\u0026rdquo; dashes -- is en-dash, --- is em-dash \u0026ndash; is en-dash, \u0026mdash; is em-dash colons can be used to align columns.\n| tables | are | cool | |---------------|:-------------:|-----:| | col 3 is | right-aligned | | | col 2 is | centered | | | zebra stripes | are neat | | tables are cool col 3 is right-aligned col 2 is centered zebra stripes are neat the outer pipes (|) are optional, and you don\u0026rsquo;t need to make the raw markdown line up prettily. you can also use inline markdown.\n| markdown | less | pretty | |----------|-----------|------------| | _still_ | `renders` | **nicely** | | 1 | 2 | 3 | markdown less pretty still renders nicely 1 2 3 you can find more information about latex mathematical expressions here.\nblockquotes blockquotes are very handy in email to emulate reply text. this line is part of the same quote.\nquote break.\nthis is a very long line that will still be quoted properly when it wraps. oh boy let\u0026rsquo;s keep writing to make sure this is long enough to actually wrap for everyone. oh, you can put markdown into a blockquote.\nhorizontal rule three or more\u0026hellip;\n--- hyphens --- asterisks --- underscores hyphens\nasterisks\nunderscores\nline breaks here\u0026#39;s a line for us to start with. this line is separated from the one above by two newlines, so it will be a _separate paragraph_. this line is also a separate paragraph, but... this line is only separated by a single newline, so it\u0026#39;s a separate line in the _same paragraph_. here\u0026rsquo;s a line for us to start with.\nthis line is separated from the one above by two newlines, so it will be a separate paragraph.\nthis line is also a separate paragraph, but\u0026hellip; this line is only separated by a single newline, so it\u0026rsquo;s a separate line in the same paragraph.\nthis is a regular paragraph. \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;foo\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; this is another regular paragraph. this is a regular paragraph.\nfoo\rthis is another regular paragraph.\noh essay 在编辑博文的时候,经常想插入一些突然闪现出来的内容,或是于行文无关的吐槽等。为了更好地与正文内容做区分,做了一个定制模式,以 html 格式插入。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt; 这就是我们插入的随笔喽…… blablablabla……\rbilibili videos youbube ? no no no ! bilibili ? yes ! bilibili 真的很不错,体验上比 youtube 要好,内容类型嘛,则没有后者丰富,这个没办法,生态大环境决定的。\n## youtube videos\r```markdown\r\u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau\r\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34;\ralt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt;\rpure markdown version:\r[![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau)\r\u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau\r\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34;\ralt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt;\rpure markdown version:\r[![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) ","date":"2022-06-29","permalink":"https://loveminimal.github.io/posts/markdown/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这里是 Hugo 解析 Markdown 内容的展示,如果你使用的是 Org Mode ,可以跳转 \u003ca href=\"/posts/org-mode\"\u003eOrg Mode\u003c/a\u003e 查看相关样式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以下 markdown 正文内容,摘自 \u003ca href=\"https://github.com/hexojs/hexo-theme-unit-test/edit/master/source/_posts/markdown.md\"\u003eMarkdown 测试文本\u003c/a\u003e ,并添加、修改了一些章节。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis post is originated from \u003ca href=\"https://gist.github.com/apackeer/4159268\"\u003ehere\u003c/a\u003e and is used for testing markdown style. This post contains nearly every markdown usage. Make sure all the markdown elements below show up correctly.\u003c/p\u003e","title":"markdown"},{"content":" 🚨 后又陆陆续续加了些辅助功能,可以点击下载最新完整的search.js 源码(另存后使用编辑器打开就不中文乱码了)。\n更新日志 2022-07-13 11:02 修改了站点内容结构解析方式,以解决随着文章数量增长导致搜索性能下降的问题 简介 近来稍闲,实现了一个 hugo 本地搜索的小功能,分享一下 🍧 。\n这个功能写了两遍,第一次不知道怎么着就在一个临时分支(切换回主分支会自动消失的那种 😠 )中开发了,然后就没什么然后了…… 感觉是 ide 的锅,对,就是它的,不是也要是!\r让我们先来看一下,它可以做什么吧:\n内容实时搜索; 搜索内容摘要显示; 搜索词高亮。 都是一些搜索时常用的功能。稍后,我们来看一下上述功能的一些细节,以及开发过程中的一些糗事 😄 。你也可以,先在这里体验一下它的大概使用效果 search 。\n使用 如何实时获取站点的所有内容呢?这里有两个方面,一就是获取站点的所有页面内容,二是实时获取。\n搜索模板页 一个大概的思路,就是创建一个模板页,如 _search.html 文件,利用 hugo 模板本身的变量(如 .site)来获取站点所有的页面内容。\n\u0026lt;div class=\u0026#34;container-search\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;data\u0026#34; style=\u0026#34;display: none;\u0026#34;\u0026gt; \u0026lt;!-- 遍历所有的站点页面 --\u0026gt; {{ range where .site.pages \u0026#34;kind\u0026#34; \u0026#34;section\u0026#34; }} {{ if ne .title \u0026#34;secrets\u0026#34; }} [{{ range .pages }} {{- dict \u0026#34;title\u0026#34; (lower .title) \u0026#34;permalink\u0026#34; .permalink \u0026#34;date\u0026#34; (.date | time.format \u0026#34;2006-01-02\u0026#34;) \u0026#34;summary\u0026#34; .summary \u0026#34;content\u0026#34; (lower .plain) | jsonify -}},{{ end }}] {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;!-- 搜索框 --\u0026gt; \u0026lt;div id=\u0026#34;search\u0026#34;\u0026gt; \u0026lt;!-- 🔎 --\u0026gt; \u0026lt;span class=\u0026#34;sc-icon\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;/imgs/icons/search.svg\u0026#34; width=\u0026#34;48\u0026#34;\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;span id=\u0026#34;sc-clear\u0026#34; onclick=\u0026#34;clearinputval()\u0026#34;\u0026gt;✖\u0026lt;/span\u0026gt; \u0026lt;input id=\u0026#34;sc-input\u0026#34; oninput=\u0026#34;search()\u0026#34; type=\u0026#34;text\u0026#34; placeholder=\u0026#34;here search search...\u0026#34; /\u0026gt; \u0026lt;div id=\u0026#34;sc-res\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 加载所需搜索脚本 --\u0026gt; \u0026lt;script src=\u0026#34;/js/search.js\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 当然,你可以进行按需进行一些修改,过滤掉一些,你不想被搜索到的页面。\n下面,我们来看一下核心的 js 搜索脚本 search.js (这里我们放在了 static/js/search.js)。其中的注释,是开发过程中帮助记忆和理清思路和一些碎碎念,不要在意。😅\n解析站点页面内容 let data = document.queryselector(\u0026#39;#data\u0026#39;).innertext.trim(); data = data.slice(0, data.length - 2) + \u0026#39;]\u0026#39;; let map = json.parse(data); 我们使用 hugo 模板提供的相关功能,组织站点内容映射,以文本形式放在元素 #data 中,后反序列化以得到当前站点所有页面内容的一个集合 map 。\n如何搜索 我们的核心就是搜索函数 search() 。在 map 的生成过程中,我们对信息串的 content 做了一些处理,如将所有字符转化为小写。在 search() 中,我们也对搜索词 scval 进行同样的处理,以实现不区分大小写的内容搜索。\n在这之前,我们定义了另一个辅助函数,用来返回搜索词 scval 在对应页面中出现的所有索引位置 _arrindex 。\nfunction scanstr(content, str) { // content 页面内容信息串 let index = content.indexof(str); // str 出现的位置 let num = 0; // str 出现的次数 let arrindex = []; // str 出现的位置集合 while(index !== -1) { arrindex.push(index); num += 1; index = content.indexof(str, index + 1); // 从 str 出现的位置下一位置继续 } return arrindex; } 有它 scanstr 我们就可以方便的知道搜索词都出现在了哪里,以方便后续的内容摘要截取及高亮。\n内容摘要截取\n通过 scanstr ,我们得到了搜索词在页面内容出现的所有位置,我们默认截取每个位置前后 100 个字符长度(后续我称之后 截取半径)的内容进行罗列展示(可以自定义长度)。这里,我们做了一些小小的优化操作,当后续搜索词的索引位置与当前搜索词的索引位置之差仍小于截取半径的时候,将不再对该位置前后内容进行截取(因为它已经包含在了之前的截取内容中),以避免大量重复性内容的展示。\n具体逻辑,还是直接看代码吧,其实不需要了解,因为它并没有什么太大的通用性,都是对字符串的蹂躏和被蹂躏。😿\nlet scinput = document.queryselector(\u0026#39;#sc-input\u0026#39;); let scres = document.queryselector(\u0026#39;#sc-res\u0026#39;) let scval = \u0026#39;\u0026#39;; scinput.focus(); // 自动聚集搜索框 function search() { let post = \u0026#39;\u0026#39;; scval = scinput.value.trim().tolowercase(); map.foreach(item =\u0026gt; { if (!scval) return; if (item.content.indexof(scval) \u0026gt; -1) { let _arrindex = scanstr(item.content, scval); let strres = \u0026#39;\u0026#39;; let _radius = 100; // 搜索字符前后截取的长度 let _strstyle0 = \u0026#39;\u0026lt;span style=\u0026#34;background: yellow;\u0026#34;\u0026gt;\u0026#39; let _strstyle1 = \u0026#39;\u0026lt;/span\u0026gt;\u0026#39; let _strseparator = \u0026#39;\u0026lt;hr\u0026gt;\u0026#39; // 统计与首个与其前邻的索引(不妨称为基准索引)差值小于截取半径的索引位小于截取半径的索引的个数 // 如果差值小于半径,则表示当前索引内容已包括在概要范围内,则不重复截取,且 // 下次比较的索引应继续与基准索引比较,直到大于截取半径, _count重新置 为 0; let _count = 0; for (let i = 0, len = _arrindex.length; i \u0026lt; len; i++) { let _idxitem = _arrindex[i]; let _relidx = i; // 如果相邻搜索词出现的距离小于截取半径,那么忽略后一个出现位置的内容截取(因为已经包含在内了) if (_relidx \u0026gt; 0 \u0026amp;\u0026amp; (_arrindex[_relidx] - _arrindex[_relidx - 1 - _count] \u0026lt; _radius)) { _count += 1; continue; } _count = 0; // 概要显示 // _startidx, _endidx 会在超限时自动归限(默认,无需处理) strres += _strseparator; let _startidx = _idxitem - _radius + (_relidx + 1) * _strseparator.length; let _endidx = _idxitem + _radius + (_relidx + 1) * _strseparator.length; strres += item.content.substring(_startidx, _endidx); } // 进一步对搜索摘要进行处理,高亮搜索词 let _arrstrres = scanstr(strres, scval); // console.log(_arrstrres) for (let i = 0, len = _arrstrres.length; i \u0026lt; len; i++) { let _idxitem = _arrstrres[i]; let _realidx = i; strres = strres.slice(0, (_idxitem + _realidx * (_strstyle0.length + _strstyle1.length))) + // 当前索引位置之前的部分 _strstyle0 + scval + _strstyle1 + strres.slice(_idxitem + scval.length + _realidx * (_strstyle0.length + _strstyle1.length)); // 之后的部分 } post += ` \u0026lt;div class=\u0026#34;item\u0026#34; \u0026gt; \u0026lt;a href=\u0026#34;${item.permalink}\u0026#34;\u0026gt; \u0026lt;span\u0026gt;📄\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;date\u0026#34;\u0026gt;${item.date}\u0026lt;/span\u0026gt; \u0026lt;span\u0026gt;${item.title}\u0026lt;/span\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;div\u0026gt;${strres}\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ` } }) let res = `\u0026lt;div class=\u0026#34;list\u0026#34;\u0026gt;${post}\u0026lt;/div\u0026gt;`; scres.innerhtml = res; 高亮显示\n在遍历获取了所有的搜索摘要 strres 后,我们需要对其进行进一步的处理,以实现搜索词高亮显示,如下:\n同样,这也是和字符串长度之间的征战,没什么太大意思。\n最后 只需要把 search.js 放在 static/js/ 目录下,或者其他你喜欢的路径,但要保证 _search.html 可以正常引用。再使用 hugo 创建一个对应的 search.md 页面,用来启用 _search.html 模板即可。比如我把 _search.html 模板放在了 single.html 模板中,并且设置只有 /search 路径才加载这部分内容。\n{{ $issearch := eq .title \u0026#34;search\u0026#34;}}\r{{ if $issearch }}\r{{- partial \u0026#34;partials/_search.html\u0026#34; . -}}\r{{ end }} 上述代码块中的代码,是完整可用的,复制粘贴即可。\n","date":"2022-06-23","permalink":"https://loveminimal.github.io/posts/hugo-local-search/","summary":"\u003cblockquote\u003e\n\u003cp\u003e🚨 后又陆陆续续加了些辅助功能,可以点击下载最新完整的\u003ca href=\"https://ovirgo.com/js/search.js\"\u003esearch.js\u003c/a\u003e 源码(另存后使用编辑器打开就不中文乱码了)。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"一种简单的 hugo 本地搜索实现"},{"content":" go 之 1、2、3、4、5…… 不得不说,golang 的语法真的挺怪异的 😢\ntodo 快速简览","date":"2022-06-08","permalink":"https://loveminimal.github.io/posts/go/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGo 之 1、2、3、4、5…… 不得不说,Golang 的语法真的挺怪异的 😢\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cimg src=\"/posts/go/imgs/2.webp\" width=\"320\" /\u003e","title":"go"},{"content":" 很强大,也让人很头大…… 这只是一个精简的摘要,相信我,即使总结的再全面,一段时间之后,也需要借助参考手册,才能正确的使用它。\n简介 正则表达式是什么?一个表达式,它定义了一种规则,用来匹配(符合规则的)字符串。\n正则表达式本身也是用字符串表示的,如何表示呢?\n必须搭配例子一起食用,味道才好 😸 ……\r简单匹配 _1.精确匹配\n在正则表达式中,如果直接给出字符,就是精确匹配。其他,还有:\n元字符 说明 \\d 匹配一个数字 \\w 匹配一个字母或数字 \\s 匹配一个空白符 . 匹配任意字符 当然,还有其他的,用的时候再查好了,反正是记不住的。\r看一些示例:\n'00\\d' 可以匹配 '007' ,但无法匹配 +'00a'+ ; '\\d\\d\\d' 可以匹配 '010' ; '\\w\\w\\d' 可以匹配 'py3' ; 'py.' 可以匹配 'pyc'、 'pyo'、 'py!' 等等。 _2.匹配变长字符\n元字符 说明 * 匹配任意个字符 (0 或 0+) + 匹配至少一个字符 (1 或 1+) ? 匹配 0 或 1 个字符 {n} 匹配 n 个字符 {m,n} 匹配 (m 到 n) 个字符 看一些示例:\n\\d{3} 表示匹配 3 个数字,如 '010' ; \\s+ 表示至少有一个空格,可以匹配 ' '、 ' ' 等; \\d{3,8} 表示匹配 3-8 个数字,如 '123'、 '12345'、 '12345678' 等。 如果要匹配 010-12345 这样的号码呢?由于 - 是特殊字符 ,在正则表达式中,要用 \\ 转义,故上述的答案是 \\d{3}\\-\\d{3-8} 。\n如果,还不够,继续……\n_3. 更精确的匹配\n要做更精确的匹配,可以用 [] 表示范围,如 [0-9a-za-z\\_] 可匹配一个数字、字母或者下划线。 其他,有:\n元字符 说明 (a|b) 匹配 a 或 b ^ 表示行的开头 $ 表示行的结束 再来看一些示例:\n[a-za-z\\_][0-9a-za-z\\_]* 可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串(看,这不就是 python 的合法变量嘛); (p|p)ython 可以匹配 'python' 或者 'python' 。 进阶 _4. 分组\n除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用 () 表示的就是要提取的分组(group)。\n比如, ^(\\d{3})-(\\d{3-8})$ 分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码。\nm = re.match(r\u0026#39;^(\\d{3})-(\\d{3-8})$\u0026#39;, \u0026#39;010-12345\u0026#39;) m.group(0) # \u0026#39;010-12345\u0026#39; m.group(1) # \u0026#39;010\u0026#39; m.group(2) # \u0026#39;12345\u0026#39; # group(0) 永远是与整个正则表达式相匹配的字符串 # group(1)、group(2)... 表示第 1、2 个子串 _5.贪婪匹配\n需要注意的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符,加个 ? 可以切换成非贪婪匹配。\n比如,我们要匹配出数字后面的 0 :\nre.match(r\u0026#39;^(\\d+)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() # → (\u0026#39;102300\u0026#39;, \u0026#39;\u0026#39;) 由于 \\d+ 默认采用贪婪匹配,直接把后面的 0 全部匹配了,结果 0* 只能匹配空字符串了。\n必须让 \\d+ 采用非贪婪匹配(也就是尽可能少的匹配),才能把后面的 0 匹配出来,如下:\nre.match(r\u0026#39;^(\\d+?)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() # → (\u0026#39;1023\u0026#39;, \u0026#39;00\u0026#39;) 结语 以上内容主要来源于 廖雪峰老师的博客中关于正则表达式的章节 ,很短但很精致。如果,你想进一步练习的话,推荐:\nhttps://regexr.com/ https://regexr-cn.com/ (中文站) ","date":"2022-05-24","permalink":"https://loveminimal.github.io/posts/regexp/","summary":"\u003cblockquote\u003e\n\u003cp\u003e很强大,也让人很头大…… 这只是一个精简的摘要,相信我,即使总结的再全面,一段时间之后,也需要借助参考手册,才能正确的使用它。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"正则表达式"},{"content":"\rgtd 分为 =横向管理 和 =纵向管理 两个方面。\n= 横向管理,其实就是事物在时空层面上的大搜集;纵向管理,则是事物在某一时空节点上的纵深。 横向收集,纵向执行! 纵向管理偏重在横向管理的行动阶段!*\n横向管理 横向管理有五个阶段:收集、分析、组织管理、检查(反馈)、行动。\n1. 收集:填充工作篮 物理空间; 精神空间。 关键在于百分之百地捕获一切未尽事宜,以最快的速度收集下来,放入工作篮。\n保持极简主义生活方式, =断、舍、离 ,可以从根本上大大缩短收集时间。\n= 好的生活方式,可以让一个人更好地沉浸在某一种生活状态,以更利于个体的成长和发展。*\n= 在纯粹的精神世界中,一切起于 念! 取舍之间的平衡点在于有序、足够、简约。极简主义中的核心虽然是断、舍、离,但在执行过程中,“度”很重要,而在保持过程中,“有序”很关键!*\n2. 分析:清空工作篮 清空工作篮的本质在于确定每一项工作的内容和实质,判断其是否需要采取行动。\n_不需要采取行动:\n垃圾 → 抛进垃圾桶; 孵化器 → 未来某一天可能会做; 参考资料 → 归档保存。 _需要采取行动的:\n小于 2 分钟的立即处理; 大于 2 分钟的: 委托他人: 等待; 日程表(有时间要求的)。 自己执行(在管理阶段列清单): 日程表(有时间要求的); 下一步行动清单。 项目: 制定项目计划。 注意,这一步中,并不需要采取任何实际的行动,关键在于合理分类,分类的时候应遵循以下原则:\n自上而下; 逐条处理; 机会均等。 = 一切都是为了尽可能快地解除‘焦虑’状态,快速地为未尽事宜分类,归入清单。*\n3. 管理 对分类后的条目进行制定 _具体的行动方案 :\n垃圾 → 删除; 未来可期 → 孵化器; 参考资料 → 归档保存供查阅; 小于 2 分钟 → 立即处理; 下一步行动 → =必须是一目了然的具体行动 ; 日程表 → 只允许放置有具体时间要求的任务; 项目 → 分解为具体的行动、安排日程。 注意, _日程表 是十分神圣的,它和 _下一步行动 共同构成了每日管理的核心。\n四象限工作法是个不错的标准:\n= 在“分析”阶段,我们要做的是快速判断清空收集篮,在“管理”阶段,则需要认真分析,依照不同类型事物的标准进行切实的处理。*\n= 这一步无疑是 重中之重 ,只有科学的、具体化的、可执行的行动,才是解决问题的关键,才是改造世界的直接力量。*\n4. 检查 新的情况不断产生,每天的工作安排也必须时常回顾并做出相应调整,以保证 gtd 的尽收一切和全面管理。\n如何检查呢?\n\u0026hellip;\n每一天:\n以 _日程表 作为开始,处理好日程表中紧要的任务; 之后,查阅 _下一步行动 清单和 _孵化器 。 =注意,定期(如每周)更新清单是至关重要的。\n= 养成习惯,让享受美好的事情像呼吸一样简单和自然!早九晚九必检一次,中间实时更新即可。\n5. 行动 选择最佳方案,如何决策?\n_在某一时刻, 4 个模式:\n(地)环境; (时)有多少时间; (人)有多少精力; (事)重要性。 = 天时,地利,人和,重要性!*\n_评估每日工作, 3 种模式:\n(预定)处理事先安排好的工作; (突发)处理随时冒出来的事件; (成长)定义你自己的工作。 = 一切都在计划之中,留些弹性给突发事件。多管齐下,生活才会更有趣。还是要每天都小总一下才好!\n_回顾工作的 6 个标准:\n目前的行动; 当前的工作(亟待处理的事项,短期成效); 责任范围(工作、生活、个人成长); 1~2 年的目标(各个领域的成就); 3~5 年的展望(着眼全局,纵观趋势); 整个生活的全景(终极的人生意义)。 = 所有的评估和回顾,终极目标都是了解‘真我’!预期的有无和意义,便在于这个求‘真’的过程之中,它是自然而然的,是会自己冒出来,而你也注定要解决的问题。*\n= 这几个标准特别适合,周、月、季、年、大周期汇总,看,一切总结都是建立在实践之上的,空想大道是没有什么意义的。\n= 其中第一类决策模式是发生在具体行动中的,后两者则都归属于检查反馈(即融于日常的生活习惯之中),可见规律性、周期性的“反馈”的重要性!反馈、检查 ✔️\n高效能人士的七个习惯系统地讲解这个体系:\n= 组织管理和行动这两步是很关键的,好的方案为行动的实施提供了便利,事半功倍,这两个环节和纵向管理又是密切相关的,不可分离的。*\n纵向管理 需要更严格地控制某一项工作,确保行动方案切实可行时,便是纵向管理的用武之地。\n= 就个人而言,横向管理使用的比较多,基本已经融入日常生活当中。纵向管理,就用的少了些,可能还是有时候太浮躁了,要静下心来,多思、多想,思而后行,行而有得。\n纵向管理也有五个阶段:目的/原则、前景/结果、集思广益、组织协调、下一步行动。\n= 看!纵向管理,是正式的组织管理和行动,它是融入其中的!*\n1. 目的/原则 目的是什么?是否清晰而具体?\n界定成功(要实现什么); 集结资源(客观环境); 激发动机(主观能动); 阐明重点(主要矛盾); 拓宽选择(可能); 澄清原则(本心)。 = 过多的解释和条目,往往让人很烦!总结来说,就是结合自身内外条件,想要阶段性的取得一种什么结果,其他一切都是围绕这个中心的!为什么要‘界定’呢?要预演!脑海中的预演可以更好、更早地发现行动过程中可能出现的问题,防患于未然,不仅大大地节省了可能要花费的并无意义用处的时间和精力,对行动的激励所产生的正面效应也是极大的。“之所以打醋,不就是为了这点饺子!”*\n= 凡事计划,很少有能一蹴而就的,多是分阶段性的,对于成功的界定就很重要,正所谓,心向高远,目视脚下,路途长而艰、寂而孤,就更需要阶段性的鼓舞与激励!由内而外,反求诸己,利用客观环境,发挥主观能动,突破经验本本,分析主次轻重,扩展思维,明晰本心,澄清并坚守原则。*\n= 这个过程是繁琐的,却是非常重要的!认真思考要达成的目标,分析客观条件、主观现实,实事求是地分析矛盾及事物可能的走向,明晰本心真我。*\n2. 前景/结果 聚焦; 阐明结果。 = 能吃到的果子,才能真正解渴!\n量子吸引力法则:\n量子力学理论中重要的一点就是振动频率相同的东西,会互相吸引而且引起共鸣。我们的意念、思想、情绪具有可感知的能量,而我们的脑电波不断产生振动频率,只要有振动,就会影响其他同样在振动的事物。我们的大脑就是这个世界上最强的“磁铁”,我们的起心动念,无时不在向宇宙发出信号,和你的脑电波振动频率相同的东西,会统统被你吸引过来。你生活中的一切,都是你自己吸引来的。佛陀在 2500 多年前所说的“唯心所见,唯识所变”,便是这个哲理。\n= 意识的更新。*\n3. 集思广益 头脑风暴,启动思维,打破常规的思考模式,基本原则:\n不判断,不质疑,不评估,不批判; 追求数量,不求质量; 把分析组织工作置于次要的位置,放在下个阶段处理。 = 在头脑中搜集所有的可能性,不放过任何蛛丝马迹。不唯经验、教条,实事求是!突然更深地认识到,以上种种,在《毛泽东选集》中都有讲到…… 教员威武!*\n4. 组织协调 _组织管理的要素:\n明确意义重大的事件; 排序(构成因素、先后顺序、重要程度); 必要程度的详述。 5. 下一步行动 如果一项工作具有可操作性,其下一步行动方案就必须予以落实,必须有切实可行的具体行动。\n= 实践,是实现一切计划的根本途径!*\n= 综上来看,纵向管理就是界定成功(明晰目标)、预演前景结果、想尽可能多的方案,从中取优协调,进行形成一个科学合理的、可明确执行的、有意义的行动方案,以保证具体执行过程顺利,达到预期目标!*\n","date":"2022-05-01","permalink":"https://loveminimal.github.io/posts/get-things-done/","summary":"\u003cimg src=\"/posts/get-things-done/imgs/gtd-1.jpg\" width=\"800\" style=\"border-radius: 6px;\" /\u003e\r\n\u003cp\u003eGTD 分为 \u003cem\u003e=横向管理\u003c/em\u003e 和 \u003cem\u003e=纵向管理\u003c/em\u003e 两个方面。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 横向管理,其实就是事物在时空层面上的大搜集;纵向管理,则是事物在某一时空节点上的纵深。 \u003cstrong\u003e横向收集,纵向执行!\u003c/strong\u003e 纵向管理偏重在横向管理的行动阶段!*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"gtd 管理系统"},{"content":" = 好的吧,我承认,对于 c 语言,我有一种莫名其妙的偏爱!是因为可以直接操作内存吗?或许是!可能吧,计算机有什么神奇的东西呢?最终就是围绕那几个概念在来回打转!\n编译器 1 = 计算机说:“无论你怎么写,怎么编,怎么译,最终都要爷能看得懂才行!😈”\n什么是编译器 可执行程序(executable program) 的内部是一系列计算机指令和数据的集合,它们是二进制形式的,cpu 可以直接识别,但对于程序员来说几乎不具备可读性。\n比如,在屏幕上输出“vip 会员”,c 语言的写法为: puts(\u0026quot;vip 会员\u0026quot;); ,但其二进制写法如下:\n直接使用二进制指令编程对程序员来说简直是噩梦!\n于是,编程语言就诞生了。比如,c 语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于 cpu 来说,c 语言代码就是天书(cpu 只认识几百个二进制形式的指令)!\n= 💻:“不好意思,爷只认二进制!”\n这就需要一个工具,将 c 语言代码转换成 cpu 能够识别的二进制指令(即可执行程序),这个工具是一个特殊的软件,叫做 编译器(compiler) 。\n用来保存代码文件叫做 源文件 ,它就一个纯文本文件,内部并没有特殊格式(其后缀仅仅是为了表明文件中保存的是某种语言的代码,易于程序员区分和编译器识别)。\n= 就一纯文本文件 📝\n在实际开发中,程序员将代码分门别类地放到多个源文件中。\nide(集成开发工具) 会为每一个程序都创建一个专门的目录,将用到的所有文件都集中到这个目录下进行管理。 不同的程序对应不同的项目类型(i.e. 工程类型) ,不同的工程类型本质上是对 ide 各个参数的不同设置 。当然,我们也可以创建一个空白的工程类型,然后自己去设置各种参数(不过一般没有会这样做)。\n源代码要经过编译(compile)和链接(link)两个过程才能变成可执行文件。\n编译器一次只能编译一个源文件(如果当前程序包含了多个源文件,那么就需要编译多次),编译器每次编译的结果是产生一个中间文件(不是最终的可执行文件,但已经非常接近可执行文件了,它们都是二进制格式,内部结构也非常相似)。\n将当前程序的所有中间文件以及系统库组合在一起,才能形成最终的可执行文件,这个组合的过程就叫做 链接(link) ,完成链接功能的软件叫做链接器(linker)。\n不管有多少个源文件(哪怕只有一个),都必须经过编译和链接两个过程才能生成可执行文件。(为什么呢?因为你至少还需要和系统库组合。)\n综上可以发现,一个完整的编程过程是:\n编写源文件(保证代码语法正确,否则编译不通过); 预处理(processing); 编译(compile 将源文件转换为目标文件); 汇编(assembly); 链接(linking 将目标文件和系统库组合在一起,转换为可执行文件); 运行(可以检验代码的正确性)。 默认情况下, gcc 指令会直接将源代码转变为可执行代码(2-5 四个过程),且不会保留各个阶段产生的中间文件。\ngcc 是什么 gcc 编译器是 linux 系统下最常用的 c/c++ 编译器,大部分 linux 发行版中都会默认安装。\n早期的 gcc 全拼为 gnu c compiler ,最初定位确实只用于编译 c 语言。经过不断迭代扩展,gcc 现在还可以处理 c++、go、ojbect-c 等多种编译语言编写的程序,故其全称被重新定义为 gnu compiler collection,即 gnu 编译器套件。\n👉 更多 gcc 和 clang / llvm 的区别\n可以通过 gcc --help 查看其常用指令选项如下:\n--version display compiler version information.\r-std=\u0026lt;standard\u0026gt; assume that the input sources are for \u0026lt;standard\u0026gt;.\r-e preprocess only; do not compile, assemble or link.\r-s compile only; do not assemble or link.\r-c compile and assemble, but do not link.\r-o \u0026lt;file\u0026gt; place the output into \u0026lt;file\u0026gt;.\r-pie create a dynamically linked position independent\rexecutable.\r-shared create a shared library.\r-x \u0026lt;language\u0026gt; specify the language of the following input files.\rpermissible languages include: c c++ assembler none\r\u0026#39;none\u0026#39; means revert to the default behavior of\rguessing the language based on the file\u0026#39;s extension. 前面说过 gcc 是支持编译多种编程语言的,可以通过 -x 选项指定要编译的语言类型,如 gcc -xc++ xxx 表示以编译 c++ 代码的方式编译 xxx 文件。\n使用 gcc 编译器编译 c 或者 c++程序,必须经历 4 个过程: 预处理 → 编译 → 汇编 → 链接 (通常 gcc/g++ 支持该过程的自动化)。\ng++ 是什么?可以认为 g++ →(等价于) gcc -xc++ -lstdc++ -shared-libgcc (因为 gcc 不会自动引入 c++ 相关的库,必须手动引入)。\n*用于手动指定链接环节中程序可以调用的库文件,如 -lstdc++ ,不建议 -l 和 library 之间有空格。\n默认情况下, gcc 指令会一气呵成将源代码历经这 4 个过程转变为可执行代码,且不会保留各个阶段产生的中间文件。\n如果我们想查看这 4 个阶段各自产生的中间文件,该怎么办呢?最简单直接的方式就是对源代码进行“分步编译”。即控制 gcc 编译器逐步对源代码进行预处理、编译、汇编及链接操作。\ngcc/g++ 指令选项 功能 -e 预处理指定的源文件,不进行编译 -s 编译指定的源文件,不进行汇编 -c 编译、汇编指定的源文件,但是不进行链接 -o 指定生成文件的文件名 -llibrary 用于手动指定链接环节中程序可以调用的库文件,如 -lstdc++ ,不建议 -l 和 library 之间有空格 -ansi 对于 c 语言程序来说,其等价于 -std=c90;对于 c++ 程序来说,其等价于 -std=c++98 -std= 手动指令编程语言所遵循的标准 \u0026gt; gcc 常用的编译选项\n假如我们编写了一个 source.c 的源程序,如下:\n#include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026#34;c program.\\n\u0026#34;); return 0; } 1. 预处理 - 生成预处理文件 *.i\n通过为 gcc 指令添加 -e 选项,即可控制 gcc 编译器仅对源代码做预处理操作。默认情况下, gcc -e 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件,因此,该指令往往会和 -o 选项连胜,将结果导入到指定文件中。\njack@jk:~/cemo/cporj$ gcc -e source.c -o source.i jack@jk:~/cemo/cporj$ ls source.c source.i jack@jk:~/cemo/cporj$ cat source.i # 1 \u0026#34;source.c\u0026#34; # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; # 1 \u0026#34;\u0026lt;command-line\u0026gt;\u0026#34; # 31 \u0026#34;\u0026lt;command-line\u0026gt;\u0026#34; # 1 \u0026#34;/usr/include/stdc-predef.h\u0026#34; 1 3 4 # .... linux 系统中通常用 \u0026quot;.i\u0026quot; 作为 c 语言程序预处理后所得文件的后缀名。显然, source.i 中的内容不是那么容易看懂的,好在可以为 gcc 指令再添加一个 -c 选项,来阻止 gcc 删除源文件和头文件中的注释,即 gcc -e -c source.c -o source.i 。\n2. 编译 - 生成汇编文件 *.s\njack@jk:~/cemo/cporj$ gcc -s source.i jack@jk:~/cemo/cporj$ ls source.c source.i source.s jack@jk:~/cemo/cporj$ cat source.s .file \u0026#34;source.c\u0026#34; .text .section .rodata .lc0: .string \u0026#34;c program.\u0026#34; .... 通过执行 gcc -s 指令,生成了个名为 source.s 的文件,这就是经过编译的汇编代码文件。(默认情况下,编译操作会自行新建一个文件名和指定文件相同、后缀名为 .s 的文件,并将编译的结果保存在该文件中。)\n同样,想要提高文件内汇编代码的可读性,可以借助 -fverbose-asm 选项,gcc 编译器会自动为汇编代码添加必要的注释,即 gcc -s source.i -fverbose-asm 。\n3. 汇编 - 生成目标文件 *.o\njack@jk:~/cemo/cporj$ gcc -c source.s jack@jk:~/cemo/cporj$ ls source.c source.i source.o source.s 上面生成的 source.o 文件就是目标文件,其本质为二进制文件(但尚未经过链接操作,所以无法直接运行)。\n4. 链接 - 生成可执行文件\ngcc 通过 -o 选项来指定输出文件,缺省默认输出 a.out ,其语法格式如下:\ngcc [-e|-s|-c] [infile] [-o outfile] _通过 -l 选项手动添加链接库\n链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。\n而且,链接器也必须将程序中所用到的所有 c 标准库函数加入其中。对于链接器来说,链接库不过是一个具在许多目标文件的集合,它们放在一个文件中以方便处理。\n标准库的大部分函数通常放在文件 libc.a 中(文件名后缀 .a 代表 achieve 读取),或者放在共享的动态链接文件 libc.so 中(文件名后缀 .so 代表 share object 共享对象)。\n如,通过 gcc source.c -o source.out -lm 链接数学库 libm.a ,前缀 lib 和后缀 .a 是标准的, m 是基本名称。(gcc 会在 -l 选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀)\n_gcc 使用静态链接库和动态链接库\n库文件的产生,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 c 语言中的 printf() 输出函数为例,程序中只需引入 \u0026lt;stdio.h\u0026gt; 头文件,即可调用 printf() 函数。\n调用库文件为什么还要牵扯到头文件呢?\n头文件和库文件并不是一码事,它们最大的区别在于:\n头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分; 所有的库文件都提供有相应的头文件作为调用它的接口,即库文件是无法直接使用的,只能通过头文件间接调用。 头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。\n= 其实,就是一种封装。\n事实上,库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。要知道,虽然库文件用于程序的链接阶段,但编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中采用静态链接方式实现链接操作的库文件,称为 静态链接库 ;采用动态链接方式实现链接操作的库文件,称为 动态链接库 。\n它们有什么不同呢?\n_静态链接库 实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,gcc 编译器就会 将该模板代码直接复制到程序文件的适当位置 ,最终生成可执行文件。\n好处是生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强),坏处如果程序文件中多次调用库中的同一个模块,则该模块代码会被复制多次(冗余),生成的可执行文件体积更大(与使用动态链接库生成的可执行文件相比)。\n在 linux 发行版中,静态链接库文件的后缀通常用 .a 表示;在 windows 系统中,静态链接库文件的后缀名为 .lib 。\n_动态链接库 ,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,gcc 编译器不会直接将该功能模块的代码拷贝到文件中,而是 将功能模块的位置信息记录到文件中,直接生成可执行文件。\n显然,这样生成的可执行文件是无法独立运行的。\n采用动态链接库生成的可执行文件运行时,gcc 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。\n在 linux 系统中,动态链接库的后缀名通常用 .so 表示;在 windows 系统中,动态链接库的后缀名为 .dll 。\n值得一提的是,gcc 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,gcc 编译器才会选择相应的静态链接库。如果两种都没有(或者 gcc 编译器未找到),则链接失败。\ngdb 调试器 gnu symbolic debugger,简称「gdb 调试器」,是 linux 平台下最常用的一款程序调试器。\n要知道,哪怕是开发经验再丰富的程序员,编写的程序也避免不了出错。程序中的错误主要分为 2 类,分别为:\n语法错误(可以借助编译器解决); 逻辑错误(只能程序员\u0026lt;自己或借助调试工具\u0026gt;调试解决)。 调试是每个程序员必须掌握的基本技能,没有选择的余地!\n所谓调试(debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。\ngdb 就是 linux 下使用最多的一款调试器,也有 windows 的移植版。\n总的来说,借助 gdb 调试器可以实现以下几个功能:\n程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量; 可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(如当前变量的值,函数执行的结果等),即支持断点调试; 程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。 默认情况下,程序不会进行调试模式,代码会瞬间从开关执行到末尾。要想观察程序运行的内部细节,可以借助 gdb 调试器在程序中的某个地方设置断点(breakpoint),如此当程序执行到这个地方时就会停下来。\ngdb 调试器支持在程序中打 3 种断点:\n普通断点(break):指定打断点的具体位置; 观察断点(watch):可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行; 捕捉断点(catch):监控程序中某一事件的发生。 ……\n= 具体调试细节,略过……\n数据 关于数据 数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。\n诸如数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别。我们需要用数据类型用来说明数据的类型,确定了 数据的解释方式 ,让计算机和程序员不会产生歧义。另外在 c 语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了 数据的长度 。\n= 反正就是一个二进制串,解释权在编译器,反正乱解释肯定出问题。\n数据是放在内存中的,在内存中存取数据要明确三件事情:数据存储在哪里、数据的长度以及数据的处理方式。\n_打印输出各种类型的数据\nputs (output string) 只能用来输出字符串; printf (print format)格式化输出,功能强大,不仅可以输出字符串,还可以输出整数、小数、单个字符等,并且输出格式可以自定义。\n数据类型 1. 整数类型 short、 int、 long\nshort(短整型)、int(整型)、long(长整型) 是 c 语言中常见的整数类型。c 语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:\n2 ≤ short ≤ int ≤ long 其中,int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。\n获取某个数据类型的长度可以使用 sizeof 操作符,如下:\n#include \u0026lt;stdio.h\u0026gt; int main() { short a = 10; int b = 100; int short_length = sizeof a; int int_length = sizeof(b); int long_length = sizeof(long); int char_length = sizeof(char); printf(\u0026#34;short=%d, int=%d, long=%d, char=%d\\n\u0026#34;, short_length, int_length, long_length, char_length); return 0; } // 在 64 位 linux 下的输出 → short=2, int=4, long=8, char=1 *注意, sizeof 是 c 语言中的操作符,不是函数(故可不带括号)。\n2. 浮点类型 float、 double\n一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。\n……\n3. 字符类型 char\n字符类型由单引号 ' ' 包围,字符串由双引号 \u0026quot; \u0026quot; 包围。\n计算机在存储字符时并不是真的要存储字符实体,而是存储该字符在字符集中的编号(也可以叫编码值)。对于 char 类型来说,它实际上存储的就是字符的 ascii 码。\n可以说,是 ascii 码表将英文字符和整数关联了起来。\n无论在哪个字符集中,字符编号都是一个整数;从这个角度考虑,字符类型和整数类型本质上没有什么区别。\n= 在 c 语言中,并没有单独定义字符串类型,字符串实际上是使用空字符 \\0 结尾的一维字符数组,如 char site[7] = {'r', 'u', 'n', 'o', 'o', 'b', '\\0'}; 。\n4. 构造类型 - 数组\n数组(array)就是一些列具有相同类型的数据的集合,这些数据在内存中依次挨着存放,彼此之间没有缝隙。\n数组的定义方式:\ndatatype arrayname[length];\r// - datatype 为数据类型\r// - arrayname 为数组名称\r// - length 为数组长度\r// 数组中每个元素都有一个索引(下标),从 0 开始,使用元素时指明下标即可:\rarrayname[index]\r// - index 为下标 数组的初始化:\n// 当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 0 int arr[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; // 给全部元素赋值,那么在定义数组时可以不给出数组长度 int arr[] = {1, 2, 3, 4, 5}; // 等价于 int arr[5] = {1, 2, 3, 4, 5}; 如何获取数组的长度呢? 通过 sizeof arr / sizeof arr[0] 。\n5. 字符数组\n在 c 语言中,没有专门的字符串变量,没有 string 类型,通常就用一个字符数组来存放一个字符串。\n在 c 语言中,字符串总是以 '\\0' 作为结尾,所以 '\\0' 也被称为字符串结束标志,或者字符串结束符。\n'\\0' 是 ascii 码表中的第 0 个字符,英文称为 nul,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在 c 语言中唯一的作用就是作为字符串结束标志。\n由 \u0026quot; \u0026quot; 包围的字符串会自动在末尾添加 '\\0' 。例如,\u0026ldquo;abc123\u0026rdquo; 从表面看起来只包含了 6 个字符,其实不然,c 语言会在最后隐式地添加一个 '\\0',这个过程是在后台默默地进行的,所以我们感受不到。\nchar str[6] = \u0026#34;abc123\u0026#34;; // ✘ char str[7] = \u0026#34;abc123\u0026#34;; // ✔,别忘记 \u0026#39;\\0\u0026#39;,使用 \u0026#34;xyzbnm..\u0026#34; 赋值会自动在末尾添加 \u0026#39;\\0\u0026#39; char str[7] = { \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;\\0\u0026#39; }; // 不嫌烦,你也可以这样 *另外,要注意字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。如:\nchar str[7]; str = \u0026#34;abc123\u0026#34;; // ✘ //正确 ✔ str[0] = \u0026#39;a\u0026#39;; str[1] = \u0026#39;b\u0026#39;; str[2] = \u0026#39;c\u0026#39;; str[3] = \u0026#39;1\u0026#39;; str[4] = \u0026#39;2\u0026#39;; str[5] = \u0026#39;3\u0026#39;; 所谓 _字符串长度 ,就是字符串包含了多少个字符( 不包含最后的结束字符 '\\0' ),如 \u0026quot;abc\u0026quot; 的长度是 3 ,而不是 4。(注意和定义时的数组长度做区分)\n在 c 语言中,我们使用 string.h 头文件中的 strlen() 函数来求字符串的长度,它的用法为:\nlength strlen(strname);\r// - length 字符串长度,一个整数\r// - strname 字符串的名字或字符数组的名字 6. 指针\n所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。\n7. 构造类型 - 结构体\nc 语言结构体(struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。\n结构体定义形式为:\nstruct tag { member-list } variable-list;\r// - tag 结构体名(标签)\r// - member-list 结构体成员(列表)\r// - variable-list 该结构体定义的类型变量 *注意,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。\n#include \u0026lt;stdio.h\u0026gt; int main() { struct student { char *name; // 姓名 int age; // 年龄 float score; // 成绩 } stu1, stu2; // 可以通过 =.= 获取和操作单个结构体成员 stu1.name = \u0026#34;tom\u0026#34;; str1.age = 18; stu1.score = 99.5; printf(\u0026#34;%s 的分数是: %d\\n\u0026#34;, stu1.name, stu1.score); return 0; } 8. 构造类型 - 共用体(联合体)\n联合体定义形式为:\nunion tag { member-list } variable-list;\r// - tag 联合体名(标签)\r// - member-list 联合体成员(列表)\r// - variable-list 该联合体定义的类型变量 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。\n输入输出 i/o 在控制台程序中,输出一般是指将数据(包括数字、字符等)显示在屏幕上,输入一般是指获取用户在键盘上输入的数据。\n输出 在 c 语言中,有三个函数可以用来在显示器上输出数据:\nputs() - 只能输出字符串,并且输出结束后自动换行; putchar() - 只能输出单个字符; printf() - 格式化输出各种类型的数据。 printf() 格式控制符的完整形式如下:\n%[flag][width][.precision]type\r// - type 表示输出类型\r// - width 表示最小输出宽度(不足时左边以空格补齐)\r// - .precision 表示输出精度,也就是小数的位数;\r// 也可以用于整数和字符串,但是功能相反:\r// - 用于整数时,表示最小输出宽度(不足时左边以 0 补齐)\r// - 用于字符串时,表示最大输出宽度\r// - flag 是标志字符:\r// - - 表示左对齐(默认右对齐)\r// - + 表示输出正负号(默认只有负数输出符号)\r// - 空格 输出正时以空格,输出负时以负号\r// - # 输出八进制、十六进制前缀;对于小数表示强迫输出小数点 关于 printf() 不能立即输出的问题\n从本质上讲, printf() 执行结束后数据并没有直接输出到显示器上,而是放入了缓冲区,直到遇见换行符 \\n 才将缓冲区中的数据输出到显示器上。\n输入输出的“命门”就在于缓存。\n输入 在 c 语言中,有多个函数可以从键盘获得用户输入:\ngets() - 获取一行数据,并作为字符串处理(可以读取含有空格的字符串); getchar() - 用于输入单个字符(就是 scanf(\u0026quot;%c\u0026quot;, c) 的简化版); scanf() - 可以格式化输入多种类型的数据。 对于 scanf() 输入数据的格式要和控制字符串的格式保持一致。\n从本质上讲,从键盘输入的数据并没直接交给 scanf() ,而是放了缓冲区中,直到我们按下回车键, scanf() 才到缓冲区中读取数据。\n文件操作 c 语言具有操作文件的能力,比如打开文件、读取/追加/插入/删除数据、关闭文件、删除文件等。\nc 语言中的文件是什么 文件是数据源的一种,最主要的作用是保存数据。\n在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。如:\n文件 硬件设备 stdin 标准输入文件,一般指键盘; scanf()、getchar() 等函数默认从 stdin 获取输入 stdout 标准输出文件,一般指显示器; printf()、putchar() 等函数默认从 stdout 输出数据 stderr 标准错误文件,一般指显示器; perror() 等函数默认向 stderr 输出数据 stdprn 标准打印文件,一般指打印机 \u0026gt; 常见硬件设备所对应的文件\n此处不去探讨硬件设备是如何被映射成文件的,只需记住,在 c 语言中硬件设备可以看成文件,有些输入输出函数不需要你指明到底读写哪个文件,系统已经为它们设置了默认的文件(当然你也可以更改,如让 printf 向磁盘上的文件输出数据)。\n操作文件的正确流程为:打开文件 → 读写文件 → 关闭文件(使用完毕要记得关闭哦)。\n关于文件流 所有的文件都要载入内存才能处理,所有的数据必须写入文件才不会丢失。\n数据在文件和内存之间传递的过程叫做文件流 ,数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。\n文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到 c 语言的变量(如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做 数据流(data stream) ,相应的,数据从数据源到程序(内存)的过程叫做输入流(input stream),从程序(内存)到数据源的过程叫做输出流(output stream)。\n输入输出(input outpt, io)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。\n我们可以说,打开文件就是打开了一个流。\n打开/关闭文件 在 c 语言中,操作文件之前必须先打开文件。\n所谓 _“打开文件” ,就是让程序和文件建立连接的过程,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 file 类型的结构体变量中; _“关闭文件” 就是断开与文件之间的联系,释放结构体变量,同时禁止再对文件进行操作。\n标准输入文件 stdin(表示键盘)、标准输出文件 stdout(表示显示器)、标准错误文件 stderr(表示显示器)是由系统打开的,可直接使用。\n使用 \u0026lt;stdio.h\u0026gt; 头文件中的 fopen() 函数即可打开文件,它的用法为:\nfile *fopen(char *filename, char *mode);\r// - filename 表示文件名称\r// - mode 表示打开方式 fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 file 类型的结构体变量中,然后将该变量的地址返回。\nfile 是 \u0026lt;stdio.h\u0026gt; 头文件中的一个结构体,它专门用来保存文件信息。如果希望接收 fopen() 的返回值,就需要定义一个 file 类型的指针。\n下面我们来看一段文件操作的规范写法:\nfile *fp; if ((fp = fopen(\u0026#34;d:\\\\demo.txt\u0026#34;, \u0026#34;rb\u0026#34;)) == null) { printf(\u0026#34;fail to open file!\\n\u0026#34;); exit(0); // 结束程序 } 我们在打开文件时 一定要 通过判断 fopen() 的返回值是否和 null 相等来判断是否打开失败。\n_关于文件打开方式 mode\n不同的操作需要不同的文件权限(只读、读写等),另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。\n在调用 fopen() 函数时,这些信息都必须提供,称为 文件打开方式 ,具体如下:\n打开方式 说明 控制读写权限的字符串 必须指明 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026ldquo;r\u0026rdquo; (read) 以“只读”方式打开文件。文件必须存在,否则打开失败。 \u0026ldquo;w\u0026rdquo; (write) 以“写入”方式打开文件。文件若不存在,新建;若存在,则清空文件内容。 \u0026ldquo;a\u0026rdquo; (append) 以“追加”方式打开文件。文件若不存在,新建;若存在,则(保留原有的文件内容)将写入的数据追加到文件的末尾。 \u0026ldquo;r+\u0026rdquo; 以“读写”方式打开文件。文件必须存在,否则打开失败。 \u0026ldquo;w+\u0026rdquo; 以“写入/更新”方式打开文件。相当于 w 和 r+ ,文件若不存在,新建;若存在,清空。 \u0026ldquo;a+\u0026rdquo; 以“追加/更新”方式打开文件。相当于 a 和 r+ ,文件若不存在,新建;若存在,追加。 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 控制读写方式的字符串 可选 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026ldquo;t\u0026rdquo; (text) 文本文件(默认) \u0026ldquo;b\u0026rdquo; (binary) 二进制文件 *注意,读写权限和读写方式可以组合使用,但是不能将读写方式放在读写权限的开头(可以放末尾和中间)。\n文件一旦使用完毕,应该使用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。文件正常关闭时, fclose() 的返回值 为 0 ,如果返回非零值则表示有错误发生。\nint fclose(file *fp); 读写文件 在 c 语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读写一个字符串,还可以读取若干个字节(数据块)。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。\n先来个完整的轮廓看看,如下:\n// 以字符形式读、写文件\rint fgetc(file *fp);\rint foutc(int ch, file *fp);\r// 以字符串形式读、写文件\rchar *fgets(char *str, int n, file *fp);\rint fputs (char *str, file *fp);\r// 以数据块形式读、写文件\rsize_t fread (void *ptr, size_t size, size_t count, file *fp);\rsize_t fwrite(void *ptr, size_t size, size_t count, file *fp);\r// 格式化读、写文件\rint fscanf (file *fp, char * format, ... );\rint fprintf(file *fp, char * format, ... );\r// 随机读、写文件\rvoid rewind(file *fp); // 用来将位置指针移动到文件开关\rint fseek(file *fp, long offset, int origin); // 用来将指针移动到任意位置 1. 以字符形式读写文件\n// int fgetc(file *fp); // - 成功时返回读取到的字符; // - 读取到文件末尾或读取失败时返回 eof (一个负数,通常为 -1) // 从 demo.txt 文件中读取一个字符,并保存到变量 ch 中 char ch; file *fp = fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;r+\u0026#34;); ch = fgetc(fp); 关于 eof (end of line),表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1 。 fgetc() 的返回值类型之所以为 int ,就是为了容纳这个负数(char 不能是负数)。\n在文件内部有一个位置指针,用来指向当前读写到的位置,也就是读写到第几个字节。在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc() 函数后,该指针会向后移动一个字节,所以可以连续多次使用 fgetc() 读取多个字符。\n*注意:这个文件内部的位置指针与 c 语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。\n下面我们来看一个示例,在屏幕上显示 demo.txt 文件的内容。\n#include \u0026lt;stdio.h\u0026gt; int main() { file *fp; char ch; // 如果文件不存在,给出提示并退出 if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;rt\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } // 每次读取一个字节,直到读取完毕 while ((ch=fgetc(fp)) != eof) { putchar(ch); } putchar(\u0026#39;\\n\u0026#39;); // 输出换行符 fclose(fp); // 关闭文件 return 0; } 写字符函数 fputc\n再看一个写入的示例,从键盘输入一行字符,写入文件。\n// int foutc(int ch, file *fp); // - 成功时返回写入的字符 // - 失败时返回 eof (一个负数) #include \u0026lt;stdio.h\u0026gt; int main(){ file *fp; char ch; // 判断文件是否打开成功 if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;wt+\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } printf(\u0026#34;input a string:\\n\u0026#34;); // 每次从键盘读取一个字符并写入文件 while ((ch=getchar()) != \u0026#39;\\n\u0026#39;) { fputc(ch, fp); } fclose(fp); return 0; } 2. 以字符串形式读写文件\nfgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中,用法如下:\nchar *fgets(char *str, int n, file *fp);\r// - str 为字符数组(长度为 n+1 ,不要忘了读取到的字符串会在末尾自动添加 \u0026#39;\\0\u0026#39;)\r// - n 为要读取的字符数目\r// - fp 为文件指针\r// 读取成功时返回字符数组的首地址,也即 str\r// 读取失败时,返回 null\r// 如果开始读取时,文件内部指针已经指向了文件末尾,将读取不到任何字符,也返回 null 来看一个示例:\n// 从 demo.txt 中读取 100 个字符,并保存到字符数组 str 中 #define n 101 char str[n]; file *fp = fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;r\u0026#34;); fgets(str, n, fp); *需要重点说明的是,在读取 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大, fgets() 最多只能读取一行数据,不能跨行。\n在 c 语言中,没有按行读取文件的函数,我们可以借助 fgets() ,将 n 的值设置地足够大,每次就可以读取到一行数据了。\n再来看一个示例,一行一行地读取文件:\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define n 100 int main() { file *fp; char str[n+1]; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;rt\u0026#34;)) == null) { puts(\u0026#34;fial to open file!\u0026#34;); exit(0); } while(fgets(str, n, fp) != null) { printf(\u0026#34;%s\u0026#34;, str); } fclose(fp); return 0; } 写字符串函数 fputs\nfputs() 函数用来向指定的文件写入一个字符串,它的用法为:\nchar *fputs(char *str, file *fp);\r// - str 为要写入的字符串)\r// - fp 为文件指针\r// 写入成功时返回非负数\r// 写入失败时,返回 eof 来看一个示例,向上例中建立的 demo.txt 文件中追加一个字符串:\n#include \u0026lt;stdio.h\u0026gt; int main() { file *fp; char str[102] = {0}, strtemp[100]; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;at+\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } printf(\u0026#34;input a string:\u0026#34;); gets(strtemp); strcat(str, \u0026#34;\\n\u0026#34;); strcat(str, strtemp); fputs(str, fp); fclose(fp); return 0; } 3. 以数据块形式读写文件\n// 以数据块形式读、写文件\rsize_t fread (void *ptr, size_t size, size_t count, file *fp);\rsize_t fwrite(void *ptr, size_t size, size_t count, file *fp);\r// - ptr 为内存区块的指针,它可以是数组、变量、结构体等\r// - fread() 中的 ptr 用来存放读取到的数据\r// - fwrite() 中的 ptr 用来存放要写入的数据\r// - size 表示每个数据块的字节数\r// - count 表示要读写的数据块的块数\r// - fp 表示文件指针\r// 理论上,每次读写 size*count 个字节的数据\r// 返回成功读写的块数,即 count\r// 如果返回值小于 count\r// - 对于 fwrite() 来说,不用发生了写入错误,可以用 ferror() 函数检测\r// - 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测 size_t 是什么呢?\nsize_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,即非负数,常用来表示数量。\n来看一个示例,从键盘输入一个数组,将数组写入文件,再读取出来:\n#include \u0026lt;stdio.h\u0026gt; #define n 5 int main() { // 从键盘输入的数据放入 a,从文件读取的数据放入 b int a[n], b[n]; int i, size = sizeof(int); file *fp; if((fp=open(\u0026#34;demo.txt\u0026#34;, \u0026#34;rb+\u0026#34;)) == null) { // 以二进制方式打开 puts(\u0026#34;fail to pen file!\u0026#34;); exit(0); } // 从键盘输入数据,并保存于数组 a for (i=0; i\u0026lt;n; i++) { scanf(\u0026#34;%d\u0026#34;, \u0026amp;a[i]); } // 将数组 a 的内容写入到文件 fwrite(a, size, n, fp); // 将文件中的位置指针重新定位到文件开头 rewind(fp); // 从文件读取内容并保存到数组 b fread(b, size, n, fp); // 在屏幕上显示数组 b 的内容 for (i=0; i\u0026lt;n; i++) { printf(\u0026#34;%d\u0026#34;, b[i]); } printf(\u0026#34;\\n\u0026#34;); fclose(fp); return 0; } 打开 demo.txt,发现文件内容根本无法阅读。这是因为我们使用 \u0026ldquo;rb+\u0026rdquo; 方式打开文件,数组会原封不动地以二进制形式写入文件,一般无法阅读。\n再来看一个示例,从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示到屏幕上:\n#include \u0026lt;stdio.h\u0026gt; #define n 2 struct stu { char name[10]; int num; int age; float score; } boya[n], boyb[n], *pa, *pb; int main() { file *fp; int i; pa = boya; pb = boyb; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;wb+\u0026#34;)) == null) { puts(\u0026#34;fail to pen file!\u0026#34;); exit(0); } // 从键盘输入数据 printf(\u0026#34;input data:\\n\u0026#34;); for (i=0; i\u0026lt;n; i++, pa++) { scanf(\u0026#34;%s %d %d %f\u0026#34;, pa-\u0026gt;name, \u0026amp;pa-\u0026gt;num, \u0026amp;pa-\u0026gt;age, \u0026amp;pa-\u0026gt;score); } // 将数组 boya 的数据写入文件 fwrite(boya, sizeof(struct stu), n, fp); // 将文件中的位置指针重置到文件开头 rewind(fp); // 从文件读取数据并保存到数据 boyb fread(boyb, sizeof(struct stu), n, fp); // 输出数组 boyb 中的数据 for (i=0; i\u0026lt;n; i++, pb++) { printf(\u0026#34;%s %d %d %f\\n\u0026#34;, pb-\u0026gt;name, pb-\u0026gt;num, pb-\u0026gt;age, pb-\u0026gt;score); } fclose(fp); return 0; } 4. 格式化读写文件\nfscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。\n// 格式化读、写文件\rint fscanf (file *fp, char * format, ... );\rint fprintf(file *fp, char * format, ... );\r// - fp 为文件指针\r// - format 为格式控制字符串\r// - ... 表示参数列表\r// 成功 返回写入的字符的个数\r// 失败 返回负数 来看一个简单的示例:\nfile *fp; int i, j; char *str, ch; fscanf(fp, \u0026#34;%d %s\u0026#34;, \u0026amp;i, str); fprintf(fp, \u0026#34;%d %c\u0026#34;, j, ch); 5. 随机读写文件\n移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek() 。\n// 随机读、写文件\rvoid rewind(file *fp); // 用来将位置指针移动到文件开关\rint fseek (file *fp, long offset, int origin); // 用来将指针移动到任意位置\r// - fp 为文件指针,也就是被移动的文件\r// - offset 为偏移量,也就是要移动的字节数,正向后移,负向前移\r// - origin 为起始位置,c 语言规定起始位置有三种:\r// - 文件开头, 常量名 seek_set, 值为 0\r// - 当前位置, 常量名 seek_cur, 值为 1\r// - 文件末尾, 常量名 seek_end, 值为 2 如 fseek(fp, 100, 0); 表示把位置指针移动到离文件开头 100 个字节处。\n","date":"2022-03-01","permalink":"https://loveminimal.github.io/posts/c/","summary":"\u003cblockquote\u003e\n\u003cp\u003e= 好的吧,我承认,对于 C 语言,我有一种莫名其妙的偏爱!是因为可以直接操作内存吗?或许是!可能吧,计算机有什么神奇的东西呢?最终就是围绕那几个概念在来回打转!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"c"},{"content":" = 这篇 promise 仅是摘录使用,内容很散碎……\npromise 是一个对象(可以用来绑定回调函数),它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 promise 实例对象,所以我们首先说明怎样使用 promise,再说明如何创建 promise 。\n使用 promise1 本质上 promise 是一个函数返回的对象 ,我们可以在它上面绑定回调函数,如此就不需要在一开始就把回调函数作为参数传入给这个函数了。\n= 看了后面的 🌰 ,就容易理解了。一定程度上避免了回调地狱 👻\n下面来看一个示例,假设现在有一个名为 createaudiofileasync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。\n// 成功时的回调函数 function successcallback(result) { console.log(\u0026#39;音频文件创建成功:\u0026#39; + result); } // 失败的回调函数 function failurecallback(error) { console.log(\u0026#39;音频文件创建失败:\u0026#39; + error); } createaudiofileasync(audiosettings, successcallback, failurecallback); 更现代的函数会返回一个 promise 对象,使得你可以将你的回调函数绑定在该 promise 上。下面我们重写函数 createaudiofileasync() 使其返回 promise,如下:\nconst promise = createaudiofileasync(audiosettings); promise.then(successcallback, failurecallback); // or 简写为 createaudiofileasync(audiosettings).then(successcallback, failurecallback); 我们把这个称为 异步函数调用 ,这种形式有若干优点,下面我们将会逐一讨论。\n不同于“老式”的传入回调,在使用 promise 时,会有 以下约定 :\n在本轮 事件循环 运行完成之前,回调函数是不会被调用的; 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用; 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。 promise 很棒的一点就是 _链式调用(chaining) 。\n链式调用 连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 _promise 链 来实现这种需求。\n!!! then() 函数会返回一个和原来不同的新的 promise 。\nconst promise = dosomething(); const promise2 = promise.then(successcallback, failurecallback); // or const promise2 = dosomething().then(successcallback, failurecallback); 其中, promise2 不仅表示 dosomething() 函数的完成,也代表了你传入的 successcallback 或者 failurecallback 的完成,这两个函数也可以返回一个 promise 对象,从而形成另一个异步操作,如此,在 promise2 上新增的回调函数会排上这个 promise 对象的后面。\n基本上,第一个 promise 都代表了链中另一个异步过程的完成。\n来看一下,过去要想做多重的异步操作,会导致经典的回调地狱,如下:\ndosomething(function(result) { dosomethingelse(result, function(newresult) { dothirdthing(newresult, function(finalresult) { console.log(\u0026#39;got the final result: \u0026#39; finalresult); }, failurecallback); }, failurecallback); }, failurecallback); 而现在,我们可以把回调绑定到返回的 promise 上,形成一个 promise 链,如下:\ndosomething().then(function(result) { return dosomethingelse(result); }) .then(function(newresult) { return dothirdthing(newresult); }) .then(function(finalresult) { console.log(\u0026#39;got the final result: \u0026#39; + finalresult); }) .catch(failurecallback); // 也可以用箭头函数来表示 dosomething() .then(result =\u0026gt; dosomethingelse(result)) .then(newresult =\u0026gt; dothirdthing(newresult)) .then(finalresult =\u0026gt; { console.log(`got the final result: ${finalresult}`); }) .catch(failurecallback); !!! 注意:一定要有返回值 ,否则,callback 将无法获取上一个 promise 的结