15

C#6 更新

C#6?.中,现在是一种语言特性

// C#1-5
propertyValue1 = myObject != null ? myObject.StringProperty : null; 

// C#6
propertyValue1 = myObject?.StringProperty;

下面的问题仍然适用于旧版本,但如果使用 new 运算符开发新应用程序?.是更好的做法。

原始问题:

我经常想访问可能为空的对象的属性:

string propertyValue1 = null;
if( myObject1 != null )
    propertyValue1 = myObject1.StringProperty;

int propertyValue2 = 0;
if( myObject2 != null )
    propertyValue2 = myObject2.IntProperty;

等等...

我经常使用它,所以我有一个片段。

在以下情况下,您可以使用内联在某种程度上缩短它:

propertyValue1 = myObject != null ? myObject.StringProperty : 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;
4

11 回答 11

16

我们独立提出了完全相同的扩展方法名称和实现:空传播扩展方法。所以我们不认为这是混淆或滥用扩展方法。

我会用链接编写你的“多层次”示例,如下所示:

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp).IfNotNull(p => p.StringProperty);

Microsoft Connect 上有一个现已关闭的错误,它提出了“?”。作为将执行此 null 传播的新 C# 运算符。Mads Torgersen(来自 C# 语言团队)简要解释了为什么他们不会实现它。

于 2008-10-10T14:32:08.443 回答
15

这是链式成员的另一种解决方案,包括扩展方法:

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;    
}

然后,您可以在 any 可以为 null 或 none 的情况下执行此操作:

foo.PropagateNulls(x => x.ExtensionMethod().Property.Field.Method());
于 2008-09-28T02:00:46.100 回答
11

如果您发现自己必须经常检查对对象的引用是否为空,那么您可能应该使用空对象模式。在这种模式中,不是使用 null 来处理没有对象的情况,而是实现一个具有相同接口但具有返回足够默认值的方法和属性的新类。

于 2008-09-23T19:28:19.270 回答
5

怎么

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );

比阅读和写作更容易

if(myObject != null && myObject.ObjectProp != null)
    propertyValue1 = myObject.ObjectProp.StringProperty;

Jafar Husain 发布了一个使用表达式树检查链中是否存在空值的示例,即C# 3 中的运行时宏

不过,这显然会对性能产生影响。现在,如果我们有办法在编译时做到这一点。

于 2008-09-23T20:14:41.297 回答
5

我只想说我喜欢这个黑客!

我没有意识到扩展方法并不意味着空检查,但它完全有道理。正如 James 所指出的,扩展方法调用本身并不比普通方法贵,但是如果你做了很多这样的事情,那么遵循 ljorquera 建议的空对象模式确实是有意义的。或者使用空对象和 ?? 一起。

class Class1
{
    public static readonly Class1 Empty = new Class1();
.
.
x = (obj1 ?? Class1.Empty).X;
于 2008-09-23T20:15:40.763 回答
1

它确实使代码更容易阅读,但以扩展对象为代价。这会出现在所有东西上,

请注意,您实际上并没有扩展任何东西(理论上除外)。

propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0);

将像编写一样生成 IL 代码:

ExtentionClass::IfNotNull(myObject2,  x => x.IntProperty, 0);

没有为对象添加“开销”来支持这一点。

于 2008-09-23T20:04:34.487 回答
1

对于不知情的读者来说,看起来您正在调用空引用的方法。如果你想要这个,我建议把它放在一个实用程序类中,而不是使用扩展方法:


propertyValue1 = Util.IfNotNull(myObject1, x => x.StringProperty );
propertyValue2 = Util.IfNotNull(myObject2, x => x.IntProperty, 0);

“实用程序”。篦,但IMO是较小的句法邪恶。

此外,如果您将其作为团队的一部分进行开发,请温和地询问其他人的想法和做法。代码库中常用模式的一致性很重要。

于 2008-09-23T20:24:45.140 回答
1

虽然从空实例调用扩展方法通常会引起误解,但我认为在这种情况下意图非常简单。

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);
    }

除此之外,去吧。

于 2008-09-23T23:39:36.170 回答
1

不是对所提出的确切问题的答案,但在 C# 6.0 中有Null-Conditional Operator 。我可以争辩说,自 C# 6.0 起在 OP 中使用该选项将是一个糟糕的选择 :)

所以你的表达更简单,

string propertyValue = myObject?.StringProperty;

如果myObject为 null,则返回 null。如果属性是值类型,则必须使用等效的可为空类型,例如,

int? propertyValue = myObject?.IntProperty;

或者,您可以使用 null 合并运算符合并以在 null 的情况下提供默认值。例如,

int propertyValue = myObject?.IntProperty ?? 0;

?.不是唯一可用的语法。对于索引属性,您可以使用?[..]. 例如,

string propertyValue = myObject?[index]; //returns null in case myObject is null

运算符的一个令人惊讶的行为是,如果 object 恰好为 null ?.,它可以智能地绕过后续调用。.Member链接中给出了一个这样的例子:

var result = value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);

在这种情况下result,如果value为 null 并且value.Length表达式不会导致NullReferenceException.

于 2015-01-07T15:17:18.560 回答
0

就个人而言,即使经过您的所有解释,我也不记得这到底是如何工作的:

if( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property ) 

这可能是因为我没有 C# 经验;但是,我可以阅读并理解您代码中的其他所有内容。我更喜欢保持代码语言不可知(尤其是琐碎的事情),以便明天,另一个开发人员可以将其更改为全新的语言,而无需太多关于现有语言的信息。

于 2008-09-23T19:13:18.870 回答
0

这是另一个使用 myObject.NullSafe(x=>x.SomeProperty.NullSafe(x=>x.SomeMethod)) 的解决方案,在 http://www.epitka.blogspot.com/进行了解释

于 2009-05-28T15:21:38.650 回答