不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址本博文翻译自: 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(),,它将接口名称作为限定符的前缀.
上面两个例子使实现更加明确
显式接口实现的一个简洁之处是,它强制用户依赖于接口.显式实现接口的类的实例对象没有可用的接口成员 - 而是必须使用接口本身.
但是,当您将其声明为接口或将此实现作为预期接口的参数传递时,成员将按预期可用.
当它强制使用接口时,这一点特别有用.通过直接使用接口,您不会将代码耦合到底层实现.同样,显式接口实现处理命名或方法签名的模糊性 - 并使单个类可以实现具有相同成员的多个接口.
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.下表将作为参考:
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