系列最終篇:彙整核心知識點、發布生產級封裝類別、提供進階架構路線圖 (強型別繫結、配置提供者整合、跨平台替代方案),助你在實際專案中優雅、安全、高效地使用 INI 檔案。
| 篇章 | 核心內容 | 關鍵技術 |
|---|---|---|
| 一 | 核心 API 與 P/Invoke 宣告 | WritePrivateProfileString/GetPrivateProfileString、CharSet.Unicode、SetLastError |
| 二 | WinForms 交互流程與按鈕邏輯 | OpenFileDialog、驗證層、緩衝區擴容、預設值語義 |
| 三 | 設計器代碼完整版 | InitializeComponent、控制項實例化、版面配置、事件綁定 |
| 四 | 按鈕事件完整邏輯 | 空值語義、狀態回饋 UX、Win32Exception 分層處理 |
| 五 (本篇) | 生產級封裝與架構演進 | IniFile 類別、強型別 Bind、IConfigurationProvider、跨平台方案 |
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
public sealed class IniFile : IDisposable
{
private readonly string _path;
private readonly object _sync = new object();
private bool _disposed;
// 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 uint GetPrivateProfileString(string section, string key, string defaultValue, StringBuilder retVal, uint size, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPrivateProfileInt(string section, string key, int defaultValue, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetPrivateProfileSection(string section, IntPtr keyValueBuffer, uint size, string filePath);
public IniFile(string path)
{
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("路徑不可為空", nameof(path));
_path = Path.GetFullPath(path);
// 確保目錄存在
Directory.CreateDirectory(Path.GetDirectoryName(_path));
}
// === 寫入 ===
public void Write(string section, string key, string value)
{
CheckDisposed();
ValidateSectionKey(section, key);
lock (_sync)
{
if (!WritePrivateProfileString(section, key, value ?? "", _path))
ThrowWin32Error("WritePrivateProfileString");
}
}
public void Write(string section, string key, int value) => Write(section, key, value.ToString());
public void Write(string section, string key, double value) => Write(section, key, value.ToString("G17", CultureInfo.InvariantCulture));
public void Write(string section, string key, bool value) => Write(section, key, value ? "1" : "0");
public void Write(string section, string key, DateTime value) => Write(section, key, value.ToString("o")); // ISO 8601
// === 讀取 (字串) ===
public string Read(string section, string key, string defaultValue = "")
{
CheckDisposed();
ValidateSectionKey(section, key);
lock (_sync)
{
var sb = new StringBuilder(256);
uint len = GetPrivateProfileString(section, key, defaultValue, sb, (uint)sb.Capacity, _path);
if (len == sb.Capacity - 1)
{
sb.Capacity = (int)(len * 2);
GetPrivateProfileString(section, key, defaultValue, sb, (uint)sb.Capacity, _path);
}
return sb.ToString();
}
}
// === 讀取 (強型別) ===
public T Read<T>(string section, string key, T defaultValue = default)
{
string val = Read(section, key, null);
if (val == null) return defaultValue;
try
{
if (typeof(T) == typeof(string)) return (T)(object)val;
if (typeof(T) == typeof(int)) return (T)(object)int.Parse(val);
if (typeof(T) == typeof(long)) return (T)(object)long.Parse(val);
if (typeof(T) == typeof(double)) return (T)(object)double.Parse(val, CultureInfo.InvariantCulture);
if (typeof(T) == typeof(bool)) return (T)(object)(val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase));
if (typeof(T) == typeof(DateTime)) return (T)(object)DateTime.ParseExact(val, "o", CultureInfo.InvariantCulture);
if (typeof(T).IsEnum) return (T)Enum.Parse(typeof(T), val, true);
// 可擴展:TypeConverter、JsonSerializer 等
}
catch { return defaultValue; }
return defaultValue;
}
// === 刪除 ===
public void DeleteKey(string section, string key)
{
CheckDisposed();
ValidateSectionKey(section, key);
lock (_sync)
WritePrivateProfileString(section, key, null, _path);
}
public void DeleteSection(string section)
{
CheckDisposed();
if (string.IsNullOrWhiteSpace(section)) throw new ArgumentException("區段不可為空");
lock (_sync)
WritePrivateProfileString(section, null, null, _path);
}
// === 列舉區段/鍵 ===
public string[] GetSections()
{
CheckDisposed();
lock (_sync)
{
// 先取得所需緩衝區大小
uint size = GetPrivateProfileSection(null, IntPtr.Zero, 0, _path);
if (size == 0) return Array.Empty<string>();
IntPtr ptr = Marshal.AllocHGlobal((int)size);
try
{
GetPrivateProfileSection(null, ptr, size, _path);
string raw = Marshal.PtrToStringUni(ptr, (int)size);
return ParseNullSeparated(raw);
}
finally { Marshal.FreeHGlobal(ptr); }
}
}
public KeyValuePair<string, string>[] GetKeys(string section)
{
CheckDisposed();
if (string.IsNullOrWhiteSpace(section)) throw new ArgumentException("區段不可為空");
lock (_sync)
{
uint size = GetPrivateProfileSection(section, IntPtr.Zero, 0, _path);
if (size == 0) return Array.Empty<KeyValuePair<string, string>>();
IntPtr ptr = Marshal.AllocHGlobal((int)size);
try
{
GetPrivateProfileSection(section, ptr, size, _path);
string raw = Marshal.PtrToStringUni(ptr, (int)size);
return ParseKeyValuePairs(raw);
}
finally { Marshal.FreeHGlobal(ptr); }
}
}
private static string[] ParseNullSeparated(string raw)
{
var list = new List<string>();
int start = 0;
for (int i = 0; i < raw.Length; i++)
{
if (raw[i] == '\0')
{
if (i > start) list.Add(raw.Substring(start, i - start));
start = i + 1;
if (start < raw.Length && raw[start] == '\0') break; // 雙 \0 結束
}
}
return list.ToArray();
}
private static KeyValuePair<string, string>[] ParseKeyValuePairs(string raw)
{
var list = new List<KeyValuePair<string, string>>();
int start = 0;
for (int i = 0; i < raw.Length; i++)
{
if (raw[i] == '\0')
{
if (i > start)
{
string pair = raw.Substring(start, i - start);
int eq = pair.IndexOf('=');
if (eq > 0)
list.Add(new KeyValuePair<string, string>(pair.Substring(0, eq), pair.Substring(eq + 1)));
}
start = i + 1;
if (start < raw.Length && raw[start] == '\0') break;
}
}
return list.ToArray();
}
// === 輔助 ===
private void ValidateSectionKey(string section, string key)
{
if (string.IsNullOrWhiteSpace(section)) throw new ArgumentException("區段不可為空", nameof(section));
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("鍵不可為空", nameof(key));
}
private void CheckDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(IniFile));
}
private static void ThrowWin32Error(string api)
{
int err = Marshal.GetLastWin32Error();
throw new Win32Exception(err, $"{api} failed (0x{err:X8})");
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
// 可選:Flush 快取、備份等
}
}
}
using (var ini = new IniFile(@"C:\Config\app.ini"))
{
// 寫入
ini.Write("Database", "Server", "192.168.1.100");
ini.Write("Database", "Port", 5432);
ini.Write("Database", "UseSsl", true);
ini.Write("Database", "Timeout", TimeSpan.FromSeconds(30));
// 讀取強型別
string server = ini.Read<string>("Database", "Server", "localhost");
int port = ini.Read<int>("Database", "Port", 5432);
bool ssl = ini.Read<bool>("Database", "UseSsl", false);
TimeSpan timeout = ini.Read<TimeSpan>("Database", "Timeout", TimeSpan.FromSeconds(10)); // 需擴展解析
// 列舉
foreach (var section in ini.GetSections())
Console.WriteLine($"[{section}]");
foreach (var kv in ini.GetKeys("Database"))
Console.WriteLine($" {kv.Key} = {kv.Value}");
}
public class DatabaseSettings
{
public string Server { get; set; } = "localhost";
public int Port { get; set; } = 5432;
public bool UseSsl { get; set; }
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
}
// 擴充方法
public static class IniFileExtensions
{
public static T Bind<T>(this IniFile ini, string section) where T : new()
{
var obj = new T();
var props = typeof(T).GetProperties();
foreach (var prop in props)
{
string val = ini.Read(section, prop.Name, null);
if (val != null)
{
var converter = TypeDescriptor.GetConverter(prop.PropertyType);
if (converter.CanConvertFrom(typeof(string)))
prop.SetValue(obj, converter.ConvertFromInvariantString(val));
}
}
return obj;
}
public static void Save<T>(this IniFile ini, string section, T obj)
{
var props = typeof(T).GetProperties();
foreach (var prop in props)
{
var val = prop.GetValue(obj);
if (val != null)
ini.Write(section, prop.Name, val.ToString());
}
}
}
// 使用
var db = ini.Bind<DatabaseSettings>("Database");
db.Port = 3306;
ini.Save("Database", db);
public class IniConfigurationSource : IConfigurationSource
{
public string Path { get; set; }
public bool Optional { get; set; }
public bool ReloadOnChange { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
=> new IniConfigurationProvider(this);
}
public class IniConfigurationProvider : ConfigurationProvider
{
private readonly IniConfigurationSource _source;
private FileSystemWatcher _watcher;
public IniConfigurationProvider(IniConfigurationSource source) => _source = source;
public override void Load()
{
using (var ini = new IniFile(_source.Path))
{
Data.Clear();
foreach (var section in ini.GetSections())
{
foreach (var kv in ini.GetKeys(section))
{
Data[$"{section}:{kv.Key}"] = kv.Value; // 扁平化鍵
}
}
}
if (_source.ReloadOnChange)
{
_watcher = new FileSystemWatcher(Path.GetDirectoryName(_source.Path), Path.GetFileName(_source.Path))
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
_watcher.Changed += (s, e) => Load();
_watcher.EnableRaisingEvents = true;
}
}
public override void Dispose()
{
_watcher?.Dispose();
base.Dispose();
}
}
// 註冊
var config = new ConfigurationBuilder()
.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true)
.Build();
| 方案 | 特點 | API 風格 |
|---|---|---|
| ini-parser (NuGet) | 純 .NET、無 P/Invoke、支援註解/巢狀區段 | IniData[section][key] |
| Microsoft.Extensions.Configuration.Ini | 官方配置提供者、支援 reload、DI 整合 | IConfiguration["section:key"] |
| YAML/JSON (推薦新專案) | 標準化、工具鏈豐富、支援複雜結構 | 強型別 Options Pattern |
lock 保護所有 P/Invoke 呼叫SetLastError=true + Win32Exception、區分檔案不存在/權限/共用違規IDisposable、Marshal.FreeHGlobal、GC.Collect (必要時).bak、定期輪替appsettings.json + Options Pattern,INI 僅作舊系統相容結語:五篇系列從 kernel32.dll 底層 API 走到 IConfigurationProvider 架構整合,完整呈現了 C# 操作 INI 的演進全景。核心心法三句:封裝底層細節、強型別消除魔術字串、架構對齊現代配置模式。若新專案無舊系統包袱,請直接擁抱 appsettings.json + Microsoft.Extensions.Configuration;若必須用 INI,請拿走 IniFile 類別,它已準備好服務於生產環境。