如何协调使用IDisposable和IoC?

42

我终于开始理解C#中的IoC和DI,但是有些地方仍然让我感到困惑。我正在使用Unity容器,但我认为这个问题更广泛适用。

使用IoC容器来分发实现IDisposable接口的实例让我很不安!你如何知道是否应该Dispose()它?这个实例可能只是专门为你创建的(因此你应该Dispose()),或者它可能是由其他地方管理生存期的实例(因此你最好不要)。代码中没有任何提示,实际上这可能会根据配置而改变!这对我来说看起来很危险。

在场的任何IoC专家能描述一下处理这种模糊性的好方法吗?


1
我的解决方案是使用具有适当和良好编码的生命周期管理的IoC:AutoFac和Castle Windsor都有这样的功能(尽管它们的工作方式略有不同);在默认生命周期管理器下处理瞬态时,Unit 2.1会简单地失败。 - user2864740
7个回答

17

绝对不应该在注入到你的类中的对象上调用Dispose()。你不能假设只有你是唯一的使用者。最好的方法是在某个托管接口中包装你的非托管对象:

public class ManagedFileReader : IManagedFileReader
{
    public string Read(string path)
    {
        using (StreamReader reader = File.OpenRead(path))
        {
            return reader.ReadToEnd();
        }
    }
}

那只是一个例子,如果我想把文本文件读入字符串中,我会使用File.ReadAllText(path)。

另一种方法是注入一个工厂并自行管理对象:

public void DoSomething()
{
    using (var resourceThatShouldBeDisposed = injectedFactory.CreateResource())
    {
        // do something
    }
}

2
但我确实想要注入IDisposable对象,而且进一步说,根据配置,我可能需要或不需要在注入点处Dispose()它们。当底层资源被短暂访问时,您的第一个示例可以工作,但如果资源更持久,则无法帮助。在第二个示例中,以这种方式注入工厂似乎对我来说很奇怪;这是IoC模型的主要功能之一。如果我将IoC应用于此概念,则似乎最终回到了最初的问题。我认为容器本身需要参与其中,就像AutoFac一样。 - Mr. Putty
8
@Mr. Putty - 依赖注入并不能消除工厂的需求,它只是使某些对工厂的(滥用)使用变得不再必要。例如,当您无法在运行时确定具体依赖项的类型,或者需要昂贵条件依赖关系(可能根本不需要但创建这些对象需要大量资源)时,您可能希望注入一个工厂而不是对象本身。根据您所使用的DI框架对IDisposable支持的程度,工厂注入可能是最佳方式——这肯定比许多其他选择更透明。(+1) - Jeff Sternal
我认为这是一个好建议。永远不要直接注入IDisposables; 而是使用托管包装器 - 或者,在特定情况下允许显式处理,例如在db上下文中使用HttpContextScoped(在StructureMap中)或PerRequestLifetimeManager(在Unity中)。 - L-Four
@L-Three 并且使用一个真正理解IDisposable生命周期的IoC容器..但是当可处理对象被处理时会有一些怪癖。到目前为止,我发现的唯一“确定”的方法是类似于Castle和[typed]工厂:虽然瞬态将最终从根释放(即没有“总体”资源泄漏),但确保立即子图处置的唯一方法是采用“factory.Release”策略。然而,这比直接处理IDisposable服务要好:1)生命周期仍由IoC处理;2)消费者对组件是否可处理是不可知的。 - user2864740

7

AutoFac 通过允许创建嵌套容器来处理此问题。当容器完成时,它会自动处理其中的所有IDisposable对象。更多信息请点击这里

...当您解析服务时,Autofac会跟踪已解析的可处理(IDisposable)组件。在工作单元结束时,您需要处理相关生命周期范围,Autofac将自动清理/处理已解析的服务。


