本文出處:http://www.cnblogs.com/zhenyulu/articles/385047.html -为什么VB.net的Shared(共享)方法在C#中叫Static(静态)? - 吕震宇 - 博客园

 

 

文中所有內容均代表本人對問題的理解,可能與實際有所差別!文中C語言代碼的調試環境為MyTc 5.4.1C#代碼調試環境為VS.NET 2003 

為什麼VB.netShared(共用)方法在C#中叫Static(靜態)? 這個問題看起來很愚蠢,但是透徹的瞭解它確需要掌握物件導向程式設計語言中深層次、本質上的內容。本文將通過以下幾個層面的分析深入剖析隱藏在SharedStatic背後的究竟是什麼。 

另外本題目僅僅是個引子,本文除了討論靜態外,同時還要更多的討論"動態"方法(Object MethodInstance Method),並試圖揭示物件導向的本質。 

一、C#中的Static方法為什麼在VB.net中叫Shared方法 

1、封裝 

下面的案例摘自微軟課程《Course 2124C: Programming with C#》。 

繼承、封裝、多態性可以說是物件導向程式設計的三大特徵。我們先來看看封裝這個概念。在傳統的C語言程式設計中是沒有封裝一說的(注意:這裡暫不談C語言中的指標使用),如果兩個函數需要對同一個變數進行操作的話,不得不將該變數聲明為公有,但這一來就不能排除其它人不通過正當管道而直接修改公有資料,如下圖左邊所示: 

  

我們通過Deposit方法與Withdraw方法完成存錢與取錢的動作,進而修改帳戶餘額balance。然而公有的balance變數允許使用者不經存、取錢動作就可以直接修改帳戶餘額, 給程式帶來了很大的風險。如果用C語言實現的話,代碼可以表示為: 

#include <stdio.h>

double balance = 0.0; 

main()

{

   Deposit(10000.0);

   Withdraw(2000.0);

   ShowBalance();

   balance = 999999999.9;

   ShowBalance();

   printf("Oh! my God!\n");

}

Withdraw(double amount)

{

   balance = balance - amount;

}

Deposit(double amount)

{

   balance = balance + amount;

}

ShowBalance()

{

   printf("%12.2f\n", balance);

}

物件導向語言的出現引入了封裝的概念,通過構築一個膠囊,膠囊的邊界將空間劃分為,將資料放到裡面作為私有資料,而將方法提供給外面以供調用,這樣就有效保護了資料,防止非法訪問。這個膠囊就是class。如上圖右邊所示。用C#實現的代碼如下:

using System ;

public class Account

{

   private double balance = 0;

   public void Withdraw(double amount)

   {

      balance -= amount;

   }

   public void Deposit(double amount)

   {

      balance += amount;

   }

   public void ShowBalance()

   {

      Console.WriteLine("Current balance is {0}", balance);

   }

}

public class Client

{

   public static void Main()

   {

      Account account1 = new Account();

      Account account2 = new Account();

      account1.Deposit(10000);

      account2.Deposit(7000);

      account1.ShowBalance();

      account2.ShowBalance();

   }

}

2、實例(Instance

帳戶類僅僅定義了一個模子,不同人應當擁有自身的帳戶以及各自的存取記錄和帳戶餘額。在這裡每個人的帳戶就是帳戶類的一個實例。實例和實例間的資料是不同的,如果兩個人的帳戶餘額相同純屬偶然。如上面代碼所示,我們通過new命令創建一個帳戶類的實例。不同實例間(account1account2)可以擁有不同的帳戶餘額。

3Shared的起源

實例和實例之間往往需要共用一些資料(例如存款利率),將這些共用資料存放在每一個實例中顯然不是什麼好的辦法,我們需要將它們抽取出來單獨存放並被所有實例所共用,這就是Shared的來源,在C#語言中叫做Static。如下圖所示:

 

圖中的Shared與非Shared的代碼和資料是分開的,然而在編寫程式時,我們需要將它們寫在同一個類裡面,但它們在運行時卻有著不同的表現,初學者一定要注意這一點。C#代碼實現如下:

using System ;

public class Account

{

   private double balance = 0;

   private static double interest = 0.07;

   public void Withdraw(double amount)

   {

      balance -= amount;

   }

   public void Deposit(double amount)

   {

      balance += amount;

   }

   public static double InterestRate()

   {

      return interest;

   }

   public void ShowBalance()

   {

      Console.WriteLine("Current balance is {0}", balance);

   }

}

public class Client

{

   public static void Main()

   {

      Account account1 = new Account();

      Account account2 = new Account();

      account1.Deposit(10000);

      account2.Deposit(7000);

      account1.ShowBalance();

      account2.ShowBalance();

      Console.WriteLine("InterestRate is: {0}", Account.InterestRate());

   }

}

對於這類方法和屬性我們在前面標示上SharedVB.net)或staticC#),當看到這些標示後,我們就知道這些內容是用來被所有實例所共用的。這就算是Shared的起源吧。

