Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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!

📅 Timeline - ewolucja generics w C#
  • C# 2.0 (2005) - 🔥 Generics! List<T>, Dictionary<K,V>
  • C# 4.0 (2010) - Covariance i contravariance (in, out)
  • C# 7.3 (2018) - Unmanaged constraint, Enum constraint
  • 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
  • Generic methods - type inference, elastyczność
  • Generic interfaces - IRepository<T>, IConverter<T>
  • 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:

  1. Generic constraint: where T : class, ICacheable, new()
  2. Automatic expiration po 5 minutach
  3. Metoda GetOrCreate - jeśli brak w cache, utwórz przez new()
  4. Statystyki: hit rate, miss rate
🎯 BONUS: Generic Expression Evaluator

Stwórz generic expression evaluator z INumber<T> i static abstract members!

Do zaimplementowania:

  1. 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>
  2. Evaluator:
    • T Evaluate<T>(Expr<T> expr) where T : INumber<T>
    • Obsługa: +, -, *, /, negation, abs
    • Pattern matching na expression types
  3. Parser:
    • Expr<T> Parse<T>(string input) where T : INumber<T>
    • Parse expressions: "5 + 3", "(10 - 2) * 4"
  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! 🚀✨🧮