专业编程基础技术教程

网站首页 > 基础教程 正文

10分钟的Scala入门 scala 教程

ccvgpt 2024-10-12 13:43:31 基础教程 10 ℃


Scala是Martin Odersky在2004年发布的一种编程语言。 它提供了对函数式编程的支持,并且设计简洁并编译为Java字节码,因此Scala应用程序可以在Java虚拟机(JVM)上执行。

10分钟的Scala入门 scala 教程

让我们检查一下该语言的核心功能。

你好,世界

首先,让我们看看如何在Scala中实现问候世界:

我们定义了一个包含main方法的HelloWorld对象。 此方法将String数组作为输入。

首先,我们调用方法println,该方法将对象作为输入以在控制台中打印内容。

同时,HelloWorld也是io.teivah.helloworld包的一部分。

package io.teivah.helloworld

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

我们可以使用val关键字来命名表达式的结果。 在下面的示例中,两个表达式都是定义值的有效方法:

val v1: String = "foo"
val v2 = "bar"

类型是可选的。 在示例中,v1和v2都键入为字符串。

Scala编译器可以推断值的类型,而不必显式声明它。 这称为类型推断。

Scala中的值是不可变的。 这意味着以下代码将无法编译:

val i = 0
i = 1 // Compilation error

最后但并非最不重要的一点是,可以使用lazy关键字延迟地求值:

lazy val context = initContext()

在这种情况下,上下文不会在声明期间而是在第一次调用期间进行评估。

变量

变量是可变值。 它用var关键字声明。

var counter = 0
counter = counter + 5

就像值一样,类型是可选的。 但是,变量不能被懒惰地求值。

此外,Scala是一种静态类型的语言。 例如,以下代码无效,因为我们尝试将Int映射到已经定义为String的变量中:

var color = "red"
color = 5 // Invalid

在Scala中,我们可以通过将表达式用{}括起来来组合表达式。 让我们考虑将对象作为输入的println()函数。 以下两个表达式是相似的:

println(7) // Prints 7

println {
  val i = 5
  i + 2
} // Prints 7

请注意,对于第二个println,最后一个表达式(i + 2)是整个块的结果。

当我们使用诸如println之类的带有单个参数的函数时,我们也可以省略括号:

println 7

基本类型

Scala被认为是一种纯粹的面向对象的语言,因为每个值都是一个对象。 因此,Scala中没有原语(例如Java int)。

