从 PowerShell 调用 COM 与 .NET 的实践 ── 一举拓宽脚本触达的范围

· · PowerShell, Windows, COM, .NET, 自动化, 脚本, 现有资产活用, 业务效率化

「找遍了 cmdlet,也没找到想要的处理方式」── 只要把 PowerShell 用到一定深度,就必然会撞上这堵墙。批量创建快捷方式、不解压就查看 ZIP 内容、窗口操作、Excel 文件的读写。仅凭标准 cmdlet 无法触及的需求,中小企业的信息系统与运维负责人几乎每天都会来咨询。

其实这堵墙的另一侧,正是 PowerShell 擅长的领域。由于 PowerShell 构建在 .NET 之上,可以在不额外安装的情况下直接调用 .NET 类库中的类型。此外,还能用 Add-Type 当场嵌入 C# 代码或 Win32 API(P/Invoke),并用 New-Object -ComObject 操作 WScript.Shell、Excel 等 COM 对象。也就是说,过去用 VBScript 或 VBA 编写的「Windows 自动化」工具箱,几乎可以原样搬过来使用。

不过,这份力量也伴随着相应的后续处理规范。尤其是 Excel 的 COM 操作,是「脚本结束后 EXCEL.EXE 仍然残留」这类经典问题的高发地带,而 Microsoft 本身就不推荐在无人执行环境中自动化 Office。本文将整理 PowerShell 调用 .NET・Add-Type・COM 操作的实务模式与陷阱,直至「脚本该坚持到什么程度、从哪里开始该改用 C# 工具化」的判断标准。

1. 先说结论

  • PowerShell 建立在 .NET 之上。Windows PowerShell 5.1 构建在 .NET Framework 之上,PowerShell 7 构建在 .NET(原 .NET Core)之上,可以像 [System.IO.Path]::GetFileNameWithoutExtension() 这样直接调用 .NET 的静态方法。1
  • 创建实例可以用 New-Object,PowerShell 5.0 及以后也可以用 [类型]::new()只要打出 [类型]::new(不加括号)就能确认构造函数列表,可以一边查阅参数一边编写。2
  • 使用 Add-Type 可以当场编译 C# 源代码并嵌入到会话中。传入带 DllImport 的签名,还能实现 Win32 API 的 P/Invoke 调用。添加的类型无法在会话内清除,同名类型也无法重新定义。3
  • COM 对象通过 New-Object -ComObject <ProgId> 创建。VBScript 的 CreateObject("Shell.Application") 直接对应 New-Object -ComObject Shell.Application,WScript.Shell 创建快捷方式等 WSH 时代的工具,都可以在 PowerShell 中使用。45
  • COM 对象的寿命通过引用计数管理,在 .NET 一侧由 RCW(Runtime Callable Wrapper)握有该引用。只要引用还残留着,Excel 等进程就不会结束。要明确释放可以使用 Marshal.ReleaseComObject,但滥用会引发另一种事故,官方指南是「只在真正必要时使用」。67
  • 对于无人执行环境下的 Office 自动化,Microsoft 明确表示「不推荐,也不支持」。从服务、任务计划程序、服务器端发起的 Excel COM 操作,即使看起来能正常运行,也属于不受支持的架构。无人处理应替换为 Open XML 系列库或 CSV 联动。8
  • 5.1 与 7 中可用的类型・方法并不相同。存在像 String.Split 重载差异那样同一段代码行为不同的例子,在 5.1 中也有些场景需要用 Add-Type 显式加载 GAC 中的程序集。13
  • 当直接调用 .NET 的场景越来越多时,就是该考虑改用 C# 工具化的分界线。当 Add-Type 无法重新定义类型的限制、或分发问题开始变得显眼时,就是脚本已接近极限的信号。

2. PowerShell 建立在 .NET 之上 ── [类型]::方法 与 New-Object

PowerShell 的 cmdlet,是把 .NET 类库的一部分「为运维工作包装得便于使用」而形成的。没有被包装的功能,也可以用方括号写出类型名直接调用。静态方法用 ::,实例的方法与属性用 .

# .NET的静态方法直接调用。不需要 cmdlet,也不需要额外安装
[System.IO.Path]::GetFileNameWithoutExtension('C:\data\订单_20260718.csv')  # -> 订单_20260718
[System.IO.Path]::Combine('C:\data', 'archive', '2026-07')                   # -> C:\data\archive\2026-07
[System.Math]::Round(123.456, 1)                                             # -> 123.5

