查看源代码 结构体

我们在之前的章节中学习了关于映射的内容

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

defstruct 一起使用的关键字列表定义了结构体将具有的字段以及它们的默认值。结构体采用定义它们的模块的名称。在上面的示例中,我们定义了一个名为 User 的结构体。

现在我们可以使用类似于创建映射的语法来创建 User 结构体。

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

结构体提供编译时保证,只有通过 defstruct 定义的字段才允许存在于结构体中。

iex> %User{oops: :field}
** (KeyError) key :oops not found expanding struct: User.__struct__/1

访问和更新结构体

结构体与固定键的映射共享相同的语法用于访问和更新字段。

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

当使用更新语法 (|) 时,Elixir 意识到不会向结构体添加任何新键,从而允许底层的映射在内存中共享它们的结构。在上面的示例中,johnjane 在内存中共享相同的键结构。

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

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

结构体在底层是裸映射

结构体只是具有名为 __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"} of type User (a struct)

结构体连同协议一起为 Elixir 开发人员提供了最重要的功能之一:数据多态性。这将在下一章中探索。

默认值和必需键

如果你在定义结构体时没有指定默认键值,则将假设为 nil

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

你可以定义一个结构体,它将具有显式默认值的字段和隐式 nil 值的字段组合在一起。在这种情况下,你必须首先指定隐式默认为 nil 的字段。

iex> defmodule User do
...>   defstruct [:email, name: "John", age: 27]
...> end
iex> %User{}
%User{age: 27, email: nil, name: "John"}

反过来执行会导致语法错误。

iex> defmodule User do
...>   defstruct [name: "John", age: 27, :email]
...> end
** (SyntaxError) iex:107: unexpected expression after keyword list. Keyword lists must always come last in lists and maps.

你还可以使用 @enforce_keys 模块属性强制某些键在创建结构体时必须指定。

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

强制键提供一个简单的编译时保证,以帮助开发人员构建结构体。它不会在更新时强制执行,也不会提供任何形式的值验证。