(06)基础强化:ArrayList与Hashtable,比较器Comparer,泛型集合List<T>与Dictionary<K,V>,枚举器与迭代器,泛型的协变与逆变

    
    
    
一、.net中的集合


    1、集合命名空间:
    
        using System.Collections,(非泛型集合)
        using System.Collections.Generic;(泛型集合)
        
        命名空间就是逻辑上对类进行分类。同一命名空间不能有同名的类。
        
        问题:既然有了数组,为什么还要用集合?
            数组一旦声明长度与类型就固定了,不灵活,可能不够用或浪费。
            而集合长度是动态的,会自动随着使用而扩展,且存储类型不限制。
    
    
    2、常用集合
    
        "类似数组"集合:ArrayList、List<T>
        "键值对"集合 ("哈希表"集合):Hashtable、Dictionary<K,V>
        
        "堆栈"集合:Stack、Stack<T> 
                    (LIFO) Last In First Out
        队列集合:Queue、Queue<T> 
                    (FIFO) First In First Out
                    
        "可排序键值对"集合:(插入、检索没有"哈希表"集合高效)
            SortedList、SortedList<K,V> (占用内存更少,可以通过索引访问)
            SortedDictionary<K,V> (占用内存更多,没有索引,
                                    但插入、删除元素的速度比SortedList快)
                                    
        Set 集合:无序、不重复。
            HashSet<T>,可以将 HashSet类视为不包含值的Dictionary集合。与List<T>类似。
            SortedSet<T> (.net4.0支持,有序无重复集合。)
            
        "双向链表"集合: LinkedList<T>,增删速度快        
    
    


