适用于 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 $testFile = [System.IO.Path ]::GetTempFileName()$lines = 1 ..10000 | ForEach-Object { "Line $_ at $ (Get-Date -Format 'HH:mm:ss.fff')" }[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 " $product = [ordered ]@ { Name = "PowerShell 7.4" Version = "7.4.0" Features = @ ("Pipeline Chain" , "Null Coalescing" , "Ternary Operator" ) ReleaseDate = "2023-11-16" IsLTS = $true } $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'])" $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 $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 ', ')" $asm = [System.Reflection.Assembly ]::LoadWithPartialName("System.Text.RegularExpressions" )$regexType = $asm .GetType("System.Text.RegularExpressions.Regex" )$pattern = "\b\w+@\w+\.\w+\b" $regexCtor = $regexType .GetConstructor([type []]@ ([string ]))$regex = $regexCtor .Invoke([object []]@ ($pattern ))$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 ReturnTypeCompareTo 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 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 / 1 GB, 2 ) $availGB = [math ]::Round($memStatus .ullAvailPhys / 1 GB, 2 ) $usedPercent = $memStatus .dwMemoryLoad Write-Host "=== 系统内存信息 ===" Write-Host "总物理内存: $ {totalGB} GB" Write-Host "可用物理内存: $ {availGB} GB" Write-Host "内存使用率: $ {usedPercent}%" $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)" 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
注意事项
性能权衡 :直接使用 .NET 类虽然性能更高,但代码可读性通常不如 cmdlet。对于一次性脚本或管理任务,优先使用 cmdlet;对于需要处理大量数据或高频调用的场景,再考虑直接使用 .NET 类。可以用 Measure-Command 对比实际性能差异后再做决策。
类型转换陷阱 :PowerShell 会自动包装 .NET 对象,某些情况下类型转换可能不如预期。例如 [System.Collections.Generic.List[int]] 和 PowerShell 数组之间存在隐式转换,但频繁转换会抵消性能优势。建议尽量在 .NET API 之间传递数据,减少跨越 PowerShell 管道的次数。
反射的性能开销 :反射调用比直接调用慢 10-100 倍。如果需要大量调用同一方法,应该缓存 MethodInfo 对象并重复使用,而不是每次都重新查找。在高性能循环中,应避免使用反射。
P/Invoke 平台限制 :P/Invoke 调用的 Win32 API 仅在 Windows 上可用。如果你的脚本需要跨平台运行,务必添加平台检测逻辑(如 $IsWindows),并提供基于 .NET API 的跨平台替代方案。同时要注意区分 x86 和 x64 的函数签名差异。
程序集加载策略 :Add-Type 编译的 C# 代码会生成临时程序集,无法卸载。大量使用 Add-Type 会导致内存增长。对于复杂场景,建议将 C# 代码编译为独立的 DLL,使用 [System.Reflection.Assembly]::LoadFrom() 按需加载。
Dispose 模式 :许多 .NET 类(如 HttpClient、StreamReader、FileStream)实现了 IDisposable 接口。在 PowerShell 中应使用 try/finally 块或 .Dispose() 方法显式释放资源,避免因垃圾回收延迟导致的文件锁或连接泄漏。