PowerShell 技能连载 - AST 抽象语法树解析

适用于 PowerShell 7.0 及以上版本

在编写和维护大量 PowerShell 脚本时,你是否想过如何程序化地”理解”一段代码的结构?正则表达式只能处理文本层面的匹配,却无法识别变量的作用域、函数的调用关系或参数的传递方式。PowerShell 内置的 AST(Abstract Syntax Tree,抽象语法树)引擎正是为此而生——它将脚本源码解析为一棵结构化的对象树,每个节点都代表一个语法元素。

AST 是 PSScriptAnalyzer、PowerShell Editor Services(VS Code 的 PowerShell 扩展底层)等工具的核心技术。掌握了 AST,你就能编写自己的代码静态分析工具、自动化重构脚本,甚至构建自定义的代码质量检查规则,在 CI/CD 流水线中实现脚本质量门禁。

本文将从 AST 的基础解析入手,逐步展示代码分析实战,最后实现一个自动重构工具,帮助你把 AST 技术应用到日常开发和运维中。

AST 基础解析

PowerShell 提供了 [System.Management.Automation.Language.Parser] 类来将脚本文本解析为 AST。解析后的对象可以通过 .Find().FindAll() 等方法遍历,轻松提取函数定义、变量引用、命令调用等语法元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 定义一段示例脚本代码
$scriptCode = @'
function Get-SystemInfo {
param(
[string]$ComputerName = $env:COMPUTERNAME,
[int]$Timeout = 30
)
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName
$cpu = Get-CimInstance -ClassName Win32_Processor -ComputerName $ComputerName
$disk = Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $ComputerName

$result = [PSCustomObject]@{
Computer = $ComputerName
OS = $os.Caption
CPU = $cpu.Name
DiskFree = ($disk | Measure-Object -Property FreeSpace -Sum).Sum
}
return $result
}

$computer = "SERVER01"
$info = Get-SystemInfo -ComputerName $computer
Write-Output $info
'@

# 使用 Parser 类解析脚本
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
$scriptCode,
[ref]$tokens,
[ref]$errors
)

# 检查解析是否有错误
if ($errors.Count -gt 0) {
Write-Host "解析错误:" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " 行 $($_.Extent.StartLineNumber): $($_.Message)" }
} else {
Write-Host "脚本解析成功,AST 类型: $($ast.GetType().Name)" -ForegroundColor Green
}

# 提取所有函数定义
$functions = $ast.FindAll(
{ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] },
$true
)

Write-Host "`n=== 函数定义 ==="
foreach ($fn in $functions) {
Write-Host "函数名: $($fn.Name)"
Write-Host " 参数: $($fn.Parameters | ForEach-Object { $_.Name.VariablePath.UserPath } )"
Write-Host " 起始行: $($fn.Extent.StartLineNumber)"
Write-Host " 结束行: $($fn.Extent.EndLineNumber)"
}

# 提取所有变量引用
$variables = $ast.FindAll(
{ param($node) $node -is [System.Management.Automation.Language.VariableExpressionAst] },
$true
)

Write-Host "`n=== 变量列表(去重)==="
$uniqueVars = $variables | ForEach-Object { $_.VariablePath.UserPath } | Sort-Object -Unique
foreach ($v in $uniqueVars) {
$count = ($variables | Where-Object { $_.VariablePath.UserPath -eq $v }).Count
Write-Host " `$$v (引用 $count 次)"
}

# 提取所有命令调用
$commands = $ast.FindAll(
{ param($node) $node -is [System.Management.Automation.Language.CommandAst] },
$true
)

Write-Host "`n=== 命令调用 ==="
$uniqueCmds = $commands | ForEach-Object { $_.GetCommandName() } | Where-Object { $_ } | Sort-Object -Unique
foreach ($cmd in $uniqueCmds) {
Write-Host " $cmd"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
脚本解析成功,AST 类型: ScriptBlockAst

=== 函数定义 ===
函数名: Get-SystemInfo
参数: ComputerName Timeout
起始行: 1
结束行: 17

=== 变量列表(去重)===
$ComputerName (引用 4 次)
$Timeout (引用 1 次)
$computer (引用 1 次)
$cpu (引用 2 次)
$disk (引用 2 次)
$env:COMPUTERNAME (引用 1 次)
$info (引用 1 次)
$os (引用 2 次)
$result (引用 2 次)

=== 命令调用 ===
Get-CimInstance
Get-SystemInfo
Measure-Object
Write-Output

代码分析实战

掌握了 AST 基础操作后,我们来实现几个实用的代码分析功能:提取脚本的依赖关系、检测未使用的变量、以及分析函数复杂度。这些功能可以帮助你在代码审查时快速发现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 封装一个通用的 AST 分析函数
function Invoke-CodeAnalysis {
param(
[Parameter(Mandatory)]
[string]$ScriptPath
)

# 读取并解析脚本
$source = Get-Content -Path $ScriptPath -Raw -Encoding Utf8
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
$source, [ref]$tokens, [ref]$errors
)

$analysis = [ordered]@{}

# 1. 提取外部依赖(Import-Module、using module、#require)
$usingStatements = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.UsingStatementAst] },
$true
)
$importCommands = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.CommandAst] -and
$n.GetCommandName() -in 'Import-Module', 'Require-Module' },
$true
)

$dependencies = @()
foreach ($u in $usingStatements) {
$dependencies += "using: $($u.Name.Value ?? $u.ModuleName)"
}
foreach ($ic in $importCommands) {
$firstArg = $ic.CommandElements[1]
if ($firstArg) {
$dependencies += "module: $($firstArg.Value ?? $firstArg.Extent.Text)"
}
}
$analysis['Dependencies'] = $dependencies

# 2. 检测未使用的变量
$allVarRefs = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.VariableExpressionAst] },
$true
)

