在.NET中读写INI文件 ——兼谈正则表达式的应用

INI文件是Windows平台上的一种较常用的软件配置文件格式,Windows应用程序常常使用它来保存一些配置信息。它一般是由数个包含key-value对的Section组成,每个key-value对保存着一些软件配置信息。例如最典型的NT系列的启动配置文件boot.ini


 

[boot loader]

timeout=30

default=multi(0)disk(0)rdisk(0)partition(2)/WINDOWS

[operating systems]

multi(0)disk(0)rdisk(0)partition(2)/WINDOWS=”Microsoft Windows XP Professional” /fastdetect

multi(0)disk(0)rdisk(0)partition(1)/WINDOWS=”Microsoft Windows XP Professional” /fastdetect


 

在这个文件中,方括号中的字符串是Section的名字,两个方括号之间的内容为一个SectionSection的内容是一些key-value对,每个key-value对占据一行,例如timeout=30就是一对key-value对,timeoutkey,对应的value30Windows平台专门提供了一组API可以方便地操作INI文件,例如GetPrivateProfileSection()GetPrivateProfileInt()等。

 

随着Windows系列操作系统的不断发展,INI文件的作用逐渐被注册表、XML格式的config文件等所取代,很少再用于系统配置,但我们仍可以在应用程序中使用它。在.NET平台上推荐使用的软件配置文件格式是基于XMLconfig文件,因此在.NET Framework中并没有提供对INI文件读写的特殊支持,使得我们有时在需要读写INI文件时不是很方便。本文将探讨如何使INI文件的读写在.NET平台上变得更加容易。当然,我们可以直接引入上述的API,但本文将不使用API,而是完全基于.NET Framework

 

创建INI文件读写类

 

要在.NET平台上处理INI文件,很自然的想法就是创建一个专门的class来负责INI文件的读写工作,这个class暴露适当的接口供外部调用。一般的INI文件的尺寸很小,因此最简单的做法就是以文本的方式将整个文件读入一个string变量中。类定义如下:


 

 

    public class FileIni

    {

        private string fileContents = null;

 

        public FileIni(string fileName)

        {

            if(File.Exists(fileName))

            {

                StreamReader r = File.OpenText(fileName);

                fileContents = r.ReadToEnd();

                r.Close();

            }

        }

    }


 

 

接下来我们要提供一些方法来操作这个字符串,比如从中返回所有的Section Name、取得特定的key所对应的value等。我们可以使用字符串查找之类的方法来完成这些工作,但是.NET Framework为我们提供了更好的方法,那就是正则表达式。

 

正则表达式

 

所谓正则表达式是一种被设计用来优化字符串操作的语言。它使用一组元字符(Metacharacters)来实现强劲的字符串操作能力。这组元字符最早来自于对DOS文件系统中?*的扩展。在DOS文件系统中,?*分别被用来代替单个字符和字符群组,它们可以被认为是最早的元字符。正则表达式在它们的基础上不断扩充,形成了一套元字符集,能够表达非常复杂的字符串。

 

举例来说,网上注册时常常需要用户输入一个有效的Email地址。当用户输入一个字符串后,我们如何验证这个Email地址是否合法呢?使用下面这个正则表达式可以轻易地实现目的:


 

 

