C#入门到进阶(记录自学之路)更新至2026.3.09
阅读声明:本文纯记录自己学习时的笔记,因为自己先前有过C语言基础,因此部分细节笔记不包含其中,还有部分笔记及代码来自其他论坛和博客
C# 教程 | 菜鸟教程
https://www.runoob.com/csharp/csharp-tutorial.html该笔记会随着自己的学习不断加深和完善
希望对同为学习C#语言的你有所启发。
C#程序结构
一个C#程序主要包括以下部分
命名空间声明
class
class属性
一个Main函数
语句或者表达式
注释
using System; //命名空间Sytem引用
namespace HelloWorldApplication //命名空间声明
{
internal class HelloWorld //class属性 class方法 class类名
{
static void Main(string[] args) //main主程序入口函数
{
Console.WriteLine("Hello World");
Console.ReadKey();
/*这是一段注释*/
}
}
}
C#基本语法
函数的引用方法
using System; //引用命名空间System
namespace helloword
{
internal class Program
{
static void Main(string[] args)
{
var n = 100;
var m = 200;
var sum = 0 ;
sum = n = m;
/*该行代码表示引用System命名空间Console类中的WriteLine函数*/
System.Console.WriteLine(sum);
/*如果该文件中已经使用using 引用system,则引用Sysem中的类时,则不需要加命名空间*/
Console.WriteLine(sum);
}
}
}
C#是一种面向对象的编程语言。在面向对象的程序设计方法中,程序由各种相互交互的对象组成。相同种类的对象通常具有相同的类型,或者说,是在相同的class类中。
让我们来看看一个矩形类的实现,并借此讨论C#的基本语法:
例如,矩形拥有长length、width、面积三个属性。
using System;
namespace RectangleGetArea
{
class Rectangle
{
double width = 0;
double length = 0;
public void Acceptdetails()
{
width = 5.0;
length = 3.5;
}
public double GetArea()
{
return (width * length);
}
public void Dispay()
{
Console.WriteLine("length:{0}",length);
Console.WriteLine("width:{0}",width);
Console.WriteLine("Area:{0}",GetArea());
}
}
class this_main
{
static void Main()
{
Rectangle r= new Rectangle();
r.Acceptdetails();
r.GetArea();
r.Dispay();
}
}
}
逐层解析代码,领悟一下C#的基本语法结构。
首先 using System,该行用于引用System命名空间,方便接下来的Console.WriteLine方法的使用。
namespace RectangleGetArea,声明RectangleGetArea的命名空间
该命名空间有一个class类Rectangle,该class类中含两个成员变量和三个成员函数
成员变量
成员是类的属性或数据成员,用于存储数据。在上面的程序中,class类Rectangle有两个成员变量width和length。
成员函数
函数是执行一系列指定任务的语句集。类的成员函数是在类内声明的。刚刚举例的类Rectangle包含了三个成员函数void Acceptdetails()、double GetArea()、void Dispay()。
在类this_main中包含着main入口函数以及实例化的类Rectangle。
类的实例化
在任何一个命名空间中的类,需要在其他函数使用时,都需要将该类实例化,并赋予一个新的名称。该步骤叫做类的实例化。例如在上面程序中的, Rectangle r= new Rectangle();
这里要注意的是,与变量的声明不一样,类的实例化需要
类名 变量名=new 类名(参数);
而变量的声明则为
变量类型 变量名=初始值;
对于类(class):必须使用new来创建实例,否则变量为null,无法使用
new的作用就是分配内存并调用构造函数,让对象真正诞生。
在这里存在一个问题,就是static修饰后的函数不需要实例化,可以直接访问。因为经static修饰后,该类为静态的,在程序运行时已经自动分配了一个静态内存,而不经static修饰的类,还没有真正分配内存,因此不能直接访问函数。例如下面这段例程。
关于修饰符访问修饰符的概念,可以先不管这一部分,后续章节会讲到,只需要记住经static修饰后的类可以不用实例化。
class MyClass
{
public static void StaticMethod() { } // 静态方法
public void InstanceMethod() { } // 实例方法
}
// 调用静态方法:直接通过类名
MyClass.StaticMethod();
// 调用实例方法:必须先创建对象
MyClass obj = new MyClass();
obj.InstanceMethod();
C#方法(函数)
方法(函数)的定义和调用
为方便接下来更好地阅读代码,我调整了一下阅读顺序。在这小节中,先将方法的定义讲出来
方法就是函数,函数就是方法,只是有不同的说法。
一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块。每一个 C# 程序至少有一个带有 Main 方法的类。
要使用一个方法,您需要:
- 定义方法
- 调用方法
当定义一个方法时,从根本上说是在声明它的结构的元素。在 C# 中,定义方法的语法如下:
<Access Specifier> <Return Type> <Method Name>(Parameter List)
{
Method Body
}
//案例
static int helloworld(int i)
{
i=10;
Console.WriteLine(i);
return i;
]
- Access Specifier:访问修饰符,这个决定了变量或方法对于另一个类的可见性。
- Return type:返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型为 void。
- Method name:方法名称,是一个唯一的标识符,且是大小写敏感的。它不能与类中声明的其他标识符相同。
- Parameter list:参数列表,使用圆括号括起来,该参数是用来传递和接收方法的数据。参数列表是指方法的参数类型、顺序和数量。参数是可选的,也就是说,一个方法可能不包含参数。
- Method body:方法主体,包含了完成任务所需的指令集。
//案例
using System;
namespace this_main
{
class this_main_1
{
//定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
static int helloworld(int i)
{
Console.WriteLine(i);
i = i + 1;
return i;
}
static void Main(string[] args)
{
int b = 10, c = 0;
//调用方法传递参数b,使用c接收返回值方法返回值
c = this_main_1.helloworld(b);
Console.WriteLine(c);
}
}
}
/*输出结果为
10
11
*/
方法的参数传递
刚刚的案例中,已经初步领略了方法的参数传递和返回,现在重点说说参数传递部分。
按值传递参数
这与刚刚的案例一致,定义方法时,需定义值传递的类型以及变量名,调用方法时需定义一个相同值类型的变量,并将该变量在调用方法时传递。此时在外部定义的int b 我们叫做实参,传入方法内部的int i(这里传入的是b)我们称之为形参。
传递过程中形参相当于复制实参的数据并传入方法内部进行数据处理,但该过程中不会改变实参的数据。
将刚刚的例子改编一下便能验证这点。
//案例
using System;
namespace this_main
{
class this_main_1
{
//定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
static int helloworld(int i)
{
i = i + 1;
return i;
}
static void Main(string[] args)
{
int b = 10, c = 0;
c = this_main_1.helloworld(b); //使用返回值c接收形参
Console.WriteLine("这是实参b:{0}",b); //显示实参b
Console.WriteLine("这是形参i:{0}",c); //用c接收形参后,显示形参i
}
}
}
/*输出结果为
这是实参b:10
这是形参i:11
*/
按引用传递参数
引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。
翻译成人话就是,按引用传递参数类似与C中的指针,它把保存数据的这段内存位置传递给了方法,方法找到内存位置后进行数据处理,因此实参的数据也会跟着形参改变。
在 C# 中,使用 ref 关键字声明引用参数。下面的实例演示了这点
//案例
using System;
namespace this_main
{
class This_main_1
{
//定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
static int Helloworld(ref int i) //按引用传递
{
i ++;
return i;
}
static void Main()
{
int b = 10, c ;
c = This_main_1.Helloworld(ref b); //使用返回值c接收按引用传递的b
Console.WriteLine("这是实参b:{0}", b); //显示实参b
Console.WriteLine("这是形参i:{0}", c); //用c接收形参后,显示形参i
}
}
}
/*输出结果为
这是实参b:11
这是形参i:11
*/
按输出传递参数
按输出传递参数与按引用参数传递有点类似,其都是将一段内存位置传递给方法后,方法内部找到该段内存位置对数据内容进行处理。
在 C# 中,使用 out 关键字声明引用参数。下面的实例演示了这点
//案例
using System;
namespace this_main
{
class This_main_1
{
//定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
static void HelloWorld(out int i)
{
i = 50; //方法内将i赋值为50
}
static void Main()
{
int a = 0; //赋初始值0
Console.WriteLine("这是未调用方法前的a:{0}",a);
HelloWorld(out a); //调用方法
Console.WriteLine("这是调用方法后的a:{0}",a);
}
}
}
/*输出结果为
这是未调用方法前的a:0
这是调用方法后的a:50
*/
out与ref的不同
按引用传递和按输出传递效果一致,但有如下差异。
1、ref可进可出,变量具体数据内容可以传递给方法,方法内部也可以读取和修改这个值,修改后的值方法结束后传递出去。
2、out只出不进,变量在传入时可以不赋值,方法内部不能读取和修改它的原始值,但必须在返回前给它赋值,调用方法拿到的是方法赋的值。
案例1:ref可进可出
//案例
using System;
namespace this_main
{
class Program
{
static void Main()
{
int a = 10; // 必须初始化,因为要“进”
Console.WriteLine($"调用前 a = {a}"); // 输出 10
ModifyWithRef(ref a);
Console.WriteLine($"调用后 a = {a}"); // 输出 30
}
static void ModifyWithRef(ref int num)
{
// 可以读取传入的值
Console.WriteLine($"方法内读取到传入的值: {num}"); // 输出 10
// 可以修改
num = num + 20; // 读取并修改
// 修改后,方法结束时会带出去
}
}
}
输出:
text
调用前 a = 10 方法内读取到传入的值: 10 调用后 a = 30
案例二:out只出不进
//案例
using System;
namespace this_main
{
class Program
{
static void Main()
{
int b; // 可以不初始化
// Console.WriteLine(b); // 这行会编译错误,因为 b 未赋值
ModifyWithOut(out b);
Console.WriteLine($"调用后 b = {b}"); // 输出 50
}
static void ModifyWithOut(out int num)
{
// 下面这行如果取消注释,会编译错误:使用了未赋值的 out 参数
// Console.WriteLine(num); // ❌ 错误!不能读取 out 参数的值
num = 50; // 必须赋值,否则编译错误
//num = num + 50; // ❌ 错误! out只出不进,它不知道自己进来的数据是多少,+50后的结果不确定
}
}
}
输出:
调用后 b = 50
总结表格
| 特性 | ref |
out |
|---|---|---|
| 调用前必须初始化 | 是 | 否 |
| 方法内能否读取参数 | 能 | 不能(编译器禁止) |
| 方法内必须赋值 | 不一定(可以只读取) | 必须(在返回前) |
| 典型用途 | 修改已有变量 | 返回多个值(如 TryParse) |
具名参数与不具名参数
方法的参数传递时分为具名参数和不具名参数。
不具名参数案例
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
PrintInfo("Tim", 50);
}
static void PrintInfo(string Name,int age)
{
Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
}
}
}
//结果输出Hello:Tim,Your age:50
具名参数案例
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
PrintInfo(Name:"Tim",age:50); //具名参数
}
static void PrintInfo(string Name,int age)
{
Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
}
}
}
//结果输出Hello:Tim,Your age:50
具名参数相比不具名参数优点
1、提高代码可读性,直接将方法定义的形参名表达,不用再跳转到方法定义时,查看该参数传递的意义。
2、使用具名参数后不再受到参数列表的限制,例如可以这样写
PrintInfo(age:50,Name: "Tim");
可选参数
若方法构造时参数具有默认值,当调用方法时没有对应参数输入,方法内部将使用默认参数
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
PrintInfo(); //这里不输入参数
}
static void PrintInfo(string Name = "Tim", int age =50) //参数具有默认值,将直接采用默认值
{
Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
}
}
}
//结果输出Hello:Tim,Your age:50
扩展方法
扩展方法的注意事项如下
1、方法必须是公有、静态的,即被public static修饰
2、必须是形参列表中的第一个,由this 修饰
3、必须由一个静态类(一般类名为SomeTypeExtension)来统一收纳SomeType类型的扩展方法
通过一个实例来理解,该例中通过一个Math.Round(值,位数)方法来四舍五入一个小数
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
double x = 3.14159;
double y=Math.Round(x,4);
Console.WriteLine(y);
}
}
}
//结果输出3.1416
在该例中,扩展方法的作用可能不太明显,扩展方法的主要作用为,为现有的方法扩展用途
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
double x = 3.14159;
double y =x.DoubleRound(4);//构造了一个拓展方法,来对原来的Math.Round进行拓展
Console.WriteLine(y);
}
}
static class DoubleExtension //拓展方法必须在一个静态类中
{
public static double DoubleRound(this double number, int i) //构造扩展方法,该方法必须是公有的 静态的
{
double result=Math.Round(number,i);
return result;
}
}
}
//结果输出3.1416
这个先不纠结,知道有这么一个东西就行。
C#数据类型
在C#中,变量分为以下几种类型:
值类型(Value types)
引用类型(Regerence types)
指针类型(Pointer types)
值类型(Value types)

