C# Day 4: Generics

Sau khi đã học về Type System, OOP, Members & Modifiers, ngày 4 tập trung vào Generics.
Generics là nền tảng của rất nhiều API trong .NET:

List<T>
Dictionary<TKey, TValue>
Task<T>
IEnumerable<T>
IRepository<T>

Mục tiêu của ngày 4 là hiểu:

  • Generic là gì?
  • T là gì?
  • Generic khác object thế nào?
  • Generic class, method, interface hoạt động ra sao?
  • Generic constraints dùng để làm gì?
  • where T : class, struct, notnull, new() nghĩa là gì?
  • Generic liên quan gì tới boxing?
  • Open generic là gì?
  • Open generic hoạt động thế nào với Dependency Injection?

1. Generic là gì?

Generic cho phép viết code có thể làm việc với nhiều type khác nhau nhưng vẫn giữ compile-time type safety.

public class Box<T>
{
    public T Value { get; set; }
}

T là một type parameter.

T
→ placeholder cho một type

Khi sử dụng:

var intBox = new Box<int>();
intBox.Value = 10;

var stringBox = new Box<string>();
stringBox.Value = "Hello";
Box<int>
→ T = int

Box<string>
→ T = string

2. Vì sao cần Generic?

Nếu không dùng generic, có thể dùng object:

public class Box
{
    public object Value { get; set; }
}

Ví dụ:

var box = new Box();
box.Value = 10;

int value = (int)box.Value;

Nhưng nếu:

box.Value = "hello";
int value = (int)box.Value;

code có thể compile nhưng runtime gặp:

InvalidCastException

Generic:

var box = new Box<int>();
box.Value = 10;

Nếu:

// box.Value = "hello";

compiler chặn ngay.

object
→ mất specific type information
→ thường phải cast
→ lỗi có thể xuất hiện runtime

generic
→ giữ type information
→ compiler kiểm tra
→ type safety

3. Type Safety

Box<int> box = new();

Compiler biết:

Value phải là int

Nên:

box.Value = 100;       // OK
// box.Value = "100";  // compile error

Một nguyên tắc quan trọng:

Compile-time error
thường tốt hơn
runtime error

4. Generic Class

public class Repository<T>
{
    public void Add(T item)
    {
    }
}

Có thể reuse:

var userRepository = new Repository<User>();
var orderRepository = new Repository<Order>();

Cùng một implementation:

Repository<T>

nhưng dùng cho nhiều type:

User
Order
Product
Coupon
...

5. Generic không chỉ dành cho Class

C# hỗ trợ:

generic class
generic struct
generic interface
generic method
generic delegate

Ví dụ generic interface:

public interface IRepository<T>
{
    void Add(T entity);
    T? GetById(Guid id);
}

Implementation:

public class UserRepository : IRepository<User>
{
    public void Add(User entity)
    {
    }

    public User? GetById(Guid id)
    {
        return null;
    }
}

6. Generic Method

Một method có thể generic ngay cả khi class không generic.

public static T GetFirst<T>(T[] items)
{
    return items[0];
}

Dùng:

int number = GetFirst<int>(new[] { 1, 2, 3 });
string name = GetFirst<string>(new[] { "Vu", "David" });

Compiler thường infer được type:

var number = GetFirst(new[] { 1, 2, 3 });
T = int

Đây gọi là type inference.


7. Type Parameter vs Type Argument

Khai báo:

public class Box<T>
{
}

T là:

type parameter

Khi sử dụng:

Box<int>

int là:

type argument
Box<T>
    ↑
type parameter

Box<int>
    ↑
type argument

8. Multiple Type Parameters

public class Pair<TKey, TValue>
{
    public TKey Key { get; set; }
    public TValue Value { get; set; }
}

Dùng:

var pair = new Pair<int, string>
{
    Key = 1,
    Value = "Vu"
};

Một ví dụ quen thuộc:

Dictionary<TKey, TValue>

9. Naming Convention

Một type parameter đơn giản thường dùng:

T

Nếu có ý nghĩa rõ ràng hơn:

TEntity
TKey
TValue
TResult
TRequest
TResponse

