更好、更简单的"语义冲突"例子是什么?

16

我喜欢从版本控制系统(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) 的变化如下。

左边的“优化”(更改循环变量的值):

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)

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

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

这是合并或更新的结果,不会被SVN检测到(这是版本控制系统的正确行为),因此它不是文本冲突。请注意,它可以编译,因此它不是语法冲突。

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失败是因为odds的值为37。

我的问题是,是否存在比这更简单的例子?是否有一个简单的例子,编译后的可执行文件会崩溃?

作为一个次要的问题,你遇到过类似于这种在真实代码中出现的情况吗?同样,欢迎提供简单的例子。


这与C++没有特别的关系,所以我已经移除了那个标签。说实话,我不理解这个问题,也看不出它与版本控制有什么关系。 - anon
1
rhubbarb可能想听听其他人在语义方面未被注意到的变化方面的经验,以便他可以建立某种清单,在合并代码时要注意哪些事项。如果这确实是重点,那么这可能会成为一个有趣的话题。 - Tomislav Nakic-Alfirevic
4
@Tomislav: 是的,那是其中一个原因。 @Neil: 我同意你移除 C++ 标签;这是我的错误。另一方面,这与版本控制有 完全 关系。合并操作是版本控制系统的核心,了解这些非常有用的系统偶尔可能会产生意外结果也很重要。 - Rhubbarb
@Neil:这让我想起了你在类似话题上的回答:https://dev59.com/IkjSa4cB1Zd3GeqPIctE#1245459 - VonC
大家好,我们一直在倾听您的需求,并推出了一个工具,很可能会解决部分痛点:http://plasticscm.com/sm/index.html,我们现在正在开始封闭测试。 - pablo
3个回答

9

很难想出简单而又相关的例子,这条评论最好地总结了原因:

如果更改比较接近,则微不足道的解决方案更有可能是正确的(因为那些不正确的解决方案更有可能触及代码的相同部分,从而导致非微不足道的冲突),在极少数情况下,它们并非如此,问题将相对迅速地表现出来,可能是显而易见的。

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

但是,检测到由代码不同区域的更改合并引入的语义冲突可能需要将程序的更多内容保留在您的头脑中,而大多数程序员可能无法做到这一点 - 或者在内核大小的项目中,任何程序员都无法做到这一点。因此,即使您手动审查了那些三方差异,它也是一种相对无用的练习:付出的努力与信心的收益相比要远远不成比例。
事实上,我认为合并是一个红鱼:
在代码的不同但相互依赖的部分之间发生这种语义冲突是不可避免的,这一刻它们可以分别发展。
并发开发过程的组织方式 - DVCS; CVCS; tarballs和补丁;每个人都在网络共享上编辑相同的文件 - 对于这个事实来说没有任何影响。合并不会导致语义冲突,编程会导致语义冲突。

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

话虽如此,最简单的例子,就像马丁·福勒在他的文章《特性分支》中所述一样,是方法重命名:

我更担心的问题是语义冲突。
这个问题的一个简单例子是:如果Plum教授更改了Green牧师代码调用的某个方法的名称。重构工具可以安全地重命名方法,但仅限于您的代码库。
因此,如果G1-6包含调用foo的新代码,则Plum教授无法在自己的代码库中找到它。只有在进行大合并时才会发现。

函数重命名是语义冲突的一个相对明显的案例。
实际上它们可能更加微妙。

测试是发现它们的关键,但要合并的代码越多,发生冲突的可能性就越大,修复它们就越困难
正是冲突的风险,特别是语义冲突,使得大型的合并过程令人生畏。


正如Ole Lynge他的回答(已赞)中提到的那样,Martin Fowler今天(此编辑时间)写了一篇关于“语义冲突”的文章,其中包括以下插图:

semantic conflict illustration

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

最简单的例子就是重命名一个函数。
比如说,我觉得方法 clcBl 如果改为 calculateBill 会更容易使用。所以我们首先要明确一点,无论你的工具有多么强大,它只能保护你免受文本冲突的影响。
但是,有几种策略可以显著帮助我们处理这些冲突:
  • 第一种方法是 自我测试代码。测试实际上是在探测我们的代码,看看他们对代码语义的理解与代码实际所做的是否一致。
  • 另一种有帮助的技术是更频繁地合并。
通常人们试图通过功能分支易于实现来为分布式版本控制系统(DVCS)辩护。但这忽略了语义冲突的问题。如果您的功能在短时间内得到快速构建,比如几天之内,那么您将遇到较少的语义冲突(如果不到一天,则实际上相当于 CI)。然而,我们很少看到这种短期功能分支。
我认为需要在短期分支和功能分支之间找到一个折中点。如果您有一组开发人员在同一个功能分支上,则经常合并是关键。

1
我提出这个问题的部分原因是因为我计划做一个关于源代码控制一般和SVN特别的演示。我想要阐述的一点是没有软件工具可以替代良好的计划和良好的沟通。另一个要阐述的观点是SVN做得很好,但它不能读取你的思想。这就是为什么我需要一个人工简单的例子。谢谢你的回答。我仍然希望对我的问题有更多的回答或评论... - Rhubbarb
1
@rhubbarb:明白了,我觉得你的问题很有意思。关于SVN,真正的问题当然是合并:参见https://dev59.com/EnE95IYBdhLWcg3wDpqg#2477089和https://dev59.com/n3E95IYBdhLWcg3wEpvo#2472251。 - VonC
哇,我已经整天在阅读有关语义冲突的内容了(实际上是为了理解在大多数关于成功自动合并的愉快谈话中对它们的病态沉默......),现在这一个简单的句子终于为正确的方向带来了清晰的光芒:“合并不会导致语义冲突,编程才会导致语义冲突。” 谢谢! :)(顺便提一下,这是原始评论所在的地方,位于一个惊人的博客文章下面:http://yosefk.com/blog/dvcs-and-its-most-vexing-merge.html) - Sz.

3

优秀的参考资料。+1 我已经在我的答案中包含了它。 - VonC

0

场景:存在一个名为 foo() 的方法。从此处开始有两个分支。

  1. 分支1将foo()重命名为food()
  2. 分支2添加了对foo()的新调用。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接