C# 发展历史及版本新功能介绍

C# 1.0 版

回想起来,C# 1.0 版非常像 Java。 在 ECMA 制定的设计目标中,它旨在成为一种“简单、现代、面向对象的常规用途语言”。 当时,它和 Java 类似,说明已经实现了上述早期设计目标。

不过如果现在回顾 C# 1.0,你会觉得有点晕。 它没有我们习以为常的内置异步功能和以泛型为中心的巧妙功能。 其实它完全不具备泛型。 那 LINQ 呢? 尚不可用。 需要几年后才会面世。

与现在的 C# 相比,C# 1.0 版少了很多功能。 你会发现自己的代码很冗长。 不过凡事总要有个开始。 在 Windows 平台上,C# 1.0 版是 Java 的一个可行的替代之选。

C# 2.0 版

从此以后事情变得有趣起来。 让我们看看 C# 2.0(2005 年发布)和 Visual Studio 2005 中的一些主要功能:

泛型

C# 语言和公共语言运行时 (CLR) 的 2.0 版本中添加了泛型。 泛型将类型参数的概念引入 .NET Framework,这样就可以设计具有以下特征的类和方法:在客户端代码声明并初始化这些类和方法之前,这些类和方法会延迟指定一个或多个类型。 例如,通过使用泛型类型参数 T,可以编写其他客户端代码能够使用的单个类,而不会产生运行时转换或装箱操作的成本或风险,如下所示:

  • // Declare the generic class. public class GenericList<T> { public void Add(T input) { } } class TestGenericList { private class ExampleClass { } static void Main() { // Declare a list of type int. GenericList<int> list1 = new GenericList<int>(); list1.Add(1); // Declare a list of type string. GenericList<string> list2 = new GenericList<string>(); list2.Add(""); // Declare a list of type ExampleClass. GenericList<ExampleClass> list3 = new GenericList<ExampleClass>(); list3.Add(new ExampleClass()); } }

泛型概述

使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。

泛型最常见的用途是创建集合类。

.NET Framework 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应尽可能使用这些类来代替某些类,如 System.Collections 命名空间中的 ArrayList。

可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。

可以对泛型类进行约束以访问特定数据类型的方法。

在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。

分部类型

可以将类或结构、接口或方法的定义拆分到两个或更多个源文件中。 每个源文件包含类型或方法定义的一部分,编译应用程序时将把所有部分组合起来。

分部类

在以下几种情况下需要拆分类定义:

处理大型项目时,使一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理。

使用自动生成的源时,无需重新创建源文件便可将代码添加到类中。 Visual Studio 在创建 Windows 窗体、Web 服务包装器代码等时都使用此方法。 无需修改 Visual Studio 创建的文件,就可创建使用这些类的代码。

若要拆分类定义,请使用 partial 关键字修饰符,如下所示:

C#复制

public partial class Employee
{
    public void DoWork()
    {
    }
}

public partial class Employee
{
    public void GoToLunch()
    {
    }
}

partial 关键字指示可在命名空间中定义该类、结构或接口的其他部分。 所有部分都必须使用 partial 关键字。 在编译时,各个部分都必须可用来形成最终的类型。 各个部分必须具有相同的可访问性,如 publicprivate 等。

如果将任意部分声明为抽象的,则整个类型都被视为抽象的。 如果将任意部分声明为密封的,则整个类型都被视为密封的。 如果任意部分声明基类型,则整个类型都将继承该类。

指定基类的所有部分必须一致,但忽略基类的部分仍继承该基类型。 各个部分可以指定不同的基接口,最终类型将实现所有分部声明所列出的全部接口。 在某一分部定义中声明的任何类、结构或接口成员可供所有其他部分使用。 最终类型是所有部分在编译时的组合。

备注

partial 修饰符不可用于委托或枚举声明中。

下面的示例演示嵌套类型可以是分部的,即使它们所嵌套于的类型本身并不是分部的也如此。

C#

class Container
{
    partial class Nested
    {
        void Test() { }
    }
    partial class Nested
    {
        void Test2() { }
    }
}

