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?
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With