在寫完那篇(不再是)近期的網頁開發文章後,我想親身體驗一下,如果不用 Electron 這類網頁技術,目前所有平台的「新開發者體驗」究竟是什麼樣子。

我接觸電腦數十年了,但從來不是個「App 開發者」,一直以來都以一種特立獨行的方式與電腦互動。雖然我曾用 WinForms 建立過 GUI 程式,但我並沒有積極跟進,這因此成了我技能上的空白!所以,讓我們來試著填補它!

這個專案始於一個即時的 Toot 串流,而這篇文章是對其的長篇總結與回顧。如果你想瀏覽程式碼,可以在這裡找到。

為了這次實驗,我選擇編寫一個程式來產生使用者指定範圍內的隨機數。這模擬了擲出不同面數、不同數量的骰子,就像用於 DnD 或其他 TTRPG 那樣,並延續了使用電腦玩遊戲的悠久傳統。

選擇像這樣「簡單」的東西背後的想法是,將我的注意力集中在每個平台的「工具設定」和「基本 UI 建構」功能上,而不是應用程式邏輯。

為了增加難度並更關注「平台整合」功能,我最終為 GUI 應用程式(即非命令列程式)增加了以下額外要求:

對於每個平台,我試圖使用最受推崇或我最容易找到的工具、技術和文件。可惜的是,程式設計師投入大量時間深入了解平台的日子已經一去不復返,但我盡力模擬了一個忙碌(但稱職)的程式設計師。

我按順序將此程式移植到了以下平台:

雖然我學到了很多,但體驗並不好。因此,我選擇為每個平台提供獨特的諷刺性總結,隨後我將詳細闡述:

為了鋪墊,我從僅使用標準 C 實現核心功能開始了這次實驗。這是我 20 多年前開始學習的程式語言,所以我假設(哈!)我不會從中學到任何東西。

這個初始程式足夠短,可以直接包含在這篇文章中:

如今,許多關於 C 的討論傾向於關注其許多(合法的)記憶體(不)安全、未定義行為和 ABI 地獄等問題,但這些對於像這樣的應用程式都不重要。儘管 C 有其缺陷,它完全有能力表示像上述程式那樣使用結構化控制流程並與使用者進行簡單文字互動的程式。

如果說有什麼值得一提的話,那就是計算機竟然發展到我可以編寫上述程式一次,然後在任何實現適當國際標準的電腦上運行它……這本身就是一項非凡的成就!如果上述程式被降級,僅依賴 C89 或 K&R 特性(留給讀者練習),它將可以在跨越數十年的電腦上運行,並且很可能在未來許多年裡繼續在電腦上運行。

然而,儘管 C 在作為一種能夠表達結構化控制流程並平衡無數相關方利益的標準化語言方面取得了巨大成功,但計算機需要的遠不止這些,首先是「使用者實際上如何在給定電腦上獲取或運行此程式?」

包括我在內的許多開發者經常使用「類 Unix」工具,因此一個合理的嘗試可能是讓這個骰子程式「像類 Unix」工具一樣工作。但這到底意味著什麼?你該怎麼做?

我還太年輕,無法經歷「Unix 大戰」以及供應商之間不必要的、不相容的差異,因此我缺乏很多歷史背景。然而,曾經有人嘗試將 Unix 行為標準化為一個名為 POSIX 的 IEEE 標準。這似乎是一個可以開始尋找的方向!

除了……一個剛開始的程式設計師今天該如何學習 POSIX?我碰巧已經知道了 POSIX,因為我多年來聽別人談論過它。我不確定一個新手程式設計師是否一定會遇到它,或者認為它相關,特別是考慮到 Unix 大戰以來已經過去的時間以及該標準存在的許多缺陷(正如我們很快將看到的)。

POSIX 的一個成功之處在於它將 c99 指定為 C 編譯器的標準可執行檔名,並為從原始碼建構軟體指定了一個可移植的 makefile 子集。標準化的子集足以編譯我們這樣簡單的應用程式:

(對於不了解的人來說,make 有各種內建規則,知道如何編譯 C 程式!你不必寫出明確的規則!)

