[搬運] C# 這些年來受歡迎的特性

原文地址:http://www.dotnetcurry.com/csharp/1411/csharp-favorite-features

在寫這篇文章的時候,C# 已經有了 17 年的歷史了,可以肯定地說它並沒有去任何地方。C# 語言團隊不斷致力於開發新特性,改善開發人員的體驗。

在這篇文章中,我在介紹 C# 歷史版本的同時分享我最喜歡的特性,在強調實用性的同時展示其優點。
csharp-versions.jpg

C# 1.0

C#1.0 (ISO-1) 確實算是語言,卻沒有什麼令人興奮的,缺少許多開發人員喜歡的特性。仔細一想,我能說得出喜歡的只有一個特別的特性 - 隱式和顯式接口實現

接口在現今開發 C# 的過程中仍然流行使用,以下面的 IDateProvider 接口爲例。

public interface IDateProvider
{
    DateTime GetDate();
}

沒有什麼特別的,現在着手兩種實現方式 - 其中第一種是隱式實現,如下:

public class DefaultDateProvider : IDateProvider
{
    public DateTime GetDate()
    {
        return DateTime.Now;
    }
}

第二種實現是如下的顯式實現方式:

public class MinDateProvider : IDateProvider
{
    DateTime IDateProvider.GetDate()
    {
        return DateTime.MinValue;
    }
}

注意顯式實現如何省略訪問修飾符。此外,方法名稱被寫爲 IDateProvider.GetDate() ,它將接口名稱作爲限定符的前綴。
這兩件事情使得調用更明確的。

顯式接口實現的一個很好的方面是它強制消費者依賴於接口。顯式實現接口的實例對象必須使用接口本身,而沒有其他可用的接口成員!

hidden-interface-members.png

但是,當您將其聲明爲接口或將此實現作爲期望接口的參數傳遞時,成員將如預期可用。

interface-members.png

這是特別有用的方面,因爲它強制使用接口。通過直接使用接口,不會將代碼耦合到底層實現。同樣,明確的接口實現避免命名或方法簽名歧義 - 並使單個類可以實現具有相同成員的多個接口。

Jeffery Richter 在他 CLR via C# 一書中提醒了我們顯式的接口實現兩個主要問題是值類型實例在投射到一個接口和明確實現的方法時將被裝箱,同時不能被派生類調用。

請記住,裝箱和拆箱會影響性能。任何編程中,你應該評估用例來確保善用工具。

C# 2.0

作爲參考,我將列出C# 2.0 (ISO-2) 的所有特性。

  • 匿名方法
  • 協變和逆變
  • 泛型
  • 迭代器
  • 可空類型
  • 部分類型

我最在最喜歡 泛型 還是 迭代器 之間的搖擺,對我來說這是一個非常困難的選擇,最終還是更喜歡泛型,順便說說其中緣由。

因爲相比於寫迭代器,我更頻繁地使用泛型。在 C# 中很多 SOLID 編程原則都是使用泛型來強化的,同樣它也有助於保持代碼的 乾爽。不要誤解我的意思,我同時也寫了一些迭代器,在 C# 同樣中值得采用!

讓我們更詳細地看看泛型。

編者注:學習如何 在 C# 中 使用泛型來提高應用程序的可維護性

泛型向.NET Framework引入了類型參數的概念,這使得可以設計類和方法來推遲一個或多個類型的規範,直到類或方法被客戶端代碼聲明和實例化爲止。

讓我們想象一下,我們有一個名爲 DataBag 的類,作爲一個數據包。它可能看起來像這樣:

public class DataBag
{
    public void Add(object data)
    {
        // omitted for brevity...
    }            
}

起初看起來這似乎是一個很棒的想法,因爲你可以在這個 DataBag 的實例中添加任何東西。但是當你真正想到這意味着什麼的時候,會覺得相當駭人。

所有添加的內容都隱式地包裝爲 System.Object 。此外,如果添加了值類型,則會發生裝箱。這些是您應該注意的性能考慮事項。

泛型解決了這一切,同時也增加了類型安全性。讓我們修改前面的例子,在類中包含一個類型參數 T ,並注意方法簽名的變化。

