List transactions
From a recent discussion on using lists, and whether or not you can remove items in while iterating, I decided to write an extension that allows you to get a “transaction” for an IList(Of t). This ListTransaction(Of T) caches adds and removes until you call commit (kudos to Duncan for the idea !!). It also implements IDisposable so you can use a using scope, eg:
Using listTran = mylist.GetTransaction
For Each item In mylist
‘ other code here
listTran.Remove(item)
Next
End Using
And here’s the implementation:
Module ListExtensions
<Runtime.CompilerServices.Extension()> _
Function GetTransaction(Of T)(ByVal list As IList(Of T)) As ListTransaction(Of T)
Return New ListTransaction(Of T)(list)
End Function
Public Class ListTransaction(Of T)
Implements IDisposable
Private _list As IList(Of T)
Private _adds As List(Of T)
Private _removes As List(Of T)
Public Sub New(ByVal list As IList(Of T))
_list = list
End Sub
Public Sub Add(ByVal item As T)
If _adds Is Nothing Then _adds = New List(Of T)
_adds.Add(item)
End Sub
Public Sub Remove(ByVal item As T)
If _removes Is Nothing Then _removes = New List(Of T)
_removes.Add(item)
End Sub
Public Sub Commit()
If _list Is Nothing Then Exit Sub
If _adds IsNot Nothing Then
For Each item As T In _adds
_list.Add(item)
Next
_adds = Nothing
End If
If _removes IsNot Nothing Then
For Each item As T In _removes
_list.Remove(item)
Next
_removes = Nothing
End If
End Sub
Public Sub Rollback()
_adds = Nothing
_removes = Nothing
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Commit()
GC.SuppressFinalize(Me)
End Sub
End Class
End Module
There’s an edge case that will fail.
Add(objA)
Remove(objA)
Add(objA)
Commit
objA will not be in the list after Commit is called. The best way that I can think to do this is to maintain two lists. One contains the committed items. The other contains the working list. Commit will replace the commited list with the working one. Rollback will replace the working one with the commited list.
This could get tricky.
Hi Martin,
Try it. You’ll find OjbA is in the list, at least with List(Of T).
If you had a unique list, then this might be a problem.
You could get around the add/remove/add problem by maintaining a single list of actions (i.e. merging _adds and _removes together) and then Commit performing them in order.
It might be nice to support Contains too; in fact having ListTransaction implement IList would be really cool.
Imports System.Collections.Generic
Module ListExtensions
Function GetTransaction(Of T)(ByVal list As IList(Of T)) As ListTransaction(Of T)
Return New ListTransaction(Of T)(list)
End Function
Public Class ListTransaction(Of T)
Implements IDisposable, IList(Of T)
Private _list As IList(Of T)
Private _clone As List(Of T)
Public Sub New(ByVal list As IList(Of T))
_list = list
End Sub
Public Sub Add(ByVal item As T) Implements ICollection(Of T).Add
If _clone Is Nothing Then _clone = New List(Of T)(_list)
_clone.Add(item)
End Sub
Public Function Remove(ByVal item As T) As Boolean Implements ICollection(Of T).Remove
If _clone Is Nothing Then _clone = New List(Of T)(_list)
Return _clone.Remove(item)
End Function
Public Sub Clear() Implements ICollection(Of T).Clear
If _clone Is Nothing Then
_clone = New List(Of T)
Else
_clone.Clear()
End If
End Sub
Public Function Contains(ByVal item As T) As Boolean Implements ICollection(Of T).Contains
If _clone Is Nothing Then
Return _list.Contains(item)
Else
Return _clone.Contains(item)
End If
End Function
Public Sub CopyTo(ByVal array() As T, ByVal arrayIndex As Integer) Implements ICollection(Of T).CopyTo
If _clone Is Nothing Then
_list.CopyTo(array, arrayIndex)
Else
_clone.CopyTo(array, arrayIndex)
End If
End Sub
Public ReadOnly Property Count() As Integer Implements ICollection(Of T).Count
Get
If _clone Is Nothing Then
Return _list.Count
Else
Return _clone.Count
End If
End Get
End Property
Public ReadOnly Property IsReadOnly() As Boolean Implements ICollection(Of T).IsReadOnly
Get
Return _list.IsReadOnly
End Get
End Property
Public Function GetEnumerator() As IEnumerator(Of T) Implements IEnumerable(Of T).GetEnumerator
If _clone Is Nothing Then
Return _list.GetEnumerator
Else
Return _clone.GetEnumerator
End If
End Function
Private Function mGetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
If _clone Is Nothing Then
Return DirectCast(_list, System.Collections.IEnumerable).GetEnumerator
Else
Return DirectCast(_clone, System.Collections.IEnumerable).GetEnumerator
End If
End Function
Public Function IndexOf(ByVal item As T) As Integer Implements IList(Of T).IndexOf
If _clone Is Nothing Then
Return _list.IndexOf(item)
Else
Return _clone.IndexOf(item)
End If
End Function
Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert
If _clone Is Nothing Then _clone = New List(Of T)(_list)
_clone.Insert(index, item)
End Sub
Default Public Property Item(ByVal index As Integer) As T Implements IList(Of T).Item
Get
If _clone Is Nothing Then
Return _list.Item(index)
Else
Return _clone.Item(index)
End If
End Get
Set(ByVal value As T)
If _clone Is Nothing Then _clone = New List(Of T)(_list)
_clone.Item(index) = value
End Set
End Property
Public Sub RemoveAt(ByVal index As Integer) Implements IList(Of T).RemoveAt
If _clone Is Nothing Then _clone = New List(Of T)(_list)
_clone.RemoveAt(index)
End Sub
Public Sub Commit()
If _clone Is Nothing Then Return
_list.Clear()
Dim genericList As List(Of T) = TryCast(_list, List(Of T))
If genericList IsNot Nothing Then
genericList.Capacity = _clone.Count
End If
For Each loItem As T In _clone
_list.Add(loItem)
Next
End Sub
Public Sub Rollback()
_clone = Nothing
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Commit()
GC.SuppressFinalize(Me)
End Sub
End Class
End Module
Nice idea, but calling ‘Commit’ on Dispose is pretty bad form. Commit should be explicitly called, like in every other transactional scenario.
Also worth noting it’s not thread-safe.