Download as pdf or txt
Download as pdf or txt
You are on page 1of 530

Hướng dẫn tự học lập trình C# toàn tập

C# là một ngôn ngữ lập trình được xây dựng riêng cho .NET Framework –
nền tảng phát triển ứng dụng chủ đạo hiện nay của Microsoft. Hiện nay,
nhu cầu tuyển dụng và đào tạo nhân lực lập trình viên .NET và C# đang rất
lớn.
Hiện nay, số lượng tài liệu lập trình C# rất nhiều. Mỗi tài liệu có hướng tiếp
cận riêng. Tuy nhiên, các tài liệu tốt nhất đều viết bằng tiếng Anh. Trên
mạng Internet bạn cũng có thể dễ dàng tìm thấy rất nhiều nội dung hướng
dẫn học lập trình C# tiếng Việt. Tuy nhiên, chúng thường là những blog post
hoặc series bài khá rời rạc. Việc tự học theo các website hoặc blog như vậy
khá khó khăn và thiếu bài bản.
Vì vậy chúng tôi quyết định xây dựng một bộ bài giảng riêng hướng tới tính
hệ thống và bài bản giúp bạn có thể tự học lập trình C#. Nội dung bài giảng
này được tham khảo từ những tài liệu dạy lập trình C# tiếng Anh mới nhất
và được đánh giá cao trên Amazon.
Ngoài nội dung lý thuyết giống như các tài liệu giảng dạy trình C# khác,
chúng tôi xây dựng riêng một phần thực hành. Nội dung thực hành sẽ hướng
dẫn các bạn theo kiểu step-by-step để tự thực hiện một mini project. Trong
mini-project sẽ cố gắng vận dụng đầy đủ kiến thức và kỹ thuật lập trình C#
để xây dựng một ứng dụng trọn vẹn.
Trong bài giảng này, chúng tôi lựa chọn cách tiếp cận để đảm bảo đủ hai
mục tiêu: cung cấp các khái niệm và kỹ thuật đặc trưng của ngôn ngữ (giống
như các tài liệu về lập trình khác); chỉ dẫn cách vận dụng trong giải quyết
một bài toán trọn vẹn.
Phần lý thuyết sẽ tiếp cận theo cách truyền thống thường gặp trong các
cuốn sách dạy lập trình C# tiếng Anh. Các bài học sẽ lần lượt đề cập đến
các vấn đề từ cơ bản nhất (như từ khóa, bộ ký tự, kiểu dữ liệu,...) đến các
vấn đề quan trọng của lập trình hướng đối tượng cũng như các chủ đề nâng
cao riêng của lập trình C#.
Trong phần thực hành, bài giảng sẽ hướng dẫn từng bước thực hiện một
mini-project trọn vẹn, từ phân tích bài toán đến lúc đóng gói và cài đặt ứng
dụng.
Trong quá trình làm đề tài, chúng tôi cố gắng áp dụng những nguyên lý ứng
dụng của lập trình hướng đối tượng ở những nơi phù hợp. Điều này nhằm
mục đích lưu ý người học xây dựng ý thức trong việc vận dụng chúng trong
quá trình phát triển ứng dụng.

1
Lưu ý rằng, cách tiếp cận này không nhất định chỉ sử dụng khi học lập trình
C#.NET mà còn có thể được vận dụng khi học bất kỳ một ngôn ngữ và công
nghệ phát triển ứng dụng nào. Cách tiếp cận (và bài toán) này sẽ được
chúng tôi sử dụng trong việc hướng dẫn học nhiều ngôn ngữ khác.
Chúng tôi xây dựng bài giảng này hướng tới người đã có kiến thức cơ bản
về lập trình nói chung và lập trình hướng đối tượng nói riêng. Nếu chưa từng
tiếp xúc với các khái niệm cơ bản như biến, hằng, phương thức/chương trình
con/hàm, biểu thức, lớp, đối tượng, kế thừa,... người học sẽ khó theo được
chương trình vì tài liệu này sẽ không giải thích chi tiết các khái niệm cơ bản
nữa.
Vì C# là một ngôn ngữ sử dụng cấu trúc cú pháp tương tự C/C++, nếu
người học đã từng biết một trong các ngôn ngữ như C/C++, Java sẽ rất dễ
dàng theo học. Nếu đã học một trong số các ngôn ngữ như JavaScript, PHP,
Perl, tài liệu này cũng rất phù hợp. Nếu xuất phát điểm là những ngôn ngữ
sử dụng cấu trúc của Pascal/Delphi hay Visual basic, việc học có chút khó
khăn hơn.
Tuy nhiên, vì đây là một tài liệu ở cấp độ cơ bản, bất kỳ ai có nguyện vọng
và ham mê học một ngôn ngữ lập trình mới đều có thể học được (miễn là
không xuất phát từ con số 0).

2
.NET Framework và ngôn ngữ lập trình Visual C#
C# là một trong những ngôn ngữ hoạt động trên một nền tảng đặc biệt gọi
là “.NET Framework”. Chương trình viết bằng C# (và các ngôn ngữ .NET
khác) có quy trình dịch và cách thực thi khác biệt với các ứng dụng Windows
“bình thường”. Sự khác biệt này có ảnh hưởng lớn đến cách nghĩ và cách
code trong C#.
Do vậy, các tài liệu về lập trình C# hầu như luôn phải giới thiệu về .NET
Framework trước khi bắt đầu nói về chính ngôn ngữ.
Bài học này sẽ giúp bạn hiểu .NET Framework là gì, sự khác biệt về quy
trình biên dịch – thực thi ứng dụng trong .NET Framework với các loại ứng
dụng truyền thống. Cuối cùng bạn sẽ làm quen với một số đặc điểm của
ngôn ngữ lập trình C#.

.NET Framework là gì?


Với câu hỏi “.NET Framework là gì” bạn sẽ nhận được hai câu trả lời:
 .NET Framework là một bộ thư viện lớp
 .NET Framework là một môi trường thực thi của ứng dụng
.NET Framework là một bộ thư viện lớp
.NET Framework chứa một bộ thư viện rất lớn chứa các loại công cụ khác
nhau giúp người lập trình xử lý được hầu hết các công việc thường gặp trong
quá trình phát triển ứng dụng. Vì vậy, lập trình ứng dụng với .NET
Framework thường có hiệu quả rất cao và thời gian làm việc ngắn hơn.
Người lập trình có thể sử dụng bất kỳ ngôn ngữ nào mà trình biên dịch của
nó có thể dịch mã nguồn sang một dạng mã trung gian theo yêu cầu của
.NET Framework. Visual C#, Visual Basic.NET, Visual C++.NET, và F# là
bốn ngôn ngữ chính thức được Microsoft phát triển để hoạt động trên .NET
Framework.
Ngoài ra còn một số ngôn ngữ khác không do Microsoft phát triển cũng được thiết kế hướng
tới nền tảng .NET như Delphi.NET, Oxygence (hai ngôn ngữ có cấu trúc cú pháp tương tự
Pascal), IronPython (phiên bản của Python dịch sang .NET Framework),...
Các ngôn ngữ hướng tới .NET Framework ngoài việc có thể truy cập tới bộ
thư viện còn có thể sử dụng thư viện người dùng viết bằng các ngôn ngữ
.NET khác. Ví dụ, người phát triển ứng dụng trên C# hoàn toàn có thể sử
dụng thư viện do người khác phát triển trên Visual Basic .NET.

3
.NET Framework là một môi trường thực thi cho ứng dụng
Chương trình viết cho .NET Framework không thực thi trực tiếp trong môi
trường hệ điều hành mà thực thi trong khuôn khổ của chương trình CLR và
được quản lý bởi chương trình này.
Vì thực thi trong một môi trường riêng khép kín và được quản lý chặt chẽ,
chương trình .NET có thể tránh được nhiều lỗi thường gặp trong quá trình
phát triển ứng dụng.
Do môi trường thực thi của các ứng dụng .NET cung cấp nhiều tính năng
cao cấp (như quản lý bộ nhớ, xử lý ngoại lệ,...), việc lập trình ứng dụng trên
.NET Framework đơn giản hơn.
Việc học lập trình với một ngôn ngữ bất kỳ của .NET Framework có giá trị
rất lớn khi ta có thể sử dụng nó để viết hầu như bất kỳ loại ứng dụng nào,
có thể viết ứng dụng không chỉ ứng dụng chạy trên Windows mà còn có thể
cho các nền tảng khác.
Một số công nghệ trong .NET Framework
Dưới đây là một số công nghệ phổ biến trong .NET Framework:
 Console: xây dựng ứng dụng với giao diện dòng lệnh.
 Windows Forms: xây dựng ứng dụng desktop (giao diện đồ họa)
cho Windows.
 Windows Presentation Foundation (WPF): công nghệ mới xây
dựng ứng dụng desktop (giao diện đồ họa) cho Windows.
 ASP.NET: nền tảng để phát triển các ứng dụng web chạy trong
chương trình máy chủ IIS, bao gồm ASP.NET Web Forms, ASP.NET
MVC, ASP.NET Web API.
 ADO.NET và Entity Framework: công nghệ cho phép chương trình
kết nối và sử dụng cơ sở dữ liệu quan hệ (SQL Server, MySQL,…).
 Windows Communications Foundation (WCF): công nghệ cho
phép phát triển ứng dụng mạng hướng dịch vụ (Service Oriented
Application - SOA).
Hiện nay bạn có thể sẽ nghe thấy cả .NET Core, hay .NET 5 (sắp ra mắt). Lưu ý rằng, mặc dù
đều có chung phần “.NET” nhưng chúng là những nền tảng khác nhau. Để phân biệt, người ta
thường nói rõ “.NET Framework” hay “.NET Core”. Trong đó, .NET Framework là nền tảng
thuần túy cho Windows, .NET Core hoạt động đa nền tảng (trong đó có cả Windows), .NET
5.0 là nền tảng hợp nhất trong tương lai.
Ngôn ngữ C# sử dụng để lập trình cho tất cả các nền tảng này.

4
Cài đặt .NET Framework trên Windows
Trên hệ điều hành Windows, .NET Framework có thể cài đặt trên phiên bản
Windows 98 và mới hơn, hoặc Windows NT 4.0 về sau. Các phiên bản cũ
hơn của Windows không cho phép cài đặt .NET Framework. Các hệ điều
hành mới như Windows 10 đều mặc định cài đặt .NET và có thể tự động cập
nhật phiên bản mới.
Các chương trình viết cho .NET Framework chỉ có thể hoạt động nếu trên
hệ điều hành có cài đặt phiên bản tương đương hoặc mới hơn của .NET (so
với phiên bản sử dụng cho quá trình phát triển ứng dụng).
Phiên bản mới nhất của .NET Framework hiện nay là 4.7. Nếu bạn đang
dùng Windows 10, hoặc bạn đã cài đặt Visual Studio, máy tính của bạn đã
có sẵn .NET bản mới nhất.

Biên dịch mã nguồn và thực thi chương trình trong .NET


Framework
.NET Framework và các ngôn ngữ hỗ trợ nền tảng này hoạt động không
giống như các chương trình “bình thường” khác trong Windows.
Biên dịch và thực thi chương trình native
Các tập tin mã nguồn của chương trình viết bằng một ngôn ngữ lập trình
nào đó sẽ được chương trình dịch của ngôn ngữ đó chuyển thành tập tin
chương trình chứa mã máy (native code/instruction). Giai đoạn này gọi
là Compile time.
Khi người dùng chạy ứng dụng, tập tin chương trình được hệ điều hành tải
vào bộ nhớ và bắt đầu thực hiện các lệnh chứa trong đó. Hệ điều hành đóng
vai trò môi trường hoạt động và là người quản lý việc thực thi của ứng dụng.
Giai đoạn tải và thực thi các lệnh gọi là Runtime.
Loại chương trình được hệ điều hành tải, thực thi và quản lý trực tiếp như
vậy thường được gọi là ứng dụng native. Ứng dụng native trong Windows
được tạo ra với các ngôn ngữ như C/C++, Pascal, Delphi, Visual Basic,....
Biên dịch và thực thi chương trình trong .NET Framework
Trong .NET Framework, mã nguồn viết bằng các ngôn ngữ lập trình không
được biên dịch trực tiếp thành mã máy để thực thi (trực tiếp trong hệ điều
hành) như các chương trình viết bằng C/C++ hay Pascal/Delphi.

5
Quy trình biên dịch và thực thi chương trình trong .NET Framework
Mã nguồn viết bằng một ngôn ngữ .NET (C#, Visual basic .NET) được trình
biên dịch của ngôn ngữ đó dịch thành tập tin mã đặc biệt gọi là tập tin mã
CIL (Common Intermediate Language). CIL là một loại ngôn ngữ trung gian
đặc biệt được Microsoft tạo ra cho nền tảng .NET.
CIL, trước đây còn được gọi là MSIL (Microsoft Intermediate Language), cũng chính là một
ngôn ngữ lập trình. Đây là ngôn ngữ cấp thấp nhất mà người lập trình còn có thể đọc được code.
Mã CIL cũng thường được gọi là bytecode.
Ở giai đoạn Runtime, một chương trình dịch đặc biệt có tên gọi là JIT (Just-
in-time compiler) đọc và dịch tiếp mã CIL thành mã máy để thực thi. Quá
trình thực thi này được kiểm soát bởi một chương trình đặc biệt gọi
là CLR (Common Language Runtime).
Như vậy có thể hình dung (gần đúng) rằng, chương trình viết bằng một
ngôn ngữ .NET sẽ chạy bên trong một chương trình khác (CLR) và bị chương
trình CLR này kiểm soát. Cả CIL và CLR đều là các thành phần của một hệ
thống tổng thể gọi là Common Language Intermediate (CLI).
Do việc biên dịch tất cả các ngôn ngữ .NET đều tạo ra CIL nên, ví dụ, một
object tạo ra bằng Visual Basic .NET có thể được truy xuất từ code viết trên
C#.

6
Ngôn ngữ lập trình C#
C# là một ngôn ngữ lập trình được phát triển riêng biệt cho .NET Framework
với phiên bản C# 1.0 ra đời vào năm 2002 cùng với .NET Framework 1.0.
Phiên bản hiện tại của C# (ở thời điểm viết tài liệu này) là 7.
Một số đặc điểm của ngôn ngữ C#
Ngôn ngữ C# chịu ảnh hưởng của nhiều ngôn ngữ lập trình khác, trong đó
có C++, Eiffel, Java. Lập trình viên đã làm việc với các ngôn ngữ như C/C++
hay Java sẽ rất dễ dàng tiếp thu C#.
C# được thiết kế theo hướng đơn giản, hiện đại, đa chức năng và hỗ trợ hầu
hết các nguyên tắc lập trình hiện có:
 Định kiểu mạnh (strong typing)
 Hướng mệnh lệnh (imperative programming)
 Hướng khai báo (declarative programming)
 Hướng hàm (functional programming)
 Hướng đối tượng (object-oriented programming)
 Hướng thành phần (component-oriented programming)
 Lập trình tổng quát (generic programming)
Bạn không nhất thiết phải hiểu hết các thuật ngữ “kỳ lạ” trên. Ở đây chỉ liệt
kê ra nhằm chứng minh sự mạnh mẽ và phong phú của ngôn ngữ C#. Tuy
nhiên, một số nguyên tắc trong số đó có ảnh hưởng rất lớn và trực tiếp đến
việc học lập trình C#.
Trình biên dịch C#
Để dịch mã nguồn C# thành chương trình cần sử dụng một trình biên dịch
(compiler) cho ngôn ngữ này.
Hiện nay đang tồn tại một số trình biên dịch C# khác nhau:
 Microsoft Visual C# của Microsoft được xem là chương trình “chính
thống”
 Mono và trình biên dịch C# mã nguồn mở (tương đương với .NET 3.5,
đồng thời cũng không hỗ trợ toàn bộ các class của .NET Framework)
 DotGNU và trình biên dịch C# mã mở (tương đương với .NET 2.0)
 Trình biên dịch C# của Microsoft’s Rotor project (chỉ hỗ trợ tới C# 2.0
Windows XP)
Nếu bạn đã cài đặt sẵn Visual Studio, bạn không cần để ý đến trình biên
dịch C# nữa. Mọi thứ đã được thiết lập sẵn sàng!

7
Một số điểm cần lưu ý
C# có cú pháp, các cấu trúc điều khiển, một số kiểu dữ liệu cơ sở,… rất
giống C/C++ và Java. Nếu bạn có xuất phát điểm từ các ngôn ngữ này có
thể dễ dàng nắm được các thành phần cơ bản của C#. Tuy nhiên đừng để
sự tương đồng này đánh lừa bạn.
C# là ngôn ngữ hướng đối tượng 100%. Tức là mọi thứ trong C# đều là lớp
(class). Do đó bạn sẽ không bao giờ nghe thấy các khái niệm như hàm toàn
cục, biến toàn cục trong C#. Đặc điểm này giống với Java. Nếu xuất phát
từ C/C++ bạn nên lưu ý vấn đề này.
Khi học lập trình C#, bạn đồng thời phải học thư viện .NET, mà trước hết là
các thành phần cơ bản của thư viện này. Sau đó bạn phải đầu tư thời gian
cho các kỹ thuật nâng cao của .NET và C#. Cuối cùng bạn nên lựa chọn và
đi sâu vào các công nghệ xây dựng trên nền tảng .NET.
Do đó, đừng suy nghĩ kiểu đốt cháy giai đoạn, muốn nhảy ngay vào các
công nghệ (như Windows Forms, ASP.NET). Nắm càng chắc C# và .NET,
bạn càng dễ dàng tiếp cận các công nghệ. Nếu nhảy ngay vào công nghệ,
bạn chỉ học được cái vỏ chứ không thể tiến xa được.

8
Cài đặt Visual Studio, tạo Solution và Project C#, C#
Interactive
Lập trình với C# nói riêng và các ngôn ngữ .NET khác nói chung hầu như
đều sử dụng Visual Studio – môi trường phát triển ứng dụng tích hợp tốt
nhất của Microsoft cho các công nghệ .NET. Bài học này sẽ hướng dẫn bạn
cài đặt Visual Studio, lựa chọn workload và các thành phần cần thiết cho
việc học lập trình C# căn bản. Cuối cùng chúng ta sẽ xây dựng một project
thử nghiệm đầu tiên cho chương trình Hello world với C#.

Trình soạn thảo code cho C#


Việc lập trình C# có thể thực hiện trên một chương trình xử lý văn bản đơn
giản như Notepad hay Notepad++. Để dịch mã nguồn C# thành chương
trình có thể sử dụng trình biên dịch (C# compiler) qua giao diện dòng lệnh.
Tuy nhiên, phương pháp này không phù hợp để phát triển các ứng dụng
phức tạp.
Cách thức hiệu quả nhất là sử dụng một môi trường phát triển ứng dụng
tích hợp (Integrated Development Environment - IDE).
Hiện nay có một số IDE hỗ trợ các công nghệ .NET như Visual Studio, Visual
Studio Code, MonoDevelop, Morfik (cho phát triển ứng dụng web),
SharpDevelop, hay Turbo C#.
Trong khóa học này chúng ta sẽ sử dụng Visual Studio.
Visual Studio của Microsoft là IDE hiệu quả nhất trên Windows cho C# (và
các ngôn ngữ .NET khác). IDE này cung cấp tất cả những công cụ cần thiết
để phát triển tất cả các loại ứng dụng .NET ở mọi cấp độ phức tạp.
Phiên bản mới nhất của Visual Studio tại thời điểm viết tài liệu này là Visual
Studio 2019. Có 3 bản Visual Studio khác nhau để lựa chọn: Ultimate,
Professional, Community (miễn phí). Để thực hiện dự án này chỉ cần sử
dụng bản Community.
Các phiên bản của Visual Studio không có nhiều khác biệt đối với học lập trình C# căn bản.
Tuy nhiên chúng tôi khuyến khích sử dụng phiên bản từ 2017 trở lên. Một số cú pháp/cấu trúc
mới của C# không được hỗ trợ ở các phiên bản thấp hơn.

Thực hành 1: cài đặt Visual Studio


Tải và chạy Visual Studio Installer
Trước hết tải Visual Studio Installer tại:
https://visualstudio.microsoft.com/downloads/

9
Visual Studio Installer là chương trình hỗ trợ cài đặt cho Visual Studio. Bắt
đầu từ phiên bản 2017, việc cài đặt (và cập nhật/điều chỉnh) các thành phần
của Visual Studio 2017 đều được thực hiện thông qua chương trình này.
Tất cả cài đặt đều yêu cầu kết nối Internet vì Visual Studio Installer sẽ tải các gói phần mềm từ
server của Microsoft.
Khi chạy lần đầu, Visual Studio Installer có giao diện như sau:

Giao diện Visual Studio Installer


Giao diện chương trình này chia thành các tab:
 Workloads: tiện lợi cho việc chọn nhóm các thành phần có liên quan
cho từng mục đích làm việc.
 Individual components: cho phép chọn các thành phần riêng rẽ để cài
đặt.
 Language packs: cho phép chọn các gói ngôn ngữ giao diện
 Installation locations: cho phép lựa chọn đường dẫn để cài đặt các
thành phần.

10
Danh sách bên phải (Installation details) liệt kê các thành phần đang được
lựa chọn.
Lựa chọn workload
Để thực hiện dự án này chúng ta chỉ cần chọn workload “.NET desktop
development”, trong đó có hỗ trợ phát triển ứng dụng dòng lệnh Console,
ứng dụng Windows Forms, ứng dụng Windows Presentation Foundation.
Bấm “Install” để bắt đầu download và cài đặt Visual Studio 2017
Community.

Quá trình cài đặt Visual Studio 2017 Community


Thời gian cài đặt phụ thuộc nhiều vào tốc độ Internet.
Sau khi cài đặt xong, giao diện của Visual Studio Installer sẽ trở thành như
sau:

11
Hoàn tất cài đặt Visual Studio 2017
Nếu cần thêm bớt các thành phần của Visual Studio thì bấm nút “Modify”,
bạn sẽ được trả về màn hình lựa chọn thành phần cài đặt như lúc trước. Ấn
nút “Launch” để khởi động Visual Studio.

12
Giao diện chính của Visual Studio
Thực hành 2: tạo project C# đầu tiên – Hello World
Tạo project trong Visual Studio
Chọn File => New => Project hoặc bấm tổ hợp Ctrl + Shift + N

13
Tạo một project mới
Chọn loại project là Console App (.NET Framework). Khi chọn loại project
chúng ta có thể thiết lập các tham số sau:
 Name: tên dự án, đây cũng là tên mặc định của chương trình về sau
 Location: thư mục chứa tất cả tập tin của dự án
 Solution name: solution cho phép quản lý nhiều dự án có liên quan
(và sử dụng chung code với nhau)
 Framework: lựa chọn phiên bản của .NET Framework; nếu lựa chọn
phiên bản nào, khi triển khai ứng dụng đòi hỏi trên hệ thống của người
dùng phải cài đặt .NET Framework phiên bản tương đương hoặc cao
hơn
 Create directory for solution: tốt nhất luôn check mục này, đặt tất cả
các dự án trong cùng một thư mục chung
 Add to source control: chọn mục này nếu bạn sử dụng một chương
trình kiểm soát mã nguồn nào đó (như Git). Mục này tạm thời không
check

14
Điền thông tin như sau:

Điền thông tin cho dự án


Viết code cho chương trình đầu tiên “Hello world”
Click đúp vào tập tin “Program.cs” để mở trong trình soạn thảo code.
Thêm code như sau vào thân phương thức static void Main(string[]
args)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello world from C#");
Console.WriteLine("Press any key to quit");
Console.ReadKey();
}
}
}

15
Giao diện code editor của Visual Studio
Nếu để ý chúng ta sẽ thấy, khi gõ một vài ký tự, Visual Studio sẽ tự động
liệt kê các code có chứa những chữ này. Chúng ta có thể trực tiếp lựa chọn
bằng cách di chuyển tới mục mong muốn bằng các phím mũi tên, sau đó ấn
phím tab mà không cần gõ hết từ.
Tính năng này của trình soạn thảo C# trong Visual Studio được gọi
là IntelliSense. IntelliSense giúp việc soạn thảo code C# đặc biệt nhanh
chóng và tiện lợi, cũng như giúp giải phóng người lập trình khỏi việc phải
ghi nhớ máy móc tất cả các tên gọi. Sau này chúng ta sẽ còn sử dụng nhiều
tính năng khác của IntelliSense.
Biên dịch và chạy debug
 Dịch và chạy chương trình ở chế độ debug: F5
 Dịch và chạy chương trình (không debug): Ctrl + F5
Cũng có thể gọi các lệnh này qua menu Debug => Start Debugging hoặc
Start Without Debugging

16
Chọn lệnh debug
Kết quả chạy chương trình

Kết quả chạy chương trình


Mặc dù chương trình của chúng ta chưa thực hiện được chức năng gì nhưng
đến đây xin chúc mừng bạn đã viết được chương trình đầu tiên với C# và
.NET Framework.
Debug là chế độ hoạt động mà chương trình được gắn vào một chương trình đặc biệt gọi là
debugger (ở đây là Visual Studio Debugger) để giúp chúng ta theo dõi được hoạt động của
chương trình, như là giá trị của các biến.
Chương trình chạy ở chế độ debug có thể được dừng tại bất kỳ câu lệnh nào (bằng cách đánh
dấu break ở câu lệnh đó) và tiếp tục chạy/dừng theo yêu cầu của người lập trình.
Debug giúp chúng ta phát hiện các lỗi logic của chương trình. Đối với lỗi cú pháp, trình soạn
thảo của C# có thể trực tiếp hiển thị trong quá trình viết code mà không cần chạy thử.

17
Thực hành 3: thêm project mới vào solution
Trong bài Thực hành 2, chúng ta mới tạo ra một Project đầu tiên cho
Solution.
Trong Visual Studio, một Solution có thể chứa nhiều Project. Chúng ta sẽ
thêm một Project thứ hai vào Solution. Cách thực hiện xem trong hình minh
họa dưới đây.

Sau bước này chúng ta sẽ gặp lại giao diện chọn loại Project như đã biết.
Điền các thông tin cần thiết và ấn OK để thêm Project mới vào Solution.

Thực hành 4: tạo Blank Solution


Nếu một Solution không chứa một Project nào nó được gọi là Solution trống
(blank solution). Một Solution mà không chứa Project nào thì có giá trị gì?
Bạn hẳn sẽ đặt câu hỏi đó.
Trên thực tế sẽ có nhiều lúc bạn muốn tạo một Solution như vậy.
Ví dụ, bạn đã có sẵn một số project rời rạc. Bạn có thể tạo một blank solution
sau đó lần lượt thêm project sẵn có vào solution này.
Hãy cùng tạo một blank solution.
Chọn File => New => Project như bạn đã thực hiện ở trên.

18
Trong hộp thoại New Project tìm đến nút Other Project Types\ Visual Studio
Solutions.
Chọn Blank Solution. Đặt cho nó một cái tên và chọn đường dẫn.

Ấn OK, bạn sẽ thu được một blank solution trống trơn. Giờ bạn có thể thoải
mái thêm project mới hoặc thêm project sẵn có vào đây.

Sử dụng C# Interactive
Khi học lập trình C# cơ bản, rất nhiều khi bạn sẽ phải thử nghiệm các cú
pháp, kiểu dữ liệu, cấu trúc điều khiển,… Đôi khi cái cần thử nghiệm chỉ là
một vài dòng code đơn giản. Nếu phải liên tục tạo project mới chỉ để thử
nghiệm một vài dòng lệnh sẽ rất bất tiện và mất thời gian.
Visual Studio cung cấp một công cụ hỗ trợ trong trường hợp này: C#
interactive. Đây là một giao diện dòng lệnh đặc biệt cho phép bạn viết và
thực thi từng dòng, hoặc một nhóm lệnh C# mà không cần tạo project.
Để mở giao diện C# Interactive, từ giao diện Visual Studio chọn View =>
Other Windows => C# Interactive.

19
Để tiện lợi bạn có thể đánh dấu ghim hoặc tách cửa sổ này ra khỏi Visual
Studio.
Từ dấu nhắc lệnh bạn có thể nhập bất kỳ lệnh nào của C#. Nếu ấn enter thì
lệnh đó sẽ được thực thi ngay lập tức.

Nếu muốn viết cả một nhóm lệnh bao gồm nhiều dòng, khi kết thúc mỗi
dòng bạn ấn tổ hợp Shift + Enter. Khi này dòng lệnh sẽ không được thực
thi ngay. Bạn có thể tiếp tục viết thêm các dòng lệnh nữa.

20
Khi kết thúc dòng lệnh cuối cùng bạn ấn Enter. Khi đó cả nhóm lệnh sẽ được
thực hiện.
Có một số lưu ý sau khi sử dụng C# Interactive:
 C# Interactive chỉ xuất ra được console (Console.WriteLine,
Console.Write) nhưng không thể đọc được dữ liệu. Nếu viết các lệnh
đọc (Console.ReadLine, Console.ReadKey), giao diện này sẽ bị đơ.
 Nếu giao diện bị đơ, bạn chỉ cần khởi động lại Visual Studio là xong.
 Giao diện này hỗ trợ Intellisense như giao diện viết code bình thường.
 Bạn có thể sử dụng bất kỳ lệnh nào (trừ vài thứ gây treo giao diện
như đã biết).
 Nếu muốn in kết quả của một biến ra màn hình, bạn có thể sử dụng
Console.Write() hoặc Console.WriteLine() như đối với ứng dụng
console.
 Hoặc đơn giản là viết tên biến và ấn Enter (không viết chấm phẩy
sau tên biến).
Trong một số bài học tiếp theo chúng ta sẽ sử dụng C# Interactive cho tiện
lợi.

21
Cấu trúc dự án C# – solution, project, tập tin mã
nguồn
Solution và project là hai cấp độ quản lý tập tin mã nguồn của C# và các
thành phần hỗ trợ khác. Trong mỗi tập tin mã nguồn, namespace là cấp độ
quản lý code cao nhất. Tất cả những khái niệm này và cách làm việc với
chúng đóng vai trò xương sống để có thể làm việc với C#.
Bài học này sẽ giúp bạn hiểu chi tiết các thành phần nêu trên.

Cấu trúc solution/project của C#


C# quản lý mã nguồn theo cấu trúc cây gần giống với cấu trúc thư mục và
bao gồm hai cấp độ cơ bản: Project và Solution.
Trong nội dung thực hành của phần cài đặt Visual Studio, chúng ta đã tạo
ra một project thuộc loại Console App bên trong một solution. Theo
đó solution được đặt tên là BOOKMAN, còn project đặt tên là ConsoleApp.
Trong suốt giai đoạn học lập trình C# cơ bản chúng ta sẽ chỉ xây dựng ứng dụng với giao diện
dòng lệnh (Console App). Đây là loại ứng dụng đơn giản nhất thường sử dụng trong việc học
lập trình cơ bản.

Solution và Project trong C#

22
Project trong C# là gì?
Project (dự án) là cấp độ quản lý mã nguồn quan trọng nhất của C#. Mỗi
project sau khi biên dịch sẽ tạo ra một chương trình.
Mỗi project mặc định đều chứa:
 các tập tin mã nguồn: là các tập tin văn bản có phần mở rộng .cs (viết
tắt của C sharp).
 các tập tin cấu hình của chương trình: là tập tin xml có phần mở rộng
.config.
 các thư viện được tham chiếu tới (References): là danh sách các tập
tin thư viện chuẩn của .NET Framework, hoặc thư viện từ các hãng
thứ ba, hoặc chính các project khác, chứa các class được sử dụng bởi
các class trong project này.
 các thuộc tính (Properties): bao gồm nhiều loại thông tin khác nhau
quyết định những tính chất quan trọng của project, như phiên bản
của .NET Framework được sử dụng, loại chương trình mà dự án này
sẽ được dịch thành, các tài nguyên được sử dụng trong project, cấu
hình của ứng dụng,.... Visual Studio cung cấp giao diện đồ họa để có
thể dễ dàng quản lý các thông tin này. Giao diện này mở ra khi click
đúp vào mục Properties của project.
Tất cả các thành phần của một project đều đặt chung trong một thư mục
cùng tên với project.
Solution trong C# là gì?
Solution (giải pháp) là cấp độ quản lý mã nguồn cao nhất trong C# cho
phép quản lý tập trung nhiều project.
Mỗi solution trong C# có thể chứa nhiều project. Nếu solution không chứa
project nào, nó gọi là Empty Solution. Tại mỗi thời điểm Visual Studio chỉ
có thể mở một solution.
Trên giao diện Visual Studio, solution và các project của nó được hiển thị
trong một cửa sổ riêng gọi là Solution Explorer. Cửa sổ này hiển thị tất cả
các thành phần trong dự án C# theo cấu trúc cây, với solution làm gốc, các
project là các nhánh trực tiếp xuất phát từ gốc này.
Ngoài ra, để tiện lợi trong việc quản lý các project thành viên, solution cho
phép tạo thêm các thư mục, gọi là Solution Folder, trong đó lại có thể chứa
các project khác. Cấu trúc quản lý này cho phép quản lý một số lượng lớn
project một cách dễ dàng. Các project trong cùng một solution thường có
quan hệ nhất định với nhau.

23
Nếu vô tình đóng cửa sổ Solution Explorer có thể mở lại bằng cách chọn View => Solution
Explorer hoặc bấm tổ hợp Ctrl + Alt + L.
Quy ước đặt tên solution và project của C#
Đối với solution, quy tắc đặt tên tương tự như đối với tập tin và thư mục
trong Windows.
Tuy nhiên, theo “truyền thống” của C#, tên solution nên đặt theo quy ước:
1. Bắt đầu bằng chữ hoa
2. Sử dụng PascalCasing (viết hoa chữ đầu mỗi từ) nếu chứa nhiều từ
3. Không nên chứa dấu cách hoặc số.
Đối với project, quy tắc đặt tên chặt chẽ hơn vì tên project mặc định sẽ
được sử dụng làm namespace (không gian tên – xem trong phần tiếp theo)
của tất cả các tập tin mã nguồn của project đó. Tuy nhiên, Visual Studio có
thể tự động chuyển đổi tên cho phù hợp quy tắc.
Một trong những quy ước thường được sử dụng để đặt tên project là:
1. Chứa tên solution
2. Đặt tên riêng của project và phân tách với phần solution bằng một
dấu chấm
3. Tuân thủ các quy ước đặt tên solution ở trên
Quy ước đặt tên solution và project này rất có ích vì nó giúp tự động tạo ra
cấu trúc namespace (xem dưới đây) phù hợp cho các tập tin mã nguồn.

Cấu trúc tập tin/thư mục của C# project


Nếu ở giai đoạn tạo project chúng ta đánh dấu lựa chọn “Create directory
for solution”, Visual Studio sẽ tạo ra một thư mục có tên được xác định
trong mục “Solution name”. Mỗi project được tạo ra sẽ đặt trong một thư
mục con của thư mục solution và có cùng tên với project. Tất cả tập tin của
một project sẽ nằm trong thư mục này.
Toàn bộ cấu hình của solution được lưu trong một tập tin có phần mở
rộng .sln nằm trong thư mục chứa solution. Thông tin cấu hình của dự án
được lưu trong tập tin có tên trùng tên dự án và phần mở rộng .csproj.

24
Thư mục solution

Thư mục project


Thư mục bin
Sau khi biên dịch project thành công, trong thư mục của nó sẽ xuất hiện
một thư mục con có tên là bin. Thư mục này không xuất hiện trong cấu trúc
dự án hiển thị ở Solution Explorer.
Nếu biên dịch ở chế độ debug, trong thư mục bin sẽ xuất hiện thư mục con
Debug. File chương trình sau khi biên dịch (ở chế độ debug) xong sẽ xuất
hiện trong thư mục này.
Nếu chạy chương trình ở chế độ debug (phím F5), thư mục Debug này sẽ
trở thành thư mục hiện hành của chương trình đang chạy thử. Tất cả các
tập tin cấu hình và tập tin tài nguyên (nếu có) cũng sẽ được tự động copy
vào thư mục này.

25
Thư mục bin\Debug
Như vậy, nếu bạn muốn tìm tập tin chạy của chương trình sau khi biên dịch,
hãy mở theo đường dẫn {tên solution}\{tên project}\bin\{Debug}.
.NET assembly
Chúng ta lúc trước có nói rằng mỗi project sau khi biên dịch xong sẽ thành
một chương trình. Tuy nhiên cách nói “chương trình” không hoàn toàn phù
hợp đối với .NET Framework.
Trong phần nói về .NET Framework chúng ta đã biết, mỗi project sau khi
biên dịch đều trở thành một tập tin chứa bytecode CIL. File mã CIL này được
gọi là Assembly. .NET Framework phân biệt hai loại assembly: một loại có
thể tự nạp vào CLI và thực thi; một loại không thể tự mình nạp vào CLI mà
cần phải có một assembly thuộc loại thứ nhất gọi, hoặc được một tiến trình
khác gọi.
Loại assembly thứ nhất được lưu trong tập tin có phần mở rộng .exe, tương
tự như các tập tin chương trình thực thi khác trong Windows. Loại assembly
thứ hai được lưu trong các tập tin có phần mở rộng .dll (Dynamic Link
Library), tương tự như các tập tin thư viện của Windows. Việc biên dịch ra
.exe hay .dll phụ thuộc vào loại project. Ứng dụng console chúng ta vừa tạo
thuộc loại thứ nhất.

26
Cú pháp C# cơ bản: lệnh, từ khóa, ghi chú, code
block, định danh
Bài học này sẽ cung cấp cho bạn những kiến thức cơ bản nhất của C# mà
bạn sẽ phải sử dụng thường xuyên trong quá trình học lập trình C#, bao
gồm lệnh và khối code (code block), từ khóa, ghi chú, entry point của
chương trình C#.

Câu lệnh (statement) và khối code (code block)


Câu lệnh trong C#
Bên trong phương thức Main bạn đã viết ba câu lệnh (statement), hai lệnh
viết ra màn hình và một lệnh đọc từ bàn phím.
Console.WriteLine("Hello world from C#");
Console.WriteLine("Press any key to quit");
Console.ReadKey();
Câu lệnh là một hành động chúng ta yêu cầu chương trình thực hiện, ví dụ:
khai báo biến, gán giá trị, gọi phương thức, duyệt danh sách, xử lý theo
điều kiện giá trị,....
Trình tự thực hiện các lệnh được gọi là luồng điều khiển (flow of control)
hay luồng thực thi (flow of execution).
Một câu lệnh có thể chứa một dòng code duy nhất, gọi là câu lệnh
đơn (single line statement). C# bắt buộc câu lệnh đơn phải kết thúc
bằng dấu chấm phẩy.
Ví dụ, trong thân của phương thức Main() bạn đã viết 3 câu lệnh đơn.
Console.WriteLine("Hello world from C#");
Console.WriteLine("Press any key to quit");
Console.ReadKey();
Một câu lệnh nếu chỉ chứa dấu chấm phẩy được gọi là một lệnh rỗng (empty
statement).
Dưới đây là một số loại câu lệnh trong C#:
 Declaration statements: lệnh khai báo, dùng để khai báo các biến và
hằng;
 Expression statements: lệnh tính toán, thường gọi là biểu thức, dùng
để thực hiện các tính toán trên dữ liệu và phải trả về giá trị có thể
gắn cho biến;
 Selection statements: lệnh lựa chọn, dùng trong các cấu trúc rẽ
nhánh như if, else, switch, case;
27
 Iteration statements: lệnh lặp, dùng để thực hiện nhiều lần một
lệnh/khối lệnh, bao gồm do, for, foreach, in, while.
Hiện tại chúng ta không giải thích chi tiết các loại lệnh này. Các loại lệnh sẽ
lần lượt được xem xét chi tiết khi gặp trong dự án.
Khối code trong C#
Một chuỗi câu lệnh đơn có thể được nhóm lại với nhau tạo thành một khối
lệnh (code block hoặc statement block).
Một khối lệnh là một danh sách các lệnh được đặt chung trong một cặp dấu
ngoặc kép {}.
Các khối lệnh có thể lồng nhau. Như trong tập tin mã nguồn dưới đây, toàn
bộ thân của phương thức Main() ở trên là một khối code. Thân của cả lớp
Program cũng là một khối code. Thân của namespace là một khối code. Ba
khối code này lồng nhau.

Bạn sẽ gặp khối code là thân của namespace, khai báo kiểu
(class/struct/enum/interface), khai báo phương thức, cấu trúc điều khiển.
Thậm chí có thể có khối code tự do.

28
Ghi chú (comment) và khoảng trắng
Ghi chú và khoảng trắng là những thông tin không ảnh hưởng đến quá trình
dịch. Chúng có nhiệm vụ hỗ trợ người lập trình nhưng sẽ bị compiler bỏ qua
khi dịch mã nguồn.
Ghi chú (comment)
Mọi ngôn ngữ lập trình đều cung cấp khả năng ghi lại lời chú thích trực tiếp
trong code (mà không ảnh hưởng đến việc dịch code và thực hiện chương
trình). Ghi chú dành cho lập trình viên. Compiler sẽ tự động loại bỏ ghi chú
khi dịch mã nguồn.
C# cung cấp 3 loại ghi chú khác nhau: ghi chú trên 1 dòng, ghi chú trên
nhiều dòng, và ghi chú tài liệu.
Ghi chú trên một dòng sử dụng cặp dấu “//” trước lời ghi chú, những gì đi
sau cặp dấu này trên dòng đó được xem là ghi chú.
Ví dụ:
Console.WriteLine("Hello world"); // lệnh này viết dòng chữ Hello
world ra màn hình
Ghi chú trên nhiều dòng sử dụng cặp dấu “/*” ở đầu khối và cặp “*/” ở cuối
khối. Ví dụ:
/*
Đây là ghi chú trên nhiều dòng.
Lệnh Console.WriteLine sẽ in dòng văn bản ra màn hình.
Lệnh Console.ReadKey sẽ dừng màn hình chờ người dùng ấn một phím
bất kỳ trước khi tiếp tục thực thi.
*/
Console.WriteLine("Hello world");
Console.ReadKey();
Hai loại ghi chú trên giống như trong C/C++.
Trong Visual Studio, bạn có thể nhanh chóng đánh dấu chú thích cho nhiều dòng code bằng
cách chọn tất cả các dòng cần đánh dấu và ấn tổ hợp phím Ctrl + K + C. Để hủy đánh dấu chú
thích đã có, chọn các dòng tương ứng và bấm tổ hợp Ctrl + K + U.
Ghi chú tài liệu (documentation comment) là loại ghi chú đặc biệt của C#,
cho phép tạo ra một dạng “hướng dẫn sử dụng” của đơn vị code được ghi
chú (như class, method, interface,...).
Loại ghi chú này cho phép người xây dựng class đưa ra các hướng dẫn cơ
bản mà người sử dụng class có thể đọc. Loại ghi chú này cũng cho phép
trình biên dịch lọc riêng ra để tạo thành tài liệu hướng dẫn cho code. Ghi

29
chú tài liệu được Visual Studio sinh tự động khi gõ cụm “///” trước đối tượng
cần chú thích.
/// <summary>
/// class for cars
/// </summary>
class Car
{
// the class body is still empty
// TODO: add more member (field and property)
}

Trong ghi chú tài liệu có thể sử dụng các thẻ xml để mô tả nội dung cho các
thành phần. Visual Studio sử dụng các thông tin này để tạo ra trợ giúp
Intellisense cho đối tượng được chú thích.

Nên hình thành thói quen viết chú thích đầy đủ cho code. Nó giúp ích rất
nhiều cho việc bảo trì code hoặc làm việc nhóm.
Khoảng trắng
Khoảng trắng (whitespace) là những ký tự “không nhìn thấy” trong code.
Trong C# các ký tự sau được xem là khoảng trắng: ký tự space, tab, ký tự
báo dòng mới (new line), ký tự về đầu dòng (carriage return).
Giống như comment, các ký tự trắng trong C# không có giá trị đối với
compiler. Nói cách khác, compiler sẽ tự động bỏ qua các ký tự trắng thừa.
Tuy nhiên, các ký tự trắng có vai trò rất quan trọng trong định dạng code,
giúp code dễ đọc hơn.
Việc sử dụng ký tự trắng để định dạng code được Visual Studio xử lý rất tốt,
giúp mã nguồn C# trong Visual Studio đặc biệt sáng sủa, dễ đọc. Visual
Studio đưa ra một loạt các quy tắc định dạng cho từng ngôn ngữ gọi là code
style. Bạn có thể can thiệp vào các thiết lập này qua cửa sổ Options (Tools
=> Options), chọn node Text Editor và ngôn ngữ cần điều chỉnh.

30
Trong quá trình viết code, bạn có thể sử dụng tổ hợp Ctrl + K + D để tự
động định dạng lại mã nguồn theo các quy ước viết code. Hãy thường xuyên
sử dụng tổ hợp này để code được ngăn nắp và quy củ.

Từ khóa (keyword) của C#

File mã nguồn đầu tiên (Program.cs)


Trong ảnh chụp code bạn để ý thấy có một số từ được thể hiện bằng màu
xanh dương như using, namespace, class, static, void, string. Đây là các từ
khóa trong C#.

31
Từ khóa (keyword) là những từ được ngôn ngữ gán cho ý nghĩa riêng xác
định, là nòng cốt của cú pháp ngôn ngữ. Bạn không được sử dụng từ khóa
cho mục đích gì khác ngoài những gì đã được ngôn ngữ quy định. Ví dụ, bạn
không được sử dụng từ khóa làm định danh (tên) của biến, hằng, phương
thức.
Dưới đây là danh sách từ khóa trong C#.

Danh sách từ khóa của C#


Từ khóa của C# phân chia làm 2 loại: từ khóa dành riêng (reserved
keyword) và từ khóa theo ngữ cảnh (contextual keyword). C# có 79 từ khóa
dành riêng và 25 từ khóa ngữ cảnh (contextual keyword).
Bạn sẽ dễ dàng nhận thấy từ khóa khi viết code bằng Visual Studio.
Từ khóa ngữ cảnh là những từ chỉ được xem là từ khóa trong những đoạn code nhất định. Ở
những chỗ khác nó không được xem là từ khóa.
Dưới đây là danh sách từ khóa ngữ cảnh của C#:

Từ khóa ngữ cảnh trong C#


Lưu ý, bạn không cần ghi nhớ chúng. Đại đa số các từ khóa này chúng ta
sẽ gặp lại trong các phần khác nhau của bài giảng.

Định danh (identifier)


Định danh (identifier) là chuỗi ký tự của ngôn ngữ dùng để đặt tên cho các
thành phần như kiểu, biến, hằng, phương thức, tham số,....

32
Quy tắc đặt định danh
Định danh trong C# chỉ được tạo ra từ một nhóm ký tự xác định chứ không
được chứa mọi loại ký tự. Nhìn chung, định danh trong C# có thể chứa các
chữ cái a-z, A-Z, chữ số 0-9, dấu gạch chân _ và ký tự @.
Tuy vậy, có những giới hạn nhất định:
 Ký tự a-z, A-Z và _ có thể có mặt ở mọi vị trí trong định danh.
 Chữ số không được phép đứng đầu định danh.
 Ký tự @ chỉ được phép đứng đầu định danh (và cũng không được
khuyến khích sử dụng).
 Khi đặt định danh có sự phân biệt giữa ký tự hoa và thường. Ví dụ
write và Write là hai định danh (của phương thức) hoàn toàn khác
nhau. Điều này khác biệt với ngôn ngữ như Pascal vốn không phân
biệt ký tự hoa và thường.
Quy ước đặt định danh
Ngoài các quy tắc trên, ứng với mỗi loại thành phần có thêm những quy ước
đặt định danh. Những quy ước này giúp tạo ra hệ thống tên gọi thống nhất
xuyên suốt chương trình, giúp việc bảo trì và đọc code dễ dàng hơn. Khi
gặp từng loại thành phần chúng ta sẽ nhắc đến quy ước cụ thể của nó.
Có hai loại quy ước thường gặp: PascalCase và camelCase.
PascalCase là quy ước đặt định danh trong đó:
 Phải bắt đầu bằng chữ cái in hoa
 Nếu định danh chứa nhiều từ thì các từ đều viết hoa chữ cái đầu tiên
Bản thân cái tên PascalCase minh họa chính xác các quy ước này: bắt đầu
là P (hoa), bao gồm hai từ Pascal và Case thì viết hoa cả P và C. PascalCase
thường dùng để đặt tên kiểu (class, struct, enum, interface, delegate),
tên phương thức (method), tên các biến/thuộc tính thành viên public của
struct/class.
Quy ước camelCase hơi khác một chút, trong đó riêng ký tự đầu tiên là chữ
cái thường. Chữ cái đầu của các từ tiếp theo viết hoa. Lối viết camelCase
thường dùng để đặt tên biến cục bộ, tham số, biến thành viên private của
struct/class.

Lớp Program và phương thức Main()


C# là một ngôn ngữ lập trình hướng đối tượng, trong mỗi project bắt buộc
phải có ít nhất một lớp. Lớp là đơn vị code quan trọng bậc nhất trong C#,

33
và cả quá trình học ngôn ngữ sau này hầu đều tập trung vào các kỹ thuật
xây dựng lớp.
Program là lớp duy nhất trong dự án của chúng ta đến giờ, được C# tự động
sinh ra khi tạo project. Tên của lớp này không bắt buộc là Program. Bạn có
thể thay đổi thành bất kỳ tên gọi nào, miễn phù hợp với quy tắc đặt tên.
Trong lớp Program có một phương thức (method) đặc biệt:
static void Main(string[] args)
{
Console.WriteLine("Hello world from C#");
Console.WriteLine("Press any key to quit");
Console.ReadKey();
}
Phương thức này có tên gọi riêng là “entry point”. Main() là phương thức
đầu tiên được gọi khi chạy chương trình C#. Đây là điểm khởi đầu trong
hoạt động của các chương trình C#.
Lưu ý rằng, ngôn ngữ C# phân biệt chữ hoa – thường, giống như C/C++. Do đó, Main và main
không giống nhau, Writeline khác với WriteLine.
Phương thức entry point của C# bắt buộc phải là static void Main() hoặc
static int Main(). Phần tham số không bắt buộc. Nói chung bạn có thể
gặp các biến thể sau của phương thức Main():
// int return type, array of strings as the parameter.
static int Main(string[] args)
{
// Must return a value before exiting!
return 0;
}

// No return type, no parameters.


static void Main()
{
}

// int return type, no parameters.


static int Main()
{
// Must return a value before exiting!
return 0;
}
Như bạn đã biết trong phần giới thiệu về .NET Framework, mỗi project sẽ được dịch thành một
assembly, thuộc về một trong hai loại: .exe, hoặc .dll. Phương thức Main bắt buộc phải có trong
project dịch ra .exe, nhưng không bắt buộc nếu project dịch ra .dll.

34
Biến và hằng trong C#
Biến trong C# hay bất kỳ ngôn ngữ lập trình nào được dùng để lưu trữ thông
tin tạm thời để sau tái sử dụng. Tương tự, hằng cũng được dùng để lưu trữ
thông tin cho tái sử dụng. Thông tin chứa trong biến có thể thay đổi, còn
hằng thì không. Những vấn đề cơ bản này chắc chắn bạn đã nắm rất rõ khi
học nhập môn lập trình, dù là bằng ngôn ngữ nào cũng vậy.
Cách khai báo và sử dụng biến/hằng trong C# rất gần gũi với C/C++ hay
Java (và các ngôn ngữ dùng cú pháp của C). Tuy nhiên, có một số điểm
khác biệt nhất định phải biết.
C# cung cấp một số loại biến khác nhau:
 Biến cục bộ: lưu trữ thông tin trong phạm vi phương thức.
 Biến thành viên: lưu trữ thông tin trong phạm vi class hoặc struct.
 Tham số: tạm thời lưu trữ thông tin để truyền vào phương thức.
Bài học này sẽ cung cấp những thông tin đầy đủ và chi tiết về biến cục bộ
trong C#. Biến thành viên sẽ xem xét trong phần nói về class. Tham số sẽ
xem xét khi khảo sát chi tiết về phương thức.

Khai báo biến trong C#


Biến trong C# được khai báo với cú pháp sau:
datatype identifier;
Trong đó datatype là tên kiểu dữ liệu mà biến đó có thể lưu giữ, identifier là
định danh (tên) của biến.
Ví dụ sau khai báo biến có tên là i, lưu giữ được các giá trị số nguyên (int).
int i;
Tuy nhiên, trong C#, lệnh khai báo biến trên mặc dù đúng cú pháp nhưng
compiler lại không cho phép bạn dùng ngay biến i trong các biểu thức. C#
bắt buộc biến phải được gán giá trị trước khi sử dụng.
Để gán giá trị cho biến bạn dùng toán tử gán (assignment operator):
i = 10;
Phép gán trong C# đơn giản chỉ là một dấu bằng (=).
Có thể kết hợp cả khai báo biến và gán giá trị vào cùng một lệnh:
int i = 10;
Có thể khai báo và gán giá trị cho nhiều biến cùng kiểu trong cùng một
lệnh:
int x = 10, y = 20; // x và y có cùng kiểu int

35
Nếu các biến khác kiểu, bạn bắt buộc phải khai báo trong các lệnh khác
nhau:
int x = 10;
bool y = true; // biến chứa giá trị logic true/false
// lệnh khai báo dưới đây là SAI
int x = 10, bool y = true; // compiler sẽ báo lỗi ở dòng này
Biến cục bộ trong C# được đặt tên theo quy tắc đặt định danh, đồng thời
nên tuân thủ quy ước camelCase.

Khởi tạo biến trong C#


Trình biên dịch C# bắt buộc mọi biến phải được khởi tạo với một giá trị nào
đó trước khi sử dụng trong biểu thức. Đây là một ví dụ về sự chú trọng tới
sự an toàn trong C#. Trong khi các ngôn ngữ khác thường chỉ coi việc sử
dụng biến mà không gán giá trị trước là một dạng cảnh báo (warning), C#
coi đây là một lỗi.
Các bạn có thể gặp các từ khởi tạo (initialize) biến hay gán giá trị (assign) đôi khi được sử dụng
lẫn lộn. Trong C#, đối với biến thuộc các kiểu như int, bool thì khởi tạo hay gán là như nhau.
Đối với các kiểu dữ liệu tham chiếu (như class sẽ học trong phần lập trình hướng đối tượng),
khởi tạo và gán giá trị là các lệnh khác nhau.
C# có hai phương pháp để đảm bảo biến được khởi tạo trước khi sử dụng.
Nếu biến là một trường dữ liệu trong class hoặc struct (bạn sẽ học sau):
nếu không được lập trình viên trực tiếp gán giá trị, C# compiler sẽ tự động
gán cho biến một giá trị mặc định tùy từng kiểu dữ liệu (ví dụ 0 cho số
nguyên).
Nếu biến nằm trong thân phương thức (gọi là biến cục bộ – local variable):
bắt buộc lập trình viên phải trực tiếp gán giá trị trước khi sử dụng nó trong
biểu thức. Trong trường hợp này, biến không nhất thiết phải được khởi tạo
ngay khi khai báo. Miễn sao nó phải có giá trị trước khi sử dụng là được. Vi
phạm này bị C# coi là lỗi và compiler sẽ dừng lại.
Dưới đây là một ví dụ LỖI về sử dụng biến không khởi tạo:
static void Main()
{
int d;
Console.WriteLine(d); // Sẽ báo lỗi ở đây. Biến d chưa có giá trị.
}
Nếu biên dịch đoạn code trên bạn sẽ gặp lỗi Use of unassigned local
variable 'd'.

36
Suy luận kiểu của biến, từ khóa var
Trong C# có một cách khác để khai báo và khởi tạo biến khác biệt với các
ngôn ngữ kiểu C (nhưng lại nhìn giống JavaScript!): tự suy luận kiểu với từ
khóa var.
var i = 0;
Trong cách khai báo (và khởi tạo) này, tên kiểu được thay bằng từ khóa var.
C# compiler khi gặp dòng lệnh này sẽ tự “suy đoán” ra kiểu của biến dựa
vào giá trị gán cho nó. Nghĩa là dòng lệnh trên được C# tự động hiểu thành:
int i = 0;
Kết quả biên dịch của cả hai lệnh trên là như nhau.
Khai báo và khởi tạo biến với từ khóa var hiện được ưa thích hơn so với kiểu khai báo “truyền
thống”. Trong một số tình huống (sẽ gặp sau) bạn thậm chí không thể sử dụng được kiểu khai
báo biến thông thường mà bắt buộc phải dùng var.
Khi khai báo biến với từ khóa var bạn phải tuân thủ quy tắc: Biến phải được
khởi tạo lúc khai báo. Nếu không, compiler sẽ không có căn cứ gì để suy
đoán cả.

Phạm vi của biến trong C#


Phạm vi (scope), còn gọi là phạm vi tác dụng, của biến là vùng code mà
các lệnh trong đó có thể truy xuất biến. Phạm vi được xác định theo quy tắc
sau:
Biến (trong thân phương thức) có thể truy xuất từ vị trí khai báo đến khi
gặp dấu } báo hiệu kết thúc của nhóm lệnh. Một nhóm lệnh đặt trong cặp
dấu { } như vậy gọi là một khối code (code block). Nói cách khác, biến có
phạm vi tác dụng là khối code mà nó được khai báo. Ra khỏi khối code này,
biến không sử dụng được nữa.
Các khối code có thể nằm lồng nhau. Biến khai báo ở khối code lớn (nằm
ngoài) sẽ có phạm là cả khối code lớn, tức là bao trùm cả các khối code con
bên trong. Biến khai báo ở khối code con (bên trong) thì có phạm vi là khối
code đó thôi. Khối code bên ngoài không thuộc phạm vi của biến đó.
Hãy xem ví dụ sau đây để hiểu rõ hơn về phạm vi của biến:
static void Main()
{// bắt đầu khối code thân của Main()
var i = 10;
Console.WriteLine(i);
{// bắt đầu một khối code tự do
var j = 100; // j có phạm vi từ đây đến hết khối code này
Console.WriteLine(i); // phạm vi của i bao trùm khối code này
Console.WriteLine(j); // vẫn trong phạm vi của j
}// kết thúc khối code tự do
Console.WriteLine(i); // i vẫn còn tác dụng

37
Console.WriteLine(j); // đã ra ngoài phạm vi của j (không dùng được j nữa)
=> báo lỗi ở đây
}// kết thúc khối code thân của Main()
Trong phạm vi của một biến, bạn không thể khai báo một biến khác trùng
tên. Với ví dụ trên, trong phạm vi của biến i (là thân của Main) bạn không
thể khai báo một biến i khác. Trong khối code tự do bạn không thể khai báo
một biến j khác.
Biến được khai báo trong cấu trúc điều khiển như for, while, foreach (bạn sẽ học sau) chỉ có
tác dụng trong khối code thân của cấu trúc đó. Trong phần lập trình hướng đối tượng bạn sẽ
còn gặp biến thành viên (member variable) – loại biến có phạm vi tác dụng là toàn bộ class, bất
kể vị trí khai báo.

Hằng trong C#
Hằng có thể xem tương tự như biến về khía cạnh lưu trữ dữ liệu. Khác biệt
duy nhất là giá trị của hằng không thể thay đổi.
Hằng được khai báo và khởi tạo với cú pháp như sau:
const int a = 1000; // giá trị của a sẽ không thể thay đổi được
sau khai báo này
Việc sử dụng hằng hoàn toàn tương tự như biến. Vai trò của hằng trong lập
trình chắc chắn bạn đã biết rõ. Do đó chúng ta sẽ không trình bày cụ thể
nữa.
Tuy nhiên có một số sự khác biệt sau giữa hằng và biến:
 Hằng bắt buộc phải được khởi tạo ngay lúc khai báo. Sau khi gán giá
trị (lúc khai báo), giá trị này sẽ không thể thay đổi.
 Giá trị của hằng phải tính toán được ở giai đoạn compile time. Do vậy,
bạn không thể khởi tạo một hằng nhưng sử dụng giá trị lấy từ một
biến.
Đến phần lập trình hướng đối tượng bạn sẽ còn gặp một đặc điểm nữa của hằng: hằng thành
viên được mặc định xem là thành viên static của class. Nghĩa là có thể truy xuất qua tên class,
thay vì truy xuất qua object.

38
Các kiểu dữ liệu cơ sở của C#
Kiểu dữ liệu (data type, hay đơn giản là type) trong C# (cũng như các ngôn
ngữ khác) là một đặc tính của dữ liệu nhằm thông báo cho C# compiler biết
về ý định sử dụng dữ liệu của lập trình viên. Một trong những việc đầu tiên
cần sử dụng đến kiểu dữ liệu là khai báo biến và hằng mà chúng ta đã biết.
C# nghiêm ngặt hơn nhiều so với các ngôn ngữ khác về vấn đề kiểu dữ liệu.
Ngoài ra có nhiều điều khác biệt về kiểu dữ liệu của C# mà bạn không thể
không biết.
Bài học này sẽ hướng dẫn bạn sử dụng các kiểu dữ liệu cơ sở của C#.

Kiểu dữ liệu của C# và kiểu dữ liệu của .NET


Trong C# bạn có thể sử dụng đến hàng chục ngàn kiểu dữ liệu khác nhau!
Thật vậy. Đó là những kiểu dữ liệu được định nghĩa trong hàng loạt thư viện
của .NET Framework. Tuy vậy, nếu nói một cách nghiêm ngặt thì C# lại
không hề định nghĩa kiểu dữ liệu nào! Đây là một điều rất lạ, rất khác biệt
của C#.
Vấn đề là, C# không tồn tại độc lập. Nó là một ngôn ngữ gắn liền với .NET.
.NET mới là người cung cấp hàng chục nghìn kiểu dữ liệu cho C#. .NET
không chỉ cung cấp những kiểu dữ liệu “đỉnh cao” mà nó cung cấp cả những
kiểu dữ liệu cơ bản nhất mà lẽ ra ngôn ngữ thường tự định nghĩa như số
nguyên, số thực, logic,....
Để đơn giản hóa code, C# định nghĩa các biệt danh (alias) riêng cho một
số kiểu cơ bản của .NET bằng từ khóa. Ví dụ, int (C#) là biệt danh
của System.Int32 (.NET), string (C#) là biệt danh của System.String
(.NET). Các biệt danh này làm cho C# nhìn rất giống C/C++ hay Java nhưng
bản chất lại khác nhau.
Ngoài việc tạo biệt danh, C# đơn giản hóa cú pháp cho việc sử dụng chúng.
Đây là lý do khiến nhiều bạn khi làm việc với C# thắc mắc sự khác biệt giữa các tên kiểu, một
số toàn viết thường (double, bool, string, char) với một số viết hoa (Double, Boolean, String,
Char). Bản chất chúng nó là một nhưng cách sử dụng khác nhau một chút. C# khuyến khích sử
dụng các biệt danh (nếu có) để code nhìn bớt phức tạp.
Dưới đây là danh sách các kiểu dữ liệu cơ bản (alias) của C# và kiểu tương
ứng của .NET. Chúng ta sẽ đi sâu vào từng nhóm sau.

39
Các kiểu số nguyên của C# và .NET

Các kiểu số thực của C# và .NET

Kiểu decimal của C# và .NET

Kiểu logic bool của C# và .NET

Kiểu ký tự của C# và .NET

Kiểu object và string của C# và .NET

40
Các bảng trên có mục đích giúp bạn có cảm nhận chung về tương quan giữa
kiểu dữ liệu của C# và kiểu tương ứng của .NET. Bạn không cần ghi nhớ
hay học thuộc chúng. Chúng ta sẽ xem xét chi tiết từng kiểu dữ liệu sau.

Đặc điểm của các kiểu dữ liệu cơ bản trong C#


Dưới đây chúng ta sẽ giới thiệu qua một số đặc điểm của các kiểu dữ liệu
cơ bản của C#. Mục này hướng tới các bạn đã có nền tảng ở một ngôn ngữ
lập trình khác để giúp bạn nhanh chóng nhìn thấy sự khác biệt của C#. Các
bạn có xuất phát điểm là C/C++ hay Java có thể dễ dàng và nhanh chóng
tiếp cận các kiểu cơ sở này.
Các kiểu số nguyên

Các kiểu số nguyên của C# và .NET


Như bạn đã thấy trong bảng trên, C# (và .NET) cung cấp 8 kiểu số nguyên,
phân biệt ở số byte để biểu diễn và vùng giá trị. Tên của các kiểu này hoàn
toàn giống như trong Java hay C++. Cách sử dụng cũng hoàn toàn tương
tự. Tuy nhiên có những điểm khác biệt cần lưu ý.
int và byte
Kiểu int của C# luôn luôn chiếm 4 byte (32 bit). Trong C++, số bit của
kiểu int thay đổi phụ thuộc vào platform (ví dụ, trên Windows là 32 bit).
Kiểu byte là 8 bit, có dải giá trị từ 0 đến 255, và không thể chuyển đổi
qua lại với kiểu char như trong C. Kiểu byte luôn luôn không dấu (khác với
C). Nếu muốn sử dụng số nguyên 8 bit có dấu, bạn phải dùng kiểu sbyte.
Cơ số
Tất cả các kiểu số nguyên đều có thể nhận giá trị biểu diễn ở nhiều cơ
số (base) khác nhau: cơ số 10 (decimal), 16 (hex), 8 (octal), 2 (binary).
Giá trị biểu diễn ở các cơ số khác 10 phải sử dụng thêm tiếp tố (prefix)
tương ứng.
41
long x = 0x12ab; // số hexa, prefix là 0x hoặc 0X
byte y = 0b1100; // số nhị phân, prefix là 0b hoặc 0B
int z = 01234; // số hệ cơ số 8, prefix là 0
Digit separator
C# 7 cho phép sử dụng dấu _ giữa các chữ số để tách các chữ số cho dễ
đọc hơn với các giá trị lớn. Dấu _ gọi là digit separator.
long l1 = 0x123_456_789_abc_def; // dấu _ giúp tách các chữ số cho dễ đọc
long l2 = 0x123456789abcdef; // cách viết thông thường
int bin = 0b1111_1110_1101; // viết tách các bit thế này dễ đọc hơn
Integer literal
Khi dùng từ khóa var để khai báo biến thuộc kiểu số nguyên, C# mặc định
sẽ hiểu nó là kiểu int. Nếu muốn chỉ định giá trị nguyên thuộc một kiểu nào
đó khác, bạn phải sử dụng một cách viết riêng gọi là integer literal.
Integer literal là các ký tự viết vào cuối giá trị số (postfix) để báo hiệu kiểu
dữ liệu, bao gồm: U (hoặc u) báo hiệu số nguyên không dấu; L (hoặc l) báo
hiệu giá trị thuộc kiểu long; UL (hoặc ul) cho kiểu ulong. Có thể sử dụng
các ký tự này khi viết ở hệ cơ số khác 10. Ví dụ:
var i0 = 123; // C# mặc định coi đây là kiểu int
var i1 = 123u; // giá trị này thuộc kiểu uint
var i2 = 123l; // giá trị này thuộc kiểu long
var i3 = 123ul; // giá trị này thuộc kiểu ulong
var i4 = 0x123L; // giá trị kiểu long ở hệ hexa
Từ giờ về sau bạn sẽ còn gặp nhiều literal nữa. Literal là cách viết giá trị của từng kiểu dữ liệu.
Nếu bạn khai báo số nguyên có giá trị đủ lớn để thoát khỏi miền giá trị của
int, C# sẽ tự chọn kiểu phù hợp có miền giá trị đủ bao trùm. Ví dụ:
var ui = 3000000000; // đây sẽ là kiểu uint
var l = 5000000000; // đây sẽ là kiểu long

Các kiểu số thực


C# (và .NET) chỉ cung cấp 2 loại số thực: float (System.Single) và double
(System.Double). Các thông tin chi tiết bạn đã xem ở phần trên. float có
miền giá trị nhỏ hơn và độ chính xác thấp hơn so với double.

Các kiểu số thực của C# và .NET


Khi dùng từ khóa var với giá trị số thực, C# sẽ mặc định hiểu nó thuộc về
kiểu double. Để chỉ định một giá trị thực thuộc kiểu float, bạn cần dùng
postfix F (hoặc f) sau giá trị. F (hoặc f) được gọi là float literal.
42
var r1 = 1.234; // r1 thuộc kiểu double
var r2 = 1.234f; // r2 thuộc kiểu float
decimal (System.Decimal) là một dạng số thực đặc biệt chuyên dùng trong
tính toán tài chính.

Kiểu decimal của C# và .NET


Literal cho decimal là M (hoặc m).
var d = 12.30M; // biến này thuộc kiểu decimal
Các kiểu số thực cũng hỗ trợ cách viết dạng khoa học (và có thể kết hợp với
float decimal):
var d1 = 1.5E-20; // cách viết khoa học bình thường là 1.5*10-20, kiểu
double
var f1 = 1.5E-10F; // số 1.5*10-10, kiểu float
var m1 = 1.5E-20M; // 1.5*10-20, kiểu decimal

Kiểu Boolean
Boolean (.NET) hay bool (C#) chỉ nhận đúng hai giá trị: true và false.
Đây cũng được gọi là literal của kiểu bool.

Kiểu logic bool của C# và .NET


Trong C# không thể tự do chuyển đổi giữa bool và số nguyên như trong
C/C++. Tức là bạn không thể sử dụng 0 thay cho false, giá trị khác 0 thay
cho true như trong C. Biến khai báo thuộc kiểu bool chỉ có thể gán giá
trị true hoặc false.
Kiểu ký tự
Kiểu char (C#) hay System.Char (.NET) dùng để biểu diễn ký tự đơn, mặc
định là các ký tự Unicode 16 bit.

Kiểu ký tự của C# và .NET


Character literal
Literal của kiểu char là cặp dấu nháy đơn. Ví dụ ‘A’, ‘a’, ‘1’, ‘@’.
var c = 'A';

43
Đừng nhầm lẫn với cặp dấu nháy kép – là literal của chuỗi ký tự. Nếu sử dụng lẫn lộn cặp nháy
đơn và nháy kép, compiler sẽ báo lỗi hoặc hiểu sai ý định của bạn.
Bạn cũng có thể sử dụng mã Unicode của ký tự như sau: '\u0041',
'\x0041'.
var c1 = '\u0041';
var c2 = '\x0041';
Một cách khác nữa để biểu diễn ký tự là dùng mã decimal cùng với ép kiểu:
(char)65.
var c3 = (char)65;
Escape sequence
Tương tự như C, C# cũng định nghĩa một số ký tự đặc biệt gọi là escape
sequence:
 \’: dấu nháy đơn
 \”: dấu nháy kép
 \\: dấu backslash (dùng trong đường dẫn)
 \0: Null
 \a: cảnh báo (alert)
 \b: xóa lùi (backspace)
 \n: dòng mới
 \r: quay về đầu dòng
 \t: dấu tab ngang
 \v: dấu tab dọc
Kiểu chuỗi ký tự
Chuỗi (xâu) ký tự (string hoặc System.String), khác biệt với các kiểu dữ
liệu bên trên, là một kiểu dữ liệu tham chiếu (reference type). Trong khi các
kiểu dữ liệu ở bên trên thuộc loại giá trị (value type). Sự khác biệt là gì bạn
xem ở phần cuối bài.
Literal của string là cặp dấu ngoặc kép:
var str1 = "Hello world";
var emptyStr = ""; // đây là một chuỗi ký tự hợp lệ, gọi là xâu rỗng
Trong chuỗi ký tự có thể sử dụng ký tự escape sequence (bạn đã biết ở
trên):
// chuỗi này chứa hai escape sequence \r và \n.
// nếu in ra console, con trỏ văn bản sẽ chuyển xuống đầu dòng tiếp theo
string message = "Press any key to continue\r\n";
Console.WriteLine(message);

44
// nếu in chuỗi này ra sẽ thu được x1 = 123 x2 = 456, tức là có 1 dấu
tab ở giữa
string solutions = "x1 = 123\tx2 = 456";
Console.WriteLine(solutions);
Trong chuỗi không được có mặt ký tự \ (backslash). Lý do là ký tự này được
sử dụng trong escape sequence. Ví dụ, dưới đây là một chuỗi sai (bị báo lỗi
cú pháp):
string path = "C:\Programs\Visual Studio"; // chuỗi này bị lỗi vì chứa ký
tự \.
Nếu muốn viết ký tự \ vào chuỗi, bạn phải viết nó hai lần:
string path = "C:\\Program\\Visual Studio"; // chuỗi này OK
hoặc thêm ký tự @ vào đầu chuỗi. Ký tự @ sẽ tắt chế độ diễn giải escape
sequence.
string path = @"C:\Program\Visual Studio"; // chuỗi này OK vì ký tự @ sẽ
tắt chế độ nhận diện escape sequence
Trong chuỗi ký tự cũng có thể chứa biến và biểu thức. Các giá trị này được
tính toán trước khi chèn vào đúng vị trí của nó trong chuỗi. Tính năng này
có tên gọi là string interpolation. Interpolated string được bắt đầu bằng
ký tự $.
int x1 = 123, x2 = 456;
string solution = $"x1 = {x1} x2 = {x2} x3 = {x1 + x2}"; // đây là
một interpolated string
Console.WriteLine(solution);
// nếu in ra console sẽ thu được x1 = 123 x2 = 456 x3 = 579
String interpolation là tính năng rất tiện lợi để tạo ra các xâu động từ biến
và biểu thức.
Chuỗi là một loại dữ liệu đặc biệt và được sử dụng rất rộng rãi. Nội dung trong bài này chưa đủ
để làm việc với chuỗi. Chúng ta sẽ có bài học riêng về cách sử dụng chuỗi trong C# ở phía sau.
Kiểu object
Object (System.Object) là kiểu dữ liệu đặc biệt trong C# và .NET. Nó là
kiểu dữ liệu “tổ tiên” của mọi kiểu dữ liệu khác (root type). Đây cũng là một
trong hai kiểu reference.
Trong các ngôn ngữ lập trình hướng đối tượng, các kiểu dữ liệu thường được tổ chức theo dạng
phân cấp (hierarchy) như một cái cây. Trong đó kiểu dữ liệu cấp cao nhất, ở gốc của cây gọi
là root type. Tất cả các kiểu còn lại đều là các nhánh xuất phát từ gốc. Nếu bạn hiểu khái niệm
kế thừa thì object chính là tổ tiên của tất cả các loại kiểu. Nói cách khác, mọi kiểu dữ liệu khác
đều là con/cháu/chắt/chút/chít của object.
Chúng ta sẽ quay lại kiểu object khi học về class và kế thừa. Tạm thời bạn
chỉ cần biết vậy là được.
Tuy nhiên có một phương thức quan trọng của object bạn cần biết:
ToString(). Phương thức này có mặt trong mọi kiểu dữ liệu mà bạn đã biết
45
(do cơ chế kế thừa từ object). Nó giúp chuyển đổi giá trị của kiểu tương
ứng về chuỗi ký tự. Bạn sẽ thường xuyên cần đến nó khi viết giá trị của một
biến ra console.
var a = 123.456; // a thuộc kiểu double
var strA = a.ToString(); // strA giờ là một chuỗi, có giá trị "123.456"

Phân loại kiểu dữ liệu trong C#


Stack và Heap
Để hiểu được cách thức phân loại kiểu dữ liệu, bạn cần nhớ lại một số vấn
đề liên quan đến stack và heap.
Stack và heap đều là các vùng bộ nhớ trong RAM của máy tính nhưng được
tổ chức và sử dụng cho các mục đích khác nhau.
Stack là vùng nhớ hoạt động theo mô hình LIFO (vào sau, ra trước) dùng
để lưu trữ các biến tạm thời được tạo ra bởi các phương thức. Khi một biến
được khai báo, nó được tự động đẩy vào stack (push).
Nếu phương thức kết thúc, tất cả các biến mà phương thức đó đã đẩy vào
stack đều bị giải phóng (pop) và mất đi. Khi một biến bị đẩy khỏi stack,
vùng nhớ nó đã chiếm có thể được sử dụng lại cho các biến khác.
Stack được CPU quản lý và tối ưu hóa cho nhiệm vụ lưu trữ biến cục bộ.
Người lập trình không cần phải can thiệp vào vùng nhớ này. Tốc độ đọc ghi
dữ liệu với stack rất cao, tuy nhiên kích thước của stack lại bị giới hạn.
Heap là một vùng nhớ khác cho phép chương trình tự do lưu trữ giá trị. Giá
trị tạo và lưu trên heap không bị giới hạn về kích thước mà chỉ phụ thuộc
và kích thước RAM. Tuy nhiên tốc độ đọc ghi dữ liệu trên heap chậm hơn so
với stack.
Để sử dụng heap, chương trình phải tự mình xin cấp phát và giải phóng bộ
nhớ chiếm dụng trên heap. Nếu không sử dụng hợp lý vùng nhớ này có thể
dẫn đến rò bộ nhớ (memory leak), vốn rất phổ biến khi lập trình C/C++.
.NET Framework hỗ trợ rất tốt việc cấp phát và quản lý bộ nhớ của chương
trình qua GC (Garbage Collector) nên người lập trình .NET không cần để ý
nhiều đến việc xin cấp phát và giải phóng bộ nhớ heap.
Kiểu value và kiểu reference
Khi nói về hệ thống kiểu dữ liệu trong C# cần phân biệt hai nhóm: các kiểu
giá trị (value types) và các kiểu tham chiếu (reference types). Cách phân
loại này liên quan đến việc cấp và quản lý bộ nhớ cho biến.
Theo cách phân loại này, object và string thuộc nhóm reference, tất cả các
kiểu còn lại thuộc nhóm value.

46
Sự phân biệt này rất quan trọng cho việc khởi tạo và gán giá trị của biến.
Sự khác biệt giữa hai nhóm này thể hiện ở nhiều vấn đề. Tuy nhiên, tạm
thời hãy chấp nhận sự khác biệt cơ bản: kiểu giá trị lưu trữ dữ liệu trực tiếp
trong biến; kiểu tham chiếu lưu trữ địa chỉ (tham chiếu) của dữ liệu (nằm ở
nơi khác). Cụ thể hơn, dữ liệu của kiểu giá trị lưu trong stack, trong khi dữ
liệu của kiểu tham chiếu (thường gọi là object) lưu tron heap. Bản thân biến
thuộc kiểu tham chiếu chỉ chứa địa chỉ tới object nằm trên heap.

Quan hệ giữa heap và stack


Việc gán giá trị của biến kiểu value cho một biến khác sẽ tạo ra bản sao
trong stack. Nghĩa là bạn sẽ có hai biến khác nhau nhưng có giá trị bằng
nhau. Thay đổi giá trị của biến này sẽ không ảnh hưởng đến giá trị của biến
kia.
Việc gán giá trị của biến kiểu reference cho một biến khác sẽ chỉ gán địa chỉ
của ô nhớ. Nói cách khác, khi này hai biến sẽ trỏ vào cùng một object. Thay
đổi giá trị của biến này thì biến khác đồng thời nhận sự thay đổi đó, vì thực
chất chúng là cùng một object, chỉ là mang hai tên khác nhau.
Sự khác nhau này rất quan trọng khi bạn bắt đầu sử dụng biến hoặc truyền
biến làm tham số cho phương thức. Không hiểu sự khác biệt này bạn có thể
mắc lỗi nghiêm trọng khi gán và thay đổi giá trị của biến.
string mặc dù là một kiểu reference nhưng hoạt động hơi khác biệt so với các kiểu reference
khác. Nó thuộc một nhóm kiểu có tên gọi là immutable. Mọi thao tác chỉnh sửa trên string đều
tạo ra một object khác, thay vì thay đổi giá trị của chính object đó. Bạn sẽ học kỹ hơn về string
ở một bài khác.

47
Giá trị null, kiểu nullable
Các biến kiểu reference có một giá trị đặc biệt, biểu diễn bằng từ khóa null.
Giá trị này báo hiệu rằng biến reference chưa trỏ vào một object nào. Mọi
thao tác xử lý trên biến reference (trừ phép gán và khởi tạo) đều báo lỗi ở
giai đoạn runtime.
Tất cả biến thuộc kiểu tham chiếu khi khai báo đều nhận giá trị mặc
định là null. Giá trị null của một biến kiểu tham chiếu thể hiện rằng biến
đó chưa chứa địa chỉ của vùng nhớ nơi lưu giá trị. Chỉ khi biến đó được khởi
tạo nó mới tham chiếu sang vùng lưu giá trị.
Mọi thao tác trên các biến tham chiếu (truy xuất thành viên) có giá trị null
đều gây lỗi NullReferenceException (“Object reference not set to an
instance of an object.”).
C# quy định tất cả các biến kiểu tham chiếu bắt buộc phải được khởi tạo
trước khi sử dụng (truy xuất các thành viên của nó).
Vì một biến kiểu giá trị không lưu địa chỉ của vùng nhớ heap mà lưu trực
tiếp giá trị trong stack, biến loại này không thể nhận giá trị null.
Tuy nhiên, từ C# 2.0, bạn có thể để cho kiểu value nhận giá trị null bằng
cách thêm modifier ? vào sau tên kiểu value:
int? count = null;
Modifier ? biến kiểu value thông thường thành một loại kiểu dữ liệu đặc biệt
có tên là nullable type.
Sở dĩ C# phải đưa nullable type vào là vì khi làm việc với một số hệ thống ngoài, ví dụ, khi
lập trình cơ sở dữ liệu với ado.net, sql server cho phép một trường nhận giá trị null. Khi đó C#
bắt buộc phải có cơ chế tương ứng để thể hiện rằng biến tương ứng với giá trị null của sql phải
“không có giá trị”. Đối với kiểu reference, điều này hoàn toàn tương đương với việc biến đó có
giá trị null. Nhưng đối với kiểu số chẳng hạn, bình thường bạn không thể biểu diễn ý tưởng “số
không có giá trị”. Kiểu nullable được đưa vào là để giải quyết những vấn đề đó.
Bạn sẽ học chi tiết hơn về kiểu nullable trong bài học về các kiểu dữ liệu đặc biệt của C#.
Kiểu class – struct – enum – interface – delegate
Ở một khía cạnh khác, trong .NET (và C#), kiểu dữ liệu cũng được phân loại
theo vào năm nhóm: class, struct, enum, interface, delegate. Đây là cách
phân loại theo chức năng và mục đích sử dụng.
Có thể bạn chưa biết, Console mà bạn đã gặp qua chương trình Hello world
thuộc nhóm class, trong khi int thuộc nhóm struct. Enum, Interface và
Delegate bạn sẽ gặp sau.

48
Tất cả các kiểu dữ liệu cơ bản ở trên đã gặp (trừ string và object) đều thuộc
nhóm struct. Object và string thuộc nhóm class.
Nếu ghép nối với cách phân loại thứ nhất, tất cả các class, interface và
delegate thuộc nhóm reference, struct và enum thuộc nhóm value.

49
Các toán tử (operator) cơ bản trong C#
Toán tử (operator, còn gọi là phép toán) là những thành phần cơ bản trong
C# cũng như bất kỳ ngôn ngữ lập trình nào. Toán tử và toán hạng (operand)
tạo ra các biểu thức (expression). Mỗi kiểu dữ liệu của C# có những toán tử
riêng. Một phần các toán tử cơ bản của C# tương tự như trong C/C++. Tuy
nhiên, C# cũng có rất nhiều toán tử đặc biệt của riêng mình. Qua mỗi phiên
bản C# lại đưa thêm vào những toán tử mới.
Bài học này sẽ giới thiệu những toán tử cơ bản của C#. Những toán tử đặc
biệt sẽ được xem xét chi tiết ở bài học phù hợp.

Toán tử trong C#
C# có khá nhiều toán tử (phép toán). Qua mỗi phiên bản C# lại đưa thêm
vào những phép toán mới sử dụng với kiểu dữ liệu mới.
Phần lớn các toán tử cơ bản đều tương tự như các ngôn ngữ kiểu C, bao
gồm các phép toán số học, phép toán logic, phép toán tăng giảm, phép toán
nhị phân, phép toán index (truy xuất mảng), hay phép toán điều kiện.
Tuy nhiên, C# có nhiều phép toán hoàn toàn khác với C/C++, ví dụ phép
toán kiểm tra giá trị null (null coalescing), kiểm tra kiểu, định danh, các
phép toán cho delegate,....
Thậm chí cho cùng một công việc nhưng phép toán của C# không giống
như C/C++. Ví dụ phép toán truy xuất thành viên (object và struct).
Dưới đây là danh sách tất cả các phép toán hiện có trong C#.

NHÓM TOÁN TỬ
Phép toán số học + – * / %
Phép toán logic và nhị phân & | ^ ~ && || !
Phép toán ghép xâu +
Phép toán tăng giảm ++ – –
Phép toán dịch bit << >>
Phép toán so sánh == != < > <= >=
= += -= *= /= %= &= |= ^= <<=
Phép gán
>>=
Phép toán truy xuất thành viên
.
(object và struct)

50
Phép toán indexer (cho mảng) []
Ép kiểu (type casting) ()
Phép toán điều kiện ?:
Phép toán cho delegate (thêm/bớt) + –
Khởi tạo object new
Lấy thông tin về kiểu dữ liệu sizeof is typeof as
Kiểm soát lỗi tràn bộ đệm checked unchecked
Phép toán liên kết null ??
Phép toán kiểm tra điều kiện null ?. ?[]
Lấy tên của phần tử nameof()

Chắc rằng bạn sẽ thấy danh sách này vừa quen vừa lạ. Chúng ta sẽ không
đi sâu vào tất cả các phép toán trên trong bài học này mà sẽ chỉ xem xét
phép toán sử dụng được với các kiểu dữ liệu cơ sở của C#.

Các phép toán số học trên các kiểu số


Các phép toán số học trong C# hoàn toàn tương tự như trong C/C++. Dưới
đây là danh sách các phép toán số học của C#:

Phép toán Ví dụ
Số dương +x
Số âm -x
Tăng sau (post increment) x++
Giảm sau (post decrement) x--
Tăng trước (pre-increment) ++x
Giảm trước (pre-decrement) --x
Nhân x * y
Chia x / y
Chia lấy dư x % y
Cộng x + y
Trừ x - y

51
Nếu bạn xuất phát từ một ngôn ngữ không thuộc họ C, các phép toán khó
hiểu nhất có lẽ là các phép toán tăng giảm (increment, decrement). Các
phép toán số học còn lại đều tương tự trong các ngôn ngữ lập trình. Chúng
ta sẽ nói kỹ hơn về các phép toán tăng giảm một chút.
Các phép toán tăng giảm khi sử dụng trong biểu thức sẽ cộng hoặc trừ đi
một (1) đơn vị của biến khi tính toán biểu thức. Tuy nhiên, có sự khác biệt
giữa tăng/giảm trước và tăng/giảm sau.
Đối với các phép tăng/giảm sau, giá trị của giá trị của toán hạng sẽ chỉ bị
thay đổi sau khi thực hiện biểu thức.
Ví dụ, nếu x = 5 và thực hiện y = x++, y sẽ nhận giá trị 5, còn x sẽ bằng
6. Lý do là giá trị của x sẽ thay đổi (cộng thêm 1) SAU khi thực hiện biểu
thức (ở đây là biểu thức gán). Do đó y sẽ nhận giá trị của x (=5) trước, sau
đó x tự cộng thêm 1 để bằng 6.
Dưới đây là kết quả thực hiện trên C# Interactive.
> var x = 5;
> var y = x++;
> x
6
> y
5
>

Đối với phép tăng giảm trước, giá trị của của toán hạng sẽ tăng 1 đơn
vị trước khi thực hiện biểu thức. Hãy xem ví dụ sau:
> var x = 5;
> var y = ++x;
> x
6
> y
6
>
tức là x sẽ tăng 1 đơn vị trước, sau đó mới thực hiện biểu thức gán. Do đó
cả x và y đều có giá trị 6.

Phép toán trên bit


Dưới đây là các phép toán thực hiện trên bit – biểu diễn nhị phân của số
nguyên.

Tên Ví dụ
Phép bù (Bitwise negation) ~x
Phép và (Bitwise AND) x & y
Phép hoặc (Bitwise OR) x | y

52
Phép Bitwise XOR x ^ y
Dịch trái (Shift left) x << y
Dịch phải (Shift right) x >> y

Các toán tử nhị phân này giống như trong C.


Nhiều bạn thường nhầm lẫn các phép toán này với các phép toán logic (xem
phần dưới).
Lưu ý rằng, các phép toán này chỉ áp dụng với số nguyên, chính xác hơn là
ở dạng biểu diễn nhị phân của số nguyên. Vì vậy nó mới có tên là bitwise
operators. Dưới đây là một số ví dụ đơn giản để minh họa:
AND nhị phân:
0101 (số thập phân 5)
AND 0011 (số thập phân 3)
= 0001 (số thập phân 1)

Bù/Đảo bit:
~ 0111 (số thập phân 7)
= 1000 (số thập phân 8)

OR nhị phân:
0101 (số thập phân 5)
OR 0011 (số thập phân 3)
= 0111 (số thập phân 7)

XOR nhị phân:


0101 (số thập phân 5)
XOR 0011 (số thập phân 3)
= 0110 (số thập phân 6)
Một số ví dụ về phép toán dịch bit (trên số nguyên 8 bit):
00010111 << 1 (số thập phân +23) Dịch chuyển trái 1 bit
= 00101110 (số thập phân +46), tương đương với nhân 2

00010111 << 2 (số thập phân +23) Dịch sang trái 2 lần.
= 01011100 (số thập phân +92), tương đương nhân với 4 (2^2)
Bạn cũng có thể thử nghiệm các phép toán trên với C# Interactive:

53
> byte i = 23;
> Convert.ToString(i, 2)
"10111"
> var j = i << 1;
> j
46
> Convert.ToString(j, 2)
"101110"
>

Phương thức Convert.ToString(value, base) sẽ chuyển giá trị value sang hệ


cơ số base.

Các phép toán logic trên kiểu bool


Tên Ví dụ
Logical negation (NOT) !x
Conditional AND x && y
Conditional OR x || y

Các toán tử logic này hoàn toàn giống như trong C.


Lưu ý rằng, các phép toán này hoạt động trên các giá trị logic (kiểu bool)
và ghép chúng lại thành các biểu thức logic phức tạp hơn. Đừng nhầm lẫn
với các toán tử nhị phân ở phần trên (chỉ hoạt động trên số nguyên).

a b !a a && b a || b
true true false true true
true false false false true
false true true false true
false false true false false

Bạn cũng có thể thử nghiệm các phép toán này trên C# Interactive như
sau:
> true && true
true
> true || false
true
> var a = false; var b = false;
> a && b
false
> a || b
false

54
>

Các phép toán logic này rất thường được sử dụng để kết hợp các biểu thức
so sánh.

Các phép toán so sánh


Các phép toán so sánh (relational operator), còn gọi là các phép toán quan
hệ, thực hiện được trên nhiều kiểu dữ liệu nhưng kết quả trả về luôn là kiểu
bool. Các phép so sánh có thể thực hiện trên các kiểu số, kiểu ký tự và
chuỗi.

Tên Ví dụ
Nhỏ hơn x < y
Lớn hơn x > y
Nhỏ hơn hoặc bằng x <= y
Lớn hơn hoặc bằng x >= y
So sánh bằng x == y
Không bằng x != y

Việc so sánh này phụ thuộc một phần vào kiểu dữ liệu của toán hạng.
 Nếu các toán hạng đều là số, ý nghĩa của các phép toán so sánh không
khác gì trong toán học.
 Nếu là ký tự, mã của ký tự đó (vốn là kiểu số nguyên) sẽ được so
sánh với nhau.
 Nếu là chuỗi, từng ký tự trong chuỗi sẽ được so sánh với nhau. Chúng
ta sẽ quay lại so sánh chuỗi ở một bài khác.
Dưới đây là một số ví dụ về sử dụng các phép toán so sánh trong C#
Interactive.
> int a = 10, b = 20;
> a < b
true
> a > b
false
> a != b
true
> a == b
false
>

55
Trong số các phép toán này, nếu bạn xuất phát từ ngôn ngữ khác C thì nên
lưu ý phép so sánh bằng (==) và khác (!=). Ví dụ Pascal sử dụng dấu bằng
= cho phép so sánh, và := cho phép gán. C# sử dụng = cho phép gán, ==
cho phép so sánh bằng.

Phép toán điều kiện


Phép toán điều kiện (conditional operator, ternary operator) là một đặc sản
của các ngôn ngữ tương tự C. Nếu bạn biết C, bạn chắc chắn đã biết phép
toán này. Nếu chưa biết, hãy cùng xem ví dụ đơn giản sau:
> int x = 10, y = 20;
> int z = (x > y) ? x : y;
> z
20
>

Đây là các lệnh để thực hiện một nhiệm vụ đơn giản: chọn giá trị lớn hơn
trong hai số x y và gán cho một biến z khác.
Có thể diễn giải biểu thức int z = (x > y) ? x : y; như sau: so sánh x và y;
nếu x lớn hơn y thì biểu thức có giá trị bằng x; ngược lại, biểu thức sẽ có
giá trị bằng y.
Trong cú pháp của phép toán này, dấu chấm hỏi là bắt buộc và đứng sau
biểu thức logic; dấu hai chấm là bắt buộc để phân tách giá trị trả về trong
hai trường hợp của biểu thức logic. Nếu biểu thức logic nhận giá trị true,
biểu thức sẽ nhận giá trị trước dấu hai chấm; nếu biểu thức logic có giá trị
false, biểu thức sẽ nhận giá trị đứng sau dấu hai chấm. Vì đây là một biểu
thức, kết quả của nó có thể gán cho một biến để sau tái sử dụng.
Phép toán điều kiện hoạt động gần giống như cấu trúc điều kiện if-else.
Hãy cùng xem vài ví dụ khác:
> int a = 10, b = 20;
. string message = (a > b) ? "a lớn hơn b" : "a nhỏ hơn b";
> message
"a nhỏ hơn b"
>
> string str = "";
> (str == "") ? "Xâu rỗng" : "Xâu không rỗng"
"Xâu rỗng"
>

56
Các phép gán phức hợp
Các phép gán phức hợp (compound assignment) là nhóm phép toán đặc sản
của các ngôn ngữ trong họ C. Trong đó, phép toán này thực hiện một thao
tác (như cộng, trừ, nhân, chia,...) và gán ngược giá trị đã biến đổi về cho
biến.
Dưới đây là một số phép toán gán phức hợp.

Phép toán Ví dụ
+= x += 1 // tương đương x = x + 1
-= x -= 1 // tương đương x = x – 1

*= x *= 2 // tương đương x = x * 2

/= x /= 2 // tương đương x = x / 2

Bạn có thể thử các phép toán này trong C# Interactive:


> var x = 2;
> x += 1
3
> x *= 2
6
> x /= 3
2
> x -= 2
0
>

Không có khó khăn gì để hiểu các phép toán này.

Các phép toán với kiểu dữ liệu: type casting, is và as,


typeof
Type casting
Type casting (tạm dịch là ép kiểu) là việc chuyển đổi giá trị của một biến
sang một kiểu khác nhưng không làm thay đổi bản chất giá trị của nó. Ví
dụ, C# tự động chuyển đổi giá trị giữa các kiểu số, như từ số nguyên sang
số thực và ngược lại. Loại ép kiểu này được gọi là implicit casting.
Tuy nhiên, trong nhiều trường hợp C# không thể tự thực hiện được mà bạn
phải tự mình chỉ định kiểu đích. Loại ép kiểu này được gọi là explicit
casting.

57
Lưu ý, nếu bạn chuyển đổi từ chuỗi “1234” thành số 1234 hay ngược lại, dữ liệu đã bị thay
đổi về bản chất. Đây được gọi là type conversion.
Để thực hiện type casting, bạn cần dùng phép toán casting theo cách sau:
(<kiểu-đích) <giá-trị>
Biểu thức này sẽ thực hiện chuyển đổi <giá-trị> sang <kiểu-đích>. Nếu quá
trình ép kiểu không thành công, biểu thức sẽ phát ra ngoại lệ (exception).
Ví dụ sau đây sẽ ép kiểu của biến s1 (kiểu object) sang kiểu string và gán
vào biến str1:
> object s1 = "Hello world";
> string str1 = (string)s1;
> str1
"Hello world"

Type casting với phép toán as


Một cách khác để thực hiện ép kiểu là sử dụng phép toán as. Phép
toán as thực hiện ép kiểu cho giá trị. Nếu không thành công sẽ trả về giá
trị null. Phép toán này an toàn hơn so với sử dụng phép toán ép kiểu trực
tiếp ở trên do nó tránh được exception khi ép kiểu không thành công.
> object s1 = "Hello world";
> string str2 = s1 as string;
> str2
"Hello world"
> object s2 = 12345;
> string str3 = s2 as string;
> str3
null
>

Tuy nhiên, phép toán as lại chỉ có thể áp dụng được đối với các kiểu tham
chiếu (reference type). Nó không áp dụng được với kiểu giá trị (value type).
Lý do là vì trong trường hợp ép kiểu không thành công, nó trả về giá
trị null. Đây là giá trị đặc trưng riêng của kiểu tham chiếu
(và kiểu nullable).
Kiểm tra kiểu – phép toán is
Để đảm bảo không gây lỗi khi ép kiểu, bạn nên kiểm tra kiểu (type checking)
trước khi thực hiện.
C# sử dụng phép toán is để kiểm tra kiểu của một giá trị (object). Hãy
thực hiện một vài ví dụ trên C# Interactive:
58
> object s1 = "Hello world";
> s1 is string
true
> s1 is int
false

Phép toán is nhận một giá trị ở bên trái và tên kiểu ở bên phải. Nó trả về
giá trị true nếu giá trị thuộc về kiểu đó. Trong ví dụ trên, giá trị của s1 thuộc
kiểu string nên biểu thức s1 is string trả về giá trị true, còn s1 is
int trả về giá trị false.
Lấy thông tin về kiểu: phép toán typeof
Phép toán typeof trả về một object chứa thông tin về kiểu dữ liệu. Từ kết
quả này bạn có thể lấy tất cả các thông tin cần thiết về chính kiểu dữ liệu.
Ví dụ:
> Type stringType = typeof(string);
> stringType
[System.String]
> stringType.UnderlyingSystemType
[System.String]
> stringType.Assembly
[mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]
> stringType.BaseType
[System.Object]
> stringType.FullName
"System.String"

Lưu ý rằng, typeof chỉ nhận tham số là tên kiểu dữ liệu. Nếu muốn lấy thông
tin về kiểu từ biến, bạn phải dùng phương thức GetType():
// Lưu ý: nếu bạn muốn lấy thông tin về kiểu dữ liệu từ biến, bạn phải dùng phương
thức GetType()
> stringType = "Hello world".GetType();
// hai phương pháp này cho cùng một kết quả

Phép toán typeof được sử dụng rất nhiều cùng với lập trình dynamic, generic
và reflection.

59
Console trong C#: Xuất – nhập dữ liệu, lớp
System.Console
Giao diện console (console user interface - CUI), còn gọi là giao diện dòng
lệnh (command line interface - CLI), là loại giao diện đơn giản nhất, trong
đó dữ liệu xuất nhập đều là văn bản. Mặc dù không có gì hấp dẫn, ứng dụng
với giao diện console (thường gọi tắt là ứng dụng console) luôn được sử
dụng để học ngôn ngữ lập trình. Tương tự, trong khi học lập trình C# căn
bản bạn sẽ gắn bó với ứng dụng console. Sự đơn giản của loại ứng dụng
này giúp bạn tập trung vào các đặc trưng của ngôn ngữ, thay vì phân tâm
cho sự phức tạp của giao diện đồ họa (Graphical User Interface - GUI) hay
giao diện web.
Bài học này sẽ cung cấp cho bạn đầy đủ các kỹ thuật để làm việc với giao
diện console trong C#: nhập, xuất, định dạng. Nắm chắc các kỹ thuật này
sẽ giúp đơn giản hóa một phần việc học lập trình C# về sau.

Giao diện console


Vai trò của ứng dụng console
Ứng dụng console là loại ứng dụng có giao diện đơn giản nhất, giúp người
mới học lập trình tránh xa những sự phức tạp (chưa cần thiết) của các công
nghệ GUI (Graphical User Interface – giao diện đồ họa).
Ứng dụng console sử dụng nhiều cho mục đích tính toán và xử lý, thay vì
thể hiện dữ liệu. Nhiều chương trình tính toán nặng (trong khoa học) hay
được viết bằng console thay vì dùng GUI. Các chương trình server thì hoàn
toàn không cần đến GUI.
Ứng dụng console có hiệu suất cao và an toàn. Trên thực tế, rất nhiều ứng
dụng dành cho quản trị hệ thống đều sử dụng giao diện console. Chắc bạn
đã từng dùng Ping, IpConfig trên Windows. Windows Server còn có hẳn một
giao diện dòng lệnh cao cấp (powershell) với hầu hết các công cụ đều thực
hiện ở giao diện dòng lệnh. Nếu làm việc với Linux thì … thôi rồi, toàn chương
trình console chạy trên shell.
Ứng dụng console không phụ thuộc quá chặt chẽ vào shell đồ họa của hệ
thống và dễ dàng phục vụ đa nền tảng. Ví dụ, một ứng dụng console viết
trên .NET Framework (cho Windows) có thể dễ dàng chạy tiếp trên Linux
(Mono).

60
Các vấn đề của giao diện console trong C#
Vấn đề hiển nhiên nhất với ứng dụng console là việc xuất nhập dữ liệu. Mọi
thứ xuất ra console đều là văn bản. Mọi thứ đọc vào từ console cũng đều là
văn bản. Từ đây dẫn đến yêu cầu phải chuyển đổi kiểu dữ liệu khi xuất/nhập
dữ liệu.
Vấn đề thứ hai là nhận lệnh và quyết định phương thức tương ứng nào sẽ
được thực thi. Đối với các chương trình nhỏ thì đây không phải là vấn đề.
Người mới học dễ dàng nghĩ tới sử dụng các cấu trúc rẽ nhánh để chọn
phương thức phù hợp. Vậy nếu như có vài chục lệnh hoặc nhiều hơn nữa?
Vấn đề thứ ba là tham số đi kèm mỗi lệnh. Một lệnh người dùng nhập không
thể thiếu tham số. Ví dụ lệnh “delete xyz.exe” có “xyz.exe” là tham số đi
cùng lệnh. Làm thế nào để dễ dàng tiếp nhận, chuyển đổi và sử dụng tham
số đó với lệnh?
Lớp System.Console
Trong C# lớp System.Console để thực hiện các công việc với giao diện
console. Tất cả các thao tác quan trọng như xuất/nhập, định dạng dữ liệu
xuất ra đều được thực hiện qua lớp này.
Dưới đây là một số phương thức, thuộc tính thành viên của lớp Console:
 Write(), WriteLine(): in thông tin ra console
 Read(), ReadLine(), ReadKey(): đọc thông tin từ console
 Beep(): phát tiếng bíp ra loa
 BackgroundColor: đặt màu nền cho văn bản
 ForegroundColor: đặt màu văn bản
 Title: Đặt tiêu đề cho cửa sổ console
 Clear(): xóa nội dung của console
 BufferWidth/BufferHeight: đặt kích thước buffer cho console
 WindowWidth/WindowHeight: đặt kích thước của console
 WindowTop/WindowLeft: đặt vị trí của console

Xuất dữ liệu ra console trong C#: Write, WriteLine


Write và WriteLine
Để xuất dữ liệu ra giao diện console trong C# có thể sử dụng phương
thức Console.Write hoặc Console.WriteLine. Hai phương thức này có thể
nhận tham số thuộc bất kỳ kiểu nào, bất kỳ số lượng nào. Write hoặc

61
WriteLine sau đó sẽ gọi tới phương thức ToString() của kiểu dữ liệu đó để
chuyển tham số sang chuỗi ký tự và in ra console.
Lưu ý rằng, ToString() là phương thức được tất cả các kiểu kế thừa từ kiểu object
(System.Object). Mỗi kiểu sẽ ghi đè ToString() theo cách riêng cho phù hợp. Bạn cũng có thể
tự ghi đè phương thức này trong class do mình xây dựng.
WriteLine khác biệt với Write ở duy nhất một điểm: Sau khi in thông tin ra
console, WriteLine sẽ xuống một dòng mới và di chuyển con trỏ văn bản về
đầu dòng mới, trong khi Write giữ nguyên vị trí con trỏ ở sau ký tự cuối
cùng được in ra.
Cùng thực hiện ví dụ đơn giản sau đây.
Tạo solution trống đặt tên là S04_Console. Thêm project Console App đặt
tên là P01_ConsoleOutput. Viết code cho Program.cs như sau:
using System;
namespace P01_ConsoleOutput
{
class Program
{
static void Main()
{
Console.WriteLine(10); // in ra một số nguyên
Console.WriteLine(10.0); // in ra một số thực
Console.WriteLine(true); // in ra một giá trị logic
Console.WriteLine("Hello world"); // in ra một chuỗi
int a = 100;
Console.WriteLine(a); // in ra một biến kiểu số nguyên
bool b = false;
Console.WriteLine(b); // in ra một biến kiểu logic
string c = "Hi there!";
Console.WriteLine(c); // in ra một biến kiểu xâu ký tự
Console.ReadKey();
}
}
}

Bạn hãy tự thử thay WriteLine bằng Write trong ví dụ trên để thấy sự
khác biệt.
Đối với các kiểu dữ liệu phức tạp (như class) thì phương thức
WriteLine/Write chỉ đơn giản là in ra tên đầy đủ của kiểu dữ liệu mà

62
không thể in ra các dữ liệu mà object đang chứa. Do đó, bạn cần tự mình
ghi đè phương thức ToString() trên class mình xây dựng nếu muốn dùng
trực tiếp với Write/WriteLine.
Thay đổi màu sắc cho console
Giao diện console nền đen chữ trắng không chỉ nhàm chán mà còn khó theo
dõi khi hiển thị nhiều dữ liệu. Lớp Console cho phép thay đổi màu nền và
màu văn bản in ra trên console thông qua thiết lập giá
trị BackgroundColor và ForegroundColor trước khi gọi lệnh in Write hoặc
WriteLine. Nếu muốn trả lại màu sắc mặc định chỉ cần gọi phương
thức ResetColor().
Hãy cùng thực hiện ví dụ sau:
Thêm project P03_ColorfulConsole vào solution và viết code cho
Program.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace P03_ColorfulConsole
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Colorful Console";
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Your name: ");
Console.ResetColor();
var name = Console.ReadLine();
Console.BackgroundColor = ConsoleColor.Blue;
Console.Write(name);
Console.ReadLine();
}
}
}
Các hằng số màu được định nghĩa trong kiểu liệt kê (enum) ConsoleColor
với 16 giá trị màu khác nhau. Một khi giá trị màu sắc đã được thiết lập nó
sẽ có tác dụng đến khi gặp thiết lập màu khác, hoặc đến khi gặp
ResetColor().
Trong ví dụ trên chúng ta cũng sử dụng property Console.Title để đặt
tiêu đề cho cửa sổ console.

63
Nhập dữ liệu từ console trong C#: ReadLine, Read,
ReadKey
So với xuất dữ liệu, việc đọc dữ liệu từ console phức tạp hơn một chút. Để
nhập dữ liệu từ giao diện dòng lệnh có thể sử dụng các phương thức của
lớp Console, bao gồm ReadLine, ReadKey, Read.
ReadLine
ReadLine đọc một dòng và trả về một chuỗi ký tự.
Khi sử dụng phương thức ReadLine, màn hình console sẽ dừng lại chờ nhập
dữ liệu. Người dùng có thể nhập nhiều ký tự liên tục cho đến khi bấm phím
Enter để kết thúc nhập.
Do ReadLine luôn trả về một chuỗi ký tự, chúng ta cần chuyển đổi
chuỗi sang các kiểu khác theo nhu cầu.
Phương thức ReadLine cũng có thể dùng để dừng màn hình console chờ
người dùng bấm Enter để tiếp tục.
Cùng thực hiện ví dụ sau. Tạo thêm project mới P02_ReadLine trong
solution và viết code cho Program.cs:
using System;
namespace P02_ReadLine
{
class Program
{
static void Main(string[] args)
{
Console.Write("Enter a number: ");
string number = Console.ReadLine();
Console.WriteLine(number);
Console.Write("Enter true or false: ");
string logic = Console.ReadLine();
Console.WriteLine(logic);
Console.Write("Enter a string: ");
string message = Console.ReadLine();
Console.WriteLine(message);
Console.Write("Press enter to quit");
Console.ReadLine(); // dừng màn hình chờ ấn enter
}
}
}

ReadKey
ReadKey đọc một ký tự và trả về kiểu ConsoleKeyInfo. Khi sử dụng
ReadKey, màn hình console sẽ dừng lại chờ nhập dữ liệu.
Khi người dùng bấm một phím bất kỳ, ReadKey sẽ đọc và kết thúc nhập
ngay lập tức (không cần dùng phím Enter để kết thúc nhập như ReadLine).

64
Thông tin về ký tự người dùng đã nhập được trả về một biến thuộc kiểu
ConsoleKeyInfo.
Phương thức ReadKey cũng thường được sử dụng để dừng màn hình console
chờ người dùng bấm một phím bất kỳ để tiếp tục.
Thêm project P04_ReadKey vào solution và viết code cho Program.cs như
sau:
using System;
namespace P04_ReadKey
{
class Program
{
static void Main(string[] args)
{
Console.Title = "ReadKey";
Console.Write("Press any key: ");
var key = Console.ReadKey();
Console.WriteLine();
Console.WriteLine(key.KeyChar);
Console.WriteLine(key.Modifiers);
if (key.Key == ConsoleKey.H)
Console.WriteLine("Hello!");
Console.ReadKey();
}
}
}
Kiểu ConsoleKeyInfo lưu trữ một số thông tin về phím người dùng đã bấm:
 KeyChar (kiểu char): ký tự người dùng đã nhập;
 Modifiers (kiểu enum ConsoleModifiers): các phím điều khiển bấm
cùng như Ctrl, Shift, Alt;
 Key (kiểu enum ConsoleKey): phím chuẩn trên console.
ConsoleKey và char hoàn toàn không giống nhau, và cũng không thể sử dụng lẫn lộn nhau.
Kiểu char bạn đã biết qua bài học các kiểu dữ liệu cơ sở của C#. Bạn có thể đọc thêm bài viết
về ConsoleKey enum.
Read
Read đọc một ký tự và trả về mã của ký tự đó. Khi sử dụng Read, màn hình
console sẽ dừng chờ nhập dữ liệu.
Người dùng có thể nhập cả một chuỗi ký tự và bấm phím Enter để kết thúc
nhập. Tuy nhiên, Read chỉ trả về mã của ký tự đầu tiên của chuỗi người
dùng nhập.

65
Một số vấn đề khi làm việc với console trong C#
Đơn giản hóa lời gọi phương thức của Console
Từ C# 6 bắt đầu đưa vào cấu trúc using static để gọi tắt tới các thành viên
static của một class. Lớp Console chứa toàn các thành viên static và rất phù
hợp với cách dùng này. Hãy cùng làm lại ví dụ trên:
using System;
using static System.Console;
namespace P03_ColorfulConsole
{
class Program
{
static void Main(string[] args)
{
Title = "Colorful Console";
ForegroundColor = ConsoleColor.Magenta;
Write("Your name: ");
ResetColor();
var name = ReadLine();
BackgroundColor = ConsoleColor.Blue;
Write(name);
ReadLine();
}
}
}
Bạn đã bớt kha khá code với cách viết này khi không cần lặp lại tên class
Console khi gọi phương thức hay thành viên tĩnh của nó nữa.
Biến đổi dữ liệu đọc từ console
Lấy ví dụ, bạn yêu cầu người dùng nhập vào một số nguyên. Do dữ liệu
nhập từ console sử dụng ReadLine đều là string, bạn phải biến đổi chuỗi đó
thành số thì mới sử dụng được trong biểu thức. Ngoài ra, do người dùng có
thể vô tình nhập lẫn cả chữ cái vào chuỗi chữ số dẫn đến lỗi khi biến đổi.
Như vậy, khi đọc dữ liệu từ console bạn phải thực hiện hai nhiệm vụ: kiểm
tra tính chính xác của chuỗi, biến đổi chuỗi về giá trị thuộc kiểu mình cần.
Hầu hết các kiểu dữ liệu cơ bản của C# đã hỗ trợ thực hiện các nhiệm vụ
này với phương thức Parse() và TryParse(). Loại biến đổi dữ liệu này được
gọi là type conversion, để phân biệt với type casting (ép kiểu).
Console.Write("Enter an integer: ");
string input = Console.ReadLine(); // đọc vào một chuỗi chữ số
int i = int.Parse(input); // biến đổi input thành int
Console.Write("Enter true or false: ");
input = Console.ReadLine(); // đọc chuỗi "true" hoặc "false"
bool b = bool.Parse(input); // biến đổi chuỗi thành bool
Console.Write("Enter a double: ");
input = Console.ReadLine(); // đọc vào một chuỗi chữ số
double d = double.Parse(input); // biến đổi input thành int

66
In chuỗi có định dạng với placeholder
Hãy cùng xem ví dụ sau đây:
int x1 = 123, x2 = 456;
Console.WriteLine("Nghiệm thứ nhất: {0}, Nghiệm thứ hai: {1}", x1, x2);
Phương thức Write/WriteLine có thể in ra một xâu ký tự đồng thời ghép các
biến vào những vị trí đánh dấu sẵn trong một xâu mẫu.
Trong ví dụ trên, vị trí đánh dấu (placeholder) được biểu diễn bằng cụm {0}
và {1}. Trong đó {0} sẽ được thay thế bởi biến thứ nhất trong danh sách
(tức là x1); {1} sẽ được thay thế bởi biến thứ hai trong danh sách (tức là
x2).
Như vậy có thể để ý, các biến trong danh sách được đánh số thứ tự từ 0, và
sẽ được thay thế vào vị trí đánh dấu có chỉ số tương ứng.
In chuỗi có định dạng với string interpolation
Bắt đầu từ C# 6 xuất hiện một phương pháp định dạng chuỗi mới gọi là
interpolated.
Một chuỗi interpolated được bắt đầu bằng ký tự $, ở những vị trí cần thay
thế bằng giá trị từ biểu thức/biến, biểu thức/biến đó sẽ đặt trong cặp
dấu {}.
Khi gặp loại chuỗi này, C# sẽ tính giá trị của biểu thức/lấy giá trị biến và
đặt vào đúng vị trí quy định.
Ví dụ trên có thể viết lại theo cách mới như sau:
int x1 = 123, x2 = 456;
Console.WriteLine($"Nghiệm thứ nhất: {x1}, Nghiệm thứ hai: {x2}");
Cách viết này rõ ràng, sáng sủa và dễ đọc hơn nhiều. Trong cặp dấu {} bạn
có thể để bất kỳ biểu thức nào của C#, thậm chí cả những biểu thức phức
tạp như phép toán điều kiện hay các đoạn code.

Định dạng số khi in ra console


Trong nhiều tình huống bạn cần in số có định dạng. Ví dụ khi in giá tiền,
bạn muốn kèm theo đơn vị tiền tệ và chỉ với 2 chữ số thập phân. Bạn cũng
có thể muốn dành một độ rộng cố định để in số (như trong bảng biểu). Bạn
muốn số in ra căn lề trái hoặc căn lề phải.
Các lệnh in của Console hỗ trợ thực hiện các thao tác định dạng này. Định
dạng in cho mỗi số bao gồm 3 phần: alignment, format và precision.
Để thử nghiệm, bạn hãy dùng C# Interactive.
Lưu ý là cách viết số này chỉ áp dụng được trong chuỗi có định dạng với placeholder hoặc string
interpolation.
67
Format specifier
Ví dụ, để in ra giá tiền, bạn dùng :C hoặc :c phía sau chỉ số trong
placeholder, hoặc sau tên biến trong chuỗi interpolated. Đơn vị tiền tệ sẽ
phụ thuộc vào cấu hình ngôn ngữ của Windows.
> Console.WriteLine("The value: {0}", 500)
The value: 500
> Console.WriteLine("The value: {0:C}", 500)
The value: $500.00
> Console.WriteLine("The value: {0:c}", 500)
The value: $500.00
> decimal value = 500;
. Console.WriteLine($"The value: {value:C}");
The value: $500.00
> Console.WriteLine($"The value: {value:c}");
The value: $500.00
>

C hoặc c là định dạng số chuẩn dành cho kiểu tiền tệ (currency). Ngoài C
(c), bạn có thể gặp thêm các trường hợp khác: D, d – Decimal; F, f –
Fixed point; G, g – General; X, x – Hexadecimal; N, n – Number; P, p –
Percent; R, r – Round-trip; E, e – Scientific.
Các ký tự định dạng số chuẩn này phải đi ngay dấu hai chấm, ví dụ :C, :c,
:P, :p như bạn đã thấy ở trên.
Bạn có thể tự mình thử nghiệm các định dạng trên để xem kết quả.
Precision specifier
Sau ký tự định dạng bạn có thể sử dụng thêm một con số để mô tả độ chính
xác (precision specifier) của giá trị được in ra.
Ví dụ, mặc định :C sẽ in ra hai chữ số thập phân. Bạn có thể yêu cầu in ra
con số với độ chính xác cao hơn (3-4 chữ số thập phân) hoặc thấp hơn.
Chẳng hạn :c3 chỉ định in ra 3 chữ số thập phân, :c4 chỉ định in 4 chữ số
thập phân.
> Console.WriteLine($"The value: {value:c3}");
The value: $500.000
> Console.WriteLine($"The value: {value:c4}");
The value: $500.0000

Cách thể hiện của “độ chính xác” phụ thuộc vào loại số và định dạng của
nó. Bạn hãy xem ví dụ sau đây:
68
> Console.WriteLine("{0 :C}", 12.5);
$12.50
> Console.WriteLine("{0 :D4}", 12);
0012
> Console.WriteLine("{0 :F4}", 12.3456789);
12.3457
> Console.WriteLine("{0 :G4}", 12.3456789);
12.35
> Console.WriteLine("{0 :x}", 180026);
2bf3a
> Console.WriteLine("{0 :N2}", 12345678.54321);
12,345,678.54
> Console.WriteLine("{0 :P2}", 0.1221897);
12.22%
> Console.WriteLine("{0 :e4}", 12.3456789);
1.2346e+001
>

Alignment specifier
Để dễ hiểu, hãy xem ví dụ sau:
> int myInt = 500;
> Console.WriteLine("|{0, 10}|", myInt);
. Console.WriteLine("|{0,-10}|", myInt);
| 500|
|500 |
>

Ở đây chúng ta mô phỏng lại việc in giá trị ra thành cột. Con số 10 và -10
viết tách với chỉ số placeholder bằng dấu phẩy được gọi là alignment
specifier. Alignement specifier chỉ định độ rộng (số lượng ký tự) tối thiểu
để in giá trị số đó. Giá trị dương báo hiệu căn lề phải; Giá trị âm báo hiệu
căn lề trái.
Format specifier mà bạn đã biết viết về phía phải của alignment specifier:
> double myDouble = 12.345678;
. Console.WriteLine("{0,-10:G} -- General", myDouble);
. Console.WriteLine("{0,-10} -- Default, same as General", myDouble);
. Console.WriteLine("{0,-10:F4} -- Fixed Point, 4 dec places", myDouble);
. Console.WriteLine("{0,-10:C} -- Currency", myDouble);
. Console.WriteLine("{0,-10:E3} -- Sci. Notation, 3 dec places", myDouble);
. Console.WriteLine("{0,-10:x} -- Hexadecimal integer", 1194719);
69
12.345678 -- General
12.345678 -- Default, same as General
12.3457 -- Fixed Point, 4 dec places
$12.35 -- Currency
1.235E+001 -- Sci. Notation, 3 dec places
123adf -- Hexadecimal integer
>

70
Các cấu trúc điều khiển trong C#
Bài học này sẽ hướng dẫn bạn cách sử dụng các cấu trúc điều khiển cơ bản
trong C#. Bạn có thể sẽ gặp những người quen như if-else, switch-case,
while, do-while nếu bạn biết C hay Java. Tuy nhiên, việc sử dụng các cấu
trúc điều khiển này trong C# có những điểm khác biệt nhất định mà bạn
cần biết. C# 7 thậm chí còn đưa thêm một số đặc điểm của lập trình hàm
(functional programming) vào cấu trúc điều khiển cơ bản.
Mặc định trong C# và hầu hết các ngôn ngữ lập trình imperative, các lệnh
được thực hiện lần lượt theo thứ tự được chỉ định trong code. Trình tự thực
hiện các lệnh thường được gọi là luồng thực thi (flow of execution). Tuy
nhiên, nếu chỉ thực thi lệnh theo thứ tự thì khả năng của chương trình sẽ bị
giới hạn. Từ đó dẫn tới trong các ngôn ngữ lập trình đều phải đưa ra các cấu
trúc điều khiển (flow control). Các cấu trúc này có tác dụng thay đổi trật tự
thực thi lệnh thông thường.
Nhìn chung các cấu trúc điều khiển được xếp vào các nhóm: cấu trúc điều
kiện (còn gọi là cấu trúc rẽ nhánh), cấu trúc lặp, cấu trúc nhảy.

Cấu trúc điều kiện (rẽ nhánh) trong C#: if-else, switch-case
Cấu trúc điều kiện, còn gọi là cấu trúc rẽ nhánh cho phép phân tách việc
thực thi code thành nhiều hướng khác nhau tùy thuộc vào một điều kiện
nào đó. Điều kiện này thông thường được xác định theo giá trị của biến/biểu
thức.
C# sử dụng hai cấu trúc điều kiện: cấu trúc if-else và cấu trúc switch-
case.
Cấu trúc rẽ nhánh if else
Cấu trúc if-else của C# hoàn toàn thừa kế từ C/C++. Cú pháp của cấu
trúc if-else như sau:
if (condition)
{
statements 1
}
else
{
statements 2
}

71
Trong đó, condition là biểu thức logic – biểu thức mà giá trị trả về là true
hoặc false. Đây là kết quả thực hiện các phép so sánh, hoặc kết quả trả về
của một số phương thức.
Các phép toán so sánh trong C# bao gồm == (so sánh bằng), > (lớn hơn), < (nhỏ hơn), >= (lớn
hơn hoặc bằng), <= (nhỏ hơn hoặc bằng), != (khác). Các biểu thức hoặc giá trị logic có thể được
kết hợp với nhau bởi các phép toán logic: && (và), || (hoặc), ! (phủ định).
statements 1 là danh sách các lệnh sẽ thực thi nếu condition có giá trị
true; statements 2 là danh sách lệnh sẽ thực thi nếu condition có giá trị
false.
Có một số lưu ý sau khi dùng if-else:
 Nếu statements 1 hoặc statements 2 chỉ có một lệnh duy nhất thì
có thể không cần dùng cặp dấu {}.
 Nhánh else {} là không bắt buộc; if thì bắt buộc phải có.
 Bình thường bạn chỉ có thể tạo ra 2 nhánh rẽ: 1 nhánh if, 1 nhánh
else.
 Để tạo thêm nhiều nhánh rẽ nữa bạn có thể kết hợp thêm các nhánh
else if vào cấu trúc trên. Số lượng nhánh else if không giới hạn.
 Bạn có thể lồng nhiều if-else với nhau.
Hãy cùng thực hiện ví dụ sau để hiểu rõ hơn cách sử dụng if-else
 Tạo solution rỗng đặt tên là S03_FlowControls.
 Trong solution này tạo thêm một project kiểu Console App đặt tên là
P01_IfElse.
Viết code cho tập tin Program.cs như sau:
using System;
using System.Text;
namespace P01_IfElse
{
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
Console.Write("Nhập nhiệt độ (oC): ");
var input = Console.ReadLine();
var temperature = int.Parse(input);
if (temperature <= 5)
{
Console.WriteLine("Lạnh quá!");
}
else
{
if (temperature <= 15)
{
72
Console.WriteLine("Mát mẻ, dễ chịu!");
}
else
{
if (temperature <= 25)
{
Console.WriteLine("Ấm áp!");
}
else
{
Console.WriteLine("Nóng quá!");
}
}
}
Console.ReadKey();
}
}
}
Đoạn code trên phân chia việc thực hiện code vào nhiều nhánh với các cấu
trúc if-else lồng nhau. Các nhánh bao gồm: dưới 5 độ, từ 5 đến 15 độ, từ
15 đến 25 độ, trên 25 độ.
Nếu bạn không thích sử dụng các khối if-else lồng nhau, bạn có thể mở
nhánh bằng cụm else if. Khối if-else bên trên hoàn toàn tương đương
với cách viết dưới đây:
if (temperature < 5)
{
Console.WriteLine("Lạnh quá!");
}
else if (temperature <= 15)
{
Console.WriteLine("Mát mẻ, dễ chịu!");
}
else if (temperature <= 15)
{
Console.WriteLine("Ấm áp!");
}
else
{
Console.WriteLine("Nóng quá!");
}
Có thể thấy, các nhánh else if thực chất chỉ là dạng viết khác của các cấu
trúc if else lồng nhau để tránh rối rắm (vì nguyên tắc if-else chỉ có hai
nhánh). Việc lựa chọn cách viết nào hoàn toàn mang tính cá nhân.
Tuy nhiên, lưu ý rằng, sau if và else NÊN sử dụng code block ngay cả khi
có 1 lệnh duy nhất.

73
Cấu trúc rẽ nhiều nhánh switch-case
Ở bên trên bạn đã gặp cấu trúc rẽ nhánh if-else. Cấu trúc này chỉ cho
phép rẽ tới 2 nhánh. Nếu muốn rẽ nhiều nhánh, bạn phải lồng ghép các
nhánh else if khiến code trở nên khó đọc.
C# cung cấp một cấu trúc khác để thực hiện rẽ nhiều nhánh thay cho việc
lồng ghép nhiều if-else: cấu trúc switch-case. Cú pháp như sau:
switch(expression)
{
case <value1>
// code block
break;
case <value2>
// code block
break;
case <valueN>
// code block
break;
default
// code block
break;
}
Cấu trúc này yêu cầu phải cung cấp một biểu thức “expression” (lệnh tính
toán). Giá trị của expression sẽ được tính ra và lần lượt so sánh với value1,
value2, .., valueN. Các value này bắt buộc phải là các hằng số hoặc biểu
thức tính ra hằng số, KHÔNG được sử dụng biến.
Nếu trùng với value nào, khối lệnh tương ứng sẽ được thực hiện, sau đó sẽ
bỏ qua tất cả các kiểm tra còn lại. Vì lý do này, C# bắt buộc mỗi “case” phải
được kết thúc bằng lệnh break hoặc return. Quy định này khiến cấu trúc
switch-case của C# an toàn hơn một chút so với trong C/C++ (vốn không
bắt buộc dùng break).
Khi một case được thực hiện, bạn có thể tiếp tục nhảy sang một case khác
bằng lệnh nhảy goto case. Cách sử dụng switch-case mà thực hiện được
nhiều case cùng lúc như vậy có tên gọi là fall-through.
Nếu giá trị của biểu thức tính ra không trùng với bất kỳ case nào, khối lệnh
default sẽ được thực hiện. Khối default không bắt buộc. Trong trường
hợp không có khối default và giá trị của expression không trùng với bất kỳ

74
case nào, cấu trúc switch đơn giản là không thực hiện bất kỳ khối lệnh
nào.
Các lệnh đi sau mỗi case không cần viết trong cặp {}, kể cả khi có nhiều
lệnh.
Hãy cùng thực hiện một ví dụ để hiểu rõ cú pháp của cấu trúc này.
Thêm project P02_SwitchCase vào solution. Viết code cho tập tin
Program.cs như sau:
using System;
using System.Text;
namespace P02_SwitchCase
{
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
Console.Write("Nhập một số từ 1 đến 8: ");
var day = Console.ReadLine();
switch (day)
{
case "2":
Console.WriteLine("Thứ hai");
break;
case "3":
Console.WriteLine("Thứ ba");
break;
case "4":
Console.WriteLine("Thứ tư");
break;
case "5":
Console.WriteLine("Thứ năm");
break;
case "6":
Console.WriteLine("Thứ sáu");
break;
case "7":
Console.WriteLine("Thứ bảy");
break;
// nhập 1 và 8 sẽ đều thực hiện chung lệnh viết ra "Chủ nhật", rồi
quay về case "2"
case "1":
case "8":
Console.WriteLine("Chủ nhật");
goto case "2";
// nếu nhập bất kỳ giá trị nào khác sẽ thực hiện lệnh ở nhóm này
default:
Console.WriteLine("Bạn nhập sai rồi");
break;
}
Console.ReadKey();
}
}
}

75
Các cấu trúc lặp trong C#
C# cung cấp 4 cấu trúc lặp khác nhau: do-while, while, for, foreach.
foreach là cấu trúc lặp đặc biệt của C# chuyên dùng với các kiểu dữ liệu tập hợp như mảng
(array), danh sách (list). Chúng ta sẽ học cấu trúc này trong bài học về mảng trong C#.
Hãy cùng thực hiện một ví dụ trước.
Thêm project mới đặt tên là P03_Loops và viết code cho Program.cs như
sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace P03_Loops
{
class Program
{
static void Main(string[] args)
{
var i = 0;
Console.WriteLine("While loop");
while (i < 10)
{
Console.Write($"{i}\t");
i++;
}
Console.WriteLine("\r\nDo-While loop");
i = 0;
do
{
Console.Write($"{i}\t");
i++;
} while (i < 10);
Console.WriteLine("\r\nFor loop");
for (i = 0; i < 10; i++)
{
Console.Write($"{i}\t");
}
Console.ReadKey();
}
}
}
Ví dụ này minh họa cách sử dụng 3 loại vòng lặp while, do-while và for để
in ra console các số từ 0 đến 9.
Cấu trúc while
while ( <biểu_thức_logic> ) { [<danh_sách_lệnh>] }
Chừng nào biểu thức logic còn nhận giá trị true thì danh sách lệnh sẽ được
thực hiện. Cấu trúc này sẽ luôn kiểm tra biểu thức logic trước, sau đó mới
thực hiện danh sách lệnh.

76
var i = 0;
while (i < 10)
{
Console.Write($"{i}\t");
i++;
}
Trong cấu trúc while, danh sách lệnh có thể không được thực hiện lần nào.
Tình huống này xảy ra khi biểu thức logic nhận giá trị false ngay từ đầu.
Lưu ý rằng trong thân của while phải có lệnh làm thay đổi giá trị của biểu
thức logic. Nếu không sẽ tạo ra vòng lặp vô hạn.
Cấu trúc do-while
do { [<danh_sách_lệnh>] } while ( <biểu_thức_logic> );
Thực hiện danh sách lệnh rồi mới kiểm tra giá trị của biểu thức logic. Nếu
biểu thức logic vẫn nhận giá trị true, danh sách lệnh sẽ lại được thực hiện.
int i = 0;
do
{
Console.Write($"{i}\t");
i++;
} while (i < 10);
Cấu trúc do-while khác biệt với while ở chỗ, danh sách lệnh sẽ được thực
hiện trước, sau đó mới kiểm tra giá trị của biểu thức logic. Do đó, khi sử
dụng cấu trúc do-while, danh sách lệnh luôn luôn thực hiện ít nhất một
lần.
Lưu ý rằng, sau while(<biểu_thức_logic>) phải có dấu chấm phẩy.
do {...}
while(i < 10);
Giống như đối với while, phải có lệnh làm thay đổi giá trị của biểu thức
logic trong khối code của do. Nếu không sẽ tạo ra vòng lặp vô hạn.
Cấu trúc for
for (<khởi tạo giá trị đầu của biến điều khiển>; <kiểm tra điều kiện dừng
của biến điều khiển>; <bước nhảy>)
{
[<danh_sách_lệnh>]
}
Cấu trúc này sẽ thực hiện danh sách lệnh một số lần xác định (trong khi hai
cấu trúc trên không xác định được số lần thực hiện).
int i;
for (i = 0; i < 10; i++)
{

77
Console.Write($"{i}\t");
}
Trong cấu trúc for, biến điều khiển, cách thay đổi giá trị của biến điều khiển
cũng như điều kiện kiểm tra biến điều khiển đều viết chung trong khai báo.
C# sẽ tự thay đổi giá trị biến điều khiển theo công thức chúng ta cung cấp.
Bạn có thể thực hiện đồng thời khai báo và khởi tạo giá trị của biến điều
khiển ngay trong cấu trúc for, thay vì phải khai báo biến riêng.
for (var i = 0; i < 10; i++)
{
...
}
Bạn có thể lồng nhiều vòng for với nhau, ví dụ, để duyệt một ma trận.
// duyệt qua các hàng
for (int i = 0; i < 100; i += 10)
{
// duyệt qua các cột trong một hàng
for (int j = i; j < i + 10; j++)
{
Console.Write($" {j}");
}
Console.WriteLine();
}

Điều khiển vòng lặp


Trong vòng lặp có thể sử dụng lệnh break hoặc continue để điều khiển
hoạt động của vòng lặp. Cụ thể như sau:
1. Lệnh break: phá vỡ vòng lặp. Khi gặp lệnh break, tất cả các lệnh
đứng sau break sẽ không thực hiện nữa, vòng lặp kết thúc.
2. Lệnh continue: phá vỡ chu kỳ hiện tại của vòng lặp. Khi gặp lệnh
continue, tất cả lệnh đứng sau continue không thực hiện nữa, vòng
lặp sẽ chuyển sang chu kỳ tiếp theo.
Ví dụ sử dụng break và continue để điều khiển vòng lặp. Thêm project
P04_BreakContinue vào solution và viết code cho Program.cs như sau:
using System;
namespace P04_BreakContinue
{
class Program
{
static void Main(string[] args)
{
var i = 0;
while (true)
{
if (i == 10)
break;

78
Console.Write($"{i}\t");
i++;
}
var j = 0;
do
{
j++;
if (j == 5)
continue;
Console.Write($"{j}\t");
} while (j < 10);
Console.ReadKey();
}
}
}
Khi chạy ví dụ trên bạn sẽ thấy, vòng lặp while chỉ in ra các số từ 0 đến
10, mặc dù biểu thức logic luôn luôn nhận giá trị true – tức là nhẽ ra đây
phải là một vòng lặp vô hạn. Vòng lặp do-while tiếp theo chỉ in ra các số
từ 1 đến 10 nhưng lại bỏ qua giá trị số 5.
Ở vòng lặp thứ nhất, nếu biến điều khiển i có giá trị bằng 10 thì sẽ thực
hiện lệnh break. Lệnh này sẽ phá vỡ (kết thúc) vòng lặp và thoát ra ngoài.
Ở vòng lặp thứ hai, nếu biến điều khiển i có giá trị bằng 5 thì sẽ phá vỡ chu
kỳ đó, tức là bỏ qua hết tất cả các lệnh phía sau continue. Dẫn đến giá trị
5 không được in ra console. Nhưng continue không phá vỡ vòng lặp như
break. Nên một chu kỳ mới lại bắt đầu như bình thường.
Bạn có thể sử dụng return thay cho break. Lúc này return không những phá vỡ vòng
lặp mà còn kết thúc luôn việc thực thi của phương thức.

79
Mảng trong C#: một chiều, nhiều chiều, răng cưa
(jagged)
Mảng là một cấu trúc dữ liệu đơn giản và có mặt trong hầu hết các ngôn
ngữ lập trình, trong đó có C#. Dữ liệu kiểu mảng có rất nhiều ứng dụng và
các thuật toán áp dụng cho nó. Trong bài học này chúng ta sẽ đi vào kiểu
dữ liệu tập hợp (collection) cơ bản nhất, đơn giản nhất và phổ biến nhất
trong C# – cấu trúc mảng (array). Đây là bài học đầu tiên về các kiểu dữ
liệu tập hợp tuyến tính (linear collection) của C#.

Mảng trong C# – các khái niệm và hoạt động cơ bản


Mảng (array) là một cấu trúc dữ liệu phổ biến bậc nhất, có mặt trong hầu
hết các ngôn ngữ lập trình, và C# không phải là ngoại lệ.
Mảng là một tập các dữ liệu có cùng kiểu. Mỗi dữ liệu đơn lẻ trong mảng
được gọi là phần tử của mảng. Các phần tử được đánh chỉ số (index) và
được sắp xếp liền kề nhau thành một chuỗi. Kiểu của phần tử được gọi
là kiểu cơ sở của mảng. Kiểu cơ sở có thể là các kiểu dữ liệu sẵn có của C#
hoặc kiểu do người dùng tự định nghĩa. Chỉ số của mảng trong C# bắt đầu
là 0, nghĩa là phần tử đầu tiên có chỉ số 0, phần tử thứ hai có chỉ số là 1,…
Khai báo biến mảng
Trong C#, mảng là object, và chúng đều được tạo ra từ lớp System.Array.
Do đó, chúng ta có thể khai báo biến mảng như khai báo các object bình
thường. Để tiện lợi và quen thuộc cho người lập trình, C# tạo ra một cấu
trúc cú pháp riêng để khai báo mảng.
//cú pháp chung
kiểu_cơ_sở[] biến_mảng;
//ví dụ dưới đây khai báo biến mảng names với kiểu cơ sở là string
string[] names;
// string là kiểu cơ sở; names là tên biến mảng; cặp dấu [] là bắt buộc, nó báo rằng
đây là khai báo một biến mảng
// khai báo mảng số nguyên
int[] numbers;

Lưu ý phân biệt các khái niệm biến mảng, kiểu mảng, kiểu cơ sở. Trong ví dụ trên, string là kiểu
cơ sở, còn string[] được gọi là kiểu mảng, còn names là biến mảng. Trong C# viết string[] có
nghĩa là đây là một kiểu mảng, trong đó kiểu cơ sở của phần tử là string, string[] names là lệnh
khai báo biến mảng.
Một biến mảng khi được khai báo sẽ chưa được cấp phát bộ nhớ ngay. Nói
cách khác, nếu chỉ khai báo thì chưa thể sử dụng được biến mảng. Để sử
80
dụng biến mảng chúng ta phải khởi tạo mảng. Điều này cũng tương tự
như đối với các object bình thường khác trong C#. Mỗi object phải được khởi
tạo sau khi khai báo và trước khi sử dụng.
Khởi tạo biến mảng
Khởi tạo biến là việc yêu cầu cấp phát bộ nhớ cho biến đó. Chỉ khi khởi tạo
xong chúng ta mới có thể sử dụng được biến mảng. C# cung cấp một số
cách khác nhau để khởi tạo biến mảng.
Cách thứ nhất là cung cấp số lượng phần tử cần dùng.
// khởi tạo mảng string với 10 phần tử
string[] names = new string[10];
Cách này tạo ra một mảng string có 10 phần tử. Mỗi phần tử sẽ nhận giá
trị mặc định là null.
Trong C#, các biến khi khai báo mà không gán sẵn giá trị sẽ đều nhận một giá trị mặc
định (default value). Giá trị này phụ thuộc vào kiểu của nó. Ví dụ biến kiểu int có giá trị mặc
định là 0, kiểu bool là false, object là null. Như vậy, ở ví dụ trên, mỗi phần tử của mảng
names có giá trị mặc định là null (do string là object).
Cách thứ hai là cung cấp sẵn danh sách phần tử cho biến mảng.
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
Cách thứ ba là một cấu trúc tắt có tên gọi là array initializer. Trong cấu
trúc này bạn không sử dụng được từ khóa var mà bắt buộc phải chỉ định rõ
kiểu mảng.
int[] numbers = { 1, 2, 3, 4 };
Lưu ý rằng, một khi được khởi tạo, số lượng phần tử của mảng sẽ không
thể thay đổi được.
Truy xuất phần tử của mạng
Một khi mảng đã được khởi tạo, chúng ta có thể sử dụng nó để lưu trữ dữ
liệu. Việc sử dụng bao gồm hai phần: truy xuất phần tử và truy xuất thông
tin meta.
Các phần tử của mảng trong C# được truy xuất (đọc/ghi) trực tiếp qua chỉ
số với cú pháp như sau:
// gán giá trị cho phần tử dùng phép toán indexing
biến_mảng[chỉ_số] = giá_trị;
// hoặc dùng phương thức SetValue theo kiểu hướng đối tượng
biến_mảng.SetValue(giá_trị, chỉ_số);
// đọc giá trị bằng phép toán indexing
biến = biến_mảng[chỉ_số];
// hoặc dùng phương thức GetValue
81
biến = biến_mảng.GetValue(chỉ_số);
Ví dụ:
names[0] = "Donald Trump";
names.SetValue("Donald Trump", 0);
var current_us_president = names[0];
var next_us_president = names.GetValue(0);
Chỉ số có giá trị từ 0 đến n-1, với n là số phần tử của mảng. Nếu chỉ số
nằm ngoài dải này, việc truy xuất sẽ báo lỗi IndexOutOfRangeException.
Đọc metadata của mảng
Metadata là những thông tin về bản thân mảng, như số lượng phần tử, kiểu
cơ sở,.... Do mảng đều là các object thuộc kiểu System.Data, chúng ta có
thể sử dụng các thuộc tính (và phương thức) của lớp này để đọc metadata
của mảng.
 Thuộc tính Length/LongLength (read-only): số phần tử của mảng
 Phương thức GetLength/GetLongLength: đọc số phần tử của mảng
 Thuộc tính Rank (read-only): số chiều của mảng. Chúng ta sẽ làm
quen với mảng nhiều chiều ngay sau đây.
 Phương thức GetType: lấy thông tin về kiểu của mảng.
Một ví dụ tổng hợp về sử dụng mảng cơ bản
Tự thực hiện lại ví dụ dưới đây để củng cố các kỹ thuật làm việc cơ bản với
mảng. Trong ví dụ này chúng ta sẽ lưu trữ tên các tháng (trong tiếng Anh)
vào một mảng và in ra màn hình console.
using System;
using System.Globalization;
namespace P01_SingleDimension
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Basic Array";
// khai báo và khởi tạo mảng chứa tên 12 tháng trong tiếng Anh
string[] months = new string[12];
// duyệt qua các phần tử và gán giá trị
for (int month = 1; month <= 12; month++)
{
DateTime firstDay = new DateTime(DateTime.Now.Year, month, 1);
string name = firstDay.ToString("MMMM",
CultureInfo.CreateSpecificCulture("en"));
months[month - 1] = name;
}
// duyệt qua các phần tử và in giá trị ra console
foreach (string month in months)
{

82
Console.WriteLine($"-> {month}");
}
Console.ReadLine();
}
}
}

Kết quả chạy chương trình


Trong ví dụ trên bạn thấy một cấu trúc lặp đặc biệt: vòng lặp foreach. Nếu
chưa biết về cấu trúc này, hãy đọc phần tiếp theo. Nếu bạn đã biết cấu trúc
này thì có thể bỏ qua để chuyển thẳng sang nội dung về mảng nhiều chiều.

Duyệt mảng trong C# với cấu trúc foreach


C# cung cấp một cấu trúc điều khiển vòng lặp riêng để duyệt phần tử của
các kiểu tập hợp như mảng: vòng lặp foreach.
Hãy cùng xem xét qua một ví dụ (bạn có thể thực hiện trên C# Interactive):
> int[] integers = new int[] { 2, 4, 6, 8, 10 };
. foreach(var i in integers)
. {
. Console.Write($"{i}\t");
. }
2 4 6 8 10
>

Cấu trúc lặp này chỉ áp dụng được với các kiểu dữ liệu tập hợp. Nói chính
xác hơn, foreach chỉ áp dụng được với các class thực thi giao diện
IEnumerable hoặc IEnumerable<T>.
Cú pháp chung của vòng lặp foreach như sau:
foreach(data_type var_name in collection_variable)
{
// statements to be executed
}
83
Vòng lặp foreach tự động duyệt qua các phần tử của mảng
collection_variable. Khi duyệt đến phần tử nào, giá trị của phần tử đó sẽ
được copy vào biến var_name. Bên trong vòng lặp, bạn trực tiếp sử dụng
luôn biến var_name thay cho dùng chỉ số và phép toán index.
Quay lại ví dụ ở trên, vòng lặp foreach sẽ tự động duyệt qua mảng integers.
Khi nó dừng ở phần tử nào, giá trị của phần tử đó sẽ được copy sang biến
tạm i. Trong thân vòng lặp, bạn sử dụng giá trị của phần tử đó thông qua
biến tạm i (ở đây chỉ đơn giản là in ra console).
Có một số lưu ý sau khi sử dụng foreach:
 foreach là cấu trúc duyệt mảng (và các collection) an toàn nhất và
đơn giản nhất trong C#. Nếu mảng không có phần tử nào, foreach
đơn giản là không thực hiện. Không cần phải kiểm tra chỉ số như các
vòng lặp khác.
 foreach duyệt phần tử của mảng luôn theo một hướng duy nhất từ
đầu đến cuối mảng, không thể theo chiều ngược lại.
 Do biến tạm var_name chỉ chứa bản sao chép của phần tử tập hợp,
nên bạn không thể thay đổi được giá trị của phần tử tập hợp khi tác
động vào biến var_name. Nói cách khác, foreach là vòng lặp chỉ đọc
để duyệt qua các phần tử của mảng.
Nếu có lệnh thay đổi giá trị của biến tạm var_name, bạn sẽ gặp lỗi. Xem ví
dụ sau:
> int[] integers = new int[] { 2, 4, 6, 8, 10 };
. foreach(var i in integers)
. {
. i *= 2;
. Console.Write($"{i}\t");
. }
(4,5): error CS1656: Cannot assign to 'i' because it is a 'foreach iteration variable'
>

Ở đây biểu thức gán i *= 2 là chỗ gây lỗi. Bạn không thể thay đổi giá trị
của biến tạm i, do nó là bản sao chép chỉ đọc của phần tử của mảng.

Mảng nhiều chiều


Mảng chúng ta xem xét ở phần trên có tên gọi là mảng một chiều. Chúng
ta hình dung nó như một danh sách (chuỗi) các phần tử. Mảng trong C# có
thể có nhiều hơn một chiều, gọi chung là mảng nhiều/đa chiều.

84
Ví dụ, chúng ta có thể có mảng hai chiều. Khi đó ta có thể hình dung nó
như một bảng (chữ nhật), trong đó mỗi ô là một phần tử. Với mảng ba
chiều, chúng ta hình dung nó như một khối hộp lớn, gồm nhiều hộp nhỏ
(giống như khối rubic). Mỗi hộp nhỏ là một phần tử.
C# hỗ trợ mảng có tối đa 32 chiều. Chắc rất ít khi phải dùng đến loại mảng
nhiều chiều như vậy! Các mảng lớn hơn 3 chiều thường rất khó hình dung.
Chúng ta xem xét cách làm việc với mảng đa chiều qua hai trường hợp cụ
thể: mảng hai chiều, mảng ba chiều. Mảng nhiều chiều hơn cũng tuân theo
quy tắc tương tự.
Mảng hai chiều
int[,] numbers = new int[3, 4]
{
{11, 12, 13, 14 },
{21, 22, 23, 24 },
{31, 32, 33, 34 }
};
Trong ví dụ trên int[,] là kiểu mảng hai chiều trong đó các phần tử thuộc
kiểu int, numbers là biến mảng, kích thước là 3 hàng x 4 cột. Có thể dễ
dàng nhận ra cú pháp khai báo mảng đa chiều: với mỗi một chiều bổ sung,
chúng ta thêm một dấu phẩy vào giữa cặp dấu [].
Lưu ý cách chúng ta khởi tạo giá trị. Có thể hình dung mảng hai chiều tổ
chức dữ liệu theo hàng và cột. Do đó, chúng ta khởi tạo mảng bằng cách:
1. Tạo ra các hàng, các phần tử của mỗi hàng đặt trong cặp {} và phân
tách bằng dấu phẩy như một mảng đơn chiều;
2. Ghép các hàng lại với nhau (phân tách bằng dấu phẩy) và đặt tiếp
trong cặp {}.
Như vậy, mảng 2 chiều là sự mở rộng của mảng 1 chiều trong mặt phẳng.
Dấu phẩy cũng có tác dụng ngăn cách các chỉ số khi truy cập vào phần tử
của mảng đa chiều. Mảng hai chiều phát sinh thêm một dấu phẩy để có thể
ghi hai chỉ số. Do mảng hai chiều hình dung như một bảng, nó sẽ bao gồm
hai chỉ số: chỉ số hàng (đứng trước dấu phẩy) và chỉ số cột (đứng sau dấu
phẩy).
numbers[0, 0] = 11; // truy xuất phần tử đầu tiên (hàng thứ nhất - chỉ số
0, cột thứ nhất - chỉ số 0)
var number34 = numbers[2, 3] = 34; // truy xuất phần tử hàng thứ 3 (chỉ số
2) và cột thứ 4 (chỉ số 3)
Khi làm việc với mảng nhiều chiều nên chú ý phân biệt kích thước của mảng với chỉ số phần
tử của mảng.

85
Mảng ba chiều
Tương tự, mảng ba chiều phải dùng hai dấu phẩy để có thể ghi 3 kích thước.
int[,,] numbers2 = new int[2, 3, 4]
{
{
{111, 112, 113, 114 },
{121, 122, 123, 124 },
{131, 132, 133, 134 }
},
{
{211, 212, 213, 214 },
{221, 222, 223, 224 },
{231, 232, 233, 234 }
},
};
Chúng ta có thể hình dung mảng 3 chiều chính là các mảng 2 chiều xếp lớp
chồng lên nhau (trong không gian). Hai chỉ số cuối chính là số lượng hàng
và cột của bảng 2 chiều. Chỉ số đầu tiên chính là số lượng lớp.
Việc truy xuất phần tử của mảng 3 chiều cũng theo quy tắc tương tự:
numbers2[0, 0, 0] = 111; // truy xuất phần tử đầu tiên
var number234 = numbers2[1, 2, 3]; // truy xuất phần tử cuối cùng (giá trị
234)
Theo quy tắc trên chúng ta hoàn toàn có thể khai báo và khởi tạo các mảng
có số chiều lớn hơn nữa.
Một ví dụ về sử dụng mảng đa chiều
Trong ví dụ dưới đây chúng ta sẽ tạo và in ra console bảng cửu chương sử
dụng mảng hai chiều.
using System;
namespace P02_MultiDimension
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Multiplication table";
// khai báo và khởi tạo mảng hai chiều chứa bảng cửu chương
var multiplications = new int[10, 10];
// gán giá trị cho các phần tử của bảng cửu chương
for (int r = 0; r < multiplications.GetLength(0); r++)
{
for (int c = 0; c < multiplications.GetLength(1); c++)
{
multiplications[r, c] = (r + 1) * (c + 1);
}
}
// in ra màn hình
for (int c = 0; c <= multiplications.GetLength(1); c++)
{
Console.ForegroundColor = ConsoleColor.Cyan;

86
if (c == 0) Console.Write("{0, 4}", "");
else
Console.Write("{0, 4}", c);
}
Console.WriteLine();
for (int r = 0; r < multiplications.GetLength(0); r++)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("{0, 4}", r + 1);
Console.ResetColor();
for (int c = 0; c < multiplications.GetLength(1); c++)
{
Console.Write("{0, 4}", multiplications[r, c]);
}
Console.WriteLine();
}
Console.ReadKey();
}
}
}

Kết quả chạy chương trình


Trong ví dụ trên lưu ý phương thức GetLength. Phương thức này cho phép
lấy thông tin về số lượng phần tử trong từng chiều của mảng. Nếu mảng
một chiều thì giá trị tham số luôn là 0. Nếu là mảng hai chiều thì giá trị
tham số có thể là 1 (số hàng) hoặc 0 (số cột).

Mảng răng cưa (Jagged array)


Mảng răng cưa (Jagged Array) là một loại mảng đặc biệt trong C# và cũng
thường được gọi là mảng của mảng. Có thể hình dung mảng răng cưa là
một mảng một chiều, trong đó mỗi phần tử của nó lại là một mảng, thay
vì là một dữ liệu cụ thể.

87
Mỗi mảng phần tử có thể có kích thước khác nhau nhưng bắt buộc phải có
chung kiểu cơ sở. Mảng phần tử thậm chí có thể không cần khởi tạo.
Nếu bạn vẫn chưa hình dung được sự khác biệt giữa mảng răng cưa và mảng hai chiều:
– Mảng hai chiều bạn hãy hình dung nó như một ma trận. Giả sử mảng 3×4, tức là có 3 hàng x
4 cột thì mỗi hàng đều phải có đủ 4 cột.
– Mảng răng cưa thì khác. Giả sử cũng có 3 hàng, nhưng hàng thứ nhất có thể có 5 cột, hàng
thứ hai chỉ có 2 cột, hàng thứ 3 lại có 4 cột. Rõ ràng nó bị lởm chởm chứ không tạo thành dạng
ma trận.
Hãy cùng xem ví dụ sau để hiểu cách sử dụng của mảng răng cưa.
int[][] numbers = new int[5][];
numbers[0] = new int[] { 1, 2, 3 };
numbers[2] = new int[] { 1, 2, 3, 4, 5 };
numbers[4] = new int[] { 1, 2 };
Trong ví dụ trên chúng ta đã khai báo và khởi tạo biến numbers là một
mảng có thể chứa đến 5 mảng thành viên. Mỗi thành viên này là một mảng
một chiều và được khởi tạo riêng rẽ theo cú pháp chúng ta đã biết.
Truy xuất phần tử của mảng răng cưa có chút khác biệt so với mảng thông
thường:
var value1 = numbers[0][0]; // truy xuất giá trị đầu tiên của mảng, thành
viên đầu tiên
var value2 = numbers[2][4]; // truy xuất giá trị cuối cùng của mảng, thành
viên số 2
Chúng ta có thể nhận thấy, việc truy xuất giá trị của mảng răng cưa bao
gồm hai thông tin: chỉ số của mảng thành viên trong cặp [] thứ nhất; chỉ
số của phần tử trong mảng thành viên trong cặp [] thứ hai.
Cùng xem một ví dụ khác.
int[][,] numbers = new int[10][,];
numbers[0] = new int[2, 3];
numbers[1] = new int[3, 4];
numbers[2] = new int[,] { { 1, 3, 5 }, { 2, 4, 6 } };
Ở đây chúng ta lại khai báo mảng numbers là một mảng của các mảng hai
chiều. Nghĩa là, mỗi phần tử của nó là một mảng hai chiều mà chúng ta
phải tự khởi tạo trước khi dùng.
Việc truy xuất phần tử của mảng này cũng theo quy tắc ở trên: chọn chỉ số
của mảng thành viên, chọn chỉ số của phần tử cụ thể trong mảng thành
viên.
var value = numbers[2][2, 2]; // truy xuất phần tử ở vị trí 2,2 của mảng
thành viên số 2

88
Kiểu chuỗi ký tự (string) trong C#, lớp String và
StringBuilder
Chuỗi ký tự (string) là kiểu dữ liệu phổ biến hàng đầu trong C# và các ngôn
ngữ lập trình. Trong hầu hết các bài học từ đầu đến giờ bạn đều đụng chạm
đến chuỗi ký tự. Trong bài học về kiểu dữ liệu cơ sở trong C# bạn đã biết
sơ lược về kiểu string (System.String). Bài học này sẽ hướng dẫn chi tiết
cách sử dụng kiểu string trong lập trình C#, bao gồm các hàm xử lý chuỗi,
lớp StringBuilder để tạo chuỗi, định dạng chuỗi.

Kiểu string trong C# và lớp System.String của .NET


Như bạn đã biết, kiểu chuỗi ký tự trong C# – string – thực chất là một tên
ngắn gọn (alias) của lớp System.String của .NET. Alias string trong C#
là một từ khóa và giúp đơn giản hóa làm việc với chuỗi. Nếu có lệnh using
System; ở đầu tập tin code, bạn có thể sử dụng alias của
class System.String là string.
Đây cũng là câu trả lời cho vấn đề nhiều bạn thắc mắc: String và string có gì khác nhau.
Chả có gì khác cả, chúng nó là một. C# khuyên nên dùng string.
String (hoặc string) cho phép lưu trữ chuỗi ký tự Unicode và cung cấp một
số lượng kha khá các phương thức để xử lý chuỗi. C# cũng có một số phép
toán đặc biệt trên chuỗi.
Khởi tạo chuỗi
Trong phần về kiểu dữ liệu cơ sở của C# bạn đã biết cách khởi tạo chuỗi sử
dụng string literal: string str = "Hello world"; Đây là cách khởi tạo
chuỗi “tự nhiên” nhất. Tuy nhiên, C# cung cấp một số cách khác để khởi
tạo chuỗi.
Phương thức khởi tạo của lớp String có 8 biến thể (overload) khác nhau giúp
bạn tạo ra chuỗi theo ý muốn.
>//Khởi tạo string từ mảng ký tự
> char[] letters = { 'H', 'e', 'l', 'l', 'o' };
. string greetings = new string(letters);
> greetings
"Hello"
>//Khởi tạo string từ một phần của mảng ký tự
> char[] letters = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
> letters
char[11] { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd' }

89
> string greeting = new string(letters, 0, 5);
> greeting
"Hello"
>//Khởi tạo string bằng cách lặp ký tự
> string aaa = new string('a', 10);
> aaa
"aaaaaaaaaa"
>//Khởi tạo chuỗi rỗng
> string empty = string.Empty;
> empty
""

Xin nhắc lại một số vấn đề về chuỗi ký tự trong C#.


Trong chuỗi không được có mặt ký tự \ (backslash). Lý do là ký tự này được
sử dụng trong escape sequence. Ví dụ, dưới đây là một chuỗi sai (bị báo lỗi
cú pháp):
string path = "C:\Programs\Visual Studio"; // chuỗi này bị lỗi vì chứa ký
tự \.
Nếu muốn viết ký tự \ vào chuỗi, bạn phải viết nó hai lần:
string path = "C:\\Program\\Visual Studio"; // chuỗi này OK
hoặc thêm ký tự @ vào đầu chuỗi. Ký tự @ sẽ tắt chế độ diễn giải escape
sequence.
string path = @"C:\Program\Visual Studio"; // chuỗi này OK vì ký tự @ sẽ
tắt chế độ nhận diện escape sequence

Các phép toán trên kiểu chuỗi


Phép cộng chuỗi
Phép toán + áp dụng trên chuỗi ký tự được gọi là phép cộng chuỗi hay phép
ghép chuỗi. Nó cho phép ghép nhiều chuỗi ký tự nhỏ thành một chuỗi ký tự
lớn. Ví dụ:
> string str1 = "Hello " + "world";
> str1
"Hello world"
> string hello = "Hello", world = "world";
. string str2 = hello + " " + world;
> str2
"Hello world"
> string str3 = "Hello";
. str3 += " ";
. str3 += "world";

90
> str3
"Hello world"

Trong 3 ví dụ trên, ví dụ đầu tiên minh họa phép cộng 2 chuỗi, ví dụ thứ
hai là cộng nhiều chuỗi và ví dụ thứ ba là sử dụng phép cộng gán trên chuỗi.
Riêng ví dụ thứ 3 cần lưu ý: phép cộng gán này không hề thay đổi giá trị
gốc của str3. Trên thực tế, mỗi lần thực hiện phép toán này, một chuỗi mới
được tạo ra và gán trở lại cho str3. Cái này liên quan đến một đặc điểm
quan trọng của string: đây là kiểu dữ liệu immutable. Mọi biến đổi trên
string đều tạo ra một object mới chứ không thay đổi giá trị của object cũ.
Do vậy, việc thực hiện quá nhiều phép biến đổi tương tự trên chuỗi rất
không hiệu quả. C# cung cấp class StringBuilder để giải quyết vấn đề này.
Chúng ta sẽ xem xét StringBuilder ở phần sau của bài học này.
Truy xuất ký tự trong chuỗi
Chuỗi ký tự trong C# có thể hình dung giống như một mảng của các ký tự.
Do đó bạn cũng có thể sử dụng phép toán indexer để truy xuất từng ký tự
trong chuỗi tương tự như truy xuất phần tử của mảng.
> var str4 = "Hello world";
> str4[0]
'H'
> str4[1]
'e'
>

Trong ví dụ trên bạn dễ dàng nhận thấy ‘H’ là phần tử số 0, ‘e’ là phần tử
số 1. Dùng phép toán indexer có thể truy xuất các ký tự này giống như truy
xuất một mảng của các ký tự.
Cần lưu ý rằng, phép toán indexer này cho kết quả thuộc loại read-only (chỉ
đọc). Nghĩa là bạn không thể thay đổi ký tự ở vị trí đó. Trong ví dụ sau
> str4[2] = 'L'
(1,1): error CS0200: Property or indexer 'string.this[int]' cannot be assigned to --
it is read only
>

Nếu bạn cố tình gán giá trị ‘L’ cho ký tự ở vị trí số 2 như trên thì sẽ phát
sinh lỗi ngay lập tức.

Property và method của lớp string


Lớp string cung cấp một số property (thuộc tính) và method (phương thức)
giúp làm việc với chuỗi thuận tiện hơn.

91
Thuộc tính Length
Thuộc tính Length trả về số ký tự trong chuỗi. Đây là property duy nhất
của lớp string.
> string greeting = "Hello world";
> greeting.Length
11

Đây là một property chỉ đọc. Bạn không thể gán giá trị cho property này.
Instance method của lớp string
Dưới đây là một số instance method thông dụng của lớp string.
Instance method là các phương thức gọi từ object, phân biệt với static method là phương thức
gọi trực tiếp từ tên class (không gọi từ object). Bạn sẽ tìm hiểu về hai loại method này sau.
bool Contains(string value): kiểm tra xem chuỗi có chứa chuỗi con value
hay không
> string greeting = "Hello world";
> greeting.Contains("world")
true

bool EndsWith(string value): kiểm tra xem chuỗi con value có nằm ở
cuối chuỗi hay không
> string greeting = "Hello world";
> greeting.EndsWith("ld")
true

StartsWith hoạt động tương tự nhưng kiểm tra xem chuỗi con value có
nằm ở đầu chuỗi hay không.
bool Equals(string value): so sánh với chuỗi value
> string greeting = "Hello world";
> string hello = "Hello";
> greeting.Equals(hello)
false
>

string ToLower(): tạo bản sao của chuỗi nhưng mọi chữ cái hoa chuyển
thành chữ cái thường
> string greeting = "Hello world";
> string lower = greeting.ToLower();
> lower
"hello world"
92
string ToUpper(): tạo bản sao của chuỗi nhưng mọi chữ cái thường chuyển
thành chữ cái hoa
> string greeting = "Hello world";
> string upper = greeting.ToUpper();
> upper
"HELLO WORLD"

string Trim(): tạo bản sao của chuỗi nhưng cắt bỏ hết khoảng trắng ở đầu
và cuối. Khoảng trắng là các ký tự như space, tab, \r, \n
> string greeting = " Hello world\r\n ";
> string trim = greeting.Trim();
> trim
"Hello world"

Tương tự, TrimEnd chỉ cắt bỏ khoảng trắng ở cuối chuối, TrimStart chỉ cắt
bỏ khoảng trắng ở đầu.
Ngoài ra Trim, TrimEnd, TrimStart còn có thể cắt bỏ những ký tự khác theo
yêu cầu:
> //Cắt bỏ hết các chữ H và d ở đầu và cuối chuỗi
> string greeting = "Hello world";
> string sub = greeting.Trim(new[] { 'H', 'd' });
> sub
"ello worl"
>

int IndexOf(string value): Xác định vị trí của xâu con value. Nếu xâu con
value xuất hiện nhiều lần, IndexOf trả lại vị trí đầu tiên bắt gặp.
> string greeting = "Hello world";
> int pos = greeting.IndexOf("wor");
> pos
6

Tương tự LastIndexOf cũng dùng để xác định vị trí của chuỗi con. Tuy
nhiên, nếu chuỗi con xuất hiện nhiều lần, LastIndexOf sẽ trả về vị trí bắt
gặp cuối cùng. Nếu chuỗi con chỉ xuất hiện một lần, IndexOf và LastIndexOf
trả về cùng một kết quả.
Ngoài ra, IndexOf và LastIndexOf cũng có thể tìm kiếm vị trí của một ký tự
trong chuỗi, có thể yêu cầu tìm kiếm từ một vị trí xác định (thay vì tìm từ
đầu chuỗi).
int IndexOfAny(char[] anyOf): xác định vị trí bắt gặp đầu tiên của một
ký tự trong nhóm
93
> string hello = "Hello world";
> int pos = hello.IndexOfAny(new[] { 'l', 'o' });
> pos
2
> // 2 là số thứ tự của ký tự 'l' đầu tiên gặp trong xâu "Hello world"

Tương tự, IndexOfAny cũng cho phép giới hạn vị trí bắt đầu tìm kiếm, thay
vì tìm từ đầu chuỗi.
string Insert(int startIndex, string value): tạo một bản sao của chuối
với một chuỗi con value chèn vào vị trí startIndex
> string hello = "Hello world";
> var insert = hello.Insert(6, "there ");
> insert
"Hello there world"
> //"there " được chèn vào vị trí số 6 của xâu "Hello world"

string Remove(int startIndex, int count): tạo ra bản sao của chuỗi
nhưng bỏ đi count ký tự từ vị trí startIndex
> string hello = "Hello world";
> string remove = hello.Remove(5, 6);
> remove
"Hello"
> //cắt bỏ 6 ký tự từ vị trí số 5

Nếu bỏ tham số count thì sẽ xóa đi tất cả các ký tự từ vị trí startIndex.


string Replace(string oldValue, string newValue): tạo bản sao của
chuỗi trong đó thay thế một chuỗi con bằng chuỗi con khác
> string replaced = "Hello world".Replace("world", "baby!");
> replaced
"Hello baby!"
>

Overload string Replace(char oldChar, char newChar) hoạt động tương


tự nhưng thay thế ký tự này bằng ký tự khác.
string[] Split(params char[] separator): cắt chuỗi thành nhiều chuỗi
con sử dụng một nhóm ký tự đánh dấu
> // cắt chuỗi thành các chuỗi con sử dụng dấu cách làm ký tự đánh dấu
> string[] result = "Hello world from C#".Split(new[] { ' ' });
> result
string[4] { "Hello", "world", "from", "C#" }

94
> // kết quả thu được là 4 từ riêng rẽ

Tương tự cũng có thể sử dụng một nhóm chuỗi đánh dấu thay cho ký tự,
chỉ định số lượng chuỗi con tối đa.
char[] ToCharArray(): chuyển đổi toàn bộ chuỗi thành mảng char[]
char[] ToCharArray(int startIndex, int length): chuyển đổi một phần
chuỗi (length ký tự, tính từ vị trí startIndex) thành mảng char[]
void CopyTo(int sourceIndex, char[] destination, int
destinationIndex, int count): Sao chép một số ký tự sang một mảng
char[]
Static method của lớp string
Dưới đây là danh sách các phương thức static – phương thức gọi trực tiếp
từ class string, không gọi từ object.
static int Compare(string strA, string strB): so sánh hai chuỗi strA và
strB. Nếu strA bằng strB trả về 0; strA > strB – trả về 1; strA < strB – trả
về -1.
> string.Compare("Hello", "Hello")
0
> string.Compare("Hello", "hello")
1
> string.Compare("hello", "Hello")
-1
> string.Compare("hello world", "Hello")
1
>

Để dễ hình dung, việc so sánh này giống như sắp xếp tên theo thứ tự abc.
Kết quả +1 biểu thị chuỗi thứ nhất sẽ đứng sau chuỗi thứ hai; Kết quả -1
biểu thị chuỗi thứ nhất sẽ đứng trước chuỗi thứ hai.
static bool Equals(string a, string b): xác định xem chuỗi a và b có giá
trị bằng nhau hay không
> string.Equals("Hello", "Hello")
true
> var str1 = "Hello";
> var str2 = "Hello";
> string.Equals(str1, str2)
true
>

95
Cùng là so sánh chuỗi nhưng Equals chỉ xác định “bằng” hay “khác”, không
giống như Compare ở bên trên.
static string Concat(params string[] values): ghép nối nhiều chuỗi con
thành một chuỗi lớn. Số lượng chuỗi con không giới hạn.
> string greeting = string.Concat("Hello ", "world ", "from ", "C#");
> greeting
"Hello world from C#"
>

Concat cũng cho phép ghép các object thành chuỗi. Khi ghép object, phương
thức ToString() của object đó sẽ được gọi tự động để chuyển đổi nó thành
chuỗi.
Ngoài ra, nếu bạn có một mảng (kiểu phần tử bất kỳ), bạn cũng có thể dùng
Concat để ghép chúng thành chuỗi.
static string Join(string separator, params string[] value): hoạt động
giống Concat nhưng tự động chèn thêm chuỗi separator vào giữa các chuỗi
con.
> var greeting = string.Join("|", "Hello", "world", "from", "C#");
> greeting
"Hello|world|from|C#"
>

static string Copy(string str): tạo bản sao của một chuỗi.
> var hello = "Hello";
> var world = string.Copy(hello);
> world
"Hello"
>

Bạn cần lưu ý rằng, Copy tạo ra một object bản sao, nghĩa là hai biến chuỗi
trỏ vào hai object khác biệt (chỉ là có giá trị bằng nhau). Nó khác biệt với
phép gán chuỗi, khi biến mới cùng trỏ vào object ban đầu.
static bool IsNullOrEmpty(string value): kiểm tra xem chuỗi value là
null hoặc là một chuỗi rỗng
> string str = string.Empty;
> string str2;
> string.IsNullOrEmpty(str)
true
> string.IsNullOrEmpty(str2)
true
96
> string str3 = "Hello world";
> string.IsNullOrEmpty(str3)
false
>

static string Format(string format, Object arg0): giúp tạo ra chuỗi


“động” từ giá trị của các biến. Cách sử dụng của Format rất giống phương
thức Write/WriteLine với placeholder mà bạn đã biết trong bài học
về Console trong C#.
> var x1 = 1.234;
> var x2 = 5.678;
> var str = string.Format("Nghiệm của phương trình là: {0} và {1}", x1, x2);
> str
"Nghiệm của phương trình là: 1.234 và 5.678"
>

Trong ví dụ trên, vị trí đánh dấu (placeholder) được biểu diễn bằng cụm {0}
và {1}. Trong đó {0} sẽ được thay thế bởi biến thứ nhất trong danh sách
(tức là x1); {1} sẽ được thay thế bởi biến thứ hai trong danh sách (tức là
x2).
Như vậy có thể để ý, các biến trong danh sách được đánh số thứ tự từ 0, và
sẽ được thay thế vào vị trí đánh dấu có chỉ số tương ứng.
Write/WriteLine với placeholder chỉ là một cách làm tắt. Trên thực tế Write/WriteLine đều tự
động gọi tới phương thức Format để định dạng chuỗi trước khi in ra console.
Chúng ta sẽ xem xét chi tiết cách dùng Format trong phần định dạng chuỗi.

Định dạng chuỗi trong C#, phương thức string.Format()


Format() là phương thức rất mạnh giúp bạn tạo chuỗi sử dụng giá trị từ các
biến và định dạng cụ thể cho giá trị. Để định dạng cho giá trị trong chuỗi,
bạn cần biết một số “cú pháp” đặc biệt – chuỗi định dạng. Chuỗi định dạng
là một nhóm ký tự viết theo những quy tắc nhất định để phương thức
Format() hiểu được ý định của bạn.
Chúng ta cùng xem xét một số trường hợp phổ biến.
Định dạng cho số
Trong nhiều tình huống bạn cần định dạng số khi tạo chuỗi. Ví dụ trong
chuỗi chứa giá tiền, bạn muốn kèm theo đơn vị tiền tệ và chỉ với 2 chữ số
phần thập phân. Bạn cũng có thể muốn dành một độ rộng cố định để sau
in số ra console (như trong bảng biểu). Bạn muốn số in ra căn lề trái hoặc
căn lề phải.

97
Định dạng cho mỗi số bao gồm 3 phần: alignment, format và precision.
Format specifier
Ví dụ, để định dạng giá tiền, bạn dùng :C hoặc :c phía sau chỉ số trong
placeholder, hoặc sau tên biến trong interpolated string. Đơn vị tiền tệ sẽ
phụ thuộc vào cấu hình ngôn ngữ của Windows.
> string.Format("The value: {0}", 500)
The value: 500
> string.Format("The value: {0:C}", 500)
The value: $500.00
> string.Format ("The value: {0:c}", 500)
The value: $500.00
> decimal value = 500;

C hoặc c là định dạng số chuẩn dành cho kiểu tiền tệ (currency). Ngoài ra,
bạn có thể gặp thêm các trường hợp khác: D, d – Decimal; F, f – Fixed
point; G, g – General; X, x – Hexadecimal; N, n – Number; P, p – Percent;
R, r – Round-trip; E, e – Scientific.
Các ký tự định dạng số chuẩn này phải đi ngay dấu hai chấm, ví dụ :C, :c,
:P, :p như bạn đã thấy ở trên.
Precision specifier
Sau ký tự định dạng bạn có thể sử dụng thêm một con số để mô tả độ chính
xác (Precision specifier) của giá trị được in ra.
Ví dụ, mặc định :C sẽ in ra hai chữ số thập phân. Bạn có thể yêu cầu in ra
con số với độ chính xác cao hơn (3-4 chữ số thập phân) hoặc thấp hơn.
Chẳng hạn :c3 chỉ định in ra 3 chữ số thập phân, :c4 chỉ định in 4 chữ số
thập phân.
> string.Format("The value: {0:c3}", 500);
The value: $500.000
> string.Format ("The value: {0:c4}", 500);
The value: $500.0000

Cách thể hiện của “độ chính xác” phụ thuộc vào loại số và định dạng của
nó. Bạn hãy xem ví dụ sau đây:
> string.Format("{0 :C}", 12.5);
$12.50
> string.Format("{0 :D4}", 12);
0012
> string.Format("{0 :F4}", 12.3456789);

98
12.3457
> string.Format("{0 :G4}", 12.3456789);
12.35
> string.Format("{0 :x}", 180026);
2bf3a
> string.Format("{0 :N2}", 12345678.54321);
12,345,678.54
> string.Format("{0 :P2}", 0.1221897);
12.22%
> string.Format("{0 :e4}", 12.3456789);
1.2346e+001
>

Alignment specifier
Để dễ hiểu, hãy xem ví dụ sau:
> int myInt = 500;
> string.Format("|{0, 10}|", myInt);
. string.Format("|{0,-10}|", myInt);
| 500|
|500 |
>

Ở đây chúng ta mô phỏng lại việc in giá trị ra thành cột. Con số 10 và -10
viết tách với chỉ số placeholder bằng dấu phẩy được gọi là alignment
specifier. Alignement specifier chỉ định độ rộng (số lượng ký tự) tối thiểu
để in giá trị số đó. Giá trị dương báo hiệu căn lề phải; giá trị âm báo hiệu
căn lề trái.
Format specifier mà bạn đã biết viết về phía phải của alignment specifier
như dưới đây:
> double myDouble = 12.345678;
> string.Format("{0,-10:G} -- General", myDouble)
"12.345678 -- General"
> string.Format("{0,-10} -- Default, same as General", myDouble)
"12.345678 -- Default, same as General"
> string.Format("{0,-10:F4} -- Fixed Point, 4 dec places", myDouble)
"12.3457 -- Fixed Point, 4 dec places"
> string.Format("{0,-10:E3} -- Sci. Notation, 3 dec places", myDouble)
"1.235E+001 -- Sci. Notation, 3 dec places"
> string.Format("{0,-10:x} -- Hexadecimal integer", 1194719)
"123adf -- Hexadecimal integer"

99
Định dạng thời gian
C# cung cấp kiểu dữ liệu DateTime để lưu trữ thông tin về thời gian.
Tương tự như đối với số, bạn cũng có thể định dạng thời gian khi tạo chuỗi
với Format(). Quy tắc viết định dạng cho thời gian giống hệt như đối với số.
Khác biệt là bạn cần sử dụng format specifier riêng cho thời gian:
 d – chỉ in thông tin về ngày ở dạng ngắn gọn;
 D – chỉ in thông tin về ngày ở dạng đầy đủ;
 t – chỉ in thông tin về thời gian ở dạng ngắn gọn;
 T – chỉ in thông tin về thời gian ở dạng đầy đủ.
Ví dụ:
> var day = new DateTime(2025, 2, 14); // tạo object chứa ngày 14 tháng 2 năm 2025
> string.Format("A future day: {0}", day)
"A future day: 2/14/2025 12:00:00 AM"
> string.Format("A future day: {0:d}", day)
"A future day: 2/14/2025"
> string.Format("A future day: {0:D}", day)
"A future day: Friday, February 14, 2025"
> string.Format("A future day: {0,-20:d}", day)
"A future day: 2/14/2025 "
> string.Format("A future day: {0,20:d}", day)
"A future day: 2/14/2025"
> string.Format("A future time: {0,20:t}", day)
"A future time: 12:00 AM"
> string.Format("A future time: {0,20:T}", day)
"A future time: 12:00:00 AM"
>

Cách hiển thị cụ thể phụ thuộc vào thiết lập vùng và ngôn ngữ của hệ điều
hành Windows bạn đang dùng.
Bạn cũng có thể tự đưa ra định dạng ngày tháng riêng độc lập khỏi hệ điều
hành như sau:
> string.Format("Một ngày nào đó trong tương lai: {0:dd-MM-yyyy}", day)
"Một ngày nào đó trong tương lai: 14-02-2025"
> string.Format("Một ngày nào đó trong tương lai: {0:dd-MMM-yyyy}", day)
"Một ngày nào đó trong tương lai: 14-Feb-2025"
> string.Format("Một ngày nào đó trong tương lai: {0, 25:dd/MM/yyyy}", day)
"Một ngày nào đó trong tương lai: 14/02/2025"

100
Trong loại định dạng tự do này bạn có thể dùng các ký tự d thay cho ngày,
M thay cho tháng, y thay cho năm. Số lượng ký tự có ý nghĩa riêng. Ví dụ:
> string.Format("Một ngày nào đó trong tương lai: {0, 25:dd/MMMM/yyyy}", day)
"Một ngày nào đó trong tương lai: 14/February/2025"
> string.Format("Một ngày nào đó trong tương lai: {0, 25:d/MMMM/yyyy}", day)
"Một ngày nào đó trong tương lai: 14/February/2025"
> string.Format("Một ngày nào đó trong tương lai: {0, 25:ddd/MMMM/yyyy}", day)
"Một ngày nào đó trong tương lai: Fri/February/2025"
> string.Format("Một ngày nào đó trong tương lai: {0, 25:ddd/M/yyyy}", day)
"Một ngày nào đó trong tương lai: Fri/2/2025"

String interpolation
String interpolation là một khả năng tạo chuỗi động từ giá trị của biến mới
đưa vào trong C# 6. String interpolation có lối viết giản dị và dễ đọc hơn
nhiều so với Format().
> string s1 = "World";
> string s2 = $"Hello, {s1}";
> s2
"Hello, World"

String interpolation phải bắt đầu bằng ký tự $, theo sau là string literal
thông thường. Trong chuỗi ký tự, ở đâu cần thay bằng giá trị của biến/biểu
thức thì đặt tên biến/biểu thức trong cặp dấu ngoặc nhọn {}.
Như trong ví dụ trên bạn đã thấy, cách viết này rất dễ đọc và dễ hiểu.
Trên thực tế, string interpolation cũng chỉ là một dạng cú pháp tắt của Format(). Nếu gặp ký tự
$ trước xâu, compiler sẽ tự động gọi đến phương thức Format.
Ví dụ trên sẽ chuyển thành string.Format(“Hello, {0}”, s1);
Đây cũng là lý do chúng ta xem xét rất chi tiết phương thức Format() ở trên.
Bên trong placeholder {} bạn có thể đặt bất kỳ biến, biểu thức, lời gọi
phương thức nào, miễn là nó trả về giá trị.
> string world = "world";
> string greeting = $"Hello, {world.ToUpper()}";
> greeting
"Hello, WORLD"

Trong chuỗi interpolation, nếu bạn muốn viết ký tự { thì bạn phải viết nó
hai lần:
> string s = "Hello";
> string s2 = $"{{s}} displays the value of s: {s}";

101
> s2
"{s} displays the value of s: Hello"

Bạn thậm chí có thể viết các biểu thức phức tạp bên trong chuỗi
interpolation:
> int a = 10, b = 20;
> string str = $"Giá trị lớn hơn là: {(a > b ? a : b)}";
> str
"Giá trị lớn hơn là: 20"

Khi này lưu ý đặt biểu thức trong cặp dấu ngoặc tròn ().
String interpolation sử dụng cách định dạng số và ngày tháng giống hệt như
của phương thức Format():
> decimal d = 500;
> string str = $"Giá tiền là: {d,10:c2}";
> str
"Giá tiền là: $500.00"

102
Kiểu dữ liệu liệt kê (Enumeration) trong C#, từ khóa
enum
Enumeration (hay kiểu dữ liệu liệt kê, thường gọi tắt là enum) trong C# là
một kiểu dữ liệu đặc biệt. Cái đặc biệt nằm ở chỗ nó là kiểu dữ liệu do người
dùng tự định nghĩa, thay vì được định nghĩa sẵn như các kiểu dữ liệu cơ sở.
Bài học này sẽ cung cấp cho bạn khái niệm, cách định nghĩa một kiểu enum
riêng, và cách sử dụng kiểu enum trong C#.

enum là gì trong C#?


Kiểu định nghĩa sẵn và kiểu do người dùng định nghĩa
Trong tất cả các bài học trước đây bạn đều sử dụng các kiểu dữ liệu cơ bản
C# định nghĩa sẵn như int, bool, string. Nó có nghĩa là bạn chỉ cần khai
báo và khởi tạo biến thuộc kiểu dữ liệu đó để sử dụng.
Sử dụng các kiểu đã được định nghĩa sẵn dĩ nhiên là tiện lợi hơn nhưng đồng
thời bạn cũng bị giới hạn. Các kiểu cơ sở định nghĩa sẵn đó không phải lúc
nào cũng đáp ứng được yêu cầu của bạn.
Do đó, các ngôn ngữ lập trình đều cung cấp cho người lập trình khả năng tự
định nghĩa kiểu dữ liệu của riêng mình.
Trong C# bạn có thể tự định nghĩa các kiểu dữ liệu riêng thuộc về một trong
các nhóm sau: enumeration, structure, class, interface, delegate. Tất cả các
kiểu định nghĩa sẵn của C# cũng rơi vào một trong các nhóm này.
Trong các kiểu dữ liệu cơ sở bạn đã biết thì các kiểu số (byte, int, long, double,...),
bool, char thuộc về nhóm structure; string và object thuộc về nhóm class.
Như vậy về bản chất, kiểu định nghĩa sẵn và kiểu tự định nghĩa không có gì
khác biệt nhau. Chẳng qua một bên do đội ngũ phát triển C# làm ra, một
bên do bạn tự làm. Sau khi định nghĩa kiểu, bạn có thể sử dụng nó như bất
kỳ kiểu dữ liệu có sẵn nào.
Tuy nhiên, để định nghĩa kiểu, bạn phải học cú pháp định nghĩa (khai báo)
riêng của từng nhóm.
Bài học này sẽ giúp bạn làm quen với cách định nghĩa kiểu dữ liệu của riêng
mình thuộc về nhóm enumeration.
Khái niệm enum trong C#
Trước khi tìm hiểu về enumeration, hãy cùng xem ví dụ sau.
Tên các ngày trong tuần theo tiếng Anh (Sunday, Monday, Tuesday,
Wednesday, Thursday, Friday, Saturday) là các giá trị cố định, hữu hạn. Khi
103
cần lưu trữ và xử lý trong lập trình C#, bạn có thể sử dụng một biến thuộc
kiểu string như thế này:
string dayOfWeek = "Sunday";
dayOfWeek = "Monday";
Một giải pháp khác thường được lập trình viên C sử dụng là các “magic
constant”:
const int SUNDAY = 0, MONDAY = 1, TUESDAY = 2;
Những tình huống như trên dẫn tới nhu cầu xây dựng một kiểu dữ liệu giúp
lưu trữ một danh sách hữu hạn hằng số, sao cho tại mỗi thời điểm, biến
tương ứng chỉ có thể nhận một giá trị trong số đó.
C# cho phép định nghĩa kiểu dữ liệu để lưu trữ danh sách hằng số như vậy.
Nó được gọi là kiểu liệt kê (enumeration).
Kiểu liệt kê là loại kiểu dữ liệu do người dùng định nghĩa chứa một danh
sách (hữu hạn) hằng số. Mỗi hằng số được đặt một tên gọi tương ứng. Giá
trị của hằng một số nguyên. Mỗi biến thuộc kiểu liệt kê tại mỗi thời điểm
chỉ có thể nhận một giá trị trong danh sách.
Một số kiểu enum đã định nghĩa sẵn trong C#
C# đã định nghĩa sẵn một số kiểu liệt kê. Một vài trong số đó bạn đã từng
sử dụng.
ConsoleColor
Khi học làm việc với console trong C# bạn đã gặp các lệnh như:
// đổi màu chữ sang đỏ
Console.ForegroundColor = ConsoleColor.Red;
// đổi màu chữ sang xanh
Console.ForegroundColor = ConsoleColor.Green;
ConsoleColor ở trên là một kiểu liệt kê được C# định nghĩa sẵn với 16
hằng số là các tên màu mà chúng ta có thể sử dụng để quy định màu chữ
hoặc màu nền trong giao diện dòng lệnh. Các giá trị của ConsoleColor như
sau:

Tên Giá trị Ý nghĩa


Black 0 The color black.
Blue 9 The color blue.
Cyan 11 The color cyan (blue-green).
DarkBlue 1 The color dark blue.
DarkCyan 3 The color dark cyan (dark blue-green).

104
DarkGray 8 The color dark gray.
DarkGreen 2 The color dark green.
DarkMagenta 5 The color dark magenta (dark purplish-red).
DarkRed 4 The color dark red.
DarkYellow 6 The color dark yellow (ochre).
Gray 7 The color gray.
Green 10 The color green.
Magenta 13 The color magenta (purplish-red).
Red 12 The color red.
White 15 The color white.
Yellow 14 The color yellow.

ConsoleKey
Cũng trong bài học về console trong C# bạn gặp một enum khác là
ConsoleKey, chứa danh sách các phím chuẩn dùng được trên console. Dưới
đây là một số tên và giá trị của enum này.

Tên hằng Giá trị Ý nghĩa


A 65 The A key.
The Add key (the addition key on the numeric
Add 107
keypad).
Applications 93 The Application key (Microsoft Natural Keyboard).
Attention 246 The ATTN key.
B 66 The B key.
Backspace 8 The BACKSPACE key.
BrowserBack 166 The Browser Back key (Windows 2000 or later).
BrowserFavorites 171 The Browser Favorites key (Windows 2000 or later).

Làm việc với enum trong C#


Khi bạn đã hiểu được khái niệm enum, giờ là lúc tự tạo cho mình các enum
riêng.

105
Khai báo kiểu enum
Trong C# kiểu enumeration được định nghĩa với từ khóa enum:
enum DayOfWeek
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
Ví dụ trên khai báo kiểu dữ liệu thuộc nhóm enumeration có tên là
DayOfWeek với các giá trị (hằng) tương ứng là DayOfWeek.Sunday,
DayOfWeek.Monday,…
Về mặt ý nghĩa, DayOfWeek.Sunday hoàn toàn tương đương với việc sử
dụng “magic constant” SUNDAY = 0, nghĩa là DayOfWeek.Sunday thực chất
là một hằng số có giá trị 0. Tuy nhiên, sử dụng enum giúp quản lý code đơn
giản tiện lợi hơn.
Rất rõ ràng, enum trong C# chỉ là một tập hợp hữu hạn của các hằng số,
với mỗi hằng số có giá trị thuộc kiểu số nguyên.
Giá trị số của mỗi hằng mặc định bắt đầu từ 0 và tăng dần theo thứ tự của
hằng số trong danh sách. Nghĩa là, Sunday = 0, Monday = 1, Tuesday =
2,.... Nếu Sunday được gán giá trị khác 0, ví dụ Sunday = 1, thì Monday sẽ
nhận giá trị 2, Tuesday nhận giá trị 3,…
Mặc định, giá trị của hằng trong enum sẽ thuộc kiểu int. Tuy nhiên bạn có
thể thay đổi thành kiểu số nguyên khác. Ví dụ:
enum Color : byte
{
Red = 1,
Green = 2,
Blue = 3
}
Trong định nghĩa kiểu Color này, giá trị của mỗi hằng số giờ thuộc kiểu byte,
thay vì int. Và chúng ta cũng chủ động gán giá trị cho nó, thay vì để compiler
làm tự động.
Các enum khai báo như trên chỉ có thể sử dụng nội bộ trong phạm vi
project. Nếu bạn muốn tạo thư viện để người khác có thể sử dụng, enum
cần được khai báo với từ khóa điều khiển truy cập public:
public enum Color : byte
{
Red = 1,
Green = 2,
106
Blue = 3
}

Như bạn đã biết trong bài học về kiểu dữ liệu cơ sở, tất cả enum đều thuộc nhóm value type,
kể cả các enum bạn tự định nghĩa.
Khai báo và khởi tạo biến enum
Với enum DayOfWeek được định nghĩa như trên chúng ta có thể dễ
dàng khai báo và khởi tạo biến thuộc kiểu DayOfWeek:
DayOfWeek day1 = DayOfWeek.Sunday, day2 = DayOfWeek.Tuesday;
var day3 = DayOfWeek.Monday;
var weekend = DayOfWeek.Saturday;
Nếu dùng kiểu Color định nghĩa như ở trên thì bạn có thể khai báo biến như
sau:
var red = Color.Red;
var green = Color.Green;
var blue = Color.Blue;
Nó không khác gì với việc sử dụng các enum “xịn” của C# như ConsoleColor:
var red = ConsoleColor.Red;
var green = ConsoleColor.Green;
var blue = ConsoleColor.Blue;

Sử dụng biến enum


Để minh họa cách sử dụng biến enum, chúng ta cùng làm một project nhỏ.
Tạo một blank solution đặt tên là S05_Enum. Thêm project mới thuộc kiểu
ConsoleApp vào solution và đặt tên project là P01_EnumVar. Viết code cho
tập tin Program.cs như sau:
Ví dụ trên đã trình bày hầu hết các vấn đề cơ bản khi sử dụng enum trong
chương trình C#. Hãy tự code lại ví dụ để nắm chắc cách sử dụng enum.
Nếu gặp khó khăn, bạn có thể tải mã nguồn trong link ở cuối bài hoặc hỏi
trong comment.
namespace P01_EnumVar
{
using System;
using static System.Console;
class Program
{
static void Main(string[] args)
{
// In biến enum ra console
var gender = Gender.Male;
WriteLine($"My gender is {gender} ({(int)gender})");
var day = DayOfWeek.Tuesday;
WriteLine($"Today is {day} ({(int)day})");
var color = Color.Blue;
WriteLine($"My favorite color is {color} ({(int)color})");
var month = Month.Aug;

107
WriteLine($"My birth month is {month} ({(int)month})");
WriteLine("--------------------------");
// Chuyển đổi từ số sang enum
gender = (Gender)1;
WriteLine($"My gender is {gender} ({(int)gender})");
month = (Month)8;
WriteLine($"My birth month is {month} ({(int)month})");
WriteLine("--------------------------");
// Sử dụng các phương thức của lớp Enum
// GetNames trả về danh sách tên hằng
foreach (var d in Enum.GetNames(typeof(DayOfWeek)))
{
Write($"{d} ");
}
WriteLine();
// GetValues trả về danh sách hằng (bao gồm cả tên và giá trị)
foreach (var d in Enum.GetValues(typeof(DayOfWeek)))
{
Write($"{(int)d} ");
}
WriteLine();
WriteLine("--------------------------");
// Đọc giá trị số từ bàn phím và chuyển thành kiểu enum sử dụng lớp Enum
Write("What is your gender? ");
// đọc một số từ bàn phím (0, 1, 2) và chuyển thành kiểu Gender
gender = (Gender)Enum.Parse(typeof(Gender), ReadLine());
WriteLine($"Your gender is {gender}");
if (gender == Gender.Unknown)
WriteLine("Sorry!");
Write("What is your favorite color? ");
// đọc một số (1, 2 hoặc 3) và chuyển thành kiểu Color
color = (Color)Enum.Parse(typeof(Color), ReadLine());
WriteLine($"Your favorite color is {color}");
Write("What is your birth month? ");
// đọc một số (từ 1 đến 12) và chuyển thành kiểu Month
month = (Month)Enum.Parse(typeof(Month), ReadLine());
switch (month)
{
case Month.Feb:
case Month.Mar:
case Month.Apr:
WriteLine("You're born in Spring!");
break;
case Month.May:
case Month.Jun:
case Month.Jul:
WriteLine("You're born in Summer!");
break;
case Month.Aug:
case Month.Sep:
case Month.Oct:
WriteLine("You're born in Autumn!");
break;
case Month.Nov:
case Month.Dec:
case Month.Jan:
WriteLine("You're born in Winter!");
break;

108
}
ReadKey();
}
}
/// <summary>
/// Enum chứa danh sách giới tính
/// </summary>
enum Gender
{
Male, Female, Unknown
}
/// <summary>
/// Enum chứa danh sách ngày trong tuần
/// </summary>
enum DayOfWeek
{
Monday = 2, // hằng Monday có giá trị bằng 2
Tuesday = 3,
Wednesday = 4,
Thursday = 5,
Friday = 6,
Saturday = 7,
Sunday = 8
}
/// <summary>
/// Enum chứa 3 hằng số màu kiểu byte
/// </summary>
enum Color : byte
{
Red = 1,
Green = 2,
Blue = 3
}
/// <summary>
/// Enum chứa tên các tháng, giá trị bắt đầu từ 1
/// </summary>
enum Month
{
Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
}
}

Enum (chữ E viết hoa) là một class đặc biệt trong C#. Class này cung cấp một số phương thức
để làm việc với bất kỳ kiểu enum nào. Trong ví dụ trên bạn đã nhìn thấy và sử dụng các phương
thức tĩnh GetNames, GetValues, Parse.

109
Struct trong C#: khai báo, sử dụng, các thành viên
chính
Struct trong C# là một nhóm kiểu dữ liệu rất phổ biến. Các kiểu dữ liệu cơ
sở trong C# mà bạn đã biết như byte, int, long, float, double, char đều
thuộc nhóm struct. Tương tự như enum, C# cũng cho phép bạn tự định
nghĩa kiểu dữ liệu thuộc nhóm struct.
Trong bài học này bạn sẽ làm quen với việc định nghĩa và sử dụng kiểu dữ
liệu thuộc loại struct trong C#.

Struct trong C# là gì?


Khái niệm struct
Struct là nhóm kiểu dữ liệu trong đó mỗi kiểu dữ liệu có thể chứa các thành
viên khác. Nói cách khác, kiểu dữ liệu thuộc nhóm struct có khả năng chứa
một nhóm dữ liệu thuộc nhiều kiểu khác nhau và khả năng xử lý thông tin.
Các thành viên chính của struct bao gồm:
 Biến thành viên (member variable), hay còn gọi là trường dữ liệu
(data field): đây là thành phần chứa dữ liệu của struct.
 Phương thức thành viên (method): đây là thành phần xử lý thông tin.
 Thuộc tính (property): đây là thành phần chịu trách nhiệm xuất nhập
dữ liệu.
Ngoài ra, struct còn có thể chứa nhiều loại thành viên khác như hằng
(constant), bộ đánh chỉ mục (indexer), phép toán (operator), sự kiện
(event), kiểu con (nested type). Các loại thành viên này sẽ được xem xét
chi tiết khi học về class. Chúng được định nghĩa và sử dụng giống hệt nhau
trong class và struct.
Như đã nói nhiều lần, struct trong C# thuộc nhóm value type.
Một số struct cơ sở trong C#
Trong C#, các kiểu dữ liệu cơ sở như int, bool, char,… (trừ object và string)
thực tế đều là các struct (nhưng hơi đặc biệt một chút). Vì vậy các kiểu này
không đơn thuần chỉ chứa dữ liệu mà còn chứa cả các phương thức để thực
hiện những công việc nhất định.
Dưới đây là một số ví dụ về sử dụng các kiểu này theo kiểu khác.
> var i = new int();
> var j = new Int32();
> j

110
0
> i
0
> i.CompareTo(j)
0
> i.Equals(j)
true
> int.MaxValue
2147483647
> int.MinValue
-2147483648
> int.Parse("20")
20
> var c = new char();
> c
'\0'
> char.MaxValue
'\uffff'
> char.MinValue
'\0'

Dấu chấm đặt sau tên biến hoặc sau tên kiểu là một phép toán có tên gọi
là phép toán truy xuất thành viên (member access operator). Các
phương thức của int như CompareTo, Equals được gọi là instance
method (chỉ gọi được từ biến), trong khi Parse là một static method (gọi
từ tên kiểu). MaxValue và MinValue là các hằng thành viên, đồng thời
là thành viên static (truy xuất từ tên kiểu thay vì truy xuất từ biến).
Trong bài học về console trong C#, bạn cũng gặp một struct:
ConsoleKeyInfo. Đây là struct chứa kết quả thực hiện của ReadKey().

111
Trong struct này chứa 3 đặc tính (property): Key (kiểu enum ConsoleKey),
KeyChar (kiểu char), Modifier (kiểu enum ConsoleModifiers). Struct này
cũng chứa 4 phương thức (method) kế thừa từ object: Equals,
GetHashCode, GetType, ToString.
Như vậy việc sử dụng kiểu struct không hề xa lạ với chúng ta.

Khai báo kiểu struct trong C#


Khi bạn đã nắm qua được khái niệm struct trong C#, bây giờ chúng ta sẽ
chuyển sang tự tạo kiểu struct của riêng mình.
Để dễ hình dung, chúng ta cùng thực hiện một project nhỏ.
Tạo một blank solution đặt tên là S06_Struct và thêm một project
ConsoleApp đặt tên là P01_StructDefinition. Viết code cho Program.cs như
sau:
using System;
namespace P01_StructDefinition
{
using static Console;
/// <summary>
/// Struct biểu diễn số phức
/// </summary>
struct Complex
{
public double Real; // trường thực
public double Imaginary; // trường ảo
/// <summary>
/// Phương thức khởi tạo
/// </summary>
/// <param name="r">phần thực</param>
public Complex(double r)
{
Real = r;
Imaginary = 0;
}
/// <summary>
/// Phương thức khởi tạo
/// </summary>
/// <param name="r">phần thực</param>
/// <param name="i">phần ảo</param>
public Complex(double r, double i)
{
Real = r;
Imaginary = i;
}
/// <summary>
/// Chuyển chuỗi hợp lệ thành giá trị của Real và Imaginery
/// </summary>
/// <param name="value"></param>
public void Parse(string value)
{
var temp = value.Trim();
if (temp.EndsWith("i") || temp.EndsWith("I"))
{
112
temp = temp.TrimEnd('i', 'I');
var tokens = temp.Split(new[] { '+', '-' }, 2);
Real = double.Parse(tokens[0]);
Imaginary = double.Parse(tokens[1]);
}
else
{
Real = double.Parse(temp);
}
}
/// <summary>
/// Chuyển chuỗi hợp lệ thành giá trị của Real và Imaginery
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static Complex FromString(string value)
{
var temp = new Complex();
temp.Parse(value);
return temp;
}
/// <summary>
/// Đặc tính, trả về module của số phức
/// </summary>
public double Modulus => Math.Sqrt(Real * Real + Imaginary * Imaginary);
/// <summary>
/// Ghi đè phép toán +
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
}
/// <summary>
/// Ghi đè phép toán -
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Complex operator -(Complex a, Complex b)
{
return new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
}
/// <summary>
/// Ghi đè phương thức ToString() của object
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (Imaginary == 0)
{
return Real.ToString();
}
return $"{Real} {(Imaginary > 0 ? '+' : '-')} {Math.Abs(Imaginary)}i";
}
}

113
class Program
{
static void Main(string[] args)
{
Title = "Complex number";
// khai báo và khởi tạo biến a thuộc kiểu Complex
var a = new Complex(1, 2);
WriteLine($"a = {a}");
// sử dụng đặc tính Modulus của Complex
WriteLine($"|a| = {a.Modulus}");
// gọi phương thức Parse
a.Parse("10-2i");
WriteLine($"a = {a}");
// gọi phương thức tĩnh FromString
var b = Complex.FromString("5 + 3i");
WriteLine($"b = {b}");
// thực hiện phép cộng trên số phức
WriteLine($"a + b = { a + b}");
ReadKey();
}
}
}

Trong project này bạn đã định nghĩa kiểu số phức Complex thuộc nhóm
struct với các thành phần:
 Real, Imaginary: các trường dữ liệu (biến thành viên) của Complex.
Hai trường dữ liệu này được khai báo với từ khóa điều khiển truy
cập public. Từ khóa này cho phép code bên ngoài struct sử dụng
được biến thành viên.
 Phương thức Parse: có tác dụng chuyển chuỗi ký tự về giá trị của
trường Real và Imaginary. Phương thức này thuộc loại instance
method, nghĩa là chỉ gọi từ object của Complex.

114
 Hai phương thức Complex (trùng tên với struct) không có tên kiểu trả
về là hai phương thức khởi tạo (constructor) của struct. Phương
thức khởi tạo được sử dụng với phép toán new để khởi tạo object của
Complex.
 Phương thức tĩnh (static method) FromString: có cùng tác dụng như
Parse nhưng gọi từ tên struct thay vì gọi từ object của struct.
 Phương thức ToString: ghi đè phương thức ToString của lớp tổ tông
Object, có tác dụng chuyển object của Complex thành chuỗi ký tự.
Phương thức này tự động được gọi khi in ra console bằng lệnh
Write/WriteLine hoặc trong phương thức Format của lớp string.
 Operator +, Operator - : ghi đè phép toán + và – cho object của
Complex. Nhờ hai phép toán ghi đè này bạn có thể viết biểu thức chứa
phép toán + và – tương tự như với các kiểu số thông thường.
 Modulus: là một thuộc tính (property) của Complex, trả về giá trị
module của số phức. Đây là thuộc tính chỉ đọc.
Cú pháp khai báo struct trong C#
C# cho phép khai báo kiểu dữ liệu mới thuộc nhóm struct sử dụng từ khóa
struct theo cú pháp sau:
[access_modifier] struct <type_name>
{
// Khai báo các thành viên
}
Access modifier là thành phần không bắt buộc và là một trong các từ khóa
public hoặc internal có tác dụng điều chỉnh phạm vi sử dụng của kiểu. Giá
trị mặc định là internal. Nếu dùng internal, kiểu chỉ có thể sử dụng nội bộ
trong phạm vi project. Nếu để public, kiểu có thể được sử dụng bởi project
khác. Nếu bạn xây dựng thư viện kiểu cho người khác sử dụng thì cần đặt
từ khóa public. Nếu không chỉ rõ từ khóa, C# sẽ coi là internal.
struct là từ khóa bắt buộc dùng để chỉ định rằng kiểu đang khai báo thuộc
nhóm struct.
Type_name (bắt buộc) là tên của kiểu đang được định nghĩa. Tên của kiểu
cũng phải tuân thủ quy tắc đặt định danh của C#. Ngoài ra, tên kiểu nên
tuân thủ quy ước viết PascalCase. Trong đó, ký tự đầu tiên của định danh
luôn là chữ cái in hoa. Nếu định danh bao gồm nhiều từ ghép lại thì chữ cái
đầu của mỗi từ cũng được viết hoa.
Như trong ví dụ trên:
struct Complex

115
{
// thân struct
}
Complex là tên struct, access modifier là internal (vì không viết gì)
Visual Studio cho phép thiết lập các quy ước này trong code style (Tools => Options => Text
Editor => C# => Code Style). Nếu vi phạm quy ước, Visual Studio sẽ biểu thị chỗ lỗi bằng cách
gạch chân.
Toàn bộ phần thân của khai báo kiểu struct phải là một khối code (đặt trong
cặp dấu {}). Trong thân của struct chứa khai báo các thành viên.
Có một quy ước khác về nơi khai báo kiểu struct. Do struct là một cấu trúc phức tạp, bạn nên
khai báo mỗi struct trong một tập tin code riêng với tên tập tin trùng với tên struct. Nếu có nhiều
struct thuộc cùng nhóm, bạn nên đặt các tập tin code vào cùng một folder. Đồng thời đặt
tên namespace theo đúng cấu trúc folder của project. Cách làm này giúp đồng bộ giữa cấu trúc
vật lý (tập tin) và cấu trúc logic (namespace), giúp dễ dàng tìm và điều chỉnh code về sau.
Sử dụng struct trong C#
Một khi đã định nghĩa xong struct, bạn có thể sử dụng nó như bất kỳ kiểu
dữ liệu nào trong C#.
Để khởi tạo object của struct, bạn cần dùng từ khóa new và gọi một trong
số các phương thức khởi tạo.
// khai báo và khởi tạo biến a thuộc kiểu Complex
var a = new Complex(1, 2);
Từ object của struct bạn có thể truy xuất các thành viên public như đọc/gán
giá trị cho biến/đặc tính, gọi phương thức, thực hiện các biểu thức.
// sử dụng đặc tính Modulus của Complex
Console.WriteLine($"|a| = {a.Modulus}");
// gọi phương thức Parse
a.Parse("10-2i");
// thực hiện phép cộng trên số phức
Console.WriteLine($"a + b = { a + b}");
Nếu trong struct có khai báo thành viên tĩnh (static member), bạn không
cần khởi tạo object mà có thể gọi trực tiếp thành viên đó qua tên struct.
var b = Complex.FromString("5 + 3i");

Khai báo các thành viên cơ bản của struct trong C#


Biến thành viên
Biến thành viên (member variable) là thành phần chứa dữ liệu của struct
trong C#. Nó được khai báo trực tiếp trong thân của struct (phải nằm trực
tiếp trong khối code của thân struct).
Dữ liệu này có thể được sử dụng bởi bất kỳ thành phần nào khác của struct.
Nói cách khác, biến thành viên có phạm vi tác dụng (scope) là toàn bộ thân

116
struct, bất kể vị trí khai báo của biến. Tùy vào thiết lập, dữ liệu này cũng
có thể được sử dụng bởi thành phần bên ngoài struct.
Biến thành viên được khai báo với cú pháp sau:
[access_modifier] <type> <variable_name>;
Tức là cú pháp khai báo biến thành viên giống hệt cú pháp khai báo biến
cục bộ trong C#, ngoại trừ access_modifier.
Access modifier là hai từ khóa public hoặc private dùng để điều khiển
truy cập vào biến thành viên. Biến public cho phép code bên ngoài struct
sử dụng; Biến private chỉ cho phép sử dụng trong nội bộ struct.
Như trong ví dụ trên, Real và Imaginary là hai biến thành viên public thuộc
kiểu double:
public double Real; // trường thực
public double Imaginary; // trường ảo
Tên biến bên cạnh tuân thủ quy tắc đặt định danh thì nên tuân theo một số
quy ước khác. Tên biến thành viên public nên tuân theo cách viết PascalCase
(giống như tên struct). Tên biến thành viên private nên bắt đầu bằng ký tự
gạch chân và tiếp theo là chữ cái thường.
Phương thức thành viên (method)
Phương thức (method) là một thành viên của struct chịu trách nhiệm xử lý
thông tin. Phương thức trong C# cho phép tái sử dụng code mà không phải
viết lặp đi lặp lại nhiều lần. Do đó dễ hình dung phương thức là một khối
code được đặt tên và chứa các lệnh để cùng thực hiện một nhiệm vụ cụ thể.
Phương thức của C# tương tự như hàm (function) và thủ tục (procedure) của Pascal, chương
trình con Sub của Visual Basic,... Khác biệt lớn nhất là phương thức của C# bắt buộc phải là
thành viên của một cấu trúc dữ liệu như struct hoặc class. Trong C# không có phương thức “tự
do” hay “toàn cục”.
Thực tế là bạn đã sử dụng (gọi) khá nhiều phương thức trong các bài học
trước nhưng chưa học cách định nghĩa (khai báo) phương thức mới. Phương
thức thành viên được khai báo với cú pháp sau:
[access_modifier] <return_type> <method_name>([parameters])
{
/* thân phương thức */
}
Như trong ví dụ trên chúng ta đã khai báo một số phương thức:
public void Parse(string value)
{
var temp = value.Trim();
if (temp.EndsWith("i") || temp.EndsWith("I"))
{

117
temp = temp.TrimEnd('i', 'I');
var tokens = temp.Split(new[] { '+', '-' }, 2);
Real = double.Parse(tokens[0]);
Imaginary = double.Parse(tokens[1]);
}
else
{
Real = double.Parse(temp);
}
}
public static Complex FromString(string value)
{
var temp = new Complex();
temp.Parse(value);
return temp;
}
Access modifier của phương thức giống hệt như đối với biến thành viên và
có cùng ý nghĩa.
Tên phương thức phải tuân thủ quy tắc đặt định danh, đồng thời cũng tuân
theo quy ước viết PascalCase giống như đặt tên biến thành viên public.
Return type là kiểu của kết quả trả về của phương thức. Return type có thể
là bất kỳ kiểu dữ liệu nào của C# và .NET. Nếu phương thức không trả về
kết quả gì thì return type là từ khóa void. Nếu return type khác void thì
trong thân phương thức bắt buộc phải có lệnh return <value> để trả giá
trị lại cho nơi gọi. Nếu thiếu lệnh return C# sẽ báo lỗi và không biên dịch
tiếp.
Parameters (danh sách tham số, còn gọi là danh sách tham số hình thức)
là danh sách biến mà chúng ta có thể sử dụng trong phương thức. Danh
sách tham số được định nghĩa theo cách sau:
(<kiểu_1> <tham_số_1>, <kiểu_2> <tham_số_2, …)
Hình dung một cách đơn giản, danh sách tham số chính là một chuỗi khai
báo biến cục bộ viết tách nhau bởi dấu phẩy. Do đó, mỗi tham số đều tuân
thủ quy tắc khai báo:
<kiểu_dữ_liệu> <tên_biến>
Danh sách tham số không bắt buộc phải có trong khai báo phương thức.
Nếu danh sách tham số trống, bạn chỉ cần viết cặp dấu ngoặc tròn sau tên
phương thức.
Bạn sẽ học kỹ hơn nữa về phương thức khi tìm hiểu về khai báo class.
Phương thức khởi tạo (constructor)
Phương thức khởi tạo (constructor) là một loại phương thức đặc biệt giúp
khởi tạo giá trị cho các thành viên của struct. Về hình thức, phương thức

118
khởi tạo có tên trùng với struct và không chỉ định kiểu trả về. Tất cả những
vấn đề khác, phương thức khởi tạo giống hệt như đối với phương thức.
public Complex(double r)
{
Real = r;
Imaginary = 0;
}
public Complex(double r, double i)
{
Real = r;
Imaginary = i;
}
Phương thức khởi tạo của struct bắt buộc phải có danh sách tham số. Bạn
không thể viết phương thức khởi tạo không tham số cho struct.
Trong phương thức khởi tạo của struct bạn bắt buộc phải khởi tạo giá trị
cho tất cả các trường của struct.
Bạn có thể không viết phương thức khởi tạo nào cho struct. Khi đó compiler
sẽ tự động sinh cho bạn một phương thức khởi tạo không tham số. Trong
trường hợp đó, khi khởi tạo object của struct, tất cả các biến thành viên sẽ
nhận giá trị mặc định của kiểu.
Thuộc tính (property)
Trong struct Complex bạn đã khai báo thuộc tính Modulus như sau:
public double Modulus
{
get
{
return Math.Sqrt(Real * Real + Imaginary * Imaginary);
}
}
Thuộc tính (property) là một loại thành viên đặc biệt dùng để kiểm soát
nhập/xuất dữ liệu cho struct. Property được sử dụng đặc biệt phổ biến trong
struct và class của C#.
Cấu trúc chung nhất để khai báo thuộc tính như sau:
[access_modifier] <type_name> <property_name>
{
[access_modifier] get { /* get method body */ };
[access_modifier] set { /* set method body */ };
} [= <value>];
Trong đó, tên thuộc tính (property name) được đặt theo quy tắc đặt định
danh và quy ước giống như biến thành viên public.
Access modifier giống hệt như của biến và phương thức.

119
Hai phương thức get và set được gọi chung là accessor. Mỗi phương thức
get hoặc set có thể sử dụng từ khóa điều khiển truy cập của riêng mình,
giúp property đó biến thành loại:
 chỉ đọc (read-only): public get, private set;
 chỉ gán (assign-only): private get, public set;
 truy cập tự do (full access): public get, public set (mặc định).
Về bản chất, get và set là hai phương thức nhưng không có danh sách tham
số. Trong phương thức set có thể sử dụng từ khóa value để đại diện cho dữ
liệu gán vào cho property. Trong phương thức get phải có lệnh return để
trả giá trị. Trong property có thể vắng mặt một trong hai phương thức get
hoặc set.
Khi sử dụng, property được truy cập giống hệt như biến thành viên.
var a = new Complex(1, 2);
// sử dụng đặc tính Modulus của Complex
WriteLine($"|a| = {a.Modulus}");
Sau khi tìm hiểu về khai báo class, bạn sẽ khảo sát chi tiết hơn nữa về thuộc tính trong C#.
Các thành phần khác
Trong struct bạn có thể ghi đè phương thức (method override) kế thừa từ
lớp Object của .NET. Trong ví dụ trên bạn đã ghi đè phương thức ToString
của Object, dùng để chuyển giá trị sang chuỗi ký tự.
public override string ToString()
{
if (Imaginary == 0)
{
return Real.ToString();
}
return $"{Real} {(Imaginary > 0 ? '+' : '-')} {Math.Abs(Imaginary)}i";
}
Bạn cũng có thể nạp chồng các toán tử (operator overloading) cho struct
để sử dụng toán tử tương ứng cho object của struct. Trong ví dụ trên, bạn
đã nạp chồng hai phép toán cộng (+) và trừ (-) để có thể thực hiện các
phép toán tương ứng trên object của Complex.
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
}
public static Complex operator -(Complex a, Complex b)
{
return new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
}
Các thành phần này (và một số thành phần khác chưa được nhắc tới) sẽ
được xem xét rất chi tiết trong các nội dung của phần lập trình hướng đối
tượng và xây dựng class.
120
Kết luận
Bài học này đã cung cấp cho bạn những kiến thức cơ bản tạm đủ để bạn
định nghĩa một struct đơn giản và sử dụng trong chương trình C#. Qua việc
khai báo struct, bạn cũng tiếp cận với một số khái niệm quan trọng của C#
như biến thành viên, phương thức và đặc tính.
Bài học này không có ý định đi sâu vào các vấn đề này vì bạn sẽ học tiếp
trong phần lập trình hướng đối tượng và xây dựng class.
Cũng lưu ý rằng, struct thường chỉ nên sử dụng để xây dựng các cấu trúc
dữ liệu không quá phức tạp và không cần tạo ra hệ thống cây dữ liệu (type
hierarchy). Trong những tình huống khác bạn nên sử dụng class.

121
Namespace (không gian tên) trong C#
Namespace (Không gian tên) trong C# có vai trò rất quan trọng khi bạn
định nghĩa và sử dụng các kiểu dữ liệu. Namespace có nhiệm vụ phân nhóm
toàn bộ các kiểu dữ liệu của C# và .NET theo một cấu trúc phân cấp. Nhờ
có namespace, kiểu dữ liệu được quản lý tốt hơn và tránh được hiện tượng
xung đột tên.
Bài học này sẽ cung cấp cho bạn những thông tin cơ bản về namespace
trong C#.

Namespace trong C#
Khi bạn bắt đầu có thể định nghĩa ra các kiểu dữ liệu của riêng mình
(enum, struct), một vấn đề bắt đầu phát sinh: khả năng đặt tên kiểu trùng
nhau. C# không cho phép đặt tên kiểu trùng nhau. Dễ hiểu thôi. Nếu tên
kiểu trùng nhau, vậy sẽ dùng cái nào?
Lấy ví dụ về struct Complex mà bạn đã xây dựng trong bài học trước. Thực
tế, C# (.NET) đã định nghĩa sẵn một kiểu struct Complex với cùng mục đích.
Nhưng tại sao bạn vẫn xây dựng được struct Complex mà không bị lỗi.
Câu trả lời là, mặc dù có cùng tên nhưng chúng được đặt trong các
namespace khác nhau.
Khái niệm Namespace trong C#

Các khối cơ bản trong tập tin mã nguồn C#


Không gian tên (namespace) trong C# được dùng để tổ chức quản lý code.
Có thể tưởng tượng namespace như một cái hộp chứa, trong đó có thể chứa
những chiếc hộp khác (namespace con) và các vật dụng (các kiểu dữ liệu

122
như struct, class, interface). Namespace đóng vai trò đặc biệt quan trọng
giúp viết code rõ ràng rành mạch, cũng như hỗ trợ đắc lực trong việc quản
lý các dự án lớn.
Namespace mặc dù có thể dịch sang tiếng Việt là Không gian tên. Tuy nhiên nó không được sử
dụng rộng rãi. Vì vậy, chúng ta sẽ gắn bó với từ gốc tiếng Anh namespace.
Namespace có thể trực tiếp chứa các đơn vị sau:
 Namespace khác (namespace con)
 Cấu trúc (struct)
 Lớp (class)
 Giao diện (interface)
 Kiểu liệt kê (enum)
 Kiểu ủy nhiệm (delegate)
Khái niệm và cách sử dụng các loại đơn vị trên sẽ lần lượt được xem xét kỹ trong các bài tương
ứng.
Namespace không thể chứa trực tiếp phương thức (method), biến, hằng,…
Nói một cách khác, trong namespace chỉ có thể chứa định nghĩa kiểu dữ
liệu (data type) và namespace con.
Namespace và thư mục
Để so sánh, chúng ta có thể hình dung cấu trúc namespace tương tự như
cấu trúc thư mục và tập tin trong hệ điều hành.
Giả sử chúng ta có một lượng lớn tập tin, thư mục cho phép chúng ta nhóm
các tập tin có liên quan vào cùng một thư mục, nhóm các thư mục có liên
quan vào một thư mục lớn hơn,.... Cách tổ chức như vậy giúp chúng ta quản
lý tập tin và thư mục hiệu quả hơn.
Trong so sánh đó, không gian tên có vai trò tương tự thư mục, các định
nghĩa kiểu dữ liệu có vai trò tương tự tập tin. Sự khác biệt ở chỗ, nếu trong
cấu trúc thư mục (Windows) sử dụng ký tự “” để phân tách các thư mục
chứa nhau thì trong cấu trúc không gian tên sử dụng dấu chấm “.” để phân
tách các không gian tên lồng nhau.
Nếu trong Windows không cho phép trong một thư mục có 2 tập tin trùng
tên nhau thì trong C# cũng không cho phép trong một không gian tên có
hai đơn vị code trùng tên. Không gian tên cũng giúp giải quyết tình trạng
trùng tên gọi (naming conflict).

123
Quy tắc đặt tên namespace trong C#
Không gian tên có thể được lựa chọn tùy ý, với các cấp độ lồng nhau (giống
như đặt tên thư mục) theo tư tưởng phân chia code của người lập trình
nhưng phải tuân thủ quy tắc đặt tên của C#:
 Các tên gọi chỉ được bắt đầu bằng ký tự chữ cái hoặc ký tự gạch chân
(ký tự underscore “_”),
 Chỉ được chứa chữ cái, chữ số và ký tự gạch chân,
 Phân biệt chữ hoa/thường.
Ngoài quy tắc bắt buộc trên, việc đặt tên không gian tên thường tuân thủ
quy ước (không bắt buộc):
 Không gian tên gốc nên đặt trùng với tên project;
 Nếu trong project có thêm các thư mục thì tập tin mã nguồn đặt trong
các thư mục này sẽ có không gian tên con trùng với tên thư mục.
Khi đó, cấu trúc không gian tên sẽ đồng nhất với cấu trúc thư mục của
project, giúp chúng ta quản lý code dễ dàng hơn. Nếu chúng ta thêm các
tập tin mã nguồn mới, Visual Studio sẽ tự động đặt không gian tên theo quy
ước này. Ngoài ra, Visual Studio mặc định sẽ lấy tên của project làm
namespace cho tất cả tập tin mã nguồn trong project.
Trong tập tin code dưới đây, chứa 1 class (Program) đặt trong không gian
tên ConsoleApp, trùng với tên project. Tập tin mã nguồn này nằm trực tiếp
trong thư mục dự án nên không đặt thêm không gian con.

Tập tin mã nguồn đầu tiên (Program.cs)

124
Cấu trúc using trong C#
Tên ngắn gọn và tên đầy đủ của kiểu dữ liệu
Với ví dụ về hệ thống tập tin, ta thấy rằng mỗi tập tin có thể có hai tên gọi:
tên ngắn gọn (chỉ chứa tên của bản thân tập tin) và tên đầy đủ (chứa thêm
đường dẫn từ thư mục gốc tới thư mục chứa tập tin đó).
Khi làm việc với hệ thống tập tin của Windows (ví dụ, khi chạy một chương
trình cần truy cập tới tập tin), chúng ta gặp tình huống: nếu đang ở trong
một thư mục nào đó, ta có thể truy xuất tới các tập tin trong thư mục đó
dùng tên ngắn gọn; nếu muốn truy xuất tới tập tin nằm trong thư mục khác,
ta phải dùng tên đầy đủ (rất dài dòng).
Tình huống tương tự xảy ra với không gian tên: mỗi kiểu dữ liệu cũng có
hai tên gọi, tên ngắn gọn (short name) và tên đầy đủ (fully-qualified
name). Tên ngắn gọn là tên (định danh) của kiểu do người lập trình đặt;
tên đầy đủ là tên ngắn gọn cộng thêm cấu trúc không gian tên chứa định
nghĩa kiểu này.
Nếu các định nghĩa kiểu nằm trong cùng một không gian tên, bạn có thể
truy xuất tới nó trực tiếp thông qua tên ngắn gọn. Nếu các định nghĩa kiểu
nằm trong các không gian tên khác nhau, bạn phải sử dụng tên đầy đủ.
Ví dụ khi học về console trong C#, bạn đã làm việc với class Console.
Console là tên “cúng cơm” (ngắn gọn). Do lớp Console nằm trong không
gian tên System, do đó nó có họ tên đầy đủ là System.Console.
Cấu trúc using
Theo nguyên tắc trên, nếu bạn muốn sử dụng lớp Console thì phải dùng
tên đầy đủ của nó là System.Console. Cách viết như vậy rất dài dòng nếu
như cấu trúc không gian tên phức tạp.
Cấu trúc using cho phép sử dụng tất cả các kiểu trong một namespace mà
không cần sử dụng tên đầy đủ của chúng.
Ví dụ, khi chúng ta sử dụng using System;, tất cả các kiểu trong không
gian tên này có thể được sử dụng thông qua tên ngắn gọn. Các lệnh using
thường được đặt cùng nhau thành một khối ở đầu mỗi tập tin mã nguồn
nhưng không bắt buộc. Khối using cũng có thể nằm luôn trong khối
namespace.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp

125
{
// khối using cũng có thể để ở đây
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello world from C#");
Console.WriteLine("Press any key to quit");
Console.ReadKey();
}
}
}

Cấu trúc using static


C# 7 đưa thêm vào cấu trúc using static giúp đơn giản hóa hơn nữa khi làm
việc với các thành viên static của struct hoặc class.
Hãy xem lại chương trình sau:
using System;
using static System.Console;
class Program
{
static void Main(string[] args)
{
// In biến enum ra console
var gender = "Male";
WriteLine($"My gender is {gender}");
var day = DayOfWeek.Tuesday;
WriteLine($"Today is {day}");
}
}
Hãy để ý tới cấu trúc lạ using static System.Console;. Cấu trúc này cho
phép bạn sử dụng trực tiếp tất cả các thành viên static của lớp Console.
Nghĩa là giờ bạn có thể gọi trực tiếp Write, WriteLine (static method), thay
vì Console.Write, Console.WriteLine. Bạn có thể sử dụng thẳng
ForegroundColor (static property) thay vì phải viết
Console.ForegroundColor.

126
Class trong C#, căn bản về lập trình hướng đối tượng
C# là ngôn ngữ lập trình hướng đối tượng thuần nhất. Hầu như mọi thứ
trong C# đều là class. Do đó, học cách xây dựng và sử dụng class là yêu
cầu bắt buộc trong khi học lập trình C#. Không nắm vững khái niệm
class/object sẽ rất khó để học C#; Không biết kỹ thuật xây dựng class thì
hầu như không làm được gì trong C#.
Bài học này sẽ đưa bạn bước đầu tiếp xúc với kỹ thuật xây dựng class trong
C#. Nắm chắc các kỹ thuật này là yêu cầu bắt buộc để có thể đi xa hơn.

Class, trừu tượng hóa, lập trình hướng đối tượng


Tài liệu này được xây dựng dựa trên giả định rằng bạn đã hiểu được các
khái niệm cơ bản của lập trình nói chung và lập trình hướng đối tượng nói
riêng. Vì vậy, tài liệu sẽ chỉ nhắc lại sơ lược một số khái niệm cơ bản của
lập trình hướng đối tượng như class, object, abstraction, các đặc trưng của
class.
Class và trừu tượng hóa
Class là bản mô tả những tính chất và hành vi chung của những gì tồn tại
trong thực tế. “Những gì tồn tại trong thực tế” đó được gọi là các đối
tượng (object) cụ thể. Từ các đối tượng cụ thể, chúng ta phân tích ra những
điểm chung về thông tin và hoạt động để tạo thành class.
Việc phân tích và tóm lược những tính chất và hành vi chung của một nhóm
những đối tượng thực tế như vậy được gọi là trừu tượng hóa (abstraction).
Việc trừu tượng hóa giúp chúng ta tách rời những thông tin cần thiết của
đối tượng để nghiên cứu, đồng thời bỏ qua những gì không liên quan.
Class là kết quả của sự trừu tượng hóa các đối tượng cùng loại về hai khía
cạnh: thông tin mô tả đối tượng, và những hành vi (hoạt động) trên các
thông tin đó.
Với ý nghĩa trên, class không nhất thiết phải liên quan đến việc lập trình.
Chúng ta có thể tạo ra các class với ý nghĩa là một sự trừu tượng hóa bất
kỳ khi nào cần nghiên cứu về các đối tượng.
Object và cụ thể hóa
Ở chiều ngược lại, nếu có một bản mô tả trừu tượng, chúng ta có thể tạo ra
nhiều phiên bản cụ thể của nó.
Ví dụ, nếu có bản mô tả trừu tượng về tủ, chúng ta có thể dùng vật liệu để
tạo ra nhiều chiếc tủ thực sự dựa trên mô tả đó.

127
Những phiên bản cụ thể tạo ra từ mô tả trừu tượng như vậy được gọi là
object.
Như vậy, quan hệ giữa class và object là loại quan hệ giữa mô tả (nằm trên
giấy, trong suy nghĩ,…) với sự vật/hiện tượng cụ thể trong thực tế.
Class trong lập trình hướng đối tượng và C#
Class trong các ngôn ngữ lập trình hướng đối tượng nói chung, trong C# nói
riêng, mang ý nghĩa là một kiểu dữ liệu. Đối với C#, class là các khối xây
dựng cơ sở của các chương trình ứng dụng, và là trung tâm của lập trình
C#.
Class trong C# là một loại kiểu dữ liệu đặc biệt chứa định nghĩa những thuộc
tính (thông tin) và phương thức (hành vi), dùng để mô tả chung cho một
nhóm những thực thể cùng loại.
Trong C#, mỗi class có thể chứa:
1. Biến thành viên (field): Lưu trữ các thông tin mô tả về đối tượng
hay trạng thái của đối tượng
2. Thuộc tính (property): có vai trò lưu trữ thông tin tương tự như biến
thành viên nhưng có khả năng kiểm soát dữ liệu xuất nhập
3. Phương thức (method): Dùng để cập nhật, tính toán, cung cấp và xử
lý thông tin
4. Sự kiện (delegate/event): Gửi thông báo về sự thay đổi trạng thái
của đối tượng ra bên ngoài
Ngoài ra, trong class còn có thể chứa định nghĩa của kiểu dữ liệu khác, gọi
là kiểu thành viên (member /inner/nested type). Class có thể chứa định
nghĩa của bất kỳ nhóm kiểu nào mà bạn đã biết (class, struct, interface,
delegate, enum).
Khi xem class như một kiểu dữ liệu thì object của class tương ứng chính là
biến thuộc kiểu dữ liệu đó. Class chứa mô tả trừu tượng còn object chứa giá
trị cụ thể của mỗi mô tả đó.

Khai báo class trong C#


Cú pháp khai báo class C#
Khai báo class trong C# sử dụng cấu trúc
[public|internal] class <tên class>{ [thân class] }
trong đó:
 class là từ khóa của C# dùng để khai báo class

128
 tên class do người lập trình lựa chọn và phải tuân thủ quy tắc đặt định
danh (xem dưới đây)
 public hoặc internal được gọi là các từ khóa điều khiển truy
cập (access modifier) của class
Các phần này viết tách nhau bởi dấu cách.
Dấu cách trong C# chỉ có tác dụng phân tách các thành phần của một lệnh,
khác với một số ngôn ngữ dùng dấu cách như một phần của cú pháp. Số
lượng dấu cách không ảnh hưởng tới ý nghĩa của code. Compiler sẽ tự bỏ
qua những dấu cách thừa.
Từ khóa class là bắt buộc khi khai báo lớp. Mặc định, Visual Studio thể hiện
từ khóa bằng màu xanh da trời. Từ khóa là những cụm ký tự được C# lựa
chọn cho những mục đích riêng để diễn đạt cú pháp của ngôn ngữ. Một số
kiểu dữ liệu dựng sẵn của C# cũng được đặt thành từ khóa (sẽ xem xét
trong các bài sau).
Từ khóa điều khiển truy cập quyết định phạm vi sử dụng của class. Mỗi
class trong C# có thể chỉ được sử dụng nội bộ trong phạm vi của project,
hoặc có thể được sử dụng bởi các project khác. Mặc định, mỗi class trong
C# chỉ được sử dụng trong phạm vi của project (sử dụng bởi các class khác
trong cùng project). Do đó, nếu không thấy điều khiển truy cập nào thì sẽ
hiểu là internal. Nếu muốn sử dụng class này trong các project khác, trước
từ khóa class cần bổ sung từ khóa public.
Từ khóa public sẽ thể hiện rõ vai trò của nó khi bạn bắt đầu tách một project lớn thành nhiều
project con. Trong đó, project con thường là các thư viện lớp. Các class trong thư viện lớp phải
đặt truy cập public thì mới sử dụng được trong project khác. Nếu chỉ có một project,
internal hay public không có gì khác biệt.
Lấy ví dụ minh họa là tập tin mã nguồn đầu tiên của chúng ta (Program.cs)
của project đã xây dựng trong bài cài đặt Visual Studio.

129
Class này được đặt tên là Program, là một internal class (không chỉ rõ từ
khóa truy cập => C# sẽ coi là internal). Khối code thân class này hiện có
một phương thức (Main() hay Entry Point).
Đặt tên class trong C#
Tên class do người lập trình tự chọn và phải tuân thủ quy tắc đặt định
danh (identifier) trong C#, cụ thể như sau:
1. Định danh phải bắt đầu bằng chữ cái, ký tự gạch chân “_” hoặc ký tự
“@”
2. Định danh chỉ được chứa chữ cái, chữ số và ký tự gạch chân
3. Định danh không được trùng với từ khóa; nếu muốn đặt định danh
trùng với từ khóa, cần đặt ký tự @ phía trước
4. Độ dài định danh không giới hạn và có thể chứa ký tự Unicode (ví dụ,
có thể đặt định danh bằng tiếng Việt có dấu)
5. Định danh có phân biệt chữ hoa và chữ thường (case-sensitive)
Ngoài các quy tắc trên, việc đặt tên class trong C# cũng nên tuân thủ
các quy ước sau:

130
1. Mỗi tập tin mã nguồn chỉ nên chứa một class và tên class đặt trùng
tên tập tin. Quy ước này kết hợp với quy ước đặt tên namespace giúp
đồng bộ giữa cấu trúc quản lý class với cấu trúc tập tin vật lý, giúp
việc quản lý mã nguồn dễ dàng hơn
2. Tên class bắt đầu bằng chữ cái in hoa
3. Nếu tên class gồm nhiều từ ghép lại thì nên đặt theo
kiểu CamelCase (viết hoa chữ cái đầu của mỗi từ)
Tên class C# được Visual Studio hiển thị bằng màu xanh nhạt.
Thân class
Thân class là một khối code và có thể chứa khai báo biến/hằng thành viên,
thuộc tính và phương thức thành viên.
Các thành phần của class sẽ lần lượt được xem xét trong các bài học tương ứng.
Đặc biệt hơn, trong thân class cũng có thể khai báo class khác, gọi là nested
class (sẽ xem xét trong bài học riêng). Toàn bộ thân class phải đặt trong
cặp dấu {}.
Toàn bộ code nằm trong một cặp dấu {} được gọi là một khối code (code
block).
Thân của class, namespace, method, interface, struct, các cấu trúc điều khiển (sẽ xem xét sau)
đều là khối code.
Xin nhắc lại, trong lập trình hướng đối tượng có 3 giai đoạn:
(1) định nghĩa class (định nghĩa kiểu dữ liệu);
(2) khai báo và khởi tạo object (khai báo và gán giá trị cho biến);
(3) sử dụng object (truy xuất dữ liệu, gọi phương thức).
Trong bài này và một số bài tiếp theo chúng ta sẽ chỉ xem xét cách định
nghĩa class mà chưa sử dụng các class này (dừng ở giai đoạn 1).

Một số ví dụ minh họa khai báo class trong C# project


Khai báo nhiều class trong cùng một tập tin
Đây là ví dụ đơn giản nhất về khai báo class trong C#. Để thực hiện, hãy
làm theo các bước sau:
1. Tạo một project mới có tên là P01_ClassSimple (Console App) nằm
trong Solution S01_Class
2. Viết code cho tập tin Program.cs như sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
131
using System.Threading.Tasks;
namespace P01_ClassSimple
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Simple class declaration examples");
Console.WriteLine("Press any key to quit ...");
Console.ReadKey();
}
}
/// <summary>
/// class for cars
/// </summary>
class Car
{
// the class body is still empty
// TODO: add more member (field and property)
}
/// <summary>
/// class for airplanes
/// </summary>
internal class Airplane
{
}
/// <summary>
/// class for motorbikes
/// </summary>
public class Motorbike
{
}
}
Trong ví dụ này chúng ta đã khai báo 3 class trong cùng một tập tin mã
nguồn Program.cs:
1. Lớp Car không có từ khóa truy cập, mặc định nó sẽ được hiểu là một
internal class (chỉ sử dụng được trong project này).
2. Lớp Airplane chỉ rõ từ khóa truy cập là internal, và có cùng tác dụng
như lớp Car.
3. Lớp Motorbike sử dụng từ khóa truy cập là public.
Cả ba class này cùng sử dụng chú thích tài liệu ở phía trước.

132
Ở bất kỳ đâu trong project, nếu bạn trỏ chuột vào kiểu Car, Visual Studio
sẽ hiển thị chú thích này. Nó giúp bạn mô tả và nhớ lại mục đích của class
nhanh chóng mà không cần mở lại tập tin mã nguồn.
Cả ba class này giờ đều đang nằm trong cùng namespace P01_ClassSimple,
nên chúng sẽ có tên đầy đủ lần lượt là P01_ClassSimple.Car,
P01_ClassSimple.Airplane, P01_ClassSimple.Motorbike.
Cách thức khai báo này không được khuyến khích. Nó làm phình tập tin
code với nhiều nội dung không liên quan, dẫn đến khả năng bị lỗi, khó theo
dõi và bảo trì về sau.
Khai báo mỗi class trong tập tin code riêng
Thêm một project mới đặt tên là P02_ClassPerFile (Console App) vào
solution trên.
Sau đó thực hiện theo các bước dưới đây:

133
Bạn có thể nhận thấy, tập tin mã nguồn mới Car.cs chứa đúng 1 class
(internal) Car, nằm trong namespace P02_ClassPerFile (trùng tên project).
Nếu số lượng class ít và không chia nhóm thì có thể dùng cách bố trí tập tin
như thế này.

134
Khai báo class trong tập tin riêng nằm trong thư mục
Thêm project P03_ClassInFolder vào Solution.
Trong project này thêm thư mục Vehicles.
Click phải vào thư mục và chọn Add => Class, tương tự như trong phần
thực hành bên trên, để tạo class Car trong tập tin code Car.cs.

Sự khác biệt là bây giờ tập tin Car.cs nằm trong thư mục Vehicles, còn lớp
Car sẽ nằm trong namespace P03_ClassInFolder.Vehicles. Cấu trúc
namespace chứa class giờ đồng nhất với cấu trúc folder chứa tập tin code.
Do mỗi tập tin chứa đúng 1 class trùng tên nên cấu trúc tập tin code cũng
đồng nhất với vị trí class.
Đây là cấu trúc được khuyến khích sử dụng khi khai báo class mới trong C#.
Nó rất phù hợp khi số lượng class lớn và phân thành nhiều nhóm khác nhau.
Một số lưu ý khi khai báo class mới trong C#
Thông thường, một class nên khai báo trực tiếp trong namespace của
project (trừ trường hợp nested class sẽ xem xét ở một bài riêng). Nếu class
nằm trong không gian tên con, hãy tạo một thư mục có tên trùng với tên
không gian con và đặt tập tin class mới trong thư mục đó.

135
Mỗi class nên khai báo trong một tập tin riêng. Tên tập tin nên đặt trùng
tên class. Điều này có thể làm tăng số lượng tập tin, nhưng mỗi tập tin có
ít code, cũng đồng nghĩa với việc sẽ dễ tìm kiếm, dễ bảo trì, ít mắc lỗi hơn.
Nhắc lại, nếu bạn tạo một thư mục bên trong project, Visual Studio sẽ tự động lấy tên thư mục
làm namespace con, tên project là namespace chính. Khi đó, một tập tin class mới đặt trong thư
mục sẽ tự động được đặt trong namespace con. Điều này tạo ra sự đồng bộ giữa cấu trúc tập
tin/thư mục với class/namespace.
Nếu project không phải là thư viện, các class nên để mức truy cập là
internal.
Đối với mỗi class nên sử dụng ghi chú tài liệu.
Ghi chú tài liệu (documentation comment) là loại ghi chú đặc biệt của C#, cho phép tạo ra một
dạng “hướng dẫn sử dụng” của đơn vị code được ghi chú (như class, method, interface,...).
Loại ghi chú này cho phép người xây dựng class đưa ra các hướng dẫn cơ bản mà người sử
dụng class có thể đọc. Loại ghi chú này cũng cho phép trình biên dịch lọc riêng ra để tạo thành
tài liệu hướng dẫn cho code. Ghi chú tài liệu được Visual Studio sinh tự động khi gõ cụm “///”
trước đối tượng cần chú thích.
Nên hình thành thói quen viết chú thích đầy đủ cho code. Nó giúp ích rất nhiều cho việc bảo trì
code hoặc làm việc nhóm.

136
Biến thành viên (member variable) trong C#
Như bạn đã biết, trong class có các thành viên chứa thông tin và thành viên
chịu trách nhiệm xử lý thông tin. Biến thành viên (member variable) là loại
thành viên có nhiệm vụ chứa thông tin của class. Với đặc điểm “strong và
static typing” của C#, biến thành viên bắt buộc phải thuộc về một kiểu dữ
liệu xác định. Học cách sử dụng biến thành viên và kiểu dữ liệu là bước đi
đầu tiên của chúng ta để xây dựng các class hữu ích trong C#.
Bài học sẽ giúp bạn nắm được khái niệm và cách làm việc với biến thành
viên của class. Bài học cũng sẽ giúp bạn làm quen với một số kiểu dữ liệu
cơ bản của C#, vốn không thể không biết khi khai báo và sử dụng biến
thành viên và thuộc tính.

Biến thành viên của class C#


Biến thành viên (member variable) là thành phần chứa dữ liệu của class
trong C#. Nó được khai báo trực tiếp trong thân của class (phải nằm trực
tiếp trong khối code của thân class). Dữ liệu này có thể được sử dụng bởi
bất kỳ thành phần nào khác của class. Tùy vào thiết lập, dữ liệu này cũng
có thể được sử dụng bởi thành phần bên ngoài class.
Trong C#, nếu một biến được khai báo nằm trong thân của phương thức, nó được gọi là biến
cục bộ (local variable). Chúng ta xem xét biến cục bộ và phương thức ở bài học khác.
Biến thành viên thường được gọi ngắn gọn là trường (field), hay trường dữ
liệu (data field).
Biến thành viên được xem là nơi lưu trữ trạng thái (status) của class. Thay
đổi giá trị biến thành viên là thay đổi trạng thái của class.
Cú pháp khai báo biến thành viên
Quy tắc khai báo biến thành viên như sau:
[public|private|protected] <kiểu-dữ-liệu> <tên-biến> [= <giá-
trị>];
Trong đó:
public, private và protected là các từ khóa điều khiển truy cập (access
modifier) cho biến thành viên, cụ thể:
 public: biến có thể được truy xuất từ bên ngoài class
 private (mặc định): biến sử dụng hoàn toàn nội bộ, cũng như không
được truyền thừa
 protected: biến sử dụng nội bộ và có thể truyền thừa

137
Nếu không ghi rõ từ khóa, C# mặc định sẽ hiểu đó biến đó là private,
nghĩa là chỉ có thể sử dụng nội bộ trong class, bên ngoài không thể truy
xuất giá trị này. Sự khác biệt giữa ba từ khóa này sẽ thể hiện rõ ràng hơn
ở bài học về kế thừa.
Khái niệm “trong” và “ngoài” class có thể hiểu đơn giản như sau: “trong” class nghĩa là tất cả
code nằm trong khối code thân class. Những code nào không nằm trong khối code thân class
được coi là “ngoài”. Code trong class đều có thể “nhìn thấy” các biến thành viên và sử dụng
chúng. Code ở bên ngoài class chỉ có thể nhìn thấy và truy xuất các thành viên đặt là public.
Kiểu dữ liệu
Kiểu dữ liệu có thể là những kiểu dữ liệu do C# định nghĩa sẵn (int, bool,
string,...) hoặc do người dùng tự định nghĩa.
C# đã định nghĩa sẵn các kiểu dữ liệu cơ bản, tương tự trong nhiều ngôn
ngữ lập trình khác.
C# cũng cho phép người dùng định nghĩa thêm các kiểu dữ liệu có cấu trúc
của riêng mình, như mảng (array), cấu trúc (struct), liệt kê (enum), lớp
(như chúng ta đang làm).
Bộ thư viện .NET Framework cung cấp một số lượng kiểu dữ liệu (lớp) khổng
lồ có thể sử dụng trong các khai báo này. Trong quá trình học bạn đã lần
lượt gặp các kiểu dữ liệu nói trên, cũng như tự định nghĩa ra các kiểu dữ
liệu của riêng mình.
Các kiểu dữ liệu cơ sở của C# được Visual Studio hiển thị màu sắc tương tự
như từ khóa; các kiểu dữ liệu do người dùng định nghĩa được hiển thị tương
tự như class.
Quy ước đặt tên biến thành viên trong C#
Tên biến do người lập trình tự chọn và tuân thủ theo quy tắc đặt định danh
giống như đối với tên class. Ngoài ra có một số quy ước (không bắt buộc
nhưng nên tuân thủ để thống nhất) sau về cách đặt tên biến:
 Tên biến public và protected nên bắt đầu bằng chữ cái in hoa và tuân
theo cách viết PascalCase
 Tên biến private nên bắt đầu bằng ký tự gạch chân và tuân theo cách
viết camelCase
Quy ước đặt tên biến thành viên khác với quy ước đặt tên biến cục bộ (biến khai báo trong thân
phương thức). Biến cục bộ đều viết theo kiểu camelCase.

138
Gán giá trị cho biến thành viên, giá trị mặc định
Khi khai báo một biến thành viên, C# sẽ tự động gán cho biến này một giá
trị mặc định (lúc khởi tạo object) tùy thuộc vào kiểu dữ liệu: giá trị 0 đối
với kiểu số nguyên, false đối với kiểu bool. Ví dụ:
int a; // a tự nhận giá trị 0
bool b; // b tự nhận giá trị false
Nếu muốn gán một giá trị khác, người dùng có thể sử dụng toán tử
gán (assignment operator) để gán giá trị cho biến ngay trong lúc khai báo.
int a = 10;
bool b = true;

Ví dụ về cách khai báo biến thành viên của class C#


Trong phần này bạn sẽ thực hiện một ví dụ nhỏ về khai báo biến thành viên
cho class để nắm rõ hơn các vấn đề lý thuyết trình bày ở trên.
Tạo một solution S02_MemberVariable với 1 project P01_VariableSample
(Console App) và viết code như sau:
using System;
namespace P01_VariableSample
{
class Program
{
static void Main(string[] args)
{
// khởi tạo biến kiểu Car
Car bmw = new Car();
// để ý là phương thức Main thuộc lớp Program nằm "bên ngoài" class Car,
// do đó nó chỉ "nhìn thấy" các biến thành viên public của Car
bmw.Color = "White";
bmw.Seats = 2;
// các biến thành viên private khác của Car hoàn toàn "vô hình" với
Program.
Console.WriteLine("A new " + bmw.Color + " BMW");
Console.ReadKey();
}
}
/// <summary>
/// Lớp mô tả ô tô
/// </summary>
class Car
{
// dưới đây là các trường private (chứa thông tin nội bộ của class)
// các trường này không được gán giá trị đầu, chúng sẽ nhận giá trị mặc định
theo kiểu
// lưu ý cách đặt tên: có dầu gạch chân ở đầu, bắt đầu là chữ cái thường,
camelCase, báo hiệu đây là trường private
string _make; // nhãn hiệu, giá trị mặc định theo kiểu là null
int _yearOfProduction; // năm sản xuất, giá trị mặc định theo kiểu là 0
bool _hasInsurance; // có mua bảo hiểm hay không, giá trị mặc định theo kiểu
là false
// dưới đây là các trường public (các class khác có thể đọc/ghi giá trị)
// mỗi trường sẽ được gán giá trị đầu
139
// lưu ý cách đặt tên: bắt đầu là chữ cái hoa
public int Seats = 5; // số chỗ ngồi, gán giá trị đầu là 5
public string Color = "Black"; // màu sắc, giá trị đầu là "Black"
}
}
Dịch và chạy debug (F5) để xem kết quả.
Trong phần thực hành này, bạn đã xây dựng một class Car với một số biến
thành viên mô tả cho một chiếc ô-tô.
Bạn đã khai báo các biến private: _make, _yearOfProduction,
_hasInsurance; các biến public Seats và Color.
Hãy lưu ý cách đặt tên các biến này. _make, _yearOfProduction và
_hasInsurance là các biến private nên đặt tên theo quy ước camelCase với
ký tự _ ở đầu. Seats và Color là các biến public nên đặt tên theo quy ước
PascalCase.
Bạn cũng để ý rằng, trong phương thức Main (nơi khởi tạo và sử dụng object
của Car) bạn chỉ có thể truy xuất biến Seats và Color. Bạn không thể truy
xuất các biến private từ bên ngoài class.
Biến thành viên của Car hoàn toàn có thể tham gia vào các lệnh và biểu
thức giống hệt như khi bạn sử dụng biến cục bộ.

Một số vấn đề khi sử dụng biến thành viên


Hạn chế sử dụng biến thành viên public
Trong lập trình hướng đối tượng, nhìn chung đều khuyến nghị hạn chế sử
dụng biến thành viên public.
Các biến thành viên thường được khai báo là private/protected, sau đó viết
các phương thức xuất/nhập dữ liệu riêng cho từng biến (gọi là các hàm
getter/setter). Cách thức này giúp kiểm soát được giá trị xuất nhập cho các
biến.
C# cung cấp một tính năng riêng giúp giải quyết vấn đề này và chúng ta sẽ
xem xét trong phần Property.
Như trong ví dụ trên bạn khai báo hai biến public Seats và Color. Ví dụ này
hoàn toàn mang tính chất minh họa. Nếu xây dựng class thực sự, bạn không
nên khai báo như vậy mà cần dùng thuộc tính (Property) thay cho hai biến
public này.
Biến thành viên chỉ đọc, từ khóa readonly
Mặc định C# cho phép tự do thay đổi giá trị của biến thành viên. Nếu biến
public, có thể thay đổi giá trị của nó ở trong hoặc ngoài class. Nếu là biến
protected/private thì chỉ có thể thay đổi giá trị của nó ở bên trong class.
140
Tuy nhiên, có một số trường hợp bạn muốn rằng giá trị của biến sau khi
khởi tạo sẽ không thay đổi được nữa. Tức là, nó biểu hiện gần giống như
hằng số.
Vậy tại sao không sử dụng hằng?
Trong C#, hằng thành viên của class (và struct) có chút hơi khác biệt. Hằng
thành viên được xem là một thành viên tĩnh (static) của class. Bạn sẽ học
về thành viên tĩnh trong một bài học khác. Đặc điểm này khiến hằng không
phù hợp với vai trò lưu trữ dữ liệu chỉ đọc cho object của class.
C# cung cấp một từ khóa riêng để tạo ra các biến chỉ đọc: từ khóa readonly.
Từ khóa này cần đặt trước tên kiểu khi khai báo biến thành viên.
Hãy cùng thực hiện ví dụ sau để hiểu rõ hơn về biến readonly
using static System.Console;
namespace P02_ReadOnly
{
class Product
{
// đây là các biến readonly, chỉ có thể khởi tạo giá trị trực tiếp ở đây
public readonly double UnitPrice = 100;
public readonly double Discount = 0.1;
// đây là một biến thông thường, có thể tự do thay đổi giá trị
public int Amount;
public Product()
{
}
public Product(double unitPrice, double discount)
{
// hoặc thay đổi giá trị trong constructor
// một khi đã khởi tạo giá trị, biến readonly không thay đổi giá trị được
nữa
UnitPrice = unitPrice;
Discount = discount;
Amount = 0;
}
/* Phương thức này lỗi.
* UnitPrice và Discount là biến readonly.
* Không thể thay đổi giá trị của biến readonly trong phương thức
*
public void Reset()
{
UnitPrice = 0;
Discount = 0;
}
*/
}
class Program
{
static void Main(string[] args)
{
var product = new Product();
WriteLine($"Unit Price = {product.UnitPrice}");
// Lệnh này lỗi. Không thể thay đổi giá trị biến readonly
//product.UnitPrice = 300;
141
product = new Product(200, 0.2);
WriteLine($"Unit Price = {product.UnitPrice}");
ReadKey();
}
}
}
Biến readonly có đặc điểm:
 Chỉ có thể gán giá trị một lần duy nhất lúc khai báo, hoặc trong
phương thức khởi tạo (constructor).
 Không thể gán giá trị ở bất kỳ phương thức nào khác.
 Một khi đã gán giá trị, nó không cho phép thay đổi giá trị.
Trong bài học này bạn đã làm quen với thành phần đầu tiên của class C#:
biến thành viên. Nhìn chung, biến thành viên của class không khác biệt gì
với struct, ngoại trừ có thêm từ khóa protected. Để khai báo và sử dụng
biến thành viên, bạn có thể vận dụng các kiểu dữ liệu và phép toán đã học
trong các bài tương ứng.

142
Phương thức thành viên (member method) của class
C#
Phương thức trong C# – thành phần xử lý thông tin của struct hoặc class –
đã được xem xét một phần trong bài học về struct trong C#. Tuy nhiên, còn
rất nhiều vấn đề quan trọng cần biết khi xây dựng và sử dụng phương thức
trong C#. Bài học này sẽ tiếp tục cung cấp những thông tin chi tiết hơn về
phương thức của C#, tập trung vào một vấn đề quan trọng: tham số của
phương thức.

Phương thức thành viên trong C#


Như bạn đã biết, phương thức (method) trong C# là một thành viên của
class (và struct), là một khối code được đặt tên và chứa các lệnh để cùng
thực hiện một nhiệm vụ cụ thể. Phương thức cho phép tái sử dụng code mà
không phải viết lặp đi lặp lại nhiều lần.
Phương thức trong C# tương tự như hàm (function) và thủ tục (procedure)
của Pascal, chương trình con Sub của Visual Basic,… Sự khác biệt lớn là
phương thức của C# bắt buộc phải là thành viên của một kiểu dữ liệu như
struct hay class. Trong C# không có phương thức “tự do” hay “toàn cục”.
Làm việc với phương thức chia làm hai giai đoạn:
 Khai báo (định nghĩa): Ở giai đoạn khai báo chúng ta mô tả các thông
tin bắt buộc về phương thức, cũng như viết các lệnh cần thực hiện
trong thân phương thức.
 Gọi (sử dụng): đây là giai đoạn chúng ta cung cấp dữ liệu thực sự để
phương thức thực hiện những lệnh đã được thiết kế sẵn ở phần định
nghĩa.
Khai báo phương thức
Cấu trúc chung để khai báo phương thức trong class C# như sau:
[public|protected|private] <kiểu ra> <tên phương thức>
([danh sách tham số])
{
[thân phương thức]
[return [giá trị];]
}
Trong đó, public, protected và private là các từ khóa điều khiển truy cập
tương tự như đối với biến thành viên. Mặc định C# xem phương thức
là private nếu không có từ khóa nào được chỉ định.

143
So với struct, phương thức thành viên class có thêm từ khóa protected.
Tên phương thức do người dùng tự đặt và tuân thủ theo quy tắc đặt định
danh của C#. Ngoài ra, trong C# quy ước tên phương thức viết theo kiểu
PascalCase (luôn bắt đầu bằng chữ in hoa).
Kiểu dữ liệu trả về
Kiểu trả về là kiểu dữ liệu của kết quả nhận được sau khi kết thúc thực hiện
các lệnh trong thân phương thức. Kiểu trả về có thể là bất kỳ kiểu dữ liệu
nào được C#/.NET định nghĩa sẵn hoặc cũng có thể là những kiểu dữ liệu
do người dùng định nghĩa.
Cũng có thể có trường hợp phương thức không trả về kết quả nào (ví dụ,
chúng ta chỉ yêu cầu phương thức viết thông tin ra màn hình). Khi đó, C#
yêu cầu phải viết kiểu trả về là void, là một từ khóa của C#.
Nếu kiểu trả về khác void, trong thân phương thức bắt buộc phải có
lệnh return để báo rằng, giá trị đi sau return sẽ là kết quả thực hiện của
phương thức. Nếu kiểu trả về là void thì không bắt buộc phải có return.
Danh sách tham số
Danh sách tham số (còn gọi là danh sách tham số hình thức) là danh sách
biến có thể sử dụng trong phương thức.
Ở giai đoạn định nghĩa phương thức, chúng ta không biết giá trị cụ thể của
các biến này mà chỉ có thể sử dụng tên biến trong các lệnh ở thân phương
thức.
Ở giai đoạn gọi phương thức người sử dụng phương thức mới cung cấp các
giá trị cụ thể (gọi là tham số thực).
Vì lý do này, ở giai đoạn định nghĩa thường phải kiểm tra hết các tình huống
có thể xảy ra với tham số hình thức.
Danh sách tham số được định nghĩa theo quy tắc sau:
(<kiểu_1> <tham_số_1>, <kiểu_2> <tham_số_2, …)
Hình dung một cách đơn giản, danh sách tham số chính là một chuỗi khai
báo biến cục bộ viết tách nhau bởi dấu phẩy. Do đó, mỗi tham số đều tuân
thủ quy tắc khai báo:
<kiểu_dữ_liệu> <tên_biến>
Danh sách tham số không bắt buộc phải có trong khai báo phương thức.
Nếu danh sách tham số trống, ta chỉ cần viết cặp dấu ngoặc tròn sau tên
phương thức.

144
Gọi phương thức trong C#
Phương thức trong C# chỉ có thể khai báo là thành viên của một class hoặc
struct nào đó (trừ phương thức lambda, phương thức vô danh và hàm cục
bộ sẽ xem xét sau). Khai báo phương thức trong C# không thể nằm trực
tiếp trong namespace, không thể nằm ngoài namespace, cũng không thể
nằm ngoài class/struct.
Ở giai đoạn sử dụng (gọi phương thức), người dùng cung cấp giá trị đầu vào
thực (nếu có) và nhận giá trị trả về (nếu có) qua “lời gọi phương thức” theo
cấu trúc:
<tên_phương_thức>([biến_1, biến_2, …]);
Trong đó biến 1, biến 2,… phải có kiểu theo đúng trật tự như khi khi định
nghĩa phương thức. Danh sách biến cung cấp cho lời gọi phương thức gọi là
các tham số thực (để phân biệt với danh sách tham số hình thức khi khai
báo phương thức).
Trong phần tiếp theo chúng ta sẽ xem xét thêm một số vấn đề khác của
phương thức (như truyền tham biến/tham trị, tham số ra, danh sách tham
số biến đổi, giá trị tham số mặc định,…).

Tham số của phương thức trong C#: value type và reference


type
Như bạn đã biết, các kiểu dữ liệu của C# chia làm hai loại: value type và
reference type. Object của value type nằm trong stack, còn object của
reference type nằm trong heap. Hai loại kiểu này biểu hiện khác nhau khi
sử dụng trong tham số của phương thức.
Truyền kiểu giá trị
Khi truyền một biến thuộc kiểu giá trị cho một phương thức, một bản sao
của biến này được tạo ra trong stack của phương thức được gọi. Tất cả các
thao tác mà phương thức thực hiện trên tham số này đều chỉ tác động trên
bản sao.
Do đó, sau khi kết thúc phương thức, giá trị của biến tham số vẫn giữ
nguyên như trước khi truyền vào phương thức. Tức là những thay đổi (nếu
có) của biến trong phương thức không được giữ lại.
Ví dụ, nếu truyền giá trị i = 100 cho phương thức, một bản sao của i được
tạo ra và truyền vào phương thức. Những gì thay đổi trong thức thực chất
đều tác động lên bản sao của i, chứ không phải chính i. Do vậy, khi kết thúc
phương thức, bản sao bị hủy bỏ, còn i không thay đổi gì.

145
Truyền kiểu tham chiếu, từ khóa ref
Khi truyền một biến thuộc kiểu tham chiếu, bản thân địa chỉ của vùng heap
nơi lưu giá trị đó được truyền vào cho phương thức. Tất cả những thao tác
trên biến tham số đó thực chất đều tác động thẳng lên giá trị nằm trong
heap. Vì vậy, sau khi kết thúc phương thức, những thay đổi này vẫn được
lưu lại.
Từ khóa ref cho phép truyền một biến thuộc kiểu giá trị nhưng có thể lưu
giữ thay đổi như khi sử dụng biến thuộc kiểu tham chiếu. Khi khai báo
phương thức, nếu tham số truyền vào thuộc kiểu giá trị nhưng cần phải giữ
lại những thay đổi thực hiện trong thân phương thức, C# cho phép sử dụng
từ khóa ref trước khai báo tham số đó.
Ví dụ về truyền tham số cho phương thức
Hãy cùng thực hiện ví dụ sau và đọc kỹ comment để hiểu rõ sự khác biệt
giữa tham số kiểu giá trị và kiểu tham chiếu, cũng như tác dụng của từ
khóa ref.
Tạo một blank solution S07_Methods và thêm vào project P01_Parameters.
Viết code cho Program.cs như sau:
using System;
namespace P01_Parameters
{
internal class Data
{
public int Id { get; set; }
public string Name { get; set; }
}
internal class ParameterPassingTest
{
// truyền tham số kiểu value
public void MethodWithValueType(int a)
{
a += 10; // thay đổi giá trị tham số
}
// truyền tham số kiểu reference
public void MethodWithReferenceType(Data s)
{
// thay đổi giá trị tham số
s.Name += " Edited";
s.Id += 10;
}
public void Method2WithReferenceType(Data s)
{
// khởi tạo object khác cho s,
// tương đương với việc cho s tham chiếu sang vùng nhớ khác
s = new Data { Id = 2, Name = "Donald Trump" };
}
// sử dụng từ khóa ref cho kiểu value
public void Method1WithRefKeyword(ref int a)

146
{
a += 10;
}
// sử dụng từ khóa ref cho kiểu reference
public void Method2WithRefKeyword(ref Data s1, ref Data s2)
{
// chỉ đổi giá trị
s1.Id += 10; s1.Name += " Edited";
// đổi thành một object khác (thay đổi địa chỉ tham chiếu tới)
s2 = new Data { Id = 100, Name = "Donald Trump" };
}
}
internal class Program
{
private static void Main()
{
ParameterPassingTest test = new ParameterPassingTest();
int a = 0;
test.MethodWithValueType(a);
Console.WriteLine(a); // a = 0, không thay đổi, vì a là biến kiểu value
Data d = new Data { Id = 0, Name = "Hello world" };
test.MethodWithReferenceType(d);
// Id = 10, Name = Hello world Edited, giá trị thay đổi vì d thuộc kiểu
reference
Console.WriteLine($"{d.Id}, {d.Name}");
// phương thức này lại không làm thay đổi d
// d vấn giữ giá trị Id = 10, Name = Hello world Edited
test.Method2WithReferenceType(d);
Console.WriteLine($"{d.Id}, {d.Name}"); // Id = 10, Name = Hello world
Edited
// như vậy, địa chỉ d trỏ tới
không đổi
test.Method1WithRefKeyword(ref a);
Console.WriteLine(a); // a = 10, đã thay đổi, vì từ khóa ref
Data d2 = new Data { Id = 1, Name = "Barrack Obama" };
test.Method2WithRefKeyword(ref d, ref d2);
// d chỉ thay đổi giá trị, giống trường hợp trên
Console.WriteLine($"{d.Id}, {d.Name}"); // Id = 10, Name = Hello world
Edited
// d2 trỏ sang object khác
Console.WriteLine($"{d2.Id}, {d2.Name}"); // Id = 100, Name = Donald Trump
// như vậy, từ khóa ref cho phép kiểu tham chiếu thay đổi cả địa chỉ vùng
nhớ trỏ tới
Console.ReadKey();
}
}
}
Qua ví dụ trên chúng ta thấy từ khóa ref giúp chúng ta truyền tham số
thuộc kiểu value nhưng lại có thể giữ lại những thay đổi đã thực hiện trong
phương thức.
Đối với kiểu reference, nếu trong thân phương thức chỉ thay đổi giá trị các
thành viên của biến tham số thì những thay đổi này sẽ được lưu lại sau khi
kết thúc phương thức.

147
Tuy nhiên, nếu trong thân phương thức chúng ta thay đổi địa chỉ biến đó
trỏ tới (ví dụ, bằng lệnh khởi tạo object mới) thì sự thay đổi này lại không
được lưu giữ. Lý do là vì địa chỉ của một vùng nhớ cũng có thể xem là một
dạng biến value (thực chất địa chỉ thuộc kiểu số nguyên), do đó không thể
thay đổi.
Khi sử dụng từ khóa ref với kiểu reference, chúng ta có thể đổi cả địa chỉ
mà biến đó trỏ tới.
Qua ví dụ trên chúng ta cũng lưu ý, nếu trong khai báo phương thức sử
dụng từ khóa ref trước tham số nào thì khi gọi phương thức cũng phải sử
dụng từ khóa ref trước tham số tương ứng.

Tham số out
Tham số out là gì?
Qua các phần trên bạn có thể thấy, mỗi phương thức có thể nhận một danh
sách biến hình thức cung cấp thông tin đầu vào cho phương thức. Các
biến này sẽ nhận giá trị thực khi gọi phương thức. Đây là cách sử dụng mặc
định của tham số. Tham số này cũng được gọi làm tham số vào.
Kết quả thực hiện của phương thức được trả về thông qua lời gọi phương
thức. Bình thường phương thức chỉ có thể trả về một giá trị thông qua lời
gọi phương thức.
Có trường hợp ta muốn nhận nhiều hơn một giá trị từ việc thực hiện phương
thức. Hãy cùng thử một phương thức đặc biệt: TryParse. TryParse là một
phương thức gặp trong hầu hết các struct cơ bản như int, bool. Nó có
nhiệm vụ biến đổi chuỗi về kiểu dữ liệu tương ứng.
> string input = "12345";
> if(int.TryParse(input, out int i))
. {
. Console.WriteLine("Success!");
. Console.WriteLine(i++);
. }
Success!
12345

Phương thức TryParse sẽ thử chuyển đổi chuỗi input sang kiểu đích. Nếu
chuỗi hợp lệ và chuyển đổi thành công phương thức sẽ trả về giá trị true;
nếu bị lỗi, phương thức sẽ trả về giá trị false. Giá trị số nguyên kết quả của
việc biến đổi này sẽ được gán cho biến i kiểu int. Như vậy, tham số thứ hai
của TryParse giờ không cung cấp dữ liệu đầu vào, mà trở thành nơi chứa
dữ liệu đầu ra của phương thức.
148
Việc gọi phương thức TryParse như vậy giúp người lập trình kiểm tra được
kết quả thực hiện mà không bị lỗi dừng chương trình. Nó hoạt động tốt hơn
nhiều so với phương thức Parse với cùng chức năng.
C# cung cấp một tính năng đặc biệt gọi là tham số ra (out parameter): nếu
trước một tham số trong định nghĩa phương thức đặt từ khóa out, tham số
đó có thể giữ lại giá trị nó có được trong quá trình thực hiện phương thức,
và qua đó có thể dùng để chứa kết quả thực hiện của các lệnh trong thân
phương thức.
Khai báo phương thức với tham số out
Cùng xem xét ví dụ sau đây để hiểu rõ hơn về cách định nghĩa và sử dụng
của tham số out:
using System;
namespace P02_OutParam
{
internal class Program
{
/// <summary>
/// Thực hiện 3 phép toán trong cùng một phương thức
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="sum">tổng (tham số ra)</param>
/// <param name="product">tích (tham số ra)</param>
/// <param name="div">thương (tham số ra)</param>
/// <returns>true nếu b != 0, false nếu b == 0 (không thực hiện được phép
chia)</returns>
private static bool DoMath(int a, int b, out int sum, out int product, out
float div)
{
sum = a + b;
product = a * b;
if (b == 0)
{
div = float.NaN;
return false;
}
div = a / b;
return true;
}
private static void Main(string[] args)
{
int sum, product;
float div;
// người dùng nhập a, b từ bàn phím và biến đổi kiểu thành int
int a = int.Parse(Console.ReadLine());
int b = int.Parse(Console.ReadLine());
// gọi phương thức DoMath,
// kết quả hiển thị phụ thuộc giá trị thu được khi gọi phương thức
bool result = DoMath(a, b, out sum, out product, out div);
Console.WriteLine($"Sum = {sum}");
Console.WriteLine($"Product = {product}");

149
if (result == true)
{
// nếu phép chia không có lỗi thì in kết quả
Console.WriteLine($"Division = {div}");
}
else
{
// nếu phép chia có lỗi thì báo "chia cho 0"
Console.WriteLine("Division by zero!!!!!!");
}
Console.ReadKey();
}
}
}
Khi sử dụng tham số ra có một số vấn đề sau cần lưu ý:
1. Tham số nào được xác định là tham số ra thì trước khi gọi phương
thức phải khai báo biến tương ứng. Biến này sẽ được truyền vào cho
phương thức và sẽ lưu lại kết quả sau khi phương thức thực hiện xong.
2. Phải dùng từ khóa out cho cả giai đoạn định nghĩa và giai đoạn gọi
phương thức.
3. Tham số ra bắt buộc phải được gán giá trị trong thân phương thức.
4. Biến được khai báo là tham số ra sẽ không bắt buộc phải gán giá trị
trước.
C# 7 cho phép khai báo và truyền tham số out trực tiếp trong lời gọi phương
thức. Trong ví dụ trên, từ C# 7 bạn có thể thực hiện lời gọi sau đây:
bool result = DoMath(a, b, out int sum, out int product, out float div);
Lời gọi phương thức này sẽ thực hiện khai báo luôn biến sum, product và
div. Sau khi kết thúc phương thức DoMath, các biến này sẽ nhận được giá
trị từ thân phương thức. Bạn không cần khai báo riêng rẽ các biến này trước
khi gọi phương thức. Thay đổi này giúp việc gọi các phương thức có tham
số out đơn giản hơn rất nhiều.

Tham số tùy chọn


Trước hết hãy cùng thực hiện một ví dụ.
using System;
namespace P04_OptionalParameter
{
internal class ConsoleHelper
{
/// <summary>
/// Xuất thông tin ra console với màu sắc (WriteLine có màu)
/// </summary>
/// <param name="message"></param>
/// <param name="bgColor"></param>
/// <param name="fgColor"></param>
/// <param name="resetColor"></param>

150
public void WriteLine(object message, ConsoleColor bgColor =
ConsoleColor.Black, ConsoleColor fgColor =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = fgColor;
Console.BackgroundColor = bgColor;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}
}
internal class Program
{
private static void Main(string[] args)
{
Console.Title = "Optional parameters";
var helper = new ConsoleHelper();
helper.WriteLine("Hello world from C#");
helper.WriteLine("Hello world from C#", ConsoleColor.Cyan);
helper.WriteLine("Hello world from C#", ConsoleColor.Cyan,
ConsoleColor.Magenta);
Console.ReadKey();
}
}
}
Trong ví dụ trên trên chúng ta xây dựng phương thức WriteLine với danh
sách tham số có chút khác biệt với những phương thức bình thường. Trong
hai phương thức này, tham số bgColor, grColor và resetColor được gán
sẵn giá trị: bgColor = ConsoleColor.Black, fgColor = White và
resetColor = true.
Tính năng này của C# được gọi là tham số với giá trị mặc định hoặc tham
số không bắt buộc hoặc tham số tùy chọn (Optional Arguments hoặc
Optional Parameters).
Tham số tùy chọn là loại tham số đã được gán sẵn giá trị mặc định khi định
nghĩa phương thức, và do đó khi gọi phương thức có thể bỏ qua việc truyền
tham số này.
Tham số tùy chọn không có gì khác biệt với tham số bình thường khi sử
dụng trong thân phương thức. Tuy nhiên, C# bắt buộc các tham số tùy
chọn phải nằm cuối cùng trong danh sách tham số.
Khi gọi phương thức, nếu không cần truyền giá trị khác với giá trị mặc định,
có thể bỏ qua tham số tùy chọn này .
Các phương thức Write và WriteLine ở trên mặc dù được định nghĩa với 3
tham số vào nhưng hai tham số sau là tham số tùy chọn: môt tham số nhận
màu sắc mặc định là White; một tham số nhận giá trị mặc định là true.
Do đó, khi gọi các phương thức này trong Main, chúng ta chỉ cung cấp giá
trị cho tham số color (do chúng ta muốn viết ra chữ màu Magenta, khác với
151
màu White mặc định) nhưng không cung cấp giá trị cho tham số thứ 3 (vì
vẫn muốn dùng giá trị true, vốn là giá trị có sẵn của tham số tùy chọn này).

Cách Visual Studio hiển thị thông tin hỗ trợ của tham số tùy chọn.
Tham số params
Bình thường, danh sách tham số của phương thức là cố định. Nó có nghĩa
là, nếu bạn khai báo phương thức với, giả sử, 3 tham số, khi gọi phương
thức, bạn phải cung cấp đúng 3 tham số theo đúng thứ tự về kiểu.
Giờ hãy nghĩ một tình huống khác. Bạn cần viết một phương thức để cộng
các số. Nếu bạn cần cộng một số lượng không giới hạn số thì phải làm sao?
Rõ ràng, cách thức sử dụng danh sách tham số bình thường không làm được
việc này.
C# cung cấp khả năng viết phương thức mà có thể tiếp nhận số lượng không
hạn chế tham số. Hãy cùng xem một ví dụ.
Tạo project mới trong solution và đặt tên là P03_Params. Viết code cho
Program.cs như sau:
using static System.Console;
namespace P03_Params
{
internal class Math
{
public double Sum(params double[] operands)
{
var sum = 0.0;
foreach (var o in operands)
{
sum += o;
}
return sum;
}
public double Product(params double[] operands)
{
var product = 1.0;
foreach (var o in operands)
{
product *= o;
}
return product;
}
}
class Message
{
public string Greeting(params string[] message)

152
{
var greeting = string.Join(" ", message);
return greeting;
}
}
internal class Program
{
private static void Main(string[] args)
{
Title = "params";
var math = new Math();
var sum1 = math.Sum(1, 2, 3, 4, 5, 6);
var sum2 = math.Sum(1, 2, 3);
var product1 = math.Product(7, 8, 9, 10);
var product2 = math.Product(4, 5, 6);
WriteLine($"Sum(1, 2, 3, 4, 5) = {sum1}");
WriteLine($"Sum(1, 2, 3) = {sum2}");
WriteLine($"Product(4, 5, 6) = {product2}");
var msg = new Message();
var greeting1 = msg.Greeting("Hello", "world", "from", "C#");
var greeting2 = msg.Greeting("Hi", "this", "is", "params", "method");
WriteLine(greeting1);
WriteLine(greeting2);
ReadKey();
}
}
}
Trong ví dụ này, bạn đã tạo một class Math với hai phương thức Sum và
Product, class Message với phương thức Greeting.
Điều đặc biệt của các phương thức này nằm ở danh sách tham số. Tất cả
chúng đều có cùng cú pháp:
(params <type>[] <name>)
Ví dụ cụ thể là:
double Sum(params double[] operands)
double Product(params double[] operands)
string Greeting(params string[] messages)
Đây là cách khai báo của loại tham số đặc biệt: tham số params. Cách khai
báo tham số này cho phép bạn cung cấp số lượng không hạn chế tham số
(thực) cùng loại khi gọi phương thức:
var sum1 = math.Sum(1, 2, 3, 4, 5, 6);
var sum2 = math.Sum(1, 2, 3);
var product1 = math.Product(7, 8, 9, 10);
var product2 = math.Product(4, 5, 6);
var greeting1 = msg.Greeting("Hello", "world", "from", "C#");
var greeting2 = msg.Greeting("Hi", "this", "is", "params", "method");
Trong thân phương thức, bạn có thể sử dụng loại tham số này như một
mảng một chiều.
foreach (var o in operands)

153
{
sum += o;
}
Nạp chồng phương thức (method overloading) trong C#
Nạp chồng phương thức trong C# là gì?
Nạp chồng phương thức (method overloading) là hiện tượng trong một class
có thể tồn tại nhiều phương thức trùng tên.
Nạp chồng phương thức cùng với nạp chồng toán tử (operator overloading) thuộc về nguyên
lý đa hình tĩnh (static polymorphism).
Ví dụ phương thức WriteLine của lớp Console có 19 overload khác nhau:

19 overload của phương thức WriteLine


Mỗi overload này nhận một danh sách tham số khác nhau. Intellisense của
Visual Studio hiển thị tất cả các overload của một phương thức vào một
danh sách như trên. Bạn có thể dùng phím mũi tên Up và Down để duyệt
qua danh sách này. Ứng với mỗi overload sẽ cung cấp thông tin chi tiết
riêng.
Khi gặp hiện tượng nạp chồng phương thức, trình biên dịch của C# sẽ căn
cứ vào danh sách tham số thực của lời gọi hàm để quyết định xem người
lập trình đang muốn gọi phương thức nào.
Vì vậy, các phương thức nạp chồng bắt buộc phải khác nhau về danh sách
tham số. Nói một cách chính xác hơn, các phương thức nạp chồng trong C#
bắt buộc phải khác nhau về signature. Ngược lại, C# compiler sẽ báo lỗi
định nghĩa phương thức trùng nhau.
Tiếp theo đây bạn sẽ biết signature của phương thức là gì.
Signature của phương thức trong C#
Signature của phương thức trong C# bao gồm các thông tin sau:
 Tên của phương thức
 Số lượng tham số
 Kiểu và trật tự của các tham số
 Các từ khóa điều khiển cho tham số (out, ref, in)

154
Kiểu dữ liệu trả về không thuộc về signature của phương thức. Tên của
tham số hình thức cũng không được tính vào signature của phương thức.
Rất nhiều bạn nhầm lẫn hai vấn đề này.

C# bắt buộc trong cùng một class không được phép có hai phương thức
trùng nhau về signature.
Trong hiện tượng nạp chồng, tên của phương thức trùng nhau, do đó ít nhất
1 trong 3 yếu tố còn lại phải khác nhau. Cả ba yếu tố này đều liên quan đến
danh sách tham số. Nói cách khác, các phương thức nạp chồng phải có danh
sách tham số khác nhau.
Ví dụ, (string s, int i, bool b) và (string str, int ii, bool bb) là hai danh sách
tham số giống nhau:
1. Cả hai đều có 3 tham số
2. Thứ tự tham số tính theo kiểu đều là (string, int, bool)
3. Tên các tham số không quan trọng
Nói tóm lại, bạn được phép khai báo các phương thức nạp chồng (trùng tên)
nhưng 3 yếu tố còn lại của signature phải khác nhau. Điều này có nghĩa là
các phương thức nạp chồng (trùng tên) phải thỏa mãn ít nhất một trong số
các điều kiện:
 Số lượng tham số khác nhau
 Thứ tự tham số tính theo kiểu (không phải tính theo tên) khác nhau
 Sử dụng modifier khác nhau

155
Một số vấn đề khác của phương thức trong C#
Phương thức với Expression body
Nếu thân phương thức chỉ có một lệnh duy nhất, C# cho phép viết phương
thức đó ở dạng đơn giản hóa, gọi là expression body. Cách viết expression
body loại bỏ cặp dấu {} và lệnh return (nếu có) ở thân phương thức.
Expression body sử dụng toán tử => để ghép tên và thân phương thức.
Hãy cùng xem ví dụ:
public bool IsSquare(Rectangle rect) => rect.Height == rect.Width;
Đây là cách viết ở dạng expression body. Cách viết “truyền thống” của
phương thức trên là:
public bool IsSquare(Rectangle rect)
{
return rect.Height == rect.Width;
}
Như vậy, nếu thân của phương thức (thông thường) chứa đúng 1 lệnh
return, bạn chỉ cần viết biểu thức tính giá trị đó và ghép với tên phương
thức qua dấu =>. C# sẽ tự hiểu cần trả lại giá trị của biểu thức cho lời gọi
phương thức.
Nếu thân phương thức là một lệnh không trả về giá trị (kiểu trả về là void),
bạn cũng chỉ cần viết đúng lệnh đó và ghép với tên phương thức bằng dấu
=>.
Named Arguments
Ở trên bạn đã biết cách gọi phương thức bằng cách cung cấp danh sách giá
trị theo đúng thứ tự về kiểu:
public void MoveAndResize(int x, int y, int width, int height)
{
//...
}

// gọi phương thức


r.MoveAndResize(30, 40, 20, 40);
C# cho phép gọi phương thức theo một cách khác:
r.MoveAndResize(x: 30, y: 40, width: 20, height: 40);
Cách gọi phương thức này có tên là named arguments. Trong cách gọi
này, mỗi tham số được truyền bằng cách viết tên tham số (đặt khi khai báo
phương thức), dấu hai chấm và giá trị. Cách gọi này không cần quan tâm
về thứ tự viết tham số.
Cách gọi này rất hữu ích nếu phương thức có nhiều tham số hoặc khi sử
dụng kết hợp với tham số tùy chọn.

156
Property (đặc tính) trong C#: full|auto property
Property (đặc tính) là một loại thành viên đặc biệt trong class C#. Property
trong C# có nhiệm vụ hỗ trợ xuất nhập dữ liệu cho biến thành viên hoặc
trực tiếp lưu trữ dữ liệu. Trong C#, property được sử dụng đặc biệt phổ biến
do nó có thể kiểm soát được việc đọc/ghi dữ liệu. Ngoài ra, property được
sử dụng trong khởi tạo object của class. Một số thư viện class (như của
Windows forms) hoàn toàn sử dụng property.
Bài học sẽ giúp bạn hiểu chi tiết về loại thành viên này của class. Nắm vững
cách sử dụng Property bạn mới có thể xây dựng được class “theo kiểu C#”.

Property trong C# là gì?


Trong bài học về biến thành viên bạn đã biết rằng có thể sử dụng biến public
để lưu trữ và truy xuất dữ liệu cho class. Tuy nhiên, class xây dựng với biến
thành viên public có một số nhược điểm:
1. Người sử dụng class có thể trực tiếp truy xuất giá trị của các trường.
Việc trực tiếp truy xuất này là khó chấp nhận nếu chúng ta cần giới
hạn một phần việc truy xuất thông tin. Ví dụ, có trường thông tin
chúng ta chỉ muốn cho đọc mà không cho ghi giá trị.
2. Giá trị nhập vào không được kiểm soát hoặc biến đổi phù hợp. Ví dụ,
năm không thể nhận giá trị âm.
Trong lập trình hướng đối tượng nên hạn chế tối đa việc sử dụng biến thành viên public.
Thông thường, để giải quyết vấn đề này, người lập trình thường xây dựng
một căp phương thức để gán/lấy giá trị cho mỗi biến thành viên. Cặp phương
thức như vậy thường được gọi là setter/getter. Phương pháp này rất phổ
biến khi lập trình PHP hay Java.
Tuy nhiên, cách viết như vậy làm code trở nên dài dòng.
C# cung cấp một công cụ đặc biệt để giải quyết vấn đề này, giúp viết code
class đơn giản hơn, an toàn hơn, cũng như tiện lợi hơn về sau (khi khởi tạo
object của class): property (đặc tính).
Property là một loại thành viên đặc biệt có vai trò lai giữa biến thành
viên (lưu trữ và truy xuất dữ liệu) nhưng cho phép kiểm soát dữ liệu (gán
vào hoặc xuất ra) giống như phương thức, cũng như cho phép kiểm soát
riêng rẽ từng chiều truy xuất. Property được sử dụng đặc biệt phổ biến trong
class C#.
Thực tế, property trong C# chỉ là một dạng viết tắt và viết gộp (syntactic sugar) của hai phương
thức get và set.
Cùng thực hiện ví dụ sau để hiểu cách thức làm việc với property.
157
Tạo blank solution S07_Property và project P00_PropertyWithBackedField.
Viết code cho Program.cs như sau:
Program.cs
namespace P00_PropertyWithBackedField
{
using static System.Console;
/// <summary>
/// sách điện tử
/// </summary>
internal class Book
{
private int _id = 1;
private string _authors = "Unknown author";
private string _title = "A new book";
private string _publisher = "Unknown publisher";
private int _year = 2018;
private string _description;
public int Id
{
get { return _id; }
protected set
{
_id = value;
}
}
/// <summary>
/// tên tác giả/ nhóm tác giả
/// </summary>
public string Authors
{
get { return _authors; }
set
{
_authors = value;
}
}
/// <summary>
/// tiêu đề
/// </summary>
public string Title
{
get { return _title; }
set
{
_title = value;
}
}
/// <summary>
/// nhà xuất bản
/// </summary>
public string Publisher
{
get { return _publisher; }
set
{
_publisher = value;
}
158
}
/// <summary>
/// năm xuất bản
/// </summary>
public int Year
{
get { return _year; }
set
{
_year = value;
}
}
/// <summary>
/// thông tin mô tả
/// </summary>
public string Description
{
get { return _description; }
set
{
_description = value;
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var book = new Book();
// lệnh này lỗi, vì setter của Id là protected
// chỉ có thể gán giá trị cho Id từ trong class
// không thể gán giá trị từ ngoài class
//book.Id = 2;
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever about the new C# 7 and the .NET
Core";
WriteLine($"{book.Authors}, {book.Title}, - {book.Publisher},
{book.Year}");
ReadKey();
}
}
}
Trong class Book bạn đã khai báo (và khởi tạo) một loạt biến thành viên
private (_id, _authors, _title,...). Tiếp theo bạn lại viết một số cấu trúc lạ
như
public string Authors
{
get { return _authors; }
set
{
_authors = value;
}
}

159
Đây chính là cách khai báo property trong C#. Property này có tên là
Authors.
Như đã nói, property trong C# thực chất chỉ là một dạng viết tắt của hai
phương thức get và set. Phương thức get dùng để trả lại giá trị của biến
_authors; Phương thức set dùng để gán giá trị cho biến _authors. Hai
phương thức này hoạt động không có gì khác biệt phương thức thông
thường, ngoại trừ chúng không có danh sách tham số.
Biến _authors có tên gọi là backed field cho property Authors. Đó là nơi
lưu dữ liệu thực sự. Còn property Authors đóng vai trò kiểm soát xuất/nhập
cho biến backed field này.
Từ khóa value có vai trò đặc biệt. Trong client code, phép gán
var book = new Book();
book.Authors = "Christian Nagel";
giá trị “Christian Nagel” cung cấp vào cho property Authors thông qua từ
khóa value. Nói cách khác, bạn có thể xem value là tham số đầu vào của
phương thức get.
Bạn cũng nhìn thấy trong phương thức Main, việc sử dụng các property
không khác biệt gì so với sử dụng biến thành viên public.
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever about the new C# 7 and the .NET
Core";
Riêng đối với Id có chút khác biệt. Do setter của nó đặt là protected, bạn
không gán giá trị cho nó được. Đây là một property chỉ đọc (đối với client
code).

Auto property trong C#


Nếu không cần kiểm soát giá trị xuất/nhập, bạn có thể thu gọn code bằng
cách sử dụng auto-property.
Auto-property là loại property trong đó trường backed field được compiler
sinh tự động. Bạn không biết tên của backed field khi viết code, do đó cũng
không trực tiếp sử dụng được nó.
Cấu trúc khai báo auto-property (kết hợp gán giá trị đầu) trong C# như
sau:
[public|protected|private] <tên-kiểu> <tên-thuộc-tính>
{
[public|protected|private] get;
[public|protected|private] set;

160
} [= <giá-trị>];
Trong đó, tên property được đặt theo quy tắc đặt định danh và quy ước
giống như biến thành viên public (sử dụng PascalCase).
Hai phương thức get và set được gọi chung là accessor, đôi khi cũng được
gọi là getter và setter. Getter hoặc setter có thể sử dụng từ khóa điều khiển
truy cập của riêng mình, giúp property đó biến thành loại:
 chỉ đọc (read-only): public get, protected/private set;
 chỉ gán (assign-only): protected/private get, public set;
 truy cập tự do (full access): public get, public set (mặc định).
Modifier mặc định cho getter và setter là “public”, do đó cấu trúc khai báo
ngắn gọn nhất là:
public <tên-kiểu> <tên-thuộc-tính> { get; set; }
Hãy cùng thực hiện một ví dụ để hiểu rõ cách khai báo và sử dụng của auto
property. Thêm project P01_AutoProperty kiểu Console App vào solution.
Viết code cho Program.cs như sau:
Program.cs
namespace P01_AutoProperty
{
using static System.Console;
/// <summary>
/// sách điện tử
/// </summary>
class Book
{
public int Id { get; protected set; } = 1;
/// <summary>
/// tên tác giả/ nhóm tác giả
/// </summary>
public string Authors { get; set; } = "Unknown author";
/// <summary>
/// tiêu đề
/// </summary>
public string Title { get; set; } = "A new book";
/// <summary>
/// nhà xuất bản
/// </summary>
public string Publisher { get; set; } = "Unknown publisher";
/// <summary>
/// năm xuất bản
/// </summary>
public int Year { get; set; } = 2018;
/// <summary>
/// thông tin mô tả
/// </summary>
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
161
{
var book = new Book();
// lệnh này lỗi, vì setter của Id là protected
// chỉ có thể gán giá trị cho Id từ trong class
// không thể gán giá trị từ ngoài class
//book.Id = 2;
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever about the new C# 7 and the .NET
Core";
WriteLine($"{book.Authors}, {book.Title}, - {book.Publisher},
{book.Year}");
ReadKey();
}
}
}

Trong ví dụ trên bạn đã khai báo một class hoàn toàn sử dụng auto property
mà không có biến thành viên.
Bạn dễ dàng để ý thấy, việc khai báo auto property không khác biệt nhiều
so với biến, ngoại trừ cặp {get; set;} đứng sau tên.
Id, Authors, Title, Publisher và Year khi khai báo được gán sẵn giá trị đầu.
Description không được gán sẵn giá trị nên sẽ nhận giá trị mặc định (null)
lúc khởi tạo object. Tất cả các property này đều có thể tự do truy xuất trong
client code (phương thức Main).
Riêng Id đặc biệt hơn một chút là setter của nó để mức truy cập là protected.
Điều này dẫn tới là trong client code không thể gán giá trị cho Id, nhưng
vẫn có thể đọc giá trị của Id.
Bạn có thể thấy, mỗi property lúc khai báo sẽ chứa hai phương thức get và
set. Tuy nhiên, khi truy xuất qua tên object sẽ chỉ nhìn thấy duy nhất tên
property tương tự như một biến thành viên public bình thường.
Property sử dụng cấu trúc này thường được sử dụng để thay thế cho biến
public. Trong thân phương thức thành viên có thể sử dụng auto property
giống hệt như sử dụng biến thành viên.

Full property trong C#


Full property đủ có cách khai báo khác với auto property. Full property
phải đi kèm với backed field, nơi thực sự lưu trữ thông tin. Property sẽ kiểm
soát giá trị dữ liệu trước khi gán giá trị đó vào biến.
Cấu trúc để khai báo thuộc tính đầy đủ như sau:
[private|protected] <tên-kiểu> <tên-biến-hỗ-trợ>;
[public|protected|private] <tên-kiểu> <tên-thuộc-tính>

162
{
[public|protected|private] get { [thân-phương-thức];
return <tên-biến-hỗ-trợ>;}
[public|protected|private] set { [thân-phương-thức];
<tên-biến-hỗ-trợ> = value;}
}
Khi sử dụng thuộc tính đầy đủ chúng ta gặp từ khóa value. Từ khóa này
được sử dụng như một biến chứa giá trị đang cần gán cho thuộc tính.
Để sử dụng cấu trúc đầy đủ, thông thường mỗi thuộc tính sẽ được tạo ra
cùng một biến thành viên private. Biến private lưu trữ thông tin, property
làm nhiệm vụ kiểm soát thông tin cho biến này.
Phương thức get sẽ trả giá trị của biến qua tên property; phương thức set
cho phép property nhận giá trị và gán lại vào biến. Biến thành viên private
này được gọi là trường/biến hỗ trợ (backed field).
Để dễ hiểu, hãy cùng thực hiện một ví dụ. Thêm project P02_FullProperty
kiểu ConsoleApp vào solution và viết code như sau:
Program.cs
namespace P02_FullProperty
{
using static System.Console;
/// <summary>
/// sách điện tử
/// </summary>
internal class Book
{
public int Id { get; protected set; } = 1;
private string _authors = "Unknown author";
private string _title = "A new book";
private int _year = 2018;
/// <summary>
/// tên tác giả/ nhóm tác giả
/// </summary>
public string Authors
{
get { return _authors; }
set { if (!string.IsNullOrEmpty(value)) { _authors = value; } }
}
/// <summary>
/// tiêu đề
/// </summary>
public string Title
{
get { return _title; }
set { if (!string.IsNullOrEmpty(value)) { _title = value; } }
}
/// <summary>
/// nhà xuất bản
/// </summary>
public string Publisher { get; set; } = "Unknown publisher";

163
/// <summary>
/// năm xuất bản
/// </summary>
public int Year
{
get { return _year; }
set { if (value > 0) _year = value; }
}
/// <summary>
/// thông tin mô tả
/// </summary>
public string Description { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
var book = new Book();
// lệnh này lỗi, vì setter của Id là protected
// chỉ có thể gán giá trị cho Id từ trong class
// không thể gán giá trị từ ngoài class
//book.Id = 2;
book.Authors = "Christian Nagel";
book.Title = "Professional C# 7 and .NET Core";
book.Publisher = "Wrox";
book.Year = 2018;
book.Description = "The best book ever about the new C# 7 and the .NET
Core";
WriteLine($"{book.Authors}, {book.Title}, - {book.Publisher},
{book.Year}");
ReadKey();
}
}
}
Trong ví dụ trên chúng ta đã điều chỉnh lớp Book để sử dụng full property.
Hãy để ý các trường Authors, Title và Year. Đây là 3 full property.
Ví dụ, property Authors giờ đây phải sử dụng kết hợp với biến backed field
_authors. Đây là nơi thực sự lưu giữ thông tin. Property Authors giờ đóng
vai trò xuất/nhập/kiểm soát dữ liệu cho biến _authors. Nhìn từ client code
thì sẽ không thấy _authors mà chỉ thấy Authors duy nhất. Thông qua
Authors có thể đọc thông tin. Tuy nhiên, nếu gán chuỗi trống hoặc chuỗi
null cho Authors thì giá trị này không được gán cho _authors. Setter của
Authors kiểm soát việc gán thông tin này.
Full property không thể gán giá trị đầu như auto-property mà chỉ có thể gán
giá trị đầu cho biến backed field.
Tình huống tương tự cũng diễn ra với Title: không chấp nhận giá trị rỗng
hoặc null. Đối với Year: không chấp nhận giá trị âm.
Như vậy, full property nên sử dụng cho các trường thông tin cần kiểm soát
dữ liệu, và sử dụng auto-property cho các trường thông tin cho phép truy
xuất tự do.
164
Trong thân phương thức thành viên có thể sử dụng full property hoặc biến
backed field cho cùng mục đích. Hai cách sử dụng này không có gì khác
biệt.
Thực tế, auto-property chỉ là một dạng viết tắt của full-property, trong đó biến backed field
được sinh tự động.
Để cho ngắn gọn, nếu thân của getter hoặc setter chỉ có 1 lệnh duy nhất
thì có thể sử dụng lối viết expression body như sau:
public string Authors
{
get => _authors;// expression body, tương đương với get { return
_authors;}
set { if (!string.IsNullOrEmpty(value)) { _authors = value; } }
}

Lưu ý về sử dụng property trong class C#


Property là một hoặc một tổ hợp phương thức giúp xuất nhập dữ liệu cho
một biến thành viên cụ thể.
Trên thực tế, bạn có thể xem property là phương thức thông thường. Chỉ có
điều C# thay đổi cú pháp đi một chút để tiện lợi hơn khi sử dụng, giúp bạn
dùng property như là dùng một biến.
Nếu tất cả các biến thành viên của class được sử dụng hoàn toàn cục bộ
(không cần tương tác với bên ngoài), bạn không cần đến property.
Nếu biến chỉ cần trả giá trị (để bên ngoài object có thể sử dụng), bạn cần
tạo ra phương thức getter cho nó – chính là tạo ra readonly property.
Nếu biến chỉ cần nhận giá trị (tức là cho phép code bên ngoài class gán giá
trị cho biến), bạn phải tạo ra setter cho nó – chính là tạo ra write-only
property.
Nếu cần xuất nhập (cả hai chiều), bạn cần tạo ra đủ bộ getter và setter.
Nếu trong quá trình xuất nhập không cần kiểm soát biến (tức là cho xuất
nhập tự do), bạn dùng auto property cho nhanh gọn (đỡ mất công code, lại
có thể dùng code snippet prop).
Nếu cần kiểm soát giá trị biến khi nhập/xuất, khi đó bạn cần dùng full-
property.
Auto property thực chất cũng là full-property. Tuy nhiên, khi này C#
compiler tự động giúp bạn sinh ra backing field. Còn trong full-property,
backing field là do bạn tự tạo ra và kiểm soát.

165
Hỗ trợ property trong Visual Studio
Code snippet
Để tăng tốc độ code, Visual Studio cung cấp một tính năng gọi là “code
snippet”. Code snippet là một khối code mẫu được xây dựng sẵn mà người
lập trình có thể gọi ra thông qua một cụm ký tự viết tắt.
Để nhanh chóng tạo ra một auto property có thể sử dụng snippet prop:
nhập cụm prop => trong danh sách lựa chọn của Visual Studio IntelliSense
xuất hiện mục prop => chọn mục này và bấm phím Tab hai lần.
Trong snippet vừa tạo chỉnh sửa các thông tin cần thiết (phần code được
bôi vàng). Di chuyển giữa các vùng bôi vàng bằng phím Tab. Kết thúc chỉnh
sửa snippet bằng phím Enter.

Sử dụng snippet prop


Tương tự, để tạo ra full property có thể dùng snippet propfull.

Kết quả thực hiện của snippet “propfull”


Quick Action
Tính năng Quick Action của Visual Studio giúp thực hiện tự động nhiều loại
công việc khác nhau trong quá trình code.
Để sử dụng tính năng Quick Action có hai cách khác nhau:
1. Đặt con trỏ chuột vào đối tượng (trong trường hợp này là tên của
property), ấn tổ hợp Ctrl + . (dấu chấm). Một menu sẽ xuất hiện ở rìa
trái trình soạn thảo. Trong menu này chỉ chứa các lệnh khả dụng trong
ngữ cảnh.

166
2. Click vào biểu tượng bóng đèn hoặc tô-vít xuất hiện khi đặt con trỏ
vào đối tượng nào đó. Click biểu tượng này tương đương với tổ
hợp Ctrl + . (dấu chấm).
Chuyển đổi giữa full và auto property
Một trong những công việc đầu tiên chúng ta có thể vận dụng Quick Action
là chuyển đổi từ auto-property sang full property và ngược lại.
Đặt con trỏ văn bản vào property cần điều chỉnh và kích hoạt Quick Action
(dùng tổ hợp Ctrl + . hoặc bấm nút tương ứng). Trong menu Quick Action
sẽ xuất hiện lệnh chuyển đổi tương ứng.

Sử dụng Quick Action trong Visual Studio


Kết luận
Bài học này đã giới thiệu với bạn tương đối chi tiết về cách sử dụng Property
trong C#.
Thực chất, property là một hoặc một tổ hợp phương thức giúp xuất nhập dữ
liệu cho một biến thành viên cụ thể. Trên thực tế, bạn có thể xem property
là phương thức thông thường. Chỉ có điều C# thay đổi cú pháp đi một chút
để tiện lợi hơn khi sử dụng, giúp bạn dùng property như là dùng một biến.
Cần lưu ý rằng, nếu trong class có dữ liệu cần tương tác với bên ngoài,
property là lựa chọn tốt nhất. Hạn chế tối đa sử dụng biến thành viên public.

167
Constructor (phương thức khởi tạo/phương thức khởi
tạo) trong C#, khởi tạo object
Constructor (phương thức khởi tạo/ phương thức khởi tạo) là một loại thành
viên đặc biệt trong class C#. Nhiệm vụ của nó là khởi tạo object của class.
Mỗi khi gọi lệnh khởi tạo, thực tế là bạn đang gọi tới constructor.
Bài học này sẽ hướng dẫn bạn cách viết phương thức khởi tạo khi xây dựng
class và một vài cách khởi tạo đối tượng cho class trong C#.

Constructor trong C# và khởi tạo object


Các class bạn xây dựng trong các bài học trước tự bản thân nó không có
nhiều giá trị với chương trình bởi vì class chỉ đơn thuần là mô tả kiểu dữ
liệu. Để sử dụng class trong chương trình C#, bạn cần khởi tạo đối tượng
của nó.
Khởi tạo đối tượng trong C# là quá trình yêu cầu tạo ra một object của class
tương ứng trên vùng nhớ heap và lấy địa chỉ của object gán cho một biến.
Sau khi object được khởi tạo, bạn có thể truy xuất các thành viên của nó để
phục vụ cho mục đích của chương trình.
Để khởi tạo object trong C# sử dụng từ khóa new và lời gọi tới một trong số
các phương thức khởi tạo (constructor) của class tương tự như đối
với struct.
Xây dựng constructor cho class C#
Phương thức khởi tạo, về mặt hình thức, luôn có cùng tên với class và không
có kiểu trả về. Danh sách tham số và thân hàm tương tự như các phương
thức thành viên.
Hãy cùng thực hiện ví dụ sau để hiểu cách xây dựng phương thức khởi tạo
của class.
Tạo một blank solution S08_ObjectInstantiation rồi thêm project
P01_DefaultConstructor. Viết code cho Program.cs như sau:
Program.cs
namespace P01_DefaultConstructor
{
using static System.Console;
internal class Book
{
private string _authors;
private string _title;
private int _year;
private string _publisher;
public Book() // đây là một phương thức khởi tạo của class Book

168
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
_year = 2019;
}
public Book(string author, string title, int year, string publisher) // đây là
phương thức khởi tạo có tham số
{
_authors = author;
_title = title;
_year = year;
_publisher = publisher;
}
public string Print()
{
return $"{_authors}, \"{_title}\", -{_publisher}, {_year}";
}
}
internal class Program
{
private static void Main(string[] args)
{
ReadKey();
}
}
}
Trong ví dụ trên bạn đã xây dựng một class Book đơn giản. Trong class này
chỉ có 4 biến thành viên private (_authors, _title, _year, _publisher), 1
phương thức thành viên Print().
Bạn có thể để ý hai thành viên đặc biệt:
public Book() // đây là một phương thức khởi tạo của class Book
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
_year = 2019;
}
public Book(string author, string title, int year, string publisher) // đây là
phương thức khởi tạo có tham số
{
_authors = author;
_title = title;
_year = year;
_publisher = publisher;
}
Đây là hai constructor của class Book. Khi khởi tạo object với lệnh new, thực
tế bạn sẽ gọi tới một trong hai constructor này. Đây cũng là khối code đầu
tiên được thực thi khi khởi tạo object.
Mỗi constructor có thể chứa một access modifier (public, private, protected)
như các thành viên khác. Điều đặc biệt là tên của constructor phải trùng với

169
tên class. Phía sau tên của constructor là danh sách tham số, tương tự như
đối với phương thức.
Với vai trò đó, trong constructor thường đặt các lệnh để khởi tạo giá trị cho
các thành viên (như bạn đã làm).
Trong mỗi class C# không giới hạn số lượng constructor. Tuy nhiên, các
constructor không được phép có danh sách tham số trùng nhau. Nếu có
nhiều constructor trong một class, mỗi constructor được gọi là
một overload (nạp chồng phương thức khởi tạo).
Danh sách tham số được gọi là trùng nhau nếu thứ tự các tham số theo kiểu (không theo tên
gọi) và số lượng tham số giống nhau. Bạn sẽ quay lại vấn đề này khi học về nạp chồng phương
thức và delegate.
Bạn có thể để ý phương thức khởi tạo và phương thức được Visual Studio
hiển thị với cùng một biểu tượng.
Visual Studio sử dụng snippet ctor để sinh đoạn code khung cho phương
thức khởi tạo.
Khởi tạo object với constructor
Bây giờ trong phương thức Main hãy viết một số lệnh như sau:
private static void Main(string[] args)
{

var book1 = new Book();


WriteLine(book1.Print());
var book2 = new Book("Christian Nagel", "Professional C# 7 and the .NET
core 2.0", 2018, "Wrox");
WriteLine(book2.Print());

ReadKey();
}
Trong đó,
var book1 = new Book();
var book2 = new Book("Christian Nagel", "Professional C# 7 and the .NET
core 2.0", 2018, "Wrox");
là hai lệnh khởi tạo object của lớp Book, sử dụng hai constructor đã xây
dựng.
Như vậy có thể thấy, lệnh khởi tạo object cần có từ khóa new và gọi tới một
trong số các constructor của class. Kết quả khởi tạo có thể gán cho một
biến để sau tái sử dụng.

170
Khởi tạo object với property
C# cung cấp một cách khởi tạo object khác: sử dụng bộ khởi tạo (object
initializer). Cú pháp khởi tạo này sử dụng property và được đưa vào từ C#
3 (.NET Framework 3.5).
Hãy cùng thực hiện một ví dụ trước.
Thêm các property sau vào class Book:
public string Authors { get => _authors; set => _authors = value; }
public string Title { get => _title; set => _title = value; }
public int Year { get => _year; set => _year = value; }
Trong phương thức Main bổ sung các lệnh sau:
var book3 = new Book
{
Authors = "Christian Nagel",
Title = "Professional C# 7 and the .NET core 2.0",
Year = 2018
};
Console.WriteLine(book3.Print());
Đây là cách khởi tạo với object initializer sử dụng property.
Trong cách khởi tạo này, chúng ta vẫn sử dụng lời gọi tới phương thức khởi
tạo như bình thường, tuy nhiên, chúng ta có thể kết hợp luôn việc gán giá
trị cho các property trong cùng lệnh khởi tạo theo quy tắc:
 Tất cả lệnh gán giá trị cho property phải đặt trong cặp dấu ngoặc
nhọn
 Mỗi lệnh gán viết tách nhau bởi một dấu phẩy
 Phải kết thúc bằng dấu chấm phẩy, vì đây thực chất là một lệnh,
không phải một khối lệnh (code block) như bình thường
 Không bắt buộc phải gán giá trị cho tất cả các thuộc tính
Cách thức khởi tạo này đặc biệt phù hợp với các class chứa dữ liệu sử dụng
property cũng như khởi tạo danh sách. Hãy tưởng tượng nếu không có cách
thức khởi tạo này, phương thức khởi tạo phải có rất nhiều tham số đầu vào
để có thể gán giá trị cho tất cả các thành viên. Một phương thức khởi tạo
với danh sách tham số quá dài nhìn rất cồng kềnh, khó nhớ thứ tự các tham
số, cũng như dễ gây lỗi khi truyền tham số.
Nếu sử dụng phương thức khởi tạo mặc định hoặc phương thức khởi tạo
không tham số có thể bỏ cả cặp dấu ngoặc tròn sau tên constructor.

171
Một số vấn đề khác với constructor trong C#
Default constructor trong class C#
Phương thức khởi tạo là bắt buộc khi định nghĩa class. Tuy nhiên chương
trình dịch của C# có khả năng tự sinh phương thức khởi tạo cho class
nếu nó không nhìn thấy định nghĩa phương thức khởi tạo nào trong class.
Loại phương thức khởi tạo này có tên gọi là phương thức khởi tạo mặc
định (default constructor). Phương thức khởi tạo mặc định không có tham
số đầu vào.
Nếu trong khai báo class chúng ta tự viết một phương thức khởi tạo không
có tham số đầu vào, phương thức khởi tạo này không được gọi là phương
thức khởi tạo mặc định nữa mà được gọi là phương thức khởi tạo không
tham số (parameter-less/zero-parameter constructor), vì nó không phải
do chương trình dịch của C# sinh ra.
Trong ví dụ trên, public Book() {...} là một phương thức khởi tạo không
tham số nhưng nó không phải là phương thức khởi tạo mặc định.
Một khi đã định nghĩa phương thức khởi tạo riêng trong class, C# compiler
sẽ không tự sinh ra phương thức khởi tạo mặc định nữa. Nghĩa là nếu bạn
muốn gọi phương thức khởi tạo không tham số, bạn phải tự viết thêm
phương thức khởi tạo đó. Nếu không, quá trình dịch sẽ báo lỗi.
Chuỗi constructor trong class C#, constructor initializer
Hãy điều chỉnh lại class Book như sau:
internal class Book
{
private string _authors = "Unknown author";
private string _title = "A new book";
private int _year = 2019;
private string _publisher = "Unknown publisher";
public Book()
{
_authors = "Unknown author";
_title = "A new book";
_publisher = "Unknown publisher";
_year = 2019;
}
public Book(string author)
{
_authors = author;
}
public Book(string author, string title) : this(author)
{
_title = title;
}
public Book(string author, string title, int year) : this(author, title)
{

172
_year = year;
}
public Book(string author, string title, int year, string publisher) :
this(author, title, year)
{
_publisher = publisher;
}
public string Print()
{
return $"{_authors}, \"{_title}\", -{_publisher}, {_year}";
}
}
Những điều chỉnh trên sử dụng một khả năng đặc biệt của C#: constructor
gọi lẫn nhau. Khi cho các constructor gọi lẫn nhau như trên bạn có thể tạo
ra một chuỗi constructor với số lượng tham số tăng dần, đồng thời tận dụng
được code của constructor xây dựng trước đó.
Cấu trúc : this (...) như trên có tên gọi là constructor initializer, là
loại cấu trúc đặc biệt cho phép gọi đến constructor khác và truyền tham số
phù hợp cho nó.
Trong ví dụ trên, this(author) là lời gọi đến constructor Book(string
author) trước đó; this(author, title) là lời gọi đến Book(string author, string
title);....
Constructor initializer luôn luôn thực thi trước constructor gọi nó. Khi kết
hợp tốt các constructor initializer như trên, bạn có thể tạo ra một chuỗi
constructor với số lượng tham số tăng dần mà không cần viết lặp lại code
của các constructor trước đó.
Khi tạo chuỗi constructor như trên, bạn có thể khởi tạo object với bất kỳ
constructor nào:
var b1 = new Book(); // dùng phương thức khởi tạo không tham số
var b2 = new Book("Donald Trump"); // gọi phương thức khởi tạo Book(string
author)
var b3 = new Book("Donald Trump", "C# for dummy"); // gọi phương thức khởi
tạo Book(string author, string title)
var b4 = new Book("Donald Trump", "C# for dummy", 2020); // gọi phương
thức khởi tạo Book(string author, string title, int year)
Lưu ý: Nếu trong class sử dụng public property thay cho biến thành viên, bạn nên khởi tạo
object với property theo cú pháp object initializer (đã xem xét ở trên), và do đó không cần tạo
chuỗi constructor. Object initializer là cú pháp được khuyến khích (và yêu thích) trong C# đối
với các domain class.

173
Vấn đề khởi tạo và sử dụng object
Quan hệ class và object
Có thể hình dung class giống như một bản thiết kế trên giấy của một ngôi
nhà. Tự bản thân bản thiết kế này không phải ngôi nhà. Và có bản thiết kế
không có nghĩa là chúng ta có ngôi nhà.
Chỉ khi bạn sử dụng bản thiết kế này để xây dựng được một/một số ngôi
nhà cụ thể, bản thiết kế đó mới có giá trị.
Quá trình sử dụng bản thiết kế để xây dựng ngôi nhà có thể xem như tương
đương với quá trình khởi tạo đối tượng (object initialization/instantiation)
trong C#. Sau khi có ngôi nhà, chúng ta mới có thể ở. Quá trình sử dụng
ngôi nhà này tương đương với việc sử dụng object để giải quyết các vấn đề
của chương trình.
Trong lập trình hướng đối tượng có thể phân biệt ba giai đoạn:
1. Xây dựng class: định nghĩa kiểu dữ liệu, tương tự như tạo bản thiết
kế ngôi nhà;
2. Khởi tạo object: khai báo và gán giá trị đầu cho biến, tương tự giai
đoạn xây nhà theo thiết kế;
3. Sử dụng object: sử dụng biến trong các lệnh và biểu thức, tương tự
khai thác ngôi nhà.
Khai báo và khởi tạo object
Việc khai báo một object thực hiện tương tự như khai báo biến thuộc các kiểu
dữ liệu cơ sở mà bạn đã biết. Tuy nhiên, việc khai báo đơn thuần như vậy không
đủ để sử dụng object, vì khi đó C# đơn giản gán cho object giá trị null – giá trị
mặc định của object, mà không thực sự cấp phát bộ nhớ cho object.
Việc truy xuất một object có giá trị null luôn luôn gây lỗi
NullReferenceException ("Object reference not set to an instance
of an object."). Ngoài ra, trình biên dịch của C# luôn bắt buộc các biến
cục bộ phải được khởi tạo (instantiation, initialization) trước khi sử dụng.
Khi khởi tạo, một object sẽ được tạo ra trong vùng nhớ heap. Nếu kết quả
khởi tạo gán cho một biến, địa chỉ của object sẽ được gán cho biến này. Bản
thân địa chỉ của object chỉ là một con số. Con số này lại được lưu trong
stack của phương thức.
Do khởi tạo object thực chất là lời gọi tới phương thức khởi tạo, C# bắt buộc
mỗi class phải có phương thức khởi tạo.

174
Truy xuất các thành viên của object
Trong định nghĩa class, chúng ta đã biết ba loại thành viên là biến thành
viên, đặc tính, và phương thức. Khi một object được khai báo và khởi tạo,
chúng ta có thể sử dụng các thành viên này để thực sự chứa dữ liệu hoặc
xử lý dữ liệu.
Việc truy xuất các thành viên chỉ có thể thực hiện thông qua tên object,
không thể thực hiện qua tên class (trừ thành viên static sẽ học sau).
Để phân biệt, người ta sử dụng thuật ngữ instance members (bao
gồm instance method, instance variable, instance property) để mô tả các
thành viên của class mà chỉ thực sự tồn tại sau khi khởi tạo object. Sự tồn
tại của các thành viên này phụ thuộc vào object (vốn cũng được gọi là
một instance của class).
Để truy xuất thành viên của object chúng ta sử dụng phép toán “.” với tên
object. Truy xuất phương thức thành viên đơn giản là một lời gọi phương
thức từ một object nào đó. Việc truy xuất phương thức thành viên cũng sử
dụng cấu trúc tương tự:
Có sự khác biệt khi truy xuất thành viên của một object từ client code với
việc truy xuất trong nội bộ một class.
Client code là đoạn code nơi thực hiện khởi tạo và sử dụng object.
Nếu trong định nghĩa class, một thành viên được xác định là protected hoặc
private sẽ không thể truy xuất được từ client code mà chỉ có thể được truy
xuất trong nội bộ class.
Chỉ những thành viên được xác định là public mới có thể được truy xuất từ
client code.
Khi truy xuất thành viên từ trong nội bộ của class thì không cần sử dụng
phép toán truy xuất thành viên. Tuy nhiên có một số tình huống đặc thù sẽ
phải sử dụng đến từ khóa this và phép toán truy xuất thành viên.
Từ khóa this
Giả sử trong constructor bạn đặt tên cho tham số như sau:
public Book(string _authors)
{
_authors = _authors; // làm sao phân biệt _authors nào là member
class, _authors nào là tham số???
}
Tham số _authors giờ trùng tên với biến thành viên _authors. Cách đặt tên
này không vi phạm gì trong C#. Vấn đề bây giờ là, làm sao để phân biệt
_authors nào là member class, _authors nào là tham số???

175
Trong những tình huống thế này, bạn có thể sử dụng từ khóa this để chỉ
rõ đâu là thành viên của class:
this._authors = _authors;
Từ khóa this cho phép chỉ định chính bản thân object nơi đang thực thi
code. Trong ví dụ trên, this._authors báo hiệu rằng cần dùng chính biến
thành viên _authors của object đó. Điều này cũng có nghĩa là bạn có thể
dùng this trước mọi thành viên của class. Tuy nhiên, bạn nên hạn chế sử
dụng this nếu có thể vì nó làm code nhìn phức tạp hơn.
Từ khóa this cũng có một tác dụng phụ khác khá tốt. Nếu bạn không nhớ
hết các thành viên của class, bạn có thể gõ this, dấu chấm, và chờ
intellisense giúp liệt kê hết các thành viên (non-static) của class đó.

Từ khóa this giúp liệt kê thành viên của class


Từ khóa this chỉ có tác dụng với các thành viên bình thường (không có từ khóa static).

176
Nested class, nested type trong C#: khai báo kiểu lồng
nhau
Nested class (khai báo lớp lồng nhau) là điểm một đặc thù trong C# và là
một trường hợp riêng của nested type (khai báo kiểu lồng nhau). Đây là
một hiện tượng đặc biệt trong C#: một class được khai báo bên trong thân
của một class khác; một kiểu dữ liệu có thể khai báo bên trong một kiểu
khác. Đối với các bạn biết Java, bài học này sẽ rất đơn giản và quen thuộc.

Nested class trong C# là gì


Class được khai báo bên trong thân của một class khác được gọi là nested
class (lớp lồng nhau) hoặc inner class (lớp trong, lớp nội bộ); class chứa
class đó được gọi là outer class (lớp ngoài, lớp bao).
Nếu một class được khai báo trực tiếp trong namespace, class đó còn được
gọi là class cấp đỉnh (top level class).
Trong tất cả các bài học từ trước đến giờ, các class bạn xây dựng đều thuộc
loại này.
Nested class trong C# được sử dụng trong trường hợp thân của một class
trở nên quá lớn nhưng có chứa những logic tương đối độc lập. Khi đó các
khối logic này có thể xem xét tách thành các lớp nội bộ để dễ dàng hơn
trong quản lý code của class chính.
Việc phân chia này đồng thời giúp gói gọn code tránh làm dự án bị phân tán
bởi các lớp nhỏ không được sử dụng bởi client code.
Client code hiểu một cách đơn giản là những code ở bên ngoài class và sử dụng class này.
Ví dụ minh họa cách khai báo và sử dụng nested class
Hãy cùng xem ví dụ sau để hiểu rõ hơn về cách sử dụng nested class trong
C#.
using System;
namespace P01_NestedClass
{
internal class OuterClass
{
private string _str = "Outer class field";
private void OuterClassMethod() => Console.WriteLine("Outer class method");
public void OuterMethodCallsPrivateInnerClassMethod()
{
PrivateInnerClass privateInner = new PrivateInnerClass();
privateInner.Method();
}
public void OuterMethodsCallsPublicInnerClassMethod()
{
PublicInnerClass publicInner = new PublicInnerClass();
publicInner.Method();
177
}
public class PublicInnerClass
{
public void Method()
{
Console.WriteLine("Inner public class method");
OuterClass outer = new OuterClass();
Console.WriteLine(outer._str);
outer.OuterClassMethod();
}
}
private class PrivateInnerClass
{
public void Method()
{
Console.WriteLine("Inner private class method");
OuterClass outer = new OuterClass();
Console.WriteLine(outer._str);
outer.OuterClassMethod();
}
}
}
internal class Program
{
private static void Main(string[] args)
{
OuterClass outer = new OuterClass();
outer.OuterMethodCallsPrivateInnerClassMethod();
outer.OuterMethodsCallsPublicInnerClassMethod();
OuterClass.PublicInnerClass inner = new OuterClass.PublicInnerClass();
inner.Method();
Console.ReadKey();
}
}
}

Kết quả chạy chương trình minh họa nested class trong C#
Qua ví dụ trên có thể thấy, nested class cũng là một class hoàn toàn bình
thường ngoại trừ ba vấn đề:
1. Nếu trong nested class khởi tạo object của class ngoài thì có thể truy
cập cả vào các thành viên private của object đó;

178
2. Nested class có thể bị che giấu hoàn toàn khỏi client code (dùng từ
khóa private);
3. Sử dụng nested class (public) phải theo quy tắc
“Tên_lớp_ngoài.Tên_lớp_nội_bộ”.

Đặc điểm của nested class trong C#


Nested class có một số điểm khác biệt so với top-level class.
 Nested class có thêm từ khóa điều khiển truy cập private. Nếu một
nested class được định nghĩa với từ khóa private thì các lớp
sibling của lớp ngoài (lớp khác cùng cấp độ với lớp ngoài) không nhìn
thấy được nó. Chỉ code của lớp ngoài mới sử dụng được nested class
dạng private. Nếu không chỉ ra từ khóa điều khiển truy cập nào, C#
mặc định sử dụng private cho lớp nội bộ.
 Nested class có thể khởi tạo object của lớp ngoài và truy cập các thành
viên private của object lớp ngoài.
 Lớp nội bộ nếu được định nghĩa truy cập public thì lớp sibling của lớp
ngoài cũng có thể sử dụng nó như sử dụng các lớp bình thường. Khi
đó, lớp sibling của lớp ngoài sẽ dùng cấu
trúc Tên_lớp_ngoài.Tên_lớp_trong để sử dụng lớp nội bộ.
Lưu ý là trong C# nên hạn chế sử dụng nested class. Việc sử dụng nested
class không hợp lý có thể dẫn đến những lỗi khó lường trước, đặc biệt là khi
cho lớp trong và lớp ngoài gọi lẫn nhau.

Nested types
Lớp lồng nhau như bạn đã xem xét ở trên là một trường hợp riêng
của nested types – đặc điểm của C# cho phép khai báo các kiểu dữ liệu
khác bên trong một class hoặc struct.
Nested types trong C# thể hiện quan hệ “has-a” giữa class/struct ngoài với
kiểu khai báo bên trong nó.
C# cho phép khai báo tất cả các nhóm kiểu bạn đã biết (enum, class, struct,
interface, delegate) bên trong một class hoặc struct. Khi đó, các kiểu “bên
trong” được gọi chung là các nested type. Mỗi nested type có thể xem như
“kiểu thành viên” của class hoặc struct, tương tự như biến thành
viên, phương thức thành viên hay đặc tính thành viên.
Về cấu trúc cú pháp, việc khai báo các kiểu nested không có gì khác biệt so
với khi nó được khai báo là kiểu cấp đỉnh (top-level type) trực thuộc
namespace.
class OuterClass
{

179
public enum InnerEnum { ValueOne, ValueTwo, ValueThree } // khai báo nested
enum
private struct InnerStruct // khai báo nested struct private
{
public int AnIntMember = 10;
}
}
Còn một lưu ý nữa, bản thân nested type lại có thể tiếp tục chứa nested
type của riêng nó với cấp độ lồng nhau không hạn chế. Tuy nhiên, đây là
những tình huống sử dụng rất hãn hữu. Việc đặt lớp lồng nhau nhiều cấp
khiến code rắc rối và khó theo dõi.

Sử dụng nested type


Vậy khi nào và tại sao bạn nên xem xét sử dụng nested type thay vì khai
báo kiểu như bình thường (thuộc namespace)?
Thứ nhất, nested type cho phép kiểm soát truy cập tốt hơn. Bạn có thể sử
dụng từ khóa private để đặt kiểu dữ liệu nó hoàn toàn nội bộ. Trong khi đó,
kiểu khai báo trong namespace chỉ có hai mức truy cập: public và internal.
Thứ hai, do nested type cũng được xem là thành viên của class/struct, nó
có thể truy xuất các thành viên private/protected khác của class/struct chứa
nó.
Thứ ba, nếu bạn xác định rằng một kiểu dữ liệu nào đó chỉ hữu dụng đối với
1 class duy nhất hoặc không được sử dụng bởi client code, kiểu dữ liệu đó
nên được khai báo làm nested type.
Nested type được sử dụng từ client code thông qua tên của class/struct chứa
nó giống hệt như đối với nested class mà bạn đã xem xét ở phần trên. Việc
sử dụng nội bộ bên trong class/struct chứa kiểu đó không có gì khác biệt so
với khi kiểu đó được khai báo trong namespace.
Ví dụ, với class OuterClass như trên bạn có thể sử dụng các kiểu nested từ
client code như sau:
// sử dụng từ client code
var innerEnumValueOne = OuterClass.InnerEnum.ValueOne;
var innerStructObject = new OuterClass.InnerStruct();
Nếu sử dụng trong nội bộ lớp OuterClass:
class OuterClass
{
public enum InnerEnum { ValueOne, ValueTwo, ValueThree } // khai báo nested
enum
private struct InnerStruct // khai báo nested struct private
{
public int AnIntMember = 10;
public ValueOne = InnerEnum.ValueOne;
}
public void Print(InnerStruct innerStruct)
{

180
Console.WriteLine(innerStruct.AnIntMember);
Console.WriteLine(innerStruct.ValueOne);
}
}

Như vậy, việc sử dụng nested type bên trong class chứa nó hoàn toàn không
khác biệt gì với các kiểu tương ứng khai báo ngoài class mà bạn đã biết.

181
Thành viên static của class; static class; extension
method
Các thành viên tĩnh (static member) trong class C# vốn không xa lạ lắm với
bạn. Ngay từ những bài học đầu tiên bạn đã gặp Write/WriteLine,
Read/ReadLine/ReadKey – những phương thức tĩnh (static method) của lớp
Console. Ngoài static method, C# còn cho phép bạn định nghĩa nhiều loại
thành viên tĩnh khác. Các loại thành viên tĩnh này có vai trò rất quan trọng
trong quá trình xây dựng class. Đôi khi, thiếu thành viên tĩnh, nhiều tính
năng sẽ không thể xây dựng được.
Bài học này sẽ giới thiệu với bạn chi tiết về các loại thành viên tĩnh, bao
gồm biến tính (static field), phương thức tĩnh (static method), phương thức
khởi tạo tĩnh (static constructor), đặc tính tĩnh (static property). Bạn cũng
sẽ làm quen với những thứ “tĩnh” đặc biệt hơn như lớp tĩnh (static class) và
phương thức mở rộng (extension method).

Biến tĩnh (static field)


Trước khi đi vào nhu cầu tạo ra biến tĩnh, hãy cùng xây dựng một class như
sau:
class Employee
{
public Employee(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Salary { get; set; } = "Not Enough";
}
Đây là một class đơn giản mô tả cho nhân viên công ty.
Bài toán đặt ra là, mỗi khi khởi tạo một object của Employee, object đó sẽ
được gán một giá trị Id độc nhất. Ngoài ra, nếu liên tục tạo ra một loạt
object mới (ví dụ, để lưu trong một mảng), giá trị của Id sẽ tăng dần. Nghĩa
là object tạo sau có Id bằng Id của object vừa tạo trước cộng thêm 1. Nhu
cầu tạo ra Id tự động như vậy rất giống với cách tăng tự động giá trị khóa
chính của bảng dữ liệu SQL Server.
Một giải pháp bạn chắc chắn sẽ nghĩ tới là tạo ra một biến đếm (counter)
độc lập nào đó (nằm ngoài class). Mỗi lần tạo ra object mới của Employee
thì gán giá trị hiện thời của counter cho Id, sau đó tăng counter thêm 1.

182
Vấn đề khi đó nằm ở chỗ, bạn phải liên tục truyền biến counter này đến cho
bất kỳ chỗ nào thực hiện khởi tạo object.
Bây giờ hãy thực hiện thay đổi nhỏ sau trong class Employee:
class Employee
{
public Employee(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
Id = NextId;
NextId++;
}
public static int NextId;
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Salary { get; set; } = "Not Enough";
}
Bạn chỉ thêm lời khai báo:
public static int NextId;
Sau đó bổ sung thêm hai lệnh vào phương thức khởi tạo:
Id = NextId;
NextId++;
Khai báo biến NextId ở trên là một lệnh khai báo biến tĩnh (static field).
Dễ thấy, biến tĩnh về mặt hình thức khác biệt với biến thành viên ở mỗi từ
khóa static. Tuy nhiên, tác dụng của nó lại hoàn toàn khác.
Biến tĩnh NextId được dùng chung trong mọi object của Employee. Nghĩa là
dù bạn khởi tạo bao nhiêu object đi nữa, tất cả các object đó đều có chung
biến NextId này. Tức là sự tồn tại của NextId thực sự độc lập với các object
của Employee.
Đối với biến thành viên, nếu bạn khởi tạo một object, một biến thành viên mới sẽ được tạo ra
(có vùng nhớ riêng của nó). Biến thành viên đó sẽ được khởi tạo giá trị của riêng nó.
Đối với biến tĩnh, trong lần truy xuất đầu tiên sẽ tạo ra vùng nhớ cho biến. Tất cả các object tạo
ra sau đó đều sử dụng chung vùng nhớ này. Do đó, biến tĩnh chỉ có 1 bản duy nhất.
Bạn đã thấy sự tương đồng với ý tưởng tạo biến counter độc lập chưa ạ! Sự
khác biệt là, biến counter độc lập đó (NextId) giờ được đặt thẳng trong
class. Tất cả các thành viên của Employee tự động sử dụng được NextId.
Bạn không cần phải mất công truyền qua truyền lại nữa, cũng không cần
tác động gì từ bên ngoài. Bằng cách này, bạn thậm chí còn đếm được là
hiện tại có bao nhiêu object của Employee đã được tạo ra.
Giờ hãy viết một số client code để thử nghiệm lớp Employee với trường
static NextId:
private static void Main(string[] args)
{
183
Employee.NextId = 1000000;
Employee employee1 = new Employee("Inigo", "Montoya");
Employee employee2 = new Employee("Princess", "Buttercup");
Console.WriteLine("{0} {1} ({2})", employee1.FirstName,
employee1.LastName, employee1.Id);
Console.WriteLine("{0} {1} ({2})", employee2.FirstName,
employee2.LastName, employee2.Id);
Console.WriteLine($"NextId = { Employee.NextId }");
Console.ReadKey();
}
Chạy thử chương trình bạn sẽ thấy object đầu tiên có Id = 1000000. Object
thứ hai Id = 1000001. Sau đó NextId lại tăng lên 1000002.
Về mặt cú pháp khai báo, biến static chỉ khác biệt duy nhất ở từ khóa static
nằm trước tên kiểu.
Khi sử dụng trong client code, bạn chỉ có thể truy xuất biến static thông qua
tên class, không phải thông qua tên object như đối với biến thành viên. Như
trong ví dụ, bạn truy xuất biến static NextId trong phương thức Main
là Employee.NextId. Bạn không thể truy xuất theo
kiểu employee2.NextId được.
Chỉ khi nào bạn sử dụng biến tĩnh bên trong class, bạn có thể bỏ qua tên
class. Khi đó, về mặt hình thức, nó không có gì khác biệt với biến thành
viên. Đây là tình huống bạn đã sử dụng NextId bên trong constructor.
Lưu ý rằng, trong cùng một class không thể khai báo biến static và biến
thành viên trùng tên. Bạn hẳn sẽ thấy ngay, nếu dùng trong class, compiler
sẽ không hiểu bạn muốn dùng biến nào.
Trong cuộc sống bạn cũng có thể thấy những ví dụ tương đồng với biến thành viên và biến
static. Lấy ví dụ về khuôn đúc và sản phẩm đúc từ khuôn. Dễ hình dung, khuôn đúc tương
đương với class, còn sản phẩm đúc tương đương với object.
Mỗi sản phẩm đúc có những thông tin của riêng nó. Những thông tin riêng biệt này tương tự
như biến thành viên, vốn chứa thông tin về trạng thái và đặc điểm riêng của object (sản phẩm
đúc).
Tuy nhiên, lại có những thông tin phải liên kết với chính khuôn đúc. Ví dụ số lượng sản phẩm
đã đúc từ khuôn, số series của sản phẩm tiếp theo, công suất đúc (bao nhiêu sản phẩm mỗi giờ).
Rõ ràng những thông tin này không liên quan đến từng sản phẩm cụ thể, mà liên quan đến chính
khuôn đúc.
Như vậy, dữ liệu không nhất thiết chỉ liên kết với object mà còn có thể liên kết với chính class.
Biến static chính là loại thông tin liên kết với bản thân class.
Về hằng thành viên của class C#.
Hằng thành viên của class C# có chút đặc biệt mà ít người để ý. Khi khai báo một hằng thành
viên, hằng đó sẽ được C# tự động coi là một thành viên static giống như biến static. Sự khác
biệt duy nhất là giá trị của nó phải được xác định ngay từ lúc khai báo và sau này không thể

184
thay đổi được nữa. Hằng thành viên khai báo giống hệt như biến thành viên, ngoại trừ từ khóa
const ở trước:
public const int MaxValue = 12345;
Phương thức tĩnh (static method)
Tương tự như biến tĩnh, phương thức tĩnh liên kết với bản thân class chứ
không liên kết với object của class đó. Do đó, cũng chỉ có 1 phiên bản duy
nhất của phương thức đó được tạo ra.
Về cú pháp khai báo, phương thức tĩnh khác biệt với phương thức thành
viên duy nhất ở từ khóa static đặt trước tên kiểu trả về.
Hãy cùng thực hiện ví dụ sau:
class ConsoleHelper
{
public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White;
public void WriteLine(object message)
{
WriteLine(message, ForegroundColor, BackgroundColor);
}
public static void WriteLine(object message,
ConsoleColor fgColor = ConsoleColor.White,
ConsoleColor bgColor = ConsoleColor.Black,
bool reset = true)
{
Console.ForegroundColor = fgColor;
Console.BackgroundColor = bgColor;
Console.WriteLine(message);
if (reset)
Console.ResetColor();
}
}
Ví dụ trên khai báo class ConsoleHelper với một phương thức static
WriteLine, một phương thức thành viên cùng tên WriteLine. Hai phương thức
này cùng hỗ trợ viết thông tin ra màn hình console nhưng có thể thiết lập
màu nền và màu văn bản. Trong class này cũng chứa hai property thành
viên BackgroundColor và ForegroundColor.
Phương thức static được gọi trực tiếp từ tên class, so với phương thức thành
viên được gọi từ tên object. Do đó, để gọi phương thức static bạn không cần
khởi tạo object.
private static void Main(string[] args)
{
// gọi phương thức tĩnh WriteLine
ConsoleHelper.WriteLine("Hello world!", ConsoleColor.Magenta,
ConsoleColor.White);
// khởi tạo object và gọi phương thức thành viên WriteLine
var helper = new ConsoleHelper { BackgroundColor = ConsoleColor.Black,
ForegroundColor = ConsoleColor.Yellow };
helper.WriteLine("Hello again!");

185
Console.ReadKey();
}
Bởi vì phương thức static không được sử dụng qua object (và cũng không
liên quan gì đến object), bạn không được sử dụng từ khóa this bên trong
phương thức static. Cũng vì lý do này, phương thức static không thể sử
dụng được các thành viên bình thường (như biến thành viên, phương thức
thành viên) khác bên trong class.
Trong ví dụ trên, phương thức tĩnh WriteLine không thể sử dụng được
BackgroundColor và ForegroundColor vì đây là hai property thành viên.
Tuy nhiên, phương thức static vẫn có thể khởi tạo và sử dụng object của
chính class chứa nó. Ví dụ, lệnh khởi tạo sau là hoàn toàn hợp lệ bên trong
thân của WriteLine:
var helper = new ConsoleHelper();
Ở chiều ngược lại, các phương thức thành viên hoàn toàn có thể sử dụng
phương thức tĩnh. Như trong ví dụ trên, phương thức thành viên WriteLine
đã gọi phương thức static WriteLine.
Nhìn chung, phương thức tĩnh có vai trò rất gần với phương thức toàn cục
trong C/C++. Sự khác biệt duy nhất là nó được quản lý tốt hơn.
Nếu một phương thức không sử dụng bất kỳ thành viên nào khác của class,
bạn nên đặt nó làm phương thức tĩnh. Cách làm này sẽ hiệu quả hơn so với
đặt nó làm phương thức thành viên vì sẽ chỉ có duy nhất 1 phiên bản của
phương thức được tạo ra và quản lý. Nếu để phương thức tương tự làm
phương thức thành viên, mỗi object sẽ phải chứa một phiên bản riêng của
nó.
Intellisense của Visual Studio có thể tự động phát hiện và gợi ý chuyển đổi phương thức thành
dạng static theo logic trên.

Phương thức khởi tạo tĩnh (static constructor)


Quay trở lại ví dụ minh họa của phần biến static. Bài toán đặt ra là: làm sao
để gán giá trị đầu của biến tĩnh NextId là một giá trị ngẫu nhiên nhưng
không dùng client code? Để tạo giá trị ngẫu nhiên, bạn có thể sử dụng class
Random như sau:
Random randomGenerator = new Random();
NextId = randomGenerator.Next(101, 999);
tức là bạn cần đến vài lệnh tính toán thì mới tạo ra được một giá trị ngẫu
nhiên.
Hãy nghĩ đến một tình huống phức tạp hơn. Giả sử bạn lưu dữ liệu employee
vào tập tin. Khi chương trình hoạt động, bạn cần lấy giá trị lớn nhất của Id
đã lưu trong tập tin để tiếp tục tăng giá trị của Id theo quy luật chứ không
muốn gán giá trị đầu ngẫu nhiên.
186
Để giải quyết tình huống khởi tạo giá trị đầu (mà phải tính toán phức tạp)
cho biến static, C# sử dụng phương thức khởi tạo tĩnh (static
constructor).
Hãy bổ sung phương thức khởi tạo sau vào lớp Employee:
class Employee
{
static Employee()
{
Random randomGenerator = new Random();
NextId = randomGenerator.Next(101, 999);
}
// ...
public static int NextId = 42;
// ...
}

Đây là khai báo cho phương thức khởi tạo static của lớp Employee. Về cú
pháp, nó khác phương thức khởi tạo (constructor) bình thường ở từ khóa
static đứng trước tên.
Phương thức khởi tạo static khác biệt với phương thức khởi tạo thông thường
ở một số điểm:
 Phương thức khởi tạo không cần dùng từ khóa điều khiển truy cập. Lý
do là vì phương thức khởi tạo static không thể gọi từ client code.
Phương thức khởi tạo này được chương trình tự động gọi khi cần thiết.
Chính xác hơn là nó được gọi khi sử dụng class lần đầu tiên trong
chương trình.
 Phương thức khởi tạo static không chấp nhận tham số. Lý do giống
như ở trên: static constructor được gọi tự động.
Phương thức khởi tạo static được sử dụng để khởi tạo giá trị cho các trường
static của class, đặc biệt là khi phải thực hiện những tính toán phức tạp.
Nếu bạn đồng thời gán giá trị cho trường static lúc khởi tạo và trong phương
thức khởi tạo static, giá trị trong phương thức khởi tạo static sẽ là giá trị
chính thức của trường static.
Nếu có thể, hãy gán giá trị đầu cho trường static lúc khai báo thay vì gán trong constructor.
Thuộc tính tĩnh (static property)
Như bạn đã biết, property thực chất là tổ hợp hai phương thức get-set để
kiểm soát xuất nhập giá trị cho một trường dữ liệu. Như vậy, nếu đã có biến
static và phương thức static thì cũng có khái niệm static property với cùng
ý nghĩa: tổ hợp hai phương thức tĩnh get – set để kiểm soát xuất nhập giá
trị cho biến static.

187
Hãy bổ sung thêm đoạn code sau vào class Employee mà bạn đã xây dựng
từ trước:
class Employee
{
// ...
public static int NextId
{
get
{
return _nextId;
}
private set
{
_nextId = value;
}
}
private static int _nextId = 42;
// ...
}

Đây là cách khai báo static property NextId để xuất nhập dữ liệu cho biến
static _nextId. Cách khai báo static property không có gì khác biệt so với
property thành viên, ngoại trừ từ khóa static.
Bạn thậm chí có thể khai báo auto static property như sau:
public static int NextId { get; private set; } = 42;
Static property được sử dụng qua tên class giống như biến static và phương
thức static.
Tương tự như thuộc tính thành viên, bạn nên sử dụng static property thay
cho biến static public.

Lớp tĩnh (static class)


Một số class được tạo ra nhưng không chứa bất kỳ biến thành viên nào. Lấy
ví dụ, nếu bạn muốn xây dựng một class chuyên thực hiện các hàm tính
toán số học, bạn chẳng cần biến thành viên nào cả. Hãy cùng thực hiện một
class như vậy:
public static class MyMath
{
public static int Max(params int[] numbers)
{
if (numbers.Length == 0)
{
throw new ArgumentException("Không có giá trị để so sánh", "numbers");
}
int max = numbers[0];
foreach (var number in numbers)
{
if (number > max)
{
max = number;

188
}
}
return max;
}
public static int Min(params int[] numbers)
{
if (numbers.Length == 0)
{
throw new ArgumentException("Không có giá trị để so sánh", "numbers");
}
int min = numbers[0];
foreach (var number in numbers)
{
if (number < min)
{
min = number;
}
}
return min;
}
}
Trong class này khai báo hai phương thức static Min và Max để tìm giá trị
nhỏ nhất và lớn nhất trong một mảng số nguyên.
Lớp MyMath không chứa bất kỳ biến hoặc phương thức thành viên thường
nào. Do đó, việc khởi tạo object của nó khá vô nghĩa. Do vậy nó được khai
báo làm lớp static với từ khóa static đứng trước từ khóa class như bạn đã
thấy.
Từ khóa static đứng trước khai báo class có mấy tác dụng:
 Thứ nhất nó không cho phép khởi tạo object từ class này.
 Thứ hai, nó không cho phép khai báo bất kỳ thành viên thông thường
nào mà chỉ có thể khai báo các thành viên tĩnh.
Khi một class được đánh dấu là static, C# compiler tự động đánh dấu nó là
abstract và sealed, nghĩa là cấm khởi tạo object và không cho phép kế thừa.
Một đặc điểm nữa của lớp static là bạn có thể sử dụng cấu trúc using static
để trực tiếp truy xuất các thành viên static của class này mà không cần chỉ
rõ tên class, nghĩa là nếu ở đầu tập tin code có lệnh using static
MyMath; thì bạn có thể gọi các phương thức của class này theo cách ngắn
gọn Max(numbers); thay cho MyMath.Max(numbers);.
using static System.Console;
using static ConsoleApplication1.MyMath;
namespace ConsoleApplication1
{
class Program
{
private static void Main(string[] args)
{
int[] numbers = new[] { 1, 2, 3, 4, 5, 6 };
// có thể gọi Max và Min theo cách ngắn gọn vì đã có using static MyMath;
189
int max = Max(numbers);
int min = Min(numbers);
// có thể gọi WriteLine ngắn gọn vì đã có using static Console;
WriteLine($"Max value: {max}");
WriteLine($"Min value: {min}");
ReadKey();
}
}
}
Lớp System.Console mà bạn đã biết cũng là một static class. Vì vậy, nếu
đặt using static System.Console; ở đầu tập tin code thì có thể gọi các
phương thức trong đó một cách ngắn gọn: WriteLine("Hello world");
thay vì Console.WriteLine("Hello world");

Extension method
Extension method (phương thức mở rộng) là một tính năng rất thú vị của
C# cho phép “chèn” một phương thức của class này vào làm phương thức
thành viên một class khác.
Để cho dễ hiểu, hãy tưởng tượng thế này. Lớp string mà bạn đã biết mặc
dù cung cấp rất nhiều phương thức hữu ích nhưng nếu bạn làm việc nhiều
với giao diện console, hẳn bạn sẽ muốn class này có một phương thức giúp
xuất trực tiếp chuỗi ra console, thay vì phải liên tục gọi đến
Console.Write/WriteLine. Hoặc bạn cũng có thể muốn lớp này hỗ trợ luôn
việc chuyển đổi chuỗi thành các kiểu dữ liệu cơ sở quen thuộc, thay vì phải
gọi Parse/TryParse của kiểu đích.
Bắt đầu từ C# 3 bạn đã có thể thực hiện mong muốn trên bằng cách sử
dụng extension method. Extension method thực chất chỉ là một static
method nằm trong một static class cùng với một thay đổi nhỏ trong danh
sách tham số.
Hãy cùng thực hiện một ví dụ nhỏ sau cho dễ hiểu.
static class ExtensionMethods
{
public static void ToConsole(this string message)
{
Console.WriteLine(message);
}
public static void ToConsole(this string message, ConsoleColor fgColor =
ConsoleColor.White, ConsoleColor bgColor =
ConsoleColor.Black, bool reset = true)
{
Console.ForegroundColor = fgColor;
Console.BackgroundColor = bgColor;
Console.WriteLine(message);
if (reset) Console.ResetColor();
}
public static double ToDouble(this string number)
{

190
return double.TryParse(number, out double d) ? d : double.NaN;
}
public static int ToInt(this string number)
{
return int.Parse(number);
}
}
Ví dụ trên xây dựng một static class ExtensionMethods với hai static method
cùng tên ToConsole. Hai phương thức này cùng thực hiện in một thông báo
ra màn hình. Overload thứ hai in ra có màu nền và màu văn bản. ToDouble
và ToInt thực hiện chuyển đổi chuỗi thành kiểu double và int.
Hãy để ý tham số thứ nhất của cả bốn phương thức. Chúng cùng có
dang this string <tên-biến>. Để ý thấy rằng, tham số này khác thường
một chút ở từ khóa this. Chỉ một từ khóa đó làm cho hai phương thức có
năng lực đặc biệt: có thể gọi chúng từ một string bất kỳ. Hãy cùng xem
client code:
static void Main(string[] args)
{
"Hello world!".ToConsole();
"Hello again!".ToConsole(ConsoleColor.Magenta);
int i = "2000".ToInt();
double d = "2000.0001".ToDouble();
Console.ReadKey();
}
Bạn đã thấy, ToConsole(), ToInt(), ToDouble() giờ được gọi thẳng từ object
của string, giống hệt như gọi các phương thức thành viên khác của string.
Nếu chỉ nhìn lời gọi phương thức ở client code, bạn không phân biệt được
đâu là phương thức thành viên “xịn” của class, đâu là extension method.
Dĩ nhiên, bạn hoàn toàn có thể gọi extension method như các static method
thông thường:
ExtensionMethods.ToConsole("Hiiiiiiiiiiiiiiiiiiiiii!");
Lưu ý, nếu bạn tạo một extension method có signature giống hệt như một
phương thức thành viên có sẵn của class, bạn sẽ không gọi nó qua object
được mà chỉ có thể gọi như phương thức static thông thường.
Nếu muốn tạo extension method cho kiểu nào, bạn đặt tên kiểu đó sau từ
khóa this của tham số đầu tiên. Trong ví dụ trên, bạn muốn tạo extension
method cho lớp string thì tham số đầu tiên phải là this string <tên-
tham-số>. Bạn có thể sử dụng tham số này trong thân method như bất kỳ
tham số bình thường nào.
Bạn có thể tạo extension method cho cả các class có sẵn của C# cũng như
class tự xây dựng. Cách thực hiện không có gì khác biệt nhau.

191
Mặc dù extension method là một tính năng rất thú vị, nhưng bạn không nên lạm dụng nó, nhất
là khi áp dụng cho các class không phải do bạn xây dựng. Extension method có vấn đề trong
việc quản lý phiên bản (versioning). Đặc biệt không nên tạo extension method cho kiểu object.

192
Nạp chồng toán tử (operator overloading) trong C#
Nạp chồng toán tử (operator overloading) trong C# là khả năng định nghĩa
lại hoạt động của một số toán tử để có thể áp dụng nó với các object của
class bạn tự định nghĩa. Nếu bạn đã biết C++, bạn sẽ rất nhanh nắm bắt
được nạp chồng toán tử trong C#. Tuy nhiên, nếu bạn xuất phát từ Java
hay Visual Basic, đây có thể là vấn đề mới.
Bài học này sẽ giúp bạn nắm được ý nghĩa của nạp chồng toán tử và cách
thực hiện trong C#.

Nạp chồng toán tử trong C#


Khái niệm nạp chồng toán tử
Đối với các kiểu dữ liệu số, C# định nghĩa sẵn một số phép toán như các
phép toán số học, phép toán so sánh, phép toán tăng giảm. Đối với kiểu
string, như chúng ta đã biết, được định sẵn phép toán cộng xâu.
Tuy nhiên, các kiểu dữ liệu (class) do người dùng định nghĩa lại không thể
sử dụng ngay các phép toán đó được.
Ví dụ, nếu người dùng định nghĩa kiểu số phức, các phép toán cơ bản trên
kiểu số phức lại không thể thực hiện được ngay, mặc dù về mặt toán học
các phép toán đối với số phức không có gì khác biệt với kiểu số được C#
định nghĩa.
Để giải quyết những vấn đề tương tự, C# cho phép nạp chồng toán tử, tức
là cho phép định nghĩa lại những phép toán đã có với các kiểu dữ liệu do
người dùng xây dựng.
Nạp chồng phương thức (method overloading) cùng với nạp chồng toán
tử (operator overloading) là hai hiện tượng thuộc về nguyên lý đa hình
tĩnh (static polymorphism).
Cách nạp chồng toán tử trong C#
Hãy cùng thực hiện và phân tích ví dụ sau để hiểu cách nạp chồng toán tử.
Chú ý xem xét cú pháp nạp chồng đối với mỗi toán tử.
using System;
namespace P01_OperatorOverload
{
/// <summary>
/// lớp biểu diễn hình hộp
/// </summary>
internal class Box
{
public double Length { get; set; }
public double Breadth { get; set; }

193
public double Height { get; set; }
public Box() { }
public Box(double length, double breadth, double height)
{
Length = length;
Breadth = breadth;
Height = height;
}
/// <summary>
/// tính thể tích khối hộp
/// </summary>
public double Volume => Length * Breadth * Height;
// nạp chồng phép cộng
public static Box operator +(Box b, Box c)
{
Box box = new Box
{
Length = b.Length + c.Length,
Breadth = b.Breadth + c.Breadth,
Height = b.Height + c.Height
};
return box;
}
// nạp chồng phép so sánh bằng
public static bool operator ==(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length == rhs.Length && lhs.Height == rhs.Height
&& lhs.Breadth == rhs.Breadth)
{
status = true;
}
return status;
}
// nạp chồng phép so sánh khác
public static bool operator !=(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length != rhs.Length || lhs.Height != rhs.Height ||
lhs.Breadth != rhs.Breadth)
{
status = true;
}
return status;
}
// nạp chồng phép so sánh nhỏ hơn
public static bool operator <(Box lhs, Box rhs)
{
bool status = false;
if (lhs.Length < rhs.Length && lhs.Height < rhs.Height
&& lhs.Breadth < rhs.Breadth)
{
status = true;
}
return status;
}
// nạp chồng phép so sánh lớn hơn
public static bool operator >(Box lhs, Box rhs)

194
{
bool status = false;
if (lhs.Length > rhs.Length && lhs.Height >
rhs.Height && lhs.Breadth > rhs.Breadth)
{
status = true;
}
return status;
}
public override string ToString()
{
return string.Format("({0}, {1}, {2})", Length, Breadth, Height);
}
}
internal class Program
{
private static void Main(string[] args)
{
Box Box1 = new Box(6, 7, 5);
Box Box2 = new Box(12, 13, 10);
Box Box3 = new Box();
Box Box4 = new Box();
/* phép cộng hai hình hộp cho ra hình hộp khác có kích thước
* bằng tổng kích thước của hai hộp
*/
Box3 = Box1 + Box2;
Console.WriteLine("Box 3: {0}", Box3.ToString());
Console.WriteLine("Volume of Box3 : {0}", Box3.Volume);
// so sánh hai hình hộp
if (Box1 > Box2)
Console.WriteLine("Box1 lớn hơn Box2");
else
Console.WriteLine("Box1 không lớn hơn Box2");
if (Box3 == Box4)
Console.WriteLine("Box3 bằng Box4");
else
Console.WriteLine("Box3 không bằng Box4");
Console.ReadKey();
}
}
}
Trong ví dụ trên chúng ta đã thực hiện nạp chồng cho phép toán cộng (+),
các phép so sánh (bằng ==, khác !=, lớn hơn >, nhỏ hơn <).
Cú pháp khai báo này được tổng hợp lại dưới đây:
public static Box operator +(Box b, Box c) {...}
public static bool operator ==(Box lhs, Box rhs) {...}
public static bool operator !=(Box lhs, Box rhs) {...}
public static bool operator <(Box lhs, Box rhs) {...}
public static bool operator >(Box lhs, Box rhs) {...}
Nếu để ý kỹ hơn nữa chúng ta thấy, đây đều là các phép toán binary. Cách
nạp chồng các phép toán này có cùng một cú pháp.

195
Mỗi loại phép toán sẽ có cách nạp chồng riêng. Tuy nhiên, cú pháp chung

public static <return_type> operator <operator>(<parameters>)
{ ... }
Các toán tử có thể nạp chồng: +, -, !, ~, ++, –, +, -, *, /, %, ==, !=, <,
>, <=, >=
Ngoài ra phép toán indexer cũng là một phép toán có thể nạp chồng.
Một số lưu ý khi nạp thực hiện nạp chồng toán tử
Các phép toán chia làm ba loại: unary (chỉ cần một toán hạng, như phép
toán tăng ++, phép toán giảm –), binary (cần hai toán hạng, như các phép
toán +,-,*,/), ternary (cần ba toán hạng, như phép toán điều kiện ?). Do
đó, khi nạp chồng phép toán nào thì phải cung cấp đủ lượng tham số phù
hợp. Ví dụ, khi nạp chồng phép toán binary (như +, -) thì phải cấp 2 tham
số như đã làm ở trên.
Phép toán tăng giảm (++, –) thuộc loại unary nên trong danh sách tham
số chỉ cần 1 tham số. Các phép toán này cũng không có giới hạn gì khi nạp
chồng. Cùng ví dụ với lớp Box trên:
public static Box operator ++(Box b)
{
return new Box(b.Length++, b.Breadth++, b.Height++);
}
Các phép toán số học (+, – *, /, %) không đặt ra giới hạn gì khi nạp chồng.
Bạn chỉ cần tuân thủ đúng cú pháp như trên là được.
Bạn thậm chí có thể nạp chồng cùng một phép toán nhiều lần. Ví dụ, bạn
hoàn toàn có thể thêm nạp chồng phép + một lần nữa như sau:
public static Box operator +(Box b, double size)
{
return new Box(b.Length += size, b.Breadth += size, b.Height + size);
}
Ở đây bạn đã nạp chồng phép cộng Box với một số thực. Điều kiện để nạp
chồng phép toán nhiều lần là danh sách tham số của mỗi lần nạp chồng
phải khác nhau.
Đối với các phép toán so sánh, bạn phải thực hiện nạp chồng cả cặp. Nghĩa
là, nếu nạp chồng phép so sánh bằng == thì đồng thời phải nạp chồng cả
phép so sánh khác !=; nếu nạp chồng phép so sánh hơn > thì phải nạp
chồng cả phép so sánh kém <.
Các phép gán (+=, -=,...) không cho phép nạp chồng trực tiếp. Tuy nhiên,
nếu bạn đã nạp chồng phép +, -,… thì các phép toán này tự nhiên sẽ được

196
nạp chồng. Ví dụ, nếu bạn đã nạp chồng phép cộng Box với 1 số như trên
thì hoàn toàn có thể gọi lệnh
var Box5 = Box4 += 5; // phép cộng gán với số
Riêng phép toán indexer có cách thực hiện nạp chồng riêng dưới đây.

Nạp chồng phép toán indexer trong C#


Indexer là một phép toán giúp client code sử dụng object tương tự như khi
sử dụng mảng. Indexer thường được sử dụng với với các kiểu dữ liệu chứa
trong nó một tập hợp dữ liệu (collection hoặc array). Indexer giúp đơn giản
hóa việc sử dụng ở client code.
Trước khi xem cú pháp nạp chồng toán tử indexer, hãy cùng thực hiện ví
dụ sau đây:
namespace P02_IndexerOverload
{
using static System.Console;
class Program
{
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
ReadLine();
}
}
class Vector
{
private double[] _components;
public Vector(int dimension)
{
_components = new double[dimension];
}
public Vector(params double[] components)
{
_components = components;
}
public override string ToString()
{
return $"({string.Join(", ", _components)})";
}
}
}
Ví dụ này xây dựng một class Vector đơn giản dành cho vector n-chiều. Cả
vector được lưu trong một mảng private _components (mỗi phần tử của
mảng là kích thước một chiều của vector). Class này có 2 overload cho
constructor, một cái nhận số chiều làm tham số, một cái nhận mảng double
làm tham số.

197
Bạn có muốn truy xuất giá trị từng chiều của vector này như truy xuất phần
tử của mảng không? Tức là viết kiểu vector[0], vector[1],..., trong đó 0, 1,
là chỉ số chiều.
Cú pháp nạp chồng indexer
Cú pháp nạp chồng phép toán indexer cho class gần giống property, trong
đó phải có ít nhất một trong hai phương thức get/set, dùng để trả lại giá trị
và gán giá trị. Khác biệt duy nhất ở chỗ indexer bắt buộc sử dụng từ
khóa this với cặp dấu ngoặc vuông. Biến làm khóa phải đặt trong cặp dấu
ngoặc vuông.
public TValue this[TKey key]
{
get{ }
set{ }
}
Trong đó:
1. TValue là kiểu dữ liệu trả về, TKey là kiểu dữ liệu của khóa
2. Số lượng khóa có thể nhiều hơn 1
3. Kiểu của khóa có thể là bất kỳ kiểu dữ liệu nào (không nhất thiết là
số hoặc chuỗi)
4. Phương thức get và set hoạt động giống như đối với thuộc tính
Nạp chồng toán tử indexer cho lớp Vector
Thêm đoạn code sau vào class Vector:
public double this[int index]
{
get => (index < _components.Length) ? _components[index] : double.NaN;
set { if (index < _components.Length) _components[index] = value; }
}
Cấu trúc này nhìn giống hệt như full property, ngoại trừ tên gọi this[int
index]. Đoạn code này đã thực hiện nạp chồng toán tử indexer cho lớp
Vector.
Logic hoạt động của getter rất đơn giản. Nếu index nhỏ hơn số phần tử của
mảng thì trả lại giá trị tương ứng index, nếu không thì trả về giá trị NaN
(Not a Number). Setter chỉ gán giá trị cho phần tử tương ứng của mảng.
Nếu phương thức get/set chỉ chứa một lệnh duy nhất có thể sử dụng cú
pháp “expression body” cho ngắn gọn. Trong code của getter ở trên chúng
ta đã sử dụng cấu trúc này.

198
Expression body là một lối viết xuất hiện từ C# 6, nếu thân của phương thức chỉ chứa một lệnh
duy nhất có thể sử dụng cấu trúc như sau để viết:
Tên_phương_thức() => lệnh;
Từ C# 7 có thể sử dụng expression body cho cả phương thức get và set của property.
Bây giờ bạn có thể truy xuất từng chiều của vector như sau:
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
var x = vector1[0];
var y = vector1[1];
var z = vector1[2];
WriteLine($"Vector components: x = {x}, y = {y}, z = {z}");
vector1[2] = 30;
vector1[1] = 20;
vector1[0] = 10;
WriteLine($"vector 1: {vector1}");
ReadLine();
}

Dưới đây là code đầy đủ của ví dụ trên:


namespace P02_IndexerOverload
{
using static System.Console;
class Program
{
static void Main(string[] args)
{
var vector1 = new Vector(1, 2, 3);
WriteLine($"vector 1: {vector1}");
var x = vector1[0];
var y = vector1[1];
var z = vector1[2];
WriteLine($"Vector components: x = {x}, y = {y}, z = {z}");
vector1[2] = 30;
vector1[1] = 20;
vector1[0] = 10;
WriteLine($"vector 1: {vector1}");
ReadLine();
}
}
class Vector
{
public double this[int index]
{
get => (index < _components.Length) ? _components[index] : double.NaN;
set { if (index < _components.Length) _components[index] = value; }
}
private double[] _components;
public Vector(int dimension)
{
_components = new double[dimension];
}
public Vector(params double[] components)
{
199
_components = components;
}
public override string ToString()
{
return $"({string.Join(", ", _components)})";
}
}
}

200
Kế thừa và đa hình trong C#, che giấu và ghi đè
phương thức
Kế thừa và đa hình trong C# cũng như bất kỳ ngôn ngữ lập trình hướng đối
tượng nào là một chủ đề đặc biệt quan trọng. Kế thừa và đa hình là hai
nguyên lý cơ bản của lập trình hướng đối tượng mà bạn không thể không
biết. Tuy nhiên, nhiều bạn có sự nhầm lẫn giữa hai khái niệm này cũng.
Cách thức thực thi hai nguyên lý này trong các ngôn ngữ lập trình cũng
không hoàn toàn giống nhau.
Để có thể làm việc và phát huy hiệu quả của lập trình hướng đối tượng trong
C#, bạn không thể không nắm chắc hai khái niệm này và các kỹ thuật liên
quan. Bài học này sẽ giúp bạn làm việc đó.

Kế thừa trong C# là gì?


Kế thừa (inheritance) là một công cụ rất mạnh trong lập trình hướng đối
tượng cho phép tạo ra các class mới từ một class đã có, và qua đó cho phép
tái sử dụng code của class đã có, giúp giảm thiểu việc lặp code giữa các
class, dễ dàng bảo trì và giảm thời gian phát triển.
Hãy cùng xem xét ví dụ sau:
using System;
namespace P01_Inheritance
{
internal class Bird
{
private int _weight;
public int Weight
{
get => _weight;
set {
if (value > 0)
_weight = value;
}
}
public void Feed() => _weight += 10;
public Bird() => Console.WriteLine($"Bird created");
public Bird(int weight)
{
_weight = weight;
Console.WriteLine($"Bird created, {_weight} gr.");
}
public void Fly() => Console.WriteLine("Bird is flying");
}
internal class Parrot : Bird
{
public Parrot() => Console.WriteLine("Parrot created");
public Parrot(int weight) : base(weight) { }
public void Speak() => Console.WriteLine("Parrot is speaking");
}

201
internal class Cockatoo : Parrot
{
public Cockatoo() => Console.WriteLine("Cockatoo created");
public void Dance() => Console.WriteLine("Cockatoo is dancing");
}
internal class MainClass
{
private static void Main(string[] args)
{
Console.WriteLine("Bird:");
Bird bird = new Bird(50) { Weight = 100 };
bird.Feed();
Console.WriteLine($"Weight: {bird.Weight}");
bird.Fly();
Console.WriteLine("rnParrot:");
Parrot parrot = new Parrot(200);
parrot.Feed();
Console.WriteLine($"Weight: {parrot.Weight}");
parrot.Fly();
parrot.Speak();
Console.WriteLine("rnCockatoo:");
Cockatoo cockatoo = new Cockatoo() { Weight = 300 };
cockatoo.Feed();
Console.WriteLine($"Weight: {cockatoo.Weight}");
cockatoo.Fly();
cockatoo.Speak();
cockatoo.Dance();
Console.ReadKey();
}
}
}
Mối quan hệ giữa các class được thể hiện qua sơ đồ code:

Code map thể hiện quan hệ giữa các class


Trong ví dụ trên chúng ta tạo ra ba class: Bird (chim), Parrot (vẹt) và
Cockatoo (vẹt châu Úc). Parrot kế thừa Bird; Cockatoo kế thừa Parrot. Quan
hệ kế thừa này trong C# được thể hiện bằng dấu hai chấm phân chia tên
của class mới với tên của một class có sẵn:

202
1. class Parrot : Bird
2. class Cockatoo : Parrot
Kết quả chạy chương trình ví dụ trên như sau:

Kết quả thực hiện chương trình kế thừa


Khi chạy chương trình trên có thể nhận xét như sau:
 Mỗi khi khởi tạo object của class con thì phương thức khởi tạo của
class cha luôn được gọi trước. Điều này có nghĩa là trước khi khởi tạo
object của class con thì object của class cha được khởi tạo, và do đó
bản thân object con có chứa trong nó object cha. Object cha này được
truy cập từ object con thông qua từ khóa base. Thông qua từ
khóa base cũng có thể truy xuất các thành viên của lớp cha.
 Phương thức khởi tạo không được kế thừa mà phương thức khởi tạo
của lớp cha được gọi tự động (nếu là phương thức khởi tạo không
tham số) hoặc được gọi từ phương thức khởi tạo của lớp con (nếu là
phương thức khởi tạo có tham số). Phương thức khởi tạo của lớp cha
được gọi bằng lệnh base(<danh sách tham số>), tương tự như gọi
phương thức (chỉ thay tên phương thức bằng từ khóa base).
 Mặc dù lớp con không thể kế thừa thành viên private của lớp cha (tức
là không thấy và không thể trực tiếp sử dụng, như lớp Parrot không
thể nhìn thấy và trực tiếp sử dụng biến thành viên _weight của Bird)
nhưng qua phương thức/thuộc tính kế thừa của lớp cha vẫn có
thể gián tiếp sử dụng thành viên private này. Phương thức Feed và
thuộc tính Weight ở trên là ví dụ. Lý do là vì trong object con có cả

203
object cha tồn tại. Lời gọi tới phương thức kế thừa từ lớp cha thực
chất là hoạt động với object cha này.

Đặc điểm của kế thừa trong C#


Lớp có sẵn mà từ đó tạo ra các lớp khác được gọi là lớp cha/lớp cơ sở; lớp
mới xây dựng trên cơ sở lớp cũ được gọi là lớp con/lớp dẫn xuất.
Để tiện lợi, trong một số trường hợp chúng ta sẽ sử dụng thuật ngữ tiếng Anh thay thế: base
class, parent class, superclass (lớp cha/lớp cơ sở), derived class, child class, subclass (lớp con,
lớp dẫn xuất).
C# chỉ cho phép mỗi lớp con có một lớp cha trực tiếp (khác với C++ cho
phép lớp có nhiều lớp cha trực tiếp). Cách kế thừa này được gọi là kế
thừa đơn (single inheritance).
Lớp con, đến lượt mình, lại có thể trở thành lớp cơ sở cho các lớp khác (tạm
gọi vui là lớp cháu). Quá trình này có thể tiếp diễn với nhiều thế hệ lớp khác
nhau, tạo ra một cấu trúc phân cấp (class hierarchy) của các class có quan
hệ kế thừa nhau.
Tất cả các lớp con, cháu, chắt,… của một class gọi chung là các lớp hậu
duệ (descendant) của class đó; các lớp cha, ông, cụ,… của một class được
gọi chung là các lớp tiền bối (ancestor) của nó.
Trong ví dụ trên, Bird là lớp cha của Parrot (Parrot là lớp con trực tiếp của
Bird), còn Parrot lại trở thành lớp cha của Cockatoo (Cockatoo là lớp con
trực tiếp của Parrot, lớp con gián tiếp của Bird). Bird => Parrot => Cockatoo
tạo ra một cấu trúc phân cấp của các class. Parrot, Cockatoo đều có thể gọi
chung là các lớp hậu duệ của Bird.
Class được đánh dấu với từ khóa sealed không cho phép kế thừa. Hiểu đơn giản dòng dõi class
này đến đây là “tuyệt tự”.
Khi một class kế thừa từ một class khác, nó thừa hưởng tất cả các thành
viên của class cha (kể cả những thành viên mà cha nó kế thừa từ ông), trừ
những thành viên được đánh dấu là private, phương thức khởi tạo, phương
thức hủy.
Trong ví dụ trên, Parrot thừa hưởng thuộc tính Weight, phương thức Feed
và Fly của Bird. Cockatoo sẽ thừa hưởng Weight, Feed và Fly (từ Bird), đồng
thời thừa hưởng phương thức Speak từ Parrot.
Tuy nhiên, Parrot (và Cockatoo) lại không kế thừa được biến thành viên
_weight từ Bird vì biến này để mức truy cập là private, cũng như không thể
kế thừa các phương thức khởi tạo Bird() và Bird(int). Tương tự, Cockatoo
cũng không kế thừa được phương thức khởi tạo Parrot() và Parrot(int).

204
Lớp Object và kế thừa trong C#
Toàn bộ .NET Framework được xây dựng dựa trên khái niệm “tất cả đều là
object”, vốn hoạt động trên cơ sở kế thừa.
Trong C#, mọi class đều là hậu duệ của lớp System.Object, kể cả khi không
ghi quan hệ kế thừa với lớp này.
Một số nguyên, số thực, biến logic,… trong C# đều là object của các class
tương ứng (Int32, Double, Boolean) kế thừa từ lớp System.Object. Tuy
nhiên, C# hỗ trợ để có thể, ví dụ, gán giá trị số trực tiếp, thay thì phải khởi
tạo object của lớp số nguyên tương ứng.
Bởi vì mọi class trong C# đều kế thừa (trực tiếp hoặc gián tiếp) từ lớp
System.Object, bất kỳ class nào xây dựng xong đều có sẵn 4 phương thức:
ToString, GetHashCode, GetType, Equals. Đây là bốn phương thức của lớp
Object. Như vậy, kế thừa là một cơ chế tái sử dụng code để mở rộng ứng
dụng.
Trong ví dụ trên, Bird, Parrot, Cockatoo đều là các hậu duệ của lớp
System.Object, vì vậy các class này đều thừa hưởng 4 phương thức kể trên.
Để xem một class có những thành viên nào có thể dùng từ khóa this ở bên
trong thân bất kỳ phương thức nào như sau:

205
Từ khóa this sử dụng trong phương thức của class
Như vậy, chúng ta thấy rằng, cơ chế kế thừa cho phép tái sử dụng code từ
những class sẵn có để tạo ra class mới một cách nhanh chóng. Mỗi class
con là một bản mở rộng của class cha bằng cách thêm vào những thành
viên của riêng mình.

Quan hệ giữa kế thừa và đa hình trong C#


Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác
nhau.
Đa hình thiết lập mối quan hệ “là” (is-a relationship) giữa kiểu cơ sở và kiểu
dẫn xuất. Ví dụ, nếu chúng ta có lớp cơ sở Bird và lớp dẫn xuất Parrot thì
một object của Parot cũng là object của Bird, kiểu Parrot cũng là kiểu Bird
(đương nhiên rồi, vẹt là chim mà!). Mối quan hệ này nhìn rất giống như
quan hệ kế thừa ở trên.

206
Trong khi đó, kế thừa liên quan chủ yếu đến tái sử dụng code: code của lớp
con thừa hưởng code của lớp cha. Một cách nói khác, đa hình liên quan tới
quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.
Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được
đồng nhất, thể hiện ở chỗ:
1. Class con thừa hưởng các thành viên của class cha (kế thừa, tái sử
dụng code).
2. Một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là
kiểu cơ sở có thể dùng để thay thế cho kiểu dẫn xuất (đa hình).
Vì những lý do trên mà các lệnh khai báo và khởi tạo sau là hoàn toàn đúng:
1. Bird parrotTheBird = new Parrot();
2. Parrot cockatooTheParrot = new Cockatoo();
3. Bird cockatooTheBird = new Cockatoo();
Lệnh thứ nhất khai báo một object thuộc kiểu Bird nhưng được gán một
object thuộc kiểu Parrot, kết quả như sau:

Lệnh thứ hai khai báo biến thuộc kiểu Parrot nhưng được gán object thuộc
kiểu Cockatoo, kết quả như sau:

Lệnh thứ ba khai báo biến thuộc kiểu Bird nhưng được gán object thuộc kiểu
Cockatoo với kết quả như sau:

Chúng ta cũng có thể để ý thấy rằng, các object của lớp con thực sự được
khởi tạo (các phương thức khởi tạo được gọi theo trật tự giống như đã gặp
ở bài trước). Nhưng các object này lại được tham chiếu tới từ các biến thuộc
kiểu cha.

207
Trong những trường hợp này, object chỉ có thể sử dụng được những thành
viên của lớp cha. Ví dụ, object parrotTheBird ở trên chỉ có thể sử dụng các
thành viên của lớp Bird mà không biết về các thành viên mới của Parrot
(như phương thức Speak).
Cơ chế quan hệ này kết hợp với ghi đè (overriding) và che giấu (hiding)
cung cấp cho người lập trình công cụ đặc biệt mạnh.

Che giấu phương thức (method hiding) trong C#


Như trên đã phân tích, class con thừa hưởng tất cả các thành viên mà lớp
cha cho phép. Vậy điều gì xảy ra nếu trong lớp con chúng ta định nghĩa một
phương thức trùng với phương thức nó kế thừa từ lớp cha?
Hãy tưởng tượng bây giờ chúng ta xây dựng lớp Chicken kế thừa từ Bird
như sau:
class Chicken : Bird
{
public void Fly()
{
Console.WriteLine("Chicken cannot fly");
}
}
Vì Chicken kế thừa Bird, nó cũng thừa hưởng phương thức Fly của Bird.
Nhưng trong lớp Chicken lại định nghĩa một phương thức Fly với mô tả giống
hệt Fly của Bird.
Nếu để ý trong trình soạn thảo code, Intellisense của Visual Studio hiển thị
như sau:

Cảnh báo của Visual Studio về che giấu phương thức


Thông báo này có ý nghĩa là phương thức Fly của lớp Chicken sẽ che đi
phương thức Fly của lớp Bird.
Trong những tình huống tương tự, C# tự động áp dụng cơ chế che giấu
phương thức (method hiding).
Để đảm bảo đây đúng là hành động mà người lập trình mong muốn, C# yêu
cầu phải ghi rõ từ khóa “new” trước khai báo phương thức như sau:
public new void Fly()
{
Console.WriteLine("Chicken cannot fly");
}

208
Trong tình huống này cả hai phương thức Fly đều cùng tồn tại trong object
(của class con) nhưng phụ thuộc vào loại biến tham chiếu tới (biến chứa địa
chỉ) object (biến khai báo thuộc kiểu con hay biến khai báo thuộc kiểu cha)
sẽ quyết định sử dụng phương thức nào:
1. Nếu biến chứa địa chỉ object được khai báo là kiểu cha, phương thức
Fly của object cha sẽ được gọi;
2. Nếu biến chứa địa chỉ object được khai báo là kiểu con, phương thức
Fly của object con sẽ được gọi.
Trong ví dụ trên, nếu khai báo và khởi tạo như sau:
Chicken chicken = new Chicken();
chicken.Fly();
Kết quả thực hiện sẽ là:

Nếu khai báo và khởi tạo như sau:


Bird chicken = new Chicken();
chicken.Fly();
Kết quả thực hiện sẽ là:

Như vậy trong trường hợp này, phương thức Fly định nghĩa ở lớp con sẽ đơn
giản là “che” phương thức Fly mà nó kế thừa từ lớp cha. Cả hai cùng tồn tại
trong cùng một object. Kiểu của biến sẽ quyết định phương thức nào được
gọi.

Ghi đè phương thức (method overriding) trong C#


Bây giờ hãy điều chỉnh lớp Bird, phương thức Fly như sau (thêm từ khóa
virtual vào trước khai báo phương thức):
public virtual void Fly() => Console.WriteLine("Bird is flying");
Thay đổi phương thức Fly của lớp Chicken như sau (đổi từ khóa new thành
override):
public override void Fly()
{
Console.WriteLine("Chicken cannot fly");
}
Đoạn code:

209
Chicken chicken = new Chicken();
chicken.Fly();
sẽ cho kết quả:

Đoạn code:
Bird chicken = new Chicken();
chicken.Fly();
cho kết quả:

Hai kết quả này giống nhau. Vậy điều gì đã xảy ra?
Đây là kết quả hoạt động của cơ chế ghi đè (overring) phương thức.
Trong cơ chế ghi đè, phương thức Fly của lớp cha (mà Chicken kế thừa) sẽ
bị xóa bỏ và thay thế bằng phương thức Fly mới định nghĩa trong lớp
Chicken. Nói cách khác, trong object tạo ra từ Chicken giờ đây chỉ còn một
phương thức Fly duy nhất. Do đó, bất kể biến tham chiếu tới nó được khai
báo là kiểu gì thì cũng chỉ truy xuất được phương thức Fly này.
Để áp dụng được cơ chế ghi đè, cả lớp cha và lớp con cần phải phối hợp:
1. Lớp cha phải cho phép phương thức được phép ghi đè bằng cách thêm
từ khóa virtual trước khai báo phương thức;
2. Lớp con phải thông báo rõ việc ghi đè bằng cách thêm từ khóa
override trước định nghĩa phương thức.
Mặc định các phương thức của class không cho ghi đè mà chỉ cho phép che
giấu.
Tuy nhiên, các phương thức Equals, GetHashCode, ToString của lớp tổ
tiên System.Object đều cho phép ghi đè ở lớp hậu duệ.
Để xác định những phương thức nào cho phép ghi đè, chỉ cần viết từ khóa
override trong thân class (bên ngoài phương thức).

210
Ghi đè (override) phương thức
Che dấu được sử dụng chủ yếu để đảm bảo tương thích ngược giữa các
class. Cơ chế này không được sử dụng nhiều trong thực tế.
Ở phía khác, ghi đè được sử dụng rất phổ biến cùng với đa hình giúp tạo ra
một class đại diện cho các biến thể khác nhau.

Lớp trừu tượng và kế thừa


Lớp và trừu tượng hóa
Theo cách suy nghĩ hướng đối tượng, chúng ta phải trừu tượng hóa các đối
tượng để tạo ra class.
Ví dụ, từ việc phân tích nhiều chiếc bàn cụ thể chúng ta rút ra:
 Những chiếc bàn cụ thể phải có chân, dù là 3 chân, 4 chân hoặc nhiều
chân hơn. Như vậy, số chân là một đặc điểm chung của bàn.
 Mỗi chiếc bàn có thể sơn màu trắng, đỏ, vàng,… Vậy màu sắc cũng là
một đặc điểm chung.
 Mỗi chiếc bàn có thể to nhỏ khác nhau nhưng đều có một diện tích
mặt để sử dụng. Vậy diện tích bề mặt sử dụng cũng là một đặc điểm
của bàn.
Qua phân tích này chúng ta thấy có 3 loại thông tin có thể dùng để mô tả
cho một chiếc bàn bất kỳ: số chân, màu sắc, diện tích mặt. Tuy nhiên, mỗi
chiếc bàn cụ thể lại không giống nhau, thể hiện ở giá trị cụ thể của số chân,
màu sắc và diện tích mặt.
Như vậy, khi chúng ta mô tả “Bàn” bằng ba loại thông tin đại diện như trên,
chúng ta đã trừu tượng hóa từ những chiếc bàn cụ thể về một loại thông tin
chung mô tả cho bàn. Loại thông tin chung này chính là class, và từng chiếc
bàn cụ thể là object. Class, do đó, là dạng trừu tượng hóa, là mô tả chung
của các đối tượng cụ thể.
Bây giờ chúng ta lại phân tích tiếp những chiếc ghế và tủ theo cách tương
tự và lần lượt thu được các lớp Ghế và Tủ.
Nếu chúng ta tiếp tục phân tích những điểm chung của Bàn, Ghế, và Tủ,
chúng ta lại có thể trừu tượng hóa một lần nữa để tạo ra lớp Nội thất.
211
Tuy nhiên, khi nói đến nội thất, chúng ta lại không thể đưa ra hình dung
chính xác của nó. Khác với khi nói đến Bàn chúng ta hình dung đại khái
được một chiếc bàn.
Như vậy, Nội thất là một loại trừu tượng hóa cấp độ cao hơn nữa. Nó không
cho ra hình dung cụ thể nào mà chỉ có thể được hình dung thông qua các
class con cụ thể của nó là Bàn, Ghế, hoặc Tủ. Loại class để mô tả nội thất
như vậy trong lập trình hướng đối tượng có tên gọi riêng: lớp trừu tượng
(abstract class).
Lớp trừu tượng trong C#
Lớp trừu tượng (abstract class) là loại class có mức độ trừu tượng cao dùng
làm khuôn mẫu để tạo ra các class khác.
Như vậy, class bình thường là khuôn mẫu để tạo ra object (là những thực
thể), còn class trừu tượng lại dùng làm khuôn mẫu để tạo ra class khác. Sự
khác biệt này dẫn đến tình huống là lớp trừu tượng không được sử dụng để
tạo ra object như class bình thường.
Trong C#, lớp trừu tượng được xây dựng bằng cách thêm từ khóa abstract
vào trước từ khóa class khi khai báo. Lớp trừu tượng không thể dùng để
khởi tạo object mà chỉ đóng vai trò lớp cơ sở để tạo ra các lớp dẫn xuất, là
những trường hợp cụ thể hơn.
Ví dụ khai báo lớp trừu tượng Animal:
abstract class Animal // đây là một lớp trừu tượng
{

}
Lớp Animal không cho phép tạo object. Do đó, lệnh khởi tạo sau sẽ bị báo
lỗi:
var animal = new Animal(); // Lỗi! Lớp Animal không cho phép khởi tạo object

Phương thức trừu tượng


Một điểm rất mạnh của lớp trừu tượng là nó chứa bên trong các phương
thức trừu tượng (abstract method).
Phương thức trừu tượng (abstract method) là loại phương thức được khai
báo trong thân lớp trừu tượng với từ khóa abstract và không có thân
phương thức. Ví dụ:
abstract class Animal // đây là một lớp trừu tượng
{
public abstract void Eat(); // đây là khai báo phương thức trừu tượng Eat
không có thân.
public abstract void Move(); // khai báo phương thức trừu tượng Move.
}

212
Nếu trong thân một class khai báo một phương thức trừu tượng thì class
chứa nó bắt buộc phải khai báo là abstract.
Một class kế thừa từ lớp trừu tượng này bắt buộc phải ghi đè tất cả phương
thức trừu tượng của class mà nó thừa kế.
class Dog : Animal
{
public override void Eat() => Console.WriteLine("I love bone!");
public override void Move() => Console.WriteLine("I walk on 4 feet");
}
Phương thức trừu tượng mặc định được đánh dấu virtual (cho phép ghi đè)
nên bạn không cần (không được) dùng từ khóa virtual trước phương thức
abstract nữa.
Nếu không ghi đè đủ các phương thức abstract của lớp cha, lớp con bắt buộc
cũng phải đánh dấu là abstract:
abstract class Dog : Animal // Dog không ghi đè hết phương thức abstract của
Animal nên nó phải đánh dấu abstract
{
public override void Eat() => Console.WriteLine("I love bone!");
}
Yêu cầu này làm cho lớp và phương thức trừu tượng trở thành một công cụ
rất mạnh: nó tạo ra một “bản hợp đồng” chứa danh sách các phương thức
mà tất cả các lớp dẫn xuất bắt buộc phải thực thi.
Nói theo cách khác, lớp trừu tượng được sử dụng làm khuôn mẫu đề tạo ra
class khác. Để bắt các class dẫn xuất tuân thủ theo các quy tắc chung, trong
lớp trừu tượng sử dụng các phương thức trừu tượng với vai trò hợp đồng.
Các class dẫn xuất từ lớp trừu tượng bắt buộc phải tuân thủ hợp đồng khi
kế thừa từ lớp trừu tượng bằng cách xây dựng các phương án cụ thể của
phương thức trừu tượng.

213
Interface trong C#, loosely coupling
Interface trong C# là một bản “hợp đồng” mô tả những gì cần phải làm mà
các class thực thi interface đó phải tuân thủ theo. Interface trong C# là một
công cụ đặc biệt mạnh giúp tạo ra mối quan hệ lỏng giữa các class, qua đó
giúp phát triển và test các thành phần (class) một cách độc lập.
Tuy vậy, đây là một kiểu dữ liệu rất khó hiểu với nhiều bạn. Đặc biệt, rất
nhiều bạn không biết cách vận dụng interface trong lập trình.
Bài học này sẽ cố gắng giúp bạn hiểu interface là gì, vai trò của interface
trong C#, cũng như các kỹ thuật làm việc với interface trong C#.

Interface trong C# là gì?


Quan hệ phụ thuộc giữa các class
Class B được coi là phụ thuộc chặt vào class A nếu class A được sử dụng
trong code của B (như tham chiếu tới A, nhận tham số kiểu A, khởi tạo
object của A, khai báo biến của A,...).
Quan hệ phụ thuộc chặt này đơn giản khi sử dụng nhưng có thể gây ra
nhiều hậu quả. Quan hệ phụ thuộc chặt yêu cầu các lớp phụ thuộc phải xây
dựng sau, dẫn tới không thể phát triển song song các class. Quan hệ chặt
cũng có thể gây khó khăn cho việc test các class độc lập (vì chúng phụ
thuộc vào nhau).
Để có thể phát triển song song hoặc dễ dàng thay thế class này bằng class
khác, người ta cần làm giảm sự phụ thuộc giữa các class, thay phụ thuộc
chặt bằng phụ thuộc lỏng (loosely-coupling).
Một công cụ rất mạnh thường được sử dụng để làm giảm sự phụ thuộc này
là giao diện (interface).
Khái niệm interface trong C#
Interface là một kiểu dữ liệu tương tự như class nhưng chỉ đưa ra mô tả
(specification / declaration) của các thành viên mà không đưa ra phần thực
thi (phần thân, body / implementation). Phần thân của các phương thức sẽ
phải được xây dựng trong các class thực thi giao diện này.
Một cách gần đúng, interface gần giống như một abstract class trong đó tất
cả các phương thức của class đều được đánh dấu là abstract.
Cũng giống như abstract class, interface không thể dùng để khởi tạo object
mà chỉ để các lớp cụ thể “kế thừa”. Khi một class “kế thừa” từ một interface,
nó bắt buộc phải cung cấp phần thực thi cho tất cả các thành viên của
interface (tương tự như phải thực thi tất cả các thành viên abstract).
214
Interface tạo ra một bản “hợp đồng” mô tả những gì cần phải làm mà các
class thực thi interface đó phải tuân thủ theo. Khi đó, các class phối hợp với
nhau thông qua bản hợp đồng này mà không cần biết đến nhau nữa (làm
mất quan hệ chặt).
Vì đặc điểm đó, interface trở thành một công cụ đặc biệt mạnh giúp tạo ra
mối quan hệ lỏng giữa các class, qua đó giúp phát triển và test các thành
phần (class) một cách độc lập.
Khi sử dụng interface vẫn phải thực hiện khởi tạo object của một class cụ thể
thực thi interface này. Thao tác khởi tạo này thực hiện ở một class trung gian.
Ví dụ minh họa
Hãy cùng thực hiện ví dụ sau để hiểu kỹ hơn về cách sử dụng interface
using System;
namespace ConsoleApp
{
internal interface IPet // khai báo interface với hai phương thức
{
void Feed(); // mô tả phương thức (không có thân)
void Sound();
}
internal interface IBird // khai báo interface với ba phương thức
{
void Fly();
void Sound();
void Feed();
}
internal class Cat : IPet // Cat thực thi IPet
{
public Cat() => Console.WriteLine("I'm a cat. ");
// thực thi cho phương thức Feed và Sound
// hai phương thức này thực thi theo kiểu implicit
public void Feed() => Console.WriteLine("Fish, please!");
public void Sound() => Console.WriteLine("Meow meow!");
}
internal class Dog : IPet // Dog thực thi IPet
{
public Dog() => Console.WriteLine("I'm a dog. ");
// cả hai phương thức Feed và Sound thực thi kiểu explicit.
// Object của Dog không thể gọi hai phương thức này.
// Hai phương thức này chỉ có thể gọi qua giao diện IPet
void IPet.Feed() => Console.WriteLine("Bone, please!");
void IPet.Sound() => Console.WriteLine("Woof woof!");
}
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao diện
{
public Parrot() => Console.WriteLine("I'm a parrot. ");
// hai phương thức này thực thi kiểu implicit, do đó
// có thể gọi từ object của Parrot
public void Feed() => Console.WriteLine("Nut, please!");
public void Fly() => Console.WriteLine("Yeah, I can fly!");
// hai phương thức này thực thi kiểu explicit, do đó
// không thể gọi từ object của Parrot

215
// mà chỉ có thể gọi qua giao diện IPet hoặc IBird
void IPet.Sound() => Console.WriteLine("I can speak!");
void IBird.Sound() => Console.WriteLine("I can sing, too!");
}
internal class BirdLover
{
private IBird _bird;
public BirdLover(IBird bird) => _bird = bird;
public void Play()
{
// _bird có thể gọi đủ các phương thức của IBird
Console.Write("Fly ...");
_bird.Fly();
Console.Write("Say something ...");
_bird.Sound();
Console.Write("What do you like to eat? ");
_bird.Feed();
}
}
internal class PetLover
{
private IPet _pet;
public PetLover(IPet pet) => _pet = pet;
public PetLover() { }
public void Play()
{
// _pet có thể gọi đủ các phương thức của IPet
Console.Write("What do you like to eat? ");
_pet.Feed();
Console.Write("Now say something ... ");
_pet.Sound();
}
}
internal class _18_interface
{
private static void Main()
{
IPet pet = new Dog();
PetLover petLover = new PetLover(pet);
petLover.Play();
petLover = new PetLover(new Parrot());
petLover.Play();
BirdLover birdLover = new BirdLover(new Parrot());
birdLover.Play();
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
cat.Feed(); cat.Sound();
IPet cat2 = new Cat();
// cat2 có thể gọi Feed và Sound
cat2.Feed(); cat2.Sound();
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi được Sound
parrot.Feed(); parrot.Fly();
IBird parrot2 = new Parrot();
// (gọi qua giao diện) parrot2 gọi được đủ 3 phương thức của IBird
parrot2.Feed(); parrot2.Fly(); parrot2.Sound();
// dog không gọi được phương thức nào (gọi qua object) do
// cả hai phương thức của Dog đều thực hiện kiểu explicit

216
Dog dog = new Dog();
IPet dog2 = new Dog();
// gọi qua giao diện: dog2 gọi được cả Feed và Sound
dog2.Feed(); dog2.Sound();
Console.ReadKey();
}
}
}

Kỹ thuật lập trình với Interface trong C#


Khai báo kiểu interface
Trong ví dụ trên chúng ta xây dựng hai interface: IPet và IBird
 IPet
 IBird
internal interface IPet // khai báo interface với hai phương thức
{
void Feed(); // mô tả phương thức (không có thân)
void Sound();
}
Interface được khai báo với từ khóa interface và danh sách mô tả các
phương thức, đặc tính hoặc biến thành viên.
Một interface có thể được sử dụng nội bộ trong project, hoặc được sử dụng
bởi các project khác. Trong tình huống thứ nhất (mặc định), interface sử
dụng từ khóa điểu khiển truy cập internal (tương tự class), và do đó có thể
không cần viết từ khóa internal. Trong tình huống thứ hai sử dụng từ khóa
public.
Interface là một kiểu dữ liệu cùng cấp độ với class, do đó có thể được khai
báo trực tiếp trong không gian tên hoặc trong phạm vi của class khác. Tên
của interface được đặt giống quy ước tên class nhưng có thêm chữ “I” đứng
trước. Như ví dụ trên, tên hai interface lần lượt là IPet, IBird.
Trong interface chỉ có các mô tả, không có thân phương thức. Mô tả phương
thức không có từ khóa điều khiển truy cập (tức là không có public, private,
protected trước các mô tả).
Thực thi interface
Mặc dù interface là một kiểu dữ liệu nhưng tự bản thân nó không có khả
năng sinh ra object mà chỉ có thể tạo ra biến tham chiếu đến object của các
class khác tuân thủ theo quy định của interface.
Interface được sử dụng làm khuôn mẫu để sinh ra các class khác (gần giống
như lớp abstract). Việc tạo ra một class trên cơ sở khuôn mẫu của interface
gọi là thực thi interface.
Cấu trúc cú pháp để một class thực thi một interface như sau:
217
internal class Cat : IPet // Cat thực thi IPet
internal class Dog : IPet // Dog thực thi IPet
Một class cũng có thể thực thi nhiều interface:
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao diện
Khi một class thực thi một hoặc nhiều interface, nó có nghĩa vụ phải xây
dựng tất cả các thành viên được mô tả trong interface. Visual Studio hỗ trợ
bằng cách đánh dấu lỗi cú pháp (gạch chân đỏ) nếu class chưa xây dựng
đủ các thành viên của interface theo yêu cầu.
Có hai cách thức thực thi các thành viên của interface: implicit và explicit.
Trong cách thực thi implicit không chỉ rõ là phương thức được thực thi thuộc
về interface nào; ngược lại, cách thực thi explicit phải chỉ rõ phương thức
đang thực thi thuộc về interface nào.
Lớp Cat ở đây hoàn toàn áp dụng cách thực thi implicit.
internal class Cat : IPet // Cat thực thi IPet
{
public Cat() => Console.WriteLine("I'm a cat. ");
// thực thi cho phương thức Feed và Sound
// hai phương thức này thực thi theo kiểu implicit
public void Feed() => Console.WriteLine("Fish, please!");
public void Sound() => Console.WriteLine("Meow meow!");
}
Lớp Dog lại hoàn toàn thực thi kiểu explicit. Mỗi phương thức khi thực thi
phải chỉ rõ nó thuộc interface nào.
internal class Dog : IPet // Dog thực thi IPet
{
public Dog() => Console.WriteLine("I'm a dog. ");
// cả hai phương thức Feed và Sound thực thi kiểu explicit.
// Object của Dog không thể gọi hai phương thức này.
// Hai phương thức này chỉ có thể gọi qua giao diện IPet
void IPet.Feed() => Console.WriteLine("Bone, please!");
void IPet.Sound() => Console.WriteLine("Woof woof!");
}
Lớp Parrot áp dụng cả implicit và explicit
internal class Parrot : IPet, IBird // Parrot thực thi cả hai giao diện
{
public Parrot() => Console.WriteLine("I'm a parrot. ");
// hai phương thức này thực thi kiểu implicit, do đó
// có thể gọi từ object của Parrot
public void Feed() => Console.WriteLine("Nut, please!");
public void Fly() => Console.WriteLine("Yeah, I can fly!");
// hai phương thức này thực thi kiểu explicit, do đó
// không thể gọi từ object của Parrot
// mà chỉ có thể gọi qua giao diện IPet hoặc IBird
void IPet.Sound() => Console.WriteLine("I can speak!");
void IBird.Sound() => Console.WriteLine("I can sing, too!");
}
Nếu phương thức được thực thi theo kiểu explicit thì không được phép sử
dụng từ khóa điều khiển truy cập.
218
Sự khác biệt lớn nhất giữa implicit và explicit thể hiện ở việc sử dụng object
của class.

Sử dụng interface trong C#


Kiểu interface
Interface có thể sử dụng như một kiểu dữ liệu để khai báo biến. Biến của
interface cho phép gọi các thành viên của interface giống như một object
bình thường của class.
 BirdLover
 PetLover
internal class BirdLover
{
private IBird _bird;
public BirdLover(IBird bird) => _bird = bird;
public void Play()
{
// _bird có thể gọi đủ các phương thức của IBird
Console.Write("Fly ...");
_bird.Fly();
Console.Write("Say something ...");
_bird.Sound();
Console.Write("What do you like to eat? ");
_bird.Feed();
}
}
Trong hai class BirdLover và PetLover chúng ta sử dụng hai biến _bird và
_pet giống như một object bình thường.
Tuy nhiên, biến của interface bắt buộc phải tham chiếu tới một object thực
sự. Như trong hai lớp trên, object của class được truyền qua tham số của
phương thức khởi tạo. Nếu không cho biến của interface tham chiếu tới một
object thực sự, khi chạy chương trình sẽ gặp lỗi ‘Object reference not set to
an instance of an object’ ở các lời gọi hàm hoặc truy xuất thành viên.
Ví dụ, lệnh sau sẽ báo lỗi khi chạy:
PetLover petLover2 = new PetLover();
petLover2.Play();
Ở đây chúng ta sử dụng constructor không tham số của lớp PetLover (nghĩa
là không truyền object nào để gán cho biến _pet. Chương trình sẽ báo lỗi ở
lời gọi _pet.Feed() vì _pet không hề tham chiếu tới một object nào.
Khởi tạo object
Interface có thể dùng để khai báo biến (như ở trên) nhưng không thể tự
khởi tạo object. Biến kiểu interface chỉ có thể tham chiếu tới object của class
thực thi interface đó.

219
IPet pet = new Dog();
PetLover petLover = new PetLover(pet);
petLover.Play();
petLover = new PetLover(new Parrot());
petLover.Play();
BirdLover birdLover = new BirdLover(new Parrot());
birdLover.Play();
Nói một cách khác, chúng ta cần sử dụng một class cụ thể thực thi interface
để khởi tạo object rồi gán object đó cho biến interface.
Đối với các class thực thi interface phụ thuộc vào cách thực thi (explicit hay
implicit), có sự khác biệt khi sử dụng object của các class này:
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
cat.Feed();
cat.Sound();
IPet cat2 = new Cat();
// cat2 có thể gọi Feed và Sound
cat2.Feed();
cat2.Sound();
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi được Sound
parrot.Feed();
parrot.Fly();
IBird parrot2 = new Parrot();
// (gọi qua giao diện) parrot2 gọi được đủ 3 phương thức của IBird
parrot2.Feed();
parrot2.Fly();
parrot2.Sound();
// dog không gọi được phương thức nào (gọi qua object) do
// cả hai phương thức của Dog đều thực hiện kiểu explicit
Dog dog = new Dog();
IPet dog2 = new Dog();
// gọi qua giao diện: dog2 gọi được cả Feed và Sound
dog2.Feed();
dog2.Sound();
Việc thực thi implicit tạo cho object của class khả năng sử dụng các phương
thức như class bình thường:
Cat cat = new Cat();
// cat có thể gọi được Feed và Sound
cat.Feed();
cat.Sound();
Những phương thức nào được thực thi kiểu explicit thì không thể gọi được
trên object:
Parrot parrot = new Parrot();
// (gọi qua object) parrot chỉ gọi được Feed và Fly, không gọi được Sound
parrot.Feed();
parrot.Fly();
// dog không gọi được phương thức nào (gọi qua object) do
// cả hai phương thức của Dog đều thực hiện kiểu explicit
Dog dog = new Dog();
Vì có sự khác biệt giữa hai cách sử dụng object (parrot và parrot2)

220
Parrot parrot = new Parrot();
parrot.Feed();
parrot.Fly();
IBird parrot2 = new Parrot();
parrot2.Feed();
parrot2.Fly();
parrot2.Sound();
Chúng ta gọi cách sử dụng thứ nhất là “gọi qua object”, cách sử dụng thứ
hai gọi là “gọi qua interface”.
Trong ví dụ trên, sự phụ thuộc giữa các class được thể hiện qua sơ đồ code
sau.

Sơ đồ code của ví dụ

221
Partial class và Partial method trong C# – tách tập tin
code
Partial class và partial method là hai kỹ thuật đặc biệt trong C# giúp bạn
phân tách code của cùng một class và method ra nhiều tập tin mã nguồn
khác nhau. Các kỹ thuật này được sử dụng rất nhiều cùng với các bộ sinh
code tự động của Visual Studio. Các kỹ thuật này không khó nhưng có chút
khác lạ. Bài học này sẽ giải thích chi tiết cho bạn các vấn đề liên quan.

Partial class trong C#


Khái niệm partial class
Trong các bài học từ đầu đến giờ, bạn xây dựng mỗi class trong một tập tin
đặt trùng tên với class. Đây là cách thức tổ chức mã nguồn của class bình
thường và phổ biến trong tất cả các ngôn ngữ lập trình hướng đối tượng.
Giờ hãy hình dung một số tình huống khác.
Visual Studio có khả năng sinh code tự động để tạo ra nhiều class khác nhau.
Ví dụ, nếu bạn học lập trình với Windows Forms sẽ thấy, khi đặt một điều
khiển lên form, trình thiết kế của Visual Studio sẽ tự động sinh code tương ứng.
Vậy giờ nếu bạn tự viết thêm code của mình vào tập tin code được sinh tự động
đó, code của bạn có thể bị mất đi nếu form thay đổi (vì bạn không thể kiểm
soát việc sinh code tự động). Ngoài ra, code sinh tự động thường rất phức tạp.
Bạn rất khó theo dõi các code sinh tự động này.
Đây là tình hình khi lập trình Windows Forms trong C# 1.0, khi chưa có khái niệm partial class.
Visual Studio cũng có một công cụ riêng giúp sinh code tự động theo mẫu,
gọi là T4 Text Template. Bộ sinh code tự động này được sử dụng rất nhiều,
ví dụ, cho ASP.NET, Entity Framework. Nếu viết code vào tập tin code sinh
tự động, mỗi lần chạy lại bạn có thể mất hết code của mình.
Từ những tình huống trên dẫn đến một nhu cầu đặc biệt: xây dựng MỘT
class trên NHIỀU tập tin vật lý. Điều này giúp giải quyết những vấn đề vừa
nêu. Phần sinh tự động đặt ở một tập tin độc lập. Phần bạn tự viết nằm trên
một tập tin khác. Thay đổi phần này không ảnh hưởng đến phần kia. C# gọi
loại class xây dựng trên nhiều tập tin vật lý như vậy là partial class.
Partial class là một tính năng của C# cho phép định nghĩa một class trên
nhiều tập tin vật lý khác nhau. C# compiler sẽ tự động ghép nối các tập tin
mã nguồn này trong quá trình biên dịch.
Partial class là tính năng phục vụ cho các công cụ hỗ trợ thiết kế và sinh
code tự động.

222
Sử dụng partial class trong C# project
Hãy cùng thực hiện ví dụ sau. Thêm project tên PartialClass vào solution.
Thêm hai tập tin mã nguồn Student.Model.cs và Student.Methods.cs vào
project.

Lần lượt viết code cho các tập tin như sau:
Student.Model.cs
Studen.Methods.cs
Program.cs
using System;
namespace PartialClass
{
public partial class Student
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string Major { get; set; }
public string Specialization { get; set; }
}
}
Bạn để ý một vấn đề sau: Hai tập tin mã nguồn Student.Model.cs và
Student.Methods.cs đều chứa khai báo class Student trong cùng namespace
PartialClass. Rõ ràng, điều này nhẽ ra phải gây ra xung đột định danh (name
conflict). Tuy nhiên, khi bạn dịch chương trình sẽ thành công.
Vấn đề là ở chỗ, trước từ khóa class bạn gặp thêm từ khóa partial. Từ khóa
này báo cho C# compiler rằng hai khai báo này thuộc về cùng một class
Student. Chỉ là code được đặt trên hai tập tin mã nguồn khác nhau. Khi dịch
chương trình, C# compiler sẽ tự động ghép nối chúng lại thành một class
duy nhất.
Do C# hiểu rằng hai khai báo thuộc về một class duy nhất, trong client code
(phương thức Main), bạn sử dụng được những thành viên khai báo trên cả
hai tập tin. Nói cách khác, client code không phân biệt đây là partial class
hay class thông thường.

223
Một số vấn đề khi sử dụng partial class trong C#
Như vậy có thể thấy, việc khai báo và sử dụng partial class thực ra rất đơn
giản về cú pháp cũng như ý tưởng:
 Nếu trong class có nhiều thành phần khác nhau nhưng bắt buộc phải
thuộc về 1 class, bạn có thể sử dụng partial class để tách các phần đó
sang các tập tin riêng rẽ. Ví dụ, phần thiết kế của form và xử lý sự
kiện vốn có bản chất khác nhau nhưng phải thuộc về cùng class.
Visual Studio tự động tạo form làm partial class.
 Nếu trong một class có những thành phần code thường biến động
theo quá trình phát triển và có những thành phần ổn định thì cũng có
thể xem xét tách chúng ra các tập tin khác nhau để dễ quản lý. Trong
đó, thành phần biến động ra một tập tin riêng, thành phần ổn định
để lại một tập tin riêng. Ví dụ, các thành phần field, property,
constructor thường cố định. Trong khi đó các member method thường
biến động nhiều hơn. Bạn có thể tách code thành hai phần riêng biệt.
 Nếu bạn sử dụng công cụ sinh code tự động, hãy để class đó làm
partial class. Biết đâu về sau bạn cần bổ sung code tự viết.
Ngoài ra, khi sử dụng partial class cần lưu ý các vấn đề sau:
 Thứ nhất, các tập tin mã nguồn của partial class phải nằm trong cùng
một assembly (cùng trong một project). Nếu bạn viết thư viện class
và biên dịch để người khác sử dụng, trong đó có xây dựng một partial
class. Người dùng class đó không thể dùng cơ chế partial class để mở
rộng tiếp partial class của bạn được. Lý do rất đơn giản, partial class
chỉ là tách code ra nhiều tập tin riêng rẽ để sau compiler tự mình gom
lại. Một partial class đã được biên dịch thì compiler không thể gom
vào cùng code khác được nữa.
 Thứ hai, trong mỗi phần code của partial class, bạn không được khai
báo các thành viên trùng nhau. Lý do là, mặc dù trải rộng trên nhiều
tập tin mã nguồn khác nhau nhưng các phần của partial class thuộc
về cùng một class. C# không cho phép khai báo cùng một thành viên
nhiều lần.
 Thứ ba, mỗi phần của class bắt buộc phải có đủ hai từ khóa partial
class. Tuy nhiên, modifier (public | internal) thì chỉ cần viết một
lần. Lớp cơ sở hoặc interface mà class này thừa kế cũng chỉ cần viết
một lần.

224
Partial method trong C#
Khái niệm partial method
Trong partial class có thể chứa một loại thành viên đặc biệt mà class thông
thường không có: partial method. Tương tự như partial class, partial
method cũng hướng tới hỗ trợ sinh code tự động. Partial method xuất hiện
từ C# 3.0 và được xem như thành phần mở rộng cho partial class (xuất hiện
từ C# 2.0).
Partial method cho phép code sinh tự động gọi phương thức nhưng không
nhất thiết phải xây dựng (implement) phương thức đó. Do vậy, partial
method chỉ chứa signature (mô tả) mà không có phần implementation
(phần thân, phần thực thi). Nếu không tìm thấy phần thực thi của partial
method, compiler sẽ bỏ qua lệnh gọi partial method.
Người ta cũng thường gọi lời gọi partial method là hook. Hook nếu gắn với
phần thực thi sẽ được gọi như phương thức bình thường. Nếu không có phần
thực thi, hook sẽ được compiler bỏ qua.
Cơ chế trên giúp giữ khối lượng code nhỏ nhưng vẫn đảm bảo tính linh hoạt.
Sử dụng partial method trong C#
Hãy cùng thực hiện một ví dụ với partial method trong partial class. Chúng
ta lặp lại ví dụ trên nhưng với một số thay đổi nhỏ:
Student.Model.cs
Studen.Methods.cs
Program.cs
using System;
namespace PartialMethod
{
public partial class Student
{
private string _firstName;
private string _lastName;
partial void OnSettingFirstName(string value);
partial void OnSettingLastName(string value);
public int Id { get; set; }
public string FirstName
{
get => _firstName;
set
{
OnSettingFirstName(value);
_firstName = value;
}
}
public string LastName
{
get => _lastName;

225
set
{
OnSettingLastName(value);
_lastName = value;
}
}
public DateTime DateOfBirth { get; set; }
public string Major { get; set; }
public string Specialization { get; set; }
}
}
Khi dịch và chạy thử, chương trình sẽ báo lỗi khi gặp lệnh gán chuỗi rỗng cho FirstName khi
khởi tạo object student.
Trong ví dụ trên, ở tập tin Student.Model.cs chúng ta đã chuyển FirstName
và LastName thành full property với hai backed field lần lượt là _firstName
và _lastName.
Bạn khai báo hai partial method OnSettingFirstName và
OnSettingLastName:
partial void OnSettingFirstName(string value);
partial void OnSettingLastName(string value);
Bạn gọi hai partial method trong setter của hai property tương ứng:
public string FirstName
{
get => _firstName; set
{
OnSettingFirstName(value);
_firstName = value;
}
}
public string LastName
{
get => _lastName; set
{
OnSettingLastName(value);
_lastName = value;
}
}
Trong tập tin Student.Methods.cs bạn thực thi hai partial method này:
private void CheckName(string value)
{
var temp = value.Trim();
if (string.IsNullOrEmpty(temp) || temp.Contains(" "))
throw new System.Exception("Tên sai quy cách");
}
partial void OnSettingFirstName(string value)
{
CheckName(value);
}
partial void OnSettingLastName(string value)
{
CheckName(value);
}
226
Logic của các phương thức này rất đơn giản: nếu giá trị gán cho FirstName
hoặc LastName chứa dấu cách, hoặc là xâu rỗng thì phát ra exception. Vì lí
do này, nếu bạn chạy client code như trên thì chương trình sẽ báo lỗi và mở
lại giao diện code ngay.
Trong ví dụ trên, giả sử Student.Model.cs được sinh tự động. Rõ ràng, bạn
không muốn gán cứng logic kiểm tra tính hợp lệ của FirstName và LastName
mà muốn để cho người dùng tự mình thực hiện logic riêng.
Do đó, bạn khai báo hai partial method và đặt sẵn hai hook (hai lời gọi
partial method) ở những vị trí phù hợp. Phần thân của partial method (chứa
logic kiểm tra giá trị của FirstName và LastName) để dành cho người lập
trình tự viết trong tập tin Student.Methods.cs.
Nếu người lập trình không tự viết phần thực thi thì hook không có ý nghĩa.
Compiler bỏ qua lời gọi hook kia. Nếu người lập trình viết phần thực thi,
hook sẽ được thực thi như phương thức bình thường.
Các lưu ý khi sử dụng partial method trong C#
Partial method chỉ có thể sử dụng bên trong partial class. Không thể sử
dụng partial method trong class thông thường.
Partial method bao gồm ba phần (và thường nằm trong các tập tin khác
nhau): phần khai báo, phần sử dụng (gọi phương thức), phần thực thi.
Khai báo partial method bắt buộc phải bắt đầu bằng từ khóa partial, kết
thúc là dấu chấm phẩy sau danh sách tham số và không có thân.
Lời gọi partial method không có gì khác biệt với member method thông
thường.
Phần thực thi thường đặt trong một tập tin code khác. Phần thực thi giống
hệt như xây dựng một phương thức thông thường nhưng có từ
khóa partial ở đầu. Ngoài ra, phần thực thi và phần khai báo phải có
signature giống hệt nhau.
Partial method bắt buộc phải có return type là void và không được có tham
số out. Tuy nhiên, tham số ref vẫn sử dụng được nếu bạn cần giữ lại thay
đổi của tham số. Partial method không được sử dụng access modifier như
public, private, protected. Nó cũng không được sử dụng các modifier khác
như virtual, abstract, sealed,…
Nếu không tìm thấy phần thực thi, compiler sẽ bỏ qua lời gọi partial method
(coi như không có gì ở đó!).
Partial class và partial method là hai khái niệm rất riêng trong C# chuyên
dùng hỗ trợ cho việc sinh code tự động. Khi xây dựng class, có thể bạn sẽ
ít dùng đến những tính năng này. Tuy nhiên, nếu bạn đi sâu vào một số công
227
nghệ phát triển ứng dụng trong .NET (như Windows Forms, Entity
Framework), bạn sẽ dùng đến nó rất thường xuyên.

228
Generic trong C# – Lập trình tổng quát, tham số hóa
kiểu dữ liệu
Khi đọc tài liệu lập trình C#, bạn đã bao giờ gặp những lối viết lạ mắt chứa
những chữ T, T1, T2, như List<T>, Swap<T>, Action<T1, T2>? Trong C#,
những thứ có <T>, <T1, T2> đi đằng sau như vậy được gọi chung
là generic (hay generics). Kỹ thuật lập trình như vậy gọi là lập trình
generic hay lập trình tổng quát/lập trình khái quát.
Tôi đoán rằng, bạn hẳn đã từng làm việc với generic trong khi học lập trình
C#. Ít nhất bạn cũng đã sử dụng kiểu List<T>. Nếu bạn từng nghe đến và
sử dụng LINQ thì chắc chắn rằng bạn đã từng làm việc với generic. Chỉ có
điều bạn chưa biết đến tên gọi của nó mà thôi.
Bạn có biết rằng, generics là loại kỹ thuật lập trình đặc biệt phổ biến và hữu
dụng trong C#? Thực tế bạn có thể sử dụng generic với kiểu (type, class),
phương thức (method), giao diện (interface), đại diện (delegate), tập hợp
(collection), trong kế thừa. Không nắm kỹ về generic, bạn đã bỏ mất một
công cụ đặc biệt mạnh mẽ và thông dụng trong C#.
Bài viết này sẽ cung cấp cho bạn những thông tin quan trọng nhất và đầy
đủ nhất về lập trình generic trong C# .NET.

Lập trình generic trong C# là gì?


Trong C#, lập trình tổng quát (generic programming, generics), còn gọi
là lập trình khái quát, là một dạng lập trình đặc biệt, trong đó kiểu dữ
liệu (của biến thành viên, biến cục bộ, tham số, kiểu trả về của phương
thức,...) không được xác định trong giai đoạn xây dựng xây dựng đơn vị
code (như class, phương thức,...) mà chỉ được xác định ở giai đoạn khởi tạo
và sử dụng.
Có thể hình dung bản chất của generics trong C# là tham số hóa kiểu dữ
liệu. Nói cách khác, với generics, kiểu dữ liệu cũng là một tham số. Khi kiểu
dữ liệu là một tham số, chúng ta có thể tạo ra class, phương thức, interface
hay delegate để sử dụng với nhiều kiểu dữ liệu khác nhau mà không cần
phải viết lại code cho từng kiểu dữ liệu riêng rẽ. Qua đó generics giúp tái sử
dụng code hiệu quả.
Để thực hiện ý tưởng trên, ở giai đoạn ĐỊNH NGHĨA (KHAI BÁO), người ta
dùng một kiểu dữ liệu giả. Ở giai đoạn SỬ DỤNG, kiểu dữ liệu giả này sẽ
được thay thế bằng kiểu dữ liệu thực. Các chữ T, T1, T2 mà bạn có thể đã
thấy chính là kiểu dữ liệu giả.

229
Chúng ta dùng từ “kiểu dữ liệu giả” ở đây để nghe cho dân dã. Thuật ngữ
chính thức của nó là tham số kiểu (type parameter), đôi khi cũng được gọi
đơn giản là placeholder. Kiểu giả không nhất thiết phải đặt là T mà có thể
là bất kỳ ký tự/cụm ký tự nào. Tuy nhiên, người ta thường dùng nhất là T
(viết tắt của Type) hoặc các ký tự ở cuối bảng chữ cái.
Bạn có thể nhận ra sự tương tự giữa tham số của phương thức với tham số kiểu của generics.
Tham số của phương thức cũng là một loại “giá trị giả” mà ở giai đoạn xây dựng phương thức
bạn có thể sử dụng. Chỉ ở giai đoạn gọi phương thức, “giá trị thật” mới được truyền vào.
Khi nào nên sử dụng generics?
Khi lập trình, nếu gặp một trong hai tình huống dưới đây thì hãy nghĩ ngay
đến generics:
Nếu có sự trùng lặp code về mặt logic và cách xử lý dữ liệu, chỉ khác biệt
về kiểu dữ liệu: hãy nghĩ đến generics để tránh lặp code. Bạn sẽ gặp tình
huống này ngay trong phần giới thiệu về generic method dưới đây.
Nếu lúc xây dựng class chưa xác định được kiểu dữ liệu của các biến thành
viên, thuộc tính hoặc biến cục bộ (của phương thức) thì cần sử dụng lập
trình generic. Bạn sẽ gặp tình huống này khi xem xét generic class ở phần
sau.
Một số đặc điểm của generics trong C#
Dưới đây là tóm lược một số đặc điểm cần lưu ý khi sử dụng generics trong
C#.
1. Phương thức/lớp tổng quát cho phép lựa chọn kiểu dữ liệu ở giai đoạn
sử dụng, không phải ở giai đoạn định nghĩa;
2. Lập trình generic yêu cầu phải cung cấp một kiểu dữ liệu “giả” thay
thế đặt trong cặp dấu <>; tên kiểu dữ liệu giả thường là một chữ cái
in hoa nằm cuối bảng chữ cái (thông dụng nhất là T, U, V);
3. Trong code có thể sử dụng kiểu dữ liệu giả này tương tự như bất kỳ
kiểu dữ liệu “thật” nào;
4. Số lượng kiểu dữ liệu giả không giới hạn; nếu có nhiều kiểu giả thì
phân tách bởi dấu phẩy;
5. Có thể giới hạn kiểu giả (sẽ xem xét ở phần sau);
Đừng lo lắng nếu bạn chưa hiểu hết các vấn đề trên vì sau đây chúng ta sẽ
đi vào từng ví dụ cụ thể. Sau khi đọc hết bài, bạn hãy quay lại đây một lần
nữa nhé.

230
Generic được áp dụng cho các đối tượng nào trong C#?
Trong C#, generics có thể áp dụng cho: (1) class, (2) method, (3) interface,
(4) delegate.
Tùy vào đối tượng áp dụng, kiểu giả của generic chỉ khác biệt về phạm vi
tác dụng.
Đối với class và interface, kiểu giả tác dụng trong toàn bộ code của class.
Kiểu giả này có thể sử dụng làm kiểu cho biến thành viên, thuộc tính, kiểu
trả về của phương thức thành viên, kiểu tham số của phương thức thành
viên.
Đối với method, kiểu giả chỉ có tác dụng trong phạm vi code của method
đó. Nghĩa là kiểu giả có thể được sử dụng làm kiểu của biến cục bộ, kiểu trả
về, kiểu tham số của method.
Đối với delegate, kiểu giả có thể sử dụng làm kiểu của tham số và kiểu kết
quả trả về.
Kỹ thuật cụ thể với từng loại đối tượng mời bạn đọc tiếp ở các phần dưới
đây.

Generic method trong C#


Ví dụ minh họa – Swap
Hẳn bạn đều biết loại phương thức Swap dùng để tráo giá trị của hai biến.
Giờ chúng ta cùng xây dựng một phương thức Swap đơn giản như sau:
using System;
namespace ConsoleApp
{
internal class Program
{
private static void Swap(ref int a, ref int b)
{
var temp = b;
b = a;
a = temp;
}
private static void Main(string[] args)
{
int a = 1, b = 2;
Console.WriteLine($"Before: a = {a}, b = {b}");
Swap(ref a, ref b);
Console.WriteLine($"After : a = {a}, b = {b}");
Console.ReadKey();
}
}
}

231
Trong ví dụ này chúng ta viết một phương thức Swap để tráo giá trị của hai
biến kiểu int. Phương thức này được sử dụng trong phương thức Main cho
kết quả như sau:

Kết quả chạy chương trình swap


Giả sử bạn cần hoán đổi giá trị của hai biến kiểu bool, bạn sẽ phải viết thêm
một phương thức có code tương tự, chỉ thay duy nhất int bằng bool. Nếu
muốn hoán đổi hai biến kiểu char (ký tự), bạn lại phải viết thêm một phương
thức nữa tương tự.
Rõ ràng ở đây các phương thức có cùng một logic, chỉ khác biệt duy nhất
kiểu dữ liệu mà nó xử lý. Trong tình huống này, generics là một lựa chọn
hợp lý giúp chống lặp code.
Bây giờ chúng ta viết lại phương thức Swap ở trên theo cách sau:
using System;
namespace ConsoleApp
{
internal class Program
{
private static void Swap<T>(ref T a, ref T b)
{
var temp = b;
b = a;
a = temp;
}
private static void Main(string[] args)
{
int a = 1, b = 2;
Console.WriteLine($"type = {a.GetType()}");
Console.WriteLine($"Before: a = {a}, b = {b}");
Swap(ref a, ref b);
Console.WriteLine($"After : a = {a}, b = {b}");
bool aa = true, bb = false;
Console.WriteLine($"type = {aa.GetType()}");
Console.WriteLine($"Before: a = {aa}, b = {bb}");
Swap(ref aa, ref bb);
Console.WriteLine($"After : a = {aa}, b = {bb}");
Console.ReadKey();
}
}
}
Đây là một ví dụ về cách xây dựng và sử dụng generic method trong C#.
Chúng ta có thể thấy cùng một phương thức Swap<T> giờ có thể dùng cho
cả kiểu int và bool mà không cần viết lại cho mỗi kiểu dữ liệu cụ thể. Nói
232
cách khác, bản thân kiểu dữ liệu của tham số giờ cũng lại là một loại tham
số mà chúng ta có thể cung cấp khi gọi phương thức.
Cách lập trình generic method
Ở giai đoạn định nghĩa phương thức Swap<T> mới, chúng ta không biết
được người dùng muốn sử dụng kiểu dữ liệu cụ thể nào. Vì vậy, chúng ta
sử dụng một kiểu dữ liệu giả T. Cú pháp generic trong C# quy định kiểu giả
phải đặt trong cặp dấu ngoặc <T>. Ở đây, hai biến phải có cùng kiểu dữ
liệu, do đó Swap chỉ dùng một kiểu giả T. Nếu có nhiều kiểu giả, chúng ta
có thể viết gộp vào cùng cặp ngoặc <T1, T2, T3>.
Tất cả các thao tác trên dữ liệu thuộc kiểu <T> này thực hiện giống như nó
là một kiểu dữ liệu thực thụ.
Đối với generic method, kiểu giả chỉ có ý nghĩa và sử dụng được trong thân
của phương thức. Ở phương thức Swap, chúng ta đã dùng kiểu T để khai
báo biến tạm temp.
Kiểu giả có thể sử dụng làm kiểu của tham số. Hai tham số a và b của
Swap<T> ở trên đều thuộc kiểu giả T. Ngoài ra, kiểu giả có thể sử dụng
làm kiểu trả về của phương thức như bất kỳ kiểu dữ liệu bình thường nào.
Phương thức generic phải được ghi rõ với cặp dấu ngoặc với kiểu giả,
Swap<T>, thay vì Swap. Bởi vì Swap<T> và Swap là hai phương thức hoàn
toàn khác nhau.
Đến giai đoạn sử dụng Swap<T> người ta mới thay T bằng một kiểu dữ liệu
cụ thể.
Nói tóm lại, generic method nên được xem xét sử dụng nếu:
1. Logic của các phương thức giống hệt nhau, chỉ khác biệt về kiểu dữ
liệu: có thể chuyển đổi về generic method để tránh lặp code.
2. Ở giai đoạn định nghĩa phương thức chúng ta xác định phải sử dụng
cho nhiều loại kiểu dữ liệu khác nhau.
3. Chưa xác định được kiểu dữ liệu cụ thể khi định nghĩa class.

Generic class trong C#


Ví dụ minh họa
Hãy cùng xem xét ví dụ sau đây:
using System;
namespace ConsoleApp
{
class ListInt
{
private int[] _data;
public int Count => _data.Length;
233
public ListInt(int size) => _data = new int[size];
public void Set(int index, int value)
{
if (index >= 0 && index < _data.Length) _data[index] = value;
}
public int Get(int index)
{
if (index >= 0 && index < _data.Length) return _data[index];
return default(int);
}
}
class ListChar
{
private char[] _data;
public int Count => _data.Length;
public ListChar(int size) => _data = new char[size];
public void Set(int index, char value)
{
if (index >= 0 && index < _data.Length) _data[index] = value;
}
public char Get(int index)
{
if (index >= 0 && index < _data.Length) return _data[index];
return default(char);
}
}
internal class Program
{
private static void Main(string[] args)
{
var listInt = new ListInt(10);
for (var i = 0; i < listInt.Count; i++)
Console.Write($"{listInt.Get(i)}t");
var listChar = new ListChar(10);
for (var i = 0; i < listChar.Count; i++)
Console.Write($"{listChar.Get(i)}t");
Console.ReadKey();
}
}
}
Trong ví dụ trên chúng ta xây dựng hai class để “bao bọc” một mảng, đồng
thời cung cấp phương thức để truy xuất mảng thay vì để người sử dụng trực
tiếp truy xuất. Một lớp dành cho kiểu int, một lớp dành cho kiểu char.
Giả sử chúng ta cần xử lý thêm kiểu bool thì sẽ lại phải viết thêm một class
riêng nữa.
Điều đáng lưu ý là logic của các class đều giống nhau. Sự khác biệt duy
nhất nằm ở kiểu dữ liệu cụ thể mà class đó xử lý. Rõ ràng là có tình trạng
lặp code ở đây.
Bây giờ chúng ta sẽ thay đổi code theo cách sau đây:
using System;
namespace ConsoleApp
{
class List<T>
234
{
private T[] _data;
public int Count => _data.Length;
public List(int size) => _data = new T[size];
public void Set(int index, T value)
{
if (index >= 0 && index < _data.Length) _data[index] = value;
}
public T Get(int index)
{
if (index >= 0 && index < _data.Length) return _data[index];
return default(T);
}
}
internal class Program
{
private static void Main(string[] args)
{
var listInt = new List<int>(10);
for (var i = 0; i < listInt.Count; i++)
Console.Write($"{listInt.Get(i)}t");
var listChar = new List<char>(10);
for (var i = 0; i < listChar.Count; i++)
Console.Write($"{listChar.Get(i)}t");
Console.ReadKey();
}
}
}
Nếu dịch và chạy thử cả hai đoạn code cho ra cùng một kết quả.
Đây là ví dụ về cách khai báo và sử dụng generic class.
Có thể nhận xét rằng, bản thân kiểu dữ liệu của biến cục bộ _data giờ cũng
là một tham số, thay vì là một kiểu cố định. Giá trị của tham số kiểu này sẽ
được cung cấp khi khởi tạo object của class.
Cách lập trình generic class
So với generic method mà chúng ta đã xem xét ở phần trên, cách sử dụng
kiểu giả đối với generic class là không khác biệt. Sự khác biệt lớn nhất nằm
ở chỗ: phạm vi có ý nghĩa của kiểu giả bây giờ là toàn bộ class, thay vì chỉ
trong một phương thức.
Như vậy chúng ta có thể thấy, nếu nhiều class có chung logic, chỉ khác biệt
về một hoặc nhiều kiểu dữ liệu cần xử lý thì có thể viết một generic class
thay cho viết nhiều class riêng rẽ. Nó sẽ giúp chúng ta tránh phải viết code
lặp nhiều lần.
Hãy xem xét một góc nhìn khác: giả sử chúng ta phải xây dựng một lớp List
để chứa một danh sách các giá trị để về sau chúng ta hoặc một lập trình
viên khác sử dụng.
Tuy nhiên, lúc xây dựng lớp List này chúng ta muốn nó có khả năng chứa
được nhiều kiểu dữ liệu khác nhau. Có những kiểu có thể tại thời điểm viết
235
lớp List thậm chí còn chưa được định nghĩa! Generics là giải pháp cho tình
huống này.
Nói tóm lại, lúc xây dựng lớp List chúng ta không xác định được kiểu dữ liệu
của các phần tử sẽ chứa trong nó là gì. Khi đó, chúng ta nên nghĩ tới sử
dụng generic.
Qua phần trình bày về generic method và generic class chúng ta giờ có thể dễ dàng hình dung
hơn về generics trong C# .NET: đó là sự tham số hóa kiểu dữ liệu.
(1) Ở trong đơn vị code nào mà kiểu dữ liệu không xác định, hoặc không cố định, kiểu dữ liệu
đó sẽ chuyển thành tham số, gọi là tham số kiểu.
(2) Cách viết tham số kiểu tuân thủ cú pháp của C#, <T1, T2, T3, … >, và đứng ngay sau đơn
vị code.
Ngoài ra, việc áp dụng generics cho interface giống hệt như đối với class, áp dụng
cho delegate giống như đối với method.

Giới hạn kiểu trong generic


Trong các ví dụ trên, kiểu giả T về sau có thể thay bằng bất kỳ kiểu dữ liệu
nào, dù là kiểu có sẵn (built-in) hoặc kiểu do người dùng tự định nghĩa. Điều
này có lợi là chúng ta về sau không chịu ràng buộc gì về kiểu dữ liệu thực.
Nhưng đi cùng với nó là những hạn chế khi dùng kiểu giả trong code.
Nếu bạn sử dụng intellisense sẽ thấy, biến thuộc kiểu T có đúng những
phương thức và thuộc tính của kiểu Object!
Có thể bạn đã biết, Object (hay object) là kiểu cha của mọi loại kiểu dữ liệu trong C#. Khi T
không bị giới hạn, nó có thể nhận cả kiểu Object. Do vậy, chúng ta chỉ có thể sử dụng được các
phương thức và thuộc tính tương tự như của Object trên biến kiểu T nếu T không bị giới hạn.
Giả sử bạn cần so sánh các biến thuộc kiểu T. Bạn thử code xem có được
không? Tôi cam đoan là không được. Chúng ta không thể so sánh hai object
bất kỳ trong C#.
Từ đây đặt ra yêu cầu về giới hạn kiểu giả T trong generic để biến thuộc
kiểu giả này có những đặc điểm chúng ta mong muốn.
Nghe có vẻ lý thuyết quá phải không?! Hãy cùng thực hiện một ví dụ.
Ví dụ minh họa
Dưới đây là code cài đặt của thuật toán sắp xếp chọn (selection sort):
using System;
namespace P01_SelectionSort
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Selection Sort";

236
var numbers = new[] { 10, 3, 1, 7, 9, 2, 0 };
Sort(numbers);
Console.ReadKey();
}
static void Swap<T>(T[] array, int i, int m)
{
T temp = array[i];
array[i] = array[m];
array[m] = temp;
}
static void Print<T>(T[] array)
{
Console.WriteLine(string.Join("\t", array));
}
static void Sort<T>(T[] array) where T : IComparable
{
for (int i = 0; i < array.Length - 1; i++)
{
int m = i;
T minValue = array[i];
for (int j = i + 1; j < array.Length; j++)
{
if (array[j].CompareTo(minValue) < 0)
{
m = j;
minValue = array[j];
}
}
Swap(array, i, m);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Step {i + 1}: i = {i}, m = {m}, min =
{minValue}");
Console.ResetColor();
Print(array);
Console.WriteLine();
}
}
}
}
Hãy nhìn dòng số 23 và 31.
Ở dòng số 23 xuất hiện một lệnh lạ mắt: where T : IComparable. Đây là
cú pháp để giới hạn kiểu (type constraint) thực mà kiểu giả T có thể nhận.
Cụ thể trong trường hợp này, T chỉ có thể được thay thế bằng những class
thực thi giao diện IComparable.
IComparable là giao diện mà nếu class nào thực thi thì ta có thể trực tiếp so sánh object của nó.
Ví dụ, bạn có thể so sánh các số (nguyên, thực), so sánh hai chuỗi, so sánh hai ký tự.
Rõ ràng, để sắp xếp các phần tử của mảng thì ta phải so sánh được giá trị
các phần tử. Nhưng để so sánh được giá trị của hai object thì class của
object đó bắt buộc phải thực thi giao diện IComparable. Từ đây dẫn đến yêu
cầu là kiểu giả T bắt buộc phải là các class thực thi giao diện IComparable.
Cấu trúc where T : IComparable chính là để thực hiện giới hạn này.

237
Việc đặt ra giới hạn kiểu giúp viết code an toàn hơn. Nếu có vi phạm giới
hạn kiểu, compiler sẽ báo lỗi ngay trong quá trình dịch. Thậm chí
Intellisense hỗ trợ kiểm tra lỗi ngay trong giai đoạn viết code.
Các loại giới hạn kiểu thường gặp
Ở trên chúng ta đã gặp một loại giới hạn kiểu: kiểu chính thức phải thực thi
một giao diện (interface) nào đó.
Ngoài ra, C# cung cấp nhiều loại giới hạn kiểu khác nhau. Sau đây là một
số thường gặp.
Giới hạn kiểu là class
where T : class
Loại giới hạn này yêu cầu kiểu thực thay thế cho T không được phép là các
kiểu như int, double, struct, enum. Tức là, T phải là các kiểu reference (các
class built-in hoặc class tự tạo), chứ không được là các kiểu value.
Giới hạn kiểu value
where T : struct
Đây là loại giới hạn ngược lại so với trường hợp trên. Ở đây T bắt buộc phải
là các kiểu value (int, double, struct, enum,...), không được phép là class.
Giới hạn về constructor của class
where T: new()
Giới hạn này yêu cầu lớp thay thế cho T phải có phương thức khởi tạo
(constructor) không tham số. Yêu cầu này sử dụng khi cần thực hiện khởi
tạo object của T trong code generic.
Giới hạn kiểu con
where T: <base class name>
Ví dụ:
where T: Bird
Giới hạn này yêu cầu T phải là class con của một lớp khác. Trong ví dụ trên,
T bắt buộc phải là lớp con của Bird.
Nhiều giới hạn đồng thời nhiều kiểu giả
where T: class where U:struct
Nếu có nhiều kiểu giả, mỗi kiểu giả được viết giới hạn riêng rẽ.
Từ khóa default – giá trị mặc định của kiểu dữ liệu
Có thể bạn đã biết, khi khai báo một biến, C# sẽ tự động gán cho biến đó
một giá trị ban đầu. Giá trị đó gọi là giá trị mặc định của kiểu (default value).

238
Ví dụ, kiểu int có giá trị mặc định là 0, kiểu bool là false, tất cả các kiểu
tham chiếu (như string, DateTime, các class do người dùng xây dựng) là
null.
Hãy đặt vào một tình huống khác. Một biến của bạn đã được thay đổi giá
trị, giờ bạn muốn nó nhận lại giá trị mặc định. Hay nói cách khác, bạn đang
muốn reset lại giá trị của biến đó về giá trị mặc định. Tình huống này chắc
chắn không hiếm gặp phải không ạ?
Khi đó bạn cho nó nhận giá trị mặc định nào đây? Bạn đâu có biết kiểu thực
sự của biến đó là gì. Vì bản thân kiểu dữ liệu cũng đang là tham số kia mà.
C# cung cấp cho chúng ta từ khóa default, dùng để tự động xác định giá
trị mặc định của một kiểu bất kỳ, kể cả khi kiểu đó chỉ là một tham số.
Giả sử tham số kiểu bạn đặt là <T> như mọi khi. Các lệnh sau:
x = default(T);
y = default(T);
sẽ gán giá trị mặc định của kiểu T về cho x và y. Còn T là gì thì chúng ta
không quan tâm.

Một số ứng dụng của generics trong C#


Các kiểu generic collection trong C# .NET
Để tiện lợi cho người lập trình trong xử lý dữ liệu, C# cung cấp nhiều kiểu
dữ liệu tập hợp khác nhau. Tất cả các kiểu dữ liệu dạng generic collection
được đặt trong không gian tên System.Collections.Generics. Trong
không gian tên này chứa nhiều class khác nhau như List<T>, Stack<T>,
Queue<T>, LinkedList<T>.
List<T> thuộc loại danh sách, là một dạng mảng động, với phần tử thuộc
kiểu T, trong đó T có thể là bất kỳ kiểu dữ liệu nào của C# và .NET. Kiểu
của phần tử được xác định trong lúc khai báo và khởi tạo object (vì đây là
kiểu generic). T có thể là bất kỳ kiểu dữ liệu nào của C# và .NET.
Tương tự như vậy, Stack<T> là lớp cài đặt cấu trúc ngăn xếp (stack) trong
C#.
Queue<T> là lớp cài đặt cấu trúc dữ liệu hàng đợi (queue).
LinkedList<T> là lớp cài đặt cấu trúc dữ liệu danh sách liên kết (linked list).
Dictionary<TKey, TValue> là lớp cài đặt cấu trúc dữ liệu từ điển (dictionary)
với hai kiểu giả: TKey là kiểu của khóa, TValue là kiểu của giá trị.
LINQ
Generics là một trong 4 thành phần tạo ra thư viện LINQ.

239
Ba thành phần còn lại lần lượt là Phương thức mở rộng (extension method), kiểu ủy nhiệm
(delegate), biểu thức lambda (lambda expression).
Kết quả của hầu hết các loại truy vấn LINQ là danh sách. Không ai biết được
người dùng viết truy vấn để lấy ra phần tử thuộc kiểu nào. Thậm chí, kiểu
của phần tử còn không xác định rõ (gọi là kiểu vô danh – anonymous type).
Do vậy, generics là không thể thiếu khi tạo ra các class chứa kết quả truy
vấn.
Generic delegate
C# hỗ trợ người lập trình bằng cách định nghĩa ra một loạt kiểu dữ
liệu generic delegate mà chúng ta có thể trực tiếp sử dụng ngay để khai báo
biến. Sử dụng generic delegate giúp bỏ qua giai đoạn khai báo kiểu
delegate.
Về cơ bản, generic delegate là các kiểu delegate đã được định nghĩa sẵn sử
dụng cơ chế generic. .NET Framework định nghĩa 3 nhóm generic delegate:
Actions, Funcs, Predicates.
Actions là các kiểu delegate tương ứng với các phương thức không trả về
dữ liệu (đầu ra là void). Các kiểu Action được định nghĩa trong namespace
System như sau:
namespace System
{
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
// còn các delegate tương tự như vậy nữa
// .NET Framework định nghĩa tổng cộng 16 delegate như vậy với số lượng tham số
đầu vào từ 1 đến 16.
}
Funcs là các kiểu delegate tương ứng với các phương thức có trả về dữ liệu.
Các kiểu funcs được định nghĩa trong không gian tên System như sau:
namespace System
{
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
// có 17 delegate tương tự như vậy
}
Predicate là kiểu delegate được định nghĩa sẵn như sau (trong System):
public delegate bool Predicate<in T>(T obj);

240
Delegate trong C# – Ủy nhiệm hàm, tham chiếu tới
phương thức
Nếu từng học C/C++, có lẽ bạn đã nghe tới khái niệm con trỏ hàm. Trong
C# cũng có một công cụ với tác dụng tương tự: delegate.
Nếu không biết đến con trỏ hàm, hãy tưởng tượng tình huống khác: Khi xây
dựng class cho người khác sử dụng bạn cần gọi một phương thức để tính
kết quả nhưng không biết người dùng thích tính toán kiểu gì.
Những tình huống tương tự dẫn đến nhu cầu về một kiểu dữ liệu đặc biệt
trong C#: kiểu delegate.
Nếu bạn chưa từng nghe hoặc chưa hiểu đầy đủ về delegate, bạn đã bỏ sót
một công cụ rất mạnh giúp class uyển chuyển hơn. Nhiều feature trong C#
như cơ chế xử lý sự kiện (trong Windows forms hoặc WPF) hay LINQ hoạt
động dựa trên delegate.
Nếu bạn chưa biết gì về delegate, hoặc còn đang biết mập mờ, bài viết này
sẽ dành cho bạn.

Delegate là gì? Tại sao trong C# lại cần delegate


Để hiểu kiểu dữ liệu này, hãy cùng xem một số tình huống sau.
Giả sử chúng ta cần xây dựng một class, trong class này sẽ phải gọi một
phương thức để thực hiện một hành động nào đó. Tuy nhiên chúng ta lại
không biết được phương thức này khi xây dựng class! Phương thức này chỉ
xuất hiện khi người khác sử dụng class để khởi tạo object.
Bạn đã từng đụng chạm vào Windows Forms hay WPF chưa? Nếu có chắc
bạn sẽ để ý, nút bấm (Button) là một class đã xây dựng sẵn, còn phương
thức xử lý sự kiện bấm nút (OnClick) lại do bạn tự viết. Làm sao để người
xây dựng class Button biết và chạy phương thức xử lý sự kiện do bạn viết
ra?
Vậy phải làm thế nào để hoàn thành yêu cầu “gọi một phương thức khi
phương thức chưa tồn tại hoặc chưa xác định” ở giai đoạn xây dựng
class?
Những tình huống khi không biết trước được phải gọi một phương thức cụ
thể nào dẫn đến việc phải sử dụng một loại công cụ đặc biệt trong C#:
delegate.
Delegate là những kiểu dữ liệu trong C# mà biến tạo ra từ nó chứa tham
chiếu tới phương thức, thay vì chứa giá trị hoặc chứa tham chiếu tới object
của các class bình thường.

241
Thuật ngữ “delegate” dịch sang tiếng Việt có thể là kiểu đại diện hoặc kiểu ủy nhiệm. Tuy
nhiên, hai lối dịch này không được sử dụng phổ biến trong các tài liệu. Vì vậy, trong bài viết
này chúng ta sẽ sử dụng thuật ngữ gốc tiếng Anh – delegate.
Một biến được tạo ra từ một kiểu delegate được gọi là một biến delegate.
Mỗi kiểu delegate khi được định nghĩa chỉ cho phép biến của nó chứa tham
chiếu tới những phương thức phù hợp với quy định của delegate này.
Vai trò của delegate trong C#
Delegate cho phép một class uyển chuyển và linh động hơn trong việc sử
dụng phương thức. Theo đó, nội dung cụ thể của một phương thức không
được định nghĩa sẵn trong class mà sẽ do người dùng class đó tự định nghĩa
trong quá trình khởi tạo object. Điều này giúp phân chia logic của một class
ra các phần khác nhau và do những người khác nhau xây dựng.
Delegate được sử dụng để giúp một class object tương tác ngược trở lại với
thực thể tạo ra và sử dụng class đó. Điều này giúp class không bị “cô lập”
bên trong thực thể đó. Ví dụ, delegate giúp gọi các phương thức của thực
tế tạo ra và chứa class object.
Với khả năng tạo ra tương tác ngược như vậy, delegate trở thành hạt nhân
của mô hình lập trình hướng sự kiện, được sử dụng trong công nghệ
winforms và WPF.
Ví dụ, khi xây dựng các điều khiển của Windows form (nút bấm, nút chọn,
menu,...), người lập trình ra các lớp này không thể xác định được người
dùng muốn làm gì khi nút được bấm, khi menu được chọn. Do đó, bắt buộc
phải sử dụng cơ chế của delegate để chuyển logic này sang cho người sử
dụng các lớp đó viết code. Như vậy, Button khi được tạo ra trong một Form
có khả năng tương tác với code của Form, chứ không cô lập chính mình.
Delegate cũng được sử dụng phổ biến với mô hình lập trình bất đồng bộ ở
dạng các phương thức callback, hoặc trong lập trình đa luồng.
Ví dụ minh họa
Hãy thực hiện và phân tích ví dụ sau để thấy rõ hơn cách khai báo và sử
dụng delegate trong C#.
Trong ví dụ này chúng ta “giả lập” một chương trình giúp tính toán và vẽ
đồ thị hàm số (vẽ giả thôi, không phải vẽ thật đâu). Trong đó chúng ta sẽ
viết một class giúp tính toán và in giá trị của hàm số trong một dải giá trị.
Hàm số thật sự sẽ do người dùng class tự tạo và cung cấp sau.
using System;
namespace ConsoleApp
{
/* khai báo kiểu delegate MathFunction:

242
* kiểu này có mô tả là (double) -> double
* nghĩa là có thể gán bất kỳ hàm nào "nhận biến kiểu double,
* trả về kiểu double" cho biến thuộc kiểu MathFunction
*/
internal delegate double MathFunction(double x);
// giả lập việc vẽ đồ thị hàm số
internal class Graph
{
/* khai báo property thuộc kiểu MathFunction.
* MathFunction được sử dụng như những kiểu dữ liệu thông thường
*/
public MathFunction Function { get; set; }
/* phương thức này có 1 tham số đầu vào là kiểu delegate MathFunction.
* Kiểu delegate làm tham số không khác gì kiểu dữ liệu bình thường
*/
public void Render(MathFunction function, double[] range)
{
// có thể gán biến thuộc kiểu delegate như bình thường
Function = function;
// vì function là một object bình thường, nó cũng có những thuộc tính
// và phương thức như các object khác. Thực tế tất cả kiểu delegate đều
// kế thừa thừa lớp System.Delegate. Ở đây đang dùng thuộc tính Method
// của lớp này.
Console.WriteLine($"Drawing the function graph: {function.Method}");
foreach (var x in range)
{
// mặc dù function là một object nhưng có thể "gọi" như gọi hàm.
// đây là sự khác biệt giữa object thuộc kiểu delegate với object
// tạo ra từ class bình thường
var y = function(x);
// ngoài cách gọi này còn còn thể dùng cấu trúc dưới đây
// var y = function.Invoke(x);
//
// var y = function?.Invoke(x);
Console.Write($"{y:f3} ");
}
Console.WriteLine("rn-----------------");
}
}
// một lớp thử nghiệm chứa các phương thức có mô tả (double)->double
internal class Mathematics
{
// đây là một instance method
public double Cos(double x) => Math.Cos(x);
// đây là một static method
public static double Tan(double x) => Math.Tan(x);
}
internal class Program
{
// một static method khác
private static double Sin(double x)
{
return Math.Sin(x);
}
private static void Main(string[] args)
{
Graph graph = new Graph();
// khởi tạo vùng giá trị của x

243
double[] range = new double[] { 1.0, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0 };
// truyền hàm Sin làm tham số cho Render
graph.Render(Sin, range);
// truyền hàm static Tan cho Render
graph.Render(Mathematics.Tan, range);
// truyền hàm instance Cos cho Render
Mathematics math = new Mathematics();
graph.Render(math.Cos, range);
// truyền một hàm có sẵn Sqrt của lớp Math trong .net
graph.Render(Math.Sqrt, range);
// tạo một hàm vô danh tuân theo mô tả (double) -> double
// và gán nó cho biến function
// biến function là biến thuộc kiểu delegate MathFunction
MathFunction function = delegate (double x) { return x *= 2; };
// truyền biến function cho hàm Render
graph.Render(function, range);
// khai báo và truyền hàm vô danh trực tiếp tại vị trí tham số
graph.Render(delegate (double x) { return x++; }, range);
// khai báo và truyền hàm lambda trực tiếp tại vị trí tham số
graph.Render((double x) => { return x *= 10; }, range);
// truyền một hàm lambda rút gọn làm tham số
graph.Render(x => x / 10, range);
Console.ReadKey();
}
}
}

Kỹ thuật lập trình với delegate trong C#


Trong phần này chúng ta sẽ phân tích chi tiết kỹ thuật sử dụng delegate đã
gặp trong ví dụ minh họa trên.
Khai báo kiểu delegate
Trong ví dụ trên, chúng ta khai báo một kiểu delegate tên là MathFunction
trực tiếp trong không gian tên.
internal delegate void MathFunction();
Vì kiểu delegate có cùng cấp độ với class, nó có thể được khai báo trực tiếp
trong không gian tên, cũng như có thể khai báo làm một kiểu nội bộ bên
trong class (giống như khai báo class bên trong class).
Kiểu MathFunction này có hình thức tương tự với một phương thức nhận
một biến kiểu double và trả lại một giá trị double.
Qua đây chúng ta thấy, về mặt hình thức, khai báo một kiểu delegate giống
như khai báo một phương thức không có thân, chỉ cần thêm từ
khóa delegate trước kiểu trả về và kết thúc khai báo bằng dấu chấm phẩy.

244
Mô tả phương thức và delegate
Để dễ dàng hơn trong việc mô tả phương thức (và kiểu delegate), người ta
đưa ra một quy ước mô tả như sau: (danh sách tham số theo kiểu) -> kiểu
đầu ra. Ví dụ, phương thức
private float Div(int a, int b){}
có mô tả là (int, int) -> float.
Khi đọc mô tả này chúng ta sẽ hiểu: phương thức (không quan tâm đến tên
gọi) có hai tham số đầu vào cùng kiểu int và trả về kết quả thuộc kiểu float.
Với quy ước viết này, MathFunction ở trên đại diện cho tất cả các phương
thước có mô tả là (double) -> double.
Tất cả các phương thức có mô tả (double)->double đều có thể gán cho biến
thuộc kiểu MathFunction. Mô tả này cũng được gọi là mô tả
của MathFunction.
Khai báo và sử dụng biến delegate
Sau khi khai báo kiểu, có thể khai báo biến thuộc kiểu dữ liệu này tương tự
như khai báo các biến bình thường:
/* khai báo property thuộc kiểu MathFunction.
* MathFunction được sử dụng như những kiểu dữ liệu thông thường
*/
public MathFunction Function { get; set; }

Tham số thuộc kiểu delegate có thể được khai báo và gán như tham số bình
thường:
/* phương thức này có 1 tham số đầu vào là kiểu delegate MathFunction.
* Kiểu delegate làm tham số không khác gì kiểu dữ liệu bình thường
*/
public void Render(MathFunction function, double[] range)
{
// có thể gán biến thuộc kiểu delegate như bình thường
Function = function;

Biến thuộc kiểu delegate cũng là một object, tương tự như các object tạo ra
từ class, nó cũng có thuộc tính và phương thức như các object bình thường.
Thực tế, tất cả kiểu delegate do người dùng định nghĩa đều kế thừa từ
lớp System.Delegate nên cũng được kế thừa các thuộc tính và phương thức
của class này.
Bởi vì biến delegate sẽ chứa tham chiếu tới một phương thức, ta có thể sử
dụng tên biến delegate như một phương thức thực thụ, nghĩa là có thể “gọi”
245
biến này như gọi phương thức bình thường. Khi “gọi” một biến delegate,
phương thức nó trỏ tới sẽ được thực thi.
// vì function là một object bình thường, nó cũng có những thuộc tính
// và phương thức như các object khác. Thực tế tất cả kiểu delegate đều
// kế thừa thừa lớp System.Delegate. Ở đây đang dùng thuộc tính Method
// của lớp này.
Console.WriteLine($"Drawing the function graph: {function.Method}");
foreach (var x in range)
{
// mặc dù function là một object nhưng có thể "gọi" như gọi hàm.
// đây là sự khác biệt giữa object thuộc kiểu delegate với object
// tạo ra từ class bình thường
var y = function(x);
// ngoài cách gọi này còn còn thể dùng cấu trúc dưới đây
// var y = function.Invoke(x);
// kiểm tra biến delegate trước khi gọi để tránh lỗi
// var y = function?.Invoke(x);
Console.Write($"{y:f3} ");
}

Ngoài cách “gọi” biến delegate như gọi phương thức, các kiểu delegate còn
có thêm phương thức Invoke giúp gọi phương thức biến này trỏ tới:
var y = function.Invoke(x);
Cẩn thận hơn nữa chúng ta có thể kiểm tra object trước khi gọi Invoke:
var y = function?.Invoke(x);
Phép toán ? cho phép kiểm tra xem một object function có giá trị null hay không. Nếu object
nhận giá trị khác null mới thực hiện phương thức Invoke. Cách sử dụng này là an toàn nhất.
Nếu function nhận giá trị null thì phương thức Invoke sẽ không được gọi. Nếu không kiểm tra
null, lời gọi phương thức trên một object null sẽ làm phát sinh lỗi.
Truyền tham số kiểu delegate cho phương thức
Ở giai đoạn khởi tạo object của class, người dùng class mới truyền phương
thức cụ thể cho tham số thuộc kiểu delegate.
// truyền hàm Sin làm tham số cho Render
graph.Render(Sin, range);
// truyền hàm static Tan cho Render
graph.Render(Mathematics.Tan, range);
// truyền hàm instance Cos cho Render
Mathematics math = new Mathematics();
graph.Render(math.Cos, range);

246
// truyền một hàm có sẵn Sqrt của lớp Math trong .net
graph.Render(Math.Sqrt, range);

Nếu một tham số của phương thức là biến delegate, chúng ta có thể trực
tiếp truyền tên của một phương thức có chung mô tả với kiểu delegate.
Lưu ý: truyền một phương thức làm tham số khác với truyền lời gọi phương thức làm tham số.
Truyền lời gọi phương thức có thể xem như tương đương với truyền dữ liệu bình thường (string,
bool, int,...), không liên quan đến delegate.

Generic delegate
Như đã biết khi xem xét về delegate, để làm việc với delegate chúng ta cần
trước hết khai báo delegate như khai báo kiểu dữ liệu bình thường, sau đó
sử dụng kiểu delegate đó để khai báo biến. Đến giai đoạn sử dụng chúng ta
gán biến delegate đó với một phương thức phù hợp với yêu cầu của kiểu
delegate.
C# hỗ trợ người lập trình bằng cách định nghĩa ra một loạt kiểu dữ
liệu generic delegate mà chúng ta có thể trực tiếp sử dụng ngay để khai báo
biến. Sử dụng generic delegate giúp bỏ qua giai đoạn khai báo kiểu
delegate.
Về cơ bản, generic delegate là các kiểu delegate đã được định nghĩa sẵn sử
dụng cơ chế generic. .NET Framework định nghĩa 3 nhóm generic delegate:
Actions, Funcs, Predicates.
Actions
Actions là các kiểu delegate tương ứng với các phương thức không trả về
dữ liệu (đầu ra là void). Các kiểu Action được định nghĩa trong không gian
tên System như sau:
namespace System
{
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
// còn các delegate tương tự như vậy nữa
// .NET Framework định nghĩa tổng cộng 16 delegate như vậy với số lượng tham số đầu vào từ 1 đến 16.
}

.NET Framework định nghĩa tổng cộng 16 generic delegate, cái đầu tiên có
1 tham số đầu vào, cái cuối cùng có 16 tham số đầu vào.
Như vậy, kiểu Action<T1,..> có thể tương ứng với bất kỳ phương thức nào
không trả về giá trị và có từ 1 đến 16 tham số đầu vào (bất kỳ kiểu gì).

247
Riêng delegate void Action() tương ứng với các phương thức không nhận
tham số và không trả về giá trị.
Như vậy, khi sử dụng Action hoặc Action<T1,..> sẽ không cần khai báo các
kiểu delegate có kiểu ra là void nữa (và có ít hơn 16 tham số đầu vào).
Dưới đây là một số ví dụ về cách sử dụng các kiểu actions:
Action action1 = () => Console.WriteLine("Hello world");
Action<string> action2 = (s) => Console.WriteLine(s);
Action<string, int> action3 = (s, i) => { for (int j = 0; j < i; j++) Console.WriteLine(s); };

Funcs
Funcs là các kiểu delegate tương ứng với các phương thức có trả về dữ liệu.
Các kiểu funcs được định nghĩa trong không gian tên System như sau:
namespace System
{
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
// có 17 delegate tương tự như vậy
}

Tương tự như với actions, .NET Framework cũng định nghĩa 17 kiểu delegate
như trên với số lượng tham số đầu vào từ 0 đến 16.
Định nghĩa kiểu funcs khác với actions ở chỗ, funcs luôn phải có kiểu đầu ra
đặt ở vị trí cuối cùng trong danh sách kiểu giả generic (out TResult). Sau
đây là một số ví dụ sử dụng funcs:
Func<int> func1 = () => 0;
Func<int, int> func2 = (i) => i * 10;
Func<int, int, float> func3 = (a, b) => a / b;
Console.WriteLine($"func1: {func1}; func2: {func2(10)}; func3: {func3(1, 2)}");

Biến func1 được khai báo và tham chiếu tới hàm lambda không nhận tham
số và luôn trả về giá trị 0 (kiểu int); biến func2 tham chiếu tới hàm lambda
nhận tham số vào kiểu int, nhân tham số đó với 10 và trả lại kết quả này
(kiểu int); biến func3 tham chiếu tới hàm lambda nhận hai số nguyên và
trả về kết quả phép chia (kiểu float).
Predicate
Predicate là kiểu delegate được định nghĩa sẵn như sau (trong System):
namespace System

248
{
public delegate bool Predicate<[NullableAttribute(2)] in T>(T obj);
}

Như vậy, predicate là kiểu delegate tương ứng với các phương thức có 1
tham số đầu vào và trả về giá trị bool. Predicate được sử dụng trong các
biểu thức so sánh. Dưới đây là một số ví dụ:
Predicate<int> predicate1 = (i) => i > 100;
Console.WriteLine($"is 10 > 100? it's {predicate1(10)}");
Predicate<string> predicate2 = (s) => s.Length > 10;

Biến predicate1 tham chiếu tới một hàm lambda thực hiện so sánh xem một
số nguyên có lớn hơn 100 hay không; biến predicate2 tham chiếu tới một
hàm lambda nhận một chuỗi ký tự và so sánh xem độ dài chuỗi đó có lớn
hơn 10 không.
Như chúng ta thấy trong các ví dụ trên đều sử dụng hàm lambda với
delegate. Khi sử dụng theo kiểu này chúng ta có thể hoàn toàn bỏ qua việc
khai báo kiểu của tham số trong hàm lambda vì C# có thể tự suy đoán ra
kiểu. Ngoài ra, nếu thân phương thức chỉ có 1 lệnh duy nhất thì sử dụng lối
viết “expression body” cho gọn.

Sử dụng delegate với phương thức vô danh, hàm lambda,


hàm cục bộ
Để truyền tham số thuộc kiểu delegate cho một phương thức, chúng ta phải
viết các phương thức có mô tả mà kiểu delegate yêu cầu.
Do đặc thù của C#, phương thức bắt buộc phải được định nghĩa trong một
class (dù là instance method hay static method). Đối với instance method,
chúng ta còn phải khởi tạo object trước khi truyền phương thức này làm
tham số cho phương thức khác qua delegate.
Nếu phương thức này chỉ được sử dụng một lần duy nhất lúc truyền làm
tham số, hoặc phương thức quá đơn giản (chỉ bao gồm 1 dòng lệnh), việc
xây dựng hàng loạt phương thức nhỏ như vậy có thể làm code bị phân mảnh
khiến khó theo dõi và quản lý.
C# 2 bắt đầu đưa vào khái niệm phương thức vô danh (anonymous
method), C# 3 đưa vào khái niệm hàm lambda (lambda statement) giúp
xây dựng các phương thức sử dụng một lần như vậy. Hiện nay hàm lambda
được sử dụng phổ biến hơn.

249
Phương thức vô danh
Phương thức vô danh (anonymous method) cũng là một loại phương thức,
tuy nhiên khác biệt với phương thức bình thường ở một số điểm:
1. Không có tên: vì phương thức vô danh chủ yếu được sử dụng làm
tham số cho phương thức khác, điều quan trọng nhất là hoạt động
của phương thức (thân phương thức), còn tên không quan trọng;
phương thức loại này cũng không được gọi lại (tái sử dụng) ở nhiều
nơi trong code như phương thức bình thường, do đó cũng không cần
tên gọi.
2. Có thể khai báo trực tiếp ở chỗ cần dùng: ví dụ có thể khai báo thẳng
trong danh sách tham số của hàm khác, và do đó, có thể truy xuất cả
các biến cục bộ của phương thức nơi nó được khai báo.
Trong các tài liệu có thể sử dụng hai cách gọi: phương thức vô danh hoặc
phương thức nặc danh. Hai cách gọi này là tương đương. Trong bài giảng
này chúng ta thống nhất gọi là phương thức vô danh.
Hãy cùng xem ví dụ sau đây về khai báo phương thức vô danh:
Chúng ta vẫn tiếp tục sử dụng ví dụ minh họa đã làm ở bài trước. Ở đây
chúng ta không khai báo thêm phương thức bình thường như trước mà trực
tiếp khai báo một phương thức vô danh và gán cho biến function thuộc
kiểu MathFunction:
// tạo một hàm vô danh tuân theo mô tả (double) -> double
// và gán nó cho biến function
// biến function là biến thuộc kiểu delegate MathFunction
MathFunction function = delegate (double x) { return x *= 2; };
// truyền biến function cho hàm Render
graph.Render(function, range);
// khai báo và truyền hàm vô danh trực tiếp tại vị trí tham số
graph.Render(delegate (double x) { return x++; }, range);

Chúng ta cũng có thể khai báo phương thức vô danh trực tiếp ở vị trí tham
số:
graph.Render(delegate (double x) { return x++; }, range);
Qua ví dụ này có thể thấy, khai báo phương thức vô danh chỉ khác biệt với
phương thức thông thường ở chỗ, vị trí tên phương thức được thay thế bằng
từ khóa delegate.

250
Hàm lambda
Hàm lambda cũng có những đặc trưng của phương thức vô danh nhưng có
thể bỏ qua luôn khai báo kiểu của các tham số.
Điều này xuất phát từ thực tế là hàm lambda khi được khai báo làm tham
số của một phương thức thì các tham số của nó bắt buộc phải tuân thủ theo
mô tả của delegate. Do đó C# compiler có thể tự suy ra kiểu của các tham
số mà không cần viết rõ ra.
Tương tự, một hàm lambda được khai báo hoàn toàn giống như một phương
thức bình thường, khác biệt lớn nhất là nó có thể được khai báo thẳng trong
thân phương thức khác (giống phương thức vô danh), và danh sách tham
số được nối với thân phương thức bằng dấu =>. Phương thức lambda cũng
rất thường được sử dụng với expression body.
// khai báo và truyền hàm lambda trực tiếp tại vị trí tham số
graph.Render((double x) => { return x *= 10; }, range);
// truyền một hàm lambda rút gọn làm tham số
graph.Render(x => x / 10, range);

Ở tình huống thứ hai chúng ta thấy danh sách tham số của hàm lambda
thậm chí không có cả tên kiểu dữ liệu.
Lý do là vì hàm này dùng làm tham số thứ nhất cho phương thức Render.
Tham số này đã được quy định mô tả là (double) -> double. Do đó, tham
số x sẽ được C# tự hiểu là thuộc kiểu double. Khả năng tự suy đoán kiểu
này kết hợp với expression body giúp hàm lambda trở nên rất ngắn gọn.
Hãy cùng xem kết quả thực hiện của chương trình ví dụ trên:

251
Kết quả thực hiện chương trình
Ở những vị trí chúng ta truyền phương thức vô danh và hàm lambda, C#
compiler tự sinh ra tên gọi cho các hàm này. Tuy nhiên, vì lúc code không
có tên, chúng ta không có khả năng gọi lại các hàm này ở những vị trí khác
trong code.
Hàm cục bộ
Từ C# 7, chúng ta có thêm một khả năng nữa để xây dựng một phương
thức trong thân một phương thức khác bên cạnh sử dụng phương thức vô
danh và hàm lambda: sử dụng hàm cục bộ (local function).
Hàm cục bộ là một phương thức được định nghĩa bên trong thân một phương
thức khác.
Hàm cục bộ hoàn toàn không khác biệt với các phương thức thông thường.
Tuy nhiên hàm cục bộ không có từ khóa điều khiển truy cập và chỉ có thể
được gọi bên trong thân phương thức chứa nó.
C# có hai điều khác biệt với nhiều ngôn ngữ khác thuộc họ C/C++: (1) có thể khai báo class
bên trong class, và (2) có thể khai báo phương thức bên trong phương thức.

252
Event (sự kiện) trong C#
Event (sự kiện) là một khái niệm rất phổ biến trong lập trình và được sử
dụng với mô hình publisher/subscriber. Trong .NET, event được sử dụng
trong các mô hình lập trình cho giao diện đồ họa như Windows Forms hoặc
Windows Presentation Foundation. Event và Delegate trong .NET có quan
hệ rất gần gũi và thường gây nhầm lẫn.
Trong bài học này chúng ta sẽ cùng xem xét chi tiết về event cũng như
phân biệt event với delegate.

Event là gì?
Trong bài học trước bạn đã nắm được khái niệm và cách sử dụng cơ bản
của delegate trong C# .NET. Một trong những vai trò của delegate mà chúng
ta đã nhắc tới là giúp object tương tác với object khác.
Khả năng hỗ trợ tương tác của delegate được vận dụng trong một mô hình
lập trình gọi là publisher/subscriber, thường gọi tắt là mô hình pub/sub.
Bạn có thể dễ dàng hình dung về mô hình này tương tự như khi bạn đăng
ký theo dõi một kênh Youtube. Trong đó kênh Youtube đóng vai
trò publisher (hoặc cũng được gọi là broadcaster). Bạn và những người
đăng ký khác đóng vai trò subscriber.
Mỗi khi có video mới, kênh sẽ thực hiện thông báo cho người theo dõi. Hành
động này trong mô hình event được gọi là raise/invoke/broadcast (tạm dịch
là phát). Khi một event được raise/invoke/broadcast, các subscriber sẽ thực
hiện các hoạt động riêng của mình (như có bạn thì thích xem ngay, có bạn
lại muốn để vào danh sách xem sau, người thì muốn tải về máy,...).
Khi sử dụng delegate bạn hoàn toàn có thể thực hiện mô hình pub/sub.
Tuy nhiên mô hình pub/sub đưa ra thêm hai yêu cầu quan trọng:
1. Các subscriber không được biết và không ảnh hưởng lẫn nhau
2. Việc raise/invoke một event chỉ được phép thực hiện bởi
broadcaster
Delegate không đáp ứng được các yêu cầu này do:
1. delegate cho phép subscriber sử dụng phép gán =. Khi đó tất cả
các subscriber sẵn có sẽ bị hủy.
2. client code cũng có thể kích hoạt delegate.
Vì vậy .NET đưa thêm vào từ khóa event để dễ dàng vận dụng delegate vào
mô hình pub/sub.

253
Như vậy, event trong .NET thực chất chỉ là một dạng hạn chế của delegate
để phù hợp với mô hình pub/sub. Hoặc cũng có thể nói rằng event được xây
dựng bên trên delegate. Do đó, trước khi làm việc với event, bạn phải hiểu
delegate.

Kỹ thuật lập trình với Event trong C# .NET


Để hiểu kỹ thuật lập trình với event trong C#, hãy cùng xem ví dụ sau:
using static System.Console;
namespace ConsoleApp1
{
internal class Program
{
private static void Main(string[] args)
{
Title = "C# events - TuHocICT";
var gasoline = new Gasoline();
// đăng ký theo dõi sự kiện
gasoline.PriceChanged += Stock_PriceChanged1;
gasoline.PriceChanged += Stock_PriceChanged2;
// tăng giá xăng
gasoline.Price += 50;
// lại tăng giá
gasoline.Price += 20;
// hủy đăng ký theo dõi sự kiện
gasoline.PriceChanged -= Stock_PriceChanged1;
// tăng giá tiếp
gasoline.Price += 10;
ReadKey();
}
// cách xử lý sự kiện 1
private static void Stock_PriceChanged1(decimal oldPrice, decimal newPrice)
{
WriteLine($"Damn it! The price changed again to {newPrice}đ");
}
// cách xử lý sự kiện 2
private static void Stock_PriceChanged2(decimal oldPrice, decimal newPrice)
{
WriteLine($"The price has been changed from ${oldPrice} to {newPrice}đ");
}
}
// delegate đứng sau event
internal delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
internal class Gasoline
{
private decimal _price = 11000; // giá gốc là 11.000đ
// khai báo event dựa trên delegate
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get => _price;
set
{
if (_price == value) return;
var oldPrice = _price;
_price = value;

254
// chạy sự kiện
PriceChanged?.Invoke(oldPrice, _price);
}
}
}
}
Đây là một chương trình đơn giản minh họa việc tăng giá xăng và phản ứng
từ hai người theo dõi. Trong ví dụ này, Gasoline là publisher/broadcaster,
hai phương thức Stock_PriceChanged1 và Stock_PriceChanged2 là
các subscriber và cũng được gọi là các event handler (phương thức xử lý sự
kiện).
Kết quả thu được như sau:

Hãy để ý, hai lần đầu tăng giá thì cả hai người đều phản ứng. Riêng lần cuối
cùng chỉ có 1 người phản ứng vì người kia đã hủy theo dõi sự kiện.
Khai báo kiểu delegate sử dụng cho event
Do event thực chất cũng là cách tương tác giữa các object, bạn cần đến một
kiểu delegate để quy định hình thức của các phương thức subscriber:
delegate void PriceChangedHandler(decimal oldPrice, decimal
newPrice);
Khai báo “biến” event
Chúng ta dùng kiểu delegate này để khai báo một sự kiện với từ khóa event:
public event PriceChangedHandler PriceChanged;
Với kiểu delegate này bạn hoàn toàn có thể khai báo một biến chấp nhận
các phương thức có cùng mô tả (decimal, decimal)->void. Khai báo biến so
với khai báo event chỉ khác biệt duy nhất là không có từ khóa event. Do vậy
bạn cũng có thể hình dung khai báo event thực chất cũng chỉ là khai báo
một biến delegate (đặc biệt).
Quy ước đặt tên event: event được đặt tên theo quy ước PascalCase (viết
hoa chữ cái đầu mỗi từ).

255
Kích hoạt sự kiện
Mỗi khi thay đổi giá xăng (gán giá trị mới cho property Price) thì kích hoạt
sự kiện, tức là gửi thông báo đến tất cả subscriber (mà thực tế mình chưa
biết).
PriceChanged?.Invoke(oldPrice, _price);
Đây thực chất là lời gọi delegate. Trên thực tế, việc sử dụng event bên trong
class publisher hoàn toàn không có gì khác biệt với sử dụng biến delegate.
Ở trên chúng ta sử dụng phương thức Invoke của kiểu Delegate cơ sở (mà
mọi kiểu delegate đều kế thừa). Đồng thời chúng ta sử dụng thêm phép
toán kiểm tra null ?.
Đây là cách kích hoạt event an toàn (mà bạn cũng nên sử dụng với biến
delegate thông thường). Lý do là nếu không có subscriber nào, biến event
PriceChanged sẽ nhận giá trị null. Mọi lệnh từ biến null sẽ đều dẫn đến
NullException.
Dĩ nhiên bạn cũng có thể kích hoạt event như một delegate thông thường
bằng lời gọi trực tiếp:
PriceChanged(oldPrice, _price);
Tuy nhiên cách làm này không an toàn và không khuyến khích sử dụng.
Đăng ký/hủy đăng ký theo dõi sự kiện
Sự khác biệt của event so với delegate là ở cách sử dụng trong client code.
Đối với event bạn chỉ có thể dùng phép toán += (đăng ký theo dõi) hoặc -
= (hủy đăng ký), không thể sử dụng phép gán =.
// đăng ký theo dõi sự kiện
gasoline.PriceChanged += Stock_PriceChanged1;
gasoline.PriceChanged += Stock_PriceChanged2;

// hủy đăng ký theo dõi sự kiện


gasoline.PriceChanged -= Stock_PriceChanged1;
Sở dĩ bạn chỉ sử dụng được phép toán += hoặc -= là để đảm bảo rằng các
handler (subscriber) không ảnh hưởng (ghi đè, xóa lãn nhau) đến nhau.
Đây là cách event hạn chế tác dụng của delegate nhằm đảm bảo thực thi
mô hình pub/sub.
Dĩ nhiên, ở vị trí của subscriber bạn có thể sử dụng phương thức thành viên,
phương thức tĩnh, hàm lambda hoặc hàm cục bộ, giống hệt như đối với
delegate.

256
Một khi hủy đăng ký, khi broadcaster kích hoạt sự kiện, phương thức
subscriber sẽ không được kích hoạt nữa.
Trong Visual Studio, event được hiển thị với biểu tượng khác biệt với biến
và property:

Nếu bạn đặt dấu chấm sau tên event, bạn sẽ thấy rằng intellisense không
hề hiển thị thêm cái gì. Đây là cách event ngăn chặn truy cập vào các thành
viên của delegate đứng sau event.
Như vậy qua phần này bạn có thể thấy, làm việc với event thực chất là làm
việc với delegate nhưng ở một dạng hạn chế để đảm bảo phù hợp với yêu
cầu của mô hình pub/sub.

Mô hình sự kiện chuẩn của .NET


Ở phần trên bạn đã thấy lập trình với event thực chất là làm việc với
delegate. Bạn có thể tiếp tục sử dụng mô hình như trên.
Tuy nhiên, .NET đưa ra một mẫu tiêu chuẩn để làm việc với event với mục
đích giúp lập trình event được chuẩn hóa ở cả các class của .NET lẫn ở client
code (người dùng viết).
Mô hình này đưa ra hai yêu cầu:
Thứ nhất, .NET yêu cầu gộp tất cả các thông tin broadcaster cần gửi cho
subscriber vào một class con của lớp EventArgs (System.EventArgs).
Lấy ví dụ, nếu cần cung cấp thông tin về giá cũ và giá mới cho event handler
(subscriber), bạn cần xây dựng một class như sau:
class PriceChangedEventArgs : EventArgs {
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs(decimal lastPrice, decimal newPrice) {
LastPrice = lastPrice;
NewPrice = newPrice;
}
}

257
Class này kế thừa từ EventArgs và chứa hai thông tin cần cho các subscriber
LastPrice và NewPrice.
Để dễ phân biệt, các class con kế thừa từ EventArgs cũng thường có hậu tố
(postfix) là EventArgs như bạn đã thấy ở lớp PriceChangedEventArgs.
Yêu cầu này thống nhất việc truyền thông tin từ publisher tới subscriber.
Thứ hai, delegate đứng sau mỗi event cần tuân thủ 3 quy định:
1. kiểu trả về phải là void.
2. phải có hai tham số: tham số thứ nhất thuộc kiểu object, tham số
thứ hai là class con của EventArgs (mà bạn đã xây dựng theo yêu cầu
thứ nhất).
Tham số thứ nhất sẽ được sử dụng để chứa thông tin về broadcaster.
Tham số thứ hai chứa thông tin broadcaster gửi cho subscriber.
3. tên của delegate phải có hậu tố EventHandler.
Yêu cầu này thống nhất cách xây dựng kiểu delegate chống lưng cho event.
Lấy ví dụ delegate trong ví dụ trên cần được cải tạo thành:
delegate void PriceChangedEventHandler(object sender,
PriceChangedEventArgs args);
Lớp Gasoline viết lại theo mô hình event tiêu chuẩn của .NET như sau:
// delegate đứng sau event
delegate void PriceChangedEventHandler(object sender, PriceChangedEventArgs args);
internal class Gasoline {
private decimal _price = 11000; // giá gốc là 11.000đ
// khai báo event dựa trên delegate
public event PriceChangedEventHandler PriceChanged;
public decimal Price {
get => _price;
set {
if (_price == value) return;
var oldPrice = _price;
_price = value;
// chạy sự kiện
var args = new PriceChangedEventArgs(oldPrice, _price);
PriceChanged?.Invoke(this, args);
}
}
}

Lối viết như trên mặc dù đúng tiêu chuẩn nhưng là phương pháp đã cũ.
258
Từ C# 2.0, .NET định nghĩa thêm generic delegate EventHandler<T> để
đơn giản hóa việc định nghĩa event. Khi sử dụng EventHandler<T> bạn có thể
trực tiếp khai báo event trong class mà không cần định nghĩa kiểu delegate
nữa:
public event EventHandler<PriceChangedEventArgs> PriceChanged;

Class Gasoline có thể viết lại theo mô hình tiêu chuẩn mới như sau:
internal class Gasoline {
private decimal _price = 11000; // giá gốc là 11.000đ
public event EventHandler<PriceChangedEventArgs> PriceChanged;
public decimal Price {
get => _price;
set {
if (_price == value) return;
var oldPrice = _price;
_price = value;
// chạy sự kiện
var args = new PriceChangedEventArgs(oldPrice, _price);
PriceChanged?.Invoke(this, args);
}
}
}

Đây là phương pháp chuẩn nhất khi lập trình sự kiện trong C# .NET mà bạn
cần tuân thủ.

Ứng dụng event trong .NET


Trong .NET, event được sử dụng ở nhiều nơi, tiêu biểu nhất là trong hai UI
Framework Windows Forms và WPF. Ngoài ra mô hình này cũng được sử
dụng rộng rãi ở những thư viện khác.
Chúng ta sẽ làm một ví dụ nhỏ với Windows Forms để minh họa.
Do đây không phải là bài học về Windows Forms, chúng ta sẽ vẫn sử dụng
một Console project thông thường cho đơn giản.
Bước 1. Tạo một Console project mới.
Bước 2. Tham chiếu tới assembly System.Windows.Forms như sau:

259
Viết code cho Program như sau:
using System;
using System.Windows.Forms;
namespace ConsoleApp3 {
internal class Program {
[STAThread]
private static void Main() {
// tạo form mới
var form = new Form();
// tạo nút bấm
var button = new Button {
Text = "Press me!",
};
// đăng ký xử lý sự kiện click nút bấm
button.Click += OnButtonOnClick;
// chèn nút bấm vào form
form.Controls.Add(button);
Application.EnableVisualStyles();
// chạy ứng dụng
Application.Run(form);

260
}
// phương thức xử lý sự kiện bấm nút.
private static void OnButtonOnClick(object sender, EventArgs args)
{
MessageBox.Show("Xin chào. Đây là thông báo từ phương thức xử lý sự kiện click của nút bấm!");
}
}
}

Hãy để ý cách chúng ta đăng ký xử lý sự kiện Click và phương thức xử lý sự


kiện OnButtonOnClick. Hãy so sánh nó với các yêu cầu và quy định của mô
hình xử lý sự kiện .NET mà chúng ta đã học ở phần trên.

261
Các kiểu đặc biệt trong C#: anonymous, nullable,
dynamic
Anonymous, nullable và dynamic là những loại kiểu dữ liệu đặc biệt trong
C#. Anonymous type là những kiểu dữ liệu không có tên. Nullable type là
những kiểu dữ liệu vốn thuộc nhóm value type nhưng giờ có thể nhận thêm
giá trị null. Dynamic giúp đưa đặc điểm “định kiểu động” vào C#. Đây là
những kiểu dữ liệu đặc thù được đưa vào C# để giải quyết một số vấn đề
riêng.
Bài học này sẽ giúp bạn hiểu rõ hơn về các nhóm kiểu dữ liệu này.

Anonymous type trong C#


Khái niệm anonymous type
Anonymous type (Kiểu dữ liệu vô danh) trong C# là loại kiểu dữ liệu tạm
thời mà C# compiler tự suy đoán ra cấu trúc khi object của nó được khởi
tạo thông qua cú pháp Object Initializer.
Anonymous type trong C# chỉ dùng để chứa dữ liệu và dữ liệu của nó là chỉ
đọc. Một khi object của anonymous type được khởi tạo thì giá trị của nó
không thể thay đổi được nữa. Vì anonymous type không có tên (!), bạn cũng
không thể dùng cách thức khai báo biến thông thường cho nó.
Cách khai báo và khởi tạo biến thông thường: <tên_kiểu> <tên_biến> = <giá_trị>;
Vì lý do này, C# đưa vào từ khóa var giúp khai báo một biến mà không cần
chỉ định tên kiểu dữ liệu cho nó. C# compiler sẽ căn cứ vào giá trị gán cho
biến đó để tự suy đoán ra kiểu.
Anonymous type không thực sự là “vô danh”. Trên thực tế, C# compiler sẽ tự tạo ra một cái tên
cho kiểu dữ liệu nhưng bạn không biết đến cái tên đó. Cái tên này chỉ tồn tại khi compiler dịch
mã nguồn.
Sử dụng anonymous type trong C#
Hãy cùng thực hiện một ví dụ sử dụng anonymous type (bạn có thể sử dụng
C# interactive):
// đây là object của một kiểu vô danh với 3 property: Name, Address, Age
// C# compiler sẽ tự sinh ra class cho object này và tự suy đoán kiểu của các property
var obj = new { Name = "Donald", Address = "Washington DC", Age = 25 };
Trong ví dụ này chúng ta sử dụng cú pháp khởi tạo Object Initializer để tạo
ra một object mới với 3 thuộc tính Name, Address và Age.
Tuy nhiên, sau từ khóa new lại không có tên class (kiểu dữ liệu) như trong
cú pháp khởi tạo object thông thường. Vì không biết tên kiểu nên khai báo

262
biến obj phải dựa vào từ khóa var để C# compiler tự suy đoán ra cấu trúc
và tạo một kiểu tạm thời (nhưng người lập trình không biết đến).

Việc truy xuất phần tử của object vô danh không có gì khác với object bình
thường:
Console.WriteLine($"Name: {obj.Name}, Address: {obj.Address}, Age: {obj.Age}");
Hãy xem một ví dụ khác:
// khai báo trước 3 biến và dùng chúng để tạo object
int id = 0; string name = "Obama"; int age = 30;
// tạo object của kiểu vô danh từ 3 biến, C# tự suy đoán ra tên property
var obj2 = new { id, name, age };
Console.WriteLine($"Name: {obj2.id}, Address: {obj2.name}, Age: {obj2.age}");
Trong trường hợp này chúng ta tạo một object thuộc kiểu anonymous type
từ các biến khai báo trước. C# thậm chí có thể tự suy đoán ra tên thuộc
tính từ tên biến.

C# có thể tự suy đoán ra tên thuộc tính từ tên biến


Anonymous type được sử dụng rất phổ biến trong LINQ với phương thức
Select để tạo ra một object mới từ một tổ hợp các thuộc tính của một object
khác. Loại kỹ thuật này có tên gọi là object projection.

263
Nullable type trong C#
Khái niệm nullable type
Trong bài học về các kiểu dữ liệu cơ sở của C#, chúng ta đã nhắc qua rất
ngắn gọn về nullable type.
Như bạn đã biết, các kiểu dữ liệu thuộc nhóm value type đều có khoảng giá
trị xác định. Ví dụ, bool (hay System.Boolean) chỉ có thể nhận một trong
hai giá trị true|false. Ngoài ra, C# cũng bắt buộc mọi biến phải được khởi
tạo và gán giá trị trước khi sử dụng. Do đó, các biến thuộc kiểu value type
luôn luôn có giá trị xác định khi tham gia vào biểu thức.
Giờ hãy tưởng tượng bạn ánh xạ một bản ghi của cơ sở dữ liệu quan hệ sang
một object của C#. Mỗi trường của bản ghi đó sẽ tương ứng với một biến
thành viên hoặc 1 property của object. Cơ sở dữ liệu quan hệ cho phép một
trường nhận giá trị null với ý nghĩa rằng trường đó không có giá trị. Khi đó,
giả sử một trường số nguyên của bản ghi nếu có giá trị null, vậy nó sẽ tương
ứng với giá trị nào của kiểu số nguyên trong C#? Các kiểu số nguyên của
C# không thể biểu diễn trạng thái “không có giá trị”.
Đối với các kiểu thuộc nhóm reference type, biến của nó có thể nhận giá trị
null. Giá trị null biểu diễn trạng thái đặc biệt: biến đó không có giá trị. Hay
chính xác hơn, biến đó không trỏ vào một object cụ thể nào. Tuy nhiên các
kiểu value lại không thể biểu diễn được trạng thái “không có giá trị”.
Để giải quyết những tình huống tương tự, C# đưa vào khái niệm nullable
type.
Nullable type trong C# là những kiểu dữ liệu value type đặc biệt có thể nhận
giá trị null. Hiểu một cách đơn giản, nullable type cũng có thể xem là những
value type (về mặt giá trị), đồng thời có thêm giá trị null. Giá trị null của
nullable type biểu diễn trạng thái “không có giá trị” gán cho biến.
Ví dụ, nullable bool giờ có thể nhận ba giá trị: true|false|null. Trong đó,
giá trị null của bool chỉ cần hiểu đơn thuần là “biến không có giá trị”.
Sử dụng nullable type trong C#
C# sử dụng modifier ? (dấu hỏi chấm) đặc sau value type tương ứng để
biến nó thành nullable type. Ví dụ:
bool? b = null; // hoàn toàn hợp lệ, vì bool? là kiểu thuộc nhóm nullable và có khoảng
giá trị truyền thống của bool
b = true;
int? i = 100;
i = null; // OK, vì int? là kiểu nullable, có khoảng giá trị tương tương int nhưng có
thể nhận giá trị null

264
Rất lưu ý rằng, modifier ? chỉ được phép đặt sau tên kiểu value type. Nếu
đặt sau tên kiểu reference type sẽ báo lỗi lúc dịch.
Về bản chất, cách viết ? sau tên kiểu chỉ là một cú pháp tắt để tạo ra object
của generic struct System.Nullable<T>. Do đó, nullable type có thêm hai
property đặc trưng mà value type không có:
> int? i = 100;
> i.HasValue // true, vì i khác null
true
> i.Value // Value thuộc kiểu int, trong khi i thuộc kiểu int?
100
> i = null
null
> i.HasValue // false, vì giờ i có giá trị null
false

HasValue trả về true nếu biến có giá trị khác null. Value trả về giá trị value
type của biến đó – hiểu đơn giản là chuyển giá trị từ nullable về value type
thông thường.

Một số phép toán đặc biệt trên nullable và reference type


Tiếp theo đây chúng ta sẽ xem một số phép toán đặc biệt áp dụng cho các
kiểu dữ liệu có khả năng nhận giá trị null, bao gồm các kiểu nullable và
reference type.
Phép toán null coalescing
Đối với các kiểu dữ liệu có khả năng nhận giá trị null (reference và nullable
type), C# cho phép áp dụng một phép toán đặc biệt: null coalescing (tạm
dịch là toán tử hợp nhất null – nghe hơi dài dòng!).
Hãy xem một ví dụ đơn giản:
> bool? b = null;
> bool b2 = b ?? true;
> b2
true
>

Đầu tiên khai báo biến nullable bool? và gán giá trị null.
Lệnh bool b2 = b ?? true; giải thích như sau: nếu biến b có giá trị khác
null thì b2 sẽ nhận giá trị b; nếu b có giá trị bằng null thì b2 sẽ nhận giá trị
true. Phép toán ?? được gọi là null coalescing.
Cú pháp chung của null coalescing như sau:
265
<variable> ?? <value>;
Nếu variable có giá trị khác null, biểu thức sẽ nhận luôn giá trị của variable.
Nếu variable có giá trị null, biểu thức sẽ nhận giá trị value. Thực chất, null
coalescing có thể xem là dạng viết tắt của cấu trúc if để kiểm tra giá trị null.
Tức là, ví dụ trên có thể viết lại như sau:
bool b2 = false;
if (!b.HasValue)
b2 = true;
else
b2 = b.Value;
// hoặc
if (b == null)
b2 = true;
else
b2 = b.Value;

Phép toán null conditional


Khi làm việc với các kiểu dữ liệu có thể nhận giá trị null như nullable hay
reference type, nếu bạn gọi phương thức hoặc truy xuất thành viên trên
biến có giá trị null sẽ gây ra lỗi trong quá trình thực thi (runtime exception).
Ví dụ:
> int? i = null;
> i.Value
Nullable object must have a value.
+ System.ThrowHelper.ThrowInvalidOperationException(System.ExceptionResource)
+ Nullable<T>.get_Value()
> string str = null;
> str.ToLower()
Object reference not set to an instance of an object.
>

Trong ví dụ trên, i có giá trị null. Việc đọc giá trị của i qua Value property
sẽ gây lỗi. Tương tự, str có giá trị null, gọi phương thức ToLower() trên str
sẽ gây lỗi.
Do đó, bạn cần phải kiểm tra giá trị của biến trước khi thực hiện bất kỳ thao
tác gì. Nếu biến có giá trị khác null mới thực hiện thao tác đó. Ví dụ:
if (str != null) str.ToLower();
Việc liên tục phải gọi lệnh kiểm tra điều kiện khác null như vậy khá nhàm
chán. C# cung cấp một cú pháp gọn nhẹ đơn giản để làm việc này:
str?.ToLower();
Dấu ? sau tên biến và trước phép toán truy xuất phần tử có tên gọi là null
conditional operator. Nó giúp bạn kiểm tra xem biến có bằng null không.
Nếu biến khác null thì mới thực hiện phép toán truy xuất phần tử.
266
Theo đó, biểu thức str?.ToLower() sẽ không thực hiện nếu str có giá trị
null, do đó sẽ không gây lỗi.

Kiểu dữ liệu dynamic


Kiểu dữ liệu đặc biệt cuối cùng xem xét trong bài này là dynamic. Kiểu dữ
liệu này khác biệt với tất cả những kiểu dữ liệu khác của C#. Nó thậm chí
còn khác biệt hẳn với cách suy nghĩ quen thuộc về kiểu dữ liệu trong C#.
Định kiểu tĩnh và định kiểu động
Nếu bạn đã từng làm việc với JavaScript hay PHP, bạn sẽ thấy một sự khác
biệt rất lớn về việc khai báo và sử dụng biến. Khi khai báo biến bạn không
cần chỉ định kiểu dữ liệu cụ thể của nó. Giá trị gán cho một biến không được
xác định và kiểm tra ở giai đoạn dịch mà là ở giai đoạn thực thi. Đặc thù
như vậy của ngôn ngữ gọi là định kiểu động (dynamically typed).
Ngược lại, C# bắt buộc kiểu của biến phải được xác định và kiểm tra ở giai
đoạn dịch. Đặc thù đó của C# được gọi là định kiểu tĩnh (statically typed).
Bạn hẳn có thể nhận ra rằng, ngôn ngữ định kiểu tĩnh như C# rất an toàn
khi viết code. Nhưng đồng thời, nó lại thiếu đi sự linh hoạt của ngôn ngữ
định kiểu động.
Từ khóa var và kiểu object không hề giúp C# có đặc điểm của ngôn ngữ định kiểu động, mặc
dù bạn không cần tự mình chỉ định kiểu cụ thể của biến khi khai báo với hai từ khóa này.
Bắt đầu từ C# 4.0, một kiểu dữ liệu đặc biệt được đưa vào để hỗ trợ những
nhu cầu về định kiểu động trong lập trình: kiểu dynamic.
Khai báo và gán giá trị cho biến kiểu dynamic trong C#
Hãy cùng thực hiện một số ví dụ (sử dụng C# interactive):
> dynamic t = "Hello worl!";
> t
"Hello worl!"
> t.GetType()
[System.String]
> t = false
false
> t.GetType()
[System.Boolean]
> t = new List<int>()
List<int>(0) { }
> t.GetType()
[System.Collections.Generic.List`1[System.Int32]]
>
267
Bạn có thể thấy, biến t khai báo với từ khóa dynamic hoạt động có nét tương
tự với object: bạn có thể gán cho nó bất kỳ giá trị nào. C# không hề phiền
lòng khi bạn gán các loại giá trị chả liên quan gì với nhau cho biến t. Bạn
vẫn có thể gọi phương thức từ biến t như với bất kỳ biến bình thường nào
khác.
Tuy nhiên, khi gõ t. (gọi phép toán truy xuất thành viên) bạn hẳn sẽ nhận
thấy sự khác biệt: Intellisense không hề hoạt động. Nếu bạn dùng từ
khóa object, ít nhất Intellisense còn đưa ra danh sách các thành viên.
Vấn đề là: dynamic cho phép bạn khai báo biến theo đúng mô hình định
kiểu động. C# không hề hiểu kiểu cụ thể của biến ở giai đoạn viết code và
dịch. Do đó nó chẳng thể làm gì để giúp đỡ bạn. Giờ đây bạn phải tự mình
xác định kiểu của biến là gì, và kiểu đó có những thành viên nào. Bạn phải
tự mình viết tên các thành viên đó một cách chính xác. Nếu không, chương
trình sẽ bị lỗi lúc chạy (mặc dù vẫn dịch thành công).
Hãy xem một ví dụ khác. Hãy viết đoạn code sau vào một Console App
project:
static void Main()
{
dynamic t = "Hello world!";
Console.WriteLine(t.ToUpper());
Console.WriteLine(t.toupper()); // compile thành công nhưng chạy sẽ bị lỗi
Console.ReadKey();
}
Hẳn bạn có thể thấy ngay, lớp string không hề có phương
thức toupper (viết thường) mà chỉ có ToUpper. Tuy nhiên, C# vẫn cho phép
bạn viết biểu thức t.toupper, khác hoàn toàn với phong cách kiểm soát
nghiêm ngặt vốn có của C#. Chương trình compile hoàn toàn bình thường.
Tuy nhiên, khi chạy bạn sẽ gặp ngay lỗi dưới đây:
Unhandled Exception:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'string'
does not contain a definition for 'toupper'
Đến đây hẳn bạn đã nhìn thấy sự khác biệt của dynamic với các cách khai
báo biến “không chỉ định kiểu” như var và object.
Kiểu dynamic cho phép bạn hoàn toàn tự do trong việc gán giá trị và gọi
thành viên. C# không hề quản việc này lúc biên dịch nữa. Bạn sẽ phải là
người chịu trách nhiệm. “Tự do luôn đi kèm trách nhiệm!” – hãy ghi nhớ khi
sử dụng dynamic.
Bình thường bạn sẽ không mấy khi muốn sử dụng đến dynamic. Tuy nhiên, khi bạn phát triển
ứng dụng web, dynamic sẽ thể hiện sức mạnh của nó.

268
Có thể bạn sẽ ít khi sử dụng trực tiếp các kiểu dữ liệu anonymous, nullable,
dynamic type trong phần lập trình cơ bản. Tuy nhiên, nếu bạn đi tiếp vào
phần lập trình LINQ, anonymous type sẽ là phần không thể thiếu. Nếu bạn
học lập trình với cơ sở dữ liệu, nullable type sẽ thể hiện vai trò của mình.
Kiểu dynamic có vai trò quan trọng khi lập trình ứng dụng web.

269
Ngoại lệ (Exception) và xử lý ngoại lệ trong C#
Ngoại lệ (exception) trong C# là những tình huống mà chương trình không
thể thực hiện được lệnh theo yêu cầu.
Ví dụ, khi thực hiện phép chia, nếu số chia vô tình nhận giá trị 0, phép chia
không thể thực hiện được. Khi người dùng yêu cầu truy xuất một tập tin
nhưng lại cung cấp sai đường dẫn khiến không thể thực hiện thao tác truy
xuất. Khi gặp những tình huống này, chương trình không biết phải làm gì
tiếp theo.
Trong lập trình, những tình huống tương tự xảy ra rất nhiều và được gọi
chung là ngoại lệ (exception). C# cung cấp giải pháp cho những tình huống
tương tự, gọi là phát ra exception và xử lý exception.

Exception (ngoại lệ) trong C#


Khái niệm exception trong C#
Ngoại lệ (exception) là những tình huống mà chương trình không thể thực
hiện được lệnh theo yêu cầu.
C# (và các ngôn ngữ lập trình khác) cung cấp các công cụ đặc biệt để sử
dụng trong những tình huống tương tự, bao gồm: thông báo ngoại lệ, bắt
và xử lý ngoại lệ.
Lệnh biểu diễn với từ khóa throw ở trên là thông báo ngoại lệ (hay còn gọi
là thông báo lỗi).
Trong C# (và .NET Framework), cách thức đơn giản nhất để phát ra thông
báo ngoại lệ là sử dụng lớp Exception với từ khóa throw theo cấu trúc:
throw new Exception(“thông tin về lỗi”);
Cấu trúc này khởi tạo một object của lớp Exception và gửi object này cho
cơ chế thông báo lỗi của .NET Framework. Khi gọi lệnh throw, luồng điều
khiển của chương trình sẽ thay đổi.
Exception là lớp mô tả ngoại lệ cơ bản nhất trong .NET. Khi học đến phần
kế thừa chúng ta có thêm khả năng để tạo ra các lớp thông báo ngoại lệ
riêng của mình. Các lớp thông báo ngoại lệ do người dùng định nghĩa có khả
năng đóng gói thêm nhiều thông tin khác giúp ích cho quá trình dò lỗi.
Cơ chế xử lý exception trong C#
Khi một ngoại lệ được phát ra ở một vị trí bất kỳ trong chương trình, việc
thực thi của chương trình sẽ dừng lại. Nếu chương trình đang chạy ở chế độ
Debug, trình soạn thảo code sẽ được mở ra và đoạn code bị lỗi sẽ được đánh
dấu giúp cho người lập trình xác định vị trí và nguyên nhân gây lỗi.
270
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh
chưa tồn tại.

Giao diện Visual Studio khi xảy ra ngoại lệ


Nếu chương trình chạy ở chế độ Release, chương trình sẽ bị dừng lại và cơ
chế xử lý ngoại lệ mặc định của .NET Framework sẽ được kích hoạt để hiển
thị lỗi. Chương trình được dịch ở chế độ này sẽ không chạy được ở chế độ
Debug nữa.
Nếu chương trình console chạy ở chế độ Release mà gặp lỗi, thông báo lỗi
sẽ được hiển thị như dưới đây.

Thông báo ngoại lệ ở giao diện console


Đây là cơ chế bắt và xử lý lỗi mặc định của .NET Framework đối với ứng
dụng console. Đối với ứng dụng Windows form, giao diện bắt và xử lý lỗi có
khác biệt.
Tuy nhiên, cơ chế thông báo lỗi mặc định của .NET Framework tương đối
không thân thiện với người dùng.
.NET cung cấp cho các chương trình tính năng bắt và xử lý ngoại lệ để tự
mình xác định xem khi xảy ra lỗi (ngoại lệ) thì sẽ làm gì.

Kỹ thuật xử lý ngoại lệ (Exception Handling) trong C#


Chúng ta đã nhắc đến khái niệm ngoại lệ (exception) và xem xét cách thức
đơn giản nhất để phát thông báo ngoại lệ bằng lệnh throw và
lớp Exception. Exception là một cơ chế rất mạnh trong .NET giúp phát hiện
lỗi logic trong chương trình ở giai đoạn Runtime.

271
Exception và chế độ Debug trong C#
Khi chạy chương trình ở chế độ debug, nếu phát sinh ngoại lệ, Visual Studio
sẽ mở tập tin mã nguồn ở đúng vị trí lỗi cùng với thông báo cụ thể. Qua đó,
chúng ta có thể xác định nguồn gốc của lỗi và đưa ra cách giải quyết.
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh
chưa tồn tại.

Ngoại lệ ở chế độ chạy debug


Đây là cơ chế bắt và xử lý lỗi ở chế độ Debug. Chương trình chúng ta viết
từ đầu dự án đến giờ đều dịch và chạy ở chế độ Debug.
.NET Framework cung cấp nhiều lớp hỗ trợ thông báo lỗi kế thừa từ lớp
Exception với các thông tin chi tiết hơn về lỗi có thể gặp phải. Nếu một
phương thức nào đó có khả năng phát sinh lỗi, Visual Studio sẽ hiển thị
danh sách các loại lỗi có thể gặp phải.
Ví dụ, đối với phương thức OpenRead của lớp File có thể phát sinh 8 loại
Exception khác nhau như lỗi vào ra (IOException), lỗi không tìm thấy tập
tin (FileNotFoundException), lỗi không tìm thấy thư mục
(DirectoryNotFoundException),…
Danh sách các loại ngoại lệ này được thể hiện bằng các class khác nhau sẽ
được sử dụng nếu tình huống lỗi tương ứng phát sinh. Khi đặt con trỏ chuột
lên tên phương thức này chúng ta sẽ xem được danh sách các lớp chứa
thông tin về ngoại lệ của phương thức:

272
Các ngoại lệ có thể phát sinh khi sử dụng phương thức OpenRead
Thông tin này có nghĩa là, nếu phát sinh bất kỳ exception nào trong 8 loại
exception có thể xảy ra khi thực hiện phương thức này, lệnh throw sẽ được
gọi cùng với một object của loại exception tương ứng.
Ví dụ, nếu cung cấp một tập tin không tồn tại, phương thức OpenRead không
thể làm gì được và sẽ phát lệnh throw new FileNotFoundException(), tương
tự như cách chúng ta tự phát ra thông báo ngoại lệ ở trên.
Xử lý ngoại lệ ở chế độ Release
Một chương trình trước khi đem triển khai cho người dùng cuối phải được
dịch ở chế độ Release. Chương trình được dịch ở chế độ này sẽ không chạy
được ở chế độ Debug nữa.
Nếu chương trình chạy ở chế độ Release mà gặp lỗi, thông báo lỗi sẽ được
hiển thị như dưới đây.

Đây là cơ chế bắt và xử lý lỗi mặc định của .NET Framework đối với ứng dụng console.
Đối với ứng dụng Windows Form, giao diện bắt và xử lý lỗi có khác biệt.
Như chúng ta thấy, cơ chế bắt và xử lý lỗi mặc định của .NET Framework
tương đối không thân thiện với người dùng.
.NET cũng cung cấp cho các chương trình tính năng bắt và xử lý ngoại
lệ (Exception Handling) để tự mình xác định hoạt động của chương trình khi
xảy ra lỗi (ngoại lệ), tránh phải sử dụng cơ chế bắt và xử lý lỗi mặc định.
Cấu trúc try – catch
Cơ chế bắt và xử lý ngoại lệ sử dụng cấu trúc cú pháp sau:
try { <code có khả năng gây lỗi viết trong block này> }
catch(<loại lỗi 1> object1) { <hành động khi xảy ra lỗi> }
catch(<loại lỗi 2> object2) { <hành động khi xảy ra lỗi> }
… // có thể kết hợp nhiều khối catch nữa ở đây
finally{ <hành động sẽ thực hiện cả khi có lỗi hay không có
lỗi> }

273
Cấu trúc này có 3 khối code:
 Khối “try”: chứa các đoạn code có khả năng gây lỗi;
 Các khối “catch”: dùng để bắt từng loại lỗi cụ thể.
Khi xảy ra lỗi, khối này sẽ bắt object của lớp exception tương ứng (mà
ở phần phát ngoại lệ tạo ra cùng lệnh throw) và thực thi đoạn code
tương ứng. Trong đoạn code này có thể sử dụng các object chứa thông
tin ngoại lệ mà khối catch này bắt được.
Chúng ta đã biết các lớp ngoại lệ kế thừa nhau tạo thành một cấu trúc
phân cấp với lớp Exception ở gốc. Nếu khối catch được chỉ định bắt
loại lỗi cha, nó đồng thời bắt tất cả các loại lỗi con kế thừa từ lớp cha
đó. Nếu chúng ta bắt lỗi thuộc loại cao nhất là Exception thì cũng đồng
thời bắt tất cả các loại lỗi có thể phát sinh trong chương trình.
 Khối “finally”: không bắt buộc. Nếu có mặt khối này, dù có xảy ra
ngoại lệ hay không thì các lệnh trong khối code này đều sẽ được thực
hiện.
Có một số điểm cần lưu ý khi làm việc với ngoại lệ:
 Phân biệt giữa phát ra ngoại lệ với bắt/xử lý ngoại lệ;
 Không nên bắt ngoại lệ ở các class cấp thấp (tức là class được sử dụng
bởi class khác). Việc bắt ngoại lệ nên đặt ở class cấp cao nhất. Như
đối với ứng dụng console, đó là ở phương thức Main.
 Ở các class cấp thấp chỉ phát ra ngoại lệ, các class cấp cao sẽ bắt và
xử lý.

274
Assembly trong C#, thư viện class, NuGet
Assembly trong C# (và .NET) là kết quả biên dịch code của mỗi project. Mỗi
project đến lượt mình lại có thể tham chiếu (reference) tới các assembly
khác để sử dụng các class (và kiểu dữ liệu khác) định nghĩa trong assembly
đó. Nói cách khác, “chương trình” C# (và .NET) chính là một assembly được
tạo ra bằng cách “ghép nối” nhiều assembly khác. Assembly của .NET có
đuôi exe hoặc dll tương tự như các tập tin chương trình hoặc thư viện của
Windows nhưng bản chất của chúng rất khác biệt.
Bài học này sẽ giới thiệu chi tiết về assembly trong C#, bao gồm quá trình
xây dựng, sử dụng (tham chiếu) assembly của bạn và của bên thứ ba. Ngoài
ra, trong bài này bạn cũng sẽ làm quen với NuGet – hệ thống quản lý gói
thư viện dành cho các nền tảng của Microsoft.

Assembly trong C#
Khái niệm và phân loại assembly trong C# .NET
Trong quá trình học lập trình C# cơ bản từ đầu đến giờ bạn đã xây dựng
khá nhiều project. Mỗi project này đều thuộc loại Console App – loại ứng
dụng tự chạy độc lập với giao diện dòng lệnh. Trong C# (và .NET) chương
trình ứng dụng này được gọi là một assembly.
Ngoài Console App, C# cũng có thể tạo ra nhiều loại project khác, tương
ứng với các loại ứng dụng riêng. Kết quả biên dịch mọi project của C#
compiler đều được gọi chung là assembly.
Tên gọi assembly cũng dùng chung cho tất cả kết quả dịch các project viết bằng ngôn ngữ .NET
khác như VB.NET, C++ CLI.
Các assembly này chủ yếu chia làm hai loại: một loại có thể chạy độc lập
như một chương trình bình thường; một loại chỉ có thể được sử dụng bởi
project khác.
Ví dụ, assembly của Console App mà bạn quen thuộc, của Windows Forms,
của Windows Presentation Foundation đều thuộc loại thứ nhất. Đặc thù của
loại assembly này là có mặt phương thức static void Main() với vai trò
entry point – điểm khởi đầu của hoạt động của chương trình. Loại assembly
này thường có đuôi exe, giống như các tập tin chạy thông thường của
Windows.
Loại assembly thứ hai không chứa static void Main, do đó không thể tự mình
chạy như một chương trình độc lập. Loại assembly này được gọi là class
library (thư viện lớp) hoặc code library. Loại assembly này được sử dụng bởi
các project khác hoặc chương trình khác, ví dụ, chạy trong IIS – chương

275
trình máy chủ web của Microsoft, hoặc chạy trong hệ thống (ở dạng system
service). Loại assembly này có đuôi là dll (dynamic link library), tương tự
như các tập tin thư viện hệ thống của Windows.
Trên thực tế, loại assembly thứ hai rất đông đảo. Chúng tạo ra cả hệ thống
thư viện class của .NET. Từ đầu đến giờ thực ra bạn đã liên tục sử dụng
mscorlib.dll – một assembly thuộc loại này.
C# compiler tự động tham chiếu tới thư viện mscorlib.dll trong tất cả các project. Thư viện này
chứa tất cả các định nghĩa kiểu cơ sở của .NET.
Về bản chất, exe hay dll assembly không có gì khác biệt nhau. Nghĩa là
hoàn toàn có thể coi exe assembly là một class library và tham chiếu tới nó
từ project khác.
Assembly chứa mã CIL (Common Intermediate Language) – mã của ngôn
ngữ trung gian không phụ thuộc platform. Ở giai đoạn thực thi, mã CIL được
JIT (Just-in-time) compiler dịch tiếp thành mã đặc trưng của platform để
thực thi. Nói một cách đơn giản, assembly cũng có thể coi là tập tin mã
nguồn viết bằng ngôn ngữ CIL. Bạn không cần quan tâm đến ngôn ngữ này
làm gì. C# compiler luôn giúp bạn dịch mã nguồn chương trình về CIL rồi.
Vai trò và đặc điểm của assembly trong .NET
Thứ nhất, assembly trong .NET cho phép tái sử dụng code độc lập với ngôn
ngữ lập trình.
Để dễ hiểu, hãy hình dung thế này. Nếu bạn xây dựng một thư viện class
qua một project trên C#. Bạn có thể sử dụng thư viện này trong project viết
bằng bất kỳ ngôn ngữ .NET nào, như VB.NET, F#. Ở chiều ngược lại cũng
đúng. Một thư viện class viết trên VB.NET (cũng là một assembly) có thể dễ
dàng sử dụng trong một project C#.
Không chỉ đơn thuần là sử dụng class trong thư viện, bạn cũng có thể mở
rộng class trong thư viện (dù là viết bằng ngôn ngữ .NET khác) thông qua
cơ chế kế thừa.
Một interface được xây dựng trong thư viện viết bằng F# cũng có thể được
thực thi bởi class viết trong C#.
Thứ hai, assembly tạo thêm một mức độ quản lý nữa đối với các kiểu dữ
liệu, bên cạnh namespace. Hãy hình dung bạn xây dựng hai thư viện class
riêng, giả sử đặt tên là MyCars.dll và YourCars.dll. Trong cả hai thư viện
này có cùng namespace CarLibrary. Trong mỗi namespace này đều xây
dựng class SportsCar. Đối với .NET, đây là hai kiểu dữ liệu khác nhau.
Thứ ba, cần phân biệt giữa private và shared assembly. Assembly trong
.NET có thể được triển khai theo kiểu “private” hoặc “shared”. Private

276
assembly nằm trong cùng thư mục (hoặc thư mục con) của chương trình sử
dụng nó và chỉ dành riêng cho chương trình đó. Shared assembly được triển
khai để nhiều chương trình trên máy tính có thể dùng sử dụng. Shared
assembly nằm trong một thư mục đặc biệt có tên gọi là Global Assembly
Cache (GAC).
Thứ tư, mỗi assembly là một đơn vị độc lập có số phiên bản riêng. Một
assembly được gán một số phiên bản bao gồm 4 phần theo mẫu
<major>.<minor>.<build>.<revision>. Nếu bạn không tự mình cung cấp
số phiên bản, assembly sẽ được tự động gán số phiên bản là 1.0.0.0.
Giá trị phiên bản cho phép nhiều phiên bản shared của cùng một assembly
cùng tồn tại trên cùng một máy. CLR đảm bảo sẽ load phiên bản phù hợp
của assembly cho client.
Mỗi assembly có một tập tin cấu hình dạng xml riêng. File cấu hình này cho
phép chỉ định vị trí đặt assembly, phiên bản sẽ được tải cho client,....

Sử dụng assembly có sẵn


Việc sử dụng assembly trong C# project rất đơn giản. Bạn có thể dễ dàng
tạo ra và sử dụng assembly của riêng mình cũng như sử dụng các assembly
sẵn có (của .NET hoặc của bên thứ ba). Trong phần này chúng ta xem xét
cách sử dụng assembly có sẵn. Phần tiếp theo sẽ hướng dẫn cách tự tạo và
sử dụng assembly của riêng mình.
Sử dụng assembly của .NET
Để minh họa, chúng ta sẽ sử dụng class MessageBox của thư viện
System.Windows.Forms. Class này cho phép chương trình hiện ra các hộp
thoại thông báo.
Thực hiện theo các bước dưới đây để tham chiếu project tới thư viện
System.Windows.Forms:

277
278
Viết vài dòng code vào Main():
using System;
namespace StandardAssembly
{
class Program
{
static void Main(string[] args)
{
while (true)
{
Console.Write("Enter a message: ");
var message = Console.ReadLine();
System.Windows.Forms.MessageBox.Show(message, "Message",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Information);
}
}
}
}
Dịch và chạy thử chương trình để xem kết quả. Bạn nhập một dòng thông
báo, chương trình sẽ hiện ra hộp thoại với thông báo đó.
Như bạn đã thấy, việc tham chiếu và sử dụng thư viện class chuẩn của .NET
rất đơn giản.
Lưu ý rằng, nếu bạn mở thư mục bin/Debug (nơi chứa assembly chương
trình), bạn sẽ không thấy tập tin thư viện đâu. System.Windows.Forms.dll
là một shared assembly của .NET Framework. Nó sẽ không được copy vào
thư mục của chương trình.
Sử dụng assembly của bên thứ ba
Đây là tình huống bạn tải từ đâu đó về một tập tin thư viện dll và bạn muốn
dùng nó trong project của mình. Để thực hiện ví dụ này, bạn có thể sử dụng

279
một bộ thư viện mà Tự học ICT đã hướng dẫn làm trong loạt bài xây
dựng thư viện hỗ trợ ứng dụng console.
Bạn tải tập tin thư viện từ đường link này:
https://1drv.ms/u/s!Ar_aj4rIJ2qGkLElqTycB2bLXGk9DQ?e=tMyoBa
Lưu ý là tránh sử dụng các thư viện không rõ nguồn gốc.
Thực hiện lại các thao tác như trên. Tuy nhiên ở bước 2 click vào
nút Browse để tìm tới tập tin dll vừa tải về.

Viết vài dòng code sử dụng thư viện vừa rồi:


using System;
namespace CustomAssembly
{
class Program
{
static void Main(string[] args)
{
var app = new Framework.Application
{
Title = "Hello C# Assembly",
Prompt = "# ",
Config = Config
};
app.Run();
}
static void Config()
{
Framework.Router.Instance.Register(
route: "hello",

280
action: (p) => Framework.ViewHelper.WriteLine($"Hello, {p["name"]}. Have a
good day!", ConsoleColor.Green)
);
Framework.Router.Instance.Register(
route: "hi",
action: (p) => Framework.ViewHelper.WriteLine($"Hi, {p["name"]}. Nice to
meet you!", ConsoleColor.Yellow)
);
}
}
}
Chạy chương trình và nhập thử hai lệnh hello ? name = Trump và hi ?
name = Obama để xem kết quả.

Nếu mở thư mục bin/Debug (nơi chứa exe assembly chương trình) bạn sẽ
thấy tập tin thư viện Framework.dll cùng nằm ở đây:

Framework.dll là một private assembly, chỉ được sử dụng bởi ứng dụng.
Đây là bộ thư viện rất hữu ích nếu bạn phải viết các chương trình thực sự với giao diện console.
Tự học ICT đã hướng dẫn chi tiết cách xây dựng thư viện này trong chuỗi bài học thư viện cho
ứng dụng console.
Tự tạo và sử dụng class library trong solution
Để hiểu rõ hơn về assembly trong C# và .NET, hãy cũng tự xây dựng một
class library, triển khai và sử dụng nó trong project. Class library này sẽ
tổng hợp lại một số class mà bạn đã xây dựng trong các bài học trước. Do
đó, bạn không cần tự gõ lại code mà chỉ cần copy code đã có sẵn.

281
Class Library project
Việc tạo ra các class library của riêng mình cũng rất đơn giản. Bạn hãy thêm
một project mới vào solution. Tuy nhiên, ở cửa sổ Add New Project bạn
chọn Class Library (.NET Framework) thay cho Console App (.NET
Framework) mà bạn quen thuộc:

Sự khác biệt so với Console App project là không hề có Program hay Main.
Bạn có một tập tin code Class1.cs tạo sẵn:

Xóa bỏ tập tin code này, thêm tập tin code mới Complex.cs dành cho lớp
Complex mà bạn đã từng xây dựng trong bài học về struct. Chúng ta chỉnh
sửa Complex một chút để nó thành class.
using System;

282
namespace MathLibrary
{
/// <summary>
/// Class biểu diễn số phức
/// </summary>
public class Complex
{
public double Real; // trường thực
public double Imaginary; // trường ảo
public Complex()
{
}
/// <summary>
/// Phương thức khởi tạo
/// </summary>
/// <param name="r">phần thực</param>
public Complex(double r)
{
Real = r;
Imaginary = 0;
}
/// <summary>
/// Phương thức khởi tạo
/// </summary>
/// <param name="r">phần thực</param>
/// <param name="i">phần ảo</param>
public Complex(double r, double i)
{
Real = r;
Imaginary = i;
}
/// <summary>
/// Chuyển chuỗi hợp lệ thành giá trị của Real và Imaginery
/// </summary>
/// <param name="value"></param>
public void Parse(string value)
{
var temp = value.Trim();
if (temp.EndsWith("i") || temp.EndsWith("I"))
{
temp = temp.TrimEnd('i', 'I');
var tokens = temp.Split(new[] { '+', '-' }, 2);
Real = double.Parse(tokens[0]);
Imaginary = double.Parse(tokens[1]);
}
else
{
Real = double.Parse(temp);
}
}
/// <summary>
/// Chuyển chuỗi hợp lệ thành giá trị của Real và Imaginery
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static Complex FromString(string value)
{
var temp = new Complex();

283
temp.Parse(value);
return temp;
}
/// <summary>
/// Đặc tính, trả về module của số phức
/// </summary>
public double Modulus => Math.Sqrt(Real * Real + Imaginary * Imaginary);
/// <summary>
/// Ghi đè phép toán +
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
}
/// <summary>
/// Ghi đè phép toán -
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Complex operator -(Complex a, Complex b)
{
return new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
}
/// <summary>
/// Ghi đè phương thức ToString() của object
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (Imaginary == 0)
{
return Real.ToString();
}
return $"{Real} {(Imaginary > 0 ? '+' : '-')} {Math.Abs(Imaginary)}i";
}
}
}
Thực ra trong class library có thể chứa bất kỳ định nghĩa kiểu nào, không nhất thiết phải là
class. Bạn có thể thoải mái định nghĩa các kiểu khác như enum, struct, interface, delegate. Class
library không có gì khác biệt với Console App mà bạn đã quen thuộc.
Để ý rằng bạn phải khai báo class Complex với từ khóa public
public class Complex
{
...
Nếu thiếu từ khóa này, lớp Complex chỉ có thể sử dụng trong nội bộ project
MathLibrary. Khi đó, Complex sẽ không có ý nghĩa gì với project sử dụng
thư viện này vì không ai nhìn thấy nó.

284
Sau khi viết code, hãy bấm tổ hợp Ctrl + Shift + B để build solution. Nếu
không build solution, bạn sẽ chưa thể sử dụng thư viện này trong project
khác.
Tham chiếu project tới thư viện vừa tạo

Nếu tham chiếu thành công, assembly MathLibrary sẽ xuất hiện trong danh
sách References của ClientProject.

Giờ bạn có thể sử dụng Complex trong client code như trước đây:
using System;
285
using MathLibrary;
namespace ClientProject
{
using static Console;
class Program
{
static void Main(string[] args)
{
Title = "Complex number";
// khai báo và khởi tạo biến a thuộc kiểu Complex
var a = new Complex(1, 2);
WriteLine($"a = {a}");
// sử dụng đặc tính Modulus của Complex
WriteLine($"|a| = {a.Modulus}");
// gọi phương thức Parse
a.Parse("10-2i");
WriteLine($"a = {a}");
// gọi phương thức tĩnh FromString
var b = Complex.FromString("5 + 3i");
WriteLine($"b = {b}");
// thực hiện phép cộng trên số phức
WriteLine($"a + b = { a + b}");
ReadKey();
}
}
}
Lưu ý, ở khối using đầu tập tin bạn có lệnh using MathLibrary; để sử dụng
tên ngắn gọn của lớp Complex, thay vì phải sử dụng tên đầy đủ
MathLibrary.Complex. MathLibrary là namespace nơi khai báo lớp Complex.
Lợi thế rất lớn của việc sử dụng class library trong solution nằm ở chỗ, nếu
bạn thay đổi code của thư viện, sau khi build solution, client project sẽ tự
cập nhật bản dịch mới của thư viện. Tức là, client project sẽ luôn sử dụng
bản dịch cập nhật của thư viện mà không cần tham chiếu lại.
Nếu bạn bỏ từ khóa public trước khai báo class Complex (chuyển nó thành
internal), client code sẽ không nhìn thấy class này nữa. Nghĩa là bạn không
thể sử dụng Complex khi nó được khai báo là internal.
Một số lưu ý khi tạo thư viện class
Class Library project không có gì khác biệt với Console App project. Tuy
nhiên, vì bạn xây dựng thư viện thường là để người khác sử dụng, có một
số điều nên lưu ý.
Default namespace
Khi tạo project mới, Visual Studio sẽ sử dụng tên project làm default
namespace. Mỗi khi bạn tạo tập tin code mới, default namespace sẽ được
sử dụng ngay cho tập tin đó.
Bạn có thể thay đổi giá trị này trong mục Properties của project như sau:

286
Xml documentation
Bạn đã biết documentation comment và vai trò của nó đối với Visual Studio
Intellisense. Nếu bạn tạo class library và sử dụng thẳng trong project,
documentation comment sẽ được Intellisense tiếp tục sử dụng trong client
project. Nghĩa là khi trỏ vào tên class/method/interface,… comment này sẽ
hiển thị để bạn hiểu đó là cái gì.
Nếu bạn dịch và cung cấp thư viện dll này cho người khác sử dụng,
documentation comment sẽ không xuất hiện trong client project. Để client
project bất kỳ có thể sử dụng documentation comment bạn đã viết trong
thư viện, bạn mở node Properties như trên nhưng chuyển sang mục Build,
click chọn mục “XML documentation tập tin“.
Khi build project, Visual Studio sẽ tự động đưa các comment này vào một
tập tin xml. Nếu bạn cung cấp tập tin xml này cùng với tập tin thư viện dll,
client project sẽ tiếp tục hiển thị các documentation comment.
Nested namespace
Khi có số lượng lớn các kiểu (class, struct, enum, interface, delegate) định
nghĩa trong class library, bạn nên xem xét phân chia chúng theo chức năng
nhiệm vụ. Điều này giúp đơn giản hóa việc sử dụng chúng trong client
project.
Bạn đã biết quy ước phân chia code thành tập tin trong project. Tuy nhiên,
điều này chỉ ảnh hưởng đến việc viết và quản lý code của chính project đó.
Đối với người sử dụng thư viện (đã biên dịch thành dll), họ chỉ nhìn thấy
cách quản lý class theo namespace.
Do vậy, bạn nên phân chia class (và các định nghĩa kiểu khác) vào các
nested namespace nếu cần thiết.
Lấy ví dụ thư viện MathLibrary bên trên. Bạn đã định nghĩa kiểu Complex.
Nếu bạn tiếp tục định nghĩa các kiểu dữ liệu phức tạp khác như Vector,
Matrix. Bạn cũng đồng thời định nghĩa class Math để thực hiện các hàm tính
toán. Như vậy, các class này có thể chia làm hai nhóm: nhóm kiểu dữ liệu
và nhóm thực hiện chức năng.

287
Để người sử dụng thư viện dễ dàng hiểu và tìm đến class theo nhu cầu, bạn
có thể điều chỉnh namespace của các class Complex, Vector, Matrix thành:
namespace MathLibrary.Types
{
public class Complex
{

namespace MathLibrary.Types
{
public class Vector
{
Khi này, trong client project bạn có thể sử dụng các class này theo tên đầy
đủ là MathLibrary.Types.Complex, MathLibrary.Types.Vector,… Hoặc
sử dụng tên ngắn gọn nếu bổ sung using MathLibrary.Types; ở khối
using.

Sử dụng thư viện từ NuGet, NuGet Packages Manager


Giới thiệu về NuGet
NuGet là hệ thống quản lý gói phần mềm mã mở miễn phí thiết kế cho các
nền tảng phát triển ứng dụng của Microsoft từ 2010. Hiện nay NuGet đã trở
thành một hệ sinh thái lớn chứa nhiều loại công cụ và dịch vụ. Trên NuGet
hiện có khoảng hơn 130 nghìn gói thư viện với 1,4 triệu phiên bản và 1,3 tỉ
lượt download.
Lập trình viên .NET có thể dễ dàng tìm, sử dụng và cung cấp các gói thư
viện thông qua NuGet.
Trước đây NuGet thường được cài đặt như một ứng dụng mở rộng của Visual
Studio. NuGet được cài đặt mặc định trên Visual Studio từ phiên bản 2012.
Chúng ta có thể sử dụng theo một số cách khác nhau:
1. Sử dụng ứng dụng giao diện đồ họa NuGet Package Manager,
2. Sử dụng giao diện dòng lệnh Package Manager Console,
3. Cài đặt tự động với các tập tin mã kịch bản.
Cách đơn giản nhất để tìm và cài đặt các gói thư viện từ NuGet là sử dụng
tiện ích mở rộng NuGet Package Manager.
Cài đặt thư viện NewtonSoft.Json với NuGet Manager
Để minh họa việc tải và cài đặt gói thư viện từ NuGet, chúng ta sẽ cùng cài
NewtonSoft.Json.

288
NewtonSoft.Json là bộ thư viện này cho phép chuyển đổi một object của C#
thành một chuỗi ký tự định dạng theo quy ước của JSON (JavaScript Object
Notation) cũng như chuyển đổi ngược chuỗi JSON về object của C#. Đây là
một trong những bộ thư viện có lượt download lớn nhất trên NuGet.
Quá trình chuyển đổi này có tên gọi là serialization (từ object về JSON) và deserialization (từ
JSON về object). Bạn sẽ học về serialization, bao gồm xml, json và binary serialization trong
một phần sau.
Bạn có thể sử dụng bộ thư viện này để tự lưu thông tin cấu hình cho ứng
dụng, hoặc lưu trữ dữ liệu đơn giản thay cho sử dụng một cơ sở dữ liệu.
Bước 1. Mở giao diện quản lý các gói thư viện NuGet
Click phải vào References, chọn Manage NuGet Packages (xem hình dưới
đây).

Mở giao diện Manage NuGet Packages


Bước 2. Chọn cài gói thư viện
Trong ô tìm kiếm ở tab Browse gõ newtonsoft, chọn gói NewtonSoft.Json và
ấn Install.

289
Giao diện Manage NuGet Packages
Sau lệnh này, Visual Studio sẽ tải gói thư viện này về và cài đặt lên project
tương ứng (trong trường hợp này là BookMan.ConsoleApp).
Kiểm tra kết quả
Sau khi cài đặt thành công bộ thư viện này, trong danh sách References sẽ
xuất hiện thêm một mục “NewtonSoft.Json”. Trong cấu trúc dự án sẽ xuất
hiện thêm tập tin “packages.config” chứa thông tin về các gói thư viện được
cài đặt đặt thêm.
Sau khi dịch chương trình (Ctrl + Shift + B) thành công, trong thư mục
BinDebug của dự án sẽ xuất hiện tập tin thư việc NewtonSoft.Json.dll.
Khi triển khai ứng dụng cho người dùng cuối, tập tin thư viện này cũng phải
đi cùng tập tin chương trình.

File thư viện Newtonsoft.json.dll sau khi cài đặt


290
Cài đặt gói NuGet sử dụng website kết hợp Package Manager Console
Cách thứ hai là sử dụng dịch vụ tìm kiếm trên website
https://www.nuget.org/packages để tìm gói thư viện phù hợp. Sau đó copy
dòng lệnh paste vào Package Manager Console.

Giao diện tìm kiếm thư viện Newtonsoft.Json trên website


Nếu không nhìn thấy tab Package Manager Console, chọn View => Other
Windows => Package Manager Console, hoặc Tools => NuGet Package
Manager => Package Manager Console.

Giao diện dòng lệnh của Package Manager Console


Khi sử dụng Package Manager Console lưu ý chọn tham số “Default project”
là project mình cần cài đặt gói thư viện.
Một số lưu ý khi sử dụng các gói thư viện từ NuGet
Khi sử dụng các gói thư viện trên NuGet cần lưu ý xem xét kỹ sự phụ thuộc
(dependency) của gói thư viện cần dùng với các gói thư viện khác.
291
Lý do là nhiều thư viện trên NuGet sử dụng lẫn nhau, cũng như được xây
dựng cho các phiên bản .NET khác nhau.
Khi cài đặt một thư viện mà nó phụ thuộc vào các thư viện khác, các thư
viện kia cũng phải được cài đặt theo và phải cài đặt phiên bản mà thư viện
chính có thể sử dụng được.
Nếu các gói thư viện có phiên bản mới, NuGet cũng cho phép cập nhật phiên
bản đang cài đặt trong dự án lên phiên bản mới. Tuy nhiên, cũng giống như
khi cài đặt, phải lưu ý sự phụ thuộc giữa các thư viện trước khi quyết định
nâng cấp.
Một lời khuyên là đừng ngần ngại tách project lớn thành các project nhỏ với
class library assembly trong đó. Nó sẽ giúp bạn quản lý code tốt hơn. Đặc
biệt, nếu bạn có các class cần tái sử dụng qua nhiều project, hãy đặt nó vào
một class library.

292
LINQ (Language Integrated Query) trong C#
LINQ là một cách tiếp cận để thống nhất việc truy vấn dữ liệu trong C# (và
Visual Basic .NET) chuyên dùng để truy vấn dữ liệu từ nhiều nguồn khác
nhau.
Ví dụ, trước đây để làm việc với các cơ sở dữ liệu quan hệ (MySQL, SQL
Server) chúng ta phải sử dụng ngôn ngữ truy vấn SQL. Tuy nhiên ngôn ngữ
này hoàn toàn khác biệt với ngôn ngữ lập trình khiến người lập trình phải
sử dụng hai loại ngôn ngữ khác nhau cùng lúc.
Cú pháp của LINQ được tích hợp thẳng trong ngôn ngữ C# (và VB.NET) giúp
loại bỏ sự khác biệt giữa ngôn ngữ lập trình và các ngôn ngữ dùng để truy
vấn dữ liệu, cũng như tạo ra một giao diện lập trình thống nhất (sử dụng
cùng một nhóm lệnh truy vấn) dùng cho nhiều loại nguồn dữ liệu khác nhau
(SQL Server, Xml, dịch vụ web, tập hợp object,...). Qua đó, LINQ giúp loại
bỏ yêu cầu sử dụng nhiều loại ngôn ngữ khác nhau trong quá trình truy vấn
dữ liệu.

Language Integrated Query (LINQ) trong C#


Để sử dụng LINQ cần có ba thành phần: nguồn dữ liệu (data source), truy
vấn (query), lời gọi thực hiện truy vấn (query execution).
Nguồn dữ liệu
Các truy vấn LINQ trong C# đều tác động trên nguồn dữ liệu. Để người lập
trình có thể sử dụng chung một cách thức viết code cho dù truy vấn từ nhiều
loại nguồn dữ liệu khác nhau, LINQ luôn luôn làm việc với object. Do đó,
đối với mỗi loại nguồn dữ liệu trong C# cần xây dựng thư viện hỗ trợ LINQ
(LINQ provider) riêng giúp chuyển đổi dữ liệu về dạng object và ngược lại.
Vì lý do này, đối với dữ liệu Xml người ta phải xây dựng thêm provider “LINQ
to Xml”, với cơ sở dữ liệu SQL Server người ta phải xây dựng thêm provider
“LINQ to SQL”,....
Hình minh họa dưới đây mô tả vai trò của LINQ trong quan hệ với ngôn ngữ
lập trình C# và các nguồn dữ liệu.

293
Kiến trúc và vị trí của LINQ
Nguồn dữ liệu IEnumerable và IQueryable
Từ một góc nhìn khác, LINQ thực chất là một bộ thư viện phương thức mở
rộng (extension method) cho các class thực thi hai giao diện (interface)
IEnumerable và IQueryable. Tất cả các class và interface cho LINQ đều
đặt trong namespace System.Linq, vốn được sử dụng mặc định khi tạo mới
tập tin mã nguồn cho bất kỳ class nào.
Các phương thức mở rộng của LINQ được xây dựng trong hai class tĩnh
Enumerable và Queryable.
Lớp Enumerable chứa các phương thức mở rộng dành cho các class thực thi
giao diện IEnumerable<T>, bao gồm các kiểu dữ liệu quen thuộc nằm trong

294
không gian tên System.Collections.Generic như List<T>,
Dictionary<TKey, TValue> (ngoài ra còn có SortedList<T>, Queue<T>,
HashSet<T>, LinkedList<T>,…).
Lớp Queryable chứa các phương thức mở rộng dành cho các class thực thi
giao diện IQueryable<T> (ví dụ Entity Framework). Vì lý do này, tất cả các
class thực thi các giao diện trên đều có thể trở thành nguồn dữ liệu.

Truy vấn LINQ trong C#


Cú pháp truy vấn và cú pháp phương thức
Có hai cách viết cho LINQ là cú pháp truy vấn (query syntax) và cú pháp
phương thức (method syntax).
 Cú pháp truy vấn (Query Syntax): Cú pháp này nhìn giống như một
truy vấn select SQL đảo ngược với từ khóa đầu tiên là from, kết thúc
là select. Cú pháp này có hình thức khác biệt với code C# thông
thường với một số từ khóa mới.
 Cú pháp phương thức (Method Syntax, còn gọi là Fluent): giống như
cách gọi một phương thức mở rộng bình thường trên object của class.
Đây lối viết cơ bản của LINQ.
 Cú pháp pha trộn (mixed syntax): thực tế là một số phương thức LINQ
không được hỗ trợ ở dạng query syntax, khi đó người ta có thể sử
dụng lối viết pha trộn của cả hai loại cú pháp.
Có một số điểm lưu ý khi lựa chọn cách viết:
1. Một số phương thức chỉ viết được theo cú pháp phương thức, không
viết được với cú pháp truy vấn;
2. Cú pháp truy vấn sẽ được chuyển sang cú pháp phương thức ở giai
đoạn biên dịch, vì vậy hai cách viết không có gì khác biệt về hiệu suất
ở giai đoạn thực thi;
3. Nếu quen thuộc với sử dụng biểu thức lambda, cú pháp phương thức
dễ dàng sử dụng hơn và không đòi hỏi phải học thêm một lối viết mới;
4. Việc lựa chọn mang tính cá nhân, tuy nhiên nên sử dụng thống nhất.
Lối viết này luôn yêu cầu cung cấp một hàm lambda để gọi đối với mỗi phần
tử của chuỗi dữ liệu. Khi quen thuộc với việc sử dụng hàm lambda, lối viết
này hoàn toàn giống như gọi hàm thông thường và không yêu cầu phải học
thêm gì cả.
Cú pháp truy vấn nhìn giống như truy vấn SQL viết ngược lại. Lối viết này
cung cấp một cách khác để mô tả hàm xử lý phần tử thay vì trực tiếp cung
cấp hàm lambda làm tham số.
295
Trong bài học này bạn sẽ chỉ học cách sử dụng cú pháp phương thức. Nếu
muốn, bạn có thể tự tìm hiểu cú pháp phương thức. Cú pháp phương thức
giúp code “có tính C#” hơn.
Danh sách phương thức truy vấn LINQ
Danh sách phân loại các phương thức truy vấn được liệt kê trong bảng dưới
đây:
Nhóm Phương thức
Lọc dữ liệu Where, OfType
Sắp xếp OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse
Nhóm GroupBy, ToLookup
Ghép nối GroupJoin, Join
Phép chiếu Select, SelectMany
Phép gộp Aggregate, Average, Count, LongCount, Max, Min, Sum
Định lượng All, Any, Contains
ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last,
Lấy phần tử
LastOrDefault, Single, SingleOrDefault
Tập hợp Distinct, Except, Intersect, Union
Phân đoạn Skip, SkipWhile, Take, TakeWhile
Ghép dữ liệu Concat
Đẳng thức SequenceEqual
Sinh dữ liệu DefaultEmpty, Empty, Range, Repeat
Biến đổi AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList
Thông qua tên gọi chúng ta có thể hình dung sơ qua về khả năng của các
phương thức.
Đặc biệt, nếu quen thuộc với ngôn ngữ SQL, nhiều phương thức trong danh
sách trên có tên gọi và khả năng tương đồng với các lệnh của SQL.
Chúng ta tạm thời chưa cần hiểu rõ tất cả các phương thức trên. Trong phần
tiếp theo chúng ta sẽ tìm hiểu cách sử dụng cụ thể của một số phương thức
phổ biến nhất thông qua các ví dụ.
Thực thi truy vấn LINQ trong C#
Một truy vấn LINQ không được thực thi ngay khi chương trình thực hiện đến
lệnh đó. Chỉ khi nào có những hoạt động thực sự cần đến dữ liệu từ truy

296
vấn đó (ví dụ như duyệt danh sách dữ liệu), truy vấn mới được thực hiện.
Việc trì hoãn thực hiện truy vấn như vậy có tên gọi là thực thi trễ (deferred
execution).
Việc thực thi trễ có tác dụng lớn đến việc tăng hiệu suất xử lý vì nó hạn chế
thực thi những lệnh chưa cần thiết. Cơ chế thực thi trễ áp dụng cho tất cả
các nguồn dữ liệu hỗ trợ LINQ hiện tại (LINQ to Objects, LINQ to SQL, LINQ
to Entities, LINQ to XML).
Trong một số trường hợp chúng ta cần thực thi truy vấn ngay khi chương
trình chạy đến vị trí lệnh đó. Để thực hiện cơ chế này, chúng ta gọi tới một
trong số các phương thức biến đổi dữ liệu bắt đầu bằng “To”: ToArray,
ToList.

Sử dụng một số phương thức LINQ trong C#


Ở phần trên chúng ta đã xem xét những vấn đề cơ bản để hiểu tư tưởng
chung của LINQ. Trong phần này chúng ta sẽ xem một số ví dụ minh họa
cụ thể. Trong các ví dụ, chúng ta sẽ sử dụng lớp thực thể Contact như sau:
private class Contact
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}

Nguồn dữ liệu:
var contacts = new List<Contact>
{
new Contact{ Age = 11, FirstName = "Trump", LastName =
"Donald", Address = "Ha Noi"},
new Contact{ Age = 21, FirstName = "Omaba", LastName =
"Barrack", Address = "Sai Gon"},
new Contact{ Age = 31, FirstName = "Bush", LastName =
"George", Address = "Ha Noi"},
new Contact{ Age = 41, FirstName = "Bill", LastName =
"Clinton", Address = "Da Nang"},
new Contact{ Age = 51, FirstName = "Reagan", LastName =
"Ronald", Address = "Da Nang"},
new Contact{ Age = 61, FirstName = "Jimmy", LastName =
"Carter", Address = "Sai Gon"},
new Contact{ Age = 71, FirstName = "Gerald", LastName =
"Ford", Address = "Ha Noi"},
new Contact{ Age = 81, FirstName = "Nixon", LastName =
"Richard", Address = "Ha Noi"},
};
Sau đây chúng ta sẽ xem xét cách sử dụng một số phương thức cụ thể của
LINQ thông qua các ví dụ.
297
Lưu ý sử dụng không gian tên System.Linq
Phương thức Where
Where là phương thức dùng để lọc dữ liệu theo các tiêu chí nào đó và trả về
một danh sách dữ liệu khác (chỉ chứa những dữ liệu phù hợp tiêu chí).
Chúng ta xem xét việc sử dụng Where qua một số ví dụ:
Yêu cầu 1: lập danh sách những người có địa chỉ là “Ha Noi”.
Đây là yêu cầu lọc dữ liệu theo tiêu chí trường Address của object phải chứa
giá trị “Ha Noi”. Chúng ta sử dụng phương thức lọc Where trên nguồn dữ
liệu contacts.
Như chúng ta thấy, Intellisense cung cấp mô tả của phương thức này với
hai overload:

overload thứ nhất của phương thức Where

overload thứ hai của phương thức Where


Overload thứ nhất tiếp nhận một biến thuộc kiểu delegate Func<Contact,
bool>.
Overload thứ hai tương tự như vậy nhưng trong phương thức tham số có
thêm một biến đầu vào kiểu int dùng để chứa index của phần tử.
Như ở phần trên đã giải thích, Func<Contact, bool> là kiểu delegate tương
ứng với phương thức có đầu vào kiểu Contact (kiểu dữ liệu cơ sở của
biến contacts) và trả về kết quả kiểu bool. Phương thức này sẽ được gọi
trên từng phần tử của contacts.
Nếu phương thức trả về kết quả true thì phần tử đó sẽ được thêm vào danh
sách kết quả. Kết quả trả về của phương thức Where là một biến thuộc
kiểu IEnumerable<Contact>, vốn có thể truy xuất tương tự như mảng.
Bạn nên học cách đọc mô tả phương thức LINQ để có thể chủ động tìm hiểu cách sử dụng của
các phương thức. Có hơn 40 phương thức LINQ. Trong bài giảng này chúng ta không thể hướng
dẫn tất cả mà chỉ hướng dẫn cách đọc và cách học là chính. Khi cần sử dụng đến phương thức
nào có thể tự học được.
Với yêu cầu trên chúng ta có thể viết truy vấn như sau:
var hn = contacts.Where(c => c.Address == "Ha Noi");
foreach (var c in hn) Console.WriteLine($"{ c.FirstName} { c.LastName}");

298
Như đã biết, ở vị trí tham số này có thể cung cấp hàm lambda, phương thức
cục bộ, phương thức instance, phương thức tĩnh, phương thức vô danh. Ở
đây chúng ta sử dụng hàm lambda cho gọn.
Do truy vấn LINQ sử dụng cơ chế thực thi trễ, chỉ đến khi gọi lệnh duyệt
danh sách, truy vấn trên mới được thực thi để trả dữ liệu về cho biến hn.
Yêu cầu 2: hãy lọc tất cả các object nằm ở vị trí lẻ
Theo yêu cầu này, chúng ta cần lọc ra tất cả các object có index 1, 3, 5,....
Để thực hiện yêu cầu này, chúng ta sử dụng overload thứ hai của phương
thức Where:
var odd = contacts.Where((c, i) => i % 2 != 0);
foreach (var c in odd) Console.WriteLine($"{c.FirstName} {c.LastName}");
Overload thứ hai có thêm tham số đầu vào thứ hai thuộc kiểu int. Tham số
này chứa index của phần tử đang được duyệt. Khi có giá trị index chúng ta
dễ dàng tính ra những phần tử ở vị trí lẻ.
Phương thức Select
Phương thức Select có vai trò rất giống với truy vấn select của ngôn ngữ
SQL. Nhiệm vụ của nó là biến đổi object từ dạng này sang dạng khác, gọi
là “projection” – phép chiếu.
Yêu cầu 1: hãy tạo ra một danh sách họ tên của các contact (chứ không cần lấy
trọn vẹn dữ liệu của mỗi contact) theo dạng: “Donald Trump”, “Barrack
Obama”,...
Chúng ta thấy dữ liệu theo yêu cầu không còn giống như dữ liệu gốc nữa.
Chúng ta phải duyệt qua danh sách dữ liệu. Với mỗi phần tử phải trích lấy
họ và tên rồi ghép lại với nhau tạo ra một object mới. Các object mới sẽ tập
trung vào một danh sách riêng.
Phương thức Select được sử dụng cho những mục đích biến đổi như vậy:

Danh sách tham số của truy vấn Select


Khi đọc mô tả này có thể xác định: đầu vào của Select là một biến kiểu
delegate Func<Contact, TResult> (gọi là selector – hàm chọn). Delegate
này tương ứng với các phương thức có kiểu đầu vào là Contact (kiểu cơ sở
của dữ liệu), kiểu đầu ra (TResult) do người lập trình tự xác định.
Phương thức tham số này có nhiệm vụ chuyển những dự liệu cần thiết từ
biến kiểu Contact sang biến kiểu TResult. Kết quả thực hiện của phương
thức Select là một danh sách object thuộc kiểu đích TResult.
299
var names = contacts.Select(c => $"{ c.FirstName} { c.LastName}");
foreach (string name in names) Console.WriteLine(name);
Phương thức Select thường được sử dụng với kiểu vô danh và từ khóa var
và có thể áp dụng lọc dữ liệu (với phương thức Where).
Yêu cầu 2: Hãy lấy ra danh sách họ tên của những contact có tuổi nhỏ hơn 50.
var youngs = contacts
.Where(c => c.Age < 50)
.Select(c => new { FullName = $"{c.FirstName} {c.LastName}", c.Age });
foreach (var c in youngs)
{
Console.WriteLine($"Full name: {c.FullName}; Age: {c.Age}");
}
Ở đây, chúng ta áp dụng phương thức Where để lọc lấy những contact có
tuổi dưới 50. Trong danh sách này, chúng ta lại áp dụng projection để tạo
ra kết quả là danh sách của các object thuộc kiểu vô danh chứa hai thuộc
tính FullName và Age, được tạo thành từ dữ liệu của mỗi contact.
Qua hai hướng dẫn trên chúng ta thấy, việc học cách sử dụng các phương
thức LINQ là tương đối đơn giản nếu như nắm chắc các khái niệm thường
dùng. Bạn có thể tự mình tìm hiểu thêm cách sử dụng các phương thức khác
bằng cách đọc mô tả của phương thức như đã thực hiện ở các ví dụ trên.

300
Danh sách trong C#: ArrayList, List, SortedList,
Dictionary
Trong bài học này chúng ta sẽ chuyển sang nội dung về các loại danh sách
(List) cơ bản thường dùng trong C#, bao gồm lớp ArrayList, SortedList, danh
sách tổng quát (Generic List) List<T>, và từ điển (Dictionary). Lưu ý thấy
ngay rằng đây là các implementation sẵn có của C#. Chúng ta có thể học
và sử dụng ngay mà không cần phải tự implement. Đây cũng là các loại dữ
liệu danh sách được sử dụng rất phổ biến trong lập trình C#.

Mảng và Danh sách trong C#


Mảng là một cấu trúc dữ liệu tập hợp rất phổ biến và hữu dụng trong nhiều
thuật toán (như các thuật toán sắp xếp). Tuy nhiên, trong nhiều trường hợp
khả năng ứng dụng của mảng bị hạn chế hoặc phức tạp hơn do bản chất
của cấu trúc mảng: kích thước của mảng là cố định sau khi khởi tạo.
Giả sử bạn không biết trước được tổng số phần tử của mảng (và đây cũng
là tình huống rất thường gặp), bạn sẽ phải tạo ra một mảng rất lớn để có
đủ chỗ cho dữ liệu về sau. Đây cũng là một giải pháp rất thường thấy khi
các bạn mới bắt đầu nhập môn lập trình. Dĩ nhiên đây chỉ là một giải pháp
tình thế và nó rất không hiệu quả.
Giải pháp triệt để là sử dụng những cấu trúc dữ liệu khác tương tự mảng
nhưng cho phép tăng giảm số lượng phần tử linh động khi có nhu cầu. Các
cấu trúc dữ liệu như vậy thường được gọi là danh sách (List).
C# và .NET Framework xây dựng sẵn nhiều loại danh sách (list) khác nhau
cho các loại nhu cầu. Trong bài học này chúng ta sẽ học lớp ArrayList, một
loại danh sách (list) đơn giản nhất trong C#. Tiếp theo chúng ta sẽ làm
quen với lớp List<T> – loại danh sách tổng quát được sử dụng rộng rãi bậc
nhất trong C#. Cuối cùng chúng ta sẽ làm quen với SortedList – loại danh
sách (list) tổng quát đặc biệt luôn luôn duy trì sắp xếp cho phần tử.

Kiểu (lớp) ArrayList trong C#


ArrayList là lớp thực thi cho một loại danh sách (list) trong C# đặc biệt
có khả năng:
1. Chứa dữ liệu thuộc bất kỳ kiểu cơ sở nào,
2. Dễ dàng thêm-bớt-tìm kiếm phần tử trong danh sách,
3. Các phần tử có thể có kiểu cơ sở khác nhau.
Lớp ArrayList nằm trong không gian tên System.Collections.
Chúng ta hãy cùng thực hành một số ví dụ nhỏ với lớp ArrayList.
301
using System;
using System.Collections;
namespace P01_ArrayList
{
class Program
{
static void Print(ArrayList list, string label)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"{label}: ");
Console.ResetColor();
if (list.Count == 0)
Console.Write("EMPTY!");
// duyệt danh sách và in các phần tử ra console
foreach (object item in list)
{
Console.Write($"{item}\t");
}
// hoặc
//for (var i = 0; i < list.Count; i++)
//{
// Console.Write($"{list[i]}\t");
//}
Console.WriteLine();
}
static void CreateInitialize()
{
Console.WriteLine("#Khởi tạo ArrayList");
// khởi tạo array list
var list1 = new ArrayList();
// khởi tạo array list và cung cấp sẵn dữ liệu ban đầu
var list2 = new ArrayList(new object[] { "Allo", 1, 2, 3, true });
// khởi tạo và cung cấp kích thước ban đầu (sau có thể thêm bớt thoải mái)
var list3 = new ArrayList(5);
Print(list1, "LIST 01");
Print(list2, "LIST 02");
Print(list3, "LIST 03");
Console.WriteLine("#####");
}
static void Add()
{
Console.WriteLine("#Thêm phần tử");
// khởi tạo array list
var arrayList = new ArrayList();
// thêm một số nguyên (vào cuối)
arrayList.Add(100);
// thêm tiếp một mảng số nguyên
arrayList.AddRange(new[] { 1, 2 });
// thêm một chuỗi
arrayList.Add("Trump");
// thêm một mảng string
arrayList.AddRange(new[] { "Washington", "Moscow", "Beijing", "London",
"Paris" });
Print(arrayList, "");
Console.WriteLine("#####");
}
static void Insert()
{

302
Console.WriteLine("#Chèn phần tử");
var list = new ArrayList(5);
list.Insert(0, 'A'); // lưu ý arrayList khởi tạo với 5 phần tử
list.InsertRange(1, new[] { 2, 3, 4 });
// chèn 1 bool vào vị trí số 1
list.Insert(1, true);
Print(list, "");
Console.WriteLine("#####");
}
static void Remove()
{
Console.WriteLine("#Xóa phần tử");
var arrayList = new ArrayList(new object[] { "Allo", 1, 2, 3, true });
// xóa phần tử có giá trị 1
arrayList.Remove(1);
Print(arrayList, "");
// xóa phần tử ở vị trí số 1
arrayList.RemoveAt(1);
Print(arrayList, "");
Console.WriteLine("#####");
}
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.Unicode;
CreateInitialize(); Console.WriteLine();
Add(); Console.WriteLine();
Insert(); Console.WriteLine();
Remove(); Console.WriteLine();
Console.ReadKey();
}
}
}
Dịch và chạy chương trình sẽ thu được kết quả như dưới đây

303
Khai báo và khởi tạo ArrayList
ArrayList cung cấp 3 overload để khởi tạo object.
// khởi tạo array list
var list1 = new ArrayList();
// khởi tạo array list và cung cấp sẵn dữ liệu ban đầu
var list2 = new ArrayList(new object[] { "Allo", 1, 2, 3, true });
// khởi tạo và cung cấp kích thước ban đầu (sau có thể thêm bớt thoải mái)
var list3 = new ArrayList(5);
Cách đơn giản nhất là dùng overload không tham số.
Overload thứ hai nhận một tập hợp giá trị bất kỳ làm tham số. Khi khởi tạo,
các phần tử của tập hợp này sẽ được sao chép sang và trở thành phần tử
của arraylist.
Overload thứ ba nhận một số nguyên làm tham số. Số nguyên này thể hiện
dung lượng (capacity) tạm thời của arraylist, nghĩa là số phần tử có thể
chứa được. Lưu ý rằng, không giống với mảng, dung lượng của arraylist có
thể tiếp tục tăng lên theo nhu cầu. Số phần tử thực chứa trong arraylist
luôn nhỏ hơn hoặc bằng với dung lượng.
Thêm/chèn phần tử vào ArrayList
Để thêm phần tử vào cuối danh sách có thể dùng hai phương thức: Add
để thêm từng phần tử đơn; AddRange để thêm danh sách phần tử.
AddRange nhận tham số là một mảng các object.
// thêm một số nguyên (vào cuối)
list.Add(100);
// thêm tiếp một mảng số nguyên
list.AddRange(new[] { 1, 2 });
// thêm một chuỗi
list.Add("Trump");
// thêm một mảng string
list.AddRange(new[] { "Hanoi", "Moscow", "Beijing", "London", "Paris" });
ArrayList chấp nhận kiểu của phần tử là object. Điều này có nghĩa là nó
chấp nhận phần tử thuộc tất cả các kiểu dữ liệu của C#.
Trong C#, lớp object (hoặc Object) là lớp tổ tông của tất cả các lớp khác, kể cả lớp do người
dùng xây dựng. Theo cơ chế đa hình và kế thừa, một đối tượng tạo ra từ lớp con cũng được xem
là đối tượng của lớp cha (biến thuộc kiểu con đồng thời cũng là biến thuộc kiểu cha). Do vậy,
bất kỳ đối tượng nào trong C# cũng đồng thời là đối tượng của lớp object. Nếu phương thức
chấp nhận tham số thuộc kiểu object, nó chấp nhận tham số thuộc bất kỳ kiểu nào của C#.
Chúng ta cũng có thể chèn phần tử vào vị trí bất kỳ trong danh sách với
phương thức Insert hoặc InsertRange
var list = new ArrayList(5);
list.Insert(0, 'A'); // lưu ý arrayList khởi tạo với 5 phần tử
list.InsertRange(1, new[] { 2, 3, 4 });
// chèn 1 bool vào vị trí số 1

304
list.Insert(1, true);
Khi chèn vào một vị trí, tất cả các phần tử đứng sau sẽ bị dồn về cuối danh
sách.
Xóa phần tử khỏi ArrayList
Để xóa phần tử khỏi danh sách có thể sử dụng một trong các phương thức:
Remove, RemoveAt, RemoveRange.
var list = new ArrayList(new object[] { "Allo", 1, 2, 3, true });
// xóa phần tử có giá trị 1
list.Remove(1);
// xóa phần tử ở vị trí số 1
list.RemoveAt(1);
 Remove sẽ xóa phần tử đầu tiên trong danh sách có giá trị trùng với
giá trị cung cấp.
 RemoveAt xóa bỏ phần tử ở một vị trí xác định.
 RemoveRange xóa bỏ một nhóm phần tử.
Ngoài ra, để xóa bỏ tất cả các phần tử, có thể dùng phương thức Clear.
Truy xuất ArrayList
Để truy xuất từng phần tử, chúng ta dùng toán tử chỉ số (index operator)
tương tự mảng:
var list = new ArrayList(new object[] { "Allo", 1, 2, 3, true });
var a = (int)list[1];
var b = (int)list[2];
var str = list[0] as string;
Sự khác biệt ở chỗ, khi đọc phần tử của danh sách, chúng ta đồng thời phải
thực hiện ép kiểu (type casting) từ kiểu object sang kiểu cụ thể khác. Trong
ví dụ trên, chúng ta ép từ object sang int và string.
Để duyệt danh sách, chúng ta sử dụng vòng lặp tương tự như mảng. Một
cách khác thường gặp là sử dụng vòng lặp foreach:
foreach (object item in list)
{
Console.Write($"{item}\t");
}
for (var i = 0; i < list.Count; i++)
{
Console.Write($"{list[i]}\t");
}
Để truy xuất một phần (danh sách con) của ArrayList có thể sử dụng phương
thức GetRange.

305
Một số tính năng khác
Lấy thông tin về số lượng phần tử đang chứa trong danh sách: thuộc tính
Count
Lấy thông tin về dung lượng hiện tại: thuộc tính Capacity
Xác định xem một giá trị có nằm trong danh sách: phương thức Contains
Xác định chỉ số của phần tử theo giá trị: phương thức IndexOf
Sắp xếp danh sách: phương thức Sort
Chuyển đổi danh sách thành mảng: phương thức ToArray

Ưu nhược điểm của ArrayList


Ưu điểm
Có thể thấy việc sử dụng ArrayList rất đơn giản và không khác biệt nhiều
với mảng. ArrayList cho phép truy xuất trực tiếp phần tử theo chỉ số tương
tự như mảng. Do đó, các thuật toán đã biết với mảng hoàn toàn có thể thực
hiện được trên ArrayList.
ArrayList có ưu thế so với mảng là khả năng chèn-thêm mới-xóa bỏ phần
tử rất linh hoạt. Đây là lợi thế chung của các loại danh sách so với mảng
tĩnh.
ArrayList không giới hạn kiểu dữ liệu của phần tử. Hoàn toàn có thể sử dụng
ArrayList như một kho dữ liệu trong bộ nhớ để chứa các object.
Nhược điểm
Các phần tử của ArrayList đều thuộc kiểu object. Do vậy, khi dùng để lưu
trữ các giá trị không tham chiếu (như int, bool) sẽ xảy ra quá trình
boxing/unboxing. Hai quá trình này mất thời gian để thực hiện. Nếu làm
boxing/unboxing với lượng dữ liệu lớn sẽ rất không hiệu quả.
Khi truy xuất phần tử sẽ phải thực hiện biến đổi kiểu (type casting). Khi
thêm (chèn) phần tử, mọi phần tử đều trải qua boxing về kiểu object. Khi
lấy dữ liệu ra chúng ta phải thực hiện biến đổi kiểu (type casting) về dạng
ban đầu. Do đó, người lập trình phải tự theo dõi kiểu dữ liệu của từng phần
tử. Nếu không theo dõi được kiểu của phần tử, quá trình biến đổi kiểu có
thể bị lỗi. Đặc điểm này của ArrayList không phù hợp với đặc thù strong-
typing (định kiểu mạnh) của C#.
Kiểu dữ liệu List<T> sau đây sẽ giải quyết các vấn đề của ArrayList và cũng
là kiểu dữ liệu nên sử dụng để thay thế mảng.

306
Generic List trong C#: List<T>
List<T> là một kiểu dữ liệu rất mạnh trong C# và được sử dụng đặc biệt
rộng rãi. List<T> cũng là kiểu cơ sở để tạo ra các kiểu tập hợp cao cấp hơn
trong C# (như BindingList). Để hiểu được kiểu dữ liệu này, bạn cần nắm
được khái niệm và kỹ thuật lập trình generic trong C#.
List<T> được định nghĩa sẵn với các phương thức và thuộc tính tương tự
như mảng và ArrayList. List<T> có các phương thức và thuộc tính với tên
gọi và tính năng giống hệt của ArrayList, bao gồm: Count, Capacity,
Add/AddRange, Clear, Contains, IndexOf/LastIndexOf, Insert/InsertRange,
Remove, RemoveAt, RemoveRange, Reverse, ToArray. Việc truy xuất phần
tử của List<T> giống hệt mảng và ArrayList.
Sự khác biệt của List<T> là ở chỗ kiểu dữ liệu cơ sở (của các phần tử) chỉ
được xác định ở giai đoạn sử dụng class, thay vì ở giai đoạn định nghĩa như
các kiểu none-generic.
Để dễ hình dung, trong C# List<T> tương tự như ArrayList, nhưng các phần
tử của nó bắt buộc phải cùng kiểu T (giống như mảng). Trong đó T được
xác định khi khai báo object của class List<T>. T có thể là bất kỳ kiểu dữ
liệu C# hợp lệ nào, dù là kiểu định nghĩa sẵn (built-in types) hay kiểu do
người dùng định nghĩa.
Hãy cùng thực hiện ví dụ nhỏ sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace P02_List
{
class Program
{
static void CreateInitialize()
{
// khởi tạo danh sách trống, các phần tử phải có kiểu int
var list1 = new List<int>();
Print(list1, "List of Ints");
// khai báo và khởi tạo danh sách có thể chứa ngay 3 phần tử, các phần tử
phải có kiểu string
var list2 = new List<string>(3);
Print(list2, "List of Strings");
// khai báo và khởi tạo danh sách ký tự, đồng thời cung cấp luôn dữ liệu
ban đầu
var list3 = new List<char>(new[] { 'a', 'b', 'c', 'd' });
Print(list3, "List of Chars");
// khai báo và khởi tạo sử dụng cú pháp object initialization
var list4 = new List<int> { 1, 2, 3, 4, 5 };
Print(list4, "List of Ints");
}
static void Print<T>(List<T> list, string label)
{

307
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"{label}: ");
Console.ResetColor();
if (list.Count == 0)
Console.Write("EMPTY!");
// duyệt danh sách và in các phần tử ra console
foreach (object item in list)
{
Console.Write($"{item} ");
}
// hoặc
//for (var i = 0; i < list.Count; i++)
//{
// Console.Write($"{list[i]}\t");
//}
Console.WriteLine();
}
static void Access()
{
Console.WriteLine("# Truy xuất phần tử");
var list = new List<int> { 1, 2, 3, 4, 5 };
var a = list[0];
var b = list[1];
// để ý rằng chúng ta không cần ép kiểu cho a và b do List<T> có đặc tính
strong-typed
var c = a + b;
Console.WriteLine($"Type of a: {a.GetType().Name}\r\nType of a:
{b.GetType().Name}");
}
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
CreateInitialize(); Console.WriteLine();
Access(); Console.WriteLine();
Console.ReadKey();
}
}
}
List<T> nằm trong không gian tên System.Collections.Generic (khác
với ArrayList nằm trong System.Collections).
So với ArrayList, List<T> có khác biệt nhỏ ở cách thức khởi tạo. Khi khởi
tạo List<T> trong C# chúng ta đồng thời phải cung cấp kiểu dữ liệu của
phần tử. Đặc điểm này làm nên tính strong-typed của List<T>, vốn là một
đặc thù trong C#. Sau này khi truy xuất chúng ta không phải ép kiểu, do
mỗi phần tử bắt buộc phải thuộc kiểu T do ta cung cấp khi khởi tạo.
Tất cả các phương thức đã biết ở ArrayList đều có mặt trên List<T> với cùng
tên gọi và chức năng. Do đó chúng ta không lặp lại chúng nữa. Bạn hoàn
toàn có thể tự mình thử code với các phương thức đó.

308
Sử dụng List với kiểu do người dùng định nghĩa
Ở phần trên chúng ta đã sử dụng List<T> với T là các kiểu có sẵn trong C#
(built-in types). Không chỉ vậy, T hoàn toàn có thể là các kiểu dữ liệu do
người dùng định nghĩa.
Hãy cùng xem xét một ví dụ nhỏ khác.
using System;
using System.Collections.Generic;
namespace P03_ListWithCustomTypes
{
class Program
{
static void Main(string[] args)
{
Console.Title = "List with custom type";
var people = Initialize();
people.Add(new Person { Name = "Theresa May", Age = 22, Country =
Country.UK });
Console.WriteLine("World leaders:");
Print(people);
Console.ReadKey();
}
static List<Person> Initialize()
{
var people = new List<Person> {
new Person { Name = "Donald Trump", Age = 18, Country = Country.US },
new Person { Name = "Vladimir Putin", Age = 19, Country = Country.RU },
new Person { Name = "Angela Merkel", Age = 20, Country = Country.DE },
new Person { Name = "Emmanuel Macron", Age = 21, Country = Country.FR },
};
return people;
}
static void Print(List<Person> people)
{
foreach (var p in people)
{
Console.WriteLine($"- {p.Name}, {p.Age} years old, from {p.Country}");
}
}
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Country Country { get; set; }
}
enum Country
{
RU, VI, UK, US, DE, FR
}
}

309
Có thể dễ dàng nhận thấy, làm việc với các kiểu tự định nghĩa không có gì
khác biệt với các kiểu có sẵn. Đặc tính strong-typed của List<T> rất hữu
ích khi làm việc với các kiểu dữ liệu phức tạp do người dùng định nghĩa.
Không cần boxing/unboxing hay type casting, đồng thời đảm bảo hiệu suất
cao khi truy xuất.
Một ưu thế rất lớn của List<T> là có thể sử dụng bộ thư viện LINQ để truy
vấn dữ liệu.
List và LINQ
Các thuật toán sắp xếp mà chúng ta đã xem xét chỉ là một trong số những
thuật toán hoạt động trên mảng và danh sách. Khi làm việc với các kiểu dữ
liệu này có một loạt các yêu cầu rất thường gặp như tìm giá trí lớn nhất/nhỏ
nhất, tính giá trị trung bình, trích danh sách con, trích một phần dữ liệu,
nhóm dữ liệu,....
LINQ là một bộ thư viện các phương thức mở rộng (extension method) hỗ
trợ thực hiện hầu như tất cả các yêu cầu có thể phát sinh khi làm việc với
dữ liệu tập hợp như mảng và danh sách. Tất các các phương thức LINQ chỉ
sử dụng được nếu trong khối using chúng ta có lệnh using System.Linq.
Hãy cùng thực hiện ví dụ sau (mở rộng tiếp ví dụ ở phần trên)
using System;
using System.Collections.Generic;
using System.Linq;
namespace P03_ListWithCustomTypes
{
class Program
{
static void Main(string[] args)
{
Console.Title = "List with custom type";
var people = Initialize();
people.Add(new Person { Name = "Theresa May", Age = 22, Country =
Country.UK });
Console.WriteLine("World leaders:");
Print(people);
Console.WriteLine("Sorted by age:");
Print(Sort(people, "age"));
Console.WriteLine("Sorted by name:");

310
Print(Sort(people, "name"));
Console.WriteLine("Leaders younger than 20");
Print(GetYoungLeaders(people, 20));
Console.ReadKey();
}
static List<Person> Initialize()
{
var people = new List<Person> {
new Person { Name = "Donald Trump", Age = 18, Country = Country.US },
new Person { Name = "Vladimir Putin", Age = 19, Country = Country.RU },
new Person { Name = "Angela Merkel", Age = 20, Country = Country.DE },
new Person { Name = "Emmanuel Macron", Age = 21, Country = Country.FR },
};
return people;
}
static void Print(List<Person> people)
{
foreach (var p in people)
{
Console.WriteLine($"- {p.Name}, {p.Age} years old, from {p.Country}");
}
}
static List<Person> Sort(List<Person> people, string criteria)
{
if (criteria.ToLower().Equals("age"))
return people.OrderBy(p => p.Age).ToList();
if (criteria.ToLower().Equals("name"))
return people.OrderBy(p => p.Name).ToList();
return people;
}
static List<Person> GetYoungLeaders(List<Person> people, int age)
{
return people.Where(p => p.Age <= age).ToList();
}
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Country Country { get; set; }
}
enum Country
{
RU, VI, UK, US, DE, FR
}
}

311
Nếu sử dụng Intellisense với một biến List<T> bạn sẽ nhận thấy số lượng
phương thức có thể sử dụng lớn hơn rất nhiều so với ArrayList. Đó chính là
các phương thức mở rộng của LINQ bổ sung vào kiểu List<T>. Dễ dàng
nhận thấy rất nhiều phương thức có tên gần giống với truy vấn SQL. Các
thao tác thường gặp nhất với dữ liệu tập hợp cũng có mặt đầy đủ trong thư
viện LINQ.
LINQ là một chủ đề hoàn toàn khác và chúng ta không trình bày chi tiết trong bài học này.
Có thể nói, List<T> là giải pháp hoàn hảo thay thế cho mảng để đồng thời
đảm bảo tính linh hoạt và hiệu suất. Thực tế, List<T> cũng là kiểu tập hợp
được dùng rộng rãi hàng đầu trong C#.
Tiếp theo đây chúng ta sẽ xem xét loại danh sách đơn giản cuối cùng: danh
sách sắp xếp (SortedList).

Danh sách sắp xếp (SortedList)


Giới thiệu chung về SortedList
SortedList là một loại danh sách generic (như List<T>) nhưng lưu các cặp
khóa-giá trị, đồng thời luôn tự động sắp xếp dữ liệu theo khóa. Yêu
cầu bắt buộc là khóa không được trùng lặp và không được nhận giá trị null.
SortedList nằm trong không gian tên System.Collections.Generic (tương
tự List).
Dưới đây là một số phương thức và thuộc tính của SortedList:
 Add: thêm một phần tử vào cuối danh sách

312
 Remove: bỏ phần tử có giá trị tương ứng khỏi danh sách
 ContainsKey: xác định xem một khóa có mặt trong danh sách hay
không
 ContainsValue: xác định xem một giá trị có mặt trong danh sách hay
không
 IndexOfKey: trả lại chỉ số của khóa trong danh sách
 IndexOfValue: trả lại chỉ số của giá trị trong danh sách
 Thuộc tính Keys: trả lại danh sách khóa
 Values: trả lại danh sách giá trị
Việc truy xuất các phần tử sử dụng phép toán [] tương tự như mảng.
Ví dụ minh họa việc sử dụng SortedList trong C#
Để dễ hình dung cách làm việc với SortedList, hãy cùng thực hiện ví dụ sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace P04_SortedList
{
class Program
{
static void Main(string[] args)
{
Console.Title = "SortedList";
var people = CreateAddressBook();
Print(people);
Console.ReadKey();
}
static void Print(SortedList<string, Person> people)
{
Console.WriteLine($"People in the list: {people.Count}");
foreach (var key in people.Keys)
{
Console.WriteLine($"-{people[key].Name}");
}
Console.WriteLine("\r\nContact details");
foreach (var p in people)
{
Console.WriteLine($"-{p.Value.Name}, born
{p.Value.DateOfBirth.ToShortDateString()}, contact:
{p.Value.Email}, {p.Value.Phone}");
}
}
static SortedList<string, Person> CreateAddressBook()
{
var addressBook = new SortedList<string, Person>
{

313
{"trump", new Person { Name = "Donald Trump", DateOfBirth = new
DateTime(1990, 1, 1), Email = "trump@gmail.com",
Phone = "01234.567.890"} },
{"putin", new Person { Name = "Vladimir Putin", DateOfBirth = new
DateTime(1990, 1, 2), Email = "putin@gmail.com",
Phone = "01234.567.890"} },
{"macron", new Person { Name = "Emmanuel Macron", DateOfBirth =
new DateTime(1990, 1, 3), Email =
"macron@gmail.com", Phone = "01234.567.890"} },
{"merkel", new Person { Name = "Angela Merkel", DateOfBirth = new
DateTime(1990, 1, 4), Email = "merkel@gmail.com",
Phone = "01234.567.890"} },
};
addressBook.Add("may", new Person { Name = "Theresa May", DateOfBirth =
new DateTime(1990, 1, 5), Email = "may@gmail.com",
Phone = "01234.567.890" });
return addressBook;
}
}
class Person
{
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
}

Có thể để ý thấy rằng, khi khai báo object thuộc kiểu SortedList, chúng ta
phải cung cấp hai kiểu dữ liệu: TKey và TValue. TKey là kiểu dữ liệu của
khóa, TValue là kiểu dữ liệu của giá trị. Chúng ta đã lựa chọn kiểu của khóa
là string, kiểu của giá trị là Person.
Khi khởi tạo cũng như thêm phần tử, chúng ta phải cung cấp cả khóa và
object thuộc kiểu Person.

314
Chúng ta cũng đã thấy, danh sách luôn được sắp xếp theo khóa. Trong
trường hợp ví dụ trên, khóa là thông tin về họ được viết thường. Các chính
khách trong danh sách luôn được sắp xếp theo họ.
Khi duyệt SortedList có chút khác biệt nhỏ so với List hoặc ArrayList ở chỗ,
chúng ta phải duyệt từng cặp khóa/giá trị. Object cần truy xuất nằm trong
phần giá trị của cặp này.

Kiểu dữ liệu Dictionary


Khái niệm
Dictionary là một kiểu dữ liệu tập hợp tổng quát (generic collection) tương
tự như List<T> nhưng được dùng cho lưu trữ danh sách các cặp khóa – giá
trị. Khóa và giá trị có thể thuộc bất kỳ kiểu dữ liệu nào của .NET.
Kiểu dữ liệu này được mô tả đầy đủ là Dictionary<TKey, TValue>, trong
đó Tkey là kiểu của khóa, Tvalue là kiểu của giá trị. Lớp Dictionary<TKey,
TValue> được định nghĩa trong không gian tên
System.Collection.Generics.
Dictionary có thể hình dung như bộ từ điển song ngữ, ví dụ, từ điển Anh –
Việt, trong đó từ tiếng Anh là khóa, nghĩa trong tiếng Việt là giá trị.
Lưu ý, khi sử dụng từ điển không được phép sử dụng lặp khóa hoặc để khóa
có giá trị null. Khóa bắt buộc phải là duy nhất (tương tự như trong từ điển
song ngữ). Nếu trùng lặp khóa sẽ báo lỗi ở giai đoạn runtime.
Thuộc tính và phương thức của Dictionary
Một số thuộc tính quan trọng của Dictionary:

Thuộc tính Mô tả
Count Cung cấp tổng số phần tử đang có trong Dictionary<TKey,TValue>.
IsReadOnly Trả về true nếu Dictionary<TKey,TValue> không cho phép ghi
Keys Trả về mảng các khóa của Dictionary<TKey,TValue>.
Values Trả về mảng các giá trị của Dictionary<TKey,TValue>.

Một số phương thức quan trọng của Dictionary:

Phương thức Mô tả
Add Thêm một phần tử mới (cặp khóa/giá trị) vào từ điển.
Remove Xóa một phần tử khỏi từ điển (theo khóa) Dictionary<TKey, TValue>.

315
Phương thức Mô tả
ContainsKey Kiểm tra xem trong Dictionary<TKey, TValue> có chứa khóa cần tìm không.
ContainsValue Kiểm tra xem trong Dictionary<TKey, TValue> có chứa giá trị cần tìm không.
Clear Xóa tất cả các phần tử khỏi Dictionary<TKey, TValue>.
Thử lấy một giá trị ra theo khóa. Nếu khóa tồn tại, giá trị được lấy ra theo một
TryGetValue
biến ra (out parameter). Nếu khóa không tồn tại trả về giá trị false.

Sử dụng kiểu từ điển


Khởi tạo biến từ điển
using System;
using System.Collections.Generic;
namespace ConsoleApp
{

class Department
{
public string Name { get; set; }
public string Office { get; set; }
}
internal class _12_dictionary
{
private static void Main(string[] args)
{
var dict = new Dictionary<string, Department>
{
["Donald Trump"] = new Department
{ Name = "Sale", Office = "New York" },
["Bill Clinton"] = new Department
{ Name = "Technical", Office = "California" },
["George Bush"] = new Department
{ Name = "Service", Office = "Alaska" }
};
Console.ReadKey();
}
}
}
Thêm cặp khóa/giá trị vào từ điển
dict.Add("Jimmy Carter", new Department { Name = "Staff", Office =
"Washington DC" });
dict["Barrack Obama"] = new Department { Name = "Personnel", Office =
"Hawaii" };
Truy xuất phần tử trong từ điển
Console.WriteLine($"Bill Clinton: working at {dict["Bill Clinton"].Name}
department in {dict["Bill Clinton"].Office}");
Xóa bỏ phần tử của từ điển
dict.Remove("Donald Trump");

316
Duyệt danh sách phần tử
foreach (var i in dict)
{
Console.WriteLine($"Key = {i.Key}, Value = {i.Value}");
}
Kiểm tra sự tồn tại của khóa hoặc giá trị
if (dict.ContainsKey("Donald Trump"))
Console.WriteLine("Donald Trump is working here");
else
Console.WriteLine("Donald Trump has quit the job");

317
Stream trong C#, kiến trúc stream
Stream là một cơ chế hỗ trợ đọc ghi dữ liệu đặc biệt trong C#. Các hoạt
động đọc ghi dữ liệu với tập tin hoặc qua mạng (lập trình socket) đều phải
sử dụng đến các phiên bản riêng của Stream. Do vậy, trước khi bắt đầu học
làm việc với tập tin, bạn cần biết rõ về stream. Do kiến trúc stream khá
phức tạp, với nhiều loại class kế thừa và sử dụng lẫn nhau, không nắm bắt
được cấu trúc của stream bạn sẽ rất dễ dàng lạc vào một “ma trận” các
class chồng chéo.
Bài học này sẽ giới thiệu với bạn thông tin chi tiết về kiến trúc stream, giúp
bạn có cái nhìn hệ thống về stream trong C#. Nó là nền tảng để bạn có thể
học làm việc với tập tin hoặc lập trình mạng với socket.

Khái niệm và kiến trúc stream trong C#


Trong .NET Framework, luồng dữ liệu (stream) là một thành phần trung
gian giữa ứng dụng và nguồn dữ liệu (tập tin, network,...) và có vai trò:
 Hỗ trợ việc đọc/ghi dữ liệu với các loại nguồn khác nhau;
 Cho phép sử dụng một khối lượng bộ nhớ nhỏ xác định để đọc dữ liệu
với khối lượng lớn bất kỳ;
 Giúp việc đọc và ghi dữ liệu ổn định, hiệu quả và đơn giản hơn.
Ví dụ, khi đọc dữ liệu từ một tập tin lớn, nếu đọc toàn bộ dữ liệu cùng lúc,
chương trình có thể treo vì không thể xử lý khối lượng dữ liệu quá lớn.
Stream giúp đọc dữ liệu theo từng khối nhỏ hoặc từng byte riêng rẽ. Chương
trình sau đó đọc dữ liệu từ stream.
Tương tự, khi cần ghi dữ liệu vào tập tin, dữ liệu trước hết được đẩy vào
stream. Sau đó, stream sẽ giúp ghi dữ liệu vào tập tin. Tình huống tương
tự cũng diễn ra khi đọc/ghi dữ liệu từ mạng (qua liên kết Tcp).
So sánh một cách hình tượng, stream giống như một đường ống nối chương
trình với nguồn dữ liệu và cho phép một chuỗi byte (giống như dòng nước)
chạy qua. Ngoài ra, stream cũng tạo ra một giao diện thống nhất để đơn
giản hóa việc đọc/ghi dữ liệu.
Do số lượng lớp hỗ trợ làm việc với luồng dữ liệu trong .NET rất nhiều, chúng
ta cần biết về kiến trúc của stream để tránh các nhầm lẫn khi sử dụng.

318
Kiến trúc stream trong .NET
Kiến trúc của stream trong .NET Framework tương đối phức tạp và được
chia làm ba nhóm: luồng làm việc với nguồn dữ liệu (backing store stream),
luồng hỗ trợ (decorator stream), bộ điều hợp luồng (stream adapter).

Các thành phần của stream trong C#


Luồng làm việc với nguồn dữ liệu (backing store stream)
Nguồn dữ liệu (backing store) là nơi dữ liệu thực sự chứa dữ liệu. Luồng làm
việc với các loại nguồn này chịu trách nhiệm đọc dữ liệu vào và/hoặc ghi ra
theo từng chuỗi byte.
Có một số loại nguồn dữ liệu chính: tập tin, bộ nhớ (memory), mạng
(network), và vùng lưu trữ riêng (isolated storage). Do đó, C# tạo ra các
loại stream khác nhau để làm việc với từng loại nguồn dữ liệu tương ứng,
phân biệt là FileStream, MemoryStream, NetworkStream,
IsolatedStorageStream.
Các loại stream này được gọi chung là backing store stream (luồng làm việc
với nguồn dữ liệu).
Isolated Storage là vùng lưu trữ riêng của các ứng dụng bị giới hạn quyền, ví dụ ứng dụng viết
bằng Silverlight. Các ứng dụng này được cài đặt trên hệ thống nhưng có độ tin tưởng nằm trong
vùng Internet, do đó bị giới hạn quyền truy xuất hệ thống tập tin của Windows.
Tất cả các lớp backing store stream của .NET đều kế thừa từ lớp abstract
Stream (System.IO.Stream).
Trong bài học tiếp theo bạn sẽ học cách làm việc với FileStream. Nếu quan
tâm đến NetworkStream, bạn có thể tìm đọc bài giảng lập trình mạng trong
C#.
319
Luồng hỗ trợ (decorator stream)
Luồng hỗ trợ (decorator stream) không làm việc trực tiếp với nguồn dữ liệu
mà làm việc với luồng backing store để cung cấp những chức năng hỗ trợ
như mã hóa, nén hoặc tạo bộ đệm.
Nếu nhìn trong code thì object của luồng hỗ trợ chứa object của luồng
backing store. Nói cách khác, luồng backing store luôn được khởi tạo trước,
luồng decorator nhận luồng backing store làm tham số khi khởi tạo.
Tất cả hoạt động của luồng decorator tác động lên luồng backing store chứa
trong nó. Các luồng hỗ trợ cũng có thể ghép nối với nhau, ví dụ, để vừa nén
vừa mã hóa dữ liệu trong luồng backing store.
Trong tập bài giảng này chúng ta không sử dụng đến luồng hỗ trợ
Bộ tiếp hợp (stream adapter)
Các bộ tiếp hợp luồng (stream adapter) được sử dụng để chuyển đổi từ byte
(do luồng backing store đọc) sang dữ liệu cấp cao giúp đơn giản hóa việc
ghi đọc các dữ liệu này.
Luồng backing store và luồng decorator hoàn toàn hoạt động với byte hoặc
mảng byte. Tuy nhiên, chương trình thường yêu cầu làm việc với dữ liệu
cấp cao hơn như văn bản hoặc xml. Các bộ tiếp hợp cung cấp các phương
thức chuyên dụng để làm việc với dữ liệu cấp cao theo từng loại định dạng
cụ thể:
 Lớp StreamWriter/StreamReader để làm việc với văn bản
 Lớp BinaryWriter/BinaryReader để làm việc với các kiểu dữ liệu cơ sở
(int, bool)
 Lớp XmlWriter/XmlReader làm việc với xml
Nói tóm lại:
 Luồng backing store cung cấp dữ liệu thô (các byte/khối byte)
 Luồng decorator cung cấp các phương pháp biến đổi cho dữ liệu này
 Các adapter cung cấp các phương thức để chuyển đổi về dữ liệu cấp
cao
Các nhóm này phối hợp với nhau theo trình tự (khởi tạo object): backing
store stream => decorator stream (có thể không sử dụng) => stream
adapter (có thể không sử dụng nếu cần làm việc với các byte thô).
StreamWriter/StreamReader, BinaryWriter/BinaryReader được trình bày chi
tiết trong bài học về FileStream.

320
Lớp Stream
Stream là một lớp trừu tượng đưa ra quy định về một tập hợp các phương
thức mà tất cả các lớp backing store stream phải thực thi.
Các phương thức này chia thành các nhóm: đọc, ghi, và định vị.
Vì lí do này, mặc dù chương trình đọc/ghi dữ liệu từ các nguồn khác nhau
nhưng đều sử dụng các phương thức có mô tả giống nhau. Điều này giúp
việc đọc ghi dữ liệu đơn giản hơn rất nhiều. Nếu sử dụng được một loại
stream có thể dễ dàng sử dụng được các loại stream khác.
Tùy thuộc vào loại backing store, các luồng có thể hỗ trợ cả đọc/ghi/định
vị, hoặc chỉ hỗ trợ một/một vài chức năng.
Ví dụ, FileStream hỗ trợ đủ ba chức năng, trong khi NetworkStream chỉ hỗ
trợ đọc và ghi (không hỗ trợ định vị).
Ngoài ba nhóm chức năng trên, luồng hỗ trợ một số thao tác quản lý như
đóng/mở luồng, đẩy dữ liệu (flush), định thời gian chờ.
Dưới đây là danh sách các thành viên chung của tất cả luồng (kế thừa từ
lớp Stream):

Nhóm Thành viên


Reading public abstract bool CanRead { get; }
public abstract int Read (byte[] buffer, int offset, int count)
public virtual int ReadByte();
Writing public abstract bool CanWrite { get; }
public abstract void Write (byte[] buffer, int offset, int count);
public virtual void WriteByte (byte value);
Seeking public abstract bool CanSeek { get; }
public abstract long Position { get; set; }
public abstract void SetLength (long value);
public abstract long Length { get; }
public abstract long Seek (long offset, SeekOrigin origin);
Closing/ flushing public virtual void Close();
public void Dispose();
public abstract void Flush();
Timeouts public virtual bool CanTimeout { get; }
public virtual int ReadTimeout { get; set; }
public virtual int WriteTimeout { get; set; }
Nhóm khác public static readonly Stream Null; // “Null” stream
public static Stream Synchronized (Stream stream);

321
322
FileStream trong C#, làm việc với tập tin và thư mục
FileStream là một loại stream đặc biệt chuyên dùng để đọc ghi dữ liệu với
tập tin. Đây là những khái niệm tương đối mới và khá đặc thù của C# và
.NET. Bài học này sẽ giúp bạn nắm được kỹ thuật đọc ghi tập tin với
FileStream và cách làm việc với tập tin/thư mục trong C#.

Làm việc với tập tin và thư mục


Trước hết chúng ta sẽ học cách sử dụng dụng các lớp .NET hỗ trợ làm việc
với hệ thống tập tin của Windows.
Tất cả các lớp để làm việc với tập tin trong .NET nằm trong không gian
tên System.IO. Ba class chính để làm việc với hệ thống tập tin
là Directory (làm việc với thư mục), File (làm việc với tập
tin), Path (làm việc với đường dẫn).
Lớp Directory
Lớp Directory chứa hầu hết các phương thức tĩnh giúp làm việc với tập tin
và thư mục. Dưới đây là một số phương thức của lớp này giúp kiểm tra
đường dẫn và giúp lấy danh sách tập tin trong một thư mục.
Phương thức tĩnh GetFiles: tìm tất cả các tập tin trong thư mục có phần
mở rộng theo yêu cầu. Ví dụ dưới đây tìm tất cả các tập tin exe trong thư
mục E:\Catalogue:
> Directory.GetFiles(@"E:\CATALOGUE", "*.exe", SearchOption.AllDirectories)
string[149] { "E:\\CATALOGUE\\Client PWI\\uninstall.exe", "E:\\CATALOGUE\\Client
PWI\\element\\360inst_wanmeigj.exe", "E:\\CATALOGUE\\Client
PWI\\element\\dxwebsetup.exe", "E:\\CATALOGUE\\Client
PWI\\element\\elementclient.exe", "E:\\CATALOGUE\\Client PWI\\element\\gt_setup.exe",
"E:\\CATALOGUE\\Client PWI\\element\\gt_updater.exe", "E:\\CATALOGUE\\Client
PWI\\element\\unitywebplayerdevelopment.exe", "E:\\CATALOGUE\\Client
PWI\\element\\reportbugs\\creportbugs.exe", "E:\\CATALOGUE\\Client
PWI\\element\\reportbugs\\pwprotector.exe", "E:\\CATALOGUE\\Client
PWI\\element\\Shaders\\vs\\vsa.exe", "E:\\CATALOGUE\\Client
PWI\\element\\Shaders\\vs\\facerender\\vsa.exe", "E:\\CATALOGUE\\Client
PWI\\element\\Shaders\\vs\\normalmap\\vsa.exe", "E:\\CATALOGUE\\Client
PWI\\launcher\\Launcher.exe", "E:\\CATALOGUE\\Client PWI\\patcher\\patcher.exe",
"E:\\CATALOGUE\\Development Tools\\Database\\SQLEXPR_x64_ENU.exe",
"E:\\CATALOGUE\\Development Tools\\Database\\SSMS-Setup-ENU.exe",...
>

Phương thức này sử dụng ba tham số:


323
1. Đường dẫn tới thư mục;
2. Mẫu tìm kiếm: mẫu văn bản mà phương thức GetFiles sử dụng
trong quá trình tìm kiếm. GetFiles chỉ trả lại những tập tin mà tên
phù hợp với mẫu văn bản của tham số này.
3. Phạm vi tìm kiếm: xác định xem phương thức GetFiles chỉ tìm trong
thư mục được chỉ định (TopDirectoryOnly) hay tìm cả trong các thư
mục con của nó (AllDirectories).
Kết quả thực hiện của phương thức này là một mảng string chứa tên đầy
đủ (bao gồm cả đường dẫn) của các tập tin tìm thấy.
Tương tự, phương thức GetDirectories trả về danh sách tất cả các thư
mục con trong một thư mục.
Phương thức tĩnh Exists : kiểm tra xem một đường dẫn tới thư mục có tồn
tại hoặc chính xác không.
> Directory.Exists(@"C:\Program Files")
true
>

Phương thức CreateDirectory: tạo thư mục mới.


Phương thức Delete: xóa thư mục.
Bạn có thể dễ dàng tìm hiểu được cách sử dụng của các phương thức còn
lại của lớp này.
Lớp Path
Lớp Path cũng chứa hầu hết các phương thức tĩnh giúp phân tích đường dẫn
tới tập tin hoặc thư mục. Dưới đây là cách sử dụng một phương thức của
lớp này:
 Phương thức GetDirectoryName trả lại phần tên thư mục trong
đường dẫn tới tập tin.
 Phương thức GetFileName trích ra phần tên tập tin trong một đường
dẫn tới tập tin, bỏ phần đường dẫn thư mục.
 Phương thức GetFileNameWithoutExtension trích ra phần tên của
tập tin, bỏ phần đường dẫn và phần mở rộng.
 Phương thức GetExtension trả về phần mở rộng của tên tập tin hoặc
thư mục.
Các phương thức của lớp Path đều tương đối dễ sử dụng. Bạn đọc có thể tự
mình tìm hiểu các phương thức khác.

324
Đọc/ghi dữ liệu với tập tin trong C#, FileStream
Ở phần trước chúng ta đã xem xét tổng thể về stream trong .NET
Framework. Trong phần này chúng ta sẽ làm việc với một loại luồng backing
store cụ thể trong C#: FileStream.
Khởi tạo FileStream
Trong C# bạn có thể khởi tạo FileStream theo nhiều cách khác nhau:
// sử dụng phương thức khởi tạo của lớp FileStream
FileStream fs = new FileStream("data1.bin", FileMode.Create);
// sử dụng các phương thức tĩnh của lớp File
FileStream fs1 = File.OpenRead("data1.bin"); // Read-only
FileStream fs2 = File.OpenWrite("data2.bin"); // Write-only
FileStream fs3 = File.Create("data3.bin"); // Read/write
Tất cả các cách trên có điểm chung là bắt buộc phải cung cấp một đường
dẫn tới tập tin.
Cách thứ nhất là linh hoạt nhất, cho phép lựa chọn chế độ làm việc với tập
tin, FileMode. Ba phương pháp còn lại đều là các “lối tắt” giúp đơn giản hóa
việc mở tập tin. Thực chất, chúng tương đương với một số chế độ của
FileMode ở phương pháp thứ nhất.
Sau khi khởi tạo có thể bắt đầu đọc/ghi dữ liệu với tập tin. Tuy nhiên, hiện
tại bạn chỉ có đọc và xử lý các byte thô trực tiếp từ FileStream. Để có thể
xử lý trong chương trình, bạn phải tự mình biến đổi các byte đó về kiểu dữ
liệu mà chương trình cần đến.
Ghi vào tập tin qua FileStream
Hãy xem ví dụ sau:
int i = 1234;
string str = "Hello world";
fs.Write(BitConverter.GetBytes(i), 0, 4);
fs.Write(Encoding.UTF8.GetBytes(str), 0, Encoding.UTF8.GetByteCount(str));
fs.Flush();
fs.Close();
Trong ví dụ này, bạn ghi vào tập tin một số nguyên i có giá trị 1234 và một
chuỗi có giá trị “Hello world”.
Như bạn đã biết từ bài học về stream, các luồng backing store hoàn toàn
làm việc với byte hoặc mảng byte. Chúng không biết về các loại giá trị cấp
cao như int, string, bool hay các object. Do đó bạn phải biến đổi tất cả
các giá trị về mảng byte.
Đối với các kiểu dữ liệu cơ sở (int, bool, char,...), .NET Framework cung cấp
lớp BitConverter để biến đổi về mảng byte và ngược lại. Đối với dữ liệu
văn bản cần sử dụng lớp Encoding.

325
Kiểu byte chỉ sử dụng 1 byte để biểu diễn, do đó biểu diễn ở dạng mảng
byte của giá trị thuộc kiểu byte là một mảng có 1 phần từ và chứa đúng giá
trị đó.
 Quá trình biến đổi một giá trị sang mảng byte phức tạp hơn đối với
các kiểu dữ liệu kích thước lớn:
 Đối với kiểu int (sử dụng 4 byte để biểu diễn 1 giá trị), mảng byte này
chứa 4 phần tử (bất kể số nguyên đó có giá trị bao nhiêu). Đối với
kiểu long (sử dụng 8 byte), mảng byte phải chứa 8 phần tử.
Đến đây phát sinh vấn đề: trật tự của các phần tử trong mảng, gọi là endianness. Có hai xu
hướng khác nhau để viết thứ tự các byte trong mảng:
(1) Lối viết big-endian (sử dụng trong Mac và Linux): byte bên trái có giá trị hơn, giống cách
chúng ta đọc số;
(2) Lối viết little-endian (sử dụng trong Windows): byte bên phải có giá trị hơn, ngược lại cách
chúng ta đọc số.
Phương thức Write của FileStream thực thi phương thức abstract tương ứng
của lớp Stream cho phép ghi một mảng byte vào luồng. Phương thức này
chỉ ghi <count> byte bắt đầu từ vị trí <offset>, trong đó offset và count lần
lượt là tham số thứ 2 và thứ 3 của phương thức này.
Trong ví dụ trên, phương thức GetByte của BitConverter chuyển biến i
thành một mảng 4 byte (do int là kiểu dữ liệu biểu diễn bằng 4 byte). Mảng
này được ghi trọn vẹn vào tập tin, do đó offset = 0, count = 4.
Đối với kiểu string, biểu diễn dạng mảng byte của nó phụ thuộc vào cách
mã hóa ký tự (encoding). Nếu dùng mã ASCII, mỗi ký tự là 1 byte nhưng
nếu dùng mã hóa nhiều byte như Unicode, số byte cho mỗi ký tự có thể
khác nhau. Vì vậy, .NET cung cấp lớp Encoding để thực hiện chuyển đổi này.
Mỗi stream thường cung cấp một bộ nhớ đệm để hỗ trợ đọc ghi dữ liệu.
FileStream cũng như vậy. Khi ghi, dữ liệu được lưu tạm ở bộ nhớ đệm trước
khi thực sự ghi vào tập tin. Nếu muốn dữ liệu được đẩy ngay vào tập tin có
thể gọi phương thức Flush.
Trong suốt quá trình làm việc, tập tin sẽ bị khóa và object khác không thể
làm việc với tập tin này. Vì vậy, sau khi kết thúc làm việc với tập tin nên
gọi phương thức Close để đóng luồng và giải phóng tập tin.
Đọc từ tập tin qua FileStream
Hãy cùng xem ví dụ sau:
var fs = new FileStream("data1.bin", FileMode.OpenOrCreate,
FileAccess.Read);
var buffer = new byte[4];
fs.Read(buffer, 0, 4);
int i = BitConverter.ToInt32(buffer, 0);
326
Console.WriteLine($"i = {i}");
int length = (int)fs.Length - 4;
buffer = new byte[length];
fs.Read(buffer, 0, length);
string str = Encoding.UTF8.GetString(buffer);
fs.Close();
Console.WriteLine($"str = {str}");
Trong ví dụ này, chúng ta mở lại tập tin đã tạo lúc trước và đọc các giá trị
lưu ở trong đó, bao gồm một số nguyên và một chuỗi ký tự.
Để đọc ra một giá trị, chúng ta phải tạo ra một mảng đệm trước để luồng
tập tin đưa giá trị vào. Mảng đệm này phải có kích thước bằng hoặc lớn hơn
dữ liệu được đọc ra.
Với kiểu int, kích thước là cố định (4 byte); với kiểu string, do kích thước
không cố định nên ta phải tính toán ra kích thước của nó (bằng tổng số byte
trong tập tin trừ đi số byte mà biến int chiếm).
Sau khi đọc được dữ liệu vào mảng đệm, chúng ta sử dụng các phương thức
tương ứng của BitConverter và Encoding để chuyển đổi về kiểu dữ liệu
cần thiết.

Các vấn đề liên quan đến FileStream trong C#


Sử dụng stream adapter
Như ở trên chúng ta thấy, việc đọc ghi trực tiếp với FileStream trong C# rất
rắc rối, đặc biệt khi cần ghi/đọc những object phức tạp. Để giải quyết một
phần vấn đề này, bạn có thể sử dụng các lớp stream adapter.
Stream adapter đóng vai trò hỗ trợ sử dụng luồng backing store bằng cách
che đi các phương thức làm việc trực tiếp với byte và cung cấp thêm các
phương thức để xử lý dữ liệu cấp cao. Tùy thuộc vào kiểu dữ liệu cần làm
việc chúng ta lựa chọn các loại adapter khác nhau.
Hãy cùng xem ví dụ sau:
FileStream fs = new FileStream("data1.bin", FileMode.Create,
FileAccess.ReadWrite);
BinaryWriter bWriter = new BinaryWriter(fs);
bWriter.Write(1234);
StreamWriter sWriter = new StreamWriter(fs);
sWriter.Write("Hello world");
sWriter.Flush();
fs.Close();
fs = new FileStream("data1.bin", FileMode.OpenOrCreate, FileAccess.Read);
BinaryReader bReader = new BinaryReader(fs);
var i = bReader.ReadInt32();
StreamReader sReader = new StreamReader(fs);
var str = sReader.ReadToEnd();
Console.WriteLine($"i = {i}");
Console.WriteLine($"str = {str}");
fs.Close();

327
Trong ví dụ này chúng ta sử dụng hai loại adapter:
 BinaryWriter/BinaryReader để làm việc với các kiểu cơ sở (trừ kiểu
string)
 StreamWriter/StreamReader để làm việc với dữ liệu văn bản.
Khi sử dụng hai loại adapter này, việc đọc/ghi dữ liệu với FileStream được
đơn giản hóa rất nhiều vì các adapter đã đứng ra chịu trách nhiệm biến đổi
dữ liệu trong quá trình đọc/ghi. Các phương thức của hai loại adapter này
cũng rất giống với cách thức đọc/ghi dữ liệu từ giao diện console mà bạn đã
quen thuộc.
Một số phương thức “tắt”
Ngoài việc sử dụng các phương pháp “chính thống” như ở trên đã xem xét,
lớp File cũng cung cấp cho chúng ta nhiều phương thức “tắt” để đơn giản
hóa việc ghi/đọc dữ liệu với tập tin:
 File.WriteAllText
 File.ReadAllText
 File.WriteAllBytes
 File.ReadAllBytes
 File.WriteAllLines
 File.ReadAllLines
 File.OpenRead
 File.OpenWrite
 File.Create
Các phương thức này tuy rằng tiện lợi nhưng có thể làm mất một phần tính
hiệu quả của FileStream. Ví dụ, các lệnh đọc tắt này đọc toàn bộ dữ liệu vào
bộ nhớ, vốn rất không hiệu quả nếu tập tin lớn.
Sử dụng using block
Trong các ví dụ trên, sau khi kết thúc làm việc với tập tin, chúng ta phải tự
mình gọi lệnh đóng luồng tập tin. Đây là một thao tác rất hay bị bỏ quên.
Trong những tình huống khác, chúng ta chỉ cần sử dụng object trong một
khối code nhất định, sau đó object bị hủy bỏ hoặc không tiếp tục sử dụng
nữa. Để giải phóng người lập trình khỏi việc phải tự mình hủy bỏ các object
như vậy, C# cung cấp một cấu trúc mới: using block. Hãy cùng xem ví dụ
sau:
using (FileStream fs = new FileStream("data1.bin", FileMode.Create,
FileAccess.ReadWrite))
{
328
BinaryWriter bWriter = new BinaryWriter(fs);
bWriter.Write(1234);
StreamWriter sWriter = new StreamWriter(fs);
sWriter.Write("Hello world");
sWriter.Flush();
}
using (var fs = new FileStream("data1.bin", FileMode.OpenOrCreate,
FileAccess.Read))
{
BinaryReader bReader = new BinaryReader(fs);
var i = bReader.ReadInt32();
StreamReader sReader = new StreamReader(fs);
var str = sReader.ReadToEnd();
Console.WriteLine($"i = {i}");
Console.WriteLine($"str = {str}");
}
Ở hai đoạn code này chúng ta không cần tự mình đóng luồng nữa.
Biến fs được tạo ra trong cấu trúc using và được cấu trúc này theo dõi. Khi
kết thúc khối code, biến fs sẽ tự bị hủy bỏ. Cấu trúc này rất thường xuyên
được sử dụng khi làm việc với luồng.
Khi sử dụng các phương thức “tắt” như File.WriteAllText,
File.ReadAllText, File.WriteAllBytes, File.ReadAllBytes,
File.WriteAllLines, File.ReadAllLines, tập tin được mở và đóng tự
động. Chúng ta không cần tự mình thực hiện các thao tác làm việc với tập
tin thông thường nữa. Vì vậy các phương thức này được gọi là các phương
thức tắt.

329
Serialization trong C#: binary, xml, json serialization
Serialization trong C# là loại kỹ thuật chuyển đổi object về dạng trung gian
(text, mảng byte) phục vụ lưu trữ (trong tập tin) hoặc truyền qua mạng
(lập trình socket). Serialization là loại kỹ thuật nền tảng cho các công nghệ
phát triển ứng dụng mạng trên .NET Framework như ASP.NET Web API,
Windows Communications Foundation.
Bài học này sẽ cung cấp cho bạn những khái niệm và kỹ thuật cơ bản về
serialization trong C# nhằm hỗ trợ cho việc đọc và ghi dữ liệu từ tập tin.

Serialization trong C# là gì?


Khái niệm serialization
Khi tạo ra một object trong chương trình, .NET Framework, bạn hầu như
không cần quan tâm đến cách thức lưu trữ và quản lý dữ liệu của object đó
trong bộ nhớ vì .NET Framework thay bạn thực hiện các công việc này.
Tuy nhiên, nếu bạn muốn lưu trữ trạng thái của object đó (ví dụ, lưu vào
tập tin) để sau này có thể khôi phục lại nó, .NET Framework lại không thể
trực tiếp làm thay.
Quá trình chuyển đổi một object về dạng trung gian để lưu trữ hoặc truyền
thông như vậy được gọi là data serialization (tạm dịch là trình tự hóa dữ
liệu ); Quá trình khôi phục lại object từ dạng trung gian được gọi là
deserialization (tạm dịch là giải trình tự hóa).
Serialization có thể xem là giai đoạn chuẩn bị dữ liệu để ghi vào tập
tin hoặc truyền qua mạng. Cũng có thể coi serialization là tiền đề cho quá
trình lưu trữ trạng thái của một object trong một môi trường trung gian để
có thể khôi phục lại khi cần thiết.
Do môi trường trung gian (truyền thông hoặc lưu trữ) chủ yếu làm việc với
hai loại dữ liệu là văn bản và nhị phân (mảng byte), quá trình serialization
thực tế có thể xem là chuyển đổi object về một mảng byte, gọi là trình tự
hóa nhị phân (binary serialization), hoặc về một chuỗi văn bản, gọi là trình
tự hóa văn bản (text serialization).
Tuy nhiên, việc chuyển đổi này không thể tùy tiện mà phải đảm bảo thực
hiện được việc giải mã để khôi phục lại object từ dạng trung gian.
Serialization thường làm việc cùng với stream để ghi dữ liệu trực tiếp vào
luồng, tránh tình trạng phải lưu trữ những chuỗi hoặc mảng byte quá lớn
trong bộ nhớ. Tương tự, deserialization thường cũng đọc dữ liệu từ một
stream.

330
Hỗ trợ serialization trong C# và .NET Framework
Việc chuyển một object về chuỗi ký tự hoặc mảng byte là một công việc
tương đối phức tạp, tốn công sức và dễ sai sót, đặc biệt đối với các class lớn
có nhiều trường dữ liệu, cũng như khi phải làm việc với nhiều class khác
nhau.
Để hỗ trợ cho người lập trình, .NET Framework cung cấp các class hỗ trợ
cho 3 loại serialization: binary, xml và json.
 Lớp BinaryFormatter: biến đổi một object về mảng byte và ghi trực
tiếp vào một stream; đọc các byte dữ liệu từ một stream và biến đổi
về object. Lớp BinaryFormatter nằm trong không gian tên
System.Runtime.Serialization.Formatters.Binary.
 Lớp XmlSerializer: tương tự như BinaryFormatter, XmlSerializer
biến đổi một object về dạng xml và ghi vào một stream, cũng như
đọc một tập tin xml và biến đổi về object. Do làm việc với xml là một
dạng dữ liệu cấp cao, XmlSerializer cần đến hai lớp adapter
XmlReader và XmlWriter để làm việc với luồng tập tin.
Đối với định dạng json, mặc dù .NET Framework có class hỗ trợ nhưng không
thực sự tốt nên chúng ta sử dụng bộ thư viện NewtonSoft.Json. Chúng ta
đã cài đặt bộ thư viện này ở bài trước.

Binary serialization trong C#


Hãy cùng thực hiện một ví dụ để xem cách sử dụng của BinaryFormatter.
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace P01_Binary
{
[Serializable]
public class Student
{
public int Id { get; set; } = 1;
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public DateTime DateOfBirth { get; set; } = DateTime.Now;
}
class Program
{
static void Main(string[] args)
{
var student = new Student
{
Id = 1,
FirstName = "Nguyen Van",
LastName = "A",
DateOfBirth = new DateTime(1990, 12, 30)
};
331
Console.WriteLine("Original object:");
Print(student);
Save(student);
var nva = Load();
Console.WriteLine("Deserialized object:");
Print(nva);
Console.ReadKey();
}
static void Print(Student student)
{
Console.WriteLine($"Id: {student.Id}\r\nFirst Name:
{student.FirstName}\r\nLast Name:
{student.LastName}\r\nDate of birth:
{student.DateOfBirth.ToShortDateString()}");
}
static void Save(Student student)
{
using (var stream = File.OpenWrite("data.bin"))
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, student);
}
}
static Student Load()
{
Student student;
using (var stream = File.OpenRead("data.bin"))
{
var formatter = new BinaryFormatter();
student = formatter.Deserialize(stream) as Student;
}
return student;
}
}
}
Trong ví dụ này chúng ta tạo ra một class Student cho thông tin về sinh
viên. Trong lớp Program tạo ra 2 method: Save() để lưu thông tin sinh viên
vào tập tin nhị phân data.bin; Load() để đọc chuỗi byte từ tập tin data.bin
và chuyển đổi ngược lại thành object kiểu Student.
Để ý rằng, bên trên khai báo class Student có dòng [Serializable].
[Serializable] được gọi là attribute (thuộc tính). Attribute này cho phép
BinaryFormatter được truy xuất thông tin của object Student cho mục đích
serialization. Thiếu attribute này, ở giai đoạn runtime chương trình sẽ báo
lỗi System.Runtime.Serialization.SerializationException ‘Type is not marked
as serializable’.
Để sử dụng BinaryFormatter, bạn phải khởi tạo object của nó trước:
var formatter = new BinaryFormatter();

Lớp BinaryFormatter nằm trong namespace


System.Runtime.Serialization.Formatters.Binary.

332
Phương thức thành viên Serialize() nhận một luồng (stream) và một object
làm tham số. Phương thức này sẽ chuyển object thành chuỗi byte và ghi
vào luồng.
formatter.Serialize(stream, student);
Phương thức thành viên Deserialize() nhận một luồng làm tham số. Phương
thức này đọc các byte từ luồng và chuyển đổi thành một object. Bạn cần
cast object này về kiểu tương ứng.
student = formatter.Deserialize(stream) as Student;
Trong ví dụ trên, bạn đều sử dụng FileStream để đọc/ghi dữ liệu với tập tin
data.bin và đặt trong cấu trúc using. File này được tự động tạo ra trong thư
mục bin. Sau khi ghi vào tập tin, nó có nội dung như sau:

Xml serialization trong C#


Chúng ta thực hiện một ví dụ tương tự để minh họa cách làm việc với Xml
serialization.
using System;
using System.IO;
using System.Xml.Serialization;
namespace P02_Xml
{
public class Student
{
public int Id { get; set; } = 1;
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public DateTime DateOfBirth { get; set; } = DateTime.Now;
}
class Program
{
static void Main(string[] args)
{
var student = new Student
{
Id = 1,

333
FirstName = "Nguyen Van",
LastName = "A",
DateOfBirth = new DateTime(1990, 12, 30)
};
Console.WriteLine("Original object:");
Print(student);
Save(student);
var nva = Load();
Console.WriteLine("Deserialized object:");
Print(nva);
Console.ReadKey();
}
static void Print(Student student)
{
Console.WriteLine($"Id: {student.Id}\r\nFirst Name:
{student.FirstName}\r\nLast Name:
{student.LastName}\r\nDate of birth:
{student.DateOfBirth.ToShortDateString()}");
}
static void Save(Student student)
{
using (var stream = File.OpenWrite("data.xml"))
{
XmlSerializer serializer = new XmlSerializer(typeof(Student));
serializer.Serialize(stream, student);
}
}
static Student Load()
{
Student student;
using (var stream = File.OpenRead("data.xml"))
{
var serializer = new XmlSerializer(typeof(Student));
student = serializer.Deserialize(stream) as Student;
}
return student;
}
}
}
Ví dụ này lặp lại hoàn toàn logic và hầu hết code của ví dụ trên.
Sự khác biệt chỉ nằm ở cách sử dụng XmlSerializer:
// biến object về dạng xml và ghi vào stream (FileStream)
XmlSerializer serializer = new XmlSerializer(typeof(Student));
serializer.Serialize(stream, student);

// đọc tập tin xml và biến đổi về object


var serializer = new XmlSerializer(typeof(Student));
student = serializer.Deserialize(stream) as Student;

334
Xml serialization là một nền tảng để truyền dữ liệu trong ASP.NET, web API
và Windows Communications Foundation (WCF).

Json Serialization trong C#


Đối với định dạng JSON, mặc dù .NET Framework có class hỗ trợ nhưng
không thực sự tốt. Người ta thường dùng thư viện Json của NewtonSoft cho
nhiệm vụ này. Có thể download thư viện này từ NuGet theo các bước sau:
Bước 1. Mở giao diện quản lý các gói thư viện NuGet
Click phải vào References, chọn Manage NuGet Packages (xem hình dưới
đây).

Bước 2. Chọn cài gói thư viện


Trong ô tìm kiếm ở tab Browse gõ newtonsoft, chọn gói NewtonSoft.Json và
ấn Install.

335
Sau lệnh này, Visual Studio sẽ tải gói thư viện này về và cài đặt lên project
tương ứng.
Bạn có thể lặp lại hoàn toàn code ví dụ với xml hoặc binary serialization.
Trong đó thay code của các phương thức Save và Load như sau:
static void Save(Student student)
{
using (var stream = File.OpenWrite("data.xml"))
{
var writer = new StreamWriter(stream) { AutoFlush = true };
var serializer = new JsonSerializer();
serializer.Serialize(writer, student);
}
}
static Student Load()
{
Student student;
using (var stream = File.OpenRead("data.xml"))
{
var reader = new StreamReader(stream);
var serializer = new JsonSerializer();
student = serializer.Deserialize(reader, typeof(Student)) as Student;
}
return student;
}
Trong đó, lưu ý đặt lệnh using Newtonsoft.Json; ở đầu tập tin code.

336
Json serialization cũng là một nền tảng để truyền dữ liệu trong Asp.net web
API.

337
Pattern và Pattern matching (so khớp mẫu) trong C#
Pattern matching (so khớp mẫu) là một tính năng quen thuộc trong các
ngôn ngữ lập trình hàm như F#. Tuy nhiên, trong các ngôn ngữ lập trình
hướng đối tượng như C#, pattern matching không phải là một tính năng
phổ biến.
Pattern matching bắt đầu xuất hiện trong C# 7 nhưng có nhiều giới hạn.
C# 8 tiếp tục mở rộng khả năng của pattern matching. Tất cả những cải
tiến trên kéo C# về hướng lập trình hàm và giúp ngôn ngữ ngày càng đa
dạng, phong phú nhưng lại ngắn gọn súc tích hơn.

Khái niệm pattern và pattern matching trong C#


Pattern matching, tạm dịch là so khớp mẫu, là một khái niệm khá trừu tượng
và hơi khó hiểu. Thay vì giải thích dài dòng, chúng ta hãy cùng xem một ví
dụ đơn giản.
switch (car.Color)
{
case Color.Red:
Console.WriteLine("Color is red!");
break;
case Color.Blue:
Console.WriteLine("Color is blue!");
break;
default:
Console.WriteLine("Color is not red or blue!");
break;
}
Đây là một cấu trúc switch-case quen thuộc mà bạn làm quen ngay từ
những bài học C# đầu tiên. Và đây cũng là một ví dụ về pattern matching
trong C#!
Mỗi hằng của một case (như Color.Red, Color.Blue) chính là một pattern.
Nếu một biến có giá trị trùng với hằng của một case, chúng ta gọi trường
hợp đó là một match. Khi có một match, chương trình sẽ thực hiện một/một
số lệnh nào đó.
Hiểu theo cách đơn giản nhất:
 Pattern là một đặc điểm nào đó của dữ liệu. Pattern có thể là một giá
trị cụ thể cố định (hằng), cũng có thể là kiểu của dữ liệu, có thể là
một bộ phận của dữ liệu,.... Nhìn chung, tất cả nhưng gì của dữ liệu
giúp chúng ta phân biệt được nó với dữ liệu khác đều có thể sử dụng
làm pattern.

338
 Pattern matching là quá trình kiểm tra xem dữ liệu có những đặc điểm
chúng ta mong muốn hay không. Nếu có – sẽ thực hiện công việc gì
đó.
Với đặc điểm trên, pattern và pattern matching thường được thực thi trong
các cấu trúc điều khiển rẽ nhánh.
Cấu trúc switch-case cổ điển chính là cấu trúc vận dụng pattern matching,
và loại pattern trong cấu trúc này là constant pattern (giá trị cố định cụ
thể của dữ liệu). Đây là trường hợp đơn giản nhất của pattern matching
trong C#.
Với ý nghĩa trên, cấu trúc if-else cũng có thể xem là cấu trúc vận dụng
pattern matching.
Các phiên bản C# về sau đưa vào nhiều loại pattern mới. Những thay đổi
này giúp viết mã C# ngắn gọn súc tích và hiệu quả hơn:
 C# 7 đưa vào từ khóa is, when và cho phép sử dụng từ khóa var
trong biểu thức case.
 C# 8 bổ sung thêm các loại pattern mới (positional, property, tuple)
và switch expression.
Pattern matching cũng là một khái niệm trong lĩnh vực học máy (machine learning). Bạn cũng
gặp khái niệm này khi học về biểu thức chính quy (regular expression). Trong bài viết này
chúng ta đề cập tới pattern matching với vai trò một tính năng của ngôn ngữ lập trình.

Từ khóa is, so khớp kiểu


Khả năng so khớp kiểu xuất hiện trong C# 7. So khớp kiểu cho phép bạn
sử dụng kiểu của object làm pattern.
Bạn có thể thực hiện so khớp kiểu trong cấu trúc if-else với từ khóa is, hoặc
sử dụng kiểu làm pattern trực tiếp trong cấu trúc switch-case.
Hãy cùng xem một vài ví dụ nhỏ. Giả sử bạn định nghĩa 3 class mô tả các
hình hình học như sau:
class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
}
class Cirle
{
public int Radius { get; set; }
}
class Square
{
public int Length { get; set; }
}

339
Giờ bạn cần viết một phương thức tính diện tích của một hình bất kỳ: double
Area(object shape) { ... }
Ở đây phát sinh mấy vấn đề.
 Thứ nhất, các class trên hoàn toàn không có liên hệ gì.
 Để tính được diện tích, bạn cần xác định đó là hình gì.
Theo các kỹ thuật thông thường đã biết, bạn cần thử cast object shape về
các kiểu đã biết. Nếu cast thành công, shape chính là object của class đó:
var rectangle = shape as Rectangle;
if (rectangle != null) return rectangle.Width * rectangle.Height;

Khi sử dụng kỹ thuật này, bạn sẽ viết phương thức Area như sau:
private static double Area(object shape)
{
var rectangle = shape as Rectangle;
if (rectangle != null) return rectangle.Width * rectangle.Height;
var circle = shape as Cirle;
if (circle != null) return Math.PI * circle.Radius * circle.Radius;
return double.NaN;
}
Type matching của C# 7 cho phép bạn viết lại phương thức Area theo cách
sau:
private static double Area(object shape)
{
if (shape is Rectangle r) return r.Width * r.Height;
if (shape is Cirle c) return Math.PI * c.Radius * c.Radius;
if (shape is Square s) return s.Length * s.Length;
return double.NaN;
}
Như vậy cặp lệnh cast kiểu và kiểm tra null tương đương với một lệnh is
duy nhất
var rectangle = shape as Rectangle;
if (rectangle != null) ...
tương đương với
if (shape is Rectangle rectangle) ...
C# cho phép sử dụng type pattern với cấu trúc switch-case như sau:
private static double Area(object shape)
{
switch (shape)
{
case Rectangle r: return r.Width * r.Height;
case Cirle c: return Math.PI * c.Radius * c.Radius;
case Square s: return s.Length * s.Length;
default: return double.NaN;
}
}

340
Trong cấu trúc switch sử dụng type pattern matching đôi khi bạn muốn kiểm
tra thêm các điều kiện bổ sung. Lấy ví dụ, bạn có thể muốn tính diện tích
nếu kích thước các hình nằm trong khoảng từ 10 đến 100.
Type pattern cho phép sử dụng từ khóa when để đặt thêm các điều kiện bổ
sung. Hãy cùng xem ví dụ sau:
private static double AreaSwitchWhen(object shape)
{
switch (shape)
{
case Rectangle r: return r.Width * r.Height;
case Cirle c when c.Radius > 10 && c.Radius < 100: return Math.PI *
c.Radius * c.Radius;
case Square s when s.Length > 10 && s.Length < 100: return s.Length *
s.Length;
default: return double.NaN;
}
}
Từ khóa when cho phép bạn viết thêm các kiều kiện bổ sung khi so khớp
kiểu. Giờ đây bạn chỉ thực hiện tính diện tích hình tròn nếu đường kính hình
tròn nằm trong khoảng (10, 100). Tương tự như vậy khi tính diện tích hình
vuông.

Từ khóa var, so khớp biến


Giờ hãy xem một ví dụ khác:
private static string GreetingSwitch(string name)
{
switch (name)
{
case var n when n.ToLower().Contains("putin"):
return "Privet Vova!";
case string n when n.ToLower().Contains("trump"):
return "Hello, Mr. president";
case var n when n.Trim() == "":
return "Sorry, who are you?";
default:
return $"Hi, {name}";
}
}
Ví dụ này có điểm khác biệt: cụm var n when và string n when trong các
case. Hai cụm này có ý nghĩa: (1) hãy lấy giá trị của name (biến kiểm tra)
và gán vào n cho tôi, (2) kiểm tra điều kiện đi sau when.
Ở đây bạn không còn sử dụng type pattern nữa, và nó cũng không phải là
constant pattern truyền thống. Ở đây bạn đang sử dụng var pattern.
Trong var pattern, giá trị của biến kiểm tra được truyền vào cho từng case
để thực hiện các biến đổi và kiểm tra riêng rẽ.

341
Trong ví dụ này, hai case đầu tiên chúng ta chuyển sang chữ thường và
kiểm tra xem có chứa cụm “putin” hoặc “trump” không. Ở case thứ 3 chúng
ta xóa bỏ các ký tự trống rồi so với xâu rỗng.
Khi dùng từ khóa var như trên, giá trị của biến kiểm tra name sẽ được
truyền vào biến n của case. Từ đây bạn có thể thực hiện bất kỳ thao tác
biến đổi và kiểm tra nào với n, cũng chính là với name.
Nếu không muốn dùng var, bạn có thể chỉ định trực tiếp kiểu dữ liệu của
biến kiểm tra trong mỗi case. Như trong case thứ hai, do biến name có kiểu
string, biến n trong case có thể được chỉ định kiểu trực tiếp là string (vì n
chính là name).
Nếu trong cấu trúc switch thông thường trước đây bạn không thể thực hiện
những biến đổi riêng rẽ và kiểm tra với từng case như vậy.
So khớp kiểu và so khớp biến giúp cấu trúc switch-case trở nên rất mạnh
mẽ. Tuy nhiên nó vẫn còn cách xa với khả năng của các ngôn ngữ lập trình
hàm.
C# 8 tiếp tục đưa vào một biểu thức mới giúp C# tiến gần hơn nữa với lập
trình hàm: switch expression.

Switch expression
Ở các phần trên chúng ta đều nói về cấu trúc điều khiển switch-case. Các
cấu trúc điều khiển trong C# đều là các lệnh (statement).
Ngôn ngữ C# phân biệt lệnh (statement) với biểu thức (expression). Lệnh
không trả về kết quả. Biểu thức trả về kết quả.
Các cấu trúc if-else, switch-case đều là các lệnh. Chúng không trả về kết
quả. Tuy nhiên biểu thức điều kiện a ? b : c lại là một biểu thức vì nó trả
lại kết quả (giá trị b hoặc c) tùy thuộc vào điều kiện a.
C# 8 đưa vào một loại biểu thức mới: biểu thức switch. Đây là cấu trúc rẽ
nhiều nhánh (như switch) nhưng trả lại kết quả.
Hãy cùng xem một số ví dụ về switch expression.
private static string Greeting(string name)
{
var greeting = name switch
{
"Putin" => "Privet Vova!",
"Elizabeth" => "Your Majesty!",
"Trump" => "Hello, Mr. president!",
_ => $"Hi, {name}!"
};
return greeting;
}

342
Trong ví dụ trên bạn đã sử dụng một biểu thức switch để ánh xạ đầu vào
(name) thành một biến đầu ra (greeting). Tùy vào giá trị đầu vào, giá trị
đầu ra sẽ khác nhau.
Bạn có thể thấy trong biểu thức switch không có các nhánh “case”. Thay
vào đó là các cặp pattern => giá trị. Mỗi cặp này được gọi là một arm.
Mỗi arm là một trường hợp đặc biệt trong quá trình ánh xạ.
Trong ví dụ nhỏ trên chúng ta đã sử dụng constant pattern quen thuộc.
Riêng arm cuối cùng, _ => $"Hi, {name}!", được gọi là discard pattern.
Nó hoạt động giống như default case trong lệnh switch thông thường.
Hãy xem một ví dụ khác:
private static double Area(object shape)
{
var area = shape switch
{
Rectangle r => r.Width* r.Height,
Cirle c => Math.PI* c.Radius* c.Radius,
Square s => s.Length* s.Length,
_ => double.NaN
};
return area;
}
Ở đây chúng ta gặp lại type pattern. Ví dụ trên minh họa cách tính diện tích
một object mà chúng ta không xác định được từ trước. Thay vì truyền một
object của một hình cụ thể, chúng ta truyền object thuộc kiểu chung nhất,
kiểu object.
Cấu trúc switch expression sẽ so khớp với từng kiểu đã biết để tính diện
tích. Nếu object thuộc về một kiểu khác biệt với 3 loại hình chúng ta đã biết
thì sẽ trả về giá trị không xác định double.NaN.
Trong switch expression bạn có thể sử dụng tất cả các loại pattern đã biết trong C#.
Ngoài ra, C# 8 đưa thêm vào một số pattern mới để sử dụng cùng với switch
expression:
 Positional pattern
 Property pattern
 Tuple pattern

Property pattern
Hãy cùng xem một ví dụ:
private static string Position(Point point)
{
return point switch
{
{ X: 0, Y: 0 } => "At the origin",

343
{ X: _, Y: 0 } => "On the X axis",
{ X: 0, Y: _ } => "On the Y axis",
{ X: var x, Y: var y } => $"({x} {y})",
_ => "Somewhere"
};
}
Trong đó class Point được định nghĩa như sau:
class Point
{
public double X { get; set; }
public double Y { get; set; }
}
Trong ví dụ này, phương thức Position nhận một object kiểu Point. Tùy thuộc
vào giá trị của tọa độ X và Y sẽ trả lại những thông báo khác nhau. Nếu X
= 0, Y = 0 thì báo “nằm ở gốc tọa độ”; Nếu Y = 0 thì báo “nằm trên trục X;
Nếu X = 0 thì báo “nằm trên trục Y; Trong những trường hợp còn lại thì in
ra tọa độ ở dạng (X, Y).
Đây là một ví dụ về cách sử dụng property pattern trong C# 8.
Trong property pattern, đặc điểm nhận dạng của mỗi pattern chính là danh
sách giá trị của các public property của object.
Trong ví dụ trên, mỗi object Point có hai publich property X và Y. Các tổ hợp
khác nhau của X và Y có thể dùng để phân biệt các object khác nhau của
Point. Ví dụ, X = 0 và Y = 0; X = 0 và Y bất kỳ; X bất kỳ và Y = 0; X bất
kỳ và Y bất kỳ.
Để biểu diễn property pattern, bạn sử dụng cặp dấu {}. Trong cặp dấu này
chứa các tổ hợp tên của property và giá trị của nó phân tách nhau bởi dấu
hai chấm.
Giá trị của property có thể là hằng số hoặc biến số.
 Trường hợp là hằng số, bạn đặt thẳng hằng số sau dấu hai chấm, ví
dụ X:0, Y:0.
 Nếu giá trị là biến số (để về sau sử dụng trong biểu thức), bạn đặt
tên biến cùng từ khóa var, ví dụ X: var x, Y: var y.
 Nếu không quan tâm đến giá trị, bạn dùng ký tự discard _. Giá trị
discard có nghĩa là bạn không quan tâm giá trị đó bằng bao nhiêu, và
bạn cũng không có ý định sử dụng nó.

Tuple pattern
Tuple pattern là loại pattern dựa trên sử dụng một kiểu dữ liệu đặc biệt của
C#: tuple.
Tuple là kiểu dữ liệu kết hợp nhiều dữ liệu theo thứ tự. Ví dụ, (string, string,
int) là một tuple với 3 giá trị theo thứ tự lần lượt là string, string và int.
344
Các giá trị thành viên của tuple có thể được đặt tên. Ví dụ, (string fname,
string lname, int age). Tuple này và (string, string, int) là tương đương
nhau.
Nếu không đặt tên, các thành viên của tuple sẽ được tự động đặt tên là
Item1, Item2,....
Hãy cùng xem ví dụ sau:
private static string Position(int x, int y)
{
return (x, y) switch
{
(0, 0) => "At the origin",
(_, 0) => "On the X axis",
(0, _) => "On the Y axis",
(var a, var b) => $"({a} {b})"
};
}
Đây là cách dùng switch expression để viết ra vị trí của một điểm dựa trên
tọa độ. Thay vì sử dụng lớp Point như trước, giờ chúng ta sử dụng tuple (int,
int) để mô tả tọa độ.
(x, y) là một biến tuple có kiểu (int, int) và là biến đầu vào cho switch
expression.
Trong switch expression, mỗi tổ hợp giá trị của tuple trở thành đặc điểm
nhận dạng của nó.
Ví dụ tuple (bool, bool) có thể tạo ra các tổ hợp giá trị (true, true), (true,
false), (false, true), (false, false) phân biệt nhau. Tuple (int, int) có thể tạo
ra vô số tổ hợp giá trị phân biệt nhau.
Tương tự như đối với property pattern, mỗi giá trị trong tuple có thể là hằng
số, là ký tự discard _, hoặc là biến. Chúng ta đã thấy cách sử dụng cả ba
loại giá trị này trong tuple ở ví dụ trên.
Trong bài học này chúng ta đã xem xét chi tiết vấn đề sử dụng pattern và
pattern matching trong cấu trúc điều khiển switch-case và biểu thức switch
của C#.
Pattern matching giúp viết code ngắn gọn, súc tích và dễ đọc hơn rất nhiều.
Các phiên bản sau này của C# hỗ trợ pattern ngày càng tốt hơn trong xu
hướng chuyển dịch lại gần lập trình hàm.

345
Đề tài dự án và phân tích bài toán cho phát triển ứng
dụng
Trong bài này, chúng ta sẽ đưa ra đề tài cho dự án và tiến hành phân tích
nhằm làm rõ các yêu cầu về chức năng và dữ liệu của phần mềm. Các phân
tích ở đây sẽ được sử dụng làm cơ sở cho việc phát triển ứng dụng trong
suốt quá trình học.

Các vấn đề thường gặp khi học lập trình C#


Khi học lập trình cơ bản, rất nhiều người thường gặp những vấn đề có liên
quan trực tiếp đến việc vận dụng kiến thức và kỹ năng đã được học để giải
quyết một bài toán trọn vẹn. Qua kinh nghiệm giảng dạy thực tế, chúng tôi
nhận thấy một số vấn đề mà người học thường gặp phải.
Không biết cách vận dụng và kết hợp các kỹ thuật đã biết khi giải quyết
bài toán
Người học chủ yếu thường được giới thiệu các khái niệm và kỹ thuật lập
trình thông qua các ví dụ nhỏ tách rời. Cách thức này có hiệu quả để người
học hiểu riêng những vấn đề đang được trình bày.
Tuy nhiên, khi gặp các tính huống thực tế, người học thường không biết
cách áp dụng các kỹ thuật mình đã biết, đặc biệt là cách phối hợp các kỹ
thuật để giải quyết vấn đề đang gặp phải.
Với cách tiếp cận của bài giảng này, người học bên cạnh việc tiếp thu những
khái niệm và kỹ thuật mới sẽ được chỉ dẫn cách vận dụng của chúng để giải
quyết những vấn đề gặp phải trong một dự án nhỏ.
Mắc lỗi trong quá trình xây dựng và sử dụng class
Ví dụ, người học lập trình hướng đối tượng mặc dù hiểu được rõ ràng các
khái niệm cơ bản như lớp, đối tượng, kế thừa, đa hình, nạp chồng, ghi đè,...
cũng như kỹ thuật thực hiện chúng trong một ngôn ngữ lập trình cụ thể.
Tuy nhiên, đến lúc lập trình lại thường xuyên mắc những lỗi như trộn lẫn
code xử lý giao diện với code xử lý logic hay dữ liệu, đưa quá nhiều code
không liên quan vào một class, lạm dụng trường dữ liệu và phương thức
tĩnh,....
Những lỗi này xuất hiện là do người học thường không được giới thiệu về
những nguyên lý cơ bản trong vận dụng lập trình hướng đối tượng, ví dụ bộ
nguyên lý SOLID, các mẫu kiến trúc cho giao diện (MVC, MVP), các mẫu
thiết kế cho dữ liệu (repository, unit of work), các mẫu thiết kế class

346
(singleton, mediator,...), các nguyên lý chung phổ biến (separation of
concern, inversion of control,...).
Nói một cách khác, đó là học lập trình hướng đối tượng nhưng chưa biết
cách vận dụng của nó (học không đi đôi với hành).
Bài giảng này không hướng tới cung cấp cho người học những vấn đề chuyên
sâu về cách vận dụng của các nguyên lý trên mà cố gắng vận dụng những
nguyên lý trên ở những nơi phù hợp (cùng với giải thích nguyên lý) giúp
người học lưu ý và bắt đầu ý thức được việc vận dụng chúng trong quá trình
lập trình ứng dụng.
Không biết cách tổ chức code của chương trình
Với cách học từ những ví dụ nhỏ, code thường rất ít và được gộp trong một
vài tập tin mã nguồn. Từ đây dẫn đến những thói quen xấu trong việc tổ
chức code và có hại về lâu dài. Ví dụ, người học không có thói quen tách
code ra các các tập tin và thư mục (và khi muốn tách thì không biết nên
tách những gì), không viết ghi chú (comment) cho các đoạn code, đặt tên
tập tin mã nguồn bất quy tắc,....
Những thói quen này làm cho việc bảo trì code trở nên khó khăn hơn vì chỉ
sau vài tháng thường rất khó nhớ được chi tiết những gì mình làm, khó tìm
lại code theo nhu cầu, vất vả khi đọc lại code kể cả khi đọc code do chính
mình viết ra.
Khi học qua một dự án với khối lượng code đủ lớn, tất cả những vấn đề nêu
trên sẽ xuất hiện rất nhanh và người học bắt buộc phải vận dụng những
cách thức tổ chức code phù hợp.
Không biết bắt đầu từ đâu
Khi được giao xử lý một vấn đề, người học thường không biết bắt đầu từ
đâu. Thông thường, người học thường suy nghĩ sơ lược về chức năng
(chương trình sẽ làm gì), tưởng tượng ngay ra giao diện (chương trình nhìn
như thế nào) và bắt tay thẳng vào code. Trong quá trình code sẽ tiếp tục
suy nghĩ chương trình sẽ làm gì tiếp theo, và liên tục sửa code theo suy
nghĩ của mình.
Thói quen xấu này làm việc giải quyết bài toán mất nhiều công sức hơn, kết
quả thu được thường khá lộn xộn và ít có khả năng cải tiến tiếp.
Phần thực hành làm project trong bài giảng này sẽ cố gắng giúp bạn biết
cách bắt đầu giải quyết một bài toán mà không sa đà lạc lối ngay vào code.

347
Đề tài dự án
Trong thời gian học đại học, tôi thường có thói quen sưu tầm sách điện tử,
chủ yếu là các tập tin e-book ở dạng .pdf, .epub, .djvu, .chm, và thuộc
nhiều thể loại khác nhau (sách học tiếng Anh, truyện, sách thuộc các lĩnh
vực chuyên môn của cá nhân,...).
Qua thời gian, số lượng sách sưu tầm được lên đến khoảng 30 Gb (với hàng
nghìn tập tin), và số lượng này vẫn tiếp tục tăng lên. Số sách này được lưu
trữ trên ổ đĩa cứng và có một bản backup trên dịch vụ OneDrive.
Vì tôi luôn phải cập nhật kiến thức và công nghệ mới, số lượng sách và
chủng loại sách vẫn ngày một tăng lên khiến việc tìm kiếm (để đọc lại hoặc
cung cấp cho người khác) những cuốn mình cần trong kho sách ngày một
khó hơn.
Cũng có nhiều cuốn sách về các chủ đề hoặc công nghệ đã cũ giờ không sử
dụng đến nữa cần loại bỏ cho nhẹ bớt kho (và tiết kiệm tài nguyên của
OneDrive).
Ngoài ra, do sưu tầm sách từ nhiều nguồn khác nhau, cách đặt tên cho các
cuốn sách cũng rất lộn xộn khiến việc tìm kiếm sách rất mất công (phải mở
thử từng tập tin một để kiểm tra). Vì vậy, tôi có nhu cầu phát triển một ứng
dụng nhỏ giúp tôi quản lý được kho tài liệu trên dễ dàng hơn.
Khi bạn được giao thực hiện một bài toán phát triển ứng dụng nào đó, thông thường yêu cầu
đưa ra thường rất chung chung, vì chính người đưa ra bài toán có khi vẫn chưa hiểu thấu đáo
bài toán của mình.
Vì vậy, hãy tự mình suy nghĩ (theo kinh nghiệm của bản thân)/tìm kiếm trên Internet/trao đổi
với chuyên gia về các vấn đề có liên quan để hiểu rõ về vấn đề (cái này người ta gọi là tìm hiểu
nghiệp vụ).
Khi đã hiểu được nghiệp vụ, hãy quay lại trao đổi với người đưa ra bài toán để cả hai cùng làm
rõ và thống nhất từng vấn đề.
Trong bài toán này, nghiệp vụ rất đơn giản, các yêu cầu cũng đã tương đối rõ ràng nên mọi
chuyện đơn giản hơn. Nếu chưa hiểu gì về bài toán thì đừng vội nghĩ về code hay giao diện.
Phân tích tổng quát
Tuy rằng đây là một bài toán về phát triển ứng dụng nhỏ và đơn giản, hàng
loạt vấn đề vẫn cần được làm rõ. Các vấn đề này có thể chia làm ba loại:
các tình huống (ca) sử dụng, thông tin cần lưu trữ/xử lý, và quy trình làm
việc trong từng tình huống sử dụng. Tất cả các vấn đề này liên quan đến
quy trình nghiệp vụ, chưa phải là lập trình và cần thực hiện kỹ càng.

348
Các tình huống sử dụng (use case)
Về tình huống sử dụng, chúng ta cần xác định được những ai sẽ sử dụng
phần mềm và từng nhóm người dùng đó có thể làm được những gì trên
phần mềm. Đây thông thường cũng là bước đầu tiên trong phát triển ứng
dụng. Việc phân tích này cho chúng ta cái nhìn tổng thể về phần mềm tương
lai và chi phối các bước phân tích tiếp theo.
Cụ thể, phần mềm này sẽ chỉ dành một người sử dụng và có thể:
1. Tìm kiếm và liệt kê sách theo tiêu chí
2. Xem thông tin chi tiết
3. Bổ sung thêm sách mới
4. Cập nhật thông tin sách
5. Loại bỏ sách khỏi kho
6. Tìm kiếm và sắp xếp sách theo tiêu chí
7. Thống kê sách theo tiêu chí
8. Xuất thông tin (sau khi tìm kiếm/thống kê) ra tập tin
9. Mở đọc tập tin sách
10. Tự động tìm sách trong thư mục
11. Đồng bộ hóa thông tin quản lý với các tập tin sách thực có trong thư
mục
12. Sao chép một/một số tập tin sách sang vị trí khác
13. Đổi tên tập tin theo quy tắc
14. Mở thư mục chứa tập tin sách
Đừng lo lắng nếu bạn chưa hiểu hết các tình huống sử dụng trên, vì chúng ta còn một bước phân
tích chi tiết nữa.
Thông tin cần quản lý
Về thông tin, câu hỏi đặt ra là để quản lý một kho sách điện tử, chúng ta
cần những thông tin gì. Một cuốn sách (dù là sách in hay sách điện tử) đều
có thông tin sau:
 tác giả (hoặc nhóm tác giả);
 tựa đề;
 nhà xuất bản;
 năm xuất bản;
 lần tái bản;

349
 mã ISBN (mã số tiêu chuẩn quốc tế cho sách).
Ngoài ra, đối với sách điện tử có thể có thêm những thông tin như:
 mô tả tóm tắt nội dung (rất tiện lợi cho việc tìm kiếm);
 từ khóa mô tả nội dung/thể loại, đánh giá của cá nhân (rating, sau
này có thể dùng trong sắp xếp);
 đánh dấu cuốn sách nào hiện đang đọc (để sau dễ dàng tìm đến những
cuốn được đánh dấu).
Vì số lượng sách lớn và thuộc nhiều thể loại, cần có thêm một thông tin nữa,
tạm gọi là “giá sách” để giúp phân loại sách, tương tự như việc mỗi tập tin
sách nằm trong một thư mục nào đó. Mỗi giá sách chứa nhiều cuốn sách;
mỗi cuốn sách chỉ nằm trong một giá nào đó. Giá sách giúp gom các cuốn
sách cùng loại lại để dễ dàng quản lý, tương tự như việc dùng thư mục để
quản lý tập tin.
Để có thể thực hiện quản lý, mỗi thông tin sách (dữ liệu) sẽ phải tương ứng
với một tập tin sách trong thư mục kho sách. Vì vậy, cần thêm một thông
tin về đường dẫn tới tập tin sách tương ứng. Vì mỗi dữ liệu sách tương ứng
với một tập tin sách, mỗi giá sách tương ứng với một thư mục chứa tập tin
sách, toàn bộ dữ liệu sách sẽ tương ứng và đồng bộ với thư mục kho sách.
Về mặt kỹ thuật, để có thể nhanh chóng xác định đúng cuốn sách đang cần
tìm, chúng ta thêm một thông tin định danh duy nhất cho mỗi cuốn sách,
gọi là Id.
Tổng kết lại, dữ liệu về mỗi cuốn sách bao gồm những trường sau:

STT Tên Mô tả Kiểu Ghi chú

1 Id Số định danh duy nhất Số nguyên Bắt buộc

2 Nhóm tác giả Danh sách tên tác giả, Văn bản Bắt buộc
(authors) phân tách bởi dấu
phẩy

3 Tiêu đề (title) Tiêu đề sách Văn bản Bắt buộc

4 Nhà xuất bản Tên nhà xuất bản Văn bản Bắt buộc
(publisher)

5 Năm xuất bản Năm xuất bản sách Số nguyên Bắt buộc, phải lớn hơn
(year) 1950, nhỏ hơn năm
hiện tại

350
6 Lần tái bản Lần tái bản của sách Số nguyên Bắt buộc, phải lớn hơn
(edition) hoặc bằng 1, mặc định
là 1

7 Mã xuất bản Mã số tiêu chuẩn quốc Văn bản Không bắt buộc
(Isbn) tế

8 Từ khóa (tags) Danh sách các từ khóa Văn bản Không bắt buộc
mô tả nội dung, thể
loại

9 Mô tả Mô tả tóm tắt nội Văn bản Không bắt buộc


(description) dung

10 Đường dẫn (tập Đường dẫn (đầy đủ) Văn bản Bắt buộc, phải là một
tin) tới tập tin pdf đường dẫn đúng

11 Đánh dấu đọc Dùng để đánh dấu Logic Không bắt buộc, mặc
(reading) một cuốn sách đang định là false
đọc

12 Đánh giá (rating) Đánh giá chất lượng Số nguyên Không bắt buộc, có giá
cuốn sách trị từ 1 (dở nhất) đến 5
(tốt nhất), mặc định là
1

Việc phân tích về mặt dữ liệu giúp chúng ta sau này xây dựng lớp thực thể.

Phân tích chi tiết


Như ở phần phân tích về tình huống sử dụng chúng ta thấy, nếu chỉ mô tả
sơ lược như vậy thì rất khó hình dung về hoạt động cụ thể của chúng. Do
đó, mỗi tình huống sử dụng thường được mô tả thêm một cách kỹ lưỡng
(gọi là đặc tả) bằng lời hoặc các sơ đồ trực quan để diễn tả đầy đủ quy trình,
thuật toán của từng tình huống sử dụng.
Ở đây chúng ta chỉ mô tả bằng lời vì các tình huống sử dụng trên không
phức tạp. Mỗi tình huống sử dụng sẽ được phân tích kỹ hơn ở phần dưới
đây.

# Tình huống sử dụng Mô tả chi tiết

1 Tìm kiếm và liệt kê Người dùng có thể liệt kê tất cả các cuốn sách đang được quản
sách theo tiêu chí lý theo một tiêu chí cụ thể. Nếu nhập một từ khóa, chương
trình phải tìm và liệt kê ra tất cả các cuốn sách mà tên tác giả,
nhà xuất bản, mô tả, tag, tiêu đề chứa từ khóa đó. Nếu không
nhập từ khóa nào thì sẽ liệt kê tất cả các cuốn sách. Nếu không

351
# Tình huống sử dụng Mô tả chi tiết
tìm thấy cuốn sách nào phù hợp tiêu chí hoặc kho sách đang
trống sẽ báo lại cho người dùng biết.

2 Xem thông tin chi tiết Người dùng cung cấp Id của cuốn sách, chương trình sẽ hiển
thị toàn bộ thông tin chi tiết của cuốn sách đó. Nếu không tìm
thấy cuốn sách có Id tương ứng thì báo lỗi.

3 Bổ sung thêm sách Khi cần thêm một cuốn sách mới vào kho, người dùng copy
mới tập tin vào nơi cần thiết và cung cấp đầy đủ thông tin về cuốn
sách. Chương trình sẽ bổ sung dữ liệu về cuốn sách mới và
lưu vào tập tin dữ liệu.

4 Cập nhật thông tin Người dùng cung cấp Id của cuốn sách, chương trình sẽ lần
sách lượt hiển thị từng trường dữ liệu (cũ) và yêu cầu nhập giá trị
mới. Nếu trường nào không cần cập nhật thì bỏ qua. Nếu
không tìm thấy cuốn sách thì báo lỗi.

5 Loại bỏ sách khỏi Người dùng cung cấp Id của cuốn sách. Chương trình tìm
kho cuốn sách có Id tương ứng. Nếu tìm thấy sẽ xóa xóa bỏ dữ
liệu của cuốn sách nhưng không xóa bỏ tập tin tương ứng.
Nếu không tìm thấy sẽ báo lỗi. Người dùng cũng có thể yêu
cầu xóa bỏ toàn bộ dữ liệu sách (nhưng không xóa các tập tin
tương ứng). Trong cả hai tình huống đều yêu cầu người dùng
phải xác nhận hành động trước khi thực hiện.

6 Tìm kiếm và sắp xếp Tương tự như tình huống số 1 nhưng có thêm khả năng sắp
sách theo tiêu chí xếp theo tiêu chí do người dùng cung cấp, bao gồm sắp xếp
theo tên tác giả, tiêu đề, nhà xuất bản, năm xuất bản.

7 Thống kê sách theo Người dùng cung cấp một tiêu chí để thống kê. Chương trình
tiêu chí hiển thị được danh sách toàn bộ các cuốn sách theo nhóm. Các
tiêu chí để nhóm bao gồm: thư mục, tác giả, nhà xuất bản,
năm xuất bản.

8 Xuất thông tin (sau Tình huống 1, 2, 6, 7 đều xuất thông tin ra màn hình. Để người
khi tìm kiếm/thống dùng có thể dễ dàng sử dụng thông tin đó trong các ứng dụng
kê) ra tập tin khác, ví dụ nhập vào excel, các thông tin này có thể đồng thời
phải xuất ra một tập tin dữ liệu riêng theo các định dạng phổ
biến như xml, json. Đây là chức năng bổ sung cho 1,2,6,7.
Người dùng có thể lựa chọn xuất thông tin ra màn hình hoặc
xuất vào tập tin.

9 Mở đọc tập tin sách Chức năng này cho phép người dùng cung cấp thông tin để
tìm ra một cuốn sách cụ thể và mở tập tin bằng chương trình
đọc mặc định của hệ thống. Chức năng này hoạt động theo

352
# Tình huống sử dụng Mô tả chi tiết
quy trình như tình huống 2 nhưng bổ sung thêm khả năng mở
tập tin để đọc.

10 Tự động tìm sách Người dùng cung cấp đường dẫn tới một thư mục, chương
trong thư mục trình tự dò tìm các tập tin sách có phần mở rộng pdf trong thư
mục đó (và thư mục con của nó). Ứng với mỗi tập tin, chương
trình tạo ra một dữ liệu sách. Chức năng này được sử dụng để
tự động tạo dữ liệu cho chương trình, hạn chế phải tạo dữ liệu
thủ công. Người dùng về sau chỉ cần dùng chức năng cập nhật
để điều chỉnh thông tin của sách. Nếu đường dẫn tới thư mục
nguồn sai sẽ báo lỗi. Nếu không có tập tin sách nào tìm thấy
sẽ thông báo lại. Nếu chức năng chạy tốt sẽ thông báo số tập
tin tìm được.

11 Đồng bộ hóa thông Khi tải được cuốn sách mới, người dùng có thể không có thời
tin quản lý với các gian để bổ sung ngay thông tin cho chương trình quản lý mà
tập tin sách thực có chỉ trực tiếp copy tập tin sách vào thư mục. Người dùng cũng
trong thư mục có thể tự xóa bớt các tập tin sách cũ. Chức năng này giúp tự
động phát hiện những cuốn sách mới trong các thư mục. Nếu
có tập tin mới, chương trình tự động tạo ra dữ liệu tương ứng.
Nếu tập tin sách đã bị xóa, dữ liệu tương ứng của nó trong
chương trình sẽ bị xóa bỏ.

12 Sao chép một/một số Khi người dùng muốn copy sách cho người khác, thay vì phải
tập tin sách sang vị trí mở thư mục và copy từng tập tin, chương trình giúp copy tất
khác cả các tập tin được lựa chọn tới một thư mục đích. Có hai tình
huống bên trong: – Người dùng cung cấp Id và thư mục đích.
Nếu tìm thấy cuốn sách và thư mục đích chính xác, chương
trình sẽ copy tập tin sách tương ứng vào đó. Nếu không tìm
thấy tập tin sách hoặc thư mục đích sẽ báo lỗi. – Người dùng
cung cấp một tiêu chí tìm kiếm (tương tự tình huống 2) và thư
mục đích. Chương trình tìm tất cả sách đáp ứng tiêu chí và
copy vào thư mục đích. Nếu không tìm tập tin sách nào phù
hợp hoặc không thấy thư mục đích sẽ báo lỗi.

13 Đổi tên tập tin theo Người dùng lưu trữ quy tắc đặt tên tập tin trong thông tin cấu
quy tắc hình của chương trình. Chương trình sử dụng quy tắc này để
đặt lại tên cho toàn bộ sách có trong kho dựa trên dữ liệu sách
đang có. Quy tắc này không được gán cố định mà phải do
người dùng cuối thiết lập. Ví dụ quy tắc đặt tên: [<tên tác
giả>] <tên sách>, <nhà xuất bản>, -<năm xuất bản, <lần tái
bản>.pdf

14 Mở thư mục chứa tập Khi cuốn sách có các tập tin đi kèm (như mã nguồn, video),
tin sách chức năng này cho phép nhanh chóng mở thư mục chứa tập
tin sách. Người dùng cung cấp Id của sách. Nếu tìm thấy dữ

353
# Tình huống sử dụng Mô tả chi tiết
liệu sách và đường dẫn tới tập tin chính xác, chương trình sẽ
mở ra thư mục chứa tập tin. Nếu không tìm thấy dữ liệu hoặc
đường dẫn sai sẽ báo lỗi.

Mặc dù ở trên chúng ta phân tích bài toán tương đối chi tiết và đưa ra nhiều
tình huống sử dụng khác nhau, thực tế không phải lúc nào cũng có thể thực
hiện ngay được tất cả các yêu cầu đó khi phát triển ứng dụng.
Có thể ở những phiên bản đầu chỉ thực hiện được những yêu cầu chính, sau
đó mới lần lượt bổ sung. Vì vậy, khi phát triển ứng dụng cần vận dụng thiết
kế phù hợp để có thể liên tục thực hiện bổ sung các yêu cầu mới một cách
dễ dàng mà không ảnh hưởng đến code đã có.
Ngoài ra, nếu làm việc theo nhóm cũng đặt ra những yêu cầu đối với thiết
kế để có thể phát triển song song các thành phần của ứng dụng.
Trong quá trình phát triển ứng dụng trong tập bài giảng này sẽ đề cập đến
tất các vấn đề trên. Chúng ta cũng không thực hiện tất cả các yêu cầu đưa
ra trong phần phân tích mà để dành lại một phần cho người học tự hoàn
thiện.

354
Tạo cấu trúc quản lý mã nguồn cho dự án
Bài thực hành này sẽ trình bày sơ lược về cách thức tổ chức quản lý mã
nguồn theo mô hình MVC, sau đó sẽ tạo project chính thức sử dụng trong
dự án với các tập tin và thư mục theo yêu cầu của MVC.

Tạo project
Chuẩn bị: tạo solution BOOKMAN
Tạo blank solution BOOKMAN như sau:

355
Thêm project “BookMan.ConsoleApp”
Tạo thêm một project thuộc loại ConsoleApp trong solution “BOOKMAN”:
1. Click phải vào tên Solution, chọn Add => New Project …
2. Trong hộp thoại “Add New Project” chọn loại project là “Console App
(.NET Framework);
3. Trong hộp văn bản “Name” nhập “BookMan.ConsoleApp”;
4. Ấn nút “Add” hoặc phím “Enter” để hoàn thành.

356
Kết quả thu được như sau:

Có thể để ý thấy rằng, tên của project “BookMan.ConsoleApp” được tô màu


đậm, vì Visual Studio đang đặt nó làm project khởi động. Project khởi động
(StartUp Project) là project được chạy mặc định (khi ấn F5 hoặc Ctrl+F5).
Nếu trong solution có những project khác, bạn có thể thiết lập project khởi động bằng cách
Click phải chuột vào project tương ứng và chọn “Set as StartUp Project”. Tên project khởi động
trong Solution được Visual Studio hiển thị bằng chữ đậm.

357
Tạo thư mục trong project
Click phải vào tên project => Add => New Folder

Trong project xuất hiện một thư mục mới “NewFolder”. Đổi tên thư mục này
thành Models.
Nếu thực hiện sai, có thể đổi tên thư mục, hoặc xóa bỏ thư mục bằng cách
click phải chuột vào tên thư mục và chọn lệnh tương ứng (Rename, Delete).
Bạn cũng có thể dùng phím tắt F2 để đổi tên thư mục.
Lặp lại bước trên để lần lượt tạo ra các thư mục Views, Controllers,
Framework.
Bạn thu được cấu trúc project và cấu trúc thư mục tương ứng trên ổ cứng
như sau:

358
Cấu trúc mã nguồn theo mô hình MVC
Người mới học lập trình hướng đối tượng thường có xu hướng trộn lẫn code
xử lý dữ liệu với code xử lý giao diện.
Ví dụ, khi tạo lớp ma trận thường trộn luôn code nhập/xuất dữ liệu cùng với
code xử lý ma trận (cộng, tích vô hướng, nhân ma trận,...).
Hoặc, khi tạo lớp mô tả sinh viên thường viết luôn trong đó code xuất/nhập
thông tin sinh viên, nhưng lại không biết nên lưu trữ danh sách sinh viên,
viết các phương thức thêm mới/sửa/xóa/lọc ở đâu.
Vấn đề tổ chức mã nguồn của project
Phân chia code xử lý dữ liệu và code xử lý giao diện là một trong những yêu
cầu đầu tiên và đặc biệt quan trọng, giúp việc quản lý code và kiểm thử ứng
dụng dễ dàng hơn.
Các ứng dụng quản lý (Line-of-Business, LOB) đều có xu hướng phân chia
code thành các phần: giao diện (gọi là các view), các lớp mô tả các đối
tượng được quản lý (các model), các lớp hỗ trợ xử lý/truy xuất/lưu trữ dữ
liệu (thường gọi là khối data service).
Để ghép nối giao diện với dữ liệu, người ta đưa ra nhiều loại lớp trung gian
khác nhau. Từ sự khác nhau về đặc điểm và chức năng của các lớp trung
gian xuất hiện một số mô hình kiến trúc (architectural pattern) thường được
sử dụng trong phát triển các ứng dụng quản lý: mô hình MVC (Model – View
– Controller), mô hình MVP (Model – View – Presenter), mô hình MVVM
(Model – View – View Model),....
Chúng ta có thể nhận thấy ngay là trong các mô hình này đều tồn tại hai
thành phần chung: View và Model. Việc lựa chọn mô hình nào sẽ do nhiều
yếu tố khác nhau quyết định. Ở đây chúng ta sẽ không đi vào phân tích các
mô hình này vì đây là nội dung của một môn học khác.
Mô hình MVC
Trong dự án này, chúng ta sẽ vận dụng mô hình MVC, với mục đích chính
là giúp phân chia code một cách rõ ràng, cụ thể như sau:
1. Tất cả code dùng để xuất nhập dữ liệu trên giao diện dòng lệnh (gọi
chung là các lớp giao diện hoặc các lớp view) sẽ đặt trong thư mục
Views;
2. Code dùng để mô tả thực thể (gọi chung là các lớp dữ liệu hoặc các
lớp model) được quản lý (như sách và giá sách) được đặt trong thư
mục Models;

359
3. Trong thư mục Controllers sẽ chứa code giúp ghép nối dữ liệu và giao
diện (gọi chung là các lớp điều khiển hay lớp controller).

Cấu trúc mã nguồn theo mô hình MVC


Ngoài các vấn đề nêu trên, khi phân chia code theo mô hình MVC cũng đặt thêm các yêu cầu
về sự phụ thuộc giữa các class, cụ thể như sau:
(1) Các lớp giao diện chỉ được biết đến sự tồn tại của các lớp dữ liệu, nói cách khác, các lớp
giao diện chỉ phụ thuộc vào các lớp giao diện;
(2) Các lớp giao diện không được biết đến sự tồn tại của các lớp điều khiển;
(3) Các lớp điều khiển có thể phụ thuộc vào các lớp dữ liệu và các lớp giao diện.
Giới hạn này cũng đưa ra thứ tự xây dựng các lớp trong mô hình MVC: dữ liệu => giao diện
=> điều khiển.
Trong các bài thực hành, bạn sẽ làm quen với các vấn đề của thể của mô hình này. Nó sẽ rất có
ích để sau bạn chuyển sang học công nghệ ASP.NET MVC và Web API.

360
Model: class, variable, property, comment,
namespace, using
Bài học này sẽ hướng dẫn bạn thực hiện các bước để xây dựng class đầu
tiên của dự án: lớp model. Class sẽ xây dựng trong bài đóng vai trò quan
trọng và sẽ được sử dụng xuyên suốt các bài thực hành tiếp theo.
Bài học này sẽ dẫn dắt từng bước thực hành để vận dụng các nội dung đã
học trong phần lý thuyết. Nội dung chủ yếu của nhóm bài này là vận dụng
các kỹ thuật xây dựng class sử dụng biến thành viên và property trong
class C#.

Phân tích các thành phần của class sẽ xây dựng


Như đã phân tích, một cuốn sách điện tử chứa những thông tin sau:
1. tác giả (hoặc nhóm tác giả),
2. tựa đề,
3. nhà xuất bản,
4. năm xuất bản,
5. lần tái bản,
6. mã ISBN (mã số tiêu chuẩn quốc tế cho sách),
7. mô tả tóm tắt nội dung (dùng cho việc tìm kiếm),
8. từ khóa mô tả nội dung/thể loại,
9. đánh giá của cá nhân (rating, sau này có thể dùng trong sắp xếp),
10. đánh dấu cuốn sách nào hiện đang đọc (để sau dễ dàng tìm đến
những cuốn được đánh dấu),
11. đường dẫn đầy đủ tới tập tin sách.
Đây là kết quả của quá trình trừu tượng hóa (phân tích và tách ra các thông
tin đặc trưng mà chúng ta quan tâm) từ các cuốn sách về mặt thông tin.
Từ sự trừu tượng hóa đó chúng ta xác định các nội dung cơ bản của class
sẽ được xây dựng như bảng dưới đây:

STT Tên Mô tả Kiểu Ghi chú

1 Id Số định danh duy Số nguyên Bắt buộc


nhất

2 Nhóm tác giả Danh sách tên tác Văn bản Bắt buộc
(authors) giả, phân tách bởi
dấu phẩy

361
3 Tiêu đề (title) Tiêu đề sách Văn bản Bắt buộc

4 Nhà xuất bản Tên nhà xuất bản Văn bản Bắt buộc
(publisher)

5 Năm xuất Năm xuất bản Số nguyên Bắt buộc, phải lớn hơn
bản (year) sách 1950, nhỏ hơn năm hiện
tại

6 Lần tái bản Lần tái bản của Số nguyên Bắt buộc, phải lớn hơn
(edition) sách hoặc bằng 1, mặc định là
1

7 Mã xuất bản Mã số tiêu chuẩn Văn bản Không bắt buộc


(Isbn) quốc tế

8 Từ khóa Danh sách các từ Văn bản Không bắt buộc


(tags) khóa mô tả nội
dung, thể loại

9 Mô tả Mô tả tóm tắt nội Văn bản Không bắt buộc


(description) dung

10 Đường dẫn Đường dẫn (đầy Văn bản Bắt buộc, phải là một
(tập tin) đủ) tới tập tin pdf đường dẫn đúng

11 Đánh dấu Dùng để đánh Logic Không bắt buộc, mặc


đọc (reading) dấu một cuốn định là false
sách đang đọc

12 Đánh giá Đánh giá chất Số nguyên Không bắt buộc, có giá
(rating) lượng cuốn sách trị từ 1 (dở nhất) đến 5
(tốt nhất), mặc định là 1

Từ đây, chúng ta sẽ xây dựng class đầu tiên với C#. Loại class chỉ chứa
thông tin của đối tượng bị quản lý cũng được gọi là class thực thể (entity
class).

Thực hành 1: xây dựng class Book với biến thành viên
Bước 1. Tạo tập tin mã nguồn cho class
Click phải vào thư mục Models, chọn Add => New Item … hoặc bấm tổ hợp
Ctrl + Shift + A.

362
Thêm class mới cho project
Trong hộp thoại “Add New Item” chọn “Class”; trong hộp văn bản “Name”
nhập “Book”.

Đặt tên tập tin (và tên class)

363
Bấm nút “Add” hoặc phím “Enter”. Một tập tin mã nguồn mới “Book.cs” đã
được thêm vào thư mục Models với nội dung như sau:

File mã nguồn của class mới tạo


Bước 2. Viết code cho lớp Book
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Models
{
class Book
{
int _id;
string _authors;
string _title;
string _publisher;
int _year;
int _edition;
string _isbn;
string _tags;
string _description;
int _rating;
bool _reading;
string _file;
}
}
Khi xây dựng class Book bạn đã vận dụng quy ước sau:
 Đặt tên class kiểu PascalCase;

364
 Đặt toàn bộ code của mỗi class trong một tập tin code riêng cùng tên;
 Áp dụng quy ước đặt tên biến thành viên private theo camelCase với
dấu _ ở đầu.

Thực hành 2: sử dụng biến public


Trong đoạn code trước, tất cả biến thành viên của “Book” đều được khai báo
là “private” và cũng không được gán giá trị đầu. Chúng ta sẽ cải tiến để
các trường dữ liệu của “Book” trở thành “public” và gán giá trị đầu phù
hợp. Điều chỉnh code của “Book” như sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Models
{
public class Book
{
public int Id = 1;
public string Authors = "Unknown authors";
public string Title = "A new book";
public string Publisher = "Unknown publisher";
public int Year = 2018;
public int Edition = 1;
public string Ibn;
public string Tags;
public string Description = "A new book";
public int Rating = 1;
public bool Reading = false;
public string File;
}
}
Với cải tiến này, các object (biến) tạo ra từ kiểu Book cho phép truy xuất
các trường dữ liệu chứa trong nó và có thể được sử dụng để lưu trữ dữ liệu
cho các cuốn sách điện tử đang cần quản lý.
Cũng lưu ý cách đặt tên biến public theo quy ước PascalCase.
Tuy nhiên, cải tiến này có một nhược điểm: các biến thành viên của class
này có thể bị truy xuất trực tiếp và chúng ta không kiểm soát được giá trị
gán cho chúng.
Ví dụ về yêu cầu giá trị của các trường:
 Id: không nhận giá trị nhỏ hơn 1;
 Authors, Title, Publisher: không được nhận xâu rỗng;
 Edition: không được nhận các giá trị nhỏ hơn 1;
 Rating: chỉ nhận các giá trị từ 1 đến 5, tương đương với các mức đánh
giá từ xấu đến tốt;
365
 File: chỉ chấp nhận đường dẫn chính xác tới tập tin sách pdf.

Thực hành 3: sử dụng auto property


Thay đổi code của class Book như sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Models
{
public class Book
{
public int Id { get; set; } = 1;
public string Authors { get; set; } = "Unknown author";
public string Title { get; set; } = "A new book";
public string Publisher { get; set; } = "Unknown publisher";
public int Year { get; set; } = 2018;
public int Edition { get; set; } = 1;
public string Isbn { get; set; }
public string Tags { get; set; }
public string Description { get; set; } = "A new book";
public int Rating { get; set; } = 1;
public bool Reading { get; set; }
public string File { get; set; }
}
}
Lưu ý cách đặt tên property theo quy ước PascalCase.
Bạn có thể sử dụng Code snippet để nhanh chóng tạo ra property theo cách
sau:
1. Nhập cụm “prop” => trong danh sách lựa chọn của Visual Studio
IntelliSense xuất hiện mục “prop” => chọn mục này và bấm phím Tab
hai lần.
2. Trong snippet vừa tạo chỉnh sửa các thông tin cần thiết (phần code
được bôi vàng). Di chuyển giữa các vùng bôi vàng bằng phím Tab. Kết
thúc chỉnh sửa snippet bằng phím Enter.

So sánh với code cũ bạn có thể thấy sau tên của mỗi trường dữ liệu xuất
hiện một khối code { get; set; }. Phần thực hành này hoàn toàn sử
dụng auto property.

366
Thực hành 4: sử dụng full property
Như đã phân tích, trong class “Book” có một số giới hạn cần đặt ra với dữ
liệu:
 Id không nhận giá trị nhỏ hơn 1;
 Authors, Title, Publisher không được nhận xâu rỗng;
 Year không nhận các giá trị nhỏ hơn 1950 (sách cũ quá!);
 Edition không nhận giá trị nhỏ hơn 1;
 Rating chỉ nhận giá trị trong khoảng [1, 5];
 File chỉ được nhận giá trị là đường dẫn chính xác tới tập tin.
Ngoài ra, chúng ta cũng muốn tạo ra một trường trong đó chứa tên ngắn
gọn của File sách (để tiện sử dụng trong một số trường hợp).
Chúng ta thay đổi code của class “Book” như sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Models
{
public class Book
{
private int _id = 1;
public int Id
{
get { return _id; }
set { if (value >= 1) _id = value; }
}
private string _authors = "Unknown author";
public string Authors
{
get { return _authors; }
set { if (!string.IsNullOrEmpty(value)) _authors = value; }
}
private string _title = "A new book";
public string Title
{
get { return _title; }
set { if (!string.IsNullOrEmpty(value)) _title = value; }
}
private string _publisher = "Unknown publisher";
public string Publisher
{
get { return _publisher; }
set { if (!string.IsNullOrEmpty(value)) _publisher = value; }
}
private int _year = 2018;
public int Year
{
get { return _year; }

367
set { if (value >= 1950) _year = value; }
}
private int _edition = 1;
public int Edition
{
get { return _edition; }
set { if (value >= 1) _edition = value; }
}
public string Isbn { get; set; } = "";
public string Tags { get; set; } = "";
public string Description { get; set; } = "A new book";
private int _rating = 1;
public int Rating
{
get { return _rating; }
set { if (value >= 1 && value <= 5) _rating = value; }
}
public bool Reading { get; set; }
private string _file;
public string File
{
get { return _file; }
set { if (System.IO.File.Exists(value)) _file = value; }
}
public string FileName
{
get { return System.IO.Path.GetFileName(_file); }
}
}
}
Như vậy, phần thực hành này đã vận dụng full property để kiểm soát dữ
liệu nhập vào cho các trường backed field.

Thực hành 5: sử dụng documentation comment


Các bạn đã biết, C# sử dụng một loại chú thích đặc biệt gọi là chú thích tài
liệu (documentation comment). Loại chú thích này rất có ích cho người lập
trình vì nó giúp mô tả các đơn vị code như class, method, interface,... xuyên
suốt trong project.
Phần thực hành này sẽ bổ sung các chú thích cần thiết cho lớp Book.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Models
{
/// <summary>
/// Lớp mô tả sách điện tử
/// </summary>
public class Book
{
private int _id = 1;
/// <summary>
/// số định danh duy nhất cho mỗi object
368
/// </summary>
public int Id
{
get { return _id; }
set { if (value >= 1) _id = value; } // id chỉ nhận giá trị >= 1
}
private string _authors = "Unknown author";
/// <summary>
/// tên tác giả hoặc nhóm tác giả, không nhận chuỗi rỗng
/// </summary>
public string Authors
{
get { return _authors; }
set { if (!string.IsNullOrEmpty(value)) _authors = value; } // không nhận
chuỗi rỗng
}
private string _title = "A new book";
/// <summary>
/// tiêu đề sách, không nhận chuỗi rỗng
/// </summary>
public string Title
{
get { return _title; }
set { if (!string.IsNullOrEmpty(value)) _title = value; } // không nhận
chuỗi rỗng
}
private string _publisher = "Unknown publisher";
/// <summary>
/// nhà xuất bản, không nhận chuỗi rỗng
/// </summary>
public string Publisher
{
get { return _publisher; }
set { if (!string.IsNullOrEmpty(value)) _publisher = value; } // không
nhận chuỗi rỗng
}
private int _year = 2018;
/// <summary>
/// năm xuất bản, không nhỏ hơn 1950
/// </summary>
public int Year
{
get { return _year; }
set { if (value >= 1950) _year = value; } // năm không nhỏ hơn 1950
}
private int _edition = 1;
/// <summary>
/// lần tái bản, không nhỏ hơn 1
/// </summary>
public int Edition
{
get { return _edition; }
set { if (value >= 1) _edition = value; } // không nhận giá trị < 1
}
/// <summary>
/// mã số quốc tế
/// </summary>
public string Isbn { get; set; } = "";

369
/// <summary>
/// từ khóa mô tả nội dung / thể loại
/// </summary>
public string Tags { get; set; } = "";
/// <summary>
/// mô tả tóm tắt nội dung
/// </summary>
public string Description { get; set; } = "A new book";
private int _rating = 1;
/// <summary>
/// đánh giá cá nhân, giá trị từ 1 đến 5
/// </summary>
public int Rating
{
get { return _rating; }
set { if (value >= 1 && value <= 5) _rating = value; } // nhận giá trị từ
1 đến 5
}
/// <summary>
/// đánh dấu là đang đọc
/// </summary>
public bool Reading { get; set; }
private string _file;
/// <summary>
/// tập tin sách (gồm dường dẫn)
/// </summary>
public string File
{
get { return _file; }
set { if (System.IO.File.Exists(value)) _file = value; } // nhận đường dẫn
đúng
}
/// <summary>
/// tập tin sách (không có đường dẫn)
/// </summary>
public string FileName
{
get { return System.IO.Path.GetFileName(_file); } // trả lại tên tập tin
ngắn gọn
}
}
}
Khi class (và kiểu dữ liệu nói chung), property hoặc phương thức có chú
thích tài liệu, nếu đặt con trỏ chuột lên trên sẽ xuất hiện thông tin này, giúp
người lập trình (bản thân hoặc người khác) dễ dàng hiểu được tác dụng của
các đơn vị code này.
Mỗi loại đối tượng trong C# đều được Visual Studio hiển thị bằng một biểu
tượng riêng. Như class, method, property, variable đều được biểu thị bằng
một biểu tượng riêng, giúp người lập trình dễ dàng nhận biết đây là loại đối
tượng nào.
Biểu tượng kết hợp với chú thích tài liệu hỗ trợ rất tốt cho việc tự tìm hiểu
code.

370
Dưới đây là ví dụ đối với class “Book” và thuộc tính “Authors”. Trong hình
là cách Visual Studio hiển thị ghi chú tài liệu khi đặt con trỏ chuột vào tên
class Book và thuộc tính Authors. Cũng lưu ý biểu tượng của class và
property.

Biểu tượng class trong Visual Studio

Biểu tượng property trong Visual Studio


Nên tập thành thói quen ghi chú code. Đối với các khai báo kiểu, property, method thì nên dùng
documentation comment. Trong thân phương thức thì dùng comment thông thường.
Thực hành 6: thu gọn khối using, làm gọn code
Sau khi hoàn thành một class có thể xóa bỏ những mục using không sử
dụng tới để tập tin code gọn gàng hơn. Visual Studio 2017 hỗ trợ tính năng
thông báo này bằng cách hiện màu nhạt cho những mục using thừa (không
sử dụng class nào trong namespace tương ứng). Xóa bỏ những mục này là
an toàn và giúp code nhìn gọn gàng hơn.
Visual Studio cung cấp một Quick Action để “thu dọn” khối using: đặt con
trỏ vào vùng using (chỗ hiện màu nhạt); kích hoạt Quick Action (tổ hợp Ctrl
+ .); chọn Remove Unnecessary Usings.

371
Sử dụng Quick Action để dọn dẹp khối using
Để format lại toàn bộ code theo quy ước viết code chung của Visual Studio,
bạn bấm tổ hợp Ctrl + K + D.
Đến đây xin chúc mừng bạn đã xây dựng được một class đầu tiên “theo kiểu
C#”.

372
View xuất dữ liệu: phương thức, phương thức khởi
tạo, xâu, xuất thông tin, enum
Trong bài này chúng ta tiếp tục xây dựng class C# giúp xuất thông tin về
một cuốn sách ra màn hình console. Qua bài thực hành này bạn sẽ học cách
áp dụng phương thức thành viên, xây dựng phương thức khởi tạo, xử
lý chuỗi ký tự, làm việc với console, sử dụng kiểu enum.

Phân tích yêu cầu


Trong bài học trước chúng ta đã cùng xây dựng hoàn thiện một class đầu
tiên dùng để mô tả thông tin của các cuốn sách điện tử. Đây là một class
đặc biệt chỉ mô tả dữ liệu mà không có phương thức xử lý nào.
Trong kiểu tiếp cận hướng nghiệp vụ (domain-driven), loại class này có tên
gọi là lớp thực thể (entity class) hay domain model class. Class này sẽ là
khuôn mẫu để chúng ta tạo ra các object là dữ liệu về các cuốn sách điện
tử cụ thể.
Với cách phân chia code theo mô hình MVC, chúng ta sẽ tạo ra các class
riêng giúp hiển thị dữ liệu hoặc nhập dữ liệu (được gọi chung là các lớp
view).
Ở bài thực hành đầu tiên chúng ta đã phân tích bài toán và các ca sử dụng.
Chúng ta thấy, trước mắt phần mềm này phải có khả năng tương tác với
người dùng ở các mặt sau:
1. Hiển thị một cuốn sách cụ thể;
2. Hiển thị một danh sách các cuốn sách điện tử;
3. Nhập thông tin cho một cuốn sách mới;
4. Cập nhật thông tin cho một cuốn sách đang có sẵn;
5. Nhận câu lệnh bất kỳ từ người dùng.
Ứng với mỗi yêu cầu về hiển thị thông tin chúng ta sẽ xây dựng một class
riêng biệt. Như vậy chúng ta sẽ phải xây dựng ít nhất bốn class giao diện
khác nhau cho các mục đích trên.
Trong bài này chúng ta sẽ bắt đầu xây dựng class giúp hiển thị thông tin về
một cuốn sách cụ thể.

Thực hành 1: xây dựng class hiển thị thông tin về một cuốn
sách
Trong phần thực hành này chúng ta bước đầu tạo ra một class mới có khả
năng hiển thị thông tin chi tiết của một cuốn sách.

373
Tạo một tập tin mã nguồn mới cho class
Click phải vào thư mục Views, chọn Add => Class …

Trong hộp thoại “Add New Item” chọn loại tập tin là “Class”, mục “Name”
nhập tên “BookSingleView.cs” Ấn nút “Add” để hoàn tất.

374
Tạo tập tin mã nguồn mới cho class BookSingleView
Một tập tin mã nguồn “BookSingleView.cs” đã được tạo ra trong thư mục
Views. Trong tập tin mã nguồn này Visual Studio đã sinh sẵn code cho một
class có cùng tên với tập tin:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
class BookSingleView
{
}
}
* Lưu ý khi đặt tên tập tin và class
Khi đặt tên tập tin/class chúng ta nên tự đặt ra một quy luật nào đó để nếu
sau này xuất hiện class mới cùng loại chúng ta sẽ dễ dàng chọn được tên
phù hợp. Chúng ta đặt ra quy ước sau đối với tên các lớp view. Tên của mỗi
lớp view gồm 3 phần:
 Tên của các class (và cũng là tên tập tin) của các lớp view bắt đầu
bằng tên loại dữ liệu mà nó hiển thị (trong trường hợp này là Book),
 Phần tiếp theo chứa một từ mô tả đặc thù của giao diện (ví dụ, lớp
view hiển thị một object của Book sẽ có chứa từ Single),
 Tên gọi kết thúc là View (để sau này sử dụng nếu nhìn thấy class tận
cùng là View, chúng ta sẽ hình dung ra ngay nhiệm vụ của nó).
375
Như vậy, sau này nếu cần xây dựng lớp view để hiển thị danh sách các cuốn
sách, có thể dễ dàng nghĩ ngay tới tên gọi “BookListView”, class để tạo cuốn
sách mới sẽ là “BookAddView” hay “BookCreateView”,....
Viết code cho class BookSingleView
Bổ sung code cho lớp BookSingleView như sau (chú ý đọc kỹ các chú thích)
using System;
// bốn dòng using dưới đây là thừa và có thể xóa đi (sử dụng Quick Action)
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Views // chú ý cách Visual Studio đặt tên namespace
{
using Models; // chú ý cách dùng using bên trong namespace
/// <summary>
/// class để hiển thị một cuốn sách, chỉ sử dụng trong dự án (internal)
/// </summary>
internal class BookSingleView
{
protected Book Model; // biến này để lưu trữ thông tin cuốn sách đang cần hiển
thị
/// <summary>
/// đây là phương thức khởi tạo, sẽ được gọi đầu tiên khi tạo object
/// </summary>
/// <param name="model">cuốn sách cụ thể sẽ được hiển thị</param>
public BookSingleView(Book model)
{
Model = model; // chuyển dữ liệu từ tham số sang biến thành viên để sử
dụng trong toàn class
}
/// <summary>
/// thực hiện in thông tin ra màn hình console
/// </summary>
public void Render()
{
if (Model == null) // kiếm tra xem object có dữ liệu không
{
Console.ForegroundColor = ConsoleColor.Red; // đổi màu chữ sang đỏ
Console.WriteLine("NO BOOK FOUND. SORRY!"); // in ra dòng thông báo
Console.ResetColor(); // trả lại màu chữ mặc định
return; // kết thúc thực hiện phương thức (bỏ qua phần còn lại)
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("BOOK DETAIL INFORMATION");
Console.ResetColor();
/* các dòng dưới đây viết ra thông tin cụ thể theo từng dòng
* sử dụng cách tạo xâu kiểu "interpolation"
* và dùng dấu cách để căn chỉnh tạo thẩm mỹ
*/
Console.WriteLine($"Authors: {Model.Authors}");
Console.WriteLine($"Title: {Model.Title}");
Console.WriteLine($"Publisher: {Model.Publisher}");
Console.WriteLine($"Year: {Model.Year}");
Console.WriteLine($"Edition: {Model.Edition}");

376
Console.WriteLine($"Isbn: {Model.Isbn}");
Console.WriteLine($"Tags: {Model.Tags}");
Console.WriteLine($"Description: {Model.Description}");
Console.WriteLine($"Rating: {Model.Rating}");
Console.WriteLine($"Reading: {Model.Reading}");
Console.WriteLine($"File: {Model.File}");
Console.WriteLine($"File Name: {Model.FileName}");
}
}
}
Ở đây bạn đã xây dựng một class C# “bình thường” và quen thuộc hơn, bao
gồm biến thành viên (biến Model), phương thức thành viên (phương
thức Render), và phương thức khởi tạo (constructor) của class
(BookSingleView(Book model)).
Trong thân phương thức Render() bạn cũng xây dựng một số property và
sử dụng các phương thức xuất nhập với giao diện dòng lệnh.
Sự tồn tại của cả dữ liệu (biến thành viên) và phương thức xử lý dữ liệu
(phương thức thành viên) trong khuôn khổ một class là biểu hiện của một
nguyên lý trong lập trình hướng đối tượng: đóng gói (encapsulation).
Trong class này chúng ta bắt đầu sử dụng class “Book” được xây dựng ở bài
trước để khai báo object. Vì “Book” là một class – một kiểu dữ liệu người
dùng tự định nghĩa, nó có thể được sử dụng như bất kỳ kiểu dữ liệu nào
(như int, bool, string mà ta đã biết).
Logic của class này như sau:
1. Khi người sử dụng class khởi tạo object (sẽ xem xét ở bài sau) của
lớp BookSingleView sẽ phải cung cấp một object của lớp Book (tức là
một cuốn sách cụ thể);
2. Phương thức khởi tạo (sẽ xem xét ở bài sau) sẽ chuyển object này
sang một biến cục bộ (biến Model) để sử dụng trong toàn
class BookSingleView;
3. Phương thức Render() sẽ kiểm tra xem object có dữ liệu hay không.
4. Nếu object không chứa dữ liệu (thực tế phải hiểu object này không
chỉ tới một vùng nhớ nào, tức nó là một object trống, C# gọi những
object như vậy là null object), phương thức sẽ viết dòng thông báo
màu đỏ.
5. Nếu object chứa dữ liệu, giá trị của từng trường dữ liệu (là property)
sẽ được viết ra màn hình.

377
Thực hành 2: xây dựng thêm phương thức mới
Trong phương thức Render() ở trên chúng ta thấy có nhóm code sau bị
trùng
Console.ForegroundColor = ConsoleColor.Red; // đổi màu chữ sang đỏ
Console.WriteLine("NO BOOK FOUND. SORRY!"); // in ra dòng thông báo
Console.ResetColor(); // trả lại màu chữ mặc định
return; // kết thúc thực hiện phương thức (bỏ qua phần còn lại)

Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("BOOK DETAIL INFORMATION");
Console.ResetColor();
Hai nhóm code này chỉ khác nhau về thông tin và màu sắc của chữ hiển thị
ra.
Giả sử chúng ta cần viết thêm một số thông tin nữa với màu sắc, 3 đoạn
code như trên sẽ lặp lại thêm nhiều lần. Điều này vi phạm nguyên lý
DRY (Don’t Repeat Yourself) – không lặp code.
Trong phạm vi một class, nếu có nhiều dòng code giống nhau về logic như
vậy và chỉ khác về tham số, chúng ta có thể nghĩ tới xây dựng một phương
thức nội bộ để tránh lặp code.
Xây dựng phương thức in ra console với màu sắc
Bổ sung thêm phương thức sau vào cuối class BookSingleView:
/// <summary>
/// in thông báo ra màn hình console với chữ màu
/// </summary>
/// <param name="message">thông báo</param>
/// <param name="color">màu</param>
protected void WriteLine(string message, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
Console.ResetColor();
}

Sử dụng phương thức mới xây dựng


Gọi phương thức mới thay cho các nhóm code cũ trong phương thức Render:
*Thay đoạn code
Console.ForegroundColor = ConsoleColor.Red; // đổi màu chữ sang đỏ
Console.WriteLine("NO BOOK FOUND. SORRY!"); // in ra dòng thông báo
Console.ResetColor(); // trả lại màu chữ mặc định
return; // kết thúc thực hiện phương thức (bỏ qua phần còn lại)
bằng
// sử dụng phương thức WriteLine vừa tạo thay cho đoạn code cũ
WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);

378
*Thay đoạn code
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("BOOK DETAIL INFORMATION");
Console.ResetColor();
bằng
// sử dụng phương thức WriteLine vừa tạo thay cho đoạn code cũ
WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);
Code của cả lớp BookSingleView giờ trở thành như sau:
BookSingleView.cs
using System;
namespace BookMan.ConsoleApp.Views // chú ý cách Visual Studio đặt tên namespace
{
using Models; // chú ý cách dùng using bên trong namespace
/// <summary>
/// class để hiển thị một cuốn sách
/// </summary>
internal class BookSingleView
{
protected Book Model; // biến này để lưu trữ thông tin cuốn sách đang cần hiển
thị
/// <summary>
/// đây là phương thức khởi tạo, sẽ được gọi đầu tiên khi tạo object
/// </summary>
/// <param name="model">cuốn sách cụ thể sẽ được hiển thị</param>
public BookSingleView(Book model)
{
Model = model; // chuyển dữ liệu từ tham số sang biến thành viên để sử
dụng trong toàn class
}
/// <summary>
/// thực hiện in thông tin ra màn hình console
/// </summary>
public void Render()
{
if (Model == null) // kiếm tra xem có dữ liệu không
{
// sử dụng phương thức WriteLine vừa tạo thay cho đoạn code cũ
WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);
return; // kết thúc thực hiện phương thức (bỏ qua phần còn lại)
}
// sử dụng phương thức WriteLine vừa tạo thay cho đoạn code cũ
WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);
/* các dòng dưới đây viết ra thông tin cụ thể theo từng dòng
* sử dụng cách tạo xâu kiểu "interpolation"
* và dùng dấu cách để căn chỉnh tạo thẩm mỹ
*/
Console.WriteLine($"Authors: {Model.Authors}");
Console.WriteLine($"Title: {Model.Title}");
Console.WriteLine($"Publisher: {Model.Publisher}");
Console.WriteLine($"Year: {Model.Year}");
Console.WriteLine($"Edition: {Model.Edition}");
Console.WriteLine($"Isbn: {Model.Isbn}");
Console.WriteLine($"Tags: {Model.Tags}");
Console.WriteLine($"Description: {Model.Description}");
Console.WriteLine($"Rating: {Model.Rating}");
379
Console.WriteLine($"Reading: {Model.Reading}");
Console.WriteLine($"File: {Model.File}");
Console.WriteLine($"File Name: {Model.FileName}");
}
/// <summary>
/// in thông báo ra màn hình console với chữ màu
/// </summary>
/// <param name="message">thông báo</param>
/// <param name="color">màu</param>
protected void WriteLine(string message, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
Console.ResetColor();
}
}
}
Trong bài này chúng ta đã xây dựng một class giao diện giúp hiển thị thông
tin của một cuốn sách. Qua đây chúng ta áp dụng cách xây dựng phương
thức thành viên trong C#, cách xuất thông tin ra giao diện console, cách
định dạng chuỗi ký tự và kiểu liệt kê. Chúng ta đã áp dụng các kiến thức
này để xây dựng một class trọn vẹn giúp hiển thị thông tin chi tiết một cuốn
sách ra console.

380
Controller, nối view – model: khởi tạo object, sử dụng
object
Trong bài này, chúng ta sẽ xây dựng một class giúp ghép nối dữ liệu một
cuốn sách điện tử (lớp Book) với lớp chuyên dùng để hiển thị một cuốn sách
riêng rẽ (lớp BookSingleView). Qua đó chúng ta sẽ áp dụng cách khởi tạo
object và truy xuất các thành viên của object.
Trong các bài trước, chúng ta đã lần lượt xây dựng hai class, một để mô tả
dữ liệu (Book), một để mô tả cách hiển thị dữ liệu (BookSingleView), với
mục đích tách rời dữ liệu và giao diện theo nguyên tắc của kiến trúc MVC.
Theo kiến trúc MVC, để ghép nối dữ liệu với giao diện, chúng ta cần đến
một class trung gian: controller (lớp điều khiển).
Lớp controller có nhiệm vụ:
1. Lấy dữ liệu và định hình dữ liệu cho phù hợp với yêu cầu của view;
2. Khởi tạo view và cung cấp dữ liệu cho view.
Như vậy controller phụ thuộc vào view và model, và do đó, được xây dựng
sau hai loại class trên.

Thực hành 1: xây dựng lớp controller


Tạo lớp BookController
Tạo tập tin mã nguồn mới BookController.cs để chứa lớp BookController
trong thư mục Controllers.

Viết code cho lớp BookController


using System;
using System.Collections.Generic;
381
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Controllers
{
using Models; //lưu ý cách dùng using với không gian tên con
using Views;
/// <summary>
/// lớp điều khiển, giúp ghép nối dữ liệu sách với giao diện
/// </summary>
class BookController
{
/// <summary>
/// ghép nối dữ liệu 1 cuốn sách với giao diện hiển thị 1 cuốn sách
/// </summary>
/// <param name="id">mã định danh của cuốn sách</param>
public void Single(int id)
{
Book model = new Book();
// khởi tạo view
BookSingleView view = new BookSingleView(model);
// gọi phương thức Render để thực sự hiển thị ra màn hình
view.Render();
}
}
}

Viết code cho phương thức Main


Nhập code như sau cho lớp Program (tập tin Program.cs):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp
{
using Controllers;
internal class Program
{
private static void Main(string[] args)
{
BookController controller = new BookController();
controller.Single(0);
Console.ReadKey();
}
}
}

Dịch và chạy thử chương trình


Dùng phím F5 hoặc tổ hợp Ctrl+F5. Kết quả chạy chương trình như sau:

382
Kết quả chạy chương trình
Trong phần thực hành trên chúng ta gặp các lệnh sau:
Book model = new Book();
BookSingleView view = new BookSingleView(model);
BookController controller = new BookController();
Đây là cấu trúc C# dùng để khởi tạo object của class.
Để khởi tạo object trong C# sử dụng từ khóa new và lời gọi tới một trong số
các phương thức khởi tạo của class. Như trong đoạn code dưới đây,
new Book();
new BookSingleView(model);
new BookController();
là các lệnh khởi tạo object.
Phương thức khởi tạo là bắt buộc khi định nghĩa class. Tuy nhiên chương trình dịch của C# có
khả năng tự thêm một phương thức khởi tạo cho class nếu nó không nhìn thấy định nghĩa
phương thức khởi tạo nào trong class. Loại phương thức khởi tạo này có tên gọi là phương thức
khởi tạo mặc định (default constructor).
Khai báo object thường đi cùng khởi tạo nhưng không bắt buộc. Ví dụ, các
lệnh
Book model = new Book();
BookSingleView view = new BookSingleView(model);
BookController controller = new BookController();
vừa thực hiện khai báo, vừa khởi tạo object.

Thực hành 2: khởi tạo object của class Book sử dụng object
initializer
Điều chỉnh code của lớp BookController
Điều chỉnh phương thức Single của lớp BookController như sau:
using System;
using System.Collections.Generic;
using System.Linq;

383
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Controllers
{
using Models; //lưu ý cách dùng using với không gian tên con
using Views;
/// <summary>
/// lớp điều khiển, giúp ghép nối dữ liệu sách với giao diện
/// </summary>
internal class BookController
{
/// <summary>
/// ghép nối dữ liệu 1 cuốn sách với giao diện hiển thị 1 cuốn sách
/// </summary>
/// <param name="id">mã định danh của cuốn sách</param>
public void Single(int id)
{
// khởi tạo object với property
Book model = new Book
{
Id = 1,
Authors = "Adam Freeman",
Title = "Expert ASP.NET Web API 2 for MVC Developers (The Expert's
Voice in .NET)",
Publisher = "Apress",
Year = 2014,
Tags = "C#, asp.net, mvc",
Description = "Expert insight and understanding of how to create,
customize, and deploy complex, flexible, and robust
HTTP web services",
Rating = 5,
Reading = true
};
// khởi tạo view
BookSingleView view = new BookSingleView(model);
// gọi phương thức Render để thực sự hiển thị ra màn hình
view.Render();
}
}
}

384
Dịch và chạy thử chương trình

Trong phần thực hành trên chúng ta gặp lại cách khởi tạo object sử dụng bộ
khởi tạo (object initializer). Cú pháp khởi tạo này sử dụng property và được
đưa vào từ C# 3 (.NET Framework 3.5).
Book model = new Book
{
Id = 1,
Authors = "Adam Freeman",
//...
}
Để truy xuất thành viên của object chúng ta sử dụng phép toán “.” với tên
object. Trên thực tế, từ bài trước trong lớp BookSingleView chúng ta đã sử
dụng phép toán này để truy xuất các thuộc tính của object Model thuộc
kiểu Book:
Console.WriteLine($"Authors: {Model.Authors}");
Console.WriteLine($"Title: {Model.Title}");
Console.WriteLine($"Publisher: {Model.Publisher}");
Console.WriteLine($"Year: {Model.Year}");
Console.WriteLine($"Edition: {Model.Edition}");
Console.WriteLine($"Isbn: {Model.Isbn}");
Console.WriteLine($"Tags: {Model.Tags}");
Console.WriteLine($"Description: {Model.Description}");
Console.WriteLine($"Rating: {Model.Rating}");
Console.WriteLine($"Reading: {Model.Reading}");
Console.WriteLine($"File: {Model.File}");
Console.WriteLine($"File Name: {Model.FileName}");
Truy xuất phương thức thành viên đơn giản là một lời gọi phương thức từ
một object nào đó. Việc truy xuất phương thức thành viên cũng sử dụng
cấu trúc tương tự:
BookSingleView view = new BookSingleView(model);
view.Render();
BookController controller = new BookController();

385
controller.Single(0);
Trong bài này chúng ta đã áp dụng khai báo và khởi tạo object của class.
Chúng ta cũng vận dụng kỹ thuật này để xây dựng lớp controller giúp ghép
nối dữ liệu và giao diện theo mô hình MVC.

386
View nhập dữ liệu: biến cục bộ, switch-case, biến đổi
kiểu, tham số
Trong bài này chúng ta tiếp tục xây dựng một lớp view nữa để nhập thông
tin từ người dùng. Qua bài này chúng ta sẽ tiếp xúc với một số vấn đề: biến
cục bộ, nhập dữ liệu từ console, cấu trúc điều khiển, biến đổi kiểu, tham số
của phương thức (tham số tùy chọn, tham số out).

Thực hành 1: xây dựng class giúp nhập thông tin của một
cuốn sách mới
Trong các bài trước chúng ta đã xây dựng lớp view đầu tiên để hiển thị dữ
liệu ra giao diện console. Chúng ta sẽ tiếp tục với lớp view thứ hai giúp nhập
thông tin từ console.
Bước 1. Thêm lớp BookCreateView
Trong thư mục Views thêm tập tin mã nguồn mới (BookCreateView.cs) cho
lớp BookCreateView
Nhập code cho lớp BookCreateView như sau:
using System;
namespace BookMan.ConsoleApp.Views
{
/// <summary>
/// class để thêm một cuốn sách mới
/// </summary>
internal class BookCreateView
{
public BookCreateView()
{
}
/// <summary>
/// yêu cầu người dùng nhập từng thông tin và lưu lại thông tin đó
/// </summary>
public void Render()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("CREATE A NEW BOOK");
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Title: ");
Console.ResetColor();
string title = Console.ReadLine(); //đọc 1 dòng và lưu vào biến title
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Authors: ");
Console.ResetColor();
string authors = Console.ReadLine(); //đọc 1 dòng và lưu vào biến authors
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Publisher: ");
Console.ResetColor();

387
string publisher = Console.ReadLine(); //đọc 1 dòng và lưu vào biến
publisher
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Year: ");
Console.ResetColor();
string yearString = Console.ReadLine(); //đọc 1 dòng và lưu vào biến
yearString
int year = int.Parse(yearString); //chuyển đổi chuỗi sang số nguyên
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Edition: ");
Console.ResetColor();
string editionString = Console.ReadLine(); //đọc 1 dòng và lưu vào biến
editionString
int edition = int.Parse(editionString); //chuyển đổi chuỗi sang số nguyên
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Reading [y/n]: ");
Console.ResetColor();
ConsoleKeyInfo readingChar = Console.ReadKey(); //đọc 1 ký tự và lưu vào
biến yearString
bool reading = readingChar.KeyChar == 'y' || readingChar.KeyChar == 'Y' ?
true : false; //chuyển sang kiểu bool dùng biểu thức điều kiện
Console.WriteLine();
// TODO: TẠM DỪNG Ở ĐÂY, SẼ QUAY LẠI SAU
}
}
}

Bước 3. Bổ sung phương thức Create cho BookController


Bổ sung phương thức Create vào cuối class BookController như sau:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp.Controllers
{
using Models; //lưu ý cách dùng using với không gian tên con
using Views;
/// <summary>
/// lớp điều khiển, giúp ghép nối dữ liệu sách với giao diện
/// </summary>
internal class BookController
{
/// <summary>
/// ghép nối dữ liệu 1 cuốn sách với giao diện hiển thị 1 cuốn sách
/// </summary>
/// <param name="id">mã định danh của cuốn sách</param>
public void Single(int id)
{
// khởi tạo object với property
Book model = new Book
{
Id = 1,
Authors = "Adam Freeman",
Title = "Expert ASP.NET Web API 2 for MVC Developers (The Expert's
Voice in .NET)",
Publisher = "Apress",

388
Year = 2014,
Tags = "C#, asp.net, mvc",
Description = "Expert insight and understanding of how to create,
customize, and deploy complex, flexible, and robust
HTTP web services",
Rating = 5,
Reading = true
};
// khởi tạo view
BookSingleView view = new BookSingleView(model);
// gọi phương thức Render để thực sự hiển thị ra màn hình
view.Render();
}
/// <summary>
/// kích hoạt chức năng nhập dữ liệu cho 1 cuốn sách
/// </summary>
public void Create()
{
BookCreateView view = new BookCreateView();// khởi tạo object
view.Render(); // hiển thị ra màn hình
}
}
}
Để nhập dữ liệu từ giao diện dòng lệnh có thể sử dụng các phương thức của
lớp Console, bao gồm ReadLine, ReadKey, Read. Trong đó:
 ReadLine đọc một dòng và trả về một chuỗi ký tự.
 ReadKey đọc một ký tự và trả về kiểu ConsoleKeyInfo.
 Read đọc một ký tự và trả về mã ascii của ký tự đó.
Thực thi lệnh theo yêu cầu của người dùng
Bạn có thể nhận thấy một vấn đề, mặc dù chúng ta đã chế tạo lớp view thứ
hai và phương thức của controller tương ứng nhưng lại chưa thể thực thi
lệnh theo yêu cầu của người dùng.
Ở bài này các bài tiếp theo, chúng ta sẽ xây dựng thêm các lớp view mới để
đáp ứng hết các nhu cầu tương tác với người dùng (như hiển thị danh sách,
nhập dữ liệu mới, cập nhật dữ liệu, xóa dữ liệu).
Ứng với mỗi lớp view, theo quy ước của MVC chúng ta đang áp dụng, sẽ có
một phương thức tương ứng của controller làm nhiệm vụ ghép nối giữa dữ
liệu (model) với giao diện (view).
Như vậy, chương trình cần có khả năng tiếp nhận yêu cầu của người dùng
và kích hoạt phương thức tương ứng của controller.
Trong các MVC Framework (như ASP.NET MVC) đều có một công cụ làm
nhiệm vụ đó và thường được gọi là router. Router cho phép ánh xạ một yêu
cầu của người dùng (ví dụ, dưới dạng một chuỗi ký tự lệnh cùng tham số

389
trong ứng dụng console, hoặc một chuỗi url đối với ứng dụng web) sang
việc thực thi một phương thức tương ứng của controller.
Với kỹ thuật C# hiện tại chúng ta chưa đủ khả năng để tạo ra một router
đúng nghĩa. Tuy nhiên, để giải quyết phần nào bài toán, sau đây chúng ta
sẽ sử dụng cấu trúc rẽ nhiều nhánh để mô phỏng khả năng của một router.

Thực hành 2: nhận lệnh từ người dùng


Bước 1. Chỉnh sửa code của phương thức Main
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp
{
using Controllers;
internal class Program
{
private static void Main(string[] args)
{
BookController controller = new BookController();
while (true)
{
Console.Write("Request> ");
string request = Console.ReadLine();
switch (request.ToLower())
{
case "single":
controller.Single(1);
break;
default:
Console.WriteLine("Unknown command");
break;
}
}
}
}
}

Bước 2. Dịch và chạy thử chương trình với lệnh single


Gõ lệnh single từ dấu nhắc lệnh

390
Kết quả chạy chương trình
Nếu nhập đúng lệnh single (không phân biệt hoa thường), phương
thức Single của controller sẽ được gọi. Khi nhập bất kỳ lệnh nào khác sẽ
viết ra màn hình “Unknown command”.
Bước 3. Bổ sung khả năng thực hiện lệnh create
Bổ sung thêm một nhánh cho cấu trúc switch-case của Main (lớp Program)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp
{
using Controllers;
internal class Program
{
private static void Main(string[] args)
{
BookController controller = new BookController();
while (true)
{
Console.Write("Request> ");
string request = Console.ReadLine();
switch (request.ToLower())
{
case "single":
controller.Single(1);
break;
case "create":
controller.Create();
break;
default:

391
Console.WriteLine("Unknown command");
break;
}
}
}
}
}

Bước 4. Dịch và chạy thử chương trình với lệnh create

Kết quả chạy chương trình với lệnh create


Trong phương thức Main ở trên, chúng ta chuyển đổi chuỗi truy vấn của
người dùng thành dạng chữ thường (để người dùng không phải quan tâm
nhập chữ thường/chữ hoa) bằng phương thức ToLower của lớp string và
cung cấp biểu thức này cho cấu trúc switch.
Trong cấu trúc này chúng ta mới xác định được một “case”: nếu giá trị chuỗi
truy vấn (sau khi chuyển về chữ thường) là “single” thì sẽ gọi phương thức
Single của controller. Trong các bài tiếp theo chúng ta sẽ lần lượt bổ sung
thêm các “case” mới. Nếu giá trị của chuỗi truy vấn không trùng với bất kỳ
“case” nào, chúng ta sẽ in ra màn hình thông báo “Unknown command”.

Thực hành 3: cải tiến BookCreateView


Bước 1. Bổ sung phương thức in ra console với màu sắc
Bổ sung thêm hai phương thức sau vào cuối lớp BookCreateView
/// <summary>
/// xuất thông tin ra console với màu sắc (WriteLine có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
private void WriteLine(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}

392
/// <summary>
/// xuất thông tin ra console với màu sắc (Write có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
private void Write(string message, ConsoleColor color = ConsoleColor.White,
bool resetColor = true)
{
Console.ForegroundColor = color;
Console.Write(message);
if (resetColor)
Console.ResetColor();
}

Bước 2. Sửa code của phương thức Render


public void Render()
{
WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
ConsoleColor labelColor = ConsoleColor.Magenta;
Write("Title: ", labelColor);
var title = Console.ReadLine(); //đọc 1 dòng và lưu vào biến title
Write("Authors: ", labelColor);
var authors = Console.ReadLine(); //đọc 1 dòng và lưu vào biến authors
Write("Publisher: ", labelColor);
var publisher = Console.ReadLine(); //đọc 1 dòng và lưu vào biến publisher
Write("Year: ", labelColor);
string yearString = Console.ReadLine(); //đọc 1 dòng và lưu vào biến
yearString
var year = int.Parse(yearString); //chuyển đổi chuỗi sang số nguyên
Write("Edition: ", labelColor);
var editionString = Console.ReadLine(); //đọc 1 dòng và lưu vào biến
editionString
var edition = int.Parse(editionString); //chuyển đổi chuỗi sang số nguyên
Write("Reading [y/n]: ", labelColor);
ConsoleKeyInfo readingChar = Console.ReadKey(); //đọc 1 ký tự và lưu vào
biến yearString
var reading = readingChar.KeyChar == 'y' || readingChar.KeyChar == 'Y' ?
true : false; //chuyển sang kiểu bool dùng biểu thức điều kiện
Console.WriteLine();
Write("Tags: ", labelColor);
var tags = Console.ReadLine();
Write("Description: ", labelColor);
var description = Console.ReadLine();
Write("Rate: ", labelColor);
var rateString = Console.ReadLine();
var rating = int.Parse(rateString);
Write("File: ", labelColor);
var file = Console.ReadLine();

}
Trong lần cải tiến này, chúng ta viết thêm hai phương thức giúp in chữ có
màu ra console và dùng hai phương thức này để giảm bớt số lượng code bị
lặp trong phương thức Render. Chúng ta cũng bổ sung code để nhập nốt
các dữ liệu còn lại của sách.

393
Trong lần cải tiến trên chúng ta xây dựng hai phương thức mới Write và
WriteLine với danh sách tham số có chút khác biệt với những phương thức
bình thường. Trong hai phương thức này, tham số color và resetColor
được gán sẵn giá trị: color = ConsoleColor.White, và resetColor =
true.
Tính năng này của C# được gọi là tham số với giá trị mặc định hoặc tham
số không bắt buộc hoặc tham số tùy chọn (Optional Arguments hoặc
Optional Parameters).
Các phương thức Write và WriteLine ở trên mặc dù được định nghĩa với 3
tham số vào nhưng hai tham số sau là tham số tùy chọn: môt tham số nhận
màu sắc mặc định là White; một tham số nhận giá trị mặc định là true.
Do đó, khi gọi các phương thức này trong Render, chúng ta chỉ cung cấp
giá trị cho tham số color (do chúng ta muốn viết ra chữ màu Magenta, khác
với màu White mặc định) nhưng không cung cấp giá trị cho tham số thứ 3
(vì vẫn muốn dùng giá trị true, vốn là giá trị có sẵn của tham số tùy chọn
này).

Cách Visual Studio hiển thị thông tin hỗ trợ của tham số tùy chọn.

Thực hành 4: tiếp tục cải tiến BookCreateView


Trong phần thực hành này chúng ta lại tiếp tục xây dựng thêm các phương
thức mới giúp đơn giản hóa hơn nữa việc nhập dữ liệu, đồng thời luyện tập
cách xây dựng phương thức.
Bước 1. Xây dựng phương thức nhập dữ liệu từ Console
Bổ sung các phương thức sau vào lớp BookCreateView: InputBool,
InputInt, InputString
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// rồi chuyển sang kiểu bool
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private bool InputBool(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{
Write($"{label} [y/n]: ", labelColor);

394
ConsoleKeyInfo key = Console.ReadKey(); //đọc 1 ký tự vào biến key
Console.WriteLine();
bool @char = key.KeyChar == 'y' || key.KeyChar == 'Y' ?
true : false; //chuyển sang kiểu bool dùng biểu thức điều kiện
return @char; // lưu ý cách viết tên biến @char
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// rồi chuyển sang số nguyên
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private int InputInt(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{
while (true)
{
var str = InputString(label, labelColor, valueColor);
var result = int.TryParse(str, out int i);
if (result == true)
{
return i;
}
}
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private string InputString(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{
Write($"{label}: ", labelColor, false);
Console.ForegroundColor = valueColor;
string value = Console.ReadLine();
Console.ResetColor();
return value;
}

Bước 2. Cải tiến code của phương thức Render


Sử dụng 3 phương thức mới để làm đơn giản code của phương thức Render
public void Render()
{
WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
var title = InputString("Title"); //đọc vào biến title
var authors = InputString("Authors"); //đọc vào biến authors
var publisher = InputString("Publisher"); //đọc vào biến publisher
var year = InputInt("Year"); // nhập giá trị cho biến year
var edition = InputInt("Edition"); // nhập giá trị cho biến edition
var tags = InputString("Tags");
var description = InputString("Description");
var rate = InputInt("Rate");

395
var reading = InputBool("Reading");
var tập tin = InputString("File");
}
Code đầy đủ của lớp BookCreateView như sau:
using System;
namespace BookMan.ConsoleApp.Views
{
/// <summary>
/// class để thêm một cuốn sách mới
/// </summary>
internal class BookCreateView
{
public BookCreateView()
{
}
/// <summary>
/// yêu cầu người dùng nhập từng thông tin và lưu lại thông tin đó
/// </summary>
public void Render()
{
WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
var title = InputString("Title"); //đọc vào biến title
var authors = InputString("Authors"); //đọc vào biến authors
var publisher = InputString("Publisher"); //đọc vào biến publisher
var year = InputInt("Year"); // nhập giá trị cho biến year
var edition = InputInt("Edition"); // nhập giá trị cho biến edition
var tags = InputString("Tags");
var description = InputString("Description");
var rate = InputInt("Rate");
var reading = InputBool("Reading");
var file = InputString("File");
}
/// <summary>
/// xuất thông tin ra console với màu sắc (WriteLine có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
private void WriteLine(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}
/// <summary>
/// xuất thông tin ra console với màu sắc (Write có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
private void Write(string message, ConsoleColor color = ConsoleColor.White,
bool resetColor = true)
{
Console.ForegroundColor = color;
Console.Write(message);

396
if (resetColor)
Console.ResetColor();
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// rồi chuyển sang kiểu bool
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private bool InputBool(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor =
ConsoleColor.White)
{
Write($"{label} [y/n]: ", labelColor);
ConsoleKeyInfo key = Console.ReadKey(); //đọc 1 ký tự vào biến key
Console.WriteLine();
bool @char = key.KeyChar == 'y' || key.KeyChar == 'Y' ?
true : false; //chuyển sang kiểu bool dùng biểu thức điều kiện
return @char; // lưu ý cách viết tên biến @char
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// rồi chuyển sang số nguyên
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private int InputInt(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor =
ConsoleColor.White)
{
while (true)
{
var str = InputString(label, labelColor, valueColor);
var result = int.TryParse(str, out int i);
if (result == true)
{
return i;
}
}
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
private string InputString(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor =
ConsoleColor.White)
{
Write($"{label}: ", labelColor, false);
Console.ForegroundColor = valueColor;
string value = Console.ReadLine();

397
Console.ResetColor();
return value;
}
}
}

Bước 3. Dịch và chạy thử chương trình


Chạy thử lệnh create như sau:

Kết quả chạy chương trình


Để ý trong phương thức InputBool ở trên chúng ta sử dụng phép toán điều
kiện (conditional operator).
bool @char = key.KeyChar == 'y' || key.KeyChar == 'Y' ? true : false;
//chuyển sang kiểu bool dùng biểu thức điều kiện
Chúng ta đã xây dựng được một class cho phép nhập dữ liệu một cuốn sách
từ giao diện console. Qua bài này chúng ta đã học cách vận dụng biến cục
bộ, nhập dữ liệu từ console, cấu trúc điều khiển, biến đổi kiểu, tham số của
phương thức (tham số tùy chọn, tham số out).

398
View cập nhật dữ liệu (1): phương thức tĩnh
Trong các bài trước chúng ta đã xây dựng được các lớp giao diện để xuất và
nhập thông tin. Theo phân tích ở bài đầu tiên, chúng ta phải cung cấp cho
người dùng khả năng cập nhật thông tin của một cuốn sách đã có sẵn.
Trong bài học này chúng ta sẽ sử dụng kỹ thuật xây dựng phương thức tĩnh
(static method) trong C# để xây dựng một class view mới giúp người dùng
cập nhật thông tin của sách.

Thực hành 1: xây dựng lớp BookUpdateView


Trong bài này, chúng ta sẽ xây dựng một lớp giao diện nữa giúp cập nhật
thông tin.
Bước 1. Tạo tập tin mã nguồn mới BookUpdateView.cs
Tạo lớp BookUpdateView trong tập tin BookUpdateView.cs trong thư mục
Views.
Viết code cho lớp BookUpdateView như sau
using System;
namespace BookMan.ConsoleApp.Views
{
using Models;
class BookUpdateView
{
protected Book Model;
public BookUpdateView(Book model)
{
Model = model;
}
public void Render()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("UPDATE BOOK INFORMATION");
Console.ResetColor();
// hiển thị giá trị cũ
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("Authors: ");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(Model.Authors);
// yêu cầu nhập giá trị mới
Console.ForegroundColor = ConsoleColor.Magenta;
Console.Write("New value: ");
Console.ResetColor();
// đọc giá trị mới
var str = Console.ReadLine();
/* nếu người dùng ấn enter luôn (bỏ qua nhập liệu) thì lấy lại giá trị cũ
* của trường Authors gán cho biến cục bộ authors.
* Nếu người dùng nhập giá trị mới thì biến authors nhận giá trị này.
* Giá trị của biến authors về sau sẽ chuyển về controller để xử lý.
*/

399
var authors = string.IsNullOrEmpty(str.Trim()) ? Model.Authors : str;
// TẠM DỪNG .... QUÁ NHIỀU CODE LẶP
}
}
}
Ở bước này chúng ta thử nghiệm việc cập nhật giá trị trường Authors theo
logic:
1. Hiển thị giá trị gốc của trường Authors, sau đó yêu cầu người dùng
nhập giá trị mới cho từng thuộc tính.
2. Nếu người dùng ấn enter luôn (bỏ qua nhập dữ liệu) thì lấy lại giá trị
cũ của trường Authors (trong object Model) để gán cho biến cục bộ
authors.
3. Giá trị của authors về sau sẽ truyền về controller (object của
BookController) để thực sự thực hiện cập nhật.
Tuy nhiên, code đang bị lặp để hiển thị giao diện nhiều màu sắc. Chúng ta
tạm dừng code cho class này để thực hiện một kỹ thuật mới.
Bước 2. Bổ sung thêm phương thức Update vào lớp BookController
/// <summary>
/// kích hoạt chức năng cập nhật
/// </summary>
/// <param name="id"></param>
public void Update(int id)
{
var model = new Book();
var view = new BookUpdateView(model);
view.Render();
}

Bước 3. Bổ sung thêm một “case” vào phương thức Main


while (true)
{
Console.Write("Request> ");
string request = Console.ReadLine();
switch (request.ToLower())
{
case "single":
controller.Single(1);
break;
case "create":
controller.Create();
break;
case "update":
controller.Update(1);
break;
default:
Console.WriteLine("Unknown command");
break;
}
}

400
Nhắc lại, ở bài trước để tránh lặp code viết chữ ra console với màu sắc chúng
ta đã xây dựng một phương thức riêng trong lớp BookSingleView như sau:
protected void WriteLine(string message, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
Console.ResetColor();
}
Ở bài này, chúng ta cũng viết hai phương thức tương tự trong lớp
BookCreateView
private void WriteLine(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}
private void Write(string message, ConsoleColor color = ConsoleColor.White,
bool resetColor = true)
{
Console.ForegroundColor = color;
Console.Write(message);
if (resetColor)
Console.ResetColor();
}
Ở đây chúng ta đã viết lặp lại một phương thức trong 2 class.
Nếu để ý nữa chúng ta thấy các phương thức này không hề sử dụng biến
thành viên của class chứa chúng. Chỉ cần cung cấp tham số đầu vào phù
hợp là sử dụng được hai phương thức này.
Đây là các phương thức phù hợp để chuyển đổi thành phương thức
tĩnh trong một class riêng, sao cho tất cả các lớp view đều có thể sử dụng.

Thực hành 2: cải tiến lớp BookUpdateView sử dụng phương


thức tĩnh
Bước 1. Tạo class ViewHelp
Trong thư mục Framework tạo mới tập tin mã nguồn ViewHelp.cs cho
class ViewHelp.
Viết code cho class này như sau (có thể cut & paste hai phương thức này từ
lớp BookCreateView cho nhanh)
using System;
namespace Framework
{
public static class ViewHelp
{
/// <summary>

401
/// xuất thông tin ra console với màu sắc (WriteLine có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
public static void WriteLine(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}
/// <summary>
/// xuất thông tin ra console với màu sắc (Write có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
public static void Write(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.Write(message);
if (resetColor)
Console.ResetColor();
}
}
}
Lưu ý từ khóa static được đặt trước kiểu trả về. ViewHelp cũng đồng thời là
một static class.
Bước 2. Điều chỉnh code của class BookUpdateView
using Framework;
using System;
namespace BookMan.ConsoleApp.Views
{
using Models;
internal class BookUpdateView
{
protected Book Model;
public BookUpdateView(Book model)
{
Model = model;
}
public void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green); //sử
dụng phương thức static
ConsoleColor labelColor = ConsoleColor.Magenta, valueColor =
ConsoleColor.White;
// hiển thị giá trị cũ
ViewHelp.Write("Authors: ", labelColor); //sử dụng phương thức static
ViewHelp.WriteLine(Model.Authors, valueColor); //sử dụng phương thức
static
// yêu cầu nhập giá trị mới

402
ViewHelp.Write("New value: ", labelColor); //sử dụng phương thức static
// đọc giá trị mới
var str = Console.ReadLine();
/* nếu người dùng ấn enter luôn (bỏ qua nhập liệu) thì lấy lại giá trị cũ
* của trường Authors gán cho biến cục bộ authors.
* Nếu người dùng nhập giá trị mới thì biến authors nhận giá trị này.
* Giá trị của biến authors về sau sẽ chuyển về controller để xử lý.
*/
var authors = string.IsNullOrEmpty(str.Trim()) ? Model.Authors : str;
// TẠM DỪNG .... VẪN CÒN NHIỀU CODE LẶP
}
}
}
Lưu ý cách sử dụng phương thức static.

Thực hành 3: cải tiến lớp BookSingleView và


BookCreateView sử dụng ViewHelp
Bước 1. Cải tiến lớp BookSingleView
using System;
using Framework;
namespace BookMan.ConsoleApp.Views // chú ý cách Visual Studio đặt tên namespace
{
using Models; // chú ý cách dùng using bên trong namespace
/// <summary>
/// class để hiển thị một cuốn sách
/// </summary>
internal class BookSingleView
{
protected Book Model; // biến này để lưu trữ thông tin cuốn sách đang cần hiển
thị
/// <summary>
/// đây là phương thức khởi tạo, sẽ được gọi đầu tiên
khi tạo object
/// </summary>
/// <param name="model">cuốn sách cụ thể sẽ được hiển
thị</param>
public BookSingleView(Book model)
{
Model = model; // chuyển dữ liệu từ tham số sang biến thành viên để sử
dụng trong toàn class
}
/// <summary>
/// thực hiện in thông tin ra màn hình console
/// </summary>
public void Render()
{
if (Model == null) // kiếm tra xem có dữ liệu không
{
// sử dụng phương thức tĩnh WriteLine của lớp ViewHelp
ViewHelp.WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);
return; // kết thúc thực hiện phương thức (bỏ qua phần còn lại)
}
// sử dụng phương thức tĩnh WriteLine của lớp ViewHelp
ViewHelp.WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);
/* các dòng dưới đây viết ra thông tin cụ thể theo từng dòng

403
* sử dụng cách tạo xâu kiểu "interpolation"
* và dùng dấu cách để căn chỉnh tạo thẩm mỹ
*/
Console.WriteLine($"Authors: {Model.Authors}");
Console.WriteLine($"Title: {Model.Title}");
Console.WriteLine($"Publisher: {Model.Publisher}");
Console.WriteLine($"Year: {Model.Year}");
Console.WriteLine($"Edition: {Model.Edition}");
Console.WriteLine($"Isbn: {Model.Isbn}");
Console.WriteLine($"Tags: {Model.Tags}");
Console.WriteLine($"Description: {Model.Description}");
Console.WriteLine($"Rating: {Model.Rating}");
Console.WriteLine($"Reading: {Model.Reading}");
Console.WriteLine($"File: {Model.File}");
Console.WriteLine($"File Name: {Model.FileName}");
}
}
}
Ở bước này chúng ta sử dụng phương thức tĩnh ViewHelp.WriteLine thay
cho phương thức cục bộ WriteLine xây dựng trong bài trước. Phương thức
cục bộ WriteLine có thể xóa bỏ cho gọn code vì giờ không cần dùng đến
nữa.
Bước 2. Xây dựng tiếp một số phương thức static cho giao diện
Cut/paste các phương thức InputString, InputInt, InputBool từ
lớp BookCreateView sang lớp ViewHelp và chuyển thành phương thức tĩnh
public
using System;
namespace Framework
{
public static class ViewHelp
{
/// <summary>
/// xuất thông tin ra console với màu sắc (WriteLine có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
public static void WriteLine(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (resetColor)
Console.ResetColor();
}
/// <summary>
/// xuất thông tin ra console với màu sắc (Write có màu)
/// </summary>
/// <param name="message">thông tin cần xuất</param>
/// <param name="color">màu chữ</param>
/// <param name="resetColor">trả lại màu mặc định hay không</param>
public static void Write(string message, ConsoleColor color =
ConsoleColor.White, bool resetColor = true)
404
{
Console.ForegroundColor = color;
Console.Write(message);
if (resetColor)
Console.ResetColor();
}
/// <summary>
/// in ra thông báo, chờ người dùng bấm phím bất kỳ.
/// Nếu bấm 'y' sẽ trả về true, bấm phím khác sẽ trả về false
/// </summary>
/// <param name="label"></param>
/// <param name="labelColor"></param>
/// <param name="valueColor"></param>
/// <returns></returns>
public static bool InputBool(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{
Write($"{label} [y/n]: ", labelColor); //phương thức tĩnh gọi phương thức
tĩnh khác trong cùng class
ConsoleKeyInfo key = Console.ReadKey(); //đọc 1 ký tự vào biến key
Console.WriteLine();
bool @char = key.KeyChar == 'y' || key.KeyChar == 'Y' ?
true : false; //chuyển sang kiểu bool dùng biểu thức điều kiện
return @char; // lưu ý cách viết tên biến @char
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// rồi chuyển sang số nguyên
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
public static int InputInt(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{
while (true)
{
var str = InputString(label, labelColor, valueColor); //phương thức
tĩnh gọi phương thức tĩnh khác trong cùng class
var result = int.TryParse(str, out int i);
if (result == true)
{
return i;
}
}
}
/// <summary>
/// in ra thông báo và tiếp nhận chuỗi ký tự người dùng nhập
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ người dùng nhập</param>
/// <returns></returns>
public static string InputString(string label, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)
{

405
Write($"{label}: ", labelColor, false); //phương thức tĩnh gọi phương thức
tĩnh khác trong cùng class
Console.ForegroundColor = valueColor;
string value = Console.ReadLine();
Console.ResetColor();
return value;
}
}
}
Lý do chúng ta chuyển hàng loạt phương thức xuất nhập về lớp ViewHelp là
vì các phương thức này có thể hoạt động độc lập (không cần biết về trạng
thái của object nào), không phụ thuộc nghiệp vụ của bài toán (có thể tái sử
dụng trong các dự án khác), và cần thiết cho nhiều class sau này sẽ xây
dựng.
Việc dồn các phương thức hỗ trợ giao diện này vào một class chung giúp
giảm số lượng code đáng kể ở các lớp view.
Bước 3. Cải tiến lớp BookCreateView
using Framework;
using System;
namespace BookMan.ConsoleApp.Views
{
/// <summary>
/// class để thêm một cuốn sách mới
/// </summary>
internal class BookCreateView
{
public BookCreateView() { }
/// <summary>
/// yêu cầu người dùng nhập từng thông tin và lưu lại thông tin đó
/// </summary>
public void Render()
{
ViewHelp.WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
var title = ViewHelp.InputString("Title"); //đọc vào biến title
var authors = ViewHelp.InputString("Authors"); //đọc vào biến authors
var publisher = ViewHelp.InputString("Publisher"); //đọc vào biến
publisher
var year = ViewHelp.InputInt("Year"); //nhập giá trị cho biến year
var edition = ViewHelp.InputInt("Edition"); //nhập giá trị cho biến
edition
var tags = ViewHelp.InputString("Tags");
var description = ViewHelp.InputString("Description");
var rate = ViewHelp.InputInt("Rate");
var reading = ViewHelp.InputBool("Reading");
var file = ViewHelp.InputString("File");
}
}
}
Ở bước này chúng ta thay các phương thức cục bộ InputString, InputBool,
InputInt bằng phương thức tĩnh tương ứng của lớp ViewHelp. Bạn để ý

406
thấy tình trạng lặp code đã giảm đáng kể. Bản thân các class view giờ đã
rất gọn gàng.

Kết luận
Trong bài học này chúng ta đã vận dụng phương thức tĩnh để tạo ra một
lớp hỗ trợ giao diện. Chúng ta đã sử dụng lớp hỗ trợ này để cải tiến hai lớp
view cũ và để xây dựng lớp view mới giúp cập nhật thông tin sách.
Việc sử dụng phương thức tĩnh ở đây giúp chúng ta tránh lặp code và có thể
tái sử dụng qua các lớp view sau này.
Cũng lưu ý rằng, hai class BookCreateView và BookUpdateView hiện thời
chưa thực hiện được trọn vẹn nhiệm vụ của mình. Bạn sẽ quay lại với hai
class này trong bài học về Router.

407
View cập nhật dữ liệu (2): nạp chồng, phương thức
mở rộng
Trong bài học này chúng ta sẽ học thêm kỹ thuật nạp chồng phương
thức (method overloading) và phương thức mở rộng (extension method)
của C#. Chúng ta sẽ vận dụng các kỹ thuật này để tiếp tục cải tiến lớp view
cập nhật thông tin.

Thực hành 1: cải tiến tiếp lớp ViewHelp và


BookUpdateView
Bước 1. Bổ sung phương thức vào lớp ViewHelp
Bổ sung phương thức sau đây vào cuối class ViewHelp
/// <summary>
/// cập nhật giá trị kiểu string. Nếu ấn enter mà không nhập dữ liệu sẽ trả
lại giá trị cũ.
/// </summary>
/// <param name="label">dòng thông báo</param>
/// <param name="oldValue">giá trị gốc</param>
/// <param name="labelColor">màu chữ thông báo</param>
/// <param name="valueColor">màu chữ dữ liệu</param>
/// <returns></returns>
public static string InputString(string label, string oldValue, ConsoleColor
labelColor = ConsoleColor.Magenta,
ConsoleColor valueColor = ConsoleColor.White)
{
Write($"{label}: ", labelColor);
WriteLine(oldValue, ConsoleColor.Yellow);
Write("New value >> ", ConsoleColor.Green);
Console.ForegroundColor = valueColor;
string newValue = Console.ReadLine();
return string.IsNullOrEmpty(newValue.Trim()) ? oldValue : newValue;
}
Đây là một overload khác của phương thức InputString mà bạn đã tạo từ
các bài học trước. Overload này có nhiệm vụ cập nhật giá trị cho một biến
kiểu string.
Phương thức này cập nhật một chuỗi ký tự từ console theo logic sau:
1. Viết dòng thông báo để người dùng nhập dữ liệu (tham số label)
2. Nếu người dùng nhập một chuỗi ký tự thì chuỗi mới sẽ được trả lại
3. Nếu ấn enter luôn (tức là nhập chuỗi trống) thì trả lại giá trị cũ (tham
số oldValue).
Bước 2. Cải tiến lớp BookUpdateView
Điều chỉnh phương thức Render để sử dụng các phương thức InputString
vừa tạo.
408
using Framework;
using System;
namespace BookMan.ConsoleApp.Views
{
using Models;
internal class BookUpdateView
{
protected Book Model;
public BookUpdateView(Book model)
{
Model = model;
}
public void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
var authors = ViewHelp.InputString("Authors", Model.Authors);
var title = ViewHelp.InputString("Title", Model.Title);
var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
var tags = ViewHelp.InputString("Tags", Model.Tags);
var description = ViewHelp.InputString("Description", Model.Description);
var tập tin = ViewHelp.InputString("File", Model.File);
// TẠM DỪNG ....
}
}
}
Ở bước này chúng ta sử dụng phương thức InputString của lớp ViewHelp
để cập nhật dữ liệu cho các trường thuộc kiểu string của lớp Book.
Bước 3. Dịch và chạy thử với lệnh update

Kết quả hoạt động của chương trình


Trong phần thực hành trên chúng ta viết thêm một phương
thức InputString nữa trong class ViewHelp. Lưu ý rằng, trong class đã có

409
sẵn một phương thức có tên InputString. Đây là nạp chồng phương
thức (method overloading).
Khi sử dụng phương thức có nạp chồng, Intellisense của Visual Studio sẽ
hiển thị số lượng các phiên bản nạp chồng và cho phép lựa chọn phiên bản
muốn sử dụng.

Cách Visual Studio hiển thị các phương thức nạp chồng

Hai phương thức InputString trong lớp ViewHelp có nguyên mẫu


(prototype) lần lượt như sau:
string InputString(string label, ConsoleColor labelColor = ConsoleColor.Magenta,
ConsoleColor valueColor = ConsoleColor.White)
string InputString(string label, string oldValue, ConsoleColor labelColor =
ConsoleColor.Magenta, ConsoleColor valueColor = ConsoleColor.White)

Như vậy, danh sách tham số của hai phương thức này là khác nhau (trước
hết là về số lượng).

Thực hành 2: tiếp tục cải tiến lớp BookUpdateView


Bước 1. Xây dựng lớp Extension
Thêm tập tin mã nguồn mới Extension.cs trong thư mục Framework. Trong
tập tin này định nghĩa một class Extension như sau:
namespace Framework
{
/// <summary>
/// Mộc số phương thức mở rộng để biến đổi kiểu dữ liệu
/// </summary>
public static class Extension
{
/// <summary>
/// Biến đổi từ chuỗi sang số nguyên
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static int ToInt(this string value)
{
return int.Parse(value);
}
/// <summary>
/// Biến đổi từ chuỗi sang số nguyên
/// </summary>
/// <param name="value"></param>
/// <param name="i"></param>

410
/// <returns></returns>
public static bool ToInt(this string value, out int i)
{
return int.TryParse(value, out i);
}
/// <summary>
/// Biến đổi chuỗi Y,y,True, true, sang true
/// Các chuỗi khác thành false
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static bool ToBool(this string value)
{
var v = value.ToLower();
if (v == "y" || v == "true") return true;
return false;
}
/// <summary>
/// Biến đổi true/false thành Yes/No hoặc có/không
/// </summary>
/// <param name="value"></param>
/// <param name="format">y/n hoặc c/k</param>
/// <returns></returns>
public static string ToString(this bool value, string format)
{
if (format == "y/n")
return value ? "Yes" : "No";
if (format == "c/k")
return value ? "Có" : "Không";
return value ? "True" : "False";
}
}
}

Bước 2. Bổ sung thêm phương thức vào lớp ViewHelp


Bổ sung thêm các overload sau vào lớp ViewHelp:
public static int InputInt(string label, int oldValue, ConsoleColor labelColor
= ConsoleColor.Magenta, ConsoleColor valueColor =
ConsoleColor.White)
{
Write($"{label}: ", labelColor);
WriteLine($"{oldValue}", ConsoleColor.Yellow);
Write("New value >> ", ConsoleColor.Green);
Console.ForegroundColor = valueColor;
string str = Console.ReadLine();
if (string.IsNullOrEmpty(str)) return oldValue;
if (str.ToInt(out int i)) return i; //sử dụng phương thức mở rộng ToInt
return oldValue;
}
public static bool InputBool(string label, bool oldValue, ConsoleColor
labelColor = ConsoleColor.Magenta, ConsoleColor
valueColor = ConsoleColor.White)
{
Write($"{label}: ", labelColor);
//sử dụng phương thức mở rộng ToString
WriteLine(oldValue.ToString("y/n"), ConsoleColor.Yellow);
Write("New value >> ", ConsoleColor.Green);

411
Console.ForegroundColor = valueColor;
string str = Console.ReadLine();
if (string.IsNullOrEmpty(str)) return oldValue;
return str.ToBool(); //sử dụng phương thức mở rộng ToBool
}

Bước 3. Cập nhật phương thức Render của lớp BookUpdateView


using Framework;
using System;
namespace BookMan.ConsoleApp.Views
{
using Models;
internal class BookUpdateView
{
protected Book Model;
public BookUpdateView(Book model)
{
Model = model;
}
public void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green); //sử
dụng phương thức static
var authors = ViewHelp.InputString("Authors", Model.Authors);
var title = ViewHelp.InputString("Title", Model.Title);
var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
var tags = ViewHelp.InputString("Tags", Model.Tags);
var description = ViewHelp.InputString("Description", Model.Description);
var file = ViewHelp.InputString("File", Model.File);
var year = ViewHelp.InputInt("Year", Model.Year);
var edition = ViewHelp.InputInt("Edition", Model.Edition);
var rating = ViewHelp.InputInt("Rate", Model.Rating);
var reading = ViewHelp.InputBool("Reading", Model.Reading);
}
}
}

412
Bước 4. Dịch và chạy thử chương trình với lệnh update

Kết quả thực hiện chương trình


Trong bước 1 của thực hành trên chúng ta gặp bốn phương thức trong static
class Extension:
public static int ToInt(this string value)
{
return int.Parse(value);
}
public static bool ToInt(this string value, out int i)
{
return int.TryParse(value, out i);
}
public static bool ToBool(this string value)
{
var v = value.ToLower();
if (v == "y" || v == "true") return true;
return false;
}
public static string ToString(this bool value, string format)
{
if (format == "y/n")
return value ? "Yes" : "No";
if (format == "c/k")
return value ? "Có" : "Không";
return value ? "True" : "False";
}
Đây là các phương thức thuộc loại phương thức mở rộng (extension
method).
413
View danh sách: kiểu mảng, cấu trúc lặp
Trong hai bài trước chúng ta đã xây dựng các lớp view để hiển thị một cuốn
sách, nhập dữ liệu cho một cuốn sách, và cập nhật thông tin của một cuốn
sách.
Bởi vì chúng ta phải quản lý nhiều cuốn sách điện tử, chúng ta sẽ phải tiếp
tục xây dựng một class mới để hiển thị một danh sách các cuốn sách điện
tử.
Qua bài này chúng ta sẽ làm việc với dữ liệu kiểu mảng, và các cấu trúc
lặp (while, do-while, for, foreach).

Thực hành 1: xây dựng lớp view để hiển thị danh sách Book
Tạo tập tin mã nguồn mới BookListView.cs cho lớp BookListView và viết
code như sau:
using System;
using Framework;
namespace BookMan.ConsoleApp.Views
{
using Models;
/// <summary>
/// class để hiển thị danh sách Book
/// </summary>
internal class BookListView
{
protected Book[] Model; // mảng của các object kiểu Book
/// <summary>
/// phương thức khởi tạo
/// </summary>
/// <param name="model">danh sách object kiểu Book</param>
public BookListView(Book[] model)
{
Model = model;
}
/// <summary>
/// in danh sách ra console
/// </summary>
public void Render()
{
if (Model.Length == 0)
{
ViewHelp.WriteLine("No book found!", ConsoleColor.Yellow);
return;
}
ViewHelp.WriteLine("THE BOOK LIST", ConsoleColor.Green);
int i = 0;
while (i < Model.Length)
{
ViewHelp.Write($"[{Model[i].Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {Model[i].Title}", Model[i].Reading ?
ConsoleColor.Cyan : ConsoleColor.White);
i++;

414
}
}
}
}
Ở đây bạn khai báo một biến mảng một chiều: Book[] model. Mỗi phần tử
của mảng thuộc kiểu Book.
Bạn cũng dùng đến cấu trúc điều khiển lặp để duyệt mảng này.
Sử dụng các cấu trúc lặp
Trong phần thực hành 1 chúng ta thấy rằng để làm việc với dữ liệu mảng
bắt buộc phải có một cấu trúc giúp chúng ta lần lượt làm việc với từng phần
tử của mảng. Ứng với mỗi phần tử sẽ cùng áp dụng chung một nhóm lệnh.
Để thực hiện yêu cầu đó cần sử dụng một trong các cấu trúc lặp.
C# cung cấp 4 cấu trúc lặp khác nhau: do-while, while, for, foreach.
Cấu trúc while
Trong phương thức Render ở trên chúng ta đang sử dụng cấu trúc while.
int i = 0;
while (i < Model.Length)
{
ViewHelp.Write($"[{Model[i].Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {Model[i].Title}", Model[i].Reading ?
ConsoleColor.Cyan : ConsoleColor.White);
i++;
}
Trong cấu trúc while, danh sách lệnh có thể không được thực hiện lần nào.
Tình huống này xảy ra khi biểu thức logic nhận giá trị false ngay từ đầu.
Cấu trúc do-while
Chúng ta có thể viết lại thân phương thức Render với vòng lặp do-while như
sau:
int i = 0;
do
{
ViewHelp.Write($"[{Model[i].Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {Model[i].Title}", Model[i].Reading ?
ConsoleColor.Cyan : ConsoleColor.White);
i++;
} while (i < Model.Length);
Cấu trúc do-while khác biệt với while ở chỗ, danh sách lệnh sẽ được thực
hiện trước, sau đó mới kiểm tra giá trị của biểu thức logic.
Khi sử dụng cấu trúc do-while, danh sách lệnh luôn luôn thực hiện ít nhất
một lần. Do đó, cần lưu ý trong trường hợp mảng rỗng (chưa có phần tử
nào), truy cập vào phần tử của mảng rỗng sẽ gây ra lỗi.

415
Trong trường hợp phương thức Render sử dụng do-while như trên, nếu
mảng Model rỗng thì sẽ gây ra lỗi.
Khi duyệt mảng với while hoặc do-while, chúng ta đều phải tự khai báo
một biến điều khiển và gán cho nó giá trị 0, là giá trị chỉ số bắt đầu mặc
định của mảng trong C#. Trong thân vòng lặp, chúng ta phải tự tăng giá trị
của biến điều khiển thêm một đơn vị, và
1. Nếu dùng vòng lặp while, giá trị của biến điều khiển sẽ được so sánh
với độ dài của mảng trước. Nếu biến điều khiển nhỏ hơn độ dài mảng,
lệnh trong thân vòng lặp sẽ được thực hiện. Ngược lại, vòng lặp sẽ
kết thúc.
2. Nếu dùng vòng lặp do-while, lệnh trong thân vòng lặp sẽ luôn thực
hiện trước, sau đó mới kiểm tra xem biến điều khiển có nhỏ hơn độ
dài mảng hay không. Nếu biến điều khiển vẫn nhỏ hơn độ dài mảng,
một chu kỳ mới sẽ bắt đầu. Ngược lại, vòng lặp sẽ kết thúc. Vì lý do
này, nếu mảng rỗng, trong thân vòng lặp vẫn cố gắng truy cập vào
phần tử số 0, vốn không tồn tại, và gây lỗi.
Cấu trúc for
Thân phương thức Render có thể viết lại với vòng lặp for như sau:
for (int i = 0; i < Model.Length; i++)
{
ViewHelp.WriteLine($"[{Model[i].Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {Model[i].Title}", Model[i].Reading ?
ConsoleColor.Cyan : ConsoleColor.White);
}
Cấu trúc này sẽ thực hiện danh sách lệnh một số lần xác định (trong khi hai
cấu trúc trên không xác định được số lần thực hiện).
Trong cấu trúc for, biến điều khiển, cách thay đổi giá trị của biến điều khiển
cũng như điều kiện kiểm tra biến điều khiển đều viết chung trong khai báo.
C# sẽ tự thay đổi giá trị biến điều khiển theo công thức chúng ta cung cấp.
Cấu trúc for đặc biệt phù hợp để duyệt các phần tử mảng.
Cấu trúc foreach
Thân phương thức Render có thể viết lại với vòng lặp foreach như sau:
foreach (Book b in Model)
{
ViewHelp.Write($"[{b.Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {b.Title}", b.Reading ? ConsoleColor.Cyan :
ConsoleColor.White);
}
Đây là một cấu trúc riêng của C#, trong đó C# sẽ tự động duyệt qua danh
sách phần tử của tập hợp. Giá trị của mỗi phần tử sẽ lần lượt đưa vào biến
và bắt đầu một chu kỳ. Biến này có thể được sử dụng trong thân vòng lặp.
416
Trong cấu trúc này, nếu tập hợp có bao nhiêu phần tử thì sẽ danh sách lệnh
sẽ được thực hiện chừng ấy lần.
Đây là loại cấu trúc lặp an toàn và ngắn gọn nhất trong C#.
Khi sử dụng cấu trúc này, C# sẽ tự duyệt qua mảng Model. Với mỗi phần
tử có trong Model, thân vòng lặp (lệnh WriteLine) sẽ được thực hiện một
lần. Giá trị của phần tử đó sẽ chuyển sang biến b và có thể được sử dụng
trong thân vòng lặp.
Cấu trúc này loại bỏ việc sử dụng phép toán chỉ số, vốn nguy hiểm nếu như
tập rỗng, cũng như giảm số lượng code cần viết.

Thực hành 2: khai báo và khởi tạo mảng của các object kiểu
Book trong controller
Bước 1. Bổ sung phương thức vào lớp BookController
Bổ sung phương thức List như dưới đây vào lớp BookController:
/// <summary>
/// kích hoạt chức năng hiển thị danh sách
/// </summary>
public void List()
{
/* khai báo và khởi tạo một mảng, mỗi phần tử thuộc kiểu Book.
* Lệnh dưới dây khai báo và khởi tạo 1 mảng gồm 6 phần tử,
* mỗi phần tử thuộc kiểu Book.
* Do Book là class, mỗi phần tử của mảng cũng phải được khởi tạo
* sử dụng từ khóa new, tương tự như khởi tạo một object bình thường
*/
Book[] model = new Book[]
{
new Book{Id=1, Title = "A new book 1"},
new Book{Id=2, Title = "A new book 2"},
new Book{Id=3, Title = "A new book 3"},
new Book{Id=4, Title = "A new book 4"},
new Book{Id=5, Title = "A new book 5"},
new Book{Id=6, Title = "A new book 6"},
};
BookListView view = new BookListView(model);
view.Render();
}

Bước 2. Điều chỉnh phương thức Main của lớp Program


Bổ sung thêm một “case” nữa vào phương thức Main để chạy lệnh hiển thị
danh sách:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp
{
417
using Controllers;
internal class Program
{
private static void Main(string[] args)
{
BookController controller = new BookController();
while (true)
{
Console.Write("Request> ");
string request = Console.ReadLine();
switch (request.ToLower())
{
case "single":
controller.Single(1);
break;
case "create":
controller.Create();
break;
case "update":
controller.Update(1);
break;
case "list":
controller.List();
break;
default:
Console.WriteLine("Unknown command");
break;
}
}
}
}
}

Bước 3. Dịch và chạy thử chương trình


Thử nghiệm lệnh list.

Kết quả thực hiện chương trình

418
Repository và quản lý dữ liệu: generic collection List
Trong bài này chúng ta sẽ học và vận dụng kỹ thuật lập trình tổng quát
(generic) và phương thức tổng quát để xây dựng class quản lý dữ liệu. Các
ứng dụng quản lý (nói chung) thường sử dụng một (nhóm) class để quản lý
tập trung việc truy xuất dữ liệu. Mô hình quản lý dữ liệu tập trung như vậy
có tên gọi là Repository. Trong dự án này, chúng ta sẽ vận dụng hình thức
đơn giản của mô hình này giúp việc quản lý dữ liệu hiệu quả hơn. Ngoài ra,
bạn sẽ học cách sử dụng lớp List<T> để lưu trữ danh sách object dữ liệu
thay cho mảng.
Trong các bài trước chúng ta đã xây dựng lớp thực thể để mô tả dữ liệu cần
quản lý. Chúng ta cũng xây dựng các lớp giao diện để hiển thị dữ liệu theo
các hình thức khác nhau cùng các lớp cho phép người dùng tương tác với
dữ liệu.
Tuy nhiên chúng ta chưa thực sự quản lý được dữ liệu. Chúng ta mới chỉ tạo
ra một số dữ liệu thử nghiệm trực tiếp trong phương thức của controller.
Cách thức tạo ra và xử lý dữ liệu này không phù hợp với một ứng dụng quản
lý, cụ thể:
1. Dữ liệu chỉ tồn tại khi chương trình hoạt động: mọi thay đổi trên dữ
liệu sẽ mất đi khi đóng chương trình;
2. Dữ liệu tạo ra ở dạng thức rời rạc: dữ liệu chỉ phục vụ riêng cho mỗi
phương thức nhằm mục đích thử nghiệm hoạt động của các view;
3. Chưa thể thực hiện các thao tác quản lý thông thường với dữ liệu:
chưa thực hiện được việc thêm, sửa, xóa, cập nhật;
4. Chưa đảm bảo được khả năng thay thế nguồn dữ liệu: cụ thể, cần
phải có khả năng thay đổi giữa dữ liệu thử nghiệm là danh sách các
object trong bộ nhớ, truy xuất dữ liệu từ tập tin, qua dịch vụ mạng
hoặc từ cơ sở dữ liệu quan hệ.

Mô hình repository
Đối với các ứng dụng quản lý chúng ta phải tổ chức code quản lý dữ liệu
đảm bảo các yêu cầu: tập trung, có khả năng lưu trữ lâu dài, dễ dàng thực
hiện hoặc bổ sung các thao tác quản lý (như lọc, nhóm dữ liệu,...), dễ dàng
thay đổi cách thức truy xuất dữ liệu.
Do đó, chúng ta phải xây dựng các lớp riêng để hỗ trợ quản lý dữ liệu.
Thành phần hỗ trợ quản lý dữ liệu như vậy không thuộc về kiến trúc MVC
mà có liên quan đến một mô hình thiết kế (design pattern) được gọi là mô

419
hình repository. Mô hình này được sử dụng đặc biệt phổ biến với các ứng
dụng quản lý.
Repository giúp tách rời phần xử lý/truy xuất dữ liệu khỏi giao diện người
dùng. Do đó, có thể tái sử dụng repository trong nhiều dự án khác nhau,
cho nhiều loại ứng dụng khác nhau về giao diện người dùng.
Trong bài này chúng ta sẽ cùng xây dựng một nhóm class, gọi chung là dịch
vụ dữ liệu (data service) cho mục đích vừa nêu trên, cụ thể hơn:
1. Chứa code để quản lý danh sách thực thể (sách/giá sách) với các thao
tác xử lý trên dữ liệu (thêm, sửa, xóa, lọc, lưu trữ, truy xuất,...);
2. Chứa code để thực hiện truy xuất đến các nguồn lưu trữ dữ liệu dài
hạn (như tập tin, cơ sở dữ liệu quan hệ).

Thực hành: xây dựng repository đơn giản


Bước 1. Xây dựng lớp SimpleDataAccess
Tạo thêm thư mục DataServices trong dự án BookMan.ConsoleApp. Tạo
thêm lớp SimpleDataAccess trong thư mục mới này (trong tập tin mã nguồn
SimpleDataAccess.cs) và code như sau:
using System.Collections.Generic;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public class SimpleDataAccess
{
public List<Book> Books { get; set; }
public void Load()
{
Books = new List<Book>
{
new Book{Id=1, Title = "A new book 1"},
new Book{Id=2, Title = "A new book 2"},
new Book{Id=3, Title = "A new book 3"},
new Book{Id=4, Title = "A new book 4"},
new Book{Id=5, Title = "A new book 5"},
new Book{Id=6, Title = "A new book 6"},
new Book{Id=7, Title = "A new book 7"},
new Book{Id=8, Title = "A new book 8"},
new Book{Id=9, Title = "A new book 9"},
};
}
public void SaveChanges() { }
}
}
Ở đây bạn đã sử dụng kiểu dữ liệu danh sách List<T>. Đây là một kiểu
thuộc nhóm generic collection.

420
Bước 2. Xây dựng lớp Repository
Tạo tập tin mã nguồn Repository.cs trong thư mục DataServices cho lớp
Repository và viết code như sau:
using System.Collections.Generic;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public class Repository
{
protected readonly SimpleDataAccess _context;
public Repository(SimpleDataAccess context)
{
_context = context;
_context.Load();
}
public void SaveChanges() => _context.SaveChanges();
public List<Book> Books => _context.Books;
public Book[] Select() => _context.Books.ToArray();
public Book Select(int id)
{
foreach (var b in _context.Books)
{
if (b.Id == id) return b;
}
return null;
}
public Book[] Select(string key)
{
var temp = new List<Book>();
var k = key.ToLower();
foreach (var b in _context.Books)
{
var logic =
b.Title.ToLower().Contains(k) ||
b.Authors.ToLower().Contains(k) ||
b.Publisher.ToLower().Contains(k) ||
b.Tags.ToLower().Contains(k) ||
b.Description.ToLower().Contains(k)
;
if (logic) temp.Add(b);
}
return temp.ToArray();
}
public void Insert(Book book)
{
var lastIndex = _context.Books.Count - 1;
var id = lastIndex < 0 ? 1 : _context.Books[lastIndex].Id + 1;
book.Id = id;
_context.Books.Add(book);
}
public bool Delete(int id)
{
var b = Select(id);
if (b == null) return false;
_context.Books.Remove(b);
return true;
421
}
public bool Update(int id, Book book)
{
var b = Select(id);
if (b == null) return false;
b.Authors = book.Authors;
b.Description = book.Description;
b.Edition = book.Edition;
b.File = book.File;
b.Isbn = book.Isbn;
b.Publisher = book.Publisher;
b.Rating = book.Rating;
b.Reading = book.Reading;
b.Tags = book.Tags;
b.Title = book.Title;
b.Year = book.Year;
return true;
}
}
}

Bước 4. Điều chỉnh BookController để sử dụng Repository


namespace BookMan.ConsoleApp.Controllers
{
using DataServices;
using Views;
using Models;
/// <summary>
/// lớp điều khiển, giúp ghép nối dữ liệu sách với giao diện
/// </summary>
internal class BookController
{
protected Repository Repository;
public BookController(SimpleDataAccess context)
{
Repository = new Repository(context);
}
/// <summary>
/// ghép nối dữ liệu 1 cuốn sách với giao diện hiển thị 1 cuốn sách
/// </summary>
/// <param name="id">mã định danh của cuốn sách</param>
public void Single(int id)
{
// lấy dữ liệu qua repository
var model = Repository.Select(id);
// khởi tạo view
BookSingleView view = new BookSingleView(model);
// gọi phương thức Render để thực sự hiển thị ra màn hình
view.Render();
}
/// <summary>
/// kích hoạt chức năng nhập dữ liệu cho 1 cuốn sách
/// </summary>
public void Create()
{
BookCreateView view = new BookCreateView();// khởi tạo object
view.Render(); // hiển thị ra màn hình
}
422
/// <summary>
/// kích hoạt chức năng hiển thị danh sách
/// </summary>
public void List()
{
// lấy dữ liệu qua repository
var model = Repository.Select();
// khởi tạo view
BookListView view = new BookListView(model);
view.Render();
}
/// <summary>
/// kích hoạt chức năng cập nhật
/// </summary>
/// <param name="id"></param>
public void Update(int id)
{
// lấy dữ liệu qua repository
var model = Repository.Select(id);
var view = new BookUpdateView(model);
view.Render();
}
}
}

Bước 5. Điều chỉnh phương thức Main


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BookMan.ConsoleApp
{
using Controllers;
using DataServices;
internal class Program
{
private static void Main(string[] args)
{
SimpleDataAccess context = new SimpleDataAccess();
BookController controller = new BookController(context);
while (true)
{
Console.Write("Request> ");
string request = Console.ReadLine();
switch (request.ToLower())
{
case "single":
controller.Single(1);
break;
case "create":
controller.Create();
break;
case "update":
controller.Update(1);
break;
case "list":
controller.List();
423
break;
default:
Console.WriteLine("Unknown command");
break;
}
}
}
}
}

Bước 5. Dịch và chạy thử chương trình


Chạy thử với các lệnh đã có (single, list, update):

Kết quả thực hiện chương trình

Repository và sơ đồ class tổng thể


Repository là một kiểu thiết kế class giúp tách rời code của chương trình
ứng dụng (tổ chức theo mô hình MVC) với phần xử lý và truy xuất dữ liệu.
Theo đó, tất cả các thao tác xử lý dữ liệu cần sử dụng trong ứng dụng đều
được tập trung ở repository.
Bản thân repository sau đó cũng gọi đến một class chuyên dụng để tải/lưu
dữ liệu (ví dụ, vào tập tin, cơ sở dữ liệu quan hệ, dịch vụ dữ liệ). Repository
cho phép dễ dàng chuyển đổi giữa các nguồn dữ liệu khác nhau (ví dụ, để
test hoặc để chạy thử ứng dụng).

424
Như phần đầu đã nói, repository không phải là một phần của kiến trúc MVC
mà thuộc hướng tiếp cận DDD (Domain-Driven Design).
Sơ đồ lớp
Qua các bài học từ đầu đến giờ chúng ta đã xây dựng nhiều class và chia
vào các nhóm theo các không gian tên, bao gồm:
 BookMan.ConsoleApp,
 BookMan.ConsoleApp.Controllers,
 BookMan.ConsoleApp.Views,
 BookMan.ConsoleApp.DataServices,
 BookMan.ConsoleApp.Models,
 Framework.
Để thấy sự phụ thuộc giữa các nhóm có thể xem sơ đồ code dưới đây:

Sơ đồ lớp tổng quan


Mũi tên biểu diễn quan hệ phụ thuộc giữa các class (“lời gọi phương thức”,
“khởi tạo object”, “tham chiếu tới”).
Chúng ta có thể thấy, khối DataServices bây giờ nằm ở vị trí trung gian giữa
Controllers và Models. Điều này có nghĩa là Controller không trực tiếp làm
các công việc xử lý dữ liệu (như khởi tạo, thêm, sửa, xóa, lọc, nhóm, cập
nhật) mà đẩy toàn bộ các công việc này cho nhóm DataServices, trong đó
có Repository và DataAccess.
Sơ đồ dưới đây biểu diễn chi tiết hơn nữa các thành phần trong dự án và
mối quan hệ giữa chúng.

425
Các sơ đồ này được tạo ra bởi công cụ Code Map của Visual Studio (Ultimate). Các phiên bản
Community và Professional không tạo ra được code map mà chỉ có thể đọc được sơ đồ này.

Sơ đồ lớp chi tiết

426
Thực thi mô hình repository
Khi sử dụng mô hình repository có hai cách thức thực thi phổ biến:
1. Xây dựng cho mỗi lớp thực thể một lớp repository riêng: ví dụ, với lớp
Book sẽ phải xây dựng BookRepository, nếu có thêm lớp thực thể
Shell (giá sách) sẽ phải xây dựng thêm ShellRepository,.... Mỗi lớp
repository này chỉ xây dựng những phương thức xử lý dữ liệu cần thiết
để sử dụng trong controller.
2. Xây dựng một lớp repository generic: lớp này chứa đầy đủ các phương
thức xử lý dữ liệu chung nhất (thêm, sửa, xóa, lọc, nhóm dữ liệu), các
lớp thực thể đều sử dụng generic repository này.
Nếu số lượng lớp thực thể ít, phương án thứ nhất sẽ phù hợp hơn; nếu số
lượng lớp thực thể lớn, phương án thứ hai phù hợp hơn.
Ngoài hai phương thức này còn có thể xây dựng một lớp repository chung
cho cả dự án. Tất cả các phương thức xử lý dữ liệu đều đặt trong lớp này.
Phương thức này chỉ áp dụng cho những bài toán nhỏ để tránh làm phức
tạp code. Chúng ta đang vận dụng phương án này trong dự án.
Ngoài ra, do repository phải đọc và ghi dữ liệu với nhiều loại nguồn dữ liệu
khác nhau trong các giai đoạn phát triển dự án, việc tương tác với các nguồn
dữ liệu khác nhau (tập tin, dịch vụ, cơ sở dữ liệu) để đọc và ghi dữ liệu được
chuyển sang một lớp trung gian (tạm gọi là lớp data access).
Ví dụ, ở giai đoạn test và thử nghiệm trong lúc code cần nguồn dữ liệu là
danh sách các object trong bộ nhớ; ở giai đoạn chạy thử nghiệm có thể sử
dụng đến nguồn dữ liệu từ dịch vụ mạng hoặc dữ liệu từ cơ sở dữ liệu quan
hệ.
Ở giai đoạn này chúng ta chỉ xây dựng một lớp truy xuất dữ liệu đơn giản
(SimpleDataAccess) chứa dữ liệu là một danh sách các object. Đến phần
sau khi học cách làm việc với tập tin và cơ sở dữ liệu chúng ta sẽ xây dựng
thêm các lớp data access khác.
Với cách tổ chức class như trên chúng ta có thể dễ dàng thay đổi thành
phần data access để làm việc với các nguồn dữ liệu khác nhau. Controller
có thể quyết định sử dụng data access nào.

427
Router (1): Kiểu từ điển, nạp chồng toán tử
Trong bài học này chúng ta sẽ học cách sử dụng kiểu từ điển (Dictionary)
để xây dựng lớp Router giúp tiếp nhận và xử lý truy vấn của người dùng.
Trong các bài trước chúng ta đã xây dựng được các thành phần chủ chốt để
tạo nên các chức năng quản lý dữ liệu cơ bản của ứng dụng. Tuy nhiên, các
thành phần này đang hoạt động rời rạc và khả năng tương tác với người
dùng hạn chế.

Xử lý truy vấn và router


Ứng dụng của chúng ta hiện nay gặp hai vấn đề nghiêm trọng.
Thứ nhất, ứng dụng chỉ tiếp nhận được một số truy vấn đơn giản (single,
create, list, update) và không tiếp nhận/xử lý được tham số đi cùng truy
vấn. Đây là một vấn đề nghiêm trọng vì nếu người dùng không cung cấp
được tham số cho lệnh sẽ hạn chế rất nhiều việc tương tác của người dùng
với ứng dụng. Ví dụ, người dùng hiện nay gọi được lệnh single nhưng không
thể cấp giá trị id của cuốn sách cần xem.
Thứ hai, các giao diện chưa thể truyền dữ liệu trở lại cho controller xử lý.
Chúng ta có lớp BookCreateView, BookUpdateView để tiếp nhận dữ liệu của
người dùng nhưng dữ liệu này chưa thể trả về BookController để xử lý. Theo
nguyên tắc của MVC, dữ liệu không được phép xử lý ở lớp giao diện mà phải
trả về cho controller xử lý.
Ngoài ra, trong phân tích hệ thống ở bài 1, chúng ta còn phải xây dựng
thêm nhiều tính năng khác. Tất cả đều yêu cầu phải tiếp nhận các truy vấn
tương đối phức tạp từ người dùng.
Vai trò của router
Theo nguyên tắc của MVC, tất cả yêu cầu của người dùng hoặc lệnh phát ra
từ giao diện đều được chuyển cho controller xử lý. Điều này có nghĩa là phải
có một cơ chế cho phép ánh xạ mỗi truy vấn của người dùng với việc thực
thi một phương thức của controller.
Trong các MVC Framework đều có một loại class đặc biệt, thường được gọi
là router, làm nhiệm vụ này.
Router trong các MVC Framework có nhiệm vụ tiếp nhận truy vấn của người
dùng, phân tích ra các thành phần chính, và gọi phương thức tương ứng của
controller. Mỗi phương thức của controller thường được gọi tắt là
mộtaction (hành động). Một truy vấn thường gọi tới một action.
Trong bài học này chúng ta sẽ xây dựng các class để tạo ra một router đáp
ứng các yêu cầu của một ứng dụng console.
428
Đặc điểm của console
Ứng dụng console thông thường hoạt động theo chế độ request/response,
nghĩa là người dùng nhập vào một truy vấn, ứng dụng thực hiện lệnh tương
ứng và trả kết quả trở lại, sau đó lại tiếp tục chờ nghe truy vấn mới.
Trong đó, mỗi truy vấn đều là một chuỗi văn bản vì console chỉ có thể
nhập/xuất chuỗi văn bản. Trong quá trình thực hiện lệnh cho đến lúc nhận
lại kết quả, giao diện sẽ “treo” và không thể tiếp nhận bất kỳ thông tin gì.
Với đặc thù này, ứng dụng dạng console thường đưa ra cấu trúc truy vấn
của riêng mình. Ví dụ, chương trình ping của Windows có cấu trúc lệnh như
sau:

Gọi chương trình ping của Windows


Tuy nhiên, truy vấn của console có một số đặc điểm chung. Ví dụ, thường
có phần “lệnh” và phần “tham số”. Phần lệnh cho chương trình biết cần làm
gì; phần tham số cung cấp thông tin cần cho việc thực hiện lệnh.
Các phần này cần được phân tách hợp lý để chương trình có thể phân tích
và lấy ra những thông tin cần thiết.
Cấu trúc truy vấn đề xuất
Trong dự án này chúng ta sẽ đưa ra cấu trúc truy vấn như sau:
lệnh ? khóa_1 = giá_trị_1 & khóa_2 = giá_trị_2
nghĩa là, một truy vấn của chúng ta sẽ bao gồm hai thành phần:

429
1. Phần lệnh (sẽ gọi là route): là một chuỗi ký tự bất kỳ không được
chứa ký tự “?”;
2. Phần tham số (sẽ gọi là parameter): là chuỗi ký tự được viết theo quy
tắc “khóa = giá_trị”; mỗi cặp này được gọi là một tham số; các tham
số viết tách nhau bởi ký tự “&”;
3. Phần lệnh và phần tham số viết tách nhau bởi ký tự “?” (vì lý do này,
ký tự “?” không được phép có mặt trong phần lệnh).
Ngoài ra cấu trúc trên còn phải có các đặc điểm sau:
1. lệnh không phân biệt ký tự hoa/thường;
2. khóa của tham số có phân biệt hoa/thường;
3. số lượng dấu cách giữa các thành phần của truy vấn là không quan
trọng.
Ví dụ
Ví dụ, chúng ta dự kiến các lệnh single, list, create, update sẽ có dạng như
sau:
single ? id = 1
list
create
update ? id = 2
Sau này chúng ta sẽ xây dựng tiếp phần trợ giúp với cấu trúc truy vấn dự
kiến như sau:
Help
Help ? cmd = single
Help ? cmd = update
Cấu trúc truy vấn này mô phỏng lại cấu trúc truy vấn GET của giao thức
HTTP.
Cấu trúc này cho phép dễ dàng phân tích các thành phần.
Với cấu trúc này, chúng ta có nhiệm vụ:
1. Phân tích một truy vấn ra thành phần lệnh và thành phần tham số;
2. Phân tích thành phần tham số và chuyển đổi về một kiểu dữ liệu khác
tiện lợi hơn cho việc lập trình.

Thực hành: xây dựng lớp hỗ trợ lưu tham số từ truy vấn
Trong phần thực hành này, chúng ta sẽ xây dựng một class cho phép chuyển
đổi phần tham số của truy vấn thành một kiểu dữ liệu khác để dễ dàng hơn

430
cho việc lập trình. Việc tách một truy vấn thành phần lệnh và phần tham số
chúng ta sẽ thực hiện ở phần thực hành tiếp theo.
Tạo tập tin Parameter.cs trong thư mục Framework với lớp Parameter như
sau:
using System;
using System.Collections.Generic;
namespace Framework
{
/// <summary>
/// lưu các cặp khóa-giá trị người dùng nhập;
/// chuỗi tham số cần viết ở dạng khóa=giá trị;
/// nếu có nhiều tham số thì viết tách nhau bằng ký tự &
/// </summary>
public class Parameter
{
private readonly Dictionary<string, string> _pairs = new Dictionary<string,
string>();
/// <summary>
/// nạp chồng phép toán indexing []; cho phép truy xuất giá trị theo kiểu
biến[khóa] = giá_trị;
/// </summary>
/// <param name="key">khóa</param>
/// <returns>giá trị tương ứng</returns>
public string this[string key] // để nạp chồng phép toán indexing phải viết
hai phương thức get,set
{
get
{
if (_pairs.ContainsKey(key))
return _pairs[key];
else return null;
} // phương thức get trả lại giá trị từ dictionary
set => _pairs[key] = value; // phương thức set gán giá trị cho dictionary
}
/// <summary>
/// Kiểm tra xem một khóa có trong danh sách tham số không
/// </summary>
/// <param name="key">khóa cần kiểm tra</param>
/// <returns></returns>
public bool ContainsKey(string key)
{
return _pairs.ContainsKey(key);
}
/// <summary>
/// nhận chuỗi ký tự và phân tích, chuyển thành các cặp khóa-giá trị
/// </summary>
/// <param name="parameter">chuỗi ký tự theo quyt tắc khóa_1=giá_trị_1 & khóa-
2=giá_trị2</param>
public Parameter(string parameter)
{
// cắt chuỗi theo mốc là ký tự & kết quả của phép toán này là một mảng,
// mỗi phần tử là một chuỗi có dạng khóa = giá_trị
var pairs = parameter.Split(new[] { '&' },
StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{

431
var p = pair.Split('='); // cắt mỗi phần tử lấy mốc là ký tự =
if (p.Length == 2) // một cặp khóa = giá_trị đúng sau khi cắt sẽ phải
có 2 phần
{
var key = p[0].Trim(); // phần tử thứ nhất là khóa
var value = p[1].Trim(); // phần tử thứ hai là giá trị
this[key] = value; // lưu cặp khóa-giá trị này lại sử dụng phép
toán indexing
// cũng có thể viết theo kiểu khác, trực tiếp
sử dụng biến _pairs
// _pairs[key] = value;
}
}
}
}
}

Kiểu dữ liệu Dictionary


Trong code ở phần thực hành trên chúng ta đã khai báo một biến thuộc
kiểu Dictionary<string, string> để lưu các tham số người dùng nhập.
Biến này dùng để lưu các cặp khóa/giá trị, trong đó khóa và giá trị đều có
kiểu string. Khi người dùng nhập một truy vấn (ở dạng chuỗi văn bản), phần
tham số sẽ được tách riêng ra, sau đó lại tách tiếp từng cặp khóa = giá trị
để lưu vào từ điển.
Dictionary là một kiểu dữ liệu tập hợp tổng quát (generic collection) tương
tự như List<T> nhưng được dùng cho lưu trữ danh sách các cặp khóa – giá
trị. Khóa và giá trị có thể thuộc bất kỳ kiểu dữ liệu nào của .NET.
Kiểu dữ liệu này được mô tả đầy đủ là Dictionary<TKey, TValue>, trong
đó TKey là kiểu của khóa, TValue là kiểu của giá trị. Lớp Dictionary<TKey,
TValue> được định nghĩa trong không gian tên
System.Collection.Generics.
Dictionary có thể hình dung như bộ từ điển song ngữ, ví dụ, từ điển Anh –
Việt, trong đó từ tiếng Anh là khóa, nghĩa trong tiếng Việt là giá trị.
Lưu ý, khi sử dụng từ điển không được phép sử dụng lặp khóa hoặc để khóa
có giá trị null. Khóa bắt buộc phải là duy nhất (tương tự như trong từ điển
song ngữ). Nếu trùng lặp khóa sẽ báo lỗi ở giai đoạn runtime.
Nạp chồng phép toán indexer
Trong lớp Parameter chúng ta gặp một phương thức có khai báo lạ mắt
public string this[string key] // để nạp chồng phép toán indexing phải viết hai phương thức get,set
{
get => _pairs[key]; // phương thức get trả lại giá trị từ dictionary
set => _pairs[key] = value; // phương thức set gán giá trị cho dictionary

432
}

Phương thức này có ý nghĩa đặc biệt: nạp chồng toán tử indexer (phép toán
indexer, phép toán chỉ mục).
Indexer là một phép toán giúp client code sử dụng object tương tự như khi
sử dụng mảng. Indexer thường được sử dụng với với các kiểu dữ liệu chứa
trong nó một tập hợp dữ liệu (collection hoặc array). Indexer giúp đơn giản
hóa việc sử dụng ở client code.
Phép toán indexer giúp client code có thể truy xuất biến của kiểu Parameter
như sau:
Parameter p = new Parameter("id= 1 & title = A new book");
Var id = p["id"];
p["title"] = "C# programming for dummy";
Client code không biết gì về dữ liệu kiểu từ điển chứa trong Parameter
nhưng có thể sử dụng phép toán indexer để dễ dàng truy xuất dữ liệu của
từ điển thông qua tên biến kiểu Parameter.
Như vậy áp dụng phép toán indexer rất tiện lợi cho việc truy xuất các cặp
khóa-giá trị chứa trong Parameter.
Expression body là một lối viết xuất hiện từ C# 6: nếu thân của phương thức chỉ chứa một lệnh
duy nhất có thể sử dụng cấu trúc như sau để viết:
Tên_phương_thức() => lệnh;
Từ C# 7 có thể sử dụng expression body cho cả phương thức get và set của property.
Trong code của indexer ở trên chúng ta đã sử dụng cấu trúc này cho ngắn
gọn. Từ giờ về sau, ở những chỗ phù hợp chúng ta sẽ sử dụng cấu trúc
expression body.

433
Router (2): lớp nội bộ, ngoại lệ
Trong bài này chúng ta tiếp tục áp dụng các kỹ thuật để xây dựng lớp
Router, bao gồm nạp chồng toán tử, lớp lồng nhau (nested class), và ngoại
lệ.

Thực hành: xây dựng lớp hỗ trợ phân tích truy vấn
Bước 1. Tạo class Router
Tạo tập tin Router.cs trong thư mục Framework cho lớp Router
using System;
using System.Collections.Generic;
using System.Text;
namespace Framework // lưu ý không gian tên
{
/// <summary>
/// lớp cho phép ánh xạ truy vấn với phương thức
/// </summary>
public class Router
{
}
}

Bước 2. Xây dựng lớp Request


Bên trong lớp Router xây dựng lớp Request với code như sau:
using System;
namespace Framework
{
public class Router
{
/// <summary>
/// lớp xử lý truy vấn
/// </summary>
private class Request
{
/// <summary>
/// thành phần lệnh của truy vấn
/// </summary>
public string Route { get; private set; }
/// <summary>
/// thành phần tham số của truy vấn
/// </summary>
public Parameter Parameter { get; private set; }
public Request(string request)
{
Analyze(request);
}
/// <summary>
/// phân tích truy vấn để tách ra thành phần lệnh và thành phần tham số
/// </summary>
/// <param name="request"></param>
private void Analyze(string request)

434
{
// tìm xem trong chuỗi truy vấn có tham số hay không
var firstIndex = request.IndexOf('?');
// trườn hợp truy vấn không chứa tham số
if (firstIndex < 0)
{
Route = request.ToLower().Trim();
}
// trường hợp truy vấn chứa tham số
else
{
// nếu chuỗi lối (chỉ chứa tham số, không chứa route)
if (firstIndex <= 1) throw new Exception("Invalid request
parameter");
// cắt chuỗi truy vấn lấy mốc là ký tự ?
// sau phép toán này thu được mảng 2 phần tử: thứ nhất là route,
thứ hai là chuỗi parameter
var tokens = request.Split(new[] { '?' }, 2,
StringSplitOptions.RemoveEmptyEntries);
// route là thành phần lệnh của truy vấn
Route = tokens[0].Trim().ToLower();
// parameter là thành phần tham số của truy vấn
var parameterPart = request.Substring(firstIndex + 1).Trim();
Parameter = new Parameter(parameterPart);
}
}
}
}
}

Phân tích code


Nested class
Để ý rằng khi xây dựng lớp Router, lớp Request xây dựng bên trong lớp
Router. Request là một nested class của Router.
Ở trường hợp trên, Request còn gọi là lớp trong/lớp nộ bộ, Router là lớp
ngoài, đồng thời là lớp cấp đỉnh.
Trong lớp Router có 3 loại logic tương đối độc lập: phân tích truy vấn, xử lý
chuỗi tham số, đăng ký lệnh và gọi phương thức. Do đó, chúng ta
tách Request thành một lớp nội bộ. Do sau này client code sẽ sử dụng đến
lớp Parameter, lớp này được tách thành một lớp cùng cấp (lớp sibling)
với Router. Trong thân lớp Router chỉ còn chứa code để đăng ký lệnh và
gọi phương thức.
Nếu một lớp sibling của Router muốn sử dụng lớp Request (giả sử
lớp Request được đánh dấu public) thì phải sử dụng tên lớp
là Router.Request trong các lệnh khai báo và khởi tạo, không thể trực tiếp
sử dụng trực tiếp tên Request. Tên gọi ngắn gọn Request chỉ có thể sử
dụng bên trong lớp Router.

435
Lưu ý: nên hạn chế sử dụng lớp lồng nhau. Việc sử dụng lớp lồng nhau
không hợp lý có thể dẫn đến những lỗi khó lường trước, đặc biệt là khi cho
lớp trong và lớp ngoài gọi lẫn nhau.
Ngoại lệ
Khi xây dựng lớp Request chúng ta gặp một lệnh
// nếu chuỗi lỗi (chỉ chứa tham số, không chứa route)
if (firstIndex <= 1) throw new Exception("Invalid request parameter");
Đây là một tình huống đặc biệt trong đó chuỗi truy vấn bị lỗi: người dùng
vô tình chỉ nhập phần tham số mà không nhập phần route. Trong trường
hợp này chúng ta không có cách gì để xử lý chuỗi truy vấn.
Một số ví dụ khác: khi thực hiện phép chia, nếu mẫu số vô tình nhận giá trị
0, phép chia không thể thực hiện được; khi người dùng yêu cầu truy xuất
một tập tin nhưng lại cung cấp sai đường dẫn khiến không thể thực hiện
thao tác truy xuất.
Trong lập trình, những tình huống tương tự xảy ra rất nhiều và được gọi
chung là ngoại lệ (exception).
Xử lý ngoại lệ
Khi một ngoại lệ được phát ra ở một vị trí bất kỳ trong chương trình, việc
thực thi của chương trình sẽ dừng lại. Nếu chương trình đang chạy ở chế độ
Debug, trình soạn thảo code sẽ được mở ra và đoạn code bị lỗi sẽ được đánh
dấu giúp cho người lập trình xác định vị trí và nguyên nhân gây lỗi.
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh
chưa tồn tại.

Giao diện Visual Studio khi xảy ra ngoại lệ


Nếu chương trình chạy ở chế độ Release, chương trình sẽ bị dừng lại và cơ
chế xử lý ngoại lệ mặc định của .NET Framework sẽ được kích hoạt để hiển
thị lỗi. Chương trình được dịch ở chế độ này sẽ không chạy được ở chế độ
Debug nữa.
Nếu chương trình console chạy ở chế độ Release mà gặp lỗi, thông báo lỗi
sẽ được hiển thị như dưới đây.

436
Thông báo ngoại lệ ở giao diện console
Đây là cơ chế bắt và xử lý lỗi mặc định của .NET Framework đối với ứng
dụng console. Đối với ứng dụng Windows form, giao diện bắt và xử lý lỗi có
khác biệt.
Tuy nhiên, cơ chế thông báo lỗi mặc định của .NET Framework tương đối
không thân thiện với người dùng.
.NET cung cấp cho các chương trình tính năng bắt và xử lý ngoại lệ để tự
mình xác định xem khi xảy ra lỗi (ngoại lệ) thì sẽ làm gì. Phần xử lý ngoại
lệ chúng ta sẽ thực hiện trong bài thực hành cuối cùng.

437
Router (3): sử dụng ủy nhiệm hàm
Trong bài học này chúng ta sẽ làm quen với ủy nhiệm hàm (delegate) và
hoàn thiện lớp Router.

Thực hành: hoàn thiện lớp Router


Bước 1. Viết code cho lớp Router
using System;
using System.Collections.Generic;
using System.Text;
namespace Framework
{
/* đây không phải là lệnh sử dụng không gian tên
* mà là tạo biệt danh cho một kiểu dữ liệu
* ở đây đang tạo một biệt danh cho kiểu Dictionary<string, ControllerAction>.
* trong cả tập tin này có thể sử dụng tên kiểu RoutingTable
* thay cho Dictionary<string, ControllerAction>
* Lưu ý rằng khai báo này nằm trực tiếp trong namespace
*/
using RoutingTable = Dictionary<string, ControllerAction>;
// Lưu ý khai báo delegate này là khai báo kiểu, nằm trong namespace
/// <summary>
/// delegate này đại diện cho tất cả các phương thức có:
/// - kiểu ra là void,
/// - danh sách tham số vào là (Parameter)
/// </summary>
/// <param name="parameter"></param>
public delegate void ControllerAction(Parameter parameter = null);
/// <summary>
/// lớp cho phép ánh xạ truy vấn với phương thức
/// </summary>
public class Router
{
// nhóm 3 lệnh dưới đây biến Router thành một singleton
private static Router _instance;
private Router()
{
_routingTable = new RoutingTable();
_helpTable = new Dictionary<string, string>();
}
// để ý: constructor là private
// người sử dụng class thông qua property này để truy xuất các phương thức của
class
// chỉ khi nào _instance == null mới tạo object. Một khi đã tạo object,
//_instance sẽ không có giá trị null nữa.
// vì là biến static, _instance một khi được khởi tạo sẽ tồn tại suốt chương
trình
public static Router Instance => _instance ?? (_instance = new Router());
// lưu ý: ở đây đang sử dụng alias của Dictionary<string, ControllerAction>
cho ngắn gọn
private readonly RoutingTable _routingTable;
private readonly Dictionary<string, string> _helpTable;
public string GetRoutes()
{

438
StringBuilder sb = new StringBuilder();
foreach (var k in _routingTable.Keys)
sb.AppendFormat("{0}, ", k);
return sb.ToString();
}
public string GetHelp(string key)
{
if (_helpTable.ContainsKey(key))
return _helpTable[key];
else
return "Documentation not ready yet!";
}
/// <summary>
/// đăng ký một route mới, mỗi route ánh xạ một chuỗi truy vấn với một phương
thức
/// </summary>
/// <param name="route"></param>
/// <param name="action"></param>
public void Register(string route, ControllerAction action, string help = "")
{
// nếu _routingTable đã chứa route này thì bỏ qua
if (!_routingTable.ContainsKey(route))
{
_routingTable[route] = action;
_helpTable[route] = help;
}
}
/// <summary>
/// phân tích truy vấn và gọi phương thức tương ứng với chuỗi truy vấn
/// <para>chuỗi truy vấn bao gồm hai phần: route và parameter, phân tách bởi
ký tự ?</para>
/// </summary>
/// <param name="request">chuỗi truy vấn, bao gồm hai phần:
/// route, paramete; phân tách bởi ký tự ?</param>
public void Forward(string request)
{
var req = new Request(request);
if (!_routingTable.ContainsKey(req.Route))
throw new Exception("Command not found!");
if (req.Parameter == null)
_routingTable[req.Route]?.Invoke();
else
_routingTable[req.Route]?.Invoke(req.Parameter);
}
// Code của lớp Request (làm trong buổi trước) nằm ở đây và tạm ẩn đi cho gọn
}
}

Bước 2. Điều chỉnh code của lớp Program


Điểu chỉnh code của lớp Program (tập tin Program.cs) như sau:
using System;
namespace BookMan.ConsoleApp
{
using Controllers;
using Framework;
using DataServices;
internal class Program

439
{
private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var context = new SimpleDataAccess();
BookController controller = new BookController(context);
Router.Instance.Register("about", About);
Router.Instance.Register("help", Help);
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
Router.Instance.Forward(request);
Console.WriteLine();
}
}
private static void About(Parameter parameter)
{
ViewHelp.WriteLine("BOOK MANAGER version 1.0", ConsoleColor.Green);
ViewHelp.WriteLine("by ChiChi@TuHocIct.com", ConsoleColor.Magenta);
}
private static void Help(Parameter parameter)
{
if (parameter == null)
{
ViewHelp.WriteLine("SUPPORTED COMMANDS:", ConsoleColor.Green);
ViewHelp.WriteLine(Router.Instance.GetRoutes(), ConsoleColor.Yellow);
ViewHelp.WriteLine("type: help ? cmd= <command> to get command
details", ConsoleColor.Cyan);
return;
}
Console.BackgroundColor = ConsoleColor.DarkBlue;
var command = parameter["cmd"].ToLower();
ViewHelp.WriteLine(Router.Instance.GetHelp(command));
}
}
}

Bước 3. Dịch và chạy thử chương trình


Dịch và chạy thử chương trình với lệnh about và help

Kết quả chạy chương trình

440
Phân tích code
Delegate
Trong phần thực hành trên chúng ta gặp một dạng khai báo:
/// <summary>
/// delegate này đại diện cho tất cả các phương thức có:
/// - kiểu ra là void,
/// - danh sách tham số vào là (Router.Parameter)
/// </summary>
/// <param name="parameter"></param>
public delegate void ControllerAction(Parameter parameter = null);

Đây là định nghĩa một kiểu dữ liệu ủy nhiệm (delegate).


Ví dụ, ở phần thực hành trên chúng ta đã định nghĩa một kiểu ủy nhiệm:
public delegate void ControllerAction(Parameter parameter = null);

Lệnh này định nghĩa một kiểu Ủy nhiệm tên là ControllerAction.


Kiểu ControllerAction này dùng để tạo ra các biến chứa tham chiếu tới
tất cả các phương thức có tham số đầu vào thuộc kiểu Parameter và không
trả về dữ liệu. Nói theo cách khác, tất cả các phương thức có tham số đầu
vào thuộc kiểu Parameter và không trả về dữ liệu đều có thể gán cho biến
thuộc kiểu ControllerAction.
Biệt danh (alias) của kiểu dữ liệu
Kiểu RoutingTable bạn nhìn thấy trong phần đầu của tập tin code thực chất
chỉ là một biệt danh (alias) của một Dictionary với khóa kiểu string và
giá trị thuộc kiểu ControllerAction.
using RoutingTable = Dictionary<string, ControllerAction>;
public delegate void ControllerAction(Parameter parameter = null);
private readonly RoutingTable _routingTable;

Dictionary này được sử dụng để khai báo ra biến _routingTable nhằm chứa
những cặp route / phương thức. Trong đó, phương thức phải tuân thủ theo
định nghĩa của ControllerAction.
Mỗi cặp này được sử dụng để ánh xạ một route tới một phương thức cụ thể.
Các phương thức này bắt buộc phải tiếp nhận chuỗi tham số của người dùng
làm tham số đầu vào. Ở trong mỗi phương thức này sẽ tách các tham số đó
ra và sử dụng để gọi tới một action tương ứng của controller.

441
Vai trò của delegate trong Router
Lớp Router của chúng ta có nhiệm vụ lưu lại một danh sách các phương
thức tương ứng với chuỗi truy vấn của người dùng. Về sau, khi người dùng
nhập một truy vấn nào đó, phương thức tương ứng sẽ được thực hiện. Mỗi
phương thức lưu trong Router sẽ tiếp tục gọi một phương thức (action) của
controller. Nhờ đó, chúng ta có thể ánh xạ mỗi truy vấn của người dùng tới
một action của controller.
Tuy nhiên, khi xây dựng lớp Router chúng ta chưa xác định được các phương
thức sẽ lưu trong nó, cũng như chưa thể xác định hết các phương thức
(action) của controller.
Trong tương lai, khi bổ sung thêm các chức năng mới, danh sách phương
thức lưu trữ trong Router lại tiếp tục tăng lên theo.
Do đó, trong class Router chúng ta phải sử dụng Ủy nhiệm. Mỗi khi trong
controller xuất hiện một action mới (bổ sung thêm chức năng cho chương
trình) thì trong Router cần đăng ký một chuỗi truy vấn cùng với một phương
thức mới chứa lời gọi action này.
Như vậy, ủy nhiệm cho phép một class uyển chuyển và linh động hơn trong
việc sử dụng phương thức. Theo đó, nội dung cụ thể của một phương thức
không được định nghĩa sẵn trong class mà sẽ do người dùng class đó tự
định nghĩa trong quá trình khởi tạo object. Điều này giúp phân chia logic
của một class ra các phần khác nhau và do những người khác nhau xây
dựng.

442
Router (4): phương thức vô danh, hàm lambda
Trong bài học này chúng ta tiếp tục các nội dung liên quan đến lớp Router,
bao gồm việc sử dụng phương thức vô danh, hàm lambda, hàm cục bộ, và
mẫu thiết kế singleton.

Thực hành: sử dụng lớp Router vừa tạo để đăng ký thêm


các truy vấn mới
Bước 1. Viết thêm code cho lớp Program
private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
SimpleDataAccess context = new SimpleDataAccess();
BookController controller = new BookController(context);
Router r = Router.Instance;
r.Register("about", About);
r.Register("help", Help);
r.Register(route: "create",
action: p => controller.Create(),
help: "[create]\r\nnhập sách mới");
r.Register(route: "update",
action: p => controller.Update(p["id"].ToInt()),
help: "[update ? id = <value>]\r\ntìm và cập nhật sách");
r.Register(route: "list",
action: p => controller.List(),
help: "[list]\r\nhiển thị tất cả sách");
r.Register(route: "single",
action: p => controller.Single(p["id"].ToInt()),
help: "[single ? id = < value >]\r\nhiển thị một cuốn sách theo id");
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
Router.Instance.Forward(request);
Console.WriteLine();
}
}
Trong đoạn code trên, bạn đã sử dụng hàm lambda để gán cho các tham số
action của phương thức Register:
p => controller.Create()
p => controller.Update(p["id"].ToInt())
p => controller.List(),
p => controller.Single(p["id"].ToInt())
Bạn hoàn toàn có thể sử dụng anonymous ở đây để tránh phải xây dựng
các phương thức “mini” làm rối code. Thực tế, các phương thức này bạn chỉ
xây dựng và sử dụng một lần duy nhất làm tham số cho phương thức
Register. Bạn không có nhu cầu tái sử dụng code của nó. Do đó, việc xây
dựng các phương thức thành viên ở đây không có ý nghĩa.

443
Bước 2. Dịch và chạy thử chương trình
Dịch và chạy thử chương trình với các truy vấn sau:
Single ? id = 2
Create
Update ? id = 1
List

Kết quả chạy chương trình

Mẫu thiết kế, singleton, mediator


Khi xây dựng lớp Router chúng ta đã vận dụng hai mẫu thiết kế: singleton
và mediator.
Mẫu thiết kế
Khi học toán ở trường phổ thông chúng ta thường gặp những mẫu bài tập
điển hình. Khi gặp một bài toán lạ chúng ta thường cố gắng quy nó về những
mẫu mình biết, từ đó giúp việc giải bài toán đơn giản hơn.
Trong lập trình ứng dụng cũng có tình trạng tương tự. Có những vấn đề
chung lặp lại trong nhiều dự án khác nhau đưa đến những kinh nghiệm
rằng, nếu gặp lại vấn đề tương tự trong một dự án mới, chúng ta hoàn toàn
có thể áp dụng lại giải pháp đó.
444
Ví dụ, trong các dự án thường gặp một yêu cầu: làm sao để một class trong
chương trình chỉ cho phép sinh ra một object duy nhất.
Lớp Router ở trên là một ví dụ. Khi tạo ra object của lớp Router, chúng ta
đăng ký một loạt lệnh và action tương ứng với nó. Danh mục lệnh-action
này rõ ràng là phải sử dụng chung trong toàn bộ chương trình, vì nếu chúng
ta tạo ra một object mới của lớp Router, chúng ta không thể sử dụng danh
mục lệnh đã đăng ký trong object trước đó.
Như vậy rất tự nhiên chúng ta gặp phải yêu cầu: chỉ được phép tạo ra và
sử dụng một object duy nhất của lớp Router trong toàn bộ chương trình.
Một ví dụ khác. Trong chương trình thường yêu cầu tính năng log để ghi lại
hoạt động của chương trình (ví dụ, để tìm lỗi). Tính năng log thường ghi lại
các hoạt động ra tập tin. Nếu một tập tin được mở trong một object nào đó
để ghi dữ liệu, object khác không thể mở lại tập tin này để ghi dữ liệu nữa
vì nó đã bị chương trình khóa lại. Chỉ khi nào object thứ nhất giải phóng kết
nối tới tập tin, object thứ hai mới có thể mở được.
Một giải pháp tự nhiên là chỉ cho phép một object duy nhất làm việc với tập
tin này, và bất kỳ ở đâu và lúc nào trong chương trình, khi cần ghi log thì
chỉ cần sử dụng object này.
Những vấn đề lặp lại nhiều lần như trên được đúc rút ra thành các hướng
dẫn về giải pháp, gọi là các mẫu thiết kế (design pattern). Như vậy, mẫu
thiết kế rất tương đồng với mẫu bài tập toán. Nó không phải là một thuật
toán mà là một hướng dẫn (guideline) để có thể áp dụng vào các bài toán
cụ thể.
Singleton
Đối với lớp Router và yêu cầu đã nói, chúng ta sử dụng mẫu thiết kế
Singleton để giải quyết.
Mẫu thiết kế singleton giúp giải quyết vấn đề: chỉ cho phép tạo ra duy nhất
một object của một class trong toàn bộ ứng dụng.
Nhóm lệnh sau giúp biến lớp Router thành một singleton:
// nhóm 3 lệnh dưới đây biến Router thành một singleton
private static Router _instance;
private Router() => _routingTable = new RoutingTable(); // để ý: constructor là private
// người sử dụng class thông qua property này để truy xuất các phương thức của class
// chỉ khi nào _instance == null mới tạo object. Một khi đã tạo object, _instance sẽ
// không có giá trị null nữa.
// vì là biến static, _instance một khi được khởi tạo sẽ tồn tại suốt chương trình
public static Router Instance => _instance ?? (_instance = new Router());

445
Nếu sau này có nhu cầu biến một class bất kỳ thành singleton, chỉ cần vận
dụng nhóm 3 lệnh này với class mới.
Khi cần sử dụng lớp Router, chúng ta sử dụng thuộc tính tĩnh Instance:
Router.Instance.Register("about", About);
Router.Instance.Register("help", Help);
Router.Instance.Register("single", delegate (Router.Parameter p) { controller.Single(p["id"].ToInt()); });
Router.Instance.Register("update", p => controller.Update(p["id"].ToInt()));

Đây là cách xây dựng và sử dụng singleton có thể vận dụng đối với bất kỳ
class nào nếu có yêu cầu tương tự đặt ra.
Mediator
Trên thực tế, lớp Router còn vận dụng một mẫu thiết kế nữa, gọi là
mẫu Mediator.
Mẫu thiết kế này dùng để giải quyết vấn đề kích hoạt một phương thức từ
một phương thức khác nhưng không cho hai class chứa các phương thức đó
phụ thuộc vào nhau. Thiết kế này hướng tới mục tiêu là phụ thuộc
lỏng (loosely coupling) giữa các class.
Ở đây chúng ta vận dụng mẫu thiết kế Mediator để các lớp giao diện gọi các
phương thức của controller nhưng trong điều kiện là các lớp giao diện không
được biết về sự tồn tại của lớp controller (theo quy ước của MVC).

446
Cải tiến view (1): NuGet, NewtonSoft, JSON
Trong bài học này chúng ta sẽ học cách sử dụng công cụ quản lý gói thư
viện NuGet để cài đặt thư viện lớp của bên thứ ba. Chúng ta sẽ xem xét thư
viện NewtonSoft Json để thêm chức năng xuất dữ liệu ra tập tin.
Trong các bài trước chúng ta đã xây dựng được một khung chương trình cơ
bản đáp ứng một số yêu cầu đặt ra từ phân tích ở bài 1.
Trong các bài còn lại của phần này, chúng ta sẽ lần lượt cải tiến các lớp giao
diện sử dụng các kỹ thuật mới.

Xuất dữ liệu ra tập tin, JSON


Trước hết chúng ta cải tiến các lớp giao diện bằng cách bổ sung thêm khả
năng xuất dữ liệu ra tập tin ở dạng json. Đây là yêu cầu có trong phần phân
tích ca sử dụng ở bài đầu tiên (ca sử dụng số 8).
Vì đây là một ứng dụng dạng console, mọi thông tin đều viết ra màn hình.
Giao diện console không tiện lợi lắm để xem danh sách dữ liệu quá dài.
Ngoài ra chúng ta có thể muốn xuất thông tin ra để sử dụng trong những
chương trình khác (chẳng hạn import vào excel, import vào cơ sở dữ liệu).
Một trong những định dạng dữ liệu được sử dụng và hỗ trợ rộng rãi hiện nay
là JSON (JavaScript Object Notation).
Chuỗi ký tự JSON được viết theo cú pháp mô tả object của ngôn ngữ
JavaScript và hiện được sử dụng rất rộng rãi để lưu trữ hoặc truyền dữ liệu.
JSON được sử dụng đặc biệt phổ biến trong môi trường web và để lưu trữ
thông tin cấu hình của ứng dụng.
.NET Framework có hỗ trợ Json nhưng không được tiện lợi và hiệu quả như
một số bộ thư viện của các bên thứ ba.
Trong phần thực hành dưới đây chúng ta sẽ học cách sử dụng bộ thư viện
JSON của NewtonSoft.

Thực hành 1: cài đặt thư viện NewtonSoft.Json


Bộ thư viện này cho phép chúng ta chuyển đổi một object của C# thành
một chuỗi ký tự định dạng theo quy ước của JSON (JavaScript Object
Notation) cũng như chuyển đổi ngược chuỗi JSON về object của C#. Đây là
một trong những bộ thư viện có lượt download lớn nhất trên NuGet.
Quá trình chuyển đổi này có tên gọi là serialization (từ object về JSON)
và deserialization (từ JSON về object).

447
Bước 1. Mở giao diện quản lý các gói thư viện NuGet
Click phải vào References, chọn Manage NuGet Packages (xem hình dưới
đây).

Mở giao diện Manage NuGet Packages


Bước 2. Chọn cài gói thư viện
Trong ô tìm kiếm ở tab Browse gõ newtonsoft, chọn gói NewtonSoft.Json và
ấn Install.

Giao diện Manage NuGet Packages


Sau lệnh này, Visual Studio sẽ tải gói thư viện này về và cài đặt lên project
tương ứng (trong trường hợp này là BookMan.ConsoleApp).

448
Kiểm tra kết quả
Sau khi cài đặt thành công bộ thư viện này, trong danh sách References sẽ
xuất hiện thêm một mục “NewtonSoft.Json”. Trong cấu trúc dự án sẽ xuất
hiện thêm tập tin “packages.config” chứa thông tin về các gói thư viện được
cài đặt đặt thêm.
Sau khi dịch chương trình (Ctrl + Shift + B) thành công, trong thư mục
BinDebug của dự án sẽ xuất hiện tập tin thư việc NewtonSoft.Json.dll.
Khi triển khai ứng dụng cho người dùng cuối, tập tin thư viện này cũng phải đi cùng tập tin
chương trình.

File thư viện Newtonsoft.json.dll sau khi cài đặt

Những các khác sử dụng NuGet Packages Manager


Chúng ta có thể sử dụng theo một số cách khác nhau:
1. Sử dụng ứng dụng giao diện đồ họa NuGet Package Manager (như
phần thực hành trên),
2. Sử dụng giao diện dòng lệnh Package Manager Console,
3. Cài đặt tự động với các tập tin mã kịch bản.
Cách đơn giản nhất để tìm và cài đặt các gói thư viện từ NuGet là sử dụng
tiện ích mở rộng NuGet Package Manager như đã thực hiện trong phần thực
hành trên.
Sử dụng website kết hợp Package Manager Console
Cách thứ hai là sử dụng dịch vụ tìm kiếm trên website
https://www.nuget.org/packages để tìm gói thư viện phù hợp. Sau đó copy
dòng lệnh paste vào Package Manager Console.

449
Giao diện tìm kiếm thư viện Newtonsoft.Json trên website
Nếu không nhìn thấy tab Package Manager Console, chọn View => Other Windows => Package Manager Console,
hoặc Tools => NuGet Package Manager => Package Manager Console.

Giao diện dòng lệnh của Package Manager Console


Khi sử dụng Package Manager Console lưu ý chọn tham số “Default project”
là project mình cần cài đặt gói thư viện.
Lưu ý
Khi sử dụng các gói thư viện trên NuGet cần lưu ý xem xét kỹ sự phụ thuộc
của gói thư viện cần dùng.
Lý do là nhiều thư viện trên NuGet sử dụng lẫn nhau, cũng như được xây
dựng cho các phiên bản .NET khác nhau.
Khi cài đặt một thư viện mà nó phụ thuộc vào các thư viện khác, các thư
viện kia cũng phải được cài đặt theo và phải cài đặt phiên bản mà thư viện
chính có thể sử dụng được.
450
Nếu các gói thư viện có phiên bản mới, NuGet cũng cho phép cập nhật phiên
bản đang cài đặt trong dự án lên phiên bản mới. Tuy nhiên, cũng giống như
khi cài đặt, phải lưu ý sự phụ thuộc giữa các thư viện trước khi quyết định
nâng cấp.

Thực hành 2: thêm chức năng xuất dữ liệu ra tập tin


Bước 1. Bổ sung phương thức RenderToFile
Bổ sung phương thức RenderToFile vào lớp BookListView
public void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
System.IO.File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}

Tương tự, bổ sung phương thức RenderToFile vào lớp BookSingleView


public void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
System.IO.File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}
* để ý thấy rằng hai phương thức này giống hệt nhau
Bước 2. Điều chỉnh các phương thức của lớp BookController
Điều chỉnh phương thức Single và List của BookController thành dạng như
sau:
/// <summary>
/// ghép nối dữ liệu 1 cuốn sách với giao diện hiển thị 1 cuốn sách
/// </summary>
/// <param name="id">mã định danh của cuốn sách</param>
public void Single(int id, string path = "")
{
// lấy dữ liệu qua repository
var model = Repository.Select(id);
// khởi tạo view
BookSingleView view = new BookSingleView(model);
// gọi phương thức Render để thực sự hiển thị ra màn hình
if (!string.IsNullOrEmpty(path)) { view.RenderToFile(path); return; }
view.Render();
}
/// <summary>
/// kích hoạt chức năng hiển thị danh sách
/// </summary>
public void List(string path = "")

451
{
// lấy dữ liệu qua repository
var model = Repository.Select();
// khởi tạo view
BookListView view = new BookListView(model);
if (!string.IsNullOrEmpty(path)) { view.RenderToFile(path); return; }
view.Render();
}

Bước 3. Điều chỉnh lớp Program


private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
SimpleDataAccess context = new SimpleDataAccess();
BookController controller = new BookController(context);
Router r = Router.Instance;
r.Register("about", About);
r.Register("help", Help);
r.Register(route: "create",
action: p => controller.Create(),
help: "[create]\r\nnhập sách mới");
r.Register(route: "update",
action: p => controller.Update(p["id"].ToInt()),
help: "[update ? id = <value>]\r\ntìm và cập nhật sách");
r.Register(route: "list",
action: p => controller.List(),
help: "[list]\r\nhiển thị tất cả sách");
r.Register(route: "single",
action: p => controller.Single(p["id"].ToInt()),
help: "[single ? id = < value >]\r\nhiển thị một cuốn sách theo id");
r.Register(route: "list tập tin",
action: p => controller.List(p["path"]),
help: "[list tập tin ? path = <value>]\r\nhiển thị tất cả sách");
r.Register(route: "single tập tin",
action: p => controller.Single(p["id"].ToInt(), p["path"]),
help: "[single tập tin ? id = <value> & path = <value>]");
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
Router.Instance.Forward(request);
Console.WriteLine();
}
}

Bước 4. Dịch và chạy thử chương trình


Dịch và chạy thử chương trình với hai lệnh mới
List tập tin ? path = list.json
Single tập tin ? id = 1 & path = single1.json

452
Kết quả chạy chương trình
Khi đó trong cùng thư mục với tập tin chạy xuất hiện hai tập tin: list.json
và single1.json với nội dung lần lượt như sau:
[{"Id":1,"Authors":"Unknown author","Title":"A new book 1","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":2,"Authors":"Unknown author","Title":"A
new book 2","Publisher":"Unknown publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A
new book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":3,"Authors":"Unknown
author","Title":"A new book 3","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":4,"Authors":"Unknown author","Title":"A
new book 4","Publisher":"Unknown publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A
new book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":5,"Authors":"Unknown
author","Title":"A new book 5","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":6,"Authors":"Unknown author","Title":"A
new book 6","Publisher":"Unknown publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A
new book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":7,"Authors":"Unknown
author","Title":"A new book 7","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":8,"Authors":"Unknown author","Title":"A
new book 8","Publisher":"Unknown publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A
new book","Rating":1,"Reading":false,"File":null,"FileName":null},{"Id":9,"Authors":"Unknown
author","Title":"A new book 9","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null}]

453
{"Id":1,"Authors":"Unknown author","Title":"A new book 1","Publisher":"Unknown
publisher","Year":2018,"Edition":1,"Isbn":null,"Tags":null,"Description":"A new
book","Rating":1,"Reading":false,"File":null,"FileName":null}

Đây là hai tập tin văn bản định dạng theo kiểu JSON do chương trình tạo ra
theo lệnh của người dùng.
Nếu mang đoạn văn bản trên vào tập tin mã nguồn của JavaScript, đoạn văn bản đó là mã nguồn
chạy được vì đây chính là các đoạn code để tạo object của ngôn ngữ này!
Đến đây chúng ta có thể thấy, việc bổ sung thêm các tính năng mới cho
ứng dụng trở nên rất đơn giản theo quy trình: điều chỉnh/ tạo mới lớp giao
diện => điều chỉnh lớp điều khiển => bổ sung phần tử mới cho routes.
Tuy nhiên, trong việc thêm tính năng xuất dữ liệu ra tập tin chúng ta lại gặp
một vấn đề: lặp code.
Trong bài trước, chúng ta đã giải quyết tình trạng lặp code bằng cách nhóm
các phương thức có chức năng liên quan vào một class mới và chuyển chúng
thành các phương thức tĩnh. Tuy nhiên, giải pháp này chỉ áp dụng tốt với
các phương thức không sử dụng biến thành viên (tức là không có liên quan
đến trạng thái của object).
Đối với các lớp giao diện xuất hiện tình trạng lặp code khác. Hai phương
thức trùng nhau RenderToFile ở trên lại bắt buộc phải sử dụng biến thành
viên Model.
Ngoài hai phương thức bị lặp này, chúng ta cũng nhận thấy, trong tất cả
các lớp giao diện, các thành viên Model, phương thức Render và phương
thức khởi tạo đều rất tương đồng nhau.

454
Cải tiến view (2): kế thừa, boxing, ép kiểu
Ở bài trước chúng ta đã xây dựng thêm chức năng xuất dữ liệu ra tập tin.
Bạn đã nhận thấy có sự trùng lặp code giữa các class. Một trong những giải
pháp chúng ta sẽ áp dụng là sử dụng cơ chế kế thừa. Trong bài này bạn sẽ
học cách vận dụng kế thừa vào cải tiến các lớp view giúp chống lặp code.
Ngoài ra bạn cũng sẽ xem xét thêm về vấn đề boxing/unboxing dữ liệu và
cách ép kiểu.

Thực hành: áp dụng kế thừa để cải tiến các lớp view


Lưu ý: do khối lượng code lớn, từ giờ về sau các ghi chú cũ trước đây sẽ bị
xóa bỏ khi đưa vào bài, chỉ giữ lại những ghi chú mới quan trọng để tránh
rối.
Bước 1. Xây dựng lớp ViewBase
Tạo tập tin ViewBase.cs trong thư mục Framework cho lớp ViewBase và viết
code như sau:
using System.IO;
namespace Framework
{
public class ViewBase
{
protected object Model;
protected Router Router = Router.Instance;
public ViewBase() { }
public ViewBase(object model) => Model = model;
public void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}
}
}
ViewBase sẽ là lớp cha của tất cả các lớp view khác. Các bước sau đây sẽ
lần lượt điều chỉnh để các lớp view đã xây dựng kế thừa từ ViewBase.
Bước 2. Điều chỉnh lớp BookListView
Điều chỉnh code của lớp BookListView như sau:
using System;
using System.IO;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookListView : ViewBase

455
{
public BookListView(Book[] model) : base(model) { }
public void Render()
{
if (((Book[])Model).Length == 0)
{
ViewHelp.WriteLine("No book found!", ConsoleColor.Yellow);
return;
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("THE BOOK LIST");
Console.ForegroundColor = ConsoleColor.Yellow;
foreach (Book b in Model as Book[])
{
ViewHelp.Write($"[{b.Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {b.Title}", b.Reading ? ConsoleColor.Cyan :
ConsoleColor.White);
}
Console.ResetColor();
}
}
}
Ở bước này chúng ta xóa bỏ phương thức RenderToFile và dòng khai
báo protected Book[] Model;. Phương thức và dòng khai báo này đã được
thực hiện ở lớp cha ViewBase và lớp BookListView đang kế thừa từ lớp cha
này.
Ngoài ra, constructor của BookListView cũng gọi tới constructor của lớp
ViewBase: public BookListView(Book[] model) : base(model) { }
Bước 3. Điều chỉnh lớp BookSingleView
Tương tự, ở bước này chúng ta cũng xóa bỏ phương thức RenderToFile và
lời khai báo protected Book Model;.
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookSingleView : ViewBase
{
public BookSingleView(Book model) : base(model) { }
public void Render()
{
if (Model == null)
{
ViewHelp.WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);
return;
}
ViewHelp.WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);
// chuyển đổi kiểu từ object sang Book, chỉ áp dụng với kiểu class
var model = Model as Book;
Console.WriteLine($"Authors: {model.Authors}");
Console.WriteLine($"Title: {model.Title}");
Console.WriteLine($"Publisher: {model.Publisher}");
456
Console.WriteLine($"Year: {model.Year}");
Console.WriteLine($"Edition: {model.Edition}");
Console.WriteLine($"Isbn: {model.Isbn}");
Console.WriteLine($"Tags: {model.Tags}");
Console.WriteLine($"Description: {model.Description}");
Console.WriteLine($"Rating: {model.Rating}");
Console.WriteLine($"Reading: {model.Reading}");
Console.WriteLine($"File: {model.File}");
Console.WriteLine($"File Name: {model.FileName}");
}
}
}

Bước 4. Điều chỉnh lớp BookUpdateView


Điều chỉnh tương tự như hai class ở trên.
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookUpdateView : ViewBase
{
public BookUpdateView(Book model) : base(model) { }
public void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
// chuyển đổi kiểu từ object sang Book, chỉ áp dụng với kiểu class
var model = Model as Book;
var authors = ViewHelp.InputString("Authors", model.Authors);
var title = ViewHelp.InputString("Title", model.Title);
var publisher = ViewHelp.InputString("Publisher", model.Publisher);
var isbn = ViewHelp.InputString("Isbn", model.Isbn);
var tags = ViewHelp.InputString("Tags", model.Tags);
var description = ViewHelp.InputString("Description", model.Description);
var tập tin = ViewHelp.InputString("File", model.File);
var year = ViewHelp.InputInt("Year", model.Year);
var edition = ViewHelp.InputInt("Edition", model.Edition);
var rating = ViewHelp.InputInt("Rate", model.Rating);
var reading = ViewHelp.InputBool("Reading", model.Reading);
}
}
}

Bước 5. Điều chỉnh lớp BookCreateView


using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
internal class BookCreateView : ViewBase
{
public BookCreateView() { }
public void Render()
{
ViewHelp.WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
var title = ViewHelp.InputString("Title");
var authors = ViewHelp.InputString("Authors");

457
var publisher = ViewHelp.InputString("Publisher");
var year = ViewHelp.InputInt("Year");
var edition = ViewHelp.InputInt("Edition");
var tags = ViewHelp.InputString("Tags");
var description = ViewHelp.InputString("Description");
var rate = ViewHelp.InputInt("Rate");
var reading = ViewHelp.InputBool("Reading");
var tập tin = ViewHelp.InputString("File");
}
}
}
Trong phần thực hành vừa rồi chúng ta đã xây dựng lớp ViewBase chứa
biến thành viên Model và Router (đều đánh dấu là protected), hai phương
thức khởi tạo và một phương thức public mới RenderToFile.
Biến thành viên Router chỉ để tiện lợi hơn khi sử dụng (thay vì phải gõ đầy
đủ là Router.Instance) ở các lớp giao diện kế thừa từ ViewBase, bởi vì hầu
hết các lớp giao diện về sau này đều có sử dụng đến Router để kích hoạt
phương thức của controller.
Biến thành viên Model chính là nơi chứa dữ liệu để về sau có thể hiển thị
qua phương thức Render hoặc ghi vào tập tin qua phương thức
RenderToFile. Biến này có thể nhận giá trị thông qua một phương thức khởi
tạo của ViewBase.
Để ý rằng, Model có kiểu là object. Như chúng ta đã biết ở phần trên,
System.Object là kiểu dữ liệu cha của mọi class. Do đó, biến kiểu
System.Object có thể tham chiếu tới bất kỳ kiểu dữ liệu nào. Nói cách khác,
biến Model ở đây có thể nhận giá trị là object của bất kỳ class nào.
Như bạn đã biết, các kiểu dữ liệu của C# cũng là kiểu dữ liệu của .NET. Tuy
nhiên, C# cung cấp một số từ khóa kiểu để giúp đơn giản hóa việc sử dụng
các kiểu dữ liệu của .NET. Do đó object tương đương với System.Object,
int tương đương với System.Int32, bool tương đương với
System.Boolean.
C# khuyến khích sử dụng từ khóa kiểu thay cho kiểu .NET.
Một số kỹ thuật khác
Boxing/Unboxing
Tuy rằng biến thuộc kiểu Object có thể nhận giá trị thuộc bất kỳ kiểu nào,
có sự khác biệt quan trọng giữa đối tượng kiểu tham chiếu (reference type)
và kiểu giá trị (value type). Nếu biến thuộc kiểu Object nhận đối tượng
thuộc kiểu giá trị sẽ xảy ra một quá trình gọi là boxing. Quá trình ngược lại
(chuyển đổi từ object về kiểu cụ thể) được gọi là unboxing.

458
Khi một số nguyên, số thực, logic (nói chung thuộc nhóm kiểu giá trị) được
gán cho một biến kiểu Object, .NET tạo một object mới trong vùng heap
và để biến kiểu này trỏ vào đó. Quá trình này gọi là boxing.
Như vậy, boxing là quá trình chuyển đổi kiểu giá trị thành kiểu tham chiếu.
Quá trình này phức tạp và hiệu suất thấp, vì vậy nên hạn chế sử dụng
boxing. Ở các bài sau chúng ta sẽ đưa ra một giải pháp khác.
Ở lớp BookSingleView, BookListView, BookUpdateView, trước khi có thể sử
dụng biến Model trong các lệnh, chúng ta đều phải thực hiện quá trình
unboxing này thông qua phép toán ép kiểu (type casting).
Type casting
Trong phần thực hành trên bạn gặp dạng chuyển đổi kiểu type casting,
thường gọi là ép kiểu cho dễ phân biệt.
Casting là quá trình chuyển đổi Kiểu của một biến nhưng không làm biến
đổi giá trị của biến đó.
Để phân biệt, conversion làm biến đổi giá trị cùng với kiểu. Khi chuyển đổi từ chuỗi “12345”
thành số 12345, bản thân giá trị đã thay đổi cùng với kiểu. Đây là dạng conversion. Trong một
số bài thực hành trước đây bạn đã gặp dạng chuyển đổi type conversion, cụ thể là chuyển đổi
từ chuỗi ký tự về số và logic.
Unboxing ở trên là một quá trình kết hợp giữa tạo một giá trị mới trong
stack, copy dữ liệu từ heap vào đó và ép kiểu (casting).
C# có hai cú pháp khác nhau để ép kiểu:
1. Đối với cả hai loại kiểu (giá trị và tham chiếu): có thể sử dụng
lệnh (Book) Model để ép kiểu về Book. Trong cách này, nếu ép kiểu
không thành công sẽ gây lỗi.
2. Riêng đối với kiểu tham chiếu: có thể sử dụng lệnh Model as Book.
Cách sử dụng này có lợi thế hơn ở chỗ, nếu việc ép kiểu bị lỗi (không
ép kiểu được), biến đích nhận giá trị null và không gây lỗi.

459
Cải tiến view (3): che giấu, ghi đè, kế thừa và generic
Trong bài học này chúng ta sẽ xem xét khái niệm và kỹ thuật ghi đè, che
giấu phương thức, và cách sử dụng lớp generic trong kế thừa. Chúng ta sẽ
vận dụng để tiếp tục cải tiến các lớp view. Ngoài ra, chúng ta sẽ tiếp tục
xem xét một số vấn đề khác của kế thừa trước khi đi vào cải tiến các lớp
giao diện vận dụng các kỹ thuật mới.
Để hiểu được bài thực hành này, bạn cần nắm được các vấn đề liên quan đến kế thừa (cụ thể là
vấn đề che giấu và ghi đè thành viên) và generic.

Nhắc lại quan hệ giữa kế thừa và đa hình


Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác
nhau.
Đa hình thiết lập mối quan hệ “là” (is-a relationship) giữa kiểu cơ sở và kiểu
dẫn xuất. Ví dụ, nếu chúng ta có lớp cơ sở Bird và lớp dẫn xuất Parrot thì
một object của Parot cũng là object của Bird, kiểu Parrot cũng là kiểu Bird
(đương nhiên rồi, vẹt là chim mà!). Mối quan hệ này nhìn rất giống như
quan hệ kế thừa ở trên.
Trong khi đó, kế thừa liên quan chủ yếu đến tái sử dụng code: code của lớp
con thừa hưởng code của lớp cha. Một cách nói khác, đa hình liên quan tới
quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.
Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được
đồng nhất, thể hiện ở chỗ:
1. Class con thừa hưởng các thành viên của class cha (kế thừa, tái sử
dụng code);
2. Một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là
kiểu cơ sở có thể dùng để thay thế cho kiểu dẫn xuất (đa hình).
Để áp dụng được cơ chế ghi đè, cả lớp cha và lớp con cần phải phối hợp:
1. Lớp cha phải cho phép phương thức được phép ghi đè bằng cách thêm
từ khóa virtual trước khai báo phương thức;
2. Lớp con phải thông báo rõ việc ghi đè bằng cách thêm từ khóa
override trước định nghĩa phương thức.
Mặc định các phương thức của class không cho ghi đè mà chỉ cho phép che
giấu.
Tuy nhiên, các phương thức Equals, GetHashCode, ToString của lớp tổ
tiên System.Object đều cho phép ghi đè ở lớp hậu duệ.

460
Để xác định những phương thức nào cho phép ghi đè, chỉ cần viết từ khóa
override trong thân class (bên ngoài phương thức).

Ghi đè (override) phương thức


Che dấu được sử dụng chủ yếu để đảm bảo tương thích ngược giữa các
class. Cơ chế này không được sử dụng nhiều trong thực tế.
Ở phía khác, ghi đè được sử dụng rất phổ biến cùng với đa hình giúp tạo ra
một class đại diện cho các biến thể khác nhau.

Thực hành 1: cải tiến lớp ViewBase sử dụng ghi đè


Bước 1. Thay đổi lớp ViewBase
public class ViewBase
{
protected object Model;
protected Router Router = Router.Instance;
public ViewBase() { }
public ViewBase(object model) => Model = model;
// bổ sung phương thức virtual Render, cho phép ghi đè
public virtual void Render() { }
// chuyển phương thức RenderToFile sang virtual
public virtual void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}
}
Ở bước này, chúng ta bổ sung phương thức Render với đánh
dấu virtual để các lớp con có thể ghi đè phương thức này. Chúng ta cũng
chuyển phương thức RenderToFile sang virtual để các lớp con nếu cần
có thể ghi đè (ví dụ, để xuất sang một định dạng khác như xml hoặc plain
text).
Bước 2. Điều chỉnh khai báo của phương thức Render
Thay đổi phương thức Render trên cả 4 lớp view như sau:
public override void Render() …

461
Bước điều chỉnh này chỉ đơn giản là thêm từ khóa override vào trước khai
báo của phương thức Render trong từng lớp giao diện.
Bước 3. Xây dựng lớp ControllerBase
Tạo tập tin ControllerBase.cs trong thư mục Framework cho lớp
ControllerBase với code như sau:
namespace Framework
{
public class ControllerBase
{
public virtual void Render(ViewBase view, string path = "", bool both = false)
{
if (string.IsNullOrEmpty(path)) { view.Render(); return; }
if (both)
{
view.Render();
view.RenderToFile(path);
return;
}
view.RenderToFile(path);
}
}
}
Lớp ControllerBase định nghĩa một phương thức Render giúp gọi tới
phương thức Render hoặc RenderToFile của các lớp view một cách tiện lợi.
Bước 4. Điều chỉnh lớp BookController
namespace BookMan.ConsoleApp.Controllers
{
using DataServices;
using Framework;
using Views;
internal class BookController : ControllerBase
{
protected Repository Repository;
public BookController(SimpleDataAccess context)
{
Repository = new Repository(context);
}
public void Single(int id, string path = "")
{
var model = Repository.Select(id);
Render(new BookSingleView(model), path);
}
public void Create()
{
Render(new BookCreateView());
}
public void List(string path = "")
{
var model = Repository.Select();
Render(new BookListView(model), path);
}
462
public void Update(int id)
{
var model = Repository.Select(id);
Render(new BookUpdateView(model));
}
}
}
Trong điều chỉnh này chúng ta vận dụng phương thức Render kế thừa
từ ControllerBase giúp đơn giản hóa làm việc với các lớp view.
Có thể để ý thấy rằng, phương thức Render kế thừa từ ControllerBase
yêu cầu kiểu đầu vào là ViewBase nhưng chúng ta có thể cung cấp bất kỳ
object nào của các lớp view kế thừa từ ViewBase. Điều này đạt được nhờ cơ
chế đa hình kết hợp kế thừa mà chúng ta đã xem xét ở phần lý thuyết trên.
Do cơ chế ghi đè, phương thức Render được gọi không phải là Render của
lớp cha ViewBase mà là phương thức Render của từng lớp con. Chúng ta
có thể thấy ghi đè là cơ chế rất mạnh giúp chúng ta chỉ cần viết code một
lần cho kiểu cha nhưng có thể vận dụng cho các kiểu con và sử dụng được
những đặc thù riêng của kiểu con. Nhờ cơ chế này chúng ta không cần viết
code xử lý cho từng lớp con cụ thể.

Thực hành 2: kết hợp kế thừa và generic


Bước 1. Điều chỉnh tập tin ViewBase.cs
using System.IO;
namespace Framework
{
public class ViewBase
{
protected Router Router = Router.Instance;
public ViewBase() { }
public virtual void Render() { }
}
public class ViewBase<T> : ViewBase
{
protected T Model;
public ViewBase(T model) => Model = model;
public virtual void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}
}
}
Ở bước này trong tập tin ViewBase.cs chúng ta tách một phần lớp ViewBase
ra thành một lớp generic riêng ViewBase<T> và cho lớp này kế thừa
từ ViewBase.

463
Trong lớp ViewBase<T> sẽ tập trung những phương thức phải sử dụng thông
tin từ model. Kiểu dữ liệu của model sẽ được quyết định khi các lớp con kế
thừa từ lớp ViewBase<T>.
Bước 2. Điều chỉnh các lớp view
Lớp BookListView
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookListView : ViewBase<Book[]>
{
public BookListView(Book[] model) : base(model) { }
public override void Render()
{
if (Model.Length == 0)
{
ViewHelp.WriteLine("No book found!", ConsoleColor.Yellow);
return;
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("THE BOOK LIST");
Console.ForegroundColor = ConsoleColor.Yellow;
foreach (Book b in Model)
{
ViewHelp.Write($"[{b.Id}]", ConsoleColor.Yellow);
ViewHelp.WriteLine($" {b.Title}", b.Reading ? ConsoleColor.Cyan :
ConsoleColor.White);
}
ViewHelp.WriteLine($"{Model.Length} item(s)", ConsoleColor.Green);
}
}
}
Lớp BookSingleView
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookSingleView : ViewBase<Book>
{
public BookSingleView(Book model) : base(model) { }
public override void Render()
{
if (Model == null)
{
ViewHelp.WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);
return;
}
ViewHelp.WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);
Console.WriteLine($"Authors: {Model.Authors}");
Console.WriteLine($"Title: {Model.Title}");
Console.WriteLine($"Publisher: {Model.Publisher}");

464
Console.WriteLine($"Year: {Model.Year}");
Console.WriteLine($"Edition: {Model.Edition}");
Console.WriteLine($"Isbn: {Model.Isbn}");
Console.WriteLine($"Tags: {Model.Tags}");
Console.WriteLine($"Description: {Model.Description}");
Console.WriteLine($"Rating: {Model.Rating}");
Console.WriteLine($"Reading: {Model.Reading}");
Console.WriteLine($"File: {Model.File}");
Console.WriteLine($"File Name: {Model.FileName}");
}
}
}
Lớp BookUpdateView
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookUpdateView : ViewBase<Book>
{
public BookUpdateView(Book model) : base(model)
{
}
public override void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
var authors = ViewHelp.InputString("Authors", Model.Authors);
var title = ViewHelp.InputString("Title", Model.Title);
var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
var tags = ViewHelp.InputString("Tags", Model.Tags);
var description = ViewHelp.InputString("Description", Model.Description);
var tập tin = ViewHelp.InputString("File", Model.File);
var year = ViewHelp.InputInt("Year", Model.Year);
var edition = ViewHelp.InputInt("Edition", Model.Edition);
var rating = ViewHelp.InputInt("Rate", Model.Rating);
var reading = ViewHelp.InputBool("Reading", Model.Reading);
}
}
}
Chúng ta thấy, lớp generic cũng có thể đóng vai trò lớp cha để tạo ra các
lớp con.
Trong bước này chúng ta điều chỉnh để các lớp view (trừ lớp
BookCreateView không cần dữ liệu từ BookController) kế thừa từ lớp cha
generic ViewBase<T>, trong đó T được thay thế bằng các kiểu dữ liệu mà
view chờ đợi từ controller: BookListView chờ đợi một mảng Book[],
BookSingleView và BookUpdateView chờ đợi một object kiểu Book.
Do đó, Book[] và Book được sử dụng cho vị trí của T khi dùng ViewBase<T>
làm lớp cơ sở. Các lớp con kế thừa từ ViewBase<T> trong trường hợp này
lại không phải là lớp generic nữa, vì chúng ta đã cung cấp kiểu dữ liệu cụ
thể cho lớp cha trước khi cho lớp con kế thừa.
465
Lưu ý rằng ViewBase và ViewBase<T> nhìn tương tự nhau nhưng đây là hai
lớp khác nhau (thể hiện rằng chúng có thể kế thừa nhau). Thông thường
các lớp non-generic và generic hay đi thành cặp với nhau.
Bước 3. Điều chỉnh lớp ControllerBase
namespace Framework
{
public class ControllerBase
{
public virtual void Render(ViewBase view) { view.Render(); }
public virtual void Render<T>(ViewBase<T> view, string path = "", bool both =
false)
{
if (string.IsNullOrEmpty(path)) { view.Render(); return; }
if (both)
{
view.Render();
view.RenderToFile(path);
return;
}
view.RenderToFile(path);
}
}
}
Bước điều chỉnh này giúp ControllerBase “thích nghi” với hai
lớp ViewBase và ViewBase<T>. Phương thức Render thứ nhất chỉ làm việc
với object kiểu ViewBase; phương thức thứ hai thuộc loại generic và làm
việc với object của ViewBase<T>.

466
Bước 4. Dịch và chạy thử chương trình với tất cả các lệnh đã biết

Kết quả thực hiện chương trình

467
Cải tiến view (4): lớp trừu tượng, phương thức trừu
tượng
Trong bài học này chúng ta sẽ xem xét vấn đề cuối cùng có liên quan đến
kế thừa: lớp trừu tượng và phương thức trừu tượng. Chúng ta cũng sẽ vận
dụng kỹ thuật này để hoàn thiện tất cả các lớp view hiện có.

Thực hành 1: Cải tiến lớp ViewBase và ViewBase<T> thành


lớp abstract
using System.IO;
namespace Framework
{
public abstract class ViewBase
{
protected Router Router = Router.Instance;
public ViewBase() { }
public abstract void Render();
}
public abstract class ViewBase<T> : ViewBase
{
protected T Model;
public ViewBase(T model) => Model = model;
public virtual void RenderToFile(string path)
{
ViewHelp.WriteLine($"Saving data to tập tin '{path}'");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
File.WriteAllText(path, json);
ViewHelp.WriteLine("Done!");
}
}
}
Khi xây dựng lớp ViewBase, bạn không biết được người sử dụng sẽ render
object nào. Nói cách khác, phần thân của Render sẽ do class kế thừa nó
quyết định thông qua cơ chế ghi đè. Do vậy, trong phần cải tiến trên bạn
chuyển phương thức Render thành phương thức abstract. Cũng do đó, lớp
ViewBase cũng phải chuyển thành lớp abstract.
Bởi vì lớp ViewBase<T> kế thừa ViewBase nhưng lại không ghi đè phương
thức Render nên cũng phải đặt là abstract.
Cơ chế này tạo ra một “hợp đồng” ràng buộc giữa các lớp view và
ViewBase/ViewBase<T>: Từ bây giờ, mọi class kế thừa ViewBase và
ViewBase<T> đều bắt buộc phải ghi đè phương thức Render. Visual Studio
sẽ thông báo lỗi nếu không nhìn thấy phương thức ghi đè của Render trong
các lớp dẫn xuất.

468
Trong phần thực hành trước chúng ta đã ghi đè phương thức Render trong
tất cả các lớp dẫn xuất của ViewBase và ViewBase<T> nên không cần điều
chỉnh ở các lớp này.

Thực hành 2: Xây dựng nhóm class hỗ trợ gửi thông báo ra
console
Bước 1. Xây dựng mới lớp Message
Tạo tập tin mã nguồn mới Message.cs trong thư mục Framework cho
lớp Message và thêm code như sau:
using System;
namespace Framework
{
public enum MessageType { Success, Information, Error, Confirmation }
public class Message
{
public MessageType Type { get; set; } = MessageType.Success;
public string Label { get; set; }
public string Text { get; set; } = "Your action has completed successfully";
public string BackRoute { get; set; }
}
public class MessageView : ViewBase<Message>
{
public MessageView(Message model) : base(model)
{
}
public override void Render()
{
switch (Model.Type)
{
case MessageType.Success:
ViewHelp.WriteLine(Model.Label != null ? Model.Label.ToUpper() :
"SUCCESS", ConsoleColor.Green);
break;
case MessageType.Error:
ViewHelp.WriteLine(Model.Label != null ? Model.Label.ToUpper() :
"ERROR!", ConsoleColor.Red);
break;
case MessageType.Information:
ViewHelp.WriteLine(Model.Label != null ? Model.Label.ToUpper() :
"INFORMATION!", ConsoleColor.Yellow);
break;
case MessageType.Confirmation:
ViewHelp.WriteLine(Model.Label != null ? Model.Label.ToUpper() :
"CONFIRMATION", ConsoleColor.Cyan);
break;
}
if (Model.Type != MessageType.Confirmation)
ViewHelp.WriteLine(Model.Text, ConsoleColor.White);
else
{
ViewHelp.Write(Model.Text, ConsoleColor.Magenta);
var answer = Console.ReadLine().ToLower();
if (answer == "y" || answer == "yes")

469
Router.Forward(Model.BackRoute);
}
}
}
}
Nhiệm vụ của lớp Message và MessageView là gửi các thông báo ngắn từ
controller tới người dùng.
Trong quá trình sử dụng giao diện về sau, mỗi hành động thành công hoặc
thất bại đều phải được thông báo trở lại cho người dùng.
Ví dụ, khi cập nhật thành công một cuốn sách sẽ phải thông báo trở lại cho
người dùng, trước khi muốn xóa một cuốn sách phải hỏi ý kiến người
dùng,....
Tất cả các thông báo này đều tương đối độc lập với nghiệp vụ của bài toán
quản lý và có thể tái sử dụng qua nhiều dự án khác. Ngoài ra, cơ chế thông
báo này cũng giúp hạn chế phải viết những lớp view nhỏ lẻ chỉ để hiển thị
một vài thông báo.
Khi nhập code cho lớp MessageView có thể để ý, ngay sau khi gõ
public class MessageView : ViewBase<Message>
{
}

Visual Studio sẽ hiện thông báo lỗi (gạch chân đỏ ở tên lớp MessageView).
Nếu sử dụng Quick Action => Implement abstract class, Visual Studio
nhanh chóng giúp tạo ra cái khung của phương thức cần ghi đè Render:
public override void Render()
{
throw new NotImplementedException();
}

Nếu dùng Quick Action => Generate Constructor sẽ sinh ra luôn hộ chúng
ta phương thức khởi tạo
public MessageView(Message model) : base(model)
{
}

Bước 2. Điều chỉnh lớp ControllerBase


namespace Framework
{
public class ControllerBase
{
public virtual void Render(ViewBase view) { view.Render(); }

470
public virtual void Render<T>(ViewBase<T> view, string path = "", bool both =
false)
{
if (string.IsNullOrEmpty(path)) { view.Render(); return; }
if (both)
{
view.Render();
view.RenderToFile(path);
return;
}
view.RenderToFile(path);
}
public virtual void Render(Message message) => Render(new
MessageView(message));
public virtual void Success(string text, string label = "SUCCESS") =>
Render(new Message { Type = MessageType.Success, Text = text, Label = label });
public virtual void Inform(string text, string label = "INFORMATION") =>
Render(new Message { Type = MessageType.Information, Text = text, Label = label });
public virtual void Error(string text, string label = "ERROR!") => Render(new
Message { Type = MessageType.Error, Text = text, Label = label });
public virtual void Confirm(string text, string route, string label =
"CONFIRMATION") => Render(new Message { Type = MessageType.Confirmation, Text = text,
Label = label, BackRoute = route });
}
}
Bước điều chỉnh này bổ sung cho lớp ControllerBase thêm một số phương
thức tiện ích hỗ trợ cho việc hiển thị các loại thông báo cho người dùng. Các
phương thức tiện ích này sẽ được sử dụng nhiều trong bài tiếp theo khi
chúng ta hoàn thiện các chức năng của chương trình ứng dụng.

Kết luận
Trong bài này chúng ta đã vận dụng lớp trừu tượng để xây dựng hoàn thiện
các lớp view (BookSingleView, BookListView, BookCreateView,
BookUpdateView) cũng như xây dựng thêm một cặp class mới giúp hiển thị
các thông báo ngắn (Message, MessageView).
Đến giai đoạn này, tất cả các lớp hỗ trợ của ứng dụng đã hoàn thiện. Trong
bài tiếp theo, chúng ta sẽ sử dụng các lớp hỗ trợ này để hoàn thiện các chức
năng hiện có cũng như xây dựng bổ sung thêm một số chức năng như đã
phân tích.

471
Hoàn thiện (1): nhập mới, cập nhật, partial class
Ở phần trước chúng ta đã xây dựng hoàn chỉnh tất cả các lớp hỗ trợ của
chương trình. Trong bài này chúng ta sẽ áp dụng để hoàn thiện các chức
năng hiện có: hiển thị (single, list), nhập, cập nhật.

Thực hành 1: hoàn thiện chức năng nhập dữ liệu


Trong các bài trước chúng ta đang làm dở chức năng nhập dữ liệu vì chưa
thể truyền được thông tin người dùng nhập về controller. MVC quy định rằng
các view không trực tiếp khởi tạo object. Khởi tạo object là nhiệm vụ của
controller.
Do vậy, lớp BookCreateView sau khi nhận thông tin từ người dùng phải
chuyển thông tin đó về cho BookController để xử lý.
Bước 1. Thay đổi nội dung của lớp BookCreateView
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
internal class BookCreateView : ViewBase
{
public BookCreateView() { }
public override void Render()
{
ViewHelp.WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
var title = ViewHelp.InputString("Title");
var authors = ViewHelp.InputString("Authors");
var publisher = ViewHelp.InputString("Publisher");
var year = ViewHelp.InputInt("Year");
var edition = ViewHelp.InputInt("Edition");
var tags = ViewHelp.InputString("Tags");
var description = ViewHelp.InputString("Description");
var rate = ViewHelp.InputInt("Rate");
var reading = ViewHelp.InputBool("Reading");
var tập tin = ViewHelp.InputString("File");
var request =
"do create ? " +
$"title = {title}" +
$" & authors = {authors}" +
$" & publisher = {publisher}" +
$" & year = {year}" +
$" & edition = {edition}" +
$" & tags = {tags}" +
$" & description = {description}" +
$" & rate = {rate}" +
$" & reading = {reading}" +
$" & tập tin = {tập tin}";
Router.Forward(request);
}
}
}

472
Ở bước này chúng ta tạo ra một truy vấn với route là “do create” và danh
sách tham số chứa tất cả giá trị mà người dùng nhập. Truy vấn này sẽ được
router gửi ngược về controller.
Bước 2. Thay đổi phương thức Create của BookController
public void Create(Book book = null)
{
if (book == null)
{
Render(new BookCreateView());
return;
}
Repository.Insert(book);
Success("Book created!");
}
Ở bước này chúng ta thay đổi phương thức Create để có thể thực hiện hai
nhiệm vụ:
1. nếu tham số book nhận giá trị null thì sẽ kích hoạt BookCreateView
để người dùng nhập thông tin;
2. nếu book khác null thì yêu cầu Repository thêm dữ liệu và hiển thị
thông báo thành công.
Lưu ý bổ sung using Models; ở ngay sau khai báo namespace.
namespace BookMan.ConsoleApp.Controllers
{
using Models;
using DataServices;
using Framework;
using Views;
...

Bước 3. Điều chỉnh phương thức Main


private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
SimpleDataAccess context = new SimpleDataAccess();
BookController controller = new BookController(context);
Router r = Router.Instance;
r.Register("about", About);
r.Register("help", Help);
r.Register(route: "create",
action: p => controller.Create(),
help: "[create]\r\nnhập sách mới");
r.Register(route: "do create",
action: p => controller.Create(toBook(p)),
help: "this route should be used only in code");
r.Register(route: "update",
action: p => controller.Update(p["id"].ToInt()),

473
help: "[update ? id = <value>]\r\ntìm và cập nhật sách");
r.Register(route: "list",
action: p => controller.List(),
help: "[list]\r\nhiển thị tất cả sách");
r.Register(route: "single",
action: p => controller.Single(p["id"].ToInt()),
help: "[single ? id = < value >]\r\nhiển thị một cuốn sách theo id");
r.Register(route: "list tập tin",
action: p => controller.List(p["path"]),
help: "[list tập tin ? path = <value>]\r\nhiển thị tất cả sách");
r.Register(route: "single tập tin",
action: p => controller.Single(p["id"].ToInt(), p["path"]),
help: "[single tập tin ? id = <value> & path = <value>]");
//hàm cục bộ để chuyển object của Parameter sang object của Book
Models.Book toBook(Parameter p)
{
var b = new Models.Book();
if (p.ContainsKey("id")) b.Id = p["id"].ToInt();
if (p.ContainsKey("authors")) b.Authors = p["authors"];
if (p.ContainsKey("title")) b.Title = p["title"];
if (p.ContainsKey("publisher")) b.Publisher = p["publisher"];
if (p.ContainsKey("year")) b.Year = p["year"].ToInt();
if (p.ContainsKey("edition")) b.Edition = p["edition"].ToInt();
if (p.ContainsKey("isbn")) b.Isbn = p["isbn"];
if (p.ContainsKey("tags")) b.Tags = p["tags"];
if (p.ContainsKey("description")) b.Description = p["description"];
if (p.ContainsKey("tập tin")) b.File = p["tập tin"];
if (p.ContainsKey("rate")) b.Rating = p["rate"].ToInt();
if (p.ContainsKey("reading")) b.Reading = p["reading"].ToBool();
return b;
}
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
Router.Instance.Forward(request);
Console.WriteLine();
}
}

Ở bước này chúng ta thêm một route nữa để gọi phương thức Create của
BookController.
r.Register(route: "do create",
action: p => controller.Create(toBook(p)),
help: "this route should be used only in code");
Để đơn giản hóa việc chuyển object của Parameter sang object của Book,
chúng ta viết thêm một hàm cục bộ toBook ở ngay trong thân của Main.

474
Bước 4. Dịch và chạy thử chương trình với chức năng thêm dữ liệu

Kết quả chạy chương trình với lệnh create

Thực hành 2: cải tiến lớp Program với partial class


Trong phần thực hành 1 chúng ta nhận thấy rằng phương thức Main đang
mở rộng ra rất nhanh khi chúng ta bổ sung thêm các route mới để hoàn
thiện các chức năng xử lý dữ liệu.
Trong các phần thực hành tiếp theo, số lượng code ở phương thức này còn
tăng lên nữa. Code được bổ sung đều là code để đăng ký route cho các chức
năng mới. Chúng ta sẽ tìm cách tách rời phần đăng ký route này sang một
tập tin riêng để dễ dàng thay đổi về sau.
Ở đây chúng ta áp dụng một giải pháp đơn giản: sử dụng partial class.
Partial class là một tính năng của C# cho phép định nghĩa một class trên nhiều tập tin vật lý
khác nhau.
Từ đầu đến giờ, mỗi class đều được xây dựng trong một tập tin đặt trùng tên với class. Nếu
trong class có nhiều logic khác nhau, chúng ta hoàn toàn có thể xem xét tách các logic đó sang
các tập tin riêng rẽ sử dụng partial class.
Nếu trong một class có những thành phần code thường biến động theo quá trình phát triển và
có những thành phần ổn định thì cũng có thể xem xét tách chúng ra các tập tin khác nhau để dễ

475
quản lý. Trong đó, thành phần biến động ra một tập tin riêng, thành phần ổn định để lại một tập
tin riêng.
Bước 1. Thêm mới tập tin Program.Config.cs
1. Click phải vào project BookMan.ConsoleApp, chọn Add => New Item;
2. Trong cửa sổ Add New Item, chọn nhánh Code bên trong Visual C#
Items (bên tay trái);
3. Chọn mục Code File trong danh sách bên tay phải

Tạo code tập tin mới


Đặt tên tập tin là Program.Config.cs và ấn nút Add.
Bước 2. Viết code cho tập tin Program.Config.cs
namespace BookMan.ConsoleApp
{
using Models;
using Controllers;
using DataServices;
using Framework;
internal partial class Program
{
private static void ConfigRouter()
{
SimpleDataAccess context = new SimpleDataAccess();
BookController controller = new BookController(context);
Router r = Router.Instance;
r.Register("about", About);
r.Register("help", Help);
r.Register(route: "create",
action: p => controller.Create(),
476
help: "[create]rnnhập sách mới");
r.Register(route: "do create",
action: p => controller.Create(toBook(p)),
help: "this route should be used only in code");
r.Register(route: "list",
action: p => controller.List(),
help: "[list]rnhiển thị tất cả sách");
r.Register(route: "list tập tin",
action: p => controller.List(p["path"]),
help: "[list tập tin ? path = <value>]rnhiển thị tất cả sách");
r.Register(route: "single",
action: p => controller.Single(p["id"].ToInt()),
help: "[single ? id = < value >]rnhiển thị một cuốn sách theo id");
r.Register(route: "single tập tin",
action: p => controller.Single(p["id"].ToInt(), p["path"]),
help: "[single tập tin ? id = <value> & path = <value>]");
//r.Register(route: "",
// action: null,
// help: "");
#region helper
//local function to convert parameter to book object
Book toBook(Parameter p)
{
var b = new Book();
if (p.ContainsKey("id")) b.Id = p["id"].ToInt();
if (p.ContainsKey("authors")) b.Authors = p["authors"];
if (p.ContainsKey("title")) b.Title = p["title"];
if (p.ContainsKey("publisher")) b.Publisher = p["publisher"];
if (p.ContainsKey("year")) b.Year = p["year"].ToInt();
if (p.ContainsKey("edition")) b.Edition = p["edition"].ToInt();
if (p.ContainsKey("isbn")) b.Isbn = p["isbn"];
if (p.ContainsKey("tags")) b.Tags = p["tags"];
if (p.ContainsKey("description")) b.Description = p["description"];
if (p.ContainsKey("tập tin")) b.File = p["tập tin"];
if (p.ContainsKey("rate")) b.Rating = p["rate"].ToInt();
if (p.ContainsKey("reading")) b.Reading = p["reading"].ToBool();
return b;
}
#endregion
}
}
}
Ở bước này chúng ta khai báo thêm một class Program nữa và nằm trong
cùng không gian tên với class Program đã có sẵn trong tập tin Program.cs.
Chúng ta thêm từ khóa partial vào trước định nghĩa của lớp Program. Nếu
không có từ khóa partial ở đây thì C# sẽ báo lỗi vì có hai class trùng tên
nhau trong cùng một không gian tên.
Trong phần này của lớp Program này định nghĩa thêm phương thức
static ConfigRouter và di chuyển một phần của phương thức Main sang
phương thức mới ConfigRouter.
Bước 3. Điều chỉnh tập tin Program.cs
using System;
477
namespace BookMan.ConsoleApp
{
using Framework;
internal partial class Program
{
private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
ConfigRouter();
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
Router.Instance.Forward(request);
Console.WriteLine();
}
}
private static void Help(Parameter parameter)
{
if (parameter == null)
{
ViewHelp.WriteLine("SUPPORTED COMMANDS:", ConsoleColor.Green);
ViewHelp.WriteLine(Router.Instance.GetRoutes(), ConsoleColor.Yellow);
ViewHelp.WriteLine("type: help ? cmd= <command> to get command
details", ConsoleColor.Cyan);
return;
}
Console.BackgroundColor = ConsoleColor.DarkBlue;
var command = parameter["cmd"].ToLower();
ViewHelp.WriteLine(Router.Instance.GetHelp(command));
}
private static void About(Parameter parameter)
{
ViewHelp.WriteLine("BOOK MANAGER version 1.0", ConsoleColor.Green);
ViewHelp.WriteLine("by Mype Nguyen @ ictu.edu.vn", ConsoleColor.Magenta);
}
}
}
Ở bước này chúng ta thêm từ khóa partial vào trước định nghĩa của class
Program, đồng thời gọi phương thức ConfigRouter đã định nghĩa ở phần
khác của lớp Program trong tập tin Program.Config.cs.
Đây có thể xem như thành phần “tĩnh” hơn của Program. Toàn bộ những
code khác của Main đã di chuyển sang phương thức ConfigRouter (thành
phần “động” của Program).
Partial class đặc biệt hữu ích khi sử dụng chung với các công cụ sinh code tự động. Phần class
sinh ra tự động thường được định nghĩa sẵn với từ khóa partial.
Người dùng có thể tự định nghĩa phần của riêng mình mà không ảnh hưởng đến phần sinh tự
động. Khi phần sinh tự động thay đổi, phần do người dùng định nghĩa không bị ảnh hưởng.
Khi học đến công nghệ Windows forms chúng ta sẽ thường xuyên gặp partial class.
Như vậy, qua phần thực hành vừa rồi chúng ta đã điều chỉnh lớp Program
thành dạng partial class nằm trên hai tập tin: Program.cs và
478
Program.Config.cs. Mặc dù nằm ở hai tập tin khác nhau nhưng lại là cùng
một class. File Program.cs chứa thành phần ít biến đổi (thành phần tĩnh
hơn); tập tin Program.Config.cs chứa thành phần thường biến động (các
route được liên tục bổ sung khi phát triển các chức năng của ứng dụng).

Thực hành 3: hoàn thiện chức năng cập nhật dữ liệu


Tương tự như chức năng thêm dữ liệu, chức năng cập nhật dữ liệu trước đây
cũng không truyền được thông tin về controller. Trong phần thực hành này
chúng ta sẽ xây dựng hoàn thiện.
Bước 1. Điều chỉnh lớp BookUpdateView
using System;
namespace BookMan.ConsoleApp.Views
{
using Framework;
using Models;
internal class BookUpdateView : ViewBase<Book>
{
public BookUpdateView(Book model) : base(model)
{
}
public override void Render()
{
ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
var authors = ViewHelp.InputString("Authors", Model.Authors);
var title = ViewHelp.InputString("Title", Model.Title);
var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
var tags = ViewHelp.InputString("Tags", Model.Tags);
var description = ViewHelp.InputString("Description", Model.Description);
var tập tin = ViewHelp.InputString("File", Model.File);
var year = ViewHelp.InputInt("Year", Model.Year);
var edition = ViewHelp.InputInt("Edition", Model.Edition);
var rating = ViewHelp.InputInt("Rate", Model.Rating);
var reading = ViewHelp.InputBool("Reading", Model.Reading);
var request =
"do update ? " +
$"id = {Model.Id}" +
$"& title = {title}" +
$"& authors = {authors}" +
$"& publisher = {publisher}" +
$"& year = {year} &" +
$"& edition = {edition}" +
$"& tags = {tags}" +
$"& description = {description}" +
$"& rate = {rating}" +
$"& reading = {reading}" +
$"& tập tin = {tập tin}";
Router.Forward(request);
}
}
}

479
Bước điều chỉnh này tương tự như đối với lớp BookCreateView: tạo và gửi
truy vấn về controller.
Bước 2. Điều chỉnh phương thức Update của BookController
public void Update(int id, Book book = null)
{
if (book == null)
{
var model = Repository.Select(id);
var view = new BookUpdateView(model);
Render(view);
return;
}
Repository.Update(id, book);
Success("Book updated!");
}
Tương tự với việc điều chỉnh phương thức Create, bước điều chỉnh này giúp
phương thức Update làm việc với hai giai đoạn của việc cập nhật:
1. Nếu chỉ cung cấp id mà không cung cấp một object của lớp Book thì
chỉ kích hoạt giao diện BookUpdateView;
2. Nếu cung cấp cả một object của lớp Book thì sẽ thực sự cập nhật
thông tin sách và thông báo kết quả lại cho người dùng.
Bước 3. Bổ sung route vào ConfigRouter
r.Register(route: "update", action: p =>
controller.Update(p["id"].ToInt()), help: "[update ? id =
<value>]rntìm và cập nhật sách");
r.Register(route: "do update", action: p =>
controller.Update(p["id"].ToInt(), toBook(p)), help: "this
route should be used only in code");

480
Bước 4. Dịch và chạy thử chương trình với lệnh update

Kết quả thực hiện lệnh update

481
Hoàn thiện (2): xóa, lọc, tìm kiếm, xử lý tập tin
Trong bài học này chúng ta tiếp tục hoàn thiện các chức năng chính như đã
phân tích, bao gồm: bổ sung chức năng xóa dữ liệu, lọc dữ liệu, tự động tìm
sách trong thư mục, mở tập tin pdf từ chương trình, đánh dấu (bookmark)
các cuốn sách đang đọc.

Thực hành 1: thêm chức năng xóa dữ liệu


Chức năng xóa dữ liệu chưa được thực hiện ở các bài trước đây. Ở chức
năng này chúng ta không xây dựng một lớp view riêng mà tận dụng khả
năng của lớp MessageView (đã xây dựng ở bài này).
Bước 1. Tạo phương thức Delete
Xây dựng phương thức Delete trong lớp BookController như sau:
public void Delete(int id, bool process = false)
{
if (process == false)
{
var b = Repository.Select(id);
Confirm($"Do you want to delete this book ({b.Title})? ", $"do
delete?id={b.Id}");
}
else
{
Repository.Delete(id);
Success("Book deleted!");
}
}
Trong phương thức Delete chúng ta vận dụng lớp Message và MessageView
đã xây dựng ở bài trước để đưa ra các thông báo ngắn cho người dùng mà
không cần xây dựng một lớp view riêng cho phương thức Delete.
Bước 2. Bổ sung thêm route
r.Register(route: "delete", action: p =>
controller.Delete(p["id"].ToInt()), help: "[delete ? id =
<value>");
r.Register(route: "do delete", action: p =>
controller.Delete(p["id"].ToInt(), true), help: "this route
should be used only in code");

482
Bước 3. Dịch và chạy thử với lệnh delete

Kết quả thực hiện lệnh delete

Thực hành 2: thêm chức năng lọc dữ liệu


Ở phần thực hành này chúng ta bổ sung thêm một tính năng mới: lọc dữ
liệu. Ở chức năng này, người dùng cung cấp một từ khóa bất kỳ. Chương
trình sẽ tìm trong danh sách dữ liệu tất cả những cuốn sách mà tiêu đề, tác
giả, nhà xuất bản, tag và mô tả có chứa từ khóa này.
Bước 1. Thêm phương thức Filter vào lớp BookController
public void Filter(string key)
{
var model = Repository.Select(key);
if (model.Length == 0)
Inform("No matched book found!");
else
Render(new BookListView(model));
}

Bước 2. Bổ sung thêm route


r.Register(route: "filter", action: p => controller.Filter(p["key"]),
help: "[filter ? key = <value>]rntìm sách theo từ khóa");
483
Bước 3. Dịch và chạy thử chương trình với lệnh filter

Kết quả thực hiện lệnh filter


Khi lọc dữ liệu, bạn cũng có thể muốn sắp xếp dữ liệu theo tiêu chí nào đó, ví dụ theo tựa sách,
theo tác giả, theo năm xuất bản. Bạn có thể đọc thêm về các thuật toán sắp xếp để tự thực hiện.
Thực hành 3: thêm chức năng tìm sách trong thư mục
Như đã mô tả ở bài 1, đây là chức năng giúp xây dựng dữ liệu sách tự động.
Người dùng cung cấp một đường dẫn tới thư mục chứa các tập tin sách,
chương trình sẽ phát hiện tất cả các tập tin pdf có trong đó và sử dụng tên
và đường dẫn của các tập tin này để tạo ra dữ liệu cơ bản về kho sách.
Trong phần thực hành này, chúng ta sẽ tạo ra thêm một lớp điều khiển mới
để thực hiện các chức năng liên quan tới tập tin, thư mục.
Bước 1. Xây dựng lớp ShellController
Tạo mới tập tin ShellController.cs trong thư mục Controllersdành cho lớp
ShellController và viết code cho lớp ShellController như sau:
using System.Diagnostics;
using System.IO;
namespace BookMan.ConsoleApp.Controllers
{
using DataServices;
using Models;
using Views;
using Framework;
internal class ShellController : ControllerBase
{
protected Repository Repository;
public ShellController(SimpleDataAccess context)

484
{
Repository = new Repository(context);
}
public void Shell(string folder, string ext = "*.pdf")
{
if (!Directory.Exists(folder))
{
Error("Folder not found!");
return;
}
var files = Directory.GetFiles(folder, ext ?? "*.pdf",
SearchOption.AllDirectories);
foreach (var f in files)
{
Repository.Insert(new Book { Title =
Path.GetFileNameWithoutExtension(f), File = f });
}
if (files.Length > 0)
{
//Render(new BookListView(Repository.Select()));
Success($"{files.Length} item(s) found!");
return;
}
Inform("No item found!", "Sorry!");
}
}
}
Ở bước này chúng ta xây dựng lớp ShellController kế thừa từ lớp
ControllerBase đã xây dựng ở các bài trước.
Bước 2. Bổ sung code cho ConfigRouter
BookController controller = new BookController(context);
ShellController shell = new ShellController(context);

Bước này chúng ta chỉ bổ sung khai báo và khởi tạo một object của
ShellController. Object này dùng để thực hiện các chức năng mới liên
quan đến tập tin và thư mục.
Bước 3. Bổ sung route mới cho ConfigRouter
r.Register(route: "add shell", action: p => shell.Shell(p["path"],
p["ext"]), help: "[add shell ? path = <value>]");

485
Bước 4. Dịch và chạy thử chương trình

Kết quả thực hiện lệnh add shell


Trong phần thực hành trên chúng ta lần đầu áp dụng các lớp .NET hỗ trợ
làm việc với hệ thống tập tin của Windows.
Tất cả các lớp để làm việc với tập tin trong .NET nằm trong không gian tên
System.IO. Ba class chính để làm việc với hệ thống tập tin là Directory
(làm việc với thư mục), File (làm việc với tập tin), Path (làm việc với
đường dẫn).
Lớp Directory
Lớp Directory chứa hầu hết các phương thức tĩnh giúp làm việc với tập tin
và thư mục. Trong phần thực hành chúng ta đã sử dụng một số phương
thức của lớp này giúp kiểm tra đường dẫn và giúp lấy danh sách tập tin
trong một thư mục.

486
 Phương thức tĩnh Exists của lớp Directory: kiểm tra xem một
đường dẫn tới thư mục có tồn tại hoặc chính xác không.
if (!Directory.Exists(folder))
{
Error("Folder not found!");
return;
}
 Phương thức tĩnh GetFiles của lớp Directory: tìm tất cả các tập tin
trong thư mục có phần mở rộng là pdf:
Directory.GetFiles(folder, ext ?? "*.pdf", SearchOption.AllDirectories);
Phương thức này sử dụng ba tham số:
1. đường dẫn tới thư mục;
2. mẫu tìm kiếm: mẫu văn bản mà phương thức GetFiles sử dụng
trong quá trình tìm kiếm. GetFiles chỉ trả lại những tập tin mà tên
phù hợp với mẫu văn bản của tham số này.
3. phạm vi tìm kiếm: xác định xem phương thức GetFiles chỉ tìm trong
thư mục được chỉ định (TopDirectoryOnly) hay tìm cả trong các thư
mục con của nó (AllDirectories).
Kết quả thực hiện của phương thức này là một mảng string chứa tên đầy
đủ (bao gồm cả đường dẫn) của các tập tin tìm thấy.
Lớp Path
Lớp Path cũng chứa hầu hết các phương thức tĩnh giúp phân tích đường dẫn
tới tập tin hoặc thư mục. Ở phần thực hành chúng ta đã sử dụng một phương
thức của lớp này để phân tách tên tập tin khỏi đường dẫn và phần mở rộng.
Phương thức GetFileNameWithoutExtension của lớp Path trích ra phần
tên của tập tin, bỏ phần đường dẫn và phần mở rộng.
Repository.Insert(new Book { Title = Path.GetFileNameWithoutExtension(f),
File = f });
Tên tập tin này được sử dụng tạm thời làm tiêu đề của sách (vì thường sách
điện tử đặt tên tập tin trùng với tiêu đề sách).
Các phương thức của hai class Directory và Path đều tương đối dễ sử dụng.
Bạn đọc có thể tự mình tìm hiểu các phương thức khác.

Thực hành 4: thêm chức năng đọc sách từ chương trình


Chức năng này cho phép mở tập tin pdf bằng chương trình đọc pdf mặc định
của Windows (Acrobat Reader, Foxit Reader,…). Chức này này tiện lợi cho
người sử dụng vì không cần phải mở các thư mục để tìm đến tập tin.

487
Bước 1. Thêm phương thức vào ShellController
public void Read(int id)
{
var book = Repository.Select(id);
if (book == null)
{
Error("Book not found!");
return;
}
if (!File.Exists(book.File))
{
Error("File not found!");
return;
}
Process.Start(book.File);
Success($"You are reading the book '{book.Title}'");
}

Bước 2. Bổ sung route vào ConfigRouter


r.Register(route: "read", action: p => shell.Read(p["id"].ToInt()), help:
"[read ? id = <value>]");

Bước 3. Dịch và chạy thử chương trình

Kết quả thực hiện lệnh read

488
Sau lệnh này, cuốn “A first course in discrete mathematics” sẽ được mở ra
bằng chương trình đọc pdf mặc định trên Windows.

Thực hành 5: đánh dấu những cuốn sách đang đọc


Chức năng này cho phép đánh dấu những cuốn sách đang đọc để có thể dễ
dàng tìm đọc tiếp. Khi xây dựng lớp Book chúng ta có thuộc tính Reading
kiểu bool dành cho chức năng này.
Bước 1. Bổ sung phương thức vào lớp Repository
public Book[] SelectMarked()
{
var list = new List<Book>();
foreach (var b in Books)
{
if (b.Reading) list.Add(b);
}
return list.ToArray();
}

Bước 2. Bổ sung hai phương thức vào lớp BookController


public void Mark(int id, bool read = true)
{
var book = Repository.Select(id);
if (book == null)
{
Error("Book not found!");
return;
}
book.Reading = read;
Success($"The book '{book.Title}' are marked as { (read ? "READ" :
"UNREAD")}");
}
public void ShowMarks()
{
var model = Repository.SelectMarked();
var view = new BookListView(model);
Render(view);
}

Bước 3. Bổ sung route vào ConfigRouter


r.Register(route: "mark", action: p => controller.Mark(p["id"].ToInt()),
help: "[mark ? id = <value>]");
r.Register(route: "unmark", action: p => controller.Mark(p["id"].ToInt(),
false), help: "[unmark ? id = <value>]");
r.Register(route: "show marks", action: p => controller.ShowMarks(), help:
"[show marks]");

489
Bước 4. Dịch và chạy thử chương trình

Kết quả thực hiện lệnh mark và show marks


Chức năng này tận dụng lại lớp BookListView đã xây dựng từ trước. Lớp này
có thể hiển thị các cuốn sách đang đọc bằng màu Cyan, còn các cuốn sách
khác hiện màu trắng.

Thực hành 6: bổ sung khả năng xóa toàn bộ dữ liệu


Bước 1. Thêm phương thức vào lớp Repository
public void Clear()
{
_context.Books.Clear();
}

Bước 2. Thêm phương thức Clear vào ShellController


public void Clear(bool process = false)
{
if (!process)

490
{
Confirm("Do you really want to clear the shell? ", "do clear");
return;
}
Repository.Clear();
Inform("The shell has been cleared");
}

Bước 3. Bổ sung các route


r.Register(route: "clear", action: p => shell.Clear(), help: "[clear]rnUse
with care");
r.Register(route: "do clear", action: p => shell.Clear(true), help:
"[clear]rnUse with care");

Bước 4. Dịch và chạy thử chương trình với lệnh clear

Kết quả thực hiện lệnh clear

491
Lưu trữ dữ liệu (1): serialization, Binary, Xml, Json
Trong bài học này chúng ta sẽ xem xét vấn đề chuyển đổi dữ liệu
(serialization) về các dạng binary, xml và json. Chúng ta sẽ vận dụng các
kỹ thuật này để lưu trữ dữ liệu vào tập tin theo các định dạng tương ứng sử
dụng FileStream.
Để thực hiện bài thực hành này, bạn cần biết: Kiến trúc stream và cách sử dụng FileStream;
Khái niệm và cách thức thực hiện serialization.
Thực hành 1: Sử dụng binary serialization để lưu trữ dữ
liệu trong tập tin nhị phân
Bước 1. Xây dựng class BinaryDataAccess
Trong thư mục DataServices tạo tập tin mã nguồn BinaryDataAccess.cs cho
lớp BinaryDataAccess và viết code cho lớp BinaryDataAccess như sau:
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public class BinaryDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = "data.dat";
public void Load()
{
if (!File.Exists(_file))
{
SaveChanges();
return;
}
using (FileStream stream = File.OpenRead(_file))
{
BinaryFormatter formatter = new BinaryFormatter();
Books = formatter.Deserialize(stream) as List<Book>;
}
}
public void SaveChanges()
{
using (FileStream stream = File.OpenWrite(_file))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, Books);
}
}
}
}

492
Ở bước này chúng ta sử dụng BinaryFormatter để thực hiện serialization
thẳng vào một FileStream, cũng như deserialization từ FileStream, thay cho
biến đổi về mảng byte như trong ví dụ ở phần trước.
Bước 2. Điều chỉnh lớp Book
using System;
namespace BookMan.ConsoleApp.Models
{
/// <summary>
/// Lớp mô tả sách điện tử
/// </summary>
[Serializable]
public class Book
{
private int _id = 1;
/// <summary>
/// số định danh duy nhất cho mỗi object
/// </summary>
public int Id
{
get { return _id; }
set { if (value >= 1) _id = value; } // id chỉ nhận giá trị >= 1
}
//...
Ở bước này chúng ta thêm attribute [Serializable] cho lớp Book.
Attribute này là bắt buộc để BinaryFormatter có thể hoạt động. Attribute
này cho phép BinaryFormatter truy xuất các thành viên của lớp Book để sử
dụng trong quá trình serialization. Nếu thiếu attribute này, ở giai đoạn
runtime sẽ báo lỗi (nhưng ở giai đoạn compile time sẽ không báo lỗi gì).
Bước 3. Điều chỉnh lớp Repository
public class Repository
{
protected readonly BinaryDataAccess _context;
public Repository(BinaryDataAccess context)
{
_context = context;
_context.Load();
}
//...
Ở bước này chúng ta thay SimpleDataAccess bằng BinaryDataAccess vừa
tạo.
Bước 4. Điều chỉnh lớp BookController và ShellController
BookController.cs
ShellController.cs
internal class BookController : ControllerBase
{
protected Repository Repository;
public BookController(BinaryDataAccess context)
493
{
Repository = new Repository(context);
}
Ở bước này chúng ta cũng thay SimpleDataAccess bằng BinaryDataAccess.
Bổ sung phương thức sau vào ShellController
public void Save()
{
Repository.SaveChanges();
Success("Data save!");
}

Bước 5. Điều chỉnh phương thức ConfigRouter


private static void ConfigRouter()
{
BinaryDataAccess context = new BinaryDataAccess();
BookController controller = new BookController(context);
ShellController shell = new ShellController(context);
Router r = Router.Instance;
//...
Tương tự hai bước trên, ở đây chúng ta cũng thay SimpleDataAccess bằng
BinaryDataAccess.
Bổ sung route sau vào ConfigRouter
r.Register(route: "save shell", action: p => shell.Save(), help: "[save
shell]");

Bước 6. Dịch và chạy thử chương trình


Dịch và chạy thử với các lệnh: Add shell ? path = <value>, Save shell

Kết quả chạy lệnh add shell và save shell

Thực hành 2: sử dụng xml serialization để lưu trữ dữ liệu


trong tập tin xml
Trong thư mục DataServices tạo tập tin mã nguồn XmlDataAccess.cs cho
lớp XmlDataAccess. Code cho lớp XmlDataAccess như sau:
494
XmlDataAccess.cs
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public class XmlDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = "data.xml";
public void Load()
{
if (!File.Exists(_file))
{
SaveChanges();
return;
}
var serializer = new XmlSerializer(typeof(List<Book>));
using (var reader = XmlReader.Create(_file))
{
Books = (List<Book>)serializer.Deserialize(reader);
}
}
public void SaveChanges()
{
var serializer = new XmlSerializer(typeof(List<Book>));
using (var writer = XmlWriter.Create(_file))
{
serializer.Serialize(writer, Books);
}
}
}
}
Để chạy thử nghiệm có thể lặp lại bước 3, 4, 5 như trong phần thực hành 1
để thay BinaryDataAccess bằng XmlDataAccess.

Thực hành 3: sử dụng json serialization để lưu trữ dữ liệu


trong tập tin json
Trong thư mục DataServices tạo tập tin mã nguồn JsonDataAccess.cs cho
lớp JsonDataAccess.
Code cho lớp JsonDataAccess như sau:
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public class JsonDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = "data.json";

495
public void Load()
{
if (!File.Exists(_file))
{
SaveChanges();
return;
}
JsonSerializer serializer = new JsonSerializer();
using (StreamReader sReader = new StreamReader(_file))
using (JsonReader jReader = new JsonTextReader(sReader))
{
Books = serializer.Deserialize<List<Book>>(jReader);
}
//var jsonString = File.ReadAllText(_file);
//Books = JsonConvert.DeserializeObject<List<Book>>(jsonString);
}
public void SaveChanges()
{
JsonSerializer serializer = new JsonSerializer();
using (StreamWriter sWriter = new StreamWriter(_file))
using (JsonWriter jWriter = new JsonTextWriter(sWriter))
{
serializer.Serialize(jWriter, Books);
}
//var jsonString = JsonConvert.SerializeObject(Books);
//File.WriteAllText(_file, jsonString);
}
}
}
Để chạy thử nghiệm có thể lặp lại bước 3-4-5 như trong phần thực hành 1
để thay BinaryDataAccess bằng JsonDataAccess.

Kết luận
Trong bài này chúng ta tập trung chính vào phương pháp làm việc với tập
tin và cách thức biến đổi dữ liệu (serialization). Chúng ta thấy rằng cơ chế
làm việc với các nguồn dữ liệu (nói chung) và tập tin (nói riêng) trong .NET
Framework rất thống nhất.
.NET Framework cũng hỗ trợ việc chuyển đổi dữ liệu tự động với nhiều loại
định dạng khác nhau, như nhị phân, xml, json.
Thông qua phần thực hành chúng ta đã xây dựng 3 lớp khác nhau để hỗ trợ
lưu trữ dữ liệu vào tập tin nhị phân, tập tin xml và tập tin json.
Ở bài tiếp theo chúng ta sẽ xem xét một tính năng khác của C# và .NET
Framework để có thể dễ dàng chuyển đổi qua lại giữa các loại dữ liệu này.

496
Lưu trữ dữ liệu (2): interface, loosely coupling
Trong bài học này chúng ta sẽ xem xét sử dụng một công cụ đặc biệt hữu
ích trong phát triển ứng dụng: interface. Chúng ta sẽ vận dụng Interface để
giúp dễ dàng chuyển đổi giữa các cách thức lưu trữ dữ liệu đã biết (xml,
binary, json).
Để thực hiện bài thực hành này, bạn cần nắm bắt rõ ý nghĩa và cách sử dụng interface trong C#.
Thực hành: áp dụng interface cho các lớp data access
Bước 1. Xây dựng giao diện IDataAccess
Tạo tập tin mã nguồn IDataAccess.cs trong thư mục DataServices (Add =>
New Item, chọn kiểu là Interface). Viết code cho IDataAccess như sau:
IDataAccess.cs
using System.Collections.Generic;
namespace BookMan.ConsoleApp.DataServices
{
using Models;
public interface IDataAccess
{
List<Book> Books { get; set; }
void Load();
void SaveChanges();
}
}
Ở bước này chúng ta xây dựng một interface IDataAccess chứa định nghĩa
của một property (Books) và hai phương thức (Load, SaveChanges). Để ý
rằng hai phương thức này chỉ có phần mô tả (specification/declaration) mà
không có thân (implementation), gần giống như phương thức abstract.
Bước 2. Điều chỉnh lớp Repository
public class Repository
{
protected readonly IDataAccess _context;
public Repository(IDataAccess context)
{
_context = context;
_context.Load();
}
//...
Ở bước này chúng ta thay thế lớp data access cụ thể bằng interface
IDataAccess. Ở bài trước chúng ta đã xây dựng 3 lớp data access để sử
dụng trong Repository. Ở đây thay vì sử dụng một trong ba lớp đó, chúng
ta sử dụng kiểu IDataAccess mới xây dựng.

497
Bước 3. Điều chỉnh lớp BookController và ShellController
BookController
ShellController
internal class BookController : ControllerBase
{
protected Repository Repository;
public BookController(IDataAccess context)
{
Repository = new Repository(context);
}
//...
Ở bước này chúng ta điều chỉnh phương thức khởi tạo của BookController
và ShellController để chúng nhận tham số đầu vào là một biến thuộc kiểu
IDataAccess.
Bước điều chỉnh này giúp các controller không còn phụ thuộc trực tiếp vào
một lớp data access cụ thể nào mà chuyển sang phụ thuộc vào interface.
Để ý rằng với bước này, chúng ta không còn cần phải xây dựng controller
sau data access nữa (vì chúng không còn phụ thuộc nhau), dẫn đến các lớp
data access và controller giờ có thể được phát triển độc lập.
Bước 4. Điều chỉnh các lớp BinaryDataAccess, XmlDataAccess,
JsonDataAccess
BinaryDataAccess
XmlDataAccess
JsonDataAccess
SimpleDataAccess
using Models;
public class BinaryDataAccess : IDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = "data.dat";
//...
Ở bước này chúng ta điều chỉnh để các lớp data access sẵn có “kế thừa” từ
IDataAccess.
Cấu trúc cú pháp này nhìn tương tự như kế thừa giữa các class nhưng nó
phản ánh một quan hệ khác giữa class và interface: quan hệ thực
thi (implementation).
Một class thực thi một giao diện sẽ phải xây dựng (implement) tất cả các
thành viên của interface. Hay nói cách khác, class này sẽ phải xây dựng tất
cả các phương thức và thuộc tính theo quy định của interface.
Ở đây, interface IDataAccess quy định rằng các class thực thi nó sẽ phải
xây dựng hai phương thức Load, SaveChanges(), và phải chứa thuộc tính
498
Books. Các thành viên này chúng ta đều đã xây dựng từ trước ở các lớp
data access nên không cần thiết phải viết thêm nữa.
Bước 5. Điều chỉnh phương thức ConfigRouter
private static void ConfigRouter()
{
IDataAccess context = new BinaryDataAccess();
BookController controller = new BookController(context);
ShellController shell = new ShellController(context);
//...
Ở bước này chúng ta khai báo một biến thuộc kiểu IDataAccess và gán nó
cho một object của BinaryDataAccess.
Giống như trong quan hệ kế thừa, object của lớp con có thể gán cho biến
thuộc kiểu cha, một biến thuộc kiểu interface cũng có thể nhận một object
của bất kỳ class nào thực thi giao diện này. Chúng ta cũng thấy, một biến
thuộc kiểu interface được sử dụng như biến của bất kỳ kiểu dữ liệu nào khác
của .NET.
Đến đây chúng ta thấy rằng, việc chuyển đổi sang một lớp data access khác
giờ rất dễ dàng: chỉ cần thay đổi ở một chỗ duy nhất trong phương
thức ConfigRouter.
Qua bước này, tất cả controller không còn phụ thuộc vào data access nữa.
Sự phụ thuộc này đẩy sang một class khác, Program. Khi controller và data
access không còn phụ thuộc nhau, chúng ta có thể phát triển và test chúng
một cách độc lập.
Cách giải quyết sự phụ thuộc giữa các class như vậy có tên gọi chung
là Inversion of Control (nguyên lý IoC). Việc khai báo và sử dụng biến thuộc
kiểu interface này còn đơn giản hơn rất nhiều và hoàn toàn tự động nếu
chúng ta sử dụng một Dependency Injection container nào đó (như Unity,
Ninject).

Vận dụng interface trong project


Ở bài trước chúng ta đã xây dựng 3 class để hỗ trợ lưu trữ dữ liệu ở các định
dạng khác nhau. Có một điều rất dễ nhận thấy là khi muốn chuyển từ định
dạng lưu trữ này sang dạng khác chúng ta phải thay đổi code ở hàng loạt
class: Repository, BookController, ShellController, Program (phương
thức ConfigRouter).
Lý do phải thay đổi code ở nhiều chỗ như vậy là vì Repository,
BookController và ShellController đều phụ thuộc vào lớp data access.
Sự phụ thuộc này thể hiện ở chỗ constructor của các lớp controller đòi hỏi
object của một lớp data access cụ thể nào đó (BinaryDataAccess,
XmlDataAccess,…).
499
Loại quan hệ này giữa các class được gọi là quan hệ chặt (tight-coupling)
giữa các class, trong đó các lớp controller phụ thuộc vào các lớp data access.
Quan hệ phụ thuộc chặt này đơn giản khi sử dụng nhưng có thể gây ra rắc
rối, giống như tình huống của các lớp controller và các lớp data access, khi
cần thay thế class bị phụ thuộc (các lớp data access). Quan hệ phụ thuộc
chặt yêu cầu các lớp phụ thuộc phải xây dựng sau, dẫn tới không thể phát
triển song song các class. Quan hệ chặt cũng có thể gây khó khăn cho việc
test các class độc lập (vì chúng phụ thuộc vào nhau).
Để có thể phát triển song song hoặc dễ dàng thay thế class này bằng class
khác, người ta cần làm giảm sự phụ thuộc giữa các class, thay phụ thuộc
chặt bằng phụ thuộc lỏng (loosely-coupling). Interface là công cụ thường
được sử dụng để làm giảm sự phụ thuộc này.
Để thực hiện ý tưởng này, trong phần thực hành trên chúng ta xây dựng
interface IDataAccess. Interface này đóng vai trò một bản hợp đồng với 3
“điều khoản”: property Books, phương thức Load và SaveChanges.
Các lớp controller và lớp Repository bây giờ hoàn toàn chỉ sử
dụng IDataAccess làm kiểu dữ liệu cho biến context mà không cần biết đến
class cụ thể nào. Bất kỳ lớp data access nào muốn làm việc với controller
bây giờ phải tuân thủ theo bản hợp đồng IDataAccess trên, nghĩa là phải
xây dựng đủ 3 thành phần Books, Load, SaveChanges theo các điều khoản
của hợp đồng.
Nếu một class nào thỏa mãn các điều kiện trên thì object của nó có thể
truyền cho Repository và controller ở giai đoạn khởi tạo. Việc này giúp
controller và Repository không phụ thuộc vào bất kỳ data access cụ thể
nào và chúng ta có thể phát triển controller và Repository hoàn toàn độc
lập với data access.
Như trong phần thực hành trên, object của một lớp data access cụ thể (ví
dụ, XmlDataAccess) được khởi tạo ở lớp Program cùng với object của các
controller. Đây là chỗ duy nhất thực hiện ghép nối data access với controller.
Việc tách rời khởi tạo object của controller và data access sang một bên thứ
ba như vậy được gọi là Inversion of Control (IoC). Việc khởi tạo object của
controller sử dụng object của một lớp data access cụ thể như vậy được gọi
là Dependency Injection (DI). DI có thể được thực hiện một cách tự động
nhờ sử dụng một DI container.

Kết luận
Trong bài này chúng ta đã xem xét một kỹ thuật đặc biệt quan trọng trong
phát triển ứng dụng: sử dụng interface. Chúng ta cũng thấy, interface cho

500
phép giảm bớt sự phụ thuộc giữa các class và tạo ra quan hệ lỏng giữa
chúng. Quan hệ này cho phép các class có thể được xây dựng và test độc
lập.
Chúng ta cũng đã vận dụng interface cho các lớp truy xuất dữ liệu (data
access) để giúp giảm bớt sự phụ thuộc của Repository và các lớp controller
và một lớp data access cụ thể. Từ đây chúng ta có thể dễ dàng thay đổi
cách thức lưu trữ dữ liệu.
Trong bài tiếp theo chúng ta sẽ xem xét sự thay đổi cuối cùng về các thức
truy xuất dữ liệu với thư viện LINQ.

501
Cải tiến repository: LINQ (Language Integrated
Query)
Trong bài học này chúng ta sẽ xem xét cách sử dụng một công cụ khác của
.NET Framework để xử lý dữ liệu: LINQ (Language Integrated Query).
Chúng ta sẽ vận dụng LINQ để cải tiến các phương thức truy xuất dữ liệu
của lớp Repository giúp đơn giản hóa code và tăng hiệu suất của ứng dụng.

Đặt vấn đề
Ở các bài đầu khi xây dựng lớp Repository để xử lý dữ liệu chúng ta đã xây
dựng các phương thức sau:
public Book Select(int id)
{
foreach (var b in _context.Books)
{
if (b.Id == id) return b;
}
return null;
}
public Book[] Select(string key)
{
var temp = new List<Book>();
var k = key.ToLower();
foreach (var b in _context.Books)
{
var logic =
b.Title.ToLower().Contains(k) ||
b.Authors.ToLower().Contains(k) ||
b.Publisher.ToLower().Contains(k) ||
b.Tags.ToLower().Contains(k) ||
b.Description.ToLower().Contains(k)
;
if (logic) temp.Add(b);
}
return temp.ToArray();
}
public Book[] SelectMarked()
{
var temp = new List<Book>();
foreach (var b in _context.Books)
{
if (b.Reading) temp.Add(b);
}
return temp.ToArray();
}
public void Insert(Book book)
{
var lastIndex = _context.Books.Count - 1;
var id = lastIndex < 0 ? 1 : _context.Books[lastIndex].Id + 1;
book.Id = id;
_context.Books.Add(book);
}

502
Trong đó phương thức Select lựa chọn một cuốn sách từ kho theo giá trị
Id. Một overload khác của Select lựa chọn tất cả những cuốn sách mà tiêu
đề, tác giả, nhà xuất bản, tags và mô tả chứa một từ khóa nhất định.
Phương thức SelecteMarked lựa chọn những cuốn sách mà trường Reading
có giá trị true.
Cả 3 phương thức này đều có đặc điểm chung là phải dò tìm trong danh
sách dữ liệu và trích ra những object đáp ứng yêu cầu. Chúng ta cũng có
thể thấy là logic của các phương thức này rất giống nhau: (1) duyệt danh
sách dữ liệu => (2) ứng với mỗi object check xem có thỏa mãn yêu cầu hay
không => (3) nếu thỏa mãn, trả lại object này.
Nhu cầu viết những phương thức như vậy rất phổ biến khi xây dựng các ứng
dụng quản lý. Để hỗ trợ người lập trình giải quyết những bài toán tương tự,
.NET Framework 3.5 (C# 3) đưa vào một bộ thư viện mở rộng có tên
gọi LINQ (Language Integrated Query).
LINQ là bộ thư viện của các phương thức mở rộng cung cấp thêm khả năng
xử lý dữ liệu dạng tập hợp. LINQ cung cấp các phương thức mở rộng
(extension method) cho tất cả các kiểu dữ liệu tập hợp mà ta đã biết. Các
phương thức của LINQ tập trung chủ yếu vào việc truy xuất dữ liệu từ các
object của dữ liệu tập hợp (như mảng, List, Dictionary,...). Vì lý do này, các
phương thức của LINQ cũng thường được gọi là truy vấn LINQ (LINQ query).
Như trong trường hợp 3 phương thức của lớp Repository, chúng ta chỉ cần
sử dụng một truy vấn LINQ là có thể chọn ra 1 object hoặc một mảng object
đáp ứng các yêu cầu, thay vì phải duyệt danh sách và tự kiểm tra từng
object như trên.

Thực hành 1: Cải tiến lớp Repository sử dụng LINQ


Bước 1. Bổ sung không gian tên System.Linq trong lớp Repository
using System.Linq;

Trên thực tế, khi xây dựng một class mới, không gian tên System.Linq luôn
được Visual Studio thêm vào một cách tự động. Nếu chưa thấy không gian
tên này trong khối using thì bổ sung vào.
Bước 2. Điều chỉnh các phương thức như sau của lớp Repository:
public Book[] SelectMarked()
{
return _context.Books.Where(b => b.Reading == true).ToArray();
}
public void Insert(Book book)
{
var id = _context.Books.Count == 0 ? 1 : _context.Books.Max(b => b.Id) + 1;
book.Id = id;

503
_context.Books.Add(book);
}
Ở phần thực hành này chúng ta đã thay thế các lệnh thông thường bằng
truy vấn LINQ với hiệu quả tương tự. Khối lượng code đã được giảm đi đáng
kể.

Cách áp dụng LINQ trong project


Như bạn đã biết, để sử dụng LINQ cần có ba thành phần: nguồn dữ liệu
(data source), truy vấn (query), lời gọi thực hiện truy vấn (query
execution). Hình minh họa dưới đây mô tả vai trò của LINQ trong quan hệ
với ngôn ngữ lập trình và các nguồn dữ liệu.

Kiến trúc và vị trí của LINQ

504
Trong phần thực hành trên, nguồn dữ liệu là _context.Books mà chúng
ta đã có sẵn. Đây là một tập hợp các object trong bộ nhớ thuộc
kiểu List<Book> (thực thi giao diện IEnumerable<Book>). Provider cho
nó là LINQ to Objects.
Trong lớp Repository ở trên chúng ta chỉ sử dụng LINQ to Objects. LINQ
to Objects cho phép sử dụng LINQ với các loại dữ liệu tập hợp quen thuộc
mà chúng ta đã sử dụng trong dự án như List<T>, Dictionary<TKey,
TValue>, mảng.
Các lệnh đã sử dụng ở phần thực hành
_context.Books.Where(b =>
b.Title.ToLower().Contains(k) ||
b.Authors.ToLower().Contains(k) ||
b.Publisher.ToLower().Contains(k) ||
b.Tags.ToLower().Contains(k) ||
b.Description.ToLower().Contains(k))
_context.Books.Where(b => b.Reading == true)

là truy vấn được viết theo cú pháp phương thức. Lối viết này luôn yêu cầu
cung cấp một hàm lambda để gọi đối với mỗi phần tử của chuỗi dữ liệu. Khi
quen thuộc với việc sử dụng hàm lambda, lối viết này hoàn toàn giống như
gọi hàm thông thường và không yêu cầu phải học thêm gì cả.
Các truy vấn trên nếu viết theo cú pháp truy vấn sẽ có dạng như sau:
from b in _context.Books
where b.Title.ToLower().Contains(k) ||
b.Authors.ToLower().Contains(k) ||
b.Publisher.ToLower().Contains(k) ||
b.Tags.ToLower().Contains(k) ||
b.Description.ToLower().Contains(k)
select b
from b in _context.Books
where b.Reading == true
select b

Lưu ý rằng, một truy vấn LINQ không được thực thi ngay khi chương trình
thực hiện đến lệnh đó. Chỉ khi nào có những hoạt động thực sự cần đến dữ
liệu từ truy vấn đó (ví dụ như duyệt danh sách dữ liệu), truy vấn mới được
thực hiện. Việc trì hoãn thực hiện truy vấn như vậy có tên gọi là thực thi
trễ (deferred execution).

505
Việc thực thi trễ có tác dụng lớn đến việc tăng hiệu suất xử lý vì nó hạn chế
thực thi những lệnh chưa cần thiết. Cơ chế thực thi trễ áp dụng cho tất cả
các nguồn dữ liệu hỗ trợ LINQ hiện tại (LINQ to Objects, LINQ to SQL, LINQ
to Entities, LINQ to XML).
Trong một số trường hợp chúng ta cần thực thi truy vấn ngay khi chương
trình chạy đến vị trí lệnh đó. Để thực hiện cơ chế này, chúng ta gọi tới một
trong số các phương thức biến đổi dữ liệu bắt đầu bằng “To”: ToArray,
ToList.
Trong phần thực hành bên trên chúng ta đã sử dụng cơ chế thực thi này:
public Book[] Select(string key)
{
var k = key.ToLower();
return _context.Books.Where(b =>
b.Title.ToLower().Contains(k) ||
b.Authors.ToLower().Contains(k) ||
b.Publisher.ToLower().Contains(k) ||
b.Tags.ToLower().Contains(k) ||
b.Description.ToLower().Contains(k)).ToArray();
}
public Book[] SelectMarked()
{
return _context.Books.Where(b => b.Reading == true).ToArray();
}

Thực hành 2: Bổ sung chức năng thống kê


Trong phần thực hành này chúng ta sẽ vận dụng LINQ để xây dựng một
chức năng thống kê đơn giản: in danh sách theo nhóm.
Bước 1. Bổ sung phương thức Stats vào lớp Repository
public IEnumerable<IGrouping<string, Book>> Stats(string key = "folder")
{
return _context.Books.GroupBy(b => System.IO.Path.GetDirectoryName(b.File));
}
Ở bước này chúng ta vận dụng phương thức GroupBy của LINQ để nhóm dữ
liệu sách theo tên thư mục chứa tập tin. Cấu trúc làm nhiệm vụ trích phần
thông tin về thư mục của tập tin sách (phương thức GetDirectoryName),
sau đó gom tất cả dữ liệu sách mà tập tin của nó nằm trong cùng một thư
mục vào một nhóm. Kết quả thực hiện của truy vấn này là một danh sách
nhóm, trong đó tên của mỗi nhóm là tên một thư mục, phần tử của mỗi
nhóm là tất cả các cuốn sách nằm trong thư mục đó.
Bước 2. Xây dựng lớp view mới BookStatsView trong thư mục Views
using BookMan.ConsoleApp.Models;
using Framework;
using System;
using System.Collections.Generic;
using System.Linq;
506
namespace BookMan.ConsoleApp.Views
{
internal class BookStatsView : ViewBase<IEnumerable<IGrouping<string, Book>>>
{
public BookStatsView(IEnumerable<IGrouping<string, Book>> model) : base(model)
{
}
public override void Render()
{
foreach (var g in Model)
{
ViewHelp.WriteLine($"# {g.Key}", ConsoleColor.Magenta);
foreach (var b in g)
{
ViewHelp.Write($"[{b.Id}] ", ConsoleColor.Yellow);
ViewHelp.WriteLine(b.Title, b.Reading ? ConsoleColor.Cyan :
ConsoleColor.White);
}
}
}
}
}
Ở đây để ý rằng, dữ liệu controller cung cấp cho view có kiểu
IEnumerable<IGrouping<string, Book>>.
Để duyệt kiểu dữ liệu này chúng ta phải sử dụng hai vòng lặp: vòng lặp thứ
nhất để duyệt danh sách nhóm, vòng lặp thứ hai để duyệt danh sách phần
tử trong mỗi nhóm.
Thông tin nhóm được truy xuất qua thuộc tính Key, trong trường hợp này
thuộc kiểu string. Tự bản thân mỗi nhóm là một danh sách các phần tử kiểu
Book, do đó, có thể truy xuất như một danh sách bình thường.
Bước 3. Bổ sung phương thức vào lớp BookController
public void Stats()
{
var model = Repository.Stats();
var view = new BookStatsView(model);
Render(view);
}

Bước 4. Bổ sung route sau vào ConfigRouter


r.Register(route: "show stats", action: p => controller.Stats(), help:
"[show stats]");

507
Bước 5. Dịch và chạy thử chương trình với lệnh “show stats”

Kết quả thực hiện lệnh show stats


Chúng ta vừa bổ sung chức năng thống kê sách theo thư mục. Lệnh “show
stats” sẽ hiển thị các cuốn sách theo từng thư mục. Theo logic này chúng
ta hoàn toàn có thể xây dựng các tính năng thống kê theo các tiêu chí khác
như tác giả, nhà xuất bản, năm xuất bản.

508
Hoàn thiện dự án: exception, try-catch, Settings
Trong loạt bài từ đầu đến giờ, chúng ta đã lần lượt hoàn thiện tất cả chức
năng của ứng dụng theo phân tích. Tuy nhiên, trước khi đưa ứng dụng đến
được người dùng cuối, chúng ta cần bổ sung thêm một số tính năng, vốn
không liên quan trực tiếp đến việc phân tích nghiệp vụ.
Các chức năng mới này mang tính kỹ thuật hơn là nghiệp vụ, bao gồm bắt
và xử lý ngoại lệ (Exception, giúp ứng dụng ổn định hơn), sử dụng tập tin
cấu hình (Settings, giúp người dùng thay đổi cấu hình của ứng dụng).

Xử lý ngoại lệ (Exception Handling)


Chúng ta đã nhắc đến khái niệm ngoại lệ (exception) và xem xét cách thức
đơn giản nhất để phát thông báo ngoại lệ bằng lệnh throw và
lớp Exception. Exception là một cơ chế rất mạnh trong .NET giúp phát hiện
lỗi logic trong chương trình ở giai đoạn Runtime.
Ngoại lệ và chế độ Debug
Khi chạy chương trình ở chế độ debug, nếu phát sinh ngoại lệ, Visual Studio
sẽ mở tập tin mã nguồn ở đúng vị trí lỗi cùng với thông báo cụ thể. Qua đó,
chúng ta có thể xác định nguồn gốc của lỗi và đưa ra cách giải quyết.
Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh
chưa tồn tại.

Ngoại lệ ở chế độ chạy debug


Đây là cơ chế bắt và xử lý lỗi ở chế độ Debug. Chương trình chúng ta viết
từ đầu dự án đến giờ đều dịch và chạy ở chế độ Debug.
Xử lý ngoại lệ ở chế độ Release
Một chương trình trước khi đem triển khai cho người dùng cuối phải được
dịch ở chế độ Release. Chương trình được dịch ở chế độ này sẽ không chạy
được ở chế độ Debug nữa.
Nếu chương trình chạy ở chế độ Release mà gặp lỗi, thông báo lỗi sẽ được
hiển thị như dưới đây.

509
Đây là cơ chế bắt và xử lý lỗi mặc định của .NET Framework đối với ứng dụng console.
Như chúng ta thấy, cơ chế bắt và xử lý lỗi mặc định của .NET Framework
tương đối không thân thiện với người dùng.
.NET cũng cung cấp cho các chương trình tính năng bắt và xử lý ngoại
lệ (Exception Handling) để tự mình xác định hoạt động của chương trình khi
xảy ra lỗi (ngoại lệ), tránh phải sử dụng cơ chế bắt và xử lý lỗi mặc định.

Thực hành 1: bổ sung chức năng bắt và xử lý lỗi


Bước 1. Điều chỉnh phương thức Main
private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
ConfigRouter();
while (true)
{
ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
string request = Console.ReadLine();
try
{
Router.Instance.Forward(request);
}
catch (Exception e)
{
ViewHelp.WriteLine(e.Message, ConsoleColor.Red);
}
finally
{
Console.WriteLine();
}
}
}
Khối try được đặt để kiểm soát “cửa ngõ” của chương trình: lời gọi phương
thức Forward. Lời gọi phương thức này là khởi đầu của bất kỳ hoạt động
nào của ứng dụng khi người dùng nhập truy vấn.
Khối catch được viết để bắt tất cả các loại lỗi (vì bắt lỗi thuộc loại Exception)
và đưa vào biến e. Thông báo lỗi cụ thể được truy xuất qua thuộc tính
Message của object Exception và viết ra mới màu đỏ.

510
Trong bất kỳ tình huống nào (dù thực hiện lệnh Forward có lỗi hay không)
sẽ luôn in thêm một dòng trống sau khi thực hiện (khối finally).
Bước 2. Dịch và chạy chương trình với các lệnh lỗi

Chạy chương trình với các lệnh lỗi


Chúng ta có thể thấy cơ chế bắt lỗi mặc định của .NET đã không còn kích
hoạt nữa. Thay vào đó, cơ chế bắt lỗi riêng của chương trình đã hoạt động.
Các thông báo cũng đơn giản nhẹ nhàng hơn. Có những thông báo xuất
phát từ lỗi do chúng ta tự phát ra, cũng có một số lỗi được phát ra từ các
phương thức chuẩn của .NET.
Chúng ta cũng để ý rằng, dù chạy ở chế độ nào (Debug hay Release), cơ
chế bắt lỗi riêng cũng đều hoạt động. Điều này cũng có nghĩa là chúng ta
đã vứt bỏ ưu thế của chế độ Debug: không xác định được vị trí gây lỗi, cũng
không theo dõi được stack khi bị lỗi. Việc bắt lỗi như vậy không thích hợp ở
giai đoạn phát triển ứng dụng. Cũng vì lý do này mà nội dung bắt và xử lý
lỗi của chúng ta xem xét ở bài cuối cùng của dự án.
Vấn đề cấu hình của ứng dụng
Khi một chương trình đã được dịch, đóng gói và triển khai cho người dùng
cuối, chúng ta không thể dễ dàng thay đổi nó được nữa vì liên quan đến
nhiều khâu. Ví dụ, nếu chúng ta quyết định sử dụng tập tin nhị phân để lưu
trữ dữ liệu của chương trình thì sau khi triển khai, nếu muốn chuyển sang
dùng tập tin json, chúng ta lại phải thực hiện trọn vẹn quy trình sửa mã
nguồn => dịch => đóng gói => triển khai phiên bản mới.
Trong nhiều tình huống chúng ta đã dự trù sẵn những thay đổi cho chương
trình mà người dùng đầu cuối có thể thực hiện. Ví dụ, chúng ta muốn cho
phép người dùng cuối thay đổi màu sắc và văn bản của con trỏ nhắc lệnh.

511
Chúng ta cũng muốn cho người dùng cuối lựa chọn loại tập tin dữ liệu để
lưu trữ (tập tin nhị phân, xml hay json).
Một tình huống khác đó là thiết lập của ứng dụng ở giai đoạn phát triển
không thể sử dụng ở giai đoạn triển khai. Ví dụ kết nối cơ sở dữ liệu ở giai
đoạn phát triển ứng dụng hoàn toàn khác với kết nối ở giai đoạn triển khai.
Khi đó, thông tin về chuỗi kết nối không thể code thẳng trong ứng dụng mà
phải đặt ở tập tin cấu hình để khi cài đặt chương trình người dùng sẽ trực
tiếp thay đổi.
Rất nhiều tình huống tương tự dẫn tới nhu cầu lưu trữ và truy xuất thông
tin cấu hình cho chương trình.
Trong phần thực hành này chúng ta sẽ vận dụng cơ chế lưu trữ và truy xuất
thông tin cấu hình của .NET để xây dựng tính năng cấu hình. Tính năng này
cho phép người dùng cuối thực hiện các thao tác sau:
 Thay đổi màu sắc và văn bản của dấu nhắc lệnh: Hiện nay chúng ta
đang thiết lập cứng văn bản của dấu nhắc lệnh là “# Request >>>”
với màu Green. Chúng ta muốn người dùng có thể tùy ý chọn văn bản
và màu sắc của dấu nhắc lệnh.
 Thay đổi cơ chế và nơi lưu trữ dữ liệu: Hiện nay chúng ta đã xây dựng
ba cơ chế lưu trữ dữ liệu khác nhau: binary, json, xml. Các cơ chế này
lưu dữ liệu vào vào các tập tin tương ứng là data.bin, data.json,
data.xml. Chúng ta muốn người dùng có thể tùy chọn cơ chế lưu trữ
và tập tin dữ liệu.

Thực hành 2: bổ sung chức năng thiết lập cấu hình


Bước 1. Tạo tập tin settings
Click đúp vào nút Properties của BookMan.ConsoleApp. Trong cửa sổ chọn
mục Settings.

512
Vì project này chưa có tập tin settings nào, phần nội dung bên tay phải đang
trống. Click vào đường link sẽ tạo ra tập tin settings đầu tiên của project.
Mặc định tập tin này có tên gọi Setttings.settings và nằm trong nút
Properties.

Visual Studio tạo ra một giao diện đồ họa để nhập các thiết lập (setting).
Cũng có thể mở giao diện này bằng cách click đúp vào nút Settings.settings.

513
Bước 2. Nhập các giá trị vào bảng thông tin cấu hình

Nhập giá trị cho bảng settings


Đây là bảng thông tin đặc biệt, trong đó dữ liệu từ bảng sẽ được Visual
Studio sử dụng để tự động sinh ra một class hỗ trợ truy xuất thông tin cầu
hình. Class này có tên là Settings nằm trong không gian tên con Properties.
Như trong project này, tên đầy đủ của lớp Settings là
BookMan.ConsoleApp.Properties.Settings. Ở tất cả các tập tin mã nguồn
của project chúng ta đều có thể sử dụng lớp Settings này.
Mỗi setting chứa 4 thông tin:
 Name: tên của setting. Thông tin Name sẽ được sử dụng để tạo ra
một property tương ứng của class Settings. Settings là một class được
Visual Studio sinh ra tự động dựa trên dữ liệu của bảng này, trong đó
mỗi setting sẽ là một property của class. Vì vậy, Name phải tuân thủ
theo quy tắc đặt tên biến và quy ước đặt tên property mà chúng ta
đã học.
 Type: kiểu dữ liệu của setting. Như trên đã nói, mỗi setting sẽ trở
thành một property của class Settings, giá trị của Type sẽ là kiểu dữ
liệu của property.
Lưu ý với setting PromptColor, kiểu dữ liệu System.ConsoleColor bình
thường sẽ không xuất hiện trong danh sách lựa chọn. Chúng ta cần tự mình
chỉ định vị trí chứa kiểu này: Trong combo box của cột Type chọn Browse;
trong hộp thoại Select a Type mở nhánh mscorlib => System =>

514
ConsoleColor. Khi đó, trong ô Value sẽ có thể mở combobox để lựa chọn
một trong 16 màu của ConsoleColor.

 Value: giá trị mặc định của setting. Giá trị này cũng tương đương với
giá trị gán ban đầu cho mỗi property. Mặc dù trong ô nhập dữ liệu
chúng ta chỉ nhập được chuỗi ký tự, giá trị này thực sự được chuyển
đổi về kiểu tương ứng của thuộc tính. Vì lý do này, chỉ những kiểu có
khả năng serialize (tuần tự hóa) về xml mới có thể được sử dụng.
 Scope: quyết định phạm vi sử dụng của setting. Scope có thể nhận
một trong hai giá trị: User hoặc Application.
Application scope quyết định rằng đây là một thuộc tính chỉ đọc (read-only).
Chúng ta không thể thay đổi giá trị của setting trong khi chương trình hoạt
động. Scope này sử dụng đối với các setting cấu hình một lần khi triển khai
hệ thống và sau đó không (hoặc hiếm khi) thay đổi nữa.
515
User cope sử dụng cho những setting cần thay đổi, ngay cả khi chương trình
hoạt động (và có hiệu lực ngay lập tức). Trong project này chúng ta đặt cả
4 setting trong user scope.
Khi lưu bảng cấu hình lại, Visual Studio sẽ tự động lưu thông tin vào tập tin
App.config như sau:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="BookMan.ConsoleApp.Properties.Settings" type="System.Configuration.ClientSettingsSection, System,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser"
requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<userSettings>
<BookMan.ConsoleApp.Properties.Settings>
<setting name="DataAccess" serializeAs="String">
<value>binary</value>
</setting>
<setting name="PromptText" serializeAs="String">
<value># Command >>></value>
</setting>
<setting name="PromptColor" serializeAs="String">
<value>Green</value>
</setting>
<setting name="DataFile" serializeAs="String">
<value>data.bin</value>
</setting>
</BookMan.ConsoleApp.Properties.Settings>
</userSettings>
</configuration>

Khi dịch ra ứng dụng, nội dung của tập tin App.config sẽ được copy vào một
tập tin được đặt tên theo tên ứng dụng với phần mở rộng .config. Đối với
ứng dụng này, tập tin cấu hình khi triển khai là
BookMan.ConsoleApp.exe.config.

516
Song song với lưu dữ liệu vào tập tin App.config, Visual Studio cũng tự sinh
ra code cho class Settings:

Không nên thay đổi nội dung của tập tin này vì nếu như dữ liệu thay đổi, Visual Studio sẽ tự
động sinh lại code cho class này. Khi đó những thay đổi của người dùng sẽ mất. Vì đây là một
partial class, nếu thực sự muốn bổ sung code có thể tạo thêm tập tin mã nguồn nữa ghép nối
với class này.
Bước 3. Tạo class Config trong tập tin mã nguồn Config.cs trực thuộc
project
using System;
namespace BookMan.ConsoleApp
{
using DataServices;
internal class Config
{
private static Config _instance;
public static Config Instance = _instance ?? (_instance = new Config());
private Config() { }
private Properties.Settings _s = Properties.Settings.Default;
public void Reload() => _s.Reload();
public IDataAccess IDataAccess
{
get
{
var da = _s.DataAccess;
switch (da.ToLower())
{
case "binary": return new BinaryDataAccess();
case "json": return new JsonDataAccess();
case "xml": return new XmlDataAccess();
default: return new BinaryDataAccess();
}
}
}
public string DataAccess
{
get => _s.DataAccess;
set
{
_s.DataAccess = value;
_s.Save();
}
}
public string PromptText

517
{
get => _s.PromptText;
set
{
_s.PromptText = value;
_s.Save();
}
}
public ConsoleColor PromptColor
{
get => _s.PromptColor;
set
{
_s.PromptColor = value;
_s.Save();
}
}
public string DataFile
{
get => _s.DataFile;
set
{
_s.DataFile = value;
_s.Save();
}
}
}
}

Bước 4. Thay đổi code của các class data access


// lớp BinaryDataAccess
public class BinaryDataAccess : IDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = Config.Instance.DataFile; // "data.dat";
//...
// lớp JsonDataAccess
public class JsonDataAccess : IDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = Config.Instance.DataFile; // "data.json";
//...
// lớp XmlDataAccess
public class XmlDataAccess : IDataAccess
{
public List<Book> Books { get; set; } = new List<Book>();
private readonly string _file = Config.Instance.DataFile; // "data.xml";
//...

Bước 5. Thay đổi code của phương thức Main


private static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var text = Config.Instance.PromptText;
var color = Config.Instance.PromptColor;
ConfigRouter();
while (true)

518
{
ViewHelp.Write(text, color);
string request = Console.ReadLine();
try
{
Router.Instance.Forward(request);
}
catch (Exception e)
{
ViewHelp.WriteLine(e.Message, ConsoleColor.Red);
}
finally
{
Console.WriteLine();
}
}
}

Bước 6. Thay đổi code của phương thức ConfigRouter


private static void ConfigRouter()
{
IDataAccess context = Config.Instance.IDataAccess; //new BinaryDataAccess();
BookController controller = new BookController(context);
ShellController shell = new ShellController(context);
//...

Bước 7. Xây dựng thêm lớp ConfigController trong tập tin


ConfigController.cs trong thư mục Controllers
using System;
namespace BookMan.ConsoleApp.Controllers
{
using Framework;
internal class ConfigController : ControllerBase
{
private Config _c = Config.Instance;
public void ConfigPromptText(string text)
{
_c.PromptText = text;
Success("The command prompt will change next time");
}
public void ConfigPromptColor(string text)
{
if (Enum.TryParse(text, true, out ConsoleColor color))
{
_c.PromptColor = color;
Success("The command prompt color will change nex time");
}
}
public void CurrentDataAccess()
{
var da = _c.DataAccess;
var tập tin = _c.DataFile;
Inform($"Current data access engine: {da}rnCurrent data tập tin: {tập
tin}");
}
public void ConfigDataAccess(string da, string tập tin)

519
{
_c.DataAccess = da;
_c.DataFile = tập tin;
Success("The changes will be available next time");
}
}
}

Bước 8. Thay đổi phương thức ConfigRouter


Bổ sung khai báo object của kiểu ConfigController:
private static void ConfigRouter()
{
IDataAccess context = Config.Instance.IDataAccess;
BookController controller = new BookController(context);
ShellController shell = new ShellController(context);
ConfigController config = new ConfigController();
Router r = Router.Instance;
//...

Bổ sung thêm các route sau:


r.Register(route: "config prompt text", action: p =>
config.ConfigPromptText(p["text"]), help: "[config prompt text ? text =
<value>]");
r.Register(route: "config prompt color", action: p =>
config.ConfigPromptColor(p["color"]), help: "[config prompt color ? color =
<value>]");
r.Register(route: "current data access", action: p => config.CurrentDataAccess(),
help: "[current data access]");
r.Register(route: "config data access", action: p => config.ConfigDataAccess(p["da"],
p["tập tin"]), help: "[config data access ? da = <value:json, binary, xml>
& tập tin = <value>]");

520
Bước 9. Dịch và chạy thử chương trình với các chức năng mới

Chạy chức năng cấu hình


Sau khi thực hiện các lệnh trên, đóng và chạy lại chương trình để thấy các
thiết lập mới đã có hiệu lực.
Như vậy, chúng ta đã cung cấp cho người dùng cuối khả năng thay đổi các
cấu hình cơ bản: màu sắc và văn bản của dấu nhắc lệnh; cơ chế lưu trữ dữ
liệu và tập tin dữ liệu.

521
522
Xuất bản và triển khai ứng dụng – Kết
Đến đây, phiên bản 1.0 của phần mềm của chúng ta đã hoàn thành những
chức năng chính theo phân tích và đã sẵn sàng để cung cấp cho người dùng
cuối. Để cài đặt phần mềm trên máy của người dùng cuối, chúng ta cần
đóng gói và tạo bộ cài đặt. Trong bài học này chúng ta sẽ xem xét cách
xuất bản, tạo bộ cài và triển khai ứng dụng trên máy khách hàng.

Thực hành: Tạo bộ cài và xuất bản ứng dụng


Visual Studio cung cấp công cụ đơn giản để tạo bộ cài đặt cho ứng dụng.
Bước 1. Trước khi bắt đầu tạo bộ cài, chúng ta cần chuyển chế độ dịch
sang Release

Chuyển sang chế độ Release

523
Bước 2. Click đúp vào nút Properties trên Solution Explorer để mở hộp
thoại và chọn nút Publish

Giao diện Publish


Bước 3. Sử dụng trình hỗ trợ Publish Wizard
Vì là lần đầu tiên xuất bản ứng dụng, nên sử dụng trình hỗ trợ Pubish
Wizard để thiết lập từng tham số. Từ những lần sau có thể click luôn
nút Publish Now để xuất bản ứng dụng. Các bước trong Publish Wizard như
sau:
1. Lựa chọn thư mục để chứa bộ cài sau khi hoàn tất

524
Lựa chọn nơi chứa bộ cài
Mặc định bộ cài sẽ được tạo ra trong thư mục publish trực thuộc thư mục
dự án. Chúng ta có thể đặt lên trên một ổ đĩa mạng hoặc ftp server. Trong
phần này chúng ta sử dụng luôn tham số mặc định.
 Lựa chọn hình thức cài đặt

525
Lựa chọn hình thức cài đặt
Bộ cài có thể được chạy từ một web site, một thư mục mạng hoặc theo cách
truyền thống (từ ổ cứng, cd-rom, usb flash). Chúng ta lựa chọn cách thứ 3
(From a CD-ROM or DVD-ROM). Cách này sẽ tạo ra bộ cài đặt như các ứng
dụng local thông thường.
 Lựa chọn cách cập nhật nếu ra phiên bản mới

526
Lựa chọn cập nhật
Bước này cho phép thiết lập để chương trình tự động cập nhật phiên bản
mới từ một Url. Tuy nhiên, chúng ta lựa chọn không cho cập nhật tự động.
 Kết thúc, ấn Finish để bắt đầu quá trình dịch – đóng gói.

527
Sẵn sàng xuất bản
Sau bước này, Visual Studio sẽ dịch và đóng gói chương trình. Khi quá trình
kết thúc, thư mục chứa bộ cài sẽ xuất hiện.

Bộ cài của ứng dụng


Có thể copy bộ cài này tới máy của người dùng cuối và sử dụng chương
trình setup.exe để cài đặt ứng dụng lên máy người dùng.
Bước 4. Cài đặt ứng dụng
Sử dụng chương trình setup.exe trong bộ cài vừa tạo để cài đặt chương
trình vào hệ thống của người dùng cuối. Trình cài đặt này cũng tự tạo ra
shortcut trong Start menu.

528
Cài đặt ứng dụng
Đến đây chúng ta có thể bắt đầu sử dụng phần mềm bằng cách thêm dữ
liệu.
Để gỡ bỏ cài đặt có thể sử dụng cách thức gỡ bỏ từ Control Panel như các
ứng dụng bình thường.

529
Chúc mừng các bạn đã hoàn thành một chương trình ứng dụng hoàn chỉnh.
Mặc dù ứng dụng còn đơn giản nhưng có thể hoạt động ổn định và có khả
năng tiếp tục phát triển.
Qua quá trình phát triển ứng dụng này chúng ta đã học được những kỹ thuật
lập trình C# cơ bản và một phần nâng cao, cũng như học thêm một số cách
thức vận dụng của lập trình hướng đối tượng trong phát triển ứng dụng.

530

You might also like