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

Understanding lazy loading optimization in C#

在阅读了一些关于 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
}

我发现这很慢,但这也可能是由于反射或需要处理大量属性。

所以我的问题是... 这是最佳的还是对延迟加载机制的良好使用? 或者我遗漏了一些东西并且只是浪费了大量的资源。

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

"lazy loading" 一词在 .NET 中的使用方式并不完全正确。 "Lazy loading" 最常用于以下内容:

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

而不是在构造实例时仅设置 _backingField。它的优点是,在 SomeValue 从未被访问的情况下,它不会花费任何成本,但代价是当它被访问时,成本会稍微高一些。因此,当 SomeValue 未被调用的机会相对较高时,它是有利的,并且通常是不利的,除了一些例外情况(当我们可能关心在实例创建和第一次调用 [=13= 之间完成事情的速度时) ]).

这里我们有延迟执行。它很相似,但又不完全相同。当您调用 GetPropertyErrors(property) 而不是接收所有错误的集合时,您会收到一个对象,该对象可以在被要求时找到这些错误。

它总是会节省获得第一个这样的项目所花费的时间,因为它允许您立即对其采取行动,而不是等到它完成处理。

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

它也将节省总时间,因为没有时间花在创建集合上。

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

如果您很少想获得相同的结果,通常总是赢。

如果您几乎总是想获得相同的结果,那通常是输了。

不过,如果您有时想要访问同一组结果,您可以将是否缓存的决定传递给调用者,一次性调用 GetPropertyErrors() 并执行直接得到结果,但重复使用调用 ToList() 然后在该列表上重复操作。

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

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

private IEnumerable<string> LazyLoadedEnumerator()
{
  if (_store == null)
    return StoringCalculatingEnumerator();
  return _store;
}

private IEnumerable<string> StoringCalculatingEnumerator()
{
  List<string> store = new List<string>();
  foreach(string str in SomethingThatCalculatesTheseStrings())
  {
    yield return str;
    store.Add(str);
  }
  _store = store;
}

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

通常,以延迟评估作为正常方法开始,并进一步决定调用链上游是否存储结果。但是,如果您可以在开始之前知道结果的大小,则有一个例外(您不能在这里,因为在检查 属性 之前您不知道是否会添加元素)。在这种情况下,您创建该列表的方式可能会提高性能,因为您可以提前设置其容量。不过,这是一个微观优化,仅当您也知道自己也总是想在列表上工作并且不会在宏伟的计划中节省那么多时才适用。

Lazy loading 不是特定于 C# 或 Entity Framework 的东西。这是一种常见的模式,允许延迟一些数据加载。延迟意味着 不立即加载 。需要时的一些示例:

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

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

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

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

第二种方法使用枚举器 - 当您询问下一个文件名时,它 returns 一个一个地归档。这意味着枚举数 立即返回 而没有获取所有文件并将它们保存到某个集合中。您可以在需要时一个接一个地处理文件。这里延迟获取文件列表。

但是你要小心。如果底层操作没有被延迟,那么返回枚举器不会给你带来任何好处。例如。

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

此处使用 GetFiles 方法,该方法在返回文件名之前填充文件名数组。因此,一个一个地生成文件不会给您带来任何速度优势。

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