2017年8月25日 星期五

C printf 編譯順序,由右到左


關鍵是編譯氣的解析順序 !!!





  1. #include <stdio.h>   
  2. void main()   
  3. {   
  4.        int a=5,b=2;  
  5.        printf("%d %d\n",b=a+1,a=a+1);  
  6. }  




輸出結果是:7 6
而不是:6 6
這是因為printf函數的計算是從右向左進行的。


C#: BackgroundWorker 非同步 /背景多執行緒 舉例




[Ref]

在專案開發時,多多少少一定會處理到大量資料或檔案存取的時候,然而每當這些工作一開始的時候,
介面總是會因為工作尚未完成而遭到鎖死或失去回應,對使用者來說十分不友善,
而且執行情況也無法被觀察,而且結果像是桃太郎一樣突然出現,又無法捉摸。
這種情況,小可在程式開發的時候也時常遇到,有沒有什麼方法可以讓程式正常回應又保持作業呢?
我們可以另外開一條執行緒來執行這些特別的工作,而最簡單又快速的方式就是使用 BackgroundWorker 了!

什麼是 BackgroundWorker 呢?簡單來說和另開新的 Thread 情況相差不遠,且可以將指派的工作分配到背景,
讓前景的介面不會因為資源被吃光而停止回應,但用途還不只這麼單調,更多用法還等待著呢!
只是這種用途是最時常被用上,也最直覺就是了。
聽起來很有趣,那這個東西該怎麼用?在使用之前小可要做一些解說。

舉著情境例子,今天小可是老師,正在某間教室裡面做監考,不過面前有一大疊考卷等著批改,
而這批考卷必須在一堂課內解決,但在批改的時候又得要監考,
監考相較於批改考卷來得嚴謹,但批改考卷又是重要的工作,而且非常分神又費時,這該怎麼做才好?
雖然有點不太貼切,但大家在學生期間,應該都有幫老師改考卷的經驗吧。
小可馬上撥打手機,指派一個值得信任的學生來教室,代替小可批改考卷,而自己就可以專注在考場監考了。

好像是個滿不錯的解決方法,但這個和這篇文章的主題有什麼關係?
上面的情境,改考卷是個粗重的工作,小可把它指派給他人作業,自己則全神在考場抓作弊。
在主題中,就好像主程式把費時的工作,轉交給 BackgroundWorker 執行,主程式則繼續監視介面的一舉一動。

好像有點關聯了,不過這還不夠。
小可在監考,應該要可以隨時打手機問考卷改到哪裡吧?如果全部改完了就叫他回撥通知,之後把考卷抱來給小可。
同理,主程式可以隨時得知工作進行的進度,如果工作完成了,就把結果回傳給主程式。

嗯!這樣就足夠了!雖然廢話有點多,不過這是希望能幫助大家了解這個 BackgroundWorker 到底在幹嘛。

上面說了這麼多,是要為 BackgroundWorker 的用途做基本解說,因為小可也不喜歡用硬梆梆的語言來說明。
BackgroundWorker 的主要用途就這樣,這就開始要做連接囉!

我們該怎麼把工作交給 BackgroundWorker 呢?很容易的!
首先,我們只要宣告一個 BackgroundWorker 物件,或是在介面設計建立,做一些設定就可以開始交派作業了。
BackgroundWorker bgwWorker = new BackgroundWorker();
bgwWorker.DoWork += new DoWorkEventHandler(bgwWorker_DoWork);
bgwWorker.RunWorkerCompleted += newRunWorkerCompletedEventHandler(bgwWorker_RunWorkerCompleted);
bgwWorker.ProgressChanged += newProgressChangedEventHandler(bgwWorker_ProgressChanged);
bgwWorker.WorkerReportsProgress = true;
bgwWorker.WorkerSupportsCancellation = true;
宣告沒問題,那麼那些設定值是什麼意思?
WorkerReportsProgress:希望能隨時知道考卷改到哪嗎?
WorkerSupportsCancellation:想要隨時叫他停下批改作業嗎?
就這麼簡單,一個是要 BackgroundWorker 回報工作進度,一個是可以隨時停止 BackgroundWorker 的作業。
如果是用介面拖拉的方式建立的話,只要在右下邊的屬性欄更改設定就行了。

