Przejdź do treści stopki
Iron Academy Logo
Naucz się C#
Naucz się C#

Inne Kategorie

Opanowanie generyków w C#

Tim Corey
38m 51s

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 i List, aby dodać milion elementów. Używa stopera, aby zmierzyć czas wykonania każdej operacji.

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 jest znacznie szybsze niż do List z powodu braku boxingu i unboxingu.

Porównanie Wydajności

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 zarządza listą liczb całkowitych, podczas gdy BetterList zarządza listą obiektów PersonRecord. Każde dodanie do listy powoduje wyświetlenie komunikatu konsolowego wskazującego dodaną wartość.

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.

Hero Worlddot related to Opanowanie generyków w C#
Hero Affiliate related to Opanowanie generyków w C#

Zarabiaj więcej, dzieląc się tym, co kochasz

Tworzysz treści dla deweloperów pracujących z .NET, C#, Java, Python, czy Node.js? Zamień swoją wiedzę specjalistyczną na dodatkowy dochód!

Zespol wsparcia Iron

Jestesmy online 24 godziny, 5 dni w tygodniu.
Czat
Email
Zadzwon do mnie