返回一个实现了IDisposable接口的对象是不是一个好主意?

4

我的情境是这样的。

我正在使用System.DirectoryServices.AccountManagement来处理AD用户和组。我的主要方法(现在是控制台应用程序)调用一个返回PrincipalSearchResult<Principal>的方法。这两个对象都实现了IDisposable接口。

如果我返回这个对象,怎么确保我能够处理所有这些可处理的对象?

class Searcher
{    
    private PrincipalSearchResult<Principal> SearchForObjects(string searchString)
    {
        PrincipalContext ctx = null;
        PrincipalSearcher principalSearcher = null;
        Principal principal = null;

        ctx = new PrincipalContext(ContextType.Domain, "blah", "dc=blah,dc=com");

        principal = new GroupPrincipal(ctx) { Name = searchString };
        principalSearcher = new PrincipalSearcher { QueryFilter = principal };

        return principalSearcher.FindAll();
    }
}

static void Main(string[] args)
{
    Searchers searchers = new Searchers();

    PrincipalSearchResult<Principal> theGroups = searchers.SearchForGroupsMatching("some*");

    foreach (GroupPrincipal group in theGroups)
    {
        Console.WriteLine(group.DisplayName);
        // do stuff...
    }
}

传递PrincipalSearchResult<Principal>是否真的是一个非常糟糕的想法,原因与未管理对象处理有关?
创建托管代理对象会更好吗?
我想象一下,如果我只关心读取属性子集,那么创建一个只包含这些属性的自定义对象可能会更好。在写回时,例如写回到AD组,我只需要将更改后的属性和键传递给方法即可。这将允许我将未管理对象的创建限制为一个范围内。或者这一切都是不必要的,而且更麻烦?对于这个半分散的意识流,我深感抱歉...

我不能同意这个观点。没有任何保证最终的清理器(finalizer)会被调用。 - Daniel Hilgarth
当对象即将被GC回收时,Finalizer将被调用,除非应用程序在此之前退出。无论如何,资源都将被释放。 - cHao
当.NET对象超出范围时,托管资源将被释放。但是,不能保证相关的非托管对象会被清理。我相信它可能会存在(咳咳)“永远”。 - Mike
@cHao:请加强您对这个主题的理解。不能保证对象的终结器被执行 一定 - Daniel Hilgarth
我在MSDN文档的Object.Finalize下的备注中找到了几行。 “Finalize方法用于在当前对象被销毁之前对当前对象持有的非托管资源执行清理操作。”--好的。 “此方法在对象变得不可访问后自动调用…”--不可访问是指它超出了范围吗? - Mike
显示剩余30条评论
4个回答

2
不,这不是一个坏主意。在.NET框架中有很多类似的例子,例如SqlClient.ExecuteReader
但是,你需要记录用户在使用完对象后需要立即处理它。
你可以这样做:
using(PrincipalSearchResult<Principal> theGroups = 
      searchers.SearchForGroupsMatching("some*"))
{
    foreach (GroupPrincipal group in theGroups)
    {
        Console.WriteLine(group.DisplayName);
        // do stuff...
    }
}

对不起,我不理解你的评论。 - Daniel Hilgarth
1
它们都指向同一个托管对象,因此:是的,它们都内部指向同一个未托管的对象。 - Daniel Hilgarth
1
如果这是一个类,那么是的,“实例”都是对同一对象的引用。如果您将对象返回给调用者,则不需要在搜索函数中处理该对象。事实上,如果您这样做了,可能会导致问题。 - cHao
感谢Daniel和cHao。这就是我所缺少的。 - Mike
我一直在查找资料,只是为了确保我说的不是胡话。 :) - cHao
显示剩余5条评论

1

返回实现IDisposable接口的类型并不是错误的,但会有一些不便之处。

如果您知道调用方除了几个简单属性外不需要其他任何东西,那么创建并返回一个代理对象的做法是有很多好处的 - 这可以简化调用方的代码,并保证非托管资源得到正确释放。

在您的情况下,如果您只关心DisplayName属性,那么我肯定会创建一个代理对象。这样您就完全不需要担心PrincipalSearchResult<Principal>的生命周期问题,更重要的是,也不需要担心这个对象保持的任何连接或其他资源。

这还简化了测试过程:您可以很容易地模拟或者替换掉返回代理对象的方法调用,因为代理对象是由您自己创建的。

然而,如果调用方的代码将使用到需要完整的PrincipalSearchResult<Principal>的方法,则不能使用代理。


感谢您解答我帖子底部的问题。到目前为止,讨论的重点都集中在对象处理上。实际上,我想要比DisplayName更多的属性,但是您关于只在需要其方法时保留“完整”对象的评论真的很好。我也没有考虑过存根/模拟问题。+1 - Mike

1

返回一个实现了IDisposable接口的对象是一种同时令人反感、有用且在某种程度上不可避免的模式。只将新创建的对象作为返回值提供给工厂会带来一个危险,即在对象执行需要清理的操作之后,但在其被存储在一个在异常被捕获时仍然存在的位置之前,可能会抛出异常。

更安全的模式是通过引用传递一个工厂用于存储正在创建的对象的位置。按照这个模式,如果工厂在构造完成并能够容忍Dispose之后、执行任何需要清理的操作之前,将新对象的引用存储下来,那么调用工厂的代码就可以在之后的任何时间点上抛出异常时对新创建的对象调用Dispose。

事实上,我认为后一种对象构造方式除了两个因素外要比前一种方式优越得多:

  1. 使用"using"结构允许一个清晰明了的语法来自动化清理代码,但是它不适用于那种工厂的风格(可以通过欺骗来实现,但不能让"using"在构造函数抛出异常时清理部分构建的对象)。
  2. 直接调用构造函数的正常方式将新实例作为返回值交给调用代码,如果构造函数抛出异常,则返回值会消失。每个对象创建最终都归结为对"new()"的调用,因此完全避免函数返回值成为IDisposable唯一现有引用的方法是不让类公开任何公共构造函数,并使所有私有和受保护的构造函数接受引用参数以夹带新的对象实例。

虽然函数返回新构造IDisposable实例的唯一现有引用并不好,但这是一种相当普遍的模式,因为目前C#和.NET尚未提供更好的替代方法。


0
调用代码应该使用using块编写,这将确保调用Dispose。IDisposable是控制资源释放时间的方法,因此返回一个实现IDisposable的对象是一种标准惯用法。

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