编译时会对分部类型定义的属性进行合并。 以下面的声明为例:

C#

[SerializableAttribute]partial class Moon { }

[ObsoleteAttribute]partial class Moon { }

它们等效于以下声明:

C#

[SerializableAttribute]
[ObsoleteAttribute]class Moon { }

将从所有分部类型定义中对以下内容进行合并:

  • XML 注释
  • 接口
  • 泛型类型参数属性
  • class 特性
  • 成员

以下面的声明为例:

C#

partial class Earth : Planet, IRotate { }partial class Earth : IRevolve { }

它们等效于以下声明:

C#

class Earth : Planet, IRotate, IRevolve { }

限制

处理分部类定义时需遵循下面的几个规则:

  • 要作为同一类型的各个部分的所有分部类型定义都必须使用 partial 进行修饰。 例如,下面的类声明会生成错误: C#复制 public partial class A { }//public class A { } // Error, must also be marked partial
  • partial 修饰符只能出现在紧靠关键字 classstructinterface 前面的位置。
  • 分部类型定义中允许使用嵌套的分部类型,如下面的示例中所示: C# partial class ClassWithNestedClass{ partial class NestedClass { } }partial class ClassWithNestedClass{ partial class NestedClass { } }
  • 要成为同一类型的各个部分的所有分部类型定义都必须在同一程序集和同一模块(.exe 或 .dll 文件)中进行定义。 分部定义不能跨越多个模块。
  • 类名和泛型类型参数在所有的分部类型定义中都必须匹配。 泛型类型可以是分部的。 每个分部声明都必须以相同的顺序使用相同的参数名。
  • 下面用于分部类型定义中的关键字是可选的,但是如果某关键字出现在一个分部类型定义中,则该关键字不能与在同一类型的其他分部定义中指定的关键字冲突:
    • public
    • 专用
    • protected
    • internal
    • abstract
    • sealed
    • 基类
    • new 修饰符(嵌套部分)
    • 泛型约束

匿名方法

在 2.0 之前的 C# 版本中,声明委托的唯一方式是使用命名方法。 C# 2.0 引入匿名方法,在 C# 3.0 及更高版本中,Lambda 表达式取代匿名方法作为编写内联代码的首选方式。 但是,本主题中有关匿名方法的信息也适用于 Lambda 表达式。 在有一种情况下,匿名方法提供 Lambda 表达式中没有的功能。 使用匿名方法可省略参数列表。 这意味着匿名方法可转换为具有多种签名的委托。 Lambda 表达式无法实现这一点。 有关 Lambda 表达式的详细信息,请参阅 Lambda 表达式。

创建匿名方法实际上是一种将代码块作为委托参数传递的方式。

匿名方法的参数范围为匿名方法块。

如果目标在匿名方法块之外,匿名方法块内具有 goto、break 或 continue 等跳转语句是一种错误。 如果目标在匿名方法块之内,匿名方法块外具有 gotobreakcontinue 等跳转语句也是一种错误。

可以为 null 的类型

可以为 null 的类型是 System.Nullable<T> 结构的实例。 可以为 null 的类型可以表示基础值类型正常范围内的值,再加上一个 null值。 例如,Nullable<Int32> 读作“可以为 null 的 Int32”,可以将 -2147483648 到 2147483647 之间的任意值赋值给它,也可以将 null 赋值给它。 可以将 true、false 或 null 赋值给 Nullable<bool>。 处理数据库和其他包含不可赋值的元素的数据类型时,能够将 null 赋值给数值类型和布尔类型会特别有用。 例如,数据库中的布尔字段可以存储值 truefalse,也可以处于未定义状态。

迭代器

迭代器可用于逐步迭代集合,例如列表和数组。

迭代器方法或 get 访问器可对集合执行自定义迭代。 迭代器方法使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。

通过 foreach 语句或 LINQ 查询从客户端代码中使用迭代器。

