一、类型
C#是一种类型安全的静态语言。这要求创建任何变量时,都必须将其数据类型告知编译器;编译器将确保只能将兼容的数据类型存储到变量中。这有助于避免常见的编程错误,让应用程序更稳定、更安全。
类型分为三大类:
- 值类型
- 引用类型
- 类型参数
ps:实际上还有第4种类型—指针,但核心C#语言不支持。指针包含数据在内存中的实际位置(地址);还可对指针执行算术运算,就像它们是数字一样。虽然指针功能强大,但要正确、安全地使用它们也很难。
为提供指针的灵活性(这也会带来危险),C#允许您编写不安全的代码,在这些代码中可创建和操作指针。使用不安全的代码和指针时,务必认识到垃圾收集器不会跟踪指针,您必须负责分配和释放内存。从某种意义上说,这类似于在C#程序中编写C语言代码。
除显式的不安全代码块外,C#不允许在其他地方使用指针,这可以完全避免一类常见的错误,让C#更安全得多。
简单地说,值类型是完全独立的,它“按值”复制。这意味着值类型变量包含其数据,不会因为处理一个变量而影响另一个变量。值类型又分为结构、枚举类型和可以为null的类型。
引用类型包含指向实际数据的引用,这意味着两个变量可能指向同一个对象,而操作其中一个变量将影响另一个变量。引用类型又分为类、数组、接口和委托。
ps:同一类型系统
虽然对类型进行了上述分类,但 C#有一个统一类型系统,使得可将任何非指针类型值视为对象。这让值类型获得了引用类型的优点,而不会增加不必要的开销;并可对任何值(包括预定义值类型)调用对象方法。
二、预定义类型
2.1 预定义类型
C#预定义了一组类型,这些类型对应于通用类型系统中的类型。
通过包括表示布尔值的类型(取值为true或false),可避免混淆布尔变量和整数变量。这有助于消除多种常见的编程错误,让编写含义不言自明的代码更容易。
ps:在C语言中,布尔值表示为整数,并让程序员去决定0表示true还是false。通常,C语言程序定义表示整数值0和1的名称常量,以帮助消除这种模糊性,但并没有禁止使用整数表示布尔值。
- 类型decimal最少包含28个有效位,旨在避免金融计算的误差。double类型主要用于物理计算,以减少表示误差。
- object是其他所有引用类型和值类型的基类;string用于表示一系列Unicode字符,这种变量赋值后就不能修改,因此string变量是不可修改的。
- 除无符号整数类型和sbyte外,其他所有预定义类型都符合CLS,但是使用这些
- 类型时,只要不将其声明为公有的,您的代码就符合CLS。如果这些类型的变量必须是公有的,那么可转而使用相应的符合CLS的类型。
- 对于sbyte,可使用符合CLS的类型short代替。
- 对于uint,通常使用符合CLS的类型long代替;如果存储的值小于2147483647.5,那么也可使用int代替。
- 对于 ulong ,通常可使用符合 CLS 的类型 decimal 代替,如果存储的值小于9223372036854775807.5,那么也可使用long代替
- 对于ushort,通常可使用符合CLS的类型int代替,如果存储的值小于32767.5,那么也可使用short代替。
- System.Object:所有的值类型以及类、数组和委托等引用类型都是从 object 派生而来的。接口类型可能是从其他接口类型派生而来的,但可转换为object。
- 类型参数实际上不是从任何类型派生而来的,但可转换为object。
- 不安全的指针类型既不是从object派生而来的,也不能转换为object,因为它们不受常规C#类型规则的管辖。
- 这一切意味着C#中的所有非指针类型都可转换为object,但可能并不是从object派生而来的。
- C#还有一些特殊的类型,其中最常见的是void。void表示不知道类型。dynamic类型类似于 object,主要的不同之处在于,对这种类型执行的所有操作都将在运行阶段(而不是编译阶段)解析。
- 虽然void和dynamic都是类型,但var是隐式类型,让编译器根据赋给变量的数据确定变量的类型。
ps:var并非Variant的缩写
最初引入 var类型时,很多人认为它相当于Visual Basic中的Variant类型。Variant 变量可用于存储其他任何数据类型的值,因此不属于强类型;而var类型仍是强类型,因为在编译阶段将用特定数据类型替换它。尽管如此,过度使用var可能降低代码的可读性,因此应慎用。
2.2 其他的常用类型
除标准的预定义类型外,.NET Framework还提供了用于表示其他常用值的类型。不同于预定义类型,这些类型没有C#别名,但对其可执行的操作不受影响。
2.2.1 日期和时间
要处理日期和时间值,可使用结构DateTime,它能够创建表示日期和时间、仅日期或仅时间的变量。新建DateTime变量时,最常见的方法有两种:使用各种重载的构造函数之一;使用4个静态的分析方法之一—Parse、ParseExact、TryParse或TryParseExact。
DateTime的常用属性
对日期或时间值进行加减运算时,可使用相应的实例方法,它们返回一个新的DateTime值,而不是修改原来的值。
DateTime常用的算术运算方法:
还可使用减法运算符将两个DateTime值相减,结果为一个TimeSpan实例。TimeSpan实例表示时间间隔,以天数、小时数、分钟数、秒数和毫秒数表示,可正可负。为确保一致性,时间间隔以天数为单位。还可将DateTime与TimeSpan相加或相减,结果为一个新的DateTime实例。
TimeSpan常用的方法和属性
2.2.2 全局唯一标识符(GUID)
GUID是一个128位的整数值,重复的可能性很小,每当需要唯一标识符时就可以使用它。结构System.Guid、能够创建和比较GUID值
常用成员:
2.2.3 统一资源标识符(URI)
URI是内联网或Internet上可用资源的简洁表示,可以是绝对URI(如网页地址),也可以是相对URI,后者必须根据基本URI进行扩展。
Uri类让您能够新建URI以及访问URI的成员,它还提供了处理URI所需的方法,如分析、比较和合并。
常用成员:
Uri实例是不能修改的。要创建可修改的URI,可使用UriBuilder。UriBuilder类让您能够轻松地修改URI的属性,而无需每次修改时都新建实例。
Uri和UriBuilder都有的属性(在Uri中,这些属性是只读的):
2.2.4 BigInteger
类型 System.Numerics.BigInteger 表示任意大的整数值,从理论上说,没有上限和下限。创建BigInteger实例后,可以像使用其他整数类型一样使用,可对其进行基本的数学运算和比较。
结构BigInteger包含其他整数类型的方法和Math类的方法,还有专门针对BigInteger的成员。
这个结构的常用成员:
2.3 运算符
C#支持大量的运算符,但是这里只介绍比较常用的运算符。运算符是一种特殊符号,用于在表达式中指出要执行哪种运算。所有C#预定义类型都支持运算符,但是并非所有类型支持的运算符都相同。
按优先级顺序列出了所有的C#运算符。在每个类别中,运算符的优先级顺序相同。
2.3.1 算术运算符和赋值运算符
C#提供了支持标准数学运算的运算符:加法(+)、减法(−)、乘法(*)和除法(/)。根据相除的数据类型的不同,除法的行为也稍有不同:将两个整数相除时,结果为整数,并将余数丢弃。要获得整数除法的余数,必须使用求模运算符%。
C#还支持复合赋值运算符,这种运算符将算术运算和赋值合而为一。对于每个标准算术运算符和求模运算符,都有相应的复合赋值运算符,分别是+ =、− =、* =、/ =和% =,这些运算符将赋值分别与加法、减法、乘法、除法和求模合而为一。
2.3.2 关系运算符
关系运算符用于比较两个值,结果为布尔值。
2.3.3 逻辑运算符
逻辑运算符用于布尔表达式中,结果为true或false。
逻辑运算符的行为规则很容易总结。假设x和y都是布尔表达式,逻辑运算的结果如下:
2.3.4 条件运算符
条件运算符(也称为三目运算符,因为它涉及3项)对编写简洁表达式很有帮助,它对条件进行评估,并根据结果返回两个值之一。
条件运算符的格式如下:
condition ? consequence : alternative
当condition为true时,将计算consequence并返回结果;如果condition为false,则计算alternative并返回结果。
ps:三目运算符常见的问题
这个运算符是右结合的,这与其他大部分运算符(左结合的)不同。这意味着对于下面的表达式相当于 a ? b : ( c ? d : e ):
a ? b : c ? d : e
条件表达式的类型是由consequence和alternative的类型决定的,而不是它被赋给的变量决定的。
因此,consequence 和 alternative 的类型必须相同,这意味着下面这样的表达式无法通过编译,因为其consequence的类型为int,而alternative的类型为string:
object x = b ? 0 : "hello";
虽然上述代码不实用,根本不应在其他地方使用,但是其正确的书写方式可能如下:
object x = b ? (object)0 : (object)"hello";
2.4 默认值
各种预定义数据类型的默认值:
对于整型类型,默认值为零。char类型的默认值为空字符,而bool类型的默认值为false。类型object和string的默认值为null,即没有指向任何对象。
2.5 null和可以为null的类型
这些默认值意味着值类型不能为 null,看起来好像是合理的。然而,当使用数据库、其他外部数据源或其他可能没有指定值的数据类型时,会带来一定的限制。一个典型的示例就是数据库中的数值字段,它可以存储任何整型数据,也可能没有值。
对于这种问题,可以为null的数据类型提供解决方案。可以为null的类型是这样一种值类型:可表示其底层类型指定范围内的值以及null值。可以为null的类型用语法Nullable<T>或T?表示,其中T是一种值类型。语法T?使用更广泛。给可以为null的类型的变量赋值时,方法与给其他变量赋值相同:
int = 10;
int? = 10;
int? = null;
要获取可以为 null 的类型的变量的值,应使用方法 GetValueOrDefault,它返回赋给变量的值,如果没有赋值,就返回底层类型的默认值。另外,还可以使用属性 HasValue (如果给变量赋值了,该属性将为true)和Value(它返回变量的实际值,如果值为null,就将引发异常)。
所有可以为 null 的类型(包括引用类型)都支持 null 合并运算符(??)。将可以为 null的类型的变量赋给不能为null的类型的变量时,可使用该运算符指定要返回的默认值。如果该运算符左边的操作数为 null,就返回右边的操作数;否则,返回左边的操作数。程序清单2.4演示了如何使用null合并运算符。
2.6 强制转换与转换
作为统一类型系统的一部分,所有值类型都可以转换为 object。值类型变量需要用作引用类型时,将自动创建一个对象“箱”,并将值复制到箱子中。装箱后,对一个变量的操作不会影响另一个。将对象箱变回到原来的值类型时,将把箱子内的值复制到变量中。
ps:装箱与取消装箱
值类型和引用类型之间的转换通常称为强制转换(cast),因为这种转换使用C# cast运算符,但是相应的CIL指令为 box和 unbox。
装箱转换总是隐式的,它将值类型转换为引用类型。取消装箱总是显式的,它将装箱的值类型(引用类型)转换为值类型。
装箱和取消装箱操作占用的资源很多,开销也很大,应尽可能避免并确保使用正确的类型来解决问题。
如下是各种预定义类型支持且能成功完成的隐式转换。之所以允许这些隐式转换,是因为从原始数值类型转换为新的数值类型时不会降低量级。
ps:隐式转换
隐式转换可能降低精度,但是不应降低量级。以将 int 值转换为 float值为例,它们都是32位的,但是并非每个int值都可以精确地表示为float,这将导致精度降低。然而,由于float的取值范围比int大,因此这种转换不会降低量级。
在转换可能降低精度时,必须进行显式转换,此时需要指定要将原始值转换为哪种类型。显式转换的形式为(T) E,如图所示,它将E的值转换为类型T。
显式转换存在的问题是,如果不小心,代码就可能能够编译,但是运行时会失败。显式转换告诉编译器,您定这种转换能够成功,如果不成功,导致的运行阶段错误也是可以接受的。
为降低显式转换在运行阶段失败的可能性,C#提供了 as运算符,其形式为 e as T,其中e是一个表达式,而T必须是引用类型或可以为null的类型。as运算符告诉编译器,有充分的理由相信转换将成功,它试图将值转换为类型T并返回结果,如果转换失败,就返回null。
为利用as运算符,可将上图所示的代码重写为如下形式:
int? i = 36;
object boxed = i;
int? j = boxed as int?;