C# cơ bản .NET Core
DẪN NHẬP VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG

Phần này bắt đầu nghiên cứu, triển khai các vấn đề kỹ thuật liên quan đến Lập trình hướng đối tượng.

Lập trình hướng đối tượng (Object-oriented programming - OOP), là kỹ thuật lập trình mà điều cốt yếu cần trừu tượng hóa các vấn đề thành các đối tượng (đối tượng có dữ liệu và các ứng xử). Kỹ thuật OOP có 4 tính chất

  • Tính trừu tượng (abstraction) mô tả một cách tổng quát hóa (tập trung vào thông tin cần thiết) mà không chi tiết thông tin về các đối tượng, không gắn cứng với một đối tượng cụ thể cần mô tả (triển khai với interface, abstract)
  • Tính đóng gói (encapsulation) dữ liệu đối tượng cố gắng như nằm trong một hộp đen, các thành phần khác bên ngoài đối tượng không trực tiếp tác động đến dữ liệu (bên ngoài truy cập, tác động thông qua các phương thức public cho phép, qua setter, getter ...)
  • Tính đa hình (polymorphism) đối tượng ứng xử khác nhau tùy trường hợp cụ thể
  • Tính kế thừa (inheritance) đặc tính của lớp được kế thừa từ một lớp khác

Khái niệm chung về lớp trong C#

Trong lập trình hướng đối tượng, lớp (class) là một kiểu dữ liệu tham chiếu nó định nghĩa một tập hợp các biến (trường dữ liệu, thuộc tính) và phương thức (gọi chúng là các member - thành viên lớp).

Từ lớp đó sinh ra các đối tượng (object), các đối tượng này còn gọi là bản triển khai của lớp (instance of a class), mỗi đối tượng có giá trị dữ liệu cụ thể (lưu trong thành viên biến, thuộc tính). Các phương thức (method) - định nghĩa ra các ứng xử của đối tượng - dựa theo dữ liệu của chúng.

Ví dụ: Một lớp mô tả các loại vũ khí, đặt tên là Vukhi, thì bên trong nó có thể định nghĩa các thành viên dữ liệu (biến) như: tên vũ khí, độ sát thương, tầm ảnh hưởng ... Cũng có thể định nghĩa các thành viên hàm (phương thức) để mô tả ứng xử của nó như: Ten(), TanCong() ...

Khi có định nghĩa lớp VuKhi nghĩa là bạn có một kiểu dữ liệu tham chiếu mới, bạn có thể tạo ra đối tượng cụ thể của nó mà mỗi đối tượng có những dữ liệu khác nhau như sung_luc, sung_may ...

Khai báo lớp, sử dụng lớp

Cú pháp, ví dụ khai báo lớp

Khi giải quyết các vấn đề thực tiễn, các vấn đề cần giải quyết bạn cần tìm cách trừu tượng hóa nó thành vấn đề tổng quát, như nó có các đặc tính gì, ứng xử của nó ra sao ... Từ đó mới có thể định nghĩa ra lớp để hiện thực hóa vấn đề trừu tượng được.

Cú pháp cơ bản như sau:

<Access Modifiers> class Class_Name {
    // khai báo các thành viên dữ  liệu (thuộc tính, biến trường dữ liệu)
     // khai báo các thành viên  hàm (phương thức)
}

Trong đó Access Modifiers áp dụng khai báo cho lớp có thể là: public (không giới hạn truy cập) hoặc internal (mặc định nếu không khai báo, giới hạn truy cập trong cùng assembly - chương trình). Nếu lớp con khai báo lồng trong một lớp khác còn có thể sử dụng private (chỉ truy cập được từ lớp chứa nó)

Khai báo và khởi tạo thành viên dữ liệu (biến, trường dữ liếu) giống với khai báo biến thông thường, nhưng có thêm Access Modifiers để quy định cấp độ truy cập. Đối với thành viên lớp (biến, thuộc tính, phương thức) thì có thể áp dụng các access modifiers sau:

  • public : không giới hạn phạm vi truy cập
  • protected : chỉ truy cập trong nội bộ lớp hay các lớp kế thừa
  • private : (mặc định) chỉ truy cập được từ các thành viên của lớp chứa nó
  • internal : chỉ truy cập được trong cùng assembly (dll, exe)
  • protected internal: truy cập được khi cùng assembly hoặc lớp kế thừa
  • private protected: truy cập từ lớp chứa nó, lớp kế thừa nhưng phải cùng assembly

