IEnumerable <idisposable>:谁处理什么和何时 - 我做对了吗?</idisposable>

时间:2011-07-21 15:51:37

标签: c# f# functional-programming idisposable sequences

这是一个假设的场景。

我拥有非常多的用户名(比如10,000,000,000,000,000,000,000。是的,我们处于星际时代:))。每个用户都有自己的数据库。我需要遍历用户列表并对每个数据库执行一些SQL并打印结果。

因为我学到了函数式编程的优点,并且因为我处理了大量的用户,所以我决定使用F#和纯序列(也就是IEnumerable)来实现它。我走了。

// gets the list of user names
let users() : seq<string> = ...

// maps user name to the SqlConnection
let mapUsersToConnections (users: seq<string>) : seq<SqlConnection> = ...

// executes some sql against the given connection and returns some result
let mapConnectionToResult (conn) : seq<string> = ...

// print the result
let print (result) : unit = ...

// and here is the main program
users()
|> mapUsersToConnections
|> Seq.map mapConnectionToResult
|> Seq.iter print

美丽?优雅?绝对

但是! 谁和什么时候处理SqlConnections?

我不知道答案 mapConnectionToResult应该这样做是对的,因为它对所给出的连接的生命周期一无所知。根据{{​​1}}的实施方式和各种其他因素,事情可能有效或无效。

由于mapUsersToConnections是唯一可以访问连接的地方,因此必须负责处理SQL连接。

在F#中,可以这样做:

mapUsersToConnections

C#等价物将是:

// implementation where we return the same connection for each user
let mapUsersToConnections (users) : seq<SqlConnection> = seq {
    use conn = new SqlConnection()
    for u in users do
        yield conn
}


// implementation where we return new connection for each user
let mapUsersToConnections (users) : seq<SqlConnection> = seq {
    for u in users do
        use conn = new SqlConnection()
        yield conn
}

我执行的测试表明,即使并行执行内容,对象也会在正确的点上正确处理:一次在共享连接的整个迭代结束时;并且在非共享连接的每个迭代周期之后。

所以,问题:我做对了吗?

编辑

  1. 有些答案在代码中指出了一些错误,我做了一些修改。编译的完整工作示例如下。

  2. SqlConnection的使用仅用于示例目的,它确实是任何IDisposable。


  3. 编译

    的示例
    // C# -- same connection for all users
    IEnumerable<SqlConnection> mapUsersToConnections(IEnumerable<string> users)
    {
        using (var conn = new SqlConnection())
        foreach (var u in users)
        {
            yield return conn;
        }
    }
    
    // C# -- new connection for each users
    IEnumerable<SqlConnection> mapUsersToConnections(IEnumerable<string> user)
    {
        foreach (var u in users)
        using (var conn = new SqlConnection())
        {
            yield return conn;
        }
    }
    

    结果是:

    共享连接: “你好” “你好” ... “处置”

    个人关系: “你好” “转让” “你好” “处置”

8 个答案:

答案 0 :(得分:7)

我会避免这种方法,因为如果你的图书馆的不知情用户做了像

那样的结构会失败
users()
|> Seq.map userToCxn
|> Seq.toList() //oops disposes connections
|> List.map .... // uses disposed cxns 
. . .. 

我不是这个问题的专家,但我认为这是一种最好的做法,不要让序列/ IEnumerables在产生它们之后弄乱它们,因为中间的ToList()调用会产生不同的结果而不仅仅直接对序列起作用 - DoSomething(GetMyStuff())将与DoSomething(GetMyStuff()。ToList())不同。

实际上,为什么不直接使用序列表达式,因为这样可以完全解决这个问题:

seq{ for user in users do
     use cxn = userToCxn user
     yield cxnToResult cxn }

(其中userToCxn和cxnToResult都是简单的一对一非处置函数)。这似乎比任何东西都更具可读性,并且应该产生所需的结果,可以并行化,并适用于任何一次性用品。可以使用以下技术将其转换为C#LINQ:http://solutionizing.net/2009/07/23/using-idisposables-with-linq/

from user in users
from cxn in UserToCxn(user).Use()
select CxnToResult(cxn)

