Согласно документации стандартной библиотеки Scala, ClassTag
определяется следующим образом.
A ClassTag[T]
хранит стертый класс заданного типа T
, доступный через runtimeClass
поле. Это особенно полезно для создания экземпляров Array
s, типы элементов которых неизвестны во время компиляции.
Таким образом, всякий раз, когда мы хотим получить доступ к информации о типе во время выполнения, мы можем использовать 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
между несовместимыми типами.
В нашей третьей попытке мы обратились к следующему:
- Пользователь более осведомлен о типах, с которыми он имеет дело сейчас, что означает, что не будет никаких ненужных ошибок во время выполнения из-за невежества пользователя. Другими словами, пользователь теперь лучше осведомлен о типах, с которыми он имеет дело, вместо того, чтобы присваивать значение супертипу «Любой», а затем приводить его к нужному типу.
- Нет
asInstanceOf
отливка не требуется. Если ключ найден, мы получимSome[value]
, в противном случае мы получимNone
. - Это масштабируемый
Все идет нормально. Но неприятная ошибка возникает, как показано выше, когда пользователь по ошибке использует неправильный тип и значение существует.
Например, следующее работает нормально, но не должно.
//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
.