Skip to content

Latest commit

 

History

History
688 lines (476 loc) · 20.7 KB

9.包和导入.md

File metadata and controls

688 lines (476 loc) · 20.7 KB

9. 包和导入

包常用于构建相关联的代码模块,并用于避免命名空间冲突。通常情况下,可以用和Java相同的格式创建Scala的包,所以大部分Scala源代码文件均以package声明开头,如下所示:

    package com.alvinalexander.myapp.model
    class Person ...

然而,Scala语法更加灵活,除此之外,你还可以使用花括号的包风格,这类似于C++和C#的命名空间。随后的9.1小节中会有该语法的展示。

Scala导入成员的方法与Java类似,而且更灵活。 在Scala里可以:

  • 随处使用import语句。
  • 导入类、包或者对象。
  • 在导入成员时隐藏和重命名成员。

本章展示了所有的上述方法。

在深入了解这些小节之前,你需要注意Scala默认会有两个包被隐式导入到所有源代码文件的作用域中:

  • java.lang.*
  • scala.*

在Scala 3中,import语句中的 * 字符类似于 Java 中的 * 字符,因此这些语句表示“导入包中的每个成员”。

Predef对象

除了这两个包之外,来自 scala.Predef 对象的所有成员也被隐式导入到源代码文件中。

