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:
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).return Write-Output -NoEnumerate $out
results in a System.Object with 42 blank records and my 42 original records nested in a 43rd record.$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.
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.