1. Wprowadzenie
Praca z kolekcjami to nie tylko iteracja, filtrowanie i sortowanie. Bardzo często pojawia się zadanie, żeby zmienić albo przerobić te kolekcje, żeby pasowały do twoich potrzeb: na przykład wziąć kolekcję stringów i zamienić ją na kolekcję liczb, usunąć duplikaty z listy albo całkowicie zmienić strukturę danych.
Wszystkie te zadania można ogarnąć za pomocą standardowych metod kolekcji, prostych pętli i bazowych algorytmów. To trochę dłużej niż z LINQ, ale za to totalnie przejrzyste i mega przydatne do zrozumienia jak działają kolekcje.
Podstawowe sposoby zmiany kolekcji
Kolekcje można zmieniać na różne sposoby, ale najczęściej chodzi o jeden z tych podejść:
- Modyfikacja zawartości: dodanie, usunięcie albo podmiana elementów.
- Przekształcenie elementów: wziąć oryginalną kolekcję i na jej podstawie zrobić nową — innej struktury, typu albo formy (np. z List<int> zrobić List<string>).
- Zmiana widoku: przebudować kolekcję — np. posortować, odwrócić (reverse) albo pogrupować.
Skupimy się na pierwszej i drugiej grupie, bo sortowanie było już dokładnie omówione na osobnym wykładzie.
Kolekcje mutowalne i niemutowalne: ważny niuans
Warto pamiętać, że niektóre kolekcje w .NET można swobodnie zmieniać (np. List<T>), a inne — nie (IReadOnlyList<T>, tablice tylko do odczytu itd.).
- Modyfikacja — zmieniamy konkretną kolekcję (np. przez Add, Remove, Clear itd.).
- Przekształcenie — nie ruszamy oryginału, tylko robimy nową kolekcję, zwykle przez stworzenie nowej listy i wypełnienie jej elementami z oryginalnej kolekcji.
2. Modyfikacja zawartości kolekcji
Wszystkie kolekcje implementujące ICollection<T> mają bazowe metody do zmiany zawartości. Zobaczmy to na przykładzie katalogu książek.
Dodawanie i usuwanie elementów
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
}
List<Book> books = new List<Book>
{
new Book { Title = "Czysty kod", Author = "Robert Martin", Year = 2008 },
new Book { Title = "CLR via C#", Author = "Jeffrey Richter", Year = 2012 }
};
// Dodanie nowej książki
books.Add(new Book { Title = "Head First C#", Author = "Andrew Stellman", Year = 2010 });
// Usunięcie książki po warunku (np. po autorze)
for (int i = books.Count - 1; i >= 0; i--)
{
if (books[i].Author == "Robert Martin")
books.RemoveAt(i);
}
// Usunąć konkretny obiekt (jeśli jest na liście)
Book someBook = books[0];
books.Remove(someBook);
// Wyczyścić całą kolekcję
books.Clear();
// Wstawić element na określoną pozycję
books.Insert(0, new Book { Title = "Pro C# 9", Author = "Andrew Troelsen", Year = 2021 });
Podmiana i aktualizacja elementów
Załóżmy, że ktoś się pomylił z rokiem wydania. Jak to poprawić?
// Znajdź odpowiednią książkę przez pętlę i popraw rok
for (int i = 0; i < books.Count; i++)
{
if (books[i].Title == "Head First C#")
{
books[i].Year = 2018; // Poprawiony rok
break;
}
}
3. Przekształcanie kolekcji
Zmiana typu elementów (np. z Book na string)
Uzyskaj kolekcję tytułów wszystkich książek:
List<string> bookTitles = new List<string>();
foreach (Book book in books)
{
bookTitles.Add(book.Title);
}
foreach (var title in bookTitles)
{
Console.WriteLine(title);
}
Uzyskaj lata wszystkich książek:
List<int> bookYears = new List<int>();
foreach (Book book in books)
{
bookYears.Add(book.Year);
}
Przekształcenie do nowego typu/struktury:
public class BriefBookInfo
{
public string Title;
public int Year;
}
List<BriefBookInfo> briefInfos = new List<BriefBookInfo>();
foreach (Book book in books)
{
briefInfos.Add(new BriefBookInfo { Title = book.Title, Year = book.Year });
}
foreach (var info in briefInfos)
{
Console.WriteLine($"{info.Title} ({info.Year})");
}
Przekształcenie kolekcji kolekcji w jedną (flat map)
Jeśli książka ma listę tagów, uzyskaj jedną listę wszystkich tagów:
public class Book
{
public string Title { get; set; }
public List<string> Tags { get; set; }
}
List<Book> booksWithTags = new List<Book>
{
new Book { Title = "Czysty kod", Tags = new List<string> { "Clean Code", "Refactoring" } },
new Book { Title = "CLR via C#", Tags = new List<string> { "CLR", "Internals" } }
};
List<string> allTags = new List<string>();
foreach (var book in booksWithTags)
{
foreach (var tag in book.Tags)
{
allTags.Add(tag);
}
}
foreach (string tag in allTags)
{
Console.WriteLine(tag);
}
4. Często używane metody przekształcania kolekcji
ToArray
Żeby uzyskać tablicę z listy:
string[] bookTitlesArray = bookTitles.ToArray();
Distinct — pozbywamy się duplikatów
Unikalni autorzy (bez LINQ):
List<string> authors = new List<string>();
foreach (Book book in books)
{
if (!authors.Contains(book.Author))
authors.Add(book.Author);
}
Reverse — odwracamy kolekcję
Żeby zmienić kolejność na odwrotną:
books.Reverse(); // zmienia na miejscu!
Jeśli nie chcesz zmieniać oryginału:
var reversed = new List<Book>(books);
reversed.Reverse();
Sortowanie (patrz wykład o sortowaniu)
books.Sort((a, b) => a.Year.CompareTo(b.Year)); // sortowanie po roku
Grupowanie po polu (emulacja GroupBy)
Pogrupuj książki po autorze (uzyskaj słownik "autor — lista książek"):
Dictionary<string, List<Book>> booksByAuthor = new Dictionary<string, List<Book>>();
foreach (Book book in books)
{
if (!booksByAuthor.ContainsKey(book.Author))
booksByAuthor[book.Author] = new List<Book>();
booksByAuthor[book.Author].Add(book);
}
foreach (var pair in booksByAuthor)
{
Console.WriteLine($"Autor: {pair.Key}");
foreach (var b in pair.Value)
Console.WriteLine($" {b.Title}");
}
5. Przydatne niuanse
- Operacje przekształcania przez pętle działają zawsze i wszędzie.
- Żeby wykluczyć duplikaty, używaj tymczasowych kolekcji i metod Contains/Add.
- Nie zmieniaj kolekcji podczas jej iterowania przez foreach! Jeśli musisz usunąć dużo elementów, najpierw zbierz je do osobnej listy.
6. Przykłady typowych zadań
Uzyskaj listę książek z określonego roku
List<Book> recentBooks = new List<Book>();
foreach (Book book in books)
{
if (book.Year > 2010)
recentBooks.Add(book);
}
foreach (var book in recentBooks)
{
Console.WriteLine($"{book.Title} ({book.Year})");
}
Wyświetl wszystkie unikalne lata publikacji rosnąco
List<int> years = new List<int>();
foreach (Book book in books)
{
if (!years.Contains(book.Year))
years.Add(book.Year);
}
years.Sort();
foreach (int year in years)
Console.WriteLine(year);
Przygotuj książki do eksportu (stwórz listę-DTO)
public class BookExport
{
public string Name;
public string Writer;
public int PublishedYear;
}
List<BookExport> booksForExport = new List<BookExport>();
foreach (Book book in books)
{
booksForExport.Add(new BookExport
{
Name = book.Title,
Writer = book.Author,
PublishedYear = book.Year
});
}
7. Typowe błędy i pułapki
Metody typu Add, Remove, Clear, Insert i Sort zmieniają kolekcję na miejscu.
Jeśli potrzebujesz nowej kolekcji — stwórz nową listę i dodaj tam potrzebne elementy.
Przy usuwaniu elementów podczas iteracji po kolekcji używaj pętli od końca (for (int i = Count-1; i >= 0; i--)).
Jeśli potrzebujesz listy z unikalnymi elementami, pilnuj powtórzeń: używaj Contains albo HashSet<T>.
GO TO FULL VERSION