2016-04-03 40 views
2

我来自C++世界,也是Scala的新手,这种行为看起来很不寻常。类型参数不能在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) 
} 

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

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

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

+0

它看起来像是因为编译器无法解决'A'是一个类。这个问题看起来相关:http://stackoverflow.com/questions/6200253/scala-classof-for-type-parameter(它使用'classOf'而不是'new',但原理是相同的,我认为) –

+1

并且更一般地回答你的要点,是的,类型参数可以出现在函数体中,当你需要一个类型时声明返回类型等等,但是你在代码中要求更多:类型被用作类。 –

回答

4

是的,你说得对,这是因为擦除 - 你不知道A在运行时什么都没有明确声明它是方法签名中的一个约束。

在JVM类型擦除仅仅是局部的,所以你可以做一些可怕的事情在斯卡拉像索要类的值:

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] = [email protected] 

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] = [email protected] 

scala> fooG1.c 
res0: Foo = [email protected] 

这里我们指的是AG1定义,但我们只是参考已确认的属性和操作,或者在编译时可用。

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] 
} 

当使用类型参数,通常你会在类型A指定更多的细节,除非你实现某种容器为A,不直接与A交互。如果您需要与A互动,您需要一些规格。你可以说A必须B

class G1[A <: B](val a : A) 

现在编译器的子类都知道AB子类,所以你可以拨打a:AB定义的所有功能。

+0

值得注意的是,尽管'Manifest'没有被正式弃用(自2.10.0开始,源代码中已有一个弃用行,但它已被注释掉),但它正在被'ClassTag'取代,并且通常如果'ClassTag'起作用(就像这里所做的那样),我们有充分的理由去选择它。 –

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呢。

相关问题