每隔幾週我就會需要一個二十行左右的小程式:找出建置代理伺服器磁碟空間被什麼佔滿、對 CSV 檔案去重複、對資料夾進行雜湊檢查。十五年來,「用哪種語言?」最誠實的答案從來不是 C#——因為我得先執行 mkdir、dotnet new console,再為這個一次性的專案取名,當我做完這些動作,原本想解決的問題早就過了。所以這些小任務都交給 bash 或 Python,我每次都暗自抱怨。
.NET 10 免除了這些步驟。你只要寫一個 .cs 檔案就能執行。我一直想確認這在真實的指令碼情境中表現如何,所以這週我實際測試了——沒什麼特別的,只用一個 Linux 容器與一個碼錶。
單一檔案,無需專案
這裡是 biggest.cs,一個列出目錄下最大檔案的小工具。整個程式就只有這一個檔案——完全沒有 csproj:
#!/usr/bin/env dotnet
#:package Humanizer@3.0.10
using Humanizer;
var root = args.Length > 0 ? args[0] : ".";
var top = args.Length > 1 && int.TryParse(args[1], out var n) ? n : 10;
var files = new DirectoryInfo(root)
.EnumerateFiles("*", new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint
})
.OrderByDescending(f => f.Length)
.Take(top)
.ToList();
foreach (var f in files)
{
var size = f.Length.Bytes().Humanize("#.#");
var age = (DateTime.UtcNow - f.LastWriteTimeUtc).Humanize();
Console.WriteLine($"{size,10} {f.FullName} (modified {age} ago)");
}
Enter fullscreen mode Exit fullscreen mode
其中有兩行是新的。#:package [email protected] 是一個以指令形式寫在原始碼裡的 NuGet 參考。shebang 我們稍後再談。其餘部分就是你平常寫的 C#,包含最上層陳述式等。
$ dotnet run biggest.cs -- ~/.dotnet 5
Top 5 files under /root/.dotnet:
37.6 MB .../FSharp.Compiler.Service.dll (modified 46 seconds ago)
18.7 MB .../Microsoft.CodeAnalysis.CSharp.dll (modified 46 seconds ago)
18.7 MB .../Roslyn/bincore/Microsoft.CodeAnalysis.CSharp.dll (modified 45 seconds ago)
14.9 MB .../System.Private.CoreLib.dll (modified 45 seconds ago)
11.4 MB .../native/singlefilehost (modified 47 seconds ago)
Total: 101.1 MB
Enter fullscreen mode Exit fullscreen mode
沒錯,.NET SDK 裡最大的檔案是 F# 編譯器。我自己覺得這件事特別有趣。
碼錶測試
最明顯的疑慮是:這是否像 Python 一樣能快速迭代?還是每次執行都要付出編譯成本?我用 SDK 10.0.302 在 Linux 容器中進行計時——這不是實驗室環境,我在意的是比例關係,而暖啟動的數據取三次中的最佳值。
第一次執行花了 10.5 秒。這是設計上的最壞情況:它會還原 Humanizer、進行編譯,還會經過 CLI 首次執行的歡迎訊息。之後,未修改的指令碼大約能在 0.3 秒內執行,修改後立即重新執行則略低於半秒。從儲存到輸出只需半秒,這已經完全符合指令碼的使用情境。我幾乎感覺不到編譯器的存在,而這正是重點。
我還確認了另一件事:我的資料夾裡仍然只有一個檔案。沒有 bin/、沒有 obj/。建置產物會被放在 ~/.local/share/dotnet/runfile/ 底下,每個指令碼各有一個以內容雜湊命名的資料夾,因此指令碼所在的目錄會和 bash 指令碼一樣乾淨。如果你不喜歡打 run,也可以直接使用 dotnet biggest.cs。
chmod +x,順理成章
那行 shebang 不是裝飾:
$ chmod +x biggest.cs
$ ./biggest.cs /tmp 3
22.3 MB /tmp/phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 (modified 12 weeks ago)
...
Enter fullscreen mode Exit fullscreen mode
一個 C# 檔案就像是一等公民的 shell 指令碼,可以用在 cron 與 CI 中。我的看法是:只要超過一個管線與一個 grep 的工作,我現在寧願用這個,而不願意繼續維護一堆 bash。bash 的字串處理常常讓我浪費週末,而這些指令碼只要加上 #:package,就能使用真正的 JSON 解析器或 HTTP 用戶端。
限制在哪裡
至少在我測試的 SDK 上,這確實是單一檔案。我在指令碼旁邊放了一個 util.cs,裡面有 Helper 類別,結果得到 error CS0103: The name 'Helper' does not exist in the current context。執行單一檔案時,只會編譯該檔案,僅此而已。
我覺得這種直白的設計其實不錯,因為當你想要第二個檔案時,就代表你的指令碼已經不再是「指令碼」了。升級的路徑只需要一個指令:
$ dotnet project convert biggest.cs
Enter fullscreen mode Exit fullscreen mode
這會產生一個 biggest/ 資料夾,包含原始碼與正式的 csproj,shebang 與指令會被移除,#:package 也會轉成真正的 PackageReference。有趣的細節是,產生的專案會自動設定 PublishAot 與 PackAsTool 為 true——SDK 對你的指令碼未來想成長為什麼有強烈的意見。
不適合使用的情境:需要與同事共同維護、需要測試、或想法多到超過一個檔案以上。另外要注意冷啟動——第一次執行需要 10 秒的還原時間,這表示短暫存在的 CI 容器每次都會付出成本,除非你快取 NuGet 資料夾。
但對於二十行左右的小任務?我已經不再伸手去用 Python。一次十秒,之後每次只要三分之一秒,而且用的是我真正習慣思考的語言。
完整可執行的範例:https://github.com/ssukhpinder/dev-to-code-samples/tree/main/016-file-based-csharp
你曾經為了多小的任務而建立過一個完整的方案?我的紀錄是三行程式碼,卻還建了一個 .sln 檔案,到現在還覺得很丟臉。歡迎在留言區分享你的故事。
—— 持續在為沒人要求的事情計時
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.