我现在有着一定的 Python 和 C 基础(Python 相对更精通些)。为了能够开发桌面应用程序,我决定让自己接触 C#。

这篇笔记是我的 C# 入门阶段学习笔记。

Tyex 的 C# 入门学习笔记

一、我的第一个 C# 项目

1.0 认识 C#

先在 Visual Studio 创建一个基于 C#、以控制台应用程序为框架的应用程序。这次不适用顶级语句,于是可见:

C# 项目结构如下:

1
2
3
4
5
解决方案
└── 项目
└── 源代码文件和命名空间
└── 类
└── 字段和方法(包含被编译后执行的Main方法)

其中,源代码文件和命名空间是两种不同的类封装模式。源代码文件属于物理封装,在文件管理系统使用。而命名空间属于逻辑封装,不依赖源代码文件的物理结构让编译器根据命名空间来整理类和子命名空间。

框架让项目被创建时自带一些现成的内容,其中就有一个 Program.cs 的源代码文件,它里面先定义了了一个和项目同名的命名空间,并定义了一个 Program 类,还定义了一个 Main 方法。Main 方法传入的 args 参数,由启动应用程序时输入,以 args[0], arg[1] 等值传入函数中调用。

C# 官方文档给的第一个示例:

1
2
3
4
5
6
7
8
9
10
using System;

class Hello
{
static void Main()
{
// This line prints "Hello, World"
Console.WriteLine("Hello, World");
}
}

上面的程序始于引用 System 命名空间的 using 指令。

程序所声明的 Hello 类只有一个成员 Main() 方法,它使用 static 修饰符进行声明。

