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 的簽章,也能透過 P/Invoke 呼叫 Win32 API。新增的型別無法在工作階段中刪除,同名的型別也無法重新定義。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
  • Microsoft 明確表示對無人執行下的 Office 自動化「不建議也不支援」。從服務、工作排程器、伺服器端進行 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()  # 確保釋放檔案控制代碼
}

# 內容沒問題的話就展開。展開目的地若有同名檔案,這個2個引數版本
# 會在展開到一半時擲出例外(不會覆寫)。應避免直接展開到既有資料夾,
# 每次都展開到新資料夾後再切換過去比較安全
$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 中沒有的功能」。把 C# 原始碼傳給 Add-Type,就會當場編譯,並把型別新增到工作階段中。此外,只要把帶有 DllImport 的簽章傳給 -MemberDefinition,就能透過 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-EventLogWindows PowerShell 5.1 的 Cmdlet,在 PowerShell 7 中已被移除(參見第 6 章的表格)。若夜間作業要在 PowerShell 7 上執行,比較直接的做法是:只把通知部分丟給 powershell.exe 處理,或是集中在記錄檔寫入,再由檔案監控端(如工作排程器的事件觸發或 Power Automate)負責通知。

Add-Type 有 3 個維運上重要的限制。3

  • 新增的型別僅限於該工作階段。在其他工作階段或遠端執行時,需要再次執行 Add-Type
  • 同名的型別無法重新定義。若簽章寫錯想要修正,需要換個名稱或啟動新的工作階段。請在用完即丟的主控台中進行嘗試錯誤。
  • 在 PowerShell 7 中,若同名的型別已經存在,會直接略過編譯本身。當「明明已經修好的程式碼卻沒有反映出來」時,請懷疑這一點。

還有一點,是 P/Invoke 共通的注意事項。簽章(引數型別、字元集、呼叫慣例)如果寫錯,有時不只是丟出例外,甚至會連處理程序一起當掉。字串與控制代碼封送處理(Marshaling)的陷阱,在「在 C# 中安全呼叫 Win32 API — P/Invoke 實務指南(DllImport / LibraryImport / CsWin32)」中有詳細整理,建議在用 Add-Type 正式使用 Win32 API 之前先讀一遍。

4. 呼叫 COM ── New-Object -ComObject 與 WSH 的工具箱

COM 是比 .NET 更早內建在 Windows 中的一群元件。可以用 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 這3個欄位
$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 的主要處理。無論這裡發生什麼失敗,之後的後續處理都一定會執行。
  • 第一層 finally(關閉活頁簿):即使儲存失敗,也要去關閉已開啟的活頁簿。不過 Close 本身也可能失敗。
  • 第二層 finally(執行 Quit:所以在不受 Close 失敗牽連的位置呼叫 Quit。若 Excel 已經無回應,Quit 也可能失敗。
  • 第三層 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
組件載入 GAC 中的組件在許多情況下需要用 Add-Type 明確載入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 -ComObject 這一種選擇24
需要 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 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