Trong bài này chúng ta sẽ tìm hiểu:

  • Toán tử (operator)
  • Quá tải toán tử (overloaded operator)
  • Các toán tử đối xứng (symmetric operators)
  • Các toán tử chuyển đổi (conversion operators)
  • Các toán tử được quá tải và không được quá tải

Toán tử (operator)

Trong bài trước chúng ta đã làm quen với các toán tử chuẩn cung cấp sẵn (build-in operators) như toán tử số học, toán tử so sánh, toán tử luận lý,…Toán tử có một số đặc điểm:

– Toán tử được dùng để kết hợp các toán hạng (operands) trong các biểu thức.

– Mỗi toán tử có một độ ưu tiên (precedence) (ví dụ trong một biểu thức số học, thực hiện *, / trước, +,-sau).

– Mỗi toán tử có tính chất kết hợp (associativity).

– Toán tử một ngôi (unary operator) là toán tử chỉ có một toán hạng (ví dụ toán tử ++); toán tử hai ngôi (binary operator) là toán tử có hai toán hạng (ví dụ toán tử +).

Toán tử có một số ràng buộc sau:

– Không thể thay đổi độ ưu tiên hay tính kết hợp của một toán tử.

– Không thể thay đổi số lượng toán hạng của một toán tử.

– Không thể tạo ra biểu tượng toán tử mới.

– Không thể thay đổi ý nghĩa của toán tử khi áp dụng với các kiểu có sẵn (build-in types).

– Một số toán tử không thể quá tải (tức định nghĩa lại).

Quá tải toán tử

Quá tải toán tử là định nghĩa lại chức năng của toán tử. Cú pháp  quá tải toán tử giống khai báo một phương thức, ngoại trừ tên phương thức được thay bằng từ khoá operator, theo sau là toán tử cần quá tải. Cũng giống như phương thức, quá tải toán tử cũng có danh sách tham số và kiểu dữ liệu trả về. Ví dụ sau chúng ta sẽ quá tải toán tử +, thay vì kết hợp hai toán hạng số học sẽ kết hợp hai toán hạng là đối tượng kiểu Hour, như sau:


struct Hour

{

public Hour(int initialValue)

{

this.value = initialValue;

}

public static Hour operator +(Hour lhs, Hour rhs)

{

return new Hour(lhs.value + rhs.value);

}

...

private int value;

}

Một số lưu ý từ ví dụ trên:

– Các toán tử phải có phạm vi là public

– Các toán tử phải là static

– Các toán tử không có tính đa hình và không thể kết hợp với các từ khoá virtual, override, abstract, hay sealed.

Tạo các toán tử đối xứng (symmetric operators)

Trong ví dụ trên, chúng ta đã quá tải toán tử + cho phép cộng hai thể hiện kiểu Hour. Tuy nhiên, trong phương thức khởi tạo của struct Hour có trả về giá trị kiểu int. Điều này có nghĩa rằng, chúng ta có thể kết hợp một giá trị kiểu Hour và một giá trị kiểu int – tuy nhiên, đầu tiên chúng ta phải dùng phương thức khởi tạo để chuyển kiểu int thành Hour. Xem xét ví dụ sau:


// thể hiện kiểu Hour

Hour a = ...;

// biến kiểu int

int b = ...;

// kết hợp a và b, chú ý dùng toán tử new để gọi phương thức khởi

// tạo của Hour chuyển int thành Hour

Hour sum = a + new Hour(b);

Cách kết hợp trên là hợp lệ như sẽ không tự nhiên. Để tự nhiên hơn, đoạn mã phải như thế này:

// thể hiện kiểu Hour

Hour a = ...;

// biến kiểu int

int b = ...;

Hour sum = a + b;

Với đoạn mã trên, chúng ta để ý rằng, toán hạng a (kiểu Hour) bên trái và toán hạng b (kiểu int) bên phải toán tử +. Chúng ta cần quá tải toán tử + như sau:


struct Hour

{

public Hour(int initialValue)

{

this.value = initialValue;

}

...

public static Hour operator +(Hour lhs, Hour rhs)

{

return new Hour(lhs.value + rhs.value);

}

public static Hour operator +(Hour lhs, int rhs)

{

return lhs + new Hour(rhs);

}

...

private int value;

}

Như vậy, lúc này chúng ta có hai phiên bản quá tải toán tử +. Chúng ta có thể kết hợp hai toán hạng kiểu Hour, một toán hạng kiểu Hour (bên trái) và một toán hạng kiểu int (bên phải) nhưng chúng ta chưa thể kết hợp toán hạng kiểu int (bên trái) và toán hạng kiểu Hour (bên phải) như ví dụ sau:


int a = ...;

