Type with a Default indexed property implements no collection interface
ID |
vbnet.maintainability.index_with_i_collection |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Api Design |
Language |
VB.NET |
Description
Reports a class that declares a Default indexed property - the VB form of an indexer - and has
no Implements clause naming a collection interface such as ICollection, ICollection(Of T),
IList, IList(Of T), IDictionary, IDictionary(Of K, V) or IEnumerable, and no base type
whose name is one of the framework collection bases. A plain indexed property that is not marked
Default is not an indexer and is not reported.
Rationale
An indexer is the syntax of a collection: bag(key) tells every reader that the object holds
items and can be asked for one. Callers then reach for the rest of what a collection offers and
find it missing. For Each does not compile against the type, so consumers write an index loop
and need a Count the type may not expose. None of LINQ is available, because every query
operator is defined over IEnumerable(Of T), so filtering or projecting the contents means
copying them into a List first. Data binding, String.Join, collection initialisers and the
many APIs that take an IEnumerable(Of T) parameter are all closed to it, and the workaround
each caller invents - exposing the private dictionary, or adding a ToList method - puts the
type’s storage back in the public surface. Implementing at least IEnumerable(Of T) opens all of
that, and the type keeps its indexer.
The following code illustrates the pattern detected by this rule:
Namespace Acme.Configuration
' FLAGGED: Type with a Default indexed property implements no collection interface
Public Class SettingBag
Private ReadOnly _values As New Dictionary(Of String, String)()
Default Public Property Item(key As String) As String
Get
Return _values(key)
End Get
Set(value As String)
_values(key) = value
End Set
End Property
Public ReadOnly Property Count As Integer
Get
Return _values.Count
End Get
End Property
End Class
Remediation
Implement IEnumerable(Of T) as the minimum so the type works with For Each and LINQ, and
implement ICollection(Of T) or IList(Of T) when callers are also meant to add, remove or
count items. For a key-based indexer, implement IDictionary(Of K, V) or expose
IReadOnlyDictionary(Of K, V). Where the type is not really a collection, drop the Default
modifier and give the accessor a name that says what the lookup means - SettingFor(key).
' Before: indexable, but not usable with For Each, LINQ or data binding
Public Class SettingBag
Default Public Property Item(key As String) As String
' After
Public Class SettingBag
Implements IEnumerable(Of KeyValuePair(Of String, String))
Default Public Property Item(key As String) As String
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of String, String)) _
Implements IEnumerable(Of KeyValuePair(Of String, String)).GetEnumerator
Return _values.GetEnumerator()
End Function