iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
1
Software Development

你會十五種程式語言?不,我會十五種HelloWorld.為了避免這種狀況,因此寫了這篇:淺入淺出十五種程式語言系列 第 5

C# 如同精靈般優雅且高貴(下)

我們繼續昨天的程式吧
昨天我們已經找出使用十六進位表達時每個位置的數值,現在我們要將這些數值轉成十六進位的表達方式

以下是一種寫法

if (Math.Pow(16,2)*i+Math.Pow(16,1)*j+Math.Pow(16,0)*k == num)
{
    var I =""; //先將要放入的字串宣告出來
    if (i<10)
    {
        I = i.ToString(); //小於10的情況不用轉換,ToString()則是將數值轉換成字串的方法
    }else{
        switch (i)
        {
            case 10: I = "A"; break; //接著依照數值的不同會轉成不同的字串
            case 11: I = "B"; break;
            case 12: I = "C"; break;
            case 13: I = "D"; break;
            case 14: I = "E"; break;
            case 15: I = "F"; break;
        }
    }
   Console.WriteLine("{0} {1} {2}",I,j,k);
}

如果你要驗證你的寫法是否正確,可以嘗試執行並輸入數值 4095

https://ithelp.ithome.com.tw/upload/images/20200905/20127836jhAv03Ut6C.png

這時你應該會看到第一個數值被成功轉換成 F 了

好,在你急著將加上一堆程式碼以便將第二個跟第三個數值也轉成字串前先等一下

我必須先跟你說一個事實

工程師們就是一群懶惰蟲

這種必須複製貼上然後還要修改數值的動作我們是不會做的

我們會使用 Function(也就是方法)
之前我們使用的是系統自動幫我們產生的方法,現在我們要來製作自己的方法

把剛才添加的程式碼改到與 Main 同級的方法下面,並修改如下

static string returnAE(int number){
    if (number<10)
    {
        return number.ToString();
    }else{
        switch (number)
        {
            case 10: return "A";
            case 11: return "B";
            case 12: return "C";
            case 13: return "D";
            case 14: return "E";
            case 15: return "F";
            default : return "";
        }
    }
}

慢著,什麼是同級?

之前有說過方法必須包在 class 裡面,因此要宣告方法的話必須也宣告在同一個層級

https://ithelp.ithome.com.tw/upload/images/20200905/20127836h0xxWIgRmt.png

從上圖可以看到 Main 跟 returnAE 的 function 都是在名叫 Program 的 class 內,這時候我們就可以稱他們為同級
如果是同級的 function 在使用上就不必特地加上 class 的名稱
這邊如果聽不懂也沒關係,之後都會在物件的章節詳細解釋

接著我們來解釋 function 的結構
https://ithelp.ithome.com.tw/upload/images/20200905/20127836qeJZFb39SY.png

  • 方法在 C#裡面方法有分靜態跟非靜態(也有人稱動態)
    靜態的話可以不用宣告就使用,詳細的分法我會在之後講物件的時候詳細說明,
    現在就先記得 static 表示方法是靜態可以直接使用即可
  • 回傳的型別表示當程式執行完後回傳值的型別,如果沒有回傳值就是 void 就可以了(之後會示範寫法),
    這邊因為我們主程式(就是 main 那隻)需要知道代表各個數值的字串,因此需要有回傳值並且型別為字串(string)
  • 方法名稱表示當你需要用這個方法的時候需要怎麼呼叫他
  • 輸入值代表當你使用這個方法時需要輸入的數值(當然也可以不輸入,不過比較少見)
    這邊因為我們需要將原本的數字轉成字串,因此輸入值的型別當然就是 int 拉,然後你可以重新命名你的變數

為什麼要重新命名變數? 直接使用原本的名稱不好嗎?

這是因為函數有可能不會只使用一次,而每次使用時變數名稱可能都不一樣,因此我們會統一一個名稱
這就好像你爸在外面會有各種不同稱呼但是回到家之後大家都統稱叫爸爸一樣

