上學時學習C#和.NET,當時網上的資源不像如今這樣豐富,因此去電腦城買了張盜版的VS2005的光盤,安裝時才發現是VS2003,當時有一種被坑的感受,但也正是如此,讓我有了一個完整的.NET的學習生涯。前端
一直都認爲學習語言應該系統的進行學習,瞭解每個版本的新增特性,才能在實際應用中作到有的放矢。最近發現團隊中有很多人雖然用着最新的技術,但知識儲備還停留在一個比較初始的狀態,這樣在編碼過程當中會走很多彎路。數據庫
本文梳理下C#從1.0到7.0版本的一些經常使用特性,對於不經常使用的或者我沒有用到過的一些特性,會列出來,但不會作詳細描述。另外C#8.0如今尚未正式推出,而且目前咱們也只是在使用dotNet Core2.1,因此C#8.0本文也不會涉及。json
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
1.0 | VS2002 | 1.0 | 1.0 |
1.1 | VS2003 | 1.1 | 1.1 |
在C#1.0或1.1版本中,從語言的角度就是基本的面向對象的語法,能夠說任何一本C#語言的書籍都包含了C#1.X的全部內容。後端
若是您已經在使用C#語言編寫代碼,那麼C#1.X的相關知識應該已經掌握。基礎語法部分這裏就再也不贅述了。數組
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
2.0 | VS2005 | 2.0 | 2.0 |
2.0中對應VS2005我用的也很少,由於很快就被VS2008替代了,不過在語言方面卻帶來了不少新的東西。多線程
C#2中最重要的一個特性應該就是泛型。泛型的用處就是在一些場景下能夠減小強制轉換來提升性能。在C#1中就有不少的強制轉換,特別是對一些集合進行遍歷時,如ArrayList、HashTable,由於他們是爲不一樣數據類型設計的集合,因此他們中鍵和值的類型都是object,這就意味着會平凡發生裝箱拆箱的操做。C#2中有了泛型,因此咱們可使用List框架
.NET已經經過了不少的泛型類型供咱們使用,如上面提到的List前後端分離
分部類能夠容許咱們在多個文件中爲一個類型(class、struct、interface)編寫代碼,在Asp.Net2.0中用的極爲普遍。新建一個Aspx頁面,頁面的CodeBehind和頁面中的控件的定義就是經過分部類來實現的。以下:異步
public partial class _Default : System.Web.UI.Page public partial class _Default
分部類使用關鍵字partial來定義,當一個類中的代碼很是多時,可使用分部類來進行拆分,這對代碼的閱讀頗有好處,並且不會影響調用。不過如今咱們先後端分離,後端代碼要作到單一職責原則,不會有不少大的類,因此這個特性不多用到。async
靜態類中的公用方法必須也是靜態的,能夠由類名直接調用,不須要實例化,比較適用於編寫一些工具類。如System.Math類就是靜態類。工具類有一些特色,如:全部成員都是靜態的、不須要被繼承、不須要進行實例化。在C#1中咱們能夠經過以下代碼來實現:
//聲明爲密封類防止被繼承 public sealed class StringHelper { //添加私有無參構造函ˉ數防止被實例化,若是不添加私有構造函數 //會自動生成共有無參構造函數 private StringHelper(){}; public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } }
C#2中可使用靜態類來實現:
public static class StringHelper { public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } }
在C#1中聲明屬性,屬性中的get和set的訪問級別是和屬性一致,要麼都是public要麼都是private,若是要實現get和set有不一樣的訪問級別,則須要用一種變通的方式,本身寫GetXXX和SetXXX方法。在C#2中能夠單獨設置get和set的訪問級別,以下:
private string _name; public string Name { get { return _name; } private set { _name = value; } }
須要注意的是,不能講屬性設置爲私有的,而將其中的get或是set設置成公有的,也不能給set和get設置相同的訪問級別,當set和get的訪問級別相同時,咱們能夠直接設置在屬性上。
命名空間能夠用來組織類,當不一樣的命名空間中有相同的類時,可使用徹底限定名來防止類名的衝突,C#1中可使用空間別名來簡化書寫,空間別名用using關鍵字實現。但還有一些特殊狀況,使用using並不能徹底解決,因此C#2中提供了下面幾種特性:
咱們在構建命名空間和類的時候,儘可能避免出現衝突的狀況,這個特性也較少用到。
當咱們但願一個程序集中的類型能夠被外部的某些程序集訪問,這時若是設置成Public,就能夠被全部的外部程序集訪問。怎樣只讓部分程序集訪問,就要使用友元程序集了,具體參考以前的博文《C#:友元程序集(http://blog.fwhyy.com/2010/11/csharp-a-friend-assembly/)》
可空類型就是容許值類型的值爲null。一般值類型的值是不該該爲null的,但咱們不少應用是和數據庫打交道的,而數據庫中的類型都是能夠爲null值的,這就形成了咱們寫程序的時候有時須要將值類型設置爲null。在C#1中一般使用」魔值「來處理這種狀況,好比DateTiem.MinValue、Int32.MinValue。在ADO.NET中全部類型的空值能夠用DBNull.Value來表示。C#2中可空類型主要是使用System.Nullable
Nullablei = 20; Nullableb = true;
C#2中也提供了更方便的定義方式,使用操做符?:
int? i = 20; bool? b = true;
C#2中對迭代器提供了更便捷的實現方式。提到迭代器,有兩個概念須要瞭解
看下面一個例子:
public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { return new PersonEnumerator(this); } private string this[int index] { get { return _names[index]; } set { _names[index] = value; } } } public class PersonEnumerator : IEnumerator { private int _index = -1; private Person _p; public PersonEnumerator(Person p) { _p = p; } public object Current { get { return _p._names[_index]; } } public bool MoveNext() { _index++; return _index < _p._names.Length; } public void Reset() { _index = -1; } }
C#2中的迭代器變得很是便捷,使用關鍵字yield return關鍵字實現,下面是C#2中使用yield return的重寫版本:
public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { foreach (string s in _names) { yield return s; } } }
匿名方法比較適用於定義必須經過委託調用的方法,用多線程來舉個例子,在C#1中代碼以下:
private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(DoWork)); thread.Start(); } private void DoWork() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action(this.ChangeLabel),i.ToString()); } } private void ChangeLabel(string i) { label1.Text = i + "/100"; }
使用C#2中的匿名方法,上面的例子中能夠省去DoWork和ChangeLabel兩個方法,代碼以下:
private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(delegate() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action(delegate() { label1.Text = i + "/100"; })); } })); thread.Start(); }
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
3.0 | VS2008 | 2.0 | 3.0 3.5 |
若是說C#2中的核心是泛型的話,那麼C#3中的核心就應是Linq了,C#3中的特性幾乎都是爲Linq服務的,但每一項特性均可以脫離Linq來使用。下面就來看下C#3中有哪些特性。
這個特性很是簡單,就是使定義屬性變得更簡單了。代碼以下:
public string Name { get; set; } public int Age { private set; get; }
隱式類型的局部變量是讓咱們在定義變量時能夠比較動態化,使用var關鍵字做爲類型的佔位符,而後由編譯器來推導變量的類型。
擴展方法能夠在現有的類型上添加一些自定義的方法,好比能夠在string類型上添加一個擴展方法ToInt32,就能夠像「20」.ToInt32()這樣調用了。
具體參見《C#3.0學習(1)—隱含類型局部變量和擴展方法(http://blog.fwhyy.com/2008/02/learning-csharp-3-0-1-implied-type-of-local-variables-and-extension-methods/)》。
隱式類型雖然讓編碼方便了,但有些很多限制:
簡化了對象和集合的建立,具體參見《C#3.0學習(2)—對象集合初始化器(http://blog.fwhyy.com/2008/02/learning-c-3-0-2-object-collection-initializer/)》。
和隱式類型的局部變量相似,能夠不用顯示指定類型來進行數組的定義,一般咱們定義數組是這樣:
string[] names = { "oec2003", "oec2004", "oec2005" };
使用匿名類型數組能夠想下面這樣定義:
protected void Page_Load(object sender, EventArgs e) { GetName(new[] { "oec2003", "oec2004", "oec2005" }); } public string GetName(string[] names) { return names[0]; }
匿名類型是在初始化的時候根據初始化列表自動產生類型的一種機制,利用對象初始化器來建立匿名對象的對象,具體參見《C#3.0學習(3)—匿名類型(http://blog.fwhyy.com/2008/03/learning-csharp-3-0-3-anonymous-types/)》。
其實是一個匿名方法,Lambda表達的表現形式是:(參數列表)=>{語句},看一個例子,建立一個委託實例,獲取一個string類型的字符串,並返回字符串的長度。代碼以下:
Funcfunc = delegate(string s) { return s.Length; }; Console.WriteLine(func("oec2003"));
使用Lambda的寫法以下:
Funcfunc = (string s)=> { return s.Length; }; Funcfunc1 = (s) => { return s.Length; }; Funcfunc2 = s => s.Length;
上面三種寫法是逐步簡化的過程。
是.NET3.5中提出的一種表達方式,提供一種抽象的方式將一些代碼表示成一個對象樹。要使用Lambda表達式樹須要引用命名空間System.Linq.Expressions,下面代碼構建一個1+2的表達式樹,最終表達式樹編譯成委託來獲得執行結果:
Expression a = Expression.Constant(1); Expression b = Expression.Constant(2); Expression add = Expression.Add(a, b); Console.WriteLine(add); //(1+2) FuncfAdd = Expression.Lambda<Func>(add).Compile(); Console.WriteLine(fAdd()); //3
Lambda和Lambda表達式樹爲咱們使用Linq提供了不少支持,若是咱們在作的一個管理系統使用了Linq To Sql,在列表頁會有按多個條件來進行數據的篩選的功能,這時就可使用Lambda表達式樹來進行封裝查詢條件,下面的類封裝了And和Or兩種條件:
public static class DynamicLinqExpressions { public static Expression<Func> True() { return f => true; } public static Expression<Func> False() { return f => false; } public static Expression<Func> Or(this Expression<Func> expr1, Expression<Func> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda<Func> (Expression.Or(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func> And(this Expression<Func> expr1, Expression<Func> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda<Func> (Expression.And(expr1.Body, invokedExpr), expr1.Parameters); } }
下面是獲取條件的方法:
public Expression<Func> GetCondition() { var exp = DynamicLinqExpressions.True(); if (txtCourseName.Text.Trim().Length > 0) { exp = exp.And(g => g.CourseName.Contains(txtCourseName.Text.Trim())); } if (ddlGrade.SelectedValue != "-1") { exp=exp.And(g => g.GradeID.Equals(ddlGrade.SelectedValue)); } return exp; }
Linq是一個很大的話題,也是NET3.5中比較核心的內容,有不少書籍專門來介紹Linq,下面只是作一些簡單的介紹,須要注意的是Linq並不是是Linq To Sql,Linq是一個大的集合,裏面包含:
下面以Linq To Object爲例子來看看Linq是怎麼使用的:
public class UserInfo { public string Name { get; set; } public int Age { get; set; } } public class Test { static void Main() { Listusers = new List() { new UserInfo{Name="oec2003",Age=20}, new UserInfo{Name="oec2004",Age=21}, new UserInfo{Name="oec2005",Age=22} }; IEnumerableselectedUser = from user in users where user.Age > 20 orderby user.Age descending select user; foreach (UserInfo user in selectedUser) { Console.WriteLine("姓名:"+user.Name+",年齡:"+user.Age); } Console.ReadLine(); } }
能夠看出,Linq可讓咱們使用相似Sql的關鍵字來對集合、對象、XML等進行查詢。
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
4.0 | VS2010 | 4.0 | 4.0 |
VB在很早就已經支持了可選參數,而C#知道4了才支持,顧名思義,可選參數就是一些參數能夠是可選的,在方法調用的時候能夠不用輸入。看下面代碼:
public class Test { static void Main() { Console.WriteLine(GetUserInfo()); //姓名:ooec2003,年齡:30 Console.WriteLine(GetUserInfo("oec2004", 20));//姓名:ooec2004,年齡:20 Console.ReadLine(); } public static string GetUserInfo(string name = "oec2003", int age = 30) { return "姓名:" + name + ",年齡:" + age.ToString(); } }
命名實參是在制定實參的值時,能夠同時指定相應參數的名稱。編譯器能夠判斷參數的名稱是否正確,命名實參可讓咱們在調用時改變參數的順序。命名實參也常常和可選參數一塊兒使用,看下面的代碼:
static void Main() { Console.WriteLine(Cal());//9 Console.WriteLine(Cal(z: 5, y: 4));//25 Console.ReadLine(); } public static int Cal(int x=1, int y=2, int z=3) { return (x + y) * z; }
經過可選參數和命名參數的結合使用,咱們能夠減小代碼中方法的重載。
C#使用dynamic來實現動態類型,在沒用使用dynamic的地方,C#依然是靜態的。靜態類型中當咱們要使用程序集中的類,要調用類中的方法,編譯器必須知道程序集中有這個類,類裏有這個方法,若是不能事先知道,編譯時會報錯,在C#4之前能夠經過反射來解決這個問題。看一個使用dynamic的小例子:
dynamic a = "oec2003"; Console.WriteLine(a.Length);//7 Console.WriteLine(a.length);//string 類型不包含length屬性,但編譯不會報錯,運行時會報錯 Console.ReadLine();
您可能會發現使用dynamic聲明變量和C#3中提供的var有點相似,其餘他們是有本質區別的,var聲明的變量在編譯時會去推斷出實際的類型,var只是至關於一個佔位符,而dynamic聲明的變量在編譯時不會進行類型檢查。
dynamic用的比較多的應該是替代之前的反射,並且性能有很大提升。假設有一個名爲DynamicLib的程序集中有一個DynamicClassDemo類,類中有一個Cal方法,下面看看利用反射怎麼訪問Cal方法:
namespace DynamicLib { public class DynamicClassDemo { public int Cal(int x = 1, int y = 2, int z = 3) { return (x + y) * z; } } } static void Main() { Assembly assembly = Assembly.Load("DynamicLib"); object obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Type type = obj.GetType(); MethodInfo method = type.GetMethod("Cal"); Console.WriteLine(method.Invoke(obj, new object[] { 1, 2, 3 }));//9 Console.ReadLine(); }
用dynamic的代碼以下:
Assembly assembly = Assembly.Load("DynamicLib"); dynamic obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Console.WriteLine(obj.Cal()); Console.ReadLine();
在先後端分離的模式下,WebAPI接口的參數也能夠採用dynamic來定義,直接就能夠解析前端傳入的json參數,不用每個接口方法都定義一個參數類型。很差的地方就是經過Swagger來生產API文檔時,不能明確的知道輸入參數的每一個屬性的含義。
C#4中還有一些COM互操做性的改進和逆變性和協變性的改進,我幾乎沒有用到,因此在此就不講述了。
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
5.0 | VS2012\2013 | 4.0 | 4.5 |
異步處理是C#5中很重要的一個特性,會涉及到兩個關鍵字:async和await,要講明白這個須要單獨寫一篇來介紹。
能夠簡單理解爲,當Winform窗體程序中有一個耗時操做時,若是是同步操做,窗體在返回結果以前會卡死,固然在C#5以前的版本中有多種方法能夠來解決這個問題,但C#5的異步處理解決的更優雅。
與其說是一個特性,不如說是對以前版本問題的修復,看下面的代碼:
public static void CapturingVariables() { string[] names = { "oec2003","oec2004","oec2005"}; var actions = new List(); foreach(var name in names) { actions.Add(() => Console.WriteLine(name)); } foreach(Action action in actions) { action(); } }
這段代碼在以前的C#版本中,會連續輸出三個oec2005,在C#5中會按照咱們的指望依次輸出oec200三、oec200四、oec2005。
若是您的代碼在以前的版本中有利用到這個錯誤的結果,那麼在升級到C#5或以上版本中就要注意了。
咱們的程序一般是以release形式發佈,發佈後很難追蹤到代碼執行的具體信息,在C#5中提供了三種特性(Attribute), 容許獲取調用者的當前編譯器的執行文件名、所在行數與方法或屬性名稱。代碼以下:
static void Main(string[] args) { ShowInfo(); Console.ReadLine(); } public static void ShowInfo( [CallerFilePath] string file = null, [CallerLineNumber] int number = 0, [CallerMemberName] string name = null) { Console.WriteLine($"filepath:{file}"); Console.WriteLine($"rownumber:{number}"); Console.WriteLine($"methodname:{name}"); }
調用結果以下:
filepath:/Users/ican_macbookpro/Projects/CsharpFeature/CsharpFeature5/Program.cs rownumber:12 methodname:Main
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
6.0 | VS2015 | 4.0 | 4.6 |
在C#6中提供了很多的新功能,我認爲最有用的就是Null條件運算符和字符串嵌入。
在C#中,一個常見的異常就是「未將對象引用到對象的實例」,緣由是對引用對象沒有作非空判斷致使。在團隊中雖然再三強調,但依然會在這個問題上栽跟頭。下面的代碼就會致使這個錯誤:
class Program { static void Main(string[] args) { //Null條件運算符 User user = null; Console.WriteLine(user.GetUserName()); Console.ReadLine(); } } class User { public string GetUserName() => "oec2003"; }
要想不出錯,就須要對user對象作非空判斷
if(user!=null) { Console.WriteLine(user.GetUserName()); }
在C#6中能夠用很簡單的方式來處理這個問題
//Null條件運算符 User user = null; Console.WriteLine(user?.GetUserName());
注:雖然這個語法糖很是簡單,也很好用,但在使用時也須要多想一步,當對象爲空時,調用其方法返回的值也是空,這樣的值對後續的操做會不會有影響,若是有,仍是須要作判斷,並作相關的處理。
字符串嵌入能夠簡化字符串的拼接,很直觀的就能夠知道須要表達的意思,在C#6及以上版本中都應該用這種方式來處理字符串拼接,代碼以下:
//字符串嵌入 string name = "oec2003"; //以前版本的處理方式1 Console.WriteLine("Hello " + name); //以前版本的處理方式2 Console.WriteLine(string.Format("Hello {0}",name)); //C#6字符串嵌入的處理方式 Console.WriteLine($"Hello {name}");
C# | VS版本 | .NET Framework |
---|---|---|
7.0 | VS2017 15.0 | .NET Core1.0 |
7.1 | VS2017 15.3 | .NET Core2.0 |
7.2 | VS2017 15.5 | .NET Core2.0 |
7.3 | VS2017 15.7 | .NET Core2.1 |
此特性簡化了out變量的使用,以前的版本中使用代碼以下:
int result = 0; int.TryParse("20", out result); Console.WriteLine(result);
優化後的代碼,不須要事先定義一個變量
int.TryParse("20", out var result); Console.WriteLine(result);
這也是一個減小咱們編碼的語法糖,直接看代碼吧
public class PatternMatching { public void Test() { Listlist = new List(); list.Add(new Man()); list.Add(new Woman()); foreach (var item in list) { //在以前版本中此處須要作類型判斷和類型轉換 if (item is Man man) Console.WriteLine(man.GetName()); else if (item is Woman woman) Console.WriteLine(woman.GetName()); } } } public abstract class Person { public abstract string GetName(); } public class Man:Person { public override string GetName() => "Man"; } public class Woman : Person { public override string GetName() => "Woman"; }
詳細參考官方文檔:https://docs.microsoft.com/zh-cn/dotnet/csharp/pattern-matching
能夠在方法中寫內部方法,在方法中有時須要在多個代碼邏輯執行相同的處理,以前的作法是在類中寫私有方法,如今可讓這個私有方法寫在方法的內部,提升代碼可讀性。
static void LocalMethod() { string name = "oec2003"; string name1 = "oec2004"; Console.WriteLine(AddPrefix(name)); Console.WriteLine(AddPrefix(name1)); string AddPrefix(string n) { return $"Hello {n}"; } }
這個最大的好處是,在控制檯程序中調試異步方法變得很方便。
static async Task Main() { await SomeAsyncMethod(); }
能夠限制在同一個程序集中的派生類的訪問,是對protected internal的一種補強,protected internal是指同一程序集中的類或派生類進行訪問。
每一個特性都須要咱們去編碼實現下,瞭解了真正的含義和用途,咱們才能在工做中靈活的運用。
本文所涉及到的實例代碼後面也會上傳到Github上。
但願本文對您有所幫助。