另一种看法是首先定义你的“getSomethingForAUserAndDisposeTheResource”函数,然后将其用作你的基本构建块:

let getUserResult selector user = 
    use cxn = userToCxn user
    selector cxn

一旦你拥有了这个,那么你可以从那里轻松地建立起来:

 //first create a selector
let addrSelector cxn = cxn.Address()
//then use it like this:
let user1Address1 = getUserResult addrSelector user1
//or more idiomatically:
let user1Address2 = user1 |> getUserResult addrSelector
//or just query dynamically!
let user1Address3 = user1 |> getUserResult (fun cxn -> cxn.Address())

//it can be used with Seq.map easily too.
let addresses1 = users |> Seq.map (getUserResult (fun cxn -> cxn.Address()))
let addresses2 = users |> Seq.map (getUserResult addrSelector)

//if you are tired of Seq.map everywhere, it's easy to create your own map function
let userCxnMap selector = Seq.map <| getUserResult selector
//use it like this:
let addresses3 = users |> userCxnMap (fun cxn -> cxn.Address())
let addresses4 = users |> userCxnMap addrSelector 

如果您想要的只是一个用户,那么您就不会致力于检索整个序列。我想这里学到的经验是让你的核心功能变得简单,并且可以更容易地在它上面构建抽象。请注意,如果在中间某处执行ToList,这些选项都不会失败。

答案 1 :(得分:4)

// C# -- new connection for each users
IEnumerable<SqlConnection> mapUserToConnection(string user)
{
    while (true)
    using (var conn = new SqlConnection())
    {
        yield return conn;
    }
}

这对我来说不合适 - 一旦下一个用户请求新连接(下一个迭代周期),您就会处置连接 - 这意味着这些连接只能一个接一个地使用 - 一旦用户B开始使用他的连接,用户A的连接就会被处理掉。这真的是你想要的吗?

答案 2 :(得分:4)

您的F#示例未进行类型检查(即使您向函数添加了一些虚拟实现,例如使用failwith)。我假设您的userToConnectionconnectionToResult函数实际上将一个用户一个连接到一个结果。 (而不是像你的例子那样处理序列):

// gets the list of user names
let users() : seq<string> = failwith "!"

// maps user name to the SqlConnection
let userToConnection (user:string) : SqlConnection = failwith "!"

// executes some sql against the given connection and returns some result
let connectionToResult (conn:SqlConnection) : string = failwith "!"

// print the result
let print (result:string) : unit = ()

现在,如果您想继续处理userToConnection专用连接,您可以对其进行更改,使其不返回连接SqlConnection。相反,它可以返回一个高阶函数,该函数提供与某个函数的连接(将在下一步中指定),并在调用该函数后,处理该连接。类似的东西:

