需要引用:预处理器使用是不好的OO练习

时间:2009-01-23 15:42:44

标签: c# oop c-preprocessor dos-donts

我相信,像#if UsingNetwork这样的预处理程序指令的使用是不好的OO实践 - 其他同事则不然。 我认为,当使用IoC容器(例如Spring)时,如果相应编程,可以轻松配置组件。在这种情况下,IoC容器可以设置属性IsUsingNetwork,或者如果“使用网络”实现的行为不同,则应该实现并注入该接口的另一个实现(例如:IService,{ {1}},ServiceImplementation)。

有人可以在书中提供引用OO-Gurus 引用,其中基本上读取“如果您尝试配置应该配置的行为,则预处理器使用是错误的OO实践通过IoC容器“?

我需要这种引用来说服同事重构......

编辑:我确实知道并同意在编译期间使用预处理程序指令来更改特定于目标平台的代码是正确的,这就是预处理程序指令的用途。但是,我认为应该使用运行时配置而不是compiletime-configuration来获得良好的设计和可测试的类和组件。换句话说:使用#defines和#if超出它们的意图将导致难以测试代码和设计糟糕的类。

有没有人读过这些内容并且可以给我这样的话我可以参考?

14 个答案:

答案 0 :(得分:23)

Henry Spencer写了一篇名为#ifdef Considered Harmful的论文。

另外,Bjarne Stroustrup本人在他的书The Design and Evolution of C++的第18章中对预处理器的使用表示不满,并希望完全消除它。但是,Stroustrup也认识到#ifdef指令和条件编译的必要性,并继续说明在C ++中没有它的替代品。

最后,Pete Goodliffe在他的书Code Craft: The Practice of Writing Excellent Code的第13章中给出了一个例子,即使用于其原始目的,#ifdef也会使你的代码混乱。

希望这会有所帮助。但是,如果你的同事一开始不听合理的论点,我怀疑书的引用会有助于说服他们;)

答案 1 :(得分:14)

恕我直言,你谈的是C和C ++,而不是一般的OO实践。而C不是面向对象的。在这两种语言中,预处理器实际上都很有用。只需正确使用它。

我认为这个答案属于C++ FAQ: [29.8] Are you saying that the preprocessor is evil?

  

是的,这正是我所说的:   预处理器是邪恶的。

     

每个#define宏都有效   在每个来源中创建一个新关键字   文件和每个范围,直到该符号   是#undef d。预处理器让   你创建一个#define符号   总是被取代独立的   {...}范围,该符号   出现。

     

有时我们需要预处理器,   例如#ifndef/#define包装器   在每个头文件中,但它应该   你可以避免。 “邪恶”   并不意味着“从不使用”。你会用的   特别是邪恶的事情   当他们是“两个中的较小者   邪恶。“但他们仍然是邪恶的: - )

我希望这个来源足够权威: - )

答案 2 :(得分:11)

C#中的预处理程序指令具有非常明确定义和实际的用例。你专门讨论的那些,称为条件指令,用于控制代码的哪些部分被编译,哪些部分不被编译。

不编译代码部分和控制对象图通过IoC连接的方式之间存在非常重要的区别。让我们看一个现实世界的例子:XNA。当您开发计划在Windows和XBox 360上部署的XNA游戏时,您的解决方案通常至少有两个可在IDE中切换的平台。它们之间会有一些差异,但其中一个区别在于XBox 360平台将定义一个条件符号XBOX360,您可以在源代码中使用以下习语:

#if (XBOX360)
// some XBOX360-specific code here
#else
// some Windows-specific code here
#endif

当然,你可以使用策略设计模式来控制这些差异,并通过IoC进行实例化控制,但条件编译至少提供三个主要优势:

  1. 您不提供不需要的代码。
  2. 您可以在该代码的正确上下文中看到两个平台的特定于平台的代码之间的差异。
  3. 没有间接开销。编译适当的代码,另一个不是,就是这样。

答案 3 :(得分:9)

  

“预处理器是邪恶的化身,是地球上所有痛苦的原因” -Robert(OO Guru)

答案 4 :(得分:4)

预处理器#ifdef的一个问题是它们有效地复制了编译版本的数量,从理论上讲,你应该测试它们,以便你可以说你交付的代码是正确的。

  #ifdef DEBUG
  //...
  #else
  //...

好的,现在我可以制作“Debug”版本和“Release”版本。这对我来说没问题,我总是这样做,因为我有断言和调试跟踪,只在调试版本中执行。

如果有人来写(现实生活中的例子)

  #ifdef MANUALLY_MANAGED_MEMORY
  ...

他们写了一个宠物优化,它们传播到四五个不同的类,然后突然有四种可能的方法来编译你的代码。

如果你只有另一个#ifdef依赖的代码,那么你将会生成八个可能的版本,而且更令人不安的是,它们中的四个将是可能的版本。

当然运行时if(),就像循环等等,创建你必须测试的分支 - 但我发现要保证配置的每个编译时间变化仍然更加困难正确的。

