第3章 C#3:LINQ 及相关特性

第3章 C#3:LINQ 及相关特性

3.1 自动实现的属性

C#3 增加了自动属性,由编译器负责实现原先的访问器部分(编译器会自动创建后台字段)。试比较如下两段代码:

// C#3 之前
private string name;
public string Name
{get { return name; }set { name = value; }
}
// C#3
public string Name { get; set; }

C#6 对自动属性进一步进行了优化(见8.2 自动实现属性的升级):

  • 自动属性可以进行赋值
  • 支持只读自动属性

3.2 隐式类型

3.2.1 类型术语

下面对静态类型、动态类型、显式类型、隐式类型进行讲解

3.2.1.1 静态类型和动态类型

  • 静态类型语言:典型的面向 编译 的语言:所有表达式的类型都由 编译器 来决定,并由 编译器 来检查类型的使用是否合法。

    这个“决定”过程称之为“绑定”

  • 动态类型语言:把绝大部分甚至所有绑定操作放到了 执行 期。

C# 总体上属于 态类型语言(不过 C#4 引入了动态绑定,第4章 C#4:互操作性提升将详述)。尽管具体调用虚函数的哪个实现取决于执行期调用对象的类型,但是执行方法签名绑定的整个过程都发生在编译时。

3.2.1.2 显式类型和隐式类型

  • 显式类型语言:源码会显式地给出所有相关的类型信息,包括局部变量、字段、方法参数或者返回类型。
  • 隐式类型语言:允许开发人员不给出具体的类型信息,而是通过其他机制(编译器或者执行期的其他方式)根据 上下文 推断出类型。

C# 总体上属于 显式 类型,不过在 C#3 之前,就已经出现了隐式类型的身影,例如2.1.4 方法类型实参的类型推断提到的对泛型类型实参的类型推断机制。另外,隐式类型转换的出现(比如从 int​ 到 long​ 的转换),也削弱了 C# 的显式类型特征。

3.2.2 隐式类型的局部变量

隐式类型的局部变量:指使用上下文关键字 var ​ 声明的变量。如下两段代码等价:

var language = "C#";
string language = "C#";

隐式类型局部变量的使用有两(三)条重要的规则:

  • 变量在 声明 时就必须被初始化;
  • 用于初始化变量的表达式必须已经具备 某个类型
  • 只能用于局部变量。

以如下代码为例,它们违背了前两条规则,无法通过编译:

var x;
x = 10;var y = null;

隐式类型常用于如下 3 个场景:

  • 变量为 匿名 类型,不能为其指定类型。(见3.4 匿名类型)
  • 变量类型名 过长,并且根据其初始化表达式可以轻松推断出类型。

试比较:

Dictionary<string, List<decimal>> mapping =new Dictionary<string, List<decimal>>();
var mapping = new Dictionary<string, List<decimal>>();
  • 变量的精确类型并不重要,并且初始化表达式可以提供足够的信息来推断。

3.2.3 隐式类型的数组

随着 C# 发展,我们对数组赋初值的方式也变得多样化。

C#3 之前

C#3 之前有两种方式初始化数组:

int[] array1 = { 1, 2, 3, 4, 5};
int[] array2 = new int[] { 1, 2, 3, 4, 5};

第 1 种方式要求必须在变量声明中指定数组类型。以下形式 法:

int[] array;
array = { 1, 2, 3, 4, 5 };

第 2 种初始化方式 则不 受此规则限制:

array = new int[] { 1, 2, 3, 4, 5 };
C#3

C#3 引入了第 3 种方式:数组为隐式类型,其类型由元素的类型决定,只要编译器可以根据元素的类型推断数组的类型。

如下代码是 法的:

int[] array;
array = new[] { 1, 2, 3, 4, 5 };

多维数组也可以这样使用:

var array = new[,] { { 1, 2, 3 }, { 4, 5, 6 } };

3.2.3.1 推断流程

编译器的推断流程大致如下:

  1. 统计每一个元素的 类型 ,将这些 类型 整合成一个类型候选集。
  2. 对于类型候选集中的每一个类型,检查是否所有元素都可以 隐式地转换为该类型 。剔除不满足该检查条件的类型,最终得到一个筛选过的类型集。
  3. 如果该类型集中 只剩一个类型 ,则该类型就是推断出来的元素类型,编译器根据该类型来创建合适的数组。如果类型集中类型的数量为 0 或大于 1,则编译时会 报错

下表展示了这一规则:

表达式 结果 备注
new[] { 10, 20 } int[] 所有元素均为 int 类型
new[] { null, null } Error 所有元素都不具有类型
new[] { "xyz", null } string[] string​ 是唯一候选类型,并且 null 可以转换为 string​ 类型
new[] { "abc", new object() } object[] 候选类型有两个:string​ 和 object​, string ​ 可以隐式转换为 object​ 类型,反之则不成立
new[] { 10, new DateTime() } Error 候选类型有两个: int ​ 和 DateTime​,但是两个类型不能相互转换
new[] { 10, null } Error int​ 是唯一候选类型,但 null 不能转换为 int 类型

