【问题标题】:IDisposable created within a method and returnedIDisposable 在方法中创建并返回
【发布时间】:2013-01-24 00:39:46
【问题描述】:

我很高兴编写了一个运行良好且在运行时不会出现任何异常的项目。所以我决定运行静态代码分析工具(我使用的是 Visual Studio 2010)。结果发现违反了CA2000规则,消息如下:

警告 - CA2000:Microsoft.Reliability:在方法“Bar.getDefaultFoo()”中,在对对象“new Foo()”的所有引用超出范围之前调用 System.IDisposable.Dispose。

引用的代码如下:

private static IFoo getDefaultFoo()
{
    return (Baz.canIDoIt()) ? new Foo() : null;
}

我自己想:也许条件表达式会破坏逻辑(我的或验证器的)。改成这样:

private static IFoo getDefaultFoo()
{
    IFoo ret = null;
    if (Baz.canIDoIt())
    {
        retFoo = new Foo();
    }
    return ret;
}

同样的事情又发生了,但现在对象被称为retFoo。我用谷歌搜索,我已经 msdn'ed,我已经 stackoverflow'ed。找到this article。创建对象后,我不需要执行任何操作。我只需要返回对它的引用。但是,我尝试应用 OpenPort2 示例中建议的模式。现在代码如下所示:

private static IFoo getDefaultFoo()
{
    Foo tempFoo = null;
    Foo retFoo = null;
    try
    {
        if (Baz.canIDoIt())
        {
            tempFoo = new Foo();
        }
        retFoo= tempFoo;
        tempFoo = null;
    }
    finally
    {
        if (tempFoo != null)
        {
            tempFoo.Dispose();
        }
    }
    return retFoo;
}

同样的消息再次出现,但 tempFoo 变量这次违反了规则。因此,基本上,代码变得扭曲、更长、有点不合理、不必要的复杂,并且执行相同的操作,但速度较慢。

我还发现了this question,其中相同的规则似乎以类似的方式攻击有效代码。并且建议提问者忽略警告。我还阅读了this thread 和大量类似的问题。

有什么我错过的吗?规则是否被窃听/无关?我该怎么办?忽视?以某种神奇的方式处理?也许应用一些设计模式?

编辑:

在 Nicole 的要求下,我以我也尝试使用的形式提交了整个相关代码。

public class DisposableFooTest
{
    public interface IFoo
    {
        void bar();
    }

    public class Foo : IFoo, IDisposable
    {
        public void bar()
        {
            Console.Out.WriteLine("Foo baring now");
        }

        public void Dispose()
        {
            // actual Dispose implementation is irrelevant, or maybe it is?
            // anyway I followed microsoft dispose pattern
            // with Dispose(bool disposing)
        }
    }

    public static class Baz
    {
        private static bool toggle = false;
        public static bool canIDoIt()
        {
            toggle ^= true;
            return toggle;
        }
    }

    private static IFoo getDefaultFoo()
    {
        IFoo result = null;
        try
        {
            if (Baz.canIDoIt())
            {
                result = new Foo();
            }

            return result;
        }
        catch
        {
            if (result != null)
            {
                (result as IDisposable).Dispose();
                // IFoo does not inherit from IDisposable, hence the cast
            }

            throw;
        }
    }

    public static void Main()
    {
        IFoo bar = getDefaultFoo();
    }
}