給定一個只有一個 .c 檔案和一個 makefile 的原始碼發行版,一個熟悉「這是 Unix 系統!」的人應該能夠弄清楚輸入 make 和 ./dice 來編譯和運行此應用程式。

有沒有辦法讓這個程式「更融入」?POSIX 有一個標題為「Utility Conventions」的章節,提供了關於工具應如何運作的一般性指導方針。它對如何格式化「用法」訊息和命令列參數的描述非常有用!然而,我們很快就會遇到一些未能標準化或記錄下來的東西。

許多「類 Unix」工具的一個慣例是,它們在正常情況下通常輸出很少甚至沒有輸出,並且它們通常以「一次性」或「批次」模式運行,而不是像「標準 C」範例那樣與使用者互動。這樣做有原因(例如方便建構 shell 腳本和管道),但這是幾年前我最初必須「口頭」學到的想法,而不是通過更正式的書面方式。

當我們嘗試重構程式碼以這種方式工作時,一個問題出現了——當出現錯誤(例如無效的骰子數量)時,我們該怎麼辦?我使用的許多工具似乎都有報告錯誤的常見慣例(即在打印實際錯誤訊息之前打印程式名稱),但這個慣例到底是什麼?原來這最初來自 GNU 編碼標準!我直到這次實驗才知道這一點,而我不知道這一點並不反映 GNU 計畫的成功。

在我完成將程式碼修改為「類 Unix」「一次性」模式後,我在測試過程中遇到了一個問題(標準 C 實現也有,但隱藏得更好)——它生成的數字停止隨機,僅在 macOS 上如此。原來 rand 的底層實現並沒有真正指定,而且 macOS 的實現直到你生成至少一個隨機數之前,並不會很好地混合種子的位元。

POSIX 以 rand48 系列函數的形式指定了不同的偽隨機數生成器。這具有明確定義的行為,但它仍然不是一個「好的」隨機數生成器。這對於玩 TTRPG 可能無關緊要,但當賭注變高時,它可能會成為一個問題。

不幸的是,「好的」隨機數生成器,如 getrandom(2) 或 /dev/urandom,實際上並未在 POSIX 中指定(儘管它們相當廣泛可用)。

2026 更新:一位訪客指出 POSIX.1-2024 已將 getentropy 標準化,終於解決了這個特定的痛點。

這是標準化進程緩慢回應的一個巨大問題!它們有變得越來越不相關的風險,而它們曾希望解決的問題(例如可移植性)又再次成為每個人的問題。這不僅適用於 POSIX,也適用於所有事物,包括 C 緩慢的現代化努力。

從這次實驗中,我對 Julia Evans 這樣持續傳播關於作業系統和開發者環境如何運作的「基本知識」的人們有了新的認識。

我選擇測試的下一個平台是 GNOME,因為它是 Ubuntu 和 Fedora 等流行 Linux 發行版的預設桌面環境。至少在我看來,Linux 上的 GNOME 或 KDE 與「自由及/或開源軟體」運動最相關(抱歉,Hurd、各種 BSD 版本或「小眾」桌面環境如 LXDE、Cinnamon 和 MATE 的粉絲)。

GNOME 有一個相當不錯的開發者入門頁面,列出了多種可與之搭配使用的程式語言。由於我想嘗試「低階、原生」體驗,我閱讀了有關使用 C 入門的文件。透過瀏覽文件,我也很快找到了 GNOME 的人機介面指南。建議的工作流程是使用 GtkBuilder XML 建構 UI,並使用 Gnome Builder 作為 IDE,透過 Flatpak SDK 編譯應用程式。總體而言,GNOME 感覺應該是一個不錯的開發環境!

不幸的是,一旦我真正開始,開發 GNOME 軟體的體驗最終變得極其令人沮喪。文件(以及整個專案)感覺「不完整」——並非因為任何特定的程式碼物件缺乏文件,而是因為現有的文件並沒有真正成功地建立起心智模型和概念理解。

