【问题标题】:Generic method to calculate Euclidean Distance using expressions使用表达式计算欧几里得距离的通用方法
【发布时间】:2012-07-20 05:45:12
【问题描述】:

我想要一种可以使用表达式计算欧几里得距离并订购 IQueryable 的方法:

sqrt[(q1 - p1)^2 + (q2 - p2)^2 + ... + (qn - pn)^2]

这是我想出的方法签名:

public static IOrderedQueryable<T> EuclideanDistanceOrder<T>(
    this IQueryable<T> query, IEnumerable<Expression<Func<T, double>>> expressions)
    {
        var orderedQuery = query.OrderBy(i => Math.Sqrt(expressions.Aggregate((total, item) => total + Math.Pow(item, 2))));
        return orderedQuery;
    }

我不确定如何处理 itemtotal(因为它们是 Expression&lt;Func&lt;T, double&gt;&gt;)。 我已经尝试了几种不同的方法,包括使用Expression.PowerExpression.Add。我尝试定义要单独组合的表达式:

Expression<Func<double, double>> power = i => Math.Pow(i, 2);
Expression<Func<List<Expression<Func<T, double>>>, double>> dist = (items) => Math.Sqrt(items.Sum(power));

但我仍然不知道如何处理power

有没有更好的方法来解决这个问题?

【问题讨论】:

  • 我应该补充一点,这需要能够在 linq to sql 和 Entity Framework 中执行。
  • @AndreCalil 现在我再看一遍,是的,它看起来确实有点像家庭作业,但这只是我为自己做的一个爱好项目......
  • @AndreCalil 如果是家庭作业会发生什么?即使家庭作业他尝试了很多以找到解决方案。
  • 你想知道 pow 函数在做什么?
  • Math.Pow(i,2) 可以替换为i*i;

标签: c# linq expression expression-trees


【解决方案1】:

要使其与 EF 或 LinqToSQL 一起使用,您必须将所有信息作为表达式传递,甚至是 P 和 Q 的属性访问器。这就是我修改您的方法声明的原因:

public static class Extension
{
    public static IOrderedQueryable<T> EuclideanDistanceOrder<T>(
        this IQueryable<T> query, 
        IEnumerable<Expression<Func<T, double>>> pExpressions, 
        IEnumerable<Expression<Func<T, double>>> qExpressions)
    {
        var parameter = Expression.Parameter(typeof(T));
        var pBodies = pExpressions
            .Select(x => ReplaceParameter(x.Body, parameter))
            .ToArray();

        var qBodies = qExpressions
            .Select(x => ReplaceParameter(x.Body, parameter))
            .ToArray();

        var distances = pBodies
            .Select((x, i) => CreateDistance(x, qBodies[i]))
            .ToArray();

        var squers = distances
            .Select(x => CreateSquerExpression(x))
            .ToArray();

        var sum = squers.First();
        for (int i = 1; i < squers.Count(); i++)
        {
            sum = Expression.Add(sum, squers[i]);
        }
        var funcExpression = Expression.Lambda<Func<T, double>>(sum, parameter);
        //the sqrt is irrelevant to order of this sequence
        return query.OrderBy(funcExpression);
    }

    private static Expression CreateDistance(Expression p, Expression q)
    {
        return Expression.Subtract(q, p);
    }

    private static Expression CreateSquerExpression(Expression x)
    {
        var method = typeof(Math).GetMethod("Pow", BindingFlags.Static | BindingFlags.Public);
        return Expression.Call(method, x, Expression.Constant(2.0));
    }

    private static Expression ReplaceParameter(Expression expression, ParameterExpression parameter)
    {
        var unaryExpression = expression as UnaryExpression;
        MemberExpression memberExpression;
        if (unaryExpression != null)
        {
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = expression as MemberExpression;
        }

        if (memberExpression == null)
            throw new NotImplementedException();

        if (!(memberExpression.Expression is ParameterExpression) || !(memberExpression.Member is PropertyInfo))
            throw new NotImplementedException();

        return Expression.Property(parameter, (PropertyInfo)memberExpression.Member);
    }
}

这样调用时:

    var list = new[]{ new Item
    {
        P1 = 0,
        Q1 = 0,
        P2 = 3,
        Q2 = 1,
    },
    new Item
    {
        P1 = 0,
        Q1 = 0,
        P2 = 2,
        Q2 = 1,
    }
};

var query = list.AsQueryable();

var result = query.EuclideanDistanceOrder(new Expression<Func<Item, double>>[]{
    x => x.P1,
    x => x.P2
},
new Expression<Func<Item, double>>[]{
    x => x.Q1,
    x => x.Q2
}).ToArray();

internal class Item
{
    public double P1 { get; set; }
    public double Q1 { get; set; }
    public double P2 { get; set; }
    public double Q2 { get; set; }
}

它适用于 liq to objects。我只是不确定EF或linqtoSql是否会将Math.Power方法映射到sql。如果不是,则很容易更改为乘法。

