【问题标题】:C# PreprocessorC# 预处理器
【发布时间】:2010-09-07 10:12:53
【问题描述】:

虽然 C# 规范确实包含预处理器和基本指令(#define、#if 等),但该语言没有 C/C++ 等语言中的灵活预处理器。我相信缺少这种灵活的预处理器是 Anders Hejlsberg 做出的设计决定(尽管很遗憾,我现在找不到参考)。从经验来看,这当然是一个不错的决定,因为当我做很多 C/C++ 时,创建了一些非常糟糕的不可维护的宏。

也就是说,在许多情况下,我可以找到一个更灵活的预处理器。一些简单的预处理器指令可以改进如下代码:

public string MyProperty
{
  get { return _myProperty; }
  set
  {
    if (value != _myProperty)
    {
      _myProperty = value;
      NotifyPropertyChanged("MyProperty");
      // This line above could be improved by replacing the literal string with
      // a pre-processor directive like "#Property", which could be translated
      // to the string value "MyProperty" This new notify call would be as follows:
      // NotifyPropertyChanged(#Property);
    }
  }
}

编写一个预处理器来处理这种极其简单的情况是个好主意吗? Steve McConnell 在Code Complete (p208) 中写道:

编写你自己的预处理器如果一种语言不包含预处理器,那么编写一个相当容易......

我被撕裂了。将如此灵活的预处理器排除在 C# 之外是一个设计决定。但是,我非常尊重的一位作者提到,在某些情况下可能还可以。

我应该构建一个 C# 预处理器吗?有没有一个可以做我想做的简单事情的?

【问题讨论】:

  • 您找到了一个好的解决方案吗?在所有地方重复“IsDirty”标志和访问器很糟糕。
  • 我没有找到完美的解决方案,但我们通过NotifyPropertyWeaver 使用 IL Weaving 取得了巨大成功。
  • 为了它的价值,我编写了一个 C# 预处理器,用于各种目的。我最近在 SO 上通过发布一个简单的“概念验证”C# 预处理器回答了另一个问题:stackoverflow.com/a/18158212/253938
  • 试试 T4 模板? hanselman.com/blog/…

标签: c# c-preprocessor


【解决方案1】:

考虑看看像PostSharp 这样的面向方面的解决方案,它基于自定义属性在事后注入代码。它与预编译器相反,但可以为您提供您正在寻找的功能(PropertyChanged 通知等)。