例如,入門文件幾乎沒有提及 GLib 或 GObject 系統,而該系統是 GNOME 和 GTK 的基礎。理解這一點極其重要,因為一個可從 C 程式碼以及更高階語言使用的跨語言物件系統涉及大量複雜性,並且該系統在響應使用者動作時會被涉及。事實上,GObject 在概念上感覺如此文件不足,以至於在這次實驗之前,我甚至不知道 GObject 是什麼或做什麼(儘管我使用 GNOME 軟體),而且我完全不知道 GNOME 專案擁有如此強大的功能!

繼續關於「概念理解」的觀點,我仍然不理解,例如,為什麼應用程式的某些部分使用「動作」來處理事件,而對話方塊卻必須使用「訊號」而不是(「動作」和「訊號」也有完全不同的指定處理器函數的機制,所以它們一點也不可互換!)。

GNOME 生態系統的除錯體驗一再導致「什麼都沒發生(但應該發生了),現在我不知道為什麼」的情況。例如,在 g_simple_action_new 和 GtkBuilder XML 中錯配類型,導致選單項目變灰且禁用,而我不知道是我 XML 寫錯了,還是忘了在某處設定「啟用」標誌,或是完全犯了其他錯誤。同樣,嘗試設定翻譯一再導致「就是載入不了翻譯」,而我不知道在建構過程中錯過了哪個步驟。(據我所知,在 Flatpak SDK 的「最新」版本(預設版本)中,翻譯根本無法正常工作。選擇一個不同的版本,例如「48」,就能奇蹟般地工作。)

持久設定是使用 GSettings 儲存的,這是一種根據結構描述儲存類型資訊的機制。理論上,這允許桌面環境的其他任意部分與給定應用程式的設定進行互動和理解。實際上,由於某些與 Flatpak 相關的原因,這並沒有奏效(例如命令列工具實際上找不到設定),事實上,與 GSettings 整合後,就無法在本地啟動程式進行開發了(它會在啟動時崩潰,因為結構描述沒有正確「安裝」,儘管在建構 Flatpak 時,沒有「安裝」結構描述也能奇蹟般地工作)。再一次,如果文件能更好地解釋背景、「為什麼?」以及整體架構和願景,這會容易得多。

總體而言,我真的很希望 GNOME 表現良好!不幸的是,以其目前的狀態,它有點不行。在這次實驗之後,我從其他人那裡聽到的反覆意見是,GNOME 一直未能傾聽、溝通並重視來自重度使用者和其他「外部人士」的貢獻。我認為,這確實顯現出來了,因為我遇到的許多困難很可能已經被常規 GNOME 開發者充分理解和內化了。

如前所述,另一個「主要」的 Linux 桌面環境是 KDE,所以我接下來嘗試了它。KDE 的開發者文件指向我使用 Kirigami 框架,可選擇 C++ 或 Python,我選擇了 C++。

與 GNOME 相比,KDE 的文件感覺有些雜亂無章,但它是「任務」導向的,並且很適合我偏好的工作方式(也就是說,我沒有抱怨,但其他開發者可能會)。

KDE 的教學不幸地直接遇到了 GNOME 透過推薦 Flatpak 所避免的更廣泛的 C++ 平台問題。教學建議使用 CMake 來定位其函式庫路徑並編譯你的應用程式,但 CMake 在提供無益且令人困惑的錯誤訊息方面有點名聲。我在開發 KDE/Qt 應用程式時遇到的絕大多數挫折都是由 CMake 引起的,而不是由 UI 框架本身引起的,問題包括:

一旦我解決了(或忽略了)所有這些錯誤,使用 Qt 和 QML 開發軟體就變得相當舒適和直接。我在 GLib/GObject 中掙扎過的細節在 Qt 的訊號和槽文件中得到了很好的解釋。雖然我選擇完全擁抱 KDE 框架,但 KDE 感覺上比較不主觀,並且更開放地進行零散採用(例如,許多 KDE 應用程式不使用 Kirigami,我必須特別選擇使用 KConfig)。