好吧,我們懂了,也宣告了,那麼我麼就開始交派作業吧!
bgwWorker.RunWorkerAsync();
RunWorkerAsync() 這個方法,表示要 BackgroundWorker 新開一條執行緒,並且觸發 DoWork 事件,
所以我們只要把工作內容寫在DoWork()方法裡面,BackgroundWorker 就會開始執行我們的工作了。
private void bgwWorker_DoWork(object sender, DoWorkEventArgs e)
{
    還沒改的考卷  = (考卷)e.Argument;
    改考卷
}
當然我們可以在 RunWorkerAsync 裡面加上我們的參數,而參數使用 e.Argument 取出,記得要轉型。

考卷改到哪了?
那就加入 ReportProgress() 來問問吧!
private void bgwWorker_DoWork(object sender, DoWorkEventArgs e)
{
    改考卷
    bgwWorker.ReportProgress(進度到哪了);
    繼續改考卷
    e.Result = 改完的考卷
}

private void bgwWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressbar1.Value = e.ProgressPercentage;
}
使用了ReportProgress() 方法後會觸發ProgressChanged事件,所以只要在這個方法裡表現進度就行了。
值得一提的,參數範圍是整數 0~100,表示進度的百分比,而怎麼建立這個值就端看工作比值了,
而這個值最適合用進度條來顯示了,程式碼中也直接使用了。
記得這個進度條要在介面設計裡面先放好。
工作完成的結果可以指派在 e.Result 裡。

考卷改完了!記得會自動抱回來吧!
當工作完成的時候,會自動觸發 RunWorkerCompleted 事件,所以我們只要在這個方法裡面做收尾就可以了。
private void bgwWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    教室.講桌 = (考卷)e.Result;
}
而工作完成的結果放在 e.Result 裡面,記得要轉型才能使用哦。

臨時反悔,不想要幫忙改了?那就一通電話休了他吧!
private void btnStop_Click(object sender, EventArgs e)
{
    bgwWorker.CancelAsync();
}
通常我們會設定一個按鈕,按下去就會停止工作。而執行 CancelAsync() 方法就會通知有停止的要求。
而在工作中的學生就要隨時注意手機有沒有響了,可以透過 CancellationPending 這個屬性來得知有沒有要求。
private void bgwWorker_DoWork(object sender, DoWorkEventArgs e)
{
    改考卷
    bgwWorker.ReportProgress(進度到哪了);
    if (bgwWorker.CancellationPending)
    {
        生氣丟下考卷
        return;
    }
    繼續改考卷
    e.Result = 改完的考卷
}
不過,如果工作停止了,他可是會生氣的,他可以把考卷堆就此丟下不管,或是他會乖乖把剩下的的交回教室,
看工作是怎麼交代的了哦。

以上就是這個血汗學生 BackgroundWorker 的情境解說,雖然介紹方式很牽強,
不過還是希望能幫助大家對這個好用的東西能有初步認識,大家可以去找更詳細的介紹文章,
說不定能有達到啟蒙的效果,或是當個笑話看看就好囉。其實是寫給未來忘記用法的自己。
另外上面的程式碼都是虛擬碼,直接複製是不能用得哦!

2017年8月24日 星期四

執行緒初探--使用BackgroundWorker



[Ref]
.Net Framework在多執行緒的支援上提供了許多方便的類別,而BackgroundWorker則是一項非常容易用來撰寫多執行緒的類別, 它不僅和System.Windows.Forms.Timer一樣也在工具箱提供了可拖曳使用的元件,並且提供了ProgressChanged事件使得更動主畫面控制項可以不需藉由Control.Invoke﹝有些時候Invoke的概念對初學者會有或多或少邏輯上的困擾﹞,個人覺得這個元件滿適合初學者當做撰寫多執行緒的入門。 
        .Net Framework在多執行緒的支援上提供了許多方便的類別,而BackgroundWorker則是一項非常容易用來撰寫多執行緒的類別, 它不僅和System.Windows.Forms.Timer一樣也在工具箱提供了可拖曳使用的元件,並且提供了ProgressChanged事件使得更動主畫面控制項可以不需藉由Control.Invoke﹝有些時候Invoke的概念對初學者會有或多或少邏輯上的困擾﹞,個人覺得這個元件滿適合初學者當做撰寫多執行緒的入門。
       在MSDN文件庫中是這樣形容BackgroundWorker元件的:﹝引述自 MSDN BackgroundWorker 元件概觀﹞
       BackgroundWorker 元件會提供您在不同於應用程式主要 UI 執行緒的執行緒上,非同步 (「在幕後執行」) 執行耗時作業的能力。若要使用 BackgroundWorker,您只要告訴它要在幕後執行哪種耗時的背景工作方法,然後呼叫 RunWorkerAsync 方法。對執行緒的呼叫會繼續正常執行,直到工作方法非同步執行。當方法完成時,BackgroundWorker 會藉由引發 RunWorkerCompleted 事件來警示呼叫執行緒,此事件會選擇性地包含作業的結果。
       BackgroundWorker的常用屬性:
1. CancellationPending 屬性:﹝Boolean值﹞當BackgroundWorker執行個體呼叫了CancelAsync方法取消背景作業後,這個值會變成true;這個屬性是唯讀的。
2. IsBusy 屬性:﹝Boolean值﹞表示BackgroundWorker執行個體正在執行中;這個屬性是唯讀的。
3. WorkerReportsProgress 屬性 :﹝Boolean值﹞這個值可以設定BackgroundWorker執行個體是否可以藉由呼叫ReprotProgress方法來引發ProgressChanged事件。
4. WorkerSupportsCancellation 屬性 :﹝Boolean值﹞這個值可以設定BackgroundWorker執行個體是否可以藉由呼叫CancelAsync方法來中斷背景作業。
      BackgroundWorker的常用方法:
1. CancelAsync 方法:呼叫這個方法會送出停止背景作業的要求,並將 CancellationPending 屬性設為 true。
2. ReportProgress 方法:呼叫這個方法會引發 ProgressChanged 事件,於是可以將對於主執行緒使用者介面﹝UI﹞控制項的處理程序撰寫於ProgressChanged 事件程序碼中。 如果不熟悉委派與Control.Invoke,以此方法呼叫ProgressChanged是一種不錯的替代方式。
3. RunWorkerAsync 方法 :呼叫這個方法將會引發 DoWork 事件開始執行背景執行緒,需要在背景中執行的程序即可撰寫於DoWork事件程序碼中。這個方法有兩個多載,其中一個多載可以將參數以Object形式傳遞給DoWork事件。
      BackgroundWorker的常用事件:
1. DoWork 事件:將背景執行緒啟動時要執行的程序內容寫在此事件當中。
2. ProgressChanged 事件:在DoWork事件程序中,若有需要更動主畫面控制項時,可以呼叫Reportgress方法來引發此事件,而將控制項更動處理的程序內容寫在此事件中。
3. RunWorkerCompleted 事件:當DoWork作業已完成、取消或引發例外狀況時會引發這個事件。
      先以一最簡單的例子來觀察BackgroundWorker是如何運作的,此程式碼在範例中的Form2。先從工具箱拖曳一個BackgroundWorker元件,並在畫面上布置四個Label物件、一個ProgressBar物件與一個Button物件。為了使得此Backgroundworker可以呼叫ReportProgress方法,故在Form2的Load事件中設定BackgroundWorker1.WorkerReportsProgress = True。接著在Button1的Click事件中撰寫以下程序:
      If BackgroundWorker1.IsBusy = True Then
           MessageBox.Show("背景作業執行中,請稍候!")
       Else
           Label1.Text = "開始於:" & Now().ToString("HH:mm:ss.fffffff")
           BackgroundWorker1.RunWorkerAsync()
           Label2.Text = "Click事件結尾:" & Now().ToString("HH:mm:ss.fffffff")
       End If
     以上這簡單程序的內容表示當按下Button1時先檢查BackgroundWorker1是否正在執行背景作業中,如果為False則在Label1上顯示Button1.Click的時間,接著呼叫RunWorkerAsync方法執行背景作業‧然後在Label2顯示Button1.Click的程序結尾時間。
     在DoWork事件中則撰寫以下程序:
       Dim i As Integer
        For i = 1 To 100
            BackgroundWorker1.ReportProgress(i)   <==呼叫ReportProgress方法以引發ProgressChanged事件,參數為百分比的分子部份。             System.Threading.Thread.Sleep(10)
        Next
      在ProgressChanged事件中撰寫以下程序:
       ProgressBar1.Value = e.ProgressPercentage  <==依據ReportProgress方法的傳入參數改變Progressbar的Value        Label3.Text = "進度i=" & e.ProgressPercentage & "/ 時間:" & Now.ToString("HH:mm:ss.fffffff") <==顯示每一次ProgressBar改變的時間
      在RunWorkerCompletedBKW01事件中撰寫:Label4.Text = "結束於:" & Now().ToString("HH:mm:ss.fffffff")
       整個Form2的程式主要是在彰顯Backgroundworker與主執行緒會在不同步的方式下進行,由左方的圖形可以發現Button1.Click事件在14:53:57.9687500秒就已進行到最後一行﹝和開始於的時間居然還相同,表示中間的過程似乎還小於千萬分之一秒﹞,但整個背景作業依然繼續進行,直到14:53:59.6562500才引發了RunWorkerCompleted事件。
       這個範例的迴圈數因為沒有很多,所以執行的時間也並沒有很長,各位想像一下,如果今天把BackgroundWorker排除,並且將迴圈數放大,以單執行緒的形式來執行這個程式,主畫面就會有一段長時間處於看起來像是已經停止反應的狀態,使用者恐怕很容易誤認程式已經當掉了。所以在遇到這種非不得已要長時工作的程序就非得使用多執行緒的方式來處理會比使用單一執行緒感覺要好一些,而BackkgroundWorker正是提供一個簡單的方式讓初學者容易上手。

