The Arrays and Strings lesson (Module 1) introduced List<T> in passing, with the promise
of studying it "in Module 4." That moment has arrived, along with Dictionary<TKey, TValue>
and the rest of the collections .NET offers. This lesson reviews List<T> in depth,
introduces Dictionary<TKey, TValue> for fast lookups by key, and — most importantly —
finally builds the complete Library class: the central point that will bring together
BiblioTech's entire catalog of items and all its members in memory, ready to be queried with
LINQ in the next lesson.
Contents
- Reminder: arrays versus
List<T> List<T>in depthDictionary<TKey, TValue>: lookups by key- Other collections:
Queue<T>andStack<T> - The interfaces behind collections:
IEnumerable<T>andICollection<T> - Building
Library:Catalog,Members,Loans - Full usage of
Library
- Reminder: arrays versus
List<T>
List<T>The Arrays and Strings lesson left the central limitation of arrays on the table: their size
is fixed when they're created and can't change afterward. List<T>, on the other hand, grows
and shrinks dynamically as elements are added or removed:
Array (Book[]) |
List<T> (List<Book>) |
|
|---|---|---|
| Size | Fixed from creation | Dynamic: grows and shrinks with Add/Remove |
| Adding a new element | Not possible directly; you have to create a larger array | list.Add(item) |
| Removing an element | Not possible directly | list.Remove(item) |
| Access by index | array[0] |
list[0] (just as direct) |
| Generic | Yes, from its declaration (Book[]) |
Yes, List<T> (Module 4) |
| When to use it | Known, fixed size in advance | The number of elements changes during execution (the common case in BiblioTech) |
A real library's catalog grows (new books are bought) and shrinks (damaged items are retired)
constantly: it's exactly the scenario for which List<T>, now that you know generics, is the
natural choice over an array.
List<T> in depth
List<T> in depthList<T> (defined in System.Collections.Generic) offers a much richer set of operations
than an array:
List<LibraryItem> catalog = new List<LibraryItem>();
catalog.Add(new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"));
catalog.Add(new Magazine("National Geographic", "Various authors", 302));
Console.WriteLine(catalog.Count); // 2 (not "Length", as with arrays)
catalog.Remove(catalog[0]); // removes the first element (searches by reference equality)
Console.WriteLine(catalog.Count); // 1
catalog.Insert(0, new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
Console.WriteLine(catalog[0].Title); // "Ficciones"
bool containsAny = catalog.Contains(catalog[0]); // True| Member | What it does |
|---|---|
Add(item) |
Adds an element at the end |
Remove(item) |
Removes the first occurrence of that element |
RemoveAt(index) |
Removes the element at that position |
Insert(index, item) |
Inserts at a specific position, shifting the rest |
Contains(item) |
Does the list contain that exact element? |
Count |
The current number of elements (equivalent to Length on an array) |
list[index] |
Direct access or assignment by index, just like an array |
List<T> internally implements an array that .NET automatically resizes when more space is
needed; this is an implementation detail you don't need to manage yourself: from the outside,
List<T> simply "grows whenever it needs to."
Dictionary<TKey, TValue>: lookups by key
Dictionary<TKey, TValue>: lookups by keyWalking an entire List<Member> with a foreach to find the member with Id == 3 is valid,
but inefficient if the catalog grows large: in the worst case, every single element has to be
checked one by one. Dictionary<TKey, TValue> — the two-type-parameter generic example that
closed the previous lesson — solves this by associating each value with a unique key,
with near-instant lookups regardless of how many elements it holds:
Dictionary<int, Member> membersById = new Dictionary<int, Member>();
membersById[1] = new Member(1, "Ana Martinez");
membersById[2] = new Member(2, "Luis Gomez");
Member member = membersById[1]; // direct access by key, with no traversal at all
Console.WriteLine(member.Name); // "Ana Martinez"Accessing a key that doesn't exist with membersById[99] throws an exception
(KeyNotFoundException); TryGetValue is the safe way to check without risking that
exception:
if (membersById.TryGetValue(99, out Member? foundMember))
{
Console.WriteLine(foundMember.Name);
}
else
{
Console.WriteLine("No member exists with that Id.");
}| Member | What it does |
|---|---|
dictionary[key] = value |
Assigns (or overwrites, if the key already existed) |
dictionary[key] |
Gets the value; throws an exception if the key doesn't exist |
TryGetValue(key, out value) |
Tries to get the value; returns false with no exception if it doesn't exist |
ContainsKey(key) |
Does that key exist? |
Remove(key) |
Removes the entry with that key |
Keys / Values |
Collections of all the keys or all the values |
- Other collections:
Queue<T> and Stack<T>
Queue<T> and Stack<T>.NET offers other generic collections specialized for a specific access order, useful in more specific scenarios:
Queue<T>(FIFO — first in, first out): the first element in is the first one out, withEnqueue()to add andDequeue()to remove. This fits, for example, a waiting list for reservations on a very popular book: the first member to sign up is the first to receive the book when it becomes available.Stack<T>(LIFO — last in, first out): the last element in is the first one out, withPush()to add andPop()to remove. Useful for "undo the last action" scenarios, where what matters is reverting in the reverse order things were done.
Queue<Member> waitingList = new Queue<Member>();
waitingList.Enqueue(new Member(1, "Ana Martinez")); // signs up first
waitingList.Enqueue(new Member(2, "Luis Gomez")); // signs up later
Member nextToReceiveIt = waitingList.Dequeue(); // "Ana Martinez": the first to sign upBoth are mentioned here so you'll recognize them if they appear in someone else's code; the
rest of this module focuses on List<T> and Dictionary<TKey, TValue>, which are, by far,
the most commonly used collections in everyday work.
- The interfaces behind collections:
IEnumerable<T> and ICollection<T>
IEnumerable<T> and ICollection<T>List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T> aren't isolated types: they
all implement a common set of interfaces (recall the Interfaces lesson) that define shared
capabilities:
| Interface | What it guarantees |
|---|---|
IEnumerable<T> |
Can be traversed with foreach; the common minimum for every collection |
ICollection<T> |
In addition to traversal, Count can be queried, and elements added/removed |
IList<T> |
In addition to the above, access by index (collection[i]) is possible |
This hierarchy of interfaces is exactly why foreach works the same way over an array, a
List<T>, or the keys of a Dictionary<TKey, TValue>: they all implement, at minimum,
IEnumerable<T>. And it's also why, in the next lesson, LINQ will be able to operate on any of
them indistinctly: most of its operations are defined on IEnumerable<T>, not on List<T>
specifically.
- Building
Library: Catalog, Members, Loans
Library: Catalog, Members, LoansWith List<T> and Dictionary<TKey, TValue> now mastered, the Library class is completed —
it appeared in the Delegates and Events lesson with only its event. Here's its full version,
with the three central collections of the model and an index by Id for fast member lookups:
class Library
{
public List<LibraryItem> Catalog { get; } = new List<LibraryItem>();
public List<Member> Members { get; } = new List<Member>();
public List<Loan> Loans { get; } = new List<Loan>();
private Dictionary<int, Member> _membersById = new Dictionary<int, Member>();
public event Action<Loan> LoanRegistered;
public void AddItem(LibraryItem item)
{
Catalog.Add(item);
}
public void AddMember(Member member)
{
Members.Add(member);
_membersById[member.Id] = member;
}
public Member? FindMemberById(int id)
{
_membersById.TryGetValue(id, out Member? member);
return member;
}
public void RegisterLoan(Loan loan)
{
Loans.Add(loan);
LoanRegistered?.Invoke(loan);
}
}A few design decisions deserve an explanation:
Catalog,Members, andLoansare read-only properties ({ get; }, recalling the Encapsulation lesson from Module 3): the reference to each list can't be reassigned from outside (library.Catalog = anotherList;doesn't compile), but its contents can be modified through the methods designed for that (AddItem,AddMember,RegisterLoan)._membersByIdisprivate: it's an internal implementation detail — an index to speed upFindMemberById— that shouldn't be exposed or managed directly from outside the class;AddMembertakes care of keeping it in sync withMembers.- The
LoanRegisteredevent, introduced in the previous lesson, stays unchanged: it now lives alongside the collections, andRegisterLoanboth adds the loan toLoansand notifies whoever has subscribed.
classDiagram
class Library {
+List~LibraryItem~ Catalog
+List~Member~ Members
+List~Loan~ Loans
-Dictionary~int, Member~ _membersById
+event LoanRegistered
+AddItem(LibraryItem)
+AddMember(Member)
+FindMemberById(int) Member
+RegisterLoan(Loan)
}
Library --> "*" LibraryItem
Library --> "*" Member
Library --> "*" Loan
- Full usage of
Library
LibraryLibrary library = new Library();
library.LoanRegistered += loan =>
Console.WriteLine($"'{loan.Book.Title}' has been lent to {loan.Member.Name}");
library.AddItem(new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"));
library.AddItem(new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
library.AddItem(new Magazine("National Geographic", "Various authors", 302));
library.AddMember(new Member(1, "Ana Martinez"));
library.AddMember(new Member(2, "Luis Gomez"));
Console.WriteLine(library.Catalog.Count); // 3
Console.WriteLine(library.Members.Count); // 2
Member? member1 = library.FindMemberById(1);
if (member1 is not null && library.Catalog[0] is Book book1)
{
book1.Lend();
library.RegisterLoan(new Loan(book1, member1));
// "'Hopscotch' has been lent to Ana Martinez"
}From this version on, Library is the single entry point to the application's entire
in-memory state: the whole catalog, every member, and the loan history, each one accessible as
a completely standard .NET collection.
Common Mistakes and Tips
- Using
List<T>.CountexpectingLength: arrays useLength;List<T>,Dictionary<TKey, TValue>, and the rest of the generic collections useCount. Mixing them up is an easy-to-fix compilation error, but a common one when starting out. - Accessing a nonexistent key with
dictionary[key]: it throwsKeyNotFoundException; useTryGetValue(orContainsKeybefore accessing) when you're not sure the key exists. - Forgetting to sync an auxiliary structure like
_membersById: if a member were added directly withMembers.Add(...)from outside the class instead of going throughAddMember, the dictionary would end up out of date; that's whyMembersis read-only, and every addition must go, without exception, throughAddMember. - Choosing the wrong collection for the access pattern: if you constantly need to look
something up by a unique identifier, a
Dictionaryis far more efficient than walking an entireList<T>every time; if you only ever need to traverse everything in order,List<T>is enough. - Tip: before choosing a collection, ask yourself how you'll most often access its data (by position? by key? always the first one in?); the answer almost always points to the right collection.
Exercises
-
Create a
List<LibraryItem>with at least three elements (a mix ofBookandMagazine). Add a new one withAdd, remove one withRemove, and showCountbefore and after each operation. -
Create a
Dictionary<int, Member>with at least three members, indexed by theirId. UseTryGetValueto look up anIdthat exists and one that doesn't, showing a different message in each case without the program throwing any exception. -
Build the complete
Libraryclass from this section. Register two items and one member, subscribe to theLoanRegisteredevent with a confirmation message, lend one of the items, and register it withRegisterLoan. Check thatLoans.Countbecomes1.
Solutions
List<LibraryItem> items = new List<LibraryItem>
{
new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
new Magazine("National Geographic", "Various authors", 302),
new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1")
};
Console.WriteLine(items.Count); // 3
items.Add(new Magazine("Very Interesting", "Various authors", 45));
Console.WriteLine(items.Count); // 4
items.Remove(items[0]);
Console.WriteLine(items.Count); // 3
Dictionary<int, Member> membersById = new Dictionary<int, Member>
{
[1] = new Member(1, "Ana Martinez"),
[2] = new Member(2, "Luis Gomez"),
[3] = new Member(3, "Marta Lopez")
};
if (membersById.TryGetValue(2, out Member? found))
{
Console.WriteLine($"Found: {found.Name}"); // "Luis Gomez"
}
if (!membersById.TryGetValue(99, out Member? notFound))
{
Console.WriteLine("No member exists with Id 99.");
}
Library library = new Library();
library.LoanRegistered += l =>
Console.WriteLine($"'{l.Book.Title}' has been lent to {l.Member.Name}");
library.AddItem(new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"));
library.AddItem(new Magazine("National Geographic", "Various authors", 302));
library.AddMember(new Member(1, "Ana Martinez"));
Member? member = library.FindMemberById(1);
if (member is not null && library.Catalog[0] is Book book)
{
book.Lend();
library.RegisterLoan(new Loan(book, member));
}
Console.WriteLine(library.Loans.Count); // 1
Conclusion
In this lesson you've gone deeper into List<T>, learned Dictionary<TKey, TValue> for fast
lookups by key, seen Queue<T> and Stack<T> in passing, and understood that all these
collections share a common set of interfaces (IEnumerable<T>, ICollection<T>). Above all,
the Library class is now complete: Catalog, Members, and Loans as its central
collections, an index by Id for fast member lookups, and the LoanRegistered event living
alongside everything else.
Having the entire catalog in a List<LibraryItem> opens the door to this module's most
powerful tool for working with collections: LINQ. The next lesson will teach you to
filter, sort, and group library.Catalog and library.Loans with expressive, compact syntax,
finally leaving manual foreach loops behind for this kind of query.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
