Generics to jeden z najważniejszych ficzerów C# - pozwalają pisać kod który działa z wieloma typami bez code duplication. List<T>, Dictionary<K,V>, LINQ - wszystko oparte na generics!
W tym wpisie poznasz generic classes, methods, interfaces, constraints (where T : ...), covariance i contravariance (in/out), i najnowsze ficzery z C# 11 - generic math ze static abstract members!
C# 8.0 (2019) - Nullable reference types w generics
C# 11 (2022) - 🔥 Static abstract members, generic math
Generic Classes - podstawy
Problem bez generics
// Przed generics - musisz tworzyć osobne klasy dla każdego typu!
public class IntList
{
private int[] _items;
public void Add(int item) { /* ... */ }
public int Get(int index) { /* ... */ }
}
public class StringList
{
private string[] _items;
public void Add(string item) { /* ... */ }
public string Get(int index) { /* ... */ }
}
// Code duplication! 😱
// Albo używasz object (brak type safety):
public class ObjectList
{
private object[] _items;
public void Add(object item) { /* ... */ }
public object Get(int index) { /* ... */ }
}
ObjectList list = new ObjectList();
list.Add(5); // boxing!
list.Add("text"); // różne typy!
int x = (int)list.Get(0); // casting - może crashować!
Rozwiązanie - Generic Class!
// Generic class - JEDEN typ dla wszystkich!
public class MyList
{
private T[] _items;
private int _count;
public MyList()
{
_items = new T[4];
_count = 0;
}
public void Add(T item)
{
if (_count == _items.Length)
{
Array.Resize(ref _items, _items.Length * 2);
}
_items[_count++] = item;
}
public T Get(int index)
{
if (index >= _count)
throw new IndexOutOfRangeException();
return _items[index];
}
public int Count => _count;
}
// Użycie - type safe!
MyList numbers = new MyList();
numbers.Add(5);
numbers.Add(10);
int x = numbers.Get(0); // ✅ Nie trzeba castowania!
MyList names = new MyList();
names.Add("Jan");
// names.Add(5); // ❌ BŁĄD kompilacji - type safe!
// Jedna klasa, wiele typów! ✨
Multiple type parameters
// Generic class z 2 parametrami typu
public class Pair
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
}
// Użycie
var pair1 = new Pair("Age", 30);
var pair2 = new Pair(1, "First");
var pair3 = new Pair(DateTime.Now, true);
Console.WriteLine($"{pair1.First}: {pair1.Second}"); // Age: 30
// Dictionary to przykład z .NET!
Dictionary scores = new();
scores["Jan"] = 100;
Generic inheritance
// Generic base class
public class Repository
{
protected List _items = new();
public void Add(T item) => _items.Add(item);
public virtual IEnumerable GetAll() => _items;
}
// Derived class - może być generic lub concrete
public class UserRepository : Repository
{
// User jest fixed - nie generic
public override IEnumerable GetAll()
{
return _items.Where(u => u.IsActive);
}
}
// Lub derived może też być generic
public class CachedRepository : Repository
{
private Dictionary _cache = new();
public T? GetFromCache(int id)
{
return _cache.TryGetValue(id, out T? value) ? value : default;
}
}
Generic Methods - elastyczność
Generic methods w non-generic class
// Non-generic class z generic methods
public class Utility
{
// Generic method - type parameter na poziomie metody
public static void Swap(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static T GetFirst(T[] array)
{
if (array.Length == 0)
throw new InvalidOperationException();
return array[0];
}
public static T[] CreateArray(int size)
{
return new T[size];
}
}
// Użycie - type inference!
int x = 5, y = 10;
Utility.Swap(ref x, ref y); // Kompilator wywnioskuje
Console.WriteLine($"{x}, {y}"); // 10, 5
// Lub explicit
Utility.Swap(ref x, ref y);
string[] names = { "Jan", "Anna" };
string first = Utility.GetFirst(names); // Type inference
Generic methods w generic class
// Generic class z dodatkowymi generic methods
public class Container
{
private T _value;
public Container(T value)
{
_value = value;
}
// Generic method z INNYM type parameter niż klasa
public TResult Transform(Func transformer)
{
return transformer(_value);
}
// Generic method z 2 parametrami
public Pair PairWith(TOther other)
{
return new Pair(_value, other);
}
}
// Użycie
var container = new Container(42);
// Transform int -> string
string text = container.Transform(x => x.ToString()); // "42"
// Transform int -> bool
bool isEven = container.Transform(x => x % 2 == 0); // true
// PairWith
var paired = container.PairWith("answer"); // Pair
Console.WriteLine($"{paired.First}: {paired.Second}"); // 42: answer
Generic Interfaces
Generic interface definition
// Generic interface
public interface IRepository
{
void Add(T item);
T? GetById(int id);
IEnumerable GetAll();
void Delete(int id);
}
// Implementacja - concrete type
public class UserRepository : IRepository
{
private List _users = new();
public void Add(User item) => _users.Add(item);
public User? GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
public IEnumerable GetAll() => _users;
public void Delete(int id) => _users.RemoveAll(u => u.Id == id);
}
// Lub implementacja też generic
public class MemoryRepository : IRepository where T : IEntity
{
private List _items = new();
public void Add(T item) => _items.Add(item);
public T? GetById(int id) => _items.FirstOrDefault(i => i.Id == id);
public IEnumerable GetAll() => _items;
public void Delete(int id) => _items.RemoveAll(i => i.Id == id);
}
Multiple interface implementations
// Klasa może implementować ten sam generic interface z różnymi typami
public class MultiConverter : IConverter, IConverter
{
// IConverter
public string Convert(int input) => input.ToString();
// IConverter
public int Convert(string input) => int.Parse(input);
}
// Explicit interface implementation gdy są konflikty
public class ExplicitConverter : IConverter, IConverter
{
// Explicit implementation
string IConverter.Convert(int input) => $"Int: {input}";
string IConverter.Convert(double input) => $"Double: {input}";
}
// Użycie explicit
IConverter intConverter = new ExplicitConverter();
Console.WriteLine(intConverter.Convert(42)); // Int: 42
IConverter doubleConverter = new ExplicitConverter();
Console.WriteLine(doubleConverter.Convert(3.14)); // Double: 3.14
Type Constraints - where T : ...
Problem bez constraints
// Bez constraints - ograniczone możliwości
public class Calculator
{
public T Add(T a, T b)
{
// return a + b; // ❌ BŁĄD - kompilator nie wie czy T ma operator+
// Nie możesz użyć operatorów, metod, properties na T!
return default;
}
}
where T : class - reference types
// where T : class - T MUSI być reference type
public class ReferenceContainer where T : class
{
private T? _value; // może być null
public void Set(T value)
{
_value = value ?? throw new ArgumentNullException();
}
public T? Get() => _value;
}
// Użycie
var container = new ReferenceContainer(); // ✅ OK - string jest class
container.Set("Hello");
// var invalid = new ReferenceContainer(); // ❌ BŁĄD - int jest struct!
where T : struct - value types
// where T : struct - T MUSI być value type (struct, enum, numeric)
public class ValueContainer where T : struct
{
private T _value; // NIE może być null (bez ?)
public ValueContainer(T value)
{
_value = value;
}
public T Get() => _value;
}
// Użycie
var intContainer = new ValueContainer(42); // ✅ OK
var dateContainer = new ValueContainer(DateTime.Now); // ✅ OK
// var invalid = new ValueContainer("text"); // ❌ BŁĄD - string jest class!
where T : new() - wymaga default constructor
// where T : new() - T MUSI mieć parameterless constructor
public class Factory where T : new()
{
public T Create()
{
return new T(); // ✅ Możesz użyć new()
}
public List CreateMany(int count)
{
var list = new List();
for (int i = 0; i < count; i++)
{
list.Add(new T());
}
return list;
}
}
public class Person
{
public string Name { get; set; } = "Unknown";
// Parameterless constructor (automatyczny)
}
// Użycie
var factory = new Factory();
Person p = factory.Create(); // ✅ OK - Person ma default constructor
var people = factory.CreateMany(10); // Lista 10 nowych Person
where T : BaseClass - dziedziczenie
// where T : BaseClass - T MUSI dziedziczyć po BaseClass
public abstract class Animal
{
public abstract string MakeSound();
}
public class Dog : Animal
{
public override string MakeSound() => "Woof!";
}
public class Cat : Animal
{
public override string MakeSound() => "Meow!";
}
// Generic class z base class constraint
public class Zoo where T : Animal
{
private List _animals = new();
public void Add(T animal) => _animals.Add(animal);
public void MakeAllSounds()
{
foreach (var animal in _animals)
{
Console.WriteLine(animal.MakeSound()); // ✅ MakeSound() jest dostępne!
}
}
}
// Użycie
var dogZoo = new Zoo();
dogZoo.Add(new Dog());
dogZoo.MakeAllSounds(); // Woof!
var catZoo = new Zoo();
catZoo.Add(new Cat());
catZoo.MakeAllSounds(); // Meow!
// var invalid = new Zoo(); // ❌ BŁĄD - string nie dziedziczy po Animal!
where T : IInterface - interface constraint
// where T : IInterface - T MUSI implementować interface
public interface IComparable
{
int CompareTo(T other);
}
public class Sorter where T : IComparable
{
public List Sort(List items)
{
var sorted = new List(items);
// Bubble sort (dla przykładu)
for (int i = 0; i < sorted.Count; i++)
{
for (int j = 0; j < sorted.Count - 1; j++)
{
if (sorted[j].CompareTo(sorted[j + 1]) > 0) // ✅ CompareTo dostępne!
{
(sorted[j], sorted[j + 1]) = (sorted[j + 1], sorted[j]);
}
}
}
return sorted;
}
}
// int implementuje IComparable
var sorter = new Sorter();
var sorted = sorter.Sort(new List { 5, 2, 8, 1, 9 });
// [1, 2, 5, 8, 9]
Multiple constraints
// Możesz łączyć wiele constraints!
public class Repository
where T : class, IEntity, new()
{
// T musi być:
// 1. Reference type (class)
// 2. Implementować IEntity
// 3. Mieć parameterless constructor
private List _items = new();
public void Add(T item)
{
if (item.Id == 0) // ✅ Id z IEntity
{
item.Id = GenerateId();
}
_items.Add(item);
}
public T Create()
{
return new T(); // ✅ new() constraint
}
}
public interface IEntity
{
int Id { get; set; }
}
public class User : IEntity // ✅ class + IEntity + default constructor
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
var repo = new Repository(); // ✅ OK
C# 7.3+ - dodatkowe constraints
// C# 7.3 - unmanaged constraint (value types bez references)
public class UnsafeBuffer where T : unmanaged
{
// T musi być unmanaged (int, double, struct bez references)
private T[] _buffer;
public unsafe void CopyTo(void* destination)
{
// Można użyć unsafe code z unmanaged types
}
}
var buffer1 = new UnsafeBuffer(); // ✅ OK
var buffer2 = new UnsafeBuffer(); // ✅ OK
// var buffer3 = new UnsafeBuffer(); // ❌ BŁĄD - string ma references
// C# 7.3 - Enum constraint
public class EnumHelper where T : Enum
{
public static T[] GetValues()
{
return (T[])Enum.GetValues(typeof(T));
}
public static T Parse(string value)
{
return (T)Enum.Parse(typeof(T), value);
}
}
enum Status { Active, Inactive, Pending }
var values = EnumHelper.GetValues();
var status = EnumHelper.Parse("Active");
Constraint
Znaczenie
Przykład
where T : class
Reference type
string, List<int>, custom classes
where T : struct
Value type (nie nullable)
int, DateTime, custom structs
where T : new()
Ma parameterless constructor
Możesz użyć new T()
where T : BaseClass
Dziedziczy po BaseClass
Animal, Vehicle, etc.
where T : IInterface
Implementuje IInterface
IComparable, IDisposable, etc.
where T : unmanaged
Unmanaged value type
int, float, Point struct
where T : Enum
Enum type
Status, Color, etc.
where T : notnull
Non-nullable type
int, string (bez ?)
🔥 Generic Math ze Static Abstract Members (C# 11)
Przypomnienie - INumber<T>
// C# 11 - generic math (poznane w poprzednim wpisie)
using System.Numerics;
// Generic math z INumber constraint
public static T Sum(T[] numbers) where T : INumber
{
T total = T.Zero; // Static property
foreach (var num in numbers)
{
total += num; // operator+
}
return total;
}
// Działa dla wszystkich numeric types!
var intSum = Sum(new[] { 1, 2, 3, 4, 5 }); // 15
var doubleSum = Sum(new[] { 1.5, 2.5, 3.5 }); // 7.5
var decimalSum = Sum(new[] { 1m, 2m, 3m }); // 6
Własny typ z static abstract members
// Własny interface ze static abstract members
public interface IMultipliable where TSelf : IMultipliable
{
static abstract TSelf operator *(TSelf left, TSelf right);
static abstract TSelf One { get; }
}
// Implementacja - Matrix
public record struct Matrix2x2(double A, double B, double C, double D)
: IMultipliable
{
// Static abstract operator
public static Matrix2x2 operator *(Matrix2x2 left, Matrix2x2 right)
{
return new Matrix2x2(
left.A * right.A + left.B * right.C,
left.A * right.B + left.B * right.D,
left.C * right.A + left.D * right.C,
left.C * right.B + left.D * right.D
);
}
// Static abstract property
public static Matrix2x2 One => new Matrix2x2(1, 0, 0, 1);
}
// Generic power używa static abstract members!
public static T Power(T value, int exponent) where T : IMultipliable
{
if (exponent == 0)
return T.One;
T result = value;
for (int i = 1; i < exponent; i++)
{
result = result * value; // operator*
}
return result;
}
// Użycie
var matrix = new Matrix2x2(2, 0, 0, 2);
var squared = Power(matrix, 2);
Console.WriteLine(squared); // Matrix2x2 { A = 4, B = 0, C = 0, D = 4 }
💡 Kompletny przykład - Vector math z generic constraints
using System.Numerics;
// Vector interface ze static abstract members
public interface IVector
where TSelf : IVector
where TScalar : INumber
{
static abstract TSelf operator +(TSelf left, TSelf right);
static abstract TSelf operator -(TSelf left, TSelf right);
static abstract TSelf operator *(TSelf vector, TScalar scalar);
static abstract TScalar Dot(TSelf left, TSelf right);
static abstract TSelf Zero { get; }
TScalar Magnitude { get; }
}
// Vector2 implementation
public record struct Vector2(T X, T Y) : IVector, T>
where T : INumber, IFloatingPoint
{
public static Vector2 operator +(Vector2 left, Vector2 right) =>
new(left.X + right.X, left.Y + right.Y);
public static Vector2 operator -(Vector2 left, Vector2 right) =>
new(left.X - right.X, left.Y - right.Y);
public static Vector2 operator *(Vector2 vector, T scalar) =>
new(vector.X * scalar, vector.Y * scalar);
public static T Dot(Vector2 left, Vector2 right) =>
left.X * right.X + left.Y * right.Y;
public static Vector2 Zero => new(T.Zero, T.Zero);
public T Magnitude => T.Sqrt(X * X + Y * Y);
}
// Generic Vector operations
public static class VectorOps
{
public static TVector Lerp(TVector start, TVector end, TScalar t)
where TVector : IVector
where TScalar : INumber
{
var one = TScalar.One;
return start * (one - t) + end * t;
}
}
// Użycie
var v1 = new Vector2(3, 4);
var v2 = new Vector2(6, 8);
var sum = v1 + v2; // (9, 12)
var diff = v2 - v1; // (3, 4)
var scaled = v1 * 2.0; // (6, 8)
var dot = Vector2.Dot(v1, v2); // 50
var mag = v1.Magnitude; // 5.0
var lerped = VectorOps.Lerp(v1, v2, 0.5); // (4.5, 6)
Covariance i Contravariance - in/out
Problem - variance w generics
// Podstawy hierarchii
class Animal { }
class Dog : Animal { }
// W C# możesz:
Animal animal = new Dog(); // ✅ OK - Dog jest Animal
// Ale z generics:
List animals = new List(); // ❌ BŁĄD!
// List NIE jest List - brak variance!
// Dlaczego? Bo byłoby niebezpieczne:
// List animals = new List();
// animals.Add(new Cat()); // 💥 Dodałbyś Cat do List!
Covariance - out (C# 4+)
🎉 C# 4 - Covariance z out
out T w interfejsie = covariance - T jest tylko OUTPUT (zwracane), nie INPUT
// Covariant interface - out T
public interface IReadOnlyRepository
{
T GetById(int id); // ✅ OK - T jest OUTPUT (return)
IEnumerable GetAll(); // ✅ OK - T w IEnumerable (też out)
// void Add(T item); // ❌ BŁĄD - T byłoby INPUT!
}
// Hierarchia
class Animal { public string Name { get; set; } }
class Dog : Animal { public string Breed { get; set; } }
// Implementacje
class AnimalRepository : IReadOnlyRepository
{
public Animal GetById(int id) => new Animal { Name = "Some animal" };
public IEnumerable GetAll() => new List();
}
class DogRepository : IReadOnlyRepository
{
public Dog GetById(int id) => new Dog { Name = "Rex", Breed = "Labrador" };
public IEnumerable GetAll() => new List();
}
// Covariance w akcji!
IReadOnlyRepository repo = new DogRepository(); // ✅ OK! Covariance!
// Dog IS-A Animal, więc IReadOnlyRepository IS-A IReadOnlyRepository
Animal animal = repo.GetById(1); // Zwraca Dog (jako Animal) - bezpieczne! ✅
Contravariance - in (C# 4+)
🎉 C# 4 - Contravariance z in
in T w interfejsie = contravariance - T jest tylko INPUT (parametry), nie OUTPUT
// Contravariant interface - in T
public interface IComparer
{
int Compare(T x, T y); // ✅ OK - T jest INPUT (parametry)
// T GetFirst(); // ❌ BŁĄD - T byłoby OUTPUT!
}
// Hierarchia
class Animal { public int Age { get; set; } }
class Dog : Animal { public string Breed { get; set; } }
// Comparer dla Animal
class AnimalComparer : IComparer
{
public int Compare(Animal x, Animal y)
{
return x.Age.CompareTo(y.Age);
}
}
// Contravariance w akcji!
IComparer animalComparer = new AnimalComparer();
IComparer dogComparer = animalComparer; // ✅ OK! Contravariance!
// Może porównywać Dog używając Animal comparer
Dog dog1 = new Dog { Age = 5 };
Dog dog2 = new Dog { Age = 3 };
int result = dogComparer.Compare(dog1, dog2); // Bezpieczne - Dog IS-A Animal
out vs in - kiedy czego używać?
Feature
Covariance (out T)
Contravariance (in T)
Keyword
out T
in T
T w pozycji
OUTPUT (return types)
INPUT (parameters)
Konwersja
Derived → Base
Base → Derived
Przykład
IEnumerable<out T>
IComparer<in T>
Use case
Read-only repositories, producers
Comparers, consumers
Bezpieczeństwo
✅ Type safe
✅ Type safe
🔍 Jak zapamiętać out/in?
out - data wychodzi OUT z interfejsu (return values) → Covariance
in - data wchodzi IN do interfejsu (parameters) → Contravariance
Przykłady z .NET
// IEnumerable<out T> - covariant
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings; // ✅ OK - covariance!
// IEnumerable tylko zwraca T (out), nie przyjmuje
foreach (object obj in objects)
{
Console.WriteLine(obj);
}
// Func<out TResult> - covariant w TResult
Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog; // ✅ OK - covariance!
Animal animal = getAnimal(); // Zwraca Dog (jako Animal)
// Action<in T> - contravariant w T
Action<Animal> processAnimal = (animal) => Console.WriteLine(animal.Name);
Action<Dog> processDog = processAnimal; // ✅ OK - contravariance!
processDog(new Dog { Name = "Rex" }); // Bezpieczne
// Func<in T, out TResult> - contravariant w T, covariant w TResult
Func<Animal, string> animalToString = (animal) => animal.Name;
Func<Dog, object> dogToObject = animalToString; // ✅ OK - oba!
Podsumowanie
Generics i constraints - potężne narzędzie C#!
✅ Generic classes - List<T>, Dictionary<K,V>, własne typy
✅ Type constraints - where T : class, struct, new(), BaseClass, IInterface
✅ Multiple constraints - łączenie ograniczeń
✅ C# 7.3+ constraints - unmanaged, Enum, notnull
✅ 🔥 Generic math (C# 11) - INumber<T>, static abstract members
✅ Covariance (out) - T tylko OUTPUT, IEnumerable<out T>
✅ Contravariance (in) - T tylko INPUT, IComparer<in T>
W kolejnym wpisie poznasz Wyjątki i error handling - try/catch/finally, throw expressions, exception filters!
Zadanie dla Ciebie 🎯
Stwórz generic cache system z constraints:
// Interface dla cacheable entities
public interface ICacheable
{
string CacheKey { get; }
DateTime LastModified { get; }
}
// Generic cache - do zaimplementowania
public class Cache where T : class, ICacheable, new()
{
// Dictionary do przechowywania
// Metody: Add, Get, Remove, Clear
// Expiration po 5 minutach
}
// Test entities
public class User : ICacheable
{
public int Id { get; set; }
public string Name { get; set; }
public string CacheKey => $"user_{Id}";
public DateTime LastModified { get; set; }
}
Wymagania:
Generic constraint: where T : class, ICacheable, new()
Automatic expiration po 5 minutach
Metoda GetOrCreate - jeśli brak w cache, utwórz przez new()
Statystyki: hit rate, miss rate
🎯 BONUS: Generic Expression Evaluator
Stwórz generic expression evaluator z INumber<T> i static abstract members!
Do zaimplementowania:
Expression tree hierarchy:
abstract record Expr<T>
record Constant<T>(T Value) : Expr<T>
record BinaryOp<T>(Expr<T> Left, string Op, Expr<T> Right) : Expr<T>
record UnaryOp<T>(string Op, Expr<T> Operand) : Expr<T>
Evaluator:
T Evaluate<T>(Expr<T> expr) where T : INumber<T>
Obsługa: +, -, *, /, negation, abs
Pattern matching na expression types
Parser:
Expr<T> Parse<T>(string input) where T : INumber<T>
Parse expressions: "5 + 3", "(10 - 2) * 4"
Optimizer:
Constant folding: "(5 + 3) * 2" → "16"
Identity elimination: "x * 1" → "x"
Przykład użycia:
// Build expression: (5 + 3) * 2
var expr = new BinaryOp(
new BinaryOp(
new Constant(5),
"+",
new Constant(3)
),
"*",
new Constant(2)
);
var result = Evaluate(expr); // 16
// Parse from string
var parsed = Parse("(10.5 + 2.5) * 4");
var doubleResult = Evaluate(parsed); // 52.0
// Optimizer
var optimized = Optimize(expr); // Constant(16)
// Działa z dowolnym INumber!
var decimalExpr = Parse("100 + 50");
var decimalResult = Evaluate(decimalExpr); // 150m
Ten projekt łączy generics, constraints, INumber<T>, records, pattern matching! Prawdziwy compiler basics! 🚀✨🧮