需要注意的是,变量类型的尺寸并不固定,其尺寸根据具体运行的平台决定,可以使用sizeof(int)的方法获取int类型的存储尺寸。
在某些PLC产品中,int尺寸就为16有符号整数,与C#中的32位有区别

引用类型(Regerence types)
对象(Object)类型
对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。
当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱。
/*使用代码进行解释装箱与拆箱*/
int i=10;
object o=i; //装箱
i=20;
console.WriteLine("object");
/*此时i=20, object=10*/
object o=10;
int x=(int)o; //拆箱,此时x=10
x=30;
/*此时x=30, o还是10*/
//多引用
object o1=10;
object o2=o1;
o1=20;
/*o2还是10*/
通俗来讲,传统的值类型接受和传递需要类型匹配,而object的装箱则不需要考虑值类型(int? short? float?),仅将值类型变量的数据复制出来并隔离。
由于装箱和拆箱需要耗费大量内存和性能,现在已有泛型代替,很少使用object。
字符串(String)类型
字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。
string str = "123\n456";
/*输出结果为:
123
456*/
C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:
string str = @"123\n456";
/*输出结果为:
123\n456*/
/*转义字符
编译器在执行string字符串时,针对某些特定字符将不会当做字符串处理,
而是当做转义字符处理,使之具有特殊功能
例如 \n 将被处理为换行
*/
指针类型(Pointer types)
指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。
声明指针类型的语法:
int* p1, p2, p3; // 正确
int *p1, *p2, *p3; // 错误

