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() 方法显式释放资源,避免因垃圾回收延迟导致的文件锁或连接泄漏。

PowerShell 技能连载 - LINQ 数据操作

适用于 PowerShell 5.1 及以上版本

在处理大规模数据集时,PowerShell 原生的管道操作(如 Where-ObjectForEach-ObjectSort-Object)虽然语法直观,但在性能上往往不尽人意。管道每次传递对象都需要包装和拆包,当数据量达到数万甚至百万级别时,这个开销会变得非常可观。

LINQ(Language Integrated Query)是 .NET 框架内置的一套强大的数据查询和操作库。虽然 PowerShell 没有像 C# 那样提供原生的 LINQ 语法糖,但我们可以直接通过 [System.Linq.Enumerable] 静态类调用 LINQ 方法。在现代 PowerShell(5.1+)中,LINQ 的集成度已经大幅提升,尤其在批量数据处理、聚合计算和集合变换等场景下,相比管道操作可以获得数倍甚至数十倍的性能提升。

本文将系统介绍如何在 PowerShell 中使用 LINQ 进行高效的数据过滤、排序、聚合和分组操作,并通过基准测试对比原生管道与 LINQ 的性能差异。

LINQ 过滤与条件筛选

最常见的数据操作是条件筛选。PowerShell 中习惯使用 Where-Object,但 LINQ 的 Where 方法在大数据集上有明显的性能优势。下面的示例创建一个包含 10 万条记录的测试数据集,然后对比两种方式的筛选速度。

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
# 构造测试数据:10 万个自定义对象
$testData = [System.Collections.Generic.List[PSObject]]::new()
$rng = [System.Random]::new(42)

foreach ($i in 1..100000) {
$testData.Add([PSCustomObject]@{
Id = $i
Name = "Item_$i"
Value = $rng.Next(1, 10001)
Active = ($rng.Next(2) -eq 0)
})
}

Write-Host "数据集大小: $($testData.Count) 条记录"

# 方式一:PowerShell 原生 Where-Object 管道筛选
$sw1 = [System.Diagnostics.Stopwatch]::StartNew()
$filtered1 = $testData | Where-Object { $_.Active -and $_.Value -gt 8000 }
$sw1.Stop()
Write-Host "Where-Object 筛选结果: $($filtered1.Count) 条,耗时: $($sw1.ElapsedMilliseconds) ms"

# 方式二:LINQ Where 方法筛选
$sw2 = [System.Diagnostics.Stopwatch]::StartNew()
$filtered2 = [System.Linq.Enumerable]::Where(
$testData,
[Func[object, bool]]{ param($x) $x.Active -and $x.Value -gt 8000 }
)
$filteredList2 = [System.Linq.Enumerable]::ToList($filtered2)
$sw2.Stop()
Write-Host "LINQ Where 筛选结果: $($filteredList2.Count) 条,耗时: $($sw2.ElapsedMilliseconds) ms"

LINQ 的 Where 方法接受一个委托函数作为筛选条件,返回的是 IEnumerable 延迟执行序列。使用 ToList() 可以立即执行并将结果物化为列表。在大数据集场景下,LINQ 避免了管道的对象传递开销,直接在内存中完成迭代筛选。

1
2
3
数据集大小: 100000 条记录
Where-Object 筛选结果: 968 条,耗时: 487 ms
LINQ Where 筛选结果: 968 条,耗时: 62 ms

LINQ 排序与聚合

除了筛选,排序和聚合也是日常数据处理的高频操作。LINQ 提供了 OrderByOrderByDescending 进行排序,SumAverageMinMax 进行聚合计算。下面我们演示如何在一个脚本中组合使用这些方法。

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
# 使用 LINQ 进行排序(按 Value 降序取前 10 名)
$sorted = [System.Linq.Enumerable]::OrderByDescending(
$testData,
[Func[object, int]]{ param($x) $x.Value }
)
$top10 = [System.Linq.Enumerable]::Take($sorted, 10)

