17

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

有人可以提供OO-Gurus 的引用或书籍中的参考资料,这些书籍基本上是“如果您尝试配置应该通过 IoC 容器配置的行为,预处理器的使用是不好的 OO 实践”?

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

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

有没有人读过这些方面的东西,可以给我,以便我参考?

4

14 回答 14

27

亨利·斯宾塞(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 Excellence Code的第 13 章中,给出了一个例子,即使用于其最初的目的,#ifdef 也会让你的代码变得一团糟。

希望这可以帮助。但是,如果您的同事一开始就不会听取合理的论点,我怀疑书名将有助于说服他们;)

于 2009-01-28T21:21:07.647 回答
15

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

当然,您可以使用 Strategy 设计模式和通过 IoC 控制哪个被实例化来排除这些差异,但条件编译提供至少三个主要优点:

  1. 你不会发布你不需要的代码。
  2. 您可以在代码的正确上下文中看到两个平台的平台特定代码之间的差异。
  3. 没有间接开销。适当的代码被编译,另一个不是,就是这样。
于 2009-01-23T17:20:25.693 回答
14

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

我认为这个答案属于C++ FAQ:[29.8] 你是说预处理器是邪恶的吗?.

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

每个#define宏有效地在每个源文件和每个作用域中创建一个新关键字,直到该符号为#undefd。预处理器允许您创建一个始终被替换的 #define 符号,而与 {...}该符号出现的范围无关。

有时我们需要预处理器,例如#ifndef/#define每个头文件中的包装器,但应尽可能避免。“邪恶”并不意味着“从不使用”。你有时会使用邪恶的东西,尤其是当它们是“两害相权取其轻”的时候。但他们仍然是邪恶的:-)

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

于 2009-01-23T16:05:11.587 回答
9

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

于 2009-01-23T16:04:06.160 回答
4

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 和 [Conditional("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) 更愿意提供可能不吸引大学教授的功能,但这些功能让我在工作中更有效率并最终使我能够在任何有发货日期的人的有限时间内构建一个更好的小部件。

于 2009-01-27T20:15:52.553 回答
4

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

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

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

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

  #ifdef MANUALLY_MANAGED_MEMORY
  ...

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

如果只有另一个 #ifdef-dependant 代码,那么您将有八个可能的版本要生成,更令人不安的是,其中四个可能是发布版本。

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

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

于 2009-01-27T21:16:34.880 回答
3

Bjarne Stroustrap 在这里提供了他的答案(一般来说,不是针对 IoC)

那么,使用宏有什么问题呢?

(摘抄)

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

于 2009-01-28T17:47:26.423 回答
2

C# 中对预处理的支持非常少……几乎没用。那是邪恶的吗?

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

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

我可能不想发布具有不同运行时标志的专业版的精简版。

托尼

于 2009-01-23T16:16:09.240 回答
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:几年来我没有为此使用预处理器指令。我确实将 Environment.UserInteractive 与特定的 arg 集(“-c” = 控制台)一起使用,并且我从这里的某个地方找到了一个巧妙的技巧,它不会阻止应用程序但仍等待用户输入。

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
于 2009-01-23T16:18:31.343 回答
2

使用 #if 而不是 IoC 或其他一些机制来控制基于配置的不同功能可能违反了单一职责原则,这是“现代”OO 设计的关键。这是关于 OO 设计原则的广泛系列文章。

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

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

在您原来的情况下,您需要记住 #if 的存在,并在三个组件中的任何一个发生更改时考虑到它,以应对重大更改的可能副作用。

于 2009-01-27T21:16:31.620 回答
1

预处理器代码注入对于编译器来说就像触发器对于数据库一样。而且很容易找到关于触发器的此类断言。

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

于 2009-01-27T21:49:20.307 回答
0

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

于 2009-01-28T21:26:11.303 回答
0

在我的脑海中,我没有关于预处理器指令使用的大师声明,也不能添加对著名指令的引用。但我想给您一个链接,指向 Microsoft 的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 容器注入多个实现的问题)。

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

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

不需要大师……只要成为大师。;-)

于 2011-09-01T22:42:57.997 回答
0

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

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

实际上,这将是 Log4J 与语言的一点整合。

于 2012-08-17T04:50:09.873 回答