@”^([/w-/.]+)@((/[[0-9]{1,3}/.[0-9]{1,3}/.[0-9]{1,3}/.)|(([/w-]+/.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(/]?)$”


 

关于这个正则比表达式的含义,在此不做过多解释,有兴趣的朋友可以参考相关的正则表达式资料。这个正则表达式虽不能保证用户输入的Email地址100%的真实有效,但至少可以保证用户输入的Email地址看上去是合法有效的。

 

.NET Framework中提供了一些使用正则表达式的类,这些类位于System.Text.RegularExpressions名字空间下。

 

使用正则表达式实现FileIni类的功能

 

现在我们可以使用正则表达式来实现FileIni类的相应功能了。为了返回INI文件中所有Section的名字,我们可以使用一个只读属性SectionNames来返回一个Section Name的字符串数组。


 

        public string[] SectionNames

        {

            get

            {

                // Using regular expression to get all section names.

                string regexPattern = @”/[(?<SectionName>/w*)/]”;

                Regex r = new Regex(regexPattern);  // Match “[anywords]”

                MatchCollection matches = r.Matches(fileContents);

                // Writing all section names to a string array.

                string[] results = new string[matches.Count];

                for(int i = 0; i < matches.Count; i++)

                {

                    results[i] = matches[i].Result(“${SectionName}”);

                }

 

                return results;

            }

        }


 

在上面的代码中,我们使用一个正则表达式:@”/[(?<SectionName>/w*)/]”,对源字符串进行一次匹配就取出了所有的Section Name

 

为了取得特定Section下的特定的keyvalue,我们先要取得此Section下的所有内容,然后再从中取出特定keyvalue


 

        public string GetSectionString(string sectionName)

        {

            string regexPattern = @”(/[” + sectionName + @”/]”

                + @”(?<SectionString>.*)/[)”;

            Regex r = new Regex(regexPattern, RegexOptions.Singleline);

            if(r.IsMatch(fileContents))

            {

                return r.Match(fileContents).Result(“${SectionString}”);

            }

 

            return string.Empty;

        }


 

GetSectionString()根据特定的sectionName取得此Section的全部内容。假设sectionName为字符串boot loader,此时的正则表达式为@”(/[boot loader/](?<SetionString>.*)/[]”。得到Section下的所有内容后,我们再从其中得到我们想要的value值。


 

 

        public string GetKeyString(string sectionName, string keyName)

        {

            string sectionString = this.GetSectionString(sectionName);

            string regexPattern = @”(” + keyName + @”=(?<value>.*)/r/n)”;

            Regex r = new Regex(regexPattern);

            if(r.IsMatch(fileContents))

            {

                return r.Match(fileContents).Result(“${value}”);

            }

           

            return string.Empty;

        }


 

在此基础上,可以得到更多的诸如GetKeyInt()之类的方法。至于写方法,利用RegexReplace()方法也是很容易实现的,在此就不做过多的叙述了。

 

总结

 

本文着重演示了正则表达式在读写INI文件时的应用。所实现的INI文件读写类FileIni扩展性稍显不足,例如,这个类只能处理通用格式的INI文件,对于格式稍有变化的INI文件,此类中的正则表达式就需要修改了。总之,正则表达式是处理字符串的强大工具,掌握了它对我们更高效地处理字符串是绝对有好处的。

 

ATL中的Thunk机制学习

ATL利用一系列的类来管理窗口。为了使代码尽量紧凑而高效,ATL使用了一种有趣的技术来实现与窗口消息相关联的HWND和负责处理消息的对象的this指针之间的映射。具体过程如下:

 

在窗口注册时声明的窗口过程为此窗口对应的窗口类的静态成员函数StartWindowProc,当第一条消息到达此函数时,其处理如下:

 

template <class TBase, class TWinTraits>

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

         CWindowImplBaseT< TBase, TWinTraits >* pThis =

(CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData();

         ATLASSERT(pThis != NULL);

         pThis->m_hWnd = hWnd;

         pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);

         WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);

         WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);

#ifdef _DEBUG

         // check if somebody has subclassed us already since we discard it

         if(pOldProc != StartWindowProc)

                   ATLTRACE2(atlTraceWindowing, 0, _T(“Subclassing through a hook discarded./n”));

#else

         pOldProc;  // avoid unused warning

#endif

         return pProc(hWnd, uMsg, wParam, lParam);

}

 

它先由全局变量_Module中取得此窗口对应的窗口类的this指针,然后通过m_thunk运用汇编指令改造此窗口类的窗口过程成员函数。m_thunkCWndProcThunk的实例,每个窗口类各有一个。它的定义如下:

 

class CWndProcThunk

{

public:

         union

         {

                   _AtlCreateWndData cd;

                   _WndProcThunk thunk;

         };

         void Init(WNDPROC proc, void* pThis)

         {

#if defined (_M_IX86)

                   thunk.m_mov = 0x042444C7;  //C7 44 24 0C

                   thunk.m_this = (DWORD)pThis;

                   thunk.m_jmp = 0xe9;

                   thunk.m_relproc = (int)proc – ((int)this+sizeof(_WndProcThunk));

#elif defined (_M_ALPHA)

                   thunk.ldah_at = (0x279f0000 | HIWORD(proc)) + (LOWORD(proc)>>15);

                   thunk.ldah_a0 = (0x261f0000 | HIWORD(pThis)) + (LOWORD(pThis)>>15);

                   thunk.lda_at = 0x239c0000 | LOWORD(proc);

                   thunk.lda_a0 = 0x22100000 | LOWORD(pThis);

                   thunk.jmp = 0x6bfc0000;

#endif

                   // write block from data cache and

                   //  flush from instruction cache

                   FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));

         }

};

 