3.3 对象和集合的初始化

3.3.1 对象初始化器和集合初始化器简介

假设我们有如下若干类型:

public class Order
{private readonly List<OrderItem> items = new List<OrderItem>();public string OrderId { get; set; }public Customer Customer { get; set; }public List<OrderItem> Items { get { return items; } }
}public class Customer
{public string Name { get; set; }public string Address { get; set; }
}public class OrderItem
{public string ItemId { get; set; }public int Quantity { get; set; }
}

是比较如下两段代码,可以看到,使用对象初始化器和集合初始化器,整体更加简洁:

var customer = new Customer();  //
customer.Name = "Jon";          // 创建 Customer
customer.Address = "UK";        //var item1 = new OrderItem();    //
item1.ItemId = "abcd123";       // 创建第1个 OrderItem
item1.Quantity = 1;             //var item2 = new OrderItem();    //
item2.ItemId = "fghi456";       // 创建第2个 OrderItem
item2.Quantity = 2;             //var order = new Order();        //
order.OrderId = "xyz";          //
order.Customer = customer;      // 创建 Order
order.Items.Add(item1);         //
order.Items.Add(item2);         //
var order = new Order
{OrderId = "xyz",Customer = new Customer { Name = "Jon", Address = "UK" },Items ={new OrderItem { ItemId = "abcd123", Quantity = 1 },new OrderItem { ItemId = "fghi456", Quantity = 2 }}
};

3.3.2 对象初始化器

对象初始化器只能用于构造器调用或其他对象初始化器中。如果构造器不需要指定参数,其 参数列表 () ​ 可以省略。以下两种写法等价:

Order order = new Order() { OrderId = "xyz" };
Order order = new Order { OrderId = "xyz" };

使用对象初始化器为对象中的引用成员赋值,有两种方式。试比较如下两段代码:

class Student
{public string Name { get; set; }public School School { get; set; }
}class School
{public string Name { get; set; }
}
class Student
{public string Name { get; set; }public School School { get; set; } = new School();
}class School
{public string Name { get; set; }
}

以下使用方式,上述类型定义方式,第 段代码会抛出 NullReferenceException ​ 异常

Student student = new Student
{Name = "John Smith",School ={Name = "Kailua Intermediate School"}
};

如下使用方式则不会抛出异常:

Student student = new Student
{Name = "John Smith",School = new School{Name = "Kailua Intermediate School"}
};

这是因为:不 new 一个实例,直接使用大括号时,对象初始化器会调用 get 访问器 ,将嵌套对象初始化器得到的结果应用于由 get 访问器返回的实例上。以下两段代码等价:

Student student = new Student
{Name = "John Smith",School ={Name = "Kailua Intermediate School"}
};
Student student = new Student();
student.Name = "John Smith";
var school = student.School;
school.Name = "Kailua Intermediate School";

3.3.3 集合初始化器

集合初始化器只能用于构造器调用或者对象初始化器中。集合初始化器多用于创建新集合,编译器会将其转化为构造器和 Add() ​ 方法的调用。如下两段代码等价:

var beatles = new List<string> { "John", "Paul", "Ringo", "George" };
var beatles = new List<string>();
beatles.Add("John");
beatles.Add("Paul");
beatles.Add("Ringo");
beatles.Add("George");

上述 List<T>​ 支持单参数的 Add()​ 方法,对于需要多参数的 Add()​ 方法(如 Dictionary<TKey, TValue>​),每个初始化元素需要 大括号 包围。如下两段代码等价:

var releaseYears = new Dictionary<string, int>
{{ "Please please me", 1963 },{ "Revolver", 1966 },{ "Sgt. Pepper's Lonely Hearts Club Band", 1967 },{ "Abbey Road", 1970 }
};
var releaseYears = new Dictionary<string, int>();
releaseYears.Add("Please please me", 1963);
releaseYears.Add("Revolver", 1966);
releaseYears.Add("Sgt. Pepper's Lonely Hearts Club Band", 1967);
releaseYears.Add("Abbey Road", 1970);

3.3.3.1 工作流程

对于集合初始化器,编译器将每个元素初始化器看作一个 Add()​ 调用,它的工作逻辑如下:

  1. 如果元素初始化器没有 大括号 ,将其作为单个元素传递给 Add()​ 方法。

  2. 如果元素初始化器带有 大括号 ,将大括号中的每个表达式当作一个参数,并执行 重载 决议:查找最合适的 Add()​ 方法。

    如果是泛型 Add()​ 方法,还需要执行类型推断

