翼度科技»论坛 编程开发 .net 查看内容

C# 面向对象

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
前言

C# 是一种面向对象、类型安全的语言。
❓什么是面向对象
面向对象编程(OOP)是如今多种编程语言所实现的一种编程范式,包括 Java、C++、C#。
面向对象编程将一个系统抽象为许多对象的集合,每一个对象代表了这个系统的特定方面。对象包括函数(方法)和数据。一个对象可以向其他部分的代码提供一个公共接口,而其他部分的代码可以通过公共接口执行该对象的特定操作,系统的其他部分不需要关心对象内部是如何完成任务的,这样保持了对象自己内部状态的私有性。
面向对象和面向过程的区别:
面向对象:用线性的思维。与面向过程相辅相成。在开发过程中,宏观上,用面向对象来把握事物间复杂的关系,分析系统。微观上,仍然使用面向过程。
面向过程:是一种是事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这写步骤实现,并按顺序调用。
简单来说:用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,就是在米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。
这个比喻还是比较贴切的。
❓为什么使用面向对象编程
面向对象编程,可以让编程更加清晰,把程序中的功能进行模块化划分,每个模块提供特定的功能,同时每个模块都是孤立的,这种模块化编程提供了非常大的多样性,大大增加了重用代码的机会,而且各模块不用关心对象内部是如何完成的,可以保持内部的私有性。简单来说面向对象编程就是结构化编程,对程序中的变量结构划分,让编程更清晰。
准确地说,本文所提及到的特性是一种特别的面向对象编程方式,即基于类的面向对象编程(class-based OOP)。当人们谈论面向对象编程时,通常来说是指基于类的面向对象编程。
类 - 实际上是创建对象的模板。当你定义一个类时,你就定义了一个数据类型的蓝图。这实际上并没有定义任何的数据,但它定义了类的名称,这意味着什么,这意味着类的对象由什么组成及在这个对象上可执行什么操作。对象是类的实例。构成类的方法和变量称为类的成员。
类的定义和使用

类中的数据和函数称为类的成员

  • 数据成员

    • 数据成员是包含类的数据 - 字段,常量和事件的成员。

  • 函数成员

    • 函数成员提供了操作类中数据的某些功能 - 方法,属性,构造器(构造方法)和终结器(析构方法),运算符,和索引器


拿控制台程序为例,当我们创建一个空的控制台项目,在Main()函数里编程的时候就是在Program类里面操作的:

而且,我们可以发现,Program类和保存它的文件的文件名其实是一样的Program.cs,一般我们习惯一个文件一个类,类名和文件名一致。当然了,这不是说一个文件只能写一个类,一个文件是可以包含多个类的。
新建一个Customer类来表示商店中购物的顾客:
  1. class Customer
  2.     {
  3.         public string name;
  4.         public string address;
  5.         public int age;
  6.         public string createTime;   // 加入会员的时间
  7.         public void Show()
  8.         {
  9.             Console.WriteLine("名字:" + name);
  10.             Console.WriteLine("地址:" + address);
  11.             Console.WriteLine("年龄:" + age);
  12.             Console.WriteLine("创建时间:" + createTime);
  13.         }
  14.     }
复制代码
Customer类里有四个公有字段和一个共有方法Show()来输出顾客信息。
创建Customer类的对象:
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     customer.name = "Test";
  5.     customer.address = "Test01";
  6.     customer.age = 24;
  7.     customer.createTime = "2023-02-27";
  8.     customer.Show();
  9.     Console.ReadKey();
  10. }
复制代码
通过类创建的变量被称之为对象,这个过程我们叫他实例化。所有对象在使用之前必须实例化,仅仅声明一个对象变量或者赋值为null都是不行的。到现在看来,其实简单的类在定义和使用起来跟结构体是差不多的,只不过结构体在创建的时候没有实例化的过程,因为结构体是值类型的数据结构,而类是引用类型。
小小练习

