Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a List property which cannot be changed externally

I have a public class in my VB.NET project which has a List(Of String) property. This list needs to be modified by other classes within the project, but since the class may (at some time in the future) be exposed outside the project, I want it to be unmodifiable at that level. The modification of the existing property within the project will only be done by calling the list's methods (notably .Add, occasionally .Clear), not by a wholesale replacement of the property value with a new List (which is why I have it as a ReadOnly property).

I have come up with a way of doing it, but I'm not sure that it's exactly what you would call "elegant".

It's this:

Friend mlst_ParameterNames As List(Of String) = New List(Of String)

Public ReadOnly Property ParameterNames() As List(Of String)
    Get
        Return New List(Of String)(mlst_ParameterNames)
    End Get
End Property

Now this just works fine and dandy. Any class in the project which accesses the mlst_ParameterNames field directly can modify it as needed, but any procedures which access it through the public property can bang away at modifying it to their heart's content, but will get nowhere since the property procedure is always returning a copy of the list, not the list itself.

But, of course, that carries overhead which is why I feel that it's just... well, viscerally "wrong" at some level, even though it works.

The parameters list will never be huge. At most it will only contain 50 items, but more commonly less than ten items, so I can't see this ever being a performance killer. However it has of course set me to thinking that someone, with far more VB.NET hours under their belt, may have a much neater and cleaner idea.

Anyone?

like image 558
Alan K Avatar asked Jun 05 '13 08:06

Alan K


2 Answers

Instead of creating a new copy of the original list, you should use the AsReadOnly method to get a read-only version of the list, like this:

Friend mlst_ParameterNames As List(Of String) = New List(Of String)

Public ReadOnly Property ParameterNames() As ReadOnlyCollection(Of String)
    Get
        Return mlst_ParameterNames.AsReadOnly()
    End Get
End Property

According to the MSDN:

This method is an O(1) operation.

Which means that the speed of the AsReadOnly method is the same, regardless of the size of the list.

In addition to the potential performance benefits, the read-only version of the list is automatically kept in sync with the original list, so if consuming code keeps a reference to it, its referenced list will still be up-to-date, even if items are later added to or removed from the list.

Also, the list is truly read-only. It does not have an Add or Clear method, so there will be less confusion for others using the object.

Alternatively, if all you need is for consumers to be able to iterate through the list, then you could just expose the property as IEnumerable(Of String) which is, inherently, a read-only interface:

Public ReadOnly Property ParameterNames() As IEnumerable(Of String)
    Get
        Return mlst_ParameterNames
    End Get
End Property

However, that makes it only useful to access the list in a For Each loop. You couldn't, for instance, get the Count or access the items in the list by index.

As a side note, I would recommend adding a second Friend property rather than simply exposing the field, itself, as a Friend. For instance:

Private _parameterNames As New List(Of String)()

Public ReadOnly Property ParameterNames() As ReadOnlyCollection(Of String)
    Get
        Return _parameterNames.AsReadOnly()
    End Get
End Property

Friend ReadOnly Property WritableParameterNames() As List(Of String)
    Get
        Return _parameterNames
    End Get
End Property
like image 126
Steven Doggart Avatar answered Nov 27 '22 15:11

Steven Doggart


What about providing a Locked property that you can set, each other property then checks this to see if it's been locked...

Private m_Locked As Boolean = False
Private mlst_ParameterNames As List(Of String) = New List(Of String)

Public Property ParameterNames() As List(Of String)
    Get
        Return New List(Of String)(mlst_ParameterNames)
    End Get
    Set(value As List(Of String))
        If Not Locked Then
            mlst_ParameterNames = value
        Else
            'Whatever action you like here...
        End If
    End Set
End Property

Public Property Locked() As Boolean
    Get
        Return m_Locked
    End Get
    Set(value As Boolean)
        m_Locked = value
    End Set
End Property

-- EDIT --

Just to add to this, then, here's a basic collection...

''' <summary>
''' Provides a convenient collection base for search fields.
''' </summary>
''' <remarks></remarks>
Public Class SearchFieldList
        Implements ICollection(Of String)

#Region "Fields..."

        Private _Items() As String
        Private _Chunk As Int32 = 16
        Private _Locked As Boolean = False
        'I've added this in so you can decide if you want to fail on an attempted set or not...
        Private _ExceptionOnSet As Boolean = False

        Private ptr As Int32 = -1
        Private cur As Int32 = -1

