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

  1. Reminder: arrays versus List<T>
  2. List<T> in depth
  3. Dictionary<TKey, TValue>: lookups by key
  4. Other collections: Queue<T> and Stack<T>
  5. The interfaces behind collections: IEnumerable<T> and ICollection<T>
  6. Building Library: Catalog, Members, Loans
  7. Full usage of Library

  1. Reminder: arrays versus 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.

  1. List<T> in depth

List<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."

  1. Dictionary<TKey, TValue>: lookups by key

Walking 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

  1. Other collections: 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, with Enqueue() to add and Dequeue() 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, with Push() to add and Pop() 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 up

Both 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.

  1. The interfaces behind collections: 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.

  1. Building Library: Catalog, Members, Loans

With 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, and Loans are 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).
  • _membersById is private: it's an internal implementation detail — an index to speed up FindMemberById — that shouldn't be exposed or managed directly from outside the class; AddMember takes care of keeping it in sync with Members.
  • The LoanRegistered event, introduced in the previous lesson, stays unchanged: it now lives alongside the collections, and RegisterLoan both adds the loan to Loans and 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

  1. Full usage of Library

Library 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>.Count expecting Length: arrays use Length; List<T>, Dictionary<TKey, TValue>, and the rest of the generic collections use Count. 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 throws KeyNotFoundException; use TryGetValue (or ContainsKey before accessing) when you're not sure the key exists.
  • Forgetting to sync an auxiliary structure like _membersById: if a member were added directly with Members.Add(...) from outside the class instead of going through AddMember, the dictionary would end up out of date; that's why Members is read-only, and every addition must go, without exception, through AddMember.
  • Choosing the wrong collection for the access pattern: if you constantly need to look something up by a unique identifier, a Dictionary is far more efficient than walking an entire List<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

  1. Create a List<LibraryItem> with at least three elements (a mix of Book and Magazine). Add a new one with Add, remove one with Remove, and show Count before and after each operation.

  2. Create a Dictionary<int, Member> with at least three members, indexed by their Id. Use TryGetValue to look up an Id that exists and one that doesn't, showing a different message in each case without the program throwing any exception.

  3. Build the complete Library class from this section. Register two items and one member, subscribe to the LoanRegistered event with a confirmation message, lend one of the items, and register it with RegisterLoan. Check that Loans.Count becomes 1.

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.

© Copyright 2026. All rights reserved