我該怎麼告訴方法我的回傳值?

就是使用 return
當你使用 return 時方法會在將你在後面接著的值回傳後結束
以我們上面已經寫好的方法為例,當我們已經用 if 確認輸入值小於 10 時,就不需要判斷判斷輸入值是 10~15 的哪一個
於是我們可以直接在判斷完後回傳

if (number<10)
{
    return number.ToString();  //直接回傳,不用繼續執行後面的程式
}

好,這裡要注意到在程式語言的世界裡面 8 跟"8"是完全不同的東西,
因此當我們要回傳字串時需要將他們轉型(就像當初你把使用者的輸入值轉成數字一樣)
使用 ToString()就可以將數字轉成字串了

另外在方法裡面有一個很重要的觀念是如果有回傳值,那就一定要回傳

static string returnAE(int number){
    if (number<10)
    {
        return number.ToString(); //小於10的在這裡就會回傳
    }else{
        switch (number)
        {
            case 10: return "A";
            case 11: return "B";
            case 12: return "C";
            case 13: return "D";
            case 14: return "E";
            case 15: return "F"; //10~15會在這之前回傳
            default : return ""; //其他的要在這裡回傳,就算是空值也一樣
        }
    }
}

雖然我們都知道在你的主程式裡面由於是用迴圈去跑 0~15,因此不會有大於 15 的情形,
但是在這隻副程式裡面他並不知道你的主程式是什麼情況,所以你必須假設他有大於 15 的情形
於是我們給他一回傳一個"",一個裡面什麼都沒有的字串(空值是一個很有趣的概念,之後會介紹)
這樣我們就接回了所有在這裡有可能的情況,讓每種情況都有回傳值

你可以試試看如果把 default 那一行拿掉的話會發生什麼事

讓我們來使用剛熱好的方法吧

修改你的主程式把副程式放進去

if (Math.Pow(16,2)*i+Math.Pow(16,1)*j+Math.Pow(16,0)*k == num)
{
   Console.WriteLine("{0} {1} {2}",returnAE(i),returnAE(j),returnAE(k));
}

注意到

returnAE(i)

這裡就會幫你把十進位的 i 轉換成他應該在十六進位時該顯示的樣子

由於你的程式有回傳值,因此你也可以把他看成單純的字串並放入 Console.WriteLine 裡

現在你可以執行你的程式拉
也可以到這裡去驗證看看你的程式正確與否

這樣我們的程式的功能就完成一半了

什麼?才一半嗎?

沒錯,因為我們還要完成將十六進位轉成十進位的功能

我們先整理一下程式碼, 把原本在 switch 內的也拿出去當方法吧
整個結果對程式沒有影響,但是這樣會比較容易看

static void Main(string[] args)
{
    Console.WriteLine("Tell me what you want to do:");
    Console.WriteLine("(1)T to H (2)H to T");
    String choose = Console.ReadLine();
    switch (choose)
    {
        case "1":
            Console.WriteLine("Please enter the number:");
            String number = Console.ReadLine();
            t2h(number); //這裡拿出去做成方法
            break;
        case "2":
            Console.WriteLine("Your input is 2");
            break;
        default:
            Console.WriteLine("Wrong selection");
            break;
    }
}

static void t2h(String number){
    Int64 num = Int64.Parse(number);
    for (int i = 0; i < 16; i++)
    {
        for (int j = 0; j < 16; j++)
        {
            for (int k = 0; k < 16; k++)
            {
                if (Math.Pow(16,2)*i+Math.Pow(16,1)*j+Math.Pow(16,0)*k == num)
                {
                    Console.WriteLine("{0} {1} {2}",returnAE(i),returnAE(j),returnAE(k));
                }
            }
        }
    }
}

我們繼續擴充主程式吧

