15-结构体

在之前的几章中,我们谈到过图:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> map[:a]
1
iex> %{map | a: 3}
%{a: 3, b: 2}

结构体是基于图的一个扩展。它将默认值,编译时保证和多态性带入Elixir。

定义一个结构体,你只需在某模块中调用defstruct/1

iex> defmodule User do
...>   defstruct name: "john", age: 27
...> end
{:module, User,
 <<70, 79, 82, ...>>, {:__struct__, 0}}

现在可以用%User()语法创建这个结构体的“实例”了:

iex> %User{}
%User{name: "john", age: 27}
iex> %User{name: "meg"}
%User{name: "meg", age: 27}
iex> is_map(%User{})
true

结构体的编译时保证指的是编译时会检查结构体的字段存不存在:

iex> %User{oops: :field}
** (CompileError) iex:3: unknown key :oops for struct User

当讨论图的时候,我们演示了如何访问和修改图现有的字段。结构体也是一样的:

iex> john = %User{}
%User{name: "john", age: 27}
iex> john.name
"john"
iex> meg = %{john | name: "meg"}
%User{name: "meg", age: 27}
iex> %{meg | oops: :field}
** (ArgumentError) argument error

通过使用这种修改的语法,虚拟机知道没有新的键增加到图/结构体中,使得图可以在内存中共享它们的结构。 在上面例子中,john和meg共享了相同的键结构。

结构体也能用在模式匹配中,它们保证结构体有相同的类型:

iex> %User{name: name} = john
%User{name: "john", age: 27}
iex> name
"john"
iex> %User{} = %{}
** (MatchError) no match of right hand side value: %{}

匹配能工作,是由于结构体再底层图中有个字段叫__struct__

iex> john.__struct__
User

总之,结构体就是个光秃秃的图外加一个默认字段。我们这里说的光秃秃的图指的是,为图实现的协议都不能用于结构体。 例如,你不能枚举也不能访问一个结构体:

iex> user = %User{}
%User{name: "john", age: 27}
iex> user[:name]
** (Protocol.UndefinedError) protocol Access not implemented for %User{age: 27, name: "john"}

结构体也不是字典,因为也不适用字典模块的函数:

iex> Dict.get(%User{}, :name)
** (ArgumentError) unsupported dict: %User{name: "john", age: 27}

下一章我们将介绍结构体是如何同协议进行交互的。