4、另外一個案例

讓我們再來舉另外一個例子。假如有一學生類。有兩個方法:自報姓名以及火車票打折。我們可以看到報姓名的方法是實例方法,因為不同人有不同的姓名,而火車票打折是對所有學生而言的,因此應當屬於共用的方法。代碼如下:

using System ;

public class Student

{

   private string name;

   public Student(string name)

   {

      this.name = name;

   }

   public void TellName()

   {

      Console.WriteLine("My name is: {0}", name);

   }

   public static double CalcTicketDiscount(double price)

   {

      return price/2;

   }

}

public class Client

{

   public static void Main()

   {

      Student stu1 = new Student("Tom");

      Student stu2 = new Student("Jim");

      

      stu1.TellName();

      stu2.TellName();

      

      Console.WriteLine("Discount: {0:C2}",Student.CalcTicketDiscount(100));

   }

}

5、問題

從上面的分析可以看出,Shared在描述問題上似乎更有道理、更容易理解,那麼為什麼在C#語言中沒有使用shared一詞,反而使用了static(靜態)呢? 這裡的靜態又是指什麼?我們將在下一部分內容中加以分析。

二、VB.net中的Shared方法為什麼在C#中叫Static方法

1"靜態"是什麼?

我們在C#中所說的靜態和傳統C語言中的靜態其實不是一個概念。C語言中的靜態指當多次進入某一方法時,該方法中的靜態變數能夠保持上次調用時的值。舉例來說,下面的C語言程式的列印結果是12。當第一次調用sub後,靜態變數i中的值被保留了下來。

#include <stdio.h>

main()

{

   sub();

   sub();

}

sub()

{

   static int i = 0;

   i++;

   printf("%d\n", i);

}

C#中的靜態則不是這個概念。它往往指方法屬性是固定的、不會發生變化的、編譯時就能確定下來的。這似乎很難懂,我們還是看看以下兩個例子。

第一個例子是C語言的例子,在C語言中,函數與函數不能出現重名,因此當告知調用的函數名時,這個函數是什麼,代碼在什麼地方就可以唯一的確定下來,不會有任何的歧義。C#中管這種唯一能夠確定代碼位置的,不會產生任何歧義的方法叫做靜態方法。

#include <stdio.h>

main()

{

   sub1();

}

sub1()

{

   printf("This is sub1");

}

sub2()

{

   printf("This is sub2");

}

上面的例子中,當主程序發出sub1方法的調用命令後,我們可以唯一的確定該方法在什麼地方,去哪裡執行代碼,因此我們說sub1方法是靜態的。簡直是廢話!不過不要著急,在C#中情況就不同了,當你調用某一方法時,你可能根本就不知到該方法在什麼地方,會執行什麼樣的代碼。

2多態中所體現出來的動態

我們看下面的案例:

using System;

public abstract class Light

{

   public abstract void TurnOn();

}

public class BulbLight : Light

{

   public override void TurnOn()

   { Console.WriteLine("Bulb Light is turned on!"); }

}

public class TubeLight : Light

{

   public override void TurnOn()

   { Console.WriteLine("Tube Light is turned on!"); }

}

public class Client

{

   public static void Main()

   {

      Light light;

      light = new BulbLight();

      TurnOnLight(light);

      

      light = new TubeLight();

      TurnOnLight(light);

   }

   public static void TurnOnLight(Light light)

   { light.TurnOn(); }

}