在以下示例中,foreach 循环的首次迭代导致 SomeNumbers 迭代器方法继续执行,直至到达第一个 yield return 语句。 此迭代返回的值为 3,并保留当前在迭代器方法中的位置。 在循环的下次迭代中,迭代器方法的执行将从其暂停的位置继续,直至到达 yield return 语句后才会停止。 此迭代返回的值为 5,并再次保留当前在迭代器方法中的位置。 到达迭代器方法的结尾时,循环便已完成。

协变和逆变

在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。 协变保留分配兼容性,逆变则与之相反。

以下代码演示分配兼容性、协变和逆变之间的差异。

// Assignment compatibility.   
string str = "test";  
// An object of a more derived type is assigned to an object of a less derived type.   
object obj = str;  

// Covariance.   
IEnumerable<string> strings = new List<string>();  
// An object that is instantiated with a more derived type argument   
// is assigned to an object instantiated with a less derived type argument.   
// Assignment compatibility is preserved.   
IEnumerable<object> objects = strings;  

// Contravariance.             
// Assume that the following method is in the class:   
// static void SetObject(object o) { }   
Action<object> actObject = SetObject;  
// An object that is instantiated with a less derived type argument   
// is assigned to an object instantiated with a more derived type argument.   
// Assignment compatibility is reversed.   
Action<string> actString = actObject;

数组的协变使派生程度更大的类型的数组能够隐式转换为派生程度更小的类型的数组。 但是此操作不是类型安全的操作,如以下代码示例所示。

object[] array = new String[10];  
// The following statement produces a run-time exception.  
// array[0] = 10;

C# 一开始是非常通用的面向对象的 (OO) 语言,而 C# 2.0 版很快改变了这一点。 做好基础准备后,他们开始追求解决一些严重影响开发者的难点。 结果他们彻底地解决了这些问题。

通过泛型,你将获得可以对任意类型操作的类型和方法,同时保持类型安全性。 例如,通过 List<T>,你将获得 List<string>List<int> 并且可以对这些字符串或整数执行类型安全操作,同时对其进行循环访问。 比起为每个操作从 Object 创建 ListInt 继承者或强制转换要好很多。

C# 2.0 版引入了迭代器。 简单地说就是可以使用 foreach 循环对 List(或其他可枚举类型)中的项进行循环访问。 这是该语言最重要的一部分,显著提升了语言的可读性以及人们推出代码的能力。

不过 C# 依然在追赶 Java 的道路上。 当时 Java 已发布包含泛型和迭代器的版本。 但是随着语言各自的演化,形势很快发生了变化。

C# 3.0 版

C# 3.0 版和 Visual Studio 2008 一起发布于 2007 年下半年,但完整的语言功能是在 C# 3.5 版中发布的。 此版本标示着 C# 发展过程中的重大更改。 C# 成为了真正强大的编程语言。 我们来看看此版本中的一些主要功能:

自动实现的属性

在 C# 3.0 及更高版本,当属性访问器中不需要任何其他逻辑时,自动实现的属性会使属性声明更加简洁。 它们还允许客户端代码创建对象。 当你声明以下示例中所示的属性时,编译器将创建仅可以通过该属性的 getset 访问器访问的专用、匿名支持字段。

匿名类型

匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。

可通过使用 new 运算符和对象初始值创建匿名类型。 有关对象初始值设定项的详细信息,请参阅对象和集合初始值设定项。

以下示例显示了用两个名为 AmountMessage 的属性进行初始化的匿名类型。

var v = new { Amount = 108, Message = "Hello" };  

// Rest the mouse pointer over v.Amount and v.Message in the following  
// statement to verify that their inferred types are int and string.  
Console.WriteLine(v.Amount + v.Message);

查询表达式

查询是什么及其作用是什么?

查询是一组指令,描述要从给定数据源(或源)检索的数据以及返回的数据应具有的形状和组织。 查询与它生成的结果不同。

通常情况下,源数据按逻辑方式组织为相同类型的元素的序列。 例如,SQL 数据库表包含行的序列。 在 XML 文件中,存在 XML 元素的“序列”(尽管这些元素在树结构按层次结构进行组织)。 内存中集合包含对象的序列。

