2013年5月2日 星期四

Code Complete II《軟體建構之道 2》#12 讀書心得與整理

第十二章 主要資料型別
Fundamental Data Types


12.1 數字概述
Numbers in General

以下是使用數字較不易出錯的提醒:
  • 避免使用「magic number」
  • 必要時,可以hard code 0和1(本身有隱喻)
  • 遞增、遞減、迴圈初始值
  • 除法小心除0
  • 讓型別轉換更明顯
  • 避免混合型別比較
  • 注意編譯器warning




12.2 整數
Integers

除法
    最簡單的方法:重新組識運算式
檢查溢位
    如果你的整數會在數年間穩定成長,就要考慮進來
檢查中繼結果的溢位
    算式最後的結果不是你唯一要擔心的數字

12.3 浮點數
Floating-Point Numbers

精確度到小數點後7位
避免數量級差距很大的數字做加減運算
    排序這些數字,從最小的開始運算,減少誤差
避免相等比較
    相同的浮點數不一定以相同的方式保存在記憶體
    替代方案:用範圍的true, false來取代==
//Java Example of a Bad Comparison of Floating-Point Numbers
double nominal = 1.0;
double sum = 0.0;
for ( int i = 0; i < 10; i++ )
um += 0.1;if ( nominal == sum )
System.out.println( "Numbers are the same." );
else
System.out.println( "Numbers are different." ) ;
將這段程式,改成下面的寫法
//Java Example of a Routine to Compare Floating-Point Numbers
double const ACCEPTABLE_DELTA = 0.00001;
boolean Equals( double Term1, double Term2 )
{
if ( Math.abs( Term1 - Term2 ) < ACCEPTABLE_DELTA )
return true;
else
return false;
}
處理四捨五入的錯誤
  1. 使用double
  2. 使用BCD變數
    • 缺點:慢
  3. 用int來替代float
    • 讓全部的運算都乘上相同的倍數,再除回來。
      ex: 10.123×1000 = 10123
檢查語言及函式庫對特定資料型別的支援

12.4 字元及字串
Characters and Strings

避免不斷重覆出現的magic characters和strings
    使用字串常數,統一管理
    使用全域變數
    統一管理,方便翻譯成不同語言
    限制記憶體開發環境,獨立字串儲存空間
小心off-by-one錯誤
    字串可以像陣列一樣索引化,有超出'\0'的問題要小心
了解您的語言和開發環境如何支援Unicode
開發初期就決定程式支援語系
選擇適合的編碼
    (只用英文)→ASCII→ISO8859→(用英文以外的語系)→Unicode
決定一致的字串的型別轉換策略

C語言的字串Strings in C
注意字串指標和字元陣列的差異
小心字串使用等號
    正確的用法:使用strcmp(), strcpy(), strlen()
    等號通常是指某種指標的錯誤
    在c語言中看見這樣的陳述式
    StringPtr = "Some Text String";
    不是將內容複製到StringPtr,而是將指標指在第一個'S'。
使用前綴詞的命名原則分辨
    ps = ptrString
    ach = arrayChar
字串長度宣告成constant+1
    很容易忘記constant長的字串,要宣告constant+1的字元陣列
    解決方式:用字串常數取代字元陣列的用法。多宣告一個byte,避免使用最後一個byte。

用null初始化字元陣列,避免在最後忘記放null
    靜態陣列宣告,直接放初始值 = { 0 };
    動態陣列宣告,用calloc取代malloc
      malloc    不會初始化為0
      calloc    會初始化為0
使用字元陣列取代c的陣列指標
    用字元陣列存放所有字串變數,編譯器也可以在出錯時給你警告

12.5 布林變數
Boolean Variables

要用錯,很難;善用,程式會變美

簡化複雜判斷
如果if (陳述式) 的陳述式是一團泥,陳述式的判斷拆開成有意義的布林變數,可增加程式碼可讀性
(將布林表示式賦序一個變數,使判斷含意變明顯)
//Java Example of Boolean Test in Which the Purpose Is Unclear
if ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) || ( elementIndex == lastElementIndex ) )
{
...
}
改成
//Java Example of Boolean Test in Which the Purpose Is Clear
finished = ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) );
repeatedEntry = ( elementIndex == lastElementIndex );
if ( finished || repeatedEntry )
{
...
}
判斷的意思會更容易浮到字面上

