Search code examples
powershellarraylist

Blank records when returning ArrayList of PSObjects from function


So I'm refactoring a Powershell script and moving a lot of stuff into functions. When I return an ArrayList of 42 PSObjects from a function (called Get-OutList) with return $out, 42 blank records are inserted into the beginning of the ArrayList, and my original records then follow.

My function looks like this:

function Get-OutList {
    param (
        [Parameter(Position=0,mandatory=$true)]
        [PSObject]$RoleCollection,
        [Parameter(Position=1,mandatory=$true)]
        [PSObject]$MemberCollection
    )

    $out = New-Object System.Collections.ArrayList
    $MemberCollection.result | ForEach-Object {
        $currentMember = $_
        $memberDetail = New-Object PSObject
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name FirstName -Value $($currentMember.user.first_name)
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name LastName -Value $($currentMember.user.last_name)
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Email -Value $($currentMember.user.email)
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Status -Value $($currentMember.status)
        
        $RoleCollection.result | ForEach-Object {
            Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name $_.name -Value (&{If($currentMember.roles.name -contains $_.name){"Y"}Else{""}})
        }
        $out.Add($memberDetail)
    }
    return $out
}

I understand Powershell enumerates each record back to where the function was called, and I've tried a few things to no avail:

  • Behaviour is the same in PS v5.1.x and v7.3.0.
  • Returning with return @($out) makes no difference (i.e. results in a System.Object with 42 blank records followed by my 42 original records for a total of 84 records).
  • Returning with return Write-Output -NoEnumerate $out results in a System.Object with 42 blank records and my 42 original records nested in a 43rd record.
  • Typing the result of the function call as ArrayList with $results = [System.Collections.ArrayList](Get-OutList) makes no difference.

Why can't I get my object to be the same as before it's returned from a function?? Any assistance would be greatly appreciated!

Edit #1 Including an easily-reproducible example:

function Get-OutList {
    param (
        [Parameter(Position=0,mandatory=$true)]
        [PSObject]$MemberCollection
    )

    $out = New-Object 'System.Collections.ArrayList'
    $MemberCollection | ForEach-Object {
        $memberDetail = New-Object PSObject
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name FirstName -Value "One"
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name LastName -Value "Two"
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Email -Value "Three"
        Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Status -Value "Four"
        $out.Add($memberDetail)
    }
    return $out
}

$members = @("Joe Bloggs", "Some Dude", "The Dude")

$memberDetails = Get-OutList -MemberCollection $members
Write-Output $memberDetails

If you add a breakpoint before $out is passed back, you'll see there are three records, and if you keep stepping, you should see $memberDetails will have six records (the first three blank).

Edit #2 Appears there's no such problem when using Generic.List instead of ArrayList. Used $out = [System.Collections.Generic.List[PSObject]]::new() instead of $out = New-Object 'System.Collections.ArrayList' and it's working just fine.


Solution

  • Your problem stems from the fact that the System.Collections.ArrayList.Add() method has a return value (the index at which the object has been added) and that that return value - as would happen with any .NET method that returns a value - becomes part of your function's "return value", i.e. stream of output objects.

    This is a manifestation of PowerShell's implicit output behavior, which is explained in this answer.

    Notably, return is not needed in PowerShell to produce output from a function - any statement can produce output. return $foo is simply syntactic sugar for Write-Output $foo; return or, relying on implicit output behavior, $foo; return

    Therefore, the immediate fix to your problem is to discard (suppress) the implicit output produced by your $out.Add($memberDetail) call, which is typically done by assigning the call to $null (there are other methods, discussed in this answer):

    # Discard the unwanted output from .Add()
    $null = $out.Add($memberDetail)
    

    In your own answer, you indirectly applied that fix by switching to the System.Collections.Generic.List`1 list type, whose .Add() method has no return value. It is for that reason, along with the ability to strongly type the list elements, that System.Collections.Generic.List`1 is generally preferable to System.Collections.ArrayList


    Taking a step back:

    In simple cases such as yours, where the entire function output is to be captured in a list, you can take advantage of the output-stream behavior and simply emit each output object individually from your function (from the ForEach-Object script block) and let PowerShell collect the individual objects emitted in a list-like data structure, which will be a regular PowerShell array, namely of type [object[] (i.e., a fixed-size analog to System.Collections.ArrayList):

    This is not only more convenient, but also performs better.

    Applied to your sample function:

    function Get-OutList {
      param (
          [Parameter(Position=0,mandatory=$true)]
          [PSObject]$MemberCollection
      )
    
      $MemberCollection | ForEach-Object {
        $first, $last = -split $_
        # Construct and implicitly output a custom object.
        [pscustomobject] @{
          FirstName = $first
          LastName = $last
          Email = 'Three'
          Status = 'Four'
        }
      }
      # No need for `return` - all output objects were emitted above.
    }
    
    $members = @("Joe Bloggs", "Some Dude", "The Dude")
    
    # By assigning the function's output stream to a variable,
    # the individual output objects are automatically collected in an array
    # (assuming *two or more* output objects; if you want an array even if 
    # only *one* object is output, use [Array] $memberDetails = ...)
    $memberDetails = Get-OutList -MemberCollection $members
    
    # Output (implicitly).
    $memberDetails 
    

    Also note the use of the [pscustomobject] @{ ... } syntactic sugar for simplified custom-object creation - see the conceptual about_Object_Creation help topic.