Init()函数完成对WndProcThunk结构的初始化工作。WndProcThunk结构针对X86体系的定义如下:

 

struct _WndProcThunk

{

         DWORD   m_mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)

         DWORD   m_this;         //

         BYTE    m_jmp;          // jmp WndProc

         DWORD   m_relproc;      // relative jmp

};

 

结构成员中保存的是一组汇编指令。在X86体系下,在Init()函数中这组汇编指令被初始化为下面的指令:

 

mov dword ptr [esp+0x4], pThis

jmp (int)proc – ((int)this+sizeof(_WndProcThunk))

 

它完成的功能是,用窗口类的指针pThis代替窗口句柄hWndesp+0x4中放的就是hWnd),然后跳转到传入的proc函数处((int)proc – ((int)this+sizeof(_WndProcThunk))procthunk之间的距离)。

 

在调用完m_thunk.Init()函数之后,实际上就得到了经过改造的窗口过程(m_thunk.thunk),此窗口过程是窗口类的一个成员函数,它的第一个参数定义虽然是HWND,但实际上是它的类的this指针。最后使用SetWindowLong()用这个新的窗口过程替换掉原有的窗口过程(也就是StartWindowProc),以后的所有消息都会被路由给新的窗口过程。

深入.NET托管堆(managed heap)(下)

在这里,对象可以通过两种方式被清除。第一种方式是通过IDisposable接口的Dispose方法。此方法在对象显式地结束时被客户代码调用,它调用InternalDispose(true)。在这种情况下所有的对象都被清除了。如果析构函数被调用,那么InternalDispose(false)被调用,此时只有外部资源会被释放。如果我们已经执行了终止操作,那么我们自己的对象有可能已经被释放了,此后对它们的引用有可能引起异常。

 

GC.SuppressFinalize的调用会阻止垃圾收集器将对象放入终止队列中。这样做可以降低在一次GC过程中由于整理对象而引起的内存消耗,并且由于终止操作不会被调用,从而使性能得到提高。

 

C#的优化

 

因此使用IDisposable.Dispose()来释放资源是个很好的方式,它不但可以减少一些在托管堆上进行操作的内存需求,而且能够减少必须执行终止操作的对象的数量。但是它使用起来比较麻烦,尤其是有多个临时对象被创建的时候更是如此。为了能够从IDisposable接口受益,C#客户程序应该书写象下面这样的代码:

 

OverdueBookLocator bookLocator = null;

try

{

    bookLocator = new OverdueBookLocator();

    // Use bookLocator here

    Book book = bookLocator.Find(“Eiffel, the Language”);

    .

    .

    .

}

finally

{

    if(bookLocator != null)

    {

        IDisposable disp = bookLocator as IDisposable;

        disp.Dispose();

    }

}

 

finally中的代码被用来在有异常发生时作适当的清理工作。为了C#客户程序能够简单有效地使用Dispose模式,Beta2引入了using表达式。Using表达式允许你简化你的代码,因此上面的代码可以写成:

 

 

using(bookLocator = new OverdueBookLocator())

