在PowerShell中调用通用静态方法

26

如何在Powershell中调用自定义类的通用静态方法?

给定以下类:

public class Sample
{
    public static string MyMethod<T>( string anArgument )
    {
        return string.Format( "Generic type is {0} with argument {1}", typeof(T), anArgument );
    }
}

这将被编译成一个程序集 'Classes.dll' 并像这样加载到 PowerShell 中:

Add-Type -Path "Classes.dll"

什么是调用MyMethod方法的最简单方式?
5个回答

20

如@Athari所说,调用MyMethod最简单的方法是使用MakeGenericMethod。虽然他实际上没有展示如何实现,但这里有一个经过验证的可工作代码示例:

$obj = New-Object Sample

$obj.GetType().GetMethod("MyMethod").MakeGenericMethod([String]).Invoke($obj, "Test Message")
$obj.GetType().GetMethod("MyMethod").MakeGenericMethod([Double]).Invoke($obj, "Test Message")

有输出

Generic type is System.String with argument Test Message
Generic type is System.Double with argument Test Message

FYI:在 PS 7.3+ 中,您可以执行[Sample]::MyMethod[string]("测试消息"); - Granger

15
您可以调用通用方法,请参阅文章在PowerShell中调用非泛型类的通用方法。使用MakeGenericMethod函数并不容易,特别是当该方法具有重载时更难处理。以下是来自该文章的复制粘贴代码:
## Invoke-GenericMethod.ps1 
## Invoke a generic method on a non-generic type: 
## 
## Usage: 
## 
##   ## Load the DLL that contains our class
##   [Reflection.Assembly]::LoadFile("c:\temp\GenericClass.dll")
##
##   ## Invoke a generic method on a non-generic instance
##   $nonGenericClass = New-Object NonGenericClass
##   Invoke-GenericMethod $nonGenericClass GenericMethod String "How are you?"
##
##   ## Including one with multiple arguments
##   Invoke-GenericMethod $nonGenericClass GenericMethod String ("How are you?",5)
##
##   ## Ivoke a generic static method on a type
##   Invoke-GenericMethod ([NonGenericClass]) GenericStaticMethod String "How are you?"
## 

param(
    $instance = $(throw "Please provide an instance on which to invoke the generic method"),
    [string] $methodName = $(throw "Please provide a method name to invoke"),
    [string[]] $typeParameters = $(throw "Please specify the type parameters"),
    [object[]] $methodParameters = $(throw "Please specify the method parameters")
    ) 

## Determine if the types in $set1 match the types in $set2, replacing generic
## parameters in $set1 with the types in $genericTypes
function ParameterTypesMatch([type[]] $set1, [type[]] $set2, [type[]] $genericTypes)
{
    $typeReplacementIndex = 0
    $currentTypeIndex = 0

    ## Exit if the set lengths are different
    if($set1.Count -ne $set2.Count)
    {
        return $false
    }

    ## Go through each of the types in the first set
    foreach($type in $set1)
    {
        ## If it is a generic parameter, then replace it with a type from
        ## the $genericTypes list
        if($type.IsGenericParameter)
        {
            $type = $genericTypes[$typeReplacementIndex]
            $typeReplacementIndex++
        }

        ## Check that the current type (i.e.: the original type, or replacement
        ## generic type) matches the type from $set2
        if($type -ne $set2[$currentTypeIndex])
        {
            return $false
        }
        $currentTypeIndex++
    }

    return $true
}

## Convert the type parameters into actual types
[type[]] $typedParameters = $typeParameters

## Determine the type that we will call the generic method on. Initially, assume
## that it is actually a type itself.
$type = $instance

## If it is not, then it is a real object, and we can call its GetType() method
if($instance -isnot "Type")
{
    $type = $instance.GetType()
}

## Search for the method that:
##    - has the same name
##    - is public
##    - is a generic method
##    - has the same parameter types
foreach($method in $type.GetMethods())
{
    # Write-Host $method.Name
    if(($method.Name -eq $methodName) -and
    ($method.IsPublic) -and
    ($method.IsGenericMethod))
    {
        $parameterTypes = @($method.GetParameters() | % { $_.ParameterType })
        $methodParameterTypes = @($methodParameters | % { $_.GetType() })
        if(ParameterTypesMatch $parameterTypes $methodParameterTypes $typedParameters)
        {
            ## Create a closed representation of it
            $newMethod = $method.MakeGenericMethod($typedParameters)

            ## Invoke the method
            $newMethod.Invoke($instance, $methodParameters)

            return
        }
    }
}

## Return an error if we couldn't find that method
throw "Could not find method $methodName"

2
抱歉,我坚持我的说法 - “无法直接在PowerShell中完成”。:-) 非常棒的解决方法...但实际上,PowerShell团队需要修复这个问题。 - Keith Hill
同意Keith的观点,内置对此的支持会很好,但由于这是一个解决方案(即使不是直接的),所以这个答案被选中。 - David Gardiner
不需要冗长的代码示例来回答OP,MakeGenericMethod就足够了。 - JohnC

6
这是PowerShell的限制,无法在PowerShell V1或V2中直接完成,据我所知。顺便说一下,你的通用方法并不真正通用。应该是:
public static string MyMethod<T>(T anArgument)
{ 
   return string.Format( "Generic type is {0} with argument {1}", 
                         typeof(T), anArgument.ToString()); 
} 

如果您拥有这段代码,并希望从PowerShell中使用它,请避免使用通用方法或编写一个非通用的C#包装方法。

2
关于这个方法你是对的。我为了问题而简化了它,可能有点过头了 :-) - David Gardiner

3
好消息是,PowerShell v3 在绑定通用方法(并具体化它们?)方面更加出色,你通常不需要做任何特殊的事情,只需像调用普通方法一样调用它即可。我无法指定所有现在适用于此的标准,但根据我的经验,在带有通用参数的某些情况下,即使在 PowerShell v4 中仍然需要解决问题(也许是存在或重载或类似的东西)。
同样地,我有时也难以将通用参数传递给方法...例如传递一个 Func 参数。
对我来说,与 MakeGenericMethod 或其他方法相比,一个解决方法要简单得多,那就是直接在脚本中放置快速的 C# 包装类,并让 C# 解决所有通用映射...
以下是此方法的示例,它包装了 Enumerable.Zip 方法。在此示例中,我的 C# 类根本不是通用的,但这并非严格必要。
Add-Type @'
using System.Linq;
public class Zipper
{
    public static object[] Zip(object[] first, object[] second)
    {
        return first.Zip(second, (f,s) => new { f , s}).ToArray();
    }
}
'@
$a = 1..4;
[string[]]$b = "a","b","c","d";
[Zipper]::Zip($a, $b);

这将产生以下结果:
 f s
 - -
 1 a
 2 b
 3 c
 4 d

我相信有更好的PowerShell方法来“压缩”两个数组,但你明白了我的意思。这里我回避了一个真正的挑战:在C#类中硬编码第三个参数到Zip,这样我就不必想办法传递Func<T1, T2, TResult>(也许也有一种PowerShell的方法可以做到这一点?)。


2

如果没有名称冲突,可以采用快速的方法:

[Sample]::"MyMethod"("arg")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接