这就是为什么我认为,作为一项政策,所有#ifdef除了Debug / Release版本之外都应该是临时,即你正在开发代码中进行实验而你呢?很快就会决定是否保持这种或那种方式。

答案 5 :(得分:3)

Bjarne Stroustrap在这里提供了他的答案(一般来说,并非特定于IoC)

So, what's wrong with using macros?

(节选)

  

宏不遵守C ++范围和   类型规则。这通常是导致的原因   微妙而不那么微妙的问题。   因此,C ++提供   替代方案更适合   其余的C ++,如内联函数,   模板和命名空间。

答案 6 :(得分:2)

C#中预处理的支持非常小......接近无用。那是邪恶吗?

预处理器与OO有什么关系吗?当然它是用于构建配置。

例如,我有一个精简版和我的应用程序的专业版。我可能想要在lite上排除一些代码,而不得不求助于构建非常相似的代码版本。

我可能不想发送一个精简版本,它是具有不同运行时标志的专业版本。

答案 7 :(得分:2)

在c#/ VB.NET中我不会说它的邪恶。

例如,在调试Windows服务时,我将以下内容放在Main中,以便在调试模式下,我可以将该服务作为应用程序运行。

    <MTAThread()> _
    <System.Diagnostics.DebuggerNonUserCode()> _
    Shared Sub Main()


#If DEBUG Then

        'Starts this up as an application.'

        Dim _service As New DispatchService()
        _service.ServiceStartupMethod(Nothing)
        System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite)

#Else

        'Runs as a service. '

        Dim ServicesToRun() As System.ServiceProcess.ServiceBase
        ServicesToRun = New System.ServiceProcess.ServiceBase() {New DispatchService}
        System.ServiceProcess.ServiceBase.Run(ServicesToRun)

#End If

    End Sub

这是配置应用程序的行为,当然不是邪恶的。至少,它并不像试图调试服务启动例程那样邪恶。

如果我读错了OP,请纠正我,但是当一个简单的布尔就足够了,你似乎在抱怨其他人使用预处理器。如果是这样的话,不要诅咒预处理器,该死的是那些以这种方式使用它们。

修改 回复:第一条评论。我不知道这个例子如何联系在这里。问题是预处理器被误用,而不是它是邪恶的。

我会再给你一个例子。我们有一个应用程序在启动时在客户端和服务器之间进行版本检查。在开发中,我们经常有不同的版本,不想进行版本检查。这是邪恶的吗?

我想我想说的是预处理器并不是邪恶的,即使在改变程序行为时也是如此。问题是有人滥用它。说那有什么问题?你为什么试图解雇语言功能?

很多以后编辑:FWIW:我在几年内没有使用预处理器指令。我确实使用带有特定arg set(“ - c”= Console)的Environment.UserInteractive,以及我在这里从某处获取的一个巧妙的技巧,以阻止应用程序但仍等待用户输入。

Shared Sub Main(args as string[])
  If (Environment.UserInteractive = True) Then
    'Starts this up as an application.
    If (args.Length > 0 AndAlso args[0].Equals("-c")) Then 'usually a "DeriveCommand" function that returns an enum or something similar
      Dim isRunning As Boolean = true
      Dim _service As New DispatchService()
      _service.ServiceStartupMethod(Nothing)
      Console.WriteLine("Press ESC to stop running")
      While (IsRunning)
        While (Not (Console.KeyAvailable AndAlso (Console.ReadKey(true).Key.Equals(ConsoleKey.Escape) OrElse Console.ReadKey(true).Key.Equals(ConsoleKey.R))))
           Thread.Sleep(1)
         End While                        
         Console.WriteLine()
         Console.WriteLine("Press ESC to continue running or any other key to continue shutdown.")
         Dim key = Console.ReadKey();
         if (key.Key.Equals(ConsoleKey.Escape))
         {
           Console.WriteLine("Press ESC to load shutdown menu.")
           Continue
         }
         isRunning = false
      End While
      _service.ServiceStopMethod()
    Else
      Throw New ArgumentException("Dont be a double clicker, this needs a console for reporting info and errors")
    End If
  Else

    'Runs as a service. '
    Dim ServicesToRun() As System.ServiceProcess.ServiceBase
    ServicesToRun = New System.ServiceProcess.ServiceBase() {New DispatchService}
    System.ServiceProcess.ServiceBase.Run(ServicesToRun)
  End If
End Sub

答案 8 :(得分:2)

IMO区分#if和#define非常重要。两者都有用,两者都可以过度使用。我的经验是#define比#if更容易被过度使用。

我花了10多年时间做C和C ++编程。在我工作的项目中(商业上可用于DOS / Unix / Macintosh / Windows的软件),我们主要使用#if和#define来处理代码可移植性问题。

我花了足够的时间与C ++ / MFC一起学习在过度使用时厌恶#define - 我相信在大约1996年的MFC就是这种情况。

然后我花了7年多的时间从事Java项目。我不能说我错过了预处理器(尽管我当然确实错过了Java当时没有的枚举类型和模板/泛型等内容。)

