Why i can't change object value in the list?
$pt = [Drawing.Point]::Empty
$pt.X = 777
$pt.X # Output: 777
$list = [Collections.Generic.List[Drawing.Point]]::new()
$list.Add([Drawing.Point]::Empty)
$list[0].X = 777
$list[0].X # Output: 0 (Expected 777)
$aList = [Collections.ArrayList]::new()
$aList.Add([Drawing.Point]::Empty)|Out-Null
$aList[0].X = 777
$aList[0].X # Output: 777
How I can change value in generic list?
UPD:
I have this code, that returns me a List of "Items". How should I edit this code to use the Offset()
method as it should be?
Add-Type -Ref '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()
$items[0].Location.Offset(5, 55)
$items[0].Location
Or, if I understood mklement0 correctly, there are no ways to work with the Point object and its methods, if it stored in a collection/array?
In 2022, there are such difficulties with just storing an object in a collection...
CodePudding user response:
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.
While noting this limitation isn't part of the documentation per se, it is mentioned indirectly in this Wiki entry; a more direct discussion is in GitHub docs issue #5833.
As for the technical reason for this limitation, see this comment in GitHub issue #12411; in short: accessing a value-type instance in PowerShell invariably means operating on a copy of it.
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