按照惯例,在不使用顶级语句时(顶级语句在 C# 9.0 才被引入),名为 Main 的静态方法充当 C# 的入口点。

1.1 控制台的输入和输出方法

使用控制台的静态方法 Console.WriteLine(输出内容) 可输出内容。而 Console.ReadLine() 则请求用户输入内容并返回为字符串。

Console.Write() 也能输出内容,但是不作换行。通常使用 Console.WriteLine() 输出,便于换行。

在字符串中写入 \n 也能让字符串换行。

1.2 变量声明

在方法体中,类型 变量名 可以声明并定义变量(未初始化)。基本的数据类型有int、string、double、float、bool、decimal。

类型 变量名 = 值 可以初始化变量(声明、定义的同时显式赋值),只有被赋值了的变量才可以被调用。

var 变量名 = 值 关键字可以让编译器根据值自动推断变量的类型。

1.3 注释

行末注释可以在当前行添加两个空格之后写上 // 注释的内容

多行注释可以使用 /* */ 包括住。

1.4 第一个 C# 项目示例

第一个 C# 项目 Main 方法的方法体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Console.WriteLine("Enter your name:");
string name = Console.ReadLine();
Console.WriteLine("Enter your age:");
int age;
while (!int.TryParse(Console.ReadLine(), out age))
{
Console.WriteLine("Invalid input. Please enter a valid age:");
}
Console.WriteLine("Your name is " + name);
Console.WriteLine("Your age is " + age);
age += 1;
Console.WriteLine("But next year you will be " + age);
Console.ReadLine();
// data type discussion
int WholeNumber = 120;
string Anytext = "Names or other things";
double decimalPoints = 345.35;
bool TrueFalse = false;
char SingleLetter = 'a';

二、基本数据类型的基本操作

2.1 字符串

$"字符串内容{内插内容表达式} 可以实现字符串内插。

@"字符串内容 可以避免字符串转义(通常在写文件位置使用)。$@可以一起用。

\n 表示换行,t 表示制表,\\ 输出一个反斜杠,\" 输出一个双引号。

{{` `}} 可以让能进行字符串内插的字符串写入大括号。

+ 加号运算可以连接两个字符串。

[索引] 可以索引指定位置的字符。

Length 获取长度

ToUpper()ToLower() 返回转为大写或小写的字符串。

Trim() TrimStart() TrimEnd() 返回去除首末空格的字符串。

Remove(起始索引, {长度}) 移除字符串中的切片。

Replace(被替换子集,替换子集) 返回替换后的字符串。

Contains(包含字符串) 判断是否包含字符串子集。

StartsWith(字符串) EndsWith(字符串) 判断首末的字符串子集。

IndexOf(包含字符串) 返回包含的第一个字符第一次出现的索引,无则返回 -1

LastIndexOf(包含字符串) 返回包含的第一个字符最后一次出现的索引,无则返回 -1

Substring(开始索引, {长度}) 返回字符串子集,默认输出开始索引至字符串的[-1]索引处。

PadLeft(填充数量, {填充符号=空格}) PadRight(填充数量, {填充符号=空格}) 返回填充之后的字符串。

IndexOfAny(字符数组) 返回字符数组任意一个字符元素第一次出现的索引。

字符串格式设置(内插或格式化使用冒号):

  • C:货币(国家/地区跟随用户系统)

  • C数字:货币,保留一定小数位数

  • N:逗号三位分隔

  • N数字:逗号三维分隔,且保留一定小数位数

  • P:百分比

  • P数字:百分比,保留小数点后一定位数

2.2 数字

数字通常有整型、浮点型、双精度、十进制。

一个整数比如 10 就可以创建一个整数。

带有小数点的数比如 10.0 就可以创建一个双精度数字。

一个带有小数点和f后缀的数比如 10.0f 就可以创建一个浮点型数字。

一个带有小数点和m后缀的数比如 10.0m 就可以创建一个十进制数字,并支持保留小数位数。

(类型)数字 可以返回对数字进行强类型转换的结果。

Math.Abs(数字) 静态方法可以取绝对值。

Math.Pow(底数, 指数) 静态方法可以求乘方。

Math.Sqrt(数字) 静态方法可以开平方。

Math.Min或Max(数字[]) 可以取最值。

Math.Round(数字, 小数位数=0) 可以四舍五入,小数位数为负则精确至整数十位数以上。

运算符 + - * / % :加、减、乘、除、取模。

2.3 布尔值

运算符 && || ! 分别表示“和”“或”“非”。

2.4 基本数据类型的基本操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Console.WriteLine("Enter your first number: ");
double firstNumber = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("Enter your second number: ");
double secondNumber = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("Enter your Operator from +, -, *, /: ");
string myOperator = Console.ReadLine();
if (myOperator.Length > 1)
{
Console.WriteLine("Please enter a valid operator: ");
myOperator = Console.ReadLine();
}
if (myOperator == "+")
{
Console.WriteLine(firstNumber + secondNumber);
}
if (myOperator == "-")
{
Console.WriteLine(firstNumber - secondNumber);
}
if (myOperator == "*")
{
Console.WriteLine(firstNumber * secondNumber);
}
if (myOperator == "/")
{
Console.WriteLine(firstNumber / secondNumber);
}
Console.ReadLine();

2.6 赋值运算

赋值运算是通过赋值表达式,产生把等号的右值赋值给等号左边对象的副作用。就像 a = 1

除了等号,还可以使用 += -= *= /= 等进行自增减乘除操作,就像 a += 1 相当于 a = a + 1

三、库、类和方法————以.NET类库为例

3.1 .NET 类库

我们所用的 Console 类、WriteLine() 方法等很多都来自 .NET 类库。前面的项目实例中和控制台交互的任何方法都被搜集在 .NET 库的 Systenm.Console 类中。.NET 还支持很多特定类型的应用程序。

可见,库像是一个提供类和方法的图书馆。

C# 的数据类型如 string int 也都由 .NET 类库提供,但 C# 会屏蔽数据类型和 .NET 类库的连接以简化工作量。

类,其实也是方法的容器,类似于库这个图书馆的各个区域,开发者会把相关的方法保留在一个类中。

3.2 方法的调用

类名·方法名(参数)对象名·方法名(参数) 分别可以调用静态方法和实例方法。

在本文第一个 C# 项目中所调用的 Console.WriteLine("Enter your name:") 就是调用一个来自 Console 类的 WriteLine() 静态方法,并输入字符串 "Enter your name:" 作为实参。

3.3 类的实例化

new 类名() 可以创建一个类的实例。完成整个实例化使用的声明格式为 类名 对象名 = new 类名()

new 的角色是运算符,和类一起返回一个新的实例。

当类被实例化之后,就可以调用实例方法:

1
2
3
Random dice = new Random();  // 实例化 
int roll = dice.Next(1, 7); // 实例方法调用
Console.WriteLine(roll); // 类方法调用

3.4 方法的返回值

方法的调用,本质上是写一个方法名加上括号对的表达式。表达式会指向一个其代表的值(也就是计算结果,如果没有也会返回 None)。

就像刚才的 dice.Next(1, 7) 返回一个 1 至 6 的随机整数。

3.5 方法的重载

方法的重载指的是方法会根据输入不同类型参数被多次定义。就像 Console.WriteLine() 既可以输入字符串,也可以输入整型。

四、数组

4.1 数组的概念

定义:数组是通过单个变量名进行访问的单个数据元素的集合,并可指定元素的类型和长度。

访问:从 0 开始的索引访问。

用途:可以使用一个变量名创建用途特征相同的数据集的集合。

4.2 数组的声明

元素类型[] 数组名 = new 元素类型[长度] 可以声明一个指定元素类型和长度的数组,如:

1
string[] fraudulentOrderIDs = new string[3];

声明了一个长度为 3 的字符串数组。声明的类型部分 string[] 告知编译器这是个字符串数组,而类名部分 string[3] 指示数组可以保存的元素数目。

4.3 数组的访问

数组使用中括号表示法访问元素:数组名[索引],如:

1
2
3
4
5
string[] fraudulentOrderIDs = new string[3];

fraudulentOrderIDs[0] = "A123";
fraudulentOrderIDs[1] = "B456";
fraudulentOrderIDs[2] = "C789";

这还说明数组是可变对象,可以改写每一个元素,而这个数组依然是这个数组。

4.4 数组的初始化

C# 12 引入了“集合表达式”语法,如

1
string[] fraudulentOrderIDs = [ "A123", "B456", "C789" ];

如果不使用“集合表达式”,可以使用旧语法:

1
string[] fraudulentOrderIDs = { "A123", "B456", "C789" };

以下两种也可以:

1
string[] fraudulentOrderIDs =  new string[3] { "A123", "B456", "C789" };
1
string[] fraudulentOrderIDs = new string[] { "A123", "B456", "C789" };

4.5 数组的属性

Length 属性返回数组的长度。

4.6 foreach 语句

foreach 语句可以通过循环遍历数组。格式为 foreach (元素类型 循环变量名 in 数组名) {循环体},如:

1
2
3
4
5
6
7
int[] inventory = { 200, 450, 700, 175, 250 };
int sum = 0;
foreach (int items in inventory)
{
sum += items;
}
Console.WriteLine($"We have {sum} items in inventory.");

4.7 数组的方法

由于数组不支持扩展,所以数组的方法都是用静态方法。

Array.Sort(数组) 可以对数组产生排序的副作用。

Array.Reverse(数组) 可以反转数组的顺序。

Array.Clear(数组, 起始索引, 元素个数) 可以切片一段元素替换为 null。

Array.Reverse(ref 数组, 长度) 可以改变数组的长度,扩大则填入空值,缩小会删除溢出的元素。

字符串.ToCharArray() 可以通过字符串转化创建一个字符数组实例。

string.Join("分隔符", 字符数组) 可以通过分隔符和字符数组串成新的字符串实例。

字符串.Split('分隔符') 可以通过分隔符分成新的字符串数组实例。

4.8

五、控制结构

5.1 条件控制

由 if 引导的条件控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (条件1)
{
符合条件1时执行的主体;
}
else if (条件2)
{
符合条件2时执行主体;
}
else if (条件3)
{
符合条件3时执行主体;
}
...
else
{
上述条件都不符合的执行的主体
}

由 switch 引导的条件控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (表达式)
{
case1: //标签,返回一个布尔表达式
表达式等于值1时执行;
跳转语句(不能用continue); //用 break、goto、return 都行
case2:
表达式等于值2时执行;
跳转语句(不能用continue);
···
default:
没一个case符合时执行;
跳转语句(不能用continue);
}

使用条件运算符 布尔值 ? 真值返回 ? 假值返回 也可以实现控制结构:

1
2
3
int saleAmount = 1001;
int discount = saleAmount > 1000 ? 100 : 50;
Console.WriteLine($"Discount: {discount}");

5.2 循环结构

由 while 引导的循环结构:

1
2
3
4
while (条件)
{
主体;
}

由 do 引导的循环结构:

1
2
3
4
do
{
主体;
} while (条件);

由 for 引导的循环结构:

1
2
3
4
for (循环变量初始化声明; 完成条件; 迭代器)
{
主体;
}

1
2
3
4
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}

