PowerShell

PowerShell – Few tricks about HashTables and Arrays I wish I knew when I started

I've been working with PowerShell for a while now and like with most things I do I've learned it by doing and not by reading. I had a job to do, and since I don't like doing things over and over, I thought it's time to learn and do it in PowerShell. Problem with that approach is that you take bits and pieces from PowerShell scripts online that sometimes do the job but not necessarily pay attention to details. And it's fine. I've learned a lot from reading someone else's work, and I learn something new every day. Hopefully, it will be useful to you to some extent.

Couple of bad practices I've used over the years

Below you can see a common scenario that I've used in multiple scripts and I've seen it all over scripts I've found.

$MyArray = @()
$MyArray += 'one'
$MyArray += 'two'
$MyArray += 'other'

Of course, the example above is simplified. Usually, there would be some functions code, and other stuff is happening that just needed the creation of array and assignment of values. What you should know about this, is that every time you add an item to an array with += new is created and content is copied over. While it works fine, it's going to be poor performance on larger objects and overall is considered bad practice.

# Some input data - simplified for example purposes
$SomeData = @('be', 'me', 'one', 'more', 'time')

# assignement of values to array in foreach with slight change to value.
$MyOtherArray = @()
foreach ($Something in $SomeData) {
    $MyOtherArray += "MyValue $Something"
}

Above you can see a simple foreach loop that goes thru a list of strings in $SomeData and foreach string it assigns them to another Array. I've done this many times because it seemed simple, yet now that I'm working with larger datasets there's heavy penalty using it. Something I wasn't aware, or paying attention to.

Things get even more complicated when there are 2 array's that have to be filled with data from one foreach loop.

$MyOtherArray = @()
$MyDifferentArray = @()
foreach ($Something in $SomeData) {
    $MyOtherArray += "MyValue $Something"
    $MyDifferentArray += "Other $Something"
}
Tricks I wish I knew before I started - Arrays

The trick to foreach and PowerShell overall is knowing that if there's nothing on the right hand side (no assignment to the variable) it gets pushed out further down the script.

# Some input data - simplified for example purposes
$SomeData = @('be', 'me', 'one', 'more', 'time')

# assignement of values to array in foreach with slight change to value.
# not optimized approach
$MyOtherArray = @()
foreach ($Something in $SomeData) {
    $MyOtherArray += "MyValue $Something"
}

# better approach
$MyOtherArray1 = foreach ($Something in $SomeData) {
    "MyValue $Something"
}

Do you see what I did there? Since I've not done any assignment and just left value as is, it will get pushed down to $MyOtherArray1 and populate it in one go. We can use a similar approach to functions. When we define a function and just leave foreach loop in it without any assignments the value will be returned from a function just like you would first assign it to Array and then return that Array.

function Show-FirstExample {
    param(
        [string[]] $Test
    )  
    foreach ($my in $Test) {
        $my
    }
}

In old days I would do something like this (which I now consider bad practice – and by bad practice, I mean using $Value += assignment)

function Show-FirstExample {
    param(
        [string[]] $Test
    )
    $Value = @()  
    foreach ($my in $Test) {
        $Value += $my
    }
    return $Value
}

While I have your attention to the first example there's one more thing that's worth mentioning here.  Let's take a look at following scenario:

Write-Color 'Example 1' -Color Cyan

$Value1 = Show-FirstExample -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-FirstExample -Test 'one'
$Value2 -is [array]

Same function, different input, different output.

As you can see above the same function is returning an Array when two strings are passed to function, but only returns a string when one string is passed. While in general, you may say it's not a big deal, sometimes you expect Array and not having it can lead to weird results that you have to deal with.

function Show-SecondExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    $Output

}
Write-Color 'Example 2' -Color Cyan

$Value1 = Show-SecondExample -Test 'one', 'two'
$Value1 -is [array]
$Value2 = Show-SecondExample -Test 'one'
$Value2 -is [array]

While you may be thinking that explicitly telling foreach loop to assign a value to Array solves this issue you're wrong. As you can see in the example above (and results below) I'm verifying that $Output variable is indeed Array before returning it from a function and it reports success, while at the same time, after I verify the output of a function it's no longer array.

The solution to this is using a comma before variable you want to output. It tells PowerShell that you want to keep value type. The comma is a specific type of a prefix unary operator.

As a binary operator, the comma creates an array. As a unary operator, the comma creates an array with one member. Place the comma before the member.

PowerShell is still doing its flattening work thou, but it results in returning your original object intact. If you need to more about operators in PowerShell Microsoft has excellent documentation on that topic. Please be aware using comma also has consequences. Have a read PowerShell – Returning one object from a function as an ArrayIt discusses some issues with a unary operator that you should address if you're going to use it.

function Show-ThirdExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    , $Output
}
Write-Color 'Example 3' -Color Cyan

$Value1 = Show-ThirdExample -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-ThirdExample -Test 'one'
$Value2 -is [array]

Of course, for this to work, you have to set type for $Output variable explicitly. Otherwise, with just one value it would be a string. In another example (below), I'm getting an output of two loops, put them into Arrays and output, as one array. But doing the addition part is most likely creating new Array for the output which is both resource intensive and something you need to remember in the end. You also have to deal with two variables, and if you've more loops in function, this may get quickly out of hand.

function Show-FourthExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output1 = foreach ($my in $Test) {
        $my
    }
    [Array] $Output2 = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if (($Output1 + $Output2) -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    , ($Output1 + $Output2) 
}

Write-Color 'Example 4' -Color Cyan

$Value1 = Show-FourthExample -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-FourthExample -Test 'one'
$Value2 -is [array]

Before giving you a solution to this one I want to show you two more examples that are very common in PowerShell.

function Show-FifthExample {
    param(
        [string[]] $Test
    )  
    if ($Test -contains 'one') {
        'one'
    }
    if ($Test -contains 'two') {
        'two'
    }
}

Write-Color 'Example 5' -Color Cyan

$Value1 = Show-FifthExample -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-FifthExample -Test 'one'
$Value2 -is [array]

As you can see above I'm using what I've learned earlier and I don't assign anything in if/else scenario, and I'm putting variables out in the open to be returned from a function. But the problem is I wanted (expected) an Array, even if there is just one element in it. So now I would go for my usual bad practice and define an Array at the top, and simply assign values during if/else scenario.

function Show-FifthExampleComplicated {
    param(
        [string[]] $Test
    )  
    $Value = @()
    if ($Test -contains 'one') {
        $Value += 'one'
    }
    if ($Test -contains 'two') {
        $Value += 'two'
    }

    # Do something with $Value
    if ($Value.Count -eq 2) {
        Write-Color 'Happy ', 'Birthday' -Color Red, Yellow
    }
    , $Value
}


Write-Color 'Example 5 Complicated' -Color Cyan

$Value1 = Show-FifthExampleComplicated -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-FifthExampleComplicated -Test 'one'
$Value2 -is [array]

What I didn't know till recently, is that you can something like this:

function Show-SixthExample {
    param(
        [string[]] $Test
    )
    $Output = @(
        if ($Test -contains 'one') {
            'one'
        }
        if ($Test -contains 'two') {
            'two'
        }
    )
    , $Output
}

Write-Color 'Example 6' -Color Cyan

$Value1 = Show-SixthExample -Test 'one', 'two'
$Value1 -is [array]

$Value2 = Show-SixthExample -Test 'one'
$Value2 -is [array]

You open an Array at the top and you just close it in the bottom. Anything that's output from if/else, or even foreach loops can be output into this Array for easy return or other data processing. While I constantly mention foreach loops following tricks also apply to While/Do loops.

function Show-SeventhExample {
    param(
        [string[]] $Test
    )
    $Output = @(
        if ($Test -contains 'one') {
            'one'
        }
        if ($Test -contains 'two') {
            'two'
        }
        foreach ($T in $Test) {
            "modified$T"
        }
        $i = 1
        do {
            $i++
            'one'
        } while ($i -le 5)
    )

    # Now we can process whole output with just one assingment
    if ($Output.Count -eq 4) {
        Write-Color 'Hurray' -Color Red
    }

    , $Output
}

