【问题标题】:Not disposed items in linq querylinq 查询中未处理的项目
【发布时间】:2018-11-28 10:00:55
【问题描述】:

我有 Select 的 linq,其中创建了 IDisposable 对象。之后有一个过滤器Where,这会导致一些对象永远不会被释放。

这是一个再现:

class Program
{
    static void Main(string[] args)
    {
        var results = "1234567890"
            .Select(o => new Test(o))
            .Where(o => o.Value > '3' && o.Value < '7')
            .ToList();

        // do something with results
        // ...

        // dispose
        foreach (var result in results)
            result.Dispose();
    }
}

class Test : IDisposable
{
    public char Value { get; }
    public Test(char value)
    {
        Value = value;
        Console.WriteLine($"{Value}");
    }
    public void Dispose() => Console.WriteLine($"{Value} disposed");
}

输出:

1
2
3
4
5
6
7
8
9
0
4 disposed
5 disposed
6 disposed

问题:

如您所见,有1237890,它们是被创建但从未被释放的。

我的解决方案:

我可以在Select 中移动Where 条件,但是我需要应用丑陋的“返回null + Where 而不是null”解决方法:

var results = "1234567890".Select(o =>
{
    if (o > '3' && o < '7')
        return new Test(o);
    return null;
}).Where(o => o != null).ToList();

输出:

4
5
6
4 disposed
5 disposed
6 disposed

有没有更好(更优雅)的方式?

如果Select 在某个返回IEnumerable&lt;T&gt; 的库方法中,我的解决方法除了丑陋之外还有一个问题,我无法更改。如何申请Where不漏?

【问题讨论】:

  • 如果your comment 为真,则您的解决方案不是有效的解决方法:“不幸的是,在我的情况下,我无法交换 Select 和 Where,因为 Where 使用 Select 返回的对象的属性” 在您的解决方法中,您没有使用这些属性。
  • @Rango,是的,问题的原因。我只是想发布一些解决方案,以防止无所作为。现在我决定在Where 中调用Dispose() 作为临时解决方法(感谢Dennis),同时等待/寻找更好的解决方案。
  • 该类不应具有检查实例是否相关所必需的IDisposable 属性。相反,它应该具有非一次性财产。该属性应该从外部(构造函数/属性)初始化,或者通过调用使用一次性的方法(例如连接)来初始化。这里太抽象了,但我想你可以将它与模型分开。如果构造函数或方法使用一次性用品,他们有责任将其处置在那里。

标签: c# linq idisposable


【解决方案1】:

如您所见,创建了 1, 2, 3, 7, 8, 9, 0 从未丢弃

这是因为Where 需要为每个Select 调用创建Test 实例以根据条件测试结果。

过滤后,results 包含已创建对象的子集 => 您的代码仅处理此子集项。

有没有更好(更优雅)的方式?

唯一的LINQ方法是将初始可枚举物化为List&lt;Test&gt;(或数组)过滤之前,并处理列表项:

        var results = "1234567890"
            .Select(o => new Test(o))
            .ToList();

        var filteredResults = results
            .Where(o => o.Value > '3' && o.Value < '7')
            .ToList();

        // do something with FILTERED results
        // ...

        // dispose
        foreach (var result in results)
            result.Dispose();

但是如果有一些原因尽快Dispose 不必要的结果,并且你不能修改代码,那会产生可枚举的,只是不要使用LINQ。写一个普通的foreach

        var enumerable = "1234567890"
            .Select(o => new Test(o));

        var results = new List<Test>();

        foreach (var item in enumerable)
        {
            if (!(item.Value > '3' && item.Value < '7'))
            {
                item.Dispose();
            }
            else
            {
                results.Add(item);
            }
        }

        // do something with results
        // ...

        // dispose
        foreach (var result in results)
            result.Dispose();

【讨论】:

  • Where 使用属性,所以它必须创建实例。嗯..你是说Where 可以/应该也处理丢弃的物品吗?我还没想过。感觉有点hackish。可以在Where 内调用o.Dispose() 吗?
  • 不,不行。 不要这样做。 Where 内部的委托必须测试条件,仅此而已。任何副作用(在您的情况下处置)都将是意外行为。支持这样的代码会让人头疼。
  • “意外行为” - 正是我的想法。没有人会期望某些代码会修改 linq 方法中的对象。在Select 内部过滤是可以的,但是在跟进Where 内部调用dispose 确实是不可预料的。虽然它可以工作,但不需要传递 null 解决方法,也不需要修改包含 Select... 的代码,这对我来说是一个解决方案。那么为什么“不这样做”呢?除了“头痛”,还有其他原因吗?
  • @Sinatr:任何支持此代码的人都会诅咒你。 :) 说真的,这将是难以维护的代码。如果您不能修改返回可枚举的代码,为什么要使用不符合您需求的工具(LINQ)?编写一个foreach 循环,它将枚举初始序列,测试每个项目的条件,如果测试失败,则将其处理,如果测试通过,则将其添加到结果列表中。使用foreach 时,副作用是可以预期的。如果可以,请修改产生可枚举的代码 - 如果条件测试失败,它不应该创建项目。