Write-Host "=== Value 最高的 10 条记录 ==="
foreach ($item in $top10) {
Write-Host " Id=$($item.Id), Name=$($item.Name), Value=$($item.Value), Active=$($item.Active)"
}

# 使用 LINQ 聚合计算
$allValues = [System.Linq.Enumerable]::Select(
$testData,
[Func[object, int]]{ param($x) $x.Value }
)

$sum = [System.Linq.Enumerable]::Sum($allValues)
$avg = [System.Linq.Enumerable]::Average(
[System.Linq.Enumerable]::Select($testData, [Func[object, int]]{ param($x) $x.Value })
)
$min = [System.Linq.Enumerable]::Min($allValues)
$max = [System.Linq.Enumerable]::Max($allValues)

Write-Host ""
Write-Host "=== 聚合统计 ==="
Write-Host " 总和: $sum"
Write-Host " 平均值: $([math]::Round($avg, 2))"
Write-Host " 最小值: $min"
Write-Host " 最大值: $max"
Write-Host " 记录数: $($testData.Count)"

这段代码展示了 LINQ 的链式调用风格。OrderByDescending 返回一个有序序列,Take 从中截取前 N 条。聚合方法 SumAverageMinMax 可以直接对数值集合进行计算,无需创建中间的 Measure-Object 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
=== Value 最高的 10 条记录 ===
Id=96827, Name=Item_96827, Value=10000, Active=True
Id=73614, Name=Item_73614, Value=10000, Active=False
Id=40291, Name=Item_40291, Value=10000, Active=True
Id=21983, Name=Item_21983, Value=10000, Active=True
Id=56802, Name=Item_56802, Value=9999, Active=False
Id=88451, Name=Item_88451, Value=9999, Active=True
Id=14567, Name=Item_14567, Value=9999, Active=True
Id=63290, Name=Item_63290, Value=9999, Active=True
Id=37148, Name=Item_37148, Value=9998, Active=False
Id=79205, Name=Item_79205, Value=9998, Active=True

=== 聚合统计 ===
总和: 500312847
平均值: 5003.13
最小值: 1
最大值: 10000
记录数: 100000

LINQ 分组与字典转换

在数据分析中,按字段分组统计是核心操作。PowerShell 的 Group-Object 可以完成分组,但 LINQ 的 GroupBy 配合 ToDictionary 在性能和灵活性上更胜一筹。下面的示例演示了按 Active 状态分组统计,以及将列表转换为字典以实现 O(1) 的查找性能。

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
# 按Active状态分组统计
$grouped = [System.Linq.Enumerable]::GroupBy(
$testData,
[Func[object, bool]]{ param($x) $x.Active }
)

Write-Host "=== 按 Active 状态分组统计 ==="
foreach ($group in $grouped) {
$groupValues = [System.Linq.Enumerable]::Select(
$group,
[Func[object, int]]{ param($x) $x.Value }
)
$count = [System.Linq.Enumerable]::Count($group)
$avgVal = [math]::Round([System.Linq.Enumerable]::Average($groupValues), 2)
Write-Host " Active=$($group.Key): 共 $count 条,平均值=$avgVal"
}

# 将数据转为字典,以 Id 为键实现快速查找
$dict = [System.Linq.Enumerable]::ToDictionary(
$testData,
[Func[object, int]]{ param($x) $x.Id },
[Func[object, object]]{ param($x) $x }
)

Write-Host ""
Write-Host "=== 字典查找演示 ==="
$lookupIds = @(42, 99999, 50000, 7)
foreach ($lid in $lookupIds) {
if ($dict.ContainsKey($lid)) {
$found = $dict[$lid]
Write-Host " 查找 Id=$lid -> Name=$($found.Name), Value=$($found.Value)"
} else {
Write-Host " 查找 Id=$lid -> 未找到"
}
}

