2016-04-03 83 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呢。

相關問題