循环体内可以使用 break; 跳出循环 或使用 continue; 跳过本次循环,这两种都是控制结构常用的跳转语句。

前面在数组所说的 foreach 也是控制结构的循环,可以视为特殊的 for 循环。

六、数据类型进阶

6.1 数据与数据类型的概念

严格上来说,C# 的数据类型应分为值类型和引用类型两种。一步步解析概念可以帮助深入认识数据类型。

什么是数据?————数据是储存在计算机内存中的值,形式为二进制位,8 位形成 1 字节,即 1 字节有 256 种组合。如整数 195 可以用二进制的 11000011 表示。想表示越大的值,就要用越多的位数,如 32 位或 64 位甚至更多。如果想储存文本数据,则可以使用字符编码系统(如 ASCII、UTF-16),字符编码系统给出了字符和编码之间的对等规则。C# 使用 UTF-16 系统储存文本数据。

什么是数据类型?————数据类型是编程语言定义为某个值保留多少内存的方式,通常分为两种:

  • 值类型:变量直接包含其数据。

  • 引用类型:变量指向储存在其它位置的数据值。

6.2 C# 中的值类型

简单的值类型在 C# 是以关键字为形式提供的预定义类型,实际上是 .NET 类库种定义的预定义类型的别名。如 int 是 .NET 库种 System.Int32 的值类型的别名。charbool 等都是 C# 的值类型。