無論好壞,我相信 KDE 在 Qt 被 Linux 桌面生態系統以外的「ISV」(獨立軟體供應商)廣泛使用(例如在工業 HMI 和汽車資訊娛樂系統中)方面受益匪淺。這種商業興趣和商業™可能為 Qt 提供了大量機會、需求和資金,用於記錄創建 UI 和程式碼之間綁定所需的細節。

KDE 和 Qt 在錯誤方面傾向於更響亮,通常在出錯時至少會在終端機列印一些東西。在一個案例中,錯誤甚至透過將一個「醜陋」的錯誤訊息直接傾印到 UI 文字中來報告。這種行為大大減少了「什麼都沒發生」的除錯挫折。

持久設定是使用 KConfig 儲存的。與 GNOME 不同,這不需要設定結構描述。除了 CMake 的困惑之外,儲存和檢索單一值非常簡單。事實上,KDE 的文件是我最終了解 XDG Base Directory Specification 存在的途徑,我以前從未聽說過它!

總體而言,我相當喜歡 KDE 的「搞定事情」的氛圍,但我確實希望「Linux 原生程式碼」的情況能更好一些。

許多 F/OSS 軟體的翻譯和本地化似乎依賴於 gettext。在不評論其 API 的情況下(我不是本地化專家),我只想說,圍繞這個主題的文化知識嚴重不足。

GNOME 的文件對於任何沒有實際使用過 *nix 本地化工具並且沒有聽說過 gettext 的人來說(也就是我,在進行這次實驗之前)是完全無用的,我花了很長時間才弄清楚所需的相關設定工作是由建構系統 Meson 文件記錄的,而不是由 GNOME 文件記錄的。

另一方面,KDE 的文件不知何故被埋藏在建構 Plasma 小工具的教學中(而不是放在更通用的地方),或者在前面提到的其他 wiki 上。

作為一個懂多國語言的人(儘管我所有的技術工作都用英語進行),在這次實驗之後,我將會對 i18n/l10n 大聲疾呼。

為了節省時間,在嘗試了 GNOME 和 KDE 之後,我從 F/OSS 桌面轉向嘗試微軟的 WinUI 3 和 C++/WinRT。如前所述,我過去的 GUI 經驗是 WinForms,所以我想看看這些年來發生了什麼變化或改進。

我立即注意到並欣賞 XAML響應式佈局相較於舊版 Win32 的固定像素或 DLU(對話方塊單位)佈局的優勢。隨著螢幕尺寸變得越來越多樣化,這種自動調整大小的概念幾乎被所有 UI 工具包普遍採用。(本次實驗中的其他所有 GUI 工具包都有自動佈局。這之所以值得注意,僅僅是因為我之前有使用 Windows 上舊式固定佈局工具包的經驗。)

與我之前嘗試過的框架不同,WinUI 和 C++/WinRT 的建構過程也能捕捉並防止許多可能因「字串類型」UI 建構器而發生的錯誤。Visual Studio 的除錯功能(例如 XAML 即時預覽器)也相當好用。

總體而言,這是微軟的一大進步!

然而,對微軟來說的進步,意味著它仍然是微軟,而整個「剩下的貓頭鷹」是一個巨大的品牌混亂、笨拙和「我為什麼要費心去學這個?」的爛攤子。

我經常難以分辨 WPF、UWP / WinUI 2 和 WinUI 3(尤其是後兩者)之間的區別。這些框架的文件之間沒有明確的區分,我曾一度意外地使用了 Windows.UI.Xaml.Data 而不是 Microsoft.UI.Xaml.Data,導致了一個難以理解的 Windows 執行緒相關例外。

儘管我在效能較差的硬體上運行,但這個框架的建構管線很慢,主觀上是所有測試平台中最慢的。它肯定涉及最多的步驟,包括處理 IDL 和 XAML 文件,然後編譯一些「現代」的、模板繁重的 C++。

C++ 並不像 WinRT 的首選「語言投影」,而且微軟的許多開發者資源似乎鼓勵使用 C#。

持久設定是使用 Windows::Storage::ApplicationData 儲存的,其中包含幾個令人困惑的不同位置用於儲存鍵值對。一旦我選擇了合適的位置(LocalSettings),將值放入其中就變得簡單了。

