Powershell arguments by reference: implied vs explicit - powershell

Powershell supports arguments by reference two different ways. Any variable can be passed by reference using [Ref] as the type, or basically I guess casting to the special [Ref] type. Alternatively, complex data types like [Hashtable] are always passed by reference. The former approach then requires using .value when either changing the value or passing the variable to another class method or function. In simplified form this is what that looks like in practice.
# implicit By Reference
class One_i {
static [Void] Act ([System.Collections.Generic.List[string]] $data) {
$data.Add("One_i::Act(): $(Get-Date)")
[Two_i]::Act($data)
}
}
class Two_i {
static [Void] Act ([System.Collections.Generic.List[string]] $data) {
$data.Add("Two_i::Act(): $(Get-Date)")
}
}
# explicit By Reference
class One_e {
static [Void] Act ([Ref] $data) {
$data.value.Add("One_e::Act(): $(Get-Date)")
[Two_e]::Act($data.value)
}
}
class Two_e {
static [Void] Act ([Ref] $data) {
$data.value.Add("Two_e::Act(): $(Get-Date)")
}
}
CLS
$data_i = [System.Collections.Generic.List[string]]::new()
$data_i.Add("Init_i: $(Get-Date)")
[One_i]::Act($data_i)
$data_i.Add("Finalize_i: $(Get-Date)")
foreach ($item in $data_i) {
Write-Host "$item"
}
Write-Host
$data_e = [System.Collections.Generic.List[string]]::new()
$data_e.Add("Init_e: $(Get-Date)")
[One_e]::Act($data_e)
$data_e.Add("Finalize_e: $(Get-Date)")
foreach ($item in $data_e) {
Write-Host "$item"
}
Write-Host
I am conflicted as to which approach I prefer. On the one hand [Ref] makes it clear which variables are By Ref, which is useful. However, the need for .value makes the code a bit harder to read, adds work, and creates an opportunity to forget and induce a bug. I tested performance like this...
(measure-Command {
$data_i = [System.Collections.Generic.List[string]]::new()
$data_i.Add("Init_i: $(Get-Date)")
foreach ($i in 1..1000) {
[One_i]::Act($data_i)
}
$data_i.Add("Finalize_i: $(Get-Date)")
}).TotalSeconds
(measure-Command {
$data_e = [System.Collections.Generic.List[string]]::new()
$data_e.Add("Init_e: $(Get-Date)")
foreach ($e in 1..1000) {
[One_e]::Act($data_e)
}
$data_e.Add("Finalize_e: $(Get-Date)")
}).TotalSeconds
and implicit seems to be VERY slightly faster, but not enough to make an argument for implicit based on performance alone. This leads me to a number of questions as I try to decide which approach to adopt, or perhaps the realization that each approach is the better choice in certain situations and using both in a single program actually has merit.
1: Why are complex types handled differently than simple types? The inconsistent behavior seems odd to me, and I suspect I will learn something fundament if I understand why the behavior is different.
2: Beyond needing to use .value or not, are there other differences between implicit and explicit By Reference? Especially are there any potential problems with one approach or the other?
3: Is there a mechanism to force a complex type to be By Value? And what would be the use case for that?
4: I found this thread, with a comment saying it's best to avoid By Reference (but without explanation). However, I have also found references to Dependency Injection being superior to Global variables and/or the Singleton pattern, and By Reference is fundamental to DI. Is there annoying important I need to be aware of here, ESPECIALLY as it relates to PowerShell/Classes/Dependency Injection, given that PowerShell classes are not as fully implemented as with other languages. For what it's worth I am working in PS 5.1 and will not be moving to Core any time soon, but it would be great to know if there are fundamental differences between Core and pre Core implementations.
EDIT: Based on #mclayton's answer and my understanding of "Changing value" vs "mutation" I tried "breaking" the reference like this
# implicit By Reference
class One_i {
static [Void] Act ([System.Collections.Generic.List[string]] $data) {
$data.Add("One_i::Act(): $(Get-Date)")
[Two_i]::Act($data)
$data = [System.Collections.Generic.List[string]]::new()
}
}
# explicit By Reference
class One_e {
static [Void] Act ([Ref] $data) {
$data.value.Add("One_e::Act(): $(Get-Date)")
[Two_e]::Act($data.value)
$data.value = [System.Collections.Generic.List[string]]::new()
}
}
I tried both $data.value = [System.Collections.Generic.List[string]]::new() and $data = [System.Collections.Generic.List[string]]::new() in the "explicit" example, and none of theme actually seemed to cause an issue. The console output was "correct" in all three cases. Which suggests that I DON'T understand the difference between changing value and mutation after all.
That said, as it relates to my initial reason for asking the question, namely deciding which of the two approaches I want to use, I am leaning towards what I call "implicit", since it does make the code a bit simpler by eliminating the need for .value, and not having the by reference nature of the variable be obvious doesn't change the fact that I have to understand that it is by reference, no matter what I do. I really wish I could find a thread that discusses the relative merits of the two approaches, but thus far my searches have failed to turn up anything useful.
Edit2: So, I found this where mklement0 says there really is no functional difference in the two approaches. And it occurred to me that I could get the best of both worlds by using [Ref] and then assigning $ref.value to a local variable, to simplify things within the method or function. Tentatively feeling like that is the best answer. It add one line at the top of each function or method, but makes the calls more obvious in behavior as well as simplifying use in the body of the method or function.
# explicit By Reference with sugar
class One_x {
static [Void] Act ([Ref] $data) {
$localData = $data.Value
$localData.Add("One_x::Act(): $(Get-Date)")
[Two_e]::Act(([Ref]$localData))
}
}
class Two_x {
static [Void] Act ([Ref] $data) {
$localData = $data.Value
$localData.Add("Two_x::Act(): $(Get-Date)")
}
}

