工作中能用到的基础知识总结(一)
简介
该博文记录一些工作中接触较多的基础知识点的总结。
系列目录
知识点
一、值类型和引用类型
定义:值类型派生自System.ValueType,包括一般的非字符串基本类型(decemal,int,float)、结构体、枚举,可空类型(T?)。引用类型包括接口,类,数组,委托,string等需要人工new创建的类型。
内存分配上有什么区别?
值类型分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储。作为局部变量时(eg:方法内部局部变量),存储在栈上;而引用类型的的声明(指针)放在栈里面,而对应的实例对象(new 的对象)放在堆里面,引用类型在 C++中需要人工回收,而在C#中GC的3级回收机制可以自动回收托管代码。
错误回答:值类型放在内存栈里面,而引用类型放在堆里面。
分配内存为什么不同?
值类型类型简单,而引用类型相对复杂,引用类型会引用值类型或与其他引用类型有相互引用关系。简单来说,对象类型需要动态内存而基本类型则需要静态内存。若需要分配动态内存(GC操作动态内存),那么就分配到堆上;反之在栈上。对于值类型,它们的内存值都分配在栈上,当我们把一个值类型分配给另外一个值类型时,需要创建一个完全不同的拷贝;对于引用类型,当我们创建一个对象,并把一个对象赋给另外一个对象时,它们的指针指向相同的内存。改变其中一个,会影响到另外一个。
二、装箱/拆箱问题
装箱:用于在垃圾回收堆中存储值类型。装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。
拆箱:从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。
为何需要装箱?
一种最普通的场景是,调用一个含类型为Object的参数的方法,该Object可支持任意为型,以便通用。当你需要将一个值类型(如Int32)传入时,需要装箱。 另一种用法是,一个非泛型的容器,同样是为了保证通用,而将元素类型定义为Object。于是,要将值类型数据加入容器时,需要装箱。
重点:拆箱只是获取引用对象中指向值类型部分的指针,而内容拷贝则是赋值语句之触发。
注:使用装箱/拆箱会有风险,也会有一定的性能影响,一般可以用泛型替代。
三、泛型
为什么要用泛型?
通俗:为合并参数类型不同,逻辑相同的方法或类,提高代码复用率而产生的。
正规:可以将类型参数化以达到代码复用提高软件开发工作效率(增大代码复用性),并且提高性能和降低装箱、拆箱的成本或风险。
泛型有哪些优点?
使用泛型类型可以最大限度地重用代码、提高性能;可避免引入运行时强制转换或装箱操作的成本或风险;类型安全的(声明T是string类型,就不能是decimal类型)
应用:接口返回实体类用到(如:微信,QQ等三方接口);持久层实体映射(Orm框架中,基本上都是用实体做数据映射)等功能。
四、List是什么,有什么优点?
List类是ArrayList类的泛型等效类,它的大部分用法都与ArrayList相似,因为List类也继承了IList接口。在声明List集合时,我们同时需要为其声明List集合内数据的对象类型,弥补了ArrayList存在不安全类型与装箱拆箱的缺点。
优点:类型安全;性能增强;代码复用。
五、抽象类和接口关系
抽象类和接口有什么相似处?
- 抽象类和接口类都不能直接实例化;
- 都包含未实现的方法声明;
- 派生类必须实现未实现的方法,抽象类是抽象方法,接口则是所有成员(方法,属性,索引)。
抽象类和接口有什么区别?
-
抽象类更多的是定义在一系列紧密相关的类间,而接口大多数是关系疏松但都实现某一功能的类中;
-
接口基本上不具备继承的任何具体特点,它只是定义了能够调用的方法;
-
一个类可以继承多个接口,只能继承一个类;抽象类可以继承接口、抽象类;接口可以继承接口,不能继承抽象类;
-
接口可以用于支持回调,而继承并不具备这个特点;抽象类不能被密封;
-
抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的;
-
抽象把可变的与不可变的分离。接口就是定义为不可变的,而把可变的给子类去实现(抽象是相同中找不同,接口是不同种找相同);
-
好的接口定义应该是具有专一功能性的,而不是多功能的,否则造成接口污染(EG:IDispose,IApiController,IEnumerable)。如果一个类只是为了实现这个接口的中一个功能,而不得不去实现接口中的其他方法,就造成接口污染;
-
尽量避免使用继承来实现组建功能,而是使用黑箱复用,即对象组合。因为继承的层次增多,造成最直接的后果就是当你调用这个类群中某一类,就必须把他们全部加载到栈中!后果可想而知。使用某个类的对象来调用另外的类的方法和属性,可以增加代码的灵活性、复用性和扩展性;
抽象类和接口使用在什么环境使用?
-
如果预计要创建组件的多个版本,则创建抽象类;
-
如果创建的功能将在大范围的全异对象间使用,要设计小而简练的功能块,则使用接口;
-
如果要设计大的功能单元,要在组件的所有实现间提供通用的已实现功能,则使用抽象类;
-
抽象类主要用于关系密切的对象;而接口适合为不相关的类提供通用功能。
引用博客园中常见例子:
1.狗和猫,他们都会叫、跑、睡觉,就继承同一个行为接口“行为”;但是加菲猫和折耳猫属于“猫”抽象类,二哈和柯基属于“狗”抽象类。
2.公鸡和母鸡属于鸡(抽象类),不属于鸭(单继承),直接要一个鸡是要犯法的(不能直接实例化),如果需要具体的公鸡、母鸡是可以获得的。公鸡、母鸡都毛、两只脚(接口实现)。
六、什么是面向对象
单从字面上说,就是以对象为起点的编程思想。由于传统面向过程的编程方式,在各个功能模块的耦合性比较强,代码的重复率太高,造成软件后期迭代扩展功能很不方便,缺乏灵活性和代码重用性。以至于越往后期,代码的可维护性越差。为了提高代码的可维护性,重用性,灵活性和扩展性,面向对象诞生了。
面向对象三大特征,封装,继承,多态。用我做过的一个三方登录功能来说明:功能要求用户可用通过PC客户端,PC网页端和APP端选者微信,QQ或直接输入账号密码登录。其中有两个难点,第一个就是涉及到3种客户端,第二个就是三方登录平台多个。这种情况下就得用到面向对象的三大特性。首先:PC客户端,PC网页端和APP端是3个不同的登录逻辑,用封装特性可以将3种客户端的功能分别封装起来,降低耦合性,便于后期各个内部功能拓展。但是考虑到3种客户端登录功能又有许多可以公用的方法,可以用继承特性,将公用的方法给单独封装起来,作为一个父类(修饰符一般用protected)大家都集成它。每种客户端类型登录中又有多重三方登录,这里也可以用到封装和继承。在登录功能中,会有和其他服务交互的功能,和每个服务交互的功能又有不同的区别。这里就可以用到多态,这里可以用一个简单工厂来实现多态,根据不同请求,与不同服务器做交互。
七、委托和事件
什么是委托,什么是多播委托,有那些优点,什么地方用到?
- 委托是一种引用类型,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递。是一种安全地封装方法的类型,它与 C 和C++ 中的函数指针类似。
- 多播委托是一个由委托串成的链表,当链表上的一个委托被回调时,所有链表上该委托的后续委托将会被顺序执行。要注意的是,多播委托必须是同类型的,返回类型必须是void,并且不能带输出参数(但可以带引用参数)。
- 委托可以引用实例和静态方法,而函数指针只能引用静态方法。在定义委托的时候,需要定义返回类型,参数类型和个数,所以委托相对于指针是类型安全和保险的。
- 委托可以用在数据持久化处理中(数据实体映射);在某些情况下,可以替代工厂模式。
-
public delegate void OperateDelegate(string name); public void OperatePeople(string name, OperateDelegate operateDelegate) { operateDelegate(name); }
什么是事件机制,有那些优点?
- 事件是某对象在发生其关注的事情时用来提供通知的一种方式,通俗的讲就是:监控+通知,有点像多播委托,但是没有同步,异步功能。
- 事件是单独开启线程监控程序,不需要主程序等待事件发生,减少了资源消耗;使用事件,可以很方便地确定程序执行顺序;
- 符合面向对象封装思想。
public class EventManager { //这一次我们在这里声明一个事件 public event EventDelegate MakeGreet; public void EventOpt(string talkStr, ref string retStr) { MakeGreet(talkStr,ref retStr); } }
八、变体、协变性、逆变性、不变性
定义
变体:带有协变或逆变参数的泛型接口或委托(可隐式转换);
协变性,子到父的转换,让一个带有协变参数的泛型接口或泛型委托可以接收类型更具体的泛型接口或泛型委托作为参数,可以看成面向对象中多态的一个延伸;
逆变性,父到子的转换;让一个带有协变参数的泛型接口或泛型委托可以接收类型更泛化的泛型接口或泛型委托作为参数,实际上是参数类型更加精细化的过程。
不变性,只能是引用类型的泛型接口或泛型委托才能做协变/逆变操作。如果不用关键字(out或in),强制转换都不行。
总结:协变类型参数可用作返回类型,而逆变类型参数可用作参数类型。
为什么要用可变性?
可变性可以安全的将一种类型转换成另一种类型。
九、Lock的作用
lock的作用,怎么使用?
lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
1、lock的是引用类型的对象,string类型除外。
2、lock推荐的做法是使用静态的、只读的、私有的对象(静态能保证每次进入的只有一个线程,共有的有被其他程序干扰的风险,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义)
3、适用于单台服务器环境。
十:单例模式
上文说了Lock,顺便说一下单例模式。
为什么需要单例模式?多线程下怎么确保目标类实例唯一?
为了保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。在多线程下,如果只是常规写法(两个私有+一个只判空的共有)是无法避免某时刻目标类只有
一个实例存在(当多个线程同时通过判空判断),在判空判断中加Lock可以解决这个问题。
-
if (instance == null) { lock (locker) { //如果类的实例不存在则创建,否则直接返回 if(instance == null) { instance = new singleClass(); } } } }
注:单例如果使用了,就会在程序的整个生命周期中存在,如果过多使用会造成空间资源浪费。
单例是为了确保某时刻只有一个实例存在,不要在某些允许同时有多个实例的地方使用,比如多数据库操作。