Search code examples
arrayspowershelljagged-arrays

How to edit value in a sub array


I can't understand this behavior in powershell :

$PSVersionTable.PSVersion

""

$matrix =, @(, @("foo") * 5) * 3

$matrix[0][1] = "bar"

foreach($subArray in $matrix){
    Write-Host $($subArray[1])
}

With this script I'm initializing an array of 3 arrays, each containing 5 string "foo".

I want to set the 2nd element of the 1st array to "bar", and each arrays are affected :

# Returned :
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      15063  1155    

bar
bar
bar

Is it a bug or am I missuntderstanding how arrays works ?


Solution

  • What the * operator de facto does with an array as the LHS is to use that array's elements as-is as the elements of the resulting array.

    In the case at hand, this means that $matrix[0], $matrix[1], and $matrix[2] all point to the same 5-element array composed of "foo" strings, so modifying that one array's elements will be reflected in all 3 $matrix elements.

    This behavior is likely by design (see bottom section), and I am unaware of a concise workaround with the * syntax.

    A viable workaround is:

    # Note: The (...) around the arrays to replicate aren't strictly needed,
    #       because `,` has higher precedence than `*`, but were added for
    #       readability.
    $nestedArrayTemplate = (, 'foo') * 5
    $matrix = (, @()) * 3  # (, $null) * 3 would work too
    
    for ($i=0; $i -lt $matrix.Count; ++$i) { $matrix[$i] = $nestedArrayTemplate.Clone() }
    

    As an aside: the above creates a jagged array, not a matrix (2-dimensional array). Here's how you would create a true 2-dimensional array:

    $matrix = [string[,]]::new(3, 5) # PSv4-: New-Object string[] 3, 5
    for ($row = 0; $row -lt $matrix.GetLength(0); ++$row) {
      for ($col = 0; $col -lt $matrix.GetLength(1); ++$col ){
        $matrix[$row, $col] = 'foo'
      }
    }
    

    Note that indexing into this 2-dimensional array then requires a single index expression with a list of indices; e.g., [0, 1] rather than [0][1].


    Design musings:

    I can only speculate about the design intent, but I presume that no attempt is made to clone collections that happen to be the elements of the LHS array, because there's no guarantee that such collections can be cloned - the same more generally applies to all element types of the LHS array that are reference types (other than strings).

    With LHS array elements that are value types or strings - which is the more typical case - there's no problem, because the resulting array's elements will (effectively) be independent copies.

    In short: for predictable array replication with *, restrict the LHS array's element types to value types or strings.