【问题标题】:Typesafe generics using TypeTag (with safe cast)使用 TypeTag 的类型安全泛型(使用安全类型转换)
【发布时间】:2016-09-03 08:45:21
【问题描述】:

我想实现可以选择进行类型安全转换(例如x.cast[List[U]])的参数类(例如List[T])。

所谓类型安全,我的意思是如果类型在运行时不正确,则转换可能会抛出异常,但可以保证如果转换成功,则结果值为List[U] 类型。 (例如,asInstanceOf 不会这样做。List(1,2,3).asInstanceOf[List[String]] 会成功,但会返回一个不包含Strings 的List。)

我的方法是用TypeTag 标记所有应该支持强制转换的对象。具体来说,我将使用cast[U] 方法实现一个特征Typesafe,该方法期望U 类型的隐式TypeTag 并在运行时检查这些类型是否是子类型。这是我设法想出的代码:

import scala.reflect.runtime.universe.TypeTag

trait Typesafe[+T <: Typesafe[T]] {
  val typeTag: TypeTag[_ <: T]

  def cast[U](implicit typeTag: TypeTag[U]) = {
    if (this.typeTag.tpe <:< typeTag.tpe)
      this.asInstanceOf[U]
    else
      throw new ClassCastException(s"Cannot cast ${this.typeTag} to ${typeTag}")
  }
}

逻辑是:继承Typesafe[T] 的类T 必须用TypeTag[T] 实例化typeTag。然后cast[U]中的测试只有在T确实是U的子类型时才能成功(否则cast的隐含参数不存在)。

我们可以如下实现这个 trait(这是一个简单的 Set 包装类):

class TypesafeSet[T](val set : Set[T])
                    (implicit val typeTag:TypeTag[_<:TypesafeSet[T]])
  extends Typesafe[TypesafeSet[T]] {
}

子类型有效,但不幸的是我们每次都需要指定extends Typesafe[...] 子句。

import scala.collection.immutable.ListSet
class TypesafeListSet[T](set: ListSet[T])
                        (implicit override val typeTag:TypeTag[_<:TypesafeListSet[T]])
  extends TypesafeSet[T](set) with Typesafe[TypesafeListSet[T]] {
}

问题 1:我们能否改进这种模式,从而不必重复 extends Typesafe[...] 子句? (目前,如果我们不重复,TypesafeListSet[T] 无法转换为TypesafeListSet[T]。)

但是,在下面的例子中,我们有一个问题:

class TypesafeList[T](val list : List[T])
                     (implicit val typeTag:TypeTag[_<:TypesafeList[T]])
  extends Typesafe[TypesafeList[T]] {
  val self = this
  def toSet : TypesafeSet[T] = new TypesafeListSet(ListSet(list : _*))
}

方法toSet 无法编译,因为编译器无法解析new TypesafeListSet 的隐式TypeTag[TypesafeListSet[T]]。需要从 typeTag 中提取 TypeTag[T],然后从中重构 TypeTag[TypesafeListSet[T]]。我不知道这怎么可能。

问题2:如何在toSet中得到需要的TypeTag? (一种选择是将TypeTag[TypesafeListSet[T]] 类型的隐式参数添加到toSet,但这会将问题向外推,并泄漏实现细节,即toSet 使用ListSet。)

最后可以写如下代码,违反类型安全:

class TypesafeOption[T](val option : Option[T])
                       (implicit val typeTag:TypeTag[_<:TypesafeList[T]])
  extends Typesafe[TypesafeList[T]] {
}

在这里,我们在 Typesafe 特征的参数中“意外地”使用了 TypesafeList。这编译得很好,但这意味着现在TypesafeOption 将有一个typeTag 用于TypesafeList! (因此cast 中的检查将是不正确的,并且可能会发生错误的强制转换。)我相信这样的混淆很容易发生,如果编译器能够捕捉到它们会很好。 (在某种程度上,类型约束T &lt;: Typesafe[T] 已经避免了这种混淆(在this 之后),但不幸的是不是TypesafeOption 中的那个。)

问题 3 (answered): 我们能否改进 trait Typesafe 的定义,以使 Typesafe 无法以某种方式实例化 cast 行为不正确?

最后,几行代码应该如何使用这些类:

import scala.reflect.runtime.universe.typeTag
object Test {
  def main(args:Array[String]) = {
    val list = new TypesafeList(List(1,2,3))
    val set = list.toSet
    val listSet : TypesafeListSet[Int] = set.cast[TypesafeListSet[Int]]
  }
}

很遗憾,此代码无法编译。编译器找不到调用new TypesafeListTypeTag。我们需要在该行中明确添加(typeTag[TypesafeList[Int]])! (原因是new TypesafeList 需要一个TypeTag[_ &lt;: TypesafeList[Int]],而编译器不够聪明,看不出他只能构造一个TypeTag[TypesafeList[Int]]。)

