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

Phi*_* P. 8 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
Run Code Online (Sandbox Code Playgroud)

美丽?优雅?绝对.

但! 谁和什么时候处置SqlConnections?

我不知道答案mapConnectionToResult应该是正确的,因为它对连接的生命周期一无所知.根据mapUsersToConnections实施方式和各种其他因素,事情可能有效或无效.

作为mapUsersToConnections唯一可以访问连接的其他地方,它必须负责处理SQL连接.

在F#中,可以这样做:

// 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
}
Run Code Online (Sandbox Code Playgroud)

C#等价物将是:

// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:

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

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


编译的例子

open System

// Stand-in for SqlConnection
type SimpeDisposable() =
    member this.getResults() = "Hello"
    interface IDisposable with
        member this.Dispose() = printfn "Disposing"

// Alias SqlConnection to our dummy
type SqlConnection = SimpeDisposable

// gets the list of user names
let users() : seq<string> = seq {
    for i = 0 to 100 do yield i.ToString()
}

// maps user names to the SqlConnections
// this one uses one shared connection for each user
let mapUsersToConnections (users: seq<string>) : seq<SqlConnection> = seq {
    use c = new SimpeDisposable()
    for u in users do
        yield c
}

// maps user names to the SqlConnections
// this one uses new connection per each user
let mapUsersToConnections2 (users: seq<string>) : seq<SqlConnection> = seq {
    for u in users do
        use c = new SimpeDisposable()
        yield c
}

// executes some "sql" against the given connection and returns some result
let mapConnectionToResult (conn:SqlConnection) : string = conn.getResults()

// print the result
let print (result) : unit = printfn "%A" result

// and here is the main program - using shared connection
printfn "Using shared connection"
users()
|> mapUsersToConnections
|> Seq.map mapConnectionToResult
|> Seq.iter print


// and here is the main program - using individual connections
printfn "Using individual connection"
users()
|> mapUsersToConnections2
|> Seq.map mapConnectionToResult
|> Seq.iter print
Run Code Online (Sandbox Code Playgroud)

结果是:

共享连接:"你好""你好"......"处置"

个人关系:"你好""处置""你好""处置"

Dax*_*ohl 7

我会避免这种方法,因为如果你的库的不知情的用户做了类似的事情,那么结构就会失败

users()
|> Seq.map userToCxn
|> Seq.toList() //oops disposes connections
|> List.map .... // uses disposed cxns 
. . .. 
Run Code Online (Sandbox Code Playgroud)

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

实际上,为什么不只是使用序列表达式来完成整个事情,因为这样可以完全解决这个问题:

seq{ for user in users do
     use cxn = userToCxn user
     yield cxnToResult cxn }
Run Code Online (Sandbox Code Playgroud)

(其中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)
Run Code Online (Sandbox Code Playgroud)

另一个需要考虑的是首先定义你的"getSomethingForAUserAndDisposeTheResource"函数,然后将它作为你的基本构建块:

let getUserResult selector user = 
    use cxn = userToCxn user
    selector cxn
Run Code Online (Sandbox Code Playgroud)

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

 //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 
Run Code Online (Sandbox Code Playgroud)

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