在方法中创建并返回IDisposable对象

15

我很高兴编写了一个相当不错的项目,在运行时没有表现出任何奇怪的问题。因此,我决定运行静态代码分析工具(我使用的是Visual Studio 2010)。结果发现违反了规则CA2000,如下所示:

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

引用的代码如下所示:

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,我也去了stackoverflow。找到了这篇文章。创建对象后没有需要执行的操作。我只需要返回对它的引用。但是,我已经尝试应用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变量违反了规则。所以基本上,代码混乱了,变得更长、更不理性、不必要地复杂,并且做同样的事情,但速度更慢。

我还发现了这个问题,在类似的方式下攻击一个有效的代码。并且回答者建议忽略警告。我也阅读了这个线程和一大堆类似的问题。

我有什么遗漏的吗?规则是否存在缺陷/不相关?我该怎么办?忽略?用某种神奇的方式处理?也许应用某些设计模式?

编辑:

根据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()' 中,在所有引用超出作用域之前调用 System.IDisposable.Dispose 对象'result'。%%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。


1
附注:虽然警告本身不适用(请参见@ReedCopsey的答案),但请确保使用真实的getDefaultFoo代码实际上会处理结果(并且真实的IFoo继承自IDisposable)。 - Alexei Levenkov
我非常确定。如果需要,对象的所有者负责适当处理它。不过还是谢谢你的提示。 - Krzysztof Jabłoński
4个回答

10

这是一个虚警。如果IFoo实现了IDisposable接口,没有办法返回一个适当的IFoo实例而不会让代码分析工具警告你未正确处理它的回收。

代码分析工具不会分析您的意图或逻辑,它只会尝试警告常见错误。在这种情况下,它“看起来”像您正在使用一个IDisposable对象但未调用Dispose()方法。在这里,您是有意这么做的,因为您希望该方法返回一个新的实例并充当一种工厂方法。


我明白我应该将方法还原为最初的样子。感谢您的回答。 - Krzysztof Jabłoński
@KrzysztofJabłoński 是的 - 我个人认为第一种选项最容易理解。你只需要忽略警告即可。 - Reed Copsey
2
@ReedCopsey:只要在所有可能的错误路径上都进行处理,从工厂方法返回一个可处理的对象而不触发CA2000是完全可行的。这并不一定是值得的,但确实是可能的。 - Nicole Calinoiu
1
@NicoleCalinoiu 绝对正确:只有在存在一条路径,其中返回值被创建但不会被返回时,才会发出警告。 - Crono

8
这里的问题并不是你的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验证问题。 - Krzysztof Jabłoński
我刚刚扩展了我的问题,没有做任何更改,并附上了整个测试代码。 - Krzysztof Jabłoński
2
你最新实现中的问题是在处理之前进行了转换,这导致CA2000无法识别处置地址的潜在问题。如果你只返回 Foonull,你可以通过将result变量的声明类型更改为 Foo 来解决这个问题。否则,在异常处理程序中进行 null 比较之前需要先进行强制转换,但你仍然需要抑制 CA2000 违规作为误报。 - Nicole Calinoiu
你可能是对的。在将方法返回类型和“result”本地变量类型更改为具体的“Foo”后,违规现象消失了。不幸的是,我不能这样做。 - Krzysztof Jabłoński
1
你不需要改变方法的返回类型,只需更改本地变量 result 的类型,以避免强制转换并允许 CA2000 识别处置。 - Nicole Calinoiu
非常好的观察,@NicoleCalinoiu!这应该被标记为答案。+1 - Crono

2
静态分析基本上是为以下原因而抱怨:
  1. IFoo接口没有继承IDisposable,
  2. 但您返回的具体实现必须被处理,调用者不知道这一点。
换句话说,您的GetDefaultFoo方法的调用者希望得到IFoo的实现,但不知道您的实现需要显式处理,并且因此可能不会处理它。如果在其他库中这是常见做法,您将不得不手动检查每个可能接口的任何可能实现是否实现了IDisposable
显然,这种方式有问题。
在我看来,最清晰的解决方法是使IFoo继承IDisposable:
public interface IFoo : IDisposable
{
    void Bar();
}

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


谢谢您的意见。没错,如果IFoo继承自IDisposable,分析器就不会报错了。问题是,有些IFoo的实现可能是IDisposable,但并非全部都是。而我的IFoo接口必须确保一个类能够执行Bar操作,无论它是否需要进行Dispose。我必须在那些不需要Dispose的类中实现Dispose方法并将其留空。 - Krzysztof Jabłoński
在这个特定问题上找到了这个答案。回答者声称这可能是不好的设计,但评论者立即表示不同意。似乎是一个品味问题。此外,一次性的foos不能在using子句中使用,因为它们是长期对象,并且在许多框架引发的调用迭代中使用。 - Krzysztof Jabłoński
是的,我同意@supercat在该答案上的评论,“如果最后一个使用它的实体可能不知道其具体类型,则接口应继承IDisposable(...)”。我通常遵循这个规则。但正如帖子所说,这取决于具体问题。例如,您的工厂方法现在只有一个IFoo实现(但这可能只是一个示例)。而且为什么具体的Foo类要实现IDisposable呢?它是否包装了不同的IDisposable?这个对象需要“尽快”被处理吗? - vgru
有趣的观点。谢谢你的支持。你的假设是正确的 - 我有几个IFoo的实现,其中大多数包装了可处理的对象。但有些不是这样的,我可以想象出许多没有任何需要处理的实现。所有这些IFoos都将长时间存在(几分钟),但有时会被其他对象替换,然后资源应该被处理。我猜你是对的,工厂方法的调用者可能不知道返回对象的IDisposable性质。因此,在这种特殊情况下,IFoo可能应该明确继承IDisposable。谢谢。 - Krzysztof Jabłoński

-1

在返回一个可处置对象时,如果值为 null,则应该在方法中处理该对象并返回 null - 这样可以消除与“private static IFoo getDefaultFoo()” 方法相关的“警告/建议”信息。当然,在调用例程中仍需要将该对象视为可处置对象(使用或处置)。

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年之后,我对这个警告不太关心了,因为这个项目现在已经关闭了。 - Krzysztof Jabłoński
然而,我仍然分析了你的答案,对此我非常感激,并且我有两点评论。首先,我有一个印象,你错过了方法getDefaultFoo()的重点。它的目的是提供一个有意义的IFoo实例,特别是不破损或处置。其次,在retnull时调用ret.dispose()将不可避免地导致抛出NullReferenceException - Krzysztof Jabłoński

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接