C# 预处理器

22
虽然 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在代码大全(第208页)中写道:

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

我很纠结。将这样灵活的预处理器留出C#是一个设计决策。然而,一位我非常尊敬的作者提到,在某些情况下可能是可以的。

我应该构建一个C#预处理器吗?是否有一个可用于执行我想要执行的简单操作的预处理器?


1
你找到了一个好的解决方案吗?在各个地方重复使用“IsDirty”标志和访问器很糟糕。 - 3Dave
我没有找到完美的解决方案,但我们通过NotifyPropertyWeaver在IL编织方面取得了巨大的成功。 - Brad Leach
说句实话,我写了一个C#预处理器,用于各种目的。最近我在SO上回答了另一个问题,发布了一个简单的“概念证明”C#预处理器:https://dev59.com/qHXYa4cB1Zd3GeqP6G57#18158212 - RenniePet
1
尝试使用T4模板?http://www.hanselman.com/blog/T4TextTemplateTransformationToolkitCodeGenerationBestKeptVisualStudioSecret.aspx - MarkJ
13个回答

11

考虑使用面向方面的解决方案,比如PostSharp,它可以基于自定义属性在事后注入代码。它是一个预编译器的相反,但可以给你想要的功能(例如PropertyChanged通知等)。


6

我应该建立一个C#预处理器吗?是否有一种简单的预处理器可以满足我的需求?

你可以使用C预处理器,因为C#在语法上很接近。M4也是一种选择。


4
我知道很多人认为短代码等于优雅的代码,但这并不真实。您所提出的例子可以通过编写代码完美解决,正如您所示,那么您为什么需要预处理指令呢?您不想“预处理”代码,您希望编译器在属性中为您插入一些代码。尽管这是常见的代码,但这不是预处理器的目的。
以您的例子为例,限制在哪里?显然,这符合观察者模式,并且毫无疑问它将是有用的,但是有很多事情是由于代码提供了灵活性才实现的,而预处理器则没有。如果您尝试通过预处理指令来实现常见模式,您最终会得到一个与语言本身一样强大的预处理器。如果您想以不同的方式处理代码,则使用预处理指令,但如果您只是想要代码片段,则找到另一种方法,因为预处理器并不是为此而设计的。

我希望更多的人在他们的C++代码中能够像这样思考。复杂的宏会损害代码的可维护性,并且通常不会提供任何性能优势。 - Sqeaky
4
我同意预处理指令可能会使代码难以维护,但我认为在无法进行抽象化的情况下存在大量重复代码更糟糕。 - Sellorio

3
使用类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个属性,那么需要大约1200行代码,而使用宏则只需要大约100行。哪一个更容易阅读和理解?哪一个更容易编写?
使用C#时,假设你复制并粘贴每个属性,那么每个属性需要8次复制和粘贴,总共800次。而使用宏,则完全不需要复制和粘贴。哪一个更容易出现编码错误?如果需要添加例如IsDirty标志,哪一个更容易更改?
当有大量自定义变体的情况时,宏并不是很有用。
像任何工具一样,宏也可能被滥用,并且在错误的手中甚至可能是危险的。对于一些程序员来说,这是一个信仰问题,一种方法胜过另一种方法的优点是无关紧要的;如果你是这样的人,你应该避免使用宏。对于我们这些经常、熟练、安全地使用极其锋利工具的人来说,宏不仅可以在编码时提供立即的生产力增益,而且在调试和维护期间也很有用。

3

针对为C#构建预处理器的主要争论是与Visual Studio的集成问题:需要花费大量精力(如果可能的话)来使智能感知和新的后台编译无缝运作。

另一种选择是使用Visual Studio生产力插件,例如ReSharperCodeRush。 到目前为止,后者拥有无与伦比的模板系统,并配备了出色的重构工具。

解决您所提到的确切类型问题的另一件事是AOP框架,例如PostSharp
然后,您可以使用自定义属性添加常见功能。


在 Visual Studio 中集成 C# 预处理器并不是什么大问题。至于提供智能感知,这可能取决于预处理器添加了什么语言支持。以下是一些链接:https://dev59.com/qHXYa4cB1Zd3GeqP6G57#18158212 和 https://dev59.com/fGDVa4cB1Zd3GeqPc18Q#12163384 - RenniePet

1
我认为在实现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;
    }
}

1
要获取当前执行的方法名称,可以查看堆栈跟踪:
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。

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

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


1
小心使用System.Diagnostics.StackTrace()。我知道它不是一个可靠的信息来源,特别是当你向上遍历调用堆栈时(可能在“旧的新东西”上读到过)。调用属性设置器set_Property的约定也是一个内部的.Net事物,因此可能会发生变化。 我所知道的最可靠的安全引用属性的方法是通过lambda表达式。否则,请使用建议在这里提供的面向方面的解决方案。 - Andre Luus

0
在VS2019下,使用生成器(参见https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)可以增强预编译能力,而不会失去智能感知。
例如:如果您需要删除readonly关键字(在操作构造函数时非常有用),则您的生成器可以充当预编译器,在编译时删除这些关键字并生成实际要编译的源代码。
然后,您的原始源代码将如下所示(§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命名空间。


0

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

事实上,编译器确实有一些获取此信息的支持,只是稍微绕了一个弯路,通过 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

0
至少对于提供的场景来说,有一个比构建预处理器更清晰、更类型安全的解决方案:使用泛型。代码如下:
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,从而允许您获取比属性名称更多的内容。

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


此外,这是类型安全的。
不能强调这一点。

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


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