Уявіть, що вам потрібно створити стек (stack) для зберігання цілих чисел:
public class IntStack
{
private int[] _items = new int[100];
private int _count = 0;
public void Push(int item)
{
_items[_count++] = item;
}
public int Pop()
{
return _items[--_count];
}
}
Чудово! Але що, якщо вам потрібен стек для рядків? Доведеться створити ще один клас:
public class StringStack
{
private string[] _items = new string[100];
private int _count = 0;
public void Push(string item)
{
_items[_count++] = item;
}
public string Pop()
{
return _items[--_count];
}
}
А якщо потрібен стек для Customer, Order, Product? Дублювання коду!
До появи generics у .NET 2.0, розробники використовували object:
public class ObjectStack
{
private object[] _items = new object[100];
private int _count = 0;
public void Push(object item)
{
_items[_count++] = item;
}
public object Pop()
{
return _items[--_count];
}
}
// Використання
ObjectStack stack = new ObjectStack();
stack.Push(42);
int value = (int)stack.Pop(); // ❌ Необхідний cast
stack.Push("Hello");
int broken = (int)stack.Pop(); // ❌ Runtime помилка!
Проблеми підходу з object:
Generics вирішують ці проблеми:
public class Stack<T>
{
private T[] _items = new T[100];
private int _count = 0;
public void Push(T item)
{
_items[_count++] = item;
}
public T Pop()
{
return _items[--_count];
}
}
// Використання
Stack<int> intStack = new Stack<int>();
intStack.Push(42);
int value = intStack.Pop(); // ✅ Без cast
Stack<string> stringStack = new Stack<string>();
stringStack.Push("Hello");
// stringStack.Push(42); // ❌ Compile-time помилка!
Переваги Generics:
Type Parameter (параметр типу) — це placeholder у визначенні generic типу:
public class Box<T> // T — це Type Parameter
{
public T Value { get; set; }
}
Type Argument (аргумент типу) — це конкретний тип, яким замінюється параметр:
Box<int> intBox = new Box<int>(); // int — це Type Argument
Box<string> strBox = new Box<string>(); // string — це Type Argument
List<T>)List<int>)T (наприклад, List<T>, Queue<T>)TTKey, TValue (Dictionary<TKey, TValue>)TInput, TOutput (Converter<TInput, TOutput>)TResult (Func<T, TResult>)public class Repository<T>
{
private readonly List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public T GetById(int id)
{
return _items[id];
}
public IEnumerable<T> GetAll()
{
return _items;
}
public void Remove(T item)
{
_items.Remove(item);
}
}
Використання:
// Repository для різних типів
Repository<Customer> customers = new Repository<Customer>();
customers.Add(new Customer { Id = 1, Name = "Alice" });
Repository<Product> products = new Repository<Product>();
products.Add(new Product { Id = 1, Name = "Laptop" });
public class KeyValueStore<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> _storage = new();
public void Set(TKey key, TValue value)
{
_storage[key] = value;
}
public TValue Get(TKey key)
{
return _storage[key];
}
public bool TryGet(TKey key, out TValue value)
{
return _storage.TryGetValue(key, out value);
}
}
Використання:
var cache = new KeyValueStore<string, Customer>();
cache.Set("customer_1", new Customer { Name = "Bob" });
Customer customer = cache.Get("customer_1");
public class Result<T>
{
// Generic property
public T Data { get; set; }
public bool IsSuccess { get; set; }
public string ErrorMessage { get; set; }
// Generic field
private T _cachedValue;
public T GetOrCache()
{
if (_cachedValue == null)
{
_cachedValue = Data;
}
return _cachedValue;
}
}
public class Counter<T>
{
private static int _count = 0;
public static void Increment()
{
_count++;
}
public static int GetCount()
{
return _count;
}
}
// Використання
Counter<int>.Increment();
Counter<int>.Increment();
Console.WriteLine(Counter<int>.GetCount()); // 2
Counter<string>.Increment();
Console.WriteLine(Counter<string>.GetCount()); // 1 (окремий лічильник!)
Generic методи можуть бути у будь-якому класі (generic або non-generic):
public class Utility
{
// Generic метод у non-generic класі
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static T[] CreateArray<T>(int size, T defaultValue)
{
T[] array = new T[size];
for (int i = 0; i < size; i++)
{
array[i] = defaultValue;
}
return array;
}
}
Використання:
int x = 5, y = 10;
Utility.Swap<int>(ref x, ref y); // Явне вказання типу
Console.WriteLine($"x={x}, y={y}"); // x=10, y=5
string a = "Hello", b = "World";
Utility.Swap(ref a, ref b); // ✅ Type inference!
Console.WriteLine($"a={a}, b={b}"); // a=World, b=Hello
Компілятор C# часто може автоматично визначити type arguments:
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// Явне вказання типу
int result1 = Max<int>(5, 10);
// Type inference — компілятор визначає тип з аргументів
int result2 = Max(5, 10); // ✅ T виводиться як int
string result3 = Max("abc", "xyz"); // ✅ T виводиться як string
Теорія Type Inference:
Type inference (виведення типів) — це механізм, за допомогою якого компілятор C# автоматично визначає тип параметрів типу (type parameters) на основі типів аргументів, переданих у виклик методу. Це робить код більш читабельним і зменшує необхідність явного вказання типів.
Алгоритм виведення типів:
Приклад складного виведення типів:
public static TResult Transform<TSource, TResult>(
TSource source,
Func<TSource, TResult> transformer)
where TSource : class
where TResult : class
{
return transformer(source);
}
// Компілятор виводить TSource як string, TResult як int
string input = "123";
int result = Transform(input, s => int.Parse(s)); // ✅ Виведення: Transform<string, int>
Обмеження type inference:
T як out або refpublic static class EnumerableExtensions
{
public static IEnumerable<T> WhereNot<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
{
return source.Where(x => !predicate(x));
}
public static T Second<T>(this IEnumerable<T> source)
{
return source.Skip(1).First();
}
}
// Використання
var numbers = new[] { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.WhereNot(n => n % 2 != 0); // Непарні виключені
int secondNumber = numbers.Second(); // 2
Constraints дозволяють обмежити типи, які можна використовувати як type arguments.
where T : class — Reference Typepublic class ReferenceRepository<T> where T : class
{
private T _cachedItem;
public void Cache(T item)
{
_cachedItem = item; // ✅ Можна зберігати null
}
public bool IsCached()
{
return _cachedItem != null; // ✅ Перевірка на null
}
}
// Використання
var repo = new ReferenceRepository<string>(); // ✅ OK
var repo2 = new ReferenceRepository<Customer>(); // ✅ OK
// var repo3 = new ReferenceRepository<int>(); // ❌ int — value type
Теорія обмеження class:
Обмеження where T : class вказує компілятору, що параметр типу T повинен бути посилальним типом (class, interface, delegate, або null). Це дозволяє:
null змінним типу TnullToString(), Equals(), GetHashCode())Внутрішні механізми обмеження class:
Під час компіляції, коли використовується обмеження where T : class, компілятор гарантує, що будь-який тип, який буде використаний як аргумент типу, є посилальним типом. Це забезпечує безпечне використання операцій, специфічних для посилальних типів, таких як перевірка на null.
Під час виконання, для посилальних типів, що відповідають цьому обмеженню, виконуються стандартні операції з посиланнями, без додаткових перевірок, оскільки компілятор вже перевірив сумісність типів.
where T : class? — Nullable Reference Typepublic class NullableRepository<T> where T : class?
{
public T? FindOrDefault(int id)
{
// Може повернути null
return default(T);
}
}
where T : struct — Value Typepublic struct Point<T> where T : struct
{
public T X { get; set; }
public T Y { get; set; }
public Point(T x, T y)
{
X = x;
Y = y;
}
}
// Використання
var intPoint = new Point<int>(10, 20); // ✅ OK
var doublePoint = new Point<double>(1.5, 2.5); // ✅ OK
// var stringPoint = new Point<string>("", ""); // ❌ string — reference type
Теорія обмеження struct:
Обмеження where T : struct вказує компілятору, що параметр типу T повинен бути типом значення (value type), тобто типом, що успадковується від System.ValueType. Це включає:
Це обмеження забороняє використання посилальних типів як аргументів типу.
Внутрішні механізми обмеження struct:
Під час компіляції, коли використовується обмеження where T : struct, компілятор гарантує, що будь-який тип, який буде використаний як аргумент типу, є типом значення. Це дозволяє:
where T : notnull — Non-Nullable TypeЗаборон використання nullable types:
public class NonNullProcessor<T> where T : notnull
{
public void Process(T item)
{
// item гарантовано не null
Console.WriteLine(item.ToString());
}
}
// Використання
var processor1 = new NonNullProcessor<int>(); // ✅ OK
var processor2 = new NonNullProcessor<string>(); // ✅ OK (якщо NRT enabled)
// var processor3 = new NonNullProcessor<string?>(); // ❌ nullable
where T : unmanaged — Unmanaged TypeТип, який не містить reference types (primitives, structs без reference fields):
public unsafe class UnsafeBuffer<T> where T : unmanaged
{
private T* _buffer;
private int _size;
public UnsafeBuffer(int size)
{
_size = size;
_buffer = (T*)Marshal.AllocHGlobal(size * sizeof(T));
}
public T this[int index]
{
get => _buffer[index];
set => _buffer[index] = value;
}
}
// Використання
var buffer1 = new UnsafeBuffer<int>(10); // ✅ OK
var buffer2 = new UnsafeBuffer<double>(10); // ✅ OK
// var buffer3 = new UnsafeBuffer<string>(10); // ❌ string не unmanaged
where T : new() — Parameterless Constructorpublic class Factory<T> where T : new()
{
public T Create()
{
return new T(); // ✅ Можемо створювати екземпляри
}
public List<T> CreateMany(int count)
{
var list = new List<T>();
for (int i = 0; i < count; i++)
{
list.Add(new T());
}
return list;
}
}
public class Customer
{
public Customer() { } // ✅ Parameterless constructor
}
// Використання
var factory = new Factory<Customer>();
Customer customer = factory.Create(); // ✅ OK
Теорія обмеження new():
Обмеження where T : new() вимагає, щоб тип, використаний як аргумент типу, мав публічний конструктор без параметрів. Це дозволяє створювати екземпляри типу T за допомогою оператора new.
Внутрішні механізми обмеження new():
Під час компіляції, коли використовується обмеження where T : new(), компілятор перевіряє, що будь-який тип, який буде використаний як аргумент типу, має публічний конструктор без параметрів. Під час виконання, виклик new T() перетворюється компілятором у виклик відповідного конструктора типу, який був переданий як аргумент типу.
Це обмеження особливо корисне при створенні фабричних класів, генераторів об'єктів або коли потрібно створювати екземпляри типу динамічно.
public abstract class Entity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
public class EntityRepository<T> where T : Entity
{
public void Save(T entity)
{
entity.CreatedAt = DateTime.Now; // ✅ Доступ до властивостей Entity
// Логіка збереження
}
public T GetById(int id)
{
// Логіка отримання
return default(T);
}
}
public class Customer : Entity
{
public string Name { get; set; }
}
// Використання
var repo = new EntityRepository<Customer>(); // ✅ Customer : Entity
Теорія Base Class Constraint:
Обмеження базового класу where T : BaseClass вказує, що параметр типу T повинен бути або класом, що безпосередньо успадковується від BaseClass, або самим BaseClass. Це дозволяє:
Внутрішні механізми Base Class Constraint:
Під час компіляції, коли використовується обмеження базового класу, компілятор перевіряє, що будь-який тип, який буде використаний як аргумент типу, є похідним від вказаного базового класу. Це забезпечує безпечне використання членів базового класу в коді, що працює з параметром типу T.
Під час виконання, об'єкти типу T можуть бути безпечно приведені до типу базового класу, що дозволяє використовувати поліморфізм і наслідування.
public class Sorter<T> where T : IComparable<T>
{
public void BubbleSort(List<T> items)
{
for (int i = 0; i < items.Count - 1; i++)
{
for (int j = 0; j < items.Count - i - 1; j++)
{
if (items[j].CompareTo(items[j + 1]) > 0) // ✅ CompareTo доступний
{
T temp = items[j];
items[j] = items[j + 1];
items[j + 1] = temp;
}
}
}
}
}
Multiple Interface Constraints:
public class AdvancedProcessor<T>
where T : IComparable<T>, IEquatable<T>, IDisposable
{
public void Process(T item)
{
// Доступні методи з усіх трьох інтерфейсів
item.CompareTo(default(T));
item.Equals(default(T));
item.Dispose();
}
}
Один type parameter може бути constraint для іншого:
public class Mapper<TSource, TDestination>
where TDestination : TSource
{
public TDestination Convert(TSource source)
{
// TDestination гарантовано успадковується від TSource
return (TDestination)source;
}
}
public class Animal { }
public class Dog : Animal { }
// Використання
var mapper = new Mapper<Animal, Dog>();
Dog dog = mapper.Convert(new Dog()); // ✅ OK
where T : allows ref structНова можливість C# 13 — дозволяє використовувати ref struct типи як generic arguments:
public class DataProcessor<T> where T : allows ref struct
{
public void Process(scoped T data)
{
// T може бути ref struct (наприклад, Span<T>)
// Параметр scoped гарантує ref safety
}
}
// Використання з ref struct
public ref struct MyRefType
{
public int Value;
}
var processor = new DataProcessor<MyRefType>();
var data = new MyRefType { Value = 42 };
processor.Process(data); // ✅ OK з C# 13
ref struct типи (як Span<T>, ReadOnlySpan<T>) як generic type arguments. Тепер з allows ref struct це можливо!public class AdvancedRepository<T>
where T : Entity, // Base class
IValidatable, // Interface 1
INotifyPropertyChanged, // Interface 2
new() // Constructor (завжди останній!)
{
public void Add(T item)
{
// Доступ до всіх властивостей та методів
item.Id = GenerateId();
if (!item.Validate())
{
throw new InvalidOperationException("Invalid entity");
}
// Збереження...
}
}
class, struct, base class)new() завжди останній!| Constraint | Опис | Приклад | Додаткові нюанси |
|---|---|---|---|
class | Reference type | where T : class | Дозволяє присвоювати null, перевіряти на null, використовувати члени System.Object |
class? | Nullable reference type | where T : class? | Вимагає ввімкнення Nullable Reference Types (NRT) |
struct | Value type | where T : struct | Уникає boxing/unboxing, гарантує копіювання при передачі |
notnull | Non-nullable type | where T : notnull | Забороняє використання nullable типів, гарантує відсутність null |
unmanaged | Unmanaged type | where T : unmanaged | Дозволяє безпечну роботу з пам'яттю, не містить посилань |
new() | Parameterless constructor | where T : new() | Дозволяє використання new T() для створення екземплярів |
| Base class | Успадковується від класу | where T : Entity | Надає доступ до членів базового класу, забезпечує певну структуру |
| Interface | Реалізує інтерфейс | where T : IComparable<T> | Надає доступ до методів інтерфейсу, забезпечує певну поведінку |
| Type parameter | Type constraint | where TDest : TSource | Встановлює відношення між параметрами типу |
allows ref struct | Дозволяє ref struct (C# 13) | where T : allows ref struct | Дозволяє використання ref struct типів як аргументів типу |
Variance (варіантність) описує, як можна замінювати generic типи в hierarchy успадкування.
Припустимо, у нас є така hierarchy:
public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
public void Bark() => Console.WriteLine("Woof!");
}
public class Cat : Animal
{
public void Meow() => Console.WriteLine("Meow!");
}
out keyword)Covariance дозволяє використовувати більш конкретний (derived) тип замість загального (base).
// IEnumerable<T> є covariant (має out modifier)
IEnumerable<Dog> dogs = new List<Dog>
{
new Dog { Name = "Rex" },
new Dog { Name = "Buddy" }
};
// ✅ Covariance: Dog → Animal
IEnumerable<Animal> animals = dogs; // Працює!
foreach (Animal animal in animals)
{
Console.WriteLine(animal.Name); // Rex, Buddy
}
Чому це працює?
Dog є AnimalIEnumerable<T> (get, iterate)Dog коли очікується AnimalВизначення Covariant Interface:
public interface IReadOnlyRepository<out T>
{
T GetById(int id); // ✅ T у return position
IEnumerable<T> GetAll(); // ✅ T у return position
// void Add(T item); // ❌ T у input position — не можна!
}
public class DogRepository : IReadOnlyRepository<Dog>
{
public Dog GetById(int id) => new Dog { Name = "Rex" };
public IEnumerable<Dog> GetAll() => new List<Dog>();
}
// Використання covariance
IReadOnlyRepository<Dog> dogRepo = new DogRepository();
IReadOnlyRepository<Animal> animalRepo = dogRepo; // ✅ Covariance
Animal animal = animalRepo.GetById(1); // Повертає Dog
Теорія Covariance:
Covariance дозволяє використовувати більш похідний тип ніж вказаний, тобто можна використовувати IEnumerable<Dog> там, де очікується IEnumerable<Animal>. Це можливо лише для операцій, що повертають значення (output positions), оскільки ми гарантовано повертаємо тип, сумісний з очікуваним.
Внутрішні механізми covariance:
out у визначенні інтерфейсу або делегатаПриклади covariance в .NET:
IEnumerable<out T> - для ітерації через колекціюIList<out T> - не існує, бо IList має методи, що приймають TFunc<out T> - функції, що повертають значенняIQueryable<out T> - для LINQ запитівin keyword)Contravariance дозволяє використовувати більш загальний (base) тип замість конкретного (derived).
// IComparer<T> є contravariant (має in modifier)
public class AnimalComparer : IComparer<Animal>
{
public int Compare(Animal x, Animal y)
{
return string.Compare(x.Name, y.Name);
}
}
var animalComparer = new AnimalComparer();
// ✅ Contravariance: Animal → Dog
IComparer<Dog> dogComparer = animalComparer; // Працює!
var dogs = new List<Dog>
{
new Dog { Name = "Buddy" },
new Dog { Name = "Rex" }
};
dogs.Sort(dogComparer); // Використовує AnimalComparer для Dog
Чому це працює?
Animal, тому може працювати з DogIComparer<T> (input parameters)Dog коли очікується AnimalВизначення Contravariant Interface:
public interface IProcessor<in T>
{
void Process(T item); // ✅ T у input position
void ProcessMany(IEnumerable<T> items); // ✅ T у input position
// T Get(); // ❌ T у return position — не можна!
}
public class AnimalProcessor : IProcessor<Animal>
{
public void Process(Animal animal)
{
Console.WriteLine($"Processing: {animal.Name}");
}
public void ProcessMany(IEnumerable<Animal> items)
{
foreach (var animal in items)
{
Process(animal);
}
}
}
// Використання contravariance
IProcessor<Animal> animalProcessor = new AnimalProcessor();
IProcessor<Dog> dogProcessor = animalProcessor; // ✅ Contravariance
dogProcessor.Process(new Dog { Name = "Rex" });
Теорія Contravariance:
Contravariance дозволяє використовувати більш базовий тип замість похідного, тобто можна використовувати IComparer<Animal> там, де очікується IComparer<Dog>. Це можливо лише для операцій, що приймають значення (input positions), оскільки ми гарантовано можемо передати похідний тип у метод, який очікує базовий.
Внутрішні механізми contravariance:
in у визначенні інтерфейсу або делегатаПриклади contravariance в .NET:
IComparer<in T> - для порівняня елементівAction<in T> - для дій, що приймають параметриPredicate<in T> - для перевірки умовComparison<in T> - для методів порівнянняЯкщо немає in або out, generic тип є invariant — не можна замінювати типи:
public interface IRepository<T> // Invariant (немає in/out)
{
void Add(T item); // Input
T GetById(int id); // Output
}
IRepository<Dog> dogRepo = new DogRepository();
// IRepository<Animal> animalRepo = dogRepo; // ❌ Не працює — invariant
Теорія Invariance:
Invariance означає, що IRepository<Dog> і IRepository<Animal> є абсолютно різними типами, навіть якщо між Dog і Animal існує відношення успадкування. Це найбезпечніший варіант, оскільки забороняє будь-які потенційно небезпечні операції з типами.
Чому invariance важлива:
Func<Dog> getDog = () => new Dog { Name = "Rex" };
// ✅ Covariance: Dog → Animal у return type
Func<Animal> getAnimal = getDog;
Animal animal = getAnimal(); // Повертає Dog
Action<Animal> processAnimal = (animal) =>
{
Console.WriteLine($"Processing: {animal.Name}");
};
// ✅ Contravariance: Animal → Dog у parameter
Action<Dog> processDog = processAnimal;
processDog(new Dog { Name = "Buddy" });
Func<Animal, Dog> converter = (animal) => new Dog { Name = animal.Name };
// ✅ Contravariance у input (Animal → Dog)
// ✅ Covariance у output (Dog → Animal)
Func<Dog, Animal> varianceConverter = converter;
Animal result = varianceConverter(new Dog { Name = "Rex" });
| Variance | Keyword | Дозволяє | Позиція | Приклад | Використання |
|---|---|---|---|---|---|
| Covariance | out | Derived → Base | Return types | IEnumerable<out T> | Для операцій, що повертають значення |
| Contravariance | in | Base → Derived | Parameters | IComparer<in T> | Для операцій, що приймають параметри |
| Invariance | — | Тільки точний тип | Input + Output | IList<T> | Для операцій, що і читають, і пишуть |
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
IEnumerable<T> GetAll();
void Remove(T item);
}
public class CustomerRepository : IRepository<Customer>
{
private readonly List<Customer> _customers = new();
public void Add(Customer item)
{
_customers.Add(item);
}
public Customer GetById(int id)
{
return _customers.FirstOrDefault(c => c.Id == id);
}
public IEnumerable<Customer> GetAll()
{
return _customers;
}
public void Remove(Customer item)
{
_customers.Remove(item);
}
}
public class CustomCollection<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// Використання
var collection = new CustomCollection<int>();
collection.Add(1);
collection.Add(2);
collection.Add(3);
foreach (int item in collection) // ✅ Працює через IEnumerable<T>
{
Console.WriteLine(item);
}
public class Product : IComparable<Product>
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CompareTo(Product other)
{
if (other == null) return 1;
return Price.CompareTo(other.Price); // Сортування за ціною
}
}
// Custom comparer
public class ProductNameComparer : IComparer<Product>
{
public int Compare(Product x, Product y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
return string.Compare(x.Name, y.Name);
}
}
// Використання
var products = new List<Product>
{
new Product { Name = "Laptop", Price = 1000 },
new Product { Name = "Mouse", Price = 25 },
new Product { Name = "Keyboard", Price = 75 }
};
products.Sort(); // Сортування за Price (IComparable)
products.Sort(new ProductNameComparer()); // Сортування за Name
public class Customer : IEquatable<Customer>
{
public int Id { get; set; }
public string Email { get; set; }
public bool Equals(Customer other)
{
if (other == null) return false;
return Id == other.Id && Email == other.Email;
}
public override bool Equals(object obj)
{
return Equals(obj as Customer);
}
public override int GetHashCode()
{
return HashCode.Combine(Id, Email);
}
}
// Використання
var customer1 = new Customer { Id = 1, Email = "test@example.com" };
var customer2 = new Customer { Id = 1, Email = "test@example.com" };
bool areEqual = customer1.Equals(customer2); // true
public class SmartCollection<T> :
IEnumerable<T>,
ICollection<T>,
IList<T>
{
private readonly List<T> _items = new();
// IEnumerable<T>
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// ICollection<T>
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Add(T item) => _items.Add(item);
public void Clear() => _items.Clear();
public bool Contains(T item) => _items.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public bool Remove(T item) => _items.Remove(item);
// IList<T>
public T this[int index]
{
get => _items[index];
set => _items[index] = value;
}
public int IndexOf(T item) => _items.IndexOf(item);
public void Insert(int index, T item) => _items.Insert(index, item);
public void RemoveAt(int index) => _items.RemoveAt(index);
}
Представляє метод, який приймає параметри та повертає значення:
// Func<TResult> — без параметрів
Func<int> getNumber = () => 42;
int number = getNumber(); // 42
// Func<T, TResult> — 1 параметр
Func<int, int> square = x => x * x;
int result = square(5); // 25
// Func<T1, T2, TResult> — 2 параметри
Func<int, int, int> add = (x, y) => x + y;
int sum = add(3, 4); // 7
// Func підтримує до 16 параметрів!
Func<int, int, int, int, int> addFour = (a, b, c, d) => a + b + c + d;
Практичний приклад:
public class Calculator
{
public int Calculate(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
}
var calc = new Calculator();
int sum = calc.Calculate(10, 5, (x, y) => x + y); // 15
int product = calc.Calculate(10, 5, (x, y) => x * y); // 50
int diff = calc.Calculate(10, 5, (x, y) => x - y); // 5
Представляє метод, який приймає параметри але не повертає значення:
// Action — без параметрів
Action sayHello = () => Console.WriteLine("Hello!");
sayHello();
// Action<T> — 1 параметр
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
// Action<T1, T2> — 2 параметри
Action<string, int> printInfo = (name, age) =>
{
Console.WriteLine($"{name} is {age} years old");
};
printInfo("Bob", 30);
Практичний приклад:
public class Logger
{
public void LogAll(IEnumerable<string> messages, Action<string> logAction)
{
foreach (var message in messages)
{
logAction(message);
}
}
}
var logger = new Logger();
var messages = new[] { "Error 1", "Error 2", "Error 3" };
logger.LogAll(messages, msg => Console.WriteLine(msg));
logger.LogAll(messages, msg => File.AppendAllText("log.txt", msg + "\n"));
Представляє метод, який приймає 1 параметр та повертає bool:
// Predicate<T> еквівалентний Func<T, bool>
Predicate<int> isEven = x => x % 2 == 0;
bool result1 = isEven(4); // true
bool result2 = isEven(5); // false
// Використання з List<T>.FindAll
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> evenNumbers = numbers.FindAll(isEven); // [2, 4, 6, 8, 10]
// Custom delegate з двома type parameters
public delegate TResult Converter<TInput, TResult>(TInput input);
// Використання
Converter<string, int> stringToInt = s => int.Parse(s);
int number = stringToInt("42"); // 42
Converter<int, string> intToString = i => i.ToString();
string text = intToString(100); // "100"
// З LINQ
var strings = new[] { "1", "2", "3", "4", "5" };
var integers = strings.Select(s => stringToInt(s)).ToList();
public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
public void Bark() => Console.WriteLine("Woof!");
}
// Covariance у Func<TResult>
Func<Dog> getDog = () => new Dog { Name = "Rex" };
Func<Animal> getAnimal = getDog; // ✅ Covariance
// Contravariance у Action<T>
Action<Animal> processAnimal = a => Console.WriteLine(a.Name);
Action<Dog> processDog = processAnimal; // ✅ Contravariance
// Комбінація у Func<T, TResult>
Func<Animal, Dog> convertAnimalToDog = a => new Dog { Name = a.Name };
Func<Dog, Animal> convertDogToAnimal = convertAnimalToDog; // ✅ Обидва!
object// ✅ ДОБРЕ: Описові імена
public class Dictionary<TKey, TValue> { }
public interface IConverter<TInput, TOutput> { }
public delegate TResult Func<T, TResult>(T arg);
// ✅ ДОБРЕ: Single parameter — просто T
public class List<T> { }
public interface IComparable<T> { }
// ❌ ПОГАНО: Незрозумілі імена
public class MyClass<A, B, C> { }
public interface IConverter<X, Y> { }
// ❌ ПОГАНО: Надмірні constraints
public class Processor<T>
where T : class, IDisposable, IComparable<T>, IEquatable<T>, new()
{
// Занадто багато вимог!
}
// ✅ КРАЩЕ: Тільки необхідні constraints
public class Processor<T>
where T : IDisposable
{
// Тільки те, що реально потрібно
}
// ❌ СТАРИЙ ПІДХІД: Object
public class OldStack
{
private object[] items;
public void Push(object item) { }
public object Pop() { return items[0]; } // Потрібен cast
}
// ✅ СУЧАСНИЙ: Generics
public class Stack<T>
{
private T[] items;
public void Push(T item) { }
public T Pop() { return items[0]; } // Без cast!
}
// Boxing з object — повільно
ArrayList list = new ArrayList();
list.Add(42); // Boxing: int → object
int value = (int)list[0]; // Unboxing: object → int
// Generics — швидко
List<int> genericList = new List<int>();
genericList.Add(42); // Без boxing
int value2 = genericList[0]; // Без unboxing
Детальніше про Performance Implications:
Boxing/Unboxing Overhead:
При використанні узагальнень з типами значень (value types) відбувається значна економія продуктивності завдяки уникненню операцій boxing та unboxing. Коли value type зберігається в об'єкті або в неузагальненій колекції, він повинен бути упакований (boxing) у heap, що вимагає додаткової операції виділення пам'яті. При зверненні до цього значення відбувається unboxing - перетворення назад у value type.
JIT-компіляція узагальнень:
Компілятор JIT (Just-In-Time) створює спеціалізовану версію узагальненого коду для кожного унікального типу, що використовується як аргумент типу. Для посилальних типів (reference types) JIT використовує одну версію коду для всіх посилальних типів, оскільки всі вони мають однаковий розмір посилання. Для типів значень JIT генерує окрему версію коду для кожного типу значення, що дозволяє уникнути операцій boxing/unboxing і забезпечує максимальну продуктивність.
Вплив на кеш CPU:
Оскільки JIT генерує окремий код для кожного типу значення, це може призвести до більшого використання кешу CPU, особливо якщо використовується багато різних типів значень. Однак переваги від уникнення boxing/unboxing зазвичай переважають ці накладні витрати.
Порівняння продуктивності:
// ✅ ДОБРЕ: out для read-only operations
public interface IReadOnlyRepository<out T>
{
T GetById(int id);
IEnumerable<T> GetAll();
}
// ✅ ДОБРЕ: in для write-only operations
public interface IWriter<in T>
{
void Write(T item);
}
// ✅ ДОБРЕ: Invariant для read-write
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
}
Помилка:
// CS0693: Type parameter has the same name as the type parameter from outer type
public class Outer<T>
{
public void Method<T>() // ❌ T приховує T з класу
{
// ...
}
}
Рішення:
// ✅ Використати різні імена
public class Outer<T>
{
public void Method<U>() // ✅ U не конфліктує з T
{
// ...
}
}
Помилка:
public class Repository<T> where T : IEntity
{
}
public class Customer { } // Не реалізує IEntity
var repo = new Repository<Customer>(); // ❌ CS0311
Рішення:
// ✅ Customer має реалізувати IEntity
public class Customer : IEntity
{
public int Id { get; set; }
}
var repo = new Repository<Customer>(); // ✅ OK
Помилка:
public class Handler<T> where T : class
{
}
var handler = new Handler<int>(); // ❌ CS0452: int — value type
Рішення:
// Використати reference type
var handler1 = new Handler<string>(); // ✅ OK
// Або видалити constraint
public class Handler<T> // Без constraint
{
}
var handler2 = new Handler<int>(); // ✅ OK
Помилка:
public interface IProcessor<T> { }
// CS0695: Cannot implement both...
public class MyClass<T1, T2> : IProcessor<T1>, IProcessor<T2>
{
// Якщо T1 == T2, конфлікт!
}
Рішення:
// ✅ Окремі інтерфейси для різних типів
public interface IProcessor1<T> { }
public interface IProcessor2<T> { }
public class MyClass<T1, T2> : IProcessor1<T1>, IProcessor2<T2>
{
// ✅ Немає конфлікту
}
Помилка:
public interface IBad<out T>
{
void Process(T item); // ❌ T у input position з 'out'
}
Рішення:
// ✅ Видалити 'out' для invariant
public interface IProcessor<T>
{
void Process(T item);
}
// ✅ Або зробити тільки output operations
public interface IReadOnly<out T>
{
T Get();
IEnumerable<T> GetAll();
}
Мета: Створити generic repository з CRUD операціями.
Опис: Реалізуйте Repository<T> клас з базовими операціями створення, читання, оновлення та видалення.
Вимоги:
where T : IEntity (де IEntity має властивість Id)Add, GetById, GetAll, Update, DeleteList<T>Початковий Код:
public interface IEntity
{
int Id { get; set; }
}
// TODO: Реалізуйте Repository<T>
public interface IEntity
{
int Id { get; set; }
}
public class Repository<T> where T : IEntity
{
private readonly List<T> _items = new List<T>();
private int _nextId = 1;
public void Add(T item)
{
item.Id = _nextId++;
_items.Add(item);
}
public T GetById(int id)
{
return _items.FirstOrDefault(x => x.Id == id);
}
public IEnumerable<T> GetAll()
{
return _items.ToList();
}
public void Update(T item)
{
var existing = GetById(item.Id);
if (existing != null)
{
int index = _items.IndexOf(existing);
_items[index] = item;
}
}
public void Delete(int id)
{
var item = GetById(id);
if (item != null)
{
_items.Remove(item);
}
}
}
// Приклад використання
public class Customer : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var repo = new Repository<Customer>();
repo.Add(new Customer { Name = "Alice", Email = "alice@example.com" });
repo.Add(new Customer { Name = "Bob", Email = "bob@example.com" });
var customers = repo.GetAll();
foreach (var customer in customers)
{
Console.WriteLine($"{customer.Id}: {customer.Name}");
}
Пояснення:
where T : IEntity для доступу до IdId при додаванніList<T>Мета: Створити generic factory pattern з constraints та dependency injection.
Опис: Реалізуйте Factory<T> який може створювати екземпляри різних типів з можливістю ін'єкції залежностей.
Вимоги:
public class Factory<T> where T : new()
{
private T _cachedInstance;
private bool _isSingleton;
public Factory(bool singleton = false)
{
_isSingleton = singleton;
}
public T Create()
{
if (_isSingleton)
{
if (_cachedInstance == null)
{
_cachedInstance = new T();
}
return _cachedInstance;
}
return new T();
}
}
// Factory з dependency injection
public class FactoryWithDI<T>
{
private readonly Func<T> _creator;
private T _cachedInstance;
private bool _isSingleton;
public FactoryWithDI(Func<T> creator, bool singleton = false)
{
_creator = creator;
_isSingleton = singleton;
}
public T Create()
{
if (_isSingleton)
{
if (_cachedInstance == null)
{
_cachedInstance = _creator();
}
return _cachedInstance;
}
return _creator();
}
}
// Приклади використання
// 1. Simple factory з parameterless constructor
public class Logger
{
public Logger() { }
public void Log(string message) => Console.WriteLine(message);
}
var loggerFactory = new Factory<Logger>(singleton: true);
var logger1 = loggerFactory.Create();
var logger2 = loggerFactory.Create();
Console.WriteLine(ReferenceEquals(logger1, logger2)); // true
// 2. Factory з DI
public class DatabaseConnection
{
private readonly string _connectionString;
public DatabaseConnection(string connectionString)
{
_connectionString = connectionString;
}
}
var dbFactory = new FactoryWithDI<DatabaseConnection>(
() => new DatabaseConnection("Server=localhost;Database=MyDb"),
singleton: true
);
var db1 = dbFactory.Create();
var db2 = dbFactory.Create();
Console.WriteLine(ReferenceEquals(db1, db2)); // true
Пояснення:
Factory<T> вимагає new() constraintFactoryWithDI<T> використовує Func<T> для гнучкостіМета: Створити generic collection wrapper з підтримкою covariance та contravariance операцій.
Опис: Реалізуйте wrapper над колекцією, який демонструє практичне використання variance.
Вимоги:
// Hierarchy
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound() => Console.WriteLine("Some sound");
}
public class Dog : Animal
{
public override void MakeSound() => Console.WriteLine("Woof!");
}
public class Cat : Animal
{
public override void MakeSound() => Console.WriteLine("Meow!");
}
// Covariant interface — тільки читання
public interface IReadOnlyAnimalCollection<out T> where T : Animal
{
T Get(int index);
IEnumerable<T> GetAll();
int Count { get; }
}
// Contravariant interface — тільки запис
public interface IWriteOnlyAnimalCollection<in T> where T : Animal
{
void Add(T animal);
void AddRange(IEnumerable<T> animals);
}
// Invariant implementation — повний функціонал
public class AnimalCollection<T> :
IReadOnlyAnimalCollection<T>,
IWriteOnlyAnimalCollection<T>
where T : Animal
{
private readonly List<T> _animals = new List<T>();
// Read operations (covariant)
public T Get(int index) => _animals[index];
public IEnumerable<T> GetAll() => _animals.ToList();
public int Count => _animals.Count;
// Write operations (contravariant)
public void Add(T animal) => _animals.Add(animal);
public void AddRange(IEnumerable<T> animals) => _animals.AddRange(animals);
// Additional operations
public void Remove(T animal) => _animals.Remove(animal);
public void Clear() => _animals.Clear();
}
// Демонстрація використання
class Program
{
static void Main()
{
// Створюємо колекцію собак
var dogCollection = new AnimalCollection<Dog>();
dogCollection.Add(new Dog { Name = "Rex" });
dogCollection.Add(new Dog { Name = "Buddy" });
// ✅ Covariance: Dog → Animal для read-only
IReadOnlyAnimalCollection<Animal> readOnlyAnimals = dogCollection;
Console.WriteLine($"Total animals: {readOnlyAnimals.Count}");
foreach (Animal animal in readOnlyAnimals.GetAll())
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
}
// ✅ Contravariance: Animal → Dog для write-only
IWriteOnlyAnimalCollection<Animal> writeOnlyAnimals =
new AnimalCollection<Animal>();
IWriteOnlyAnimalCollection<Dog> writeOnlyDogs = writeOnlyAnimals;
writeOnlyDogs.Add(new Dog { Name = "Max" });
// Практичний приклад: метод, який приймає read-only animal collection
PrintAllAnimals(dogCollection); // ✅ Працює через covariance
// Практичний приклад: метод, який приймає write-only dog collection
var animalWriter = new AnimalCollection<Animal>();
AddDogs(animalWriter); // ✅ Працює через contravariance
}
// Метод приймає read-only колекцію тварин
static void PrintAllAnimals(IReadOnlyAnimalCollection<Animal> animals)
{
Console.WriteLine("\n=== All Animals ===");
foreach (var animal in animals.GetAll())
{
Console.WriteLine($"- {animal.Name}");
}
}
// Метод, який додає собак у write-only колекцію
static void AddDogs(IWriteOnlyAnimalCollection<Dog> collection)
{
collection.Add(new Dog { Name = "Charlie" });
collection.Add(new Dog { Name = "Luna" });
}
}
Пояснення:
Covariance (out T):
IReadOnlyAnimalCollection<Dog> може бути присвоєна IReadOnlyAnimalCollection<Animal>Contravariance (in T):
IWriteOnlyAnimalCollection<Animal> може бути присвоєна IWriteOnlyAnimalCollection<Dog>Практичне Застосування:
Generics — це потужний механізм C#, який дозволяє писати type-safe та ефективний код:
List<T>, Dictionary<K,V>, LINQ базуються на genericsКлючові Концепції:
class, struct, new(), where T : Base)allows ref struct для підтримки ref struct типівout) — виведення більш конкретних типівin) — введення більш загальних типівIEnumerable<T>, IComparable<T>, IEquatable<T>Func<T>, Action<T>, Predicate<T>Best Practices:
object для type safetyТаблиця порівняння основних концепцій:
| Концепція | Призначення | Приклад використання | Переваги |
|---|---|---|---|
| Generic Classes | Повторне використання коду для різних типів | List<T>, Dictionary<TKey, TValue> | Type safety, уникнення дублювання коду |
| Generic Methods | Методи, що працюють з різними типами | Convert<T>(), Max<T>() | Гнучкість, type inference |
| Constraints | Обмеження на типи, що можуть бути використані | where T : class, where T : new() | Безпечний доступ до членів типу |
| Covariance | Використання більш похідного типу | IEnumerable<Dog> як IEnumerable<Animal> | Гнучкість при читанні даних |
| Contravariance | Використання більш базового типу | IComparer<Animal> як IComparer<Dog> | Гнучкість при записі даних |