官术网_书友最值得收藏!

1.3 類和對象

面向對象編程中,最基本的概念就是“類”和“對象”,深刻把握這兩個概念,在編程中時刻具備“面向對象”的意識,對于一個.NET工程師而言非常重要。

1.3.1 類、類的實例和對象

請看示例項目UseForm(見圖1-12),當它運行時,每點擊一次主窗體上的按鈕,就會在屏幕上出現一個從窗體,盡管這些從窗體外觀一樣,但彼此之間是完全獨立的,比如每個從窗體都可以在屏幕上自由移動和修改大小,不會影響到其他的窗體。

這個簡單的Windows Forms示例程序的背后,隱藏著.NET面向對象編程的最基本特性。

打開UseForm項目的源碼,可以看到其中定義了兩個窗體類:frmMain(代表主窗體)和frmOther(代表從窗體)。主窗體上按鈕單擊事件的響應代碼如下:

圖1-12 示例程序UseForm

private void btnShowOtherForm_Click(object sender,EventArgs e)
{
   frmOther frm=new frmOther();//創建從窗體對象
   frm.Show();                      //顯示從窗體
}

上述代碼包容了整個示例程序的“核心機密”。

程序運行時我們看到的從窗體,其實都是frmOther類的“對象(Object)”。在屏幕上看到了多少個從窗體,實際中就有多少個frmOther對象存在。

程序員所編寫的代碼放在frmMain或frmOther類中,但在程序運行時,“類”根本就不存在,存在的僅僅是“對象”。

為什么所看到的“從窗體”都一模一樣?

因為它們都是以frmOther類作為“模板”創建出來的。

所以,對象是以類為模板而創建出來的實例

在面向對象領域,“對象”這個概念與“類的實例”是等同的,它們指代同一事物。我們有時會將“創建對象”稱為“類的實例化”。

在示例程序中,我們看到有一個主窗體(frmMain)對象和多個從窗體(frmOther)對象,而它們之間的“地位”是不同的,主窗體對象負責創建從窗體對象,關閉從窗體對象對其他窗體對象沒有影響,而關閉主窗體對象,將導致屏幕上現有的所有窗體全部“消失”,程序運行結束。由此可知,對象在程序中可以擁有不同的地位、作用和角色。

從中我們可以得出另外一個結論:

面向對象的程序在運行時,會創建多個對象,這些對象之間可能有著復雜的關聯,它們相互協作,共同完成應用程序所提供的各項功能。

可以將上述結論以一個簡單的公式來表達:

正在運行的面向對象的程序=對象+對象之間的相互協作關系

在面向對象程序的開發階段,“類”是核心,而在面向對象程序運行之后,“對象”是核心。

舉個例子,在使用ASP.NET開發的Web應用程序中,每個.aspx網頁其實就是一個類。當用戶使用瀏覽器向Web服務器發出一個訪問特定.aspx網頁的HTTP請求時,“ASP.NET運行時(ASP.NET runtime)“ASP.NET runtime”是一個統稱,它包容一組相互合作的ASP.NET系統核心對象,這些對象負責完成響應用戶HTTP請求、管理ASP.NET應用程序等工作。”會依據請求的URL找到相應的.aspx網頁,“裝配”出一個完整的頁面類(派生自基類Page,故稱為“頁面類”),然后以此頁面類為模板創建一個頁面對象,調用此頁面對象的ProcessRequest方法生成HTML代碼,然后發回給客戶端瀏覽器。

所以,ASP.NET響應并處理HTTP請求的過程就是一個以頁面對象的創建為核心的過程。

再舉一個例子,在使用WCF開發的分布式系統中,客戶端可以在本地創建一個“代理對象(proxy)”,此代理對象其實對應著遠程服務器上的一個“WCF服務對象”,兩者擁有一致的訪問接口,客戶端對此本地“代理對象”的訪問請求,將會被轉發到遠程的服務器上,由相應的“WCF服務對象”負責響應。

