在一些 C# 代码中看到一些奇怪的行为,我无法解释。可能是我缺少一些重要的理解,所以希望有人可以帮我打开灯。
得到一个如下所示的代码块:
IEnumerable<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 属性。执行过程中不会抛出任何错误。
但是......每个对象中存在的 SubObject.Title 属性(只有标准的 get; set; 代码)顽固地保持为空。
我很茫然。有谁解释一下这是怎么回事吗?
编辑:对于那些建议我不应该使用 for 循环和
ElementAt
的人,我从 foreach 循环开始,但认为这可能是问题的根源,因为它每次都会获取一个新的子对象。感谢您的帮助,现已修复,并且 ForEach 已恢复。
干杯, 马特
我会这样解决它:
var objects = GetObjectsFromApiCall().ToList();
然后您可以保持循环不变(它可以工作),或者按照其他答案的建议使用 foreach 和一些 Linq 对其进行一些优化,但这并不重要:问题是您试图更改 IEnumerator 上的元素<> 正如 @Ahmet Kakıcı 所指出的这个问题 中所解释的。
试试这个
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;
}
首先,这种代码不应该使用
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;
});
至于值未正确设置(如果之前的操作没有帮助) - 尝试在
throw new Exception(value)
属性的设置器中添加 Title
- 看看是否使用正确的值调用它。
但是如何检查
Title
属性是否已更改?你再打电话给GetObjectsFromApiCall()
吗?或者您是否再次 foreach
通过同一个 objects
实例?
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
源每次被调用时都会生成新的数据副本,然后对副本进行修改是没有用的。
我猜这个函数
GetObjectsFromApiCall
看起来像下面这样:
public IEnumerable<myObject> GetObjectsFromApiCall()
{
for(var i = 0; i < 10; i++)
{
yield return new myObject();
}
}
如果我是对的,每次调用
objects.ElementAt(i)
函数获取对象时,都会通过 yield return new myObject()
获得一个新对象。