Статьи

Как использовать Scala ClassTag

Согласно документации стандартной библиотеки Scala,  ClassTag определяется следующим образом.

A ClassTag[T]хранит стертый класс заданного типа T, доступный через runtimeClassполе. Это особенно полезно для создания экземпляров Arrays, типы элементов которых неизвестны во время компиляции.

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

Без дальнейших церемоний, давайте погрузимся в наш вариант использования.

Случай использования

Учитывая  Map[String, Any],  мы хотим проверить,  Map содержит ли значение требуемого типа для данного ключа. Если такое значение существует, верните его и обработайте изящно. Звучит просто, правда? Посмотрим, как мы к этому подходим. Давайте не будем вдаваться в дискуссию о том, правильно ли использовать  Any тип в  Map . Давайте предположим, что такая ситуация существует в приложении, и приступим к пониманию нашей задачи  ClassTag.

Первая попытка

Следующее является нашей первой попыткой удовлетворить наш вариант использования.

  def main(args: Array[String]): Unit = {
    class Animal
    val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
      "Animal" -> new Animal)

    /* the following will not work because of compilation error
     * we are trying to assining Any to Int
     */
    //val number:Int = myMap("Number")
    //println("number is " + number)

    // Now this works because we are casting the value into Int
    val number:Int = myMap("Number").asInstanceOf[Int]
    println("number  is " + number)

    //the following will throw ClassCastException
    val greeting:String = myMap("Number").asInstanceOf[String]

  }

Очевидно, что в приведенном выше коде есть несколько проблем.

Во-первых, компилятор пожалуется на то, что видит следующую строку, потому что мы пытаемся присвоить значение типа «Any» типу «Int»:

// this will not compile
val number:Int = myMap("Number")

Да, компилятор верен, но дело в том, что мы не можем использовать тип, который, как мы знаем, мы выберем из нашей карты. Другими словами, мы не используем систему богатых типов, предоставляемую Scala, если мы просто присваиваем значение типу «Any». Нам нужен какой-то способ это исправить.

Итак, чтобы сделать компилятор счастливым, мы прибегаем к кастингу. Теперь мы можем присвоить значение из Map переменной Int без каких-либо ошибок компилятора.

val number:Int = myMap("Number").asInstanceOf[Int]

Это работает, но  asInstanceOf будет выброшено,  ClassCastException когда мы попытаемся выполнить приведение между несовместимыми типами (скажем, из-за непреднамеренной ошибки пользователя).

Итак, следующее является проблемой во время выполнения, потому что мы пытаемся привести значение Int к String.

val greeting:String = myMap("Number").asInstanceOf[String]

Мы вводим новую проблему, решая ее.

Даже если мы перейдем к  get() методу Map, мы не сможем присвоить  Option[Any] его,  Option[Int] потому что компилятор снова не будет счастлив, и мы вернулись к исходной точке.

 // The following will not compile assigning Option[Any] to Option[Int]
 val number:Option[Int] = myMap.get("Number")

Теперь вы можете быть удивлены, почему  Map[String, Any] существует в любом приложении. Хотя это запах кода, давайте на некоторое время отключим наш опыт проектирования и предположим, что Map[String, Any] в приложении существует сценарий  . Давайте вернемся к нашей проблеме.

Очевидно, что использование  asInstanceOf не является хорошей практикой, и, как мы видим выше, существует вероятность того, что  ClassCastException мы попытаемся выполнить приведение между несовместимыми типами (например, String to Int).

Один из способов справиться с этим ClassCastException — окружить его методом  try / catch, и если мы воспользуемся этим подходом, это не будет действительно масштабируемым и элегантным решением. Итак, давайте не будем пытаться это сделать.

Вторая попытка