【解决方案2】:

您可以先使用char 过滤您的string,然后选择所需的类型:

var results = "1234567890".Where(ch => ch > '3' && ch < '7').Select(s => new Test(s)).ToList();

foreach (var item in results)
{
    item.Dispose();
}

【讨论】:

    【解决方案3】:

    你需要记住所有项目:

    var allItems= "1234567890"
            .Select(o => new Test(o)).ToArray();
    var result = allItems.Where(o => o.Value > '3' && o.Value < '7')
            .ToList();
    

    比处置所有物品:

    foreach (var result in allItems)
            result.Dispose();
    

    【讨论】:

    • 这很有趣,甚至比 OP 的还要糟糕。它将实例化对象两次,因此仍然存在潜在的内存泄漏。如果您对其进行测试,您会看到相同的值被打印了两次。
    • 我忘了加.ToArray() :) 现在我觉得应该没问题了。
    • 由于 OP 将 Test 类设为 IDisposable 是有原因的(可能是直接或间接持有/锁定某些资源),因此创建 only 此类 IDisposable 将被进一步使用。此外,您的方法不允许保留有用的 Test 对象(无论出于何种目的),同时立即丢弃未使用/浪费的 Test 对象......
    • OP 声明他的Select 在某个库方法中,所以我假设他有IDisposable 对象列表并过滤它们而不是源字符串。
    【解决方案4】:

    从库中获取项目后,您可以将它们存储在数组中。您可以过滤数组,对相关项目做一些事情并使用数组来处理所有项目:

    var items = GetItemsFromLibrary().ToArray();
    try
    {
      var relevantItems = items.Where(o => o.Value > '3' && o.Value < '7');
      // Do something with relevant items
      // ...
    }
    finally
    {
      // Dispose all items
      foreach (var item in items)
        item.Dispose();
    }
    

    ToArray 将项目存储在一个数组中,以便您在处置它们时再次处理相同的项目,而不是再次查询库,这可能会导致返回新项目。

    如果您担心内存不足,可以使用以下(丑陋的)方法尽早丢弃不必要的项目。这可能是一个优势,但与使用ToArray 存储所有项目相比,我怀疑这不是一个很大的优势。

    var items = GetItemsFromLibrary()
      .Where(o => {
               if (o.Value > '3' && o.Value < '7')
                 return true;
               // Object not relevant in this case
               o.Dispose()
               return false;
             })
      .ToArray();
    try
    {
      // Do something with relevant items
      // ...
    }
    finally
    {
      // Dispose all items
      foreach (var item in items)
        item.Dispose();
    }
    

    即使在 Where 子句中处理不必要的项目时,该对象也已在库中实例化。因此,您最好的选择是将库更改为仅返回相关项目。

    【讨论】:

    • 有这种可能,但是如果有无数个项目,ToArray会导致OOM。我想要一个仅实例化过滤项目的解决方案。
    • @Sinatr:如果有无数的项目,你不应该将它们全部实例化。找到一种在不创建实例的情况下过滤它们的方法。为什么它们是一次性的?
    • @Rango,好吧,我必须创建每个项目的实例,以便以后能够过滤它们(使用创建对象的属性)。 Select 出现在其他地方,理论上我可以通过过滤器 Func&lt;&gt; 传递委托,但我不想在那里更改代码。它们是一次性的,因为其中一项财产是。
    • @Rango,如果我可以避免创建IDisposable 实例,那么我不会问这个问题。
    【解决方案5】:

    正如其他人已经说过的,您正在创建对象,然后丢弃对它们的引用,这意味着无法调用 Dispose()

    这些对象最终会在未来的某个时间被 GC 收集,但 GC 确实不会自动为您调用 Dispose() 而无需任何帮助。

    如果这是您真正需要的模式,您可以在一次性对象上实现终结器,以便在 GC 启动时为您调用 Dispose()

    class Program
    {
        static void Main(string[] args)
        {
            var results = "1234567890"
                          .Select(o => new Test(o))
                          .Where(o => o.Value > '3' && o.Value < '7')
                          .ToList();
    
            // do something with results
            // ...
    
            // dispose
            foreach (var result in results)
                result.Dispose();
    
            // Force GC to prove dispose called...
            GC.Collect();
    
            Console.ReadLine();
        }
    }
    class Test : IDisposable
    {
        public char Value { get; }
        public Test(char value)
        {
            Value = value;
            Console.WriteLine($"{Value}");
        }
        public void Dispose() => Console.WriteLine($"{Value} disposed");
    
        ~Test()
        {
            Dispose();
        }
    }
    

    【讨论】:

    • 你试过了吗?添加终结器不会做任何事情。强制 GC 只会再次处理过滤后的results (4, 5, 6)。
    • 使用 GC.collect 运行我的示例。它处理所有对象。或者,至少在我运行它时它确实如此。
    • 唯一的原因是为了证明这种行为,我并不是说 GC.Collect 是一个解决方案。我只是用它来证明正在调用终结器
    猜你喜欢
    • 2013-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多