В этом уроке вы узнаете:

Метод apply

Метод apply – это синтаксический сахар, который применяется для класса или объекта с единственной целью.

object FooMaker {
  def apply() = new Foo
}

scala> class Bar {
     |   def apply() = 0
     | }
defined class Bar

scala> val bar = new Bar
bar: Bar = Bar@47711479

scala> bar()
res8: Int = 0

Здесь наш экземпляр объекта выглядит так, будто мы просто вызываем метод, но это не так. Подробнее об этом позже!

Объекты

Объекты используются для хранения одного экземпляра класса. Чаще всего они используются с фабриками объектов.

object Timer {
  var count = 0

  def currentCount(): Long = {
    count += 1
    count
  }
}

Как можно это использовать?

scala> Timer.currentCount()
res0: Long = 1

Классы и Объекты могут иметь похожие имена. В этом случае Объект называется ‘Объект-спутник’(Companion Object). Чаще всего мы будем использовать Объекты-спутники с Фабриками объектов.

Далее представлен простой пример, который показывает, как можно использовать Объект-спутник, для того чтобы исключить необходимость в использовании ключевого слова ‘new’ для создания экземпляра объекта.

class Bar(foo: String)

object Bar {
  def apply(foo: String) = new Bar(foo)
}

Функции, тоже являются Объектами

В Scala, мы часто говорим об объектно-функциональном стиле. Что это значит? Чем на самом деле является Функция?

Функция – это набор трейтов. В частности, функция, которая принимает один аргумент является экземпляром трейта Function1. Этот трейт определяет метод apply(), который является синтаксическим сахаром, о нем мы узнали ранее, он позволяет вам вызывать объект, словно он является функцией.

scala> object addOne extends Function1[Int, Int] {
     |   def apply(m: Int): Int = m + 1
     | }
defined module addOne

scala> addOne(1)
res2: Int = 2

Существует функция с 22 аргументами. Почему с 22? Это произвольное магическое число. Я никогда не нуждался в функции с более чем 22 аргументами.

Синтаксический сахар метода apply объединяет двойственность объектного и функционального стилей программирования. Вы можете передавать классы и использовать их в качестве функций, кроме этого функции могут быть просто экземплярами классов.

Означает ли это, что каждый раз, когда вы определяете метод в своем классе, вы фактически получаете экземпляр Function*? Нет, методы в классах – это просто методы. Методы-одиночки, определенные в REPL будут экземплярами Function*.

Классы могут расширять Function* и в этих случаях они могут быть вызваны при помощи ().

scala> class AddOne extends Function1[Int, Int] {
     |   def apply(m: Int): Int = m + 1
     | }
defined class AddOne

scala> val plusOne = new AddOne()
plusOne: AddOne = <function1>

scala> plusOne(1)
res0: Int = 2

Запись extends Function1[Int, Int] мы можем переписать, используя extends (Int => Int)

class AddOne extends (Int => Int) {
  def apply(m: Int): Int = m + 1
}

Пакеты

Вы можете организовывать ваш код, используя пакеты.

package com.twitter.example

В верхней части файла объявляется все, что будет в этом пакете.

Значения и функции не могут быть объявлены за пределами класса или объекта. Объекты представляют собой полезный инструмент для организации статических функций.

package com.twitter.example

object colorHolder {
  val BLUE = "Blue"
  val RED = "Red"
}

После этого у вас есть доступ к членам пакета напрямую

println("the color is: " + com.twitter.example.colorHolder.BLUE)

Обратите внимание, что Scala REPL говорит вам когда вы объявляете объект:

scala> object colorHolder {
     |   val Blue = "Blue"
     |   val Red = "Red"
     | }
defined module colorHolder

Это дает вам небольшую подсказку, которую разработчики Scala используют для проектирования объектов, которые станут частью модульной системы Scala.

Сопоставление с образцом

Одна из самых часто используемых возможностей Scala.

