.NET类处理系统中触发的事件,有两种方法:使用事件处理函数或覆写基类中的虚方法。在派生类中,只应该覆写虚方法,而事件处理函数则应该仅用在不相关对象的交互中;
如果事件处理函数中抛出了异常,那么事件处理函数链上的后续函数则无法得到调用(委托的多播,可参考条目24);
从效率角度考虑,覆写比事件处理函数更快。事件构建于多播委托之上,任何的事件源都可以支持多个侦听者。事件处理机制与处理器相比将执行更多的工作,如坚持事件、判断是否有处理函数挂接于其上。如有责必须便利整个调用列表,执行其中的每个方法。运行时判断是否存在事件处理函数并遍历调用每一个要比调用一个虚函数花费更多的时间;
覆写虚方法代码更容易维护,事件处理函数需要同时维护事件处理函数本身以及挂接该事件的代码;
使用事件处理函数而非覆写基类方法的原因:
应使用泛型版本的 IComparable<T> 。非泛型的 IComparable 接口只包含一个方法 CompareTo() ,遵循C类库 strcmp 函数的传统。.NET中很多旧的API依然使用 IComparable 接口,它接受的参数类型时System.Object,因此必须在比较时检查参数的类型,传入的参数必须装箱拆箱。
IComparable<T>
IComparable
CompareTo()
strcmp
使用非泛型版本 IComparable 的理由:
实现 IComparable 时,请使用显示接口实现,并提供一个强类型版本的重载。强类型的重载不仅提高性能,还能够降低使用者误用 .CompareTo() 方法的可能(这里的性能提升并不能在.NET的 Sort 方法中体现出来,因为 Sort 方法通过接口指针访问 CompareTo() 方法);
.CompareTo()
Sort
下边是一个同时实现了 IComparable<T> 和 IComparable ,并且重载了标准关系操作符的类的示例:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
public struct Customer : IComparable<Customer>, IComparable{ private readonly string name; public Customer(string name) { this.name = name; } #region IComparable<Customer> Members public int CompareTo(Customer other) { return name.CompareTo(other.name); } #endregion #region IComparable Members int IComparable.CompareTo(object obj) { if (!(obj is Customer)) throw new ArgumentException( "Argument is not a Customer", "obj"); Customer otherCustomer = (Customer)obj; return this.CompareTo(otherCustomer); } #endregion // Relational Operators. public static bool operator <(Customer left, Customer right) { return left.CompareTo(right) < 0; } public static bool operator <=(Customer left, Customer right) { return left.CompareTo(right) <= 0; } public static bool operator >(Customer left, Customer right) { return left.CompareTo(right) > 0; } public static bool operator >=(Customer left, Customer right) { return left.CompareTo(right) >= 0; }}
自定义的排序,可以通过 Comparison<T> 委托来实现,通常做法是在类型中创建一个静态属性,如:
Comparison<T>
12345678
public static Comparison<Customer> CompareByReview{ get { return (left,right) => left.revenue.CompareTo(right.revenue); }}
更早的类库中会通过 IComparer 来获取此类型的比较功能,下边是示例代码:
IComparer
12345678910111213141516171819202122232425262728
public struct Customer : IComparable<Customer>, IComparable{ // ... 略去重复代码 private static RevenueComparer revComp = null; // return an object that implements IComparer // use lazy evaluation to create just one. public static IComparer<Customer> RevenueCompare { get { if (revComp == null) revComp = new RevenueComparer(); return revComp; } } // Class to compare customers by revenue. // This is always used via the interface pointer, // so only provide the interface override. private class RevenueComparer : IComparer<Customer> #region IComparer<Customer> Members int IComparer<Customer>.Compare(Customer left, { return left.revenue.CompareTo(right.revenue); } #endregion
一旦某个类型实现了 ICloneable 接口,那么其派生类也必须同样实现 ICloneable ,该类型的所有成员也必须支持 ICloneable ,或者提供其它方式来创建一个副本;
ICloneable
所有仅包含内建类型(int等)的值类型(struct)都不需要支持 ICloneable ,赋值语句就能完整地复制结构中所有的值,且比 clone() 更加高效;
clone()
一种基类与衍生类的复制的实现可以参考以下的方式,基类不实现 ICloneable ,而是提供一个受保护的复制构造函数,让衍生类可以复制属于基类中的那一部分,叶子类则应该都是密封的,根据需要实现 ICloneable :
123456789101112131415161718192021222324252627282930313233343536
class BaseType{ private string label; private int[] values; protected BaseType() { label = "class name"; values = new int[10]; } // Used by derived values to clone protected BaseType(BaseType right) { label = right.label; values = right.values.Clone() as int[]; }}sealed class Derived : BaseType, ICloneable{ private double[] dValues = new double[10]; public Derived() { dValues = new double[10]; } // Construct a copy // using the base class copy ctor private Derived(Derived right) : base(right) { dValues = right.dValues.Clone() as double[]; } public object Clone() { Derived rVal = new Derived(this); return rVal; }}
new 修饰符的滥用会造成对象调用方法的二义性;
new
new 修饰符只是用来解决升级基类所造成的基类方法和衍生类方法冲突的问题;
与条目33类似,重载基类中的方法会使使用者产生疑惑,在使用时可能需要额外的强制类型转换才能得到希望的结果;
首先是一组示例,获取源数据中小于150的数字的阶乘:
1234567891011121314151617
// 使用方法调用语法来查询:var nums = data.Where(m => m < 150). Select(n => Factorial(n));// 增加`AsParallel()`使其成为并行查询:var numsParallel = data.AsParallel(). Where(m => m < 150).Select(n => Factorial(n));// 使用查询语法来查询:var nums = from n in data where n < 150 select Factorial(n);// 并行版的查询语法:var numsParallel = from n in data.AsParallel() where n < 150 select Factorial(n);
AsParallel() 的后续操作会以多线程形式在多个核中执行,其返回的是一个 IParallelEnumerable 而不是 IEnumerable ;
AsParallel()
IParallelEnumerable
IEnumerable
并行查询始于一个分区(partitioning)操作。PLINQ的分区操作有4种策略:
除了分区算法之外,PLINQ还使用了3种不同的算法来让任务并行执行:
PLINQ会尝试为你编写的查询创建出最好的实现,用最少的工作和最短的时间来生成你需要的结果。需要对底层的不同实现有一定的理解,才能尽量确保发挥底层技术的最大功效;
对于并行算法,使用多处理器带来的程序性能提升受限于程序的顺序执行部分(Amdahl定律),可以使用 AsOrdered() 或 AsUnordered() 来告知PLINQ结果序列中的顺序是否重要;
AsOrdered()
AsUnordered()
有时算法依赖于一些副作用(side effects)而无法并行执行,此时可以使用 ParallelEnumerable.AsSequential() 扩展方法来强制顺序执行;
ParallelEnumerable.AsSequential()
ParallelEnumerable还包含了一些方法,用于控制PLINQ如何执行并行查询:
WithExecutionMode()
WithDegreeOfParallelism()
WithMergeOptions()
并行任务库不仅可以优化CPU密集操作,也可以在I/O密集场景中发挥作用。可以使用方法调用或LINQ查询语法来使用并行执行模型,通常I/O密集的并行执行行为会比CPU核数更多的线程,因为I/O密集的线程将花费更多的时间来等待外部的事件;
一个简单的示例:
12345
foreach (var url in urls){ var result = new WebClient().DownloadData(url); UseResult(result);}
其中 DownloadData 将发出一个同步的web请求并等待接收到所有的数据。可以使用并行的for循环将其改为并行执行:
DownloadData
Parallel.ForEach(urls, url =>{ var result = new WebClient().DownloadData(url); UseResult(result);});
也可以使用PLINQ和查询语法实现同样的结果:
123
var results = from url in urls.AsParallel() select new WebClient().DownloadData(url);results.ForAll(result => UseResult(result));
PLINQ执行方式将使用固定数目的线程,而 Parallel.ForEach 将调整线程的数量来增加吞吐量;
Parallel.ForEach
上述代码虽然使用了多个线程来并行执行任务,但程序的其它部分仍需等待所有的web请求结束才能继续其它的工作。并行任务库还支持这样的机制:执行一系列的I/O密集操作,同时对其结果进行处理,例如:
1234
urls.RunAsync( url => startDownload(url), task => finishDownload(task.AsyncState.ToString(), task.Result));
其中涉及到的函数的定义如下:
1234567891011121314151617181920212223242526272829303132333435
public static void RunAsync<T, TResult>( this IEnumerable<T> taskParms, Func<T, Task<TResult>> taskStarter, Action<Task<TResult>> taskFinisher){ taskParms.Select(parm => taskStarter(parm)). AsParallel(). ForAll(t => t.ContinueWith(t2 => taskFinisher(t2)));}private static void finishDownload(string url, byte[] bytes){ Console.WriteLine("Read {0} bytes from {1}", bytes.Length, url);}private static Task<byte[]> startDownload(string url){ var tcs = new TaskCompletionSource<byte[]>(url); var wc = new WebClient(); wc.DownloadDataCompleted += (sender, e) => { if (e.UserState == tcs) { if (e.Cancelled) tcs.TrySetCanceled(); else if (e.Error != null) tcs.TrySetException(e.Error); else tcs.TrySetResult(e.Result); } }; wc.DownloadDataAsync(new Uri(url), tcs); return tcs.Task;}
使用并行任务库和PLINQ,和可支持多种异步模式的Task类,配合I/O密集操作或者混合了I/O和CPU密集的操作来执行。
注意事项:
并行操作使用一个新的 AggregateException 类型来处理并行操作中的异常。它将作为一个容器,在其 InnerExceptions 属性中包含了并行操作中生成的所有异常。通常有两种做法来处理这些异常:
AggregateException
InnerExceptions
第一种做法,需要注意 InnerExceptions 中也可能会包含 AggregateException 对象,对于可能出现的异常必须区分对待,可以处理和恢复的直接处理,其它的再向外层抛出。上一条目中的 RunAsync() 方法若考虑异常则可以写为:
RunAsync()
try{ urls.RunAsync( url => startDownload(url), task => finishDownload(task.AsyncState.ToString(), task.Result));}catch (AggregateException problems){ var handlers = new Dictionary<Type, Action<Exception>>(); handlers.Add(typeof(WebException), ex => Console.WriteLine(ex.Message)); if (!HandleAggregateError(problems, handlers)) throw;}private static bool HandleAggregateError( AggregateException aggregate, Dictionary<Type, Action<Exception>> exceptionHandlers){ foreach (var exception in aggregate.InnerExceptions) if (exception is AggregateException) return HandleAggregateError( exception as AggregateException, exceptionHandlers); else if (exceptionHandlers.ContainsKey( exception.GetType())) { exceptionHandlers[exception.GetType()] (exception); } else return false; return true; }
第二种做法,为保证没有异常离开线程,必须修改执行后台任务的代码,即是说使用 TaskCompletionSource<> 时永远不要调用 TrySetException() 方法,而是必须保证每个任务都会调用到 TrySetResult() ,表示任务完成:
TaskCompletionSource<>
TrySetException()
TrySetResult()
123456789101112131415161718192021
private static Task<byte[]> startDownload(string url){ var tcs = new TaskCompletionSource<byte[]>(url); var wc = new WebClient(); wc.DownloadDataCompleted += (sender, e) => { if (e.UserState == tcs) { if (e.Cancelled) tcs.TrySetCanceled(); else if (e.Error != null) { if (e.Error is WebException) tcs.TrySetResult(new byte[0]); else tcs.TrySetResult(e.Result); } else tcs.TrySetException(e.Error); } }; wc.DownloadDataAsync(new Uri(url), tcs); return tcs.Task;}
注意,这里的异常仍是分为两类。当出现异常是 WebException 时,则认为是读取到0字节的数据(在后台线程中可以处理),而其它的异常则还是需要抛出 AggregateException (致命错误,必须外部处理)。
WebException
《C#高效编程 改进C#代码的50个行之有效的办法》
https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/parallel-linq-plinq