2

I have three arraylists in below class. I want to keep them unique. However if there's only one item (string) in the arraylist and you use select -unique (or any other method to achieve this) it will return the string instead of a list of strings. Surrounding it with @() also doesn't work because that transforms it to an array instead of an arraylist, which I can't add stuff to.

Any suggestions that are still performant? I tried HashSets before but somehow had horrible experiences with those. See my previous post for that.. Post on hashset issue

Code below:

Class OrgUnit
{
    [String]$name
    $parents
    $children
    $members


    OrgUnit($name){
        $this.name = $name
        $this.parents = New-Object System.Collections.ArrayList
        $this.children = New-Object System.Collections.ArrayList
        $this.members = New-Object System.Collections.ArrayList
    }

    addChild($child){
        # > $null to supress output
        $tmp = $this.children.Add($child)
        $this.children = $this.children | select -Unique
    }

    addParent($parent){
        # > $null to supress output
        $tmp = $this.parents.Add($parent) 
        $this.parents = $this.parents | select -Unique
    }

    addMember($member){
        # > $null to supress output
        $tmp = $this.members.Add($member)
        $this.members = $this.members | select -Unique
    }
}
Spyral
  • 760
  • 1
  • 12
  • 33

4 Answers4

2

You're adding a new item to the array, then selecting unique items from it, and reassingning it every time you add a member. This is extremely inefficient, maybe try the following instead:

if (-not $this.parents.Contains($parent)) {
  $this.parents.Add($parent) | out-null
}

Would be much faster even with least efficient output supressing by out-null.

