C# 8.0和.NET Core 3.0高级编程 分享笔记三:控制流程和转换类型

控制流程和转换类型

本章的内容主要包括编写代码、对变量执行简单的操作、做出决策、重复执行语句块、将变量或表达式值从一种类型转换为另一种类型、处理异常以及在数值变量中检查溢出。

本章涵盖以下主题:

  • 操作变量
  • 理解选择语句
  • 理解迭代语句
  • 类型转换
  • 处理异常
  • 检查溢出

3.1操作变量

运算符可将简单的操作(如加法和乘法)应用于操作数(如变量和字面值)。它们通常返回一个新值,作为分配给变量的操作的结果。

大多数运算符是二元的,这意味着它们可以处理两个操作数,如下所示:

var resultOfOperation= onlyOperand operator;

var resultOfOperation2=operator onlyOperand;

一元运算符可用于递增操作以及检索类型或大小(以字节为单位)。如下所示:

int x=5;

int incrementedByOne=x++;

int incrementedByOneAgain=++x;

Type theTypeOfAnInteger=typeof(int);

int howManyBytesInAnInteger=sizeof(int);

三元运算符则作用于三个操作数,如下所示:

var resultOfOperation=firstOperand firstOperator

secondOperand secondOperator=thirdOperand;

3.1.1一元运算符

有两个常用的一元运算符,它们可用于递增(++)或递减(--)数字。

(1)如果完成了前面的章节,那么user文件夹中应该已经由了Code文件夹。如果没有,就创建Code文件夹。

(2)在Code文件夹中创建一个名为Chapter03的文件夹。

(3)启动Visual Studio Code,关闭任何打开的工作区或文件夹。

(4)将当前工作区保存在Chapter03文件夹中,名为Chapter03.code-workspace。

(5)创建一个Operators的新文件夹,并添加到Chapter03工作区。

(6)导航到Terminal|NEW Terminal。

(7)在终端窗口中输入命令,从而在Operators文件夹中创建新的控制台应用程序。

(8)打开Program.cs

(9)静态导入System.Console名称空间。

(10)在Main方法中,声明两个名为a和b的整型变量,将a设置为3,在将结果赋值给b的同时增加a,然后输出它们的值,如下所示:

int a=3;
int b=a++;
Console.WriteLine($"a is {a},b is {b}");

(11)在运行控制台程序应用程序之前,问自己一个问题:当输出时,b的值是多少?考虑到这一点后,运行控制台应用程序,并将预测结果与实际结果进行比较,如下所示:

a is 4, b is 3

变量b的值为3,因为++运算符在赋值之后执行;这称为后缀运算符。如果需要在赋值之前递增,那么可以使用前缀运算符。

(12)复制并粘贴语句,然后修改它们以重命名变量,并使用前缀运算符,如下所示:

 int c=3;
 int d=++c;
 Console.WriteLine($"c is {c}, d is {d}");

(13)重新运行控制台应用程序并观察结果,输出如下所示:

a is 4,b is 3

c is 4, d is 4

由于递增、递减运算符与赋值运算符在前缀和后缀方面容易让人混淆。Swift编程语言的设计者决定在Swift3 中取消对递增、递减运算符的支持。建议在C#中不要将++和--运算符与赋值运算符=结合使用。可将操作作为单独的部件执行。

3.1.2 二元算术运算符

递增和递减运算符都是一元算术运算符。其他算术运算符通常是二元的,允许对两个数字执行算术运算。

(1)将如下语句添加到Main方法的底部,对两个著型变量e和f进行声明并赋值,然后对这两个变量执行5种常见的二元算术运算:

int e=11;
int f=3;
Console.WriteLine($"e is {e}, f is {f}");
Console.WriteLine($"e+f={e+f}");
Console.WriteLine($"e-f={e-f}");
Console.WriteLine($"e*f={e*f}");
Console.WriteLine($"e/f={e/f}");
Console.WriteLine($"e%f={e%f}");

(2)重新运行控制台应用程序并观察结果,输出如下所示:

e is 11, f is 3
e+f=14
e-f=8
e*f=33
e/f=3
e%f=2

为了理解将除法/和取模%运算符应用到整数时的i情况,需要回想一下小学课程。假设有11颗糖果和3名小朋友。怎么把这些糖果分给这些小朋友呢?可以给每个小朋友分3颗糖果,还剩下两颗。剩下的这两颗糖果是模数,也称为余数。如果有12颗糖果,那么每个向朋友正好可以分得4颗。所以余数是0。

(3)添加如下语句,声明名为g的double变量并赋值,以显示整数和实数之差:

double g=11.0;
Console.WriteLine($"g is {g:N1}, f is {f}");
Console.WriteLine($"g/f={g/f}");

(4)运行控制台程序并观察结果,输出如下所示:

g is 11.0, f is 3
g/f=3.6666666666666665

如果第一个操作是浮点数,比如变量g,值为11.0,那么除法运算符也将返回一个浮点数(比如3.6666666666666665)而不是整数。

3.1.3赋值运算符

前面使用了最常用的赋值运算符=。

为了使代码更加简洁,可以把赋值运算符和算术运算符等其他运算符结合起来,如下所示:

int p=6;
Console.WriteLine(p+=3); //equivalent to p=p+3;
Console.WriteLine(p-=3); //equivalent to p=p-3;
Console.WriteLine(p*=3); //equivalent to p=p*3;
Console.WriteLine(p/=3); //equivalent to p=p/3;

3.1.4 逻辑运算符

逻辑运算符对布尔值进行操作,因此它们返回true或false。下面研究一下用于操作两个布尔值的二元逻辑操作符。

(1)创建一个新的文件夹名为BooleanOperators的控制台应用程序,并将它们添加到Chapter03工作区。记得使用Command Palette选择BooleanOperators作为当前项目。

(2)在Program.cs的Main方法中添加语句以声明两个布尔变量,它们的值分别为true和false,然后输出真值表,显示应用AND、OR和XOR(exclusive OR)逻辑运算符之后的结果,如下所示:

  			bool a=true;
            bool b=false;
            Console.WriteLine($"AND |  a  |  b  ");
            Console.WriteLine($"a   |{a&a,-5}|{a&b,-5}");
            Console.WriteLine($"b   |{b&a,-5}|{b&b,-5}");
            Console.WriteLine();
            Console.WriteLine($"OR  |  a  |  b ");
            Console.WriteLine($"a   |{a|a,-5}|{a|b,-5}");
            Console.WriteLine($"b   |{b|a,-5}|{b|b,-5}");
            Console.WriteLine();
            Console.WriteLine($"XOR |  a  |  b ");
            Console.WriteLine($"a   |{a^a,-5}|{a^b,-5}");
            Console.WriteLine($"b   |{b^a,-5}|{b^b,-5}");

对于AND逻辑运算符&,如果结果为true,那么两个操作数都必须为true。对于OR逻辑运算符|,如果结果为true,那么操作数可以为true。对于XOR逻辑运算符^,如果结果为true,那么任何一个操作数都可以为true(但不能2个都是true)。

3.1.5条件逻辑运算符

条件逻辑运算符类似于逻辑运算符,但需要使用两个符号而不是一个符号。例如,需要使用&&而不是&,以及使用||而不是|。

第四章详细介绍函数,但是现在需要简单介绍一下函数以解释条件逻辑运算符(也称为短路布尔运算符)。

函数执行语句,然后返回一个值。这个值可以是布尔值,如true,从而在布尔操作中使用。

(1)在Main方法之后声明一个函数,用于向控制台写入消息并返回true,如下所示:

 private static bool DoStuff()
        {
            Console.WriteLine("I am doing some stuff.");
            return true;
        }

(2)在Main方法的底部,对变量a和变量b以及函数的调用结果执行AND操作,如下所示:

Console.WriteLine($"a & DoStuff()={a&DoStuff()}");
Console.WriteLine($"b & DoStuff()={b&DoStuff()}");

(3)运行控制台应用程序,查看结果,注意函数被调用了两次,一次是为变量a,一次是为变量b,输出如下所示:

I am doing some stuff.
a & DoStuff()=True
I am doing some stuff.
b & DoStuff()=False

(4)将代码中的&运算符修改为&&运算符,如下所示。

Console.WriteLine($"a && DoStuff()={a&&DoStuff()}");
Console.WriteLine($"b && DoStuff()={b&&DoStuff()}");

(5)运行控制台应用程序,查看结果,注意函数在与变量a合并时会运算,但函数在与变量b合并时不会运行。因为变量b为false,结果为false,所以不需要执行函数。输出如下:

I am doing some stuff.
a && DoStuff()=True
b && DoStuff()=False。

3.1.6 按位和二元移位运算符

按位运算符影响的是数字中的位。二元移位运算符相比传统运算符能够更快地执行一些常见的算术运算。

下面研究按位和二元移位运算符。

(1)创建一个名为BitwiseAndShiftOperators的新文件夹和一个控制台应用程序项目,并将这个项目添加到工作区。

(2)想Main方法添加如下语句,声明两个整型变量,值分别为10和6,然后输出应用AND、OR和XOR(exclusive OR) 按位运算符后的结果:

		  int a=10;//0000 1010
          int b=6;//0000 0110
          Console.WriteLine($"a ={a}");
          Console.WriteLine($"b ={b}");
          Console.WriteLine($"a &b={a&b}");
          Console.WriteLine($"a |b={a|b}");
          Console.WriteLine($"a &b={a^b}");

(3)运行结果如下:

a =10
b =6
a &b=2
a |b=14
a &b=12
&按位运算,如果2个数的同一位都为1,则为1,否则为0.
|按位运算,如果2个数的同一位有一个满足为1,就为1。否则为0.
(4)向Main方法添加如下语句,应用左移运算符并输出结果:

		  //0101 0000 left-shift a by three bit columns
          Console.WriteLine($"a<<3={a<<3}");
          //multiply a by 8
          Console.WriteLine($"a *8={a*8}");
          //0000 0011 right-shift b by one bit column
          Console.WriteLine($"b>>1={b>>1}");

(5)运行并观察结果如下:

a<<3=80
a *8=80
b>>1=3

将变量a左移3位相当于乘以8,变量右移一位相当于除以2。

3.1.7 其他运算符