spinlock,mutex,semaphore,critical section的作用和區別


[ref]



spinlock,mutex,semaphore,critical section的作用和區別

Mutex是一把鑰匙,一個人拿了就可進入一個房間,出來的時候把鑰匙交給隊列的第一個。一般的用法是用於串行化對critical section代碼的訪問,保證這段代碼不會被並行的運行。
(A mutex is really a semaphore with value 1.)

Semaphore是一件可以容納N人的房間,如果人不滿就可以進去,如果人滿了,就要等待有人出來。對於N=1的情況,稱為binary semaphore。一般的用法是,用於限制對於某一資源的同時訪問。

Binary semaphore與Mutex的差異:

在有的系統中Binary semaphore與Mutex是沒有差異的。在有的系統上,主要的差異是mutex一定要由獲得鎖的進程來釋放。而semaphore可以由其它進程釋放(這時的semaphore實際就是個原子的變量,大家可以加或減),因此semaphore可以用於進程間同步。 Semaphore的同步功能是所有系統都支持的,而Mutex能否由其他進程釋放則未定,因此建議mutex只用於保護critical section。而semaphore則用於保護某變量,或者同步。

另一個概念是spin lock,這是一個內核態概念。 spin lock與semaphore的主要區別是spin lock是busy waiting,而semaphore是sleep。對於可以sleep的進程來說,busy waiting當然沒有意義。對於單CPU的系統,busy waiting當然更沒意義(沒有CPU可以釋放鎖)。因此,只有多CPU的內核態非進程空間,才會用到spin lock。 Linux kernel的spin lock在非SMP的情況下,只是關irq,沒有別的操作,用於確保該段程序的運行不會被打斷。其實也就是類似mutex的作用,串行化對critical section的訪問。但是mutex不能保護中斷的打斷,也不能在中斷處理程序中被調用。而spin lock也一般沒有必要用於可以sleep的進程空間。


Mutex vs. Semaphore, what is the difference?

The Toilet Example (c) Copyright 2005, Niclas Winquist ;)

Mutex:

Is a key to a toilet. One person can have the key - occupy the toilet - at the time. When finished, the person gives (frees) the key to the next person in the queue.

Officially: "Mutexes are typically used to serialise access to a section of re-entrant code that cannot be executed concurrently by more than one thread. A mutex object only allows one thread into a controlled section, forcing other threads which attempt to gain access to that section to wait until the first thread has exited from that section."
Ref: Symbian Developer Library

(A mutex is really a semaphore with value 1.)

Semaphore:

Is the number of free identical toilet keys. Example, say we have four toilets with identical locks and keys. The semaphore count - the count of keys - is set to 4 at beginning (all four toilets are free), then the count value is decremented as people are coming in. If all toilets are full, ie. there are no free keys left, the semaphore count is 0. Now, when eq. one person leaves the toilet, semaphore is increased to 1 (one free key), and given to the next person in the queue.

Officially: "A semaphore restricts the number of simultaneous users of a shared resource up to a maximum number. Threads can request access to the resource (decrementing the semaphore), and can signal that they have finished using the resource (incrementing the semaphore)."
Ref: Symbian Developer Library

C#中Thread.Join()




[Ref] 

首先來看一下有關的概念: 我們執行一個.exe文件實際上就是開啟了一個進程,同時開啟了至少一個線程,

但是真正幹活的是線程,就好比一個Team有好幾個人,但是真正幹活的是人不是Team.

      具體到代碼來說,以Console Application為例:程序Test.exe從Main函數開始運行,實際上是有一個線程

在執行Main函數,我們稱作MainThread.假如我們在Main函數中聲明了一個Thread,稱作NewThread,並且調用了

NewThread.Start()的方法,那麼 MainThread在處理Main函數里面的代碼時遇到NewT​​hread.Start()時,就會

去調用NewThread.

       基於上面的討論,我們可以得出結論:在我們剛才的例子中the calling thread就是MainThread,而a thread

指的洽洽就是MainThread調用的NewThread線程。

       現在回到MSDN的解釋,我們可以這麼翻譯:當NewThread調用Join方法的時候,MainThread就被停止執行,

直到NewT​​hread線程執行完畢。這樣就好理解了吧O(∩_∩)O哈哈~



       好了,前面分析完了,現在來看測試用例吧:



Titleusing System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Test
{
    class TestThread
    {
        private static void ThreadFuncOne()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name +"   i =  " + i);
            }
            Console.WriteLine(Thread.CurrentThread.Name + " has finished");
        }

        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "MainThread";

            Thread newThread = new Thread(new ThreadStart(TestThread.ThreadFuncOne));
            newThread.Name = "NewThread";

            for (int j = 0; j < 20; j++)
            {
                if (j == 10)
                {
                    newThread.Start();
                    newThread.Join();
                }
                else
                {
                    Console.WriteLine(Thread.CurrentThread.Name + "   j =  " + j);
                }
            }
            Console.Read();
        }
    }
}
 下面是测试的结果:
 


結論:從測試中我們可以很清楚的看到MainThread在NewThread.Join被調用後被阻塞,直到NewT​​hread
        執行完畢才繼續執行。

2017年8月21日 星期一

Program、Process 和 Thread 及其差別



[Ref]

請簡介 Program、Process 和 Thread 及其差別

這一題倒是比較少考到,不過為了後面講解多執行緒 (Multi-Thread) 以及 JVM Stack/Heap 方便,所以還是必須要提一下。
聲明:
這些內容比較簡略、粗糙,忽略了很多細節,只揀選能回答到問題的部份去解說,並不是百分之百準確,除非每一題都有兩個小時以上的回答時間,否則這些內容應該足夠回答到面試官想聽的點。如果你真的很在乎每一個部份的細節內容,你不該讀我的筆記,而是去讀文件,自己寫自己的筆記。當然,我會隨著越講越深入而帶入更多細節的部份,但是我也需要考量文章篇幅以及流暢度。
若有任何錯誤或遺漏之處,請在底下用力鞭我,不用客氣,隱瞞缺失比直指痛處更讓人害怕。

Program 程式

還沒有被執行的程式,或是說可以驅動 / 命令電腦的指令集合,它可能是指令、可讀的程式碼、編譯過的位元碼 (bytecode) 或是機器碼。

Process 程序

當 Program 被執行時,所產生的執行個體,Program 被執行就會產生 Process,所以如果同時執行同一個 Program 十次,就會產生至少十個 Process。要切記一點,一顆 CPU 同時只能做一個運算,多工作業系統 (Multitasking Operating System) 可以同時運行多個 Process 是因為它將 Process 進行排程 (Scheduling),令 CPU 在各 Process 切換,因為速度很快,所以才會有同時處裡好多個 Process 的錯覺。
Process 粗略包含以下兩個部分 (實際上更複雜,未來再討論):