二、集合ArrayList与Hashtable(List<T>、Dictionary<K,V>)

 1、集合的特点:
    
        数组特点:类型统一、长度固定。
        
        集合的特点:
        1)长度不确定,灵活。根据需要自动扩大容量,可节省空间避免浪费。
                            数组则是初始化时就固定长度,不可改变。

        2)集合可以添加任何类型的元素,数组则必须是同一类型。

        3)集合可以通过equalscomparTo方法自定义元素的比较标准,实现任何类型
            的元素的比较。        4)集合还有许多自带的功能,很方便解决更多的实际问题。
        
        
        问题:ArrayList能自动缩小容量(缩容)吗?为什么?
        
            答:不能。
            因为很多时候都无法判断是否需要进行缩容操作。能不能一有多余的就自动
        进行缩容呢?想法很美好,但是,如果一直在增容,每次增容前自动进行了缩容,
        更造成资源的浪费。所以,没有必要进行自动缩容操作。
        
            但有时确有必须进行缩容,怎么办?
            
            可以用ArrayList的TrimToSize()方法进行手动缩容。
            将容量设置为 ArrayList 中元素的实际数目。如果未向集合添加新元素,
        则此方法可用于最大程度地减少集合的内存开销。

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList();
            Console.WriteLine(al.Capacity);//0
            al.Add(1);
            Console.WriteLine(al.Capacity);//4
            al.AddRange(new ArrayList() { 2, 3, 4, 5 });
            Console.WriteLine(al.Capacity);//8

            al.RemoveRange(2, 3);
            al.TrimToSize();  //手动缩容
            Console.WriteLine(al.Count);//2
            Console.WriteLine(al.Capacity);//2
            al.Add(3);
            Console.WriteLine(al.Capacity);//4

            Console.ReadKey();
        }


        
        
    2、ArrayList构造函数
        
        有三个:
            ArrayList();实例为空,初始大小(0)
            ArrayList (int capacity); 设置初始容量大小
            ArrayList (System.Collections.ICollection c);用集合初始化。
            
        ICollection 接口
            定义所有非泛型集合的大小、枚举数和同步方法。该 ICollection 接口是命
        名空间中类的 System.Collections 基接口。
            比如用集合,数组,等都可以。
            
            用集合接口进行初始化时:从指定集合复制的元素,并具有与复制的元素数
        相同的初始容量。
        
        
    3、集合常用操作添加、遍历、移除
    
        命名空间System.Collections
        ArrayList 可变长度数组,使用类似于数组
        属性:
            Capacity(集合中可以容纳元素的个数,翻倍增长); 
            Count(集合中实际存放的元素的个数。)
        方法:
            Add(10); 
            AddRange(Collection c); 
            
            Remove(); 
            RemoveAt(int index);
            RemoveRange(int index,int count);
            
            Clear();
            Contains(); 
            ToArray(); 
            Sort();    //排序
            Reverse(); //反转
            
            Insert(int index, object value);   //插入元素(索引前插入)
            INsertRange();

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList();
            al.Add(0);
            al.AddRange(new int[] { 1, 2, 3, 4 });
            al.Insert(4, 5);
            Console.WriteLine(string.Join(",", al.ToArray()));
            al.InsertRange(5, new int[] { 6, 7, 8, 9 });
            al.Remove(8);
            Console.WriteLine(string.Join(",", al.ToArray()));
            al.RemoveAt(4);
            al.Sort();
            al.Reverse();
            Console.WriteLine(string.Join(",", al.ToArray()));
            al.Clear();
            Console.WriteLine(al.Count);

            Console.ReadKey();
        }


            
            
    4、易错题:下面a处的结果是多少? 集合al中还有元素么?

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
            for (int i = 0; i < al.Count; i++)
            {
                al.RemoveAt(i);
            }
            Console.WriteLine(al.Count);//a

            Console.ReadKey();
        }


        
        答:结果是4。
            当i是增加变大时,al.Count也在减少变小,直到走到一半时即i=4,al.Count=4
        时,条件不成立,就跳出循环了。所以还有4个元素。
        
            al中还有哪些元素呢?
            当索引0的值1被删除后,集合成了{2,3,4,5,6,7,8}。此时集合索引0的值变成2,
        索引1的值变成3,索引2的值变成4...,
            当第二次循环i=1时,将删除索引1的值3,于是前面索引为0的值2就幸存下来,
        第二次循环完后,集合成了{2,4,5,6,7,8}.
            同样第三次循环时,将删除索引2的值5,集合变成{2,4,6,7,8}....最终为
        {2,4,6,8}
            因此从前面i=0增长删除,会跳个删除,最终剩下一半(奇数时取整)
            把上面改成{1,2,3,4,5,6,7,8,9},结果仍然是4,元素与上相同。
            
        结论:集合不能用变化的i从前面删除。因此用索引删除时应特别小心索引变化。
        
        方法一:从后面最后一个索引进行删除:

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
            for (int i = al.Count - 1; i >= 0; i--)//递减
            {
                al.RemoveAt(i);
            }
            Console.WriteLine(al.Count);//a
            Console.WriteLine(string.Join(",", al.ToArray()));

            Console.ReadKey();
        }


        
        方法二:从前面一直删除第0个索引。注意:al.Count一直在变化,要先提出来固定。

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });

            //for (int i = 0; i < al.Count; i++)//仍然错误,因为al.count一直在动态变化。
            //{
            //    al.RemoveAt(0);
            //}

            int c = al.Count;  //正确,先提取出来,下面循环次数就固定了
            for (int i = 0; i < c; i++)
            {
                al.RemoveAt(0);
            }

            Console.WriteLine(al.Count);//a
            Console.WriteLine(string.Join(",", al.ToArray()));

            Console.ReadKey();
        }


        
        注意:
            上面操作不能用foreach。因为foreach是只读遍历。
        不能在foreach中对List以及Dictionary等进行元素的添加和删除操作,否则异常。
        
        
        
    5、易错题:下面删除后的结果分别是怎样的?

        internal class Program
        {
            private static void Main(string[] args)
            {
                ArrayList al = new ArrayList();

                string s = "小红";
                al.Add(s);
                Person p = new Person() { Name = "小明" };
                al.Add(p);

                Person p1 = new Person() { Name = "小明" };
                al.Add("小红");
                al.Add(p1); 

                Person p2 = new Person() { Name = "小明" };
                string s1 = new string(new char[] { '小', '红' });

                Console.WriteLine("小红删除前:{0}", al.Count);//a
                al.Remove(s1);
                Console.WriteLine("小红删除后:{0}", al.Count);
                Console.WriteLine(al[0] is Person ? "是Person对象" : "不是Person对象");
                Console.WriteLine("---------------------------------------------------");

                Console.WriteLine("P2删除前:{0}", al.Count);//b
                al.Remove(p2);
                Console.WriteLine("P2删除后:{0}", al.Count);
                Console.WriteLine("---------------------------------------------------");

                Console.WriteLine("P1删除前:{0}", al.Count);//c
                al.Remove(p);//d
                Console.WriteLine("P1删除后:{0}", al.Count);
                Console.WriteLine(al[0]);

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
        }


        
        提示:比较两个对象相等用EqualsReference。
                Equal在比较字符串时,只要值相等,它也会显示True。
                ArrayList在删除对象Remove时,对比相等时会用Equals。
        
        a处,删除时由4变成3,尽管字符串两对象不同,但因remove中比较对象是用Equals所以
            值相等,第一个"小红"也被删除。因此第1索引变成第0索引,是Person对象。
        b处,删除时是3不变。因为Equals时是不同对象,所以不能删除,元素不变也不异常.
        c处,删除时由3变2,索引0的Person对象被删除,于是第0索引为字符串"小红"
            
        如果d处,改成p1,删除后索引0仍然为原来的Person,后面的Person对象被删除。
            
        
        结论:
            删除时比较对象时内部用的Equals()
            删除不存在的元素不会异常。
            删除时有多个相等元素,则先从第一个相等处开始删除。
        
    
    6.Contains(object item)
        
        判断内部元素是否包含对象。
        它同Remove类似,都是用Equals来比较两个对象。

        internal class Program
        {
            private static void Main(string[] args)
            {
                ArrayList al = new ArrayList();

                string s = "小红";
                al.Add(s);
                string s1 = new string(new char[] { '小', '红' });

                Console.WriteLine(al.Contains(s1));    //a
                Console.WriteLine(al.Contains(new char[] { '小', '红' }));  //b

                Person p = new Person() { Name = "小明" }, p1 = new Person() { Name = "小明" };
                al.Add(p);
                Console.WriteLine(al.Contains(p1));  //c

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
        }


        
        a处为真,因为用的Equals比较对象,当字符串时内容相同,则认定为同一对象;
        b处为假,因为里面是数组Array而不是字符串。
        C处为假,因为p与p1不是同一个对象。
        
        
    7、排序
        
        Sort()进行升序排列(没有降序排列);
        Reverse()对原集合反转。与上面结合:相当于降序排列。

        internal class Program
        {
            private static void Main(string[] args)
            {
                ArrayList al = new ArrayList(new int[] { 1, 3, 8, 7, 6, 2, 5, 4 });
                al.Sort();//a
                Console.WriteLine(string.Join(",", al.ToArray()));

                al.Clear();
                al.AddRange(new string[] { "Dd", "f", "d", "ad", "E", "ab", "b", "ac", "abcd", "D" });
                al.Sort();//b
                Console.WriteLine(string.Join(",", al.ToArray()));

                al.Clear();
                al.AddRange(new char[] { '1', 'a', '9', 'z', '5', 'b', 'x', 'd', 'D', 'A' });
                al.Sort();//c
                Console.WriteLine(string.Join(",", al.ToArray()));

                al.Clear();
                al.AddRange(new Person[] { new Person() { Name = "小明", Age = 17 }, new Person() { Name = "小红", Age = 18 }, new Person() { Name = "小李", Age = 20 } });
                //al.Sort();  //异常,不能比较两个对象,它并不明白用什么比较,比较什么??
                Console.WriteLine(string.Join(",", al.ToArray()));

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }


        
        a处,数值升序排序,按数字大小。
        b处,字符串升序排序,按字母大小(忽略大小写);同一字符时小写在前,大写在后。
        c处,字符比较。按ASC码大小比较升序。
        最后,对象的比较,程序并不知道用Name还是Age进行比较,它盲然了,直接异常。
        
        
        为什么前面都能比较,而后面对象就不能比较呢?
            因为前面都实现了IComparable接口(微软内部实现定义),而Person是我们
        自己的,并不能判断如何比较。
        
            如果要比较,可以对自己的类实现IComparable比较接口:
            方法的int CompareTo(object o) 实现必须返回具有三个 Int32 值之一:
            小于零    当前对象小于o.
            零        两对象相等.
            大于零    当前对象大于o.

        internal class Program
        {
            private static void Main(string[] args)
            {
                ArrayList al = new ArrayList(new Person[] { new Person() { Name = "小明", Age = 27 }, new Person() { Name = "小红", Age = 18 }, new Person() { Name = "小李", Age = 20 } });
                al.Sort();//已经定义IComparable接口,所以此时不会异常。
                for (int i = 0; i < al.Count; i++)
                {
                    Console.Write(((Person)al[i]).Name + " ");
                }

                Console.ReadKey();
            }
        }

        internal class Person : IComparable
        {
            public string Name { get; set; }
            public int Age { get; set; }

            public int CompareTo(object obj)
            {
                Person o = obj as Person;
                if (o != null)
                {
                    return this.Age - o.Age;//相差,大于零则大于o
                }
                return 0; //为空设置为相等
            }
        }


        
        简言之:只要实现了IComparable,就可以比较。
        
            因此,对于已知类型,微软已经实现了该接口,可以直接比较。但对于自定义
        的类型要比较,就必须实现IComparable接口,微软自动调用该接口进行比较。
            在排序Sort()方法中,微软自动循环调用CompareTo()进行比较排序,从而得出
        整个排序的序列(鼠标定位到Sort(),逐步按F12查看,可以看到比较的方法)
        
        
        作业:
            写一Student类,有两属:姓名(3-5字符),年龄。实现几种排序:
            1)按照年龄升序或升序排序(2种);
            2)按照姓名长度升序排序;
            3)按姓名的ASC升序或降序排序;
        分析:由于每次ComparaTo只能比较一次,那么五种比较是不是分别写五种?
        尝试:在类中用一个属性来确定比较的类型,再进行排序。
            用一个枚举来说明比较类型,便于用户选择排序类型时直接提示选择了解。
        然后在比较中逐个判断枚举的设置,并完成设置。
        

        internal class Program
        {
            private static void Main(string[] args)
            {
                Student s1 = new Student() { Name = "a", Age = 27 };//汉字ASC码为负
                Student s2 = new Student() { Name = "ab", Age = 18 };
                Student s3 = new Student() { Name = "c", Age = 20 };
                ArrayList al = new ArrayList();
                al.Add(s1); al.Add(s2); al.Add(s3);

                Student.SortType = eSortType.NameLen;
                al.Sort();
                for (int i = 0; i < al.Count; i++)
                {
                    Console.Write(((Student)al[i]).Name + " ");
                }

                Console.ReadKey();
            }
        }

        internal class Student : IComparable
        {
            private static eSortType _sorttype;

            public static eSortType SortType
            {
                get { return _sorttype; }
                set { _sorttype = value; }
            }

            public string Name { get; set; }
            public int Age { get; set; }

            public Student()
            {
                _sorttype = eSortType.NameAsc;
            }

            public int CompareTo(object obj)
            {
                Student o = obj as Student;
                if (o != null)
                {
                    switch (_sorttype)
                    {
                        case eSortType.NameAsc:
                            return this.Name.CompareTo(o.Name);

                        case eSortType.NameDes:
                            return -this.Name.CompareTo(o.Name);

                        case eSortType.NameLen:
                            return this.Name.Length - o.Name.Length;

                        case eSortType.AgeAsc:
                            return this.Age - o.Age;

                        default:
                            return -(this.Age - o.Age);
                    }
                }
                return 0;
            }
        }

        public enum eSortType
        {
            NameAsc,
            NameDes,
            NameLen,
            AgeAsc,
            AgeDes
        }


        
        
    8、比较器Comparer
        
            上面7中的作业使用了枚举及新添加了属性,算是完成了作业。但实际上我们
        再仔细看一下Sort()方法的三种重载:
        
        Sort()    使用已经定义好的进行排序。(没有定义则异常)
        Sort(System.Collections.IComparer comparer)    使用指定的比较器进行排序。
        Sort(int index, int count,IComparer comparer))用指定比较器及范围进行排序    
                    
        前面用的是第一种无参Sort,第二种带有一个比较器,这是一个比较器接口。
        也就是说一个实现比较的类。
        
        它实际上也是多态的实现。多个比较器,都用这个统一的接口作为参数,方便!
        随便comparer变化,都使用这个参数。
        
        下面对7进行实现,它并不复杂,但为了写全五种方法代码较长。
        
        提示:
            注意区别,sort()无参时,自定义用的是IComparable接口,这样CompareTo()
        在Student中被实现,在Sort()方法内被调用比较(微软内部)。
            而Sort(comparer),比较器时,是实现比较器IComparer(不是IComparable)
        需要另创建类来实现这个接口。由于没用过,可以写上接口后Ctrl+Alt+F10来自动
        实现创建的接口的方法与参数。或者直接对IComparer接口器按F1查找帮助。
            创建好后,则是Sort(comparer)来调用比较器(微软内部).

        internal class Program
        {
            private static void Main(string[] args)
            {
                Student s1 = new Student() { Name = "a", Age = 27 };//汉字ASC码为负
                Student s2 = new Student() { Name = "ab", Age = 18 };
                Student s3 = new Student() { Name = "c", Age = 20 };
                ArrayList al = new ArrayList();
                al.Add(s1); al.Add(s2); al.Add(s3); al.Add(new Student());

                al.Sort(new SortByAgeAsc());//这里选择比较器
                for (int i = 0; i < al.Count; i++)
                {
                    Console.WriteLine(((Student)al[i]).Name);
                }

                Console.ReadKey();
            }
        }

        internal class Student
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        //这个类就是一个比较器。
        internal class SortByNameAsc : IComparer
        {
            public int Compare(object x, object y)
            {
                Student s1 = x as Student;
                Student s2 = y as Student;
                if (s1 == null && s2 == null)
                {
                    return 0;
                }
                else if (s1 == null)
                {
                    return -1;
                }
                else if (s2 == null)
                {
                    return 1;
                }
                else
                {
                    return s1.Name.CompareTo(s2.Name);
                }
            }
        }

        internal class SortByNameDec : IComparer
        {
            public int Compare(object x, object y)
            {
                Student s1 = x as Student;
                Student s2 = y as Student;
                if (s1 == null && s2 == null)
                {
                    return 0;
                }
                else if (s1 == null)
                {
                    return -1;
                }
                else if (s2 == null)
                {
                    return 1;
                }
                else
                {
                    return s2.Name.CompareTo(s1.Name);
                }
            }
        }

        internal class SortByNameLen : IComparer
        {
            public int Compare(object x, object y)
            {
                Student s1 = x as Student;
                Student s2 = y as Student;
                if (s1 == null && s2 == null)
                {
                    return 0;
                }
                else if (s1 == null)
                {
                    return -1;
                }
                else if (s2 == null)
                {
                    return 1;
                }
                else
                {
                    return s1.Name.Length - s2.Name.Length;
                }
            }
        }

        internal class SortByAgeAsc : IComparer
        {
            public int Compare(object x, object y)
            {
                Student s1 = x as Student;
                Student s2 = y as Student;
                if (s1 == null && s2 == null)
                {
                    return 0;
                }
                else if (s1 == null)
                {
                    return -1;
                }
                else if (s2 == null)
                {
                    return 1;
                }
                else
                {
                    return s1.Age - s2.Age;
                }
            }
        }

        internal class SortByAgeDes : IComparer
        {
            public int Compare(object x, object y)
            {
                Student s1 = x as Student;
                Student s2 = y as Student;
                if (s1 == null && s2 == null)
                {
                    return 0;
                }
                else if (s1 == null)
                {
                    return -1;
                }
                else if (s2 == null)
                {
                    return 1;
                }
                else
                {
                    return s2.Age - s1.Age;
                }
            }
        }


        
        无论是7中的枚举,还是8中的比较器,都可以实现比较方法的选择。
        但还是建议用8中的比较器,毕竟通用,大家熟知。
        

 
        