// getValueFromMap for the Int, String and Animal
  def getValueFromMapForInt(key:String, dataMap: collection.Map[String, Any]): Option[Int] =
    dataMap.get(key) match {
      case Some(value:Int) => Some(value)
      case _ => None
    }

  def getValueFromMapForString(key:String, dataMap: collection.Map[String, Any]): Option[String] =
    dataMap.get(key) match {
      case Some(value:String) => Some(value)
      case _ => None
    }

  def getValueFromMapForAnimal(key:String, dataMap: collection.Map[String, Any]): Option[Animal] =
    dataMap.get(key) match {
      case Some(value:Animal) => Some(value)
      case _ => None
    }

  def main(args: Array[String]): Unit = {
    class Animal {
    override def toString = "I am Animal"
  }
    val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
      "Animal" -> new Animal)

    // returns Some(1)
    val number1:Option[Int] = getValueFromMapForInt("Number", myMap)
    println("number is " + number1)

    // returns None
    val numberNotExists:Option[Int] = getValueFromMapForInt("Number2", myMap)
    println("number is " + numberNotExists)

    println

    // returns Some(Hello World)
    val greeting:Option[String] = getValueFromMapForString("Greeting", myMap)
    println("greeting is " + greeting)

    // returns None
    val greetingDoesNotExists:Option[String] = getValueFromMapForString("Greeting1", myMap)
    println("greeting is " + greetingDoesNotExists)

    println()

    // returns Some[Animal]
    val animal:Option[Animal] = getValueFromMapForAnimal("Animal", myMap)
    println("Animal is " + animal)

    // returns None
    val animalDoesNotExist:Option[Animal] = getValueFromMapForAnimal("Animal1", myMap)
    println("Animal is " + animalDoesNotExist)

  }

Вывод вышеуказанного кода приведен ниже:

number is Some(1)
number is None

greeting is Some(Hello World)
greeting is None

Animal is Some(I am Animal)
Animal is None

Теперь у нас есть решение, чтобы избежать  ClassCastException. У нас есть  getValueFromMapForXXX метод, где XXX принимает тип значения, которое мы ожидаем от карты.

Проблемы, которые мы видели в предыдущей версии, теперь исчезли. У нас его нет,  ClassCastException потому что мы вообще не выполняем приведение, и компилятор также ловит нас, если мы пытаемся назначить неправильный тип без использования  asInstanceOf.

Но, тем не менее, решение не является хорошим, потому что это не масштабируемое решение. Зачем? Это потому, что теперь мы должны ввести  getValueFromMapForXXX для каждого возможного типа (XXX) значений на карте.

Третья попытка

Давайте попробуем решить проблему масштабируемости в нашей предыдущей попытке, используя параметр типа в нашем  getValueFromMapForXXX методе.

def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
    dataMap.get(key) match {
      case Some(value:T) => Some(value)
      case _ => None
    }

Теперь у нас есть один  getValueFromMap[T] метод, сигнатура которого совпадает с его предыдущей версией, но теперь он принимает параметр типа T.

// getValueFromMap which takes type parameter T 
// Now this single method will give the values for Int, String and Animal
// from our Map
  def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
    dataMap.get(key) match {
      case Some(value:T) => Some(value)
      case _ => None
    }


  def main(args: Array[String]): Unit = {
    class Animal {
      override def toString = "I am Animal"
    }

    val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
      "Animal" -> new Animal)

    // returns Some(1)
    val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)
    println("number is " + number1)

    // returns None
    val numberNotExists:Option[Int] = getValueFromMap[Int]("Number2", myMap)
    println("number is " + numberNotExists)

    println

    // returns Some(Hello World)
    val greeting:Option[String] = getValueFromMap[String]("Greeting", myMap)
    println("greeting is " + greeting)

    // returns None
    val greetingDoesNotExists:Option[String] = getValueFromMap[String]("Greeting1", myMap)
    println("greeting is " + greetingDoesNotExists)

    println()

    // returns Some[Animal]
    val animal:Option[Animal] = getValueFromMap[Animal]("Animal", myMap)
    println("Animal is " + animal)

    // returns None
    val animalDoesNotExist:Option[Animal] = getValueFromMap[Animal]("Animal1", myMap)
    println("Animal is " + animalDoesNotExist)

    println

    // PROBLEM STARTS HERE
    // The following is really a problem and
    // now there is nothing in the control of compiler.
    // Even if the return from getValueFromMap is Option[String]
    // the compiler does not complain because it is all happening at runtime.
    // The compiler does not have any control over the runtime behavior now.
    // The consequence is that it
    // gives a problem when we take this Option value and go for 
    // some integer tranformation

    //Wow, assigning Option[String] to Option[Int]
    //Still works. No one complains
    val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)

    // prints Some(Hello World)
    println("greetingInt is " + greetingInt)

    // The problem comes here and now it will throw the ClassCastException
    val somevalue = greetingInt.map((x) => x + 5)
    // The following will not get printed at all
    println(somevalue)

  }

