如何在不收到此错误消息的情况下将哈希转换为别名类型?

How do I cast a Hash as an aliased type without getting this error message?

当我使用 alias 定义哈希时,我遇到了一个有趣的错误:

alias MyHash = Hash(Symbol, String | Int32)
hash = {:one => 2}.as(MyHash)

如果我 运行 这输出不是我预期的:

Error in ffile2.cr:2: can't cast Hash(Symbol, Int32) to Hash(Symbol, Int32 | String)

hash = {:one => 2}.as(MyHash)
       ^

你不能?为什么不?这不是定义统一类型的目的吗?

请注意,如果我将类型放在方法签名中,一切都很好:

def foo(h : Hash(Symbol, String | Int32) )
  puts h[:bar]?.nil?
end

foo( {:one => 2} )

更新:这行得通,但看起来有点傻:

alias MyHash = Hash(Symbol, String | Int32)
hash = {:one => 2.as(String | Int32)}.as(MyHash)

这与别名无关。如果您将原始示例中的别名替换为别名类型,它也会失败。

.as 无法神奇地将 Hash(Symbol, Int32) 转换为 Hash(Symbol, String | Int32)。这些是不同的类型并且表现不同。一开始这可能并不明显,因为在检索条目时,两种类型都 return 类型匹配 String | Int32 的值。存储条目时,它们的行为不同:Hash(Symbol, String | Int32) 可以接收类型为 String | Int32 的值,Hash(Symbol, Int32) 不能。

这方面的语言设计术语是协变和逆变。

为避免必须在文字中指定预期值类型,您还可以使用泛型转换,例如:

literal = {:one => 2}

mapped = literal.map do |key, value|
  {key, value.as(String | Int32)}
end.to_h

typeof(mapped) # => Hash(Symbol, Int32 | String)

这将采用匹配类型的任何散列并将其转换为 Hash(Symbol, Int32 | String)

方法参数中的类型不是 "fine"它们只是表现不同。只要您没有对它们做任何错误,它们就会匹配未指定的类型。尝试给字符串赋值,还是会失败:

def foo(h : Hash(Symbol, String | Int32) )
  puts h[:bar] = "bar" # error: no overload matches 'Hash(Symbol, Int32)#[]=' with types Symbol, String
end

这些不同的语义显然不是很直观,但这是正在进行的工作的当前状态。有一个问题也更详细地解释了这是关于什么的:https://github.com/crystal-lang/crystal/issues/3803