# 需要实例的话,用 New-Object 或 [类型]::new() 创建
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('server01')

# ::new 不加括号打出,会返回构造函数的列表(作为查阅参数的方法很方便)
[System.IO.StreamWriter]::new

[类型]::new() 是 PowerShell 5.0 新增的写法,比 New-Object 更快,还能像上面的例子那样当场查看构造函数的重载列表。2 另一方面也有需要注意的地方。Get-Item 等 cmdlet 返回的对象,PowerShell 有时会为其追加属性(NoteProperty),因此可能与 ::new() 直接创建的同类型对象在成员构成上不一致。2 如果遇到「通过 cmdlet 获取时明明有的属性却不见了」而感到困惑,请想起这个机制。

分享一个实务技巧。ZIP 处理有 Compress-Archive / Expand-Archive 这两个 cmdlet,但它们所依赖的底层 ZipArchive API 存在 2GB 的文件大小限制。这一限制在压缩与解压两侧的官方文档中均有记载,两者都明确写明「上限来自底层的 System.IO.Compression.ZipArchive API」,并列出该类的参考文档作为出处。910 另外,像「不解压、只想确认内容列表」这类细致的操作,cmdlet 中并不存在。这时就轮到 .NET 的 ZipFile 类登场了。11

# Windows PowerShell 5.1 中,需要显式加载 GAC 中的程序集
# (PowerShell 7 中随附程序集会在需要时自动加载,因此没有这一行也能运行)
Add-Type -AssemblyName System.IO.Compression.FileSystem

# 先从读取开始 ── 不解压,确认 ZIP 的内容
$archive = [System.IO.Compression.ZipFile]::OpenRead('C:\deploy\release.zip')
try {
    $archive.Entries | Select-Object FullName, Length, LastWriteTime
}
finally {
    $archive.Dispose()  # 确保文件句柄被归还
}

# 内容没问题的话就展开。如果目标目录中存在同名文件,这个双参数版本
# 会在展开到一半时抛出异常(不会覆盖)。应避免直接展开到已有文件夹,
# 更安全的做法是每次都展开到新文件夹后再切换
$dest = "C:\apps\web_$(Get-Date -Format yyyyMMddHHmmss)"
[System.IO.Compression.ZipFile]::ExtractToDirectory('C:\deploy\release.zip', $dest)

在 .NET Framework 中使用 ZipFile 类需要引用 System.IO.Compression.FileSystem 程序集,这一点在 PowerShell 5.1 中也一样。11 在 7 中,随附程序集会在需要时自动加载,因此不写 Add-Type 也能运行,但只要写上就能两边通用,共用脚本中显式声明会更稳妥。3

3. 用 Add-Type 嵌入自定义 C# 与 Win32 API(P/Invoke)

在「.NET 已有的功能」之后,就轮到「.NET 没有的功能」了。向 Add-Type 传入 C# 源代码,就会当场编译并把类型添加到会话中。此外,向 -MemberDefinition 传入带 DllImport 的签名,就能通过 P/Invoke 调用 Win32 API。这种用法在官方文档中也作为示例刊载,是正当的手段。3

# 用对话框通知手动运行的长时间处理已完成(通过 P/Invoke 调用 user32.dll 的 MessageBoxW)
$signature = @'
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
'@

$native = Add-Type -MemberDefinition $signature -Name 'NativeMethods' `
    -Namespace 'Win32' -PassThru

# 附加 MB_ICONINFORMATION(0x40) 后显示
[void]$native::MessageBoxW([IntPtr]::Zero, '备份处理已完成。', '长时间处理', 0x40)

有一点需要注意。模态对话框只适用于自己坐在桌面前的交互式执行场景。在任务计划程序的无人执行、Windows 服务等非交互式会话中,处理会卡在一个谁也无法关闭的对话框前,再也不会返回。无人任务的通知应该使用日志文件、事件日志、发送邮件等不需要等待响应的手段。

把同样是「通知处理完成」这件事,改写成面向无人任务的版本,就会是下面这样。用向事件日志写入一条记录来代替对话框,同时也保留一份日志文件。

# 无人任务版的通知: 不等待响应,留在事后可以追踪的地方
$source  = 'KomuraSoft.NightlyJob'   # 事件的源名称(任意字符串)
$logPath = 'C:\logs\nightly.log'

# 注册源需要管理员权限。在部署时执行一次即可,
# 夜间任务本体只在源已注册的前提下写入
#   New-EventLog -LogName Application -Source 'KomuraSoft.NightlyJob'

Write-EventLog -LogName Application -Source $source -EntryType Information `
    -EventId 1000 -Message '备份处理已完成。'
