正如你所发现的,PowerShell拒绝运行包含
class
定义并引用尚未加载的类型的脚本 - 脚本解析阶段会失败。
截至 PowerShell (Core) 7.3.4(根据本文撰写时的最新版本),即使在脚本顶部使用
using assembly
语句也无法解决这个问题,因为在你的情况下,该类型是在
PS类定义的上下文中引用的 - 这个问题
可能最终会得到修复;修复已经获得批准,但还没有人着手实施;相关工作以及其他与类相关的问题正在
GitHub issue #6652中跟踪。
适当的解决方案是创建一个脚本模块(*.psm1),其关联清单(*.psd1)声明包含所引用类型的程序集为先决条件,通过RequiredAssemblies键。
如果不能使用模块,则请参见底部的替代解决方案。
以下是一个简化的步骤指南:
按照以下方式创建测试模块tm:
创建模块文件夹
./tm
并在其中创建清单文件(
*.psd1
):
$null = New-Item -Type Directory -ErrorAction Stop ./tm
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
-RequiredAssemblies C:\path\to\WinSCPnet.dll
在模块文件夹中创建脚本模块文件(
*.psm1
):
创建文件
./tm/tm.psm1
并定义你的类;例如:
class Foo {
[string] Bar() {
return [WinSCP.Protocol].FullName
}
}
注意:在现实世界中,模块通常放置在
$env:PSMODULEPATH
定义的标准位置之一,这样就可以仅通过
名称引用模块,而无需指定(相对)路径。
使用模块:
PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol
使用
using module
语句导入模块,并且与
Import-Module
不同的是,它还使得模块中定义的
类在当前会话中可用。
由于导入模块隐式加载了WinSCP程序集,这要归功于模块清单中的
RequiredAssemblies
键,因此实例化引用该程序集类型的
Foo
类成功。
如果您需要动态确定依赖程序集的路径以加载它,甚至可以临时编译一个(在这种情况下,使用
RequiredAssemblies
清单条目不是一个选择),您应该能够使用
Justin Grote的有用答案中推荐的方法 - 即使用指向一个
*.ps1
脚本的
ScriptsToProcess
清单条目,该脚本调用
Add-Type
来在加载脚本模块(
*.psm1
)之前动态加载依赖程序集 - 但是截至本文撰写时,这实际上在PowerShell 7.3.4(当前版本)中无法正常工作:尽管依赖程序集类型的
class
在依赖程序集的
*.psm1
文件中的定义成功,但在执行带有
using module ./tm
语句的脚本第二次之前,调用者无法看到该
class
。
创建一个示例模块:
$null = New-Item -Type Directory -ErrorAction Stop ./tm
@'
Add-Type @"
namespace demo {
public class FooHelper {
}
}
"@
'@ > ./tm/loadAssemblies.ps1
@'
class Foo {
# Simply return the full name of the dependent type.
[string] Bar() {
return [demo.FooHelper].FullName
}
}
'@ > ./tm/tm.psm1
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1
现在,截至 PowerShell 7.3.4 版本,尝试使用模块的 [Foo] 类只有在调用 "using module ./tm" 两次后才能成功 - 而在单个脚本中无法实现这一点,因此目前这种方法是无效的。
PS> using module ./tm; [Foo]::new().Bar()
InvalidOperation: Unable to find type [Foo]
PS> using module ./tm; [Foo]::new().Bar()
demo.FooHelper
问题是众所周知的,事实证明,它可以追溯到2017年-请参阅GitHub问题#2962
如果您的使用情况不允许使用模块:
- 在紧急情况下,您可以使用Invoke-Expression,但请注意,为了保证稳定性并避免安全风险,通常最好避免使用Invoke-Expression[1]。
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll
Invoke-Expression @'
class Foo {
# Simply return the full name of the WinSCP type.
[string] Bar() {
return [WinSCP.Protocol].FullName
}
}
'@
[Foo]::new().Bar()
或者,可以采用“两脚本方法”:
- 主脚本加载依赖的程序集,
- 然后点源(dot-source)第二个包含依赖程序集类型的类定义的脚本。
这种方法在Takophiliac的有用答案中有示例演示。
[1] 这个案例并不担心,但是一般来说,由于Invoke-Expression
可以调用存储在字符串中的任意命令,在对不完全受您控制的字符串应用它时可能会导致执行恶意命令 - 更多信息请参见this answer。
这个警告同样适用于其他语言,比如Bash的内置eval
命令。
type
?必须使用C#吗,还是所有内容都可以用PowerShell编写?如果可以的话,我在哪里可以找到一个例子? - litclass
定义也可以创建.NET类型,但仍有一些情况需要借助C#来解决。 - mklement0