【问题标题】:Possible pitfalls of using this (extension method based) shorthand使用这种(基于扩展方法的)速记的可能陷阱
【发布时间】:2010-09-12 11:49:17
【问题描述】:

C#6 更新

C#6 ?. is now a language feature:

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

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

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

原问题:

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

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

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

等等……

我经常使用这个,所以我有一个sn-p。

如果:

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;

【问题讨论】:

    标签: c# .net extension-methods


    【解决方案1】:

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

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

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

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

    【讨论】:

    • 是的,我直接在 TechEd 上询问了 Mads - 基本上这个功能一直缺少剪辑,他们可能仍会在未来的 C# 版本中添加它。
    • @Keith 感谢您的更新! (不断提醒 C# 团队客户会发现这很有用,这不会有什么坏处。)
    • 他们现在正在认真考虑:blogs.msdn.com/b/jerrynixon/archive/2014/02/26/…
    • “空传播”功能的状态为“完成”。请参阅 Roslyn 的Language feature implementation status
    • ?. 在 C#6 中实现 - 我已经更新了问题以反映这一点。这里的答案仍然适用于 C#1-5
    【解决方案2】:

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

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

    【讨论】:

    • 我喜欢这个主意!但是,它(以及此处的所有其他解决方案)似乎在 LINQ 语句中不起作用。例如,当将 .Select 转换为新的匿名类型时,例如 .Select(s=> new {MyNewProperty = s.PropogateNulls(p=>p.Thing)})。这行不通。仍然必须在那里使用旧的空检查。
    • 你必须修改代码以接受带有两个参数的静态方法,对于Enumerable.Select(src, lambda)
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

      怎么样

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

      更容易读写
      if(myObject != null && myObject.ObjectProp != null)
          propertyValue1 = myObject.ObjectProp.StringProperty;
      

      Jafar Husain 发布了一个使用表达式树检查链中空值的示例,Runtime macros in C# 3

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

      【讨论】:

      • 我同意你的第一条评论。在我看来,麻烦在于从代码中并不能立即看出它在做什么。它不那么杂乱,但只有知道它的作用后才更容易理解。
      【解决方案5】:

      我只想说我喜欢这个 hack!

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

      class Class1
      {
          public static readonly Class1 Empty = new Class1();
      .
      .
      x = (obj1 ?? Class1.Empty).X;
      

      【讨论】:

      • 扩展方法本身并不昂贵,但是,这种特殊的扩展方法更昂贵,因为它需要一个 lambda/匿名方法。 Lambda 在后台编译为类分配。因此,它更昂贵,因为它需要分配。
      • @JudahHimango Lamdas 只有在捕获变量(成为闭包)时才符合类分配。如果不这样做,它们就会变成编译器生成的静态方法...您可以通过使用 dotPeek (jetbrains.com/decompiler) 之类的编译后的 DLL 来验证这一点
      【解决方案6】:

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

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

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

      将生成与编写时完全相同的 IL 代码:

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

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

      【讨论】:

        【解决方案7】:

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

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

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

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

        【讨论】:

          【解决方案8】:

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

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

          除此之外,去吧。

          【讨论】:

          • 这就是为什么有两个重载 - 一个需要默认值但对结果类型没有限制,另一个不需要默认值但将结果限制为引用类型。
          • 那是因为 int?实际上编译为 Nullable,实际上是一个结构体。它是唯一可以让您将其与 null 进行比较的编译器魔法(它正确地使 where TResult : 类约束失败)。我可能需要添加另一个特定于 Nullable 的重载
          • 再想一想 - Nullable 只有两个属性: HasValue 和 Value - 都在 ??句法。 int 不需要这个?
          【解决方案9】:

          不是确切问题的答案,但有 空条件运算符 in C# 6.0。我可以说自 C# 6.0 起在 OP 中使用该选项将是一个糟糕的选择:)

          所以你的表达方式比较简单,

          string propertyValue = myObject?.StringProperty;
          

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

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

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

          【讨论】:

            【解决方案10】:

            就个人而言,即使经过你的所有解释,我也不记得这到底是怎么回事:

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

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

            【讨论】:

              【解决方案11】:

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

              【讨论】:

              • 谢谢,但是做同样事情的代码要多得多。此外,您的 Maybe 类与框架的 Nullable 非常相似,并且通过使用 Invoke 您会增加不必要的性能损失。仍然 - 很高兴看到对同一问题的替代方案。
              猜你喜欢
              • 2018-03-25
              • 2016-12-14
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-01-10
              • 1970-01-01
              • 2018-03-04
              • 2011-06-28
              相关资源
              最近更新 更多