Cấu trúc điều khiển Câu lệnh điều khiển cho phép một lập trình viên xác định thứ tự thực thi các lệnh trong mỗi chương trình, thể hiện qua việc: Bỏ qua một vài câu lệnh để thực thi các c
NỘI DUNG
Semantic (Ngữ nghĩa)
Ngữ nghĩa là nghĩa của biểu thức, câu lệnh, chương trình, ý nghĩa của cấu trúc trong một ngôn ngữ và ý nghĩa của chương trình được viết bởi ngôn ngữ đó Cú pháp chỉ là dòng lệnh đê mô tả ngữ nghĩa còn ngữ nghĩa là cơ sở cho mọi thứ bạn làm trong ngôn ngữ đó hay ngữ nghĩa có thể coi như một chức năng mà bản đồ cấu trúc cú phapscho mô hình tính toán.
Nhận xét: Nhận thấy ý nghĩa của 2 dòng lệnh khác nhau Java thoát khỏi vòng lặp khi điều kiện sai còn pascal thì ngược lại.
Phân tích ngữ nghĩa có hai loại gồm kiểm tra tĩnh (static semantics) và kiểm tra động (dynamic semantics) Kiểm tra động xảy ra lúc chương trình đích chạy vậy nên trong phần này sẽ giới thiệu qua về kiểm tra động và tập trung sâu vào kiểm tra tĩnh đặc biệt là kiêm tra kiểu dữ liệu.
2.1 Ngữ nghĩa động (Dynamic semantics):
2.1.1 Ngữ nghĩa tác vụ (Operational semantics): Đặc tả ý nghĩa một chương trình khi thực thi từng câu lệnh trên máy tính (máy thật hoặc mô phỏng) Sự thay đổi trạng thái của máy tính cho ta ý nghĩa của câu lệnh Dựa vào một máy ảo mà tập các tác vụ của nó đã được định nghĩa chính xác hay ngữ nghĩa của mỗi phần tử chương trình được đặc tả bằng tập các tác vụ của máy ảo.
2.1.2 Ngữ nghĩa tiên đề (Axiomatic semantics): Ý nghĩa của chương trình được xác định bởi tập hợp các quy tắc làm thay đổi dữ liệu sau khi thực hiện một phép toán nào đó của ngôn ngữ lập trình Ngữ nghĩa tiên đề dùng để chứng minh các tính chất của chương trình.
Tác dụng của một phát biểu (statement) được định nghĩa thông qua tiền điều kiện (precondition) và hậu điều kiên (postcondition).
Tập hợp các tiên đề và luật cho định nghĩa tác dụng của chương trình.
Nếu P đúng thì sau khi S được thực thi xong thì Q đúng
Tiền điều kiện yếu nhất (weakest precondition) wp(S,Q) là tiền đề điều kiện yếu nhất có thể thỏa mãn.
L4: Luật phát biểu ghép (sequences) if P S1 Q Q S2 R then P S1;S2 {R} ∧
L5: Nếu P B S1 Q ∧ ∧P∧ơB S2 Q thỡ P if B then S1 else S2 {Q}
L6: Nếu P B S1 Q ∧ ∧P∧ơB=> Q thỡ P if B then S1 {Q}
L7: Nếu P B S P thỡ Pwhile B do S P ơB ∧ ∧
Là phương pháp đặc tả ngữ nghĩa trừu tượng nhất, dựa trên co sở lý thuyết hàm đệ quy. Phiên bản gốc được phát triển bởi Scott và Strachey (1970)
Tiến trình xây dựng đặc tính biểu thị cho một ngôn ngữ:
Định nghĩa các đối tượng toán học cho mỗi thực thể ngôn ngữ
Định nghĩa hàm ánh xạ mỗi thể hiện của thực thể đến thể hiện của đối tượng toán học tương ứng.
Mỗi cấu trúc chương trình là một hàm ánh xạ đầu vào đến đầu ra Một chương trình là sự hợp thành của nhiều hàm.
Ngữ nghĩa biểu thị của các ký tự số thập phân như sau:
Mdec( ‘0’) = 10 * Mdec()
Mdec( ‘1’) = 10 * M () + 1dec
Mdec( ‘9’) = 10 * M () + 9dec
Tất cả những phương pháp đặc tả trên đều là ngữ nghĩa động, mô tả hành vi theo cách này hay cách khác xảy ra lúc chương trình đích chạy Ngược lại ngữ nghĩa tĩnh (static semantics) của một ngôn ngữ mô tả các kiểm tra kiểu trong trình biên dịch.
2.2 Ngữ nghĩa tĩnh (Static semantics):
Một số dạng kiểm tra tĩnh như sau:
Kiểm tra kiểu: Kiểm tra tính đúng đắn của các kiểu toán hạng trong biểu thức.
Kiểm tra dòng điều khiển: Một số điều khiển phải có cấu trúc hợp lý Ví dụ: Lệnh break trong ngôn ngữ C phải nằm trong vòng lặp.
Kiểm tra tính nhất quán: Có những ngữ cảnh mà trong đó một đối tượng được định nghĩa chỉ đúng một lần Ví dụ: Một tên phải được khai báo duy nhất, các nhãn trong lệnh case phải khác nhau và các phần tử trong kiểu vô hướng không được lặp lại.
Kiểm tra quan hệ tên: Đôi khi một tên phải xuất hiện từ hai lần trở lên Ví dụ: Trong Assembly, một chương trình con có một tên mà chúng phải xuất hiện ở đầu và cuối chương trình con này.
2.2.1 Bộ kiểm tra kiểu của định danh
Luật cú pháp Luật ngữ nghĩa
D -> id:T AddType(id.entry, T.type);
T -> array[num] of T T.type:=array(1…num.val, T1 1.type)
Ví dụ: fruits = [‘apples’,’cheries’,’bananas’,’oranges’,’mangos’] fruits.type:=array(‘apples’,’cheries’,’bananas’,’oranges’,’mangos’,string)
3.2.2 Bộ kiểm tra kiểu của biểu thức
Luật cú pháp Luật ngữ nghĩa
S -> id:=E S.type := if id.type=E.type then void else type_error;
E -> E + E1 2 E.type:= if E type=interger and E type=interger then interger1 2
Else if E type=interger and E type=real then real1 2
Else if E type=real and E type=interger then real1 2
Else if E type=real and E type=real then real1 2
E -> id E.type:=GetType(id,entry)
E.type:= if E type=interger and E type=interger then interger else1 2 type_error
E -> E1[E2] E.type:=if E type=interger and E type=array(s,t) then t else type_error2 1
Giả sử boolean là kiểu “real” trong ngôn ngữ của bạn (tức là chúng không chỉ là số nguyên như trong C) và bạn có một chương trình như (7> 2) + 3 Đây là lỗi vì 7> 2 là boolean và bạn không thể thêm boolean vào một số Cụ thể, đó là lỗi loại, vì vấn đề là loại của hai toán hạng cho toán tử cộng là "không tương thích", có nghĩa là về cơ bản, chúng không thể được cộng với nhau.
Cây cấu trúc được tạo tại thời điểm biên dịch, ngay sau giai đoạn phân tích cú pháp Vì vậy, bất kỳ loại kiểm tra nào được thực hiện với cây cấu trúc sẽ là kiểm tra thời gian tĩnh Đối với một biểu thức đơn giản như (7> 2) + 3, quy trình khá đơn giản Bắt đầu từ các lá, tôi sẽ gắn nhãn mọi nút trong cây cấu trúc với loại của nó Bất cứ khi nào có một thao tác như > hoặc +, tôi kiểm tra xem các loại đối số có tương thích hay không, sau đó gắn nhãn cho nút với loại kết quả bất kỳ Trong trường hợp này, nút > được gắn nhãn là boolean, và tất nhiên 3 là một số, vì vậy khi tôi gắn nhãn nút +, tôi thấy rằng chúng không tương thích và xuất hiện lỗi.
Cụ thể hơn, kiểm tra kiểu tĩnh được thực hiện bằng cách gắn nhãn cho tất cả các nút cây cấu trúc là biểu thức với các kiểu của chúng và kiểm tra trong tất cả các nút không phải lá xem các loại biểu thức con có tương thích không Ý nghĩa của "tương thích" sẽ khác nhau đối với các nút khác nhau: ví dụ: một nút * có thể yêu cầu cả hai biểu thức con là kiểu số, trong khi nút lệnh if có thể yêu cầu điều kiện của nó là boolean. Nhưng việc kiểm tra kiểu tĩnh trở nên phức tạp hơn rất nhiều khi chúng ta có các biến. Nhìn lại AST cho chương trình này ở trên: Đây là một chương trình hoàn toàn hợp lệ, nó đọc một thứ gì đó từ tiêu chuẩn trong, gán biến a cho phù hợp, rồi cố gắng trả giá trị đúng về biến đó Nói tóm lại, tính linh hoạt của một ngôn ngữ như Python không khiến chúng ta phải khai báo các loại biến được dùng trong chương trình, và chúng ta phải đợi đến thời điểm chạy để phát hiện ra lỗi hoặc lỗi trong mã của chúng ta.
2.2.3 Bộ kiểm tra kiểu của câu lệnh:
Luật cú pháp Luật ngữ nghĩa
S -> if E then S1 S.type:=if E.type=boolean then S type else type_error1
S -> while E do S S.type:=if E.type=boolean then S type else type_error1 1
S -> S1;S2 S.type:=if S type=void and S type=void then void else type_error1 2
Ví dụ: Áp dụng cây cấu trúc vào một chương trình hoàn toàn hợp lệ về mặt ngữ nghĩa như sau: CHơ
2.2.4 Bộ kiểm tra kiểu của hàm:
Luật cú pháp Luật ngữ nghĩa
E -> E1 (E2) E.type:=if E type=s and E type=s->t then t else type_error2 1
Ví dụ: Áp dụng cây cấu trúc vào phân tích ngữ nghĩa cho chương trình hoàn toàn hợp lệ như sau:
Exception (Ngoại lệ)
Các ngoại lệ còn được gọi là lỗi thời gian chạy Vì vậy, về cơ bản, một ngoại lệ cũng là một lỗi Nó không được phát hiện trong giai đoạn biên dịch không giống như các lỗi Cú pháp Chúng cũng không thực thi bình thường trong thời gian chạy, không giống như các lỗi ngữ nghĩa Những lỗi này chỉ được phát hiện trong thời gian chạy và ngăn việc thực thi chương trình bình thường.
Trong chương trình, có một số các "thao tác không chắc chắn", ví dụ như các thao tác vào/ra: đĩa mềm chưa sẵn sàng, máy in có lỗi, nối kết mạng không thực hiện được, sẽ dẫn đến lỗi thực thi chương trình Các ngôn ngữ lập trình hạn chế các lỗi sinh ra từ
"thao tác không chắc chắn" bằng cơ chế Ngoại lệ (Exception)
3.2 Mục đích của việc xử lý ngoại lệ:
Ngoại lệ tức là một sự kiện xảy ra ngoài dự tính của chương trình nếu không xử lý sẽ làm cho chương trình chuyển sang trạng thái không còn kiểm soát được Một chương trình nên có cơ chế xử lý ngoại lệ thích hợp Nếu không, chương trình sẽ bị ngắt khi một ngoại lệ xảy ra Trong trường hợp đó, tất cả các nguồn tài nguyên mà hệ thống đã cấp không được giải phóng Điều này gây lãng phí tài nguyên Để tránh trường hợp này, tất cả các nguồn tài nguyên mà hệ thống cấp nên được thu hồi lại Tiến trình này đòi hỏi cơ chế xử lý ngoại lệ thích hợp.
Ví dụ điều gì sẽ xảy ra nếu chương trình truy xuất đến phần tử thứ 11 của một mảng 10 phần tử? Một số ngôn ngữ như C, C++ sẽ không báo lỗi gì cả, chương trình vẫn tiếp tục vận hành nhưng kết quả thì không thể xác định được.
3.3 Mô hình xử lý ngoại lệ: Để hạn chế những lỗi như thế, một số ngôn ngữ lập trình bắt buộc các lệnh có thể dẫn đến các ngoại lệ phải có các đoạn mã xử lý phòng hờ khi ngoại lệ xảy ra theo cú pháp sau: try { Các thao tác vào ra có thể sinh ra các ngoại lệ } catch (KiểuNgoạiLệ_01 biến)
{ ứng xử khi ngoại lệ KiểuNgoaiLệ_01 sinh ra } catch (KiểuNgoạiLệ_02 biến)
{ ứng xử khi ngoại lệ KiểuNgoaiLệ_02 sinh ra } finally { Công việc luôn luôn được thực hiện }
Trong cơ chế này, các lệnh có thể tạo ra ngoại lệ sẽ được đưa vào trong khối bao bọc bởi từ khóa try {} Tiếp theo đó là một loạt các khối catch{} Một lệnh có thể sinh ra một hoặc nhiều loại ngoại lệ Ứng với một loại ngoại lệ sẽ có một khối catch{} để xử lý cho loại ngoại lệ đó Tham số của catch chỉ ra loại ngoại lệ mà nó có trách nhiệm xử lý Khi thực thi chương trình, nếu một lệnh nào đó nằm trong khối try{} tạo ra ngoại lệ, điều khiển sẽ được chuyển sang các lệnh nằm trong các khối catch{} tương ứng với loại ngoại lệ đó Các lệnh phía sau lệnh tạo ra ngoại lệ trong khối try{} sẽ bị bỏ qua Các lệnh nằm trong khối finally{} thì luôn luôn được thực hiện cho dù có xảy ra ngoại lệ hay là không Khối lệnh finally{} là tùy chọn có thể không cần.
Ngoại lệ có loại bắt buộc phải xử lý, tức phải có try{}, có catch{} khi sử dụng lệnh đó.
Ví dụ như lệnh đọc từ bàn phím Trình biên dịch của java sẽ báo lỗi nếu chúng ta không xử lý chúng.
Ngược lại, có loại ngoại lệ không bắt buộc phải xử lý, ví dụ như truy xuất đến phần tử bên ngoài chỉ số mảng
3.3.1 Trường hợp handle exception điển hình bằng try catch:
Mọi exception xảy ra khi thực hiện ở try đều được catch và sau đó thì không có gì phát sinh thêm Ví dụ: public void doNotIgnoreExceptions() { try {
● Ưu điểm: Kiểm soát được các lỗi phát sinh để chương trình luôn available.
● Nhược điểm: Một hình thức dấu lỗi, những xử lý (ở phương thức khác) khi dùng phương thức đó sẽ không biết được lỗi phát sinh từ đâu và lỗi như thế nào.
3.3.2 Trường hợp khi ta phát triển một chức năng có thể phát sinh exception nhưng ta không muốn giải quyết chúng:
Chỉ cần thêm các exception sau từ khóa throws, không phải try catch gì nữa Ví dụ: /**
* This method does something extremely useful
*/ public void doSomething(String input) throws MyBusinessException {
// do something with throw MyBusinessException
● Ưu điểm: Người lập trình không cần chú ý đến các lỗi phát sinh trong phương thức mình đang xây dựng vì sẽ có người sau đó (cùng những phương thức sau đó) xử lý.
● Nhược điểm: Nếu lạm dụng, chỉ biết ném exception từ phương thức này sang phương thức khác thì ai sẽ là người giải quyết cuối cùng (?)
3.3.3 Trường hợp là sự kết hợp giữa hai tình huống trên:
Ta vẫn xử lý những exception có thể phát sinh đồng thời thông báo cho người sau biết là cái gì đang xảy ra Ví dụ: try { new Long("xyz");
} catch (NumberFormatException e) { log.error(e); throw e;
} Ở ví dụ trên có thể xem là một cách để xử lý exception Log error khi nó diễn ra và rethrow để người gọi nó có thể xử lý một cách thích hợp Tuy nhiên với cùng một exception thì việc xử lý ghi log chẳng hạn, sẽ ghi lại cùng một lỗi nhiều lần Message lặp lại không mang lại bất kì lợi ích nào thậm chí còn gây khó khăn trong việc debug Vì vậy nếu ta có ném lại exception thì nên tiết chế Đừng ném y nguyên lại cái exception đó cho người sau Và tất nhiên với ví dụ trên, chúng ta nên chuyển thành như sau: public void wrapException(String input) throws MyBusinessException { try { new Long("xyz");
} catch (NumberFormatException e) { throw new MyBusinessException("String can’t input.", e);
● Ưu điểm: Mọi chuyện đều diễn ra tường minh, người lập trình sau cũng nhưng như những phương thức sau đó đều biết chuyện gì đang xảy ra, lỗi phát sinh ở đâu và như thế nào Đã có người đằng trước gỡ lỗi thì người đằng sau sẽ chịu ít ảnh hưởng hơn
● Nhược điểm: Có một biệt lệ mà cả hai hướng thì phải xử lý một việc hai lần Đối với trường hợp nhiều phương thức sau sử dụng phương thức đó thì tất nhiên là chúng ta vẫn sẽ phải catch đến cuối cùng.
3.3.4 Cá nhân hóa biệt lệ (custom exception):
Các ngôn ngữ lập trình hiện tại đều đã xây dựng sẵn gần như tất cả các exception có thể xảy ra trong chương trình Tuy nhiên gần như tất cả không phải là tất cả Vẫn có một số trường hợp chẳng hạn như lỗi thuật toán, lỗi nghiệp vụ, lỗi thao tác với application… thì không thể nào try catch bằng exception sẵn có được Chính vì thế các ngôn ngữ lập trình cho phép lập trình viên có thể tạo ra một exception mới để sử dụng cho các trường hợp này.
Ví dụ: Một công ty không thể có nhân viên dưới 18 Nếu có nhân viên nào dưới 18 tuổi thì đó sẽ là exception Trường hợp này thì các exception sẵn có không thể giúp gì được. if(age