处理类型时,nameof和sizeof是十分常用的运算符。nameof运算符以字符串值的形式返回变量、类型或成员的短名称(没有名称空间),这在输出异常消息时非常有用。sizeof运算符返回简单类型的字节大小,这对于确定数据存储的效率很有用。

还有很多其他运算符,例如,变量与其成员之间的点称为成员访问运算符,函数或方法名末尾的圆括号称为调用运算符。

3.2理解选择语句

每个应用程序都需要能够从选项中进行选择,并沿着不同的代码路径进行分支。C#中的两个选择语句是if和switch。可以对所有代码使用if语句,但是switch语句可以在一些常见的场景中简化代码。例如当一个变量有多个值,而每个值都需要进行不同的处理时。

3.2.1使用if语句进行分支

if语句通过计算布尔表达式来确定要执行哪个分支。如果布尔表达式为true,就执行if语句块否则执行else语句块。if语句可以嵌套。

if语句也可以与其他if语句以及else if分支结合使用。

每个if语句的布尔表达式都独立于其他语句,而不像switch语句那样需要引用单个值。

下面创建一个控制台应用程序来研究if语句。

(1)创建一个文件夹和一个名为SelectionStatements的控制台应用程序项目,并将这个项目添加到工作区。

(2)在Main方法中添加如下语句,检查是否有参数传递给这个控制台应用程序。

  		if(args.Length==0)
         {
            WriteLine("There are no arguments");
         }
         else
         {
             WriteLine("There is at least one argument.")
         }

3.2.2 if语句为什么应总是使用花括号

由于每个语句块中只有一条语句,因为前面的代码可以不使用花括号来编译,但是要避免使用这种不带花括号的if语句,因为可能引入严重的缺陷。例如,苹果的IOS操作系统中就存在臭名昭著的¥gotofail缺陷。IOS6的安全套接字层(SSL) 加密代码存在缺陷,这意味着任何用户在运行IOS6设备上的网络浏览器Safari时,如果试图连接到安全的网站,比如银行,将得不到适当的安全保护,因为不小心跳过了一项重要检查。

你可以通过以下链接进一步了解这个臭名昭著的缺陷https://gotofail.com/

你不能仅仅因为可以省去花括号就真的这样做。没有了它们,代码不会“更有效率”;相反,代码的可维护性会更差,而且可能更危险。

3.2.3模式匹配与if语句

模式匹配是C#7.0及后续版本引入的一个特性。if语句可以将is关键字与局部变量声明结合起来使用,从而使代码更加安全。

(1)将如下语句添加到Main方法的末尾。这样,如果存储在变量o中的值是int类型,就将值分配给局部变量i,然后可以在if语句中使用局部变量i。这比使用变量o安全,因为可以确定i是int变量。

object o="3";
         int j=4;
         if(o is int i)
         {
            WriteLine($"{i} x {j}={i*j}");  
         }
         else
         { 
             WriteLine("o is not an int so it cannot multiply!");
         }        

(2)运行控制台应用程序并查看结果,输出如下所示:

o is not an int so it cannot multiply!

(3)删除3两边的双引号,从而使变量o中存储的值改变为int类型。

(4)重新运行控制台应用程序并查看结果,输出如下:

3 x 4=12

3.2.4使用switch语句进行分支

switch语句与if语句不同,因为前者会将单个表达式与多个可能的case语句进行比较。每个case语句都与单个表达式相关。每个case部分必须以如下内容结尾:

  • break关键字(比如下面代码中的case 1).
  • 或者goto case 关键字,(比如下面代码中的case 2).
  • 或者没有语句(比如下面代码中的case 3).

下面编写一些代码来研究switch语句。

(1)在前面编写的if语句之后,为switch语句输入一些代码。注意,第一行是一个可以跳转到的标签,第二行将生产一个随机数。switch语句将根据这个随机数的值进行分支,如下所示:

A_label:
         var number=(new Random()).Next(1,7);
         WriteLine($"My random number is {number}");
         switch(number)
         {
             case 1:
             WriteLine("One");
             break;
             case 2:
             WriteLine("Two");
             break;
             case 3:
             case 4:
             WriteLine("Three or four");
             goto case 1;
             case 5:
             System.Threading.Thread.Sleep(500);
             goto A_label;
             default:
             WriteLine("Default");
             break; 
         }     

(2)多次运行控制台应用程序,以查看对于不同的随机数会发生什么,输出如下:

PS D:\Code\Chapter03\SelectionStatements> dotnet run

o is not an int so it cannot multiply!
My random number is 1

PS D:\Code\Chapter03\SelectionStatements> dotnet run

o is not an int so it cannot multiply!
My random number is 6
Default

PS D:\Code\Chapter03\SelectionStatements> dotnet run

o is not an int so it cannot multiply!
My random number is 3
Three or four
One

PS D:\Code\Chapter03\SelectionStatements> dotnet run

o is not an int so it cannot multiply!
My random number is 4
Three or four
One

PS D:\Code\Chapter03\SelectionStatements> dotnet run

o is not an int so it cannot multiply!
My random number is 3
Three or four
One

3.2.5模式匹配与switch语句

与if语句一样,switch语句在C#7.0及更高版本中支持模式匹配。case值不再必须是字面值,而可以是模式。

