Làm sao để sử dụng biến hiệu quả hơn (part2)
Bài viết trước mình đã nói về cách khai báo biến và khởi tạo biến và bài viết này sẽ nói về cách chọn phạm vi biến sao cho code hiệu quả, dễ đọc, dễ hiểu và tất nhiên là dễ debug
Part2: Phạm vi biến (Scope)
Hãy để tham chiếu (reference) đến các biến gần nhau
Có một phương pháp để tính toán mức độ gần nhau của các tham chiếu/lời gọi biến đó là ước tính số span của biến . Đây là ví dụ về số span:
a=0; <--- Tham chiếu đến a lần 1
b=0; <--- Tham chiếu đến b lần 1
c=0;<--- Tham chiếu đến c lần 1
a= b + c; <--- Tham chiếu đến a lần 2, Tham chiếu đến b lần 2, Tham chiếu đến c lần 2
Trong trường hợp này, có 2 dòng nằm giữa tham chiếu đến biến a
thứ nhất và thứ hai, vậy a
có số span là 2, tương tự ta có số span của b và c lần lượt là 1 và 0. Và đây là một ví dụ khác
a = 0;
b = 0;
c = 0;
b = a + 1;
b = b / c;
Trong trường hợp này, có 1 dòng giữa tham chiếu đến b
thứ nhất và thứ hai nên nó có span là 1. Và không có dòng nào giữa tham chiếu thứ hai và tham chiếu thứ ba, vậy có span là 0.
Số span trung bình được tính toán dựa trên những span riêng lẻ. Trong ví dụ 2, với b, (1+0)/2 = 0.5, vậy số span trung bình của b là 0.5. Khi mà bạn giữ các tham chiếu đến biến gần nhau, bạn có thể giúp người đọc code của bạn tập trung vào một phần của code, còn nếu bạn gọi biến cách xa nhau, bạn sẽ bắt người đọc lục tung cả chương trình. Vì thế lợi ích chính của việc sắp xếp biến gần nhau là tăng khả năng đọc code rõ ràng và dễ hiểu hơn.
Hãy giữ cho biến "sống" trong thời gian ít nhất có thể
Ở đây ta có thêm một khái niệm nữa là "live time" là tổng số dòng lệnh mà biến được sử dụng. Biến tồn tại từ dòng lệnh đầu tiên mà nó được gọi đến và nó kết thúc ở dòng lệnh cuối cùng mà nó được gọi đến.
Không giống như span, live time của biến không bị ảnh hưởng bởi số lần biến được sử dụng từ lần đầu tới lần cuối mà biến được gọi đến. Nếu biến được gọi lần đầu tiên ở dòng 1 và lần cuối cùng ở dòng 25 thì nó sẽ tồn tại trong 25 dòng lệnh. Nếu chỉ có 2 dòng trong đó biến được sử dụng thì nó có span trung bình là 23 dòng lệnh. Nếu biến đã được dùng ở mỗi dòng từ dòng 1 đến 25, nó sẽ có số span trung bình là 0 dòng lệnh, nhưng nó vấn sẽ có live time là 25 dòng lệnh. Hình 10-1 minh họa cả 2 thông số span và live time.
"Live time" thể hiện tuổi thọ của biến, "Span" là số "bước" nó thể hiện độ gần nhau của cách tham chiếu đến biến
Tóm lại, cũng như với span, mục tiêu cần quan tâm với live time đó là giữ con số đó NHỎ, tức là hãy làm cho biến "chết sớm" nhất có thể.
Tính toán thời gian tồn tại của biến
Bạn có thể tính live time của biến bằng cách đếm số dòng giữa lần đầu và lần cuối gọi biến (bao gồm cả lần đầu và lần cuối). Đây là ví dụ khi để biến "sống quá dai".
1 // khởi tạo tất cả các biến
2 recordIndex= 0;
3 total = 0;
4 done = false;
...
26 while ( recordIndex < recordCount ) {
27 ...
28 recordIndex = recordIndex +1; **<--1**
...
64 while (!done) {
...
69 if ( total > projectedTotal ) { **<--2**
70 done=true; **<--3**
(1) lần cuối gọi recordIndex.
(2) lần cuối gọi total.
(3) lần cuối gọi done.
Đây là live time của 3 biến trong ví dụ này:
- recordIndex: ( line 28 - line 2 + 1 ) = 27
- total: ( line 69 - line 3 + 1 ) = 67
- done: ( line 70 - line 4 + 1 ) = 67
- Live Time trung bình: ( 27 + 67 + 67 ) / 3 ≈ 54
Còn đây là bản sửa lại:
...
25 recordIndex = 0; <-- 1
26 while ( recordIndex < recordCount ) {
27 ...
28 recordIndex = recordIndex + 1;
...
62 total = 0; <-- 2
63 done = false; <-- 2
64 while ( !done ) {
...
69 if ( total > projectedTotal ) {
70 done = true;
(1)Khởi tạo recordIndex được chuyển xuống từ dòng 3.
(2)Khởi tạo total và done được chuyển xuống từ dòng 4 and 5
Đây là thời gian tồn tại của biến trong ví dụ:
- recordIndex: ( line 28 - line 25 + 1 ) = 4
- total: ( line 69 - line 62 + 1 ) = 8
- done : ( line 70 - line 63 + 1 ) = 8
- Live Time trung bình: ( 4 + 4+ 8 ) / 3 ≈ 7
Như đã thấy, ví dụ thứ 2 tốt hơn ví dụ thứ nhất bởi khởi tạo biến được biểu diễn gần nhau ở nơi biến được sử dụng. Sự khác nhau của hai live time trung bình thật sự rất đáng chú ý: 54 và 7
Một con số làm nên sự khác biệt code tốt và không tốt? Mặc dù các nhà nghiên cứu chưa có con số nào chính xác nhưng hoàn toàn có thể tin rằng: live time với span càng nhỏ thì càng tốt.
Nếu bạn thử có áp dụng khái niệm span và live time vào biến toàn cục thì bạn sẽ được con số rất lớn, đây là một trong nhiều lí do ta nên tránh dùng biến toàn cục khi có thể dùng biến cục bộ.
Một số thủ thuật làm nhỏ "địa bàn" của biến
Đây là một số thủ thuật đặc biệt để giúp bạn thu gọn tầm hoạt động của biến lại:
Khởi tạo biến được dùng trong vòng lặp ngay trước vòng lặp đó chứ đừng đặt ở đầu hàm chứa vòng lặp đó. Việc này sẽ có ích khi bạn sửa đổi vòng lặp, bạn sẽ nhớ phải thay đổi cả các khởi tạo vòng lặp.
Chỉ gán giá trị cho biến khi nó được sử dụng. Nếu gán bừa bãi sau này bạn sẽ gặp khó khăn khi đọc code.
Nhóm những câu lệnh liên quan lại với nhau. Các ví dụ sau sẽ minh hoạ việc nhóm các tham chiếu đến các biến lại với nhau làm cho chúng dễ tìm thấy hơn. Đầu tiên là ví dụ của việc vi phạm nguyên tắc này:
void SummarizeData(...) {
...
GetOldData( oldData, &numOldData ); **<-- 1**
GetNewData( newData, &numNewData ); |
totalOldData = Sum( oldData, numOldData ); |
totalNewData = Sum( newData, numNewData ); |
PrintOldDataSummary( oldData, totalOldData, numOldData );
PrintNewDataSummary( newData, totalNewData, numNewData );
SaveOldDataSummary( totalOldData, numOldData );
SaveNewDataSummary( totalNewData, numNewData ); **<-- 1**
...
}
(1)Các câu lệnh sử dụng 2 tập hợp biến
Trong ví dụ này, bạn ném các biến oldData, newData, numOldData, numNewData, totalOldData vào một block. Ví dụ sau sẽ để 3 biến trong một block:
void SummarizeData( ... ) {
GetOldData( oldData, &numOldData ); <-- 1
totalOldData = Sum( oldData, numOldData ); |
PrintOldDataSummary( oldData, totalOldData, numOldData );
SaveOldDataSummary( totalOldData, numOldData ); <-- 1
...
GetNewData( newData, &numNewData ); <-- 2
totalNewData = Sum( newData, numNewData ); |
PrintNewDataSummary( newData, totalNewData, numNewData );
SaveNewDataSummary( totalNewData, numNewData ); <-- 2
...
}
(1)Các câu lệnh sử dụng oldData.
(2)Các câu lệnh sử dụng newData.
Khi mà code bị lỗi thì việc tách như trên sẽ giúp ta sửa dễ dàng sửa lỗi hơn và cả khi bạn muốn chia code này ra thành các hàm thì rõ ràng cách viết thứ hai này sẽ tách nhanh hơn cách thứ nhất.
Phá những nhóm câu lệnh có liên quan để đưa vào các hàm riêng. Một biến trong một hàm sẽ có khuynh hướng làm span và live time nhỏ hơn. Bằng cách phá nhóm câu lệnh ra như vậy bạn làm dảm "địa bàn hoạt động" của các biến rất nhiều.
Bắt đầu với phạm vi nhỏ nhất có thể, chỉ mở rộng khi nào thật cần thiết. Việc thu nhỏ phạm vi một biến đã được mở rộng thì khó hơn rất nhiều việc mở rộng phạm vi của biến đang có phạm vi nhỏ, đưa một biến toàn cục thành một biến class thì đương nhiên sẽ khó hơn rất nhiều với việc đưa biến class thành biến toàn cục. Với lí do đó, hãy ưu tiên cho phạm vi nhỏ nhất có thể: một biến cục bộ cho mỗi vòng lặp, một biến cục bộ cho một hàm, rồi biến private cho class, rồi đến biến protected, rồi package( nếu ngôn ngữ của bạn hỗ trợ nó), và tất nhiên biến toàn cục là phương án cuối cùng!
Đối với nhiều lập trình viên, việc thu nhỏ phạm vi biến hay không còn phụ thuộc vào quan điểm của họ về "sự tiện lợi" và "sự dễ quản lí". Một số lập trình viên dùng rất nhiều biến toàn cục vì nó có thể truy cập từ bất cứ đâu mà không bị mấy cái quy định về phạm vi biến làm phiền. Trong suy nghĩ của họ, sự tiện lợi đó quan trọng hơn cả những rủi ro cực kì rắc rối.
Những lập trình viên khác thì thích giữ các biến càng cục bộ càng tốt bởi vì nó giúp cho code dễ quản lí hơn. Càng ẩn đi nhiều thông tin thì bạn càng dễ tập trung hơn vào một thứ trong một thời điểm. Như thế có thể giúp bạn tránh những lỗi do bạn quên mất một trong số hàng tá thông tin mà bạn phải nhớ cùng lúc.
Sự khác biệt giữa triết lí "thuận tiện" và triết lí "dễ quản lí" nhấn mạnh sự khác nhau trong viết và đọc chương trình. Mở rộng hết mức phạm vi biến có thể làm cho việc viết chương trình dễ hơn nhiều nhưng một chương trình mà một hàm cũng có thể gọi các biến từ bất kì đâu sẽ rất khó để tìm ra đâu là yếu tố mà những hàm đó thật sự sử dụng. Trong một chương trình như thế, bạn không thể hiểu chỉ một hàm mà phải hiểu toàn bộ chương trình. Một chương trình như vậy thì rất khó đọc, rất khó Debug, cũng như rất khó để chỉnh sửa.
Tóm lại, bạn nên khai báo/sử dụng mỗi biến làm sao cho nó được nhìn thấy trong đoạn code nhỏ nhất có thể. Rồi bạn sẽ nhận ra rằng bạn rất hiếm khi bạn sử dụng một biến toàn cục một cách đơn thuần
Bài viết có tham khảo một số nội dung trong cuốn Code Complete và các tài liệu khác
Part 1 ở đây: http://daynhauhoc.com/t/lam-sao-de-su-dung-bien-hieu-qua-hon-part1/6921
Part3 ở đây: http://daynhauhoc.com/t/lam-sao-de-su-dung-bien-hieu-qua-hon-part3/6992
Part4 ở đây: http://daynhauhoc.com/t/lam-sao-de-su-dung-bien-hieu-qua-hon-part-4/7063