Write-Color 'Example 7' -Color Cyan

$Value1 = Show-SeventhExample -Test 'one', 'two'
$Value1 -is [array]
$Value1.Count
Write-Color 'Example 7', ' - ', 'Value 1 output in one line' -Color DarkMagenta, White, Green
$Value1 -join ','

$Value2 = Show-SeventhExample -Test 'one'
$Value2 -is [array]
$Value2.Count
Write-Color 'Example 7', ' - ', 'Value 2 output in one line' -Color DarkMagenta, White, Yellow
$Value2 -join ','

Tricks I wish I knew before I started - HashTables / OrderedDictionary

Let's take a look at simple HashTable creation process.

$Test = @{
    Name    = 'Przemek'
    Surname = 'Klys'
    Job     = 'IT'
    Other = 'Some random comment'
    Other1 = 'Some random comment'
    Other2 = 'Some random comment'
    Other3 = 'Some random comment'
    Other4 = 'Some random comment'
    Other5 = 'Some random comment'
    Other6 = 'Some random comment'
}

$Test | Format-Table -AutoSize

As you can see above, I've created a rather simple HashTable with some values, but when I display it all the names are out of order. For storage purposes, the order doesn't matter, and when we ask HashTable to provide Name or Surname, it will be returned without any problems. But for display purposes it can be important to get HashTable properties in the same order they were put into. That's where OrderedDictionary comes into play. You define it just like you would do with HashTable but you add [ordered] to it.

$Test1 = [ordered] @{
    Name    = 'Przemek'
    Surname = 'Klys'
    Job     = 'IT'
    Other = 'Some random comment'
    Other1 = 'Some random comment'
    Other2 = 'Some random comment'
    Other3 = 'Some random comment'
    Other4 = 'Some random comment'
    Other5 = 'Some random comment'
    Other6 = 'Some random comment'
}

$Test1 | Format-Table -AutoSize

It's simple, yet important knowledge. But that's something you probably already know about. It gets a bit tricky when you work with HashTables/OrderedDictionary with functions. Here's a simplified version of it. In normal circumstances, you would do some processing on HashTable/OrderedDictionary but I want to show you the difference in how things are processed.

function Get-ProcessMyTable {
    param(
        $Table
    )
    $Table | Format-Table -AutoSize
}

Get-ProcessMyTable -Table $Test
Get-ProcessMyTable -Table $Test1

Is it exactly as you would expect it right? But it's not best practice to leave parameters for a function without a type. An adequately defined function has some rules on has to follow for optimal performance and readability. So let's give a type to $Table parameter. Since you can only pick one Type for same parameter let's see what happens

That looks bad. Just because we defined the type our nicely formatted OrderedDictionary was converted to HashTable on the fly and our output is ruined. Let's do this other way around now.

Get-ProcessMyTable : Cannot process argument transformation on parameter ‘Table'. Cannot create object of type “System.Collections.Specialized.OrderedDictionary”. The Other4 property was not found for the System.Collections.Speciali
zed.OrderedDictionary object. The available property is: [Count <System.Int32>] , [IsReadOnly <System.Boolean>] , [Keys <System.Collections.ICollection>] , [Values <System.Collections.ICollection>] , [IsFixedSize <System.Boolean>] ,
[SyncRoot <System.Object>] , [IsSynchronized <System.Boolean>]
At C:\Users\pklys\OneDrive – Evotec\Support\GitHub\PSSharedGoods\Examples\FunTest2.ps1:37 char:27
+ Get-ProcessMyTable -Table $Test
+ ~~~~~
+ CategoryInfo : InvalidData: (:) [Get-ProcessMyTable], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Get-ProcessMyTable

Even worse. Our HashTable was not up for conversion so an error was thrown. In my case, I usually either opted for leaving it without strongly typed parameters or I would always remember to define proper types. But it's not always possible with users. They may want to do this differently. So what do you do? Do you define two sets of parameters?

