代理,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 和 表达树对比:

LambdaExpression Tree
在运行时直接编译为可执行代码以结构树来表示Lambda,可以在执行前检查,修改或转换
运行动态编码,就是在运行时确定程序逻辑
E.F 框架用表达树把C#代码转换为 SQL 查询
可以检查代码结构,生成新代码或修改

表达树适用很多进阶的场合:

  1. 自定义查询:自定义LINQ查询提供者把C#表达式转换为其他语言(比如:SQL或其他查询)
  2. 动态查询:根据用户输入在运行时里构造过滤器或者排序逻辑
  3. 延期运行:构造复杂的查询并让它延期运行

我们来演示一个简单的比较例子:

// 使用 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的按钮。说得技术流一点,事件机制用于让聆听者(或者订阅者)响应主体(发布者)的变化或行动。

事件的基本结构:

  1. 定义一个代理:指定事件处理程序的方法签名
  2. 声明这个事件:依据代理
  3. 发起事件:当发生事件时通知订阅者
  4. 订阅事件:用事件处理程序响应事件
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 操作符

  1. 过滤: Where
  2. 映射: Select Select Many
  3. 排序: OrderBy OrderByDescending
  4. 分组: Groupby
  5. 量化: Any All
  6. 汇总: Count Sum Average Max Min
  7. 连结: Join
  8. 集合: Distinct Union

这些只是 LINQ 标准查询操作符常用几个,但是只要掌握了这些常用的操作符,您也能以优雅、高效的方式处理大多数数据查询和操作场景!因为后面我们开发实际应用会碰到很多例子,所以这里不多举了,减少篇幅。

反射

通用运行时 CLR 对我们的程序所定义和使用的类型了如指掌。它要求所有程序集提供详细的元数据,描述每个类型的每个成员、包括私有实现细节。反射 API 允许我们访问这些信息,更重要的是,我们可以用反射激活某些操作,比如用反射方法对象调用该方法。甚至,在运行时生成代码。
具体说来,反射通常用于

  • 动态类型发现: 检查元数据,如类型、方法、属性和属性。
  • 运行时代码执行: 动态调用方法或访问字段和属性。
  • 动态代码生成: 在运行时创建新类型或方法。
  • 处理未知类型 用于在编译时不知道类型的插件或框架。
  • 序列化和反序列化 在数据转换过程中检查属性或字段。
  • 单元测试 测试私有方法或字段。
  1. 检查类型信息
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
  1. 动态创建实例
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
    }
}
  1. 检查和激活私有成员
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
    }
}
  1. 加载一个程序集
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# 的多线程语言特性。你只要会用这两个关键词,asyncawait ,你就已经会多线程的基础编程了。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 应用的实例。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注