{

   // Use bookLocator here

   Book book = bookLocator.Find(“Eiffel, the Language”);

}

 

无论何时分配具有明确定义的生存期的类型时,你都应该使用using表达式。它能保证对IDisposable接口的适当调用,即使是在有异常发生的时候。

 

使用System.GC

 

System.GC类用来访问被.NET framework暴露出来的垃圾回收机制。这个类包含以下一些有用的方法:

 

     GC.SuppressFinalize 这个方法在前面已经描述过了,它能够抑制终止操作。如果你已经将属于一个对象的外部资源释放了,调用这个方法来抑制此对象的终止操作的执行。

     GC.Collect 具有两个版本。不带参数的版本在托管堆的所有generation上执行回收动作。另一个版本带有一个整型参数,此参数指明所要进行回收操作的generation。你将很少调用这个方法,因为垃圾收集器在需要的时候会自动调用它。

     GC.GetGeneration 返回作为参数传入的对象所在的generation。这个方法在由于性能的原因而进行的调试和跟踪中很有作用,但是在大部分应用中作用有限。

     GC.GetTotalMemory 返回堆中已经被分配的内存总量。由于托管堆的工作方式,这个数字并不精确,但是如果你以true作为参数的话,还是会得到一个比较接近的近似值。这个方法在计算之前会先执行一遍回收操作。

 

下面是使用这些方法的一个例子:

 

/// <summary>

/// Displays current GC information

/// </summary>

/// <param name=”generation”>The generation to collect</param>

/// <param name=”waitForGC”>Run GC before calculating usage?</param>

public void CollectAndAudit(int generation, bool waitForGC)

{

  int myGeneration = GC.GetGeneration(this);

  long totalMemory = GC.GetTotalMemory(waitForGC);

  Console.WriteLine(“I am in generation {0}.”, myGeneration);

  Console.WriteLine(“Memory before collection {0}.”, totalMemory);

  GC.Collect(generation);

  Console.WriteLine(“Memory after collection {0}.”, totalMemory);

}

 

关于本文作者

 

Mickey WilliamsCodev Technologies的创始人之一。Codev Technologies是一家从事位Windows程序开发者提供咨询和工具的机构。他同时也是.NET Experts (http://www.codeguru.com/columns/DotNet/www.dotnetexperts.com)的主要成员,他在此讲授.NET Framework的课程。他时常在美国和欧洲的一些研讨会上发表演讲,并且已经写了八本有关Windows程序设计方面的著作。他目前正被微软出版社邀请写作“Microsoft Visual C#”。你可以在mw@codevtech.com找到他。

深入.NET托管堆(managed heap)(上)

.NET的所有技术中,最具争议的恐怕是垃圾收集(Garbage CollectionGC)了。作为.NET框架中一个重要的部分,托管堆和垃圾收集机制对我们中的大部分人来说是陌生的概念。在这篇文章中将要讨论托管堆,和你将从中得到怎样的好处。

 

为什么要托管堆?

 

.NET框架包含一个托管堆,所有的.NET语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,但是所有的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆。

 

垃圾收集器的基本算法很简单:

将所有的托管内存标记为垃圾

寻找正被使用的内存块,并将他们标记为有效

释放所有没有被使用的内存块

整理堆以减少碎片

 

托管堆优化

 

看上去似乎很简单,但是垃圾收集器实际采用的步骤和堆管理系统的其他部分并非微不足道,其中常常涉及为提高性能而作的优化设计。举例来说,垃圾收集遍历整个内存池具有很高的开销。然而,研究表明大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成三个段,称作generations。新分配的对象被放在generation 0中。这个generation是最先被回收的——在这个generation中最有可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器的L2 cache中),因此在它里面的回收将是最快和最高效的。

 

托管堆的另外一种优化操作与locality of reference规则有关。该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分得很远。

 

还有一种优化是与大对象有关的。通常,大对象具有很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆所能提高的性能。

 

关于外部资源(External Resources)的问题

 

