创建工程

编写代码首先要创建一个工程文件,我这里使用的是Visual Studio 2022

创建工程

如图所示,安装好C#环境后,选择WPF 应用(.NET Frameword),点击下一步,选择相应的文件目录,创建即可。

工程文件

通过以上操作,就可以得到一个工程文件。

界面中间的白色窗体,就是我们运行程序之后得到的窗体,可以摁F5来运行。

运行窗体

对于这个工程文件,可以右键中间的白框,找到属性一栏,可以更改它的一些基本信息,例如下面这种,可以更改一下它的背景颜色。

更改背景颜色

C#提供了许多的空间可以去使用,可以在视图中找到工具箱,或者通过快捷键Ctrl+Alt+X来快速打开工具箱。

通过拖拽的方式添加一个TextBox控件用于放置文本内容,添加一个Button控件作为按钮,通过可视化操作更改它们的属性值(也可以通过修改相应的代码)。

控件设置

上图中,红色区域可以直接修改相应空间的属性值,黄色区域可以通过代码的方式进行修改,橙色框圈出来的按钮可以为相应的空间添加事件。

添加事件

通过双击Click后面的文本框可以为该按钮添加点击事件,会直接进入到代码编写的页面。

点击事件自动生成代码

Visual Studio会自动生成相应的代码。

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
29
30
31
32
33
34
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void button_Click(object sender, RoutedEventArgs e)
{
this.textBox1.Text = "Hello World!";
}
}
}

在第$30$行添加了一行代码,其作用是将当前textBox1控件中的Text设置为Hello World!,这样就实现了点击按钮在文本框中生成Hello World!的功能了。

单击事件效果

Hello World!

Console

首先,创建一个控制台应用,注意后缀有(.NET Framework),找到合适的位置保存,点击创建即可。

创建控制台应用

进入之后,内部会有一个默认的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
}
}
}

C#的全称是C sharp,因此其源码文件默认使用cs作为扩展名。

我们通过在第$13$行写输出Hello World!的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

运行结果:

1
2
Hello World!
请按任意键继续. . .

上述代码中的Console指的是控制台,WriteLine的作用是打印字符串。需要注意的是,如果直接运行或者使用F5运行,会导致控制台一闪而过,这个时候需要使用Ctrl+F5来运行,可以出现我们想要的结果。

Windows Forms

首先,创建一个Windows窗体应用,注意后缀有(.NET Framework),找到合适的位置保存,点击创建即可。

创建Windows窗体应用

进入之后,左上角会有一个白色的窗体,我们可以通过拖拽空间的方式设计界面,这部分内容与之前的内容基本完全一致,也是在工具箱中选择,并设置相应的属性。

Windows窗体设计

接着,设计按钮的点击事件,和之前一样,选中按钮后,找一个闪电样式的图标,在Click后面的文本框中进行双击,可以跳转到相应的代码页面,然后编写相应的代码:

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
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void buttonSayHello_Click(object sender, EventArgs e)
{
textBoxShowHello.Text = "Hello World!";
}
}
}

点击按钮后会更改文本框中的属性值(文本框和按钮的名字已更改)。

Windows窗体效果

该技术是一项比较老且比较过时的技术,现在的更新换代版本叫做WPF(Windows Presentation Foundation),也就是我们最开始使用的那个例子。这两种技术在窗体上没有什么不同,对于WPF而言,下面多出了一些看起来像是HTML的代码,这种代码叫做XML代码,这样可以让设计师直接使用类似于前端的方式直接对界面进行设计,可以不用像之前一样进行拖拽,能够让设计师直接加入到开发团队中。

类与名称空间

基本概念

类(class)构成程序的主体;名称空间(namespace)以树型结构组织类(和其他类型),例如ButtonPath类。

我们先回到之前写的Hello World!的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

在这段代码中,一共有两个类,分别是ProgramConsole,其中Program这个类是我们自己写的,它代表了我们的程序。对于C#而言,它是一门完全面向对象的语言,用这种语言编程的时候,程序本身也是一个类。Console是自带的一个类,后面的WriteLine是这个类中的一个方法,作用是将内容输出到控制台中。和C语言一样,Main和主函数很像,是程序的入口,不同点在于它也需要包含在这个Program类中。

第$7$行中的代码表示,我们的这段代码放在了一个叫做ConsoleApp的名称空间中,这个名称空间默认会跟我们创建的工程文件的名字保持一致。使用名称空间可以让我们自己写的类用相应的名称空间组织起来,当别人想用里面的类的时候,可以非常方便地从名称空间中把这个类找出来。

1
2
3
4
5
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

创建项目时,会自动生成这五个名称空间,里面包含了最常用的名称空间,就不需要我们再进行调用了。例如第一个名称空间中,就包含了Console类,如果删掉它就无法直接调用Console了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
System.Console.WriteLine("Good morning!");
}
}
}

虽然不能直接调用,但是可以通过声明相应的名称空间来调用相关的类和对应的方法,这种方法会增加一些代码量,每次调用的时候都得声明相应的名称空间。为了防止这些麻烦,一般来讲都会在最开始进行声明。不过有一种情况比较特殊,对于不同的名称空间,可能包含类名相同的类,如果直接调用的话,编译器就没法区分这个类属于哪个名称空间,所以只能通过在前面加上想要调用的类的名称空间。

类库的引用

类和名称空间放在一个叫做类库的东西里,类库是我们这个类和名称空间的物理基础,不同技术类型的项目会默认引用不同的类库,如果没有类库的话,即使知道这个名称空间中的类也没有办法使用。

类库的引用一共有两种方式,一种是DLL引用,又称为黑盒引用,没有它的源代码,然后直接进行引用;另一种是项目引用,又称为白盒引用,有源代码。

黑盒引用

首先我们来看一下黑盒引用的方式,也就是直接引用相应的DLL

我们可以直接添加自带的引用,可以右键右侧框中的引用,然后添加引用。

添加引用

通过在程序集的框架中可以搜索想要使用的配件,例如我们如果希望让我们的程序显示窗口,那么就可以导入System.Windows.Forms这样一个框架。

添加窗口显示配件

如果想要知道这个框架中具体有哪些功能,那么就可以去查找相应的MSDN文档。使用的时候,首先需要在代码中调用相应的名称空间,然后才能够正常使用。通过查找相应的方法,可以编写我们想要的代码,例如下面这段代码就可以生成一个窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form form = new Form();
form.ShowDialog();
}
}
}

输出结果:

窗口显示

如果你引用的类库是一个上层的类库的话,即该类库中还含有一些下层的类库,这就会导致其依赖关系非常复杂,在引用该上层类库的时候,也需要引用相应的下层类库。但是如果你引用的类库版本不匹配或者有着各种各样的问题,这就会导致非常难排除这些错误,从而使得程序无法运行。这种时候只有DLL,没有源代码,几乎可以说是“蒙着眼睛引用类库”,这对于很多大型项目而言是非常危险的。

为了解决上述问题,有一种叫做NuGet的技术,相当于把相应的类库封装成了一个包,可以直接一键进行导入,大大提高了效率和安全性。可以直接右键引用,然后选择管理NuGet程序包,就可以进行在线安装了。

白盒引用

对于白盒引用,我们可以创建属于自己的库,即建立自己的类库项目。

首先,创建一个Windows窗体应用,注意后缀有(.NET Framework),找到合适的位置保存,点击创建即可。

创建类库

我们可以写一个进行运算的类库:

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
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tools
{
public class Calculator
{
public static double Add(double a,double b)
{
return a + b;
}

public static double Sub(double a,double b)
{
return a - b;
}

public static double Mul(double a,double b)
{
return a * b;
}

public static double Div(double a,double b)
{
if (b == 0)
return double.PositiveInfinity; //返回正无穷大
else
return a / b;
}
}
}

在我们的项目中引用这个类库,然后就可以调用相应的成员函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tools;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
double result = Calculator.Div(5, 0);
Console.WriteLine(result);
}
}
}

输出结果:

1

依赖关系

通过前文我们可以发现,类或对象之间的耦合关系都属于依赖关系,优秀的程序追求“高内聚,低耦合”,简单来说就是把同一类的东西都严格放在一起,程序与程序之间的依赖关系比较低。在我们设计的时候,会涉及到许许多多的类,可以使用UML(通用建模语言)类图,可以使用图的方式将程序中的一会关系表达得非常清楚。

输出程序的类图

上图中就是我们只有一个输出的程序所对应的类图。

类、对象、类成员

(class)是现实世界事物的模型,是对现实世界事物进行抽象所得到的结果。其中事物包括“物质”(实体)与“运动”(逻辑),建模是一个去伪存真、由表及里的过程。

例如在现实世界中存在着战斗机和飞行员,这两个都是物质,可以将它们的属性抽象出来。飞行员驾驶着战斗机,也就代表着飞行员依赖着战斗机。

去伪存真指的是,即使现实中该事物有着某种属性或者功能,但是我们在程序中用不上,那我们就可以将其去除,保留下来的就是我们程序真正用的到的东西。

由表及里指的是,对于一个类,我们只对外界开放相应的接口,内部的运作过程是封装起来的。

类与对象的关系

基本概念

对象也叫实例,是类经过“实例化”后得到的内存中的实体。

对象和实例是一回事,“飞机”和“一架飞机”的区别在于前者是一种概念,后者是一个真实存在的东西,能够具体执行概念中的各种行为,拥有概念中的相应属性。同时,有些类是不能进行实例化的,例如“数学”,这只是一种概念,我们不能说“一个数学”,无法实例化出来一个具体的对象。

从本质上来讲,“对象”和“实例”是一回事,但是一般我们在讨论现实世界中的类的时候会说“对象”,在讨论程序世界中的类时会说“实例”。两种说法语境不同用不同的表达方式,但是指代的是同一个概念。不需要特别关注这个概念,二者并不太大区别,常常混用。

实例化

C#语言中使用new操作符来创建类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
(new Form()).ShowDialog();
}
}
}

输出结果:

窗口显示

通过使用new操作符来实例化了一个Form对象,也就是表单对象,后面的括号可以对其进行初始化,后面会具体讲到。为了验证该对象是否成功被实例化,调用ShowDialog()方法来进行验证。

引用变量

如果使用上述的方式进行实例化,每次只能对对象执行一次操作。如果我们想先要改变表单的标题再进行输出的话,使用上面的方法是远远不够的,因此就引入了引用的概念。

引用可以声明一个引用变量,让实例化的对象赋值给该变量,然后对该变量进行操作就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form myForm;
myForm = new Form();
myForm.Text = "好耶!";
myForm.ShowDialog();
}
}
}

输出结果:

引用变量实例化对象

通过这种方法,直接对引用变量进行操作,实现了给表单改名之后输出结果。

这里有一个比较形象的比喻:引用变量相当于孩子,实例相当于气球。如果只是单纯实例化的话,气球就飞走了,也可以把气球给孩子,让他牵着气球。总的来说,气球不一定有孩子牵着,多个孩子可以使用各自的绳子牵着同一个气球,也都可以通过一根绳子牵着气球。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form myForm1, myForm2;
myForm1 = new Form();
myForm2 = myForm1;
myForm1.Text = "好耶!";
myForm2.Text = "坏耶!";
myForm1.ShowDialog();
}
}
}

