控制流与类型转换
- 关于变量的简单操作
- 判断
- 循环
- 类型转换
- 异常处理
- 检查数字类型的溢出
变量操作
一元运算符
Unary operators
x++
,++x
,x--
,--x
。
这些运算符同 C++。
postfix operator 后置运算符
还有
typeof(int)
,sizeof(int)
。
二元运算符
Binary arithmetic operators
无非是:+
、-
、*
、/
、%
modulus 模
remainder 余数
赋值运算符
也可以向 C++ 那样:+=
、-=
、*=
、/=
、
逻辑运算符
&
、|
、^
,分别代表 与、或、异或。
与 C++ 一样,这里看到是针对布尔值的,后面看到也可以用于整数进行位级运算。
条件逻辑运算符
Conditional logical operators
类似于逻辑运算符,但是连续使用两个符号而不是一个:&&
、||
。不同的是这个条件逻辑运算符像 C++ 那样有短路的能力。
c#
private static bool func()
{
return true;
}
...
bool a = false;
bool b = a && func(); // func 不会执行
bool c = true || func(); // func 不会执行
位级和二进制移位运算符
&
、|
、^
,分别代表用于整数的位级 与、或、异或。
<<
、>>
分别代表左移和右移。
和 C/C++ 一样。
杂项运算符
Miscellaneous operators
nameof
和 sizeof
都是可用于类型的运算符。
nameof
返回一个字符串,表示变量、类型或类成员的简称(不含名称空间),可用于输出异常信息。sizeof
返回一个简单类型的大小(单位为字节)
还有其他,比如 .
表示成员访问运算符;()
表示调用运算符
选择语句
Understanding selection statements
主要是 if
和 switch
。
if
语句
通过评估一个布尔表达式来选择分支。
c++
if (expression1)
{
// runs if expression1 is true
}
else if (expression2)
{
// runs if expression1 is false and expression2 if true
}
else
{
// runs if all expressions are false
}
为什么你应该永远在 if 语句中使用大括号?
就像 C/C++ 一样,C# 中 if
语句当大括号中的语句只有一个时可以省略大括号,但是无论是哪个语言。都不建议省略!
使用 if
进行模式匹配
C#7.0 引入了模式匹配(pattern matching)。if
语句可以通过 is
关键字与声明局部变量结合使用,以使代码更安全。
例如:
c#
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!");
}
运行结果:
o is not an int so it cannot multiply!
表示 o 不是 int 类型,所以 if 判断为假。
我们把第一行的字面值去掉双引号,即改为:object o = "3";
,重新运行结果为:
3 x 4 = 12
...
使用 switch
语句
使用 case
表达每种情况,每个 case
语句与一个表达式有关,每个 case
节必须以以下方式结束:
break
关键字goto case
/goto
关键字- 没有语句
注意,这与 C 不同,C# 的 case
下面要不一条语句也没有,要么必须用 break
或 goto
结束。
例子:
c#
A_label:
var number = (new Random()).Next(1,7);
WriteLine($"My random number is {number}");
switch (number)
{
case 1:
WriteLine("One");
break; //结束 switch 语句
case 2:
WriteLine("Two");
goto case 1;
case 3:
WriteLine("Three");
case 4:
WriteLine("Three or four");
goto case 1;
case 5:
// go to sleep for half a second
System.Threading.Thread.Sleep(500);
goto A_label;
default:
WriteLine("Default");
break;
} // end of switch statement
...
无论
default
的位置在哪,它都会被最后考虑
可以使用goto
关键字跳转到另一个案例或标签。goto
关键字不被大多数程序员所接受,但在某些情况下可能是代码逻辑的一个很好的解决方案。但是,您应该谨慎使用它。
switch 语句的模式匹配
Pattern matching with the switch statement
与 if 语句一样,switch
语句在 C# 7.0 及更高版本中支持模式匹配。 case 值不再需要是文字值。它们可以是模式。
一个例子,更具对象的类型和能力设置不同的 message:
c#
using System.IO;
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 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: // 无论 default 的位置在哪,它都会被最后考虑
message = "The stream is some other type.";
break;
case null:
message = "The stream is null.";
break;
}
WriteLine(message);
...
在 .NET 中,Stream
有许多子类型(派生类型?),比如 FileStream
和 MemoryStream
。在 C#7.0 及之后,我们的代码可以更加简洁与安全地对类型进行分支,并用一个临时变量来安全地使用它。
另外,case
语句可以包含一个 when
关键字来进行更多的模式匹配,如上面例子的第一个 case
。
使用 switch 表达式简化 switch 语句
Simplifying switch statements with switch expressions
在C# 8.0或更高版本中,可以使用switch
表达式简化switch
语句。
大多数 switch
语句都非常简单,但需要大量输入。 switch
表达式旨在简化您需要键入的代码,同时仍表达相同的意图。
用 switch
表达式将上一个例子简化:
c#
message = s switch
{
FileStream writeableFile when s.CanWrite
=> "The stream is a file that I can write to.",
FileStream readOnlyFile
=> "The stream is a read-only file.",
MemoryStream ms
=> "The stream is a memory address.",
null
=> "The stream is null.",
_
=> "The stream is some other type."
};
WriteLine(message);
...
主要的区别是去掉了case
和break
关键字。下划线(underscore)字符用于表示默认返回值。
迭代语句
iteration statements
除了 foreach
,其他语句 while
、for
、do-while
和 C 一样。
foreach
类似于 C++ 的范围 for 循环。
使用 while
语句进行循环
例如:
C#
int x = 0;
while (x < 10)
{
WriteLine(x);
x++;
}
...
do
-while
语句
C#
string password = string.Empty;
do
{
Write("Enter your password: ");
password = ReadLine();
}
while (password != "Pa$$w0rd");
WriteLine("Correct!");
...
for
循环
c#
for (int i = 0; i < 10; i++)
{
WriteLine(i);
}
...
foreach
语句
用于遍历序列,比如 array 或 collection,每一项通常是只读的。如果在迭代的过程中原序列结构被析构,比如删除或增加一项,将会抛出异常。
举例:
c#
string[] names = { "Adam", "Barry", "Charlie" };
foreach (string name in names)
{
WriteLine($"{name} has {name.Length} characters.");
}
...
foreach 内部是如何工作的呢?
从技术上讲,foreach
语句适用于遵循以下规则的任何类型:
- 该类型必须有一个名为
GetEnumerator
的方法,该方法返回一个对象。 - 返回的对象必须具有名为
Current
的属性和名为MoveNext
的方法。 - 如果还有更多项可供枚举,则
MoveNext
方法必须返回true
;如果没有更多项目,则必须返回false
。
有名为 IEnumerable
和 IEnumerable<T>
的接口正式定义了这些规则,但从技术上讲,编译器不需要类型来实现这些接口。
编译器将前面示例中的 foreach
语句转换为类似于以下伪代码的内容:
c#
IEnumerator e = names.GetEnumerator();
while (e.MoveNext())
{
string name = (string)e.Current; // Current 是只读的!
WriteLine($"{name} has {name.Length} characters.");
}
由于使用了迭代器,foreach
语句中声明的变量不能用来修改当前项的值。
类型转换
Casting and converting between types
Converting is also known as casting,他有两种类型:隐式 implicit 的和显式 explicit 的。
-
隐式强制转换是自动发生的,而且是安全的,这意味着您不会丢失任何信息。
-
显式强制转换必须手动执行,因为它可能会丢失信息,例如,数字的精度。通过显式强制转换,您告诉c#编译器您理解并接受风险。
Casting and converting 的区别见 "使用 System.Convert
类型进行 converting " 一节。其实就是 converting 在浮点数转换为整数时会舍入,而 casting 直接舍去小数部分。
隐式和显式的数字 casting
隐式转换:
c#
int a = 10;
double b = a; //int可以安全地转换为double
WriteLine(b);
错误地隐式转换:
c#
double c = 9.8;
int d = c; //编译器报错 无法隐式转换
WriteLine(d);
我们需要将 double
变量显式转换为 int 变量。用 ()
来括住要转换到的目的类型。称之为转换运算符(cast operator
)。注意,将一个 double
转换成 int
后,小数点后的部分将会被不加警告地裁剪掉。如:
c#
int d = (int)c;
将一个将较大的整数类型转换为较小的整数类型时,也需要使用显式类型转换。注意可能会丢失信息,因为 bit 复制后可能会以意想不到的方式被解释。例如:
c#
long e = 10;
int f = (int)e;
WriteLine($"e is {e:N0} and f is {f:N0}");
e = long.MaxValue;
f = (int)e;
WriteLine($"e is {e:N0} and f is {f:N0}");
e = 5_000_000_000;
f = (int)e;
WriteLine($"e is {e:N0} and f is {f:N0}");
运行结果:
e is 10 and f is 10
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
...
可以想象以下补码编码,从大整型到小整型显式转换会截断字节。
使用 System.Convert
类型进行 converting
使用强制转换运算符的另一种方法是使用 System.Convert
类型。 System.Convert
类型可以与所有 C# 数字类型以及布尔值、字符串以及日期和时间值进行相互转换。
需要引入 System.Convert
类。
c#
using staic System.Convert;
具体使用:
c#
double g = 9.8;
int h = ToInt32(g);
WriteLine($"g is {g} and h is {h}")
输出结果:
c#
g is 9.8 and h is 10
casting 和 converting 之间的一个区别是,转换将双精度值 9.8
舍入到 `10,而不是修剪小数点后的部分。
舍入数字
Rounding numbers
我们知道 converting 会进行舍入,那舍入的规则是?
默认舍入规则
即 偶数舍入(这和 CSAPP 中讲浮点数时的舍入规则一样)。
例子:
c#
double[] doubles = new[]{ 9.49, 9.5, 9.51, 10.49, 10.5, 10.51 };
foreach (double n in doubles)
{
WriteLine($"ToInt({n}) is {ToInt32(n)}");
}
输出:
c#
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
这和我们常见的四舍五入不同:
- 如果小数部分小于中点 0.5,则始终向下舍入。
- 如果小数部分大于中点0.5,则始终四舍五入。
- 小数部分为中点0.5且非小数部分为奇数时向上舍入,非小数部分为偶数时向下舍入
此规则称为"银行家舍入"(Banker's Rounding),它是首选规则,因为它通过交替向上或向下舍入来减少统计偏差。
遗憾的是,其他语言(例如 JavaScript)使用的是小学的四舍五入规则。
控制舍入规则
可以使用 Math 类的 Round 函数类控制舍入规则
例子:
c#
double[] doubles = new[]{ 9.49, 9.5, 9.51, 10.49, 10.5, 10.51 };
foreach (double n in doubles)
{
WriteLine(
format:"Math.Round({0}, 0, MidpointRounding.AwayFromZero) is {1}",
arg0: n,
arg1: Math.Round(value: n,
digits: 0,
mode: MidpointRounding.AwayFromZero));
}
运行结果:
c#
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
...
可见 MidpointRounding.AwayFromZero
就是小学的四舍五入规则。
更多控制舍入规则的资料:https://docs.microsoft.com/en-us/dotnet/api/system.math.round
良好实践:我们在用每个编程语言时都要注意它的舍入规则。
将任意类型转换为 string
所有继承自 System.object
类型都有 ToString
成员。
ToString
方法将任何变量的当前值转换为文本表示形式。有些类型不能合理地表示为文本,因此它们返回其名称空间和类型名称。
例子:
c#
int number = 12;
WriteLine(number.ToString());
bool boolean = true;
WriteLine(boolean.ToString());
DateTime now = DateTime.Now;
WriteLine(now.ToString());
object me = new object();
WriteLine(me.ToString());
运行结果:
12
True
2024/1/10 18:59:15
System.Object
将一个二进制 object 转换为 string
Converting from a binary object to a string
当您想要存储或传输二进制对象(例如图像或视频)时,有时您不想发送原始位,因为您不知道这些位可能如何被误解,例如被网络协议误解传输它们或正在读取存储二进制对象的另一个操作系统。
最安全的做法是将二进制对象转换为安全字符的字符串。序员称之为 Base64 编码。
Convert
类型有一对方法:ToBase64String
和 FromBase64String
,可以为您执行此转换。
例子:创建一个随机填充字节值的字节数组,将每个字节写入格式良好的控制台,然后将转换为 Base64 的相同字节写入控制台,如下所示代码:
c#
// allocate array of 128 bytes
byte[] binaryObject = new byte[128];
// 用随机字节填充数组
(new Random()).NextBytes(binaryObject);
WriteLine("Binary Object as bytes:");
for(int index = 0; index < binaryObject.Length; index++)
{
Write($"{binaryObject[index]:X} ");
}
WriteLine();
// 转换为 Base64 字符串并输出为文本
string encoded = Convert.ToBase64String(binaryObject);
WriteLine($"Binary Object as Base64: {encoded}");
注意这里使用 x
作为格式化字符串,表示十六进制展示。
运行结果:
Binary Object as bytes:
F 6D FA 40 C5 A9 E9 F9 E9 FD 43 D6 5B E4 AC 94 D5 B8 BA D2 73 35 E EA 69 13 4B C7 D7 F6 D7 93 79 1D 35 AA 28 EE 50 43 2 E9 5D E4 70 CC 4A 2B 70 6A 1A 64 B3 14 4 27 9A F7 98 28 9 FD F1 10 E5 95 D2 14 5D 42 89 DE 1D 27 40 B6 EA AC B8 2F 34 E8 41 73 B 11 21 9D 95 F3 21 BE F7 A9 79 6C 82 59 D 34 11 92 C9 D1 8B B8 81 FD 30 27 AF 72 F8 1E 23 2B 7D A3 59 17 E 65 C8 5F D5 B5 28 BF
Binary Object as Base64: D236QMWp6fnp/UPWW+SslNW4utJzNQ7qaRNLx9f215N5HTWqKO5QQwLpXeRwzEorcGoaZLMUBCea95goCf3xEOWV0hRdQoneHSdAtuqsuC806EFzCxEhnZXzIb73qXlsglkNNBGSydGLuIH9MCevcvgeIyt9o1kXDmXIX9W1KL8=
从字符串解析数字和日期
ToString
的对立面是 Parse
。只有少数类型有 Parse
方法,包括所有数字类型和 DateTime
。
例子:
c#
int age = int.Parse("27");
DateTime birthday = DateTime.Parse("4 July 1980");
WriteLine($"I was born {age} years ago.");
WriteLine($"My birthday is {birthday}.");
WriteLine($"My birthday is {birthday:D}.");
birthday = DateTime.Parse("2023 7 9");
WriteLine($"My birthday is {birthday}.");
WriteLine($"My birthday is {birthday:D}.");
输出:
I was born 27 years ago.
My birthday is 1996/7/4 0:00:00.
My birthday is 1996年7月4日.
My birthday is 2023/7/9 0:00:00.
My birthday is 2023年7月9日.
默认情况下,日期和时间值以短日期和时间格式输出。您可以使用 D 等格式代码以长日期格式仅输出日期部分。
(这部分和书上不太一样,书上的结果是。注释书上没有第二次日期 "2023 7 9"
的解析)
I was born 27 years ago.
My birthday is 04/07/1980 00:00:00.
My birthday is 04 July 1980.
...
关于日期格式化的更多资料:https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings.
假如让 int 去解析一个内容非数字的字符串,则会异常:
c#
int count = int.Parse("abc");
异常:
Unhandled exception. System.FormatException: The input string 'abc' was not in a correct format.
使用 TryParse
方法避免异常
TryParse
尝试转换输入字符串,如果可以转换则返回 true
,如果不能转换则返回 false
。需要 out
关键字来允许 TryParse
方法在转换工作时设置计数变量。
例子:
c#
Write("How many eggs are there? ");
int count;
string input = Console.ReadLine();
if (int.TryParse(input, out count))
{
WriteLine($"There are {count} eggs.");
}
else
{
WriteLine("I could not parse the input.");
}
运行结果:
How many eggs are there? 42
There are 42 eggs.
输入非数字内容试试:
How many eggs are there? abc
I could not parse the input.
...
您还可以使用 System.Convert
类型的方法将字符串值转换为其他类型;但是,与 Parse
方法一样,如果无法转换,则会出现错误。
处理转换类型时的异常
Handling exceptions when converting types
您已经见过几种在转换类型时发生错误的情况。当发生这种情况时,我们说抛出了运行时异常。
良好实践 :尽可能避免编写会引发异常的代码,也许可以通过执行
if
语句检查来实现,但有时却做不到。在这些情况下,您可以捕获异常并以比默认行为更好的方式处理它。
将容易出错的代码包装在 try
块中
Wrapping error-prone code in a try block
例子:
c#
WriteLine("Before parsing");
Write("What is your age?");
string input = Console.ReadLine() ?? "";
try
{
int age = int.Parse(input);
WriteLine($"You are {age} years old.");
}
catch
{
}
WriteLine("After parsing");
仅当 try 块中的语句抛出异常时,catch 块中的任何语句才会执行。我们没有在 catch 块内执行任何操作。
捕获所有异常
为了捕获所有可能发生异常的类型,可以在 catch
块中声明一个 System.Exception
类型的变量。如:
c#
catch(Exception ex)
{
WriteLine($"{ex.GetType()} says {ex.Message}");
}
我们将它对应的修改到上面的例子中,运行结果:
Before parsing
What is your age?abc
System.FormatException says The input string 'abc' was not in a correct format.
After parsing
捕获特定异常
我们可以多次使用 catch 块来捕获多种异常,并为每种异常写处理代码,
例子:
c#
catch(FormatException)
{
WriteLine("The age you entered is not a valid number format.");
}
catch (OverflowException)
{
WriteLine("Your age is a valid number format but it is either too big or small.");
}
catch (FormatException)
{
WriteLine("The age you entered is not a valid number format.");
}
catch(Exception ex)
{
WriteLine($"{ex.GetType()} says {ex.Message}");
}
这时,捕获异常的顺序很重要。正确的顺序与异常类型的继承层次结构有关。不过,不用太担心这一点------如果您以错误的顺序收到异常,编译器会给您生成错误。
假如,catch(Exception ex)
在 catch (FormatException)
前面,编译器就会报错。因为前者包含后者,这种错误的顺序会使得后者异常永远不会被捕获。
检查溢出
Checking for overflow
之前我们注意到,较大的整型转换为较小的整型时可能产生溢出。
使用checked语句抛出溢出异常
Throwing overflow exceptions with the checked statement
checked
语句告诉 .NET 当一个溢出发生时抛出一个异常,而不是保持静默。
先给出一个会发生溢出的例子:
c#
int x = int.MaxValue - 1;
WriteLine($"Initial value: {x}");
x++;
WriteLine($"After incrementing: {x}");
x++;
WriteLine($"After incrementing: {x}");
x++;
WriteLine($"After incrementing: {x}");
运行输出:
Initial value: 2147483646
After incrementing: 2147483647
After incrementing: -2147483648
After incrementing: -2147483647
现在我们用 checked 包住这一块代码,来让溢出时抛出异常:
c#
checked
{
int x = int.MaxValue - 1;
WriteLine($"Initial value: {x}");
x++;
WriteLine($"After incrementing: {x}");
x++;
WriteLine($"After incrementing: {x}");
x++;
WriteLine($"After incrementing: {x}");
}
运行结果:
Initial value: 2147483646
After incrementing: 2147483647
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
我们可以用 try-catch 来捕获溢出异常:
c#
try
{
// 之前那些代码用来抛出溢出异常
}
catch (OverflowException)
{
WriteLine("The code overflowed but I caught the exception.");
}
使用 unchecked
语句不让编译器进行溢出检查
Disabling compiler overflow checks with the unchecked statement
相关关键字 unchecked
。该关键字关闭编译器在代码块内执行的溢出检查。
例子:
c#
unchecked
{
int y = int.MaxValue + 1;
WriteLine($"Initial value: {y}");
y--;
WriteLine($"After decrementing: {y}");
y--;
WriteLine($"After decrementing: {y}");
}
运行结果:
Initial value: -2147483648
After decrementing: 2147483647
After decrementing: 2147483646
如果没有 unchecked
,则第一句就会提示异常:
c#
int y = int.MaxValue + 1;
//在 checked 模式下,运算在编译时溢出
当然,您很少会想要显式关闭这样的检查,因为它允许发生溢出。但是,也许您可以想象一个您可能想要这种行为的场景