tl;dr
Here's the key differences between value types, reference types, passing by value and passing by reference:
Pass by value
Pass by reference
Value type
Assignments to parameters inside a function are not persisted to outer variables
Assignments to parameters inside a function are persisted to outer variables
Reference type
Assignments to parameters inside a function are not persisted to outer variablesMutations to object inside function are visible outside function
Assignments to parameters inside a function are persisted to outer variablesMutations to object inside function are visible outside function
Loooooooong version
First off, I think the documentation for [ref] is a bit misleading, despite improvements to it in the past (see about_Ref and Improve the about_Ref topic to clarify its primary purpose).
The main problem is there's a subtle difference between "Value types vs Reference types" and "Passing by value vs Passing by reference" - the about_Ref documentation touches on it, but sort of blurs it all into one big single concept.
Value Types vs Reference Types
Ignoring [ref] for a moment, there are two fundamental types in the .Net world, and there are some similarities and differences in the way they're passed as parameters:
Value types - https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-type
A variable of a value type contains an instance of the type. This differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on assignment, passing an argument to a method, and returning a method result, variable values are copied. In the case of value-type variables, the corresponding type instances are copied.
Reference types - https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types
With reference types, two variables can reference the same object; therefore, operations on one variable can affect the object referenced by the other variable.
The default behaviour for both value types and reference types is to pass "by value" - that is, parameters passed into a method are a copy of the variable value from the parent scope. For value types this means a full copy of the entire value (including for struct types), and for reference types it means passing in a duplicate reference to the same underlying object. Assigning a new value or object reference to the parameter within the method won't persist beyond the scope of the method because it only affects the parameter, which is a copy of the outer value-type or reference-type variable.
To use examples from about_Ref:
In the following example, the function changes the value of the variable passed to it. In PowerShell, integers are value types so they are passed by value. Therefore, the value of $var is unchanged outside the scope of the function.
(Note - this should probably say "the function changes the value of the parameter passed to it", since it doesn't actually change the value of the outer variable)
Function Test($data)
{
$data = 3
}
$var = 10
Test -data $var
$var
# 3 # no change
When $var is a value-type, assigning a new value to $data inside the function makes no difference to the value of $var outside the function because the function is passed a "copy" of the value-type variable and any changes only affect the copy.
A similar example for reference types that shows the $var variable is also unchanged after the function finishes:
Function Test($data)
{
$data = #{ "Test" = "New Text" }
}
$var = #{ "xxx" = "yyy"}
Test -data $var
$var
# #{ "xxx" = "yyy" } # no change
Again, $data is a duplicate reference to the same reference-type object as in the $var variable - they're independent references even though they point to the same object, so assigning a new value to $data doesn't affect $var.
Note however, that about_Ref gives this example as well:
In the following example, a variable containing a Hashtable is passed to a function. Hashtable is an object type so by default it is passed to the function by reference.
When passing a variable by reference, the function can change the data and that change persists after the function executes.
This is a bit misleading though because:
A hashtable is not an "object type", it's a "reference type"
The parameter isn't being "passed to the function by reference" - it's actually a reference-type variable being passed into the function by value!
Function Test($data)
{
$data.Test = "New Text"
}
$var = #{}
Test -data $var
$var
# #{ "Test" = "New Test" }
To clarify, this example isn't changing the value of the $data parameter (i.e. which object it points to) - it's simply calling an instance method on the reference-type object the parameter points to, and that method mutates the object. Since the $var variable in the outer scope is pointing to the same reference-type object, changes to $data in the function are "visible" to $var outside the function because they're both referring to the same object in memory.
It's basically equivalent to this after PowerShell has resolved the appropriate type accelerators:
Function Test($data)
{
$data.Add("Test", "New Text")
}
$var = #{}
Test -data $var
$var
# #{ "Test" = "New Text" }
And that's the important difference between value types and reference types - we can mutate reference types inside a function and the changes will be visible in the outer variable when the function exists because even though the parameter and the variable are independent references, they refer to the same object, so mutations made via the function parameter are visible from the outer variable.
Summary: By default, assigning a value to a parameter inside a function doesn't affect what is assigned to the variable in the parent scope, regardless of whether the parent variable is a value type or reference type. Reference type objects can be mutated within the function by calling methods on the object referred to by the parameter, and the effect is visible in the variable outside the function because they're pointing to the same reference-type object.
Passing by value vs passing by reference
When you pass a variable by value you get the behaviour described above in "Value Types vs Reference Types" - that is, assignments to a parameter inside a function do not persist back into the variable in the parent scope.
By contrast, when you pass a variable by reference you can assign a new value-type or reference-type value to the variable in the parent scope:
Passing Parameters - https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters
Passing by reference enables function members, methods, properties, indexers, operators, and constructors to change the value of the parameters and have that change persist in the calling environment.
And about_Ref says:
You can code your functions to take a parameter as a reference, regardless of the type of data passed.
Although it should probably say "You can code your functions to pass a parameter by reference" to avoid confusion with "reference-type parameters".
The example it gives is a value-type parameter passed by reference:
Function Test([ref]$data)
{
$data.Value = 3
}
$var = 10
Test -data ([ref]$var)
$var
# 3
Note that here, changes to $data.Value inside Test persist into $var after the function call because the variable is passed by reference.
Similarly for reference types passed by reference:
Function Test([ref]$data)
{
$data.Value = #{ "Test" = "New Text" }
}
$var = #{ "xxx" = "yyy" }
Test -data ([ref]$var)
$var
# #{ "Test" = "New Text" } # new value
In this case, we've not mutated the original hashtable - we've simply assigned a whole different hashtable to $var. We can prove this with a couple more lines of code:
$new = #{ "xxx" = "yyy" }
$old = $new
Test -data ([ref]$new)
$new
# #{ "Test" = "New Text" } # new value
$old
# #{ "xxx" = "yyy" } # old value is still there unchanged
$new and $old both pointed to the same hashtable before the function call, but afterwards $new points to a completely different object.
Summary: When passing parameters by reference, assigning a new value to a parameter inside a function also assigns the new value to the variable in the parent scope, regardless of whether the parent variable is a value type or reference type.
Grand Summary
Bringing it right back to your original question, you can hopefully see now that your two examples aren't really equivalent:
class One_i {
static [Void] Act ([System.Collections.Generic.List[string]] $data) {
$data.Add("One_i::Act(): $(Get-Date)")
[Two_i]::Act($data)
}
}
class One_e {
static [Void] Act ([Ref] $data) {
$data.value.Add("One_e::Act(): $(Get-Date)")
[Two_e]::Act($data.value)
}
}
There's no "implicit" and "explicit" [ref] as such - both approaches simply mutate the contents of the $data reference-type parameter in-place but (if it wanted to) One_e could potentially change the object referred to by $data_e which would be persisted into $var after the function call ends. To prove this, try adding this at the end of your two methods $data.Value = #{ "xxx" = "yyy" } and see which version persists the change back up to $data_i or $data_e (you can probably guess One_e does :-).

Related

PowerShell, cannot append a variable inside a function

I'm a bit confused about something in PowerShell.
At the end of a function it appends some text to a variable so that I can log what happened. But when I do this, $x just contains the header and nothing else. What am I doing wrong?
$x = "Header`n=========="
function xxx ($z) {
$z = "$z ... 1"
$x += "`noutput from $z"
}
xxx 123
xxx 234
xxx 345
xxx 456
To summarise the comments, and lean on this answer by #mklement0 from a similar question - https://stackoverflow.com/a/38675054/3156906 - the default behaviour is for PowerShell to let you read variables from a parent scope, but if you assign a value it creates a new variable in the child scope which "hides" the parent variable until the child scope exists.
The about_Scopes documentation says something similar as well, but doesn't really lay it out in such specific detail unfortunately:
If you create an item in a scope, and the item shares its name with an item in a different scope, the original item might be hidden under the new item, but it is not overridden or changed.
You can assign values to the variable in the parent scope if you refer to it explicitly by scope number - e.g:
$x = "some value"
function Invoke-MyFunction
{
# "-Scope 1" means "immediate parent scope"
Set-Variable x -Value "another value" -Scope 1
}
Invoke-MyFunction
write-host $x
In your case though, you might want to wrap your logging logic into separate functions rather than litter your code with lots of Set-Variable x -Value ($x + "another log line") -Scope 1 (which is an implementation detail of your logging approach).
Your current approach will degrade performance over time by creating a new (and increasingly long) string object every time you add some logging output, and it will also make it really hard to change your logging mechanism at a later date (e.g. what if you decide want to log to a file instead?) as you'll need to go back to every logging line and rewrite it to use the new mechanism.
What you could do instead is something like this:
Logging Code
$script:LogEntries = new-object System.Collections.ArrayList;
function Write-LogEntry
{
param( [string] $Value )
$null = $script:LogEntries.Add($Value)
}
function Get-LogEntries
{
return $script:LogEntries
}
Application Code
function Invoke-MyFunction
{
Write-LogEntry -Value "another logging line"
}
and then your logging mechanism is abstracted away from your main application, and your application just has to call Write-LogEntry.
Note that $script:<varname> is another way of referencing variables in the containing script's root scope using Scope Modifiers - the link describes some other options including global, local and private.

PowerShell classes with single-argument constructors do not validate data type

I'm designing a module and using classes to type-validate my parameters. I noticed that, when attempting to type-validate input parameters, a class with a single-argument constructor appears to act as a type accelerator instead of validating data type.
Example:
Class stack {
$a
$b
stack($inp) {
$this.a = $inp
$this.b = 'anything'
}
}
function foo {
Param(
[stack]$bar
)
$bar
}
PS>foo -bar 'hello'
a b
- -
hello anything
$bar has been type accelerated into an instantiation of stack.
Compare this to the same class with a constructor that takes 2 arguments:
Class stack {
$a
$b
stack($inp,$inp2) {
$this.a = $inp
$this.b = 'anything'
}
}
function foo {
Param(
[stack]$bar
)
$bar
}
PS>foo -bar 'hello'
foo : Cannot process argument transformation on parameter 'bar'. Cannot convert the "hello" value of type "System.String" to type "stack".
Now the class type is correctly validating the input parameter.
I first saw this in PS5.1 on Windows 10, but I just tried it on my private laptop with pwsh 7.2.1 and seems to be the same.
Is there a workaround to this behavior? Is it a bug?
Edit: Well, after further testing, I realized this also happens if I supply 2 input parameters for the constructor with 2 arguments, e.g., foo -bar 'hello' 'world'. So I guess it's probably intended, and I'm doing something wrong. Can I use classes to validate my data types for input parameters? How?
What you're seeing is unrelated to type accelerators, which are simply short alias names for .NET type names; e.g., [regex] is short for [System.Text.RegularExpressions.Regex].
Instead, you're seeing PowerShell's flexible automatic type conversions, which include translating casts (e.g. [stack] ...) and type constraints (ditto, in the context of an assignment or inside a param(...) block) into constructor calls or ::Parse() calls, as explained in this answer.
Therefore, given that your [stack] class has a (non-type-constrained) single-argument constructor, something like [stack] 'hello' is automatically translated into [stack]::new('hello'), i.e. a constructor call - and that is also what happens when you pass argument 'hello' to a parameter whose type is [stack].
I suggest not fighting these automatic conversions, as they are usually helpful.
In the rare event that you do need to ensure that the type of the argument passed is exactly of the type specified in the parameter declaration (or of a derived type), you can use the following technique (using type [datetime] as an example, whose full .NET type name is System.DateTime):
function Foo {
param(
# Ensure that whatever argument is passed is already of type [datetime]
[PSTypeName('System.DateTime')]
$Bar
)
"[$Bar]"
}
Kudos to you for discovering the [PSTypeName()] attribute for this use case.
Without the [PSTypeName(...)] attribute, a call such as Foo 1/1/1970 would work, because the string '1/1/1970' is automatically converted to [datetime] by PowerShell.
With the [PSTypeName(...)] attribute, only an actual [datetime] argument is accepted (or, for types that can be sub-classed, an instance of a type derived from the specified type).
Important: Specify the target type's full .NET type name (e.g. 'System.DateTime' rather than just 'datetime') to target it unambiguously.
However, for PowerShell custom classes, their name is the full name (they are not inside a namespace), so in the case of your [stack] class, the attribute would be [PSTypeName('stack')]
Any type name is accepted, even if it doesn't refer to an existing .NET type or custom class, and any such non-existent type would require an argument to use a matching virtual ETS (PowerShell's Extended Type System) type name. In fact, supporting such virtual type names is the primary purpose of this attribute.[1] E.g., if [PSTypeName('Bar')] were used, you could pass a custom object with an ETS type name of Bar as follows:
[pscustomobject] #{ PSTypeName = 'Bar'; Baz = 'quux' }
[1] To quote from the linked docs (emphasis added): "This attribute is used to restrict the type name of the parameter, when the type goes beyond the .NET type system."
If you really want to validate the passed type you need to actually validate, not just cast the input as a specific type.
function foo {
Param(
[ValidateScript({$_ -is [stack]})]
$bar
)
$bar
}
Doing this will not try to cast the input as a specific type, and will fail if the input type is wrong.
Your 'foo' function requires a [stack] argument, it doesn't create one.
So your call should be as follow:
foo -bar ([stack]::new('fun:foo','hello'))
I don't know exactly how you will use it but if the goal is to validate arguments, I would suggest to specify the types everywhere... here is a small example but it can be improved, just an example:
Class stack {
[string]$a
[string]$b
stack([string]$inp,[string]$inp2) {
$this.a = $inp
$this.b = $inp2
}
}
function foo {
Param([stack]$bar)
$bar
}
function foo2 {
Param([array]$bar)
[stack]::new($bar[0],$bar[1])
}
foo -bar ([stack]::new('fun:foo','hello'))
foo2 -bar 'fun:foo2','hello'
foo2 -bar #('fun:foo2','hello2')
Aha, I thought I had seen something to this effect somewhere. I luckily managed to get it working by applying the description from an article on using the PSTypeName in PSCustomObjects for type-validation to classes. It turns out that classes also work with the same syntax.
In summary, it seems one has to type [PSTypeName('stack')] to use class types to validate data types.
Class stack {
$a
$b
stack($inp) {
$this.a = $inp
$this.b = 'anything'
}
}
function foo {
Param(
[PSTypeName('stack')]$bar
)
$bar
}
PS>foo -bar 'hello'
foo : Cannot bind argument to parameter 'bar', because PSTypeNames of the argument do not match the PSTypeName required by the parameter: stack.
PS>$test = [stack]::new('Overflow')
PS>foo -bar $test
a b
- -
Overflow anything

