数据类型
这篇笔记我们介绍C#语言中的数据类型和其使用方法,以及相关的转换规则。
数据类型的分类
C#语言中的数据类型按定义可以分为预定义数据类型和用户定义类型,按参数传递行为可以分为值类型和引用类型。
预定义数据类型
C#语言中有16种预定义数据类型,包括13种基本数据类型和3种复杂类型。
基本数据类型:
布尔 bool 字符 char
带28位小数位的定点数 decimal 单精度浮点数 float 双精度浮点数 double
8位有符号数 sbyte 8位无符号数 byte 16位有符号数 short 16位无符号数 ushort 32位有符号数 int 32位无符号数 uint 64位有符号数 long 64位无符号数 ulong
复杂数据类型:
字符串 string
所有类的基类 object
动态类型 dynamic
上面的16种数据类型实际上是C#语言规范定义的,而具体运行代码时C#预定义类型会映射到.Net
运行时中的类上,比如int
会映射到System.Int32
。我们实际开发中,要尽量使用C#类型,而非具体运行时的类型。
注:一些资料会说C#语言具有15种预定义类型,这是因为dynamic
类型为.Net Framework4.0
中新增的,该类型可以让变量不遵循强类型语言的类型检查,被赋予不同的类型。
用户定义类型
C#语言中,用户可以自己创建的类型包括:
类 class
结构体 struct
数组 array
枚举 enum
委托 delegate
接口 interface
有关这些类型的使用我们会在后续章节中逐步介绍。
值类型和引用类型
和Java类似,C#基本预定义类型中object
、string
和dynamic
是引用类型,其余为值类型,而string
是immutable的(不可变的),因此具体使用时其表现类似于值类型。
用户定义类型中,struct
和enum
是值类型,而其它都为引用类型。
变量定义和声明
和大多数其它编程语言一样,C#语言中变量可以先声明在赋值,也可以声明时直接赋值。此外,C#还可以对同一类型的变量声明简写为一行。下列变量的定义和声明方式都是合法的:
int i = 0;
int j = 1, k = 2;
int m, n = 3;
另外一点要注意的是,和Java类似C#中变量声明后必须赋值才能使用,使用未初始化的变量是无法通过编译的。这和C/C++不同,在后者中这属于未定义行为,编译器不同其结果也不同。
装箱和拆箱
我们知道类似Java或是C#语言常常号称一切皆对象,所有类型都源自一个object
基类,然而如int
等基础值类型如果也设计为对象,那么对于涉及数值的计算密集型程序运行时的开销就太大了,出于性能考虑Java设计了基本类型的包装类,而C#设计了自动装箱拆箱机制。
C#中装箱指值类型隐式转换为引用类型,拆箱则正相反,下面是一个例子。
int i = 1;
object j = i;
int k = (int)j;
将i
赋予object
类型的变量j
的过程中,就发生了自动装箱,其中并没有什么特殊的语法,它是一个隐式转换;而j
赋予int
类型的变量k
时发生了拆箱,这里注意只有被装箱的数据才能拆箱。
可空值类型
可空类型指变量可以被赋予空值null
。一般来说C#中值类型都不是可空的,比如int
是无法被赋予null
的。但这种情况也并非不会出现,比如对一个可能会变成null
的变量拆箱该如何处理的?如果再if
判断一下就比较麻烦了,这个问题可以使用可空值类型语法,下面是一个例子。
int? i = null;
int j = i ?? 0;
上面代码中int?
用于定义可空值类型,其实int? i = null;
这个语法糖等同于Nullable<int> i = new Nullable<int>();
,Nullable
接口是一层对于原基础值类型的封装。第二行??
运算符通常用于可空类型给原基础类型赋值,因为int
不能接受null
值,??
运算符可以做到判断i
是否为null
,如果是则将j
赋予给定默认值。
变量类型自动推导
C#支持变量类型的自动推导,我们可以直接使用var
关键字而非具体类型来声明变量并赋值,此时编译器会根据紧随其后的赋值进行变量类型的自动推导,下面是一个例子。
var i = 1;
var j = 1.2;
var str = "hello";
Console.WriteLine("type i : " + i.GetType());
Console.WriteLine("type j : " + j.GetType());
Console.WriteLine("type str : " + str.GetType());
编译输出结果如下。
type i : System.Int32
type j : System.Double
type str : System.String
不过这里要注意不要乱用变量类型的自动推导,自动推导语法会极大的降低强类型语言代码的可读性,一般仅用于声明一些临时变量,或是用于简单计数的变量等情况。
const 常量
C#的关键字const
可以定义常量,编译时常量的具体值会替换掉引用常量位置的代码。常量声明在类内部或是方法内部都是可以的。建议能够声明为常量的字段,都声明为常量。
const int i = 100;
C#常量和Java的区别:Java没有直接实现“常量”这个功能,而是使用static final
声明一个不可改变的静态变量,其实这就是常量,效果是一样的。Java中static
是不能用于方法内部的,final
则可以用于方法内部。
类型转换
C#中可以使用is
关键字来判断变量的类型。
int i = 1;
if (i is int)
{
// ...
}
C#的类型转换有三种:隐式类型转换,强制类型转换,安全的类型转换(使用as
关键字)。
// 隐式类型转换
int i = 1;
long j = i;
// 强制类型转换
long m = 1L;
int n = (int)m;
// 安全类型转换
object a = new Dog();
Dog b = a as Dog;
注意:安全类型转换只能用于引用类型。
ref和out
在Java中,语法本身模糊了指针(引用)这些概念,C#也是如此,看下面C#例子(Java也是同理)。
using System;
namespace Gacfox.Demo.Demonet
{
class Program
{
static void Main(string[] args)
{
int i = 1;
foo(i);
Console.WriteLine(i);
}
static void foo(int i)
{
i = 0;
}
}
}
我们知道,在foo
中设置i
是不会影响外部的,因此上面代码中,Main
函数内部的int i
最终值为1
。
但是在C/C++中,我们其实是可以实现类似功能的,我们需要使用引用或指针(C++概念),下面例子使用引用实现。
#include <iostream>
void foo(int &i);
int main()
{
int i = 1;
foo(i);
std::cout << i << std::endl;
return 0;
}
void foo(int &i)
{
i = 0;
}
C#中可以使用ref
实现类似C++“引用”的效果(Java中没有任何办法)。
using System;
namespace Gacfox.Demo.Demonet
{
class Program
{
static void Main(string[] args)
{
int i = 1;
foo(ref i);
Console.WriteLine(i);
}
static void foo(ref int i)
{
i = 0;
}
}
}
最终Main
函数中的i
值为0
。
除了ref
外,C#中还有个out
,ref
的作用我们已经了解,其实就是实现类似C++引用的效果,而out
和ref
的区别就是不能在传入函数之前赋值。
using System;
namespace Gacfox.Demo.Demonet
{
class Program
{
static void Main(string[] args)
{
int result1;
int result2;
foo(out result1, out result2);
Console.WriteLine("i {0} j {1}", result1, result2);
}
static void foo(out int i, out int j)
{
i = 0;
j = 1;
}
}
}
out
用于函数参数作返回值的情况。熟悉C/C++的同学应该知道我在说什么,C中有一种面向过程式编码产生的写法,就是当函数需要返回多个值时,通常将指向结果变量内存使用指针(或引用)传给函数,在函数体内设置对应内存后函数返回,在C#中就可以使用out
实现了。
其实ref
和out
是相对比较鸡肋的特性,应该是为了兼容C/C++风格设计的,Java中没有这个功能也工作的很好,这里仅作了解。