自定义集合类型想使用集合初始化器,需要满足以下两个条件:

  • 实现了 IEnumerable ​ 接口;
  • 具有 Add() ​ 方法。

C# 语言设计团队限制必须实现 IEnumerable ​ 接口,是为了区分集合类型和非集合类型,实际上 IEnumerable ​ 接口无需实现。为此,自定义集合类的 IEnumerable.GetEnumerator()​ 方法可以直接抛出 NotImplementedException​ 异常(仅推荐测试项目中如此使用,产品代码不建议)。

3.3.4 仅用单一表达式就能完成初始化的好处

  1. 适用于 LINQ

    用于 LINQ 的语句都要求具备的单一表达式的表达能力

  2. 可以简化字段初始化器、方法实参的传入、条件表达式的使用(三目运算符 ?:.​)、静态字段初始化器构建查找表

Eureka

我认为最重要的一点,是初始化时发生了异常,不会创造出一个残破的实例。见:3.1.4 对象初始化器

此处使用了临时变量,是为了确保在初始化过程中如果 抛出异常 ,不会得到一个 部分初始化的 对象。

3.4 匿名类型

3.4.1 基本语法和行为

我们以如下代码为例,讲解匿名类型的几个要素:

var player = new        // 
{                       // 创建一个匿名类型对象,Name = "Rajesh",    // 包含 Name 和 SourceScore = 3500        // 两个属性
};                      //Console.WriteLine("Player name: {0}", player.Name);     // 打印
Console.WriteLine("Player score: {0}", player.Score);   // 属性
  • 匿名类型的语法类似于对象初始化器,但无须指定 类型 名称,只需要 new 关键字、左大括号、属性以及右大括号。

    这一形式称为匿名对象创建表达式。其中属性部分可以继续嵌套匿名对象创建表达式。

  • 声明变量需使用 var 关键字。

    也可以使用 object ​ 来声明,不过意义不大(如果使用 object​ 声明,变量的属性将难以消费)。

  • 上述代码依然属于静态类型范畴。

    Visual Studio 会为 player​ 变量自动设置 Name​ 和 Score​ 属性。如果要访问一个不存在的属性(比如 player.Points​),编译器会 报错 。属性的类型是根据 赋值的类型 进行推断的:player.Name​ 是 string ​ 类型,player.Score​ 是 int ​ 类型。

3.4.1.1 投射初始化器

通过其他对象的属性 or 字段为匿名类型成员赋值时,匿名类型可以将** 源对象的成员名称 作为属性名。该语法被称为投射初始化器**。

以前文的电子商务类为例:

public class Order
{private readonly List<OrderItem> items = new List<OrderItem>();public string OrderId { get; set; }public Customer Customer { get; set; }public List<OrderItem> Items { get { return items; } }
}public class Customer
{public string Name { get; set; }public string Address { get; set; }
}public class OrderItem
{public string ItemId { get; set; }public int Quantity { get; set; }
}

假设我们现有 Order​ 实例,则如下两段代码等价:

var flattenedItem = new
{order.OrderId,CustomerName = customer.Name,customer.Address,item.ItemId,item.Quantity
};
var flattenedItem = new
{OrderId = order.OrderId,CustomerName = customer.Name,Address = customer.Address,ItemId = item.ItemId,Quantity = item.Quantity
};

在复制多个属性时,使用投射初始化器可以大幅减少代码冗余。

3.4.2 编译器生成类型

编译器会为匿名类型 生成一个类型 。对于 Runtime 来说只是一个普通的类型,只是这个类型的名称不是一个有效的 C# 名称。

采用微软的 C# 编译器时,匿名类型具备以下特点(除了附注“不保证”的项外,其余项都保证):

  • 它是一个类。

  • 基类是 object ​。

  • 该类是密封的。

    不保证,虽然非密封的类并没有什么优势。

  • 属性是 只读 的。

  • 构造器的参数名称与属性名称保持一致。

    不保证,有时对于反射有用。

    Eureka

    是的,匿名类型有构造器!并且因为属性是 只读 的,属性只能通过构造器赋值。

  • 对于程序集的访问等级是 internal 的。

    不保证,在处理动态类型时会比较棘手。

  • 该类会覆盖 GetHashCode() ​ 和 Equals() ​ 方法。

    两个匿名类型只有在所有属性都等价的情况下才等价。(可以正常处理 null 值。)只保证会覆盖这两个方法,但不保证散列值的计算方式。

  • 覆盖并完善 ToString() ​ 方法,用于呈现各属性名称及其对应值。

    不保证,但对于问题诊断来说作用重大。

  • 该类型为 泛型 类,其类型形参会应用于每一个属性。具有相同属性名称但属性类型不同的匿名类型,会使用相同的泛型类型,但拥有不同的类型实参。

    不保证,不同编译器的实现方式不同。

  • 如果两个匿名对象创建表达式使用相同的属性 名称 ,具有相同的属性 类型 以及属性 顺序 ,并且在同一个程序集中,那么这两个对象的类型相同。