下面看一个使用文件夹路径与switch语句匹配的模式示例。

(1)将以下语句添加到文件的顶部,以导入用于处理输入输出的类型:

using System.IO;

(2)在Main方法的末尾添加如下语句以声明文件的字符串路径,将其作为流打开,然后根据流的类型和功能显示消息:

 string path=@"d:\Code\Chapter03";
         Stream s=File.Open(Path.Combine(path,"file.txt"),FileMode.OpenOrCreate);
         string message=string.Empty;
         switch(s)
         {
             case FileStream writeableFile when s.CanWrite:
               message="The stream is a file stream that I can write to.";
               break;
               case FileStream readOnlyFile:
               message="The stream is a read-only file.";
               break;
               case  MemoryStream ms:
               message="The stream is a memory address.";
               break;
               default:
               message ="The stream is some other type.";
               break;
               case null:
               message="The stream is null.";
               break;
         }
         WriteLine(message);

(3)运行控制台应用程序比注意,名为s的变量被声明为Stream类型,因而可以是流的任何子类型,比如内存流或文件流。在上面这段代码中,流是使用File.Open方法创建的文件流。由于使用了FileMode,文件流是可以写的,因此得到一条描述的消息,如下所示:

The stream is a file stream that I can write to.

在.NET中,有多种类型的流,包括FileStream和MemoryStream。在C#7.0及后续版本中,代码可以基于流的字类型更简洁地进行分支,你可以声明并分配本地变量以安全地使用流。第9章将详细介绍System.IO名称空间和Stream类型。

此外,case语句可以包含when关键字以执行更具体地模式匹配。在前面步骤(2)中地第一个case 语句中,只有当流是FileStream且CanWrite属性为true时,s变量才是匹配的。

3.2.6 使用switch表达式简化switch语句

在C#8.0或更高版本中,可以使用switch表示简化switch语句。

大多数switch语句非常简单,但是它们需要大量的输入。switch表达式的涉及目的时简化需要输入的代码,同时仍然表达相同的意图。

下面实现前面使用switch语句的代码,这样就可以比较这两种风格了。

(1)在Main方法的末尾添加如下语句,根据流的类型和功能,使用switch表达式设置消息:

  message=s switch
         {
             FileStream writeableFile when s.CanWrite=>"The stream is a file stream that I can write to.",
             FileStream readOnnlyFile=>"The stream is memory address.",
             MemoryStream memoryStream=>"The stream is a memory address",
             null=> "The stream is null",
             _=>"The stream is some other type."
         };
         WriteLine(message);

区别主要是去掉了case和break关键字。下划线字符用于表示默认的返回值。

(2)运行控制台应用程序,注意结果与前面相同。

3.3理解迭代语句

当条件为真时,迭代语句会重复执行语句块,或为集合中的每一项重复执行语句块。具体使用哪种循环语句则取决于解决逻辑问题的易理解性和个人偏好。

3.3.1 while循环语句

while循环语句会对布尔表达式求值,并在布尔表达式为true时继续循环。

(1)创建一个新的文件夹和一个名为IterationStatements的控制台应用程序,并将这个项目添加到工作区。

(2)在Main方法中输入以下代码:

 while(x<10)
          {
              Console.WriteLine(x);
              x++;
          }

(3)运行控制台应用程序并查看结果,结果应该时数字0~9,如下所示:

0
1
2
3
4
5
6
7
8
9

3.3.2do循环语句

do循环语句与while循环语句类似,知识布尔表达式是在语句块的底部而不是顶部进行检查的,这意味着语句块总是至少执行一次。

(1)在Main方法的后面输入以下代码:

string password=string.Empty;
          do{
              Console.WriteLine("Enter your password:");
              password=Console.ReadLine();
          }
          while(password!="pa$$w0rd");
          Console.WriteLine("Correct!");     

(2)运行控制台应用程序,程序将重复提示你输入密码,直到输入的密码正确为止,如下所示:

Enter your password:
passwpord
Enter your password:
12345678
Enter your password:
ninja
Enter your password:
correct horse battery staple
Enter your password:
pa$$w0rd
Correct!

(3)做额外的挑战,可添加语句,使用户在显示错误消息之前只能尝试输入密码10次。

3.3.3for循环语句

for循环语句与while循环语句类似,只是更简洁。for循环语句结合了如下表达式:

  • 初始化表达式,它在循环开始时执行一次。

  • 条件表达式,它在循环开始后的每次迭代中执行,以检查循环是否应该继续。

  • 迭代器表达式,它在每个循环的底部语句中执行。

    for(int y=1;y<=10;y++)
              {
                  Console.WriteLine(y);
              }  
    

(2)运行控制台应用程序以查看结果,结果应该是数字1~10。

3.3.4foreach循环语句

foreach 循环语句与前面的三种循环语句稍有不同。foreach循环语句用于对序列(例如数组或集合)中的每一项执行语句块。序列中的每一项通常是只读的,如果在循环期间修改序列结构,例如添加或删除项,就将抛出异常。

(1)使用类型语句创建一个字符串变量数组,然后输出每个字符串变量的长度,如下所示:

 string[] names={"Adam","Barry","Charlie"};
          foreach(string name in names)
          {
              Console.WriteLine($"{name} has {name.Length} characters.");
          } 