Hour b = ...;

Hour sum = a + b; // biên dịch sẽ bị lỗi

Lúc này, chúng ta nói rằng, toán tử + chưa có tính đối xứng. Chúng ta cần thêm một phiên bản quá tải toán tử + như sau:


struct Hour

{

public Hour(int initialValue)

{

this.value = initialValue;

}

// phiên bản thứ nhất

public static Hour operator +(Hour lhs, Hour rhs)

{

return new Hour(lhs.value + rhs.value);

}

// phiên bản thứ hai

public static Hour operator +(Hour lhs, int rhs)

{

return lhs + new Hour(rhs);

}

// phiên bản thứ ba

public static Hour operator +(int lhs, Hour rhs)

{

return new Hour(lhs) + rhs;

}

...

private int value;

}

Các toán tử chuyển đổi (conversion operators)

Thỉnh thoảng chúng ta cần chuyển đổi một biểu thức có giá trị trả về từ kiểu này sang kiểu khác. Xét ví dụ sau:


class Example

{

public static void MyDoubleMethod(double parameter)

{

...

}

}

Phương thức MyDoubleMethod chứa một tham số kiểu double. Mặc định, đối số chuyển cho phương thức này có kiểu double nhưng trong thực tế chúng ta có thể chuyển một đối số có kiểu khác, ví dụ kiểu int. Đoạn mã sau là hợp lệ:


int a = 42;

Example.MyDoubleMethod(a);

Trình biên dịch sẽ tự động chuyển đối số từ kiểu int sang kiểu double. Tuy nhiên, quá trình chuyển đổi ngầm định này không phải lúc nào cũng thành công, ví dụ sẽ thất bại nếu chuyển từ kiểu int sang double như sau:


class Example

{

public static void MyDoubleMethod(double parameter)

{

...

}

}

....

// phát sinh lỗi

Example.MyDoubleMethod(42.0);

Chúng ta có thể ép kiểu như sau:

Example.MyDoubleMethod((int)42.0);

Tuy nhiên, khi ép kiểu như thế này, có thể dẫn tới kết quả không như mong đợi do ép một giá trị từ double (64 bit) sang kiểu  int (32 bit) và có thể phát sinh ngoại lệ OverfowException. C# cho phép chúng ta định nghĩa các toán tử kiểm soát quá trình chuyển đổi các giá trị từ kiểu này sang kiểu khác một cách an toàn. Chuyển kiểu có thể là tường minh bằng cách dùng từ khoá explicit hay ngầm định bằng cách dùng từ khoá implicit theo sau là từ khoá operator. Cú pháp trông như sau:


public static implicit|explicit operator TypeTo(TypeFrom parameter)

{

….

}

TypeTo là kiểu chúng ta muốn chuyển đến, TypeFrom là kiểu chúng ta cần chuyển

Ví dụ định nghĩa toán tử chuyển cho phép một đối tượng kiểu int được chuyển sang kiểu Hour:


struct Hour

{

...

public static implicit operator Hour (int from)

{

return new Hour (from);

}

private int value;

}

Với việc sử dụng toán tử chuyển, chúng ta có thể tạo ra các toán tử có tính đối xứng dễ dàng. Cấu trúc Hour, thay vì tạo ra tới 3 phiên bản toán tử +, có thể định nghĩa lại như sau:


struct Hour

{

public Hour(int initialValue)

{

this.value = initialValue;

}

public static Hour operator +(Hour lhs, Hour rhs)

{

return new Hour(lhs.value + rhs.value);

}

// toán tử chuyển cho phép chuyển tử int sang Hour

public static implicit operator Hour (int from)

{

return new Hour (from);

}

...

private int value;

}

Lúc này chúng ta có thể kết hợp một toán hạng kiểu int và một toán hạng kiểu Hour một cách dễ dàng mà không cần quan tâm thứ tự trái hay phải. Ví dụ


void Example(Hour a, int b)

{

Hour eg1 = a + b; // b được chuyển đến Hour

Hour eg2 = b + a; // b được chuyển đến Hour

}

Các toán tử được quá tải và không được quá tải

Trong các đặc điểm của toán tử có một đặc điểm là không phải toán tử nào cũng có thể được quá tải. Sau đây là danh sách các toán tử có thể được quá tải và không được quá tải trong C#:

Toán tử Quá tải
+, -, !, ~, ++, — Có thể được quá tải
+, -, *, /, % Có thể được quá tải
==, !=, <, >, <=, >= Có thể được quá tải
&&, || Không thể được quá tải
+=, -=, *=, /=, %= Không thể được quá tải
=, ., ?:, ->, new, is, sizeof, typeof Không thể được quá tải

Học C# và WPF >