【问题标题】:Initializing a 2D (multi-dimensional) array in Scala在 Scala 中初始化二维(多维)数组
【发布时间】:2012-12-01 12:05:05
【问题描述】:

在 Java 中很容易初始化一个二维数组(或者,事实上,任何多维数组),通过这样的方式:

int[][] x = new int[][] {
        { 3, 5, 7, },
        { 0, 4, 9, },
        { 1, 8, 6, },
};

易于阅读,类似于二维矩阵等。

但我如何在 Scala 中做到这一点?

我能想到的最好的外观,嗯,简洁得多:

val x = Array(
    Array(3, 5, 7),
    Array(0, 4, 9),
    Array(1, 8, 6)
)

我在这里看到的问题:

  • 它一遍又一遍地重复“数组”(好像除了Array 之外还有其他东西)
  • 它要求在每个 Array 调用中省略尾随 ,
  • 如果我搞砸并在数组中间插入除Array() 之外的其他内容,编译器会正常运行,但x 的类型会默默变为Array[Any] 而不是Array[Array[Int]]

    val x = Array(
        Array(3, 5, 7),
        Array(0, 4), 9, // <= OK with compiler, silently ruins x
        Array(1, 8, 6)
    )
    

    有一个防范措施,直接指定类型,但看起来比在 Java 中更加矫枉过正:

    val x: Array[Array[Int]] = Array(
        Array(3, 5, 7),
        Array(0, 4), 9, // <= this one would trigger a compiler error
        Array(1, 8, 6)
    )
    

    最后一个示例需要 Array 甚至比我在 Java 中必须说的 int[][] 多 3 倍。

有什么明确的方法可以解决这个问题吗?

【问题讨论】:

  • 请注意,Scala 中的List 是链表,而不是类似数组的数据结构。
  • 这是你想要做的:stackoverflow.com/questions/2381908/…
  • @jcern:不,它只是指定了我上面已经引用的内容。我想到了一个更优雅的方法。
  • @Jesper:你说得对,谢谢,我现在会修复有问题的示例
  • @Eduardo:这些逗号打算在那里 - 请更仔细地阅读我的问题。事实上,我喜欢它们,并且我希望成为易于处理的容器初始化程序。

标签: scala multidimensional-array initialization


【解决方案1】:

为了清楚起见,我个人会把它吸起来并输入(或剪切和粘贴)“数组”几次。当然,为了安全起见,请包含类型注释。但如果你真的用完了电子墨水,一个快速简单的破解方法就是为Array 提供一个别名,例如:

val > = Array

val x: Array[Array[Int]] = >(
  >(3, 5, 7),
  >(0, 4, 9),
  >(1, 8, 6)
)

如果您想缩短注解,您还可以为Array 提供类型别名:

type >[T] = Array[T]

val x: >[>[Int]] = ...