public class DataBag
{
    public void Add(T data)
    {
        // omitted for brevity...
    }
}

例如現在一個 DataBag 實例將只允許調用者添加 DateTime 實例。要類型安全,沒有裝箱或拆箱 ... 讓更美好的事情發生。

泛型類型參數也可以被約束。通用約束是強有力的,因爲它們必須遵守相應的約束條件,只允許有限範圍的可用類型參數。

有幾種編寫泛型類型參數約束的方法,請考慮以下語法:

public class DataBag where T : struct { /* T 值類型 */ }
public class DataBag where T : class { /* T 類、接口、委託、數組 */ }
public class DataBag where T : new() { /* T 有無參構造函數 */ }
public class DataBag where T : IPerson { /* T 繼承 IPerson */ }
public class DataBag where T : BaseClass { /* T 派生自 BaseClass */ }
public class DataBag where T : U { /* T 繼承 U, U 也是一個泛型參數 */ }

多個約束是允許的,用逗號分隔。類型參數約束立即生效,即編譯錯誤阻止程序員犯錯。考慮下面的DataBag約束。

public class DataBag where T : class
{
    public void Add(T value)
    {
        // omitted for brevity...
    }
}

現在,如果我試圖實例化DataBag,C#編譯器會讓我知道我做錯了什麼。更具體地說,它要求類型 'DateTime' 必須是一個引用類型,以便將其作爲 'T' 參數用於泛型類型或 'Program.DataBag' 方法中。

C# 3.0

下面是C#3.0的主要特性列表。

  • 匿名類型
  • 自動實現的屬性
  • 表達樹
  • 擴展方法
  • Lambda表達
  • 查詢表達式

我徘徊於選擇 Lambda表達式 還是 擴展方法 。但是,聯繫我目前的 C# 編程,相對於任何其他的 C# 運算符,我更多地使用lambda 操作符。我無法表達對它的喜愛。
在C#中有很多機會來利用 lambda 表達式和 lambda 運算符。=> lambda 運算符用於將左側的輸入與右側的 lambda 表達式體隔離開來。

一些開發人員喜歡將 lambda 表達式看作是表達委託調用的一種較爲冗長的方式。Action、Func 類型只是 System 名稱空間中的預定義的一般委託。

讓我們從解決一個假設的問題開始,使用 lambda 表達式來幫助我們編寫一些富有表現力和簡潔的 C# 代碼。

想象一下,我們有大量代表趨勢天氣信息的記錄。我們可能希望對這些數據執行一些操作,不是在一個典型的循環中遍歷它,而是在某個時候,我們可以採用不同的方式。

public class WeatherData
{
    public DateTime TimeStampUtc { get; set; }
    public decimal Temperature { get; set; }
}
 
private IEnumerable GetWeatherByZipCode(string zipCode) { /* ... */ }

由於 GetWeatherByZipCode 的調用返回一個 IEnumerable,它可能看起來想讓你循環迭代。假設我們有一個方法來計算平均溫度。

private static decimal CalculateAverageTemperature(
    IEnumerable weather, 
    DateTime startUtc, 
    DateTime endUtc)
{
    var sumTemp = 0m;
    var total = 0;
    foreach (var weatherData in weather)
    {
        if (weatherData.TimeStampUtc > startUtc &&
            weatherData.TimeStampUtc < endUtc)
        {
            ++ total;
            sumTemp += weatherData.Temperature;
        }
    }
    return sumTemp / total;
}

我們聲明一些局部變量來存儲所有過濾日期範圍內的溫度總和及其總和,以便稍後計算平均值。在迭代內是一個 if 邏輯塊,用於檢查天氣數據是否在特定的日期範圍內。可以重寫如下:

private static decimal CalculateAverageTempatureLambda(
    IEnumerable weather,
    DateTime startUtc,
    DateTime endUtc)
{
    return weather.Where(w => w.TimeStampUtc > startUtc &&
                              w.TimeStampUtc  w.Temperature)
                  .Average();
}

