mjolinor's helpful answer provides the crucial pointer: To have the function operate on a copy of the input ArrayList, it must be cloned via .Clone()
first.
Unfortunately, the explanation offered there for why this is required is not correct:[1]
No PowerShell-specific variable behavior comes into play; the behavior is fundamental to the .NET framework itself, which underlies PowerShell:
Variables are technically passed by value (by default[2]), but what that means depends on the variable value's type:
- For value types, for which variables contain the data directly, a copy of the actual data is made.
- For reference types, for which variables only contain a reference to the data, a copy of the reference is made, resulting in effective by-reference passing.
Therefore, in the case at hand, because [System.Collections.ArrayList]
is a reference type (verify with -not [System.Collections.ArrayList].IsValueType
), parameter $local
by design points to the very same ArrayList instance as variable $names
in the calling scope.
Unfortunately, PowerShell can obscure what's happening by cloning objects behind the scenes with certain operations:
Using +=
to append to an array ([System.Object[]]
):
$a = 1, 2, 3 # creates an instance of reference type [Object[]]
$b = $a # $b and $a now point to the SAME array
$a += 4 # creates a NEW instance; $a now points to a DIFFERENT array.
Using +=
to append to a [System.Collections.ArrayList]
instance:
While in the case of an array ([System.Object[]
) a new instance must be created - because arrays are by definition of fixed size - PowerShell unfortunately quietly converts a [System.Collections.ArrayList]
instance to an array when using +=
and therefore obviously also creates a new object, even though [System.Collections.ArrayList]
can be grown, namely with the .Add()
method.
$al = [Collections.ArrayList] @(1, 2, 3) # creates an ArrayList
$b = $al # $b and $al now point to the SAME ArrayList
$al += 4 # !! creates a NEW object of type [Object[]]
# By contrast, this would NOT happen with: $al.Add(4)
Destructuring an array:
$a = 1, 2, 3 # creates an instance of reference type [Object[]]
$first, $a = $a # creates a NEW instance
[1] mjolinor's misconception is around inheriting / shadowing of variables from the parent (ancestral) scope: A parameter declaration is implicitly a local variable declaration. That is, on entering testlocal()
$local
is already a local variable containing whatever was passed as the parameter - it never sees an ancestral variable of the same name. The following snippet demonstrates this: function foo([string] $local) { "`$local inside foo: $local" }; $local = 'hi'; "`$local in calling scope: $local"; foo; foo 'bar'
- foo()
never sees the calling scope's definition of $local
.
[2] Note that some .NET languages (e.g., ref
in C#) and even PowerShell itself ([ref]
) also allow passing a variable by reference, so that the local parameter is effectively just an alias for the calling scope's variable, but this feature is unrelated to the value/reference-type dichotomy.