powershellcasting

can not cast/convert a string array `string[]` to an object array `object[]` but I can for all other types in powershell


For some reason powershell will not let me cast a string[] into an object[] but it will for all other types. The example below shows it working for several types except string.

     [int[]]$intary = 2,4
    [char[]]$chrary = 'a','b'
    [bool[]]$booary = $true,$false
[DateTime[]]$datary = (Get-Date),(Get-Date)
  [string[]]$strary = 'c','d'
            $splary = 'e,f' -split ','  # -split returns [string[]] type specifically
  [object[]]$objaryi = @()
  [object[]]$objaryc = @()
  [object[]]$objaryb = @()
  [object[]]$objaryd = @()
  [object[]]$objarys = @()
  [object[]]$objarysp = @()
$intary.GetType()           # Int32[]
$chrary.GetType()           # Char[] 
$booary.GetType()           # Boolean[]
$datary.GetType()           # DateTime[]
$strary.GetType()           # String[]
$splary.GetType()           # String[]
$objaryi.GetType()          # Object[]

'conversions'
$objaryi = $intary          # Int32[] converted to Object[]
$objaryi.GetType()          # Object[]

$objaryc = $chrary          # Char[] converted to Object[]
$objaryc.GetType()          # Object[]

$objaryb = $booary          # Boolean[] converted to Object[]
$objaryb.GetType()          # Object[]

$objaryd = $datary          # DateTime[] converted to Object[]
$objaryd.GetType()          # Object[]

$objarys = $strary          # NOT converted to Object[]; instead changes type of $objarys to String[]
$objarys.GetType()          # String[]

$objarysp = $splary         # NOT converted to Object[]; instead changes type of $objarysp to String[]
$objarysp.GetType()         # String[]

$objarys = @($strary)       # the array subexpression operator @() DOES convert to Object[]
$objarys.GetType()          # Object[]

$objarysp = @($splary)       # the array subexpression operator @() DOES convert to Object[]
$objarysp.GetType()          # Object[]

'inline cast'
$objarys = @([string[]]@(1,2)) # the array subexpression operator @() does not work on an inline cast [1]
$objarys.GetType()          # String[]

One reason it matters, is that if you try to store a hash in an array that's typed string[] powershell will silently convert your hash definition to a string. The string is the sting for the full path typename of the hash.

$objaryi[0] = @{a=1;b=2}    # can hold a hashtable because it's now an Object[]
$objaryi[0].GetType()       # Hashtable
$objaryi[0]['b']            # 2

$objarys[0] = @{a=1;b=2}    # silently converts to a string because it's a String[]
$objarys[0].GetType()       # String
$objarys[0]['b']            # $null; perhaps unexpected
$objarys[0]                 # the String value "System.Collections.Hashtable"
$objarys[0].Length          # 28

Solution

  • With respect to use of @(...), the array-subexpression operator, to create an [object[]]-typed copy of an array:


    With respect to using array type constraints / casts:

    Note:

    Daniel hit the nail on the head in a comment:

    Surprisingly, PowerShell - in both editions - varies its casting behavior based on whether the input array type is (based on) a .NET value type vs. a .NET reference type. Value types are the various numeric types, e.g. [int], as well as types such as [bool] and [char] that (invariably directly) derive from the abstract System.ValueType base class:[2]


    Workarounds to ensure that an array of a specified type is constructed:

    If performance is paramount, use the following [Array]-based approach instead:

    $arrayString = [string[]] ('foo', 'bar')
    
    # Allocate an empty array of the desired target type.
    $arrayObject = [object[]]::new($arrayString.Length)
    
    # Copy the input array to the target array.
    [Array]::Copy($arrayString, $arrayObject, $arrayString.Length)
    

    The above is by far the best-performing approach, but is both verbose and non-obvious.

    The next fastest option - which is much slower - is to use a foreach statement combined with a type constraint:

    [object[]] $arrayObject = foreach ($el in $arrayString) { $el }
    

    The conceptually most direct approach - though even slower than the foreach approach - is to use a specific overload of the intrinsic .ForEach() method, which allows you to pass a type literal to cast each input object to; e.g.:

    $arrayObject = 
      ([string[]] 'foo', 'bar').ForEach([object])
    

    Note that the above doesn't output an [object[]] array, but an instance of type [System.Collections.ObjectModel.Collection`1[object]], which, however, in practical terms, behaves like an array.
    If you do need an [object[]] array, specifically, you can simply enclose the above in @(...), which, however, involves enumerating the collection and then collecting the enumerated objects in an array, which adds to the inefficiency.

    The slowest option by far is the workaround mentioned in the top section:

    [object[]] $arrayObject =
      $arrayString | Write-Output -NoEnumerate
    

    [1] There is a curious exception: $b = [bool] 'foo' and [bool] $b = 'foo' do not behave the same in their (initial) assignment; the latter causes an error, because parameter-binding logic is unexpectedly applied: see GitHub issue #10426.

    [2] For a given type, you can easily determine whether it is a .NET value type or a .NET reference type by accessing the type's .IsValueType property; e.g. [int].IsValueType is $true, whereas [string].IsValueType is $false
    However, note that arrays are themselves always .NET reference types; e.g. [int[]].IsValueType is $false.
    C# provides the struct keyword for creating value types; PowerShell offers no general way to create them (the class keyword creates reference types, though an enum definition, specifically, is a value type).