Как мне обойти стирание типа на Scala? Или, почему я не могу получить параметр типа моих коллекций?

370

Печальный факт жизни в Scala заключается в том, что если вы создаете экземпляр List [Int], вы можете убедиться, что ваш экземпляр является списком, и вы можете проверить, что любой отдельный его элемент является Int, но не то, что это List [ Int], что легко проверить:

scala> List(1,2,3) match {
     | case l : List[String] => println("A list of strings?!")
     | case _ => println("Ok")
     | }
warning: there were unchecked warnings; re-run with -unchecked for details
A list of strings?!

Опция -unchecked прямо обвиняет в стирании типов:

scala>  List(1,2,3) match {
     |  case l : List[String] => println("A list of strings?!")
     |  case _ => println("Ok")
     |  }
<console>:6: warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
        case l : List[String] => println("A list of strings?!")
                 ^
A list of strings?!

Почему это так, и как мне обойти это?

Даниэль С. Собрал
источник
Scala 2.8 Beta 1 RC4 только что внесла некоторые изменения в работу стирания типов. Я не уверен, если это напрямую влияет на ваш вопрос.
Скотт Моррисон
1
Вот только какие типы стирание к , что изменилось. Суть его можно суммировать как « Предложение: стирание« Объекта с А »- это« А »вместо« Объекта ». ) Фактическая спецификация довольно сложна. Во всяком случае, речь идет о миксинах, и этот вопрос касается дженериков.
Даниэль С. Собрал
Спасибо за разъяснение - я новичок в Scala. Я чувствую, что сейчас плохое время, чтобы прыгнуть в Скалу. Раньше я мог бы узнать об изменениях в 2.8 на хорошей базе, позже мне никогда бы не пришлось знать разницу!
Скотт Моррисон
1
Вот несколько связанный вопрос о TypeTagс .
Пворб
2
В процессе работы scala 2.10.2я увидел это предупреждение: <console>:9: warning: fruitless type test: a value of type List[Int] cannot also be a List[String] (but still might match its erasure) case list: List[String] => println("a list of strings?") ^я считаю, что ваш вопрос и ответ очень полезны, но я не уверен, что это обновленное предупреждение полезно для читателей.
Кевин Мередит

Ответы:

243

В этом ответе используется Manifest-API, который устарел в Scala 2.10. Пожалуйста, смотрите ответы ниже для более актуальных решений.

Scala был определен с помощью типа Erasure, поскольку виртуальная машина Java (JVM), в отличие от Java, не получила обобщений. Это означает, что во время выполнения существует только класс, а не его параметры типа. В этом примере JVM знает, что он обрабатывает a scala.collection.immutable.List, но не этот параметр параметризован Int.

К счастью, в Scala есть функция, позволяющая обойти это. Это Манифест . Манифест - это класс, экземплярами которого являются объекты, представляющие типы. Поскольку эти экземпляры являются объектами, вы можете передавать их, хранить и вообще вызывать методы для них. С поддержкой неявных параметров он становится очень мощным инструментом. Возьмите следующий пример, например:

object Registry {
  import scala.reflect.Manifest

  private var map= Map.empty[Any,(Manifest[_], Any)] 

  def register[T](name: Any, item: T)(implicit m: Manifest[T]) {
    map = map.updated(name, m -> item)
  }

  def get[T](key:Any)(implicit m : Manifest[T]): Option[T] = {
    map get key flatMap {
      case (om, s) => if (om <:< m) Some(s.asInstanceOf[T]) else None
    }     
  }
}

scala> Registry.register("a", List(1,2,3))

scala> Registry.get[List[Int]]("a")
res6: Option[List[Int]] = Some(List(1, 2, 3))

scala> Registry.get[List[String]]("a")
res7: Option[List[String]] = None

При хранении элемента мы также храним его «Манифест». Манифест - это класс, экземпляры которого представляют типы Scala. Эти объекты содержат больше информации, чем JVM, что позволяет нам проверять полный параметризованный тип.

Обратите внимание, однако, что это Manifestвсе еще развивающаяся особенность. Как пример его ограничений, в настоящее время он ничего не знает о дисперсии и предполагает, что все является ко-вариантом. Я ожидаю, что она станет более стабильной и надежной, когда библиотека отражений Scala, находящаяся в стадии разработки, будет готова.

