Scala двойное определение (2 метода имеют один и тот же тип стирания)



Я написал это в scala, и он не будет компилироваться:



class TestDoubleDef{
def foo(p:List[String]) = {}
def foo(p:List[Int]) = {}
}


компилятор сообщит:



[error] double definition:
[error] method foo:(List[String])Unit and
[error] method foo:(List[Int])Unit at line 120
[error] have same type after erasure: (List)Unit


Я знаю, что JVM не имеет собственной поддержки для дженериков, поэтому я понимаю эту ошибку.



я мог бы написать обертки для List[String] и List[Int] но я ленивый :)



Я сомневаюсь, но есть ли другой способ выражения List[String] - это не тот же тип, чем List[Int]?



спасибо.

585   11  

11 ответов:

мне нравится идея Майкла Кремера использовать неявные преобразования, но я думаю, что он может быть применен более прямо:

case class IntList(list: List[Int])
case class StringList(list: List[String])

implicit def il(list: List[Int]) = IntList(list)
implicit def sl(list: List[String]) = StringList(list)

def foo(i: IntList) { println("Int: " + i.list)}
def foo(s: StringList) { println("String: " + s.list)}

Я думаю, что это довольно читабельно и просто.

[обновление]

есть еще один простой способ, который, кажется, работает:

def foo(p: List[String]) { println("Strings") }
def foo[X: ClassManifest](p: List[Int]) { println("Ints") }
def foo[X: ClassManifest, Y: ClassManifest](p: List[Double]) { println("Doubles") }

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

[обновление 2]

для ровно двух методов я нашел еще один хороший трюк:

def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}

вместо того, чтобы изобретать фиктивные неявные значения, вы можете использовать DummyImplicit определена в Predef который, кажется, сделан именно для этого:

class TestMultipleDef {
  def foo(p:List[String]) = ()
  def foo(p:List[Int])(implicit d: DummyImplicit) = ()
  def foo(p:List[java.util.Date])(implicit d1: DummyImplicit, d2: DummyImplicit) = ()
}

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

понять решение Михаэля Кремера, необходимо признать, что типы неявных параметров не имеют значения. Что и важно то, что их типы различны.

следующий код работает таким же образом:

class TestDoubleDef {
   object dummy1 { implicit val dummy: dummy1.type = this }
   object dummy2 { implicit val dummy: dummy2.type = this }

   def foo(p:List[String])(implicit d: dummy1.type) = {}
   def foo(p:List[Int])(implicit d: dummy2.type) = {}
}

object App extends Application {
   val a = new TestDoubleDef()
   a.foo(1::2::Nil)
   a.foo("a"::"b"::Nil)
}

на уровне байт-кода, оба foo методы становятся методами с двумя аргументами, поскольку байт-код JVM ничего не знает о неявных параметрах или списках нескольких параметров. На месте вызова, Scala компилятор выбирает соответствующий foo метод для вызова (и, следовательно, соответствующий фиктивный объект для передачи), глядя на тип передаваемого списка (который не стирается до тех пор, пока позже).

хотя это более подробно, этот подход освобождает вызывающего от бремени предоставления неявных аргументов. На самом деле, это даже работает, если объекты dummyN являются частными для TestDoubleDef класса.

Как уже сказал Виктор Кланг, универсальный тип будет удален компилятором. К счастью, есть обходной путь:

class TestDoubleDef{
  def foo(p:List[String])(implicit ignore: String) = {}
  def foo(p:List[Int])(implicit ignore: Int) = {}
}

object App extends Application {
  implicit val x = 0
  implicit val y = ""

  val a = new A()
  a.foo(1::2::Nil)
  a.foo("a"::"b"::Nil)
}

спасибо Michid за совет!

если я совмещаю Даниилs ответ и Шандор Murakoziответ здесь:

@annotation.implicitNotFound(msg = "Type ${T} not supported only Int and String accepted")   
sealed abstract class Acceptable[T]; object Acceptable {
        implicit object IntOk extends Acceptable[Int]
        implicit object StringOk extends Acceptable[String]
}