# 找出赋值语句左侧的变量(定义)
$assignments = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] },
$true
)
$assignedVars = @{}
foreach ($a in $assignments) {
if ($a.Left -is [System.Management.Automation.Language.VariableExpressionAst]) {
$varName = $a.Left.VariablePath.UserPath
$assignedVars[$varName] = $a.Left.Extent.StartLineNumber
}
}

# 对比定义和引用,找出仅赋值但未读取的变量
$unusedVars = @()
foreach ($varName in $assignedVars.Keys) {
$refCount = ($allVarRefs | Where-Object {
$_.VariablePath.UserPath -eq $varName
}).Count
# 赋值本身算一次引用,如果只有1次则说明未使用
if ($refCount -le 1 -and $varName -notin '_', 'null', 'true', 'false') {
$unusedVars += [PSCustomObject]@{
Variable = "`$$varName"
Line = $assignedVars[$varName]
}
}
}
$analysis['UnusedVariables'] = $unusedVars

# 3. 函数复杂度分析(循环和条件分支数量)
$functions = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] },
$true
)

$funcMetrics = foreach ($fn in $functions) {
$loops = $fn.Body.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.LoopStatementAst] },
$true
)
$conditions = $fn.Body.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.IfStatementAst] },
$true
)
$pipelines = $fn.Body.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.PipelineAst] },
$true
)
$cmdCount = ($fn.Body.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.CommandAst] },
$true
)).Count

# 计算行数
$lineCount = $fn.Extent.EndLineNumber - $fn.Extent.StartLineNumber + 1

# 简单复杂度评分
$complexity = $loops.Count * 2 + $conditions.Count + [math]::Floor($cmdCount / 5)
$level = switch ($complexity) {
{ $_ -le 3 } { "低" }
{ $_ -le 8 } { "中" }
{ $_ -le 15 } { "高" }
default { "极高" }
}

[PSCustomObject]@{
Function = $fn.Name
Lines = $lineCount
Loops = $loops.Count
Conditions = $conditions.Count
Commands = $cmdCount
Complexity = "$complexity ($level)"
}
}
$analysis['FunctionMetrics'] = $funcMetrics

return [PSCustomObject]$analysis
}

# 分析示例脚本
$testScript = Join-Path $env:TEMP "sample-analysis.ps1"
@'
function Get-DiskReport {
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3"
$report = foreach ($disk in $disks) {
$freePercent = [math]::Round($disk.FreeSpace / $disk.Size * 100, 1)
if ($freePercent -lt 10) {
$status = "Critical"
} elseif ($freePercent -lt 25) {
$status = "Warning"
} else {
$status = "OK"
}
[PSCustomObject]@{
Drive = $disk.DeviceID
FreePct = $freePercent
Status = $status
}
}
return $report
}

$unusedVar = "这个变量只赋值不使用"
$result = Get-DiskReport
$result | Format-Table
'@ | Set-Content -Path $testScript -Encoding Utf8

$report = Invoke-CodeAnalysis -ScriptPath $testScript

Write-Host "=== 依赖关系 ===" -ForegroundColor Cyan
$report.Dependencies | ForEach-Object { Write-Host " $_" }
if ($report.Dependencies.Count -eq 0) {
Write-Host " (无外部依赖)"
}

Write-Host "`n=== 未使用的变量 ===" -ForegroundColor Cyan
$report.UnusedVariables | Format-Table -AutoSize

Write-Host "`n=== 函数复杂度 ===" -ForegroundColor Cyan
$report.FunctionMetrics | Format-Table -AutoSize

Remove-Item -Path $testScript -Force

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
=== 依赖关系 ===
(无外部依赖)

=== 未使用的变量 ===
Variable Line
-------- ----
$unusedVar 20

=== 函数复杂度 ===

Function Lines Loops Conditions Commands Complexity
-------- ----- ----- ---------- -------- ----------
Get-DiskReport 17 1 2 2 4 (中)

自动重构工具

AST 不仅能用来分析代码,还可以用来生成和修改代码。下面实现一个实用的重构工具:批量重命名变量、提取代码片段为独立函数,以及从函数定义自动生成帮助文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# 基于 AST 的自动重构工具
function Invoke-CodeRefactor {
param(
[Parameter(Mandatory)]
[string]$ScriptPath,

[Parameter(Mandatory)]
[ValidateSet('RenameVariable', 'GenerateHelp', 'ExtractMetrics')]
[string]$Operation,

[string]$OldName,
[string]$NewName
)

$source = Get-Content -Path $ScriptPath -Raw -Encoding Utf8
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
$source, [ref]$tokens, [ref]$errors
)

switch ($Operation) {
'RenameVariable' {
if (-not $OldName -or -not $NewName) {
throw "RenameVariable 操作需要指定 OldName 和 NewName"
}
# 找到所有匹配的变量 token
$varTokens = $tokens | Where-Object {
$_.Kind -eq 'Variable' -and
$_.Text -match "^\$$([regex]::Escape($OldName))$"
}

if ($varTokens.Count -eq 0) {
Write-Host "未找到变量 `$$OldName" -ForegroundColor Yellow
return
}

# 从后往前替换,避免偏移量错乱
$sb = [System.Text.StringBuilder]::new($source)
$replaced = 0
foreach ($vt in ($varTokens | Sort-Object { $_.Extent.StartOffset } -Descending)) {
$sb.Remove($vt.Extent.StartOffset, $vt.Extent.EndOffset - $vt.Extent.StartOffset) | Out-Null
$sb.Insert($vt.Extent.StartOffset, "`$$NewName") | Out-Null
$replaced++
}

$newSource = $sb.ToString()
$backupPath = "$ScriptPath.bak"
Copy-Item -Path $ScriptPath -Destination $backupPath -Force
$newSource | Set-Content -Path $ScriptPath -Encoding Utf8 -NoNewline

Write-Host "已将 `$$OldName 重命名为 `$$NewName (共 $replaced 处)" -ForegroundColor Green
Write-Host "备份文件: $backupPath"
}

'GenerateHelp' {
# 提取所有函数,自动生成基于 AST 的帮助文档
$functions = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] },
$true
)

