如何在 PowerShell v5 模块中导出 class

How to export a class in a PowerShell v5 module

我有一个模块设置,就像其他一些脚本的库一样。我不知道如何在调用 Import-Module 的脚本范围内获取 class 声明。我尝试用 -class 参数安排 Export-Module,例如 -function,但没有可用的 -class。我只需要在每个脚本中声明 class 吗?

设置:

这是 class 的样子:

Class data_block
{
    $array
    $rows
    $cols
    data_block($a, $r, $c)
    {
        $this.array = $a
        $this.rows = $r
        $this.cols = $c
    }
}

你几乎不能。根据about_Classes帮助:

Class keyword

Defines a new class. This is a true .NET Framework type. Class members are public, but only public within the module scope. You can't refer to the type name as a string (for example, New-Object doesn't work), and in this release, you can't use a type literal (for example, [MyClass]) outside the script/module file in which the class is defined.

这意味着,如果你想给自己一个 data_block 实例或使用操作那些 class 的函数,创建一个函数,比如 New-DataBlock 并使它 return 一个新的 data_block 实例,然后您可以使用它来获取 class 方法和属性(可能包括静态方法和属性)。

我解决这个问题的方法是将您的自定义 class 定义移动到一个空的 .ps1 同名文件(就像您在 Java/C# ), 然后通过点源将它加载到模块定义和你的依赖代码中。我知道这不是很好,但对我来说,这比必须在多个文件中维护相同 class 的多个定义要好...

根据 here and here,您可以通过在 PowerShell 5 中执行以下操作来使用模块中定义的 类:

using module holidays

PSA:有一个已知问题会在内存中保留 classes 的旧副本。如果您不了解它,那么使用 classes 真的很令人困惑。你可以阅读它 here.


using 容易犯错

using关键字容易出现如下各种陷阱:

  • using 语句对不在 PSModulePath 中的模块不起作用,除非您在 using 语句中指定模块的完整路径。这是相当令人惊讶的,因为尽管通过 Get-Module 可以使用模块,但 using 语句可能无法工作,具体取决于模块的加载方式。
  • using语句只能用在"script"的开头。 [scriptblock]::Create()New-Module 的组合似乎无法解决这个问题。传递给 Invoke-Expression 的字符串似乎充当一种独立脚本; using 这样的字符串开头的语句。也就是说,Invoke-Expression "using module $path" 可以成功,但是模块内容可用的范围似乎很难理解。例如,如果 Invoke-Expression "using module $path" 在 Pester 脚本块内使用,则模块内的 classes 不可用于同一个 Pester 脚本块。

以上说法基于this set of tests.

ScriptsToProcess 阻止访问私有模块函数

在模块清单的 ScriptsToProcess 引用的脚本中定义 class 乍一看似乎是从模块中导出 class。但是,它没有导出 class,而是 "creates the class in the global SessionState instead of the module's, so it...can't access private functions"。据我所知,使用 ScriptsToProcess 就像按以下方式在模块外定义 class:

#  this is like defining c in class.ps1 and referring to it in ScriptsToProcess
class c {
    [string] priv () { return priv }
    [string] pub  () { return pub  }
}

# this is like defining priv and pub in module.psm1 and referring to it in RootModule
New-Module {
    function priv { 'private function' }
    function pub  { 'public function' }
    Export-ModuleMember 'pub'
} | Import-Module

[c]::new().pub()  # succeeds
[c]::new().priv() # fails

调用此结果

public function
priv : The term 'priv' is not recognized ...
+         [string] priv () { return priv } ...

模块函数 priv 无法从 class 访问,即使 priv 是从导入该模块时定义的 class 调用的。这可能是你想要的,但我还没有找到它的用途,因为我发现 class 方法通常需要访问我想保密的模块中的某些功能。

.NewBoundScriptBlock() 似乎工作可靠

调用绑定到包含 class 的模块的脚本块似乎可以可靠地导出 class 的实例,并且不会遇到 using 的陷阱。考虑这个包含 class 并已导入的模块:

New-Module 'ModuleName' { class c {$p = 'some value'} } |
    Import-Module

在绑定到模块的脚本块中调用 [c]::new() 会生成类型为 [c]:

的对象
PS C:\> $c = & (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()})
PS C:\> $c.p
some value