Scala有8种基本类型:

  • Byte
  • Short
  • Int
  • Long
  • Float
  • Double
  • Char
  • Boolean
  • 每个基本的Scala类型都继承自AnyVal。 另一方面,AnyRef是java.lang.Object的别名。 最后,AnyVal和AnyRef都从Any继承。

    字符串插值

    Scala提供了一种优雅的方式来将变量/值引用直接嵌入到处理后的字符串文字中。 作为一个具体的例子:

    val name = "Bob"
    println(s"Hello $name!") // Hello Bob!

    s插补器可以在引号之前使之成为可能。 否则,它将打印 Hello $ name!。

    Scala提供的插值器很少,但这是一种可自定义的机制。 例如,我们可以创建自己的插值器来处理JSON转换,例如:println(json" {name:$ name}")。

    数组和列表

    数组在Scala中也作为对象处理:

    val a = new Array[Int](2)
    a(0) = 5
    a(1) = 2

    这里要强调两点。

    首先,设置元素的方式。 而不是像许多语言一样使用a [0],我们使用语法a(0)。 这是一种语法糖,可以让我们像调用函数一样调用对象。 在后台,编译器正在调用默认方法apply(),该方法接受单个输入(在我们的示例中为Int)以使其成为可能。

    其次,尽管在此示例中将Array对象声明为val,但它是可变的,因此我们可以更改索引0和1的值。val只是强制不对引用(而不是对应的对象)进行突变。

    也可以通过以下方式初始化数组:

    val a = Array(5, 2)

    该表达与以上相似。 此外,由于使用5和2进行了初始化,因此编译器将a推断为Array [Int]。

    要管理多维数组:

    val m = Array.ofDim[Int](3, 3)
    m(0)(0) = 5

    此代码创建一个二维数组,并将第一个元素初始化为5。

    有许多不同的数据结构构成Scala标准库。 其中之一是不可变列表:

    val list = List(5, 2)
    list(0) = 5 // Compilation error

    与数组相比,在初始化列表之后修改索引将导致编译错误。

    Map

    可以像这样初始化Map:

    val colors = Map("red" -> "#FF0000", "azure" -> "#F0FFFF", "peru" -> "#CD853F")

    请注意->运算符,将颜色键与其对应的十六进制值相关联。

    映射是一个不变的数据结构。 添加元素意味着创建另一个Map:

    val colors1 = Map("red" -> "#FF0000", "azure" -> "#F0FFFF", "peru" -> "#CD853F")
    val colors2 = colors1 + ("blue" -> "#0033FF")

    同时,不能修改元素。 在需要可变结构的情况下,可以使用scala.collection.mutable.Map:

    在此示例中,我们突变了AK键。

    val states = scala.collection.mutable.Map("AL" -> "Alabama", "AK" -> "tobedefined")
    states("AK") = "Alaska"

    方法/函数:基础

    我们必须区分方法和函数。 方法是属于类,特征或对象的成员的函数(我们将沿用这些概念)。

    让我们看一个基本的方法示例:

    def add(x: Int, y: Int): Int = {
      x + y
    }

    在这里,我们使用def关键字定义了一个add方法。 它以两个Int作为输入并返回一个Int。 两个输入都是不可变的(从某种意义上来说,就像将它们声明为val一样对其进行管理)。

    return关键字是可选的。 该方法将自动返回最后一个表达式。 此外,值得一提的是,在Scala(与Java相比)中,return退出当前方法,而不是当前块。

    最后一件事,返回类型是可选的。 Scala编译器也可以推断出它。 但是,出于代码可维护性的考虑,明确设置它可能是一个不错的选择。

    此外,两种方法都可以编写没有输出的方法:

    def printSomething(s: String) = {
      println(s)
    }
    
    def printSomething(s: String): Unit = {
      println(s)
    }

    我们还可以像这样返回多个输出:

    def increment(x: Int, y: Int): (Int, Int) = {
      (x + 1, y + 1)
    }

    这避免了我们必须将一组输出包装在特定对象中。

    还有句法糖要提。 让我们考虑一个没有参数的bar方法。 我们可以通过两种方式调用此方法:

    def foo(): Unit = {
      bar()
      bar
    }

    最佳做法是仅在bar产生副作用时才保留括号。 否则,我们像第二个表达式一样调用bar。

    同样,Scala允许我们指出方法参数可以重复。 就像Java中一样,此可重复参数必须是最后一个参数:

    def variablesArguments(args: Int*): Int = {
      var n = 0
      for (arg <- args) {
        n += arg
      }
      n
    }

    在这里,我们遍历每个args元素,然后返回汇总和。

    最后但并非最不重要的一点是,我们还可以定义默认参数值:

    def default(x: Int = 1, y: Int): Int = {
      x * y
    }

    可以通过两种方式在不提供x值的情况下调用默认值。

    首先,使用_运算符:

    default(_, 3)

    或者,使用这样的命名参数:

    default(y = 3)

    方法/函数:高级

    嵌套方法

    在Scala中,我们可以嵌套方法定义。 让我们考虑以下示例:

    def mergesort1(array: Array[Int]): Unit = {
      val helper = new Array[Int](array.length)
      mergesort2(array, helper, 0, array.length - 1)
    }
    
    private def mergesort2(array: Array[Int], helper: Array[Int], low: Int, high: Int): Unit = {
      if (low < high) {
        val middle = (low + high) / 2
        mergesort2(array, helper, low, middle)
        mergesort2(array, helper, middle + 1, high)
        merge(array, helper, low, middle, high)
      }
    }

    在这种情况下,mergesort2方法仅由mergesort1使用。 为了限制其访问,我们可能决定将其设置为私有(稍后我们将在不同的可见性级别中进行查看)。

    但是,在Scala中,我们还可以决定将第二种方法嵌套到第一种方法中,如下所示:

    def mergesort1(array: Array[Int]): Unit = {
      val helper = new Array[Int](array.length)
      mergesort2(array, helper, 0, array.length - 1)
    
      def mergesort2(array: Array[Int], helper: Array[Int], low: Int, high: Int): Unit = {
        if (low < high) {
          val middle = (low + high) / 2
          mergesort2(array, helper, low, middle)
          mergesort2(array, helper, middle + 1, high)
          merge(array, helper, low, middle, high)
        }
      }
    }

    mergesort2仅在mergesort1的范围内可用。

    高阶函数

    高阶函数将一个函数作为参数或作为结果返回一个函数。 作为以函数为参数的方法的示例:

    def foo(i: Int, f: Int => Int): Int = {
      f(i)
    }

    f是一个将Int作为输入并返回Int的函数。 在我们的示例中,foo通过将i传递给f将执行委托给f。

    函数字面量

    从每个功能都是一个值的意义上说,Scala被视为一种功能语言。 这意味着我们可以用如下的函数文字语法来表示一个函数:

    val increment: Int => Int = (x: Int) => x + 1
    
    println(increment(5)) // Prints 6

    增量是一个具有Int => Int类型的函数(可能已由Scala编译器推断出)。 对于每个整数x,它返回x + 1。

    如果再次使用前面的示例,则可以将增量传递给foo:

    def foo(i: Int, f: Int => Int): Int = {
      f(i)
    }
    
    def bar() = {
      val increment: Int => Int = (x: Int) => x + 1
    
      val n = foo(5, increment)
    }

    我们还可以管理所谓的匿名函数:

    val n = foo(5, (x: Int) => x + 1)

    第二个参数是一个没有任何名称的函数。

    闭包

    函数文字中的闭包,取决于此函数外部声明的一个或多个变量/值的值。

    一个简单的例子:

    val Pi = 3.14
    
    val foo = (n: Int) => {
      n * Pi
    }

    在此,foo取决于在foo外部声明的Pi。

    部分函数

    让我们考虑以下方法来根据距离和时间计算速度:

    def speed(distance: Float, time: Float): Float = {
      distance / time
    }

    Scala允许我们仅通过强制输入的子集调用速度来部分应用速度:

    val partialSpeed: Float => Float = speed(5, _)

    请注意,在此示例中,speed的任何参数都没有默认值。 因此,为了进行调用,我们需要填充所有参数。

    在此示例中,partialSpeed是类型Float => Float的函数。

    然后,以与调用增量相同的方式,我们可以这样调用partialSpeed:

    Currying

    一个方法可以定义多个参数列表:

    def multiply(n1: Int)(n2: Int): Int = {
      n1 * n2
    }

    此方法执行的工作与以下操作完全相同:

    def multiply2(n1: Int, n2: Int): Int = {
      n1 * n2
    }

    但是,调用乘法的方式有所不同:

    val n = multiply(2)(3)

    像方法签名要求的那样,我们用两个参数列表来调用它。 然后,如果我们只用一个参数列表调用乘法呢?

    val partial: Int => Int = multiply(2)

    在这种情况下,我们部分应用了乘法,这使我们得到一个Int => Int函数。

    有什么好处? 让我们考虑一个在特定情况下发送消息的函数:

    def send(context: Context, message: Array[Byte]): Unit = {
      // Send message
    }

    如您所见,我们正在努力使此功能纯净。 无需依赖于外部上下文,我们可以将其用作send函数的参数。

    但是,在每个send调用期间都必须传递此上下文可能有些乏味。 或者,也许某个函数不需要了解上下文。

    一种解决方案是在预定义的上下文中部分应用send并管理Array [Byte] => Unit函数。

    另一个解决方案是咖喱发送和使上下文参数隐式,如下所示:

    def send(message: Array[Byte])(implicit context: Context): Unit = {
      // Send message
    }

    在这种情况下,我们怎么称呼发送? 我们可以在调用send之前定义一个隐式上下文:

    implicit val context = new Context(...)
    send(bytes)

    隐式关键字意味着对于管理隐式Context参数的每个函数,我们甚至都不需要传递它。 Scala编译器将自动映射它。

    在我们的例子中,send将Context对象作为潜在的隐式对象进行管理(我们也可以决定显式地传递它)。 因此,我们可以简单地使用第一个参数列表调用send。

    Scala中的类与Java中的类类似:

    class Point(var x: Int, var y: Int) {
      def move(dx: Int, dy: Int): Unit = {
        x += dx
        y += dy
    
        println(s"$x $y")
      }
    }

    由于语法行1,Point暴露了默认的(Int,Int)构造函数。同时,x和y是该类的两个成员。

    类也可以包含方法的集合,就像前面的示例中的move一样。

    我们可以使用new关键字实例化Point:

    val point = new Point(5, 2)

    一个类可以是抽象的,意味着它不能被实例化。

    Case 类

    Case 类是一种特殊的类。 如果您熟悉DDD(域驱动设计),那么案例类就是一个值对象。

    默认情况下,case类是不可变的:

    case class Point(x: Int, y: Int)

    x和y的值不能更改。

    Case类必须不通过new实例化:

    val point = Point(5, 2)

    Case类(与常规类相比)通过值(而不是引用)进行比较:

    if (point1 == point2) {
      // ...
    } else {
      // ...
    }

    对象

    Scala中的一个对象是一个单例:

    object EngineFactory {
      def create(context: Context): Engine = {
        // ...
      }
    }

    使用object关键字()定义对象。

    Traits 特质

    特性在某种程度上类似于Java接口。 它们用于在类之间以及字段之间共享接口。 举个例子:

    trait Car {
      val color: String
      def drive(): Point
    }

    特征方法也可以具有默认实现。

    特性不能被实例化,但是它们可以被类和对象扩展。

    Visibility

    在Scala中,默认情况下,类/对象/特征的每个成员都是公共的。 还有其他两个访问修饰符:

    · protected:成员只能从子类访问

    · private:仅可从当前类/对象访问成员

    此外,我们还可以通过指定一个应用了限制的包来采用更精细的方式来限制访问。

    让我们考虑一下酒吧包装中的Foo类。 如果我们只想在bar之外将方法设为私有,则可以通过以下方式实现:

    class Foo {
      private[bar] def foo() = {}
    }

    泛型

    泛型也是Scala提供的功能:

    class Stack[A] {
      def push(x: A): Unit = { 
        // ...
      }
    }

    实例化一个通用类:

    val stack = new Stack[Int]
    stack.push(1)

    If-else

    Scala中的if-else语法与其他几种语言相似:

    if (condition1) {
    
    } else if (condition2) {
    
    } else {
    
    }

    但是,在Scala中,if-else语句也是一个表达式。 这意味着我们可以例如定义如下方法:

    def max(x: Int, y: Int) = if (x > y) x else y

    循环

    基本循环可以像这样实现:

    // Include
    for (a <- 0 to 10) {
      println(a)
    }
    
    // Exclude
    for (a <- 0 until 10) {
      println(a)
    }

    到表示包括0到10,而直到表示从0到10排除。

    我们还可以遍历两个元素:

    for (a <- 0 until 2; b <- 0 to 2) {
      
    }

    在此示例中,我们遍历了所有可能的元组组合:

    a=0, b=0
    a=0, b=1
    a=0, b=2
    a=1, b=0
    a=1, b=1
    a=1, b=2

    我们也可以在中包含条件。 让我们考虑以下元素列表:

    val list = List(5, 7, 3, 0, 10, 6, 1)

    如果我们需要遍历list的每个元素并仅考虑偶数整数,则可以通过以下方式实现:

    for (elem <- list if elem % 2 == 0) {
      
    }

    此外,Scala提供了所谓的理解,以for()yield element的形式创建元素序列。 举个例子:

    val sub = for (elem <- list if elem % 2 == 0) yield elem

    在此示例中,我们通过遍历每个元素并在偶数情况下产生它来创建偶数整数的集合。 结果,sub将被推断为整数序列(一个Seq对象,List的父对象)。

    以与if-else语句相同的方式,for也是一个表达式。 因此我们还可以定义如下方法:

    def even(list: List[Integer]) = for (elem <- list if elem % 2 == 0) yield elem

    模式匹配

    模式匹配是一种根据给定模式检查值的机制。 它是Java switch语句的增强版本。

    让我们考虑一个简单的函数,将整数转换为字符串:

    def matchA(i: Int): String = {
      i match {
        case 1 => return "one"
        case 2 => return "two"
        case _ => return "something else"
      }
    }

    Scala添加了一些语法糖来以这种方式实现等效功能:

    def matchB(i: Int): String = i match {
      case 1 => "one"
      case 2 => "two"
      case _ => "something else"
    }

    首先,我们删除了return语句。 然后,随着我们在函数定义之后删除了block语句,matchB函数成为了模式匹配器。

    除了糖以外,还有其他吗? 模式匹配是对案例类的重要补充。 让我们考虑一个取自Scala文档的示例。

    我们想根据通知类型返回一个字符串。 我们定义了一个抽象类Notification和两个案例类Email和SMS:

    abstract class Notification
    case class Email(sender: String, title: String, body: String) extends Notification
    case class SMS(caller: String, message: String) extends Notification

    在Scala中最优雅的方法是在通知上使用模式匹配:

    def showNotification(notification: Notification): String = {
      notification match {
        case Email(email, title, _) =>
          s"You got an email from $email with title: $title"
        case SMS(number, message) =>
          s"You got an SMS from $number! Message: $message"
      }
    }

    这种机制使我们能够投射给定的通知并自动解析我们感兴趣的参数。例如,对于电子邮件,也许我们不希望显示正文,因此只需使用_关键字将其省略。

    异常

    让我们考虑一个具体的用例,其中我们需要打印给定文件中的字节数。 为了执行I / O操作,我们将使用java.io.FileReader,它可能会引发异常。

    如果您是Java开发人员,最常见的方法是使用try / catch语句执行以下操作:

    try {
      val n = new FileReader("input.txt").read()
      println(s"Success: $n")
    } catch {
      case e: Exception =>
        e.printStackTrace
    }

    第二种实现方法与Java Optional类似。 提醒一下,Optional是Java 8中引入的用于可选值的容器。

    在Scala中,"尝试"是成功或失败的容器。 它是一个抽象类,并通过成功和失败两个案例类进行了扩展。

    val tried: Try[Int] = Try(new FileReader("notes.md")).map(f => f.read())
        
    tried match {
      case Success(n) => println(s"Success: $n")
      case Failure(e) => e.printStackTrace
    }

    我们首先在Try调用中包装新FileReader的创建。 我们使用地图通过调用read方法将最终的FileReader转换为Int。 结果,我们得到了一个Try [Int]。

    然后,我们可以使用模式匹配来确定已尝试的类型。

    隐式转换

    让我们分析以下示例:

    case class Foo(x: Int)
    case class Bar(y: Int, z: Int)
    
    object Consumer {
      def consume(foo: Foo): Unit = {
        println(foo.x)
      }
    }
    
    object Test {
      def test() = {
        val bar = new Bar(5, 2)
        Consumer.consume(bar)
      }
    }

    我们定义了两个案例类Foo和Bar。

    同时,对象消费者使用参数Foo公开了消耗方法。

    在测试中,我们调用Consumer.consume(),但不按方法签名的要求使用Foo调用,而是使用Bar。 这怎么可能?

    在Scala中,我们可以定义两个类之间的隐式转换。 在最后一个示例中,我们只需要描述如何将Bar转换为Foo:

    implicit def barToFoo(bar: Bar): Foo = new Foo(bar.y + bar.z)

    如果导入了barToFoo这个方法,Scala编译器将确保我们可以使用Foo或Bar调用使用者。

    并发

    为了处理并发,Scala最初基于参与者模型。 Scala提供了scala.actors库。 但是,从Scala 2.10开始,该库就不再支持Akka actor了。

    Akka是用于实现并发和分布式应用程序的一组库。 尽管如此,我们也只能在单个过程的规模上使用Akka。

    主要思想是将参与者作为并发计算的原语进行管理。 演员可以向其他演员发送消息,接收消息并对消息做出反应并生成新的actor。

    就像其他并行计算模型(如CSP(通信顺序过程))一样,关键是通过消息进行通信,而不是在不同线程之间共享内存。


    在您的计算机上安装Scala并开始编写一些Scala代码!

    Scala是一种非常优雅的语言。 但是,与Go等其他语言相比,学习曲线并不小。 初学者阅读现有的Scala代码可能有些困难。 但是一旦您开始掌握它,就可以以非常有效的方式来开发应用程序。


    (本文翻译自Teiva Harsanyi的文章《A 10-Minute Introduction to Scala》,参考:https://itnext.io/a-10-minute-introduction-to-scala-d1fed19eb74c)

    Tags:

    最近发表
    标签列表