使用此(基于扩展方法)速记的可能陷阱
C#6更新
在C#6中," ?."现在是一种语言功能:
// C#1-5 propertyValue1 = myObject != null ? myObject.StringProperty : null; // C#6 propertyValue1 = myObject?.StringProperty;
下面的问题仍然适用于旧版本,但是如果使用新的?.
运算符开发新应用程序则更好。
原始问题:
我经常想访问可能为空的对象的属性:
string propertyValue1 = null; if( myObject1 != null ) propertyValue1 = myObject1.StringProperty; int propertyValue2 = 0; if( myObject2 != null ) propertyValue2 = myObject2.IntProperty;
等等...
我经常使用它,因此我有一个摘要。
我们可以在以下情况下使用内联将其缩短到某种程度:
propertyValue1 = myObject != null ? myObject.StringProperty : null;
但是,这有点笨拙,尤其是在设置许多属性或者多个级别可以为null的情况下,例如:
propertyValue1 = myObject != null ? (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null : null;
我真正想要的是??
样式语法,该语法非常适合直接空类型:
int? i = SomeFunctionWhichMightReturnNull(); propertyValue2 = i ?? 0;
所以我想出了以下几点:
public static TResult IfNotNull<T, TResult>( this T input, Func<T, TResult> action, TResult valueIfNull ) where T : class { if ( input != null ) return action( input ); else return valueIfNull; } //lets us have a null default if the type is nullable public static TResult IfNotNull<T, TResult>( this T input, Func<T, TResult> action ) where T : class where TResult : class { return input.IfNotNull( action, null ); }
这让我们有了以下语法:
propertyValue1 = myObject1.IfNotNull( x => x.StringProperty ); propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0); //or one with multiple levels propertyValue1 = myObject.IfNotNull( o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );
这简化了这些调用,但是我不确定是否检查这种扩展方法,因为它确实使代码更易于阅读,但以扩展对象为代价。这将出现在所有内容上,尽管我可以将其放在专门引用的名称空间中。
此示例是一个相当简单的示例,稍微比较复杂的一个示例是比较两个可为空的对象属性:
if( ( obj1 == null && obj2 == null ) || ( obj1 != null && obj2 != null && obj1.Property == obj2.Property ) ) ... //becomes if( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property ) ...
以这种方式使用扩展的陷阱是什么?其他编码人员是否可能会感到困惑?这仅仅是滥用扩展名吗?
我想我这里真正想要的是编译器/语言扩展:
propertyValue1 = myObject != null ? myObject.StringProperty : null; //becomes propertyValue1 = myObject?StringProperty;
这将使复杂的情况变得容易得多:
propertyValue1 = myObject != null ? (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null //becomes propertyValue1 = myObject?ObjectProp?StringProperty;
这仅适用于值类型,但我们可以返回可为空的等效项:
int? propertyValue2 = myObject?ObjectProp?IntProperty; //or int propertyValue3 = myObject?ObjectProp?IntProperty ?? 0;
解决方案
就个人而言,即使经过所有解释,我也不记得它是如何工作的:
if( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property )
这可能是因为我没有经验。但是,我可以阅读并理解我们代码中的所有其他内容。我更喜欢保持代码语言的不可知性(特别是对于琐碎的事情),以便明天,另一个开发人员可以将其更改为一种全新的语言,而无需过多了解现有语言。
如果发现自己不得不经常检查对一个对象的引用是否为空,则可能是我们应该使用"空对象模式"。在这种模式下,我们可以使用一个具有相同接口但返回适当值的方法和属性的新类,而不是使用null来处理没有对象的情况。
it does make the code a little easier to read, but at the cost of extending object. This would appear on everything,
请注意,我们实际上并没有进行任何扩展(理论上除外)。
propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0);
将完全按照编写的方式生成IL代码:
ExtentionClass::IfNotNull(myObject2, x => x.IntProperty, 0);
没有"开销"添加到对象以支持此操作。
怎么
propertyValue1 = myObject.IfNotNull(o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );
读写比
if(myObject != null && myObject.ObjectProp != null) propertyValue1 = myObject.ObjectProp.StringProperty;
Jafar Husain发布了一个示例,该示例使用"表达式树"来检查链中C3中的运行时宏是否为空。
但是,这显然会对性能产生影响。现在,如果我们有办法在编译时执行此操作。
我只需要说我喜欢这个技巧!
我没有意识到扩展方法并不意味着要进行空检查,但这完全是有道理的。正如James指出的那样,扩展方法调用本身并不比普通方法昂贵,但是,如果我们要进行大量操作,则遵循ljorquera建议的Null Object Pattern是有意义的。或者使用null对象和??一起。
class Class1 { public static readonly Class1 Empty = new Class1(); . . x = (obj1 ?? Class1.Empty).X;
对于读者来说,这似乎是我们在对空引用调用方法。如果需要,建议将其放在实用程序类中,而不要使用扩展方法:
propertyValue1 = Util.IfNotNull(myObject1, x => x.StringProperty ); propertyValue2 = Util.IfNotNull(myObject2, x => x.IntProperty, 0);
"实用程序"。 ates,但IMO是句法上的小恶魔。
另外,如果我们将此作为团队的一部分进行开发,请轻轻地询问其他人的想法和行为。跨代码库的频繁使用模式的一致性很重要。
虽然从空实例调用扩展方法通常会引起误解,但我认为这种情况下的意图非常简单。
string x = null; int len = x.IfNotNull(y => y.Length, 0);
我想确保此静态方法适用于可以为null的值类型,例如int?
编辑:编译器说这些都不是有效的:
public void Test() { int? x = null; int a = x.IfNotNull(z => z.Value + 1, 3); int b = x.IfNotNull(z => z.Value + 1); }
除此之外,继续努力。
这是针对链接成员的另一种解决方案,包括扩展方法:
public static U PropagateNulls<T,U> ( this T obj ,Expression<Func<T,U>> expr) { if (obj==null) return default(U); //uses a stack to reverse Member1(Member2(obj)) to obj.Member1.Member2 var members = new Stack<MemberInfo>(); bool searchingForMembers = true; Expression currentExpression = expr.Body; while (searchingForMembers) switch (currentExpression.NodeType) { case ExpressionType.Parameter: searchingForMembers = false; break; case ExpressionType.MemberAccess: { var ma= (MemberExpression) currentExpression; members.Push(ma.Member); currentExpression = ma.Expression; } break; case ExpressionType.Call: { var mc = (MethodCallExpression) currentExpression; members.Push(mc.Method); //only supports 1-arg static methods and 0-arg instance methods if ( (mc.Method.IsStatic && mc.Arguments.Count == 1) || (mc.Arguments.Count == 0)) { currentExpression = mc.Method.IsStatic ? mc.Arguments[0] : mc.Object; break; } throw new NotSupportedException(mc.Method+" is not supported"); } default: throw new NotSupportedException (currentExpression.GetType()+" not supported"); } object currValue = obj; while(members.Count > 0) { var m = members.Pop(); switch(m.MemberType) { case MemberTypes.Field: currValue = ((FieldInfo) m).GetValue(currValue); break; case MemberTypes.Method: var method = (MethodBase) m; currValue = method.IsStatic ? method.Invoke(null,new[]{currValue}) : method.Invoke(currValue,null); break; case MemberTypes.Property: var method = ((PropertyInfo) m).GetGetMethod(true); currValue = method.Invoke(currValue,null); break; } if (currValue==null) return default(U); } return (U) currValue; }
然后,我们可以在null可以为null或者不为null的情况下执行以下操作:
foo.PropagateNulls(x => x.ExtensionMethod().Property.Field.Method());
我们独立地提出了完全相同的扩展方法名称和实现:空传播扩展方法。因此,我们不认为这会造成混淆或者滥用扩展方法。
我将用链接编写"多个级别"示例,如下所示:
propertyValue1 = myObject.IfNotNull(o => o.ObjectProp).IfNotNull(p => p.StringProperty);
Microsoft Connect上有一个现已关闭的错误,提示"?"。作为执行此空传播的新Coperator。来自语言团队的Mads Torgersen简要解释了为什么他们不实施它。
这是使用myObject.NullSafe(x => x.SomeProperty.NullSafe(x => x.SomeMethod))的另一种解决方案,在
http://www.epitka.blogspot.com/