Public member exposes a concrete collection type instead of an interface
ID |
vbnet.maintainability.do_not_expose_generic_lists |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Api Design |
Language |
VB.NET |
Description
Reports a Public member whose signature names a concrete collection type rather than a collection interface: the property or field type, the return type of a function, or the type of a parameter is List(Of T), Dictionary(Of TKey, TValue), SortedList, Queue(Of T), Stack(Of T), ArrayList or Hashtable, with or without a System.Collections prefix and with or without an array suffix. Members declared on an interface are examined as well. Only the declared type name is inspected, so an interface type such as IList(Of T), IReadOnlyDictionary(Of TKey, TValue) or IEnumerable(Of T) is accepted, as is a type designed to be exposed such as ReadOnlyCollection(Of T) or a Collection(Of T) subclass. Non-public class members are not examined.
Rationale
A concrete collection in a public signature makes an implementation choice part of the contract. Once a property is typed List(Of OrderLine), callers may legitimately depend on anything List offers - Capacity, Sort, AddRange, indexer assignment - so the type can never be changed to a different collection, wrapped for validation, made lazy, or replaced by a result that streams instead of materializing, without a breaking change. On a return value it also hands callers a mutable handle to the object’s own state: book.Lines.Clear() reaches past every invariant the class enforces, and there is no place to intercept the mutation because the caller never calls a member of the class. Concrete collection types have no variance either, so a method taking List(Of OrderLine) rejects an array, a HashSet, a LINQ query result, or any other sequence the caller already has, forcing a defensive ToList() at every call site - an extra copy whose only purpose is to satisfy the signature. ArrayList and Hashtable carry the further problem of being untyped, so every read needs a cast and a type error surfaces at run time rather than at compile time.
The following code illustrates the pattern detected by this rule:
Public Class OrderBook
' FLAGGED: Public member exposes a concrete collection type instead of an interface
Public Property Lines As List(Of OrderLine)
' FLAGGED: Public member exposes a concrete collection type instead of an interface
Public Totals As Dictionary(Of String, Decimal)
' FLAGGED: Public member exposes a concrete collection type instead of an interface
Public Shared Cache As Hashtable = New Hashtable()
' FLAGGED: Public member exposes a concrete collection type instead of an interface
Public Function GetPendingLines() As List(Of OrderLine)
Return New List(Of OrderLine)()
End Function
Remediation
Type the member with the least capable interface that expresses what callers actually need. For a sequence they only read, use IEnumerable(Of T); for one they index, use IList(Of T) or - when they must not modify it - IReadOnlyList(Of T); for a keyed lookup use IDictionary(Of TKey, TValue) or IReadOnlyDictionary(Of TKey, TValue). On a parameter this is a pure widening change and callers keep compiling. On a property or return value returning an interface is not by itself protection - hand back New ReadOnlyCollection(Of T)(items) or a copy if the caller must not mutate the object’s state, and expose Add/Remove members on the declaring type instead of a settable collection. Replace ArrayList and Hashtable outright with their generic counterparts so element types are checked at compile time.