The assignment operator = “Sets the value of a variable to the specified value” as said in the reference doc . Not surprisingly, changes on a variable (in my case an array) that has been previously assigned to another variable do not affect the value of this latter one.
PS C:\> $V="a", "b", "c"
PS C:\> $A=$V
PS C:\> write-host "A before new value of V: $A"
A before new value of V: a b c
PS C:\> $V="e","f"
PS C:\> write-host "A after new value of V: $A"
A after new value of V: a b c
PS C:\>
But when the method clear() is used the behaviour seems different.
PS C:\> $V="a", "b", "c"
PS C:\> $A=$V
PS C:\> write-host "A before clearing V: $A"
A before clearing V: a b c
PS C:\> $V.clear()
PS C:\> write-host "A after clearing V: $A"
A after clearing V:
PS C:\>
It seems that clear() method applied on $V acts also on $A. As if the assignment were by reference, strange enough, only for this method. In fact, if a new value is assigned to $V after having cleared it, $A is still affected only by the clear.
PS C:\> $V="a", "b", "c"
PS C:\> $A=$V
PS C:\> write-host "A before clearing V: $A"
A before clearing V: a b c
PS C:\> $V.clear()
PS C:\> $V="e","f"
PS C:\> write-host "A after clearing V: $A"
A after clearing V:
PS C:\>
There are possibilities to avoid this effect, although not precisely identical: $A=$V.clone() or use of cmdlet Clear-Variable -name V or $V=$null instead $V.clear() or perhaps others better than these that somebody could suggest.
But my question is:
how to explain the "propagation" of the effect of clear method on $V to the other array $A?
Tests have been done on PS ver.5.1.
Use Clone() method to get true copy of an array.
$V = "a", "b", "c"
$A = $V.Clone()
Write-Host "A before new value of V: $A"
$V = "e","f"
Write-Host "A after new value of V: $A"
A before new value of V: a b c
A after new value of V: a b c
For explanation, read Copying Arrays and Hash Tables:
Copying arrays or hash tables from one variable to another works, but
may produce unexpected results. The reason is that arrays and hash
tables are not stored directly in variables, which always store only a
single value. When you work with arrays and hash tables, you are
dealing with a reference to the array or hash table. So, if you
copy the contents of a variable to another, only the reference will be
copied, not the array or the hash table. That could result in the
following unexpected behavior:
$array1 = 1,2,3
$array2 = $array1
$array2[0] = 99
$array1[0]
99
Although the contents of $array2 were changed in this example, this
affects $array1 as well, because they are both identical. The
variables $array1 and $array2 internally reference the same
storage area. Therefore, you have to create a copy if you want to
copy arrays or hash tables,:
$array1 = 1,2,3
$array2 = $array1.Clone()
$array2[0] = 99
$array1[0]
1
Whenever you add new elements to an array (or a hash table) or remove
existing ones, a copy action takes place automatically in the
background and its results are stored in a new array or hash table.
The following example clearly shows the consequences:
# Create array and store pointer to array in $array2:
$array1 = 1,2,3
$array2 = $array1
# Assign a new element to $array2. A new array is created in the process and stored in $array2:
$array2 += 4
$array2[0]=99
# $array1 continues to point to the old array:
$array1[0]
1
BTW, you can meet the terms Value type, Reference type and Pointer type more often…
For me what you see is normal, I try to explain myself :
$V="a", "b", "c"
$a = $V
now $a contains the same reference as $V. if you clear $V then $a is cleared.
now if you write $V = "b","c", you affect $V with the reference of a new tab. and this reference is not the same that you affect to $a. so now if you clear $V, $a is not cleared.
Am I clear enought ?
Related
When working with an array of values, indexof can be used to find the position of the value in the array.
#this returns '1', correctly identifying 'blue' in position '1' of the array
$valueArray = #('cup','blue','orange','bicycle')
[array]::indexof($valueArray,'blue')
I would like to use this command to find the position of a file (image) in an array of objects generated with Get-ChildItem, however the returned position is always '-1' no matter where the object I have called for actually is. Note that image123.jpg is in the middle of the array.
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray,'image123.jpg')
I have noticed that if I change the array to filenames only, it works returning the actual position of the filename.
$imageArray = Get-ChildItem "C:\Images" | select -expand Name
[array]::indexof($imagesToReview,'image123.jpg')
Is this just the nature of using indexof or is there a way to find the correct position of the image file in the array without converting?
The easiest solution here is the following:
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray.Name,'image123.jpg')
Explanation:
[array]::IndexOf(array array,System.Object value) searches an array object for an object value. If no match is found, it returns the array lower bound minus 1. Since the array's first index is 0, then it returns the result of 0-1.
Get-ChildItem -Path SomePath returns an array of DirectoryInfo and FileInfo objects. Each of those objects has various properties and values. Just using $imageArray to compare to image123.jpg would be comparing a System.IO.FileInfo object to a String object. PowerShell won't automatically convert a FileInfo object into a string while correctly parsing to find your target value.
When you choose to select a property value of each object in the array, you are returning an array of those property values only. Using $imageArray | Select -Expand Name and $imageArray.Name return an array of Name property values. Name contains a string in your example. This means you are comparing a String to a String when using [array]::IndexOf($imageArray.Name,'image123.jpg').
The way that .NET by default compares things is just not as forgiving as PowerShell is!
[array]::IndexOf($array, $reference) will go through the array and return the current index when it encounters an item for which the following is true:
$item.Equals($reference)
... which is NOT necessarily the same as doing
$item -eq $reference
For simple values, like numbers and dates and so on, Equals() works exactly like -eq:
PS C:\> $a = 1
PS C:\> $b = 1
PS C:\> $a.Equals($b) # $true
... which is the reason your first example works as expected!
For more complex objects though, Equals() works a bit differently. Both values MUST refer to the same object, it's not enough that they have similar or even identical values:
PS C:\> $a = New-Object object
PS C:\> $b = New-Object object
PS C:\> $a.Equals($b) # $false
In the example above, $a and $b are similar (if not identical) - they're both empty objects - but they are not the same object.
Similarly, if we test with your input values, they aren't the same either:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $a.Equals($b) # $false
One of the reasons they can't be considered the same, as AdminOfThings excellently explains, is type mismatch - but PowerShell's comparison operators can help us here!
You'll notice that this works:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $b -eq $a
True
That's because the behavior of -eq depends on the left-hand operand. In the example above, "C:\" is a string, so PowerShell converts $a to a string, and all of a sudden the comparison is more like "C:\".Equals("C:\")!
With this in mind, you could create your own Find-IndexOf function to do $reference -eq $item (or any other comparison mechanism you'd like) with a simple for() loop:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value
)
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
return -1
}
Now you'd be able to do:
PS C:\> $array = #('','PowerShell is case-insensitive by default')
PS C:\> $value = 'POWERsheLL iS cASe-InSenSItIVe BY deFAuLt'
PS C:\> Find-IndexOf -Array $array -Value $value
1
Or:
PS C:\> $array = Get-ChildItem C:\images
PS C:\> $value = 'C:\images\image123.png'
PS C:\> Find-IndexOf -Array $array -Value $value
5
Adding comparison against a specific property on each of the array items (like the file's Name in your example), we end up with something like this:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value,
[string]$Property
)
if($Property){
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx].$Property){
return $idx
}
}
}
else {
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
}
return -1
}
Find-IndexOf -Array #(Get-ChildItem C:\images) -Value image123.png -Property Name
So lets say my variable $a is an array containing "1" and "2" as string.
$a = "1", "2"
Now I want to use foreach through a pipeline to subtract 1 from each value, so I'd do something like
$a | foreach{$_ = [int]$_ - 1}
but this seems do nothing, yet produces no error. So $a still contains "1" and "2". I struggle to understand where I went wrong... It's possible if i don't have an array, so this works:
$b = "3"; $b - 2
And it will return 1. So I also tried without "[int]" but it still fails, so I'm guessing it either has to do with the pipeline or my foreach but I wouldn't know why it does that.
Any suggestions?
Your foreach isn't mutating the items in your original array like you think it is - you're assigning the calculated value to the context variable $_, not updating the array index.
You can either create a new array with the calculated values as follows:
$a = $a | foreach { [int]$_ - 1 }
or mutate the items in the original array in-place:
for( $i = 0; $i -lt $a.Length; $i++ )
{
$a[$i] = [int]$a[$i] - 1
}
Note that your second example doesn't quite do what you think either:
PS> $b = "3"; $b - 2
1
PS> $b
3
The $b - 2 part is an expression which is evaluated and echoed out to console - it doesn't change the value of $b because you haven't assigned the result of the expression back to anything.
Just add the instance variable to the last line of your loop like so:
$a = $a | foreach{$_ = [int]$_ - 1; $_}
I noticed odd behaviour using arrays in scriptblocks. The following code shows the problem:
$array = #("x", "y")
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$bad = {
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
$good = {
$array = $array.Clone()
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $good
& $bad
Executing the script will produce the following output:
Object[]
array
Object[]
array
x
y
z
String
System.Object
z
The scriptblock $bad does not work as I would expect. It converts the array to string, but it should simply add the element z to the array. If there is no element added, the array can be used as expected.
I noticed this behaviour in powershell 5.0 and 5.1 but not in the ISE. Is it a bug or can anyone explain this?
It's a scope issue. The variable on the left side of the assignment operation in the scriptblocks is defined in the local scope.
This statement
$array = $array.Clone()
clones the value of the global variable $array and assigns it to the local variable $array (same name, but different variable due to different scope). The local variable $array then contains a copy of the original array, so the next statement
$array += "z"
appends a new element to that array.
In your other scriptblock you immediately append a string to the (local) variable $array. In that context the local variable is empty, so $array += "z" has the same effect as $array = "z", leaving you with a variable containing just the string "z".
Specify the correct scope and you'll get the behavior you expect:
$array = #("x", "y")
$not_bad = {
$script:array += "z"
Write-Host "$($script:array.GetType().Name)"
Write-Host "$($script:array.GetType().BaseType)"
$script:array
}
& $not_bad
Beware, however, that this will actually modify the original array in the global/script scope (your $good example leaves the original array unchanged).
I'm not sure if I would consider this behavior a bug, but it's definitely a gotcha.
I would like to post my preferred solution which bases on Ansgars explanation:
$array = #("x", "y")
$not_bad = {
$array = $array + "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $not_bad
Important is the assignment to the local variable (or better to create a local variable) before adding further elements. A simple
$array = $array
would do, but this line may be confusing.
I have an example of a program that creates an array, and then attempts to assign the value of that array multiple times into another array as a multidimensional array.
$a =#(0,0,0)
$b = #($a,$a,$a)
$b[1][2]=2
$b
'And $a is changed too:'
$a
The output is:
PS E:\Workarea> .\what.ps1
0
0
2
0
0
2
0
0
2
And $a is changed too:
0
0
2
So in this instance, the variable is actually pointing to the original variable. This is very unexpected behavior. It's rather neat that one can do this, although I never did use unions that much in my C programming. But I'd like a way to actually just do the assignment of the value, not of the variable.
$b = #($a.clone(),$a.clone(),$a.clone())
I guess would work, but something tells me that there may be something a little more elegant than that.
Thanks for the input.
This is PowerShell 2.0 under Windows 7 64-bit.
To assign the values of $a to $b instead of the reference to $a, you can wrap the variable in $(). Anything in $() gets evaluated before using it in the command, so $($a) is equivalent to 0, 0, 0.
$a =#(0,0,0)
$b = #($($a),$($a),$($a))
$b[1][2]=2
$b
'$a is not changed.'
$a
Be careful in PowerShell ',' is not the enumerator operator, but an array aoperator. The thing you present as multidimensional array is in fact an array of array, you'll find here under the definition of a multidimensional array :
$a= new-object ‘object[,]’ 3,3
$a[0,2]=3
PS > for ($i=0;$i -lt 3;$i++)
>> {
>> for($j=0;$j -lt 3;$j++)
>> {
>> $a[$i,$j]=$i+$j
>> }
>> }
Everything work here as $b is an array of reference.
Is there a difference between these two array creation statements? So, is '#' sign optional when creating arrays?
$a = "This", "Is", "a", "cat"
$a.GetType()
$a | gm
$a = #("This", "Is", "a", "cat")
$a.GetType()
$a | gm
$a = #() # declare an empty array.
$a = #(mysingleitem) # declare an array with a single element
In other case is optional.
Is there a difference between these two array creation statements?
Though I am not 100% sure (it depends on PowerShell guts) the difference may be the following: "This", "Is", "a", "cat" creates an array. #("This", "Is", "a", "cat") creates the same array and then applies the operator #() to it (apparently redundant operation in this particular case).
Using, for example, this profiler we can see that the second expression is quite slower (14% or something), so that my guess may be correct. Ideally, PowerShell code interpretator could treat these two expressions in the same way but it probably does not.
See also the help topic (the end, about operators #() and ,)
help about_operators