从应用程序的角度来看,原始源数据的特定类型和结构并不重要。 应用程序始终将源数据视为 IEnumerable<T> 或 IQueryable<T> 集合。 例如在 LINQ to XML 中,源数据显示为 IEnumerable<XElement>。

对于此源序列,查询可能会执行三种操作之一:

检索元素的子集以生成新序列,而不修改各个元素。 查询然后可能以各种方式对返回的序列进行排序或分组,如下面的示例所示(假定 scoresint[]):

IEnumerable<int> highScoresQuery =
    from score in scores
    where score > 80
    orderby score descending
    select score;

Lambda 表达式

Lambda 表达式是一种可用于创建 委托 或 表达式目录树 类型的 匿名函数 。 通过使用 lambda 表达式,可以写入可作为参数传递或作为函数调用值返回的本地函数。 Lambda 表达式对于编写 LINQ 查询表达式特别有用。

若要创建 Lambda 表达式,需要在 Lambda 运算符 =>左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。 例如,lambda 表达式 x => x * x 指定名为 x 的参数并返回 x 的平方值。 如下面的示例所示,你可以将此表达式分配给委托类型:

delegate int del(int i);  
static void Main(string[] args)  
{  
    del myDelegate = x => x * x;  
    int j = myDelegate(5); //j = 25  
}

=> 运算符具有与赋值运算符 (=) 相同的优先级并且是右结合运算(参见“运算符”文章的“结合性”部分)。

Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法(如 Where)的参数。

使用基于方法的语法在 Where 类中调用 Enumerable 方法时(如在 LINQ to Objects 和 LINQ to XML中一样),参数是委托类型 System.Func<T,TResult>。 使用 Lambda 表达式创建该委托最为方便。 例如,在 System.Linq.Queryable 类中调用相同的方法时(如在 LINQ to SQL 中一样),参数类型为 System.Linq.Expressions.Expression<Func>,其中 Func 是最多具有十六个输入参数的任何一个 Func 委托。 同样,Lambda 表达式只是一种非常简洁的构造该表达式目录树的方式。 尽管事实上通过 Lambda 创建的对象具有不同的类型,但 Lambda 使得 Where 调用看起来类似。

在上一个示例中,请注意委托签名具有一个 int类型的隐式类型输入参数,并返回 int。 可以将 Lambda 表达式转换为该类型的委托,因为该表达式也具有一个输入参数 (x),以及一个编译器可隐式转换为 int 类型的返回值。 (以下几节中将对类型推理进行详细讨论。)使用输入参数 5 调用委托时,它将返回结果 25。

在 is 或 as 运算符的左侧不允许使用 Lambda。

适用于匿名方法的所有限制也适用于 Lambda 表达式。 有关详细信息,请参阅匿名方法。

表达式 lambda

表达式位于 => 运算符右侧的 Lambda 表达式称为“表达式 lambda”。 表达式 lambda 广泛用于表达式树的构造。 表达式 lambda 会返回表达式的结果,并采用以下基本形式:

C#复制

(input-parameters) => expression

仅当 lambda 只有一个输入参数时,括号才是可选的;否则括号是必需的。 括号内的两个或更多输入参数使用逗号加以分隔:

C#复制

(x, y) => x == y

有时,编译器难以或无法推断输入类型。 如果出现这种情况,你可以按以下示例中所示方式显式指定类型:

C#复制

(int x, string s) => s.Length > x

使用空括号指定零个输入参数:

C#复制

() => SomeMethod()

在上一个示例中,请注意表达式 Lambda 的主体可以包含一个方法调用。 但是,如果要创建在 .NET Framework 之外计算的表达式目录树(例如,在 SQL Server 中),则不应在 lambda 表达式中使用方法调用。 在 .NET 公共语言运行时上下文之外,方法将没有任何意义。

语句 lambda

语句 lambda 与表达式 lambda 表达式类似,只是语句括在大括号中:

(input-parameters) => { statement; }

语句 lambda 的主体可以包含任意数量的语句;但是,实际上通常不会多于两个或三个。

表达式树

Expression Tress 表示树状数据结构的代码

应该是不可变的,要修改某个表达式树,则必须通过复制现有的表达式树并替换其中的节点来构造一个新的表达式树