正如你所看到的那樣,極大地簡化了代碼。if 邏輯塊實際上只是一個謂詞,如果天氣日期在範圍內,我們將繼續進行一些額外的處理 - 就像一個過濾器。然後就像調用 Average 一樣,當我們需要合計溫度時,我們只需要投射 (或選擇) IEnumerable 的溫度過濾列表。

在 IEnumerable 接口上的 Where 和 Select 擴展方法中,使用 lambd a 表達式作爲參數。Where 方法需要一個 Func<T, bool> ,Select 方法 需要一個 Func 。

C# 4.0

相比之前的版本,C# 4.0 新增的主要特性較少。

  • 動態綁定
  • 嵌入式互操作類型
  • 泛型中的協變和逆變
  • 命名/可選參數

所有這些特性都是非常有用的。但是對於我來說,更傾向於命名可選參數,而不是泛型中的協變和逆變。這兩者的取捨,取決於哪個是我最常用的,以及近年來最令 C# 開發人員受益的那個特性。

命名可選參數實至名歸,儘管這是一個非常簡單的特性,其實用性卻很高。我就想問,誰沒有寫過重載或者帶有可選參數的方法?

當您編寫可選參數時,您必須爲其提供一個默認值。如果你的參數是一個值類型,那麼它必須是一個文字或者常數值,或者你可以使用 default 關鍵字。同樣,您可以將值類型聲明爲 Nullable ,並將其賦值爲 null 。假設我們有一個帶有 GetData 方法的倉儲。

public class Repository
{
    public DataTable GetData(
        string storedProcedure,
        DateTime start = default(DateTime),
        DateTime? end = null,
        int? rows = 50,
        int? offSet = null)
    {
        // omitted for brevity...        
    }
}

正如我們所看到的,這個方法的參數列表相當長 - 好在有好幾個可選參數。因此,調用者可以忽略它們,並使用默認值。正如你聲明的那樣,我們可以通過只傳遞 storedProcedure 參數來調用它。

var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales");

現在我們已經熟悉了可選參數特性以及這些特性如何工作,順便使用一下命名參數。以上面的示例爲例,假設我們只希望我們的數據表返回 100 行而不是默認的 50 行。我們可以將我們的調用改爲包含一個命名參數,並傳遞所需的重寫值。

var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales", rows: 100);

C# 5.0

像C#4.0版本一樣,C#5.0版本中沒有太多特性 - 但是其中有一個特性非常強大。

  • 異步/等待
  • 調用方信息

當 C# 5.0 發佈時,它實際上改變了 C# 開發人員編寫異步代碼的方式。今天仍然有很多困惑,我在這裏向您保證,這比大多數人想象的要簡單得多。這是 C# 的一個重大飛躍 - 它引入了一個語言級別的異步模型,它極大地賦予了開發人員編寫外觀和感覺同步 (或者至少是連續的) 的“異步”代碼。

異步編程在處理 I/O 相關(如與數據庫、網絡、文件系統等進行交互)時非常強大。異步編程通過使用非阻塞方法幫助處理吞吐量。這種機制在透明的異步狀態機中代以使用暫停點和相應的延續的方式。

同樣,如果 CPU 負載計算的工作量很大,則可能需要考慮異步執行此項工作。這將有助於用戶體驗,因爲UI線程不會被阻塞,而是可以自由地響應其他UI交互。

編者注:關於 C# 異步編程中使用異步等待的最佳實踐,http://www.dotnetcurry.com/csharp/1307/async-await-asynchronous-programming-examples

在 C# 5.0 中,當語言添加了兩個新的關鍵字async和await時,異步編程被簡化了。這些關鍵字適用於 Task 和 Task 類型。下表將作爲參考:
async-await.png

Task 和 Task 類型表示異步操作。這些操作既可以通過返回一個 Task ,也可以返回void Task。當您使用 async 關鍵字修改返回方法時,它將使方法主體能夠使用await 關鍵字。在評估 await 關鍵字時,控制流將返回給調用者,並在該方法中的那一點暫停執行。當等待的操作完成時,會同時恢復執行。

