Home > Software engineering >  Change value in the list
Change value in the list

Time:05-26

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.

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
  • Related