GroupBy 返回的是 IGrouping 对象的集合,每个分组有一个 Key 属性和一组属于该分组的元素。ToDictionary 将集合转换为 Dictionary<TKey, TValue>,后续通过键查找的时间复杂度为 O(1),比 Where-Object 的线性扫描快得多。

1
2
3
4
5
6
7
8
9
=== 按 Active 状态分组统计 ===
Active=True: 共 49963 条,平均值=5008.74
Active=False: 共 50037 条,平均值=4997.52

=== 字典查找演示 ===
查找 Id=42 -> Name=Item_42, Value=6789
查找 Id=99999 -> Name=Item_99999, Value=2345
查找 Id=50000 -> Name=Item_50000, Value=8901
查找 Id=7 -> Name=Item_7, Value=1234

综合实战:日志数据分析

将前面的 LINQ 操作组合起来,可以构建一个高效的日志分析脚本。以下示例模拟了一批服务器日志数据,使用 LINQ 完成多维度分析。

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
# 模拟服务器日志数据
$logEntries = [System.Collections.Generic.List[PSObject]]::new()
$levels = @("INFO", "WARN", "ERROR", "DEBUG")
$servers = @("WEB-01", "WEB-02", "API-01", "DB-01", "CACHE-01")
$logRng = [System.Random]::new(123)

$baseTime = [DateTime]::new(2025, 10, 23, 0, 0, 0)
foreach ($i in 1..50000) {
$logEntries.Add([PSCustomObject]@{
Timestamp = $baseTime.AddSeconds($logRng.Next(0, 86400))
Level = $levels[$logRng.Next($levels.Length)]
Server = $servers[$logRng.Next($servers.Length)]
Message = "Request processed in $($logRng.Next(1, 5000))ms"
RequestId = "REQ-$([guid]::NewGuid().ToString('N').Substring(0, 8))"
})
}

Write-Host "日志条数: $($logEntries.Count)"
Write-Host ""

# 1. 按日志级别分组统计
$byLevel = [System.Linq.Enumerable]::GroupBy(
$logEntries,
[Func[object, string]]{ param($x) $x.Level }
)

Write-Host "=== 按日志级别统计 ==="
foreach ($g in $byLevel) {
$cnt = [System.Linq.Enumerable]::Count($g)
$pct = [math]::Round($cnt / $logEntries.Count * 100, 1)
Write-Host " $($g.Key): $cnt 条 ($pct%)"
}

# 2. 按服务器分组,找出每个服务器 ERROR 数量
Write-Host ""
Write-Host "=== 各服务器 ERROR 统计 ==="
$errorEntries = [System.Linq.Enumerable]::Where(
$logEntries,
[Func[object, bool]]{ param($x) $x.Level -eq "ERROR" }
)
$byServer = [System.Linq.Enumerable]::GroupBy(
$errorEntries,
[Func[object, string]]{ param($x) $x.Server }
)
foreach ($g in $byServer) {
$errorCount = [System.Linq.Enumerable]::Count($g)
Write-Host " $($g.Key): $errorCount 个 ERROR"
}

# 3. 使用 LINQ Distinct 获取所有唯一 RequestId 的数量
$allReqIds = [System.Linq.Enumerable]::Select(
$logEntries,
[Func[object, string]]{ param($x) $x.RequestId }
)
$uniqueCount = [System.Linq.Enumerable]::Count(
[System.Linq.Enumerable]::Distinct($allReqIds)
)
Write-Host ""
Write-Host "唯一 RequestId 数量: $uniqueCount(总条目: $($logEntries.Count))"

这个综合示例演示了 LINQ 的实际工程应用:从日志过滤(Where)、分组聚合(GroupBy)、去重统计(Distinct)到快速查找(ToDictionary),覆盖了日志分析的核心需求。5 万条日志的分析在毫秒级内即可完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
日志条数: 50000