输出结果:

多引用变量指向同一实例

在这个例子中,一共有两个引用变量。第一个引用变量进行了实例化,并赋值给第二个引用变量,通过操作第二个引用变量也可以更改第一个引用变量的属性。这说明了,在C#中,可以实现多个引用变量指向同一对象,也就是前文所说的“多个孩子可以使用各自的绳子牵着同一个气球”。

类的三大成员

属性

属性(Property)用于存储数据,组合起来表示类或对象当前状态,可以根据一个对象的属性判断出它处于什么状态。

方法

方法(Method)由C语言中的函数(function)进化而来,表示类或对象“能做什么”。

在实际工作中,基本上$90%$的时间是在与方法打交道,因为它是“真正做事”、“构成逻辑”的成员。

事件

事件(Event)是类或对象通知其它类或对象的机制,为C#所特有的(Java通过其它办法来实现这个机制)。

特殊类或对象

C#中存在着一些特殊的类或对象,它们在成员方面上的侧重点不同:

  • 模型类或对象重在属性,如Entity Framework
  • 工具类或对象重在方法,如MathConsole
  • 通知类或对象重在事件,如各种Timer

静态成员与实例成员

静态(Static)成员在语义上表示它是“类的成员”,例如对于人类而言,有总数、平均身高、平均体重等属性,这些均为静态成员。

实例(非静态)成员在语义表示它是“对象的成员”,例如对于每个人而言,有身高、体重、年龄等属性,这些均为实例成员。

绑定(Binding)指的是编译器把一个成员与类或对象关联起来,分为早绑定和晚绑定,与C++中的多态概念相类似。

基本元素

基本元素有很多种,其中包括关键字(Keyword)、操作符(Operator)、标识符(Identifier)、标点符号、文本(字面值)、注释与空白,除了最后一种注释与空白以外,前面所有的元素也被称之为标记(Token)。

标识符

标识符的基本命名规则和其他的语言基本一致,可以用字母、下划线、数字的组合进行命名,同时数字不能作为标识符的开始。

对于C#而言,允许使用@字符作为前缀以使关键字能够用作标识符。C#也允许使用中文进行命名,只要符合Unicode标准的字符均可以使用,但是实际开发中尽量不要使用这种命名方式。

为了增强程序的可读性,我们一般会对变量名进行大小写规范的约束,下面介绍两种C#常用的命名规则。

驼峰法

驼峰法指的是在命名时,如果需要使用多个单词进行组合,第一个单词的首字母小写,其余单词的首字母大写。这种命名方法的变量很像是一只低着头的骆驼,大写字母相当于是驼峰,因此被称为驼峰法。

1
2
3
myVariable
myName
catAge

上述名称均属于驼峰法的命名规则,该方法一般用于对变量名的命名。

Pascal

Pascal法指的是每一个单词的首字母均大写,与Pascal中的命名规则很相似,因此被称为Pascal法。

1
2
3
MyVariable
MyName
CatAge

上述名称均属于Pascal法的命名规则,该方法一般用于对方法名、类名、命名空间等名字的命名。

文本(字面值)

整数

整数一共有两种类型,分别是intlong,二者都可以用于表示整数。它们之间的区别在于int类型使用$32$位bit位来进行表示,而long类型使用$64$位bit位来进行表示。需要注意的是,对于long类型,需要在后面加一个l或者L

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 2;
long y = 3l;
long z = 4L;
}
}
}

实数

实数指的是小数,如果想要表示小数需要使用floatdouble。二者的区别在于前者使用32bit位来进行表示,后者使用$64$位bit位来进行表示,float是单精度,double是双精度。在C#中,小数默认为双精度,所以在声明float类型时,需要在后面添加f或者F

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
float x = 3.0f;
float y = 4.0F;
double z = 5.0;
}
}
}

字符和字符串

字符只能由单个字符组成,使用char来表示,使用英文的单引号;字符串可以由任意个字符组成(也可以没有字符,我们将其称之为空串),使用string来表示,使用英文的双引号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
char c = 'a';
string str = "abc";
}
}
}

布尔

布尔使用bool来声明变量,只有两种值,分别为truefalse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
bool a = true;
bool b = false;
}
}
}

空(null

空值表示为null,表示空的意思,可以赋值给别的变量。

注释

注释有两种方式,一种是单行注释,一种是多行注释。前者使用//,后者使用/**/,被注释的内容不会被编译和执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
//这是一个单行注释

/*这是
一个
多行
注释*/
}
}
}

类型、变量和方法

类型

类型(Type)又称为数据类型(Data Type),在C#中,数据是有自己的类型的。

我们可以使用var来声明一个变量,令其存储数据,使用GetType()获取当前数据是什么类型的,调用Name属性可以显示相应的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
var x = 3;
Console.WriteLine(x.GetType().Name);
}
}
}

输出结果:

1
Int32

这说明将$3$赋值给x之后,其数据类型变为了$32$位的整数了。

一般来讲,不会使用var来定义,会使用明确的定义方式来进行数据的定义,例如直接int x = 3;

变量

变量是存放数据的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 3;
Console.WriteLine(x);
}
}
}

输出结果:

1
3

这个例子中,int是变量类型,需要与后面的数据相匹配,x是变量,然后将$3$赋值给x

方法

方法也被称为函数,是处理数据的逻辑,又称为“算法”。

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Calculator c = new Calculator();
int result = c.Add(3, 5);
Console.WriteLine(result);
}
}

class Calculator
{
public int Add(int a, int b)
{
int result = a + b;
return result;
}
}
}

输出结果:

1
8

上述代码中定义了一个Calculator类,该类中有一个Add方法,与C/C++中的函数是一个原理。该方法的返回值是int类型,因为最后要返回一个整数类型的值,该方法还有两个参数,调用时需要进行传递。需要注意一点,为了能够在类外调用该方法,需要将其声明为public,否则是无法访问的,访问时需要先实例化,才可以进行调用。

类型

概念

类型又称数据类型(Data Type),指的是数据在内存中存储时的“星号”,小内存容纳大尺寸数据会丢失精确度、发生错误,大内存容纳小尺寸数据会导致浪费,编程语言的数据类型与数据的数据类型不完全相同。

作用

数据类型可以说明存储此类型变量所需的内存空间大小,还有此类型的值可表示的最大、最小值范围。

对于不同的整数数据类型,其相关信息如下所示:

类型 范围 所占位数
sbyte $-128\sim127$ $8$
byte $0\sim255$ $8$
char $U+0000\sim U+ffff$ $16$
short $-32768\sim32767$ $16$
ushort $0\sim65535$ $16$
int $-2147483648\sim2147483647$ $32$
uint $0\sim4294967295$ $32$
long $-9223372036854775808\sim9223372036854775807$ $64$
ulong $0\sim18446744073709551615$ $64$

还有相应的浮点数数据类型,其相关信息如下所示:

类型 范围 精度 所占位数
float $\pm1.5e-45\sim\pm3.4e38$ $7$位小数 $32$
double $\pm5.0e-324\sim\pm1.7e308$ $15-16$位小数 $64$

变量

基本概念

表面上看,变量的用途是存储数据。实际上,变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量。

变量一共有七种,分别是:静态变量,实例变量(成员变量,字段),数组元素,值参数,引用参数,输出形参,局部变量。

狭义的变量指局部变量,因为其它种类的变量都有自己的预定名称。简单来讲,局部变量就是方法体(函数体)里声明的变量。

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
29
30
31
32
33
34
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student = new Student();
int x = 1, y = 2, z;
Console.WriteLine(student.Func(x, ref y, out z));
int[] array = new int[3];
array[0] = 1;
Console.WriteLine(array[0]);
}

class Student
{
public static int account; //学生数量
public string name; //学生姓名
public int Func(int a, ref int b, out int c)
{
c = 314;
b++;
return a + b;
}
}
}
}

输出结果:

1
2
4
1

上述代码中,Student类中的account是静态变量,name是实例变量。$16$行中定义了一个array数组,下一行中对其进数组元素进行赋值,要注意数组是从$0$开始计数的。$25$行的函数中,分别定义了值参数,引用参数和输出形参。对于这段代码而言,xyz等均为局部变量。

在使用变量之前,需要对其进行声明,使用有效的修饰符组合类型,并在后面写一下变量名即可。

总的来说,变量是以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域。

变量的默认值

开辟在堆区的变量默认值全部为$0$,对于局部变量而言,是不会有默认值的,因此需要显示赋值,否则无法通过编译。

常量

常量的值不能发生改变,可以通过下述方式进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
const int x = 100;
Console.WriteLine(x);
}
}
}

输出结果:

1
100

装箱和拆箱

装箱和拆箱使用的频率很低,因为它会造成性能损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 100;
//装箱
object y = x;
Console.WriteLine(y);
//拆箱
int z = (int)y;
Console.WriteLine(z);
}
}
}

输出结果:

1
2
100
100

装箱指的是将一个值变量封装成一个实例,开辟到堆区,object会存储堆区该变量的地址。拆箱时需要对其进行类型转化,以此来获取相应的值。很显然,装箱和拆箱会造成性能损失,因此现在使用的次数越来越少了。

方法

基本概念

方法(method)的前身是C/C++语言的函数(function)。在C#语言中,函数不可能独立于类(或结构体)之外,只有作为类(结构体)的成员时才被称为方法。在C++中没有该限制,函数可以独立存在,称之为“全局函数”。

下面编写一个程序,包含一个计算器类,该类中有三个方法,分别计算圆的面积,圆柱的体积,圆锥的体积。

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
29
30
31
32
33
34
35
36
37
38
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Console.WriteLine(calculator.GetCircleArea(10));
Console.WriteLine(calculator.GetCylinderVolume(10, 2));
Console.WriteLine(calculator.GetConeVolume(10, 2));
}

class Calculator
{
public double GetCircleArea(double radius)
{
return Math.PI * radius * radius;
}

public double GetCylinderVolume(double radius, double h)
{
return GetCircleArea(radius) * h;
}

public double GetConeVolume(double radius, double h)
{
return GetCylinderVolume(radius, h) / 3;
}
}
}
}

输出结果:

1
2
3
314.159265358979
628.318530717959
209.43951023932

上述代码中,对算法进行了分解,采用自顶向下,逐步求精的方法,实现了目标需求。

上面的三个方法均为实例方法,需要使用实例去对其调用,与对象绑定。如果在前面添加static关键字,将会变为静态方法,静态方法与类绑定,需要使用类名去调用 。

调用方法需要考虑两个参数,一个是”实际参数”(Argument),简称”实参”;另一个是“形式参数”(Parameter),简称“形参”。