二叉树

Binary expression tree

树状结构的每个节点都是一个表达式

the expression tree is an inmemory data representation of the lambda expression ---msdn

优点:

1.可动态修改可执行代码

2.动态执行各个数据库中的LINQ查询

3.创建动态查询

4.表达式树还可在动态语言运行时DLR中用来提供动态语言和.NET Framework之间的互操作性

用法

1.匿名lambda表达式让编辑器创建表达式树

2.使用System.linq.expressions命名空间手动创建表达式树

3.Expression<TDelegate> : LambdaExpression

Expression类型创建表达式树

1.在system.linq.expressions命名空间中定义

2.若要使用Expresstion类创建表达式树,类静态工厂方法 ParameterExpression(表示一个变量或参数),MethodCallExpression(表示一个方法调用),ParameterExpression、MethodCallExpression这些类型派生自抽象类型Expression

扩展方法

在使用面向对象的语言进行项目开发的过程中,较多的会使用到“继承”的特性,但是并非所有的场景都适合使用“继承”特性,在设计模式的一些基本原则中也有较多的提到。

继承的有关特性的使用所带来的问题:对象的继承关系实在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写它或被其他更适合的类替换,这种依赖关系限制了灵活性并最终限制了复用性。替代继承特性的方式,较多的会采用 合成/聚合复用原则,“合成/聚合复用原则”:尽量使用合成/聚合,尽量不要使用类继承。

如果在新类型的对象应当携带有关额外行为的细节,在使用继承特性时,有时可能不太适合,例如:处理指类型,密封类,或者接口时。在面对这些要求时,我们有时候会写一些静态类包含一些静态方法。但是过多的静态方法会造成额外的不必要的开销。

面对以上的有关“继承”的问题,以及在面对项目的一些需求时,我们需要解决这些问题的方式就是“扩展方法”。在C#3.0中引入了“扩展方法”,既有静态方法的优点,又使调用它们的代码的可读性得到了提高。在使用扩展方法时,可以像调用实例方法那样调用静态方法。扩展方法”是C#独有的一种方法,在扩展方法中会使用ExtensionAttribute这个attribute。

C#一旦使用this关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制的attribute,这个attribute会在最终生成的文件的元数据中持久性的存储下来,此属性在System.Core dll程序集中。

任何静态类只要包含了至少一个扩展方法,它的元数据中也会应用这个attribute,任何一个程序集包含了至少一个符合上述特点的静态类,它的元数据也会应用这个attribute。如果代码用了一个不存在的实例方法,编译器会快速的扫描引用的所有程序集,判断它们哪些包含了扩展方法,然后,在这个程序集中,可以扫描包含了扩展方法的静态类。

如果同一个命名空间中的两个类含有扩展类型相同的方法,就没有办法做到只用其中一个类中的扩展方法。为了通过类型的简单名称(没有命名空间前缀)来使用类型,可以导入该类型所有在的命名空间,但这样做的时候,你没有办法阻止那个命名空间中的扩展方法也被导入进来。

回顾过去,这些功能中大多数似乎都是不可或缺,难以分割的。 它们的组合都是经过巧妙布局。 我们通常认为 C# 版本的杀手锏是查询表达式,也就是语言集成查询 (LINQ)。

LINQ 的构造可以建立在更细微的视图检查表达式树、Lambda 表达式以及匿名类型的基础上。 不过无论如何 C# 3.0 都提出了革命性的概念。 C# 3.0 开始为 C# 转变为面向对象/函数式混合语言打下基础。

具体来说,你现在可以编写 SQL 样式的声明性查询对集合以及其他项目执行操作。 无需再编写 for 循环来计算整数列表的平均值,现在可改用简单的 list.Average() 方法。 组合使用查询表达式和扩展方法让各种数字变得智能多了。

人们需要一些时间来掌握和吸收这种概念,不过已经逐渐做到了。 现在又过了几年,代码变得更简洁,功能也更强大了。

C# 4.0 版

C# 4.0 版很难达到 3.0 版的创新水平。 在 3.0 版中,C# 已经完全从 Java 的阴影中脱颖而出,崭露头角。 很快成为一种简洁精炼的语言。