分析报告包含以下内容:
`CA2000:Microsoft.Reliability:在方法“DisposableFooTest.getDefaultFoo()”中,在对对象“result”的所有引用超出范围之前调用 System.IDisposable.Dispose。 %%projectpath%%\DisposableFooTest.cs 44 测试

编辑2:

以下方法解决了 CA2000 问题:

private static IFoo getDefaultFoo()
{
    Foo result = null;
    try
    {
        if (Baz.canIDoIt())
        {
            result = new Foo();
        }
        return result;
    }
    finally
    {
        if (result != null)
        {
            result.Dispose();
        }
    }
}

很遗憾,我不能走那条路。更重要的是,我更希望遵循面向对象的原则、良好实践和指南来简化代码,使其具有可读性、可维护性和可扩展性。我怀疑有人会按预期阅读它:如果可能,给它 Foo,否则为 null。

【问题讨论】:

  • 旁注:虽然警告本身不适用(请参阅@ReedCopsey 答案),但请确保使用真正 getDefaultFoo 的任何代码实际上都会处理结果(并且真正的 IFoo 继承自 IDisposable)
  • 我很确定。如果需要,对象的所有者负责对其进行适当的处​​置。不过感谢您的提示。

标签: c# design-patterns static-analysis idisposable


【解决方案1】:

这是一个误报警告。如果IFoo 实现IDisposable,则无法返回IFoo 的适当实例,而代码分析工具会警告您未正确处理它。

代码分析不会分析您的意图或逻辑,它只是尝试警告常见错误。在这种情况下,“看起来”您正在使用IDisposable 对象而不是调用Dispose()。在这里,您按设计这样做,因为您希望您的方法返回一个新实例并将其作为工厂方法的一种形式。

【讨论】:

  • 我知道我应该将方法恢复为最初的样子。谢谢你的回答。
  • @KrzysztofJabłoński 是的 - 我个人认为第一个选项最容易理解。您只需忽略警告即可。
  • @ReedCopsey:只要在所有潜在的错误路径上处理,从工厂方法返回一次性而不触发 CA2000 实际上是非常可行的。不一定值得,但有可能。
  • @NicoleCalinoiu 是绝对正确的:只有在存在创建返回值但不会返回的路径时才会发出警告。
【解决方案2】:

这里的问题不是您的 C# 代码明确执行的操作,而是生成的 IL 使用多条指令来完成 C# 代码中的一个“步骤”。

例如,在第三个版本中(使用 try/finally),return retFoo 行实际上涉及分配由编译器生成的附加变量,而您在原始代码中看不到该变量。由于此分配发生在 try/finally 块之外,它确实会引入未处理异常的可能性,从而使您的一次性 Foo 实例“孤立”。

这是一个可以避免潜在问题(并通过 CA2000 筛选)的版本:

private static IFoo getDefaultFoo()
{
    IFoo result = null;
    try
    {
        if (Baz.canIDoIt())
        {
            result = new Foo();
        }

        return result;
    }
    catch
    {
        if (result != null)
        {
            result.Dispose();
        }

        throw;
    }
}

显然,与第一个版本相比,这在一定程度上影响了可读性和可维护性,因此您必须确定孤立的可能性以及不处置孤立的后果是否真的值得更改代码,或者是否抑制在这种特殊情况下,违反 CA2000 可能更可取。

【讨论】:

  • 感谢您的回答。我刚刚在实际代码上检查了这种方法。它不能解决 CA2000 验证问题。
  • 我刚刚用整个测试代码扩展了我的问题,没有任何更改。
  • 最新实施中的问题是处置前的强制转换,这使 CA2000 无法识别处置解决了潜在问题。如果您只返回Foonull,您可以通过将result 变量的声明类型更改为Foo 来解决此问题。否则,您应该在异常处理程序中的 null 比较之前强制转换,但您最终仍需要将 CA2000 违规作为误报来抑制。
  • 你可能是对的。将方法返回类型和result 局部变量类型更改为具体的Foo 后,违规就消失了。不幸的是,我不能走那条路。
  • 您不需要更改方法返回类型,只需更改本地result变量的类型,以避免强制转换并让CA2000识别配置。
【解决方案3】:

当返回 IDisposable 对象时,如果值为 null,则处置该对象并在方法中返回 null - 这应该清除与您的“私有静态 IFoo getDefaultFoo()”相关的“警告/建议”消息方法。当然,您仍然需要在调用例程中将对象作为 IDisposable(使用或处置)来处理。

private static IFoo getDefaultFoo()
{
    IFoo ret = null;
    if (Baz.canIDoIt())
    {
        retFoo = new Foo();
    }
    return ret;
}

改成

private static IFoo getDefaultFoo()
{
    IFoo ret = null;
    if (Baz.canIDoIt())
    {
        retFoo = new Foo();
    }
    if (ret == null)
    {
        ret.dispose();
        return null;
    }
    else
        return ret;
}

【讨论】:

  • 感谢您的努力,但在将近 7 年之后,我不再关心警告,因为该项目现已关闭。
  • 不过,我还是分析了您的回答,对此我表示感谢,我有两点意见。首先,我的印象是您错过了方法getDefaultFoo() 的要点。它的目的是提供一个有意义的IFoo 实例,特别是没有损坏或处置。其次,当retnull时调用ret.dispose()必然会导致NullReferenceException被抛出。
【解决方案4】:

静态分析基本上是在抱怨,原因如下:

  1. IFoo 接口不继承自IDisposable
  2. 但是您返回的是一个必须处理的具体实现,而调用者将无法知道这一点。

换句话说,您的GetDefaultFoo 方法的调用者需要一个IFoo 实现,但不知道您的实现需要显式处置,因此可能不会处置它。如果这是其他库中的常见做法,您将不得不手动检查项目是否实现了IDisposable,因为任何可能的接口的每个可能实现。

显然,这有问题。

恕我直言,解决此问题的最简单方法是让 IFoo 继承自 IDisposable

public interface IFoo : IDisposable
{
    void Bar();
}

这样,调用者知道他们收到的任何可能的实现都必须被处理掉。这也将允许静态分析器检查您使用 IFoo 对象的所有位置,并在您未正确处理它们时警告您。

【讨论】:

  • 感谢您的意见。没错,如果 IFoo 从 IDisposable 调用,分析器不会抱怨。问题是,如果 IFoo 的某些实现可能是 IDisposable,但不一定是全部。而且我的 IFoo 接口必须确保一个类实际上可以 Bar,无论它是否处理。我必须在不需要它的类中实现 Dispose 方法并将其留空。
  • 发现 this answer 关注该问题。响应者声称这可能是糟糕的设计,但评论者立即不同意。似乎是口味问题。此外,一次性 foo 不能在 using 子句中使用,因为它们是长期对象,并且在框架诱导调用的许多迭代中使用。
  • 是的,我同意@supercat 对that answer 的评论,如果最后一个使用它的实体可能不知道它的具体类型,接口应该继承 IDisposable (...)。我通常遵循这个规则。但正如帖子所说,这取决于具体问题。例如,您的工厂方法现在只有一个 IFoo 实现(但这可能只是一个示例)。为什么具体的Foo 类完全实现IDisposable?它是否包装了不同的IDisposable?该对象是否需要“尽快”处置?
  • 有趣的观点。谢谢您的支持。你的假设是正确的 - 我有几个 IFoo 的实现,其中大多数包装一次性对象。不过,有些没有,我可以想象更多的实现,根本不需要处理。所有这些 IFoo 都会安静地生活很长时间(几分钟),但有时会被其他人取代,然后应该处置资源。我猜你是对的,工厂方法的调用者可能不知道返回对象的 IDisposable 性质。因此,在这种特殊情况下,也许 IFoo 应该显式继承 IDisposable 。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-05
  • 1970-01-01
  • 2011-02-04
  • 1970-01-01
相关资源
最近更新 更多