switch (choose)
{
    case "1":
        Console.WriteLine("Please enter the number:");
        String number = Console.ReadLine();
        t2h(number);
        break;
    case "2":
        Console.WriteLine("Please enter the number:");
        number = Console.ReadLine(); //這裡不用給他型別,給的話反而會報錯
        h2t(number);
        break;
    default:
        Console.WriteLine("Wrong selection");
        break;
}

我們不需要給 number 型別,因為在 case"1"時就已經給他了
但是每種語言的特性不一樣,像是 golang 的話每個 case 都是分開的,所以需要分別宣告型別
這部份等我們談 golang 時會提到

然後是 h2t 的方法本體

static void h2t(String input){
    var pow = input.Length;
    int i = 0;
    while (i < pow)
    {
        Console.WriteLine(input[i]);
        i++;
    }
}

方法的結構就不重複說明了,我們來談談新觀念

var pow = input.Length;

.Length 會把字串的長度取出來放入變數裡面,這邊我們的變數叫 pow
稍微舉個例子,當我們輸入為 AAA 時,由於 AAA 有三個字,因此字串長度為 3
接著我們要依照這三個字分別去做運算,因此我們需要迴圈

int i = 0;
while (i < pow)
{
    Console.WriteLine(input[i]);
    i++;
}

while 是另外一種迴圈的結構,通常用於不是很明顯的知道實際的執行次數時
當 while 內的條件符合時,就會不斷執行內部的程式
於是我們先宣告一個 i 為 0,當他比字串長度還小時,就會執行裡面的程式,並且最後會++
這樣我們的迴圈就會從 i=0 開始執行到 i=pow-1
至於為什麼是從 0 開始而不是 1,為什麼是到 pow-1 而不是 pow 則要看

Console.WriteLine(input[i]);

這邊有一個重要的概念

陣列

陣列的用意在於將相同的元素放入一個集合內,並且可以根據集合的索引(index)去將需要的資料取出
比較明顯的例子是學生的學號,當你把班上同學的名字依照學號的順序排好時其實就是一個學生名字的陣列
然後使用學號就可以將相對應的同學名字取出了

https://ithelp.ithome.com.tw/upload/images/20200905/20127836GjfldTvsji.png

在這個過程中我完全不需要知道哪個學生的學號是多少,
因為只要我在哪個位置取出,他就會是哪個學號
這邊要注意的是索引是從 0 開始而不是 1,這也是為什麼長度為 3 的陣列最後一個索引卻是 2 的原因

OK, 讓我們來看看我們的輸入吧

我們一直以來在說的字串其實是一連串的字元陣列
https://ithelp.ithome.com.tw/upload/images/20200905/20127836VQfmGGemmA.png

先等等,什麼是字元?

字元就是最小的符號單位,他只有單純的一個符號,絕不會多於一個(跟字串一樣可能是空的)
通常我們為了跟字串分開表示會使用兩個'
比方說 'A' 就是一個字元,而 "A" 就是一個字串

兩個不一樣嗎?

在程式語言不一樣,字元沒辦法擴充,你只能修改他,
而字串由於是一連串的字元組成,你可以修改或擴充他,讓他變成"ABC"

由於"ABC"是一個陣列,因此你要找他的第幾個字元就很簡單了
[0]就能找到 A(大部分的程式語言順序都是從 0 開始)
這樣我們就能依照順序去處理每個字元代表的數值

執行程式然後隨意輸入十六進位的數字看看吧

https://ithelp.ithome.com.tw/upload/images/20200905/20127836HC5XSgPwv4.png

這樣你就將輸入的字依照順序找出來了
現在我們只需要將每一個十六進位所代表的含意轉成數字再加總即可

現在修改你的程式碼

static void h2t(String input){
    double output = 0;
    var pow = input.Length;
    int i = 0;
    while (i < pow)
    {
        output += AEreturn(input.Substring(i,1))*Math.Pow(16,pow-i-1);
        i++
    }
    Console.WriteLine(output);
}

