C#在IEnumerable中无法设置属性

7

我在一些C#代码中发现了奇怪的行为,无法解释。可能是因为我缺少重要的理解,所以希望有人能为我点亮灯。

以下是一段类似于这样的代码块:

    IEnumberable<myObject> objects = GetObjectsFromApiCall();

    for (int i = 0; i < objects.Count(); i++)
        {
            if (String.IsNullOrEmpty(objects.ElementAt(i).SubObject.Title))
            {
                SubObject sub = GetSubObjectFromDatabase((long)objects.ElementAt(i).SubObject.Id);
                if (sub != null)
                {
                    objects.ElementAt(i).SubObject.Title = sub.Title;
                }
            }
        }

当你逐步执行它时,这段代码似乎一切正常。"objects"集合按预期被填充。"sub"作为收集到的内容被获取并具有完整的预期属性,包括一个填充的Title属性。在执行过程中没有抛出任何错误。
但是...每个Object中存在的SubObject.Title属性(只有标准的get;set;代码)仍然保持为空。
我很困惑。有人能解释一下发生了什么吗?
编辑:对于那些建议我不应该使用for循环和ElementAt的人,我开始使用foreach循环,但认为它可能是问题的根源,因为它每次都会获取一个新的SubObject。现在已经修复,感谢您的帮助,ForEach已恢复。
谢谢, Matt

4
你好,以下是我对这段内容的翻译:在IEnumerable中更新一个项目属性,但该属性似乎无法保持设置? - Ahmet Kakıcı
这段代码有可能会非常慢,甚至不好笑。 - ChaosPandion
你能复制/粘贴你实际拥有的代码,而不是看起来像实际代码的东西吗? - ken2k
它可以工作,但您可能正在比较数据的不同副本。您能否确认myObjectSubObject属性(或字段)的类型都是class类型,而不是struct(或interface)类型?如果方法调用GetObjectsFromApiCall()每次调用时生成新的数据副本,则您只会修改该副本。在这种情况下,“原始”数据将保持不变。 - Jeppe Stig Nielsen
@ChaosPandion 我猜你的意思是希望我们互相合作。我知道这可能会很慢,但最多也只会处理不超过10个对象。 - Bob Tway
@MattThrower - 我确实这样做了,尽管我的表达很糟糕。我的目标是评论,以便回答者记得添加有关代码的特殊说明。 - ChaosPandion
5个回答

6
我会这样修复它:
var objects = GetObjectsFromApiCall().ToList();

那么你可以保留当前的循环(它是有效的),或者像其他答案建议的那样使用foreach和一些Linq进行优化,但这并不重要:问题在于你试图更改IEnumerator<>中的元素,正如@Ahmet Kakıcı指出的这个问题所解释的那样。


1
-1 这是错误的。修改 IEnumerable 返回的元素没有问题。实际上,当你在 ToList() 之后使用 foreach 时,你正好使用了一个 IEnumerable,因为 List<T> 实现了 IEnumerable<T>。一个问题可能是延迟执行数据库查询,但这绝对不是由于简单存在 IEnumerable 接口而引起的... - ken2k
你说得对,我读得太快了... 问题不在于IEnumerable,而在于它的实现方式。这就是为什么使用ToList()是有意义的。感谢您的澄清。 - Larry
@ken2k 正确。请查看我的答案,其中包括我修改 IEnumerable 返回项的示例。 - Jeppe Stig Nielsen

2

试试这个

List<myObject> objects = GetObjectsFromApiCall().ToList();

foreach(var obj in objects.Where(o => string.IsNullOrEmpty(objects.SubObject.Title)).ToList())
{
    var subObject = GetSubObjectFromDatabase(obj.SubObject.Id);
    if(subObject == null) continue;

    obj.SubObject.Title = subObject.Title;
}

1

首先,你不应该在这种代码中使用 ElementAt(),应该使用