值类型变量将其值直接存储在栈中,并通过复制传递。栈是为 CPU 上当前运行的代码分配的内存(也称为堆栈帧或激活帧)。堆栈帧执行完毕后,栈中的值将被删除。

检查值类型的范围,可以通过静态属性 MinValueMaxValue 来查询值类型的最小值和最大值,如:

1
2
3
4
5
6
Console.WriteLine("Signed integral types:");

Console.WriteLine($"sbyte : {sbyte.MinValue} to {sbyte.MaxValue}");
Console.WriteLine($"short : {short.MinValue} to {short.MaxValue}");
Console.WriteLine($"int : {int.MinValue} to {int.MaxValue}");
Console.WriteLine($"long : {long.MinValue} to {long.MaxValue}");

6.3 C# 中的引用类型

引用类型将数据储存于单独的堆里面,变量储存引用(堆中的某个内存地址)。堆是一个由操作系统上同时运行的多个应用程序共享的内存区域。多个引用类型变量可以引用同一个对象。

1
2
int[] data;  // 创建数组 data,引用类型,目前暂时为空引用
data = new int[3]; // 与操作系统协调调用三个 int 的内存空间,并初始化三个元素的默认值 0

当引用类型可变的时候,某些情况就不需要用 ref 传入方法的参数了。因为 ref 表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int a = 1;
void b(ref int c) // 用了 ref,为了输出的时候创建一个新对象
{
c += 1;
}
b(ref a);
Console.WriteLine(a);

