【问题标题】:Improper use of slices causes unintended side effects切片使用不当会导致意想不到的副作用
【发布时间】:2019-12-14 20:09:53
【问题描述】:

我有以下函数可以生成给定数组的所有子集。

这个想法很简单 - 我从一个包含空集(切片)的结果数组开始,对于输入数组 nums 中的每个元素,遍历所有先前生成的集合,将 nums 的当前元素添加到它们并将生成的新集合添加回结果数组。没什么特别有趣的。

func subsets(nums []int) [][]int {
  result := [][]int{{}}
  for _, n := range nums {
    newSets := [][]int{}
    for _, set := range result {
      newSets = append(newSets, append(set, n))
    }
    result = append(result, newSets...)
  }
  return result
}

问题是使用append(newSets, append(set, n)) 会破坏result 切片,set 是其中的成员。我用一些调试代码(见下文)稍微修改了函数,还找到了一个不会导致相同行为的解决方法(注释代码)。

我非常怀疑这是由通过引用传递而不是被复制的东西引起的(我将newSets 的元素附加到result)。问题是我找不到它。 :( 我从不在循环中更改结果

func subsets(nums []int) [][]int {

  result := [][]int{{}}
  for _, n := range nums {

    newSets := [][]int{}
    var before, after []int
    for _, set := range result {

      lastResultIdx := len(result)-1
      if lastResultIdx > 0 {
        before = make([]int, len(result[lastResultIdx]))
        copy(before, result[lastResultIdx])
      }

      //ns := []int{}
      //for _,v := range set {
      //  ns = append(ns, v)
      //}
      //ns = append(ns, n)
      //newSets = append(newSets, ns)

      newSets = append(newSets, append(set, n))

      if lastResultIdx > 0 {
        after = result[lastResultIdx]
        if before[len(before)-1]!=after[len(after)-1] {
          fmt.Println(n, "before", before, "after", after)
        }
      }
    }

    result = append(result, newSets...)
  }
  return result
}

func main() {
  subsets([]int{0, 1, 2, 3, 4})
}

【问题讨论】:

    标签: go append slice


    【解决方案1】:

    问题出在这里:

    append(newSets, append(set, n))
    

    问题不在于它是嵌套追加。问题是您假设append(set,n) 将返回一个新切片。情况并非总是如此。切片是数组的视图,当您向切片添加新元素时,如果添加未导致数组重新分配,则返回的切片与您传入的切片相同,len 字段递增。因此,当您浏览结果数组时,您正在修改已经存在的元素,同时再次添加它们,就好像它们是不同的结果一样。

    为了解决,当你得到result的元素时,创建一个新切片,将result的元素复制到它,追加新元素,然后将新切片添加到result

    【讨论】:

      【解决方案2】:

      问题很简单:append 接受一个切片参数——[]T 用于某些类型 T——当然还有要附加的元素,并返回一个 []T 结果.但是[]T,如果非零,则由两部分组成:一个 slice header,它指向某个后备数组并带有当前长度和容量,加上 后备数组 .当append 工作时,它有一个选择:

      • 就地修改后备数组,并返回一个新的切片头,以重用现有的后备数组,或者
      • 创建一个新的后备数组,将原始值复制到新的后备数组,并返回一个使用新后备数组的新切片头。

      只要append 复制后备数组,您的代码就可以工作。每当它重用后备数组时,您的代码可能会或可能不会工作,这取决于其他切片标头是否使用相同的后备数组。

      假设您的后备数组的长度为 5,并且现有切片标头之一读取“长度 1,容量 5”,后备数组的元素 0 为零。也就是说,现有的切片头h 包含[0]。现在你打电话给append(h, 1)。追加操作重新使用后备数组并将1 放入第二个元素并返回包含[0, 1] 的新切片头h1。现在你把h 再次,追加2,并制作一个包含[0, 2]的二元素切片h2。但是这会重复使用 h1 重复使用的相同后备数组,所以现在 h1 也包含 [0, 2]

      要在不大量修改算法的情况下解决问题,您需要:

      • 始终复制的append 的变体,或
      • 将一个 int 附加到 int 切片的变体,始终复制。

      后者更简单:

      func setPlusInt(set []int, n int) []int {
          return append(append([]int(nil), set...), n)
      }
      

      它可以让您替换现有代码的一行。

      (我做了另外一个小改动 here 并添加了足够的内容以在 Go Playground 中提供一个工作示例。)

      (另一种解决方案是设置您自己的每个切片标头以不提供额外容量,因此append 必须始终复制。我没有说明此方法。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-12-17
        • 2013-12-07
        • 1970-01-01
        • 2021-11-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多