代理,Lambda 和事件
现代语言都会大量使用回叫 Callback 机制,虽然 C# 的接口和虚拟方法也可以实现回叫,但是所有的 .NET 的 UI 框架都不会用接口的方式去实现,因为当有多种回叫时,就是成倍的麻烦来了。比如:最简单的鼠标单击,双击的交互事件,每个 UI 元素都会提供上百种通知,但是大多数时候,我们只是处理一两个事件,那么一个有上百种方法的接口就会非常的累赘。这只是一种情形,还有其他的情形让接口不适合做回叫。
C# 提供了一种简单的解决方式叫代理,代理指向一个方法。当你需要回叫时,你只需把代理的参考指向你需要的方法。
这样描述还是比较拗口,没关系,我们可以用前面一章的数组的 FindIndex() 方法实际演示一下代理。那段代码是这样的:
int[] numbers = { 1, 2, -3, 4, -5 };
int firstNegativeNumberIndex = Array.FindIndex(numbers, delegate1);
int findNumber4 = Array.FindIndex(numbers, delegate2);
int findFirstEvenNumber = Array.FindIndex(numbers, delegate3);
bool delegate1(int number)
{
return number < 0;
}
bool delegate2(int number)
{
return number == 4;
}
bool delegate3(int number)
{
return number % 2 == 0;
}
我们只要把 FindIndex 方法的第二参数也就是代理指向指定的方法,等待方法完成后回叫,就可以得到符合不同条件的结果。
对于代理,我们可以把他理解为函数指针(如果有C语言的知识,就更好理解了)。比如我们可以直接构造出代理函数在赋值给一个参考:
var p = new Predicate<int>(SomeFunc);
我们经常会选择另一种实用的方式创建代理,特别是在我们不知道目标方法或者对象,除非直到程序运行。C# 提供了反射 API 来帮助完成这个任务。
首先,我们要取得描述方法的对象 MethodInfo ;接着调用 CreateDelegate<TDelegate> 方法,指定代理类型作为类型参数传给目标对象。这样我们就可以创建任何由 MethodInfo 取得信息的方法的代理。
// Step 1: 为了示例,先自己定义一个方法
public static int Add(int x, int y)
{
return x + y;
}
// 定义方法签名相同的代理类型
delegate int MathOperation(int a, int b);
static void Main()
{
// Step 2: 用反射取得方法信息,名字要和寻找的目标方法名相同
MethodInfo methodInfo = typeof(Program).GetMethod("Add", BindingFlags.Public | BindingFlags.Static);
// Step 3: 创建定义过的代理类型的代理
MathOperation addDelegate = (MathOperation)methodInfo.CreateDelegate(typeof(MathOperation));
// Step 4: 运行代理
int result = addDelegate(5, 3);
Console.WriteLine($"Result of Add(5, 3): {result}");
}
通用代理类型
为了让我们不必每次都要自己定义代理,运行时库(runtime libraries)预先定义了几个常用的代理类型,不只它们自己会用到,我们也可以用哦。
常用的由两类,一是没有返回值的 Action ,二是带返回值的 Func。
public delegate void Action();
public delegate void Action<in T1>(T1 arg1);
public delegate void Action<in T1, in T2 >(T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg1);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, in T3, out TResult>(
T1 arg1, T2 arg2, T3 arg3);
匿名函数
我们不必显式地创建一个有名字的代理,而是创建一个类似函数的表达式,就是匿名函数。其实,运行时肯定要知道方法名称才能运行程序,匿名函数是指我们写代码的时候匿名,运行时会给它生成唯一的名字。
public static int GetIndexOfFirstPositiveNum(int[] numbers)
{
return Array.FindIndex( numbers, delegate(int value) { return value > 0 });
}
我们这里把函数匿名了。更进一步,现代 C# 代码用 Lambda 语法,更加直观,紧凑。
...
return Array.FindIndex( numbers, value => value > 0 );
...
到 C# 12.0, 我们可以定义带缺省值参数的 Lambda 表达式:
var defaultVal = (int x = 5) => x * 2;
我们从普通匿名函数跃进了 lambda ,真正的现代 C# 语言总算来了。试问,现在还有哪个流行语言没有 lambda ? 还有谁 ?
匿名函数除了简洁紧凑,还有一个有关上下文的超级实用的特性:容器方法(就是把匿名函数装进去)的变量可以被匿名函数拿来用!
using System;
class Program
{
static void Main()
{
// Define a local variable in the Main method
int counter = 0;
// Create an anonymous function (lambda) that captures the 'counter' variable
Action incrementCounter = () =>
{
counter++;
Console.WriteLine($"Counter value: {counter}");
};
// Call the anonymous function multiple times
incrementCounter(); // Output: Counter value: 1
incrementCounter(); // Output: Counter value: 2
incrementCounter(); // Output: Counter value: 3
}
}
Lambda 和表达树
表达树可以将代码表示为数据,允许我们在运行时动态操作和编译代码。最强大的应用场景之一,是构建 LINQ 提供程序或动态生成代码。既然有了 Lambda ,为啥还要用相对繁琐的表达树来完成工作?因为很多场景 Lambda 没有办法支持,特别在复杂的 LINQ 查询应对数据库数据的时候。
using System;
using System.Linq;
using System.Linq.Expressions;
class Program
{
static void Main()
{
var numbers = new[] { 1, 2, 3, 4, 5, 6 };
// Expression tree for the condition: n => n > 3
Expression<Func<int, bool>> condition = n => n > 3;
// Use the expression in a LINQ query
var result = numbers.AsQueryable().Where(condition).ToList();
Console.WriteLine(string.Join(", ", result)); // Output: 4, 5, 6
}
}
Lambda 和 表达树对比:
Lambda | Expression Tree | |
---|---|---|
在运行时直接编译为可执行代码 | 以结构树来表示Lambda,可以在执行前检查,修改或转换 | |
运行动态编码,就是在运行时确定程序逻辑 | ||
E.F 框架用表达树把C#代码转换为 SQL 查询 | ||
可以检查代码结构,生成新代码或修改 |
表达树适用很多进阶的场合:
- 自定义查询:自定义LINQ查询提供者把C#表达式转换为其他语言(比如:SQL或其他查询)
- 动态查询:根据用户输入在运行时里构造过滤器或者排序逻辑
- 延期运行:构造复杂的查询并让它延期运行
我们来演示一个简单的比较例子:
// 使用 lambda
Func<int, bool> isEven = x => x % 2 == 0;
var result = Enumerable.Range(1, 10).Where(isEven);
//使用表达树
using System.Linq.Expressions;
Expression<Func<int, bool>> expr = x => x % 2 == 0;
// Inspect the expression tree
Console.WriteLine($"Expression: {expr}");
Console.WriteLine($"Body: {expr.Body}");
Console.WriteLine($"Parameter: {expr.Parameters[0]}");
// Compile and execute the expression
Func<int, bool> isEven = expr.Compile();
var result = Enumerable.Range(1, 10).Where(isEven);
事件
当某些情况发生时,事件提供一个让一个类发送通知给另一个类或对象的方法;比如我们想知道用户什么时候点击了某个客户端UI的按钮。说得技术流一点,事件机制用于让聆听者(或者订阅者)响应主体(发布者)的变化或行动。
事件的基本结构:
- 定义一个代理:指定事件处理程序的方法签名
- 声明这个事件:依据代理
- 发起事件:当发生事件时通知订阅者
- 订阅事件:用事件处理程序响应事件
using System;
class Button
{
// Step 1: 指定一个代理(clickHandler),确定方法签名
public delegate void ClickHandler(object sender, EventArgs e);
// Step 2: 用这个代理声明一个事件(Clicked)
public event ClickHandler Clicked;
// 用方法来模拟点击
public void Click()
{
Console.WriteLine("Button clicked!");
// Step 3: 发起事件 (激活代理)
Clicked?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
// 创建实例
Button button = new Button();
// Step 4: 订阅 Clicked 事件
button.Clicked += OnButtonClick;
// 模拟点击
button.Click();
}
// 事件处理方法
static void OnButtonClick(object sender, EventArgs e)
{
Console.WriteLine("Button click event handled.");
}
}
我们先用这个经典的事件处理程序的写法,设置步骤可以看得更清楚。由于第一步,第二步是一个固定套路,现代版本是直接用 EventHandler ,像这样: public event EventHandler Clicked;
两步并一步搞定。public delegate void EventeHandler(object sender, EventArgs e)
叫标准事件代理模式。
虽然事件本质上的机制也是代理,但是从定义到语法都要很大的差别,我们看看这个比较表格,相信对我们理解这两个东东有帮助。
特性 | 代理 | 事件 |
---|---|---|
定义 | 函数指针指向方法 | 封装代理,用于在事情发生时通知订阅者 |
访问 | 被任何对象直接调用 | 只能被声明的类调用 |
使用场合 | 方法链,回叫,动态调用 | 发布-订阅模式(比如:点击,通知) |
安全性 | 较少限制 | 可控制限制——只允许订阅或退订 |
内建语法 | delegate 关键词 | event 关键词(基于代理) |
多点转换发送 | 可以指向多个方法 | 支持多个订阅者 |
语言集成查询 LINQ
LINQ 是 C# 威力强大的武器,主要用来应对集合类数据。LINQ 把 C# 和其他的提供者(providers)整合一起,这些提供者一些内建在运行时库里,一些在另外的 NuGet 包里。最典型的提供者就是我们在 Web 应用里总是用到的实体框架核心 Entity Framework Core,一个用于处理数据库数据的对象/关系映射系统。 Cosmos DB 云数据库(Microsoft Azure 的一项功能)提供了一个 LINQ 提供者,等等。
我们试着用日常语言描述它:“ LINQ 是和C#整合,让开发者可以用C#语法来构造查询。这些查询被 LINQ 提供者翻译成适合底层数据源的语言或命令,从而实现高效的数据操作。” 不知道这样是否更好理解。
请看一个用 LINQ 大炮打蚊子的例子,实际开发中对数据库的数据集操作也很相似。
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry", "Date" };
var result = fruits.Where(f => f.StartsWith("B"))
.Select(f => f.ToUpper());
foreach (var fruit in result)
{
Console.WriteLine(fruit); // Output: Banana
}
对于数据库,我们甚至可以用非常相似的 SQL 查询语言写 LINQ :
using (var db = new MyDbContext()) // db 是数据库上下文
{
var customers = from customer in db.Customers
where customer.City == "London"
select customer;
foreach (var c in customers)
{
Console.WriteLine(c.Name);
}
}
对于已经有 SQL 查询语言知识的同学来说就更容易理解掌握了。
LINQ 和泛型
LINQ 支持泛型,LINQ 查询到对象是使用 IEnumerrable<T> 接口。比如 SQL Sever 还提供 IQueryable<T> 接口。如果对数据库数据进行操作,我们最好把这两位好好比较,作一点点深入了解才行。
特性 | IEnumerable<T> | IQueryable<T> |
---|---|---|
定义 | 表示可以被枚举的对象集合 | 代表在数据源执行的一个查询 |
运行类型 | 在内存里 | 在数据库里(远程源) |
查询执行 | 当调用就马上执行查询 | 查询会在需要枚举是执行(延迟执行) |
查询转换 | 不能转换成数据库查询 | 把LINQ转换成数据库提供者的查询(比如:SQL) |
性能 | 在过滤前会全部加载到内存 | 在数据源过滤,减少内存和网络负载 |
可扩展 | 仅限于内存LINQ操作符 | 提供者支持的高级操作符(比如: GroupBy, Join ) |
所以当我们要从远端数据库查询大量数据时,最好要用IQueryable<T>。
标准 LINQ 操作符
- 过滤:
Where
- 映射:
Select
Select Many
- 排序:
OrderBy
OrderByDescending
- 分组:
Groupby
- 量化:
Any
All
- 汇总:
Count
Sum
Average
Max
Min
- 连结:
Join
- 集合:
Distinct
Union
这些只是 LINQ 标准查询操作符常用几个,但是只要掌握了这些常用的操作符,您也能以优雅、高效的方式处理大多数数据查询和操作场景!因为后面我们开发实际应用会碰到很多例子,所以这里不多举了,减少篇幅。
反射
通用运行时 CLR 对我们的程序所定义和使用的类型了如指掌。它要求所有程序集提供详细的元数据,描述每个类型的每个成员、包括私有实现细节。反射 API 允许我们访问这些信息,更重要的是,我们可以用反射激活某些操作,比如用反射方法对象调用该方法。甚至,在运行时生成代码。
具体说来,反射通常用于
- 动态类型发现: 检查元数据,如类型、方法、属性和属性。
- 运行时代码执行: 动态调用方法或访问字段和属性。
- 动态代码生成: 在运行时创建新类型或方法。
- 处理未知类型 用于在编译时不知道类型的插件或框架。
- 序列化和反序列化 在数据转换过程中检查属性或字段。
- 单元测试 测试私有方法或字段。
- 检查类型信息
using System;
using System.Reflection;
class Example
{
public int MyProperty { get; set; }
public void MyMethod() => Console.WriteLine("Hello from MyMethod");
static void Main()
{
Type type = typeof(Example);
Console.WriteLine($"Type Name: {type.Name}");
Console.WriteLine("Properties:");
foreach (var property in type.GetProperties())
{
Console.WriteLine($" - {property.Name}");
}
Console.WriteLine("Methods:");
foreach (var method in type.GetMethods())
{
Console.WriteLine($" - {method.Name}");
}
}
}
// Output:
// Type Name: Example
// Properties:
// - MyProperty
// Methods:
// - MyMethod
// - ToString
// - Equals
// - GetHashCode
// - GetType
- 动态创建实例
class Example
{
public string Message { get; set; }
public void PrintMessage() => Console.WriteLine(Message);
static void Main()
{
Type type = typeof(Example);
// Create an instance
object instance = Activator.CreateInstance(type);
// Set property value
var property = type.GetProperty("Message");
property.SetValue(instance, "Hello from Reflection");
// Invoke method
var method = type.GetMethod("PrintMessage");
method.Invoke(instance, null); // Output: Hello from Reflection
}
}
- 检查和激活私有成员
class Example
{
private string SecretMessage() => "This is a private message";
static void Main()
{
Type type = typeof(Example);
object instance = Activator.CreateInstance(type);
// Access private method
var method = type.GetMethod("SecretMessage", BindingFlags.NonPublic | BindingFlags.Instance);
string result = (string)method.Invoke(instance, null);
Console.WriteLine(result); // Output: This is a private message
}
}
- 加载一个程序集
class Example
{
static void Main()
{
Assembly assembly = Assembly.Load("mscorlib"); // Load an assembly by name
Console.WriteLine("Types in mscorlib:");
foreach (Type type in assembly.GetTypes())
{
Console.WriteLine(type.FullName);
}
}
}
属性 Attribute
在 .NET 里,我们用属性来注解组件,类型和它们的成员。一个属性的目的是控制或修改框架、工具、编译器或CLR的行为。
Web Appliction 开发的实体框架用属性定义C#的域类(比如:本地用C#定义的‘数据表’)和数据库结构之间的映射。这些属性属于 System.ComponentModel.DataAnnotations
命名空间的一部分。我们会经常用到这几个属性:
- [Key] 指定数据表的主键
- [Require] 表示一个属性是强制的(非空)
- [MaxLength]和[StringLength] 定义字符串或数组的最大长度
- [Column] 指定数据库的列名字
- [Table] 定义数据表的实体名字
- [ForeignKey] 指定可导航属性的外键
- [NotMapped] 取消属性映射到数据库
- [InverseProperty] 指定导航属性的反向关系
如果我们没有用到这些属性注解的域类,很大概率我们映射到的数据库表本身是错的或者以后使用中会出错。
在其他程序逻辑中,我们很可能需要自定义属性(你会发现自定义属性会让代码显得非常优雅和简洁),我们先学习一个简单的例子:
定义一个属性
// Define a custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
class MyCustomAttribute : Attribute
{
public string Description { get; }
public int Version { get; }
public MyCustomAttribute(string description, int version)
{
Description = description;
Version = version;
}
}
使用这个自定义属性
[MyCustomAttribute("This is a sample class", 1)]
class SampleClass
{
[MyCustomAttribute("This is a sample method", 2)]
public void SampleMethod()
{
Console.WriteLine("Executing SampleMethod.");
}
}
自定义属性的注解属性,我们望文生义就好。
泛型属性
从 C# 11.0 和 .NET 7 开始,加入了定义泛型属性类型。
public class MyGenericAttribute<T> : Attribute
{
public string Description { get; }
public MyGenericAttribute(string description)
{
Description = description;
}
}
// 使用泛型属性
[MyGenericAttribute<int>("This is an integer attribute")]
public class MyClass
{
[MyGenericAttribute<string>("This is a string attribute")]
public void MyMethod() { }
}
泛型属性的出现主要是避免因为不同的类型重复同样的程序逻辑;与反射相结合,可用于验证、序列化或自定义行为等高级场景。因为C#想把自己越做越大,一站解决所有问题,重用性总是优先考虑的。
多线程
我们想之间跳过多线程的理论,直接进入 C# 的多线程语言特性。你只要会用这两个关键词,async 和 await ,你就已经会多线程的基础编程了。Web Application 要用到 HTTP 请求,如果不用异步技术,你知道我们可能就会坐在那里傻等了。
// 这只是的例子,异步方法最好有返回值
private async void FetchHeaders(string url, IHttpClientFactory factory)
{
using (HttpClient hc = factory.CreateClient())
{
var req = new HttpRequestMessage(HttpMethod.Head, url);
HttpRequestMessage res =
await w.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
Console.WriteLine(res.Headers.ToString());
}
}
我们可以异步处理数据:
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}
}
static async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(500); // Simulate async work
yield return i;
}
}
}
我们也可以异步处理异常
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
await ThrowExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Caught Exception: {ex.Message}");
}
}
static async Task ThrowExceptionAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("An error occurred.");
}
}
请一定记住,async 和 await 一定是配对出现的,否则我们会得到难以预料的结果。
开发 Web 应用的知识点就总结到这里了。从第二章开始到第四章的内容应该会覆盖超过95%的基本 Web 应用开发技术。祝大家开发愉快。下一章就先牛刀小试一个 Web 应用的实例。
发表回复