diff --git a/translated/tech/20171007 A Large-Scale Study of Programming Languages and Code Quality in GitHub.md b/translated/tech/20171007 A Large-Scale Study of Programming Languages and Code Quality in GitHub.md index 519e37b240..1fd39cc35b 100644 --- a/translated/tech/20171007 A Large-Scale Study of Programming Languages and Code Quality in GitHub.md +++ b/translated/tech/20171007 A Large-Scale Study of Programming Languages and Code Quality in GitHub.md @@ -16,7 +16,7 @@ 受控实验是检验语言选择在面对如此令人气馁的混淆影响时的一种方法,然而,由于成本的原因,这种研究通常会引入一种它们自己的混淆,也就是说,限制了范围。在这种研究中,完整的任务是必须要受限制的,并且不能去模拟 _真实的世界_ 中的开发。这里有几个最近的这种大学本科生使用的研究,或者,通过一个实验因素去比较静态或动态类型的语言。^[7][4],[12][5],[15][6] -幸运的是,现在我们可以基于大量的真实世界中的软件项目去研究这些问题。GitHub 包含了多种语言的大量的项目,并且在大小、年龄、和开发者数量上有很大的差别。每个项目的仓库都提供一个详细的记录,包含贡献历史、项目大小、作者身份以及缺陷修复。然后,我们使用多种工具去研究语言特性对缺陷发生的影响。对我们的研究方法的最佳描述应该是“混合方法”,或者是三角测量法 ^[5][7];我们使用文本分析、聚簇和可视化去证实和支持量化回归研究的结果。这个以经验为根据的方法,帮助我们去了解编程语言对软件质量的具体影响,因为,他们是被开发者非正式使用的。 +幸运的是,现在我们可以基于大量的真实世界中的软件项目去研究这些问题。GitHub 包含了多种语言的大量的项目,并且在大小、年龄、和开发者数量上有很大的差别。每个项目的仓库都提供一个详细的记录,包含贡献历史、项目大小、作者身份以及缺陷修复。然后,我们使用多种工具去研究语言特性对缺陷发生的影响。对我们的研究方法的最佳描述应该是“混合方法”,或者是三角测量法; ^[5][7] 我们使用文本分析、聚簇和可视化去证实和支持量化回归研究的结果。这个以经验为根据的方法,帮助我们去了解编程语言对软件质量的具体影响,因为,他们是被开发者非正式使用的。 ### 2 方法 @@ -24,7 +24,7 @@ #### 2.1 数据收集 -我们选择了 GitHub 上的排名前 19 的编程语言。剔除了 CSS、Shell 脚本、和 Vim 脚本,因为它们不是通用的编程语言。我们包含了 `Typescript`,它是 `JavaScript` 的超集。然后,对每个被研究的编程语言,我们检索出以它为主要编程语言的前 50 个项目。我们总共分析了 17 种不同的语言,共计 850 个项目。 +我们选择了 GitHub 上的排名前 19 的编程语言。剔除了 CSS、Shell 脚本、和 Vim 脚本,因为它们不是通用的编程语言。我们包含了 Typescript,它是 JavaScript 的超集。然后,对每个被研究的编程语言,我们检索出以它为主要编程语言的前 50 个项目。我们总共分析了 17 种不同的语言,共计 850 个项目。 我们的编程语言和项目的数据是从 _GitHub Archive_ 中提取的,这是一个记录所有活跃的公共 GitHub 项目的数据库。它记录了 18 种不同的 GitHub 事件,包括新提交、fork 事件、PR(拉取请求)、开发者信息和以每小时为基础的所有开源 GitHub 项目的问题跟踪。打包后的数据上传到 Google BigQuery 提供的交互式数据分析接口上。 @@ -38,7 +38,7 @@ **检索流行的项目**  -对于每个选定的编程语言,我们先根据项目所使用的主要编程语言来选出项目,然后根据每个项目的相关 _星_ 的数量排出项目的流行度。 _星_ 的数量表示了有多少人主动表达对这个项目感兴趣,并且它是流行度的一个合适的代表指标。因此,在 C 语言中排名前三的项目是 _linux、git、php-src_ ;而对于 C++,它们则是 _node-webkit、phantomjs、mongo_ ;对于 `Java`,它们则是 _storm、elasticsearch、ActionBarSherlock_ 。每个编程语言,我们各选了 50 个项目。 +对于每个选定的编程语言,我们先根据项目所使用的主要编程语言来选出项目,然后根据每个项目的相关 _星_ 的数量排出项目的流行度。 _星_ 的数量表示了有多少人主动表达对这个项目感兴趣,并且它是流行度的一个合适的代表指标。因此,在 C 语言中排名前三的项目是 linux、git、php-src;而对于 C++,它们则是 node-webkit、phantomjs、mongo ;对于 Java,它们则是 storm、elasticsearch、ActionBarSherlock 。每个编程语言,我们各选了 50 个项目。 为确保每个项目有足够长的开发历史,我们剔除了少于 28 个提交的项目(28 是候选项目的第一个四分位值数)。这样我们还剩下 728 个项目。[表 1][50] 展示了每个编程语言的前三个项目。 @@ -56,62 +56,62 @@ #### 2.2 语言分类 -我们基于影响语言质量的几种编程语言特性定义了语言类别,^[7][9],[8][10],[12][11] ,如 [表 3][53] 所示。 _编程范式_ 表示项目是以命令方式、脚本方式、还是函数语言所写的。在本文的下面部分,我们分别使用 _命令_ 和 _脚本_ 这两个术语去代表命令方式和脚本方式。 +我们基于影响语言质量的几种编程语言特性定义了语言类别,^[7][9],[8][10],[12][11] ,如 [表 3][53] 所示。 + +编程范式Programming Paradigm 表示项目是以命令方式、脚本方式、还是函数语言所写的。在本文的下面部分,我们分别使用 命令procedural脚本scripting 这两个术语去代表命令方式和脚本方式。 [![t3.jpg](http://deliveryimages.acm.org/10.1145/3130000/3126905/t3.jpg)][54] *表 3. 语言分类的不同类型* - _类型检查_  代表静态或者动态类型。在静态类型语言中,在编译时进行类型检查,并且变量名是绑定到一个值和一个类型的。另外,(包含变量的)表达式是根据运行时,它们可能产生的值所符合的类型来分类的。在动态类型语言中,类型检查发生在运行时。因此,在动态类型语言中,它可能出现在同一个程序中,一个变量名可能会绑定到不同类型的对象上的情形。 +类型检查Type Checking  代表静态或者动态类型。在静态类型语言中,在编译时进行类型检查,并且变量名是绑定到一个值和一个类型的。另外,(包含变量的)表达式是根据运行时,它们可能产生的值所符合的类型来分类的。在动态类型语言中,类型检查发生在运行时。因此,在动态类型语言中,它可能出现在同一个程序中,一个变量名可能会绑定到不同类型的对象上的情形。 - _隐式类型转换_ 允许一个类型为 T1 的操作数,作为另一个不同的类型 T2 来访问,而无需进行显式的类型转换。这样的隐式类型转换在一些情况下可能会带来类型混淆,尤其是当它表示一个明确的 T1 类型的操作数时,把它再作为另一个不同的 T2 类型的情况下。因为,并不是所有的隐式类型转换都会立即出现问题,通过我们识别出的允许进行隐式类型转换的所有编程语言中,可能发生隐式类型转换混淆的例子来展示我们的定义。例如,在像 `Perl、 JavaScript、CoffeeScript` 这样的编程语言中,一个字符和一个数字相加是允许的(比如,"5" + 2 结果是 "52")。但是在 `Php` 中,相同的操作,结果是 7。像这种操作在一些编程语言中是不允许的,比如 `Java` 和 `Python`,因为,它们不允许隐式转换。在强数据类型的 C 和 C++ 中,这种操作的结果是不可预料的,例如,`int x;float y;y=3.5;x=y`;是合法的 C 代码,并且对于 x 和 y 其结果是不同的值,具体是哪一个值,取决于含义,这可能在后面会产生问题。[a][12] 在 `Objective-C` 中数据类型 _id_ 是一个通用对象指针,它可以被用于任何数据类型的对象,而不管分类是什么。[b][13] 像这种通用数据类型提供了很好的灵活性,它可能导致隐式的类型转换,并且也会出现不可预料的结果。[c][14] 因此,我们根据它的编译器是否 _允许_  或者  _不允许_  如上所述的隐式类型转换,对编程语言进行分类;而不允许隐式类型转换的编程语言,会显式检测类型混淆,并报告类型不匹配的错误。 +隐式类型转换Implicit Type Conversion 允许一个类型为 T1 的操作数,作为另一个不同的类型 T2 来访问,而无需进行显式的类型转换。这样的隐式类型转换在一些情况下可能会带来类型混淆,尤其是当它表示一个明确的 T1 类型的操作数时,把它再作为另一个不同的 T2 类型的情况下。因为,并不是所有的隐式类型转换都会立即出现问题,通过我们识别出的允许进行隐式类型转换的所有编程语言中,可能发生隐式类型转换混淆的例子来展示我们的定义。例如,在像 Perl、 JavaScript、CoffeeScript 这样的编程语言中,一个字符和一个数字相加是允许的(比如,`"5" + 2` 结果是 `"52"`)。但是在 Php 中,相同的操作,结果是 `7`。像这种操作在一些编程语言中是不允许的,比如 Java 和 Python,因为,它们不允许隐式转换。在强数据类型的 C 和 C++ 中,这种操作的结果是不可预料的,例如,`int x; float y; y=3.5; x=y`;是合法的 C 代码,并且对于 `x` 和 `y` 其结果是不同的值,具体是哪一个值,取决于含义,这可能在后面会产生问题。^[a][12] 在 `Objective-C` 中,数据类型 _id_ 是一个通用对象指针,它可以被用于任何数据类型的对象,而不管分类是什么。^[b][13] 像这种通用数据类型提供了很好的灵活性,它可能导致隐式的类型转换,并且也会出现不可预料的结果。^[c][14] 因此,我们根据它的编译器是否 _允许_  或者  _不允许_  如上所述的隐式类型转换,对编程语言进行分类;而不允许隐式类型转换的编程语言,会显式检测类型混淆,并报告类型不匹配的错误。 -不允许隐式类型转换的编程语言,使用一个类型判断算法,比如,Hindley[10][15] 和 Milner[17][16],或者,在运行时上使用一个动态类型检查器,可以在一个编译器(比如,使用 `Java`)中判断静态类型的结果。相比之下,一个类型混淆可能会悄无声息地发生,因为,它可能因为没有检测到,也可能是没有报告出来。无论是哪种方式,允许隐式类型转换在提供了灵活性的同时,最终也可能会出现很难确定原因的错误。为了简单起见,我们将用 _implicit_ 代表允许隐式类型转换的编程语言,而不允许隐式类型转换的语言,我们用 _explicit_ 代表。 +不允许隐式类型转换的编程语言,使用一个类型判断算法,比如,Hindley ^[10][15] 和 Milner,^[17][16] 或者,在运行时上使用一个动态类型检查器,可以在一个编译器(比如,使用 Java)中判断静态类型的结果。相比之下,一个类型混淆可能会悄无声息地发生,因为,它可能因为没有检测到,也可能是没有报告出来。无论是哪种方式,允许隐式类型转换在提供了灵活性的同时,最终也可能会出现很难确定原因的错误。为了简单起见,我们将用 隐含implicit 代表允许隐式类型转换的编程语言,而不允许隐式类型转换的语言,我们用 明确explicit 代表。 - _内存分类_  表示是否要求开发者去管理内存。尽管 `Objective-C` 遵循了一个混合模式,我们仍将它在不托管的分类中来对待,因为,我们在它的代码库中观察到很多的内存错误,在第 3 节的 RQ4 中会讨论到。 +内存分类Memory Class  表示是否要求开发者去管理内存。尽管 Objective-C 遵循了一个混合模式,我们仍将它放在不管理的分类中来对待,因为,我们在它的代码库中观察到很多的内存错误,在第 3 节的 RQ4 中会讨论到。 -请注意,我们之所以使用这种方式对编程语言来分类和研究,是因为,这种方式在一个“真实的世界”中被大量的开发人员非正式使用。例如,`TypeScript` 被有意地分到静态编程语言的分类中,它不允许隐式类型转换。然而,在实践中,我们注意到,开发者经常(有 50% 可变化,并且跨 `TypeScript` - 在我们的数据集中使用的项目)使用 `any` 类型,一个笼统的联合类型,并且,因此在实践中,`TypeScript` 允许动态地、隐式类型转换。为减少混淆,我们从我们的编程语言分类和相关的模型中排除了 `TypeScript`(查看 [表 3][55] 和 [7][56])。 +请注意,我们之所以使用这种方式对编程语言来分类和研究,是因为,这种方式在一个“真实的世界”中被大量的开发人员非正式使用。例如,TypeScript 被有意地分到静态编程语言的分类中,它不允许隐式类型转换。然而,在实践中,我们注意到,开发者经常(有 50% 的变量,并且跨 TypeScript —— 在我们的数据集中使用的项目)使用 `any` 类型,这是一个笼统的联合类型,并且,因此在实践中,TypeScript 允许动态地、隐式类型转换。为减少混淆,我们从我们的编程语言分类和相关的模型中排除了 TypeScript(查看 [表 3][55] 和 [7][56])。 +#### 2.3 识别项目领域 - **2.3 识别项目领域** +我们基于编程语言的特性和功能,使用一个自动加手动的混合技术,将研究的项目分类到不同的领域。在 GitHub 上,项目使用 `project descriptions` 和 `README` 文件来描述它们的特性。我们使用一种文档主题生成模型(Latent Dirichlet Allocation,缩写为:LDA) ^[3][17] 去分析这些文本。提供一组文档给它,LDA 将生成不同的关键字,然后来识别可能的主题。对于每个文档,LDA 也估算每个主题分配的文档的概率。 -我们基于编程语言的特性和功能,使用一个自动和手动的混合技术,将研究的项目分类到不同的领域。在 GitHub 上,项目使用 `project descriptions` 和 README 文件来描述它们的特性。我们使用文档主题生成模型(Latent Dirichlet Allocation,缩写为:LDA)[3][17] 去分析这些文本。提供一组文档给它,LDA 将生成不同的关键字,然后来识别可能的主题。对于每个文档,LDA 也估算每个主题分配的文档的概率。 - -我们检测到 30 个不同的领域,换句话说,就是主题,并且评估了每个项目从属于每个领域的概率。因为,这些自动检测的领域包含了几个具体项目的关键字,例如,facebook,很难去界定它的底层的常用功能。为了给每个领域分配一个有意义的名字,我们手动检查了识别到的、独立于项目名字的 30 个领域、领域识别关键字。我们手动重命名了所有的 30 个自动检测的领域,并且找出了以下六个领域的大多数的项目:应用程序、数据库、代码分析、中间件、库、和框架。我们也找出了不符合以上任何一个领域的一些项目,因此,我们把这个领域笼统地标记为 _其它_ 。随后,我们研究组的另一名成员检查和确认了这种项目领域分类的方式。[表 4][57] 汇总了这个过程识别到的领域结果。 +我们检测到 30 个不同的领域(换句话说,就是主题),并且评估了每个项目从属于每个领域的概率。因为,这些自动检测的领域包含了几个具体项目的关键字,例如,facebook,很难去界定它的底层的常用功能。为了给每个领域分配一个有意义的名字,我们手动检查了 30 个与项目名字无关的用于识别领域的领域识别关键字。我们手动重命名了所有的 30 个自动检测的领域,并且找出了以下六个领域的大多数的项目:应用程序、数据库、代码分析、中间件、库,和框架。我们也找出了不符合以上任何一个领域的一些项目,因此,我们把这个领域笼统地标记为 _其它_ 。随后,我们研究组的另一名成员检查和确认了这种项目领域分类的方式。[表 4][57] 汇总了这个过程识别到的领域结果。 [![t4.jpg](http://deliveryimages.acm.org/10.1145/3130000/3126905/t4.jpg)][58] -**表 4 领域特征** + +*表 4 领域特征* +#### 2.4 bug 分类 - **2.4 bugs 分类** +尽管修复软件 bug 时,开发者经常会在提交日志中留下关于这个 bug 的原始的重要信息;例如,为什么会产生 bug,以及怎么去修复 bug。我们利用很多信息去分类 bug,与 Tan 的 _et al_ 类似。 ^[13][18],[24][19] -尽管修复软件 bugs 时,开发者经常会在提交日志中留下关于这个 bugs 的原始的重要信息;例如,为什么会产生 bugs,以及怎么去修复 bugs。我们利用很多信息去分类 bugs,与 Tan 的 _et al_ 类似 [13][18],[24][19]。 - -首先,我们基于 bugs 的 _原因_ 和 _影响_ 进行分类。_ 原因 _ 进一步分解为不相关的错误子类:算法方面的、并发方面的、内存方面的、普通编程错误、和未知的。bug 的 _影响_  也分成四个不相关的子类:安全、性能、失败、和其它的未知类。因此,每个 bug 修复提交也包含原因和影响的类型。[表 5][59] 展示了描述的每个 bug 分类。这个类别分别在两个阶段中被执行: +首先,我们基于 bug 的 原因Cause影响Impact 进行分类。_ 原因 _ 进一步分解为不相关的错误子类:算法方面的、并发方面的、内存方面的、普通编程错误,和未知的。bug 的 _影响_  也分成四个不相关的子类:安全、性能、失败、和其它的未知类。因此,每个 bug 修复提交也包含原因和影响的类型。[表 5][59] 展示了描述的每个 bug 分类。这个类别分别在两个阶段中被执行: [![t5.jpg](http://deliveryimages.acm.org/10.1145/3130000/3126905/t5.jpg)][60] -**表 5 bugs 分类和在整个数据集中的描述** -**(1) 关键字搜索** 我们随机选择了 10% 的 bug 修复信息,并且使用一个关键字基于搜索技术去对它们进行自动化分类,作为可能的 bug 类型。我们对这两种类型(原因和影响),单独使用这个注释。我们选择了一个限定的关键字和习惯用语集,如 [表 5][61] 所展示的。像这种限定的关键字和习惯用语集可以帮我们降低误报。 +*表 5 bug 分类和在整个数据集中的描述* -**(2) 监督分类** 我们使用前面步骤中的有注释的 bug 修复日志作为训练数据,为监督学习分类技术,通过测试数据来矫正,去对剩余的 bug 修复信息进行分类。我们首先转换每个 bug 修复信息为一个词袋(译者注:bag-of-words,一种信息检索模型)。然后,删除在所有的 bug 修复信息中仅出现过一次的词。这样减少了具体项目的关键字。我们也使用标准的自然语言处理技术来解决这个问题。最终,我们使用支持向量机(译者注:Support Vector Machine,缩写为 SVM,在机器学习领域中,一种有监督的学习算法)去对测试数据进行分类。 +**(1) 关键字搜索** 我们随机选择了 10% 的 bug 修复信息,并且使用一个基于关键字的搜索技术去对它们进行自动化分类,作为可能的 bug 类型。我们对这两种类型(原因和影响)分别使用这个注释。我们选择了一个限定的关键字和习惯用语集,如 [表 5][61] 所展示的。像这种限定的关键字和习惯用语集可以帮我们降低误报。 + +**(2) 监督分类** 我们使用前面步骤中的有注释的 bug 修复日志作为训练数据,为监督学习分类技术,通过测试数据来矫正,去对剩余的 bug 修复信息进行分类。我们首先转换每个 bug 修复信息为一个词袋(LCTT 译注:bag-of-words,一种信息检索模型)。然后,删除在所有的 bug 修复信息中仅出现过一次的词。这样减少了具体项目的关键字。我们也使用标准的自然语言处理技术来解决这个问题。最终,我们使用支持向量机(LCTT 译注:Support Vector Machine,缩写为 SVM,在机器学习领域中,一种有监督的学习算法)去对测试数据进行分类。 为精确评估 bug 分类器,我们手动注释了 180 个随机选择的 bug 修复,平均分布在所有的分类中。然后,我们比较手动注释的数据集在自动分类器中的结果。最终处理后的,表现出的精确度是可接受的,性能方面的精确度最低,是 70%,并发错误方面的精确度最高,是 100%,平均是 84%。再次运行,精确度从低到高是 69% 到 91%,平均精确度还是 84%。 -我们的 bug 分类的结果展示在 [表 5][62] 中。大多数缺陷的原因都与普通编程错误相关。这个结果并不意外,因为,在这个分类中涉及了大量的编程错误,比如,类型错误、输入错误、编写错误、等等。我们的技术并不能将在任何(原因或影响)分类中占比为 1.4% 的 bug 修复信息再次进行分类;我们将它归类为 Unknown。 +我们的 bug 分类的结果展示在 [表 5][62] 中。大多数缺陷的原因都与普通编程错误相关。这个结果并不意外,因为,在这个分类中涉及了大量的编程错误,比如,类型错误、输入错误、编写错误、等等。我们的技术并不能将在任何(原因或影响)分类中占比为 1.4% 的 bug 修复信息再次进行分类;我们将它归类为未知。 - **2.5 统计方法** +#### 2.5 统计方法 -我们使用回归模型对软件项目相关的其它因素中的有缺陷的提交数量进行了建模。所有的模型使用 _负二项回归(译者注:negative binomial regression,缩写为NBR,一种回归分析模型)_ 去对项目属性计数进行建模,比如,提交数量。NBR 是一个广义的线性模型,用于对非负整数进行响应建模。[4][20] +我们使用回归模型对软件项目相关的其它因素中的有缺陷的提交数量进行了建模。所有的模型使用负二项回归negative binomial regression(缩写为 NBR)(LCTT 译注:一种回归分析模型) 去对项目属性计数进行建模,比如,提交数量。NBR 是一个广义的线性模型,用于对非负整数进行响应建模。^[4][20] -在我们的模型中,我们对每个项目的编程语言,控制几个可能影响最终结果的因素。因此,在我们的回归分析中,每个(语言、项目)对是一个行,并且可以视为来自流行的开源项目中的样本。我们依据变量计数进行对象转换,以使变量保持稳定,并且提升了模型的适用度。[4][21] 我们通过使用 AIC 和 Vuong 对非嵌套模型的测试比较来验证它们。 +在我们的模型中,我们对每个项目的编程语言,控制几个可能影响最终结果的因素。因此,在我们的回归分析中,每个(语言/项目)对是一个行,并且可以视为来自流行的开源项目中的样本。我们依据变量计数进行对象转换,以使变量保持稳定,并且提升了模型的适用度。^[4][21] 我们通过使用 AIC 和 Vuong 对非嵌套模型的测试比较来验证它们。 -去检查那些过度的多重共线性(译者注:多重共线性是指,在线性回归模型中解释变量之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确。)并不是一个问题,我们在所有的模型中使用一个保守的最大值 5,去计算每个依赖的变量的膨胀因子的方差。[4][22] 我们通过对每个模型的残差和杠杆图进行视觉检查来移除高杠杆点,找出库克距离(译者注:一个统计学术语,用于诊断回归分析中是否存在异常数据)的分离值和最大值。 +去检查那些过度的多重共线性(LCTT 译注:多重共线性是指,在线性回归模型中解释变量之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确。)并不是一个问题,我们在所有的模型中使用一个保守的最大值 5,去计算每个依赖的变量的膨胀因子的方差。^[4][22] 我们通过对每个模型的残差和杠杆图进行视觉检查来移除高杠杆点,找出库克距离(LCTT 译注:一个统计学术语,用于诊断回归分析中是否存在异常数据)的分离值和最大值。 -我们利用 _效果_ ,或者 _差异_ ,编码到我们的研究中,以提高编程语言回归系数的表现。[4][23] 加权的效果代码允许我们将每种编程语言与所有编程语言的效果进行比较,同时弥补了跨项目使用编程语言的不均匀性。[23][24] 去测试两种变量因素之间的联系,我们使用一个独立的卡方检验(译者注:Chi-square,一种统计学上的假设检验方法)测试。[14][25] 在证实一个依赖之后,我们使用 Cramer 的 V,它是与一个 _r_  ×  _c_ 等价的正常数据的 phi(φ)系数,去建立一个效果数据。 - -[Back to Top][63] +我们利用 _效果_ ,或者 _差异_ ,编码到我们的研究中,以提高编程语言回归系数的表现。^[4][23] 加权的效果代码允许我们将每种编程语言与所有编程语言的效果进行比较,同时弥补了跨项目使用编程语言的不均匀性。^[23][24] 去测试两种变量因素之间的联系,我们使用一个独立的卡方检验(LCTT 译注:Chi-square,一种统计学上的假设检验方法)测试。^[14][25] 在证实一个依赖之后,我们使用 Cramer 的 V,它是与一个 `r × c` 等价的正常数据的 `phi(φ)` 系数,去建立一个效果数据。 ### 3 结果