Ví dụ:

public interface IRepository<TEntity>
{
}

Generic Constraints

10. Generic Constraint là gì?

Nếu:

public class Repository<T>
{
}

thì T có thể là rất nhiều type.

Nếu muốn giới hạn:

where T : ...

Constraint nói:

Type dùng làm T phải thỏa điều kiện nào đó.


11. where T : class

public class Repository<T>
    where T : class
{
}

Nghĩa là:

T phải là reference type

12. where T : struct

public class ValueStore<T>
    where T : struct
{
}

Nghĩa là:

T phải là non-nullable value type

Ví dụ:

ValueStore<int> a = new();
ValueStore<DateTime> b = new();

13. where T : notnull

public class Cache<TKey, TValue>
    where TKey : notnull
{
}

Nghĩa là type argument không nên là nullable theo nullable analysis.

Bạn sẽ gặp concept này ở:

Dictionary<TKey, TValue>

14. Base Class Constraint

public class AnimalProcessor<T>
    where T : Animal
{
}

Nghĩa là:

T phải là Animal
hoặc derived type của Animal

15. Interface Constraint

public interface IEntity
{
    Guid Id { get; }
}
public class Repository<T>
    where T : IEntity
{
    public Guid GetId(T entity)
    {
        return entity.Id;
    }
}

Nếu không có constraint, compiler không biết T có property Id.

Sau khi có:

where T : IEntity

compiler biết:

T có contract IEntity
→ có Id

Constraint không chỉ để chặn type sai.

Nó còn cung cấp thêm type information cho compiler.


16. where T : new()

public class Factory<T>
    where T : new()
{
    public T Create()
    {
        return new T();
    }
}

new() là constraint.

Nó nói:

T phải có public parameterless constructor.

Ví dụ:

public class User
{
    public User()
    {
    }
}

thì:

var factory = new Factory<User>();
var user = factory.Create();

được.

Nhưng:

public class Order
{
    public Order(Guid id)
    {
    }
}

thì không thỏa new() constraint.

Vì sao cần new()?

Nếu không có constraint, compiler không thể đảm bảo new T() hợp lệ.

Khi thêm:

where T : new()

compiler có guarantee:

T tạo được bằng new T()

Mental model:

where T : new()
→ guarantee về constructor

new T()
→ sử dụng guarantee đó

17. Public Parameterless Constructor là gì?

public class User
{
    public User()
    {
    }
}

Constructor này:

  • public
  • không có parameter

nên gọi là:

public parameterless constructor

Nếu class không khai báo constructor nào:

public class User
{
}

compiler tự tạo default public parameterless constructor.

Nhưng nếu đã khai báo:

public class User
{
    public User(string name)
    {
    }
}

compiler không tự thêm constructor rỗng nữa.


18. Multiple Constraints

public class Repository<T>
    where T : class, IEntity, new()
{
}

Nghĩa là T phải:

reference type
AND
implement IEntity
AND
có public parameterless constructor

Mental model:

primary type constraint
→ trước

interface constraints
→ giữa

new()
→ cuối

Generic và Performance

19. Generic và Boxing

Nhớ kiến thức ngày 1:

int number = 10;
object obj = number;

đây là boxing.

API dựa trên object có thể gây boxing với value type.

Generic:

List<int> numbers = new();
numbers.Add(10);

giữ int là int.

object-based API
+ value type
→ có thể boxing

generic<T>
→ giữ type cụ thể
→ thường tránh boxing không cần thiết

20. Generic vs object

object:

void Print(object value)
{
}

Generic:

void Print<T>(T value)
{
}

Khác nhau ở type information.

object
→ specific type bị nhìn qua object

generic
→ compiler vẫn giữ type information của T

Generic trong .NET

21. Các Generic Type dùng hàng ngày

List<User>
Dictionary<Guid, User>
Task<User>
IEnumerable<Order>
Nullable<int>

Generics là nền tảng của rất nhiều .NET API.


22. Generic Interface

public interface IRepository<TEntity>
{
    Task<TEntity?> GetAsync(Guid id);
    Task AddAsync(TEntity entity);
}

Implementation:

public class UserRepository : IRepository<User>
{
    public Task<User?> GetAsync(Guid id)
    {
        return Task.FromResult<User?>(null);
    }

    public Task AddAsync(User entity)
    {
        return Task.CompletedTask;
    }
}

23. Generic Inheritance

public class BaseRepository<T>
{
}

Derived:

public class UserRepository : BaseRepository<User>
{
}

Ở đây:

T = User

Open Generic

24. Open vs Closed Generic Type

List<T>

là open generic type.

List<int>

là closed generic type.

List<T>
→ open

List<int>
→ closed

25. Open Generic trong Dependency Injection

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

Nghĩa là:

Với mọi IRepository<T> hợp lệ, DI có thể dùng Repository<T>.

Ví dụ:

IRepository<User>
→ Repository<User>

IRepository<Order>
→ Repository<Order>

IRepository<Coupon>
→ Repository<Coupon>

IRepository<> là open generic interface.

Repository<> là open generic implementation.


26. Open Generic không áp dụng cho mọi Interface

Registration:

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

chỉ áp dụng cho:

IRepository<T>

Nó không tự áp dụng cho:

IService<T>
IValidator<T>
IHandler<T>

Muốn register phải khai báo riêng.


27. Implementation phải Match Interface

public interface IRepository<T>
{
}
public class Repository<T> : IRepository<T>
{
}

Registration:

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

Implementation phải implement đúng interface generic tương ứng.


28. Generic có 2 Type Parameters

public interface IRepository<TKey, TEntity>
{
}
public class Repository<TKey, TEntity>
    : IRepository<TKey, TEntity>
{
}

Registration:

services.AddScoped(
    typeof(IRepository<,>),
    typeof(Repository<,>)
);

Cách đọc:

<>      → 1 type parameter
<,>     → 2 type parameters
<,,>    → 3 type parameters

29. DI Resolve Open Generic như thế nào?

public class Repository<T> : IRepository<T>
{
    private readonly AppDbContext _db;

    public Repository(AppDbContext db)
    {
        _db = db;
    }
}

Registration:

services.AddScoped<AppDbContext>();

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

Controller:

public class UserController
{
    public UserController(
        IRepository<User> repository)
    {
    }
}

Flow:

UserController
↓
cần IRepository<User>
↓
DI thấy registration

IRepository<>
→ Repository<>
↓
T = User
↓
Repository<User>
↓
constructor cần AppDbContext
↓
DI resolve AppDbContext
↓
tạo Repository<User>
↓
inject vào UserController

30. new() Constraint và DI là hai chuyện khác nhau

new():

where T : new()

là C# generic constraint.

Nó cho phép:

new T()

Open Generic DI:

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

là runtime DI registration.

DI không yêu cầu service phải có parameterless constructor.

Ví dụ hợp lệ:

public class Repository<T> : IRepository<T>
{
    private readonly AppDbContext _db;

    public Repository(AppDbContext db)
    {
        _db = db;
    }
}

Mental model:

where T : new()
→ compiler generic constraint

AddScoped(typeof(...<>))
→ runtime DI resolution

Variance — Nhận diện trước

31. in và out

Generics còn có:

covariance
contravariance

Ví dụ:

IEnumerable<out T>
IComparer<in T>

Mental model tạm:

out T
→ T chủ yếu đi ra khỏi API

in T
→ T chủ yếu đi vào API

Phần variance nên học sâu sau khi đã chắc inheritance, generic, interface và delegate.


Common Mistakes

32. Generic không phải dynamic

public void Process<T>(T item)
{
}

Không có nghĩa compiler cho gọi bất kỳ member nào.

// item.Save();

Muốn gọi:

public void Process<T>(T item)
    where T : ISavable
{
    item.Save();
}

Generic vẫn là:

static typing

33. Không phải cái gì cũng cần Generic

Đừng biến mọi service thành:

Service<T1, T2, T3, T4>

nếu business logic chỉ dành cho một domain type cụ thể.

Generic nên được dùng khi abstraction thực sự có thể reuse cho nhiều type.


Bảng Tổng Kết

