一、.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)集合可以通过equals和comparTo方法自定义元素的比较标准,实现任何类型
的元素的比较。 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.youkuaiyun.com/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.youkuaiyun.com/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.youkuaiyun.com/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().