Wednesday, January 24, 2018

An array is not an array: Discovering an abstract class in PowerShell

Did you know that an array is not an array? The [Array] class is not an array class. And arrays you create in PowerShell are not [Array] class objects, even if you explicitly told PowerShell to create an [Array]. Weird.

I was poking around in the PowerShell code that defines how PowerShell handles casting. In file LanguagePrimitives.cs, I came across logic similar to the following (here oversimplified and translated into PowerShell).

If ( $TargetObjectType.IsArray     ) { <# Do something #>      ; break }
If ( $TargetObjectType -eq [Array] ) { <# Do something else #> }

This confused me. Wouldn’t an array trigger the first set of code and never get to the second If statement? So I did some digging.

It turns out that [Array] is an “abstract” class. That means you can never create an [Array] object. You--or, more accurately, PowerShell--can create a new custom subclass based on the [Array] class, and then create an object of that subclass.

Array-based subclasses specify what type of object the elements of the array will be.

The element type can be specific. In this example, we explicitly ask for an array with [String] elements. PowerShell then creates a new subclass named System.String[] based on the abstract System.Array class. It then creates an object of the new subclass and sets it as the value of the variable.

[string[]]$Names1 = 'Tim', 'Joe'
$Names1.GetType().FullName  # yields System.String[]

If we don’t specify a type for the elements, PowerShell defaults to using [System.Object] for the elements. All object classes are derived directly or indirectly from [System.Object], so we can put whatever types of object we want in default PowerShell arrays.

In this example, PowerShell creates a new subclass named System.Object[].

$Names2 = 'Tim', 'Joe'
$Names2.GetType().FullName  # yields System.Object[]

We can never have an object that is of type [Array], that is, one where $Object.GetType().FullName is “System.Array”. If we tell PowerShell we want an [Array] object, it is interpreted to mean the default subclass Object[].

[array]$Names3 = 'Tim', 'Joe'
$Names3.GetType().FullName  # yields System.Object[]

Custom subclasses based on [Array] have their .IsArray flag set to $True so that .Net and PowerShell know they can do Array related things to them.

$Names1.GetType().IsArray   # yields $True
$Names2.GetType().IsArray   # yields $True
$Names3.GetType().IsArray   # yields $True

The abstract [System.Array] class has the .IsArray flag set to $False, because PowerShell can’t do Array related things to it, because there can’t be an array of type Array.

[System.Array].IsArray      # yields $False

However, for all arrays, $MyArray -is [Array] yields $True, because -is also checks to see if the class of the object in question is a subclass of the specified class.

[string[]]$Names1 = 'Tim', 'Joe'
$Names1.GetType().FullName                   # yields System.String[]
$Names1.GetType().BaseType.FullName          # yields System.Array
$Names1.GetType().BaseType.BaseType.FullName # yields System.Object
$Names1 -is [System.String[]] # yields $True
$Names1 -is [System.Array]    # yields $True
$Names1 -is [System.Object]   # yields $True
$Names1 -is [String[]]        # yields $True
$Names1 -is [Array]           # yields $True
$Names1 -is [Object]          # yields $True

Looking back on the code in LanguagePrimitives.cs, it now makes sense.

This code block would handle converting to a specified subclass of [Array], such as [System.String[]].

If ( $TargetObjectType.IsArray     ) { <# Do something #>      ; break }

This code block would handle converting to a default [Array], and will result in a [System.Object[]] subclass object. Because [Array] subclasses would be handled by the above code block, only [Array]

If ( $TargetObjectType -eq [Array] ) { <# Do something else #> }

No comments:

Post a Comment