代码和数据的分离如何成为一种惯例?


29

请仔细阅读问题:它询问的是方式,而不是原因

我最近遇到了这个答案,它建议使用数据库存储不可变数据:

听起来您描述的许多魔术数字(尤其是如果它们是部分依赖的)实际上是数据,而不是代码。[...]它可能意味着一个SQL类型的数据库,或者可能只是意味着一个格式化的文本文件。

在我看来,如果您的数据属于程序的一部分,那么要做的就是将其放入程序中。例如,如果您程序的功能是计算元音,那么里面有什么问题vowels = "aeiou"呢?毕竟,大多数语言都具有专门为此目的设计的数据结构。您为什么要麻烦将数据放入“格式化的文本文件”中来分离数据,如上所述?为什么不只使用您选择的编程语言来格式化该文本文件?现在是数据库吗?还是代码?

我敢肯定,有些人会认为这是一个愚蠢的问题,但我会严肃地问这个问题。我觉得“分开的代码和数据”在文化上正在逐渐成为一种不言而喻的真理,以及诸如“不要给变量带来误导性的名称”和“不要仅因为您的语言考虑而使用空格”之类的显而易见的事情。无关紧要”。

例如,这篇文章:从P代码中分离数据的问题有问题吗?什么问题?如果Puppet是用于描述我的基础结构的语言,为什么它不能还描述名称服务器是8.8.8.8?在我看来,问题不在于代码和数据混杂在一起,1而是Puppet缺乏足够丰富的数据结构和与其他事物交互的方式。

我发现这种转变令人不安。面向对象的编程说“我们想要任意丰富的数据结构”,因此赋予数据结构以代码的能力。结果就是封装和抽象。甚至SQL数据库都有存储过程。当您将数据隔离到YAML或文本文件或哑数据库中时,就好像要从代码中删除肿瘤一样,所有这些都将丢失。

谁能解释这种将数据与代码分离的做法是如何发生的以及它的发展方向?谁能引用知名人士的出版物,或者提供一些相关的数据,作为“新兴的诫命”来说明“从数据中分离代码”,并说明其起源?

1:甚至可以做出这样的区分。Lisp程序员,我在看着你。


5
随意使用您选择的语言来掩埋所有html和css。
JeffO 2014年

3
我认为引文的作者的意思是魔术数字并不是真正不变的。
Pieter B

4
硬编码元音没有错。如果您的应用程序仅用于计算英语中的元音。
Michael Paulukonis 2014年

3
分离代码和数据的一个重要的技术原因是,当数据更改时不必重新编译代码。因此,我想问一下它是否同样适用于脚本语言。
user16764 2014年

1
@MichaelPaulukonis:并将其存储在数据库中是一种伪造的解决方案。荷兰人需要零钱吗?零(甚至不更改数据库)。法语/德语需要更改吗?至少支持ISO-8859-1。(超过DB)。希腊语/俄语需要更改吗?Unicode支持(不仅仅是数据库)。实际上,我想不出任何对DB有帮助的语言。
MSalters 2014年

Answers:


22

有很多很好的理由将数据与代码分开,而有些则没有。想到以下几点。

及时性。何时知道数据值?是在编写代码时,在编译,链接,发布,许可,配置,开始执行或运行时。例如,一周中的天数(7)较早,但是USD / AUD汇率要较晚。

结构体。这是根据单个考虑因素设置的单个数据时间,还是可以继承或作为更大项目集合的一部分?诸如YAML和JSON之类的语言可以组合来自多个来源的价值。也许最初看起来是不可变的某些东西可以更好地作为配置管理器中的属性来访问。

地区性。如果所有数据项都存储在有限数量的位置中,则管理起来要容易得多,尤其是当某些数据项可能需要更改为新的(不可变的)值时。仅编辑源代码以更改数据值会带来无意更改和错误的风险。

关注点分离。使算法正确工作最好与考虑使用哪些数据值分开。需要数据来测试算法,而不是成为算法的一部分。另请参见http://c2.com/cgi/wiki?ZeroOneInfinityRule

回答您的问题,这不是新事物。核心原则在过去的30多年中没有发生变化,并且在这段时间里反复被撰写。我不记得有任何关于该主题的主要出版物,因为通常不会将其视为有争议的,而只是向新来者进行解释。这里还有更多内容:http : //c2.com/cgi/wiki?SeparationOfDataAndCode

我的个人经验是,这种分离在特定软件中的重要性会随着时间的流逝变得越来越大,而不是越来越少。硬编码的值将移入头文件,已编译的值将移入配置文件,简单的值将成为层次结构和托管结构的一部分。

关于趋势,我没有看到专业程序员在态度上有任何重大变化(超过10年),但是这个行业越来越多地出现年轻人,我认为众所周知的许多事情决定不停地挑战和重新发明,有时甚至是新事物。见解,但有时出于无知。


2
您能否详细介绍这种做法的历史和趋势?如果每个人都考虑这些问题,我就不会问这个问题。问题的前提是人们没有仔细考虑数据应该放在哪里(编译常量,外部数据库,YAML ...),而是在思考“代码和数据混合错误!错误!”。为什么或何时成为事物?
Phil Frost 2014年

这不是我的经验的一部分,所以我不能告诉你。我在回答中添加了几段。
david.pfx 2014年

我认为“大量涌入年轻人”是一个有效的解释,但我拒绝接受,因为我希望听到其中一些年轻人的意见,以了解他们的想法。显然,他们得到了“分离的代码和数据”部分,但我认为剩下的没有。他们是否在博客文章中阅读过?一本书?何时何地?
Phil Frost 2014年

您将始终收到“ _____糟糕!笨拙的短信!” -并不代表是真的。通常这类事情(例如“'GOTO'BAD!HULK SMASH!”)是针对初学者的,而不是教他们为什么或例外。
AMADANON Inc. 2014年

Locality也可以反向使用:由于不同客户的自定义要求,我们最终得到了一种插件类型的系统,并通过数年的尝试和错误学习,以保持其常数(即使是表格,通过字典列表)数据库和代码中。既因为在“插件”之外的任何地方都使用它是不正确的,也因为更改发生时会自动对更改进行版本控制。
Izkata 2014年

8

与代码分离时,数据的伸缩性更好,并且可以更轻松地查询和修改。即使您的数据本质上是编造的(例如,您的数据代表规则或命令),如果您可以将代码存储为结构化数据,也可以享受分别存储的好处:

权限

如果数据是硬编码的,则需要编辑源文件才能编辑该数据。这意味着:

  • 只有开发人员可以编辑数据。这很不好-数据输入不需要开发人员的技能和知识。

  • 非开发人员可以编辑源文件。这很不好-他们甚至可能不知道源文件就搞砸了!

  • 数据被硬编码到单独的源文件中,非开发人员只能访问这些文件。但这并没有真正算在内-现在将数据与代码分离并存储在自己的文件中...

编辑

因此,关于可以编辑数据,最好将其分开存储。怎么样怎么样,他们将编辑的数据?如果您有大量数据,则手动键入它既乏味又容易出错。为此有一些UI会更好!即使您仍然必须键入所有内容,也不必键入格式的样板,因此您几乎不会弄乱格式并弄乱整个文件!

如果数据是硬编码的,则创建该UI意味着自动化工具将编辑您的手写源文件。让它沉入其中-自动化工具将打开您的源文件,尝试查找数据应在的位置,然后修改该代码。Brrr ... Microsoft为避免这些事情而向C#引入了部分类...

如果数据是分开的,则您的自动化工具将只需要编辑数据文件。我宁愿相信如今编辑数据文件的计算机程序并不少见...

缩放

代码和数据的比例差异很大。随着代码的增长,您希望将其分为更多的类和方法(或数据结构和函数),但是数据(无论增长多少)都希望保留在一个地方。即使必须将其分离为多个文件,也希望以某种方式将这些文件捆绑在一起,这样可以更轻松地从代码中访问该数据。

因此,假设您在源文件中有数千行数据。每次读取文件时,编译器/解释器都必须遍历所有数据,并使用昂贵的lexer&parser对其进行解析-即使您不打算在程序的特定运行期间访问该数据。另外,当您在该文件中编辑实际代码时,您必须处理数据,这使整个过程变得很麻烦。同样,数据文件可以被索引。硬编码数据?没那么多...

搜寻

您拥有大量的数据-您自然要搜索它。

  • 如果将其存储在数据库中,则可以使用数据库查询语言。

  • 如果将其存储在XML文件中,则可以使用XPath。

  • 如果将其存储在JSON / YAML中,则可以将其加载到您喜欢的脚本语言的REPL中并进行搜索。

  • 即使将其存储在普通的旧文本文件中,由于其结构可以识别程序,因此您可以使用grep / sed / awk进行搜索。

当然,您也可以grep / sed / awk遍历源文件中的硬编码数据,但是它也不能正常工作,因为您的查询可以与其他不相关的行匹配,也可以丢失写不同的行,因为编程语言的数据表示语法允许这样做。

有一些用于搜索代码的工具,但是它们对于查找声明而不是硬编码数据很有用。

话虽如此...

区分数据和代码非常重要。仅仅因为某些东西被编写为代码并不意味着它就不能成为数据。而且,仅因为用数据表示形式编写的东西并不意味着它实际上不是代码。

当我们对“魔术数字”有非常严格的规则时,我上了一堂课。我们的代码中没有数字。这意味着我们必须做以下事情:

#define THE_NUMBER_ZERO 0
//....
for(int i=THE_NUMBER_ZERO;i<cout;++i){
//....

简直荒谬!是的,0从技术上讲是“数据”,但它与for循环的其余部分一样,是代码的一部分!因此,即使我们可以将其表示为数据并将其与代码分开,但这并不意味着我们应该这样做。并不是因为我们想将数据保留在代码中,而是因为它实际上不是数据-仅比其余代码(也被编译为1和0)更多。


7

我认为目前有些混乱。您将两件事混合在一起:“分离代码和数据”和“将程序的行为表示为数据”。

在您的情况下,您实际上担心的是将第二个混入其中。当您将程序的行为表示为数据时,将使其更易于扩展。在带有的示例中vowels = "aeiou",添加新元音与添加字符一样简单。如果您在外部具有此数据,则可以更改此行为而不必重新编译程序。

当您考虑时,OOP就是这种想法的延伸。将数据和行为绑定在一起将使您可以基于程序的数据来更改程序的行为。


2
自然地,元音的列表将会改变。
cHao 2014年

13
@cHao只要i18n介入,它就是。
恢复莫妮卡

2
i18n可能会伤脑筋
Rory Hunter

2
@Angew:不过,只要 i18n介入,您还是会被搞砸了。您需要为此的代码;幼稚的解决方案甚至不能用英语处理所有案件。(忘了ï一秒钟,让我们谈yw!)移动列表输出到一个数据库是不会解决这个问题,实际上是有害的-它的复杂性将是毫无价值的,如果做错了,但你不会甚至不知道“错误” 什么,除非您是从头开始为i18n设计的。在这一点上,您已经意识到元音列表根本不会减少它。
cHao 2014年

1
@BenLee:实际上,我不会感到惊讶。我目前正在努力更改我们所说的某些代码。但是,将所有内容外包给数据库是另一种形式的算命。如果您还不知道是否需要修改某些内容,更重要的是,如果您还不知道如何进行修改,那么IMO最好等到您需要这种灵活性再添加它。
cHao 2014年

5

例如,如果您的程序的功能是计算元音,那么在其中包含元音=“ aeiou”怎么了?

从外部存储配置可让您拥有一个预期可用于许多配置的代码版本,另一种选择是维护许多仅因配置而异的软件版本。

您提到元音=“ aeiou”,如果我有时想要“ y”,我应该重新构建整个程序吗?修改代码后,我可以轻松升级版本吗?如果有错误,是我导致的,还是程序损坏了?

如果这在您的程序内部,则表明您的程序不希望用户在不扫描代码以查看可能的副作用的情况下更改元音的定义。如果定义存储在外部,则意味着程序不应因配置中设置的任何合理值而中断。

当您将数据隔离到YAML或文本文件或哑数据库中时,就像从代码中删除肿瘤一样

有人认为这是相反的,也就是说,您正在从宝贵的数据中消除代码的烦恼,请参阅:Torvalds关于优秀程序员的报价


4
Torvalds引用是指数据结构,而不是数据。
user949300

OP指出:“面向对象的编程说“我们想要任意丰富的数据结构”,因此赋予了数据结构以代码能力。
FMJaguar 2014年

1
如果对元音的定义进行了根本性的更改,则需要重新运行所有自动测试。当配置文件在已部署的系统上更改时,系统很少能够重新运行测试。因此,此类定义需要内置到系统中。可能是两个带有配置选项的硬编码集,可以在它们之间进行选择。
soru 2014年

+1为Torvalds的报价。我同意这种观点:在人偶的示例中,我认为问题在于人偶没有良好的数据结构来表示人们想要放入的信息。木偶开发人员并没有修复数据结构,而是断言“代码中的数据”是问题(为什么?这就是问题!),他开发了hiera,我认为这仅仅是将问题移至其他地方,并且使其不可能实现。将行为与数据相关联。
Phil Frost 2014年

2

我在一个项目中,负责人坚持将参考数据放入小表中,我认为这很愚蠢。但是,由于我们已经设置了持久性基础结构和连接性,因此与我们正在执行的其他持久性操作相比,它最终的成本非常低。

现在,我仍然认为这是一个愚蠢的决定,如果我们手头没有基础架构,那我就不会做。

但是我看到一些赞成的论点是:

  • 如果您具有数据库思维方式,则可以将参考数据放入SQL数据库中,以便您加入该数据库以进行报告。
  • 如果您具有管理实用程序或对数据库的访问权限,则可以在运行时调整值。(尽管这可能会玩火。)

另外,有时政策会妨碍编码实践。例如,我曾在几家商店工作过,其中推送.xml文件是可以的,而触摸代码中的一行则需要一个完整的回归周期,甚至可能需要进行负载测试。因此,有一个团队在我的项目的.xml文件非常丰富的地方(也许-heh-可能包含一些代码)。

我总是问自己,我是否会享受将数据从代码中推送到外部数据存储中的好处,即使它只是文本文件也是如此,但是我与曾经以这种方式看待它们的人们一起工作过冲动。


3
关于商店程序的好评论,其中“编辑XML”是可以的,但是在代码中编辑相同的东西却很麻烦。
user949300

在一家商店工作,那里的一切都在数据库中,直到屏幕上的文字。除了用户界面代码外,数据库中唯一没有的是数据库位置和凭据...
jwenting 2014年

3
它听起来总是很愚蠢,直到有一天有人问“我们是否可以为需要它的用户X重新配置它”,然后看起来似乎并不那么愚蠢。该死的客户:)
gbjbaanb 2014年

2
...如果那一天是“从不”,那将是很长一段时间的愚蠢
Rob

2

让我问您一个完全严肃的反问:您认为“数据”和“代码”之间的区别是什么?

当我听到“数据”一词时,我认为是“状态”。从定义上讲,数据是应用程序本身旨在管理的事物,因此,它是应用程序在编译时永远无法知道的事物。这是不能够进行硬编码的数据,因为只要你硬编码,就变成行为-没有数据。

数据类型因应用程序而异;商业发票系统可能会将客户和订单信息存储在SQL数据库中,而矢量图形程序可能会将几何数据和元数据存储在二进制文件中。在这两种情况之间以及两者之间的所有情况下,代码和数据之间都存在清晰且牢不可破的分离。数据属于用户而不是程序员,因此永远不能进行硬编码。

您似乎在谈论的是,使用当前词汇表中可用的技术上最准确的描述:控制程序行为的信息,该信息不是用开发大多数应用程序的主要编程语言编写的。

即使这个定义比“数据”一词的意义要小得多,也存在一些问题。例如,如果程序的重要部分各自以不同的语言编写,该怎么办?我亲自参与了多个项目,这些项目的C#比例约为50%,JavaScript的比例约为50%。JavaScript代码是“数据”吗?大多数人会拒绝。那么HTML是“数据”呢?大多数人还是会拒绝。

CSS呢?那是数据还是代码?如果我们认为代码是控制程序行为的东西,那么CSS并不是真正的代码,因为它只会(主要是)影响外观,而不影响行为。但这也不是真正的数据。用户不拥有它,应用程序甚至都不真正拥有它。这等效于UI设计人员的代码。它类似于代码,但不完全是代码。

我可能将CSS称为一种配置,但是更实际的定义是它只是使用特定域的语言编写的代码。这就是您的XML,YAML和其他“格式化文件”经常代表的内容。而且,我们之所以使用特定领域的语言,是因为通常来说,与使用C或C#或Java这样的通用编程语言对相同信息进行编码相比,它在特定领域中同时更加简洁和更具表现力。

您能识别以下格式吗?

{
    name: 'Jane Doe',
    age: 27,
    interests: ['cats', 'shoes']
}

我敢肯定大多数人都会这样做。它是JSON。这是关于JSON的有趣的事情:在JavaScript中,它显然是代码,在其他每种语言中,它都是清晰格式的数据。几乎每种主流编程语言都至少具有一个用于“解析” JSON的库。

如果我们在JavaScript文件中的函数内使用完全相同的语法,则除了代码之外,其他语法都不可能。但是,如果我们采用该JSON,将其存储在.json文件中,然后在Java应用程序中进行解析,则突然间它就是“数据”。这真的有意义吗?

我认为,以“数据性”或“配置性”或“码率”是与生俱来的东西被描述,而不是如何它被描述。

如果您的程序需要一百万个单词的字典才能生成随机密码,那么您是否要像这样编写代码:

var words = new List<string>();
words.Add("aa");
words.Add("aah");
words.Add("ahhed");
// snip 172836 more lines
words.Add("zyzzyva");
words.Add("zyzzyvas");

还是只是将所有这些单词推到以行分隔的文本文件中,并告诉程序从中读取?单词列表是否永不改变并不重要,这不是您要硬编码还是软编码(在不适当应用的情况下,许多人正确地认为这是反模式),这仅仅是一个问题。哪种格式最有效,并且最容易描述“东西”,无论“东西”是什么。称它为代码还是数据是无关紧要的。它是程序运行所需的信息,而平面文件格式是管理和维护程序的最便捷方法。

假设您遵循正确的做法,那么所有这些东西都将进入源代码控制,因此您最好将其称为代码,只是以不同且可能是非常简约的格式进行代码。或者,您可以将其称为配置,但真正将代码与配置区分开来的唯一一件事就是是否编写文档并告诉最终用户如何进行更改。您可能会发明一些关于在启动时或运行时而不是在编译时解释配置的虚假论点,但是随后您将开始描述几种动态类型的语言,几乎可以肯定其中嵌入了脚本引擎的任何内容(例如,大多数游戏)。代码和配置是您决定将其标记为的内容,仅此而已。

现在,存在实际上无法安全修改的信息外部化的危险(请参见上面的“软编码”链接)。如果将元音数组外部化到配置文件中,并将其作为配置文件记录给最终用户,您将为他们提供一种几乎简单的方法来立即破坏您的应用程序,例如通过将“ q”作为元音。但这不是“代码和数据分离”的根本问题,这只是糟糕的设计意义。

我告诉初级开发人员的是,他们应该始终外部化他们希望根据环境进行更改的设置。其中包括连接字符串,用户名,API密钥,目录路径等。它们在您的开发人员环境和生产环境中可能是相同的,但可能并非如此,系统管理员将决定他们希望它在生产环境中(而不是开发人员)的外观。因此,您需要一种方法,将一组设置应用到某些计算机上,并将其他设置应用到其他计算机上-ergo,外部配置文件(或数据库中的设置等)

但我强调,简单地将一些“数据”放入“文件”与将其外部化为配置不同。将单词词典放入文本文件并不意味着您想要用户(或IT)对其进行更改,而只是使开发人员更容易理解到底发生了什么,并且在必要时进行修改的一种方式。偶尔的变化。同样,如果该表是只读的和/或指示DBA永不使用该表,则将相同的信息放入数据库表中不一定会将其视为行为的外部化。配置意味着数据是可变的,但实际上是由流程和职责而不是格式选择来确定的。

因此,总结一下:

  • “代码”不是严格定义的术语。如果您将定义扩展到包括特定于领域的语言以及任何其他会影响行为的语言,那么许多明显的摩擦将简单地消失,并且都将变得有意义。您可以在一个平面文件中包含未编译的DSL“代码”。

  • “数据”表示由用户或至少开发商以外的其他人拥有的信息,通常在设计时不可用。即使您愿意,也无法对其进行硬编码。除了可能自行修改的代码外,代码与数据之间的分隔只是定义问题,而不是个人喜好。

  • 当过度使用“软编码”时,这可能是一个糟糕的实践,但是并非每个外部化实例都必须构成软编码,并且在“平面文件”中存储信息的许多实例也不一定是进行外部化的真正尝试。

  • 配置是一种特殊类型的软编码,该编码的因为知识的应用程序可能需要在不同的环境中运行的必要。与向每个环境部署不同版本的代码相比,与应用程序一起部署单独的配置文件要少得多(工作也更少危险)。因此,某些类型的软编码实际上是有用的。