三、键值对Hashtable


        所有集合中的数据都存储在数组中,只是优化的方向与程度不同。
        
    1、Hashtable键值对的集合,类似于字典。Hashtable在查找元素的时候,速度很快。
    
        Add(object key,obiect value);  参数为object很灵活多样
        
        hash["key"];   返回是value,类型为object
        hash["key]="修改";
        
        ContainsKey(object key);     与Contains(object key)相同。
        ContainsValue(object value); 此方法查找是否包含value
                        注意:上面比较key与value用Equals方法(字符串时值相等为真)
                        
        Remove(object key); 根据键删除
        
        
    2、遍历:
        
        键值对的"集合"里面有两个:
            1)键key的集合:hash.Keys
            2)值value的集合:hash.Values/DictionaryEntry
            
            键值对集合中的"键",绝对不能重复。        
        
        Hashtable没有数字的索引器,用for循环时无法引用每一个元素。
        所以,它只能用Foreach进行遍历访问。
        
            foreach (var item in collection)
            上面的var是什么?
            var是一个类型推断,后面会讲。不知道时可将var改为object.
        
        问题:
            什么类型能用foreach进行访问呢?(后面讲)
            
        根据集合的个数,可以有以下的遍历:

        internal class Program
        {
            private static void Main(string[] args)
            {
                Hashtable hash = new Hashtable();
                hash.Add("sealed", "new");
                hash.Add(33, 22.7);
                hash["object"] = new Person() { Name = "Comparer" };

                //遍历一:根据keys
                foreach (object k in hash.Keys)
                {
                    Console.WriteLine("Key:{0}->value:{1}", k, hash[k]);
                }
                Console.WriteLine("--------------------------");

                //遍历二:根据values
                foreach (object v in hash.Values)
                {
                    Console.WriteLine("value:{0}", v);//无法反推key
                }
                Console.WriteLine("-------------------------");

                //遍历三:根据键值对
                foreach (DictionaryEntry p in hash)
                {
                    Console.WriteLine("key:{0}->value:{1}", p.Key, p.Value);
                }

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
        }


        
        问题:
            键值对DictionaryEntry与KeyValuePair的区别?
            
            前者是无法确定类型时的键值对。在新的开发中,微软不推荐再使用它。
            后者是集合泛型中的键值对,在主流的泛型中使用。不能在前者的环境使用。
            
        问题:
            怎么知道foreach中要用DictionaryEntry呢?
            对Hashtable按F1,在注解里有:每个元素都是存储在对象中的DictionaryEntry
        键/值配对。 键不能 null,但值可以是。
        
        
        
        
        犹如根据拼音(键key)去查找汉字(值value)一样,查找很快。为此,它不允许
        有两个拼音一样的(键key不允许有相同的),否则报异常。
        
        
    3、为什么Hashtable表访问速度很快?
        
        Hashtable根据key,通过一定哈希算法,算出索引。由索引直接提取出value。
        而不是一个一个逐个去找,所以速度很快。
        
            犹如我们查字典一样,不必一页一页去找。而是根据拼音(键key)找到页数,
        然后直接取该页的汉字(值value)一样,查找很快。
        
            因此,它不允许有两个拼音一样的(键key不允许有相同的),否则报异常。
        
        

   
四、集合练习


   案例1:两个(ArrayList)集合{"a","b","c","d","e"}和{"d","e","f","g","h"},把这
        两个集合去除重复项合并成一个。

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList(new string[] { "a", "b", "c", "d", "e" });
            ArrayList al1 = new ArrayList(new string[] { "d", "e", "f", "g", "h" });
            ArrayList al2 = new ArrayList();

            //第一个集合检索添加
            foreach (string s in al)
            {
                if (!al2.Contains(s))
                {
                    al2.Add(s);
                }
            }

            //第二个集合检索添加
            foreach (string s in al1)
            {
                if (!al2.Contains(s))
                {
                    al2.Add(s);
                }
            }
            Console.WriteLine(string.Join(",", al2.ToArray()));

            Console.ReadKey();
        }


        
        注意:
             new ArrayList(new string[] { "a", "b", "c", "d", "e" });
             这是用构造函数中的重载集合接口(数组作集合)进行构造集合。
             new ArrayList() { new string[] { "a", "b", "c", "d", "e" } };
             这是用集合初始化器进行赋值初值。用的是一个数组作为一个元素进行赋值。
             前者共有5个元素,后者只有一个元素string[]是一个数组。
             
             
        
    案例2: 随机生成10个1-100之间的数放到ArrayList中,要求这10个数不能重复,并且
        都是偶数(添加10次,可能循环很多次。)

        提示:    Random random=new Random();
                random.next(1,101);//随即生成1-100之间的数

        private static void Main(string[] args)
        {
            ArrayList al = new ArrayList();
            Random r = new Random();
            while (al.Count < 10)
            {
                int n = r.Next(1, 101);
                if (n % 2 == 0 && !al.Contains(n))
                {
                    al.Add(n);
                }
            }
            Console.WriteLine(string.Join(",", al.ToArray()));

            Console.ReadKey();
        }


        
        注意:
            new Random()无参构造随机数,将使用系统启动后经过的毫秒数作为种子。
            [__DynamicallyInvokable]
            public Random() : this(Environment.TickCount)
            {
            }
            因此,如果将构造函数放在循环内,在1毫秒内,种子相同产生的随机数也相同,将会引
        发意外情况。比如,你需要不重复的,1毫秒内它一直产生重复的。下一个毫秒内也将一直是
        相同的...而往往程序1毫秒会循环很多次,浪费时间与资源。
        
        结论:
            不要把Random的实例化放到循环里面!可以使用两个集合来降低产生随机数的循环次数。
            
            Random在循环中会降低执行效率(每次new的时候的种子是一样的当前时间。)(*)。        
        
                
                
    练习:有一个字符串是用空格分隔的一系列整数,写一个程序把其中的整数做如下重新
        排列打印出来:奇数显示在左侧、偶数显示在右侧。比如"2 7 8 3 22 9 5 11"显示
        成"7 3 9 5 11 2 8 22

        private static void Main(string[] args)
        {
            string s = "2 7 8 3 22 9 5 11";
            string[] p = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            ArrayList al = new ArrayList();
            int count = 0;
            for (int i = 0; i < p.Length; i++)
            {
                int n = Convert.ToInt32(p[i]);
                if (n % 2 == 0)
                {
                    al.Add(p[i]);
                }
                else
                {
                    al.Insert(count, p[i]); //奇数也按顺序添加在左侧
                    count++;
                }
            }
            Console.WriteLine(string.Join(" ", al.ToArray()));

            Console.ReadKey();
        }


        
        