總體而言,WinUI 3 感覺像是一種「還能用吧?」的技術,既適合也體現了微軟的特點。它不是最新、最花哨的技術,但它本來就不是。它確實可能幫助「商業™軟體™」更好地工作。然而,特別是考慮到 Windows 11 不斷增加的對使用者不友善的功能時,一個新的開發者為什麼或應該費心去投資微軟的生態系統,這根本不清楚。

SwiftUI 是我用過的第一个「萬物皆程式碼」的聲明式 UI 框架,而 Apple 的開發者文件非常出色,再次完美契合我個人「以任務和範例為導向」的思維方式。

SwiftUI 顯然是 Apple 建立高度垂直整合、主觀意見強烈的圍牆花園風格的產物。例如,SwiftUI 和 Xcode 擁有非常強大的即時預覽功能,其實現簡直是魔法。

這種高度緊密的整合造就了玩起來很有趣的東西。不幸的是,這樣做需要使用 Apple 硬體、Apple 軟體、Apple 程式語言,以及 Apple 對「App」應該能做什麼的看法。這似乎導致了許多「App」相當簡化、「千篇一律」,並且是電腦潛力的零頭。

Apple 也以「快速變動」聞名,我也遇到了不少。我有些軟體更新較慢,因此無法使用 SwiftUI NavigationStack,不得不自行模擬。Apple 開發者生態系統似乎並不重視向前或向後相容性——這與標準 C 範例甚至早期 Apple 時代形成了鮮明對比。

使用 @AppStorage 屬性設定持久設定非常容易。與 Apple 的其他一切一樣,只要你不質疑資料是如何或在哪裡儲存的,它就能神奇地工作。

總體而言,我非常享受在這裡有限的開發者體驗時間,我可以理解為什麼至少有些人非常喜歡 Apple 生態系統,無論是作為使用者還是開發者。不幸的是,作為一個同樣非常重視 F/OSS 運動某些理念的人,我無法完全投入 Apple,而且我可以看到這個生態系統有多麼受限,以及在其他平台上重複使用任何東西會有多麼困難。

在這個實驗階段,疲勞感開始襲來,但我仍然必須嘗試世界上最受歡迎的作業系統——Android,其推廣的 UI 框架是 Jetpack Compose。

坦白說,Jetpack Compose 感覺就像一個非常沒有靈魂的「我希望它是 SwiftUI」。文件和教學是一個可怕的混亂,過於簡化到極致,而且一點也不普遍適用或有用。

Android 的文件分散在 API 參考、俗氣的行銷影片、過於簡化的「程式碼實驗室」、類似 Apple 的「教學」和「快速指南」之間。所有這些感覺上幾乎是故意迴避解釋「如何從頭到尾建構一個完整的應用程式」。這還沒觸及低階 API、Jetpack Compose 之前的 API 等等。儘管身為搜尋巨頭,Google 搜尋在遇到錯誤時經常找不到有用的資源。

在煩人的地方,Android Studio 在處理裝置和模擬器時似乎有點不穩定,嘗試除錯在應用程式啟動完成前發生的例外似乎根本不起作用。

持久設定在 Android 上是最困難的,因為與所有其他平台不同,androidx.datastore.core.DataStore 是非同步的。我從未能完全弄清楚這應該如何正確整合到 Jetpack Compose 模型中,最終我放棄了,並使用了 runBlocking 將其轉換為同步介面。

Jetpack Compose 框架本身似乎……還可以。然而,它的一切感覺都非常粗糙,對於一個由世界上最大科技公司之一發布的產品來說,這簡直令人難以置信。

這幾乎是對開發者時間的不尊重,是那些知道自己處於無可匹敵的壟斷地位的公司所產生的產品。

即使體驗有點糟糕,我也學到了很多。我將花費大量時間鑽研我一直忽略的各種事物。

如果你真的想建立一個原生的跨平台應用程式,我個人會推薦 Qt(儘管我不會使用 CMake)。