下一版本引入了一些有趣的新功能:

动态绑定

在通过 dynamic 类型实现的操作中,该类型的作用是绕过编译时类型检查。 改为在运行时解析这些操作。 dynamic 类型简化了对 COM API(例如 Office Automation API)、动态 API(例如 IronPython 库)和 HTML 文档对象模型 (DOM) 的访问。

在大多数情况下,dynamic 类型与 object 类型的行为类似。 但是,如果操作包含 dynamic 类型的表达式,那么不会通过编译器对该操作进行解析或类型检查。 编译器将有关该操作信息打包在一起,之后这些信息会用于在运行时评估操作。 在此过程中,dynamic 类型的变量会编译为 object 类型的变量。 因此,dynamic 类型只在编译时存在,在运行时则不存在。

下面的示例将 dynamic 类型的变量与 object 类型的变量进行对比。 若要在编译时验证每个变量的类型,请将鼠标指针放在 WriteLine 语句中的 dynobj 上。 IntelliSense 对 dyn 显示“dynamic”,对 obj 显示“object”。

命名参数/可选参数

C# 4 介绍命名实参和可选实参。 通过命名实参,你可以为特定形参指定实参,方法是将实参与该形参的名称关联,而不是与形参在形参列表中的位置关联。 通过可选参数,你可以为某些形参省略实参。 这两种技术都可与方法、索引器、构造函数和委托一起使用。

使用命名参数和可选参数时,将按实参出现在实参列表(而不是形参列表)中的顺序计算这些实参。

命名形参和可选形参一起使用时,你可以只为可选形参列表中的少数形参提供实参。 此功能极大地方便了对 COM 接口(例如 Microsoft Office 自动化 API)的调用。

命名实参

有了命名实参,你将不再需要记住或查找形参在所调用方法的形参列表中的顺序。 每个实参的形参都可按形参名称进行指定。 例如,通过以函数定义的顺序按位置发送实参,可以采用标准方式调用打印订单详细信息(例如卖家姓名、订单号和产品名称)的函数。

PrintOrderDetails("Gift Shop", 31, "Red Mug");

如果不记得形参的顺序,但却知道其名称,则可以按任意顺序发送实参。

PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");

PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

命名实参还可以标识每个实参所表示的含义,从而改进代码的可读性。 在下面的示例方法中,sellerName 不得为 NULL 或空白符。 由于 sellerNameproductName 都是字符串类型,所以使用命名实参而不是按位置发送实参是有意义的,可以区分这两种类型并减少代码阅读者的困惑。

当命名实参与位置实参一起使用时,只要

  • 没有后接任何位置实参或

PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");

  • 以 C# 7.2 开头,则它们就有效并用在正确位置。 在以下示例中,形参 orderNum 位于正确的位置,但未显式命名。

PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");

但是,如果其后接位置实参,则无序命名实参无效。

泛型协变和逆变

协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。 在引用类型系统时,协变、逆变和不变性具有如下定义。 这些示例假定一个名为 Base 的基类和一个名为 Derived的派生类。

  • Covariance 使你能够使用比原始指定的类型派生程度更大的类型。 你可以向 IEnumerable<Derived> 类型的变量分配IEnumerable(Of Derived) (在 Visual Basic 中为 IEnumerable<Base>)的实例。
  • Contravariance 使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。 你可以向 IEnumerable<Base> 类型的变量分配IEnumerable(Of Base) (在 Visual Basic 中为 IEnumerable<Derived>)的实例。
  • Invariance 这意味着,你只能使用原始指定的类型;固定泛型类型参数既不是协变类型,也不是逆变类型。 你无法向 IEnumerable<Base> 类型的变量分配 IEnumerable(Of Base)(在 Visual Basic 中为 IEnumerable<Derived>)的实例,反之亦然。

利用协变类型参数,你可以执行非常类似于普通的多形性的分配,如以下代码中所示。

嵌入的互操作类型

