CSharp从入门到入门

开发环境:VSCode,需要先装NET的SDK,不装的话打开一个cs文件也会提醒你装。

win64下载: https://dotnet.microsoft.com/zh-cn/download/dotnet/thank-you/sdk-6.0.401-windows-x64-installer?journey=vs-code

你的SDK会安装在这里:C:\Program Files\dotnet\sdk

c#与java相似之处可能会很简略。

项目创建

我们创建一个控制台应用,可以参考 https://learn.microsoft.com/zh-cn/dotnet/core/tutorials/with-visual-studio-code?pivots=dotnet-6-0。

首先打开你的项目文件夹,控制台输入dotnet new console --framework net6.0,回车。

然后将 Program.cs 的内容替换为你的代码,比如hello world:

1
2
3
4
5
6
7
8
9
10
using System;
namespace HelloWorld{
class Program{
static void Main(string[] args)
{
//hello world
Console.WriteLine("Hello World!");
}
}
}

控制台输入dotnet run,回车,即可。

其中using 关键字用来在程序中引入 System 命名空间,一个程序中可以有多个 using 语句。

namespace 关键字用来声明一个命名空间,“HelloWorld”则是命名空间的名字,命名空间是类的集合。

WriteLine 是 System 命名空间中定义的 Console 类里面的方法,用来输出一些消息。

 

基本语法

关键字

C# 中的关键字是编译器预先定义好的一些单词,也可以称为保留字或者保留标识符,这些关键字对编译器有特殊的意义,不能用作标识符。但是,如果非要使用的话也不是没有办法,只需要在关键字前面加上@前缀即可,例如@if就是一个有效的标识符,而if则是一个关键字。

 

数据类型

C# 语言中内置了一些基本的数据类型,数据类型用来指定程序中变量可以存储的数据的类型,C# 中的数据类型可以大致分为三类:

  • 值类型(Value types);
  • 引用类型(References types);
  • 指针类型(Pointer types)。

细节:值类型作为方法中的局部变量时,在栈中分配,而作为类的成员变量时,在堆中分配;引用类型变量在栈中分配,引用类型的实例在堆中分配。

值类型

C# 中的值类型是从 System.ValueType 类中派生出来的,对于值类型的变量我们可以直接为其分配一个具体的值。当声明一个值类型的变量时,系统会自动分配一块内存区域用来存储这个变量的值,需要注意的是,变量所占内存的大小会根据系统的不同而有所变化。

C# 中的值类型有很多,如下表所示:

引用类型

引用类型的变量中并不存储实际的数据值,而是存储的对数据(对象)的引用,换句话说就是,引用类型的变量中存储的是数据在内存中的位置。当多个变量都引用同一个内存地址时,如果其中一个变量改变了内存中数据的值,那么所有引用这个内存地址的变量的值都会改变。C# 中内置的引用类型包括 Object(对象)、Dynamic(动态)和 string(字符串)。

1) 对象类型(Object)

对象类型是 C# 通用类型系统(Common Type System:CTS)中所有数据类型的最终基类,Object 是 System.Object 类的别名。任何类型的值都可以分配给对象类型,但是在分配值之前,需要对类型进行转换。

C#也有装箱拆箱

2) 动态类型(Dynamic)

您可以在动态类型的变量中存储任何类型的值,这些变量的类型检查是在程序运行时进行的。动态类型的声明语法:dynamic <variable_name> = value;,例如:dynamic d = 20;

动态类型与对象类型类似,但对象类型变量的类型检查是在编译时进行的,而动态类型变量的类型检查则是在程序运行时进行的。

3) 字符串类型(String)

字符串类型的变量允许您将一个字符串赋值给这个变量,字符串类型需要通过 String 类来创建,String 类是 System.String 类的别名,它是从对象(Object)类型中派生的。在 C# 中有两种定义字符串类型的方式,分别是使用" "@" "

示例代码如下:

1
2
3
4
//使用引号的声明方式
String str = "http://c.biancheng.net/";
//使用 @ 加引号的声明形式
@"http://c.biancheng.net/";

使用@" "形式声明的字符串称为“逐字字符串”,逐字字符串会将转义字符\当作普通字符对待,例如string str = @"C:\Windows";等价于string str = "C:\\Windows";

另外,在@" "形式声明的字符串中可以任意使用换行换行符及缩进空格等都会计算在字符串的长度之中

指针类型

C# 语言中的指针是一个变量,也称为定位器或指示符,其中可以存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针具有相同的功能。例如:

1
2
char* cptr;
int* iptr;

 

变量

这个没什么好说的,只需要注意一下内置的类型转换函数:

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Programme{
class StringConversion {
static void Main(string[] args) {
int i = 75;
float f = 53.005;
double d = 2345.7652;
bool b = true;
Console.WriteLine(i.ToString());
Console.WriteLine(f.ToString());
Console.WriteLine(d.ToString());
Console.WriteLine(b.ToString());
Console.ReadKey();
}
}
}

 

常量

注意常量关键字是const即可,而非final

 

流程控制

if 语句、if else 语句、if else if 语句、switch 语句、for循环、while循环、do while循环、break、continue语法与使用逻辑、写法是与java一样的。

