Skip to content

Latest commit

 

History

History
600 lines (427 loc) · 16.9 KB

README-zhCN.md

File metadata and controls

600 lines (427 loc) · 16.9 KB

Emacs Lisp 风格指南

榜样很重要
——墨菲警官《机器战警》

这份 Emacs Lisp 风格指南向你推荐实际使用中的最佳实践,Emacs Lisp 程序员如何写出可被别的 Emacs Lisp 程序员维护的代码。我们只说实际使用中的用法。指南再好,但里面说的过于理想化结果大家拒绝使用或者可能根本没人用,又有何意义。

本指南依照相关规则分成数个小节。我尽力在规则后面说明理由(如果省略了说明,那是因为其理由显而易见)。

规则不是我凭空想出来的——绝大部分来自我作为从业多年的职业软件工程师的经验,从 Emacs Lisp 社区成员得到的反馈及建议,和几个评价甚高的 Emacs Lisp 编程资源,像 "GNU Emacs Lisp Reference Manual"

本指南仍在完善中——有些章节还没写,另一些则不完整,某些规则缺乏实例,某些例子也不够清楚。到时候都会解决的——放心吧。

请注意,Emacs 开发者也维护着一份 coding conventions and tips

你可以使用 Transmuter 生成本指南的 PDF 或 HTML 版本。

目录

源代码排版和组织

所有风格都又丑又难读,自己的除外。几乎人人都这样想。把“自己的除外”拿掉,他们或许是对的...
——Jerry Coffin(论缩排)

