C#常识篇(一)
面向对象的三大特性
继承:子类通过继承父类来获取基础特性,并且能够基于父类进行扩展以及提升代码的复用性。继承具有传递性,例如A继承自B,C继承自A,那么C就间接继承自B。在C#中,每个类仅允许继承一个父类。继承能够让程序的结构更加明晰。
封装:将数据和行为封装为一个独立的单元,对外部隐藏具体的实现细节,仅提供必要的调用接口,从而简化外部调用和提升代码的复用性和可维护性。
多态:同一操作针对不同的的对象或应用情景而存在不同的行为,例如运行时的多态——重写,编译时的多态——重载,重写则是根据不同的实例对象而表现不同的行为,重载则是根据传递的参数而自动选择对应的行为。
值类型和引用类型
在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自动管理。
栈内存相比堆内存更轻量和便捷,访问更加快速,内存管理的性能开销更小;堆内存相比栈内存拥有更大的存储空间,其中的数据没有固定的生命周期,可以跨函数或代码块使用。值得注意的是堆内存可能涉及到内存碎片化的问题,并且涉及为对象分配合适的内存块而需要更大的性能开销。
析构函数
析构函数的形式是"~类名",且该函数无需添加任何修饰符,没有返回值也没有参数列表。析构函数是在对象销毁时自动调用的函数,尽管如此官方并不建议在该函数中去释放托管资源,因为GC执行析构函数的顺序是不确定的,自主释放托管资源可能导致未知错误。
抽象类和接口的区别
抽象类本质上是一种特殊的类,具备构造函数,能够声明和定义实例化的字段、属性以及函数,同时也能够声明抽象的函数,抽象类能够继承其它类和实现接口。与普通的类不同的是抽象类无法直接通过new创建实例,而需要通过其派生类间接创建实例,并且抽象类的抽象函数必须被派生类实现。
接口仅允许声明而不需要实现字段和函数,且均是公开的,接口可以嵌套实现其它接口,其相比抽象类是更高层次的抽象,所以同样无法创建实例。
如果这篇文章对你有帮助,请给作者点个赞吧!
本站大部分文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了您的权益请来信告知我们删除。邮箱:1451803763@qq.com