C# Interview Questions
Nền tảng ngôn ngữ (Language Fundamentals)
Q1: Sự khác biệt giữa readonly và const 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ăng | const | readonly |
|---|---|---|
| Thời điểm gán | Thờ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) | Có | Không (có thể là instance hoặc static) |
| Kiểu được phép | Kiểu nguyên thủy (Primitives), chuỗi (strings), enum, null | Bất kỳ kiểu nào |
Có thể dùng new | Không | Có |
| 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;
}
}
readonly cho các giá trị publicCá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
}
}
Đá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 Class | Lớp dẫn xuất (Derived Class) | Cùng Assembly | Assembly bên ngoài |
|---|---|---|---|---|
public | Có | Có | Có | Có |
private | Có | Không | Không | Không |
protected | Có | Có | Không | Không |
internal | Có | Không | Có | Không |
protected internal | Có | Có | Có | Không |
private protected | Có | Có (cùng assembly) | Không | Khô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ăng | Abstract Class | Interface |
|---|---|---|
| Constructor | Có | Không |
| Trường / trạng thái (Fields / state) | Có | Không (chỉ static fields, C# 8+) |
| Cài đặt mặc định (Default implementation) | Có | 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ùng | Quan hệ "Là một" (Is-a) với logic dùng chung | Hợ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
staticclass ở 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
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ạnh | Stack | Heap |
|---|---|---|
| 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ước | Nhỏ (~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êng | Chia 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 string và StringBuilder là gì?
string là bất biến (immutable) — mọi sửa đổi đều tạo ra một string object mới. StringBuilder là khả 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ăng | string | StringBuilder |
|---|---|---|
| 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ần | Nối thêm vào cùng buffer |
| Hiệu suất | Chậm khi nhiều thao tác | Nhanh khi nhiều thao tác |
| An toàn luồng (Thread safety) | Tự nhiên an toàn theo luồng | Không an toàn theo luồng |
| Trường hợp sử dụng | Ít thao tác, so sánh | Vòng lặp, nhiều lần nối chuỗi |
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ử is và as 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ăng | is | as |
|---|---|---|
| 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 matching | Có (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);
is với pattern matchingSử 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ăng | Liê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ất | Nhanh 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ịch | Có thể lỗi tại runtime |
| Hỗ trợ IDE | IntelliSense đầ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ồng | dynamic, reflection, phân phối virtual |
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");
DateTimeOffset thay vì DateTimeDateTime 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> và Dictionary<TKey, TValue> là gì?
| Tính năng | HashSet<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ếm | Theo giá trị (Trung bình O(1)) | Theo khóa (Trung bình O(1)) |
| Trùng lặp (Duplicates) | Bỏ qua âm thầm | Ném ngoại lệ khi khóa trùng |
| Trường hợp sử dụng | Kiể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ăng | GroupBy | ToLookup |
|---|---|---|
| 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ấn | Không (đã cache) |
| Khóa thiếu (Missing key) | Không áp dụng | Trả 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
OfType<T> để lọc thay vì ném lỗiOfType<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ử
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ăng | List<T> | ImmutableList<T> |
|---|---|---|
| Khả biến (Mutability) | Khả biến | Bất biến (trả về mới) |
| Add/Remove | O(1) khấu hao | O(log n) — cấu trúc cây |
| An toàn luồng (Thread safety) | Không | Tự nhiên an toàn theo luồng |
| Trường hợp sử dụng | Mục đích chung | Pipeline hàm, trạng thái chia sẻ |
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() và 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ăng | CopyTo() | Clone() |
|---|---|---|
| Đích đến (Destination) | Sao chép vào một mảng đã tồn tại | Tạo một mảng mới |
| Chỉ số bắt đầu | Có thể chỉ định chỉ số bắt đầu | Luôn bắt đầu từ 0 |
| Kiểu trả về | void | object (phải ép kiểu) |
| Mảng phải tồn tại | Có (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
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 Array và ArrayList là gì?
| Tính năng | Array | ArrayList |
|---|---|---|
| An toàn kiểu (Type safety) | Kiểu mạnh (T[]) | Lưu object (không an toàn kiểu) |
| Boxing | Không | Có (kiểu giá trị bị boxing thành object) |
| Kích thước | Cố định khi tạo | Động (tự động thay đổi) |
| Namespace | System | System.Collections |
| Hiệu suất | Nhanh hơn (không ép kiểu/boxing) | Chậm hơn (ép kiểu + boxing) |
| Thay thế hiện đại | T[] | 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
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 SortedList và SortedDictionary 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ăng | SortedList<TKey, TValue> | SortedDictionary<TKey, TValue> |
|---|---|---|
| Cấu trúc bên trong | Mả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óa | O(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ếp | O(n) — dịch chuyển mảng | O(log n) — cân bằng lại cây |
| Chèn dữ liệu đã sắp xếp | O(1) khấu hao — nối thêm | O(log n) |
| Lấy min/max | O(1) — phần tử đầu/cuối | O(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
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)
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ăng | class | record | struct |
|---|---|---|---|
| Loại kiểu | Tham chiếu (Heap) | Tham chiếu (Heap) | Giá trị (Stack) |
| Khả biến (Mutability) | Khả biến theo mặc định | Bất biến theo mặc định | Khả 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 with | Không | Có | Có (C# 10+) |
| Tự động tạo | Không gì | Equals, GetHashCode, ToString, with | Khô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
asynchoặc lambda (trừ khiref-safe) - Bị bắt bởi closure
Trường hợp sử dụng chính là Span<T> và 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
- 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
- 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");
}
}
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()vsstring? 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.