static int AEreturn(string number){
    try {
        return Convert.ToInt32(number);
    }
    catch{
        switch (number){
            case "A":
                return 10;
            case "B":
                return 11;
            case "C":
                return 12;
            case "D":
                return 13;
            case "E":
                return 14;
            case "F":
                return 15;
            default:
                Console.WriteLine("Some thing wrong!");
                return 0;
        }
    }
}

我們先從比較簡單的 AEreturn 的方法開始說起
從他的定義

static int AEreturn(string number){

我們可以發現這是一個將輸入的字串轉成數字的方法

然後你應該會注意到這個涵蓋了整個方法的結構

try{

}
catch{

}

這個是 C#的錯誤處理方式

錯誤處理方式?

就是只當程式發生非預期狀況時的處理方式
注意到這一行

return Convert.ToInt32(number);

這行的目的是將輸入的文字轉成數字
如果輸入的是"5","6","7"之類的就能夠正確的轉型成 5,6,7
但是有時候輸入的數字並不是數字的文字,比方說ABC等等沒辦法轉成數字的文字
這時候就產生了非預期的狀況(預期狀況是輸入的是可以轉成數字的字串)
這時這個錯誤狀況就會由 catch 接住(如果程式內有的話)並且由 catch 內的程式繼續往下運行
來到

switch (number){
    case "A":
        return 10;
    case "B":
        return 11;
    case "C":
        return 12;
    case "D":
        return 13;
    case "E":
        return 14;
    case "F":
        return 15;
    default:
        Console.WriteLine("Some thing wrong!");
        return 0;
}

這時我們就可以將剛才轉型錯誤的字串繼續判斷是哪一個十六進位所代表的符號,
並且回傳這個符號所代表的數字
當然,使用者有可能輸入非法的十六進位符號,因此我們需要 default 來處理非法的情況

這是一個 C#內很有趣的機制,有興趣可以看這裡

好,回到方法 h2t 吧

static void h2t(String input){
    double output = 0;
    var pow = input.Length;
    int i = 0;
    while (i < pow)
    {
        output += AEreturn(input.Substring(i,1))*Math.Pow(16,pow-i-1);
        i++
    }
    Console.WriteLine(output);
}

注意到這裡

double output = 0;

double 是一種型別,我們稱之為浮點數,為什麼要用他往下會提到

output += AEreturn(input.Substring(i,1))*Math.Pow(16,pow-i-1);

+=? 這是什麼新符號?

就如同我之前說的,工程師們其實都是一種很懶惰的生物
如果我們要對一個數值增加 2,比方說

i=i+2

i 這個字必須重複打兩次

所以我們會這麼做

i+=2

這樣 i 這個數值就會自動去找+=後面的數值並且加到自己身上

就像我上面說的,我們必須把十六進位所有符號一一取出並計算成十進位之後加總
因此我們需要+=來幫助我們
當然,你也可以這麼做

output = output + AEreturn(input.Substring(i,1))*Math.Pow(16,pow-i-1);

看到+=後面那麼一長串數字不必害怕,就像當初學數學一樣,我們將計算式用*分開

AEreturn(input.Substring(i,1))  *  Math.Pow(16,pow-i-1);

先看左邊的吧
AEreturn 就是我們剛才完成的方法,裡面的

input.Substring(i,1)

是一個將字串切割的方法
第一個參數 i 表示要取出字串的起點,第二個參數表示取出的長度
由於我們已經定義過 for 迴圈的 i 會遍歷每個輸入的字,因此這裡會將輸入值的每個字都取出過一次

為什麼不用 input[i]? 剛才不是就這麼用嗎?

理由在於字串與字元在某些時候的處理方式不一樣
input[i]是直接去字串內取出某一個位置的字元,所以取出來的東西當然是字元
但是 Substring 由於並不知道你取出來的長度會是多少,有可能會超過 1,於是取出來的東西就會是字串(只是在這裡長度剛好是 1)
所以雖然兩者在這裡取出的數值看似相同,其實裡面裝的東西是不一樣的,我們等一下會實做一個範例

回到計算式,

AEreturn(input.Substring(i,1))

這樣我們就能夠把十六進位上每個位置的數值都轉成十進位了
但是我們還要考慮到每個數值在不同位置上都要乘上這個位置所代表的幕次
就像 111 雖然三個數字都是 1,但是三個位置的 1 所表達的數值肯定是完全不同的對吧

讓我們回想一下計算的方式

https://ithelp.ithome.com.tw/upload/images/20200905/201278364w0kqJsSrM.png

如果這個數值長度為 4,此數值最左側的數值就要再乘上 16^(長度-1)
於是我們有了下面的計算式

Math.Pow(16,pow-i-1)

還記得嗎? 我上面說過陣列的索引第一個都是 0
因此你可以在上面計算式中帶入如果索引為 0(也就是處理第一個數值)時計算值為多少
是不是就是 16^(長度-1)

這樣,我們就分別計算出十六進位上每個符號的數值並且做加總了

在我們執行程式之前,讓我們玩一下字串跟字元的差異

在 while 迴圈內

while (i < pow)
{
    Console.WriteLine(Convert.ToInt32(input[i])); //加上這一行
    output += AEreturn(input.Substring(i,1))*Math.Pow(16,pow-i-1);
    i++;
}

加上的這一行會幫你把字元轉成 int 並且印出來
注意喔,由於我們是臨時加上這一行的,並不像我們自己寫的方法會把例外狀況做另外的處理
因此你必須確保你輸入的每一個字都可以轉型成 int

現在執行看看吧

https://ithelp.ithome.com.tw/upload/images/20200905/20127836M53mmjt978.png

欸欸欸 為什麼字元的 1 轉型成 int 後會變成 49 而 0 會變成 48 阿?

這跟 C#的資料結構有關
不過我們留到討論型別的時候再提吧
有興趣的人可以去看這裡

把我們剛才試驗用的那一行刪掉,這樣你的程式就完成了

使用

dotnet run

執行它吧

我們複習一下我們在 C#內學了什麼吧

  • C#的程式結構
  • 印出/讀取
  • 邏輯控制
    • switch,每一個條件使用break分開
    • if ()裡面放條件,{}裡面放需要執行的程式
  • 迴圈控制
    • for 如果有明顯要執行幾次的時候用這個
    • while 如果你當前不知道要執行幾次的時候可以用這個
  • 創造自己的方法,回傳/不回傳值
  • 字串轉型成 int,int 轉型成字串
  • 計算上常用的冪次計算
  • 異常處理

一般程式語言最常用的語法就是上面這些了,所以之後在介紹其他語言時也會以這些為主

下一個語言我們來學跟 C# 差相當遠的 python 吧


上一篇
C# 如同精靈般優雅且高貴 (上)
下一篇
python 簡單又不失強大
系列文
你會十五種程式語言?不,我會十五種HelloWorld.為了避免這種狀況,因此寫了這篇:淺入淺出十五種程式語言30

1 則留言

1
skycover
iT邦新手 5 級 ‧ 2020-09-05 00:09:24

如果有任何寫不清楚或是觀念沒有很明白的話請留言告知我
會盡快補上

如果有任何寫錯的地方也麻煩留言告知我
會盡快修正

感謝各位

ultrahsin iT邦新手 5 級 ‧ 2021-05-16 21:36:37 檢舉

感謝skycover大製做如此高品質又帶有人性的作品。描述得簡潔又清楚,好到註冊帳號+完成新手任務,為求讚嘆一番。期待讀完您所有文章。

兩個疑似小筆誤供參考:

  1. 部分h2t function中的while迴圈少了i++
  2. "好,回到方法t2h吧"之標題推估是指h2t
skycover iT邦新手 5 級 ‧ 2021-05-17 17:24:37 檢舉

已修正,感謝回覆!

我要留言

立即登入留言