编译错误重构是否错误?

时间:2009-01-01 08:27:38

标签: refactoring compiler-errors

我已经习惯通过引入编译错误来进行一些重构。例如,如果我想从我的类中删除一个字段并使其成为某些方法的参数,我通常首先删除该字段,这会导致该类的编译错误。然后我会将参数引入我的方法,这将打破调用者。等等。这通常给我一种安全感。我还没有读过关于重构的任何书籍,但我曾经认为这是一种相对安全的方法。但我想知道,它真的安全吗?或者这是一种糟糕的做事方式?

13 个答案:

答案 0 :(得分:10)

重构时我从不依赖简单的编译,代码可以编译,但可能已经引入了bug。

我认为只为你想要重构的方法或类编写一些单元测试才是最好的,然后通过在重构之后运行测试你将确保没有引入错误。

我不是说去测试驱动开发,只是编写单元测试以获得你需要重构的必要信心。

答案 1 :(得分:9)

这是一种用于静态编译语言的常用且有用的技术。您正在做的一般版本可以表述如下:

  

当您对可能使该模块的客户端中的某些用途无效的模块进行更改时,请以导致编译时错误的方式进行初始更改。

有各种各样的推论:

  • 如果方法,功能或过程的含义发生变化,且类型也不变,则更改名称。 (当您仔细检查并修复所有用途时,您可能会更改名称。)

  • 如果将新案例添加到数据类型或枚举的新文字,请更改所有现有数据类型构造函数或枚举文字的名称。 (或者,如果你足够幸运,有一个编译器可以检查案例分析是否详尽无遗,那么有更简单的方法。)

  • 如果您使用的是重载语言,只需更改一个变体或添加新变体。您可能会以不同的方式以静默方式解决重载问题。如果你使用重载,很难让编译器以你希望的方式为你工作。我知道处理重载的唯一方法是全局推理所有用途。如果您的IDE无法帮助您,则必须更改所有重载变体的名称。不愉快。

您真正在做的是使用编译器来帮助您检查代码中可能需要更改的所有位置。

答案 2 :(得分:5)

我认为没有任何问题。它是安全的,只要你在编译之前没有提交更改,它就没有长期影响。此外,Resharper和VS拥有的工具可以让您的工作更轻松。

你在TDD的另一个方向上使用类似的过程 - 你编写的代码可能没有定义的方法,导致它不能编译,然后你编写足够的代码来编译(然后传递测试,等等.. 。)

答案 3 :(得分:5)

