Opanowanie generyków w C#
Generyki w C# stały się integralną częścią języka od momentu ich wprowadzenia, oferując liczne korzyści, nawet jeśli ich działanie nie jest w pełni zrozumiałe przez wszystkich deweloperów. W swoim filmie "How To Create Generics in C#, Including New Features" Tim Corey wyjaśnia, dłączego generyki są ważne, jak je tworzyć oraz demonstruje ich praktyczne zastosowania.
Ten artykuł oferuje kompleksowy przewodnik po generykach w C#, zawierający cenne informacje z filmu Tima Coreya na ten temat. Omówiono tutaj podstawy generyków, w tym bezpieczeństwo typów, korzyści wydajnościowe oraz praktyczne zastosowania. Artykuł obejmuje również tworzenie metod generycznych, klas, interfejsów oraz stosowanie ograniczeń na generyki. Dodatkowo podkreśla rzeczywiste przypadki użycia oraz znaczenie stosowania generyków w celu pisania efektywnego, bezpiecznego pod względem typów kodu.
Wprowadzenie
Generyki w C# oferują potężny sposób na tworzenie elastycznego, wielokrotnego zastosowania i bezpiecznego pod względem typów kodu, pozwalając deweloperom definiować klasy, metody, interfejsy i kolekcje działające z dowolnym typem danych. Korzystając z klasy generycznej lub metody generycznej, deweloperzy mogą zdefiniować parametr typu generycznego (np. T), który może reprezentować dowolny typ danych. To eliminuje potrzebę duplikacji kodu i zwiększa jego ponowne wykorzystanie, zapewniając jednocześnie bezpieczeństwo typów w czasie kompilacji. Klasy kolekcji generycznych, takie jak List i Dictionary<TKey, TValue> umożliwiają efektywne zarządzanie różnymi typami danych, podczas gdy interfejsy generyczne i delegaty generyczne pozwalają na tworzenie niestandardowych typów generycznych, które mogą działać z wieloma parametrami typów. Wykorzystując generyki, deweloperzy mogą maksymalizować wydajność kodu i minimalizować wady klas niegenerycznych. Ta elastyczność umożliwia tworzenie bardziej wielokrotnego zastosowania kodu bez poświęcenia wydajności czy bezpieczeństwa typów.
Tim (0:00) wprowadza temat, podkreślając powszechne użycie generyków w C#. Jego celem jest wyjaśnienie, dłączego generyki są niezbędne oraz jak je tworzyć i efektywnie używać.
Tworzenie projektu
Tim zaczyna od stworzenia nowej aplikacji konsolowej o nazwie "GenericsDemoApp", aby skupić się wyłącznie na demonstrowaniu generyków bez rozpraszania uwagi przez interfejs użytkownika. Ustawia projekt korzystając z .NET 8 i Visual Studio 2022.
Podstawy Generyków
Tim (2:22) rozpoczyna od przykładu korzystając z klasy List, aby zilustrować koncepcję generyków. Generyki pozwalają określać typ elementów, które kolekcja może zawierać, zapewniając bezpieczeństwo typów i unikając błędów w czasie wykonania.
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
Klasa List zapewnia, że do listy numerów mogą być dodawane tylko liczby całkowite, a do listy napisów tylko napisy.
Bezpieczeństwo typów i wydajność
Tim podkreśla znaczenie bezpieczeństwa typów zapewnianego przez generyki. Kompiator sprawdza typy już na etapie projektowania, zapobiegając niezgodności typów i zapewniając bezpieczne wykonanie kodu. Generyki również eliminują potrzebę stosowania boxingu i unboxingu, co prowadzi do bardziej efektywnego kodu.
Nieskuteczność Kolekcji Niegenerycznych
Aby zademonstrować nieskuteczność stosowania kolekcji niegenerycznych, Tim tworzy Listę i pokazuje, jak może ona przechowywać różne typy obiektów.
List<object> objects = new List<object> { "Tim", 4, 3.6m };
List<object> objects = new List<object> { "Tim", 4, 3.6m };
Wyjaśnia, że stosowanie kolekcji niegenerycznych może prowadzić do niezgodności typów i nieskuteczności z powodu boxingu i unboxingu.
Porownanie wydajności
Tim (6:15) przeprowadza porównanie wydajności między korzystaniem z List
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
Wyniki pokazują, że dodawanie liczb całkowitych do List

