The specificity of generics in PowerShell

I have recently been listening in on a discussion about PowerShell generics and some challenges they pose in PowerShell, and one of the questions raised in that discussion that wasn’t being answered was from some IT admins asking why they should care about generics.  In my opinion the easiest question that can determine if someone should care about generics or not, regardless of their role at work, is this:

Is strong typing important to you when you write your scripts?

If that question is confusing to you, then you likely aren’t really writing any scripts complex enough that you need to know how to create generics just yet, or maybe you are and there are bugs in your scripts that you just don’t know about.  Regardless, you likely haven’t developed a really strong need for them yet.  But, if you know what strong typing is and the advantages it provides in terms of script readability and in terms of easier script troubleshooting / debugging due to automatic type checking then it would be worthwhile for you to understand how generics can benefit you as well.

An example may help illustrate the benefits you’ll get when using generics.  For this example, keep in mind it may not be that realistic, but I have seen people do similar things and scratch their head wondering what was going on when it didn’t work the way they thought it would.  For this example lets say you wanted a table of something, we’ll use modules, where you could look them up quickly by name.  Maybe you’ve heard of hash tables / associative arrays, so you try something like this:

PS C:\> $moduleTable = @{}
PS C:\> Get-Module -ListAvailable | ForEach-Object {$moduleTable[$_] = $_}

No errors were returned, so you want to check if it worked.  You try to show the module table you built using PowerShell:

PS C:\> $moduleTable
Name                           Value
—-                           —–
PSDiagnostics                  PSDiagnostics
CmdletDesigner                 CmdletDesigner
TroubleshootingPack            TroubleshootingPack
AppLocker                      AppLocker
BitsTransfer                   BitsTransfer
WebAdministration              WebAdministration
AdminConsole                   AdminConsole
Class                          Class

That looks right, sort of, so you then try accessing one of the module table values:

PS C:\> $moduleTable.WebAdministration
PS C:\> $moduleTable[‘WebAdministration’]
PS C:\>

That’s no good, something isn’t working right at all.  If you’ve learned at some point that hash tables can contain any type of value as their key you might have an idea what the problem is here, but if not this can be really puzzling and it’s time to revisit hash tables 101.  A hash table is a dictionary that associates an object of any type to an object of any other type.  In the case of a hash table though, both types are generic objects (*not* to be confused with generics), so you can mix key types and value types as you wish, like this:

PS C:\> [System.Reflection.Assembly]::LoadWithPartialName(‘System.Drawing’)
PS C:\> $mixedUpMotherGoose = @{}
PS C:\> $mixedUpMotherGoose[1] = ‘One’
PS C:\> $mixedUpMotherGoose[‘Two’] = 2
PS C:\> $mixedUpMotherGoose[‘Red’] = [System.Drawing.Color]::Red
PS C:\> $mixedUpMotherGoose[[System.Drawing.Color]::Blue] = ‘Blue’
PS C:\> $mixedUpMotherGoose
Name                           Value
—-                           —–
Color [Blue]                   Blue
Red                            Color [Red]
Two                            2
1                              One

Accessing values in tables like this can be challenging, as can be shown here:

PS C:\> $mixedUpMotherGoose.$([int]1)
One
PS C:\> $mixedUpMotherGoose.Two
2
PS C:\> $mixedUpMotherGoose.Red
R             : 255
G             : 0
B             : 0
A             : 255
IsKnownColor  : True
IsEmpty       : False
IsNamedColor  : True
IsSystemColor : False
Name          : Red
PS C:\> $mixedUpMotherGoose.$([System.Drawing.Color]::Blue)
Blue

In many cases though, when you are creating a hash table you know the type of association (or rather, the types you want to associate) ahead of time.  You can use the generic hash table (again, this is generic in terms of the objects used inside the hash table and is not to be confused with generics) and many times this will be sufficient for what you want to do.  But if you know the type of association ahead of time, wouldn’t it be great if you could ask PowerShell to watch your back and make sure that the objects you place into your hash table are of the correct type, and to let you know via an error if ever they are not so that you will have one less thing to worry about when you are debugging your script?  Or wouldn’t it be great if you could define your collection with strong typing in mind so that later someone else who works with the same scripts you do doesn’t come along and put other associations of different types into your collection, which may break the logic in your script?  Enter generics.

Generics are collections that have strong type associations enforced by the language itself (in this case, PowerShell).  Depending on your perspective, you may think of them as type-specific collections (or “specifics” as Hal Rottenberg jokingly refers to them) instead of as generics because they enforce strong typing on any items added to the collection once they are created.  System.Collections.Generic.Dictionary is a generic collection that allows you to create strongly typed System.Collections.HashTable’s.  System.Collections.Generic.List is a generic collection that allows you to create strongly typed System.Collections.ArrayList’s.  These collections are generic in the sense that they can be applied generically to any object type resulting in the ability to create strongly-typed collections.  This allows you get all the advantages that strong typing provides in your collections just like you do when you create strongly-typed variables (script readability and easier troubleshooting/debugging, remember?).