当您准备阅读有关该主题的书籍时,我推荐Michael Feather的“Working Effectively with Legacy Code”。 (非作者添加:Fowler的经典着作“Refactoring” - Refactoring网站可能有用。

他谈到在进行更改之前确定您正在使用的代码的特征,并执行他所谓的临时重构。这是重新发现以找到代码的特征,然后抛弃结果。

您正在做的是使用编译器作为自动测试。它将测试您的代码是否编译,但如果行为因您的重构而发生更改或者是否存在任何副作用,则会进行测试。

考虑一下

class myClass {
     void megaMethod() 
     {
         int x,y,z;
         //lots of lines of code
         z = mysideEffect(x)+y;
         //lots more lines of code 
         a = b + c;
     }
}

你可以重构添加

class myClass {
     void megaMethod() 
     {
         int a,b,c,x,y,z;
         //lots of lines of code
         z = addition(x,y);
         //lots more lines of code
         a = addition(b,c);  
     }

     int addition(int a, b)
     {
          return mysideaffect(a)+b;
     }
}

这会起作用,但第二个附加因为调用方法会有误。除了编译之外,还需要进一步的测试。

答案 4 :(得分:4)

很容易想到一个例子,其中编译器错误的重构会无声地失败并产生意想不到的结果。
想到的几个案例:(我假设我们正在谈论C ++)

  • 将参数更改为使用默认参数存在其他重载的函数。在重构之后,参数的最佳匹配可能不是您所期望的。
  • 具有强制转换运算符或非显式单个参数构造函数的类。根据所涉及的星座,更改,添加,删除或更改任何这些参数可以改变被调用的最佳匹配。
  • 更改虚函数而不更改基类(或以不同方式更改基类)将导致调用被定向到基类。
仅当您绝对确定编译器将捕获需要进行的每个更改时,才应使用

依赖于编译器错误。我几乎总是对此持怀疑态度。

答案 5 :(得分:3)

我想补充一下这里的所有智慧,还有一个案例可能不安全。反射。这会影响.NET和Java等环境(当然还有其他环境)。您的代码将编译,但当Reflection尝试访问不存在的变量时仍会存在运行时错误。例如,如果您使用像Hibernate这样的ORM而忘记更新映射XML文件,这可能很常见。

在整个代码文件中搜索特定的变量/方法名称可能会更安全一些。当然,它可能会带来很多误报,因此它不是一个通用的解决方案;你也可以在你的反射中使用字符串连接,这也会使这无用。但它距离安全至少还有一步之遥。

除了手动完成所有代码之外,我认为不存在100%万无一失的方法。

答案 6 :(得分:2)

这是我认为非常常见的方式,因为它找到了对该特定事物的所有引用。但是,现代IDE(如Visual Studio)具有“查找所有引用”功能,这使得这不必要。

然而,这种方法存在一些缺点。对于大型项目,编译应用程序可能需要很长时间。另外,不要长时间这样做(我的意思是,尽快让事情恢复正常)并且不要一次做多件事,因为你可能会忘记正确修改的方法第一次。

答案 7 :(得分:2)

这是一种方式,如果不知道你重构的代码是什么以及你做出的选择,就没有明确的说明它是安全还是不安全。

如果它适合你,那么没有理由为了改变而改变,但是当你有时间阅读时,这里的资源可能会给你一些你可能想要探索的新想法。

http://www.refactoring.com/

答案 8 :(得分:2)

如果您使用其中一个dotnet语言,您可以考虑的另一个选项是使用Obsolete属性标记“旧”方法,该属性将引入所有编译器警告,但仍然可以保留代码可调用您无法控制的代码(例如,如果您正在编写API,或者您未在VB.Net中使用option strict)。你可以快乐地重构,让过时的版本调用新版本;举个例子:

    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    public int Login()
    {
        /* do stuff */
    }

变为:

    [ObsoleteAttribute()]
    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    [ObsoleteAttribute("Replaced by Login(username, password)")]
    public int Login()
    {
        Login(Username, Pasword);
    }

    public int Login(string username, string password)
    {
        /* do stuff */
    }

无论如何,我倾向于这样做......

答案 9 :(得分:1)

这是一种常见方法,但结果可能会因您的语言是静态还是动态而有所不同。

在静态类型语言中,这种方法有一定意义,因为您引入的任何差异都将在编译时捕获。但是,动态语言通常只会在运行时遇到这些问题。这些问题不会被编译器捕获,而是由您的测试套件捕获;假设你写了一个。

我的印象是你正在使用像C#或Java这样的静态语言,所以继续这种方法,直到遇到某种主要问题,说你应该这样做。

答案 10 :(得分:1)

我做了通常的重构,但仍然通过引入编译器错误来进行重构。我通常在更改不那么简单并且这种重构不是真正的重构(我正在改变功能)时执行它们。那些编译器错误给了我一些我需要看一看的地方,并且比名称或参数更改做出更复杂的更改。

答案 11 :(得分:1)

这听起来类似于测试驱动开发中使用的绝对标准方法:编写测试引用一个不存在的类,因此执行测试的第一步是添加类,然后是方法,等等。有关详尽的Java示例,请参阅Beck's book

你的重构方法听起来很危险,因为你没有任何安全测试(至少你没有提到你有任何测试)。您可能会创建实际上并不执行所需操作的编译代码,或者破坏应用程序的其他部分。

我建议你在练习中添加一个简单的规则:只在单元测试代码中进行非编译更改 。这样,您确定每次修改都至少有一个本地测试,并且您在测试之前记录了修改的意图。

顺便说一下,Eclipse在Java中使这个“失败,存根,写入”方法非常简单:每个不存在的对象都标记为你,而Ctrl-1加上一个菜单选项告诉Eclipse为你编写一个(可编译的)存根!我很想知道其他语言和IDE是否提供类似的支持。

答案 12 :(得分:1)

在某种意义上说,它是“安全的”,在经过充分编译时检查的语言中,它会强制您更新所有已更改内容的实时引用。

如果你有条件编译的代码,例如你已经使用过C / C ++预处理器,它仍然会出错。因此,请确保在所有可能的配置和所有平台上重建(如果适用)。

它不会消除测试更改的需要。如果您已向函数添加了参数,则编译器无法告诉您更新该函数的每个调用站点时提供的值是否为正确值。如果您删除了参数,仍然可能会出错,例如,更改:

void foo(int a, int b);

void foo(int a);

然后从以下地址更改通话:

foo(1,2);

为:

foo(2);

编译很好,但这是错误的。

就个人而言,我确实使用编译(和链接)失败作为一种搜索代码的方法,以便对我正在更改的函数进行实时引用。但你必须记住,这只是一种省力的装置。它不保证结果代码是正确的。