C#3.0
自动实现的属性
如果只是为了封装一个支持字段而创建属性(属性访问器中没有任何逻辑时),C# 3.0
提供了一种更简洁的语法,称为自动实现的属性(Automatically Implemented Property,简称AIP)。一般也称为自动属性
。
class MyClass
{
public int MyProperty { get; set; }
}
例如上面的代码,声明属性而不提供get/set方法的实现。C#执行了一个简单的编译时转换,自动为你声明了一个私有字段,并实现get/set方法。如下图所示
class MyClass
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int <MyProperty>k__BackingField;
public int MyProperty
{
[CompilerGenerated]
get
{
return <MyProperty>k__BackingField;
}
[CompilerGenerated]
set
{
<MyProperty>k__BackingField = value;
}
}
}
注意不能在接口中声明自动实现的属性。(接口中不能声明实例字段)
匿名类型
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。
可通过使用 new 运算符
和 对象初始化器
创建匿名类型。
var obj = new { Time = DateTime.Now, Name = "张三", Message = "Hello" };
Console.WriteLine("{0:yyyy-MM-dd HH:mm:ss} {1}:{2}", obj.Time, obj.Name, obj.Message);
匿名类型包含一个或多个公共只读属性。 包含其他种类的类成员(如方法或事件)为无效。 用来初始化属性的表达式不能为 null
、匿名函数
或指针类型
。
在使用匿名类型初始化变量时,如果需要在以后访问对象的属性,则必须将变量声明为 var
。
匿名类型通常用在LINQ的Select
语句中,以便返回需要的属性集合。
如果你没有在匿名类型中指定成员名称,编译器会为匿名类型成员指定与用于初始化这些成员的属性相同的名称。
var files = new FileStream[] { File.Create("t1"), File.Create("t2") };
var fileInfos = files.Select(f => new { f.Name, f.Length });
foreach (var item in fileInfos)
{
Console.WriteLine("Name={0},Length={1}", item.Name, item.Length);
}
Lambda表达式
Lambda表达式基于数学中的λ演算
得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。
C#的Lambda 表达式都使用 Lambda 运算符 =>
,该运算符读为“goes to
”。语法如下
- 以表达式为主体
(输入参数) => 表达式
- 或者以语句块为主体
(输入参数) => { 语句块 }
Lambda表达式可以看作是C#2
中匿名方法的一种演变。匿名方法能做的基本上都可以用ambda表达式来完成。特别是,捕获的变量在Lambda表达式中的行为和在匿名方法中是一样的。
与匿名方法相似,Lambda表达式有特殊的转换规则:表达式的类型本身并非委托类型,但它可以通过多种方式隐式或显式的转换成一个委托实例。
匿名函数
这个术语同时涵盖了匿名方法和Lambda表达式。- 有返回值的Lambda表达式对应
Func<T>
委托. - 无返回值的Lambda表达式对应
Action<T>
委托. - 匿名函数的返回类型是根据所有
return
语句的类型来推断的。 - Lambda表达式要想被编译器理解,所有参数的类型必须为已知。
查询表达式
查询表达式 是以查询语法表示的查询。 查询表达式是一流的语言构造。 它如同任何其他表达式一样,可以在 C# 表达式
有效的任何上下文中使用。 查询表达式由一组用类似于SQL
或XQuery
的声明性语法所编写的子句组成。 每个子句进而包含一个或多个 C# 表达式
,而这些表达式可能本身是查询表达式或包含查询表达式。
查询表达式必须以 from
子句开头,且必须以 select
或 group
子句结尾。 在第一个 from
子句与最后一个 select
或 group
子句之间,可以包含以下这些可选子句中的一个或多个:where
、orderby
、join
、let
,甚至是其他 from
子句。 还可以使用 into
关键字,使 join
或 group
子句的结果可以充当相同查询表达式中的其他查询子句的源。
查询表达式示例:
int[] scores = new int[] { 97, 92, 81, 60 };
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;
表达式树
表达式树(Expression)提供了一种抽象的方式将代码表示成一个对象树。
表达式树是对象构成的树,树中每个节点本身都是一个表达式。不同的表达式类型代表能在代码中执行的不同操作:二元操作(例如加法),一元操作(例如获取数组的长度),方法调用,构造函数调用等等。
System.Linq.Expression
命名空间包含了代表表达式的各个类,它们都继承自Expression
(一个抽象的主要包含一些静态工厂方法的类,这些方法用于创建其他表达式类的实例)。
表达式树是不可变的数据结构!在创建完表达式树后,它就永远不会改变。
创建表达式树
我们先来创建一个最简单的表达式树,让两个整数常量相加。
Expression firstArg = Expression.Constant(2);
Expression secondArg = Expression.Constant(3);
Expression add = Expression.Add(firstArg, secondArg);
Console.WriteLine(add);
运行代码会输出(2+3)
,这意味着表达式树类重写了ToString方法来产生可读的输出。
表达式树的创建有两种: Lambda表达式法
和 组装法
。
//Lambda表达式法:这种方法创建出的表达式根结点类型为ExpressionType.Lambda
Expression<Func<int, int>> addFuncExpression = (x) => x + 2;
//组装法:这种方法创建出的表达式根结点类型为ExpressionType.Add
ParameterExpression numParam = Expression.Parameter(typeof(int), "n");
ConstantExpression constantParam = Expression.Constant(2, typeof(int));
BinaryExpression addExpression = Expression.Add(numParam, constantParam);
Expression<Func<int, int>> addLambdaExpression =
Expression.Lambda<Func<int, int>>(addExpression, new ParameterExpression[] { numParam });
注意:并非所有Lambda表达式都能转换成表达式树。不能将带有一个语句块(即使只有一个return语句)的Lambda转换成表达式树,只有对单个表达式进行求值的Lambda才可以。表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。
执行表达式树
表达式树是表示一些代码的数据结构。 它不是已编译且可执行的代码。 如果想要执行由表达式树表示的 .NET 代码,则必须将其转换为可执行的 IL 指令。
Expression<TDelegate>
用于表示映射到任何委托类型的表达式。由于此类型映射到一个委托类型,因此.NET可以检查表达式,并为匹配Lambda表达式签名的适当委托生成IL。它的父类就是LambdaExpression
。
LambdaExpression
类型的Compile
方法可以将表达式树转换为可执行代码,创建委托。
将Lambda表达式
编译为委托并调用该委托是可对表达式树执行的最简单的操作之一。 但是,即使是执行这个简单的操作,也存在一些必须注意的事项。必须保证作为委托的一部分的任何变量在调用Compile
的位置处和执行结果委托时可用。
简单来说就是必须保证Lambda表达式捕获的外部变量,在编译时和执行时可用(不会被释放掉或被GC回收)。
Expression<Func<int>> add = () => 1 + 2;// 使用Lambda表达式法创建表达式树
var func = add.Compile(); // 创建委托
var answer = func(); // 调用委托
Console.WriteLine(answer);
表达式的解析
表达式树包含:
- 参数(
Parameters
) - 类型(
NodeType
) - 主体(
Body
) - 返回类型(
ReturnType
)
除此之外,Lambda表达式树还包含Compile
方法以及Name
属性。
所有的表达式都包含:
- 左节点
Left
- 右节点
Right
- 节点类型
NodeType
不同的表达式还会有其他属性,左右节点依旧是表达式。该设计使得访问表达式树中的所有节点可以使用递归操作。
扩展方法
扩展方法使我们能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。扩展方法是一种静态方法,但可以像扩展类型上的实例方法一样进行调用。
扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的。它们的第一个参数指定该方法作用于哪个类型,并且该参数以 this
修饰符为前缀。仅当你使用 using 指令将命名空间显式导入到源代码中之后,扩展方法才位于范围中。
例如:
//给string类型新增扩展方法
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
调用扩展方法
using ExtensionMethods;
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
string s = "Hello Extension Methods";
int i = s.WordCount();
Console.WriteLine(i);
}
}
}
可以使用扩展方法来扩展类或接口,但不能重写扩展方法。与接口或类方法具有相同名称和签名的扩展方法永远不会被调用。
编译时,扩展方法的优先级总是比类型本身中定义的实例方法低。(添加接口或类的实例方法将导致扩展方法无效)
在C#中,你不能在空引用上调用实例方法,但你可以在空引用上调用扩展方法。
隐式类型本地变量
如果要使用隐式类型,唯一要做的就是将普通局部变量声明中的类型名称替换为var
。例如:
var n = 1;
编译器所做的工作很简单,就是获取初始化表达式在编译时的类型,并使变量也具有那种类型。这跟你直接使用强类型声明的变量没有区别。
不是在所有的情况下都能为所有变量使用隐式类型,只有在以下情况下才能用它:
- 被声明的变量是一个局部变量,而不是静态字段和实例字段
- 变量在声明的同时被初始化
- 一旦初始化完成,就不能再给赋值给其他类型的值了
- 初始化表达式不是
Null
、不是方法组、也不是匿名函数 - 语句中只声明了一个变量
分部方法
分部方法在分部类型的一部分中定义了签名,并在该类型的另一部分中定义了实现。 通过分部方法,类设计器可提供与事件处理程序类似的方法挂钩,以便开发者决定是否实现。 如果开发者不提供实现,则编译器在编译时删除签名。 以下条件适用于分部方法:
- 分部类型各部分中的签名必须匹配。
- 方法必须返回 void。
- 不允许使用访问修饰符。 分部方法是隐式专用的。
namespace PM
{
partial class A
{
partial void OnSomethingHappened(string s);
}
// This part can be in a separate file.
partial class A
{
// Comment out this method and the program
// will still compile.
partial void OnSomethingHappened(String s)
{
Console.WriteLine("Something happened: {0}", s);
}
}
}
对象和集合初始器
对象初始化器
使用对象初始值设定项,你可以在创建对象时向对象的任何可访问字段或属性分配值,而无需调用后跟赋值语句行的构造函数。 利用对象初始值设定项语法,你可为构造函数指定参数或忽略参数(以及括号语法)。
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public Person()
{
}
public Person(string name)
{
this.Name = name;
}
}
class Program
{
static void Main(string[] args)
{
Person person = new Person { Age = 10, Name = "张三" };
Person namePerson = new Person("张三") { Age = 10 };
}
}
集合初始化器
在初始化实现 IEnumerable
的集合类型和初始化使用Add
方法时,集合初始化器允许指定一个或多个元素进行初始化。
List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> list2 = new List<int> { 0 + 1, 12 % 3, int.MaxValue };
LINQ
Language Integrated Query
语言集成查询 (LINQ) 是一系列直接将查询功能集成到C#语言的技术统称。
LINQ
系列技术提供了针对对象 (LINQ to Objects
)、关系数据库 (LINQ to SQL
) 和 XML (LINQ to XML
) 的一致查询体验。
对于编写查询的开发者来说,LINQ 最明显的“语言集成”部分就是查询表达式。 查询表达式采用声明性查询语法编写而成。 使用查询语法,可以用最少的代码对数据源执行筛选、排序和分组操作。 可使用相同的基本查询表达式模式来查询和转换 SQL 数据库
、ADO .NET 数据集
、XML 文档
和流以及 .NET 集合
中的数据。
LINQ的中心思想在于:我们可以从一个熟悉的源语言(如C#)生成一个表达式树,将结果作为一个中间格式,再将其转换成目标平台上的本地语言(比如SQL)。
所有 LINQ 查询操作都由以下三个不同的操作组成:
- 获取数据源(支持
IEnumerable<T>
或其派生接口的类型称为可查询类型。可查询类型不需要进行修改或特殊处理就可以用作LINQ数据源
。) - 创建查询(在
LINQ
中,查询变量本身不执行任何操作并且不返回任何数据。它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。可以使用查询表达式语法
和方法语法
来创建查询。) - 执行查询(由于查询变量本身只存储查询命令,所以要执行查询的话就需要对查询变量进行枚举或者调用标准查询运算符。)
class IntroToLINQ
{
static void Main()
{
// LINQ查询的三个步骤:
// 1. 获取数据源.
int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
// 2. 创建查询.
//查询表达式语法:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
//方法语法:
//因为查询变量不存储查询的结果,所以可以随时修改它或将它用作新查询的基础(即使在执行过它之后)。
IEnumerable<int> numQuery2 = numQuery1.Where(num => num % 2 == 0).OrderBy(n => n);
// 3. 执行查询
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
}
}
标准查询运算符
标准查询运算符是组成LINQ
模式的方法。这些方法中的大多数都作用于序列;其中序列指其类型实现 IEnumerable<T>
接口或 IQueryable<T>
接口的对象。 标准查询运算符提供包括筛选、投影、聚合、排序等在内的查询功能。
共有两组 LINQ 标准查询运算符,一组作用于类型 IEnumerable<T>
的对象,另一组作用于类型 IQueryable<T>
的对象。 构成每个集合的方法分别是 Enumerable
和 Queryable
类的静态成员。 这些方法被定义为作为方法运行目标的类型的扩展方法。 可以使用静态方法语法或实例方法语法来调用扩展方法。
各个标准查询运算符在执行时间上有所不同,具体情况取决于它们是返回单一值还是值序列。 返回单一实例值的这些方法(例如 Average
和 Sum
)立即执行。 返回序列的方法会延迟查询执行,并返回一个可枚举的对象。
对于在内存中的集合上运行的方法(即扩展 IEnumerable<T>
的那些方法),返回的可枚举对象将捕获传递到方法的参数。 在枚举该对象时,将使用查询运算符的逻辑,并返回查询结果。
相反,扩展 IQueryable<T>
的方法不会实现任何查询行为。 它们生成一个表示要执行的查询的表达式树。 源 IQueryable<T>
对象执行查询处理。
按执行方式的分类
标准查询运算符方法的 LINQ to Objects
实现主要通过两种方式执行:立即执行(immediate)
和延迟执行(deferred)
。
- 立即执行:指在代码中声明查询的位置读取数据源并执行运算。 返回单个不可枚举的结果的所有标准查询运算符都是立即执行。
- 延迟执行:指不在代码中声明查询的位置执行运算。 仅当对查询变量进行枚举时才执行运算,例如通过使用
foreach
语句执行。
使用延迟执行的查询运算符可以进一步分为两种类别:流式处理(streaming)
和非流式处理(non-streaming)
。
- 流式处理:流式处理运算符不需要在生成元素前读取所有源数据。 在执行时,流式处理运算符一边读取每个源元素,一边对该源元素执行运算,并在可行时生成元素。 流式处理运算符将持续读取源元素直到可以生成结果元素。 这意味着可能要读取多个源元素才能生成一个结果元素。
- 非流式处理:非流式处理运算符必须先读取所有源数据,然后才能生成结果元素。 排序或分组等运算均属于此类别。 在执行时,非流式处理查询运算符将读取所有源数据,将其放入数据结构,执行运算,然后生成结果元素。
实现细节
标准查询运算符拥有共同的含义,但这并不代表每个实现工作起来都是完全一样的。例如,有些LINQ提供器可能在查询获取第一个元素时就加载了所有数据(如访问Web服务)。同样,LINQ to Objects
与 LINQ to SQL
查询的语义也可能不尽相同。当然,这并不是说LINQ是失败的,只是在我们编写查询时,要考虑访问的是什么样的数据源。拥有一组查询运算符以及一致的查询语法,恰恰是LINQ一个巨大的优势,尽管它不上万能的。
注意:LINQ不是一种关于XML、内存数据查询、SQL数据查询、可观察对象,甚至是枚举器的技术!它是关于表达式一致性的技术,并让C#编译器有机会至少在某种程度上去验证你的查询,而不用关心最终执行的平台。
最后我们回顾下C#3.0的特性:
- 匿名类型提供一种在查询结果中对一组属性临时分组的简便方法,无需定义单独的命名类型。
- Lambda表达式是一种内联函数,该函数使用
=>
运算符将输入参数与函数体分离,并且可以在编译时转换为委托或表达式树。 在LINQ
编程中,在对标准查询运算符进行直接方法调用时,会遇到Lambda 表达式。 - 查询表达式使用类似于
SQL
或XQuery
的声明性语法来查询IEnumerable
集合。 在编译时,查询语法转换为对标准查询运算符方法的调用。 - 扩展方法是一种可与类型关联的静态方法,因此可以像实例方法那样对类型调用它。 实际上,利用此功能,可以将新方法“添加”到现有类型,而不会实际修改它们。 标准查询运算符是一组扩展方法,它们为实现
IEnumerable<T>
的任何类型提供 LINQ 查询功能。 - 隐式类型本地变量可以使用
var
修饰符来指示编译器推断并分配类型,而不必在声明并初始化变量时显式指定类型。
var query = from str in stringArray
group str by str[0] into stringGroup
orderby stringGroup.Key
select new { stringGroup.Key };
有没有发现,这些新功能都在一定程度上用于LINQ查询,但它们并不只限于LINQ,我们在任何情况下都可以使用这些功能。