我是Scala的新手,这是一个非常愚蠢的问题,但我被困在这里。 我有多个Int的2D阵列,我想合并成一个大的2D阵列。
到目前为止的代码:
object Generator
{
// Use a HashMap for greate flexibility in the future. We could more easily add additional checks for biome placement like if(x, y-1) ...
val biomes:mutable.HashMap[Vector2, Biome] = new mutable.HashMap[Vector2, Biome]
def create(width:Int, height:Int)
{
for(x <- 0 until width; y <- 0 until height)
{
if(y < height / 3 * 2)
biomes.put(new Vector2(x, y), new GrasslandBiome(128, 64))
else
biomes.put(new Vector2(x, y), new SkyBiome(128, 64))
}
terrain
}
def terrain
{
var tileMap:ArrayBuffer[Array[Array[Int]]] = new ArrayBuffer[Array[Array[Int]]]
for((position, biome) <- biomes)
{
tileMap += biome.tileMap
}
Predef.println(tileMap.toString) // TODO remove
}
}
Biome只不过是2D数组的生成器:var data:Array [Array [Int]](是的,这将是一个相当简单的游戏,可以更好地了解Scala! :))
我的问题是ArrayBuffer似乎是错误的选择,因为我没有得到预期的结果,这是一个大的2D阵列。事实上我得到了:
ArrayBuffer([[I@1ed8f33e, [[I@78c86c9a, [[I@46c4ace3, [[I@4de0dedb)
然后是NullPointerException
的{{1}} :(
我非常感谢任何帮助解决我在推理(和编程)中的错误,并希望祝你有个美好的一天! :)
编辑:当然相应的Biome数据不是未初始化/ null!如果我手动打印,一切都按预期进行。
edit2:根据要求:
println
和一个例子:
abstract class Biome(width:Int, height:Int)
{
var tileMap: Array[Array[Int]]
def create();
def printTileMap()
{
printTileMap(tileMap, width, height)
}
def printTileMap(map: Array[Array[Int]], mapWidth:Int, mapHeight:Int)
{
var x = mapHeight - 1
while(x >= 0)
{
for (y <- 0 until mapWidth) Predef.print(map(y)(x) + " ")
println()
x -= 1
}
}
}
另外,一个丑陋的[b]解决方案[/ b]将迭代整个HashMap,总结每个生物群系的宽度和高度,用该信息初始化tileMap并简单地将所有内容复制到正确的位置。但这似乎是一个令人讨厌的解决方案。
答案 0 :(得分:1)
首先,我找到了各种数组类型之间差异的解释: scala - array vs arrayseq
现在关于如何从二维数组中获取一维数组的问题:但是让我们假设,而不是Array[Array[T]]
我们有List[List[T]]
(因为我喜欢列表)更多;但你应该没有问题应用于数组)。
foldLeft[A](initial: A)(function: (A, B) => A)
做什么?它通过在每个元素上应用函数A
来迭代集合并聚合类型(A, B) => A
的值。这里,B
是列表的通用类型。在我们的示例中,我们通过将中间值B
连接起来(A
)来将整数列表(++
)的列表折叠为整数列表(a
)。列表中的下一个元素。
编辑2:
将此应用于Biome示例时,我们可以创建从位置到生物群系的地图,如下所示:
def create(width:Int, height:Int): Map[Vector2, Biome] =
{
// we can define functions within functions
def isGrasslandBiome(x: Int, y: Int): Boolean =
y < height / 3 * 2
// A for-comprehension returns a sequence of all computed elements
val positionWithBiomes: Seq[(Vector2, Biome)] =
for{
x <- 0 until width
y <- 0 until height
} yield
{
val biome =
if (isGrasslandBiome(x, y))
GrasslandBiome(DefaultWidth, DefaultHeight)
else
SkyBiome(DefaultWidth, DefaultHeight)
((x, y), biome)
}
// We fold each element of the sequence into a map
val biomes: Map[Vector2, Biome] =
positionWithBiomes.foldLeft[Map[Vector2, Biome]](Map.empty){
case (map, ((x, y), biom)) => map + ((x, y) -> biom)
}
biomes
}
我们迭代((Int, Int), Biome)
s的序列并将它们聚合在Map
中。我更喜欢不可变集合(大多数标准Scala集合)到可变集合。在调试代码以获得可能的性能优势时,可变性可能会给您带来困难。此外,不变性符合函数式,因为函数(在数学意义上)总是纯粹的,即不会改变状态。
尽管如此,为了示例,并且因为tileMap
的内部表示可能实际上对性能至关重要,让我们定义一个类TileMap
,它本质上是一个两个包装器 - 维数组:(注:type TileMapInternal = mutable.ArraySeq[mutable.ArraySeq[Int]]
)
/** A tile map is our internal, mutable representation of a Biome
*
* Note that this makes Biome mutable
*/
/*mutable*/ class TileMap private(private val value: TileMapInternal, private val width: Int, private val height: Int) {
def print()
{
value foreach { row =>
row foreach { tile =>
Predef.print(s"$tile ")
}
println()
}
}
def setAllTiles(tileValue: Int) {
for{
x <- 0 until width
y <- 0 until height
}{
setTile(tileValue)(x, y)
}
}
def setTile(tileValue: Int)(x: Int, y: Int) {
value(y)(x) = tileValue
}
def addRows(other: TileMap): TileMap =
new TileMap(value ++ other.value,
width, // we add rows -> width stays the same
height + other.height
)
def addColumns(other: TileMap): TileMap =
new TileMap(
value zip other.value map {case (first, second) => first ++ second},
width + other.width,
height // we add columns -> height stays the same
)
}
这个类有一个私有构造函数,因为我们不希望内部表示泄露出来。在Scala中,您可以定义一个可以访问所有私有成员的协同对象。基本上,伴随对象包含您在Java中声明静态的所有方法。
// companion object
object TileMap {
def empty(width: Int, height: Int): TileMap = {
val rows = new mutable.ArraySeq[mutable.ArraySeq[Int]](height)
for (row <- 0 until height) {
rows(row) = new mutable.ArraySeq[Int](width)
for (cell <- 0 until width) {
rows(row)(cell) = 0
}
}
new TileMap(rows, width, height)
}
}
伴侣对象将为我们初始化一个空的TileMap。这很可能写得更优雅,但它完成了它应该做的事情:它创建一个宽度为x的数组,并将所有元素设置为零。您的Nullpointer异常可能是因为您没有正确初始化元素。当您尝试手动打印时,由于自动装箱,他们可能只打印0
而不是null
。
修改强>
如何在单个2d数组中累积地图:
方法addRows
和addColumns
通过组合内部或外部数组来创建新的TileMap
(即您的二维数组)。组合外部数组很简单:只需使用在所有scala集合上定义的方法++
添加它们。要组合内部数组,我们可以先将zip
两个数组放在一起,这样就可以得到一个数组元组的数组(Array[(Array[Int], Array[Int])]
)。然后,我们可以通过在每个元组上应用map
将此数组++
转换为新的常规二维数组。
我们现在可以使用这两个函数将二维数组的tile map(基本上就是我们使用val biomes: Map[Vector2, Biome]
存储的内容)减少到一个tile map中。
我应该提一下,按照设计,这看起来非常脆弱:一旦你的瓷砖地图尺寸不同,我想象要破坏的东西。另外,搞乱行/列组合的顺序几乎不可能调试,如果你的生物群系地图还没有完成,计算地形可能会崩溃。然而,这就是我如何通过当前设计解决问题:
def terrain(biomes: Map[Vector2, Biome]): TileMap = {
val (rows, _) = biomes.keys.unzip
rows.toList.sortBy{id => id} map {
row => terrainForRow(row, biomes)
} reduceLeft {
(accumulator: TileMap, nexTileMap: TileMap) => accumulator addColumns nexTileMap
}
}
def terrainForRow(column: Int, biomes: Map[Vector2, Biome]): TileMap =
biomes.filter{
case ((x, y), _) => x == column
}.toList.sortBy{
case ((_, y), _) => y
}.map{
case ((_, _), biome) => biome.tileMap
}.reduceLeft[TileMap]{
(accumulator, nextTileMap) => accumulator addRows nextTileMap
}
这当然需要一些解释:terrainForRow
将占用一行(即地图中的所有值与x
具有相同的值)并以正确的顺序连接它们。订单由y
确定,因为我们希望保留列的顺序。
首先,我们filter
所有具有x
其次,我们通过将其转换为列表来为集合添加排序,并根据y
的值
第三,我们丢弃位置(x
和y
的值),并提取平铺贴图。如果我们想要使用reduceLeft
,则需要这样做,因为它的类型签名。
最后,我们累积了瓷砖地图,调用我们之前定义的addRows
。请注意,reduceLeft
仅针对大小为2且更大的
在方法terrain
中,我们为每一行调用terrainForRow
(也就是说,在我们按行对图块进行排序之后)。每个结果都是TileMap
,因此我们可以对结果序列调用reduceLeft
,并使用我们定义的函数TileMap
将它们缩减为单个addColumns
。
我已将更新的代码放在 http://pastie.org/9598459
我想在scala中添加一些你可以做的事情,因为你想学习这门语言:
结束和部分应用功能
查看方法setTile
的签名:它有两个参数列表。这称为currying,允许我们部分应用函数。例如,我们可以创建一个这样的函数:
// let us partially apply the setTile function
val setGrassTileAt: (Int, Int) => Unit =
biomesTileMap.setTile(GrassTileValue)
// now tileBuilder will set the value of GrassTile at each position we provide
setGrassTileAt(0, 0)
setGrassTileAt(3, 1)
setGrassTileAt(0, 1)
函数setGrassTile
(存储在变量中,因为在scala中,函数是一等公民),获取两个整数值(坐标)并将该坐标处的生物群系的值设置为{{ 1}}。
统一访问原则和案例类
GrassTileValue
在Scala中,客户端在访问对象成员时不应区分变量和方法。例如,abstract class Biome
{
// Scala supports the uniform access princple
// A client is indifferent whether this is def, val or var
// A subclass can implement width and height as val
def width: Int
def height: Int
def tileMap: TileMap
}
// Case classes automatically define the methods equals, hashCode and toString
// They also have an implicit companion object defining apply and unapply
// Note that equals is now defined on width and height, which is probably not what
// we want in this case.
case class SkyBiome(width: Int, height: Int) extends Biome {
val tileMap: TileMap = TileMap.empty(width, height)
}
case class GrasslandBiome(width: Int, height: Int) extends Biome {
val tileMap: TileMap = TileMap.empty(width, height)
}
是超类型中的width
还是子类型中的def
无关紧要。
案例类是覆盖val
,equals
和hashCode
的类。此外,它们还提供了一个提取器toString
(可以在unapply
时使用)和match
方法(当您将对象视为函数时应用它)。案例类对价值对象最有利。
我希望我能帮助你一点。 修改:我更新的答案前的代码:http://pastie.org/9586136