在上面舉的兩個例子中,涉及了.NET兩個重要的技術領域:ASP.NET和WCF,可以看到其中涉及多個對象間的合作問題,很明顯如果不深刻地理解“類”和“對象”這兩個基本概念,諸如ASP.NET和WCF這類復雜的技術,是無法掌握的。

在.NET世界里,在一個運行的.NET應用程序中,“對象”無處不在。

1.3.2 溫故知新——面向對象編程的基本規則

使用C#編寫一個類很容易,例如以下代碼定義了一個MathOpt類,它完成兩數相加的功能(參見示例代碼UseMathOpt)。

class MathOpt
{
    public int Add(int x,int y)
    {
        return x+y;
    }
}

以下代碼創建了一個MathOpt對象,并且使用它來計算兩整數之和:

01 class Program
02 {
03    static void Main(string[] args)
04    {
05       MathOpt mathobj ;//定義MathOpt對象變量
06       mathobj=new MathOpt();//創建對象
07       int IResult=mathobj.Add(100,200);//調用類的整數相加方法
08       double FResult=mathobj.Add(5.5,9.2);
09       Console.WriteLine("100+200="+IResult);//輸出結果
10       Console.WriteLine("5.5+9.2="+FResult);//輸出結果
11    }
12 }

上述的示例非常簡單,然而“麻雀雖小,五臟俱全”,這個看似簡單的例子中卻蘊含著面向對象編程的基本原理。

首先,可以看到所有功能代碼都放在Main和Add兩個方法放在類中的函數改稱為“方法(method)”。“函數”是結構化編程中的術語,“方法”是面向對象編程中的術語,兩者嚴格地說是有區別的,比如在C#中我們可以為方法定義public/private/protected等訪問權限,而C語言中的函數就不具備劃分這么細的訪問權限特性。但實際上經常看到人們在面向對象開發中“混用”這兩個術語指代同一事物,用多了也就“約定俗成”了。中,而這兩個方法又分別放在類Program和MathOpt中,可由此總結出一個要點:

1.類是面向對象程序中最基本的可復用軟件單元,類中可包含多個方法。

提示

這里有一個需要強調的地方:

在面向對象程序的源碼中,不存在獨立于類之外的方法。

但這只是C#編程語言的限制,并非CLR的限制,如果使用IL編程,完全可以定義一個“全局”的函數,而此函數并不歸屬于某個類型。

那么,編好了一個類,是否就可以直接使用呢?

仔細看看Main方法中的代碼,第5句定義了一個MathOpt類型的變量mathobj,第6句使用new關鍵字創建了一個MathOpt對象,并用mathobj變量來引用這一對象。緊接著第7句調用MathOpt類的Add方法完成兩數相加的功能。由此,總結出另外一個要點:

2.外界通過對象變量來調用類中的實例方法類中的方法有兩種類型:實例方法和靜態方法。本章中的方法都是實例方法。

不允許直接調用已歸屬于某個類的實例方法,是面向對象程序不同于結構化程序的特征之一。

現在修改Main方法中的代碼,注釋掉第6句創建對象的代碼,再次編譯程序,Visual Studio會報告:

使用了未賦值的局部變量“mathobj”

這說明C#編譯器要求變量必須“顯式”初始化后才能使用。

修改第5句代碼,初始化mathobj變量為null值,再次編譯可以成功。

MathOpt mathobj=null;//定義并初始化MathOpt對象變量

運行修改后的程序,Visual Studio這次報告出現了一個“未處理NullReferenceException”的錯誤。

當某個.NET程序在運行過程中引發了一個錯誤時,CLR會創建一個異常對象,將程序出錯的信息封裝到此對象中,NullReferenceException就是這樣的一個異常對象,它包含的基本信息就是:“未將對象引用設置到對象的實例”。

交叉鏈接