五、泛型集合


    1、ArrayList与Hashtable受限
    
        前面ArrayList与Hashtable以后不会再使用,它只是理解集合的基础。真正在实际运用中
    不会使用这两个集合,而且微软也不推荐使用这两个集合。
        
        原因:它里面是object,装入这两个集合时非常方便,但取出时,鬼知道里面是什么类型。
    还要进行类型的转换才能进行运算,非常麻烦不方便,这个缺点导致它们的使用受限。
    
    
    2、使用泛型集合
        
        List<string>整体是一个类型,不能拆开
        List<int>一样不能拆开,而且与前者有区别,是另一类型。
        
        泛型(generic):
            泛型实际上是一个类型参数(type parameters)的概念。
            
                类型参数使得设计类和方法时,不必确定一个或多个具体参数,它的具体参数可
            延迟到客户代码中声明、实现。使用泛型的类型参数T,避免运行时类型转换或装箱
            操作的代价和风险。
            
        泛型写法:
                在类名称的后面用<T>表示,T表示一种暂未指明的类型,相关的地方用T表示,
            在类创建对象时才指明数据类型,这样,就不会产生装箱拆箱操作,大大提高了效率
            和安全性。
        
        
        使用泛型的好处是什么?
            因为指定了类型,所以:
            1)性能高:不需类型转换,避免拆装箱带来的性能损失;
            2)类型安全:在编译时可以检查类型错误,及早发现错误。
            3)提高代码可重用性。
        
        
        List<T>、Dictionary<K,V>
            List<T>类似于ArrayList,ArrayList的升级版
                各种方法: 
                    Sort()、Max()、Min()、Sum()...
                    
            Dictionary<K,V>类似于Hashtable,Hashtable的升级版。
            键值对是KeyValuePair。(不能用DictionaryEntry)
            

            private static void Main(string[] args)
            {
                List<string> ls = new List<string>();
                List<int> li = new List<int>();
                //类型不同
                Console.WriteLine(ls);//System.Collections.Generic.List`1[System.String]
                Console.WriteLine(li);//System.Collections.Generic.List`1[System.Int32]

                Dictionary<string, int> dic = new Dictionary<string, int>() { { "a", 3 }, { "b", 4 }, { "c", 2 } };
                dic.Add("d", 5);
                dic["e"] = 8;
                //通过key遍历
                foreach (string key in dic.Keys)
                {
                    Console.Write($"{key}->{dic[key]},");
                }
                Console.WriteLine("\r\n---------------------");
                //通过value遍历
                foreach (int value in dic.Values)
                {
                    Console.Write($"{value}");
                }
                Console.WriteLine("\r\n--------------------");
                //通过键值对
                foreach (KeyValuePair<string, int> pair in dic)
                {
                    Console.Write($"{pair.Key}->{pair.Value},");
                }

                Console.ReadKey();
            }


            
        
    3、推荐使用泛型集合。
            
        T,K,V就像一把锁,锁住集合只能存某种特定的类型,这里的T,K,V也可以是其它字母.
            犹如代数中的x,y,z一样,只是占位,随时可以用其它来代替。下面用X占位:

            class Person<X>
            {
                public X Id { get; set; }
            }


        
        
    4、List.ToArray()返回的是什么类型数组?
        
        写一个泛型List,用ToArray后,鼠标定位ToArray后按F12,可以看到:

            public T[] ToArray()
            {
                T[] array = new T[_size];
                Array.Copy(_items, 0, array, 0, _size);
                return array;
            }  

     
        可以看到返回类型是T[],泛型里面是什么类型就返回什么类型。
        
        所以,如果要返回数组是int类型,那么定义尽量是List<int>,这样li.ToArray()
        就是int[]类型。(见下面六中2练习)
        
        
    5、var是什么?var表示什么意思?var怎么用?(推断类型)
            
        有时C#里面可以看到 var定义,见得最多的就是foreach:
             foreach (var item in collection)
        那么var到底是什么呢? var: variable变量
            引用:https://blog.csdn.net/m0_65636467/article/details/127692279
        
        var关键字是C#3.0开始新增的特性,称为推断类型(其实也就是弱化类型的定义) 。

        VAR可代替任何类型,编译器会根据上下文来判断你到底是想用什么类型,类似 OBJECT,
            但是效率比OBJECT高点。

        我们可以赋予局部变量推断"类型"var而不是显式类型。
        
        var 关键字指示编译器根据初始化语句右侧的表达式推断变量的类型。
        
        推断类型可以是内置类型、匿名类型、用户定义类型、.NET Framework 类库中定义的类型
            或任何表达式。
        
        通常定义是这样:
            int a = 1;
            string b ="2";
            即"必须先明确地"指定变量是什么数据类型,才能给它赋值.

        现在在C# 3.0里,有了变化,就是可以不用像上面那样定义变量了.如:
            var a =1 ;
            IDE或编译器会根据你给a 的值1,来"推论,断定"a是一个整数类型.

        同理:
            var b ="2";
            因为给b的值是"2"这样一个字符串,所以,b就是string类型。

        提示:
            当你无法确定自己将用的是什么类型,就可以使用VAR
    
    
        使用var定义变量时有以下四个特点:
          1)必须在定义时初始化赋值。也即必须有值。如var s = "abcd"形式。
                下面是错误的:
               var s;
               s = "abcd";//不能分开写,要一口气定义赋值写完,以便编译器推断。
          2)一但初始化完成,推断类型将固定。不再更改其类型。
          3)var要求是局部变量。
            4)使用var定义变量和object不同。
                var它在效率上和使用强类型方式定义变量完全一样。
        

   