Даниэль С. Собрал
источник
3
getМетод может быть определен как for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Аарон Новструп
4
@ Аарон Очень хорошее предложение, но я боюсь, что оно может затенить код для людей, относительно новичков в Scala. У меня не было особого опыта работы со Scala, когда я писал этот код, что было когда-то, прежде чем я включил его в этот вопрос / ответ.
Даниэль С. Собрал
6
@KimStebel Вы знаете, что TypeTagна самом деле автоматически используются при сопоставлении с образцом? Круто, а?
Даниэль С. Собрал
1
Круто! Может быть, вы должны добавить это к ответу.
Ким Стебель
1
Чтобы ответить на мой собственный вопрос чуть выше: Да, компилятор генерирует сам Manifestпараметр, см .: stackoverflow.com/a/11495793/694469 «экземпляр [manifest / type-tag] [...] неявно создается компилятором "
KajMagnus
96

Вы можете сделать это, используя TypeTags (как уже упоминал Даниэль, но я просто объясню это явно):

import scala.reflect.runtime.universe._
def matchList[A: TypeTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if typeOf[A] =:= typeOf[String] => println("A list of strings!")
  case intlist: List[Int @unchecked] if typeOf[A] =:= typeOf[Int] => println("A list of ints!")
}

Вы также можете сделать это, используя ClassTags (что избавляет вас от необходимости зависеть от scala-рефлекса):

import scala.reflect.{ClassTag, classTag}
def matchList2[A : ClassTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if classTag[A] == classTag[String] => println("A List of strings!")
  case intlist: List[Int @unchecked] if classTag[A] == classTag[Int] => println("A list of ints!")
}

ClassTags можно использовать до тех пор, пока вы не ожидаете, что параметр типа Aсам по себе является универсальным типом.

К сожалению, это немного многословно, и вам нужна аннотация @unchecked для подавления предупреждения компилятора. TypeTag может быть включен в сопоставление с образцом автоматически компилятором в будущем: https://issues.scala-lang.org/browse/SI-6517

tksfz
источник
2
Как насчет удаления ненужного, так [List String @unchecked]как оно ничего не добавляет к этому сопоставлению с образцом (просто использование case strlist if typeOf[A] =:= typeOf[String] =>сделает это, или даже case _ if typeOf[A] =:= typeOf[String] =>если связанная переменная не нужна в теле case).
Надер Ганбари,
1
Я думаю, что это подойдет для данного примера, но я думаю, что большинство реальных применений выиграют от наличия типа элементов.
tksfz
В вышеприведенных примерах разве ли не проверяемая часть перед состоянием охраны выполняет приведение? Разве вы не получили бы исключение приведения класса при просмотре совпадений первого объекта, который не может быть приведен к строке?
Тоби
Хм, нет, я полагаю, что до применения защиты не произойдет приведение - неконтролируемый бит не работает, пока не будет выполнен код справа от него =>. (И когда код на rhs выполняется, охранники предоставляют статическую гарантию на тип элементов. Там может быть приведение, но это безопасно.)
tksfz
Создает ли это решение значительные накладные расходы во время выполнения?
stanislav.chetvertkov
65

Вы можете использовать Typeableкласс типов из бесформенного, чтобы получить результат, который вы ищете,

Образец сессии REPL,

scala> import shapeless.syntax.typeable._
import shapeless.syntax.typeable._

scala> val l1 : Any = List(1,2,3)
l1: Any = List(1, 2, 3)

scala> l1.cast[List[String]]
res0: Option[List[String]] = None

scala> l1.cast[List[Int]]
res1: Option[List[Int]] = Some(List(1, 2, 3))

castОперация будет столь же точным WRT стирания , насколько это возможно , учитывая в области видимости Typeableэкземпляры доступны.