Add-Content -LiteralPath $logPath -Value "$(Get-Date -Format o) 备份处理已完成。"

在事件查看器 > Windows 日志 > Application 中,按源名称 KomuraSoft.NightlyJob 筛选即可追溯历史。如果环境中部署了监控工具,可以让它捕获这个事件 ID 并转发通知。另外需要说明,New-EventLog / Write-EventLog 是 Windows PowerShell 5.1 的 cmdlet,在 PowerShell 7 中已被删除(参见第 6 章的表格)。如果夜间任务运行在 PowerShell 7 上,比较直接的做法是把通知部分单独交给 powershell.exe 执行,或者干脆只记录到日志文件,把通知交给文件监控一侧处理(例如任务计划程序的事件触发或 Power Automate 等)。

Add-Type 在运维上有三个重要的限制。3

  • 添加的类型仅限于该会话有效。在其他会话或远程环境中,需要再次执行 Add-Type
  • 同名类型无法重新定义。当签名写错想要修正时,需要更换名称或启动新的会话。反复试错请在可随时丢弃的控制台中进行。
  • 在 PowerShell 7 中,如果同名类型已经存在,整个编译过程会被跳过。如果出现「明明改过了代码却没有生效」的情况,请怀疑这一点。

还有一点,是 P/Invoke 共通的注意事项。如果签名(参数类型、字符集、调用约定)写错,不仅会抛出异常,甚至可能导致整个进程崩溃。字符串与句柄封送处理的陷阱,已经在《在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)》中详细整理,建议在用 Add-Type 正式使用 Win32 API 之前先读一遍。

4. 调用 COM ── New-Object -ComObject 与 WSH 工具箱

比 .NET 更早就搭载在 Windows 中的一整套部件就是 COM。可以用 New-Object -ComObject <ProgId> 创建,VBScript 的 Set objShell = CreateObject("Shell.Application") 直接对应 $objShell = New-Object -ComObject Shell.Application4 WScript.Shell、WScript.Network、Scripting.FileSystemObject 等源自 WSH(Windows Script Host)的对象,同样可以这样使用。5 关于 COM 是什么这一基础话题,请参考《COM / ActiveX / OCX 是什么 - 区别与关系整理》。

一个具有代表性的实务技巧,就是批量创建快捷方式。快捷方式(.lnk)的创建并没有对应的 cmdlet,官方文档也提到「像创建快捷方式这类部分任务,使用 WSH 类会更简单」,并附上了 WScript.Shell 的示例。5

# 假设要把指向共享文件夹上工具的快捷方式,分发到所有终端的公共桌面
$shortcuts = Import-Csv 'C:\deploy\shortcuts.csv'   # Name, Target, Args 三列
$wsh = New-Object -ComObject WScript.Shell

foreach ($item in $shortcuts) {
    $lnkPath = Join-Path 'C:\Users\Public\Desktop' "$($item.Name).lnk"
    $lnk = $wsh.CreateShortcut($lnkPath)   # 如果已存在同名 .lnk,将成为覆盖对象
    $lnk.TargetPath = $item.Target
    $lnk.Arguments  = $item.Args
    $lnk.Save()
    Write-Host "已创建: $lnkPath"
}

COM 对象的成员可以通过 $wsh | Get-Member 查看。5 应用范围十分广泛,例如在 Outlook 中创建邮件草稿、通过 Shell.Application 操作特殊文件夹等。不过,像 Excel.Application 这样的 ActiveX 可执行文件(在独立进程中启动的 COM 服务器),会伴随下一章要讲的后续处理问题。官方文档也提醒,释放引用后进程是否会终止取决于对方的应用程序,使用前应先测试终止行为。5

5. COM 的后续处理 ── EXCEL.EXE 残留问题与「无人 Office 不受支持」

5.1. 为什么进程会残留

