C#最受歡迎功能 -- C#1至C#7


不定時更新翻譯系列,此系列更新毫無時間規律,文筆菜翻譯菜求各位看官老爺們輕噴,如覺得我翻譯有問題請挪步原博客地址

本博文翻譯自:
http://www.dotnetcurry.com/csharp/1411/csharp-favorite-features

在這篇文章中,請您和我一起瀏覽C#的各種版本,並分享每個版本中我最喜歡的特性。我將在強調實用性的同時展示其優點。

C#我最喜歡的功能 - V1至V7

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

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

interface-members

當它強制使用接口時,這一點特別有用。通過直接使用接口,您不會將代碼耦合到底層實現。同樣,顯式接口實現處理命名或方法簽名的模糊性 - 並使單個類可以實現具有相同成員的多個接口。

Jeffery Richter在他的書 CLR via C#中警告我們關於顯式接口的現。兩個主要的關注點是,當轉換到顯式實現的接口和方法時,值類型被裝箱,而派生類型不能調用它們。

請記住,裝箱和拆箱會帶來額外能耗,和所有的編程一樣,您應該評估測試用例以確定適合該工作的工具。

C#2.0版本

作為參考,我將列出C#2.0(ISO-2)的所有功能。

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

我最喜歡的功能是介於泛型和迭代器之間,我選擇了泛型,下面我來說說原因。

對我來說這是一個非常困難的選擇,我最終決定了泛型,因為我相信我比寫迭代器更頻繁地使用泛型。很多SOLID編程原則都是通過在C#中使用泛型來優化的,同樣它也有助於保持代碼的簡潔。不要誤解我的意思,我確實寫了很多迭代器,這是一個值得在你的C#中采用的特性!

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

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

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

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

public class DataBag
{
    public void Add(object data) {
        // 為了簡便起見,我們省略了...
    }            
}

乍一看,這似乎是個很棒的主意,因為您可以在這個數據對象包的實例中添加任何東西。但當你真正思考這意味着什么時,這可能是相當令人擔憂的。

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

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

public class DataBag
{
    public void Add(T data) {
       // 為了簡便起見,我們省略了...
    }
}

現在,例如,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) {
        // 為了簡便起見,我們省略了...
    }
}

現在,如果我試圖實例化DataBag,C#編譯器會讓我知道我做錯了什么。更具體地說,它指出:

類型'DateTime'必須是一個引用類型,以便將其用作泛型類型或方法'Program.DataBag'中的參數'T'

C#3.0版本

這里是C#3.0的主要功能列表。

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

我在選擇Lambda表達式的擴展方法的邊緣蹣跚而行。但是,當我思考今天寫的C#時,我實際上比其他任何C#運算符都更多地使用lambda運算符

我喜歡寫富有表現力的C#。

在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<WeatherData> 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<WeatherData> weather, DateTime startUtc, DateTime endUtc) {
    return weather.Where(w => w.TimeStampUtc > startUtc &&
                              w.TimeStampUtc  w.Temperature)
                  .Average();
}

如您所見,這大大簡化了。邏輯if塊實際上只是一個謂詞,如果天氣日期在范圍內,我們將繼續進行一些額外的處理——比如過濾器。然后我們把溫度相加,所以我們只需要把這個項目選擇出來。我們最終得到了一個經過篩選的溫度列表,我們現在可以簡單地調用平均值。

lambda表達式被用作在通用IEnumerable接口上的Where和選擇擴展方法的參數。

C#4.0版本

從以前的版本發布來看,C#4.0的主要特性數量較少。

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

所有這些功能都是非常有用的。但對我來說,它歸結為實名和可選參數,而不泛型協變和逆變。在這兩者之間,我討論了我最常使用哪個特性,並且在多年的時間里,它確實使我受益最大。

我相信這個特性實名/可選的參數。這是一個非常簡單的功能,但實用性得分很高。我的意思是,誰沒有寫一個重載或可選參數的方法?

當您編寫可選參數時,您必須為其提供一個默認值。如果你的參數是一個值類型,那么它必須是一個字面值或者常數值,或者你可以使用default關鍵字。同樣,您可以將值類型聲明為Nullable,並將其賦值為null。讓我們想象我們有一個Repository類,並有一個GetData方法。

public class Repository
{
    public DataTable GetData( string storedProcedure, DateTime start = default(DateTime), DateTime? end = null,
        int? rows = 50,
        int? offSet = null)
    {
        //為了簡便起見,我們省略了... 
    }
}

我們可以看到,這個方法的參數列表相當長,但是有幾個任務。表示這些值是可選的。因此,調用者可以省略它們,並使用默認值。正如您可能假設的那樣,我們可以僅通過提供存儲過程名稱來調用它。

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

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

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

C#5.0版本

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

  • 異步/等待
  • CallerInfoAttributes

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

異步編程在處理I/O綁定工作負載(如與數據庫,網絡,文件系統等進行交互)時非常強大。異步編程通過使用非阻塞方法幫助處理吞吐量。這種方法使用了一個透明的異步狀態機中的掛點和相應的延續。

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

