Chương 12: Câu lệnh và luồng thực thi
Tác giả: Sưu tầm
Khái quát
Các mục sau đây trình bày chi tiết các câu lệnh khác nhau có sẵn trong ngôn ngữ C#.
Câu lệnh chọn lựa
Các câu lệnh chọn lựa được sử dụng để thực hiện các thao tác dựa trên giá trị của một biểu thức.
If
Câu lệnh if trong C# đòi hỏi điều kiện bên trong câu lệnh if định giá một biểu thức kiểu bool. Ngược lại, như sau là bất hợp lệ:
// error
using System;
class Test {
public static void Main() {
int value;
if (value) // invalid
System.Console.WriteLine(“true”);
if (value == 0) // must use this
System.Console.WriteLine(“true”);
}
}
Switch
Câu lệnh switch thường có lỗi dễ xảy ra; là quá dễ để tình cờ bỏ sót một câu lệnh break ở cuối một case, hay không chú ý có một sự không dẫn đến kết quả nào khi đọc mã. C# đã giải quyết vấn đề này bằng cách đòi hỏi hoặc một câu lệnh break ở cuối mỗi khối case, hoặc một câu lệnh goto sang một nhãn case khác trong câu lệnh switch.
using System;
class Test {
public void Process(int i) {
switch (i) {
case 1:
case 2:
// code here handles both 1 and 2
Console.WriteLine(“Low Number”);
break;
case 3:
Console.WriteLine(“3”);
goto case 4;
case 4:
Console.WriteLine(“Middle Number”);
break;
default:
Console.WriteLine(“Default Number”);
}
}
}
C# cũng cho phép câu lệnh switch được sử dụng với các biến kiểu chuỗi:
using System;
class Test {
public void Process(string htmlTag) {
switch (htmlTag) {
case “P”:
Console.WriteLine(“Paragraph start”);
break;
case “DIV”:
Console.WriteLine(“Division”);
break;
case “FORM”:
Console.WriteLine(“Form Tag”);
break;
default:
Console.WriteLine(“Unrecognized tag”);
break;
}
}
}
Không chỉ đơn giản hơn khi viết một câu lệnh switch thay vì một loạt các câu lệnh if, mà nó còn hiệu quả hơn, khi trình biên dịch sử dụng một thuật toán hiệu quả để thực hiện việc so sánh.
Đối với một số lượng nhỏ đầu vào trong switch, trình biên dịch sử dụng một đặc tính trong .NET Runtime là giam giữ chuỗi. Thời gian chạy duy trì một bảng tên trong chứa tất cả các chuỗi hằng để tất cả các biến cố của chuỗi đó trong một chương trình đơn giãn sẽ có cùng một đối tượng. Trong câu lệnh switch, trình biên dịch tìm một chuỗi switch trong bảng thời gian chạy. Nếu không có, chuỗi không thể là một trong các case, nên case default được gọi. Nếu nó tìm thấy, một tìm kiếm tuần tự được thực hiện trên các case bên trong để tìm một sự so khớp.
Đối với một số lượng lớn các đầu vào trong case, trình biên dịch phát sinh một hàm băm và bảng băm và sử dụng bảng băm để tìm kiếm hiệu quả một chuỗi.
Câu lệnh lặp
Câu lệnh lặp được sử dụng để thực hiện các thao tác khi một điều kiện nhất định là đúng.
While
Câu lệnh lặp while như mong đợi: khi điều khiện đúng, vòng lặp được thực hiện. Như câu lệnh if, câu lệnh while đòi hỏi một điều kiện kiểu Boolean:
using System;
class Test {
public static void Main() {
int n = 0;
while (n < 10) {
Console.WriteLine(“Number is {0}”, n);
n++;
}
}
}
Câu lệnh break có thể được sử dụng để thoát khỏi vòng lặp while, và câu lệnh continue có thể được sử dụng để bỏ qua lần lặp hiện thời, và sau đó tiếp tục với lần lặp tiếp theo.
using System;
class Test {
public static void Main() {
int n = 0;
while (n < 10) {
if (n == 3) {
n++;
continue;
}
if (n == 8)
break;
Console.WriteLine(“Number is {0}”, n);
n++;
}
}
}
Đoạn mã này cho ra kết quả sau:
0
1
2
3
4
5
6
7
Do
Câu lệnh do thực hiện lặp các chức năng giống như vòng lặp while, ngoại trừ điều kiện là được kiểm tra ở cuối vòng lặp chứ không phải ở đầu:
using System;
class Test {
public static void Main() {
int n = 0;
do {
Console.WriteLine(“Number is {0}”, n);
n++;
} while (n < 10);
}
}
Cũng như vòng lặp while, câu lệnh break và continue có thể được sử dụng để điều khiển luồng thực thi trong vòng lặp.
For
Vòng lặp for được sử dụng để duyệt qua vài giá trị. Một biến lặp có thể được khai báo như một phần của câu lệnh for:
using System;
class Test {
public static void Main() {
for (int n = 0; n < 10; n++)
Console.WriteLine(“Number is {0}”, n);
}
}
Phạm vi của biến lặp trong vòng lặp for là phạm vi của câu lệnh hay khối lệnh theo sau for. Nó không thể được truy xuất bên ngoài cấu trúc lặp:
// error
using System;
class Test {
public static void Main() {
for (int n = 0; n < 10; n++) {
if (n == 8)
break;
Console.WriteLine(“Number is {0}”, n);
}
// error; n is out of scope
Console.WriteLine(“Last Number is {0}”, n);
}
}
Như với vòng lặp while, câu lệnh break và continue có thể được sử dụng để điều khiển luồng thực thi trong vòng lặp.
Foreach
Đây là một cách sử dụng vòng lặp rất phổ biến:
using System;
using System.Collections;
class MyObject {}
class Test {
public static void Process(ArrayList arr) {
for (int nIndex = 0; nIndex < arr.Count; nIndex++) {
// cast is required because ArrayList stores
// object references
MyObject current = (MyObject) arr[nIndex];
Console.WriteLine(“Item: {0}”, current);
}
}
}
Điều này làm việc tốt, nhưng nó đòi hỏi người lập trình bảo đảm rằng mảng trong câu lệnh for phù hợp với mảng được sử dụng trong thao tác chỉ mục. Nếu chúng không phù hợp, đôi khi có thể là khó để theo dõi và bắt được các bug. Nó cũng đòi hỏi định nghĩa một biến chỉ mục riêng, mà có thể ngẫu nhiên được sử dụng ở nơi khác.
Điều đó cũng là hiển nhiên.
Một số ngôn ngữ, như Perl, cung cấp một cấu trúc khác để giải quyết với vấn đề này, và C# cũng cung cấp một cấu trúc như vậy. Ví dụ trước có thể được viết lại như sau:
using System;
using System.Collections;
class MyObject {}
class Test {
public static void Process(ArrayList arr) {
foreach (MyObject current in arr) {
Console.WriteLine(“Item: {0}”, current);
}
}
}
Nó đơn giản hơn nhiều, và không có những cơ hội tương tự để xảy ra lỗi. Kiểu được trả lại bởi một thao tác chỉ mục trên arr là được chuyển đổi mặc nhiên thành kiểu được khai báo trong foreach. Điều này là tốt, bởi vì các kiểu sưu tập như ArrayList chỉ có thể lưu trữ các giá trị của kiểu object.
Foreach cũng làm việc với các đối tượng khác ngoài các mảng. Sự thật, nó làm cho bất kỳ đối tượng nào cài đặt một giao diện thích hợp. Ví dụ, nó có thể được sử dụng để lặp qua các khoá của một bảng băm:
using System;
using System.Collections;
class Test {
public static void Main() {
Hashtable hash = new Hashtable();
hash.Add(“Fred”, “Flintstone”);
hash.Add(“Barney”, “Rubble”);
hash.Add(“Mr.”, “Slate”);
hash.Add(“Wilma”, “Flintstone”);
hash.Add(“Betty”, “Rubble”);
foreach (string firstName in hash.Keys) {
Console.WriteLine(“{0} {1}”, firstName, hash[firstName]);
}
}
}
Các đối tượng người sử dụng định nghĩa có thể cài đặt để chúng có thể được duyệt qua bằng cách sử dụng foreach; hãy xem mục “Chỉ mục và foreach” trong Chương 19, “Chỉ mục” để biết thêm chi tiết.
Một điều mà không thể làm được trong foreach là thay đổi nội dung của vật chứa. Nếu vật chứa hỗ trợ chỉ mục, nội dung có thể được thay đổi thông qua cách này, tuy nhiên nhiều vật chứa cho phép sử dụng foreach không cung cấp chỉ mục.
Như với các cấu trúc lặp khác, break và continue có thể được sử dụng với câu lệnh foreach.
Câu lệnh nhảy
Câu lệnh nhảy được sử dụng chỉ để nhảy từ câu lệnh này sang câu lệnh khác.
Break
Câu lệnh break được sử dụng để ngắt ra ngoài vòng lặp hiện thời hoặc câu lệnh switch, và tiếp tục thực thi sau câu lệnh đó.
Continue
Câu lệnh continue bỏ qua tất cả các dòng còn lại trong vòng lặp hiện thời, và sau đó tiếp tục thực thi câu lệnh lặp.
Goto
Câu lệnh goto có thể được sử dụng để nhảy trực tiếp tới một nhãn. Bởi vì việc sử dụng câu lệnh goto được xem là khá tồi tệ, nên C# cầm một số lạm dụng tồi tệ nhất. Ví dụ, một lệnh goto không thể được sử dụng để nhảy vào trong một khối lệnh. Chỉ nơi, những nơi mà việc sử dụng chúng được khuyến cáo, như câu lệnh switch hay để chuyển điều khiển ra ngoài một vòng lặp lồng nhau, tuy nhiên chúng có thể được sử dụng ở nơi khác.
Return
Câu lệnh return trả lại cho hàm gọi, và tuỳ ý trả lại một giá trị cũng được.
Định rõ phép gán
Các quy tắc định rõ phép gán ngăn cản các giá trị của một biến chưa được gán khỏi bị thực hiện. Giả sử đoạn mã được viết như sau:
// error
using System;
class Test {
public static void Main() {
int n;
Console.WriteLine(“Value of n is {0}”, n);
}
}
Khi nó được biên dịch, trình biên dịch sẽ báo một lỗi bởi vì giá trị của n được sử dụng trước khi nó được khởi tạo.
Tương tự, các thao tác không thể được thực hiện với một biến lớp trước khi nó được khởi tạo:
// error
using System;
class MyClass {
public MyClass(int value) {
this.value = value;
}
public int Calculate() {
return(value * 10);
}
public int value;
}
class Test {
public static void Main() {
MyClass mine;
Console.WriteLine(“{0}”, mine.value); // error
Console.WriteLine(“{0}”, mine.Calculate()); // error
mine = new MyClass(12);
Console.WriteLine(“{0}”, mine.value); // okay now…
}
}
Các struct làm việc hơi khác khi việc định rõ phép gán được xem xét. Thời gian chạy sẽ luôn đảm bảo rằng chúng có giá trị rỗng, nhưng trình biên dịch vẫn sẽ kiểm tra xem chúng đã được khởi tạo giá trị trước khi chúng được sử dụng.
Struct được khởi tạo hoặc thông qua lời gọi đến cấu tử hoặc bằng cách thiết tất cả các thành phần của một thể hiện trước khi sử dụng nó:
using System;
struct Complex {
public Complex(float real, float imaginary) {
this.real = real;
this.imaginary = imaginary;
}
public override string ToString() {
return(String.Format(“({0}, {0})”, real, imaginary));
}
public float real;
public float imaginary;
}
class Test {
public static void Main() {
Complex myNumber1;
Complex myNumber2;
Complex myNumber3;
myNumber1 = new Complex();
Console.WriteLine(“Number 1: {0}”, myNumber1);
myNumber2 = new Complex(5.0F, 4.0F);
Console.WriteLine(“Number 2: {0}”, myNumber2);
myNumber3.real = 1.5F;
myNumber3.imaginary = 15F;
Console.WriteLine(“Number 3: {0}”, myNumber3);
}
}
Trong phần đầu tiên, myNumber1 được khởi tạo bằng một lời gọi đến new. Nên nhớ rằng với cấu trúc, không có cấu tử mặc định, nên lời gọi này không làm gì cả; nó chỉ đơn thuần thực hiện việc đánh dấu một thể hiện như đã được khởi tạo.
Trong phần thứ hai, myNumber2 được khởi tạo bằng lời gọi thông thường đến một cấu tử. Trong phần thứ ba, myNumber3 được khởi tạo bằng cách gán các giá trị cho tất cả các thành phần của thể hiện. Rõ ràng điều này chỉ có thể thực hiện được nếu các thành phần đều là public.
Định rõ phép gán và mảng
Các mảng làm việc hơi khác một chút trong việc định rõ phép gán. Đối với các mảng của cả kiểu tham chiếu và kiểu giá trị (class và struct), một phần tử của mảng có thể được truy xuất, dù nó chưa được khởi tạo.
Ví dụ, giả sử có một mảng có kiểu Complex:
using System;
struct Complex {
public Complex(float real, float imaginary) {
this.real = real;
this.imaginary = imaginary;
}
public override string ToString() {
return(String.Format(“({0}, {0})”, real, imaginary));
}
public float real;
public float imaginary;
}
class Test {
public static void Main() {
Complex[] arr = new Complex[10];
Console.WriteLine(“Element 5: {0}”, arr[5]); // legal
}
}
Bởi vì các thao tác có thể được thực hiện trên một mảng – như Reverse() – trình biên dịch không thể kiểm soát việc định rõ phép gán trong tất cả các tình huống, và nó có thể dẫn đến một lỗi giả tạo. Do đó nó không được sử dụng.