【问题标题】:Any way to project "original plus a few changes" with a LINQ query?有什么方法可以使用 LINQ 查询来投射“原始加上一些更改”?
【发布时间】:2014-07-08 22:13:22
【问题描述】:

我有构建对象集合的代码。我正在尝试减少我拨打的.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 设置为传入的值。

有没有优雅的方法来做到这一点?

附:我认为这并不重要,但该集合最终将作为视图模型发送到 ASP.NET 视图。显然,.ToList() 可能必须在 View 获取它之前被调用,但我希望这是唯一一次。

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

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

不会有用,因为 ... 将是另外 26 个属性。

【问题讨论】:

    标签: c# linq projection


    【解决方案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
    });
    

    我认为没有比这两种更好的方法了。

    【讨论】:

    • 这在.Select 中应该是纯函数的情况下确实具有变异状态的 ick 因素。
    • @KirkWoll 好吧,唯一的其他解决方案是在select 中创建新的Xs,除了Property3Property4 之外的所有内容都设置为以前的值。我将更改我的答案以显示这一点。
    • 是的,但这个解决方案不正是约翰桑德斯试图避免的——即.* 的愿望吗?我认为约翰的问题的答案是没有优雅的方式来做他想做的事。
    【解决方案2】:

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

    如果定义了复制构造函数并且对象是可变的,您可以这样做:

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

    但是,匿名对象没有可访问的复制构造函数,也不是可变的,因此您必须自己完成复制值的工作。但是在一些辅助函数的帮助下,使用一些反射和一些好的 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
        });
    

    【讨论】:

      【解决方案3】:

      您可以像这样在您的课程中创建一个私有方法(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 方法中过滤您的东西:

      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 =&gt; PlaceValues(x, SetMe));

      并放置sorted.SetValues(SetMe);

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

      【讨论】:

      • 几件事:1.你为什么要ref MyClass c?您没有分配给PlaceValues 内部的c。也就是说,那里没有c = new MyClass(),所以你不需要ref。 2.引入并行并不是我所说的优雅。 3.根据你的评论,如果我不使用AsParallel,那么我应该使用ForEach,这正是我想要避免的。
      • OPSS 是的,我的错我之前尝试过其他选项(关于裁判),关于其余的,我很抱歉约翰,这只是另一种选择......除了我头顶的 ForEach打电话给乔恩·斯基特 :)
      • 这样会导致多次枚举。一次设置值,然后再次设置 ToList。
      【解决方案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;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2019-10-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多