推荐大家开发过程中,尽量一个文件里面一个类,当然一个文件可以放多个类,但管理起来不方便,一个类一个文件管理起来方便,如果程序很小,怎么写都无所谓,如果程序大或团队合作,最好一个类一个文件。
而且一个类定义也可以在多个文件中哦 - partial className
定义一个车辆Vehicle类,具有Run、Stop等方法,具有 Speed ( 速度 ) 、MaxSpeed ( 最大速度 ) 、Weight ( 重量 )等(也叫做字段)。
使用这个类声明一个变量(对象)。
  1. static void Main(string[] args)
  2. {
  3.     Vehicle vehicle = new Vehicle();
  4.     vehicle.brand = "BMW X5";
  5.     vehicle.speed = 90;
  6.     vehicle.maxSpeed = 215;
  7.     vehicle.weight = 32;
  8.     vehicle.Run();
  9.     vehicle.Stop();
  10.     Console.ReadKey();
  11. }
  12. class Vehicle
  13. {
  14.     // 字段
  15.     public string brand;
  16.     public int speed;
  17.     public int maxSpeed;
  18.     public float weight;
  19.     // 方法
  20.     public void Run()
  21.     {
  22.         Console.WriteLine("Run!");
  23.     }
  24.     public void Stop()
  25.     {
  26.         Console.WriteLine("Stop!");
  27.     }
  28. }
复制代码
定义一个向量Vector类,里面有x,y,z三个字段,有取得长度的方法,有设置属性Set的方法使用这个类声明一个变量(对象)。
  1. class Vector3
  2. {
  3.     // 字段
  4.     private double x;
  5.     private double y;
  6.     private double z;
  7.     // 属性【X】 - SetX为一个普通方法
  8.     public void SetX(double temp)
  9.     {
  10.         x = temp;
  11.     }
  12.     public void SetY(double temp)
  13.     {
  14.         y = temp;
  15.     }
  16.     public void SetZ(double temp)
  17.     {
  18.         z = temp;
  19.     }
  20.     // 方法
  21.     public double GetLength()
  22.     {
  23.         return Math.Sqrt(x * x + y * y + z * z);
  24.     }
  25. }