六、IEnumerable、IEnumerator


    1、为什么要有枚举接口?
    
            平时常见集合,可以用foreach进行枚举,列出里面的内容。那是因为里面已
        经实现了可枚举接口与枚举器,能够顺利地用foreach列出集合中的元素。
        
            但有些自定义数据类型,也有一堆数据,但没有实现这些枚举器。如果这个时
        候使用foreach逐个枚举遍历里面的元素,就会报错。
        


            因此就需要人为添加可枚举接口与枚举器,这就是学习它的原因。
        
        学习进入之前明白二点:
        1)可枚举接口针对的是一堆数据,例如集合。
        3)枚举接口要做的事就是一个一个地把这一堆的数据列举遍历出来(如foreach).
        
    
    2、学哪些东西来实现枚举?
    
        IEnumerable这个接口主要用于对外可以使用的迭代器。
        IEnumerator接口定义了仅向前方式遍历或枚举集合元素的基本底层协议。
        
        两者还可以用泛型。即IEnumerable<T>、IEnumerator<T>.
        
        另外就是迭代器,但它的本质仍然是上面。
        
        
    3、什么是IEnumerable、IEnumerator?
        
        IEnumerable 可枚举的
        
            首先,它跟enum关键字所表达的意思是不同的。IEnumerable:可枚举的。就是集合
        中的数据,是一枚一枚可以让我们列举遍历出来的。
        
            可枚举类就是实现了IEnumerable接口的类。
        
            IEnumerable接口只有一个成员:GetEnumerator()方法。它返回对象的枚举器。

        public interface IEnumerable<out T> : IEnumerable
        {
            new IEnumerator<T> GetEnumerator();
        }    

    
            因为IEnumerable接口一般都是用其泛型版,我们直接看这种。接口内容很简单,
        但是其中又出现了一个名为IEnumerator的接口,我们可以称之为枚举器。
    
            在使用foreach时就会调用GetEnumerator()取得枚举器以便进行枚举。
        
        
        IEnumerator 枚举器
        
            枚举器的定义(泛型版T):

        public interface IEnumerator<out T> : IDisposable, IEnumerator
        {
            new T Current
            {
                get;
            }
        }   

 
            再来看看IEnumerator内部成员,主要有三个

        public interface IEnumerator
        {
            object Current{    get;}

            bool MoveNext();

            void Reset();
        }    

            Current: 返回序列中当前位置项的属性。
                它是只读的(只有Get),返回object类型可用于任何类型对象。
                
        MoveNext:把枚举器位置前进到集合中下一项的方法。
                返回bool:为真时新位置是有效的,false无效(已达尾部)
                枚举器原始位置为-1,因此MoveNext必须在第一次使用Current之前调用。
                
        Reset:把枚举器位置重置为原始状态的方法(-1)
        
            泛型版接口,则对其内部名为Current的成员指定了类型,也就是说,通过枚举我们
        可以获取一个枚举器(逐个展现数据的方式方法)。 
    
            通过枚举,我们能找到、取得一个个数据对象。明白了这一点,我们就能大体上了解
        如何通过IEnumerable来获取数据了。
        
        
    4、枚举器在外部的例子:
        
        下面为枚举器专门创建一个类,以便在本类的GetEnumerator()方法中取得返回枚举器对象.

        internal class Program
        {
            private static void Main(string[] args)
            {
                //int[]数组是实现了IEnumerable
                int[] ints = new int[] { 1, 2, 3, 4, 5, 6 };
                foreach (int i in ints)
                {
                    Console.Write(i + ",");
                }
                Console.WriteLine();

                //Person并没有实现,下面的枚举将出错
                Person p = new Person();
                foreach (string s in p)
                {
                    Console.WriteLine(s);
                }
                Console.ReadKey();
            }
        }

        public class Person
        {
            private string[] s = new string[] { "中国人", "美国人", "英国人", "日本人", "印第安人", "韩国人" };
        }


        
        显然自定义类型Person没有实现 IEnumerable,哪些内部成员s改为public也无法枚举。
        修改Person类:

        public class Person
        {
            private string[] s = new string[] { "中国人", "美国人", "英国人", "日本人", "印第安人", "韩国人" };

            public IEnumerator GetEnumerator()
            {
                foreach (string t in s)
                {
                    yield return t;
                }
            }
        }


        用迭代器循环不断地返回s中元素,上面代码也可以正确运行。再修改一下Person,添加枚举器:

        public class Person : IEnumerable
        {
            private string[] s = new string[] { "中国人", "美国人", "英国人", "日本人", "印第安人", "韩国人" };

            public IEnumerator GetEnumerator()
            {
                return new PersonIEnumerator(s);
            }
        }

        public class PersonIEnumerator : IEnumerator
        {
            private readonly string[] ps;
            private int index = -1;

            public PersonIEnumerator(string[] _ps)
            {
                this.ps = _ps;
            }

            public PersonIEnumerator()
            {
            }

            public object Current
            {
                get
                {
                    if (index < 0)
                    {
                        throw new InvalidCastException();
                    }
                    if (index >= ps.Length)
                    {
                        throw new InvalidCastException();
                    }
                    return this.ps[index];
                }
            }

            public bool MoveNext()
            {
                return ++index < this.ps.Length;
            }

            public void Reset()
            {
                index = -1;
            }
        }


        上面Person类可以不用IEnumerable接口。因为主程序中foreach主要是检测GetEnumerator
        有这个方法就可以枚举。因此,后面专门创建一个枚举器。
        
        
    5、枚举器在内部的例子
    
        单独创建一个枚举器类适合于多个类调用的情况。如果要使用的类较少,就创建在本类内部。

        internal class Program
        {
            private class CustomArray<T> : IEnumerable, IEnumerator
            {
                private T[] array;

                public CustomArray(T[] array)
                {
                    this.array = array;
                }

                //定义一个索引变量
                private int index = -1;//1  应该在头部之前

                //IEnemuerator提供的用于返回当前数据的属性
                public object Current => this.array[index];//2

                public IEnumerator GetEnumerator()//3  IEnumerable提供可以用于返回迭代器的方法
                {
                    Console.WriteLine("----调用迭代器-----");//4
                    //return null;//5
                    return this;//6
                }

                public bool MoveNext()//7 IEnumerator提供的用于移动指针索引的方法
                {
                    return ++index < this.array.Length;
                }

                public void Reset()//8 IEnumerator提供用于复位索引的方法
                {
                    index = -1;
                }
            }

            private static void Main(string[] args)
            {
                CustomArray<int> custom = new CustomArray<int>(new int[] { 2, 4, 6, 8 });
                foreach (int item in custom)
                {
                    Console.WriteLine(item);
                }
                CustomArray<string> cstr = new CustomArray<string>(new string[] { "jack", "lucy", "peter" });
                foreach (string item in cstr)
                {
                    Console.WriteLine(item);
                }
                Console.ReadKey();
            }
        }


        说明:
            枚举检查是否有3的GetEnumerator()方法,这个方法必须实现有1,2,7,8。但是不能
        返回空5处,相当于没有创建成员枚举器。所以改为6,因为枚举器就在本身内部。
        
        
    6、迭代器:
    
            是一种设计模式,可以让开发人员无需关心容器对象的底层架构,就可以遍访这个容器
        对象。简单来说,迭代器就是用来遍历一个序列中的所有对象。
    
            自己创建可枚举类和枚举器,有一小点麻烦。因此编译器将使用迭代器(iterator)自动
        创建可枚举类型和枚举器。比如yield.

        
            迭代器块是由一个或多个yield的语句代码块。
            
            迭代器块与以前代码块不同。这之前学的代码块都是命令式的,一条执行完成就执行
        下一条代码。
            
            但迭代器块是声明性的、描述性的,并不需要在同一时间执行一串命令式命令,它描述
        了如何枚举元素。编译器得到有关如何枚举项的描述后,使用它来构建包含所有需要的方法
        和属性实现的枚举器类。产生的类被嵌套包含在声明迭代器的类中

    
            那下面请看代码:(逐个返回当前的时间,10次循环10个数据结果)

        private static void Main(string[] args)
        {
            IEnumerable<string> enumerable = DateTimeEnum();
            foreach (var item in enumerable)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

        private static IEnumerable<string> DateTimeEnum()//返回可枚举的集合
        {
            for (int i = 0; i < 10; i++)
            {
                string result = DateTime.Now.ToString("HH:mm:ss");
                Thread.Sleep(1000);
                yield return result;//yield return类似continue一直返回,直到循环完成才终止
            }
        }


        
            代码中有一个返回IEnumerable<string>的方法,用来模拟数据的产生。其中用到了一
        个yield的关键字。简单来说yield return就是部分返回(产生了一个数据,就返回一个) 
            
            这个类似循环中的continue与break:continue是做完继续循环,break直接中断跳出循
        环。同样,yield return类似continue,返回一个数据后,并不结束方法,而是继续循环体,
        然后返回下一个数据...直到循环体完成。而return则是返回数据后,直接结束方法,不再
        进入循环和方法。
        
            这个方法最终的运行效果,就是一秒钟返回一个当前时间。循环10次后的共10个数据构
        成一个IEnumerable<string>集合。
             
            foreach就是为遍历IEnumerable数据打造的,它里面为我们封装了访问枚举器的操作。
        所以我们用它来遍历数据非常方便。
        
            当然,我们也想知道不用foreach时,应该怎么遍历IEnumerable数据。所以请看如下
        代码:

        IEnumerable<string> enumerable = DateTimeEnum();
        IEnumerable<string> enumerator= (IEnumerable<string>)enumerable.GetEnumerator();
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerable.Current);
        }


            
            拿到枚举器 我们就可以调用movenext()找数据(为什么要先调用MoveNext,而不是先
        取值?因为最开始时,内部指针是指向-1,移动一下就到了第一条记录,其指针是索引0)

            MoveNext方法返回bool值,有数据可寻则返回true,无数据则返回false,这就是循环的
        关键。
        
    
        
        
    7、泛型的协变与逆变
        
        这个主要是后面要用到参数out,所以必须先了解这个:
        引用:https://blog.csdn.net/LWR_Shadow/article/details/125708700
        
        (1)协变性
        
            A、什么是协变性?
            
            当我们使用泛型编程时,可能会遇到如下问题,即将一个较具体的类型赋值给一个较
        泛化的类型是可行的,但在泛型中却无法编译通过。

            object s = new string(new char[] { 'a', 'b', 'c' });//正确string->object
            List<object> list = new List<string>();//错误.List<string>不能转List<object>


            
            用不同类型参数声明同一个泛型类的两个变量,这两个变量不是类型兼容的。即使是
        将一个较具体的类型赋值给一个较泛化的类型。
        
            简言之:List<int>是整体,与List<string>、List<double>等是不同的类型。
            也就是说,他们不是协变量。 
            
            那么什么是协变?
            举个例子来讲,假设有X和Y两个类型,每个X类型的值都能转换成Y类型。我们

             将I<X>向I<Y>的转换称为协变转换。反之我们将I<Y>向I<X>的转换称为逆变转换。
            
            那么为什么泛型不支持协变呢?我们可以假设C#允许泛型协变,看一下会发生什么:

            List<string> strList = new List<string>() { "aaa", "bbb" };
            // 假设下面都是合法的,正确的.
            List<object> objList = strList;//实际上这里出错了。
            objList.Add(1);//添加int类型
            objList.Add(2);


        
            可以看到,因为objList是List<object>类型的,因此向其中添加int类型的数据完全
        合法,但objList又是strList的别名,而strList只能是一个字符串列表,向其中添加整
        型数据就破坏了其(strList)类型安全。
            
            因此若允许不受限制的泛型协变性,类型安全将完全失去保障。
            又想用这种,但又不想背锅,咋办?
        
        
        B、使用out修饰符允许协变性
            
            根据前面的描述,泛型类型之所以限制协变性是因为List<T>允许向其内    容写入
        (可能有不同类型进入),从而使类型安全失去保障。
            
                那么如果只允许读取,不允许写入呢?
                C#从4开始加入了对安全协变性的支持。如果要指出泛型接口应该对它的某个类型
        参数协变,就用out修饰该类型参数。

                List<string> strList = new List<string>() { "aaa", "bbb" };
                // List<object> objList = strList;//错误
                IReadOnlyList<object> objList = strList;//正确

            上面的代码中,IReadOnlyList<T>就是使用了out修饰符的泛型接口,而    List<T>实现
        了这一接口。因为IReadOnlyList<T>接口只提供了读取方法,并没有提供写入方法,因此
        该协变转换是合法的。
            
        协变转换也存在一些限制:
            (1)只有泛型接口和泛型委托才支持协变,泛型类和结构不支持。
            (2)来源T和目标T必须都为引用类型,不能是值类型。
                
        
        (2)逆变性
        
            逆变性就是协变性的反方向。仍然是之前的例子,假设有X和Y两个类型,且X和Y之间
        有特殊关系,即每个X类型的值都能转换成Y类型。
        
            如果I<X>与I<Y>总具有相反的特殊关系,即I<Y>类型的每个值都能转换为I<X>类型,
        那么就可以说“I<T>对T逆变”。            固定泛型同样不允许逆变:

            internal class Program
            {
                private static void Main(string[] args)
                {
                    IExample<Fruit> fruit = new ExampleClass<Fruit>() { Item = new Orange() };
                    // 编译不通过
                    IExample<Apple> apple = fruit;//1:出错
                    Apple app = apple.Item;       //2
                    
                    Console.WriteLine("Hello, World!");
                }
            }

            public class Fruit
            { }

            public class Apple : Fruit
            { }

            public class Orange : Fruit
            { }

            public interface IExample<T>
            {
                public T Item { get; set; }
            }

            public class ExampleClass<T> : IExample<T>
            {
                public T Item { get; set; }
            }


            上面,1处会出错。假定上面通过编译,我们就可以在2处,合法地将Item赋值给一个
        apple变量,但问题在于Item原本是一个Orange对象,这样的转换显然是不正确的。
        
            去除2处代码,运行则会在接口IExample<T>内部的T上弹出错误:
            CS1961    变型无效: 类型参数“T”必须是在“IExample<T>.Item”上有效的固定式。“T”
        为 逆变。...

            
            通俗地说:在apple(苹果集)里面只能放进入(写入)苹果,不能拿出来(读取)水
        果。因为苹果集来源水果集,从苹果集拿出的可能是非苹果,也许是香蕉,也许是桔子...
        等等,这就违背了苹果集的初衷。但是可以写入(放入)苹果集,因为,你写入的是苹
        果,最终进入是水果集,苹果属于水果,没毛病!
            这也是in的用法(下面解说)。
        
            根据上面的示例,我们可以发现,固定泛型接口不允许逆变的原因在于其内部存在
        有返回值的方法。
        
            那么假如泛型接口内部不存在有返回值的方法,是不是就允许逆变了呢?
    
    
        使用in修饰符允许逆变性
        
            C#提供了in操作符来允许泛型逆变。它的使用方法与out修饰符类似。通过in修饰的
        泛型接口不允许T作为属性取值方法或方法返回类型使用。
        
            修改上面的接口IExample<in T>:

            public interface IExample<in T>
            {
                public T Item { set; }//只写不读
            }


        
            主程序:

            IExample<Fruit> fruit = new ExampleClass<Fruit>() { Item = new Orange() };
            IExample<Apple> apple = fruit;//正确


            
            这看起来还是有些反直觉,因为我们还是将“一筐苹果”的指针指向了“一筐橘子”,但
        实际上因为无法返回Apple类型的值,所以该过程只发生了从Orange向Fruit类型的转换,
        而没有发生从Fruit向Apple类型的转换,这是完全合法的。
        
        (3)总结
        
        协变:
            从子类转换到父类;(fruit<-apple,可取get不可set,即可out不可in)
            泛型参数定义的类型只能作为返回类型,不能作为参数类型;使用out修饰。
        逆变:
            从父类转换到子类;(apple<-fruit,不可取get可set,即不可out可in)
            泛型参数定义的类型只能作为参数类型,不能作为返回类型;使用in修饰。
            
            协变之所以不允许泛型参数作为参数类型,是因为IExample<Apple>的接口方法要求
        传入一个Apple作为参数,但把IExample<Apple>赋值给IExample<Fruit>之后,你传入的
        就是Fruit对象。从Fruit->Apple是类型不安全的。

            逆变之所以不允许泛型参数作为返回类型,是因为IExample<Fruit>的接口方法返回
        的是一个Fruit对象,但把IExample<Fruit>赋值给IExample<Apple>之后,返回的就是
        Apple对象。从Fruit->Apple是类型不安全的。

            其实它们本质上还是符合里氏替换原则。
            协变与逆变更严格地说明了它们的区别和变通处理方法。
            通过限制输入或输出,就可以防止类型不安全的转换。
        
        
        
    8、List.AddRange()里面参数是什么?
        
            看一下这个方法的全写:

        public void AddRange (System.Collections.Generic.IEnumerable<T> collection);


        
            里面用了一个IEnumerable<T>的接口,这个又是什么?继续看它:
        
        IEnumerable<T> 接口,全写:

            public interface IEnumerable<out T> : System.Collections.IEnumerable


            
            引用:https://www.cnblogs.com/DoNetCoder/p/4083778.html
        IEnumerable<out T>接口是最基础的泛型集合接口,表示可迭代的项的序列。

        为什么泛型参数要带一个“out”?是不是参数out?
            此“out”和C#中的“out”类型参数的“out”并非一个意思。
            IEnumerable<out T>中的out表示这个接口支持“协变性”。
            
            何谓“协变性”?
                简言之:子转父,可取(get/out)不可入(set/in).
                    可由苹果集转为水果集,这时的苹果集只能取out不能设置in.
            IEnumerable<string> str = new List<string>() { "a", "b", "c" };
            IEnumerable<object> obj = str;            
                
            在C#4.0之前,由于IEnumerable<T>的声明并未包含“out”关键字,所以上面的代码是无法
        通过编译的,编译器会告知你类型转换失败,因为str对应的类型为IEnumerable<string>,
        而obj对应的类型为IEnumerable<object>。
        
            在C#5.0后,加入了"out"与"in",出现了协变与逆变。
            
            协变out意味着:子类转父类。子类(string字符串集)更具体,转换到了父类object
        对象集(实际内部仍然存储的是string集)。
        
            这时只能取出out里面的string字符串元素,而不能设置in里面的元素。因为object
        集很多种,可能是int,也许是float...等等,一旦设置,内部的就得不是纯一的string
        的。str集将变得不伦不类,因此用out来限制它。
        
            接口定义中interface IEnumerable<T>只有getter,因此您不能使用该接口
        (interface)将任何元素插入(设置)到集合中。
        
            因此,在接口IEnumerable<out T>的声明out.它告诉编译器,类型为T的值只能朝一
        个方向前进,正在"走出去",只能取出来。这使得它在一个方向上兼容。
        
            您可以使用 IEnumerable<string>作为IEnumerable<object> ,因为您只是在阅读
        它。您可以将字符串作为对象读取。但是你不能写入它,你不能将对象作为字符串传递。
        因为object它可以是整数或任何其他类型,一旦写入将破坏string集里的统一,直接出
        错。
        
        问题:foreach为什么是只读的?
            foreach实现了IEnumerable或IEnumerable<T>,这个调用了枚举器IEnumerator里面
        的Current只有get(只读)。定义上就限制了它只能是只读的。
            因为遍历,不能一边遍历一边修改删除等,造成不可控的因素,因此是只读的
            
        
    9、IEnumerable、IEnumerator区别是什么
    
        IEnumerable是一个声明式的接口,继承此接口需要实现GetEnumerator方法,
        返回一个IEnumerator对象:

            public interface IEnumerable
            {
                IEnumerator GetEnumerator();
            }  

     
        IEnumerator接口是实现式接口,有三个成员:
            Current 返回当前序列的元素,
            方法MoveNext() 索引加1,移动到下一个元素,
            Reset 索引复位-1,方法重置.

            public interface IEnumerator
            {
                object Current { get; } 
                bool MoveNext();
                void Reset();
            }   

     
        对比两接口,枚举一个容器,真正起作用的是IEnumerator。
        
            用foreach去遍历,它就是通过IEnumerable中的GetEnumerator()取得枚举器
        IEnumerator。IEnumerator接口的作用才是迭代的主体,负责遍历序列。
        
            IEnumerable和IEnumerator通过IEnumerable的GetEnumerator()方法建立了连接,
        client可以通过IEnumerable的GetEnumerator()得到IEnumeratorobject,在这个意义上,
        将GetEnumerator()看作IEnumerator object的factorymethod也未尝不可。
            
        
 


