IEnumerable <IDisposable>: кто распоряжается чем и когда - правильно ли я понял? - PullRequest
8 голосов
/ 21 июля 2011

Вот гипотетический сценарий.

У меня очень большое количество имен пользователей (скажем, 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

Результаты:

Совместное подключение: "Привет" "Привет" ... "Располагая"

Индивидуальные подключения: "Привет" «Располагая» "Привет" "Располагая"

Ответы [ 8 ]

7 голосов
/ 21 июля 2011

Я бы избегал такого подхода, поскольку структура потерпела бы крах, если бы невольный пользователь вашей библиотеки сделал что-то вроде

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 где-то посередине.

4 голосов
/ 21 июля 2011

Ваш образец 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

Вы можете использовать curry , поэтому, когда вы напишите 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

Я не совсем уверен, хотите ли вы привязать одного пользователя к одному соединению и т. Д., Но вы можете использовать точно такой же принцип (с возвращением функций), даже если вы работаете с коллекциями коллекций.

4 голосов
/ 21 июля 2011
// C# -- new connection for each users
IEnumerable<SqlConnection> mapUserToConnection(string user)
{
    while (true)
    using (var conn = new SqlConnection())
    {
        yield return conn;
    }
}

Мне это не кажется правильным - вы бы избавились от соединения, как только следующий пользователь запросит новое соединение (следующий цикл итерации) - это означает, что эти соединения могут использоваться только одинпосле другого - как только пользователь B начинает работать со своим соединением, соединение пользователя A уничтожается.Вы действительно этого хотите?

3 голосов
/ 21 июля 2011

Я думаю, что в этом есть огромные возможности для улучшения. Не похоже, что ваш код должен компилироваться, поскольку mapUserToConnection возвращает последовательность, а mapConnectionToResult принимает соединение (изменение map s на collect s исправит это).

Мне неясно, должен ли пользователь сопоставлять несколько соединений или есть одно соединение на пользователя. В последнем случае кажется чрезмерным возвращать одноэлементную последовательность для каждого пользователя.

Как правило, возвращать IDisposable из последовательности - плохая идея, поскольку вы не можете контролировать время удаления элемента. Лучший подход - ограничить область действия IDisposable одной функцией. Эта «управляющая» функция может принимать обратный вызов, использующий ресурс, и после запуска обратного вызова она может распоряжаться ресурсом (например, функция using). В вашем случае объединение mapUserToConnection и mapConnectionToResult может полностью избежать этой проблемы, поскольку функция может контролировать время жизни соединения.

В итоге вы получите что-то вроде этого:

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

, где mapUserToResult - это string -> string (принятие пользователя и возвращение результата, тем самым контролируя время жизни каждого соединения).

1 голос
/ 21 июля 2011

Dispose должен вызывать тот, кто может гарантировать объект больше не используется.Если вы можете дать такую ​​гарантию (скажем, единственный раз, когда объект используется в вашем методе), то ваша задача - избавиться от него.Если вы не можете гарантировать, что объект сделан (например, вы выставляете итератор с объектами), тогда ваша задача - не беспокоиться об этом и позволить им разобраться с ним.

О возможном решении по проектувы можете следить за тем, что делает CLR для Stream экземпляров.Многие конструкторы, которые кроме Stream также принимают bool.Если это bool истинно, то объект знает, что он отвечает за избавление от Stream, как только это будет сделано с ним.Если вы возвращаете итератор, вы можете вместо него вернуть tuple типа Disposable,bool.

Однако я бы посмотрел глубже на реальную проблему, с которой вы столкнулись.Возможно, вместо того, чтобы беспокоиться о таких вещах, вам нужно изменить архитектуру, чтобы избежать этих проблем.Например, вместо базы данных на пользователя есть одна база данных.Или, может быть, вам нужно использовать пул соединений, чтобы уменьшить нагрузку на живые, но неактивные соединения (я не на 100% об этом последнем, исследующем такие варианты).

1 голос
/ 21 июля 2011

Мне все это не кажется правильным - например, почему вы возвращаете последовательность соединений для одного имени пользователя? Разве ваша подпись не хотела выглядеть так (написано как метод расширения для Linq-ness):

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

Anayway, двигаясь дальше - в первом примере:

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

Это будет работать, но только , если перечислена вся коллекция. Если (например) итерируется только первый элемент, то соединение не будет удалено (по крайней мере, в C #), см. Выход и использование - ваш Dispose не может быть вызван! .

Второй пример, кажется, работает нормально для меня, но у меня были проблемы с кодом, который сделал похожую вещь и привел к удалению соединений, когда они не должны были.

Вообще говоря, я обнаружил, что объединение dispose и yield return является сложным делом, и я склонен избегать его в пользу реализации собственного перечислителя, который явно реализует как IDisposable, так и IEnumerable. Таким образом, вы можете быть уверены, когда объекты будут утилизированы.

0 голосов
/ 22 июля 2011

Я считаю, что здесь есть проблема дизайна.Если вы посмотрите на постановку задачи, речь идет о получении некоторой информации о пользователе.Пользователь представлен в виде строки, а информация также представлена ​​в виде строки.Итак, нам нужна такая функция, как:

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

Использование этого так же просто, как:

users() |> Seq.map getUserInfo 

Теперь, как эта функция получает информацию о пользователе, зависит от этой функции, будь тоиспользует SqlConnection, File stream или любой другой объект, который может быть одноразовым или нет, эта функция отвечает за создание соединения и правильную обработку ресурсов.В вашем коде вы полностью разделили создание и извлечение информационных частей, что вызывает путаницу в том, кто распоряжается соединением.

Теперь, если вы хотите использовать одно соединение, которое будет использоваться всеми методами getUserInfo, вы можетесделать этот метод как

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

Теперь эта функция принимает соединение (или может принимать любой другой одноразовый объект).В этом случае эта функция не будет избавляться от объекта подключения, а вызывающая функция будет избавляться от него.Мы можем использовать это как:

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

Все это проясняет, кто обрабатывает ресурсы.

0 голосов
/ 21 июля 2011

Попытка решить эту проблему только с помощью функциональных конструкций - это IMO хороший пример ловушки F #. Чисто функциональные языки обычно используют неизменные структуры данных. F #, основанный на .NET, часто этого не делает, что иногда может быть очень полезно для таких вещей, как производительность.

Мое решение этой проблемы состоит в том, чтобы выделить императивный бит создания и уничтожения объекта 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
...