简介

本节给大家介绍一下 C# 的数据类型,以及变量声明的方式和用法。

Part 1 让我们先来学习一下基本的用法

数据类型(Data Type)是为了规范化管理所有世间万物编程写入代码的时候,进行处理的一种体系。如果没有数据类型这个概念,可能就会像是 Python 那样,编译期间无法察觉任何数据的问题;而在运行的时候才会知道,“Shoot,数据计算的时候出错了”。

数据类型,是把世间万物以编程的视角定义出来的一种“属相”。比如说买菜的单价,是小数类型的(因为可以精确到毛和分);再比如说排队人数,是整数类型的(严格来说是正整数类型的)。我们通过这样的机制,来表述一个事物的确切数量或者数据的具体大小。

用来临时传达和表示数据大小的手段,就是变量(Variable)。比如下面的这段代码,就是通过定义变量,表示一个指定的数据,然后显示在屏幕上的过程:

  1. using System;
  2. class Program
  3. {
  4. static void Main()
  5. {
  6. // 变量定义。
  7. // dogName、age、price 都称为变量(Variable)。
  8. // "Shiny"、5、299.98D 都称为字面量(Literal)。
  9. // string、int、double 都称为变量的数据类型,或简称数据类型或类型(Data Type 或 Type)。
  10. // C# 变量必须先定义后使用。定义的方式是:
  11. //
  12. // 数据类型 变量名 = 字面量;
  13. //
  14. // 整个语句叫赋值语句(Assignment Statement),其中的“赋值”(Assign)表示
  15. // 把右侧的字面量信息给左边的变量存储起来。
  16. string dogName = "Shiny";
  17. int age = 5;
  18. double price = 299.98D;
  19. // {0} 和 {1} 叫占位符(Placeholder)。
  20. // 占位符里面的数字一定要从 0 开始。
  21. Console.WriteLine("The dog's name is {0}, {1} years old.", dogName, age);
  22. Console.WriteLine("When I bought her home, the price is {0}.", price);
  23. }
  24. }

首先,我们来说明一下,每一个数据都是表达什么。首先,我们前面说过,程序的整体执行顺序是从第 7 行开始,到第 12 行结束,那么我们一行一行来看:

第 7 行写的是 string dogName = "Shiny";,这句话的意思是,定义了一个叫做 dogName 的变量。dogName 是我们随手写的名称。显然它并不是 C# 语法规定的成分,因此它是一个标识符。

既然是标识符,那么必然需要满足标识符的书写规范和规则:数字、字母、下划线和 @ 的组合,首字符不能是数字;后面的字母里不能有 @,等等。显然,dogName 是符合取名的要求的,因此是一个正确的标识符。从英语本身的角度来说,dogName 的实际意思就是“狗狗的名字”。是的,后面我们直接写上了 "Shiny",这个以双引号引起来的东西,我们称为字符串字面量(String Literal)。我们通过等号 = 来将右侧的这个字面量赋值给(Assign)左边的变量,这个行为就叫做赋值(Assignment),从人类可以理解的逻辑来看,你可以把 dogName 当成一个箱子,这个 "Shiny" 就是箱子里装的东西。赋值语句(string dogName = "Shiny";)的过程就是把这个东西装进箱子。

我们稍微注意一下,dogName 前面有一个 string。这就是 dogName 的数据类型:string 的意思是“串”,比如英语里的 a string of(一串……)。在这里,string 被认为是一个关键字。它的作用就是用来声明一个变量是“串”类型的。所谓的数据类型“串”,就是用来表示“原封不动的数据序列”。它们直接用双引号写出来,然后通过赋值的形式给到变量上去。

同样地,我们可以得到第 8 行和第 9 行类似的理解方式:定义一个 intdouble 类型的变量为 ageprice,然后分别赋值 5299.98D。首先,299.98D 的字母 D 是可以不写的,即 299.98,那么就是一个很普通的小数;而 5 则是一个很普通的整数。然后,分别通过赋值语句进行赋值。

最后我们来看一下第 11、12 行。第 11 行是我们前文介绍过的 Console.WriteLine 方法。这个过程用来输出显示一行文字到屏幕上。不过这里有所变化,因为在使用和显示内容的时候,后面还使用逗号分隔了 dogNameage。细心一点可以看到,整个输出内容里,有两处大括号和数字编号构成的占位符(Placeholder)。占位符最后会被后面的变量的数据以对位的形式替换掉。比如说,{0} 对应第一个逗号分隔的变量 dogName,而 {1} 就对应第二个逗号分隔的变量 age

占位符的作用就是体现在 Console.WriteLine 里,用来被逗号分隔的变量作替换的。按照 C# 语法的约定,占位符的编号(0、1、2 这些)可以重复使用,但必须从 0 开始编号。比如说

  1. string variable = "years old";
  2. int a = 30;
  3. int b = 40;
  4. Console.WriteLine("I'm {1} {0}, while she is {2} {0}.", variable, a, b);

这里的 {0}"years old" 字符串字面量替换,{1} 则被 30 替换,{2} 被 40 替换,所以输出内容就是

  1. I'm 30 years old, while she is 40 years old.

大概就是这么一个感觉。

顺带一提,第 10 行没有东西,我们把这种“单纯是为了排版好看,为了分清执行逻辑部分”的空白行称为空行(Null Line)。空行仅用来分隔和划分执行逻辑的部分,比如说这个空行的前面是定义变量,后面则是输出内容,它们的作用是不一样的,使用空行可以让代码更美观。

最后,前面示例最终的显示结果是这样的:

  1. The dog's name is Shiny, 5 years old.
  2. When I bought her home, the price is 299.98.

再顺带一提,intdouble 在 C# 里分别表示整数和小数。整数和小数在 C# 里分得非常细致,所以并不是只有 int 就表示整数类型,也并不是只能用 double 来表示小数。下面我们就来看一下,C# 预定义的数据类型。

Part 2 C# 自带的数据类型

在 C# 的世界里,规定了许多基本的数据类型。我们都来看一看。