class IOBoundAsyncExample
{
    // Yes, this is the internet Chuck Norris Database of jokes!
    private const string Url = "http://api.icndb.com/jokes/random?limitTo=[nerdy]";
 
    internal async Task GetJokeAsync()
    {
        using (var client = new HttpClient())
        {
            var response = await client.GetStringAsync(Url);
            var result = JsonConvert.DeserializeObject(response);
 
            return result.Value.Joke;
        }
    }
}
public class Result
{
    [JsonProperty("type")] public string Type { get; set; }
    [JsonProperty("value")] public Value Value { get; set; }
}
 
public class Value
{
    [JsonProperty("id")] public int Id { get; set; }
    [JsonProperty("joke")] public string Joke { get; set; }
}

我們用一個名爲 GetJokeAsync 的方法定義一個簡單的類,當我們調用方法時,該方法返回一個 Task 。對於調用者,GetJokeAsync 方法最終會給你一個字符串 - 或可能出錯。

當響應返回時,從被暫停的地方恢復延續執行。然後,將結果 JSON 反序列化到 Result類的實例中,並返回 Joke 屬性。

C# 6.0

C# 6.0 有很多很不錯的改進,很難選擇我最喜歡的特性。

  • 字典初始化
  • 異常過濾器
  • 表達式體成員
  • nameof 操作符
  • 空合併運算符
  • 屬性初始化
  • 靜態引用
  • 字符串插值

我把範圍縮小到三個突出的特性:字符串插值,空合併運算符和 nameof 操作符。

儘管 nameof 操作符很棒,而且我經常用,但是顯然另外兩個特性更具影響力。又是一個兩難的選擇,最終還是字符串插值獲勝出。

空合併運算符很有用,它能讓我少寫代碼,但不一定防止我的代碼中的錯誤。而使用字符串插值時,可以防止運行時出錯。

使用 $ 符號插入字符串文字時,將啓用 C# 中的字符串插值語法。相當於告訴 C# 編譯器,我們要用到各種 C# 變量、邏輯或表達式來插入到此字符串。這對於手動拼接字符串、甚至是 string.Format 方法來說是一個重要的升級。先看一看如下代碼:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
 
    public override string ToString()
        => string.Format("{0} {1}", FirstName);
}

我們有一個簡單的 Person 類,具有兩個屬性,表示名字和姓氏。我們使用 string.Format 重寫 ToString 方法。問題是,編譯時,開發人員在希望將姓氏也作爲結果字符串的一部分時,使用 “{0} {1} ”參數很容易出錯。如上述代碼中,他們忘了加姓氏。同樣,開發人員可以很容易地交換參數位置,在混亂的格式文字只傳遞了第一個索引,等等...現在考慮用字符串插值實現。

class Person
{
    public string FirstName { get; set; } = "David";
    public string LastName { get; set; } = "Pine";
    public DateTime DateOfBirth { get; set; } = new DateTime(1984, 7, 7);
 
    public override string ToString()
        => $"{FirstName} {LastName} (Born {DateOfBirth:MMMM dd, yyyy})";
}

我冒昧添加 DateOfBirth 屬性和一些默認的屬性值。另外,我們現在使用字符串插值重寫 ToString 方法。作爲一名開發人員,犯上述錯誤要困難得多。最後,我也可以在插值表達式中進行格式化。注意第三次插值,DateOfBirth 是 DateTime 類型 - 因此我們可以使用習慣的所有標準格式。只需使用 :運算符來分隔變量和格式化。

示例輸出:

  • David Pine (Born July 7, 1984)

編者注:有關C#6.0新特性的詳細內容,請閱讀 http://www.dotnetcurry.com/csharp/1042/csharp-6-new-features

C# 7.0

  • 表達式體成員
  • 局部方法
  • Out 變量
  • 模式匹配
  • 局部引用和引用返回
  • 元組和解構

模式匹配、元組和 Out 變量之間,我選擇了 Out 變量。
模式匹配是偉大的,但我真的不覺得自己經常使用它,至少現在還沒有。也許我會在將來更多地使用它,但是到目前爲止我所寫的所有 C# 代碼中,沒有太多的地方可以運用。再次,這是一個了不起的特性,只不過不是我最喜歡的 C# 7.0 特性。

