C#编程-第二季-面向对象-Siki

发布于 2024-04-22  7 次阅读


http://www.sikiedu.com/course/931

https://www.runoob.com/csharp/csharp-tutorial.html

调试和错误处理

正常模式下的调试

正常模式指的是不会影响程序的正常运⾏。

  • 在VS中我们使⽤Console.Write(或者WriteLine)⽅法向控制台输出变量的值,通过这个 我们可以查看变量的值是否符合我们的预期来调试错误。
  • 在Unity中我们使⽤Debug.Log("") Debug.LogError("") Debug.LogWarn(""),向unity的 Console窗⼝输出信息,帮助我们调试错误。

中断(Debug)模式下的调试

断点是源代码中⾃动进⼊中断模式的⼀个标记,当遇到断点的时候,程序会进⼊中断模 式。

  1. 右击代码⾏,选择breakpoint(断点) -> insert breakpoint(插⼊断点)
  2. 光标定位到代码⾏,选择菜单上的Debug(调试)->Toggle Breakpoint(切换断点)
  3. 光标定位到代码⾏,按下F9键,在此按下F9是取消断点
  4. 在需要添加断点的⾏⾸位置,直接单击,再次单击取消断点

调⽤堆栈:在调⽤堆栈窗⼝下我们可以观察到当前代码执⾏到哪⼀⾏了,并且可以看到这个代码的是 被什么语句调⽤的

即时窗⼝:即时窗⼝我们可以在这⾥输⼊⼀些命令,查看变量的值,修改变量的值,可以输⼊表达式 查看结果

单步执⾏代码:在中断模式下我们可以单步执⾏代码,单步执⾏带有有两种 逐过程逐语句,都是逐条语句的执⾏。
逐过程遇到函数,不会进⼊函数内部,⽽把函数当成⼀条语句去执⾏。

异常处理(捕捉异常) try … catch … finally 语句

try {
  // 要捕获的异常
} catch () {
  // 出现异常时执行的代码
} finally { 
  // 不管是否出现异常都会执行的代码
}

其中catch块可以有0或者多个,finally可以有0或者1个 但是如果没有catch块,必须有finally块,没有finally块,必须有catch块,catch块和finally 块可以同时存在

try中有一行出现异常则不会继续执行后面代码

Console.WriteLine("输入两个数字,每行一个");
int n1=0, n2=0;
while (true) {
  try {
    n1 = Convert.ToInt32(Console.ReadLine());
    n2 = Convert.ToInt32(Console.ReadLine());
    break;
  } catch (FormatException e) {
    Console.WriteLine("输入数据不符合规则,重新输入");
  }
  Console.WriteLine(n1 + n2);

类(Class)

当你定义一个类时,你定义了一个数据类型的蓝图。这实际上并没有定义任何的数据,但它定义了类的名称意味着什么,也就是说,类的对象由什么组成及在这个对象上可执行什么操作。对象是类的实例。构成类的方法和变量称为类的成员。

类的定义

类的定义是以关键字 class 开始,后跟类的名称。类的主体,包含在一对花括号内。

<access specifier> class  class_name {
  // member variables
  <access specifier> <data type> variable1;
  <access specifier> <data type> variable2;
  ...
    <access specifier> <data type> variableN;
  // member methods
  <access specifier> <return type> method1(parameter_list) {
    // method body 
  }
  <access specifier> <return type> method2(parameter_list) {
    // method body 
  }
  ...
    <access specifier> <return type> methodN(parameter_list) {
    // method body 
  }
}

请注意:

  • 访问标识符 <access specifier> 指定了对类及其成员的访问规则。如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal,成员的默认访问标识符是 private
  • 数据类型 <data type> 指定了变量的类型,返回类型 <return type> 指定了返回的方法返回的数据类型。
  • 如果要访问类的成员,你要使用点(.)运算符。
  • 点运算符链接了对象的名称和成员的名称。
class Customer{
  public string name;
  public string address;
  public int age;
  public string buyTime;
  public void Show(){
    Console.WriteLine("名字:"+name);
    Console.WriteLine("年龄:"+age);
    Console.WriteLine("地址:"+address);
    Console.WriteLine("购买时间:"+buyTime);
  }
}

成员函数和封装

类的成员函数是一个在类定义中有它的定义或原型的函数,就像其他变量一样。作为类的一个成员,它能在类的任何对象上操作,且能访问该对象的类的所有成员。成员变量是对象的属性(从设计角度),且它们保持私有来实现封装。这些变量只能使用公共成员函数来访问。

using System;
namespace BoxApplication{
  class Box{
    private double length;   // 长度
    private double breadth;  // 宽度
    private double height;   // 高度
    public void setLength( double len ){
      length = len;
    }

    public void setBreadth( double bre ){
      breadth = bre;
    }

    public void setHeight( double hei ){
      height = hei;
    }
    public double getVolume(){
      return length * breadth * height;
    }
  }
  class Boxtester{
    static void Main(string[] args){
      Box Box1 = new Box();        // 声明 Box1,类型为 Box
      Box Box2 = new Box();        // 声明 Box2,类型为 Box
      double volume;               // 体积


      // Box1 详述
      Box1.setLength(6.0);
      Box1.setBreadth(7.0);
      Box1.setHeight(5.0);

      // Box2 详述
      Box2.setLength(12.0);
      Box2.setBreadth(13.0);
      Box2.setHeight(10.0);

      // Box1 的体积
      volume = Box1.getVolume();
      Console.WriteLine("Box1 的体积: {0}" ,volume);

      // Box2 的体积
      volume = Box2.getVolume();
      Console.WriteLine("Box2 的体积: {0}", volume);

      Console.ReadKey();
    }
  }
}

构造函数

类的 构造函数 是类的一个特殊的成员函数,当创建类的新对象时执行。构造函数的名称与类的名称完全相同,它没有任何返回类型。

using System;
namespace LineApplication{
  class Line{
    private double length;   // 线条的长度
    public Line(){
      Console.WriteLine("对象已创建");
    }

    public void setLength( double len ){
      length = len;
    }
    public double getLength(){
      return length;
    }

    static void Main(string[] args){
      Line line = new Line();    
      // 设置线条长度
      line.setLength(6.0);
      Console.WriteLine("线条的长度: {0}", line.getLength());
      Console.ReadKey();
    }
  }
}

默认的构造函数没有任何参数。但是如果你需要一个带有参数的构造函数可以有参数,这种构造函数叫做参数化构造函数。这种技术可以帮助你在创建对象的同时给对象赋初始值,创建参数化构建函数前需要有无参构建函数

析构函数

类的 析构函数 是类的一个特殊的成员函数,当类的对象超出范围时执行。析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。析构函数用于在结束程序(比如关闭文件、释放内存等)之前释放资源。析构函数不能继承或重载。

using System;
namespace LineApplication{
  class Line{
    private double length;   // 线条的长度
    // 构造函数
    public Line(){
      Console.WriteLine("对象已创建");
    }
    //析构函数
    ~Line(){
      Console.WriteLine("对象已删除");
    }

    public void setLength( double len ){
      length = len;
    }
    public double getLength(){
      return length;
    }

    static void Main(string[] args){
      Line line = new Line();
      // 设置线条长度
      line.setLength(6.0);
      Console.WriteLine("线条的长度: {0}", line.getLength());           
    }
  }
}

静态成员

关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过直接调用类而不需要创建类的实例来获取。静态变量可在成员函数或类的定义外部进行初始化。你也可以在类的定义内部初始化静态变量。

using System;
namespace StaticVarApplication{
    class StaticVar{
       public static int num;
        public void count(){
            num++;
        }
        public int getNum(){
            return num;
        }
    }
    class StaticTester{
        static void Main(string[] args){
            StaticVar s1 = new StaticVar();
            StaticVar s2 = new StaticVar();
            s1.count();
            s1.count();
            s1.count();
            s2.count();
            s2.count();
            s2.count();         
            Console.WriteLine("s1 的变量 num: {0}", s1.getNum());
            Console.WriteLine("s2 的变量 num: {0}", s2.getNum());
            Console.ReadKey();
        }
    }
}

你也可以把一个成员函数声明为 static。这样的函数只能访问静态变量。静态函数在对象被创建之前就已经存在。下面的实例演示了静态函数的用法

using System;
namespace StaticVarApplication{
  class StaticVar{
    public static int num;
    public void count(){
      num++;
    }
    public static int getNum(){
      return num;
    }
  }
  class StaticTester{
    static void Main(string[] args){
      StaticVar s = new StaticVar();
      s.count();
      s.count();
      s.count();                   
      Console.WriteLine("变量 num: {0}", StaticVar.getNum());
      Console.ReadKey();
    }
  }
}

属性

属性(Property) 是类(class)、结构(structure)和接口(interface)的命名(named)成员。类或结构中的成员变量或方法称为 域(Field)。属性(Property)是域(Field)的扩展,且可使用相同的语法来访问。它们使用 访问器(accessors) 让私有域的值可被读写或操作。

属性(Property)不会确定存储位置。相反,它们具有可读写或计算它们值的 访问器(accessors)。例如,有一个名为 Student 的类,带有 age、name 和 code 的私有域。我们不能在类的范围以外直接访问这些域,但是我们可以拥有访问这些私有域的属性。

访问器

属性(Property)的访问器(accessor)包含有助于获取(读取或计算)或设置(写入)属性的可执行语句。访问器(accessor)声明可包含一个 get 访问器、一个 set 访问器,或者同时包含二者,传入参数为value;

// 声明类型为 string 的 Code 属性
public string Code
{
   get
   {
      return code;
   }
   set //value参数
   {
      code = value;
   }
}

get和set可以进一步设置权限,不设置会使用属性的访问权限,可以设置只读或只写

public get{}

get和set可以简写

public string name{
  get;set;
}

当没有数据成员时,系统会自动创建数据成员

//private int age;
public int Age{get;set;}

实例

using System;
namespace runoob
{
   class Student
   {

      private string code = "N.A";
      private string name = "not known";
      private int age = 0;

      // 声明类型为 string 的 Code 属性
      public string Code
      {
         get
         {
            return code;
         }
         set
         {
            code = value;
         }
      }

      // 声明类型为 string 的 Name 属性
      public string Name
      {
         get
         {
            return name;
         }
         set
         {
            name = value;
         }
      }

      // 声明类型为 int 的 Age 属性
      public int Age
      {
         get
         {
            return age;
         }
         set
         {
            age = value;
         }
      }
      public override string ToString()
      {
         return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
      }
    }
    class ExampleDemo
    {
      public static void Main()
      {
         // 创建一个新的 Student 对象
         Student s = new Student();

         // 设置 student 的 code、name 和 age
         s.Code = "001";
         s.Name = "Zara";
         s.Age = 9;
         Console.WriteLine("Student Info: {0}", s);
         // 增加年龄
         s.Age += 1;
         Console.WriteLine("Student Info: {0}", s);
         Console.ReadKey();
       }
   }
}

抽象属性

抽象类可拥有抽象属性,这些属性应在派生类中被实现。

using System;
namespace runoob
{
   public abstract class Person
   {
      public abstract string Name
      {
         get;
         set;
      }
      public abstract int Age
      {
         get;
         set;
      }
   }
   class Student : Person
   {

      private string code = "N.A";
      private string name = "N.A";
      private int age = 0;

      // 声明类型为 string 的 Code 属性
      public string Code
      {
         get
         {
            return code;
         }
         set
         {
            code = value;
         }
      }

      // 声明类型为 string 的 Name 属性
      public override string Name
      {
         get
         {
            return name;
         }
         set
         {
            name = value;
         }
      }

      // 声明类型为 int 的 Age 属性
      public override int Age
      {
         get
         {
            return age;
         }
         set
         {
            age = value;
         }
      }
      public override string ToString()
      {
         return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
      }
   }
   class ExampleDemo
   {
      public static void Main()
      {
         // 创建一个新的 Student 对象
         Student s = new Student();

         // 设置 student 的 code、name 和 age
         s.Code = "001";
         s.Name = "Zara";
         s.Age = 9;
         Console.WriteLine("Student Info:- {0}", s);
         // 增加年龄
         s.Age += 1;
         Console.WriteLine("Student Info:- {0}", s);
         Console.ReadKey();
       }
   }
}

堆栈和静态存储区

程序内存区域:堆 栈 静态存储区。程序所有的数据,也就是所有的变量,都是存储在内存中的。

  • 栈空间⽐较⼩,但是读取速度快
  • 堆空间⽐较⼤,但是读取速度慢

后进先出:

  • 数据只能从栈的顶端插⼊和删除
  • 把数据放⼊栈顶称为⼊栈(push)
  • 从栈顶删除数据称为出栈(pop)

堆是⼀块内存区域,与栈不同,堆⾥的内存能够以任意顺序存⼊和移除

垃圾回收器

GC Garbage Collector垃圾回收器 CLR的GC就是内存管理机制,我们写程序不需要关⼼内存的使⽤,因为这些都是CLR帮 我们做了

值类型和引⽤类型

类型被分为两种:

  • 值类型(整数,bool struct char ⼩数):值类型只需要⼀段单独的内存,⽤于存储实际的数据,(单独定义的时候放在栈中)
  • 引⽤类型(string 数组 ⾃定义的 类,内置的类):引⽤类型需要两段内存 第⼀段存储实际的数据,它总是位于堆中 第⼆段是⼀个引⽤,指向数据在堆中的存放位置

继承

当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。

继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。

一个类继承自另一个类时,它会获得被继承类的特征,父类及子类。继承结果是父类中存在的项 ,也将出现在子类中。假设父类中有两个方法,子类中也同时有这两个方法,无需在子类中创建。

修饰符

处理继承时需要注意三个访问修饰符:public、private、protected

  • public公开的类特征将存在于子类中,并且可供访问
  • private私有的类存在于子类中,但不可访问
  • protected相当于两者混合:与公开的特征一样,受保护的父类的所有特征将在子类中且可供访问;但在父类或子类之外将不可访问,就像私有的特征一样
  • internal: 同⼀程序集中的任何代码都可以访问该类型或成员,但别的代码不可以。
  • protected internal: 在⼀程序集中,protected internal体现的是internal的性质;在其 他程序集中,protected internal体现的是protected的性质。
public class 类名 : 继承的父类名{}

在子类继承的项中,构造函数是一个例外,因为他们对类是唯一的不会共享。但是在子类中调用构造函数时,其父类的构造函数会立刻被调用。

其他修饰符

abstract:

使用abstract修饰的类为抽象类,抽象类只能是其他类的基类,不能与sealed、static一起使用。

abstract可以修饰抽象类中的方法或属性,此时,方法或属性不能包含实现,且访问级别不能为私有。

抽象类不能被实例化。

sealed:

使用sealed修饰的类为密封类,密封类无法被继承,不能和abstract、static一起使用。

当sealed用于方法或属性时,必须始终与override一起使用。

static:

使用static修饰的类为静态类,静态类所有成员都必须是静态的,不能与abstract、sealed一起使用。

static可以修饰方法、字段、属性或事件,始终通过类名而不是实例名称访问静态成员,静态字段只有一个副本。

静态类不能被实例化。

const:

使用const关键字来声明某个常量字段或常量局部变量,必须在声明常量时赋初值。

不能与static一起使用,常量默认是static的,常量字段只有一个副本。

readonly:

使用readonly关键字来声明只读字段。

只读字段可以在声明或构造函数中初始化,每个类或结构的实例都有一个独立的副本。

可以与static一起使用,声明静态只读字段。

静态只读字段可以在声明或静态构造函数中初始化,静态常量字段只有一个副本。

virtual:

virtual关键字用于修饰方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。

默认情况下,方法是非虚拟的。 不能重写非虚方法。

virtual修饰符不能与static、abstract、private或override修饰符一起使用。

override:

要扩展或修改继承的方法、属性、索引器或事件的抽象实现或虚实现,必须使用override修饰符。

重写的成员必须是virtual、abstract或override的。

基类和派生类

一个类可以继承自另一个类,被称为基类(父类)和派生类(子类)。C# 不支持类的多重继承,但支持接口的多重继承,一个类可以实现多个接口。一个类可以继承多个接口,但只能继承自一个类。

<访问修饰符> class <基类>{
 ...
}
class <派生类> : <基类>{
 ...
}

如果没有写继承,则默认继承Object

派生类会继承基类的成员(字段、方法、属性等),除非它们被明确地标记为私有(private)。

class BaseClass{
    public void SomeMethod(){
        // Method implementation
    }
}

class DerivedClass : BaseClass{
    public void AnotherMethod(){
        // Accessing base class method
        base.SomeMethod();

        // Method implementation
    }
}

this和base

  • this可以访问当前类中定义的字段,属性和⽅法,有没有this都可以访问,另外当⽅法的参数跟字段重名的时候,使⽤this可以表明访问的是类 中的字段,
  • base可以调⽤⽗类中的公有⽅法和字段,有没有base都可以访问
this.attack=attack;
base.hp=hp;

由于类可能有多个不同的构造函数,因此我们可能想控制调用那个基类的构造函数,为此可以使用关键字base,通过在子类构造函数的参数列表后面加一个冒号,可以使用关键字base在构造函数的参数列表中,显式调用基类的具体构造函数。

public class Apple : Furit{
  public Apple() :base("apple"){
    //Constructor Code...
  }
}

如果不显示调用基类的构造函数,则会隐式调用默认构造函数。

基类的初始化

派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。

using System;
namespace RectangleApplication{
   class Rectangle{
      // 成员变量
      protected double length;
      protected double width;
      public Rectangle(double l, double w){
         length = l;
         width = w;
      }
      public double GetArea(){
         return length * width;
      }
      public void Display(){
         Console.WriteLine("长度: {0}", length);
         Console.WriteLine("宽度: {0}", width);
         Console.WriteLine("面积: {0}", GetArea());
      }
   }//end class Rectangle  
   class Tabletop : Rectangle{
      private double cost;
      public Tabletop(double l, double w) : base(l, w){ }
      public double GetCost(){
         double cost;
         cost = GetArea() * 70;
         return cost;
      }
      public void Display(){
         base.Display();
         Console.WriteLine("成本: {0}", GetCost());
      }
   }
   class ExecuteRectangle{
      static void Main(string[] args){
         Tabletop t = new Tabletop(4.5, 7.5);
         t.Display();
         Console.ReadLine();
      }
   }
}

继承接口

一个接口可以继承自一个或多个其他接口,派生接口继承了基接口的所有成员。派生接口可以扩展基接口的成员列表,但不能改变它们的访问修饰符。

interface IBaseInterface{
    void Method1();
}

interface IDerivedInterface : IBaseInterface{
    void Method2();
}

继承接口的实例可以通过以下方式来实现:MyClass 类实现了 IDerivedInterface 接口,因此必须提供 IDerivedInterface 中定义的所有方法,包括从 IBaseInterface继承的 Method1() 方法。 在 Main 方法中,我们创建了 MyClass 的实例 obj 并调用了它的方法。

using System;

// 定义一个基接口
interface IBaseInterface{
    void Method1();
}

// 定义一个派生接口,继承自基接口
interface IDerivedInterface : IBaseInterface{
    void Method2();
}

// 实现派生接口的类
class MyClass : IDerivedInterface{
    public void Method1(){
        Console.WriteLine("Method1 implementation");
    }

    public void Method2(){
        Console.WriteLine("Method2 implementation");
    }
}

class Program{
    static void Main(string[] args){
        // 创建 MyClass 类的实例
        MyClass obj = new MyClass();

        // 调用继承自基接口的方法
        obj.Method1();

        // 调用派生接口新增的方法
        obj.Method2();
    }
}

多重继承

多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。C# 不支持多重继承。但是,您可以使用接口来实现多重继承。

using System;
namespace InheritanceApplication{
   class Shape {
      public void setWidth(int w){
         width = w;
      }
      public void setHeight(int h){
         height = h;
      }
      protected int width;
      protected int height;
   }

   // 基类 PaintCost
   public interface PaintCost {
      int getCost(int area);

   }
   // 派生类
   class Rectangle : Shape, PaintCost{
      public int getArea(){
         return (width * height);
      }
      public int getCost(int area){
         return area * 70;
      }
   }
   class RectangleTester{
      static void Main(string[] args){
         Rectangle Rect = new Rectangle();
         int area;
         Rect.setWidth(5);
         Rect.setHeight(7);
         area = Rect.getArea();
         // 打印对象的面积
         Console.WriteLine("总面积: {0}",  Rect.getArea());
         Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area));
         Console.ReadKey();
      }
   }
}

覆盖

指更改子类中的父类方法,结果是当我们调用方法时将最新覆盖的版本

使用virtual和override关键字,这些关键字位于方法的返回类型之前,父类中的方法定义为virtual,而所有子类中的方法定义为override。声明为virtual的任何方法可被任何子类覆盖,可以使用base关键字来同时调用方法的父版本

虚⽅法

把⼀个基类函数声明为virtual,就可以在任何派⽣类中重写该函数:

class MyBaseClass{
  public virtual string VirtualMethod(){
      return "Method is called in base class";
  }
}

在派⽣类中重写另外⼀个函数时,要使⽤override关键字显⽰声明

class MyDerivedClass:MyBaseClass{
  public override string VirtualMethod(){
      return "Method is called in derivedclass.";
  }
}

隐藏⽅法

如果签名相同的⽅法在基类和派⽣类中都进⾏了声明,但是该⽅法没有分别声明为virtual 和override,派⽣类就会隐藏基类⽅法。(要使⽤new关键字进⾏声明)

class MyBaseClass{
  public int MyMethod(){
  }
}

派⽣类(在派⽣类中把基类同名的⽅法隐藏掉了)

class MyDerivedClass :MyBaseClass{ 
  public new void MyMethod() { 
  } 
}

多态

多态是继承的一个特征,它允许一个类具有多种类型。

在继承层次结构中,任何子类都可以作为父类使用。这意味着在需要基类的地方,可以使用派生类来代替它。

多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。

多态性可以是静态的或动态的。在静态多态性中,函数的响应是在编译时发生的。在动态多态性中,函数的响应是在运行时发生的。

在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。

多态就是同一个接口,使用不同的实例而执行不同操作

在调用 onTrigger 函数时,我们不知道会使用什么类型的 Collider。每个对象的特定 Collider 都会传入函数,由于它们都继承自 Collider 父类,因此它们都可以被正确处理。

public class SomeScript : MonoBehaviour{
  void onTriggerEnter(Collider other){
    //Do Something Fun!
  }
}

多态的一种明智用法涉及构造函数和对象引用。你可以声明基类类型的对象,然后调用其中一个派生类的构造函数,因为变量需要的是基类的类型。

静态多态性

在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C# 提供了两种技术来实现静态多态性。分别为:

  • 函数重载
  • 运算符重载

函数重载

重载方法的操作是为新方法指定名称相同,但形参不同。通过重载过程可以为单个方法提供多个定义,意味着可以使用同一方法名称执行两项不同的操作

下面的实例演示了几个相同的函数 Add(),用于对不同个数参数进行相加处理:

public int Add(int a, int b, int c)  {  
  return a + b + c;  
}  
public int Add(int a, int b)  {  
    return a + b;  
}  

在其他类中,当我们尝试访问Add方法时,我们可以看到有两个版本,可能出现三种情况

  1. 与传入参数完全匹配,运行这个版本的已重载方法
  2. 如果不匹配,根据匹配项,选择需要最少转换量的方法
  3. 如果没有可能的匹配项,或者多版本所需的转换量相同,则抛出错误

动态多态性

动态多态性是通过 抽象类虚方法 实现的。

C# 允许您使用关键字 abstract 创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自该抽象类时,实现即完成。抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。

抽象类不能实例化,抽象类可以包含普通函数和抽象函数(抽象函数必须包含在抽象类中),抽象函数就是只有函数定义没有函数体。 显然,抽象函数本⾝也是虚拟的Virtual(只有函数定义,没有函数体实现)。 类是⼀个模板,那么抽象类就是⼀个不完整的模板,我们不能使⽤不完整的模板去构造对象。抽象类不能去构造,但可以用子类去构造

abstract class Enemy{
    public abstract void Attack();
}
class Boss:Enemy{
    public override void Attack(){
        Console.WriteLine("BOSS进行攻击")
    }
}
static void Main(string[] args){
    Enemy e = new Boss();
    e.Attack();
}

请注意,下面是有关抽象类的一些规则:

  • 不能创建一个抽象类的实例。
  • 不能在一个抽象类外部声明一个抽象方法。
  • 通过在类定义前面放置关键字 sealed,可以将类声明为密封类。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed。

密封类和密封⽅法

C#允许把类和⽅法声明为 sealed。 对于类:这表⽰不能继承该类;对于⽅法:表⽰不能重写该⽅法。防⽌重写某些类导致代码混乱,被重写的方法才能声明为密封方法

sealed FinalClass{
    // etc
}

向上转型

子类的构造函数会创建衍生类型的对象。由于子类是父类的一种,因此这种转换是有效的,这个过程被称为向上转型。当对象向上转型时,它只能被视为其父类的一个对象。

利用父类声明对象,利用子类进行构造。在本实例中,子类向上转型时,它只能被视作父类,这表示只能使用父类中可用的变量和方法,在使用时会把它们视作位于父类对象中。虚拟函数是一个例外,虚拟函数将调用最新覆盖版本。

public class SomeScript : MonoBehaviour{
  void Start(){
    ParentClass myClass = new ChildClass();
    myClass.ParentMethod();
  }
}

为了将这个子类视作子类,我们需要向下转型子类变量,使其恢复为子类类型。具体方法是将类型名称括在括号内并将其置于变量前面。我们可以再用一组括号括起来,并使用点运算符来访问成员,也可以创建对这个新版本的引用。

public class SomeScript : MonoBehaviour{
  void Start(){
    ParentClass myClass = new ChildClass();
    myClass.ParentMethod();
    ChildClass myChild = (ChildClass)myClass;
    myChild.ChildMethod();
  }
}

接口

接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分。

  1. - 定义了属性、方法和事件,成员的声明在接口中定义。
  • 不包含成员的具体实现,仅提供了成员的声明。
  • 提供了派生类应遵循的标准结构,使得实现接口的类或结构在形式上保持一致。
  • 接口本身不实现任何功能,而是与实现该接口的对象订立一个必须实现哪些行为的契约。
  1. 与接口类似,但大多只在基类声明少数方法,由派生类实现。抽象类不能直接实例化,但允许派生出具体的、具有实际功能的类。

定义接口

接口使用 interface 关键字声明,它与类的声明类似。接口声明默认是 public 的。下面是一个接口声明的实例:

interface IMyInterface{
    void MethodToImplement();
}

以上代码定义了接口 IMyInterface。通常接口命令以 I 字母开头,这个接口只有一个方法 MethodToImplement(),没有参数和返回值,当然我们可以按照需求设置参数和返回值。值得注意的是,该方法并没有具体的实现。

using System;

interface IMyInterface{
        // 接口成员
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface{
    static void Main(){
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
    }

    public void MethodToImplement(){
        Console.WriteLine("MethodToImplement() called.");
    }
}

InterfaceImplementer 类实现了 IMyInterface 接口,接口的实现与类的继承语法格式类似:

class InterfaceImplementer : IMyInterface

继承接口后,我们需要实现接口的方法 MethodToImplement() , 方法名必须与接口定义的方法名一致。

接口继承

如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。以下实例定义了两个接口 IMyInterface 和 IParentInterface。以下实例 IMyInterface 继承了 IParentInterface 接口,因此接口实现类必须实现 MethodToImplement() 和 ParentInterfaceMethod() 方法:

using System;

interface IParentInterface{
    void ParentInterfaceMethod();
}

interface IMyInterface : IParentInterface{
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface{
    static void Main(){
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
        iImp.ParentInterfaceMethod();
    }

    public void MethodToImplement(){
        Console.WriteLine("MethodToImplement() called.");
    }

    public void ParentInterfaceMethod(){
        Console.WriteLine("ParentInterfaceMethod() called.");
    }
}

索引器

索引器允许一个对象可以像数组一样使用下标的方式来访问。

当您为类定义一个索引器时,该类的行为就会像一个 虚拟数组(virtual array) 一样。您可以使用数组访问运算符 [ ] 来访问该类的的成员。

element-type this[int index] {
   // get 访问器
   get {
      // 返回 index 指定的值
   }

   // set 访问器
   set {
      // 设置 index 指定的值 
   }
}

索引器的行为的声明在某种程度上类似于属性(property)。就像属性(property),您可使用 getset 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。

定义一个属性(property)包括提供属性名称。索引器定义的时候不带有名称,但带有 this 关键字,它指向对象实例。下面的实例演示了这个概念:

using System;
namespace IndexerApplication{
   class IndexedNames{
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames(){
         for (int i = 0; i < size; i++)
         namelist[i] = "N. A.";
      }
      public string this[int index]{
         get{
            string tmp;

            if( index >= 0 && index <= size-1 ){
               tmp = namelist[index];
            }else{
               tmp = "";
            }

            return ( tmp );
         }set{
            if( index >= 0 && index <= size-1 ){
               namelist[index] = value;
            }
         }
      }

      static void Main(string[] args){
         IndexedNames names = new IndexedNames();
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         for ( int i = 0; i < IndexedNames.size; i++ ){
            Console.WriteLine(names[i]);
         }
         Console.ReadKey();
      }
   }
}

重载索引器

参数,且每个参数可以是不同的类型。没有必要让索引器必须是整型的。C# 允许索引器可以是其他类型

using System;
namespace IndexerApplication
{
   class IndexedNames
   {
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames()
      {
         for (int i = 0; i < size; i++)
         {
          namelist[i] = "N. A.";
         }
      }
      public string this[int index]
      {
         get
         {
            string tmp;

            if( index >= 0 && index <= size-1 )
            {
               tmp = namelist[index];
            }
            else
            {
               tmp = "";
            }

            return ( tmp );
         }
         set
         {
            if( index >= 0 && index <= size-1 )
            {
               namelist[index] = value;
            }
         }
      }
      public int this[string name]
      {
         get
         {
            int index = 0;
            while(index < size)
            {
               if (namelist[index] == name)
               {
                return index;
               }
               index++;
            }
            return index;
         }

      }

      static void Main(string[] args)
      {
         IndexedNames names = new IndexedNames();
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         // 使用带有 int 参数的第一个索引器
         for (int i = 0; i < IndexedNames.size; i++)
         {
            Console.WriteLine(names[i]);
         }
         // 使用带有 string 参数的第二个索引器
         Console.WriteLine(names["Nuha"]);
         Console.ReadKey();
      }
   }
}

运算符重载

您可以重定义或重载 C# 中内置的运算符。因此,程序员也可以使用用户自定义类型的运算符。重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的。与其他函数一样,重载运算符有返回类型和参数列表。

public static Box operator+ (Box b, Box c)
{
   Box box = new Box();
   box.length = b.length + c.length;
   box.breadth = b.breadth + c.breadth;
   box.height = b.height + c.height;
   return box;
}

上面的函数为用户自定义的类 Box 实现了加法运算符(+)。它把两个 Box 对象的属性相加,并返回相加后的 Box 对象。

运算符重载的实现

using System;

namespace OperatorOvlApplication
{
   class Box
   {
      private double length;      // 长度
      private double breadth;     // 宽度
      private double height;      // 高度

      public double getVolume()
      {
         return length * breadth * height;
      }
      public void setLength( double len )
      {
         length = len;
      }

      public void setBreadth( double bre )
      {
         breadth = bre;
      }

      public void setHeight( double hei )
      {
         height = hei;
      }
      // 重载 + 运算符来把两个 Box 对象相加
      public static Box operator+ (Box b, Box c)
      {
         Box box = new Box();
         box.length = b.length + c.length;
         box.breadth = b.breadth + c.breadth;
         box.height = b.height + c.height;
         return box;
      }

   }

   class Tester
   {
      static void Main(string[] args)
      {
         Box Box1 = new Box();         // 声明 Box1,类型为 Box
         Box Box2 = new Box();         // 声明 Box2,类型为 Box
         Box Box3 = new Box();         // 声明 Box3,类型为 Box
         double volume = 0.0;          // 体积

         // Box1 详述
         Box1.setLength(6.0);
         Box1.setBreadth(7.0);
         Box1.setHeight(5.0);

         // Box2 详述
         Box2.setLength(12.0);
         Box2.setBreadth(13.0);
         Box2.setHeight(10.0);

         // Box1 的体积
         volume = Box1.getVolume();
         Console.WriteLine("Box1 的体积: {0}", volume);

         // Box2 的体积
         volume = Box2.getVolume();
         Console.WriteLine("Box2 的体积: {0}", volume);

         // 把两个对象相加
         Box3 = Box1 + Box2;

         // Box3 的体积
         volume = Box3.getVolume();
         Console.WriteLine("Box3 的体积: {0}", volume);
         Console.ReadKey();
      }
   }
}

可重载和不可重载运算符

下表描述了 C# 中运算符重载的能力:

运算符描述
+, -, !, ~, ++, --这些一元运算符只有一个操作数,且可以被重载。
+, -, *, /, %这些二元运算符带有两个操作数,且可以被重载。
==, !=, <, >, <=, >=这些比较运算符可以被重载。
&&, ||这些条件逻辑运算符不能被直接重载。
+=, -=, *=, /=, %=这些赋值运算符不能被重载。
=, ., ?:, ->, new, is, sizeof, typeof这些运算符不能被重载。

实例

using System;

namespace OperatorOvlApplication
{
    class Box
    {
       private double length;      // 长度
       private double breadth;     // 宽度
       private double height;      // 高度

       public double getVolume()
       {
         return length * breadth * height;
       }
      public void setLength( double len )
      {
          length = len;
      }

      public void setBreadth( double bre )
      {
          breadth = bre;
      }

      public void setHeight( double hei )
      {
          height = hei;
      }
      // 重载 + 运算符来把两个 Box 对象相加
      public static Box operator+ (Box b, Box c)
      {
          Box box = new Box();
          box.length = b.length + c.length;
          box.breadth = b.breadth + c.breadth;
          box.height = b.height + c.height;
          return box;
      }

      public static bool operator == (Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length == rhs.length && lhs.height == rhs.height 
             && lhs.breadth == rhs.breadth)
          {
              status = true;
          }
          return status;
      }
      public static bool operator !=(Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length != rhs.length || lhs.height != rhs.height 
              || lhs.breadth != rhs.breadth)
          {
              status = true;
          }
          return status;
      }
      public static bool operator <(Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length < rhs.length && lhs.height 
              < rhs.height && lhs.breadth < rhs.breadth)
          {
              status = true;
          }
          return status;
      }

      public static bool operator >(Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length > rhs.length && lhs.height 
              > rhs.height && lhs.breadth > rhs.breadth)
          {
              status = true;
          }
          return status;
      }

      public static bool operator <=(Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length <= rhs.length && lhs.height 
              <= rhs.height && lhs.breadth <= rhs.breadth)
          {
              status = true;
          }
          return status;
      }

      public static bool operator >=(Box lhs, Box rhs)
      {
          bool status = false;
          if (lhs.length >= rhs.length && lhs.height 
             >= rhs.height && lhs.breadth >= rhs.breadth)
          {
              status = true;
          }
          return status;
      }
      public override string ToString()
      {
          return String.Format("({0}, {1}, {2})", length, breadth, height);
      }

   }

   class Tester
   {
      static void Main(string[] args)
      {
        Box Box1 = new Box();          // 声明 Box1,类型为 Box
        Box Box2 = new Box();          // 声明 Box2,类型为 Box
        Box Box3 = new Box();          // 声明 Box3,类型为 Box
        Box Box4 = new Box();
        double volume = 0.0;   // 体积

        // Box1 详述
        Box1.setLength(6.0);
        Box1.setBreadth(7.0);
        Box1.setHeight(5.0);

        // Box2 详述
        Box2.setLength(12.0);
        Box2.setBreadth(13.0);
        Box2.setHeight(10.0);

       // 使用重载的 ToString() 显示两个盒子
        Console.WriteLine("Box1: {0}", Box1.ToString());
        Console.WriteLine("Box2: {0}", Box2.ToString());

        // Box1 的体积
        volume = Box1.getVolume();
        Console.WriteLine("Box1 的体积: {0}", volume);

        // Box2 的体积
        volume = Box2.getVolume();
        Console.WriteLine("Box2 的体积: {0}", volume);

        // 把两个对象相加
        Box3 = Box1 + Box2;
        Console.WriteLine("Box3: {0}", Box3.ToString());
        // Box3 的体积
        volume = Box3.getVolume();
        Console.WriteLine("Box3 的体积: {0}", volume);

        //comparing the boxes
        if (Box1 > Box2)
          Console.WriteLine("Box1 大于 Box2");
        else
          Console.WriteLine("Box1 不大于 Box2");
        if (Box1 < Box2)
          Console.WriteLine("Box1 小于 Box2");
        else
          Console.WriteLine("Box1 不小于 Box2");
        if (Box1 >= Box2)
          Console.WriteLine("Box1 大于等于 Box2");
        else
          Console.WriteLine("Box1 不大于等于 Box2");
        if (Box1 <= Box2)
          Console.WriteLine("Box1 小于等于 Box2");
        else
          Console.WriteLine("Box1 不小于等于 Box2");
        if (Box1 != Box2)
          Console.WriteLine("Box1 不等于 Box2");
        else
          Console.WriteLine("Box1 等于 Box2");
        Box4 = Box3;
        if (Box3 == Box4)
          Console.WriteLine("Box3 等于 Box4");
        else
          Console.WriteLine("Box3 不等于 Box4");

        Console.ReadKey();
      }
    }
}

集合

集合类是专门用于数据存储和检索的类。这些类提供了对栈(stack)、队列(queue)、列表(list)和哈希表(hash table)的支持。大多数集合类实现了相同的接口。

集合类服务于不同的目的,如为元素动态分配内存,基于索引访问列表项等等。这些类创建 Object 类的对象的集合。在 C# 中,Object 类是所有数据类型的基类。

下面是各种常用的 System.Collection 命名空间的类。

描述和用法
动态数组(ArrayList)它代表了可被单独索引的对象的有序集合。它基本上可以替代一个数组。但是,与数组不同的是,您可以使用索引在指定的位置添加和移除项目,动态数组会自动重新调整它的大小。它也允许在列表中进行动态内存分配、增加、搜索、排序各项。
哈希表(Hashtable)它使用来访问集合中的元素。当您使用键访问元素时,则使用哈希表,而且您可以识别一个有用的键值。哈希表中的每一项都有一个键/值对。键用于访问集合中的项目。
排序列表(SortedList)它可以使用索引来访问列表中的项。排序列表是数组和哈希表的组合。它包含一个可使用键或索引访问各项的列表。如果您使用索引访问各项,则它是一个动态数组(ArrayList),如果您使用键访问各项,则它是一个哈希表(Hashtable)。集合中的各项总是按键值排序。
堆栈(Stack)它代表了一个后进先出的对象集合。当您需要对各项进行后进先出的访问时,则使用堆栈。当您在列表中添加一项,称为推入元素,当您从列表中移除一项时,称为弹出元素。
队列(Queue)它代表了一个先进先出的对象集合。当您需要对各项进行先进先出的访问时,则使用队列。当您在列表中添加一项,称为入队,当您从列表中移除一项时,称为出队
点阵列(BitArray)它代表了一个使用值 1 和 0 来表示的二进制数组。当您需要存储位,但是事先不知道位数时,则使用点阵列。您可以使用整型索引从点阵列集合中访问各项,索引从零开始。