Майлз Сабин
источник
14
Следует отметить, что операция «cast» будет рекурсивно проходить через всю коллекцию и ее подколлекции и проверять, все ли участвующие значения имеют правильный тип. (То есть l1.cast[List[String]]делает примерно for (x<-l1) assert(x.isInstanceOf[String]) Для больших структур данных или если преобразования происходят очень часто, это может быть недопустимым расходом.
Доминик Унру
16

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

case class StringListHolder(list:List[String])

StringListHolder(List("str1","str2")) match {
    case holder: StringListHolder => holder.list foreach println
}

Это имеет ожидаемый результат и ограничивает содержимое нашего класса case желаемым типом, String Lists.

Подробнее здесь: http://www.scalafied.com/?p=60

thricejamie
источник
14

В Scala есть способ преодолеть проблему стирания типов. В книге «Преодоление стирания типов при сопоставлении 1» и « Преодоление стирания типов при сопоставлении 2» (дисперсия) приводятся некоторые объяснения того, как закодировать некоторые помощники для переноса типов, включая дисперсию, для сопоставления.

axaluss
источник
Это не преодолевает стирание типа. В его примере выполняется val x: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s «Нет совпадений»)} создает «Нет совпадений»
user48956
Вы можете посмотреть макросы scala 2.10.
Алекс
11

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

В Scala проблема стирания типа не возникает с массивами. Я думаю, что это легче продемонстрировать на примере.

Допустим, у нас есть список (Int, String), тогда следующее выдает предупреждение об удалении типа

x match {
  case l:List[(Int, String)] => 
  ...
}

Чтобы обойти это, сначала создайте класс case:

case class IntString(i:Int, s:String)

затем в сопоставлении с образцом сделайте что-то вроде:

x match {
  case a:Array[IntString] => 
  ...
}

который, кажется, работает отлично.

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

Обратите внимание, что использование по- case a:Array[(Int, String)]прежнему выдаст предупреждение об удалении типа, поэтому необходимо использовать новый класс контейнера (в этом примереIntString ).

Jus12
источник
10
«Ограничение в остальном потрясающим языком» - это не ограничение Scala, а ограничение JVM. Возможно, Scala можно было бы спроектировать так, чтобы он включал информацию о типе, поскольку он работал на JVM, но я не думаю, что подобный дизайн сохранил бы совместимость с Java (т. Е., Как было задумано, вы можете вызывать Scala из Java.)
Carl G
1
В качестве продолжения поддержка постоянных обобщений для Scala в .NET / CLR является постоянной возможностью.
Карл Дж
6

Поскольку Java не знает фактический тип элемента, я нашел его наиболее полезным, чтобы просто использовать List[_]. Затем предупреждение исчезает, и код описывает реальность - это список чего-то неизвестного.

rained_in
источник
4

Мне интересно, если это подходящий обходной путь:

scala> List(1,2,3) match {
     |    case List(_: String, _*) => println("A list of strings?!")
     |    case _ => println("Ok")
     | }

Он не соответствует случаю «пустого списка», но выдает ошибку компиляции, а не предупреждение!

error: type mismatch;
found:     String
requirerd: Int

Это с другой стороны, кажется, работает ....

scala> List(1,2,3) match {
     |    case List(_: Int, _*) => println("A list of ints")
     |    case _ => println("Ok")
     | }

Разве это не даже лучше, или я здесь упускаю смысл?

agilesteel
источник
3
Не работает со списком (1, "a", "b"), который имеет тип List [Any]
sullivan-
1
Хотя точка зрения Салливана верна и есть проблемы с наследованием, я все же нашел это полезным.
Сет
0

Я хотел добавить ответ, который обобщает проблему: Как получить представление типа String типа моего списка во время выполнения

import scala.reflect.runtime.universe._

def whatListAmI[A : TypeTag](list : List[A]) = {
    if (typeTag[A] == typeTag[java.lang.String]) // note that typeTag[String] does not match due to type alias being a different type
        println("its a String")
    else if (typeTag[A] == typeTag[Int])
        println("its a Int")

    s"A List of ${typeTag[A].tpe.toString}"
}

val listInt = List(1,2,3)
val listString = List("a", "b", "c")

println(whatListAmI(listInt))
println(whatListAmI(listString))
Стив Робинсон-Бернс
источник
-18

Использование паттерна

    list match  {
        case x:List if x.isInstanceOf(List[String]) => do sth
        case x:List if x.isInstanceOf(List[Int]) => do sth else
     }
Хуангмао Цюань
источник
4
Причина, по которой этот isInstanceOfметод не будет работать, заключается в том, что выполняется проверка во время выполнения на основе информации о типе, доступной для JVM. И эта информация во время выполнения не будет содержать аргумент типа для List(из-за стирания типа).
Доминик Унру,