嵌入的互操作类型缓和了部署难点。 泛型协变和逆变提供了更强的功能来使用泛型,但风格比较偏学术,应该最受框架和库创建者的喜爱。 命名参数和可选参数帮助消除了很多方法重载,让使用更方便。 但是这些功能都没有完全改变模式。

主要功能是引入 dynamic 关键字。 在 C# 4.0 版中引入 dynamic 关键字让用户可以替代编译时类型上的编译器。 通过使用 dynamic 关键字,可以创建和动态类型语言(例如 JavaScript)类似的构造。 可以创建 dynamic x = "a string" 再向它添加六个,然后让运行时理清下一步操作。

这就存在出错的可能性,不过同时也为你提供了强大的语言功能。

C# 5.0 版

C# 5.0 版是该语言非常有针对性的一个版本。 在此版本中所做的所有工作几乎都针对另一个突破性的语言概念。 下面是主要功能列表:

异步成员

如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则需要利用异步编程。 还可以使用 CPU 绑定代码(例如执行成本高昂的计算),对编写异步代码而言,这是一个不错的方案。

C# 拥有语言级别的异步编程模型,它使你能轻松编写异步代码,而无需应付回叫或符合支持异步的库。 它遵循基于任务的异步模式 (TAP)。

异步模型的基本概述

异步编程的核心是 TaskTask<T> 对象,这两个对象对异步操作建模。 它们受关键字 asyncawait 的支持。 在大多数情况下模型十分简单:

对于 I/O 绑定代码,当你 await 一个操作,它将返回 async 方法中的一个 TaskTask<T>

对于 CPU 绑定代码,当你 await 一个操作,它将在后台线程通过 Task.Run 方法启动。

await 关键字有这奇妙的作用。 它控制执行 await 的方法的调用方,且它最终允许 UI 具有响应性或服务具有灵活性。

除上方链接的 TAP 文章中介绍的 asyncawait 之外,还有其他处理异步代码的方法,但本文档将在下文中重点介绍语言级别的构造。

调用方信息特性

调用方信息特性让你可以轻松检索上下文的信息,不需要采用大量样本反射代码。 这在诊断和日志记录任务中也很有用。

但是 asyncawait 才是此版本真正的主角。 C# 在 2012 年推出这些功能时,将异步引入语言作为最重要的组成部分,另现状大为改观。 如果你以前处理过冗长的运行操作以及实现回调的 Web,应该会爱上这项语言功能。

C# 6.0 版

C# 在 3.0 版和 5.0 版对面向对象的语言添加了令人影响深刻的功能。 在 6.0 版中,它不再推出主导性的杀手锏,而是发布了很多让语言用户喜爱的功能。 以下介绍了部分功能:

  • 静态导入
  • 异常筛选器
  • 属性初始值设定项
  • Expression bodied 成员
  • Null 传播器
  • 字符串内插
  • nameof 运算符
  • 字典初始值设定项

这些功能每一个都很有趣。 但从整体来看,可以发现一个有趣的模式。 在此版本中,C# 消除语言样本,让代码更简洁且更具可读性。所以对喜欢简洁代码的用户来说,此语言版本非常成功。

除了发布此版本,他们还做了另一件事,虽然这件事本身与传统的语言功能无关。 他们发布了 Roslyn 编译器即服务。 C# 编译器现在是用 C# 编写的,你可以使用编译器作为编程工作的一部分。

C# 7.0 版

C# 7.0 版是最新的主版本。 虽然该版本继承和发展了 C# 6.0,但不包含编译器即服务。 以下介绍了部分新增功能:

  • Out 变量
  • 元组和析构函数
  • 模式匹配
  • 本地函数
  • 已扩展 expression bodied 成员
  • Ref 局部变量和返回结果

这些都为开发者提供了很棒的新功能,帮助编写比以往任何时候都简洁的代码。 重点是缩减了使用 out 关键字的变量声明,并通过元组实现了多个返回值。

但 C# 的用途更加广泛了。 .NET Core 现在面向所有操作系统,着眼于云和可移植性。 语言设计者除了推出新功能外,也会在这方面付出时间和精力。

语言功能与库类型之间的关系

