[译] 印度朋友手把手教你学Scala(1):Scala入门

/**
 * 谨献给我最爱的YoYo
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-1-getting-started/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-02-23
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

Scala入门

这是关于Scala系列教程的第一章。点击这里可查看整个系列。

如果你之前是Java开发人员,这里将会有很多让你头疼的东西,而最容易的学习方式就是忘却已知的一切,从头学起(译者注:即保持空性)。

视频:《就像水那样,我的朋友!!》

让我们开始吧。

目录

以下是本章将会覆盖的内容。

  • Scala简介和环境搭建
  • Scala REPL,一段简短的介绍
  • Hello World !!
  • 变量
  • 引用和值不可变性
  • 幕后的不可变性
  • valfinal的对比
  • Scala里的数据类型
  • 类型接口
  • 变量初始化
  • 类型注解
  • 类型归属
  • 惰性求值

Scala简介和环境搭建

如果你完全没有接触过Scala,我建议你先阅读一下这篇博客

在网上,有很多谈论Scala安装的教程,所以我就不再赘述。

我推荐的安装如下:

  • 操作系统:首选Mac/Linux
  • 选择社区版本的IDE,并安装Scala插件

如果你已完成了安装,那么就开始吧。

Scala REPL(交互模式),一段简短的介绍

以下是关于Scala REPL一段简短的介绍。 
Scala REPL介绍

在学习Scala的过程中,REPL就是你的瑞士军刀。

Hello World !!

如果你是来自Java背景的,那么Scala在表达方面会与Java有所不同。先来看一些代码。

object Test {  
  def main(args : Array[String]){
    println("Hello World")
  }
}

我推荐把这些代码放到你的IDE里去,然后右击运行它。如果一切顺利的话,你应该会在控制台看到Hello World。

恭喜你!!你刚刚编写了第一个Hello World程序。

现在来对这段代码进行分解。

首先要注意的事,就是整段代码是在一个Object代码块里。Java程序员可能觉得这样会很困惑。暂且把这个问题放一边,下一章会以更详细讲解这点。不像Java,类名不需要与文件名匹配,虽然这并不是什么大不了的事情,但这里我们有这样的自由。^_^

下一件事是def main()奇怪的语法。以def为开头的def是声明方法的关键字。在接下来的教程里我们会更详尽地讲解方法。

既然有了方法,那么自然就会有参数。在这里,它就是Array[String]。这与Java的main方法相似,对于main方法有一个string类型的数组作为参数。可用于启动配置或者其他任何东西,当然使用说明完全是可选的。

接下来调用了一个叫println()的方法,它会在控制台上打印语句。如果使用的是IDE(你应该是),你可以通过在这个方法上按住“ctrl + 单击”来追踪整个调用。Python的同学应该会觉得这种语法很熟悉,并且事实上对于一名程序员来说,这无非就是把一段字符串打印到控制台上而已,别无其他。

但让我们更进一步。对于Java的同学,通常在控制台上打印应该像这样System.out.println。那么,为什么这里会不一样呢?

答案就是,其实并没有什么不一样,println()是一个叫做Predef.scala类里的一个方法,随后这个方法会调用Console.scala的println方法,近而调用PrintStream.class或者PrintStream.java上的out.println,如果你有源代码绑定的话,否则来自Intellij的编译器就会显示反编译后的代码。来自Scala和Java的打印都是以相同的方法调用而结束。正如前面说过的,Scala是构建在JVM之上的,所以可以像我们刚刚看到的那样,Scala能和Java代码无缝地互操作,如果这种实现不同的话,那么应该有一个特定的理由,否则它就是在重复造轮子。

事实上,在我们学习Scala的旅途上,还有很多很多采用了Java类库的示例。

一个简单的Hello World开启了很多有待学习的主题,尤其是这三部分:

  • 方法(将会有后面覆盖到)
  • 对象和类(将会有后面覆盖到)
  • 类型接口 -- 为什么Scala是一种静态类型动态语言的原因,下面会解释。

Scala里的变量

在深入变量之前,我应该先解释一下数据类型,但有一些基本的区别我想先说说,以便我们可以更深入地理解他们。

varval是在Scala用来声明变量的两个关键字。

var 用来声明可以修改的变量,而val用来声明不可变的变量。但这些变量是什么类型呢?对于原始数据类型,可变性的概念来自哪里?我建议你阅读一下StackOverflow上的这篇文章。原始自己(Primitives by themselves)是不可变的,例如他们的类型一旦声明了就不能改变,但他们的值是可变的,例如他们可以被修改。

关于为什么不可变的概念来自变量而不是对象,一开始会令人困惑,这点会在下面的数据类型部分中进行解释,在Scala里没有原语类型,一切皆是对象。

引用和值不可变性

如果val是不可变的,那么它就不能被改变吗?这是不是和Java里的final关键字类似,或者它是否和String的不可变性有所联系?

让我们来看一些示例代码,如下所示,来帮助我们更好地理解。

class Test {  
  def main (args: Array[String]) {
    var myVar = 10
    // 完美运行
    myVar = myVar + 10

    val myNum = 6
    // 会导致编译时错误
    // Reassignment to val
    myNum = myNum + 10
  }
}

如果你运行上面的代码,你会在编译里发现一个诸如“reassignment to val”的错误。如果你正在使用IDE,这些在你敲代码时就会显示出来,因为IDE提供了预编译。

首先,它绝然不像是String的不可变性,String的不可变性对程序员是不可见的,而这里是在编译级别控制的。下一个问题,它和Java里的final类似吗?

从鸟瞰图来看,这看起来是类似的,因为一旦某个值赋给它后,它就不能被改变了,但在JVM内部final与不可变性没有任何关系,并且使用它是为了类不能被扩展,以及以防它的方法被重载。

考虑以下用来演示他们是不同的Java代码。

final ArrayList<Integer> arrList = new ArrayList<Integer>();

/*
这样不会导致错误,因为我们改变的是对象本身。如果它被改变了的话,会触发像它不能被修改这样的错误
*/
arrList.add(20);

