As has been pointed out, it's worth changing your approach due to its inefficiency:
Instead of blindly appending and then possibly removing the new element if it turns out to be duplicate with Select-Object -Unique
, use a test to decide whether an element needs to be appended or is already present.
Patrick's helpful answer is a straightforward implementation of this optimized approach that will greatly speed up your code and should perform acceptably unless the array lists get very large.
As a side effect of this optimization - because the array lists are only ever modified in-place with .Add()
- your original problem goes away.
To answer the question as asked:
Simply type-constrain your (member) variables if you want them to retain a given type even during later assignments.
That is, just as you did with $name
, place the type you want the member to be constrained to the left of the member variable declarations:
[System.Collections.ArrayList] $parents
[System.Collections.ArrayList] $children
[System.Collections.ArrayList] $members
However, that will initialize these member variables to $null
, which means you won't be able to just call .Add()
in your .add*()
methods; therefore, construct an (initially empty) instance as part of the declaration:
[System.Collections.ArrayList] $parents = [System.Collections.ArrayList]::new()
[System.Collections.ArrayList] $children = [System.Collections.ArrayList]::new()
[System.Collections.ArrayList] $members = [System.Collections.ArrayList]::new()
Also, you do have to use @(...)
around your Select-Object -Unique
pipeline; while that indeed outputs an array (type [object[]]
), the type constraint causes that array to be converted to a [System.Collections.ArrayList]
instance, as explained below.
The need for @(...)
is somewhat surprising - see bottom section.
Notes on type constraints:
If you assign a value that isn't already of the type that the variable is constrained to, PowerShell attempts to convert it to that type; you can think of it as implicitly performing a cast to the constraining type on every assignment:
This can fail, if the assigned value simply isn't convertible; PowerShell's type conversions are generally very flexible, however.
In the case of collection-like types such as [System.Collections.ArrayList]
, any other collection-like type can be assigned, such as the [object[]]
arrays returned by @(...)
(PowerShell's array-subexpression operator). Note that, of necessity, this involves constructing a new [System.Collections.ArrayList]
every time, which becomes, loosely speaking, a shallow clone of the input collection.
Pitfalls re assigning $null
:
If the constraining type is a value type (if its .IsValueType
property reports $true
), assigning $null
will result in the type's default value; e.g., after executing
[int] $i = 42; $i = $null
, $i
isn't $null
, it is 0
.
If the constraining type is a reference type (such as [System.Collections.ArrayList]
), assigning $null
will truly store $null
in the variable, though later attempts to assign non-null values will again result in conversion to the constraining type.
In essence, this is the same technique used in parameter variables, and can also be used in regular variables.
- With regular variables (local variables in a function or script), you must also initialize the variable in order for the type constraint to work (for the variable to even be created); e.g.:
[System.Collections.ArrayList] $alist = 1, 2
Applied to a simplified version of your code:
Class OrgUnit
{
[string] $name
# Type-constrain $children too, just like $name above, and initialize
# with an (initially empty) instance.
[System.Collections.ArrayList] $children = [System.Collections.ArrayList]::new()
addChild($child){
# Add a new element.
# Note the $null = ... to suppress the output from the .Add() method.
$null = $this.children.Add($child)
# (As noted, this approach is inefficient.)
# Note the required @(...) around the RHS (see notes in the last section).
# Due to its type constraint, $this.children remains a [System.Collections.ArrayList] (a new instance is created from the
# [object[]] array that @(...) outputs).
$this.children = @($this.children | Select-Object -Unique)
}
}
With the type constraint in place, the .children
property now remains a [System.Collections.ArrayList]
:
PS> $ou = [OrgUnit]::new(); $ou.addChild(1); $ou.children.GetType().Name
ArrayList # Proof that $children retained its type identity.
Note: The need for @(...)
- to ensure an array-valued assignment value in order to successfully convert to [System.Collections.ArrayList]
- is somewhat surprising, given that the following works with the similar generic list type, [System.Collections.Generic.List[object]]
:
# OK: A scalar (single-object) input results in a 1-element list.
[System.Collections.Generic.List[object]] $list = 'one'
By contrast, this does not work with [System.Collections.ArrayList]
:
# !! FAILS with a scalar (single object)
# Error message: Cannot convert the "one" value of type "System.String" to type "System.Collections.ArrayList".
[System.Collections.ArrayList] $list = 'one'
# OK
# Forcing the RHS to an array ([object[]]) fixes the problem.
[System.Collections.ArrayList] $list = @('one')