int[] d = [1, 2];
void e(int[] f) // 不使用 ref,已经可以把引用传入方法体
{
f[1] = 6;
}
e(d);
foreach (int i in d)
{
Console.WriteLine(i);
}

6.4 选择适当的数据类型

选择数据类型,既要与应用场景相匹配,也要让运行效能足以优化。通常情况下:

  • int:适用于大部分整数
  • decimal:适用于表示资金
  • bool:表示 true 或 false
  • string:表达文本

视情况使用复杂类型:

  • byte:采集来自其它计算机系统或使用不同字符集的编码数据
  • double:常用于几何和科学研究
  • System.DateTime:适用于特定日期和时间值
  • System.TimeSpan:适用于毫秒至年的时间段

6.5 数据类型转换

6.5.1 隐式转换

隐式转换就是通过声明来转换,这是一种安全转换,如

1
2
3
4
int myInt = 3;
Console.WriteLine($"int: {myInt}");
decimal myDecimal = myInt;
Console.WriteLine($"decimal: {myDecimal}");

隐式转换只允许扩大转换,所以它一定能安全保留完整的信息。

6.5.2 显式转换

(数据类型)数字 可以对数字进行强制转换,括号在这里是强制转换运算符。扩大转换此时依然是安全的,但收缩转换可能会因为截断而丢失数据,如

1
2
3
4
decimal myDecimal = 3.14m;
Console.WriteLine($"decimal: {myDecimal}");
int myInt = (int)myDecimal;
Console.WriteLine($"int: {myInt}");
6.5.3 字符串和数字的互相转化

TryParse() 方法可以尝试将字符串转化为指定数字类型,并返回布尔值说明是否转换成功。

1
2
3
4
string s = "1";
int result;
bool whether_success = int.TryParse(s, out result);
Console.WriteLine($"{whether_success} to change s to {result}.");

ToString() 方法可以将数字类型转为字符串。

1
2
3
int number = 123;
string numberAsString = number.ToString();
Console.WriteLine("整数转换为字符串: " + numberAsString);

七、方法进阶

7.1 方法工作原理

开发方法,先从 创建方法签名 (声明)开始:

1
void SayHi();

返回类型为 void 意味着方法不需要返回数据。方法也可以返回 int 等类型。括号中可以接受用逗号分割的多个参数,而此时未输入参数。

能被运行的方法是需要包含 定义 的。使用 {} 可以包含调用方法时执行的代码,如:

1
2
3
4
void SayHi()
{
Console.WriteLine("Hi");
}

方法通过其名称和输入参数进行 调用 ,就像

1
Console.Write("Input!");

方法在其定义之前或之后均可调用,就像前面的 SayHi()

1
2
3
4
5
6
SayHi();

void SayHi()
{
Console.WriteLine("Hi");
}

习惯上,通常在程序末尾才定义方法。

return 语句可以终止方法执行,通常在有循环的时候写个 return; 跳出方法。

7.2 方法中使用参数

参数需要在方法签名中被声明。

参数有两种具体的术语:形参 和 实参。形参多指方法签名中的变量,实参多指调用方法时传递的值。

这个例子表示方法接受一个整型参数 max,在循环中引用。调用时传入整数 5 作为参数提供。

1
2
3
4
5
6
7
8
9
CountTo(5);

void CountTo(int max)
{
for (int i = 0; i < max; i++)
{
Console.Write($"{i}, ");
}
}

对参数产生副作用时,值类型会拷贝副本,而引用类型(如果可变)会被修改,如:

1
2
3
4
5
6
7
8
9
10
11
12
int a = 3;
int b = 4;
int c = 0;