#End Region
#Region "Properties..."

        Public Property Items(ByVal index As Int32) As String
            Get
                'Make sure we're within the index bounds...
                If index < 0 OrElse index > ptr Then
                    Throw New IndexOutOfRangeException("Values between 0 and " & ptr & ".")
                Else
                    Return _Items(index)
                End If
            End Get
            Set(ByVal value As String)
                'Make sure we're within the index bounds...
                If index >= 0 AndAlso Not _Locked AndAlso index <= ptr Then
                    _Items(index) = value
                ElseIf _ExceptionOnSet Then
                    Throw New IndexOutOfRangeException("Values between 0 and " & ptr & ". Use Add() or AddRange() method to append fields to the collection.")
                End If
            End Set
        End Property

        Friend Property ChunkSize() As Int32
            Get
                Return _Chunk
            End Get
            Set(ByVal value As Int32)
                _Chunk = value
            End Set
        End Property

        Public ReadOnly Property Count() As Integer Implements System.Collections.Generic.ICollection(Of String).Count
            Get
                Return ptr + 1
            End Get
        End Property
        ''' <summary>
        ''' Technically unnecessary, just kept to provide coverage for ICollection interface.
        ''' </summary>
        ''' <returns>Always returns false</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property IsReadOnly() As Boolean Implements System.Collections.Generic.ICollection(Of String).IsReadOnly
            Get
                Return False
            End Get
        End Property

#End Region
#Region "Methods..."

        Public Shadows Sub Add(ByVal pItem As String) Implements System.Collections.Generic.ICollection(Of String).Add
            If Not _Items Is Nothing AndAlso _Items.Contains(pItem) Then Throw New InvalidOperationException("Field already exists.")
            ptr += 1
            If Not _Items Is Nothing AndAlso ptr > _Items.GetUpperBound(0) Then SetSize()
            _Items(ptr) = pItem
        End Sub

        Public Shadows Sub AddRange(ByVal collection As IEnumerable(Of String))
            Dim cc As Int32 = collection.Count - 1
            For sf As Int32 = 0 To cc
                If _Items.Contains(collection.ElementAt(sf)) Then
                    Throw New InvalidOperationException("Field already exists [" & collection.ElementAt(sf) & "]")
                Else
                    Add(collection.ElementAt(sf))
                End If
            Next
        End Sub

        Public Function Remove(ByVal item As String) As Boolean Implements System.Collections.Generic.ICollection(Of String).Remove
            Dim ic As Int32 = Array.IndexOf(_Items, item)
            For lc As Int32 = ic To ptr - 1
                _Items(lc) = _Items(lc + 1)
            Next lc
            ptr -= 1
        End Function

        Public Sub Clear() Implements System.Collections.Generic.ICollection(Of String).Clear
            ptr = -1
        End Sub

        Public Function Contains(ByVal item As String) As Boolean Implements System.Collections.Generic.ICollection(Of String).Contains
            Return _Items.Contains(item)
        End Function

        Public Sub CopyTo(ByVal array() As String, ByVal arrayIndex As Integer) Implements System.Collections.Generic.ICollection(Of String).CopyTo
            _Items.CopyTo(array, arrayIndex)
        End Sub

#End Region
#Region "Private..."

        Private Sub SetSize()
            If ptr = -1 Then
                ReDim _Items(_Chunk)
            Else
                ReDim Preserve _Items(_Items.GetUpperBound(0) + _Chunk)
            End If
        End Sub

        Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of String) Implements System.Collections.Generic.IEnumerable(Of String).GetEnumerator
            Return New GenericEnumerator(Of String)(_Items, ptr)
        End Function

        Private Function GetEnumerator1() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
            Return GetEnumerator()
        End Function

#End Region

End Class

Friend Class GenericEnumerator(Of T)
        Implements IEnumerator(Of T)

#Region "fields..."

        Dim flist() As T
        Dim ptr As Int32 = -1
        Dim size As Int32 = -1

#End Region
#Region "Properties..."

        Public ReadOnly Property Current() As T Implements System.Collections.Generic.IEnumerator(Of T).Current
            Get
                If ptr > -1 AndAlso ptr < size Then
                    Return flist(ptr)
                Else
                    Throw New IndexOutOfRangeException("=" & ptr.ToString())
                End If
            End Get
        End Property

        Public ReadOnly Property Current1() As Object Implements System.Collections.IEnumerator.Current
            Get
                Return Current
            End Get
        End Property

#End Region
#Region "Constructors..."


        Public Sub New(ByVal fieldList() As T, Optional ByVal top As Int32 = -1)
            flist = fieldList
            If top = -1 Then
                size = fieldList.GetUpperBound(0)
            ElseIf top > -1 Then
                size = top
            Else
                Throw New ArgumentOutOfRangeException("Expected integer 0 or above.")
            End If
        End Sub

#End Region
#Region "Methods..."

        Public Function MoveNext() As Boolean Implements System.Collections.IEnumerator.MoveNext
            ptr += 1
            Return ptr <= size
        End Function

        Public Sub Reset() Implements System.Collections.IEnumerator.Reset
            ptr = -1
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            GC.SuppressFinalize(Me)
        End Sub

#End Region

End Class
like image 44
Paul Avatar answered Nov 27 '22 13:11

Paul