[System.Collections.Generic.List[string]] as return value
Asked Answered
C

2

6

I have a need/want to return a [System.Collections.Generic.List[string]] from a function, but it is being covered into a System.Object[]

I have this

function TestReturn {
    $returnList = New-Object System.Collections.Generic.List[string]
    $returnList.Add('Testing, one, two')

    return ,@($returnList)
}

$testList = TestReturn
$testList.GetType().FullName

which returns it as a System.Object[], and if I change the return line to

return [System.Collections.Generic.List[string]]$returnList

or

return [System.Collections.Generic.List[string]]@($returnList)

it returns a [System.String] when there is one item in the list, and a System.Object[] if there is more than one item, in both cases. Is there something odd with a list that it can't be used as a return value?

Now, oddly (I think) it DOES work if I type the variable that receives the value, like this.

[System.Collections.Generic.List[string]]$testList = TestReturn

But that seems like some weird coercion and it doesn't happen with other data types.

Cygnet answered 20/5, 2021 at 19:31 Comment(1)
I guess by default, PowerShell will assume System.Array, but if you do [collections.generic.list[string]]$testList = TestReturn it should work fine even if the function is returning a string.Skantze
M
12

If you remove the array subexpression @(...) and just precede with a comma. The below code seems to work:

function TestReturn {
    $returnList = New-Object System.Collections.Generic.List[string]
    $returnList.Add('Testing, one, two')

    return , $returnList
}

$testList = TestReturn
$testList.GetType().FullName

Note: technically this causes the return of [Object[]] with a single element that's of type [System.Collections.Generic.List[string]]. But again because of the implicit unrolling it sort of tricks PowerShell into typing as desired.

On your later point, the syntax [Type]$Var type constrains the variable. It's basically locking in the type for that variable. As such subsequent calls to .GetType() will return that type.

These issues are due to how PowerShell implicitly unrolls arrays on output. The typical solution, somewhat depending on the typing, is to precede the return with a , or ensure the array on the call side, either by type constraining the variable as shown in your questions, or wrapping or casting the return itself. The latter might look something like:

$testList = [System.Collections.Generic.List[string]]TestReturn
$testList.GetType().FullName

To ensure an array when a scalar return is possible and assuming you haven't preceded the return statement with , , you can use the array subexpression on the call side:

$testList = @( TestReturn )
$testList.GetType().FullName

I believe this answer deals with a similar issue

Maure answered 20/5, 2021 at 19:55 Comment(3)
It seems I forgot how to properly handle the unroll behavior, then attributed it to the type. I had been banging my head thinking I might have pipeline solution somehow too. I am moving all my code to classes to avoid the pipeline entirely, and I just verified that the unroll behavior is only in functions. As expected classes don't do unexpected things. I am working on a short term fix to the current function based code, but MAN am I looking forward to moving to classes.Cygnet
Pipeline POLLUTION that is. Spellcheck and me, we don't get along. And it's late.Cygnet
I'm glad I could act as your reminder. BTW "pollution" I used the same term here.Maure
P
4

In addition to Steven's very helpful answer, you also have the option to use the [CmdletBinding()] attribute and then just call $PSCmdlet.WriteObject. By default it will preserve the type.

function Test-ListOutput {
    [CmdletBinding()]
    Param ()
    Process {
        $List = New-Object -TypeName System.Collections.Generic.List[System.String]
        $List.Add("This is a string")
        $PSCmdlet.WriteObject($List)
    }
}
$List = Test-ListOutput
$List.GetType()
$List.GetType().FullName

For an array, you should specify the type.

function Test-ArrayOutput {
    [CmdletBinding()]
    Param ()
    Process {
        [Int32[]]$IntArray = 1..5
        $PSCmdlet.WriteObject($IntArray)
    }
}
$Arr = Test-ArrayOutput
$Arr.GetType()
$Arr.GetType().FullName

By default the behaviour of PSCmdlet.WriteObject() is to not enumerate the collection ($false). If you set the value to $true you can see the behaviour in the Pipeline.

function Test-ListOutput {
    [CmdletBinding()]
    Param ()
    Process {
        $List = New-Object -TypeName System.Collections.Generic.List[System.String]
        $List.Add("This is a string")
        $List.Add("This is another string")
        $List.Add("This is the final string")
        $PSCmdlet.WriteObject($List, $true)
    }
}

Test-ListOutput | % { $_.GetType() }

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object

(Test-ListOutput).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array


# No boolean set defaults to $false
function Test-ListOutput {
    [CmdletBinding()]
    Param ()
    Process {
        $List = New-Object -TypeName System.Collections.Generic.List[System.String]
        $List.Add("This is a string")
        $List.Add("This is another string")
        $List.Add("This is the final string")
        $PSCmdlet.WriteObject($List)
    }
}

Test-ListOutput | % { $_.GetType() }

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

(Test-ListOutput).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

Just wanted to add some information on what I usually use in functions and how to control the behaviour.

Pinup answered 21/5, 2021 at 8:33 Comment(3)
man I wish I knew that trick years ago. I have wasted SO much time chasing down pipeline pollution over the years. Many times I have wished there was a directive to disable the pipeline as the function return mechanism, but there it is. I have so much code now, it's better to just refactor as classes for other reasons and get predictable behavior as a bonus. But learning something new is always good. :)Cygnet
I only knew it from writing C# cmdlets. It's really useful. I would be careful with PowerShell classes. They do basic things well, but there are a lot of open issues around them on GitHub still, and I believe they were mainly introduced to support DSC. Again, I still write any classes in C#.Pinup
My needs are pretty simple, and my knowledge simpler still. So far classes have worked a treat, but a HUGE learning curve for me learning to think different. Hopefully I don't run into any deal breakers. Might need to poke around GitHub a bit to see if I can head any potential issues off at the pass.Cygnet

© 2022 - 2024 — McMap. All rights reserved.