如果想了解Scala的工作原理,强烈建议花点时间深入研究Predef 对象( https://oreil.ly/KtxXV )的源码。虽然代码不长,但它展示了Scala语言的很多特性。

正如我在“这些方法从何而来?”的讨论中所说,隐式转换被Predef对象引入到作用域中,在Scala 2.13的Predef对象在Scala 3.0仍在使用,代码如下所示:

    implicit def long2Long(x: Long): java.lang.Long = x.asInstanceOf[java.lang.Long]
    implicit def Long2long(x: java.lang.Long): Long = x.asInstanceOf[Long]
    // more implicit conversions ...

同样,如果想知道为什么可以在不需要 import 语句的情况下调用 MapSetprintln 的代码,也可以在 Predef 中找到这些代码:

    type Map[A, +B] = immutable.Map[A, B]
    type Set[A] = immutable.Set[A]
    def println(x: Any) = Console.println(x)
    def printf(text: String, xs: Any*) = Console.print(text.format(xs: _*))
    def assert(assertion: Boolean) { ... }
    def require(requirement: Boolean) { ... }

9.1 花括号风格的包记号法

问题

你想使用嵌套风格的包表示法,类似于 C++ 和 C# 中的命名空间表示法。

解决方案

提供包名的同时,将一个或多个类包装在一对花括号内,如下所示:

    package com.acme.store {
        class Foo:
            override def toString = "I am com.acme.store.Foo"
    }

这个类的规范名称是com.acme.store.Foo。 和这样声明代码是一样的:

    package com.acme.store
    class Foo:
        override def toString = "I am com.acme.store.Foo"

好处

使用这种方法,可以将多个包放在一个文件中,也可以创建嵌套包。 为了展示这两种方法,以下例子创建了三个Foo类,都位于不同的包中:

    package orderentry {
        class Foo:
            override def toString = "I am orderentry.Foo"
    }

    package customers {
        class Foo:
            override def toString = "I am customers.Foo"

            package database {
                class Foo:
                    override def toString = "I am customers.database.Foo"
      }
    }

    // the output is shown after the comment tags.
    @main def packageTests =
        println(orderentry.Foo())               // I am orderentry.Foo
        println(customers.Foo())                // I am customers.Foo
        println(customers.database.Foo())       // I am customers.database.Foo

这表明每个Foo类都在不同的包中,并且database包嵌套在customers包中。

讨论

我看过很多Scala代码,据我所知,在文件顶部声明包名是最流行的包风格:

    package foo.bar.baz

    class Foo:
        override def toString = "I'm foo.bar.baz.Foo"

但是,由于Scala代码可以非常简洁,如果想在一个文件中声明多个类和包时,另一种花括号打包的语法会很方便。 比如在本书的源码仓库( https://github.com/alvinj/ScalaCookbookV2Examples )中,会看到我经常使用这种风格。

链式包子句

有时查看Scala程序时,会在源码文件的顶部看到多个包声明,如下所示:

    package com.alvinalexander
    package tests
    ...

这和编写两个嵌套的包的代码完全相同,如下所示:

    package com.alvinalexander {
        package tests {
        ...
        }
    }

使用第一种形式的原因是Scala程序员通常不喜欢使用花括号样式缩进代码,特别是在大文件中。所以他们用第一种形式。

如果使用两个包子句而不是一个,则和每种方式在当前作用域中的可用性有关。而如果只使用一个包语句:

    package com.alvinalexander.tests

然而只有com.alvinalexander.tests的成员被引入作用域。但如果使用两个包声明:

    package com.alvinalexander
    package tests
    ...

com.alvinalexandercom.alvinalexander.tests 的成员都被引入作用域。

之所以采用这种方法,与Scala 2.7中发现的并在Scala 2.8中解决的一个情况有关。详细信息可参阅Martin Odersky文章 chained package clauses( https://oreil.ly/YNvjN )。

9.2 导入一个或多个成员

问题

你想将一个或多个成员导入当前代码的作用域。

解决方案

用这样的语法导入一个类:

    import java.io.File

还可以像这样导入多个类:

    import java.io.File
    import java.io.IOException
    import java.io.FileNotFoundException

更简洁的像这样:

    import java.io.{File, IOException, FileNotFoundException}

我将其称为花括号语法,但更正式地称为导入选择子句。

这样导入java.io 包中所有内容:

    import java.io.* 

讨论

Scala的语法很灵活,你可以:

  • import 语句放在任何地方,包括类的顶部、类或对象内、方法内或代码块内。 该技术将在9.6小节展示。
  • 导入包、类、对象和方法。
  • 导入成员时隐藏和重命名成员。 9.3小节和9.4小节中展示这些技术。

9.3 导入时重命名成员

问题

你想在导入时重命名成员,以避免命名空间冲突或混淆。

解决方案

使用以下语法导入时,为导入的类指定一个新名字:

    import java.awt.{List as AwtList}

然后在代码中,通过别名来引用这个类:

    scala> val alist = AwtList(1, false)
    val alist: java.awt.List = java.awt.List[list0,0,0,0x0,invalid,selected=null]

通过AwtList来使用java.awt.List类,也可以通过惯用名使用Scala的List类:

    scala> val x = List(1, 2, 3)
    val x: List[Int] = List(1, 2, 3)

可以在导入的时候重命名多个类:

    import java.util.{Date as JDate, HashMap as JHashMap}

还可以在导入的最后位置使用 * 字符从而导入其他所有内容(无需重命名其他成员):

    import java.util.{Date as JDate, HashMap as JHashMap, *}

在导入的时候创建了别名,所以不能在代码中使用类的原始(真实)名字。 在使用最后一个 import 语句后,下面代码将失败,编译器找不到 java.util.HashMap 类,因为被重命名:

    scala> val map = HashMap[String, String]()
    <console>:12: error: not found: type HashMap
        val map = HashMap[String, String]
                  ^

正如预期的那样失败了,但是可以用别名来引用这个类:

    scala> val map = JHashMap[String, String]()
    map: java.util.HashMap[String,String] = {}

由于 import 语句末尾的 *java.util 包中导入了其余所有内容,所以别的 java.util 类的代码可以使用:

    scala> val x = ArrayList[String]()
    x: java.util.ArrayList[String] = []

    scala> val y = LinkedList[String]()
    y: java.util.LinkedList[String] = []

讨论

如上所示,在导入时为类创建新名字,在用新名字或别名来引用类。 Programming in Scala 将这种做法称为renaming clause

这样做有助于避免命名空间冲突和混淆。如ListenerMessageHandlerClientServer 这些类的名字很常见,在导入时重命名会很有帮助。

Scala 3的语法与Scala 2不同,以下代码展示了Scala 3 与Scala 2的区别:

    // scala 2
    import java.util.{Date => JDate, HashMap => JHashMap, _}

    // scala 3
    import java.util.{Date as JDate, HashMap as JHashMap, *}

在编写本章时,仍可以在 Scala 3代码中使用Scala 2语法,但下划线的语法最终会被淘汰,所以优先使用新语法。

这些有趣的技巧组合,不仅可以在导入时重命名类,也可以重命名类的成员和Java静态成员,在下面的脚本中,println被重命名为更短的名字,如REPL中所示:

    scala> import System.out.{println as p}

    scala> p("hello")
    hello

因为outPrintStreamjava.lang.System中一个的static final实例,而printlnPrintStream的方法。最终结果是,pprintln方法的别名。

9.4 导入时隐藏类

问题

为了避免命名冲突或混淆,你想在引入来自同一个包的其他成员时,隐藏一个或多个类。

解决方案

导入时隐藏类可以使用9.3小节重命名的语法,但需要把类名指向字符 _,以下例子在导入java.util包中所有成员时隐藏了Random类:

    import java.util.{Random => _, *}

REPL中运行验证:

    scala> import java.util.{Random => _, *}
    import java.util.{Random=>_, _}
    // can’t access Random
    scala> val r = Random()
    1     |val r = Random()
        | ^^
        | Not found: Random

    // can access other members
    scala> val x = ArrayList()
    val x: java.util.ArrayList[Nothing] = []

讨论

在这个例子中,下面代码隐藏了Random类:

    import java.util.{Random => _}

之后,大括号内的 * 字符就相当于说明你要导入包中的其他所有内容,像这样:

    import java.util.*

注意导入通配符 * 必须在最后一个位置。 在其他位置会出错:

    scala> import java.util.{*, Random => _}
    1   |import java.util.{*, Random => _}
        | ^^
        | named imports cannot follow wildcard imports

这是因为导入中要隐藏多个成员,得先列出它们。

导入时,在最后的通配符前列出要隐藏的成员:

    import java.util.{List => _, Map => _, Set => _, *}

在这个导入语句之后,可以使用 java.util 中的其他类:

    scala> val x = ArrayList[String]()
    val x: java.util.ArrayList[String] = []

你仍可以使用Scala的 ListSetMap 类,而不会和同名的java.util中的类发生命名冲突:

    // these are all Scala classes

    scala> val a = List(1, 2, 3)
    val a: List[Int] = List(1, 2, 3)

    scala> val b = Set(1, 2, 3)
    val b: Set[Int] = Set(1, 2, 3)

    scala> val c = Map(1 -> 1, 2 -> 2)
    val c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)

当使用 * 通配符导入包的多个成员时,但因为命名冲突,需要隐藏一个或多个成员时,这种方式很有用。

9.5 导入静态成员

问题

你想用类似Java静态导入的方式导入成员,这样可以直接引用成员名,而不用在前面加上包名或类名。

解决方案

通过名字或Scala的 * 通配符导入静态成员。从 scala.math 包中导入静态 cos 方法:

    import scala.math.cos
    val x = cos(0) // 1.0

scala.math 包中导入所有成员:

    import scala.math.*

这种语法可以访问 scala.math 包的所有静态成员,而不用在前面加上类名:

    import scala.math.*
    val a = sin(0)  // Double = 0.0
    val b = cos(Pi) // Double = −1.0

Java的 Color 类也展示了该技术的好处:

    import java.awt.Color.*
    println(RED)    // java.awt.Color[r=255,g=0,b=0]
    println(BLUE)   // java.awt.Color[r=0,g=0,b=255]

讨论

该技术的一个常见示例是对象和枚举类型。例如下面 StringUtils 对象:

    object StringUtils:
        def truncate(s: String, length: Int): String = s.take(length)
        def leftTrim(s: String): String = s.replaceAll("^\\s+", "")

可以这样导入和使用方法:

    import StringUtils.*
    truncate("four score and seven ", 4)    // "four"
    leftTrim(" four score and ")            // "four score and "

同样的,Scala3的枚举:

    package days {
        enum Day:
        case Sunday, Monday, Tuesday, Wednesday,
        Thursday, Friday, Saturday
    }

可以这样导入和使用枚举:

    // a different package
    package bar {
        import days.Day.*

        @main def enumImportTest =
            val date = Sunday

            // more code here ...

            if date == Saturday || date == Sunday then
                println("It’s the weekend!")
    }

有些开发人员不喜欢静态导入,我却觉得这样让枚举更加可读,相反,在一个常量前加上类名或枚举名会降低代码的可读性:

    if date == Day.Saturday || date == Day.Sunday then
        println("It’s the weekend!")

使用静态导入,代码中不需要以“Day.”开头,反而更容易阅读:

    if date == Saturday || date == Sunday then ...

9.6 在任何地方使用导入语句

问题

你想在任何地方都可以使用import语句,而不仅仅是文件顶部。通常为了限制导入的范围,而使代码更清晰。

解决方案

可以将import语句放在程序任何地方。和Java以及其他语言一样,常见用法在类的顶部导入成员,接着在之后的代码中使用这些导入资源:

    package foo

    import scala.util.Random

    class MyClass:
        def printRandom =
            val r = Random() //use the imported class

要获得更多控制,可以在类内部导入成员:

    package foo

    class ClassA:                   //inside ClassA
        import scala.util.Random    //inside ClassA
        def printRandom =
            val r = Random ()

        class ClassB:
            // the import is not visible here
            val r = Random ()       //error: not found: Random

这样import的作用域被限制在导入语句之后ClassA内的代码。

可以在方法中使用 import 语句:

    def getPandoraItem(): Any =
        import com.alvinalexander.pandorasbox.*
        val p = Pandora()
        p.getRandomItem

甚至可以把import语句放在代码块中,并将其作用域限制在import语句后的代码。下面例子正确声明了r1,因为它在代码块中且在import 语句后,但字段 r2 的声明不能通过编译,因为没有正确的引入Radom类:

    def printRandom =
        {
            import scala.util.Random
            val r1 = Random()   //this works, as expected
        }
        val r2 = Random()       //error: not found: Random

讨论

import语句使导入的成员在导入后才可用,这也限制了其作用域。下面代码无法通过编译,因为在import 语句之前引用Random类:

    // this does not compile
    class ImportTests:
        def printRandom =
            val r = Random() //error: not found: type Random

    import scala.util.Random

当一个文件中包含多个类和包时,可以结合 import 语句和花括号风格的包方式(如9.1小节所示),用以限制 import 语句的范围,如下所示:

    package orderentry {
        import foo.*
        // more code here ...
    }

    package customers {
        import bar.*
        // more code here ...

        package database {
            import baz.*
            // more code here ...
        }
    }

这个例子中成员访问方法如下:

  • orderentry包中的代码可以访问foo的成员,但无法访问barbaz的成员。
  • customerscustomer.database中的代码不能访问foo的成员。
  • customers的代码可以访问bar的成员。
  • customers.database的代码可以访问barbaz中的成员。

同样的概念适用于一个文件中定义多个类:

    package foo
    // available to all classes defined below
    import java.io.File
    import java.io.PrintWriter

    class Foo:
        // only available inside this class
        import javax.swing.JFrame
        // ...

    class Bar:
        // only available inside this class
        import scala.util.Random
        // ...

尽管在文件的顶部或者只是在使用前加入import语句是一种风格,但我发现在一个文件中有多个类或包时,这种灵活性显得很有用。在这些情况下,最好将导入保持在尽可能小的作用域内,从而限制命名空间冲突,且在代码在增长时更容易重构。

9.7 导入given

问题

你需要将一个或多个given实例导入到当前作用域,同时也可能从同一个包中导入类型。

解决方案

given实例简称given,通常在单独的模块中定义,且必须使用特殊的 import 语句将其导入当前作用域。例如在包名为co.kbhr.givens,对象名为Addrgiven代码中:

    package co.kbhr.givens

    object Adder:
        trait Adder[T]:
            def add(a: T, b: T): T
        given intAdder: Adder[Int] with
            def add(a: Int, b: Int): Int = a + b

使用两个 import 语句将其导入当前作用域:

    @main def givenImports =
        import co.kbhr.givens.Adder.*       // import all nongiven definitions
        import co.kbhr.givens.Adder.given   // import all `given` definitions

        def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
        println(genericAdder(1, 1))

也可以将两个import语句合二为一:

    import co.kbhr.givens.Adder.{given, *} 

可以按类型导入匿名given实例,如本例中的第二个import语句所示:

    package co.kbhr.givens

    object Adder:
        trait Adder[T]:
            def add(a: T, b: T): T
        given Adder[Int] with
            def add(a: Int, b: Int): Int = a + b
        given Adder[String] with
            def add(a: String, b: String): String = "" + (a.toInt + b.toInt)

    @main def givenImports =
        // when put on separate lines, the order of the imports is important.
        // the second import statement imports the givens by their type.
        import co.kbhr.givens.Adder.*
        import co.kbhr.givens.Adder.{given Adder[Int], given Adder[String]}

        def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
        println(genericAdder(1, 1))     // 2
        println(genericAdder("2", "2")) // 4

该例中,这两行代码展示了如何导入Addr特质和given

    import co.kbhr.givens.Adder.*
    import co.kbhr.givens.Adder.{given Adder[Int], given Adder[String]}

根据所需也可以按类型导入given,如下所示:

    import co.kbhr.givens.Adder.*
    import co.kbhr.givens.Adder.{given Adder[?]}

第二行可以理解为:“导入任意类型的Addr given,如Addr[Int]或Addr[String]

讨论

根据Scala 3 文档关于导入given( https://oreil.ly/aobrq )的描述,新语法有两个原因和好处:

  • 更清楚地说明了作用域内的given从何而来。
  • 可以导入所有given而不导入其他任何东西。

given实例可以替换Scala 2中使用的implicits。如上所述,givenimplicits更清晰。 given 的动机之一,尤其是given 导入语句,在Scala 2中并不总是清楚implicits是如何进入当前作用域。

Scala 3 中使用 given 解决了这种情况,且创建了新的 import given 语法。正如示例所见,现在可以很容易地查看given语句列表,从而知道given的来源。

另见

  • 有关如何使用 given的更多内容,请参阅23.8小节“使用given和using的术语推断”。
  • 有关given的更多内容,请参阅Scala 3文档:given实例( https://oreil.ly/5rep7 )。
  • 有关导入given的更多内容,请参阅Scala 3文档:导入given( https://oreil.ly/aobrq )。
  • Scala 3 contextual抽象的文档( https://oreil.ly/c2IYn )详细说明了从implicits到given实例变化背后的动机。