在using() { }块中使用yield return语句时,在执行之前进行处理

55

我编写了自己的自定义数据层以将数据持久化到特定文件,并使用自定义DataContext模式进行了抽象。

这完全基于.NET 2.0框架(给定目标服务器的限制),因此即使其中一些看起来像LINQ-to-SQL,也不是!我只是实现了类似的数据模式。

请参考下面的示例,其中有一个我尚未解释清楚的情况。

要获取所有Animal的实例-我执行以下操作,它可以正常工作

public static IEnumerable<Animal> GetAllAnimals() {
        AnimalDataContext dataContext = new AnimalDataContext();
            return dataContext.GetAllAnimals();
}

以下是 AnimalDataContextGetAllAnimals() 方法的实现:

public IEnumerable<Animal> GetAllAnimals() {
        foreach (var animalName in AnimalXmlReader.GetNames())
        {
            yield return GetAnimal(animalName);
        }
}

AnimalDataContext 实现了 IDisposable,因为我在其中使用了一个 XmlTextReader,我想确保它被迅速清理掉。

如果我像下面这样在第一次调用外层加上 using 语句:

public static IEnumerable<Animal> GetAllAnimals() {
        using(AnimalDataContext dataContext = new AnimalDataContext()) {
            return dataContext.GetAllAnimals();
        }
}

AnimalDataContext.GetAllAnimals() 方法的第一行设置一个断点,并在 AnimalDataContext.Dispose() 方法的第一行设置另一个断点,然后执行...

Dispose() 方法首先被调用,所以当在 Dispose() 中将 AnimalXmlReader 设置为 null 后,在调用 AnimalXmlReader.GetNames() 时会抛出“对象引用未设置为对象实例”的异常。

有什么想法吗?我有一种预感,这可能与不允许在 try-catch 块内调用 yield return 有关,而 using 在编译后就像是代表了它...


这基本上也是我遇到的问题之一,请参见我的问题:https://dev59.com/PknSa4cB1Zd3GeqPP6qQ - Lasse V. Karlsen
2个回答

64

当你调用GetAllAnimals时,它实际上不会执行任何代码,直到你在foreach循环中枚举返回的IEnumerable。

在包装方法返回之后立即释放了dataContext,而在枚举IEnumerable之前。

最简单的解决方案是将包装方法也做成迭代器,像这样:

public static IEnumerable<Animal> GetAllAnimals() {
    using (AnimalDataContext dataContext = new AnimalDataContext()) {
        foreach (var animalName in dataContext.GetAllAnimals()) {
            yield return GetAnimal(animalName);
        }
    }
}

这样,使用语句将在外部迭代器中编译,并且只有在外部迭代器被处理时才会被处理。

另一种解决方案是在包装器中枚举IEnumerable。最简单的方法是返回一个List<Animal>,如下所示:

public static IEnumerable<Animal> GetAllAnimals() {
    using (AnimalDataContext dataContext = new AnimalDataContext()) {
        return new List<Animal>(dataContext.GetAllAnimals());
    }
}

请注意,这会失去延迟执行的好处,因此它将获取所有动物,即使您不需要它们。


1
谢谢SLaks - 第二个选项解决了问题,而且很整洁 - 代码行数更少,错误也更少!该调用进入一个“AnimalDataContextAdapter”,它位于WebForms项目的表示层中 - 具体用于枚举集合,因此延迟执行没有真正的损失。 - Neil Fenwick
我会将此抽象为:永远不要在使用 yield return 或其他延迟执行方法的方法中接受 IDisposable 参数。 - Jader Dias
1
使用 yield return 时,只有在完全枚举集合时才是安全的,并会调用流上的 Dispose。详情请参见:http://blogs.msdn.com/b/dancre/archive/2008/03/14/yield-and-usings-your-dispose-may-not-be-called.aspx - yzorg
这基本意味着我只能在内存集合中使用yield return进行迭代,因为远程查询将始终使用某些类来获取数据,这些类通常实现IDisposable接口。 - RBT

12
这是因为GetAllAnimals方法并不返回一个动物集合,而是返回一个能够逐个返回动物的枚举器。
当你在using块内返回GetAllAnimals调用的结果时,你只是返回了这个枚举器。使用块在该方法退出之前释放数据上下文,在那时枚举器还没有读取任何动物。当你尝试使用枚举器时,它无法从数据上下文中获取任何动物。
一种解决方法是使GetAllAnimals方法也创建一个枚举器。这样,直到您停止使用该枚举器,using块才会关闭:
public static IEnumerable<Animal> GetAllAnimals() {
   using(AnimalDataContext dataContext = new AnimalDataContext()) {
      foreach (Animal animal in dataContext.GetAllAnimals()) {
         yield return animal;
      }
   }
}

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