以下指定的缩排规则 Emacs 已经默认采用了,当你输入 <tab> 时, 缩排总是正确的。

  • 使用空格缩进。不要使用硬 tab。

  • 对于普通的函数,垂直对齐参数。

    ;; 好
    (format "%s %d"
            something
            something-else)
    
    ;; 差
    (format "%s %d"
      something
      something-else)
  • 如果第一个参数在新的一行上,与函数名对齐。

    ;; 好
    (format
     "%s %d"
     something
     something-else)
    
    ;; 差
    (format
      "%s %d"
      something
      something-else)
  • 有些特殊 form,它们接受一个或多个_"special"参数,紧接着一个"body"(一个任意长度的参数,但只有最后的返回值有意义),比如 ifletwith-current-buffer 等等。"special"参数应该与 form 名放在同一行或用四个空格缩排。"body"_参数应该用两个空格缩排。

    ;; 好
    (when something
      (something-else))
    
    ;; 差 -  body 参数用了四个空格缩排
    (when something
        (something-else))
    
    ;; 差 - 像普通的函数一样对齐了
    (when
     something
     (something-else))
  • if form 的 "if" 分支是一个_"special"_参数,应该用四个空格缩排。

    ;; 好
    (if something
        if-clause
      else-clause)
    
    ;; 差
    (if something
      if-clause
      else-clause)
  • 竖直对齐 let 的绑定。

    ;; 好
    (let ((thing1 "some stuff")
          (thing2 "other stuff")
      ...)
    
    ;; 差
    (let ((thing1 "some stuff")
      (thing2 "other stuff"))
      ...)
  • 使用 Unix 风格的换行符。(BSD/Solaris/Linux/OSX 的用户不用担心,Windows 用户要格外小心。)

    • 如果你使用 Git ,可用下面这个配置,来保护你的项目不被 Windows 的换行符干扰:
    $ git config --global core.autocrlf true
  • 如果有 text 位于开括号(({[)之前,或位于闭括号()}])之后,用一个空格隔开 text 和括号。相反地,不要在开括号之后和 text 之前,或 text 之前和闭括号之前,留任何空白字符。

  • 把所有的尾缀括号放在同一行上,而不是另起一行。

    ;; 好 ; 同一行
    (when something
      (something-else))
    
    ;; 差 ; 另起一行
    (when something
      (something-else)
    )
  • 使用空行来区分 top-level forms。

    ;; 好
    (defvar x ...)
    
    (defun foo ...)
    
    ;; 差
    (defvar x ...)
    (defun foo ...)

    一个例外需要把相关的 defs 放在一起时。

    ;; 好
    (defconst min-rows 10)
    (defconst max-rows 20)
    (defconst min-cols 15)
    (defconst max-cols 30)
  • 不要在函数或者宏定义的中间使用空白行。一个例外是要提示成对出现的构造语句,比如在 letcond 中出现的。

  • 每一行限制在 80 个字符内。

  • 避免行尾空格。

  • 避免参数列表中多于三个或四个占位的参数。

  • 总是开启 lexical scoping。且必须在作为一个 file local variable 放在文件首行。

    ;;; -*- lexical-binding: t; -*-

语法

  • 不要把 if form 的 "else" 分支包括在 pron 中(默认已经包括了)。

    ;; 好
    (if something
        if-clause
      (something)
      (something-else))
    
    ;; 差
    (if something
        if-clause
      (progn
        (something)
        (something-else)))
  • when 而不是 (if ... (progn ...)

    ;; 好
    (when pred
      (foo)
      (bar))
    
    ;; 差
    (if pred
      (progn
        (foo)
        (bar)))
  • unless 而不是 (when (not ...) ...)

    ;; 好
    (unless pred
      (foo)
      (bar))
    
    ;; 差
    (when (not pred)
      (foo)
      (bar))
  • 作比较时,留意 <> 等函数可以接受任意数量的参数,从 Emacs 24.4 起。

    ;; 更好
    (< 5 x 10)
    
    ;; 老的方法
    (and (> x 5) (< x 10))
  • t 作为 cond 的默认测试表达式。

    ;; 好
    (cond
      ((< n 0) "negative")
      ((> n 0) "positive")
      (t "zero"))
    
    ;; 差
    (cond
      ((< n 0) "negative")
      ((> n 0) "positive")
      (:else "zero"))
  • (1+ x) & (1- x) 而不是 (+ x 1)(- x 1)

命名

程式设计的真正难题是替事物命名及使缓存失效。
——Phil Karlton

  • 方法与变量使用 lisp-case

    ;; 好
    (defvar some-var ...)
    (defun some-fun ...)
    
    ;; 差
    (defvar someVar ...)
    (defun somefun ...)
    (defvar some_fun ...)
  • 用函数库的前缀避免命名冲突。

    ;; 好
    (defun projectile-project-root ...)
    
    ;; 差
    (defun project-root ...)
  • 给未使用到的局部(lexically scoped)变量名加前缀 _

    ;; 好
    (lambda (x _y) x)
    
    ;; 差
    (lambda (x y) x)
  • -- 表示私有定义(比如:projectile--private-fun)。

  • 断言函数(返回布尔值的函数)应该以 p 结尾,单字用 p、多字用 -p(比如:evenpbuffer-live-p)。

    ;; 好
    (defun palindromep ...)
    (defun only-one-p ...)
    
    ;; 差
    (defun palindrome? ...) ; Scheme 风格
    (defun is-palindrome ...) ; Java 风格
  • Face 名不应该-face 结尾。

    ;; 好
    (defface widget-inactive ...)
    
    ;; 差
    (defface widget-inactive-face ...)

  • 如果用函数能做到,不用宏。

  • 先写个宏的用例,而后实现这个宏。

  • 尽量把复杂的宏分解成较小的函数。

  • 一个宏通常仅提供语法糖,其关键部分应该是函数。如此一来,能易与组合。

  • 优先使用引号语法,而不是手动构造列表。

函数

  • 局部绑定使用 lambda,hook 或全局变量不该使用 lambda。后者应该用有名称的函数,可读性更好、易于定制。

    ;;; 好
    (mapcar (lambda (x) (or (car x) "")) some-list)
    (let ((predicate (lambda (x) (and (numberp x) (evenp x)))))
      (funcall predicate 1000))
    
    ;;; 差 - Define real functions for these.
    (defcustom my-predicate (lambda (x) (and (numberp x) (evenp x)))
      ...)
    (define-key my-keymap (kbd "C-f")
      (lambda () (interactive) (forward-char 1)))
    (add-hook 'my-hook (lambda () (save-some-buffers)))
  • 永远不要 hard quote lambda,会导致无法 byte-compilation。

    ;;; 好
    (lambda (x) (car x))
    
    ;;; 可以,但是不必要。
    #'(lambda (x) (car x))
    
    ;;; 差
    '(lambda (x) (car x))
  • 不要用匿名函数 wrap 你不需要的函数。

    ;; 好
    (cl-remove-if-not #'evenp numbers)
    
    ;; 差
    (cl-remove-if-not (lambda (x) (evenp x)) numbers)
  • 用一个井号连着引号(#')来 quote 函数名。它能给 byte-compiler 更好的提示:如果函数未定义,会给出警告。有些宏甚至会有不同效果(像 cl-labels)。

    ;; 好
    (cl-remove-if-not #'evenp numbers)
    (global-set-key (kbd "C-l C-l") #'redraw-display)
    (cl-labels ((butterfly () (message "42")))
      (funcall #'butterfly))
    
    ;; 差
    (cl-remove-if-not 'evenp numbers)
    (global-set-key (kbd "C-l C-l") 'redraw-display)
    (cl-labels ((butterfly () (message "42")))
      (funcall 'butterfly))

宏声明

  • 声明 debug-specification,用来告诉 edebug 哪些参数会被执行。如果所有的参数都会被执行,用 (declare (debug t)) 就够了。

  • 声明 indent specification,如果,macro 的参数不应该像函数(想想 defunwith-current-buffer)。

    (defmacro define-widget (name &rest forms)
      "Description"
      (declare (debug (sexp body))
               (indent defun))
      ...)

加载和自动加载

  • 每个库函数文件的结果都需要一个 provide 语句和依据适当的注释(provide 允许其它库用 require 导入)。

    (provide 'foo)
    
    ;;; foo.el ends here
  • require 加载库依赖,而不是 loadload-library(前者重复执行恒等,而后两者会重复执行)。

  • 给 mode 定义和用户函数和命令(也就是,设置函数和应该绑定快捷键的命令)添加 autoload。相反的,不要给全局变量和内部函数添加 autoload。

    ;;; 好
    ;;;###autoload
    (define-derived-mode foo-mode ...)
    
    ;;;###autoload
    (define-minor-mode foo-minor-mode ...)
    
    ;;;###autoload
    (defun foo-setup () ...)
    
    ;;; 差
    ;;;###autoload
    (defun foo--internal () ...)
    
    ;;;###autoload
    (defvar foo-option)
  • 不要给非定义用的 top-level from(autoloading 一个永远不应该修改用户的配置功能)。但有一个例外:auto-mode-alist 可以用来改变新的 major mode。

    ;;; 好
    ;;;###autoload
    (add-to-list 'auto-mode-alist '("\\.foo\\'" . foo-mode))
    
    ;;; 差
    ;;;###autoload
    (foo-setup)

注释

良好的代码是最佳的文档。当你要加一个注释时,扪心自问,“如何改善代码让它不需要注释?” 改善代码,再写相应文档使之更清楚。
——Steve McConnell

  • 尽量使你的代码自我文档化。

  • 写标题的注释至少使用三个分号。

  • 如果顶级注释表示一个标题,用三个分号,否则用两个分号。

  • 在代码片段之前写注释,要与之对齐,使用两个分号。

  • 写边缘注释使用一个分号。

  • 分号和注释文字之间至少留一个空格。

    ;;; Frob Grovel
    ;; This is where Frob grovels and where Grovel frobs.
    
    ;; This section of code has some important implications:
    ;;   1. Foo.
    ;;   2. Bar.
    ;;   3. Baz.
    
    (defun fnord (zarquon)
      ;; If zob, then veeblefitz.
      (quux zot
            mumble             ; Zibblefrotz.
            frotz))
  • 注释超过一个单词了,应句首大写并使用标点符号。句号后使用两个空格。

  • 避免肤浅的注释。

    ;; 差
    (1+ counter) ; 计数器加一
  • 及时更新注释。过时的注释比没有注释还要糟糕。

好代码就像是好的笑话 - 它不需要解释
——Russ Olsen

  • 避免替烂代码写注释。重构代码让它们看起来一目了然。(要嘛就做,要嘛不做——不要只是试试看。——Yoda)

注解

  • 注解应该直接写在相关代码那行之前。

  • 注解关键字后面,跟着一个冒号及空格,接着是描述问题的文字。

  • 如果需要用多行来描述问题,后续行要与第一行对齐。

  • 用字母缩写和日期给你的注解打标签,使得它们的关联更易于辨识。

    (defun some-fun ()
      ;; FIXME: This has crashed occasionally since v1.2.3. It may
      ;;        be related to the BarBazUtil upgrade. (xz 13-1-31)
      (baz))
  • 在问题是显而易见的情况下,任何的文档会是多余的,注解应放在有问题的那行的最后,并且不需更多说明。这个用法应该是例外而不是规则。

    (defun bar ()
      (sleep 100)) ; OPTIMIZE
  • 使用 TODO 标记以后应加入的特征与功能。

  • 使用 FIXME 标记需要修复的代码。

  • 使用 OPTIMIZE 标记可能影响性能的缓慢或效率低下的代码。

  • 使用 HACK 标记代码异味,即那些应该被重构的可疑编码习惯。

  • 使用 REVIEW 标记需要确认其编码意图是否正确的代码。举例来说:REVIEW: 我们确定用户现在是这么做的吗?

  • 如果你觉得恰当的话,可以使用其他定制的注解关键字,但别忘记录在项目的 README 或类似文档中。

Docstrings

Emacs 因其中众多的、深厚的、全面的文档而出名。花时间给你的 package 写 docstrings,可以延续这个传统。

  • 用简练、完成的句子。用祈使语气。比如:"Verify" 比 "Verifies" 好,"Check" 比 "Checks" 好。

  • 当函数需要参数时,解释参数是什么、是否必要,诸如此类。描述这些参数时应该全部大写,并按使用的顺序排序。

  • 援用大写首字母 "Emacs".

  • documentation string 的后续行不要缩进。它在源代码中看起来不错,但用户看的时候会变得奇怪。

    ;; 好
    (defun goto-line (line &optional buffer)
      "Go to LINE, counting from line 1 at beginning of buffer.
    
    If called interactively, a numeric prefix argument specifies
    LINE; without a numeric prefix argument, read LINE from the
    minibuffer..."
    ...)
    
    ;; 差
    (defun goto-line (line &optional buffer)
      "Go to LINE, counting from line 1 at beginning of buffer.
    
       If called interactively, a numeric prefix argument specifies
       LINE; without a numeric prefix argument, read LINE from the
       minibuffer..."
      ...)
    
    ;; 还是差
    (defun goto-line (line &optional buffer)
      "Go to LINE, counting from line 1 at beginning of buffer.
    
       If called interactively, a numeric prefix argument specifies
     LINE; without a numeric prefix argument, read LINE from the
     minibuffer..."
      ...)

工具

  • checkdoc 检查代码风格问题。
    • 不少人会把 checkdocFlycheck 结合起来用。
  • 在尝试提交你的 Package 给 MELPA 之前用 package-lint 检查。
    • 查看 package-lint 的 README 来了解与 flycheck 的整合。

其它

  • 保持一致性。在理想的世界里,遵循这些准则。
  • 使用常识。

贡献

在本指南所写的每条规则都不是定案。这只是我渴望想与同样对 Emacs Lisp 编程风格有兴趣的大家一起工作,以致于最终我们可以替整个 Emacs 社区创造一个有益的资源。

欢迎 open tickets 或 push 一个带有改进的更新请求。在此提前感谢你的帮助!

授权

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License

口耳相传

一份社区驱动的风格指南,如果没多少人知道,对一个社区来说就没有多少用处。微博转发这份指南,分享给你的朋友或同事。我们得到的每个评价、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是最好的指南,不是吗?

共勉之,
Bozhidar