【问题标题】:Variable 'x.Sub' of type 'SubType' referenced from scope '' but it is not defined error从范围“”引用了“SubType”类型的变量“x.Sub”,但未定义错误
【发布时间】:2019-04-17 15:09:21
【问题描述】:

检查这个小提琴是否有错误:https://dotnetfiddle.net/tlz4Qg

我有两个这样的课程:

public class ParentType{
    private ParentType(){}

    public int Id { get; protected set; }
    public SubType Sub { get; protected set; }
}

public class SubType{
    private SubType(){}

    public int Id { get; protected set; }
}

我要将多级匿名表达式转换为多级非匿名表达式。为了实现这一点,我有一个类似于下面提到的表达式:

x => new
{
   x.Id,
   Sub = new
   {
      x.Sub.Id
   }
}

为了实现这个目标,我把它变成了这样的表达式:

x => new ParentType()
{
   Id = x.Id,
   Sub = new SubType()
   {
      Id = x.Sub.Id
   },
 }

但是当我调用Compile() 方法时,我得到以下错误:

从范围 '' 引用的类型为 'SubType' 的变量 'x.Sub' 但未定义

这是我的访问者类:

public class ReturnTypeVisitor<TIn, TOut> : ExpressionVisitor
{
    private readonly Type funcToReplace;
    private ParameterExpression currentParameter;
    private ParameterExpression defaultParameter;
    private Type currentType;

    public ReturnTypeVisitor() => funcToReplace = typeof(Func<,>).MakeGenericType(typeof(TIn), typeof(object));

    protected override Expression VisitNew(NewExpression node)
    {
        if (!node.Type.IsAnonymousType())
            return base.VisitNew(node);

        if (currentType == null)
            currentType = typeof(TOut);

        var ctor = currentType.GetPrivateConstructor();
        if (ctor == null)
            return base.VisitNew(node);

        NewExpression expr = Expression.New(ctor);
        IEnumerable<MemberBinding> bindings = node.Members.Select(x =>
        {
            var mi = currentType.GetProperty(x.Name);

 //if the type is anonymous then I need to transform its body
                if (((PropertyInfo)x).PropertyType.IsAnonymousType())
                {
 //This section is became unnecessary complex!
 //
                    var property = (PropertyInfo)x;

                    var parentType = currentType;
                    var parentParameter = currentParameter;

                    currentType = currentType.GetProperty(property.Name).PropertyType;

                    currentParameter = Expression.Parameter(currentType, currentParameter.Name + "." + property.Name);

 //I pass the inner anonymous expression to VisitNew and make the non-anonymous expression from it
                    var xOriginal = VisitNew(node.Arguments.FirstOrDefault(a => a.Type == property.PropertyType) as NewExpression);

                    currentType = parentType;
                    currentParameter = parentParameter;

                    return (MemberBinding)Expression.Bind(mi, xOriginal);
                }
                else//if type is not anonymous then simple find the property and make the memberbinding
                {
                    var xOriginal = Expression.PropertyOrField(currentParameter, x.Name);
                    return (MemberBinding)Expression.Bind(mi, xOriginal);
                }
        });

        return Expression.MemberInit(expr, bindings);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        if (typeof(T) != funcToReplace)
            return base.VisitLambda(node);

        defaultParameter = node.Parameters.First();

        currentParameter = defaultParameter;
        var body = Visit(node.Body);

        return Expression.Lambda<Func<TIn, TOut>>(body, currentParameter);
    }
}

并像这样使用它:

public static Expression<Func<Tin, Tout>> Transform<Tin, Tout>(this Expression<Func<Tin, object>> source)
    {
        var visitor = new ReturnTypeVisitor<Tin, Tout>();
        var result = (Expression<Func<Tin, Tout>>)visitor.Visit(source);
        return result;// result.Compile() throw the aforementioned error
    }

这是我的访问者类中使用的扩展方法:

public static ConstructorInfo GetPrivateConstructor(this Type type) =>
            type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);

// this hack taken from https://stackoverflow.com/a/2483054/4685428
// and https://stackoverflow.com/a/1650895/4685428
public static bool IsAnonymousType(this Type type)
{
 var markedWithAttribute = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), inherit: false).Any();
 var typeName = type.Name;

 return markedWithAttribute
               && (typeName.StartsWith("<>") || type.Name.StartsWith("VB$"))
               && typeName.Contains("AnonymousType");
}

