【问题标题】:IEnumerable - Update objects inside foreach loopIEnumerable - 更新 foreach 循环内的对象
【发布时间】:2015-12-14 13:39:06
【问题描述】:

我有一个非常简单的程序,它创建一堆对象并遍历它们以设置每个对象的 Priority 属性。

static void Main(string[] args)
{
    foreach (var obj in ObjectCreator.CreateObjectsWithPriorities())
        Console.WriteLine(String.Format("Object #{0} has priority {1}",
                                         obj.Id, obj.Priority));
}

class ObjectCreator
{
    public static IEnumerable<ObjectWithPriority> CreateObjectsWithPriorities()
    {
        var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });
        ApplyPriorities(objs);
        return objs;
    }

    static void ApplyPriorities(IEnumerable<ObjectWithPriority> objs)
    {
        foreach (var obj in objs)
        {
            obj.Priority = obj.Id * 10;
            Console.WriteLine(String.Format("Set priority of object #{0} to {1}", obj.Id, obj.Priority));
        }
    }
}

class ObjectWithPriority
{
    public int Id { get; set; }
    public int Priority { get; set; }
}

我希望 Main 方法中的 IEnumerable 包含具有修改优先级的对象。但是,它们都具有默认值 0。 这是日志:

Set priority of object #1 to 10 
Set priority of object #2 to 20 
Set priority of object #3 to 30 
Object #1 has priority 0 
Object #2 has priority 0 
Object #3 has priority 0

这种行为的原因是什么?为了让我的优先事项发挥作用,我应该在这里改变什么?

【问题讨论】:

  • 关于 LINQ 必须内化的两个重要事实:(1) 查询表达式的值是 查询,以及 (2) 查询是在 查询被执行。这些陈述似乎是重言式,但您的代码表明您不相信它们;你认为查询的价值就是查询的执行,执行两次只会执行一次。
  • @EricLippert,你说得对,我一直认为 IEnumerable 是我可以枚举的元素序列(顾名思义),而不是作为查询表示。如果我只需要循环遍历序列(并且从不添加/删除元素),我可以在整个程序中安全地使用 IEnumerable 而无需调用 ToList,这似乎也是有道理的。正如许多书籍/文章所建议的那样,“您应该使用最通用的类​​型”在方法/类/程序之间传递对象,这就是我在这里尝试做的。现在我发现它比这更复杂。

标签: c# foreach ienumerable


【解决方案1】:

当你这样做时:

var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });

您只是在创建一个惰性求值的迭代器,它不会分配一个数组/列表来存储您的项目 ObjectWithPriorty。每次枚举迭代器时,它都会再次迭代值并为每次迭代投射一个ObjectWithPriority,但会丢弃它们。

您要做的是在传递查询之前具体化查询,因此稍后您将实际修改已分配的列表/数组。这可以使用Enumerable.ToListEnumerable.ToArray 来实现:

public static IEnumerable<ObjectWithPriority> CreateObjectsWithPriorities()
{
    var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i })
                                .ToList();
    ApplyPriorities(objs);
    return objs;
}

您可以额外使用Enumerable.Range 而不是分配一个固定大小的数组,这将根据要求延迟投影数字:

var objs = Enumerable.Range(1, 3).Select(i => new ObjectWithPriority { Id = i })
                                 .ToList();

【讨论】:

  • 感谢您的解释!整个 IEnumerable 概念有点令人困惑。因此,如果我有一个读取一些外部数据(即文件或数据库表)并将其转换为我的域模型对象的方法,我是否应该将其返回类型更改为 List 而不是 IEnumerable 以避免出现此类问题其他代码操作此方法返回的数据?
  • 当您查询数据库时,您应该非常小心返回给调用者的任何IEnumerable&lt;T&gt;。例如,如果您将此类枚举交给调用者,并且他使用 LINQ 或foreach 语句对其进行多次枚举,则查询将被执行多次。也许,您最好返回 T[]IList&lt;T&gt;
【解决方案2】:

为了更好地理解你的程序中发生了什么,你应该考虑这个表达式

var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });

作为查询,而不是作为对象的序列/列表/集合。

但是从您的代码中可以明显看出,在这个特定程序中您不需要查询。您需要一个包含有限数量对象的集合,并且每次使用 foreach 循环遍历它时都返回相同的对象。

所以一个不错的做法是使用ICollection&lt;ObjectWithPriority&gt; 而不是IEnumerable&lt;ObjectWithPriority&gt;。这将更好地代表程序的逻辑,并有助于避免一些错误/误解,就像你偶然发现的那样。

代码可以修改如下:

public static ICollection<ObjectWithPriority> CreateObjectsWithPriorities()
{
    IEnumerable<ObjectWithPriority> queryThatProducesObjectsWithPriorities = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i }); // just for clarification
    ICollection<ObjectWithPriority> objectsWithPriorities = queryThatProducesObjectsWithPriorities.ToList();
    ApplyPriorities(objectsWithPriorities);
    return objectsWithPriorities;
}

static void ApplyPriorities(ICollection<ObjectWithPriority> objs)
{
    foreach (var obj in objs)
    {
        obj.Priority = obj.Id * 10;
        Console.WriteLine(String.Format("Set priority of object #{0} to {1}", obj.Id, obj.Priority));
    }
}

【讨论】:

    【解决方案3】:

    除了Yuval Itzchakov的回答:

    如果您想延迟加载对象的优先级,您可以:

    仅为一个对象定义您的 ApplyPriorities() 方法并在 select-Method 中使用它,或者向您的 ObjectWithPriority 类添加一个委托,以计算优先级,如下面的代码所示:

    class ObjectWithPriority
    {
        public int Id { get; set; }
    
        private int? priority;
        public int Priority {
            get
            {
                return (priority.HasValue ? priority.Value : (priority = PriorityProvider(this)).Value);
    
            }
    
            set { priority = value; }
        }
    
        Func<ObjectWithPriority, int> PriorityProvider { get; set; }
    
        public ObjectWithPriority(Func<ObjectWithPriority, int> priorityProvider = null)
        {
            PriorityProvider = priorityProvider ?? (obj => 10 * obj.Id);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-05-20
      • 2016-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-14
      • 2016-09-26
      相关资源
      最近更新 更多