/*
这样会导致错误,因为我们修改了引用,而非对象本身
*/
arrList = null;  

如果你尝试对原始类型用同样的final,那么它的值就不能被改变。这是不是意味着原始类型就是不可变的?这把我们带回了上面stackoverflow的讨论,但是之所会出现错误的原因是因为Java对原始类型使用了按值传递,既然他们根本不是对象,所以按引用传递毫无意义。所以如果一个变量改变了,它的引用(也可以说成是在内存里的位置)随之改变,这是因为按值传递的机制,而不是因为原始类型是不可变的。

理解的要点就是在Scala里,引用和不可变性的区分并不是特别重要,因为Scala根本就没有原始类型,只有对象。

所以不管何时我们谈论Scala的不可变性,我们就是在谈论引用不可变性。

不可变的变量确实有性能上的优势,而且也更进一步地学习编写没有副作用代码这一概念。

幕后的不可变性

让我们来通过查看反编译生成的Scala类文件的字节码,进一步加深对不可变性的理解。

我们声明了一个叫做Parent的类,以及在它里面的一个变量。

class Parent {  
  val x = 10
}

看着生成的字节码,我们可以看到它翻译成了一种Java原始类型,就运行时而言没有什么特别的。 
javap -c filename.class生成了以下反编译的代码。

public class Parent {  
  public int x();
    Code:
       0: aload_0
       1: getfield      #13     // 字段 x:I
       4: ireturn

  public Parent();
    Code:
       0: aload_0
       1: invokespecial #19     // 方法 java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #13     // 字段 x:I
      10: return
}

很明显,val只是一个编译时的限制,与生成的字节码没有半毛钱关系。

通过阅读反编译的字节码这一途径,我们可以理解地更深入,但在大多数情况里,这并不是必须的。

valfinal的对比

另一方面,Scala也有final关键字,其工作非常类似。区别可视化val和final更好的方式是通过一个示例。

让我们来看一个简单的Parent类。

class Parent {

  val age = 10

}

不像Java,Scala也能重载变量。

class Child extends Parent{

  override val age = 30

  def printVal ={
    println(age)
  }

}

现在如果我们在Parent类里把age变量声明为final,那么其子类就不能重载它,否则就会抛出以下的异常。

这是现实世界中会使用到final的示例。

注意,通过在子类里重载某个val,在这里我们并没有破坏不可变性。子类创建了一个它自己的实例,就像Java字符串池里的一个字符串一样。

Scala里的数据类型

Scala有和Java一样的数据类型,以及一样的内存和缜密。

全部的数据类型都是对象,并且在他们里面可以调用方法,就像你是在一个对象上面那样。

val t = 69  
// 打印“E”,E对应的ASCII码是 69
println(t.toChar)

val s = "Hello World"  
// 就像是在字符串里的字符,打印 l
// 可追踪到相同的String类charAt方法
println(s.charAt(2))  


到现在为止,另一个问题是否已水落大出?在Scala代码里类型在哪?

不像Java那样用数据类型声明变量然后给变量一个名字,Scala有一些叫做类型接口(参考下面的主题)的东西,但先别跳到那去,这是为什么我把它从变量部分单独出来的一个原因。

类型接口

如果你对这个短语不熟悉的话,它不过就是在编译时的类型推断而已。且慢,这不正是动态类型的意思吗?其实不是的,请注意我说的是类型推断,这明显与动态类型语言所做的大为不同,而另一件事是它是在编译时而不是在运行时完成的。

很多语言内置了这一点,但实现方式各有各有不同。可能一开始会困惑,但通过示例代码这将会慢慢变得清晰明朗。

