C#常识篇(一)

面向对象的三大特性

继承:子类通过继承父类来获取基础特性,并且能够基于父类进行特异化扩展。继承具有传递性,例如B继承自A,C继承自B,那么C就间接继承自A。在C#中,每个类仅允许继承一个父类。继承不仅能够提升代码的复用性也可以让程序的结构更加明晰。

例如我们有时候会发现两个及以上的功能会存在相同的或类似的逻辑和组成,可以尝试将它们共有的部分进行抽离,然后将这些共有部分设计在一个父类中,这样就可以通过继承来避免重复的设计和代码,父类可以衍生出很多子类,所谓"龙生龙,凤生凤,老鼠的儿子会打洞",作为子类会具备父类的基础特性,当然父类也有自己的隐私,所以并非会对子类开放所有成员的访问权限。父类也会对衍生的子类有一定约束,可能会强行要求子类实现某些成员,例如被继承而来的抽象成员,因为有时候父类可能不包括具体实现,仅仅作为一个模板或者协议存在,而在这种情况下,具体的实现则是交给各个子类,有时候开发者为了给模块提供可扩展性,会特意进行这种设计,就是为了满足后续替换或衍生的需求,这种时候我们通常可以继承父类并开发一个新的子类,避免对原有代码进行修改,也符合了“开闭原则”。

 

封装:将数据和行为封装为一个独立的单元,对外部隐藏具体的实现细节,仅提供必要的调用接口,从而简化外部调用和提升代码的复用性和可维护性。

 

例如一个系统可以看作由多个功能模块组成,每个模块都可以被视为一个被封装的独立单元,各司其职,又作为系统不可或缺的部分,结合注释或文档可以使得每个模块便于开发者维护,同时独立的模块又可以独自完成某些工作,所以是可以被复用的,除此之外,不同模块之间几乎无须关注其它模块的内部逻辑和组成,只需要关注其功能,直接通过其开放的接口进行调用即可。所以对于维护者和调用者可以采用两份文档,针对每个模块,内部逻辑和组成相关注释和描述可以记录在维护者文档中,对于开放的接口则记录在调用文档中,这个调用文档往往就是我们所说的API文档。

 

多态:同一对象针对不同的的对象或应用情景而存在不同的行为,例如运行时的多态——重写,编译时的多态——重载,重写则是根据不同的实例对象而表现不同的行为,重载则是根据传递的参数而自动选择对应的行为。

 

例如一个银行柜台职员,会根据客户的需求办理不同的业务,这是类似重载的体现,而重写则依托于继承关系,子类可以重写父类的方法,这样即便是同签名的方法也会因为实例对象的不同而表现不同行为,就好比不同的人在街上看到一条流浪猫,有的人会投喂一些食物,有的人则冷漠视之,同样为人却也会因为三观不同而表现出不同的反应。

值类型和引用类型 

在C#中值类型继承自System.ValueType类,包括整型、浮点型、布尔类型、字符型四种简单值类型,以及结构体和枚举两种复合值类型,这些简单值类型本质上就是结构体。引用类型继承自System.Object类,包括class、delegate、array、object等。

GC 

GC是一种垃圾回收机制,为了高效、便捷和安全地管理内存,C#中采用了自动垃圾回收机制,由系统代理内存管理,从而提高开发效率和避免内存泄漏等问题。C#的GC采用的是"标记-清理"算法和分代管理方式。

 

(1)"标记-清理"算法是从根对象开始,根据对象的引用关系递归标记可达对象,在清理阶段则对不可达对象进行内存清理,某些GC还会在清理阶段进行内存压缩从而减少内存碎片化。

(2)分代管理是在托管堆上根据所创建对象的生命周期进行分类,刚创建的对象被称为"第一代",在上一次"标记-清理"阶段存活下来的对象会被分类为"第二代",以此类推,分代管理的目的是为了通过控制对象进行"标记-清理"的频率从而提高GC的效率。

反射 

反射是一种在运行时动态访问程序集的方式。在C#中通过System.Reflection命名空间中的API在运行时获取程序集的元数据,通常我们可以通过获取指定类型的Type对象再通过反射获取其字段、属性和方法等成员,实现在运行时间接操作对象,不过反射通常会带来一定的性能开销,所以不建议大量使用。

StringBuilder和String 

StringBuilder顾名思义就是字符串构建器,对StringBuilder对象的操作是基于可变内存缓冲区的,这个特性减少了创建新对象的需求,相较于String类型,它更适合频繁操作字符串的应用场景,但是它不是线程安全的。String类型是不可变的,对其进行的字符串操作通常涉及到需要创建新的String类型对象。总而言之,对于需要频繁操作字符串的情况建议使用StringBuilder类型,反之使用String类型。

常见容器类 