【讨论】:

  • 我打算投反对票,理由是“我会接受它”......因为我不想满足于香草语法。但是你的类型别名已经弥补了它,所以你收到了一个赞成票;)
  • 您的语法至少在 repl 中不起作用::8: error: not found: value > val x: >[>[Int]] = >(
  • 语法在我的 REPL 上运行良好,你定义了 val > = Array 吗?
  • 我又试了一次,现在可以了。很久以前不记得我申请的方式是否有任何差异。我相信已经剪切和粘贴了。
  • 哦,我明白了。此语法在 REPL 中有效,但在普通 Scala 类中无效。是否有可能在真正的程序中工作?错误是“未找到:值>”
【解决方案2】:

我建议使用 Scala 2.10 和宏:

object MatrixMacro {

  import language.experimental.macros

  import scala.reflect.macros.Context
  import scala.util.Try

  implicit class MatrixContext(sc: StringContext) {
    def matrix(): Array[Array[Int]] = macro matrixImpl
  }

  def matrixImpl(c: Context)(): c.Expr[Array[Array[Int]]] = {
    import c.universe.{ Try => _, _ }

    val matrix = Try {
      c.prefix.tree match {
        case Apply(_, List(Apply(_, List(Literal(Constant(raw: String)))))) =>

          def toArrayAST(c: List[TermTree]) =
            Apply(Select(Select(Ident("scala"), newTermName("Array")), newTermName("apply")), c)

          val matrix = raw split "\n" map (_.trim) filter (_.nonEmpty) map {
            _ split "," map (_.trim.toInt)
          }
          if (matrix.map(_.length).distinct.size != 1)
            c.abort(c.enclosingPosition, "rows of matrix do not have the same length")

          val matrixAST = matrix map (_ map (i => Literal(Constant(i)))) map (i => toArrayAST(i.toList))

          toArrayAST(matrixAST.toList)
      }
    }

    c.Expr(matrix getOrElse c.abort(c.enclosingPosition, "not a matrix of Int"))
  }

}

用于:

scala> import MatrixMacro._
import MatrixMacro._

scala> matrix"1"
res86: Array[Array[Int]] = Array(Array(1))

scala> matrix"1,2,3"
res87: Array[Array[Int]] = Array(Array(1, 2, 3))

scala> matrix"""
     |   1, 2, 3
     |   4, 5, 6
     |   7, 8, 9
     | """
res88: Array[Array[Int]] = Array(Array(1, 2, 3), Array(4, 5, 6), Array(7, 8, 9))

scala> matrix"""
     |   1, 2
     |   1
     | """
<console>:57: error: rows of matrix do not have the same length
matrix"""
^

scala> matrix"a"
<console>:57: error: not a matrix of Int
              matrix"a"
              ^

我不认为你会缩短它。 ;)

【讨论】:

  • 一般来说,从输入字符串解析矩阵总是一种可能的解决方案,即使没有宏。我还不熟悉 2.10 和宏。我是否理解正确,通过使用宏,对输入字符串的检查发生在编译时而不是运行时?
  • @bluenote10:是的,没错。使用普通字符串处理它意味着您必须等到程序执行才能查看您输入的矩阵是否正确。
  • ... 这使它成为一个极好的解决方案:) 也是一个非常好的宏示例。
  • 如果您需要使用变量而不是硬编码数字怎么办?代码不会一下子膨胀吗?
  • @gsimard:字符串插值允许添加变量。如果您的目标是对齐所有行,您可以添加任意数量的空格。
【解决方案3】:

如果仅使用List 中的List(它本身不能保证每个子列表的大小相同)对您来说不是问题,并且您只关心简单的语法和避免创建时的错误-时间,scala 有很多方法可以创建好的语法结构。

一个这样的可能性是一个简单的助手:

object Matrix {
  def apply[X]( elements: Tuple3[X, X, X]* ): List[List[X]] = {
    elements.toList.map(_.productIterator.toList.asInstanceOf[List[X]] )
  }
  // Here you might add other overloads for Tuple4, Tuple5 etc if you need "matrixes" of those sizes
}

val x = Matrix(
  (3, 5, 7),
  (0, 4, 9),
  (1, 8, 6)
)

关于您的疑虑:

它一遍又一遍地重复“列表”(就像除了列表之外可能还有其他东西)

这里不是这样。

它需要在每个 List 调用中省略结尾

不幸的是,这里仍然如此,鉴于 scala 的语法规则,您无能为力。

如果我搞砸并在数组中间插入除 List() 之外的其他内容,编译器会正常运行,但 x 的类型会默默变为 List[Any] 而不是 List[List[Int]]:

val x = List(
  List(3, 5, 7),
  List(0, 4), 9, // <= OK with compiler, silently ruins x
  List(1, 8, 6)
)

等效代码现在无法编译:

scala> val x = Matrix(
     |   (3, 5, 7),
     |   (0, 4), 9,
     |   (1, 8, 6)
     | )
<console>:10: error: type mismatch;
 found   : (Int, Int)
 required: (?, ?, ?)
         (0, 4), 9,

最后,如果您想显式指定元素的类型(假设您想防止无意中混淆Ints 和Doubles),您只需指定Matrix[Int] 而不是丑List[List[Int]]

val x = Matrix[Int](
  (3, 5, 7),
  (0, 4, 9),
  (1, 8, 6)
)

编辑:我看到您在问题中将List 替换为Array。要使用数组,您只需在上面的代码中将List 替换为ArraytoListtoArray

【讨论】:

  • 可以使用 shapeless 概括您的 TupleN 解决方案。
  • 我实际上是要提到无形的:)。我之所以没有这样做,是因为我个人对它的经验为零(目前),并且只有 95% 的人确信它允许对产品数量进行抽象。 +1 确认。
【解决方案4】:

由于我也对这个尾随逗号问题感到厌恶(即,我不能简单地将最后一行与任何其他行交换)我有时使用流利的 API 或构造函数语法技巧来获得我喜欢的语法。使用构造函数语法的示例是:

trait Matrix {
  // ... and the beast
  private val buffer = ArrayBuffer[Array[Int]]()
  def >(vals: Int*) = buffer += vals.toArray
  def build: Array[Array[Int]] = buffer.toArray
}

允许:

// beauty ... 
val m = new Matrix {
  >(1, 2, 3)
  >(4, 5, 6)
  >(7, 8, 9)
} build

不幸的是,这依赖于可变数据,尽管它只是在构建过程中临时使用。如果我想要构造语法的最大美感,我会更喜欢这个解决方案。

如果build 太长/太冗长,您可能想用一个空的应用函数替换它。

【讨论】:

    【解决方案5】:

    我不知道这是否是简单的方法,但我在下面包含了一些用于将嵌套元组转换为“二维”数组的代码。

    首先,您需要一些样板来获取元组的大小以及将元组转换为[Array[Array[Double]]。我使用的一系列步骤是:

    1. 计算元组中的行数和列数
    2. 将嵌套元组变成单行数组
    3. 根据原始元组的大小重新调整数组的形状。

    代码如下:

    object Matrix {
    
        /**
         * Returns the size of a series of nested tuples. 
         */
        def productSize(t: Product): (Int, Int) = {
            val a = t.productArity
            val one = t.productElement(0)
            if (one.isInstanceOf[Product]) {
                val b = one.asInstanceOf[Product].productArity
                (a,  b)
            }
            else {
                (1, a)
            }
        }
    
        /**
         * Flattens out a nested tuple and returns the contents as an iterator. 
         */
        def flattenProduct(t: Product): Iterator[Any] = t.productIterator.flatMap {
            case p: Product => flattenProduct(p)
            case x => Iterator(x)
        }
    
        /**
         * Convert a nested tuple to a flattened row-oriented array.
         * Usage is:
         * {{{
         *  val t = ((1, 2, 3), (4, 5, 6))
         *  val a = Matrix.toArray(t)
         *  // a: Array[Double] = Array(1, 2, 3, 4, 5, 6)
         * }}}
         *
         * @param t The tuple to convert to an array
         */
        def toArray(t: Product): Array[Double] = flattenProduct(t).map(v =>
            v match {
                case c: Char => c.toDouble
                case b: Byte => b.toDouble
                case sh: Short => sh.toDouble
                case i: Int => i.toDouble
                case l: Long => l.toDouble
                case f: Float => f.toDouble
                case d: Double => d
                case s: String => s.toDouble
                case _ => Double.NaN
            }
    
        ).toArray[Double]
    
        def rowArrayTo2DArray[@specialized(Int, Long, Float, Double) A: Numeric](m: Int, n: Int,
                rowArray: Array[A]) = {
            require(rowArray.size == m * n)
            val numeric = implicitly[Numeric[A]]
            val newArray = Array.ofDim[Double](m, n)
            for (i <- 0 until m; j <- 0 until n) {
                val idx = i * n + j
                newArray(i)(j) = numeric.toDouble(rowArray(idx))
            }
            newArray
        }
    
        /**
         * Factory method for turning tuples into 2D arrays
         */
        def apply(data: Product): Array[Array[Double]] = {
            def size = productSize(data)
            def array = toArray(data)
            rowArrayTo2DArray(size._1, size._2, array)
        }
    
    }
    

    现在要使用它,您只需执行以下操作:

    val a  = Matrix((1, 2, 3))
    // a: Array[Array[Double]] = Array(Array(1.0, 2.0, 3.0))
    
    val b = Matrix(((1, 2, 3), (4, 5, 6), (7, 8, 9)))
    // b: Array[Array[Double]] = Array(Array(1.0, 2.0, 3.0), 
    //                                 Array(4.0, 5.0, 6.0), 
    //                                 Array(7.0, 8.0, 9.0))
    
    val c = Matrix((1L, 2F, "3"))    // Correctly handles mixed types
    // c: Array[Array[Double]] = Array(Array(1.0, 2.0, 3.0))
    
    val d = Matrix((1L, 2F, new java.util.Date())) // Non-numeric types convert to NaN
    // d: Array[Array[Double]] = Array(Array(1.0, 2.0, NaN))
    

    或者,如果您可以直接使用所需数组的大小和一维值数组调用 rowArrayTo2DArray:

    val e = Matrix.rowArrayTo2DArray(1, 3, Array(1, 2, 3))
    // e: Array[Array[Double]] = Array(Array(1.0, 2.0, 3.0))
    
    val f = Matrix.rowArrayTo2DArray(3, 1, Array(1, 2, 3))
    // f: Array[Array[Double]] = Array(Array(1.0), Array(2.0), Array(3.0))
    
    val g = Matrix.rowArrayTo2DArray(3, 3, Array(1, 2, 3, 4, 5, 6, 7, 8, 9))
    // g: Array[Array[Double]] = Array(Array(1.0, 2.0, 3.0), 
    //                                 Array(4.0, 5.0, 6.0), 
    //                                 Array(7.0, 8.0, 9.0))
    

    【讨论】:

    • 我可以看到使用这种方法的一个问题是您仍然可以传递任何类型的元组(例如 Tuple2[SomeType, AnotherType]),这显然不好并且可以正常编译(仅在运行时吹)
    • @Régis Jean-Gilles:'这是真的......产品不会公开其内容的类型。但是,可以直接使用rowArrayTo2DArray 方法,这是类型安全的。我添加了一个示例来说明这一点。
    • @Régis Jean-Gilles:另外,为了防止运行时爆破,可以将行 case _ =&gt; throw new UnsupportedOperationException(... 更改为 case _ =&gt; Double.NaN,这将防止爆破并提供一种“正确”的方式来处理非数字值.我将更改代码以反映这一点。
    【解决方案6】:

    浏览答案,我没有找到对我来说似乎最明显和最简单的方法。您可以使用元组来代替Array
    看起来像这样:

    scala> val x = {(
         | (3,5,7),
         | (0,4,9),
         | (1,8,6)
         | )}
    
    x: ((Int, Int, Int), (Int, Int, Int), (Int, Int, Int)) = ((3,5,7),(0,4,9),(1,8,6))
    

    看起来干净优雅?
    我是这么认为的:)

    【讨论】:

    • 这种方法存在一些问题:(1) 使用元组对行/列进行迭代不太优雅。 (2) 在矩阵的固定位置更新一个条目很麻烦。 (3) 你绝对不想输入整个签名。
    • 同意。好吧,这完全取决于用例……如果您只需要简单地存储值,而无需太多更新,这可能会很好。此外,如果您需要Matrix,您可以编写一个隐式函数,采用这个 3X3 元组结构,并将其转换为Matrix -> 这样您就可以享受两个世界。优雅的初始化,易于使用的类型。
    • 我认为问题中的 3x3 数组只是一个用例。我假设 GreyCat 正在寻找可以应用于任意大小的 2D 数组的通用解决方案。
    猜你喜欢
    • 2012-11-29
    • 2012-04-14
    • 2012-05-20
    • 1970-01-01
    • 2015-08-01
    • 1970-01-01
    相关资源
    最近更新 更多