元組也是一個很好的改進,是服務於語言的這一重要部分,能成爲一等公民真是值得慶祝。逃離了 .Item1,.Item2,.Item3等...的日子,但這麼說不夠準確,在反序列化中無法還原元組名稱使這個公共 API 不太有用。

我同時不喜歡可變的 ValueTuple 類型。不明白這是誰設計的,希望有人能向我解釋,感覺就像是一個疏忽。因此,只有 Out 變量合我心意。

從 C# 版本1.0以來,try-parse 模式已經在各種值類型中出現了。模式如下:

public boolean TryParse(string value, out DateTime date)
{
    // omitted for brevity...
}

該函數返回一個布爾值,指示給定的字符串值是否能夠被解析。如果爲 true,則將解析後的值分配給 data參數。它使用方式如下:

if (DateTime.TryParse(someDateString, out var date))
{
    // date is now the parsed value
}
else
{
    // date is DateTime.MinValue, the default value
}

這種模式儘管有用的,卻有點麻煩。有時開發人員採取相同的模式,無論解析是否成功。有時可以使用默認值。C# 7.0中的 out變量使得這個更加複雜,儘管我不覺得複雜。

if (DateTime.TryParse(someDateString, out var date))
{
    // date is now the parsed value
}
else
{
    // date is DateTime.MinValue, the default value
}

現在我們移除了 if 語句塊的外部聲明,並把聲明作爲參數本身的一部分。使用 var 是合法的,因爲類型是已知的。最後,date 變量的範圍沒有改變。它在聲明中內聯回 if 語句塊之前。

你可能會問:“爲什麼這是我最喜歡的功能之一?”......這種看起來真的沒有什麼變化。

不要懷疑,它使我們的 C# 代碼更具有表現力。每個人都喜歡擴展方法吧,那麼請思考以下代碼:

public static class StringExtensions
{
    private delegate bool TryParseDelegate(string s, out T result);
 
    private static T To(string value, TryParseDelegate parse)
        => parse(value, out T result) ? result : default;
 
    public static int ToInt32(this string value)
        => To(value, int.TryParse);
 
    public static DateTime ToDateTime(this string value)
        => To(value, DateTime.TryParse);
 
    public static IPAddress ToIPAddress(this string value)
        => To(value, IPAddress.TryParse);
 
    public static TimeSpan ToTimeSpan(this string value)
        => To(value, TimeSpan.TryParse);
}

這個擴展方法類看起來簡潔、明確、強有力。在定義了一個遵循 try-parse 模式的私有委託之後,我們可以編寫一個泛型複合方法,它可以傳遞泛型類型參數、字符串和 tryparse 泛型委託。現在我們可以放心地使用這些擴展方法,用法如下:

ublic class Program
{
    public static void Main(string[] args)
    {
        var str =
            string.Join(
                "",
                new[] { "James", "Bond", " +7 " }.Select(s => s.ToInt32()));
 
        Console.WriteLine(str); // prints "007"
    }
}

編輯注意:要了解C#7的所有新功能,請查看教程 http://www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features

結論

這篇文章對我個人而言頗具挑戰性。C# 的許多特性受我喜歡,因此在每個版本選出一個最喜歡的特性是非常困難的。

每個 C# 版本都包含了強大而有影響力的特性。C# 語言團隊以無數的方式進行創新 - 其中之一就是迭代發佈。在撰寫本文時,C#7.1 和 7.2已正式發佈。作爲 C# 開發人員,我們正在生活在令人激動人心的語言進化時代!

排列出所有特性對我來說是非常有指示,有助於揭示哪些是實際有用的,哪些對我日常影響最大。我會一如既往的努力,成爲務實的開發者!並非每一種特性對於手頭的工作來說都是必要的,但瞭解什麼是可用的是很有必要的。

當我們期待 C# 8 的提議和原型時,我對 C# 的未來感到興奮,它正滿懷信心、積極地試圖減輕 “十億美元的錯誤” (譯者注: 圖靈獎得主 Tony Hoare 曾指出空引用將造成十億美元損失)

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章