很有趣。我之前不了解AutoFac。我认为使用Unity做类似的事情不会太难。我需要再仔细思考一下这个问题。谢谢! - Mr. Putty
1
这可能在Unity中很难实现,因为自始至终确定性处理已经嵌入到Autofac的架构中。 - Rinat Abdullin
1
Castle Windsor是另一个容器,它允许您使用子容器、作用域生命周期或通过显式释放组件来实现这一点,使用container.Release方法。 - Krzysztof Kozmic
1
StructureMap还有一个嵌套容器的概念来处理这个问题。然而,它只适用于短暂的对象,不会处理在http上下文级别作用域的对象(有一个方法调用可以处理)或单例对象(完全由您决定是否处理)。 - Andy
AutoFac似乎总是针对容器/生命周期范围的IDisposable生命周期(这很好用,而且与Unity不同,它确实提供了瞬态的最终清理)。Castle Windsor可以[也]释放子组件树,无需创建显式作用域,这在实现诸如工厂之类的东西时非常有用。(但为了使其按广告所述工作并允许早期释放,必须有明确的“factory.Release”调用,因此在某种程度上它转移了所有权。) - user2864740

4
这也经常困扰着我。虽然不太高兴,但我总是得出结论:最好不要以短暂的方式返回IDisposable对象。
最近,我为自己重新提出了这个问题:这真的是一个IoC问题,还是一个.net框架问题?Dispose本身就很棘手。它没有意义上的功能目的,只有技术上的作用。所以这更多是一个我们必须处理的框架问题,而不是一个IoC问题。
我喜欢DI的原因是我可以要求提供给我功能的契约,而不必担心技术细节。我不是所有者。不知道它在哪一层。不知道需要哪些技术来实现合同,不必担心寿命。我的代码看起来漂亮整洁,易于测试。我可以在它们所属的层中实现职责。
因此,如果有一个例外情况需要我组织寿命,请让那个例外情况存在。无论我是否喜欢它。如果实现接口的对象要求我将其处理掉,我想知道它,因为这样我会尽可能短地使用该对象。通过使用稍后被处理的子容器来解析它的技巧可能会导致我保持对象的生命周期比我应该的时间更长。对象的允许寿命是在注册对象时确定的。而不是由创建子容器并保持一段时间的功能决定的。
因此,只要我们开发人员需要担心处理(这会改变吗?),我将尽量少地注入短暂可处理的对象。 1. 我尝试使对象不可处理,例如通过不在类级别上保存可处理对象,而是在更小的范围内保存它们。 2. 我尝试使对象可重用,以便可以应用不同的生命周期管理器。
如果这不可行,我使用工厂来表明注入合同的用户是所有者,并应对其负责。
有一个注意事项:将契约实现者从不可处理更改为可处理将是一个重大变化。这时候接口不再注册,而是注册工厂的接口。但我认为这也适用于其他情况。忘记使用子容器将从那时起导致内存问题。工厂方法将引起IoC解析异常。
下面是一些示例代码:
using System;
using Microsoft.Practices.Unity;

namespace Test
{
    // Unity configuration
    public class ConfigurationExtension : UnityContainerExtension
    {
        protected override void Initialize()
        {
            // Container.RegisterType<IDataService, DataService>(); Use factory instead
            Container.RegisterType<IInjectionFactory<IDataService>, InjectionFactory<IDataService, DataService>>();
        }
    }

    #region General utility layer

    public interface IInjectionFactory<out T>
        where T : class
    {
        T Create();
    }

    public class InjectionFactory<T2, T1> : IInjectionFactory<T2>
        where T1 : T2
        where T2 : class

    {
        private readonly IUnityContainer _iocContainer;

        public InjectionFactory(IUnityContainer iocContainer)
        {
            _iocContainer = iocContainer;
        }

        public T2 Create()
        {
            return _iocContainer.Resolve<T1>();
        }
    }

    #endregion

    #region data layer

    public class DataService : IDataService, IDisposable
    {
        public object LoadData()
        {
            return "Test data";
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                /* Dispose stuff */
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    #endregion

    #region domain layer

    public interface IDataService
    {
        object LoadData();
    }