COM 对象的寿命通过引用计数管理。从 .NET(也就是 PowerShell)操作 COM 时,每个 COM 对象都会创建一个名为 RCW(Runtime Callable Wrapper)的代理,只要 RCW 存活,COM 一侧的引用就不会被释放。RCW 的回收交由垃圾回收负责。6 也就是说,即使调用了 $excel.Quit(),只要还有引用残留在某处(其中也包括像 $excel.Workbooks.Open(...) 这样没有存入变量的中间对象的 RCW),EXCEL.EXE 就不会终止。

接下来的代码把 try/finally 嵌套了三层,初次阅读时结构应该不太好理解。先说明为什么要这样写。说白了就是「越靠后的处理越绝对不能跳过」,因此按照不想跳过的优先顺序把它们逐层放到内侧而已。

  • 最外层的 try:操作 Excel 的主体处理。无论这里发生什么失败,都一定会进入后续的后续处理。
  • 第 1 层 finally(关闭工作簿):即使保存失败,也要去关闭已打开的工作簿。不过 Close 本身也可能失败。
  • 第 2 层 finally(执行 Quit:所以要在不会被 Close 的失败牵连的位置调用 Quit。如果 Excel 已进入无响应状态,Quit 也可能失败。
  • 第 3 层 finally(释放 RCW 并触发 GC):所以无论 Quit 成功与否,都要保证引用的释放一定被执行。这一步一旦被跳过,进程就会残留。

也就是说,写成三层并不是一种形式上的讲究,而是逐层消解「后续处理本身也可能失败」这一前提所得到的结果。反过来说,如果可以假设 CloseQuit 都不会失败,那么一个 finally 就够用了。

$excel = New-Object -ComObject Excel.Application
try {
    $excel.DisplayAlerts = $false
    $books = $excel.Workbooks              # 中间对象也接到变量里,以便之后能够释放
    $book  = $books.Open('C:\work\月度销售.xlsx')
    $sheet = $book.Worksheets.Item(1)
    $sheet.Cells.Item(1, 1).Value2 = "更新: $(Get-Date -Format 'yyyy-MM-dd')"
    $book.Save()
}
finally {
    # Close 失败的路径恰恰是 EXCEL.EXE 最容易残留的地方,
    # 因此用嵌套的 finally 确保 Quit 与释放一定会被执行
    try {
        if ($book) { $book.Close($false) }
    }
    finally {
        # 即使 Quit 失败(Excel 无响应、COM 服务器断开等),也一定要执行 RCW 的释放
        try {
            if ($excel) { $excel.Quit() }
        }
        finally {
            # 明确减少 RCW 的引用计数(按子到父的顺序)
            foreach ($obj in @($sheet, $book, $books, $excel)) {
                if ($obj) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($obj) }
            }
            # 让 GC 回收未能接入变量的 RCW 的保险措施
            [System.GC]::Collect()
            [System.GC]::WaitForPendingFinalizers()
            [System.GC]::Collect()
        }
    }
}

Marshal.ReleaseComObject 会减少 RCW 的引用计数,在归零的那一刻释放 COM 一侧的引用。不过官方文档强烈提醒,访问已释放的 RCW 会导致异常或访问违规,因此「只应在绝对必要时使用」。7

上面的代码之所以把中间对象逐一接到变量里,是遵循了俗称「两个点规则」的做法。如果像 $excel.Workbooks.Open(...) 这样连续使用两个以上的点,中间 Workbooks 对应的 RCW 就会在没有存入任何变量的情况下生成,从而失去释放手段。所以这条规则的内容就是每一行最多只用一个点,中间的对象一定要用变量接住。释放模式的流派(ReleaseComObject 派与 GC 派)、两个点规则的细节,以及即便如此仍然残留的进程的定位方法,已在《C# 操作 Excel 时 EXCEL.EXE 残留的问题 ── COM 引用释放模式与替换判断》中详细说明。虽然那是一篇 C# 的文章,但 RCW 的机制在 PowerShell 中同样适用。

5.2. 从根本上:不在无人执行中使用 Office

还有一个更根本的判断。Microsoft 官方明确表示,「目前不推荐,也不支持从无人值守、非交互式的客户端应用程序或组件(包括 ASP、ASP.NET、DCOM、NT 服务)自动化 Office」。这是因为 Office 的设计前提是交互式使用,在无人环境中可能表现出不稳定行为或死锁。8 官方还列举了诸如因意外弹出的对话框导致处理停止、基于 STA 而无法承受多重执行等具体问题。8 这里所说的 STA 是单线程单元(Single-Threaded Apartment)的缩写,是 COM 线程模型的一种。它把对某个对象的调用集中到创建该对象的那个线程上,逐一处理,Office 应用程序正是基于这一前提构建的。所以「在一台服务器上并行运行多份相同处理」这种用法,在结构上就是不合适的。COM 线程模型本身在《COM STA/MTA 基础 - 线程模型与避免 Hang 的思路》中有详细说明。