【讨论】:

  • 非常感谢。当我运行它时,我得到“System.Func`2[System.Double]”类型的表达式不能用于方法“Double Pow(Double, Double)”的“System.Double”类型的参数
  • 我在 linq to objects 上执行它,它工作正常。我不明白为什么它会扔给你。作为 Pow 函数的参数,我传递了SimpleBinaryExpression 类型的距离,另一个参数是ConstantExpression。也许是 linq 到失败的对象尝试将 CreateSquerExpression 正文更改为 return Expression.Multiply(x, x);
  • 感谢您的回复。我已经改变了一些事情,现在我得到了 NotImplementedException,因为unaryExpression.Operand 不是MemberExpression(所以memberExpression 为空)。也许我需要以其他方式做到这一点?
  • ReplaceParameter 方法背后的想法是找到所有ParameterExprsion 实例并用给定的参数替换它们。这是必需的,因为我们基于方法集合创建具有一个参数的单个方法。输入的方法越复杂,该方法就必须越复杂。只需调试以查看您拥有的表达式并遍历其LeftRight 或其他任何表达式。最简单的方法是使用递归,这对于小树来说是可以的。基本上,Iridium 向您展示了您需要做什么。
【解决方案2】:

我无法对此进行测试,但它似乎应该可以工作。最后没有平方根,但顺序应该是一样的。

public static IOrderedQueryable<T> EuclideanDistanceOrder<T>(this IQueryable<T> query, IEnumerable<Expression<Func<T, double>>> expressions)
{
    var parameter = Expression.Parameter(typeof(T), "item");
    var seed = Expression.Lambda<Func<T, double>>(Expression.Constant((double)0), parameter);
    return query.OrderBy(expressions.Aggregate(seed, GetAggregateExpression));
}

private static Expression<Func<T, double>> GetAggregateExpression<T>(Expression<Func<T, double>> sum, Expression<Func<T, double>> item)
{
    var parameter = Expression.Parameter(typeof(T), "item");
    return Expression.Lambda<Func<T, double>>(Expression.Add(Expression.Invoke(sum, parameter), Expression.Power(Expression.Invoke(item, parameter), Expression.Constant((double)2))), parameter);
}

编辑:

由于您不能使用Expression.Invoke(),您需要内联传递给EuclideanDistanceOrder 的表达式的主体。似乎没有任何“好”的方法可以做到这一点,所以我写了一个Replace 方法来做到这一点。我只为一些更常见的Expression 类型实现了Replace,希望这足以涵盖您的使用,但您可能需要为其他Expression 类型实现它。

public static IOrderedQueryable<T> EuclideanDistanceOrder<T>(this IQueryable<T> query, IEnumerable<Expression<Func<T, double>>> expressions)
{
    var parameter = Expression.Parameter(typeof(T), "item");
    var seed = Expression.Constant((double)0);
    var agg = expressions.Aggregate((Expression)seed, (s, item) => Expression.Add(s, Expression.Power(Replace(item.Body, item.Parameters[0], parameter), Expression.Constant((double)2))));
    return query.OrderBy(Expression.Lambda<Func<T, double>>(agg, parameter));
}

private static Expression Replace(Expression expression, ParameterExpression original, ParameterExpression replacement)
{
    if (expression is BinaryExpression)
    {
        var binaryExpression = (BinaryExpression)expression;
        return Expression.MakeBinary(expression.NodeType, Replace(binaryExpression.Left, original, replacement), Replace(binaryExpression.Right, original, replacement), binaryExpression.IsLiftedToNull, binaryExpression.Method, binaryExpression.Conversion);
    }
    if (expression is ConditionalExpression)
    {
        var conditionalExpression = (ConditionalExpression)expression;
        return Expression.Condition(Replace(conditionalExpression.Test, original, replacement), Replace(conditionalExpression.IfTrue, original, replacement), Replace(conditionalExpression.IfFalse, original, replacement), conditionalExpression.Type);
    }
    if (expression is ConstantExpression)
    {
        return expression;
    }
    if (expression is MemberExpression)
    {
        var memberExpression = (MemberExpression)expression;
        return Expression.MakeMemberAccess(Replace(memberExpression.Expression, original, replacement), memberExpression.Member);
    }
    if (expression is ParameterExpression)
    {
        var parameterExpression = (ParameterExpression)expression;
        return parameterExpression == original ? replacement : parameterExpression;
    }
    if (expression is UnaryExpression)
    {
        var unaryExpression = (UnaryExpression)expression;
        return Expression.MakeUnary(unaryExpression.NodeType, Replace(unaryExpression.Operand, original, replacement), unaryExpression.Type, unaryExpression.Method);
    }
    throw new Exception(string.Format("Unsupported expression type: {0}", expression.NodeType));
}

例如,如果我们的输入表达式是:

p => p.X1 - p.X2
p => p.Y1 - p.Y2

最初的实现应该是这样构建的:

i => 0 + expressions[0](i) ^ 2 + expressions[1](i) ^ 2

新实现采用原始表达式,并将输入参数(上面的p)替换为将传递给最终lambda的参数(i),并直接使用表达式的主体输出:

i => 0 + (i.X1 - i.X2) ^ 2 + (i.Y1 - i.Y2) ^ 2

【讨论】:

  • 小错误是常量应该是 double 而不是 int。然后我得到 ArgumentExection “至少一个对象必须实现 IComparable。”
  • 感谢您的建议。不幸的是,Linq to Entities 不支持 Expression.Invoke。
  • @cofiem 我已经编辑了答案以提供无调用的实现。看看这对你有什么作用。
  • @Iridium 非常感谢您的帮助。我已经尝试过了,确实有一些我需要实现的表达式。第一个抛出异常的是NodeType = Call。我在想,如果没有大量的工作,我正在尝试做的事情可能是不可能的。也许我应该考虑一种不太通用的方法来做到这一点?
  • 如果您有许多不同的表达式集要用于欧几里得排序,或者您将它用于许多不同类型的“T”,那么采用通用路线可能会很有用。不过,这是您需要回答的问题。如果您确实决定继续使用此路由,那么在需要时为 Call 添加替换处理程序应该很容易。
猜你喜欢
  • 2013-04-07
  • 2021-01-31
  • 2015-09-23
  • 1970-01-01
  • 1970-01-01
  • 2017-07-07
  • 1970-01-01
  • 2017-02-02
相关资源
最近更新 更多