foreach ($fn in $functions) {
$helpBuilder = [System.Text.StringBuilder]::new()
$null = $helpBuilder.AppendLine("<#")
$null = $helpBuilder.AppendLine(".SYNOPSIS")
$null = $helpBuilder.AppendLine(" $($fn.Name) 函数")
$null = $helpBuilder.AppendLine("")
$null = $helpBuilder.AppendLine(".DESCRIPTION")
$null = $helpBuilder.AppendLine(" 自动生成的帮助文档(基于 AST 解析)")
$null = $helpBuilder.AppendLine("")

if ($fn.Parameters) {
$null = $helpBuilder.AppendLine(".PARAMETERS")
foreach ($param in $fn.Parameters) {
$paramName = $param.Name.VariablePath.UserPath
$paramType = if ($param.Attributes.TypeName) {
$param.Attributes.TypeName.Name
} else {
"object"
}
$defaultValue = if ($param.DefaultValue) {
$param.DefaultValue.Extent.Text
} else {
"(必需)"
}
$null = $helpBuilder.AppendLine(" -$paramName [$paramType] 默认值: $defaultValue")
}
$null = $helpBuilder.AppendLine("")
}

# 统计函数体中的命令
$bodyCmds = $fn.Body.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.CommandAst] },
$true
) | ForEach-Object { $_.GetCommandName() } | Where-Object { $_ }

$null = $helpBuilder.AppendLine(".NOTES")
$null = $helpBuilder.AppendLine(" 函数行数: $($fn.Extent.EndLineNumber - $fn.Extent.StartLineNumber + 1)")
$null = $helpBuilder.AppendLine(" 调用命令: $($bodyCmds -join ', ')")
$null = $helpBuilder.AppendLine("#>")

Write-Host "=== $($fn.Name) 的帮助文档 ===" -ForegroundColor Cyan
Write-Host $helpBuilder.ToString()
}
}

'ExtractMetrics' {
# 提取脚本的整体度量信息
$totalLines = ($source -split "`n").Count
$codeLines = ($source -split "`n" | Where-Object {
$_.Trim() -and $_.Trim() -notmatch '^\s*#' -and $_.Trim() -notmatch '^\s*<#' -and $_.Trim() -notmatch '^\s*#>'
}).Count
$commentLines = $totalLines - $codeLines

$functions = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] },
$true
)
$allCommands = $ast.FindAll(
{ param($n) $n -is [System.Management.Automation.Language.CommandAst] },
$true
)
$uniqueCommands = $allCommands | ForEach-Object { $_.GetCommandName() } |
Where-Object { $_ } | Sort-Object -Unique

$metrics = [PSCustomObject]@{
FilePath = $ScriptPath
TotalLines = $totalLines
CodeLines = $codeLines
CommentLines = $commentLines
Functions = $functions.Count
CommandCount = $allCommands.Count
UniqueCommands = $uniqueCommands.Count
CommandList = $uniqueCommands -join ", "
ParseErrors = $errors.Count
}

Write-Host "=== 脚本度量 ===" -ForegroundColor Cyan
$metrics | Format-List
}
}
}

# 创建测试脚本并演示重构功能
$demoScript = Join-Path $env:TEMP "refactor-demo.ps1"
@'
function Get-ServiceStatus {
param(
[string[]]$ComputerName = @("localhost"),
[string]$ServiceName = "WinRM"
)
foreach ($comp in $ComputerName) {
$svc = Get-Service -Name $ServiceName -ComputerName $comp -ErrorAction SilentlyContinue
if ($svc) {
$output = [PSCustomObject]@{
Computer = $comp
Service = $ServiceName
Status = $svc.Status
}
Write-Output $output
}
}
}

$computers = @("SERVER01", "SERVER02", "SERVER03")
Get-ServiceStatus -ComputerName $computers -ServiceName "Spooler"
'@ | Set-Content -Path $demoScript -Encoding Utf8

Write-Host "--- 演示: 变量重命名 ---" -ForegroundColor Yellow
Invoke-CodeRefactor -ScriptPath $demoScript -Operation RenameVariable -OldName "computers" -NewName "serverList"

Write-Host "`n--- 演示: 生成帮助文档 ---" -ForegroundColor Yellow
Invoke-CodeRefactor -ScriptPath $demoScript -Operation GenerateHelp

Write-Host "`n--- 演示: 提取度量信息 ---" -ForegroundColor Yellow
Invoke-CodeRefactor -ScriptPath $demoScript -Operation ExtractMetrics

# 清理
Remove-Item -Path $demoScript -Force
Remove-Item -Path "$demoScript.bak" -Force -ErrorAction SilentlyContinue

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
--- 演示: 变量重命名 ---
已将 $computers 重命名为 $serverList (共 1 处)
备份文件: /tmp/refactor-demo.ps1.bak

--- 演示: 生成帮助文档 ---
=== Get-ServiceStatus 的帮助文档 ===
<#
.SYNOPSIS
Get-ServiceStatus 函数

.DESCRIPTION
自动生成的帮助文档(基于 AST 解析)

.PARAMETERS
-ComputerName [string[]] 默认值: @("localhost")
-ServiceName [string] 默认值: "WinRM"

.NOTES
函数行数: 15
调用命令: Get-Service, Write-Output
#>

--- 演示: 提取度量信息 ---
=== 脚本度量 ===

FilePath : /tmp/refactor-demo.ps1
TotalLines : 18
CodeLines : 17
CommentLines : 1
Functions : 1
CommandCount : 3
UniqueCommands : 3
CommandList : Get-Service, Get-ServiceStatus, Write-Output
ParseErrors : 0

