java - 如何将类型映射到Scala中的值?
问题描述
我认为在 Scala 中创建Map[Class[_ <: SomeSuperClass], V]
了一个不可变映射,这K
就是. 我可能是错的,这可能与我的问题有关,也可能无关。classOf[T]
T
SomeSuperClass
我发现我无法真正访问这些V
值。如何将 的子类映射SomeSuperClass
到V
值?
例如,打牌。在二十一点中,J、Q 和 K 的值都与 10 相同。但在其他游戏中,它们可能具有不同的值,例如 J 的值是 11,Q 的值是 12,K 的值是 13。
给定包中的抽象Rank
类和子类Ace
, Two
, Three
, ..., King
,cards
包中的类blackjack
可以具有如下函数:
def valueOf(rank: Rank): Int = rank match {
case _: Ace => 11 // TODO: Figure out when value 1
case _: King => 10
case _: Queen => 10
case _: Jack => 10
case _: Ten => 10
case _: Nine => 9
case _: Eight => 8
// etc.
}
在 Java 中,我会(并且确实)只使用枚举类型。Scala 中有一种叫做密封类的东西,但实际上这似乎比 Java 中的枚举要麻烦得多。或者也许我还没有弄清楚如何正确使用它们。
但我更喜欢一种更灵活的方法,它允许我或其他任何人添加卡片等级,并以最少的麻烦(例如,通过简单的子类化Rank
)对这些等级进行估值。像这样的东西:
class Valuations(val map: Map[Class[_ <: Rank], Int]) {
def valueOf(rank: Rank): Int = {
val key = rank.getClass
if (map.contains(key) map.get(key) else {
throw new NoSuchElementException("No match for " + rank.toString)
}
}
}
我可以让 IntelliJ 给我一个绿色复选标记,但我无法让我的单元测试通过。
在 Java 版本中,枚举类型Rank
具有值函数,因此您可以依赖Rank
实例来获得该值。但是,即使我不想添加新的卡片等级,向其他类型的值添加新的映射int
也可能很笨拙,并且不符合单一责任原则的精神。
Scala 中肯定有更优雅的解决方案。如何将这些类型匹配为值的键,同时保持添加不同子类型的灵活性,而无需重写任何已经存在的内容?
解决方案
你真正想要的是从Rank
to的偏函数Int
。与为域的所有元素定义的总函数不同,部分函数只能为其中的一些元素定义。
此外,如果您不介意另一个建议,抛出异常不符合函数式编程的精神。我没有强加函数式方法,因为 Scala 非常适合以面向对象(类似 Java)的方式使用。但我会建议它。它说 - 不要扔!我总是说投掷就像完全撕裂你的程序结构。它撕开墙壁说“哎呀,现在我要走捷径了”。这几乎就像 goto 语句。如果您决定使用异常,我很确定您可以修改我的示例以合并它们。
我的建议是,不要抛出,而是明确表明您的函数最终可能会产生错误值。您可以使用Either[MyErrorType, MyValueType]
. 这样,您最终返回的值必须是Left(instanceOfMyErrorType)
or Right(instanceOfMyValueType)
。之后,在一些需要处理错误的代码中,您可以随时检查您手中是否有 aRight
或 a Left
,并采取相应的行动(例如,在 Left 记录错误并返回 400 HTTP 响应或其他情况下)。
这是代码:
sealed trait Rank
case object Ace extends Rank
case object King extends Rank
case object Queen extends Rank
case object Jack extends Rank
case object Ten extends Rank
// ...
final case class Error(msg: String)
val exampleMapping: PartialFunction[Rank, Int] =
(rank: Rank) => rank match {
case Ace => 11
case King => 10
case Queen => 10
case Jack => 10
// No Ten!
}
def valuations(rank: Rank, f: PartialFunction[Rank, Int]): Either[Error, Int] =
f.andThen(rank => Right(rank)).applyOrElse(
rank,
(v: Rank) => Left(Error(s"No such element: $v"))
)
val a = valuations(King, exampleMapping) // Right(10)
val b = valuations(Ten, exampleMapping) // Left(Error(No such element: Ten))
上面代码中最有趣的部分是valuations
方法。这是它的作用:
- 给我一个
rank: Rank
和一个偏函数 fromRank
toInt
,我将返回一个Int
或一个Error
- 我现在所做的是,我将提供的函数与将值转换为的函数组合在一起
Right(thatInteger)
(或者,用数学术语,将后者与前者组合;f andThen g
意味着g(f(x))
而f compose g
意味着f(g(x))
......我总是觉得它在精神上更容易使用andThen
) - 将上面定义的组合应用于
rank
参数,如果该函数没有为 定义的结果rank
,那么返回Left(error)
当然,你可以有很多不同的部分函数,其他用户可以定义他们自己的,等等。但请注意两点:
- 使用密封特性意味着没有人可以扩展
Rank
。这对我来说很有意义。但是,如果您不仅希望以后能够添加不同的映射,甚至还希望添加新Rank
的 s,那么就不要让 trait 密封。 - 将您的案例类设为最终类,因为它们不应该被扩展,并且通过使它们成为最终类,如果您尝试定义一个完整的函数(不是部分的!),编译器会警告您,但忘记处理某些情况,因为意外省略了
case
某个等级。对于 case 对象,也可以将它们标记为 final,但这不是强制性的,因为它们无论如何都不能扩展(它们只能有一个实例,如 Java 中的单例)。
如果出于某种原因您稍后决定不想在未定义映射时返回错误,而是想使用默认映射,该映射具有所有等级的所有默认值(= 它不是偏函数Rank => Int
,而是总共一个),那么这也很容易做到。试试看练习!
最后一点,Scala 3 将有非常好的枚举,这是你说你从 Java 中熟悉的东西,而 Scala 2 缺乏。
推荐阅读
- php - 如何使相同的文件包含在位于不同目录中的两个不同文件中以重定向到相同的最终路径?
- node.js - 无法让 websocket 客户端作为子进程运行
- reactjs - React useState Hook setter 没有数组变量的结果
- docker - 如何解决docker容器内的权限被拒绝?
- shell - adb:找不到命令 - Window 10 shell
- android - 用于 Android 的 Qt 缺少编译器
- ruby-on-rails - 在没有开发选项的 Plesk 管理的机器上使用带有本机扩展的预编译 gem 时出错
- git - 参考 `remotes/origin/HEAD` 的用途是什么?
- uipangesturerecognizer - 如何在 panGesture 之后修复新的 textField 位置?
- sqlite - 如何在有序表中选择行并检索其上方和下方的行数?