Professional Documents
Culture Documents
Hướng Dẫn Tự Học Lập Trình C# Toàn Tập
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#.
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.
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#.
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:
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.
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:
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
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.
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.
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.
24
Thư mục solution
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#.
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ủ.
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#.
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.
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;
}
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.
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.
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ả.
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#.
39
Các kiểu số nguyên 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.
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.
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"
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.
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#.
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.
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
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)
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"
>
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.
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.
Đâ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
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"
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.
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
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.
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();
}
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#.
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();
}
}
}
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.
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();
}
}
}
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.
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
""
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.
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
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
>
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.
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#.
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.
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;
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#.
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.
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");
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#
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.
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();
}
}
}
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.
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ả đó.
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).
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.
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;
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.
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,…).
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.
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:
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)
{
//...
}
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#”.
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).
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.
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; } }
}
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.
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.
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#.
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)
{
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 đó.
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.
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ộ”.
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.
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).
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.
Đâ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.
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#.
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
là
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.
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();
}
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 đó.
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:
202
1. class Parrot : Bird
2. class Cockatoo : Parrot
Kết quả chạy chương trình ví dụ trên như sau:
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.
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.
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.
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à:
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.
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 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
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#.
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();
}
}
}
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.
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.
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.
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:
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.
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.
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();
}
}
}
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.
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.
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;
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.
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ủ.
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!");
}
}
}
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.
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.
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.
Đầ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;
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.
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.
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.
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,....
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ề.
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.
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).
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.
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.
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.
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.
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:
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:
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#.
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
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).
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.
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>.
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.
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.
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).
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):
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#.
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.
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.
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.
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:
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);
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).
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.
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.
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.
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.
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:
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
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
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ể.
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:
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).
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#.
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
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
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”.
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:
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.
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.
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.
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.
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)
và
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();
}
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.
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
}
}
}
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.
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;
}
}
}
}
}
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();
}
}
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.
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;
}
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;
}
}
}
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.
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();
}
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.
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.
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.
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
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).
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";
}
}
}
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
}
412
Bước 4. Dịch và chạy thử chương trình với lệnh update
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();
}
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ệ).
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;
}
}
}
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:
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.
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ế.
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;
}
}
}
}
}
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
{
}
}
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);
}
}
}
}
}
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.
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.
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
}
}
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));
}
}
}
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);
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.
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
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.
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).
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.
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.
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();
}
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}]
Và
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.
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}");
}
}
}
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.
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).
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ể.
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
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ó.
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)
{
}
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.
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;
...
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
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
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
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.
482
Bước 3. Dịch và chạy thử với lệnh delete
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
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.
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}'");
}
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.
489
Bước 4. Dịch và chạy thử chương trình
490
{
Confirm("Do you really want to clear the shell? ", "do clear");
return;
}
Repository.Clear();
Inform("The shell has been cleared");
}
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!");
}
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).
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.
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ể.
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();
}
507
Bước 5. Dịch và chạy thử chương trình với lệnh “show stats”
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).
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.
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
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.
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
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();
}
}
}
}
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();
}
}
}
519
{
_c.DataAccess = da;
_c.DataFile = tập tin;
Success("The changes will be available next time");
}
}
}
520
Bước 9. Dịch và chạy thử chương trình với các chức năng mới
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.
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
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.
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