言
經過前面專題的介紹,大家應該對C# 1和C# 2中的特性有了進一步的理解了吧,現在終於迎來我們期待已久的C# 3中特性,C# 中Lambda表達式和Linq的提出相當於徹底改變我們之前的編碼風格了,剛開始接觸它們,一些初學者肯定會覺得很難理解,但是我相信,只要多多研究下並且弄明白之後你肯定會愛上C# 3中的所有特性的,因為我自己就是這麼過來的,在去年的這個時候,我看到Lambda表達式和Linq的時候覺得很難理解,而且覺得很奇怪的(因為之前都是用C# 3之前的特性去寫代碼的,雖然C# 3中的特性已經出來很久了,但是自己卻寫的很少,也沒有怎麼去研究,所以就覺得很奇怪,有一種感覺就是——怎麼還可以這樣寫的嗎?),經過這段時間對C# 語言系統的學習之後,才發現新的特性都是建立在以前特性的基礎上的,只是現在編譯器去幫助我們解析C# 3中提出的特性,所以對於編譯器而言,用C# 3.0中的特性編寫的代碼和C# 2.0中編寫的代碼是一樣的。從這個專題開始,將會為大家介紹C# 3 中的特性,本專題就介紹下C# 3中提出來的一些基礎特性,這些特性也是Lambda表達式和Linq的基礎。
一、自動實現的屬性
當我們在類中定義的屬性不需要一些額外的驗證時,此時我們可以使用自動實現的屬性使屬性的定義更加簡潔,對於C# 3中自動實現的屬性,編譯器編譯時會創建一個私有的匿名的字段,該字段只能通過屬性的get和set訪問器進行訪問。下面就看一個C#3中自動實現的屬性的例子:
/// <summary>
/// 自定義類
/// </summary>
public class Person
{
// C# 3之前我們定義屬性時,一般會像下面這樣去定義
// 首先會先定義私有字段,再定義屬性來對字段進行訪問
//private string _name;
//public string Name
//{
// get { return _name; }
// set { _name = value; }
//}
// C# 3之後有自動實現的屬性之後
// 對於不需要額外驗證的屬性,就可以用自動實現的屬性對屬性的定義進行簡化
// 不再需要額外定義一個私有字段了,
// 不定義私有字段並不是此時沒有了私有字段,只是編譯器幫我們生成一個匿名的私有字段,不需要我們在代碼中寫出
// 減少我們書寫的代碼
// 下面就是用自動實現的屬性來定義的一個屬性,其效果等效於上面屬性的定義,不過比之前更加簡潔了
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; private set; }
/// <summary>
/// 自定義構造函數
/// </summary>
/// <param name="name"></param>
public Person(string name)
{
Name = name;
}
}
有些人會問——你怎麼知道編譯器會幫我們生成一個匿名的私有字段的呢?對於這點當然通過反射工具來查看經過編譯器編譯之後的代碼了,下面是用Reflector工具查看的一張截圖:
如果在結構體中使用自動屬性時,則所有構造函數都需要顯式地調用無參構造函數this(),否則,就會出現編譯時錯誤,因為只有顯式調用無參構造函數this(),編譯器才知道所有字段都被賦值了。下面是一段測試代碼:
/// <summary>
/// 在結構體使用自動屬性
/// </summary>
public struct TestPerson
{
// 自動屬性
public string Name { get; set; }
// 在結構中所有構造函數都需要顯示地調用無參數構造函數this(),
// 否則會出現編譯錯誤
// 只有調用了無參數構造函數,編譯器才知道所有字段都被賦值了
public TestPerson(string name)
//: this()
{
this.Name = name;
}
}
把this()注釋掉後就會出現編譯時錯誤,如下圖:
二、隱式類型
用關鍵字var定義的變量則該變量就是為隱式類型,var 關鍵字告訴編譯器根據變量的值類推斷變量的類型。所以對於編譯器而言,隱式類型同樣也是顯式的,同樣具有一個顯式的類型。
2.1 隱式類型的局部變量
用var 關鍵字來聲明局部變量,下面一段演示代碼:
static void Main(string[] args)
{
// 用var聲明局部變量
var stringvariable = "learning hard";
stringvariable = 2;
}
為什麼說用var定義的變量對於編譯器來說還是具有顯式類型呢?在Visual studio中,將鼠標放在var部分的時候就可以看到編譯器為變量推斷的類型。並且變量仍然是靜態類型,只是我們在代碼中沒有寫出類型的名稱而已,這個工作交給編譯器根據變量的值去推斷出變量的類型,為了證明變量時靜態類型,當我們把2賦給變量stringvariable時就會出現編譯時錯誤,然而在其他動態語言中,這樣的賦值是可以編譯通過,所以用var聲明的變量仍然還是靜態類型,只是我們在代碼中沒有寫出來而已。下面是證明上面兩點的截圖:
然而使用隱式類型時有一些限制,具體限制有:
被聲明的變量是一個局部變量,不能為字段(包括靜態字段和實例字段)
變量在聲明時必須被初始化(因為編譯器要根據變量的賦值來推斷變量的類型,如果沒有被初始化則編譯器就無法推斷出變量類型了, 然而C#是靜態語言則必須在定義變量時指定變量的類型,所以此時變量不知道什麼類型,就會出現編譯時錯誤)
變量的初始化不能初始化為一個方法組,也不能為一個匿名函數(前提是不進行強制類型轉化的匿名函數)
變量不能初始化為null(因為null可以隱式轉化為任何引用類型或可空類型,所以編譯器不能推斷出該變量到底應該為什麼類型)
不能用一個正在聲明的變量來初始化隱式類型 (如不能這樣來聲明隱式類型
// 不能用一個未賦值的變量來初始化隱私類型 // 如果變量s被初始化了就可以了 string s; var stringvariable = s;
)
不能用var來聲明方法中的參數類型
同時使用隱式類型有優點也有缺點,下面的一段示例代碼完全诠釋了:
// 隱式類型的優點
// 對於復雜類型,減少打字量
// 使用隱式類型,此時就不需要再賦值的左右兩側都指定Dictionary<string,string>
var dictionary = new Dictionary<string, string>();
// 在foreach中使用隱式類型
foreach (var item in dictionary)
{
//
}
// 隱式類型的缺點
// 下面代碼使用隱式類型就會使得開發人員很難知道變量的具體類型
// 所以對於什麼情況下使用隱式類型,完全取決個人情況,自己感覺是否使用了隱式類型會使代碼看起來更整潔和容易理解
var a = 2147483649;
var b = 928888888888;
var c = 2147483644;
Console.WriteLine( "變量a的類型為:{0}",a.GetType());
Console.WriteLine("變量b的類型為:{0}", b.GetType());
Console.WriteLine("變量c的類型為:{0}", c.GetType());
Console.Read();
2.2 隱式類型的數組
var不僅可以創建隱式類型的局部變量,還可以創建數組,下面是一段演示代碼:
// 隱式類型數組演示
// 編譯器推斷為int[]類型
var intarray = new[] { 1,2,3,4};
// 編譯器推斷為string[] 類型
var stringarray = new[] { "hello", "learning hard" };
// 隱式類型數組出錯的情況
var errorarray = new[] { "hello", 3 };
使用隱式類型的數組時,編譯器必須推斷出使用什麼類型的數組,編譯器首先會構造一個包含大括號裡面的所有表達式(如上面代碼中的 1,2,3,4和"hello","learning hard")的編譯時類型的集合,在這個集合中如果所有類型都能隱式轉換為衛衣的一種類型,則該類型就成為數組的類型,否則,就會出現編譯時錯誤,如代碼中隱式類型數組出錯的情況, 因為"hello"轉化為string,而3卻轉化為int,此時編譯器就不能確定數組的類型到底為什麼,所以就會出現編譯錯誤,錯誤信息為:"找不到隱式類型數組的最佳類型"
三、對象集合初始化
3.1 對象初始化
有了對象初始化特性之後,我們就不需要考慮定義參數不同的構造函數來應付不同情況的初始化了,就減少了在我們實體類中定義的構造函數代碼,這樣使代碼更加簡潔,下面就具體看下C# 3中的對象初始化的使用和注意事項:
namespace 對象集合初始化器Demo
{
class Program
{
static void Main(string[] args)
{
#region 對象初始化演示
// 在C# 3.0之前,我們可能會使用下面方式來初始化對象
Person person1 = new Person();
person1.Name = "learning hard";
person1.Age = 25;
Person person2 = new Person("learning hard");
person2.Age = 25;
// 如果類沒有無參的構造函數就會出現編譯時錯誤
// 因為下面的語句是調用無參構造函數來對類中的字段進行初始化的
// 大括號部分就是對象初始化程序
Person person3 = new Person { Name = "learning hard", Age = 25 };
// 下面代碼和上面代碼是等價的,只不過上面省略了構造函數的圓括號而已
Person person4 = new Person() { Name = "learning hard", Age = 25 };
Person person5 = new Person("learning hard") { Age = 25 };
#endregion
}
}
/// <summary>
/// 自定義類
/// </summary>
public class Person
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; set; }
/// <summary>
/// 定義無參的構造函數
/// 如果類中自定義了帶參數的構造函數,則編譯不會生成默認的構造函數
/// 如果沒有默認的構造函數,則使用對象初始化時就會報錯說沒有實現無參的構造函數
/// </summary>
public Person()
{
}
/// <summary>
/// 自定義構造函數
/// </summary>
/// <param name="name"></param>
public Person(string name)
{
Name = name;
}
}
}
上面代碼中我用紅色標注出使用對象初始化時需要注意的地方,大家也可以通過反射工具查看編譯器是如何去解析對象初始化代碼的。
3.2 集合初始化
C# 3中還提出了集合初始化特性來對集合初始化進行了優化,下面是一段集合初始化的使用演示代碼:
namespace 對象集合初始化器Demo
{
class Program
{
static void Main(string[] args)
{
#region 集合初始化演示
// C# 3.0之前初始化集合使用的代碼
List<string> names = new List<string>();
names.Add("learning hard1");
names.Add("learning hard2");
names.Add("learning hard3");
// 有了C# 3.0中集合初始化特性之後,就可以簡化代碼
// 同時下面也使用了隱式類型(使用了var關鍵字)
var newnames = new List<string>
{
"learning hard1","learning hard2", "learning hard3"
};
#endregion
}
}
/// <summary>
/// 自定義類
/// </summary>
public class Person
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; set; }
/// <summary>
/// 定義無參的構造函數
/// 如果類中自定義了帶參數的構造函數,則編譯不會生成默認的構造函數
/// 如果沒有默認的構造函數,則使用對象初始化時就會報錯說沒有實現無參的構造函數
/// </summary>
public Person()
{
}
/// <summary>
/// 自定義構造函數
/// </summary>
/// <param name="name"></param>
public Person(string name)
{
Name = name;
}
}
}
集合初始化同樣是編譯器自動幫我們調用List的無參構造函數,然後調用Add()方法一個一個地添加進去,對於編譯器而言,C# 3中使用集合初始化的代碼和C#3之前寫的代碼是一樣.然而對於開發人員來說,有了C#3的集合初始化之後,這個過程就不需要我們自己去編碼,而是交給編譯器幫我們做就好了, 為了證明編譯器幫我們所做得事情,下面看看用反射工具來查看編譯器到底是怎樣幫我們來翻譯集合初始化的:
List<string> names = new List<string>();
names.Add("learning hard1");
names.Add("learning hard2");
names.Add("learning hard3");
List<string> <>g__initLocal3 = new List<string>();
<>g__initLocal3.Add("learning hard1");
<>g__initLocal3.Add("learning hard2");
<>g__initLocal3.Add("learning hard3");
List<string> newnames = <>g__initLocal3;
從上面反射出來的代碼可以看出,編譯器確實是一位大好人,幫我們做了那麼多的事情。
可能大家會有這樣的疑問——對象集合初始化只不過是一個語法糖而已,就是簡單地讓我們少寫點代碼而已啊,也沒有其他什麼用啊?下面部分的介紹將會解決你們的疑問。
四、匿名類型
看到匿名類型可能大家會聯想到前面介紹的匿名方法,編譯器對匿名類型和匿名方法都采用同樣的處理方式,該方式為編譯器為匿名類型生成類型名,我們在代碼中不需要顯式自定義一個類型,下面就看看匿名類型的使用:
namespace 匿名類型Demo
{
class Program
{
static void Main(string[] args)
{
#region 匿名類型的使用Demo
// 定義匿名類型
// 因為這裡不知道初始化的類型是什麼,所以這裡就必須使用隱式類型
// 此時隱式類型就發揮出了功不可沒的作用,從而說明隱式類型的提出是為了服務於匿名類型的
// 而匿名類型的提出又是服務於Linq,一步步都是在微軟團隊的計劃當中
Console.WriteLine("進入匿名類型使用演示:");
var person1 = new { Name = "learning hard", Age = 25 };
Console.WriteLine("{0} 年齡為: {1}", person1.Name, person1.Age);
Console.Read();
Console.WriteLine("按下Enter鍵進入匿名類型數組演示:");
Console.WriteLine();
#endregion
#region 匿名類型數組演示
// 定義匿名類型數組
var personcollection = new[]
{
new {Name ="Tom",Age=30},
new {Name ="Lily", Age=22},
new {Name ="Jerry",Age =32},
// 如果加入下面一句就會出現編譯時錯誤
// 因為此時編譯器就不能推斷出要轉換為什麼類型
// new {Name ="learning hard"}
};
int totalAge = 0;
foreach (var person in personcollection)
{
// 下面代碼證明Age屬性是強類型的int類型
totalAge += person.Age;
}
Console.WriteLine("所有人的年齡總和為: {0}", totalAge);
Console.ReadKey();
#endregion
}
}
}
運行結果:
上面匿名類型的演示中使用了前面幾部分介紹的所有特性——隱式類型,對象集合初始化,所以對於前面說對象集合初始化也沒有其他方面的用處的疑問也可以得到答案了,如果沒有對象集合初始化,要寫出這樣的代碼(指的是 var person1 = new { Name = "learning hard", Age = 25 };)還可能嗎?所以前面的隱式類型和對象集合初始化另外的一個用處就是服務於匿名類型的, 然而匿名類型又是服務於Linq的,對於Linq的好處當時是多的數不勝數了, 後面專題中會為大家介紹Linq。
上面還指出雖然我們在代碼中沒有為匿名類型指定類型名,而編譯器會為我們生成一個類型,為了證明這點我們同樣反射工具Reflector查看下編譯器最後為我們生成的代碼到底是怎樣的?截圖如下:
從上面截圖中可以看出編譯器確實為我們生成了一個匿名類型)<>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>(其中代碼相當於我們上面中定義的Person類),編譯器為我們生成的這個類型是直接繼承自System.Object的,並且是internal sealed(指的是該類型只在程序集內可見,並且不能被繼承)。
五、總結
到這裡,本專題的介紹也就結束了, 本專題就介紹了C# 3中幾個基礎的特性——自動實現的屬性、隱式類型、對象集合初始化和匿名類型,這些類型的提出都是服務於後面更復雜的特性Linq的,所以只有掌握好這些基礎特性之後,才能更好更快地掌握好Linq。在後面一個專題將和大家聊下C#3中的Lambda表達式。