注意事项

  1. AST 是只读的:PowerShell 的 AST 对象模型是只读的,不能直接修改 AST 节点来改变代码。要实现代码修改(如重命名变量),需要在原始源码字符串上基于 AST 提供的位置偏移量进行文本替换,然后重新写入文件。

  2. 解析与执行分离:AST 解析仅进行语法分析,不会执行任何代码。这意味着即使脚本中包含危险操作(如 Remove-Item),解析过程也是完全安全的。但也因此无法获取运行时信息,如变量的实际值和类型推断。

  3. Token 与 AST 的区别ParseInput 同时返回 AST 树和 Token 列表。Token 是词法分析的最小单元(标识符、运算符、字符串等),而 AST 是语法层面的结构化表示。做变量重命名等精确文本替换时应使用 Token 的偏移量,做结构分析时应使用 AST 节点。

  4. 嵌套脚本的解析:对于通过 dot-sourcing. .\script.ps1)或 Invoke-Expression 动态加载的脚本,AST 无法自动追踪。如果需要分析完整的依赖链,必须递归解析所有引用的脚本文件。

  5. 性能考虑:对于超大脚本文件(数千行),FindAll() 使用递归遍历可能会产生大量对象。建议在 $predicate 回调中尽可能精确地过滤节点类型,避免不必要的全树遍历。如果只需要顶层元素,可以将第二个参数(searchNestedScriptBlocks)设为 $false

  6. 与 PSScriptAnalyzer 配合:本文演示了如何从零编写 AST 分析逻辑。在实际项目中,建议优先使用 PSScriptAnalyzer 的自定义规则框架,它封装了 AST 遍历的细节,提供了标准化的规则接口和输出格式,与 VS Code 扩展和 CI/CD 流水线的集成也更加方便。

PowerShell 技能连载 - 类定义与面向对象编程

适用于 PowerShell 5.1 及以上版本

在 PowerShell 的早期版本中,我们通常使用 PSCustomObject 或哈希表来构建自定义数据结构。虽然它们足够灵活,但缺乏类型约束、无法定义方法、也不支持继承,在构建大型自动化项目时显得力不从心。PowerShell 5.0 引入了 class 关键字,让我们可以直接在脚本中定义真正的 .NET 类型。

class 不仅仅是语法糖,它带来了完整的面向对象编程能力:类型安全的属性、可重载的构造函数、继承与多态、以及与 .NET 生态的无缝集成。你可以用 class 来建模业务实体、封装复杂逻辑、甚至实现设计模式,让脚本从”一次性工具”进化为可维护的工程化代码。

本文将从基础类定义开始,逐步深入继承与多态,最后通过一个服务器管理框架的实战案例,展示 class 在真实自动化场景中的威力。

基础类定义

下面通过一个 ServerInfo 类来演示属性、构造函数和方法的定义与使用。这个类用于封装服务器的基本信息,并提供格式化输出和状态检查方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class ServerInfo {
# 类型化的属性
[string]$Name
[string]$IPAddress
[int]$Port
[string]$Environment
[datetime]$LastChecked

# 默认构造函数
ServerInfo() {
$this.Port = 443
$this.Environment = 'Development'
$this.LastChecked = Get-Date
}

# 带参数的构造函数
ServerInfo([string]$Name, [string]$IPAddress, [int]$Port) {
$this.Name = $Name
$this.IPAddress = $IPAddress
$this.Port = $Port
$this.Environment = 'Production'
$this.LastChecked = Get-Date
}

# 方法:获取连接地址
[string] GetEndpoint() {
return "$($this.IPAddress):$($this.Port)"
}

# 方法:检查端口是否可达
[bool] TestConnection() {
try {
$tcp = [System.Net.Sockets.TcpClient]::new()
$connect = $tcp.BeginConnect($this.IPAddress, $this.Port, $null, $null)
$wait = $connect.AsyncWaitHandle.WaitOne(3000, $false)
if ($wait) {
$tcp.EndConnect($connect)
$this.LastChecked = Get-Date
return $true
}
return $false
}
finally {
$tcp.Dispose()
}
}

# 重写 ToString 方法
[string] ToString() {
return "[$($this.Environment)] $($this.Name) ($($this.GetEndpoint()))"
}
}

# 使用默认构造函数
$devServer = [ServerInfo]::new()
$devServer.Name = 'DEV-WEB-01'
$devServer.IPAddress = '192.168.1.100'
Write-Host $devServer.ToString()

# 使用带参数的构造函数
$prodServer = [ServerInfo]::new('PROD-WEB-01', '10.0.0.50', 8443)
Write-Host "服务器: $($prodServer.Name)"
Write-Host "端点: $($prodServer.GetEndpoint())"
Write-Host "环境: $($prodServer.Environment)"
Write-Host "上次检查: $($prodServer.LastChecked.ToString('yyyy-MM-dd HH:mm:ss'))"

执行结果示例:

1
2
3
4
5
[Development] DEV-WEB-01 (192.168.1.100:443)
服务器: PROD-WEB-01
端点: 10.0.0.50:8443
环境: Production
上次检查: 2026-01-05 08:30:15

继承与多态

当类之间具有层次关系时,继承可以让子类复用父类的属性和方法,同时添加或重写自己的行为。下面的例子定义了一个通用的部署任务基类,然后派生出两种具体的任务类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 基类:部署任务
class DeploymentTask {
[string]$TaskName
[string]$TargetServer
[datetime]$StartTime
[datetime]$EndTime
[string]$Status = 'Pending'

DeploymentTask([string]$TaskName, [string]$TargetServer) {
$this.TaskName = $TaskName
$this.TargetServer = $TargetServer
}

# 虚方法:执行部署(子类应重写)
[void] Execute() {
$this.StartTime = Get-Date
Write-Host "正在执行任务: $($this.TaskName)"
}

# 标记完成
[void] Complete() {
$this.EndTime = Get-Date
$this.Status = 'Completed'
$duration = ($this.EndTime - $this.StartTime).TotalSeconds
Write-Host "任务完成: $($this.TaskName) (耗时 $([math]::Round($duration, 2)) 秒)"
}

# 标记失败
[void] Fail([string]$Reason) {
$this.EndTime = Get-Date
$this.Status = "Failed: $Reason"
Write-Host "任务失败: $($this.TaskName) - $Reason"
}

# 获取摘要信息
[hashtable] GetSummary() {
return @{
TaskName = $this.TaskName
TargetServer = $this.TargetServer
Status = $this.Status
Duration = if ($this.StartTime -and $this.EndTime) {
($this.EndTime - $this.StartTime).TotalSeconds
} else { 0 }
}
}
}

