为什么这个 Sorbet 错误算作动态常量引用?

Why does this Sorbet error count as a dynamic constant reference?

我在 Sorbet 中遇到以下错误:

lib/guardian.rb:24: Dynamic constant references are unsupported https://srb.help/5001
    24 |      self.class::MIN_AUTH || raise("Minimum auth must be specified")

守护者 class 具有以下结构

class Guardian
  MIN_AUTH = AuthLevel.new(:STRONG)
  # ...

  def min_auth
    self.class::MIN_AUTH || raise("Minimum auth must be specified")
  end
end

它被设计成可以在子对象上改变。它还被设计成如果它没有在子对象上指定,我们就会听到它。我很想知道为什么这样的设计模式是(隐含的)不好的做法。我是否应该跳过常量而只在辅助方法中定义它:get_min_auth?

Should I skip the constant and just define it in the helper method instead: get_min_auth?

是的,就是这样。

# typed: true
class AuthLevel < T::Struct
  const :level, Symbol
end

class Guardian
  def get_min_auth
    AuthLevel.new(level: :STRONG)
  end

  def min_auth
    get_min_auth || raise("Minimum auth must be specified")
  end
end

→ View on sorbet.run

为了真正说明为什么在这里使用方法更好:您可以使用抽象方法来要求子 classes 实现该方法。没有办法要求子 classes 定义常量。这将使您的整个示例基本上消失,因为您将不再需要 raise。它会变成静态类型错误:

# typed: true
class AuthLevel < T::Struct
  const :level, Symbol
end

class Guardian
  extend T::Sig
  extend T::Helpers
  abstract!

  sig {abstract.returns(AuthLevel)}
  def min_auth
  end
end

class MyGuardian < Guardian
end

→ View on sorbet.run


I'm interested to know why such a design pattern is (implied) bad practice.

冰糕分阶段进行。首先,它了解代码库中的所有 classes/modules/constants。然后,它了解那些 classes/modules 上的所有方法。然后它了解这些方法的类型。最后,它了解这些方法中局部变量的类型。

当 Sorbet 查看 (...)::MIN_AUTH 是否是实际存在的常量时,它 除了到目前为止已定义的常量 之外什么都不知道,而不是方法而不是局部变量。 self 本质上是一个局部变量,而 .class 是一个可以在子 class 上覆盖的方法。因为它既不知道局部变量也不知道方法,所以它报告一个动态常量引用。 self.class 是“任意表达式”,不是静态常量。

所以也许下一个问题是:为什么 Sorbet 强加这种看似随意的顺序 首先解析常量?两个最大的原因:

  • 速度。与允许循环引用相比,说“这是一个动态常量引用”并要求程序员重构代码需要更少的分析。鉴于有一个相对容易的重构(你提到的),这似乎是一个值得的权衡,以使每个后续类型检查 运行 更快。

  • 可读性self.class::MIN_AUTH 本质上是通过 Ruby 的恒定分辨率算法进行动态调度。事实上,常量解析在很多方面都比方法解析更难理解,因为它同时受到模块嵌套和继承层次结构的影响(而方法查找仅受继承影响)。依赖于复杂的查找和调度比仅使用人们更熟悉的方法更难阅读(尤其是来自其他语言 Ruby)。