# 基本定义

# 类的定义

定义格式如下:

[访问级别][功能] class [ClassName]:[BaseClassName],[intefaceName]
{

}
1
2
3
4

注意点如下:

  • 访问级别有两个:
    • 无修饰符或internal:它是仅包内可访问,也就是仅在当前程序集内可访问,其他程序集不能访问。
    • public:该类是公开的,其他程序集可访问。
  • 功能性修饰符有两个:
    • abstract:把类声明为抽象的,这种类只能被继承不能被实例化,可以有抽象成员。
    • sealed:将类声明为密封的,这种类不能被继承,只能被实例化。
  • 在C#中,每个类只能有一个基类,如果一个类继承了一个抽象类,除非该类也要被设计成是抽象的,否则该类必须实现所继承的所有抽象成员。
  • 编译器不允许派生类的访问级别高于基类,如果基类的访问级别是internal,那么它的派生类不能是public。
  • 如果一个类没有使用基类,在C#中该类只继承于基类System.Object,在C#中它的别名是object。
  • 一个类除了指定基类外,可能还要指定接口,注意基类名必须是冒号后第一个,必须在接口名前面。
  • 一个类如果要实现一个接口,必须实现接口的所有成员。如果不想实现给定的接口成员,一种方法是在类中实现接口空的方法,另一种方法是把接口成员实现为抽象类中的抽象成员。

# 接口的定义

它的格式如下:

[访问级别] interface [interfaceName1],[interfaceName2]...[interfaceNameN]
{

}
1
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)){

}
1
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);
    }
}
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

# 析构函数

下面来看下面样例:

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();
    }

}
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

它的运行结果是先执行派生类的析构函数,再执行基类的析构函数。

# 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();
    }

}
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

上面是手动调用Dispose方法来进行垃圾清理,这样 .Net中实际有另种方法来进行释放资源了,它们有什么区别呢?

  1. Finalize的目的是只用于释放非托管资源,Dispose的目的是释放所有资源,包括托管资源和非托管资源。
  2. Finalize由GC执行,而Dispose则是手动执行的,在执行了Dispose后已释放所有资源,可能不需要执行Finalize,此时可以使用GC.SuppressFinalize(obj),来告诉GC不要执行obj的Finalize函数了。

# 接口和抽象类

它们的相同之处是接口和抽象类都不能直接实例化,但可以被派生类实现和继承。

它们的不同之处:

  1. 派生类只能继承一个类,但是它可以实现多个接口。
  2. 抽象类可以拥有抽象成员和非抽象成员。但是接口必须只能有接口,只能由实现它们的类实现接口。

从应用场景上看,抽象类主要用作某些对象的基类,这些对象共享某些主要特性,比如有共同的目的和结构。而接口主要用于一些类,这些类可能存在根本性的区别,但仍可以完成某些任务。举个例子,比如动物和人,一般会有两个抽象基类,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}");

 }

}
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

这两个结果完全不同,操作对象是引用类型时,对该类型变量赋值,实际上是把一个指针变量的值赋给了目标指针变量。而指针变量的值是内存中的地址,所以这两个指针变量指向内存中同一个对象,对其中一个指针变量操作等同于对另一个指针变量操作。而操作对象是值类型时,相当于把一个对象在内存中的值赋值给另一个对象,它们是两个不同的内存空间。

# 深度复制和浅度复制

注意从一个变量到另一个变量按值复制,而不是按引用复制会非常复杂,因为这样的对象可能包含许多其他对象的引用,在 .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);
}
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