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 在Code Complete (p208) 中写道:

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

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

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

4

13 回答 13

11

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

于 2008-08-31T23:37:27.910 回答
6

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

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

于 2008-08-31T23:20:02.087 回答
4

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

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

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

于 2008-09-01T00:20:52.703 回答
3

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

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

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

于 2008-09-01T12:59:13.270 回答
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 个属性要处理,则大约是 1,200 行代码,而大约是 100 行。哪个更容易阅读和理解?哪个更容易写?

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

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

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

于 2013-09-30T16:40:01.083 回答
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。

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

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

于 2008-09-01T13:29:22.980 回答
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;
    }
}
于 2009-08-27T01:30:49.200 回答
0

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

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

于 2008-08-31T23:47:48.540 回答
0

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

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

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

于 2008-09-01T00:34:14.123 回答
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;
    }
}

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

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


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

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

于 2013-09-25T09:30:18.420 回答
0

在 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 命名空间。

于 2021-05-25T08:08:15.397 回答
0

虽然这里有很多很好的基于反射的答案,但最明显的答案是缺失的,那就是在编译时使用编译器。请注意,自 .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
于 2021-05-25T08:24:53.673 回答
-1

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

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

于 2010-07-12T13:13:29.180 回答