更新

这是该问题的 .Net Fiddle 链接:https://dotnetfiddle.net/tlz4Qg

更新

我已经删除了似乎超出问题范围的额外代码。

【问题讨论】:

  • 在没有上下文的情况下无法真正看出问题所在。你能创建一个我们可以运行的 minimal reproducible example,显示一个简单的输入和预期的输出吗?忘记你的访问者类 - 只是数据和方法。
  • @VahidFarahmandian 请包括GetPrivateConstructorIsAnonymousType 等扩展方法。还要检查 node.Members.Select 中使用的 lambda:它不应该编译,因为并非所有代码路径都有返回。
  • @VahidFarahmandian 我知道。我只是提请您注意以澄清错字
  • 我不太清楚你希望你当前的代码如何工作 - 但我很确定你不应该使用Expression.Parameter,因为你并不是真的想创建一个新的范围。您希望结果中的参数表达式看起来与原始参数表达式相同,对吧?所以我认为你在那里做错了事情。
  • 如果不了解您当前的代码是如何工作的,很难提供帮助 - 我基本上会从头开始。如果你能弄清楚你为什么要创建参数表达式,并将 cmets 添加到你的代码中,那将有助于解释事情。

标签: c# linq lambda expression visitor-pattern


【解决方案1】:

问题的原因是线路

currentParameter = Expression.Parameter(currentType, currentParameter.Name + "." + property.Name);

VisitNew 方法内。

使用您的示例,它会创建一个名为“x.Sub”的新参数,因此如果我们将参数标记为{},则实际结果为

Sub = new SubType()
{
    Id = {x.Sub}.Id
}, 

而不是预期

Sub = new SubType()
{
    Id = {x}.Sub.Id
},

一般来说,您不应该创建新的ParameterExpressions,除非重新映射 lambda 表达式。并且所有新创建的参数都应该传递给Expression.Lambda调用,否则将被视为“未定义”。

另外请注意,访问者代码有一些通常不成立的假设。比如

var xOriginal = Expression.PropertyOrField(currentParameter, x.Name);

在嵌套的new 中不起作用,因为您需要访问x 参数的成员,例如x.Sub.Id,而不是x.Id。这基本上是来自NewExpression.Arguments的对应表达式。

使用表达式访问者处理嵌套的 lambda 表达式或集合类型成员和 LINQ 方法需要更多的状态控制。虽然转换示例中的简单嵌套匿名new 表达式甚至不需要ExpressionVisitor,因为它可以通过简单的递归方法轻松实现,如下所示:

public static Expression<Func<Tin, Tout>> Transform<Tin, Tout>(this Expression<Func<Tin, object>> source)
{
    return Expression.Lambda<Func<Tin, Tout>>(
        Transform(source.Body, typeof(Tout)),
        source.Parameters);
}

static Expression Transform(Expression source, Type type)
{
    if (source.Type != type && source is NewExpression newExpr && newExpr.Members.Count > 0)
    {
        return Expression.MemberInit(Expression.New(type), newExpr.Members
            .Select(m => type.GetProperty(m.Name))
            .Zip(newExpr.Arguments, (m, e) => Expression.Bind(m, Transform(e, m.PropertyType))));
    }
    return source;
}

【讨论】:

  • 这真是太棒了,而且效果很好,但是我在收集类型成员方面遇到了一些挑战,我正在努力克服这个挑战。
  • 你也有关于改造这个的想法吗?:Tests = x.SubType.Tests.Select(u => new {Y = new {u.Id})
  • 如果是简单的Select,可能没什么大不了的。但如果它几乎可以是所有东西,例如Select 之前和/或之后的其他 LINQ 运算符,比它复杂得多,因为您必须更改 lambdas 返回或输入类型、LINQ 方法泛型类型参数(绑定到不同的方法定义)等 - 所有这些都使用适当的状态机.基本上所有 AutoMapper 和类似的都在使用预定义的映射。
  • 不,这只是一个简单的 SELECT,如上述评论中所述。
  • 由于评论空间有限,您能否发布另一个(跟进)新场景的问题,并且肯定需要一些代码:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-10
  • 2015-10-08
  • 1970-01-01
  • 2011-06-08
相关资源
最近更新 更多