3

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
    }
}

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

3 Comments

I assume the $tmp is used to get rid of the message after the .Add().
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: take a look here. It seems there is no 100% right / wrong :-)
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))
}

1 Comment

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)
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')

Comments

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)
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.