14

我喜欢从版本控制系统 (VCS) 中区分三种不同类型的冲突:

  • 文本的
  • 句法
  • 语义

文本冲突是由合并或更新过程检测到的冲突。这是由系统标记的。在解决冲突之前,VCS 不允许提交结果。

VCS 不会标记语法冲突,但结果不会编译。因此,即使是稍微细心的程序员也应该了解这一点。(一个简单的示例可能是Left重命名变量,而Right使用该变量添加了一些行。合并可能会有一个未解析的符号。或者,这可能会通过变量隐藏引入语义冲突。)

最后,VCS 不标记语义冲突,结果编译,但代码可能在运行时出现问题。在轻微的情况下,会产生不正确的结果。在严重的情况下,可能会导致崩溃。即使是这些也应该由非常细心的程序员在提交之前通过代码审查或单元测试来检测。

我的语义冲突示例使用 SVN(Subversion)和 C++,但这些选择与问题的本质并不真正相关。

基本代码是:

int i = 0;
int odds = 0;
while (i < 10)
{
    if ((i & 1) != 0)
    {
        odds *= 10;
        odds += i;
    }
    // next
    ++ i;
}
assert (odds == 13579)

左 ( L) 和右 ( R) 更改如下。

Left '优化'(改变循环变量的值):

int i = 1; // L
int odds = 0;
while (i < 10)
{
    if ((i & 1) != 0)
    {
        odds *= 10;
        odds += i;
    }
    // next
    i += 2; // L
}
assert (odds == 13579)

正确的“优化”(改变循环变量的使用方式):

int i = 0;
int odds = 0;
while (i < 5) // R
{
    odds *= 10;
    odds += 2 * i + 1; // R
    // next
    ++ i;
}
assert (odds == 13579)

这是合并或更新的结果,SVN 没有检测到(这对于 VCS 来说是正确的行为),因此它不是文本冲突。请注意,它可以编译,因此它不是句法冲突。

int i = 1; // L
int odds = 0;
while (i < 5) // R
{
    odds *= 10;
    odds += 2 * i + 1; // R
    // next
    i += 2; // L
}
assert (odds == 13579)

assert失败是因为是odds37。

所以我的问题如下。还有比这更简单的例子吗?有没有一个简单的例子,编译的可执行文件有一个新的崩溃?

作为第二个问题,您在实际代码中是否遇到过这种情况?同样,简单的例子特别受欢迎。

4

3 回答 3

8

提出简单的相关示例并不明显,此评论最好地总结了原因:

如果更改就在附近,那么微不足道的解决方案更有可能是正确的(因为那些不正确的解决方案更有可能触及代码的相同部分,从而导致非微不足道的冲突),并且在少数情况下,它们不是,问题会相对较快地表现出来,并且可能以明显的方式表现出来。

[这基本上就是你的例子所说明的]

但是,检测由广泛分离的代码区域中的更改之间的合并引入的语义冲突可能需要比大多数程序员拥有更多的程序 - 或者在内核大小的项目中,比任何程序员都可以。
因此,即使您确实手动查看了这些 3 向差异,这也将是一个相对无用的练习:努力与信心的获得相去甚远。

事实上,我认为合并是一条红鲱鱼:
代码的不同但相互依赖的部分之间的这种语义冲突是不可避免的,因为它们可以分开发展。
这个并发开发过程是如何组织的——DVCS;CVCS;压缩包和补丁;每个人都在网络共享上编辑相同的文件——这与这一事实无关。
合并不会导致语义冲突,编程会导致语义冲突。

换句话说,我在合并后的实际代码中遇到的语义冲突的真实情况并不简单,而是相当复杂。


话虽如此,Martin Fowler 在他的文章 Feature Branch 中说明的最简单的例子是方法重命名:

我更担心的问题是语义冲突。
一个简单的例子是,如果 Plum 教授更改了 Reverend Green 的代码调用的方法的名称。重构工具允许您安全地重命名方法,但仅限于您的代码库。
因此,如果 G1-6 包含调用 foo 的新代码,Plum 教授在他的代码库中无法分辨,因为他没有。你只会发现大合并。

函数重命名是语义冲突的一个相对明显的例子。
在实践中,它们可以更加微妙。

测试是发现它们的关键,但是要合并的代码越多,你就越有可能发生冲突,修复它们就越困难
冲突的风险,尤其是语义冲突,让大合并变得可怕。


正如Ole Lynge在他的回答(赞成)中提到的那样, Martin Fowler今天(本次编辑时)确实写了一篇关于“语义冲突”的帖子,包括以下插图:

语义冲突图

同样,这是基于函数重命名,即使提到了基于内部函数重构的更微妙的情况:

最简单的例子是重命名函数。
假设我认为clcBl如果调用该方法会更容易使用calculateBill

所以这里的第一点是,无论你的工具多么强大,它只会保护你免受文本冲突的影响。

然而,有一些策略可以极大地帮助我们处理它们

  • 其中第一个是SelfTestingCode。测试有效地探测我们的代码,看看他们对代码语义的看法是否与代码实际所做的一致
  • 另一种有用的技术是更频繁地合并

人们经常尝试根据 DVCS 如何使功能分支变得容易来证明其合理性。但这忽略了语义冲突的问题。
如果您的功能在几天内快速构建,那么您将遇到更少的语义冲突(如果不到一天,那么它实际上与 CI 相同)。但是,我们不会经常看到如此短的功能分支。

我认为需要在快照分支和特征分支之间找到一个中间地带。
如果您有一组开发人员在同一个功能分支上,那么合并通常是关键。

于 2010-03-25T11:53:12.530 回答
3

查看 Martin Fowler 在这篇文章中的示例:http ://martinfowler.com/bliki/SemanticConflict.html

于 2011-08-04T16:51:26.557 回答
0

场景:foo()存在一个方法。两个分支从这里开始。

  1. 分支 1 重命名foo()food().
  2. 分支 2 向foo().

当分支 1 和 2 合并时,没有可检测到的冲突。但是,分支 2 的调用foo()现在引用了一个不再存在的方法。

于 2020-01-31T19:15:34.633 回答