接觸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變成靜態方法,就可以直接使用了。