数据类型

这篇笔记我们介绍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#基本预定义类型中objectstringdynamic是引用类型,其余为值类型,而string是immutable的(不可变的),因此具体使用时其表现类似于值类型。

用户定义类型中,structenum是值类型,而其它都为引用类型。

变量定义和声明

和大多数其它编程语言一样,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#中还有个outref的作用我们已经了解,其实就是实现类似C++引用的效果,而outref的区别就是不能在传入函数之前赋值。

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实现了。

其实refout是相对比较鸡肋的特性,应该是为了兼容C/C++风格设计的,Java中没有这个功能也工作的很好,这里仅作了解。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。