数组

var list=new List<int>();

首先,List 是个强类型,很安全。其次看那个尖括号,它是 C#2.0 时加入的泛型,所以并不存在像 ArrayList。那样要拆/装箱以此造成性能浪费。然后,List 通过索引分配,索引与数组一样,从 0 开始。它可以通过索引来读取值:

var a=new List<int>();
a.Add(12);
a.Add(10);
Console.WriteLine(a[0]);

列表可以有相同的项,而且项是手动排序。在改变项后,要注意项的索引会发生改变:

var a=new List<int>();
a.Add(12);
a.Add(10);
Console.WriteLine(a[0]);
a.Remove(12);
Console.WriteLine(a[0]);

提供一下常用的列表方法:

  • 1、Add() 将东西加入到列表的最后。
  • 2、Remove() 删掉项中第一个匹配你想删除的条件的项(删去第一个匹配此条件的项)。
  • 3、Clear() 清空所有项。
  • 4、Sort() 用系统默认的方式对项进行排序。
  • 5、Contains() 查看某项是否存在于列表中。

当列表的中的 容量发⽣改变的时候,它会创建⼀个新的数组,使⽤Array.Copy()⽅法将旧数组中的元素 复制到新数组中。并且扩容为原来的2倍

动态数组

动态数组(ArrayList)代表了可被单独索引的对象的有序集合。它基本上可以替代一个数组。但是,与数组不同的是,您可以使用索引在指定的位置添加和移除项目,动态数组会自动重新调整它的大小。它也允许在列表中进行动态内存分配、增加、搜索、排序各项。

下表列出了 ArrayList 类的一些常用的 属性

属性描述
Capacity获取或设置 ArrayList 可以包含的元素个数。
Count获取 ArrayList 中实际包含的元素个数。
IsFixedSize获取一个值,表示 ArrayList 是否具有固定大小。
IsReadOnly获取一个值,表示 ArrayList 是否只读。
IsSynchronized获取一个值,表示访问 ArrayList 是否同步(线程安全)。
Item[Int32]获取或设置指定索引处的元素。
SyncRoot获取一个对象用于同步访问 ArrayList。

下表列出了 ArrayList 类的一些常用的 方法

序号方法名 & 描述
1public virtual int Add( object value ); 在 ArrayList 的末尾添加一个对象。
2public virtual void AddRange( ICollection c ); 在 ArrayList 的末尾添加 ICollection 的元素。
3public virtual void Clear(); 从 ArrayList 中移除所有的元素。
4public virtual bool Contains( object item ); 判断某个元素是否在 ArrayList 中。
5public virtual ArrayList GetRange( int index, int count ); 返回一个 ArrayList,表示源 ArrayList 中元素的子集。
6public virtual int IndexOf(object); 返回某个值在 ArrayList 中第一次出现的索引,索引从零开始。
7public virtual void Insert( int index, object value ); 在 ArrayList 的指定索引处,插入一个元素。
8public virtual void InsertRange( int index, ICollection c ); 在 ArrayList 的指定索引处,插入某个集合的元素。
9public virtual void Remove( object obj ); 从 ArrayList 中移除第一次出现的指定对象。
10public virtual void RemoveAt( int index ); 移除 ArrayList 的指定索引处的元素。
11public virtual void RemoveRange( int index, int count ); 从 ArrayList 中移除某个范围的元素。
12public virtual void Reverse(); 逆转 ArrayList 中元素的顺序。
13public virtual void SetRange( int index, ICollection c ); 复制某个集合的元素到 ArrayList 中某个范围的元素上。
14public virtual void Sort(); 对 ArrayList 中的元素进行排序。
15public virtual void TrimToSize(); 设置容量为 ArrayList 中元素的实际个数。

泛型

定义⼀个泛型类就是指的是,定义⼀个类,这个类中某些字段的类型是不确定的,这些类 型可以在类构造的时候确定下来,举例: 创建⼀个类处理int类型和double类型的相加

class ClassA<T>{
  private T a;
  private T b;
  public ClassA(T a,T b){
      this.a = a ;this.b = b;
  }
  public T GetSum(){
      return a+“”+b;
  }
}

定义泛型⽅法就是定义⼀个⽅法,这个⽅法的参数的类型可以是不确定的,当调⽤这个⽅ 法的时候再去确定⽅法的参数的类型。 实现任意类型组拼成字符串的⽅法

public static T GetSum<T>(T a,T b){
    return a+""+b;
}
GetSum<int>(23,12);
GetSum<double>(23.2,12);

使用泛型是一种增强程序功能的技术,具体表现在以下几个方面:

  • 它有助于您最大限度地重用代码、保护类型的安全以及提高性能。
  • 您可以创建泛型集合类。.NET 框架类库在 System.Collections.Generic 命名空间中包含了一些新的泛型集合类。您可以使用这些泛型集合类来替代 System.Collections 中的集合类。
  • 您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
  • 您可以对泛型类进行约束以访问特定数据类型的方法。
  • 关于泛型数据类型中使用的类型的信息可在运行时通过使用反射获取。

泛型限定条件

在声明泛型方法/泛型类的时候,可以给泛型加上一定的约束来满足我们特定的一些条件。

using System;
using System.Web.Caching;

namespace Demo.CacheManager{
   public class CacheHelper<T> where T:new(){

   }
}
  • T:结构(类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型)
  • T:类 (类型参数必须是引用类型,包括任何类、接口、委托或数组类型)
  • T:new() (类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时new() 约束必须最后指定)
  • T:<基类名> 类型参数必须是指定的基类或派生自指定的基类
  • T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。
  • T:U

泛型委托

您可以通过类型参数定义泛型委托。

delegate T NumberChanger<T>(T n);

THE END