雖然 .NET 推薦 **XML (`app.config`/`appsettings.json`)**、`JSON`、資料庫儲存設定,但在**舊系統整合、跨語言共用設定 (C++/Delphi/VB6)、無需安裝的綠色軟體、極簡單鍵值對**等場景,INI 仍具備不可替代的優勢:人類可讀、無解析依賴、Windows 原生 API 支援。
Windows 提供兩個核心 API (在 `kernel32.dll`):
所有操作**自動建立檔案/區段/鍵**,無需手動檢查存在性。
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
public class IniFile : IDisposable
{
// P/Invoke 宣告
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WritePrivateProfileString(string section, string key, string value, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPrivateProfileString(string section, string key, string defaultValue, StringBuilder returnValue, int size, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPrivateProfileInt(string section, string key, int defaultValue, string filePath);
private readonly string _filePath;
private bool _disposed;
public IniFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentException("檔案路徑不可為空");
_filePath = Path.GetFullPath(filePath);
// 確保目錄存在
Directory.CreateDirectory(Path.GetDirectoryName(_filePath));
}
// 寫入字串
public bool Write(string section, string key, string value)
{
ThrowIfDisposed();
if (section == null) throw new ArgumentNullException(nameof(section));
if (key == null) throw new ArgumentNullException(nameof(key));
return WritePrivateProfileString(section, key, value ?? "", _filePath);
}
// 讀取字串
public string Read(string section, string key, string defaultValue = "")
{
ThrowIfDisposed();
var sb = new StringBuilder(1024); // 足夠大緩衝區
int len = GetPrivateProfileString(section, key, defaultValue, sb, sb.Capacity, _filePath);
return sb.ToString(0, len);
}
// 讀取整數
public int ReadInt(string section, string key, int defaultValue = 0)
{
ThrowIfDisposed();
return GetPrivateProfileInt(section, key, defaultValue, _filePath);
}
// 讀取布林 (擴充)
public bool ReadBool(string section, string key, bool defaultValue = false)
{
string val = Read(section, key, defaultValue ? "1" : "0");
return val.Equals("1", StringComparison.OrdinalIgnoreCase) ||
val.Equals("true", StringComparison.OrdinalIgnoreCase);
}
// 寫入布林
public bool WriteBool(string section, string key, bool value) => Write(section, key, value ? "1" : "0");
// 刪除 Key
public bool DeleteKey(string section, string key) => WritePrivateProfileString(section, key, null, _filePath);
// 刪除 Section
public bool DeleteSection(string section) => WritePrivateProfileString(section, null, null, _filePath);
// 列舉所有 Section
public string[] GetSections()
{
ThrowIfDisposed();
var sb = new StringBuilder(8192);
int len = GetPrivateProfileString(null, null, "", sb, sb.Capacity, _filePath);
// 回傳格式: "Section1\0Section2\0\0"
return sb.ToString(0, len).Split('\0', StringSplitOptions.RemoveEmptyEntries);
}
// 列舉 Section 下所有 Key
public string[] GetKeys(string section)
{
ThrowIfDisposed();
var sb = new StringBuilder(8192);
int len = GetPrivateProfileString(section, null, "", sb, sb.Capacity, _filePath);
return sb.ToString(0, len).Split('\0', StringSplitOptions.RemoveEmptyEntries);
}
private void ThrowIfDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(IniFile));
}
public void Dispose()
{
if (!_disposed) _disposed = true;
}
}
string iniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.ini");
using (var ini = new IniFile(iniPath))
{
// 寫入
ini.Write("Database", "Server", "127.0.0.1");
ini.Write("Database", "Port", "3306");
ini.Write("Database", "UseSSL", "true");
ini.Write("UI", "Theme", "Dark");
ini.WriteBool("UI", "AutoSave", true);
// 讀取
string server = ini.Read("Database", "Server", "localhost");
int port = ini.ReadInt("Database", "Port", 3306);
bool useSsl = ini.ReadBool("Database", "UseSSL", false);
string theme = ini.Read("UI", "Theme", "Light");
bool autoSave = ini.ReadBool("UI", "AutoSave", false);
Console.WriteLine($"Server={server}, Port={port}, SSL={useSsl}, Theme={theme}, AutoSave={autoSave}");
// 列舉
foreach (var section in ini.GetSections())
{
Console.WriteLine($"[{section}]");
foreach (var key in ini.GetKeys(section))
{
Console.WriteLine($" {key}={ini.Read(section, key)}");
}
}
}
| 問題 | 解決 |
|---|---|
| **中文亂碼** | `CharSet = CharSet.Unicode` (對應 `WritePrivateProfileStringW`),專案設定 ` |
| **緩衝區太小截斷** | `StringBuilder` 容量設 1024~8192,長值自動擴充 (或先呼叫取得長度再配置)。 |
| **相對路徑在服務/排程任務失效** | 建構式用 `Path.GetFullPath` 解析為絕對路徑,或傳入絕對路徑。 |
| **多執行緒並發寫入** | Kernel32 API **有內部鎖**,單檔案多執行緒安全;但建議應用層加 `lock` 避免邏輯競態。 |
| **INI 檔案超過 64KB 限制** | 舊版 API 有 64KB 上限,現代 Windows 已移除;大量設定改用 JSON/XML。 |
| **權限問題 (Program Files 寫入失敗)** | 設定檔放在 `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)` 或 `CommonApplicationData`。 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| `appsettings.json` + `IConfiguration` | 強型別綁定、多環境覆蓋、DI 整合、標準化 | 需 .NET Core/5+、JSON 格式 | **現代 .NET 應用首選** |
| `Settings.settings` (專案屬性) | 設計器強型別、使用者/應用範圍分離、自動序列化 | WinForms/WPF 專用、版本升級遷移麻煩 | 傳統桌面應用快速開發 |
| SQLite / LocalDB | 查詢靈活、ACID、大量資料 | 部署較重、需 ADO.NET/EF Core | 結構化設定、歷史記錄 |
| Registry | 系統級、權限隔離、無檔案管理 | 跨平台差、寫入需高權限、難備份 | 系統整合、授權資訊 |
| INI (本文方案) | 跨語言、人類可讀、無依賴、綠色軟體友好 | 型別弱、無巢狀、大量資料效能差 | **舊系統整合、跨語言共用、極簡設定** |
透過 **P/Invoke 封裝 `kernel32.dll`** 實作 `IniFile` 類別,提供**型別安全、資源管理、列舉支援、編碼正確**的現代化 INI 讀寫體驗。但在**新專案中優先選擇 `appsettings.json` + `IConfiguration`**,僅在**跨語言相容、綠色軟體、極簡部署**等剛性需求下使用 INI。封裝良好的 `IniFile` 可直接複製到專案中即用,並易於擴充 `ReadDouble`、`ReadDateTime` 等型別轉換方法。