C#與C++的發展歷程第四 - C#6的新時代

C#6.0隨着.NET Framework 4.6而來,.NET Framework 4.6相較於.NET Framework 4.5(包括4.5.一、4.5.2等)變化不是太大,C#6.0也不像以前版本升級時總有幾個吸引眼球的變化,而只是一些語法糖般的變化,不過這個版本在編譯器方面作了許多工做。每次升級總會有一些側重點吧,並且C#發展這麼多年,這麼多特性已經使其位於頂級編程語言行列(從編碼溫馨度來看甩Java好幾條街),實在是也很難再有什麼突破性變化了吧。html

本文整理下C# 6的新變化,但願對看到的園友有必定幫助。mysql

C#6開始,C#和C++有了很大的不一樣,本文也再也不繼續介紹C#6新特性對應的C++特性。git

自動屬性改進

1.只讀自動屬性 能夠聲明真正的只讀(不可變)屬性(自動屬性),在以前版本中若是要讓自動屬性不可寫,惟一的方法就是將set設置爲private。下面的示例提供了新舊兩種代碼的對比。github

// 之前
public string Name {get; private set; }

// 當前
public string Name {get; }

對於這種只有get的只讀自動屬性,能夠在構造函數中進行賦值,或者使用下面這種新的初始化語法進行賦值。web

2.自動屬性初始化
可使用以下的語法對自動屬性或自動只讀屬性進行初始化。sql

public string Name {get; set;} = "World";

public string Name {get; } = "World";

注意賦值語句最後有個分號,固然少了這個分號,VS立馬就給錯誤提示了。編程

表達式體做爲函數實現

對於不少只有一行代碼的函數,使用這個新特性能夠減小一對括號,使代碼看起來更簡潔。以下面兩種方法是等價的。canvas

public string Hello(string name)
{
    return $"Hello, {name}";
}

public string Hello(string name) => $"Hello, {name}";

對於只讀屬性也可使用這個特性,如:ruby

// .NET Core由Configuration中讀取配置
public string DefaultFileName => this._configuration["DefaultFileName"];

null條件運算符

這個運算符有兩種形式,分別爲?.?[]。在C#支持這個運算符以前,咱們訪問引用類型對象的屬性或索引器都須要首先判斷該對象是否爲空以避免發生「空引用」異常。常見寫法如:markdown

if (section != null)
{
    var path = section[name];
}

而使用null條件運算符能夠將上面的語句簡化爲:

var path = section?[name];

雖然看似只是消滅了兩行括號,但看過周愛民老師的《JavaScript語言精粹》後瞭解到這是由過程式語言到函數式語言一種轉變,即由命令語句轉變爲表達式。
若是訪問的屬性爲引用類型,經過null條件運算符獲得的結果的類型不變,而若是訪問的屬性爲值類型,則經過null條件運算符獲得的結果的類型爲該值類型對於的可空類型的包裝。
即若是name爲string類型,則path依然爲string類型。而若是name爲int類型,則path會變成int?類型。能夠經過??使path的類型和name的類型一致:

var path = section?[name]??0;

雖然null條件運算符能夠大大減小運行時錯誤(忘了檢查引用是否爲空)的發生。但不能忽視因爲引用類型對象爲空而致使屬性取默認值所帶來的結果錯誤。

null可空運算符不僅對訪問屬性、索引器等,也能夠用來調用方法或觸發事件。以下面兩種寫法:

if (_mysqlConn != null)
{
    MysqlConn.Close();
}

_mysqlConn?.Close();

調用事件也是同理:

PropertyChanged?.Invoke(e);

using導入靜態類型

在這個特性出現以前,咱們使用using指令只能引入命名空間。這個特性出現後,也能夠將類型導入,從而能夠直接調用類型中的靜態方法。下面的例子能夠很好的展現這個語法的使用:

using static System.String;

return !IsNullOrEmpty(path);

至今位置在實際編碼過程當中沒發現這個新功能有啥太明顯的做用。可能惟一能少碼一些代碼的地方就像控制檯應用程序中能夠經過導入Console類,來減小調用頻繁調用Console.WriteLine()方法時的輸入量。

字符串插值

在這個特性出現以前,咱們用的最多的的字符串插值方法就是string.Format(),如:

var str = string.Format("{0}-{1}",No,Name);

string.Format()的主要缺點就是很容易弄亂參數與佔位符的位置,致使拼出錯誤的字符串。
如今有了這個特性,string.Format()方法基本能夠退役了。以前的代碼能夠直接改寫爲下面的樣子:

var str = $"{No}-{Name}";

之前用於string.Format()佔位符的格式化字符串對於字符串插值語法也有效:

var str = string.Format("{0:00}",No);
var str = $"{No:00}";

對於時間格式化也能夠按以下簡化,而且這種寫法能夠更容易的把時間「融入」到字符串中:

var dateStr = DateTime.Now.ToString("yyyy-MM-dd");
var dataStr = $"{DateTime.Now:yyyy-MM-dd}";

