前言
本文来自w3c教程的C#教程的学习笔记,对其中的示例有所删减与变更,建议以以下链接为准。
一、C# 与 C++的区别
C# 和 C++ 在语法上有许多相似之处,因为两者都是 C 语言家族的成员,继承了许多 C 语言的语法特性。但它们也有不少差异,主要体现在内存管理、面向对象编程的支持、以及语言设计哲学等方面。有关C/C++文档见以下链接:
C/C++:基本语法看这一篇就够了_h,.n:)!i-CSDN博客
1. 基本语法结构
特性 | C# | C++ |
---|---|---|
程序入口点 | static void Main(string[] args) | int main() |
语句结束 | 语句以分号 ; 结束 | 语句以分号 ; 结束 |
大括号 | 使用大括号 {} 来表示代码块 | 使用大括号 {} 来表示代码块 |
变量声明 | int x = 5; | int x = 5; |
2. 面向对象编程
特性 | C# | C++ |
---|---|---|
类的声明 | class MyClass { } | class MyClass { }; |
类的继承 | class Derived : Base { } | class Derived : public Base { }; |
访问修饰符 | public , private , protected , internal | public , private , protected |
构造函数 | public MyClass() {} | MyClass() {} |
析构函数 | 使用 IDisposable 接口来实现资源释放,自动垃圾回收, 以及 ~MyClass() {} | ~MyClass() {} |
接口 | interface IMyInterface { } | 没有直接的接口概念,使用抽象类来实现类似接口的功能 |
属性 | public int MyProperty { get; set; } | 没有直接的属性概念,需要通过方法来实现属性功能 |
3. 内存管理
特性 | C# | C++ |
---|---|---|
内存管理 | 自动垃圾回收 (GC) 管理内存 | 手动内存管理,使用 new 和 delete 或 malloc 和 free |
指针 | 不直接支持指针,只有通过 unsafe 代码块才支持 | 支持指针,允许直接访问内存 |
引用 | 支持引用类型,变量和对象引用的赋值是按引用传递的 | 支持引用类型,使用 & 符号声明引用变量 |
4. 异常处理
特性 | C# | C++ |
---|---|---|
异常处理 | try , catch , finally | try , catch , throw |
异常声明 | 异常不需要在方法声明中声明 | 异常可以声明为 throw() 来表示不抛出异常 |
自定义异常类 | 可以继承 Exception 类,定义自定义异常类 | 可以继承 std::exception 类,定义自定义异常类 |
5. 泛型与模板
特性 | C# | C++ |
---|---|---|
泛型 | 使用 generic 类型:List<T> | 使用模板:template <typename T> class MyClass { }; |
类型安全 | 泛型类型在运行时类型安全 | 模板类型在编译时类型安全,但没有运行时类型检查 |
6. 内联函数与委托/回调
特性 | C# | C++ |
---|---|---|
内联函数 | 使用 inline 不常见,更多使用方法封装和事件 | 使用 inline 关键字来提示编译器内联函数 |
委托与事件 | 委托用于封装方法引用,事件用于发布/订阅模式 | 使用函数指针或 std::function 来实现回调功能 |
7. 标准库/类库
特性 | C# | C++ |
---|---|---|
标准库 | .NET 提供丰富的类库,如 System , System.Linq 等 | 标准库提供多种功能,如 STL (标准模板库)、iostream 等 |
字符串类型 | string 类型为引用类型,自动管理内存 | std::string 为值类型,需要手动管理内存 |
8. 多线程与并发
特性 | C# | C++ |
---|---|---|
多线程库 | 使用 System.Threading 来管理线程和并发任务 | 使用 std::thread 来创建线程,并通过 std::mutex 等管理同步 |
异步编程 | 使用 async 和 await 来简化异步编程 | C++11 引入了异步编程,但没有专门的关键字,通常使用 std::async |
9. 语言特点
特性 | C# | C++ |
---|---|---|
语言设计 | 面向对象为主,强调开发效率和安全性 | 支持面向对象、面向过程和泛型编程,强调对性能的控制 |
多态实现 | 通过虚方法和接口实现多态 | 通过虚函数和继承实现多态 |
类型系统 | 强类型语言,类型检查严格 | 强类型语言,类型检查严格 |
二、开发环境
C# 的开发环境有很多种,Visual Studio、Visual Studio Code等,这里采用Visual Studio。
Visual Studio 是由微软(Microsoft)开发的集成开发环境(IDE),主要用于开发 Windows 和跨平台应用程序,支持多种编程语言,包括 C#、C++、Python、JavaScript、F# 等。它是开发 Windows 应用程序、Web 应用程序、移动应用程序以及游戏等的强大工具之一,具有丰富的功能和广泛的使用场景。
很强,也很复杂,以至于感觉有点难用,所以得讲,避免大家被劝退,这里简单走一下流程。
1、下载与安装
下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux (microsoft.com)
Visual Studio安装可以根据自己需要的组件进行安装,如果还不知道自己需要什么组件,先采用默认安装。后面可以双击安装程序,点击修改,继续组件的安装。
2、新建应用
这个是窗体应用,就是cmd那样的窗体,新建完成后会加载窗体环境与配置运行环境,文件很多。如果是一些复杂的应用,加载的东西更多。不同的应用,加载的环境有些区别,有些应用,对于小白来说,不太友好。窗体应用算是比较友好的了。
3、解决方案
在 Visual Studio 中,"解决方案"(Solution)是一个容器,用来组织一个或多个相关的项目。解决方案本质上是 Visual Studio 中管理和部署项目的方式,它帮助开发者组织代码、资源和依赖项,并控制多个项目之间的关系。
(1)解决方案的基本概念
解决方案(Solution):解决方案是 Visual Studio 中的顶级容器,包含了一个或多个项目。一个解决方案可以包含多个不同类型的项目,例如一个项目是 C# 应用程序,另一个是数据库项目,或者 Web 应用程序等。解决方案通过 .sln
文件来表示,文件名后缀为 .sln
,该文件包含了解决方案内各个项目的元数据和设置。
项目(Project):每个解决方案可以包含多个项目。项目是实现特定功能的代码单元,它包含源代码、资源文件、配置文件等内容。每个项目有自己的构建配置和输出目标,如 DLL、EXE 或其他类型的可执行文件。项目通过 .csproj
文件来表示(C# 项目是 .csproj
格式),该文件管理了项目的所有资源和配置。
解决方案资源管理器(Solution Explorer):它是 Visual Studio 的一个工具窗口,用来展示和管理当前打开的解决方案和项目结构。在这个窗口中,你可以查看和操作项目中的文件、引用的库、配置文件等。在这个窗口中,解决方案(Solution)作为最上层的节点展示,下面是项目(Project)和项目中的具体文件、文件夹等内容。
项目类型:项目可以是多种类型的应用程序,例如控制台应用程序(Console App)、Web 应用程序、类库(Library)等。每个项目的目标是实现特定的功能。
(2)对于上述界面
解决方案"ConsoleApp1" (1 个项目/共 1 个):这是一个名为 ConsoleApp1
的解决方案,它包含一个项目,并且该项目的类型是 C# 控制台应用程序。解决方案中当前只有一个项目(ConsoleApp1
),并且它包含的代码文件和资源,显示的是 C# 代码。
ConsoleApp1:这是解决方案中的一个项目,表示一个 C# 控制台应用程序。
C#:这个标签说明该项目是一个 C# 类型的项目,即使用 C# 语言编写的。
Properties:Properties
是每个 C# 项目中都有的文件夹,包含项目的设置和配置文件,如 AssemblyInfo.cs
文件,它定义了程序集的元数据。
引用 (References):References
文件夹包含了项目依赖的所有外部库(如 DLL 文件)和其他项目。通过引用,项目可以使用外部库或其他项目中的代码。
App.config:这是 C# 项目中的配置文件,用于存储应用程序的配置信息,例如数据库连接字符串、应用程序设置等。
Program.cs:Program.cs
是 C# 控制台应用程序的默认入口点。这个文件包含了 Main
方法,这是应用程序执行的起始点。
4、入口文件
这个界面就是我们编程的界面,把程序编入static void Main(string[] args)函数里面即可。
5、加载库
右键方案》添加》引用:浏览库文件添加。
6、运行
三、语法基础
1、数据类型
类型类别 | 类型示例 | 描述 | 范围/默认值 | 使用方式/示例代码 |
---|---|---|---|---|
值类型 (Value Types) | bool , byte , char , decimal , double , float , int , long , sbyte , short , uint , ulong , ushort | 值类型直接包含数据,存储在栈上。每次赋值都会复制值。 | bool : False, int : 0, char : '' , float : 0.0F, double : 0.0D 等 | int a = 10; float f = 3.14F; char c = 'A'; |
引用类型 (Reference Types) | object , dynamic , string , class , interface , delegate | 引用类型存储的是内存地址而非数据本身,指向堆上的对象。多个变量可以引用相同的内存位置。 | 由具体对象决定,如 string 默认空字符串 "" ,object 默认 null | string str = "Hello"; object obj = 100; dynamic d = 10.5; |
指针类型 (Pointer Types) | char* , int* , float* , 等 | 指针类型存储内存地址,允许直接操作内存。只能在 unsafe 代码块中使用。 | 在 C# 中需在 unsafe 环境下使用,无默认值 | unsafe { int* ptr = &a; } |
(1)指针使用方式示例
// 指针使用示例,还需要设置Visual Code,让其运行不安全代码
unsafe
{
int a = 10;
int* ptr = &a; // 获取 a 的内存地址
Console.WriteLine(*ptr); // 通过指针访问 a 的值
Console.ReadLine();
}
2、类型转换
转换类型 | 描述 | 示例代码 |
---|---|---|
隐式类型转换 (Implicit Conversion) | C# 默认的类型转换方式,编译器会自动进行转换,通常是从较小类型到较大类型,或从派生类到基类。 | int i = 10; long l = i; // int 转换为 long |
显式类型转换 (Explicit Conversion) | 用户通过强制转换运算符显式地进行转换,通常用于不同类型之间的转换,需要注意类型兼容性。 | double d = 5673.74; int i = (int)d; // 强制转换为 int |
ToBoolean | 将类型转换为布尔型。 | bool b = Convert.ToBoolean("true"); |
ToByte | 将类型转换为字节类型。 | byte b = Convert.ToByte(123.45); |
ToChar | 将类型转换为单个 Unicode 字符类型。 | char c = Convert.ToChar(65); // ASCII 'A' |
ToDateTime | 将类型(整数或字符串类型)转换为日期时间类型。 | DateTime dt = Convert.ToDateTime("2021-12-31"); |
ToDecimal | 将浮点型或整数类型转换为十进制类型。 | decimal dec = Convert.ToDecimal(123.45); |
ToDouble | 将类型转换为双精度浮点型。 | double d = Convert.ToDouble(123); |
ToInt16 | 将类型转换为 16 位整数类型。 | short s = Convert.ToInt16(123); |
ToInt32 | 将类型转换为 32 位整数类型。 | int i = Convert.ToInt32(123.45); |
ToInt64 | 将类型转换为 64 位整数类型。 | long l = Convert.ToInt64(123.45); |
ToSByte | 将类型转换为有符号字节类型。 | sbyte sb = Convert.ToSByte(127); |
ToSingle | 将类型转换为单精度浮点数类型。 | float f = Convert.ToSingle(123.45); |
ToString | 将类型转换为字符串类型。 | string str = Convert.ToString(123); |
ToType | 将类型转换为指定的目标类型。 | object obj = 123; int i = (int)obj; |
ToUInt16 | 将类型转换为 16 位无符号整数类型。 | ushort us = Convert.ToUInt16(123); |
ToUInt32 | 将类型转换为 32 位无符号整数类型。 | uint ui = Convert.ToUInt32(123.45); |
ToUInt64 | 将类型转换为 64 位无符号整数类型。 | ulong ul = Convert.ToUInt64(123.45); |
3、变量
概念/类型 | 描述 | 举例 |
---|---|---|
变量定义 | 在 C# 中,变量是供程序操作的存储区,必须声明其数据类型。 | int i, j, k; |
数据类型 | 每个变量都有一个特定的类型,决定了内存大小和布局。 | int , double , char , float |
变量初始化 | 变量可以在声明时进行初始化,通过赋值给变量。 | int i = 100; , double pi = 3.14; |
整数类型 | 用于存储整数值。包括有符号和无符号整数类型。 | int , long , byte , sbyte , char |
浮点类型 | 用于存储小数(浮点数)值。 | float , double |
十进制类型 | 用于高精度存储十进制数,通常用于财务和货币计算。 | decimal |
布尔类型 | 用于存储逻辑值,通常表示真(true)或假(false)。 | bool |
空类型 | 用于表示可能为空的值。 | Nullable<int> , int? |
Lvalue | 左值(Lvalue):可以出现在赋值语句的左边,表示可修改的内存位置。 | int g = 20; (g 是 Lvalue) |
Rvalue | 右值(Rvalue):可以出现在赋值语句的右边,表示常量或表达式的结果。 | 20 = 20; (无效,因为 20 是 Rvalue) |
4、常量
概念 | 描述 | 示例 |
---|---|---|
常量 | 常量是固定值,在程序执行期间不能改变。常量可以是任何基本数据类型,如整数、浮点数、字符、字符串等。 | const int x = 10; |
整数常量 | 整数常量可以是十进制、八进制、十六进制,带有后缀的无符号和长整型常量。 | 85 , 0213 , 0x4b , 30u , 30l |
浮点常量 | 浮点常量由整数部分、小数部分和指数部分组成,支持小数形式和指数形式。 | 3.14159 , 314159E-5L , 510E , 210f |
字符常量 | 字符常量用单引号括起来,可以是普通字符、转义序列或通用字符。 | 'x' , '\t' , '\n' , '\u02C0' |
字符串常量 | 字符串常量用双引号 "" 括起来,也可以使用 @"" 形式表示原始字符串。 | "hello, dear" , "hello, \ndear" , @"hello dear" |
转义序列 | 用于表示特殊字符,如换行符、制表符等。常用于字符常量和字符串常量中。 | \n , \t , \\ , \u02C0 |
常量声明 | 常量通过 const 关键字声明,声明后其值不可更改。 | const double pi = 3.14159; |
常量实例 | 示例代码演示常量的定义和使用。通过常量计算圆的面积。 | const double pi = 3.14159; double area = pi * r * r; |
5、运算符
这里是对 C# 中运算符的总结,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符以及杂项运算符:
(1)算术运算符
运算符 | 描述 | 实例 |
---|---|---|
| 把两个操作数相加 |
|
| 从第一个操作数中减去第二个操作数 |
|
| 把两个操作数相乘 |
|
| 分子除以分母 |
|
| 取模运算符,整除后的余数 |
|
| 自增运算符,整数值增加 1 |
|
| 自减运算符,整数值减少 1 |
|
(2)关系运算符
运算符 | 描述 | 实例 |
---|---|---|
| 检查两个操作数的值是否相等 |
|
| 检查两个操作数的值是否不相等 |
|
| 检查左操作数的值是否大于右操作数的值 |
|
| 检查左操作数的值是否小于右操作数的值 |
|
| 检查左操作数的值是否大于或等于右操作数 |
|
| 检查左操作数的值是否小于或等于右操作数 |
|
(3)逻辑运算符
运算符 | 描述 | 实例 |
---|---|---|
| 逻辑与运算符,如果两个操作数都为真则结果为真 |
|
` | ` | |
| 逻辑非运算符,用来逆转操作数的逻辑状态 |
|
(4)位运算符
运算符 | 描述 | 实例 |
---|---|---|
| 位与运算符,如果同时存在于两个操作数中 |
|
` | ` | 位或运算符,如果存在于任一操作数中 |
| 位异或运算符,如果存在于其中一个操作数中但不同时存在 |
|
| 位补码运算符,翻转位效果 |
|
| 位左移运算符,将左操作数的值左移指定的位数 |
|
| 位右移运算符,将左操作数的值右移指定的位数 |
|
(5)赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
| 简单的赋值运算符,把右边操作数的值赋给左边操作数 |
|
| 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 |
|
| 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 |
|
| 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 |
|
| 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 |
|
| 求模且赋值运算符,求两个操作数的模赋值给左边操作数 |
|
| 左移且赋值运算符 |
|
| 右移且赋值运算符 |
|
| 按位与且赋值运算符 |
|
| 按位异或且赋值运算符 |
|
` | =` | 按位或且赋值运算符 |
(6)杂项运算符
运算符 | 描述 | 实例 |
---|---|---|
| 返回数据类型的大小 |
|
| 返回类的类型 |
|
| 返回变量的地址 |
|
| 变量的指针 |
|
| 条件表达式 |
|
| 判断对象是否为某一类型 |
|
| 强制转换,即使转换失败也不会抛出异常 |
|
(7)运算符优先级
C# 运算符的优先级决定了在一个表达式中运算符的执行顺序。高优先级的运算符会先执行。以下是按优先级从高到低排列的运算符:
优先级 | 运算符 | 结合性 |
---|---|---|
1 |
| 从左到右 |
2 |
| 从右到左 |
3 |
| 从左到右 |
4 |
| 从左到右 |
... | ... | ... |
此优先级列表会影响运算顺序,在没有括号的情况下,具有高优先级的运算符会首先被计算。
6、 判断
语句类型 | 描述 | 示例 |
---|---|---|
if 语句 | 由一个布尔表达式后跟一个或多个语句组成,执行条件为真时的语句。 | if (a >= 60) { Console.WriteLine("及格"); } |
if...else 语句 | 一个 if 语句后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。 | if (a >= 60) { Console.WriteLine("及格"); } else { Console.WriteLine("不及格"); } |
嵌套 if 语句 | 在一个 if 或 else if 语句内使用另一个 if 或 else if 语句。 | if (a > 90) { Console.WriteLine("优秀"); } else if (a >= 60) { Console.WriteLine("及格"); } |
switch 语句 | 允许测试一个变量等于多个值时的情况,通常用于多重条件判断。 | switch (a) { case 1: Console.WriteLine("一"); break; case 2: Console.WriteLine("二"); break; } |
嵌套 switch 语句 | 在一个 switch 语句内使用另一个 switch 语句,处理复杂的多重条件。 | switch (a) { case 1: switch (b) { case 1: Console.WriteLine("一"); break; } break; } |
三目运算符 (? :) | 通过 Exp1 ? Exp2 : Exp3 表达式替代 if...else 语句,Exp1 为真时返回 Exp2,否则返回 Exp3。 | result = (a >= 60) ? "及格" : "不及格"; |
条件运算符替代 if | ? : 运算符只能用于表达式,不能执行函数操作,但适用于简单的判断逻辑,常用于返回值的判断。 | a >= 60 ? true : false; |
7、循环
(1)循环类型
循环类型 | 描述 | 示例 |
---|---|---|
while 循环 | 当给定条件为真时,重复执行语句或语句组。在执行循环主体之前会测试条件。 | while (a < 10) { Console.WriteLine(a); a++; } |
for 循环 | 多次执行一个语句序列,简化循环变量的管理。通常用于已知循环次数的情况。 | for (int i = 0; i < 10; i++) { Console.WriteLine(i); } |
foreach 循环 | 用于遍历集合类型(如数组、列表等),简化了循环访问集合元素的方式。 | foreach (var item in array) { Console.WriteLine(item); } |
do...while 循环 | 在循环体结束后测试条件,保证循环体至少执行一次。 | do { Console.WriteLine(a); a++; } while (a < 10); |
嵌套循环 | 在一个循环内部再嵌套另一个或多个循环,常用于处理多维数据。 | for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Console.WriteLine(i + " " + j); } } |
(2)循环控制
控制语句 | 描述 | 示例 |
---|---|---|
break | 终止当前循环或 switch 语句,程序流会继续执行 loop 或 switch 后的下一条语句。 | while (true) { if (a == 10) break; Console.WriteLine(a); a++; } |
continue | 跳过循环主体的剩余部分,立即回到循环条件的判断阶段,继续执行下一个循环。 | for (int i = 0; i < 10; i++) { if (i % 2 == 0) continue; Console.WriteLine(i); } |
无限循环 | 如果条件永远为真,循环将持续无限执行。通常可以使用 for(;;) 结构实现。 | for (; ;) { Console.WriteLine("Hey! I am Trapped"); } |
8、封装
修饰符 | 描述 | 访问范围 | 示例 |
---|---|---|---|
Public | 公开访问修饰符,允许类的成员被所有类和对象访问。 | 任何类或对象都可以访问。 | public int x; |
Private | 私有访问修饰符,仅允许类内部的成员函数访问该成员。其他类或对象无法访问。 | 只能在定义该成员的类内部访问。 | private int x; |
Protected | 保护访问修饰符,允许子类访问基类的成员,但类外的其他对象无法访问。 | 当前类及其派生类可以访问。 | protected int x; |
Internal | 内部访问修饰符,允许同一个程序集内的其他类或对象访问。 | 同一程序集内的类和对象可以访问。 | internal int x; |
Protected internal | 受保护内部访问修饰符,允许同一程序集内的类、派生类及其成员访问。 | 同一程序集内的类、派生类以及当前类可以访问。 | protected internal int x; |
(1)Public示例
using System;
namespace RectangleApplication
{
class Rectangle
{
public double length; // 公共成员变量
public double width;
public double GetArea() // 公共方法
{
return length * width;
}
public void Display()
{
Console.WriteLine("Length: " + length);
Console.WriteLine("Width: " + width);
Console.WriteLine("Area: " + GetArea());
}
}
class ExecuteRectangle
{
static void Main(string[] args)
{
Rectangle r = new Rectangle();
r.length = 5; // 直接访问公有成员
r.width = 10;
r.Display();
}
}
}
9、方法
概念 | 描述 | 示例代码 |
---|---|---|
方法定义 | 定义方法的语法:<Access Specifier> <Return Type> <Method Name>(Parameter List) { Method Body } | public int FindMax(int num1, int num2) { return (num1 > num2) ? num1 : num2; } |
方法调用 | 使用方法名调用方法,方法可以带参数。 | ret = n.FindMax(a, b); Console.WriteLine("最大值是: {0}", ret); |
递归方法调用 | 方法可以自我调用,这被称为递归。 | public int factorial(int num) { if (num == 1) return 1; else return factorial(num - 1) * num; } |
值参数传递 | 传递的是值的副本。方法修改参数时,不会影响实际参数的值。 | public void swap(int x, int y) { int temp = x; x = y; y = temp; } n.swap(a, b); // a 和 b 的值不变 |
引用参数传递 (ref) | 传递的是参数的引用,方法可以修改参数的值,影响实际参数。 | public void swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } n.swap(ref a, ref b); // a 和 b 的值会交换 |
输出参数传递 (out) | 用于从方法中返回多个值,参数无需初始化。在方法内赋值后,调用者获取新的值。 | public void getValue(out int x) { x = 5; } n.getValue(out a); // a 被赋值为 5 |
方法返回值 | 方法通过 return 语句返回一个值。 | public int Add(int x, int y) { return x + y; } |
实例:交换值(值传递) | 演示了值传递时,交换操作对原始值没有影响。 | Console.WriteLine("交换前: a={0}, b={1}", a, b); n.swap(a, b); Console.WriteLine("交换后: a={0}, b={1}", a, b); // 结果显示 a 和 b 值没有变化 |
实例:交换值(引用传递) | 演示了引用传递时,交换操作会影响原始值。 | Console.WriteLine("交换前: a={0}, b={1}", a, b); n.swap(ref a, ref b); Console.WriteLine("交换后: a={0}, b={1}", a, b); // 结果显示 a 和 b 值交换 |
实例:获取多个值 | 演示了输出参数可以从方法中返回多个值。 | public void getValues(out int x, out int y) { x = 7; y = 8; } n.getValues(out a, out b); Console.WriteLine("a={0}, b={1}", a, b); |
10、可空类型
概念 | 描述 | 语法与示例 | 结果示例 |
---|---|---|---|
可空类型(Nullable Types) | 可空类型允许值类型(如 int 、bool 、double )可以接受一个 null 值,表示该值未定义。 | <data_type>? <variable_name> = null; | int? num1 = null; int? num2 = 45; Console.WriteLine(num1, num2); 输出: null, 45 |
可空类型声明 | 用 ? 声明可空类型,允许为该类型赋值 null 或者其正常值范围内的数值。 | int? num1 = null; double? num3 = new double?(); | num1 = null num3 = null |
空值输出 | 可空类型变量若未赋值,输出为空值 null 。 | bool? boolval = new bool?(); Console.WriteLine(boolval); | 输出: null |
空值合并运算符(??) | ?? 运算符用于提供一个默认值。如果可空类型的值为 null ,则返回指定的默认值,否则返回可空类型的值。 | num3 = num1 ?? 5.34; num3 = num2 ?? 5.34; | 输出: num3 = 5.34 (当 num1 为 null 时使用默认值 5.34)num3 = 3.14157 (当 num2 不为 null 时使用 num2 值) |
11、数组
概念 | 描述 | 语法与示例 | 结果示例 |
---|---|---|---|
数组声明 | 数组是一个固定大小的顺序集合,用来存储相同类型的元素。数组变量可以通过索引访问。 | datatype[] arrayName; double[] balance; | 声明一个 double[] 类型的数组变量 balance 。 |
数组初始化 | 声明数组后需要使用 new 关键字初始化数组,并指定数组的大小或赋初值。 | double[] balance = new double[10]; int[] marks = { 99, 98, 92, 97, 95 }; | 初始化数组 balance ,大小为 10。初始化数组 marks ,并赋值 { 99, 98, 92, 97, 95 } 。 |
赋值给数组 | 使用索引为数组中的特定元素赋值。可以在声明数组时同时初始化数组元素。 | balance[0] = 4500.0; int[] marks = new int[5] { 99, 98, 92, 97, 95 }; | 赋值 balance[0] = 4500.0; 和初始化 marks 数组。 |
访问数组元素 | 数组元素通过索引来访问。索引从 0 开始。数组元素通过 array[index] 的方式访问。 | double salary = balance[9]; | 访问 balance[9] 元素的值并赋值给 salary 。 |
使用 foreach 循环 | 使用 foreach 循环遍历数组元素。比 for 循环更简洁。 | foreach (int j in n) { Console.WriteLine("Element[{0}] = {1}", i, j); } | 输出数组 n 中的每个元素值。 |
多维数组 | C# 支持多维数组,最简单的是二维数组。多维数组中的元素通过多个索引访问。 | int[,] matrix = new int[3, 4]; | 声明并初始化一个 3 行 4 列的二维数组 matrix 。 |
交错数组 | C# 支持交错数组,即数组的数组,类似于二维数组但每行的长度不固定。 | int[][] jaggedArray = new int[3][]; jaggedArray[0] = new int[2]; | 声明一个交错数组 jaggedArray ,其中每个子数组的长度可以不同。 |
传递数组给函数 | 数组可以作为参数传递给函数,传递的是数组的引用。 | void DisplayArray(int[] arr) { } DisplayArray(marks); | 将 marks 数组传递给 DisplayArray 函数。 |
参数数组 | 通过 params 关键字传递未知数量的参数,允许传递不同长度的参数数组。 | void DisplayNumbers(params int[] numbers) { } | 使用 params 传递多个数值给函数 DisplayNumbers 。 |
Array 类 | Array 类是所有数组的基类,提供了数组的各种方法和属性。 | Array.Sort(array); Array.Length | 使用 Array.Sort() 对数组进行排序,使用 array.Length 获取数组的长度。 |
12、 字符串
概念/方法 | 描述 | 示例代码 |
---|---|---|
创建 String 对象 | 创建 String 对象的几种方式: | |
1. 通过给 String 变量指定一个字符串 | 直接给变量赋值字符串。 | string fname = "Rowan"; |
2. 通过使用 String 类构造函数 | 使用字符数组构造一个新的 String 对象。 | char[] letters = { 'H', 'e', 'l', 'l', 'o' }; string greetings = new string(letters); |
3. 通过字符串串联运算符(+) | 使用 + 运算符将字符串连接成一个新的 String 对象。 | string fullname = fname + lname; |
4. 通过方法返回字符串 | 调用方法并返回字符串。 | string message = String.Join(" ", sarray); |
5. 通过格式化方法 | 使用 String.Format 方法将值转换为字符串表示形式。 | string chat = String.Format("Message sent at {0:t} on {0:D}", waiting); |
String 类的属性 | String 类的常用属性。 | |
1. Chars | 获取当前 String 对象中指定位置的字符。 | char ch = myString[0]; |
2. Length | 获取 String 对象中的字符数。 | int length = myString.Length; |
String 类的常用方法 | String 类的常用方法。 | |
1. Compare | 比较两个字符串并返回它们在排序顺序中的位置。 | int result = String.Compare(str1, str2); |
2. Concat | 连接两个或多个字符串。 | string fullName = String.Concat(firstName, lastName); |
3. Contains | 判断当前字符串是否包含指定的子字符串。 | bool contains = str.Contains("test"); |
4. Copy | 创建一个新的 String 对象,值与指定字符串相同。 | string newStr = String.Copy(str); |
5. CopyTo | 从指定位置开始将当前字符串的字符复制到字符数组。 | myString.CopyTo(0, destArray, 0, 5); |
6. EndsWith | 判断当前字符串是否以指定的子字符串结尾。 | bool endsWith = str.EndsWith("test"); |
7. Equals | 判断两个字符串是否相等。 | bool equals = str1.Equals(str2); |
8. Format | 格式化字符串,将指定的格式项替换为对象的字符串表示。 | string formattedStr = String.Format("Price: {0:C}", price); |
9. IndexOf | 返回指定字符或子字符串第一次出现的索引。 | int index = str.IndexOf("test"); |
10. Insert | 在指定索引位置插入字符串。 | string result = str.Insert(5, "ABC"); |
11. IsNullOrEmpty | 判断字符串是否为 null 或空字符串。 | bool isEmpty = String.IsNullOrEmpty(str); |
12. Join | 将字符串数组中的所有元素连接成一个单一字符串,并用指定分隔符分隔。 | string result = String.Join(", ", array); |
13. LastIndexOf | 返回指定字符或子字符串最后一次出现的索引。 | int lastIndex = str.LastIndexOf("test"); |
14. Remove | 从指定位置开始移除字符。 | string result = str.Remove(5); |
15. Replace | 替换指定字符或子字符串。 | string result = str.Replace("old", "new"); |
16. Split | 按指定字符分割字符串并返回子字符串数组。 | string[] substrings = str.Split(','); |
17. StartsWith | 判断字符串是否以指定的子字符串开始。 | bool startsWith = str.StartsWith("test"); |
18. ToCharArray | 将字符串转换为字符数组。 | char[] charArray = str.ToCharArray(); |
19. ToLower | 将字符串转换为小写。 | string lowerStr = str.ToLower(); |
20. ToUpper | 将字符串转换为大写。 | string upperStr = str.ToUpper(); |
21. Trim | 移除字符串的前导和尾随空白字符。 | string trimmedStr = str.Trim(); |
13、 结构
序号 | 主题 | 描述 |
---|---|---|
1 | 定义结构 | 使用 struct 关键字定义结构,例如 struct Books 。 |
2 | 结构成员 | 结构可以包含字段、方法、属性、索引器、事件等。 |
3 | 构造函数 | 结构可以定义构造函数,但不能定义析构函数。结构没有默认构造函数。 |
4 | 值类型与引用类型 | 结构是值类型,赋值给另一个结构时会复制其数据,而类是引用类型。 |
5 | 结构的继承 | 结构不能继承类或其他结构,也不能作为其他结构或类的基类。 |
6 | 接口实现 | 结构可以实现一个或多个接口。 |
7 | 成员权限 | 结构成员不能指定为 abstract 、virtual 或 protected 。 |
8 | 默认实例化 | 结构可以通过 new 运算符创建实例,也可以在未使用 new 的情况下实例化。 |
9 | 字段初始化 | 当不使用 new 时,结构的字段只有在初始化后才能使用。 |
10 | 结构与类的对比 | 结构和类的主要不同点包括:结构是值类型,结构不支持继承,结构不能声明默认构造函数等。 |
(1)对比类与结构
特性 | 结构(Struct) | 类(Class) |
---|---|---|
类型 | 值类型 | 引用类型 |
继承 | 不支持继承 | 支持继承 |
默认构造函数 | 自动提供但不可更改 | 可以定义自定义构造函数 |
字段初始化 | 可以不使用 new 关键字实例化 | 必须使用 new 关键字实例化 |
大小 | 存储在栈上(一般较小) | 存储在堆上(一般较大) |
垃圾回收 | 不受垃圾回收控制 | 由垃圾回收器管理 |
14、枚举
序号 | 主题 | 描述 |
---|---|---|
1 | 定义枚举 | 使用 enum 关键字声明枚举类型,例如:enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat }; |
2 | 枚举的值 | 枚举成员的默认值从 0 开始,每个后续成员的值比前一个大 1。可以通过显式指定值来改变默认值。 |
3 | 枚举成员的值 | 每个枚举成员实际上是一个整数值,默认从 0 开始。如果需要,可以手动赋予枚举成员不同的整数值。 |
4 | 枚举类型 | 枚举是值类型,它具有自己的数据值,并且不能被继承或传递继承。 |
5 | 枚举与整数转换 | 枚举成员可以通过显式转换为整数值,整数也可以被转换回对应的枚举类型。 |
15、 类
概念 | 描述 | 示例代码 |
---|---|---|
类的定义 | 使用 class 关键字定义类,类的成员包括变量和方法。 | class Box { public double length; public double breadth; public double height; } |
实例化类 | 使用 new 关键字创建类的实例(对象)。 | Box Box1 = new Box(); Box Box2 = new Box(); |
成员变量 | 类的属性,定义类的状态。 | public double length; |
成员方法 | 类的行为或操作,用于定义类的功能。 | public double getVolume() { return length * breadth * height; } |
封装 | 通过访问控制符将类的成员限制访问,常将变量设为 private ,并通过 public 方法访问。 | private double length; public void setLength(double len) { length = len; } |
构造函数 | 特殊方法,用于在创建对象时初始化对象的成员。构造函数名称与类名相同。可以是默认构造函数或参数化构造函数。 | public Line() { Console.WriteLine("对象已创建"); } public Line(double len) { length = len; } |
析构函数 | 特殊方法,用于对象超出作用域时释放资源。析构函数的名称是类名加波浪符(~ )。 | ~Line() { Console.WriteLine("对象已删除"); } |
静态成员 | 属于类而非实例的成员,所有实例共享一个副本。静态成员使用 static 关键字。 | public static int num; public static int getNum() { return num; } |
静态方法 | 静态方法只能访问静态变量,不能访问实例成员。 | public static int getNum() { return num; } |
访问控制符 | 控制类成员的访问权限,常见的有 public 、private 、protected 、internal 。 | public double length; private double length; |
成员的访问 | 使用点运算符 (. ) 来访问对象的成员。 | Box1.length = 5.0; Console.WriteLine(Box1.getVolume()); |
类与对象的关系 | 类是一个模板,定义了对象的结构和行为,对象是类的实例。 | Box Box1 = new Box(); |
16、继承
概念 | 描述 | 示例代码 |
---|---|---|
继承概述 | 继承是面向对象程序设计中的重要概念,允许一个类继承另一个类的成员,使得代码复用和维护变得更容易。 | 派生类继承了基类的成员,可以重用基类的代码。 |
基类与派生类 | 基类是被继承的类,派生类是继承基类的类。派生类可访问基类的公有和保护成员,不能访问基类的私有成员。 | 基类 Shape 和派生类 Rectangle 。 |
属于(IS-A)关系 | 继承实现了“属于”关系。例如,狗属于哺乳动物 ,哺乳动物属于动物 ,所以狗属于动物 。 | Shape 是一个基类,Rectangle 是派生类。 |
基类的成员 | 基类的成员(变量和方法)会被派生类继承,派生类可以重用基类的代码并扩展功能。 | 基类成员:public void setWidth(int w) ,public void setHeight(int h) 。 |
构造函数与初始化 | 派生类无法继承基类的构造函数,但可以通过构造函数初始化基类成员。派生类对象创建前,基类对象先被创建。 | public Rectangle(double l, double w) : base(l, w) 。 |
代码示例:继承实现 | 创建基类和派生类,派生类扩展了基类的功能。 | class Shape { ... } class Rectangle : Shape { ... } |
多重继承 | C# 不支持多重继承,但可以通过接口来实现类似的功能。 | 使用接口 PaintCost 实现多重继承:class Rectangle : Shape, PaintCost { ... } |
多重继承示例 | 通过接口模拟多重继承,实现多个类的功能组合。 | public interface PaintCost { int getCost(int area); } class Rectangle : Shape, PaintCost { ... } |
派生类扩展功能 | 派生类不仅继承基类的功能,还可以添加更多成员或覆盖基类的方法来扩展功能。 | 在 Rectangle 中添加方法 getArea() 和 getCost() ,并通过 Display() 输出数据。 |
基类初始化与成员 | 基类在派生类的构造函数中进行初始化,派生类在初始化时获得基类的成员。 | public Tabletop(double l, double w) : base(l, w) base.Display() |
17、多态性
概念 | 描述 | 示例代码 |
---|---|---|
多态性概述 | 多态性意味着有多重形式,它允许通过同一接口调用不同的功能。在面向对象编程中,多态性通常表现为"一个接口,多个功能"。 | - 静态多态性:函数重载和运算符重载。 - 动态多态性:通过抽象类和虚方法来实现运行时的多态。 |
静态多态性 | 静态多态性(也称为早期绑定)是通过编译时确定函数的响应。静态多态性通过函数重载和运算符重载实现。 | - 函数重载示例:同一个函数名根据参数类型或数量的不同执行不同的功能。 |
函数重载 | 函数重载是指在同一个类中,允许有多个相同函数名,但它们的参数列表不同(如类型不同或个数不同)。 |
|
动态多态性 | 动态多态性(也称为晚期绑定)是指在运行时根据对象的实际类型来决定调用哪个方法。通过抽象类和虚方法来实现。 | - 使用抽象类和虚方法实现不同派生类的不同实现。例如,基类定义一个虚方法,派生类覆盖它。 |
抽象类与抽象方法 | 抽象类不能被实例化,它包含抽象方法,派生类必须重写这些方法。抽象方法在基类中没有实现,而是由派生类来实现。 |
|
虚方法 | 虚方法有默认实现,派生类可以选择是否重写它。虚方法支持运行时多态,允许基类与派生类的行为不同。 |
|
抽象方法与虚方法区别 | - 抽象方法没有实现,必须在派生类中实现,派生类不能实例化。 - 虚方法有默认实现,可以在派生类中覆盖,也可以不覆盖。 | - 抽象方法:必须在派生类中重写,且派生类必须是抽象类。 - 虚方法:可以选择覆盖或不覆盖。 |
18、运算符重载
运算符 | 描述 | 是否可以重载 |
---|---|---|
+ | 二元运算符,用于执行加法操作。 | 可重载 |
- | 二元运算符,用于执行减法操作。 | 可重载 |
* | 二元运算符,用于执行乘法操作。 | 可重载 |
/ | 二元运算符,用于执行除法操作。 | 可重载 |
% | 二元运算符,用于执行求余操作。 | 可重载 |
! | 一元运算符,用于取反操作。 | 可重载 |
~ | 一元运算符,用于按位取反操作。 | 可重载 |
++ | 一元运算符,用于执行自增操作。 | 可重载 |
-- | 一元运算符,用于执行自减操作。 | 可重载 |
== | 比较运算符,用于检查两个对象是否相等。 | 可重载 |
!= | 比较运算符,用于检查两个对象是否不相等。 | 可重载 |
< | 比较运算符,用于检查左边的对象是否小于右边的对象。 | 可重载 |
> | 比较运算符,用于检查左边的对象是否大于右边的对象。 | 可重载 |
<= | 比较运算符,用于检查左边的对象是否小于或等于右边的对象。 | 可重载 |
>= | 比较运算符,用于检查左边的对象是否大于或等于右边的对象。 | 可重载 |
&& | 逻辑运算符,用于执行逻辑与操作。 | 不可重载 |
+= | 赋值运算符,用于执行加法赋值操作。 | 不可重载 |
-= | 赋值运算符,用于执行减法赋值操作。 | 不可重载 |
*= | 赋值运算符,用于执行乘法赋值操作。 | 不可重载 |
/= | 赋值运算符,用于执行除法赋值操作。 | 不可重载 |
= | 赋值运算符,用于执行简单赋值操作。 | 不可重载 |
. | 用于访问对象的成员(字段、方法等)。 | 不可重载 |
?: | 三元运算符,用于根据条件表达式返回不同的值。 | 不可重载 |
-> | 用于访问结构体的成员。 | 不可重载 |
new | 用于创建对象实例。 | 不可重载 |
is | 用于检查对象是否为指定类型。 | 不可重载 |
sizeof | 用于获取数据类型的大小。 | 不可重载 |
typeof | 用于获取类型的实例。 | 不可重载 |
(1)可重载示例
// 在下面的例子中,我们重载了 + 运算符,使得 Box 类的对象可以通过加法运算符相加。
using System;
namespace OperatorOverloadExample
{
class Box
{
public double Length { get; set; }
public double Width { get; set; }
public double Height { get; set; }
// 重载 + 运算符
public static Box operator +(Box b1, Box b2)
{
Box result = new Box();
result.Length = b1.Length + b2.Length;
result.Width = b1.Width + b2.Width;
result.Height = b1.Height + b2.Height;
return result;
}
public override string ToString()
{
return $"Length: {Length}, Width: {Width}, Height: {Height}";
}
}
class Program
{
static void Main()
{
Box box1 = new Box { Length = 5, Width = 3, Height = 2 };
Box box2 = new Box { Length = 4, Width = 2, Height = 1 };
Box box3 = box1 + box2; // 使用重载的 + 运算符
Console.WriteLine("Box1: " + box1);
Console.WriteLine("Box2: " + box2);
Console.WriteLine("Box3 (Box1 + Box2): " + box3);
}
}
}
(2)不可重载示例
// C# 中的逻辑运算符,如 && 和 || 是不可重载的,不能自定义它们的行为。以下是一个试图重载 && 运算符的例子,它会导致编译错误。
using System;
namespace OperatorOverloadExample
{
class Box
{
public double Length { get; set; }
// 编译时错误:无法重载 && 运算符
public static bool operator &&(Box b1, Box b2)
{
return b1.Length > 5 && b2.Length > 5;
}
}
class Program
{
static void Main()
{
Box box1 = new Box { Length = 6 };
Box box2 = new Box { Length = 7 };
bool result = box1 && box2; // 编译错误
Console.WriteLine(result);
}
}
}
19、接口
特性 | 接口 | 抽象类 |
---|---|---|
定义方式 | 使用 interface 关键字声明 | 使用 abstract class 关键字声明 |
成员定义 | 只包含成员声明,没有实现。成员可以是方法、属性、事件。 | 包含方法声明和实现。可以包含字段、属性、构造函数等。 |
构造函数 | 不能包含构造函数 | 可以包含构造函数 |
字段 | 不能包含字段 | 可以包含字段 |
访问修饰符 | 成员默认是 public ,不能使用 private 、protected 等修饰符 | 可以使用不同的访问修饰符,如 private 、protected 、public 等 |
成员实现要求 | 实现接口时,必须实现接口中声明的所有方法和属性 | 只需要实现抽象方法,其他方法可以有实现。 |
多重继承 | 支持多重继承,一个类可以实现多个接口 | 不支持多重继承,只能继承一个抽象类或其他类 |
继承对象 | 类继承接口时,必须实现接口中的所有方法 | 类继承抽象类时,必须实现抽象方法 |
适用场景 | 用于定义类的契约或协议,类通过接口规范行为 | 用于定义类的通用行为并提供部分实现 |
20、命名空间
特性 | 描述 |
---|---|
定义 | 使用 namespace 关键字定义,后跟命名空间的名称。例如:namespace namespace_name { } |
作用 | 提供一种机制将一组相关名称(如类、函数)分组,以避免与其他命名空间中的同名元素冲突。 |
访问命名空间中的成员 | 通过将命名空间名称放在前面访问成员,例如:namespace_name.item_name 。 |
嵌套命名空间 | 一个命名空间可以包含另一个命名空间,使用点(. )运算符访问嵌套命名空间中的成员。例如:first_space.second_space.member_name |
using 关键字 | using 关键字允许在程序中使用命名空间中的成员时,不需要每次都写出完整的命名空间路径。例如:using first_space; |
完全限定名称 | 如果没有使用 using 关键字,可以使用完全限定名称访问命名空间中的成员,例如:System.Console.WriteLine("Hello"); |
访问多个命名空间 | 可以使用多个 using 语句引入不同的命名空间。例如:using first_space; using second_space; |
成员访问 | 直接访问成员时,使用命名空间作为前缀,如:first_space.namespace_cl 。通过实例化类后,调用其方法:fc.func() 。 |
命名空间成员的限制 | 命名空间不包含实现,只有类、结构、接口和枚举等成员的声明。实现由类或其他类型负责。 |
21、预处理指令
预处理器指令 | 描述 |
---|---|
#define | 定义一个符号常量,后续可在条件编译中使用。例如,#define PI 定义一个符号 PI 。 |
#undef | 取消已定义的符号。例如,#undef PI 取消对符号 PI 的定义。 |
#if | 用于测试一个符号是否为真,条件成立时编译器编译 #if 和下一个指令之间的代码。例如,#if DEBUG 测试 DEBUG 是否已定义。 |
#else | 与 #if 配合使用,在条件不成立时执行的代码部分。例如,#else 语句会在 #if 条件未满足时执行。 |
#elif | 与 #if 配合使用,表示如果 #if 条件不满足时,使用新的条件进行判断。例如,#elif DEBUG 。 |
#endif | 结束一个条件编译块。必须在 #if 或 #elif 语句之后使用。 |
#line | 修改编译器的行号,或指定输出错误和警告时的文件名。例如,#line 100 可以修改当前行号为 100。 |
#error | 生成一个编译时错误,允许开发者在代码中指定错误。 |
#warning | 生成一个编译时警告,允许开发者在代码中指定警告。 |
#region | 在 Visual Studio 中指定一个可折叠的代码区域,使代码更易于管理和查看。 |
#endregion | 结束一个 #region 块,标识该代码区域的结束。 |
22、正则表达式
类别 | 描述 | 示例模式 | 匹配 |
---|---|---|---|
字符转义 | 在正则表达式中使用反斜杠来转义字符。 | \a , \b , \t | 匹配警告符、退格符、制表符等特定字符。 |
字符类 | 用于匹配字符集中的任意一个字符。 | [a-z] | 匹配任何小写字母。 |
定位点 | 零宽度断言,指示匹配应位于特定位置。 | ^ , $ | 匹配字符串的开头或结尾。 |
分组构造 | 用来创建子表达式,通常用于捕获子字符串。 | (abc) | 匹配 "abc" 并捕获该部分。 |
限定符 | 用于指定一个元素的出现次数。 | * , + , ? | 匹配字符零次或多次,至少一次,或零次一次。 |
反向引用构造 | 用于引用之前捕获的子表达式。 | \1 , \k<name> | 匹配捕获组的内容。 |
备用构造 | 用于启用“或”条件匹配。 | `a | b` |
替换 | 替换匹配到的字符串。 | $1 , $& | 替换捕获组内容或整个匹配内容。 |
杂项构造 | 其他类型的构造,包括注释、模式选项等。 | (?x) | 在模式中启用或禁用选项。 |
Regex 类方法 | 用于在代码中实现正则表达式匹配、替换、分割等操作。 | IsMatch() , Replace() | 检查字符串匹配、替换字符串、分割字符串等操作。 |
23、异常处理
(1)处理机制
关键词 | 描述 |
---|---|
try | 标识一段可能抛出异常的代码。后跟一个或多个 catch 块。 |
catch | 捕获异常并处理,通常会使用异常类型作为条件。 |
finally | 无论是否发生异常,都会执行的代码块,通常用于清理资源。 |
throw | 用于显式抛出异常。可以在 catch 块内重新抛出当前异常。 |
(2)异常类
异常类 | 描述 |
---|---|
System.IO.IOException | 处理 I/O 错误。 |
System.IndexOutOfRangeException | 处理数组索引越界错误。 |
System.ArrayTypeMismatchException | 处理数组类型不匹配错误。 |
System.NullReferenceException | 处理空对象引用错误。 |
System.DivideByZeroException | 处理除以零错误。 |
System.InvalidCastException | 处理类型转换错误。 |
System.OutOfMemoryException | 处理内存不足错误。 |
System.StackOverflowException | 处理栈溢出错误。 |
24、文件的输入输出
(1)文件相关类
I/O 类 | 描述 |
---|---|
BinaryReader | 从二进制流读取原始数据。 |
BinaryWriter | 以二进制格式写入原始数据。 |
BufferedStream | 提供字节流的临时存储。 |
Directory | 用于操作目录结构。 |
DirectoryInfo | 用于对目录执行操作。 |
DriveInfo | 提供驱动器的信息。 |
File | 用于处理文件(例如:创建、删除、打开文件)。 |
FileInfo | 用于对文件执行操作(例如:获取文件信息、修改文件)。 |
FileStream | 用于文件中任何位置的读写。 |
MemoryStream | 用于随机访问存储在内存中的数据流。 |
Path | 对路径信息执行操作。 |
StreamReader | 用于从字节流中读取字符。 |
StreamWriter | 用于向一个流中写入字符。 |
StringReader | 用于读取字符串缓冲区。 |
StringWriter | 用于写入字符串缓冲区。 |
(2) FileStream类
参数 | 描述 |
---|---|
FileMode | 定义了打开文件的方式(例如:Create 、Open 等)。 |
FileAccess | 定义对文件的访问类型(例如:Read 、Write 、ReadWrite )。 |
FileShare | 定义文件访问的共享模式(例如:None 、Read 、ReadWrite )。 |
(3) FileMode类
枚举值 | 描述 |
---|---|
Append | 打开文件并将光标放在文件末尾,若文件不存在则创建。 |
Create | 创建一个新文件,若文件已存在,则覆盖文件。 |
CreateNew | 创建新文件,若文件已存在则抛出异常。 |
Open | 打开现有文件,若文件不存在则抛出异常。 |
OpenOrCreate | 打开现有文件,若文件不存在则创建新文件。 |
Truncate | 打开现有文件并截断为零字节大小。若文件不存在则抛出异常。 |
(4) FileAccess类
枚举值 | 描述 |
---|---|
Read | 仅允许读取文件。 |
Write | 仅允许写入文件。 |
ReadWrite | 允许同时读取和写入文件。 |
(5) FileShare类
枚举值 | 描述 |
---|---|
None | 不允许任何进程共享文件。 |
Read | 允许其他进程以只读方式访问文件。 |
Write | 允许其他进程以写方式访问文件。 |
ReadWrite | 允许其他进程以读写方式访问文件。 |
Delete | 允许其他进程删除文件。 |
25、特性
特性名称 | 描述 | 语法示例 |
---|---|---|
规定特性 | 用于为程序元素(如类、方法等)提供元数据。包括位置参数和命名参数。 | [attribute(positional_parameters, name_parameter = value, ...)] |
AttributeUsage | 描述如何使用自定义特性,指定特性可以应用的目标。 | `[AttributeUsage(AttributeTargets.Class |
Conditional | 用于标记条件方法,只有在指定的条件下才会执行。 | [Conditional("DEBUG")] |
Obsolete | 标记已过时的方法或类,编译器会生成警告或错误提示。 | [Obsolete("Don't use OldMethod, use NewMethod instead", true)] |
自定义特性声明 | 自定义特性类必须派生自System.Attribute 。 | `[AttributeUsage(AttributeTargets.Class |
自定义特性构建 | 通过构造函数传递必需的参数,同时可以设置可选参数来扩展特性的信息。 | public class DeBugInfo : System.Attribute { public DeBugInfo(int bugNo, string developer, string lastReview) { ... } } |
自定义特性应用 | 使用自定义特性时,必须将其放置在目标元素的前面。 | [DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")] class Rectangle { ... } |
26、反射
内容 | 描述 |
---|---|
定义 | 反射是程序访问、检测和修改自身状态或行为的能力。 |
组成 | 程序集包含模块,模块包含类型,类型包含成员。反射提供了封装程序集、模块和类型的对象。 |
作用 | - 动态创建类型实例 - 绑定类型到现有对象 - 获取类型 - 调用类型方法,访问字段和属性 |
命名空间 | System.Reflection |
优点 | - 提高程序灵活性和扩展性 - 降低耦合性,提高自适应能力 - 允许程序创建和控制任何类的对象,无需硬编码目标类 |
缺点 | - 性能问题:反射比直接代码慢 - 反射模糊程序内部逻辑,增加维护难度 - 反射代码较复杂,比直接代码难以理解 |
用途 | - 查看属性信息 - 审查集合中的类型,实例化这些类型 - 延迟绑定方法和属性 - 运行时创建新类型并执行任务 |
查看元数据 | 使用System.Reflection.MemberInfo 对象,初始化并发现类相关属性(attribute)。 |
示例 | 自定义属性示例: 通过反射读取类 MyClass 的自定义属性HelpAttribute 。示例代码展示了如何通过 GetCustomAttributes() 方法访问和输出附加的属性信息。 |
实例化对象 | 通过反射读取和输出类Rectangle 及其成员的方法和属性的元数据,输出调试信息。例如,DeBugInfo 自定义属性可用于跟踪类成员及方法的调试信息。 |
27、属性
概念 | 描述 |
---|---|
属性 (Property) | 属性是类、结构和接口的命名成员,用来扩展域 (Field),通过访问器 (accessor) 让私有域的值可被读写或操作。 |
域 (Field) | 域是类或结构中的成员变量或方法。 |
访问器 (Accessors) | 访问器包含有助于获取(读取或计算)或设置(写入)属性的可执行语句,通常包括 get 和 set 访问器,或仅一个访问器。 |
属性实例 | 属性通过定义 get 和 set 访问器来访问私有域。例如:public string Code { get { return code; } set { code = value; } } |
抽象属性 (Abstract Properties) | 抽象类可以拥有抽象属性,这些属性需要在派生类中实现。例如,public abstract string Name { get; set; } |
简化版抽象属性 (C# 6.0) | 使用简化的语法定义属性和自动实现的属性。例如:public string Code { get; set; } = "N.A"; |
属性用法示例 | 在实例中,创建一个 Student 对象并通过属性访问和设置值,最终输出学生信息。例如:Student Info: Code = 001, Name = Zara, Age = 9 |
抽象类示例 | 抽象类定义抽象属性,派生类实现这些属性,并使用访问器来设置和获取值。 |
C# 6.0 新特性 | 使用简化的语法和自动属性初始化器,简化代码并提高可读性。 |
28、索引器
概念 | 描述 |
---|---|
索引器 (Indexer) | 索引器允许对象像数组一样被索引,通过 this 关键字定义,在类中允许使用数组访问运算符 [ ] 来访问对象实例的特定部分。 |
索引器语法 | 一维索引器的语法为: element-type this[int index] { get { } set { } } 。其中 get 和 set 用于获取或设置指定索引的值。 |
索引器用途 | 索引器类似于属性,但不需要名称,使用 this 关键字来访问。它通过 get 和 set 访问器来定义,允许获取或设置对象实例中的特定值。 |
索引器示例 | this[int index] 用于访问 namelist 数组中的元素。例如:names[0] = "Zara" 设置值,names[0] 获取值。 |
索引器重载 | 索引器可以被重载,允许使用不同类型的参数。例如,使用 int 类型和 string 类型的索引器,可以分别通过索引访问和按名称查找元素。 |
重载索引器示例 | 在同一类中定义两个索引器:一个使用 int 类型来设置和获取 namelist 数组中的元素,另一个使用 string 类型来查找数组中指定名称的元素的索引。例如:public int this[string name] { get { } } |
索引器应用 | 索引器使得对象能够表现得像数组一样,通过数组的方式访问类中的成员数据。通过重载索引器,类可以处理不同类型的访问方式,提供灵活的操作接口。 |
示例结果 | 执行代码时,输出索引器访问的内容。例如:Zara, Riz, Nuha, ... 和 2 表示 "Nuha" 的索引位置。 |
29、委托
概念 | 描述 |
---|---|
委托 (Delegate) | 委托是一个引用类型变量,存储对某个方法的引用,允许动态地在运行时改变引用。它类似于 C 或 C++ 中的函数指针,常用于事件和回调方法的实现。 |
声明委托 | 委托声明决定了可由该委托引用的方法。语法:delegate <return type> <delegate-name> <parameter list> ,例如:public delegate int MyDelegate(string s); |
实例化委托 | 声明委托类型后,使用 new 关键字实例化委托,并将其与特定的方法关联。示例:printString ps1 = new printString(WriteToScreen); |
委托实例化示例 | 示例代码中,委托 NumberChanger 引用了两个方法 AddNum 和 MultNum ,并通过委托对象调用这些方法。结果展示了 num 的值发生了变化。 |
委托的多播 | 委托对象可以使用 + 运算符合并,调用时会依次执行所有合并的委托。可以使用 - 运算符从合并的委托中移除某个委托。多播委托实现了一个方法调用列表。 |
多播示例 | 示例中,使用 nc += nc2 合并两个委托,调用时依次执行 AddNum 和 MultNum ,输出 num 的变化。结果显示 num 从 10 变为 75。 |
委托用途 | 委托可用于在运行时动态调用方法,常用于事件和回调机制。示例代码展示了如何使用 printString 委托分别将字符串打印到控制台和写入文件。 |
委托作为参数 | 委托可以作为参数传递给其他方法,在方法中使用该委托调用指定的目标方法。示例代码中,sendString 方法接受委托 printString ,并调用传递给它的具体方法(打印到控制台或写入文件)。 |
委托示例输出 | 执行委托时,输出控制台信息:“The String is: Hello World”。 |
30、事件
概念 | 描述 |
---|---|
事件 (Event) | 事件是程序对某些用户操作或系统生成的通知的响应。常见的事件包括按键、鼠标点击或移动。事件用于进程间通信,并要求应用程序在事件发生时做出响应。 |
事件与委托 | 事件通过委托来处理。委托用于将事件处理程序与事件关联,通常使用发布-订阅模型(Publisher-Subscriber),其中发布器类发布事件,订阅器类响应事件。 |
发布器 (Publisher) | 发布器类包含事件和委托定义,并负责触发事件,通知订阅器类。 |
订阅器 (Subscriber) | 订阅器类响应事件,提供事件处理程序来处理事件。 |
声明事件 | 在类中声明事件之前,首先声明事件的委托类型。事件使用 event 关键字声明,示例:public event BoilerLogHandler BoilerEventLog; |
实例 1:事件的声明与使用 | 示例中,EventTest 类定义了 NumManipulationHandler 委托和 ChangeNum 事件。在 SetValue 方法中触发事件,如果值发生变化则调用 OnNumChanged 方法触发事件。 |
实例 2:锅炉事件日志 | 通过事件发布器类 DelegateBoilerEvent 和事件 BoilerEventLog ,结合委托 BoilerLogHandler 处理锅炉的状态。事件处理程序会记录温度和压力信息,并将日志保存到文件和输出到控制台。 |
事件触发 | 事件通过发布器调用触发方法 OnBoilerEventLog ,检查事件是否已被订阅,如果订阅了事件,则调用所有订阅的处理方法(如日志记录器)。 |
事件订阅 | 订阅器类通过 += 运算符订阅事件,例如 boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger); ,该方法被调用时会执行相应的处理。 |
事件输出示例 | 执行事件时,输出以下信息:Logging Info: Temperature 100 Pressure 12 Message: O.K ,并且日志写入文件 boiler.txt 。 |
31、集合
类名 | 描述和用法 |
---|---|
动态数组 (ArrayList) | 代表一个可以单独索引的有序集合,类似于数组,但大小动态调整,支持在任意位置添加、移除项目,提供动态内存分配、搜索和排序功能。 |
哈希表 (Hashtable) | 使用键来访问集合中的元素,键值对存储,每个元素都有一个键用于访问集合中的项目。 |
排序列表 (SortedList) | 结合了数组和哈希表的特性,支持通过键或索引访问项目。列表按键值排序,当使用索引访问时类似动态数组,当使用键访问时类似哈希表。 |
堆栈 (Stack) | 代表一个后进先出的对象集合,支持推入(添加元素)和弹出(移除元素)操作。 |
队列 (Queue) | 代表一个先进先出的对象集合,支持入队(添加元素)和出队(移除元素)操作。 |
点阵列 (BitArray) | 使用 1 和 0 表示的二进制数组,用于存储位数据,适用于位数未知的情况,支持通过整型索引访问每一位,索引从零开始。 |
32、泛型
概念/特性 | 描述 |
---|---|
泛型(Generic) | 允许延迟指定类或方法中的数据类型,直到实际使用时。它使得类或方法可以与任何数据类型一起工作,提高代码重用性和类型安全性。 |
泛型类实例 | 通过数据类型的替代参数编写类的规范,在编译时根据类型生成代码。例如,MyGenericArray<T> 类使用泛型 T 来处理不同的数据类型。 |
泛型的特性 | 1. 最大化代码重用。 2. 提高类型安全性。 3. 提升性能。 4. 创建泛型集合类。 5. 创建自定义泛型接口、类、方法、事件和委托。 6. 可以对泛型类进行约束以访问特定数据类型的方法。 |
泛型方法实例 | 泛型方法允许使用类型参数声明方法,例如 Swap<T> 方法可以交换任意类型的两个变量的值。 |
泛型委托 | 泛型委托允许使用类型参数定义委托。例如,NumberChanger<T> 委托可以用来定义处理不同类型数据的操作。实例中展示了使用泛型委托来实现对变量的操作。 |
示例代码输出 | - 泛型数组:输出整型数组 0 5 10 15 20 和字符数组 a b c d e 。- 泛型方法:交换整数和字符的值,输出交换前后的结果。 - 泛型委托:输出 Value of Num: 35 和 Value of Num: 175 。 |
33、匿名方法
(1)将委托加入对比
概念/特性 | 描述 |
---|---|
委托(Delegate) | 委托用于引用具有相同签名的方法,允许通过委托对象调用被引用的方法。 |
匿名方法(Anonymous Methods) | 匿名方法没有名称,只有方法体。它提供了一种传递代码块作为委托参数的技术,返回类型是通过方法主体的 return 语句推断出来的。 |
匿名方法语法 | 通过 delegate 关键字创建委托实例并指定代码块。例如:NumberChanger nc = delegate(int x){ Console.WriteLine("Anonymous Method: {0}", x); }; |
委托调用方式 | 委托可以通过匿名方法调用,也可以通过命名方法调用。例如,委托实例化后,可以使用 nc(10) 来调用匿名方法或命名方法。 |
示例代码 | 1. 使用匿名方法创建委托实例并调用:nc(10) 输出 Anonymous Method: 10 。2. 使用命名方法实例化委托并调用: nc(5) 输出 Named Method: 15 ,nc(2) 输出 Named Method: 30 。 |
34、不安全代码
概念/特性 | 描述 |
---|---|
unsafe修饰符 | unsafe 修饰符标记的代码块允许使用指针变量,表示使用了指针的非托管代码。 |
指针变量 | 指针是存储另一个变量地址的变量。声明格式:type *var-name; 。例如,int* p; 表示指向整数的指针。 |
指针声明示例 | - int* p; :p 是指向整数的指针。- double* p; :p 是指向双精度数的指针。- int** p; :p 是指向整数指针的指针。 |
指针声明规则 | 多个指针声明时,星号 * 只与基础类型一起声明,不能为每个指针单独加星号,例如:int* p1, p2, p3; 是正确的。 |
指针使用示例 | - int var = 20; - int* p = &var; 示例输出: Data is: 20 , Address is: 99215364 |
使用ToString检索数据值 | 使用 ToString() 方法可以检索指针引用位置的数据。例如:p->ToString() 会输出指针所指向的数据。示例输出:Data is: 20 , Data is: 20 , Address is: 77128984 |
传递指针作为方法参数 | 可以向方法传递指针作为参数。例如:交换两个整数的指针。示例输出:Before Swap: var1: 10, var2: 20 , After Swap: var1: 20, var2: 10 |
使用指针访问数组元素 | 使用指针访问数组时,需使用 fixed 关键字固定指针地址。例如:fixed(int *ptr = list) ,然后通过指针访问数组元素。输出显示数组地址和值。 |
编译不安全代码 | 编译包含不安全代码时需使用 /unsafe 标志。例如:csc /unsafe prog1.cs 。在 Visual Studio 中,需要在项目属性中启用不安全代码。 |
35、多线程
类别 | 描述 |
---|---|
线程定义 | 线程是程序的执行路径,具有独特的控制流。线程可以帮助应用程序执行复杂、耗时的操作。线程是轻量级进程,在现代操作系统中用于并行编程,节省 CPU 周期,提高应用程序效率。 |
线程生命周期 | 1. 未启动状态:线程实例已创建,但未调用 Start 方法。 2. 就绪状态:线程准备运行,等待 CPU 周期。 3. 不可运行状态:例如调用 Sleep、Wait 或 I/O 操作阻塞。 4. 死亡状态:线程执行完毕或已中止。 |
主线程 | 进程中第一个执行的线程,自动创建并执行。通过 Thread.CurrentThread 属性可访问主线程。 |
常用属性 | 1. CurrentThread:获取当前线程。 2. IsAlive:指示线程是否处于活动状态。 3. IsBackground:指示线程是否为后台线程。 4. ThreadState:获取线程的状态。 |
常用方法 | 1. Abort():终止线程。 2. Join():阻塞调用线程,直到某个线程结束。 3. Sleep():让线程暂停一段时间。 4. Start():启动线程执行。 5. Interrupt():中断线程。 |
创建线程 | 通过继承 Thread 类并调用 Start() 方法创建线程。例如:Thread childThread = new Thread(childref); |
管理线程 | 1. Sleep():暂停线程一段时间。 2. Interrupt():中断等待状态的线程。 |
销毁线程 | 通过 Abort() 方法中止线程,抛出 ThreadAbortException 异常。此异常无法捕获,控制会转移到 finally 块。 |
36、字典
功能 | 描述 |
---|---|
定义 | Dictionary 是一个从一组键(Key)到一组值(Value)的映射结构,包含在System.Collections.Generic 命名空间中。 |
键的要求 | 键必须是唯一的,且不能为空引用(null)。如果值为引用类型,则可以为空值。 |
键和值类型 | Key 和 Value 可以是任何类型(如 string , int , 自定义类等)。 |
常用方法与属性 | |
创建和初始化 | Dictionary<int, string> myDictionary = new Dictionary<int, string>(); |
添加元素 | myDictionary.Add(1, "C#"); |
通过Key查找元素 | if(myDictionary.ContainsKey(1)) { Console.WriteLine("Key:{0}, Value:{1}", "1", myDictionary[1]); } |
通过KeyValuePair遍历元素 | foreach(KeyValuePair<int, string> kvp in myDictionary) { Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value); } |
仅遍历键 | Dictionary<int, string>.KeyCollection keyCol = myDictionary.Keys; foreach (int key in keyCol) { Console.WriteLine("Key = {0}", key); } |
仅遍历值 | Dictionary<int, string>.ValueCollection valueCol = myDictionary.Values; foreach (string value in valueCol) { Console.WriteLine("Value = {0}", value); } |
移除指定键值 | myDictionary.Remove(1); if(!myDictionary.ContainsKey(1)) { Console.WriteLine("不存在 Key : 1"); } |
常见属性和方法 | |
Comparer | 获取用于确定字典中键是否相等的 IEqualityComparer 。 |
Count | 获取字典中键/值对的数量。 |
Item | 获取或设置与指定键相关联的值。 |
Keys | 获取字典中所有键的集合。 |
Values | 获取字典中所有值的集合。 |
Add | 将指定的键和值添加到字典中。 |
Clear | 从字典中移除所有键和值。 |
ContainsKey | 检查字典是否包含指定的键。 |
ContainsValue | 检查字典是否包含指定的值。 |
GetEnumerator | 返回用于遍历字典的枚举器。 |
GetType | 获取字典实例的类型。 |
Remove | 从字典中移除指定键的值。 |
ToString | 返回字典的字符串表示。 |
TryGetValue | 获取与指定键关联的值。 |
四、实操
1、Hello World
/*
* 该文件是一个简单的C#控制台应用程序,主要用于演示如何在控制台输出“Hello World”。
* 程序使用了System命名空间下的基本功能来进行输出操作,并等待用户输入以结束程序。
*
* 文件包含以下部分:
* - 引用必要的命名空间
* - 程序入口Main方法
* - 控制台输出功能
*/
// 导入C#中常用的系统库,用于提供基本的功能
using System; // 提供基本的类和基类支持,例如Console类
using System.Collections.Generic; // 提供泛型集合的接口和类
using System.Linq; // 提供对集合进行查询操作的功能(LINQ)
using System.Text; // 提供对文本处理和编码转换的功能
using System.Threading.Tasks; // 提供对异步编程和任务并行的支持
// 定义命名空间,用于组织代码和避免名称冲突
namespace ConsoleApp1 // 定义项目的命名空间,命名为ConsoleApp1
{
// 定义一个类,类是C#中代码的基本结构单元
class Program // 定义Program类,通常是程序的入口点
{
// 定义Main方法,是程序执行的入口
static void Main(string[] args) // Main方法,程序从这里开始运行
{
// 使用Console类输出一行文本到控制台
Console.WriteLine("Hello World"); // 输出"Hello World"到控制台
// 等待用户按下回车键,以便查看控制台输出
Console.ReadLine(); // 阻止程序立即关闭,等待用户输入
}
}
}
(1)入口函数
Main()是默认的入口函数,可以被修改,但我不告诉你怎么修改。
我想说的是他的特殊性,通常来说,一个静态的函数,都是需要现行调用的,不能直接运行。但入口函数不一样,运行程序会被直接调用,不需要显性调用。
入口函数有且只有一个。
2、模块化
(1)创建文件夹与cs文件
双击 .sln
文件(解决方案文件)来打开解决方案或项目 》 右键解决方案名称添加文件夹 》 右键文件夹添加类
(2)修改cs文件名,类名可自定义
(3)命名空间
命名空间也可以自定义,如可以跟其他文件的命名空间名称一样,或者写其他字符。
规范1:所有文件中的命名空间都跟项目名称一致,这样在导入不同模块时,是需要使用同个命名空间即可,适合小型项目。
规范2:命名空间.目录路径, 这种方式适合大型项目,在引用时,需要 using 命名空间.目录路径,这样就可以导入该空间下的类或方法了。
(4)引用定义好的类
在Hello.cs文件中,写入:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class HelloWorld
{
public void PrintMessage()
{
Console.WriteLine("Hello World");
}
}
}
在Program.cs中写入:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApp1; // 使用命名空间
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
// 创建 HelloWorld 类的实例
HelloWorld helloWorld = new HelloWorld();
// 调用 PrintMessage 方法
helloWorld.PrintMessage();
Console.ReadLine();
}
}
}
可正常运行:
3、实现MC服务器
(1)下载第三方应用程序
Download - 胡工科技官网 (hsltechnology.cn)
(2)加载应用程序的dll库
解压安装包》找到以下库
使用VS添加库,详见本文:开发环境》加载库
(3)查看接口文档
MelsecMcServer 类 (hslcommunication.cn)
(4)编程
实现功能:启动MC服务器,往D0寄存器写入“AA”,100ms后往D0寄存器写入“00”,接着读取D10数据,5秒后重复该动作。
新增McServer.cs,写入以下代码:
using System;
using System.Text;
using System.Timers;
using HslCommunication.Profinet.Melsec;
using TimerThreading = System.Threading.Timeout;
namespace ConsoleApp1
{
class McServer
{
private readonly string writeAddress;
private readonly string writeValue;
private readonly string readAddress;
private MelsecMcServer mcServer;
private Timer writeTimer;
private int elapsedTime; // 用来记录已延迟的时间
private int delayTime; // 延迟时间
private int existTime; // 指令存在时间
public McServer(string writeAddress, string writeValue, string readAddress)
{
this.writeAddress = writeAddress;
this.writeValue = writeValue;
this.readAddress = readAddress;
this.elapsedTime = 0; // 初始化已延迟时间
}
// 启动服务器时,增加了 exist_time 和 delayTime 参数
public void StartServer(int port, int existTime, int delayTime)
{
try
{
// 初始化并启动 MC 服务器
mcServer = new MelsecMcServer
{
ActiveTimeSpan = TimeSpan.FromHours(24) // 运行1小时
// ActiveTimeSpan = TimerThreading.InfiniteTimeSpan //永久运行,可能不支持
};
mcServer.ServerStart(port);
Console.WriteLine($"MC 3E Frame Server started on port {port}.");
// 保存延迟时间
this.delayTime = delayTime;
this.existTime = existTime;
// 初始化定时器,使用 exist_time 控制定时器触发的间隔
writeTimer = new Timer(delayTime);
writeTimer.Elapsed += WriteTimer_Elapsed; // 每次定时器触发时执行写入操作
writeTimer.AutoReset = true;
writeTimer.Start();
Console.WriteLine("Write timer started.");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start server: {ex.Message}");
}
}
// 停止服务器
public void StopServer()
{
try
{
writeTimer.Stop();
mcServer.ServerClose();
Console.WriteLine("Server and timer stopped.");
}
catch (Exception ex)
{
Console.WriteLine($"Error stopping server: {ex.Message}");
}
}
// 写入并延迟修改寄存器的操作
private void WriteTimer_Elapsed(object sender, ElapsedEventArgs e)
{
try
{
// 每次定时器触发,增加已延迟时间
elapsedTime += (int)writeTimer.Interval;
// 写入AA到寄存器
mcServer.Write(writeAddress, Encoding.ASCII.GetBytes(writeValue));
Console.WriteLine($"Wrote '{writeValue}' to {writeAddress}");
// 延时
System.Threading.Thread.Sleep(existTime);
// 延迟时间(delayTime)后修改寄存器为00
mcServer.Write(writeAddress, Encoding.ASCII.GetBytes("00"));
Console.WriteLine($"Changed {writeAddress} to '00'");
// 读取寄存器值并打印调试信息
var readResult = mcServer.Read(readAddress, (ushort)writeValue.Length);
if (readResult.IsSuccess)
{
string readValue = Encoding.ASCII.GetString(readResult.Content);
Console.WriteLine($"Successfully read '{readValue}' from {readAddress}");
}
else
{
Console.WriteLine($"Failed to read from {readAddress}: {readResult.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in WriteTimer_Elapsed: {ex.Message}");
}
}
}
}
在Program.cs文件中写入
using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;
namespace ConsoleApp1
{
class Program
{
static McServer mcServer; // MC 服务器实例
static void Main(string[] args)
{
// 打印欢迎信息
HelloWorld helloWorld = new HelloWorld();
helloWorld.PrintMessage();
// 初始化并启动 3E 帧 MC 服务器
mcServer = new McServer("D0", "AA", "D10");
mcServer.StartServer(9600, 100, 5000);
Console.WriteLine("Press Enter to exit...");
// 等待用户按下 Enter 键退出
Console.ReadLine();
// 停止服务器和定时器
mcServer.StopServer();
}
}
}
(5)运行
(6)命名空间那些事儿
对于内置模块,可以不使用using关键字引入,可直接通过完整路径使用,如上述代码System.Threading.Thread.Sleep(1000);
对于同个命名空间,可以不使用using关键字引入,可直接在该命名空间下使用属于该命名空间的类和方法,无论他在哪里,如上述代码 static McServer mcServer;
对于命名空间别名,用法如using TimerThreading = System.Threading.Timeout; 即TimerThreading为System.Threading.Timeout的别名,用于解决不同模块的Timeout的同名冲突。
(7)构造函数
class McServer的构造函数是public McServer,即如果你想给这个class McServer类传参,需要构造函数public McServer,它的名字必须和类名相同,且没有返回类型(连 void
也没有)。
(8)static关键字
static定义了一个静态变量,因为Main的方法是static,只能访问同样是 static
的类成员。原因是 static
方法属于类,而非某个对象实例,因此它不能访问非静态成员(需要实例来调用)。我们需要将 mcServer
声明为 static
,以便与 Main
方法在同一静态上下文中使用。如果为非静态实例,将会出现以下报错:
4、继承
问大家一个问题,爸爸的爸爸叫什么?当你很快速的回答这个问题后,你就发现,你有了继承的意识。
新增McServer_inherit.cs文件写入:
namespace ConsoleApp1
{
class McServer_Base : McServer // 基本继承
{
public McServer_Base(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress)
{
}
}
class McServer_Override : McServer // 方法重写
{
public McServer_Override(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress)
{
}
public override void StartServer(int port, int existTime, int delayTime)
{
System.Console.WriteLine("Override StartServer");
base.StartServer(port, existTime, delayTime);
}
}
class McServer_New : McServer // 方法隐藏
{
public McServer_New(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress)
{
}
public new void StartServer(int port, int existTime, int delayTime)
{
System.Console.WriteLine("New StartServer");
base.StartServer(port, existTime, delayTime);
}
}
class McServer_Constructor : McServer // 构造函数继承
{
public McServer_Constructor(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress)
{
System.Console.WriteLine("McServer_Constructor initialized");
}
}
abstract class AbstractServer
{
public abstract void StartServer(int port, int existTime, int delayTime);
public abstract void StopServer();
}
class McServer_Abstract : AbstractServer // 抽象类继承
{
private McServer mcServer;
public McServer_Abstract(string writeAddress, string writeValue, string readAddress)
{
mcServer = new McServer(writeAddress, writeValue, readAddress);
}
public override void StartServer(int port, int existTime, int delayTime)
{
mcServer.StartServer(port, existTime, delayTime);
}
public override void StopServer()
{
mcServer.StopServer();
}
}
interface IServer
{
void StartServer(int port, int existTime, int delayTime);
void StopServer();
}
class McServer_Interface : IServer // 接口继承
{
private McServer mcServer;
public McServer_Interface(string writeAddress, string writeValue, string readAddress)
{
mcServer = new McServer(writeAddress, writeValue, readAddress);
}
public void StartServer(int port, int existTime, int delayTime)
{
mcServer.StartServer(port, existTime, delayTime);
}
public void StopServer()
{
mcServer.StopServer();
}
}
class McServer_Parent: McServer
{
public McServer_Parent(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress) // 调用父类的构造函数
{
System.Console.WriteLine("McServer_Parent constructor called.");
}
public override void StartServer(int port, int existTime, int delayTime)
{
System.Console.WriteLine("Starting server in McServer_Parent");
base.StartServer(port, existTime, delayTime);
}
}
class McServer_MultiLevel : McServer_Parent // 多层继承
{
public McServer_MultiLevel(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress) // 调用父类的构造函数
{
System.Console.WriteLine("McServer_MultiLevel constructor called.");
}
}
sealed class McServer_Sealed // 防止继承
{
private readonly McServer mcServer;
public McServer_Sealed(string writeAddress, string writeValue, string readAddress)
{
mcServer = new McServer(writeAddress, writeValue, readAddress);
}
public void StartServer(int port, int existTime, int delayTime)
{
mcServer.StartServer(port, existTime, delayTime);
}
public void StopServer()
{
mcServer.StopServer();
}
}
}
在Program.cs文件中依次调用
using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;
namespace ConsoleApp1
{
class Program
{
// static McServer mcServer; // MC 服务器实例
// static McServer_Base mcServer; // 0基本继承
// static McServer_Override mcServer; // 1方法重写
// static McServer_New mcServer; // 2方法隐藏
// static McServer_Constructor mcServer; // 3构造函数继承
// static McServer_Abstract mcServer; // 4抽象类继承
// static McServer_Interface mcServer; // 5接口继承
// static McServer_MultiLevel mcServer; // 6多层继承
static McServer_Sealed mcServer; // 7防止继承
static void Main(string[] args)
{
// 打印欢迎信息
HelloWorld helloWorld = new HelloWorld();
helloWorld.PrintMessage();
// 初始化并启动 3E 帧 MC 服务器
//mcServer = new McServer("D0", "AA", "D10");
//mcServer = new McServer_Base("D0", "AA", "D10"); // 0基本继承
//mcServer = new McServer_Override("D0", "AA", "D10"); // 1方法重写
//mcServer = new McServer_New("D0", "AA", "D10"); // 2方法隐藏
//mcServer = new McServer_Constructor("D0", "AA", "D10"); // 3构造函数继承
//mcServer = new McServer_Abstract("D0", "AA", "D10"); // 4抽象类继承
//mcServer = new McServer_Interface("D0", "AA", "D10"); // 5接口继承
//mcServer = new McServer_MultiLevel("D0", "AA", "D10"); // 6多层继承
mcServer = new McServer_Sealed("D0", "AA", "D10"); // 7防止继承
mcServer.StartServer(9600, 100, 5000);
Console.WriteLine("Press Enter to exit...");
// 等待用户按下 Enter 键退出
Console.ReadLine();
// 停止服务器和定时器
mcServer.StopServer();
}
}
}
需要改造一下父类方法才能重写方法,在McServer.cs文件中:
将
public void StartServer(int port, int existTime, int delayTime)
改为
public virtual void StartServer(int port, int existTime, int delayTime)
(1)继承的注意事项
当父类有构造函数时,需要继承构造函数,如:
public McServer_Base(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress)
{
}
当父类方法被重写时,如果还想继续使用父类方法,需要显性调用,如:
base.StartServer(port, existTime, delayTime);
当父类方法被重写,而且多个子类都在继续使用父类方法,那么会被调用多次,但父类方法会被实例化一次,如多层继承的:
base.StartServer(port, existTime, delayTime);
方法隐藏跟重写方法有些相似。方法隐藏是指子类定义了与父类同名的方法,但没有使用 override
关键字,而是使用 new
关键字显式地隐藏父类中的方法。在这种情况下,子类的方法会隐藏父类的方法,而不是重写父类的方法。起到一个效果:即使父类的方法不是 virtual
,子类依然可以使用 new
关键字来隐藏父类的方法。这种情况下,隐藏的是父类的方法,而不是重写父类的方法。
构造函数继承即在继承父类构造函数后,可添加一些行为。
对于抽象类继承,可以强制子类实现抽象方法: 抽象类通过定义抽象方法,强制继承该类的子类必须提供这些方法的实现。这样可以确保所有子类都遵循相同的接口规范。可以统一接口: 通过抽象类,父类提供了一种统一的接口,子类可以根据自己的需求实现这些接口。这样可以让代码结构更加清晰,并且容易扩展。可以提供共用的代码: 抽象类除了抽象方法,还可以包含普通的已实现方法。在这种情况下,子类继承抽象类不仅可以实现抽象方法,还可以直接使用父类中已经实现的方法,从而减少代码重复。这得用代码来解释:
// 抽象类,定义了必须由子类实现的抽象方法
abstract class AbstractServer
{
// 抽象方法,子类必须重写此方法并提供具体实现
public abstract void StartServer(int port, int existTime, int delayTime);
public abstract void StopServer();
}
// 具体的子类,继承自抽象类并实现其抽象方法
class McServer_Abstract : AbstractServer // 抽象类继承
{
private McServer mcServer;
// 构造函数:子类在构造时实例化 McServer 对象
public McServer_Abstract(string writeAddress, string writeValue, string readAddress)
{
mcServer = new McServer(writeAddress, writeValue, readAddress);
}
// 重写抽象方法 StartServer,提供具体实现
public override void StartServer(int port, int existTime, int delayTime)
{
mcServer.StartServer(port, existTime, delayTime); // 调用 McServer 类的方法
}
// 重写抽象方法 StopServer,提供具体实现
public override void StopServer()
{
mcServer.StopServer(); // 调用 McServer 类的方法
}
}
接口继承和抽象类继承实在太像了,直接上对比:
特性 | 接口(Interface) | 抽象类(Abstract Class) |
---|---|---|
继承方式 | 可以被多个类实现(支持多重继承) | 只能单继承一个抽象类 |
方法实现 | 只能声明方法,不能提供实现,除非是 C# 8.0 后的默认实现 | 可以声明抽象方法,也可以提供方法实现 |
成员限制 | 只能包含方法签名、属性、事件、索引器等声明 | 可以包含方法、字段、属性、事件、构造函数等 |
访问修饰符 | 所有成员默认为 public,不能指定其他访问修饰符 | 可以使用任何访问修饰符(private、protected、public) |
构造函数 | 不能有构造函数 | 可以有构造函数 |
字段 | 不能包含字段 | 可以包含字段 |
适用场景 | 用于定义行为规范,支持不同类的统一接口,适合松耦合设计 | 用于共享部分实现和公共基础设施,适合有共性的类 |
C#没有直接实现多重继承的方式,但可由接口继承来实现。PS:多重继承 是指一个类可以同时继承多个父类的特性和行为。这意味着子类可以从多个父类继承方法、属性和其他成员,允许类从多个来源组合其功能。这种继承方式是面向对象编程中的一种常见特性,但并非所有编程语言都支持多重继承。
防止继承表明这个类,不会被其他类继承。
5、实现TCPclient
新增TCP.cs
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ConsoleApp1
{
public class TcpClientWrapper : IDisposable
{
private TcpClient client;
private NetworkStream stream;
private bool disposed = false; // 用于标记是否已释放资源
private readonly ManualResetEvent connectDone = new ManualResetEvent(false); // 用于实现连接超时
private readonly int connectTimeout = 5000; // 连接超时时间(毫秒)
// 初始化TCP客户端并连接到服务器。
public TcpClientWrapper(string serverIP, int serverPort)
{
try
{
client = new TcpClient
{
ReceiveTimeout = 30000, // 接收超时时间(30秒)
SendTimeout = 30000, // 发送超时时间(30秒)
NoDelay = true // 禁用Nagle算法,减少延迟
};
Console.WriteLine("正在连接到服务器...");
var asyncResult = client.BeginConnect(serverIP, serverPort, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(connectTimeout))
{
throw new TimeoutException("连接服务器超时!");
}
client.EndConnect(asyncResult); // 完成连接
stream = client.GetStream();
Console.WriteLine("已连接到服务器,长连接已建立!");
}
catch (SocketException ex)
{
Console.WriteLine($"连接服务器失败: {ex.Message}");
Dispose();
throw;
}
catch (TimeoutException ex)
{
Console.WriteLine($"连接超时: {ex.Message}");
Dispose();
throw;
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
Dispose();
throw;
}
}
// 发送消息到服务器。
public void Send(string message)
{
if (disposed)
{
throw new ObjectDisposedException(nameof(TcpClientWrapper));
}
if (stream == null || !client.Connected)
{
throw new InvalidOperationException("尚未连接到服务器。");
}
try
{
byte[] data = Encoding.UTF8.GetBytes(message);
stream.Write(data, 0, data.Length);
Console.WriteLine($"发送消息: {message}");
}
catch (Exception ex)
{
Console.WriteLine($"发送消息失败: {ex.Message}");
}
}
// 从服务器接收消息。
public string Receive()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(TcpClientWrapper));
}
if (stream == null || !client.Connected)
{
throw new InvalidOperationException("尚未连接到服务器。");
}
try
{
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"接收到消息: {response}");
return response;
}
catch (Exception ex)
{
Console.WriteLine($"接收消息失败: {ex.Message}");
return string.Empty;
}
}
// 关闭连接并释放资源。
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 告诉GC不需要再调用终结器
}
// 释放资源的核心方法
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
if (stream != null)
{
stream.Close();
stream = null;
}
if (client != null)
{
client.Close();
client = null;
}
}
// 如果有非托管资源,可以在这里释放
disposed = true;
Console.WriteLine("已关闭连接并释放资源。");
}
}
// 析构函数,用于释放资源
~TcpClientWrapper()
{
Dispose(false);
}
}
}
更新Program文件:
using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string serverIP = "192.168.66.188";
int serverPort = 5000;
try
{
using (TcpClientWrapper tcpClient = new TcpClientWrapper(serverIP, serverPort))
{
while (true)
{
Console.Write("请输入发送消息 (输入 'exit' 退出): ");
string message = Console.ReadLine();
if (message?.ToLower() == "exit")
{
break;
}
tcpClient.Send(message);
string response = tcpClient.Receive();
Console.WriteLine($"服务器响应: {response}");
}
}
}
catch (TimeoutException ex)
{
Console.WriteLine($"连接超时: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"发生异常: {ex.Message}");
}
}
}
}
(1)using方法
using方法除了引入命名空间,还是可以用于自动释放资源。如:
using (TcpClientWrapper tcpClient = new TcpClientWrapper(serverIP, serverPort))
此时,using
语句的主要目的是:资源管理,即自动调用实现了 IDisposable
接口的类的 Dispose
方法。
这里需要注意:
Dispose
方法的名称是固定的,可以在这个方法里面加入其他资源释放方法。
所引用的类,需要继承IDisposable,即必须实现 IDisposable
接口。
(2)析构函数
析构函数,是 C# 中的一种特殊方法,用于对象销毁时执行清理操作。如:
~TcpClientWrapper()
析构函数在垃圾回收器 (Garbage Collector, GC) 释放对象时被调用,用于释放非托管资源或执行一些清理工作。
(3)内存释放机制对比
特点 | 析构函数 | IDisposable 接口 |
---|---|---|
调用方式 | 自动调用:由垃圾回收器在对象被销毁时自动触发。 | 手动调用:需要显式调用 Dispose 方法,或者使用 using 语句。 |
执行时间确定性 | 不确定性:无法保证析构函数的执行时间,因为垃圾回收的时机不可控。 | 确定性:Dispose 方法立即执行,适合需要确定释放资源的场景。 |
性能成本 | 可能增加垃圾回收器的负担,频繁调用可能对性能产生负面影响。 | 性能友好,资源可以及时释放,不会额外增加垃圾回收的负担。 |
推荐性 | 适合作为资源清理的最后保障机制,不建议单独依赖。 | 微软推荐使用 IDisposable 接口来清理资源,符合 .NET 开发最佳实践。 |
(4)同名方法其实是不同方法
以下是名称相同的方法,但是因签名不一样而不同的方法:
// 关闭连接并释放资源。
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 告诉GC不需要再调用终结器
}
// 释放资源的核心方法
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
if (stream != null)
{
stream.Close();
stream = null;
}
if (client != null)
{
client.Close();
client = null;
}
}
// 如果有非托管资源,可以在这里释放
disposed = true;
Console.WriteLine("已关闭连接并释放资源。");
}
}
在 C# 中,方法的合法性由方法签名(方法名+参数列表)决定。public void Dispose()
方法没有参数。protected virtual void Dispose(bool disposing)
方法有一个 bool
类型的参数。这两者是不同的方法,因此在同一个类中可以同时存在。
(5)托管资源与非托管资源
特性 | 托管资源 | 非托管资源 |
---|---|---|
管理方式 | 由垃圾回收器自动管理 | 需要显式管理,通常通过 Dispose 或析构函数处理 |
资源类型 | 常见例子:字符串 (string )、数组 (int[] )、集合 (List<> 、Dictionary<> ) | 常见例子:文件 (FileStream )、数据库连接 (SqlConnection )、图形对象 (Bitmap )、套接字 (Socket ) |
释放方式 | 无需显式释放 | 显式调用释放方法(Dispose ),如 stream.Dispose() |
风险 | 无需担心资源泄漏 | 未释放可能导致内存泄漏或句柄耗尽,影响系统稳定性 |
(6)释放系统资源的操作方式
// 释放托管资源
if (stream != null)
{
stream.Close();
stream = null;
}
if (client != null)
{
client.Close();
client = null;
}
stream.Close()
用于关闭 NetworkStream
对象,释放与其关联的系统资源,如缓冲区或文件描述符。client.Close()
用于关闭 TcpClient
对象,断开底层 TCP 连接,释放与之关联的系统资源(如套接字句柄)。置为 null
,是为了告诉垃圾回收器(GC),该对象已经不再需要,可以回收其内存,方便垃圾回收器清理无用的对象。