ClassCastException в общей функции более высокого порядка

У меня есть код, который пытается обернуть функцию в другую, которая выполняет динамическую проверку типов:

class Base
class Foo extends Base
class Bar extends Base
object Main{
 def checker[A <: Base]( func : A => String) : Base => String =
 (b : Base) => b match {
 case a : A => func(a)
 case _ => "error"
 }
 def fooFunc(f : Foo) = "It a foo"
 def main(arg : Array[String]) {
 val check = checker(fooFunc)
 println(check(new Foo) + ", " + check(new Bar))
 }
}

Это приводит к следующей ошибке:

Exception in thread "main" java.lang.ClassCastException: Bar cannot be cast to Foo
 at Main$$anonfun$1.apply(Main.scala:17)
 at Main$.main(Main.scala:19)
 at Main.main(Main.scala)

Если я удалю параметр типа и заменим A на Foo в определении checker, он работает хорошо. Однако, если я сохраняю параметр типа, но опускаю аргумент функции и заменяю func (a) "хорошим", я получаю "хороший" как для Foo, так и для Bar.

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

3 ответа

Да, вы оказались в стране стирания.

В первом случае (исходный код) компилятор знает, что A есть Foo, но во время выполнения параметр типа стирается до ограничения верхнего типа, который в этом примере равен Base (если вы не указывайте привязку верхнего типа, параметр типа стирается до Object). Поэтому JVM видит ваш код следующим образом (не замечайте параметризацию типа):

def checker(func: Base => String): Base => String =
 (b: Base) => b match {
 case a : Base => func(a)
 case _ => "error"
 }

Любой объект Foo или Bar будет соответствовать Base, а затем JVM попытается применить его к Foo и вызвать func. Работает, если b является объектом класса Foo, но выбрасывает исключение для Bar. Никаких сюрпризов.

Во втором случае вы удаляете параметр типа и заменяете A на Foo в определении checker. Таким образом, ваш код во время выполнения выглядит так (функция не параметризуется с начала, поэтому ничего не стирается):

def checker(func: Foo => String): Base => String =
 (b: Base) => b match {
 case a: Foo => func(a)
 case _ => "error"
 }

Это работает так, как ожидалось, но исправлено только для Foo. Поэтому вам нужно написать отдельный checker для Bar и любой другой тип, который вы хотите проверить.

В третьем случае вы сохраняете параметр типа, но опускаете аргумент функции и заменяете func(a) на "good". Это приводит к аналогичному коду к случаю 1 (снова стирайте от Foo до Base), за исключением того, что func не вызывается, поэтому приведение в Foo не требуется. Следовательно, никакого исключения.

def checker: Base => String =
 (b: Base) => b match {
 case a: Base => "good"
 case _ => "error"
 }

Просто любой объект, который вы передаете (подкласс базы), сопоставляется первому предложению, а checker всегда возвращает "good".

Теперь решение. Вы были на правильном пути с помощью Manifest (но код, который вы показали, слишком сложный для того, чего вы пытаетесь достичь). Когда вы запрашиваете Manifest Scala, компилятор передает дополнительный объект методу/функции, который можно использовать во время выполнения, чтобы "восстановить" стертые типы (ы). Ниже я использую 'context bound' вместо того, чтобы указывать его как (implicit manifest : Manifest[A]), но это то же самое, короче.

def checker[A <: Base: Manifest](func: A => String): Base => String =
 (b: Base) => if(manifest[A].erasure == b.getClass) func(b.asInstanceOf[A])
 else "error"

Итак, когда вы называете это следующим образом:

def fooFunc(f: Foo) = "It a foo"
 def barFunc(f: Bar) = "It a bar"
 def main(arg: Array[String]) {
 val check1 = checker(fooFunc)
 val check2 = checker(barFunc)
 println(check1(new Foo) + ", " + check1(new Bar))
 println(check2(new Foo) + ", " + check2(new Bar))
 }

Вы получите результат как ожидалось:

It a foo, error
error, It a bar

Erasure является источником всех видов развлечений в Scala, поскольку параметризация типа здесь более распространена, чем в Java. Ни в коем случае, я рекомендую изучить все, что вы можете.


Я нашел способ использования манифестов.

class Base
class Foo extends Base
class Bar extends Base
trait Functor[A] {
 def apply[B](b : B)(implicit mb : Manifest[B]) : A
}
case class Checker[A](func : A => String)(implicit manifest : Manifest[A]) extends Functor[String]{
 def apply[B](b : B)(implicit mb : Manifest[B]) = {
 if (mb == manifest) func(b.asInstanceOf[A])
 else "error"
 }
}
object Main{
 def fooFunc(f : Foo) = "good"
 def main(arg : Array[String]) {
 val check = Checker(fooFunc)
 println(check(new Foo) + ", " + check(new Bar))
 }
}

Мне бы хотелось услышать предложения от кого-то, кто знает, что они делают, хотя.


как вы его определили, checker может принимать только функцию, которая принимает Foo.

Если вы также создадите общий файл fooFunc, он должен работать:

def fooFunc[A <: Base](f : A) = "It a foo"

но тогда fooFunc не будет подходящим именем, поскольку он может вернуть все, что происходит от Base.

def baseFunc[A <: Base](f : A) = "It a "+f.getClass

может быть то, что вы ищете

ИЗМЕНИТЬ

class Base
class Foo extends Base
class Bar extends Base
 def checker[A <: Base]( func : A => String) : Base => String =
 (b : Base) => b match {
 case a : A => func(a)
 case _ => "error"
 }
 def fooFunc[A <: Base](f : A) = "It a "+f.getClass.getName
 val check = checker(fooFunc)
 println(check(new Foo) + ", " + check(new Bar))

licensed under cc by-sa 3.0 with attribution.