Scala(或Java)中泛型函数的专业化

Posted

技术标签:

【中文标题】Scala(或Java)中泛型函数的专业化【英文标题】:Specialization of generic functions in Scala (or Java) 【发布时间】:2012-11-01 08:04:57 【问题描述】:

是否可以在 Scala 中专门化泛型函数(或类)?例如,我想编写一个将数据写入 ByteBuffer 的通用函数:

def writeData[T](buffer: ByteBuffer, data: T) = buffer.put(data)

但由于 put 方法只需要一个字节并将其放入缓冲区,因此我需要将其专门用于 Ints 和 Longs,如下所示:

def writeData[Int](buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData[Long](buffer: ByteBuffer, data: Long) = buffer.putLong(data)

它不会编译。当然,我可以分别编写 3 个不同的函数 writeByte、writeInt 和 writeLong,但假设还有另一个函数用于数组:

def writeArray[T](buffer: ByteBuffer, array: Array[T]) 
  for (elem <- array) writeData(buffer, elem)

如果没有专门的 writeData 函数,这将无法工作:我将不得不部署另一组函数 writeByteArray、writeIntArray、writeLongArray。每当我需要使用依赖于类型的写入函数时,必须以这种方式处理这种情况并不酷。我做了一些研究,一种可能的解决方法是测试参数的类型:

def writeArray[T](buffer: ByteBuffer, array: Array[T]) 
  if (array.isInstanceOf[Array[Byte]])
    for (elem <- array) writeByte(buffer, elem)
  else if (array.isInstanceOf[Array[Int]])
    for (elem <- array) writeInt(buffer, elem)
  ...

这可能有效,但效率较低,因为类型检查是在运行时完成的,与专用函数版本不同。

所以我的问题是,在 Scala 或 Java 中解决此类问题的最理想和首选的方法是什么?提前感谢您的帮助!

【问题讨论】:

等待类型类答案在 3... 2... 1.. 添加了 specialized 标签,因为事实证明这意味着 Scala 中的特定内容,而这正是您所需要的。 @RexKerr:我看不出 @specialized 在这里有什么相关性——这无助于选择需要 putIntputLong 等中的哪一个。 @TravisBrown - 不,但它会让你不会因为你这样做而不是每次都从头开始实施而感到抱歉。向缓冲区添加字节不是您通常想要调用装箱的事情。 【参考方案1】:

如果您可以同时拥有一个紧凑且高效的解决方案,那不是很好吗?事实证明,鉴于 Scala 的 @specialized 功能,您可以。首先是一个警告:该功能有些错误,如果您尝试将其用于过于复杂的事情,可能会损坏。但是对于这种情况,它几乎是完美的。

@specialized 注释为每个原始类型创建单独的类和/或方法,然后在编译器确定原始类型是什么时调用它而不是泛型版本。唯一的缺点是它完全自动完成所有这些工作——您无法填写自己的方法。这有点可惜,但您可以使用类型类来解决这个问题。

让我们看一些代码:

import java.nio.ByteBuffer
trait BufferWriter[@specialized(Byte,Int) A]
  def write(b: ByteBuffer, a: A): Unit

class ByteWriter extends BufferWriter[Byte] 
  def write(b: ByteBuffer, a: Byte)  b.put(a) 

class IntWriter extends BufferWriter[Int] 
  def write(b: ByteBuffer, a: Int)  b.putInt(a) 

object BufferWriters 
  implicit val byteWriter = new ByteWriter
  implicit val intWriter = new IntWriter

这给了我们一个通用的BufferWriter trait,但是我们用适当的实现覆盖了我们想要的每个特定的原始类型(在本例中为ByteInt)。专业化足够聪明,可以将这个显式版本与它通常用于专业化的隐藏版本联系起来。所以你有你的自定义代码,但你如何使用它?这就是隐式 vals 的用武之地(为了速度和清晰度,我已经这样做了):

import BufferWriters._
def write[@specialized(Byte,Int) A: BufferWriter](b: ByteBuffer, ar: Array[A]) 
  val writer = implicitly[BufferWriter[A]]
  var i = 0
  while (i < ar.length) 
    writer.write(b, ar(i))
    i += 1
  

A: BufferWriter 表示法意味着为了调用这个write 方法,你需要有一个隐式的BufferWriter[A] 方便。我们已经为他们提供了BufferWriters 中的 val,所以我们应该做好准备。让我们看看这是否有效。

val b = ByteBuffer.allocate(6)
write(b, Array[Byte](1,2))
write(b, Array[Int](0x03040506))
scala> b.array
res3: Array[Byte] = Array(1, 2, 3, 4, 5, 6)

如果您将这些内容放在一个文件中并开始使用javap -c -private 查找类,您会发现正在使用适当的原始方法。

(请注意,如果您不使用特化,此策略仍然有效,但它必须在循环内将值装箱以将数组复制出来。)

【讨论】:

作为 Scala 的新手,我不知道这个特性。 +1,谢谢 太棒了,正是我想要的!不过,不那么强调@specialized 功能会更好。因为虽然它使代码更高效,因此非常适合实际使用,但它与问题没有直接关系,所以我担心它可能会让寻求答案的人有些困惑。类型类模式是这里的重点。但除此之外,答案是完美的。谢谢,雷克斯! @KJ - 如果您完全使用ByteBuffer 而不是序列化为字符串,您可能关心性能。所以我不相信对specialized 的关注是错误的。类型类只是语法糖;你总是可以在 Scala 或 Java 中做同样的事情,但必须手动传递 BufferWriterspecialized 也是糖,但它是很多的糖。 很好的答案,解决了我的问题。不过有个问题——为什么不把专门的 write 方法放在 BufferWriters 对象中呢? @DominicBou-Samra - 我认为你做得对 - 这很明显。但是您发布的测试试图将三个整数放入 6 个字节,这是行不通的。我把write 方法去掉的唯一原因是为了证明你可以(因为你以后可能还有更多的方法要定义),所以我可以分解代码和解释更多。【参考方案2】:

使用类型类模式。与 instanceOf 检查(或模式匹配)相比,它具有类型安全的优势。

import java.nio.ByteBuffer

trait BufferWriter[A] 
  def write(buffer: ByteBuffer, a: A)


class BuffPimp(buffer: ByteBuffer) 
  def writeData[A: BufferWriter](data: A) =  
    implicitly[BufferWriter[A]].write(buffer, data)
  


object BuffPimp 
  implicit def intWriter = new BufferWriter[Int] 
    def write(buffer: ByteBuffer, a: Int) = buffer.putInt(a)
  
  implicit def doubleWriter = new BufferWriter[Double] 
    def write(buffer: ByteBuffer, a: Double) = buffer.putDouble(a)
  
  implicit def longWriter = new BufferWriter[Long] 
    def write(buffer: ByteBuffer, a: Long) = buffer.putLong(a)
  
  implicit def wrap(buffer: ByteBuffer) = new BuffPimp(buffer)


object Test 
  import BuffPimp._
  val someByteBuffer: ByteBuffer
  someByteBuffer.writeData(1)
  someByteBuffer.writeData(1.0)
  someByteBuffer.writeData(1L)

所以这段代码并不是类型类的最佳演示。我对他们还是很陌生。该视频对它们的好处以及如何使用它们进行了非常全面的概述:http://www.youtube.com/watch?v=sVMES4RZF-8

【讨论】:

您应该在示例中添加对数组的支持。就目前而言,它看起来像是一种不必要的复杂方法,可以实现良好的旧重载可以实现的效果。一旦您尝试处理复合结构,类型类模式就显示出它的优势:使用这种模式,无需定义 writeIntArray、writeLongArray 等。只需编写一个通用的 writeArray 方法,该方法采用元素类型的隐式 BufferWriter 实例 +1,有一个非常小的观察:您可以将implicit object intWriter extends BufferWriter[Int] ... 用于不通用的实例 - 在我看来,它使意图更加清晰,并且可能更有效。 +1 - 这是一个很好的答案,但还有一个额外的问题,即使使用数组,它也可以以原始速度全速工作。 这并没有让我觉得有什么大的变化(只是稍微更惯用,更有效),尤其是当他明确要求清理时。他深思熟虑的回答完好无损,只是稍微增强了一点。正如我上面所建议的那样,添加对数组的支持更像是对答案的实际更改,这就是为什么我自己没有做它并建议将更改作为评论。 没有问题!类型类是惊人的,这不是它们强大的一个很好的例子。至于那些修改代码的人 - 去吧,但似乎 *** 的限制正在阻止它?【参考方案3】:

    声明

    def writeData[Int](buffer: ByteBuffer, data: Int) 
    def writeData[Long](buffer: ByteBuffer, data: Long)
    

不要编译,因为它们是等价的,因为 Int 和 Long 是 formal 类型参数,而不是标准的 Scala 类型。要使用标准 Scala 类型定义函数,只需编写:

def writeData(buffer: ByteBuffer, data: Int) = buffer.putInt(data)
def writeData(buffer: ByteBuffer, data: Long) = buffer.putLong(data)

这样你就可以用相同的名字声明不同的函数了。

    由于它们是不同的函数,因此您不能将它们应用于静态未知类型的 List 的元素。您必须首先确定列表的类型。请注意,可能会发生 List 的类型是 AnyRef,然后您可以动态确定每个元素的类型。可以在原始代码中使用isInstanceOf 进行确定,也可以使用rolve 建议的模式匹配来完成确定。我认为这会产生相同的字节码。

    总之,你必须选择:

    具有多个函数的快速代码,如writeByteArray, writeIntArray 等。它们都可以具有相同的名称writeArray,但可以通过它们的实际参数进行静态区分。 Dominic Bou-Sa 提出的变体就是这种类型。

    具有运行时类型确定的简洁但缓慢的代码

不幸的是,你不能同时拥有快速和简洁的代码。

【讨论】:

writeData 的两个定义等价(它们只是错误的): Int 和 Long 是不同的类型,因此我们有两个不同的方法签名,scala 的重载规则处理正好。您对形式参数的解释令人困惑。 +1 any away 强调静态和动态方法之间的区别。 @Régis Jean-Gilles 你在说什么“两个定义”?我的回答中有两对定义。第一对取自 OP,是等价的,因为 Int 和 Long 有类型参数的名称,在定义中引入。我的解释让你感到困惑的是什么? 我说的是第一对(来自 OP)。当您说它们是等效的时,可以将其理解为“相同的签名”,尤其是考虑到您谈论的是“形式参数”而不是“形式 type 参数(最好更加清楚,因为有也是Int/Long 类型的形式参数“数据”here)。重读后,我认为您实际上指向正确的东西,但立即指出Int 和方括号内的Long 标识符是type 参数,也可以更改为FooBar,而无需任何语义更改。 哦,是的,当我在之前的评论中说这两个定义不相等时,我错了。如上所述,我对您提到的“形式参数”感到困惑。 说“形式参数”我的意思是“形式类型参数”,是的,这令人困惑。我更正了答案。【参考方案4】:

这个怎么样:

def writeData(buffer: ByteBuffer, data: AnyVal) 
  data match 
    case d: Byte => buffer put d
    case d: Int  => buffer putInt d
    case d: Long => buffer putLong d
    ...
  

在这里,您在writeData 方法中区分大小写,这使得所有其他方法都非常简单:

def writeArray(buffer: ByteBuffer, array: Array[AnyVal]) 
  for (elem <- array) writeData(buffer, elem)


优点:简单、简短、易懂。

缺点:如果您不处理所有AnyVal 类型,则不完全类型安全:有人可能会调用writeData(buffer, ())(第二个参数的类型为Unit),这可能会导致在运行时出错。但是您也可以将() 的处理设为无操作,从而解决问题。完整的方法如下所示:

def writeData(buffer: ByteBuffer, data: AnyVal) 
  data match 
    case d: Byte   => buffer put d
    case d: Short  => buffer putShort d
    case d: Int    => buffer putInt d
    case d: Long   => buffer putLong d
    case d: Float  => buffer putFloat d
    case d: Double => buffer putDouble d
    case d: Char   => buffer putChar d
    case true      => buffer put 1.asInstanceOf[Byte]
    case false     => buffer put 0.asInstanceOf[Byte]
    case ()        =>
  

顺便说一句,由于 Scala 严格的面向对象特性,这很容易实现。在 Java 中,原始类型不是对象,这会更加麻烦。在那里,您实际上必须为每个原始类型创建一个单独的方法,除非您想做一些丑陋的装箱和拆箱。

【讨论】:

感谢您的详尽回答,rolve!您的模式匹配版本看起来比我的 isInstanceOf 版本更干净。 :) 但它似乎比 Dominic 和 Rex Kerr 引入的类型类模式效率低,因为它仍然在运行时动态地进行类型检查。

以上是关于Scala(或Java)中泛型函数的专业化的主要内容,如果未能解决你的问题,请参考以下文章

scala中泛型,协变和逆变

Scala中泛型类型之间的比较

swift中泛型和 Any 类型

Scala入门到精通——第十六节 泛型与注解

NSManagedObjectContext 扩展中泛型函数中的奇怪 Swift 行为

Swift参数及泛型参数参考!