复制代码
属性 - 是类的一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们是称为“访问器”的特殊方法。 此功能使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。
这里先不详细说,后续章节再展开。Vector3类里面的Set*属性是用来给x,y,z赋值的,可以看到与之前的简单类不同的是,Vector3类里的字段是private也就是私有的,这意味着在类的外部是没有办法访问这写字段的,它只在类自己内部是大家都知道的,到外面就不行了。
这里一开始写错了,类Vector3中的SetX、SetY 和 SetZ 方法是普通的方法,而不是属性。它们仅仅是修改和访问实例中私有字段的方法。它们需要一个参数才能设置相应的字段值,而属性是通过访问器方法来设置或获取字段的值,并且不需要额外的参数。
public 和 private 访问修饰符


  • 访问修饰符(C# 编程指南)
  • public修饰的数据成员和成员函数是公开的,所有的用户都可以进行调用。
  • private修饰词修饰的成员变量以及成员方法只供本类使用,也就是私有的,其他用户是不可调用的。
public和private这两个修饰符其实从字面意思就可以理解,没什么不好理解的,前者修饰的字段大家可以随意操作,千刀万剐只要你乐意,而后者修饰的字段就不能任你宰割了,你只能通过Get、Set进行一系列的访问或者修改。
举个例子,生活中每个人都有名字、性别,同时也有自己的银行卡密码,当别人跟你打交道的时候,他一般会先得知你的名字,性别,这些告诉他是无可厚非的,但是当他想知道你的银行卡密码的时候就不太合适了对吧。假设我们有一个类Person,我们就可以设置Name,Sex等字段为公有的public,大家都可以知道,但是银行卡密码就不行,它得是私有的,只有你自己知道。但是加入你去银行ATM机取钱,它就得知道你的银行卡密码才能让你取钱对吧,前面我们已经了密码是私有的,外部是没办法访问的,那该怎么办呢,这个时候就用到属性了。我们用Get获取密码,用Set修改密码。
放在代码里面:
  1. static void Main(string[] args)
  2. {
  3.     Vector3 vector = new Vector3();
  4.     vector.w = 2;
  5.     vector.SetX(1);
  6.     Console.WriteLine(vector.GetX());
  7.     Console.ReadKey();
  8. }
  9. class Vector3
  10. {
  11.     // 字段
  12.     private double x;
  13.     public double w;
  14.     // 属性
  15.     public void SetX(double temp)
  16.     {
  17.         x = temp;
  18.     }
  19.     // ......
  20.     public double GetX()
  21.     {
  22.         return x;
  23.     }
  24. }
复制代码
w字段在类外部可以直接操作,x只能通过Get、Set来操作。
日常开发推荐不要把字段设置为共有的,至少要有点访问限制,当然了除了这两个修饰符,还有其他的,比如internal、protect等等,以后的文章可能会专门来写(❓)。
使用private修饰符除了多了一堆属性(访问器)有什么便利吗?显然得有,public的字段你在设置的时候说啥就啥,即使它给到的内容可能不适合这个字段,在后者,我们可以在属性里设置一些限制或者是操作。比如,Vector3类的x字段显然长度是不会出现负值的,这时候我们就可以在SetX里面做些限制:
  1. public void SetX(double temp)
  2. {
  3.     if (temp<0)
  4.     {
  5.         Console.WriteLine("数据不合法。");
  6.     }
  7.     x = temp;
  8. }
复制代码
C# 7进一步允许在set访问器上使用表达式体:
  1. public class Person
  2. {
  3.     public int age;
  4.     public string name = "unknown";
  5. }
  6. class Example
  7. {
  8.     static void Main()
  9.     {
  10.         var person = new Person();
  11.         Console.WriteLine($"Name: {person.name}, Age: {person.age}");
  12.         // Output:  Name: unknown, Age: 0
  13.     }
  14. }
复制代码
自动实现的属性

当属性访问器中不需要任何其他逻辑时,自动实现的属性会使属性声明更加简洁。
自动实现的属性是C# 3.0引入的新特性,它可以让我们在不显式定义字段和访问器方法的情况下快速定义一个属性。具体来说,一个属性包含一个字段和两个访问器方法,其中get和set访问器方法都是自动实现的。
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     // Output :我一个构造函数。
  5.     Console.ReadKey();
  6. }
  7. class Customer
  8. {
  9.     public string name;
  10.     public string address;
  11.     public int age;
  12.     public string createTime;   // 加入会员的时间
  13.     public Customer()
  14.     {
  15.         Console.WriteLine("我一个构造函数。");
  16.     }
  17.     public void Show()
  18.     {
  19.         Console.WriteLine("名字:" + name);
  20.         Console.WriteLine("地址:" + address);
  21.         Console.WriteLine("年龄:" + age);
  22.         Console.WriteLine("创建时间:" + createTime);
  23.     }
  24. }
复制代码
属性初始化器

C# 6开始支持自动属性的初始化器。其写法就像初始化字段一样:
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     Customer customer2 = new Customer("光头强", "狗熊岭", 30, "2305507");
  5.     customer2.Show();
  6.     // Output:
  7.     // 我一个构造函数。
  8.     // 名字:光头强
  9.     // 地址:狗熊岭
  10.     // 年龄:30
  11.     // 创建时间:2305507
  12.     Console.ReadKey();
  13. }
  14. class Customer
  15. {
  16.     public string name;
  17.     public string address;
  18.     public int age;
  19.     public string createTime;   // 加入会员的时间
  20.     public Customer()
  21.     {
  22.         Console.WriteLine("我一个构造函数。");
  23.     }
  24.     public Customer(string arg1, string arg2, int arg3, string arg4)
  25.     {
  26.         name = arg1;
  27.         address = arg2;
  28.         age = arg3;
  29.         createTime = arg4;
  30.     }
  31.     public void Show()
  32.     {
  33.         Console.WriteLine("名字:" + name);
  34.         Console.WriteLine("地址:" + address);
  35.         Console.WriteLine("年龄:" + age);
  36.         Console.WriteLine("创建时间:" + createTime);
  37.     }
  38. }