function Get-ProcessMyTable {
    param(
        [System.Collections.Specialized.OrderedDictionary] $Table,
        [HashTable] $HashTable
    )
    if ($HashTable) {
        $HashTable | Format-Table -AutoSize
    } else {
        $Table | Format-Table -AutoSize
    }
}

Get-ProcessMyTable -HashTable $Test
Get-ProcessMyTable -Table $Test1

That's not really pretty right? Fortunately, there is a way.


$Test = @{
    Name    = 'Przemek'
    Surname = 'Klys'
    Job     = 'IT'
    Other   = 'Some random comment'
    Other1  = 'Some random comment'
    Other2  = 'Some random comment'
    Other3  = 'Some random comment'
    Other4  = 'Some random comment'
    Other5  = 'Some random comment'
    Other6  = 'Some random comment'
}


$Test1 = [ordered] @{
    Name    = 'Przemek'
    Surname = 'Klys'
    Job     = 'IT'
    Other   = 'Some random comment'
    Other1  = 'Some random comment'
    Other2  = 'Some random comment'
    Other3  = 'Some random comment'
    Other4  = 'Some random comment'
    Other5  = 'Some random comment'
    Other6  = 'Some random comment'
}

function Get-ProcessMyTable {
    param(
        [System.Collections.IDictionary] $Table
    )
    $Table | Format-Table -AutoSize
}

Get-ProcessMyTable -Table $Test
Get-ProcessMyTable -Table $Test1

As you can see, there is one type that covers both cases [System.Colllections.IDictionary]. This gives us the flexibility we wanted and typed parameters. The same thing applies to ArrayList. Whether it's a string array,  mixed array or int array you can define it as [Array] which will cover all scenarios.

$MyValue = @('value1', 1, 5)
[string[]] $SmallArray = 'string1', 'string2', 'string3'
[int[]] $SmallInt = 1, 5, 7, 9

function Get-ProcessMyArray {
    param(
        [Array] $Table
    )
    $Table | Format-Table -AutoSize
}

Get-ProcessMyArray -Table $MyValue
Get-ProcessMyArray -Table $SmallArray
Get-ProcessMyArray -Table $SmallInt

Of course, it all depends on your usage scenario. Sometimes, or more likely very often it's better to use direct definitions for the type you expect them in. One benefit of using Array over defined types is that it doesn't do the conversion to types. Take a look at the code below. Notice how three differently defined arrays (mixed, string and int array) are passed to 3 functions and what happens to their outputs.

$MyValue = @('1', 1, 5)
[string[]] $SmallArray = '1', '2', '3'
[int[]] $SmallInt = 1, 5, 7, 9

function Get-ProcessMyArray {
    param(
        [Array] $Array
    )

    foreach ($A in $Array) {
        if ($A -is [string]) {
            Write-Color 'String' -Color Green
        } elseIf ($A -is [int]) {
            Write-Color 'Int' -Color Red
        } else {
            Write-Color 'shouldnt happen' -Color Yellow
        }
    }
    $Array | Format-Table -AutoSize
}


function Get-ProcessMyIntArray {
    param(
        [int[]] $Array
    )

    foreach ($A in $Array) {
        if ($A -is [string]) {
            Write-Color 'String' -Color Green
        } elseIf ($A -is [int]) {
            Write-Color 'Int' -Color Red
        } else {
            Write-Color 'shouldnt happen' -Color Yellow
        }
    }
    $Array | Format-Table -AutoSize
}

function Get-ProcessMyStringArray {
    param(
        [string[]] $Array
    )

    foreach ($A in $Array) {
        if ($A -is [string]) {
            Write-Color 'String' -Color Green
        } elseIf ($A -is [int]) {
            Write-Color 'Int' -Color Red
        } else {
            Write-Color 'shouldnt happen' -Color Yellow
        }
    }
    $Array | Format-Table -AutoSize
}