Trở lại ví dụ trên, định nghĩa lớp VuKhi

// File VuKhi.cs
using System;

namespace CS007_Class
{
    public class VuKhi
    {
        /// Tên của vũ khí: Súng Lục, Súng Trường, Dao ...
        public string name = "Tên Vũ Khí";

        /// Độ sát thương 10 cấp độ
        int dosatthuong = 0;
        
        /// Phương thức khởi tạo (được gọi khi toán tử new tạo đối tượng)
        /// tên phương thức trùng tên lớp, trường hợp này không tham số
        public VuKhi()
        {
            this.dosatthuong = 1;
        }
        
        /// Phương thức khởi tạo (được gọi khi toán tử new tạo đối tượng)
        /// tên phương thức trùng tên lớp, trường hợp này có tham số
        public VuKhi(string name, int dosatthuong)
        {
            this.name  = name;
            SetDoSatThuong(dosatthuong);
        }
        
        /// Hàm này thiết lập  độ sát thương
        public void SetDoSatThuong(int mucdo)
        {
            this.dosatthuong = mucdo;
        }
        
        // In ra: Tên vu khí: * * * * * * * * (bằng độ sát thương)
        public void TanCong()
        {
            Console.Write(name + ": \t");
            for (int i = 0; i < dosatthuong; i++)
            {
                Console.Write(" * ");
            }
            Console.WriteLine();
        }
    }
}

Lớp VuKhi trên mô tả chung về loại vũ khí, nó có tên lưu ở trường dữ liệu name, có độ sát thương lưu ở trường (biến) dosatthuong, nó có các phương thức SetDoSatThuong để thiết lập độ sát thước, TanCong() trừu tượng hóa khi vú khí đó tấn công.

Tạo và sử dụng đối tượng

Giờ ta tiến hành tạo ra các đối tượng cụ thể, áp dụng VuKhi. Như trên đã nói, class là một kiểu dữ liệu, nên để sử dụng nó có thể khai báo biến với kiểu dữ liệu là tên lớp do bạn định nghĩa. Để tạo ra đối tượng lớp thì dùng từ khóa new với cú pháp new ClassName();

// Khai báo và khởi tạo đối tượng luôn
var ob1 = new ClassName();

// Khai báo, sau đó khởi tạo
ClassName ob2;
ob2 = new ClassName();

Toán tử .

Sau khi đối tượng lớp (object) được tạo, bạn có thể truy cập đến các thuộc tính, trường dữ liệu và phương thức của đối tượng đó bằng ký hiệu . theo quy tắc object.tên_thuộc_tính hay object.tên_phương_thức

Áp dụng:

static void Main(string[] args)
{
    var sungluc = new VuKhi();              // Khai báo và khởi tạo
    sungluc.name = "SÚNG LỤC";              // Truy cập và gán thuộc tính
    sungluc.SetDoSatThuong(5);             // Truy cập (gọi) phương thức

    VuKhi sungtruong = new VuKhi();
    sungtruong.name = "SÚNG TRƯỜNG";
    sungtruong.SetDoSatThuong(20);

    sungluc.TanCong();                      // Gọi phương thức
    sungtruong.TanCong();                   // Gọi phương thức

    //Kết quả chạy
    SÚNG LỤC:        *  *  *  *  *
    SÚNG TRƯỜNG:     *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
                
}

Từ khóa this

Từ khóa this dùng trong các phương thức của lớp, nó tham chiếu đến đối tượng hiện tại sinh ra từ lớp. Sử dụng this để tường minh, tránh sự không rõ ràng khi truy cập thuộc tính, phương thức hoặc để lấy đối tượng lớp làm tham số cho các thành phần khác ...

Ví dụ, hàm SetDoSatThuong, bạn có thể viết:

public void SetDoSatThuong(int dosatthuong)
{
    this.dosatthuong = dosatthuong;
}

Nếu viết thiếu this thì là dosatthuong = dosatthuong, làm cho khó hiểu không biết là gán dosatthuong từ tham số hàm cho dosatthuong là dữ liệu của lớp. Các trường hợp khác, không gây ra sự bối rối kiểu này thì có thể bỏ this (tuy nhiên nên sử dụng this ngay kể cả khi bỏ được nó để giúp ích quá trình bảo trì sau này)