让我们进入到Scala交互模式,做一些实验。 

从上面的图中,明显没有什么魔法,这些变量在编译时会自动被推断成他们认为最合适的类型。

为了进一步理解,这里有更多的代码。

val x = 20

// 打印到控制台
// 合法调用,将x推断为一个整型
println(x+10)

// 以下是一些愚蠢的做法,会抛出一个编译时错误
val z = 40  
println(z * "justastring")  

继续往前,和这些变量玩一玩,你会受到编译时类型安全的保护,所以不要犹豫,尽情尝试吧。

如果对类是怎么对变量进行扩展的感到好奇,你可以更深入地挖掘并且会找到一个叫AnyVal的类。这是Scala统一类型系统完全不同的主题的一部分,也就是类层次结构。

译者注:Scala的类层次结构如下: 

类型接口会在这系列教程的第2章,进一步讲解。

变量初始化

在Scala里,你不能简单地创建一个变量,而未对它进行初始化。 

这是Scala语言开发者所做的一个设计选择。明显的原因是为了避免变量未初始化和空指针异常。

唯一一个地方不需要把值赋给变量的地方是在抽象类里。当学习Scala的类时会看到更多相关的内容。

类型注解

Scala可以让我们明确地指定类型。 
val y : Integer = 20

这些类型注解的类型在公开的API/方法里很重要。

温馨提示:方法会在第3章解释。

def getInfoFromBackend() =  {

    val dataList = List(1,"Literature",2,"Science")

    dataList

  }

没有明确地注释类型信息的话,开发人员觉得这样会引起理解上的困惑。记住,并不是在JVM上的所有语言都有像Java这样的类型接口,所以对类型的小小改变就会破坏用户(假设)的代码。

更好的版本如下:

 def getInfoFromBackend() = List {

    val dataList = List(1,"Literature",2,"Science")

    dataList

  }

译者注:多了List类型注解。

这种理念不仅适用于方法参数,也适用于使用val关键字声明的变量。上面的示例只是演示了一种更容易理解的方式这一想法。

类型归属(Type ascriptions)

类型归属是更为复杂的东西。它指的是告诉编译器,你将要执行的某个操作所期望输出的是什么类型这一过程。

一个典型的使用场景是Java里的类型转换。

int x = 20;

// 有效的转换
System.out.println((byte) x);

// 运行时错误
Object s = new Object();  
System.out.println((byte) s);  

上面代码会导致一个运行时的异常/错误,如下所示。

20  
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Byte  
    at JavaExample.main(JavaExample.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

在Scala里,当我们使用类型归属,此代码甚至不能编译。 
类型注释和类型归属之间没有语法区别,并且经常会导致这两者之间的混淆。

使用在运行时的类型转换,我们可以取得同样的Java路线。

object Demo extends App{

  val x = new Object().asInstanceOf[Byte]

}

在上面场景中,你可以观察到它并没有抛出一个编译时的错误,但会导致运行时同样的异常堆栈,例如:java.lang.ClassCastException

在执行类型转换时,类型归属令人难以置信地好用。你可以在编译时而不是在运行时检查类型安全,以确保不会导致有缺陷的代码。

惰性求值(lazy val)

正如名字所示,Scala的惰性求值和val类似,但它的值仅当此变量被用到时才会被求值。

import scala.io.Source._

object ReadFileExample extends App{

println(System.getProperty("user.dir"))

lazy val lines = fromFile(System.getProperty("user.dir") + "/file1.txt").getLines

println(lines)

}

我建议你亲自尝试一下这段代码。首先通过注释掉println(line),你会看到即使那个文件不存在也不会导致错误。

但这是一个可以工作的示例,你可以在项目的顶级放置一个真实的文件,这里是项目根目录下的一个指定路径,随后这段程序将会把文件内容打印出来。

这是在编译时控制的,因为在编译时可以知道变量访问。

在诸如浏览器里的文件上传窗口这样的场景里相当有用。用户有可能上传文件,也可以不上传,所以最好是延迟/懒初始化直到事件发生。

这里接近本章的尾声了,我们从重要的部分开始了。如果还没明白的话,我建议你重新阅读这篇文章,并且参考在网上的相关文档。

把其他语言简单的代码片段翻译成Scala也会有所帮助。

一旦我发布了我在这里提及到的更多的主题,我会尽快更新相关链接到这篇文章。如果想保持更新你也可以进行订阅。

Scala并不容易,但它也不难,如果我们脚踏实地,一步一个脚印慢慢地学的话。我的目标不是要教会你所有东西,而是简单地指明一个方向,之前很少被覆盖过的一个方向。

好好学习,敬请关注更多博客。^_^


------------------------ 

转载于:https://my.oschina.net/dogstar/blog/846131

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值