You're seeing a limitation in PowerShell:
In-place updating of .NET value types accessed either (a) as a type's property or field or (b) by index of a strongly typed collection isn't supported.
[Drawing.Point].IsValueType
indicates that the type at hand is a value type.
Unfortunately, the non-support is silent: a temporary, transient copy of the property/field value or collection element is modified, leaving the original unmodified, which is what you saw.
A mitigating factor is that mutable .NET value types are rare and, in fact, even officially recommended against, and that PowerShell constructs [object[]]
arrays by default, in which value-type instances are boxed, i.e. wrapped in a helper [object]
instance that is itself a .NET reference type and therefore avoids the problem.
Thus, the workaround for collections: use an [object]
-typed/-storing collection rather than a strongly typed one:
Use a PowerShell array constructed with ,
Use System.Collections.ArrayList
, which stores its elements as [object]
instances (which, as your code shows, also avoids the problem.
As Theo notes, [object]
-typing your generic list ([System.Collections.Generic.List[object]]::new()
) is an effective workaround too.
The following sample code shows what does and doesn't work:
# These do NOT work as expected, due to being strongly typed, and the
# type being a mutable *value type*.
[Drawing.Point[]] @([Drawing.Point]::Empty),
[Collections.Generic.List[Drawing.Point]] @([Drawing.Point]::Empty),
# These DO work as expected, thanks to boxing.
[object[]] @([Drawing.Point]::Empty), # Note: @(...) implicitly creates [object[]]
[Collections.ArrayList] @([Drawing.Point]::Empty),
[Collections.Generic.List[object]] @([Drawing.Point]::Empty) |
ForEach-Object {
$_[0].X = 42 # Try to update the 1st element in place.
$_[0].X # Output the potentially updated value.
}
Output:
0 # !! Assignment was in effect ignored.
0 # !! "
42
42
42
The workaround for types that expose mutable value types as properties or fields as well as if working with a strongly typed collection can't be avoided:
As implied by zett42's comment, the workaround is to obtain a copy of the element or property / field value, modify it, and then replace the original element or property / field value with the modified copy; using a strongly typed collection as an example:
# Create a strongly typed collection with a mutable value type.
$arr = [Drawing.Point[]] @([Drawing.Point]::Empty)
# Get a copy of the element, modify it, replace the original
# element with the copy.
$element0Copy = $arr[0]; $element0Copy.X = 42; $arr[0] = $element0Copy
$arr[0].X # -> 42
The same goes for self-mutating methods such as .Offset()
:
$arr = [Drawing.Point[]] @([Drawing.Point]::Empty)
# Get a copy of the element, modify it, replace the original
# element with the copy.
$element0Copy = $arr[0]; $element0Copy.Offset(42, 43); $arr[0] = $element0Copy
$arr[0] # -> X = 42, Y = 43
Your updated example requires two workarounds, due to combining a value-type collection element (in a strongly typed [Item]
list) with a value type that exposes a different value type (a [System.Drawing.Point]
instance) as a field:
Add-Type -ReferencedAssemblies System.Drawing '
using System;
using System.Drawing;
using System.Collections.Generic;
public class MyClass {
public struct Item {
public string Name;
public Point Location;
}
public static List<Item> result = new List<Item> { };
public static List<Item> foo() {
result.Add(new Item
{
Name = "One",
Location = new Point(12, 15),
});
result.Add(new Item
{
Name = "Two",
Location = new Point(17, 22),
});
if(result[0].Location.X == 12){Console.WriteLine("ok");}
return result;
}
}'
$items = [MyClass]::foo()
# Get a copy of the [Item] element at index 0
$itemCopy = $items[0]
# Get a copy of the Location field value (of type [Point])...
$locationCopy = $itemCopy.Location
# ... and update it in place.
$locationCopy.Offset(5 ,55)
# Assign the modified [Point] back to the Location field.
$itemCopy.Location = $locationCopy
# Replace the first element with the modified [Item] copy.
$items[0] = $itemCopy
$items[0].Location # -> X = 17, Y = 70