C#基础笔记(2)—— C# 类

C# Class专题

一、C# 类

1. 什么是类class

class(类),是面向对象中的概念,是面向对象编程的基础。类是对现实生活中一类具有共同特征的事务的抽象,用来清晰管理你的行为。

2. 如何定义实例化一个class

class的实例化:定义一个类后,就必须实例化才能使用。实例化就是创建一个对象的过程。在C#中,使用new关键字来创建。

类 对象 = new 类 () ;

类的声明是以关键字class开始,后跟类的名称组成的。

类的实例是以关键字new开始,后跟类的名称组成的。

3. 类内的变量&函数

在类内声明的非静态变量,称之为 普通成员变量。

在类内声明的变量之前追加static关键字,称之为 静态成员变量。

在类内声明的非静态函数,称之为 普通成员函数。

在类内声明的函数之前追加static关键字,称之为 静态成员函数。

在类的内部声明的静态变量或函数,若想访问,必须通过类名来访问;普通成员变量或函数通过实例化的对象来访问。

4. 构造函数&析构函数

构造函数:

public 类名()

{

……

}

析构函数:

~类名()

{

……

}

// 构造函数   发生在new实例时,会被自动执行。 构造函数可以携带参数,当你显式地编写了自己的构造函数后,系统将不再提供默认的构造函数
// 析构函数   当当前类对象被销毁时,会被自动执行

// 作用域
// 函数内声明的变量(包括类对象),在执行完本函数时,会被自动销毁
// 但是,在类内声明的成员变量(包括类对象),只有当前类被销毁时,那它管理的其他变量(包括类对象)才会被销毁

// 如果不加命名空间,定义的类将在全局命名空间之下
namespace MySpace
{
   public class Myclass
  {
       public int a;
       // 构造函数
       public Myclass()   // 无参构造函数
      {
           Debug.Log("MySpace命名空间下的MyClass的构造函数执行");
      }

       public Myclass(int value)   // 有参构造函数
      {
           a = value;
           Debug.Log("a = ", a);
      }

       // 析构函数
       ~Myclass()
      {
           Debug.Log("MySpace命名空间下的MyClass的析构函数执行");
      }

       public void Show()
      {
           Debug.Log("MySpace命名空间下的MyClass的Show函数执行");
      }
  }
}

public class function : MonoBehaviour
{
   void Start()
  {
       MySpace.Myclass myclass = new MySpace.Myclass();
       int b = myclass.a;
       myclass.Show();
  }

   void Update()
  {
       // Input 键盘鼠标监听工具类
       if (Input.GetMouseButton(0))  // 0 鼠标左键   1 鼠标右键   2 鼠标滚轮  
      {
           Destroy(this.gameObject);

      }
  }
}

5. 访问修饰符

所谓的访问修饰符,指的是 是否能够访问的到。

  1. Public:任何公有成员可以被外部的类访问。

  2. Private:只有同一个类的函数可以访问它的私有成员。

  3. Protected:该类的内部和继承类可以访问。

  4. internal:同一个程序集(命名空间)的对象可以访问。

  5. Protected internal:3和4的并集,符合任意一条可以访问。

范围比较:

private < internal/protected < protected internal < public

protected限定的是只有在继承的子类中才可以访问,可以跨程序集

internal限定的是只有在同一个程序集中才可以访问,可以跨类

 

二、C#面向对象

1. 继承

继承是面向对象程序设计中最重要的概念之一,继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得容易。同时也有利于重用代码和节省开发时间。

当创建一个类时,不需要完全重写新的成员变量和成员函数,只需要设计一个新的类,继承已有的类的成员即可。这个已有的类被称为 基类,这个新的类被称为 派生类

C#中用“ : ”表示继承。C#不支持多重继承。

继承,就是将 共用的属性或方法抽离到基类 的过程,这个思维称之为面向对象

 

2. 封装

封装,被定义为“把一个或多个项目封闭在一个物理的或逻辑的包中”。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。

封装,是将实现细节通过接口的方式暴露给第三方,而不需要关心实现细节。

