1.11 - Ôn tập và đánh giá Module 1
Tóm tắt: Bài ôn tập cuối Module 1 gồm ba phần: 10 câu quiz bám vào những cái bẫy chính của module (tiền dùng
decimal, kiểu tham chiếu,switch, sửa collection khi duyệt, N+1,HashSet, deferred execution, đệ quy), bốn bài lab từ dễ tới khó có lời giải và tiêu chí chấm kiểm tra được, và một rubric mô tả từng mức điểm. Làm quiz trước khi xem đáp án, làm lab trước khi mở lời giải.
Mục tiêu bài học
Sau bài này bạn có thể:
- Trả lời đúng ít nhất 8/10 câu quiz và giải thích lý do cho từng câu.
- Viết bốn chương trình console nhỏ dùng
switch, vòng lặp,List,Dictionaryvà hàm, xử lý đúng các ca biên được nêu. - Tự chấm bài lab của mình theo rubric và chỉ ra phần còn yếu để ôn lại.
Cần biết trước: toàn bộ Module 1, từ bài 1.1 tới bài 1.10.
Nội dung bài học
1.11.1 — Cách làm bài
- Làm Phần A (quiz) trên giấy hoặc file nháp, ghi đáp án kèm một dòng lý do. Xong cả 10 câu mới mở đáp án.
- Làm Phần B (lab) trong một project console (
dotnet new console), mỗi bài một hàm hoặc một file. Chạy được, đúng output mong đợi, rồi mới mở lời giải để so. - Tự chấm theo rubric ở mục 1.11.4. Câu quiz sai và tiêu chí lab bị trừ điểm cho biết bài nào cần đọc lại.
1.11.2 — Phần A: Quiz (20 điểm, mỗi câu 2 điểm)
Câu 1. Kiểu nào nên dùng cho số tiền trên hóa đơn CRM? A. double B. float C. decimal D. int
Đáp án câu 1
C. decimal lưu theo cơ số 10 nên giữ chính xác mọi số thập phân bạn gõ vào. double và float lưu theo cơ số 2, nên 0.1 + 0.2 không bằng 0.3. int không có phần lẻ. Xem bài 1.1.
Câu 2. Đoạn code sau in ra gì?
// Customer.cs
class Customer { public string Name { get; set; } = ""; }// Program.cs
var c1 = new Customer { Name = "An" };
var c2 = c1;
c2.Name = "Bình";
Console.WriteLine(c1.Name);A. An B. Bình C. Lỗi biên dịch D. Chuỗi rỗng
Đáp án câu 2
B. Customer là class, tức kiểu tham chiếu: c2 = c1 chép địa chỉ, hai biến cùng trỏ tới một object. Nếu Customer là struct thì kết quả là An. Xem bài 1.1.
Câu 3. Trong C#, một case có lệnh nhưng thiếu break (cũng không có return, throw hay goto case) thì sao? A. Chương trình chạy tiếp xuống case dưới (fall-through) B. Lỗi biên dịch C. Ném ngoại lệ lúc chạy D. Trình biên dịch tự thêm break
Đáp án câu 3
B. C# không cho phép rơi ngầm từ case này sang case khác như C hay JavaScript. Build sẽ báo error CS0163: Control cannot fall through from one case label ('case "new":') to another. Xem mục 1.2.5 ở bài 1.2.
Câu 4. Vì sao nên viết if (customer is null) thay cho if (customer == null)? A. is null chạy nhanh hơn nhiều lần B. Toán tử == có thể bị một lớp định nghĩa lại, còn is null luôn là so sánh tham chiếu C. == null là lỗi biên dịch khi bật nullable D. Hai cách hoàn toàn giống nhau trong mọi trường hợp
Đáp án câu 4
B. Một lớp có thể nạp chồng == và chạy code bất kỳ. is null được trình biên dịch dịch thẳng thành phép so sánh tham chiếu, không bị can thiệp. Xem bài 1.2.
Câu 5. foreach (var n in numbers) if (n % 2 == 0) numbers.Remove(n); với numbers là List<int> gồm 1 tới 10. Chuyện gì xảy ra? A. Xóa hết số chẵn, chạy bình thường B. Ném InvalidOperationException: Collection was modified C. Lỗi biên dịch D. Chỉ xóa được số chẵn đầu tiên rồi dừng, không lỗi
Đáp án câu 5
B. Lần xóa đầu tiên làm số phiên bản của danh sách thay đổi; lần foreach xin phần tử tiếp theo, MoveNext() phát hiện ra và ném lỗi. Sửa bằng numbers.RemoveAll(n => n % 2 == 0). Xem bài 1.3.
Câu 6. Một màn hình lấy 200 đơn hàng bằng một truy vấn, rồi trong vòng lặp gọi database lấy khách hàng của từng đơn. Tổng cộng bao nhiêu truy vấn? A. 1 B. 2 C. 200 D. 201
Đáp án câu 6
D. Một truy vấn lấy danh sách cộng 200 truy vấn trong vòng lặp: lỗi N+1. Sửa bằng cách lấy hết khách hàng cần thiết trong một truy vấn rồi tra trong Dictionary, tổng còn 2. Xem bài 1.3.
Câu 7. Đối chiếu hai danh sách 20.000 số: với mỗi phần tử của a, kiểm tra nó có trong b không. Đổi b từ List<int> sang HashSet<int> thì độ phức tạp đổi thế nào? A. Từ O(n) sang O(1) B. Từ O(n²) sang O(n) C. Từ O(n log n) sang O(n) D. Không đổi, chỉ nhanh hơn một hằng số
Đáp án câu 7
B. List.Contains quét tuần tự O(n), đặt trong vòng lặp n lần thành O(n²). HashSet.Contains trung bình O(1), cả vòng lặp còn O(n). Đo thật trên .NET 10 ở bài 1.5: chênh hàng trăm lần với 20.000 phần tử.
Câu 8. Đoạn code sau in ra gì?
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x => x > 1);
numbers.Add(10);
Console.WriteLine(query.Count());A. 2 B. 3 C. 4 D. Lỗi vì sửa danh sách sau khi tạo truy vấn
Đáp án câu 8
B. Where chưa chạy lúc được viết (deferred execution). Count() mới chạy công thức trên danh sách hiện tại {1, 2, 3, 10}, có ba phần tử lớn hơn 1. Muốn chốt kết quả tại một thời điểm thì gọi .ToList(). Xem bài 1.8.
Câu 9. Hai vòng lặp lồng nhau: vòng ngoài 5.000 khách, vòng trong 200.000 đơn. Dữ liệu tăng gấp 10 ở cả hai chiều thì số phép so sánh tăng bao nhiêu lần? A. 10 B. 20 C. 100 D. 1.000
Đáp án câu 9
C. Số phép so sánh là tích hai kích thước: 10 × 10 = 100 lần. Thời gian đo thật tăng xấp xỉ như vậy (khoảng 119 lần ở bài 1.10), không đúng tuyệt đối 100 vì bộ nhớ đệm của CPU và nhiễu đo.
Câu 10. Hàm đệ quy duyệt cây danh mục gặp dữ liệu có vòng (A là con của B, B là con của A). Bọc lời gọi trong try/catch (Exception) có cứu được tiến trình không? A. Có, catch bắt được StackOverflowException B. Có, nếu bắt đúng StackOverflowException C. Không, tiến trình bị hủy và catch không bao giờ chạy D. Không cần, .NET tự phát hiện vòng và dừng đệ quy
Đáp án câu 10
C. Khi stack đã đầy, runtime không còn chỗ để chạy khối catch, nên nó hủy cả tiến trình. Cách phòng: theo dõi các nút đã thăm bằng HashSet, hoặc dùng Stack<T> tường minh thay cho đệ quy. Xem bài 1.8.
1.11.3 — Phần B: Lab (80 điểm)
Mỗi bài là một hàm trong Program.cs. Các hàm có tên đúng như đề để người chấm tìm được.
Bài 1 — Máy tính đơn giản (Dễ, 15 điểm)
Viết hàm Calculate(double a, double b, char op) nhận hai số và một toán tử (+, -, *, /), trả về kết quả.
// Code khởi đầu
double Calculate(double a, double b, char op)
{
// TODO: dùng switch expression xử lý 4 phép tính
// Chia cho 0: trả về double.NaN
// Toán tử khác: ném ArgumentException
throw new NotImplementedException();
}
Console.WriteLine(Calculate(10, 3, '+')); // 13
Console.WriteLine(Calculate(10, 3, '/')); // 3,3333333333333335 (máy vùng Việt Nam)
Console.WriteLine(Calculate(10, 0, '/')); // NaNTiêu chí chấm:
- (5 điểm) Bốn phép tính cho đúng kết quả như ba dòng mẫu.
- (5 điểm) Chia cho 0 trả về
NaN. Lưu ý: không xử lý gì thì10.0 / 0không ném lỗi mà trả về∞(Infinity); chỉ phép chia số nguyên cho 0 mới némDivideByZeroException. Đề yêu cầuNaNđể kết quả "không phải số" hiện rõ. - (5 điểm) Toán tử khác (ví dụ
'%') némArgumentExceptioncó thông báo nói rõ toán tử nào.
Lời giải — Bài 1
double Calculate(double a, double b, char op) => op switch
{
'+' => a + b,
'-' => a - b,
'*' => a * b,
'/' when b == 0 => double.NaN,
'/' => a / b,
_ => throw new ArgumentException($"Toán tử không hỗ trợ: {op}", nameof(op)),
};'/' when b == 0 là nhánh có thêm điều kiện: chỉ khớp khi toán tử là / và b bằng 0. Nhánh này phải đứng trước nhánh '/' thường, vì nhánh đầu tiên khớp sẽ thắng. Gọi Calculate(10, 3, '%') ném System.ArgumentException: Toán tử không hỗ trợ: % (Parameter 'op').
Bài 2 — Phân tích danh sách điểm (Trung bình, 20 điểm)
Viết hàm AnalyzeScores(List<int> scores) in ra điểm trung bình, điểm cao nhất, thấp nhất và số học viên đạt (từ 50 điểm trở lên). Chỉ dùng foreach, không dùng LINQ, để luyện vòng lặp.
var scores = new List<int> { 45, 72, 88, 35, 60, 91, 48, 77 };
AnalyzeScores(scores);
AnalyzeScores(new List<int>());Output mong đợi (máy đặt vùng Việt Nam):
Trung bình: 64,5
Cao nhất: 91 | Thấp nhất: 35
Số đạt: 5/8 (62,5%)
Danh sách rỗng!Tiêu chí chấm:
- (8 điểm) Ba dòng đầu đúng như mẫu với danh sách trên.
- (4 điểm) Trung bình là
64,5chứ không phải64: phép chia không bị làm tròn thành số nguyên. - (4 điểm) Danh sách rỗng in
Danh sách rỗng!, không ném lỗi. - (4 điểm) Ngưỡng 50 là hằng số có tên (
const int PassMark = 50;), và điểm đúng 50 được tính là đạt.
Lời giải — Bài 2
void AnalyzeScores(List<int> scores)
{
if (scores.Count == 0)
{
Console.WriteLine("Danh sách rỗng!");
return;
}
const int PassMark = 50;
int sum = 0, max = scores[0], min = scores[0], passed = 0;
foreach (var s in scores)
{
sum += s;
if (s > max) max = s;
if (s < min) min = s;
if (s >= PassMark) passed++;
}
double average = (double)sum / scores.Count; // ép sang double để không chia nguyên
Console.WriteLine($"Trung bình: {average}");
Console.WriteLine($"Cao nhất: {max} | Thấp nhất: {min}");
Console.WriteLine($"Số đạt: {passed}/{scores.Count} ({(double)passed / scores.Count:P1})");
}Bẫy chính: sum / scores.Count với hai số int là chia nguyên, cho 64 (516 / 8 = 64 dư 4). (double)sum đổi một vế sang double trước khi chia. :P1 định dạng phần trăm với một chữ số thập phân.
max và min khởi tạo bằng scores[0] chứ không phải bằng 0: nếu mọi điểm đều âm (dữ liệu lỗi) thì max = 0 sẽ sai. Vì vậy phải kiểm tra danh sách rỗng trước.
Bài 3 — Đếm tần suất từ (Trung bình - Khá, 20 điểm)
Viết hàm CountWordFrequency(string sentence) trả về Dictionary<string, int>: mỗi từ (viết thường) xuất hiện bao nhiêu lần.
var frequency = CountWordFrequency("crm giúp quản lý khách hàng crm rất hữu ích");
foreach (var pair in frequency)
Console.WriteLine($"{pair.Key}: {pair.Value} lần");Output mong đợi bắt đầu bằng:
crm: 2 lần
giúp: 1 lần
quản: 1 lầnTiêu chí chấm:
- (8 điểm)
crmđếm được 2, mọi từ khác đếm được 1. - (4 điểm) Không phân biệt hoa thường:
"CRM crm"chocrm: 2. - (4 điểm) Nhiều dấu cách liên tiếp hoặc dấu cách ở hai đầu không sinh ra "từ rỗng".
- (4 điểm) Dùng
TryGetValue(một lần tra) thay cho cặpContainsKeyrồi[key].
Lời giải — Bài 3
Dictionary<string, int> CountWordFrequency(string sentence)
{
var result = new Dictionary<string, int>();
string[] words = sentence.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
if (result.TryGetValue(word, out int count))
result[word] = count + 1;
else
result[word] = 1;
}
return result;
}StringSplitOptions.RemoveEmptyEntries bỏ các phần rỗng sinh ra khi có hai dấu cách liền nhau. Thiếu nó, "crm crm" cho ra một "từ" rỗng "".
Mở rộng (không chấm): với câu có dấu câu, "CRM, crm" hiện cho hai từ khác nhau là crm, và crm. Muốn gộp thì tách theo cả dấu câu: Split(new[] { ' ', ',', '.', '!', '?' }, StringSplitOptions.RemoveEmptyEntries).
Thứ tự in ra trùng thứ tự từ xuất hiện lần đầu, nhưng đó là chi tiết cài đặt của Dictionary hiện tại, tài liệu không bảo đảm. Cần thứ tự cố định thì sắp xếp trước khi in.
Bài 4 — Quản lý lead CRM mini (Nâng cao, 25 điểm)
Viết chương trình quản lý lead (khách hàng tiềm năng) dùng một Dictionary<string, decimal> lưu tên lead và doanh thu ước tính, với bốn hàm:
| Hàm | Việc làm |
|---|---|
AddLead(string name, decimal revenue) | Thêm lead. Tên rỗng, doanh thu âm, hoặc tên đã có: in thông báo và không ghi đè |
ListAllLeads() | In bảng tên, doanh thu, phân loại |
TotalRevenue() | Trả về tổng doanh thu của mọi lead |
FilterByTier(string tier) | Trả về danh sách tên lead thuộc phân loại đó |
Phân loại: Hot từ 100.000.000 trở lên, Warm từ 20.000.000 trở lên, Cold dưới 20.000.000.
Dữ liệu thử:
AddLead("Công ty Minh Phát", 150_000_000m);
AddLead("Startup GreenTech", 35_000_000m);
AddLead("Hộ kinh doanh Lan", 8_000_000m);
AddLead("Tập đoàn Bắc Nam", 500_000_000m);
AddLead("Cửa hàng Thuận Phát", 12_000_000m);
AddLead("Startup GreenTech", 40_000_000m); // trùng tênTiêu chí chấm:
- (8 điểm) Đủ bốn hàm, đúng tên như bảng;
TotalRevenue()trả705.000.000với dữ liệu thử. - (6 điểm) Phân loại đúng ở cả hai mốc: 100.000.000 là Hot, 20.000.000 là Warm. Hai ngưỡng là hằng số có tên, không viết số trần trong code.
- (5 điểm) Thêm trùng tên in thông báo và giữ nguyên doanh thu cũ (35.000.000).
- (3 điểm)
FilterByTier("Cold")trả về đúng hai lead Hộ kinh doanh Lan và Cửa hàng Thuận Phát. - (3 điểm) Bảng in ra thẳng cột.
Lời giải — Bài 4
const decimal HotThreshold = 100_000_000m;
const decimal WarmThreshold = 20_000_000m;
var leads = new Dictionary<string, decimal>();
AddLead("Công ty Minh Phát", 150_000_000m);
AddLead("Startup GreenTech", 35_000_000m);
AddLead("Hộ kinh doanh Lan", 8_000_000m);
AddLead("Tập đoàn Bắc Nam", 500_000_000m);
AddLead("Cửa hàng Thuận Phát", 12_000_000m);
AddLead("Startup GreenTech", 40_000_000m); // trùng tên: báo, không ghi đè
ListAllLeads();
Console.WriteLine($"Tổng doanh thu ước tính: {TotalRevenue():N0}");
Console.WriteLine($"Lead Cold: {string.Join(", ", FilterByTier("Cold"))}");
void AddLead(string name, decimal revenue)
{
if (string.IsNullOrWhiteSpace(name))
{
Console.WriteLine("Tên lead không được để trống");
return;
}
if (revenue < 0)
{
Console.WriteLine($"Doanh thu của {name} không được âm");
return;
}
if (!leads.TryAdd(name, revenue)) // TryAdd trả false nếu khóa đã có
Console.WriteLine($"Lead '{name}' đã tồn tại, bỏ qua");
}
string Classify(decimal revenue) => revenue switch
{
>= HotThreshold => "Hot",
>= WarmThreshold => "Warm",
_ => "Cold",
};
void ListAllLeads()
{
Console.WriteLine($"{"Tên lead",-22} {"Doanh thu",15} {"Loại",-5}");
foreach (var (name, revenue) in leads)
Console.WriteLine($"{name,-22} {revenue,15:N0} {Classify(revenue),-5}");
}
decimal TotalRevenue()
{
decimal total = 0;
foreach (var revenue in leads.Values) total += revenue;
return total;
}
List<string> FilterByTier(string tier)
{
var result = new List<string>();
foreach (var (name, revenue) in leads)
if (Classify(revenue) == tier) result.Add(name);
return result;
}Output chạy thật trên .NET 10 (máy đặt vùng Việt Nam):
Lead 'Startup GreenTech' đã tồn tại, bỏ qua
Tên lead Doanh thu Loại
Công ty Minh Phát 150.000.000 Hot
Startup GreenTech 35.000.000 Warm
Hộ kinh doanh Lan 8.000.000 Cold
Tập đoàn Bắc Nam 500.000.000 Hot
Cửa hàng Thuận Phát 12.000.000 Cold
Tổng doanh thu ước tính: 705.000.000
Lead Cold: Hộ kinh doanh Lan, Cửa hàng Thuận PhátVài điểm đáng chú ý:
{name,-22}căn trái trong 22 ký tự,{revenue,15:N0}căn phải trong 15 ký tự có dấu phân cách nghìn: đó là cách in bảng thẳng cột.foreach (var (name, revenue) in leads)tách mỗi cặp khóa, giá trị củaDictionaryra hai biến.- Dùng
leads[name] = revenuethay choTryAddlà lỗi hay gặp: trùng tên sẽ ghi đè âm thầm, đúng cái bẫy ở bài 1.2.
Mở rộng (không chấm): in danh sách lead theo doanh thu giảm dần bằng leads.OrderByDescending(l => l.Value).
1.11.4 — Rubric và ngưỡng điểm
Tổng điểm = quiz (20) + lab (80). Ngoài điểm theo tiêu chí từng bài, người chấm xem thêm chất lượng chung:
| Mức | Tổng điểm | Mô tả |
|---|---|---|
| Chưa đạt | dưới 60 | Lab chưa chạy được ít nhất hai bài, hoặc sai từ 5 câu quiz trở lên. Đọc lại các bài ứng với câu sai trước khi sang Module 2. |
| Đạt | 60 – 74 | Bài 1 tới 3 chạy đúng output mẫu. Bài 4 có thể thiếu xử lý trùng tên hoặc chưa thẳng cột. Giải thích được đáp án quiz bằng lời của mình. |
| Khá | 75 – 89 | Cả bốn bài chạy đúng, xử lý đủ ca biên trong tiêu chí (danh sách rỗng, chia cho 0, dấu cách thừa, trùng tên). Tên hàm, tên biến bằng tiếng Anh, ngưỡng là hằng số có tên. |
| Giỏi | 90 – 100 | Như mức Khá, cộng thêm: tự viết test xUnit cho ít nhất hai bài theo cách ở bài 0.4 (giá trị mong đợi tính tay), hoặc làm phần mở rộng của Bài 3 và Bài 4. |
1.11.5 — Tóm tắt Module 1
| Bài | Điều cần mang theo |
|---|---|
| 1.1 | Tiền dùng decimal; gán class là chép địa chỉ; bật nullable và đọc cảnh báo |
| 1.2 | Guard clause; switch expression trên enum được kiểm tra vét cạn; is null |
| 1.3 | Không sửa collection khi đang foreach; không gọi database trong vòng lặp (N+1) |
| 1.4 | Hàm đặt tên cho một ý tưởng; truyền theo giá trị; đệ quy cần điểm dừng; nhận thời gian làm tham số |
| 1.5 | List, Dictionary, HashSet; Contains trên List trong vòng lặp là O(n²) |
| 1.6 | Bảng ví dụ trước khi code; danh sách ca biên; chia đôi để tìm lỗi |
| 1.7 | Big-O đo tốc độ tăng; đo cho đúng: Release, làm nóng, lấy giá trị nhỏ nhất |
| 1.8 | LINQ và deferred execution; lambda bắt biến; đệ quy trên dữ liệu có vòng |
| 1.9 | Hỏi lại chỗ mơ hồ; bảng quyết định; test với giá trị tính tay |
| 1.10 | Vòng lặp lồng trên hai tập lớn; tỉ lệ đáng tin hơn con số tuyệt đối |
Tham khảo
- C# documentation — Microsoft Learn — tài liệu chính thức
- Selection statements — if, switch — quy tắc không fall-through của
switch - Commonly used collection types — độ phức tạp của các collection
- Standard numeric format strings —
N0,P1dùng trong lab
Điều hướng
- Bài trước: 1.10 — Sự cố thực tế: vòng lặp lồng làm treo báo cáo
- Bài tiếp theo: Module 2 — Computer Science Basics
- Về module: Trang mục lục