垃圾收集器能够有效地管理从托管堆中释放的资源,但是资源回收操作只有在内存紧张而触发一个回收动作时才执行。那么,类是怎样来管理像数据库连接或者窗口句柄这样有限的资源的呢?等待,直到垃圾回收被触发之后再清理数据库连接或者文件句柄并不是一个好方法,这会严重降低系统的性能。

 

所有拥有外部资源的类,在这些资源已经不再用到的时候,都应当执行Close或者Dispose方法。从Beta2译注:本文中所有的Beta2均是指.NET Framework Beta2,不再特别注明)开始,Dispose模式通过IDisposable接口来实现这将在本文的后续部分讨论。

 

需要清理外部资源的类还应当实现一个终止操作(finalizer)。在C#中,创建终止操作的首选方式是在析构函数中实现,而在Framework层,终止操作的实现则是通过重载System.Object.Finalize 方法。以下两种实现终止操作的方法是等效的:

 

~OverdueBookLocator()

{

    Dispose(false);

}

 

和:

 

public void Finalize()

{

    base.Finalize();

    Dispose(false);

}

 

C#中,同时在Finalize方法和析构函数实现终止操作将会导致错误的产生。

 

除非你有足够的理由,否则你不应该创建析构函数或者Finalize方法。终止操作会降低系统的性能,并且增加执行期的内存开销。同时,由于终止操作被执行的方式,你并不能保证何时一个终止操作会被执行。

 

内存分配和垃圾回收的细节

 

GC有了一个总体印象之后,让我们来讨论关于托管堆中的分配与回收工作的细节。托管堆看起来与我们已经熟悉的C++编程中的传统的堆一点都不像。在传统的堆中,数据结构习惯于使用大块的空闲内存。在其中查找特定大小的内存块是一件很耗时的工作,尤其是当内存中充满碎片的时候。与此不同,在托管堆中,内存被组制成连续的数组,指针总是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单地递增——由此而来的一个好处是,分配操作的效率得到了很大的提升。

 

当对象被分配的时候,它们一开始被放在generation 0中。当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操作被触发。由于generation 0的大小很小,因此这将是一个非常快的GC过程。这个GC过程的结果是将generation 0彻底的刷新了一遍。不再使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。

 

generation 1的大小随着从generation 0中移入的对象数量的增加而接近它的上限的时候,一个回收动作被触发来在generation 0generation 1中执行GC过程。如同在generation 0中一样,不再使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,因为在generation 0中最有可能存在大量的已不再使用的临时对象。对generation 2的回收过程具有很高的开销,并且此过程只有在generation 0generation 1GC过程不能释放足够的内存时才会被触发。如果对generation 2GC过程仍然不能释放足够的内存,那么系统就会抛出OutOfMemoryException异常

 

带有终止操作的对象的垃圾收集过程要稍微复杂一些。当一个带有终止操作的对象被标记为垃圾时,它并不会被立即释放。相反,它会被放置在一个终止队列(finalization queue)中,此队列为这个对象建立一个引用,来避免这个对象被回收。后台线程为队列中的每个对象执行它们各自的终止操作,并且将已经执行过终止操作的对象从终止队列中删除。只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被从内存中删除。这样做的一个后果是,等待被终止的对象有可能在它被清除之前,被移入更高一级的generation中,从而增加它被清除的延迟时间。

 

需要执行终止操作的对象应当实现IDisposable接口,以便客户程序通过此接口快速执行终止动作。IDisposable接口包含一个方法——Dispose。这个被Beta2引入的接口,采用一种在Beta2之前就已经被广泛使用的模式实现。从本质上讲,一个需要终止操作的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操作,就象下面这个程序片断所演示的那样:

 

public class OverdueBookLocator: IDisposable

{

    ~OverdueBookLocator()

    {

        InternalDispose(false);

    }

 

    public void Dispose()

    {

        InternalDispose(true);

    }

 

    protected void InternalDispose(bool disposing)

    {

        if(disposing)

        {

            GC.SuppressFinalize(this);

            // Dispose of managed objects if disposing.

        }

        // free external resources here

    }

}