Scala中的語言特性是如何實現的(2)


上篇博文的末尾留了三個問題,現在自問自答一下。

在Scala中被聲明為val的v4為什么在反編譯的Java中不是final的呢?

在方法中聲明局部變量時,如果用Scala的val關鍵字(或者是Java中的final)來修飾變量,則代表着此變量在賦過初始值之后不可以再被重新賦值。這個val或者final只是給編譯器用的,編譯器如果發現你給此變量重新賦值會拋出錯誤。

而bytecode不具備表達一個局部變量是immutable的能力,也就是說對於JVM來說,不存在不可變的局部變量這個概念。所以v4在反編譯之后,就和普通的局部變量無異了。

在Scala中被聲明為val的v2為什么在反編譯的C#中不是readonly的呢?

這是個挺tricky的問題,我試着解釋一下。Scala .NET是基於IKVM實現的,IKVM可以把Java bytecode翻譯為CIL。 所以Scala編譯為CIL的過程實際是這樣的:

Scala —–Scala編譯器—–> bytecode —–IKVM—–> CIL

Scala編譯器編譯出的bytecode實際是用final修飾了v2的,但是bytecode中的final和CIL中的initonly(對應C#的readonly)是不一樣的。

Java中,final實例變量定義的時候,可以先聲明,而不給初值,然后我們可以在任何一個方法中給它賦初值。這提供了更大的靈活性,一個Java類中的final成員可以依對象而不同,卻保持其immutable的特征。

而CIL的initonly則要嚴格一點,CLI標准(ECMA-334)這樣描述:

initonly marks fields which are constant after they are initialized. These fields shall only be mutated inside a constructor. If the field is a static field, then it shall be mutated only inside the type initializer of the type in which it was declared. If it is an instance field, then it shall be mutated only in one of the instance constructors of the type in which it was defined. It shall not be mutated in any other method or in any other constructor, including constructors of derived classes.

可見,一個initonly的成員,不是隨便在哪兒都可以賦初值的。由於這點不同IKVM就沒有直接把final翻譯成initonly。如果想讓v2在C#代碼中變成readonly的,可以給IKVM加上strictfinalfieldsemantics這個參數。

為什么反編譯出來的C#代碼中的實例級公開方法都是標有override的呢?

這個問題還沒搞明白。

但是有個有趣的現象,如果用Scala .NET來編譯Scala源碼,編譯出的實例級方法都是標有override的;而如果先把Scala代碼編譯為.class然后再用IKVM把.class文件轉換為CIL的話,方法則是標有virtual的。我猜這可能和Java中的方法默認是可以被overirde的有關。

下面開始正文,前面填坑用了不少篇幅,所以這次只分析一個語言特性:Scala中的constructor。

Constructor

Scala中可以在聲明class的同時聲明一個constructor,比如這樣:

1
2
3
class ScalaConstructorExample(val x: Double, y: String) {  println(x + y) } 

構造函數接收兩個參數x和y,然后把x和y拼在一起打印出來。反編譯為Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ScalaConstructorExample {  private final double x;  public double x()  {  return this.x;  }  public ScalaConstructorExample(double x, String y)  {  Predef..MODULE$.println(new StringBuilder().append(x).append(y).toString());  } } 

可以發現編譯器給標為val的x生成了一個getter,很方便的語法糖。而直接寫在類內的打印語句則被放到了構造函數內。下面是反編譯為C#的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ScalaConstructorExample : ScalaObject {  private double x = x;  public override double x()  {  return this.x;  }  public ScalaConstructorExample(double x, string y)  {  Predef$.MODULE$.println(new StringBuilder().Append(x).Append(y).ToString());  } } 

和Java代碼基本無異。比較一下,Scala用3行代碼表達的含義,Java和C#要用14行才行。

現在加一個重載的構造函數:

1
2
3
4
5
6
7
class ScalaConstructorExample(val x: Double, y: String) {  println(x + y)  def this(x: Double) = {  this(x, "hello")  } } 

這個構造函數給了y一個默認值“hello”。反編譯為Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ScalaConstructorExample {  private final double x;  public double x()  {  return this.x;  }  public ScalaConstructorExample(double x, String y)  {  Predef..MODULE$.println(new StringBuilder().append(x).append(y).toString());  }  public ScalaConstructorExample(double x)  {  this(x, "hello");  } } 

對應的C#代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ScalaConstructorExample : ScalaObject {  private double x = x;  public override double x()  {  return this.x;  }  public ScalaConstructorExample(double x, string y)  {  Predef$.MODULE$.println(new StringBuilder().Append(x).Append(y).ToString());  }  public ScalaConstructorExample(double x) : this(x, "hello")  {  } } 

構造函數重載這個特性就顯得平淡無奇了,不過還是比較一下行數。定義兩個構造函數,打印出構造函數的參數,聲明一個getter,這三件事Scala只用7行代碼就完成了,Java和C#都需要將近20行。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM