Chuyển tới nội dung chính

C# Interview Questions

Nền tảng ngôn ngữ (Language Fundamentals)

Q1: Sự khác biệt giữa readonlyconst là gì?

Cả hai đều tạo ra các giá trị bất biến (immutable), nhưng chúng khác nhau về thời điểm giá trị được gán và loại kiểu dữ liệu được chấp nhận:

Tính năngconstreadonly
Thời điểm gánThời điểm biên dịch (Compile time)Thời điểm chạy — Runtime (constructor hoặc khai báo)
Tĩnh ngầm định (Implicitly static)Không (có thể là instance hoặc static)
Kiểu được phépKiểu nguyên thủy (Primitives), chuỗi (strings), enum, nullBất kỳ kiểu nào
Có thể dùng newKhông
Thay đổi giá trịGây lỗi nhị phân (Binary-breaking change, được inline)An toàn giữa các assembly
public class Settings
{
// const — giá trị được nhúng tại thời điểm biên dịch, ngầm định static
public const int MaxRetries = 3;
public const string AppName = "MyApp";

// readonly — giá trị được gán tại runtime
public readonly DateTime CreatedAt;
public readonly string ConnectionString;

public Settings(string connectionString)
{
ConnectionString = connectionString;
CreatedAt = DateTime.UtcNow;
}
}
Nên dùng readonly cho các giá trị public

Các giá trị const được inline tại thời điểm biên dịch — mọi assembly tham chiếu đều nhận giá trị literal được nhúng vào. Nếu bạn thay đổi một const, tất cả assembly tham chiếu phải được biên dịch lại. Hãy dùng public static readonly cho các giá trị có thể thay đổi giữa các phiên bản.


Q2: Từ khóa sealed được sử dụng để làm gì?

sealed ngăn chặn việc kế thừa hoặc ghi đè thêm:

// Sealed class — không thể được kế thừa
public sealed class ConnectionString
{
public string Value { get; }
public ConnectionString(string value) => Value = value;
}
// public class CustomConnStr : ConnectionString { } // Lỗi biên dịch

// Sealed method — không thể bị ghi đè tiếp trong chuỗi kế thừa
public class BaseRenderer
{
protected virtual void RenderHeader() { }
}

public class HtmlRenderer : BaseRenderer
{
protected sealed override void RenderHeader()
{
// Ghi đè này là cuối cùng — không cho phép ghi đè thêm
}
}
Seal theo mặc định

Đánh dấu các class là sealed trừ khi bạn cố ý thiết kế cho kế thừa. Trình biên dịch JIT có thể tối ưu hóa các lệnh gọi virtual trên các kiểu sealed (devirtualization — hủy ảo hóa). Các kiểu sealed phổ biến: string, Exception.


Q3: Nêu tất cả các bổ từ truy cập (Access Modifiers) cho kiểu

Bổ từ (Modifier)Cùng ClassLớp dẫn xuất (Derived Class)Cùng AssemblyAssembly bên ngoài
public
privateKhôngKhôngKhông
protectedKhôngKhông
internalKhôngKhông
protected internalKhông
private protectedCó (cùng assembly)KhôngKhông

Kiểu cấp cao nhất (Top-level types, không lồng nhau) chỉ có thể là public hoặc internal. Mặc định là internal.


Q4: Sự khác biệt giữa interface và abstract class là gì?

Tính năngAbstract ClassInterface
ConstructorKhông
Trường / trạng thái (Fields / state)Không (chỉ static fields, C# 8+)
Cài đặt mặc định (Default implementation)Có (C# 8+, qua DIM)
Đa kế thừa (Multiple inheritance)Không (kế thừa đơn class)Có (nhiều interface)
Bổ từ truy cập (Access modifiers)Bất kỳpublic (ngầm định)
Khi nào dùngQuan hệ "Là một" (Is-a) với logic dùng chungHợp đồng khả năng "Có thể làm" (Can-do)
// Abstract class — trạng thái và logic dùng chung
public abstract class Vehicle
{
public string Model { get; set; } // trạng thái
public void Start() => Console.WriteLine("Engine started"); // phương thức cụ thể
public abstract double CalculateFuelEfficiency(); // phải ghi đè
}

// Interface — hợp đồng thuần túy
public interface IWriter
{
void WriteFile();
// Cài đặt mặc định C# 8+
void Log() => Console.WriteLine("Writing...");
}

Kể từ C# 8, interface có thể có cài đặt mặc định (Default Implementation), nhưng vẫn không thể chứa trạng thái instance (không có instance fields).


Q5: Khi nào static constructor được gọi?

Static constructor chạy một lần cho mỗi AppDomain, theo cách lười biếng (lazily) — trước khi instance đầu tiên được tạo hoặc bất kỳ static member nào được truy cập. Nó không bao giờ được gọi trực tiếp.

public class Configuration
{
public static int InstanceCount { get; private set; }

static Configuration()
{
InstanceCount = 0;
Console.WriteLine("Static ctor ran");
}

public Configuration() => InstanceCount++;
}

var c1 = new Configuration(); // "Static ctor ran" in ra ở đây
var c2 = new Configuration(); // Không có output — static ctor đã chạy rồi

Quy tắc:

  • Không có bổ từ truy cập (Access modifiers), không có tham số
  • Chỉ chạy một lần cho mỗi AppDomain
  • Được gọi lười biếng (trước lần sử dụng đầu tiên)
  • An toàn theo luồng (Thread-safe) — CLR đảm bảo chỉ một luồng thực thi nó

Q6: Cách tạo extension method?

Extension method thêm phương thức mới vào các kiểu hiện có mà không sửa đổi chúng. Chúng phải được định nghĩa trong một static class và tham số đầu tiên sử dụng từ khóa this:

public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
=> string.IsNullOrEmpty(str);

public static string Truncate(this string str, int maxLength)
=> str.Length <= maxLength ? str : str[..maxLength];
}

// Cách sử dụng — hiển thị như instance method
string name = "CommitToMemory";
var shortName = name.Truncate(6); // "Commit"
bool empty = "".IsNullOrEmpty(); // true

Quy tắc:

  • Phải nằm trong static class ở cấp namespace
  • Tham số đầu tiên phải có bổ từ this
  • Không thể truy cập private members của kiểu được mở rộng
  • Instance method luôn được ưu tiên hơn extension method có cùng chữ ký (signature)

Q7: C# có hỗ trợ đa kế thừa class không?

Không. C# chỉ hỗ trợ kế thừa đơn class (Single Class Inheritance) — một class chỉ có thể có đúng một base class trực tiếp. Tuy nhiên, một class có thể implement nhiều interface:

public class Base { }
public interface IReader { void Read(); }
public interface IWriter { void Write(); }

// Kế thừa đơn class + nhiều interface
public class Document : Base, IReader, IWriter
{
public void Read() { }
public void Write() { }
}

Lý do: đa kế thừa class dẫn đến vấn đề kim cương (Diamond Problem) — sự mơ hồ khi hai base class định nghĩa cùng một member. Interface tránh được điều này vì chúng không mang trạng thái cài đặt (trước khi có C# 8 default methods, được xử lý theo cách khác).


Q8: Giải thích boxing và unboxing

Boxing chuyển đổi một kiểu giá trị (Value Type) thành kiểu tham chiếu (Reference Type, lưu trên heap). Unboxing trích xuất kiểu giá trị từ object.

int number = 42;

// Boxing — kiểu giá trị → kiểu tham chiếu (cấp phát heap)
object boxed = number;

// Unboxing — kiểu tham chiếu → kiểu giá trị (ép kiểu tường minh)
int unboxed = (int)boxed;
// Boxing ẩn trong code hàng ngày
ArrayList list = new(); // lưu object — boxing kiểu giá trị
list.Add(42); // boxing!
int val = (int)list[0]; // unboxing

// Generic collections tránh hoàn toàn boxing
List<int> genericList = new();
genericList.Add(42); // không boxing — lưu dưới dạng int
Tác động hiệu suất (Performance Impact)

Boxing cấp phát trên heap và gây áp lực GC (GC Pressure). Tránh bằng cách sử dụng generic collections (List<T> thay vì ArrayList) và generic interfaces (IEnumerable<T> thay vì IEnumerable).


Q9: Heap và Stack là gì?

Khía cạnhStackHeap
Lưu trữKiểu giá trị (Value types), khung phương thức (Method frames), tham chiếuĐối tượng (Object — instance của kiểu tham chiếu)
Tốc độRất nhanh (CPU stack pointer)Chậm hơn (truy cập gián tiếp)
Vòng đời (Lifetime)Tự động — giải phóng khi phương thức trả vềĐược quản lý bởi GC
Kích thướcNhỏ (~1 MB mỗi luồng)Lớn (nhiều GB sẵn có)
An toàn luồng (Thread safety)Mỗi luồng riêngChia sẻ giữa các luồng
void Example()
{
int x = 10; // x nằm trên stack
string s = "hello"; // tham chiếu 's' trên stack, object "hello" trên heap
var p = new Person(); // tham chiếu 'p' trên stack, object Person trên heap
} // khung stack được gỡ — các tham chiếu x, s, p biến mất; object Person tồn tại cho đến khi GC thu gom

Kiểu giá trị (int, double, bool, struct, enum) thường được lưu trên stack (trừ khi bị closure bắt, nằm trong class, hoặc bị boxing). Kiểu tham chiếu (class, string, array, delegate) luôn được lưu trên heap — biến giữ một tham chiếu (con trỏ) đến object trên heap.


Q10: Sự khác biệt giữa stringStringBuilder là gì?

stringbất biến (immutable) — mọi sửa đổi đều tạo ra một string object mới. StringBuilderkhả biến (mutable) — nó sửa đổi một buffer duy nhất, hiệu quả cho việc nối chuỗi nhiều lần.

// string — mỗi + tạo ra một cấp phát mới
string result = "";
for (int i = 0; i < 1000; i++)
result += i.ToString(); // 1000 lần cấp phát string mới!

// StringBuilder — sửa đổi một buffer
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i.ToString()); // không có cấp phát trung gian
string final = sb.ToString();
Tính năngstringStringBuilder
Khả biến (Mutability)Bất biến (Immutable)Khả biến (Mutable)
Nối chuỗi (Concatenation)Tạo object mới mỗi lầnNối thêm vào cùng buffer
Hiệu suấtChậm khi nhiều thao tácNhanh khi nhiều thao tác
An toàn luồng (Thread safety)Tự nhiên an toàn theo luồngKhông an toàn theo luồng
Trường hợp sử dụngÍt thao tác, so sánhVòng lặp, nhiều lần nối chuỗi
Sử dụng StringBuilder khi bạn có hơn ~3-4 lần nối chuỗi trong vòng lặp. Đối với nối chuỗi đơn giản một lần, string là ổn và trình biên dịch tối ưu + thành string.Concat.

Q11: Sự khác biệt giữa toán tử isas là gì?

Cả hai đều liên quan đến kiểm tra kiểu tại runtime, nhưng phục vụ mục đích khác nhau:

Tính năngisas
Giá trị trả vềbool (kết quả kiểm tra kiểu)Tham chiếu (object đã ép kiểu hoặc null)
Ném ngoại lệKhông bao giờKhông bao giờ
Kiểu giá trịHỗ trợ (Pattern matching C# 7+)Chỉ tham chiếu/boxing/unboxing
Pattern matchingCó (is int x, is string s)Không
object obj = "Hello";

// is — kiểm tra kiểu, trả về bool
if (obj is string) { /* true */ }

// is với pattern matching (C# 7+) — kiểm tra VÀ trích xuất
if (obj is string str)
Console.WriteLine(str.Length); // 5

// as — thử ép kiểu, trả về null nếu thất bại
string? s = obj as string; // "Hello"
int? n = obj as int?; // null — int là kiểu giá trị, phải dùng nullable

// typeof — lấy object Type tại thời điểm biên dịch
Type t = typeof(string);
Nên dùng is với pattern matching

Sử dụng if (obj is string s) thay vì var s = obj as string; if (s != null) — ngắn gọn hơn và hoạt động với kiểu giá trị.


Q12: Sự khác biệt giữa liên kết sớm (Early Binding) và liên kết muộn (Late Binding) là gì?

Liên kết sớm (Early Binding) phân giải lệnh gọi phương thức tại thời điểm biên dịch (Compile Time). Trình biên dịch biết chính xác kiểu và xác thực sự tồn tại của phương thức. Liên kết muộn (Late Binding) phân giải tại runtime — phương thức thực tế được xác định khi code thực thi.

// Liên kết sớm — trình biên dịch biết kiểu và phương thức
string name = "Alice";
int len = name.Length; // phân giải tại thời điểm biên dịch

// Liên kết muộn qua dynamic — phân giải tại runtime
dynamic value = "Hello";
int length = value.Length; // phân giải tại runtime, không có kiểm tra tại thời điểm biên dịch

// Liên kết muộn qua reflection — phân giải tại runtime
var type = obj.GetType();
var method = type.GetMethod("DoWork");
method?.Invoke(obj, null);

// Liên kết muộn qua virtual method — phân phối tại runtime
Animal animal = new Dog();
animal.Speak(); // Dog.Speak() được phân giải tại runtime (nhưng kiểu được kiểm tra tại thời điểm biên dịch)
Tính năngLiên kết sớm (Early Binding)Liên kết muộn (Late Binding)
Phân giải (Resolution)Thời điểm biên dịch (Compile time)Runtime
Hiệu suấtNhanh hơn (lệnh gọi trực tiếp)Chậm hơn (chi phí tìm kiếm)
An toàn kiểu (Type safety)Bắt lỗi tại thời điểm biên dịchCó thể lỗi tại runtime
Hỗ trợ IDEIntelliSense đầy đủKhông có
Ví dụLệnh gọi phương thức thông thường, phương thức nạp chồngdynamic, reflection, phân phối virtual
Nên dùng liên kết sớm (Early Binding)

Sử dụng liên kết sớm bất cứ khi nào có thể để đảm bảo an toàn kiểu (Type Safety), hiệu suất và hỗ trợ IDE. Chỉ sử dụng liên kết muộn (dynamic, reflection) khi kiểu thực sự không biết tại thời điểm biên dịch (ví dụ: COM interop, hệ thống plugin).

Collections & LINQ

Q13: Cách tạo ngày tháng với múi giờ cụ thể?

Sử dụng TimeZoneInfo để chuyển đổi DateTime sang múi giờ cụ thể, hoặc sử dụng DateTimeOffset cho các giá trị thời điểm không mơ hồ (Unambiguous Point-in-time):

// Sử dụng TimeZoneInfo
var tz = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time"); // Windows ID
var utcNow = DateTime.UtcNow;
var localDate = TimeZoneInfo.ConvertTimeFromUtc(utcNow, tz);

// Sử dụng DateTimeOffset — lưu ngày + giờ + offset
var dto = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.FromHours(7)); // UTC+7
Console.WriteLine(dto); // 6/15/2025 10:30:00 AM +07:00
Console.WriteLine(dto.UtcDateTime); // 6/15/2025 3:30:00 AM

// Cross-platform timezone IDs (IANA) — sử dụng TimeZoneInfo trên .NET 6+
var ianaTz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Ho_Chi_Minh");
Nên dùng DateTimeOffset thay vì DateTime

DateTime một mình là mơ hồ — bạn không biết nó là local, UTC, hay không xác định (Unspecified). DateTimeOffset luôn kèm offset, an toàn cho tuần tự hóa (Serialization), API và các tình huống liên múi giờ (Cross-timezone).


Q14: Cách thay đổi culture hiện tại?

Sử dụng CultureInfo.CurrentCulture để định dạng/phân tích (Formatting/Parsing) và CultureInfo.CurrentUICulture để tra cứu tài nguyên (Resource Lookup):

using System.Globalization;

// Thay đổi culture cho luồng hiện tại
CultureInfo.CurrentCulture = new CultureInfo("vi-VN");
CultureInfo.CurrentUICulture = new CultureInfo("vi-VN");

Console.WriteLine(1234.56.ToString("N2")); // "1 234,56" (định dạng tiếng Việt)

// Thay đổi culture trong phạm vi (khôi phục sau khi sử dụng)
using var _ = new CultureScope(new CultureInfo("en-US"));
Console.WriteLine(1234.56.ToString("N2")); // "1,234.56"

// Lớp helper đơn giản
public class CultureScope : IDisposable
{
private readonly CultureInfo _original = CultureInfo.CurrentCulture;
public CultureScope(CultureInfo culture) => CultureInfo.CurrentCulture = culture;
public void Dispose() => CultureInfo.CurrentCulture = _original;
}

Q15: Sự khác biệt giữa HashSet<T>Dictionary<TKey, TValue> là gì?

Tính năngHashSet<T>Dictionary<TKey, TValue>
Lưu trữChỉ giá trị duy nhất (Unique values)Cặp khóa-giá trị (Key-value pairs)
Tìm kiếmTheo giá trị (Trung bình O(1))Theo khóa (Trung bình O(1))
Trùng lặp (Duplicates)Bỏ qua âm thầmNém ngoại lệ khi khóa trùng
Trường hợp sử dụngKiểm tra thành viên (Membership testing), phép toán tập hợpÁnh xạ khóa đến giá trị
// HashSet — các mục duy nhất, phép toán tập hợp
var tags = new HashSet<string> { "csharp", "dotnet", "linq" };
tags.Add("csharp"); // bị bỏ qua — đã tồn tại
tags.Add("async"); // được thêm
bool has = tags.Contains("dotnet"); // true

// Dictionary — ánh xạ khóa → giá trị
var ages = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};
ages["Alice"] = 31; // cập nhật existing
ages["Charlie"] = 28; // thêm mới

Q16: Mục đích của phương thức ToLookup là gì?

ToLookup tạo một từ điển một-nhiều (ILookup<TKey, TElement>) trong đó mỗi khóa có thể ánh xạ đến nhiều giá trị. Khác với GroupBy (trì hoãn — Deferred), ToLookup thực thi ngay lập tức (Immediately):

var students = new[]
{
new { Name = "Alice", Grade = "A" },
new { Name = "Bob", Grade = "B" },
new { Name = "Charlie", Grade = "A" },
new { Name = "Diana", Grade = "B" },
};

// ToLookup — thực thi ngay lập tức, một-nhiều
ILookup<string, string> byGrade = students.ToLookup(s => s.Grade, s => s.Name);

foreach (var group in byGrade)
{
Console.WriteLine($"Grade {group.Key}: {string.Join(", ", group)}");
}
// Grade A: Alice, Charlie
// Grade B: Bob, Diana

// Tra cứu theo khóa — trả về chuỗi rỗng nếu không tìm thấy khóa (không có KeyNotFoundException)
var gradeC = byGrade["C"]; // chuỗi rỗng, không phải lỗi
Tính năngGroupByToLookup
Thực thi (Execution)Trì hoãn (Deferred)Ngay lập tức (Immediate)
Kiểu trả vềIEnumerable<IGrouping>ILookup<TKey, TElement>
Liệt kê lại (Re-enumeration)Thực thi lại truy vấnKhông (đã cache)
Khóa thiếu (Missing key)Không áp dụngTrả về chuỗi rỗng

Q17: Phương thức LINQ Cast<T> có tạo object mới không?

Không. Cast<T> chỉ thực hiện chuyển đổi tham chiếu (Reference Conversion) — nó ép kiểu từng phần tử thành T mà không tạo object mới. Nếu việc ép kiểu thất bại tại runtime, nó ném InvalidCastException.

var list = new ArrayList { "a", "b", "c" };

// Cast<string> — không có object mới, chỉ là ép kiểu tham chiếu
IEnumerable<string> strings = list.Cast<string>();

// Cái này ném lỗi — không thể ép int thành string
var mixed = new ArrayList { "a", 1, "b" };
var result = mixed.Cast<string>().ToList(); // InvalidCastException tại phần tử 1
Sử dụng OfType<T> để lọc thay vì ném lỗi

OfType<T> lọc ra các phần tử không thể ép kiểu, thay vì ném ngoại lệ:

var mixed = new ArrayList { "a", 1, "b", 2 };
var strings = mixed.OfType<string>().ToList(); // ["a", "b"] — không có ngoại lệ

Q18: Giải thích thực thi trì hoãn (Deferred Execution) trong LINQ

Thực thi trì hoãn (Deferred Execution) có nghĩa là truy vấn không được đánh giá cho đến khi bạn thực sự liệt kê (Enumerate) nó. Hầu hết các toán tử LINQ (Where, Select, OrderBy, GroupBy) đều trì hoãn. Các toán tử như ToList(), ToArray(), Count(), First() ép buộc thực thi ngay lập tức (Immediate).

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Trì hoãn — truy vấn được định nghĩa nhưng CHƯA thực thi
var query = numbers.Where(n =>
{
Console.WriteLine($"Filtering {n}");
return n > 2;
});

// Chưa in gì — không có thực thi

// Ngay lập tức — ToList() ép buộc liệt kê
var result = query.ToList();
// Filtering 1, Filtering 2, Filtering 3, Filtering 4, Filtering 5

// Mỗi lần liệt kê lại thực thi lại truy vấn!
numbers.Add(6);
var result2 = query.ToList(); // Lọc lại tất cả 6 phần tử
Bẫy thực thi lại (Re-execution Trap)

Các truy vấn trì hoãn được đánh giá lại mỗi lần liệt kê. Nếu nguồn thay đổi giữa các lần liệt kê, bạn sẽ nhận được kết quả khác nhau. Cache bằng ToList() hoặc ToArray() nếu cần kết quả ổn định.


Q19: ImmutableList hoạt động như thế nào?

ImmutableList<T> là một collection không bao giờ sửa đổi chính nó — mọi "thay đổi" đều trả về một instance mới trong khi chia sẻ dữ liệu không thay đổi bên trong (chia sẻ cấu trúc — Structural Sharing):

var list = ImmutableList.Create(1, 2, 3);

// Mỗi thao tác trả về một danh sách MỚI — danh sách gốc không thay đổi
var list2 = list.Add(4); // list2 = [1, 2, 3, 4], list = [1, 2, 3]
var list3 = list2.Remove(2); // list3 = [1, 3, 4], list2 = [1, 2, 3, 4]
var list4 = list3.SetItem(0, 99); // list4 = [99, 3, 4]

// Builder — hiệu quả cho thay đổi hàng loạt
var builder = list.ToBuilder();
builder.Add(4);
builder.Add(5);
var result = builder.ToImmutable(); // [1, 2, 3, 4, 5]
Tính năngList<T>ImmutableList<T>
Khả biến (Mutability)Khả biếnBất biến (trả về mới)
Add/RemoveO(1) khấu haoO(log n) — cấu trúc cây
An toàn luồng (Thread safety)KhôngTự nhiên an toàn theo luồng
Trường hợp sử dụngMục đích chungPipeline hàm, trạng thái chia sẻ
Sử dụng ImmutableArray<T> cho các collection nhỏ, đọc nhiều — có truy cập đọc O(1) (bộ nhớ liền kề — Contiguous Memory). Sử dụng ImmutableList<T> khi cần "thay đổi" hiệu quả thông qua chia sẻ cấu trúc (Structural Sharing).

Q20: Sự khác biệt giữa Array.CopyTo()Array.Clone() là gì?

Cả hai đều sao chép các phần tử mảng, nhưng khác nhau về đích đến và độ sâu:

Tính năngCopyTo()Clone()
Đích đến (Destination)Sao chép vào một mảng đã tồn tạiTạo một mảng mới
Chỉ số bắt đầuCó thể chỉ định chỉ số bắt đầuLuôn bắt đầu từ 0
Kiểu trả vềvoidobject (phải ép kiểu)
Mảng phải tồn tạiCó (phải được cấp phát trước)Không (tạo mới)
int[] source = [1, 2, 3, 4, 5];

// Clone() — tạo mảng mới, trả về object (sao chép nông — Shallow Copy)
int[] cloned = (int[])source.Clone();
// cloned = [1, 2, 3, 4, 5] — mảng mới

// CopyTo() — sao chép vào mảng đã tồn tại
int[] target = new int[5];
source.CopyTo(target, 0);
// target = [1, 2, 3, 4, 5]

// CopyTo với offset
int[] target2 = new int[7];
source.CopyTo(target2, 2);
// target2 = [0, 0, 1, 2, 3, 4, 5] — bắt đầu từ chỉ số 2
Cả hai đều thực hiện sao chép nông (Shallow Copy) — đối với kiểu tham chiếu, chỉ các tham chiếu được sao chép, không phải các object本身. Sử dụng LINQ Select(x => ...) hoặc logic sao chép sâu thủ công khi cần sao chép sâu thực sự (Deep Copy).

Q21: Sự khác biệt giữa ArrayArrayList là gì?

Tính năngArrayArrayList
An toàn kiểu (Type safety)Kiểu mạnh (T[])Lưu object (không an toàn kiểu)
BoxingKhôngCó (kiểu giá trị bị boxing thành object)
Kích thướcCố định khi tạoĐộng (tự động thay đổi)
NamespaceSystemSystem.Collections
Hiệu suấtNhanh hơn (không ép kiểu/boxing)Chậm hơn (ép kiểu + boxing)
Thay thế hiện đạiT[]List<T>
// Array — an toàn kiểu, kích thước cố định
int[] arr = new int[3];
arr[0] = 1;
// arr[0] = "text"; // Lỗi biên dịch — an toàn kiểu

// ArrayList — lưu object, kích thước động
ArrayList list = new ArrayList();
list.Add(1); // boxing: int → object
list.Add("text"); // cho phép kiểu hỗn hợp (không an toàn kiểu)
int val = (int)list[0]; // unboxing: object → int

// Thay thế hiện đại — List<T>
List<int> generic = new List<int>();
generic.Add(1);
// generic.Add("text"); // Lỗi biên dịch — an toàn kiểu
Luôn sử dụng List<T> thay vì ArrayList. ArrayList là kiểu遗留 (Legacy) từ thời trước generics (C# 1.x). List<T> cung cấp an toàn kiểu, tránh boxing và có hiệu suất tốt hơn.

Q22: Sự khác biệt giữa SortedListSortedDictionary là gì?

Cả hai đều duy trì các cặp khóa-giá trị được sắp xếp theo khóa, nhưng với cấu trúc bên trong và đặc điểm hiệu suất khác nhau:

Tính năngSortedList<TKey, TValue>SortedDictionary<TKey, TValue>
Cấu trúc bên trongMảng đã sắp xếp (liền kề — Contiguous)Cây đỏ-đen (Red-black tree)
Bộ nhớÍt bộ nhớ hơn (mảng nhỏ gọn)Nhiều bộ nhớ hơn (các node cây)
Tìm kiếm theo khóaO(log n) — tìm kiếm nhị phân (Binary Search)O(log n) — duyệt cây
Chèn dữ liệu chưa sắp xếpO(n) — dịch chuyển mảngO(log n) — cân bằng lại cây
Chèn dữ liệu đã sắp xếpO(1) khấu hao — nối thêmO(log n)
Lấy min/maxO(1) — phần tử đầu/cuốiO(log n)
Liệt kê (Enumeration)Nhanh (bộ nhớ liền kề)Chậm hơn (duyệt cây)
// SortedList — nhỏ gọn, nhanh cho tìm kiếm trên dữ liệu tĩnh
var sortedList = new SortedList<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Các khóa luôn được sắp xếp: Alice, Bob, Charlie

// SortedDictionary — chèn hiệu quả cho dữ liệu động
var sortedDict = new SortedDictionary<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Kết quả giống nhau, nhưng nhanh hơn cho việc chèn thường xuyên
Sử dụng SortedList khi bạn điền dữ liệu một lần và truy vấn thường xuyên (ví dụ: bảng tra cứu). Sử dụng SortedDictionary khi dữ liệu thường xuyên được chèn/xóa. Đối với hầu hết trường hợp, Dictionary thông thường với OrderBy tại thời điểm truy vấn đơn giản và đủ dùng.

Kiểu C# hiện đại (Modern C# Types)

Q23: Giải thích Kế thừa (Inheritance) vs Cấu thành (Composition)

Kế thừa (Inheritance) ("là một" — is-a) tạo ra mối quan hệ kiểu con (Subtype Relationship). Cấu thành (Composition) ("có một" — has-a) xây dựng kiểu bằng cách kết hợp các thành phần nhỏ, tập trung.

// Kế thừa — "Worker là một Person"
public class Person { public string Name { get; set; } }
public class Worker : Person { public string Company { get; set; } }

// Cấu thành — "Car có một Engine"
public class Engine { public int HorsePower { get; set; } }
public class Car
{
private readonly Engine _engine; // Car CÓ MỘT Engine
public Car(Engine engine) => _engine = engine;
}

Nên ưu tiên cấu thành (Composition) vì:

  • Tránh được hệ thống phân cấp kế thừa sâu, dễ vỡ
  • Hỗ trợ thay đổi hành vi tại runtime (hoán đổi thành phần)
  • Tuân theo Nguyên tắc Trách nhiệm đơn (Single Responsibility Principle)
  • Cho phép nhiều "khả năng" thông qua cấu thành (so với kế thừa đơn)
Cấu thành ưu tiên hơn Kế thừa (Composition over Inheritance)

Kế thừa nên mô hình hóa mối quan hệ "là một" thực sự. Nếu mối quan hệ là "có một" hoặc "có thể làm", hãy sử dụng cấu thành với interface.


Q24: Sự khác biệt giữa class, record, và struct

Tính năngclassrecordstruct
Loại kiểuTham chiếu (Heap)Tham chiếu (Heap)Giá trị (Stack)
Khả biến (Mutability)Khả biến theo mặc địnhBất biến theo mặc địnhKhả biến theo mặc định
So sánh bằng (Equality)So sánh tham chiếu (Reference Equality)So sánh theo giá trị (Value-based Equality)So sánh theo giá trị (Value-based Equality)
Kế thừa (Inheritance)Có (đơn)Có (đơn)Không (có thể implement interface)
Biểu thức withKhôngCó (C# 10+)
Tự động tạoKhông gìEquals, GetHashCode, ToString, withKhông gì
// class — kiểu tham chiếu, so sánh tham chiếu
public class PointClass { public int X { get; set; } public int Y { get; set; } }

// record — kiểu tham chiếu, so sánh theo giá trị, thân thiện với bất biến
public record PointRecord(int X, int Y);

// struct — kiểu giá trị, cấp phát trên stack
public struct PointStruct { public int X { get; set; } public int Y { get; set; } }

var r1 = new PointRecord(1, 2);
var r2 = new PointRecord(1, 2);
Console.WriteLine(r1 == r2); // True — so sánh theo giá trị
Console.WriteLine(ReferenceEquals(r1, r2)); // False — các tham chiếu khác nhau

var c1 = new PointClass { X = 1, Y = 2 };
var c2 = new PointClass { X = 1, Y = 2 };
Console.WriteLine(c1 == c2); // False — so sánh tham chiếu

Q25: ref struct được sử dụng để làm gì?

ref struct là một struct được đảm bảo chỉ tồn tại trên stack — nó không bao giờ thoát lên heap. Điều này cho phép các thao tác an toàn, không cấp phát với con trỏ và span:

public ref struct SpanReader<T>
{
private readonly ReadOnlySpan<T> _span;
private int _position;

public SpanReader(ReadOnlySpan<T> span)
{
_span = span;
_position = 0;
}

public bool TryRead(out T value)
{
if (_position >= _span.Length) { value = default; return false; }
value = _span[_position++];
return true;
}
}

Hạn chế — một ref struct không thể:

  • Bị boxing (không ép kiểu thành object, dynamic, hoặc interface)
  • Là trường của một class thông thường
  • Implement interface
  • Được sử dụng trong phương thức async hoặc lambda (trừ khi ref-safe)
  • Bị bắt bởi closure

Trường hợp sử dụng chính là Span<T>ReadOnlySpan<T> — cắt mảng (Slicing) hiệu năng cao, không cấp phát trên mảng, bộ nhớ native, hoặc buffer stackalloc.


Q26: Nêu hai dạng của record

  1. Positional record — constructor chính định nghĩa các thuộc tính:
public record Person(string Name, int Age);

// Trình biên dịch tự động tạo: thuộc tính init-only, constructor, Deconstruct()
var p = new Person("Alice", 30);
var (name, age) = p; // Deconstruct
  1. Record với cú pháp tiêu chuẩn — bạn tự định nghĩa các thuộc tính:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}

Cả hai dạng đều tạo ra cùng các Equals, GetHashCode, ToString, và hỗ trợ with do trình biên dịch tạo. Dạng positional ngắn gọn hơn; dạng tiêu chuẩn cho bạn nhiều kiểm soát hơn đối với định nghĩa thuộc tính.


Q27: Từ khóa with được sử dụng để làm gì?

with tạo một bản sao của record (hoặc struct, C# 10+) với một hoặc nhiều thuộc tính được thay đổi — đột biến không phá hủy (Non-destructive Mutation):

public record Person(string Name, int Age, string City);

var alice = new Person("Alice", 30, "Hanoi");
var bob = alice with { Name = "Bob", Age = 25 };

Console.WriteLine(alice); // Person { Name = Alice, Age = 30, City = Hanoi }
Console.WriteLine(bob); // Person { Name = Bob, Age = 25, City = Hanoi }

Bên dưới, trình biên dịch tạo một copy constructor và sau đó áp dụng các gán with. Object gốc không bao giờ bị sửa đổi.


Q28: Mục đích của Primary Constructors là gì?

Primary constructors (C# 12) cho phép bạn khai báo các tham số constructor trực tiếp trên khai báo class/struct. Các tham số có sẵn trong toàn bộ thân class:

// Trước C# 12 — constructor tường minh + trường
public class Service
{
private readonly IRepository _repo;
private readonly ILogger _logger;

public Service(IRepository repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
}

// C# 12 — primary constructor
public class Service(IRepository repo, ILogger logger)
{
public void DoWork()
{
repo.GetAll(); // có sẵn ở mọi nơi
logger.Log("Working");
}
}
Tham số primary constructor không được lưu dưới dạng trường

Nếu bạn cần bắt chúng (ví dụ: cho khởi tạo lười hoặc thao tác khả biến), hãy gán chúng vào trường hoặc thuộc tính tường minh. Các tham số chỉ được sử dụng trong quá trình khởi tạo thì giữ nguyên là ổn.


Q29: Giải thích cách hoạt động của Nullable Reference Types

Nullable Reference Types (NRT), được bật trong C# 8+, làm cho khả năng null của kiểu tham chiếu trở nên tường minh tại thời điểm biên dịch. Trình biên dịch cảnh báo khi một tham chiếu non-nullable có thể là null:

#nullable enable

string name = "Alice"; // non-nullable — không thể là null
string? nickname = null; // nullable — có thể là null

// Cảnh báo của trình biên dịch
name = null; // Cảnh báo: Converting null to non-nullable type
int length = nickname.Length; // Cảnh báo: Possible null reference

// Kiểm tra phòng thủ (Defensive checks)
if (nickname is not null)
length = nickname.Length; // OK — trình biên dịch biết nó không null

// Toán tử bỏ qua null (!)
length = nickname!.Length; // "Tin tôi đi, nó không null" — triệt tiêu cảnh báo

Điểm chính:

  • NRT là một tính năng thời điểm biên dịch (Compile-time Feature) — không thay đổi hành vi runtime
  • Được bật cho từng dự án qua <Nullable>enable</Nullable> trong .csproj
  • Chữ ký phương thức truyền đạt ý định: string Get() vs string? Find()

Q30: Switch expression có hạn chế về kiểu trả về không?

Không. Switch expression có thể trả về bất kỳ kiểu nào, nhưng tất cả các nhánh (Arms) phải trả về cùng kiểu (hoặc một kiểu mà tất cả nhánh có thể chuyển đổi ngầm định sang):

// Trả về string
string category = age switch
{
< 13 => "Child",
< 20 => "Teenager",
< 65 => "Adult",
_ => "Senior"
};

// Trả về double
double discount = customerType switch
{
"VIP" => 0.3,
"Regular" => 0.1,
_ => 0.0
};

// Trả về object (kiểu cơ sở chung)
object result = input switch
{
int i => i,
string s => s,
bool b => b,
_ => "unknown"
};

Trình biên dịch suy luận kiểu trả về từ tất cả các nhánh. Nếu các nhánh trả về kiểu không tương thích, đó là lỗi biên dịch.


Q31: yield return được sử dụng để làm gì?

yield return tạo ra một giá trị lười (Lazily) — các phần tử được tạo ra từng cái một khi người gọi liệt kê, mà không tạo toàn bộ collection trước:

// Háo hức (Eager) — xây dựng toàn bộ danh sách trong bộ nhớ
public static List<int> GetEvens(int max)
{
var result = new List<int>();
for (int i = 0; i <= max; i++)
if (i % 2 == 0) result.Add(i);
return result;
}

// Lười (Lazy) — tạo ra từng cái một
public static IEnumerable<int> GetEvensLazy(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0) yield return i;
}

// Cách sử dụng — chỉ tính toán các giá trị khi cần
foreach (var n in GetEvensLazy(1_000_000).Take(5))
Console.WriteLine(n); // 0, 2, 4, 6, 8 — không bao giờ tính toán cả 1 triệu

yield break thoát iterator sớm (tương đương với return từ phương thức thông thường). Trình biên dịch chuyển đổi các phương thức yield return thành state machine.

C# Nâng cao (Advanced C#)

Q32: Garbage Collector có bao nhiêu thế hệ?

.NET GC có 3 thế hệ (0, 1, 2) cộng thêm Large Object Heap (LOH):

Thế hệ (Generation)Lưu trữTần suất thu gomChi phí
Gen 0Object sống ngắn (Short-lived)Thường xuyên nhấtRẻ nhất
Gen 1Sống sót qua Gen 0Vừa phảiVừa phải
Gen 2Object sống lâu (Long-lived)Ít thường xuyên nhấtĐắt nhất
LOHObject >= 85,000 byteCùng với Gen 2Rất đắt
// Sống ngắn → Gen 0, thu gom nhanh
void Process()
{
var temp = new byte[100]; // Gen 0
} // đủ điều kiện cho thu gom Gen 0

// Sống lâu → sống sót đến Gen 2
static byte[] cache = new byte[100]; // được thăng cấp lên Gen 2

// Lớn → LOH (85KB+)
var large = new byte[100_000]; // đi thẳng vào LOH

Cách thăng cấp (Promotion) hoạt động: Các object sống sót qua lần thu gom Gen 0 được thăng cấp lên Gen 1. Gen 1 sống sót chuyển lên Gen 2. GC sử dụng giả thuyết thế hệ (Generational Hypothesis) — object mới chết trẻ, nên thu gom Gen 0 nhanh và thu hồi nhiều bộ nhớ nhất.


Q33: Lớp Interlocked được sử dụng để làm gì?

Interlocked cung cấp các thao tác nguyên tử, an toàn theo luồng (Atomic, Thread-safe) trên biến — mà không cần lock tường minh. Nó nhanh hơn lock cho các thao tác số đơn giản:

private static int _counter = 0;

// Tăng an toàn theo luồng (nguyên tử)
Interlocked.Increment(ref _counter); // _counter++
Interlocked.Decrement(ref _counter); // _counter--

// Cộng nguyên tử
Interlocked.Add(ref _counter, 5);

// Trao đổi nguyên tử (đặt và trả về giá trị cũ)
int old = Interlocked.Exchange(ref _counter, 0); // đặt lại và lấy giá trị cũ

// So sánh-và-hoán đổi (CAS — Compare-and-Swap)
int expected = 10;
Interlocked.CompareExchange(ref _counter, 20, expected); // nếu _counter==10, đặt thành 20
Sử dụng Interlocked cho các thao tác nguyên tử đơn giản (tăng, trao đổi). Đối với các vùng tới hạn (Critical Sections) phức tạp nhiều bước, hãy sử dụng lock hoặc SemaphoreSlim.

Q34: Code được trình biên dịch tạo ra cho auto-property là gì?

Một auto-property như public string Name { get; set; } được trình biên dịch mở rộng thành một thuộc tính với trường nền ẩn (Hidden Backing Field):

// Những gì bạn viết
public class Person
{
public string Name { get; set; }
public int Age { get; init; }
}

// Những gì trình biên dịch tạo ra (IL đơn giản hóa → tương đương C#)
public class Person
{
// Trường nền ẩn — tên do trình biên dịch tạo
[CompilerGenerated]
private string <Name>k__BackingField;
[CompilerGenerated]
private int <Age>k__BackingField;

public string Name
{
get => <Name>k__BackingField;
set => <Name>k__BackingField = value;
}

public int Age
{
get => <Age>k__BackingField;
init => <Age>k__BackingField = value; // init-only setter
}
}

Tên trường nền bao gồm dấu ngoặc nhọn để tránh xung đột đặt tên với code người dùng.


Q35: Đa hình (Polymorphism) được triển khai trong C# như thế nào?

Đa hình trong C# được thực hiện thông qua:

  1. Đa hình kiểu con (Subtype Polymorphism)virtual/override (phân phối tại runtime):
public class Animal
{
public virtual string Speak() => "...";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
Animal a = new Dog();
a.Speak(); // "Woof!" — runtime phân phối đến implementation của Dog
  1. Đa hình interface (Interface Polymorphism) — nhiều kiểu implement cùng interface:
public interface IDrawable { void Draw(); }
public class Circle : IDrawable { public void Draw() => Console.WriteLine("Circle"); }
public class Square : IDrawable { public void Draw() => Console.WriteLine("Square"); }

void Render(IDrawable shape) => shape.Draw(); // hoạt động với bất kỳ IDrawable
  1. Đa hình tùy ý (Ad-hoc Polymorphism) — nạp chồng phương thức (Method Overloading, tại thời điểm biên dịch):
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;

Q36: Đóng gói (Encapsulation) được triển khai trong C# như thế nào?

Đóng gói được thực hiện bằng cách ẩn trạng thái bên trong với bổ từ truy cập (Access Modifiers) và phơi bày truy cập được kiểm soát thông qua thuộc tính (Properties)phương thức (Methods):

public class BankAccount
{
private decimal _balance; // trạng thái bên trong ẩn

public decimal Balance => _balance; // phơi bày chỉ đọc

public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Must be positive");
_balance += amount;
}

public bool Withdraw(decimal amount)
{
if (amount > _balance) return false;
_balance -= amount;
return true;
}
}

Cơ chế: trường private, thuộc tính public, private set, setter init, trường readonly, và kiểu phạm vi file (File-scoped Types).


Q37: Sự khác biệt giữa tham số refout là gì?

Cả hai đều truyền đối số tham chiếu (By Reference), nhưng khác nhau về yêu cầu khởi tạo:

Tính năngrefout
Người gọi phải khởi tạo (Caller must initialize) — trước khi gọiKhông
Phương thức phải gán (Method must assign)Không (có thể đọc giá trị hiện có) — phải gán trước khi trả về
Trường hợp sử dụngTruyền giá trị hiện có để sửa đổiTrả về nhiều giá trị
// ref — người gọi khởi tạo, phương thức có thể sửa đổi
void Double(ref int x) => x *= 2;

int num = 5;
Double(ref num);
Console.WriteLine(num); // 10

// out — phương thức phải gán
bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}

if (TryParse("42", out int value))
Console.WriteLine(value); // 42

Q38: Câu lệnh using hoạt động như thế nào?

using đảm bảo Dispose() được gọi trên object IDisposable, ngay cả khi có ngoại lệ xảy ra:

// Câu lệnh using cổ điển
using (var stream = new FileStream("data.txt", FileMode.Open))
{
// sử dụng stream
} // stream.Dispose() được gọi ở đây — đảm bảo

// Khai báo using (C# 8+) — Dispose tại cuối phạm vi
using var stream = new FileStream("data.txt", FileMode.Open);
// sử dụng stream...
// Dispose được gọi tự động tại cuối phương thức/phạm vi

// Những gì trình biên dịch tạo ra:
var stream = new FileStream("data.txt", FileMode.Open);
try
{
// sử dụng stream
}
finally
{
stream?.Dispose();
}

await using hoạt động tương tự cho IAsyncDisposable:

await using var resource = new AsyncResource();
// DisposeAsync được gọi tại cuối phạm vi

Q39: Delegate là gì và được sử dụng như thế nào?

Delegate là con trỏ hàm an toàn kiểu (Type-safe Function Pointer) — nó giữ tham chiếu đến một phương thức có chữ ký (Signature) khớp:

// Định nghĩa kiểu delegate
public delegate int MathOp(int a, int b);

// Các phương thức khớp chữ ký
int Add(int a, int b) => a + b;
int Multiply(int a, int b) => a * b;

// Cách sử dụng
MathOp op = Add;
Console.WriteLine(op(3, 4)); // 7

op = Multiply;
Console.WriteLine(op(3, 4)); // 12

// Multicast — kết hợp nhiều delegate
MathOp combined = (a, b) => a + b;
combined += (a, b) => a * b;
// Lưu ý: multicast trả về kết quả CUỐI CÙNG

Delegate được xây dựng sẵn:

  • Action — trả về void, tối đa 16 tham số
  • Func<T> — trả về kiểu cụ thể, tối đa 16 tham số
  • Predicate<T> — trả về bool
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

Q40: Giải thích nạp chồng phương thức (Method Overloading) và ghi đè phương thức (Method Overriding)

Nạp chồng (Overloading) — cùng tên phương thức, tham số khác nhau (đa hình thời điểm biên dịch — Compile-time Polymorphism):

public int Sum(int a, int b) => a + b;
public double Sum(double a, double b) => a + b;
public int Sum(int a, int b, int c) => a + b + c;

Ghi đè (Overriding) — thay thế phương thức virtual/abstract của base class trong derived class (đa hình runtime — Runtime Polymorphism):

public class Shape
{
public virtual double Area() => 0;
}

public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}

// Đa hình runtime
Shape shape = new Circle { Radius = 5 };
Console.WriteLine(shape.Area()); // 78.54 — gọi Circle.Area()
Tính năngNạp chồng (Overloading)Ghi đè (Overriding)
Liên kết (Binding)Thời điểm biên dịch (Compile-time)Runtime
Cùng tên
Tham sốPhải khác nhauPhải khớp chính xác
Từ khóaKhông cóvirtual + override
Mối quan hệCùng classBase → Derived

Q41: Sự khác biệt giữa IEnumerable<T>IQueryable<T>

Tính năngIEnumerable<T>IQueryable<T>
NamespaceSystem.Collections.GenericSystem.Linq
Thực thiTrong bộ nhớ (Phía client)Có thể dịch sang nguồn từ xa (ví dụ: SQL)
LọcThực hiện trong C# sau khi tải dữ liệuĐược dịch sang SQL — lọc tại cơ sở dữ liệu
Phù hợp nhất choCollection trong bộ nhớ (Lists, Arrays)Truy vấn cơ sở dữ liệu (EF Core)
// IEnumerable — tải TẤT CẢ sản phẩm, sau đó lọc trong bộ nhớ
IEnumerable<Product> enumerable = context.Products;
var result = enumerable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products (tải tất cả!)

// IQueryable — bộ lọc được dịch sang SQL
IQueryable<Product> queryable = context.Products;
var result = queryable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products WHERE Price > 100 (lọc tại DB)
Luôn sử dụng IQueryable cho truy vấn cơ sở dữ liệu để tránh tải dữ liệu không cần thiết vào bộ nhớ.

Q42: Expression Tree trong LINQ là gì?

Expression tree (Cây biểu thức) biểu diễn code dưới dạng cấu trúc dữ liệu (Cây — Trees) có thể được kiểm tra, sửa đổi và dịch tại runtime. Chúng là nền tảng của IQueryable<T> — các nhà cung cấp LINQ (LINQ Providers) dịch expression tree thành SQL, REST API calls, v.v.

// Lambda dưới dạng delegate — code thực thi được
Func<int, bool> isLarge = x => x > 100;

// Lambda dưới dạng expression tree — cấu trúc dữ liệu mô tả code
Expression<Func<int, bool>> isLargeExpr = x => x > 100;

// Kiểm tra cây
Console.WriteLine(isLargeExpr.Body); // "x > 100"
Console.WriteLine(isLargeExpr.Parameters[0]); // "x"

// Biên dịch và thực thi
Func<int, bool> compiled = isLargeExpr.Compile();
Console.WriteLine(compiled(150)); // True

EF Core sử dụng expression tree để dịch Where(p => p.Price > 100) thành WHERE Price > 100 trong SQL — lambda không bao giờ được thực thi dưới dạng code C#.


Q43: Xử lý ngoại lệ (Exception Handling) hoạt động trong C# như thế nào?

C# sử dụng mô hình xử lý ngoại lệ có cấu trúc với try, catch, finally, và throw:

try
{
var result = int.Parse("abc"); // ném FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
{
// Bộ lọc ngoại lệ (Exception Filter) — chỉ bắt nếu điều kiện đúng
Console.WriteLine("Specific error");
}
finally
{
// Luôn chạy — có hoặc không có ngoại lệ
Console.WriteLine("Cleanup");
}

Điểm chính:

  • Ngoại lệ nổi lên (Bubble Up) call stack cho đến khi bị bắt
  • finally luôn thực thi (ngay cả với return trong try/catch)
  • Bộ lọc ngoại lệ (Exception Filters, when) cho phép kiểm tra mà không cần bắt
  • Các ngoại lệ cụ thể nhất nên được bắt trước tiên

Q44: Nêu tất cả các cách để ném lại ngoại lệ (Rethrow Exception)

  1. throw; — bảo toàn stack trace gốc (được khuyến nghị):
catch (Exception)
{
Log("Something failed");
throw; // bảo toàn stack trace gốc
}
  1. throw ex; — đặt lại stack trace tại điểm này (tránh dùng):
catch (Exception ex)
{
Log("Something failed");
throw ex; // CẢNH BÁO: stack trace bắt đầu từ đây — vị trí gốc bị mất
}
  1. throw new Exception("message", innerEx); — bao bọc với ngữ cảnh:
catch (SqlException ex)
{
throw new DataAccessException("Failed to load user", ex);
// Ngoại lệ gốc được bảo toàn dưới dạng InnerException
}
Luôn sử dụng throw; (throw thuần) để bảo toàn stack trace gốc. Chỉ sử dụng throw ex khi bạn cố ý muốn ẩn nguồn gốc. Luôn bao bọc với inner exception khi thêm ngữ cảnh.

Q45: Giải thích Generics

Generics cho phép bạn viết các class, phương thức, và interface được tham số hóa kiểu (Type-parameterized) — một triển khai hoạt động cho bất kỳ kiểu nào trong khi duy trì an toàn kiểu (Type Safety):

// Generic class
public class Repository<T> where T : class, new()
{
private readonly List<T> _items = new();

public void Add(T item) => _items.Add(item);
public T FindById(int id) => _items[id];
}

// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;

// Generic interface
public interface IService<T>
{
T GetById(int id);
void Save(T entity);
}

// Cách sử dụng — an toàn kiểu, không boxing
var userRepo = new Repository<User>();
userRepo.Add(new User { Name = "Alice" });

int max = Max(10, 20); // kiểu được suy luận: int
string maxStr = Max("a", "z"); // kiểu được suy luận: string

Ràng buộc phổ biến (Common Constraints):

Ràng buộc (Constraint)Ý nghĩa
where T : classT phải là kiểu tham chiếu (Reference Type)
where T : structT phải là kiểu giá trị (Value Type)
where T : new()T phải có constructor không tham số
where T : BaseClassT phải dẫn xuất từ BaseClass
where T : IInterfaceT phải implement IInterface
where T : unmanagedT phải là kiểu không quản lý (Unmanaged Type)

Q46: Sự khác biệt giữa Dispose()Finalize() là gì?

Cả hai đều dọn dẹp tài nguyên, nhưng khác biệt cơ bản về khi nàonhư thế nào chúng được gọi:

Tính năngDispose()Finalize() (Destructor)
Được gọi bởiTường minh bởi nhà phát triển (using, gọi trực tiếp)Tự động bởi GC trước khi thu gom
Thời điểm (Timing)Xác định (Deterministic) — bạn kiểm soát khi nàoKhông xác định (Non-deterministic) — bất cứ khi nào GC chạy
InterfaceIDisposableCú pháp ngôn ngữ (~ClassName())
Hiệu suấtKhông có overhead cho GCThăng cấp object lên thế hệ cũ hơn
Tài nguyên không quản lý (Unmanaged resources)Luôn dọn dẹp ở đâyChỉ là mạng lưới an toàn (nếu Dispose không được gọi)
Gọi nhiều lầnPhải là lũy đẳng (Idempotent — an toàn khi gọi nhiều lần)Chỉ được gọi một lần bởi GC
public class DatabaseConnection : IDisposable
{
private IntPtr _handle;
private bool _disposed;

public DatabaseConnection(string connStr)
{
_handle = NativeOpenConnection(connStr);
}

// Dispose — được gọi bởi nhà phát triển (dọn dẹp xác định)
public void Dispose()
{
if (_disposed) return;
NativeCloseConnection(_handle);
_handle = IntPtr.Zero;
_disposed = true;
GC.SuppressFinalize(this); // Ngăn finalizer chạy
}

// Finalizer — mạng lưới an toàn, được gọi bởi GC (không xác định)
~DatabaseConnection()
{
Dispose(); // Ủy thác cho Dispose
}
}

// Cách sử dụng với using — Dispose được gọi tại cuối phạm vi
using var conn = new DatabaseConnection("Server=...");
// conn.Dispose() được gọi tự động
Luôn sử dụng Dispose() thông qua câu lệnh using. Chỉ implement finalizer như mạng lưới an toàn (Safety Net) cho tài nguyên không quản lý. Gọi GC.SuppressFinalize(this) trong Dispose để ngăn GC gọi finalizer không cần thiết.

Q47: Singleton Design Pattern là gì?

Singleton đảm bảo một class chỉ có một instance và cung cấp điểm truy cập toàn cục (Global Access Point) đến nó. Trường hợp sử dụng phổ biến: cấu hình (Configuration), logging, caching.

// Singleton an toàn luồng với Lazy<T> (được khuyến nghị)
public sealed class DatabaseConfig
{
// Lazy<T> đảm bảo khởi tạo lười biếng, an toàn theo luồng
private static readonly Lazy<DatabaseConfig> _instance =
new(() => new DatabaseConfig());

public static DatabaseConfig Instance => _instance.Value;

private DatabaseConfig() // Private — ngăn khởi tạo từ bên ngoài
{
ConnectionString = "Server=localhost;Database=MyApp;";
}

public string ConnectionString { get; }
}

// Cách sử dụng
var config = DatabaseConfig.Instance;
Console.WriteLine(config.ConnectionString);

Các cách triển khai khác:

Cách tiếp cậnAn toàn luồng (Thread-Safe)Lười biếng (Lazy)Ghi chú
Không lockKhôngĐơn giản, nhưng không an toàn theo luồng
lock ở mọi nơiOverhead hiệu suất trên mỗi lần truy cập
Double-check lockingPhức tạp, dễ lỗi
Lazy<T>Được khuyến nghị — đơn giản và chính xác
Khởi tạo tĩnh (Static initializer)KhôngInstance được tạo lúc chương trình bắt đầu
// Thay thế: Khởi tạo tĩnh (cách an toàn luồng đơn giản nhất)
public sealed class Singleton
{
public static readonly Singleton Instance = new();

// Static constructor đảm bảo tính lười biếng (chạy trước lần truy cập đầu tiên)
static Singleton() { }

private Singleton() { }
}
Cân nhắc xem bạn thực sự có cần Singleton hay không. Thường thì Dependency Injection với vòng đời singleton (services.AddSingleton<T>()) là cách tiếp cận tốt hơn — có thể kiểm tra (Testable), tường minh và tuân theo nguyên tắc IoC.

Q48: Tại sao private virtual method không thể bị ghi đè?

Một private virtual method là một mâu thuẫn vì:

  1. private có nghĩa là member chỉ có thể truy cập trong class khai báo — các derived class không thể thấy nó.
  2. virtual có nghĩa là member được thiết kế để bị ghi đè trong derived class — nhưng derived class không thể truy cập những gì là private.
public class Base
{
// Lỗi biên dịch: virtual hoặc abstract member không thể là private
// private virtual void DoWork(); // LỖI!

// Đúng: protected virtual — truy cập được bởi derived class
protected virtual void DoWork() { }
}

public class Derived : Base
{
protected override void DoWork() { } // OK
}

Trình biên dịch thực thi điều này — một private virtual method vốn dĩ mâu thuẫn và tạo ra lỗi thời điểm biên dịch.

Đồng thời (Concurrency)

Q49: Lợi ích của việc sử dụng Frozen Collections là gì?

FrozenSet<T>FrozenDictionary<TKey, TValue> (.NET 8+) là các collection bất biến được tối ưu hóa cho đọc nhanh sau giai đoạn đóng băng ban đầu. Một khi đã tạo, chúng không bao giờ thay đổi, cho phép runtime tối ưu hóa các thao tác tìm kiếm:

using System.Collections.Frozen;

var data = new Dictionary<string, int>
{
["apple"] = 1, ["banana"] = 2, ["cherry"] = 3
};

// Đóng băng — chi phí một lần, tối ưu hóa cho hiệu suất đọc
FrozenDictionary<string, int> frozen = data.ToFrozenDictionary();

// Tìm kiếm cực nhanh — không có lock, không có kiểm tra phiên bản
if (frozen.TryGetValue("apple", out int value))
Console.WriteLine(value); // 1

// FrozenSet
var tags = new[] { "csharp", "dotnet", "linq" }.ToFrozenSet();
bool has = tags.Contains("csharp"); // đường dẫn tối ưu
Lợi íchChi tiết
Không cấp phát khi đọc (Zero allocation on read)Không sao chép phòng thủ (Defensive Copies), không kiểm tra phiên bản
Nội dung tối ưu hóaCó thể sử dụng hashing hoàn hảo (Perfect Hashing), mảng nhỏ gọn
An toàn luồng (Thread-safe)Bất biến — an toàn để chia sẻ giữa các luồng
Phù hợp nhất choDữ liệu cấu hình, bảng tra cứu, hằng số
Sử dụng frozen collections khi bạn xây dựng collection một lần và truy vấn nhiều lần (đặc biệt trong các đường dẫn nóng — Hot Paths). Chi phí đóng băng được bù đắp qua các thao tác tìm kiếm nhanh hơn.

Q50: Nêu các collection an toàn theo luồng (Thread-safe Collections)

System.Collections.Concurrent cung cấp các collection an toàn theo luồng sử dụng không lock (Lock-free) hoặc lock tinh vi (Fine-grained Locking):

CollectionMô tảTrường hợp sử dụng
ConcurrentDictionary<TKey, TValue>Dictionary an toàn theo luồngCache chia sẻ, bộ đếm
ConcurrentQueue<T>FIFO an toàn theo luồngProducer-consumer
ConcurrentStack<T>LIFO an toàn theo luồngWork stealing
ConcurrentBag<T>Không có thứ tự, tối ưu cho cùng luồngPool công việc cục bộ luồng
BlockingCollection<T>Wrapper có giới hạn, chặnProducer-consumer với giới hạn
var cache = new ConcurrentDictionary<string, string>();
cache.TryAdd("key1", "value1");
cache.AddOrUpdate("key1", "new", (k, old) => $"{old}_updated");

var queue = new ConcurrentQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
if (queue.TryDequeue(out int item))
Console.WriteLine(item); // 1

Q51: Cách thực hiện lock cho code bất đồng bộ (Asynchronous Code)?

Bạn không thể sử dụng lock với await (lock không hỗ trợ async). Sử dụng SemaphoreSlim thay thế:

// SAI — câu lệnh lock không thể sử dụng với await
// lock (_sync) { await DoWorkAsync(); } // Lỗi biên dịch!

// ĐÚNG — SemaphoreSlim
private readonly SemaphoreSlim _semaphore = new(1, 1);

public async Task DoWorkAsync()
{
await _semaphore.WaitAsync();
try
{
await SomeAsyncOperation();
}
finally
{
_semaphore.Release();
}
}

// Pattern C# — sử dụng await using với async lock tùy chỉnh
public sealed class AsyncLock
{
private readonly SemaphoreSlim _sem = new(1, 1);
public Task<IDisposable> LockAsync() =>
_sem.WaitAsync().ContinueWith(_ => (IDisposable)new Releaser(_sem));
private sealed class Releaser(SemaphoreSlim sem) : IDisposable
{
public void Dispose() => sem.Release();
}
}

Q52: Nêu tất cả các cách tạo luồng mới (Creating a New Thread)

// 1. Thread class — luồng OS (nặng)
var thread = new Thread(() => Console.WriteLine("New thread"));
thread.Start();

// 2. Thread pool — tái sử dụng luồng (được khuyến nghị)
ThreadPool.QueueUserWorkItem(_ => Console.WriteLine("Thread pool work"));

// 3. Task.Run — thread pool qua TPL (phổ biến nhất)
Task.Run(() => Console.WriteLine("Task on thread pool"));

// 4. Task.Factory.StartNew — nhiều tùy chọn hơn (long-running, cancellation)
Task.Factory.StartNew(() => Console.WriteLine("Custom task"),
TaskCreationOptions.LongRunning);

// 5. async/await — không phải luồng mới, nhưng chuyển sang thread pool
await Task.Run(() => Compute());

// 6. BackgroundWorker —遗留 (WinForms/WPF)
var worker = new BackgroundWorker();
worker.DoWork += (s, e) => { /* background work */ };
worker.RunWorkerAsync();
Nên dùng Task.Run thay vì new Thread. Task sử dụng thread pool (tái sử dụng hiệu quả), hỗ trợ cancellation, continuation và xử lý ngoại lệ. Chỉ sử dụng new Thread khi bạn cần luồng OS chuyên dụng (ví dụ: STA thread cho COM interop).

Q53: Cách thực thi nhiều async task cùng lúc?

Sử dụng Task.WhenAll để chạy các task đồng thời (Concurrently) và đợi tất cả hoàn thành:

var urls = new[] { "api1", "api2", "api3" };

// Thực thi đồng thời — tất cả task bắt đầu cùng lúc
var tasks = urls.Select(url => FetchAsync(url)).ToArray();
string[] results = await Task.WhenAll(tasks);

// Với tổng hợp kết quả
var userIds = new[] { 1, 2, 3, 4, 5 };
var users = await Task.WhenAll(userIds.Select(id => GetUserAsync(id)));

// Task.WhenAll vs Task.WhenAny
await Task.WhenAll(tasks); // Đợi TẤT CẢ hoàn thành
Task<string> first = await Task.WhenAny(tasks); // Đợi task ĐẦU TIÊN hoàn thành

Xử lý lỗi:

try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Chỉ ngoại lệ ĐẦU TIÊN được bắt
// Để xem tất cả ngoại lệ:
}

// Truy cập tất cả ngoại lệ
var allResults = await Task.WhenAll(tasks.Select(t => TaskSafe(t)));

async Task<TResult> TaskSafe<TResult>(Task<TResult> task)
{
try { return await task; }
catch { return default; }
}

Q54: Sự khác biệt giữa AutoResetEventManualResetEvent là gì?

Cả hai đều là nguyên thủy đồng bộ hóa luồng (Thread Synchronization Primitives) chặn các luồng cho đến khi nhận được tín hiệu:

Tính năngAutoResetEventManualResetEvent
Sau Set()Tự động đặt lại (Auto-resets) thành chưa tín hiệuGiữ tín hiệu (Signaled) cho đến khi đặt lại thủ công
Giải phóng luồngMột luồng mỗi Set()Tất cả luồng đang chờ
Trường hợp sử dụngProducer-consumer (một lần một)Cổng/cửa (giải phóng tất cả cùng lúc)
// AutoResetEvent — giải phóng MỘT luồng đang chờ, sau đó tự động đặt lại
var are = new AutoResetEvent(false);

// Luồng 1
are.WaitOne(); // chặn cho đến khi Set() được gọi
are.Set(); // giải phóng MỘT luồng, sau đó đặt lại thành chưa tín hiệu

// ManualResetEvent — giải phóng TẤT CẢ luồng đang chờ, giữ mở
var mre = new ManualResetEvent(false);

mre.Set(); // giải phóng TẤT CẢ luồng đang chờ — giữ tín hiệu
mre.Reset(); // đóng cổng thủ công

Q55: Channel trong C# là gì?

Channel<T> (System.Threading.Channels) là hàng đợi hiệu suất cao, thân thiện với async cho producer-consumer — thay thế hiện đại cho BlockingCollection<T>:

using System.Threading.Channels;

// Unbounded — không giới hạn dung lượng
var channel = Channel.CreateUnbounded<string>();

// Bounded — giới hạn dung lượng với chiến lược áp lực ngược (Backpressure)
var bounded = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait // chặn khi đầy
});

// Producer
await channel.Writer.WriteAsync("message1");
channel.Writer.TryComplete(); // báo hiệu không còn ghi

// Consumer
await foreach (var msg in channel.Reader.ReadAllAsync())
{
Console.WriteLine(msg);
}
Tính năngChannel<T>BlockingCollection<T>
Hỗ trợ asyncTích hợp sẵn (ReadAllAsync)Chỉ chặn (Blocking only)
Hiệu suấtKhông lock (Lock-free), tối ưu hóaDựa trên lock
Áp lực ngược (Backpressure)Có (bounded + FullMode)Có (bounded)
Phiên bản .NET.NET Core 3.0+.NET Framework

Q56: Sự khác biệt giữa volatileInterlocked

Cả hai đều giải quyết vấn đề an toàn luồng, nhưng ở các mức độ khác nhau:

Tính năngvolatileInterlocked
Chức năngNgăn sắp xếp lại bộ đệm/đọc CPU (Prevents CPU Cache/Read Reorder)Cung cấp thao tác nguyên tử (Atomic Operations)
Thao tácChỉ Đọc/GhiIncrement, Add, Exchange, CompareExchange
Trường hợp sử dụngKiểm tra cờ (Flag checking)Bộ đếm (Counters), hoán đổi nguyên tử (Atomic Swaps)
// volatile — đảm bảo đọc/ghi không bị sắp xếp lại bởi CPU/trình biên dịch
private volatile bool _running = true;

// Luồng 1
while (_running) { DoWork(); }

// Luồng 2
_running = false; // đảm bảo hiển thị ngay lập tức

// Interlocked — tăng nguyên tử (bộ đếm an toàn theo luồng)
private int _count = 0;
Interlocked.Increment(ref _count); // nguyên tử _count++
volatile KHÔNG làm cho ++ hoặc += trở nên nguyên tử. Đây là các thao tác đọc-sửa đổi-ghi (Read-modify-write) — sử dụng Interlocked.Increment thay thế.

Q57: Giải thích cách hoạt động của Async Streams

Async streams (IAsyncEnumerable<T>, C# 8) cho phép bạn liệt kê bất đồng bộ (Asynchronously Enumerate) các phần tử từng cái một — lý tưởng khi mỗi phần tử yêu cầu I/O bất đồng bộ:

// Producer — tạo các phần tử bất đồng bộ
public async IAsyncEnumerable<User> GetUsersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
int page = 0;
while (!ct.IsCancellationRequested)
{
var users = await _api.GetUsersPageAsync(page, ct);
if (users.Count == 0) yield break;

foreach (var user in users)
yield return user;

page++;
}
}

// Consumer — await từng phần tử
await foreach (var user in GetUsersAsync())
{
Console.WriteLine(user.Name);
}

