C# Access Modifiers: public, private, protected, internal và cách chọn đúng
Access Modifier trả lời một câu hỏi rất đơn giản:
Code ở đâu được phép truy cập member hoặc type này?
Ví dụ:
public class User
{
private string _passwordHash = "";
}
Ở đây:
Userlàpublic→ code bên ngoài có thể nhìn thấy type này._passwordHashlàprivate→ chỉ code bên trongUserđược truy cập trực tiếp.
Mental model:
Access Modifier
→ đặt boundary
→ ai được nhìn thấy?
→ ai được gọi?
→ ai được thay đổi?
Access modifier là một phần quan trọng của encapsulation.
1. Vì sao cần Access Modifier?
Nếu tất cả mọi thứ đều public:
public class BankAccount
{
public decimal Balance;
}
Caller có thể:
account.Balance = -1000000;
Business rule bị phá vỡ.
Thiết kế tốt hơn:
public class BankAccount
{
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException();
if (amount > Balance)
throw new InvalidOperationException();
Balance -= amount;
}
}
Bây giờ caller không được:
// account.Balance = -100;
Mà phải:
account.Withdraw(100);
Mental model:
Access Modifier
→ không chỉ để hide code
mà còn để
→ bảo vệ invariant
→ giảm API surface
→ kiểm soát cách state thay đổi
2. public
public là mức accessibility rộng nhất.
public class UserService
{
public void CreateUser()
{
}
}
Code khác có thể:
var service = new UserService();
service.CreateUser();
Mental model:
public
→ expose ra bên ngoài
Thường dùng cho:
- API của library
- Controller
- DTO
- public service contract
- method thực sự muốn consumer gọi
3. private
private nghĩa là:
Chỉ code bên trong chính type đó được truy cập.
public class User
{
private string _passwordHash = "";
private void ValidatePassword()
{
}
}
Bên ngoài:
var user = new User();
// user._passwordHash // lỗi
// user.ValidatePassword(); // lỗi
Mental model:
private
→ implementation detail của chính type
Một mindset tốt:
Nếu không có lý do expose ra ngoài thì giữ
private.
4. protected
protected liên quan tới inheritance.
public class Animal
{
protected string Name = "";
}
Derived class:
public class Dog : Animal
{
public void PrintName()
{
Console.WriteLine(Name);
}
}
Dog truy cập được Name vì Dog là derived type của Animal.
Nhưng code bên ngoài:
var dog = new Dog();
// dog.Name // lỗi
Mental model:
protected
→ class hiện tại
+
derived classes
5. private vs protected
public class Animal
{
private int _age;
protected string Name = "";
}
public class Dog : Animal
{
public void Test()
{
// _age = 10; // không được
Name = "Lucky"; // được
}
}
private
→ chỉ Animal
protected
→ Animal + derived types
6. internal
internal có nghĩa:
Chỉ code nằm trong cùng assembly mới truy cập được.
internal class TokenGenerator
{
}
Nếu cùng project/assembly thì dùng được. Project khác reference assembly này thì không truy cập được.
Mental model:
internal
→ public bên trong assembly
→ private đối với assembly bên ngoài
7. Assembly là gì?
Hiểu đơn giản:
C# project
↓
compile
↓
.dll / .exe
↓
assembly
Ví dụ solution:
MyApp.Api
MyApp.Application
MyApp.Domain
MyApp.Infrastructure
Thông thường mỗi project compile thành một assembly riêng.
8. Khi nào internal hữu ích?
Ví dụ:
public interface IEmailSender
{
Task SendAsync(string email);
}
Implementation:
internal sealed class SendGridEmailSender : IEmailSender
{
public Task SendAsync(string email)
{
return Task.CompletedTask;
}
}
Consumer chỉ cần biết IEmailSender, không cần biết implementation cụ thể.
9. protected internal
protected internal void Log()
{
}
Mental model:
protected OR internal
Nghĩa là access được nếu:
cùng assembly
OR
derived type
10. private protected
private protected void Log()
{
}
Mental model:
protected AND internal
Nghĩa là caller phải:
- là derived type
- và nằm trong cùng assembly
11. So sánh protected internal và private protected
protected internal
→ protected OR internal
private protected
→ protected AND internal
| Modifier | Cùng assembly | Derived khác assembly |
|---|---|---|
internal |
✅ | ❌ |
protected |
nếu derived | ✅ |
protected internal |
✅ | ✅ |
private protected |
chỉ derived | ❌ |
12. Bảng đầy đủ
| Modifier | Chính type | Same assembly | Derived khác assembly | Code ngoài |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
protected |
✅ | nếu derived | ✅ | ❌ |
internal |
✅ | ✅ | ❌ | ❌ |
protected internal |
✅ | ✅ | ✅ | ❌ |
private protected |
✅ | nếu derived | ❌ | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
13. Default Access Modifier
Top-level class:
class User
{
}
mặc định là:
internal class User
{
}
Member trong class:
public class User
{
string Name;
}
Name mặc định là private.
Mental model:
top-level type
→ internal mặc định
class member
→ private mặc định
14. Accessibility của Member bị giới hạn bởi Type chứa nó
internal class UserService
{
public void Run()
{
}
}
Run() là public, nhưng UserService là internal.
Project khác không thể truy cập UserService để gọi method đó.
Mental model:
Member accessibility
không thể vượt qua boundary
của containing type
15. Access Modifier trên Property
public string Name { get; private set; }
Nghĩa là:
get
→ public
set
→ private
Bên ngoài đọc được nhưng không set trực tiếp được.
16. Ví dụ Domain Entity
public class Order
{
public Guid Id { get; }
public OrderStatus Status { get; private set; }
public Order(Guid id)
{
Id = id;
Status = OrderStatus.Pending;
}
public void Pay()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException();
Status = OrderStatus.Paid;
}
}
Bên ngoài không được:
// order.Status = OrderStatus.Paid;
Mà phải:
order.Pay();
Đây chính là encapsulation.
17. Access Modifier trên Constructor
Constructor cũng có access modifier.
public User()
{
}
mọi nơi có thể tạo object.
Nhưng:
private User()
{
}
caller bên ngoài không thể new User().
Private constructor thường dùng trong:
- factory
- singleton
- controlled creation
18. file Access Modifier
C# hiện đại còn hỗ trợ:
file class Helper
{
}
file nghĩa là type chỉ accessible trong cùng source file.
Mental model:
file
→ visibility chỉ trong source file
Thường hữu ích cho helper implementation hoặc generated code.
19. Access Modifier và Encapsulation
Đừng học modifier như các keyword rời rạc.
Hãy nhìn theo boundary:
private
→ boundary của type
protected
→ boundary của inheritance hierarchy
internal
→ boundary của assembly
public
→ boundary rộng nhất
file
→ boundary của source file
20. Principle: Least Privilege
Một rule thiết kế hữu ích:
Chỉ expose mức accessibility rộng đến mức thực sự cần thiết.
Mindset:
Nếu chỉ dùng trong type
→ private
Nếu chỉ phục vụ inheritance
→ protected / private protected
Nếu chỉ dùng trong assembly
→ internal
Nếu thực sự là public contract
→ public
21. Ví dụ Backend thực tế
public class CouponService
{
private readonly ICouponRepository _repository;
public CouponService(ICouponRepository repository)
{
_repository = repository;
}
public async Task RedeemAsync(Guid couponId)
{
await ValidateAsync(couponId);
await _repository.RedeemAsync(couponId);
}
private Task ValidateAsync(Guid couponId)
{
return Task.CompletedTask;
}
}
Ở đây:
CouponService
→ public type
RedeemAsync
→ public behavior
_repository
→ private implementation detail
ValidateAsync
→ private internal behavior
22. Sai lầm thường gặp
Tất cả đều public
Không nên nếu caller không thực sự cần access.
Nghĩ internal = private
Sai:
private
→ cùng type
internal
→ cả assembly
Nhầm protected internal
Đúng là:
protected OR internal
Nhầm private protected
Đúng là:
protected AND internal
23. Câu hỏi Interview + Đáp án
1. private và internal khác gì?
private chỉ accessible trong containing type. internal accessible từ code trong cùng assembly.
2. protected là gì?
Cho phép containing type và derived types truy cập member.
3. protected internal nghĩa là gì?
Accessible nếu caller ở cùng assembly hoặc là derived type.
4. private protected nghĩa là gì?
Accessible cho derived type nhưng chỉ khi derived type ở cùng assembly.
5. Top-level class không có modifier thì là gì?
Mặc định internal.
6. Member trong class không có modifier thì sao?
Mặc định private.
7. public method trong internal class có public toàn hệ thống không?
Không. Containing type là internal, nên code ngoài assembly không thể truy cập class để gọi method đó.
8. public get; private set; nghĩa là gì?
Code bên ngoài đọc được property nhưng chỉ class chứa property mới dùng setter.
9. Khi nào nên dùng internal?
Khi type/member cần dùng trong assembly nhưng không muốn trở thành public API cho assembly khác.
10. Vì sao không nên để mọi thứ public?
Vì tăng API surface, coupling, khó thay đổi implementation và dễ phá encapsulation.
24. Key Takeaways
private
→ only this type
protected
→ this type + derived types
internal
→ same assembly
protected internal
→ derived OR same assembly
private protected
→ derived AND same assembly
public
→ accessible rộng nhất
file
→ same source file
Access modifier không chỉ là “ai thấy code”, mà là công cụ thiết kế boundary và bảo vệ state của hệ thống.
Nếu một member không cần public thì đừng làm nó public.