Concept Mental model
Generic reusable code + type safety
T type parameter
int trong Box<int> type argument
Generic method method có type parameter
Constraint giới hạn + cung cấp type info
where T : class reference type
where T : struct value type
where T : IEntity phải implement interface
where T : Animal Animal hoặc derived type
where T : new() public parameterless constructor
Open generic chưa có type argument
Closed generic đã có type argument
Generic + value type thường tránh boxing không cần thiết

Câu hỏi Ôn tập + Đáp án

1. Generic là gì?

Cho phép viết code reusable cho nhiều type nhưng vẫn giữ compile-time type safety.

2. T là gì?

Type parameter.

3. Trong Box<int>, int là gì?

Type argument.

4. Generic tốt hơn object ở đâu?

Giữ type information, giảm cast, phát hiện nhiều lỗi ở compile time và thường tránh boxing không cần thiết.

5. Generic method là gì?

Method có type parameter riêng.

T Get<T>()

6. where T : class nghĩa là gì?

T phải là reference type.

7. where T : struct nghĩa là gì?

T phải là non-nullable value type.

8. where T : IEntity nghĩa là gì?

T phải implement IEntity.

9. where T : Animal nghĩa là gì?

T phải là Animal hoặc derived type của Animal.

10. where T : new() nghĩa là gì?

T phải có public parameterless constructor.

11. Vì sao new() cần thiết để gọi new T()?

Vì compiler cần guarantee rằng mọi T hợp lệ đều có constructor không tham số public.

12. Constraint chỉ dùng để chặn type sai đúng không?

Không. Constraint còn giúp compiler biết thêm capability/member của T.

13. Generic có giúp tránh boxing không?

Có trong nhiều value-type scenario vì không cần chuyển dữ liệu thành object.

14. Dictionary<TKey,TValue> có bao nhiêu type parameter?

Hai: TKey và TValue.

15. List<T> là open hay closed generic?

List<T> là open generic; List<int> là closed generic.

16. Open generic DI registration nghĩa là gì?

services.AddScoped(
    typeof(IRepository<>),
    typeof(Repository<>)
);

nghĩa là:

IRepository<T>
→ Repository<T>

cho các T hợp lệ.

17. Open generic registration có áp dụng cho mọi generic interface không?

Không. Nó chỉ áp dụng cho generic type family được register.

18. DI có cần where T : new() để tạo Repository không?

Không. DI resolve constructor dependency và không yêu cầu parameterless constructor.

19. Generic có phải dynamic typing không?

Không. Generic vẫn là static typing.

20. Vì sao constraint quan trọng?

Vì nó vừa giới hạn T, vừa cung cấp thêm type information cho compiler.


Bài tập Ngày 4

Tạo contract:

public interface IEntity
{
    Guid Id { get; }
}

Tạo generic repository:

public class Repository<TEntity>
    where TEntity : class, IEntity
{
    public Guid GetId(TEntity entity)
    {
        return entity.Id;
    }
}

Tạo entity:

public class User : IEntity
{
    public Guid Id { get; init; }
}

Sử dụng:

var repository = new Repository<User>();

var user = new User
{
    Id = Guid.NewGuid()
};

Console.WriteLine(
    repository.GetId(user)
);

Sau đó thử:

// Repository<int>

và tự giải thích vì sao compiler không cho.


Key Takeaways

Generic
→ reusable code nhưng vẫn giữ type safety

T
→ type parameter

Box<int>
→ int là type argument

Constraint
→ giới hạn T
+
cho compiler thêm type information

where T : new()
→ T phải có public parameterless constructor

Generic
→ thường tránh cast và boxing không cần thiết

IRepository<>
→ open generic

IRepository<User>
→ closed generic

Open generic DI
→ mapping một generic contract family
  sang generic implementation family

Nếu hiểu chắc năm câu sau thì nền tảng Generics đã khá ổn:

1. Vì sao Generic tốt hơn object?

2. T khác int trong List<int> như thế nào?

3. Constraint giúp compiler điều gì?

4. Vì sao where T : IEntity
   cho phép gọi entity.Id?

5. where T : new()
   khác open generic DI như thế nào?