Lập trình C# - Phần 4: C# Nâng cao (THPT Chuyên Lê Hồng Phong)

Chia sẻ: Lê Văn Vương | Ngày: | Loại File: PDF | Số trang:17

0
27
lượt xem
4
download

Lập trình C# - Phần 4: C# Nâng cao (THPT Chuyên Lê Hồng Phong)

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Không gian tên là một định nghĩa mới trong ngôn ngữ máy tính, nhưng chúng rất hữu dụng, và một số có thể chứng minh rằng sự tồn tại của chúng là cần thiết. Một trong những vấn đề lớn nhất của lập trình là quá tải tên (name overlapping).

Chủ đề:
Lưu

Nội dung Text: Lập trình C# - Phần 4: C# Nâng cao (THPT Chuyên Lê Hồng Phong)

  1. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Lập trình C# Dịch từ cuốn sách Beginning C Sharp Game Programming Phần 4: C# Nâng cao Không gian tên (Namespace) Không gian tên là một định nghĩa mới trong ngôn ngữ máy tính, nhưng chúng rất hữu dụng, và một số có thể chứng minh rằng sự tồn tại của chúng là cần thiết. Một trong những vấn đề lớn nhất của lập trình là quá tải tên (name overlapping). Hãy nói bạn tạo một rừng các lớp cho chương trình của bạn, và sau đó bạn muốn nhập thư viện của một người nào khác để giúp cho chương trình của bạn. Điều gì sẽ xảy ra nếu một số lớp của người đó có cùng tên với các lớp của bạn, nhưng làm các việc khác nhau? Thật không may, điều đó xảy ra rất nhiều. Lấy ví dụ, cả Direct3D và DirectSound đều có các lớp tên là Device, và bạn rõ ràng không thể có hai lớp có cùng tên. Không gian tên làm nó trở nên đơn giản, vì vậy bạn có thể ám chỉ các thiết bị khác nhau như Direct3D.Device và DirectSound.Device. Bạn có thể nghĩ không gian tên giống như một thành phố. Nếu bạn chỉ nói với một người nào đó rằng bạn sống ở đường Nguyễn Văn Cừ (chẳng hạn), thì có đến hàng ngàn đường Nguyễn Văn Cừ trong đất nước. Nếu muốn xác định chính xác bạn sống ở đâu, bạn cần phải nói với người đó cả thành phố bạn sống nữa. Tạo ra một không gian tên giống như việc cụ thể thành phố và đường của bạn mà trong đóbạn có thể đặt một số lớp (đường xá) bên trong một không gian tên xác định (thành phố), do đó chương trình của bạn sẽ được phân chia gọn gàng hơn. Bạn có thể cho hầu như mọi thứ vào một không gian tên, bao gồm lớp, cấu trúc, trích xuất và cả một không gian tên khác! Hình 4.1 cho thấy một ví dụ về không gian tên. Không gian tên System Không gian tên Chapter04 Không gian tên System.Data Lớp Chapter04.Spaceship Không gian tên System.Collections Lớp Chapter04.Spacesation Lớp System.Console Hình 4. 1 Đây là hai không gian tên, System và Chapter04. Trong các không gian tên là các không gian tên con và các lớp. Lập trình C# - Phần 4: C# Nâng cao Trang 1
  2. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Không gian tên rất tuyệt bởi vì bạn có thể phân cấp chúng – bạn có thể cho nhiều không gian tên vào một không gian tên đang tồn tại. Ví dụ, .NET framework bắt đầu với không gian tên System, với các không gian tên khác trong nó, như là System.Data hay System.Collections. Không gian tên phần cấp cho phép bạn tạo cả những hệ thống phân cấp lớn hơn, giống như Việt Nam tồn tại ở Đông Nam Á, Thành phố Hồ Chí Minh tồn tại trong Việt Nam, và đường Nguyễn Văn Cừ tồn tại ở Thành phố Hồ Chí Minh. Tạo Không gian tên Đây là một số mã mô tả việt sử dụng một không gian tên: namespace Chapter04 { class Spaceship { // Mã ở đây }; class Spacestation { // Mã ở đây }; } Và sau đó, ở bên ngoài không gian tên, bạn sẽ truy cập các lớp đó như thế này: Chapter04.Spaceship s = new Chapter04.Spaceship(); Một tính năng khác của không gian tên là chúng có thể được phân chia thành nhiều phần. Ví dụ, bạn có thể có mã như thế này trong một tập tin: namespace Chapter04 { class Spaceship // blah blah } và sau đó cho một trạm không gian vào một tập tin khác: namespace Chapter04 { class Spacestation // blah blah } Trình dịch C# sẽ tự động ghép các không gian tên cho bạn, vì vậy bạn không phải cho tất cả vào một tập tin lớn. Sử dụng Không gian tên Khi bạn đang ở trong một không gian tên, bạn có thể sử dụng tất cả những gì trong không gian tên đó mà không cần phải định danh nó. Nếu bạn muốn truy cập lớp Spaceship bên trong lớp Spacestation trong ví dụ trước tôi cho bạn thấy, thì bạn chỉ cần đánh vào Spaceship và C# sẽ cho rằng bạn đang nói đến Chapter04.Spaceship bởi vì bạn đang ở trong cùng không gian tên. Tuy nhiên, nếu bạn đang ở ngoài không gian tên, bạn phải định danh không gian tên bằng cách gõ Chapter04. phía trước. Lập trình C# - Phần 4: C# Nâng cao Trang 2
  3. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Đương nhiên, gõ Chapter04.Spaceship nhiều lần có thể gây phiền toái một lúc, đặc biệt là bạn biết rằng bạn chỉ sử dụng Spaceship trong Chapter04 chứ không phải nơi nào khác. May mắn thay, bạn được phép nói cho trình dịch C# rằng bạn muốn sử dụng tất cả những gì trong không gian tên đó, bằng cách sử dụng từ khóa using. Sử dụng nó như thế này: // Ở phía trên của mã nguồn: using Chapter04; // phía sau trong tập tin: Spaceship s = new Spaceship(); Ghi chú Từ khóa using có thể được đặt ở một số nơi. Từ khóa không thể được đặt trong một lớp, cấu trúc hay trích xuất, nhưng nó có thể đặt ở gần như mọi chỗ khác. Thực tế thì nó thường được đặt ở đầu tập tin mã nguồn, vì vậy bạn sẽ biết những thư viện nào bạn cần ngay tức thì. Bí danh của Không gian tên Không gian tên phân cấp có thể gây phiền toái. Có thể bạn chưa thấy điều đó, nhưng khi bạn đi sâu vào lập trình DirectX, bạn sẽ hét lên “Microsoft chết tiệt!” ở torng cổ họng của bạn… trừ khi bạn biết về bí danh cho không gian tên. Mọi thứ liên quan đến Direct3D đều ở bên trong không gian tên Microsoft.DirectX.Direct3D. Vậy nếu bạn muốn truy cập một thiết bị Direct3D, bạn sẽ phải gõ Microsoft.DirectX.Direct3D.Device, đúng không? Thật may mắn, đặt bí danh cho không gian tên sẽ làm mọi thứ tốt hơn! Về cơ bạn, bạn có thể lấy một không gian tên và nói với C# cho nó một bí danh. Đây là một bí danh cho không gian tên Direct3D: using D3D = Microsoft.DirectX.Direct3D; D3D.Device d; // thay cho: Microsoft.DirectX.Direct3D.Device d; Hãy xem, đặt bí danh cho không tên gian làm mọi thứ đơn giản như thế nào? Tính đa hình (Polymorphism) Chủ đề về tính đa hình rất là rộng và phức tạp – các trường đại học thường mở cả khóa học về chủ đề này. Tôi có thể cho bạn cái nhìn thoáng qua về chủ đề này. Nhưng dù sao bạn cũng không cần phải học bất cứ quá phức tạp về tính đa hình. Về ngôn ngữ, cái từ đa hình nghĩa là “nhiều dạng”. Trong ngôn ngữ máy tính, tính đa hình cho phép bạn tương tác với nhiều đối tượng khác nhau mà không cần lo về việc những đối tượng này thực sự là gì. Đâu là một ví dụ đời thực của tính đa hình theo nghĩa của ngôn ngữ máy tính, hãy nghĩa về một cái xe hơi. Bạn vào một chiếc xe, mở nó lên, đạp chân ga, và bạn biết điều gì sẽ xảy ra: Chiếc xe bắt đầu chạy! Bây giờ ra khỏi chiếc xe đó, vào một chiếc xa hoàn toàn khác, và làm tương tự: Chiếc xe đó cũng bắt đầu chạy! Cá hai chiếc xa đều có chung giao diện (interface), và bạn thực sự không cần quan tâm làm sao động cơ hoạt động ở bên dưới. Cho dù bạn đang lái một cái xe bốn xi lanh bình thường, một chiếc xe đua tám xi lanh, hay một chiếc xa hơi điện, bạn biết rằng khi đạp ga, chiếc xe bắt đầu Lập trình C# - Phần 4: C# Nâng cao Trang 3
  4. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC chạy. Đây là ví dụ tốt nhất cho tính đa hình. Máy vi tính gọi một đối tượng làm việc, và đối tượng đó, không quan tâm nó là gì, sẽ làm việc. Đa hình cơ bản Hãy nói bạn có một cây phân cấp rất cơ bản: một gốc và hai con. Gốc là Spaceship (tàu vũ trụ) và các con là CombatShip (tàu chiến) và CargoShip (tàu chở hàng), như hình 4.2. Hình 4. 2 Một cây phân cấp đơn giản Bạn có thể chơi với chúng như bình thường bạn vẫn làm: Spaceship s = new Spaceship(); CargoShip c = new CargoShip(); Đương nhiên, không có gì mới. Nhưng điều đặc biệt là một CargoShip là một Spaceship cho phép bạn thực hiện một vài thủ thuật. Nhìn dòng mã này: Spaceship s = new CargoShip(); Đoạn mã đó hoàn toàn hợp lệ. Dù sao, một CargoShip là một SpaceShip, vì vậy nó sẽ có nghĩa khi làm một Spaceship trỏ tham chiếu tới một CargoShip, đúng không? Có một giới hạn cho điều này: Tham chiếu SpaceShip sẽ không cho phép truy cập những phần riêng nào trong lớp CargoShip mà nó không thừa kế từ SpaceShip. Giả sử rằng SpaceShip có hàm Refuel, và CargoShip có hàm LoadCargo, thì hãy nhìn vào đoạn mã ví dụ sau: Spaceship s = new CargoShip(); s.Refuel(); // ok // s.LoadCargo(); // LỖI BIÊN DỊCH. SpaceShip không được LoadCargo CargoShip c = (CargoShip)s; // vì vậy, chuyển nó thành CargoShip c.LoadCargo(); // ok Lập trình C# - Phần 4: C# Nâng cao Trang 4
  5. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Bất kể khi nào mà bạn có một tham chiếu đến một lớp, bạn chỉ có thể truy cập vào những tính năng của lớp xác định, bất kể nếu đối tượng thực sự có thể hỗ trợ nhiều tính năng hơn nữa. Ghi chú Ghi chú rằng bạn không thể sử dụng tính đa hình theo chiều ngược lại. Nếu bạn cố gắng viết CargoShip c = new SpaceShip();, bạn sẽ có một lỗi biên dịch. Một SpaceShip không phải là một CargoShip. Hàm ảo (Virtual Function) Một trong những khía cạnh quan trọng nhất của tính đa hình là ý tưởng về một hàm ảo. Một hàm ảo cơ bản cho phép bạn định nghĩa một hàm trong lớp ốc và sau đó có thể thay đổi nó. Hãy nói như thế này, mọi tàu vũ trụ lo việc bị bắn bởi laser một cách khác nhau, vì vậy bạn định nghĩa việc xử lý việc đó trong lớp SpaceShip gốc. Sau đó, bạn đề nghị rằng tàu chiến có thể lo việc bị trúng đạn một cách khác bởi vì chúng có giáp tốt hơn. Hàm ảo cho phép bạn giải quyết tình huống này một cách dễ dàng, như bạn sẽ thấy trong những mục sau. Khi không có Hàm ảo Đây là một số mã sẽ làm rõ cái gì sẽ xảy ra trong ví dụ của tôi mà không có hàm ảo: class Spaceship { public void LaserHit() { // Thiệt hại nhiều } } class CombatShip : Spaceship { public void LaserHit() { // Thiệt hại ít } } Điều mà tôi vừa làm là tạo một lớp SpaceShip và mặc định làm cho tàu bị nhiều thiệt hại khi bị trúng laser. Tôi muốn CombatShip sẽ bị thiệt hại ít hơn bởi vì chúng có nhiều giáp hơn, vì vậy tôi tạo một hàm LaserHit mới thực hiện thiệt hại ít hơn. Đoạn mã làm chính xác những gì bạn nghĩ là: Spaceship s = new Spaceship(); CombatShip c = new CombatShip(); s.LaserHit(); // Thiệt hại nhiều c.LaserHit(); // Thiệt hại ít Không có thủ thuật gì ở đây. Nhưng về các mã này thì sao? Spaceship s = new Spaceship(); Spaceship c = new CombatShip(); s.LaserHit(); // Thiệt hại nhiều c.LaserHit(); // Thiệt hại nhiều... tại sao? Lập trình C# - Phần 4: C# Nâng cao Trang 5
  6. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Điểm khác nhau là gì ? Thay vì sử dụng một tham chiếu CombatShip như ví dụ đầu tiên, tôi lại sử dụng tham chiến SpaceShip thay vào đó, vì vậy tại sao CombatShip lại bị thiệt hại giống như một SpaceShip bình thường? Lý do là hàm LaserHit của SpaceShip không bao giờ biến mất – nó vẩn ở đó. Khi bạn gọi c.LaserHit(), nó gọi SpaceShip.LaserHit() bởi vì nó biết c là SpaceShip. Vì sao trình biên dịch quá ngu ngốc khi không nhận ra c thực sự là một CombatShip đang ngụy trang ? Đó là cách mà trình biên dịch phải làm việc, nếu bạn muốn nó thực hiện theo cách mà bạn muốn nó thực hiện, bạn phải dùng tính ảo. Chào mừng đến với tính Ảo Hàm ảo là một phát minh tuyệt vời. Chúng cũng không thực sự phức tạp. Trước khi tôi giải thích về chúng, để tôi thay đổi các định nghĩa lớp của SpaceShip và CombatShip từ mục trước, để thêm vào vài từ khóa trong các định nghĩa hàm : class SpaceShip { virtual public void LaserHit() { // Thiệt hại nhiều } } class CombatShip : Spaceship { override public void LaserHit() { // Thiệt hại ít hơn } } Hai điểm nhỏ đã được thay đổi: từ khóa virtual đã được thêm và hàm SpaceShip.LaserHit, và override được thêm vài CombatShip.LaserHit. Bây giờ, nếu bạn chạy mã này, nó sẽ làm chính xác những gì bạn muốn: Spaceship s = new Spaceship(); Spaceship c = new CombatShip(); s.LaserHit(); // Thiệt hại nhiều c.LaserHit(); // Bây giờ thiệt hại ít hơn rồi. Hooray! Tại sao cái này lại hoạt động? Khai báo một hàm virtual nói với trình biên dịch rằng hàm này có thể bị thay thế bằng một phiên bản khác trong một lớp con. Nó nói: “Hey, hàm LaserHit này hoạt động với tất cả SpaceShip đó, như một số SpaceShip sau này có thể thay đổi nó!.” Tương tự, khai báo một hàm override nói với trình biên dịch rằng hàm đó là ghi đè lên một phiên bản cũ hơn. Nó nói: “Hey, tôi biết hàm này được khai báo sớm hơn rồi, nhưng phiên bản mới này tốt hơn nè, nên hãy thay thế cái trước đó!” Ghi chú Nếu bạn không sử dụng từ khóa override khi khai báo CombatShip.LaserHit, thì bạn sẽ đi vào chung vấn đề trước đó. Mọi CombatShip, khi được xử lý như SpaceShip, sẽ gọi SpaceShip.LaserHit thay vì cái hàm mà bạn muốn nó gọi. Bạn phải khai báo rõ ràng là một hàm là ghi đè lên phiên bản cũ hơn. Ngôn ngữ như C++ và Java không yêu cầu điều này, vì vậy nó có thể gây một chút lầm lẫn với bạn lần đầu tiên. Lập trình C# - Phần 4: C# Nâng cao Trang 6
  7. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Ghi chú Từ khóa trái ngược với override là từ khóa new. Để trở lại với tình trạng ban đầu, bạn sẽ gõ new public void LaserHit() thay vì override public void LaserHit(). Điều nàu sẽ ngăn SpaceShip thế chỗ phiên bản mới; phiên bản cũ sẽ được gọi khi bạn đang làm việc với một tham chiếu SpaceShip, và phiên bản mới sẽ được gọi khi bạn làm việc với một tham chiếu CombatShip. Trừu tượng (Abstraction) Bạn đôi khi gặp một tình huống mà bạn không biết hành vi mặc định của một lớp gốc là gì. Có thể bạn sẽ nhận ra rằng câu nói “mọi tàu vủ trụ sẽ bị trúng laser theo cách này” là một cách nói ngu ngốc bởi vì mỗi tàu vũ trụ là khác nhau và dù sao đi nữa, bạn cũng ghi đè hàm LaserHit trong mỗi lớp con. Vậy tại sao phải định nghĩa hàm Spaceship.LaserHit trong lần đầu tiên? Trong trường hợp này, bạn sẽ muốn sử dụng một tính năng gọi là trừu tượng. Hàm Spaceship.LaserHit là trừu tượng – bạn không biết các tàu sẽ bị trúng laser như thế nào, nhưng bạn vẫn biết mọi tàu vẫn có thể bị trúng laser. Nếu bạn đơn giản loại bỏ hàm LaserHit khỏi lớp Spaceship, bạn sẽ làm mọi chuyện rói tung lên: Spaceship s = new CargoShip(); s.LaserHit(); // Lỗi! Spaceship không biết bị trúng laser như thế nào Thật may mắn, C# cho bạn một cách để nói, “Mọi tàu vũ trụ biết làm sao khi trúng laser, nhưng tôi chưa chắc chắn nó làm như thế nào.” Đây là một định nghĩa lại của lớp Spaceship: abstract class Spaceship { abstract public void LaserHit(); } Bây giờ bạn có một lớp Spaceship, và bạn biết rằng tất cả tàu vũ trụ có thể bị trúng laser. Điều này sẽ ảnh hưởng như thế nào? Liệu dòng mã sau có hợp lệ? Spaceship s = new Spaceship(); // Lỗi! Oops. Bạn không thể tạo Spaceship thêm được nữa. Điều này OK, bởi vì bạn có lẽ không muốn làm điều đó nữa – thay vào đó bạn có lẽ muốn tạo CombatShip và CargoShip: Spaceship s1 = new CargoShip(); Spaceship s2 = new CombatShip(); s1.LaserHit(); s2.LaserHit(); Ghi chú Bạn không thể làm nhanh các lớp trừu tượng. Hơn nữa, nếu bạn có bất kỳ hàm trửu tượng nào trong lớp, lớp đó cũng phải được khai báo thành trừu tượng. Ghi chú Bất kỳ hàm được khai báo là trừu tượng phải được khai báo như một hàm ghi đè trong lớp con. Nếu bạn không làm điều này, bạn sẽ có một lỗi biên dịch. Lập trình C# - Phần 4: C# Nâng cao Trang 7
  8. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Tính đa hình và các Hàm Tôi nghĩ tôi sẽ têm một ghi chú nhỏ trong việc sử dụng tính đa hình với các thông số của hàm, trong trường hợp định nghĩa này vẫn chưa thật sự rõ với bạ. Hãy nói bạn tạo một hàm làm việc với tất cả các loại tàu vũ trụ. Một số thứ như thế này: class Foo { static void ProcessSpaceship( Spaceship s ) { // Một vài mã s.LaserHit(); } } Bạn có thể turyền các tàu chở hàng hay các tàu chiến vào hàm và nó sẽ không quan tâm đến loại tàu vũ trụ của bạn là gì: CargoShip cargo = new CargoShip(); CombatShip combat = new CombatShip(); Foo.ProcessSpaceship( cargo ); Foo.ProcessSpaceship( combat ); Hàm Foo.ProcessSpaceship không quan tâm đến loại tàu nào bạn dùng bởi vì tất cả các tàu có cùng các khả năng cơ bản. Đó là sức mạnh của tính đa hình. Object Trong C#, có một lớp tên là object (đối tượng), nơi mà mọi thứ đều thừa kế tự động. Điều này cho phép bạn lưu trữ dữ liệu trong một đối tượng chứa một các dễ dàng (bạn sẽ thấy trong mục sau của phần này). Hãy nhìn đoạn mã này: object o = new CargoShip(); o = new int(); o = new float(); o = new WeebulCapacitorInfluxGasket(); Object có thể lưu bất cứ cái gì. Sử dụng lớp object là một cách dễ dàng để biến kiểu giá trị - như kiểu số dựng sẵn hay các cấu trúc – thành kiểu tham chiếu. Điều này được thực hiện bởi vì object có thể được sử dụng như là một cách đóng gói kiểu giá trị. Khi bạn cho một kiểu giá trị vào một object, object lập tức cấp phát bộ nhớ cho kiểu giá trị đó, sao chép giá trị và trỏ nó tới bộ nhớ mới. Hãy nhìn mã sau: int x = 10; object o = x; // o bây giờ trỏ tới bản sao của số nguyên 10 x = 20; // thay đổi x; o sẽ không thay đổi bởi vì nó được sao chép x = (int)o; // mở gói o, x bây giờ lại là 10 Đoạn mã đó cho bạn thấy làm sao để thực hiện việc đóng gói và mở gói cơ bản. Lập trình C# - Phần 4: C# Nâng cao Trang 8
  9. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Ghi chú Bất kể khi nào bạn mở gói một object, bạn phải gọi kiểu ban đầu một cách rõ ràng (hay một số kiểu liên quan, nếu nó có thể tương thích). Sự chuyển đổi ngầm là không thể xảy ra. Mảng (Array) Bạn biết đấy, tôi thực sự không thể tin được là phải học lâu như vậy mới đến phẩn mảng này. Tôi có thể nói điều gì – C# là một cái thứ ngôn ngữ phức tạp chết tiệt. Mảng là những hộp chứa cho phép bạn lưu trữ một số đối tượng trong nó. Cơ bản, bạn cần một số cách để lưu trữ rất nhiều dữ liệu, và sử dụng các biến bình thường để lưu tất cả sẽ trở nên tẻ nhạt một cách rất nhanh chóng: Spaceship s1; // ok Spaceship s2; // meh Spaceship s3; // ok cái này gây phiền phức một chút ... Spaceship s20; // ngón tay bắt đầu đau rồi ... Spaceship s42; // TRỜI ƠI DỪNG LẠI ĐI Thật là kinh khủng khi lưu trữ dữ liệu theo cách đó. Đừng làm vậy. Không bao giờ. Hay tôi sẽ gửi một con sóc đến nhà bạn và nhai dây điện máy vi tính của bạn trước khi bạn nhấn nút Save, sau khi bạn đã dành 10 tiếng đồng hồ để đánh mã trong cơn điên vớ vẩn của bạn. Thay vì phải làm tất cả những thứ ngu ngốc như thế, hãy tạo một mảng, một khối dữ liệu mà bạn có thể truy cập bằng số thứ tự. Một ví dụ cơ bản về Mảng Đây là một ví dụ về sử dụng mảng: int[] array = new int[10]; array[0] = 0; // phần tử đầu tiên là 0 array[1] = 10; // phần tử thứ hai là 10 ... array[9] = 90; // phần tử cuối cùng là 90 Bây giờ bạn có mười số nguyên, và bạn có thể truy cập chúng dễ dàng bằng cách sử dụng ngoặc vuông sau tên mảng. Ghi chú Mảng sử dụng các đánh số từ không, tức phần tử đầu tiên trong bất kỳ mảng nào đều có chỉ số là 0 thay vì 1, theo nhiều người vẫn nghĩ. Điều này nghĩa là mảng trong ví dụ trước chỉ có chỉ số hợp lệ từ 0 đến 9, và 10 là không hợp lệ. Mảng là cái gì? Một mảng, như tôi đã nói từ trước, là một khối dữ liệu. Khi bạn gõ mã sau, nó nói rằng bạn đang tạo một biến tên là a, và một tham chiếu đến một mảng số nguyên: int[] a; Tất cả mảng đều là kiểu tham chiếu, có nghĩa là bạn phải sử dụng từ khóa new để tạo một mảng thực sự: Lập trình C# - Phần 4: C# Nâng cao Trang 9
  10. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC a= new int[8]; Dòng mã đó tạo một mảng với tám số nguyên, như cho thấy trong hình 4.3. Hình 4. 3 Hình này cho thấy một mảng gồm tám số nguyên Với tất cả ý đồ và mục đích, bạn có thể điều chỉnh a như những kiểm tham chiếu khác. Đây là một số mã cho thấy làm sao bạn có thể sử dụng mảng: int[] a = new int[10]; int[] b = a; // bây giờ b đã trỏ đến cùng mảng b[0] = 10; // thay đổi b thì cũng thay đổi a int i = a[0]; // i bây giờ là 10 b = null; // b không trỏ đến thứ gì nữa a = new int[20]; // mảng cũ đã bị mất, bị gom rác về sau object c = a; // bạn có thể tạo nó là một kiểu “object” Mảng khá dễ để sử dụng, như bạn đã thấy. Khởi tạo trong dòng Bạn có thể khởi tạo giá trị trực tiếp cho một mảng khi bạn tạo nó, khi sử dụng đoạn mã này: int[] array = new int[] { 1, 2, 3, 4, 5 }; Mã này tạo ra một mảng số nguyên mới có năm phần tử, mỗi phần tử có giá trị là 1, 2, 3, 4, và 5. Tham chiếu vs Giá trị Trong mục trước, tôi đã cho bạn thấy một mảng số nguyên, mà nó là kiểu giá trị. Điều này khá đơn giản để hiểu. Nhưng điều gì xảy ra khi bạn tạo một mảng của kiểu tham chiếu, như là Spaceship? Spaceship[] s = new Spaceship[5]; Điều này có tạo ra một mảng có năm tàu vũ trụ hay không? Không. Nó thực sự đã tạo ra một mảng có năm tham chiếu tàu vũ trụ. Đoạn mã sau sẽ tạo ra tình huống mà hình 4.4 mô tả. Lập trình C# - Phần 4: C# Nâng cao Trang 10
  11. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Spaceship s = new Spaceship[5]; s[0] = new Spaceship(); s[2] = new Spaceship(); Hình 4. 4 Một mảng tham chiếu, với một số hợp lệ và một số là null Phần tử 0 và 2 đã được trỏ tới các tàu vũ trụ, nhưng những phần tử còn lại vẫn không trỏ vào đâu. Bạn có thể thấy rằng một mảng của kiểu tham chiếu thực sự chỉ giữ các tham chiếu, không phải là kiểu thực. Bạn có thể tạo từng đối tượng torng mảng bằng tay nếu bạn cần sử dụng chúng. Như bạn đã học ở Phần 2, bạn có thể sử dụng vòng lặp for để thực hiện quá trình này đơn giản hơn. Thừa kế và Mảng Một trong những điều tốt nhất về mảng là chúng hỗ trợ hoàn toàn sự thừa kế. Hãy nhìn vào đoạn mã ví dụ này: Spaceship[] s = new Spaceship[4]; s[0] = new CombatShip(); s[1] = new CargoShip(); s[2] = new CargoShip(); s[3] = new CombatShip(); for( int i = 0; i < 4; i++ ) s[i].LaserHit(); // bắn từng tàu Đoạn mã này tạo ra một mảng bốn tàu vũ trụ, và điền vào đó hai tàu chở hảng và hai tàu chiến. Hai dòng cuối cho bạn thấy một vòng lặp for lặp xuyên suốt mảng và bắn laser vào từng tàu. Điều này hoạt động tốt bởi vì trình biên dịch biết tất cả tàu vũ trụ biết làm thế nào khi bị trúng laser, và nó không quan tâm đến tàu đó là tàu chiến hay tàu chở hàng! Ghi chú Bạn có thể dùng thuộc tính Length của mảng để lấy số phần tử của mảng nếu bạn không biết mảng rộng bao nhiêu. Lập trình C# - Phần 4: C# Nâng cao Trang 11
  12. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Mảng nhiều chiều Cho đến nay, tất cả những gì tôi cho bạn thấy là các mảng một chiều. Nếu bạn biết về hình học, bạn biết rằng thứ gì đó mà chỉ có một chiều thì chỉ có một định nghĩa chiều dài; không có chiều rộng hay chiều cao. Trong một chiều, bạn chỉ có thể vẽ được đoạn thẳng. Nó cũng giống như trong mảng: mảng một chiều có thể hình dung như một đoạn thẳng. Nếu bạn dùng định nghĩa trong hình học và mở rộng nó cho hai và ba chiều, bạn có thể tưởng tượng mảng nhìn giống như hình 4.5. Chiều dài Chiều cao Chiều cao Chiều dài Chiều dài Hình 4. 5 Hình này cho bạn thấy một bố cục thị giác và ba loại mảng khác nhau Một mảng 2D có thể được nghĩ như một lưới vuông, như một bàn cờ. Một mảng 3D có thể nghĩ như một lưới lập phương, giống như cục Rubik. Bạn có thể có các mảng có nhiều chiều hơn nữa, như 4D, 5D, hay lên tới 32D, nhưng với nhiều người, hình dung chúng có vẻ khá khó, và chúng cũng không được sử dụng nhiều cho lắm. Cách đơn giản C# khác với C/C++/Java là hỗ trợ mảng nhiều chiều. Nếu bạn đã quen thuộc với những ngôn ngữ này, thì điều này hơi khó chịu một chút, nhưng nó thực sự không khó. Về cơ bản, để khai báo một mảng 2D hay một mảng 3D, bạn sẽ gõ thế này: int[,] array2d; int[,,] array3d; Và một mảng 32D: int[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,] array32d; Bạn đơn giản chỉ cần cho n-1 dấu phẩy vào trong dấu ngoặc vuông để khai báo một mảng n-chiều. Lập trình C# - Phần 4: C# Nâng cao Trang 12
  13. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Bước tiếp theo là tạo một mảng thực sự: array2d = new int[5,5]; // mảng 5x5, như trong hình 4.5 array3d = new int[5,5,3]; // mảng 5x5x3, như trong hình 4.5 Bạn có thể đổi số chiều để phù hợp với mục đích của bạn. Truy cập các phần tử của mảng cũng khá là đơn giản: array2d[0,0] = 100; // hình vuông góc trên trái, nhìn hình 4.6 array2d[2,2] = 200; // hình vuông ở giữa array2d[0,4] = 300; // hình vuông góc dưới trái array3d[0,0,0] = 400; // khối hộp góc trên trái trước array3d[2,2,1] = 500; // khối hộp ở giữa array3d[4,0,2] = 600; // khối hộp góc trên phải sau Cách khó khăn Có một cách khác để tạo mảng, nhưng không dễ dàng như cách đầu tiên tôi cho bạn thấy. Đây là cách tiếp cận theo kiểu Java, nhưng nó thực sự khó khăn hơn rất nhiều. Cơ bản, ý tưởng mảng 2D chỉ là một mảng của các mảng 1D. Nghe kỳ kỳ, đúng không? Nhưng nó đúng. Hãy nhìn hình 4.6 để đối chiếu. Hình 4. 6 Một cách vụng về để tạo một mảng 2D bằng cách sử dụng mảng 1D để lưu trữ các mảng 1D khác. Một mảng có thể lưu trữ bất cứ thứ gì, vậy tại sao không cho nó lưu một mảng? Đây là cách mà bạn sẽ khai báo một mảng 2D và một mảng 3D theo kiểu này: int[][] array2d; int[][][] array3d; Lập trình C# - Phần 4: C# Nâng cao Trang 13
  14. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Tuy nhiên, cấp phát mảng là một công việc khó khăn. Bạn không thể chỉ gõ một số thứ như thế này: int[][] array2d = new int[5][5]; // LỖI int[][][] array3d = new int[5][5][3]; // LỖI Trong ví dụ đầu tiên, tôi đã cố gắng cấp phét sáu mảng khác nhau trong một lần (một mảng cho các mảng và năm mảng cho các số nguyên), và C# không cho phép bạn làm như vậy. Thay vào đó, bạn cần làm nhiều việc hơn một chút, đầu tiên là cấp phát mảng cho các mảng: int[][] array2d = new int[5][]; sau đó tạo mỗi mảng số nguyên riêng biệt: for( int i = 0; i < 5; i++ ) { array2d[i] = new int[5]; } Khi mà bạn đã xong, bạn có thể bắt đầu điền dữ liệu vào mảng: array2d[0][0] = 100; array2d[2][2] = 200; array2d[0][4] = 300; Đương nhiên, nó sẽ gây bừa bộn hơn với mảng 3D bởi vì bạn sẽ có một mảng cho các mảng cho các mảng. Vì vậy, bạn sẽ có khối việc để làm: int[][][] array3d = new int[5][][]; // Tạo chiều thứ hai của mảng for( int i = 0; i < 5; i++ ) { array3d[i] = new int[5][]; } // bây giờ tạo chiều thứ ba của mảng for( int i = 0; i < 5; i++ ) { for( int j = 0; j < 5; j++ ) { array3d[i][j] = new int[3]; } } Bạn có thể thu gọn mã nếu bạn muốn, nhưng tôi để đoạn mã như thế để cho bạn thấy rõ điều gì đang xảy ra ở đây. Điều đầu tiên tôi là trong ví dụ trước là tạo một mảng. Vòng lặp tiếp theo sẽ đi xuyên và điền vào năm phần tử mảng mới. Đến bây giờ, tôi đã có một mảng với 5 phần tử, mỗi phần tử là một mảng có năm phần tử khác. Và bây giờ tôi có thứ gì đó nhìn giống hình 4.6. Vòng lặp cuối cùng đi qua 25 phần tử trong mảng 2D và điền vào đó một mảng ba số nguyên. Một đống lộn xộn lớn, đúng không? Đó là vì sao phương pháp đầu tiên tôi giới thiệu là thông dụng. Lợi ích duy nhất của cách này là bạn không cần tạo một mảng hình chữ nhật. Ví dụ hãy nhìn hình 4.8. Lập trình C# - Phần 4: C# Nâng cao Trang 14
  15. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Hình 4. 7 Sử dụng phương pháp này, bạn có thể tạo mảng không có hình chữ nhật. Từ khi bạn lưu trữ một mảng của các mảng, các mảng ở chiều cuối cùng không cần có chung kích cỡ. Điều này khá hữu dụng trong một số tình huống, nhưng những tình huống này không gặp quá thường xuyên. Một kiểu lặp khác Quay về Phần 2, tôi đã cho bạn thấy làm sao để thực hiện những vòng lặp khác nhau trong C# bằng cách sử dụng cấu trúc lặp for, while và do while. Thực ra có một vòng lặp nữa trong C# nhưng tôi chưa nói đến: vòng lặp foreach. Vòng lặp foreach thực sự dễ dàng để sử dụng một cách đáng ngạc nhiên, nhưng nó chỉ có thể được dùng trong các bộ dữ liệu (collection), như là mảng. Ghi chú Vòng lặp foreach có thể được sử dụng cho những bộ dữ liệu khác mà tôi vẫn chưa cho bạn biết. Tôi sẽ nói đến chúng trong Phần 5. Đây là cú pháp cơ bản của câu lệnh: foreach( kiểu biến in bộ_dữ_liệu ) { // Mã lặp ở đây } Câu lệnh sẽ đi qua từng đối tượng bên trong bộ_dữ_liệu với bất cứ kiểu nào mà bạn chỉ định trong phần kiểu, và bạn sẽ truy cập biến bằng cách sử dụng biến. Ví dụ: Lập trình C# - Phần 4: C# Nâng cao Trang 15
  16. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC int[] array = new int[] { 1, 2, 3, 4, 5 }; int sum = 0; foreach( int i in array ) { sum = sum + i; } Đoạn mã này sẽ đi qua từng phần tử trong array và cộng chúng lại. Hạn chế duy nhất của vòng lặp foreach là nó không cho phép thay đổi nội dung của bộ dữ liệu mà nó đang hoạt động. Nếu bạn có gắng tạo ra câu lệnh như thế này thay vì cái trước, bạn sẽ có một lỗi biên dịch: foreach( int i in array ) { i = 0; } Đó là vì trình biên dịch sẽ xử lý biến bằng cách chỉ đọc, nên bạn không thể thay đổi nó. Thật không may, điều này có nghĩa là bạn chỉ có thể đọc giá trị trong mảng kiểu giá trị, và bạn không thể thay đổi tham chiếu của một mảng kiểu tham chiếu. Tuy nhiên, tin tốt là bạn có thể thay đổi kiểu tham chiếu thực, vì vậy nếu bạn có một mảng của lớp, bạn có thể tiếp tục và thay đổi các lớp như bạn muốn – bạn chỉ không thể thay đổi chỉ số trong mảng đến một lớp. Chuỗi (String) Kể từ xa xưa, nhiều cuộc liên lạc vẫn được thực hiện nhờ văn bản. Đó là lý do mà hầu hết mọi ngôn ngữ lập trình hiện nay đều có một thư viện chuỗi rất toàn diện. Không có gì ngạc nhiên, C# cũng vậy. Thật may mắn là chuỗi rất dễ sử dụng. Nếu bạn đã từng sử dụng char*s của C, thì bạn sẽ thích các chuỗi của C#. Chúng làm cho việc sử dụng chuỗi rất thú vị! Hãy để tôi vào cuộc ngay và cho bạn thấy một số ví dụ: string str = “Hello!”; // “Hello!” str = str + “ How are you?”; // “Hello! How are you?” if( str == “Hello! How are you?” ) { str = “HI!”; // “HI!” } if( str != “HI!” ) { // điều kiện sai nên mã này không còn quan trọng } Chuỗi có một tính chất độc nhất là chỉ được đọc. Bạn không thể thay đổi chúng, dù bạn cố gắng cỡ nào. Nếu bạn muốn thay đổi một chuỗi, bạn phải tạo một chuỗi mới và ghi đè lên nó (như dòng 2 trong ví dụ trước). Lớp string, bên cạnh những điều cơ bản tôi cho bạn thấy, hỗ trợ tất cả các hàm hữu dụng: string str = “Hello”; string a; a = str.ToUpper(); // trả về “HELLO” a = str.ToLower(); // trả về “hello” a = str.Remove( 0, 2 ); // trả về “llo” Lập trình C# - Phần 4: C# Nâng cao Trang 16
  17. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC a = str.Substring( 1, 3 ); // trả về “ell” Có hàng tấn các hàm như vậy, và các hàm hữu dụng nhất được liệt kê trong bảng 4.1. Ghi chú là biến str không thay đổi trong ví dụ trước, mỗi hàm đã trả về một chuỗi mới, thay vì thay đổi str. Bảng 4.1 Hàm xử lý chuỗi hữu dụng Hàm Mô tả bool Endswith( string ) Xác định một chuỗi có kết thúc bằng string hay không string Insert( index, string) Thêm string bắt đầu từ index string PadLeft( width, fillchar ) Tăng độ dài chuỗi đến width, thêm vào số fillchars là số khoảng cách bên trái string PadRight( width, fillchar ) Giống như PadLeft, nhưng về bên phải string Remove( index, count ) Bỏ count ký tự bắt đầu từ index string[] Spilt() Trả về một mảng là tất cả các từ trong chuỗi bool Strartswith( string ) Xác định một chuỗi có bắt đầu bằng string hay không string Substring( index, count ) Trả lại chuỗi con bắt đầy từ index có số ký tự là count string ToUpper() Chuyển các ký tự thường thành hoa string ToLower() Chuyển các ký tự hoa thành thường string Trim() Cắt các khoảng trắng trước và sau chuỗi string TrimEnd() Cát các khoảng trắng sau chuỗi string TrimStart() Cắt các khoảng trắng trước chuỗi Ghi chú Nếu bạn cần làm một số thao tác phức tạp với chuỗi, thay vì dùng lớp string, bạn nên dùng System.Text.StringBuilder. Thật không may là nó nằm ngoài phạm vi của sách. Tôi chỉ muốn để bạn biết về StringBuilder, cách hiệu quả quả hơn để thực hiện các thao tác chuỗi phức tạp. Một chuỗi chỉ là một mảng các ký tự, đó là vì sao mà tôi chờ cho đến giờ này để bạn biết về mảng trước khi tôi giới thiệu về chuỗi cho bạn. Bạn có thể sử dụng chuỗi gần như là một mảng khi truy cập vào từng ký tự riêng lẻ: string str = “hello!”; char c = str[0]; // ‘h’ c = str[3]; // ‘l’ Đương nhiên, chuỗi chỉ được đọc, bạn không thể thay đổi ký tự theo kiểu đó – bạn phải tạo một chuỗi mới. Nó có thể phiền phức, nhưng điều đó không thể thay đổi được. Lập trình C# - Phần 4: C# Nâng cao Trang 17
Đồng bộ tài khoản