C# 入門指南
單元 20 - 程式結構與重構
C# 程式檔案的結構如下圖,主要分成兩大部分
「using 區」就是引入 .NET 程式庫 (library) 相關內容的地方,「namespace 區」則是程式檔案的相關定義,凡是類別 (class) 、結構 (structure) 、介面 (interface) 等等,都在這裡面定義,如果這個命名空間 (namespace) 需要可以執行,就要定義含有 Main() 方法 (method) 的類別。
我們一路走來的範例程式都是這樣的程式結構來寫,要記住的有三大項
- 開頭引入 .NET 程式庫;
- 其餘程式相關定義都放在 namespace 裡;
- 可執行程式要定義含有 Main() 方法的類別。
接下來繼續討論重構,重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護
簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。
我們利用數學公式設計一種演算法 (algorithm) ,利用這種演算法來建立密碼表,然而這個演算法並不是最理想的方式,因為這個公式建立的密碼表只有
5 × 10 = 50
依據 a 可能有 5 種數字, b 可能有 10 種數字,因此這個演算法最多就只能產生 50 種密碼表。
可是 26 個英文字母的排列會有
1 × 2 × 3 × ⋯⋯ × 25 × 26 = 4.0329 × 1026
由上可見有非常非常多種。
有沒有更理想的方式呢?有的,事實上我們可以直接攪亂字串中字母的順序,這需要另一個攪亂順序的演算法,外加密碼表欄位 (field) 應該要封裝 (encapsulate) 成屬性 (property) ,然後編碼 ToEncode() 跟 ToDecode() 都按照 Unicode 原始數值來做計算也太不直覺了,事實上有更簡單直覺的處理方式。
下面是重構後的程式碼
using System;
namespace EncryptNamespace
{
class Encrypt
{
// 宣告私有欄位
private string _letter, _code;
// 存取 _letter 的屬性
public string Letter {
get {return _letter;}
set {
_letter = value;
}
}
// 存取 _code 的屬性
public string Code {
get {return _code;}
set {
// value 必須是符合英文字母表的字串
if (value is string && value.Length == 26)
{
_code = value;
}
// 不符合條件不做設定,印出錯誤訊息
else
{
Console.WriteLine("密碼字串錯誤,請修改程式。");
}
}
}
// 沒有參數的建構子,建立隨機密碼表
public Encrypt()
{
Letter = "abcdefghijklmnopqrstuvwxyz";
Code = Shuffle(this.Letter);
}
// 有參數的的建構子,由參數建立密碼表
public Encrypt(string newCode)
{
Letter = "abcdefghijklmnopqrstuvwxyz";
Code = newCode;
}
// 攪亂字串順序
string Shuffle(string s)
{
// 將參數字串轉換成字元陣列
char[] s_array = s.ToCharArray();
// 建立隨機物件
Random r = new Random();
// 取得字串長度
int n = s.Length;
// 依序攪亂字元陣列的順序
while (n > 1)
{
n--;
int k = r.Next(n + 1);
var v = s_array[k];
s_array[k] = s_array[n];
s_array[n] = v;
}
// 最後回傳將字元陣列合併的字串
return new string(s_array);
}
// 編碼方法
public string ToEncode(string s)
{
// result 為處理過程中的暫存字串
string result = "";
// 逐一取得字元進行處理
foreach (char i in s)
{
// 判斷是否為英文小寫字母
if (this.Letter.Contains(i.ToString()))
{
result += this.Code[this.Letter.IndexOf(i)];
}
// 不是英文小寫字母,直接將字元附加到 result 之後
else
{
result += i;
}
}
// 回傳結果
return result;
}
// 解碼方法
public string ToDecode(string s)
{
// 建立暫存字串
string result = "";
// 逐一取得字元進行處理
foreach (char i in s)
{
// 判斷是否為英文小寫字母
if (this.Letter.Contains(i.ToString()))
{
result += this.Letter[this.Code.IndexOf(i)];
}
// 不是英文小寫字母,直接將字元附加到 result 之後
else
{
result += i;
}
}
// 回傳結果
return result;
}
}
class Program
{
static void Main(string[] args)
{
// 建立密碼物件 e
Encrypt e = new Encrypt();
// 印出密碼表
Console.WriteLine(e.Code);
// 設定測試字串
string s1 = "There is no spoon.";
// 印出測試字串
Console.WriteLine(s1);
// 進行編碼
string s2 = e.ToEncode(s1);
// 印出編碼後的字串
Console.WriteLine(s2);
// 進行解碼
string s3 = e.ToDecode(s2);
// 印出解碼後的字串
Console.WriteLine(s3);
// 設定長度不符的新密碼物件
Encrypt e2 = new Encrypt("no");
// 設定長度相符的新密碼物件
Encrypt e3 = new Encrypt("qazwsxedcrfvtgbyhnujmikolp");
// 再次進行編碼
string s4 = e3.ToEncode(s1);
// 印出編碼後的字串
Console.WriteLine(s4);
// 再次進行解碼
string s5 = e3.ToDecode(s4);
// 印出解碼後的字串
Console.WriteLine(s5);
}
}
}
//《程式語言教學誌》的範例程式
// http://kaiching.org/
// 專案:EncryptNamespace
// 檔名:Program.cs
// 功能:示範重構後的 Encrypt 類別
// 作者:張凱慶
Encrypt 類別中,首先封裝 _letter 與 _code , _letter 是英文小寫字母表, _code 則是攪亂順序的密碼表
// 宣告私有欄位
private string _letter, _code;
為什麼需要英文小寫字母表 _letter 呢?因為利用 _letter 與 _code 兩者索引值對照的方式,就可以實現編碼及解碼。
下面建立 Letter 與 Code 兩個屬性,分別存取 _letter 與 Code
// 存取 _letter 的屬性
public string Letter {
get {return _letter;}
set {
_letter = value;
}
}
// 存取 _code 的屬性
public string Code {
get {return _code;}
set {
// value 必須是符合英文字母表的字串
if (value is string && value.Length == 26)
{
_code = value;
}
// 不符合條件不做設定,印出錯誤訊息
else
{
Console.WriteLine("密碼字串錯誤,請修改程式。");
}
}
}
注意 Code 中的 set 進行條件檢查
// value 必須是符合英文字母表的字串
if (value is string && value.Length == 26)
這是說 _code 是會符合長度 26 的字串 (string) ,只要是符合長度 26 的字串都可以當作密碼表,並不一定全都要是小寫英文字母。
建構子 (constructor) 有兩個,沒有參數 (parameter) 的建構子利用 Shuffle() 攪亂 Letter 順序,產生隨機的密碼表,有參數的建構子利用參數建立密碼表
// 沒有參數的建構子,建立隨機密碼表 public Encrypt() { Letter = "abcdefghijklmnopqrstuvwxyz"; Code = Shuffle(this.Letter); } // 有參數的的建構子,由參數建立密碼表 public Encrypt(string newCode) { Letter = "abcdefghijklmnopqrstuvwxyz"; Code = newCode; }
之所以要有參數的建構子版本,這是因為如果將密碼表存檔,載入後可以直接用密碼表設定 Encrypt 物件。
Shuffle() 方法用來攪亂字串順序
// 攪亂字串順序
string Shuffle(string s)
{
// 將參數字串轉換成字元陣列
char[] s_array = s.ToCharArray();
// 建立隨機物件
Random r = new Random();
// 取得字串長度
int n = s.Length;
// 依序攪亂字元陣列的順序
while (n > 1)
{
n--;
int k = r.Next(n + 1);
var v = s_array[k];
s_array[k] = s_array[n];
s_array[n] = v;
}
// 最後回傳將字元陣列合併的字串
return new string(s_array);
}
事實上,字串是不可變的,這意思是說一但建立字串物件後,就不能再去改變該字串物件中的字元,所以這裡將字串轉換成字元陣列 (character array) ,字元陣列就可以替換陣列 (array) 元素,也就是字元 (character)
// 將參數字串轉換成字元陣列
char[] s_array = s.ToCharArray();
攪亂字元陣列順序採用 while 迴圈,這叫做 Fisher–Yates shuffle 演算法,可以有效地攪亂陣列元素的順序
// 依序攪亂字元陣列的順序
while (n > 1)
{
n--;
int k = r.Next(n + 1);
var v = s_array[k];
s_array[k] = s_array[n];
s_array[n] = v;
}
簡單講, Fisher–Yates shuffle 演算法隨機取得陣列元素,上例用 Random 的 Next() 取得隨機位置 k ,然後把該陣列元素放到暫存變數 v ,接著把 n 跟 k 的元素互換位置,因此像是小寫英文字母表,只要跑 26 次就可以得到攪亂順序的字元陣列,非常的有效率。
編碼方法 ToEncode() 改成用索引值對照的方式,注意到編碼工作實際上只有一行程式碼
result += this.Code[this.Letter.IndexOf(i)];
因為 Letter 中的索引值的字元,剛好就是密碼表中要轉換的字元,相對解碼方法 ToDecode() 中的解碼工作,也只有一行程式碼
result += this.Letter[this.Code.IndexOf(i)];
變成用 Code 的索引值取得 Letter 的字元
最後執行部分,除了將測試字串編碼、解碼外,另外增加第二個建構子的測試程式,除了長度不符的字串也有長度相符的字串
static void Main(string[] args)
{
// 建立密碼物件 e
Encrypt e = new Encrypt();
// 印出密碼表
Console.WriteLine(e.Code);
// 設定測試字串
string s1 = "There is no spoon.";
// 印出測試字串
Console.WriteLine(s1);
// 進行編碼
string s2 = e.ToEncode(s1);
// 印出編碼後的字串
Console.WriteLine(s2);
// 進行解碼
string s3 = e.ToDecode(s2);
// 印出解碼後的字串
Console.WriteLine(s3);
// 設定長度不符的新密碼物件
Encrypt e2 = new Encrypt("no");
// 設定長度相符的新密碼物件
Encrypt e3 = new Encrypt("qazwsxedcrfvtgbyhnujmikolp");
// 再次進行編碼
string s4 = e3.ToEncode(s1);
// 印出編碼後的字串
Console.WriteLine(s4);
// 再次進行解碼
string s5 = e3.ToDecode(s4);
// 印出解碼後的字串
Console.WriteLine(s5);
}
執行結果如下
C:\Encrypt> dotnet run |
tgkqjpzamvnhedrlbuycwfxois |
There is no spoon. |
Tajuj my dr ylrrd. |
There is no spoon. |
密碼字串錯誤,請修改程式。 |
Tdsns cu gb uybbg. |
There is no spoon. |
C:\Encrypt> |
這樣 Encrypt 專案的程式碼是不是更容易理解了呢?接下來我們就要進入 GUI 的部份了,在此之前先來認識一下 Visual Studio Community 2019 吧!
相關教學影片
中英文術語對照 | |
---|---|
演算法 | algorithm |
陣列 | array |
字元 | character |
字元陣列 | character array |
類別 | class |
建構子 | constructor |
封裝 | encapsulate |
欄位 | field |
識別字 | identifier |
介面 | interface |
程式庫 | library |
方法 | method |
命名空間 | namespace |
參數 | parameter |
屬性 | property |
重構 | refactoring |
軟體 | software |
原始碼 | source code |
字串 | string |
結構 | structure |
重點整理 |
---|
1. C# 程式結構主要是定義命名空間,然後在命名空間中放所有的定義,如果這個命名空間是可以執行的,就要在類別中定義 Main() 方法。 |
2. 重構是指重新整理程式碼,讓程式更易於維護。 |
3. 常見的重構技術包括整理屬性、方法,挑出類別的共通特性定義父類別等等。 |
問題與討論 |
---|
1. 什麼是重構?為什麼要替開發好的程式進行重構? |
2. Encrppt 類別進行了哪些重構?為什麼要作這些重構? |
練習 |
---|
1. 承接上一個單元的猜數字遊戲,將新專案寫在 Exercise2001 中,替建構子新增一個參數 digit ,並且將 digit 預設為 4 ,注意呼叫 Substring() 也要改成 digit 。 |
2. 承上題,將新專案寫在 Exercise2002 中,將 Guess 類別的欄位 answer 、 a 、 b 封裝,並設定相關屬性,注意原本在 Program 類別的 Main() 呼叫的欄位也要同時改成屬性。 |