編者注:這里有一些關於C#異步編程的最佳實踐,使用Async Await.

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

async-await

Task表示異步操作。操作可以通過Task返回值,也可以通過Task返回void。當您使用async關鍵字修飾Task返回方法時,它使方法主體可以使用await關鍵字。當您請求await關鍵字的返回值時,控制流將返回給調用者,並且在方法的那個點執行暫停。當await的操作完成后,在同一點上恢復執行。部分代碼如下!

class IOBoundAsyncExample
{
  
    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方法最終會給您一個字符串,或者可能出錯。

該方法使用async關鍵字進行修飾,該關鍵字允許使用等待關鍵字。我們實例化並使用一個HttpClient對象。然后我們調用GetStringAsync函數,它接受一個字符串url並返回一個Task 。我們等待從GetStringAsync調用返回的Task。

當響應已經准備好時,就會繼續發生並控制從我們曾經掛起的位置恢復。然后,我們將JSON反序列化到Result類的實例中,並返回Joke屬性。

一些我最喜歡的成果

  • 查克·諾里斯(Chuck Norris)可以用單一的斷言來測試整個應用程序。
  • 查克·諾里斯(Chuck Norris)可以編譯語法錯誤。
  • 項目經理永遠不會要求查克·諾里斯(Chuck Norris)做出估計。

歡鬧隨之而來!我們了解了C#5.0的驚人的異步編程模型。

C#6.0版本

C#6.0的推出有很多很大的進步,很難選擇我最喜歡的功能。

  • 字典初始化
  • 異常過濾器
  • 在屬性里使用Lambda表達式
  • 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類,具有兩個名稱屬性,用於名字和姓氏。我們重寫ToString方法並使用string.Format。問題是,編譯時,由於開發人員顯然希望將姓氏也作為結果字符串的一部分,因此很容易出錯,這一點在“{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新特性的詳細內容,請閱讀www.dotnetcurry.com/csharp/1042/csharp-6-new-features

C#7.0版本

從所有集成到 C# 7.0的特性中。

  • 更多的函數成員的表達式體
  • 局部函數
  • Out變量
  • 模式匹配
  • 局部變量和引用返回
  • 元組和解構

我結束了模式匹配,元組和Out變量之間的爭論。我最終選擇了Out變量,這是為什么。

模式匹配但是我真的不經常使用它,至少現在還沒有。也許以后我會更多地使用它,但是對於我迄今為止編寫的所有c#代碼,沒有太多地方可以利用它。同樣,這是一個很棒的功能,我確實看到了它的位置 - 只是在C#7.0中這不是我最喜歡的。

元組也是一個很好的補充。元組是語言的重要組成部分,成為一流的公民是非常棒的。我會說,“寫tem1,.Item2,.Item3等...的日子已經過去了,但這並不一定是正確的。反序列化失去了元組的名稱,使得這個公共API不那么有價值

我也不喜歡ValueTuple類型是可變的這一事實。我只是不明白設計者的決定。我希望有人能給我解釋一下,但感覺有點像疏忽。因此,我得到了選擇out變量的特性。

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

public boolean TryParse(string value, out DateTime date) {
    // 為了簡便起見,我們省略了.....
}

該函數返回一個布爾值,指示給定的字符串值是否能夠被解析。如果為true,則將分析的值分配給生成的輸出參數date。它的使用如下:

DateTime date;
if (DateTime.TryParse(someDateString, out date))
{
    //  date現在是解析值
}
else
{
    // date是DateTime.MinValue,默認值
}

這種模式是有用的,但有點麻煩。有時,不管解析是否成功,開發人員都會采取相同的操作過程。有時使用默認值是可以的。C#7.0中的out變量使得這個更復雜,不過在我看來不那么復雜。

示例如下:

if (DateTime.TryParse(someDateString, out var date))
{
    // date現在是解析值
}
else
{
    // date是DateTime.MinValue,默認值
}

現在我們移除了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模式的私有委托之后,我們可以編寫一個泛型復合函數,它需要一個泛型類型的參數、要解析的字符串值和TryParseDelegate。現在我們可以安全地依賴這些擴展方法,考慮以下幾點::

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

        Console.WriteLine(str); // 打印 "007"
    }
}

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

結論

這篇文章對我個人而言頗具挑戰性。我喜歡C#的許多特性,因此每次發布只收集一個最喜歡的內容是非常困難的。

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

然而,對我來說,對所有這些特性進行分類是相當有見地的;因為它幫助我們了解了什么是實際的,最影響我的日常發展。一如既往,努力成為一個務實的開發者!並不是語言中所有可用的特性都是當前任務所必需的,但了解什么是可用的,這一點很重要。

當我們期待C#8的建議和原型時,我對C#的未來感到興奮。它看起來確實很有希望,而且語言正在積極地試圖緩解“價值億萬美金的錯誤”。

歡迎轉載,轉載請注明翻譯原文出處(本文章),原文出處(原博客地址),然后謝謝觀看

如果覺得我的翻譯對您有幫助,請點擊推薦支持:)

 

出處:https://www.cnblogs.com/chen-jie/p/csharp-favorite-features.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM