如何通过符号化名称设置变量

How to set a variable by its symbolized name

我正在学习 crystal(只是为了好玩)并尝试为结构实现某种 []= 方法。这是第一次尝试:

struct Foo
  @str : String | Int32 # Have to share all the types, NOT OK
  @int : Int32 | String # Have to share all the types, NOT OK

  def initialize(@str = "foo", @int = 111)
  end

  def []=(variable, value)
    {% for ivar in @type.instance_vars %}
      @{{ivar}} = value if {{ivar.id.symbolize}} == variable
    {% end %}
  end
end

foo = Foo.new
foo[:str] = "bar" # OK
foo[:int] = 222   # OK

可以,但缺点是所有的实例变量应该共享相同的类型,否则编译器会报错。我的第二次尝试是重新实现这样的方法:

def []=(variable, value)
  {% for ivar in @type.instance_vars %}
    {% if ivar.id.symbolize == variable %} # ERROR: undefined macro variable 'variable'
      @{{ivar}} = value
    {% end %}
  {% end %}
end

但这不起作用,因为无法解析 {%%} 语法中的 variable。如何克服这个问题?也许还有其他惯用的方法来实现这种方法?

您不需要实例变量具有所有可能类型的联合,您可以在 setter 方法中限制为匹配类型:

struct Foo
  @str : String
  @int : Int32

  def initialize(@str = "foo", @int = 111)
  end

  def []=(variable, value)
    {% for ivar in @type.instance_vars %}
      if {{ivar.id.symbolize}} == variable
        if value.is_a?({{ ivar.type.id }})
          @{{ivar}} = value
        else
          raise "Invalid type #{value.class} for {{ivar.id.symbolize}} (expected {{ ivar.type.id }})"
        end
      end
    {% end %}
  end
end

foo = Foo.new
foo[:str] = "bar"
foo[:int] = 222
foo[:int] = "string"  # => Invalid type String for :int (expected Int32)

虽然这当然是可能的,但缺点是这些类型不匹配只能在运行时检测到。如果您直接访问属性(foo.int = "string" 甚至不会编译),您将获得更好的类型安全性。 应该可以将 []= 作为一个宏来实现,它可能会在编译时抱怨类型不匹配,但我不确定这就是你要找的。

更好的解决方案可能是只使用 NamedTuple。或者您可能根本不需要使用符号访问字段。我不知道是否有真正的用例。