本文主要由zbx1425大佬所写

再谈数据类型

数据类型是一个初学者不太容易搞懂的概念,所以我们还要再强调一下。
首先,C#中的每一个“值”都是有类型的。包括由我们直接写出的称为“字面量”的东西(如 123, “Hello World”),变量的内容,以及函数返回的结果。(注:“返回 xxx” 意思就是函数运行的结果是xxx)

  • 1 - int: 不带小数点的数字是 int 类型的 “字面量”

  • 1.0 - double: 带上小数点之后,它就成了一个 double 类型的 “字面量”
    请仔细注意这里的区别。例如 3 / 2 结果是 1,因为被除数和除数都是 int 所以执行了整数除法;但 3.0 / 2 由于被除数是 double 会执行浮点数除法,结果就是 1.5 了,这就是类型不同引发的差异,即使是对于直接给出的“字面量”也是同样。

被除数和除数中只要有一个是浮点数就会执行浮点数除法

  • "1.0" - string: 双引号括起来的是 string 类型的 “字面量”,注意它和不带双引号的值 1.0 的类型与意义可完全不同!以及,(与Python不同)单引号括起来的是另一个没提过的类型,C# 中区分单双引号,只能用双引号表示 string,不要用错。

  • Convert.ToInt32("3") - 一个 string 传入 Convert.ToInt32 函数,该函数返回 int 类型,所以它整体的结果是 int 类型的 3。

一个函数接收什么类型的值,与返回什么类型的值,是由其定义决定的。难道要一个个背?不用!SharpDevelop 提供了一个贴心小功能来让您快速了解这一关键信息。
在提示列表中查找函数时,或将鼠标指针放在代码中一个函数的名称上时,将会显示一个黄底的提示框,里面有形如这样的信息(此处以ToInt32为例):

1
[↑] 1 of 19 [↓] public static int ToInt32(string value);
  • 1 of 19: 表示该函数有19种用法,也就是说可以接受19种参数(用专业名词说叫“重载”)(想不到这么多吧,233,这个函数挺强大的,不仅是string,一大堆东西都能给它转换成int),此处显示了第一种。可以点击左右的上下箭头按钮来查看全部的用法。
  • public static: 这俩咱现在不用管。
  • int: 这个在 ToInt32 函数名之前的第一个单词,就代表了它的返回类型。在这个例子中,您就知道这个函数(在这种用法中)运行之后的结果一定是一个int整数。
  • (string value): 括号里的是使用这个函数时要传入的参数。前一个单词是类型,后一个单词是名字,有多个时会用逗号分开显示。您在上节课的练习中把Console.ReadLine()读出来的string转换成int时,不就是在括号中传入了一个string类型的值-输入进来的文字么?

我们再来看一看下面这段代码。

1
Console.WriteLine(Convert.ToString(Convert.ToDouble(Console.ReadLine()) / 2));

这段代码看起来很长,对于初学可能相对难以理解,让我们一点一点来解释。

我们上次讲过嵌套函数的运行类似数学中的复合函数 f(g(x))f(g(x)) , 从最内层的函数开始运行, 然后把返回的数值代入到外面一层的函数中作为参数。

  1. 首先是最内层的 Console.ReadLine() 运行,并把读入的一行字以 string 类型返回。如果输入的是 5,现在代码就变成了:
1
Console.WriteLine(Convert.ToString(Convert.ToDouble("5") / 2));
  1. Convert.ToDouble(“5”) 运行,将 “5” 转换成 double 的 5.0 并返回。
1
Console.WriteLine(Convert.ToString(5.0 / 2));
  1. 由于一边是 double 一边是 int,运行了浮点数除法,结果是 double 的 2.5。随后 2.5 被Convert.ToString 转换成 string 的 “2.5”,然后被 Console.WriteLine 写到窗口中。
  2. 其实在这个例子中,如果去掉 Convert.ToString,也照样能够输出。这是因为 Console.WriteLine 也是有一大堆“用法”,不仅接受 string 也接受 double,您不妨在那个“用法列表”里找一找。

显然,如果不把值放在一个变量里,那一行代码执行完了之后值就没了。变量就是一种储存值的容器。正如上一节课中介绍的,在C#中,一个变量只能储存一种特定类型的值。

比如我们需要以 double 类型保存下来输入的内容以备以后再用, 那就把上面的代码改成:

1
2
double a = Convert.ToDouble(Console.ReadLine());
Console.WriteLine(Convert.ToString(a / 2));

new 运算符

编程中要处理各种各样的数据, 要是只有我们学过的 int string double 那几种类型可能会十分麻烦, 例如要是用 int 来保存当前时间的话 就需要 6 个 int 类型的变量来分别保存年月日时分秒。因此 C# 里除了我们已经用到的 string int double 之外,还提供了很多其他的类型。