七、泛型集合练习


    1、把分拣奇偶数的程序用泛型实现。(上面五里面的练习。List<int>)

        private static void Main(string[] args)
        {
            string s = "2 7 8 3 22 9 5 11";
            List<string> li = new List<string>(s.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries));
            List<int> li2 = new List<int>();
            int count = 0;
            foreach (string s2 in li)
            {
                int n = Convert.ToInt32(s2);
                if (n % 2 == 0)
                {
                    li2.Add(n);
                }
                else
                {
                    li2.Insert(count++, n);
                }
            }
            Console.WriteLine(string.Join(",", li2.ToArray()));
            Console.ReadKey();
        }


        
        
    
    2、将int数组中的奇数放到一个新的int数组中返回。

        private static void Main(string[] args)
        {
            int[] n1 = new int[] { 2, 7, 8, 3, 22, 9, 5, 11 };
            int[] n2 = GetOdd(n1);
            Console.WriteLine(string.Join(",", n2));

            Console.ReadKey();
        }

        private static int[] GetOdd(int[] n1)
        {
            List<int> li = new List<int>();
            for (int i = 0; i < n1.Length; i++)
            {
                if (n1[i] % 2 != 0)
                {
                    li.Add(n1[i]);
                }
            }
            return li.ToArray();//直接转
        }


    
    
    3、从一个整数的List<int>中取出最大数(找最大值)。别用max方法。

        private static void Main(string[] args)
        {
            List<int> li = new List<int>(new int[] { 2, 7, 8, 3, 22, 9, 5, 11 });
            Console.WriteLine("{0},{1}", GetMax(li), li.Max());

            Console.ReadKey();
        }

        private static int GetMax(List<int> li)
        {
            int max = int.MinValue;
            foreach (int value in li)
            {
                if (value > max)
                {
                    max = value;
                }
            }
            return max;
        }


    
    
    4、把123转换为: 壹贰叁。 Dictionary<char,char>

        private static void Main(string[] args)
        {
            Dictionary<int, string> dic = new Dictionary<int, string>() { { 1, "壹" }, { 2, "贰" }, { 3, "叁" } };
            Console.WriteLine(dic[1]);
            Console.ReadKey();
        }


    
    
    5、计算字符串中每种字母出现的次数《面试题》。
        "Welcome to Chinaworld",不区分大小写,打印"W 2","e 2","0 3"...
        提示: Dictionarye<char,int>,char的很多静态方法。char.lsLetter()

        private static void Main(string[] args)
        {
            string s = "Welcome to Chinaworld";
            Dictionary<char, int> dic = new Dictionary<char, int>();
            foreach (char x in s)
            {
                if (char.IsLetter(x))
                {
                    if (dic.ContainsKey(char.ToUpper(x)))
                    {
                        dic[char.ToUpper(x)]++;
                    }
                    else if (dic.ContainsKey(char.ToLower(x)))
                    {
                        dic[char.ToLower(x)]++;
                    }
                    else
                    {
                        dic[x] = 1;
                    }

                    //char upper = char.ToUpper(x);
                    //char lower = char.ToLower(x);

                    //if (!dic.ContainsKey(upper) && !dic.ContainsKey(lower))//既不是大写也不是小写
                    //{
                    //    dic[x] = 1;
                    //}
                    //else//剩下的要么大写要么小写
                    //{
                    //    if (dic.ContainsKey(upper))//是大写就在原来的大写上加1
                    //    {
                    //        dic[upper]++;
                    //    }
                    //    else  //否则第一个必定是小写,于是就在小写上加1
                    //    {
                    //        dic[lower]++;
                    //    }
                    //}
                }
            }
            foreach (char c in dic.Keys)
            {
                Console.WriteLine($"{c} {dic[c]}");
            }
            Console.ReadKey();
        }


        
        Char类型的静态方法
            IsControl();是否为控制符
            IsDigit();  是否数字(十进制)
            IsLetter(); 是否字母
            IsLettorOrDigit();
            
            IsNumber();     是否数字
            IsPunctuation();是否标点
            IsWhiteSpace();是否空白符
            IsSymbol();    是否符号 
            IsSeparator(); 是否分隔符
            
            ToLower();
            ToUpper()
            Parse();
            TryParse();
            ToString();
            GetNumericValue();指定数字Unicode字符转为双精度浮点数
            
            
        
    6、简繁体转换 。"一二三四五六七八九十","壹贰叁肆伍陆柒捌玖拾".Dictionary。

        private static void Main(string[] args)
        {
            string s = "一二三四五六七八九十";
            string s1 = "壹贰叁肆伍陆柒捌玖拾";
            Dictionary<string, string> dic = new Dictionary<string, string>();
            for (int i = 0; i < s.Length; i++)
            {
                dic[s[i].ToString()] = s1[i].ToString();
            }
            foreach (string item in dic.Keys)
            {
                Console.WriteLine($"{item}->{dic[item]}");
            }
            Console.ReadKey();
        }


    
    
        下面有两题需要辅助文件实现。下载地址(需要分数)
        https://download.csdn.net/download/dzweather/87761488?spm=1001.2014.3001.5501
        尽管我不需要你点赞,也不需要你打赏,因为我知道查资源的人都懒,一般不会点赞。
        但我需要分数,因为我有时也要下载一些文件。所以只有无耻设置分数了...
    
    
    7、英汉翻译。WinForm做。发英汉词典txt。
        增加点难度,弄个动态自动提示。textBox1是题目所要求。textBox3是动态输入提示。

        public partial class Form1 : Form
        {
            private Dictionary<string, string> dic = new Dictionary<string, string>();
            private List<string> li = new List<string>();//动态单词
            private ListBox lbAuto;                      //动态控件

            public Form1()
            {
                InitializeComponent();
            }

            private void textBox3_TextChanged(object sender, EventArgs e)
            {
                int index = 0;
                if (lbAuto == null)
                {
                    lbAuto = CreateAutoListBox();
                }
                lbAuto.Items.Clear();

                foreach (string item in li)
                {
                    if (textBox3.Text != "" && item.StartsWith(textBox3.Text))
                    {
                        lbAuto.Items.Add(item);
                        index++;
                    }
                }

                label4.Text = lbAuto.Items.Count.ToString();//调试使用

                if (index == 1)
                {
                    textBox3.Text = lbAuto.Items[0].ToString();
                    lbAuto.Visible = false;

                    textBox2.Text = dic[textBox3.Text];
                    textBox3.SelectionStart = textBox3.TextLength;
                }
                else if (index > 1)
                {
                    lbAuto.SelectedIndex = 0;
                    textBox2.Text = dic[lbAuto.SelectedItem.ToString()];
                    lbAuto.Visible = true;
                }
                else
                {
                    lbAuto.Visible = false;
                    textBox2.Text = "";
                }
            }

            private ListBox CreateAutoListBox()
            {
                ListBox lb = new ListBox();

                lb.Name = "lbAuto";
                lb.Location = new Point(textBox3.Location.X, textBox3.Location.Y + textBox3.Height);
                lb.Width = textBox1.Width;
                lb.Height = textBox1.Height * 5;
                lb.Items.Clear();
                lb.Visible = false;

                lb.Click += new EventHandler(lbClick);
                this.Controls.Add(lb);
                lb.BringToFront();

                return lb;
            }

            private void lbClick(object sender, EventArgs e)
            {
                textBox3.Text = lbAuto.SelectedItem.ToString();
                textBox2.Text = dic[textBox3.Text];
                lbAuto.Visible = false;
            }

            private void Form1_Load(object sender, EventArgs e)
            {
                string[] s = File.ReadAllLines(@"E:\英汉词典.txt", Encoding.Default);
                foreach (string s2 in s)
                {
                    string[] s1 = s2.Split(new string[] { " " }, 2, StringSplitOptions.RemoveEmptyEntries);
                    if (!dic.ContainsKey(s1[0]))
                    {
                        dic[s1[0]] = s1[1];
                        li.Add(s1[0]);
                    }
                }
                label4.Text = "加载完成!";
            }

            private void button1_Click(object sender, EventArgs e)
            {
                if (textBox1.Text == "")
                {
                    MessageBox.Show("未输入单词");
                    return;
                }
                if (!dic.ContainsKey(textBox1.Text.Trim()))
                {
                    MessageBox.Show("词典没有该单词");
                }
                else
                {
                    textBox2.Text = dic[textBox1.Text.Trim()].Trim();
                }
            }

            private void textBox1_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                {
                    this.button1_Click(null, null);
                }
            }

            private void textBox3_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter && textBox3.Text != "" && lbAuto.Visible)
                {
                    if (lbAuto.SelectedIndex > -1)
                    {
                        textBox3.Text = lbAuto.SelectedItem.ToString();
                        textBox2.Text = dic[textBox3.Text];
                        lbAuto.Visible = false;
                    }
                }
            }
        }


        
    
    8、火星文翻译器。发字库。(作业。)

        private static void Main(string[] args)
        {
            string s1 = File.ReadAllText(@"E:\简体.txt");
            string s2 = File.ReadAllText(@"E:\繁体.txt");
            Dictionary<char, char> dic = new Dictionary<char, char>();
            for (int i = 0; i < s1.Length; i++)
            {
                dic.Add(s1[i], s2[i]);
            }
            Console.WriteLine("请输入要转换成火星文的语句:");
            string s = Console.ReadLine();
            string p = "";
            for (int i = 0; i < s.Length; i++)
            {
                if (dic.ContainsKey(s[i]))
                {
                    p += dic[s[i]].ToString();
                }
                else
                {
                    p += s[i].ToString();
                }
            }
            Console.WriteLine(p);

            Console.ReadKey();
        }


    
    
    9、编写一个函数进行日期转换,将输入的中文日期转换为阿拉伯数字日期,比如:二零一二年
        十二月二十一日要转换为2012-12-21。

        (处理"十"的问题:1.*月十日;2.*月十三日;3.*月二十三日;4.*月三十日;)
        4中情况对“十”的不同翻译。1->10;2->1;3->不翻译;4->0
        (年部分不可能出现’十’,都出现在了月与日部分。)
        测试数据:
            二零一二年十二月二十一日(2012年12月21日)、二零零九年七月九日、
            二零一零年十月二十四日、二零一零年十月二十日

        private static Dictionary<string, int> dic;

        private static void Main(string[] args)
        {
            dic = new Dictionary<string, int>() { { "零", 0 }, { "一", 1 }, { "二", 2 }, { "三", 3 } };
            dic.Add("四", 4); dic.Add("五", 5); dic.Add("六", 6); dic.Add("七", 7);
            dic.Add("八", 8); dic.Add("九", 9); dic.Add("十", 0);

            string p = "二零一零年十月二十日";
            string[] s = p.Split(new string[] { "年", "月", "日" }, StringSplitOptions.RemoveEmptyEntries);
            string year = GetNum(s[0]);
            string month = GetNum(s[1]);
            month = ConvertNum(month);
            string day = GetNum(s[2]);
            day = ConvertNum(day);

            Console.WriteLine(year + "年" + month + "月" + day + "日");
            Console.ReadKey();
        }

        private static string GetNum(string str)
        {
            string s = "";
            for (int i = 0; i < str.Length; i++)
            {
                s += dic[str[i].ToString()];
            }
            return s;
        }

        private static string ConvertNum(string str)
        {
            int n;
            if (str.StartsWith("0"))//十开头,只能两种:01(10) 0(10)
            {
                if (str.Length > 1)
                {
                    n = 10 + Convert.ToInt32(str);
                }
                else
                {
                    n = 10;
                }
            }
            else if (str.Length > 2)//大于2位,只能如:203(23)
            {
                n = Convert.ToInt32(str);
                n = n / 100 * 10 + n % 100;
            }
            else  //剩下的就是正常值,直接转换
            {
                n = Convert.ToInt32(str);
            }
            return n.ToString();
        }


            
        


