The Motivation for Collection Classes
To help overcome the limitations of a simple array, the .NET Core base class libraries ship with a number of namespaces containing collection classes. Unlike a simple C# array, collection classes are build to dynamically resize themselves on the fly as you insert or remove items.
- Nongeneric Collections (System.Collections namespace)
- Generic Collections (System.Collections.Generic namespace)
The System.Collections Namespace
- ArrayList : Represents a dynamically sized collection of objects listed in sequential order.
- BitArray : Manages a compact array of bit values which are represented as Booleans, where true indicates that the bit is on (1) and false indicates the bit is off (0).
- Hashtable : Represents a collection of key-value pairs that are organized based on the hash code of the key.
- Queue : Represents a standard FIFO collection of objects.:
- SortedList : Represents a collection of key-value pairs that are sorted by the keys and are accessible by key and by index.
- Stack : A LIFO (Last in First out) stack providing push and pop (and peek) functionality.
A Survey of System.Collections.Specialized Namespace
The System.Collections.Specialized namepsace defines a number of specialized collection types.
- HybridDictionary : This class implements IDictionary by using a ListDirectory while they collection is small and then switching to a Hashtable when the collection gets large.
- ListDictionary : This class is useful when you need to manage a small number of items that can change over time. This class makes use of a singly linked list to maintain its data.
- StringCollection : This class provides an optimal way to manage large collections of string data.
- BitVector32 : This class provides a simple structure that stores Boolean values and small integers in 32 bits of memory.
The Problems of Nongeneric Collections
- The first is that using the System.Collections and System.Collections.Specialized classes can result in same poorly performing code, especially when you are manipulating numerical data -> boxing,unboxing.
- The second issue is that most of the nongeneric collection classes are not type-safe because they were developed to operate on System.Objects and they could therefore contain anything at all.
The issue of Performance
C# provides a simple mechanism , termed boxing , to store the data in a value type with a reference variable.
static void SimpleBoxUnboxOperation()
{
int myInt = 25;
// Box the int into an object reference
object boxedInt = myInt;
}Boxing can be formally defined as the process of explicitly assigning a value type to a System.Object variable. When you box a value CoreCLR allocates a new object on the heap and copies the value type's value into that instance. What is returned to you is a reference to the newly allocated heap-based object.
The opposite operation is also permitted through unboxing. Unboxing is the process of converting the value held in the object reference back into a corresponding value type on the stack. Syntactically speaking , an unboxing operation looks like a normal casting operation. However, the semantics are quite different.
The CoreCLR begins by verifying that the receiving data type is equivalent to the boxed type , and if so , it copies the value back into a local stack-based variable.
static void SimpleBoxUnboxOperation()
{
int myInt = 25;
// Box the into an object reference
object boxedInt = myInt;
// Unbox the reference back into a corresponding int
int unboxedInt = (int)boxedInt;
}
When the C# compiler encounters boxing/unboxing syntax, it emits CIL code that contains the box/unbox op codes.
Unlike when performing a typical cast, you must unbox into an appropriate data type otherwise InvalidCaseException exception will be thrown.
int myInt = 25;
object boxedInt = myInt;
try
{
long unboxedLong = (long)boxedInt; // Unbox in the worng data type trigger runtime ex.
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}ArrayList has been built to operate on objects, which represent data allocated on the heap, so it might seem strange that the following code compiles and executes without throwing an error.
static void WorkWithArrayList()
{
// Value types are automatically boxed when passed to a method requesting an object.
ArrayList myInts = new ArrayList();
myInts.Add(10);
myInts.Add(20);
myInts.Add(35);
}Although you pass in numerical data directly into methods requiring an object the runtime automatically boxes the stack-based data on your behalf. Later, if you want to retrieve an item from the ArrayList using the type indexer, you must unbox the heap-allocated object into a stack-allocated integer using a casting operation.
// Unboxing occurs when an object is converted back to stack-based data.
int i = (int)myInts[0];
// Now, it is reboxed, as WriteLine() requires object types.
Console.WriteLine($"Value {i}");To understand the performance issues, ponder these steps that must occur to box and unbox a simple integer:
- A new object must be allocated on the managed heap.
- The value of the stack-based data must be transferred into that memory location.
- When unboxed, the value stored on the heap-based object must be transferred back to the stack.
- The row unused object on the heap will (eventually) be garbage collected.
A First Look at Generic CollectionsT
public class Person
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Person()
{
}
public Person(int age, string firstName, string lastName)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}
}
static void UseGenericList()
{
List<Person> morePeople = new List<Person>(); // in Heap
morePeople.Add(new Person(50,"Frank","Black"));
List<int> moreInts = new List<int>(); // in Stack
moreInts.Add(10);
moreInts.Add(2);
}
Here is a shortlist of the benefits generic containers provide over their nongeneric counterparts:
- Generics provide better performance because they do not result in boxing or unboxing penalties when storing value types.
- Generics are type-safe because they can contain only the type of type you specify.
- Generics greatly reduce the need to build custom collection types because you specify the "type of type" when creating the generic container.
The Role of Generic Type Parameters
Note: Only classes, structures, interfaces, and delegates can be written generically; enum types cannot.
Formally speaking, you call these tokens (List<T>) type parameters; however, in more user-friendly terms, you can simply call them placeholders. You can read the symbol <T> as "of T". Thus, you can read IEnumerable<T> as "IEnumerable of T" or to say it another way "IEnumerable of type T"
Note: The name of a type parameter (placeholder) is irrelevant, and it is up to the developer who created the generic item. However, typically T is used to represent types, TKey or K is used for keys, and TValue or V is used for values.
Specifying Type Parameters for Generic Members
It's fine for a nongeneric class or structure to support generic properties. In these cases, you'd also need to specify the placeholder value at the time you invoke the method.
int[] myInts = { 0, 1, 2, 3 };
Array.Sort<int>(myInts);
Specifying Type Parameters for Generic Interfaces
It's common to implement generic interfaces when you build classes or structures that need to support various framework behaviors.
public interface IComparable
{
int CompareTo(object? obj);
}
class Car : IComparable<Car>
{
public int CarId { get; set; }
public int CompareTo(Car? other)
{
if (this.CarId > other.CarId)
{
return 1;
}
if (this.CarId < other.CarId)
{
return -1;
}
return 0;
}
}