=== 按日志级别统计 ===
INFO: 12543 条 (25.1%)
WARN: 12487 条 (25.0%)
ERROR: 12462 条 (24.9%)
DEBUG: 12508 条 (25.0%)

=== 各服务器 ERROR 统计 ===
WEB-01: 2512 个 ERROR
WEB-02: 2487 个 ERROR
API-01: 2498 个 ERROR
DB-01: 2478 个 ERROR
CACHE-01: 2487 个 ERROR

唯一 RequestId 数量: 50000(总条目: 50000)

注意事项

  1. 委托类型必须匹配:LINQ 方法要求传入强类型的委托(如 [Func[object, bool]]),PowerShell 的脚本块不会自动转换。务必显式指定委托类型,否则会抛出方法重载解析失败的异常。这也是 LINQ 在 PowerShell 中语法相对冗长的根本原因。

  2. 延迟执行与物化:LINQ 的 WhereSelectOrderBy 等方法返回的是 IEnumerable 延迟序列,数据在遍历时才真正处理。如果需要多次使用结果,应调用 ToList()ToArray() 进行物化,避免重复计算。

  3. 小数据集不必用 LINQ:当数据量在几百条以内时,PowerShell 原生的 Where-ObjectGroup-Object 性能已经足够好,代码可读性反而更好。LINQ 的优势在万级以上数据集才显著体现。不要为了用 LINQ 而用 LINQ。

  4. 类型转换是性能关键:LINQ 的 SumAverage 等数值方法需要特定类型的集合(如 int[]double[])。对于 PSObject 集合,需要先用 Select 提取数值字段再聚合。如果数据源本身就是强类型数组,性能会更好。

  5. PowerShell 7 的改进:在 PowerShell 7 中,可以通过 using namespace System.Linq 简化调用,也可以用 ::new() 直接创建泛型委托。此外,PowerShell 7 的核心是基于 .NET 6/8,LINQ 方法的行为与 C# 完全一致,不必担心兼容性问题。

  6. 避免在管道中使用 LINQ:LINQ 和 PowerShell 管道是两种不同的编程范式。在同一个脚本中应选择一种方式为主:要么全用管道,要么全用 LINQ + foreach 循环。混合使用会导致代码风格不一致,增加维护难度,且无法获得最佳性能。

PowerShell 技能连载 - 类型加速器与 .NET 互操作

适用于 PowerShell 5.1 及以上版本

PowerShell 构建在 .NET 之上,这意味着你可以直接使用 .NET 的全部类库——从文件操作到加密算法,从网络编程到并行处理。但很多用户不知道的是,PowerShell 提供了大量”类型加速器”(Type Accelerator),让你可以用简短的名称代替冗长的命名空间路径。比如 [xml] 实际上是 [System.Xml.XmlDocument][mailaddress][System.Net.Mail.MailAddress]

本文将讲解常用的类型加速器、.NET 类的直接调用,以及一些通过 .NET 互操作实现的高级功能。

内置类型加速器

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
# 查看所有内置类型加速器
[System.Management.Automation.PSObject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)::Get | Sort-Object Key | ForEach-Object {
[PSCustomObject]@{
Shortcut = $_.Key
FullType = $_.Value.FullName
}
} | Format-Table -AutoSize | Out-String -Width 120 | Write-Host

# 常用加速器示例
# [xml] => System.Xml.XmlDocument
[xml]$doc = '<config><server>prod-db01</server><port>5432</port></config>'
Write-Host "XML 服务器:$($doc.config.server)"

# [regex] => System.Text.RegularExpressions.Regex
$matches = [regex]::Matches("版本 1.2.3 和 2.0.1", '\d+\.\d+\.\d+')
$matches | ForEach-Object { Write-Host "匹配:$($_.Value)" }

# [guid] => System.Guid
$guid = [guid]::NewGuid()
Write-Host "新 GUID:$guid"