封装和抽象是相辅相成的,抽象允许相关信息可视化,而封装则是使开发者实现所需级别的抽象。

C#的封装根据具体的需要,设置使用者的权限,并通过 访问修饰符 来实现。

访问修饰符

  1. Public:任何公有成员可以被外部的类访问。

  2. Private:只有同一个类的函数可以访问它的私有成员。

  3. Protected:该类的内部和继承类可以访问。

  4. internal:同一个程序集(命名空间)的对象可以访问。

  5. Protected internal:3和4的并集,符合任意一条可以访问。

范围比较:

private < internal/protected < protected internal < public

虚函数

virtual代表虚函数,意味着 子类可以覆盖其实现,如果子类不覆盖,那将使用父类的同名函数。

子类使用override重写虚函数。

3. 多态

多态,是同一个行为具有多个不同表现形式或形态的能力;多态,就是同一个接口,使用不同的实例而执行不同操作。

  1. 静态多态(编译时)

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

  2. 动态多态(运行时)

    在运行时,根据实例对象,执行同一个函数的不同行为。

    运行时多态,在运行前无法确认调用哪个方法,只有在运行时才能确定的方法,这种行为称之为动态多态。

    具体实现为,将派生类实例化对象赋给基类实例化的对象,用后者调用继承的方法。

    // 注意: 只能子类给父类对象赋值,不能反过来
    Rectangle rectangle = new Rectangle();
    Triangle triangle = new Triangle();
    Polygon baseParent1 = rectangle;
    Polygon baseParent2 = triangle;

    baseParent1.Show();  // 结果为rectangle show
    baseParent2.Show();  // 结果为triangle show

4. 重载和覆盖

覆盖,发生在继承关系中,通过virtualoverride实现,函数名和函数参数一模一样;

重载,发生在任何关系中,只要保证函数名字一致参数不一致(带参数和不带参数,带参数顺序不一致,或者参数个数不一致),即可实现重载。

5. this和base关键字

this:可访问当前类能访问到的属性和方法;

base:只能访问基类的属性和方法。

 

三、C#类的更多表现形式

1. 静态类

类可以声明为static,这将变成一个静态类,不得被继承,特点是仅包含静态成员或常量。

静态类的特点:

  1. 不能被实例化,意味着 不能使用new关键字创建静态类的实例;

  2. 仅包含静态成员或常量

  3. 不能被继承

  4. 不能包含实例构造函数,但可以包含静态构造函数

  5. 静态构造函数不可被调用

静态类一般用于 工具类

补充知识点:

  • const

    在类内声明的const常量

  • (1)外部访问时,必须通过类名进行访问;

  • (2)只能在声明时初始化,不允许在任何其他地方对其初始化(包括构造函数);

  • (3)在某种程度上,被const修饰的变量(常量)为不可变值。

 

  • readonly

    在类内声明的readonly常量

  • (1)readonly const不可共同修饰一个数据类型(基本数据量类型 + 自定义数据类型);

  • (2)readonly修饰的类型,可以被类的实例进行访问,但不可修改值;

  • (3)readonly的初始化只能发生在构造函数或声明中。

 

  • 变量访问修饰符的控制

  • 可通过访问修饰符构成的语法块,来实现类似 外部只读的效果。get set 以及学习 value赋值和访问代码的执行流程

    public int mValue3 {get; private set;}

     

 

  • 静态构造函数

  • (1)静态构造函数不需要增加访问修饰符;

  • (2)静态构造函数无论多少实例,都只被系统自动调用一次。

 

  • 静态类:

  • (1)静态类不允许有实例构造函数,也不能有析构函数,只允许存在一个静态构造函数(静态类的静态构造函数不会执行);

  • (2)静态类不允许被实例化;

  • (3)静态类中的成员必须是 静态成员或常量;

  • (4)静态类无法作为基类派生。

 

2. 密封类

类可以被声明为sealed,这将变成为一个密封类。

密封类的特点:

  1. 不能被继承,但可以继承别的类或接口;

  2. 密封类不能声明为抽象类;

  3. 密封类内的成员函数,不能声明为sealed