CLR擁有一個異常處理子系統,向所有.NET應用程序(不論其編程語言如何)“一視同仁”地提供異常處理功能。有關這方面的內容,請看第6章《異常捕獲與處理》。

現在分析一下:

為何示例程序的運行會引發NullReferenceException異常?

關鍵是因為我們沒有使用new關鍵字創建MathOpt對象就直接使用了變量mathobj。不創建類的對象就直接使用是編程中的一種常見錯誤。程序改起來很簡單,恢復被注釋掉的第6句代碼即可。

由此得到面向對象編程的第3個要點:

3.創建完類的對象并賦值給相同類型(或相兼容類型指對象賦值給基類型或它所實現的接口類型的變量。)的變量之后,可以通過此變量調用對象的實例方法,存取對象的字段(或屬性)。

示例代碼中mathobj這個變量用于引用一個真實的MathOpt對象,是一種“引用類型變量”,由于它引用一個對象,所以人們通常又將其稱為“對象變量”。

對象變量擁有一個數據類型(即類),可以引用此類創建出的任何一個對象,對象變量與對象之間的引用關系是通過賦值語句確定的。

現在回到本節示例,在MathOpt.cs文件中再次修改代碼,將Add方法前的public關鍵字刪除后再編譯程序,此時Visual Studio報告:

“UseMathOpt.MathOpt.Add(int,int)”不可訪問,因為它受保護級別限制

由此得到了面向對象編程的第4個要點:

4.聲明為public的方法可以被外界這里所說的“外界”,指的是需要調用此方法的、不歸屬于本類(或其子類)成員的代碼。調用。

要點4說明,雖然一個類中可以有多個方法,但不是所有的方法都可以被外界調用的,只有聲明為公有(public)的方法才行,對外界而言,類中的非公有方法等于不存在一樣。這就體現出一個重要的面向對象基本特性——封裝。

只要保持類中聲明為public的成員定義不變,程序員可以在類的內部添加新的私有成員,這種改變不會影響到外界調用代碼的運行。這種“封裝”特性使得在開發大規模的系統時多個軟件工程師可以相互獨立地工作,只需相互協商好類的對外接口(指類中聲明為public的成員),就不必擔心他們的工作成果無法協同工作。

1.3.3 “匿名”的對象類型

在C# 3.0以上版本中,我們可以使用var關鍵字定義一種奇怪的“沒有類型”的變量(稱為“隱式類型的局部變量”):

var Sum=100;
Console.WriteLine(Sum * 2);//輸出:200

貌似CLR很聰明地可以動態推斷出Sum是一個int類型的變量。

事實上,是C#編譯器而不是CLR完成了類型推斷的工作,C#編譯器根據賦的值“100”推斷Sum是一個int類型的變量,就直接生成了將常量“100”賦值給它的IL代碼,CLR僅僅只是機械地執行罷了。

當使用var定義隱式類型的局部變量時,必須保證編譯器能推斷得出變量類型,否則,不能通過編譯。

var只能用于方法內部定義局部變量,不能定義為類的字段。例如,以下代碼將無法通過編譯:

class A
{
    var Value=100;
}

之所以在這里介紹var關鍵字,是因為我們可以利用它實現“不定義類而直接創建一個對象”的目的。

請看以下代碼(示例程序UseAnonymousType):

var v=new { Amount=108,Message="Hello"};

上述代碼創建了一個匿名類型對象v,它擁有兩個字段:Amount為int型,而Message為string型。

匿名類型對象的用法與普通對象沒有什么區別:

Console.WriteLine("Amount:{0},Message:{1}",v.Amount,v.Message);

上述代碼輸出的結果是:

Amount:108,Message:Hello

等一下,這個例子好像違背了面向對象編程的基本原則了,沒有定義類怎么就可以創建對象呢?

其實一切都是C#編譯器在后面玩的魔術。

使用ildasm工具查看示例程序生成的程序集(UseAnonymousType.exe),可以看到有一個名字非常奇怪的類型生成(見圖1-13)。

