# 基本定义
# 类的定义
定义格式如下:
[访问级别][功能] class [ClassName]:[BaseClassName],[intefaceName]
{
}
2
3
4
注意点如下:
- 访问级别有两个:
- 无修饰符或internal:它是仅包内可访问,也就是仅在当前程序集内可访问,其他程序集不能访问。
- public:该类是公开的,其他程序集可访问。
- 功能性修饰符有两个:
- abstract:把类声明为抽象的,这种类只能被继承不能被实例化,可以有抽象成员。
- sealed:将类声明为密封的,这种类不能被继承,只能被实例化。
- 在C#中,每个类只能有一个基类,如果一个类继承了一个抽象类,除非该类也要被设计成是抽象的,否则该类必须实现所继承的所有抽象成员。
- 编译器不允许派生类的访问级别高于基类,如果基类的访问级别是internal,那么它的派生类不能是public。
- 如果一个类没有使用基类,在C#中该类只继承于基类System.Object,在C#中它的别名是object。
- 一个类除了指定基类外,可能还要指定接口,注意基类名必须是冒号后第一个,必须在接口名前面。
- 一个类如果要实现一个接口,必须实现接口的所有成员。如果不想实现给定的接口成员,一种方法是在类中实现接口空的方法,另一种方法是把接口成员实现为抽象类中的抽象成员。
# 接口的定义
它的格式如下:
[访问级别] interface [interfaceName1],[interfaceName2]...[interfaceNameN]
{
}
2
3
4
注意接口不是类,没有继承System.Object。
# System.Object
因为所有类都继承System.Object,所有这些类都可以访问类中受保护的成员和公共成员。System.Object包含的方法如下:
方法名 | 返回类型 | 虚拟 | 静态 | 解释 |
---|---|---|---|---|
Object() | 无 | 否 | 否 | System.Object类型的构造函数,由派生类型的构造函数自动调用 |
~Object() 或 Finalize() | 无 | 否 | 否 | System.Object类型的析构函数,由派生类的析构函数自动调用,不能手动调用 |
Equals(object) | bool | 是 | 否 | 把调用该方法的对象与另一个对象相比,如果相等则返回true,默认是比较对象参数是否引用了同一个对象,如果想以不同的方式来比较,比如比较对象的状态,可以重写该方法。 |
Equals(object,object) | bool | 否 | 是 | 该方法比较两个对象参数,使用了Equals(object)方法,注意如果两个对象都是空引用,这个方法返回true。 |
ReferenceEquals(object,object) | bool | 否 | 是 | 这个方法比较两个对象看它们是不是一个实例的引用。 |
ToString() | string | 是 | 否 | 返回一个对应于对象实例的字符串。默认情况下,是一个类类型的限定名称,可以重写它,给类类型提供合适的实现代码。 |
MemberwiseClone() | object | 否 | 否 | 通过创建一个新对象实例并复制成员,以复制该对象。成员复制不会得到这些成员的新实例,新对象的任何引用类型成员都将引用与源类相同的对象,这个方法是受保护的,所以只能在类或派生的类中使用。 |
GetType() | System.Type | 否 | 否 | 返回对象的类型 |
GetHashCode() | int | 是 | 否 | 用作对象的散列函数,它返回一个以压缩形式标识对象的值。 |
可以组合使用GetType()和typeof来判定目标是否某个类类型:
if(myObj.GetType() == typeof(MyClass)){
}
2
3
# 构造函数和析构函数
在C#中会自动提供默认构造函数,我们也可以自己定义有参数的构造函数,另外在 .Net中使用析构函数称为Finalize(),但是定义析构函数需要使用~类名定义,注意不能通过对象显式调用析构函数,只能通过GC来自动调用析构。
# 构造函数
在C#中都是先执行基类的构造函数,再执行自身的构造函数,一般总是最先执行System.Object.Object()方法,在定义构造函数时还有两个关键字base和this,用于在构造函数后调用基类的构造函数和自身的构造函数,如果没有这两个关键字,默认调用基类的默认构造函数。请看下例:
class BaseClass
{
public BaseClass()
{
Console.WriteLine("Base default construct");
}
protected BaseClass(int i) {
Console.WriteLine($"Base construct int:{i}");
}
}
class DerivedClass:BaseClass
{
public DerivedClass():base(4)
{
Console.WriteLine("Derived default construct!");
}
public DerivedClass(int i):this()
{
Console.WriteLine($"Derived construct int:{i}!");
}
public DerivedClass(int i,int j):this(3)
{
Console.WriteLine($"Derived construct int:{i},{j}!");
}
}
class Program
{
static void Main(string[] strs)
{
DerivedClass d1 = new DerivedClass(1,2);
}
}
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
# 析构函数
下面来看下面样例:
class BaseClass
{
~BaseClass()
{
Console.WriteLine($"Base finalize!");
}
}
class DerivedClass:BaseClass
{
~DerivedClass()
{
Console.WriteLine($"Derived finalize!");
}
}
class Program
{
static void TestFinalize()
{
DerivedClass d1 = new DerivedClass();
}
static void Main(string[] strs)
{
TestFinalize();
GC.Collect();
Console.ReadKey();
}
}
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
它的运行结果是先执行派生类的析构函数,再执行基类的析构函数。
# IDisposable接口
由于析构函数是由GC调用的,在程序运行过程中什么时候执行是不确定的,如果把上例中的GC.Collect()去掉,那么可能得等待几分钟之后才会执行析构函数。
为了用一种更可控的手段对内存进行管理,C#提供了另一种方法,就是IDisposable接口,每个类实现这个接口,然后实现Dispose方法,就可以手动调用清理内存。看下面的实例:
class BaseClass
{
~BaseClass()
{
Console.WriteLine($"Base finalize!");
}
}
class DerivedClass:BaseClass,IDisposable
{
~DerivedClass()
{
Console.WriteLine($"Derived finalize!");
}
public void Dispose()
{
Console.WriteLine("Derived Class dispose method!");
GC.SuppressFinalize(this);
}
}
class Program
{
static void TestFinalize()
{
DerivedClass d1 = new DerivedClass();
d1.Dispose();
//GC.SuppressFinalize(d1);
}
static void Main(string[] strs)
{
TestFinalize();
using(DerivedClass d1 = new DerivedClass())
{
}
GC.Collect();
Console.ReadKey();
}
}
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
上面是手动调用Dispose方法来进行垃圾清理,这样 .Net中实际有另种方法来进行释放资源了,它们有什么区别呢?
- Finalize的目的是只用于释放非托管资源,Dispose的目的是释放所有资源,包括托管资源和非托管资源。
- Finalize由GC执行,而Dispose则是手动执行的,在执行了Dispose后已释放所有资源,可能不需要执行Finalize,此时可以使用GC.SuppressFinalize(obj),来告诉GC不要执行obj的Finalize函数了。
# 接口和抽象类
它们的相同之处是接口和抽象类都不能直接实例化,但可以被派生类实现和继承。
它们的不同之处:
- 派生类只能继承一个类,但是它可以实现多个接口。
- 抽象类可以拥有抽象成员和非抽象成员。但是接口必须只能有接口,只能由实现它们的类实现接口。
从应用场景上看,抽象类主要用作某些对象的基类,这些对象共享某些主要特性,比如有共同的目的和结构。而接口主要用于一些类,这些类可能存在根本性的区别,但仍可以完成某些任务。举个例子,比如动物和人,一般会有两个抽象基类,Person和Animal。但是可以从它们身上抽象很多共同的接口,比如Eat,Run,Sleep。
# 类和结构体
前面说了类和结构体很多不同的地方,但是最根本的是类是引用类型,而结构体是值类型。这意味着什么呢?看下面的例子:
class myClass
{
public int val;
}
struct myStruct
{
public int val;
}
class Program
{
static void Main(string[] strs)
{
myClass myClass1 = new myClass();
myClass1.val = 10;
myClass myClass2 = myClass1;
myClass2.val = 20;
Console.WriteLine($"myClass1 val:{myClass1.val},myClass2 val:{myClass2.val}");
myStruct myStruct1 = new myStruct();
myStruct1.val = 10;
myStruct myStruct2 = myStruct1;
myStruct2.val = 20;
Console.WriteLine($"myStruct1 val:{myStruct1.val},myStruct2 val:{myStruct2.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
这两个结果完全不同,操作对象是引用类型时,对该类型变量赋值,实际上是把一个指针变量的值赋给了目标指针变量。而指针变量的值是内存中的地址,所以这两个指针变量指向内存中同一个对象,对其中一个指针变量操作等同于对另一个指针变量操作。而操作对象是值类型时,相当于把一个对象在内存中的值赋值给另一个对象,它们是两个不同的内存空间。
# 深度复制和浅度复制
注意从一个变量到另一个变量按值复制,而不是按引用复制会非常复杂,因为这样的对象可能包含许多其他对象的引用,在 .Net Framework中,简单地按照引用复制对象可以通过System.Object的MemberwiseClone()方法来完成,这是一个protected方法,C#中的所有对象都可以创建一个public方法来调用该方法,以这种方式复制称为浅度复制(swallow copy),这种方法并未考虑引用类型的成员,新对象中的引用成员会指向源对象中的成员。
要想进行深度复制(deep copy),也就是复制值而不是复制引用,可以实现一个ICloneable接口,以标准方式进行深度复制,实现Clone()方法,这个方法会返回一个System.Object的值,可以在这个方法内实现深度复制的操作。
public class ContentClass
{
public int Val;
}
public class ClonerClass1
{
public ContentClass sourceClass=new ContentClass();
public ClonerClass1(int val) => this.sourceClass.Val = val;
public object GetCopy() => MemberwiseClone();
}
public class ClonerClass2:ICloneable
{
public ContentClass sourceClass = new ContentClass();
public ClonerClass2(int val) => this.sourceClass.Val = val;
public object Clone()
{
ClonerClass2 target = new ClonerClass2(sourceClass.Val);
return target;
}
}
static void Main(string[] args)
{
ClonerClass1 cc1 = new ClonerClass1(10);
ClonerClass1 cc1C = (ClonerClass1)cc1.GetCopy();
cc1C.sourceClass.Val = 100;
Console.WriteLine(cc1.sourceClass.Val);
ClonerClass2 cc2 = new ClonerClass2(10);
ClonerClass2 cc2C = (ClonerClass2)cc2.Clone();
cc2C.sourceClass.Val = 100;
Console.WriteLine(cc2.sourceClass.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