Let’s see what this means for PowerShell and the example that we looked at earlier.  To create a table of modules where we can easily look up any module with its string representation (which is its name), we can define a generic dictionary that associates System.String to System.Management.Automation.PSModuleInfo.  We need to be careful and make sure we set the lookup comparison to case insensitive when we create it if we want it to work just like hash tables do in PowerShell.  The command to do that looks like this:

PS C:\> $moduleTable = New-Object -TypeName ‘System.Collections.Generic.Dictionary[System.String,System.Management.Automation.PSModuleInfo]’ -ArgumentList @([System.StringComparer]::CurrentCultureIgnoreCase)

It is important to note the syntax of this command.  The single-quotes that enclose the type name are required when working with generics.  There is definitely room for improvement in how this sort of thing could be done in the future (or today if you don’t want to wait and you want to create a type accelerator called Dictionary using the Accelerator module that Joel Bennett created), but for now knowing this is the format for generic dictionaries and knowing that you can use any type names inside the dictionary definition should be sufficient.  Also as mentioned you need to make sure you don’t forget to set the generic collection up to be case insensitive when you are working with PowerShell and you don’t want to worry about case by passing in the appropriate static property of the StringComparer class.  By default string comparisons are done case sensitive in .NET, so if you leave the ArgumentList parameter out your dictionary keys will be case sensitive.

Once you have that collection, watch what happens when you try to build the contents of the table using the same command you used before.

PS C:\> Get-Module -ListAvailable | ForEach-Object {$moduleTable[$_] = $_}

You get a bunch of errors that look something like this:

Array assignment to [WebAdministration] failed: The value “WebAdministration” is not of type “System.String” and cannot be used in this generic collection.
Parameter name: key.
At line:1 char:58
+ Get-Module -ListAvailable | ForEach-Object {$moduleTable[ <<<< $_] = $_}
    + CategoryInfo          : InvalidOperation: (WebAdministration:PSModuleInfo) [], RuntimeException
    + FullyQualifiedErrorId : ArrayAssignmentFailed

The important details here are in the errors themselves, and they are what you get when using strongly typed collections with incorrect types.  They inform you that the value you are assigning to the “key” parameter is not of type System.String.  In my opinion, when working with scripts that are even a little bit complicated, it is better to get this error that points you to a problem in your script than no error at all.  That’s one of the ways generics add value.

The other way they add value is in the assignment of the collection itself.  The command above that created the generic dictionary associating strings to PSModuleInfo objects tells you what types of objects you are associating, which can go a long way towards helping someone else other than you when they are looking at your script and trying to understand what you’re doing.  Of course they would have to understand generics, but you must be realizing by now that they aren’t all that hard, right?

To eliminate the logic error you were facing in your original hash table experience and properly create your module table, you can do this instead:

PS C:\> Get-Module -ListAvailable | ForEach-Object {$moduleTable.Item($_) = $_}

Now when you try to access a module in the module table using the name, you get the results you were originally after:

PS C:\> $moduleTable.WebAdministration
ModuleType Name                      ExportedCommands
———- —-                      —————-
Manifest   WebAdministration         {}

I should note that when using generic dictionaries, it is easier to use the Item parameterized property by name than it is to use the square brackets identifying the Item parameterized property when populating the dictionary.  The reason for this is because the named Iteme parameterized property takes care of all of the typecasting for you, but assignment when using the square brackets identifying the Item parameterized property does not.  For example, if I were to try populating my dictionary using a syntax similar to my original syntax, the only way I can get that to work in PowerShell 2.0 without errors is as follows:

PS C:\> Get-Module -ListAvailable | ForEach-Object {$moduleTable[[string]$_] = [System.Management.Automation.PSModuleInfo]$_}

This is another area where there is room for improvement in PowerShell when working with generics.  Both methods give you strong typing and therefore raise errors if there are any unexpected types, but the syntax when using the Add method is obviously much easier to type and read afterwards.

Anyhow, that about covers it.  If after reading all of this you still don’t care about generics, even though it’s very likely you use them in PowerShell without realizing it, then you can simply come back here later if you find yourself in a position where you start to care about them.

I hope this helps!

Kirk out.

Share this post:
Advertisements

2 thoughts on “The specificity of generics in PowerShell

  1. There is a difference between using $collection.Add(key, value) and $collection.[key] = value to populate the dictionary or collection. The .Add method in .NET collections usally throws an exception, if the key already exists, whereas the direct assignment via .[key] = value overwrites existing keys with the new value.

    One should know this difference when populating a collection.

    Like

    1. That’s a great point, thanks! I’ll just went back and updated the post recommending people use the Item parameterized property by name instead since that handles both the add and update scenarios without the extra typecasting.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s