Làm sao để sử dụng biến hiệu quả hơn (part2)

Bài [viết trước][1] 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 :blush:

#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 :blush:


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 :blush:


Part 1 ở đây: Làm sao để sử dụng biến hiệu quả hơn (part1)
Part3 ở đây: Làm sao để sử dụng biến hiệu quả hơn (part3)
Part4 ở đây: Làm sao để sử dụng biến hiệu quả hơn (part 4)
[1]: Làm sao để sử dụng biến hiệu quả hơn (part1)

16 Likes

Bài viết chất lượng quá. Phạm vi biến(scope) chắc mọi người cũng để ý khi lập trình một thời gian, nhưng mà lại ít để ý tới “live time”

Hãy giữ cho biến “sống” trong thời gian ít nhất có thể

Rất thích chuỗi bài viết này.

3 Likes

phải là sống dai chứ nhỉ :smile:

2 Likes

Đã sửa ạ, em hay bị lỗi chính tả lắm :smile:

2 Likes

wow, chất lượng thật. Mình bắt đầu thấy hứng thú với cuốn này từ bài viết này

2 Likes

a= b + c; <— Tham chiếu đến a lần 2, Tham chiếu đến b lần 2, Tham chiếu đến b lần 2

Tham chiếu đến c lần 2 chứ cậu, dòng cuối cùng ấy =))

2 Likes

:+1: Thanks, đã edit :blush:

bài viết bổ ích và hay quá. Nhưng cho mình hỏi thuật ngữ “reference” trong bài trên có nghĩa là gì thế? có phải thuật ngữ “reference” trong mỗi ngôn ngữ lập trình là khác nhau

1 Like

Nó là tham chiếu đến biến đó bạn, ví dụ:

a=1;
x=a+b;
j=a+v;

Code trên có tham chiếu đến các biến x, b, j, a,v :blush:


A, có phải cái bạn thắc mắc liên quan đến cái này không?, mình chưa học đến đoạn này nên không rõ, hoá ra có hẳn một khái niệm reference trong C++. Trong cuốn Code Complete ở chương 10 thì mình chỉ thấy tác giả dùng từ reference như là một lời gọi đến biến vậy, tức là dòng nào có biến đó thì coi là reference :blush:


@ltd Anh Đạt nghiên cứu em vụ này phát, không biết là em có nhầm lẫn gì không :smile:

Ít nhất là có khác nhau trong C và C++.

  1. C không có khái niệm reference, nhưng ta cũng có thể gọi là reference pointer thay cho pointer. Hai cái này là một, nhưng hiếm khi nào người ta dùng reference pointer mà chỉ gọi pointer cho nó gọn.
  • reference pointer
int x = 3;
int *ptr = &x;
  • dereference pointer
*ptr = 4; // change the value of x
  1. C++ có khái niệm reference, xem video @thanhmssl10 share để rõ hơn :slight_smile:
2 Likes

I moved a post to a new topic: Khi nào nên dùng biến static, live time của nó?

bài viết quá hay…có cuốn ebook code complete mà chưa rãnh để đọc, thấy mọi người ca tụng nó quá nên cũng nôn :smile:

1 Like

Đang phải phát triển thêm tính năng của 1 app android của 1 ông đề cao tính tiện lợi của code đây, nhiều lúc muốn đạp cho cái, đọc căng cả mắt mới hiểu ổng đang viết gì, biến dùng loạn xạ, tên biến, tên hàm thì dị

2 Likes

Thanks bạn.
Trước tôi hay viết theo cách 2 của bạn, nhưng khi vào dự án đều có 1 rule: “khai báo biến lên đầu hàm / đầu chương trình” (2 cty khác nhau đều có rule này)
mn có phản biện gì không nhỉ?

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?