(1)数组(Array)在定义或初始化时需要明确指定数组长度的容器类,严格意义上来说数组可以进行数据修改,但是无法进行增加和删除,因为数组的长度是固定的,要对数组进行增加和删除操作,通常需要创建一个新的数组对象。数组的内存是连续的,所以其查询和修改的效率是可观的,其通常适合存储一些数量固定的数据。

(2)列表(List<T>)列表可以看作是一个动态的泛型数组,能够根据需要动态调整容量大小,避免了ArrayList中的装箱和拆箱操作,由于其底层实现依旧是数组,所以增加和删除操作是耗时操作。

(3)(Stack<T>)栈是一种后进先出的数据结构,涉及到元素的入栈和出栈操作,后入栈的元素则先出栈,栈的操作仅支持栈顶操作,通常其不适合具有广泛的查询元素需求的应用场景。

(4)队列(Queue<T>)队列是一种先进先出的数据结构,涉及到元素的入队和出队操作,先入队的元素则先出队,队列的操作支持队首和队尾操作,通常其不适合具有广泛的查询元素需求的应用场景。

(5)字典(Dictionary<K,V>)字典是一种基于哈希表的数据结构,以键值对的方式保存元素,键唯一而值不唯一,同一个字典中不允许重复的键,但允许重复的值,因其基于哈希表的特性,所以对于元素的查询、插入和删除操作效率是可观的。

(6)集合(HashSet<T>)集合是一种基于哈希表的数据结构,它不允许存储重复的元素,所以非常适合需要去重的应用场景,因其基于哈希表的特性,所以对于元素的查询、插入和删除操作效率是可观的。

(7)哈希表(Hashtable)哈希表的底层实现是数组,数组元素由一种名为"桶"的数据结构组成,哈希表通过哈希函数将键映射到数组索引,严格来说一个键应该对应一个数组索引,当存在不同的键映射到同一个数组索引时则为哈希冲突,哈希表的设计关键则在于如何制定哈希函数和处理哈希冲突,处理哈希冲突常见的方法有链地址法和开放地址法,链地址法则是在数组索引处维护一个链表用于存储映射到该索引的不同键,而开放地址法则是通过一定的规则在哈希表中寻找到下一个可用的索引位置,然后插入元素。官方建议在新项目的开发中使用字典替代哈希表。

隐式继承和显式继承 

在C#中通过英文符号":"来实现显式继承,显式继承则需要显式指定基类,而隐式继承通常由编译器隐藏基类,例如声明自定义类时并不需要显式继承System.Object,而由编译器自动完成这个继承行为,所以声明的自定义类即使没有显式继承自System.Object,但实际上却是System.Object的派生类,在编译时就会有所体现,可以通过自定义类的实例查阅到可调用的方法中包括System.Object的基本方法。

栈内存和堆内存 

栈内存是基于栈这种数据结构构建的内存空间,通常用于存储代码上下文,例如某个被调用的函数的局部变量、参数和返回地址等信息,栈内存由编译器或解释器自动管理内存的分配和释放。

 

堆内存是基于堆这种数据结构构建的内存空间,通常用于存储动态分配的数据,例如实例对象和数组等,堆内存通常由程序员手动管理或者GC自动管理。

 

栈内存相比堆内存更轻量和便捷,访问更加快速,内存管理的性能开销更小;堆内存相比栈内存拥有更大的存储空间,其中的数据没有固定的生命周期,可以跨函数或代码块使用。值得注意的是堆内存可能涉及到内存碎片化的问题,并且涉及为对象分配合适的内存块而需要更大的性能开销。

析构函数 

析构函数使用语法“~类名()”定义,无需访问修饰符、返回类型和参数。它实际上被编译器转换为重写Finalize()方法。

析构函数在垃圾回收器销毁对象时自动调用,但调用时机是不确定的,可能在对象不再被引用后的任意时间点执行。

官方强烈不建议在析构函数中释放托管资源,因为:

  1. 托管资源由垃圾回收器自动管理

  2. 析构函数执行顺序不确定,可能依赖的对象已被销毁

  3. 有析构函数的对象需要多代GC处理,影响性能

现代C#推荐使用IDisposable模式来管理资源,析构函数仅作为释放非托管资源的最后保障。

抽象类和接口的区别 

抽象类本质上是一种特殊的类,具备构造函数,能够声明和定义实例化的字段、属性以及函数,同时也能够声明抽象的函数,抽象类能够继承其它类和实现接口。与普通的类不同的是抽象类无法直接通过new创建实例,而需要通过其派生类间接创建实例,并且抽象类的抽象函数必须被派生类实现。

 

接口仅允许声明而不需要实现字段和函数,且均是公开的,接口可以嵌套实现其它接口,其相比抽象类是更高层次的抽象,所以同样无法创建实例。

 

如果这篇文章对你有帮助,请给作者点个赞吧! 

posted @ 2024-02-24 21:20  我与岁月的森林  阅读(29)  评论(0)    收藏  举报  来源