// Với cancellation
await foreach (var user in GetUsersAsync().WithCancellation(ct))
{
Console.WriteLine(user.Name);
}

Khi nào sử dụng: Gọi API phân trang (Paginated API Calls), streaming con trỏ cơ sở dữ liệu (Database Cursor Streaming), feed sự kiện thời gian thực (Real-time Event Feeds) — bất cứ đâu bạn cần kéo (Pull) các phần tử bất đồng bộ.


Q58: Sự khác biệt giữa Task.RunTaskFactory.StartNew

Tính năngTask.RunTaskFactory.StartNew
Độ đơn giảnĐơn giản, trường hợp phổ biếnNhiều tùy chọn và kiểm soát hơn
Bộ lập lịch mặc định (Default Scheduler)TaskScheduler.Default (thread pool)TaskScheduler hiện tại
Long-runningKhông có tùy chọn trực tiếpTaskCreationOptions.LongRunning
Kiểu trả vềTask / Task<T>Task (không generic — phải ép kiểu)
CancellationQua overloadQua overload
// Task.Run — đơn giản, được khuyến nghị cho hầu hết trường hợp
var task = Task.Run(() => Compute());

// TaskFactory.StartNew — nhiều kiểm soát hơn
var task = Task.Factory.StartNew(() =>
{
Compute();
}, CancellationToken.None,
TaskCreationOptions.LongRunning, // luồng chuyên dụng
TaskScheduler.Default);
Sử dụng Task.Run theo mặc định. Chỉ sử dụng Task.Factory.StartNew khi bạn cần LongRunning, TaskScheduler tùy chỉnh, hoặc các tùy chọn tạo nâng cao khác. Lưu ý — StartNew trả về Task (không phải Task<T>), nên sử dụng StartNew<T> cho task generic.