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

不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如以为我翻译有问题请挪步原博客地址javascript

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

在这篇文章中,请您和我一块儿浏览C#的各类版本,并分享每一个版本中我最喜欢的特性。我将在强调实用性的同时展现其优势。java

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

C#1.0版本

C#1.0版本(ISO-1)真的是一种很是无趣的东西,没有什么特别使人兴奋的东西,并且它缺乏不少开发者喜欢的语言。然而,有一种特别的特征,我认为是我最喜欢的。- 隐式和显式接口实现。数据库

接口一直在使用,而且在现代的C#中仍然很流行。如下面的IDateProvider接口为例。编程

public interface IDateProvider
{
    DateTime GetDate();
}

没有什么特别的,如今设想两个实现 - 其中第一个隐式实现以下:swift

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

第二个显示实现是这样的:c#

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

注意显式实现如何省略访问修饰符。此外,方法名称被写为IDateProvider.GetDate(),,它将接口名称做为限定符的前缀。api

上面两个例子使实现更加明确安全

显式接口实现的一个简洁之处是,它强制用户依赖于接口。显式实现接口的类的实例对象没有可用的接口成员 - 而是必须使用接口自己。markdown

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