专栏首页喵叔's 专栏通过运行期类型检查实现泛型算法

通过运行期类型检查实现泛型算法

Tip:本文首发于喵叔的 CSDN 博客,转载于喵叔的 InfoQ 博客,本人未授权任何网站、公众号以及其他任何形式的转载。发布不等于免费、开源不等于无所顾忌,请遵守职业道德。

零、第一次优化

虽然我们可以通过指定不同的类型参数来实现泛型类的复用,但是在某些情况下通用就意味着我们无法利用具体类型的优势。针对这一点 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);
}

三、总结

我们在开发中不仅可以对泛型增加少量合理的限制,还可以在它所表示的类型具备很多功能时提供更好的实现方式,但是我们需要在算法的效率和泛型的复用程度之间找到平衡点。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Entity Framework Core 实现全局查询过滤

    微软在 Entity Framework Core 2+ 中引入了全局查询过滤器,简化了构建多租户应用程序和实体软删除的复杂度。这篇文章我将通过代码的形式对全局...

    喵叔
  • Entity Framework 小知识(二)

    接下来使用 DbConfigurationType 属性在上下文类中设置基于代码的配置类:

    喵叔
  • Entity Framework 小知识(五)

    在 多对多关系映射 中关联表是EF自动生成的。但有时候我们需要显示定义关联表。我们可以按照如下步骤定义(继续使用多对多关系映射这篇文章饿代码):

    喵叔
  • 【Java】基础篇-LinkedList

    说到 LinkedList,那么我们大家的第一想法就是 链表,是插入删除快,随机访问慢,今天我们就来一探究竟,究竟内部的它是什么构造导致的问题,我们是否可以在使...

    haoming1100
  • Java I/O 模型的演进

    什么是同步?什么是异步?阻塞和非阻塞又有什么区别?本文先从 Unix 的 I/O 模型讲起,介绍了5种常见的 I/O 模型。而后再引出 Java 的 I/O 模...

    九州暮云
  • JAVA 快速排序算法

    zcqshine
  • 一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移?

    先抛一个问题给我聪明的读者,如果你们使用微服务SpringCloud-Netflix进行业务开发,那么线上注册中心肯定也是用了集群部署,问题来了:

    一枝花算不算浪漫
  • Builder设计模式构建整个应用的头部(NavigationBar)

    开发中基本上每个APP都会有自己的头部,如何去写这个头部呢?一部分人会在xml布局中直接写,一部分人会调用系统的ToolBar自定义布局,这两种方式都可以去实现...

    CatEatFish
  • Jackson常用注解详解1 初级2 中级

    JavaEdge
  • 电商推荐那点事

    石晓文

扫码关注云+社区

领取腾讯云代金券