class TestDoubleDef {
   def foo[A : Acceptable : Manifest](p:List[A]) =  {
        val m = manifest[A]
        if (m equals manifest[String]) {
            println("String")
        } else if (m equals manifest[Int]) {
            println("Int")
        } 
   }
}

Я получаю typesafe (ish) вариант

scala> val a = new TestDoubleDef
a: TestDoubleDef = TestDoubleDef@f3cc05f

scala> a.foo(List(1,2,3))
Int

scala> a.foo(List("test","testa"))
String

scala> a.foo(List(1L,2L,3L))
<console>:21: error: Type Long not supported only Int and String accepted
   a.foo(List(1L,2L,3L))
        ^             

scala> a.foo("test")
<console>:9: error: type mismatch;
 found   : java.lang.String("test")
 required: List[?]
       a.foo("test")
             ^

логика также может быть включена в класс type как таковой (благодаря jsuereth): @комментарий.implicitNotFound(msg = " Foo не поддерживает ${T} только Int и строка принята") запечатанный признак Foo [T] { def apply(list : List[T]): Unit }

object Foo {
   implicit def stringImpl = new Foo[String] {
      def apply(list : List[String]) = println("String")
   }
   implicit def intImpl = new Foo[Int] {
      def apply(list : List[Int]) =  println("Int")
   }
} 

def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)

что дает:

scala> @annotation.implicitNotFound(msg = "Foo does not support ${T} only Int and String accepted") 
     | sealed trait Foo[T] { def apply(list : List[T]) : Unit }; object Foo {
     |         implicit def stringImpl = new Foo[String] {
     |           def apply(list : List[String]) = println("String")
     |         }
     |         implicit def intImpl = new Foo[Int] {
     |           def apply(list : List[Int]) =  println("Int")
     |         }
     |       } ; def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)
defined trait Foo
defined module Foo
foo: [A](x: List[A])(implicit evidence: Foo[A])Unit

scala> foo(1)
<console>:8: error: type mismatch;
 found   : Int(1)
 required: List[?]
       foo(1)
           ^    
scala> foo(List(1,2,3))
Int
scala> foo(List("a","b","c"))
String
scala> foo(List(1.0))
<console>:32: error: Foo does not support Double only Int and String accepted
foo(List(1.0))
        ^

обратите внимание, что мы должны написать implicitly[Foo[A]].apply(x) так как компилятор думает, что implicitly[Foo[A]](x) означает, что мы называем implicitly с параметрами.

есть (по крайней мере один) другой способ, даже если это не слишком приятно и не очень безопасно для типа:

import scala.reflect.Manifest

object Reified {

  def foo[T](p:List[T])(implicit m: Manifest[T]) = {

    def stringList(l: List[String]) {
      println("Strings")
    }
    def intList(l: List[Int]) {
      println("Ints")
    }

    val StringClass = classOf[String]
    val IntClass = classOf[Int]

    m.erasure match {
      case StringClass => stringList(p.asInstanceOf[List[String]])
      case IntClass => intList(p.asInstanceOf[List[Int]])
      case _ => error("???")
    }
  }


  def main(args: Array[String]) {
      foo(List("String"))
      foo(List(1, 2, 3))
    }
}

неявный манифест paramenter может быть использован для" овеществления " стираемого типа и, таким образом, взломать вокруг стирания. Вы можете узнать немного больше об этом во многих сообщениях в блоге, например этот.

что происходит, так это то, что манифестный парам может вернуть вам то, что было до стирания. Тогда простая отправка, основанная на T для различных реальных реализаций, делает остальное.

вероятно, есть более хороший способ сделать сопоставление шаблонов, но я его еще не видел. То, что люди обычно делают, соответствует m.toString, но я думаю, что сохранение классов немного чище (даже если это немного более подробно). К сожалению, документация манифеста не слишком детализирована, возможно, в ней также есть что-то, что может упростить ее.

большим недостатком этого является то, что это не совсем безопасно для типа: foo будет доволен любым T, если вы не можете справиться с этим, вы будете иметь проблему. Я думаю, что это можно было бы обойти с некоторыми ограничениями на T, но это еще больше усложнило бы его.

и конечно все это тоже не слишком приятно, я не уверен, стоит ли это делать, особенно если вы ленивы ; -)

