在 elixir 中使用来自已声明结构的原子适用于 repl 但不适用于应用程序

Using atoms from declared structs in elixir works on repl but not in application

我想在 elixir 中使用 String.to_existing_atom 以避免内存泄漏。

这 100% 适用于 REPL:

iex(1)> defmodule MyModule do
...(1)> defstruct my_crazy_atom: nil
...(1)> end
{:module, MyModule,
 <<70, 79, 82, ...>>,
 %MyModule{my_crazy_atom: nil}}

所以现在原子 my_crazy_atom 存在了。我可以验证这一点:

iex(2)> String.to_existing_atom "my_crazy_atom"
:my_crazy_atom

相比于:

iex(3)> String.to_existing_atom "my_crazy_atom2"
** (ArgumentError) argument error
    :erlang.binary_to_existing_atom("my_crazy_atom2", :utf8)

但是我有一些代码看起来像这样:

defmodule Broadcast.Config.File do
  defstruct channel_id: nil, parser: nil
end

从启动 GenServer 进程后的方法调用, 我可以用 Poison 的

解码
keys: :atoms! 

甚至直接打电话给

String.to_existing_atom("parser")

在代码的同一个地方,我得到一个错误:

** (Mix) Could not start application broadcast: exited in: 
Broadcast.Application.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (ArgumentError) argument error
            :erlang.binary_to_existing_atom("parser", :utf8)

奇怪的是,如果我实例化结构并检查它,那么问题就消失了!

IO.puts inspect %Broadcast.Config.File{}
String.to_existing_atom("parser")

这是怎么回事?这是某种订购方式吗?

REPL 和您的应用程序之间的区别在于,在 REPL 中编译过程会立即发生。也就是说,虚拟机,如它所见

iex(1)> defmodule MyModule do
...(1)>   defstruct my_crazy_atom: nil
...(1)> end

立即编译。在编译阶段,正在创建原子并且一切正常。

在您的应用程序 OTOH 中,正在通过 VM 的不同调用提前执行编译过程。因此,除非显式使用该结构,否则不会创建原子。

可以将其视为 OOP 中的 class 声明与实例化:class 定义的存在并不能保证存在此 class 的实例。

要查看实际发生的情况和时间,请尝试将 IO.puts "I AM HERE" 放入模块声明中,紧接在 defstruct 之前。在 REPL 中,你会立即看到这一行。在您的应用程序中,您会在编译期间看到它,而在 运行 应用程序正常时不会看到它。


重现步骤:

$ mix new blah && cd blah
$ cat lib/blah.ex
defmodule Blah do
  defstruct my_crazy_atom: nil
end

defmodule Foo do
  def to_existing_atom, do: String.to_existing_atom("my_crazy_atom")
end
$ mix compile
$ iex -S mix
iex|1 ▶ Foo.to_existing_atom
** (ArgumentError) argument error
    :erlang.binary_to_existing_atom("my_crazy_atom", :utf8)
    (blah) lib/blah.ex:6: Foo.to_existing_atom/0
iex|1 ▶ %Blah{}
%Blah{my_crazy_atom: nil}
iex|2 ▶ Foo.to_existing_atom
:my_crazy_atom

为了全面起见,我把它放在这里(取自this brilliant answer):

defmodule AtomLookUp do
  defp atom_by_number(n),
    do: :erlang.binary_to_term(<<131, 75, n::24>>)

  def atoms(n \ 0) do
    try do
      [atom_by_number(n) | atoms(n + 1)]
    rescue
      _ -> []
    end
  end
  def atom?(value) when is_binary(value) do
    result = atoms()
             |> Enum.map(&Atom.to_string/1)
             |> Enum.find(& &1 == value)
    if result, do: String.to_existing_atom(result)
  end
end

iex|1 ▶ AtomLookUp.atom? "my_crazy_atom"
nil
iex|2 ▶ %Blah{}
%Blah{my_crazy_atom: nil}
iex|3 ▶ AtomLookUp.atom? "my_crazy_atom"
:my_crazy_atom

发生这种情况是因为 Elixir 默认情况下会在首次使用时从已编译的 .beam 文件中延迟加载模块。 (如果在 mix.exs 中将 start_permanent 设置为 true,您的代码将起作用,在 :prod 环境中默认设置为 true,因为那时 Elixir 会急切地加载所有模块包裹。)

在下面的代码中,原子 :my_crazy_atom 将出现在 Blah 模块的代码中,但它不会出现在 Foo 中。如果您启动 REPL 会话并且 运行 Foo.to_existing_atomBlah 模块 加载,这会导致 String.to_existing_atom("my_crazy_atom") 失败。

# Credits: @mudasobwa
defmodule Blah do
  defstruct my_crazy_atom: nil
end

defmodule Foo do
  def to_existing_atom, do: String.to_existing_atom("my_crazy_atom")
end

如您所见,如果您手动创建结构一次,所有后续对 String.to_existing_atom("my_crazy_atom") return 的调用都是正确的原子。这是因为当您创建一个结构时,Elixir 将加载该模块的 .beam 文件,该文件还将加载该模块使用的所有原子。

加载模块的更好方法(与创建结构相比)是使用 Code.ensure_loaded/1 加载模块:

{:module, _} = Code.ensure_loaded(Blah)