C#类型转换
在C#中,类型转换是将一个数据类型的值转换为另一个数据类型的过程。
C#类型转换分为隐式类型转换和显式类型转换(强制类型转换)。
隐式类型转换
隐式类型转换不需要编写指定的代码来执行转换,编译器会自动进行。
隐式类型转换只发生在从小范围数据到较大范围数据类型时,编译器会将其自动转换,这不会导致数据丢失。
例如:从int 到 long ,float 到double
int a = 10;
long b =a;
long c;
c=100+a;
float j=1.32;
double i= j;
显式类型转换
显式类型转换类似与C中的强制类型转换,需要程序员在代码中明确指定。
因为从小范围到大范围数据类型转换,通常编译器自动执行,因此编写显式类型转换时,可能存在数据丢失或数据紊乱的情况。
C#中自带了很多类型转换函数,目前先介绍两种转换方式,其他的遇到了再详细查看C#官方手册或者询问AI。
第一种转换方式
变量 =(显式变量类型)变量;
b = ( byte ) i ;
/*强制转换为byte类型*/
int i=10;
byte b = (byte)i; //该过程i丢失高位24位,整数截断规则:丢失高位,保留低位
/*强制转换为整数类型*/
double doubleValue=3.1415926
int intValue=(int)doubleValue; //该过程丢失小数部分,保留整数部分,intValue为3
/*double强制转换为float*/
double doubleValue=3.1415926465;
float floatValue=doubleValue; //该过程保留前七位小数,向零取整规则,结果为3.1415926
第二种转换方式
第二种转换方式是一种较为安全的转换方式,在第一种转换方式中,可能存在变量为null(空)时的异常情况。该类方法都定义在System.Convert类中,使用时需要using System命名空间。
转换失败时,将会触发报错。
| 序号 | 方法 | 描述 |
| 1 | ToBoolean | 转换为布尔型 |
| 2 | ToByte | 转换为字节类型 |
| 3 | ToChar | 转换为Char类型 |
| 4 | ToDateTime | 转换为日期-时间结果 |
| 5 | ToDecimal | 转换为十进制类型 |
| 6 | ToDouble | 转换为双精度浮点型 |
| 7 | ToInt16 | 转换为16位整数 |
| 8 | ToInt32 | 转换为32位整数 |
| 9 | ToInt64 | 转换为64位整数 |
| 10 | ToSbyte | 转换为有符号字节类型 |
| 11 | ToSingle | 转换为小浮点数类型 |
| 12 | ToString | 转换为字符串类型 |
| 13 | ToType | 转换为指定类型 |
| 14 | ToUInt16 | 转换为无符号16位整数 |
| 15 | ToUInt32 | 转换为无符号32位整数 |
| 16 | ToUInt64 | 转换为无符号64位整数 |
using System;
namespace TypeConversionApplication
{
class StringConversion
{
static void Main(string[] args)
{
// 定义一个整型变量
int i = 75;
// 定义一个浮点型变量
float f = 53.005f;
// 定义一个双精度浮点型变量
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();
}
}
}
C#中变量定义
变量定义较为简单
变量类型 变量名;//无初始化
变量类型 变量名=0;//初始化为0 通常建议以下这种,赋予默认值
与C一样,变量的定义需要遵从以下规则。
1、变量名可以包括字母、数字和下划线。
2、变量名必须以字母或下划线开头。
3、变量名区分大小写。
4、避免使用C#的关键字作为变量名。关键字通常为蓝色高亮显示
C#变量的作用域
C#中变量定义在不同位置,其作用区域不一样,不同的作用域变量不能直接访问。
局部变量
在方法、循环、条件语句等代码块内声明的变量是局部变量,它们只在声明它们的代码块中可见。
void MyMethod() { int localVar = 10; // 局部变量 // ... for (int i = 0; i < 5; i++) { // i 在循环体内可见 } // i 在这里不可见 } // localVar 在这里不可见
全局变量
在类的成员级别定义的变量是成员变量,它们在整个类中可见。另外,变量无法在命名空间中定义。
namespace YourClass
{
class MyClass
{
int memberVar = 30; // 成员变量,在整个类中可见
}
}
C#判断
/*if的使用与C中一致*/
if(判断条件) /*需要注意的是等于为==,不等于为!=*/
{
/*if()
{
}
else
{
} 可嵌套使用 */
}
else if()
{
}
else
{
/*该else为 else if 的else*/
}
if判断语句还有一个简化的条件运算符
Exp1 ? Exp2 : Exp3;其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置。
? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个 ? 表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值。
/*另外还有case语句的用法*/
swtich (判断状态变量){
case 1:
....
break;
case 2:
....
break;
case 3:
....
break;
}
需要注意的是C#的Switch语句需要遵从如下规则
1、C/C++中运行case语句中没有braek,而C#中要求,每个case语句背后必须包括显示的跳转语句 如下 braek,return,go to case。
2、switch语句中的判断状态变量,必须是一个整型或者枚举类型,或者是一个class类型,其中class有一个单一的转换函数将其转换为整型或枚举类型。而C/C++对此没有要求,可以为字符型等其他类型。
C#循环
C#中While循环
while(condition)
{
statement(s);
}
在这里,statement(s) 可以是一个单独的语句,也可以是几个语句组成的代码块。condition 可以是任意的表达式,当为任意非零值时都为真。当条件为真时执行循环。
当条件为假时,程序流将跳过循环执行,while的下一条语句,而不是执行while循环中的语句。

C#中的for/foreach循环
for ( init; condition; increment )
{
statement(s);
}
for(int i=1;i<10;i++)
{
Console.WriteLine(i);
}
1、init会被首先执行且只执行一次,在这里可以用于声明并初始循环控制变量.
2、接下来,会先判断condition。如果为真,则执行statement(s),如果为假则结束循环,跳到for循环的下一跳语句。
3、在执行完statement(s)后,控制流会跳回并执行上面的increment。
4、执行完上面的increment后,再次判断condition,如果为真则继续循环。

由于编程者经常使用fou循环遍历数组元素,因此C# 支持 foreach 循环,C# 的 foreach 循环可以用来遍历集合类型,例如数组、列表、字典等。它是一个简化版的 for 循环,使得代码更加简洁易读。
foreach (var item in collection)
{
// 循环
}
item是当前遍历到的元素,collection是需要遍历的数组
using System;
using System.Xml.Linq;
namespace this_main
{
class this_main_1
{
static void Main(string[] args)
{
int[] fibarray = new int[] { 5, 2, 0, 1, 3, 1, 4 };
//使用foreach 遍历数组的每个元素并输出 结果为 5 2 0 1 3 1 4
foreach (var element in fibarray)
{
Console.WriteLine(element);
}
Console.WriteLine();
//使用for循环遍历数组的每个元素并输出,结果为 5 2 0 1 3 1 4
for (int i = 0; i < fibarray.Length; i++)
{
Console.WriteLine(fibarray[i]);
}
//使用foreach遍历数组的每个元素,并使用count计算数组内元素的数量
int count = 1;
foreach (var element in fibarray)
{
Console.WriteLine("ordinal number:{0} elment:{1}",count,element);
count++;
}
Console.WriteLine("Number of element in the array:{0}",count);
}
}
}
C# do...while 循环
不像 for 和 while 循环,它们是在循环头部测试循环条件。do...while 循环是在循环的尾部检查它的条件。
do...while 循环与 while 循环类似,但是 do...while 循环会确保至少执行一次循环。
do
{
statement(s);
}while( condition );
请注意,条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。
如果条件为真,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。这个过程会不断重复,直到给定条件变为假为止。

using System;
namespace Loops
{
class Program
{
static void Main(string[] args)
{
/* 局部变量定义 */
int a = 10;
/* do 循环执行 */
do
{
Console.WriteLine("a 的值: {0}", a);
a = a + 1;
} while (a < 20);
Console.ReadLine();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
a 的值: 10
a 的值: 11
a 的值: 12
a 的值: 13
a 的值: 14
a 的值: 15
a 的值: 16
a 的值: 17
a 的值: 18
a 的值: 19
循环控制语句
循环控制语句更改执行的正常序列。当执行离开一个范围时,所有在该范围中创建的自动对象都会被销毁。
C# 提供了下列的控制语句。
| break语句 | 终止 循环 或 switch 语句,程序流将继续执行紧接着 循环 或 switch 的下一条语句。 |
| continue | 跳过本轮循环,开始下一轮循环。 |
C#封装
C#封装被定义为“把一个或多个项目封闭在一个物理的或者逻辑的包中”。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。
需要注意的是访问修饰词,只能用来修饰class类成员(class、方法、成员变量等),不能在方法中修饰。
//案例 using System; namespace this_main { public int a=0; //❌ ,这不是class类成员 public class This_main_1 //✅,这属于class类成员 { public int b=0; //✅,这属于class类成员 static int Helloworld(int i) //✅,这属于class类成员 { public int c=0; //❌ ,这不是class类成员 } static void Main() { } } } /*输出结果为 这是实参b:10 这是形参i:11 */
C#封装根据具体的需要,设置使用者的访问权限,并通过访问修饰符来实现。

这里用程序和示意图的形式来逐个理解,以下所有的成员都处于同一个namespace RectangleApplication 下,不存在不同命名空间的情况,在不同命名空间的情况下,会在后面进行解释。
Public 访问修饰符
using System;
namespace RectangleApplication
{
class Rectangle
{
//成员变量
public double length;
public double width;
public double GetArea()
{
return length * width;
}
public void Display()
{
Console.WriteLine("长度: {0}", length);
Console.WriteLine("宽度: {0}", width);
Console.WriteLine("面积: {0}", GetArea());
}
}// Rectangle 结束
class ExecuteRectangle
{
static void Main(string[] args)
{
Rectangle r = new Rectangle();
r.length = 4.5;
r.width = 3.5;
r.Display();
Console.ReadLine();
}
}
}
该例中,class Rectangle在class ExecuteRectangle被实例化为r
该实例化r中的成员变量length、width均可在class ExecuteRectangle被访问和修改
Private 访问修饰符
using System;
namespace RectangleApplication
{
class Rectangle
{
// 私有成员变量
private double length;
private double width;
// 公有方法,用于从用户输入获取矩形的长度和宽度
public void AcceptDetails()
{
Console.WriteLine("请输入长度:");
length = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("请输入宽度:");
width = Convert.ToDouble(Console.ReadLine());
}
// 公有方法,用于计算矩形的面积
public double GetArea()
{
return length * width;
}
// 公有方法,用于显示矩形的属性和面积
public void Display()
{
Console.WriteLine("长度: {0}", length);
Console.WriteLine("宽度: {0}", width);
Console.WriteLine("面积: {0}", GetArea());
}
}//end class Rectangle
class ExecuteRectangle
{
static void Main(string[] args)
{
// 创建 Rectangle 类的实例
Rectangle r = new Rectangle();
// 通过公有方法 AcceptDetails() 从用户输入获取矩形的长度和宽度
r.AcceptDetails();
// 通过公有方法 Display() 显示矩形的属性和面积
r.Display();
Console.ReadLine();
}
}
}
该例中,class Rectangle在class ExecuteRectangle被实例化为r
该实例化r中的成员变量length、width不能在class ExecuteRectangle中被访问和修改
只能使用公有方法对成员变量进行修改访问。
打个比方,实例化r为你的银行卡,r中一个变量,我们称之为余额,余额为私有成员变量,不能被外部直接访问修改,只可以通过某些方法、接口修改余额变量,例如纸币存取、ATM等
之所以衍生出访问修饰符,因为程序设计者希望类中的某些变量被保护起来,不能被外部直接修改和访问,这有助于变量的保护和安全。
注意事项:
1、如果一个class被Private修饰,那么即便它内部成员为public,那么其他class也不能访问。因为class本身对外不可见。
Internal 访问修饰符
该访问修饰符修饰后,只能在同一个程序集的对象可以访问。
我们先解释程序集的概念。
程序集是 .NET 中代码编译、版本控制和部署的基本单位。简单来说,它就是你编译项目后生成的那个文件——要么是 .exe(可执行程序),要么是 .dll(类库)。
代码示例说明程序集概念
假设你有两个项目:
项目 A:类库(ClassLibrary.dll)
// ClassLibrary.dll
namespace MyLibrary
{
public class PublicClass
{
public void Hello() { }
}
internal class InternalClass
{
public void Secret() { }
}
}
项目 B:控制台应用(ConsoleApp.exe),引用了 ClassLibrary.dll
// ConsoleApp.exe
using MyLibrary;
class Program
{
static void Main()
{
var pub = new PublicClass(); // ✅ 可以,PublicClass 是 public
pub.Hello(); // ✅ 可以
// var inter = new InternalClass(); // ❌ 编译错误,InternalClass 是 internal
// 因为它在另一个程序集(ClassLibrary.dll)中,而 internal 只在本程序集内可见
}
}
Protected、Protected Internal 访问修饰符
这两个修饰符将在后面的继承中提到。
C#数组
数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,通常认为数组是一个同一类型变量的集合。
声明数组变量并不是声明 number0、number1、...、number99 一个个单独的变量,而是声明一个就像 numbers 这样的变量,然后使用 numbers[0]、numbers[1]、...、numbers[99] 来表示一个个单独的变量。数组中某个指定的元素是通过索引来访问的。
所有的数组都是由连续的内存位置组成的。最低的地址对应第一个元素,最高的地址对应最后一个元素。

声明数组
在 C# 中声明一个数组,您可以使用下面的语法:
datatype[] arrayName;
datype为值类型,arrayName为数组名称
初始化数组
声明一个数组不会在内存中初始化数组。当初始化数组变量时,您可以赋值给数组。
数组是一个引用类型,所以您需要使用 new 关键字来创建数组的实例。
例如:
//可以先声明,后使用new创建
int []arrayName;
arrayName =new int[10]{ 1 , 2 ,3 };
//也可以声明并初始化
int[] arraayName = new int[10] ;
赋值给数组
您可以通过使用索引号赋值给一个单独的数组元素,比如:
int[] arrayName=new int[10];
arrayName[0]=4500;
您可以在声明数组的同时给数组赋值,比如:
int[] arrayName={1,2,3};
您也可以声明并初始化一个数组,比如:
int [] arrayName=new int[3]{1,2,3};
在上述情况下,你也可以省略数组的大小,比如:
int [] arrayName=new int[]{1,2,3};
您也可以赋值一个数组变量到另一个目标数组变量中。在这种情况下,目标和源会指向相同的内存位置:
int [] marks = new int[] { 99, 98, 92, 97, 95};
int[] score = marks;
当您创建一个数组时,C# 编译器会根据数组类型隐式初始化每个数组元素为一个默认值。例如,int 数组的所有元素都会被初始化为 0。
数组参数
假设我需要把一个数组传入方法中,并由方法进行数据处理后打印
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
int[] _main_array = new int[] { 1, 2, 3 };
int sum = arrayName_metod(_main_array);
Console.WriteLine(sum);
}
static int arrayName_metod(int[] array)
{
int sum = 0;
foreach (var item in array) //遍历数组并累加
{
sum = sum + item;
}
return sum;
}
}
}
//结果输出为6
在该案例中,我们需要先声明并初始化一个数组后,传入方法arrayName_metod进行数据处理,这里就比较麻烦,微软提供了params关键字。在方法构造时直接使用params关键字便可以不用声明数组,直接代入数组数据
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
int sum = arrayName_metod(1,2,3); //直接代入数组数据
Console.WriteLine(sum);
}
static int arrayName_metod(params int[] array) //params关键字修饰
{
int sum = 0;
foreach (var item in array) //遍历数组并累加
{
sum = sum + item;
}
return sum;
}
}
}
//结果输出为6
需要注意的是方法中的数组参数必须是形参参数表中的最后一个,由params修饰。
C#进阶内容
至此,C#的入门内容基本写完了,其中有部分例如算术运算符、逻辑运算符、常量、操作符等一小部分知识,因为与C是一致的,因此在这里我就不一一详细讲了。
C#委托
什么是委托?
在C中我们可以使用指针去传递地址,从而在多个函数操纵同一个变量。但是在C#中,指针被认为是不安全的代码,因此C#设计了一系列方法去避免使用指针,但是实现指针的功能。
例如函数实参形参传递避免使用指针,从而有按引用传递ref、按输出传递out
在函数指针中,C#构造了委托这么一个东西,去代替函数指针。
1、委托(delegate)是函数指针的"升级版"
2、一切都是地址
变量(数据)是保存在机器内存中的一段地址中,那么函数(算法)其实也有一段地址,在方法参数传递中,我们知道了out、ref相当于将内存地址传递给方法,那么委托与其类似,是将方法的内存地址传递给某个地方进行使用。
3、直接调用与间接调用
直接调用:通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并开始执行->返回
间接调用:通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行->返回
委托的简单使用
Action委托
Func委托
C#自带委托
Action委托
用于指向无形参、无返回值的方法
其声明方法为
Action 委托名 =new Action(类.方法)
//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
calculator.Report(); //直接调用calculator.Report
action.Invoke(); //间接通过action.Invoke调用
action(); //间接调用,仿函数指针用法
}
}
class Calculator
{
public void Report() //无返回值、无形参的方法
{
Console.WriteLine("我有三个方法");
}
}
}
/*结果输出
我有三个方法
我有三个方法
我有三个方法
*/
另外,Action委托还有一个泛型为Action<>
Action<>是Action的是一个泛型,它表示支持封装一个带参数、无返回值的方法
例如
Action<int> Action_1=new Action<int>(目标方法) //目标方法带一个int类型参数参数
Action<int,int> Action_2=new Action<int,int>(目标方法) //目标方法带两个参数
Fuction委托
针对有返回值,有形参的方法,我们采用Fuction委托
其声明方法为
Func<参数类型表,返回值>委托名=new Fun<<参数类型表,返回值>>(类.方法)
注意:VS输入时Func具有17个泛型,重载可以将鼠标放入<>中选择,我们选择第三个泛型,该泛型具有两个参数传递,一个返回值

