PowerShell doesn’t include any syntactic sugar for dealing with generic objects, so a number of people have posted code snippets online showing how to create generic lists, dictionaries, etc. Interestingly, nobody has pointed out that the real problem isn’t creating the objects—New-Object does that without issue (for the most part)—but simply that it’s tedious to build correct closed type references for generics. This post attempts to address this core problem.
If we leverage the $args
array, a trivial solution is quickly discovered:
function TypeOf-Generic ([type] $base) { $base.MakeGenericType($args) }
Which is quite simple to use:
$intList = New-Object (TypeOf-Generic System.Collections.Generic.List int)
However, a bit more code yields a more complete solution:
function global:Get-Type ( $type = $(throw "Please specify a type") ) { trap [System.Management.Automation.RuntimeException] { throw ($_.Exception.Message) } if($args -and $args.Count -gt 0) { $types = $args[0] -as [type[]] if(-not $types) { $types = [type[]] $args } if($genericType = [type] ($type + '`' + $types.Count)) { $genericType.MakeGenericType($types) } } else { [type] $type } }
Trapping RuntimeException
allows us to repackage failed casts without the full ErrorRecord
. We get a bit of usage flexibility by accepting our type arguments in either a single array or as the full $args
array. With this function, all of the following will return valid types:
$a = Get-Type ([System.Collections.ArrayList]) $b = Get-Type System.Collections.Hashtable $c = Get-Type System.Collections.Generic.List int $d = Get-Type System.Collections.Generic.Dictionary int,string $e = Get-Type System.Collections.Generic.Dictionary int $d
Corresponding to:
System.Collections.ArrayList System.Collections.Hashtable System.Collections.Generic.List`1[System.Int32] System.Collections.Generic.Dictionary`2[System.Int32,System.String] System.Collections.Generic.Dictionary`2[System.Int32,System.Collections.Generic.Dictionary`2[System.Int32,System.String]]
Having delegated the responsibility of resolving type references, we can update Lee Holmes‘s New-GenericObject
:
function New-GenericObject( $type = $(throw "Please specify a type"), [object[]] $typeParameters = $null, [object[]] $constructorParameters = @() ) { $closedType = (Get-Type $type $typeParameters) ,[Activator]::CreateInstance($closedType, $constructorParameters) }
I’ve included a few extra tweaks:
- I replaced
[string] $typeName
with[object] $type
and changed$typeParameters
from[string[]]
to[object[]]
, specifically to accept[type]
values if they are provided. - If
$typeParameters
is$null
, I assume we were passed a closed type; if not,Get-Type
fails accordingly. $constructorParameters
needs a default value to get around this exception:
Exception calling “CreateInstance” with “2” argument(s): “Ambiguous match found.”
Cross-Assembly Generics
A user on Stack Overflow found an interesting quirk when trying to instantiate a generic SortedDictionary
in PowerShell. The problem is easy to reproduce:
PS 1> $sdt = Get-Type System.Collections.Generic.SortedDictionary string,string PS 2> $sdt.FullName System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] PS 3> $sd = New-Object $sdt New-Object : Cannot find type [System.Collections.Generic.SortedDictionary`2[System.String,System.String]]: make sure t he assembly containing this type is loaded. At line:1 char:17 + $sd = New-Object <<<< $sdt
Yet the type works fine with New-GenericObject
:
PS 4> $sd = New-GenericObject $sdt PS 5> $sd.GetType().FullName System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
As the answer on Stack Overflow points out, SortedDictionary
and System.String
live in different assemblies (System.dll and mscorlib.dll, respectively), which apparently confuses the PowerShell type converter. And because New-Object
expects -typeName
to be a string, it automatically casts our [type]
object (which has the necessary assembly information) back to [string]
:
PS 6> [string] $sdt System.Collections.Generic.SortedDictionary`2[System.String,System.String]
If we pass in the type’s FullName
instead, everything works as expected:
PS 7> $sd2 = New-Object $sdt.FullName PS 8> $sd2.GetType().FullName System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Note that this behavior extends to all assemblies, not just within the System
namespace:
PS 11> $l = New-Object (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb])) New-Object : Cannot find type [System.Collections.Generic.List`1[Microsoft.SharePoint.SPWeb]]: make sure the assembly c ontaining this type is loaded. At line:1 char:16 + $l = New-Object <<<< (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb])) PS 12> $l = New-Object (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb])).FullName PS 13> $l.GetType().FullName System.Collections.Generic.List`1[[Microsoft.SharePoint.SPWeb, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c]]
If someone on the PowerShell team reads this, it would be awesome if New-Object had a parameter set that accepted a [type] instead of a [string]. Thanks in advance!