在Scala的函数体中无法引用类型参数?

时间:2016-04-03 16:53:24

标签: scala

我来自C ++世界,是Scala的新手,这种行为看起来很不寻常。

class G1[A]( val a : A) {
  //val c:A = new A //This gives compile error
  def fcn1(b: A): Unit = {
    //val aobj = new A // This gives compile error
    println(a.getClass.getSimpleName)
    println(b.getClass.getSimpleName)
  }
}

def fcnWithTP[A](): Unit = {
  //val a = new A // This gives compile error
  //println(a.getClass.getSimpleName)
}

我无法使用函数体或类体中的类中的type参数来创建对象。我只能在函数参数中使用它。

这是什么原因?这是因为类型擦除吗?在运行时,该函数不知道实际类型A是什么,因此它无法创建该类型的对象?

这是一般规则是什么?这是否意味着类型参数根本不能出现在函数体或类定义中?如果他们真的可以出现,那么这些例子是什么?

3 个答案:

答案 0 :(得分:5)

是的,你是对的,这是因为删除 - 你在运行时对A没有任何了解,你还没有明确断言它是一个约束。方法签名。

JVM上的类型擦除只是部分的,所以你可以在Scala中做一些可怕的事情,比如要求类的值:

scala> List(1, 2, 3).getClass
res0: Class[_ <: List[Int]] = class scala.collection.immutable.$colon$colon

但是,一旦你进入泛型,一切都被删除了,所以例如你不能区分以下事项:

scala> List(1, 2, 3).getClass == List("a", "b", "c").getClass
res1: Boolean = true

(如果不清楚,我认为类型擦除毫无疑问是一件好事,而且JVM上类型擦除的唯一问题是它不完整。)

可以撰写以下内容:

import scala.reflect.{ ClassTag, classTag }

class G1[A: ClassTag](val a: A) {
  val c: A = classTag[A].runtimeClass.newInstance().asInstanceOf[A]
}

并像这样使用它:

scala> val stringG1: G1[String] = new G1("foo")
stringG1: G1[String] = G1@33d71170

scala> stringG1.c
res2: String = ""

但这是一个非常糟糕的主意,因为它会在运行时因许多类型参数而崩溃:

scala> class Foo(i: Int)
defined class Foo

scala> val fooG1: G1[Foo] = new G1(new Foo(0))
java.lang.InstantiationException: Foo
  at java.lang.Class.newInstance(Class.java:427)
  ... 43 elided
Caused by: java.lang.NoSuchMethodException: Foo.<init>()
  at java.lang.Class.getConstructor0(Class.java:3082)
  at java.lang.Class.newInstance(Class.java:412)
  ... 43 more

更好的方法是传入构造函数:

class G1[A](val a: A)(empty: () => A) {
  val c: A = empty()
}

很多更好的方法是使用类型类:

trait Empty[A] {
  def default: A
}

object Empty {
  def instance[A](a: => A): Empty[A] = new Empty[A] {
    def default: A = a
  }

  implicit val stringEmpty: Empty[String] = instance("")
  implicit val fooEmpty: Empty[Foo] = instance(new Foo(0))
}

class G1[A: Empty](val a: A) {
  val c: A = implicitly[Empty[A]].default
}

然后:

scala> val fooG1: G1[Foo] = new G1(new Foo(10101))
fooG1: G1[Foo] = G1@5a34b5bc

scala> fooG1.c
res0: Foo = Foo@571ccdd0

我们在A的定义中引用G1,但我们只是参考我们已确认持有或可用的属性和操作在编译时。

答案 1 :(得分:4)

泛型与模板不同。在C ++中,Foo<Bar>Foo<Bat>是两个不同的类,在编译时生成。 在scala或java中,Foo[T]是具有类型参数的单个类。考虑一下:

  class Foo(val bar)
  class Bar[T] {
    val foo = new T // if this was possible ... 
  }

  new Bar[Foo]

在C ++中,(相当于)这将无法编译,因为没有Foo的可访问构造函数不带参数。当编译器尝试为Bar<Foo>类实例化模板时,编译器会知道并且失败。

在scala中,Bar [Foo]没有单独的类,因此,在编译时,编译器对T一无所知,除了它是某种类型。它无法知道调用构造函数(或任何其他方法)是可行的还是合理的(例如,您无法实例化特征或抽象类),因此在该上下文中new T具有失败:它根本没有意义。

粗略地说,您可以在可以使用任何类型的地方使用类型参数(例如,声明返回类型或变量),但是当您尝试仅执行某些操作时适用于某些类型,而不适用于其他类型,您必须使您的类型参数更具体。例如,这样:def foo[T](t: T) = t.intValue不起作用,但是:def foo[T <: Number](t: T) = t.intValue确实如此。

答案 2 :(得分:1)

编译器不知道如何创建类型为A的实例。您需要提供返回A实例的工厂函数,或使用Manifest从反射创建A实例。

有工厂功能:

class G1[A](val a:A)(f: () => A) {
  val c:A = f()
}

使用Manifest

class G1[A](val a: A)(implicit m: scala.reflect.Manifest[A]) {
  val c: A = m.erasure.newInstance.asInstanceOf[A]
}

使用type参数时,通常会指定有关A类型的更多详细信息,除非您为A实现某种不直接与{{1}交互的容器}。如果您需要与A进行交互,则需要对其进行一些说明。您可以说A必须是A

的子类
B

现在编译器会知道class G1[A <: B](val a : A) A的子类,因此您可以在B上调用B中定义的所有函数。