需要的話,創造自己的布林型別
C++、Java、Visual Basic都有bool的型別,但是像c這種常見的語言卻沒有。
//C Example of Defining the BOOLEAN Type
typedef int BOOLEAN; // define the boolean type
本書原文並無繁簡中的這個例子
//C Example of Defining the BOOLEAN Type Using an Enum
enum Boolean {
True = 1,
False = (!True)
};


12.6 列舉型別
Enumerated Types

是一種允許在物件的類別包含的每個資料成員用英文描述的資料
Public Enum Color
Color_Red
Color_Green
Color_Blue
End Enum
Public Enum Country
Country_China
Country_England
Country_France
Country_Germany
Country_India
Country_Japan
Country_Usa
End Enum
Public Enum Output
Output_Screen
Output_Printer
Output_File
End Enum
增加可讀性
    if chosenColor = 1
    寫得更有可讀性,可改成
    if chosenColor = Color_Red
    在此出現只有繁簡中版才有的例子,而原文版沒出現的例子
    但因為之前VB的例子已具代表性,所以就不在此介紹

增加可靠度
    編譯器會檢查使用列舉型別的變數,只允許該變數使用該列舉的成員。
輕鬆新增/移除新項目
    列舉對應的數字,並不需要管它,在新增移除列舉成員項目時,程式碼只要管「英文」部份的正確性,而所對應的數字會在編譯時重新調整
列舉可當作布林變數的替代方案
    將true/false或1/0命名成英文,成為有意義明顯的變數。warning
列舉成員命名為ELSE,可當作無效值(case中的default,if中的else)

列舉的第一項和最後一項,當作迴圈控制變數使用
    enmu trafficLightColor{ ColorBegin = 0, Red = 0, Green, Yellow, ColorEnd };
    for (int i = ColorBegin; i < ColorEnd; ++i)
    //...
列舉、陣列、迴圈互相搭配使用(補充)
    VB的索引,是以1開始,在此不適用,C、C++、Java都適用於這種方式。
    陣列宣告時,中括號內要填入的數字往往代表的是總數。
    在迴圈計數時,計數器的範圍,往往設定成「小於總數」。
    在列舉宣告時,如果這個列舉的使用要配合迴圈回陣列,即可使用這種方式。
    enmu trafficLightColor{ TC_Red = 0, TC_Green, TC_Yellow, TC_Total };
    //用在陣列
    int something[TC_Total ];
    //用在迴圈
    for (int i = 0; i < TC_Total; ++i)
    //...
列舉的第一個項目保留為無效
    (因為第一個項目可以訂為0)
定訂一致的列舉用法(第一項的命名是無效?還是第一項?那最後一項呢?)

注意全指定值列舉,在迴圈使用
    enum Color
    {
    Color_InvalidFirst = 0,
    Color_Red = 1,
    Color_Green = 2,
    Color_Blue = 4,
    Color_InvalidLast = 8
    };
    for (int i = Color_InvalidFirst; i < Color_InvalidLast ;++i)
    //...
    此例的3, 5, 6, 7就是無效值。

如果你的語言不支援列舉
If Your Language Doesn’t Have Enumerated Types

像Java,可以使用類別,或者用全域變數,都可以達到相同的效果
// Java Example of Simulating Enumerated Types
// set up Color enumerated type
class Color
{
private Color() {}
public static final Color Red = new Color();
public static final Color Green = new Color();
public static final Color Blue = new Color();
}
// set up Country enumerated type
class Country
{
private Country() {}
public static final Country China = new Country();
public static final Country England = new Country();
public static final Country France = new Country();
public static final Country Germany = new Country();
public static final Country India = new Country();
public static final Country Japan = new Country();
}
// set up Output enumerated type
class Output
{
private Output() {}
public static final Output Screen = new Output();
public static final Output Printer = new Output();
public static final Output File = new Output();
}


12.7 常數
Named Constants

參數化程式,讓軟體更軟。
宣告時使用常數
    任何一種可能變化之事物進行集中控制的技術,是減少維護工作量的好技術
避免字面表示常數,即使是安全的
    For i = 1 To 12
    profit( i ) = revenue( i ) – expense( i )
    Next
    上面這個例子,12是代表一年的十二個月嗎?
    如果是,改成下列的寫法是不是比較好?
    For i = 1 To NUM_MONTHS_IN_YEAR
    profit( i ) = revenue( i ) – expense( i )
    Next
    那索引用的i 是不是也可以給予更有意義的變數名稱?
    For month = 1 To NUM_MONTHS_IN_YEAR
    profit( month ) = revenue( month ) – expense( month )
    Next
    嗯!現在看起來,程式似乎是可以了。
    那,如果把1也替換掉,要怎麼改寫呢?
    用列舉的方式將1月到12月的寫法取代掉。

    For month = Month_January To Month_December
    profit( month ) = revenue( month ) – expense( month )
    Next
    這樣就變成全英文的程式碼了,可以表達的意思更加明確。
