题
虽然 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);
}
}
}
编写一个预处理器来处理像这样的极其简单的情况是一个好主意吗?史蒂夫·麦康奈尔写道 代码完成 (第208页):
编写自己的预处理器 如果一种语言不包含预处理器,那么编写一个预处理器相当容易......
我很伤心。将如此灵活的预处理器从 C# 中剔除是一个设计决定。然而,我非常尊敬的一位作者提到,在某些情况下这可能没问题。
我应该构建一个 C# 预处理器吗?有没有一个可以完成我想做的简单事情?
解决方案
考虑看看面向方面的解决方案,例如 后锐利, ,它根据自定义属性事后注入代码。它与预编译器相反,但可以为您提供您正在寻找的功能(PropertyChanged 通知等)。
其他提示
我应该构建一个 C# 预处理器吗?有没有一个可以完成我想做的简单事情?
您始终可以使用 C 预处理器——C# 在语法方面足够接近。M4也是一个选择。
我知道很多人认为短代码等于优雅的代码,但事实并非如此。
您提出的示例在代码中完美解决,正如您所展示的那样,您需要预处理器指令来做什么?您不想“预处理”您的代码,您希望编译器在您的属性中为您插入一些代码。这是通用代码,但这不是预处理器的目的。
以你的例子来说,你把限制放在哪里?显然,这满足了观察者模式,并且毫无疑问这将是有用的,但是有很多有用的事情实际上已经完成了,因为代码提供了 灵活性 而预处理器则没有。如果您尝试通过预处理器指令实现常见模式,您将得到一个需要与语言本身一样强大的预处理器。如果你想 使用预处理器指令以不同的方式处理代码 但如果您只想要一个代码片段,那么请寻找另一种方法,因为预处理器并不打算这样做。
使用 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 行代码与 1,200 行代码。〜100。哪个更容易阅读和理解?哪个更容易写?
对于 C#,假设您通过剪切和粘贴来创建每个属性,则每个属性需要粘贴 8 次,总共 800 次。有了宏,根本就不用粘贴了。哪个更有可能包含编码错误?如果您必须添加例如,则更容易更改IsDirty 标志?
当大量情况下可能存在自定义变化时,宏就没那么有用了。
与任何工具一样,宏可能会被滥用,甚至在错误的人手中可能会产生危险。对于一些程序员来说,这是一个宗教问题,一种方法相对于另一种方法的优点并不重要。如果是你,你应该避免使用宏。对于我们这些经常、熟练、安全地使用极其锋利的工具的人来说,宏不仅可以在编码时立即提高生产力,还可以在下游的调试和维护过程中提高生产力。
要获取当前执行的方法的名称,您可以查看堆栈跟踪:
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。
使用相同的技术,您还可以查询源文件和行/列信息。
但是,我没有对此进行基准测试,为每个属性集创建一次堆栈跟踪对象可能是一项非常耗时的操作。
我认为在实现 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;
}
}
如果我正在设计 C# 的下一个版本,我会考虑每个函数都有一个自动包含的局部变量,用于保存类的名称和函数的名称。在大多数情况下,编译器的优化器会将其删除。
但我不确定对这类事情的需求有多大。
@豪尔赫写道:如果您想以不同的方式处理代码,请使用预处理器指令,但如果您只想要代码片段,请找到另一种方法,因为预处理器并不打算这样做。
有趣的。我真的不认为预处理器一定以这种方式工作。在提供的示例中,我正在进行简单的文本替换,这与预处理器的定义一致 维基百科.
如果这不是预处理器的正确使用,那么我们应该将其称为简单文本替换(通常需要在编译之前进行)?
至少对于所提供的场景,有比构建预处理器更干净、类型安全的解决方案:
使用泛型。就像这样:
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
, ,您几乎可以在每个对象上使用此方法。
此外,这是类型安全的。
无论如何强调这一点都不过分。
(我知道这是一个老问题,但我发现它缺乏实用的解决方案。)