web-dev-qa-db-fra.com

Comment puis-je contourner l'effacement des caractères sur Scala? Pourquoi ne puis-je pas obtenir le paramètre de type de mes collections?

C'est une triste réalité de la vie sur Scala que si vous instanciez une liste [Int], vous pouvez vérifier que votre instance est une liste et que tout élément individuel de celle-ci est un entier, mais pas qu'il s'agisse d'une liste [ Int], comme on peut facilement le vérifier:

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?!

L'option -unchecked attribue directement le blâme à l'effacement du type:

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?!

Pourquoi est-ce et comment puis-je le contourner?

359
Daniel C. Sobral

Cette réponse utilise la Manifest- API, obsolète à partir de Scala 2.10. Veuillez voir les réponses ci-dessous pour des solutions plus actuelles.

Scala a été défini avec Type Erasure car la machine virtuelle Java (JVM), contrairement à Java, n’a pas reçu les génériques. Cela signifie que, au moment de l'exécution, seule la classe existe, pas ses paramètres de type. Dans cet exemple, JVM sait qu'elle gère un scala.collection.immutable.List, mais ne précise pas que cette liste est paramétrée avec Int.

Heureusement, il existe une fonctionnalité dans Scala qui vous permet de contourner ce problème. C’est le manifeste . Un manifeste est une classe dont les instances sont des objets représentant des types. Comme ces instances sont des objets, vous pouvez les faire circuler, les stocker et généralement appeler des méthodes dessus. Avec le support de paramètres implicites, il devient un outil très puissant. Prenons l'exemple suivant, par exemple:

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

Lorsque vous stockez un élément, nous en stockons également un "manifeste". Un manifeste est une classe dont les instances représentent des types Scala. Ces objets contiennent plus d'informations que la machine virtuelle Java, ce qui nous permet de tester le type paramétré complet.

Notez cependant qu'une Manifest est toujours une fonctionnalité en évolution. À titre d'exemple de ses limites, il ne connait actuellement rien à la variance et suppose que tout est co-variant. Je pense qu’elle deviendra plus stable et solide une fois que la bibliothèque de réflexions Scala, en cours de développement, sera terminée.

239
Daniel C. Sobral

Vous pouvez le faire en utilisant TypeTags (comme Daniel l'a déjà mentionné, mais je vais simplement le préciser explicitement):

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!")
}

Vous pouvez également le faire en utilisant ClassTags (ce qui vous évite de dépendre de scala-reflect):

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!")
}

Les ClassTags peuvent être utilisés tant que vous ne vous attendez pas à ce que le paramètre de type A soit lui-même un type générique.

Malheureusement, c'est un peu détaillé et vous avez besoin de l'annotation @unchecked pour supprimer un avertissement du compilateur. Le TypeTag peut être incorporé automatiquement dans la correspondance de modèle par le compilateur à l'avenir: https://issues.scala-lang.org/browse/SI-6517

92
tksfz

Vous pouvez utiliser la classe de type Typeable de informeless pour obtenir le résultat recherché.

Exemple de session 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))

L'opération cast sera aussi précise que possible, compte tenu des instances Typeable dans la portée disponibles.

61
Miles Sabin

J'ai proposé une solution relativement simple qui suffirait dans des situations d'utilisation limitée, consistant essentiellement à encapsuler des types paramétrés qui seraient affectés par le problème d'effacement de type dans les classes wrapper pouvant être utilisées dans une instruction match.

case class StringListHolder(list:List[String])

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

Ceci a la sortie attendue et limite le contenu de notre classe de cas au type souhaité, Listes de chaînes.

Plus de détails ici: http://www.scalafied.com/?p=60

15
thricejamie

Il existe un moyen de résoudre le problème de l'effacement des types dans Scala. Dans Surmonter l’effacement de type dans la correspondance 1 et Surmonter l’effacement de type dans la correspondance 2 (Variance) expliquent comment coder certaines aides pour envelopper les types, y compris Variance, pour l'appariement. 

14
Alex

J'ai trouvé une solution légèrement meilleure pour cette limitation du langage génial. 

Dans Scala, le problème de l'effacement des types ne se produit pas avec les tableaux. Je pense qu'il est plus facile de démontrer cela avec un exemple. 

Disons que nous avons une liste de (Int, String), alors ce qui suit donne un avertissement de type effacement

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

Pour contourner ce problème, créez d'abord une classe de cas:

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

puis, dans le filtrage, faites quelque chose comme:

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

qui semble fonctionner parfaitement. 

Cela nécessitera des modifications mineures dans votre code pour pouvoir utiliser des tableaux plutôt que des listes, mais cela ne devrait pas être un problème majeur.

Notez que l'utilisation de case a:Array[(Int, String)] donnera toujours un avertissement d'effacement de type. Il est donc nécessaire d'utiliser une nouvelle classe de conteneur (dans cet exemple, IntString).

11
Jus12

Comme Java ne connaît pas le type d’élément, j’ai trouvé très utile d’utiliser simplement List[_]. Puis l'avertissement s'en va et le code décrit la réalité - c'est une liste de quelque chose d'inconnu.

6
rained_in

Je me demande s'il s'agit d'une solution de contournement adaptée:

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

Cela ne correspond pas à la casse "liste vide", mais cela donne une erreur de compilation, pas un avertissement!

error: type mismatch;
found:     String
requirerd: Int

Cela par contre semble fonctionner ....

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

N'est-ce pas encore mieux ou est-ce que je manque le point ici?

4
agilesteel

Pas une solution, mais une façon de vivre avec sans le dissimuler complètement: Ajouter l’annotation @unchecked. Voir ici - http://www.scala-lang.org/api/current/index.html#scala.unchecked

1
matanster

Je voulais ajouter une réponse qui généralise le problème à: Comment obtenir une représentation sous forme de chaîne du type de ma liste à l'exécution

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))
0
Steve Robinson