C# 学习笔记2-控制流与类型转换

控制流与类型转换

  • 关于变量的简单操作
  • 判断
  • 循环
  • 类型转换
  • 异常处理
  • 检查数字类型的溢出

变量操作

一元运算符

Unary operators

x++++xx----x

这些运算符同 C++。

postfix operator 后置运算符

还有

typeof(int)sizeof(int)

二元运算符

Binary arithmetic operators

无非是:+-*/%

modulus 模

remainder 余数

赋值运算符

也可以向 C++ 那样:+=-=*=/=

逻辑运算符

&|^ ,分别代表 与、或、异或。

与 C++ 一样,这里看到是针对布尔值的,后面看到也可以用于整数进行位级运算。

条件逻辑运算符

Conditional logical operators

类似于逻辑运算符,但是连续使用两个符号而不是一个:&&||。不同的是这个条件逻辑运算符像 C++ 那样有短路的能力。

private static bool func()
{
    return true;
}
...
bool a = false;
bool b = a && func(); // func 不会执行
bool c = true || func(); // func 不会执行

位级和二进制移位运算符

&|^ ,分别代表用于整数的位级 与、或、异或。

<<>> 分别代表左移和右移。

和 C/C++ 一样。

杂项运算符

Miscellaneous operators

nameofsizeof 都是可用于类型的运算符。

  • nameof 返回一个字符串,表示变量、类型或类成员的简称(不含名称空间),可用于输出异常信息。
  • sizeof 返回一个简单类型的大小(单位为字节)

还有其他,比如 . 表示成员访问运算符;() 表示调用运算符

选择语句

Understanding selection statements

主要是 if switch

if 语句

通过评估一个布尔表达式来选择分支。

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 关键字与声明局部变量结合使用,以使代码更安全。

例如:

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 下面要不一条语句也没有,要么必须用 breakgoto 结束。

例子:

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:

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 表达式将上一个例子简化:

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);

主要的区别是去掉了casebreak关键字。下划线(underscore)字符用于表示默认返回值。

迭代语句

iteration statements

除了 foreach ,其他语句 whilefordo-while 和 C 一样。

foreach 类似于 C++ 的范围 for 循环。

使用 while 语句进行循环

例如:

int x = 0;
while (x < 10)
{
    WriteLine(x);
    x++;
}

do-while 语句

string password = string.Empty;
do
{
	Write("Enter your password: ");
	password = ReadLine();
} 
while (password != "Pa$$w0rd");

WriteLine("Correct!");

for 循环

for (int i = 0; i < 10; i++)
{
    WriteLine(i);
}

foreach 语句

用于遍历序列,比如 array 或 collection,每一项通常是只读的。如果在迭代的过程中原序列结构被析构,比如删除或增加一项,将会抛出异常。

举例:

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

foreach 内部是如何工作的呢?

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

  1. 该类型必须有一个名为 GetEnumerator 的方法,该方法返回一个对象。
  2. 返回的对象必须具有名为 Current 的属性和名为 MoveNext 的方法。
  3. 如果还有更多项可供枚举,则 MoveNext 方法必须返回 true;如果没有更多项目,则必须返回 false

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

编译器将前面示例中的 foreach 语句转换为类似于以下伪代码的内容:

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

隐式转换:

int a = 10;
double b = a; //int可以安全地转换为double
WriteLine(b);

错误地隐式转换:

double c = 9.8;
int d = c; //编译器报错 无法隐式转换
WriteLine(d);

我们需要将 double 变量显式转换为 int 变量。用 () 来括住要转换到的目的类型。称之为转换运算符(cast operator)。注意,将一个 double 转换成 int 后,小数点后的部分将会被不加警告地裁剪掉。如:

int d = (int)c;

将一个将较大的整数类型转换为较小的整数类型时,也需要使用显式类型转换。注意可能会丢失信息,因为 bit 复制后可能会以意想不到的方式被解释。例如:

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 类。

using staic System.Convert;

具体使用:

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

输出结果:

g is 9.8 and h is 10

castingconverting 之间的一个区别是,转换将双精度值 9.8 舍入到 `10,而不是修剪小数点后的部分。

舍入数字

Rounding numbers

我们知道 converting 会进行舍入,那舍入的规则是?

默认舍入规则

即 偶数舍入(这和 CSAPP 中讲浮点数时的舍入规则一样)。

例子:

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)}");
}

输出:

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 函数类控制舍入规则

例子:

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));
}

运行结果:

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 方法将任何变量的当前值转换为文本表示形式。有些类型不能合理地表示为文本,因此它们返回其名称空间和类型名称。

例子:

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 的相同字节写入控制台,如下所示代码:

// 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

例子:

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 去解析一个内容非数字的字符串,则会异常:

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 方法在转换工作时设置计数变量。

例子:

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

例子:

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 类型的变量。如:

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 块来捕获多种异常,并为每种异常写处理代码,

例子:

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 当一个溢出发生时抛出一个异常,而不是保持静默。

先给出一个会发生溢出的例子:

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 包住这一块代码,来让溢出时抛出异常:

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 来捕获溢出异常:

try
{
	// 之前那些代码用来抛出溢出异常
}
catch (OverflowException)
{
    WriteLine("The code overflowed but I caught the exception.");
}
使用 unchecked 语句不让编译器进行溢出检查

Disabling compiler overflow checks with the unchecked statement

相关关键字 unchecked。该关键字关闭编译器在代码块内执行的溢出检查。

例子:

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,则第一句就会提示异常:

int y = int.MaxValue + 1;
//在 checked 模式下,运算在编译时溢出

当然,您很少会想要显式关闭这样的检查,因为它允许发生溢出。但是,也许您可以想象一个您可能想要这种行为的场景

你可能感兴趣的:(c#,学习,笔记)