0

Thought I have read enough examples here and elsewhere. Still I fail creating arrays in Power Shell.

With that code I hoped to create slices of pair values from an array.

    $values = @('hello','world','bonjour','moon','ola','mars')

function slice_array {
    param (
        [String[]]$Items
    )
    [int16] $size = 2
    $pair = [string[]]::new($size)     # size is 2
    $returns = [System.Collections.ArrayList]@()
    
    [int16] $_i = 0
    foreach($item in $Items){
        $pair[$_i] = $Item
        $_i++;
        if($_i -gt $size - 1){
            $_i = 0
            [void]$returns.Add($pair)
        }
    }
    return $returns
}

slice_array($values)

the output is

ola
mars
ola
mars
ola
mars

I would hope for

'hello','world'
'bonjour','moon'
'ola','mars'

Is possible to slice that array to an array of arrays with length 2 ?

Any explenation why it doesn't work as expected ? How should the code be changed ?

Thanks for any hint to properly understand Arrays in PowerShell !

user3732793
  • 1,699
  • 4
  • 24
  • 53
  • As an aside: PowerShell functions, cmdlets, scripts, and external programs must be invoked _like shell commands_ - `foo arg1 arg2` - _not_ like C# methods - `foo('arg1', 'arg2')`. If you use `,` to separate arguments, you'll construct an _array_ that a command sees as a _single argument_. To prevent accidental use of method syntax, use [`Set-StrictMode -Version 2`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode) or higher, but note its other effects. See [this answer](https://stackoverflow.com/a/65208621/45375) for more information. – mklement0 Mar 02 '21 at 14:32

2 Answers2

1

Here's a PowerShell-idiomatic solution (the fix required for your code is in the bottom section):

  • The function is named Get-Slices to adhere to PowerShell's verb-noun naming convention (see the docs for more information).

    • Note: Often, the singular form of the noun is used, e.g. Get-Item rather than Get-Items, given that you situationally may get one or multiple output values; however, since the express purpose here is to slice a single object into multiple parts, I've chosen the plural.
  • The slice size (count of elements per slice) is passed as a parameter.

  • The function uses .., the range operator, to extract a single slice from an array.

  • It uses PowerShell's implicit output behavior (no need for return, no need to build up a list of return values explicitly; see this answer for more information).

  • It shows how to output an array as a whole from a function, which requires wrapping it in an auxiliary single-element array using the unary form of ,, the array constructor operator. Without this auxiliary array, the array's elements would be output individually to the pipeline (which is also used for function / script output; see this answer for more information.

# Note: For brevity, argument validation, pipeline support, error handling, ...
#       have been omitted.
function Get-Slices {
  
  param (
    [String[]] $Items
    ,
    [int] $Size   # The slice size (element count)
  )

  $sliceCount = [Math]::Ceiling($Items.Count / $Size)
  if ($sliceCount -le 1) {
    # array is empty or as large as or smaller than a slice? -> 
    # wrap it *twice* to ensure that the output is *always* an 
    # *array of arrays*, in this case containing just *one* element
    # containing the original array.
    ,, $Items
  }
  else {
    foreach ($offset in 0..($sliceCount-1)) {
      , $Items[($offset * $Size)..(($offset+1) * $Size - 1)] # output this slice
    }
  }

}

To slice an array into pairs and collect the output in an array of arrays (jagged array):

$arrayOfPairs = 
  Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2

Note:

  • Shell-like syntax is required when you call functions (commands in general) in PowerShell: arguments are whitespace-separated and not enclosed in (...) (see this answer for more information)

  • Since a function's declared parameters are positional by default, naming the arguments as I've done above (-Item ..., -Size ...) isn't strictly necessary, but helps readability.

Two sample calls:

"`n-- Get pairs (slice count 2):"

Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2 |
  ForEach-Object { $_ -join ', ' }


"`n-- Get slices of 3:"

Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 3 |
  ForEach-Object { $_ -join ', ' }

The above yields:

-- Get pairs (slice count 2):
hello, world
bonjour, moon
ola, mars

-- Get slices of 3:
hello, world, bonjour
moon, ola, mars

As for what you tried:

The only problem with your code was that you kept reusing the very same auxiliary array for collecting a pair of elements, so that subsequent iterations replaced the elements of the previous ones, so that, in the end, your array list contained multiple references to the same pair array, reflecting the last pair only.

This behavior occurs, because arrays are instance of reference types rather than value types - see this answer for background information.

The simplest solution is to add a (shallow) clone of your $pair array to your list, which ensures that each list entry is a distinct array:

[void]$returns.Add($pair.Clone())
mklement0
  • 382,024
  • 64
  • 607
  • 775
1

Why you got 3 equal pairs instead of different pairs:

.Net (powershell based on it) is object-oriented language and it has consept of reference types and value types. Almost all types are reference types.

What happens in your code:

  1. You create $pair = [string[]] object. $pair variable actually stores memory address of (reference to) [string[]] object, because arrays are reference types
  2. You fill $pair array with values
  3. You add (!) $pair to $returns. Remember that $pair is reference to memory block. And when you add it to $returns, it adds memory address of [string[]] you wrote values to.
  4. You repeat step2: You fill $pair array with different values, but address of this array in memory keeps the same. Doing this you actually replace values from step2 with new values in the same $pair object.
  5. = // = step3
  6. = // = step4
  7. = // = step3

As a result: in $returns there are three same memory addresses: [[reference to $pair], [reference to $pair], [reference to $pair]]. And $pair values were overwritten by code with last pair values.

On output it works like this:

  1. Powershell looks at $results which is array.
  2. Powershell looks to $results[0] which reference to $pair
  3. Powershell outputs reference to $pair[0]
  4. Powershell outputs reference to $pair[1]
  5. Powershell looks to $results[1] which reference to $pair
  6. Powershell outputs reference to $pair[0]
  7. Powershell outputs reference to $pair[1]
  8. Powershell looks to $results[1] which reference to $pair
  9. Powershell outputs reference to $pair[0]
  10. Powershell outputs reference to $pair[1]

So you see, you triple output the object from the same memory address. You overwritten it 3 times in slice_array and now it stores only last pair values.

To fix it in your code, you should create a new $pair in memory: add $pair = [string[]]::new($size) just after $returns.Add($pair)

filimonic
  • 3,988
  • 2
  • 19
  • 26