1. Memory Space 記憶體空間

所有此 Process 的共享資料、資源放置於此,同一個 Process 底下的所有 Thread 皆可存取,但是不同 Process 的 Memory Space 不同,Process 彼此之間無法存取對方的 Memory Space。

2. Thread 執行緒


Thread 執行緒

CPU 實際運算的部分,Thread 包含兩個部分 (實際上更複雜,未來再討論):

1. Stack 堆疊

儲存從起點開始 (例如main),到目前為止所有函數呼叫路徑,以及這些呼叫路徑上所用到的區域變數。

2. CPU 狀態

存放 CPU 暫存器 (如 Program Counter, Stack Pointer, Program Status Word…) 等狀態,以便在 CPU 再次回來執行時能知道上次執行狀況。
–
一個 Process 包含一個以上的 Thread,以 Java SE 為例,應用程式從起始的入口 main 開始執行,這就是主執行緒 (Main Thread),如果程式內沒有創建其他 Thread,則稱為單一執行緒;若是程式內有使用類似以下方式去主動創建 Thread,則稱為多執行緒 (Multithreading)。
1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadExample1 extends Thread {
@Override
public void run() {
System.out.println("Custom Thread");
}
public static void main(String[] args) {
Thread t = new ThreadExample1(); // 創建 Thread
t.start(); // 執行 Thread
System.out.println("Main Thread");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadExample2 implements Runnable {
@Override
public void run() {
System.out.println("Custom Thread");
}
public static void main(String[] args) {
Thread t = new Thread(new ThreadExample2()); // 創建 Thread
t.start(); // 執行 Thread
System.out.println("Main Thread");
}
}
前面說到 CPU 會在 Process 之間做切換,其實 CPU 被分配給 Process 時主要是執行 Thread 運算,當 CPU 要處理的 Process 底下有多個 Thread,CPU 會依照 Thread 優先順序以及 CPU 使用時間去分配,在 Thread 之間做切換,讓每個 Thread 都有被執行到的機會,達到多個 Thread 同時運算的效果。

總結


  1. Program 是還沒被執行的指令或程式集合
  2. Process 是 Program 執行時產生的執行個體
  3. Process 下有 Memory Space 和一個以上的 Thread
  4. Memory Space 是同 Process 下所有 Thread 共用,主要用來存放共享的資料及資源
  5. Thread 有自己的 Stack 來儲存函數路徑及區域變數,所以不同 Thread 之間的區域變數是互相獨立的
  6. CPU 會在 Process 之間快速切換來達到多工的目的
  7. CPU 也會根據 Thread 優先權以及 CPU 使用時間,在 Thread 之間切換,讓每個 Thread 都有被執行的機會

Heap and Stack 舉例圖解


心得:
1. 先理解 value type, and reference type,
2. stack 存放變數, 實際的 value
     當new  一個東西時,是在stack 存放位置   指向到  heap 真正資料的位置

以下我用簡單的程式來講解資料儲存實際上是如何運作。
1
2
3
int a = 123;
long b = 456L;
User c = new User("Mark", Gender.MALE);
以上程式碼的資料儲存順序大致上可以視為以下流程圖 (在此先不考慮 Constant Pool)。
exampleexample
前兩行指令都是 Primitive Type,會在 Stack 內建立變數 a 和 b,並將實際值 123 和 456L 存放於變數內。
第三行指令要切分成兩個部份,等號右邊及左邊,首先是右邊的部份,先以 name 和 gender參數創建一個 User 實例,此時會在 Heap 中的一個位址 (ex. 0x1234) 存放實例的屬性資料 (ex. name、gender、address…),並將實例所在的記憶體位址 (0x1234) 指定給變數 c 參考。
這篇是以變數為例子去比較兩者儲存方式的不同,至於「Stack 如何儲存函式」、「常數池 (Constant Pool) 是什麼」、「實例的 method 存放在哪」等等問題,留待後面文章再做解答。
從以上的例子可以發現,Stack 內的變數值分為兩種類型,一種為 Value Type 實質類別 ,儲存的是 實際的值 (ex. 123、456L);另一種為 Reference Type 參考類別,儲存的是 資料在 Heap 中的記憶體位址 (ex. 0x1234)。