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



5 Comments so far

  1.   MartinJ on December 18th, 2008          

    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.

  2.   bill on December 18th, 2008          

    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.

  3.   andy on December 30th, 2008          

    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.

  4.   andy on December 30th, 2008          

    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

  5.   silky on January 1st, 2009          

    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.