[搬运] 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 接口为例。api

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() ,它将接口名称做为限定符的前缀。
这两件事情使得调用更明确的。网络

显式接口实现的一个很好的方面是它强制消费者依赖于接口。显式实现接口的实例对象必须使用接口自己,而没有其余可用的接口成员!dom

hidden-interface-members.png

可是,当您将其声明为接口或将此实现做为指望接口的参数传递时,成员将如预期可用。异步

interface-members.png

这是特别有用的方面,由于它强制使用接口。经过直接使用接口,不会将代码耦合到底层实现。一样,明确的接口实现避免命名或方法签名歧义 - 并使单个类能够实现具备相同成员的多个接口。async

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 曾指出空引用将形成十亿美圆损失)

引用

相关文章
相关标签/搜索