3

As I need to be able to add and remove items from an array I need to cast it as an ArrayList as opposed to the more common options ([string[]], [Array], $Var=@()). I also need to preset it with data, but the caller of the function needs the ability to change the preset as they desire. The preset in the Param() block is required as another entity is looking for preset data. I've tried several variations, but here's the one that makes the most sense:

Function Test-Me{
    Param
    (
     $Properties = [System.Collection.ArrayList]@(
                   "red",
                   "blue",
                   "green")
    )
    $Properties.GetType()
    $Properties.Add("orange")
}

The above is great except that as soon as someone calls Test-Me -Properties "Purple","Yellow","Black" the $Properties variable becomes a standard Array type (whereby add and remove methods don't work).

I tried methods of changing how I declared the preset values. It appears that the act of pre-populating is converting the type to a regular array. I figured this was because I was using @() with the presets so I've tried () as well.

This doesn't work either:

Param
(
[System.Collection.ArrayList]
 $Properties = @("red",
                 "blue",
                 "green")
)

I have a work around which converts the type outside the Param block and it looks like this:

Function Test-Me{
    Param
    (
     [String[]]
     $Properties = @("red",
                     "blue",
                     "green")
    )

    if("Orange" -notin $Properties){
        [System.Collections.ArrayList]$Properties = $Properties
        $Properties.Add("orange")
    }
}

I feel like I should be able to cast as an ArrayList in the param block and preset it with data, and return as the same datatype, but I couldn't figure it out. If anyone does, or finds documentation why it won't work, please answer.

Colyn1337
  • 2,397
  • 2
  • 23
  • 40
  • +1, but this reads as more SO-y than SF-y. – jscott Jul 27 '15 at 19:54
  • You know, I struggled with that one too..... Though my go to for professional and well thought out Q/A is SF. SO is the junk pile, unfortunately. My wedge was that PoSh is on topic, so I leveraged that one :D – Colyn1337 Jul 27 '15 at 19:58
  • Looks like a bug(?) when Powershell processes the CLI args. The implied CLI vartype [array] if overriding what is specified in the paramblock [arraylist]. I gave it a try, but all I could come up with is a slightly different work around that still manipulated $properties outside of the param block. You can do the following to pass it as an arraylist, and it works, but them usage becomes rather clunky. #line1#[System.Collections.ArrayList]$al = "grey","purple" #line2#test-me -properties $al – Clayton Jul 27 '15 at 20:01
  • @Craig620 My first thought was a bug... But I wanted to rule out the most common source of my problems (me), first. If there's no answer in a day or two I'll post it as a bug on the MS site. – Colyn1337 Jul 27 '15 at 20:21
  • @Colyn1337 The second example that you claim "does not work either" - how did you come to that conclusion? If I use your example `param()` block, correct the `Collection` typo and call `GetType()` on `$Properties` inside the body of the scriptblock, it returns the correct type - and the Add() method works flawlessly – Mathias R. Jessen Jul 28 '15 at 00:38
  • @MathiasR.Jessen run the function again but use the parameter to change the presets. The data changes to the array type and not the arraylist type. – Colyn1337 Jul 28 '15 at 13:22
  • @Colyn1337 nope, type is preserved until return – Mathias R. Jessen Jul 28 '15 at 13:56
  • @MathiasR.Jessen I'll be damned, it's working today. Though today I have a fresh ISE instance. I must have foobar'd my environment somehow yesterday... Do I delete the question or leave it? – Colyn1337 Jul 28 '15 at 14:03
  • You wait five minutes for me to write a proper answer ;-) – Mathias R. Jessen Jul 28 '15 at 14:05

2 Answers2

3

Explicit parameter type casting

The second example you posted (explicitly casting the parameter variable) is the correct way to go:

Function Test-Me {
    Param(
        [System.Collections.ArrayList]
        $Properties = @("red","blue","green")
    )

    Write-Host $Properties.GetType().FullName

    if("Orange" -notin $Properties){
        [void]$Properties.Add("orange")
    }

    $Properties
}

Resulting in:

PS C:\> Test-Me
System.Collections.ArrayList
red
blue
green
orange

PS C:\> Test-Me -Properties "one","two","three"
System.Collections.ArrayList
one
two
three
orange

OutputType

One thing that surprised me though, is that even with the [OutputType] attribute, the function outputs a regular array (this may actually be a bug intended behavior, see update below):

Function Test-Me {
    [OutputType([System.Collections.ArrayList])]
    Param(
        [System.Collections.ArrayList]
        $Properties = @("red","blue","green")
    )
    
    if("Orange" -notin $Properties){
        [void]$Properties.Add("orange")
    }

    $Properties
}

Still resulting in a regular object array being returned:

PS C:\> (Test-Me).GetType()

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

Update (with easy workaround)

As demonstrated in the comment on your Connect bug submission, PowerShell deliberately enumerates the items of any enumerable output in order to provide a consistent behavior for the pipeline (pseudo-C# stolen from commentor Derp McDerp on Connect):

if (returned_value is IEnumerable) {
    foreach (r in returned_value) {
        yield r;
    }
} else {
    yield returned_value;
}

The trick is then to wrap the collection in a single-item array, causing PowerShell to pipe it as a single item (notice to , before $Properties):

Function Test-Me {
    [OutputType([System.Collections.ArrayList])]
    Param(
        [System.Collections.ArrayList]
        $Properties = @("red","blue","green")
    )
    
    if("Orange" -notin $Properties){
        [void]$Properties.Add("orange")
    }

    ,$Properties
}

and now, we get an output with the correct type:

PS C:\> (Test-Me).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     ArrayList                                System.Object
Mathias R. Jessen
  • 25,161
  • 4
  • 63
  • 95
0

A co-worker was just asking me to explain some details related to this, so I thought it may be beneficial to share some supplemental information with others who come here with this question in the future.

In PowerShell, there are three basic ways to assign values to variables:

# Loosely typed
# Can be reassigned to anything without error
$myVariable1 = @('Red','Green','Blue')

# Type cast
# Still loosely typed
# Can still be reassigned to anything without issue
$myVariable2 = [System.Collections.ArrayList]@('Red','Green','Blue')

# Strongly typed
# Can only be assigned objects of that type or objects that implicitly convert into that type
[System.Collections.ArrayList]$myVariable3 = 'Red','Green','Blue'

If you want to ensure that a variable (or a function parameter) is of a specific type, you must use strong typing, as was done with $myVariable3. Doing so prevents the type from changing to something else accidentally (e.g. $myVariable3 = 42 will fail with an error).

It is useful to know that you can identify when a variable is strongly typed by using the Get-Variable cmdlet. For example, invoking Get-Variable myVariable1,myVariable2,myVariable3 | Format-List Name,Value,Visibility,Options,Attributes yields the following output:

Name       : myVariable1
Value      : {Red, Green, Blue}
Visibility : Public
Options    : None
Attributes : {}

Name       : myVariable2
Value      : {Red, Green, Blue}
Visibility : Public
Options    : None
Attributes : {}

Name       : myVariable3
Value      : {Red, Green, Blue}
Visibility : Public
Options    : None
Attributes : {System.Management.Automation.ArgumentTypeConverterAttribute}

Note how these are all the same except that the myVariable3 variable has an attribute in the Attributes property. An ArgumentTypeConverterAttribute is added to any variable that is strongly typed, and that attribute will ensure that variable remains of the same type no matter what you assign it to.

Understanding how variable assignment works, as well as how to return a collection intact as Mathias pointed out above, makes it much easier to work with data in PowerShell.

Kirk Munro
  • 101
  • 2