This initializer was the source of an extremely difficult to track down bug in my project.
The gotcha is of course that if you initialize a class instance in the repeating initializer, every element in the array is just a reference to the same object.
Has anyone else fallen victim to this? Maybe we could mention this in the docs if it's a common issue?
/// Creates a new collection containing the specified number of a single,
/// repeated value.
///
/// Here's an example of creating an array initialized with five strings
/// containing the letter Z.
///
/// let fiveZs = Array(repeating: "Z", count: 5)
/// print(fiveZs)
/// // Prints "["Z", "Z", "Z", "Z", "Z"]"
///
/// - Parameters:
/// - repeatedValue: The element to repeat.
/// - count: The number of times to repeat the value passed in the
/// repeating parameter. count must be zero or greater. @inlinable public init(repeating repeatedValue: Element, count: Int)
Read the documentation carefully. It is clearly stating "a single repeated value". The type of repeatedValue is the element type of the array, not a function (in this case initializer or constructor) that produces values of that type upon each invocation.
When you pass a function that produces values of element type as repeatedValue, the code will evaluate the function once, produces a value (in your case, constructs a single object) and passes it as repeatedValue. This is the normal behavior for all function arguments (in most common programming languages).
On the other hand, I have seen many people being surprised by this. If we could figure out what leads people to expect repeatedValue to accept constructor function instead of a value, we might be able to improve the documentation.
Maybe I should elaborate a bit on how this "got me":
I had a type that I was using with the repeating init, initially this type was a struct, so everything was great, no bugs. Eventually, I decided that this type actually needed to be a class, so I changed it, not remembering that I used it with the repeating init elsewhere. This was my tragic mistake, because as it turns out, for this particular example, this only manifested as a bug in a rather rare scenario in my project. There were no signs of a bug until months after I made the change from struct to class.
So, it's not so much that I didn't understand how the repeating initializer works, it's just that I accidentally ended up using it with a reference type!
Based on this experience, I feel that the repeating initializer is effectively an easy-to-fall-into trap.
Perhaps documentation alone is not a strong enough solution, but I'm not sure that I have a better idea.
Changing a type from struct into a class (and vice versa) is a pretty big refactoring. What would help in this instance is probably a smart refactoring tool with advanced static code analysis capabilities to be able to detect most such aliasing issues and at least add FIXME comments for you. You are very lucky if this is the only bug crept into your code as a result of this change.
Back to Array(repeatedValue:count:), maybe a different label instead of repeatedValue would help, but it is too late for such a change. Also, we can specifically call out this particular case in the documentation. If you have an idea, you can submit a bug report for documentation improvement, or even propose the improved documentation via a pull request.
An addition to the documentation would be welcome! This definitely isn't the first time this has tripped people up.
The buffer[i] = element() line writes a new instance to uninitialized memory, but the subscript is only to be used with initialized memory. This is unsafe, undefined behavior. To initialize elements of a buffer, you need to access the memory location through the buffer's base address (we obviously need a better interface for this directly on the buffer):
extension Array {
init(generating element: @autoclosure () -> Element, count: Int) {
self.init(unsafeUninitializedCapacity: count) {
buffer, initializedCount in
let baseAddress = buffer.baseAddress!
for i in 0..<count {
(baseAddress + i).initialize(to: element())
initializedCount += 1
}
}
}
}
Note that you can also accomplish this same "generating" behavior by calling map — this is how I normally write this: let objects = (0..<n).map { _ in MyObject() }