вместо использования манифестов можно также использовать объекты dispatchers, неявно импортированные аналогичным образом. Я написал об этом в блоге, прежде чем появились манифесты:http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

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

Я попытался улучшить ответы Аарона Новструпа и Лео, чтобы сделать один набор стандартных объектов доказательств импортируемым и более кратким.

final object ErasureEvidence {
    class E1 private[ErasureEvidence]()
    class E2 private[ErasureEvidence]()
    implicit final val e1 = new E1
    implicit final val e2 = new E2
}
import ErasureEvidence._

class Baz {
    def foo(xs: String*)(implicit e:E1) = 1
    def foo(xs: Int*)(implicit e:E2) = 2
}

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

таким образом, я предлагаю только следующее, которое в некоторых случаях является более кратким. И это улучшение работает с классами значений (те, что extend AnyVal).

final object ErasureEvidence {
   class E1[T] private[ErasureEvidence]()
   class E2[T] private[ErasureEvidence]()
   implicit def e1[T] = new E1[T]
   implicit def e2[T] = new E2[T]
}
import ErasureEvidence._

class Baz {
    def foo(xs: String*)(implicit e:E1[Baz]) = 1
    def foo(xs: Int*)(implicit e:E2[Baz]) = 2
}

если содержащее имя типа довольно длинное, объявите внутренний trait чтобы сделать его более кратким.

class Supercalifragilisticexpialidocious[A,B,C,D,E,F,G,H,I,J,K,L,M] {
    private trait E
    def foo(xs: String*)(implicit e:E1[E]) = 1
    def foo(xs: Int*)(implicit e:E2[E]) = 2
}

однако классы значений не допускают внутренних признаков, классов или объектов. При этом также обратите внимание, что ответы Аарона Новструпа и Лео не работают с классами значений.

хороший трюк я нашел от http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html Аарон Новструп

избиение этой мертвой лошади еще немного...

мне пришло в голову, что более чистый Хак должен использовать уникальный фиктивный тип для каждого метода со стертыми типами в его сигнатуре:

object Baz {
    private object dummy1 { implicit val dummy: dummy1.type = this }
    private object dummy2 { implicit val dummy: dummy2.type = this } 

    def foo(xs: String*)(implicit e: dummy1.type) = 1
    def foo(xs: Int*)(implicit e: dummy2.type) = 2
} 

[...]

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

def foo[T <: String](s: List[T]) { println("Strings: " + s) }
def foo[T <: Int](i: List[T]) { println("Ints: " + i) }

перевод стирания изменяется с foo( List[Any] s ) дважды, на foo (List[String] s ) и foo (List[Int] i):

http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ108

Я думаю, что прочитал, что в версии 2.8 верхние границы теперь кодируются таким образом, а не всегда Any.

перегрузить на ковариантные типы, используют инвариантную привязку (есть ли такой синтаксис в Scala?...Ах, я думаю, что нет, но возьмите следующее в качестве концептуального дополнения к основному решению выше):

def foo[T : String](s: List[T]) { println("Strings: " + s) }
def foo[T : String2](s: List[T]) { println("String2s: " + s) }

тогда я предполагаю, что неявное приведение устраняется в стертой версии кода.


UPDATE: проблема в том, что JVM стирает больше информации о типе сигнатур метода, чем "необходимо". Я предоставил ссылку. Он стирает переменные типа из конструкторов типов, даже конкретную привязку из этих переменных типа. Существует концептуальное различие, потому что нет концептуального нереализованного преимущества для стирания привязки типа функции, как это известно во время компиляции и не меняется ни с одним экземпляром generic, и необходимо, чтобы вызывающие не вызывали функцию с типами, которые не соответствуют привязке типа, так как JVM может принудительно использовать привязку типа, если она стирается? Ну ссылка говорит, что привязка типа сохраняется в метаданных, которые являются компиляторами предполагается доступ. И это объясняет, почему использование границ типов не позволяет перегрузку. Это также означает, что JVM является широко открытой дырой безопасности, так как ограниченные методы типа могут быть вызваны без границ типа (yikes!), поэтому извините меня за предположение, что дизайнеры JVM не будут делать такую небезопасную вещь.

Comments

    Ничего не найдено.