C#中的OOP 技术
1.类和接口的定义语法
首先是类的定义语法
using System;
using System.Collections.Generic;
using System.Text;
namespace OOP_Example_Program
{
/// 在C#中,abstract 关键字和 sealed 关键字是互斥的(实际上在所有的OOP语言中这两者都是互斥的)
/// 同时抽象类和密封类都可以是公开类和内部类
/// 总的来说公开类和内部类只是指定类的访问范围,而抽象和密封实际上是在描述类的行为
///
/// <summary>
/// 下面的类为默认创建的类,一般为内部的与之后使用internal关键字
/// 指定的类其实是相同的。
/// 默认创建的类一般只能供项目内代码访问,这也是internal关键字的含义
/// </summary>
class myClass
{
}
internal class myClass_2
{
}
/// <summary>
/// 当类被指定为public时,可供项目之外的代码访问
/// </summary>
public class myClass_3
{
}
/// <summary>
/// 使用 abstract 关键字修饰的类为抽象类,这种类只能被继承
/// 不能创建实例(即对象)
/// </summary>
public abstract class myClass_4
{
}
/// <summary>
/// 使用sealed关键字创建的类为密封的,密封类是不可继承的类
///
/// </summary>
public sealed class myClass_5
{
}
}
接下来是接口的定义语法
using System;
using System.Collections.Generic;
using System.Text;
namespace OOP_Example_Program
{
interface IMyInterface
{
}
interface IMyInterface_2
{
}
}
综合以上两点可以看出类和接口的定义并不困难,其主要难度在于对修饰符(关键字,keyword)的理解
下面是用于类定义的修饰符列表及其含义
修饰符(keyword) | 含义 |
---|---|
无或internal | 只能在当前项目中访问类 |
public | 可以在任何地方访问类 |
abstract 或 internal abstract | 类之恩呢在当前项目中访问,不能实例化,只能被继承 |
public abstract | 类可以在任何地方被访问,不能实例化,只能被继承 |
sealed 或 internal sealed | 类只能在当前项目中访问,不能被继承,只能实例化 |
public sealed | 类可以在任何地方访问,不能被继承,只能实例化 |
用于接口的修饰符列表及其含义
修饰符(keyword) | 含义 |
---|---|
无或internal | 只能在当前项目中访问接口 |
public | 可以在任何地方访问接口 |
abstract 和 sealed 关键字不可用于接口,这是十分合理的,因为在OOP中接口存在的意义就是被继承,且接口不可实例化,只能建立接口类型的变量。这里对实例化和建立变量做区分如下
实例化
string helloworld = new string("HelloWorld!");
建立变量
int a;
注:接口不是类,自然不继承自System.Object,但是为了实现接口的多态性,c#中允许接口类型的变量访问object成员。
2.继承
using System;
using System.Collections.Generic;
using System.Text;
namespace OOP_Example_Program
{
class Inherit
{
}
/// <summary>
/// 指定类Inherit_kid继承自Inherit类
/// 这里主要展示继承的语法
/// 注意:每个类只能继承一个基类,但是可以连接多个接口
/// 同时注意c#中不允许派生类的可访问性(public 、 internal)高于基类
/// 如果没有使用继承语法指定类的继承关系,那类默认是继承自System.Object(object)
/// 在C#的继承层次结构中所有的类都继承自object
///
/// 此处除了展示继承的语法之外也展示类连接接口的语法,可以看到接口同样是写到了冒号之后,但是这里有限制
/// 指定继承时基类必须先于接口填写
/// 同时这里也体现了类可以继承多个接口(在OOP中连接接口一般也称为继承)
/// </summary>
class Inherit_kid : Inherit, IMyInterface, IMyInterface_2
{
}
}
3.接口和继承使用实例
using System;
namespace OOP_Example_Program
{
public abstract class MyBase
{
}
internal class MyClass : MyBase { }
public interface IMyBaseInterface { }
internal interface IMyBaseInterface2 { }
internal interface IMyInterfaceExample : IMyBaseInterface, IMyBaseInterface2 { }
internal sealed class MyComplexClass : MyClass, IMyInterfaceExample { }
class Program
{
static void Main(string[] args)
{
MyComplexClass myObj = new MyComplexClass();
Console.WriteLine(myObj.ToString());
Console.WriteLine("Hello World!");
}
}
}
上例中的ToString()函数是Object类的一个方法。
4.构造函数和析构函数
C#中的构造函数和析构函数可以不明确指定,当进行实例化和垃圾回收时就会默认调用Object中的构造函数和析构函数。
这里从定义方法和执行顺序两个方面对构造函数和析构函数的相关操作进行总结。
4.1 定义方法
下面的示例中给出了常用的构造函数和析构函数定义方法。可以主义到构造函数用了两个,事实上构造函数的个数不受限制,因为构造函数的任务是对实例进行初始化,而实例的初始化往往是多样的,有时候提供参数有时候不提供参数,有时候提供这些参数有时候提供那些参数。
实际上需要探讨的是public 这类修饰符。
在OOP中public 和 private 用于控制函数的访问性质,如果一个函数被private修饰,该函数仅能供类内部访问,在此处如果对默认构造函数使用private修饰,可以强制用户使用非默认构造函数创建对象。如果一个类没有公有构造函数,那这个类就是不可创建的类。
class myClass
{
public myClass()
{
// Default constructor code.
}
public myClass(int myInt)
{
// Nondefault constructor code (uses myInt).
}
~myClass()
{
//Destructor body.
}
}
4.2 执行顺序
析构函数是默认在C#的垃圾回收机制中执行的函数,当进行垃圾回收时,首先执行析构函数中的代码,然后执行System.Object中的Finalize()调用,这项技术可以保证根类中析构函数的执行。
构造函数的执行顺序与析构函数正好相反。
构造函数的执行顺序是自上而下的,以3中的程序为例,构造函数的执行顺序如下:
- System.Object
- OOP_Example_Program.MyBase
- OOP_Example_Program.MyClass
- OOP_Example_Program.MyComplexClass
在讨论构造函数执行顺序时,应注意base、this这两个关键字的应用,总的来说这两个关键字用于改变进行构造和析构时函数的执行顺序(当然也可进行其他操作)。
在讨论构造函数的执行顺序之前给出一段示例代码
public class MyBaseClass
{
public MyBaseClass()
{
}
public MyBaseClass(int i)
{
}
}
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass()
{
}
public MyDerivedClass(int i)
{
}
public MyDerivedClass(int i, int j)
{
}
}
这里不在给出常见的构造函数执行顺序,转而给出几个不常见的执行顺序,并讨论怎样实现。
首先是:
- System.Object.Object()
- MyBaseClass.MyBaseClass(int i )
- MyDerivedClass.MyDerivedClass(int i, int j)
可以看到,我们想在进行构造是,指定不使用父类的默认构造函数,转而使用其非默认构造函数,这里需要使用base关键字
只需要对子类中的非默认构造函数进行一些限定即可
public class MyDerivedClass
{
...
public MyDerivedClass(int i, int j) : base(i)
{
}
}
其中base关键字指定.NET实例化过程中使用基类中具有指定参数的构造函数。
除了base关键字外,还可将另一个关键字this用作构造函数初始化器。
假定我们需要这样的执行顺序:
- System.Object.Object()
- MyBaseClass.MyBaseClass()
- MyDerivedClass.MyDerivedClass(int i, int j)
- MyDerivedClass.MyDerivedClass()
只需要对子类中的默认构造函数进行一些限定
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass() : this(5,6)
{
}
public MyDerivedClass(int i)
{
}
public MyDerivedClass(int i, int j) : base(i)
{
}
}
唯一的限制是使用构造函数初始化器只能指定一个构造函数。
注意:在定义构造函数是不要创建无限循环。
5.多态性
C#中OOP的多态性体现类和接口的使用方式上。
从类的角度:利用父类实例(对象)可以调用子类实例(对象)中继承自父类的方法、属性和字段
从接口的角度:利用接口变量(注意此处的不同描述,接口不可实例化)可以调用实现该接口的类的对象中接口中包含的方法(可能有点绕,实践一下就好)
6.抽象类
抽象类和接口有很多共同点,下面用一个表来对比。
名称 | 是否可实例化 | 是否可创建变量 | 是否可访问派生类的变量 |
---|---|---|---|
抽象类 | 否 | 是 | 是 |
接口 | 否 | 是 | 是 |
抽象类和接口虽然不能实例化,但是可以通过创建变量来使用多态性(OOP中的多态性体现在接口和类上,总的来说是可以以上调下,但是只能调用继承来的方法、属性和字段)
子类只可以继承一个基类,因此只可以继承一个抽象类,但是可以在一个继承链中包含多个抽象类。此处需要注意抽象类定义,抽象类中是可以包含虚拟成员,当然也可以包含实现代码,这并不冲突。
至于接口,其成员必须在使用接口的类中实现,因为它们没有代码体。除此之外,接口成员是public的,因为它们存在的意义是供外部访问。
抽象类的成员可以是private、protected、internal、internal protected的。
此外,接口不能包含字段、构造函数、析构函数、静态成员或常量。
即接口中不能出现static关键字。
从OOP设计角度看,抽象类一般用作基类,相关对象共享某些主要特性,例如共同的目的和结构。
接口则主要用于类,这些类具有根本性的差异,但是仍可以完成某些相同的任务。
7.引用类型和值类型
引用类型和值类型的区别,这里使用一段代码来解释。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
using MyClassLib;
namespace OOP_Example_Program
{
public abstract class MyBase
{
}
internal class MyClass : MyBase { }
public interface IMyBaseInterface { }
internal interface IMyBaseInterface2 { }
internal interface IMyInterfaceExample : IMyBaseInterface, IMyBaseInterface2 { }
internal sealed class MyComplexClass : MyClass, IMyInterfaceExample
{
public int val;
}
struct myStruct
{
public int val;
}
class Program
{
static void Main(string[] args)
{
MyComplexClass myObj = new MyComplexClass();
Console.WriteLine(myObj.GetType());
Console.WriteLine("Hello World!");
MyExternalClass myObj_2 = new MyExternalClass();
WriteLine(myObj_2.GetType());
MyComplexClass myComplexClassA = new MyComplexClass();
MyComplexClass myComplexClassB = myComplexClassA;
myComplexClassA.val = 10;
myComplexClassB.val = 20;
myStruct myStructA = new myStruct();
myStruct myStructB = myStructA;
myStructA.val = 30;
myStructB.val = 40;
WriteLine($"myComplexClassA.val = {myComplexClassA.val}");
WriteLine($"myComplexClassB.val = {myComplexClassB.val}");
WriteLine($"myStructA.val = {myStructA.val}");
WriteLine($"myStructB.val = {myStructB.val}");
}
}
}
运行上面的代码结果如下:
OOP_Example_Program.MyComplexClass
Hello World!
MyClassLib.MyExternalClass
myComplexClassA.val = 20
myComplexClassB.val = 20
myStructA.val = 30
myStructB.val = 40
可以看到引用类型(对象)改一个全都改了。
而值类型,改一个就改一个。
这里其实涉及深度复制和浅度复制的问题,在下一节中进行论述。
8.浅度和深度复制
明确一点,不论浅度复制还是深度复制都是针对引用类型来说的,在C#中就是对象。
浅度复制,不处理引用;深度复制创建新的引用。
这里先简述,之后会提供示例代码。
简单来说C#中进行浅度复制,使用MemberwiseClone(),这个方法是继承自System.Object的方法。
如果要进行深度复制,需要在类中继承ICloneable接口,并实现Clone()方法。