# 子类:Web 应用部署
class WebAppDeployment : DeploymentTask {
[string]$AppPoolName
[string]$SiteName
[string]$PackagePath

WebAppDeployment(
[string]$TargetServer,
[string]$SiteName,
[string]$PackagePath
) : base("Deploy-WebApp-$SiteName", $TargetServer) {
$this.SiteName = $SiteName
$this.AppPoolName = "$SiteName-Pool"
$this.PackagePath = $PackagePath
}

# 重写 Execute 方法
[void] Execute() {
([DeploymentTask]$this).Execute()
Write-Host " 停止应用池: $($this.AppPoolName)"
Write-Host " 备份当前版本..."
Write-Host " 解压部署包: $($this.PackagePath)"
Write-Host " 配置站点: $($this.SiteName)"
Write-Host " 启动应用池: $($this.AppPoolName)"
$this.Complete()
}
}

# 子类:数据库迁移部署
class DatabaseMigration : DeploymentTask {
[string]$DatabaseName
[string]$MigrationScript
[bool]$BackupBeforeMigration = $true

DatabaseMigration(
[string]$TargetServer,
[string]$DatabaseName,
[string]$MigrationScript
) : base("DB-Migration-$DatabaseName", $TargetServer) {
$this.DatabaseName = $DatabaseName
$this.MigrationScript = $MigrationScript
}

# 重写 Execute 方法
[void] Execute() {
([DeploymentTask]$this).Execute()
Write-Host " 目标数据库: $($this.DatabaseName)"
if ($this.BackupBeforeMigration) {
Write-Host " 正在备份数据库..."
}
Write-Host " 执行迁移脚本: $($this.MigrationScript)"
Write-Host " 验证迁移结果..."
$this.Complete()
}
}

# 多态调用:统一执行不同类型的部署任务
$tasks = @(
[WebAppDeployment]::new('WEB-SVR-01', 'CustomerPortal', 'D:\Packages\v2.5.0.zip')
[DatabaseMigration]::new('DB-SVR-01', 'CustomerDB', 'v2.5.0_schema_update.sql')
)

