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

问题描述 投票:2回答:2

在阅读了一些如何使用,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
}

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

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

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

c# linq lazy-loading deferred-execution
2个回答
5
投票

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

  • 在(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方法返回属性数组。所以你不会减少获得第一个结果的时间。


3
投票

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

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

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

这里我们推迟了执行。它是相似的,但不完全相同。当你调用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;
}

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

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

© www.soinside.com 2019 - 2024. All rights reserved.