使用缓存迭代器块结果的最佳实践

3

迭代器块的延迟加载行为导致缓存数据时出现困难。考虑以下简单测试程序:

class Program
{
    static IEnumerable<int> LoadDataFromDatabase()
    {
        Console.WriteLine("Hitting database....");
        yield return 13;
    }

    static IEnumerable<int> _cachedData = null;
    static IEnumerable<int> CachedData
    {
        get
        {
            if (_cachedData == null)
            {
                _cachedData = LoadDataFromDatabase();
            }
            return _cachedData;
        }
    }

    static void Main(string[] args)
    {
        Console.WriteLine(string.Format("Collection contains {0} items.", CachedData.Count()));
        Console.WriteLine(string.Format("Collection contains {0} items.", CachedData.Count()));
    }
}

这的输出结果为:

正在访问数据库....

集合包含1个项。

正在访问数据库....

集合包含1个项。

我想只访问一次数据库(因此进行缓存),但是由于LoadDataFromDatabase()是一个迭代器块,实际的数据库调用是被缓存的——而不是数据本身。
在这种情况下,最好的做法是什么?我是否应该只执行_cachedData = LoadDataFromDatabase().ToList()来存储已计算出的数据?

2
你预计 LoadDataFromDatabase 函数需要加载多少数据?在应用程序的生命周期内,它是否需要更改?如果答案是“很少”且“不需要更改”,那么为了简单起见,你应该将数据存储为列表。 - Michael Graczyk
我可以假设,如果我正在缓存数据,那么我确实需要整个列表,因此始终可以评估整个数据集。而且我已经有了一个用于dirtying data的机制。我想知道.ToList()是否是解决延迟评估的标准方法,或者是否有更推荐的方法(例如,我可以在迭代器块方法上放置某个属性来告诉C#不要进行延迟评估)。 - tenfour
你想要缓存一些东西,以免重复查询数据库-对吗? - Konstantin Chernov
1
我不知道这种情况下的“最佳实践”是什么,但调用 ToList 应该完全没问题。至于惰性求值,编译器只是将您写入迭代器块中的逻辑构建成状态机。在这种情况下,“急切”的求值将准确地将数据存储在列表中。因此,要么在迭代器块的结果上调用 ToList,要么只需修改您的 LoadFromDataBase 方法以填充列表,而不是生成枚举。 - Michael Graczyk
我建议将其转换为列表并缓存该列表。 - NoviceProgrammer
1个回答

3
你可以添加 .ToList()
static IEnumerable<int> CachedData
{
    get
    {
        if (_cachedData == null)
        {
            _cachedData = LoadDataFromDatabase().ToList();
        }
        return _cachedData;
    }
}

缺点是,如果列表中有100,000个项目,并执行以下操作:
var list1 = CachedData.Take(2).Sum();
var list2 = CachedData.Take(3).Sum();
var list3 = CachedData.Take(1).Sum();

...它将加载列表的100,000个项目。

解决方案是实现一个LazyList,在迭代时缓存可枚举对象而非预先加载。只需用.ToLazyList()替换.ToList()

这样可以得到最佳选择:

  • 前3个项目仅加载1次总共。
  • 第4个项目从未被加载过。

这里提供了懒惰列表的实现示例


我认为你应该像回答的第一部分那样保持简单。只有在性能出现问题时,他才应考虑替代方案。 - Tomas Jansson
KISS是我非常喜欢的原则,很少会出现在两个嵌套的for each循环中使用相同的迭代器。这是第一个简单解决方案的好理由。然而,在某些情况下,它并不正确,因此我制作了一个更复杂的版本,在所有(单线程)情况下都能正确工作。您可以选择自己的实现方式。 - Alex Siepman

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