# [datetime] => System.DateTime
$dt = [datetime]::ParseExact("2025-07-03 08:30", "yyyy-MM-dd HH:mm", $null)
Write-Host "解析日期:$($dt.ToString('yyyy年M月d日 HH:mm'))"

# [timespan] => System.TimeSpan
$ts = [timespan]::FromHours(2.5)
Write-Host "时间跨度:$($ts.ToString('hh\:mm\:ss'))"

# [math] => System.Math
Write-Host "PI:$([math]::PI)"
Write-Host "四舍五入:$([math]::Round(3.14159, 2))"
Write-Host "最大值:$([math]::Max(10, 20))"

执行结果示例:

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
Shortcut   FullType
-------- --------
adsi System.DirectoryServices.DirectoryEntry
adsisearcher System.DirectoryServices.DirectorySearcher
bool System.Boolean
byte System.Byte
char System.Char
datetime System.DateTime
decimal System.Decimal
double System.Double
float System.Single
guid System.Guid
hashtable System.Collections.Hashtable
int System.Int32
long System.Int64
mailaddress System.Net.Mail.MailAddress
regex System.Text.RegularExpressions.Regex
scriptblock System.Management.Automation.ScriptBlock
string System.String
timespan System.TimeSpan
uri System.Uri
version System.Version
xml System.Xml.XmlDocument

XML 服务器:prod-db01
匹配:1.2.3
匹配:2.0.1
新 GUID:a1b2c3d4-e5f6-7890-abcd-ef1234567890
解析日期:20257308:30
时间跨度:02:30:00
PI:3.14159265358979
四舍五入:3.14
最大值:20

常用 .NET 类直接调用

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
# System.IO —— 文件操作(比 cmdlet 更快的批量操作)
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 快速读取所有行
$lines = [System.IO.File]::ReadAllLines("C:\Logs\app.log")
Write-Host "读取 $($lines.Count) 行,耗时:$($sw.ElapsedMilliseconds)ms"

# 快速写入
[System.IO.File]::WriteAllLines("C:\Temp\output.txt", $lines)

$sw.Stop()

# System.Text.StringBuilder —— 高效字符串拼接
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("服务器巡检报告") | Out-Null
$sb.AppendLine("生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") | Out-Null
$sb.AppendLine("") | Out-Null

@("SRV01", "SRV02", "SRV03") | ForEach-Object {
$sb.AppendLine(" $_ : 在线") | Out-Null
}

Write-Host $sb.ToString()

# System.Net —— DNS 查询
$dnsResult = [System.Net.Dns]::GetHostAddresses("www.microsoft.com")
$dnsResult | ForEach-Object { Write-Host "IP:$($_.IPAddressFamily) $($_.ToString())" }

# System.Security.Cryptography —— 计算文件哈希
function Get-FileHashFast {
param([string]$Path, [string]$Algorithm = "SHA256")

$hashAlg = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
$stream = [System.IO.File]::OpenRead($Path)
try {
$hashBytes = $hashAlg.ComputeHash($stream)
$hashHex = [BitConverter]::ToString($hashBytes) -replace '-', ''
return $hashHex.ToLower()
} finally {
$stream.Close()
}
}

$hash = Get-FileHashFast -Path "C:\Windows\notepad.exe"
Write-Host "SHA256:$($hash.Substring(0, 16))..."

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
读取 54230 行,耗时:45ms

服务器巡检报告
生成时间:2025-07-03 08:30:15

SRV01 : 在线
SRV02 : 在线
SRV03 : 在线

IP:InterNetwork 20.190.159.2
IP:InterNetworkV6 2603:1030:20...
SHA256:a1b2c3d4e5f67890...

自定义类型加速器

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
# 注册自定义类型加速器
$accel = [System.Management.Automation.PSObject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)

# 添加自定义加速器
$accel::Add("ws", [System.Net.WebSockets.ClientWebSocket])
$accel::Add("http", [System.Net.Http.HttpClient])

