用于使用 INI 文件存储应用程序设置的跨平台 C# 类






3.95/5 (7投票s)
该类可以在 Mono 或 .NET 下运行,并允许使用 Windows 风格的“INI”文件来存储和检索应用程序设置
本文的新版本
对于感兴趣的任何人,我将更新版本 的文章在这里。 :)
介绍
“INI”文件在整个 Windows 操作系统中被广泛使用。在注册表出现之前它们就存在了,并且至今仍然存在。
它们是文本文件,通常(但不一定)带有“.ini”扩展名,用于存储各种类型的设置。
它们的结构相当简单:设置按“节”组织,节标题用方括号(“[" "]”)括起来。每个节包含一个或多个“键/值”字符串对。每个键在一个节内是唯一的,并标识一个特定的设置。
INI 文件内容的一个示例如下:
[Section1]
Key1=somevalue
Key2=somevalue
...
[Section2]
Key1=somevalue
Key10=somevalue
...
使用 INI 文件可以方便地存储您的应用程序设置,因为该文件格式易于阅读、理解,并且只需使用文本编辑器即可进行修改。
背景
在 Windows 应用程序中使用 INI 文件通常是通过“kernel32.dll”库公开的一些 API 调用来实现的。在 C# 中,这可以通过使用“DllImport
”指令来完成。
#region "WIN32API"
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileInt
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,int nDefault,
[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
byte[] lpReturnedString,int nSize,[MarshalAs(UnmanagedType.LPStr)]
string lpFileName);
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
[MarshalAs(UnmanagedType.LPStr)] string lpReturnedString,
int nSize,[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32.dll")] private static extern int WritePrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpString,
[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
#endregion
我决定写一个自己的跨平台类,以便在 Mono 和 .NET 下都能轻松使用 INI 文件。
结果是 INIFile
类,它与标准的 Windows INI 文件兼容,但有几个例外:
- 节和键名区分大小写。
- 它使用缓存来提高性能。
- 更改或添加值时,它不会保留原始 INI 文件结构。
缓存是通过字典实现的,字典将所有节和键/值对保存在内存中。
Using the Code
INI 文件在创建类的实例时被读取和解析,其内容存储在缓存中。INI 文件名(到所需文件的完整路径)会传递给构造函数。
INIFile MyINIFile = new INIFile("myinifilename.ini");
用户程序可以通过调用 GetValue()
方法,提供节名和键名来查询缓存内容。
string Value = MyINIFile.GetValue("Section", "Key", "Default value");
为了与 Windows API 调用的行为保持兼容,如果 INI 文件不存在,或者请求的节或键不存在,都不会抛出异常。相反,将返回用户提供的默认值。这使得轻松处理设置由最终用户可选修改且默认值硬编码在软件中的情况变得容易。
还有一个选项可以实现 INI 文件内容的延迟加载(只需将 true
作为第二个参数传递给构造函数调用),这会告诉类实例仅在第一次查询或值修改时加载 INI 文件内容。这在必要时可以加快应用程序启动速度。
INIFile MyINIFile = new INIFile("myinifilename.ini", true);
可以通过调用 SetValue()
方法来更改值。
MyINIFile.SetValue("Section", "Key", "Value");
如果提供的节或键不存在,它们将被自动添加到缓存中。通过显式调用 Flush()
方法将所有更改保存回 INI 文件。
MyINIFile.Flush();
如果缓存中有任何修改,所有节和键/值对将被写回磁盘,覆盖旧文件。如果文件不存在,则会创建该文件。用户程序需要通过显式避免两个不同的 INIFile
类实例指向同一个文件的情况来确保文件内容的完整性。还值得注意的是,在 INI 文件加载到本地缓存后,通过外部文本编辑器所做的任何更改将在调用 Flush()
后丢失(但仅当缓存中有修改时)。
可以通过调用 Refresh()
方法强制刷新缓存。INI 文件将被重新解析,并且缓存的内容将被文件内容替换。在上次调用 Flush()
后对缓存所做的任何修改都将丢失。
MyINIFile.Refresh();
在大多数典型情况下,管理 INI 文件的最佳方法是每次需要读取或写入应用程序设置时都创建一个 INIFile
类的实例,然后销毁它。这将确保通过文本编辑器所做的任何更改不会被覆盖。
例如,您可以在应用程序启动时创建一个类实例来读取设置并进行存储,然后在应用程序关闭时,或者当用户在“更改设置”对话框中按下“确定”按钮时,创建另一个实例来将更改写入设置,依此类推。
由于所有值都是 string
类型,用户程序可以轻松地将任何所需的数据类型编码为 string
以便写入 INI 文件,并在读取同一文件时相应地解码。然而,INIFile
类提供了一些 getter 和 setter 来自动管理一些基本数据类型,以满足所有典型需求。
internal string GetValue(string SectionName, string Key, string DefaultValue)
internal bool GetValue(string SectionName, string Key, bool DefaultValue)
internal int GetValue(string SectionName, string Key, int DefaultValue)
internal double GetValue(string SectionName, string Key, double DefaultValue)
internal byte[] GetValue(string SectionName, string Key, byte[] DefaultValue)
internal void SetValue(string SectionName, string Key, string Value)
internal void SetValue(string SectionName, string Key, bool Value)
internal void SetValue(string SectionName, string Key, int Value)
internal void SetValue(string SectionName, string Key, double Value)
internal void SetValue(string SectionName, string Key, byte[] Value)
int
和 double
仅转换为 string
(使用不区分区域性的设置,因此文件将具有跨区域性兼容性),而 bool
将转换为整数值,其中 0
表示 false
,任何其他值表示 true
。这使得用户可以轻松地使用文本编辑器修改这些值。
还可以存储 byte
数组。当存储哈希值、uuid 或任何其他类型的二进制数据时,这非常有用。框架提供了将 string
和其他数据转换为字节数组的方法,从而使使用此功能变得极其容易。字节数组在写入 INI 文件时会被转换为十六进制 string
,这使得可以使用文本编辑器修改它们的值。
关于线程安全和多进程访问的说明
该类包含一个基本的锁定系统来实现线程安全。所有对类方法的调用都是线程安全的。
锁定发生在对文件和缓存的访问上,并且两者使用相同的锁,以确保多线程应用程序中最大的数据完整性。
也就是说,您必须记住,确保完整的数据完整性取决于用户代码,因为锁定是为每个方法调用单独进行的!因此,例如,让两个不同的线程对同一个类的实例执行几次 SetValue()
调用,然后是 Flush()
调用,将需要用户进行显式锁定以防止数据丢失。如果线程使用不同的类实例指向同一个文件,那么使用锁定来防止数据丢失和文件访问异常就更加重要了。
从不同进程访问文件也是如此:您必须要么设置一个进程间锁定系统,要么确保您能够管理所有可能的并发问题。
源代码和示例代码
您可以通过页面顶部的链接下载类源代码以及一些示例代码。
它是在 Mono 2.0 中使用 MonoDevelop 2.0 Alpha 1 开发的,但与 .NET 完全兼容 - 只需将代码复制到 Visual Studio 项目中,或使用 .NET 编译器编译源代码即可。
这是示例代码的摘录,它展示了如何使用该类的功能:
StreamReader sr = null;
try
{
Console.WriteLine("Creating INIFile object for \"test.ini\"...");
INIFile MyINIFile = new INIFile("test.ini");
Console.WriteLine("\nGetting values...\n");
int Value1 = MyINIFile.GetValue("Section1","Value1",0);
bool Value2 = MyINIFile.GetValue("Section1","Value2",false);
double Value3 = MyINIFile.GetValue("Section1","Value3",(double)0);
byte[] Value4 = MyINIFile.GetValue("Section1","Value4",(byte[])null);
Console.Write("(int) Value1=");
Console.WriteLine(Value1.ToString());
Console.Write("(bool) Value2=");
Console.WriteLine(Value2.ToString());
Console.Write("(double) Value3=");
Console.WriteLine(Value3.ToString());
Console.Write("(byte[]) Value4=");
Console.WriteLine(PrintByteArray(Value4));
Console.WriteLine("\nSetting values...\n");
Value1++;
Value2 = !Value2;
Value3 += 0.75;
Value4 = new byte[] { 10, 20, 30, 40 };
MyINIFile.SetValue("Section1","Value1", Value1);
MyINIFile.SetValue("Section1","Value2", Value2);
MyINIFile.SetValue("Section1","Value3", Value3);
MyINIFile.SetValue("Section1","Value4", Value4);
Console.Write("(int) Value1=");
Console.WriteLine(Value1.ToString());
Console.Write("(bool) Value2=");
Console.WriteLine(Value2.ToString());
Console.Write("(double) Value3=");
Console.WriteLine(Value3.ToString());
Console.Write("(byte[]) Value4=");
Console.WriteLine(PrintByteArray(Value4));
Console.WriteLine("\nFlushing cache...");
MyINIFile.Flush();
Console.WriteLine("\nFile content:\n");
sr = new StreamReader("test.ini");
string s;
while ((s=sr.ReadLine()) != null) Console.WriteLine(s);
Console.WriteLine("\nDone.");
}
catch (Exception ex)
{
Console.Write("\n\nEXCEPTION: ");
Console.WriteLine(ex.Message);
}
finally
{
if (sr != null) sr.Close();
sr = null;
}
这是首次运行示例的输出结果:
Creating INIFile object for "test.ini"...
Getting values...
(int) Value1=0
(bool) Value2=False
(double) Value3=0
(byte[]) Value4=
Setting values...
(int) Value1=1
(bool) Value2=True
(double) Value3=0,75
(byte[]) Value4=10, 20, 30, 40
Flushing cache...
File content:
[Section1]
Value1=1
Value2=1
Value3=0.75
Value4=0a141e28
Done.
关注点
该类可以在很多方面得到改进:实现更好的缓存系统、在处置时自动刷新(虽然我个人不喜欢这个解决方案,所以我放弃了这个想法)、只将修改后的值写入文件(牺牲性能,但在某些情况下是确保数据完整性的更好方法)、使用泛型和包装类自动编码任何类型数据的能力,或者甚至使用反射将类转储和重新加载到 INI 文件中,将其映射到节/键结构,等等。
基础知识都已具备,并且可以很容易地为该类添加更多功能。尽情享受吧!:)
历史
- 2009-04-13 - V1.0 - 首次发布