Multiply(a, b, c); // 输出 3 x 4 = 12
Console.WriteLine($"global statement: {a} x {b} = {c}"); // 输出 3 x 4 = 0

void Multiply(int a, int b, int c)
{
c = a * b;
Console.WriteLine($"inside Multiply method: {a} x {b} = {c}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int[] array = [1, 2, 3, 4, 5];

PrintArray(array); // 输出 1 2 3 4 5
Clear(array); // 修改了数组
PrintArray(array); // 输出 0 0 0 0 0

void PrintArray(int[] array)
{
foreach (int a in array)
{
Console.Write($"{a} ");
}
Console.WriteLine();
}

void Clear(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
array[i] = 0;
}
}

由于字符串是不可变的,于是不会被修改:

1
2
3
4
5
6
7
8
9
10
11
string status = "Healthy";

Console.WriteLine($"Start: {status}");
SetHealth(status, false);
Console.WriteLine($"End: {status}");

void SetHealth(string status, bool isHealthy)
{
status = (isHealthy ? "Healthy" : "Unhealthy");
Console.WriteLine($"Middle: {status}");
}

但可以创建新字符串覆盖全局变量:

1
2
3
4
5
6
7
8
9
10
11
string status = "Healthy";

Console.WriteLine($"Start: {status}");
SetHealth(false);
Console.WriteLine($"End: {status}");

void SetHealth(bool isHealthy)
{
status = (isHealthy ? "Healthy" : "Unhealthy");
Console.WriteLine($"Middle: {status}");
}

如果不按照方法签名中形参的顺序输入实参,可以用 : 将实参赋值给形参。

1
2
3
4
5
6
7
8
9
10
void PrintMessage(string message, int count)
{
for (int i = 0; i < count; i++)
{
Console.WriteLine(message);
}
}

PrintMessage("Hello", 3);
PrintMessage(count: 2, message: "World"); // 不按顺序参数赋值

参数可以使用默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
string[,] AssignGroup(int groups = 6, string defaultStatus = "Healthy")
{
string[,] result = new string[groups, pettingZoo.Length / groups];

for (int i = 0; i < groups; i++)
{
for (int j = 0; j < pettingZoo.Length / groups; j++)
{
result[i, j] = defaultStatus;
}
}

return result;
}

7.3 方法中使用返回值

方法体里面使用 return 返回值表达式 就可以让对应的方法调用表达式返回一个值。

1
2
3
4
5
6
7
int result = Add(5, 3);
Console.WriteLine($"5 + 3 = {result}");

int Add(int a, int b)
{
return a + b;
}

八、异常处理

8.1 测试、调试和异常处理

C# 作为一门编译型语言,开发代码都需要完成一定程度的测试和调试,并通常需要异常处理。

调试:在开发过程中更新代码逻辑。

测试:验证代码生成和运行。

异常处理:开发人员在代码中管理运行时的异常,而异常是程序运行时发生的错误。

8.2 异常处理语法

异常处理使用 try-catch-finally 语句实现。

1
2
3
4
5
6
7
8
9
10
11
12
try
{
// 可能包含异常的代码
}
catch
{
// 对于不同异常的处理方式
}
finally
{
// 无论是否发生异常都执行
}

8.3 常用异常

ArrayTypeMismatchException:储存错误类型。

DivideByZeroException:整数或十进制数除以零。

FormatException:参数格式无效。

IndexOutOfRangeException:索引过大。

InvalidCastException:显式转换无效。

NullReferenceException:试图访问 null 的成员。

OverflowException:数值溢出。

8.4 异常处理示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string[] inputValues = new string[] { "three", "9999999999", "0", "2" };

foreach (string inputValue in inputValues)
{
int numValue = 0;
try
{
numValue = int.Parse(inputValue);
}
catch (FormatException)
{
Console.WriteLine("Invalid readResult. Please enter a valid number.");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

以上就是 Tyex 的 C# 入门学习笔记。