//案例
using System;
using System.Diagnostics.Eventing.Reader;
namespace this_main
{
class Program
{
static void Main()
{
Calculator C1 = new Calculator();
Func<int, int, int> func1 = new Func<int, int, int>(C1.Add);
Func<int,int,int> func2 = new Func<int, int, int>(C1.Sub);
int a = 100;
int b = 50;
int c = 0;
c=func1.Invoke(a, b);
Console.WriteLine(c);
c=func2.Invoke(a, b);
Console.WriteLine(c);
}
}
class Calculator
{
public void Report()
{
Console.WriteLine("我有三个方法");
}
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;
}
}
}
/*结果输出
150
50
*/
自定义委托的声明
讲解了 C# 自带的Action和Function两种委托的用法和声明,现在来具体讲一讲自定义委托的声明
首先委托是一种类(class),类是数据类型所以委托也是一种数据类型。
它的声明方式与一般的类不同
声明方式
public delegate 返回类型 委托名(参数类型 参数名, ...);
例如以下代码,我们定义一个接受两个整数并返回一个整数的委托:
public delegate int MathOperation(int x, int y);
与类平级,需要声明在命名空间下。
如果不小心写在了类里面,就变成了嵌套类,编译器也可以通过,程序也可以正常运行,但有风险
/*案例代码*/
using System;
namespace helloword
{
public delegate int Calc(int x, int y); //自定义声明委托
internal class Program
{
public delegate int class_Cacl(int x, int y); //嵌套类自定义声明委托
static void Main(string[] args)
{
}
}
}
/*案例代码*/
using System;
namespace helloword
{
public delegate int Calc(int x, int y); //自定义声明委托
internal class Program
{
static void Main(string[] args)
{
int x = 10;
int y = 20;
Program_2 program_2=new Program_2(); //实例化Program_2
Calc calc_1 = new Calc(program_2.Add); //实例化自定义委托calc_1
Calc calc_2 = new Calc(program_2.Sub); //实例化自定义委托calc_2
Console.WriteLine(calc_1.Invoke(x,y)); //Invoke调用
Console.WriteLine(calc_1(x,y)); //函数指针用法调用
Console.WriteLine(calc_2.Invoke(x, y)); //Invoke调用
Console.WriteLine(calc_2(x, y)); //函数指针用法调用
}
}
public class Program_2
{
public int Add(int a,int b)
{
return a + b;
}
public int Sub(int a, int b)
{
return a - b;
}
}
}
/*输出结果
*30
-10
*/

委托的简单应用
模板方法
什么是模板方法?
模板方法是委托的应用方式之一。就像做菜:无论做什么菜,流程都是“洗菜 → 切菜 → 烹饪 → 装盘”。但“烹饪”这一步不同:红烧肉用炖,炒青菜用炒。
那么我们就可以构建一个模板方法,其中包含洗菜切菜 装盘的固定流程,至于烹饪这个可变流程就可以交给委托去完成。

/*案例代码*/
using System;
namespace helloword
{
internal class Program
{
static void Main(string[] args)
{
CookDish("青菜", chao);
CookDish("红烧肉", dun);
}
/*定义一个模板方法,标准做菜流程,唯一的区别是菜不同
* 因此有两个输入,dishName为菜的名字,委托Funtion cookMethod为不同菜时的烹饪做法,有一个string类型的参数和int返回值*/
static void CookDish(string cookName, Func<string,int> cookMethod)
{
Console.WriteLine("1. 洗菜");
Console.WriteLine("2. 切菜");
cookMethod(cookName);
Console.WriteLine("4. 装盘");
Console.WriteLine("-------------------");
}
//定义具体的烹饪方法
//炖
static int dun(string Name)
{
Console.WriteLine("3.把{0}放进锅里炖30分钟", Name);
return 0;
}
//炒
static int chao(string Name)
{
Console.WriteLine("3.把{0}放进锅里炒5分钟", Name);
return 0;
}
}
}
结果输出
1. 洗菜
2. 切菜
3.把青菜放进锅里炒5分钟
4. 装盘
-------------------
1. 洗菜
2. 切菜
3.把红烧肉放进锅里炖30分钟
4. 装盘
-------------------

回调方法
什么是回调方法?
就像你去银行办理业务:你把银行卡交给柜台,然后去逛街。等业务办好了,工作人员打电话通知你来取。这个“打电话通知”就是回调。
回调方法就是:你先告诉程序“事情做完后,请调用这个方法通知我”。
/*案例代码*/
using System;
namespace helloword
{
public class Program
{
static void Main()
{
//1、创建一个办事员类
Clerk clerk = new Clerk();
//办理业务
Console.WriteLine("提交银行卡");
Console.WriteLine("我先去逛下街");
//办理完成后通过这个CallMe委托方法提醒我HandleBuiness方法执行完成
clerk.HandleBuiness(CallMe);
}
static void CallMe()
{
Console.WriteLine("办理完成,请回来取银行卡");
}
}
public class Clerk
{
//业务办理过程
public void HandleBuiness(Action Callback)
{
Console.WriteLine("正在办理业务");
// 模拟耗时操作(用 Sleep 代替,只是为了演示)
System.Threading.Thread.Sleep(2000); // 暂停2秒
Callback(); //执行委托函数
}
}
}
结果输出
提交银行卡
我先去逛下街
正在办理业务
办理完成,请回来取银行卡
无论是模板方法还是回调方法,本质上其实都是将委托作为参数输入进方法中,并在方法中跳转至委托方法并执行委托方法。
委托的缺点
1、这是一种方法级别的紧密耦合,现实工作中使用时需要谨慎。
2、委托会使可读行、调试难度增加,因为委托会使程序在方法中跳转,增加程序失控风险
3、把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护。
4、委托使用不当可能造成内存泄漏和程序性能下降。
委托的高级应用
多播(multicast)委托
刚刚上面所有的案例都是一个委托封装一个方法。
多播委托就是一个委托封装多个方法,方法的执行顺序为封装的顺序。

多播委托的声明与自定义委托一致。
通过以下符号,操作多播委托
+= 新增封装方法
-= 减少已有方法
using System;
public class Program
{
//自定义一个委托无返回值 无参数
public delegate void My_Ac();
//新建一个静态方法My_Mthod1
public static void My_Mthod1()
{ Console.WriteLine("第一个方法被执行"); }
//新建一个静态方法My_Mthod2
public static void My_Mthod2()
{ Console.WriteLine("第二个方法被执行"); }
//新建一个静态方法My_Mthod3
public static void My_Mthod3()
{ Console.WriteLine("第三个方法被执行"); }
public static void Main()
{
My_Ac My_Ac_1 = new My_Ac(My_Mthod1); //实例化委托My_Ac_1
My_Ac_1 +=My_Mthod2; //加入My_Mthod2方法
My_Ac_1 += My_Mthod3; //加入My_Mthod3方法
My_Ac_1.Invoke(); //运行委托
Console.WriteLine(); //空行
My_Ac_1 -= My_Mthod3; //减去My_Mthod3方法
My_Ac_1.Invoke(); //运行委托
}
}
运行结果:
第一个方法被执行
第二个方法被执行
第三个方法被执行第一个方法被执行
第二个方法被执行
刚刚例子多播委托封装的三个方法,无返回值,无参数输入。这对我们来说比较好理解,
但是如果封装的方法有返回值和参数,C#是如何处理的?
using System;
public class Program
{
// 1. 定义一个委托类型:接受 int,返回 int
public delegate int IntOperation(int x);
// 几个符合该自定义委托的方法
public static int Square(int x)
{
Console.WriteLine($"Square({x}) 被执行");
return x * x;
}
public static int Double(int x)
{
Console.WriteLine($"Double({x}) 被执行");
return x * 2;
}
public static int SubtractOne(int x)
{
Console.WriteLine($"SubtractOne({x}) 被执行");
return x - 1;
}
public static void Main()
{
// 2. 创建委托实例,指向 Square
IntOperation op = Square;
// 3. 添加更多方法
op += Double;
op += SubtractOne;
// 4. 调用多播委托(传入参数 5)
Console.WriteLine("调用 op(5):");
int result = op(5);
Console.WriteLine($"最终返回结果:{result}\n");
}
}
结果输出:
调用 op(5):
Square(5) 被执行
Double(5) 被执行
SubtractOne(5) 被执行
最终返回结果:4
通过该案例可以看到,如果多播委托封装的方法含参数输入、返回值,C#会进行如下处理。
1、将委托的参数传递给每一个封装的方法(如果方法有参数输入),并由每一个方法进行独立的处理。
2、多播委托的运行结果(即多播委托返回值),只会返回最后一个方法的返回值。
同步与异步
这里先区分一下概念,由于翻译问题,中文语境的同步异步与英文语境下的同步有所差异
中文语境下:
同步,我们同步进行
英文语境下:
同步,你先做完了我(在你的基础上)继续做。
在C#中,同步和异步概念如下。
同步:我们同处于一个流水线,你在我之上,程序先分配给你做完后,我在你的基础上继续做

异步:我们同时进行,各自独立输出结果。

