C# - public static void Main()

  • 8493
  • 0
  • C#
  • 2021-04-25

接觸C#也一陣子了,一開始其實沒甚麼太大的障礙,語法的不同花點時間習慣一下就好。寫的程式逐漸變複雜時,開始會考慮將部分程式碼,放到其他類別或方法,開始延伸很多啊哩阿紮的概念,而且開始發現,有的時候程式語法看起來沒啥問題,也可以run,但結果就不是我要的阿!! 魔鬼就藏在細節裡,不搞清楚每個細節的影響,寫出來的程式,就像在玩猜杯子的遊戲,有時候答案是對的,有時候答案又變錯的(崩潰中…) ! 看了不少資料後,發現這篇的主角會有點多,如果各別解釋,我覺得大概還是會停留在一知半解的狀態,所以我盡可能的,用連貫的思考脈絡寫這篇網誌。

 

 

C# - 魔鬼小細節 public static void Main()


打開Visual Studio,建立一個新的C#專案時,應該可以在裡面找到下列的程式碼。

class Program
{
    static void Main(string[] args)
    {
    }
}

透過關鍵字class,可以將整個程式碼,用{ }內打包成同一個類別後續就可以重複呼叫。在C#或Java的程式中,class裡面又會包覆著主程式,通常名稱是Main,一開始學習時,會把所有要執行的程式碼都丟進去。Main()的前面,還會出現public、static、void 等關鍵字。接下來,我們以C#實做一個程式碼,要記錄狗的資料,例如名稱、顏色、歲數。

static void Main(string[] args)
{
    string dog_1, dog_2;
    string color_1, color_2;
    int age_1, age_2;

    dog_1 = "Lucky";
    color_1 = "yellow";
    age_1 = 3;

    dog_2 = "Lucy";
    color_2 = "black";
    age_2 = 5;

    Console.WriteLine(dog_1);
    Console.WriteLine(color_1);
    Console.WriteLine(age_1);
    Console.WriteLine(dog_2);
    Console.WriteLine(color_2);
    Console.WriteLine(age_2);
}

要先宣告一堆變數、再把數據存到變數,要打印也必須逐行撰寫。2隻狗的程式碼就有18行,當有更多的狗要輸入屬性時,那不就瘋掉!透過物件導向設計,可以幫助我們更容易完成這個範例。在主程式之外,新增一個新的class,取名叫Dog,並宣告Dog內部的屬性與方法,每個屬性或方法都是一個物件。在這裡不難發現,透過關鍵字void,就可以定義函數[function],或稱方法[method],在設計時,也可以傳遞參數給function。

class Dog
{
    string name;
    string color;
    int age;
    
    //傳遞參數的方法
    void life(int age)                                      
    {
        Console.WriteLine("狗狗的壽命剩下約" + (15-age) + "歲");
    }
    
    //不傳遞參數的方法
    void barking()                                          
    {
        Console.WriteLine(name + "在叫");
    }

    //印出所有Dog的屬性
    void print()
    {
         Console.WriteLine(name);
         Console.WriteLine(color);
         Console.WriteLine(age);
    }
}

在Main()裡,要使用Dog裡面的物件時,要先輸入下列程式碼。

Dog myDog;             //產生一個Dog類別,名稱叫myDog,概念如int X
myDog = new Dog();     //賦予myDog一個記憶體空間

或

Dog myDog = new Dog(); //上述兩步驟縮寫法

宣告完之後,就可以直接指定屬性並賦值,甚至可以寫一個方法打印出所有結果,這樣是不是方便很多?

myDog.name = "Lucky";
myDog.color = "yellow";
myDog.age = 3;

實際執行後,會有「由於其保護層之故,所以無法存取」報錯訊息,MSDN中有存取修飾符(Access Modifier)的說明,class內的變數、函數,預設為private,所以只能在Dog這個類別裡使用,所以需要在Dog的類別裡,幫各屬性、方法前面加上public,才能讓Main()取用Dog內的物件。嘗試輸入第二筆資訊,打印時居然只印出一隻狗的資訊!這個問題,要了解參數傳遞的內容與細節才能說明白了。

 

型別 到底是甚麼?


根據資料的不同,可以分成int、float、string等資料型別,如果以記憶體的特性來區分,可以分成實值型別(value type)參考型別(reference type)實值型別的變數,不同變數會在不同的記憶體區塊儲存資料,而參考型別,如同名稱所示,是使用參考的記憶體位址將值取出來用,所以不同變數,對應到的記憶體區塊是一樣的,這麼說有點複雜。我們先來看下列的程式碼。

static void function(int x, int y) 
    {
        int temp = x;
        x = y;
        y = temp;
        Console.WriteLine("function內的結果: x = {0}, y = {1}", x, y);
    }

static void Main(string[] args)
    {
        int x, y;
        x = 10;
        y = 20;
        Console.WriteLine("呼叫function前的結果: x = {0}, y = {1}", x, y);
        function(x, y);
        Console.WriteLine("呼叫function後的結果: x = {0}, y = {1}", x, y);
        Console.ReadKey();
    }
}