(2)运行控制台应用程序并查看结果,输出如下所示:

Adam has 4 characters.
Barry has 5 characters.
Charlie has 7 characters.

理解foreach循环语句如何工作的

从技术上讲,foreach循环语句适用于符合以下规则的任何类型:

  • 类型必须有一个名为GetEnumerator的方法。该方法会返回一个对象。
  • 返回的这个对象必须有一个名为Current的属性和一个名为MoveNext的方法。
  • 如果有更多的项要枚举,那么MoveNext方法必须返回true,否则返回false。

有2个名为IEnumerable和IEnumerable的接口,它们正式定义了这些规则,但是从技术上将,编译器不需要类型来实现这些接口。

编译器会将前一个例子中的foreach语句转换成下面的伪代码:

 IEnumerator e=names.GetEnumerator();
          while(e.MoveNext())
          {
              string name=(String)e.Current;
              Console.WriteLine($"{name} has {name.Length} characters.");
          }

由于使用了迭代器,因此foreach循环语句中声明了变量不能用于修改当前项的值。

3.4类型转换

我们常常需要在不同类型之间转换变量的值。例如,数据通常在控制台中以文本形式输入,因此它们最初储存字符串类型的变量中,但随后需要将它们转换为日期/时间、数字或其他数据类型,这取决于它们的存储和处理方式。

有时需要在数字类型之间进行转换,比如在整数和浮点数之间进行转换,然后才执行计算。转换也称为强制类型转换,分为隐式和显式的两种。隐式的强制类型转换是自动进行的,并且是安全的,这意味着不会丢失任何信息。

显式的强制类型转换必须手动执行,因为可能会丢失一些信息,例如数字的精度。通过进行显式的强制类型转换,可以告诉C#编译器,我们理解并接受这种风险。

3.4.1隐式和显式的转换数字

将int变量隐式转换为double 变量是安全的,因为不会丢失任何信息。

(1)创建一个新的文件夹和一个名为CastingConverting的控制台应用程序项目,并将这个项目添加到工作区。

(2)在Main方法中输入如下语句以声明并赋值一个int变量和一个double变量,然后在给double 变量b赋值时,隐式地转换int变量a的值:

int a=10;
            double b=a;
            Console.WriteLine(b);
            double c=9.8;
            int d=c;
            Console.WriteLine(d);

(3)查看终端窗口,运行dotnet run命令,观察错误信息:

Program.cs(13,19): error CS0266: 无法将类型“double”隐式转换为“int”。存在一个显式转换(是否缺少强制转换?) [D:\Code\Chapter03\CastingConverting\CastingConverting.csproj

不能隐式的将double变量转换为int变量,因为这可能是不安全的,可能会丢失数据。

必须在要转换的double类型的两边使用一对圆括号,才能显式的将double变量转换为int变量,这对圆括号是强制类型转换运算符。即使这样,也必须注意小数点后的部分将自动删除,因为我们选择了执行显式的强制类型转换。

(6)修改变量d的赋值语句,如下所示:

   int d=(int)c;
            Console.WriteLine(d);

(7)运行控制台应用程序查看结果,输出如下所示:

10
9

在大整数和小整数之间转换时,必须执行类似的操作,再次提醒,可能会丢失信息,因为任何太大的值都将以意想不到的方式复制并解释二进制位。

(8)输入如下语句以声明一个64位的long变量并将它们赋值给一个32位的int变量,它们两者都使用一个可以工作的小值和一个不能工作的大值:

 long e=10;
            int f=(int)e;
            Console.WriteLine($"e is {e:N0} and f is {f:N0}");

            e=long.MaxValue;
            f=(int)e;
            Console.WriteLine($"e is {e:N0} and f is {f:N0}");

(9)运行控制台,查看输出结果如下:

e is 10 and f is 10
e is 9,223,372,036,854,775,807 and f is -1

(10)将变量e的值修改为50亿,如下所示:

  e=5_000_000_000;
            f=(int)e;
            Console.WriteLine($"e is {e:N0} and f is {f:N0}");

(11)运行结果如下:

e is 9,223,372,036,854,775,807 and f is -1
e is 5,000,000,000 and f is 705,032,704

3.4.2使用System.Convert类型进行转换

使用强制类型转换运算符的另一种方法时使用System.Convert类型。System.Convert类型可以转换为所有C#数字类型,也可以转换为布尔值、字符串、日期和时间值。

(1)在Program.cs文件的顶部静态导入System.Convert类型,如下所示:

using static System.Convert;

(2)在Main方法的底部添加如下语句以声明double变量g并为之赋值,将变量g的值转换为整数,然后将这两个值都写入控制台;

 double g=9.8;
            int h=ToInt32(g);
            WriteLine($"g is {g} and h is {h}");

(3)运行控制台应用程序并查看结果,输出如下所示:

g is 9.8 and h is 10

可以看出,double值9.8被转换为整数位10,而不是去掉小数点后的部分。

3.4.3圆整数字

如前所述,强制类型转换运算符会对实数的小数部分进行处理,而使用System.Convert类型的话,则会向上或向下圆整。然而,圆整规则是什么?

1、理解默认的圆整规则

如果小数部分是0.5或更大,则向上圆整;如果小数部分比0.5小,则向下圆整。

下面探索C#是否遵循相同的规则。

(1)在Main方法的底部添加如下语句以声明一个double数组并赋值,将其中的每个double值转换为整数,然后将结果写入控制台:

double[] doubles=new double[]{9.49,9.5,9.51,10.49,10.5,10.51};
           foreach(var n in  doubles)
           {
               Console.WriteLine($"ToInt({n}) is {ToInt32(n)}");
           }

(2)运行控制台并查看结果如下:

ToInt(9.49) is 9
ToInt(9.5) is 10
ToInt(9.51) is 10
ToInt(10.49) is 10
ToInt(10.5) is 10
ToInt(10.51) is 11

C#中的圆整规则略有不同:

  • 如果小数部分小于0.5,则向下圆整。
  • 如果小数部分大于0.5,则向上圆整。
  • 如果小数部分是0.5,那么在非小数部分是奇数的情况下向上圆整,在非小数部分是偶数的情况下向下圆整。

以上规则又称为“银行家的圆整法",以上规则之所以受青睐,是因为可通过上下圆整的交替来减少偏差。遗憾的是,其他编程语言使用的是默认的圆整规则。

2控制圆整规则

可以使用math类的Round方法来控制圆整规则。

(1)在Main方法的底部添加如下语句,使用”远离0“的圆整规则(也称为向上圆整)来圆整每个double值,然后将结果写入控制台:

  foreach(var num in doubles)
           {
               Console.WriteLine(format:"Math.Round({0},0,MidpointRounding.AwayFromZero) is {1}",arg0:nn,arg1:Math.Round(value:num,digits:0,mode:MidpointRounding.AwayFromZero));
           }

(2)运行控制台并查看结果:

Math.Round(9.49,0,MidpointRounding.AwayFromZero) is 9
Math.Round(9.5,0,MidpointRounding.AwayFromZero) is 10
Math.Round(9.51,0,MidpointRounding.AwayFromZero) is 10
Math.Round(10.49,0,MidpointRounding.AwayFromZero) is 10
Math.Round(10.5,0,MidpointRounding.AwayFromZero) is 11
Math.Round(10.51,0,MidpointRounding.AwayFromZero) is 11

3.4.4从任何类型转换为字符串

最常见的转换时从任何类型转换为字符串变量,以便输出人类可读的文本,因此所有类型都提供了从System.Object类继承的ToString方法。

ToString方法可将任何变量的当前值转换为文本教师性时。有些类型不能合理地表示为文本,因此它们返回名称空间和类型名称。

(1)在Main方法的底部输入如下语句以声明一些变量,将它们转换为字符串表示形式,并将它们写入控制台:

 int number=12;
           Console.WriteLine(number.ToString());
           
           bool boolean=true;
           Console.WriteLine(boolean.ToString());

            DateTime now=DateTime.Now;
           Console. WriteLine(now.ToString());

           object me=new object();
           Console.WriteLine(me.ToString());

(2)运行控制台并查看结果,如下所示:

12
True
2021/5/12 22:46:56
System.Object

3.4.5从二进制对象转换为字符串

对于将要存储或传输的二进制对象,例如图像或视频,又是不想发送原始位,因为不知道如何解释那些位,例如通过网络协议传输或由另一个操作系统读取及存储的二进制对象。

最安全的做法是将二进制对象转换成安全字符串,程序员称之为Base64编码。

Convert类型提供了两个方法-ToBase64String和FromBase64String,用于执行这种转换。

(1)将如下语句添加到Main方法的末尾,创建一个字节数组,在其中随机填充字节值,将格式良好的每个字节写入控制台,然后将相同 的字节转换成Base64编码并写入控制台:

 byte[] binaryObject =new byte[128];
           (new Random()).NextBytes(binaryObject);
           Console.WriteLine("Binary Object as bytes:");
           for(int index=0;index<binaryObject.Length;index++)
           {
               Console.Write($"{binaryObject[index]:X} ");
           }
           Console.WriteLine();
           string encoded=Convert.ToBase64String(binaryObject);
           Console.WriteLine($"Binary Object as Base64:{encoded}");

(2)默认情况下,如果采用十进制计数法,就会输出一个int值。可以使用:X这样的格式,通过十六进制计数法对值进行格式化。

Binary Object as bytes:
54 B3 69 CD 11 79 8E A2 1F 2A 76 61 B CC 4C AC 95 B8 82 CF 57 E2 8C C4 39 9C B9 E D8 DE D 7E BB 5E 6D 40 67 4A 5 88 7E DA A3 16 69 2D 38 15 6 F8 8D 43 1D A7 A9 72 26 A4 94 75 BF 80 C9 40 1D F7 A5 AD 5F B 45 4B B3 FA 51 7C 3E 76 5A C 63 A4 7 DF 50 CA 61 14 B4 FA 19 C2 B6 8F C8 FE 10 62 F9 71 87 8A B6 FA B3 42 8F 22 56 2D 33 B0 4A 87 12 1B 78 EA F0 52 96 91 F9 64 F2 82 95 23
Binary Object as Base64:VLNpzRF5jqIfKnZhC8xMrJW4gs9X4ozEOZy5DtjeDX67Xm1AZ0oFiH7aoxZpLTgVBviNQx2nqXImpJR1v4DJQB33pa1fC0VLs/pRfD52WgxjpAffUMphFLT6GcK2j8j+EGL5cYeKtvqzQo8iVi0zsEqHEht46vBSlpH5ZPKClSM=

3.4.6将字符串转换为数字或日期和时间

还有一种十分常见的转换是将字符串转换为数字或日期和时间。

(1)在Main方法中添加如下语句,从字符串中解析出整数以及日期和时间,然后将结果写入控制台:

int age=int.Parse("27");
           DateTime birthday=DateTime.Parse("4 July 1980");
           Console.WriteLine($"I was born {age} years ago.");
           Console.WriteLine($"My birthday is {birthday}.");
           Console.WriteLine($"My birthday is {birthday:D}");

(2)运行控制台应用程序并查看结果,输出如下所示:

I was born 27 years ago.
My birthday is 1980/7/4 0:00:00.
My birthday is 1980年7月4日

默认情况下,日期和时间输出为短日期格式。可以使用诸如D的格式代码,仅输出使用了长日期格式的日期部分。

Parse方法存在的问题是:如果字符串不能转换,该方法就会报错。只有少数类型具有Parse方法,包括所有的数字类型和DateTime。

(3)在Main方法的底部添加如下语句,尝试将一个包含字母的字符串解析为整型变量:

  nt count =int.Parse("abc");

(4)运行控制台应用程序并查看结果,输出如下:

未能找到类型或命名空间名“nt”(是否缺少 using 指令或程序集引用?) [D:\Code\Chapter03\CastingConverting\CastingConverting.csproj]

与前面的异常消息一样,你会看到堆栈跟踪。这个系列的笔记不包含堆栈跟踪,因为它们会占用太多的篇幅。

使用TryParse方法避免异常

为了避免错误,可以使用TryParse方法。TryParse方法将尝试转换输入字符串,如果可以转换,则返回true,否则返回false。

out关键字是必须的,从而允许TryParse方法在转换时设置Count变量。

(1)将int count声明替换为使用TryParse方法的语句,并要求用户输入鸡蛋的数量,如下所示:

  Console.WriteLine("How many eggs are there?");
        int count;
        string input=Console.ReadLine();
        if(int.TryParse(input,out count))
        {
            Console.WriteLine($"There are {count} eggs.");
        }
        else
        {
            Console.WriteLine($"I Could not parse the input.");
        }

(2)运行控制台程序。

(3)输入12并查看结果,输出如下所示:

How many eggs are there?
12
There are 12 eggs.

(4)再次运行控制台应用程序。

(5)输入twelve并查看结果。

How many eggs are there?
twelve
I Could not parse the input.

你还可以使用System.Convert类型将字符串转换为其他类型,但是,与Parse方法一样,如果不能进行转换,这里也会报错。

3.4.7在转换类型时处理异常

前面介绍了在转换类型时发生错误的几个常见。当发生这种情况时,就会抛出运行时异常。

可以看到,控制台应用程序的默认行为时编写关于异常的消息,包括输出中的堆栈跟踪,然后停止运行应用程序。

一定要避免编写可能会抛出异常的代码,这可以通过执行if语句检查来实现,但有时也可能做不到。在这些场景中,可以捕获异常,并以比默认行为更好的处理它们。

1.将容易出错的代码封装到try块中。

当知道某个语句可能导致错误时,就应该将其封装到try块中,例如,从文本到数字的解析可能会导致错误。只有当try块中的语句抛出异常时,才会执行catch块中的任何语句。我们不需要在catch块中做任何事。

(1)创建Main方法中添加如下语句,提示用户输入年龄,然后将年龄写入控制台,因为和前面代码定义变量名冲突,我们在变量后加a:

Console.Write("What is your age?");
string inputa=Console.ReadLine();
try{
    int agea=int.Parse(inputa);
    Console.WriteLine($"You are {agea} years old.");
}
catch
{}
Console.WriteLine("After parsing");

上面这段代码包含两条信息,分别在解析之前和解析之后显式,以帮助你清楚的理解代码中的流程。当示例代码变得更复杂时,这将特别有用。

(3)运行控制台程序。

(4)输入有效的年龄,例如47随,然后查看结果,输出如下所示:

Before parsing
What is your age?47
You are 47 years old.
After parsing

(5)再次运行控制台应用程序。

(6)输入无效的年龄,例如kermit,然后查看结果,输出如下所示:

Before parsing

What is your age?kermit

After parsing

当执行代码时,异常被捕获,不会输出默认消息和堆栈跟踪,控制台应用程序继续运行。这笔默认行为更好,但是查看发生的错误类型可能更有用。

2.捕获所有异常。

要获取可能发生的任何类型的异常信息,可以为catch块声明类型为System.Exception的变量。

(1)向catch块添加如下异常变量声明,并将有关异常的信息写入控制台:

catch(Exception ex)
{
    Console.WriteLine($"{ex.GetType()} says {ex.Message}");
}

(2)运行控制台应用程序。

(3)输入无效的年龄,例如kermit,然后查看结果,输出如下所示:

Before parsing
What is your age?kermit
System.FormatException says Input string was not in a correct format.
After parsing

3.捕获特定异常

现在,在知道发生了哪种特定类型的异常后,就可以捕获这种类型的异常,并制定想要显式给用户的消息以改进代码。

(1)退出现有的catch块,在上方为格式一次类型添加另一个新的catch块,FormatException这个catch,如下所示:

try{
    int agea=int.Parse(inputa);
    Console.WriteLine($"You are {agea} years old.");
}
catch(FormatException)
{
    Console.WriteLine($"The age you entered is not a valid number format.");
}
catch(Exception ex)
{
    Console.WriteLine($"{ex.GetType()} says {ex.Message}");
}
Console.WriteLine("After parsing");

(2)运行控制台应用程序。

(3)输入无效的年龄,例如Kermit,然后查看结果,输出如下:

Before parsing
What is your age?kermit
The age you entered is not a valid number format.
After parsing

之前所保留的前面的那个catch块,时因为可能会发生其他类型的异常。

(4)运行控制台程序

(5)输入一个对于整数来说过大的数字,例如9876543210,查看结果:

Before parsing
What is your age?9876543210
System.OverflowException says Value was either too large or too small for an Int32.
After parsing

你可以为这种类型的异常添加另一个catch块。

(6)退出现有的catch块,为溢出异常类型添加新的catch块,如下面显式的代码所示:

catch(OverflowException)
{
    Console.WriteLine("Your age is a valid number format but it is either too big or small.");
}

(7)运行控制台程序

(8)输入一个对于整数来说过大的数字,然后查看结果:

Before parsing
What is your age?9876543210
Your age is a valid number format but it is either too big or small.
After parsing

异常捕获的顺序很重要,正确的顺序与异常类型的继承层次结构有关。第五章将介绍继承。但时,你不用太担心-如果以错误的顺序得到异常,编译器会报错。

3.4.8检查溢出

如前所述,在数字类之间进行强制类型转换时,可能会丢失信息,例如在将long变量强制转换为int变量时。如果类型中存储的值太大,就会溢出。

1.使用checked语句抛出溢出异常

checked语句告诉.NET,要在发生溢出时抛出异常,而不是沉默不语。

下面我们把int变量x的初值设置为int类型所能存储的最大值减1。然后,将变量x递增几次,每次递增是都输出值。一旦超出最大值,就会溢出到最小值,并从那里继续递增。

(1)创建一个文件夹和一个名为CheckingForOverflow的控制台应用程序项目,并将这个项目添加到工作区。

(2)在M爱你方法中输入如下语句,声明int变量x并赋值为int类型所能存储的最大值减1,然后将x递增三次,并且每次递增时都把值写入控制台:

 int x=int.MaxValue-1;
            Console.WriteLine($"Initial value: {x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");

(3)运行控制台应用程序并检查结果,输出如下所示:

Initial value: 2147483646
After incrementing:2147483647
After incrementing:-2147483648
After incrementing:-2147483647

(4)现在,使用checked块封装语句,编译器会警告出现了溢出。

checked
            {
            int x=int.MaxValue-1;
            Console.WriteLine($"Initial value: {x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            }

(5)运行控制台应用程序并查看结果,输出如下:

Initial value: 2147483646
After incrementing:2147483647
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
at CheckingForOverflow.Program.Main(String[] args) in D:\Code\Chapter03\CheckingForOverflow\Program.cs:line 15

(6)与任何其他异常一样,应该将这些语句封装在try块中,并未用户显式更友好的错误信息,如下所示:

 try{
            checked
            {
            int x=int.MaxValue-1;
            Console.WriteLine($"Initial value: {x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            x++;
            Console.WriteLine($"After incrementing:{x}");
            }
            }
            catch(OverflowException)
            {
                Console.WriteLine("The code overflowed but I caught the exception.");
            }

(7)运行控制台程序,输出如下:

Initial value: 2147483646
After incrementing:2147483647
The code overflowed but I caught the exception.

2.使用unchecked语句禁用编译时检查

unchecked语句能够关闭由编译器在一段代码内执行的溢出检查。

(1)在前面语句的末尾输入下面的语句。编译器不会编译这条语句,因为编译器知道会发生溢出:

    int y=int.MaxValue+1;

(2)使用控制台运行,并观察输出:

Program.cs(26,19): error CS0220: 在 checked 模式下,运算在编译时溢出 [D:\Code\Chapter03\CheckingForOverflow\CheckingForOverflow.csproj]

(3)要禁用编译时检查,请将语句封装在unchecked块中,将y的值写入控制台,递减y,然后重复,如下所示:

 unchecked{
                int y=int.MaxValue+1;
                Console.WriteLine("Initial value:{y}");
                y--;
                Console.WriteLine($"After decrementing:{y}");
                y--;
                Console.WriteLine($"After decrementing: {y}");
            }

(4)运行控制台应用程序并查看结果,输出如下:

Initial value:-2147483648
After decrementing:2147483647
After decrementing: 2147483646

当然,我们很少希望像这样显式的 关闭编译时检查,从而允许发生溢出。但是,也许在某些场景中,我们需要显式的关闭溢出检查。

posted @ 2021-05-13 23:21  杜文龙  阅读(196)  评论(0编辑  收藏  举报