Phương thức khởi tạo - Constructor

Phương thức khởi tạo là phương thức của lớp, nó được thi hành ngay khi đối tượng được tạo (bởi toán tử new), phương thức khởi tạo có tên trùng với tên của lớp, không có kiểu trả về, bạn có thể tạo nhiều phương thức khởi tạo - các phương thức này đều cùng tên với tên lớp nhưng tham số khác nhau. Lúc này khi khởi tạo đối tượng với toán tử new tùy tham số khởi tạo mà nó sẽ gọi phương thức khởi tạo tương ứng.

Ví dụ trên, lớp VuKhi có hai phương thức khởi tạo:

public class VuKhi
{
    /...
    public VuKhi()
    {
        this.dosatthuong = 1;
    }

    public VuKhi(string name, int dosatthuong)
    {
        this.name  = name;
        SetDoSatThuong(dosatthuong);
    }
    //...
}
    

Lúc này có thể sử như sau:

// Khởi tạo đối tượng, hàm tạo VuKhi() được gọi
var sungluc = new VuKhi();
sungluc.name = "SÚNG LỤC";
sungluc.SetDoSatThuong(5);

// Khởi tạo đối tượng, hàm tạo VuKhi(name, dosatthuong) được gọi
VuKhi sungtruong = new VuKhi(name: "SÚNG TRƯỜNG", dosatthuong: 20);

Kết quả tương tự như ví dụ trên, việc sử dụng hàm khởi tạo đảm bảo dữ liệu của đối tượng bắt buộc phải khởi tạo ngay khi đối tượng đó được tạo - tránh việc sử dụng đối tượng mà dữ liệu không chính xác.

Quá tải (Overloading) phương thức

Kỹ thuật quá tải phương thức (Method Overloading) là cách thức triển khai khái niệm tính đa hình của lập trình hướng đối tượng. Quá tải phương thức là các phương thức có cùng tên nhưng tham số khác nhau (hàm có thể trả về kiểu dữ liệu khác nhau)

Tính đa hình (polymorphism) là cách ứng xử của đối tượng - ứng xử này là khác nhau tùy thuộc vào tình huống cụ thể.

Ví dụ lớp Console của NET CORE (mã nguồn Console.cs) nó quá chồng một loạt phương thức ví dụ:

public static void WriteLine();
public static void WriteLine(bool value);
public static void WriteLine(decimal value);
public static void WriteLine(int value);
...

Điều này giúp cho bạn khi bạn gọi Console.Writeline(a), tùy thuộc vào kiểu dữ liệu của a mà một hàm WriteLine tương ứng được thi hành.

Ví dụ:

public class OverloadingExample {

    public static int Sum(int a, int b)
    {
        return a + b;
    }

    public static double Sum(double a, double b)
    {
        return a + b;
    }
}

Lớp trên có hàm Sum quá tải, tùy thuộc vào kiểu tham số mà hàm Sum cụ thể được gọi.

double a = 1;
double b = 2;
var c  = OverloadingExample.Sum(a, b); // c = 3 có kiểu double
int a = 1;
int b = 2;
var c  = OverloadingExample.Sum(a, b); // c = 3 nhưng có kiểu int

Chú ý: Khai báo hai hàm cùng tên, giống nhau hoàn toàn về tham số chỉ khác kiểu trả về sẽ gây lỗi.

Tìm hiểu tính đóng gói lập trình hướng đối tượng

Tính đóng gói mục đích hạn chế tối đa việc can thiệp trực tiếp vào dữ liệu, hoặc thi hành các tác vụ nội bổ của đối tượng. Nói cách khác, một đối tượng là hộp đen đối với các thành phần bên ngoài, nó chỉ cho phép bên ngoài tương tác với nó ở một số phương thức, thuộc tính, trường dữ liệu nhất định - hạn chế.

