了解C#中的延迟加载优化

时间:2016-02-02 12:05:28

标签: c# linq lazy-loading deferred-execution

在阅读了一些有关yield,foreach,linq延迟执行和迭代器如何在C#中工作之后。我决定尝试在一个小项目中优化基于属性的验证机制。结果:

private IEnumerable<string> GetPropertyErrors(PropertyInfo property)
{
    // where Entity is the current object instance
    string propertyValue = property.GetValue(Entity)?.ToString();

    foreach (var attribute in property.GetCustomAttributes().OfType<ValidationAttribute>())
    {
        if (!attribute.IsValid(propertyValue))
        {
            yield return $"Error: {property.Name} {attribute.ErrorMessage}";
        }
    }
}

// inside another method
foreach(string error in GetPropertyErrors(property))
{
    // Some display/insert log operation
}

我觉得这很慢但也可能是由于反射或要处理的大量属性。

所以我的问题是...... 这是最佳还是很好地利用了懒惰的加载机制?或者我错过了什么,只是浪费了大量的资源。

注意:代码意图本身并不重要,我担心的是在其中使用延迟加载。

2 个答案:

答案 0 :(得分:5)

Lazy loading不是C#或Entity Framework特有的。这是一种常见的模式,允许延迟一些数据加载。延期意味着不立即加载。有些样品需要时:

  • 在(Word)文档中加载图像。文档可能很大,可能包含数千张图像。如果您在打开文档时加载所有文件,则可能需要花费大量时间。没有人想坐下来观看加载文件30秒。在Web浏览器中使用相同的方法 - 资源不与页面主体一起发送。浏览器推迟了资源加载。
  • 加载对象图。它可能是来自数据库,文件系统对象等的对象。加载完整图可能等于将所有数据库内容加载到内存中。这要花多长时间?它有效吗?不。如果您正在构建一些文件系统资源管理器,您将在开始使用之前加载有关系统中每个文件的信息吗?如果你只加载关于当前目录的信息(可能是直接的孩子),它会快得多。

延迟加载并不总是意味着在您确实需要数据之前推迟加载。在您真正需要该数据之前,可能会在后台线程中进行加载。例如。您可能永远不会滚动到网页的底部以查看页脚图像。延迟加载意味着只能推迟。 C#枚举器可以帮助您。考虑获取目录中的文件列表:

string[] files = Directory.GetFiles("D:");
IEnumerable<string> filesEnumerator = Directory.EnumerateFiles("D:");

第一种方法返回文件数组。这意味着目录应该获取其所有文件,并在之前将其名称保存到数组,您甚至可以得到第一个文件名。这就像在看到文档之前加载所有图像一样。

第二种方法使用枚举器 - 当您要求下一个文件名时,它会逐个返回文件。这意味着枚举器立即返回 而不获取所有文件并将其保存到某个集合中。并且您可以在需要时逐个处理文件。这里获取文件列表是推迟的。

但你应该小心。如果不延迟基础操作,那么返回枚举器将不会给您带来任何好处。例如。

public IEnumerable<string> EnumerateFiles(string path)
{
    foreach(string file in Directory.GetFiles(path))
        yield return file;
}

这里使用GetFiles方法填充文件名数组,然后再返回它们。所以逐个产生文件会给你带来速度上的好处。

顺便说一句,在你的情况下你有完全相同的问题 - GetCustomAttributes扩展在内部使用Attribute.GetCustomAttributes方法返回属性数组。所以你不会减少获得第一个结果的时间。

答案 1 :(得分:3)

这并不完全是如何在.NET中使用术语“延迟加载”。 “延迟加载”最常用于:

public SomeType SomeValue
{
  get
  {
    if (_backingField == null)
      _backingField = RelativelyLengthyCalculationOrRetrieval();
    return _backingField;
  }
}

而不是在构造实例时设置_backingField。它的优点是,在永远不会访问SomeValue的情况下,它不需要任何费用,但代价是成本稍高一些。因此,当SomeValue未被调用的机会相对较高时通常是不利的,否则通常会有一些例外(当我们可能关心在实例创建和第一次调用{{1}之间完成事情的速度有多快时) })。

这里我们推迟了执行。它是相似的,但不完全相同。当您调用SomeValue而不是收到所有错误的集合时,您会收到一个可以在被要求时找到这些错误的对象。

它将始终节省获得第一个此类项目所需的时间,因为它允许您立即对其进行操作,而不是等到它完成处理。

它总是会减少内存使用量,因为它不会在集合上花费内存。

它还可以节省总时间,因为没有时间用于创建集合。

但是,如果您需要多次访问它,那么虽然集合仍然会有相同的结果,但它必须再次计算它们(不像延迟加载加载其结果和存储它们以供后续重复使用)。

如果您很少想要达到同样的结果,那么通常总是一场胜利。

如果你几乎总是想要达到同样的结果,那通常就是失败。

如果您有时想要获得相同的结果集,则可以通过调用GetPropertyErrors(property)并根据结果执行一次调用来决定是否缓存到调用者。直接,但重复使用调用GetPropertyErrors(),然后在该列表上重复操作。

因此,不发送列表的方法更灵活,允许调用代码决定哪种方法对其特定用途更有效。

你也可以将它与延迟加载结合起来:

ToList()

这种组合在实践中很少有用。

作为一项规则,从延迟评估开始作为正常方法,并进一步决定是否存储结果的调用链。例外情况是,如果您在开始之前就能知道结果的大小(您不能在这里,因为在检查属性之前您不知道是否会添加元素)。在这种情况下,您可以在创建该列表时提高性能,因为您可以提前设置其容量。这虽然是一个微优化,但只有当你也知道你也总是希望在列表上工作并且不会在宏观方案中保存那么多时,它才适用。