前者可以理解为调用方法时的真实条件,后者只是一个形式上的条件,调用方法时argument列表要与parameter`列表相匹配,程序会将实参的值传递给形参。

构造器

构造器(constructor)是类型的成员之一,狭义的构造器指的是“实例构造器”(instance constructor)。简单来说,构造器和C++中的构造函数是一样的,也存在有参构造,无参构造和带默认参数的构造。具体案例如下所示:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student();
Console.WriteLine(student1.ID);
Console.WriteLine(student1.Name);
Student student2 = new Student(5);
Console.WriteLine(student2.ID);
Console.WriteLine(student2.Name);
Student student3 = new Student(100,"haoye");
Console.WriteLine(student3.ID);
Console.WriteLine(student3.Name);
}

class Student
{
//无参构造器
public Student()
{
this.ID = 1;
this.Name = "No name";
}

//有参构造器
public Student(int ID, string Name="Unnamed")
{
this.ID = ID;
this.Name = Name;
}

public int ID;
public string Name;
}
}
}

输出结果:

1
2
3
4
5
6
1
No name
5
Unnamed
100
haoye

上述代码中的this指代的是当前对象,可以区分相应的参数和该实例中的成员。

方法的重载

方法的重载和C/C++中的函数重载基本一样,同名函数可以根据参数的类型和数量进行区分,从而实现调用不同的函数。

方法签名(method signature)由方法的名称。类型形参的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。方法前面不包含返回类型。

实例构造函数前面由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。

重载决策(到底调用哪一个重载):用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。

操作符

操作符概览

操作符(Operator)也译为“运算符”。

操作符是用来操作数据的,被操作符操作的数据称为操作数(Operand)。

类别 运算符
基本 x.y f(x) a[x] x++ x– new typeof default checked unchecked delegate sizeof ->
一元 + - ! ~ ++x (T)x await &x *x
乘法 * / %
加减 + -
移位 << >>
关系和类型检测 < > <= >= is as
相等 == !=
逻辑“与” &
逻辑XOR ^
逻辑OR |
条件AND &&
条件OR ||
null合并 ??
条件 ?:
赋值和lambda表达式 = *= /= %= += -= <<= >>= &= ^= |= =>

上述操作符的运算优先级从下向上依次递增。

优先级与运算顺序

可以使用圆括号提高被括起来表达式的优先级,且圆括号可以嵌套。不像数学里有方括号和花括号,在C#语言里[]{}有专门的用途。

除了带有赋值功能的操作符,同优先级操作符都是由左向右进行运算,带有赋值功能的操作符的运算顺序是由右向左。与数学运算不同,计算机语言的同优先级运算没有“结合律”。

基本操作符

点操作符

该操作符可以调用相应的成员,可以看一下下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

输出结果:

1
Hello Wordld!

这段代码中,就使用了点操作符调用了Console类下的WriteLine成员函数。

方法调用操作符

在进行方法调用时,需要使用相应的操作符。

还是用上面这个代码举例,WriteLine就是调用输出一行内容的方法,括号中的内容是调用方法时传输的参数。

数组操作符

数组是C#中很重要的一个部分,创建数组的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int[] array1 = new int[10]; //创建一个10个元素的数组
int[] array2 = new int[3] { 1, 2, 3 }; //创建一个3个元素的数组
int[] array3 = new int[] { 5, 6, 7 }; //创建一个3个元素的数组
}
}
}

上面这三种方法均可以创建数组,$13$行的方式可以直接创建一个大小为$10$的整型数组,内部的值默认为$0$。$14$行创建了一个$3$个元素的数组,其初始值和后面花括号中的值一样。下一行没有显示说明一共有多少个元素,那么会自动开辟一块与花括号中元素数量一样的空间,初始值也是花括号中的值。

要注意的是,$14$行的方式方括号的值必须和花括号中的元素数量保持一致,否则会报错,其效果和下一行中的方法差不多,因此该方法很少被使用。数组下标是从$0$开始索引的,最大下标是$length-1$。

自加加、自减减操作符

这两个操作符等价于x += 1;x -= 1;,可以实现变量的加一和减一。

当操作符在变量后面时,会先执行语句,后进行相应的操作;如果操作符在变量前面时,会先进行自加加和自减减操作,再执行整个语句。

new操作符

new操作符可以帮助我们在内存中创建一个类型的实例,并立刻调用该实例的实例构造器。

这里先介绍一个新的关键字varC#是一个强数据类型的语言,一般来讲,我们可以直接显示声明变量的类型。对于var关键字而言,其作用与C/C++中的auto相类似,可以自动识别给它赋值的数据的类型,但是该变量的类型不能再发生改变了。同时,使用这种隐式类型化的方法必须在定义时就对其进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form myForm = new Form();
myForm.Text = "Hello World!";
myForm.ShowDialog();
}
}
}

输出结果:

窗体效果

使用这种方式可以直接实例化一个对象出来,还可以直接对其成员属性进行赋值操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form myForm = new Form() { Text = "Hello World!" };
myForm.ShowDialog();
}
}
}

输出结果:

窗体效果

可以直接在实例化后使用一个花括号初始化其成员属性。

new操作符还可以不使用任何数据类型,直接使用花括号初始化,这种被称之为匿名类型,可以使用var类型的变量进行接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
var test = new { Name = "好耶!", Age = 18 };
Console.WriteLine(test.Name);
Console.WriteLine(test.Age);
}
}
}

输出结果:

1
2
好耶!
18

上述代码中,使用var来接收了new操作符实例化的对象,并且可以正常输出其属性值。

一元操作符

指针操作符

C#中是存在指针的,使用方法和C/C++类似,但是需要允许使用不安全的代码。

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
29
30
31
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
unsafe
{
Student student;
Student* pStudent = &student;
pStudent->Id = 10;
pStudent->Name = "Test";
Console.WriteLine((*pStudent).Name);
Console.WriteLine((*pStudent).Id);
}
}

struct Student
{
public int Id;
public string Name;
}
}
}

输出结果:

1
2
Test
10

上述代码中创建了一个结构体,由于指针属于不安全的代码,因此需要用unsafe进行声明。定义一个指针用于指向Student类型的变量,会用到相应的指针操作符。

在第$19$行和第$20$行代码处,由于*操作符优先级低于.操作符,因此需要加上括号,来提高其优先级。

类型转换

先看一个简单的类型转换代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
string str1 = Console.ReadLine();
string str2 = Console.ReadLine();
int x = Convert.ToInt32(str1);
int y = Convert.ToInt32(str2);
Console.WriteLine(x + y);
}
}
}

输出结果:

1
2
3
2
3
5

这段代码实现了输入两个数字然后进行加法运算,如果不进行类型转换的话,将会变为字符串拼接。为了能够实现我们的需求,需要将输入的数据转换为合适的数据类型,再进行运算。

转换一共有三种,分别是隐式(implicit)类型转换,显式(explicit)类型转换和自定义类型转换操作符。

隐式类型转换

这种转换方式一般会在不丢失精度的转换、子类向父类的转换、装箱这些情况时触发。

int类型转换为long类型的时候,由于int类型是$4$个字节,long类型是$8$个字节,二者都是用于存储整数的,因此在该转换过程,会触发一次隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = int.MaxValue;
long y = x;
Console.WriteLine(y);
}
}
}

输出结果:

1
2147483647

隐式数值转换

上述的这些转换全都是不损失精度的隐式数值转换。

除此之外,当子类转换为父类的时候也是隐式类型转换:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Teacher t = new Teacher();
t.Eat();
t.Think();
t.Teach();
Human h = t;
h.Eat();
h.Think();
Animal a = h;
a.Eat();
}

class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
}

class Human : Animal
{
public void Think()
{
Console.WriteLine("Thinking...");
}
}

class Teacher : Human
{
public void Teach()
{
Console.WriteLine("Teaching...");
}
}
}
}

输出结果:

1
2
3
4
5
6
Eating...
Thinking...
Teaching...
Eating...
Thinking...
Eating...

显式类型转换

显式类型转换是有可能丢失精度(甚至发生错误)的转换,在拆箱,使用Convert类,ToString方法与各数据类型的Parse/TryParse方法时均为显式类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine(ushort.MaxValue);
uint x = 65536;
ushort y = (ushort)x;
Console.WriteLine(y);
}
}
}

输出结果:

1
2
65535
0

这段代码中,我们可以发现ushort能够存储的最大值是$65535$,如果我们把值为$65536$的变量赋值给一个ushort类型的变量的话,会造成报错,因此我们可以采用显式类型转换的方法,但是这会造成数据存储不下,不能正确的显示。

显式数值转换

上面这个图片中,包含了部分显式类型转换,而有些数据类型不能单纯通过该方式转换,这种情况可以借助Convert类和ToString方法来实现。

自定义类型转换

对于一些自己定义的类型,是没有办法通过上述方式对其进行类型转换的,对此可以自定义类型转换。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Centigrade c1 = new Centigrade();
c1.value = 100;
Kelvin k1 = new Kelvin();
k1 = (Kelvin)c1;
Console.WriteLine(k1.value);
Kelvin k2 = new Kelvin();
k2.value = 100;
Centigrade c2 = new Centigrade();
c2 = (Centigrade)k2;
Console.WriteLine(c2.value);
}

class Centigrade
{
public double value;

public static explicit operator Centigrade(Kelvin k)
{
Centigrade c = new Centigrade();
c.value = k.value - 274.15;
return c;
}
}

class Kelvin
{
public double value;

public static explicit operator Kelvin(Centigrade c)
{
Kelvin k = new Kelvin();
k.value = c.value + 274.15;
return k;
}
}
}
}

输出结果:

1
2
374.15
-174.15

上述代码实现了两个类,分别用于存储摄氏度和开氏度,类的内部写了两个自定义类型转换的函数,用于二者的转换。

同时,也可以声明为隐式类型转换,只需要将转换方式改变即可:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Centigrade c1 = new Centigrade();
c1.value = 100;
Kelvin k1 = new Kelvin();
k1 = c1;
Console.WriteLine(k1.value);
Kelvin k2 = new Kelvin();
k2.value = 100;
Centigrade c2 = new Centigrade();
c2 = k2;
Console.WriteLine(c2.value);
}

class Centigrade
{
public double value;

public static implicit operator Centigrade(Kelvin k)
{
Centigrade c = new Centigrade();
c.value = k.value - 274.15;
return c;
}
}

class Kelvin
{
public double value;

public static implicit operator Kelvin(Centigrade c)
{
Kelvin k = new Kelvin();
k.value = c.value + 274.15;
return k;
}
}
}
}

输出结果:

1
2
374.15
-174.15

乘法和加法操作符

乘法和加法操作符统称为算术运算操作符,这些操作符与数学运算中基本类似,但是有几点需要注意一下。首先算术运算符与其操作的数据类型是相关的,操作不同的数据类型时,其行为也不一样;第二点是C#中有一个%操作符,用于取余数,该操作符在数学中是没有的;第三点是在使用算术操作符时需要注意“数值提升”。

在整数乘法中,如果使用checked且积超出结果类型的范围,则会引发System.OverflowException错误;如果使用unchecked,则不报告溢出并且结果类型范围外的任何有效高序位都被放弃。

移位操作符

移位操作符有两,分别是<<>>,二者的作用分别是对二进制串进行左移和右移操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 5;
Console.WriteLine(x << 1);
Console.WriteLine(x >> 1);
}
}
}

输出结果:

1
2
10
2

每向左移动一位,其实就相当于乘以$2$,向右移动一位就相当于是除以$2$。

如果当前操作的是一个正数,右移补进来的是$0$,如果是负数,右移补进来的是$1$。

语句详解

声明语句

局部变量声明

局部变量在声明时可以声明一个或多个局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 5;
int y;
y = 10;
}
}
}

上述的两种定义方式虽然都可以实现变量的声明和赋值,但其本质上是不一样的。

x的声明是调用了其初始化器;y的声明是先声明了一个变量y,再对其进行赋值操作。

局部常量声明

局部常量无法在后续改变其值,并且在初始定义时,需要赋初始值,否则无法通过编译器的编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)

{
const double PI = 3.1415926;
}
}
}

块语句

block用于在只允许使用单个语句的上下文中编写多条语句。

1
2
3
4
block:
{
statement-list
}

块语句被看做完整的一条语句,因此后面不需要使用分号。

if语句

if语句根据布尔表达式的值选择要执行的雨具。

1
2
3
if-statement:
if(boolean-expression) embedded-statement
if(boolean-expression) embedded-statement else embedded-statement

else部分与语法允许的、词法上最接近的上一个if语句相关联。

现在写一个成绩评定的案例:

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
29
30
31
32
33
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 55;
if (score >= 0 && score <= 100)
{
if (score >= 60)
{
Console.WriteLine("Pass");
}
else
{
Console.WriteLine("Failed");
}
}
else
{
Console.WriteLine("Input Error!");
}
}
}
}

switch语句

switch语句选择一个要执行的语句列表,此列表具有一个相关联的switch标签,它对应于switch表达式的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch-statement:
switch(expression) switch-block

switch-block:
{
switch-sections
}

switch-sections:
switch-section
switch-sections switch-section

switch-section:
swicth-labels statement-list

switch-labels:
switch-label
switch-labels switch-label

switch-label:
case constant-expression:
default:

switch-statement包含关键字switch,后面带括号的表达式(称为switch表达式),然后是一个switch-blockswitch-block包含零个或多个括在大括号内的switch-section。每个switch section包含一个或多个switch-labels,后接一个statement-list

switch语句的主导类型(governing type)由switch表达式确定。

如果switch表达式的类型为sbytebyteshortushortintuintlongulongboolcharstringenum-type,或者是对应于以上某种类型的可以为null的类型,则该类型就是switch语句的主导类型。

使用switch语句实现分数评级功能:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 55;
switch (score / 10)
{
case 10:
if (score == 100)
{
goto case 8;
}
else
{
goto default;
}
case 8:
case 9:
Console.WriteLine("A");
break;
case 6:
case 7:
Console.WriteLine("B");
break;
case 4:
case 5:
Console.WriteLine("C");
break;
case 0:
case 1:
case 2:
case 3:
Console.WriteLine("D");
break;
default:
Console.WriteLine("Error!");
break;
}
}
}
}

输出结果:

1
C

try语句

try语句提供一种机制,用于捕捉在块的执行期间发生的各种异常。此外,try语句还能让您指定一个代码块,并保证当控制离开tyr语句时,总是先执行该代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try-statement:
try block catch-clauses
try block finally-clause
try block catch-clauses finally-clause

catch-clauses:
specific-catch-clauses general-catch-clause_opt
specific-catch-clauses_opt general-catch-clause

specific-catch-clauses:
specific-catch-clause
specific-catch-clauses specific-catch-clause

specific-catch-clause:
catch(class-tvpe identifier_opt ) block

general-catch-clause:
catch block
finally-clause:
fina1ly block

有三种可能的try语句形式:

  • 一个try块后接一个或多个catch块。
  • 一个try块后接一个finally块。
  • 一个try块后接一个或多个catch块,后面再跟一个finally块。

迭代语句

迭代语句重复执行嵌入语句。

while语句

while语句按不同条件执行一个嵌入语句零次或多次。

1
2
while-statement:
while(boolean-expression) embedded-statement;

while语句按如下规则执行:

  • 计算boolean-expression
  • 如果布尔表达式为真,控制将转到嵌入语句。当(如果)控制到达嵌入语句的结束点(可能是通过执行一个continue语句)时,控制将转到while语句的结束点。
  • 如果布尔表达式为假,控制将转到while语句的结束点。

现在做一个小游戏,用户每次输入两个数字,如果二者之和为一百,将会加一分,反正会游戏结束并公布当前得分。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 0;
bool canContinue = true;
while (canContinue)
{
Console.WriteLine("Please input first number:");
string str1 = Console.ReadLine();
int x = int.Parse(str1);

Console.WriteLine("Please input second number:");
string str2 = Console.ReadLine();
int y = int.Parse(str2);

int sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct!{0}+{1}={2}", x, y, sum);
}
else
{
canContinue = false;
Console.WriteLine("Error!{0}+{1}={2}", x, y, sum);
}
}
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER!");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Please input first number:
99
Please input second number:
1
Correct!99+1=100
Please input first number:
50
Please input second number:
50
Correct!50+50=100
Please input first number:
1
Please input second number:
1
Error!1+1=2
Your score is 2.
GAME OVER!

do语句

do语句按不同条件执行一个嵌入语句一次或多次。

1
2
do-statement:
do embedded-statement while (boolean-expression);

do语句按如下规则执行:

  • 控制转到嵌入语句。
  • 当(如果)控制到达嵌入语句的结束点(可能是由于执行了一个continue语句)时,计算boolean-expression。如果布尔表达式为真,控制将转到do语句的起点。否则,控制转到do语句的结束点。

我们使用do语句完成上述的小游戏:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 0;
do
{
Console.WriteLine("Please input first number:");
string str1 = Console.ReadLine();
int x = int.Parse(str1);

Console.WriteLine("Please input second number:");
string str2 = Console.ReadLine();
int y = int.Parse(str2);

sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct!{0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error!{0}+{1}={2}", x, y, sum);
}
} while (sum == 100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER!");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Please input first number:
99
Please input second number:
1
Correct!99+1=100
Please input first number:
50
Please input second number:
50
Correct!50+50=100
Please input first number:
1
Please input second number:
1
Error!1+1=2
Your score is 2.
GAME OVER!

跳转语句

continue语句会放弃当前这次循环,立刻开始一次新的循环。

break语句会直接结束这个循环。

在上面的小游戏中,如果输入有问题的话,程序会直接崩溃。为了避免崩溃,我们可以使用try语句来进行判断,同时需要引入continue

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 100;
do
{
Console.WriteLine("Please input first number:");
string str1 = Console.ReadLine();
int x = 0;
try
{
x = int.Parse(str1);
}
catch
{
Console.WriteLine("First number has problem! Restart.");
continue;
}

Console.WriteLine("Please input second number:");
string str2 = Console.ReadLine();
int y = 0;
try
{
y = int.Parse(str2);
}
catch
{
Console.WriteLine("Second number has problem! Restart.");
continue;
}

sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct!{0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error!{0}+{1}={2}", x, y, sum);
}
} while (sum == 100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER!");
}
}
}

输出结果:

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
29
30
Please input first number:
abc
First number has problem! Restart.
Please input first number:
123
Please input second number:
999999999999999999999
Second number has problem! Restart.
Please input first number:
5
Please input second number:
95
Correct!5+95=100
Please input first number:
10
Please input second number:
90
Correct!10+90=100
Please input first number:
100
Please input second number:
0
Correct!100+0=100
Please input first number:
1
Please input second number:
1
Error!1+1=2
Your score is 3.
GAME OVER!

现在我们想要增加一个新的功能,当用户输入End的时候,可以立刻结束游戏。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 100;
do
{
Console.WriteLine("Please input first number:");
string str1 = Console.ReadLine();
if (str1.ToLower() == "end")
{
break;
}
int x = 0;
try
{
x = int.Parse(str1);
}
catch
{
Console.WriteLine("First number has problem! Restart.");
continue;
}

Console.WriteLine("Please input second number:");
string str2 = Console.ReadLine();
if (str2.ToLower() == "end")
{
break;
}
int y = 0;
try
{
y = int.Parse(str2);
}
catch
{
Console.WriteLine("Second number has problem! Restart.");
continue;
}

sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct!{0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error!{0}+{1}={2}", x, y, sum);
}
} while (sum == 100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER!");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
Please input first number:
5
Please input second number:
95
Correct!5+95=100
Please input first number:
a
First number has problem! Restart.
Please input first number:
end
Your score is 1.
GAME OVER!

上述代码中,使用了ToLower方法实现了字符串转换为小写,可以适用于所有形式的End

for语句

for语句计算一个初始化表达式序列,然后当某个条件为真时,重复执行相关的嵌入语句并计算一个迭代表达式序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Hello World!");
}
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!

上述代码使用for循环输出了打印了十次Hello World!

foreach语句

foreach语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次相关的嵌入语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int[] intArray = new int[] { 1, 2, 3, 4, 5, 6 };
foreach (var i in intArray)
{
Console.WriteLine(i);
}
}
}
}

输出结果:

1
2
3
4
5
6
1
2
3
4
5
6

字段

定义

字段(filed)是一种表示与对象或类型(类与结构体)关联的变量。

字段是类型的成员,旧称“成员变量”。

与对象关联的字段也被称作“实例字段”。

与类型关联的字段称为“静态字段”,由static修饰。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu1 = new Student();
stu1.Age = 18;
stu1.Score = 90;

Student stu2 = new Student();
stu2.Age = 20;
stu2.Score = 85;

Student.ReportAmount();
}

class Student
{
public int Age;
public int Score;

public static int AverageAge;
public static int AverageScore;
public static int Amount;

public Student()
{
Student.Amount++;
}

public static void ReportAmount()
{
Console.WriteLine(Student.Amount);
}
}
}
}

输出结果:

1
2

上述代码创建了一个学生类,每实例化出来一个对象,都会让学生总数增加一个,该变量使用static进行声明。

同时也可以改写成下述代码获取其他的参数。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
List<Student> stuList = new List<Student>();
for(int i=0;i<100;i++)
{
Student student = new Student();
student.Age = 25;
student.Score = i;
stuList.Add(student);
}

int totalAge = 0;
int totalScore = 0;
foreach(var student in stuList)
{
totalAge += student.Age;
totalScore += student.Score;
}

Student.AverageAge = totalAge / Student.Amount;
Student.AverageScore = totalScore / Student.Amount;

Student.ReportAmount();
Student.ReportAverageAge();
Student.ReportAverageScore();
}

class Student
{
public int Age;
public int Score;

public static int AverageAge;
public static int AverageScore;
public static int Amount;

public Student()
{
Student.Amount++;
}

public static void ReportAmount()
{
Console.WriteLine(Student.Amount);
}

public static void ReportAverageAge()
{
Console.WriteLine(Student.AverageAge);
}

public static void ReportAverageScore()
{
Console.WriteLine(Student.AverageScore);
}
}
}
}

输出结果:

1
2
3
100
25
49

声明

字段声明带有分号,但它不是语句。

字段的名字一定是名词。

在声明字段时,可以直接对其初始化,这与在构造函数中的初始化效果一致。

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
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu = new Student();
Console.WriteLine(stu.Age);
}

class Student
{
public int Age = 18;
}
}
}

输出结果:

1
18

上述代码在声明字段时就对其进行赋值了,这样实例化出来的所有对象的年龄均为$18$。

类中也可以声明静态构造函数,这样的函数只会在程序最开始执行一次:

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
29
30
31
32
33
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu = new Student();
Console.WriteLine(stu.Age);
Console.WriteLine(Student.Amount);
}

class Student
{
public int Age = 18;

public static int Amount;

static Student()
{
Student.Amount = 1;
}
}
}
}

输出结果:

1
2
18
1

在声明字段时,可以使用readonly修饰符,该修饰符时只读修饰符。对于只读字段而言,只有一次赋值机会,即构造函数中进行赋值操作。

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
29
30
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu = new Student(314);
Console.WriteLine(stu.ID);
}

class Student
{
public readonly int ID;

public Student(int ID)
{
this.ID = ID;
}
}
}
}

输出结果:

1
314

初始值

无显式初始化时,字段获得其类型的默认值,所以字段“永远都不会未被初始化”。

实例字段初始化的时机是对象创建时。

静态字段初始化的时机是类型被加载(load)时。

属性

定义

属性(property)是一种用于访问对象或类型的特征的成员,特征反映了状态。

属性是字段的自然扩展。

从命名上看,filed更偏向于实例对象在内存中的布局,property更偏向于反映现实世界对象的特征。

对外而言,可以暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的。

对内而言,可以保护字段不被非法值“污染”。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
try
{
Student stu1 = new Student();
stu1.SetAge(20);
Console.WriteLine(stu1.GetAge());

Student stu2 = new Student();
stu2.SetAge(314);
Console.WriteLine(stu2.GetAge());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

class Student
{
private int age;

public int GetAge()
{
return this.age;
}

public void SetAge(int age)
{
if (age >= 0 && age <= 120)
{
this.age = age;
}
else
{
throw new Exception("Age value has error!");
}
}
}
}
}

输出结果:

1
2
20
Age value has error!

通过将字段声明为私有类型的方法,可以有效避免数据被污染,在设置值的时候可以对其进行检查。

为了降低变成复杂性,推出了getset,可以快速获取和设置属性值。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
try
{
Student stu1 = new Student();
stu1.Age = 20;
Console.WriteLine(stu1.Age);

Student stu2 = new Student();
stu2.Age = 314;
Console.WriteLine(stu2.Age);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

class Student
{
private int age;

public int Age
{
get
{
return this.age;
}

set
{
if (value >= 0 && value <= 120)
{
this.age = value;
}
else
{
throw new Exception("Age value has error!");
}
}
}
}
}
}

输出结果:

1
2
20
Age value has error!

声明

属性的声明可以使用完整的声明,也可以使用简略的声明。

上面使用的方式是完整声明,具有很强的鲁棒性。

对于简略的声明,其代码非常简洁:

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
29
30
31
32
33
34
35
36
37
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
try
{
Student stu1 = new Student();
stu1.Age = 20;
Console.WriteLine(stu1.Age);

Student stu2 = new Student();
stu2.Age = 314;
Console.WriteLine(stu2.Age);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

class Student
{
public int Age { get; set; }
}
}
}

1
2
20
314

这种方法一般用于传递值,不能对属性起到很好的保护作用。

参数

值参数

值参数是我们平时用的最多的参数种类,声明时不带修饰符的形参是值参数。一个值形参对应于一个局部变量,只是它的初始值来自该方法调用所提供的相应实参。

当形参是值形参时,方法调用中的对应实参必须是表达式,并且它的类型可以隐式转换为形参的类型。

允许方法将新值赋给值参数。这样的赋值只影响由该值形参表示的局部存储位置,而不会影响在方法调用时由调用方给出的实参。

值类型

值参数相当于创建了一个变量的副本,对值参数的操作永远不会影响到变量的值。

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
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 0;
AddOne(x);
Console.WriteLine("Main value:{0}", x);
}

static void AddOne(int x)
{
x++;
Console.WriteLine("AddOne value:{0}", x);
}
}
}

输出结果:

1
2
AddOne value:1
Main value:0

在上述结果中,将参数进行传入,即使方法中的参数值发生了改变,也不会影响传入的值。

引用类型

当使用值参数传递一个引用类型的变量时,之操作对象,不创建新对象。

所传递的对象还是那个对象,但对象里的值(字段/属性已经改变)。

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
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu = new Student() { Name = "Tom" };
Student.UpdateObject(stu);
Console.WriteLine("Student:{0}", stu.Name);
Student.CreateObject(stu);
Console.WriteLine("Student:{0}", stu.Name);
}

class Student
{
public string Name { get; set; }

public static void UpdateObject(Student stu)
{
stu.Name = "Bob";
Console.WriteLine("Update:{0}", stu.Name);
}

public static void CreateObject(Student stu)
{
stu = new Student() { Name = "Alice" };
Console.WriteLine("Create:{0}", stu.Name);
}
}
}
}

输出结果:

1
2
3
4
Update:Bob
Student:Bob
Create:Alice
Student:Bob

上述例子中,如果更改了对象的属性值,会造成初始变量的变化。但是如果用值参数指向了一个新的参数,并不会对传入的值参数造成任何影响。

引用参数

引用形参是用ref修饰符声明的形参。与值形参不同,引用形参并不创建新的存储位置。相反,引用形参表示的存储位置恰是在方法调用中作为实参给出的那个变量所表示的存储位置。

当形参为引用形参时,方法调用中的对应实参必须由关键字ref并后接一个与形参类型想用的variable-reference组成。变量在可以作为引用形参传递之前,必须先明确赋值。

在方法内部,引用形参始终被认为是明确赋值的。

值类型

引用参数并不创建变量的副本,使用ref修饰符显式指出——此方法的副作用是改变实际参数的值。

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
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int x = 0;
AddOne(ref x);
Console.WriteLine("Main value:{0}", x);
}

static void AddOne(ref int x)
{
x++;
Console.WriteLine("AddOne value:{0}", x);
}
}
}

引用类型

当使用引用参数处理引用类型的变量时,创建对象也会对引用参数产生相应的影响。

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
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu = new Student() { Name = "Tom" };
Student.UpdateObject(ref stu);
Console.WriteLine("Student:{0}", stu.Name);
Student.CreateObject(ref stu);
Console.WriteLine("Student:{0}", stu.Name);
}

class Student
{
public string Name { get; set; }

public static void UpdateObject(ref Student stu)
{
stu.Name = "Bob";
Console.WriteLine("Update:{0}", stu.Name);
}

public static void CreateObject(ref Student stu)
{
stu = new Student() { Name = "Alice" };
Console.WriteLine("Create:{0}", stu.Name);
}
}
}
}

输出结果:

1
2
3
4
Update:Bob
Student:Bob
Create:Alice
Student:Alice

输出参数

使用out修饰符声明的形参是输出形参。类似于引用形参,输出形参不创建新的存储位置。相反,输出形参表示的存储位置恰是在该方法调用中作为实参给出的那个变量所表示的存储位置。

当形参为输出形参时,方法调用中的相应实参必须由关键字out并后接一个与形参类型相同的variable-reference组成。变量在可以作为输出形参传递之前不一定需要明确赋值,但是在将变量作为输出形参传递的调用之后,该变量被认为是明确赋值的。

在方法内部,与局部变量相同,输出形参最初被认为是未赋值的,因而必须在使用它的值之前明确赋值。

在方法返回之前,该方法的每个输出形参都必须明确赋值。

值类型

在处理值类型的变量时,输出参数并不创建变量的副本。

方法体内必须要有对输出变量的赋值的操作。

使用out修饰符显式指出——此方法的副作用是通过参数向外输出值。

从语义上来讲——ref是为了“改变”,out是为了“输出”。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
double x = 0;
bool b1 = DoubleParser.TryParse("314", out x);
if (b1 == true)
{
Console.WriteLine(x);
}
else
{
Console.WriteLine("Error!");
}

bool b2 = DoubleParser.TryParse("Abc", out x);
if (b2 == true)
{
Console.WriteLine(x);
}
else
{
Console.WriteLine("Error!");
}
}

class DoubleParser
{
public static bool TryParse(string input, out double result)
{
try
{
result = double.Parse(input);
return true;
}
catch
{
result = 0;
return false;
}
}
}
}
}

输出结果:

1
2
314
Error!

上述例子中写了一个用于判断转换值是否能合法转换的代码,由于返回值被是否能够正常转换的布尔类型值所占用,因此需要使用一个输出参数获取转换之后的值。

引用类型

引用类型的输出参数与上述没有什么太大区别,可以看一下下面的例子:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student stu;
bool b = Create("Biggleswroth", 18, out stu);
if (b == true)
{
Console.WriteLine("Name:{0} Age:{1}", stu.Name, stu.Age);
return;
}
else
{
Console.WriteLine("Error!");
return;
}
}

class Student
{
public int Age { get; set; }
public string Name { get; set; }
}

static bool Create(string stuName, int stuAge, out Student stu)
{
stu = null;
if (stuName == null)
{
return false;
}
if (stuAge < 0 || stuAge > 80)
{
return false;
}
stu = new Student();
stu.Name = stuName;
stu.Age = stuAge;
return true;
}
}
}

输出结果:

1
Name:Biggleswroth    Age:18

数组参数

必须是形参列表中的最后一个,由params修饰。例如String.Format方法和String.Split方法。

现在需要实现一个数组中的所有元素加和后返回,如果直接使用数组进行传递的话,代码如下所示:

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
29
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int[] myIntArray = new int[] { 1, 2, 3, 4, 5, 6, 7 };
int result = CalculateSum(myIntArray);
Console.WriteLine(result);
}

static int CalculateSum(int[] intArray)
{
int sum = 0;
foreach (var i in intArray)
{
sum += i;
}
return sum;
}
}
}

输出结果:

1
28

除此之外,还可以使用数组参数进行传递,如下所示:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int result = CalculateSum(1, 2, 3, 4, 5, 6, 7);
Console.WriteLine(result);
}

static int CalculateSum(params int[] intArray)
{
int sum = 0;
foreach (var i in intArray)
{
sum += i;
}
return sum;
}
}
}

输出结果:

1
28

使用这种方式可以不用提前声明一个数组,编译器会直接将输入的数据编写成一个数组进行相应的操作。

具名参数

具名参数可以让参数的位置不再受约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
PrintInfo(age: 18, name: "Bigglesworth");
}

static void PrintInfo(string name, int age)
{
Console.WriteLine("Name:{0}", name);
Console.WriteLine("Age:{0}", age);
}
}
}

输出结果:

1
2
Name:Bigglesworth
Age:18

使用具名参数可以很好的提高代码的可读性,同时调用时不需要再严格按照原有的顺序,使得代码编写更加灵活。

可选参数

参数因为具有默认值而变得“可选”,但是在实际使用中不推荐使用可选参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
PrintInfo(age: 25);
PrintInfo(age: 18, name: "Bigglesworth");
}

static void PrintInfo(string name = "Bigglesworth", int age = 18)
{
Console.WriteLine("Name:{0}", name);
Console.WriteLine("Age:{0}", age);
}
}
}

输出结果:

1
2
3
4
Name:Bigglesworth
Age:25
Name:Bigglesworth
Age:18

可以在参数处为其添加默认值,这样传入参数时就可以不对其进行设置,程序运行时会直接将提前写好的默认值作为参数来使用。

扩展方法(this参数)

方法必须是公有、静态的,即被public static所修饰。

必须是形参列表中的第一个由this修饰。

必须由一个静态类(一般类名为SomeTypeExtension)来统一收纳对SomeType类型的扩展方法。

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
double x = 3.14159265358;
double y = x.Round(4);
Console.WriteLine(y);
}
}

static class DoubleExtension
{
public static double Round(this double input, int digits)
{
double result = Math.Round(input, digits);
return result;
}
}
}

输出结果:

1
3.1416

简单来说,使用扩展方法可以为目标数据类型“追加”新的方法。

委托

基本概念

委托(delegate)是函数指针的“升级版”。

变量(数据)是以某个地址为起点的一段内存中所存储的值。

函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令。

对函数(方法)的调用分为直接调用和间接调用,直接调用是通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并开始执行;间接调用是通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行。

Action

使用Action可以实现类似于函数指针的效果,例如下面这段程序:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
calculator.Report();
action.Invoke();
action();
}
}

class Calculator
{
public void Report()
{
Console.WriteLine("I have 3 methods.");
}

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

public int Sub(int a, int b)
{
int result = a - b;
return result;
}
}
}

输出结果:

1
2
3
I have 3 methods.
I have 3 methods.
I have 3 methods.

上述代码通过三种方式分别调用了对应的方法,第一种是使用对象直接进行调用,剩下两种是使用Action实例化的对象进行间接调用。

Action委托只能指向没有返回值的方法,如果想要指向有返回值的方法需要使用Func

Func

为了能够调用AddSub,需要使用Func委托进行间接调用:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
Func<int, int, int> func2 = new Func<int, int, int>(calculator.Sub);

int x = 100;
int y = 200;
int z = 0;

z = func1.Invoke(x, y);
Console.WriteLine(z);
z = func2.Invoke(x, y);
Console.WriteLine(z);
}
}

class Calculator
{
public void Report()
{
Console.WriteLine("I have 3 methods.");
}

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

public int Sub(int a, int b)
{
int result = a - b;
return result;
}
}
}

输出结果:

1
2
300
-100

Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);这句代码中,第一个尖括号中的三个参数分别表示:函数的第一个参数类型,函数的第二个参数类型,函数的返回值类型,后面的尖括号也同理,这样就可以直接进行调用了。

委托的声明

委托是一种类(class),类是数据类型所以委托也是一种数据类型。

它的声明方式与一般的类不同,主要是为了照顾可读性和C/C++传统。

声明委托时一定要注意委托的位置,避免写错地方结果声明称嵌套类型。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
public delegate double Calc(double x, double y);

internal class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Calc calc1 = new Calc(calculator.Add);
Calc calc2 = new Calc(calculator.Sub);
Calc calc3 = new Calc(calculator.Mul);
Calc calc4 = new Calc(calculator.Div);

double a = 100;
double b = 200;
double c = 0;

c = calc1.Invoke(a, b);
Console.WriteLine(c);
c = calc2.Invoke(a, b);
Console.WriteLine(c);
c = calc3.Invoke(a, b);
Console.WriteLine(c);
c = calc4.Invoke(a, b);
Console.WriteLine(c);
}
}

class Calculator
{
public double Add(double x, double y)
{
return x + y;
}

public double Sub(double x, double y)
{
return x - y;
}

public double Mul(double x, double y)
{
return x * y;
}

public double Div(double x, double y)
{
return x / y;
}
}
}

输出结果:

1
2
3
4
300
-100
20000
0.5

委托所封装的方法必须“类型兼容”,即返回值的数据类型一致,参数列表在个数和数据类型上一致(参数名不需要一样)。

委托的一般使用

委托可以把一个方法作为参数传给另一个方法。

模板方法

模板方法“借用”指定的外部方法来产生结果,相当于“填空题”,常位于代码中部,且委托有返回值。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
ProductFactory productFactory = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();

Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);

Box box1 = wrapFactory.WrapProduct(func1);
Box box2 = wrapFactory.WrapProduct(func2);

Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}

class Product
{
public string Name { get; set; }
}

class Box
{
public Product Product { get; set; }
}

class WrapFactory
{
public Box WrapProduct(Func<Product> getProduct)
{
Box box = new Box();
Product product = getProduct.Invoke();
box.Product = product;
return box;
}
}

class ProductFactory
{
public Product MakePizza()
{
Product product = new Product();
product.Name = "Pizza";
return product;
}

public Product MakeToyCar()
{
Product product = new Product();
product.Name = "Toy Car";
return product;
}
}
}

输出结果:

1
2
Pizza
Toy Car

上述代码相当于将函数作为参数传递其他函数进行调用,通过改变外面的委托,从而实现不同的调用。

回调方法

回调(callback)方法调用指定的外部方法,相当于“流水线”,常位于代码末尾,且委托无返回值。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
ProductFactory productFactory = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();

Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);

Logger logger = new Logger();
Action<Product> log = new Action<Product>(logger.Log);

Box box1 = wrapFactory.WrapProduct(func1, log);
Box box2 = wrapFactory.WrapProduct(func2, log);

Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}

class Logger
{
public void Log(Product product)
{
Console.WriteLine("Product '{0}' created at{1}. Price is {2}.", product.Name, DateTime.UtcNow, product.Price);
}
}

class Product
{
public string Name { get; set; }
public double Price { get; set; }
}

class Box
{
public Product Product { get; set; }
}

class WrapFactory
{
public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallback)
{
Box box = new Box();
Product product = getProduct.Invoke();
if (product.Price > 50)
{
logCallback(product);
}
box.Product = product;
return box;
}
}

class ProductFactory
{
public Product MakePizza()
{
Product product = new Product();
product.Name = "Pizza";
product.Price = 12;
return product;
}

public Product MakeToyCar()
{
Product product = new Product();
product.Name = "Toy Car";
product.Price = 100;
return product;
}
}
}

输出结果:

1
2
3
Product 'Toy Car' created at2024/2/1 10:56:49. Price is 100.
Pizza
Toy Car

上述代码相当于写了一个日志,当产品的价格大于$50$时才会触发调用。

委托注意事项

委托是一种易使用,难精通且功能强大的东西,一旦被滥用会造成很严重的后果。

缺点:

  1. 这是一种方法级别的紧耦合,现实工作中要慎之又慎。
  2. 使可读性下降、debug的难度增加。
  3. 把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护。
  4. 委托使用不当有可能造成内存泄漏和程序性能下降。

委托的高级使用

多播委托

多播(multicast)委托指的是用一个委托封装多个方法,且多播委托的执行顺序是按照封装的顺序执行的。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Work work = new Work();
Action action1 = new Action(work.DoHomework);
Action action2 = new Action(work.HaveDinner);
Action action3 = new Action(work.Sleep);

action1.Invoke();
action2.Invoke();
action3.Invoke();

Console.WriteLine("__________________");

Action action = action2 + action1 + action3;

action.Invoke();
}
}

class Work
{
public void DoHomework()
{
Console.WriteLine("正在做作业……");
}

public void HaveDinner()
{
Console.WriteLine("正在吃晚饭……");
}

public void Sleep()
{
Console.WriteLine("正在睡觉……");
}
}
}

输出结果:

1
2
3
4
5
6
7
正在做作业……
正在吃晚饭……
正在睡觉……
__________________
正在吃晚饭……
正在做作业……
正在睡觉……

可以像上述代码一样,将多个委托放在一起,封装成一个新的委托,这种委托被称为多播委托。

隐式异步调用

先来介绍一下同步和异步的概念,同步指的是两件事情先做完第一件再做第二件,异步指的是两个事情同时做。

每一个运行的程序都是一个进程(process),每个进程可以有一个或者多个线程(thread),同步调用是在同一线程内,异步调用的底层机理是多线程。

串行、同步、单线程是同一意思。

并行、异步、多线程是同一意思。

对于同步调用而言,其代码和运行结果如下所示:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student() { Name = "甲" };
Student student2 = new Student() { Name = "乙" };
Student student3 = new Student() { Name = "丙" };

Action action1 = new Action(student1.Work);
Action action2 = new Action(student2.Work);
Action action3 = new Action(student3.Work);

action1.Invoke();
action2.Invoke();
action3.Invoke();
}
}

class Student
{
public string Name { get; set; }

public void Work()
{
this.DoHomework();
this.HaveDinner();
this.Sleep();
}

public void DoHomework()
{
Console.WriteLine(this.Name + "正在做作业……");
}

public void HaveDinner()
{
Console.WriteLine(this.Name + "正在吃晚饭……");
}

public void Sleep()
{
Console.WriteLine(this.Name + "正在睡觉……");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
甲正在做作业……
甲正在吃晚饭……
甲正在睡觉……
乙正在做作业……
乙正在吃晚饭……
乙正在睡觉……
丙正在做作业……
丙正在吃晚饭……
丙正在睡觉……

同步调用是按照甲乙丙的顺序依次进行调用的。

对于异步调用而言,其代码和运行结果如下所示:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student() { Name = "甲" };
Student student2 = new Student() { Name = "乙" };
Student student3 = new Student() { Name = "丙" };

Action action1 = new Action(student1.Work);
Action action2 = new Action(student2.Work);
Action action3 = new Action(student3.Work);

action1.BeginInvoke(null, null);
action2.BeginInvoke(null, null);
action3.BeginInvoke(null, null);
}
}

class Student
{
public string Name { get; set; }

public void Work()
{
this.DoHomework();
this.HaveDinner();
this.Sleep();
}

public void DoHomework()
{
Console.WriteLine(this.Name + "正在做作业……");
}

public void HaveDinner()
{
Console.WriteLine(this.Name + "正在吃晚饭……");
}

public void Sleep()
{
Console.WriteLine(this.Name + "正在睡觉……");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
甲正在做作业……
丙正在做作业……
甲正在吃晚饭……
丙正在吃晚饭……
丙正在睡觉……
乙正在做作业……
乙正在吃晚饭……
乙正在睡觉……
甲正在睡觉……

可以发现,异步调用是三个不同的线程分别进行运算,这也会导致他们访问资源时可能会发生冲突,并且运行顺序也是不一定的。

显式异步调用

上述是使用委托进行隐式异步调用,当然,我们可以自己声明线程来进行显示异步调用。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student() { Name = "甲" };
Student student2 = new Student() { Name = "乙" };
Student student3 = new Student() { Name = "丙" };

Thread thread1 = new Thread(new ThreadStart(student1.Work));
Thread thread2 = new Thread(new ThreadStart(student2.Work));
Thread thread3 = new Thread(new ThreadStart(student3.Work));

thread1.Start();
thread2.Start();
thread3.Start();
}
}

class Student
{
public string Name { get; set; }

public void Work()
{
this.DoHomework();
this.HaveDinner();
this.Sleep();
}

public void DoHomework()
{
Console.WriteLine(this.Name + "正在做作业……");
}

public void HaveDinner()
{
Console.WriteLine(this.Name + "正在吃晚饭……");
}

public void Sleep()
{
Console.WriteLine(this.Name + "正在睡觉……");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
甲正在做作业……
乙正在做作业……
乙正在吃晚饭……
甲正在吃晚饭……
乙正在睡觉……
丙正在做作业……
丙正在吃晚饭……
丙正在睡觉……
甲正在睡觉……

也可以使用task进行异步调用:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student() { Name = "甲" };
Student student2 = new Student() { Name = "乙" };
Student student3 = new Student() { Name = "丙" };

Task task1 = new Task(new Action(student1.Work));
Task task2 = new Task(new Action(student2.Work));
Task task3 = new Task(new Action(student3.Work));

task1.Start();
task2.Start();
task3.Start();

Thread.Sleep(1000);
}
}

class Student
{
public string Name { get; set; }

public void Work()
{
this.DoHomework();
this.HaveDinner();
this.Sleep();
}

public void DoHomework()
{
Console.WriteLine(this.Name + "正在做作业……");
}

public void HaveDinner()
{
Console.WriteLine(this.Name + "正在吃晚饭……");
}

public void Sleep()
{
Console.WriteLine(this.Name + "正在睡觉……");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
甲正在做作业……
丙正在做作业……
丙正在吃晚饭……
甲正在吃晚饭……
丙正在睡觉……
乙正在做作业……
乙正在吃晚饭……
乙正在睡觉……
甲正在睡觉……

事件

基本概念

事件(Event)简单来说就是能够发生的什么事情,是一种使对象或类具备通知能力的成员。

事件的功能就是通知加上可选的事件参数(即详细信息)。

事件和接收事件带来通知的对象就构成了事件模型(event model)。

“发生到响应”有五个部分,例如“闹铃响了你起床”,“闹铃”是发生事件的对象,“响了”是事件动作,“我”是响应事件的对象,“起床”是响应事件所做的动作,这其中还包含着一个隐含着的“订阅”关系,即我订阅了“闹铃响了”这个事件。

同时还有五个动作,分别是:

  1. 我有一个事件。
  2. 一个人或者一群人关心我的这个事件。
  3. 我的这个事件发生了。
  4. 关心这个事件的人会被依次通知到。
  5. 被通知到的人根据拿到的事件信息(又称“事件数据”、“事件参数”、“通知”)对事件进行响应(又称“处理事件”)。

应用

在基本概念中,我们了解到,事件的模型一共有五个组成部分:

  1. 事件的拥有者(event source,对象)。
  2. 事件的成员(event,成员)。
  3. 事件的响应者(event subscriber,对象)。
  4. 事件处理器(event handler,成员)——本质上是一个回调方法。
  5. 事件订阅——把时间处理器与事件关联在一起,本质上是一种以委托类型为基础的”约定“。

事件不会主动发生,一定是被拥有者的某些逻辑触发之后才能够发生,才能够发挥通知的作用。

注意:

  • 事件处理器是方法成员。
  • 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名,这是一个“语法糖”。
  • 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测。
  • 事件可以同步调用也可以异步调用。
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
29
30
31
32
33
34
35
36
37
using System;
using System.Timers;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Timer timer = new Timer();
timer.Interval = 1000; //设置时间间隔为1秒
Boy boy = new Boy();
timer.Elapsed += boy.Action; //让boy订阅timer中的Elapsed事件
Girl girl = new Girl();
timer.Elapsed += girl.Action;
timer.Start();
Console.ReadLine();
}
}

class Boy
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Jump!");
}
}

class Girl
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Sing!");
}
}
}

输出结果

1
2
3
4
5
6
7
Jump!
Sing!
Jump!
Sing!
Jump!
Sing!
……

上述这段代码是一个简单的小例子,事件的拥有者是Timer实例化出来的timer,每隔一秒钟都会触发一次事件。同时有两个类,分别订阅了该事件,当事件发生时,会执行相关的操作。

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
29
30
31
32
33
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Form form = new Form();
Controller controller = new Controller(form);
form.ShowDialog();
}
}

class Controller
{
private Form form;
public Controller(Form form)
{
if (form != null)
{
this.form = form;
this.form.Click += this.FormClicked;
}
}

private void FormClicked(object sender, EventArgs e)
{
this.form.Text = DateTime.Now.ToString();
}
}
}

输出结果:

事件窗体

这个例子中,创建了一个类,用于存放Form,并为其增加了一个订阅,每当点击一下窗体,都会将标题文本更新为当前的时间。

和第一个例子相比,事件处理器中的参数不一样。也就是说,不能拿影响Elapsed事件的事件处理器去相应Click事件,因为遵循的约束不同,所以他们是不通用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
MyForm form = new MyForm();
form.Click += form.FormClicked;
form.ShowDialog();
}
}

class MyForm : Form
{
internal void FormClicked(object sender, EventArgs e)
{
this.Text = DateTime.Now.ToString();
}
}
}

输出结果:

事件窗体

这个例子和上一个例子的效果是一样的,该例子事件的拥有者同时也是事件的响应者,通过派生了一个类,也就是继承了原本的Form类来实现的。

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
29
30
31
32
33
34
35
36
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
MyForm myForm = new MyForm();
myForm.ShowDialog();
}
}

class MyForm : Form
{
private TextBox textBox;
private Button button;

public MyForm()
{
this.textBox = new TextBox();
this.button = new Button();
this.Controls.Add(this.button);
this.Controls.Add(this.textBox);
this.button.Click += this.ButtonClicked;
this.button.Text = "Click Me!";
this.button.Top = 100;
}

private void ButtonClicked(object sender, EventArgs e)
{
this.textBox.Text = "Hello World!";
}
}
}

输出结果:

事件点击窗体

这段代码中,事件的拥有者是时间的响应者中的一个成员,然后该类实例化的对象又订阅了其成员的事件。

声明

完整声明

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}

//传递事件信息
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}

//声明事件
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

public class Customer
{
private OrderEventHandler orderEventHandler;

public event OrderEventHandler Order
{
add
{
this.orderEventHandler += value;
}

remove
{
this.orderEventHandler -= value;
}
}

public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.", this.Bill);
}

public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}

public void SitDown()
{
Console.WriteLine("Sit down.");
}

public void Think()
{
Console.WriteLine("Let me think ...");

if (this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "可乐鸡翅";
e.Size = "large";
this.orderEventHandler.Invoke(this, e);
}
}

public void Action()
{
this.WalkIn();
this.SitDown();
this.Think();
}
}

public class Waiter
{
internal void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price = 5;
break;
case "large":
price = 15;
break;
}

customer.Bill += price;
}
}
}

输出结果:

1
2
3
4
5
Walk into the restaurant.
Sit down.
Let me think ...
I will serve you the dish - 可乐鸡翅.
I will pay $15.

上述代码实现了一个顾客点单功能,顾客有一些基础的功能,同时也是事件的拥有者。服务员可以订阅顾客,有一个传递信息的信息的类是OrderEventArgs,里面存储了相应的参数,并且全都派生自EventArgs类。

简略声明

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}

//传递事件信息
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}

//声明事件
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

public class Customer
{
public event OrderEventHandler Order;

public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.", this.Bill);
}

public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}

public void SitDown()
{
Console.WriteLine("Sit down.");
}

public void Think()
{
Console.WriteLine("Let me think ...");

if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "可乐鸡翅";
e.Size = "large";
this.Order.Invoke(this, e);
}
}

public void Action()
{
this.WalkIn();
this.SitDown();
this.Think();
}
}

public class Waiter
{
internal void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price = 5;
break;
case "large":
price = 15;
break;
}

customer.Bill += price;
}
}
}

输出结果:

1
2
3
4
5
Walk into the restaurant.
Sit down.
Let me think ...
I will serve you the dish - 可乐鸡翅.
I will pay $15.

这两段代码实现的东西是一样的,区别在于这段代码相对来说简写了事件声明的部分。

命名约定

用于声明事件的委托,一般命名为FooEventHandler(除非是一个非常通用的事件约束)。

FooEventHandler委托的参数一般有两个:

  • 第一个是object类型,名字为sender,实际上就是事件的拥有者,事件的source
  • 第二个是EventArgs类的派生类,类名一般为FooEventArgs,参数名为e。也就是前面讲过的事件参数。
  • 虽然没有官方的说法,但我们可以把委托的参数列表看做是事件发生后发送给事件响应者的“事件消息”。

触发Foo事件的方法一般命名为OnFoo,即“因何引发”、“事出有因”。访问级别为protected,不能为public,不然可能会被别的对象调用。

基本概念

类是一种数据结构(data structure),同时也是一种数据类型,代表现实世界中的“种类”。

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
29
30
31
32
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student = new Student();
student.ID = 1;
student.Name = "Bigglesworth";
student.Report();
}
}

class Student
{
public int ID { get; set; }
public string Name { get; set; }
public void Report()
{
Console.WriteLine($"I'm #{this.ID} student, my name is {this.Name}");
}

~Student()
{
Console.WriteLine("Bye bye! Release the system resources ...");
}
}
}

输出结果:

1
2
I'm #1 student, my name is Bigglesworth
Bye bye! Release the system resources ...

类声明的位置

C#中,类声明的位置一般有三个地方,分别是命名空间里、类中、命名空间外。

其中,声明在命名空间外是全局类,基本上不会使用。如果声明在类中,那么新声明的这个类就是其子类。

类的继承

一个类可以继承另一个类,被继承的类称为父类,继承的类称为子类。

所有的类都默认继承Object类。

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
29
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Type type1 = typeof(Bus);
Type type2 = type1.BaseType;
Type type3 = type2.BaseType;
Console.WriteLine(type1.FullName);
Console.WriteLine(type2.FullName);
Console.WriteLine(type3.FullName);
}
}

class Vehicle
{

}

class Bus : Vehicle
{

}
}

输出结果:

1
2
3
ConsoleApp.Bus
ConsoleApp.Vehicle
System.Object

一个子类的实例同样也是其父类的实例,但是父类的实例并不是子类的实例。

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
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Vehicle vehicle = new Vehicle();
Console.WriteLine(vehicle is Bus);
Bus bus = new Bus();
Console.WriteLine(bus is Vehicle);
}
}

class Vehicle
{

}

class Bus : Vehicle
{

}
}

输出结果:

1
2
False
True

根据这个特性,可以使用一个父类的变量引用一个子类类型的实例。

如果一个类被sealed关键字修饰,那么这个类就变成了一个封闭类,那么这个类就不能作为基类被继承。

一个类只能有一个基类,不能同时继承多个基类。

子类的访问级别不能超过基类的访问级别,可以与父类的访问级别同级或者比他低。

重写与多态

C#中的这部分内容与C++类似,使用virtual关键字将要重写的方法转换为虚函数,在其子类中使用Override重写父类的虚函数,这种方法可以正确调用相应类中的方法。

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
29
30
31
32
33
34
35
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Vehicle vehicle = new Vehicle();
Vehicle car = new Car();
vehicle.Run();
car.Run();
}
}

class Vehicle
{
public virtual void Run()
{
Console.WriteLine("I'm running!");
return;
}
}

class Car : Vehicle
{
public override void Run()
{
Console.WriteLine("Car is running!");
return;
}
}
}

输出结果:

1
2
I'm running!
Car is running!

抽象类

抽象类与C++中的纯虚函数非常类似。首先需要明确一下抽象方法,抽象方法指的是没有写其内部逻辑,由子类去完成相应的功能。拥有抽象方法的类就被称为抽象类,抽象类使用abstract关键字声明,内部的抽象方法也需要使用该关键字,在进行重写时需要使用override关键字来声明。

抽象类不能够实例化对象,但是可以引用其子类。抽象类中的方法可以通过多态来实现,内部可以存在具体的实现了的方法。

如果一个类中全都是抽象方法,那么这个类可以写成接口,使用interface来修饰,其内部成员全部默认为public且只有函数成员。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Vehicle vehicle = new Car();
vehicle.Run();
}
}

interface IVehicle
{
void Stop();
void Run();
}

abstract class Vehicle : IVehicle
{
public abstract void Run();

public void Stop()
{
Console.WriteLine("Stopped!");
}
}

class Car : Vehicle
{
public override void Run()
{
Console.WriteLine("Car is running...");
}
}

class Truck : Vehicle
{
public override void Run()
{
Console.WriteLine("Truck is running...");
}
}
}

输出结果:

1
Car is running...

接口和抽象类都是“软件工程的产物”。

具体类->抽象类->接口:越来越抽象,内部实现的东西越来越少。

抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了“具体逻辑”)。

抽象类为复用而生:专门作为基类来使用,也具有解耦功能。

封装确定的,开放不确定的,推迟到合适的子类中去实现。

接口是完全为实现逻辑的“类”(“纯虚类”;只有函数成员;成员全部public)。

接口为解耦而生:“高内聚,低耦合”,方便单元测试。

接口是一个“协约”,早已为工业生产所熟知(有分工必有协作,有协作必有协约)

它们都不能实例化,只能用来声明变量、引用具体类(concrete class)的实例。

接口

使用接口实现一个手机的例子,手机持有基础的几个功能。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
var user1 = new PhoneUser(new NokiaPhone());
user1.UsePhone();
Console.WriteLine();
var user2 = new PhoneUser(new EricssonPhone());
user2.UsePhone();
}
}

class PhoneUser
{
private IPhone _phone;
public PhoneUser(IPhone phone)
{
_phone = phone;
}

public void UsePhone()
{
_phone.Dail();
_phone.PickUp();
_phone.Send();
_phone.Receive();
}
}

interface IPhone
{
void Dail();
void PickUp();
void Send();
void Receive();
}

class NokiaPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Nokia calling ...");
}

public void PickUp()
{
Console.WriteLine("Hello! This is Bigglesworth!");
}

public void Receive()
{
Console.WriteLine("Nokia message ring ...");
}

public void Send()
{
Console.WriteLine("Hello!");
}
}

class EricssonPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Ericsson calling ...");
}

public void PickUp()
{
Console.WriteLine("Hello! This is Bigglesworth!");
}

public void Receive()
{
Console.WriteLine("Ericsson message ring ...");
}

public void Send()
{
Console.WriteLine("Good morning!");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
Nokia calling ...
Hello! This is Bigglesworth!
Hello!
Nokia message ring ...

Ericsson calling ...
Hello! This is Bigglesworth!
Good morning!
Ericsson message ring ...

接口之间也可以进行继承,且一个接口可以继承多个接口,例如下面这个例子:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Driver driver1 = new Driver(new Car());
driver1.Drive();
Driver driver2 = new Driver(new Tank());
driver2.Drive();
}
}

class Driver
{
private IVehicle _vehicle;
public Driver(IVehicle vehicle)
{
_vehicle = vehicle;
}

public void Drive()
{
_vehicle.Run();
}
}

interface IVehicle
{
void Run();
}

class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running ...");
}
}

class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running ...");
}
}

interface IWeapon
{
void Fire();
}

interface ITank : IVehicle, IWeapon
{

}

class Tank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}

public void Run()
{
Console.WriteLine("Tank is running ...");
}
}
}

输出结果;

1
2
Car is running ...
Tank is running ...

可以隔离某些方法,使其只有在被特定类型引用时才可以调用。

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
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
var wk = new WarmKiller();
wk.Love();
IKiller killer = wk;
killer.Kill();
}
}

interface IGentleman
{
void Love();
}

interface IKiller
{
void Kill();
}

class WarmKiller : IGentleman, IKiller
{
public void Love()
{
Console.WriteLine("I will Love you for ever ...");
}

void IKiller.Kill() //只有被IKiller类型引用时才能使用该方法
{
Console.WriteLine("Let me kill the enemy ...");
}
}
}

输出结果:

1
2
I will Love you for ever ...
Let me kill the enemy ...

泛型

基本概念

泛型(generic)无处不在,为了避免成员膨胀或者类型膨胀,因此有了泛型这一概念。

举一个例子,现在有一个商店,卖书和苹果两种货物,需要对书和苹果各构造一个类,二者又各自需要一个对应的箱子类,这就造成了类型膨胀,每一个物品都需要有一个类与其对应,会造成很大的麻烦。

如果把所有的盒子作为属性写在同一个Box类中,又会导致每一组商品和盒子只会使用Box中的一个属性,这就造成了成员膨胀。

为了解决这一问题,我们可以使用泛型类,其具体方法如下所示:

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
29
30
31
32
33
34
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
Book book = new Book() { Name = "New Book" };
Box<Apple> box1 = new Box<Apple>() { Cargo = apple };
Console.WriteLine(box1.Cargo.Color);
Box<Book> box2 = new Box<Book>() { Cargo = book };
Console.WriteLine(box2.Cargo.Name);
}
}

class Apple
{
public string Color { get; set; }
}

class Book
{
public string Name { get; set; }
}

class Box<TCargo>
{
public TCargo Cargo { get; set; }
}
}

输出结果:

1
2
Red
New Book

在这个例子中使用了泛型类,$29$行用一组尖括号将泛型框了起来,在类中可以直接把它作为一种数据类型来使用。

泛型接口

下面介绍一下泛型接口,泛型接口非常常用,如果一个类实现的是泛型接口,那么这个类也是泛型类。

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
29
30
31
32
33
34
35
36
37
38
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student1<long> student1 = new Student1<long>();
student1.Id = 1;
student1.Name = "Bigglesworth";
Console.WriteLine($"Id:{student1.Id} Name:{student1.Name} Id Type:{student1.Id.GetType().Name}");
Student2 student2 = new Student2();
student2.Id = 2;
student2.Name = "Areskey";
Console.WriteLine($"Id:{student2.Id} Name:{student2.Name} Id Type:{student2.Id.GetType().Name}");
}
}

interface IUnique<TId>
{
TId Id { get; set; }
}

class Student1<TId> : IUnique<TId>
{
public TId Id { get; set; }
public string Name { get; set; }
}

class Student2 : IUnique<ulong>
{
public ulong Id { get; set; }
public string Name { get; set; }
}
}

输出结果:

1
2
Id:1 Name:Bigglesworth Id Type:Int64
Id:2 Name:Areskey Id Type:UInt64

在上述例子中,有一个泛型接口,两个类。第一个类是一个泛型类,因为没有指定TId的类型,在第二个类中,就不需要将其声明为泛型类了,因为直接指定了TIdulong类型。

泛型方法

不仅可以在类中使用泛型,在方法中也可以使用泛型来简化代码。

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
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2 = { 1, 2, 3, 4, 5, 6 };
double[] a3 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
double[] a4 = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
var result1 = Zip(a1, a2);
Console.WriteLine(string.Join(",", result1));
var result2 = Zip<double>(a3, a4);
Console.WriteLine(string.Join(",", result2));
}

static T[] Zip<T>(T[] a, T[] b)
{
T[] zipped = new T[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai < a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai < a.Length || bi < b.Length);
return zipped;
}
}
}

输出结果:

1
2
1,1,2,2,3,3,4,4,5,5,6
1.1,1.1,2.2,2.2,3.3,3.3,4.4,4.4,5.5,5.5,6.6

上述例子中,使用了一个泛型方法,C#可以自己推断出相应的类型,因此$16$行中的double是可以省略掉的。

partial类

partial允许把一个类的代码分成两部分或者多部分,可以以此减少派生类。

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
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student = new Student();
student.ID = 1;
student.Name = "Bigglesworth";
Console.WriteLine($"ID:{student.ID} Name:{student.Name}");
}
}

partial class Student
{
public int ID;
}

partial class Student
{
public string Name;
}
}

输出结果:

1
ID:1 Name:Bigglesworth

上述例子中,两个类的名字都是Student,但是它们都被声明为了partial类,这样二者都构成了Student类。使用partial可以保证在不同的文件里共同声明的类可以同时存在。

枚举

枚举类型是认为限定取值范围的整数,枚举与整数值相互对应。

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
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Level = Level.Employee;

Person boss = new Person();
boss.Level = Level.Boss;

Console.WriteLine(boss.Level > person.Level);

Console.WriteLine(Level.Employee + ":" + (int)Level.Employee);
Console.WriteLine(Level.Manager + ":" + (int)Level.Manager);
Console.WriteLine(Level.Boss + ":" + (int)Level.Boss);
Console.WriteLine(Level.BigBoss + ":" + (int)Level.BigBoss);
}
}

enum Level
{
Employee = 100,
Manager,
Boss = 300,
BigBoss,
}

class Person
{
public int ID { get; set; }
public string Name { get; set; }
public Level Level { get; set; }
}
}

输出结果:

1
2
3
4
5
True
Employee:100
Manager:101
Boss:300
BigBoss:301

枚举类型中的成员可以直接赋值,如果不赋值默认是前一个成员$+1$,因此可以进行大小比较。同时,枚举类型默认的第一个成员值为$0$。

结构体

结构体是一种值类型,可以进行装箱和拆箱,也可以实现接口,但是不能派生自类或结构体,同时也不能有显示无参构造器。

结构体与类最大的区别在于结构体是一种值类型数据,类是一种引用类型数据。

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
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
Student student1 = new Student() { ID = 1, Name = "Bigglesworth" };
Student student2 = student1;
student2.ID = 2;
student2.Name = "Areskey";
Console.WriteLine($"ID:{student1.ID} Name:{student1.Name}");
Console.WriteLine($"ID:{student2.ID} Name:{student2.Name}");
}
}

struct Student
{
public int ID { get; set; }
public string Name { get; set; }
}
}

输出结果:

1
2
ID:1 Name:Bigglesworth
ID:2 Name:Areskey