通常我们写程序的时候都会有一些可调控的参数,简单的控制可以通过命令行参数来实现,然而复杂一些的则一般会使用配置文件的形式。可以说这几乎是每一个程序都会需要考虑的一个问题:使用什么样形式的配置文件?虽然 Microsoft 在 Windows API 中提供了注册表以及 ini 文件的读写功能,但是在这一块似乎还是没有像 zlib 之于压缩那样被广泛接受的方式。其实原因也很简单:需求差别太大了。
例如 Emacs 的配置文件,目标是达到最大的灵活性,因此它就纯粹是 Elisp 代码了。类似的有 Rake 的 Rakefile ,其内容实际上是合法的 Ruby 代码,因此写起来和用起来都比 Makefile 要方便了许多。另一方面,对于 pymmseg-cpp 或者 rmmseg-cpp 来说,性能最重要,因此配置文件(辞典)采用了最简单的以行分割的形式,没有使用二进制格式,是因为希望保持配置文件的可编辑性,虽然这个大概说成是数据文件似乎更贴切一些。
除去以上两个极端情况,其他时候使用什么格式的文件就通常是看怎么用方便了。比如我在 YASnippet 中给 snippet 文件定制的属性格式就是简单的每行一个
prop: value |
的形式,使用正则表达式来手工解析。这种做法通常简单有效,虽然不能处理一些特殊情况(比如属性名里面有冒号),但是够用就好。实际上,有一些编程语言内置了很方便的读取数据的功能,例如,在 lisp 里用 read
来读取一个 S-expression 是非常方便的事情,在 Erlang 里也有类似的功能,采用下面格式书写的配置信息:
{config, {robots, [{wasp, {speed, 5}, {mass, 1000}, {shield, 50}}, {apache, {speed, 3}, {mass, 2500}, {shield, 100}}]}, {system, {music, true}, {map, desert}}}. |
可以方便地读取出来:
{ok, [Config]} = file:consult("config.dat") |
另一种格式是 json (JavaScript Object Notation),它格式很简单,因为在 Javascript 里使用很方便,所以在互联网上应用非常广泛,而且其他许多语言都有内置的或第三方的库可以用来读写 json 信息。并且可读性也很好,适合人手工编辑。
当然,另一个不得不提的就是 XML 了,几乎已经算是文本形式的数据交换格式的工业标准了吧,虽然用于手工编辑显得有些冗余,但是这完全可以通过更先进的编辑器来解决,而且它的书写方式让程序的解析工作变得轻松了许多,开源领域也有各种高性能的 XML 处理库的实现。不过,现在的 XML 已经不仅仅是 XML 文件格式这么简单一件事了,它的地位不可撼动。
不过,对于一些轻便的小程序来说,我还是更喜欢用 YAML 一点。这里小 8g 一下,XML 是 Extensible Markup Language 的缩写,而 YAML 则针锋相对:YAML ⇒ YAML Ain’t Markup Language 。这里用了递归形式的缩写方式,就如同 GNU ⇒ Gnu’s Not Unix 一样。而且,Microsoft 也会玩这个,而且技术相当高,它的游戏引擎 XNA 的缩写来自于 XNA’s Not Acronymed 比起 GNU 来更有意思一些。 😀
再回到 YAML ,Ruby 在标准库里内置了 YAML 的支持,Python 也可以使用 PyYAML 库,其他许多语言也都有相关的库可以用,因此使用起来相当方便。不过它之于 XML 最主要的优点还是在手工编辑的时候,一个 YAML 文件大概看起来像这个样子:
engine: width: 988 height: 676 fps: 50 fullscreen: false setting: robots: wasp: speed: 0.15 hp: 500 strike: 0.8 defend: 0.15 apache_II: speed: 0.08 hp: 1000 strike: 1 defend: 0.4 overlays: hp: size: [32, 5] border_color: [189, 190, 189] fill_color: [66, 215, 66] bg_color: [50, 50, 50] |
不像 XML 那样要写那么多 tag ,基本上是类似于 Python 那样依靠缩进的,而且字符串、数字、bool 型变量之类的都会自动识别,在程序中读取出这样一个文件得到的结果通常就是一些 Array
和 Hash
的嵌套(在 Python 中是 list
和 dict
),用起来也很方便。不过更加自由的格式也就自然地增加了机器处理的难度,解析的性能就会受到影响,但是我想一般的程序瓶颈都不会出在配置文件是 YAML 还是 XML 的差别这种地方,主要还是要看自己的需求了。
话说回来,刚才提到的 json 或者 YAML 或者 XML 也好,通常读取进来的配置信息都是以字典或数组的形式存在的,一些时候如果得到的是一个结构良好的对象的形式的话就更好了。当然,在动态语言里很容易通过一些 tricky 让 foo['bar']
和 foo.bar
等价起来(这在 Lua 中甚至是自动的),不过在像 Java 这类的语言里,我们也可以做。比如用 spring 框架的支持,我们可以很轻松地把 XML 格式的 bean 配置文件实例化为具体的 Java 对象。当然,如果不需要 Inversion of control 的支持的话,在 .Net 里实际上内置了 XML 格式的序列化支持,也可以很轻松地把 XML 格式的配置信息还原为对象。
不过我在使用的过程中还是遇到了一个小问题,就是 .Net 中的泛型映射容器 Dictionary
居然不是可序列化的!我非常吃惊,觉得也许背后藏了很深的 8g ,可是并没有 google 到特别有趣的东西,只有一些看起来不是很靠谱的解决办法。幸好我这里的问题还比较特殊一点,实际上我要存储的并不是 key ⇒ value 这样形式的数据,而是有一些数据项,每个数据项有一个 Name
属性,但是我希望能像访问 Dictionary
那样快速地通过 Name
属性来索引到对应的数据项。在 .Net 中专门有一个叫做 KeyedCollection
的东西来做这件事情,并且:
- 它也是泛型的。
- 它是可序列化的。
用法很简单,只要继承之然后覆写 GetKeyForItem
方法即可。这里我会将其用于几种不同类型的数据项,不过约定好了 key 都是字符串类型的并且通过 Name
属性得到,因此我定义了一个接口,最后代码像这个样子:
public interface INamedObject { string Name { get; } } public class NamedCollection<T> : KeyedCollection<string, T> where T : INamedObject { protected override string GetKeyForItem(T item) { return item.Name; } } |
这里的泛型语法看起来有些奇怪,因为 Java 和 C# 的泛型都不是像 C++ template 那样工作的,简单地说就是由编译器帮你加上了一些类型转换的代码。但是编译器不能保证任何类型都有 Name
属性,所以加一句 where T : INamedObject
告诉编译器 T
必须是实现了 INamedObject
接口的某种类型就可以了,这实际上是把泛型和原来的继承系统结合了起来,可以说和 C++ template 是完全不同的东西。
最后,还是小感慨一下一段时间没有碰 C# 没想到它又变化了这么多,仅仅是从语法层面,如果把 LINQ 写进去的话,现在都已经有点让人眼花缭乱了。不断进步吸收新的东西固然是件振奋人心的事,不过也得要平衡好,我一下子就联想到了 scala ,最近似乎是语言百花齐放的年代,这个语言最近也颇受关注,实际上我并不是很了解,只稍微看过一些例子,它给我的印象大概就是包含了所有最新最 cool 的特性,以至于无比复杂了。说起复杂,也不得不提 C++ ,最近身边的 C++ guru 好像也比较低调,或许是我没有太关注了,而如今已是 2009 年,看来果然 C++ 0x 中的 x
必须是个 16 进制数了。 :p
我一直以为yaml是yet another ml..
动态语言的配置文件似乎都喜欢是自己的可执行代码,Lua也是的,而且由于语法糖的存在,看起来很像普通的配置文件。
@quark
恩,不过直接执行代码灵活性太大了,就好象 C 语言中的指针一样,有时候会造成一些诡异的难以调试的 bug ,所以不得不用到或者清楚地知道自己在干什么,一般还是用纯数据格式的配置文件比较安全一些吧。 🙂
我怀疑其实 C++ 0x 的 0x 只是一个 16 进制数前缀…
YAML对于简单的数据还行,对于复杂的数据就麻烦了,我看过它的specification,后来引入了类似!!str的诡异的东西
我不认识叫做C#的东西,我只是用它……