有了同步和异步的概念后,我们发现,在以往的例程中,方法都属于同步调用,按照程序执行顺序逐步调用方法。
同步调用与异步调用
1、每一个运行的程序是一个进程(process)。
2、每个进程可以有一个或者多个线程(thread)。
3、同步调用是在同一线程内,可以理解为流水线模式。
4、异步调用的底层机理是多线程,可以理解为环形树状模式。
同步调用的三种形式
1、直接同步调用:使用方法名
2、使用单播委托的Invoke方法
3、使用多播委托的Invoke方法
异步调用的三种方式
1、隐式异步调用:使用委托的BeginInvoke(该方式基本已被淘汰)
2、显式异步调用:使用Thread,通过新建线程,并将委托放入线程执行的方式实现异步调用效果。
3、显示异步调用,使用Task通过新建线程,并将委托放入线程执行的方式实现异步调用效果。
案例一:Thread显式异步调用
声明方式 :
Thread 线程名= new Thread(ThreadStart(方法/委托);
Thread thread = new Thread(new ThreadStart(action_2));
线程启动用法
线程名.Start();
thread.Start();
using System;
using System.Threading;
namespace helloword
{
class My_main
{
static void Main()
{
My_Method my_method = new My_Method();
// 创建委托
Action action_1 = new Action(my_method.Method_1);
Action action_2 = new Action(my_method.Method_2);
Action action_3 = new Action(my_method.Method_3);
// 同步执行方法一
action_1.Invoke();
// 显式使用 Thread 异步执行方法二
Thread thread_1 = new Thread(new ThreadStart(action_2));
/*这样也可以*/
//Thread thread_1 = new Thread(new ThreadStart(my_method.Method_2));
thread_1.Start();
// 同步执行方法三
action_3.Invoke();
Console.WriteLine("主线程继续执行...");
Console.ReadLine(); // 等待用户输入,防止控制台提前退出,以便观察异步输出
}
}
class My_Method
{
public void Method_1()
{
Console.WriteLine("方法一被执行 ");
}
public void Method_2()
{
Console.WriteLine("方法二被执行");
}
public void Method_3()
{
Console.WriteLine("方法三被执行");
}
}
}
输出结果
方法一被执行
方法三被执行
主线程继续执行...
方法二被执行
案例二:Task异步调用
声明方式:
Task Task名 =newTask(new Action(方法/委托))
线程启动
Task名.Start();
using System;
using System.Threading;
using System.Threading.Tasks;
namespace helloword
{
class My_main
{
static void Main()
{
My_Method my_method = new My_Method();
// 创建委托
Action action_1 = new Action(my_method.Method_1);
Action action_2 = new Action(my_method.Method_2);
Action action_3 = new Action(my_method.Method_3);
// 同步执行方法一
action_1.Invoke();
// 显式使用 Task步执行方法二
Task task_1=new Task(new Action(action_2));
/*这样也可以*/
//Task task_1 = new Task(new Action(my_method.Method_2));
task_1.Start();
// 同步执行方法三
action_3.Invoke();
Console.WriteLine("主线程继续执行...");
Console.ReadLine(); // 等待用户输入,防止控制台提前退出,以便观察异步输出
}
}
class My_Method
{
public void Method_1()
{
Console.WriteLine("方法一被执行 ");
}
public void Method_2()
{
Console.WriteLine("方法二被执行");
}
public void Method_3()
{
Console.WriteLine("方法三被执行");
}
}
}
输出结果
方法一被执行
方法三被执行
主线程继续执行...
方法二被执行
C#事件(Event)
事件的概念
使用:用于对象或类间的动作协调与信息传递(消息推送)
事件的功能:通知+可选的事件参数(即详细信息)
事件的现实模型
事件是从现实中抽象出来的一种编程思想,以一个现实生活的简单例子举例
闹钟响了我起床
在该事件当中,闹钟(事件发布者)、响了(事件发生)、我(事件的响应者)、起床(事件发生后的处理动作)、响铃时间(事件的详细信息)。
C#事件抽象模型
从刚刚现实例子中,可以总结出C#的事件模型
事件拥有者:它是类或者类成员,负责定义和触发事件。它可以拥有一些事件。
事件本身:基于委托类型,在发布者内部声明的成员,外界通过 += 和 -= 订阅。
比如刚刚的闹钟的例子,“响了”是一个动词,动词是依赖于发布者主体的,没有闹钟就没有响这个事件。
事件的响应者:它是类或者类成员,提供事件处理方法,并订阅拥有者的事件。
事件处理程序:订阅者中实际处理事件的方法,从订阅者提供的事件处理方法中(可能有多个方法)使用具体的方法。
例如闹钟响了,我可以先关闹钟,再起床,或者先起床,再关闹钟(继续响会)。
事件参数/事件详细信息(可能有):可选的,用于传递事件相关的数据,如响铃时间。
案例一
Timer是C#中的一个标准事件函数,它在达到一定间隔时可以触发一个事件
using System.Timers; //引用C#库Timer
Timer timer_1=new Timer(); //新建一个对象
timer_1.Interval = 1000; //创建一个事件的拥有者

这里代表1000ms后触发一个Elapsed事件,现在已经满足了事件模型中的两个 事件的拥有者、Elasped事件。
我在事件的使用方法中提到,事件用于对象或类间的信息传递。因此,接下来创建一个类并声明,这个类是我们的事件的响应者。
/*案例代码*/
using System;
using System.Timers;
namespace program
{
class My_main
{
static void Main()
{
Timer timer_1=new Timer();
timer_1.Interval = 1000; //这是事件拥有者
Boy boy_1=new Boy(); //这是事件响应者
}
}
class Boy
{
}
}
timer_1.Elapsed += Boy.Action; //事件响应者订阅事件,并创建一个事件处理器
这里的Action为事件处理器的方法,可以先强行写下来,通过VS自动为我们生成一个事件处理器。


/*案例代码*/
using System;
using System.Timers;
namespace program
{
class My_main
{
static void Main()
{
Timer timer_1=new Timer(); //创建一个事件拥有者
timer_1.Interval = 1000; //设置1000ms触发Elapsed事件
Boy boy_1=new Boy(); //事件响应者
/*事件响应者Boy通过+=订阅事件拥有者timer_1的Elapsed 事件,并创建一个Action事件处理器*/
timer_1.Elapsed += Boy.Action;
timer_1.Start();
Console.ReadLine();
}
}
class Boy
{
internal static void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("事件被响应");
}
}
}
在这个案例中
timer_1为事件拥有者,通过timer_1.Interval设置1000ms触发一个Elapsed事件
boy_1为事件响应者,Elapsed为事件本身
Action为事件处理器
事件详细信息为隐式的1000ms计时完成
案例二

在这个关系中,事件响应者与事件拥有者是两个类,通过+=订阅事件,其实与案例一是一样的关系,只不过这里更复杂一点,将事件拥有者作为参数传进了事件响应者,并在事件响应者内部订阅shi
/*案例代码*/
using System;
using System.Windows.Forms;
namespace program
{
class My_main
{
static void Main()
{
/*初始化类,该类为事件拥有者*/
Form form_1 = new Form();
/*初始化类,该类为事件的响应者,并将事件拥有者作为参数传进事件响应者当中*/
Controller Controller_1=new Controller(form_1);
form_1.ShowDialog(); //显示窗口
}
}
class Controller
{
private Form form_2; //新建一个Form
/*Controller的构造方法,它在Controller被初始化时自动执行
其中form用来接收初始化时的参数form_1*/
public Controller(Form form)
{
if (form != null)
{
/*this用来区分类成员和构造方法参数,this指向的是类成员form_2*/
//这里类似于委托,将传进来的事件拥有者赋给form_2 ,此时form_2就是事件拥有者
this.form_2 = form;
/*事件订阅者this(代指Controller)通过+=订阅事件拥有者的Click事件,并添加事件 处理器Action*/
this.form_2.Click += this.Action;
}
}
private void Action(object sender, EventArgs e) //事件处理器
{
this.form_2.Text = DateTime.Now.ToString(); //在点击窗口时将时间显示在窗体标题上
}
}
}
运行结果:

案例三
这个例子中其事件模型的”5“,分别为

在这个案例中,事件拥有者也是事件响应者。
/*案例代码*/
using System;
using System.Windows.Forms;
namespace program
{
class My_main
{
static void Main()
{
/*创建一个对象,该对象既是事件拥有者 也是事件响应者*/
Myform Myform_1=new Myform();
/*事件响应者Myform_1通过+=订阅事件拥有者Myform_1的Click事件,并添加事件处理器acition*/
Myform_1.Click += Myform_1.acition;
Myform_1.ShowDialog();
}
}
class Myform : Form //类的继承,让自建类拥有其他类的成员,后面详细讲,现在先用
{
internal void acition(object sender, EventArgs e)
{
this.Text = DateTime.Now.ToString(); //在点击窗口时将时间显示在窗体标题
}
}
}
运行结果:

案例四

事件拥有者是事件响应者的某个字段成员。事件响应者在自己内部class订阅事件。
/*案例代码*/
using System;
using System.Windows.Forms;
namespace program
{
class My_main
{
static void Main()
{
Myform My_form_1=new Myform(); //初始化一个类,该类为事件响应者
My_form_1.ShowDialog();
}
}
class Myform : Form //类的继承,让自建类拥有其他类的成员,后面详细讲,现在先用
{
private TextBox textbox_1; //声明TextBox类型的成员
private Button button_1 ; //这个是事件拥有者,它是事件响应者的一个成员
public Myform() //构造方法,在Myform被初始化时自动执行
{
this.textbox_1 = new TextBox(); //初始化
this.button_1 = new Button(); //初始化
this.Controls.Add(this.textbox_1); //这个是将文本框添加进窗体中
this.Controls.Add(this.button_1);
/*事件响应者this(这里代值Myform)通过+=订阅事件拥有者button_1的Click事件*/
/*并创建一个action事件处理器*/
this.button_1.Click+=this.action;
this.button_1.Text = "Say hello"; //按键文本设置为say hello
this.button_1.Top = 50; //按键距离窗口上边沿50
}
private void action(object sender, EventArgs e) //事件处理器
{
this.textbox_1.Text = "Hello!";
}
}
}
运行结果:

自定义事件
在刚刚的一些案例中,我们使用的事件都是C#官方早已配好的,在日常开发中,大多数使用的都是已经配好的事件,但仍有一部分的事件需要自己编写,接下来就学习C#中自定义事件,并利用自定义事件去处理一些程序逻辑。
首先,在编写自定义委托类型之前,再重复一边一开始讲的事件本身基于委托类型,因此编写自定义事件时,需先声明一个委托,使该事件与委托挂钩。
其次再编写程序之前看几个重要语法
1.事件详细信息声明语法
该语法用于声明事件的详细信息
class 名称:EventArgs
{
}
为了统一代码规范,事件详细信息的clss名称后面统一加上EventArgs表示这是一个事件详细信息
2.事件声明语法
public event 事件基于的委托类型 事件名称
{
add{委托实例+=value}
remove{委托实例-=value}
}
为了统一代码规范,事件的名称后面统一加上EventHandler表示这是一个事件声明
例子
public event OrderHandler order
{
add { handler += value; }
remove { handler -= value; }
}这里的+=、-=其实就是多播委托,我们抛弃掉事件这一概念,剖析这段代码,我们知道在外部时事件订阅格式是这样的
事件拥有者.事件+=事件响应者.事件处理程序
而在内部,event声明事件内部处理逻辑是add(委托实例+=value)。
因此整个程序逻辑就是事件拥有者声明事件时执行内部逻辑,内部逻辑通过+=将传入的事件处理器添加进来,这就是多播委托,后续在委托触发时,会将多播委托中的方法依次执行依次。因此这也是为什么VS自动生成的事件处理程序会自带参数值,因为多播委托添加进来的方法,与一开始委托声明时的方法在结构上一致(返回值类型、参数类型等)!
在待会的案例中,我会用图例来再次讲解这一关系。
在接下来的案例中,假设我去到一个餐馆,点菜后服务员根据我点的菜进行上菜。
这里,我是事件的拥有者
点菜是事件
服务员是事件响应者
上菜是事件处理器。
该案例中委托关系如下,事件位于事件拥有者的类中