C# 语言定义要求标准库拥有某些类型以及这些类型的特定可访问成员。 编译器针对多种不同语言功能生成使用这些必需类型和成员的代码。 如有必要,在针对尚未部署这些类型或成员的环境编写代码时,可使用包含较新版本的语言所需类型的 NuGet 包。

此标准库功能的依赖项自其第一个版本起就是 C# 语言的一部分。 在该版本中,相关示例包括:

  • Exception - 用于编译器生成的所有异常。
  • String - C# string 类型是 String 的同义词。
  • Int32 - int 的同义词。

第一个版本很简单:编译器和标准库一起提供,且各自都只有一个版本。

后续版本的 C# 偶尔会向依赖项添加新类型或成员。 相关示例包括:INotifyCompletion、CallerFilePathAttribute 和 CallerMemberNameAttribute。 C# 7.0 继续添加 ValueTuple 的依赖项,以实现元组语言功能。

语言设计团队致力于最小化符合标准的标准库所需的类型和成员的外围应用。 该目标针对新库功能无缝集成到语言的简洁设计进行了平衡。 未来版本的 C# 中还会包括需要标准库中的新类型和成员的新功能。 必须了解如何管理工作中的这些依赖项。

管理依赖项

C# 编译器工具现在从支持的平台上 .NET 库的发布周期分离。 实际上,不同的 .NET 库有不同的发布周期:Windows 上的 .NET Framework 作为 Windows 更新发布,.NET Core 在单独的计划中提供,Xamarin 版本的库更新随适用于每个目标平台的 Xamarin 工具提供。

大多数时候,用户都不会注意到这些更改。 但是,如果使用的较新版本语言需要该平台上的 .NET 库中尚未包含的功能,则会引用 NuGet 包以提供这些新类型。 应用支持的平台会随着新框架的安装而更新,因此可以删除额外的引用。

此分离意味着即使面向没有相应框架的计算机,仍可使用新语言功能。

原文发布于微信公众号 - 程序你好(codinghello)

原文发表时间:2018-04-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

Python进阶系列连载(5)——生成器(上)

作者:王大伟 Python爱好者社区唯一小编 博客:https://ask.hellobi.com/blog/wangdawei 生成器 还记得在迭代器里我们...

38810
来自专栏大闲人柴毛毛

Java8新特性——StreamAPI(一)

1. 流的基本概念 1.1 什么是流? 流是Java8引入的全新概念,它用来处理集合中的数据,暂且可以把它理解为一种高级集合。 众所周知,集合操作非常麻烦,若...

3559
来自专栏Coco的专栏

高性能Javascript--高效的数据访问

972
来自专栏CRPER折腾记

Typescript 2+迷你书 :从入门到不放弃

众所周知:JS中有这么几种类型的数据: Symbol,boolean,Number,Object[Array在js中也属于对象],undefind,null,S...

1391
来自专栏影子

jsp的C标签一般使用方法以及js接收servlet中的对象及对象数字

3808
来自专栏liulun

Nim教程【二】

第一篇教程1秒内就被管理员从首页踢掉了 管理员嫌内容太少,没有含金量,这次多写一些。 这应该是国内第一个关于Nim入门的系列教程 好,闲话休提,言归...

23710
来自专栏影子

jsp的C标签一般使用方法以及js接收servlet中的对象及对象数字

1834
来自专栏代码世界

Python之面向对象四

面向对象进阶 一、关于面向对象的两个内置函数 isinstance   判断类与对象的关系    isinstance(obj,cls)检查obj是否是类 cl...

37713
来自专栏Golang语言社区

实效go编程--1

实效Go编程 版本:2013年12月22日 引言 示例 格式化 注释 命名 包名 获取器 接口名 驼峰记法 分号 控制结构 If 重新声明与再次赋值 For S...

3599
来自专栏用户2442861的专栏

《Effective Modern C++》读书笔记

Note:为避免各种侵权问题,本文并没有复制原书任意文字(代码除外,作者已经声明代码可以被使用)。需要原书完整中文翻译的读者请等待官方译本的发布。

3822

扫码关注云+社区