CLR-基元类型以及溢出检查
=========(CLR via C#阅读笔记)========
基元类型(primitive type):
基元类型也不做过多的解释,举个例子即可清晰的辨别
在java里曾使用过Sting s="java"; 定义字符串,然后就会觉得很诧异,为啥是大写开头,我写C#,一直都是 string ,int ,double,float等等小写开头,这时候,来了解基元类型方可解惑。
1 int a=0; 2 System.Int32 a=0; 3 int a=new int(); 4 System.Int32 a=new System.Int32();
以上定义int类型的语法,都是能正确编译与运行的,其中第一种最方便简洁,那么这种方便简洁的int,string,byte..decimal,object等等都称作C#的基元类型,对应FCL类型则为System.Int32(其它依次类推,注意是大写开头)。
以上,基元类型和FCL类型效果完全一致。
基元类型转换可能造成精度或数量级的丢失:
然后下面谈到,基元类型中的一些转换问题:
1 int i=5; //System.Int32 2 long j=i; //System.Int64
上述代码能够正确的完成隐式转换,也支持一些字面值的转换。
但是将代码调换一下:
1 1 longi=5; //System.Int64 2 2 int j=i; //System.Int32
这个时候是不安全的转换,因为long 可以容纳更长的数字,如果long对象的值超过了int,那么会发生丢失精度或数量级,这个时候必须使用显示转换。
但是即使做了显示转换,也无法避免误差的产生:
对数据转换造成误差做了一个小小的测试:
得出结论,在超过int(-2,147,483,648 到 2,147,483,647)范围后,int会从头在依次计算,比如比范围多1,就会从最大值回到开头,多2,就会往后再数一个
而浮点型,不论是float还是double都是直接截取整数位不会进行四舍五入(有些语言不是截取)。
checkd和uncheckd基元类操作
作用:
一些元算可能会有溢出发生,例如
1 byte b=100; //无符号八位值,范围0-255 2 b=(byte)(b+200); //溢出得到值 (100+200)-255-1=44
如果,你觉得这种溢出是非法的不可接受的,那么可以加上checked:
几种常见用法:
checked语句对指定的代码块进行检查:
1 //unchecked写法亦然,但是表示不对溢出进行检查 2 checked 3 { 4 byte b=100; //无符号八位值,范围0-255 5 b=(byte)(b+200); //溢出得到值 (100+200)-255-1=44 6 }
需要注意的是,checked操作符仅仅对块里面进行的运算表达式生效,如果你放一个方法进去,是不会有任何效果的。
使用checked操作符:
1 b=checked((byte)(b+200)); 2 b=(byte)checked(b+200); //两种写法都可以
这时候,再发生溢出的时候会抛出System.OverflowException: 算术运算导致溢出。
当然在极少数时候,异常是允许的,甚至是希望的,比如:计算哈希值或者校验和。
checked和unchecked到底是啥?
引申:CLR提供了一些特殊的IL指令,允许编译器选择它认为最恰当的行为。CLR有一个add指令,作用是将两个值相加,但不执行溢出的检查。还有一个add.ovf指令,作用也是将两个值相加,但会发生溢出时抛出System.OverflowException,当然减乘除也有对应方法,分别是sub/sub.ovf,mul/mul.ovf和conv/conv.ovf。而上面说到的checked和unchecked则是C#层面上提供的编译器开关,自然用来指定编译器下对应的指令(上面提到的IL指令)。
需要注意的是,使用了/checked+(/check-)编译器开关,会使代码执行变得稍慢,因为CLR会进行对应的语句检查。
如何有效的避免对基元类型的不良操作和编码:
- 尽量使用有符合数值类型,这允许编译器检测更多的上溢/下溢错误(有符号意味着区分正负,无符号是没有负数的),也会减少可能的强制类型转换,另外无符号数值不符合CLS(Common Language Standard)。
- 如果代码可能发生你不希望的溢出(比如错误的输入造成的),就把这些代码放到checked块中,并捕捉异常。
- 将允许溢出的代码块放到unchecked块中
- 没有上述检查操作符的,意味着溢出是bug
- 开发环境最好,打开编译器的/checked+开关(生成-高级),尽可能暴露问题,发布时应使用/checked-开关,确保代码更快的运行(当然如果能够对检查要求严格,又能接受带来的性能损失也可以打开开发来编译,可以防止应用程序在包含已损坏的数据的前提下运行)
特别的地方:
- System.Decimal虽然在C#中是基元类型,但是在CLR中并不是,意味着没有对应的IL指令,它使用int,float,double,uint等数组来表示范围内的大小,如图:
- 可以分析出:1.操作符“+”,“-”...对于decimal其实是无效的,但是我们不仅可以对他进行操作符的基本运算,也可以用提供的Add...等方法进行运算,因为它内部其实是对操作符进行了重载,且在运算不安全时是会自动抛出OverFlowException的异常的。2.checked 和 unchecked无效,它无需cheked操作符来检查抛出异常,也无法unchecked阻止异常抛出。
- System.Numerics.BigInteger类型类似Decimal,也使用数组来表示任意大的数,但是它如何计算都不会溢出,只有在内存不足以改变数组大小时才会抛出异常OutMemoryException。
上述有更为详细的demo,效果如下:
Github地址:
https://github.com/JOJOJOFran/CLR-Via-C--TEST/tree/master/Design_Type/Primitive%20Type
有啥问题欢迎指正!
(CLR-Via-C#) 类型基础
CLR要求每个类型最终都派生自System.Object
Object提供的公共方法:
- Equals: 如果两个对象具有相同的值,就返回true
- GetHashCode: 返回对象的哈希码
- ToString:默认返回类型的完整名称(this.GetType().FullName)
- GetType: 返回从type派生的一个实例
object的protected方法:
- MemberwiseClone:这个非虚方法创建类型的新实例,并将新对象的实例字段设与this的字段完全一致
- Finalize:在垃圾回收器判断对象应该作为垃圾回收之后。在对象的内存被实际回收之前,会调用这个虚方法。需要在回收内存钱执行清理工作的类型,应该重写该方法。
所有对象都需要使用new操作符,new的实际操作如下:
- 计算类型及其所有基类型定义的所有实例字段所需要的字节数。堆上哦度需要一些额外的开销成员(overhead成员)包括类型对象指针(type object pointer)和同步索引块(sync block index).CLR利用这些成员管理对象。额外的成员也要计入对象大小。
- 从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0
- 初始化对象的“类型对象指针”和“同步索引块“成员
- 调用类型的实例构造器(构造函数),传递在new中指定的实参
类型转换
CLR重要的就是类型安全,所以不是所有的类型都能进行互相转换,首先说派生类和基类的转换。
提到继承的转换,第一个应该想到的就是里氏转换原则:
子类一定可以转换为父类,而父类不一定能转换成子类
internal class Employee
{
……
}
internal class Manager:Employee
{
……
}
public class XXXX()
{
public void main()
{
Manager m =new Manager();
Test(m);
DataTime d=new DataTime ();
Test(d);
}
public void Test(Object o)
{
Employee e =(Employee) o;
}
}
以上,值得注意的是编译器都不会报错,但是实际代码段Test(d)运行时会报InvalidCastException。因为d是一个DateTime类型,自然可以隐式转换为Object,但是它不能转换为Employee,以后看见这个异常,应该觉得很眼熟才对。
然后涉及到is和as运算符的问题:
首先了解一下两个运算符:
is: 判断是否是某一个类型,返回true和false并用永不会抛出异常,但是它并不会将类型进行转换。
as: 尝试将类型进行相应的转换,如果失败返回null,也不会抛出任何异常。
使用方法,不做赘述,那么在进行类型转换操作时:
is: 只能先判断是否能够转换,再进行转换,相当于要运算两次
as:直接进行转换,转换失败则返回类,所以为了避免返回类型报NullReferenceException,对结果做一次非空判断即可,只用运算一次,自然效率高一些。
委托
什么是委托
深入理解C#有这么一个例子:一份遗嘱可以是还钱,可以是捐款,也可以是留给某某,但是这么一份遗嘱放在那,你只知道是遗嘱,并不知道其中的内容,你可以委托给一个律师去执行这份遗嘱,但是律师并不知道这份遗嘱要执行的内容,这个例子很好的比喻了什么是委托。</br>
正如上面的例子所说,委托像这份遗嘱一样,需要在特定的时间点去执行一系列操作,而你可能并不知道操作的细节,你也可以参照C里面的函数指针去理解。</br>
简单的委托构成
想要让委托做事,必须满足四个条件:
-
声明委托类型
-
必须有一个方法包含了要执行的代码
-
必须创建一个委托实例
-
必须调用(invoke)委托实例
声明委托类型
1.什么是委托类型?
一个用来约束你要定义的委托方法的返回值和参数个数以及类型的类型。
2.如何声明委托?
<关键字> <返回类型> <委托类型的名称> <参数>
delegate void StringProcessor(string input)
3.声明的意义?
根据上面的示例,分析一下,其实它是创建了一个对象,派生自System.MulticastDelegate(又派生自System.Delegate),它约束了你将要执行操作的方法的返回类型,和参数个数,以及参数类型,必须严格对应。根据遗嘱的例子你可以这么假设,它定义了,遗产分配的东西,给予的对象,它更像一个规则或者大纲,具体的执行方法,还要看下面所要定义的方法,例子也许不是十分恰当,但是帮助你更容易理解。
4.需要注意的东西
什么是委托类型,上面我们定义的就是委托类型即Delegate。
构造委托要执行操作的方法
1.为什么要构造方法?
假设遗嘱指定了,给500w给死者的儿子。但是现在儿子又太小,那么你需要设立一系列的条件和操作去帮助死者的儿子拿到这份遗产,这一系列有条件的操作,就是你构造函数的意义所在。
即为委托类型找一个方法,并且这个方法能执行我们想要的操作。
2.构造方法的约束或者规则?
委托类型已经定义了返回值,参数类型及个数,所以构造方法需要严格遵守,如:
void test(string x)
以上方法就是符合前文中定义的委托类型。
3.需要注意的东西
创建委托实例
1.如何创建?
创建其实就像把你定义的委托方法,当作构造参数传递给委托类型,从而构造一个委托实例,下面的例子将展示,静态方法和非静态方法的创建方式:
//假设test方法属于TestClass中的方法
StringProcessor proc1,proc2;
//如果不是静态方法
TestClass tClass =new TestClass();
proc1=new StringProcessor(tClass.test);
//如果是静态方法
proc1=new StringProcessor(TestClass.test);
2.需要注意的东西?
必须注意,假如委托实例本身不能被Gc回收。委托实例会阻止它的目标被作为垃圾回收。这可能造成内存泄漏(leak),特别是一个“生命周期”短的对象调用一个“生命周期”长的对象中的事件,并用它作为自身目标,这间接延长了“生命周期”短的对象的生命周期。
调用委托实例
1.如何调用?
这里只需要调用委托实例中的Invoke方法即可,异步委托则调用BeginInvoke和EndInvoke(后面异步委托再来讲)
接着上面的示例 我们可以执行以下方法
proc1.Invoke("我是委托");
2.调用的约束或者规则
其实挺简单,没啥特别好说的,Invoke的参数其实还是传给了test()方法。
3.注意事项
Invoke这个方法来源于哪里?
合并和删除委托
对事件的简单讨论
委托总结