密封类一般用于 防止重写某些类或接口影响功能的稳定

密封类:

  • 不允许被继承

  • sealed和abstract无法共存;

  • 密封类内的函数,不允许增加sealed关键字;

  • 密封类 可以正常继承 常见类(普通类、抽象类)接口;

3. 抽象类

类可以被声明为abstract,这将变成一个抽象类。

抽象类的特点:

  1. 不能被实例化,意味着 不能使用new关键字创建实例;

  2. 可只提供部分函数实现,也可仅声明抽象函数。

抽象类一般用在什么地方?

抽象类是提炼出了一些类共有的属性或函数接口的组织,为子类提供设计思路,配合多态多用于代码架构设计。

抽象类 1. 不允许实例化 2. 支持构造函数 3. 抽象类可继承抽象类 4. 静态构造函数只执行一次,但是其他的构造函数则根据不同实例,分别再次调用 5. 允许存在 virtual 虚函数 6. 若函数声明为abstract,则不允许包含函数体,子类必须显式覆盖父类的该方法。

4. 泛型类

类名后可以添加<T1, T2, T3……>,这将变成一个泛型类。泛型T1,T2,T3可以通过where关键字 来限定类型。

泛型类的特点:

  1. 在声明时可以不指定具体类型,但是在new实例化时必须指定T类型

  2. 可指定泛型类型约束;

  3. 如果子类也是泛型,那么继承的时候可以不指定具体类型。

泛型类一般用于 处理一组功能一样,但类型不同的任务

// 需求:
//   在类中定义一个数组,让这个类具备设置数据和访问数据的能力。

/* .NET含有以下5种泛型约束
* where T : class | T必须是一个类
* where T : struct | T必须是一个结构类型
* where T : new() | T必须要有一个无参构造函数
* where T : NameOfBaseClass | T必须继承名为NameOfBaseClass的类
* where T : NameOfInterface | T必须实现名为NameOfInterface的接口
*/
public class MyClassType
{
   public int a;

   public MyClassType(int value)
  {
       this.a = value;
  }
}

public class MyClass15<T> where T:MyClassType
{
   private T[] m_array;

   public MyClass15(int size)
  {
       m_array = new T[size];
  }

   public void Set(int index, T value)
  {
       m_array[index] = value;
  }
   public int Get(int index)
  {
       return m_array[index].a;
  }
}

public class TClass : MonoBehaviour
{
   void Start()
  {
       /*MyClass15 myClass = new MyClass15(5);
       myClass.Set(0, 1);
       myClass.Set(1, 2);
       int a = myClass.Get(0);
       int b = myClass.Get(1);

       Debug.LogFormat("第{0}位,值为{1}", 0, a);
       Debug.LogFormat("第{0}位,值为{1}", 1, b);*/

       /*MyClass15<string> myClass = new MyClass15<string>(5);
       myClass.Set(0, "哈哈哈");
       myClass.Set(1, "嘿嘿嘿");

       string a = myClass.Get(0);
       string b = myClass.Get(1);

       Debug.LogFormat("第{0}位,值为{1}", 0, a);
       Debug.LogFormat("第{0}位,值为{1}", 1, b);*/

       MyClass15<MyClassType> myClass = new MyClass15<MyClassType>(5);
       myClass.Set(0, new MyClassType(1));
       myClass.Set(1, new MyClassType(2));

       int a = myClass.Get(0);
       int b = myClass.Get(1);

       Debug.LogFormat("第{0}位,值为{1}", 0, a);
       Debug.LogFormat("第{0}位,值为{1}", 1, b);

  }
}

// 还有一种情况,基类是泛型类
public class Parent<T, X>
{

}

public class Child1<T, X> : Parent<T, X>
{

}

public class child2 : Parent<int, string>
{

}

public class child3<T, X, Y, A> : Parent<T, X>
{

}

public class child4<T, X> : Parent<int, string>
{
   
}

5. 接口

interface + name,这将变成一个接口。

接口的特点:

  1. 接口只声明接口函数,不能包含实现

  2. 接口函数访问修饰符,必须是public,默认也就是public;

  3. 接口成员函数的定义是派生类的责任,接口提供了派生类应遵循的标准结构;

  4. 接口不可被实例化,即不可 new ;

  5. 接口可继承其他接口,可进行多继承;

  6. 一个类不能继承多个类,但是可以继承多个接口。

接口一般用在 约束一些行为规范时 。

C#中一个类继承接口,必须实现接口中定义的函数方法,实现方法可分为隐式实现和显式实现:

public interface BaseInterface1
{
   void ShowWindow();
   void HideWindow();    
}

public interface BaseInterface2
{
   void PlaySound();
   void CloseSound();
}

public interface MyInterface : BaseInterface1, BaseInterface2
{
}

public class MyClass16 : MyInterface
{
   // 显式实现
   void BaseInterface2.CloseSound()
  {
       throw new System.NotImplementedException();
  }

   void BaseInterface1.HideWindow()
  {
       throw new System.NotImplementedException();
  }

   void BaseInterface2.PlaySound()
  {
       throw new System.NotImplementedException();
  }

   void BaseInterface1.ShowWindow()
  {
       throw new System.NotImplementedException();
  }
   
   // 隐式实现
   public void CloseSound()
  {
       throw new System.NotImplementedException();
  }

   public void HideWindow()
  {
       throw new System.NotImplementedException();
  }

   public void PlaySound()
  {
       throw new System.NotImplementedException();
  }

   public void ShowWindow()
  {
       throw new System.NotImplementedException();
  }
}

显式和隐式实现接口的区别在于:

对于隐式实现的成员,既可以通过类对象实例来访问,也可以通过接口来访问;而对于显式实现的对象只能通过接口来访问,不能使用类对象来访问

6. 接口和抽象类的区别

相同点:

  1. 都可以被继承;

  2. 都不能被实例化;

  3. 都可以包含方法声明;

  4. 派生类必须实现未实现的方法。

不同点:

  1. 抽象类可以定义字段、属性、方法实现;接口只能方法声明,不能包含字段;

  2. 接口不允许有构造函数(包括普通构造函数和静态构造函数),而抽象类可以;

  3. 接口不允许有函数实现,而抽象类可以;

  4. 抽象类只能被单一继承;接口可以多重继承;

  5. 抽象类更多的是定义一系列紧密相关的类间;而接口大多数是关系疏松但都实现某一功能的类中。

  6. 函数的访问修饰符,接口默认为public,不允许改变为private;抽象类默认private,函数前若是abstract,那访问修饰符也不能是private,但是非abstract声明的函数 是允许private protected的。

 

四、Class专题的更多知识

1. 结构体Struct

struct + name {…………} 声明一个结构体。

结构体是值类型数据结构,它使得一个单一变量可以存储各种数据类型的相关数据。

结构体的特点:

  1. 结构体可带有方法、字段、索引、属性、运算符方法和事件;

  2. 结构体可定义构造函数,但不能定义析构函数、不能定义无参构造函数。无参构造函数是自动定义的,且不能被改变;

  3. 与类不同,结构体不能继承其他结构体或类,但是可以实现一个或多个接口;

  4. 结构体不能作为其他结构体或类的基础结构;

  5. 结构成员不能指定为abstract、virtual 或 protected;

  6. 结构体不用通过 new 来实例化。

struct和class的异同

相同点 1. 都支持静态构造函数、有参构造函数; 2. 都支持自定义函数; 3. 结构体和类对于const修饰的变量的使用方式是一样的。

不同点 1. 构造函数:结构体 不允许定义无参构造函数,只允许定义有参构造函数,但是类可以; 2. 析构函数:结构体不允许定义析构函数,但类可以; 3. 函数修饰符:结构体函数不允许声明为virtual、protected,但是类可以; 4. 类型修饰符:结构体类型不允许声明为abstract,但是类可以; 5. 关于变量 (1)普通变量 结构体声明的全局普通变量(不带修饰符),不能在声明时直接赋值,只能在构造函数里赋值,但是类都可以; (2)readonly类型的变量 结构体声明的全局readonly变量,只能在构造函数里赋值,而类都可以。 6. 关于继承 结构体之间不可以互相继承,但是类与类之间是可以继承的(sealed密封类除外)。 7. 在使用上

(1)访问变量 ​ 结构体访问成员变量,给变量显式赋值,就可直接访问;而类必须实例化后才能访问; ​ 结构体如果不通过new初始化,是不可以直接访问其内部变量的(const除外)。 ​ (2)访问函数 ​ 结构体变量和类对象 必须进行初始化,才可以访问

  1. new (1)结构体属于值类型,结构体的new,并不会在堆上分配内存,仅仅是调用结构体的构造函数初始化而已; (2)类属于引用类型,类的new,会在堆上分配内存,而且也会调用类的构造函数。

 

2. is as 显式/隐式转换

isas就是为了解决 强制类型转换可能导致异常 的问题而使用的。

is:检查对象类型的兼容性,并返回结果true(false)

as:检查对象类型的兼容性,并返回转换结果,如果不兼容则返回null

isas的区别在于:

  1. 使用 as 更加安全,使用 as 如果转换失败,返回 Null ,不会抛异常;

  2. 使用 as 效率会更高。使用 is 时会检查两次对象的类型,一次是核实,一次是强制类型转换,而使用 as 只进行了一次对象类型的检查。

//使用is
if(a is Dog)
{
   Dog d = (Dog)a;
  ...
}

//使用as
Dog d = a as Dog;
if(d!=null)
{
  ...
}

 

强制(显式)类型转换。形式通过(type)a来表示,一般用于 高精度数据类型转换为低精度数据类型。

隐式类型转换。发生在 低精度数据类型自动转换为高精度数据类型。

 

前置知识:

引用类型: string、数组、类、接口

值类型: (s)byte、(u)short、(u)int、(u)long、bool、enum、struct

 

is:可检测 值类型和引用类型,成功返回true,否则返回false。

string a = "abc";
bool b = a is string; // b=true
bool c = a is int; // c=false

as:首先会判断 源数据类型 是否是 目标数据类型,不是的话编译器会报错。as转换成功,返回源数据类型存储的数据,否则返回空。用于检测引用类型。

int[] d = {1, 2, 3};
int[] c = d as int[];
if (c != null)
{
   Debug.Log(c.Length);  // 打印3
}
public class Base { }
public class Son : Base { }

public class Test
{
void start()
{
Son son = new Son();
if (son as Son != null)
{
Debug.Log("son as Son"); // 成功打印语句
}

if (son as Base != null)
{
Debug.Log("son as Base"); // 成功打印语句
}
}
}

 

3. 什么是装箱&拆箱

类型:

  • 值类型

    • 内置值类型

    • 用于定义的值类型

    • 枚举类型

  • 引用类型

    • 指针类型

    • 接口类型

    • 自描述类型

      • 数组

      • 类类型

        • 用户定义的类

        • 已装箱的值类型

        • 委托

装箱:值类型转换为引用类型;

拆箱:引用类型转换为值类型。

public class Box
{
void Start()
{
int a = 20;
Object b = (Object) a; // 装箱,发生GC 内存分配

int c = (int) b; // 拆箱
}
}

 

4. 值类型与引用类型的内存模型

堆和栈
1. C#中的值类型和引用类型

 

  • 值类型只需要一段单独的内存,用于存储实际的数据(单独定义时放在栈中)。

  • 引用类型需要两段内存:

a. 第一段存储实际的数据,它总是位于堆中;

b. 第二段是一个引用,指向数据在堆中的存放位置。

  1. 值类型与引用类型的存储方式

    • 值类型:值类型总是分配在它声明的地方,作为局部变量时,存储在栈上;作为类对象的字段时,则跟随此类存储在堆中。

    • 引用类型:引用类型存储在堆中。类型实例化时,会在堆中开辟一部分空间存储类的实例。类对象的引用还是存储在栈中。

  2. 案例

    // 值类型,保存在栈中
    int num = 100;
    // 引用类型,保存在堆中
    int[] nums = {1, 2, 3, 4, 5};
    // 输出
    Console.WriteLine(num); // 输出结果:100
    Console.WriteLine(nums); // 输出结果:System.Int32[]
    // 案例表明,num为值类型,直接输出了值,而nums为引用类型,输出了一个引用
  3. 值类型与引用类型的区别

    1. 值类型和引用类型都继承自 System.Object 类。不同之处在于,几乎所有引用类型都是直接从System.Object继承,而值类型则是继承System.Object的子类System.ValueType类。

    2. 在给引用类型的变量赋值时,其实只是赋值了对象的引用;而给值类型变量赋值时,是创建了一个副本。

2. 堆与栈简单理解

C#程序在CLR上运行时,内存从逻辑上划分两大块:栈、堆。这两个基本元素组成了C#程序的运行环境。

  1. 堆与栈

    堆:在C语言中叫堆,在C#中其实叫托管堆;

    栈:即堆栈,因为和堆一起叫别扭,所以简称为栈。

  2. 托管堆

    托管堆不同于堆,它是由CLR(公共语言运行库Common Language Runtime)管理,当堆中满了之后,会自动清理堆中的垃圾。所以,做 .NET 开发,不需要关心内存释放的问题。

  3. 内存堆栈和数据堆栈

    1. 内存堆栈:存在内存中的两个存储区(堆区,栈区)。

      • 栈区:存放函数的参数、局部变量、返回数据等值,由编译器自动释放。

      • 堆区:存放着引用类型的对象,由CLR释放。

    2. 数据堆栈:是一种后进先出的数据结构,它是一个概念,主要是栈区。

 

3. 堆与栈区别分析

栈通常保存着我们代码执行的步骤,如一个值类型的变量的初始化或者一个方法的声明。而堆上存放的多是对象、数据等。

我们可以将栈想象成一个接一个叠放在一起的盒子。当我们使用时,每次从最顶部取走一个盒子。同样我们的栈就是如此,当一个方法(或类型)被调用完成时,就从栈顶取走,接着下一个,这就是所谓的“先进后出”。

堆则不然,堆更像是一个仓库,储存的是我们使用的各种对象等信息,当我们需要调用时,会去里面自行寻找并调用。跟栈不同的是它们被调用完毕后不会立即被清理掉。

注意:栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕时,立马释放。而堆则需要GC(Garbage Collection垃圾回收)清理。

 

4. 堆与栈的存储

我们把内存分为堆空间和栈空间,区别如下:

  1. 栈空间比较小,但是读取速度快;

  2. 堆空间比较大,但是读取速度慢。

1、栈的深入讲解

栈(stack)最明显的特征就是“先进后出”,本质上来讲堆栈也是一种线性结构,符合线性结构的基本特点:即每个节点有且只有一个前驱节点和一个后续节点。栈把所有操作限制在“只能在线性结构的某一端”进行,而不能在中间插入或删除元素。我们把数据放入栈顶称为入栈(push),从栈顶删除数据称为出栈(pop)。

 

2、堆的深入讲解

堆(Heap)是一块内存区域,与栈不同,堆里的内存能够以任意顺序存入和移除。

 

 

5. GC(Garbage Collection)垃圾收集器介绍

CLR 的 GC 就是内存管理机制,我们写程序时不需要关心内存的使用,因为这些都是CLR帮我们做了。

 

6. 内存空间分配实例
// 测试程序
public class TestClass
{
public string name = "张三";
public int num = 10;
}

public class HeapStack : MonoBehaviour
{
void Start()
{
Example();
}

void Example()
{
TestClass[] test = { new TestClass(), new TestClass(), new TestClass() };
TestClass test01 = test[0];
test01.name = "李四";
TestClass test02 = test[0];
test02.name = "王五";
Debug.Log(test01.name);

}
}

结果:

 

原因在于test01和test02在栈中的引用指向的是同一个元素:test[0],而数组test[]作为引用类型,存储的元素为类对象,同样为引用类型,指向的是同一个name,故两次修改同一个值,输出为王五。

 

posted @   Dukenone  阅读(1035)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
点击右上角即可分享
微信分享提示