    public class DomainService
    {
        private readonly IInjectionFactory<IDataService> _dataServiceFactory;

        public DomainService(IInjectionFactory<IDataService> dataServiceFactory)
        {
            _dataServiceFactory = dataServiceFactory;
        }

        public object GetData()
        {
            var dataService = _dataServiceFactory.Create();
            try
            {
                return dataService.LoadData();
            }
            finally
            {
                var disposableDataService = dataService as IDisposable;
                if (disposableDataService != null)
                {
                    disposableDataService.Dispose();
                }
            }
        }
    }

    #endregion
}

2
将一个外观放在容器前面也可以解决这个问题。此外,您可以扩展它以跟踪更丰富的生命周期,例如服务关闭和启动或ServiceHost状态转换。
我的容器倾向于存在于实现IServiceLocator接口的IExtension中。它是Unity的外观,并允许在WCF服务中轻松访问。此外,我可以从ServiceHostBase访问服务事件。
您最终得到的代码将尝试查看是否有任何单例注册或创建的任何类型实现了外观跟踪的任何接口。
仍然无法及时处理处置,因为您被绑定在这些事件上,但它有所帮助。
如果您想及时处理处置(即现在与服务关闭),则需要知道您获取的项是可处置的,它是业务逻辑的一部分来处置它,因此IDisposable应该是对象接口的一部分。并且可能应该验证与调用处置方法相关的期望单元测试。

你不会失去构造函数注入的优势吗? - L-Four

2

我认为通常最好的方法是不要Dispose已经注入的东西;你必须假设注入器正在进行分配和释放。


3
非常不方便,因为我经常需要注入一次性对象。通常,IDisposable 的语义要求对象的创建者/所有者在适当的时间Dispose它。如果容器负责,则实例的使用者需要告诉容器何时完成。这很容易被遗忘。 - Mr. Putty
1
因为你回答的后半部分不太好,所以被踩了。大多数容器都有处理这个问题的方法,可以使用嵌套容器或类似StructureMaps的“ReleaseAndDisposeAllHttpScopedObjects”方法。但是你仍然需要弄清楚可释放对象如何被正确释放...除非你喜欢出现连接不足等问题。 - Andy

2
这取决于DI框架。一些框架允许您指定是否要为每个依赖项注入共享实例(始终使用相同的引用)。在这种情况下,您很可能不想处理。
如果您可以指定要注入唯一实例,则需要处理(因为它是专门为您构建的)。但我对Unity不太熟悉,您需要查看文档以了解如何在其中实现此功能。尽管我已经尝试过MEF和其他一些框架,它们的属性中都包含此部分信息。

1
在Unity框架中,有两种注册注入类的方式:作为单例(当您解析它时,始终会获得相同的类实例)或者每次解析时都获得该类的新实例。
在后一种情况下,您有责任在不再需要已解析的实例时将其处理掉(这是一种相当合理的方法)。另一方面,当您处理容器(处理对象解析的类)时,所有单例对象也会被自动处理。
因此,在使用Unity框架时,似乎没有关于注入可处理对象的问题。我不知道其他框架,但我认为只要依赖注入框架足够稳定,肯定会以某种方式处理此问题。

1
这是一篇很棒的文章:http://www.ladislavmrnka.com/2011/03/unity-build-in-lifetime-managers/ - TrueWill
2
这是一种可怕的方法,注入和未跟踪的对象需要被处理,没有任何“合理”的地方。这很复杂,因为组件无法手动处理它们注入的依赖项,因为生命周期是未知的 - 单例、瞬态等。在标准生命周期下,Unity (2.x) 容器根本无法处理瞬态对象的处理。 - user2864740
在后一种情况下......客户端不知道是第一种情况还是后一种情况,也不应该知道。它应该对注入对象的选择寿命保持不知情状态。我不同意user2864740的评论,认为Unity失败了。我认为这是一个我们不得不处理的框架特性,不幸的是。 - Remco te Wierik

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