【问题标题】:IEnumerable<IDisposable>: who disposes of what and when -- Did I get it right?IEnumerable<IDisposable>:谁在什么时候处理什么——我做对了吗?
【发布时间】:2011-10-10 08:58:14
【问题描述】:

这是一个假设的场景。

我有大量的用户名(比如 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 应该这样做 的答案是正确的,因为它对所提供的连接的生命周期一无所知。根据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
}

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;
    }
}

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

那么,问题:我做对了吗?

编辑

  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

结果是:

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

个人连接: “你好” “处置” “你好” “处置”

【问题讨论】:

  • 10 亿用户。我认为您的系统中有一些多帐户。
  • 请注意:不要忘记在这种情况下对函数式编程的热爱:纯粹的函数式编程定义为没有副作用的编码。 IDisposable.Dispose()(以及几乎任何返回 void 的 任何东西)仅针对其副作用执行,因此根据定义,它是纯函数式编码的相反

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


【解决方案1】:

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

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 用于将整个事物包装在序列表达式中。这使它变得懒惰并且保持资源生命周期的确定性。
  • 关于 Seq.toList() 的一个非常好的观点,感谢您指出这一点。它实际上回答了最初的问题“我做对了吗”。好像没有。
【解决方案2】:
// C# -- new connection for each users
IEnumerable<SqlConnection> mapUserToConnection(string user)
{
    while (true)
    using (var conn = new SqlConnection())
    {
        yield return conn;
    }
}

这对我来说看起来不对 - 一旦下一个用户(下一个迭代周期)请求新连接,您就会丢弃连接 - 这意味着这些连接只能一个接一个地独占使用- 一旦用户 B 开始使用他的连接,用户 A 的连接就会被释放。这真的是你想要的吗?

【讨论】:

    【解决方案3】:

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

    // 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 -&gt; 'R) -&gt; '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
    

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

    【讨论】:

    • 这看起来是一个非常好的技术,唯一的问题是你不能多次调用那个高阶函数,因为第二次调用将使用已处理的对象。如果该函数被包装并传播到其他东西等中,直到我们在程序的完全不相关的部分中出现奇怪的错误,问题就会变得更糟。所以并不是一个理想的解决方案。除非我错过了什么?
    【解决方案4】:

    我认为这方面还有很大的改进空间。看起来您的代码不应该编译,因为mapUserToConnection 返回一个序列并且mapConnectionToResult 接受一个连接(将您的maps 更改为collects 将解决此问题)。

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

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

    你最终会得到这样的结果:

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

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

    【讨论】:

    • 是的,你是对的,函数应该返回 SqlConnection 和字符串,而不是 seq 和 seq。呵呵,我在想什么。我想进一步的样本不再起作用了......将更正帖子。谢谢!
    【解决方案5】:

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

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

    无论如何,继续 - 在第一个示例中:

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

    这会起作用,但在整个集合被枚举的情况下。如果(例如)仅迭代第一项,则不会释放连接(至少在 C# 中)请参阅Yield and usings - your Dispose may not be called!

    第二个示例对我来说似乎工作得很好,但我遇到了一些问题,代码执行了类似的操作,导致连接在不应该有的时候被丢弃。

    总的来说,我发现将disposeyield return 结合起来是一件棘手的事情,我倾向于避免使用它,而倾向于实现我自己的枚举器,它明确地实现了IDisposableIEnumerable。这样您就可以确定何时处理对象。

    【讨论】:

    • 关于为一次性资源实施IEnumerable 的建议。那是自找麻烦……而且我想不出没有更好的解决方案的情况。我认为“正确”的答案是告诉 OP 这是一种严重的代码异味。
    • @kragen:我进行了编辑以更正代码。请看一看。
    • @Daniel:但是函数式编程和地图和朋友的使用怎么样?有时也别无选择。
    • @Komrade:我认为你不应该从序列中返回IDisposables。此问题的答案中提到了几种可行的替代方案。
    【解决方案6】:

    Dispose 应该由能够保证对象不再被使用的人调用。如果您可以做出保证(例如在您的方法中使用该对象的唯一时间),那么您的工作就是处理它。如果你不能保证对象已经完成(比如你要暴露一个带有对象的迭代器),那么你的工作就是不用担心它,让他们处理它。

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

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

    【讨论】:

    • 好点。尽管我不同意使用bool 值来控制谁处理Stream 示例中的类似内容,但这是解决此问题的最佳方法的演示。是的,这行得通,但对我来说,这突出了问题而不是解决了问题。当然,这里我们谈论的是包装其他对象的对象,而不是纯粹的独立函数。
    • @Komrade P:我认为这不是最好的演示,但我也认为如果需要处置,那么您需要从架构的角度弄清楚谁负责处置对象,而不是试图不定义该事实。另一种选择是在被调用者不负责的情况下使用忽略 Dispose 调用的代理。
    【解决方案7】:

    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
    

    【讨论】:

      【解决方案8】:

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

      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)
      

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

      【讨论】:

      • 是的,您是对的,在这个特定示例中,您的建议可能是正确的做法。我的问题是关于IDisposable 的更普遍使用,特别是在迭代器和产生/使用迭代器的独立函数的上下文中。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-09
      • 1970-01-01
      • 2017-09-29
      • 2021-03-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多