基于最后一条特点,对匿名类型实例二次赋值的行为是合法的:

var player = new { Name = "Pam", Score = 4000 };
player = new { Name = "James", Score = 5000 };

进一步推演,如下两段使用匿名类型声明数组的代码,第 2 段是非法的,因为数组中的属性顺序出现了不同:

var players = new[]
{new { Name = "Priti", Score = 6000 },new { Name = "Chris", Score = 7000 },new { Name = "Amanda", Score = 8000 },
};
var players = new[]
{new { Name = "Priti", Score = 6000 },new { Score = 7000, Name = "Chris" },new { Name = "Amanda", Score = 8000 },
};

3.4.3 匿名类型的局限性

匿名类型在需要实现数据的局部化表示时能够发挥作用。

如果需要在多处使用同一个数据形态,匿名类型将无能为力。虽然匿名类型的实例可以用于方法返回值或者方法参数,但是必须使用泛型或者 object ​ 类型。因为匿名类型不具名的特性,所以很难应用于方法签名之中。

Tips

局部化:指某个数据形态的使用范围限制在特定方法中。

3.5 lambda 表达式

3.5.1 lambda 表达式语法简介

lambda 表达式的基本语法形式如下:

\[参数列表=>主体 \]

其中参数列表和主题(body)都可以有多种呈现方式。以如下最完整的 lambda 表达式代码为例(带有大括号,被称为“具有语句主体”):

Action<string> action = (string message) =>
{Console.WriteLine("In delegate: {0}", message);
};

接下来我们一步步进行简化:

  1. 简化 主体部分

    如果主体只包含一条 return 语句或一个 表达式 ,它就可以简化成只有这一条语句(不带大括号,被称为“具有表达式主体”):

    Action<string> action =(string message) => Console.WriteLine("In delegate: {0}", message);
    
  2. 简化 参数列表

    编译器(部分时候)可以根据 lambda 表达式转化后的类型推断参数类型:

    Action<string> action =(message) => Console.WriteLine("In delegate: {0}", message);
    
  3. 简化 圆括号

    如果 lambda 表达式只有一个参数,并且可以推断出参数类型,那么参数列表的 圆括号 也可以省略:

    Action<string> action =message => Console.WriteLine("In delegate: {0}", message);
    

3.5.2 捕获变量

如下代码展示了 lambda 表达式捕获各种变量(除静态字段未包含,包含了实例字段、this 变量、方法参数、局部变量):

class CapturedVariablesDemo
{private string instanceField = "instance field";public Action<string> CreateAction(string methodParameter){string methodLocal = "method local";string uncaptured = "uncaptured local";Action<string> action = lambdaParameter =>{string lambdaLocal = "lambda local";Console.WriteLine("Instance field: {0}", instanceField);Console.WriteLine("Method parameter: {0}", methodParameter);Console.WriteLine("Method local: {0}", methodLocal);Console.WriteLine("Lambda parameter: {0}", lambdaParameter);Console.WriteLine("Lambda local: {0}", lambdaLocal);};methodLocal = "modified method local";return action;}
}// 其他代码
var demo = new CapturedVariablesDemo();
Action<string> action = demo.CreateAction("method argument");
action("lambda argument");

lambda 表达式捕获的是这些变量本身,而非委托创建时这些变量的值(另见8.4.2 捕获变量)!lambda 也能修改这些捕获到的变量,编译器是如何做到的呢?

3.5.2.1 通过生成类来实现捕获变量

一般来说捕获变量有 3 种情形:

  • 没有捕获任何变量:编译器可以创建一个 静态 方法,不需要额外上下文;

  • 仅捕获了实例字段:编译器可以创建一个 实例 方法;

    这种情况下捕获多少个实例字段都可以,只需 this 便可访问

  • 捕获局部变量或参数:编译器会创建一个访问级别为 private嵌套 类保存上下文信息,在当前类创建一个实例方法容纳原 lambda 表达式,原先包含 lambda 表达式的方法会被修改为使用嵌套类来访问捕获变量。

Tips

实际情况可能和上述表述不同,如第一种情形也可能创建实例方法。

第三种情况最复杂。我们以上一节的示例代码为例,编译器为它生成的私有嵌套类如下:

private class LambdaContext         // 生成的私有嵌套类
{public CapturedVariablesDemoImpl originalThis;  //public string methodParameter;                  // 捕获的变量public string methodLocal;                      //public void Method(string lambdaParameter)  // lambda 表达式体变成{                                           // 嵌套类中的一个实例方法string lambdaLocal = "lambda local";Console.WriteLine("Instance field: {0}",originalThis.instanceField);Console.WriteLine("Method parameter: {0}", methodParameter);Console.WriteLine("Method local: {0}", methodLocal);Console.WriteLine("Lambda parameter: {0}", lambdaParameter);Console.WriteLine("Lambda local: {0}", lambdaLocal);}
}public Action<string> CreateAction(string methodParameter)
{LambdaContext context = new LambdaContext();        //context.originalThis = this;                        //context.methodParameter = methodParameter;          // 生成类用于所有context.methodLocal = "method local";               // 捕获的变量string uncaptured = "uncaptured local";             //Action<string> action = context.Method;             //context.methodLocal = "modified method local";      //return action;
}

3.5.2.2 局部变量的多次实例化

上一节的示例为捕获变量仅创建了一个上下文。下面是一个更复杂的例子,通过循环创建多个 Action​,每个 Action​ 都会捕获 text​ 变量:

static List<Action> CreateActions()
{List<Action> actions = new List<Action>();for (int i = 0; i < 5; i++){string text = string.Format("message {0}", i);  // 循环内部声明局部变量actions.Add(() => Console.WriteLine(text)); // 在 lambda 表达式中捕获该变量}return actions;
}// 其他代码
List<Action> actions = CreateActions();
foreach (Action action in actions)
{action();
}

上述代码中,text​ 在循环中声明,每次声明 text​ 都会进行一次实例化,因此每个 lambda 表达式捕获的都是不同的变量实例

编译器的做法是:每次初始化都创建一个不同的 生成类型实例 ,上述代码的 CreateAction()​ 方法会转译成如下形式:

private class LambdaContext
{public string text;public void Method(){Console.WriteLine(text);}
}static List<Action> CreateActions()
{List<Action> actions = new List<Action>();for (int i = 0; i < 5; i++){LambdaContext context = new LambdaContext();    // 为每次循环都创建一个型的“上下文”context.text = string.Format("message {0}", i);actions.Add(context.Method);        // 使用上下文创建一个 action}return actions;
}

3.5.2.3 多个作用域下的变量捕获

下面这个例子涉及多个 作用域 ,因此更为复杂:

static List<Action> CreateCountingActions()
{List<Action> actions = new List<Action>();int outerCounter = 0;       // 下文通过 for 循环创建的两个委托,都会捕获该变量for (int i = 0; i < 2; i++){int innerCounter = 0;   // 每次循环都创建一个新变量Action action = () =>{Console.WriteLine("Outer: {0}; Inner: {1}",outerCounter, innerCounter);outerCounter++;innerCounter++;};actions.Add(action);}return actions;
}// 其他代码
List<Action> actions = CreateCountingActions();
actions[0]();               //
actions[0]();               // 每个委托
actions[1]();               // 调用两次
actions[1]();               //
执行结果:
Outer: 0; Inner: 0
Outer: 1; Inner: 1
Outer: 2; Inner: 0
Outer: 3; Inner: 1

上述代码中的两个委托,每个委托都需要各自的上下文,各自的上下文还需要指向一个公共的上下文。编译器的解决方案是: 创建两个私有嵌套类 。如下是编译器处理后的代码:

private class OuterContext      //
{                               // 外层作用域的public int outerCounter;    // 上下文
}                               //private class InnerContext              //
{                                       // 包含外层上下public OuterContext outerContext;   // 文引用的内层public int innerCounter;            // 作用域上下文public void Method(){Console.WriteLine("Outer: {0}; Inner: {1}",outerContext.outerCounter, innerCounter);outerContext.outerCounter++;innerCounter++;}
}static List<Action> CreateCountingActions()
{List<Action> actions = new List<Action>();OuterContext outerContext = new OuterContext(); // 创建一个外层上下文outerContext.outerCounter = 0;for (int i = 0; i < 2; i++){InnerContext innerContext = new InnerContext(); //innerContext.outerContext = outerContext;       // 每次循环都创建innerContext.innerCounter = 0;                  // 一个内层上下文Action action = innerContext.Method;actions.Add(action);}return actions;
}

Question

如果上述委托在不同线程调用,岂不是会造成“竞态条件”吗?

3.5.3 表达式树

表达式树:将代码按照数据来表示的一种形式。

委托的作用是提供可运行的代码,而表达式树的作用是提供可 查看 的代码。试比较如下两段代码:

Expression<Func<int, int, int>> adder = (x, y) => x + y;
Console.WriteLine(adder);
// 输出 (x, y) => (x + y)
Func<int, int, int> adder = (x, y) => x + y;
Console.WriteLine(adder);
// 输出 System.Func`3[System.Int32,System.Int32,System.Int32]

从第一段代码的输出是表达式树动态构建得到的,它表明“代码是可以进行执行期检查的”。这是表达式树的所有关键所在。

接下来我们拆解 Expression<Func<int, int, int>> adder = (x, y) => x + y;​ 这句代码的各个部分:

  • Func<int, int, int>​:委托类型

  • Expression<TDelegate>​:用于处理 TDelegate​ 类型的 表达式树 类型,TDelegate​ 必须是 委托 类型,由 Runtime 强制保证。

    Expression<TDelegate>​ 仅是表达式树相关的诸多类型之一,它们均位于 System.Linq.Expressions 命名空间,非泛型的 Expression​ 类是所有表达式类型的 抽象基

  • adder​ 变量:接收两个整型值并返回一个整型值方法的表达式树表示。可以用 lambda 表达式为该变量赋值

上述表达式树比较简单,我们也可以手动创建:

ParameterExpression xParameter = Expression.Parameter(typeof(int), "x");    // x 参数
ParameterExpression yParameter = Expression.Parameter(typeof(int), "y");    // y 参数
Expression body = Expression.Add(xParameter, yParameter);                   // 表达式体
ParameterExpression[] parameters = new[] { xParameter, yParameter };
Expression<Func<int, int, int>> adder = Expression.Lambda<Func<int, int, int>>(body, parameters);
Console.WriteLine(adder);

3.5.3.1 转换表达式树的局限性

  • 只有拥有 表达式主体 的 lambda 表达式才能转换成表达式树

    如果主体只包含一条 return 语句或一个 表达式 ,它就可以简化成只有这一条语句(不带大括号,被称为“具有表达式主体”):

(x, y) => x + y​ 符合该规则,但下面这句代码会编译报错:

Expression<Func<int, int, int>> adder = (x, y) => { return x + y; };

3.5.3.2 将表达式树编译成委托

表达式树除了转为 SQL 语句进行查询,还可以在执行期动态构建委托(这种情况一般需要手动编写部分代码)。

Expression<TDelegate>​ 的 Compile() ​ 方法用于执行这一操作,该方法返回一个委托类型,且与普通的委托类型无异。下面是该方法的简单应用:

Expression<Func<int, int, int>> adder = (x, y) => x + y;
Func<int, int, int> executableAdder = adder.Compile();  // 将表达式树编译成委托
Console.WriteLine(executableAdder(2, 3));       // 正常调用委托

该功能可以和反射特性搭配使用,用于访问属性、调用方法来生成并缓存委托,其结果与手动编写委托结果相同。

Question

表达式树的具体用法我仍然不清楚。

3.6 扩展方法

3.6.1 声明扩展方法

扩展方法必须声明在一个非嵌套非泛型静态 类中,而且在 C#7.2 之前第一个参数不能是 ref 参数(见13.5 使用 ref 参数或者 in 参数的扩展方法(C# 7.2))。扩展方法所在的类不能是泛型类,但扩展方法自身 可以 是泛型方法。

下面是一个扩展方法的简单示例:

using System;namespace NodaTime.Extensions
{public static class DateTimeOffsetExtensions{public static Instant ToInstant(this DateTimeOffset dateTimeOffset){return Instant.FromDateTimeOffset(dateTimeOffset);}}
}

编译器会为扩展方法添加 ExtensionAttribute​ 特性(位于 System.Runtime.CompilerServices),用于标记扩展方法可以像 实例 方法那样调用。

3.6.2 调用扩展方法

扩展方法可以像实例方法那样调用还有一个前提:编译器可以 查找到 这个扩展方法。方法的调用遵循如下优先级:

  1. 如果存在 同名普通实例 方法,优先调用 实例 方法;
  2. 未找到可调用的实例方法,查找扩展方法

编译器会从最 层的命名空间一路向 查找至 全局 命名空间。在查找的每条路径上,都要查找当前命名空间下的 静态 类,或者查找 using 指令指定的命名空间中的类。查找的每一步中都有可能找到多个适合调用的扩展方法。此时编译器会对当前所有候选方法执行常规的重载决议。在决策完成后,编译器为调用扩展方法所生成的 IL 代码和调用普通静态方法所生成的 IL 代码是完全相同的。

以如下代码为例,编译器会在如下位置查找扩展方法:

  • CSharpInDepth.Chapter03 命名空间下的静态类;
  • CSharpInDepth 命名空间下的静态类;
  • 全局命名空间下的静态类;
  • using 指令指定的命名空间下的静态类(例如 using System 这样的指向命名空间的命令);
  • (只在 C# 6 中)using static 指定的静态类(见10.1 using static 指令)。
using NodaTime.Extensions;
using System;namespace CSharpInDepth.Chapter03
{class ExtensionMethodInvocation{static void Main(){var currentInstant = DateTimeOffset.UtcNow.ToInstant();Console.WriteLine(currentInstant);}}
}

Info

关于扩展方法同签名的情况,见4.8.2.3 同名的扩展方法

Tips

null 值也可以调用扩展方法,以如下代码为例,如果 Method()​ 的扩展方法,即便 x 为 null,也会将 x 作为首个参数进行方法调用。

x.Method(y);

参考《框架设计指南》,对于扩展方法应有:

  • DO​​:当扩展方法中的 this​ ​参数为 null​ ​时,要抛出 ArgumentNullException ​ ​异常。

3.6.3 扩展方法的链式调用

扩展方法的链式调用极大的改善了代码的可读性。以如下 LINQ 调用为例,试比较两段代码的简洁程度:

string[] words = { "keys", "coat", "laptop", "bottle" };
IEnumerable<string> query = words.Where(word => word.Length > 4).OrderBy(word => word).Select(word => word.ToUpper());
string[] words = { "keys", "coat", "laptop", "bottle" };
IEnumerable<string> query =Enumerable.Select(Enumerable.OrderBy(Enumerable.Where(words, word => word.Length > 4),word => word),word => word.ToUpper());

可以明显感受到第二段代码可读性较差:

  • 方法调用顺序和实际执行顺序刚好 相反
  • lambda 表达式 word => word.ToUpper()​ 究竟属于哪个方法调用很不明确。

当然我们可以定义若干局部变量改善代码的可读性,但仍然不如链式表达式来的简洁:

string[] words = { "keys", "coat", "laptop", "bottle" };
var tmp1 = Enumerable.Where(words, word => word.Length > 4);
var tmp2 = Enumerable.OrderBy(tmp1, word => word);
var query = Enumerable.Select(tmp2, word => word.ToUpper());

3.7 查询表达式

查询表达式转为 LINQ 设计,其语法更加简洁,编译器负责将查询表达式翻译为链式语法并进行编译。试比较如下两段代码:

string[] words = { "keys", "coat", "laptop", "bottle" };
IEnumerable<string> query = words.Where(word => word.Length > 4).OrderBy(word => word).Select(word => word.ToUpper());
string[] words = { "keys", "coat", "laptop", "bottle" };
IEnumerable<string> query = from word in wordswhere word.Length > 4orderby wordselect word.ToUpper();

3.7.1 从 C# 到 C# 的查询表达式转换

我们前面提到的诸多特性编译器会将其直接转为 IL 代码,只有查询表达式最为特殊:编译器会将其转为 C# 的 方法语法(扩展方法的调用) 再进行编译(被称为语法转译),且转译发生在绑定或重载之前。

3.7.2 范围变量和隐形标识符

  • 范围变量:因查询表达式引入,充当查询语句中每条子句中的输入。

引入范围变量的方式有二:

  • 通过 from ​ 关键字
  • 通过 let ​ 关键字
from word in words          // from 子句引入范围变量
where word.Length > 4   //
orderby word            // 后续子句中使用
select word.ToUpper();  // 范围变量
from word in words
let length = word.Length    // 通过 let 关键字引入新的范围变量
where length > 4
orderby length
select string.Format("{0}: {1}", length, word.ToUpper());

对于 let 关键字会有一个新的疑问:上述查询表达式可以同时使用 word​ 和 length​ 变量,那转为方法语法时,这两个变量以什么形式存在?

答案是:查询表达式会为这两个变量创建一个 匿名类型 实例。转化得到的代码如下:

words.Select(word => new { word, length = word.Length }).Where(tmp => tmp.length > 4).OrderBy(tmp => tmp.length).Select(tmp =>string.Format("{0}: {1}", tmp.length, tmp.word.ToUpper()));

上述 tmp​ 变量名是我们演示使用的,在语言规范中使用符号 *​ 表示。这个名称并不重要,在编写查询时它是不可见的,因此被称为隐形标识符

3.7.3 选择使用哪种 LINQ 语法

采用哪种语法,应遵循可读性强、代码简洁、易于编写的原则。查询表达式要求:

查询表达式一般以 from ​子句开始,最后以 select ​或者 group ​子句结束。

对于一些简单的筛选,使用查询表达式就显得笨拙:

from word in words
where word.Length > 4
select word
words.Where(word => word.Length > 4)

此外部分 LINQ 方法只有方法语法支持,如:

  • ​Select​ 支持两种 Func(L2S 和 EF 仅支持一种)Func<TSource, TResult> ​ 和 Func<TSource, TResult, int> ​,其 int​ 参数标注当前 TSource​ 元素是第几个。例如:

  • Where​ 支持两种 ​Func(L2S 和 EF 只支持第一种)​: Func<TSource> ​ 和 Func<TSource, int> ​,其 int​ 参数标注当前 TSource​ 元素是第几个。例如:

当我们需要手动创建匿名类型,或查询较为复杂,查询语法来的更为简便。

建议开发者掌握两种方式,根据需要自行选择。

Tips

非查询标傲世的语法目前没有统一术语,常见的叫法有:

  • 方法语法
  • 点式语法
  • 流式语法
  • lambda 语法

本书统一采用方法语法代称。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907467.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

直播软件怎么开发,Redis触发扩容的两种情况

直播软件怎么开发,Redis触发扩容的两种情况1、如果没有fork子进程在执行RDB或者AOF的持久化,一旦满足ht[0].used >= ht[0].size,此时触发扩容;2、如果有fork子进程在执行RDB或者AOF的持久化时,则需要满足ht[0].used > 5 * ht[0].size,此时触发扩容。下面将结合源码…

怎么快速干净拆焊直插元件的方法成都承接电路板设计

我处提供优质的单片机、PLC、电路板、控制器/箱、仪器仪表、机电设备或系统、自动化、工控、传感、数据采集、自控系统、控制系统、物联网、电子产品、软件、APP开发设计定制服务(业务www点yonko-tech点com),在做项目的时候,拆除电路板上的元件也是有的事情,拆元件说难不难…

flutter:用http库下载文件

一,安装第三方库 地址: https://pub.dev/packages/http 编辑pubspec.yaml: dependencies:flutter:sdk: flutterpath_provider: ^2.1.5http: ^1.3.0 然后点击 pub get 二,代码: import package:flutter/material.dart; import package:http/http.dart show get; import packa…

flutter:从接口获取json数据后并解析

一,代码: dart代码:model class GoodsListItem {String name;String desc;int id;GoodsListItem(this.name,this.desc, this.id) {}GoodsListItem.fromJson(Map<String, dynamic>json):name=json["name"],desc = json["desc"],id = json["id&q…

图解 CSS 选择器

https://zhuanlan.zhihu.com/p/715717977CSS 选择器用于选择 HTML 元素并将样式应用于它们。使用这些选择器,可以定义特定条件下应用哪些样式。除了普通的选择器外,还有伪类和伪元素,用于选择具有特定状态或特定部分的元素,并将样式应用于它们。本文将通过图文并茂的方式展…

二分图学习笔记

使用题单:二分图 - 从入门到入土。 二分图概念 对于一个图,如果能够把它的点集恰好分成两个部分,使得这第一个部分里面的点两两不连边,第二个部分里面的点也两两不连边,则该图是二分图。或者说每一条边都横跨了两个集合。 举个例子:这个图是二分图,因为我们可以将它分成…

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式 + 分流器采样百安级大电流的微电压信号 + 微电压信号放大

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式## 分流器采样百安级大电流的微电压信号 OpAmp(运算放大器)微电压信号放大 如上图所示,\(\large V_{out} = V_{in} \times (1+ \dfrac{R_{2}}{R_{1}})\) TL431+MOS管,充满自停的充电器…

读DAMA数据管理知识体系指南34数据仓库和商务智能概念

读DAMA数据管理知识体系指南34数据仓库和商务智能概念1. 业务驱动因素 1.1. 主要驱动力是运营支持职能、合规需求和商务智能活动 1.2. 用数据来证明他们是合规的,因为数据仓库中包含历史数据,所以经常被用来响应这类要求 1.3. 商务智能支持一直是建设数据仓库的主要原因 2. 目…

环境检测 温湿度 噪声 建大仁科

环境检测 温湿度 噪声 建大仁科 1、温湿度 wifi版本 配置软件2、噪声 wifi 版本 配置软件 android手机上安装 蓝牙连接配置3、平台下载 RS-RJ-K监控平台-平台软件-温湿度传感器产品说明书下载及选型erwa.cn 二娃测试备忘

C# 13 中的新增功能实操

前言 今天大姚带领大家一起来看看 C# 13 中的新增几大功能,并了解其功能特性和实际应用场景。 前提准备 要体验 C# 13 新增的功能可以使用最新的 Visual Studio 2022 版本或 .NET 9 SDK 尝试这些功能。 Visual Studio 2022安装https://visualstudio.microsoft.com/zh-hans/dow…

Open R1 项目进展第一期

DeepSeek R1 发布已经两周了,而我们启动 open-r1 项目——试图补齐它缺失的训练流程和合成数据——也才过了一周。这篇文章简单聊聊:Open-R1 在模仿 DeepSeek-R1 流程和数据方面的进展 我们对 DeepSeek-R1 的认识和相关讨论 DeepSeek-R1 发布后社区搞出来的有趣项目这既是项目…

GPU内核实现(下)

3. ELLPACK 内核 ELLPACK SpMV实现沿行并行计算。由于数据已被重新排序为以列为主存储,因此沿ELLPACK数据连续行的内存访问被合并。在下面显示的实现中,假设输入cols和vals数组已经转换为ELLPACK格式。这种格式的一个关键部分是元数据参数,即每行非零的最大数量,它也作为参…