更強大的是$能夠和@結合使用,這樣遇到多行字符串,使用@表示的字符串字面量能夠直接寫成多行,同時可使用$來實現的字符串插值。對於在代碼中嵌入SQL來講這是一個很是方便的特性。

var sql = $@"insert into {table} (fromid,toid,strength)
    values ({fromid},{toid},{strength})";

如上面這個字符串,咱們既不須要用+作多行鏈接,又不用寫string.Format(),整個代碼看上去幹淨、整潔。
只要是C#表達式,即便包含很是複雜的計算也均可以用於字符串插值。

nameof關鍵字

nameof的功能很單一,就是獲取一個符號的名稱,這個「符號」能夠是參數,成員,屬性等。nameof的一個用途經過下面的例子來展現:

以下方法是一個常見的檢查參數是否爲空的方法(這段代碼本身的項目用了好久,但忘記最初是從哪「借鑑」的了):

public static void CheckNotNull<T>(this T value, string paramName) where T : class
{
    Require<ArgumentNullException>(value != null, string.Format(Resources.ParameterCheck_NotNull, paramName));
}
// Require方法的實現省略,其功能是檢查參數值是否爲空,若是爲空記錄一條含有參數名稱的日誌

調用這個方法也很簡單:

public void Process(int no, string name)
{
    no.CheckNotNull(nameof(no));
    name.CheckNotNull(nameof(name));
    // ...省略
}

在nameof關鍵字出現以前,咱們只能寫常量字符串。

no.CheckNotNull("no");

若是參數名一直不變,這樣的常量字符串寫法就不會有問題。但現實狀況是項目重構常常會發生,一但參數名改變,咱們可能會忘記修改常量字符串。而若是咱們使用nameof關鍵字,咱們在更改參數名的同時VS這樣的IDE都是自動幫咱們進行重構,把全部用到此參數的地方都進行重命名操做。

另一個nameof經常使用的場景是如WPF這種的XAML應用中,當屬性須要觸發PropertyChanged時便利性會有很大提高。在nameof關鍵出現以前,MVVMLight庫的作法是要求傳入一個lambda表達式,經過解析Lambda表達式體來使調用強類型話,並保證傳給PropertyChanged的參數名是正確的。代碼以下:

public string Name
{
    get { return _name; }
    // Set方法會最終調用下面的RaisePropertyChanging方法
    set { Set(() => Name, ref _name, value); }
}

// MvvmLight源代碼(部分,來自ObservableObject.cs文見)
protected virtual void RaisePropertyChanging<T>(Expression<Func<T>> propertyExpression)
{
    var handler = PropertyChanging;
    if (handler != null)
    {
        var propertyName = GetPropertyName(propertyExpression);
        handler(this, new PropertyChangingEventArgs(propertyName));
    }
}

如今有了nameof關鍵字,上面的Name屬性能夠實現爲:

public string Name
{
    get { return _name; }
    set
    {
        if (value != _name)
        {
            _name = value;
            PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(nameof(UXComponents.ViewModel.Name)));
        }
    }
}

節省的代碼和運算複雜度都是不少的。

異常過濾器

異常過濾去用於在catch捕獲異常前進行一次過濾。博主尚未在項目中用到過這個特性,這裏用MSDN上的一段代碼來講明。

public static async Task<string> MakeRequest()
{ 
    var client = new HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try {
        var responseText = await streamTask;
        return responseText;
    } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
    {
        return "Site Moved";
    }
}

catch語句中when開始那部分就是新增的異常過濾器。
若是when後面語句(即異常過濾器)執行結果爲truecatch段中的代碼會正常執行,而若是異常過濾器執行結果爲falsecatch段會被跳過。
在異常過濾器出現以前,相似功能的代碼要實現爲:

try {
    var responseText = await streamTask;
    return responseText;
} catch (System.Net.Http.HttpRequestException e)
{
    if (e.Message.Contains("301"))
        return "Site Moved";
    else
        throw;
}

可是以前這種實現方式中經過throw來從新拋出異常會致使一些異常信息丟失。而使用異常過濾器返回false跳過的異常會保留全部原始的異常信息。
異常過濾器也是疊加使用,如:

try {
    var responseText = await streamTask;
    return responseText;
} catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
{
    return "Site Moved";
} catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("304"))
{
    return "Use the Cache";
}

第二個推薦的異常過濾器的使用模式是須要將一個更泛化的異常catch放在具體的catch以前的。
好比,記錄日誌這種需求,咱們須要在一個泛化的異常catch中記錄日誌,但不處理異常,異常能夠繼續向下傳遞,並被更具體的catch進行處理。

能夠實現一個這樣的記錄異常的擴展方法。

public static bool LogException(this Exception e)
{
    Console.Error.WriteLine(@"Exceptions happen: {e}");
    return false;
}

而後能夠像以下這樣進行使用:

try {
    PerformFailingOperation();
} catch (Exception e) when (e.LogException())
{
    // This is never reached!
}
catch (RecoverableException ex)
{
    Console.WriteLine(ex.ToString());
}

因爲上面的LogException方法返回false,因此第一個catch不會處理異常,異常會向下傳播並被第二個catch所捕獲。

第三個異常過濾器的使用場景是用於區分在調試模式下和生產模式下的異常的處理。

try {
    PerformFailingOperation();
}
catch (RecoverableException ex) when (!System.Diagnostics.Debugger.IsAttached)
{
    Console.WriteLine(ex.ToString());
}

如上代碼,在附加調試器的狀況下catch將不被執行,異常向下拋出並被調試器捕獲從而進入調試狀態。而在生產模式,異常會被捕獲並處理。
在這個特性出現以前,若是咱們想方便的調試出現的異常最多見的方法就是在catch段的第一行打上斷點。而如今有了異常過濾器,只須要添加這樣一個when子句就能夠了。

C#異常過濾器的語法有點支持模式匹配的語言的影子,聽說C#7會全面支持模式匹配。期待一下。

索引初始化器

在C#3起出現的集合初始化器可使咱們用以下這樣的方式去初始化ListDictionary。代碼例子以前的博文

List<Plant> plants = new List<Plant> {
    new Plant { Name = "牡丹", Category = "芍藥科", ImageId =6},
    new Plant { Name = "蓮", Category = "蓮科", ImageId =10 },
    new Plant { Name = "柳", Category = "楊柳科", ImageId = 12 }
};

Dictionary<int, Plant> plantsDic = new Dictionary<int, Plant>
{
    { 11, new Plant { Name = "牡丹", Category = "芍藥科", ImageId =6}},
    { 12, new Plant { Name = "蓮", Category = "蓮科", ImageId =10 }},
    { 13, new Plant { Name = "柳", Category = "楊柳科", ImageId = 12 }}
};

C#6中新增了一種索引初始化器,可使Dictionary的初始化更直觀:

Dictionary<int, Plant> plantsDic = new Dictionary<int, Plant>
{
    [11] = new Plant { Name = "牡丹", Category = "芍藥科", ImageId =6},
    [12] = new Plant { Name = "蓮", Category = "蓮科", ImageId =10 },
    [13] = new Plant { Name = "柳", Category = "楊柳科", ImageId = 12 }
};

添加Add擴展方法使類支持集合初始化去

咱們按以下方式實現一個集合類:

public class Enrollment : IEnumerable<Plant>
{
    private List<Plant> allPlants = new List<Plant>();

    public void Add(Plant s)
    {
        allPlants.Add(s);
    }

    public IEnumerator<Plant> GetEnumerator()
    {
        return ((IEnumerable<Plant>)allPlants).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<Plant>)allPlants).GetEnumerator();
    }
}

因爲這個類的實現有符合要求的Add方法,咱們可使用集合初始化器來給類的成員變量添加對象。

Plantation plantation = new Plantation()
{
    new Plant {Name = "牡丹", Category = "芍藥科", ImageId = 6},
    new Plant {Name = "蓮", Category = "蓮科", ImageId = 10},
    new Plant {Name = "柳", Category = "楊柳科", ImageId = 12}
};

但若是因爲各類緣由,咱們的Add方法被命名爲其它名稱,如:

public void Plant(Plant s)
{
    allPlants.Add(s);
}

則集合初始化器方式再也不可用。爲了讓集合初始化其繼續可用,能夠添加下面這樣的擴展方法:

public static class PlantExtensions
{
    public static void Add(this Plantation e, Plant s) => e.Plant(s);
}

這樣集合初始化器就又能夠用了。

其它

  1. struct中能夠聲明無參構造函數。在以前版本的C#中,struct只能包含有參構造函數。
  2. 能夠在catch/finally使用await語句了,一個典型的做用就是須要在catch中使用異步的logger方法這樣的狀況。
  3. C#6新的編譯器會更智能的區分Task.Run(Action)Task.Run(Func<Task>())這種的重載,再遇到Task DoThings(){ }這種簽名的重載時會智能的選擇後者。

提示:
C#語言的編譯與項目所依賴的.Net Framework版本無關。雖然VS在2015版本才內置支持C#6.0的編譯器,但咱們仍然可使用VS2015編寫基於.Net Framework 3.5甚至更早版本Framework的項目並享受C#6帶來的如字符串插值等便利特性。
若是想脫離VS編譯C#6的項目,須要使用隨VS2015安裝的Microsoft Build Tools 2015(也能夠單獨下載安裝,安裝位置在%SystemDrive%\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe),而不能使用位於%SystemDrive%\Windows\Microsoft.NET\Framework64\v4.0.30319中的Build Tool。

展望

C#7應該年末就會到來,對C#7比較期待的幾點包括「外觀」很簡單的值類型元組,對象展開功能。有了這些C#7就能達到比Python還要流暢的代碼編寫感覺了。與各位C#er共勉。

相關文章
相關標籤/搜索