• C# -- 泛型(3)


    简介:

      前两篇文章讲了关于泛型的一些基础,下面笔者通过这篇文章来给刚刚接触泛型的朋友介绍一下

      <1>.原理性的东西----” 泛型的协变和逆变 “

      <2>.以及常用的接口----” IEnumerable 及其泛型版的IEnumerable<out T> “

    ------------------------------------------------------------------------------------------------------------------------------------------------------------

     

     

    <泛型的协变逆变|泛型修饰符‘out’与‘in’>

    |首先这2个拗口的名词先不用去管它,先知道协变和逆变主要是用在泛型的接口委托上就可以了,下面我们通过一个例子来看看:

    |在这之前我们插点别的东西,我们知道接口是可以体现多态的,当然接口体现的多态注重功能上的多态,这和抽象类不同,抽象类更注重的是建立在血缘关系上的多态

    知道接口是可以体现多态的之后,我们来看看一个相关的例子--

    鸟和飞机都会飞,把飞定义成一个借口,在定义2个类

        public interface IFlyable
        {
            void fly();
        }
        class Bird:IFlyable
        {
            public void fly()
            {
                Console.WriteLine("鸟儿飞!");
            }
        }
        class Plane:IFlyable
        {
            public void fly()
            {
                Console.WriteLine("飞机飞!");
            }
        }

     

    下面看看接口体现的多态性:

                IFlyable ifly; 
                
                ifly = new Bird();
                ifly.fly();
    
                ifly = new Plane();
                ifly.fly();

    运行结果:

    鸟儿飞!

    飞机飞!

     

    了解了接口的多态性后我们再来看一个例子:

    这里定义了2个类 Animal 和 Cat (Cat继承了Animal)

        public class Animal
        {
        }
    
        public class Cat:Animal
        {
        }

     

    继续往下看:

    Cat cat = new Cat();

     

    下面这句代码,cat向animal转,子类向父类转换,这时cat会隐式转换为animal 我们说“儿子像父亲” 这是完全可以理解的

    Animal animal = cat;

     

    但是 说”父亲像儿子“ 这是说不过去的 ,但是有的时候如果儿子坑爹强制转换了一下还是可以的

    cat = (Cat)animal;

     

     

    (协变)

                List<Cat> catArray = new List<Cat>();
                List<Animal> animalArray = catArray;

     

    如果是上面说的类,这样写是可以的,但是这里是会报错的  如图

    继续往下看 这样写却可以

                IEnumerable<Cat> lCat = new List<Cat>();
                IEnumerable<Animal> lAnimal = lCat;

     

    对 IEnumerable<Cat> 转到定义 如图 我们发现这里多了一个 “out” 关键字

    概念引入

    1.对于泛型类型参数,out 关键字指定该类型参数是协变的。 可以在泛型接口和委托中使用 out 关键字。“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。

    --对于 “协变” 笔者是这样理解的就是”说的通变化“ 就像 “儿子像父亲一样”(假定父亲派生程度0那么儿子的派生程度就是1了,所以父亲可以使用派生程度更大的儿子)

    协变与多态性类似,因此它看起来非常自然。

     

    (逆变)

    我们知道IComparable<T>接口中,T的修饰符是‘in’,下面我们修改一下上面的代码演示一下 

     

        class Cat : Animal, IComparable<Cat>
        {
            //仅演示
            public int CompareTo(Cat other)
            {
                return 1;
            }
        }
    
        class Animal : IComparable<Animal>
        {
            //仅演示
            public int CompareTo(Animal other)
            {
                return 1;
            }
        }

     

     

    这里Cat和Animal都实现了IComparable<T>接口,然后我们这样写

                IComparable<Cat> ICat = new Cat();
                IComparable<Animal> IAnimal = new Animal();
                ICat = IAnimal;

     

    代码中ICat(高派生程度)使用 IAnimal(低派生程度) “父亲像儿子” 和上面的例子完全相反。

     

    概念引入:

    2.对于泛型类型参数,in 关键字指定该类型参数是逆变的。 可以在泛型接口和委托中使用 in 关键字。“逆变”则是指能够使用派生程度更小的类型。

    --对于 “逆变” 笔者的理解则是 “坑爹儿子” 反过来硬说 “父亲像儿子” 这是 “说不过去的” 只是利用了强硬的手段

     

     

    在了解了上面的内容后,我们来看看“out” 与 “in” 关键字的特性

    IEnumerable<T>接口的IEnumerator<T> GetEnumerator()方法返回了一个迭代器 ,不难发现T如果用 out 标记,则T代表了输出,也就说只能作为结果返回。

    IComparable<T>接口的CompareTo(T other)方法传入了一个T类型的Other参数,不难发现T如果用 in 标记,则T代表了输入,也就是它只能作为参数传入。

    下面我们演示一个例子

    将动物会叫这功能,定义成一个泛型借口用 out 修饰

    这里会出现一个错误

    把第二个带参数的setSound方法,去掉后编译可以正常通过

    下面我们把 out 改成 in

    这里会出现一个错误

    把第一个setSound方法,去掉后编译可以正常通过,或者把第一个方法的返回值,改成其它非T类型,编译也可通过

    这个演示充分说明了:out 修饰 T 则 T只能作为结果输出而不能作为参数  ; in 修饰 T 则 T只能作为参数而不能作为结果返回;

     

    ------------------------------------------------------------------------------------------------------------------------------------------------------------

     

     

    <IEnumerable接口及其泛型版>

     为什么要用IEnumerable接口? 下面我们通过一个例子看看:

        //定义Person类
        public class Person
        {
            public Person(string _name)
            {
                this.name = _name;
            }
    
            public string name;
        }
    
        //定义People类
        public class People
        {
            private Person[] _people;
    
            public People(Person[] pArray)
            {
                //实例化数组 用于存Person实例
                _people = new Person[pArray.Length];
    
                for (int i = 0; i < pArray.Length; i++)
                {
                    _people[i] = pArray[i];
                }
            }
        }

    上面的代码我们定义了一个 Person 类和一个 People 类,显然 People是用来存放多个Person实例的集合,下面我们尝试用 Foreeach 遍历集合的每个元素 输出:

            static void Main(string[] args)
            {
                Person[] personArray = new Person[3]{
                new Person("Keiling1"),
                new Person("Keiling2"),
                new Person("Keiling3"),
                };
    
                People people = new People(personArray);
                foreach (Person item in people)
                {
                    Console.WriteLine(item.name);
                }
            }

    这里编译不能通过,出现了一个错误

    GetEnumerator:是IEnumerable接口中的一个方法,它返回一个 IEnumerator(迭代器),如下图 

    IEnumerator内部规定了,实现一个迭代器的所有基本方法,包括 如下图

    为了在foreach中使用 People的实例, 我们给People实现IEnumerable接口,代码如下:

        public class People:IEnumerable
        {
            private Person[] _people;
    
            public People(Person[] pArray)
            {
                //实例化数组 用于存Person实例
                _people = new Person[pArray.Length];
    
                for (int i = 0; i < pArray.Length; i++)
                {
                    _people[i] = pArray[i];
                }
            }
    
    ////IEnumerable和IEnumerator通过IEnumerable的GetEnumerator()方法建立了连接,可以通过IEnumerable的GetEnumerator()得到IEnumerator对象。
            IEnumerator IEnumerable.GetEnumerator()
            {
                return (IEnumerator)GetEnumerator();
            }
    
            public PeopleEnum GetEnumerator()
            {
                return new PeopleEnum(_people);
            }
        }
    
        public class PeopleEnum:IEnumerator
        {
            public Person[] _people;
    
            public PeopleEnum(Person [] pArray)
            {
                _people = pArray;
            }
            //游标
            int position = -1;
    
            //是否可以往下 移
            public bool MoveNext()
            {
                position++;
                return (position < _people.Length);
            }
    
            //集合的所有元素取完了之后 重置position
            public void Reset()
            {
                position = -1;
            }
    
            //实现 IEnumerator的 Current方法 返回当前所指的Person对象
            object IEnumerator.Current
            {
                get
                {
                    return Current;
                }
            }
    
            //Current是返回Person类实例的只读方法
            public Person Current
            {
                get
                {
                    try
                    {
                        return _people[position];
                    }
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
            }
        }

    测试运行:

            static void Main(string[] args)
            {
                Person[] personArray = new Person[3]{
                new Person("Keiling1"),
                new Person("Keiling2"),
                new Person("Keiling3"),
                };
    
                People people = new People(personArray);
                foreach (Person item in people)
                {
                    Console.WriteLine(item.name);
                }
            }

    结果:

    总结:

    1.一个集合要支持foreach方式的遍历,必须实现IEnumerable接口,描述这类实现了该接口的对象,我们叫它 ‘序列’。

    比如 List<T> 支持 foreach 遍历 是因为它实现了IEnumerable接口和其泛型版,如图--

     

     

    2. IEnumerator对象具体实现了迭代器(通过MoveNext(),Reset(),Current)。

     

    3. 从这两个接口的用词选择上,也可以看出其不同:IEnumerable是一个声明式的接口,声明实现该接口的class是“可枚举(enumerable)”的,但并没有说明如何实现迭代器,

    而IEnumerator是一个实现式的接口,IEnumerator对象就是一个迭代器。 

     

    关于IEnumerable<T>我们来了解一下它的代码:

     

    4.由于IEnumerable<T>继承了IEnumerable接口,所以要实现IEnumerator<T> ,还需要实现IEnumerator接口,由于和泛型版本的方法同名,所以该方法的实现需要使用显式接口实现。这里就不继续介绍它的具体实现了,和IEnumerator基本一致,这里就不详述了,读者可以自己动手写一下。
     
    ps 了解IEnumerable和IEnumerable<T>对今后学西理解LINQ是有很大帮助的。

     

    出自: Keiling_J'Blog http://www.cnblogs.com/keiling/

     

     

     

     

  • 相关阅读:
    linux CGI GET POST 用户登录
    linux内核 简化版ksetexample.c解析
    定制.vimrc配置文件
    procfs信息读取实现案例
    基于Extent 的文件存储(fiemap)
    inode_operations介绍
    Linux 文件系统概述
    linux硬链接与软连接的区别
    procfs读写信息实例
    VC 常见问题百问 20080129 13:37 271人阅读 评论(0) 收藏
  • 原文地址:https://www.cnblogs.com/keiling/p/3687638.html
Copyright © 2020-2023  润新知