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ì?
Tlà gì?- Generic khác
objectthế 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
Tphả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:
Tphả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ùngRepository<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?