我从2003年开始使用C#。我们已经大量使用#if和[条件(“DEBUG”)]来进行调试构建 - 但是#if只是一种更方便,更有效的方式。做与Java相同的事情。

展望未来,我们已经开始为Silverlight准备核心引擎。虽然我们所做的一切都可以在没有#if的情况下完成,但使用#if可以减少工作量,这意味着我们可以花更多的时间添加客户要求的功能。例如,我们有一个值类,它封装了一个系统颜色,用于存储在我们的核心引擎中。下面是前几行代码。由于System.Drawing.Color和System.Windows.Media.Color之间的相似性,顶部的#define在普通的.NET和Silverlight中为我们提供了许多功能而无需重复代码:

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
#if SILVERLIGHT
using SystemColor = System.Windows.Media.Color;
#else
using SystemColor = System.Drawing.Color;
#endif

namespace SpreadsheetGear.Drawing
{
    /// <summary>
    /// Represents a Color in the SpreadsheetGear API and provides implicit conversion operators to and from System.Drawing.Color and / or System.Windows.Media.Color.
    /// </summary>
    public struct Color
    {
        public override string ToString()
        {
            //return string.Format("Color({0}, {1}, {2})", R, G, B);
            return _color.ToString();
        }

        public override bool Equals(object obj)
        {
            return (obj is Color && (this == (Color)obj))
                || (obj is SystemColor && (_color == (SystemColor)obj));
        }
        ...

对我而言,最重要的是有许多语言功能可以过度使用,但这并不足以让这些功能退出或制定严格的规则禁止使用它们。我必须说,在用Java编程这么长时间后转到C#有助于我理解这一点,因为微软(Anders Hejlsberg)更愿意提供可能对大学教授没有吸引力的功能,但这使我在工作中更有成效并最终使我能够在有限的时间内建立一个更好的小部件,任何有发货日期的人都可以。

答案 9 :(得分:2)

使用#if而不是IoC或其他一些机制来控制基于配置的不同功能可能违反了单一责任原则,这是“现代”OO设计的关键。 Here is关于面向对象设计原则的大量文章。

由于#if定义的不同部分中的部分涉及系统的不同方面,因此您现在将至少两个不同组件的实现细节耦合到使用#if的代码的依赖关系链中。

通过重构这些问题,您创建了一个类,假设它已完成并经过测试,将不再需要打开,除非公共代码被破坏。

在您的原始案例中,您需要记住#if的存在,并在三个组件中的任何一个组件发生变化时考虑到突破性变化的可能副作用。

答案 10 :(得分:1)

预处理器代码注入是编译器对数据库的触发器。并且很容易找到关于触发器的这种断言。

我主要想到#define用于内联短表达式,因为它节省了函数调用的开销。换句话说,这是不成熟的优化。

答案 11 :(得分:0)

告诉你的同事的一个快点是:如果在这样的语句中使用符号,预处理器会破坏数学语句中的运算符优先级。

答案 12 :(得分:0)

我没有关于预处理器指令的使用的大师声明,并且无法添加对着名指令的引用。但是我想给你一个链接到微软MSDN发现的简单样本。

#define A
#undef B
class C
{
#if A
   void F() {}
#else
   void G() {}
#endif
#if B
   void H() {}
#else
   void I() {}
#endif
}

这将导致简单的

class C
{
   void F() {}
   void I() {}
}

并且我认为它不是很容易阅读,因为您必须查看顶部以查看此时确切定义的内容。如果你已经在其他地方定义了它,那就越来越复杂了。

对我来说,创建不同的实现看起来更简单 将它们注入调用者而不是切换定义以创建“新”类定义。 (...因此我理解为什么要将预处理器定义的使用与IoC的使用进行比较)。除了使用预处理器指令的代码可怕的可读性之外,我很少使用预处理器定义,因为它们会增加测试代码的复杂性,因为它们会导致多个路径(但这也是外部IoC-Container注入多个实现的问题)。

Microsoft本身在win32 api中使用了很多预处理器定义,您可能知道/记住char和w_char方法调用之间的丑陋切换。

也许你不应该说“不要使用它”。告诉他们“如何使用它”和“何时使用它”。我想如果你想出好的(更好理解的)替代品并且可以描述使用预处理器定义/ makros的风险,每个人都会同意你的看法。

不需要大师...只是做个大师。 ; - )

答案 13 :(得分:0)

我想问一个新问题,但看起来很适合这里。我同意拥有一个完整的预处理器可能对Java来说太过分了。 C世界中的预处理器有一个明确的需求,在Java世界中根本没有涉及: 我希望编译器完全忽略调试打印输出,具体取决于调试级别。现在我们依赖于“良好实践”,但在实践中,这种做法很难实施,仍然存在一些冗余CPU负载。

在Java风格中,可以通过使用一些指定的方法来解决,例如debug(),warning()等,为其调用代码生成条件。

实际上,这会将Log4J集成到语言中。