foreach ($task in $tasks) {
Write-Host "`n--- 部署任务 ---"
$task.Execute()
$summary = $task.GetSummary()
Write-Host "摘要: $($summary.Status)"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--- 部署任务 ---
正在执行任务: Deploy-WebApp-CustomerPortal
停止应用池: CustomerPortal-Pool
备份当前版本...
解压部署包: D:\Packages\v2.5.0.zip
配置站点: CustomerPortal
启动应用池: CustomerPortal-Pool
任务完成: Deploy-WebApp-CustomerPortal (耗时 12.35 秒)
摘要: Completed

--- 部署任务 ---
正在执行任务: DB-Migration-CustomerDB
目标数据库: CustomerDB
正在备份数据库...
执行迁移脚本: v2.5.0_schema_update.sql
验证迁移结果...
任务完成: DB-Migration-CustomerDB (耗时 8.72 秒)
摘要: Completed

实战应用:服务器管理框架

在实际运维场景中,我们可以用 class 构建一个完整的服务器管理框架,包含数据验证、JSON 序列化和结构化错误处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 验证属性类
class ValidateRangeAttribute : System.Management.Automation.ValidateArgumentsAttribute {
[int]$MinValue
[int]$MaxValue

ValidateRangeAttribute([int]$Min, [int]$Max) {
$this.MinValue = $Min
$this.MaxValue = $Max
}

[void] Validate([object]$arguments, [System.Management.Automation.EngineIntrinsics]$engineIntrinsics) {
$val = [int]$arguments
if ($val -lt $this.MinValue -or $val -gt $this.MaxValue) {
throw "值 $val 不在允许范围 ($($this.MinValue) - $($this.MaxValue)) 内"
}
}
}

# 服务器管理基类
class ManagedServer {
[ValidateNotNullOrEmpty()][string]$HostName
[string]$IPAddress
[string]$Role
[string]$DataCenter
[bool]$IsMonitored = $false

# 静态属性:已注册的服务器列表
static [System.Collections.Generic.List[ManagedServer]]$Registry =
[System.Collections.Generic.List[ManagedServer]]::new()

ManagedServer([string]$HostName, [string]$IPAddress) {
$this.HostName = $HostName
$this.IPAddress = $IPAddress
[ManagedServer]::Registry.Add($this)
}

# 转换为 JSON
[string] ToJson() {
$ordered = [ordered]@{
hostName = $this.HostName
ipAddress = $this.IPAddress
role = $this.Role
dataCenter = $this.DataCenter
isMonitored = $this.IsMonitored
}
return $ordered | ConvertTo-Json -Depth 3
}

# 从 JSON 创建实例
static [ManagedServer] FromJson([string]$Json) {
$data = $Json | ConvertFrom-Json
$server = [ManagedServer]::new($data.hostName, $data.ipAddress)
$server.Role = $data.role
$server.DataCenter = $data.dataCenter
$server.IsMonitored = $data.isMonitored
return $server
}

# 获取所有已注册服务器
static [array] GetRegisteredServers() {
return [ManagedServer]::Registry.ToArray()
}
}

# 托管 Web 服务器
class ManagedWebServer : ManagedServer {
[string[]]$BoundUrls = @()
[int]$WorkerProcesses = 4
[string]$RuntimeVersion = '8.0'
[hashtable]$HealthMetrics = @{}

ManagedWebServer([string]$HostName, [string]$IPAddress) : base($HostName, $IPAddress) {
$this.Role = 'WebServer'
}

# 健康检查
[hashtable] CheckHealth() {
$result = @{
Server = $this.HostName
Timestamp = Get-Date -Format 'o'
Checks = @()
}

$checks = @(
@{ Name = 'CPU'; Value = (Get-Random -Min 10 -Max 95); Unit = '%' }
@{ Name = 'Memory'; Value = (Get-Random -Min 30 -Max 90); Unit = '%' }
@{ Name = 'Disk'; Value = (Get-Random -Min 20 -Max 85); Unit = '%' }
)

foreach ($check in $checks) {
$status = if ($check.Value -gt 80) { 'Warning' } else { 'OK' }
$result.Checks += @{
Name = $check.Name
Value = $check.Value
Unit = $check.Unit
Status = $status
}
}

$this.HealthMetrics = $result
$this.IsMonitored = $true
return $result
}

# 重写 ToJson 以包含扩展属性
[string] ToJson() {
$ordered = [ordered]@{
hostName = $this.HostName
ipAddress = $this.IPAddress
role = $this.Role
dataCenter = $this.DataCenter
isMonitored = $this.IsMonitored
boundUrls = $this.BoundUrls
workerProcesses = $this.WorkerProcesses
runtimeVersion = $this.RuntimeVersion
}
return $ordered | ConvertTo-Json -Depth 3
}
}

# --- 使用示例 ---

# 创建并注册服务器
$web1 = [ManagedWebServer]::new('WEB-PROD-01', '10.1.0.10')
$web1.DataCenter = 'EastAsia'
$web1.BoundUrls = @('https://portal.contoso.com', 'https://api.contoso.com')

$web2 = [ManagedWebServer]::new('WEB-PROD-02', '10.1.0.11')
$web2.DataCenter = 'EastAsia'
$web2.BoundUrls = @('https://portal.contoso.com')
$web2.WorkerProcesses = 8

Write-Host "已注册服务器数量: $([ManagedServer]::Registry.Count)"
Write-Host "`n--- $web1 健康检查 ---"
$health = $web1.CheckHealth()
foreach ($check in $health.Checks) {
Write-Host (" {0,-10} {1}{2,-5} [{3}]" -f $check.Name, $check.Value, $check.Unit, $check.Status)
}

Write-Host "`n--- JSON 序列化 ---"
Write-Host $web1.ToJson()

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
已注册服务器数量: 2

--- WEB-PROD-01 健康检查 ---
CPU 42% [OK]
Memory 67% [OK]
Disk 31% [OK]

--- JSON 序列化 ---
{
"hostName": "WEB-PROD-01",
"ipAddress": "10.1.0.10",
"role": "WebServer",
"dataCenter": "EastAsia",
"isMonitored": true,
"boundUrls": [
"https://portal.contoso.com",
"https://api.contoso.com"
],
"workerProcesses": 4,
"runtimeVersion": "8.0"
}

注意事项

  1. PowerShell 版本要求class 关键字需要 PowerShell 5.0 及以上版本。在 Windows PowerShell 5.1 中功能完整可用,PowerShell 7 进一步增强了与 .NET Core 的兼容性。如果你需要在旧版本中运行脚本,请改用 PSCustomObject 或 C# 编译的 cmdlet。

  2. 属性初始化时机:类的属性默认值在类定义时求值,不是在实例化时求值。如果需要动态默认值(如当前时间),应放在构造函数中赋值,而不是在属性声明处直接使用 Get-Date

  3. 继承的限制:PowerShell class 只支持单继承(一个父类),但可以实现多个接口。方法重写时需要使用 ([BaseClass]$this).Method() 语法调用父类方法,这与 C# 的 base.Method() 略有不同。

  4. 序列化注意事项:class 实例默认不能被 Export-Clixml 正确序列化和反序列化,反序列化后会变成 Deserialized.ClassName 对象,丢失方法。如果需要持久化,建议使用 ToJson() / FromJson() 模式。

  5. 调试与类型检查:class 中的方法不支持 Write-Output 返回值(会被忽略),必须使用 return 语句。同时,类方法内的 Write-Verbose 等流输出在某些宿主环境中可能不会显示,建议在方法外部进行日志记录。

  6. 性能考量:虽然 class 提供了强类型和结构化的优势,但对于简单的数据传递场景,PSCustomObject 仍然更轻量。仅在需要封装逻辑、继承关系或类型约束时使用 class,避免过度设计。

PowerShell 技能连载 - .NET 互操作深入

适用于 PowerShell 7.0 及以上版本

PowerShell 本质上是 .NET 生态的一部分,每一个变量、每一个对象都运行在 CLR 之上。虽然日常工作中我们习惯使用 cmdlet 来完成任务,但在面对高性能计算、底层系统调用或需要精确控制内存的场景时,直接使用 .NET 类往往能获得数量级的性能提升。

理解 .NET 互操作不仅是突破 cmdlet 限制的手段,更是深入理解 PowerShell 运行时行为的钥匙。当你知道 [System.IO.File]::ReadAllText() 比同等的 Get-Content 快数十倍时,你就能够在脚本中做出更明智的选择——何时追求简洁,何时追求性能。

本文将从三个维度展开:直接调用 .NET 类实现高性能操作、利用反射动态调用方法和创建实例、以及通过 P/Invoke 调用原生 Win32 API,帮助你全面掌握 PowerShell 中的 .NET 互操作能力。

直接调用 .NET 类——高性能替代方案

PowerShell 的 cmdlet 设计强调易用性和管道友好性,但这通常会引入额外的开销。当你处理大量数据或需要极致性能时,直接使用 .NET 类是更优的选择。

以下示例展示了几个常见的场景:使用 System.IO 进行高性能文件操作,使用 System.Text.Json 进行快速 JSON 序列化,以及使用 System.Net.Http 发起 HTTP 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 场景一:高性能文件读写
# 使用 [System.IO.File] 替代 Get-Content,性能提升数十倍
$testFile = [System.IO.Path]::GetTempFileName()
$lines = 1..10000 | ForEach-Object { "Line $_ at $(Get-Date -Format 'HH:mm:ss.fff')" }

# 直接写入,比 Set-Content 快得多
[System.IO.File]::WriteAllLines($testFile, $lines)

# 读取全部内容为单个字符串
$content = [System.IO.File]::ReadAllText($testFile)
Write-Host "文件大小: $([System.IO.FileInfo]::new($testFile).Length) 字节"

# 按行读取(延迟枚举,内存友好)
$lineCount = 0
$reader = [System.IO.StreamReader]::new($testFile)
while ($null -ne $reader.ReadLine()) { $lineCount++ }
$reader.Close()
Write-Host "总行数: $lineCount"

# 场景二:System.Text.Json 高性能序列化
$product = [ordered]@{
Name = "PowerShell 7.4"
Version = "7.4.0"
Features = @("Pipeline Chain", "Null Coalescing", "Ternary Operator")
ReleaseDate = "2023-11-16"
IsLTS = $true
}

# 使用 System.Text.Json(比 ConvertTo-Json 更快)
$jsonOptions = [System.Text.Json.JsonSerializerOptions]::new()
$jsonOptions.WriteIndented = $true
$jsonOptions.PropertyNamingPolicy = [System.Text.Json.JsonNamingPolicy]::CamelCase

$json = [System.Text.Json.JsonSerializer]::Serialize(
[System.Collections.Generic.Dictionary[string, object]]$product,
$jsonOptions
)
Write-Host $json

# 反序列化
$deserialized = [System.Text.Json.JsonSerializer]::Deserialize(
$json,
[System.Collections.Generic.Dictionary[string, object]].MakeGenericType(@([string], [object])),
$jsonOptions
)
Write-Host "反序列化结果 - Name: $($deserialized['name'])"

# 场景三:使用 System.Net.Http 发起请求(比 Invoke-WebRequest 更灵活)
$httpClient = [System.Net.Http.HttpClient]::new()
$httpClient.Timeout = [System.TimeSpan]::FromSeconds(10)
$httpClient.DefaultRequestHeaders.Add("User-Agent", "PowerShell-DotNetInterop/1.0")

try {
$response = $httpClient.GetAsync("https://httpbin.org/get").GetAwaiter().GetResult()
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
Write-Host "状态码: $($response.StatusCode)"
Write-Host "响应长度: $($body.Length) 字符"
}
finally {
$httpClient.Dispose()
}

# 清理临时文件
[System.IO.File]::Delete($testFile)

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
文件大小: 358896 字节
总行数: 10000
{
"name": "PowerShell 7.4",
"version": "7.4.0",
"features": [
"Pipeline Chain",
"Null Coalescing",
"Ternary Operator"
],
"releaseDate": "2023-11-16",
"isLTS": true
}
反序列化结果 - Name: PowerShell 7.4
状态码: OK
响应长度: 358 字符

使用反射动态调用方法和创建实例

反射是 .NET 的核心能力之一,它允许你在运行时检查类型信息、动态调用方法、创建实例,甚至修改私有字段。在 PowerShell 中,反射特别适合处理那些无法在编译时确定类型的场景,比如插件系统、动态加载程序集,或者绕过某些访问限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 演示:使用反射探索和调用 .NET 类型

# 获取 System.String 的所有公共方法
$stringType = [string]
$methods = $stringType.GetMethods([System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Instance)
Write-Host "String 类型共有 $($methods.Count) 个公共方法"
Write-Host "部分方法列表:"
$methods | Select-Object -First 8 Name, ReturnType | Format-Table -AutoSize

# 使用反射动态调用方法
$text = "Hello, PowerShell .NET Interop!"
$methodInfo = $stringType.GetMethod("Contains", [type[]]@([string]))
$result = $methodInfo.Invoke($text, [object[]]@("PowerShell"))
Write-Host "Contains('PowerShell'): $result"

# 动态创建泛型集合
$listType = [System.Collections.Generic.List`1].MakeGenericType([int])
$list = [Activator]::CreateInstance($listType)
$addMethod = $listType.GetMethod("Add")
1..5 | ForEach-Object { $addMethod.Invoke($list, [object[]]$_) }
Write-Host "动态创建的 List<int> 内容: $($list -join ', ')"

# 反射加载外部程序集并调用其中的类型
# 模拟:加载 System.Text.RegularExpressions 并使用反射调用
$asm = [System.Reflection.Assembly]::LoadWithPartialName("System.Text.RegularExpressions")
$regexType = $asm.GetType("System.Text.RegularExpressions.Regex")
$pattern = "\b\w+@\w+\.\w+\b"

# 通过反射创建 Regex 实例
$regexCtor = $regexType.GetConstructor([type[]]@([string]))
$regex = $regexCtor.Invoke([object[]]@($pattern))

# 调用 Matches 方法
$matchesMethod = $regexType.GetMethod("Matches", [type[]]@([string]))
$sampleText = "联系我们: admin@vichamp.com 或 support@example.org"
$matches = $matchesMethod.Invoke($regex, [object[]]@($sampleText))

Write-Host "在文本中找到 $($matches.Count) 个邮箱地址:"
foreach ($m in $matches) {
Write-Host " - $($m.Value) (位置: $($m.Index))"
}

# 使用反射访问静态方法和属性
$env = [System.Environment]
$props = $env.GetProperties([System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::Public)
Write-Host "`nSystem.Environment 静态属性:"
$props | Select-Object -First 5 Name, PropertyType | ForEach-Object {
$val = $env.GetProperty($_.Name).GetValue($null)
Write-Host " $($_.Name) = $val"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
String 类型共有 108 个公共方法
部分方法列表:
Name ReturnType
---- ----------
CompareTo System.Int32
Contains System.Boolean
CopyTo System.Void
EndsWith System.Boolean
Equals System.Boolean
Equals System.Boolean
Equals System.Boolean
GetEnumerator System.CharEnumerator

Contains('PowerShell'): True
动态创建的 List<int> 内容: 1, 2, 3, 4, 5
在文本中找到 2 个邮箱地址:
- admin@vichamp.com (位置: 5)
- support@example.org (位置: 28)

System.Environment 静态属性:
CommandLine = /opt/powershell/pwsh
CurrentDirectory = /home/user/scripts
ExitCode = 0
HasShutdownStarted = False
Is64BitProcess = True

P/Invoke 调用 Win32 API

P/Invoke(Platform Invocation Services)是 .NET 提供的调用非托管 DLL 函数的机制。在 PowerShell 中,你可以通过 Add-Type 定义签名,然后直接调用 Windows 系统 DLL 中的原生函数。这在需要与操作系统底层交互时非常有用,例如获取系统信息、操作窗口句柄或访问注册表的底层 API。

以下示例展示了几种常见的 Win32 API 调用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# P/Invoke 调用 Win32 API 示例
# 注意:以下代码仅在 Windows 平台上运行

if ($IsWindows -or ($env:OS -eq "Windows_NT")) {

# 示例一:获取系统内存信息
$memorySignature = @"
using System;
using System.Runtime.InteropServices;

public class MemoryHelper
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;

public MEMORYSTATUSEX()
{
this.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
}
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalMemoryStatusEx(
[In, Out] MEMORYSTATUSEX lpBuffer);
}
"@

Add-Type -TypeDefinition $memorySignature -Language CSharp

$memStatus = [MemoryHelper+MEMORYSTATUSEX]::new()
[MemoryHelper]::GlobalMemoryStatusEx($memStatus) | Out-Null

$totalGB = [math]::Round($memStatus.ullTotalPhys / 1GB, 2)
$availGB = [math]::Round($memStatus.ullAvailPhys / 1GB, 2)
$usedPercent = $memStatus.dwMemoryLoad

Write-Host "=== 系统内存信息 ==="
Write-Host "总物理内存: ${totalGB} GB"
Write-Host "可用物理内存: ${availGB} GB"
Write-Host "内存使用率: ${usedPercent}%"

# 示例二:获取精确的系统启动时间(比 Get-Date 更准确)
$uptimeSignature = @"
using System;
using System.Runtime.InteropServices;

public class UptimeHelper
{
[DllImport("kernel32.dll")]
public static extern UInt64 GetTickCount64();
}
"@

Add-Type -TypeDefinition $uptimeSignature -Language CSharp

$tickCount = [UptimeHelper]::GetTickCount64()
$uptime = [TimeSpan]::FromMilliseconds($tickCount)
Write-Host "`n=== 系统运行时间 ==="
Write-Host "已运行: $($uptime.Days) 天 $($uptime.Hours) 小时 $($uptime.Minutes) 分钟"

# 示例三:获取当前控制台窗口标题
$consoleSignature = @"
using System;
using System.Text;
using System.Runtime.InteropServices;

public class ConsoleHelper
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetConsoleTitle(
StringBuilder lpConsoleTitle,
int nSize);
}
"@

Add-Type -TypeDefinition $consoleSignature -Language CSharp

$titleBuilder = [System.Text.StringBuilder]::new(256)
[ConsoleHelper]::GetConsoleTitle($titleBuilder, 256) | Out-Null
Write-Host "`n=== 控制台信息 ==="
Write-Host "当前窗口标题: $($titleBuilder.ToString())"

} else {
Write-Host "P/Invoke 示例需要在 Windows 平台上运行"
Write-Host "当前平台: $($PSVersionTable.OS)"

# 跨平台替代方案:使用 .NET API 获取系统信息
Write-Host "`n=== 跨平台系统信息 ==="
Write-Host "机器名: $([System.Environment]::MachineName)"
Write-Host "用户名: $([System.Environment]::UserName)"
Write-Host "处理器核心数: $([System.Environment]::ProcessorCount)"
Write-Host "系统目录: $([System.Environment]::SystemDirectory)"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
=== 系统内存信息 ===
总物理内存: 32.00 GB
可用物理内存: 14.37 GB
内存使用率: 55%

=== 系统运行时间 ===
已运行: 3 天 7 小时 42 分钟

=== 控制台信息 ===
当前窗口标题: Windows PowerShell

注意事项

  1. 性能权衡:直接使用 .NET 类虽然性能更高,但代码可读性通常不如 cmdlet。对于一次性脚本或管理任务,优先使用 cmdlet;对于需要处理大量数据或高频调用的场景,再考虑直接使用 .NET 类。可以用 Measure-Command 对比实际性能差异后再做决策。

  2. 类型转换陷阱:PowerShell 会自动包装 .NET 对象,某些情况下类型转换可能不如预期。例如 [System.Collections.Generic.List[int]] 和 PowerShell 数组之间存在隐式转换,但频繁转换会抵消性能优势。建议尽量在 .NET API 之间传递数据,减少跨越 PowerShell 管道的次数。

  3. 反射的性能开销:反射调用比直接调用慢 10-100 倍。如果需要大量调用同一方法,应该缓存 MethodInfo 对象并重复使用,而不是每次都重新查找。在高性能循环中,应避免使用反射。

  4. P/Invoke 平台限制:P/Invoke 调用的 Win32 API 仅在 Windows 上可用。如果你的脚本需要跨平台运行,务必添加平台检测逻辑(如 $IsWindows),并提供基于 .NET API 的跨平台替代方案。同时要注意区分 x86 和 x64 的函数签名差异。

  5. 程序集加载策略Add-Type 编译的 C# 代码会生成临时程序集,无法卸载。大量使用 Add-Type 会导致内存增长。对于复杂场景,建议将 C# 代码编译为独立的 DLL,使用 [System.Reflection.Assembly]::LoadFrom() 按需加载。

  6. Dispose 模式:许多 .NET 类(如 HttpClientStreamReaderFileStream)实现了 IDisposable 接口。在 PowerShell 中应使用 try/finally 块或 .Dispose() 方法显式释放资源,避免因垃圾回收延迟导致的文件锁或连接泄漏。