问题 4:我们如何定义TypesafeList,这样就不需要明确给出TypeTags?

最后,我对整个示例有一些疑问:

问题 5: 运行时中有(至少)两个不同的TypeTag 类,即scala.reflect.runtime.universe.TypeTagscala.reflect.api.TypeTags#TypeTag。这里哪一个是正确的?

问题 6: 我正在比较 TypeTags 中包含的类型(即 typeTag.tpe)。我忽略了镜子。镜子也应该比较吗? (也就是说,如果两个类型标签有兼容的类型但不同的镜像,它们会赋值兼容吗?)

问题 7:(可能与问题 6 有关。)如果不同的类加载器加载了相同限定名称的类型会发生什么?在这种情况下,上面的代码是否正确? (即,据我所知,应该不可能将从类加载器 1 加载的 test.Test 转换为从类加载器 2 加载的 test.Test。)

问题 8 (answered): TypeTag 是正确的工具吗?还是我应该直接用Types 标记对象? (毕竟,我只比较 cast 中的类型。)但据我所知(从各种讨论中),TypeTags 是作为类型安全标记类问题的解决方案提出的。为什么?或者TypeTags 是干什么用的?

问题 9: 有没有关于这方面的表现?在运行时比较两种类型(使用&lt;:&lt;)听起来可能很昂贵......有替代方案吗? (我想可能构建一个从 TypeTags-pairs 到 Booleans 的映射,它记住哪些类型是赋值兼容的。但是,这样的查找会更快吗?TypeTags 没有用于快速查找的唯一 ID据我所知。(我认为,GHC 为此使用“指纹”。))

问题 10: 还有其他观察结果吗?我做错了什么?我的结论是cast 是类型安全的吗?

【问题讨论】:

  • lt;博士。你的第一部分似乎是github.com/milessabin/shapeless 所做的。
  • shapeless/Typeable 有类似的目标,但有两个重要区别: (a) 它通过递归遍历数据结构来检查强制转换是否安全(例如,在 list.cast(List[Int ]),它将遍历列表中的所有元素并检查它们是否为整数。) (b) 需要为任何可能用作类型参数的类型定义一个 Typeable 实例。 (即,对于list.cast(List[Bla]),我们需要 Typeable[Bla] 在范围内。)这使得它更难用作库(因为用户必须为他的所有类提供 Typeable-instances。
  • @DominiqueUnruh 你有很多问题,虽然它们都是相关的,但没有一个问题可以合理地涵盖所有问题。你能把它分成几个问题吗?

标签: scala


【解决方案1】:

问题 8 的答案 (TypeTag 是正确的工具吗?还是我应该直接用 Types 标记对象?)

一个原因是TypeTag 有一个类型参数,表示被标记的类型。也就是说,TypeTag[T] 静态地强制我们有一个TypeTag 用于T。如果我们改用Type,则不会强制要求Typesafe.typeTag 是正确类型的标记。

例子:

trait Typesafe[+T <: Typesafe[T]] {
  val typ: Type
  [...]
}

现在可以写了:

class TypesafeSet[T](val set : Set[T])
  extends Typesafe[TypesafeSet[T]] {
  val typ = typeOf[String] // !!!
}

如果没有TypeTag,似乎没有办法避免这种情况。

(不过,这并不能解释为什么TypeTag 除了类型之外还需要一个镜像。)

【讨论】:

    【解决方案2】:

    编辑:这个解决方案是错误的。使用此方案时,仍然可以使用val typeTag = typeTag[Nothing],然后将该类强制转换为任何类型。

    问题 3 的答案我们能否改进 trait Typesafe 的定义,以便不可能以某种方式实例化 Typesafe,从而使强制转换行为不正确? em>)

    这可以通过指定“自我类型”来完成(参见spec)。在特征中,可以指定继承类的类应具有的类型T。这是通过在特征定义的开头写this:T =&gt; 来完成的。在我们的特殊情况下:

    trait Typesafe[+T <: Typesafe[T]] {
      this : T =>
      val typeTag: TypeTag[_ <: T]
    
      def cast[U](implicit typeTag: TypeTag[U]) = {
        if (this.typeTag.tpe <:< typeTag.tpe)
          this.asInstanceOf[U]
        else
          throw new ClassCastException(s"Cannot cast ${this.typeTag} to ${typeTag}")
      }
    }
    

    现在任何扩展Typesafe[T] 的类都必须是T 类型。也就是说,如果TX 的超类型,则只能写class X extends Typesafe[T],因此提供给Typesafe 的类型标记将保证是X 的超类型。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-17
      • 2018-08-26
      • 2010-12-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多