let userToConnection (user:string) (action:SqlConnection -> 'R) : 'R = 
  use conn = new SqlConnection("...")
  action conn

您可以使用 currying ,因此当您编写userToConnection user时,您将获得一个需要函数并返回结果的函数:(SqlConnection -> 'R) -> 'R。然后你可以像这样编写你的整体功能:

// and here is the main program
users()
|> Seq.map userToConnection
|> Seq.map (fun f -> 
     // We got a function that we can provide with our specific behavior
     // it runs it (giving it the connection) and then closes connection
     f connectionToResult)
|> Seq.iter print

我不太确定您是否要将单个用户映射到单个连接等,但即使您正在处理集合集合,也可以使用完全相同的原则(使用返回函数)。

答案 3 :(得分:3)

我认为这有很大的改进空间。由于mapUserToConnection返回一个序列并且mapConnectionToResult接受一个连接(将map更改为collect s会修复此问题),因此您的代码看起来不应该编译。

我不清楚用户是否应映射到多个连接,或者每个用户是否有一个连接。在后一种情况下,为每个用户返回单例序列似乎有点过分。

通常,从序列中返回IDisposable是一个坏主意,因为您无法控制项目的处置时间。更好的方法是将IDisposable的范围限制为一个函数。这个“控制”函数可以接受使用该资源的回调,并且在回调被触发后,它可以处理该资源(using函数就是这样的一个例子)。在您的情况下,组合mapUserToConnectionmapConnectionToResult可以完全避免此问题,因为该函数可以控制连接的生命周期。

你最终会得到这样的东西:

users
|> Seq.map mapUserToResult
|> Seq.iter print

其中mapUserToResultstring -> string(接受用户并返回结果,从而控制每个连接的生命周期)。

答案 4 :(得分:1)

这些对我来说都不合适 - 例如,为什么要为单个用户名返回一系列连接?你的签名不想看起来像这样(写作Linq-ness的扩展方法):

IEnumerable<SqlConnection> mapUserToConnection(this IEnumerable<string> Usernames)

Anayway,继续前进 - 在第一个例子中:

using (var conn = new SqlConnection())
{
    while (true)
    {
        yield return conn;
    }
}

如果枚举整个集合,这将有效,但。如果(例如)只迭代第一个项目,则不会处理连接(至少在C#中),请参阅Yield and usings - your Dispose may not be called!

第二个例子似乎对我来说很好,但是我遇到了类似问题的代码问题,导致连接在他们不应该处理时被处理掉。

一般来说,我发现合并disposeyield return是一项棘手的业务,我倾向于避免使用它来支持实现我自己的枚举器,该枚举器明确地实现了IDisposable和{ {1}}。这样你就可以确定何时处理对象。

答案 5 :(得分:1)

任何能够保证不再使用该对象的人都应该调用

Dispose。如果你可以做出这样的保证(比如说只有你的方法使用了对象),那么处理它就是你的工作。如果你不能保证对象已经完成(假设你正在使用对象公开迭代器)那么你就不用担心它了,让它们处理它。

您可以遵循的潜在设计决策是CLR对Stream个实例所做的事情。除Stream之外的许多构造函数也接受bool。如果bool为真,则对象知道它一旦完成就会负责处理Stream。如果要返回迭代器,则可以返回tuple,而不是Disposable,bool类型。

但是,我会更深入地了解您所面临的实际问题。也许不要担心这样的事情,你需要改变你的架构,以避免这些问题。例如,不是每个用户拥有一个数据库就有一个数据库。或者您可能需要使用连接池来减少实时但非活动连接的负担(我不是100%关于最后一个关于此类选项的研究)。

答案 6 :(得分:0)

尝试用功能结构解决这个问题是IMO F#陷阱的一个很好的例子。纯函数式语言通常使用不可变数据结构。基于.NET的F#通常不会对性能等事情有所帮助。

我对这个问题的解决方案是隔离在自己的函数中创建和销毁SqlConnection对象的命令性位。在这种情况下,我们会使用useUserConnection

let users() : seq<string> = // ...

/// Takes a function that uses a user's connection to the database
let useUserConnection connectionUser user =
    use conn = // ...
    connectionUser conn

let mapConnectionToResult conn = 
    // ... *conn is not disposed of here* 

// Function currying is used here
let mapUserToResult = useUserConnection mapConnectionToResult

let print result = // ...

// Main program 
users() 
    |> Seq.map mapUserToResult 
    |> Seq.iter print

答案 7 :(得分:0)

我相信这里存在设计问题。如果您查看问题陈述,那就是获取有关用户的一些信息。用户表示为字符串,信息也表示为字符串。所以我们需要的是一个像以下的函数:

let getUserInfo (u:string) : string = <some code here>

使用方法很简单:

users() |> Seq.map getUserInfo 

现在该函数如何获取用户信息取决于此函数,无论是使用SqlConnection,文件流还是任何其他可能是一次性的对象,此函数都有责任创建连接并进行适当的资源处理。在您的代码中,您完全分离了连接创建和获取信息部分,这导致了对谁处置连接的混淆。

现在,如果您想使用所有getUserInfo方法使用的单个连接,那么您可以将此方法设为

let getUserInfoFromConn (c:SqlConnection) (u:string) : string = <some code here>

现在这个函数接受连接(或者可以接受任何其他一次性对象)。在这种情况下,此函数不会丢弃连接对象,而是此函数的调用者将其处理掉。我们可以使用它:

use conn = new SqlConnection()
users() |> Seq.map (conn |> getUserInfoFromConn)

所有这些都清楚地说明了谁来处理资源。