Complex data types, byReference arguments and data not changing as expected

I have a function that can either initialize itself and return an ordered dictionary with initial values, or if a collection is provided as an argument, it manipulates that collection. And that collection is a key within a parent collection.
In my actual code I am seeing an odd behavior, where a key in the initial collection is initially $Null or has a specified value, but when I try to revise that value I do NOT get an error, but I also do not get a changed value. However, when I try creating a minimally functional example to post here, it does work correctly, both in the console and the IDE.
So, given this code
function Write-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
foreach ($key in $Collection.Keys) {
try {
$type = $Collection.$key.GetType().FullName
} catch {
$type = 'NULL'
}
Write-Host "$key $type $($Collection.$key)"
}
Write-Host
}
function Manage-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
if (-not $Collection) {
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
return $initialCollection
} else {
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
}
}
CLS
$parentContainer = New-Object System.Collections.Specialized.OrderedDictionary
$parentContainer.Add('data', (Manage-Data))
Write-Data $parentContainer.data
Manage-Data -Collection $parentContainer.data
Write-Data $parentContainer.data
is there any obvious scenario where either of the lines revising values would not throw an error, but would also not change the value? For example, if there are actually more functions doing other things with that initialized collection object before the attempt to revise data? Or perhaps more generally, since I am depending on the default byReference behavior of complex objects, is there some situation where this behavior breaks down and I am effectively modifying a new complex object when I think I am modifying the original? Or is the fact that I am having problems with a simple data type within the complex type potentially the issue?
For what it is worth, the idea here is to basically be able to use Dependency Injection, but with functions rather than classes, and also mimic to some extent the the concept of a constructor and a method in a class, but again in a function. And that has generally been working well, if a little messy, which has reenforced in my mind that I need to move to classes eventually. But this particular issue has me worried that I will see the same problem in classes, and unless I can understand it now I will have issues. But since I can't seem to recreate the issue in a simplified example, I seem to be unable to figure anything out.
It occurs to me that one thing I haven't tried is to actually get the memory address of the collection I think I am modifying, or even of the individual key, so I can verify I actually am changing the same data that I initialized. But HOW to get the memory address of a variable escapes me, and is maybe not possible in PowerShell or .NET?
"the default byReference behavior of complex objects" concerns the properties of the object not the object itself:
The difference between (if):
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
and (else)
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
Is that the later (else) statements indeed change the values of the parent values as $Collection refers to the same object as $parentContainer.data but the former (if) statement creates a new $initialCollection in the scope of the Manage-Data function which isn't visible in the parent (even if you assign it to $Collection, it would create a new object reference in the scope of the Manage-Data function).
You might return $initialCollection but then, how are you handling the different returns (either a $initialCollection or enumerable null ) in your parent function? Therefore I would just return $initialCollection for both conditions and reassign the object (where the properties are still by reference and only the $parentContainer.data reference will change/reset):
$parentContainer.data = Manage-Data -Collection $parentContainer.data
Potential problems
In other words, the potential issue in your Manage-Data function lies in the fact that parent function needs a different approach in calling it based on the condition if (-not $Collection) which is actually defined within the function. (What will be the value of this condition, as the caller already need to act differently on the condition?)
This leaves two pitfalls:
You call the function in the assumption that the argument is not a collection but it actually is:
$parentContainer = [Ordered]#{ data = [Ordered]#{} }
$parentContainer.Add('data', (Manage-Data))
In this case you get an error:
MethodInvocationException: Exception calling "Add" with "2" argument(s): "Item has already been added. Key in dictionary: 'data' Key being added: 'data'"
And the opposite (which is less obvious): you call the function in the assumption that the argument is a collection but it is actually not:
$parentContainer = [Ordered]#{}
Manage-Data $ParentContainer.Data
This will leave an unexpected object on the pipeline:
(See: PowerShell Pipeline Pollution)
Name Value
---- -----
initialNull
initialString initial string
And doesn't add anything to the $parentContainer object:
$parentContainer # doesn't return anything as it doesn't contain anything
Suggestions
See about scopes
Enable Set-StrictMode -Version Latest.
This will show you that a property is potentially empty.
Use ([ref]$MyVar).Value = 'new value' to replace a value in a parent scope.
(not related to the question) Use the IDictionary interface: [Collections.IDictionary]$Collection to accept a more general collection type in your functions.