Write-Color 'Example 1 - Array' -Color Blue -LinesBefore 1
Get-ProcessMyArray -Array $MyValue
Write-Color 'Example 2 - Array' -Color Blue -LinesAfter 1
Get-ProcessMyArray -Array $SmallArray
Write-Color 'Example 3 - Array' -Color Blue -LinesAfter 1
Get-ProcessMyArray -Array $SmallInt


Write-Color 'Example 1 - INT Array' -Color Blue -LinesBefore 1
Get-ProcessMyIntArray -Array $MyValue
Write-Color 'Example 2 - INT Array' -Color Blue -LinesAfter 1
Get-ProcessMyIntArray -Array $SmallArray
Write-Color 'Example 3 - INT Array' -Color Blue -LinesAfter 1
Get-ProcessMyIntArray -Array $SmallInt


Write-Color 'Example 1 - STRING Array' -Color Blue -LinesBefore 1
Get-ProcessMyStringArray -Array $MyValue
Write-Color 'Example 2 - STRING Array' -Color Blue -LinesAfter 1
Get-ProcessMyStringArray -Array $SmallArray
Write-Color 'Example 3 - STRING Array' -Color Blue -LinesAfter 1
Get-ProcessMyStringArray -Array $SmallInt

PowerShell did a conversion from string to int or int to string on the fly when requesting type was of a particular type. Of course, it only does that if it's able to do so. If you would pass a word one and not number, it wouldn't automatically do a conversion. It would throw an error.

Get-ProcessMyIntArray : Cannot process argument transformation on parameter ‘Array'. Cannot convert value “one” to type “System.Int32”. Error: “Input string was not in a correct format.”
At C:\Users\pklys\OneDrive – Evotec\Support\GitHub\PSSharedGoods\Examples\FunTest2.ps1:111 char:30
+ Get-ProcessMyIntArray -Array $SmallArray
+ ~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Get-ProcessMyIntArray], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Get-ProcessMyIntArray

Whether the conversion is something you want or expect it to happen is up to you. You need to be aware of things that are happening, so you're not surprised when it happens.

Foreach Loop with multiple outputs

Finally, I wanted to come back to the last case I'm still struggling to solve in a most optimized manner. Since I want to assign values from one loop to two different Array's my usual scenario would look like this

$MyOtherArray = @()
$MyDifferentArray = @()
foreach ($Something in $SomeData) {
    $MyOtherArray += "MyValue $Something"
    $MyDifferentArray += "Other $Something"
}

I know it's bad, you know it's bad so how do I solve this? I'm actually using ArrayList or GenericLists in that case.

# Some input data - simplified for example purposes
$SomeData = @('be', 'me', 'one', 'more', 'time')

$MyArray = @()
$MyArray += 'one'
$MyArray += 'two'
$MyArray += 'other'

$MyOtherArray = @()
$MyDifferentArray = @()
foreach ($Something in $SomeData) {
    $MyOtherArray += "MyValue $Something"
    $MyDifferentArray += "Other $Something"
}

Write-Color 'Array - Output' -Color Blue

$MyOtherArray -join ','
$MyDifferentArray -join ','


$ArrayList1 = [System.Collections.ArrayList]::new()
$ArrayList2 = [System.Collections.ArrayList]::new()

foreach ($Something in $SomeData) {
    $null = $ArrayList1.Add("MyValue $Something")
    $null = $ArrayList2.Add("Other $Something")
}
Write-Color 'ArrayList - Output' -Color Blue

$ArrayList1 -join ','
$ArrayList2 -join ','


$GenericList1 = [System.Collections.Generic.List[string]]::new()
$GenericList2 = [System.Collections.Generic.List[string]]::new()

foreach ($Something in $SomeData) {
    $GenericList1.Add("MyValue $Something")
    $GenericList2.Add("Other $Something")
}

Write-Color 'GenericList - Output' -Color Blue

$GenericList1 -join ','
$GenericList2 -join ','

Both GenericLists and ArrayLists have their pros and cons and require some getting used to dealing with different problems. For example in the code above GenericList is defined with string type, while ArrayLists are typeless. Also, you should notice how I'm using $null to send an output of Add for ArrayLists because Arraylists return number of elements in Array when using add.

