Các mẹo trong việc sử dụng các kiểu dữ liệu số.

Các mẹo trong việc sử dụng các kiểu dữ liệu số.

Bài này được lượt dịch từ cuốn Code Complete, có gì sai các bạn bổ sung nhé.

12 Các kiểu dữ liệu cơ bản

Nội dung:
12.1 Kiểu dữ liệu số nói chung
12.2 Số nguyên (Integers)
12.3 Số thực dấu chấm động (Floating-Point Numbers)
12.4 Kí tự và chuỗi (Characters and Strings)
12.5 Kiểu luận lí (Boolean)
12.6 Kiểu liệt kê (Enumerated Types)
12.7 Đặt tên các hằng số
12.8 Mảng
12.9 Kiểu dữ liệu người dùng định nghĩa
Các kiểu dữ liệu cơ bản là khối dữ liệu cơ sở để xây dựng nên các kiểu dữ liệu khác. Chương này bao gồm các mẹo trong việc sử dụng số nguyên, số thực, kí tự và chuỗi, kiểu luận lí, kiểu liệt kê, cách đặt tên các hằng số và mảng. Phần cuối của chương mô tả cách tự tạo các kiểu dữ liệu riêng.Chương này trình bày cách gỡ rối cho các kiểu dữ liệu cơ bản. Nếu bạn đã có kiến thức về các kiểu dữ liệu cơ bản thì bạn có thể bỏ qua các phần khác để ôn lại danh sách các điều nên tránh ở cuối chương và chuyển qua Chương 13 để thảo luận về các kiểu dữ liệu khác.12.1 Kiểu dữ liệu số
Dưới đây là một vài dòng hướng dẫn cách tránh các lỗi khi sử dụng kiểu dữ liệu số.
Tránh các “số ma lực” (magic numbers).Số ma lực” là các hằng số chẳng hạn như các số 100 hay 47524 xuất hiện ở giữa chương trình mà không có bất kì sự giải thích nào. Nếu bạn lập trình với ngôn ngữ hỗ trợ việc đặt tên hằng thì hãy sử dụng cách này thay vì sử dụng các con số cụ thể. Nếu bạn không thể đặt tên hằng hãy sử dụng biến toàn cục khi khả thi.3 thuận lợi khi tránh các “số ma thuật”:
Sự thay đổi có thể được tạo ra một cách đáng tin cậy hơn. Nếu bạn sử dụng các hằng được đặt tên, bạn sẽ không bỏ sót bất kì 1 hằng nào trong số 100 hằng.
Sự thay đổi có thể được tạo ra một cách dễ dàng hơn. Khi các số nhập vào thay đổi từ 100 đến 200, nếu sử dụng “số ma thuật” bạn phải tìm tất cả 100 số ấy và thay bằng 200. Nếu bạn sử dụng 100+1 hay 100-1 bạn cũng phải tìm tất cả 100 và 99 hằng số ấy và thay đổi chúng bằng 201 và 199. Nếu bạn sử dụng các hằng đặt tên, một cách đơn giản bạn chỉ cần thay đổi hằng số đã định nghĩa 100 thay bằng 200 chỉ ở một chỗ.
Mã (code) của bạn sẽ dễ đọc hơn. Chắc chắn, hãy xem biểu thức sau:for i = 0 to 99 do… bạn cũng có thể đoán được rằng số các điểm nhập tối đa là 99. Nhưng biểu thức:
For i = 0 to MAX_ENTRIES – 1 do… sẽ không còn nghi ngờ nữa. Thậm chí nếu bạn chắc chắn số sẽ không bao giờ thay đổi, bạn cũng sẽ nhận được sự thuận lợi về tính dễ đọc nếu sử dụng tên hằng.Hãy sử dụng hard-coded với các số 0 và 1 nếu cần thiết Các giá trị 0 và 1 được dùng để tăng, giảm, và bắt đầu các vòng lặp tại phần tử đầu tiên của mảng. Sử dụng số trong vòng lặp for
for i = 0 to CONSTANT do …
và số 1 trong:
total = total + 1
Chỉ sử dụng trực tiếp các hằng số 0 và 1 trong chương trình còn các hằng số khác nên thay thế bằng một cái tên nào đó có cụ thể hơn.

Đoán trước lỗi chia 0 (divide-by-zero error) Mỗi lần sử dụng kí hiệu chia (/ trong hầu hết các ngôn ngữ), luôn xét xem liệu mẫu số (denominator) của biểu thức có bằng 0 hay không, nếu có khả năng bằng 0 ngay lập tức viết code (mã) để xác định lỗi chia 0.

Tạo các ép kiểu (type conversions) một cách rõ ràng Hãy chắc chắn khi một ai đó đọc code của bạn sẽ nhận ra được điểu đó khi có ép kiểu giữa các kiểu dữ liệu khác nhau xảy ra.
Trong C++ bạn có thể dùng:
y = x + (float) i còn trong Visual Basic:
y = x + CSng(i) Cách thực hành này sẽ giúp bảo đảm rằng sự ép kiểu là một trong những điều bạn muốn xáy ra – các trình biên dịch khác nhau sẽ làm việc này khác nhau, vì vậy hãy nắm lấy cơ hội.

Tránh so sánh các kiểu dữ liệu không đồng nhất (mixed-type) Nếu x là một số thực dấu chấm động và i là một số nguyên thì kết quả của việc kiểm tra
if ( i = x) hầu như không làm việc hoặc sẽ cho kết quả không như ý muốn. Trình biên dịch luận ra kiểu dữ liệu mà nó muốn sử dụng để so sánh, chuyển đổi kiểu dữ liệu này thành kiểu dữ liệu kia, thực hiện làm tròn và xác định câu trả lời, bạn sẽ gặp may mắn nếu chương trình chạy. Hãy thực hiện việc chuyển đổi kiểu bằng tay để trình biên dịch có thể so sánh 2 số cùng kiểu và bạn thực sự biết chính xác những gì đang được so sánh.

Chú ý đến những lời cảnh báo (warnings) của trình biên dịch Nhiều trình biên dịch tốt sẽ báo cho bạn biết khi có các kiểu dữ liệu số khác nhau trong cùng một biểu thức. Hãy chú ý! Lập trình viên chỉ có thể giúp một ai đó truy tìm các lỗi khó chịu bằng cách theo dõi các cảnh báo của trình biên dịch. Trình biên dịch làm việc này một cách dễ dàng hơn chúng ta tự làm.

12.2 Số nguyên Đây là một vài điều cân nhắc khi sử dụng số nguyên.

Kiểm tra phép chia nguyên (integer division) Khi sử dụng số nguyên, 7/10 sẽ không bằng 0.7 mà thường bằng
0. Điều này ảnh hướng ngay vào kết quả. Trong thế giới số thực 10*(7/10) = (10*7)/7 = 7 khác với thế giới số nguyên kết quả 10*(7/10) bằng 0 vì phép chia 7/10 = 0. Cách dễ dàng nhất để giải quyết vấn đề này là sắp xếp lại các thừa số để đạt kết quả: (10*7)/10.

Kiểm tra tràn số nguyên (integer overflow) Khi thực hiện phép nhân hay phép cộng cần chú ý đến số nguyên lớn nhất có thể đạt đến. Số nguyên không dấu lớn nhất thường là 65,535 hay 231 – 1. Vấn đề này sẽ xảy ra khi nhân hai số nguyên mà tích số lớn hơn số nguyên lớn nhất, ví dụ: 250 * 300 kết quả đúng là 75,000 nhưng nếu số nguyên lớn nhất là 65,535 thì kết quả bạn nhận được có lẽ là 9464 do số nguyên đã bị tràn (75,000 – 65,535 = 9464). Đây là tầm trị của các kiểu số nguyên chung chung:

Kiểu số nguyên Tầm trị Signed 8-bit (8 bit có dấu)
-128 – 127 Unsigned 8-bit (8 bit không dấu)
0 – 255 Signed 16-bit (16 bit có dấu)
-32,768 – 32,767 Unsigned 16-bit (16 bit không dấu)
0 – 65,535 Signed 32-bit (32 bit có dấu)
-2,147,483,648 – 2,147,483,647 Unsigned 32-bit (32 bit không dấu)
0 – 4,294,967,295 Signed 64-bit (64 bit có dấu)
-9,223,372,036,854,775,808 – 9,223,372,036,854,775,807 Unsigned 64-bit (64 bit không dấu)
0 – 18,446,744,073,709,551,615
Cách dễ nhất để chống lại tràn số nguyên là nhìn vào các toán hạng trong biểu thức số học và cố gắng hình dung giá trị lớn nhất có thể gắn vào đó. Ví dụ, biểu thức m = j * k , giá trị lớn nhất có thể của j là 200, của k là 25 thì giá trị lớn nhất của m là 200 * 25 = 5,000. Kết quả này đạt được trên các máy 32 bit vì giá trị lớn nhất của số nguyên trên máy này là 2,147,483,647. Nếu giá trị lớn nhất có thể của j là 200,000, của k là 100,000 thì giá trị lớn nhất của m nhận được là 200,000 * 100,000 = 20,000,000,000. Kết quả này không thể đạt được trên máy 32 bit. Trong trường hợp này phải dùng đến số nguyên 64 bit hoặc số thực dấu chấm động để nhận được kết quả mong đợi của m.

Cũng cần phải chú ý đến sự mở rộng của chương trình về sau. Nếu như giá trị của m không bao giờ vượt quá 5,000, điều này ổn định nhưng nếu muốn m phát triển một cách chắc chắn trong vài năm thì hãy cân nhắc.

Kiểm tra tràn cho kết quả trung gian Xem đoạn mã java sau:
int termA = 1000000;
int termB = 1000000;
int product = termA * termB / 1000000;
System.out.println( “(“ + termA + “ * ” + termB + “ ) / 1000000 = “ + product);

Nếu product được gán kết quả như phép tính (1000, 000 * 1000,000 ) / 1000,000 thì có lẽ kết quả sẽ là 1000,000. Nhưng đoạn mã tính ngay kết quả của 1000,000 * 1000,000 trước khi chia cho 1000,000 và điều này có nghĩa là cần 1 con số khá lớn 1000,000,000,000. Hãy đoán thử xem điều gì xảy ra?
Đây là kết quả:

(1000000 * 1000000)/1000000 = -727
Nếu số nguyên trong biểu thức tính toán lớn hơn 2,147,483,647 kết qủa sẽ rất lớn và vượt khỏi kiểu dữ liệu số nguyên sẽ gây nên tràn. Trong trường hợp này kết quả trung gian 1000,000,000,000 sẽ là -727,379,968 nên sẽ nhận được kết quả là -727 chứ không phải 1000,000.

Chúng ta có thể xử lý tràn kết quả trung gian giống như cách xử lý tràn số nguyên hoặc chuyển qua số long-integer hay floating-point.

12.3 Số thực dấu chấm động (Floating-Point Numbers)
Điều cần chú ý trong việc sử dụng số thực dấu chấm động là có rất nhiều số thập phân ở dạng phân số không thể biểu diễn một cách chính xác bằng các số 0 và 1 – kiểu dữ liệu cơ bản của máy tính số. Các số thập phân vô hạn không tuần hoàn như 1/3 hay 1/7 chỉ có thể được biểu diễn 7 hay 15 chữ số của số chính xác. Trong phiên bản Visual Basic của tôi, biểu diễn số thực dấu chấm động 32 bit của 1/3 là 0.33333330, chính xác đến 7 chữ số. Độ chính xác này đáp ứng đủ cho hầu hết các mục đích của chúng ta, nhưng sự thiếu chính xác đôi khi đánh lừa chúng ta.

Sau đây là các mẹo đặc biệt khi dùng số thực dấu chấm động:

Tránh cộng hay trừ với các số có độ lớn khác nhau Với biến số thực dấu chấm động 32 bit, 1,000,000.00 + 0.1 có thể có kết quả là 1,000,000.00 vì 32 bit không cho đủ các chữ số có nghĩa để chứa tầm trị giữa 1,000,000 và 0.1. Giống như thế phép trừ 5,000,000.02 – 5,000,000.01 sẽ cho kết quả là 0.0.

Giải pháp cho vấn đề trên ? Nếu phải cộng một dãy các số có độ lớn khác nhau quá lớn như vậy hãy sắp xếp các số trước rồi cộng lại bắt đầu với số giá trị nhỏ nhất. Nói cách khác nếu muốn tính tổng của một dãy các số vô hạn thì hãy bắt đầu với phần tử nhỏ nhất – về bản chất là tính tổng lùi các toán hạng. Điều này không loại trừ sự làm tròn nhưng nó hạn chế đến mức tối thiểu việc này. Có rất nhiều sách thuật toàn đề cập đến cách giải quyết vấn đề này.

Tránh so sánh bằng Số thực dấu chấm động có thể bằng hoặc luôn luôn không bằng. Vấn đề chính là 2 con đường khác nhau dẫn đến một con số giống nhau thì không luôn dẫn đến con số giống nhau đó. Ví dụ cộng 10 lần 0.1 hiếm khi bằng 1.0. Xem đoạn mã bằng java sau so sánh hai biến nominalsum có vẻ như chúng bằng nhau nhưng thực sự thì chúng không bằng:
double nominal = 1.0;
double sum = 0.0;
for ( int i = 0; i

Có lẽ bạn đoán được, chương trình xuất ra: “Number are different”.
Giá trị của sum trong vòng lặp như sau:
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

Như vậy, chúng ta làm thế nào đề so sánh sự bằng nhau của các số thực dấu chấm động? Một cách tiếp cận hiệu quả là xác định độ chính xác có thể chấp nhận được rồi sau đó sử dụng hàm luận lý để kiểm tra giá trị của chúng có xấp xỉ không. Ta có thể viết hàm Equals() trả về [I]True[/I] nếu giá trị của chúng xấp xỉ nhau hoặc ngược lại trả về [I]False[/I]. Trong java , chúng ta có thể viết như sau:

double const ACCEPTABLE_DELTA = 0.00001;
boolean Equals(double Term1, double Term2){
if(Math.abs(Term1-Term2) return true;
} else {
return false;
}
}

Trở lại ví dụ trên để so sánh hai số nominalsum ta dùng if(Equals(nominal, sum))… thì chương trình sẽ xuất ra: “Numbers are the same”.
Tùy thuộc vào yêu cầu của ứng dụng, sẽ không tốt nếu ta sử dụng hard-coded cho giá trị của AcceptableDelta vì thế cần phải tính giá trị của AccpetableDelta dựa vào kích thước của hai số được so sánh.

Comments

3 comments in this post. Add your comments below

  1. Duy Do’s WebLog cho mình hỏi cách viết chương trình cộng hai phân số trong VB6.0? Sẽ thiết kế giao diện sao cho phù hợp,ràng buộc sao cho không nhập vào mẫu bằng 0. Chân thành cảm ơn câu trả lời. Vui lòng gởi qua mail loihuynh@ymail.com DT 0935216046

  2. Hello and thanks for your kind review! Word 2007 or TeamViewer

Add comment

  • required
  • required