首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 讀寫 INI 檔案完整指南:P/Invoke 封裝、IniFile 類別實作與現代替代方案

     編輯:關於C語言

前言:為什麼還在用 INI?

雖然 .NET 推薦 **XML (`app.config`/`appsettings.json`)**、`JSON`、資料庫儲存設定,但在**舊系統整合、跨語言共用設定 (C++/Delphi/VB6)、無需安裝的綠色軟體、極簡單鍵值對**等場景,INI 仍具備不可替代的優勢:人類可讀、無解析依賴、Windows 原生 API 支援。

核心原理:P/Invoke 呼叫 Kernel32.dll

Windows 提供兩個核心 API (在 `kernel32.dll`):

  • 寫入:`WritePrivateProfileString(section, key, value, filePath)`
  • 讀取字串:`GetPrivateProfileString(section, key, default, StringBuilder, size, filePath)`
  • 讀取整數:`GetPrivateProfileInt(section, key, default, filePath)`

所有操作**自動建立檔案/區段/鍵**,無需手動檢查存在性。

完整封裝類別:IniFile (現代化重構)

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`),專案設定 `x64` 亦需一致。
**緩衝區太小截斷**`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` 等型別轉換方法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved