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

Configuration

This detector does not need any configuration.