AdamL
  • 12,421
  • 5
  • 50
  • 74
  • I assume the `$tmp` is used to get rid of the message after the `.Add()`. – Patrick Nov 19 '19 at 11:31
  • I feel almost embarrassed not doing the last solution.. I did it differently twice before and then I started tunnelvisioning which led me to doing it the way as in the first post. But yes that approach seems to be the obvious answer. THANK YOU! :) Also the one with the hashset led me to issues as in the post I linked to. – Spyral Nov 19 '19 at 11:41
  • @Spyral: take a look [here](https://stackoverflow.com/questions/5260125/whats-the-better-cleaner-way-to-ignore-output-in-powershell). It seems there is no 100% right / wrong :-) – Patrick Nov 19 '19 at 12:25
2

Check with .Contains() if the item is already added, so you don't have to eliminate duplicates with Select-Object -Unique afterwards all the time.

if (-not $this.children.Contains($child))
{
    [System.Void]($this.children.Add($child))
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
Patrick
  • 2,128
  • 16
  • 24
  • Nicely done (+1). As an aside: a syntactically simpler output-suppression technique is to use `$null = ...` - that way you don't need the `(...)`; to wit: `$null = $this.children.Add($child)` – mklement0 Nov 19 '19 at 14:20
1

As has been pointed out, it's worth changing your approach due to its inefficiency:

Instead of blindly appending and then possibly removing the new element if it turns out to be duplicate with Select-Object -Unique, use a test to decide whether an element needs to be appended or is already present.

Patrick's helpful answer is a straightforward implementation of this optimized approach that will greatly speed up your code and should perform acceptably unless the array lists get very large.

As a side effect of this optimization - because the array lists are only ever modified in-place with .Add() - your original problem goes away.


To answer the question as asked:

Simply type-constrain your (member) variables if you want them to retain a given type even during later assignments.

That is, just as you did with $name, place the type you want the member to be constrained to the left of the member variable declarations:

[System.Collections.ArrayList] $parents
[System.Collections.ArrayList] $children
[System.Collections.ArrayList] $members

However, that will initialize these member variables to $null, which means you won't be able to just call .Add() in your .add*() methods; therefore, construct an (initially empty) instance as part of the declaration:

[System.Collections.ArrayList] $parents =  [System.Collections.ArrayList]::new()
[System.Collections.ArrayList] $children = [System.Collections.ArrayList]::new()
[System.Collections.ArrayList] $members =  [System.Collections.ArrayList]::new()

Also, you do have to use @(...) around your Select-Object -Unique pipeline; while that indeed outputs an array (type [object[]]), the type constraint causes that array to be converted to a [System.Collections.ArrayList] instance, as explained below.

The need for @(...) is somewhat surprising - see bottom section.

Notes on type constraints:

  • If you assign a value that isn't already of the type that the variable is constrained to, PowerShell attempts to convert it to that type; you can think of it as implicitly performing a cast to the constraining type on every assignment:

    • This can fail, if the assigned value simply isn't convertible; PowerShell's type conversions are generally very flexible, however.

    • In the case of collection-like types such as [System.Collections.ArrayList], any other collection-like type can be assigned, such as the [object[]] arrays returned by @(...) (PowerShell's array-subexpression operator). Note that, of necessity, this involves constructing a new [System.Collections.ArrayList] every time, which becomes, loosely speaking, a shallow clone of the input collection.

    • Pitfalls re assigning $null:

      • If the constraining type is a value type (if its .IsValueType property reports $true), assigning $null will result in the type's default value; e.g., after executing
        [int] $i = 42; $i = $null, $i isn't $null, it is 0.

      • If the constraining type is a reference type (such as [System.Collections.ArrayList]), assigning $null will truly store $null in the variable, though later attempts to assign non-null values will again result in conversion to the constraining type.

  • In essence, this is the same technique used in parameter variables, and can also be used in regular variables.

    • With regular variables (local variables in a function or script), you must also initialize the variable in order for the type constraint to work (for the variable to even be created); e.g.:
      [System.Collections.ArrayList] $alist = 1, 2

Applied to a simplified version of your code:

Class OrgUnit
{

  [string] $name
  # Type-constrain $children too, just like $name above, and initialize
  # with an (initially empty) instance.
  [System.Collections.ArrayList] $children = [System.Collections.ArrayList]::new()

  addChild($child){
    # Add a new element.
    # Note the $null = ... to suppress the output from the .Add() method.
    $null = $this.children.Add($child)

    # (As noted, this approach is inefficient.)
    # Note the required @(...) around the RHS (see notes in the last section).
    # Due to its type constraint, $this.children remains a [System.Collections.ArrayList] (a new instance is created from the
    # [object[]] array that @(...) outputs).
    $this.children = @($this.children | Select-Object -Unique)
  }

}

With the type constraint in place, the .children property now remains a [System.Collections.ArrayList]:

PS> $ou = [OrgUnit]::new(); $ou.addChild(1); $ou.children.GetType().Name
ArrayList   # Proof that $children retained its type identity.

Note: The need for @(...) - to ensure an array-valued assignment value in order to successfully convert to [System.Collections.ArrayList] - is somewhat surprising, given that the following works with the similar generic list type, [System.Collections.Generic.List[object]]:

# OK: A scalar (single-object) input results in a 1-element list.
[System.Collections.Generic.List[object]] $list = 'one'

By contrast, this does not work with [System.Collections.ArrayList]:

# !! FAILS with a scalar (single object)
# Error message: Cannot convert the "one" value of type "System.String" to type "System.Collections.ArrayList".
[System.Collections.ArrayList] $list = 'one'

# OK
# Forcing the RHS to an array ([object[]]) fixes the problem.
[System.Collections.ArrayList] $list = @('one')
mklement0
  • 382,024
  • 64
  • 607
  • 775
0

Try this one:

Add-Type -AssemblyName System.Collections

Class OrgUnit
{
    [String]$name
    $parents
    $children
    $members


    OrgUnit($name){
        $this.name = $name
        $this.parents  = [System.Collections.Generic.List[object]]::new()
        $this.children = [System.Collections.Generic.List[object]]::new()
        $this.members  = [System.Collections.Generic.List[object]]::new()
    }

    addChild($child){
        # > $null to supress output
        $tmp = $this.children.Add($child)
        $this.children = [System.Collections.Generic.List[object]]@($this.children | select -Unique)
    }

    addParent($parent){
        # > $null to supress output
        $tmp = $this.parents.Add($parent) 
        $this.parents = [System.Collections.Generic.List[object]]@($this.parents | select -Unique)
    }

    addMember($member){
        # > $null to supress output
        $tmp = $this.members.Add($member)
        $this.members = [System.Collections.Generic.List[object]]@($this.members | select -Unique)
    }
}
f6a4
  • 1,684
  • 1
  • 10
  • 13