foreach循环

是跟Dart一样的:

1
2
3
foreach(数据类型 变量名 in 数组或集合对象){
语句块;
}

goto

C# 中的 goto 语句也称为跳转语句,使用它可以控制程序跳转到指定的位置执行。不过并不建议在程序中多次使用 goto 语句,因为它会使程序变得更加复杂。goto 语句的语法格式如下所示:

1
2
3
4
goto Labels;
语句块1;
Labels:
语句块2;

想要使用 goto 语句来跳转程序,必须先在想要跳转的位置定义好一个标签(Labels),标签名称的定义和变量名类似,然后使用goto 标签名;即可使程序跳转到指定位置执行。如上面语法中所示,程序会跳过“语句块1”直接执行“语句块2”。

提示:goto 语句并不限于在循环中使用,其它的情况也可以使用。但是,goto 语句不能从循环外跳转到循环语句中,而且不能跳出类的范围

【示例】使用 goto 语句将程序跳转到指定位置执行。

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;

namespace c.biancheng.net{
class Demo{
static void Main(string[] args){
int count = 1;
login:
Console.WriteLine("请输入用户名");
string username = Console.ReadLine();
Console.WriteLine("请输入密码");
string userpwd = Console.ReadLine();
if (username == "c.biancheng.net" && userpwd == "123456"){
Console.WriteLine("登录成功");
}else{
count++;
if (count > 3){
Console.WriteLine("用户名或密码错误次数过多!退出!");
}else{
Console.WriteLine("用户名或密码错误");
goto login;//返回login标签处重新输入用户名密码
}
}
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
请输入用户名
C语言中文网
请输入密码
123456
用户名或密码错误
请输入用户名
c.biancheng.net
请输入密码
123456
登录成功

 

函数/方法

跟java一样。

C# 中的访问权限修饰符有以下几种:

  • public:公共的,所有对象都可以访问,但是需要引用命名空间;
  • private:私有的,类的内部才可以访问;
  • internal:内部的,同一个程序集的对象可以访问,程序集就是命名空间;
  • protected:受保护的,类的内部或类的父类和子类中可以访问;
  • Protected internal:protected 和 internal 的并集,符合任意一条都可以访问。

 

nullable:可空类型

同kotlin

在 C# 1.x 的版本中,一个值类型的变量是不可以被赋值为 null(空值)的,否则会产生异常。而在 C# 2.0 中,新增了一个 nullable 类型,可以使用 nullable 类型定义包含 null 值的数据,例如,可以在 nullable <Int32>(可为空的 int32 类型)类型的变量中存储 -2147483648 到 2147483647 之间的任何值或者 null。声明可空类型的语法如:data_type? variable_name = null;

【示例】下面通过示例来演示可空类型的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
namespace c.biancheng.net{
class Demo{
static void Main(string[] args){
int? num1;
int? num2 = 123;
num1 = null;

double? num3 = new double?();
double? num4 = 3.1415926;
bool? boolval = null;
// 输出这些值
Console.WriteLine("num1 = {0} \r\n num2 = {1} \r\n num3 = {2} \r\n num4 = {3} \r\n boolval = {4}", num1, num2, num3, num4, boolval);
Console.ReadLine();
}
}
}

运行结果如下:

1
2
3
4
5
num1 =
num2 = 123
num3 =
num4 = 3.1415926
boolval =

Null 合并运算符(??)

同kotlin

在 C# 中 Null 合并运算符用于定义可空类型和引用类型的默认值。如果此运算符的左操作数不为 null,那么运算符将返回左操作数,否则返回右操作数。例如表达式a??b中,如果 a 不为空,那么表达式的值则为 a,反之则为 b。

需要注意的是,Null 合并运算符左右两边操作数的类型必须相同,或者右操作数的类型可以隐式的转换为左操作数的类型,否则将编译错误。

 

数组

一维数组同java。

多维数组创建则不同:

1
2
int[,] arr=new int[3,3];         // 声明一个二维数组
int[,,] arr=new int[3,3,3]; // 声明一个三维数组

初始化二维数组例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第一种方式
int[,] arr = new int[3,4]{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 第二种方式
int[,] arr = new int[,]{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 第三种方式
int[,] arr = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};

交错数组(数组的数组)

交错数组中的每个元素都可以是维度和大小不同的数组。

声明交错数组

交错数组的声明语法如data_type[][] array_name;

假如要声明一个具有三个元素的一维交错数组,并且数组中的每个元素都是一个一维的整型数组,示例代码如:

int[][] jaggedArray = new int[3][];

初始化交错数组

和普通数组相同,交错数组也需要初始化后才可以使用,可以使用下面的方式初始化一个交错数组。

1
2
3
4
int[][] jaggedArray = new int[3][]; // 定义一个交错数组
jaggedArray[0] = new int[5]; // 对数组的第一个元素初始化
jaggedArray[1] = new int[4]; // 对数组的第二个元素初始化
jaggedArray[2] = new int[2]; // 对数组的第三个元素初始化

上面的交错数组中包含三个元素,第一个元素是长度为 5 的整型数组,第二个元素是长度为 4 的整型数组,第三个元素是长度为 2 的整型数组。

除了上面的方法外,还可以直接使用具体的值来填充数组,这种情况下就不需要再设定数组的大小了,如下所示:

1
2
3
4
int[][] jaggedArray = new int[3][]; // 定义一个交错数组
jaggedArray[0] = new int[] {1, 2, 3, 4, 5};
jaggedArray[1] = new int[] {6, 7, 8, 9};
jaggedArray[2] = new int[] {10, 11};

还可以在声明数组时,直接将数组初始化,如下所示:

1
2
3
4
5
int[][] jaggedArray = new int[][]{
new int[] {1, 2, 3, 4, 5},
new int[] {6, 7, 8, 9},
new int[] {10, 11}
};

上面的声明方式还有一种简写形式,如下所示:

1
2
3
4
5
int[][] jaggedArray = {
new int[] {1, 2, 3, 4, 5},
new int[] {6, 7, 8, 9},
new int[] {10, 11}
};

注意:不能从元素初始化中省略 new 运算符,因为不存在元素的默认初始化。

参数数组

某些情况下,我们在定义函数时可能并不能提前确定参数的数量,这时可以使用 C# 提供的参数数组,参数数组通常用于为函数传递未知数量的参数。

若要使用参数数组,则需要利用 params 关键字,语法格式如下:

访问权限修饰符 返回值类型 函数名(params 类型名称[] 数组名称)

提示:使用参数数组时,既可以直接为函数传递一个数组作为参数,也可以使用函数名(参数1, 参数2, ..., 参数n)的形式传递若干个具体的值作为参数。

下面通过示例来演示以下参数数组的使用:

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;
namespace c.biancheng.net{
class Demo{
static void Main(string[] args){
Demo Obj = new Demo();
string str = Obj.getSum(1, 2, 3, 4, 5, 6);
Console.WriteLine(str);
int[] arr = {2, 4, 6, 8, 10};
string str2 = Obj.getSum(arr);
Console.WriteLine(str2);
}
public string getSum(params int[] arr){
int sum = 0;
string str = "";
foreach(int i in arr){
sum += i;
str += "+ " + i + " ";
}
str = str.Trim('+');
str += "= "+sum;
return str;
}
}
}

运行结果如下:

1
2
1 + 2 + 3 + 4 + 5 + 6 = 21
2 + 4 + 6 + 8 + 10 = 30

Array类

具体参阅C# 的官方文档

 

结构体

结构体也被称为结构类型(“structure type”或“struct type”),它是一种可封装数据和相关功能的值类型,在语法上结构体与类(class)非常相似,它们都可以用来封装数据,并且都可以包含成员属性和成员方法。

定义结构体

要定义一个结构体需要使用 struct 关键字,每个结构体都可以被看作是一种新的数据类型,其中可以包含多个成员(成员属性和成员方法),例如下面声明的 Books 结构体:

1
2
3
4
5
6
struct Books {
public string title;
public string author;
public string subject;
public int book_id;
};

在设计结构体时有以下几点需要注意:

  • 不能为结构体声明无参数的构造函数,因为每个结构体中都已经默认创建了一个隐式的、无参数的构造函数;
  • 不能在声明成员属性时对它们进行初始化,静态属性和常量除外;
  • 结构体的构造函数必须初始化该结构体中的所有成员属性;
  • 结构体不能从其他类或结构体中继承,也不能作为类的基础类型,但是结构类型可以实现接口;
  • 不能在结构体中声明析构函数。

下面通过一个示例来演示结构体的使用:

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;

namespace c.biancheng.net{
struct Books {
public string title;
public string author;
public string subject;
public int book_id;
}

class Demo{
static void Main(string[] args) {
Books book1; // 声明 Books 类型的 book1
Books book2; // 声明 Books 类型的 book2
// 定义 book1 的属性
book1.title = "C#教程";
book1.author = "C语言中文网";
book1.subject = "C#编程教程";
book1.book_id = 123456;
// 定义 book2 的属性
book2.title = "HTTP教程";
book2.author = "C语言中文网";
book2.subject = "HTTP协议教程";
book2.book_id = 123455;
// 输出 book1 的属性信息
Console.WriteLine("book1 title : {0}", book1.title);
Console.WriteLine("book1 author : {0}", book1.author);
Console.WriteLine("book1 subject : {0}", book1.subject);
Console.WriteLine("book1 book_id :{0}", book1.book_id);
// 输出 book2 的属性信息
Console.WriteLine("book2 title : {0}", book2.title);
Console.WriteLine("book2 author : {0}", book2.author);
Console.WriteLine("book2 subject : {0}", book2.subject);
Console.WriteLine("book2 book_id :{0}", book2.book_id);
Console.ReadKey();
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
book1 title : C#教程
book1 author : C语言中文网
book1 subject : C#编程教程
book1 book_id :123456
book2 title : HTTP教程
book2 author : C语言中文网
book2 subject : HTTP协议教程
book2 book_id :123455

结构体的特征

C# 中的结构体与 C/C++ 中的结构体有很大的不同,在 C# 中结构体具有以下功能:

  • 结构体中可以具有方法、字段、索引、属性、运算符方法和事件;
  • 结构体中可以定义构造函数,但不能定义析构函数,需要注意的是,定义的构造函数不能没有参数,因为没有参数的构造函数是 C# 默认自动定义的,而且不能更改;
  • 与类不同,结构体不能继承其他结构体或类;
  • 结构体不能用作其他结构体或类的基础结构;
  • 一种结构体可以实现一个或多个接口;
  • 结构体成员不能被设定为 abstract、virtual 或 protected;
  • 与类不同,结构体可以不用 New 操作符来实例化,当使用 New 操作符来实例化结构体时会自动调用结构体中的构造函数;
  • 如果不使用 New 操作符来实例化结构体,结构体对象中的字段将保持未分配状态,并且在所有字段初始化之前无法使用该结构体实例。

类与结构体

类和结构体的主要区别如下所示:

  • 类是引用类型,结构体是值类型;
  • 结构体不支持继承,但可以实现接口;
  • 结构体中不能声明默认的构造函数。

根据以上特征,让我们来完善一下前面的示例:

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;
namespace c.biancheng.net{
struct Books {
public string title;
public string author;
public string subject;
public int book_id;
public void getValue(string t, string a, string s, int id){
title = t;
author = a;
subject = s;
book_id = id;
}
public void disPlay(){
Console.WriteLine("Title:{0}", title);
Console.WriteLine("Author:{0}", author);
Console.WriteLine("Subject:{0}", subject);
Console.WriteLine("Book_id:{0}", book_id);
}
}

class Demo{
static void Main(string[] args) {
Books book1 = new Books(); // 实例化 Books 结构体
Books book2 = new Books(); // 实例化 Books 结构体
// 定义 book1 的属性
book1.getValue("C#教程","C语言中文网","C#编程教程",123456);
// 定义 book2 的属性
book2.getValue("HTTP教程","C语言中文网","HTTP协议教程",123455);
// 输出 book1 的属性信息
book1.disPlay();
// 输出 book2 的属性信息
book2.disPlay();
Console.ReadKey();
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
Title:C#教程
Author:C语言中文网
Subject:C#编程教程
Book_id:123456
Title:HTTP教程
Author:C语言中文网
Subject:HTTP协议教程
Book_id:123455

 

类与对象

几乎就是java风格,如下例:

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;
namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
Student stu1 = new Student();
Student stu2 = new Student();
stu1.insert(101, "张三", "男", 18);
stu1.display();
stu2.insert(102, "李四", "女", 16);
stu2.display();
}
}

public class Student{
public int id;
public string? name;//这里声明可以为null,因为VSCode给了个warning,虽然有点奇怪
public string? sex;
public int age;
public void insert(int i, string n, string s, int a){
this.id = i;//也可以没有this
this.name = n;
this.sex = s;
this.age = a;
}

public void display(){
Console.WriteLine("编号:{0} 姓名:{1} 性别:{2} 年龄:{3}", id, name, sex, age);
}
}
}

静态构造函数

静态构造函数用于初始化类中的静态数据或执行仅需执行一次的特定操作。静态构造函数将在创建第一个实例或引用类中的静态成员之前自动调用

静态构造函数具有以下特性:

  • 静态构造函数不使用访问权限修饰符修饰或不具有参数;
  • 类或结构体中只能具有一个静态构造函数;
  • 静态构造函数不能继承或重载;
  • 静态构造函数不能直接调用,仅可以由公共语言运行时 (CLR) 调用;
  • 用户无法控制程序中静态构造函数的执行时间;
  • 在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数以初始化类;
  • 静态构造函数会在实例构造函数之前运行。

【实例】下面通过一个示例来演示实例构造函数与静态构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
namespace c.biancheng.net{
class Demo{
public static int num = 0;
// 构造函数
Demo(){
num = 1;
}
// 静态构造函数
static Demo(){
num = 2;
}
static void Main(string[] args) {
Console.WriteLine("num = {0}", num);
Demo Obj = new Demo();
Console.WriteLine("num = {0}", num);
Console.Read();
}
}
}
//num = 2
//num = 1

当执行上面程序时,会首先执行public static int num = 0,接着执行类中的静态构造函数,此时 num = 2,然后执行 Main 函数里面的内容,此时打印 num 的值为 2,接着初始化 Demo 类,这时会执行类中的构造函数,此时 num 会重新赋值为 1。

私有构造函数

私有构造函数是一种特殊的实例构造函数,通常用在只包含静态成员的类中。如果一个类中具有一个或多个私有构造函数而没有公共构造函数的话,那么其他类(除嵌套类外)则无法创建该类的实例。 例如:

1
2
3
4
5
class NLog{
// 私有构造函数
private NLog() { }
public static double e = Math.E; //2.71828...
}

上例中定义了一个空的私有构造函数,这么做的好处就是空构造函数可阻止自动生成无参数构造函数。需要注意的是,如果不对构造函数使用访问权限修饰符,则默认它为私有构造函数

析构函数

C# 中的析构函数具有以下特点:

  • 析构函数只能在类中定义,不能用于结构体;
  • 一个类中只能定义一个析构函数;
  • 析构函数不能继承或重载;
  • 析构函数没有返回值;
  • 析构函数是自动调用的,不能手动调用;
  • 析构函数不能使用访问权限修饰符修饰,也不能包含参数。

析构函数的名称同样与类名相同,不过需要在名称的前面加上一个~作为前缀,如下所示:

1
2
3
class Car{
~Car() {...} // 析构函数
}

【示例】结合前面介绍的构造函数,让我们来演示一下类中构造函数和析构函数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
Student stu1 = new Student();
Student stu2 = new Student();
}
}
public class Student{
public Student(){
Console.WriteLine("类中的构造函数");
}
~Student(){
Console.WriteLine("类中的析构函数");
}
}
}
//类中的构造函数
//类中的构造函数
//类中的析构函数
//类中的析构函数

注意:析构函数不能对外公开,所以我们不能在析构函数上应用任何访问权限修饰符。

this

除了使用 this 表示当前类的对象,还可以使用 this 关键字串联构造函数、作为类的索引器、作为原始类型的扩展方法,不过都不是很常用,略。

http://c.biancheng.net/csharp/this.html

 

继承

要使用一个类继承另一个类需要使用到冒号:,如下所示:

1
2
3
class 派生类 : 基类{
... ...
}

跟cpp风格一致,但是要记住不支持多重继承

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;

namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
Rectangle oblong = new Rectangle();
oblong.setWidth(3);
oblong.setHeight(4);
int area = oblong.getArea();
Console.WriteLine("长方形的面积为:{0}", area);
}
}

// 基类
class Shape{
protected int width, height;
public void setWidth(int w){
width = w;
}
public void setHeight(int h){
height = h;
}
}

// 派生类
class Rectangle : Shape{
public int getArea(){
return width*height;
}
}
}

 

多态

编译时多态

在编译期间将函数与对象链接的机制称为早期绑定,也称为静态绑定。C# 提供了两种技术来实现编译时多态,分别是函数重载和运算符重载,本节主要来介绍一下函数重载,运算符重载会在后面进行讲解。

在同一个作用域中,可以定义多个同名的函数,但是这些函数彼此之间必须有所差异,比如参数个数不同或参数类型不同等等,返回值类型不同除外。

【示例】定义名为 print 的函数来打印不同类型的数据:

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

namespace c.biancheng.net{
class Demo{
void print(int i){
Console.WriteLine("打印 int 类型的数据:{0}", i);
}
void print(double d){
Console.WriteLine("打印 double 类型的数据:{0}", d);
}
void print(string s){
Console.WriteLine("打印 string 类型的数据:{0}", s);
}

static void Main(string[] args) {
Demo p = new Demo();
p.print(123);
p.print("C语言中文网");
p.print(3.1415926);
}
}
}

运行时多态

C# 允许您使用 abstract 关键字来创建抽象类,抽象类用于实现部分接口。另外,抽象类包含抽象方法,可以在派生类中实现。

下面列举了一些有关抽象类的规则:

  • 不能创建一个抽象类的实例;
  • 不能在一个抽象类外部声明抽象方法;
  • 通过在类定义时使用 sealed 关键字,可以将类声明为密封类,密封类不能被继承,因此抽象类中不能声明密封类。

【示例】下面的程序演示了一个抽象类:

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;

namespace c.biancheng.net{
abstract class Shape{
public abstract int area();
}
class Rectangle : Shape{
private int width, height;
public Rectangle(int w, int h){
width = w;
height = h;
}
public override int area(){
return (width * height);
}
}

class Demo{
static void Main(string[] args)
{
Rectangle r = new Rectangle(12,15);
double a = r.area();
Console.WriteLine("长方形的面积为: {0}",a);
Console.ReadKey();
}
}
}

 

运算符重载

用的不多,放链接

http://c.biancheng.net/csharp/operator-overloading.html

 

接口

接口与java也基本类似,如:

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;

namespace c.biancheng.net{
public interface Iwebsite{
void setValue(string str1, string str2);
void disPlay();
}

public class Website : Iwebsite{
public string name, url;
public void setValue(string n, string u){
name = n;
url = u;
}
public void disPlay(){
Console.WriteLine("{0} {1}", name, url);
}
}

class Demo{
static void Main(string[] args) {
Website web = new Website();
web.setValue("C语言中文网", "http://c.biancheng.net");
web.disPlay();
}
}
}

在 C# 中,一个接口可以继承另一个接口,例如可以使用接口 1 继承接口 2,当用某个类来实现接口 1 时,必须同时实现接口 1 和接口 2 中的所有成员。

 

命名空间

在 C# 中,可以将命名空间看作是一个范围,用来标注命名空间中成员的归属,一个命名空间中类与另一个命名空间中同名的类互不冲突,但在同一个命名空间中类的名称必须是唯一的

命名空间的结构类似于我们计算机系统中的目录,我们可以将某个目录看作是一个命名空间,在这个目录下可以存在若干不同的文件夹,这些文件夹就可以看作是命名空间下的类。而在每个文件夹下又存放着一些文件或文件夹,这些文件和文件夹则可以看作是类中的成员。

使用命名空间的好处是可以避免命名冲突,同时也便于查找类的位置

举个简单的例子,在一个简单的 C# 程序中,假如我们要输出某些数据,就需要使用System.Console.WriteLine(),其中 System 就是命名空间,而 Console 是类的名字,WriteLine 则是具体要使用方法。也就是说,如果要访问某个命名空间中的类,我们需要使用namespacename.classname.funcname()的形式。下面通过一个示例来演示命名空间的使用:

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
namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
First.demoClass demo1 = new First.demoClass();
Second.demoClass demo2 = new Second.demoClass();
demo1.sayHello();
demo2.sayHello();
}
}
}

