这是一个假设的场景。
我拥有非常多的用户名(比如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
}
我执行的测试表明,即使并行执行内容,对象也会在正确的点上正确处理:一次在共享连接的整个迭代结束时;并且在非共享连接的每个迭代周期之后。
所以,问题:我做对了吗?
编辑:
有些答案在代码中指出了一些错误,我做了一些修改。编译的完整工作示例如下。
SqlConnection的使用仅用于示例目的,它确实是任何IDisposable。
编译
的示例// 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;
}
}
结果是:
共享连接: “你好” “你好” ... “处置”
个人关系: “你好” “转让” “你好” “处置”
答案 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
)。我假设您的userToConnection
和connectionToResult
函数实际上将一个用户一个连接到一个结果。 (而不是像你的例子那样处理序列):
// 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
函数就是这样的一个例子)。在您的情况下,组合mapUserToConnection
和mapConnectionToResult
可以完全避免此问题,因为该函数可以控制连接的生命周期。
你最终会得到这样的东西:
users
|> Seq.map mapUserToResult
|> Seq.iter print
其中mapUserToResult
为string -> 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!。
第二个例子似乎对我来说很好,但是我遇到了类似问题的代码问题,导致连接在他们不应该处理时被处理掉。
一般来说,我发现合并dispose
和yield 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)
所有这些都清楚地说明了谁来处理资源。