How do I change the value of a variable using a function in powershell?

I want to check the count of the items in a variable and change the value of the variable depending upon the count. Further, I want to use this function to validate other variables as well. Below mentioned is the code.
$c=#()
function Status($s)
{
if($s.Count -eq "0"){
$s = "Fail"
}else{
$s="success"
}
}
Status $c
Here I expected the value of $c would be "Fail". But instead the value remains to be null.
Without fiddling around with scopes or sending variables by reference, why don't you simply have the function return 'Fail' or 'Success'?
BTW The .Count property is of type Int32, so you should not surround the value 0 with quotes, making it a string.
function Get-Status($s) {
if($s.Count -eq 0) { 'Fail' } else { 'Success' }
}
Now, if you want to overwrite the variable $c with the outcome of whatever the function returns, simply do:
$c = #()
$c = Get-Status $c # will reset variable $c to the string value of 'Fail'
P.S. I renamed the function so it conforms to the Verb-Noun naming convention in PowerShell
If you want to change multiple variables in one function you need references or scopes. Scopes will change the variable with the same name inside the function and globally. Calling a variable by reference is indifferent to the variable names outside the function.
Reference:
Working with references your variable in the function needs to be of type [ref] ( or System.Management.Automation.PSReference ). In that case the argument you use must be cast to [ref] and to this before calling the function enclose the var with brackets ([ref]$c). When using references, you can't just change the variable itself, but you need to work with its .value. The value of your reference represents your original variable. ([ref]$s.Value -eq $c)
Using that your code would look like this:
$c=#()
function Status([ref]$s) #Define $s as [ref]. only arguments of type [ref] are valid
{
if($s.value.Count -eq 0)
{
$s.Value = "Fail" #This will change the type of $c from array to string
}
else
{
$s.Value += "success" #This will recreate the array with an additional object (string)
}
}
Status ([ref]$c) #The variable must be cast to [ref] to be valid
$c
Scopes:
Normally a function is executed in a lower scope than the rest of the script. That means variables only exist in their scope and in lower scopes and changes in lower scopes won't reflect to higher scopes. However, you can directly address a variable in another scope using $<scope>:var ($script:s). The downside is you work with the variable itself. The name of the variable inside the function and outside must be the same. (reading the help for scopes is highly recommended)
Here is your code with scopes:
$s=#() #var needs to have the same name
function Status #No parameter here
{
if($script:s.Count -eq "0")
{
$script:s = "Fail" #This will change the type of $s from array to string
}
else
{
$script:s += "success" #This will recreate the array with an additional object (string)
}
}
Status
$s
For a more global function, here is a “get the value of the specified variable” function.
# PowerShell
function getVar($name){
return (Get-Variable -Name $name).Value
}
The only problem with this is that if you have two variables with different scopes, the function may return the wrong variable, therefore the wrong value.
Here is a function to set a variable. It suffers from the same cons as above though.
# PowerShell
function setVar($name, $value){
Set-Variable -Name $name -Value $value
}
You can use the -Scope $scope part to help if you need to.
Happy coding!