那么问题来了。在使用 string int double 的时候,除了从函数中取得(如Console.ReadLine()),我们想要用一个值的时候都可以用字面量的形式写出来。比如我们想要整数1就可以直接写1,想要字符串Hello就直接"Hello"。但是字面量就只能表示这些最基础的类型了。
假如我们想要一个表示 1919年8月10日11:45:14 的时间(在C#中有个专门的类型用来表示和处理时间,叫DateTime),就没有办法用字面量写出来了,怎么办呢?

大佬们自然给我们想好了。这就是"new 运算符"。new DateTime(1919, 8, 10, 11, 45, 14) 就会给我们一个我们想要的值了。

1
2
DateTime a = new DateTime(1919, ...); // 既可以存在变量里(此处省略后面的数字)
Console.WriteLine(Convert.ToString(new DateTime(1919, ...))); // 也可以像之前的字面量一样直接用

这个用法是不是很像在用一个函数?实际上它就是一个特殊的函数(叫“构造函数”),必须要前面配着"new"来用,而且会给你一个该类型的值。和函数一样,你也可以使用那个小黄框来看它的各种用法。

不过为什么要用这些别的类型呢?因为微软在这些类型内部封装了一些便利的功能,来帮助我们完成一些与它们有关的操作。专事专用,用日期专用的类型来处理日期就会方便很多。例如,如果我们要算两个日期之间差几天,如果你用 int 来表示每个的年月日,自己来算日期,就得自己处理进位借位(进到下个月,退到上个月)、每个月多少天、是不是闰年等麻烦事。但是微软的大佬们已经给我们预先准备好了日期减日期的运算程序。我们可以直接用:

1
2
3
4
5
6
7
DateTime date1 = new DateTime(2021, 12, 5, 0, 0, 0); // 2021-12-5
DateTime date2 = new DateTime(2022, 6, 7, 0, 0, 0); // 2022-6-7
TimeSpan delta = date2 - date1;
// 这里用了微软在C#语言里面提前帮我们写好的 “日期 - 日期” 计算,它能自动处理好日期计算里的各种问题!完全不用咱自己动脑子
// TimeSpan 又是个新类型,它用来专门表示一个“时间差”。不需要记,这里只是拿来举例子
// 显然 日期 - 日期 得到一个“时间差”类型的结果,挺合理的
Console.WriteLine(Convert.ToString(delta)); // 而且ToString也支持TimeSpan类型

当然,不是所有问题都有提前做好的解决程序(不然咱还学编程干嘛),不过要是有为啥不用呢?多方便。

成员

接下来我们深入地讲一下上一节课讲到的成员访问(“.”)。
为了简便(以及与专有名词接轨)起见,我们从现在开始将某一个类型的值称为“对象”。对象这个词中文上可能相对不太好理解, 对象的英文是 object, 作为编程术语时翻译为对象, 它也有 “宾语”, "物体"的意思, 换句话说, 我们生活中能见到的非抽象的东西都可以叫对象。

如,上面这个例子中,date1 变量里储存了一个 DateTime 类型的对象。把它理解成“一个东西”即可。
成员可以理解为是一个对象“里面的”内容,可以使用"."来获得或使用。例如:Year Month Day 都是 DateTime 的成员。在上面一个例子中,date1.Year 就会返回 int 类型的 2021,它是这个日期的“年”成份。

有些成员是“非静态”的。别被这个不知所云的名词吓倒,它的意思是,这个内容是和一个特定的对象相关的,而不是和这个类型的总体泛泛地相关的。也就是说,非静态的不是对于每个对象都一样的。而“静态”(就是我们之前看到的 static 修饰符)与之正相反,是一种总体的东西,与整个类型有关,和每个单独的对象没有关系。

好像有些不太好理解,我们举个例子。例如,刚才提到的Year就是DateTime 的非静态成员,因为不同的日期的值的年份可能不同。或者再举个例子,如果我们有一个“学生”类型,那么“班级”就是它的非静态成员,因为不同的学生可能在不同的班;但是“腿数”就可能是个静态成员,因为所有学生都是两条腿——当然在设计的时候把它做成非静态的也行,不过不太有必要。

如果要用一个非静态的成员,在一个值的后面加".“就可以访问到它了。例如上面的 date1.Year 就是获取了date1 这个对象 的 Year 成员。如果要用一个静态的成员,在类型名的后面加”."即可。如 int.MaxValue 就获取到了 int 类型所能表示的最大数值。您也想必很容易理解,“能表示的最大数值”这种东西一般是“静态”的。

您甚至可以套好几层的成员访问。用上面那个学生的例子就是:“小明.班.学生人数” 获得小明这个学生所在的班的学生人数。

成员不仅可以是值,还可以是函数。例如我们一直在用的 Console.WriteLine 就是 Console 类型的一个静态成员函数(您是直接用"Console"这个类型名,而不是在 new 一个 Console 对象来用的)。

您可能会说:不对呀,输出内容不是和特定的窗口相关的吗,为什么它是静态的呢?这是因为Windows系统规定每个程序只能有一个控制台窗口,所以既然只有一个为了方便起见就干脆搞成静态的了。

您应该已经感受到值和函数的区别还是挺大的,值可以直接用,但是函数就得加()来调用,有时候还得在括号里写参数。如何区分呢?

您可能已经注意到,当您打出一个"."字时,SharpDevelop将自动弹出提示列表,里面就会列出它所有成员的名字。每个名字的左侧都有一个图标,形如“蓝色方块”和“手拿着表格”的分别是“成员变量”和“属性”,不用太计较它们的区别,它们都可以直接当作一个值来用,例如刚才的date1.Year。形如“紫色方块”的就是函数,它们要用括号来调用,例如Console.WriteLine(...)

善用自动提示!SharpDevelop的提示功能与小黄框可以显示各个成员的名称、类型和用法,您几乎不用背诵任何内容。

什么是 “null”?

在开始讲这一节之前,我们先来引入一个有趣的东西:随机数发生器。
这同样也是个类型——Random,一个 Random 的对象有一个叫 Next 的非静态成员函数,使用时需提供两个 int 类型参数分别表示最小和最大值(左闭右开),反复调用即可从里面不断取出不同的数。用来做猜数游戏想必很有趣。还是那句话,只是为了举例子,不用背。

假设我们想得到一个1~50的随机数,写了这样的代码:

1
2
Random rand;
Console.WriteLine(rand.Next(1, 51)); // 左开右闭所以右边是51

运行一下,报错了!出现了"NullReferenceException"(空引用错误)。这是为什么呢?

原来,Random rand 这样声明变量,只是创建了一个叫 rand 的,可以存储 Random 对象的“盒子”。rand的里面是空的,并没有把一个实际的 Random 随机数生成器对象“装”到里面。

当我们在 rand.Next 的时候,我们要把这个生成器从"rand"这个"盒子"拿出来让它给我们生成个随机数--但rand里现在是空的,并没有一个生成器被装在里面。所以自然出现了问题。

那我们应该怎么做呢?我们要用某种方法获得一个生成器的对象(值),然后把它装到 rand 这个“盒子”里。怎么获得呢?还记得 new 运算符么?

1
2
3
Random rand;
rand = new Random(); // 用 new 运算符搞出一个Random对象来(Random类型构造函数不需参数)
Console.WriteLine(rand.Next(1, 51)); // 现在rand已经“有对象”了,可以正常用了

接下来介绍"null"。"null"是个特殊的值,表示“没有值(没有对象)”的这么一个状态。例如,最开始的时候,rand里没有对象(没有值),也可以说“rand的值是null”。
你还可以给变量赋值为null。这代表着把里面已经有的对象(如果有)给扔掉。

1
2
3
Random rand = new Random(); // 搞出来一个生成器搁进去
rand = null; // 又给扔了
Console.WriteLine(rand.Next(1, 51)); // 又会报错了,因为已经给扔了,现在又没了

类似的,如果想知道一个变量里面有没有正在存着一个值,可以使用 rand == nullrand != null 判断。

特殊的是,内置的 int 和 double (以及部分别的,如DateTime)是“值类型”。与刚刚的 random 的区别在于,一个值类型的变量(如int a)一被创建就会自动地装有一个对象(一般会是0)。可以把它修改成别的值(a = ...),但是不能扔掉(a = null 在编译时就会报错)。所以您永远都不会担心自己 int 类型的变量会是 null 。
相反的,大多数其他类型就都是可以为"null"的了(这种类型的学名叫“引用类型”),所以用的时候一定要注意。如果您不太拿得准,最好在声明变量的时候就用"new"搞个值出来赋给它。

string 是个有趣的例子,因为它也是个“引用类型”。也就是说,“” 是个空字符串, 而 null 则是“没有字符串”。

关于这个编程上有一个相关的非常形象的笑话:卫生间里挂厕纸的那个挂钩,字符串 "abc" 是挂钩上挂着一卷纸;"" 是挂钩上没纸了,但是还挂着那个硬质壳的芯;null 则是干脆钩上啥也没了。

paper-and-null

这也是要注意的一点。当访问 string 的成员时(string 有个叫 Length 的非静态属性,代表它的长度):

1
2
3
string a = "abc"; Console.WriteLine(a.Length); // 输出 3
string b = ""; Console.WriteLine(b.Length); // 输出 0
string c; Console.WriteLine(c.Length); // 不会输出0 - 程序会报错崩溃!

因为c是null,所以试图访问c.Length会直接导致"NullReferenceException"错误。
当然,日常使用中我们不太会经常遇到"null"的string,所以不用过于担心;但您还是需要了解这种现象。