//呼叫function前的結果: x = 10, y = 20;
//呼叫function內的結果: x = 20, y = 10;
//呼叫function後的結果: x = 10, y = 20;

值傳遞(call by value)
在Main()中定義x,y變數,另外寫一個function,功能為將x與y的值互換。這裡不新增class,直接在Main()外寫funcion,實際執行後,會有「需要有物件參考,才可以使用非靜態欄位」報錯訊息,這裡先在function前面加上static,後面小節再對static作補充。我們先釐清整個程式碼的流程。

1.          在Main() 宣告了變數 x, y;

2.          印出呼叫function前的結果

3.          把x, y 傳給 function 並打印function內的結果。

4.          印出呼叫function後的結果。

卻發現步驟2與步驟4的結果居然一樣!關鍵就在步驟3的參數傳遞的細節裡。執行function(x, y)前...

會先取得原本Main()內定義的 x與y的 !

會先取得原本Main()內定義的 x與y的 !

會先取得原本Main()內定義的 x與y的 !

因為很重要所以要說三遍,透過形參的方式,把原本Main()內的 x與y的值,傳遞給function()內的 x與y,所以等於是複製了一份資料。Main( )內的 x與y稱為實參,而test ( )內的 x與y,則稱為形參實參與形參各自有不同的記憶空間,所以其實function是真的有將x與y互換,只是換的x與y不是Main()裡面的x與y,通常呼叫要function傳遞參數的時候,只要是實值型別(像是int、float、char這類),就是單純的把value複製一份到Function中,傳遞方與接收方互不影響,這裡提到的方式稱為傳遞值,接著來聊聊傳遞位址的工作方式。

傳遞位址(call by address )
一樣先看下述程式,在程式碼中新增一個新的Initial類別。順道一提,通常class名稱開頭會用大寫,物件會用小寫。在Main()中把Initial()初始化、命名為A並賦值。後續流程跟call by value的範例一樣,我們來看看結果變怎樣。

Class Initial 
{
    public int x, y;
}

static void function(Initial initial)
{
    int temp = initial.x;
    initial.x = initial.y;
    initial.y  = temp;
    Console.WriteLine("function內的結果: x = {0}, y = {1}", initial.x, initial.y);
}

static void Main(string[] args)
{
    Initial initial = new Initial();

    initial.x = 10;
    initial.y = 20;

    Console.WriteLine("呼叫function前的結果: x = {0}, y = {1}", initial.x, initial.y);
    function(initial);
    Console.WriteLine("呼叫function後的結果: x = {0}, y = {1}", initial.x, initial.y);
    Console.ReadKey();
}

//呼叫function前的結果: x = 10, y = 20;
//呼叫function內的結果: x = 20, y = 10;
//呼叫function後的結果: x = 20, y = 10;

function傳值不是call by value嗎? 怎麼結果居然轉換了,關鍵就是: 我們是新增一個類別,並把值存在類別的的物件裡。簡單說就是,參考型別(如array、object等)會直接傳遞變數或物件的位址。所以這個案例,在function()執行後會顯示互換成功。另外,附上其他大神整理的圖表,看了後應該會更清楚。

 

變數(Variable)


這裡又有個新發現,為何同一份程式裡,我用了兩個initial卻不會出錯,所以這小節要說明甚麼是變數。變數又可以分成全域變數、區域變數、及靜態變數

全域變數(Global Variable)
簡單說,全域變數就是宣告在Main()以外的變數,這個變數可以被所有程式使用。哪裡算是最外面呢? 我們來一個個亂測試一下(笑)。

namespace ConsoleApp1
{
    int a;                 //命名空間不能直接宣告
    class Program
    {
        int b;             //顯示使欄位唯讀,無法賦值給b
        b = 0;
        int c = 0;         //成功

        static void Main(string[] args)
        {
            int d;         //肯定成功

        } 
    }
}

區域變數(local Variable)
區域通常會用{ }包覆程式碼,不同區域的變數,可以使用同樣的命名。所以在call by value的範例中,才可以在兩個方法中,都使用Initial做為變數命名。區域變數會在該區塊程式碼被執行時,才開始建立相關的變數及分配儲存空間,並保存到該區塊工作結束後失效,若要更精準的解釋上述call by value的程式流程,

進入Main() - > 儲存Main()中的區域變數 -> 取出Main()的區域變數的值 -> 將值傳給fucntion並賦值給function內的變數 -> 儲存function區域變數 -> 執行變數交換 -> 結束function() 並讓function的區域變數失效 ->繼續執行Main()下一行程式。

靜態變數(Static Variable)
加上static的變數或方法,在程式一開始就會被建立,要使用時並不需要透過new產生實體物件。在C#中,static的用法有點像全域變數,要小心使用,因為static的變數生命週期,會保存到整個程式結束。在Call by value的案例中,因為是屬於同一個class,而且沒有另外用class包住function,無法用new來產生物件,錯誤訊息才會說,需要有物件參考,才可以使用非靜態欄位所以直接加上static讓function變成靜態方法,就可以直接使用了