foreach (var o in objects)
{
    if (string.IsNullOrEmpty(o.SubObject.Title))
    {
        o.SubObject.Title = ...;
    }
}

此外,您应该注意,如果您的方法返回一个动态的 IEnumerable,那么每次调用 objects.Something() 时都会再次调用 API 并检索到新的副本。如果是这种情况,您应该使用 .ToList() 方法将可枚举对象复制到列表中。
还有一种方法可以不将副本放入列表中-通过创建动态枚举器,如下所示:
objects = objects.Select(o =>
{
    if (string.IsNullOrEmpty(o.SubObject.Title))
    {
        o.SubObject.Title = ...;
    }
    return o;
});

如果之前的方法都没能解决值未正确设置的问题,可以尝试在Title属性的setter中添加throw new Exception(value),看看是否使用了正确的值。请保留HTML标签。

首先,你不应该在这种代码中使用ElementAt()。为什么? - Kenneth K.
.NET会使用Enumerator.MoveNext()枚举每个元素来获取值,这种方法比list[i]的方式要慢得多。 - Knaģis

1
我猜函数 GetObjectsFromApiCall 看起来像下面这样:
public IEnumberable<myObject> GetObjectsFromApiCall(){
    for(var i = 0; i < 10; i++)
    {
         yield return new myObject();
    }
}

如果我没错的话,每次调用objects.ElementAt(i)函数以获取对象时,你都会通过"yield return new myObject()"获得一个新的对象。

那是一个好的理论。我正在考虑发布一个类似的例子。 - Jeppe Stig Nielsen
哦,你应该将“objects.ElementAt(i).SubObject.Title = sub.Title;”更改为“var obj = objects.ElementAt(i).SubObject; obj.Title = sub.Title;”。 - fengyj
关于您的评论:这会改变什么? - Jeppe Stig Nielsen
我写了一个测试,类似于 objects.ElementAt(i).SubObject.Title = sub.Title,但它无法编译。 - fengyj
SubObject是一个类型为可变struct的属性吗? - Jeppe Stig Nielsen

1
但是如何检查Title属性是否已更改?您再次调用GetObjectsFromApiCall()吗?还是再次通过相同的objects实例进行foreach循环?
一个IEnumerable实例每次“枚举”时可能会创建和生成新对象。因此,这里有一个简单的示例以进行说明。对于该示例,请定义:
class SomeObject
{
    public string Title { get; set; }
}

接下来,我们将考虑两种类型的“源”,首先是数组,然后是定义如下的迭代器块:

  static IEnumerable<SomeObject> GetSomeSequence()
  {
      yield return new SomeObject { Title = "Alpha", };
      yield return new SomeObject { Title = "Beta", };
      yield return new SomeObject { Title = "Gamma", };
  }

然后以这种方式进行测试:
  static void Main()
  {
      IEnumerable<SomeObject> thingsToModify;

      // set source to an array
      thingsToModify = new[] { new SomeObject { Title = "Alpha", }, new SomeObject { Title = "Beta", }, new SomeObject { Title = "Gamma", }, };

      foreach (var t in thingsToModify)
          Console.WriteLine(t.Title);

      foreach (var t in thingsToModify)
          t.Title = "Changed!";

      foreach (var t in thingsToModify)
          Console.WriteLine(t.Title);    // OK, modified


      // set source to something which yields new object each time a new GetEnumerator() call is made
      thingsToModify = GetSomeSequence();

      foreach (var t in thingsToModify)
          Console.WriteLine(t.Title);

      foreach (var t in thingsToModify)
          t.Title = "Changed!";          // no-one keeps these modified objects

      foreach (var t in thingsToModify)
          Console.WriteLine(t.Title);    // new objects, titles not modified

  }

结论:修改可变对象的状态是完全可能的,该对象属于我们正在迭代的源。但某些类型的IEnumerable源每次调用时都会产生新的数据副本,因此对副本进行修改是无用的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接