有什么方法可以用 LINQ 查询投影“原始加上一些更改”吗?

Any way to project "original plus a few changes" with a LINQ query?

提问人:John Saunders 提问时间:7/9/2014 最后编辑:John Saunders 更新时间:7/24/2014 访问量:110

问:

我有正在构建对象集合的代码。我试图减少我拨打的电话数量,所以我试图尽可能长时间地保留它。.ToList()IEnumerable<T>

我几乎完成了它,除了两个属性需要设置为传递给调用方法的值:

private IEnumerable<X> BuildCollection(int setMe){
    IEnumerable<Y> fromService = CallService();
    IEnumerable<X> mapped = Map(fromService);
    IEnumerable<X> filteredBySomething = FilterBySomething(mapped);

    IEnumerable<X> sorted = filteredBySomething
                                .OrderBy(x=>x.Property1)
                                .ThenBy(x=>x.Property2);

    // Here's the problem: return "sorted", but with each "Property3"
    //  and "Property4" set to "setMe". I don't want to do:

    var sortedList = sorted.ToList();
    sortedList.ForEach(s=>
    {
        s.Property3 = setMe;
        s.Property4 = setMe;
    };

    return sortedList;
}

如果可以在 中使用某种通配符,那么我可以做这样的事情:select

return from f in filteredBySomething
    order by f.Property1, f.Property2
    select new {
        f.*,
        f.Property3 = setMe,
        f.Property4 = setMe
    };

也就是说,我想流回排序的对象,但将 Property3 和 Property4 设置为传入的值。

有没有一种优雅的方法可以做到这一点?

P.S. 我认为这无关紧要,但集合最终会作为视图模型发送到 ASP.NET 视图。显然,可能必须在视图获得它之前调用它,但我希望这是唯一的一次。.ToList()

附注我应该说该类型大约有 30 个属性!用X

select new {
    x.Property1,
    x.Property2,
    Property3 = setMe,
    Property4 = setme,
    // ...
}

将没有用,因为这将是另外 26 个属性。...

C# LINQ 投影

评论


答:

6赞 Jashaszun 7/9/2014 #1

取而代之的是:

var sortedList = sorted.ToList();
sortedList.ForEach(s=>
{
    s.Property3 = setMe;
    s.Property4 = setMe;
};

这样做:

sorted = sorted.Select(x =>
{
    x.Property3 = setMe;
    x.Property4 = setMe;
    return x;
});

但是,如果您不想修改对象,则可以改为执行以下操作:

sorted = sorted.Select(x => new X()
{
    Property3 = setMe,
    Property4 = setMe,
    // set all other properties to what they were before
    // example: Property1 = x.Property1
});

我不相信有比这两个更好的方法。

评论

2赞 Kirk Woll 7/9/2014
这确实具有突变状态的 ick-factor ,这应该是 中的纯函数。.Select
0赞 Jashaszun 7/9/2014
@KirkWoll 好吧,唯一的其他解决方案是在 except 中创建新的 s,并将其设置为以前的状态。我将更改我的答案以显示这一点。XselectProperty3Property4
0赞 Kirk Woll 7/9/2014
没错,但这个解决方案不正是约翰·桑德斯(John Saunders)试图避免的吗?我认为约翰问题的答案是,没有优雅的方式去做他想做的事。.*
0赞 terrybozzio 7/9/2014 #2

你可以在你的类中创建一个私有方法,如下所示(MyClass 当然是你的类):

private void PlaceValues(MyClass c, int SetMe)
{
    PropertyDescriptorCollection col = TypeDescriptor.GetProperties(c);
    foreach (PropertyDescriptor prop in col)
    {
        if (prop.DisplayName != "Property1" & prop.DisplayName != "Property2")
        {
            prop.SetValue(c, SetMe);
        }
    }
}

然后在 BuildCollection 方法中,filteredbysomething:

private IEnumerable<X> BuildCollection(int setMe){
    IEnumerable<Y> fromService = CallService();
    IEnumerable<X> mapped = Map(fromService);
    IEnumerable<X> filteredBySomething = FilterBySomething(mapped);

    IEnumerable<X> sorted = filteredBySomething
                                .OrderBy(x=>x.Property1)
                                .ThenBy(x=>x.Property2);

    // Here's the problem: return "sorted", but with each "Property3"
    //  and "Property4" set to "setMe". I don't want to do:

    sorted.AsParallel().ForAll(x => PlaceValues(x, SetMe));
    //Or without AsParallel(),using .ForEach() instead....

    return sorted.ToList();
}

编辑:扩展方法怎么样,如下所示:

public static void SetValues<TInn>(this IEnumerable<TInn> col, int ValueToApply)where TInn:MyClass
{
    PropertyDescriptorCollection pdCollection = TypeDescriptor.GetProperties(typeof(TInn));
    foreach (var item in col)
    {
        foreach (PropertyDescriptor des in pdCollection)
        {
            if (des.DisplayName != "Property1" & des.DisplayName != "Property2")
            {
                des.SetValue(item, ValueToApply);
            }
        }
    }
}

然后,您这样做,而不是我上面建议的:

删除sorted.AsParallel().ForAll(x => PlaceValues(x, SetMe));

和地方sorted.SetValues(SetMe);

或者在扩展方法中放置一个参数 string[],这样你就可以告诉方法哪些属性不能设置或设置取决于......

评论

0赞 John Saunders 7/9/2014
几件事:1.你为什么要?您不会将 .也就是说,那里没有,所以你不需要.2. 引入并行性不是我所说的优雅。3.根据你的评论,如果我不使用,那么我应该使用,这正是我想避免的。ref MyClass ccPlaceValuesc = new MyClass()refAsParallelForEach
0赞 terrybozzio 7/9/2014
OPSS 是的,我的坏,我之前尝试过其他选项(关于参考),关于其余的,然后对不起约翰,只是另一种选择......除了 ForEach 之外,我头顶只叫 Jon Skeet :)
0赞 John Saunders 7/9/2014
这将导致多重枚举。一次设置值,然后再次为 ToList 设置值。
1赞 Jeff Mercado 7/9/2014 #3

除非您投影到的对象类型提供复制构造函数并且是可变的,否则没有优雅的方法可以做到这一点。

如果定义了复制构造函数并且对象是可变的,则可以执行以下操作:

var updatedSorted = sorted.Select(x => new X(x)
    {
        Property3 = setMe,
        Property4 = setMe,
    });

但是,匿名对象没有可访问的复制构造函数,也不是可变的,因此您必须自己复制值。但是在一些辅助函数的帮助下,可以使用一些反射和一些好的 ol' LINQ 来减轻它的痛苦。幸运的是,对于匿名对象,尽管我们在编译时无法访问这些类型,但这并不意味着我们不能在运行时创建新实例。

public static class AnonExtensions
{
    public static TSource SetValues<TSource, TValue>(
        this TSource source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        var copier = copierExpr.Compile();
        return copier(source);
    }

    public static IEnumerable<TSource> UpdateValues<TSource, TValue>(
        this IEnumerable<TSource> source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        var copier = copierExpr.Compile();
        return source.Select(copier);
    }

    public static IQueryable<TSource> UpdateValues<TSource, TValue>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        return source.Select(copierExpr);
    }

    private class Copier<TSource, TValue> : ExpressionVisitor
    {
        private readonly ParameterExpression param =
            Expression.Parameter(typeof(TSource));
        public Expression<Func<TSource, TSource>> Rewrite(
            Expression<Func<TSource, TValue>> setter)
        {
            var newExpr = new SubstitutionVisitor(
                setter.Parameters.Single(), param).Visit(setter.Body);
            var body = this.Visit(newExpr);
            return Expression.Lambda<Func<TSource, TSource>>(body, param);
        }

        protected override Expression VisitNew(NewExpression node)
        {
            var type = typeof(TSource);
            var ctor = type.GetConstructors().Single();
            var arguments = new List<Expression>();
            var members = new List<MemberInfo>();
            var propMap = GetPropertyMap(node);
            foreach (var prop in type.GetProperties())
            {
                Expression arg;
                if (!propMap.TryGetValue(prop.Name, out arg))
                    arg = Expression.Property(param, prop);
                arguments.Add(arg);
                members.Add(prop);
            }
            return Expression.New(ctor, arguments, members);
        }

        private Dictionary<string, Expression> GetPropertyMap(
            NewExpression node)
        {
            return node.Members.Zip(node.Arguments, (m, a) => new { m, a })
                .ToDictionary(x => x.m.Name, x => x.a);
        }
    }

    private class SubstitutionVisitor : ExpressionVisitor
    {
        private Expression oldValue, newValue;
        public SubstitutionVisitor(Expression oldValue, Expression newValue)
        { this.oldValue = oldValue; this.newValue = newValue; }

        public override Expression Visit(Expression node)
        {
            return node == oldValue ? newValue : base.Visit(node);
        }
    }
}

这将允许您执行此操作:

var updatedSorted = sorted.UpdateValues(x => new
    {
        Property3 = setMe, // the types here should match the corresponding 
        Property4 = setMe, // property types
    });
0赞 John Saunders 7/24/2014 #4

这是我最终得到的:

private IEnumerable<X> BuildCollection(int setMe){
    IEnumerable<Y> fromService = CallService();
    IEnumerable<X> mapped = Map(fromService);
    IEnumerable<X> filteredBySomething = FilterBySomething(mapped);

    IEnumerable<X> sorted = filteredBySomething
                                .OrderBy(x=>x.Property1)
                                .ThenBy(x=>x.Property2);
    // The method already returns IEnumerable<X> - make it an iterator    
    foreach (var x in sorted)
    {
        x.Property3 = setMe;
        x.Property4 = setMe;
        yield return x;
    }
}