Sửa lỗi và gỡ lỗi
Nghệ thuật lập trình dựa trên khả năng hướng dẫn chương trình phải làm gì và làm như thế nào, cũng như bảo vệ chương trình khỏi việc có thể làm sai điều gì đó. Thật không may, điều sau khó thực hiện hơn nhiều do nhiều yếu tố không rõ ràng ảnh hưởng đến hành vi của chương trình. Dữ liệu không chính xác, tài nguyên không đủ, lỗi mã hóa của người khác và của chính mình chỉ là một số vấn đề.
Không ai được bảo hiểm chống lại lỗi trong chương trình mã hóa. Lỗi có thể xảy ra ở các giai đoạn khác nhau và được chia thành:
- Lỗi biên dịch được trình biên dịch trả về khi xác định mã nguồn không đáp ứng cú pháp yêu cầu (chúng ta đã tìm hiểu về các lỗi như vậy ở trên); cách dễ nhất để sửa chúng là trình biên dịch sẽ tìm kiếm chúng;
- Lỗi thời gian chạy chương trình được trả về bởi thiết bị đầu cuối, nếu một điều kiện không chính xác xảy ra trong chương trình, chẳng hạn như chia cho số không, tính căn bậc hai của một số âm hoặc cố gắng tham chiếu đến một phần tử không tồn tại của mảng, như trong trường hợp của chúng ta; chúng khó phát hiện hơn vì chúng thường không xảy ra ở bất kỳ giá trị nào của tham số đầu vào, mà chỉ xảy ra trong các điều kiện cụ thể;
- Lỗi thiết kế chương trình dẫn đến việc tắt hoàn toàn mà không có bất kỳ mẹo nào từ thiết bị đầu cuối, chẳng hạn như bị kẹt ở vòng lặp vô hạn; những lỗi như vậy có thể trở thành lỗi phức tạp nhất về mặt xác định vị trí và tái tạo chúng, trong khi khả năng tái tạo một vấn đề trong chương trình là điều kiện cần thiết để sửa lỗi sau đó; và
- Lỗi ẩn, khi chương trình dường như hoạt động trơn tru, nhưng kết quả đưa ra lại không chính xác; rất dễ phát hiện nếu 2*2 không phải là 4, trong khi rất khó để nhận ra sự khác biệt.
Nhưng hãy quay lại tình huống cụ thể với tập lệnh của chúng ta. Theo thông báo lỗi được cung cấp cho chúng ta bởi môi trường chạy chương trình MQL, câu lệnh sau là sai:
return messages[hour / 8]
Khi tính toán chỉ số của một phần tử trong mảng, tùy thuộc vào giá trị của biến hour
, có thể thu được giá trị vượt quá kích thước mảng là ba.
Trình gỡ lỗi được nhúng trong MetaEditor cho phép đảm bảo rằng điều đó thực sự xảy ra. Tất cả các lệnh của nó được thu thập trong menu Debug
. Chúng cung cấp nhiều chức năng hữu ích. Ở đây chúng ta sẽ chỉ giải quyết hai điều: Debut -> Start on Real Data (F5)
và Debug -> Start on History Data (Ctrl+F5)
. Bạn có thể đọc về những điều khác trong MetaEditor Help.
Cả hai lệnh đều biên dịch chương trình theo cách đặc biệt — với thông tin gỡ lỗi. Phiên bản chương trình như vậy không được tối ưu hóa như trong biên dịch chuẩn (chi tiết hơn về tối ưu hóa, vui lòng xem Tài liệu), đồng thời, nó cho phép sử dụng thông tin gỡ lỗi để 'xem bên trong' chương trình trong khi thực thi: Xem trạng thái của các biến và ngăn xếp lệnh gọi hàm.
Sự khác biệt giữa gỡ lỗi trên dữ liệu thực và trên dữ liệu lịch sử bao gồm việc khởi động chương trình trên biểu đồ trực tuyến với biểu đồ trước và trên biểu đồ thử nghiệm ở chế độ trực quan với biểu đồ sau. Để hướng dẫn trình chỉnh sửa về biểu đồ chính xác nào và với cài đặt nào để sử dụng, tức là biểu tượng, khung thời gian, phạm vi ngày, v.v., trước tiên bạn nên mở hộp thoại Settings -> Debug
và điền vào các trường bắt buộc trong đó. Tùy chọn Sử dụng cài đặt đã chỉ định (Use specified settings) phải được bật. Nếu tùy chọn này bị tắt, biểu tượng đầu tiên từ Market Watch
và khung thời gian H1 sẽ được sử dụng trong gỡ lỗi trực tuyến, trong khi cài đặt thử nghiệm được sử dụng khi gỡ lỗi trên dữ liệu lịch sử.
Xin lưu ý rằng chỉ có thể gỡ lỗi Indicators
và Expert Advisors
trong trình kiểm tra. Chỉ có thể gỡ lỗi trực tuyến cho các tập lệnh (Scripts
).
Hãy chạy tập lệnh của chúng ta bằng F5
và nhập 100 vào tham số GreetingHour
để tái tạo tình huống sự cố trên. Tập lệnh sẽ bắt đầu thực thi và thiết bị đầu cuối sẽ hiển thị ngay lập tức thông báo lỗi và yêu cầu mở trình gỡ lỗi.
Critical error while running script 'GoodTime1 (EURUSD,H1)'.
Array out of range.
Continue in debugger?
2
3
Sau khi trả lời là có, chúng ta sẽ vào MetaEditor, tại đó chuỗi hiện tại được đánh dấu trong mã nguồn, nơi xảy ra lỗi (vui lòng chú ý đến mũi tên màu xanh lá cây ở trường bên trái).
Ngăn xếp lệnh gọi hiện tại được hiển thị ở phần cửa sổ bên trái phía dưới: Tất cả các hàm được liệt kê trong đó (theo thứ tự từ dưới lên), đã được gọi trước khi thực thi mã dừng ở chuỗi hiện tại. Cụ thể, trong tập lệnh của chúng ta, hàm OnStart
đã được gọi (bởi chính thiết bị đầu cuối) và hàm Greeting
đã được gọi từ đó (chúng ta đã gọi nó từ mã của mình). Một bảng tổng quan nằm ở phần bên phải phía dưới của cửa sổ. Tên của các biến có thể được nhập vào đó hoặc toàn bộ biểu thức vào cột Expression và theo dõi giá trị của chúng trong các cột Value trong cùng một chuỗi.
Ví dụ, chúng ta có thể sử dụng lệnh Add
trong menu ngữ cảnh hoặc nhấp đúp chuột vào chuỗi ký tự trống đầu tiên để nhập biểu thức "hour / 8
" và đảm bảo rằng nó bằng 12
.
Vì quá trình gỡ lỗi bị dừng lại do lỗi nên không cần tiếp tục chương trình nữa; do đó, chúng ta có thể thực hiện lệnh Debug -> Stop (Shift+F5
).
Trong những trường hợp phức tạp hơn của nguồn sự cố không quá rõ ràng, trình gỡ lỗi cho phép theo dõi từng chuỗi trình tự thực thi các câu lệnh và nội dung của các biến.
Để giải quyết vấn đề, cần đảm bảo rằng, trong mã, chỉ số phần tử luôn nằm trong phạm vi 0-2
, tức là tuân thủ kích thước mảng. Nói một cách chính xác, chúng ta nên viết một số câu lệnh bổ sung để kiểm tra tính chính xác của dữ liệu đã nhập (trong trường hợp của chúng ta, GreetingHour
chỉ có thể lấy giá trị trong phạm vi 0-23
), sau đó hiển thị mẹo hoặc tự động sửa lỗi trong trường hợp vi phạm các điều kiện.
Trong dự án giới thiệu này, chúng ta sẽ không đi sâu hơn một sửa lỗi đơn giản: Chúng ta sẽ cải thiện biểu thức tính toán chỉ số phần tử để kết quả của nó luôn nằm trong phạm vi yêu cầu. Với mục đích này, chúng ta hãy tìm hiểu thêm một toán tử nữa — toán tử modulus (chia lấy dư) chỉ hoạt động với số nguyên. Để biểu thị phép toán này, chúng ta sử dụng ký hiệu '%
'. Kết quả của phép toán modulus là phần dư của phép chia số nguyên của số bị chia cho số chia. Ví dụ:
11 % 5 = 1
Ở đây, khi chia 11 cho 5, ta sẽ thu được 2, tương ứng với ước lớn nhất của 5 trong 11 là 10. Số dư giữa 11 và 10 chính xác là 1.
Để sửa lỗi trong hàm Greeting
, chỉ cần thực hiện phép chia lấy dư của giờ cho 24 trước, điều này sẽ đảm bảo rằng số giờ sẽ nằm trong khoảng từ 0-23. Hàm Greeting sẽ trông như sau:
string Greeting(int hour)
{
string messages[3] = {"Good morning", "Good afternoon", "Good evening"};
return messages[hour % 24 / 8];
}
2
3
4
5
Mặc dù bản sửa lỗi này chắc chắn sẽ hoạt động tốt (chúng ta sẽ kiểm tra sau một phút), nhưng nó không liên quan đến một vấn đề khác nằm ngoài phạm vi tập trung của chúng ta. Vấn đề là tham số GreetingHour
có kiểu int
, tức là nó có thể nhận cả giá trị dương và giá trị âm. Ví dụ, nếu chúng ta thử nhập -8
hoặc một số 'âm hơn', thì chúng ta sẽ nhận được cùng một lỗi thời gian chạy, tức là vượt ra ngoài mảng; chỉ là, trong trường hợp này, chỉ số không vượt quá giá trị cao nhất (kích thước mảng) mà trở nên nhỏ hơn giá trị thấp nhất (cụ thể, -8
dẫn đến tham chiếu đến phần tử thứ -1, điều thú vị là các giá trị từ -7 đến -1 được hiển thị trên phần tử thứ 0 và không gây ra bất kỳ lỗi nào).
Để khắc phục vấn đề này, chúng ta sẽ thay thế kiểu tham số GreetingHour
bằng số nguyên không dấu: Chúng ta sẽ sử dụng uint
thay vì int
(chúng ta sẽ nói về tất cả các kiểu có sẵn trong phần hai, và ở đây chúng ta cần uint
). Được hướng dẫn bởi giới hạn cho tính không âm của các giá trị, được tích hợp ở cấp độ trình biên dịch cho uint
, MQL5 sẽ độc lập đảm bảo rằng cả người dùng (trong hộp thoại thuộc tính) và chương trình (trong quá trình tính toán) đều "không có giá trị âm".
Hãy lưu phiên bản mới của tập lệnh dưới dạng GoodTime2
, biên dịch và khởi chạy nó. Chúng ta nhập giá trị 100
cho tham số GreetingHour
và đảm bảo rằng, lần này, tập lệnh được thực thi mà không có bất kỳ lỗi nào, trong khi lời chào "Chào buổi sáng" được in trong nhật ký thiết bị đầu cuối. Đây là hành vi mong đợi (đúng) vì chúng ta có thể sử dụng máy tính và kiểm tra xem phần dư của phép chia lấy dư của 100
cho 24
cho kết quả là 4, trong khi phép chia nguyên của 4
cho 8
là 0
, nghĩa là buổi sáng, trong trường hợp của chúng ta. Tất nhiên, theo quan điểm của người dùng, hành vi này có thể được coi là không mong muốn. Tuy nhiên, việc nhập 100 làm số giờ cũng là một hành động không mong muốn của người dùng. Người dùng có thể nghĩ rằng chương trình của chúng ta sẽ ngừng hoạt động. Nhưng điều này đã không xảy ra và đây là một điểm tốt. Tất nhiên, với các chương trình thực tế, các giá trị đã nhập phải được xác thực và người dùng phải được thông báo về các lỗi.
Để ngăn ngừa việc nhập sai số, chúng ta cũng sẽ sử dụng một tính năng đặc biệt của MQL5 để đặt tên chi tiết và thân thiện hơn cho tham số đầu vào. Với mục đích này, chúng ta sẽ sử dụng chú thích sau mô tả tham số đầu vào trong cùng một chuỗi. Ví dụ, như thế này:
input uint GreetingHour = 0; // Greeting Hour (0-23)
Xin lưu ý rằng chúng ta đã viết riêng các từ từ tên biến trong bình luận (nó không còn là một định danh trong mã nữa mà là một mẹo cho người dùng trong đó). Hơn nữa, chúng ta đã thêm phạm vi các giá trị hợp lệ trong dấu ngoặc đơn. Khi khởi chạy tập lệnh, GreetingHour
trước đó sẽ xuất hiện trong hộp thoại để nhập các tham số như sau:
Greeting Hour (0-23)
Bây giờ chúng ta có thể chắc chắn rằng, nếu nhập số 100 vào giờ thì đó không phải là lỗi của chúng ta.
Một độc giả cẩn thận có thể tự hỏi tại sao chúng ta định nghĩa hàm Greeting
với tham số giờ và gửi GreetingHour
vào đó nếu chúng ta có thể sử dụng trực tiếp tham số đầu vào trong đó. Hàm, như một đoạn logic rời rạc của mã, được hình thành để chia chương trình thành các phần dễ thấy và dễ hiểu và sử dụng lại chúng sau đó. Các hàm thường được gọi từ nhiều phần của chương trình hoặc là một phần của thư viện được kết nối với nhiều chương trình khác nhau. Do đó, một hàm được viết đúng cách phải độc lập với ngữ cảnh bên ngoài và có thể di chuyển giữa các chương trình.
Ví dụ, nếu chúng ta cần chuyển hàm Greeting
của mình sang một tập lệnh khác, nó sẽ dừng biên dịch vì sẽ không có tham số GreetingHour
trong đó. Không hoàn toàn đúng khi yêu cầu thêm nó vì tập lệnh kia có thể tính toán thời gian theo cách khác. Nói cách khác, khi viết một hàm, chúng ta nên cố gắng hết sức để tránh các phụ thuộc bên ngoài không cần thiết. Thay vào đó, chúng ta nên khai báo các tham số hàm có thể được điền bằng mã gọi.