stripedneck
stripedneck

Reputation: 87

How can you correctly create and modify a hashset of tuples in powershell?

I'm trying to make a hashset of 2-element (pair) tuples in PowerShell thus:

$MySet = New-Object System.Collections.Generic.HashSet[System.Tuple]

Which seems to work:

$MySet.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     HashSet`1                                System.Object

$MySet.Add

OverloadDefinitions
-------------------
bool Add(System.Tuple item)
void ICollection[Tuple].Add(System.Tuple item)
bool ISet[Tuple].Add(System.Tuple item)

But if I create a tuple and add it to the set:

$Tuple = [System.Tuple]::Create("Test", "Hello")
$MySet.Add($Tuple)
MethodException: Cannot find an overload for "Add" and the argument count: "1".

What's the way to go about this correctly?

Bonus: There's no threadsafe version of hashset - is there a way to modify/read hashsets in a threadsafe manner then?

Upvotes: 3

Views: 1606

Answers (2)

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174485

The problem here is that [System.Tuple]::Create($a,$b) doesn't actually create an instance of [System.Tuple], but of [System.Tuple[T1,T2]] where T1 is the type of $a, and T2 is the type of $b.

Although they share part of the name, [System.Tuple[T1,T2]] is not assignable to [System.Tuple], so we'll have to find another type argument for your [HashSet].

Since your tuple item values are strings, go with [System.Tuple[string,string]]:

$set = [System.Collections.Generic.HashSet[System.Tuple[string,string]]]::new()
$tuple = [System.Tuple]::Create("Hello", "World")
$set.Add($tuple)

BONUS ANSWER

If you go in the other direction and just create a HashSet[object] for the widest possible assignability, you could create a thread-safe ConcurrentSet by wrapping the HashSet[object] methods you need to expose in a custom powershell class, and then use a ReaderWriteLockSlim to facilitate concurrent reads but exclusive writes:

using namespace System.Collections.Concurrent
using namespace System.Collections.Generic
using namespace System.Threading

# Custom IEqualityComparer based on [scriptblock]
# Use [PSComparer]::new({$args[0].Equals($args[1])}) to "restore" default comparer logic
class PSComparer : IEqualityComparer[object]
{
    [scriptblock]
    $Comparer

    PSComparer()
    {
        $this.Comparer = {$args[0] -eq $args[1]}
    }

    PSComparer([scriptblock]$comparer)
    {
        $this.Comparer = $comparer
    }

    [bool]
    Equals($a,$b)
    {
        return & $this.Comparer $a $b
    }

    [int]
    GetHashCode($obj)
    {
        if($obj -is [object]){
            return $obj.GetHashCode()
        }
        throw [System.ArgumentNullException]::new('obj')
    }
}

class ConcurrentSet : IDisposable
{
    hidden [ReaderWriterLockSlim]
    $_lock

    hidden [HashSet[object]]
    $_set

    ConcurrentSet()
    {
        # Default to PowerShell comparison logic, ie. `"1" -eq 1`
        $this.Initialize([PSComparer]::new())
    }

    ConcurrentSet([IEqualityComparer[object]]$comparer)
    {
        $this.Initialize($comparer)
    }

    hidden
    Initialize([IEqualityComparer[object]]$comparer)
    {
        $this._set = [HashSet[object]]::new($comparer)
        $this._lock = [System.Threading.ReaderWriterLockSlim]::new()
    }

    [bool]
    Add([object]$item)
    {
        $this._lock.EnterWriteLock()
        try{
            return $this._set.Add($item)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    [bool]
    Contains([object]$item)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.Contains($item)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    Remove([object]$item)
    {
        $this._lock.EnterUpgradeableReadLock()
        try{
            if($this._set.Contains($item)){
                $this._lock.EnterWriteLock()
                try {
                    return $this._set.Remove($item)
                }
                finally {
                    $this._lock.ExitWriteLock()
                }
            }

            return $false
        }
        finally{
            $this._lock.ExitUpgradeableReadLock()
        }
    }

    UnionWith([IEnumerable[object]]$other)
    {
        $this._lock.EnterWriteLock()
        try{
            $this._set.UnionWith($other)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    IntersectWith([IEnumerable[object]]$other)
    {
        $this._lock.EnterWriteLock()
        try{
            $this._set.IntersectWith($other)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    ExceptWith([IEnumerable[object]]$other)
    {
        $this._lock.EnterWriteLock()
        try{
            $this._set.ExceptWith($other)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    SymmetricExceptWith([IEnumerable[object]]$other)
    {
        $this._lock.EnterWriteLock()
        try{
            $this._set.SymmetricExceptWith($other)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    [bool]
    IsSubsetOf([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.IsSubsetOf($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    IsSupersetOf([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.IsSupersetOf($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    IsProperSubsetOf([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.IsProperSubsetOf($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    IsProperSupersetOf([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.IsProperSupersetOf($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    Overlaps([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.Overlaps($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    [bool]
    SetEquals([IEnumerable[object]]$other)
    {
        $this._lock.EnterReadLock()
        try{
            return $this._set.SetEquals($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    hidden [int]
    get_Count()
    {
        return $this._set.Count
    }

    Dispose()
    {
        if($this._lock -is [System.IDisposable])
        {
            $this._lock.Dispose()
        }
    }
}

Upvotes: 4

Theo
Theo

Reputation: 61068

Another (ugly) approach is

$MySet = [System.Collections.Generic.HashSet[[System.Tuple[string,string]]]]::new()
$Tuple = [System.Tuple[string,string]]::new("Test", "Hello")
[void]$MySet.Add($Tuple)

$MySet

Result:

Item1 Item2 Length
----- ----- ------
Test  Hello      2

Upvotes: 1

Related Questions