也就是说,从任务计划程序的夜间任务或服务中调用 Excel COM 的架构,从一开始「能运行」的那一刻起就已经是不受支持的。对于无人处理,官方推荐替换为不启动 Office 软件本体的 Open XML 系列文件直接编辑或 CSV 联动方案。8 PowerShell 中具体的替代技巧,已汇总在同时发布的《用 PowerShell 自动化 Excel・CSV 业务处理 ── 汇总・核对・报表输出的实务技巧》中。请把 Excel COM 的使用边界划定在「有人登录的桌面上,用于辅助人的工作」这一范围以内。

6. 5.1 与 7 中「可用的 .NET」并不相同

Windows PowerShell 5.1 构建在 .NET Framework 4.5 系列之上,PowerShell 7 构建在 .NET(原 .NET Core)之上。1 只要仅使用 cmdlet,需要意识到这种差异的场景有限,但一旦像本文这样开始直接调用 .NET,差异就会浮出水面。

论点 Windows PowerShell 5.1 PowerShell 7
底层基础 .NET Framework 4.5 系列1 .NET(7.4 对应 .NET 8.0 等,随版本更新)1
方法的重载 较少(例如: String.Split 有 6 种)1 较多(官方展示了同样调用 Split('pq') 结果不同的例子)1
程序集加载 许多场景需要用 Add-Type 显式加载 GAC 中的程序集3 随附程序集会在需要时自动加载3
已不可用的功能 *-EventLog 等部分 Windows 专用 cmdlet 已被删除1

「在 5.1 中能运行、在 7 中却不行」以及反过来的情况都可能发生。需要同时兼容两者的脚本,请务必在两种环境中分别测试。迁移判断的整体思路,已在同时发布的《Windows PowerShell 5.1 与 PowerShell 7 的区别 ── 企业内部脚本迁移实务指南》中说明。

7. 实务定式(判断表)

论点 选项(推荐项标注「推荐」) 判断标准
想做的处理没有对应的 cmdlet 放弃 / 直接调用 .NET 类(推荐) 首先查找 [类型]::方法。System.IO、System.Text、System.IO.Compression 附近可以填补大部分「差一步」的需求1
实例的创建 New-Object / [类型]::new()(推荐) 只需支持 5.0 及以后的话用 ::new()。可以看到构造函数列表,而且更快。只有 COM 只能用 New-Object -ComObject24
需要 Win32 API 手动忍耐 / 用 Add-Type 做 P/Invoke(推荐) 只有几个 API 的话 Add-Type 就足够了。签名写错会导致整个进程崩溃,所以要在可丢弃的会话中验证3
创建快捷方式等 WSH 来源的操作 COM(WScript.Shell)(推荐) 没有 cmdlet 的领域,COM 至今仍然现役。官方示例也采用这种方式5
辅助人工操作的 Excel 处理 COM + 确实的后续处理(推荐) 在 finally 中把 Quit・ReleaseComObject・GC 打包成一套。中间对象也要接到变量里76
无人任务中的 Excel/Office 处理 COM / 不启动 Office 的方式(推荐) 无人的 Office 自动化不受支持。应替换为 Open XML 系列或 CSV8
脚本长得太大 继续用 PowerShell 硬撑 / 改用 C# 工具化(推荐) 如果 Add-Type 的代码已达数百行、类型无法重新定义的限制妨碍了开发、或不想让分发目标每次都重新编译 ── 出现任意一种情况,就是该迁移的信号

最后一行,是本文的总结性判断。当用 Add-Type 嵌入的 C# 代码开始膨胀时,那就已经是「用 PowerShell 的外皮包裹着一个 C# 程序」的状态了。转移到能使用 Visual Studio 的类型检查与调试器、NuGet 包、单元测试的 C# 项目中,开发与维护都会更快。由于也存在从 C# 一侧调回 PowerShell 资产的方法,迁移并不意味着全部重写。《在 C#(CSharp)中执行 PowerShell 并以对象形式接收结果》介绍了桥接的模式。