Вывод вышеуказанного кода приведен ниже:

number is Some(1)
number is None

greeting is Some(Hello World)
greeting is None

Animal is Some(I am Animal)
Animal is None

greetingInt is Some(Hello World)
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
at scala.runtime.java8.JFunction1$mcII$sp.apply(JFunction1$mcII$sp.java:12)
at scala.Option.map(Option.scala:146)

Теперь все, что нам нужно сделать, это просто вызвать метод, передав ожидаемый тип, ключ и карту, чтобы получить значение из нашей карты. Итак, чтобы получить значение Int из нашей карты для ключа «Number», все, что нам нужно сделать, — это вызвать метод с этой информацией.

 val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)

Итак, теперь у нас есть масштабируемое решение, потому что у нас есть только один  getValueFromMap метод, и у нас нет других проблем, таких как ошибка компилятора для несовместимых типов или время выполнения  ClassCastException  из-за выполнения  asInstanceOf между несовместимыми типами.

В нашей третьей попытке мы обратились к следующему:

  1. Пользователь более осведомлен о типах, с которыми он имеет дело сейчас, что означает, что не будет никаких ненужных ошибок во время выполнения из-за невежества пользователя. Другими словами, пользователь теперь лучше осведомлен о типах, с которыми он имеет дело, вместо того, чтобы присваивать значение супертипу «Любой», а затем приводить его к нужному типу.
  2. Нет  asInstanceOf отливка не требуется. Если ключ найден, мы получим  Some[value], в   противном случае мы получим   None.
  3. Это масштабируемый

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

Например, следующее работает нормально, но не должно.

//Wow, assigning Option[String] to Option[Int]
val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)

Мы знаем, что ключ «Приветствие» на нашей карте имеет значение «Строка», но мы назначаем его  Option[Int]. Удивительная часть в том, что это работает. Он даже печатает значение.

// prints Some(Hello World)
println("greetingInt is " + greetingInt)

Это приведет к неожиданным результатам, когда мы примем это значение и сделаем несколько монадических преобразований.

Следующее даст  ClassCastException.

// The problem comes here and now it will throw the ClassCastException
val somevalue = greetingInt.map((x) => x + 5)
// The following will not get printed at all because of above exception
println(somevalue)

Разве это не наш метод  getValueFromMap[T] должен захватить это?

Есть ли проблема в этом методе? Давайте посмотрим на метод снова.

def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
    dataMap.get(key) match {
      case Some(value:T) => Some(value)
      case _ => None
    }

Метод выглядит хорошо. Все, что мы делаем, это вызываем  get() метод переданного ключа и проверяем, существует ли «значение» переданного «T». По сути, мы ищем, Some(value:T) существует ли  , и если он существует, то мы просто возвращаем его. В противном случае он должен вернуться  None.

Таким образом, в нашем случае, когда мы передаем ключ «Приветствие» и набираем «Int», он должен проверить, Some(value:Int) существует ли   для ключа «Приветствие», и вернуть значение,  Some(value) если значение существует на карте, и, в противном случае, Нет. Очевидно, что в этом случае он должен вернуть «None», потому что ключ «Greeting» имеет значение «Hello World», но это String, а не Int. Но мы просили значение Int при вызове метода, передав ключ «Приветствие».

Вопрос в том, почему следующий оператор case внутри метода не уловил этого.

case Some(value:T) => Some(value)

Проблема состоит в том, что среда выполнения только проверяет, имеет ли она какое-либо значение для ключа, и не проверяет, принадлежит ли значение переданному типу «T». Почему? Среда выполнения не имеет никакой информации о типе, который мы передали вообще. Во время выполнения «T» (в нашем случае «Int») стирается и не существует. Это называется Type Erasure в мире JVM. Вот почему метод не может уловить эту ошибку.

Итак, теперь нам нужно убедиться, что тип «T», который мы передаем, также существует во время выполнения и помогает среде выполнения в некоторой логике. Добро пожаловать в мир  ClassTag.

Все, что нам нужно сделать, это передать  ClassTag[T] неявно. Итак, если мы хотим получить значение из карты типа Int, то нам нужно передать  ClassTag[Int] объект неявно. Затем, при вызове метода, нам не нужно предоставлять неявное,  ClassTag, и компилятор автоматически предоставит его нам.