类型名 类型名称 取值范围(用闭区间表示) 全称 独特的字面量格式
sbyte 带符号字节型 [-128, 127] System.SByte
byte 字节型 [0, 255] System.Byte
ushort 无符号短整数 [0, 65535] System.UInt16
short 短整数 [-32768, 32767] System.Int16
uint 无符号整数 [0, 4294967295] System.UInt32 0U0u
int 整数 [-2147483648, 2147483647] System.Int32
ulong 无符号长整数 [0, 三、C#数据类型 - 图1] System.UInt64 0UL0ul
long 长整数 [-三、C#数据类型 - 图2, 三、C#数据类型 - 图3] System.Int64 0L0l
float 单精度二进制浮点数 System.Single 0F0f
double 双精度二进制浮点数 System.Double 0D0d
1E+21e+2
decimal 十进制浮点数 System.Decimal 0M0m
bool 布尔 { true, false} System.Boolean truefalse
char 字符 System.Char 'c'
'\\u0067'
'\\x43'
string 字符串 System.String "string"
@"C:\\Users"

整个表格根本不需要背下来。毕竟不是考试。你只需要知道,什么是无符号、什么是二进制小数、什么是十进制小数、什么是布尔就可以了。

无符号(Unsigned)和带符号(Signed)就是我们俗称的“非负整数”和“正负整数”。无符号整数就是没有负数范围的整数(也就是 0 和正整数);带符号整数就是这个取值范围既可以在正整数这一边,也可以到负整数这一边,也可以是 0 的情况。由于电脑存储机制有限的缘故,我们无法存储一个特别大的数据进去(而这样的数据确实用得也很少)。C# 将我们使用的数据做了一个区分和范围定界。比如说,如果要用变量表示开会人数的话,显然开会的人数再怎么多,也不可能超过几万吧;而且,人数这个量不能是负值。所以按照 C# 的约定,我们应当选取 ushort 作为最合适的数据类型来表示:ushort popularity = 3000;

你可能会问我,我选 int 不是也可以?反正 int 范围那么大,3000 肯定是在其中的。是的,你没有说错。也正是因为这个原因,前文说的是“我们应当选取”,而不是“我们必须选取”。只要我们预估的数据的可能取值在一个指定范围内的话,就随便你怎么选都 OK。所以,int popularity = 3000; 是没有问题的;但是,如果你写 byte popularity = 3000; 必然是错误的。

接着说一下浮点数(Floating-point Number)。在计算机的世界里,浮点数其实就是小数的官方说法。因为在计算机的内部,小数是通过科学计数法表达的,因此小数点就变成了漂浮不定的了,因而称为浮点数(小数点漂浮不定的数字)。

小数在计算机里有两种表达模型,一种叫二进制小数(Binary Floating-point),一种则是十进制小数(Decimal Floating-point)。二进制小数就是将小数用 三、C#数据类型 - 图4 的方式表达出来;而十进制小数就是 三、C#数据类型 - 图5 了。别看只是 2 和 10 变了,实际上数据的存储精度也会变化。

在学习小数的二进制表达的时候,我们知道,很多小数在十进制下是精确的,但表示成二进制之后,小数就不再精确了;相反,它们在二进制表达下,反而变成了无限循环小数。因为计算机存储机制的限制,这些数据不能精确存储,“无限循环小数”直接被截断(Truncate)为“有限小数”,因而数据本身都发生变化了,所以就不精确了。

我们无需了解底层存储浮点数的方式(如果确实需要了解相关内容,请参看 IEEE 754 规范),所以只需要知道浮点数是不精确的就行了。

浮点数一共有三种,它们的范围也有一些区别,最小的是 float,能精确大约 6 位有效数字;double 则大约能到 12 位有效数字;decimal 则精度更高(甚至能达到 28 位左右)。但是,它们范围最小都能表达到 10 的 38 次方,所以完全不用担心运算超出数据范围。由于从使用上来说,三种数据类型只有精度和数据范围的区别,实际上没别的区别,因此这里就不作很细的区分了;如果用到了确实需要讲的区别,我们会在后续内容里说明。

布尔(Boolean)类型说白了就是我们平时生活中用来回答“对”和“错”的这两个数值。“对”在 C# 里用关键字 true 表示,而“错”就用的是关键字 false 表示。比如说:

  1. bool condition = true;
  2. bool anotherCondition = false;

大概是这么一种感觉。当然,从另外一个角度来说,布尔型数据的数据取值范围只有这两个数值;但别看只有这两个取值,但它的用处很多,也很重要,以后在讲解条件、循环结构的时候,会广泛使用布尔型数据。

最后,字符和字符串我们将放在后面的内容给大家介绍。因为这个内容说起来很多,因此单独成一节内容。

然后看到表格,稍微注意一下的是,字节型数据和带符号字节型,以及其它的整数类型是有区别的:字节型数据是默认无符号的,要使用带有负数的范围的字节型数据,需要追加 s 字母(signed 这个单词);而其它的整数默认就是带符号的(有负数范围的),需要使用无符号的类型,则需要追加 u 字母(unsigned 单词)。

Part 3 基础类库名

前文提到的这些数据类型,除了使用前面的关键字作为类型名称以外,还可以使用全称。可以从前面的表格里看到,它们的全称都带“System.”。是不是很眼熟?是的,这就是我们之前说 Console 的时候,需要引入的命名空间。是的,所有这些类型都是系统自带的,因此全部被包含在 System 命名空间下。如果你使用它们,要么直接使用关键字,要么写全称。写全称就需要你先写上 using System; 才可以使用它们。

  1. // BCL name way to declare a variable.
  2. using System;
  3. Int32 i = 10;
  4. // Keyword way to declare a variable.
  5. int j = 30;

它们是等价的两种写法,唯一的区别就是,全称写法需要引入 System;而 int 这类关键字的话,直接用,不用写引用指令,系统会自动追加 using System; 这一条指令。

哦对了,这些数据类型的全称,我们称为基础类库(Basic Class Library,简称 BCL)名称。

Part 4 总结

本节内容给大家做了一个基本的数据类型的介绍,还说了变量的声明方式,以及使用的方式(通过占位符和 Console.WriteLine 方法配套使用)。

下一节内容,我们会继续探讨数据类型。因为……字面量,还有字符、字符串类型我们都还没说呢。

字面量和数据类型的关系

Part 1 定义

什么是字面量(Literal)?字面量是一种直接写到代码里的数值。在编程的时候,我们直接写的数字符号(包括小数和整数)、原封不动输出的字符串,这些都被称为字面量。只是说,字符串是字符串字面量,而数字呢,则叫做数字字面量。

  1. int age = 25;

在这里示例里,25 这个数字是我们直接写到程序代码里的数据信息。这个 25 我们就称为整数字面量。

Part 2 字面量的对应类型

在 C# 的世界里,我们使用的字面量都对应一个“默认的”数据类型。换句话说,即使我们左侧的变量不给出数据类型的名称(不管是 BCL 名称还是关键字),我们总可以通过字面量本身确定变量的类型。

  1. using System;
  2. class Program
  3. {
  4. static void Main()
  5. {
  6. // 字面量(Literal)。
  7. {
  8. // 整数类型的字面量。
  9. sbyte a = -16; // 成功。
  10. byte b = 16; // 成功。
  11. short c = -12345; // 成功。
  12. ushort d = 34567; // 成功。
  13. int e = -100000000; // 成功。
  14. uint f = 3234567890; // 成功。
  15. //int e2 = 100000000u; // 失败。
  16. uint f2 = 100000000u;
  17. //long g = _;
  18. //ulong h = _;
  19. // 1、整数字面量的兼容规则:看数值是否超过左边变量类型的取值范围。
  20. // 2、整数字面量一般都有默认的数据类型:整数字面量默认为 int 类型。
  21. // 3、给字面量末尾加上 U 或 u 改变该字面量的类型为 uint 类型。
  22. // 4、给字面量末尾加上 L 或 l 改变该字面量的类型为 long 类型。
  23. // 5、给字面量末尾加上 LU 或 UL 或 lu 或 ul 改变该字面量的类型为 ulong 类型。
  24. // 6、一般不建议给整数字面量加上后缀。
  25. }
  26. }
  27. }

2-1 正整数字面量

我们可以总结出如下两点:

  1. 如果是一个整数字面量的话,那么它默认就是 int 类型的数据;
  2. 如果这个数据可以用范围更小的数据类型表达出来,那么这个数值也可以直接自动赋值给这个类型的变量。

下面我们来看一下,这个解释是什么意思。首先

  1. byte age = 25;

这个赋值语句下,25 是默认 int 类型的。大家都知道,int 类型和 byte 类型来比较的话,显然 int 范围更大;如果将一个能表示出范围更大的数据类型赋值给一个表示范围较小的数据类型,就可能产生错误,比如说一个 byte 类型的变量,接受了 300 这个数,显然就是不合理的。但是我们通过第二点可以得到,显然例子里的 25 是属于 byte 类型的数据范围的,所以这样的赋值语句,编译器根本不应该告诉你,这是错误的赋值。所以,编译器允许这么赋值。

因此,我通过这则示例告诉你,25 是 int 类型的;而 agebyte 类型的,编译器也允许这么赋值。

当然,25 也可以写成 +25,因为数学上的正号加不加都没关系,因此 C# 也是一样。

2-2 负整数字面量

除了正整数,还有负整数的字面量。和数学上的表达完全一致,我们使用减号来表示负数:

  1. int temperature = -13;

这里的 -13 就是负整数字面量。很有趣的一点是,因为 int 类型的取值范围是包含负数的,因此实际上负整数字面量的默认数据类型依然是 int。而且,它依旧遵循前文提到的“范围最小赋值原则”(说白了就是前面那一节内容里面的那个第二点)。

2-3 长整数字面量

C# 的语法是严谨的。由于前文的整数字面量是默认 int 类型的,那么我们就无法直接写一个超出 int 范围的整数字面量。但是 long 类型必须要赋一个初始数值的话,怎么办呢?

我们使用后缀字母 L(或者小写 l)来告诉编译器,这个字面量是 long 类型的。

  1. long population = 7800000000000L;

显然,这里的 78 亿是超出了 int 的取值范围的(int 最大才到 2147483647),因此如果我们直接写上 7800000000000 的话,就会出现编译器错误。因此,我们需要强制追加后缀 L 来告诉编译器,这里的这个数字是 long 类型。这样的话,左侧的变量就可以正常接受字面量数值了。
同理,如果数字为负数,依旧添加负号即可:long p = -1000000000000L

另外,这么赋值也是没问题的:long p = 13L,但是反过来就不对了:如果右侧是 long 的字面量,而左边是 int 类型,即使范围合适,依然不允许赋值(即错误赋值):int p = 13L。这是因为,既然 13 是正常的 int 字面量,那么为什么要追加 L 后缀,然后再赋值给 int 类型呢?这不是就绕了一步吗?因此编译器不允许我们这么搞。

2-4 无符号整数字面量

我们知道,一个整数字面量是没办法确保是不是正整数的。为了确保字面量本身就可以表示无符号类型,那么就产生了这样的字面量。

  1. uint age = 25U;

比如,这样。我们使用 U 字母(或小写 u)来表示和暗示这个字面量是 uint 类型的(即无符号整数)。

不过,它和最开始就说的正整数字面量有区别吗?是的,有区别。前面的正整数字面量默认类型是 int 类型,而这里的无符号的整数字面量默认是 uint 类型的。正是因为这个区别,因此我们如下的四种组合里,有一种就是不正确的:

  • int age = 25;(正确)
  • int age = 25U;(错误)
  • uint age = 25;(正确)
  • uint age = 25U;(正确)

这里的 int age = 25U 是错误的赋值。因为 int 类型和 uint 类型并不能说“范围完全兼容”,uint 类型只包含正整数,因此范围比 int 类型多了一截;而 int 可表示负数,所以 int 可以表达负数。

显然,这样的数据类型是无法完整兼容的,因此我们无法通过字面量来把 uint 赋值给 int 类型。

另外,由于无符号字面量本身就是不可负的,因此无符号字面量的前面是不能加负号的。换句话说,-25U 是错误的写法;但是,带正号可以:+25U

最后,U 后缀和 L 后缀是可以混用的。不论你的 U 字母和 L 字母的顺序如何,大小写如何,只要写在一起作为字面量后缀,就可以表示一个字面量是 ulong 类型的。

  1. ulong population = 7800000000000UL;

同样地,这里的带 UL 后缀的字面量也无法赋值给范围较小类型的其它整数类型。比如 uint age = 25UL 就是错误的写法。

2-5 浮点数字面量

浮点数类型有三种,所以有三种不同的字面量类型。

  • 使用 fF 后缀,表示一个小数是 float 类型的;
  • 使用 dD 后缀,表示一个小数是 double 类型的;
  • 使用 mM 后缀,表示一个小数是 decimal 类型的;
  • 如果任何后缀都没有的小数表达式,默认就是 double 类型的。 ```csharp using System;

class Program { static void Main() { { // 浮点字面量(小数字面量)。 float a = 3.1415926F; double b = 3.1415926535897932384626433832795; decimal c = 3.1415926M;

  1. // 1、浮点字面量默认是 double 类型的。
  2. // 2、使用 F 或 f 后缀来强制让浮点字面量改成 float 类型。
  3. // 3、使用 M 或 m 后缀来强制让浮点字面量改成 decimal 类型。
  4. // 4、如果小数位过长,会被编译器按照当前数据类型的精度直接截断(Truncate)。
  5. // 科学计数法。
  6. double b2 = 1e6;
  7. //int i = 1e6;
  8. double b3 = 1e-6;
  9. double b4 = -1e-6;
  10. double b5 = 1.4e10;
  11. //double b6 = 1e1.4;
  12. float a2 = 1e6f;
  13. decimal c2 = 1.4e10m;
  14. // 1、科学计数法字面量的默认数据类型是 double 类型的,即使你给出的数据可能是一个整数。
  15. // 2、使用 aEb 或 aeb 表示 a * 10 的 b 次方。
  16. // 3、b 是可以带有 +/- 符号的。
  17. // 4、a 也是可以带有 +/- 符号的。
  18. // 5、a 和 b 里面,只有 a 可以带小数点,而 b 不能。
  19. }
  20. }

}

  1. 从精度上和数据取值范围上来说,`double` 都比起 `float` 类型要大,因此 `float` 类型的字面量可以赋值给 `double`,但反过来不行:
  2. ```csharp
  3. float f = 30F;
  4. double d = 30F;
  5. float g1 = 30D; // Wrong.
  6. float g2 = 30.0D; // Wrong.
  7. float g3 = 30.0; // Wrong.
  8. double e1 = 30D;
  9. double e2 = 30;

这样的赋值里,g1g2g3 的赋值是错误的。因为 float 类型范围较小,却接受了一个 double 类型的字面量。

另外,这里简单说一下 decimal 类型字面量。decimal 类型的取值范围比较小,但精度特别高(能精确到 29 位),所以它和 float 还有 double 范围上并不能直接兼容。和 uint 还有 int 的赋值逻辑一样,我们不能将 decimal 字面量赋值给 floatdouble 类型的变量;反过来把 floatdouble 类型的字面量赋值给 decimal 类型的变量也不可以。

  1. decimal d1 = 30M;
  2. decimal d2 = 30F; // Wrong.
  3. decimal d3 = 30D; // Wrong.
  4. float f = 30M; // Wrong.
  5. double d = 30M; // Wrong.

这里,后面四种赋值全部都是错误的。

如果整数部分是 0 的小数可以不写这个 0,即比如说 0.37 可以写成 .37;但是小数部分就算为 0,也不能省略:30.0 不能写 30.,这个写法 C 语言允许,但 C# 里不行。

2-6 整数和浮点数

前面我们一直说的是整数和小数的字面量,但它们没有互相赋值。

思考一下,decimal d = 30U; 会不会成功呢?C# 里规定,所有前文提到的字面量全部都可以通过赋值的方式赋值给任何浮点数类型,但反过来的话,任何浮点数类型的数据都不可以赋值给任何整数的数据类型。因为浮点数的范围我们没有在前文里提及,因此我们无法介绍这一点。

大概来说,float 这个最大可以到大约 10 的 38 次方(再怎么说都超出 ulong 最大的整数的数据取值范围了),double 能达到大约 10 的 308 次方,而 decimal 的最大值大约是 三、C#数据类型 - 图6。所以范围上来说,最小范围的 float 类型,也可以完美覆盖任何整数数据类型的数据,因此,用浮点数类型的变量接受整数类型的字面量,是怎么都允许的;但反过来因为数据的取值范围不兼容的缘故,这样就不行了。

稍微啰嗦一下,30 和 30.0 的区别是,在 C# 里,30 是整数(int 字面量),而 30.0 是浮点数(double 字面量)。因此,double d = 30double d = 30.0double d = 30D 的赋值意义不太一样。后面两种是同样的模式(因为都是 double 类型字面量),而第一个则是通过“浮点数接受整数类型的变量”这个规则而赋值的。

2-7 科学计数法字面量

为了保证数据能够简单表达,C# 允许使用科学计数法类似的写法来表达一个浮点数。举个例子:

  1. double d = 1E5;

在这里例子里,1E5 称为科学计数法。科学计数法的表达规则是这样的:如果一个数学上的科学计数法表达是 三、C#数据类型 - 图7 的话:

  • 三、C#数据类型 - 图8 部分使用字母 Ee 代替;
  • a 和 b 则按照科学计数法的基本写法即可。

比如 1E5 就是 三、C#数据类型 - 图9。当然,如果 b 是负数的话,科学计数法里这样表达的数据就是一个小数了。比如 1E-3 就是 10 的 -3 次方,即 1/1000(0.001)。

2-8 字符和字符串的字面量

因为这一个内容牵扯到字符串的使用,因此这里我们不作介绍,我们在下一节就会介绍它们。

2-9 布尔型字面量

最后还剩下布尔型数据的字面量没有说了。因为布尔型只用来表示对错,所以只有(True)和(False)两个结果。在 C# 里,直接采用 truefalse 这两个关键字就可以充当了。

  1. bool condition1 = true;
  2. bool condition2 = false;

就是这样的。前面的文章其实就已经说过了,不过这里再说一下。这里的 truefalse 是布尔型里唯有的两个字面量。

另请注意,因为它只表示对错,因此跟整数、浮点数完全没有关联,因为我们无法把布尔型数据赋值给任何浮点数数据类型和任何整数数据类型;反之亦然。

  1. bool condition = 3; // Wrong.
  2. int variable = false; // Wrong.

这两种赋值都是错的。bool 字面量只能赋值给 bool 类型的变量。

Part 3 总结

我相信你根本记不住前面的东西。所以我这里做一个简要的回顾和归纳。

3-1 字面量默认类型

  • 整数字面量默认是 int 类型的(不论带不带正负号,都是 int 类型的);
  • L 后缀(或 l)的整数字面量是 long 类型的(不论带不带正负号);
  • U 后缀(或 u)的整数字面量是 uint 类型的(这样的话,只能带正号,或者不要符号);
  • 同时带有 UL 后缀的整数字面量是 ulong 类型的(这两个后缀记号不需要考虑先后顺序,也不需要考虑大小写);
  • Ff 后缀的字面量是 float 类型的;
  • Dd 后缀(或不带后缀的小数)的字面量是 double 类型的;
  • Mm 后缀的字面量是 decimal 类型的;
  • 科学计数法字面量默认是 double 类型的;
  • bool 字面量只有两个:truefalse,且均使用关键字表示。

3-2 字面量赋值关系

  • 所有字面量的默认类型,可以直接赋值给这个类型的变量;
  • 不带后缀的整数字面量是可以允许在兼容范围的时候,赋值给较小数据类型的变量;
  • 带后缀的整数字面量,不能赋值给范围较小的变量(不论是否兼容取值范围);
  • double 类型的变量能接受 float 字面量和 double 类型的字面量的赋值,但反之不然;
  • decimal 类型的变量只能接受 decimal 字面量,其它的浮点数字面量都不可以;
  • 任何整数类型的字面量均可以直接赋值给任何浮点数类型,但反之不然;
  • 由于科学计数法是 double 类型的字面量,所以它的赋值规则和 double 字面量的规则是一样的;
  • bool 字面量不能赋值给其它任何类型的变量,反之亦然。

字符和字符串

字符和字符串是 C# 里广泛使用的一种专用表示“只用来呈现原封不动的数据”的数据类型。字符是 char 关键字表示的,它的 BCL 名称是 System.Char,表示一个单独的字符;而字符串是表示一系列字符构成的序列,用 string 关键字表示,它的 BCL 名称是 System.String

下面我们来看一下基本的赋值方式和书写格式。

  1. using System;
  2. class Program
  3. {
  4. static void Main()
  5. {
  6. // 字符字面量(Character Literal)和字符串字面量(String Literal)。
  7. char c = '0';
  8. int i = 0;
  9. Console.WriteLine(c);
  10. Console.WriteLine(i);
  11. string s = "今天吃什么?";
  12. Console.WriteLine(s);
  13. // 转义字符(Escape Character)。
  14. string quote = "He said, \"I'm tired now.\"";
  15. Console.WriteLine(quote);
  16. char c2 = '\'';
  17. Console.WriteLine(c2);
  18. string twoLinesString = "The first-line sentence.\nThe second-line sentence.";
  19. Console.WriteLine(twoLinesString);
  20. string s2 = "abc\\def";
  21. Console.WriteLine(s2);
  22. //string wrongString = "\%";
  23. // 原义字符串(Verbatim String)。
  24. string verbatimString = @"The first-line sentence.
  25. The second-line sentence.";
  26. Console.WriteLine(verbatimString);
  27. }
  28. }

Part 1 字符字面量和字符串字面量

我们先来说字符。字符就是一个单独的原始数据。

  1. char c = 'a';

C# 里使用单引号来表达 a 是一个字符。

当然,一个数字字符和一个数字是有区别的。比如说 '3'3 是有区别的。因为字符 3 仅用来呈现结果,所以它并不应当参与任何数值上的计算;而数字 3 是可以进行运算的。运算的功能我们将在下一章(运算符)里讲到。

虽然,如下的写法都是可以输出一个字符 3 的:

  1. Console.WriteLine('3');
  2. Console.WriteLine(3);

但显然,它们的区别在于,一个是呈现字符本身,一个则是将数字当成字符呈现(绕了一步)。因此它们是有区别的。

和 C 语言不同,C# 的字符是包含中文汉字、日语假名等等符号的。比如说:

  1. char ki = 'き';
  2. char xiao = '笑';

在 C 语言里,一个字符是只占一个字节的,因此它只能表达一个很简单的字符;汉字、日文甚至别的一些符号并不属于标准 ASCII 码里的内容,因此无法用一个字符来表示。但 C# 里是可以的。

这就是为什么,字符没办法参与计算了。

顺带一说,这里以单引号引起来的内容称为字符字面量(Character Literal)。

Part 2 字符串字面量

显然,一个单独的字符并不能表达很多的信息,因此大多数时候我们都会使用字符串(String)来表示一个字符序列,以达到描述复杂信息的功能。

  1. string hello_en = "Hello, how are you?";
  2. string hello_jp = "こんにちは、お元気ですか。";
  3. string hello_cn = "你好啊,状态好不?";

我们可以通过双引号,来表述一组字符信息。把它们串起来,表示一个整体。以双引号引起来的字符序列就称为字符串字面量(String Literal),当然,有些时候也简称字符串或者

C# 贴心地为我们提供了 Console.WriteLine 方法,可以让我们使用前面的所有类型,全部内嵌到小括号里参与输出,因此,这些字符串依然可以写进去。

  1. Console.WriteLine("こんにちは、お元気ですか。");
  2. Console.WriteLine("はい、元気ですね。ありがとうございます。");

由于字符串可以任意伸缩长度,因此,一个空字符串(Empty String)也是可以的:

  1. string emptyString = "";

这样的话,emptyString 就是一个空的字符串,里面没有任何字符。当然,空字符串你也可以写成 String.Emptystring.Empty

  1. string emptyString = string.Empty;

前文稍微提到过,string 是关键字,而 String 是 BCL 名称。它们的区别仅仅是“BCL 名需要引入 System 命名空间,而关键字不需要考虑命名空间的事情,编译器会自动导入”。初学的话,可能你不会愿意记很多复杂的东西,因此我建议你使用关键字,这样就不用引入命名空间了。

Part 3 原义字符串和转义字符

由于双引号在编程的时候有特殊用途(表示字符串字面量的开头和结尾),所以很明显,我们并不能在字符串字面量里使用引号。如果偏要输出引号的话,我们必须在左边添加反斜杠:\"。这个行为称为转义(Escape)。那么理所应当地,这里带反斜杠的字符就是转义字符(Escape Character)了。

  1. Console.WriteLine("She said, \"Don't be lazy.\"");

在这个例子里,双引号内嵌到字符串里作为输出的一部分,就需要添加反斜杠标记。

在 C 语言里,\"\' 都是转义字符,即双引号里不可以直接出现单引号;但 C# 里是允许的。

转义字符除了上面说的双引号以外,还有一些别的。比如说制表符。制表符占据 8 个空格的空间。如果是写入字符串的时候,我们可以直接插入制表符:“ ”,但这样显得很不正常,因此 C# 里提供了一个字符写法:\t,这就表示一个制表符:

  1. Console.WriteLine("Name\tAge");
  2. Console.WriteLine("Sunnie\t30");

最后,由于反斜杠本身作为转义字符的开始,因此我们单纯要输出一个反斜杠的时候,必须双写它。

  1. char c = '\\'; // A real backslash.
  2. string directory = "C:\\Users\\Admin\\Desktop";

我们目前就只需要接触这几个就可以了。后续用到新的转义字符的时候,我们再作出说明。

但是有些时候,我们要写一大段的文字(很有可能自带换行)。这个时候,字符串怎么写呢?我们采用的办法是通过原义字符串(Verbatim String)来搞定。

为了避免字符串里出现的特殊字符,C# 提供了一种字符串:

  1. string directory = @"C:\Users\Admin\Desktop";

如果是一个很普通的字符串,就必须双写反斜杠来表示一个反斜杠字符。当我们在字符串最开头追加原义标记 @ 后,字符串就成了原义字符串。原义字符串的内部除了双引号以外的其它所有字符都可以随便用了:不管你怎么写,原义字符都不作处理。

如果原义字符串里需要表示一个双引号的话,由于原义字符串会把反斜杠当成一个很普通的反斜杠,因此 \" 这种写法会失效。所以,此时用到的是双写:""。我们双写双引号,就表示原义字符串里的一个双引号字符。

  1. Console.WriteLine("She said, \"Don't be lazy.\"");
  2. Console.WriteLine(@"She said, ""Don't be lazy.""");

对比两种写法(普通字符串和原义字符串),你可以看到区别。

总结一下:

  • 普通字符串里,\\ 表示一个反斜杠字符,\" 表示一个双引号字符,\t 表示制表符(还有别的,这里不作介绍);
  • 原义字符串里,"" 表示一个双引号字符。

Part 4 字符串的长度

由于字符串是不定长的,因此它拥有长度(Length)的概念。字符串可以很长,它的长度是所有字符的总数。比如说,"こんにちは、お元気ですか。" 字符串里,有 9 个假名、2 个日语汉字和 2 个标点符号,因此长度是 13。

当然了,空字符串长度为 0,这个是不用多说的。

Part 5 字符串的基本操作(超纲)

说起字符串,就不得不提一下字符串的基本操作。

不得不说,这些内容有点超纲,但是既然重要我们就得先说。超纲内容也不一定难,可能仅仅就是超前说明而已。

后续如果有超纲的内容,我们都会在标题上标记超纲一词。

5-1 获取字符串长度

我们直接使用 .Length 的语法来表示和获取字符串的具体长度:

  1. string s = "你好啊,我的小天使。";
  2. Console.WriteLine(s.Length);

这样的话,输出和显示的就是 s.Length 这个东西。s 是字符串,后直接跟上 .Length 则会得到字符串的长度。前文说过,字符串的长度就是所有字符总数,因此这个字符串是 10 个字符,因此结果是 10,输出的内容也是 10。

顺带一提,s.Length 的结果是一个数字,那么它会是什么类型的呢?对了,int 类型的。因为 int 在 C# 里起到很重要的作用,因此很多地方都会使用 int,即使我们知道长度是不可能为负数的,按道理应该用 uint,但因为为了兼容性(底层,比如说和 C 语言以及别的没有无符号数据类型的语言进行交互的时候),int 是最合适的一种类型。

5-2 获取特定位置上的字符

我们采用类似 C 语言的中括号语法,可以获取一个字符串里指定位置上的字符是什么。

  1. string s = "Hello";
  2. char c = s[3];
  3. Console.WriteLine(c);

我故意拆开写的。我们使用 [] 语法来确定和获取 s 字符串里的字符信息。中括号的数字是 n,那么就取的是第 (n + 1) 个字符。换句话说,n 最小是 0,而不是 1。

这个例子里,s[3] 就相当于取的是 s 这个字符串里的第四个字符('l'),因此,c 变量得到的结果就是 'l',最后输出的也是这个字符。

但请注意,你不能修改字符串的内容。虽然 C 语言里,你可以使用索引器来接受一个字符,以达到修改的目的,但 C# 里,整个字符串一旦声明后,就无法修改:

  1. string s = "Hello";
  2. s[3] = 't'; // Wrong.

这个写法是不允许的。编译器会直接告诉你“索引器不存在 set 方法”;这个句子换句话说就是,你没办法赋值一个新字符进去。

5-3 拼接字符串

有些时候我们可能需要拼接多个字符串,然后把这些字符串合并成一个字符串。我们需要用到的是一个叫 Concat 的方法。

  1. string s = "Hello";
  2. string t = ", ";
  3. string u = "Mike";
  4. string v = "!";
  5. string result = string.Concat(s, t, u, v);
  6. Console.WriteLine(result);

这个例子应该比较好理解。我们使用 string.Concat 方法,把所有需要拼接在一起的字符串挨个顺次写进去,用逗号隔开;然后最后得到的结果就是整体的结果了。此时,用 result 变量接受结果。显示出来就是 Hello, Mike!

5-4 查看字符串是不是包含一个字符或字符串

如果一个字符串较长,我们可能会去查找里面的字符。比如说一个电话号码(用字符串表示后),我们需要查看是不是包含 666 这个序列。那么,我们可以这么写:

  1. bool contains666 = "13666137777".Contains("666");

我们在字符串字面量(或变量)的后面追加 .Contains("666") 来表示我们需要看这个字符串是不是包括 666 这个序列。最后,这个整体得到一个 bool 的结果,赋值给 contains666 变量。

看到了吧,这里就用上了 bool 类型。

当然,小括号里不一定非得是序列,也可以是一个单独的字符:

  1. bool contains9 = "135792468".Contains('9');

这样就表示,是否字面量里包含字符 9 的信息。

5-5 查看字符串开头和结尾是不是固定的序列

如果我们要查看一个用字符串表达的电话号码里是不是以 13 开头的话,我们需要追加 .StartsWith("13") 来表示:

  1. bool startsWith13 = "13666137777".StartsWith("13");

同理,如果要看字符串结尾是不是固定的序列,则使用 .EndsWith(序列) 表示:

  1. bool endsWith13 = "13666137777".EndsWith("13");

这个依然也可以判断一个单独的字符:

  1. bool startsWith1 = "135792468".StartsWith('1');

5-6 比较字符串的具体序列是不是完全一样

如果要比较字符串是不是完全一致的话,我们用的是 string.Equals

  1. string s = "hello";
  2. string t = "Hello";
  3. bool areSame = string.Equals(s, t);

5-7 查看字符串里指定字符(或字符串序列)的具体位置

比如说,如果我想看一个身份证号里的年份信息在具体哪个位置的话,我们会直接在字符串变量(或字面量)后面追加 .IndexOf(内容)

  1. string id = "123456199601011234"; // May be invalid ID.
  2. int position = id.IndexOf("1996");
  3. Console.WriteLine("The year 1996 is of index {0}.", position);

我想,理解起来应该不难。这个结果是 6,也就是说,从第 7 个字符(从 0 开始叫第一个字符,n 就是第 (n + 1) 个字符,所以结果 6 代表第 7 个字符)开始,就可以找到 1996 这个序列。如果整个字符串没有我们要找的序列的话,结果就是 -1。为什么不是 0 呢?因为 0 表示第一个字符。

5-8 删掉一个字符或字符串

要想把字符串里的一部分完整序列去掉,我们用的是 .Remove(位置)

  1. string s = "Hello";
  2. string t = s.Remove(2);

这表示从第 3 个位置的字符开始,后面的字符我们都不要了。所以 t 变量的结果是 "He"

如果你要删掉一系列的字符的话,光有个位置还不行,还得加上删除长度:

  1. string s = "Hello";
  2. string t = s.Remove(2, 2);

.Remove(2, 2) 表示从第 3 个字符开始删,删掉 2 个字符。所以最后 t 结果是 "Heo"

5-9 替换掉一个字符或字符串

要想替换掉序列的话,我们可以用 .Replace(字符, 字符).Replace(字符串, 字符串)

  1. string s = "北京话生成器"; // May be unused.
  2. string t = "今天天气好";
  3. string u = t.Replace("今天", "今儿");

可以从例子看到,u 变量结果自然就是 "今儿天气好"

  1. string s = "北京话生成器"; // May be unused.
  2. string t = "今儿天气好";
  3. string u = t.Replace('今', '明');

这样就变成了 "明儿天气好" 了。

5-10 取字符串的一部分

要想取一部分字符,我们用 .Substring(开始位置, 取的长度) 来获取:

  1. string s = "Hello";
  2. string t = s.Substring(0, 4);

t 从第 1 个字符开始取,取 4 个字符,所以 t 结果是 "Hell"

行,那么字符串的操作我们就介绍到这里。字符串还有别的操作,但是因为用不上,或者需要高阶的知识点,所以我们就不作介绍了。

Part 6 不可变的字符串

我们看到前面的这些操作,我们发现共同点:我们发现怎么去操作字符串,最后原始的字符串都是没有变动的:即使要修改字符串,变动的结果也会通过赋值给到新的变量上去,但原始的字符串并没有变化。这是 C# 里字符串的一大特殊性质:不可变(Immutable)。

  1. string s = "Hello";
  2. string t = s.Remove(2, 2);
  3. Console.WriteLine("The variable 's' is {0}.", s);
  4. Console.WriteLine("The variable 't' is {0}.", t);

比如这个例子,我们已经删掉了 s 的字符,但代码运行起来后,我们可以看到的是,s 还是 "Hello",而 t 才是 "He"

Part 7 总结

本节我们学习了字符和字符串的内容,以及操作。再怎么说,我们都不必考虑类似 C 语言那样字符串操作的复杂细节。所以从这个角度来说,C# 还是比较简单的。

格式化输出

格式化输出(Formatted Output)是为了保证数据显示和呈现有序和控制显示规范的一种模式。在我们之前的输出内容里,我们完全不必需要显示任何额外的东西,因为本身显示出来就已经达到了目的。

那么,如果我们要显示一个表格的话,单纯的字符串就没办法控制得那么好了。下面我们来介绍这种控制输出格式的语法规则和用法。

Part 1 填充(Padding)

填充分两种:空格填充和非空格填充,所以分两部分介绍。

1-1 空格填充

我们在之前显示一个数字的时候,我们通过 Console.WriteLine 方法来显示。而如果要把数值嵌入到字符串的指定位置的时候,我们用的是一个叫做占位符的东西,写成一个大括号,里面是编号。

那么,如果我们要控制显示的数据的前后空格个数,怎么办呢?举个例子。假设我要输出表格,表格假设为两列,而第一列放数字信息,且这一列的宽度占据 8 个数字字符的位置:

  1. number | name
  2. --------+--------
  3. 1 | Sunnie
  4. 2 | Jerry
  5. 3 | Tom
  6. 4 | Foo
  7. 5 | Bar

比如说这样。为了保证显示的空格后面补充 7 个空格(使得数字一共占 8 个字符位置),我们可以这么写:

  1. Console.WriteLine("{0} | {1}", id, name);

即直接内嵌 7 个空格到字符串里,这样的话,就补充到 8 个字符位置了。这是一个好办法,但是数字可能变动到两位数,甚至三位数。单纯的空格是无法满足我们的需求的,因此我们需要一个叫做填充空格(Space Padding)的东西。

我们推广一下占位符的语法。我们在占位符的编号后添加逗号,然后增加一个整数,用来表示这个位置显示的数据,左侧或右侧补充一定数量的空格来保证整体数字的显示。

举个例子,我们使用语法 {0,8} 表示,这个占位符的编号是 0(即第一个替换数据),且它在输出的时候占据 8 个字符的位置;如果数据不够长(比如前文表格里的那个数字,只有一位数),那么就会在左侧补充空格。

如果逗号后面这个数字为负数的话,就表示在右侧补充空格来保证数据占多少个字符的位置。比如说:

  1. Console.WriteLine("{0,8}", 1);
  2. Console.WriteLine("{0,-8}", 1);

这两个数据的显示结果分别是

  1. 1 第一个显示结果
  2. 1 第二个显示结果
  3. --------
  4. 8 个字符

总结一下:

  • 正整数表示在左边补充空格达到指定宽度;
  • 负整数表示在右边补充空格达到指定宽度。

当然,这个占位符同样可以适用于浮点数类型数据。只是,浮点数的小数点、整数部分、小数部分全部都算在格式排版的范围内。比如 3.5 是由整数部分 3、小数点和小数部分 5 构成的。整数部分占一个字符、小数点占一个字符、小数部分占一个字符,所以 3.5 是算三个字符位置。因此,如果对小数使用这个占位符补充空格的话,一定要注意字符的宽度的补充空格数量。

顺带一提,如果数据超出指定宽度的范围的话,那么这个指定的宽度就没有意义了。比如说一个浮点数 123.456 一共占据 7 个字符的位置,但是用 {0,6} 作为占位符的话,输出宽度显然是不够的,所以这个占位符里的指定宽度 6 是无效的。所以这样一来,占位符就等价于 {0}

1-2 非空格填充

当然,空格可以用来填充,但是我们有时候也可以用别的符号来作为填充对象,但占位符的填充行为只能是使用空格,因此占位符只能做空格的填充。如果要别的符号,我们这里要用别的办法了。

C# 里有一个方法叫做 string.PadLeft 或者 string.PadRight。这两个方法专门用来填充指定的内容(不一定是空格)。

  1. string s = "1"; // Suppose the first output value is '1'.
  2. Console.WriteLine("{0} | {1}", s.PadRight(8, ' '), name);

我们使用 .PadLeft.PadRight 语法,在一个字符串字面量(或字符串变量)后面追加这个方法名,就表示在这个字符串的左边(或右边)补充一些符号。可以看到,例子里用的是 .PadRight(8, ' '),这个 8 和前面占位符里的指定宽度(-8)是一个逻辑(指定宽度里是可以写负整数的,但是这里用 PadRight 的时候,因为 PadLeft 是左侧补充字符,PadRight 在右侧补充字符,所以用 PadRight 就会自动在右侧补充字符,因此不需要负号);而逗号后面的这个 ' ' 是空格字符,这表示补充的时候用空格来补充。所以,上面这种写法完全和前面占位符写法 {0,-8} 是等价的。

但是,你也可以用别的填充字符。比如说:

  1. string s = "1";
  2. Console.WriteLine("{0} | {1}", s.PadLeft(8, '0'), name);

此时我们用的 PadLeft 就表示在左侧补充。逗号后面写的是 '0' 表示我们在左侧补充字符 0(注意这里用的是原封不动显示的字符 0,而不是数字 0)。填充直到占据 8 个字符位置。因此,这样的话,显示结果就是 00000001。这是一个相当方便的填充方式。

唯一一点需要注意的是,这里用的 .PadLeft.PadRight,小数点左侧都必须是字符串(变量也好,字面量也好,但不能是别的东西)。

Part 2 说明符(Specifier)

前文的排版填充空格或非空格的内容其实不太难,但是语法严谨导致很多时候初学它们会很容易记不住它们。没有关系,这毕竟不是考试,所以不要担心用不来。只要你记不住,还可以回来翻文档,也可以网上查资料。

  1. using System;
  2. class Program
  3. {
  4. static void Main()
  5. {
  6. // 格式化(Formatting)。
  7. // 说明符(Specifier)。
  8. int price = 12;
  9. Console.WriteLine("{0:C}", price);
  10. double population = 7800000000D;
  11. Console.WriteLine("{0:E}", population);
  12. }
  13. }

下面这个内容,是用来转换呈现数据模型的。在 C# 里,除了我们可以显示整数、小数以外,它还提供了显示货币、百分比等等模型。显然货币并不是一种数据类型,但用来把一个数值作为呈现对象的话,就需要这样的说明符号来表达出我到底希望它显示成啥样。

比如说,我想把 123 这个数字显示成货币表达式,而因为货币并不是数据类型,因此我们没有办法通过变量赋值和字面量的方式来把 123 给到一个“货币类型”的变量上去。因此,这里只能靠呈现来完成任务。

  1. Console.WriteLine("{0:C}", 123);

我们使用冒号 :,和一个字母 C(或小写字母 c)来表示显示数据为货币表达式,因此我这里显示的数字 123 要把它当成一个货币,然后显示出来。因此,这个代码显示的结果应当是 ¥123.00

货币显示的话,不同国家使用这个方法产生的结果是有偏差的。比如在中国,这段输出就是显示人民币的结果;而其它国家可能是别的货币符号,比如说英镑()、美元()等等。电脑会自动抉择符号选取,因此非常方便。

说明符有如下的一些(不全,用不上的就不介绍了):

说明符(大小写都可以) 说明 支持的数据类型
C 把数值显示成货币 整数、浮点数均可
D 把数值当成整数显示 整数
E 把数值当成科学计数法显示 整数、浮点数均可
N 把数值当成三位一组呈现的数值表达式显示 整数、浮点数均可
P 把数值当成百分比数值显示 整数、浮点数均可
X 把数值当成十六进制显示 整数

其中,只有 DX 只支持整数,其它的则整数、小数均可。所谓的“只支持整数”,其实就是说,占位符替换的变量(或字面量)必须得是一个整数。不论它是短整数、长整数、甚至无符号整数等等,它们起码用来表示整数,所以它们都属于整数类型。但是,浮点数就不行。如果尝试把浮点数用类似 {0:D} 的语法的话,就会直接在运行程序的时候看到严重的错误信息提示。

另外,D 后是可以跟上一个正整数的,这样就可以保证显示的数据的左侧补充一定数量的字符 0。之前我们用 PadLeft 可以给数字补充 7 个 0 达到显示 8 个字符宽度的方法,而现在我们可以直接使用这个语法来完成:

  1. Console.WriteLine("{0:D8}", 1);

后面的 1 不再需要强制使用字符串,而是一个整数,然后前面的占位符的说明符写的是 D8,就表示用整数形式显示,且填充 0 到数字之前以保证显示宽度为 8 个字符。

当然,别的说明符后也可以加数字,比如货币类型后加数字表示显示到小数位后多少位;不过这里用不上其它的,就不用多说了:说多了反而记不住。

Part 3 混用说明符和宽度指定

如果我们既想要指定宽度,又想要指定说明符的话,我们就需要都写上。因此,C# 的语法是这样的:

  1. Console.WriteLine(" Beginning Balance Ending Balance");
  2. Console.WriteLine(" {0,-28:C2}{1,14:C2}", amount1, amount2);
  3. // Displays:
  4. // Beginning Balance Ending Balance
  5. // $16,305.32 $18,794.16

比如这个例子里,{0,-28:C2} 就是在说,这里显示的数据是第一个代替数值 amount1,占 28 个字符宽度(不够长度就在右边填充空格);C2 表示显示货币表达式,且显示两位小数;同理后面的 {1,14:C2} 是一样的用法。

Part 4 显示数字的通配符

除了使用说明符 D(或小写 d)来显示数字,我们还可以使用通配符表达式来显示一个数字。这里说的数字既包含整数又包含浮点数。

如果前面的这些格式并不能满足你自定义显示一种数据模型的话,那么这一节的内容会比较灵活。

我们使用数字 0 来表示一个显示的数位(如果这个位置上没有数字,则故意显示一个字符 0 占位)。比如说

  1. Console.WriteLine("{0:000}", 12);

显然 12 不够三个数位的,所以 {0:000} 里的 000 保证显示三个数位,最高位用 0 填充,即显示结果是 012。

这个用法前面已经有一样的了。所以这里就得看你用 PadLeft 方便一些,还是 D3 方便一些,还是这个 000 方便了。三种方法都显示一样的结果,但是 PadLeft 需要接的是字符串而不是整数,稍微麻烦一点。

其它还有逗号(三位一组分隔符)、逗点组合(缩小 1000 倍数据)、井号(数字)、百分号(百分比数据)等等通配符,但这里就不说明了。

类型转换

在前面的文章里,我们只简单介绍了数据的基本用法、变量的声明以及输出格式的问题。今天我们要把数据类型完结。

  1. using System;
  2. class Program
  3. {
  4. static void Main()
  5. {
  6. // 类型转换(Type Conversion 或 Type Cast)。
  7. // 1、显式转换(强制转换,Explicitly Cast)。
  8. // 用于一个范围较大的数据类型的数值赋值给较小的的情况。
  9. decimal m = (decimal)30D;
  10. double d = 40D;
  11. int i = (int)d;
  12. //char c = (char)"1434"; // 不可以。因为字符串的长度不定,可以超过字符表达的总数量。
  13. // 2、隐式转换(Implicitly Cast)。
  14. // 用于一个较小范围的数据类型的数值赋值给较大的的情况。
  15. int i2 = 40;
  16. double d2 = i2;
  17. //string s = 'h'; // 不可以。因为处理机制不一样。
  18. // 3、字符串转其它数据类型(以及反过来)。
  19. // 一般被翻译成“解析”(Parse),即把字符串里有意义的数据部分提取出来。
  20. string str = "13.4";
  21. double targetValue = double.Parse(str);
  22. Console.WriteLine(targetValue);
  23. // 反过来。
  24. Console.WriteLine(13.56);
  25. }
  26. }

Part 1 类型需要转换吗?

数据决定了数据处理过程的模式和方法。当不同数据之间混用的时候,就必须要注意数据的转换。举个例子,intdouble 是可以互相转换的,因为我们之前介绍字面量的时候,是允许数据直接赋值的。但 C# 是严谨的,这种转换是一种叫做隐式转换(Implicit Cast 或 Implicit Conversion)的机制才成功的;另外,C# 里也提供了一些复杂类型之间的转化。举个例子,string 类型的数据(字符串)显然是不可能转换为一个整数的。因为 string 里可以放任何东西进去,所以它里面不一定只有数字字符。因此,如果一个可能的只有数字的字符串转换为数值类型的话,它们之间就需要更为复杂的转换。

今天我们就需要给大家介绍这类东西。

Part 2 显式转换(强制转换)

我们允许一种语法形式:(T)变量,来表示变量强制转换(Explicit Cast 或 Explicit Conversion)为指定的类型。举个例子:

  1. int temperature = (int)35.5;

我们使用这个语法,在数值左侧写上 (int),表示将 35.5 这个字面量转换为 int 类型的数据。如果没有这个 int 类型的强制转换的话,赋值就会失败:因为右侧的是小数,左边是整数,小数可以表示的数据的范围较大,因此赋值会失败。

稍微注意一下的是,布尔类型、字符串这样的数据类型和整数、浮点数是无法转换的(即使使用了强制转换符号):bool b = (bool)3; 以及 int i = (int)false; 类似于这样的语句一定是失败的——因为范围完全没有交集。

可以使用强制转换的类型有这样的一些:

  • float 转所有的整数数据类型;
  • double 转所有的整数数据类型;
  • doublefloat
  • decimal 转所有的整数数据类型;
  • decimalfloatdouble
  • 整数类型转范围更小的整数数据类型。

但是,如果数据超出了可转换的范围的话,就会在运行期间产生转换数据的错误。

Part 3 隐式转换

俗话说,只要数据类型不同的时候,数据赋值就需要类型转换。但有些时候,数据完全没有必要明确转换过程(比如前面这个强制转换的符号)。比如说

  1. double d = 3;

显然,3 也可以是一个 double 类型的数据,即使这里写出来的 3 这个字面量本身是 int 类型的。我们将这种(不用书写强制转换符号就允许转换)称为隐式转换。

隐式转换一般适用于所有取值范围较小转到较大的时候。只要范围更大的话,必然就可以完全包含全部较小的数据,那么写不写转换的话,C# 肯定都应该是允许的:因为 C# 知道,这样的转换必然是成功的。

当然了,你依然可以书写上强制转换的符号:

  1. double d = (double)3;

只是这个强制转换符号 (double) 完全可以不写出来。

从这个角度来说,强制转换(显式转换)和隐式转换并非是互补的两种转换模式:隐式转换只是强制转换里的一种特例。

Part 4 其它转换

显然,C# 里的字符串是不能给其它类型进行转换的。因为字符串本身是用字符拼凑起来的,而字符只是起到显示的作用:它并非真正的数值。

那么,我们想把一个只有数字字符构成的字符串,转换为一个数字的话,这怎么做呢?

4-1 字符串转其它数据类型

  1. int i = int.Parse("123");
  2. double d = double.Parse("123.45");
  3. short s = short.Parse("-6");
  4. byte b = byte.Parse("+4");
  5. bool condition = bool.Parse("false");

我随便写了这么几个。可以发现,直接将类型名称写出来,在后面追加 .Parse(字符串) 将字符串转换为数值类型。

有意思的是,Parse 还能把字符串转成布尔数据。比如一个 "false" 字符串,里面是由 f、a、l、s、e 五个字母构成的只提供显示用的字符串。

所有基本数据类型都自带 .Parse 方法。你完全可以把字符串数据(变量、字面量)带入,然后就可以得到结果。

4-2 其它数据类型转字符串

如果反过来的话,我们使用是 数值.ToString() 这样的格式进行转换。

  1. uint q = 10;
  2. string s1 = 12.125.ToString();
  3. string s2 = false.ToString();
  4. string s3 = q.ToString();
  5. string s4 = -5.ToString();

我们直接在字面量或变量后直接追加 .ToString(),就可以转化为字符串。

4-3 布尔型转整数

前面介绍了很多数据类型,但很遗憾的是,布尔型无法和整数进行转化。学过 C 语言的朋友都知道,非 0 表示为真、0 表示为假;真表示成 1、假表示为 0。怎么说,布尔和整数都是可以转换的。

C# 里,布尔型数据是无法和整数进行互相转换的。因此,我们必须转换数据的话,唯一的办法是使用这样复杂的语句:

  1. bool b = Convert.ToBoolean(3);
  2. int i = Convert.ToInt32(false);

这里的 Convert.ToBooleanConvert.ToInt32 方法就专门转化这两种数据类型。但希望你注意,要使用这两个方法,你需要和 Console.WriteLine 一样,先写上 using System; 这条引用指令。

Part 4 综合示例

我们来看一则完整的 C# 程序:

  1. using System;
  2. class Program
  3. {
  4. private static void Main()
  5. {
  6. // Variable declarations.
  7. int age1, age2, age3;
  8. string ageString1, ageString2, ageString3;
  9. string name1, name2, name3;
  10. // Read values into the program.
  11. name1 = Console.ReadLine();
  12. ageString1 = Console.ReadLine();
  13. name2 = Console.ReadLine();
  14. ageString2 = Console.ReadLine();
  15. name3 = Console.ReadLine();
  16. ageString3 = Console.ReadLine();
  17. // Conversions.
  18. age1 = int.Parse(ageString1);
  19. age2 = int.Parse(ageString2);
  20. age3 = int.Parse(ageString3);
  21. // Output values.
  22. Console.WriteLine("Name | Age");
  23. Console.WriteLine("{0,-15} | {1,3:D}", name1, age1);
  24. Console.WriteLine("{0,-15} | {1,3:D}", name2, age2);
  25. Console.WriteLine("{0,-15} | {1,3:D}", name3, age3);
  26. }
  27. }

我想,前面的内容应该能够帮助你理解这个程序。稍微注意一下的是,Console.ReadLineConsole.WriteLine 方法的执行方向是相反的:Console.ReadLine 是在屏幕上通过用户输入,来读取一个字符串。输入的内容就会自动读入到左侧变量里。

然后,变量定义是可以不用赋初始数值的,因为 Console.ReadLine 可以读取信息给变量,因此没有必要给变量添加赋值过程。

比如,我输入的一些数据读入进去了之后,结果可以是这样的:

  1. Name | Age
  2. Sunnie Shine | 25
  3. Foo Tom | 30
  4. Bar Jelly | 40

Part 5 总结

那么,至此我们就把数据类型的内容给大家介绍完毕了。前面我们学习了很多数据类型的知识点,比如说数据类型的意义、数据类型的转换、数据类型的使用方式等等。我想你对 C# 的数据类型有了一个基本的认识。没有关系,就算你没有完全记住它们。文档都是可以提供给你以后查看和参考的,所以不用担心忘记了。