八、问题

  下面中1处键入SayHi时会提示有两个重载。而在6处会提示只有一个重载。

        internal class Program
        {
            private static void Main(string[] args)
            {
                Teacher t = new Teacher();
                t.SayHi();  //1
                Console.ReadKey();
            }
        }
        
        internal class Person
        {
            public virtual void SayHi()//2
            {
                Console.WriteLine("基类");
            }
        }

        internal class Student : Person
        {
            public override sealed void SayHi()//3
            {
                Console.WriteLine("子类1");
            }
        }

        internal class Teacher : Student
        {
            public new void SayHi()//4
            {
                Console.WriteLine("孙类2");
            }

            public void Show()//5
            {
                this.SayHi();//6    孙类2
                base.SayHi();//7   子类1
                Person a = this;//试图用最顶层爷类调用
                a.SayHi();//8    子类1    因为sealed堵住,只能在父类处阻断。
            }
        }


        
        3处重写的同时进行了密封。
        4处因3处密封后是不能重写的,但不写new是有警告。
        
        sealed只是冻结了override在Student层。但内部仍然向下在继承。
        所以1处输入时会提示两个重载,但因4处用了new,又将这两个重载其中之一由
        student来的给隐藏了,所以实际上写的就是Teacher.SayHi().
            所以在6处,就直接使用了本身的方法Techer.SayHI().
            
        通过7验证了,虽然被sealed了,但Student.SayHi()仍然继承下来了(只是不准override)
        通过8,可以看到,重载不能直达Teacher了,因为sealed的原因,冻结重写在3处,也
        就是Student.SayHi().
        
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我可以为您解答这个问题。 以下是 Bubble<T> 泛型类的实现: ```java import java.util.ArrayList; public class Bubble<T extends Comparable<T>> { private ArrayList<T> list; public Bubble(ArrayList<T> list) { this.list = list; } public ArrayList<T> sort() { int n = list.size(); for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (list.get(j).compareTo(list.get(j + 1)) > 0) { T temp = list.get(j); list.set(j, list.get(j + 1)); list.set(j + 1, temp); } } } return list; } } ``` 上述代码中,Bubble<T> 类实现了一个泛型类,其中 T extends Comparable<T> 表示 T 类型必须实现 Comparable 接口,以便进行比较排序。该类包含一个 ArrayList 集合对象,可以通过构造函数将一个 ArrayList 对象传递给该类。sort() 方法实现了对集合内数据的冒泡排序,并返回已排序的 ArrayList 对象。 以下是一个使用 Bubble<T> 泛型类的示例: ```java import java.util.ArrayList; import java.util.Arrays; public class Main { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(Arrays.asList(5, 3, 8, 1, 2)); Bubble<Integer> bubble = new Bubble<>(list); ArrayList<Integer> sortedList = bubble.sort(); System.out.println(sortedList); } } ``` 输出结果为:[1, 2, 3, 5, 8]。 在上述示例中,我们创建了一个包含整数的 ArrayList 对象,并将其传递给 Bubble<Integer> 泛型类的构造函数。然后,我们调用 sort() 方法来对集合内的数据进行排序,并将排序后的 ArrayList 对象打印出来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值