powershellclassshallow-copy

Workaround `#24898`: MemberwiseClone is missing after upgrade to powershell 7.5.0


I bounced into this issue: #24898: MemberwiseClone is missing after upgrade to powershell 7.5.0:

class MyClass {
     [string]$Name

     [object] CloneProblem() {
         return $this.MemberwiseClone()
    }
}

$obj = [MyClass]::new()
$obj.CloneProblem()

InvalidOperation: Line | 6 | return $this.MemberwiseClone() # Fails with "does not contai … | ~~~~~~~~~~~~~~~~~~~~~~~ | Method invocation failed because [MyClass] does not contain a method named 'MemberwiseClone'.

What would be the most concise and/or performant workaround to create a PowerShell shallow copy method alternative taking in consideration that the concerned class might have several derivatives MyClass1 : MyClass { } meaning that I don't want the class type hardcoded in the concerned method.


Solution

  • The fastest is to create the new object hardcoding all existing members.

    class MyClass {
        [string] $Name
    
        [object] CloneProblem() {
            return [MyClass]@{ Name = $this.Name }
        }
    }
    

    The most concise but less performant if the type has many members is to enumerate the properties accessing PSObject member.

    class MyClass {
        [string] $Name
    
        [object] CloneProblem() {
            $clone = [ordered]@{}
            foreach ($property in $this.PSObject.Properties) {
                $clone[$property.Name] = $property.Value
            }
    
            return [MyClass] $clone
        }
    }
    

    Alternatively, if you don't want to hardcode the type in the return statement, you could use LanguagePrimitives.ConvertTo:

    return [System.Management.Automation.LanguagePrimitives]::ConvertTo(
                $clone, $this.GetType())
    

    Yet another less performant method is to invoke MemberwiseClone via reflection, ideally the MethodInfo should be cached in a static field.

    class MyClass {
        [string] $Name
        hidden static [System.Reflection.MethodInfo] $s_method
    
        [object] CloneProblem() {
            if (-not $this::s_method) {
                $this::s_method = [object].GetMethod(
                    'MemberwiseClone',
                    [System.Reflection.BindingFlags] 'NonPublic, Instance')
            }
    
            return $this::s_method.Invoke($this, $null)
        }
    }
    

    A follow-up on the previous approach, probably overkilling it as the previous approach should be sufficient in every possible case, can be storing Func<> delegates stored in a static dictionary for the base and derived classes.

    using namespace System.Collections.Generic
    using namespace System.Linq.Expressions
    using namespace System.Reflection
    
    class BaseClass {
        [int] $Age
        hidden static [Dictionary[type, Delegate]] $s_cloneDelegates
    
        [object] CloneProblem() {
            if (-not $this::s_cloneDelegates) {
                $this::s_cloneDelegates = [Dictionary[type, Delegate]]::new()
            }
    
            $type = $this.GetType()
            if (-not $this::s_cloneDelegates.ContainsKey($type)) {
                $this::s_cloneDelegates[$type] = [Delegate]::CreateDelegate(
                    [Expression]::GetFuncType($type, [object]),
                    [object].GetMethod(
                        'MemberwiseClone',
                        [BindingFlags] 'NonPublic, Instance'))
            }
    
            return $this::s_cloneDelegates[$type].Invoke($this)
        }
    }
    
    class MyClass : BaseClass {
        [string] $Name
    }
    
    $base = [BaseClass]::new()
    $base.CloneProblem().GetType()    # BaseClass
    $derived = [MyClass]::new()
    $derived.CloneProblem().GetType() # MyClass