Scala использует packages для создания пространств имен, которые позволяют модульно разбивать программы. Scala поддерживает стиль именования пакетов, используемый в Java, а также нотацию пространства имен “фигурные скобки”, используемую такими языками, как C++ и C#.
Подход Scala к импорту похож на Java, но более гибкий. С помощью Scala можно:
- импортировать пакеты, классы, объекты, trait-ы и методы
- размещать операторы импорта в любом месте
- скрывать и переименовывать участников при импорте
Эти особенности демонстрируются в следующих примерах.
Создание пакета
Пакеты создаются путем объявления одного или нескольких имен пакетов в начале файла Scala. Например, если ваше доменное имя acme.com и вы работаете с пакетом model приложения с именем myapp, объявление пакета выглядит следующим образом:
package com.acme.myapp.model
class Person ...
По соглашению все имена пакетов должны быть строчными, а формальным соглашением об именах является <top-level-domain>.<domain-name>.<project-name>.<module-name>.
Хотя это и не обязательно, имена пакетов обычно совпадают с именами иерархии каталогов.
Поэтому, если следовать этому соглашению, класс Person
в этом проекте будет найден
в файле MyApp/src/main/scala/com/acme/myapp/model/Person.scala.
Использование нескольких пакетов в одном файле
Показанный выше синтаксис применяется ко всему исходному файлу:
все определения в файле Person.scala
принадлежат пакету com.acme.myapp.model
в соответствии с предложением package
в начале файла.
В качестве альтернативы можно написать package
, которые применяются только к содержащимся в них определениям:
package users:
package administrators: // полное имя пакета - users.administrators
class AdminUser // полное имя класса - users.administrators.AdminUser
package normalusers: // полное имя пакета - users.normalusers
class NormalUser // полное имя класса - users.normalusers.NormalUser
Обратите внимание, что за именами пакетов следует двоеточие, а определения внутри пакета имеют отступ.
Преимущество этого подхода заключается в том, что он допускает вложение пакетов и обеспечивает более очевидный контроль над областью видимости и инкапсуляцией, особенно в пределах одного файла.
Операторы импорта
Операторы импорта используются для доступа к сущностям в других пакетах. Операторы импорта делятся на две основные категории:
- импорт классов, трейтов, объектов, функций и методов
- импорт
given
предложений
Первая категория операторов импорта аналогична тому, что использует Java, с немного другим синтаксисом, обеспечивающим большую гибкость. Пример:
import users.* // импортируется все из пакета `users`
import users.User // импортируется только класс `User`
import users.{User, UserPreferences} // импортируются только два члена пакета
import users.{UserPreferences as UPrefs} // переименование импортированного члена
Эти примеры предназначены для того, чтобы дать представление о том, как работает первая категория операторов import
.
Более подробно они объясняются в следующих подразделах.
Операторы импорта также используются для импорта given
экземпляров в область видимости.
Они обсуждаются в конце этой главы.
import не требуется для доступа к членам одного и того же пакета.
Импорт одного или нескольких членов
В Scala импортировать один элемент из пакета можно следующим образом:
import scala.concurrent.Future
несколько:
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.blocking
При импорте нескольких элементов их можно импортировать более лаконично:
import scala.concurrent.{Future, Promise, blocking}
Если необходимо импортировать все из пакета scala.concurrent, используется такой синтаксис:
import scala.concurrent.*
Переименование элементов при импорте
Иногда необходимо переименовать объекты при их импорте, чтобы избежать конфликтов имен.
Например, если нужно использовать Scala класс List
вместе с java.util.List
,
то можно переименовать java.util.List
при импорте:
import java.util.{List as JavaList}
Теперь имя JavaList
можно использовать для ссылки на класс java.util.List
и использовать List
для ссылки на Scala класс List
.
Также можно переименовывать несколько элементов одновременно, используя следующий синтаксис:
import java.util.{Date as JDate, HashMap as JHashMap, *}
В этой строке кода говорится следующее: “Переименуйте классы Date
и HashMap
, как показано,
и импортируйте все остальное из пакета java.util
, не переименовывая”.
Скрытие членов при импорте
При импорте часть объектов можно скрывать.
Следующий оператор импорта скрывает класс java.util.Random
,
в то время как все остальное в пакете java.util
импортируется:
import java.util.{Random as _, *}
Если попытаться получить доступ к классу Random
, то выдается ошибка,
но есть доступ ко всем остальным членам пакета java.util
:
val r = new Random // не скомпилируется
new ArrayList // доступ есть
Скрытие нескольких элементов
Чтобы скрыть в import несколько элементов, их можно перечислить перед использованием постановочного знака:
scala> import java.util.{List as _, Map as _, Set as _, *}
Перечисленные классы скрыты, но можно использовать все остальное в java.util:
scala> new ArrayList[String]
val res0: java.util.ArrayList[String] = []
Поскольку эти Java классы скрыты, можно использовать классы Scala List
, Set
и Map
без конфликта имен:
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)
Импорт можно использовать в любом месте
В Scala операторы import
могут быть объявлены где угодно.
Их можно использовать в верхней части файла исходного кода:
package foo
import scala.util.Random
class ClassA:
def printRandom(): Unit =
val r = new Random // класс Random здесь доступен
// ещё код...
Также операторы import
можно использовать ближе к тому месту, где они необходимы:
package foo
class ClassA:
import scala.util.Random // внутри ClassA
def printRandom(): Unit =
val r = new Random
// ещё код...
class ClassB:
// класс Random здесь невидим
val r = new Random // этот код не скомпилится
“Статический” импорт
Если необходимо импортировать элементы способом, аналогичным подходу “статического импорта” в Java, то есть для того, чтобы напрямую обращаться к членам класса, не добавляя к ним префикс с именем класса, используется следующий подход.
Синтаксис для импорта всех статических членов Java класса Math
:
import java.lang.Math.*
Теперь можно получить доступ к статическим методам класса Math
,
таким как sin
и cos
, без необходимости предварять их именем класса:
import java.lang.Math.*
val a = sin(0) // 0.0
val b = cos(PI) // -1.0
Пакеты, импортированные по умолчанию
Два пакета неявно импортируются во все файлы исходного кода:
java.lang.*
scala.*
Члены object Predef
также импортируются по умолчанию.
Например, такие классы, как
List
,Vector
,Map
и т.д. можно использовать явно, не импортируя их - они доступны, потому что определены в objectPredef
Обработка конфликтов имен
Если необходимо импортировать что-то из корня проекта и возникает конфликт имен,
достаточно просто добавить к имени пакета префикс _root_
:
package accounts
import _root_.accounts.*
Импорт экземпляров given
Как будет показано в главе “Контекстные абстракции”,
для импорта экземпляров given
используется специальная форма оператора import
.
Базовая форма показана в этом примере:
object A:
class TC
given tc: TC
def f(using TC) = ???
object B:
import A.* // импорт всех non-given членов
import A.given // импорт экземпляров given
В этом коде предложение import A.*
объекта B
импортирует все элементы A
, кроме given
экземпляра tc
.
И наоборот, второй импорт, import A.given
, импортирует только given
экземпляр.
Два предложения импорта также могут быть объединены в одно:
object B:
import A.{given, *}
Обсуждение
Селектор с подстановочным знаком *
помещает в область видимости все определения, кроме given
,
тогда как селектор выше помещает в область действия все данные, включая те, которые являются результатом расширений.
Эти правила имеют два основных преимущества:
- более понятно, откуда берутся данные given. В частности, невозможно скрыть импортированные given в длинном списке других импортируемых подстановочных знаков.
- есть возможность импортировать все given, не импортируя ничего другого. Это особенно важно, поскольку given могут быть анонимными, поэтому обычное использование именованного импорта нецелесообразно.
Импорт по типу
Поскольку given-ы могут быть анонимными, не всегда практично импортировать их по имени, и вместо этого обычно используется импорт подстановочных знаков. Импорт по типу предоставляет собой более конкретную альтернативу импорту с подстановочными знаками, делая понятным то, что импортируется.
import A.{given TC}
Этот код импортирует из A
любой given
тип, соответствующий TC
.
Импорт данных нескольких типов T1,...,Tn
выражается несколькими given
селекторами:
import A.{given T1, ..., given Tn}
Импорт всех given
экземпляров параметризованного типа достигается аргументами с подстановочными знаками.
Например, есть такой объект
:
object Instances:
given intOrd: Ordering[Int]
given listOrd[T: Ordering]: Ordering[List[T]]
given ec: ExecutionContext = ...
given im: Monoid[Int]
Оператор import
ниже импортирует экземпляры intOrd
, listOrd
и ec
, но пропускает экземпляр im
,
поскольку он не соответствует ни одному из указанных шаблонов:
import Instances.{given Ordering[?], given ExecutionContext}
Импорт по типу можно смешивать с импортом по имени.
Если оба присутствуют в предложении import, импорт по типу идет последним.
Например, это предложение импорта импортирует im
, intOrd
и listOrd
, но не включает ec
:
import Instances.{im, given Ordering[?]}
Пример
В качестве конкретного примера представим, что у нас есть объект MonthConversions
,
который содержит два определения given
:
object MonthConversions:
trait MonthConverter[A]:
def convert(a: A): String
given intMonthConverter: MonthConverter[Int] with
def convert(i: Int): String =
i match
case 1 => "January"
case 2 => "February"
// остальные случаи здесь ...
given stringMonthConverter: MonthConverter[String] with
def convert(s: String): String =
s match
case "jan" => "January"
case "feb" => "February"
// остальные случаи здесь ...
Чтобы импортировать эти given-ы в текущую область, используем два оператора import
:
import MonthConversions.*
import MonthConversions.{given MonthConverter[?]}
Теперь создаем метод, использующий эти экземпляры:
def genericMonthConverter[A](a: A)(using monthConverter: MonthConverter[A]): String =
monthConverter.convert(a)
Вызов метода:
@main def main =
println(genericMonthConverter(1)) // January
println(genericMonthConverter("jan")) // January
Как уже упоминалось ранее, одно из ключевых преимуществ синтаксиса “import given” состоит в том,
чтобы прояснить, откуда берутся данные в области действия,
и в import
операторах выше ясно, что данные поступают из объекта MonthConversions
.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java