# 验证
Write-Host "ws => $($accel::Get['ws'].FullName)"
Write-Host "http => $($accel::Get['http'].FullName)"

# 实际使用:用 [http] 代替完整类型名
$client = [http]::new()
$client.Timeout = [timespan]::FromSeconds(30)
Write-Host "HTTP 客户端超时:$($client.Timeout)"

# 添加常用 .NET 缩写
$mappings = @{
"sb" = [System.Text.StringBuilder]
"stopw" = [System.Diagnostics.Stopwatch]
"zip" = [System.IO.Compression.ZipArchive]
"json" = [System.Text.Json.JsonSerializer]
"crypto" = [System.Security.Cryptography.Aes]
}

foreach ($key in $mappings.Keys) {
$accel::Add($key, $mappings[$key])
Write-Host "已注册:$key => $($mappings[$key].FullName)" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
ws => System.Net.WebSockets.ClientWebSocket
http => System.Net.Http.HttpClient
HTTP 客户端超时:00:00:30
已注册:sb => System.Text.StringBuilder
已注册:stopw => System.Diagnostics.Stopwatch
已注册:zip => System.IO.Compression.ZipArchive
已注册:json => System.Text.Json.JsonSerializer
已注册:crypto => System.Security.Cryptography.Aes

使用 .NET 泛型集合

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
# List<T> —— 动态数组
$list = [System.Collections.Generic.List[string]]::new()
$list.AddRange(@("apple", "banana", "cherry"))
$list.Add("date")
Write-Host "列表内容:$($list -join ', ')"
Write-Host "数量:$($list.Count)"

$list.Remove("banana")
$list.Sort()
Write-Host "排序后:$($list -join ', ')"

# Dictionary<TKey, TValue> —— 强类型字典
$dict = [System.Collections.Generic.Dictionary[string, int]]::new()
$dict["apple"] = 5
$dict["banana"] = 3
$dict["cherry"] = 7

Write-Host "苹果数量:$($dict['apple'])"

# 遍历强类型字典
foreach ($entry in $dict) {
Write-Host " $($entry.Key):$($entry.Value) 个"
}

# HashSet<T> —— 去重集合
$set = [System.Collections.Generic.HashSet[string]]::new()
$items = @("server01", "server02", "server01", "server03", "server02")
foreach ($item in $items) {
$added = $set.Add($item)
if (-not $added) {
Write-Host "重复项:$item" -ForegroundColor Yellow
}
}
Write-Host "去重后:$($set -join ', ')"

# Queue<T> —— 先进先出队列
$queue = [System.Collections.Generic.Queue[string]]::new()
"task1", "task2", "task3" | ForEach-Object { $queue.Enqueue($_) }

while ($queue.Count -gt 0) {
$task = $queue.Dequeue()
Write-Host "处理:$task"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
列表内容:apple, banana, cherry, date
数量:4
排序后:apple, cherry, date
苹果数量:5
apple:5
banana:3
cherry:7
重复项:server01
重复项:server02
去重后:server01, server02, server03
处理:task1
处理:task2
处理:task3

注意事项

  1. 类型加速器作用域:自定义类型加速器只在当前会话中生效,放入 $PROFILE 中可持久化
  2. 性能差异:批量操作(大量文件读写、字符串拼接)使用 .NET 类比 cmdlet 快 10-100 倍
  3. 内存管理IDisposable 对象(如 StreamHttpClient)应使用 using 语句或 try/finally 释放
  4. 泛型语法:PowerShell 中泛型用方括号表示 [List[string]],不能使用 C# 的 List<string> 语法
  5. 平台兼容:部分 .NET 类仅在 Windows 上可用(如 System.Drawing),跨平台脚本注意检查
  6. 加载程序集:使用 Add-Type -AssemblyName 加载非默认引用的程序集