How to change array of PSCustomObject in Powershell

I can modify PSCustomObject property which is Array like:
$a.b.c += "new item"
Is it possible to do it with function? I mean to pass $a.b.c to function and to modify it. Seems that it's not trivial (if possible even): I tried [ref] but without success. Only one way which works for me is to return new value and to assign it, but this involve $a.b.c expression on both sides of function call which makes line long. I tried also:
function AddItem {
Param(
$Obj,
$Value,
[switch]$Uniq
)
$MutObj = [System.Collections.ArrayList]$Obj
$MutObj.Add($Value) > $null
}
but this also does not work, seems that $a.b.c += x is actually change property "c" of $a.b object and I have not $a.b in function scope. Is it achievable even with function and with modification in place in its body?
I think what you are asking is why the object doesn't change in the function.
Powershell handles Function variables as Values which means it creates another variable exactly like the one passed in and changes it in scope of the function. What you want to do is make this by Reference meaning it will mess with the same object passed into the function. This is done by the type [Ref] added to the value passed into the parameter. In this example the magic is handled here Additem -Object ([ref]$JSON) -Value "TEST"
Full script
function AddItem {
Param(
$Object,
$Value,
[switch]$Uniq
)
$Object.Value += $Value
}
$JSON = '[["aaa", "bbb"], ["ccc", "ddd"]]' | ConvertFrom-Json
Additem -Object ([ref]$JSON) -Value "TEST"
"0 : $($JSON[0])"
"0 : $($JSON[1])"
"0 : $($JSON[2])"