不知各位能否說出最後一行代碼light.TurnOn()方法在什麼地方嗎?恐怕不能。因為主程式中先後兩次調用TurnOnLight方法,然而所運行的代碼位置不同,產生的結果也各不相同。因此,我們說TurnOn方法是非靜態的,究竟調用哪段代碼要在運行時決定,而不能在編譯時決定。其實這就是C#語言中的實例方法(Instance Method),也叫做物件方法(Object Method)。不過我個人更喜歡叫它動態方法(Dynamic Method),因為TurnOn方法的代碼位置是在運行時動態決定的。(注意:動態方法這個名稱是錯誤的,因為動態方法與實例方法是兩個孑然不同的概念!本文最後要比較實例方法和動態方法的區別。在不是很嚴格的情況下,我總是喜歡將它們混為一談

3、編譯時與運行時

簡單說來,C#中的靜態是指在編譯時就能夠唯一確定其代碼位置的,調用時目的地靜止不動、不會發生變化的方法。而非靜態方法往往指在運行時動態決定代碼的調用位置、能夠呈現出多態特徵的方法,其調用往往綁定到某一物件的具體實例上。

關於實例方法為什麼不能叫做動態方法以及實例方法究竟是怎麼一回事,我們將在下一部分內容中加以詳細介紹。

4、為什麼Main必須是靜態的?

現在我們可以思考一下為什麼Main方法必須是靜態的?從一個角度上說,Main是程式的入口點,它在編譯時必須是能夠唯一確定其位置的,不能發生變化的方法,其調用不能產生任何的二意性,因此必須是靜態的。當然這裡的分析還比較片面,當我們深入學習了後面的實例方法後,對Main方法為什麼是靜態的會有更深入的瞭解。

5、再次分析銀行帳戶的案例

對於第一部分中銀行帳戶案例裡銀行利率必須是Shared屬性的解釋,我們現在可以站在static的角度加以分析了。因為銀行利率的存儲位址必須是唯一的,當你訪問銀行利率時必須沒有二意性。從這個角度上說,銀行利率必須是靜態的。

三、實例方法及其實現

在前面分析了靜態方法後,我們在這部分內容中將重點說說什麼是實例方法、實例方法的威力以及它是如何在內部實現的。另外我們還要討論為什麼實例方法不能叫做動態方法。

1、數值型別與參考類型

關於這個問題我不準備多說什麼了,有太多的資料介紹這個問題。如果現在還不知道什麼是數值型別和參考類型的話,我想下面的那部分內容也就可以不用讀了。

2、什麼是this

首先看看下面這段程式:

using System ;

public class Car

{

   public int speed = 0;

   public void SetSpeed(int speed)

   {

      this.speed = speed;

   }

}

public class Client

{

   public static void Main()

   {

      Car car1 = new Car();

      Car car2 = new Car();

      car1.SetSpeed(60);

      car2.SetSpeed(100);

   }

}

大家注意,我們在主程序中聲明了兩個Car的實例,分別是car1car2,然後分別調用其SetSpeed方法,於是兩個car實例被賦予了不同的速度值。然而SetSpeed方法是如何知道究竟是修改car1speed還是car2的呢?這兩個speed又在什麼地方呢?SetSpeed方法中的this.speed是誰,speed又是誰呢?

其實實例方法在調用時隱含的傳遞了一個this指標,使得我們的SetSpeed知道到哪裡去修改相應的資料。上面的程式運行時記憶體佈局如下圖所示:

 

當創建了car1物件與car2物件後,實際上在堆中給這兩個物件分配了相應的記憶體並初始化了欄位的取值。由於物件是參考類型,因此變數car1car2存儲的是記憶體位址。緊接著我們要調用SetSpeed方法,上面說過,實例方法在調用時實際上隱含的傳遞了一個this指標。因此程式實際執行時,SetSpeed方法實際上形式如下:

SetSpeed(Car this, int speed)

注意:上面的形式並不完全正確,僅僅說明了非virtualInstance方法的調用方式,在後面說完VMT的概念後再說virtual方法的調用方式。

在上面的SetSpeed方法調用中,第一個參數是一個Car類型的參數,參數名是this,第二個參數才是代碼中書寫的int speed參數。為什麼會是這樣呢?我們看看SetSpeed方法實際被調用時的情形:

 