ArrayList Usage
As per Microsoft recommendation you shouldn't use ArrayList class for new development. Instead, they recommend that you use the generic List<T> class. The ArrayList class is designed to hold heterogeneous collections of objects. However, it does not always offer the best performance. Instead, they recommend the following:
  • For a heterogeneous collection of objects, use the List<Object> (in C#) or List(Of Object) (in Visual Basic) type.
  • For a homogeneous collection of objects, use the List<T> class. See Performance Considerations in the List<T> reference topic for a discussion of the relative performance of these classes. See Non-generic collections shouldn't be used on GitHub for general information on the use of generic instead of non-generic collection types.

The warning above basically means you shouldn't use it in PowerShell unless you have to. They propose using GenericList instead.

There's also a problem that they don't behave the same way Array does when it comes to adding two ArrayLists or GenericLists together. Can you guess what will be an output of adding two ArrayLists together, where ArrayList1 and ArrayList2 have 5 elements each?

$ArrayList1.Count
$ArrayList2.Count

$null = $ArrayList1.Add($ArrayList2)

$ArrayList1.Count

You guessed it right! It will be 6! This is because what we did is we now have an ArrayList that consists of 5 string elements and 1 ArrayList. If we would like to to have both ArrayLists merged we would do AddRange. This would cause both ArrayLists to be combined giving us ten elements in total. Same things apply to GenericLists.

$ArrayList1.Count
$ArrayList2.Count

$null = $ArrayList1.AddRange($ArrayList2)

$ArrayList1.Count

What's great about ArrayLists and GenericLists is that they allow you to remove elements from them. They have Remove, RemoveAt, RemoveRange, RemoveAll options. So just like you can add elements you can remove them without any rebuilding of your Array.

$GenericList1.Count
$GenericList2.Count

$GenericList1.Remove('MyValue be')
$GenericList1.Count
$GenericList1 -join ','

Keep in mind that when removing elements you will get True/False depending on the success of your action. If you know any other way to deal with the scenario above that doesn't involve using ArrayLists or GenericLists, please let me know. I'm happy to learn! It's not that I don't like them but if there's another way to solve this why not?

Keep in mind that GenericList can also act as ArrayList. You don't have to use strong type string, but you can use Object like in the example below

# Some input data - simplified for example purposes
$SomeData = @('be', 'me', 'one', 'more', 'time')

$GenericList1 = [System.Collections.Generic.List[Object]]::new()
$GenericList2 = [System.Collections.Generic.List[Object]]::new()

foreach ($Something in $SomeData) {
    $GenericList1.Add("MyValue $Something")
    $GenericList2.Add("Other $Something")
}

$GenericList1.Count
$GenericList2.Count

$GenericList1.Remove('MyValue be')
$GenericList1.Count
$GenericList1 -join ','
ArrayList Usage
As per Microsoft recommendation you shouldn't use ArrayList class for new development. Instead, they recommend that you use the generic List<T> class. The ArrayList class is designed to hold heterogeneous collections of objects. However, it does not always offer the best performance. Instead, they recommend the following:
  • For a heterogeneous collection of objects, use the List<Object> (in C#) or List(Of Object) (in Visual Basic) type.
  • For a homogeneous collection of objects, use the List<T> class. See Performance Considerations in the List<T> reference topic for a discussion of the relative performance of these classes. See Non-generic collections shouldn't be used on GitHub for general information on the use of generic instead of non-generic collection types.

Considering the above warning I would recommend using GenericList over ArrayList.

Performance considerations for ArrayList vs GenericList vs Array - Creating Objects

Some performance testing on ArrayList vs GenericList vs Array vs using the additional function. There's also a comparison between using New-Object vs New().

function New-GenericList1 {
    [CmdletBinding()]
    param()
    [System.Collections.Generic.List[Object]]::new()
}

Write-Color 'Test - GenericList via External Function' -Color Green

$Start = Start-TimeLog

for ($i = 0; $i -le 10000; $i++) {
    $Value = New-GenericList #-Type [string]
    #$Value.Add('Test')
}
Stop-TimeLog -Time $Start

Write-Color 'Test - GenericList via External Function' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu2 = New-GenericList1
}
Stop-TimeLog -Time $Start

Write-Color 'Test - ArrayList via External Function' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu3 = New-ArrayList
}
Stop-TimeLog -Time $Start

