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
With respect to use of @(...)
, the array-subexpression operator, to create an [object[]]
-typed copy of an array:
Your code works as-is in PowerShell (Core) 7; that is, @(...)
reliably produces an [object[]]
-typed array, irrespective of the type of the input array and irrespective of whether you use a literal cast (e.g. [string[]]
inside @(...)
)
In Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1) you need a workaround when you use a literal cast inside @(...)
, because v5.1 has an ill-advised optimization that optimizes the @(...)
away in this case (an optimization that was later reverted in PowerShell 7):
@([string[]] (42, 43))
as an example:
As implied by the above, simply omitting the [string[]]
cast will result in an [object[]]
array.
If you do need the cast, use the following workaround:
@([string[]] (42, 43) | Write-Output -NoEnumerate)
[object[]]
array.Whenever @(...)
works as intended, note that while it is a convenient way to create an [object[]]
-typed copy of a given array, it is also inefficient:
It involves enumerating the array, i.e. processing its elements one by one, and then collecting them in a(n invariably new) array, always of type [object[]]
).
See the next section for better-performing alternatives.
With respect to using array type constraints / casts:
Note:
A type constraint on a variable (e.g., [object[]] $arrayO = ...
differs from a cast (e.g., $arrayO = [object[]] ...
) mainly in that the former in essence implicitly re-applies the cast to future assignments to the same variable.[1]
I'm only referring to and using casts in the discussion below, but the same applies equally to type constraints.
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]
Casting a .NET value type-based array to any other type - including [object[]]
- does result in an array of the specified type (which is of necessity a (type-modified) copy of the input array).
Casting a .NET reference type-based array to [object[]]
is effectively a no-op.
More generally, the logic is: If the cast type is a (direct or indirect) base type of the input array, the cast is effectively a no-op, meaning that the input array is passed through.
Since [object]
(System.Object
) is the root of the .NET type hierarchy, it follows that an [object[]]
cast is a no-op with any .NET reference type-based array; to illustrate that the same applies to base classes in general:
class Foo {} # sample class
class Foo2: Foo {} # sample class that derives from [Foo]
# Create a [Foo2]-based array
$fooArray = [Foo2[]]::new(2)
# Casting to [Foo[]] is a *no-op*, because [Foo2] derives from [Foo]
([Foo[]] $fooArray).GetType().ToString() # -> !! 'Foo2[]'
However, note that this implies that the special treatment of .NET value type-based arrays is ultimately arbitrary, given that value types too ultimately derive from [object]
.
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).