零、第一次优化
虽然我们可以通过指定不同的类型参数来实现泛型类的复用,但是在某些情况下通用就意味着我们无法利用具体类型的优势。针对这一点 C# 允许在发现类型参数所表示的对象具有更多的功能时编写更具体的代码。这一点是利用了泛型依据对象的编译器类型来进行实例化的这一特点,如果我们在开发时没有想到这一点就有很大的可能降低程序的性能。为了能讲清楚这一点,我们先来看一段代码,这段代码要做的是倒序输出序列中的内容。
public sealed class DemoEnumerable<T> : IEnumerable<T> { private class DemoEnumerator:IEnumerator<T> { int index; IList<T> collection; public DemoEnumerator(IList<T> source) { collection = source; index=collection.Count; } public T Current=>collection[index]; public void Dispose() { // more code } object System.Collections.IEnumerator.Current=>this.Current; public bool MoveNext() =>--index>=0; public void Reset()=>index=collection.Count; } Ienumerable<T> srcSequence; IList<T> orgSequence; public DemoEnumerable(IEnumerable<T> sequence) { sreSequence = sequence; } public IEnumerator<T> GetEnumerator() { if(orgSequence == null) { orgSequence = new List<T>(); foreach (T item in sreSequence) { orgSequence.Add(item); } } return new DemoEnumerator(orgSequence); } }
上述代码中只针对 DemoEnumerable 构造函数做了限制,要求它的参数必须支持 IEnumerable ,因此我们要实现序列中元素的倒叙访问就必须采用 GetEnumerator 种的方式。首次调用这个方法时会把输入的序列访问一遍,然后让嵌套类可以在这个列表上反向访问元素。但是这里存在一个问题,大部分序列都支持随机访问,那么如果输入的序列支持 IList 这种写法就是多此一举,因为这种写法会创建出一份和源序列一摸一样的序列。要解决这个问题我们只需要修改一下 DemoEnumerable 的构造函数然后增加一个参数为 IList 类型的构造函数即可:
public DemoEnumerable(IEnumerable<T> sequence) { sreSequence = sequence; orgSequence = sequence as IList<T>; } public DemoEnumerable(IList<T> sequence) { sreSequence = sequence; orgSequence = sequence; }
Tip:这里之所以要修改源构造函数并增加一个参数类型为 IList 的构造函数,是因为只有参数的编译器类型是 IList 的时候新的构造函数才会生效。有时尽管参数实现了 IList 但是它的编译期类型仍然是 IEnumerable,因此我们必须提供新的构造函数的同时修改旧的构造函数。
一、第二次优化
上述代码基本上囊括了大部分情况,但有时我们还会遇到一些集合只实现了 ICollection 而没有实现 IList 的情况,这种情况下我们代码中的 GetEnumerator 方法性能就不是很高了,因为它可以利用 Count 属性将 IList 的大小确定下来。因此我们需要修改一下代码。
public IEnumerator<T> GetEnumerator() { if(orgSequence == null) { if(orgSequence is ICollection<T>) { ICollection<T> src = orgSequence as ICollection<T>; orgSequence = new List<T>(src.Count); } else { orgSequence = new List<T>(); } foreach (T item in sreSequence) { orgSequence.Add(item); } } return new DemoEnumerator(orgSequence); }
二、终极优化
到这里我们现在基本上覆盖了大部分的情况,但是我们还需要注意的是前面代码中 DemoEnumerable 都是执行的运行期测试,测试的是参数在运行期的状态,因此为了确定参数所表示的对象是否具有一些功能,我们的程序必须消耗一定的时间去判断,在绝大多数情况下这种做法消耗的性能不是很多。但是当 T 是 string 时性能就会大打折扣,因为我们的代码本身并没有实现 IList ,因此我们需要在泛型类中编写更具体的代码才能解决这个问题,我们需要在 DemoEnumerable 类中加入如下的嵌套类。
private class DemoStringEnumerator:IEnumerator<char> { int index; string collection; public DemoEnumerator(string source) { collection = source; index=source.Length; } public char Current=>collection[index]; public void Dispose() { // more code } object System.Collections.IEnumerator.Current=>this.Current; public bool MoveNext() =>--index>=0; public void Reset()=>index=collection.Length; }
下面我们还要修改 GetEnumerator 的代码,这样 DemoEnumerable 就可以正常使用我们定义的 DemoStringEnumerator 了。
public IEnumerator<T> GetEnumerator() { if(sreSequence is string) { return new DemoStringEnumerator(sreSequence as string); } if(orgSequence == null) { if(orgSequence is ICollection<T>) { ICollection<T> src = orgSequence as ICollection<T>; orgSequence = new List<T>(src.Count); } else { orgSequence = new List<T>(); } foreach (T item in sreSequence) { orgSequence.Add(item); } } return new DemoEnumerator(orgSequence); }
三、总结
我们在开发中不仅可以对泛型增加少量合理的限制,还可以在它所表示的类型具备很多功能时提供更好的实现方式,但是我们需要在算法的效率和泛型的复用程度之间找到平衡点。