极客学院团队出品 · 更新于 2018-11-28 11:00:43

6 几种 Erlang 的特殊惯例

6.1 用记录作为主要的数据结构

把记录作为主要的数据结构。记录是Erlang 4.3 时才引入的一种带标记的元组(参看 EPK/NP 95:034)。它类似于 C 语言中的 struct 或 Pascal 中的 record

如果记录将用于多个模块,它的定义应放在这些模块所包括的一个头文件中(带有 .hrl 后缀)。如果记录只用在一个模块中,则它的定义应位于定义模块的文件的最前面位置处。

Erlang 的记录特性可以保证数据结构跨模块的一致性,因此在模块间传递数据结构时,接口函数应使用记录。

6.2 使用选择器和构建函数

使用记录特性所提供的选择器(selector)和构造函数(constructor)来管理记录实例。不要使用明显假定记录是一个元组的匹配。范例如下:

demo() ->
  P = #person{name = "Joe", age = 29},
  #person{name = Name1} = P,% 使用匹配,或者......
  Name2 = P#person.name. % 像这样使用 selector  

不要像下面这样编程:


demo() ->
  P = #person{name = "Joe", age = 29},
  {person, Name, _Age, _Phone, _Misc} = P. % 不要这样做

6.3 使用带标记的返回值

使用带标记的返回值。

不要像下面这样编程:

keysearch(Key, [{Key, Value}|_Tail]) ->
  Value; %% Don't return untagged values!
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
  keysearch(Key, Tail);
keysearch(Key, []) ->
  false.  

{Key, Value} 不含有 false 值。下面是正确的方法。


keysearch(Key, [{Key, Value}|_Tail]) ->
  {value, Value}; %% Correct. Return a tagged value.
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
  keysearch(Key, Tail);
keysearch(Key, []) ->
  false.  

6.4 慎重使用 catchthrow

千万不要在搞不清用法的情况下使用 catchthrow!要尽可能少地使用它们。

复杂与非信任输入(从外部环境中所输入的内容,而非来自于可信的程序时)可能会导致代码很多深层次位置的错误,当程序处理这些类型的输入时,catchthrow 就显得非常有用了。关于这一点,典型的例子就是编译器。

6.5 慎重使用进程字典

千万不要在搞不清用法的情况下使用 getput 等函数!尽量少用它们。

引入一个新的参数,就可以重写使用进程字典的函数。

范例如下:
不要像下面这样编程:


tokenize([H|T]) ->
  ...;
tokenize([]) ->
  case get_characters_from_device(get(device)) of % Don't use get/1!
    eof -> [];
    {value, Chars} ->
      tokenize(Chars)
  end.  

正确的方法是:


tokenize(_Device, [H|T]) ->
  ...;
tokenize(Device, []) ->
  case get_characters_from_device(Device) of     % This is better
    eof -> [];
    {value, Chars} ->
      tokenize(Device, Chars)
  end.  

使用 getput 会容易导致,在同样输入下函数的结果却随情况不同而改变。这使得代码难以阅读,因为代码已经失去了确定性。调试也变得很复杂,因为使用 getput 的函数不仅是自身参数的函数,而且还是进程字典的函数。Erlang 中的很多运行时错误(比如 bad_match)就包括函数的参数,但从来不包括进程字典。

6.6 不要使用导入

不要使用 -import,使用它会让代码变得难以阅读,这是因为无法弄清函数定义所在的模块。使用 exref(交叉引用工具)查找模块依赖。

6.7 导出函数

一定要搞清楚某个函数之所以导出的原因。这种原因可能包括以下几种:

  • 该函数是模块的用户接口;
  • 该函数是面向其他模块的接口函数;
  • 该函数从 applyspawn 调用,但只限于本模块内部调用。

使用不同的 -export 进行分组,并相应对它们进行注释。范例如下:


%% 用户接口
-export([help/0, start/0, stop/0, info/1]).

%% 模块间导出
-export([make_pid/1, make_pid/3]).
-export([process_abbrevs/0, print_info/5]).

%% 导出,只限于模块内部使用
-export([init/1, info_log_impl/1]).