Сопоставление значений

val times = 1

times match {
  case 1 => "one"
  case 2 => "two"
  case _ => "some other number"
}

Сопоставление с использованием условий

times match {
  case i if i == 1 => "one"
  case i if i == 2 => "two"
  case _ => "some other number"
}

Заметьте, как мы пытаемся поймать значение переменной ‘i’.

Используемый знак _ в последнем утверждении – это спецсимвол, он
гарантирует, что мы сможем отловить любое значение. В противном случае вы можете получить
ошибку времени выполнения, если вы попадаете в утверждение, которого не существует. Мы обсудим
это чуть позже.

Смотрите также: В Effective Scala описывается когда использовать сопоставление с образцом и правила форматирования сопоставления с образцом. В “Туре по Scala” также описывается Сопоставление с образцом

Сопоставление типов

Вы можете использовать match, чтобы управлять значениями типов различными способами.

def bigger(o: Any): Any = {
  o match {
    case i: Int if i < 0 => i - 1
    case i: Int => i + 1
    case d: Double if d < 0.0 => d - 0.1
    case d: Double => d + 0.1
    case text: String => text + "s"
  }
}

Сопоставление методов класса

Вспомните про наш калькулятор, который мы рассматривали ранее.

Давайте проведем классификацию по типам.

def calcType(calc: Calculator) = calc match {
  case calc.brand == "HP" && calc.model == "20B" => "financial"
  case calc.brand == "HP" && calc.model == "48G" => "scientific"
  case calc.brand == "HP" && calc.model == "30B" => "business"
  case _ => "unknown"
}

Ничего себе, как-то все слишком сложно. К счастью, Scala предоставляет некоторые полезные инструменты специально для этого случая.

Case Классы

Case классы используются для удобного хранения и поиска соответствий по содержимому класса. Вы можете создавать их без использования ‘new’.

scala> case class Calculator(brand: String, model: String)
defined class Calculator

scala> val hp20b = Calculator("HP", "20b")
hp20b: Calculator = Calculator(hp,20b)

У case классов есть метод ToString, работающий автоматически, и который опирается на аргументы конструктора.

scala> val hp20b = Calculator("HP", "20b")
hp20b: Calculator = Calculator(hp,20b)

scala> val hp20B = Calculator("HP", "20b")
hp20B: Calculator = Calculator(hp,20b)

scala> hp20b == hp20B
res6: Boolean = true

case классы могут иметь методы, как и обычные классы.

Case Классы и сопоставление с образцом

case классы предназначены для использования вместе с сопоставлением с образцом. Давайте упростим наш классификатор из примера с калькулятором.

val hp20b = Calculator("HP", "20B")
val hp30b = Calculator("HP", "30B")

def calcType(calc: Calculator) = calc match {
  case Calculator("HP", "20B") => "financial"
  case Calculator("HP", "48G") => "scientific"
  case Calculator("HP", "30B") => "business"
  case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)
}

А это другой способ для последнего сопоставления

  case Calculator(_, _) => "Calculator of unknown type"

мы можем не объявлять, что это Calculator совсем.

  case _ => "Calculator of unknown type"

Или мы можем связать найденное значение с другим именем

  case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)

Исключения

Исключения доступны в Scala при использовании синтаксиса try-catch-finally, который использует сопоставление с образцом.

try {
  remoteCalculatorService.add(1, 2)
} catch {
  case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.")
} finally {
  remoteCalculatorService.close()
}

try тоже ориентирован на выражения

val result: Int = try {
  remoteCalculatorService.add(1, 2)
} catch {
  case e: ServerIsDownException => {
    log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.")
    0
  }
} finally {
  remoteCalculatorService.close()
}

Этот код не является примером прекрасного стиля программирования, а просто пример того, как try-catch-finally вычисляет выражения, подобно всему остальному в Scala.

Finally будет вызван после того, как исключение будет обработано.