當調用car1.SetSpeed(60);時,實際上後臺執行的代碼是SetSpeed(car1, 60); car1的引用地址傳了進去並賦值給this參數。這樣一來,this.speed = speed;就不難理解了,this.speed就是順著位址00A0找到的speed屬性(即car1.speed),而第二個speedSetSpeed方法中的第二個參數。這句代碼的意思就很明顯了。

3、型與值的概念

型與值在程式設計語言中通常是統一的。例如代碼int i = 10; 變數i的型是整型,而變數i的值是1010是整型,因此我們說i的型與值是一致的。這種例子多得是,我就不再多說了。然而在物件導向程式設計中,還存在一種型與值不一致的情形(注意:嚴格的說,並不存在型與值不一致的情形,型只是編譯時而已,而值才是運行時,在運行時,型和值其實是一致的。關於這點我們將在後面再探討)。請看下面的代碼:

using System;

public abstract class Light

{

   public abstract void TurnOn();

}

public class BulbLight : Light

{

   public override void TurnOn()

   { Console.WriteLine("Bulb Light is turned on!"); }

}

public class TubeLight : Light

{

   public override void TurnOn()

   { Console.WriteLine("Tube Light is turned on!"); }

}

public class Client

{

   public static void Main()

   {

      Light light = new BulbLight();

      light.TurnOn();

   }

}

在上面的案例中,變數light的值是燈泡(BulbLight),然而它的型確是燈(Light),值是型的子類,因此程式在執行時會呈現出多態的行為:當我們調用燈的開燈方法時,會調用燈泡的開燈方法。這又是如何實現的呢?其實一切秘密都隱藏在虛擬方法表中(VMT, Virtual Method Table)。

4Virtual Method Table

實例方法之所以在運行時能夠產生動態的行為,其秘密就在於虛擬方法表(VMT)。其實,使用虛擬方法表的目的就是讓子類與父類中相同的方法與屬性在各自的虛擬方法表中具有相同的偏移量(當然也有不同的實現,比如後面說的Dynamic方法,實現手段就不是這樣)。用一個圖來描述的話就是:

 

該圖對於屬性和方法是同樣適用的,只是屬性並不存放在虛擬方法表中。從圖中可以看出,子類包含了父類的所有方法和屬性(包括私有成員),並且它們的相對位置(偏移量)是完全相同的。

另外,每個物件實例都會有一個指向自己虛擬方法表的指標(如下圖左側的 VMT ptr),實例屬性就存儲在堆中,每個實例擁有自己的屬性值,而虛擬方法表中記錄下了各個實例方法的具體代碼位置(指向函數的指標)。值得注意的是,虛擬方法表除了存儲實例方法指標外,還會存儲一些其它資訊,例如類的類名(下圖中間最上面的name指針),並且是負偏移量。不同語言的實現手段也各不相同。如果對此感興趣的話,可以分析一下Delphi的代碼。網上也有很多文章介紹Delphi中虛擬方法表是什麼樣子的,在這裡就不再重複。感興趣的人也可以讀一讀臺灣李維的《Inside VCL(深入核心——VCL架構剖析)》一書。

 

下圖演示了同一個物件的多個實例共用一個VMT,但各自存儲自己的屬性。

 

5"多態"是怎麼實現的

物件導向程式設計的魅力所在就是針對抽象程式設計,而沒有多態,針對抽象程式設計就會成為空談。關於多態是什麼不是本文要論述的問題,我在這裡重點說說多態是如何通過虛擬方法表實現的。假設有如下圖所示的類繼承關係:

 

MotorcycleCar繼承自VehicleMortorcycle覆寫了Drive方法與Stop方法;Car覆寫了Drive方法並提供了一個新屬性maxSpeed和一個新方法SetMaxSpeedPassengerCarTruck繼承自Car,並分別提供了各自的新屬性與方法,另外Truck覆寫了Stop方法。如果用C#代碼書寫下來的話,可以表示成:

using System ;

public class Vehicle

{

   public int wheels;

   public int speed;

   public virtual void Drive()

   {

      Console.WriteLine("Drive Vehicle.");

   }

   public virtual void Stop()

   {

      Console.WriteLine("Stop Vehicle.");

   }

}

public class Motorcycle : Vehicle

{

   public override void Drive()

   {

      Console.WriteLine("Drive Motorcycle.");

   }

   public override void Stop()