【讨论】:

    【解决方案2】:

    我应该构建一个 C# 预处理器吗?有没有可以做我想做的简单事情的?

    您始终可以使用 C 预处理器——在语法方面,C# 已经足够接近了。 M4也是一种选择。

    【讨论】:

      【解决方案3】:

      我知道很多人认为短代码等于优雅的代码,但事实并非如此。

      您提出的示例已在代码中完美解决,正如您所展示的,您需要预处理器指令做什么?您不想“预处理”您的代码,您希望编译器在您的属性中为您插入一些代码。这是通用代码,但这不是预处理器的目的。

      以你的例子,你把限制放在哪里?显然,这满足了观察者模式,毫无疑问,这将是有用的,但实际上有很多有用的事情已经完成,因为代码提供了灵活性,而预处理器却没有。如果您尝试通过预处理器指令实现通用模式,您将得到一个需要与语言本身一样强大的预处理器。如果您想以不同的方式处理您的代码,请使用预处理器指令,但如果您只是想要一个代码 sn-p,那么请找到另一种方式,因为预处理器并不是要这样做的。 p>

      【讨论】:

      • 我希望更多的人会在他们的 C++ 代码中这样想。复杂的宏会损害代码的可维护性,并且通常不会提供任何性能优势。
      • 我同意预处理器指令会使代码更难维护,但我认为在无法抽象的情况下拥有大量重复代码会更糟。
      【解决方案4】:

      使用 C++ 风格的预处理器,OP 的代码可以减少到这一行:

       OBSERVABLE_PROPERTY(string, MyProperty)
      

      OBSERVABLE_PROPERTY 看起来或多或少像这样:

      #define OBSERVABLE_PROPERTY(propType, propName) \
      private propType _##propName; \
      public propType propName \
      { \
        get { return _##propName; } \
        set \
        { \
          if (value != _##propName) \
          { \
            _##propName = value; \
            NotifyPropertyChanged(#propName); \
          } \
        } \
      }
      

      如果您有 100 个属性要处理,则大约需要 1,200 行代码,而大约 100 行。哪个更容易阅读和理解?哪个更容易写?

      使用 C#,假设您通过剪切和粘贴来创建每个属性,即每个属性 8 次粘贴,总共 800 次。使用宏,根本没有粘贴。哪个更可能包含编码错误?如果您必须添加例如,哪个更容易更改IsDirty 标志?

      当大量案例中可能存在自定义变化时,宏就没有那么有用了。

      与任何工具一样,宏可能会被滥用,落入坏人手中甚至可能很危险。对于一些程序员来说,这是一个宗教问题,一种方法相对于另一种方法的优点是无关紧要的;如果那是你,你应该避免使用宏。对于我们这些经常、熟练和安全地使用极其锋利的工具的人来说,宏不仅可以在编码时立即提高生产力,而且在调试和维护期间也可以在下游提供。

      【讨论】:

        【解决方案5】:

        反对为 C# 构建前处理器的主要论点是在 Visual Studio 中的集成:需要付出很多努力(如果可能的话)才能让智能感知和新的后台编译无缝工作。

        替代方案是使用 Visual Studio 生产力插件,例如 ReSharperCodeRush。 据我所知,后者拥有无与伦比的模板系统和出色的refactoring 工具。

        另一个可能有助于解决您所指的确切类型问题的 AOP 框架,例如 PostSharp
        然后,您可以使用自定义属性来添加常用功能。

        【讨论】:

        【解决方案6】:

        要获取当前执行的方法的名称,可以查看堆栈跟踪:

        public static string GetNameOfCurrentMethod()
        {
            // Skip 1 frame (this method call)
            var trace = new System.Diagnostics.StackTrace( 1 );
            var frame = trace.GetFrame( 0 );
            return frame.GetMethod().Name;
        }
        

        在属性设置方法中,名称为 set_Property。

        使用相同的技术,您还可以查询源文件和行/列信息。

        但是,我没有对此进行基准测试,为每个属性集创建一次堆栈跟踪对象可能是一个太耗时的操作。

        【讨论】:

        • 小心使用 System.Diagnostics.StackTrace()。我知道我已经读过它不是可靠的信息来源,尤其是当您沿调用堆栈向上移动时(可能在“The Old New Thing”上阅读)。调用属性设置器 set_Property 的约定也是内部 .Net 事物,因此可能会发生变化。安全地引用属性 afaik 的最可靠方法是通过 lambda 表达式。否则,请使用此处建议的面向方面的解决方案。
        【解决方案7】:

        我认为您在实施 INotifyPropertyChanged 时可能遗漏了问题的一个重要部分。您的消费者需要一种确定属性名称的方法。出于这个原因,您应该将属性名称定义为常量或静态只读字符串,这样消费者就不必“猜测”属性名称。如果使用预处理器,消费者如何知道属性的字符串名称是什么?

        public static string MyPropertyPropertyName
        public string MyProperty {
            get { return _myProperty; }
            set {
                if (!String.Equals(value, _myProperty)) {
                    _myProperty = value;
                    NotifyPropertyChanged(MyPropertyPropertyName);
                }
            }
        }
        
        // in the consumer.
        private void MyPropertyChangedHandler(object sender,
                                              PropertyChangedEventArgs args) {
            switch (e.PropertyName) {
                case MyClass.MyPropertyPropertyName:
                    // Handle property change.
                    break;
            }
        }
        

        【讨论】:

          【解决方案8】:

          如果我正在设计 C# 的下一个版本,我会考虑每个函数都有一个自动包含的局部变量,其中包含类的名称和函数的名称。在大多数情况下,编译器的优化器会将其取出。

          不过,我不确定对这种东西的需求是否很大。

          【讨论】:

            【解决方案9】:

            @Jorge 写道:如果您想以不同的方式处理您的代码,请使用预处理器指令,但如果您只想要一个代码 sn-p,那么请找到另一种方式,因为预处理器并不意味着这样做。

            有趣。我真的不认为预处理器必须以这种方式工作。在提供的示例中,我正在做一个简单的文本替换,这与 Wikipedia 上的预处理器定义一致。

            如果这不是预处理器的正确使用,我们应该称之为简单的文本替换,这通常需要在编译之前发生?

            【讨论】:

            • 你应该使用字符串常量,有什么理由不这样做吗?另外,您的#property 的类型是什么?如果我将#property 定义为double 怎么办?为什么我要用前驱指令替换常量?
            • 鉴于我在问题中的示例,必须维护一个表示属性名称的常量,遗憾的是不会节省任何维护。当然,如果我需要在类的任何其他部分重用字符串,我肯定会使用常量。
            • (cont) 可能有趣的是同时具有 #Property 语法(默认为当前属性)和 #Property.MyProperty 语法 - 一种工具可重构构造,如果没有,则会产生编译时错误映射到实际属性并避免反射的运行时开销。
            【解决方案10】:

            至少对于提供的场景,有一个比构建预处理器更清洁、类型安全的解决方案:

            使用泛型。像这样:

            public static class ObjectExtensions 
            {
                public static string PropertyName<TModel, TProperty>( this TModel @this, Expression<Func<TModel, TProperty>> expr )
                {
                    Type source = typeof(TModel);
                    MemberExpression member = expr.Body as MemberExpression;
            
                    if (member == null)
                        throw new ArgumentException(String.Format(
                            "Expression '{0}' refers to a method, not a property",
                            expr.ToString( )));
            
                    PropertyInfo property = member.Member as PropertyInfo;
            
                    if (property == null)
                        throw new ArgumentException(String.Format(
                            "Expression '{0}' refers to a field, not a property",
                            expr.ToString( )));
            
                    if (source != property.ReflectedType ||
                        !source.IsSubclassOf(property.ReflectedType) ||
                        !property.ReflectedType.IsAssignableFrom(source))
                        throw new ArgumentException(String.Format(
                            "Expression '{0}' refers to a property that is not a member of type '{1}'.",
                            expr.ToString( ),
                            source));
            
                    return property.Name;
                }
            }
            

            这可以很容易地扩展为返回 PropertyInfo,从而让您获得比属性名称更多的东西。

            由于它是Extension method,因此您几乎可以在每个对象上使用此方法。


            另外,这是类型安全的。
            怎么强调都不过分。

            (我知道这是一个老问题,但我发现它缺乏实用的解决方案。)

            【讨论】:

              【解决方案11】:

              虽然这里有很多很好的基于反射的答案,但缺少最明显的答案,那就是在编译时使用编译器。 请注意,自 .NET 4.5 和 C# 5 起,C# 和 .NET 就支持以下方法。

              编译器实际上对获取此信息有一些支持,只是以一种稍微迂回的方式,即通过CallerMemberNameAttribute 属性。这允许您让编译器注入正在调用方法的成员的名称。还有两个兄弟属性,但我认为一个例子更容易理解:

              鉴于这个简单的类:

              public static class Code
              {
                  [MethodImplAttribute(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
                  public static string MemberName([CallerMemberName] string name = null) => name;
                  
                  [MethodImplAttribute(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
                  public static string FilePath([CallerFilePathAttribute] string filePath = null) => filePath;
                  
                  [MethodImplAttribute(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
                  public static int LineNumber([CallerLineNumberAttribute] int lineNumber = 0) => lineNumber;
              }
              

              在这个问题的上下文中你实际上只需要第一种方法,你可以这样使用它:

              public class Test : INotifyPropertyChanged
              {
                  private string _myProperty;
                  public string MyProperty
                  {
                      get => _myProperty;
                      set
                      {
                          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Code.MemberName()));
                          _myProperty = value;
                      }
                  }
                  
                  public event PropertyChangedEventHandler PropertyChanged;
              }
              

              现在,由于此方法只是将参数返回给调用者,因此它很可能会被完全内联,这意味着运行时的实际代码只会抓取包含属性名称的字符串。

              示例用法:

              void Main()
              {
                  var t = new Test();
                  t.PropertyChanged += (s, e) => Console.WriteLine(e.PropertyName);
                  
                  t.MyProperty = "Test";
              }
              

              输出:

              MyProperty
              

              反编译后的属性代码实际上是这样的:

              IL_0000 ldarg.0 
              IL_0001 ldfld   Test.PropertyChanged
              IL_0006 dup 
              IL_0007 brtrue.s    IL_000C
              IL_0009 pop 
              IL_000A br.s    IL_0021
              IL_000C ldarg.0 
              
              // important bit here
              IL_000D ldstr   "MyProperty"
              IL_0012 call    Code.MemberName (String)
              // important bit here
              
              IL_0017 newobj  PropertyChangedEventArgs..ctor
              IL_001C callvirt    PropertyChangedEventHandler.Invoke (Object, PropertyChangedEventArgs)
              IL_0021 ldarg.0 
              IL_0022 ldarg.1 
              IL_0023 stfld   Test._myProperty
              IL_0028 ret
              

              【讨论】:

                【解决方案12】:

                在 VS2019 下,使用生成器时,您确实获得了增强的预编译能力,而不会丢失智能感知(参见 https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)。

                例如:如果您需要删除只读关键字(在操作构造函数时很有用),那么您的生成器可以充当预编译器,在编译时删除这些关键字并生成要编译的实际源代码。

                您的原始源代码将如下所示(§RegexReplace 宏将由生成器执行并随后在生成的源代码中被注释掉):

                #if Precompiled || DEBUG
                 #if Precompiled
                    §RegexReplace("((private|internal|public|protected)( static)?) readonly","$1")
                 #endif
                 #if !Precompiled && DEBUG
                 namespace NotPrecompiled
                 {
                 #endif
                
                 ... // your code
                
                 #if !Precompiled && DEBUG
                 }
                 #endif
                #endif // Precompiled || DEBUG
                

                然后生成的源将具有:

                #define Precompiled
                

                在顶部,生成器将对源执行其他所需的更改。

                因此,在开发过程中,您仍然可以拥有智能感知,但发布版本将只有生成的代码。应注意不要在任何地方引用 NotPrecompiled 命名空间。

                【讨论】:

                  【解决方案13】:

                  如果您准备放弃 C#,您可能想查看 Boo 语言,该语言通过 AST(抽象语法树)操作具有非常灵活的 macro 支持。如果您可以放弃 C# 语言,那真是太棒了。

                  有关 Boo 的更多信息,请参阅以下相关问题:

                  【讨论】:

                  • 对那些没有留下 cmets 的反对者表示敬意。真的吗?那是糟糕的社交技巧。 (不过我能期待什么,这是一群超级极客。)
                  猜你喜欢
                  • 1970-01-01
                  • 2011-07-02
                  • 2014-02-26
                  • 2020-02-09
                  • 2019-07-08
                  • 1970-01-01
                  • 2018-02-10
                  • 1970-01-01
                  相关资源
                  最近更新 更多