C# 数据类型与流程控制
在哪里写代码?
在学习每一门语言时,我们最应该知道的就是该种编程语言,应该在哪个位置写代码才会执行,怎么才能打印出内容?
入口函数
入口函数 是程序执行的起始点,是程序开始执行时调用的第一个函数。在不同的编程语言中,入口函数的定义和名称可能有所不同。
在 C# 中,入口函数是 Main
方法。每个 C# 控制台应用程序或 Windows 应用程序至少包含一个 Main
方法。以下是一个简单的 C# 程序示例:
1 | using System; |
简单来说,就是把代码写在上面代码中static void Main(string[] args){}
的两个大括号之间就可以运行,写好代码点击菜单栏的绿色三角,就会弹出黑色窗口,里面写着Hello, World
。
控制台输出语句
比如 Java 的 System.out.Println()
,JavaScript 的console.log()
,Golang 的fmt.Println()
,Python 的pirnt()
等。通过这些控制台输出语句,我们可以方便的去确定程序的运行结果。
而我们的 C#
使用的则是Console.WriteLine()
,全称System.Console.WriteLine()
,上面的代码中就有。
控制台输入语句
C#
使用的是Console.ReadLine()
,用于读取一行内容,返回值为字符串格式。
不让控制台退出
在第一次运行代码时,我们发现,点击运行,一个黑窗口一闪而过,还没看到内容就消失了
这时候,我们只用在Main方法最后一行写上Console.ReadKey()
,再次执行代码,黑窗口显示完所有信息后会等待读取输入的按键,就不会立刻消失了。
命名空间
中空间和可维护性。一个文件中可以定义多个命名空间,而且命名空间也可以嵌套定义。
在我们使用 Visual Studio 创建一个项目后,映入我们眼帘的是这样的代码:
1 | using System; |
其中,前 6 行代码都以using
开头,表示引用命名空间,从而可以在代码中直接使用该命名空间中的类型,而不需要使用全名。
上面代码中控制台输出语句,如果使用全名的话就是System.Console.WriteLine("ciallo~")
,也就是命名空间.类名.方法()
但上面代码中第一行使用了using System;
这时倒数第四行就可以直接写成Console.WriteLine("Ciallo~")
。
注释
我最痛恨两种人,一种是写代码不写注释的人,一种是要求我写注释的人。 ——某程序猿
注释,见名知意,就是解释,解释你干了什么的一句话或者一段话。它并不参与到程序的实际运行当中,编译器在编译代码时也会忽略掉它。
一份含有规范的注释的代码可以让其他人快速了解你这些代码做了什么,在做什么,也可以提醒自己该做什么。相对于直接读代码,读注释显然更让人舒服。
注释部分在开发工具中,一般以 绿色 或者 灰色 显示,表示这些行不参与代码的实际运行。
单双斜杠注释
双斜杠注释// 这里是注释哦~
,表示单行注释,也就是这一行从这两个斜杠开始,后面的都算注释。
/* */
注释,多行注释,以/*
开始,以*
表示此行仍然是注释,以 */
作结尾。
1 | // namespace 声明命名空间 |
提示
如果使用Visual Studio,可以选中某一行或某几行,按下ctrl + k
,这几行前会自动添加 //
,也就是注释掉这几行。
如果是其他开发工具,一般是ctrl + /
键
三斜杠注释
三斜杠注释又称 XML文档注释 ,一般用在为类、方法、属性等添加文档注释,这些注释可以被各种工具解析,用于生成代码文档,以及代码提示。
在 Visual Studio 中,直接输入三个///
会自动生成下面一堆东西。
1 | /// <summary> |
说明:
<summary>
:提供简短的说明。<param>
:描述方法参数。<returns>
:描述方法的返回值。<remarks>
:提供详细说明或备注。<example>
:提供示例代码。<see>
和<seealso>
:用于创建代码元素的交叉引用。<exception>
:描述方法可能抛出的异常。
这时,在其他地方调用这个方法时,鼠标放在上面会提示你你写的这些信息。
如下面的代码,把鼠标放在SayWorld("Ciallo!")
上,会提示这些信息:
string Program.SayWorld(string s)
在你输入的字符串后面加上 World!返回结果:
你输入的内容+World!
1 | namespace csharp基础 |
数据类型
数据类型大致分为 存文字的,存整数的,存小数的,存地址的这几个类别。
值类型
值类型,也就是实际存放数据的类型,下表列出了所有的值类型。
类型 | 描述 | 范围 | 默认值 |
---|---|---|---|
bool | 布尔值 | True 或 False | False |
byte | 8 位无符号整数 | 0 到 255 | 0 |
char | 16 位 Unicode 字符 | U +0000 到 U +ffff | ‘\0’ |
decimal | 128 位精确的十进制值,28-29 有效位数 | (-7.9 x 10^28^ 到 7.9 x 10^28^) / 10^0到28^ | 0.0M |
double | 64 位双精度浮点型 | (+/-)5.0 x 10^-324^ 到 (+/-)1.7 x 10^308^ | 0.0D |
float | 32 位单精度浮点型 | -3.4 x 10^38^ 到 + 3.4 x 10^38^ | 0.0F |
int | 32 位有符号整数类型 | -2,147,483,648 到 2,147,483,647 | 0 |
long | 64 位有符号整数类型 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0L |
sbyte | 8 位有符号整数类型 | -128 到 127 | 0 |
short | 16 位有符号整数类型 | -32,768 到 32,767 | 0 |
uint | 32 位无符号整数类型 | 0 到 4,294,967,295 | 0 |
ulong | 64 位无符号整数类型 | 0 到 18,446,744,073,709,551,615 | 0 |
ushort | 16 位无符号整数类型 | 0 到 65,535 | 0 |
这里解释几个概念,
- 布尔值,
True
与False
,真 与 假,可以理解为 正确 与 错误 - 整型,存放整数的
- 有符号与无符号,指的是有没有
-
号,也就是能不能存负数 - 浮点型,在日常使用时可以直接理解为小数。
- 单精度与双精度,指浮点数表示中的两种精度级别。它们主要区别在于存储浮点数所需的位数以及由此带来的精度和范围的差异。
- 位(bit):指一个二进制位,一个位只能有两种状态,0 和 1,
8 位指的就是八个二进制位,0000 0000
- 字节(byte):一个字节由 8 个位组成,能表示的无符号整数范围是
0000 0000 ~ 1111 1111
也就是 十进制的0~255
- Unicode字符:也叫万国码,将世界上所有的文字统一进行编码。参考: 统一码
获取一个类型或变量在特定平台的准确尺寸,用sizeof()
来获取,返回的是数字单位是字节。如Console.WriteLine(sizeof(int))
,输出结果是 8
,而从上表可知,int 是 32位的,也就是 8 字节。
一种方便的记忆方式:
有符号整型:sbyte(8) < short(16) < int(32) < long(64)
无符号整型:byte(8) < ushort(16)< uint(32)< ulong(64)
浮点型:float(32) < double(64) < decimal(128)
其他:bool(8)与 char(16)
信息
浮点数之所以称为浮点数而不是小数,是因为它们使用科学计数法来表示,允许小数点的位置浮动,从而能够表示非常大或非常小的数值。这种表示方式在计算机科学和工程计算中具有极大的优势,提供了更大的数值范围和更高的灵活性。
信息
尽管 bool
类型只需要 1 位来表示,但在内存中通常分配 1 个字节(8 位)。
原因:
- 内存对齐:处理器通常以字节为单位进行内存读取和写入操作。以字节为单位进行内存访问可以提高效率。
- 结构体对齐:在包含多个字段的结构体中,按字节对齐可以简化结构体的布局和访问。
- 平台独立性:在不同的平台上,不同的处理器和编译器可能有不同的对齐要求。统一为 1 个字节分配空间可以提高跨平台的兼容性和一致性。
引用类型
引用类型不包含实际数据,它主要用来存放对变量的引用,也就是用来存放变量的地址。
引用类型实际数据一般存放在堆(托管堆 Managed Heap)上。
C# 的引用类型有如下几种:对象(Object)类型,字符串(string)类型,动态(dynamic)类型。
对象类型:各种类的实例化对象都是引用类型。
字符串类型:字符串也是引用类型,但是和其他引用类型有些不同,具体参考string相关内容。
动态(dynamic)类型:不管赋给动态类型变量的值是不是引用类型,它都是引用类型。
可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。
声明动态类型的语法:
1
2
3
4dynamic <variable_name> = value;
// 例如
dynamic d = 20;
指针类型
指针类型变量存储另一种类型的内存地址。声明指针类型的语法:
1 | type* identifier; |
例如:
1 | char* cptr; |
到这里可能会有疑惑,既然引用类型是存放地址的,那么指针类型是否是多余的?
指针类型 和 引用类型 是两个不同的概念。尽管它们都涉及到内存地址的操作,但它们在使用方式和特性上有显著区别。
指针类型的主要特性:
使用范围:只能在不安全上下文中使用,需要使用 unsafe
关键字。
内存管理:由开发者手动管理,不受垃圾回收机制的控制。
类型检查:编译时进行类型检查,但由于直接操作内存,有更高的风险。
引用类型的主要特性:
使用范围:在所有上下文中都可以使用,不需要 unsafe
关键字。
内存管理:由垃圾回收机制自动管理,开发者不需要手动管理内存。
类型检查:编译时进行严格的类型检查
可空类型
?
单问号用于对 int、double、bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 Nullable 类型的。
1 | int? i = 3; |
等同于:
1 | Nullable<int> i = new Nullable<int>(3); |
Null 合并运算符 ??
1 | int? a = null; // 定义一个可空的int类型并设置值为null |
变量
变量顾名思义就是可变的量,与之相对的还有常量,也就是不可变的量。
定义一个变量的语法:
1 | 变量类型 变量名; |
变量作用域
作用域也就是起作用的区域
作用域范围一般由大括号决定,大括号内的变量,大括号外无法访问。
1 | namespace csharp基础{ |
变量存储位置
在说明存储位置时,首先要了解一下栈与堆。
栈(Stack)
栈是一种后进先出(LIFO Last In First Out)的数据结构,它主要用于存储局部变量和函数调用的相关信息。
特点
- 自动分配和释放:当函数调用时,分配新的栈帧,当函数返回时,栈帧自动销毁。
- 高效:由于栈的操作(如压栈、弹栈)通常只涉及移动栈指针,因此非常高效。
- 有序:栈内存是连续的,分配方式是有序的。
- 固定大小:栈的大小通常是预先定义的,并且有一定的限制。
堆(Heap)
堆是一种非结构化的内存区域,用于存储动态分配的内存块。这些内存块在运行时根据需要分配和释放。堆内存的管理通常由程序员手动控制(例如使用 new
操作符在 C# 中分配对象),并由垃圾回收机制自动处理内存回收。
特点
- 动态分配:内存块可以在运行时动态分配和释放。
- 手动管理:程序员负责请求内存,垃圾回收器负责内存回收。
- 不连续:堆内存块在内存中可以是非连续的,通过指针或引用来连接。
- 灵活:堆的大小只受系统可用内存的限制。
栈与堆的区别
内存管理:
- 栈:自动管理,函数调用结束后自动释放内存。
- 堆:手动管理,分配的内存需要垃圾回收器来回收。
存储内容:
- 栈:存储局部变量、函数参数、返回地址等临时数据。
- 堆:存储动态分配的对象和数据。
内存分配方式:
- 栈:顺序分配,内存块是连续的。
- 堆:随机分配,内存块可以是不连续的。
大小限制:
- 栈:通常有固定的大小限制。
- 堆:大小只受系统可用内存的限制。
访问速度:
- 栈:由于内存连续且有序,访问速度较快。
- 堆:由于内存不连续,访问速度相对较慢。
存储位置
变量存储位置由变量类型决定。
值类型与指针类型的变量都存储在栈中。
引用类型的 实际数据的地址也就是变量本身存储在栈中,实际数据存储在堆 中。
信息
string
虽然是引用类型,使用 string str = "xxxx"
方式来创建变量的话,编译器会在常量池检查是否出现过,如果已经出现过,那么这个内容只会固定有一个引用,所以在创建两个内容相同的string时,他们的引用是相同的。
两个内容相同的string
共用同一个地址。
修改其中一个string
后,被修改的string会在常量池分配新空间。
常量
常量的值不可更改。
常量是使用 const 关键字来定义的 。
定义一个常量的语法如下:
1 | const <数据类型> <常量名> = value; |
在 C# 中,常量不能用于存储引用类型的实例。const
关键字只能用于表示编译时已知的简单值类型和字符串。对于引用类型的常量,我们需要使用 readonly
关键字,它允许在运行时初始化后保持不变。
类型转换
隐式类型转换
隐式转换指的是不在代码中写出类型转换语句,便自动完成转换的情况,这种情况下,数据能够完整的转换。
隐式转换遵循 小范围类型 转 大范围类型,派生类 转到 基类。
下面列出几个例子:
1 | // sbyte 8位,short 16位 |
提示
float
使用 IEEE 754 标准,32位由 1位符号位(S),8位指数位(E),23位尾数位(M)组成,
int 使用二进制计算,32位有效数字也就是
- 最大正数:
01111111 11111111 11111111 11111111
(31 个 1),即2,147,483,647
- 最小负数:
10000000 00000000 00000000 00000000
(最高位为 1,其余为 0),即-2,147,483,648
显式类型转换
显式类型转换又称强制类型转换,需要在代码中明确使用强制转换相关语句。
强制转换是将一个大范围的类型转换到小范围的类型,以及将一个对象类型转换为另一个对象类型。
强制转换不仅不一定成功,而且会导致数据损失。
1 | // int (32位) 转 sbyte (8位) |
显式类型转换可以使用如下几种方式:
1 | // 1. 括号强转 |
运算符
算术运算符
下表显示了 C# 支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
– | 自减运算符,整数值减少 1 | A– 将得到 9 |
关系运算符
下表显示了 C# 支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 不为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 不为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 不为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C# 支持的所有逻辑运算符。假设变量 A 为布尔值 true,变量 B 为布尔值 false,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
位运算符
下表列出了 C# 支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 如果同时存在于两个操作数中,二进制 AND 运算符复制一位到结果中。 | (A & B) 将得到 12,即为 0000 1100 |
| | 如果存在于任一操作数中,二进制 OR 运算符复制一位到结果中。 | (A | B) 将得到 61,即为 0011 1101 |
^ | 如果存在于其中一个操作数中但不同时存在于两个操作数中,二进制异或运算符复制一位到结果中。 | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 按位取反运算符是一元运算符,具有”翻转”位效果,即0变成1,1变成0,包括符号位。 | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 | A >> 2 将得到 15,即为 0000 1111 |
1 | A = 0011 1100 |
赋值运算符
下表列出了 C# 支持的赋值运算符:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
其他运算符
下表列出了 C# 支持的其他一些重要的运算符,包括 sizeof、typeof 和 ? :
。
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回数据类型的大小。 | sizeof(int),将返回 4. |
typeof() | 返回 class 的类型。 | typeof(StreamReader); |
& | 返回变量的地址。 | &a; 将得到变量的实际地址。 |
* | 变量的指针。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则为 X : 否则为 Y |
is | 判断对象是否为某一类型。 | If( Ford is Car) // 检查 Ford 是否是 Car 类的一个对象。 |
as | 强制转换,即使转换失败也不会抛出异常。 | Object obj = new StringReader(“Hello”); StringReader r = obj as StringReader; |
流程控制
if else
1 | if (条件又称布尔表达式) { |
switch
switch一般用于结果有限且较少的情况下使用。毕竟条件复杂了不太好写。
1 | switch(expression){ |
switch 语句必须遵循下面的规则:
- switch 语句中的 ==expression 必须是一个整型或枚举类型,或者是一个 class 类型==,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。
- case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量。
- 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
- 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
- 不是每一个 case 都需要包含 break。如果 case 语句为空,则可以不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
- C# 不允许从一个 case 部分继续执行到下一个 case 部分。如果 case 语句中有已经执行,则必须包含 break 或其他跳转语句。
- 一个 switch 语句可以有一个可选的 default 语句,在 switch 的结尾。default 语句用于在上面所有 case 都不为 true 时执行的一个任务。default 也需要包含 break 语句,这是一个良好的习惯。
- C# 不支持从一个 case 标签显式贯穿到另一个 case 标签。如果要使 C# 支持从一个 case 标签显式贯穿到另一个 case 标签,可以使用 goto 一个 switch-case 或 goto default。
1 | int a = Convert.ToInt32(Console.ReadLine()); |
输入2,执行结果
1 | 3 |
三元运算符
三元运算符用在进行简单的逻辑判断并返回数据的场景下,以最短且最清晰的方式进行条件判断并返回结果。
警告
请尽量不要使用三元运算符嵌套,以及判断复杂的条件,这会使代码的可读性变得非常差,并且与三元运算符的设计初衷相悖。
1 | Exp1 ? Exp2 : Exp3; |
while
1 | while(条件){ |
do while
1 | do{ |
for
1 | for (init; condition; increment){ |
foreach
foreach
是 C# 中用于迭代集合(如数组、列表、字典等)的循环语句。它提供了一种简洁、安全且高效的方式来遍历集合中的每一个元素,而无需显式地使用索引变量或处理边界条件。
1 | foreach (var element in collection){ |
element
:在每次循环中代表集合中的一个元素。它的类型通常由集合元素的类型推断。
collection
:要遍历的集合或数组。
break
终止循环或switch;
continue
跳过当前循环剩余语句,直接进入下一次循环;
参考内容
- 标题: C# 数据类型与流程控制
- 作者: 日之朝矣
- 创建于 : 2024-07-25 13:08:53
- 更新于 : 2024-08-18 09:25:27
- 链接: https://blog.rzzy.fun/2024/07/25/csharp-datatype-and-process-control/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。