Scala: массивы и стирание типа

Я хотел бы написать перегруженные функции следующим образом:

case class A[T](t: T)
def f[T](t: T) = println("normal type")
def f[T](a: A[T]) = println("A type")

И результат будет таким, каким я ожидал:

f (5)       = > нормальный тип
f (A (5))   = > Тип типа

Пока все хорошо. Но проблема в том, что одно не работает для массивов:

def f[T](t: T) = println("normal type")
def f[T](a: Array[T]) = println("Array type")

Теперь компилятор жалуется:

double definition: method f: [T] (t: Array [T]) Единица и метод f: [T] (t: T) Единица в строке 14 имеет один и тот же тип после стирания: (t: java.lang. Объект) Единица

Я думаю, что подпись второй функции после стирания типа должна быть (a: Array [Object]) Unit not (t: Object) Unit, поэтому они не должны сталкиваться друг с другом. Что мне здесь не хватает?

И если я делаю что-то не так, как было бы правильно писать f так, чтобы правый вызывался в соответствии с типом аргумента?

Ответ 1

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

public static <T> void f(T t){out.println("normal type");}
public static <T> void f(T[] a){out.println("Array type");}

С другой стороны, Scala поддерживает generics для всех типов. Хотя язык Scala не имеет примитивов, полученный байт-код использует их для таких типов, как Int, Float, Char и Boolean. Это делает разницу между кодом Java и кодом Scala. Java-код не принимает int[] как массив, потому что int не является java.lang.Object. Поэтому Java может стереть эти типы параметров метода до Object и Object[]. (Это означает Ljava/lang/Object; и [Ljava/lang/Object; на JVM.)

С другой стороны, ваш код Scala обрабатывает все массивы, включая Array[Int], Array[Float], Array[Char], Array[Boolean] и так далее. Эти массивы являются (или могут быть) массивами примитивных типов. Они не могут быть переведены на Array[Object] или Array[anything else] на уровне JVM. Существует только один супертип Array[Int] и Array[Char]: это java.lang.Object. Это более общий супертип, который вы можете пожелать.

Чтобы поддерживать эти утверждения, я написал код с менее общим методом f:

def f[T](t: T) = println("normal type")
def f[T <: AnyRef](a: Array[T]) = println("Array type")

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

def f[T](t: T) = println("normal type")
def f[T <: AnyVal](a: Array[T]) = println("Array type")

Добавление @specialized не решает проблему, поскольку генерируется общий метод:

def f[T](t: T) = println("normal type")
def f[@specialized T <: AnyVal](a: Array[T]) = println("Array type")

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

Ответ 2

Я думаю, что подпись второй функции после стирания типа должна быть (a: Array [Object]) Unit not (t: Object) Unit, поэтому они не должны сталкиваться друг с другом. Что мне здесь не хватает?

Erasure точно означает, что вы теряете информацию о параметрах типа универсального класса и получаете только сырой тип. Таким образом, подпись def f[T](a: Array[T]) не может быть def f[T](a: Array[Object]), потому что у вас все еще есть параметр типа (Object). Как правило, вам просто нужно отбросить параметры типа, чтобы получить тип стирания, который дал бы нам def f[T](a: Array). Это будет работать для всех других общих классов, но массивы являются специальными для JVM, и, в частности, их стирание просто Object (ther не является array raw type). И, таким образом, подпись f после стирания действительно def f[T](a: Object). [Обновлено, я был не прав] Собственно, после проверки спецификации java, похоже, что я был совершенно не прав. Спектр говорит

Стирание типа массива T [] равно | T | []

Где |T| - стирание T. Итак, действительно, массивы обрабатываются специально, но особенность заключается в том, что, хотя параметры типа действительно удалены, тип помечен как массив T вместо T. Это означает, что Array[Int] после стирания еще Array[Int]. Но Array[T] отличается: T является параметром типа для общего метода f. Чтобы иметь возможность обрабатывать любой массив в общем случае, scala не имеет другого выбора, кроме как превратить Array[T] в Object (и я полагаю, что Java делает то же самое кстати). Это потому, что, как я сказал выше, нет такого типа необработанного типа array, поэтому он должен быть Object.

Я постараюсь сделать это по-другому. Обычно при компиляции общего метода с параметром типа MyGenericClass[T] сам факт, что стираемый тип MyGenericClass позволяет (на уровне JVM) передавать любое инстанцирование MyGenericClass, например MyGenericClass[Int] и MyGenericClass[Float], потому что они на самом деле все одинаковы во время выполнения. Однако это не относится к массивам: Array[Int] - это полностью не связанный тип с Array[Float], и они не будут стираться с общим типом array. Их наименее распространенный тип Object, и поэтому это то, что манипулируется под капотом, когда массивы обрабатываются в общем случае (каждый раз, когда компилятор не может знать статически тип элементов).

ОБНОВЛЕНИЕ 2: v6ak answer добавил полезный бит информации: Java не поддерживает примитивные типы в дженериках. Таким образом, в Array[T], T обязательно (в Java, но не в Scala), подкласс класса Object и, следовательно, его стирание до Array[Object] полностью имеет смысл, в отличие от scala, где T может быть, например, примитивным типом Int, который определенно не является подклассом Object (aka AnyRef). Чтобы быть в той же ситуации, что и Java, мы можем ограничить T верхней границей и, конечно же, теперь она компилируется отлично:

def f[T](t: T) = println("normal type")
def f[T<:AnyRef](a: Array[T]) = println("Array type") // no conflict anymore

Что касается того, как вы можете обойти эту проблему, общим решением является добавление фиктивного параметра. Поскольку вы, конечно же, не хотите явно передавать фиктивное значение для каждого вызова, вы можете либо дать ему фиктивное значение по умолчанию, либо использовать неявный параметр, который всегда будет неявным образом найден компилятором (например, dummyImplicit найден в Predef): ​​

def f[T](a: Array[T], dummy: Int = 0)
// or:
def f[T](a: Array[T])(implicit dummy: DummyImplicit)
// or:
def f[T:ClassManifest](a: Array[T])

Ответ 3

[Scala 2.9] Решение заключается в использовании неявных аргументов, которые естественным образом изменяют сигнатуру методов, так что они не конфликтуют.

case class A()

def f[T](t: T) = println("normal type")
def f[T : Manifest](a: Array[T]) = println("Array type")

f(A())        // normal type
f(Array(A())) // Array type

T : Manifest является синтаксическим сахаром для второго списка аргументов (implicit mf: Manifest[T]).

К сожалению, я не знаю, почему Array[T] будет удалено только Object вместо Array[Object].

Ответ 4

Чтобы получить стирание типа в scala, вы можете добавить неявный параметр, который даст вам Manifest (scala 2.9. *) или TypeTag (scala 2.10), а затем вы можете получить всю необходимую информацию о типах, например:

def f [T] (t: T) (неявный манифест: манифест [T])

Вы можете проверить, является ли m экземпляром массива и т.д.