8. 总结

  • PowerShell 构建在 .NET 之上,可以通过 [类型]::静态方法New-Object[类型]::new() 直接调用 .NET 类库。当 cmdlet 中没有所需功能时,.NET 是首选方案。
  • Add-Type 可以当场编译 C# 代码,传入 DllImport 签名后还能通过 P/Invoke 调用 Win32 API。类型仅限于该会话有效,无法重新定义同名类型。
  • COM 通过 New-Object -ComObject 操作。创建快捷方式等源自 WSH 的工具至今仍然现役。
  • COM 的寿命通过引用计数 + RCW 管理,只要引用残留,EXCEL.EXE 等进程就会残留。要在 finally 中编写包含 Quit・ReleaseComObject・GC 的完整后续处理。
  • Microsoft 明确表示,不推荐也不支持无人执行环境下的 Office 自动化。夜间任务应替换为不启动 Office 的方式。
  • 5.1 与 7 底层的 .NET 不同,可用的类型・重载也不同。需要同时兼容两者的脚本,必须在两边都进行测试。
  • Add-Type 一旦膨胀,就是该改用 C# 工具化的信号。脚本与可执行文件之间的桥接已有现成的模式可用。

相关文章

相关咨询领域

合同会社小村软件承接基于 PowerShell 的公司内部业务自动化、包含 COM 资产(Excel 联动・遗留部件)在内的脚本设计与改造,以及从「脚本长得太大」状态迁移到 C# 工具化的服务。也承接 EXCEL.EXE 残留这类故障排查工作。

参考链接

  1. Microsoft Learn,Differences between Windows PowerShell 5.1 and PowerShell 7.x。关于 Windows PowerShell 5.1 构建在 .NET Framework 4.5 之上、PowerShell 6.0 及以后构建在 .NET Core(现 .NET)之上、各版本所依赖的 .NET 版本一览、因 String.Split 重载差异导致同一段代码结果不同的例子,以及在 7 中被删除的 cmdlet。  2 3 4 5 6 7 8 9

  2. Microsoft Learn,about_Object_Creation。关于 PowerShell 5.0 为所有 .NET 类型新增的静态 new() 方法、通过输入 ::new 可以确认构造函数的重载列表,以及通过 cmdlet 获取的对象因 PowerShell 会追加 NoteProperty,可能与 ::new() 创建的对象在成员上不一致。  2 3 4

  3. Microsoft Learn,Add-Type。关于 Add-Type 会编译 C# 源代码并向会话添加类型、通过 MemberDefinition 实现 P/Invoke(调用 user32.dll 的 ShowWindowAsync)的官方示例、添加的类型仅限于该会话且同名类型无法重新定义、PowerShell 7 中若同名类型已存在则会跳过编译,以及 5.1 中加载 GAC 程序集需要 Add-Type 而 6 及以后会自动加载。  2 3 4 5 6 7 8

  4. Microsoft Learn,New-Object。关于 New-Object 会创建 .NET 或 COM 对象的实例、在 -ComObject 参数中指定 ProgId,以及 VBScript 的 CreateObject(“Shell.Application”) 对应 New-Object -ComObject “Shell.Application”。  2 3

  5. Microsoft Learn,Creating .NET and COM objects (New-Object)。关于创建 WScript.Shell 等 WSH 对象、文档中提到创建快捷方式使用 WSH 类会更简单以及 CreateShortcut 的实例、对 COM 对象应用 Get-Member、ActiveX 可执行文件的终止行为取决于应用程序需要事先测试,以及 New-Object 使用 .NET 的 RCW。  2 3 4 5 6

  6. Microsoft Learn,Runtime Callable Wrapper。关于 .NET 通过名为 RCW 的代理公开 COM 对象、每个 COM 对象会创建一个对应的 RCW,以及 RCW 在被垃圾回收时会释放对该 COM 对象的引用。  2 3

  7. Microsoft Learn,Marshal.ReleaseComObject(Object) Method。关于 ReleaseComObject 会减少 RCW 的引用计数,并在归零时释放底层的 COM 对象、释放后访问 RCW 会导致异常或访问违规、「只在绝对必要时使用」的官方提醒,以及与 FinalReleaseComObject 的关系。  2 3

  8. Microsoft Learn,Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment。关于 Microsoft 不推荐也不支持从无人值守、非交互式的客户端应用或组件(包括 ASP、ASP.NET、DCOM、NT 服务)自动化 Office、无人环境下不稳定行为与死锁的可能性、因对话框导致停止・源自 STA 的多重执行限制等具体问题,以及直接编辑 Open XML 文件格式作为推荐替代方案。  2 3 4 5

  9. Microsoft Learn,Expand-Archive。关于 Expand-Archive 使用 System.IO.Compression.ZipArchive API、该 API 存在最大 2GB 的文件大小限制,以及该 .NET API 处理的是符合 PKWARE 公司官方 ZIP 文件格式规范的文件。 

  10. Microsoft Learn,Compress-Archive。关于 Compress-Archive 使用 System.IO.Compression.ZipArchive API 进行压缩,文档以「The API limits the maximum file size to 2GB」明确写明 2GB 上限是底层 API 一侧的限制,并列出 ZipArchive 类 作为参考出处(由于 ZipArchive 类的参考文档正文中并未记载 2GB 这一具体数值,因此数值的第一手信息来源是这个 cmdlet 一侧的页面)。 

  11. Microsoft Learn,ZipFile Class。关于 ZipFile 类提供 ZIP 创建・展开・打开的静态方法、在 .NET Framework 中使用需要引用 System.IO.Compression.FileSystem 程序集,以及 CreateFromDirectory/ExtractToDirectory/OpenRead 各自的用途。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