Итак, теперь  getValueFromMap будет так, как показано ниже:

 def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any])(implicit t:ClassTag[T]): Option[T] = {
    dataMap.get(key) match {
      case Some(value: T) => Some(value)
      case _ => None
    }
  }

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

def getValueFromMap[T : ClassTag](key:String, dataMap: collection.Map[String, Any]): Option[T] = {
    dataMap.get(key) match {
      case Some(value: T) => Some(value)
      case _ => None
    }

Вызывающая сторона не изменяется вообще, потому что компилятор предоставляет неявный  ClassTag[T] параметр.

Последняя попытка

// getValueFromMap
  def getValueFromMap[T : ClassTag](key:String, dataMap: collection.Map[String, Any]): Option[T] = {
    dataMap.get(key) match {
      case Some(value: T) => Some(value)
      case _ => None
    }
  }

  def main(args: Array[String]): Unit = {
    class Animal {
      override def toString = "I am Animal"
    }

    val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
      "Animal" -> new Animal)

    // returns Some(1)
    val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)
    println("number is " + number1)

    // returns None
    val numberNotExists:Option[Int] = getValueFromMap[Int]("Number2", myMap)
    println("number is " + numberNotExists)

    println

    // returns Some(Hello World)
    val greeting:Option[String] = getValueFromMap[String]("Greeting", myMap)
    println("greeting is " + greeting)

    // returns None
    val greetingDoesNotExists:Option[String] = getValueFromMap[String]("Greeting1", myMap)
    println("greeting is " + greetingDoesNotExists)

    println()

    // returns Some[Animal]
    val animal:Option[Animal] = getValueFromMap[Animal]("Animal", myMap)
    println("Animal is " + animal)

    // returns None
    val animalDoesNotExist:Option[Animal] = getValueFromMap[Animal]("Animal1", myMap)
    println("Animal is " + animalDoesNotExist)

    println

    // NOW THERE IS NO PROBLEM BECAUSE WE WILL GET None
    val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)

    // prints None
    println("greetingInt is " + greetingInt)

    // No ClassCastException because it will give None
    val somevalue = greetingInt.map((x) => x + 5)
    // The following will print None
    println(somevalue)

    println

    // other map with list
    val someMap = Map("Number" -> 1, "Greeting"->"Hello World",
      "Animal" -> new Animal, "goodList" -> List("good", "better", "best"))

    // gets the list from map
    val goodList:Option[List[String]] = getValueFromMap[List[String]]("goodList", someMap)
    // prints the list
    println(goodList)
    println

    println("Now let us try to get bad list")

    // tries to get bad list from the map
    val badListNotExists:Option[List[String]] = getValueFromMap[List[String]]("badList", someMap)
    // prints None
    println(badListNotExists)

  }

Вывод, произведенный вышеуказанным кодом, выглядит следующим образом:

number is Some(1)
number is None

greeting is Some(Hello World)
greeting is None

Animal is Some(I am Animal)
Animal is None

greetingInt is None
None

Some(List(good, better, best))

Now let us try to get bad list
None

Теперь все наши проблемы исчезли, благодаря  ClassTag, что помогает среде выполнения получить информацию о параметре типа, который мы передали.

Заключение

ClassTag полезен при передаче информации о параметре типа T во время выполнения. Это было бы удобно во многих ситуациях в наших приложениях. Однако у него есть свои ограничения. Мы можем получить только информацию более высокого типа из,  ClassTag а не тип аргумента переданного более высокого типа.

Например, в приведенном выше коде, если мы использовали следующее:

val goodList:Option[List[Int]] = getValueFromMap[List[Int]]("goodList", someMap)
    // prints the list
    println(goodList)

Вместо:

val goodList:Option[List[String]] = getValueFromMap[List[String]]("goodList", someMap)
    // prints the list
    println(goodList)

Тем не менее, это будет работать, потому что  ClassTag единственное обеспечивает более высокую информацию о типе во время выполнения (в нашем случае это «Список»), а не информацию о типе аргумента (в нашем случае это Int или String). Вот почему, даже если мы перейдем  getValueFromMap[List[Int]] к ключу  goodList, код будет работать. В большинстве ситуаций нам просто нужно иметь более высокий тип, а не тип аргумента во время выполнения. В случае, если нам нужна информация о типе аргумента вместе с более высоким типом во время выполнения, тогда мы должны пойти на  TypeTag.