使用fill()初始化数组数组出现意外结果的原因及优化方案
Julia中fill()初始化数组数组的行为疑问及解决方案
问题场景
我使用fill()初始化两个数组数组:
A = fill(Vector{Float32}(zeros(Float32, 4)), 3); B = fill(Vector{Float32}(ones(Float32, 4)), 3);
执行原地加法A[1] .+= B[1]后,发现A的所有元素都被修改:
3-element Vector{MVector{4, Float32}}: [1.0, 1.0, 1.0, 1.0] [1.0, 1.0, 1.0, 1.0] [1.0, 1.0, 1.0, 1.0]
改用循环初始化后:
A = Array{Vector{Float32},1}(undef, 3) for x in 1:3 A[x] = zeros(Float32,4) end B = Array{Vector{Float32},1}(undef, 3) for x in 1:3 B[x] = ones(Float32,4) end
执行相同操作后结果符合预期:
3-element Vector{Vector{Float32}}: [1.0, 1.0, 1.0, 1.0] [0.0, 0.0, 0.0, 0.0] [0.0, 0.0, 0.0, 0.0]
问题解答
1. 为何两种初始化方式行为不同?
fill(x, n)的核心是创建一个包含n个同一对象引用的数组。
当你用fill(Vector{Float32}(zeros(Float32, 4)), 3)时,仅生成1个长度为4的Float32向量对象,A数组的3个元素全部指向这个对象。.+=是原地修改操作,直接改变向量本身的内容,而非修改数组元素的引用——所以所有指向该向量的数组元素都会同步看到变化,看起来就像A的所有元素都被修改了。
而循环初始化时,每次迭代调用zeros(Float32,4)都会生成全新的独立向量对象,并赋值给A的对应位置。此时A的3个元素分别指向3个互不关联的向量,修改其中一个自然不会影响其他元素。
2. 用单行方式实现循环初始化的效果?
可以,以下是几种简洁的单行写法:
列表推导式(推荐)
A = [zeros(Float32, 4) for _ in 1:3] B = [ones(Float32, 4) for _ in 1:3]
列表推导式会为每个循环迭代创建新的向量对象,和手动循环的效果完全一致,语法简洁直观。
map函数
A = map(_ -> zeros(Float32, 4), 1:3) B = map(_ -> ones(Float32, 4), 1:3)
map会对1:3中的每个元素执行匿名函数,每次调用zeros/ones都会生成新的向量对象。
注意:fill本身无法实现这个效果,因为它的参数是预先创建好的单个对象,无法自动为每个数组元素生成新对象。如果一定要用类似fill的形式,可结合Ref和广播,但这种写法冗余,不推荐:
A = [zeros(Float32,4) for _ in fill(Ref(()), 3)]
内容的提问来源于stack exchange,提问作者thehardyreader
相关产品推荐
相关产品推荐

