【问题标题】:Why is the innermost exception in an expression tree not thrown?为什么不抛出表达式树中最里面的异常?
【发布时间】:2016-01-13 21:37:51
【问题描述】:

我一直在尝试创建一个自定义 ExpressionVisitor,它会生成一个表达式,该表达式(可选)在第一个 null 值上抛出一个 NullReferenceException。表达式的 DebugView 对我来说看起来不错,但它不能作为 exptecte (由我)工作。我以为它会先抛出

.Throw .New System.NullReferenceException("c3")

因为测试变量是null,而是抛出了这个变量

.Throw .New System.NullReferenceException("p")

我不明白为什么它会向后执行语句。不应该先执行最里面的If吗?

调试视图:

.Block() {
    .If (.Block() {
        .If (.Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0) == null) {
            .Throw .New System.NullReferenceException("c3")
        } .Else {
            .Default(System.Void)
        };
        .Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0).c3
    } == null) {
        .Throw .New System.NullReferenceException("p")
    } .Else {
        .Default(System.Void)
    };
    (.Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0).c3).p
}

我的完整测试代码:

namespace ExpressionTrees
{
    class c1
    {
        public c2 c2 { get; set; }
    }

    class c2
    {
        public c3 c3 { get; set; }
    }

    class c3
    {
        public string p { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            c3 c3 = null; 
            var test_c3 = NullGuard.Check(() => c3.p, true);
        }
    }

    public static class NullGuard
    {
        public static T Check<T>(Expression<Func<T>> expression, bool canThrowNullReferenceException = false)
        {
            var nullGuardVisitor = new NullGuardVisitor(canThrowNullReferenceException);
            var nullGuardExpression = nullGuardVisitor.Visit(expression.Body);            
            var nullGuardLambda = Expression.Lambda<Func<T>>(nullGuardExpression, expression.Parameters);
            var value = nullGuardLambda.Compile()();
            return value;
        }
    }

    public class NullGuardVisitor : ExpressionVisitor
    {
        private readonly bool _canThrowNullReferenceException;

        internal NullGuardVisitor(bool canThrowNullReferenceException)
        {
            _canThrowNullReferenceException = canThrowNullReferenceException;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            var expression = Visit(node.Expression);

            // expression == null
            var expressionEqualsNull = Expression.Equal(expression, Expression.Constant(null, expression.Type));

            if (_canThrowNullReferenceException)
            {
                var nullReferenceExceptionConstructorInfo = typeof(NullReferenceException).GetConstructor(new[] { typeof(string) });

                // if (expression == null) { throw new NullReferenceException() } else { node }
                var result = 
                    Expression.Block(
                        Expression.IfThen(
                            expressionEqualsNull,
                            Expression.Throw(Expression.New(nullReferenceExceptionConstructorInfo, Expression.Constant(node.Member.Name)))
                        ),
                        node
                    );
                return result;
            }
            else
            {
                var result = Expression.Condition(
                    expressionEqualsNull,
                    Expression.Constant(null, expression.Type),
                    node);
                return result;
            }

        }
    }
}

【问题讨论】:

  • 为什么要显式检查 null 并抛出 NRE?如果你不想传播空值,而只是想抛出,你需要做的就是保持表达式不变,当值为空时执行表达式的效果将简单导致 NRE 被抛出。您只是在复制 C# 已经为您执行的空值检查
  • 附带说明,处理值类型使解决这个问题变得更加复杂。目前,如果您的表达式导致不可为空的值,您仍在尝试在传播空路径中返回 null,这将不起作用。你需要在那里使用Nullable&lt;T&gt;
  • @Servy 我这样做是因为我想向 NRE 添加一条有意义的消息,而不仅仅是一般的异常。目前它只是成员名称,但后来它应该包含到第一个 null 的完整路径。
  • @Servy 附注到附注。我在你的回答here 中读到了它,实际上这是基于你的代码;-) 我刚刚开始学习表达式树,这些是我第一次尝试创建有用的东西......但是这个 邪恶的 异常不会让我 ;-) 我想一旦我设法使异常抛出工作,我就会得到可为空的值。
  • 对于您的第一点,这是有道理的。顺便说一句,只是想确保你知道你最终需要解决它。首先解决简单的问题当然是个好主意。

标签: c# expression-trees expressionvisitor


【解决方案1】:

它按预期工作。

这是具有不同空白和行号的相同调试视图:

1   .Block() 
2   {
3       .If (.Block() 
4       {
5           .If (.Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0) == null) 
6           {
7               .Throw .New System.NullReferenceException("c3")
8           } 
9           .Else 
10          {
11              .Default(System.Void)
12          };
13          .Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0).c3
14      } == null) 
15      {
16          .Throw .New System.NullReferenceException("p")
17      } .Else 
18      {
19          .Default(System.Void)
20      };
21      (.Constant<ExpressionTrees.Program+<>c__DisplayClass0_0>(ExpressionTrees.Program+<>c__DisplayClass0_0).c3).p
22  }

看第 13 行:它有效地说明了 if ((closure class).c3 == null) throw new NullReferenceException("p")。而您的第一次检查(第 5 行)实际上是 if ((closure class) == null) throw new NullReferenceException("c3")。问题在于误导性的异常消息比什么都重要。

【讨论】:

  • 谢谢。现在我也能看到了。我将完全重写它并尝试使用while 循环而不是自定义访问者和递归。很难决定我应该接受哪个答案。你似乎都是对的;-)
【解决方案2】:

您的问题在以下 sn-p 中:

Expression.Throw(Expression.New(nullReferenceExceptionConstructorInfo,
    Expression.Constant(node.Member.Name)))

假设此成员访问权限为a.b。您正在检查 a,然后抛出异常并显示一条消息说 b 为 null 即使您刚刚检查了 a

如果您想说a 为空,则需要在异常消息中使用node.Expression,而不是说您无法访问b,因为它的容器为空。

【讨论】:

  • 我开始相信不可能通过递归遍历所有部分来创建这样的表达式。使用node.Expression.Type.Name 似乎也并不总是有效,奇怪的是最后一个属性访问没有被检查。我尝试在所有嵌套表达式上使用while 循环来重写它,并在没有递归的情况下构建一个新的。也许那时我将能够使用变量,以便调试视图更易于阅读,并且表达式不会被计算两次(在条件中,然后在返回值时)。
猜你喜欢
  • 1970-01-01
  • 2015-12-24
  • 2015-11-17
  • 1970-01-01
  • 2010-12-09
  • 1970-01-01
  • 2014-10-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多