读书笔记-C#8.0本质论-03
15. 委托和lambda表达式
15.1 委托概述
namespace ConsoleApp1;
internal static class Program
{
private enum SortType
{
Ascending,
Descending
}
private static void BubbleSort(ref IList<int> items, SortType sortOrder)
{
for (var i = items.Count - 1; i >= 0; i--)
{
for (var j = 1; j <= i; j++)
{
var swap = sortOrder switch
{
SortType.Ascending => items[j - 1] > items[j],
SortType.Descending => items[j - 1] < items[j],
_ => false
};
if (swap)
{
(items[j - 1], items[j]) = (items[j], items[j - 1]);
}
}
}
}
private static void Main(string[] args)
{
IList<int> items = new[] { 1000000, 11, 13, 15, 16, 20, 1, 2, 3, 4, 5 };
BubbleSort(ref items, SortType.Ascending);
foreach (var item in items)
{
Console.Write($"{item}, ");
}
Console.WriteLine();
BubbleSort(ref items, SortType.Descending);
foreach (var item in items)
{
Console.Write($"{item}, ");
}
}
}
上述代码只照顾到了两种排序方式,如果需要添加更多的排序方式还要添加更多的switch分支和分支方法,BubbleSort排序操作与判断操作紧耦合,不利于扩展,违反单一职责原则。委托允许捕捉对方法的引用,并向传递其他对象那样传递该引用(类似于C++中的函数指针),像调用其他方法那样调用被捕捉的方法。委托可以解耦排序操作与判断操作,示例如下:
namespace ConsoleApp1;
internal static class Program
{
private delegate bool CustomCompare(int int1, int int2);
private static void BubbleSort(ref IList<int> items, CustomCompare compare)
{
for (var i = items.Count - 1; i >= 0; i--)
{
for (var j = 1; j <= i; j++)
{
if (compare(items[j - 1], items[j]))
{
(items[j - 1], items[j]) = (items[j], items[j - 1]);
}
}
}
}
private static void Main(string[] args)
{
IList<int> items = new[] { 1000000, 11, 13, 15, 16, 20, 1, 2, 3, 4, 5 };
// 通过匿名方法传递委托 从小到大排序 items[j-1]>items[j]时才交换
BubbleSort(ref items, delegate(int int1, int int2) { return int1 > int2; });
foreach (var item in items)
{
Console.Write($"{item}, ");
}
Console.WriteLine();
// 通过lambda表达式传递委托 从大到小排序 items[j-1]<items[j]时才交换
BubbleSort(ref items, (int1, int2) => int1 < int2);
foreach (var item in items)
{
Console.Write($"{item}, ");
}
}
}
从C#3.0开始,基本库预定义了两种泛型委托System.Action和System.Func。由于是泛型委托,所以这两种委托在大多数情况下可以替代自定义委托,但自定义委托如果能带来可读性提升,则优先使用自定义委托。下面的示例代码使用System.Func委托并实现按照字母排序:
namespace ConsoleApp1;
internal static class Program
{
private static void BubbleSort(ref IList<int> items, Func<int, int, bool> compare)
{
for (var i = items.Count - 1; i >= 0; i--)
{
for (var j = 1; j <= i; j++)
{
if (compare(items[j - 1], items[j]))
{
(items[j - 1], items[j]) = (items[j], items[j - 1]);
}
}
}
}
private static bool AlphabeticalGreaterThan(int first, int second)
{
// 得到两个字符串编码的差值
var comparison = string.Compare(first.ToString(), second.ToString(), StringComparison.Ordinal);
// 如果差值大于0则交换:在转换成字符串后,first比second大
return comparison > 0;
}
private static void Main(string[] args)
{
IList<int> items = new[] { 100, 1, 200, 2, 300, 3, 4, 400, 5, 50 };
BubbleSort(ref items, AlphabeticalGreaterThan);
foreach (var item in items)
{
Console.Write($"{item}, ");
}
Console.WriteLine();
}
}
15.2 委托的内部机制
委托实际上是特殊的类,虽然C#标准没有明确说明类的层次结构,但委托必须直接或间接派生自System.Delegate类。System.Delegate有两个属性:
第一个属性是System.Reflection.MethodInfo类型,MethodInfo描述方法签名,包括:名称、参数和返回类型。除了MethodInfo,委托还需要一个对象实例来包含要调用的方法,这正是第二个属性的作用。Target属性为object类型,当要调用的方法为静态方法时Target为null。详见:Delegate 类 (System) | Microsoft Learn
需要注意的是,委托创建好之后无法更改,如果想要引用不同的方法,则只能创建新的委托再把它赋给变量。换言之,所有委托都是不可变的(immutable)。
15.3 委托不具有结构相等性
.NET委托类型不具备结构相等性,也就是说,不能将一个委托类型的对象引用转换成一个不相关的委托类型,即使两者的形参和返回类型完全一致。下面两个委托虽然形参和返回值完全一致但不能相互转换:
private delegate bool CustomCompare(int int1, int int2);
private static Func<int, int, bool> compare;
15.4 lambda表达式的外部变量捕获
namespace ConsoleApp1;
internal static class Program
{
private static void Main(string[] args)
{
var items = new string[] { "moe", "larry", "Curly" };
var actions = new List<Action>();
foreach (var item in items)
{
actions.Add(() => Console.WriteLine(item));
}
foreach (var action in actions)
{
action.Invoke();
}
}
}
上述代码在C#5.0及以后的输出结果:
moe
larry
Curly
上述代码在C#4.0的输出结果:
Curly
Curly
Curly
原因在于,Lambda表达式捕捉变量并总是使用其最新值,而不是捕捉变量并保留变量在委托创建时的值。
C#4.0在捕捉循环变量时,每个委托都捕获了同一个循环变量,循环变量的值发生变化时,捕捉它的每一个委托都应用了这一变化。
C#5.0对此进行了更改,认为在每一次循环迭代中所捕获的循环变量都应该视为"新"的(状态发生了改变),不能视为同一个变量。上述代码在编译器层面的循环变量捕获如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.DisableOptimizations)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
namespace ConsoleApp1
{
internal static class Program
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public string item;
internal void <Main>b__0()
{
Console.WriteLine(item);
}
}
private static void Main()
{
string[] array = new string[3];
array[0] = "moe";
array[1] = "larry";
array[2] = "Curly";
string[] array2 = array;
List<Action> list = new List<Action>();
string[] array3 = array2;
int num = 0;
while (num < array3.Length)
{
// 每次循环都会创建一个由编译器自动创建的委托类的实例
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
// 每次循环都会在委托类的实例字段中保留当前值
<>c__DisplayClass0_.item = array3[num];
// 添加新委托,这个委托调用委托类中的Main方法
list.Add(new Action(<>c__DisplayClass0_.<Main>b__0));
num++;
}
List<Action>.Enumerator enumerator = list.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
Action current = enumerator.Current;
current();
}
}
finally
{
((IDisposable)enumerator).Dispose();
}
}
}
}
设计规范
避免在用于委托的lambda表达式中捕获循环变量,如果确实需要捕获循环变量,则需要在循环体内部显式声明一个新变量,这样可以确保每个委托中捕获都是不同变量。示例如下:
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
var items = new string[] { "moe", "larry", "Curly" };
var actions = new List<Action>();
foreach (var item in items)
{
// 在循环体中显式定义新变量
var value = item;
// 在委托中捕获这个新变量
actions.Add(() => Console.WriteLine(value));
}
foreach (var action in actions)
{
action.Invoke();
}
}
}
***
# 16. 事件
委托是Publish-Subscribe模式的基本单位,该模式又被称为Observer(观察者)模式,这些模式都能用委托单独实现,但事件(event)提供了额外的封装性,让publish-subscribe模式更容易实现且不易出错。
## 16.1 多播委托
### 16.1.1 使用多播委托实现publish-subscribe模式
以恒温器为例,一个恒温器由加热器、冷却器组成,该恒温器接收布置在室内的温度传感器发出的信号,并决定是升温还是降温。在这个示例中,温度传感器是“温度改变”这一事件的发布者,恒温器是“温度改变”这一事件的订阅者。
~~~ CSharp
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
// 设定恒温器温度和温度变化范围
Thermostat.Temperature = 30;
Thermostat.Precision = 2;
// 连接发布者与订阅者
Thermometer.OnTemperatureChanged += Thermostat.Heater.OnTemperatureChanged;
Thermometer.OnTemperatureChanged += Thermostat.Cooler.OnTemperatureChanged;
for (var i = 0; i < 20; i++)
{
// 模拟温度变化
Console.Write("Enter temperature: ");
var currentTemperature = Console.ReadLine();
Thermometer.CurrentTemperature = float.TryParse(currentTemperature, out var result)
? result
: Thermometer.CurrentTemperature;
}
}
}
// 事件的发布者
internal static class Thermometer
{
// 定义要发送的事件
public static Action<float>? OnTemperatureChanged { get; set; }
// 如果当前温度发生变化就发布温度变化事件
private static float _currentTemperature;
public static float CurrentTemperature
{
get => _currentTemperature;
set
{
// 注意:浮点型有精度问题,不能用 "value != _currentTemperature" 判断温度是否发生变化,需要设置一个最大容许极限
// 如果变化范围在这个极限内,则认为 "value == _currentTemperature"
if (Math.Abs(value - _currentTemperature) < 0.05) return;
_currentTemperature = value;
// 线程安全的委托调用:条件性调用委托,只有委托非空时才Invoke
OnTemperatureChanged?.Invoke(value);
}
}
}
// 事件的订阅者 恒温器中的加热器和冷却器
internal static class Thermostat
{
public static float Temperature { get; set; } = 50;
public static float Precision { get; set; } = 1;
internal static class Cooler
{
public static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange > Precision / 2f ? "Cooler: On" : "Cooler: Off");
}
}
internal static class Heater
{
public static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange < -Precision / 2f ? "Heater: On" : "Heater: Off");
}
}
}
~~~
### 16.1.2 多播委托的线程安全性
如前所述,由于订阅者可由不同线程从委托中增删,所以有必要条件性地调用委托,或者在空检查前将委托引用拷贝到局部变量中。虽然这样能防范调用空委托,但不能防范所有可能的竞争。例如,一个线程拷贝委托,另一个线程将委托重置为null,然后原始线程调用委托之前的值(该值已过时),向一个已经不在列表中的订阅者发送通知。在多线程程序中,订阅者应确保在这种情况下的健壮性,随时准备好调用一个过时委托的准备。
### 16.1.3 多播委托的内部机制
向多播委托添加方法时,MulticastDelegate类会创建委托类型的一个新实例,在新实例中为新增的方法存储对象引用和方法引用,并在委托实例列表中添加新的委托实例作为下一项,所以MulticastDelegate类实际上维护着一个Delegate对象链表,也就是说,多播委托是顺序执行的。
### 16.1.4 多播委托中的错误处理
如上所述,多播委托是顺序执行的,在不采取防御性编程措施的情况下,如果一个订阅者抛出异常,链中的后续订阅者就收不到通知,这是不合理的。为避免该问题,必须手动遍历订阅者链表,并单独调用它们,这样无论上一个订阅者有过什么样的行为都不会影响到下一个订阅者。示例如下:
~~~ CSharp
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
// 设定恒温器温度和温度变化范围
Thermostat.Temperature = 30;
Thermostat.Precision = 2;
// 连接发布者与订阅者
Thermometer.OnTemperatureChanged += Thermostat.Heater.OnTemperatureChanged;
// 手动创建一个会抛出异常的订阅者
Thermometer.OnTemperatureChanged += _ => throw new InvalidOperationException();
Thermometer.OnTemperatureChanged += Thermostat.Cooler.OnTemperatureChanged;
for (var i = 0; i < 20; i++)
{
try
{
// 模拟温度变化
Console.Write("Enter temperature: ");
var currentTemperature = Console.ReadLine();
Thermometer.CurrentTemperature = float.TryParse(currentTemperature, out var result)
? result
: Thermometer.CurrentTemperature;
}
catch (AggregateException e)
{
Console.WriteLine(e.Message);
}
}
}
}
// 事件的发布者 需要在事件的发布者中处理来自事件订阅者的异常
internal static class Thermometer
{
// 定义要发送的事件
public static Action<float>? OnTemperatureChanged { get; set; }
// 如果当前温度发生变化就发布温度变化事件
private static float _currentTemperature;
public static float CurrentTemperature
{
get => _currentTemperature;
set
{
// 注意:浮点型有精度问题,不能用 "value != _currentTemperature" 判断温度是否发生变化,需要设置一个最大容许极限
// 如果变化范围在这个极限内,则认为 "value == _currentTemperature"
if (Math.Abs(value - _currentTemperature) < 0.05) return;
_currentTemperature = value;
// 线程安全的委托调用:在空检查前将委托引用拷贝到局部变量中 防御性编程:处理可能来自事件订阅者的异常
var onTemperatureChanged = OnTemperatureChanged;
if (onTemperatureChanged is null) return;
// 收集来自订阅者的异常
var exceptionCollection = new List<Exception>();
// 异常处理
foreach (var handler in onTemperatureChanged.GetInvocationList())
{
try
{
// handler是Delegate类型,Delegate类型是实际类型Action<float>的泛化类型,不能直接使用Invoke调用委托
// 要不然将handler强制转型为Action<float> 【没有装箱操作,执行效率高】
//((Action<float>)handler).Invoke(value);
// 要不然使用DynamicInvoke方法利用反射调用委托 【有一次装箱操作 float => object】
handler.DynamicInvoke(value);
}
catch (Exception e)
{
exceptionCollection.Add(e);
}
}
if (exceptionCollection.Count > 0)
throw new AggregateException(
"one or more exceptions thrown by OnTemperatureChange Event subscribers.", exceptionCollection);
}
}
}
// 事件的订阅者 恒温器中的加热器和冷却器
internal static class Thermostat
{
public static float Temperature { get; set; } = 50;
public static float Precision { get; set; } = 1;
internal static class Cooler
{
public static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange > Precision / 2f ? "Cooler: On" : "Cooler: Off");
}
}
internal static class Heater
{
public static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange < -Precision / 2f ? "Heater: On" : "Heater: Off");
}
}
}
~~~
### 16.1.5 多播委托中的返回值问题
除了16.1.4中的在多播委托调用链中处理可能的错误外,还有一种情况需要遍历委托调用列表而非直接调用一个委托。这种情形涉及的委托要么不返回void,要么具有ref或out参数。在这种情况下,必须遵循和错误处理一样的模式使用GetInvocationList方法遍历每一个委托调用列表来获取每一个单独的返回值。但一般原则是:
<span style="color:crimson;font-weight:bold">通过只返回void且不用ref和out参数来彻底避免需要逐一处理多播委托中返回值的情况。</span>
## 16.2 理解事件
上面提到,事件能做的事情委托也能做,但在委托结构中事件的订阅和发布都不能得到充分控制。
使用事件的好处有:
1. 只有直接持有一个事件对象的类可以调用这个事件对象,其他类只能使用+=或-=向这个事件对象添加或删除对事件的订阅,而不能重写整个事件的订阅。
2. 事件确保了只有包容类才能触发事件通知。
### 16.2.1 委托无法对事件订阅封装
如果在16.1.4的Main方法中不小心将一个`+=`运算符写成了`=`会怎样?
~~~ CSharp
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
// 设定恒温器温度和温度变化范围
Thermostat.Temperature = 30;
Thermostat.Precision = 2;
// 连接发布者与订阅者
Thermometer.OnTemperatureChanged += Thermostat.Heater.OnTemperatureChanged;
// 手动创建一个会抛出异常的订阅者
Thermometer.OnTemperatureChanged += _ => throw new InvalidOperationException();
// Bug:少了一个加号
Thermometer.OnTemperatureChanged = Thermostat.Cooler.OnTemperatureChanged;
for (var i = 0; i < 20; i++)
{
try
{
// 模拟温度变化
Console.Write("Enter temperature: ");
var currentTemperature = Console.ReadLine();
Thermometer.CurrentTemperature = float.TryParse(currentTemperature, out var result)
? result
: Thermometer.CurrentTemperature;
}
catch (AggregateException e)
{
Console.WriteLine(e.Message);
}
}
}
}
~~~
在上述代码中:
~~~ CSharp
Thermometer.OnTemperatureChanged += Thermostat.Cooler.OnTemperatureChanged;
~~~
变成了
~~~ CSharp
Thermometer.OnTemperatureChanged = Thermostat.Cooler.OnTemperatureChanged;
~~~
这一错误操作直接重写了订阅列表,即:取消了OnTemperatureChanged事件的其他订阅者,只保留了Thermostat.Cooler这一个订阅者。事实上应该禁止其他类重写整个事件的订阅,只能使用+=或-=向这个事件对象添加或删除对事件的订阅。
### 16.2.2 委托无法对事件发布封装
如果在16.1.4的Main方法中直接调用发布者的委托方法会怎样?(假传圣旨)
~~~ CSharp
internal static class Program
{
private static void Main()
{
// 设定恒温器温度和温度变化范围
Thermostat.Temperature = 30;
Thermostat.Precision = 2;
// 连接发布者与订阅者
Thermometer.OnTemperatureChanged += Thermostat.Heater.OnTemperatureChanged;
// 手动创建一个会抛出异常的订阅者
Thermometer.OnTemperatureChanged += _ => throw new InvalidOperationException();
Thermometer.OnTemperatureChanged += Thermostat.Cooler.OnTemperatureChanged;
// 假传圣旨中 即使温度没有发生变化也成功触发了事件,还跳过了所有发布者对订阅者的异常处理机制
Thermometer.OnTemperatureChanged.Invoke(10);
for (var i = 0; i < 20; i++)
{
try
{
// 模拟温度变化
Console.Write("Enter temperature: ");
var currentTemperature = Console.ReadLine();
Thermometer.CurrentTemperature = float.TryParse(currentTemperature, out var result)
? result
: Thermometer.CurrentTemperature;
}
catch (AggregateException e)
{
Console.WriteLine(e.Message);
}
}
}
}
~~~
在上述代码中,与事件无关的类Program(既不是事件的订阅者也不是事件的发布者)绕过发布者成功触发了虚假的温度变化事件。事实上,事件的发布者应禁止其他任何类发布事件。
### 16.2.3 事件声明和编码规范
C\#使用了event关键字解决了委托对事件封装不足的问题,示例如下:
~~~ CSharp
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
// 设定恒温器温度和容许的温度变化范围
Thermostat.Temperature = 30;
Thermostat.Precision = 2;
var senderThermometer1 = new Thermometer();
var senderThermometer2 = new Thermometer();
// 同时订阅来自两个不同Thermometer的OnTemperatureChange事件
senderThermometer1.OnTemperatureChange += Thermostat.Cooler.OnTemperatureChanged;
senderThermometer1.OnTemperatureChange += Thermostat.Heater.OnTemperatureChanged;
senderThermometer2.OnTemperatureChange += Thermostat.Cooler.OnTemperatureChanged;
senderThermometer2.OnTemperatureChange += Thermostat.Heater.OnTemperatureChanged;
// 模拟温度变化
for (var i = 0; i < 20; i++)
{
try
{
// 模拟温度变化
Console.Write("Enter temperature: ");
var currentTemperature = Console.ReadLine();
senderThermometer1.CurrentTemperature = float.TryParse(currentTemperature, out var result)
? result
: senderThermometer1.CurrentTemperature;
senderThermometer2.CurrentTemperature = senderThermometer1.CurrentTemperature;
}
catch (AggregateException e)
{
Console.WriteLine(e.Message);
}
}
}
}
// 事件的发布者
internal class Thermometer
{
// 事件参数 派生自 System.EventArgs
public class TemperatureArgs : EventArgs
{
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
public float NewTemperature { get; }
}
// 使用EventHandler<T>定义事件,其中T为事件参数
public event EventHandler<TemperatureArgs>? OnTemperatureChange;
// 事件发布
public float CurrentTemperature
{
get => _currentTemperature;
// 触发事件
set
{
if (Math.Abs(value - CurrentTemperature) < 0.05) return;
_currentTemperature = value;
// 调用事件委托
// 将OnTemperatureChange赋值给一个局部变量,使得null检查成为线程安全的操作
// 原因在于:OnTemperatureChange改变后会重新实例化一个多播委托然后再将地址交给OnTemperatureChange,
// 这一操作不会对原始的多播委托(局部变量onTemperatureChange也指向该委托)产生任何影响。
// 这就使得代码具有了线程安全性(现在只有一个线程能访问onTemperatureChange指向的内存区域了)
var onTemperatureChange = OnTemperatureChange;
if (onTemperatureChange is null) return;
var exceptionCollection = new List<Exception>();
foreach (var handler in onTemperatureChange.GetInvocationList())
{
try
{
//handler.DynamicInvoke(this, new TemperatureArgs(value));
// 泛型EventHandler类型的定义为 public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);
((EventHandler<TemperatureArgs>)handler).Invoke(this, new TemperatureArgs(value));
}
catch (Exception e)
{
exceptionCollection.Add(e);
}
}
if (exceptionCollection.Count > 0)
throw new AggregateException(
"one or more exceptions thrown by OnTemperatureChange Event subscribers.", exceptionCollection);
}
}
private float _currentTemperature;
}
// 事件的订阅者 恒温器中的加热器和冷却器
internal static class Thermostat
{
public static float Temperature { get; set; } = 50;
public static float Precision { get; set; } = 1;
internal static class Cooler
{
private static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange > Precision / 2f ? "Cooler: On" : "Cooler: Off");
}
public static void OnTemperatureChanged(object? sender, Thermometer.TemperatureArgs e)
{
OnTemperatureChanged(e.NewTemperature);
Console.WriteLine($"事件发布者: {sender ?? "null".GetType()} HashCode: {sender?.GetHashCode()}");
}
}
internal static class Heater
{
private static void OnTemperatureChanged(float newTemperature)
{
var temperatureChangeRange = newTemperature - Temperature;
Console.WriteLine(temperatureChangeRange < -Precision / 2f ? "Heater: On" : "Heater: Off");
}
public static void OnTemperatureChanged(object? sender, Thermometer.TemperatureArgs e)
{
OnTemperatureChanged(e.NewTemperature);
Console.WriteLine($"事件发布者: {sender ?? "null".GetType()} HashCode: {sender?.GetHashCode()}");
}
}
}
~~~
对16.1.4示例进行了如下5项修改:
1. 在Thermometer类中,将`Action<float>`类型的属性替换为`EventHandler<TemperatureArgs>?`类型的事件字段,关键词event为委托提供了额外的封装
2. 在Thermometer类中,定义了嵌套类TemperatureArgs,该类派生自EventArgs,用于封装事件参数,而不是通过`Action<float>`类型的属性直接传递事件参数
3. 在Thermometer类中,定义了float类型的新属性CurrentTemperature,在这个属性中发布并调用事件委托。其中,在调用事件委托时按照编码规范向委托传递了事件参数和调用委托类的实例,这样订阅者就能区分是哪个发布者发布的事件。如果发布者是静态类,应向订阅者传递null和事件参数,意思是只有一个发布者能发布这个名称的事件。
4. 去除Thermometer类的static修饰并实例化Thermometer类,以演示订阅者如何通过发布者传递的sender参数区分不同的发布者。
5. 重载订阅者中的OnTemperatureChanged方法以匹配事件发布者传递的参数。
### 16.2.4 事件的线程安全性
在16.1.4和16.2.3的示例中,事件发布者在Invoke来自事件订阅者的多播委托或EventHandler,将OnTemperatureChange赋值给一个局部变量,然后再检查是否为空,这样做的目的是保证线程安全。如果没有将委托赋值给局部变量,那么在检查OnTemperatureChange是否为空到实际触发事件的这段时间内,OnTemperatureChange可能被其它线程设置为null,在这种情况下调用委托会抛出NullReferenceException。
<span style="color:crimson;font-weight:bold">问题:</span>多播委托也是引用类型,为什么将多播委托赋值一个局部变量然后再触发这个局部变量,就足以保证线程安全了呢?
<span style="color:crimson;font-weight:bold">解答:</span>正如问题所描述的那样,多播委托也是引用类型,局部变量和多播委托指向同一地址,但是多播委托和其他类型的变量不同,多播委托不可原地修改,即:对委托的任何操作都不会在原地址上修改,而是在托管堆上开辟另一块空间建立全新的多播委托。在这种情况下:
1. 如果这个局部变量所指向的多播委托没有被其他线程更改,没有写操作,自然是线程安全的。
2. 如果这个局部变量所指向的多播委托被其他线程修改了,那么其他线程修改后的多播委托是全新的多播委托,与局部变量所指向的这个旧多播委托没有任何关系,那么能够同时操作同一块内存区域的线程只有一个,也就是建立这个局部变量的线程,没有竞态条件,也就保证了线程安全。
### 16.2.5 泛型和委托
在定义事件时,使用了`EventHandler<TEventArgs>`泛型类型,理论上任何委托类型都可以,但按照约定第一个参数为object类型的sender,第二个参数是从System.EventArgs派生的类型,如果有需要可以自定义事件类型,只需满足上述要求即可。对16.2.3中的Thermometer类进行改进,将:
~~~ CSharp
public event EventHandler<TemperatureArgs>? OnTemperatureChange;
~~~
修改为:
~~~ CSharp
public delegate void TemperatureChangeHandler(object? sender, TemperatureArgs args);
public event TemperatureChangeHandler? OnTemperatureChange;
~~~
可以达到同样的效果。
虽然通常应该使用`EventHandler<TEventArgs>`泛型类型,而不是创建TemperatureChangeHandler这样的自定义事件委托,但在处理遗留代码或者需要自定义事件委托澄清参数名的含义时仍然需要自定义事件委托。
### 16.2.6 事件的内部机制
在16.2.3中使用了以下代码声明了一个事件:
~~~ CSharp
internal class Thermometer
{
public event EventHandler<TemperatureArgs>? OnTemperatureChange;
}
~~~
但这只是C\#对事件声明的简化,实际上C\#编译器遇到event关键字后生成的CLR代码等价于:
~~~ CSharp
private EventHandler<TemperatureArgs>? _OnTemperatureChange;
public event EventHandler<TemperatureArgs> OnTemperatureChange
{
add
{
EventHandler<TemperatureArgs> eventHandler = this.OnTemperatureChange;
while (true)
{
EventHandler<TemperatureArgs> eventHandler2 = eventHandler;
EventHandler<TemperatureArgs> value2 = (EventHandler<TemperatureArgs>)Delegate.Combine(eventHandler2, value);
// 多线程同步 原子操作
eventHandler = Interlocked.CompareExchange(ref this.OnTemperatureChange, value2, eventHandler2);
if ((object)eventHandler == eventHandler2)
{
break;
}
}
}
remove
{
EventHandler<TemperatureArgs> eventHandler = this.OnTemperatureChange;
while (true)
{
EventHandler<TemperatureArgs> eventHandler2 = eventHandler;
EventHandler<TemperatureArgs> value2 = (EventHandler<TemperatureArgs>)Delegate.Remove(eventHandler2, value);
// 多线程同步 原子操作
// 将this.OnTemperatureChange的值与eventHandler2比较,如果两个对象引用相等,用value2替换this.OnTemperatureChange
eventHandler = Interlocked.CompareExchange(ref this.OnTemperatureChange, value2, eventHandler2);
if ((object)eventHandler == eventHandler2)
{
break;
}
}
}
}
~~~
从上面的示例代码可以看出,事件的声明和属性的声明十分相似,事件有add和remove,属性有get和set,只是事件的声明中涉及到多线程同步看起来比较复杂。由此可见,和属性一样,事件的add和remove具体执行什么操作也是可以自定义的,虽然在绝大部分情况下没有必要自定义事件的add和remove操作。
## 16.3 事件总结
属性是对字段的封装,事件则是对委托的封装。现在先回顾下属性的完整写法:
~~~ CSharp
internal class PriceManager
{
/* 属性的完整写法 */
private decimal _price;
public decimal Price
{
get => _price;
set => _price = value;
}
}
~~~
然后是事件的完整写法:
~~~ CSharp
/* 事件参数类型定义 */
public sealed class PriceChangeEnventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangeEnventArgs(decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
/* 委托类型定义 */
public delegate void PriceChangedHandler(object sender, PriceChangeEnventArgs enventArgs);
internal sealed class PriceManager
{
/* 定义事件 */
// 先定义委托
private PriceChangedHandler? _priceChangedHandler;
// 再用事件封装委托
public event PriceChangedHandler PriceChangeEvent
{
add => _priceChangedHandler += value;
remove => _priceChangedHandler -= value;
}
private void OnPriceChange(PriceChangeEnventArgs e)
{
_priceChangedHandler?.Invoke(this, e);
}
}
~~~
和属性的定义及其相似。在定义事件时,先定义了一个私有的委托,然后再定义一个事件去封装这个委托,这个事件提供了add和remove方法去操作这个委托,对委托加了一层封装后,在PriceManager之外就无法直接操作委托,添加和删除委托要通过事件代理,提升了安全性。
下面演示同时使用属性和事件,在价格改变时向外发送通知,并调用对应的事件处理程序。
~~~ CSharp
namespace LearnDotNetCore;
internal static class Program
{
private static void Main(string[] args)
{
var priceManager = new PriceManager(100);
priceManager.PriceChangeEvent += PriceChangeListener1;
priceManager.PriceChangeEvent += PriceChangeListener2;
// price没有改变,PriceManager没有发出通知
priceManager.Price = 100;
// price改变,PriceManager发出通知
priceManager.Price = 300;
}
private static void PriceChangeListener1(object sender, PriceChangeEnventArgs enventArgs)
{
Console.WriteLine(
$"PriceChangeListener1 Execute. LastPrice:{enventArgs.LastPrice} CurrentPrice: {enventArgs.NewPrice}");
}
private static void PriceChangeListener2(object sender, PriceChangeEnventArgs enventArgs)
{
Console.WriteLine(
$"PriceChangeListener2 Execute. LastPrice:{enventArgs.LastPrice} CurrentPrice: {enventArgs.NewPrice}");
}
}
/* 事件参数类型定义 */
public sealed class PriceChangeEnventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangeEnventArgs(decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
/* 委托类型定义 */
public delegate void PriceChangedHandler(object sender, PriceChangeEnventArgs enventArgs);
internal sealed class PriceManager
{
public PriceManager(decimal price = 0)
{
Price = price;
}
/* 定义属性 */
private decimal _price;
public decimal Price
{
get => _price;
set
{
// 如果价格没有发生改变就直接返回
if (_price == value) return;
// 如果价格变化了,发送价格变动的通知(实质是执行与多播委托PriceChangedHandler实例关联的所有方法)
OnPriceChange(new PriceChangeEnventArgs(lastPrice: _price, newPrice: value));
// 更新价格
_price = value;
}
}
/* 定义事件 */
// 先定义委托
private PriceChangedHandler? _priceChangedHandler;
// 再用事件封装委托
public event PriceChangedHandler PriceChangeEvent
{
add => _priceChangedHandler += value;
remove => _priceChangedHandler -= value;
}
private void OnPriceChange(PriceChangeEnventArgs e)
{
_priceChangedHandler?.Invoke(this, e);
}
}
~~~
如果使用简化的事件声明方式并且使用预定义的EventHandler\<CustomEventArgs\>类型,上述代码可以得到极大的简化,简化后的代码如下:
~~~ CSharp
namespace LearnDotNetCore;
internal static class Program
{
private static void Main(string[] args)
{
var priceManager = new PriceManager(100);
priceManager.PriceChangeEvent += PriceChangeListener1;
priceManager.PriceChangeEvent += PriceChangeListener2;
// price没有改变,PriceManager没有发出通知
priceManager.Price = 100;
// price改变,PriceManager发出通知
priceManager.Price = 300;
}
private static void PriceChangeListener1(object? sender, PriceChangeEnventArgs enventArgs) =>
Console.WriteLine(
$"PriceChangeListener1 Execute. LastPrice:{enventArgs.LastPrice} CurrentPrice: {enventArgs.NewPrice}");
private static void PriceChangeListener2(object? sender, PriceChangeEnventArgs enventArgs) =>
Console.WriteLine(
$"PriceChangeListener2 Execute. LastPrice:{enventArgs.LastPrice} CurrentPrice: {enventArgs.NewPrice}");
}
/* 事件参数类型定义 */
public sealed class PriceChangeEnventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangeEnventArgs(decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
internal sealed class PriceManager
{
public PriceManager(decimal price = 0) => Price = price;
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price == value) return;
OnPriceChange(new PriceChangeEnventArgs(lastPrice: _price, newPrice: value));
_price = value;
}
}
public event EventHandler<PriceChangeEnventArgs>? PriceChangeEvent;
private void OnPriceChange(PriceChangeEnventArgs e) => PriceChangeEvent?.Invoke(this, e);
}
~~~
最后做如下总结:
1. 就像16.2.6事件的内部机制所述,事件的add和remove方法也可以像属性的get和set方法一样自定义其行为,但多数情况下没有必要。
2. 事件是对委托的封装,事件的类型与事件内部的委托的类型是一致的,就像属性的类型与属性内部的字段类型是一致的。
3. EventHandler\<TEventArgs\>实际上泛型委托,因为事件处理程序的结构是一致的,都接收`object sender`和`TEventArgs args`,没必要每次都为这种重复的结构用delegate建立只是委托名称和事件参数不一致的全新委托。另外,在C\#1.0时期,由于没有泛型,为了区分具有不同事件参数的事件,还真要创建无数个结构一致只是事件参数的委托,已经过去20年了,我们仍然能看到dotNET类库中有大部分事件的委托是这么定义的。
4. 事件处理程序(订阅者提供)不一定是方法(Method)还可以是lambda表达式或匿名方法(delegate关键字引导,后来进化成了匿名方法),只要定义和事件类型能对的上就行。事件发布程序提供了一个object类型的sender和一个EventArgs类型的eventArgs,事件处理程序可以不用这些参数,但不得不接收它们。
# 17. 反射、特性和动态编程
## 17.1 反射
### 17.1.1 使用反射调用类成员
现在定义一个类来代表应用程序的命令行,并将其命名为CommandLineInfo。对于这个类来说,最困难的是如何在类中填充启动应用程序时的实际命令行数据。但利用反射,可将命令行选项映射到属性名,并在运行时动态设置属性。
~~~ CSharp
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
namespace ConsoleApp1;
internal static class Program
{
private class CommandLineInfo
{
public bool Help { get; set; }
public string? Out { get; set; }
public ProcessPriorityClass Priority { get; set; } = ProcessPriorityClass.Normal;
}
private static void DisplayHelp()
{
Console.WriteLine("None etc.");
}
public static void Main(string[] args)
{
var commandLine = new CommandLineInfo();
if (!CommandLineHandler.TryParse(args, commandLine, out var errorMessage))
{
Console.WriteLine(errorMessage);
DisplayHelp();
}
if (commandLine.Help) DisplayHelp();
else
{
// Change threadPriority
if (commandLine.Priority != ProcessPriorityClass.Normal)
{
//...
}
else if (commandLine.Priority != ProcessPriorityClass.Normal)
{
//...
}
//...
else
{
//...
}
}
}
}
internal static class CommandLineHandler
{
public static void Prase(string[] args, object commandLine)
{
if (!TryParse(args, commandLine, out var errorMessage))
{
throw new InvalidOperationException();
}
}
public static bool TryParse(string[] args, object commandLine, out string? errorMessage)
{
var success = false;
errorMessage = null;
foreach (var arg in args)
{
if (arg[0] != '/' && arg[0] != '-') continue;
var optionParts = arg.Split(new char[] { ':' }, 2);
var option = optionParts[0].Remove(0, 1);
// 反射:从object对象获取类型信息
var typeOfCommandLine = commandLine.GetType();
const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public;
// 反射:从object对象的类型信息中获取指定的属性信息
var propertyOfCommandLine = typeOfCommandLine.GetProperty(option, bindingFlags);
//处理获取到的属性
if (propertyOfCommandLine is not null)
{
var propertyType = propertyOfCommandLine.PropertyType;
if (propertyType == typeof(bool))
{
propertyOfCommandLine.SetValue(commandLine, true, null);
success = true;
}
else if (propertyType == typeof(string))
{
propertyOfCommandLine.SetValue(commandLine, optionParts[1], null);
success = true;
}
else if (propertyType == typeof(ProcessPriorityClass))
{
try
{
propertyOfCommandLine.SetValue(commandLine,
Enum.Parse(typeof(ProcessPriorityClass), optionParts[1], true), null);
success = true;
}
catch (ArgumentException)
{
success = false;
errorMessage = $@"The option '{optionParts[1]}' is invalid for '{option}'";
}
}
else
{
success = false;
errorMessage = $@"DataType '{propertyType}' on '{commandLine.GetType()} is not supported.'";
}
}
else
{
success = false;
errorMessage = $"{option} is not supported.";
}
}
return success;
}
}
~~~
### 17.1.2 泛型类型上的反射
~~~ CSharp
using System;
using System.Text;
namespace ConsoleApp1;
internal static class Program
{
public static void Main(string[] args)
{
CustomStack<StringBuilder>.Check();
Console.WriteLine("======");
CustomStack<string>.Check();
Console.WriteLine("======");
// 判断类或方法是否支持泛型
var type = typeof(Nullable<>);
Console.WriteLine(type.ContainsGenericParameters);
Console.WriteLine(type.IsGenericType);
type = typeof(DateTime?); // Nullable<DateTime>
Console.WriteLine(type.ContainsGenericParameters);
Console.WriteLine(type.IsGenericType);
// 注意:所有可空类型均是泛型类型
type = typeof(int?); // Nullable<int>
Console.WriteLine(type.IsGenericType);
}
}
internal static class CustomStack<T>
{
public static void Check()
{
// 判断参数类型
var t = typeof(T);
Console.WriteLine(t.FullName + Environment.NewLine);
// 获取该类型支持的接口
var @interface = t.GetInterfaces();
foreach (var item in @interface)
{
Console.WriteLine(item);
}
Console.WriteLine();
// 获取泛型实参
var getGenericArguments = t.GetGenericArguments();
if (getGenericArguments.Length == 0)
{
Console.WriteLine("没有泛型实参");
return;
}
foreach (var item in getGenericArguments)
{
Console.WriteLine(item);
}
}
}
~~~
## 17.2 特性
使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。 将特性与程序实体相关联后,可以在运行时使用反射这项技术查询特性。特性具有以下属性:
- 特性向程序添加元数据。元数据是程序中定义的类型的相关信息。所有.NET 程序集都包含一组指定的元数据,用于描述程序集中定义的类型和类型成员。可以添加自定义特性来指定所需的其他任何信息。
- 可以将一个或多个特性应用于整个程序集、模块或较小的程序元素(如类和属性)。
- 特性可以像方法和属性一样接受自变量。
- 程序可使用反射来检查自己的元数据或其他程序中的元数据。
### 17.2.1 特性使用举例
可以将特性附加到几乎任何声明中,尽管特定特性可能会限制可有效附加到的声明的类型。在 C\#中,通过用方括号 (\[\]) 将特性名称括起来,并置于应用该特性的实体的声明上方以指定特性。例如:
1. 将SerializableAttribute特性用于将具体特征应用于类,表示该类能够被序列化:
~~~ CSharp
[Serializable]
public class SampleClass
{
// Objects of this type can be serialized.
}
~~~
2. System.Runtime.InteropServices.DllImport("xxxxx.dll")可以为方法引入外部程序集,其中user32.dll为动态链接库:
~~~ CSharp
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();
~~~
3. 可以将多个特性附加到函数声明中,下面的示例将命名空间System.Runtime.InteropServices中的In(指示数据应从调用方封送到被调用方)和Out(指示数据应从被调用方封送回调用方)特性添加到了函数声明中。该命名空间提供各种支持COM互操作和平台调用服务的成员,通常用于与非托管代码交互。
~~~ CSharp
void MethodA([In][Out] ref double x) { }
void MethodB([Out][In] ref double x) { }
void MethodC([In, Out] ref double x) { }
~~~
4. 对于给定实体,一些特性可以指定多次。ConditionalAttribute特性的功能与宏`if/endif`相似,只要预处理标识符没有定义,方法就不会执行,但该特性存在限制。如果要修饰的方法返回值类型不为void或包含out修饰的参数,就不能用ConditionalAttribute特性进行限制。
~~~ CSharp
// 只要没有预处理标识符"DEBUG"或者"TEST1",该方法就不会执行
[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
// ...
}
~~~
5. 可以将特性应用到返回值
~~~ CSharp
// ValidatedContract为自定义类型
// 该特性只对Method1方法的返回值生效
[return: ValidatedContract]
private static int Method1()
{
return 0;
}
private class ValidatedContractAttribute : Attribute
{
}
~~~
### 17.2.2 特性参数
许多特性都有参数,可以是位置参数、未命名参数或已命名参数。必须以特定顺序指定任何位置参数,且不能省略。已命名参数是可选参数,可以通过任何顺序指定。首先指定的是位置参数。例如:
1. 下面这三个特性是等同的,SetLastError和ExactSpelling是可选参数且默认值为false
~~~ CSharp
[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]
~~~
2. **特性也是类**,该类派生自Attribute,某些特性具有若干个不同的构造函数以实现不同功能。以ObsoleteAttribute为例,该特性用于向调用者指出一个特定的成员或类型已过时,示例如下:
~~~ CSharp
using System;
namespace ConsoleApp1;
internal static class Program
{
// 调用特性的其中一个有参构造函数,“编译时”将该警告视为错误,然后输出错误,于此同时还输出用户指定的文本
[Obsolete("Obsolete error", true)]
public static void Main(string[] args)
{
InvokeObsoleteMethods();
}
// 调用特性的其中一个有参构造函数,“编译时”在输出警告的同时还输出用户指定的文本
[Obsolete("give somebody a message.")]
public static void InvokeObsoleteMethods()
{
ObsoleteMethod();
}
[Obsolete] // 调用特性的默认构造函数,“编译时”只输出警告
public static void ObsoleteMethod()
{
}
}
~~~
### 17.2.3 特性目标
特性目标是指应用特性的实体。例如,特性可应用于类、特定方法或整个程序集。在默认情况下,特性应用于紧跟在它后面的元素。不过,还可以进行显式标识。例如,可以标识为将特性应用于方法,还是应用于其参数或返回值。若要显式标识特性目标,请使用:`[target: attribute-list]`,下表列出了可能的 `target` 值:

下面的示例展示了如何将特性应用于程序集和模块,将特性应用于C\#中的方法、方法参数和方法返回值的示例将17.2.1和17.2.2
~~~ CSharp
using System;
using System.Reflection;
// 指定当前程序集的名称
[assembly: AssemblyTitle("Production assembly 4")]
// 指定当前程序集中的当前模块的行为与CLS保持一致
[module: CLSCompliant(true)]
~~~
### 17.2.4 System.AttributeUsageAttribute与自定义特性
自定义特性也可以用System.AttributeUsageAttribute特性限定自定义特性的使用范围,示例如下:
~~~ CSharp
// validOn: 此位置参数指定指示的属性可放置到的程序元素。可以使用按位 "或" 运算组合几个值,以获取所需的有效程序元素组合。
// AllowMultiple: 此命名参数指定是否可为给定的程序元素多次指定指定的属性
// Inherited: 此命名参数指定所指示的属性是否可由派生类和重写成员继承
[AttributeUsage(validOn: AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.ReturnValue,
Inherited = false,
AllowMultiple = true)]
public sealed class MyAttribute : Attribute
{
public MyAttribute()
{
//...
}
}
~~~
### 17.2.5 特性的常见用途
- 在 Web 服务中使用WebMethod特性标记方法,以指明方法应可通过SOAP协议进行调用。
- 使用MarshalAsAttribute描述在与本机代码互操作时如何封送方法参数。
- 描述类、方法和接口的COM属性。
- 使用DllImportAttribute类调用非托管代码。
- 从标题、版本、说明或商标方面描述程序集。
- 描述要序列化并暂留类的哪些成员。
- 描述如何为了执行XML序列化在类成员和XML节点之间进行映射。
- 描述的方法的安全要求。
- 指定用于强制实施安全规范的特征。
- 通过实时(JIT)编译器控制优化,这样代码就一直都易于调试。
- 获取方法调用方的相关信息。
## 17.3 动态编程
### 17.3.1 使用dynamic调用反射
~~~ CSharp
using System;
namespace ConsoleApp1;
internal static class Program
{
private static void Main()
{
dynamic data = "Hello! My Name is Montoya.";
Console.WriteLine(data);
data = 3.1546m;
Console.WriteLine(data);
// 实际上Member成员在data中不存在但仍然能编译通过
data.Member();
}
}
~~~
本例子不是用显式的代码判断对象类型,查找特定MemberInfo实例并调用它。相反,data声明为dynamic类型并直接在它上面调用方法。“编译时”不会检查指定成员是否可用,甚至不会检查dynamic的基础类型是什么。所以,只要语法有效,“编译时”就可以发出任何调用,至于被调用的成员是否存在,这并不是“编译时”要关心的事。
### 17.3.2 dynamic的原则和行为
#### dynamic是通知编译时生成代码的指令
dynamic涉及一个机制,当“运行时”遇到一个dynamic调用时,它可以将请求编译成CIL,再调用新编译的调用。
dynamic实际上是一个System.Object。事实上,如果没有任何调用,dynamic类型的声明和System.Object没有区别,但一旦调用它的成员,区别就变得明显了。
为调用成员,编译器要先声明`System.Runtime.CompilerService.CallSite<T>`类型的一个变量。T视成员签名的变化而变化。但即使简单如ToString的调用也需要实例化一个`CallSite<Func<Callsite,object,string>>` 类型。另外还会动态定义一个方法,该方法可通过参数Callsite site, object dynamicTarget和string result进行调用。其中site是调用点本身,dynamicTarget是要在上面调用方法的object对象,而result是ToString方法调用的基础类型的返回值。注意,不是直接实例化`CallSite<Func<Callsite,object,string>>`,而是通过一个Create工厂方法来实例化它。这个方法接收一个`Microsoft.CSharp.RuntimeBinder.CSharpConvertBinder`类型的参数。在得到`CallSite<T>`的一个实例后,最后一步是调用`CallSite<T>.Target()`来调用实际成员。
在代码首次执行时,框架会在幕后用“反射”来查找成员,并验证签名是否匹配。然后,“运行时”生成一个表达树,它代表由调用点定义的动态表达式。表达式树编译好后,就得到了和本来应由编译器生成的结果相似的CIL,这些CIL代码在调用点缓存下来,并通过一个委托调用来实际地触发调用。由于CIL已经缓存于调用点,所以后续不会再产生“反射”和编译的开销。
也就是说,将类型指定成dynamic后,相当于从概念上“包装”了原始类型,这样便不会发生“编译时”验证。此外,在“运行时”调配用一个成员时,“包装器”会解释调用,并相应地调度或拒绝它。如果在dynamic对象上调用GetType方法,该方法并不会返回dynamic,而是返回dynamic实例的基础类型。
1. 任何能转换成object的类型都能转换成dynamic
2. 从dynamic到一个替代类型的成功转换需要依赖基础类型的支持
3. dynamic类型的基础类型在每次赋值时都有可能改变
4. 验证基础类型上是否存在指定签名要推迟到运行时才能进行,以17.3.1中的`data.Member()`方法为例,这一方法在data对象中并不存在,但编译器不会验证dynamic类型的data是否真的存在这样一个成员方法,这个验证会推迟到“运行时”来做。
5. 任何dynamic成员调用都返回dynamic对象,调用dynamic对象的任何成员都将返回一个dynamic对象,但在代码执行时在dynamic对象会被“运行时”转译为具体类型的对象
6. dynamic对象中的指定成员在运行时不存在会抛出异常,这个异常是`Microsoft.CSharp.RuntimeBinder.RuntimeBinderException`。
7.dynamic类型的变量不支持扩展方法,只有在实现类型上才能调用扩展方法,如果想为dynamic类型的变量调用扩展方法需要将其转化为具体类型
8. dynamic类型仍然是System.Object类型,dynamic类型的动态行为只在“编译时”出现,其他行为与System.Object类型别无二致,至少看起来如此。
### 17.3.3 实现自定义动态对象
定义自定义动态类型的关键是实现System.Dynamic.IdynamicMetaObjectProvider接口,但该接口不必从头实现,实际使用中应该从System.Dynamic.DynamicObject派生出自定义的动态类型。示例如下:
~~~ CSharp
internal class DynamicXml : DynamicObject
{
private XElement Element { get; set; }
public DynamicXml(XElement element)
{
Element = element;
}
public static DynamicXml Parse(string text)
{
return new DynamicXml(XElement.Parse(text));
}
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
result = null;
var firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant is null) return false;
if (firstDescendant.Descendants().Any()) result = new DynamicXml(firstDescendant);
else result = firstDescendant.Value;
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object? value)
{
var firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant is null) return false;
if (value?.GetType() == typeof(XElement)) firstDescendant.ReplaceWith(value);
else firstDescendant.Value = value?.ToString() ?? string.Empty;
return true;
}
}
~~~
读书笔记-C#8.0本质论-03的更多相关文章
- 《C#本质论》读书笔记(18)多线程处理
.NET Framework 4.0 看(本质论第3版) .NET Framework 4.5 看(本质论第4版) .NET 4.0为多线程引入了两组新API:TPL(Task Parallel Li ...
- 【英语魔法俱乐部——读书笔记】 0 序&前沿
[英语魔法俱乐部——读书笔记] 0 序&前沿 0.1 以编者自身的经历引入“不求甚解,以看完为目的”阅读方式,即所谓“泛读”.找到适合自己的文章开始“由浅入深”的阅读,在阅读过程中就会见到 ...
- 《玩转Django2.0》读书笔记-探究视图
<玩转Django2.0>读书笔记-探究视图 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 视图(View)是Django的MTV架构模式的V部分,主要负责处理用户请求 ...
- 《玩转Django2.0》读书笔记-编写URL规则
<玩转Django2.0>读书笔记-编写URL规则 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. URL(Uniform Resource Locator,统一资源定位 ...
- 《玩转Django2.0》读书笔记-Django配置信息
<玩转Django2.0>读书笔记-Django配置信息 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 项目配置是根据实际开发需求从而对整个Web框架编写相应配置信息. ...
- 《玩转Django2.0》读书笔记-Django建站基础
<玩转Django2.0>读书笔记-Django建站基础 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.网站的定义及组成 网站(Website)是指在因特网上根据一 ...
- 《C# 6.0 本质论》 阅读笔记
<C# 6.0 本质论> 阅读笔记 阅读笔记不是讲述这本书的内容,只是提取了其中一部分我认为比较重要或者还没有掌握的知识,所以如果有错误或者模糊之处,请指正,谢谢! 对于C# 6.0才 ...
- 《鸟哥的Linux私房菜》读书笔记--第0章 计算机概论 硬件部分
一个下午看了不少硬件层面的知识,看得太多太快容易忘记.于是在博客上写下读书笔记. 有关硬件 个人计算机架构&接口设备 主板芯片组为“南北桥”的统称,南北桥用于控制所有组件之间的通信. 北桥连接 ...
- 《C# 6.0 本质论》 - 学习笔记
<C# 6.0 本质论> ========== ========== ==========[作者] (美) Mark Michaelis (美) Eric Lippert[译者] (中) ...
- 读书笔记之第五回深入浅出关键字---把new说透
第五回深入浅出关键字---把new说透 ------你必须知道的.net读书笔记 new一个class时,new完成了以下两个方面的内容:一是调用newobj命令来为实例在托管堆中分配内存:二是调用 ...
随机推荐
- compileSdkVersion, minSdkVersion 和 targetSdkVersion,傻傻分不清楚【转】
原文 https://blog.csdn.net/gaolh89/article/details/79809034 在Android Studio项目的app/build.gradle中,我们可以看到 ...
- C++性能优化——能用array就不要用unordered_map作为查询表
unordered_map需要哈希值计算和表查询的开销,当key值为整数且连续,直接用数组作为查询表具有更高的效率. #include <iostream> #include <ch ...
- Angular Material 18+ 高级教程 – CDK Layout の Breakpoints
前言 CDK Layout 主要是用于处理 Breakpoints,它底层是依靠 window.matchMedia 来实现的. Material Design 2 & 3 Breakpoin ...
- CSS – 管理
前言 CSS 有好几种写法. 它们最终出来的效果是一样的, 区别只是在你如何 "写" 和 "读" 或者说开发和维护. 这已经不是如何"实现" ...
- Identity – 安全基础知识
前言 一旦涉及到用户, 那么安全就上一个层次了. 这篇主要是说说一些安全的基础 用户密码保存 网络上有太多资料说这些基础了, 我就不拉过来了. 大致记入一些重点就好了. - 为什么不可以明文保存 因为 ...
- POJ-2229 Sumsets(基础dp)
Farmer John commanded his cows to search for different sets of numbers that sum to a given number. T ...
- [TK] Tourist Attractions
题目描述 给出一张有 \(n\) 个点 \(m\) 条边的无向图,每条边有边权. 你需要找一条从 \(1\) 到 \(n\) 的最短路径,并且这条路径在满足给出的 \(g\) 个限制的情况下可以在所有 ...
- LeetCode 1819. 序列中不同最大公约数的数目(数论)
题目描述 给你一个由正整数组成的数组 nums . 数字序列的 最大公约数 定义为序列中所有整数的共有约数中的最大整数. 例如,序列 [4,6,16] 的最大公约数是 2 . 数组的一个 子序列 本质 ...
- Android 基于 Choreographer 的渲染机制详解
本文介绍了 App 开发者不经常接触到但是在 Android Framework 渲染链路中非常重要的一个类 Choreographer.包括 Choreographer 的引入背景.Choreogr ...
- window使用VNC远程ubuntu16.04
首先保证在同一局域网下 一.设置Ubuntu 16.04 允许进行远程控制 首先在ubuntu下找到下图图标 将[允许其他人查看您的桌面]这一项勾上,然后在安全那项,勾选[要求远程用户输入此密码],并 ...