Я начал отвечать на вопрос, но в итоге я сделал пару изменений в структуре вашего примера, так что это уже не прямой ответ, а скорее другой подход к решению проблемы, который, я полагаю, вы пытаетесьрешать.Надеюсь, это все равно поможет!
Вместо того, чтобы использовать конкретный класс для Dataslot
, я использую интерфейс и также сделал интерфейс универсальным, так что Dataslot<'T>
представляет значение типа 'T
:
type Dataslot<'T> =
abstract Value : 'T
abstract Subscribe : (unit -> unit) -> IDisposable
Мои механизмы подписки больше похожи на то, как работает IObservable
- вы даете ему функцию, которая должна вызываться при изменении значения, и она возвращает IDisposable
, который вы можете использовать дляотмените подписку и прекратите получать уведомления об изменениях.
Затем я определил следующие три примитива, которые можно использовать для работы со слотами данных (реализация ниже):
val mutableSlot : initial:'T -> ('T -> unit) * Dataslot<'T>
val immutableSlot : value:'T -> Dataslot<'T>
val ( <*> ) : f:Dataslot<('T -> 'R)> -> a:Dataslot<'T> -> Dataslot<'R>
immutableSlot
создает слот данных, который никогда не изменяется и всегда возвращает начальное значение. mutableSlot
создает слот данных с начальным значением и возвращает установщик вместе со слотом данных.Вы можете использовать функцию установки для изменения значения в слоте данных. - Оператор
<*>
берет слот данных, содержащий функцию, слот данных, содержащий аргумент, и возвращает слот данных с результатом - результатамиизменяется каждый раз, когда изменяется функция или аргумент.
Стоит отметить, что оператор <*>
и функция immutableSlot
являются шаблоном, который Haskellers вызывает аппликативный функтор .Приятно то, что благодаря тому, как работает частичное приложение и каррирование, теперь вы также можете использовать функции с несколькими аргументами:
let a = immutableSlot 10
let setB, b = mutableSlot 30
let res = immutableSlot (fun a b -> a + b) <*> a <*> b
let sub = res.Subscribe(fun () ->
printfn "Result changed to: %d" res.Value )
Теперь вы можете попробовать вызвать изменение несколько раз и затем вызвать Dispose
дляотписаться от уведомлений:
setB 32
setB 30
sub.Dispose()
setB 1
Реализация этих трех операций очень похожа на часть кода, который вы имели изначально.Главное, что делает это уродливым, это отслеживание обработчиков, которые должны быть уведомлены, когда происходит изменение.
mutableSlot
необходимо инициировать событие изменения всякий раз, когда вызывается установщик:
let mutableSlot initial =
let mutable value = initial
let handlers = ResizeArray<_>()
(fun newValue ->
value <- newValue
for h in handlers do h()),
{ new Dataslot<'T> with
member x.Value = value
member x.Subscribe h =
handlers.Add(h)
{ new IDisposable with
member x.Dispose() = handlers.Remove(h) |> ignore } }
immutableSlot
проще, потому что он никогда не изменяет:
let immutableSlot value =
{ new Dataslot<'T> with
member x.Value = value
member x.Subscribe _ =
{ new IDisposable with member x.Dispose () = () } }
Оператор <*>
более уродлив, поскольку ему необходимо подписаться на уведомления по двум аргументам.Однако, чтобы избежать утечек памяти, необходимо также отписаться, когда число зарегистрированных подписок достигнет нуля (я действительно написал статью об этой утечке памяти !)
let (<*>) (f:Dataslot<'T -> 'R>) (a:Dataslot<'T>) =
let mutable value = f.Value a.Value
let handlers = ResizeArray<_>()
let update () =
value <- f.Value a.Value
for h in handlers do h()
let mutable fsub = { new IDisposable with member x.Dispose() = () }
let mutable asub = { new IDisposable with member x.Dispose() = () }
{ new Dataslot<'R> with
member x.Value =
if handlers.Count > 0 then value else f.Value a.Value
member x.Subscribe h =
handlers.Add(h)
if handlers.Count = 1 then
fsub <- f.Subscribe(update)
asub <- a.Subscribe(update)
value <- f.Value a.Value
{ new IDisposable with
member x.Dispose() =
handlers.Remove(h) |> ignore
if handlers.Count = 0 then
fsub.Dispose()
asub.Dispose() } }
РЕДАКТИРОВАТЬ: Есть довольно сложный аспект в реализации <*>
, когда он пересчитывает свою стоимость.Если кто-то подписывается на уведомления об изменениях, мы предполагаем, что ему понадобится это значение, и поэтому мы пересчитываем его каждый раз, когда один из аргументов изменяется (охотно).Когда никто не подписывается, мы предполагаем, что они могут не получить доступ к значению, поэтому мы выполняем повторные вычисления только при обращении к Value
.Мы могли бы просто подписаться и никогда не отменять подписку (и обновлять всегда с нетерпением), но это может привести к утечкам памяти, если вы неоднократно подписываетесь и отписываетесь.