强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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()任意 atomatom()
integer()任意整数integer()
float()任意浮点数float()
number()integer() 或 float()number()
boolean()truefalseboolean()
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 子句中绑定的变量不能在外面使用

💡 最佳实践

  1. 使用 binary (<<"hello">>) 处理字符串,比列表更高效
  2. 不要动态创建 atom,使用已有的 atom
  3. 使用 -type-spec 标注类型
  4. 理解模式匹配是 Erlang 编程的核心

4.8 扩展阅读


上一章:03 - Hello World 下一章:05 - 模式匹配