复制代码
上述写法将``age`的值初始化为24。拥有初始化器的属性可以为只读属性:
  1. public Customer(string name,string address,int age,string createTime)
  2. {
  3.     this.name = name;
  4.     this.address = address;
  5.     this.age = age;
  6.     this.createTime = createTime;
  7. }
复制代码
就像只读字段那样,只读自动属性只可以在类型的构造器中赋值。这个功能适于创建不可变(只读)的对象。
匿名类型

匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断,是一个由编译器临时创建来存储一组值的简单类。如果需要创建一个匿名类型,则可以使用new关键字,后面加上对象初始化器,指定该类型包含的属性和值。例如:
​        var dude = new { Name = "Bob", Age = 23 };
编译器将会把上述语句(大致)转变为:
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     customer.SetAge(24);
  5.     Console.WriteLine(customer.GetAge());
  6.     // Output: 24
  7.     Console.ReadKey();
  8. }
  9. class Customer
  10. {
  11.     public string name;
  12.     public string address;
  13.     public int age;
  14.     public string createTime;
  15.     public void SetAge(int age)
  16.     {
  17.         this.age = age;
  18.     }
  19.     public int GetAge()
  20.     {
  21.         return this.age;    // 这里 this 可加可不加
  22.     }
  23.     public void Show()
  24.     {
  25.         Console.WriteLine("名字:" + name);
  26.         Console.WriteLine("地址:" + address);
  27.         Console.WriteLine("年龄:" + age);
  28.         Console.WriteLine("创建时间:" + createTime);
  29.     }
  30. }
复制代码
匿名类型只能通过var关键字来引用,因为它并没有一个名字。
堆、栈

程序在运行时,内存一般从逻辑上分为两大块 - 堆、栈。

  • 堆栈(Stack - 因为和堆一起叫着别扭,所以简称为栈):栈是一种先进后出(Last-In-First-Out,LIFO)的数据结构。当你声明一个变量时,它会自动地被分配到栈内存中,并且它的作用域仅限于当前代码块。在方法中声明的局部变量就是放在栈中的。栈的好处是,由于它的操作特性,栈的访问非常快,它也没有垃圾回收的问题。栈空间比较小,但是读取速度快。
  • 堆(Heap):堆是一种动态分配内存的数据结构。堆内存的大小不受限制,而且程序员可以控制它的生命周期,也就是说,在堆上分配的内存需要手动释放。堆空间比较大,但是读取速度慢。
堆和栈就相当于仓库和商店,仓库放的东西多,但是当我们需要里面的东西时需要去里面自行查找然后取出来,后者虽然存放的东西没有前者多,但是好在随拿随取,方便快捷。


栈是一种先进后出(Last-In-First-Out,LIFO)的数据结构。本质上讲堆栈也是一种线性结构,符合线性结构的基本特点:即每个节点有且只有一个前驱节点和一个后续节点。

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



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

GC

GC(Garbage Collector)垃圾回收器,是一种自动内存管理技术,用于自动释放内存。在.NET Framework中,GC由.NET的运行时环境CLR自动执行。在公共语言运行时 (CLR) 中,垃圾回收器 (GC) 用作自动内存管理器。 垃圾回收器管理应用程序的内存分配和释放。 因此,使用托管代码的开发人员无需编写执行内存管理任务的代码。 自动内存管理可解决常见问题,例如,忘记释放对象并导致内存泄漏,或尝试访问已释放对象的已释放内存。
通过GC进行自动内存管理得益于C#是一种托管语言。C#会将代码编译为托管代码。托管代码以中间语言(Intermediate Language, IL)的形式表示。CLR通常会在执行前,将IL转换为机器(例如x86或x64)原生代码,称为即时(Just-In-Time, JIT)编译。除此之外,还可以使用提前编译(ahead-of-time compilation)技术来改善拥有大程序集,或在资源有限的设备上运行的程序的启动速度。
托管语言是一种在托管执行环境中运行的编程语言,该环境提供了自动内存管理、垃圾回收、类型检查等服务。
托管执行环境是指由操作系统提供的一种高级运行时环境,例如Java虚拟机、.NET Framework、.NET Core 等。这种执行环境为程序提供了许多优势,例如:

  • 自动内存管理:托管执行环境为程序管理内存分配和释放,程序员无需手动管理内存,避免了内存泄漏和越界等问题。
  • 垃圾回收:托管执行环境提供了垃圾回收服务,自动回收不再使用的内存,提高了程序的性能和可靠性。
  • 类型检查:托管执行环境提供了强类型检查,防止了类型错误等问题。
  • 平台无关性:托管语言编写的程序可以在不同操作系统和硬件平台上运行,提高了程序的可移植性。
CLR中:

  • 每个进程都有其自己单独的虚拟地址空间。 同一台计算机上的所有进程共享相同的物理内存和页文件(如果有)。
  • 默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。
  • 作为一名应用程序开发人员,你只能使用虚拟地址空间,请勿直接操控物理内存。 垃圾回收器为你分配和释放托管堆上的虚拟内存。
  • 初始化新进程时,运行时会为进程保留一个连续的地址空间区域。 这个保留的地址空间被称为托管堆。 托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。
既然垃圾回收是自动进行的,那么一般什么时候GC会开始回收垃圾呢?

  • 系统具有低的物理内存。内存大小是通过操作系统的内存不足通知或主机指示的内存不足检测出来的。
  • 由托管堆上已分配的对象使用的内存超出了可接受的阈值。 随着进程的运行,此阈值会不断地进行调整。
  • 调用 GC.Collect 方法。几乎在所有情况下,你都不必调用此方法,因为垃圾回收器会持续运行。 此方法主要用于特殊情况和测试。
我们开发人员可以使用new关键字在托管堆上动态分配内存,不需要手动释放,GC会定期检查托管堆上的对象,并回收掉没有被引用的对象,从而释放它们所占用的内存。
❗❗❗需要注意的是,栈内存无需我们管理,同时它也不受GC管理。当栈顶元素使用完毕以后,所占用的内存会被立刻释放。而堆则需要依赖于GC清理。
值类型、引用类型

文章之前部分已经提到过C#是托管语言,在托管执行环境中运行的编程语言,该环境提供了强类型检查,所以与其他语言相比,C#对其可用的类型及其定义有更严格的描述 ———— C#是一种强类型语言,每个变量和常量都有一个类型,每个求值的表达式也是如此。 每个方法声明都为每个输入参数和返回值指定名称、类型和种类(值、引用或输出)。
所有的C#类型可以分为以下几类:

  • 值类型
  • 引用类型
  • 泛型类型
    C#泛型可以是值类型也可以是引用类型,具体取决于泛型参数的类型。
    如果泛型参数是值类型,那么实例化出来的泛型类型也是值类型。例如,List就是一个值类型,因为int是值类型。
    如果泛型参数是引用类型,那么实例化出来的泛型类型也是引用类型。例如,List就是一个引用类型,因为string是引用类型。
    需要注意的是,虽然泛型类型可以是值类型或引用类型,但是泛型类型的实例总是引用类型。这是因为在内存中,泛型类型的实例始终是在堆上分配的,无论它的泛型参数是值类型还是引用类型。因此,使用泛型类型时需要注意它的实例是引用类型。

  • 指针类型
    指针类型是C#中的一种高级语言特性,允许程序员直接操作内存地址。指针类型主要用于与非托管代码交互、实现底层数据结构等。指针类型在普通的C#代码中并不常见。

撇去指针类型,我们可以把C#中的数据类型分为两种:

  • 值类型 - 分两类:struct和enum,包括内置的数值类型(所有的数值类型、char类型和bool类型)以及自定义的struct类型和enum类型。
  • 引用类型 - 引用类型包含所有的类类型、接口类型、数组类型或委托类型。和值类型一样,C#支持两种预定义的引用类型:object和string。
❗❗❗ object类型是所有类型的基类型,其他类型都是从它派生而来的(包括值类型)。
各自在内存中的存储方式

在此之前,我们需要明白Windows使用的是一个虚拟寻址系统,该系统把程序可用的内存地址映射到硬件内存中的实际地址上,这些任务完全由Windows在后台管理。其实际结果是32位处理器上的每个进程都可以使用4GB的内存————不管计算机上实际有多少物理内存。这4个GB的内存实际上包含了程序的所有部分,包括可执行的代码、代码加载的所有DLL,以及程序运行时使用的所有变量的内容。这4个GB的内存称为虚拟地址空间、虚拟内存,我们这里简称它为内存。
我们可以借助VS在直观地体会这一特性,任意给个断点,把变量移到内存窗口就可以查看当前变量在内存中的地址以及存储的内容:

例举一些常用的变量:
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     customer.Age = 10;
  5.     Console.WriteLine(customer.Age);
  6.     // Output: 10
  7.     Console.ReadKey();
  8. }
  9. class Customer
  10. {
  11.     private string name;
  12.     private string address;
  13.     private int age;
  14.     private string createTime;
  15.     // 属性
  16.     public int Age
  17.     {
  18.         get
  19.         {
  20.             return this.age;
  21.         }
  22.         set // value 参数
  23.         {
  24.             this.age = value;
  25.         }
  26.     }
  27.     public void Show()
  28.     {
  29.         Console.WriteLine("名字:" + name);
  30.         Console.WriteLine("地址:" + address);
  31.         Console.WriteLine("年龄:" + age);
  32.         Console.WriteLine("创建时间:" + createTime);
  33.     }
  34. }
复制代码
它们在内存中是怎么存储的呢?


  • 值类型就直观的存储在堆中。
  • array1在栈中存储着一个指向堆中存放array1数组首地址的引用,array2和customer同理
  • name字符串,尽管它看上去像是一个值类型的赋值,但是它是一个引用类型,name对象被分配在堆上。
关于字符串在内存中的存储,虽然它是引用类型,但是它与引用类型的常见行为是有一些区别的,例如:字符串是不可变的。修改其中一个字符串,就会创建一个全新的string对象,而对已存在的字符串不会产生任何影响。例如:
  1. static void Main(string[] args)
  2. {
  3.     Customer customer = new Customer();
  4.     customer.Age = -10;
  5.     // 引发 ArgumentOutOfRangeException 异常
  6.     Console.ReadKey();
  7. }
  8. class Customer
  9. {
  10.     private string name;
  11.     private string address;
  12.     private int age;
  13.     private string createTime;
  14.     // 属性
  15.     public int Age
  16.     {
  17.         get
  18.         {
  19.             return this.age;
  20.         }
  21.         set // value 参数
  22.         {
  23.             if (value < 0)
  24.             {
  25.                throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
  26.             }
  27.             this.age = value;
  28.         }
  29.     }
  30. }
复制代码
借助VS的内存窗口:

s1也就是存储着a string字符串的地址是0x038023DC,再执行你就会发现s2的内存地址也是0x038023DC,但是当s1中存储的字符串发生变化时,s1的内存地址也会随之变化,但是s2的内存地址还是之前a string所在的位置。

也就是说,字符串的值在发生变化时并不会替换原来的值,而是在堆上为新的字符串值分配一个新的对象(内存空间),之前的字符串值对象是不受影响的【这实际上是运算符重载的结果】。
To sum up,值类型直接存储其值,而引用类型存储对值的引用。这两种类型存储在内存的不同地方:值类型存储在栈(stack)中,而引用类型存储在托管堆(managed heap)上。


  • 值类型只需要一段内存,总是分配在它声明的地方,做为局部变量时,存储在栈上;假如是类对象的字段时,则跟随此类存储在堆中。
  • 引用类型需要两段内存,第一段存储实际的数据【堆】,第二段是一个引用【栈】,用于指向数据在堆中的存储位置。引用类型实例化的时候,会在托管堆上分配内存给类的实例,类对象变量只保留对对象位置的引用,引用存放在栈中。
对象引用的改变

因为引用类型在存储的时候是两段内存,所以对于引用类型的对象的改变和值类型是不同的,以Customer类的两个对象为例:
  1. class Customer
  2. {
  3.     private string name;
  4.     private string address;
  5.     private int age;
  6.     private string createTime;
  7.     // 属性
  8.     public int Age  // 读 - 写
  9.     {
  10.         get
  11.         {
  12.             return this.age;
  13.         }
  14.         set // value 参数
  15.         {
  16.             if (value < 0)
  17.             {
  18.                 throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
  19.             }
  20.             this.age = value;
  21.         }
  22.     }
  23.     public string Name  // 只读
  24.     {
  25.         get { return this.name; }
  26.     }
  27.     public string Address   // 只写
  28.     {
  29.         set { this.address = value; }
  30.     }
  31. }
复制代码
执行结果为:
  1. class Customer
  2. {
  3.     private string name;
  4.     private string address;
  5.     private int age;
  6.     private string createTime;
  7.    
  8.     public int Age => age;        // 表达式属性 只读属性
  9. }
复制代码
可以发现当我们修改了对象s2中的address字段以后s1也跟着发生了变化,之所以这样和引用类型在内存中的存储方式是密不可分的:

在创建s2时并没有和创建s1一样通过new来创建一个全新的对象,而是通过=赋值来的,因为引用类型存储是二段存储,所以赋值以后s2在栈中存储的其实是s1对象在堆中的存储空间的地址,所以修改s2的时候s1也会随之变化,因为二者指向的是同一块内存空间。如果你通过new关键字来实例化s2,那s2就是存储的一个全新的Customer对象了。感兴趣可以看看不同方式创建的s2对象在内存中的地址一不一样。
  1. class Customer
  2. {
  3.     private string name;
  4.     private string address;
  5.     private int age;
  6.     private string createTime;
  7.    
  8.     public int Age => age;
  9.     public string Name { get => name; set => name = value; }
  10.     public string Address{ set => address = value; }
  11. }
复制代码
这里面的s1和s2就存储在两段不同的内存中。
继承

本篇文章的标题是“C# 面向对象”,但是,C#并不是一种纯粹的面向对象编程语言,C#中还包含一些非面向对象的特性,比如静态成员、静态方法和值类型等,还支持一些其他的编程范式,比如泛型编程、异步编程和函数式编程。虽然但是,面向对象仍然是C#中的一个重要概念,也是.NET提供的所有库的核心原则。
面向对象编程有四项基本原则:

  • 抽象:将实体的相关特性和交互建模为类,以定义系统的抽象表示。
  • 封装:隐藏对象的内部状态和功能,并仅允许通过一组公共函数进行访问。
  • 继承:根据现有抽象创建新抽象的能力。
  • 多形性:跨多个抽象以不同方式实现继承属性或方法的能力。【多态性】
在我们学习和使用类的过程中都或多或少在应用抽象、封装这些概念,或者说这些思想,我们之前都是在使用单个的某一个类,但在开发过程中,我们往往会遇到这样一种情况:很多我们声明的类中都有相似的数据,比如一个游戏,里面有Boss类、Enermy类,这些类有很多相同的属性,但是也有不同的,比方说Boss和Enermy都会飞龙在天,但是Boss还会乌鸦坐飞机这种高阶技能等等,这个时候我们可以如果按照我们之前的思路,分别编写了两个类,假如飞龙在天的技能被“聪明的”策划废弃了或者调整了参数,我们在维护起来是很不方便的,这个时候就可以使用继承来解决这个问题,它有父类和子类,相同的部分放在父类里就可以了。
继承的类型:

  • 由类实现继承:
    表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承非常有用。
  • 由接口实现继承:
    表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
细说的话,继承有单重继承和多重继承,单重继承就是一个类派生自一个基类(C#就是采用这种继承),多重继承就是一个类派生自多个类。
派生类也称为子类(subclass);父类、基类也称为超类(superclass)。
一些语言(例如C++)是支持所谓的“多重继承”的,但是关于多重继承是有争议的:一方面,多重继承可以编写更为复杂且较为紧凑的代码;另一方面,使用多重继承编写的代码一般很难理解和调试,也会产生一定的开销。C#的重要设计目标就是简化健壮代码,所以C#的设计人员决定不支持多重继承。一般情况下,不使用多重继承也是可以解决我们的问题的,所以很多编程语言,尤其是高级编程语言就不支持多重继承了。

虽然C#不支持多重继承,但是C#是允许一个类派生自多个接口的,这个后面章节再展开论述。

只需要知道,C#中的类可以通过继承另一个类来对自身进行拓展或定制,子类可以继承父类的所有函数成员和字段(继承父类的所有功能而无需重新构建),一个类只能有一个基类(父类),而且它只能继承自唯一一个父类❗但是,一个类可以被多个类继承,这会使得类之间产生一定的层次,也被称为多层继承(C#支持,并且很常用)。到这,你可能会想到,我们之前写的声明Customer类啊或者Vehicle啊它们有父类嘛❓答案当然是有的。就像在值类型、引用类型所说的,所有类型都有一个基类型就是Object类,当然了Object可没有基类,不能套娃嘛不是
来源:https://www.cnblogs.com/BoiledYakult/archive/2023/05/23/17422301.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

上一篇: C# 面向对象

下一篇: C# 面向对象

举报 回复 使用道具