圖1-13 C#編譯器為匿名對象生成的“匿名類型”

再打開Main方法對應的IL代碼,一切都真相大白。

原來C#編譯器動態創建了一個類型(它的名字如圖 1-13所示),它的構造函數包括Amount和Message兩個參數。Main方法中的變量v被設置為此類型的局部變量。

Main方法先使用“108”和“Hello”作為實參調用此類型的構造函數創建對象,然后讓變量v引用這個創建好了的對象。

讀取v對象兩個字段Amount和Message的值,是通過直接調用匿名類型所定義的get_Amount方法和get_Message方法實現的。

所以,C#的匿名類型特性并未違反“先定義好類再創建對象”這一面向對象編程原則

對于匿名類型對象比較有趣的是我們可以寫出這樣的代碼:

var v=new { Amount=108,Message="Hello"};
Console.WriteLine(v);

上述代碼輸出:

{ Amount=108,Message=Hello }

這一運行結果引發出一連串的問題:

1)這里輸出的結果是從哪兒來的?

2)Console.WriteLine怎么可以直接接收一個臨時創建出來的匿名類型對象v?它怎么知道這個對象有幾個字段?

如果讀者掌握了面向對象的基礎知識,并且會使用ildasm和Reflector這兩個工具,那么解釋這個現象一點也不難。

首先,從Main方法生成的IL代碼中可以知道,“Console.WriteLine(v);”實際上調用的是Console.WriteLine方法的以下重載4.2節將更深入地討論方法重載。形式:

public static void WriteLine(object value);

現在使用Reflector工具查看Console.WriteLine(object)方法的實現代碼,會發現上述方法在內部調用參數value的ToString方法生成一個字符串,然后再輸出此字符串。

現在回到ildasm,找到C#編譯器生成的匿名類型中的ToString方法,打開它,看看里面的IL代碼,就知道為何示例會得到那樣的結果了,這個工作留給讀者作為練習。

現在剩余最后兩個問題,涉及C#為何要引入這樣“隱晦”的語法特性:

1)為什么不直接指定數據類型而要使用隱式類型來定義變量?

2)匿名類型的對象到底有什么實際用途?

回答是:

隱式類型變量和匿名類型主要用于LINQ本書第11章介紹LINQ。中。

請看以下LINQ to SQLLINQ to SQL是LINQ技術的一個分支,程序員可以在C#代碼中嵌入LINQ查詢語句,直接從SQL Server中提取數據并處理。深入介紹LINQ to SQL超出了本書的范圍,請讀者自行閱讀相關的技術書籍。代碼:

//從SQL Server數據庫中提取產品信息
var productQuery=
    from prod in products
    select new { prod.Color,prod.Price };
//顯示找到的產品信息
foreach(var v in productQuery)
    Console.WriteLine("Color={0},Price={1}",v.Color,v.Price);

上述代碼使用了匿名類型對象來生成數據庫查詢的結果。隱式類型的局部變量productQuery的真實類型為“IEnumerable<編譯器自動生成的自定義匿名類型>”,如果不使用C#的隱式類型變量和匿名類型特性,編寫同樣功能的代碼會變得比較麻煩——因為現在必須“顯式”定義一個用于封裝查詢結果(Color和Price)的類型。

主站蜘蛛池模板: 胶州市| 方正县| 平舆县| 五河县| 繁峙县| 长汀县| 宁津县| 巴林左旗| 阜平县| 商都县| 蓬莱市| 荣成市| 平和县| 雷波县| 阳高县| 崇礼县| 龙井市| 营山县| 尉犁县| 都匀市| 宣汉县| 兴仁县| 大安市| 三原县| 兴安县| 阳新县| 永吉县| 昂仁县| 东至县| 江阴市| 隆德县| 岳阳市| 右玉县| 皮山县| 光山县| 武城县| 板桥市| 阳江市| 抚松县| 瑞丽市| 安丘市|