Home > Enterprise >  Trying and failing to pass an array of custom objects by reference
Trying and failing to pass an array of custom objects by reference

Time:04-12

I am creating an array of custom objects in my powershell script

$UnreachablePCs = [PSCustomObject]@()

Then I am passing this into a function like this

function GetComputerData {
param (
    $Computers, [ref]$unreachable
)
...
$unreachablePC = [PSCustomObject]@{
            ComputerName = $i.DNSHostName
            CPU = "n/a"
            Cores = "n/a"
            IP = "n/a"
            Memory = "n/a"
            Uptime = "n/a"
            OS = "n/a"
            Board = "n/a"
        }
    $UnreachablePCs  = $unreachablePC
    Write-Output $UnreachablePCs.Count
...
}


GetComputerData -Computers $TraderWorkstations -unreachable ([ref]$UnreachablePCs)
Write-Output $UnreachablePCs.Count

$TraderWorkstations is a list of pcs which are iterated over in the function. All pcs that aren't reachable are added to the $UnreachablePCs array in an else branch in the function. In the function the .Count I'm calling will increment as workstations are added to the list. But After the function is called the final .Count is returning 0. What am I missing here?

CodePudding user response:

Don't use [ref] parameters in PowerShell code: [ref]'s purpose is to facilitate calling .NET API methods; in PowerShell code, it is syntactically awkward and can lead to subtle bugs, such as in your case - see this answer guidance on when [ref] use is appropriate.

Instead, make your function output the objects that make up the result array (possibly one by one), and let PowerShell collect them for you in an array:

function GetComputerData {
param (
    $Computers # NO [ref] parameter
)
   # ...
   # Output (one or more) [pscustomobject] instances.
   [PSCustomObject]@{
            ComputerName = $i.DNSHostName
            CPU = "n/a"
            Cores = "n/a"
            IP = "n/a"
            Memory = "n/a"
            Uptime = "n/a"
            OS = "n/a"
            Board = "n/a"
        }
  # ...
}

# Collect the [pscustomobject] instances output by the function
# in an array.
$UnReachablePCs = @(GetComputerData -Computers $TraderWorkstations)

@(), the array-subexpression operator, always creates an [object[]] array. To create a strongly typed array instead, use:
[pscustomobject[]] $unreachablePCs = GetComputerData -Computers $TraderWorkstations

Important:

  • [PSCustomObject]@{ ... directly produces output from the function, due to PowerShell's implicit output feature, where any command or expression whose output isn't captured or redirected automatically contributes to the enclosing function's (script's) output (written to PowerShell's success output stream) - see this answer for details. All the objects written to the function's success output streams are captured by a variable assignment such as $UnReachablePCs = ...

  • Write-Output is the explicit (rarely needed) way to write to the success output stream, which also implies that you cannot use it for ad hoc debugging output to the display, because its output too becomes part of the function's "return value" (the objects sent to the success output stream).

  • If you want to-display output that doesn't "pollute" the success output stream, use Write-Host. Preferably, use cmdlets that target other, purpose-specific output streams, such as Write-Verbose and Write-Debug, though both of them require opt-in to produce visible output (see the linked docs).


As for the problems with your original approach:

$UnreachablePCs = [PSCustomObject]@()

This doesn't create an array of custom objects. Instead, it creates an [object[]] array that is (uselessly, mostly invisibly) wrapped in a [psobject] instance.[1]

Use the following instead:

[PSCustomObject[]] $UnreachablePCs = @()

As for use of [ref] with an array variable updated with =:

  • Fundamentally, you need to update a (parameter) variable containing a [ref] instance by assigning to its .Value property, not to the [ref] instance as a whole ($UnreachablePCs.Value = ... rather than $UnreachablePCs = ...)

  • However, the = technique should be avoided except for small arrays, because every = operation requires creating a new array behind the scenes (comprising the original elements and the new one(s)), which is necessary, because arrays are fixed-size data structures.

    • Either: Use an efficiently extensible list data type, such as [System.Collections.Generic.List[PSCustomObject]] and grow it via its .Add() method (in fact, if you create the list instance beforehand, you could pass it as a normal (non-[ref]) argument to a non-[ref] parameter, and the function would still directly update the list, due to operating on the same list instance when calling .Add() - that said, the output approach at the top is generally still preferable):

      $unreachablePCs = [System.Collections.Generic.List[PSCustomObject]] @()
      foreach ($i in 1..2) {
        $unreachablePCs.Add([pscustomobject] @{ foo = $i })
      }
      
    • Or - preferably - when possible: Use PowerShell's loop statements as expressions, and let PowerShell collect all outputs in an array for you (as also shown with output from a whole function above); e.g.:

      # Automatically collects the two custom objects output by the loop.
      [array] $unreachablePCs = foreach ($i in 1..2) {
         [pscustomobject] @{ foo = $i }
      }
      

[1] This behavior is unfortunate, but stems from the type accelerators [pscustomobject] and [psobject] being the same. A [pscustomobject] cast only works meaningfully in the context of creating a single custom object literal, i.e. if followed by a hashtable (e.g., [pscustomobject] @{ foo = 1 }). In all other cases, the mostly invisible wrapping in [psobject] occurs; e.g., [pscustomobject] @() -is [object[]] is $true - i.e., the result behaves like a regular array - but [pscustomobject] @() -is [psobject] is also $true, indicating the presence of a [psobject] wrapper.

  • Related