using System; namespace this_main { /*声明一个事件详细信息类,包括菜名*/ public class me_EventArgs: EventArgs { public string DishName; } /*声明一个事件的基础委托,参数为事件详细信息*/ public delegate void OrderEventHandler(me_EventArgs me_EventArgs); /*声明一个事件拥有者*/ public class me { /*实例化委托*/ private OrderEventHandler OrderEventHandler_me; /*声明事件*/ public event OrderEventHandler Order { add { OrderEventHandler_me += value; } remove { OrderEventHandler_me -= value; } } /*还缺一个触发事件的方法,这里编写一个触发事件的逻辑*/ public void Event_input() { Console.WriteLine("我要点"); me_EventArgs me_EventArgs_1=new me_EventArgs(); //实例化事件信息参数 me_EventArgs_1.DishName=Console.ReadLine(); //将输入填写进事件信息参数 OrderEventHandler_me.Invoke(me_EventArgs_1); //触发实例化的委托OrderEventHandler_me,并将事件处理参数传进去 } } /*声明一个事件响应者*/ public class waiter { /*事件处理程序*/ internal void action(me_EventArgs me_EventArgs) { Console.WriteLine("上菜" + me_EventArgs.DishName); } } public class this_main{ static void Main() { me me_1=new me(); waiter waiter_1=new waiter(); me_1.Order += waiter_1.action; me_1.Event_input(); } } }

所以事件如果去除事件详细参数声明、事件声明步骤,事件本质上就是通过多播委托的方式逐次执行事件处理程序。
自定义事件的简单声明
在刚刚的例子中,事件声明需要
public event OrderHandler order
{
add { handler += value; }
remove { handler -= value; }
}
并将事件不能被直接调用,而是通过其基于的实例化委托进行调用
OrderEventHandler_me.Invoke(me_EventArgs_1); //触发委托
而自定义事件的简单声明可以不需要进行add和remove操作,事件可以被直接调用,不用再通过其基于的实例化委托调用。
public event OrderHandler order; //声明事件
order.Invoke(me_EventArgs_1); //触发事件
怎么样是不是简单很多,不用再理解其复杂的调用关系,跟委托类似声明后直接调用即可,
详细案例如下。
using System;
namespace this_main
{
/*声明一个事件详细信息类,包括菜名*/
public class me_EventArgs : EventArgs
{
public string DishName;
}
/*声明一个事件的基础委托,参数为事件详细信息*/
public delegate void OrderEventHandler(me_EventArgs me_EventArgs);
/*声明一个事件拥有者*/
public class me
{
/*实例化委托*/
private OrderEventHandler OrderEventHandler_me;
/*声明事件*/
public event OrderEventHandler Order;
/*还缺一个触发事件的方法,这里编写一个触发事件的逻辑*/
public void Event_input()
{
Console.WriteLine("我要点");
me_EventArgs me_EventArgs_1 = new me_EventArgs(); //实例化事件信息参数
me_EventArgs_1.DishName = Console.ReadLine(); //将输入填写进事件信息参数
Order.Invoke(me_EventArgs_1); //触发事件Order,并将事件处理参数传进去
}
}
/*声明一个事件响应者*/
public class waiter
{
/*事件处理程序*/
internal void action(me_EventArgs me_EventArgs)
{
Console.WriteLine("上菜" + me_EventArgs.DishName);
}
}
public class this_main
{
static void Main()
{
me me_1 = new me();
waiter waiter_1 = new waiter();
me_1.Order += waiter_1.action;
me_1.Event_input();
}
}
}
运行结果:

事件的必要性
我在自定义事件中提到,事件本质上就是通过多播委托的方式逐次执行事件处理程序。
那么既然已经存在了委托这一方式,为什么还要创造出委托的概念呢?
1、防止误用、滥用委托。
在大型项目开发中,通常是多人对同一代码文件进行编程,因为委托是public类型访问修饰的,外界可以公开访问,在实际开发中,自己写的委托容易被他人随意调用,且不按照自己写的委托规范进行调用。事件基于委托,且生成的事件处理器也基于委托的构造,有了事件后,他人想要调用你这个事件,就必须按照你写的规范进行调用,这很好地防止了被误用、用错的情况。
2、封装、隐藏委托
当你希望自己的委托,不被他人随意查看源码时,但由于委托是public公开的。因此事件可以很好地封装程序代码,只向外界暴露访问器的功能(+=,-+)。
事件编程规范
为了统一编程规范,保证代码可读性,一般遵循以下规范
1、事件详细信息的clss名称后面统一加上EventArgs表示这是一个事件详细信息
public class MeEventArgs:EventArgs
{
}
2、事件声明后面统一加上EventHandler表示这是一个事件声明
public event OrderEventHandler xxxEventHandler;
3、若您声明事件时,不想自己自定义委托类型,可以采用官方的EventHandler进行事件声明
public event EventHandler xxxEventHandler;
EventHandler为C#库自带的委托类型,它的构造如下
public delegate void EventHandler(object sender, EventArgs e);
object sender :object为所有类型的基类,它可以传任何类型的参数
EventArgs e :为事件详细信息,传参进来时需保证类型一致。
4.触发事件的方法一般命名为On+事件名
protected void OnxxxEventHandler();
protected修饰后只对类的内部可见,防止在事件拥有者之外的人触发事件。
C#类
类的成员
在这之前,还没有讲类的成员,所以有些小伙伴云里雾里的,不知道自己的变量、方法、委托声明等放在哪,所以这里再详细地讲一下。
类一般有如下几个成员(这里只挑了常见的)
字段(Fields):存储数据的变量。
属性(Properties):封装字段,提供get和set访问器。
方法(Methods):定义类的行为。
构造函数(Constructors):用于初始化类的实例。。
事件(Events):实现基于委托的通知机制。
索引器(Indexers):使对象可以像数组一样被索引。
运算符(Operators):重载运算符。
嵌套类型(Nested Types):在类中定义的其他类型(类、接口、结构等)。
常量(Constants):不可变的字段。
1. 字段(Field)
段是类中直接声明的变量,用于存储数据。可以是实例字段(属于对象)或静态字段(属于类)
public class Person
{
public string name; // 实例字段
public static int count; // 静态字段,记录实例数量
}
2.属性(Property)
属性就像是有规矩的变量:
-
用起来像变量(赋值、读取)
-
实际上可以加各种规矩(验证、计算、通知)
public class 钱包
{
private int money; // 真正的钱是私有的,别人不能直接碰
public int money// 这是一个属性,相当于一个"窗口"
{
get { return money; } // 别人想看钱时,显示多少
set // 别人想放钱时,检查一下
{
if (value < 0)
Console.WriteLine("不能放负数钱!");
else if (value > 10000)
Console.WriteLine("一次不能放超过1万!");
else
money = value;
}
}
}
3. 方法(Method)
public class Calculator
{
public int Add(int a, int b) // 实例方法
{
return a + b;
}
public static void ShowInfo() // 静态方法
{
Console.WriteLine("Calculator class");
}
}
4. 构造函数(Constructor)
public class Person
{
public string Name { get; set; }
// 实例构造函数,每次被实例化时调用
public Person(string name)
{
Name = name;
}
// 静态构造函数(初始化静态成员,只执行一次)
//在应用程序域(AppDomain)生命周期内,最多执行一次(即使从未创建实例,只要访问了静态成员或创建了第一个实例,就会触发)。
static Person()
{
Console.WriteLine("静态构造函数执行");
}
}
5. 事件(Event)
事件基于委托,提供一种发布-订阅机制,允许对象通知其他对象发生了某些事情。
public class Button
{
public event EventHandler Click; // 声明事件
public void OnClick()
{
Click?.Invoke(this, EventArgs.Empty); // 触发事件
}
}
6. 索引器(Indexer)
索引器允许对象像数组一样通过索引访问,通常用于封装集合。
public class StringCollection
{
private string[] data = new string[10];
public string this[int index] // 索引器定义
{
get { return data[index]; }
set { data[index] = value; }
}
}
7. 运算符重载(Operator)
通过 operator 关键字可以为类定义自定义的运算符行为(如 +、- 等)。
public class Vector
{
public int X { get; set; }
public int Y { get; set; }public static Vector operator +(Vector v1, Vector v2) // 重载 +
{
return new Vector { X = v1.X + v2.X, Y = v1.Y + v2.Y };
}
}
8.嵌套类型(Nested Type)
在类内部可以定义其他类型(类、结构、接口、枚举等),用于实现更紧密的封装。
public class OuterClass
{
private class InnerClass // 嵌套类
{
public void Display()
{
Console.WriteLine("内部类");
}
}
}
9. 常量(Constant)
常量是使用 const 关键字声明的字段,其值在编译时确定且不可更改。
public class MathUtil
{
public const double PI = 3.14159;
}
类的概念
什么是类?
- 类是一种数据结构,它是一个物品的抽象特征合集
- 类是一种特殊的数据类型,它可以像int、double被声明及实例化
- 代表现实世界中的"种类"
用一个代码来演示一下
using System;
namespace Person
{
class Student //类是一种数据结构,这里指的是学生特征,学号、姓名、行为等等
{
public int Id { get; set; }
public string Name { get; set; }
public void Report()
{
Console.WriteLine("我的学号是:{0} 我的姓名是:{1}",Id,Name);
}
}
class This_main
{
static void Main()
{
Student student = new Student(); //类是一种特殊的数据类型
student.Name = "李华";
student.Id = 20;
student.Report();
}
}
}
类的构造和析构
类在被实例化时我们可以让其运行一小部分程序逻辑,这个就叫构造方法
class Person
{
public Person(参数)
{}
}
using System;
using System.Xml.Linq;
namespace This_Main
{
public class Person
{
public string Name { get; set; }
static int id;
// 实例构造函数,每次被实例化时调用
public Person(int a)
{
Console.WriteLine("构造方法被执行");
Console.WriteLine("传进来的参数为{0}", a);
}
// 静态构造函数(初始化静态成员,只执行一次)
//在应用程序域(AppDomain)生命周期内,最多执行一次(即使从未创建实例,只要访问了静态成员或创建了第一个实例,就会触发)。
static Person()
{
id = 20;Console.WriteLine(id);
}
}
class This_main
{
static void Main()
{
Person person = new Person(1);
}
}
}
运行结果:

类在被实例化时,C#会为其创建一个内存,若类在被调用后再无其他地方调用它时,这段内存称为垃圾内存,基于C#垃圾内存回收机制,这段内存会被回收,回收时也会触发一段程序(如果有的话),该过程称为析构方法
class Person
{
~Person
{
// ❌ 不能带参数 // ❌ 不能有访问修饰符(public/private等) // ❌ 不能显式调用 // ✅ 只能有一个 // ✅ 自动被GC调用}
}
using System;
using System.Xml.Linq;
namespace This_Main
{
public class Person
{
public string Name { get; set; }
static int id;
// 实例构造函数,每次被实例化时调用
public Person(int a)
{
Console.WriteLine("构造方法被执行");
Console.WriteLine("传进来的参数为{0}", a);
}
// 静态构造函数(初始化静态成员,只执行一次)
//在应用程序域(AppDomain)生命周期内,最多执行一次(即使从未创建实例,只要访问了静态成员或创建了第一个实例,就会触发)。
~Person() { Console.WriteLine("被回收"); }
}
class This_main
{
static void Main()
{
Person person = new Person(1);
}
}
}
运行结果:

类的继承
类的访问
在C#封装中,简单地描述了一下几个修饰符的用法,现在再把它丰富一下
- new : 在实例化时用到,表示新建一个实例化
- Public : 修饰后该类可以被外部任何类访问
- Protected:该类内部和继承类中可以访问
- interna: 只有同一个程序集的对象可以访问
- private: 只有同一个类中的方法可以访问类的私有成员
- Protected innternal:Protected和innternal并集,符合任意一条都可以访问
- static : 静态类,可以直接访问
- sealed: 封闭类,使该类不能成为父类被继承。
- abstract:抽象类或成员。
- interface:接口,纯抽象类
| 修饰符 | 可访问性说明 | 适用范围 |
|---|---|---|
| public | 访问不受限制。任何代码都可以访问。 | 顶级类、嵌套类 |
| internal | 只能在同一程序集内访问。 | 顶级类、嵌套类 |
| protected | 只能在当前类或派生类中访问(派生类可跨程序集)。 | 仅限嵌套类 |
| protected internal | 同一程序集中的任何代码,或者其他程序集中的派生类都可以访问。注意:这是 protected 和 internal 的并集(即满足任一条件即可)。 |
仅限嵌套类(不能用于顶级类) |
| private | 只能在当前类内部访问。。 | 仅限嵌套类 |
| private protected | 只能在同一程序集中的派生类中访问(C# 7.2 引入)。 | 仅限嵌套类 |
注意:
- 顶级类(即直接定义在命名空间下的类)只能使用
public或internal(默认为internal)。其他修饰符(如protected、private)只能用于嵌套类。 -
嵌套类的可访问域不能超过包含类的可访问域。如果包含类(外部类)是
internal,那么嵌套类不能声明为public,否则会导致可访问性扩大,编译器会报错。 -
外部类和嵌套类之间可以互相访问对方的任何私有成员(包括实例字段、静态字段、私有方法等)。这是因为嵌套类被视为外部类的成员,两者处于同一个声明区域内,因此享有特殊的“友元”关系。
-
无论是顶级类还是嵌套类的成员访问级别受类自身限制,类内部的成员(如方法、字段)可以声明为
public、private等,但这些成员的实际可访问性受类本身的可访问性限制。
类继承实现方法
在面向对象编程中,派生和继承其实是同一个概念的两个侧面,通常可以互换使用,但严格来说它们有细微的区别:
继承:指一个类(子类)自动获得另一个类(父类)的成员(字段、属性、方法等)的机制。它强调的是类与类之间的关系——子类“是”一种父类。
派生:指通过扩展已有类来创建新类的过程。它强调的是从基类生成新类的动作或结果——新类是从基类“派生”出来的。
比如汽车是交通工具,这是继承。汽车继承了交通工具的属性。
交通工具有汽车,这是派生,汽车是交通工具的子类,交通工具是汽车的父类。
在日常交流中,这两个词经常混用,无需过度纠结。但在技术文档或面试中,了解这种细微差别有助于更精准地表达。
继承的声明比较简单,继承后子类将会继承父类的全部成员
class 子类class名 :父类class名
{
}
using system;
namespace FatherAndSon
{
public class Father
{
}
public class Son:Father
{
}
}
但是声明时需要注意以下规范
1、任何类只能有一个基类,一个类在继承父类后,就不能在继承了,但是可以被继承。
2、子类的访问级别不能超越基类,可以平级。
例如父类是interna访问修饰符,子类就不能是Pulbic
继承规则:派生类可访问性 ≤ 基类可访问性
3、子类相对于父类,只能增加不能缩减
4、不能继承父类的构造器,但子类构造器与父类构造器最好一致,如果有参数的话。
类的重写
在将类的成员之前,先说一下以下一些概念
- 类成员的“横向扩展,子类增加父类没有的成员。
- 类成员的"纵向扩展",不增加成员,而是基于原来的成员作修改。
这种类成员的纵向扩展,是继承的一种特殊用法,现在用一个案例来演示一下虚方法和普通方法的实际用法。
using System;
namespace Program
{
class Animal
{
//虚方法,允许重写
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
public void Runn()
{
Console.WriteLine("动物在跑");
}
}
class Dog : Animal
{
//重写基类Animal的MakeSound虚方法
public override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
class This_main
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.MakeSound(); //实际只会调用子类Dog的MakeSound方法,不会调用父类的MakeSound
}
}
}
运行结果

注意:
如果没有用virtual、override修饰,那么只会调用基类的Run,可以利用以上例子自己试一试。
多态
多态允许我们将子类类型的对象赋值给父类类型的引用,然后通过这个父类引用调用方法时,实际执行的是子类中重写后的方法实现。
using System;
// 基类
public class Animal
{
// 虚方法 - 允许子类重写
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
// 普通方法 - 不允许重写
public void Eat()
{
Console.WriteLine("动物在吃东西");
}
}
// 派生类 - 狗
public class Dog : Animal
{
// 重写基类的虚方法
public override void MakeSound()
{
Console.WriteLine("狗叫:汪汪汪!");
}
}
// 派生类 - 猫
public class Cat : Animal
{
// 重写基类的虚方法
public override void MakeSound()
{
Console.WriteLine("猫叫:喵喵喵~");
}
}
class Program
{
static void Main()
{
Console.WriteLine("=== 演示 virtual 和 override ===");
// 创建不同类型的动物
Animal animal1 = new Animal();
Animal animal2 = new Dog(); // 多态:父类引用指向子类对象
Animal animal3 = new Cat();
// 调用方法 - 展示多态行为
Console.WriteLine("动物1:");
animal1.MakeSound(); // 输出:动物发出声音
Console.WriteLine("\n动物2 (狗):");
animal2.MakeSound(); // 输出:狗叫:汪汪汪!
Console.WriteLine("\n动物3 (猫):");
animal3.MakeSound(); // 输出:猫叫:喵喵喵~
Console.WriteLine("\n=== 普通方法没有多态 ===");
animal1.Eat(); // 输出:动物在吃东西
animal2.Eat(); // 输出:动物在吃东西(没有重写,调用基类方法)
animal3.Eat(); // 输出:动物在吃东西
}
}
可能一些小伙伴就会有这样的疑惑,既然有Dog这个类,那么我们在实例化时直接
Dog dog_1=new Dog();
不是会更加直接吗,的确,在这个案例中更加地直接和易读
假设,我现在是动物园工作人员,我需要一个方法,使所有动物一起叫起来,在程序中应该
// 需要为每个动物都写一行代码
void Animal_AllMakeSound(Dog dog1, Cat cat1, Bird bird1, Duck duck1)
{
dog1.MakeSound(); // 狗叫
cat1.MakeSound(); // 猫叫
bird1.MakeSound(); // 鸟叫
duck1.MakeSound(); // 鸭叫
// 来一个新动物,就要加一行代码!
}
看到没,每多一种类型的动物,我就需要给它输入该动物类型的参数,并在代码中调用MakeSound,但如果有了多态,程序将会变得这样。
using System;
// 基类
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
// 派生类
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪汪!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("喵喵喵~");
}
}
public class Bird : Animal
{
public override void MakeSound()
{
Console.WriteLine("叽叽喳喳!");
}
}
class Program
{
// ⭐ 多态方法 - 一个方法处理所有动物
static void AnimalAllMakeSound(Animal[] animals)
{
foreach (Animal animal in animals)
{
animal.MakeSound(); // 自动调用对应动物的叫声
}
}
static void Main()
{
// 创建动物数组
Animal[] zoo = { new Dog(), new Cat(), new Bird() };
Console.WriteLine("=== 动物大合唱 ===");
// 一行代码,所有动物一起叫
AnimalAllMakeSound(zoo);
}
}
这样便实现了方法参数和具体参数的解耦,使我们在编写程序时,不用具体传入的子类,利用其多态性,降低代码复杂度。
面向对象编程
这一章节的内容主要讲的是C#中针对面向对象编程的一些语法,分为接口、抽象类、SOLID、单元测试、反射五个小章节。
在讲abstract的用法之前,先来看看以下案例代码,基于重写机制,如何将未实现的方法放到子类实现。
using System;
namespace Program
{
class Vehicle //交通工具基类
{
public void Stop()
{
Console.WriteLine("Stop");
}
public virtual void Run() { }
}
class Car : Vehicle
{
public override void Run()
{
Console.WriteLine("Car is Running");
}
}
class This_Main
{
static void Main(string[] args)
{
Vehicle C = new Car();
C.Run();
}
}
}
运行结果

案例中,基于重写机制,使用virtual、override虚方法重写Run这个方法,针对这种频繁的,需要将父类未实现的方法、属性交给子类实现的步骤,C#中有abstract修饰符。
1、abstract用于声明抽象类和抽象成员
2、抽象类不能被实例化,可以包含抽象和非抽象成员
3、抽象成员没有实现,必须在派生类中重写
using System;
namespace Program
{
abstract class Vehicle //交通工具抽象基类
{
public void Stop()
{
Console.WriteLine("Stop");
}
public abstract void Run(); //Run抽象方法
}
class Car : Vehicle
{
public override void Run() //子类Run方法实现
{
Console.WriteLine("Car is Running");
}
}
class This_Main
{
static void Main(string[] args)
{
Vehicle C = new Car();
C.Run();
}
}
}
抽象类
- 抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了"具体逻辑”)
- 抽象类成员不能是private,因为private限定只能在当前类访问,子类无法访问,也就无法对抽象成员进行具体化实现。
- 抽象类不能被实例化,只用来声明变量,因为抽象类中含有未实现的成员,编译器无法得知这些抽象成员如何实现。
- 抽象类用于封装确定,开放不确定的,推迟到合适的子类中去实现。
接口
- 接口是完全未实现逻辑的类(纯虚类:只有方法成员且成员全部public)
- 接口不能被实例化,只能用来声明变量、引用具体类的实例。
- 接口用interface修饰
基于抽象类的案例代码,Vehicle 交通工具基类中包含了未实现的Run方法和已经实现的Stop方法。既然如此,就存在一种类,它里面全是未实现的方法,这种类就被定义为接口。
下面请看案例代码,领悟案例代码中的逐层继承关系,理解由接口->抽象类->具体类的逐层递进关系。
using System;
namespace Program
{
interface IVehicle //接口,纯虚类
{
void Run();
void stop();
}
abstract class Vehicle : IVehicle //抽象类,继承接口IVehicle,实现stop方法,含有抽象Run方法
{
public void stop()
{
Console.WriteLine("Stop");
}
abstract public void Run();
}
class Car : Vehicle //具体类,其基于的接口IVehicle所有方法均实现
{
public override void Run()
{
Console.WriteLine("Car is running");
}
}
class This_Mian
{
static void Main()
{
Vehicle c = new Car();
c.Run();
c.stop();
}
}
}
接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计五大原则(SOLID)中的“I”。它的核心思想是:客户端不应该被迫依赖于它们不使用的接口。
违反接口隔离原则的例子
假设我们设计一个设备接口,包含了打印机、扫描仪、传真机等所有可能的功能:
public interface IDevice
{
void Print();
void Scan();
void Fax();
void Copy();
}
如果有一个简单的打印机类只支持打印,它也必须实现 Scan、Fax 等方法,这显然不合理:
public class SimplePrinter : IDevice
{
public void Print() => Console.WriteLine("打印");
public void Scan() => throw new NotSupportedException("不支持扫描");
public void Fax() => throw new NotSupportedException("不支持传真");
public void Copy() => throw new NotSupportedException("不支持复印");
}
这样的设计迫使客户端依赖它们不需要的方法,增加了不必要的复杂度,也容易引发运行时异常。
如何遵守接口隔离原则
应该将庞大的接口拆分为多个小接口,每个接口代表一组紧密相关的功能:
// 定义两个基础接口
public interface IPrintable
{
void Print();
}
public interface IScannable
{
void Scan();
}
// IAllInOne 同时继承 IPrintable 和 IScannable
public interface IAllInOne : IPrintable, IScannable
{
// 可以添加额外的成员,但不是必需的
void Fax(); // 比如再添加一个传真功能
}
// 实现复合接口的类
public class MultiFunctionPrinter : IAllInOne
{
public void Print()
{
Console.WriteLine("打印文档...");
}
public void Scan()
{
Console.WriteLine("扫描文档...");
}
public void Fax()
{
Console.WriteLine("发送传真...");
}
}
// 使用示例
class Program
{
static void Main()
{
IAllInOne device = new MultiFunctionPrinter();
device.Print(); // 输出:打印文档...
device.Scan(); // 输出:扫描文档...
device.Fax(); // 输出:发送传真...
}
}
依赖
依赖是基于现实世界的抽象概念,例如汽车运动依赖于引擎,用户依赖于手机打电话,这是一种紧耦合,以用户依赖手机打电话这一个案例,假设用户有两个手机Nokia、Samsung’,用C#编写这一过程。
using System;
namespace Program
{
class Nokia
{
public void Call()
{
Console.WriteLine("Nokia Call");
}
}
class Samsung
{
public void Call()
{
Console.WriteLine("Samsung Call");
}
}
class user
{
public void NokiaCall(Nokia Nokia)
{
Nokia.Call();
}
public void SamsungCall(Samsung Samsung)
{
Samsung.Call();
}
}
class This_Main
{
static void Main()
{
user user_1=new user();
Nokia nokia=new Nokia();
Samsung Samsung = new Samsung();
user_1.NokiaCall(nokia);
user_1.SamsungCall(Samsung);
}
}
}
运行结果:

在这段案例代码中,用户分别使用Nokia和Samsung打电话,并且用户中的代码强依赖于Nokia、Samsung这两个class,假设编写这段Nokia、Samsung这两个类中的Call方法编写错误,那么整段代码都无按照预期运行了。这种一个类强依赖于另一个类运行的关系,在软件工程中叫耦合度高。
如果在一个公司团队中,一个人负责编写手机class类,另一个人编写用户class,那么这个编写用户class的进度就会受到编写手机class的影响。
因此,引入一个接口,该接口默认你的Call是正常的,不依赖于具体的手机类型,任何手机类型都按照接口这个协议去编写程序,用户class只需要调用接口,以此实现解耦。
using System;
namespace Program
{
interface Phone //定义一个手机接口
{
void Call(); //打电话虚方法
}
class Nokia : Phone //基于手机接口的Nokia
{
public void Call()
{
Console.WriteLine("Nokia Call"); //Nokia的Call实现
}
}
class Samsung : Phone //基于手机接口的Samsung
{
public void Call()
{
Console.WriteLine("Samsung Call"); //Samsung的call实现
}
}
class user //定义用户类
{
Phone _Phone; //声明接口类
public user(Phone phone) //构造器,传接口参数
{
this._Phone = phone;
}
public void UserCall()
{
_Phone.Call(); //使用接口.call的虚方法
}
}
class This_Main {
static void Main()
{
user user_1 = new user(new Nokia()); //也可以new Samsung
user_1.UserCall();
}
}
}
在这上面的案例中,我们利用在接口中定义了Call的虚方法,Call由实际类去实现,并且在用户中,声明接口变量并由构造器,将外部实际类传入构造器,通过UserCall调用接口的Call,接口的Call在调用实际类的Call,实现了用户类与实际类的解绑。
这样一来,用户类在编写具体方法时,不依赖与于实际类,只与接口绑定,只在实际Main方法中使用时传入实际类。
无论是抽象类、接口还是C中的指针、函数指针,其最终目的,都是实现代码与代码的解耦合,我在编写函数时,虽然需要用到其他代码的变量、函数来实现我的逻辑,但倘若你的函数没写好或你的函数有问题,我就无法实现我的逻辑,因此针对这一情况,就诞生了指针的一些特殊用法和C#中的抽象类与接口。

用一张图来总结一下最近所学
- 接口:里面全是完全未实现逻辑的方法
- 抽象类:里面逻辑未完全实现
- 虚类:完全实现逻辑,但有一部分方法可以重写
- 实际类:所有方法均已完全实现,且不可再重写
泛型
什么是泛型?
泛型 可以理解为类型的参数化。就像方法可以有参数一样,泛型允许你把"类型"本身作为参数来传递。
泛型类
生活类比:盒子与内容物
非泛型方式(老式做法):
// 你需要为每种物品准备专门的盒子
class 苹果盒子 { public 苹果 内容; }
class 香蕉盒子 { public 香蕉 内容; }
class 书本盒子 { public 书本 内容; }
// 使用
苹果盒子 a = new 苹果盒子();
a.内容 = new 苹果(); // 只能放苹果
// 一个通用盒子,想装什么就装什么
class 盒子<T> { public T 内容; }
// 使用
盒子<苹果> a = new 盒子<苹果>();
a.内容 = new 苹果(); // 装苹果
盒子<香蕉> b = new 盒子<香蕉>();
b.内容 = new 香蕉(); // 装香蕉
盒子<书本> c = new 盒子<书本>();
c.内容 = new 书本(); // 装书本
泛型类的声明
class ClassName<类型参数>
{
public 类型参数 内容;
}
泛型类的实例化
ClassName<类型参数>Name = new ClassName<类型参数>
泛型接口
using System;
namespace Program
{
interface IUnique<T_id> //声明泛型接口
{
T_id id{ get; set; }
}
class student<T_id> : IUnique<T_id> //继承泛型接口
{
public string Name { get; set; }
public T_id id { get; set; }
}
class This_main
{
static void Main(string[] args)
{
student<int> student = new student<int>(); //实例化类
student.Name = "Lihua";
student.id = 102;
Console.WriteLine(student.Name+"\n" +student.id);
}
}
}
另一种写法,直接在student声明时特化
using System;
namespace Program
{
interface IUnique<T_id> //声明泛型接口
{
T_id id{ get; set; }
}
class student : IUnique<int> //继承泛型接口,此处特化
{
public string Name { get; set; }
public int id { get; set; }
}
class This_main
{
static void Main(string[] args)
{
student student = new student(); //正常声明
student.Name = "Lihua";
student.id = 102;
Console.WriteLine(student.Name+"\n" +student.id);
}
}
}
泛型方法
泛型方法声明
public 返回值类型参数名 方法名(类型参数名)<类型参数名,返回值类型参数名>
{
return (返回值类型参数名)Convert.ChangeType(参数, typeof(返回值类型参数名));}
泛型方法的使用
方法名<具体类型,具体类型>(参数)
public class CircleCalculator
{
// 泛型方法:计算圆的面积
// TInput: 输入参数的类型(半径)
// TOutput: 返回值的类型(面积)
public TOutput CalculateArea<TInput, TOutput>(TInput radius)
{
// 1. 先将输入转换为double进行计算
double radiusValue = Convert.ToDouble(radius);
// 2. 计算面积(π * r²)
double area = Math.PI * radiusValue * radiusValue;
// 3. 将结果转换为输出类型
return (TOutput)Convert.ChangeType(area, typeof(TOutput));
}
}
// 使用示例
class Program
{
static void Main()
{
var calculator = new CircleCalculator();
// 场景1:输入int,返回double(最常用)
int radius1 = 5;
double area1 = calculator.CalculateArea<int, double>(radius1);
Console.WriteLine($"半径: {radius1} (int) → 面积: {area1:F2} (double)");
// 输出:半径: 5 (int) → 面积: 78.54 (double)
// 场景2:输入double,返回string
double radius2 = 3.5;
string area2 = calculator.CalculateArea<double, string>(radius2);
Console.WriteLine($"半径: {radius2} (double) → 面积: {area2} (string)");
// 输出:半径: 3.5 (double) → 面积: 38.48451000647496 (string)
// 场景3:输入string,返回decimal
string radius3 = "2.5";
decimal area3 = calculator.CalculateArea<string, decimal>(radius3);
Console.WriteLine($"半径: {radius3} (string) → 面积: {area3:F2} (decimal)");
// 输出:半径: 2.5 (string) → 面积: 19.63 (decimal)
// 场景4:输入int,返回float
int radius4 = 10;
float area4 = calculator.CalculateArea<int, float>(radius4);
Console.WriteLine($"半径: {radius4} (int) → 面积: {area4:F2} (float)");
// 输出:半径: 10 (int) → 面积: 314.16 (float)
}
}
泛型委托
这个在委托小节讲过,这里就不详细赘述了,主要是Action<>、Func<>。
partial类
在 C# 中,partial 关键字允许将一个类、结构或接口的定义拆分到多个源文件中。每个文件包含类型定义的一部分,编译时这些部分会被合并成一个完整的类型。这种机制主要用于大型项目的代码管理、自动生成代码与手动编写代码的分离,以及实现部分方法等场景。
使用规则
所有部分必须位于相同的命名空间和程序集中。
每个部分都必须使用
partial关键字修饰。各部分在编译时会被合并,因此不能存在冲突的成员(如同名方法)。
部分方法必须返回
void,且不能使用out参数(C# 9 之前),并且可以没有实现。
示例 1:将单个类拆分到两个文件
假设我们有一个 Person 类,包含基本属性和方法,将其拆分为两个文件:
File1: Person.Part1.cs
namespace PartialClassDemo
{
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Introduce()
{
Console.WriteLine($"Hello, I'm {FirstName} {LastName}");
}
}
}
File2: Person.Part2.cs
namespace PartialClassDemo
{
public partial class Person
{
public int Age { get; set; }
public void ShowAge()
{
Console.WriteLine($"I'm {Age} years old.");
}
}
}
主程序调用
class Program
{
static void Main()
{
Person person = new Person { FirstName = "John", LastName = "Doe", Age = 30 };
person.Introduce(); // 输出:Hello, I'm John Doe
person.ShowAge(); // 输出:I'm 30 years old.
}
}
示例 2:使用部分方法
部分方法允许在一个部分中声明,在另一个部分中实现(可选)。如果未实现,编译器会移除所有调用。
File1: PersonWithMethod.Part1.cs
namespace PartialClassDemo
{
public partial class PersonWithMethod
{
public string Name { get; set; }
partial void OnNameChanged(); // 声明部分方法
public void ChangeName(string newName)
{
Name = newName;
OnNameChanged(); // 调用部分方法
}
}
}
File2: PersonWithMethod.Part2.cs
namespace PartialClassDemo
{
public partial class PersonWithMethod
{
partial void OnNameChanged() // 实现部分方法
{
Console.WriteLine($"Name changed to {Name}");
}
}
}
主程序调用
class Program
{
static void Main()
{
PersonWithMethod p = new PersonWithMethod();
p.ChangeName("Alice"); // 输出:Name changed to Alice
}
}
如果注释掉 PersonWithMethod.Part2.cs 中的实现,则 OnNameChanged 调用会被编译器移除,没有任何输出,且不会产生运行时开销。
更多推荐




所有评论(0)