Tworzenie Metody Sprawdzającej Typ
Tim (10:14) demonstruje, jak stworzyć metodę generyczną o nazwie TypeChecker. Ta metoda sprawdza typ podanej wartości i wypisuje go, pokazując elastyczność i moc generyków.
public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
Metoda TypeChecker używa operatora typeof do określenia typu parametru generycznego T i wypisuje zarówno typ, jak i wartość.
Korzystanie z Metody Sprawdzającej Typ
Tim prezentuje przykłady wywołania metody TypeChecker z różnymi typami argumentów.
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
Przekazując różne typy do metody TypeChecker, Tim pokazuje, jak generyki mogą bezproblemowo obsługiwać różne typy danych.
Tworzenie Klasy Generycznej: Better List
Tim (16:25) przechodzi do stworzenia klasy generycznej o nazwie BetterList. Ta klasa enkapsuluje listę określonego typu i zapewnia dodatkową funkcjonalność.
public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
Klasa BetterList zawiera prywatną List i metodę AddToList, która dodaje wartość do listy i wypisuje wiadomość wskazującą dodanie.
Korzystanie z Klasy Better List
Tim przedstawia przykłady korzystania z klasy BetterList z różnymi typami.
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
W tych przykładach, BetterList
Tworzenie Generycznego Interfejsu
Tim (21:48) wprowadza pomysł generycznego interfejsu o nazwie IImportance. Ten interfejs definiuje metodę do określenia, która z dwóch wartości jest ważniejsza.
public interface IImportance<t>
{
T MostImportant(T a, T b);
}
public interface IImportance<t>
{
T MostImportant(T a, T b);
}
Implementacja Generycznego Interfejsu
Tim pokazuje, jak zaimplementować ten interfejs dla różnych typów. Zaczyna od implementacji dla liczb całkowitych.
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
Następnie implementuje interfejs dla napisów, używając długości napisów do określenia ważności.
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
Te implementacje pokazują, jak ten sam interfejs może być zastosowany do różnych typów z określoną logiką dla każdego typu.
Stosowanie Ograniczeń na Generyki
Tim (25:21) wyjaśnia, jak stosować ograniczenia na generyki, zapewniając, że spełniają one określone warunki. Na przykład, typ generyczny może być ograniczony do posiadania konstruktora bezparametrowego lub implementacji określonego interfejsu.
public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}
public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}
Te ograniczenia pomagają zapewnić, że typ generyczny spełnia niezbędne kryteria, zapobiegając błędom w czasie wykonania i zwiększając bezpieczeństwo typów.
Implementacja INumber przez Microsoft
Tim omawia, jak Microsoft używa interfejsu INumber do ograniczania operacji numerycznych. Pozwala to na operacje arytmetyczne takie jak dodawanie i odejmowanie na typach generycznych.
public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}
public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}
Ograniczając typ generyczny T do INumber, zapewnia się, że typ obsługuje operacje numeryczne.
Korzystanie z Generyków z Różnymi Typami Numerycznymi
Tim (33:55) rozszerza klasę MathOperations, aby zademonstrować, jak generyki mogą być używane z różnymi typami numerycznymi, takimi jak double i decimal.
Tim pokazuje, jak tworzyć instancje MathOperations dla liczb całkowitych i double:
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
To demonstruje elastyczność generyków, pozwalając różnym typom numerycznym być obsługiwanym bezproblemowo w tej samej klasie.
Obsługa Różnych Typów Numerycznych
Tim podkreśla znaczenie bezpieczeństwa typów, pokazując, że nie można mieszać różnych typów numerycznych. Na przykład, próba dodania double do liczby całkowitej spowoduje błąd w czasie kompilacji.
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
Unikanie Kosztów Konwersji Typów
Tim wyjaśnia korzyści płynące z korzystania z generyków, by unikać kosztów związanych z konwersją typów. Na przykład, konwersja liczb całkowitych na double dla operacji matematycznych i z powrotem na liczby całkowite może być kosztowna. Korzystanie z generyków pozwala na bezpośrednie operacje na natywnych typach, zachowując wydajność i precyzję.
Generyki w Praktyce
Tim zaleca ostrożność przy korzystaniu z generyków, sugerując, aby deweloperzy używali ich odpowiednio i unikali nadmiernego stosowania. Podkreśla korzyści z generyków, takie jak bezpieczeństwo typów, redukcja boxingu i unboxingu, sprawdzanie w czasie kompilacji i zwiększona czytelność kodu.
Wskazuje również, że generyki są powszechnie stosowane w kolekcjach jak List i Dictionary<TKey, TValue>, a także w ramach logowania, które mogą obsługiwać różne typy bez potrzeby znajomości specyfiki z góry.
Wnioski
Szczegółowe omówienie zaawansowanych generyków w C# przez Tima Coreya dostarcza cennych spostrzeżeń na temat ich praktycznych zastosowań i korzyści. Jeśli chcesz pogłębić swoje rozumienie generyków i zobaczyć rzeczywiste przykłady w użyciu, koniecznie obejrzyj szczegółowy film Tima Coreya na temat Generyków w C#. Jego jasne wyjaśnienia i praktyczne demonstracje pomogą ci w pełni zrozumieć koncepcje i skutecznie je zastosować w swoich własnych projektach.
