水晶:确保返回值不为 Nil



我有一个帮助程序类,定义如下:

require "toml"
module Test
  class Utils
    @@config
    def self.config
      if @@config.is_a?(Nil)
        raw_config = File.read("/usr/local/test/config.toml")
        @@config = TOML.parse(raw_config)
      end
      @@config
    end
  end
end

当我在代码中的其他地方调用此方法时:

server = TCPServer.new("localhost", Utils.config["port"])

我收到以下编译时错误:

in src/test/daemon.cr:10: undefined method '[]' for Nil (compile-time type is (Hash(String, TOML::Type) | Nil))
      server = TCPServer.new("localhost", Utils.config["port"])

Utils.config没有办法运行Nil的东西,所以我不明白这个错误。

  1. 如何告诉编译器Utils.config将始终返回不Nil的内容?
  2. 小的附加问题)对于将在类之间共享但只应创建一次的资源(config),这是否是一个很好的设计模式?

代码的问题在于,在 if 分支中检查@config是否为 nil(顺便说一句,使用 @config.nil? 更容易)时,该实例变量的值可能已更改,直到它到达返回行。例如,编译器必须假设它可以再次为 nil,例如,如果它是从不同的光纤更改的。

您可以将其保存到局部变量并返回此

class Utils
  @@config
  def self.config
    if (config = @@config).nil?
      raw_config = File.read("/usr/local/test/config.toml")
      @@config = TOML.parse(raw_config)
    else
      config
    end
  end
end

或者稍微重构一下,但本质上是一回事:

class Utils
  @@config
  def self.config
    @@config ||= begin
      raw_config = File.read("/usr/local/test/config.toml")
      TOML.parse(raw_config)
    end
  end
end

我更喜欢将@@config设置为使用空对象nil默认初始化,因为它清楚地表明该对象不可用。例如,如果配置文件恰好为空,则检查empty?将始终触发重新加载和解析,从而消除记忆功能。

||=运算符基本上意味着

if config = @@config
  config
else
  @@config = # ...
end

编辑:请参阅下面的Johannes Müller的答案,这是一个更好的解决方案。


通常,如果要避免Nil,则应键入类和实例变量:

@@config : Hash(String,Toml::Type)

这将有助于编译器通过查找可能导致Nil值的代码路径并在编译时提醒您来帮助您。

代码的潜在修复:

require "toml"
module Test
  class Utils
    @@config = {} of String => TOML::Type # This line prevents union with Nil
    def self.config
      if @@config.empty?
        raw_config = File.read("/usr/local/test/config.toml")
        @@config = TOML.parse(raw_config)
      else
        @@config
      end
    end
  end
end
puts Test::Utils.config["port"]

由于 toml 要求,我无法直接测试它,但这里有一个使用字符串的运行示例:https://play.crystal-lang.org/#/r/30kl

对于您的第二个问题,此方法可能有效:

require "toml"
module Test
  class Utils
    CONFIG = TOML.parse(File.read("/usr/local/test/config.toml"))
  end
end
puts Test::Utils::CONFIG["port"]

使用字符串而不是 TOML 的示例代码:https://play.crystal-lang.org/#/r/30kt

最新更新