1

我建议阅读Oren Eini(又名Ayende Rahien)的经典文章

http://ayende.com/blog/3545/enabling-change-by-hard-coding-everything-the-smart-way

我自己从中获得的收获是专注于简单性和可读性。这可能意味着不太可能重新配置的内容最好进行硬编码(可读)。这使您可以使用编程语言的完整语法来表达参数,并获得有益的副作用,例如代码完成和滥用时的编译器错误。

这样,您就可以避免解析/解释的复杂性(“但其他人可以解析我的YAML / JSON”-将解析的文本映射到特定的API调用可以是一种解释形式),并且可以避免“数据之间的另一步骤”的复杂性。 ”及其使用。

某些情况下即使在这种情况下,也确实可以在数据中进行表达:例如,在3D空间中指定数千个点可能比代码更适合文本文件,尽管在某些语言中,包括使用struct初始化程序的C语言,代码甚至可以做到这一点。


1

好的,假设您想休闲地编写某种c ++程序。您确切知道它必须做什么以及它将永远不需要做什么。现在上任何有关“现代软件设计”的书。这是游戏规则:对于项目中的每个类以及每一个很小的案例,您都必须实现书中描述的每一个奇特的模式,以使您的代码成为“干净的设计”。好吧,“依赖注入”对于许多人来说已经足够了。(它是c ++,不是Java!)从越来越多的理论角度讲授编程。仅仅完成工作还不够,您必须编写可维护的代码,傻瓜证明……一切正常。问题从ppl开始。停止思考实际原因,发明了设计模式并变得教条主义。

让我停止通过(过度)使用一种简单的设计原则来编写字母计数工具:当您编写对某种类型的输入数据执行特定工作的代码时,请确保它能够对任何给定的输入执行该任务该类型的数据。-当您要编写字母计数工具时,以某种方式编写它显然很有意义,这样它不仅可以计算元音,而且可以计算“任何字母”。-由于您可能不知道您实际上要解析的语料库,因此您不妨选择一种非常通用的编码(UTF-16),并涵盖大多数(全部?)书面语言及其符号。

至此,我们有了一个带有两个参数的函数(语料和要计数的字母)。我们只关心找到字母也属于一个合理的通用“类型”或“类”:我们当然可以做得比ASCII符号好!

输入一个带有“一般化和可重用性”教条的恶魔:-为什么不在该类别的输入流中计算任何类别的任何符号?(从字母到任意长度但有限长度的位序列,因为这是您在计算机上获得的最通用的摘要,所以...)-等等,即使如此,我们仍在计算自然数。但是,计数可以概括为从可数集到满足公理的自身的映射... [您明白了]

现在,该示例可能很愚蠢,但是如果您考虑使用比计数工具更复杂的设计任务,则很可能会找到所有机会根据书中发现的某种设计模式引入所需的其他抽象。

“数据”和“代码”的分隔可能很简单(函数参数),或者您发现自己将不变量视为变量(“数据”)。

如果有任何混淆,则很可能是关于“接口”和“服务”,而所有类的特定内容(例如类型)突然变成了“数据”,那就是要从外部注入的依赖项。我觉得在大学教授的信息学课程已经变得非常像哲学讲座,并且真正的项目所需的时间更少了,因此学生可以获得如何制作有效软件的经验。如果您想知道为什么为什么需要使用疯狂的复杂模式而不是显而易见的解决方案,那么这种发展就是(可能)如何“创建”该要求...

针对您的特定问题:如果您可以1.)为您的特定情况编写一个具有最大程度硬编码的程序,然后2.)通过一种简单明了的方式从该代码中进行概括。引入更多的函数参数并使用其他“琐碎的模式”,可以确保以明显的方式分离代码和数据,就像自发明函数式编程以来所做的那样。(您可以跳过1.并立即执行2.。)

这里任何不明显的情况都可能是“ theory-deadlock”的情况:就像编写一个引用一个接口和另一个接口的接口一样……最后,您有一个简洁的xml文件来配置所有这些接口以及要注入到类接口混乱中的依赖项。

只是希望,您随后需要的xml解析器不需要xml-config即可工作...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.