Lập trình C# - Phần 3: Giới thiệu về lớp (THPT Chuyên Lê Hồng Phong)

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

0
25
lượt xem
5
download

Lập trình C# - Phần 3: Giới thiệu về lớp (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 đủ

Gía trị với Tham chiếu Có những cách khác nhau để một trình biên dịch nói về dữ liệu, và C# có hai cách. Mọi kiểu dữ liệu trong C# đều rơi vào một trong hai loại: Kiểu giá trị (Value type) Kiểu tham chiếu (Reference type)

Chủ đề:
Lưu

Nội dung Text: Lập trình C# - Phần 3: Giới thiệu về lớp (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 3: Giới thiệu về Lớp Gía trị với Tham chiếu Có những cách khác nhau để một trình biên dịch nói về dữ liệu, và C# có hai cách. Mọi kiểu dữ liệu trong C# đều rơi vào một trong hai loại:  Kiểu giá trị (Value type)  Kiểu tham chiếu (Reference type) Tôi sẽ giải thích các điểm khác nhau của mỗi loại trong những mục sau. Kiểu dữ liệu Một kiểu giá trị thường là một miếng nhỏ của dữ liệu mà hệ thống dành rất ít thời gian để sắp xếp. Bạn đã sử dụng kiểu giá trị trong Phần 2 với tất cả các kiểu dữ liệu số được xây dựng sẵn. Mọi thứ được liệt kê trong bảng 2.1 – như là int, float và vâng vâng – là một kiểu giá trị. Ghi chú Kiểu giá trị được tạo trên ngăn xếp hệ thống (system stack). Bạn không cần thiết để biết nó là gì, nhưng nếu bạn cảm thấy thú vị, tôi khuyên bạn rằng nên tự nghiên cứu nó. Chủ đề này vượt ra ngoài phạm vi của quyển sách này, vì vậy tôi không đủ chỗ để giải thích nó ở đây, nhưng nó sẽ giúp bạn hiểu chính xác làm sao máy vi tính hoạt động, nó sẽ ảnh hưởng đến việc tạo chương trình của bạn nhanh hơn và hiệu quả hơn. Kiểu giá trị đơn giản và rất minh bạch để sử dụng, như là đoạn mã sau: int x = 10, y = 20; x = y; // Giá trị của y được chép vào x y = 10; // y được gán bằng 10 Bên cạnh những kiểu đượcdựng sẵn, cấu trúc (structure) cũng là một kiểu giá trị, nhưng tôi sẽ đề cập về sau trong phần này. Kiểu tham chiếu Kiểu tham chiếu hoàn toàn khác biệt so với kiểu giá trị. Lớp, khác với cấu trúc, luôn luôn là kiểu tham chiếu. Kiểu tham chiếu, thay vì lưu dữ liệu một cách trực tiếp, nó lại lưu một địa chỉ, và địa chỉ đó trỏ tới dữ liệu thật trong nơi nào đó của máy vi tính. Xem hình 3.1. Khai báo một Kiểu Tham chiếu Một trong những điểm khác nhau lớn nhất giữa kiểu giá trị và tham chiếu là cách mà bạn khai báo cho nó. Một kiểu tham chiếu phải được tạo bởi từ khóa new (giả sử như chúng ta có một lớp tên Foo): Foo x = new Foo(); Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 1
  2. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Trông có vẻ nhiều việc cần làm, nhưng bạn sẽ quen với nó. Cơ bản, đoạn mã thực hiện hai công việc. Đó là: 1. Tạo một kiểu tham chiếu mới tên x, và 2. Tạo một đối tượng Foo mới trong đống dữ liệu và trỏ x đến đó. int x Kiểu giá trị 10 Int32 y địa chỉ của 10 Kiểu tham chiếu dữ liệu Hình 3.1 Kiểu giá trị được lưu trực tiếp, trong khi kiểu tham chiếu lưu một địa chỉ trỏ tới dữ liệu thật. Ghi chú Đống dữ liệu (Heap) là một phần khác của máy vi tính để lưu trữ dữ liệu. Tôi không có đủ chỗ để giải thích nó ở đây; đó chỉ là một vài thứ khác bạn nên nghiên cứu cho riêng bạn nên bạn cảm thấy thú vị. Đương nhiên, bạn không cần phải thực hiện chúng một lượt. Bạn có thể dễ dàng tách chúng ra như thế này: Foo x; x = new Foo(); Nó tùy thuộc vào bạn. Chơi đùa với các Tham chiếu Và bây giờ là lúc để chơi đùa với các tham chiếu, nó là thứ mà bạn chưa gặp trước đây. Không may cho bạn, các tham chiếu không hoạt động theo cách của các kiểu dữ liệu, mà nó có thể gây bối rối một chút. Đây là phần mà các tham chiếu có vẻ hơi khó hiểu một chút. Bạn hoàn toàn phải nhớ điều này mọi lúc môi nơi khi sử dụng các tham chiếu, còn không chươn trình của bạn sẽ trở thành một mớ hỗn độn. Ví dụ, hãy thử đoán xem đoạn mã này làm gì: Foo x = new Foo(); Foo y = new Foo(); y = x; // Thay đổi y ở đây Bạn có nghĩ sau khi đọan mã này thực thi, x giữ nguyên trạng thái ban đầu và y thay đổi, đúng không ? Sai !!! Chúng cùng thay đổi. Hãy nghe tôi – có một chút khó khăn để nhìn nhận nó, nhưng nó có nghĩa. Sơ đồ có thể giúp, nhìn hình 3.2. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 2
  3. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC x dữ liệu Foo x = new Foo(); Foo y = new Foo(); y dữ liệu x dữ liệu y = x; y dữ liệu x dữ liệu thay đổi // Thay đổi y y dữ liệu Hình 3.2 Gán x cho y làm y trỏ tới dữ liệu của x, và không thực sự sao chép giá trị như bạn mong đợi. Cơ bản, dòng này thật sự đã làm rối tung mọi thứ lên: y = x; Điều gì thật sự đã hoàn thành? Bạn muốn sao chép giá trị từ x sang y, nhưng điều đó đã không xảy ra. Thay vào đó, vì chúng là những kiểu tham chiếu, máy vi tính sẽ trỏ y vào cùng dữ liệu mà x trỏ vào. Vì vậy x và y bây giờ trỏ đến cùng một dữ liệu trong bộ nhớ, và thực hiện thao tác nào trên y sẽ làm giống vậy đối với x. Bộ thu gom rác Trong ví dụ hình 3.2, bạn có thể ghi nhận rằng y đã giành lấy một vùng nhớ, và sau đó nó bị bỏ qua khi gán x cho y. Vậy điều gì sẽ xảy ra đối với vùng nhớ mà y trỏ tới trước đó? Trong những ngôn ngữ cũ, như C, vùng nhớ đó sẽ bị mất mãi mãi. Bạn sẽ tạo cái gọi là con trỏ treo (dangling pointer), là các con trỏ giống các tham chiếu; máy vi tính biết rằng vùng nhớ đã được sử dụng, nhưng chương trình của bạn sẽ quên nó ở đâu, và bạn sẽ không bao giờ có thể lấy lại vùng nhớ đó được nữa trừ khi bạn tắt chương trình đi. C# giải quyết được vấn đề này bởi bộ thu gom rác. Mỗi lần bạn tạo một mảnh dữ liệu trong C#, .NET runtime sẽ theo dõi chương trình của bạn trỏ đến dữ liệu đó bao nhiêu lần, và nếu số lần đó về 0, thì bộ thu gom rác sẽ phát hiện và giải phóng vùng nhớ đó để sử dụng cho thứ khác. Không thể có rò rỉ bộ nhớ trong C#. null Có một giá trị đặc biệt mà bạn có thể sử dụng với kiểm tham chiếu; nó gọi là null. Giá trị null có nghĩa là “chẳng có gì”. Nếu bạn đặt một tham chiếu đến null, thì bạn đang nói với máy vi tính rằng tham chiếu đó chẳng trỏ tới đâu. Những ngôn ngữ cũ hơn sử dụng giá trị 0 để biểu thị nó, nhưng null thì dễ đọc hơn. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 3
  4. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Cơ bản về Cấu trúc (Structure) và Lớp (Class) Trong những ngày đầu của ngôn ngữ máy tính, ngôn ngữ lập trình khá đơn giản, và bạn có thể chỉ tạo được một số lượng biến giới hạn. Điều này rõ ràng làm cho chương trình rất hạn chế và khá tồi tệ. Ví dụ, bạn sẽ tạo ra chương trình như thế này trong những ngôn ngữ lập trình cũ: int SpaceshipArmor; int SpaceshipPower; int SpaceshipFuel; int EnemyArmor; int EnemyPower; int EnemyFuel; Điều này sẽ là rối tung mọi thứ rất nhanh, và làm cho nó rất khó để quản lý mã của bạn. Sử dụng lớp và cấu trúc sẽ làm cho cuộc sống của bạn đơn giản hơn khi đóng gói các dữ liệu đó vào những gói dữ liệu “rất dễ sử dụng”. Tạo ra Lớp và Cấu trúc Về cơ bản, ý tưởng đằng sau lớp và cấu trúc là tạo ra kiểu đối tượng cho riêng bạn bằng những đối tượng đã có sẵn. Một cấu trúc (structure) là một kiểu dữ liệu mà có thể giữ các dữ liệu khác ở bên trong, cho phép bạn xây dựng kiểu dữ liệu của riêng bạn. Ví dụ, đây là một cấu trúc mô tả một đối tượng tàu vũ trụ đơn giản trong C#: struct Spaceship { public int fuel; public int armor; public int power; } Ghi chú Từ khóa public nói cho trình biên dịch rằng bất kỳ hàm nào ở bất cứ đâu đầu có thể truy cập dữ liệu bên trong một cấu trúc. Bạn không phải lo về việc này; tôi sẽ đi sâu hơn vào điều này trong những mục sau. Nếu public bị bỏ đi, thì máy vi tính sẽ xem rằng bạn không muốn thứ gì bên ngoài truy cập nó. Ghi chú Để tạo một lớp, đơn giản hãy thay từ khóa struct thành class trong ví dụ trước. Và bậy giờ, bên trong chương trình của bạn, bạn có thể tạo biến tàu vũ trụ cho riêng bạn: Spaceship player; Spaceship enemy; player.fuel = 100; enemy.fuel = 100; Điều này đơn giản, phải không? Sự khác nhau giữa Cấu trúc và Lớp Trong C#, có một vài điều khác nhau cơ bản giữa cấu trúc và lớp. Cấu trúc thường đơn giản và không có nhiều thứ phức tạp bên trong chúng. Cấu trúc thường nhỏ hơn lớp, và C# luôn tạo cấu trúc là kiểu giá trị (nghĩa là chúng sẽ luôn luôn tạo trong ngăn xếp). Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 4
  5. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Lớp, khác với cấu trúc, luôn luôn là kiểu tham chiếu, và luôn được tạo trên một đống (heap), thay vì trên ngăn xếp. Lớp có nhiều thứ mà cấu trúc không có, tôi sẽ giải thích những thứ này khi chúng ta gặp nó. Đưa Hàm (Function) và Lớp và Cấu trúc Lớp và cấu trúc không chỉ có tính năng lưu trữ dữ liệu, mà chúng còn có thể thực hiện một vài phép tính nữa, nếu bạn cho chúng khả năng đó. Ví dụ, bạn muốn đặt lại mọi dữ liệu của tàu thành 100 một cách nhanh chóng; nếu không có một hàm, nó trông giống đoạn mã sau: player.fuel = 100; player.armor = 100; player.power = 100; Rõ ràng, điều này không phải là thứ bạn chỉ làm một lần, vì vậy tại sao thay vào đó không cho nó vào một hàm, nên lớp Spaceship sẽ trông như thế này (phần mới được tô đậm): struct Spaceship { public int fuel; public int armor; public int power; public void Recharge() { fuel = 100; armor = 100; power = 100; } } Bây giờ bạn có thể chỉ cần gọi hàm Recharge trên tàu vũ trụ của bạn khi bạn muốn đặt lại tất cả cái biến: player.Recharge(); Trả về giá trị Các hàm không chỉ trhực hiện các tác vụ, mà chúng còn có thể trả về giá trị. Ví dụ, hãy nói bạn có một con tàu vũ trụ; bạn biết nhiên liệu và năng lượng nó có, nhưng bạn không thực sự chắc chắn về thời gian mà năng lượng cung cấp sẽ hết. Để tính toán nó, bạn cần thiết lập một công thức – hãy nói rằng mỗi đơn vị năng lượng duy trì được hai giờ; và bạn muốn tìm xem còn bao nhiêu thời gian với số năng lượng còn lại, bạn sẽ làm điều gì đó như thế này: int hoursleft = player.power * 2; Đó là một cách để giải quyết vấn đề, nhưng đó không thực sự là một giải pháp tốt. Về sau, bạn có thể đề nghị mỗi đơn vị năng lượng tương đương với ba giờ thay vì hai. Để thay đổi điều này, bạn phải dò hết lại mã của bạn và tìm xem chỗ nào bạn sử dụng số 2 và thay nó bằng số 3. Không vui chút nào. Vì vậy, hãy đưng quá trình này vào một hàm! int HoursofPowerLeft() { return power * 2; } Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 5
  6. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Ta-da! Chữ int trước tên hàm nói cho máy vi tính biết kiểu dữ liệu nào sẽ được trả về từ hàm, và bạn sử dụng từ khóa return để trả về một giá trị. Nếu bạn không muốn trả về cái gì, thì hãy sử dụng void, như bạn đã thấy trước đó. Bạn nên ghi chú rằng mệnh đề return sẽ làm cho hàm kết thúc ngay lập tức. Nếu bạn nhìn vào đoạn mã sau, bạn sẽ thấy một số chúng sẽ không bao giờ được thực thi: int Function() { return 0; int x = 10; // Dòng này không bao giờ được thực thi } Ghi chú Bạn nên ghi chú rằng trình biên dịch C# đủ thông minh để nhận ra là đoạn mã đó sẽ không thực thi, và nó sẽ hét lên khi bạn viết mã kiểu đó. Bạn cũng có thể có nhiều câu lệnh return trong mã của bạn: int Function() { if( something ) return 0; return 1; } Trong đoạn mã này, nếu something là true, thì 0 được trả về; nếu không, 1 được trả về (và thoát, tất nhiên). Thông số Bạn cũng có thể đưa một hàm một vài thông số. Ví dụ của tôi về tính thời gian còn lại dựa trên số năng lượng còn lại là một phép tính rất đơn giản không sử dụng bất kỳ tham số nào, nhưng tôi có thể thay đổi điều này và làm cho nó linh hoạt hơn. Điều gì xảy ra nếu năng lượng giảm đi phụ thuộc vào một số tác nhân bên ngoài, như là có bao nhiêu phóng xạ trong hệ thống? Hãy nói rằng mức độ phóng xạ càng thấp, càng ít năng lượng bị tiêu tốn. Vì vậy, nếu bạn viết lại hàm trước với ý tưởng này, bạn có: int HoursofPowerLeft( int radiationlevel ) { return (power * 2) / radiationlevel; } Nếu mức độ phóng xạ là 1 (tôi đang hoàn toàn hư cấu dữ liệu đo được ở đây; hãy xem như là nó có nghĩa) và năng lượng của bạn là 100, thì số giờ còn lại là 200. Nếu mức độ phóng xạ là 2, thì bạn còn 100 giờ; và nếu là 3, thì bạn chỉ còn 66 giờ. Bạn sẽ gọi hàm đó như thế này: int hoursleft = player.HoursofPowerLeft( 1 ); Nhiều thông số Sẽ có lúc bạn muốn đưa nhiều hơn một thông số, và C# cho phép bạn làm điều đó: int Function1( int parameter1, float parameter2, double parameter3 ) Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 6
  7. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Thông số giá trị vs Thông số tham chiếu Đây là phần hơi lắt léo một chút. Hãy nói bạn có một lớp với hai thông số trông như thế này: class MyClass { public void Function1( int parameter ) { parameter = 10; } public void Function2() { int x = 0; Function1( x ); // X bằng mấy? } } Vậy, x sau đoạn mã này bằng mấy? Là 0 hay là 10? Câu trả lời là 0 bởi vì bạn đưa x vào bằng giá trị. Điều này có nghĩa là máy vi tính lấy giá trị của x, sao chép nó, vào đặt nó vào một biến mới tên là parameter; bây giờ, khi parameter thay đổi, chẳng có gì xảy ra với x. Vậy làm sao bạn có thể đưa vào bằng tham chiếu? Chỉ cần làm hai việc. Thứ nhất, thay đổi khai báo của Function1: public void Function1( ref int parameter ) Thứ hai, thay đổi cách gọi hàm như thế này: Function1( ref x ); Và bây giờ bạn đã đưa vào tham chiếu của x, và giá trị của x sẽ thay đổi. Bạn nên ghi chú rằng lớp luôn luôn đưa vào bằng tham chiếu. Ví dụ: public void Function1( Int32 parameter ) Trong hàm này, bất kỳ Int32 nào bạn đưa vào sẽ luôn luôn là tham chiếu, không phải giá trị. Quá tải hàm Bạn có thể có một lớp hay một cấu trúc với vài hàm cùng tên. Điều này nghe có vẻ ngu ngốc, nhưng nó hoạt động khá tốt trong đời thực. Ví dụ, hãy nói bạn có hai cách khác nhau để tính toán quãng đường đi của một tàu vũ trụ, đưa bởi lượng nhiên liệu còn lại của nói; một cách lấy số hàng hóa mà tàu đang chở, cách khác thì bỏ qua số hàng hóa và cho bạn kết quả trong một “điều khiện tốt nhất”. Bạn có thể tạo các hàm như thế này: public int DistanceLeft( int cargoweight ) // với hàng hóa { // Tính toán ở đây } public int DistanceLeft() // tối ưu, không hàng hóa { // Tính toán ở đây } sau đó bạn có thể gọi chúng thế này: Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 7
  8. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC int distance; distance = player.DistanceLeft( 100 ); distance = player.DistanceLeft(); Bạn có thể quá tải hàm bao nhiều lần tùy thích; điều hạn chế duy nhất là mỗi hàm quá tải phải có chữ ký khác nhau. Chữ ký được định nghĩa là các thông số mà bạn đưa vào, không phải giá trị trả về. Vì vậy bạn có thể làm thế này: void Function1(); void Function1( int p1 ); void Function1( float p1, int p2 ); Và bạn cũng có thể làm thế này: int Function1(); void Function1( int p1 ); float Function2( float p1, int p2 ); Nhưng hoàn toàn không thể làm thế này: int Function1(); float Function1(); // cùng chữ ký! LỖI! Hàm xây dựng (Constructor) Hàm xây dựng là một thứ rất có ích trong hầu hết các ngôn ngữ lập trình hiện đại. Chúng cho phép bạn tự động khởi tạo lớp và cấu trúc của bạn. Quay về những ngày xa xưa, khi bạn tạo một lớp hay cấu trúc mới, bạn thật sự không biết cái gì bên trong nó. Như bạn thấy, máy vi tính không xóa đi bộ nhớ, vì vậy khi bạn ngưng sử dụng một mảnh của bộ nhớ, máy vi tính chỉ đánh dấu là nó không được sử dụng nữa, và sắp nó ở vị trí đầu tiên trong danh mục tái sử dụng, với cùng dữ liệu mà nó đã lưu trước đó. Điều này có nghĩa là bạn thường sẽ có các cấu trúc đầy dữ liệu rác không có nghĩa, và nó có thể gây rất nhiều vấn đề nếu bạn bắt đầu sử dụng các cấu trúc đó mà không kiểm tra nó có giá trị hợp lệ hay không. Hàm xây dựng là những hàm cơ bản mà C# sẽ gọi tự động khi bạn tạo một lớp mới. Hàm xây dựng mặc định (Default Constructor) Đây là một ví dụ đơn giản của một hàm xây dựng trong một lớp: class Spaceship { public int fuel; public Spaceship() // Hàm xây dựng mặc định { fuel = 100; } } Đoạn mã đậm là cái được gọi là hàm xây dựng mặc định. Khi nào bạn tạo một tàu vũ trụ mới, hàm Spaceship (ghi chú là nó có cùng tên với lớp) sẽ tự động được gọi. Mã này tạo ra một tàu vũ trụ với nhiên liệu là 100: Spaceship s = new Spaceship(); Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 8
  9. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Hàm xây dựng không mặc định (Non-default Constructor) Như những hàm bình thường, hàm xây dựng cũng có những phiên bản quá tải, mà nó rất hữu dụng khi bạn cần cung cấp thêm dữ liệu khi tạo một lớp. Đây là một ví dụ cho thấy làm thế nào để tạo một hàm xây dựng không mặc định đưa vào một biến chỉ lượng nhiên liệu để gán cho tàu vũ trụ: public Spaceship( int p_fuel ) { fuel = p_fuel; } Về khi bạn tạo một tàu vũ trụ, bạn có thể gọi hàm xây dựng mới này như thế này: Spaceship s = new Spaceship( 50 ); // tạo một tàu với 50% nhiên liệu Bạn có thể tạo bao nhiêu hàm xây dựng tùy thích, miễn là chúng có chữ ký khác nhau. Cấu trúc và Hàm xây dựng Cấu trúc cũng có thể có hàm xây dựng, nhưng có một điều: Các cấu trúc không thể có hàm xây dựng mặc định. Microsoft tuyên bố như thế bởi vì cấu trúc phải nhẹ. Vì vậy cấu trúc có thể có hàm xây dựng khô mặc định, nhưng không thể có hàm xây dựng mặc định. Hàm phá hủy (Destructor) Nếu hàm xây dựng được gọi bất cứ khi nào lớp được tạo, thì hàm phá hủy được gọi bất cứ khi nào một lớp bị loại bỏ. Trong những ngôn ngữ cụ như C++, hàm phá hủy rất quan trọng bởi vì bạn luôn luôn phải giải phóng bộ nhớ mà bạn không sử dụng nữa. Nhưng từ khi xuất hiện bộ thu gom rác, hàm phá hủy đã gần như đi theo lối của khủng long. Chúng thật sự không được sử dụng nhiều nữa, nhưng vẫn được thêm vào để đề phóng. Ví dụ cơ bản Một hàm phá hủy trông giống thế này: class MyClass { ~MyClass() { // Mã ở đây } } Trong những ngôn nhữ giống C++, đây là nơi mà bạn sẽ tạo một hàm tự động dọn dẹp bộ nhớ cho bạn nếu nó có yêu cầu. Nhưng .NET runtime sẽ lo tất cả những vấn đề bộ nhớ này cho bạn, bạn thực sự không cần phải làm như trên. Sự phá hủy bị trì hoãn Một điều khác về hàm phá hủy là chúng không được gọi ngay tức thời. Ví dụ, hãy nhìn đoạn mã này: Spaceship s = new Spaceship(); // tàu vũ trụ mới s = null; // tham chiếu đến tàu vũ trụ bị mất Trong đoạn mã này, một tàu vũ trụ mới được tạo và một tham chiếu đến nó được gán cho s. Tuy nhiên, trên dòng tiếp theo, tham chiếu được đặt về null, nhưng hệ thống có thể giữ vùng nhớ đó một thời gian dài sau khi bạn đặt lại tham chiếu. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 9
  10. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Do đó, vấn đề chính với hàm phá hủy là bạn thật sự không biết khi nào chúng được gọi. Bạn không thể làm một lớp bị phá hủy ngay tức thời, vì vậy đừng nói rằng là nó sẽ bị phá hủy khi bạn xóa hết mọi tham chiếu. Còn nhiều thứ khác về hàm phá hủy, nhưng dù sao bạn cũng không sử dụng chúng nhiều. Nên đừng bận tâm tới chúng. Ghi chú Cấu trúc không có hàm phá hủy vì chúng là những thứ rất nhẹ. Các thủ thuật nâng cao về Lớp Hiện nay, bạn đã có vẻ quen với các lớp và bạn có thể tạo ra một cái để thực hiện những tác vụ nhỏ như lưu trữ dữ liệu và thực hiện phép tính đơn giản. Tuy nhiên, lớp còn có nhiều điều mà bạn chưa thấy. Cơ bản về thừa kế (Inheritance) Một trong những ưu điểm lớn nhất của ngôn ngữ lập trình hướng đối tượng là sự thừa kế. Tôi sẽ không đi sâu vào chủ đề thừa kế, nhưng bạn nên hiểu và có thể sử dụng sự thừa kế sau khi đọc xong cuốn sách này. Sự thừa kế cho phép bạn tạo chương trình của bạn theo một cách thực tế, vì khả năng phân cấp sẽ làm cho lớp của bạn giống như đối tượng trong đời thực. Cách dễ nhất để nghĩ về sự thừa kế là nghĩ về sự phân lớp của động vật. Một ví dụ về Thừa kế Nếu bạn nhớ về môn sinh học thời trung học, thì bạn biết các lòa thú đều có một số đặc điểm chung, nhưng sinh ra con và có tim bốn ngăn. Trong một chương trình máy vi tính, bạn có thể tạo ra lớp thú của bạn và cho nó những đặc điểm này. Nhưng dù mọi loài thú chia sẽ chung một số đặc điểm, chúng lại không chia sẻ hết mọi đặc điểm. Con người có hai chân, cừu thì có bốn – rõ ràng là vậy, và bạn không tể sử dụng một lớp thú để biểu diễn cả người lẫn cừu. Thật là ngu ngốc thì tạo ra hai lớp hoàn toàn khác nhau để biễn diễn cho người và cừu. Đoạn mã bạn viết biểu diễn những đặc điểm chung của người và cừu sẽ bị lặp lại trong hai lớp. Hình 3.3 biểu diễn tình huống này. Một cách TỒI TỆ Lớp Cừu Lớp Người Đoạn mã Đoạn mã thú Đoạn mã thú lặp lại Đoạn mã cừu Đoạn mã người Hình 3. 3 Hình này cho thấy cách sai để tạo một hệ thống người/cừu Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 10
  11. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Cảnh báo Lặp mã luôn luôn là ý tưởng tồi. Một ngày nào đó, bạn sẽ cần thay đổi một đoạn mã bạn đã viết, và nếu bạn có đoạn mã đó ở chỗ khác, tôi đảm bảo rằng bạn sẽ quên một vài nơi và sẽ có hai phiên bản khác nhau đang chạy trong mã của bạn. Đây là lúc mà thừa kế vào làm việc. Thừa kế cho phép bạn tạo một lớp mới và tự động sử dụng tất cả những đặc điểm của một lớp khác. Điều này được gọi là một mối quan hệ (con cừu là một con thú). Ví dụ 3.4 cho thấy kế thừa được sử dụng như thế nào. Một cách TỐT Lớp Thú Đoạn mã thú Lớp Cừu Lớp Người Đoạn mã cừu Đoạn mã người Hình 3. 4 Bạn có thể sử dụng thừa kế để chia sẽ đoạn mã chung giữa các lớp đúng cách Sử dụng Thừa kế Thừa kế khá dễ sử dụng trong C#. Đầu tiên bạn cần tạo một lớp gốc (base class), và nó sẽ đứng ở trên trong cây thừa kế của bạn (lớp thú trong phần trước là ví dụ). Hãy nói bạn muốn tạo một lớp tàu vũ trụ gốc, một lớp sẽ mô tả những đặc điểm chung của tất cả các tàu vũ trụ tồn tại. Như tất cả tàu vũ trụ đều có nhiên liệu trong nó, bạn có thể tạo nó trong lớp gốc của bạn: class Spaceship { public int fuel; }; Ghi chú Nhớ trong đầu là những ví dụ này rất cực kỳ đơn giản, với những đoạn mã được tôi thiểu hóa để mô tả các khái niệm cho bạn. Tôi sẽ cố gắng không làm bạn lầm lẫn giữa các khái niệm. Vậy bây giờ bạn có một tàu vũ tàu, nhưng tất cả chúng đều có nhiên liệu. Bây giờ có thể bạn muốn tạo ra một tàu chiến, và chúng có vũ khí: class Warship : Spaceship { public int weapons; Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 11
  12. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC }; Bạn đã nói với trình biên dịch rằng tàu chiến là một tàu vũ trụ khi đặt dấu hai chấm giữa tên lớp và tên lớp mà bạn thừa kế. Ví dụ, bạn cũng có thể tạo một tàu chở hàng: class Cargoship : Spaceship { public int storage; }; Bây giờ bạn có thể sử dụng những đặc điểm mà bạn vừa thêm vào và cả những đặc điểm của lớp gốc nữa: Warship w = new Warship(); w.weapons = 100; // Đặc điểm mới w.fuel = 100; // Đặc điểm thừa kế từ Spaceship Cargoship c = new Cargoship(); c.storage = 100; // Đặc điểm mới c.fuel = 100; // Đặc điểm thừa kế từ Spaceship Và điều đó khá tốt đẹp với thừa kế cơ bản. Mức độ Truy cập và Giấu Dữ liệu Cho đến hiện nay, bạn đã thấy từ public trong đoạn mã ví dụ, nhưng tôi vẫn chưa giải thích nó nghĩa là gì và tại sao nó ở đó. Về cơ bản, khi bạn nói điều gì đó là public, bạn đã nói với trình biên dịch rằng bất cứ thứ gì đều có thể truy cập chúng. Nếu bạn cho một lớp một số nguyên public, thì bất cứ thứ gì cũng có thể đọc số nguyên và thay đổi nó. Điều này được tất cả các ngôn ngữ lập trình sử dụng cho đến khi ý tưởng về giấu dữ liệu xuất hiện. Ý tưởng đằng sau việc Giấu Dữ liệu Giấu dữ liệu là một khái niệm cho phép bạn giấu dữ liệu từ những phần khác của chương trình của bạn. Bạn có thể thắc mắc tại sao bạn muốn giấy dữ liệu làm quái gì. Câu trả lời sẽ cần một ít lời giải thích. Bạn không muốn bất cứ ai đụng đến dữ liệu của bạn; bạn sẽ không biết nếu bất cứ ai làm điều gì đó gây hại đến nó. Lấy ví dụ là một tàu vũ trụ. Trong một tàu vũ trụ, khi nhiên liệu giảm còn 0, thì động cơ sẽ tắt đi. Bây giờ tưởng tượng tất cả những nơi trong mã của bạn mà bạn sẽ có các hàm chỉnh sửa số nhiên liệu của tàu vũ trụ của bạn. Có thể tàu của bạn sẽ rò rỉ một số nhiên liệu khi trúng đạn. Hay có thể bạn có một gói nhiên liệu để thêm một số nhiên liệu vào bình chứa. Có tới hàng triệu khả năng. Có rất nhiều nơi có thể tay đổi số nhiên liệu, và tất cả chúng cần phải tắt động cơ khi nhiên liệu xuống 0, điều đó là một điều tồi tệ. Hơn nữa, một ai có thể cố gắng cho nhiều nhiên liệu hơn vào bình chứa đến nỗi nó vượt quá dung tích bình, điều mà bạn không muốn xảy ra. Vì vậy, nếu bạn cho bất cừ hàm nào chỉnh sửa dữ liệu của bạn, những hàm này có thể làm chúng rối lên một cách sơ ý, và điều đó chắc chắn không phải là một điều tốt. Vì vậy bạn cần một tác vụ kiểm tra. Ví dụ, nếu bạn có sáu đạon mã khác nhau có thể chỉnh sửa số nhiên liệu của bạn, mỗi đoạn mã cần phải kiểm tra nếu nhiên liệu rơi xuống 0, thì nó cần phải tắt động cơ. Hay đạon mã cần kiểm tra để chắc chắn rằng số nhiên liệu không vượt quá lượng tối đa, vì vậy nó sẽ không tràn ra ngoài bình. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 12
  13. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Có rất nhiều đoạn mã bị lặp lại ở tất cả mọi nơi, và sẽ làm cho chương trình của bạn phức tạp và tồi tệ hơn. Vì vậy cơ bản đó là một ý tưởng tệ hại khi để cho các phần khác của chương trình đụng vào dữ liệu của bạn. Mức độ truy cập C# có định nghĩa một số mức độ truy cập. Bạn đã thấy một trong số chúng, public (công khai). Nhưng bạn có thể đoán, public nghĩa là bất cứ ai đều có thể truy cập những đặc điểm này. Hai mức độ truy cập phổ biến khác là protected (được bảo vệ) và private (riêng tư). Truy cập private nghĩa là không phần nào của mã có thể truy cập những đặc điểm này trừ khi nó từ chính lớp đó. Các phần khác không được truy cập, ngay cả lớp thừa kế. Hãy xem ví dụ này: class Spaceship { private int fuel; }; class Warship : Spaceship { public void SomeFunction() { fuel = 10; // Lỗi, không thể truy cập “fuel” } }; Spaceship s = new Spaceship(); s.fuel = 10; // Lỗi, không thể truy cập “fuel” Bên trong warship, hàm SomeFunction cố gắng truy cập vào fuel. Như từ trước ta đã nói, bạn không có vấn đề gì khi làm thế này; sau tất cả, tàu chiến là tàu vũ trụ, và do đó có nhiên liệu và có thể bị chỉnh sửa. Nhưng bây giờ nhiên liệu là private, tàu chiến không thể truy cập nó được nữa. Nhiên liệu vẫn ở đó, đương nhiên, nhưng tàu chiến không cho phép đụng vào chúng. Đoạn mã khác cũng không được phép đụng vào chúng; dữ liệu đã được giấu. Ghi chú Nếu bạn quên gán một mức độ truy cập vào hàm hay biến, thì trình biên dịch C# sẽ tự động cho rằng nó sử dụng mức truy cập private. Mức độ truy cập protected tương tự private, với một thay đổi nhỏ: Bất cứ thứ gì protected vẫn được giấu với đoạn mã bên ngoài lớp đó, nhưng những lớp thừa kết từ lớp gốc vẫn có thể truy cập những đặc điểm này. Hãy xem ví dụ sau: class Spaceship { protected int fuel; }; class Warship : Spaceship { public void SomeFunction() { fuel = 10; // Bây giờ thì ổn, bởi vì nó là protected } }; Spaceship s = new Spaceship(); Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 13
  14. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC s.fuel = 10; // Lỗi, không thể truy cập “fuel” Ví dụ này rất giống với cái trước, nhưng lần này nhiên liệu được protected và tàu chiến có thể truy cập nó được. Các Thành phần Tĩnh (Static Member) Cho đến giờ, tất cả các thành phần bạn thấy trong lớp đều là thành phần cá biệt (instance member). Một thành phần cá biệt là một phần của lớp chỉ tồn tại với duy nhất một cá thể của lớp đó. Nếu bạn có hai tàu vũ trụ, và các tàu vũ trụ đều có một số nguyên biễu thị nhiên liệu, thì bạn sẽ có hai số nguyên, một số cho mỗi tàu. Mặt khác, bạn cũng có thể có thành phần tĩnh. Một thành phần tĩnh là một mẩu dữ liệu (hay một hàm) được chia sẻ với tất cả các cá thể trong lớp, thay vì lặp lại với từng cá thể. Hình 3.5 là một ví dụ. Dữ liệu tĩnh Spaceship.count Định nghĩa Spaceship static int count Dữ liệu cá biệt int fuel Cá thể Cá thể Cá thể int cargo Spaceship Spaceship Spaceship fuel fuel fuel cargo cargo cargo Hình 3. 5 Sự khác nhau giữa dữ tĩnh và dữ liệu cá biệt Dữ liệu Tĩnh Hãy nhìn đoạn mã sau đây: class Spaceship { public static int count; public int fuel; public int cargo; }; Đoạn mã này tạo một lớp định nghĩa cho một tàu vũ trụ, với mỗi tàu sẽ có nhiên liệu và hàng hóa, và định nghĩa lớp sẽ theo dõi một số nguyên tên là count. Bạn có thể trut cập số nguyên này mọi lúc gọi dòng mã này (hay bất cứ gì tương tự): Spaceship.count = 10; Bạn không cần có bất cứ cá thể tàu vũ trụ nào để sử dụng biến này; nó luôn luôn tồn tại. Nó không thuộc về tàu vũ trụ nào cả; bất cứ ai đầu có thể sử dụng nó. Tĩnh là một cách dễ dàng để có một biến Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 14
  15. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC cùng chức năng với biến toàn cục như trong C hay C++; hơn nữa nó gọn gàng hơn, theo quan điểm thiết kế. Hàm Tĩnh Hàm cũng có thể là tĩnh. Về cơ bản, một hàm tĩnh có thể được gọi mà không cần một cá thể để hoạt động. Ví dụ: class Spaceship { public static void FunctionA() { // Làm gì đó } }; Và bây giờ bạn có thể gọi hàm này bằng cách gọi nó như sau: Spaceship.FunctionA(); Bạn không cần bất cứ cá thể tàu vũ trụ nào để gọi hàm này. Chú ý Hàm tĩnh không thể hoạt động với dữ liệu cá biệt mà không có một cá thể thực để hoạt động. Nó có vẻ rõ ràng khi bạn nghĩ về nó, nhưng một số người không hiểu điều này ngay lập tức. Thuộc tính (Property) Tôi đã nói với bạn trước đó rằng giấu dữ liệu là một điều tốt. Trong thực tế, tôi đã sử dụng một số thủ thuật tồi tệ để tạo ra những ví dụ dễ hiểu trong cuốn sách này. Nói chung, bạn nên không bao giờ để một dữ liệu public trong lớp của bạn. Sự cám dỗ khi cho phép truy cập trực tiếp đến dữ liệu của bạn đôi khi rất mạnh mẽ, và sớm hay muộn bạn sẽ rơi vào những vấn đề tôi đã kể cho bạn trước đó. Và khi bạn bắt đầu có lỗi từ không đâu, tôi không muốn bạn đến than thở với tôi. Hàm truy cập (Accessors) Nói chung, trong quá khứ, phương pháp ưa thích để làm cho dữ liệu có thể truy cập được mà không cần no public là sử dụng hàm truy cập. Để làm vậy, bạn tạo hai hàm cho mỗi biến: một để lấy giá trị, và một để đặt giá trị. Như thế này: class Spaceship { protected int fuel; public int GetFuel() { return fuel; } public void SetFuel( int p_fuel ) { fuel = p_fuel; } }; Và bạn có thể truy cập nhiên liệu như thế này: Spaceship s = new Spaceship(); s.SetFuel( 10 ); int f = s.GetFuel(); Ghi chú Phương thức này được xem xét là anh toàn bởi vì bạn có thể thay đổi khi nhiên liệu được truy cập sau đó mà không cần làm rối tung bất kỳ đoạn mã nào khác. Ví dụ, nếu bạn đề nghị một ngày mà bạn không muốn mọi người có khả năng đặt số nhiên liệu dưới 0, bạn có thể thay đổi hàm SetFuel để nó kiểm tra một cách tự động. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 15
  16. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Điều này, theo quan điểm kỹ thuật, thì rất an toàn. Nhưng theo quan điểm “tôi đã đánh máy hàng giờ và tôi chỉ muốn về nhà”, thì giải pháp này thật vớ vẩn. Có quá nhiều mã thêm vào để làm cho chương trình của bạn “an toàn hơn”. Giải pháp của C#: Thuộc tính Để sửa vấn đề “đánh máy quá nhiều” tồn tại trong những ngôn ngữ cũ, C# giới thiệu một định nghĩa mới gọi là thuộc tính. Một thuộc tính cho phép bạn dử dụng ít mã hơn để làm cho biến của bạn có thể truy cập hơn, khi vẫn giữ sự bảo vệ của một hàm truy cập. Đây là một ví dụ: class Spaceship { protected int fuel; public int Fuel { get { return fuel; } set { fuel = value; } } }; Đoạn mã đậm là vùng thuộc tính. Về cơ bản tôi đã tạo một thuộc tính có tên là Fuel (chữ F hoa), mà nó sẽ hành động giống hệt như một mẩu dữ liệu ở ngoài: Spaceship s = new Spaceship(); s.Fuel = 10; int f = s.Fuel; Vậy cái gì thực sự đã diễn ra đằng sau bức màn, đó là các hàm thuộc tính get và set được gọi, và thực thi mã của chúng. Bạn có thể làm bất cứ thứ gì bạn muốn trong cà hai hàm thuộc tính, nhưng bạn sẽ thường làm nhiều điều hơn trong hàm set. Ví dụ, bạn có thể chắc chắn rằng không ai có thể đặt số nhiên liệu của tàu vũ trụ dưới 0 khi thay đổi hàm Fuel.set như thế này: public int Fuel { get { return fuel; } set { if( value < 0 ) fuel = 0; else fuel = value; } } Và bây giờ, nếu bạn thực hiện mã này: s.Fuel = -10; thì hàm set sẽ tự động đặt nhiên liệu xuống 0 thay vì -10. Chú ý Thuộc tính không có bộ phát hiện đệ quy vô hạn, nó đôi khi gây ra phiền toái. Ví dụ, nếu bạn vô tình đánh Fuel = value thay vì fuel = value bên trong hàm set, máy vi tính sẽ tự động gọi hàm set lần nữa, và sẽ tiếp tục như thế cho đến khi chương trình bị treo. Hãy cẩn thận với điều đó. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 16
  17. Câu lạc bộ Khoa học - THPT Chuyên Lê Hồng Phong TPHCM LHPSC Trích xuất (Enumeration) Một đặc điểm nữa của C# (và của những ngôn ngữ khác) sẽ làm cho đời bạn đơn giản hơn là định nghĩa của kiểu trích xuất (enumeration). Đã bao nhiêu lần khi bạn viết một chương trình mà bạn có một số kiểu dữ liệu không phải là số, nhưng cũng không phải là một kiểu dữ liệu tùy chỉnh? Hãy tưởng tượng thế này: Bạn đang làm một hệ thống đơn giản mà trong đó mỗi tàu vũ trụ trong trò chơi của bạn sẽ biết nó đang ở trạng thái nào – như di chuyển vòng quanh, dừng, hau chiến đấu với một số kẻ thú. Bạn có thể đề nghị sử dụng số nguyên, như thế này: class Spaceship { int state; // Mã khác ở đây }; OK, vậy trạng thái 0 là di chuyển, 1 là dừng lại, và 2 là chiến đấu. Nghe khá hợp lý, đúng không? Không! Điều này rất tồi tệ. Đừng bao giờ làm thế này! Bạn sẽ không thể nhớ được số nào có nghĩa gì. Giải pháp của C# là trích xuất. Một kiểu trích xuất đóng gói một nhóm các tên vào một kiểu mà nó có thể dễ dàng tham chiếu và đọc hiểu. Đây là một kiểu trích xuất biểu diễn trạng thái của tàu: enum SpaceshipState { moving, stopped, battle }; Mã này tạo một kiểu trích xuất có tên là SpaceshipState, mà có tổng cộng ba giá trị khác nhau: moving, stopped và battle. Bạn có thể sử dụng nó như thế này: SpaceshipState s; s = SpaceshipState.moving; if( s == SpaceshipState.battle ) // Mã ở đây // và tiếp tục Trích xuất thực ra là các số nguyên; bạn không cần phải nghĩ về chúng như thế, nhưng bạn có thể nếu bạn muốn. Thông thường, trích xuất đầu tiên được gán giá trị 0, và chúng tuần tự tăng 1. int i; i = (int)SpaceshipState.moving; // 0 i = (int)SpaceshipState.stopped; // 1 i = (int)SpaceshipState.battle; // 2 Nếu bạn không thích những giá trị mặc định này, bạn có thể thay đổi chúng, như thế này: enum SpaceshipState { moving = 10, stopped = 12, battle // tự động là 13 }; Vậy bây giờ moving đã có giá trị 10, stopped đã có giá trị 12, và battle, không được định nghĩa rõ ràng, sẽ lấy giá trị của trích xuất ngay trước và cộng thêm một, khiến nó là 13. Lập trình C# - Phần 3: Giới thiệu về Lớp Trang 17

CÓ THỂ BẠN MUỐN DOWNLOAD

Đồng bộ tài khoản