Tablice i kolekcje w C#
Tablice i kolekcje są fundamentalnymi strukturami danych w języku C#, które umożliwiają efektywne przechowywanie i zarządzanie danymi. Ten przewodnik przedstawia zarówno podstawowe, jak i zaawansowane aspekty pracy z tymi strukturami.
Oto podstawowe informacje na temat tablic i kolekcji w C#:
Tablice (Arrays):
Tablice to sekwencyjne struktury danych o stałym rozmiarze, które przechowują elementy tego samego typu.
Deklaracja i Inicjalizacja:
Tablice są deklarowane za pomocą nawiasów kwadratowych []
. Przykład deklaracji i inicjalizacji tablicy liczb całkowitych:
// Deklaracja tablicy o określonym rozmiarze
int[] liczby = new int[10];
// Inicjalizacja podczas deklaracji
int[] liczby2 = { 1, 2, 3, 4, 5 };
// Alternatywna składnia
int[] liczby3 = new int[] { 1, 2, 3, 4, 5 };
// Inicjalizacja z użyciem konstruktora
string[] imiona = new string[3] { "Anna", "Jan", "Piotr" };
Dostęp do elementów i właściwości:
Elementy tablicy są indeksowane od zera. Możesz uzyskać dostęp do elementu za pomocą indeksu:
int pierwszyElement = liczby[0];
int[] tablica = { 10, 20, 30, 40, 50 };
// Dostęp do elementów (indeksowanie od 0)
int pierwszyElement = tablica[0]; // 10 - Pierwszy element tablicy
int ostatniElement = tablica[tablica.Length - 1]; // 50 - ostatnie element
// Właściwości tablicy
Console.WriteLine($"Długość tablicy: {tablica.Length}");
Console.WriteLine($"Ranga tablicy: {tablica.Rank}");
// Iteracja przez tablicę
foreach (int element in tablica)
{
Console.WriteLine(element);
}
Tablice wielowymiarowe
// Tablica dwuwymiarowa
int[,] macierz = new int[3, 4];
int[,] macierz2 = { {1, 2}, {3, 4}, {5, 6} };
// Tablica postrzępiona (jagged array)
int[][] tablicaPostrzepiona = new int[3][];
tablicaPostrzepiona[0] = new int[4];
tablicaPostrzepiona[1] = new int[2];
tablicaPostrzepiona[2] = new int[3];
Kolekcje (Collections):
Kolekcje oferują większą elastyczność niż tablice i dynamicznie zarządzają pamięcią.
Kilka popularnych kolekcji w C#:
List<T> – Dynamiczna lista
List<T>
to dynamiczna lista, która automatycznie dostosowuje swój rozmiar w miarę dodawania lub usuwania elementów.
List<int> liczby = new List<int>();
// Dodawanie elementów
liczby.Add(1);
liczby.Add(2);
liczby.AddRange(new int[] { 3, 4, 5 });
// Dostęp do elementów
int pierwszyElement = liczby[0];
int ostatniElement = liczby[liczby.Count - 1];
// Modyfikacja
liczby.Insert(2, 99); // Wstaw 99 na pozycję 2
liczby.Remove(99); // Usuń pierwszą instancję 99
liczby.RemoveAt(0); // Usuń element na pozycji 0
// Przydatne metody
bool zawiera = liczby.Contains(3);
int indeks = liczby.IndexOf(4);
liczby.Sort();
liczby.Reverse();
Dictionary<TKey, TValue> – Słownik
Dictionary<TKey, TValue>
to kolekcja par klucz-wartość, która pozwala na efektywne przechowywanie i wyszukiwanie danych za pomocą kluczy.
Dictionary<string, int> punkty = new Dictionary<string, int>();
// Dodawanie elementów
punkty["Matematyka"] = 90;
punkty.Add("Fizyka", 85);
punkty["Biologia"] = 60;
// Dostęp do wartości
if (punkty.ContainsKey("Matematyka"))
{
int punktyMat = punkty["Matematyka"];
}
// Bezpieczny dostęp
if (punkty.TryGetValue("Chemia", out int punktyChemia))
{
Console.WriteLine($"Punkty z chemii: {punktyChemia}");
}
// Iteracja
foreach (var para in punkty)
{
Console.WriteLine($"{para.Key}: {para.Value}");
}
// Iteracja tylko po kluczach lub wartościach
foreach (string przedmiot in punkty.Keys)
{
Console.WriteLine(przedmiot);
}
HashSet<T> – Zbiór unikalnych elementów
HashSet<T>
to kolekcja, która przechowuje unikalne elementy i jest przydatna do operacji na zbiorach matematycznych.
HashSet<int> unikalneLiczby = new HashSet<int>();
unikalneLiczby.Add(1);
unikalneLiczby.Add(2);
unikalneLiczby.Add(2); // Nie zostanie dodane - duplikat
// Operacje zbiorów
HashSet<int> zbior1 = new HashSet<int> { 1, 2, 3, 4 };
HashSet<int> zbior2 = new HashSet<int> { 3, 4, 5, 6 };
zbior1.UnionWith(zbior2); // Suma zbiorów
zbior1.IntersectWith(zbior2); // Przecięcie zbiorów
zbior1.ExceptWith(zbior2); // Różnica zbiorów
Queue<T> i Stack<T> – Kolejka i Stos
Queue<T>
to kolejka FIFO (First-In-First-Out), a Stack<T>
to stos LIFO (Last-In-First-Out). Obydwa są przydatne do zarządzania danymi w kolejności.
// Queue - FIFO (First In, First Out)
Queue<string> kolejka = new Queue<string>();
kolejka.Enqueue("Pierwszy");
kolejka.Enqueue("Drugi");
kolejka.Enqueue("Trzeci");
string pierwszy = kolejka.Dequeue(); // "Pierwszy"
string nastepny = kolejka.Peek(); // "Drugi" (bez usuwania)
// Stack - LIFO (Last In, First Out)
Stack<string> stos = new Stack<string>();
stos.Push("Pierwszy");
stos.Push("Drugi");
stos.Push("Trzeci");
string ostatni = stos.Pop(); // "Trzeci"
string wierzcholek = stos.Peek(); // "Drugi" (bez usuwania)
IEnumerable<T>
Interfejs IEnumerable<T>
pozwala na iterację przez kolekcje.
IEnumerable<int> wyniki = liczby.Where(x => x > 2);
To tylko kilka podstawowych informacji na temat tablic i kolekcji w języku C#. Wybór odpowiedniej struktury danych zależy od konkretnej sytuacji i wymagań projektu. Kolekcje są zazwyczaj bardziej elastyczne i użyteczne w bardziej zaawansowanych scenariuszach niż tradycyjne tablice.
Oto kilka zaawansowanych koncepcji i praktyk związanych z tablicami i kolekcjami w języku C#:
Kolekcje niemutowalne (Immutable Collections):
Kolekcje niemutowalne pozwalają na tworzenie kolekcji, które nie mogą być zmieniane po ich zainicjowaniu. To jest przydatne, gdy chcesz zachować niemutowalność danych.
using System.Collections.Immutable;
// Tworzenie niemutowalnej listy
ImmutableList<int> niemutowalnaLista = ImmutableList.Create(1, 2, 3);
var nowaLista = niemutowalnaLista.Add(4); // Zwraca nową instancję
// Niemutowalny słownik
ImmutableDictionary<string, int> niemutowalnySłownik =
ImmutableDictionary<string, int>.Empty
.Add("Klucz1", 10)
.Add("Klucz2", 20);
Kolekcje do równoległego programowania (Concurrent Collections):
Kolekcje do równoległego programowania, takie jak ConcurrentQueue<T>
i ConcurrentDictionary<TKey, TValue>
, są zoptymalizowane pod kątem operacji wielowątkowych i zapewniają bezpieczny dostęp do danych z wielu wątków.
using System.Collections.Concurrent;
// Bezpieczna kolejka dla wielu wątków
ConcurrentQueue<int> bezpiecznaKolejka = new ConcurrentQueue<int>();
bezpiecznaKolejka.Enqueue(1);
if (bezpiecznaKolejka.TryDequeue(out int wynik))
{
Console.WriteLine($"Pobrano: {wynik}");
}
// Bezpieczny słownik
ConcurrentDictionary<string, int> bezpiecznySlownik =
new ConcurrentDictionary<string, int>();
bezpiecznySlownik.TryAdd("Klucz", 100);
bezpiecznySlownik.AddOrUpdate("Klucz", 1, (klucz, stareWart) => stareWart + 1);
Kolekcje tylko do odczytu
List<int> lista = new List<int> { 1, 2, 3, 4, 5 };
IReadOnlyList<int> tylkoDoOdczytu = lista.AsReadOnly();
// Interfejsy tylko do odczytu
IReadOnlyCollection<int> kolekcjaTylkoDoOdczytu = lista;
IReadOnlyDictionary<string, int> slownikTylkoDoOdczytu = new ReadOnlyDictionary<string, int>(punkty);
LINQ – Language Integrated Query
LINQ umożliwia wykonywanie zapytań na kolekcjach w sposób deklaratywny.
List<int> liczby = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Składnia zapytań (query syntax)
var parzysteLiczby = from liczba in liczby
where liczba % 2 == 0
select liczba;
// Składnia metod (method syntax)
var nieparzysteLiczby = liczby
.Where(x => x % 2 == 1)
.OrderByDescending(x => x)
.Take(3);
// Przykłady operacji LINQ
var suma = liczby.Sum();
var srednia = liczby.Average();
var maksymalna = liczby.Max();
var pierwszaParzyssta = liczby.FirstOrDefault(x => x % 2 == 0);
// Grupowanie
var osoby = new List<Osoba>
{
new Osoba { Imie = "Anna", Wiek = 25 },
new Osoba { Imie = "Jan", Wiek = 30 },
new Osoba { Imie = "Piotr", Wiek = 25 }
};
var grupy = osoby.GroupBy(o => o.Wiek);
LINQ to zestaw narzędzi, które pozwalają na wykonywanie zapytań i operacji na danych w kolekcjach. Dzięki LINQ możesz wykonywać filtry, sortowania, grupowania i wiele innych operacji na danych w sposób czytelny i efektywny.
Kolekcje niestandardowe:
Możesz tworzyć własne kolekcje niestandardowe, dziedzicząc po klasach Collection<T>
, KeyedCollection<TKey, TItem>
lub implementując interfejsy ICollection<T>
i IEnumerable<T>
. To pozwala na dostosowanie zachowania kolekcji do swoich potrzeb.
Operacje na Kolekcjach:
Przysłanianie elementów, sortowanie, przeszukiwanie, tworzenie kopii kolekcji, łączenie kolekcji i wiele innych operacji można wykonywać na kolekcjach w C#.
Warto poznać różne metody i techniki dostępne w standardowej bibliotece.
Delegaty do Porównywania (Comparers) i Filtrów (Predicates):
Możesz dostosować sortowanie i filtrowanie kolekcji za pomocą delegatów. To pozwala na bardziej elastyczną kontrolę nad operacjami na danych.
To zaawansowane techniki związane z tablicami i kolekcjami w języku C#. Wybór odpowiedniej struktury danych i techniki operacji zależy od konkretnej sytuacji i wymagań projektu. Kolekcje są kluczowym elementem programowania w C#, więc warto zgłębić te koncepcje, aby tworzyć efektywne i skalowalne aplikacje.
Serializacja Kolekcji
Jeśli musisz przechowywać lub przesyłać kolekcje danych między różnymi częściami aplikacji, to serializacja jest kluczowym zagadnieniem. Możesz użyć różnych mechanizmów serializacji, takich jak XML, JSON, lub Binary Serialization, aby zapisywać i odtwarzać kolekcje w formie danych.
Kolekcje niestandardowe z interfejsami nie mutowalnymi
W C# możesz tworzyć niemutowalne kolekcje niestandardowe, które implementują interfejsy niemutowalne, takie jak IReadOnlyList<T>
, IReadOnlyCollection<T>
, IImmutableList<T>
i IImmutableDictionary<TKey, TValue>
. To pozwala na dostarczenie użytkownikom kolekcji tylko do odczytu, co jest użyteczne w pewnych scenariuszach.
Kolekcje nie mutowalne w wielowątkowym programowaniu
Kolekcje niemutowalne są szczególnie przydatne w programowaniu wielowątkowym, ponieważ nie wymagają synchronizacji dostępu do danych między wątkami. Możesz bezpiecznie używać tych kolekcji w wielu wątkach bez ryzyka wystąpienia błędów synchronizacji.
Kolekcje przyjazne dla LINQ
Jeśli często korzystasz z LINQ do manipulacji danymi, warto poznać kolekcje przyjazne dla LINQ, takie jak List<T>
, Dictionary<TKey, TValue>
, i HashSet<T>
. Te kolekcje posiadają metody rozszerzeń LINQ, które ułatwiają operacje na danych.
Używanie własnych typów danych w kolekcjach
Możesz tworzyć kolekcje, które przechowują niestandardowe typy danych. Aby to zrobić, musisz zaimplementować interfejsy IEquatable<T>
i IComparable<T>
lub dostarczyć odpowiednie funkcje porównujące.
Optymalizacja wydajności
Przy pracy z dużymi danymi lub wymagającymi wydajności aplikacjami, warto poznać różne techniki optymalizacji kolekcji, takie jak wstępne alokacje, używanie kolekcji dostosowanych do konkretnej sytuacji, unikanie niepotrzebnych operacji kopiowania itp.
Przykłady zaawansowanych struktur danych
W języku C# istnieje wiele zaawansowanych struktur danych, takich jak drzewa, grafy, stosy, kolejki priorytetowe itp. Zrozumienie tych struktur i ich zastosowań może być kluczowe w bardziej zaawansowanych projektach.
Wybór odpowiedniej struktury danych i technik zależy od konkretnego przypadku użycia i wymagań projektu. Zrozumienie tych zaawansowanych koncepcji może pomóc w tworzeniu bardziej efektywnych i skalowalnych aplikacji.
Porady dotyczące wydajności
Wybór odpowiedniej kolekcji
// Dla częstych operacji dostępu po indeksie
List<T> lista; // O(1) dostęp po indeksie
// Dla częstych operacji wyszukiwania po kluczu
Dictionary<TKey, T>; // O(1) średni czas dostępu
// Dla zachowania kolejności i unikalności
LinkedHashSet<T>; // (nie jest dostępny w .NET, użyj OrderedDictionary)
// Dla operacji na zbiorach
HashSet<T>; // O(1) dodawanie, usuwanie, sprawdzanie
// Dla dużych zbiorów posortowanych danych
SortedDictionary<TKey, TValue>; // O(log n) operacje
Optymalizacje
// Określanie początkowej pojemności
List<int> lista = new List<int>(1000); // Unika realokacji
// Używanie StringBuilder dla string operations
StringBuilder sb = new StringBuilder();
// Unikanie niepotrzebnych alokacji
var wynik = lista.Where(x => x > 0).ToArray(); // Lepiej użyć foreach
// ❌ ŹLE - tworzy nową tablicę w pamięci
var wynikTablica = liczby.Where(x => x > 0).ToArray();
foreach (int liczba in wynikTablica)
{
Console.WriteLine(liczba);
}
// ✅ LEPSZE - nie tworzy dodatkowej kolekcji
foreach (int liczba in liczby.Where(x => x > 0))
{
Console.WriteLine(liczba);
}
// ✅ JESZCZE LEPSZE - klasyczna pętla, najwydajniejsza
foreach (int liczba in liczby)
{
if (liczba > 0)
{
Console.WriteLine(liczba);
}
}
// PRZYKŁAD Z WIĘKSZĄ RÓŻNICĄ WYDAJNOŚCI
// ❌ ŹLE - Multiple allocations
var wynik1 = liczby
.Where(x => x > 0)
.Select(x => x * 2)
.Where(x => x < 10)
.ToList(); // Alokuje nową listę
// ✅ LEPIEJ - Lazy evaluation, jedna iteracja
var wynik2 = liczby
.Where(x => x > 0)
.Select(x => x * 2)
.Where(x => x < 10); // Bez .ToList() - IEnumerable
foreach (var item in wynik2) // Dopiero tutaj następuje przetwarzanie
{
Console.WriteLine(item);
}
// ✅ NAJBARDZIEJ WYDAJNE - bez LINQ
var wynikOptymalne = new List<int>();
foreach (int liczba in liczby)
{
if (liczba > 0)
{
int podwojona = liczba * 2;
if (podwojona < 10)
{
wynikOptymalne.Add(podwojona);
}
}
}
Przykład praktyczny – Zarządzanie danymi studentów
public class Student
{
public string Imie { get; set; }
public string Nazwisko { get; set; }
public Dictionary<string, int> Oceny { get; set; } = new Dictionary<string, int>();
public double SredniaOcen => Oceny.Values.Average();
}
public class ZarzadzanieStudentami
{
private List<Student> studenci = new List<Student>();
public void DodajStudenta(Student student)
{
studenci.Add(student);
}
public IEnumerable<Student> ZnajdzNajlepszychStudentow(int liczba)
{
return studenci.OrderByDescending(s => s.SredniaOcen).Take(liczba);
}
public Dictionary<string, List<Student>> GrupujPoPrzedmiocie()
{
return studenci
.SelectMany(s => s.Oceny.Keys.Select(przedmiot => new { Student = s, Przedmiot = przedmiot }))
.GroupBy(x => x.Przedmiot)
.ToDictionary(g => g.Key, g => g.Select(x => x.Student).ToList());
}
}
Podsumowanie
Wybór odpowiedniej struktury danych ma kluczowe znaczenie dla wydajności i czytelności kodu. Oto ogólne wytyczne:
- Użyj
List<T>
dla dynamicznych list z częstym dostępem po indeksie - Użyj
Dictionary<TKey, TValue>
dla szybkiego dostępu po kluczu - Użyj
HashSet<T>
dla unikalnych elementów i operacji zbiorów - Użyj
Queue<T>/Stack<T>
dla struktur FIFO/LIFO - Użyj kolekcji niemutowalnych w programowaniu wielowątkowym
- Używaj LINQ do czytelnych operacji na danych
- Zawsze rozważ wydajność przy pracy z dużymi zbiorami danych
Zrozumienie tych koncepcji pozwoli na tworzenie efektywnych i skalowalnych aplikacji w C#.
C# Wprowadzenie Do Kolekcji
Chcesz wiecej... W tym kursie pokażę Ci, jak kodować za pomocą prawdopodobnie najbardziej przydatnych i najczęściej używanych kolekcji, takich jak: tablica, lista, stos, kolejka i słownik oraz, co ważne, kiedy używać każdej z tych kolekcji.
Świetny przegląd tablic i kolekcji w C#! Bardzo mi pomogło zrozumieć różnice między nimi. Teraz będę wiedział, kiedy używać tablic, a kiedy lepiej sięgnąć po kolekcje. Dzięki za klarowne wyjaśnienia!
Ciekawe, że wspomniałeś o kolekcjach niemutowalnych. Myślę, że to świetna praktyka, szczególnie w kontekście zachowywania integralności danych. Czy masz jakieś doświadczenia z ich użyciem w większych projektach?
LINQ to jedna z moich ulubionych funkcji w C#! Ułatwia pracę z danymi na wiele różnych sposobów. Tworzenie zapytań LINQ jest takie intuicyjne, a kod staje się znacznie bardziej czytelny.
Zastanawiam się, czy moglibyście podać więcej przykładów zastosowań kolekcji niemutowalnych w rzeczywistych scenariuszach. Jakie korzyści można uzyskać przy ich użyciu, zwłaszcza w kontekście niemutowalności?
Optymalizacja wydajności zawsze jest kluczowa, szczególnie w dużych projektach. Czy mógłbyś podać więcej informacji na temat konkretnych technik optymalizacyjnych związanych z kolekcjami w C#? Może jakieś doświadczenia praktyczne?