PowerShell 能直接调用 .NET 的类吗?
可以。PowerShell 构建在 .NET 之上,因此可以像 [System.IO.Path]::GetFileNameWithoutExtension() 这样用方括号写出类型名,用 :: 调用静态方法。如果需要实例,可以用 New-Object,或者在 PowerShell 5.0 及以后使用 [类型]::new() 创建。即使某个操作没有对应的 cmdlet,只要 .NET 类库中存在,就能在不额外安装的情况下从脚本中直接调用。
New-Object 与 [类型]::new() 应该用哪一个?
两者都能创建对象,但 [类型]::new() 不带参数输入时会显示该类型的构造函数列表,适合一边查阅参数一边编写代码。在性能上 [类型]::new() 也更占优势。不过创建 COM 对象只能用 New-Object -ComObject。如果要兼顾比 PowerShell 5.0 更旧的环境,同样应使用 New-Object。
在 PowerShell 中通过 COM 操作 Excel,为什么 EXCEL.EXE 会残留?
COM 对象的寿命通过引用计数管理,在 .NET 一侧则由 RCW(Runtime Callable Wrapper)握有该引用。像 $excel.Workbooks.Open() 这样书写时,中间的 Workbooks 对象也会生成一个看不见的 RCW,留下没有存入变量的引用。即使调用了 Quit(),只要引用还残留着,进程就不会终止。使用结束后需要用 Marshal.ReleaseComObject 明确释放,或者把变量设为 null,再用 GC.Collect() 与 WaitForPendingFinalizers() 进行回收,做好后续处理。
可以在服务器或夜间批处理中通过 COM 操作 Excel 吗?
不推荐这样做。Microsoft 官方明确表示,不推荐也不支持从无人值守、非交互式的客户端应用或组件中自动化 Office 应用程序。Office 的设计前提是交互式的桌面使用,在无人执行环境下可能出现不稳定行为或死锁。无人处理的标准做法是替换为 Open XML 系列库或 CSV 联动等不启动 Office 软件本体的方式。
Add-Type 能调用 Win32 API 吗?
可以。向 Add-Type 的 -MemberDefinition 传入带 DllImport 的 C# 签名,就会当场编译,并可通过 P/Invoke 调用 Win32 API。官方文档中也刊载了调用 user32.dll 的 ShowWindowAsync 的示例。不过添加的类型会一直留在该会话中,同名类型无法重新定义,因此反复试错时请在新会话中重新执行。签名写错时甚至会导致整个进程崩溃,因此建议在可随时丢弃的控制台中进行验证。
Windows PowerShell 5.1 与 PowerShell 7 在调用 .NET 时有区别吗?
有区别。5.1 构建在 .NET Framework 4.5 系列之上,PowerShell 7 构建在 .NET(原 .NET Core)之上,可用的类型与方法重载并不相同。例如 String.Split 的重载在 7 中更多,官方也介绍了同一段代码结果不同的例子。此外,在 5.1 中许多场景需要用 Add-Type 显式加载 GAC 中的程序集,而在 7 中随附的程序集会在需要时自动加载。需要在两个环境中都运行的脚本,请务必在两边都进行测试。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表