15-结构体

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

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

结构体是基于图的一个扩展。它引入了默认值、编译期验证和多态性。

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

iex> defmodule User do
...>   defstruct name: "john", age: 27
...> end

现在可以用%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}

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