简体   繁体   中英

Using asInstanceOf on Generic Types in Scala

Say, I have a class like this:

class Funky[A, B](val foo: A, val bar: B) {
  override def toString: String = s"Funky($foo, $bar)"
}

With some method that looks like this:

def cast(t: Any): Option[Funky[A, B]] = {
  if (t == null) None
  else if (t.isInstanceOf[Funky[_, _]]) {
    val o = t.asInstanceOf[Funky[_, _]]
    for {
      _ <- typA.cast(o.foo)
      _ <- typB.cast(o.bar)
    } yield o.asInstanceOf[Funky[A, B]]
  } else None
}

How does the isInstanceOf and asInstanceOf work? The Runtime has no information on the actual types that are contained in Funky. So how is this code working? Any clues?

Let's just take it apart.

Suppose that you somehow obtain instances typA: Typable[A] and typB: Typable[B] , so that typA has a method

def cast(a: Any): Option[A] = ...

and similarly for typB . The cast method is expected to return Some[A] if the argument is indeed of type A , and None otherwise. These instances can obviously be constructed for primitive types Int and String (they are already provided by the library).

Now you want to use typA and typB to implement cast for Funky[A, B] .

The null check should be clear, you can perform it on anything. But then comes the first isInstanceOf :

  else if (t.isInstanceOf[Funky[_, _]]) {

Note that the type arguments of Funky have been replaced by underscores. This is because the generic parameters of Funky are erased, and not available at runtime. However, we still can differentiate between Funky[_, _] and, for example, Map[_, _] , because the parameterized type itself is preserved, even though the parameters are erased. Moreover, isInstanceOf could even differentiate between a TreeMap and a HashMap , even if both instances had compile-time type Map : the runtime type is available, it's only the generic parameters that are forgotten.

Similarly, once you know that t is of type Funky , you can cast it into Funky[_, _] ,

    val o = t.asInstanceOf[Funky[_, _]]

replacing the generic parameters by existential types. This means: you know only that there are some types X and Y such that o is of type Funky[X, Y] , but you don't know what those X and Y are. However, now you at least know that o has methods foo and bar (even though you don't know what their return types are).

But now you can take o.foo and o.bar and feed them into the typA.cast and typeB.cast . The underscores on the left hand side of the monadic bind mean that you discard the instances of type A and B , which are returned wrapped in Some . You only care that both casts don't return a None :

    for {
      _ <- typA.cast(o.foo)
      _ <- typB.cast(o.bar)
    } yield /* ... */

If one of the casts failed and returned a None , the entire monadic expression would evaluate to None , and so the method would return None , signifying that the overall cast into Funky[A, B] has failed. If both casts succeed, then we know that o is indeed of type Funky[A, B] , so we can cast o into Funky[A, B] :

o.asInstanceOf[Funky[A, B]]

You might wonder "how is this possible, we don't know anything about A and B at runtime!", but this is ok, because this asInstanceOf is there only to satisfy the type-checking stage of the compiler. It cannot do anything at runtime , because it can only check the Funky part, but not the erased parameters A and B .

Here is a shorter illustration of the phenomenon:

val m: Map[Long, Double] = Map(2L -> 100d)
val what = m.asInstanceOf[Map[Int, String]]
println("It compiles, and the program does not throw any exceptions!")

Just save it as a script and feed it to the scala interpreter. It will compile and run without complaints, because the asInstanceOf is blind for anything beyond Map . So, the second asInstanceOf is there only to persuade the type-checker that the returned value is indeed of the type Funky[A, B] , and it is the responsibility of the programmer not to make any nonsensical claims.

To summarize: isInstanceOf is the thing that does something at runtime (it checks that instances conform to some concrete type, and returns runtime-values true - false ). The asInstanceOf has two different functions. The first one (which casts to a concrete class Funky[_, _] ) has a side effect at runtime (the cast can fail and throw an exception). The second one ( asInstanceOf[Funky[A, B]] ) is there only to satisfy the type-checking phase at compile time.

Hope this helps somewhat.

You can check that the elements are specific types, with something like:

Funky(1, 2).foo.isInstanceOf[String] // false
Funky(1, 2).foo.isInstanceOf[Int] // true

But if you try to check for a generic type it will not work. For example:

def check[A](x: Any) = x.isInstanceOf[A]
check[String](1) // true
check[String](Funky(1, 2).foo) // true

And the compiler gives you a warning message that explains the error:

abstract type A is unchecked since it is eliminated by erasure

However, the code you showed seems to be working around this by some other method here:

_ <- typA.cast(o.foo)
_ <- typB.cast(o.bar)

Without seeing the implementation of those objects, my guess is they have some sort of TypeTag or ClassTag and use that. That is almost always the recommended way to work around erasure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM