如何在 Crystal 中进行通用记忆?

问题描述

我想在 Crystal 中定义一个通用的记忆包装器。 我有以下水晶代码

  module Scalar(T)
    abstract def value: T
  end

  class ScSticky(T)
    include Scalar(T)

    def initialize(sc : Scalar(T))
      @sc = sc
      @val = uninitialized T
    end

    def value: T
      @val ||= @sc.value
    end
  end

换句话说,我希望 ScSticky调用底层 Scalar(T) 一次,并为所有后续调用返回缓存的输出。 但是,如果 TInt32

,上述方法不起作用

比如包装这个类时


class ScCounter
  include Scalar(Int32)

  def initialize
      @val = 100
  end

  def value: Int32
    @val += 1
    @val
  end
end

ScSticky(ScCounter.new).value 将始终等于 0(据我所知,因为 unitialized Int32 实际上初始化为 0 值)

我非常感谢您对此问题的帮助

Upd:似乎实现这一点的正确方法是使用 nil ,但是我在理解这种实现应该是什么样子时遇到了问题。我也希望能够记住 .value 方法,即使它返回 nil (换句话说,如果 T一个 nilable 类型)

解决方法

您正在使用不安全的功能“uninitialized”,这意味着“保留之前内存中的任何内容”(理论上该值是随机的并且可能无效,实际上您经常以 0 结束 - - 但它仍然无法保证)。
关于 uninitialized 功能的简短故事是请永远不要使用它

如果你写了 @val = 0,这种行为不会让你感到惊讶——这就是你写的。

您必须定义 @val : T? = nil -- 以使其变为 nilable(具有 nil 这个单独的可能值,它是它自己的类型 - Nil)。
您可能认为 unitialized 会将 nil 带入图片中,但它绝对没有。


为了回应您关于将 nil 也包含到可能值中的评论,这里有一个完整的解决方案,它使用用户永远无法创建的独特“哨兵”结构而不是 Nil。

module Scalar(T)
  abstract def value: T
end
 
private struct Sentinel
end
 
class ScSticky(T)
  include Scalar(T)
 
  @val : T | Sentinel = Sentinel.new
  
  def initialize(@sc : Scalar(T))
  end
 
  def value: T
    val = @val
    if val.is_a?(Sentinel)
      @val = @sc.value
    else
      val
    end
  end
end
 
class ScCounter
  include Scalar(Int32)
 
  def initialize
    @val = 100
  end
 
  def value: Int32
    @val += 1
  end
end
 
sc = ScSticky.new(ScCounter.new)
 
p! sc.value #=> 101
p! sc.value #=> 101