# 集合
集合跟前面的数组有点类似,但是数组是一旦创建好,它的大小就是固定的,不能在数组的末尾添加新项。而集合可以添加删除元素。C#中的数组实现为System.Array类的实例,它们只是集合类(Collection Class)中的一种类型,集合类的功能大多实现System.Collections名称空间中的接口而获得的,集合的语法已经标准化了,这个名称空间中还包含其他以不同于System.Array的方式实现这些接口的类。集合类的功能可以通过接口来实现,所以不仅可以使用基本集合类,比如System.Array,还可以创建自己的定制集合类,这些集合可以专用于要枚举的对象,这么做的一个优点是该集合是强类型化的,从集合中提取项时,不需要把它们转换类型,因为我们知道这个集合只有这个类型的,还有一个优点是提供专用于这种类型的方法,比如从一个球类的集合,获取所有的篮球。
System.Collections名称空间中的以下几个接口提供了基本的功能:
- IEnumerable——可以迭代集合中的项。
- ICollection——继承于IEnumerable。可以获取集合中项的个数,并能把项复制到一个简单的数组类型中。
- IList——继承于IEnumerable和ICollection。提供了集合的项列表,允许访问这些项,并提供其他一些与项列表相关的基本功能。
- IDictionary——继承于IEnumerable和ICollection。类似于IList,但提供了可通过键值而不是索引访问的项列表。
System.Array类实现了IList,ICollection和IEnumerable,但不支持IList的一些更高级功能,它表示大小固定的项列表。
# ArrayList
System.Collections名称空间中的类System.Collections.ArrayList也实现了IList,ICollection和IEnumerable接口,它要比Array复杂些,System.Array的大小是固定不变的,而这个类可以用于表示大小可变的项列表。看下面实例:
class Animal
{
string name = "Animal!";
protected int id;
public virtual void Run()
{
Console.WriteLine("Animal run!");
}
}
class Tiger : Animal
{
public Tiger(int idT)
{
id = idT;
}
public override void Run()
{
Console.WriteLine("Tiger Run!");
}
public override string ToString()
{
return "tiger id:"+id;
}
}
class Program
{
static void Main(string[] args)
{
Animal[] animalArray = new Animal[2];
animalArray[0] = new Tiger(3);
animalArray[1] = new Tiger(4);
ArrayList animalArrayList = new ArrayList();
Tiger t1=new Tiger(1);
animalArrayList.Add(t1);
animalArrayList.Add(new Tiger(2));
animalArrayList.AddRange(animalArray);
animalArrayList.RemoveAt(2);
animalArrayList.Remove(t1);
foreach(Animal one in animalArrayList)
{
Console.WriteLine(one);
}
Console.WriteLine(animalArrayList.IndexOf(t1));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# CollectionBase
System.Collections.CollectionBase类,这个抽象类提供了集合类的大量实现代码,它的接口有IEnumerable,ICollection和IList,但只提供了一些必要的实现代码,主要是IList的Clear()和RemoveAt()方法,以及ICollection的Count属性。该类还提供了两个受保护的属性,List和InnerList,使用它们可以访问所存储的对象本身,List可以通过IList接口访问,InnerList用于存储项的ArrayList对象。
# 索引符
索引符(indexer)是一种特殊的属性,可以把它添加到一个类中,以提供类似于数组的访问形式。看下面实例:
class Book
{
public string bookName;
public Book(string name)
{
bookName = name;
}
public override string ToString()
{
return "书名:" + bookName;
}
}
class Library:CollectionBase
{
public void Add(Book book) => List.Add(book);
public void Remove(Book removeBook) => List.Remove(removeBook);
public Book this[int bookId]
{
get
{
return (Book)List[bookId];
}
set
{
List[bookId] = value;
}
}
}
class Program
{
static void Main(string[] args)
{
Book b1 = new Book("历史");
Book b2 = new Book("数学");
Book b3 = new Book("英语");
Library lib = new Library();
lib.Add(b1);
lib.Add(b2);
lib.Add(b3);
lib.Remove(b2);
foreach(Book oneBook in lib)
{
Console.WriteLine(oneBook);
}
Console.WriteLine(lib[1]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# DictionaryBase
除实现IList接口外,还可以实现类似IDictionary接口,允许项通过键值进行索引,而不是根据顺序索引。这是通过IDictionary接口的实现,这个基类就是DictionaryBase,它也实现了IEnumerable和ICollection,提供了对任何集合都相同的基本集合处理功能。看实例:
class Book
{
public string bookName;
public Book(string name)
{
bookName = name;
}
public override string ToString()
{
return "书名:" + bookName;
}
}
class Library:DictionaryBase
{
public void Add(string bookName,Book book) => Dictionary.Add(bookName,book);
public void Remove(string bookName) => Dictionary.Remove(bookName);
public Book this[string bookId]
{
get
{
return (Book)Dictionary[bookId];
}
set
{
Dictionary[bookId] = value;
}
}
}
class Program
{
static void Main(string[] args)
{
Book b1 = new Book("历史");
Book b2 = new Book("数学");
Book b3 = new Book("英语");
Library lib = new Library();
lib.Add("history",b1);
lib.Add("math",b2);
lib.Add("english",b3);
foreach(DictionaryEntry one in lib)
{
Console.WriteLine(one.Key+":"+one.Value);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 迭代器
上面两个例子使用foreach来遍历元素,这是因为DictionaryBase和CollectionBase实现了IEnumerable接口,现在说明下在foreach循环中,迭代一个集合Library的过程:
- 调用Library.GetEnumerator(),返回一个IEnumerator引用,这个方法可通过实现IEnumerable接口的代码来获得,但它是可选的。
- 调用所返回的IEnumerator引用的MoveNext()方法。
- 如果MoveNext()方法返回true,就使用IEnumerator接口的Current属性获取对象的一个引用,用于foreach循环。
- 重复2,直到MoveNext()方法返回false为止。
为了让类能使用foreach,必须重写上面的几个方法,这有些麻烦,但是可以使用迭代器,它能有效地自动生成许多代码,简化流程。
迭代器的定义:它是一个代码块,按顺序提供了要在foreach块中使用的所有值,一般情况下,它是一个方法。无论代码块是什么,其返回类型都是有限制的,这个返回值与所枚举对象类型不同,它是IEnumerable和IEnumerator,它们的应用场合是:
- 如果要迭代一个类,使用方法GetEnumerator(),其返回类型是IEnumerator。
- 如果要迭代一个类成员,例如一个方法,则使用IEnumerable。
下面来看迭代一个方法的例子:
class Program
{
public static IEnumerable SimpleEnumerator()
{
yield return "hello1";
yield return "hello2";
yield break;
yield return "hello3";
}
static void Main(string[] args)
{
foreach (string item in SimpleEnumerator())
{
Console.WriteLine(item);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
下面是修改上面DictionaryBase的例子:
class Book
{
public string bookName;
public Book(string name)
{
bookName = name;
}
public override string ToString()
{
return "书名:" + bookName;
}
}
class Library:DictionaryBase
{
public void Add(string bookName,Book book) => Dictionary.Add(bookName,book);
public void Remove(string bookName) => Dictionary.Remove(bookName);
public new IEnumerator GetEnumerator()
{
foreach(object one in Dictionary.Values)
{
yield return one;
}
}
public Book this[string bookId]
{
get
{
return (Book)Dictionary[bookId];
}
set
{
Dictionary[bookId] = value;
}
}
}
class Program
{
static void Main(string[] args)
{
Book b1 = new Book("历史");
Book b2 = new Book("数学");
Book b3 = new Book("英语");
Library lib = new Library();
lib.Add("history",b1);
lib.Add("math",b2);
lib.Add("english",b3);
foreach(Book one in lib)
{
Console.WriteLine(one);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
使用迭代器来查找某个区间的所有素数:
class PrimeNumber
{
private int min;
private int max;
public PrimeNumber()
{
}
public PrimeNumber(int min, int max)
{
if(min < 2)
{
throw (new ArgumentOutOfRangeException("max over 2,actual value is:", min,"change it!"));
}
if(min >= max)
{
throw (new ArgumentOutOfRangeException("min is bigger than max","change it!"));
}
this.min = min;
this.max = max;
}
public IEnumerator GetEnumerator()
{
for(int i = min; i <= max; i++)
{
bool isPrime = true;
for(int j = 2; j <= Math.Floor(Math.Sqrt(i)); j++)
{
if (i % j == 0) { isPrime = false; break; }
}
if (isPrime)
{
yield return i;
}
}
}
}
class Program
{
static void Main(string[] args)
{
PrimeNumber primeNum = new PrimeNumber(2, 100);
foreach(int i in primeNum)
{
Console.WriteLine(i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 比较
# is运算符
在比较对象时常需要确定它们的类型,我们之前用GetType()和typeof联合一块使用,但是还有很多其他复杂的情况,比如判断从基类派生的子类和孙类是否属于这个基类,这里就涉及到is运算符。
is运算符并不是用来说明对象是某种类型,而是用来检查对象是不是给定类型,或者是否可以转换为给定类型,如果是就返回true。
它的语法如下:
sourceVariable is <targetType>
如果<targetType>是一个类类型,而sourceVariable也是该类型,或者它继承了该类型,或者它可以封箱到该类型中,返回True。 如果<targetType>是一个接口类型,而sourceVariable也是该类型,或者它是实现该接口的类型,则返回True。 如果<targetType>是一个值类型,而sourceVariable也是该类型,或者它可以拆箱到该类型中,则返回True。
interface IMyInterface{}
struct MyStruct : IMyInterface
{
public int val;
}
class MyClass1 : IMyInterface{}
class MyClass2 : MyClass1{}
class Program
{
static void Main()
{
object m1 = new MyStruct();
MyClass1 c1 = new MyClass1();
MyClass2 c2 = new MyClass2();
if(c2 is MyClass1)
{
Console.WriteLine("c2 is MyClass1!");
}
if(c2 is IMyInterface)
{
Console.WriteLine("c2 is IMyInterface");
}
if(m1 is MyStruct)
{
Console.WriteLine("m1 is MyStruct!");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# is模式匹配
is运算符是进行模式匹配和过滤数据集的一种非常有效的技术,在下面的语法下,如果mc1是MyClass就创建一个MyClass的新变量mc11:
class MyClass
{
public string name;
public MyClass(string n)
{
name = n;
}
}
class Program
{
static void Main()
{
object mc1 = new MyClass("hello");
if(mc1 is MyClass mc11)
{
Console.WriteLine(mc11.name);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 比较值
要比较自定义的类有两种方法,一种是使用运算符重载,另一种是使用IComparable和IComparer接口,.Net Framework的各种集合类也支持这种方式,使得他们成为对集合中的对象进行排序的一种极佳的方式。
# 运算符重载
通过运算符重载(operator overloading),可以对我们自己定义的类使用标准的运算符,比如>,<。看下面实例:
public class MyClass1
{
public int val;
public MyClass1(int value)
{
val = value;
}
public static MyClass3 operator +(MyClass1 c1,MyClass2 c2)
{
return new MyClass3(c1.val + c2.val);
}
public static bool operator >(MyClass1 c1,MyClass2 c2)
{
return c1.val > c2.val;
}
public static bool operator < (MyClass1 c1,MyClass2 c2)
{
return c1.val < c2.val;
}
}
public class MyClass2
{
public int val;
public MyClass2(int value)
{
val = value;
}
}
public class MyClass3
{
public int val;
public MyClass3(int value)
{
val = value;
}
}
class Program
{
static void Main(string[] args)
{
MyClass1 c1 = new MyClass1(10);
MyClass2 c2 = new MyClass2(12);
MyClass3 c3 = c1 + c2;
//MyClass3 c3 = c2 + c1;
//MyClass2 c2 = c1 + c2;
Console.WriteLine(c1 < c2);
Console.WriteLine(c3.val);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
注意可重载的运算符有:
- 一元运算符:+,-,!,~,++,--,true,false
- 二元运算符:+,-,*,/,%,&,|,^,<<,>>
- 比较运算符:==,!=,<,>,<=,>=
如果重载true和false运算符,就可以在布尔表达式中用,比如if(MyClass1){}。
不能重载的运算符有:+=,&&和||。但是有相应的+,&,|替代。重载==或!=时通常也要重写Object.Equals()和Object.GetHashCode(),这保证了在所有情景都能有相同的表现。
public class MyClass1
{
public int val;
public MyClass1(int value)
{
val = value;
}
public static bool operator ==(MyClass1 c1, MyClass1 c2)
{
return c1.val == c2.val;
}
public static bool operator !=(MyClass1 c1, MyClass1 c2)
{
return c1.val != c2.val;
}
public override bool Equals(object obj)
{
Console.WriteLine($"use Equals,self value:{val};compare value:{((MyClass1)obj).val}");
if (obj is MyClass1)
{
return val == ((MyClass1)obj).val + 2;
}
return false;
}
public override int GetHashCode()
{
Console.WriteLine($"Use GetHashCode function,object value is:{val}");
return val;
}
}
class Program
{
static void Main(string[] args)
{
MyClass1 c1 = new MyClass1(10);
MyClass1 c2 = new MyClass1(12);
ArrayList arrayList = new ArrayList();
Console.WriteLine(c1 == c2);
arrayList.Add(c1);
arrayList.Add(c2);
Console.WriteLine(arrayList.IndexOf(c1));
Console.WriteLine("array 0 element is :"+((MyClass1)arrayList[0]).val);
Hashtable ht = new Hashtable();
ht.Add(c1,"1");
ht.Add(c2,"2");
Console.WriteLine(ht.Contains(c1));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
从上面的实例是否看出为什么要重写Equals了,因为很多容器类中就是使用Equals方法来判断自定义类的。最好就是在重载运算符==和!=中调用Equals做二次封装。
# IComparable接口
IComparable和IComparer接口是 .Net Framework中比较对象的标准方式。它们的区别如下:
- IComparable在要比较的对象的类中实现CompareTo方法,可以比较该对象和另一个对象。
- IComparer在一个单独的类中实现Compare方法,可以比较任意两个对象。
一般是使用IComparable给出类的默认比较代码,使用其他类给出非默认的比较代码。一些集合类对其中对象进行排序时就需要这两种实现方法。下面看实例:
class Person:IComparable
{
public int age;
public string name;
public Person(int ageT,string nameT)
{
age = ageT;
name = nameT;
}
public int CompareTo(object obj)
{
if(obj is Person tg)
{
int ageResult = age - tg.age;
if (ageResult == 0)
{
return Comparer.Default.Compare(name,tg.name);
}
else
{
return ageResult;
}
}
else
{
throw new ArgumentException("Can not compare not Person class!");
}
return 0;
}
public override string ToString()
{
return $"age:{age},name:{name}";
}
}
public class PersonComparer : IComparer
{
public static IComparer Default = new PersonComparer();
public int Compare(object x, object y)
{
if(x is Person source && y is Person target)
{
int ageResult = target.age - source.age;
if (ageResult == 0)
{
return Comparer.Default.Compare(target.name, source.name);
}
else
{
return ageResult;
}
}
else
{
throw new ArgumentException("compare objects need be Person objects");
}
}
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Person(15, "x1");
Person p2 = new Person(17, "x2");
Person p3 = new Person(15, "x3");
Person p4 = new Person(24, "x4");
Console.WriteLine(p1.CompareTo(p2));
Console.WriteLine(PersonComparer.Default.Compare(p1,p3));
ArrayList list = new ArrayList();
list.Add(p1);
list.Add(p2);
list.Add(p3);
list.Add(p4);
list.Sort();
foreach(Person onePerson in list)
{
Console.WriteLine(onePerson);
}
Console.WriteLine("----------Use PersonComparer!---------");
list.Sort(PersonComparer.Default);
foreach (Person onePerson in list)
{
Console.WriteLine(onePerson);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
.Net Framework在类Comparer上提供了IComparer接口的默认代码,这个类位于System.Collection名称空间中,可以对简单类型以及支持IComparable接口的任意类型进行特定文化的比较。
static void Main(string[] args)
{
string str1 = "abc";
string str2 = "bcd";
Console.WriteLine(Comparer.Default.Compare(str1,str2));
}
2
3
4
5
6
注意:
- 检查传递给Comparer.Default.Compare()的对象,看看它们是否支持IComparable,如果支持就使用该实现代码。
- 允许使用null值,它被解释为"小于"其他任意对象。
- 字符串根据当前本地化规则来处理,要使用不同的本地化规则处理字符串,Comparer类必须使用其构造函数进行实例化,以便传送用于指定本地化规则的System.Globalization.CultureInfo对象。
- 字符串处理时会区分大小,如果要忽略大小写,需要使用CaseInsensitiveComparer类。
# 封箱拆箱
# 封箱
封箱是把值类型转换为System.Object类型,或者转换为由值类型实现的接口类型。它的内部机制如下:
- 在堆中开辟内存空间。
- 将值类型的数据复制到堆中。
- 返回堆中新分配对象的地址。
# 拆箱
从object类型到值类型或从接口类型到实现该接口的值类型的显示转换。它的内部机制:
- 判断给定类型是否是装箱时的类型。
- 返回已装箱实例中属于原值类型字段的地址。
# 实例说明
struct MyStruct
{
public int value;
}
class MyClass
{
public int value;
}
class Program
{
static void Main()
{
MyStruct myStruct = new MyStruct();
myStruct.value = 100;
object myStructObj = myStruct;
myStruct.value = 1000;
Console.WriteLine(((MyStruct)myStructObj).value);
MyClass myClass = new MyClass();
myClass.value = 100;
object myClassObj = myClass;
myClass.value = 1000;
Console.WriteLine(((MyClass)myClassObj).value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
装箱到一个接口中:
interface IMyInterface
{
}
struct MyStruct : IMyInterface
{
public int val;
}
class Program
{
static void Main()
{
MyStruct myStruct = new MyStruct();
myStruct.val = 100;
IMyInterface refType = myStruct;
myStruct.val = 1000;
Console.WriteLine(((MyStruct)refType).val);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:
- 以装箱方式创建的对象,包含值类型变量的一个副本引用,而不包含原值类型变量的引用。相对比来说引用类型做同样的操作却是一直引用同一对象。
- 拆箱后的类型必须与装箱时的类型相同。
- 伴随拆箱的字段复制步骤不属于拆箱过程。
- 装箱和拆箱不是互逆过程,装箱的性能开销远大于拆箱。
# 意义
封箱有两个非常重要的原因:
- 允许项的类型是object的集合(比如ArrayList)中使用值类型。
- 有一个内部机制允许在值类型(比如int和结构)上调用object方法。
# 转换
在将我们自定义的类转换为其他类时,可重载转换运算符,类类型之间有显式和隐式转换。
class ConvertClass1
{
public int val;
public ConvertClass1(int value)
{
val = value;
}
public static implicit operator ConvertClass2(ConvertClass1 op1)
{
Console.WriteLine("Convert ConvertClass1 to ConvertClass2!value:"+op1.val);
return new ConvertClass2(op1.val);
}
}
class ConvertClass2
{
public int val;
public ConvertClass2(int value)
{
val = value;
}
public static explicit operator ConvertClass1(ConvertClass2 op2)
{
Console.WriteLine("Convert ConvertClass2 to ConvertClass1!value:"+op2.val);
return new ConvertClass1(op2.val);
}
}
class Program
{
static void Main(string[] args)
{
ConvertClass1 c1 = new ConvertClass1(10);
ConvertClass2 c2 = new ConvertClass2(20);
//ConvertClass1 c3 = c2;
ConvertClass1 c3 = (ConvertClass1)c2;
Console.WriteLine(c3.val);
//显式转换也可以做隐式转换。
ConvertClass2 c4 = c1;
ConvertClass2 c5 = (ConvertClass2)c1;
Console.WriteLine(c4.val);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# as运算符
as运算符能把一种类型转换为指定的引用类型:
operObj as 目标类型
它只适用于如下情况:
- operObj的类型是目标类型
- operObj可以隐式转换为目标类型
- operObj可以封箱到目标类型
如果不能将operObj转换为目标类型,则表达式的结果就是null。
interface IMyInterface
{
}
class ClassOne : IMyInterface
{
}
class ClassTwo : ClassOne
{
}
static void Main(string[] args)
{
ClassOne obj1 = new ClassOne();
ClassTwo obj2 = new ClassTwo();
ClassTwo obj3 = obj1 as ClassTwo;
Console.WriteLine($"派生类引用=基类对象as派生类,obj3:{obj3}");
//ClassTwo obj6 = obj1 as ClassOne;
//Console.WriteLine($"派生类引用=基类对象as基类,obj3:{obj3}");
//提示缺乏ClassOne To ClassTwo的显式转换。
ClassOne obj4 = obj2 as ClassTwo;
Console.WriteLine($"基类引用=派生类对象as派生类,obj4:{obj4}");
ClassOne obj5 = obj2 as ClassOne;
Console.WriteLine($"基类引用=派生类对象as基类,obj5:{obj5}");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
像上面的obj3,如果要是简单类型转换的话会抛出一个异常,但是如果使用as的话,就可以将一个null值赋给obj3,判断这个值是否为null要比捕捉异常简单。