   {

      Console.WriteLine("Stop Motorcycle.");

   }

}

public class Car : Vehicle

{

   public int maxSpeed;

   public override void Drive()

   {

      Console.WriteLine("Drive Car.");

   }

   public virtual void SetMaxSpeed(int maxSpeed)

   {

      this.maxSpeed = maxSpeed;

      Console.WriteLine("Set car max speed to {0}.", maxSpeed);

   }

}

public class PassengerCar : Car

{

   public int maxPassengers;

   public void SetMaxPassengers(int maxPassengers)

   {

      this.maxPassengers = maxPassengers;

      Console.WriteLine("Set max passengers to {0}.", maxPassengers);

   }

}

public class Truck : Car

{

   public int carryingCapacity;

   public override void Stop()

   {

      Console.WriteLine("Stop Truck.");

   }

   public void SetCarryingCapacity(int carryingCapacity)

   {

      this.carryingCapacity = carryingCapacity;

      Console.WriteLine("Set carrying capacity to {0}t.", carryingCapacity);

   }

}

public class Client

{

   public static void Main()

   {

      Vehicle v = new Truck();

      v.Drive();

      v.Stop();

      Car c = (Car)v;

      c.SetMaxSpeed(90);

      Truck t = (Truck)c;

      t.SetCarryingCapacity(10);

      Car car = new PassengerCar();

      car.Drive();

      PassengerCar p = (PassengerCar)car;

      p.SetMaxPassengers(21);

      Truck truck = new Truck();

      truck.SetCarryingCapacity(20);

   }

}

注意用戶端調用代碼中使用了型與值不一致的表示方法,將子類的實例賦值給了父類,並進行了多次強制類型轉換。那麼上面那段代碼在執行時記憶體的佈局是什麼樣子的呢?我們可以使用下圖來描述:

 

從上圖中我們可以得出以下結論:

1)當子類覆寫了父類的方法時,相當於將VMT中對應方法指標指向了新的代碼位置。

子類與父類中相同的方法具有相同的偏移量(相對位置),如果子類override了父類的方法,將使得該位置上的方法指標指向新的位置。上圖中,PassengerCar通過Car繼承自VehiclePassengerCarCar均沒有覆寫VehicleStop方法,因此,PassengerCarVMTStop方法仍然指向Vehicle.Stop法方法。而Car覆寫了VehicleDrive方法,PassengerCar沒有覆寫CarDrive方法,導致PassengerCarDrive方法指向了CarDrive方法。依此類推……

2)非靜態方法在編譯時能夠確定的僅僅是虛擬方法表中的偏移量

 

如上圖,由於子類與父類中相同的方法具有相同的偏移量(相對位置),因此當程式對虛擬方法進行調用時,實際上是對VMT中指定位置索引的方法進行調用,於是產生了多態。在上面的例子中,當執行Vehicle v = new Truck();後,系統在堆中創建了一個值為Truck的物件(虛擬方法表的指標指向了TruckVMT),並將該物件的位址賦值給了一個型為Vehicle的變數v。當我們調用v.Drive()時,其實在後臺執行的代碼是(* v.VMT[idx])(v),在這裡idxDrive方法在VMT中的偏移量,即0該偏移量可以在編譯時就確定下來)。整個命令可以表示成:調用v所指向的VMT中偏移量為0處的方法,並將v的引用傳遞給該方法(這就是前面所說的隱含傳遞了一個this指標)。

因此,我們說非靜態方法的調用位址是不固定的,唯一能夠在編譯時確定下來的就是方法在虛擬方法表中的相對偏移位置。

3)型是編譯時、值是運行時

通過上面的分析我們還可以得到一個結論:型是編譯時,值是運行時(儘管在IL代碼中仍然保留了型的成分,但是我始終堅信在最終的機器代碼中是沒有型這個概念的)。關於這個結論我們可以從程式運行時記憶體佈局上看出。注意在前面的記憶體佈局圖中,tcv分別表示了型為TruckCarVehcile的三個物件,然而在記憶體中它們是完全一樣的。型的存在是為了提供多態、提供針對抽象程式設計生存的環境,同時幫助編譯器確認方法偏移量,而真正程式執行時只有值,沒有型。我們也可以從下面這段代碼中看出型僅僅是編譯時:

using System ;

public class Light

{}

public class BulbLight : Light

{}

public class Client

{

   public static void Main()

   {

      Light l = new BulbLight();

      Console.WriteLine(l.GetType().ToString());

   }

}

注意變數l的型是Light,而它的值是BulbLight,當我們調用lGetType方法時,它返回的不是Light型,而是BulbLight型,究其原因,其實這個Type是根據物件的虛擬方法表(及相關因素)產生的,而l的虛擬方法表就是指向BulbLight的,因此程式執行的結果是BulbLight而不是Light。這也從一個側面說明了型是編譯時、值是運行時,程式一旦運行起來就沒有型的概念了。

有人可以會提及down cast或者up cast的概念以及is運算子與as運算子,這些不都是和型相關嗎?我個人認為這些東西在運行時所做工作僅僅是根據虛擬方法表(很多虛擬方法表中都提供了一個指向父類VMT的指標,於是便串成了一個鏈表)判斷類型是否匹配而已,如果類型匹配,則無需再做任何工作。

也許有人會提及介面的cast是如何進行的,其實介面的記憶體實現與VMT不一樣,應當單獨討論,我個人理解應當有介面表,每個介面又對應VMT中的不同方法,不過這也僅僅是根據Assembly內部結構作的推斷而已,就不再加以討論。

四、為什麼實例方法不能叫動態方法?

與靜態方法相對應的應當叫做動態方法,然而我們可以在C#教材上看到實例方法物件方法的稱呼,唯獨沒有叫動態方法,儘管這種叫法顯得比較自然,這是為什麼呢?其原因就在於動態方法其實另有它指。在Delphi語言中動態方法有自己專門的實現機制。動態方法的英文名字叫做“Dynamic Method”,儘管從功能上和我們的實例方法沒有任何區別,然而在實現技術上確相差很大,因為動態方法使用的不是VMT,而是DMTDynamic Method Table)。DMT是什麼樣子?又有什麼好處呢?針對前面的Vehicle案例,我們如果用動態方法表表述的話可以描述成下圖:

 

可以看到相對於VMT,每個DMT的子類中不再包含父類中所有項目,相同方法也不再擁有相同的相對位置,取而代之的是每個DMT都有一個指向父類DMT的指標,並且每個DMT中只包含有自己覆寫或實現的方法。這樣一來節省了記憶體的佔用,然而確影響了調用效率。

由於DMT在父類和子類中的大小不一定相同,同一個方法在DMT中的位置也不一定相同,所以動態方法在調用時必須從當前類的DMT向上動態查找,調用效率就低一些。而VMT能夠在編譯時就確定方法偏移量,因此調用很快,但使用的記憶體要比DMT多。

因此,從運行時的表現上,動態方法實例方法是完全相同的,但從實現機制上說又各不相同,在不是很嚴格的情況下可以混為一談。如果深入實現機理,我們還要搞清楚動態的真正含義。

五、補充

看到裝配腦袋雙魚座的回復後,發現文中確實有幾處漏洞,在這裡補充一下:

我對static的看法僅僅是本文的一個引子,觀點似乎不完全正確(位址固定的、不會發生變化的方法),如果按照我的觀點,非virtual的實例方法也有某種靜態成分在裡面,這顯然有點離譜,所以我還是贊同雙魚座所說,純粹為了尊重群體習慣,我有點偷換概念了。

另外對static、非virtualInstance方法以及virtualInstance方法在這裡再作個歸納,希望批評指正:

1)靜態方法:MethodName(parameters);其實在調用時也應當傳遞了一個this指標,只不過該指標指向的是類資訊位址,該位址其實也是一個實例,只不過是一個更低層次上的、且結構完全固定的實例(即所謂中繼資料的中繼資料)

2)非virtualInstance方法:MethodName(this, parameters);

3virtualInstance方法:(* obj.VMT[idx])(this, parameters);

注:文中有關物件導向實現方面資料部分參考了《Introduction to Object Oriented Programming, 3rd Ed》一書第27章,可以從:

http://undergraduate.csse.uwa.edu.au/units/230.224/timBudd/chap27/chap27.html 處找到相關資訊。

 

arrow
arrow
    全站熱搜

    mouse 發表在 痞客邦 留言(0) 人氣()