namespace First{
public class demoClass{
public void sayHello(){
System.Console.WriteLine("First 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

namespace Second{
public class demoClass{
public void sayHello(){
System.Console.WriteLine("Second 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

using关键字

using 关键字用来引用指定的命名空间,它可以告诉编译器后面的代码中我们需要用到某个命名空间。例如我们在程序中需要使用到 System 命名空间,只需要在程序的开始使用using System引用该命名空间即可,这时我们在使用 System 命名空间下的类时就可以将System.省略,例如Console.WriteLine();。下面通过一个示例演示一下:

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 First;
using Second;

namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
firstClass first = new firstClass();
secondClass second = new secondClass();
first.sayHello();
second.sayHello();
}
}
}

namespace First{
public class firstClass{
public void sayHello(){
Console.WriteLine("First 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

namespace Second{
public class secondClass{
public void sayHello(){
Console.WriteLine("Second 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

命名空间嵌套

命名空间可以嵌套使用,也就是说我们可以在一个命名空间中再定义一个或几个命名空间,可以使用点.运算符来访问嵌套的命名空间成员,例如namespaceName1.namespaceName2

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 First;
using First.Second;//注意这里

namespace c.biancheng.net{
class Demo{
static void Main(string[] args) {
firstClass first = new firstClass();
secondClass second = new secondClass();
first.sayHello();
second.sayHello();
}
}
}
namespace First{
public class firstClass{
public void sayHello(){
System.Console.WriteLine("First 命名空间下 demoClass 类中的 sayHello 函数");
}
}
namespace Second{
public class secondClass{
public void sayHello(){
System.Console.WriteLine("Second 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}
}

 

预处理器指令

http://c.biancheng.net/csharp/preprocessor-directives.html

 

枚举类

在使用枚举类型时有以下几点需要注意:

  • 枚举类型中不能定义方法;
  • 枚举类型具有固定的常量集;
  • 枚举类型可提高类型的安全性;
  • 枚举类型可以遍历。

默认情况下,枚举类型中的每个成员都为 int 类型,它们的值从零开始,并按定义顺序依次递增。但是我们也可以显式的为每个枚举类型的成员赋值,如下所示:

1
2
3
4
5
6
7
enum ErrorCode
{
None,
Unknown,
ConnectionLost = 100,
OutlierReading = 200
}

【示例】使用 GetValues() 遍历枚举类型中的所有成员:

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;
namespace c.biancheng.net
{
class Demo
{
enum Season {
winter = 10,
spring,
summer = 15,
autumn
};
static void Main(string[] args)
{
foreach(Season i in Enum.GetValues(typeof(Season))){
Console.WriteLine("{0} = {1}", i, (int)i);
}
Console.ReadKey();
}
}
}
winter = 10
spring = 11
summer = 15
autumn = 16

使用 GetNames() 遍历枚举类型中的所有成员:

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;
namespace c.biancheng.net
{
class Demo
{
enum Season {
winter = 10,
spring,
summer = 15,
autumn
};
static void Main(string[] args)
{
foreach(String s in Enum.GetNames(typeof(Season))){
Console.WriteLine(s);
}
Console.ReadKey();
}
}
}
//winter
//spring
//summer
//autumn

 

异常

java风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args)
{
try{
int a = 123;
int b = 0;
int x = a / b;
}catch (Exception e) {
Console.WriteLine("捕获到的异常:{0}", e);
}finally{
Console.WriteLine("finally 语句块中的代码");
}
Console.WriteLine("程序中的其它代码");
}
}
}

 

特性(Attribute)

特性(Attribute)是一种用于在程序运行时传递各种元素(例如类、方法、结构、枚举等)行为信息的声明性代码。使用特性可以将元数据(例如编译器指令、注释、描述、方法和类等信息)添加到程序中。.Net Framework 提供了两种类型的特性,分别是预定义特性和自定义特性。

在 C# 中,特性具有以下属性:

  • 使用特性可以向程序中添加元数据,元数据是指程序中各种元素的相关信息,所有 .NET 程序中都包含一组指定的元数据;
  • 可以将一个或多个特性应用于整个程序、模块或者较小的程序元素(例如类和属性)中;
  • 特性可以像方法和属性一样接受自变量;
  • 程序可使用反射来检查自己的元数据或其他程序中的元数据。

预定义特性

.Net Framework 中提供了三个预定义的属性:

  • AttributeUsage;
  • Conditional;
  • Obsolete。

AttributeUsage

预定义特性 AttributeUsage 用来描述如何使用自定义特性类,其中定义了可以应用特性的项目类型。AttributeUsage 的语法格式如下:

1
2
3
4
5
[AttributeUsage (
validon,
AllowMultiple = allowmultiple,
Inherited = inherited
)]

参数说明如下:

  • 参数 validon 用来定义特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All;
  • 参数 allowmultiple(可选参数)用来为该特性的 AllowMultiple 属性(property)提供一个布尔值,默认值为 false(单用的),如果为 true,则该特性是多用的;
  • 参数 inherited(可选参数)用来为该特性的 Inherited 属性(property)提供一个布尔值,默认为 false(不被继承),如果为 true,则该特性可被派生类继承。

示例代码如下:

1
2
3
4
5
6
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

Conditional

VSCode表示没看出怎么用

预定义特性 Conditional 用来标记一个方法,它的执行依赖于指定的预处理标识符。根据该特性值的不同,在编译时会起到不同的效果,例如当值为 Debug 或 Trace 时,会在调试代码时显示变量的值。

预定义特性 Conditional 的语法格式如下:

1
2
3
[Conditional(
conditionalSymbol
)]

如:

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
#define DEBUG
using System;
using System.Diagnostics;
namespace c.biancheng.net
{
class Demo
{
static void function1()
{
Myclass.Message("Function1 函数");
function2();
}
static void function2()
{
Myclass.Message("Function2 函数");
}
static void Main(string[] args)
{
Myclass.Message("Main 函数");
function1();
Console.ReadKey();
}
}
public class Myclass
{
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
}
}

Obsolete

预定义特性 Obsolete 用来标记不应被使用的程序,您可以使用它来通知编译器放弃某个目标元素。例如当您需要使用一个新方法来替代类中的某个旧方法时,就可以使用该特性将旧方法标记为 obsolete(过时的)并来输出一条消息,来提示我们应该使用新方法代替旧方法。

预定义特性 Obsolete 的语法格式如下:

1
2
3
4
[Obsolete (
message,
iserror
)]

语法说明如下:

  • 参数 message 是一个字符串,用来描述项目为什么过时以及应该使用什么替代;
  • 参数 iserror 是一个布尔值,默认值是 false(编译器会生成一个警告),如果设置为 true,那么编译器会把该项目的当作一个错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
namespace c.biancheng.net
{
class Demo
{
[Obsolete("OldMethod 已弃用,请改用 NewMethod", true)]
static void OldMethod()
{
Console.WriteLine("已弃用的函数");
}
static void NewMethod()
{
Console.WriteLine("新定义的函数");
}
static void Main(string[] args)
{
OldMethod();
}
}
}
//demo.cs(18,10): error CS0619: “c.biancheng.net.Demo.OldMethod()”已过时:“OldMethod 已弃用,请改用 NewMethod”

自定义特性

 

反射(Reflection)

暂时略,大概率用不上,暂时

 

索引器(Indexer)

索引器(英文名:Indexer)是类中的一个特殊成员,它能够让对象以类似数组的形式来操作,使程序看起来更为直观,更容易编写。索引器与属性类似,在定义索引器时同样会用到 get 和 set 访问器,不同的是,访问属性不需要提供参数而访问索引器则需要提供相应的参数。

http://c.biancheng.net/csharp/indexer.html

 

委托(Delegate)

C# 中的委托(Delegate)类似于 C 或 C++ 中的函数指针,是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。委托特别适用于实现事件和回调方法,所有的委托都派生自 System.Delegate 类。在实例化委托时,可以将委托的实例与具有相同返回值类型的方法相关联,这样就可以通过委托来调用方法。另外,使用委托还可以将方法作为参数传递给其他方法,

委托具有以下特点:

  • 委托类似于 C/C++ 中的函数指针,但委托是完全面向对象的。另外,C++ 中的指针会记住函数,而委托则是同时封装对象实例和方法;
  • 委托允许将方法作为参数进行传递;
  • 委托可用于定义回调方法;
  • 委托可以链接在一起,例如可以对一个事件调用多个方法;
  • 方法不必与委托类型完全匹配;
  • C# 2.0 版引入了匿名函数的概念,可以将代码块作为参数(而不是单独定义的方法)进行传递。C# 3.0 引入了 Lambda 表达式,利用它们可以更简练地编写内联代码块。匿名方法和 Lambda 表达式都可编译为委托类型,这些功能现在统称为匿名函数。

声明委托

声明委托需要使用 delegate 关键字,语法格式如下:

delegate <return type> delegate-name(<parameter list>)

其中 return type 为返回值类型,delegate-name 为委托的名称,parameter list 为参数列表。

提示:委托可以引用与委托具有相同签名的方法,也就是说委托在声明时即确定了委托可以引用的方法。

实例化委托

委托一旦声明,想要使用就必须使用 new 关键字来创建委托的对象,同时将其与特定的方法关联。如下例

1
2
3
4
public delegate void printString(string s);                      // 声明一个委托
...
printString ps1 = new printString(WriteToScreen); // 实例化委托对象并将其与 WriteToScreen 方法关联
printString ps2 = new printString(WriteToFile); // 实例化委托对象并将其与 WriteToFile 方法关联

【示例】下面通过具体的示例来演示委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。

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;
delegate int NumberChanger(int n); // 定义委托
namespace c.biancheng.net
{
class Demo
{
static int num = 10;
public static int AddNum(int p){
num += p;
return num;
}
public static int MultNum(int q){
num *= q;
return num;
}
public static int getNum(){
return num;
}
static void Main(string[] args){
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("num 的值为: {0}", getNum());
nc2(5);
Console.WriteLine("num 的值为: {0}", getNum());
Console.ReadKey();
}
}
}

多播委托(合并委托)

委托对象有一个非常有用的属性,那就是可以通过使用+运算符将多个对象分配给一个委托实例,同时还可以使用-运算符从委托中移除已分配的对象,当委托被调用时会依次调用列表中的委托。委托的这个属性被称为委托的多播,也可称为组播,利用委托的这个属性,您可以创建一个调用委托时要调用的方法列表。

注意:仅可合并类型相同的委托。

下面通过示例程序演示委托的多播:

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;
delegate int NumberChanger(int n); // 定义委托
namespace c.biancheng.net
{
class Demo
{
static int num = 10;
public static int AddNum(int p){
num += p;
return num;
}
public static int MultNum(int q){
num *= q;
return num;
}
public static int getNum(){
return num;
}
static void Main(string[] args){
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("num 的值为: {0}", getNum());
Console.ReadKey();
}
}
}

 

事件(Event)

在 C# 中,事件(Event)可以看作是用户的一系列操作,例如点击键盘的某个按键、单击/移动鼠标等,当事件发生时我们可以针对事件做出一系列的响应,例如退出程序、记录日志等等。C# 中线程之间的通信就是使用事件机制实现的。

事件需要在类中声明和触发,并通过委托与事件处理程序关联。事件可以分为发布器和订阅器两个部分,其中发布器是一个包含事件和委托的对象,事件和委托之间的联系也定义在这个类中,发布器类的对象可以触发事件,并使用委托通知其他的对象;订阅器则是一个接收事件并提供事件处理程序的对象,发布器类中的委托调用订阅器类中的方法(事件处理程序)。

有关事件我们需要注意以下几点:

  • 发布器确定何时触发事件,订阅器确定对事件作出何种响应;
  • 一个事件可以拥有多个订阅器,同时订阅器也可以处理来自多个发布器的事件;
  • 没有订阅器的事件永远也不会触发;
  • 事件通常用于定义针对用户的操作,例如单击某个按钮;
  • 如果事件拥有多个订阅器,当事件被触发时会同步调用所有的事件处理程序;
  • 在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。

若要在类中声明一个事件,首先需要为该事件声明一个委托类型,例如:

1
public delegate void delegate_name(string status);

然后使用 event 关键字来声明事件本身,如下所示:

1
2
// 基于上面的委托定义事件
public event delegate_name event_name;

上例中定义了一个名为 delegate_name 和名为 event_name 的事件,当事件触发的时侯会调用委托。

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;
namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args){
PublisherDemo e = new PublisherDemo(); /* 实例发布器类*/
SubscriberDemo v = new SubscriberDemo(); /* 实例订阅器类 */
e.MyEvent += new PublisherDemo.MyEntrust( v.printf );
e.SetValue("C语言中文网");
}
}
/***********发布器类***********/
public class PublisherDemo{
private string value;
public delegate void MyEntrust(string str);
public event MyEntrust MyEvent;
public void SetValue( string s ){
value = s;
MyEvent(value); // 触发事件
}
}
/***********订阅器类***********/
public class SubscriberDemo{
public void printf(string str){
Console.WriteLine(str);
}
}
}

 

集合(Collection)

 

指针变量与unsafe

为了保持类型的安全性,默认情况下 C# 是不支持指针的,但是如果使用 unsafe 关键字来修饰类或类中的成员,这样的类或类中成员就会被视为不安全代码,C# 允许在不安全代码中使用指针变量。在公共语言运行时 (CLR) 中,不安全代码是指无法验证的代码,不安全代码不一定是危险的,只是 CLR 无法验证该代码的安全性。因此 CLR 仅会执行信任程序集中包含的不安全代码。

指针跟cpp是差不多的,但是必须在unsafe代码块中运行。编译代码时需要在编译命令中添加-unsafe,例如csc -unsafe demo.cs或者csc /unsafe demo.cs

在 C# 中,我们使用 ToString() 来获取指针变量所指向的数据的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
namespace c.biancheng.net
{
class Demo
{
public static void Main()
{
unsafe
{
int var = 123456;
int* p = &var;
Console.WriteLine("变量 var 的值为: {0} " , var);
Console.WriteLine("指针 p 指向的值为: {0} " , p->ToString());
Console.WriteLine("指针 p 的值为: {0} " , (int)p);
}
Console.ReadKey();
}
}
}

 

参考

http://c.biancheng.net/csharp/operator-priority.html