结构

在第7章中,我们了解了地图:

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

结构是构建在提供编译时检查和默认值的映射之上的扩展。

定义结构

要定义一个结构,使用defstruct结构:

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

用于defstruct定义结构将具有哪些字段以及其默认值的关键字列表。

结构体采用它们定义的模块的名称。在上面的例子中,我们定义了一个名为 struct 的结构体User

我们现在可以通过使用类似于用于创建地图的语法来创建User结构:

iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Meg"}
%User{age: 27, name: "Meg"}

结构提供了编译时保证,只有通过定义的字段(以及所有字段)defstruct才会被允许存在于结构中:

iex> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "John"}

访问和更新结构

当我们讨论地图时,我们展示了我们如何访问和更新地图的字段。相同的技术(以及相同的语法)也适用于结构:

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john.name
"John"
iex> meg = %{john | name: "Meg"}
%User{age: 27, name: "Meg"}
iex> %{meg | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "Meg"}

当使用更新语法(|)时,虚拟机知道没有新的键将被添加到结构中,允许下面的地图在内存中共享它们的结构。在上面的例子中,两个johnmeg共享存储器中的相同的密钥的结构。

结构体也可用于模式匹配,既用于匹配特定键的值,又用于确保匹配值是与匹配值相同类型的结构体。

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

结构是底下裸露的地图

在上面的例子中,模式匹配是有效的,因为在结构下面的是裸露的地图和一组固定的字段。作为地图,结构存储一个名为“special”的字段__struct__,其中包含结构体的名称:

iex> is_map(john)
true
iex> john.__struct__
User

请注意,我们将结构称为地图,因为没有为地图实现的协议可用于结构。例如,你既不能枚举也不能访问一个结构体:

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
             User.fetch(%User{age: 27, name: "John"}, :name)
iex> Enum.each john, fn({field, value}) -> IO.puts(value) end
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"}

但是,由于结构只是地图,因此它们可以与Map模块中的功能一起使用:

iex> kurt = Map.put(%User{}, :name, "Kurt")
%User{age: 27, name: "Kurt"}
iex> Map.merge(kurt, %User{name: "Takashi"})
%User{age: 27, name: "Takashi"}
iex> Map.keys(john)
[:__struct__, :age, :name]

与协议结合提供了 Elixir 开发人员最重要的功能之一:数据多态性。这就是我们将在下一章探讨的内容。

默认值和必需的键

如果您在定义结构时未指定默认键值,nil则将假定:

iex> defmodule Product do
...>   defstruct [:name]
...> end
iex> %Product{}
%Product{name: nil}

您还可以强制在创建结构时必须指定某些键:

iex> defmodule Car do
...>   @enforce_keys [:make]
...>   defstruct [:model, :make]
...> end
iex> %Car{}
** (ArgumentError) the following keys must also be given when building struct Car: [:make]
    expanding struct: Car.__struct__/1