.NewBoundScriptBlock()

的惯用替代

似乎有一个比 .NewBoundScriptBlock() 更短的、惯用的替代方法。以下两行分别在 Get-Module:

模块输出的会话状态中调用脚本块
& (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()})
& (Get-Module 'ModuleName') {[c]::new()}}

后者的优点是,当对象写入管道时,它会将控制流交给管道中间脚本块。另一方面,.NewBoundScriptBlock() 收集写入管道的所有对象,并且仅在整个脚本块执行完成后才会产生。

我在 v5 中也遇到过关于 PowerShell classes 的多个问题。

我决定暂时使用以下解决方法,因为它与 .NET 和 PowerShell 完全兼容:

Add-Type -Language CSharp -TypeDefinition @"
namespace My.Custom.Namespace {
    public class Example
    {
        public string Name { get; set; }
        public System.Management.Automation.PSCredential Credential { get; set; }
        // ...
    }
}
"@

好处是您不需要自定义程序集来添加类型定义。您可以在 PowerShell 脚本或模块中内联添加 class 定义。

唯一的缺点是您需要创建一个新的运行时来在第一次加载后重新加载 class 定义(就像在 C#/.NET 域中加载程序集一样)。

这当然不能按预期工作。
PowerShell 5 中的想法是,您可以在扩展名为 .psm1 的单独文件中定义 class。
然后您可以使用命令加载定义(例如):

using module C:\classes\whatever\path\to\file.psm1

这必须是脚本的第一行(在注释之后)。

造成如此痛苦的原因是,即使从脚本调用 class 定义,模块也会在整个会话中加载。你可以通过运行ning看到这个:

Get-Module

您将看到您加载的文件的名称。无论您是否再次 运行 脚本,它都会 而不是 重新加载 class 定义! (它甚至不会读取psm1文件。)这导致咬牙切齿。

有时 - 有时 - 您可以 运行 在 运行 脚本之前执行此命令,这将重新加载刷新后的模块 class定义:

Remove-Module  file

其中文件是没有路径或扩展名的名称。但是,为了保持理智,我建议重新启动 PowerShell 会话。这显然很麻烦;微软需要以某种方式清理它。

我找到了一种无需“使用模块”即可加载 类 的方法。 在您的 MyModule.psd1 文件中使用以下行:

ScriptsToProcess = @('Class.ps1')

然后将您的 类 放入 Class.ps1 文件中:

class MyClass {}

更新:虽然您不必通过此方法使用“使用模块 MyModule”,但您仍然必须:

  • 运行“使用模块 MyModule”
  • 或运行“导入模块MyModule”
  • 或者调用你模块中的任何函数(这样它会自动导入你的模块)

Update2:这会将 Class 加载到当前作用域,因此如果您从函数内部导入模块,例如 Class 将无法在函数外部访问。遗憾的是,我看到的唯一可靠方法是用 C# 编写 Class 并使用 Add-Type -Language CSharp -TypeDefinition 'MyClass...'.

加载它

要在开发时更新 class 定义,select class 的代码并按 F8 到 运行 select编辑代码。它不如 Import-Module 命令上的 -Force 选项干净。

看到 using Module 没有那个选项,Remove-Module is sporadic at best, this is the best way I have found to develop a class and see the results without having to close down the PowerShell ISE 重新启动它。

using 语句是适合您的方法。否则这似乎也有效。

文件测试class.psm1

使用函数传递 class

class abc{
    $testprop = 'It Worked!'
    [int]testMethod($num){return $num * 5}
}

function new-abc(){
    return [abc]::new()
}

Export-ModuleMember -Function new-abc

文件someScript.ps1

Import-Module path\to\testclass.psm1
$testclass = new-abc
$testclass.testProp        # Returns 'It Worked!'
$testclass.testMethod(500) # Returns 2500


$testclass | gm


Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
testMethod  Method     int testMethod(System.Object num)
ToString    Method     string ToString()
testprop    Property   System.Object testprop {get;set;}