C# triển khai tính đóng gói này chính là sử dụng các Access Modifiers: public private protected internal khi khai báo lớp, phương thức, thuộc tính, trường dữ liệu (biến).

  • public thành viên có thể truy cập được bởi code bât kỳ đâu, ngoài đối tượng, không có hạn chế truy cập nào.
  • private phương thức, thuộc tính, trường khai báo với private chỉ có thể truy cập, gọi bởi các dòng code cùng lớp.
  • protected phương thức, thuộc tính, trường chỉ có thể truy cập, gọi bởi các dòng code cùng lớp hoặc các lớp kế thừa nó.
  • internal truy cập được bởi code ở cùng assembly (file).
  • protected internal truy cập được từ code assembly, hoặc lớp kế thừa nó ở assembly khác.
  • private protected truy cập được code khi cùng assembly trong cùng lớp, hoặc các lớp kế thừa nó.

Khi không chỉ rõ Modify thì mặc định là private cho phương thức, thuộc tính, trường. Assembly là file exe, dll

Ví dụ:

class Student
{
    private string Name;
}

Khi sử dụng

var s = new Student();
s.Name = "ABC";

Biên dịch sẽ lỗi error CS0122: 'Student.Name' is inaccessible due to its protection level. Vì trường Nameprivate không thể truy cập bằng code bên ngoài lớp như trên. Nhưng nếu thay bằng public thì không lỗi.

Khi lập trình cố gắng tối đa ẩn thông tin ra bên ngoài lớp càng nhiều càng tốt để đảm bảo tính đóng gói của kỹ thuật lập trình OOP, nó giúp cho code dễ bảo trì và giám sát lỗi.

Thuộc tính trong lớp

Trường dữ liệu của lớp

Trường dữ liệu - khai báo như biến trong lớp, nó là thành viên của lớp, nó là biến. Trường dữ liệu có thể sử dụng bởi các phương thức trong lớp, hoặc nếu là public nó có thể truy cập từ bên ngoài, nhưng cách hay hơn để đảm bảo tính đóng gói khi cần truy cập thuộc tính hãy sử dụng phương thức, còn bản thân thuộc tính là private. Chúng ta đã sử dụng các trường dữ liệu ở những ví dụ trên.

Thuộc tính, bộ truy cập accessor setter/getter

Ngoài cách sử dụng trường dữ liệu, khai báo như biến ở phần trước, khai báo THUỘC TÍNH tương tự nhưng nó có cơ chế accessor (bộ truy cập), một cơ chế hết sức linh hoạt khi bạn đọc / ghi dữ liệu vào thuộc tính. Hãy tìm hiểu qua một ví dụ sau:

class Student
{
    private string name; // đây là trường dữ liệu
}

Lớp này có một trường dữ liệu private là name. Giờ ta sẽ khai báo một thuộc tính có tên Name với modify là public, thuộc tính này khi đọc sẽ thi hành một đoạn code gọi là get, khi ghi (gán) dữ liệu nó thi hành đoạn code gọi là set, thuộc tính Name sẽ phối hợp cùng trường dữ liệu name

class Student
{
    private string name;     // Đây là trường dữ liệu

    public string Name       // Đây là thuộc tính
    {
        // set thi hành khi gán, write
        // dữ liệu gán là value
        set
        {
            Console.WriteLine("Ghi dữ liệu <--" + value);
            name = value;
        }

        //get thi hành ghi đọc dữ liệu
        get {
            return "Tên là: " + name;
        }
    }
}

Khi thực hiện

var s = new Student();
s.Name = "XYZ";                                             // set thi hành
// In ra: Ghi dữ liệu <--XYZ
// Và trường name giờ bằng XYZ

Console.WriteLine(s.Name);                                  // get được thi hành
// In ra: Tên là: XYZ

Thuộc tính accessor có thể khai báo thiếu set hoặc get, nếu thiếu set nó trở thành loại chỉ đọc (readonly). Sử dụng set rất tiện lợi cho thao tác kiểm tra tính hợp lệ của dữ liệu khi gán, hoặc tự động thực hiện một số tác vụ mỗi khi dữ liệu được gán.

Bạn có thể khai báo một thuộc tính tự động, nó hoạt động giống như trường dữ liệu.

public string Name {set; get;}

Bạn lưu ý khai báo như trên nó là thuộc tính, bạn vẫn có thể khai báo như là trường:

public string Name;

Tuy nhiên khi không là thuộc tính, thì sẽ thiếu đi một số tính năng ứng dụng cao cấp sau này mà C# hỗ trợ.

Source code: CS007_Class (Git), hoặc tải ex007


Đăng ký nhận bài viết mới