Erlang/OTP 完全指南 / 04 - 变量与类型
第 04 章:变量与类型
Erlang 的变量与其他语言截然不同——单次赋值(Single Assignment),绑定后不可修改。本章深入理解不可变性、模式匹配和基本数据类型。
4.1 变量:单次赋值
4.1.1 基本概念
%% 变量绑定(binding)
X = 1.
%% X 现在等于 1
%% 尝试重新绑定
X = 2.
%% ** exception error: no match of right hand side value 2
%% 这就是"单次赋值"——变量一旦绑定,不可更改
%% 但是可以用相同的值"模式匹配"
X = 1.
%% ok(因为 X 已经是 1,1 = 1 成功)
4.1.2 变量命名规则
| 规则 | 示例 | 说明 |
|---|---|---|
| 以大写字母或下划线开头 | X, Name, _Temp | 区分大小写 |
| 下划线开头 | _Var | 不会产生"未使用变量"警告 |
| 纯下划线 | _ | 匿名变量,完全忽略 |
_Name | _Count | 明确忽略,但可读性好 |
| 小写字母开头 | name, x | ❌ 这是 atom,不是变量 |
%% 正确的变量名
Name = "Alice".
Age = 25.
_private_var = 42.
_User = ignored.
%% 错误的变量名(小写开头 = atom)
name = "Alice".
%% ** exception error: no match of right hand side value "Alice"
4.1.3 匿名变量 _
%% _ 用于忽略不需要的值
{_, B} = {1, 2}. %% 只取第二个元素,B = 2
[_, _, C] = [1, 2, 3]. %% 只取第三个元素,C = 3
%% 函数中忽略参数
handle(_Request, _State) ->
ok.
%% 多个 _ 不绑定任何东西
{_, _} = {1, 2}. %% ok
{_, _} = {1, 2, 3}. %% 错误!元组大小不匹配
4.1.4 为什么不可变?
| 特性 | 可变变量 | 不可变变量(Erlang) |
|---|---|---|
| 并发安全 | 需要锁 | 天然安全 |
| 数据共享 | 危险 | 安全(不会被修改) |
| 调试难度 | 高(状态随时变化) | 低(值不会变) |
| GC 复杂度 | 高 | 低(每进程独立) |
| 心智负担 | 高 | 低 |
💡 Erlang 的不可变性是实现"轻量级进程并发"的关键基础。
4.2 基本数据类型
4.2.1 类型总览
Erlang 数据类型
├── Number(数字)
│ ├── Integer(整数)
│ └── Float(浮点数)
├── Atom(原子)
├── Bit String / Binary(位串/二进制)
├── Reference(引用)
├── Fun(函数)
├── Port(端口)
├── Pid(进程标识符)
├── Tuple(元组)
├── Map(映射)
├── List(列表)
│ ├── Proper List(正规列表)
│ ├── Improper List(非正规列表)
│ └── String(字符串,即整数列表)
└── Record(记录,编译时转换为元组)
4.2.2 整数(Integer)
%% 基本整数
42
-17
0
%% 大整数(自动扩展,无溢出!)
99999999999999999999999999999999.
%% 不同进制
2#1010. %% 二进制 = 10
8#77. %% 八进制 = 63
16#FF. %% 十六进制 = 255
36#ZZ. %% 三十六进制 = 1295
%% $字符 获取 ASCII 码值
$A. %% 65
$a. %% 97
$\n. %% 10(换行符)
$\s. %% 32(空格)
%% 整数运算
1 + 2. %% 3
10 - 3. %% 7
4 * 5. %% 20
10 div 3. %% 3(整数除法)
10 rem 3. %% 1(取余)
4.2.3 浮点数(Float)
3.14
-0.5
1.0e10 %% 1.0 × 10^10
1.0e-3 %% 0.001
%% 浮点数运算
1.0 + 2.5. %% 3.5
10 / 3. %% 3.3333333333333335
%% 注意精度问题
0.1 + 0.2. %% 0.30000000000000004
%% 转换
float(42). %% 42.0
round(3.7). %% 4(四舍五入)
trunc(3.7). %% 3(截断)
floor(3.7). %% 3(向下取整)
ceil(3.2). %% 4(向上取整)
4.2.4 Atom(原子/符号)
Atom 是常量名称,类似其他语言的 symbol 或 enum:
%% 基本 atom(小写字母开头)
hello
ok
error
true
false
undefined
%% 带特殊字符的 atom(需要单引号)
'hello world'
'123abc'
'++'
'Atom with spaces'
%% 布尔值就是 atom
true = is_atom(true). %% true
false = is_atom(false). %% true
%% 原子比较:按字母序排序
apple < banana. %% true
apple < 'Apple'. %% false(大写字母 < 小写字母)
%% ⚠️ 注意:atom 不会被垃圾回收!
%% 每个 atom 存入全局 atom 表,最多约 1,048,576 个
%% 动态创建 atom(如 list_to_atom)有内存泄漏风险
4.2.5 元组(Tuple)
%% 创建元组
Point = {10, 20}.
Person = {person, "Alice", 25}.
%% 模式匹配解构
{X, Y} = Point. %% X = 10, Y = 20
{person, Name, Age} = Person. %% Name = "Alice", Age = 25
%% 只取部分值
{_, Name, _} = Person. %% Name = "Alice"
%% 元素替换(创建新元组,原元组不变)
NewPoint = setelement(1, Point, 100). %% {100, 20}
Point. %% 仍然是 {10, 20}
%% 元组大小
tuple_size({1, 2, 3}). %% 3
4.2.6 列表(List)
%% 空列表
[].
%% 基本列表
[1, 2, 3].
["a", "b", "c"].
%% 混合类型(Erlang 是动态类型)
[1, "hello", true, {x, y}].
%% Cons 操作符 | (构造列表)
[H | T] = [1, 2, 3, 4]. %% H = 1, T = [2,3,4]
%% 字符串本质是整数列表
"hello" = [104, 101, 108, 108, 111].
%% 列表操作
length([1, 2, 3]). %% 3
lists:reverse([1, 2, 3]). %% [3, 2, 1]
lists:nth(2, [a, b, c]). %% b(第2个元素)
lists:sort([3, 1, 2]). %% [1, 2, 3]
lists:flatten([[1,2],[3,[4]]]). %% [1,2,3,4]
4.2.7 字符串(String)
Erlang 没有原生字符串类型,字符串就是整数列表:
%% 字符串字面量
"hello" = [104, 101, 108, 108, 111].
%% 字符串操作
length("hello"). %% 5
lists:nth(1, "hello"). %% 104(ASCII 码)
binary_to_list(<<"hello">>). %% "hello"
%% 字符串拼接
"Hello, " ++ "World!". %% "Hello, World!"
%% ⚠️ ++ 对长字符串效率低,推荐使用 IO list
%% 现代方式:binary 字符串(更高效)
<<"hello">>.
binary:bin_to_list(<<"hello">>). %% [104,101,108,108,111]
%% 格式化
io_lib:format("Name: ~s, Age: ~p", ["Alice", 25]).
4.2.8 Binary(二进制)
%% 二进制字面量
<<1, 2, 3>>. %% 二进制数据
<<"hello">>. %% 字符串的二进制表示
%% 二进制操作
byte_size(<<"hello">>). %% 5
bit_size(<<1, 2, 3>>). %% 24(3 字节 = 24 位)
%% 位语法(Bit Syntax)—— 二进制模式匹配
<<A:8, B:8, C:8>> = <<1, 2, 3>>. %% A=1, B=2, C=3
%% 解析网络协议包
<<Version:4, Type:4, Length:16, Data/binary>> = <<1:4, 2:4, 10:16, "hello">>.
%% Version=1, Type=2, Length=10, Data="hello"
4.2.9 Reference(引用)
%% 创建唯一引用
Ref = make_ref().
%% #Ref<0.1234.5678>
%% 每次调用 make_ref() 都生成唯一的值
Ref1 = make_ref().
Ref2 = make_ref().
Ref1 = Ref2. %% 错误!引用不相等
%% 常用于消息的唯一标识
MsgRef = make_ref(),
self() ! {MsgRef, hello},
receive
{MsgRef, Reply} -> Reply
end.
4.2.10 Fun(函数)
%% 匿名函数
Add = fun(X, Y) -> X + Y end.
Add(3, 5). %% 8
%% 函数引用
F = fun io:format/2.
F("Hello~n", []).
%% 函数捕获(Capture)
Double = fun(X) -> X * 2 end.
lists:map(Double, [1, 2, 3]). %% [2, 4, 6]
%% 闭包(捕获外部变量)
Multiplier = fun(N) ->
fun(X) -> X * N end
end.
Triple = Multiplier(3).
Triple(10). %% 30
4.2.11 Pid(进程标识符)
%% 获取当前进程 PID
self(). %% <0.123.0>
%% 创建新进程
Pid = spawn(fun() ->
receive stop -> ok end
end).
%% Pid 是一个 PID 类型
%% 检查
is_pid(Pid). %% true
is_pid(self()). %% true
4.3 类型检查函数
| 函数 | 返回值 | 说明 |
|---|---|---|
is_atom(X) | boolean() | 是否是 atom |
is_integer(X) | boolean() | 是否是整数 |
is_float(X) | boolean() | 是否是浮点数 |
is_number(X) | boolean() | 是否是数字 |
is_list(X) | boolean() | 是否是列表 |
is_tuple(X) | boolean() | 是否是元组 |
is_map(X) | boolean() | 是否是 map |
is_binary(X) | boolean() | 是否是二进制 |
is_bitstring(X) | boolean() | 是否是位串 |
is_pid(X) | boolean() | 是否是进程 ID |
is_reference(X) | boolean() | 是否是引用 |
is_function(X) | boolean() | 是否是函数 |
is_function(X, N) | boolean() | 是否是 N 元函数 |
is_port(X) | boolean() | 是否是端口 |
is_boolean(X) | boolean() | 是否是布尔值 |
is_record(X, Tag) | boolean() | 是否是某 record |
is_atom(hello). %% true
is_atom("hello"). %% false
is_list([1, 2]). %% true
is_list("hello"). %% true(字符串是列表!)
is_binary("hello"). %% false
is_binary(<<"hello">>). %% true
4.4 类型转换
%% 数字转换
float(42). %% 42.0
round(3.7). %% 4
trunc(3.7). %% 3
floor(3.7). %% 3
ceil(3.2). %% 4
%% Atom ↔ String/List
atom_to_list(hello). %% "hello"
list_to_atom("hello"). %% hello
%% Number ↔ String/List
integer_to_list(42). %% "42"
list_to_integer("42"). %% 42
float_to_list(3.14). %% "3.140000000000000124e+00"
list_to_float("3.14"). %% 3.14
%% Binary ↔ List
binary_to_list(<<"hello">>). %% [104,101,108,108,111]
list_to_binary([104,101,108,108,111]). %% <<"hello">>
%% Binary ↔ String
binary_to_list(<<"hello">>). %% "hello"(同上)
list_to_binary("hello"). %% <<"hello">>
%% Tuple ↔ List
tuple_to_list({1, 2, 3}). %% [1, 2, 3]
list_to_tuple([1, 2, 3]). %% {1, 2, 3}
%% Term ↔ Binary (序列化)
term_to_binary({1, 2, 3}). %% <<131,104,3,...>>
binary_to_term(<<131,104,3,...>>). %% {1, 2, 3}
4.5 自定义类型
4.5.1 -type 声明
%% types_demo.erl
-module(types_demo).
-export([greet/1]).
%% 自定义类型
-type age() :: 0..150.
-type name() :: string().
-type person() :: {name(), age()}.
-type result(T) :: {ok, T} | {error, string()}.
%% 函数规范使用自定义类型
-spec greet(person()) -> string().
greet({Name, Age}) ->
io_lib:format("Hello ~s, you are ~p years old!", [Name, Age]).
%% 导出类型供其他模块使用
-export_type([age/0, person/0]).
4.5.2 类型语法
| 类型表达式 | 含义 | 示例 |
|---|---|---|
term() | 任意 Erlang 值 | term() |
atom() | 任意 atom | atom() |
integer() | 任意整数 | integer() |
float() | 任意浮点数 | float() |
number() | integer() 或 float() | number() |
boolean() | true 或 false | boolean() |
string() | 字符串(整数列表) | string() |
binary() | 二进制 | binary() |
list() | 任意列表 | list() |
list(T) | 类型 T 的列表 | list(integer()) |
[T] | list(T) 的简写 | [integer()] |
{T1, T2} | 二元组 | {integer(), atom()} |
#{K => V} | Map 类型 | #{atom() => integer()} |
T1 | T2 | 联合类型 | ok | error |
.. | 范围 | 1..100 |
fun((Args) -> Ret) | 函数类型 | fun((integer()) -> atom()) |
4.6 实战:温度转换器
%% temperature.erl
-module(temperature).
-export([celsius_to_fahrenheit/1, fahrenheit_to_celsius/1,
classify/1, convert_all/1]).
-type temp() :: number().
-type scale() :: celsius | fahrenheit.
-type classification() :: freezing | cold | warm | hot | boiling.
-spec celsius_to_fahrenheit(temp()) -> float().
celsius_to_fahrenheit(C) ->
C * 9 / 5 + 32.
-spec fahrenheit_to_celsius(temp()) -> float().
fahrenheit_to_celsius(F) ->
(F - 32) * 5 / 9.
-spec classify({temp(), scale()}) -> classification().
classify({Temp, celsius}) ->
if
Temp =< 0 -> freezing;
Temp =< 10 -> cold;
Temp =< 25 -> warm;
Temp =< 100 -> hot;
true -> boiling
end;
classify({Temp, fahrenheit}) ->
classify({fahrenheit_to_celsius(Temp), celsius}).
-spec convert_all([{temp(), scale()}]) -> [{temp(), scale(), classification()}].
convert_all(Temps) ->
[{T, S, classify({T, S})} || {T, S} <- Temps].
$ erl
1> c(temperature).
{ok, temperature}
2> temperature:celsius_to_fahrenheit(100).
212.0
3> temperature:classify({25, celsius}).
warm
4> temperature:convert_all([{0, celsius}, {100, fahrenheit}, {30, celsius}]).
[{0,celsius,freezing},{100,fahrenheit,boiling},{30,celsius,hot}]
4.7 注意事项
⚠️ 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 字符串是整数列表 | "hello" 实际是 [104,101,108,108,111],效率低 |
| Atom 不可回收 | 动态创建 atom(list_to_atom)会导致内存泄漏 |
| 浮点精度 | 0.1 + 0.2 ≠ 0.3,金融计算需注意 |
| 小写开头不是变量 | name 是 atom,Name 才是变量 |
| 变量作用域 | 每个 case/if/receive 子句中绑定的变量不能在外面使用 |
💡 最佳实践
- 使用 binary (
<<"hello">>) 处理字符串,比列表更高效 - 不要动态创建 atom,使用已有的 atom
- 使用
-type和-spec标注类型 - 理解模式匹配是 Erlang 编程的核心
4.8 扩展阅读
上一章:03 - Hello World 下一章:05 - 模式匹配