Write-Color 'Test - GenericList using new()' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu4 = [System.Collections.Generic.List[Object]]::new()
}
Stop-TimeLog -Time $Start

Write-Color 'Test - GenericList using New-Object' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu4 = New-Object "System.Collections.Generic.List[Object]"
}
Stop-TimeLog -Time $Start

Write-Color 'Test - ArrayList using New-Object' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu5 = New-Object "System.Collections.ArrayList"
}
Stop-TimeLog -Time $Start

Write-Color 'Test - Array' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu5 = @()
}
Stop-TimeLog -Time $Start

Write-Color 'Test - ArrayList using new()' -Color Green

$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu6 = [System.Collections.ArrayList]::new()
    #$Valu2.Add('Test')
}
Stop-TimeLog -Time $Start

As you can see above creating different object types 10000 times is fastest using built-in Array object. Next, we have GenericList and ArrayList using New(). Keep in mind that it's highly unlikely to create 10000 objects, so this test shows how something impact building data-intensive scripts.

Performance considerations for ArrayList vs GenericList vs Array - Adding Elements

Following is confirmation why you should avoid += for assigning values to Array.

function New-GenericList1 {
    [CmdletBinding()]
    param()
    [System.Collections.Generic.List[Object]]::new()
}

Write-Color 'Test - GenericList using new()' -Color Green
$Valu2 = [System.Collections.Generic.List[Object]]::new()
$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu2.Add('Test')
}
Stop-TimeLog -Time $Start

Write-Color 'Test - ArrayList using new()' -Color Green
$Valu6 = [System.Collections.ArrayList]::new()
$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {  
    $null = $Valu6.Add('Test')
}
Stop-TimeLog -Time $Start

Write-Color 'Test - Array Bad Practice' -Color Green
$Valu5 = @()
$Start = Start-TimeLog
for ($i = 0; $i -le 10000; $i++) {
    $Valu5 += 'Test'
}
Stop-TimeLog -Time $Start

Write-Color 'Test - Array Best Practice' -Color Green
$Start = Start-TimeLog
$Value5 = for ($i = 0; $i -le 10000; $i++) {
    'Test'
}
Stop-TimeLog -Time $Start

Just for 10000 objects, the difference is noticeable. For 100000 it's even more apparent. But in case it's not possible to use built-in Array using methods described above you should try and use GenericList instead.

Write-Color 'Test - GenericList using new()' -Color Green
$Valu2 = [System.Collections.Generic.List[Object]]::new()
$Start = Start-TimeLog
for ($i = 0; $i -le 100000; $i++) {
    $Valu2.Add('Test')
}
Stop-TimeLog -Time $Start

Write-Color 'Test - ArrayList using new()' -Color Green
$Valu6 = [System.Collections.ArrayList]::new()
$Start = Start-TimeLog
for ($i = 0; $i -le 100000; $i++) {  
    $null = $Valu6.Add('Test')
}
Stop-TimeLog -Time $Start

Write-Color 'Test - Array Best Pratice' -Color Green
$Start = Start-TimeLog
$Value5 = for ($i = 0; $i -le 100000; $i++) {
    'Test'
}
Stop-TimeLog -Time $Start

Write-Color 'Test - Array Bad Practice' -Color Green
$Valu5 = @()
$Start = Start-TimeLog
for ($i = 0; $i -le 100000; $i++) {
    $Valu5 += 'Test'
}
Stop-TimeLog -Time $Start

This is not a mistake. For 100k elements, it took over five minutes to add them to Array the “old” way. So if you've not been convinced from above usage examples, hopefully, those performance tests will get you on board to try and optimize for speed.

This post was last modified on May 8, 2019 12:27

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

2 months ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

3 months ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 months ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

12 months ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 year ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 year ago