使用適當作用域的變數,來模擬常數
    像Java這種不支援列舉的語言,使用區域作用域→類別作用域→全域作用域的順序來運用常數。
一致的使用常數
    不要一邊使用常數變數,一邊使用將數字寫死的程式寫法,要改也不是修改一處改全部,這是很危險的事。


12.8 陣列
Arrays

陣列是一組相同資料型別的項目,可使用索引直接存取。
確定所有的索引,都在陣列的邊界內
考慮使用循序存取的容器類別取代陣列
    陣列的「隨機存取」產生可靠性降低的問題。
    循序結構: sets、stacks、and queues
檢查陣列的端點
  • 第一個項目
  • 第一個項目的Off-by-one錯誤
  • 最後一個項目
  • 最後一個項目的Off-by-one錯誤
  • 陣列的中繼項目
多維陣列,少用[i][j],提高可讀性,降低索引互相干擾

在c語言中

使用ARRAY_LENGTH()巨集搭配陣列
    宣告這個
    //C Example of Defining an ARRAY_LENGTH() Macro
    #define ARRAY_LENGTH( x ) (sizeof(x)/sizeof(x[0]))
    下面實作這個
    //C Example of Using the ARRAY_LENGTH() Macro for Array Operations
    ConsistencyRatios[] =
    { 0.0, 0.0, 0.58, 0.90, 1.12,
    1.24, 1.32, 1.41, 1.45, 1.49,
    1.51, 1.48, 1.56, 1.57, 1.59 };
    ...
    for ( RatioIdx = 0; RatioIdx < ARRAY_LENGTH( ConsistencyRatios ); RatioIdx++ );
    ...
    這個方法適合用在一維陣列上,修改陣後,不需修改迴圈上的數字。
    陣列全長/陣列一個單元 = 陣列個數。
只可在宣告的function內使用,function互傳值時無效(補充)
    陣列在宣告時,索引的操作,其實是指標在記憶體中的移動次數,當遇到function呼叫,需要傳遞陣列(其實不可以這樣做)時,都是傳遞指標,再操作指標位移,以達到陣列的操作。


12.9 創造自有型別(型別別名)
Creating Your Own Types

這是語言所能提供的最有力的功能之一
C/C++使用者請善用typedef
其它支援的使用者也請務必使用等效功能

使用時機: 
  1. 表達經緯度,在使用double還是float舉棋不定。
  2. 構築薪資系統,員工名字最多30個字元,但是有些員工還是會超過
  3. 結合自我構築型別及資訊隱藏的方法
創造自己型別參考的原因:
  1. 修改更容易
  2. 修改的資訊集中在一個地方
  3. 讓編譯器可以檢查
  4. 彌補語言的弱點

為什麼自行構築型別的範例要用Pascal和Ada?
Why Are the Examples of Creating Your Own Types in Pascal and Ada?

    Pascal和Ada和恐龍一樣,已經在走向滅亡。取代庫們的語言都更好用,但是在這方面卻反而退步了。
    例子:希望可以確保currentTemperature的賦值,正確性提昇的用法
    currentTemperature: INTEGER range 0..212
    這一句包含了溫度是整數表示
    int temperature;

    再進一步,像這樣的類型宣告
    type Temperature is range 0..212;
       ...
    currentTemperature: Temperature;
    充許currentTemperature賦值的動作受Temperature 的定義所規範。

    以下是重新翻譯的一段:
    「當然,程式設計師可以創造一個Temperature類別來執行相同的語義。(自己刻Ada的自動強制執行語法),但是,從用一行程式碼創造一個簡單的資料型別到用一個類別來創造,是跨了好大一步,許多情況之下,程式設計師會寫創造一個簡單的型別,但不會創造一個類別」

構築你的UDT原則
Guidelines for Creating Your Own Types

    針對真實世界所面臨的問題本身來取名字
    避免內建的資料型別
    不要用相同的意思來重覆定義內建的資料型別
    為可攜性定義替代型別
    考慮用 class取代typedef

沒有留言:

張貼留言